本文节选自《奔跑吧linux内核 入门篇》第二版第16.2.1章

和笨叔一起玩树莓派,动手写BenOS!

实验16-1:输出“Welcome BenOS!”

1.实验目的
1)了解和熟悉ARM64汇编。
2)了解和熟悉如何使用QEMU和GDB调试裸机程序。

2.实验要求
1)编写一个裸机程序并在QEMU模拟器中运行,输出“Welcome BenOS!”字符串。
2)在树莓派上运行编译好的裸机程序。

3.实验详解
由于我们写的是裸机程序,因此需要手动编写Makefile和链接脚本。
对于任何一种可执行程序,不论是.elf还是.exe文件,都是由代码段.text、数据段.data、未初始化数据段.bss等段(section)组织的。链接脚本最终会把一大堆编译好的二进制文件(.o文件)综合生成为二进制可执行文件,也就是把所有二进制文件整合到一个大文件中。这个大文件由总体的.text/.data/.bss段描述。下面是本实验中的一个链接文件,名为link.ld。

1 SECTIONS
2 {
3 . = 0x0;
4 .text.boot : { *(.text.boot) }
5 .text : { *(.text) }
6 .rodata : { *(.rodata) }
7 .data : { *(.data) }
8 . = ALIGN(0x8);
9 bss_begin = .;
10 .bss : { (.bss) }
11 bss_end = .;
12 }
在第1行中,SECTIONS是LS(Linker Script)语法中的关键命令,用来描述输出文件的内存布局。SECTIONS命令告诉链接文件如何把输入文件的段映射到输出文件的各个段,如何将输入段合为输出段,以及如何把输出段放入程序地址空间(VMA)和进程地址空间(LMA)。SECTIONS命令格式如下。

SECTIONS
{
sections-command
sections-command

}
sections-command有4种。

ENTRY命令。

符号赋值语句。

输出段的描述(output section description)。

段的叠加描述(overlay description)。

在第3行中,“.”非常关键,它代表位置计数(Location Counter,LC),这里把.text段的链接地址被设置为0x0,这里链接地址指的是装载地址(load address)。
在第4行中,输出文件的.text.boot段内容由所有输入文件(其中的“”可理解为所有的.o文件,也就是二进制文件)的.text.boot段组成。在第5行中,输出文件的.text段内容由所有输入文件(其中的“”可理解为所有的.o文件,也就是二进制文件)的.text段组成。
在第6行中,输出文件的,rodata段由所有输入文件的.rodata段组成。
在第7行中,输出文件的,data段由所有输入文件的.data段组成。
在第8行中,设置为按8个字节对齐。
在第9~11行中,定义了一个.bss段。
因此,上述链接文件定义了如下几个段。

.text.boot段:启动首先要执行的代码。

.text段:代码段。

.rodata段:只读数据段。

.data段:数据段。

.bss段:包含初始化的或初始化为0的全局变量和静态变量。

下面开始编写启动用的汇编代码,将代码保存为boot.S文件。

1 #include "mm.h"
2
3 .section ".text.boot"
4
5 .globl _start
6 _start:
7 mrs x0, mpidr_el1
8 and x0, x0,#0xFF
9 cbz x0, master
10 b proc_hang
11
12 proc_hang:
13 b proc_hang
14
15 master:
16 adr x0, bss_begin
17 adr x1, bss_end
18 sub x1, x1, x0
19 bl memzero
20
21 mov sp, #LOW_MEMORY
22 bl start_kernel
23 b proc_hang
启动用的汇编代码不长,下面作简要分析。
在第3行中,把boot.S文件编译链接到.text.boot段中。我们可以在链接文件link.ld中把.text.boot段链接到这个可执行文件的开头,这样当程序执行时将从这个段开始执行。
在第5行中,_start为程序的入口点。
在第7行中,由于树莓派有4个CPU核心,但是本实验的裸机程序不希望4个CPU核心都运行起来,我们只想让第一个CPU核心运行起来。mpidr_el1寄存器是表示处理器核心的编号 。
在第8行中,and指令为与操作。
第9行,cbz为比较并跳转指令。如果x0寄存器的值为0,则跳转到master标签处。若x0寄存器的值为0,表示第1个CPU核心。其他CPU核心则跳转到proc_hang标签处。
在第12和13行,proc_hang标签这里是死循环。
在第15行,对于master标签,只有第一个CPU核心才能运行到这里。
在第16~19行,初始化.bss段。
在第21行中,设置sp栈指针,这里指向内存的4 MB地址处。树莓派至少有1 GB内存,我们这个裸机程序用不到那么大的内存。
在第22行中,跳转到C语言的start_kernel函数接下来需要跳转到C语言的start_kernel函数,这里最重要的一步是设置C语言运行环境,即堆栈。

总之,上述汇编代码还是比较简单的,我们只做了3件事情。

只让第一个CPU核心运行,让其他CPU核心进入死循环。

初始化.bss段。

设置栈,跳转到C语言入口。
接下来编写C语言的start_kernel函数。本实验的目的是输出一条欢迎语句,因而这个函数的实现比较简单。将代码保存为kernel.c文件。

