阅读本文您不需要掌握的知识有
- 高深的操作系统理论
- 高深的计算机体系结构理论
阅读本文您需要具备
- 全日制小学生学历及其同等学历 ★★★★★
- GNU工具链(make/GCC/LD) ★★☆☆☆
- ARM汇编语言 ★☆☆☆☆
- C语言 ★★★★☆
- Python ★☆☆☆☆
0. keyword
raspberry pi 1 bcm2835 armv6 bootloader embedded operating systems OS uart fat32 sd driver low level 底层开发 树莓派 裸机 C语言 arm汇编
1. requirement
在平时对树莓派的底层系统开发时,我需要频繁的替换kernel.img来对kernel进行更新调试。
这是我的工作流程
- 树莓派断电关机
- 从树莓派取出sd卡
- 把sd卡插到我的开发机上
- 挂载sd卡的文件系统
- 编写,修改程序,build出新的kernel.img
- 将kernel.img复制到sd卡上
- 卸载sd卡的文件系统
- 从我的开发机上取出sd卡
- 把sd卡插到树莓派上
- 树莓派上电开机
DUMMY!这个过程非常的枯燥麻烦,sd卡插不紧还会接触不良,有时候为了一个很小的改动要折腾半天。长期的插拔sd卡,也会显著降低sd slot的使用寿命,我有好几个树莓派的损坏原因只是sd slot接触不良。
这让我联想到使用Arduino的时候,只需要点击upload就可以上传程序,非常方便。后来Arduino的风靡流行,一定程度上也和它的易用性离不了关系。
那么它是什么原理呢,这就要归功与Arduino的bootloader。
bootloader即引导加载程序,是计算机复位后最开始运行的一段小程序,通常用来加载和启动操作系统内核。嵌入式设备通常小而精密,手机、路由器以及Arduino开发板,它们的bootloader不仅可以引导操作系统,通常也拥有刷机的功能。
仿照Arduino的思路,我的解决方案是在kernel.img的开头实现一个bootloader:在树莓派启动的前3秒,它监听串口是否有数据要传输。此时在开发机运行upload程序,向串口发送数据,树莓派则开始接收数据,并将数据保存到sd卡上第一个fat32分区的根目录,覆盖掉原有的kernel.img。这一系列工作完成后reset重启系统。整个过程不需取下sd卡即可完成系统的更新。
2. rpi bootloader
对于大部分计算机系统来说,FSBL(第一阶段bootloader)位于非易失性存储中,由各个硬件vendor来实现。它会加载一个image到内存(这个image可以是第二阶段bootloader,例如grub、lilo等,也可以是kernel),并将CPU的控制权移交。
对于树莓派来说,FSBL已经由VideoCore内置firmware实现,上电后固定的第一件事就是在第一个fat32文件系统分区下的根目录搜索kernel.img文件,然后将kernel.img load到0x8000地址起始的内存中,并将pc指针指向0x8000,将CPU的控制权交出,执行kernel.img的内容。
3. the constructure and workflow of the bootloader
左侧为启动顺序,右侧为bootloader工作流程
4. implement
A. 上位机
我们需要usb-to-ttl uart串口转接线,ch340/pl2303/ft232芯片都可以。PC的操作系统生态已经非常完善,不管Linux、Windows还是MacOS,都可以很容易找到相关的驱动,安装即可。
上位机的实现很简单,这里我通过python的pyserial操作串口写了一个upload.py程序。
1 | import sys |
开头先发送4个byte组成的int整型做为头部,这是待传文件所包含的字节数,之后发送整个文件。第一个参数是串口的port的设备文件,第二个参数是要待传文件的文件名。
1 | $ python upload.py /dev/ttyUSB0 path/to/kernel.img |
B. 下位机
下位机的实现相对复杂,首先我们要完成下面5个外设的驱动
1). gpio
通用IO驱动,可以控制某一个IO引脚的行为,这里我们需要通过亮灯来指示bootloader当前的运行状态。
我们首先通过操作GPIO_GPFSEL寄存器将GPIO设置为output功能,
然后写GPIO_GPSET/GPIO_GPCLR对应的bit。
这里不再赘述,可以参考我之前的博客
http://blog.74ls74.org/2022/06/18/20220618_hello_world_raspberry_pi_led_blink/
2). uart
串口驱动,用于和上位机通信,收发数据。
收发数据都是通过寄存器AUX_MU_IO,这是一个可读可写的寄存器。
寄存器AUX_MU_LSR[1]表示读fifo里已经有数据,可以读下一个;
寄存器AUX_MU_LSR[6]表示写fifo里的数据已经满了,需要等待外设完成发送。
3). sd和fat32文件系统
树莓派提供两套sd的外设都可以与sd卡交互(https://gist.github.com/eggman/40612fdeb6d081a9a7d1a63ddef647f1)
1 | sdhci 0x20300000 |
这两套外设的驱动在Linux kernel中是sdhci-iproc和sdhost。sd相关的资料很不完善,bcm2835的datasheet中仅有一章External Mass Media Controller,文中涉及的spec也无法从Arasan获取,只能从sd协会官网找到一些有用的东西。
所以sd驱动除了从Linux Kernel扒过来,只能从网上搜一些民间实现,这里列出我找到的一些比较可靠的来源
https://github.com/jncronin/rpi-boot/blob/master/emmc.c
https://github.com/bztsrc/raspi3-tutorial/blob/master/0B_readsector/sd.c
https://github.com/GrassLab/osdi/blob/master/supplement/sdhost.c
这里推荐第三个,来自台湾的国立交通大学的GrassLab,这个sdhost实现对其他模块没有依赖和耦合,使用起来更方便。
有了sd驱动,我们就可以对LBA(逻辑区块地址)所指向的block进行读写操作。但还是不能方便的读写文件,还需要在block驱动之上套一个fat32的库才可以。所幸fat32有很多开源实现,FatFs(http://elm-chan.org/fsw/ff/00index_e.html)代码质量很高,ANSI C兼容。移植非常方便,只需要在fatfs/diskio.c的接口函数中填入对应的block驱动。
4). timer
bcm2835有三套计时器,分别是system timer、arm timer和watchdog。
这里只用到system timer的SYSTIMER_CNT寄存器,SYSTIMER_CNT是一个64位计数器,频率大概1MHz,每自增1000000大概为一秒。
5). power
电源驱动,用来重启系统。
C. bootloader实现
1 | 接下来我们终于可以着手bootloader的逻辑了。 |
5. experiment and sumary
将以上逻辑构建的kernel.img复制到sd卡上(build具体过程可以参考之前的文章),将sd卡插到到树莓派上(再也不需要从树莓派拿下来sd卡了~)。
把串口线接到树莓派上,在led灯亮起的5秒内,上位机运行upload.py,等待数秒,新的kernel.img就被上传到sd卡中去了!
6. future work
本文实现了一个非常简单的demo,验证了用arduino的方式使用树莓派的可行性。未来的工作可以移植更多的arduino库,bringup起来uart、iic、spi、pwm等外设。仿照RasPiArduino和arduino-pico的实现,对老款bcm2836树莓派提供支持。将开发工具集成到arduino IDE中。(如果你有兴趣可以联系我)
7. reference
dwelch67
raspi3-tutorial
GrassLab
emperorOS
本文相关代码均已上传到github,请使用git命令下载
1 | $ git clone -b bootloader https://github.com/996refuse/emperorOS.git |
若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏
扫描二维码,分享此文章