为树莓派4编写裸机操作系统--part4
本文最后更新于:2022年3月14日 晚上
本文档所有内容均翻译自 https://github.com/isometimes/rpi4-osdev,作为自己学习树莓派及操作系统的记录。由于本人英文水平有限,如果出现翻译错误请指出,本人会及时改正。如果出现无法理解的内容,请参考原文学习。
为树莓派4编写操作系统 4 - miniuart
内存映射 I/O
我们已经让我们的 “Hello world!” 示例程序成功启动并运行。让我们花一点时间来解释一下 io.c 文件,它成功实现使用 UART 向我们的开发机发送消息的功能。
我们从 UART开始的一个原因是 - 它是一个相对简单的硬件,因为它使用内存映射 I/O(MMIO)技术。这意味着我们可以通过读取和写入 RPi4 上一组预先设定的内存地址实现与硬件的交流。我们可以写入不同的地址以不同的方式来影响硬件的行为。
这些内存地址从 0xFE0000(我们的 PERIPHERAL_BASE)开始。
配置 GPIO (GENERAL Purpose Input/Output) 引脚
GPIO 引脚 (请记住 - 我们使用转换线连接这些引脚) 使用 MMIO。io.c 文件的开始(标有 // GPIO)实现了一些函数来配置这些引脚。
在此,我建议你深入研究 BCM2711 ARM Peripherals document 文档。它有一节关于 GPIO 的非常详细的内容。只是不要相信你阅读到的所有内容,因为该文档中可能有很多错误。
文档将告诉你我们使用的内存映射 GPIO 寄存器(如 GPFSEL0、GPSET0、GPCLR0 和 GPPUPPDN0)的具体作用。这些寄存器都与 PERIPHERAL_BASE 地址保持一定的偏移,该地址在我们的第一个枚举变量中定义。
mmio_read 和 mmio_write 两个函数分别用于从上述寄存器中读取值并将值写入这些寄存器中。
关于 GPIO 引脚
还记得我们说过计算机是以 0 和 1 进行通信的吗?我们可能想做的一件事就是将引脚设置为高电平(二进制 1)或将引脚清除为低电平(二进制 0)。gpio_set 和 gpio_clear 就是实现这两个功能的函数。相应的引脚在设置为高电平时会接入电压,而在清除为低电平时则不会。
除此之外,引脚也可能出于三种拉动状态之一。这告诉 RPi4 引脚的默认状态是什么。如果引脚被设置为 “上拉”,则除非另有说明,否则其默认状态是高电平。如果引脚被设置为“下拉”,则其默认状态是低电平。这对于连接不同类型的设备很有用。如果某个引脚被设置为“空拉”,则称其为“浮动”,这是我们的 UART 需要的。gpio_pull 函数为我们设置给定引脚的拉动状态。
你只需要了解有关 GPIO 引脚的以下信息:
- RPi4 的功能比可用的硬件引脚更多
- 为了解决上述问题,我们的代码可以将引脚动态的映射到不同的功能
- 在我们的实例中,我们希望 GPIO14 和 GPIO 15采用可选功能5(分别为 TXD1 和 RXD1)
- 这会将 RPi4 的 mini UART 功能映射到我们的转换线连接的 GPIO 引脚
- 我们调用 gpio_function 函数来实现
现在 io.c 文件中的 GPIO 部分应该很清楚了。让我们继续前进。
配置 UART
io.c 文件的第二部分(标有 // UART)实现了一些函数来帮助我们使用 UART。幸运的是,这个设备也使用了 MMIO,你会看到在第一个枚举变量中设置的寄存器,就像你在前面看到的那样,查看 BCM2711 ARM Peripherals document 外设文档以获取有关这些寄存器更详细的说明。
我只想强调 AUX_UART_CLOCK 参数,我们将其设置为 500000000。还记得前面我们说过 UART 通信是和时序相关的。这个值与我们在上一节中添加到 config.txt 文件中的 core_freq_min=500 配置表示的运行时钟 500Mhz 完全相同。这绝非巧合。
你还应该注意到 uart_init() 函数中其他一些熟悉的数字,我们在 kernel.c 文件的 main() 函数中调用它。在这个函数中,我们将波特率设置为 115200,数据位设置为 8 位。
最后编写一些对我们有用的辅助函数:
- uart_isWriteByteReady - 检查 UART 线路状态寄存器以确保我们“准备好发送”
- uart_writeByteBlockingActual - 等待我们“准备好发送”,然后发送单个字符
- uart_writeText - 调用 uart_writeByteBlockingActual 函数发送整个字符串
你应该记得我们是在 main() 函数中调用 uart_writeText 发送 “Hello world!” 字符串。
一些额外的代码
我不希望本教程只是代码实现的解释,因此,在代码中你会看到我已经向 io.c 文件中添加了更多功能并在我们的内核实现中调用它们。通读一遍代码,看看你能否理解这些代码的作用。如果需要,请再次查阅技术文档。
我们现在可以从 UART 中读取数据了!如果你像以前那样构建内核并启动 RPi4, 它会显示 “Hello world!”。而且,你可以在终端仿真器窗口中输入字符串,RPi4 会将你输入的字符串回显到你的开发机上。
现在我们可以在两个方向上进行通信!
我们还为我们的 UART 通信实现了一个软件 FIFO 缓冲区。RPi4 对于到达 UART 的数据的缓冲空间是有限的,但是使用我们自己的缓冲区可能在将来使我们更容易管理接收到的数据。