欢迎光临散文网 会员登陆 & 注册

06-对 part4-miniuart 的搬运和翻译

2023-08-11 17:40 作者:Xdz-2333  | 我要投稿

非黑色字体均为我自己添加,原文放在末尾

地址映射 IO

        我们拥有并且运行了"Hello world"的例子.让我们来花点时间解释一下 io.c 中用到的,把消息通过UART传送到我们的开发设备上的概念.

        我们从UART开始的原因是 -- 它是一个(相对)简单的硬件,因为它使用内存映射I/O (MMIO).这意味着我们可以通过对树莓派上一组预先确定好的地址进行读写来与硬件进行沟通.我们可以对不同的地址进行写入来影响树莓派不同的行为.

        对于内存地址映射的补充:

        CPU实际上只能做三件事:读数据,运算,写数据,因此CPU想要控制其他的部分要么有专用的指令,要么向特定的地址写入或者读取数据.如果读者们读过CSAPP或者对计算机中"抽象"这个概念有所了解,那么应该就能很好的理解这中方式.各种外围部件隐藏自己的实现细节,只对CPU以寄存器的设置暴露接口,降低系统耦合性,方便操作系统移植等.

        这些内存地址从 0xFE000000 (我们的 PERIPHERAL_BASE )

        注意:你可能好奇为什么基地址与树莓派官网文档里面的不同(https://datasheets.raspberrypi.com/bcm2711/bcm2711-peripherals.pdf).这是因为树莓派启动时默认进入 Low Peripheral Mode.这将把外设映射到内存的最后64mb,因此它们能"被arm 从 0xFEnn_nnnn看见".(mini UART 在树莓派的手册里也有,和UART不是一个东西)

        人们可能希望启动 High Peripheral Mode (完全的 35-bit 地址映射)从而避免"丢失"内存的最后64mb地址.这有许多副作用,然而,做到这点需要对内核进行重构(即使是在这个简单的教程中)来使其工作.

设置GPIO(通用输入/输出)引脚

        GPIO引脚(记得吗 -- 我们的USB转TTL就用的它)使用了MMIO. io.c 顶部的那节(用 //GPIO 做的标记) 实现了一些函数来对这些引脚进行设置.

        在这一点上,我推荐对树莓派官方手册进行深挖(见上方的链接).它有详细的GPIO的部分.只是不要相信你读到的所有内容,因为目前这份文档中有很多错误.

        然而它会告诉你我们的内存映射到的GPIO寄存器都做了什么,比如 GPFSEL0 ,GPSET0,GPCRL0和 GPPUPPDN0 .这些都是从 PERIPHERAL_BASE 开始的已知的偏移量,在第一个enum中定义.

        mmio_read和mmio_write两个函数可以用来从这些寄存器中读写数据.

关于GPIO引脚

        记得我们说过计算机使用0和1通信吗?我们想做的其中一件事就是设置一个引脚为高(二进制的 1 ),或者清空一个引脚(二进制的 0 ).gpio_set和gpio_clear这两个函数就是做这个的.相应的引脚会接收一个高电平,当它被设置为高,反之当它设置为低.

        然而,引脚可以处于三种pull 状态之一.这将告诉树莓派这个引脚的默认状态.如果一个引脚被设定为"上拉",然后它的静止状态就是高电平(接受电压)除非另有说明.如果它被设定为"下拉",然后它的静止状态就是低电平.这在连接不同的设备中很有用.如果一个引脚被设置为 "Pull None",我们就说它被"悬空"了,这就是我们的UART所需要的.gpio_pull函数设定给定引脚的状态.

        关于GPIO的一些补充:

        如果读者学过数电或者模电,应该不难理解这里的引脚状态,然而还是需要进行一下补充,实际引脚上除了上拉,下拉,浮空,还有高阻态,推挽输出,开漏等状态,这些对应了不同的晶体管和电阻电容等的连接以及设置.这里的引脚抽象概念比晶体管,电阻等电路更高一级.它隐藏了0和1物理实现,并使得逻辑上的0和1有可靠的物理保证.

        然而为了顺利使用引脚进行输入输出,我们还是需要一些关于各种状态的知识.

        首先是引脚是有极限的,一般情况下引脚工作电流都是uA量级,特殊的引脚有mA量级,这就意味这如果引脚如果使用不当(举个极端例子,给空调供电),要么引脚会烧掉,要么无法达到要求的电平.引脚的耐压值也是有限的,有的只有3.3V耐压,而有的有5V.

        其次在这里对这些状态做个简单解释,上拉就是输出高电平,下拉就是输出低电平,浮空取决于引脚连线上电压,一般用于输入,高阻态就是不导通,推挽输出一般用于PWM驱动等,开漏的高电平取决于连线(如果引脚耐压值为5V,而上拉电平为3.3V,改变状态为开漏输出后再外接合适的电阻,就可以把高电平调为5V).拉电流是指电流从晶体管往外流,灌电流是指外面电流流向晶体管.

        关于GPIO的你需要知道的更多的一些事:

  1. 树莓派的可以容纳比现有的硬件引脚更多的函数

  2. 为了解决这个问题,我们可以将引脚动态映射到函数上

  3. 在我们的例子中,我们希望GPIO 14 和GPIO 15 采用函数5 (分别为TXD1和RXD1)

  4. 这将把树莓派的miniUART映射到我们连接了线的引脚上

  5. 我们调用gpio_function 来设置这些

        现在io.c的GPIO 部分应该已经清楚了.我们继续.

设置UART

        io.c的第二节(由UART标记)实现了一些函数来帮助我们和UART交流.这个装置同样使用MMIO,并且你可以再次看到寄存器设置,就像你第一次看到的那样.去看树莓派的手册(见上方链接)来了解寄存器更详细的解释.

        我只是想调用AUX_UART_CLOCK参数,我们将其设置为500000000。还记得我说过UART通信是关于时间的吗?好吧,这与我们在config.txt中添加core_freq_min=500行时设置的时钟速度(500 MHz)完全相同。这不是巧合!

        这里应该是采用轮询方法,所以要设置CPU速率,一般访问外设的方法有三种,轮询,中断,DMA,具体可自行百度.

        你会注意到uart_init()函数中其他熟悉的数值,我们直接在kernel.c的main()例程中调用它们.我们设置波特率为115200,数据位为8

        最后我们加上了这些有用的函数:

  • uart_isWriteByteReady -- 检查串口状态保证我们已经"准备好发送"

  • uart_writeByteBlockingActual -- 等待直到我们 "准备好发送" 然后发送一个字符

  • uart_writeText -- 使用 uart_writeByteBlockingActual 发送字符

        你应该记得 uart_writeText 是我们从main() 中调用来打印 "Hello World"的函数!

一些其他代码

        我不希望这个教程只是一个简单的解释,所以在代码中,你会看见我往io.c中添加了更多功能,并且在内核中使用.通读看看你是否能理解发生了什么,如果你需要,去翻文档.

        现在我们可以从串口中读数据了.如果你编译这个内核并且像之前那样启动树莓派,它将再次打印出Hello World.但是,在此之后你可以像终端中输入然后让树莓派发回给你.

        现在我们可以进行双向通信了.

        我们同样为我们的串口通信实现了一个软件 FIFO 缓冲(https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics)).树莓派只给了到达串口的数据有限的缓冲空间,合并到我们自己的缓冲区可以对将要到来的数据进行更好的管理.

        

Writing a "bare metal" operating system for Raspberry Pi 4 (Part 4)


Memory-Mapped I/O

We have our "Hello world!" example up and running. Let's just take a little time to explain the concepts that _io.c_ is using to send this message over the UART to our dev machine.


We started with the UART for a reason - it's a (relatively) simple piece of hardware to talk to because it uses **memory-mapped I/O** (MMIO). That means we can talk directly to the hardware by reading from and writing to a set of predetermined memory addresses on the RPi4. We can write to different addresses to influence the hardware's behaviour in different ways.


These memory addresses start at `0xFE000000` (our `PERIPHERAL_BASE`).


Note: you might wonder why this base address differs from the one shown throughout the [BCM2711 ARM Peripherals document](https://www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2711/rpi_DATA_2711_1p0.pdf). It's because the RPi4 boots into Low Peripheral Mode by default. This maps the peripherals over the last 64mb of RAM, therefore they're "visible to the ARM at 0x0_FEnn_nnnn".


People might wish to enable High Peripheral mode (full 35-bit address map) so as to avoid "losing" that last 64mb of RAM. There are various side effects, however, of doing this and it would require some refactoring of the kernel (even in this simple tutorial) to make it work.


Configuring the GPIO (General Purpose Input/Output) pins

The GPIO pins (remember - we connected our USB to serial TTL cable to these) use MMIO. The top section of _io.c_ (marked with `// GPIO`) implements a few functions to configure these pins.


At this point, I recommend digging into the [BCM2711 ARM Peripherals document](https://www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2711/rpi_DATA_2711_1p0.pdf). It has a very detailed section on GPIO. Just don't believe everything you read as there are plenty of mistakes in this document at the moment.


It will, however, tell you what our memory-mapped GPIO **registers** like `GPFSEL0`, `GPSET0`, `GPCLR0` and `GPPUPPDN0` do. These are all at known offsets from the `PERIPHERAL_BASE` and are defined by our first `enum`.


The two functions `mmio_read` and `mmio_write` can be used to read a value from and write a value to these registers.


About the GPIO pins

Remember how we said that computers communicate in 1's and 0's? One thing we might want to do is to **set a pin** high (binary 1) or **clear a pin** low (binary 0). The two functions `gpio_set` and `gpio_clear` do just this. The corresponding hardware pin will receive a voltage when it is set high, and not when it cleared low. 


That said, however, pins can also have one of three **pull states**. This tells the RPi4 what the default state of a pin is. If a pin is set to "Pull Up", then its resting state is high (receiving voltage) unless it's told otherwise. If a pin is set to "Pull Down", then its resting state is low. This can be useful for connecting different types of devices. If a pin is set to "Pull None" then it is said to be "floating", and this is what our UART needs. The `gpio_pull` function sets the pull state of a given pin for us.


You need to know just a few more things about the GPIO pins:


 * The RPi4 is capable of more functions than there are hardware pins available for

 * To solve this, our code can dynamically map a pin to a function

 * In our case, we want GPIO 14 and GPIO 15 to take alternate function 5 (TXD1 and RXD1 respectively)

 * This maps the RPi4's mini UART (UART1) to the pins we connected our cable to!

 * We use the `gpio_function` call to set this up


Now the GPIO section of _io.c_ should be clear. Let's move on.


Configuring the UART

The second section of _io.c_ (marked with `// UART`) implements a few functions to help us talk to the UART. Thankfully, this device also uses MMIO, and you'll see the registers set up in the first `enum` just like you saw before. Look in the [BCM2711 ARM Peripherals document](https://www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2711/rpi_DATA_2711_1p0.pdf) for a more detailed explanation of these registers.


I do just want to call out the `AUX_UART_CLOCK` parameter, which we set to `500000000`. Remember how I said that UART communication is all about timing? Well, this is exactly the same clock speed (500 MHz) that we set in _config.txt_ when we added the `core_freq_min=500` line. This is no coincidence!


You'll also note some other familiar numbers in the `uart_init()` function, which we call directly from our `main()` routine in _kernel.c_. We set the baud rate to `115200`, and the number of bits to `8`.


Finally we add some useful functions:


 * `uart_isWriteByteReady` - checks the UART line status to ensure we are "ready to send"

 * `uart_writeByteBlockingActual` - waits until we are "ready to send" and then sends a single character

 * `uart_writeText` - sends a whole string using `uart_writeByteBlockingActual` 


You'll remember that `uart_writeText` is what we call from `main()` to print "Hello world!".


Some extra code

I don't want this tutorial to just be an explanation so, in the code, you'll see I've added some more functionality to _io.c_ and made use of it in our kernel. Have a read through and see if you can understand what's going on. Refer to the documentation again if you need to.


We can now read from our UART too! If you build the kernel and power on the RPi4 just like before, it'll say hello to the world again. But, after that, you can type into the terminal emulator window and the RPi4 sends the characters right back to you.


_Now we're communicating in two directions!_


We also implemented a software [FIFO buffer](https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics)) for our UART communication. The RPi4 has limited buffer space for data arriving on the UART, and incorporating our own is likely to make it easier to manage incoming data in future.



06-对 part4-miniuart 的搬运和翻译的评论 (共 条)

分享到微博请遵守国家法律