[RPi bring up] 给树莓派写一个bootloader!
像使用arduino一样给树莓派下载程序 | 袋鼠鱼子式的博客 本人擅长朴素贝叶斯科学算命, 大家没事可以多看我的书吃我的药听我的讲座http://blog.74ls74.org/2022/09/08/20220908_bootloader_raspberry_pi_uart_upload/
阅读本文您不需要掌握的知识有
高深的操作系统理论
高深的计算机体系结构理论
阅读本文您需要具备
全日制小学生学历及其同等学历 ★★★★★
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程序。
import sys
import serial
import time
program = open(sys.argv[2], "rb")
payload = program.read()
payloadsize = len(payload).to_bytes(4,byteorder="big")
ser = serial.Serial(sys.argv[1], 115200, timeout=3)
print(ser.name)
ser.reset_input_buffer()
ser.reset_output_buffer()
ser.write(payloadsize)
c = ser.read(4)
if payloadsize != c:
raise Exception("payloadsize received incorrect!")
print("total bytes to send: ", len(payload))
index = 0
step = 1000
for i in range(0, len(payload), step):
n = ser.write(payload[i:i+step])
#print(n)
c = ser.read(n)
if payload[i:i+n] != c:
raise Exception("payload received incorrect!")
index += n
#print(index)
c = ser.read()
if c == b"#":
print("total received bytes:", index)
ser.close()
program.close()
开头先发送4个byte组成的int整型做为头部,这是待传文件所包含的字节数,之后发送整个文件。第一个参数是串口的port的设备文件,第二个参数是要待传文件的文件名。
$ 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),
sdhci 0x20300000
sdhost 0x20202000
这两套外设的驱动在Linux kernel中是sdhci-iproc和sdhost。sd相关的资料很不完善,bcm2835的datasheet中仅有一章External Mass Media Controller,文中涉及的spec也无法从Arasan获取,只能从sd协会官方找到一些有用的东西https://www.sdcard.org/。
所以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
电源驱动,用来重启系统。
接下来我们终于可以着手bootloader的逻辑了。
// bootloader
// 亮灯
gpio_func_sel(16, 0b001);
gpio_output(16, 0);
// 等待5秒
for(int i=0; i<5; ++i)
{
systimer_sleep(1);
// 如果uart有数据
if(uart_dataready()) {
// 接收前4个byte
char c;
uint32_t size = 0;
for(int i=0; i<4; ++i)
{
c = uart_getc();
uart_send(c);
size = size << 8;
size = size + c;
}
// 写入内存
char* data = (char *)0x80000;
char* bp = data;
for(int s=0; s<size; ++s) {
*bp = uart_getc();
uart_send(*bp);
bp += 1;
}
uart_send('#');
// 写入sd卡kernel.img文件
FIL fdst;
FRESULT res = f_open(&fdst, "0:/KERNEL.IMG", FA_CREATE_ALWAYS | FA_WRITE);
if (res != FR_OK)
{
uart_puts("f_open failed\n");
return;
}
uint32_t sizewrite = 0;
res = f_write(&fdst, (void *)data, size, (unsigned int*)&sizewrite);
f_close(&fdst);
// 关闭led
gpio_output(16, 1);
// 重启
reset();
}
}
// 关闭led
gpio_output(16, 1);
uart_puts("no data from uart!\n\r");
// 正常启动树莓派
kernel_main();
5. experiment and sumary
将以上逻辑构建的kernel.img复制到sd卡上,将sd卡插到到树莓派上(再也不需要从树莓派拿下来sd卡了~)。
把串口线接到树莓派上,
在led灯亮起的5秒内,上位机运行upload.py,等待数秒,新的kernel.img就被上传到sd卡中去了!
6. reference
https://github.com/dwelch67/raspberrypi/
https://github.com/bztsrc/raspi3-tutorial/
https://github.com/GrassLab/osdi/
https://github.com/996refuse/emperorOS
本文相关代码均已上传到github,请使用git命令下载
$ git clone -b bootloader https://github.com/996refuse/emperorOS.git
$ cd emperorOS
$ make
$ cp kernel.img /path/to/sd/root/dir