先进操作系统理论
高级计算机体系结构理论
要阅读本文,您需要具备
全日制小学教育及同等学历 ★★★★★
GNU工具链(make/GCC/LD) ★★☆☆☆
ARM汇编语言★☆☆☆☆
C语言★★★★☆
蟒蛇★☆☆☆☆
0.关键字
树莓派 1 bcm2835 armv6 bootloader 嵌入式操作系统 OS uart fat32 sd 驱动 底层底层开发 树莓派裸机 C语言arm汇编
1. 要求
在开发树莓派底层系统时,需要经常更换kernel.img来更新和调试内核。
这是我的工作流程
将树莓派断电,从树莓派中取出SD卡,将SD卡插入我的开发机,挂载SD卡的文件系统,写入文件系统,修改程序,构建新的kernel.img,并将kernel.img复制到SD卡上。 卸载SD卡的文件系统。 从我的开发机中取出 SD 卡并将 SD 卡插入 Raspberry Pi。 打开树莓派电源。
假的! 这个过程是非常无聊和麻烦的。 SD卡插不紧,接触不良。 有时,一个小小的改变需要很长时间。 长期插拔SD卡也会明显缩短SD插槽的使用寿命。 我有几个 Raspberry Pi 仅因 SD 插槽接触不良而损坏。
这让我想起在使用Arduino时,只需点击upload即可上传程序,非常方便。 后来Arduino的流行一定程度上和它的易用性是分不开的。
那么原理是什么呢? 这是由于 Arduino 引导加载程序造成的。
Bootloader是计算机重置后开始运行的一个小程序。 它通常用于加载和启动操作系统内核。 嵌入式设备通常体积小、结构复杂,例如手机、路由器、Arduino 开发板等。 他们的bootloader不仅可以引导操作系统,通常还具有刷机的功能。
遵循Arduino的思路,我的解决方案是在kernel.img开头实现一个bootloader:在树莓派启动前3秒,监听串口是否有数据要传输。 此时,在开发机上运行上传程序,并向串口发送数据。 树莓派开始接收数据,并将数据保存到SD卡第一个fat32分区的根目录下,覆盖原来的kernel.img。 这一系列工作完成后,重置并重新启动系统。 整个过程无需移除SD卡即可完成系统更新。
2.rpi引导加载程序
对于大多数计算机系统来说,FSBL(第一阶段引导加载程序)位于非易失性存储中,由各个硬件供应商实现。 它将加载一个镜像到内存中(这个镜像可以是第二阶段引导加载程序,如grub、lilo等,也可以是内核)并转移CPU的控制权。
对于 Raspberry Pi,FSBL 已由 VideoCore 内置固件实现。 上电后首先要修复的是在第一个fat32文件系统分区的根目录下搜索kernel.img文件,然后将kernel.img加载到地址0x8000开始的内存中,将pc指针指向0x8000 ,交出CPU的控制权,执行kernel.img的内容。
3. Bootloader的构建及工作流程
左侧是启动序列,右侧是引导加载程序工作流程 4. 实现A。 主机
我们需要usb转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个字节组成的int作为头,即要传输的文件包含的字节数,然后发送整个文件。 第一个参数是串口端口的设备文件,第二个参数是要传输的文件的文件名。
$ python upload.py /dev/ttyUSB0 path/to/kernel.img
B、下位机
下位机的实现相对复杂。 首先,我们需要完成以下五个外设的驱动。
1)。 GPIO
通用IO驱动程序可以控制某个IO引脚的行为。 这里我们需要通过点亮灯来指示Bootloader当前的运行状态。
我们首先通过操作GPIO_GPFSEL寄存器将GPIO设置为输出功能,
然后写入GPIO_GPSET/GPIO_GPCLR对应的位。
这里就不详细说了,可以参考我之前的博客
/2022/06/18/20220618_hello_world_raspberry_pi_led_blink/
2)。 串口
串口驱动程序用于与上位机通信并发送和接收数据。
数据的发送和接收是通过寄存器AUX_MU_IO来进行的,该寄存器是一个可读写的寄存器。
寄存器AUX_MU_LSR[1]表示读fifo中已有数据,可以读取下一个;
寄存器AUX_MU_LSR[6]表示写fifo中的数据已满,需要等待外设完成发送。
3)。 sd 和 fat32 文件系统
Raspberry Pi 提供了两组可以与 SD 卡交互的 SD 外设(/eggman/40612fdeb6d081a9a7d1a63ddef647f1)。
sdhci 0x20300000
SD主机0x20202000
这两套外设的驱动是Linux内核中的sdhci-iproc和sdhost。 关于sd的资料很不完整。 bcm2835的数据表中只有一章关于外部媒体控制器。 文章中涉及的规格无法从 Arasan 获得。 我们只能从sd协会官方/上找到一些有用的东西。
因此,除了从Linux Kernel中提取sd驱动之外,我只能从网上搜索一些私有的实现。 以下是我找到的一些更可靠的来源。
/jncronin/rpi-boot/blob/master/emmc.c
/bztsrc/raspi3-tutorial/blob/master/0B_readsector/sd.c
/GrassLab/osdi/blob/master/supplement/sdhost.c
这里推荐的第三个是台湾交通大学的GrassLab。 这种sdhost实现对其他模块没有依赖和耦合,使用起来更加方便。
有了sd驱动,我们就可以读写LBA(逻辑块地址)指向的块了。 但仍然无法方便地读写文件,需要在块驱动之上安装一个fat32库。 幸运的是,fat32 有很多开源实现。 FatFs (/fsw/ff/00index_e.html) 具有较高的代码质量并且兼容 ANSI C。 移植非常方便。 只需要在fatfs/diskio.c的接口函数中填写对应的块驱动即可。
4). 定时器
bcm2835有三组定时器,分别是系统定时器、arm定时器和看门狗。
这里只用到了系统定时器的SYSTIMER_CNT寄存器。 SYSTIMER_CNT是一个64位计数器,频率约为1MHz。 每增加 1,000,000 大约为一秒。
5)。 力量
电源驱动程序,用于重新启动系统。
接下来我们终于可以开始了解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 failedn");
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!nr");
// 正常启动树莓派
kernel_main();
5.实验与总结
将以上逻辑构建的kernel.img复制到sd卡中,并将sd卡插入树莓派(不再需要从树莓派上拔下sd卡了~)。
将串行电缆连接到 Raspberry Pi。
LED灯亮后5秒内,上位机运行upload.py,等待几秒,新的kernel.img就上传到SD卡了!
6.参考
/dwelch67/树莓派/
/bztsrc/raspi3-tutorial/
/GrassLab/osdi/
/996拒绝/emperorOS
本文相关代码已上传至github,请使用git命令下载
$ git clone -b bootloader https://github.com/996refuse/emperorOS.git
$ cd emperorOS
$ make
$ cp kernel.img /path/to/sd/root/dir