Changchun Master Li

[RPi bring up] hello world! 树莓派裸机点亮led

2022-06-18

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

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

阅读本文您需要具备

  • 全日制小学生学历及其同等学历 ★★★★★
  • GNU工具链(make/GCC/LD) ★★☆☆☆
  • ARM汇编语言 ★☆☆☆☆

0. keyword

raspberry pi 1 bcm2835 armv6 hello world led blink embedded operating systems

1. abstract

得益于超大规模集成电路技术,SoC可以在单个芯片上集成CPU、GPU、内存和外设,使其成为一个五脏俱全的完整系统。因此制造尺寸更小的单板计算机成为可能。单板计算机的应用非常广泛,从家用微波炉电视机到商用加油机广告机,单板计算机无处不在。树莓派就是一款信用卡大小的单板计算机。

遗憾的是,大多数人只是用它跑一个web服务或者家用nas。而树莓派基金会最初开发树莓派的目的就是在学校中推广计算机基础科学,通过树莓派,可以很容易的将计算机体系结构和操作系统的知识付诸实践!

在2022年,搭载bcm2835的1代树莓派显得非常性能嬴弱。但它在芯片短缺中仍然非常廉价,某些培训机构的IMX6ULL开发板却动辄几百块!目前一代b价格大概 20-30 左右,一代b+大概 40 元,比单片机最小系统都便宜。经典的ARMv6架构,aarch32指令与ARMv7a完全兼容(协处理器不同),简单易学,至今流行。比韦东山老师淘宝卖的s3c2440好多了。

重要的是,因为社区巨大的用户基数,几乎所有问题都能找得到答案,网上也可以找到BCM2835和arm1176jzfs的规格书。

搞懂计算机从软件到硬件是怎样工作的并不容易,和硬件打交道,有很多hardfault,难以发现和修正bug。硬件系统不提供标准IO,console驱动程序需要自己实现。为了保持简单,和编程语言第一个代码示例hello world不同,我们的bring up是从点亮一个led开始。

点亮一个led分三步:

  • 启动
  • 找到开关
  • 开灯

2. boot 启动

树莓派最小裸机启动流程为

片上ROM → 挂载: tf卡上第一个fat32分区 → bootcode.bin → start.elf → 载入运行: kernel.img

树莓派firmware是闭源的,bootcode.bin和start.elf可以从官方下载firmware。
将tf卡格式化为fat32格式,然后把bootcode.bin和start.elf复制过来。
接下来就是制作一个kernel.img了。在裸机环境中,没有系统和库,我们会在kernel.img直接与硬件打交道,实现led的逻辑。这就是bare metal或者bare bone环境编程。

3. CPU通过MMIO控制外设

启动之后怎么找到led的开关呢?

CPU地址空间

CPU视角的地址空间,低地址(0-448MB)被直接映射到内存芯片,蓝色区域没有映射,好像一片荒漠,读写指令对它不起作用。从0x2000_0000开始的一段空间被映射到外设中,被称为MMIO(memory-mapped IO)。usb控制器、mmu、tf卡控制器等外设均可以通过MMIO连接到cpu上,由cpu控制。

0x2020_0000到0x2020_00b0 所映射的设备为GPIO控制器,可以控制所有54个GPIO引脚状态。树莓派上板载的led(status_ok)位于bcm2825 GPIO16

0x00到0x14 共六个GPIO功能选择寄存器GPFSEL0-GPFSEL0,三个bit可以控制每一个GPIO

  • 000 = GPIO Pin is an input
  • 001 = GPIO Pin is an output
  • 100 = function 0
  • 101 = function 1
  • 110 = function 2
  • 111 = function 3
  • 011 = function 4
  • 010 = function 5

0x1c到0x20 共两个置位寄存器GPSET0-GPSET1,每bit可以置位一个GPIO,写有效

0x28到0x2c 共两个清零寄存器GPCLR0-GPCLR1,每bit可以清零一个GPIO,写有效

所以想要控制,GPIO16作为输出口,首先把控制位设置为001,

GPFSEL1[18:21]=0b001

如果需要高电平,需要写GPSET0

GPSET0[16]=1

如果需要低电平,需要写GPCLR0

GPCLR0[16]=1

下拉输入

led电路为下拉输入,因此高电平led不亮,低电平led亮。

下面是我的汇编实现:

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
ldr r1, =0x20200004 // GPFSEL1
ldr r0, [r1]
ldr r2, =0xffe3ffff
and r0, r2
ldr r2, =0b001 // output
orr r0, r2, lsl #18
str r0, [r1]


ldr r1, =0x20200028 // GPCLR0
ldr r2, =0x2020001C // GPSET0
mov r0, #1
lsl r0, #16

loop:
// led on
str r0, [r1]

// sleep
ldr r3, =0x00400000
1:
subs r3, #1
beq 2f
b 1b
2:

// led off
str r0, [r2]

