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章 开发环境搭建知识体系 目标:验证嵌入式开发环境 安装ARM GCC工具链 配置Makefile 编写LED闪烁程序 下载解压 配置环境变量 验证版本 定义工具链变量 设置编译/链接选项 自动化构建规则 寄存器配置 启动文件 链接脚本 输出:bootloader.bin + LED闪烁 三个环节环环相扣,任何一个出错都会导致LED不亮。建议按顺序逐一验证。

3.5 常见问题与避坑

这一路走过来,我踩过不少坑。挑几个典型的跟你说说:

  • 编译器找不到头文件:检查Makefile里的 -I./inc 路径对不对。我习惯用绝对路径,省得出错。
  • 链接报错:多半是链接脚本里的内存地址写错了。STM32F103的Flash从0x08000000开始,RAM从0x20000000开始。别搞混。
  • 烧录后没反应:先检查硬件连接。ST-Link的SWDIO、SWCLK、GND三根线必须接对。我遇到过SWDIO和SWCLK接反的情况,折腾了一下午。
  • LED不闪烁:看看GPIO配置对不对。PC13是低电平点亮,别写成高电平了。

调试建议:如果灯不亮,先用逻辑分析仪抓一下PC13的波形。有方波说明程序在跑,问题在硬件;没波形说明程序根本没执行到那里。

好,环境搭好了,灯也亮了。后面我们就可以正式开始Bootloader的编写了。