为树莓派4编写裸机操作系统--part3

本文最后更新于:2022年3月14日 晚上

本文档所有内容均翻译自 https://github.com/isometimes/rpi4-osdev,作为自己学习树莓派及操作系统的记录。由于本人英文水平有限,如果出现翻译错误请指出,本人会及时改正。如果出现无法理解的内容,请参考原文学习。


为树莓派4编写操作系统 3 - helloworld

让我们做些什么

到目前为止,我们的操作系统只能产生黑屏。我们该如何确定我们的代码确实在运行?让我们做一些更有趣的事情来证明我们可以真正的控制硬件。

通常,软件开发者学习的第一件事情就是打印 “Hello world!” 到屏幕上。然而在裸机开发中,打印到屏幕可能是一个相当大的挑战。因此,我们将从一些简单的事情开始做起。

UART 简介

也许我们可以从我们的操作系统“发送消息”的最简单的方法是使用 UART 或串行通信电路。UART 代表通用异步接收器/发送器,它是一种非常古老且相当简单的接口,仅使用两条连线在两个设备之间通信。在 USB 出现之前,鼠标、打印机和调制解调器等设备都是以这种方式连接的。

我们将你的开发机直接连接到你的 RPi4 上,并上 RPi4 向你的开发机发送 “Hello world!” 消息。你的开发机会该消息打印到屏幕上。

你需要:

  • 一条 USB 到串口转换线
  • 下载并安装转换线的驱动程序
  • 在开发机上下载并安装 PuTTY
  • 如果你使用 Mac,建议你安装 Serial Tools 作为 PuTTY 的替代

如果你想在开始之前阅读更多串行通信的知识,我建议你浏览 SparkFun website

连接你的开发机和树莓派

如果你已经成功安装了驱动程序,请继续将转换线连接到开发机的备用 USB端口,打开控制面板,点击设备管理器并打开端口部分,你将看到一个 “Prolific” 项目。这告诉我们连接线工作正常。

这是我电脑上看到的样子:

记住 Prolific 项后括号中的 COMx 编号 - 对于我来说,它是 COM5。

相同的转换线可以在 Mac 上使用,无需安装任何驱动程序。

现在我们需要检查 RPi4 一端以确定如何连接转换线。你需要找到 GPIO 引脚,全部 40 个,它们就在 Raspberry Pi 版权声明的上方。

下图显示了你需要进行连接的位置。我推荐的转换线的不同颜色分线的含义如下:

  • 黑线 = 接地
  • 红线 = +5V 电源
  • 绿线 = TX (从 USB 端向 RPi4 端发送)
  • 白线 = RX (从 PRi4端向 USB 端接收)

接地线(在我的示例中是黑线)连接到 RPi4 的 6号引脚,RX 线(白线)连接到 TXD(GPIO14/引脚8),TX 线(绿线)连接到 RXD(GPIO15/引脚10)。请注意如何交叉连接 RX 和 TX。由于我们使用专用电源为 RPi4 供电,请确保不要在引脚上连接红线

这是我的 RPi4 正确连接好转换线后的样子:

3-helloworld-cable

设置 PuTTY

  • 在你的开发机上运行 PuTTY
  • 在左边窗口中的点击 “Session”
  • 将 “Connection type” 设置为 “Serial”
  • 将 “Serial line to connect to” 设置为我们在上面找到的 COMx 编号,我的是 COM5
  • 将 “Speed (baud)” 设置为 115200
  • 确保 “Data bits” 是 8,”Stop bits” 是 1,”Parity” 和 “Flow control” 是 None
  • 在左侧窗口点击返回到 “Connection type”,你应该会开到更改的设置
  • 为刚才的设置命名并保存这些设置,例如在 “Saved Sessions” 下的文本框中输入 “Raspberry Pi 4” 并点击保存
  • 你现在可以通过双击 “Raspberry Pi 4” 来启动连接 - 如果你这样做了,你将看到的只是一个空的黑色窗口

