📅 2024-09-04
🔄 2024-09-04
⌚ Reading time: 4 min
(2024.09.04 创建,2026.01.23 完善。)
在 STM32 开发领域,Keil、IAR 这样的 IDE 几乎是默认选择。
在 Ubuntu 环境下,直接使用 arm-none-eabi-gcc 来开发 STM32,看起来多少有点“吃力不讨好”。
但真正掌握这套流程的意义,远不止是“不用 IDE 写单片机程序”。
用 GCC 裸机开发这件事背后隐含着一个非常重要的能力:
拿到一颗全新的、基于 ARM Cortex-M 内核的芯片,只有芯片手册,甚至连官方 IDE 都还没准备好的时候,你依然能把它跑起来。只要掌握了
那么芯片是否“被官方支持”并不重要。
更现实的一层意义,很多大型“裸机”项目比如 u-boot,甚至 linux kernel,各类 SoC BSP,这些项目开发、调试并不依赖图形化工具。而 STM32 裸机开发,恰好是理解这一整套工具链的最小项目。从一颗 Cortex-M3 芯片开始,去理解“代码是如何真正跑到 CPU 上的”。
我的环境
int main()
{
return 0;
}
这是一个合法的 C 程序,但它还不是一个能在 MCU 上运行的程序。CPU 上电以后,并不知道什么是 main()。
上电之后,以 cortex-m3 为例,上电或复位后,从 0x0000_0000 ( 一般是从 0x0800_0000 内部 flash 映射过去的)取 4Byte 到 MSP,然后取再往后取 4Byte 到 PC。即跳转 Reset Handler,这是硬件与程序员的约定,所有的 CPU 都有类似的约定。
为了让 main 函数能在 CPU 上执行,还需要做到芯片通电后从 Reset Handler 跳转到 main 入口。
一段最小启动代码(Reset Handler)
__attribute__((naked, noreturn)) void _reset(void)
{
extern long _sbss, _ebss, _sdata, _edata, _sidata;
// 清零 .bss
for (long *dst = &_sbss; dst < &_ebss; dst++)
*dst = 0;
// 初始化 .data
for (long *dst = &_sdata, *src = &_sidata; dst < &_edata;)
*dst++ = *src++;
main(); // Call main()
for (;;) (void) 0; // Infinite loop in the case if main() returns
}
这段代码建立 C 运行时环境,初始化全局变量,调用 main。
本质上就是 crt0 的最小实现。
此外还需要一个 0x0000_0000 处的布局
extern void _estack(void);
__attribute__((section(".vectors"))) void (*const tab[16 + 60])(void) = {
_estack, // 初始 SP
_reset // Reset Handler
};
链接脚本是内存布局的最终裁决者,有好多变量都用了 extern ,这些变量在链接脚本内,链接时会计算然后填入。此外有好多修饰也是给连接器看的。
ENTRY(_reset);
MEMORY {
flash(rx) : ORIGIN = 0x08000000, LENGTH = 64k
sram(rwx) : ORIGIN = 0x20000000, LENGTH = 20k /* remaining 64k in a separate address space */
}
_estack = ORIGIN(sram) + LENGTH(sram); /* stack points to end of SRAM */
SECTIONS {
.vectors : { KEEP(*(.vectors)) } > flash
.text : { *(.text*) } > flash
.rodata : { *(.rodata*) } > flash
.data : {
_sdata = .; /* .data section start */
*(.first_data)
*(.data SORT(.data.*))
_edata = .; /* .data section end */
} > sram AT > flash
_sidata = LOADADDR(.data);
.bss : {
_sbss = .; /* .bss section start */
*(.bss SORT(.bss.*) COMMON)
_ebss = .; /* .bss section end */
} > sram
. = ALIGN(8);
_end = .; /* for cmsis_gcc.h */
}
链接脚本控制了代码存放的位置。解决了三个问题
最后,如何构建的问题,如何生成可执行文件。写一个 makefile
CFLAGS ?= -W -Wall -Wextra -Werror -Wundef -Wshadow -Wdouble-promotion \
-Wformat-truncation -fno-common -Wconversion \
-g3 -Os -ffunction-sections -fdata-sections -I. \
-mcpu=cortex-m3 -mthumb $(EXTRA_CFLAGS)
LDFLAGS ?= -Tlink.ld -nostartfiles -nostdlib --specs nano.specs -lc -lgcc -Wl,--gc-sections -Wl,-Map=$@.map
SOURCES = main.c
ifeq ($(OS),Windows_NT)
RM = cmd /C del /Q /F
else
RM = rm -f
endif
build: firmware.bin
firmware.elf: $(SOURCES)
arm-none-eabi-gcc $(SOURCES) $(CFLAGS) $(LDFLAGS) -o $@
firmware.bin: firmware.elf
arm-none-eabi-objcopy -O binary $< $@
flash: firmware.bin
st-flash --reset write $< 0x8000000
clean:
$(RM) firmware.*
语法倒不是重点,可以让 GPT 帮忙写,主要是思路,所有的 gcc 工具都可以使用,如 readelf、objdump,gdb 。
下载使用 st-link 工具包。这个工具包除了 st-flash 工具还有一个 st-util 工具,这个工具可以启动一个 gdb server 在 4242 端口。然后就可以使用 gdb 工具来调试 STM32 了。
上面的代码下载进去以后是没有任何现象的。想要看看代码是不是真的运行起来了,需要用到调试工具。
load 加载被调试的程序,然后剩下的操作就是 gdb 的操作。从这一刻起,用的已经是和 Linux 内核开发同一套调试方式。
到了 C 语言的领域,那么几乎就可以做任何 c 语言能做到的事情了。先点个灯。点灯要做的是开时钟,设置 GPIO 模式,然后控制引脚寄存器。对着手册去写
struct rcc {
volatile uint32_t CR, CFGR, CIR, APB2RSTR, APB1RSTR, AHBENR,
APB2ENR, APB1ENR, BDCR, CSR;
};
struct gpio {
volatile uint32_t CRL, CRH, IDR, ODR, BSRR, BRR, LCKR;
};
static inline void spin(volatile uint32_t count) {
while (count--) asm("nop");
}
static inline void gpio_write(uint8_t val) {
if (val == 1)
GPIOC->BSRR = 0x20 << 8;
else
GPIOC->BSRR = 0x20 << 24;
}
int main(void) {
//寄存器映射,是所有外设驱动的起点。
struct rcc* rcc = (void *)0x40021000;
struct gpioc* gpioc = (void *)0x40011000;
rcc->APB2ENR |= (1<<4);
gpioc->CRH &= ~(0x0fU << 20); // Clear existing setting
gpioc->CRH |= 0x20 << 16; // Set new mode
for (;;) {
gpio_write(1);
spin(999999);
gpio_write(0);
spin(999999);
}
return 0;
}
功能是可以实现的,但是可读性也是几乎没有的。功能很简单,但这已经是所有 HAL / CMSIS 的原型。原理上是这些原理,真正的想用起来,还有非常多的细节要去考虑。
原理就是这些原理。真正工程化以后,细节会非常多,但不会再神秘。
2026.01.23
ROS2 提供了 micro-ros,但是是以 .a 和一个 .h 的形式,并且要求项目对 cmake 的支持。这里不聊 cmake。
那么还是在 ubuntu 环境下可能会更顺手一些,正好 rt-thread 提供了一些基础设施。
接续裸机开发的思路,开始真正的做一些事情。
安装
# 工具链准备
wget https://developer.arm.com/-/media/files/downloads/gnu/11.2-2022.02/binrel/gcc-arm-11.2-2022.02-x86_64-arm-none-eabi.tar.xz
tar xvf gcc-arm-11.2-2022.02-x86_64-arm-none-eabi.tar.xz
sudo mv gcc-arm-11.2-2022.02-x86_64-arm-none-eabi /opt/
rm gcc-arm-11.2-2022.02-x86_64-arm-none-eabi.tar.xz
# rt-thread 需要的工具
sudo apt install -y scons
sudo apt install -y pip
pip install kconfiglib
pip install tqdm
pip install scons==4.5.2
echo "source ~/.env/env.sh" >> ~/.bashrc
打开一个 rtt 的 bsp,scons –meunconfig 之后会自动 clone .env,
source ~/.env/env.sh
之后可以使用 pkgs 相关工具
stm32 的 HAL 库作为软件包,需要 pkgs –update 去拉一下
然后就可以 scons –target=cmake
可以手动在 rtconfig.py 写死,或者用环境变量指定
export RTT_CC=gcc
export RTT_EXEC_PATH=/opt/gcc-arm-11.2-2022.02-x86_64-arm-none-eabi/bin
然后就可以编译了
mkdir build-cmake
cd build-cmake
cmake ..
make -j
关于下载和调试。
使用 jlink
大型项目构建与管理中的一些工具,思路,方法
scons menuconfig与宏定义开关
makefile
Sconscript
SConstruct