// sleep
ldr r3, =0x00400000
3:
subs r3, #1
beq 4f
b 3b
4:
b loop

具体可参考 emperorOS

BCM2835 主要有以下外设,它们都由 MMIO 操作来控制

  • Timers
  • Interrupt controller
  • GPIO
  • USB
  • PCM / I2S
  • DMA controller
  • I2C master
  • I2C / SPI slave
  • SPI0, SPI1, SPI2
  • PWM
  • UART0, UART1

4. Implementation

为了得到kernel.img二进制文件,我们需要编译链接。
接下来需要准备工具链(toolschain),GNU toolchain是一坨可以用来构建操作系统的工具,它包括:

  • make 自动化
  • gcc 编译器
  • linker 链接器
  • as 汇编器
  • gdb 调试

等工具,都可以编译安装,如果没有相关经验
可以从ubuntu source安装

sudo apt install g++-arm-linux-gnueabi

或者从官网下载安装

GNU Arm Embedded Toolchain Downloads

树莓派开机时会将kernel.img载入到0x8000开始的内存中,因此需要为链接器定义程序开始的位置

1
2
3
4
5
6
7
. = 0x8000;
__start = .;
.text :
{
KEEP(*(.text.boot))
*(.text)
}

相关实现已经push到github,直接clone下来编译即可

1
2
3
git clone --branch led_blink https://github.com/996refuse/emperorOS.git
make kernel
cp kernel.img /path/to/tfcard

然后将kernel.img拷贝到tf卡中。
插电开机,就可以观察到树莓派启动后led闪烁了。

4. summary

点亮led是个简单的实验,它向我们初步展示了树莓派的工作逻辑,帮助我们迈出从软件到硬件的第一步,同时引出了更多有待我们挖掘的有趣话题,我们会再后续的文章中进一步探索。原理并不复杂,但涉及的知识很广泛。为了保持简单易懂,本文有意隐藏了一些更深入的讨论,想要更多的了解可参考后面的补充材料。

5. 补充材料

5.1 树莓派启动流程

关注一下 rpi 开源的固件 rpi-open-firmware

固件可配置分配给GPU的内存,默认为64MB。
最小可设置为16MB,在config.txt加入一行gpu_mem=16,并且将cutdown版本的fixup_cd.datstart_cd.elf复制到根目录。

5.2 了解片上系统

bcm2835这款soc有两个处理器,AP 是一个 arm core,BP 被称为 VC,videocore。
BCM2835所集成的GPU是一个博通的 VideoCore IV核心,它支持1080p30,功耗低性能强,典型的买GPU送CPU了。但遗憾的是,因为博通专利限制,VC core并不开源,从网上可以搜到一些口口相传的资料。

BCM2835 address map

bcm2835的CPU和GPU都各带一个mmu,从左往右,分别是从GPU,CPU,操作系统视角看到的内存映射。
操作系统视角看到内存映射即虚拟地址空间:如图所示,arm虚拟地址空间布局是一个标准的linux kernel实现,地址空间被分割成 1G 内核空间和 3G 用户空间,内核空间地址范围为0xC0000000 - 0xEFFFFFFF, 用户空间地址范围为 0x00000000 - 0xBFFFFFFF。需要注意的是,外设(Peripherals)的物理地址空间为 0x20000000+16MB,一般在内核虚拟地址中被映射到0x7E000000+16MB。
CPU视角看到内存映射即物理地址空间:SDRAM 由 VC 和 ARM 共享,可设置 FAT32 的启动分区的 start*.elf 来设定 ARM 可使用的内存大小,VC至少占用16MB内存以确保可以正常启动。对于 256MB 的rpi来说,SDRAM的地址空间为 0x00000000 - 0x10000000。对于 512MB 的rpi来说,SDRAM的地址空间为 0x00000000 - 0x20000000。
总线地址是有cache的,VC mmu通过最高两个bit把地址空间划为四块,每块1GB,都映射到同样的设备上,区别只是cache机制不同。

  • 0x0 L1和L2 cache
  • 0x4 L2 cache,write-through no-write allocate cache, 保证可以直接读写外设MMIO
  • 0x8 L2 cache
  • 0xC no cache,cpu由此直接访问内存

注意,DMA会直接使用总线地址,DMA不会更新cache,所有需要使用0xC区域内存
同样需要注意,BCM2835 AXI system实现方式为低复杂度高带宽,同时读取多个外设数据,硬件不保证读取数据返回的顺序为代码序,需要加memory barrier指令。同一个外设可以保证顺序。

5.3 USB设备电流大导致树莓派重启

把c6电容搞大点,换个1000uf以上的

5.4 工具链编译

可以参考pilfs Linux From Scratch

5.5 gcc编译参数

GCC-ARM-Options

5.6 reference

Schematics
BCM2835
arm1176jzfs
elinux
osdev
emperorOS

使用支付宝打赏
使用微信打赏

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

扫描二维码,分享此文章