include "mini_uart.h"

void start_kernel(void)
{

uart_init();
uart_send_string("Welcome BenOS!\r\n");

while (1) {
    uart_send(uart_recv());
}

}
上述代码很简单,主要操作是初始化串口和往串口里输出欢迎语句。
接下来实现一些简单的串口驱动代码。树莓派有两个串口设备。

PL011串口,在BCM2837芯片手册中简称UART0,是一种全功能的串口设备。

Mini串口,在BCM2837芯片手册中简称UART1。

本实验使用PL011串口设备。Mini串口设备比较简单,不支持流量控制(flow control),在高速传输过程中还有可能丢包。

BCM2837芯片里有不少片内外设复用相同的GPIO接口,这称为GPIO可选功能配置(GPIO Alternative Function)。GPIO14和GPIO15可以复用UART0和UART1串口的TXD引脚和RXD引脚,如表16.1所示。关于GPIO可选功能配置的详细介绍,读者可以查阅BCM2837芯片手册的6.2节。在使用PL011串口之前,我们需要通过编程来使能TXD0和RXD0引脚。

640.png

BCM2837芯片提供了 GFPSELn寄存器用来设置GPIO可选功能配置,其中 GPFSEL0用来配置GPIO0~GPIO9,而GPFSEL1用来配置GPIO10~GPIO19,以此类推。其中,每个GPIO使用3位来表示不同的含义。

000:表示GPIO设置为输入

001:表示GPIO设置为输出。

100:表示GPIO配置为可选项0。

101:表示GPIO配置为可选项1。

110:表示GPIO配置为可选项2。

111:表示GPIO配置为可选项3。

011:表示GPIO配置为可选项4。

010:表示GPIO配置为可选项5。

首先在include/asm/base.h头文件中加入树莓派寄存器的基地址。

ifndef _P_BASE_H

define _P_BASE_H

ifdef CONFIG_BOARD_PI3B

define PBASE 0x3F000000

else

define PBASE 0xFE000000

endif

endif /_P_BASE_H /

下面是PL011串口的初始化代码。

