3. 开发环境搭建:安装ARM GCC工具链、配置Makefile、编写第一个LED闪烁程序验证环境
好,到了真正动手的时候了。
前面两章我们把Bootloader的概念和启动流程理了一遍。说实话,光看理论容易犯困。我当年刚学的时候也是这样,看了三天文档,结果打开终端还是一脸懵。所以这一章,咱们直接搭环境、写代码、点灯。
环境搭好了,后面所有实验才有根。你想想看,如果连编译器都没装对,后面调试Bootloader简直就是给自己挖坑。
3.1 安装ARM GCC工具链
嵌入式开发,编译器是第一步。我们用的芯片是ARM Cortex-M系列,所以需要ARM交叉编译工具链。说白了,就是在PC上编译出能在ARM芯片上跑的机器码。
我个人习惯用ARM官方的GNU Arm Embedded Toolchain。这个工具链是免费的,支持ARMv6-M、ARMv7-M、ARMv8-M架构,覆盖了Cortex-M0到M7全系列。
3.1.1 下载与安装
去ARM官网下载gcc-arm-none-eabi的压缩包。我建议选最新稳定版,别追太新的,容易踩坑。
推荐版本:gcc-arm-none-eabi-10.3-2021.10
这个版本我用了两年多,没出过兼容性问题。
安装步骤很简单,解压就行。但要注意路径里别有中文和空格。
# Linux / macOS
tar -xjf gcc-arm-none-eabi-10.3-2021.10-x86_64-linux.tar.bz2
sudo mv gcc-arm-none-eabi-10.3-2021.10 /opt/
# 配置环境变量(加到 ~/.bashrc 或 ~/.zshrc)
export PATH=$PATH:/opt/gcc-arm-none-eabi-10.3-2021.10/bin
Windows用户我建议用WSL,或者直接装ARM官方提供的exe安装包,它会自动配环境变量。
3.1.2 验证安装
装完别急着写代码。先验证一下工具链能不能用。
arm-none-eabi-gcc --version
如果看到版本号输出,说明装好了。我曾经遇到过一个问题:装完以后终端找不到命令。后来发现是环境变量没生效,重新source一下就好了。
小技巧:用 which arm-none-eabi-gcc 查看编译器路径,确认是不是你装的那个版本。
3.2 配置Makefile
有了编译器,还得有个构建系统。我选Makefile,原因很简单:轻量、可控、嵌入式圈子里大家都在用。
你想想看,一个Bootloader工程,少则十几个文件,多则上百个。每次改完代码都手动敲编译命令?那太原始了。Makefile就是帮你自动化这件事的。
3.2.1 工程目录结构
先搭个干净的目录结构。我个人习惯这样组织:
bootloader_project/
├── src/ # 源文件
│ ├── main.c
│ ├── startup.c
│ └── led.c
├── inc/ # 头文件
│ ├── main.h
│ └── led.h
├── ld/ # 链接脚本
│ └── stm32f103.ld
├── obj/ # 编译中间文件
└── Makefile # 构建脚本
这个结构不复杂,但够用。后面加文件也方便。
3.2.2 编写Makefile
好,核心来了。一个能用的Makefile长这样:
# 工具链定义
CC = arm-none-eabi-gcc
LD = arm-none-eabi-ld
OBJCOPY = arm-none-eabi-objcopy
SIZE = arm-none-eabi-size
# 编译选项
CFLAGS = -mcpu=cortex-m3 -mthumb -Wall -O2 -g
CFLAGS += -I./inc
# 链接选项
LDFLAGS = -T ./ld/stm32f103.ld -nostartfiles
# 源文件
SRCS = $(wildcard src/*.c)
OBJS = $(SRCS:src/%.c=obj/%.o)
# 目标文件
TARGET = bootloader
# 默认目标
all: $(TARGET).elf $(TARGET).bin
# 编译规则
obj/%.o: src/%.c
@mkdir -p obj
$(CC) $(CFLAGS) -c $< -o $@
# 链接
$(TARGET).elf: $(OBJS)
$(LD) $(LDFLAGS) $^ -o $@
$(SIZE) $@
# 生成bin文件
$(TARGET).bin: $(TARGET).elf
$(OBJCOPY) -O binary $< $@
# 清理
clean:
rm -rf obj *.elf *.bin
.PHONY: all clean
这段代码我解释一下关键点:
-mcpu=cortex-m3指定CPU内核,我用的是STM32F103,Cortex-M3架构。你如果换芯片,记得改这个参数。-mthumb指定Thumb指令集。ARM Cortex-M只支持Thumb模式,别写成ARM模式。-nostartfiles告诉链接器不要自动加启动文件。Bootloader的启动我们自己写。$(wildcard src/*.c)自动扫描src目录下的所有.c文件。加新文件不用改Makefile。
注意:Makefile里的缩进必须用Tab,不能用空格。我当年因为这个坑编译报错,查了半小时才发现是缩进问题。
3.3 编写第一个LED闪烁程序
环境搭好了,Makefile写好了。现在写个最简单的程序验证一下——让LED闪烁。
为什么选LED?因为这是最直观的反馈。代码烧进去,灯亮了,说明编译器、链接器、烧录工具全都没问题。如果灯不亮,问题出在哪一目了然。
3.3.1 硬件准备
我用的是STM32F103最小系统板。LED接在PC13引脚(板上自带的那颗)。
| 元件 | 说明 |
|---|---|
| MCU | STM32F103C8T6 |
| LED引脚 | PC13(高电平灭,低电平亮) |
| 调试器 | ST-Link V2 |
3.3.2 代码实现
先写寄存器版本的,不依赖HAL库。这样你能看到最底层的东西在干什么。
src/main.c
#include "main.h"
// 寄存器地址定义
#define RCC_APB2ENR (*(volatile unsigned int *)0x40021018)
#define GPIOC_CRH (*(volatile unsigned int *)0x40011004)
#define GPIOC_ODR (*(volatile unsigned int *)0x4001100C)
// 简单延时函数
void delay(volatile unsigned int count) {
while (count--);
}
int main(void) {
// 使能GPIOC时钟
RCC_APB2ENR |= (1 << 4);
// 配置PC13为推挽输出,50MHz
GPIOC_CRH &= ~(0xF << 20);
GPIOC_CRH |= (0x3 << 20);
while (1) {
// 点亮LED(PC13输出低电平)
GPIOC_ODR &= ~(1 << 13);
delay(500000);
// 熄灭LED(PC13输出高电平)
GPIOC_ODR |= (1 << 13);
delay(500000);
}
}
src/startup.c
// 最小启动文件
void Reset_Handler(void) __attribute__((interrupt));
extern int main(void);
// 中断向量表
unsigned int *vectors[] __attribute__((section(".vectors"))) = {
(unsigned int *)0x20001000, // 栈顶地址
(unsigned int *)Reset_Handler // 复位向量
};
void Reset_Handler(void) {
main();
while (1);
}
ld/stm32f103.ld
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K
RAM (rw) : ORIGIN = 0x20000000, LENGTH = 20K
}
SECTIONS
{
.vectors : {
*(.vectors)
} > FLASH
.text : {
*(.text)
} > FLASH
.data : {
*(.data)
} > RAM AT> FLASH
.bss : {
*(.bss)
} > RAM
}
3.3.3 编译与烧录
在工程根目录执行:
make
如果一切顺利,你会看到:
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -Wall -O2 -g -I./inc -c src/main.c -o obj/main.o
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -Wall -O2 -g -I./inc -c src/startup.c -o obj/startup.o
arm-none-eabi-ld -T ./ld/stm32f103.ld -nostartfiles obj/main.o obj/startup.o -o bootloader.elf
arm-none-eabi-size bootloader.elf
text data bss dec hex filename
168 0 0 168 a8 bootloader.elf
arm-none-eabi-objcopy -O binary bootloader.elf bootloader.bin
看到168字节的text段了吗?一个最小Bootloader的雏形就这么点代码。
烧录用ST-Link:
st-flash write bootloader.bin 0x08000000
验证结果:如果LED以大约1Hz的频率闪烁,恭喜你——开发环境搭建成功!
3.4 知识体系总览
这一章的内容,我用一张图帮你理清楚:
3.5 常见问题与避坑
这一路走过来,我踩过不少坑。挑几个典型的跟你说说:
- 编译器找不到头文件:检查Makefile里的
-I./inc路径对不对。我习惯用绝对路径,省得出错。 - 链接报错:多半是链接脚本里的内存地址写错了。STM32F103的Flash从0x08000000开始,RAM从0x20000000开始。别搞混。
- 烧录后没反应:先检查硬件连接。ST-Link的SWDIO、SWCLK、GND三根线必须接对。我遇到过SWDIO和SWCLK接反的情况,折腾了一下午。
- LED不闪烁:看看GPIO配置对不对。PC13是低电平点亮,别写成高电平了。
调试建议:如果灯不亮,先用逻辑分析仪抓一下PC13的波形。有方波说明程序在跑,问题在硬件;没波形说明程序根本没执行到那里。
好,环境搭好了,灯也亮了。后面我们就可以正式开始Bootloader的编写了。