如果你使用不同的终端仿真器,则需要按照应用程序供应商提供的如何使用该软件的说明来完成与上述设置相同的设置。例如,此处讲解了 Mac 上的串行工具。https://www.w7ay.net/site/Applications/Serial%20Tools/

快速更改 config.txt

你还记得吗?在第一节的教程中,为了让我的 Raspbian 能够在我的电视屏幕上显示,我编辑了 SD卡上的 config.txt 文件。现在我们需要添加一行来确保我们的 UART 连接是可靠的。

UART 通信与时序有很大关系,重要的是两端就收发数据的确切速度达成一致。我们将 PuTTY设置为以波特率 115200 进行通信,因此我们需要 RPi4 设置为相同的速率。事实上,我们不能确定 RPi4 是否会按照该设定的速率通信 - 根据 CPU 的繁忙程度,本教程中 RPi4 的 UART 使用的 Mini UART 速率可能会更快或更慢。

将下面这行命令添加到 你的 config.txt 中,用于设置 PRi4 的最小运行速率,能够缓解上面提到的 UART 通信速率问题

1
core_freq_min = 500

在你的代码中操作 UART

首先,让我们更新 kernel.c 文件以使用一些新的函数调用

1
2
3
4
5
6
7
8
#include "io.h"

void main()
{
uart_init();
uart_writeText("Hello world!\n");
while (1);
}

我们首先包含了一个新的头文件 “io.h”。这允许我们在 kernel.c 文件之外编写新的代码,并在需要时调用它们

你注意到我们的 main() 函数中也新增加了几行代码。首先我们调用一个函数来初始化 UART,然后我们调用另一个函数来写入 “Hello world!” 字符串。字符串的末尾的奇怪字符 “ \n “ 用于换行,就像我们在文字处理器中按下 Enter 键一样。

现在我们在新建的 io.h 文件中增加以下内容:

1
2
void uart_init();
void uart_writeText(char *buffer);

这是一个非常简短的头文件,它有两个函数定义。就像 main() 函数的形式一样,uart_init() 是一个没有参数且返回 void 的函数。这意味着它不需要任何调用者传入的数据也不会向调用者返回任何数据。你会注意到 uart_writeTetx() 也是一个返回 void 的函数,但它需要一个参数,因为我们需要告诉它需要写入什么文本。

我们将这两个函数的具体实现代码放在另一个新文件 io.c 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// GPIO

enum {
PERIPHERAL_BASE = 0xFE000000,
GPFSEL0 = PERIPHERAL_BASE + 0x200000,
GPSET0 = PERIPHERAL_BASE + 0x20001C,
GPCLR0 = PERIPHERAL_BASE + 0x200028,
GPPUPPDN0 = PERIPHERAL_BASE + 0x2000E4
};

enum {
GPIO_MAX_PIN = 53,
GPIO_FUNCTION_ALT5 = 2,
};

enum {
Pull_None = 0,
};

void mmio_write(long reg, unsigned int val) { *(volatile unsigned int *)reg = val; }
unsigned int mmio_read(long reg) { return *(volatile unsigned int *)reg; }

unsigned int gpio_call(unsigned int pin_number, unsigned int value, unsigned int base, unsigned int field_size, unsigned int field_max) {
unsigned int field_mask = (1 << field_size) - 1;

if (pin_number > field_max) return 0;
if (value > field_mask) return 0;

unsigned int num_fields = 32 / field_size;
unsigned int reg = base + ((pin_number / num_fields) * 4);
unsigned int shift = (pin_number % num_fields) * field_size;

unsigned int curval = mmio_read(reg);
curval &= ~(field_mask << shift);
curval |= value << shift;
mmio_write(reg, curval);

return 1;
}