void uart_init ( void )
{

unsigned int selector;

selector = readl(GPFSEL1); selector &= ~(7<<12);
/* 为GPIO14设置可选项0*/
selector |= 4<<12;
selector &= ~(7<<15);
/* 为GPIO15设置可选项0 */
selector |= 4<<15;
writel(selector, GPFSEL1);

上述代码把 GPIO14 和 GPIO15 设置为可选项0,也就是用作PL011 串口的RXD0 和TXD0引脚。

writel(0, GPPUD);
delay(150);
writel((1<<14)|(1<<15), GPPUDCLK0);
delay(150);
writel(0, GPPUDCLK0);

通常GPIO引脚有3个状态——上拉(pull-up)、下拉(pull-down)以及连接(connect)。连接状态指的是既不上拉也不下拉,仅仅连接。上述代码已把GPIO14和GPIO15设置为连接状态。
下列代码用来初始化PL011串口。

/* 暂时关闭串口 */
writel(0, U_CR_REG);

/* 设置波特率 */
writel(26, U_IBRD_REG);
writel(3, U_FBRD_REG);

/* 使能FIFO */
writel((1<<4) | (3<<5), U_LCRH_REG);

/* 屏蔽中断 */
writel(0, U_IMSC_REG);
/* 使能串口,打开收发功能 */
writel(1 | (1<<8) | (1<<9), U_CR_REG);

接下来实现如下几个函数以收发字符串。

void uart_send(char c)
{

while (readl(U_FR_REG) & (1<<5))
    ;

writel(c, U_DATA_REG);

}

char uart_recv(void)
{

while (readl(U_FR_REG) & (1<<4))
    ;

return(readl(U_DATA_REG) & 0xFF);

}
uart_send()和uart_recv()函数分别用于在while循环中判断是否有数据需要发生和接收,这里只需要判断U_FR_REG寄存器的相应位即可。
接下来编写Makefile文件。

board ?= rpi3

ARMGNU ?= aarch64-linux-gnu

ifeq ($(board), rpi3)
COPS += -DCONFIG_BOARD_PI3B
QEMU_FLAGS += -machine raspi3
else ifeq ($(board), rpi4)
COPS += -DCONFIG_BOARD_PI4B
QEMU_FLAGS += -machine raspi4
endif

COPS += -g -Wall -nostdlib -nostdinc -Iinclude
ASMOPS = -g -Iinclude

BUILD_DIR = build
SRC_DIR = src

all : benos.bin

clean :

rm -rf $(BUILD_DIR) *.bin 

$(BUILD_DIR)/%_c.o: $(SRC_DIR)/%.c

mkdir -p $(@D)
$(ARMGNU)-gcc $(COPS) -MMD -c $< -o $@

$(BUILD_DIR)/%_s.o: $(SRC_DIR)/%.S

$(ARMGNU)-gcc $(ASMOPS) -MMD -c $< -o $@

C_FILES = $(wildcard $(SRC_DIR)/*.c)
ASM_FILES = $(wildcard $(SRC_DIR)/*.S)
OBJ_FILES = $(C_FILES:$(SRC_DIR)/%.c=$(BUILD_DIR)/%_c.o)
OBJ_FILES += $(ASM_FILES:$(SRC_DIR)/%.S=$(BUILD_DIR)/%_s.o)

DEP_FILES = $(OBJ_FILES:%.o=%.d)
-include $(DEP_FILES)

benos.bin: $(SRC_DIR)/linker.ld $(OBJ_FILES)

$(ARMGNU)-ld -T $(SRC_DIR)/linker.ld -o $(BUILD_DIR)/benos.elf  $(OBJ_FILES)
$(ARMGNU)-objcopy $(BUILD_DIR)/benos.elf -O binary benos.bin

QEMU_FLAGS += -nographic

run:

qemu-system-aarch64 $(QEMU_FLAGS) -kernel benos.bin

debug:

qemu-system-aarch64 $(QEMU_FLAGS) -kernel benos.bin -S -s

board用来选择板子,目前支持树莓派3和树莓派4。
ARMGNU用来指定编译器,这里使用aarch64-linux-gnu-gcc。
COPS和ASMOPS用来在编译C语言和汇编语言时指定编译选项。

-g:表示编译时加入调试符号表等信息。

-Wall:打开所有警告信息。

-nostdlib:表示不连接系统的标准启动文件和标准库文件,只把指定的文件传递给连接器。这个选项常用于编译内核、bootloader等程序,它们不需要标准启动文件和标准库文件。

-nostdinc:表示不包含C语言的标准库的头文件。
上述文件最终会被编译链接成名为kernel8.elf的.elf文件,这个.elf文件包含了调试信息,最后使用objcopy命令把.elf文件转换为可执行的二进制文件。
在Linux主机上使用make命令编译文件。在编译之前可以选择需要编译的板子类型。例如,要编译在树莓派3上运行的程序,可使用如下命令。

$ export board=rpi3
$ make
要编译在树莓派4上运行的程序,可使用如下命令。

$ export board=rpi4
$ make
在放到树莓派之前,可以使用QEMU虚拟机来模拟树莓派以运行我们的裸机程序,可直接输入“make run”命令。

$ make run
qemu-system-aarch64 -machine raspi3 -nographic -kernel benos.bin
Welcome BenOS!
也可输入如下命令。

$ qemu-system-aarch64 -machine raspi3 -nographic -kernel benos.bin
如果读者想用QEMU虚拟机模拟树莓派4B,那么需要打上树莓派4B的补丁并且重新编译QEMU虚拟机。
要在树莓派上运行刚才编译的裸机程序,需要准备一张格式化好的MicroSD卡。

使用MBR分区表。

格式化boot分区为FAT32文件系统。

参照实验3-1中介绍的方法烧录MicroSD卡,这样就可以得到格式化好的boot分区和烧录的树莓派固件。读者也可以使用Linux主机上的分区工具(比如GParted)来格式化MicroSD卡,把树莓派固件复制到这个FAT32分区里,其中包括如下几个文件。

bootcode.bin:引导程序。树莓派复位上电时,CPU处于复位状态,由GPU负责启动系统。GPU首先会启动固化在芯片内部的固件(BootROM代码),读取MicroSD卡中的bootcode.bin文件,并装载和运行bootcode.bin中的引导程序。树莓派4B已经把bootcode.bin引导程序固化到BootROM里。

start4.elf:树莓派4上的GPU固件。bootcode.bin引导程序检索MicroSD卡中的GPU固件,加载固件并启动GPU。

start.elf:树莓派3上的GPU固件。

config.txt:配置文件。GPU启动后读取config.txt配置文件,读取Linux内核映像(比如kernel8.img等)以及内核运行参数等,然后把内核映像加载到共享内存中并启动CPU,CPU结束复位状态后开始运行Linux内核。
把benos.bin文件复制到MicroSD卡的boot分区,修改里面的config.txt文件。

<config.txt文件>

[pi4]
kernel=benos.bin
max_framebuffers=2

[pi3]
kernel=benos.bin

[all]
arm_64bit=1

enable_uart=1

kernel_old=1
disable_commandline_tags=1
插入MicroSD卡到树莓派,连接USB电源线,使用Windows端的串口软件可以看到输出,如图16.6所示。

640 (1).png

图16.6 输出欢迎语句

下载入门篇第二版全套海量资料

登录“奔跑吧linux社区”微信公众号,在微信公众号里输入“奔跑吧2”,下载奔跑吧第二版入门篇全套资料:

全套实验环境,基于Ubuntu 20.04制作的VMware/VirtualBox虚拟机镜像

实验指导手册

电子课件

实验参考代码

插图下载

勘误

免费视频

Docker镜像

20220318_151733_021.jpg
20220318_151733_022.jpg
20220318_151733_023.jpg

好多同学问金色那本实验指导手册哪里可以买?

其实,实验指导手册pdf版本已经开源,大家可以免费下载,自由打印。

打印小贴士:到淘宝上随便找一家打印店,打印B5大小即可。


奔跑吧Linux社区
4 声望4 粉丝

奔跑吧Linux社区,为广大小伙伴布道Linux开源!


引用和评论

0 条评论