其实操作系统从来就没有那么复杂过。
差不多每个稍有野心的程序员都会考虑以下问题:写一个操作系统、写一个数据库、写一个编程语言和它的编辑器。在打了十年键盘之后,我们终于可以考虑着手第一个问题:编写一个操作系统了。虽然在前几年有过这样的尝试,但是最后算是无疾而终。总体还是因为叠起来有几千页的《英特尔® 64 位和 IA-32 架构开发人员手册》把我震撼到了,加上当时还在上高中,所以并没有多少时间来干这样的事情。不过现在出于「为了更好的嵌入式开发」的原因打算着手进行嵌入式系统的开发,那么最好是写一下整件事情究竟是怎么一回事以及操作系统究竟是怎样的一个东西。
前言
X86 的操作系统构建已经很好地在 OSDev Wiki 上阐明了。而且也有《30 天自制操作系统》和《Orange'S: 一个操作系统的实现》这两本书进行手把手教学,即使完全不知道自己在干什么,照着例子敲代码也能出结果。我必须承认之前的尝试确实就是对例子的抄写,所以实际上没有什么实质性的进步。不过现在就是真正实打实地进行编程,希望最后是能学到点什么新的东西。
我们的开发板是 STM32F407VET6
. 不过说实话软件的兼容性差不多是 STM32F407
这样,因为操作系统内核对于外设的依赖性并不强。以及这个操作系统是在(民用)无人机上运行的,所以会更注重实时性。但之所以不直接说是 RTOS 是因为我对实时性这事没谱,只能说「注重」而不能说「保证」。不过毕竟只是实验,对吧?
语言仍然是 C 混合一点汇编。标准配置。我一直很想用 Rust, 但是 Rust 混合汇编这事我觉得很痛苦,所以就老老实实回到 C. 我也一直很想借助 C++, 但是 C++ 对平台是有一定要求的,但这些要求在内核启动之前是无法满足的,所以——用户程序大概可以用 C++ 写,但是内核必须用 C 写是不争的事实。
So let's jump into it.
交叉编译器
交叉编译器这回总算是不用自己再进行配置和编译。不过如果真希望自己编译的话,我之前写过如何构建一个目标为 x86
的编译器的。这回我们只需要直接从官网下载 arm-none-eabi-gcc
就可以了。
工具链
除了 arm-none-eabi-gcc
以外,我们还将使用 openocd
作为片上调试工具,配合 gdb
进行相关的调试工作。openocd
已经支持 jlink
, stlink-v2
和 cmsis-dap
三种常见的调试器(协议),所以也不需要购买特定的调试器,可以直接利用手头上的东西进行调试。
我们还需要 arm-none-eabi-binutils
将 ELF 文件剥离成裸二进制以及 HEX 文件。当然,如果不需要兼容队友的话,可以直接使用 openocd
烧录 ELF 文件。
构建系统
我希望你还记得怎么使用 make
. 至于为什么不使用 cmake
, 是因为我有点累,而且 cmake
又是一个构建系统的构建系统,这层抽象用在这里有些牛刀杀鸡的感觉。而且 cmake
也不能输出 Keil 工程,还不如直接扔一个 zip
解压导入呢。
当然,我是不用 Keil 的。因为用惯了 Linux 之后不能用命令行驱动的工具我通常都觉得很难受。不过周围人都在用 Keil, 所以还得兼容一下。之前用 CLion 搭配 cmake
写飞控,然后队友就都不会用,我就独自揽下所有工作,结果可想而知。不过你也可以诘问我「装什么,别人都用 Keil 你也用不就结了」。答案是其实由于手抖和记忆力差的原因,我离开类型提示和变量名纠正是写不了程序的。况且两头都是拔钉器的锤子虽然仍然可以砸钉子,但是用一个正常的锤子砸钉子更好对不对?
不过由于临近毕业,CLion 使用的时日也不多了。所以这次我决定使用 VS Code. 作为自带 IntelliSense 的操作系统编辑器,类型提示和变量名纠正自然是不在话下。以及 VS Code 的 GDB 前端和运行配置已经足够其成为一个 IDE.
环境配置
安装完成之后,你需要保证这些程序在 PATH
内:
arm-none-eabi-gcc
arm-none-eabi-ar
arm-none-eabi-gdb
arm-none-eabi-ld
arm-none-eabi-objcopy
make
code
如果需要进行版本控制的话,还额外地需要 git
. 如果你更习惯用 svn
或者 hg
可自行调整。
接下来,新建文件夹作为工程目录,使用 code
打开该目录。接下来以该目录作为工作目录。
VS Code 需要安装 C/C++ 语言插件、ARM 插件和 Cortex-Debug 插件,这些插件均可从插件库中安装。
基本结构
工程的文件结构如下:
.
|_ driver/ - 标准外设库
| |_ inc/
| |_ src/
|_ lib/ - 库文件,如 .a 或者 .lib 等
|_ src/ - 源码
|_ Makefile
|_ STM32F407.cfg - OpenOCD 脚本
|_ STM32F407VETx_FLASH.ld - 链接脚本
首先,我们需要从 ST 官网上获取标准外设库。外设库的内容放在 driver/
下。同时,我们需要对应的启动汇编脚本 startup.s
用于设置中断和复制程序到内存。这些内容都可以直接从 ST 官网下载,或者用 CubeMX 生成。
我们需要按需编写链接脚本 STM32F407VETx_FLASH.ld
. 这一部分我们会在下一节讨论。
Makefile
我们的 Makefile
其实十分直白:寻找所有 .c
和 .s
文件,喂给编译器拿到目标文件,再喂给链接器得到最后的成品。接下来的内容虽然看着很多,但大多数都是配置变量,实际的规则还是十分直白的。
# Handwritten Makefile because I suck at CMake
# Author: dousha99
# Date: 2020-03
# Variables
TARGET=arm-none-eabi
LINK_SCRIPT=STM32F407VETx_FLASH.ld
PROJECT=skylab
# Files
INC=inc/ driver/inc/
SRC=$(wildcard src/*.c driver/src/*.c) $(wildcard src/*/*.c)
OBJ=$(SRC:.c=.o) src/startup.o
OCD_SCRIPT=STM32F407.cfg
# Toolchain
CC=${TARGET}-gcc
CXX=${TARGET}-g++
AR=${TARGET}-ar
ASM=${TARGET}-gcc
OBJCOPY=${TARGET}-objcopy
OBJDUMP=${TARGET}-objdump
SIZE=${TARGET}-size
DBG=${TARGET}-gdb
OCD=openocd
# Build flags
DEFINITIONS=-DSTM32F40_41xxx -D"assert_param(expr) ((void) 0);"
COMMON_FLAGS=-mcpu=cortex-m4 -mtune=cortex-m4 -mfpu=auto -mthumb -mthumb-interwork -ffunction-sections -fdata-sections -fno-common -fmessage-length=0 -specs=nosys.specs
FPU_FLAGS=-mfloat-abi=hard -mfpu=fpv4-sp-d16
OPTIMIZATION=-g
INC_FLAGS=-Iinc/ -Idriver/inc/
LIB_FLAGS=-Llib/ -larm_cortexM4lf_math
C_FLAGS=${DEFINITIONS} ${COMMON_FLAGS} ${FPU_FLAGS} ${INC_FLAGS} ${LIB_FLAGS} ${OPTIMIZATION} -std=gnu11
CXX_FLAGS=${DEFINITIONS} ${COMMON_FLAGS} ${FPU_FLAGS} ${INC_FLAGS} ${LIB_FLAGS} ${OPTIMIZATION} -std=c++11
LINK_FLAGS=${DEFINITIONS} ${COMMON_FLAGS} ${FPU_FLAGS} ${INC_FLAGS} ${LIB_FLAGS} ${OPTIMIZATION} -Wl,-gc-sections,--print-memory-usage -T ${LINK_SCRIPT}
# Build rules
${PROJECT}.bin : ${PROJECT}.elf
${OBJCOPY} -Obinary $< $@
${PROJECT}.elf : ${OBJ}
${CC} ${LINK_FLAGS} -o $@ ${OBJ}
%.o : %.s
${ASM} ${COMMON_FLAGS} -c -o $@ $<
%.o : %.c
${CC} ${C_FLAGS} -c -o $@ $<
%.o : %.cpp
${CXX} ${CXX_FLAGS} -c -o $@ $<
clean:
rm -rf ${OBJ}
flash: ${PROJECT}.bin
${OCD} -f ${OCD_SCRIPT}
OpenOCD 脚本
接下来要调用 OpenOCD 连接调试器,脚本也很简单。
# change stlink-v2 to either jlink, cmsis-dap to match with
# your debugger hardware
source [find interface/stlink-v2.cfg]
# use serial wire debug mode
transport select hla_swd
# increase working area to 64KB
set WORKAREASIZE 0x10000
# load device config
source [find target/stm32f4x.cfg]
# download firmware to user flash
program skylab.elf 0x08000000
# The gdb port will be opend at 3333
arm semihosting enable
构建和调试
编辑 .vscode/tasks.json
, 添加构建工作:
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"type": "shell",
"command": "make",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [ "$gcc" ]
},
]
}
编辑 .vscode/launch.json
, 添加启动项目:
{
"version": "0.2.0",
"configurations": [
{
"cwd": "${workspaceRoot}",
"executable": "./skylab.elf",
"name": "Debug Microcontroller",
"request": "launch",
"type": "cortex-debug",
"servertype": "openocd",
"configFiles": [ "${workspaceRoot}/STM32F407.cfg" ],
"preLaunchTask": "build",
"svdFile": "${workspaceRoot}/STM32F407.svd",
"interface": "swd",
"device": "STM32F407VE",
}
]
}
接下来即可按 f5
启动构建和调试了。当然,我们到现在还没写链接脚本,算是啥都没做,所以构建可能会失败。
下一节我们将会说明链接是怎么回事,以及如何从参考手册中读出如何编写链接脚本。
Your comments will be submitted to a human moderator and will only be shown publicly after approval. The moderator reserves the full right to not approve any comment without reason. Please be civil.