unsigned int gpio_set (unsigned int pin_number, unsigned int value) { return gpio_call(pin_number, value, GPSET0, 1, GPIO_MAX_PIN); }
unsigned int gpio_clear (unsigned int pin_number, unsigned int value) { return gpio_call(pin_number, value, GPCLR0, 1, GPIO_MAX_PIN); }
unsigned int gpio_pull (unsigned int pin_number, unsigned int value) { return gpio_call(pin_number, value, GPPUPPDN0, 2, GPIO_MAX_PIN); }
unsigned int gpio_function(unsigned int pin_number, unsigned int value) { return gpio_call(pin_number, value, GPFSEL0, 3, GPIO_MAX_PIN); }

void gpio_useAsAlt5(unsigned int pin_number) {
gpio_pull(pin_number, Pull_None);
gpio_function(pin_number, GPIO_FUNCTION_ALT5);
}

// UART

enum {
AUX_BASE = PERIPHERAL_BASE + 0x215000,
AUX_ENABLES = AUX_BASE + 4,
AUX_MU_IO_REG = AUX_BASE + 64,
AUX_MU_IER_REG = AUX_BASE + 68,
AUX_MU_IIR_REG = AUX_BASE + 72,
AUX_MU_LCR_REG = AUX_BASE + 76,
AUX_MU_MCR_REG = AUX_BASE + 80,
AUX_MU_LSR_REG = AUX_BASE + 84,
AUX_MU_CNTL_REG = AUX_BASE + 96,
AUX_MU_BAUD_REG = AUX_BASE + 104,
AUX_UART_CLOCK = 500000000,
UART_MAX_QUEUE = 16 * 1024
};

#define AUX_MU_BAUD(baud) ((AUX_UART_CLOCK/(baud*8))-1)

void uart_init() {
mmio_write(AUX_ENABLES, 1); //enable UART1
mmio_write(AUX_MU_IER_REG, 0);
mmio_write(AUX_MU_CNTL_REG, 0);
mmio_write(AUX_MU_LCR_REG, 3); //8 bits
mmio_write(AUX_MU_MCR_REG, 0);
mmio_write(AUX_MU_IER_REG, 0);
mmio_write(AUX_MU_IIR_REG, 0xC6); //disable interrupts
mmio_write(AUX_MU_BAUD_REG, AUX_MU_BAUD(115200));
gpio_useAsAlt5(14);
gpio_useAsAlt5(15);
mmio_write(AUX_MU_CNTL_REG, 3); //enable RX/TX
}

unsigned int uart_isWriteByteReady() { return mmio_read(AUX_MU_LSR_REG) & 0x20; }

void uart_writeByteBlockingActual(unsigned char ch) {
while (!uart_isWriteByteReady());
mmio_write(AUX_MU_IO_REG, (unsigned int)ch);
}

void uart_writeText(char *buffer) {
while (*buffer) {
if (*buffer == '\n') uart_writeByteBlockingActual('\r');
uart_writeByteBlockingActual(*buffer++);
}
}

你已经看到我们在 io.h 头文件中定义的两个函数现在有了一些具体的代码,以及一些辅助函数。我将在下一节的教程中解释这段代码中发生了什么,但现在让我们直接跳过这一步。

有了新的 io.c 和 io.h 文件,以及对 kernel.c 所做的更改。运行 make 命令来构建新的 OS。

然后:

  • 将新构建的 kernel8.img 镜像复制到 SD 卡,然后将SD卡插入你的 RPi4
  • 确保你的 USB 转串行线连接正确
  • 运行你的终端模拟器(例如 PuTTY )并连接到你之间设置的 “Raspberry Pi 4” 会话中 - 你将看到一个空白的黑屏并且没有错误提示
  • 启动你的 RPi4

如果你已经按照本教程所有的说明进行操作,几秒钟之后你将看到 “Hello World!” 出现在你开发机的终端仿真窗口中。

这个消息来自你的 RPi4,它表明你的 OS 运行正常。终于实现了!


为树莓派4编写裸机操作系统--part3
http://yoursite.com/2022/03/13/为树莓派4编写裸机操作系统-part3/
作者
BinGoo
发布于
2022年3月13日
许可协议