Changchun Master Li

[RPi bring up] 给树莓派写一个bootloader!
像使用arduino一样给树莓派下载程序

2022-09-08

阅读本文您不需要掌握的知识有

  • 高深的操作系统理论
  • 高深的计算机体系结构理论

阅读本文您需要具备

  • 全日制小学生学历及其同等学历 ★★★★★
  • 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进行更新调试。
这是我的工作流程

  1. 树莓派断电关机
  2. 从树莓派取出sd卡
  3. 把sd卡插到我的开发机上
  4. 挂载sd卡的文件系统
  5. 编写,修改程序,build出新的kernel.img
  6. 将kernel.img复制到sd卡上
  7. 卸载sd卡的文件系统
  8. 从我的开发机上取出sd卡
  9. 把sd卡插到树莓派上
  10. 树莓派上电开机

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工作流程

左侧为启动顺序,右侧为bootloader工作流程

4. implement

A. 上位机

我们需要usb-to-ttl uart串口转接线,ch340/pl2303/ft232芯片都可以。PC的操作系统生态已经非常完善,不管Linux、Windows还是MacOS,都可以很容易找到相关的驱动,安装即可。
上位机的实现很简单,这里我通过python的pyserial操作串口写了一个upload.py程序。

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
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的设备文件,第二个参数是要待传文件的文件名。

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
2
sdhci 0x20300000
sdhost 0x20202000

这两套外设的驱动在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
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
接下来我们终于可以着手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卡上(build具体过程可以参考之前的文章),将sd卡插到到树莓派上(再也不需要从树莓派拿下来sd卡了~)。
把串口线接到树莓派上,在led灯亮起的5秒内,上位机运行upload.py,等待数秒,新的kernel.img就被上传到sd卡中去了!

6. future work

本文实现了一个非常简单的demo,验证了用arduino的方式使用树莓派的可行性。未来的工作可以移植更多的arduino库,bringup起来uart、iic、spi、pwm等外设。仿照RasPiArduinoarduino-pico的实现,对老款bcm2836树莓派提供支持。将开发工具集成到arduino IDE中。(如果你有兴趣可以联系我)

7. reference

dwelch67
raspi3-tutorial
GrassLab
emperorOS

本文相关代码均已上传到github,请使用git命令下载

1
2
3
4
$ git clone -b bootloader https://github.com/996refuse/emperorOS.git
$ cd emperorOS
$ make
$ cp kernel.img /path/to/sd/root/dir
使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章