树莓派的冒险之旅…… 第 4 部分





5.00/5 (15投票s)
介绍 SmartStart,类似于树莓派的 BIOS
提供的示例代码
GitHub 现已上线
https://github.com/LdB-ECM/Raspberry-Pi
引言
这篇文章的内容非常丰富,篇幅会比较长。我目前用树莓派在做的事情是开发我自己的 64 位操作系统,作为这个过程的一部分,我需要一个加载器,它是一种外观和行为都非常像普通计算机 BIOS 的代码。最简单的描述就是,它是一段可以初始化树莓派、识别树莓派型号、内存、外设等信息,然后在启动操作系统时将这些信息传递给操作系统的代码。最后,操作系统甚至可能在替换并强制执行自己的函数时将这段加载器代码从内存中移除。
加载器代码需要覆盖各种功能,从挂载驱动器和分区、读取文件系统,到更基础的事情,比如屏幕显示。我的加载器目前仍未完成,但代码已经可以作为其他用途的起点。这是我当前发布的版本的屏幕截图。
背景
与我之前的文章一样,代码的组织方式是使用与你正在使用的树莓派型号相匹配的批处理文件(pi1、pi2、pi3.bat)进行编译,并提供一个简单的命令行链接到我安装 Windows ARM 交叉编译器的目录(对我来说是 G:\Pi\gcc_pi_6_2)。我使用的是 ARM 官方网站 此处提供的标准 ARM 交叉编译器。
所以我们需要深入了解 SmartStart 的工作细节。对于不熟悉汇编语言编程的人来说,可能需要做一些基础阅读才能完全理解接下来的部分。为什么选择汇编而不是 C,对我来说有两方面的原因。第一,这类似 BIOS。它需要是最低版本的树莓派代码(ARM6),因为它必须能在所有树莓派主板上运行。你可以设置一个 makefile 来为 ARM6 编译某些 C 文件,为 ARM7/8 编译其他文件,但这个过程非常麻烦。第二个因素是精确地确定 C 代码块的大小。这更加麻烦且不可靠,在某种程度上,你永远无法确定代码中包含的所有内容都会被包含进去。这尤其适用于高优化级别,因为小的 C 代码块甚至可能被内联到大的 C 代码块中。所以这就是我选择汇编而不是 C 的基础。
SmartStart 首先会执行一系列的自动检测功能,存储这些信息,然后传递给正常的 C main 函数块。C 代码随后可以访问 SmartStart 提供的 API 函数(它们总是以 RPI_xxxxxxxx 开头),由于自动检测阶段,这些函数完全了解底层的树莓派型号结构。它们利用这些保存的信息,在任何型号的树莓派上无缝执行功能,而无需用户干预。
SmartStart 本质上是由汇编代码块组成,其核心是围绕一个相当简单的系统构建的,该系统有 7 个关键点:
- 将每个汇编函数源代码放在一个汇编文件中。
- 在你的汇编过程中,将每个函数包含为普通段(.txt、.data 等)自己的一个段。
- 为库函数原型和任何
typedef
创建一个单一的 C 头文件。 - 上面的头文件被共享并包含到汇编器中,这样它们永远不会不一致或出错。
- 使用标志
-ffunction-sections
-Wl
,-gc-sections
(垃圾回收并丢弃未使用的段)进行编译。 - 将你的源代码发布给毫无戒心的客户/公众。
- 他们不需要其他东西,只有使用的代码块才会被包含进去。这是“用就保留,不用就丢弃”的编译方式。
一个典型的段块如下所示:
/* "PROVIDE C FUNCTION: uint64_t RPI_GetArmTickCount (void);" */
.section .text.RPI_GetArmTickCount, "ax", %progbits
.align 2
.globl RPI_GetArmTickCount;
.type RPI_GetArmTickCount, %function
.syntax unified
.arm
;@"================================================================"
;@ RPI_GetArmTickCount -- Composite Pi1, Pi2 & Pi3 code
;@ C Function: uint64_t RPI_GetArmTickCount (void)
;@ Return: R0, R1 will have tickcount value
;@"================================================================"
RPI_GetArmTickCount:
stmfd sp!, {r4, lr}
ldr r3, =RPi_IO_Base_Addr
ldr r3, [r3]
add r3, r3, #0x3000
.HiTimerMoved2:
ldr r2, [r3, #8] ;@ Read timer hi count
ldr r0, [r3, #4] ;@ Read timer lo count
ldr r1, [r3, #8] ;@ Re-Read timer hi count
cmp r2, r1
bne .HiTimerMoved2 ;@ Check both hi count reads were same
ldmfd sp!, {r4, pc}
.align 2
.ltorg ;@ Tell assembler ltorg data for this code can go here
.size RPI_GetArmTickCount, .-RPI_GetArmTickCount
代码会编译到普通 .text 段自己的一个子段(子段 .RPI_GetArmTickCount
),我们甚至携带了段的大小,以便帮助链接器,并且如果以后我想丢弃代码块或移动它,我就会知道它的尺寸。
.ltorg 告诉汇编器在哪里可以放置来自伪指令(如 "ldr r3, =RPi_IO_Base_Addr
")的硬编码字面量。这实际上会在 ltorg
位置放置一个字面量 ".word RPi_IO_Base_Addr
",然后加载将从该字面量进行。这些字面量必须在精确的位置与代码块链接,它们构成代码的一部分,因此也构成代码块大小的一部分。
封装该函数的 C 头文件如下所示:
/*-GetArmTickCount-----------------------------------------------------------
Same as GetTickCount but at full 1Mhz Rapberry Pi system timer resolution.
The timer read is as per the Broadcom specification of two 32bit reads
(http://embedded-xinu.readthedocs.io/en/latest/arm/rpi/BCM2835-System-Timer.html)
24Jan17 LdB
--------------------------------------------------------------------------*/
extern uint64_t RPI_GetArmTickCount(void) __attribute__((pcs("aapcs")));
“aapcs
”属性是 ARM 当前的标准调用约定,并且在未来如果出现其他标准时提供保护。该标准仅涵盖进入和退出 C 函数时在寄存器中传递的内容,并且由于我们的汇编器假定“aapcs
”标准,我只是向编译器明确了这一点。如果你对此感兴趣,可以随时阅读“aacps
”。
现在,如果我们上面的函数在编译我们的程序时被使用,代码块将被添加到输出文件中;如果未使用,它将被我们的编译器标志指令(-ffunction-sections -Wl
, -gc-sections
)丢弃。
那么,让我们来看一个绝对最小化的代码——类似这样:
#include "rpi-smartstart.h" // Needed for smart start API
int main (void) {
while (1){
RPI_Activity_Led(1); // Turn LED on
RPI_WaitMicroSeconds(500000); // 0.5 sec delay
RPI_Activity_Led(0); // Turn Led off
RPI_WaitMicroSeconds(500000); // 0.5 sec delay
}
return(0);
}
这是经典的闪烁活动 LED 的代码,以半秒为间隔亮起,然后熄灭,如此循环。SmartStart 知道每个型号树莓派的活动 LED 位置以及计时器,并且代码能整洁地压缩到每个型号大约 9K 的最小镜像文件中。所有未在程序中引用的代码块(例如上面的块)都将被链接器忽略。如果你有兴趣尝试一个最小化的示例,这里是代码。
代码块确实是以“用就保留,不用就丢弃”的模式运行的。你不必担心汇编文件中的代码量,只有你使用的段才会被包含在你的镜像文件中。
更高级的用户可以查看链接器文件“rpi.ld”,并查看堆栈、数据和文本段的布局。里面有一些更高级的技巧,例如,我有两个数据段——一个是 4 字节对齐,一个是 16 字节对齐,以尽量减少数据空隙。许多邮箱数据结构要求 16 字节对齐,通过将它们放在一个 16 字节对齐的块中,可以帮助整洁地压缩数据。
另一个有趣的事情是,“rpi-smartstart.h”同时被汇编器和 C 编译器包含,因为其中的一些定义是两者都需要的,以确保它们匹配。你需要记住 `#ifdef` 之间的部分可以是用于汇编器、C 编译器或两者。
最后的警告是关于 ARM7 和 ARM 8 编译的对齐问题。许多固定结构,如 FAT32 和 BMP 文件中使用的结构,并未按 4 字节边界对齐。即使是简单的代码,如 memcpy
等,也会导致处理器抛出未对齐的错误。你会在代码中的多个地方看到对未对齐访问的引用,你需要小心不要忽略它。ARM 网站上对此主题有支持,例如,关于 memcpy
此处。我不喜欢将 memcpy
重定向回本质上是 ARM6 版本的 memcpy
的建议,并在所有地方承担速度损失,而这些仅仅是为了少数几个我可以轻松识别的特定情况。然而,如果这对你来说变得麻烦,那么这可能是一个选择。
我个人的工作流程是,我编译并使用 ARM6(pi1 编译)中的所有内容,即使是在 Pi2、Pi3 上。SmartStart 自动检测会处理两个型号之间的所有差异,我的 ARM6 代码可以在 Pi2、Pi3 上完美运行,且没有对齐问题。最后,当我把所有东西都按我想要的方式运行起来后,我会切换到 ARM7 或 ARM8 编译,然后看看哪些地方会出错,而这些通常只是对齐问题,可以轻松解决。
Using the Code
SmartStart API 包含许多函数,例如所有常规函数,如定时器和 GPIO 访问,一直到图形和 SD 卡访问。我鼓励你打开 rpi-SmartStart.h 文件,查看其中记录的各种函数。我正试图抽出时间创建 SmartStart API 的详细文档,但目前,最新的描述都在标题文件本身中。
好了,在讲完基本背景之后,我需要讨论提供的示例。该示例提供了一个不完整的文件系统(filesys.c 和 filesys.h),我正在用它来测试和调试。关键词是“不完整”——它能做一些事情,但还有很多事情做不了。进一步的警告——文件系统基于 Windows API,我只是对 Linux 不够熟悉,无法用它来做示例。
一个磁盘驱动器包含一个或多个特定格式的分区,如 FAT16、FAT16、NTFS、EXT3。提供的文件系统文件当前识别 FAT16 和 FAT32,由于 SD 卡在树莓派上的格式就是这样,所以使用在我们的示例中是显而易见的。
所以要使用一个驱动器,必须首先将其挂载到一个你想要通过其知道该驱动器的选定驱动器号上,该驱动器号可以是 A 到 Z 的任意一个。挂载代码形式如下,很容易看到示例将 SD 卡分区 0 挂载为“C”驱动器。请注意,代码目前只挂载分区 0,我还没有处理多分区驱动器,但已提供接口以在不久的将来进行扩展。
// Mount the SD card onto a drive letter ... 'C' in this example
if (mountSDCard('C', 0) == SD_OK) {
由于我们的裸机文件正在启动,我们几乎可以确定 SD 卡有一个 FAT16 或 FAT32 分区,所以它应该能够挂载。一旦挂载成功,你就可以访问 SmartStart 提供的各种文件函数。如果你对 FAT16/32 文件系统的工作原理感兴趣,我强烈推荐 维基百科页面。
与所有 Windows 文件操作一样,它们都基于一个叫做文件句柄的东西。我们的文件系统遵循这个方案,我们有两种类型的文件句柄,称为搜索句柄和访问句柄。
示例的第一部分使用搜索句柄通过 FindFirst
、循环 FindNext
(只要有效)然后使用 FindClose
来清理句柄来列出驱动器上的文件。现在你需要注意一些限制,因为我还没有完成完整的 Windows API 实现。所有这些限制都已在函数代码中清楚注释,请参考以获取更多详细信息。
FindFirst
没有“当前驱动器”的概念,你必须提供一个有效的已挂载驱动器的驱动器号。FindFirst
目前不支持子目录,只能访问根目录(待办事项)。- 搜索句柄大约占用 600 字节,在 filesys.c 中最多只能有 4 个(你可以查看代码进行更改)。
- 从第 3 点开始,当你完成搜索后,使用
FindClose
来释放该句柄,以便它能被再次使用。
假设 FindFirst
返回成功,我们然后调用带有返回的有效搜索句柄的 findnext
。它要么返回下一个文件条目,要么在没有更多文件时最终失败。示例显示了从文件搜索返回的一些详细信息(日期/时间/大小等)每个有效文件。然后我执行 FindClose
来释放搜索句柄以便重用。这都很简单明了,我使用标准的 C 时间函数进行显示。
你可以在这里看到的代码中看到标记的 3 个简单步骤。
// Step 1: Execute a FindFirst
FHANDLE handle = FindFirstFile("C:\\*.*", &search_data);
if (handle & SHE_ERROR_PRESENT) printf("Findfirst failed with Error ID: %8lx\n", handle);
// Step 2: Loop on FindNextFile until it errors
while ((handle & SHE_ERROR_PRESENT) == 0) {
char buffer[26]; // Buffer to format time into
strftime(buffer, sizeof(buffer),
"%d%b%Y %H:%M:%S", &search_data.CreateDT); // Use c function to format date/time
printf("Entry: %s Size: %8lu bytes Created: %s, LFN: %s\n",
search_data.cAlternateFileName, search_data.nFileSizeLow,
&buffer[0], search_data.cFileName); // Display each entry
if (FindNextFile(handle, &search_data) == false) break; // Find next file entry
}
// Step3: Free the search handle
FindClose(handle);
演示的第二部分进行了基本的文件读取访问功能。提供的写入函数不完整,因为它没有正确设置 FAT 条目文件大小,这是一个待办练习。由于我正在做一个加载器,我一直更关注读取函数,而不是写入函数。我写这篇文章时才想起这个缺点,并将在未来几天内尝试更新它。
所以我们的文件读取测试基本上是将两个位图文件显示到屏幕上。它首先创建一个有效的文件访问句柄,这里也有一些限制:
CreateFile
没有“当前驱动器”的概念——你必须提供一个有效的已挂载驱动器的驱动器号。CreateFile
目前不支持子目录,只能访问根目录(待办事项)。- 文件访问句柄大约占用 640 字节——在 filesys.c 中最多只能有 4 个。
- 从第 3 点开始,完成后,调用
FileClose
来释放该句柄,以便它能被再次使用。
在位图显示中使用的另外两个函数 ReadFile
和 SetFilePosition
使用返回的有效文件句柄来执行相应的函数。如果你想更改位图文件,它们必须是 24 位或 32 位 BMP 文件,如果你想使用我提供的粗略显示代码。我没有为使用低颜色深度的 BMP 文件(如 16 色或 256 色)提供任何支持。
关注点
这有望成为 SmartStart 版本发布的起点,它将添加越来越多的功能。我目前正在进行挂载 USB 驱动器的研究,它在 Pi1、Pi2 上工作正常,但在 Pi3 上不行,这有望成为下一个版本。
如果有人有公开发布的 C 代码来读取 Linux ext2/3/4 分区,我非常希望能听到你的意见,可以在下面的评论区留言,或通过我的联系方式发送电子邮件给我。
历史
- 1.01 首次发布