4. 编写Makefile构建脚本

任务目标

为嵌入式C项目编写Makefile,实现一条make命令完成编译、链接和生成可烧录的二进制文件(.bin),并支持清理操作。项目包含主程序、外设驱动和公共头文件,目标平台为ARM Cortex-M(以STM32F103为例),使用arm-none-eabi-工具链。

项目结构与依赖

假设项目目录结构如下(本案例直接提供全部源文件内容,无需额外下载):

project/
├── Makefile
├── inc/
│   └── common.h
├── src/
│   ├── main.c
│   └── gpio.c
└── linker/
    └── stm32f103.ld

依赖:arm-none-eabi-gcc、arm-none-eabi-objcopy(安装gcc-arm-none-eabi包即可)。链接脚本使用STM32F103C8的Flash(64KB)/RAM(20KB)布局。

完整实现

以下Makefile实现增量编译、自动头文件依赖、生成ELF与bin文件:

# 工具链
CROSS   := arm-none-eabi-
CC      := $(CROSS)gcc
OBJCOPY := $(CROSS)objcopy
RM      := rm -f

# 目标与目录
TARGET  := firmware
BUILD   := build
SRC_DIR := src
INC_DIR := inc
LINKER  := linker/stm32f103.ld

# 源文件与对象文件
SRCS    := $(wildcard $(SRC_DIR)/*.c)
OBJS    := $(patsubst $(SRC_DIR)/%.c,$(BUILD)/%.o,$(SRCS))
DEPS    := $(OBJS:.o=.d)

# 编译参数
MCU     := cortex-m3
CFLAGS  := -mcpu=$(MCU) -mthumb -Wall -O2 -g
CFLAGS  += -I$(INC_DIR)
LDFLAGS := -T$(LINKER) -nostdlib --specs=nano.specs

# 默认目标
all: $(BUILD)/$(TARGET).bin

# 链接生成ELF
$(BUILD)/$(TARGET).elf: $(OBJS)
	$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)

# 生成bin文件
$(BUILD)/$(TARGET).bin: $(BUILD)/$(TARGET).elf
	$(OBJCOPY) -O binary $< $@

# 编译生成对象文件
$(BUILD)/%.o: $(SRC_DIR)/%.c | $(BUILD)
	$(CC) $(CFLAGS) -MMD -MP -c $< -o $@

# 自动创建build目录
$(BUILD):
	mkdir -p $@

# 清理
clean:
	$(RM) -r $(BUILD)

# 包含自动依赖文件
-include $(DEPS)

.PHONY: all clean

源文件内容

为验证Makefile可独立运行,提供最小但完整的源文件:

/* inc/common.h */
#ifndef COMMON_H
#define COMMON_H
#define REG(addr) (*(volatile unsigned int *)(addr))
#endif

/* src/gpio.c */
#include "common.h"
void gpio_init(void) {
    REG(0x40021018) |= (1 << 4);   /* RCC->APB2ENR 使能GPIOC时钟 */
    REG(0x40011004) |= (1 << 20);  /* GPIOC->CRH  PC13推挽输出 */
}

/* src/main.c */
#include "common.h"
void gpio_init(void);
int main(void) {
    gpio_init();
    while(1) {
        REG(0x4001100C) ^= (1 << 13);  /* GPIOC->ODR 翻转PC13 */
        for(volatile int i = 0; i < 100000; i++);
    }
}
/* 启动入口:为最小演示,直接以main作为入口(真实项目需startup文件) */
void Reset_Handler(void) __attribute__((alias("main")));

链接脚本(stm32f103.ld)

MEMORY
{
  FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K
  RAM (rwx)  : ORIGIN = 0x20000000, LENGTH = 20K
}
SECTIONS
{
  .text : { *(.text*) } > FLASH
  .rodata : { *(.rodata*) } > FLASH
  .data : { *(.data*) } > RAM AT> FLASH
  .bss : { *(.bss*) } > RAM
}

运行与验证

  1. 将上述文件按目录结构放置,在项目根目录执行:
make
  1. 通过条件:终端无报错,且生成以下文件:
build/firmware.elf   # ELF调试文件
build/firmware.bin   # 可烧录的二进制文件
  1. 验证bin文件有效性(预期输出,非实测):
arm-none-eabi-size build/firmware.elf
# 预期输出示例(数值取决于编译结果):
#   text    data     bss     dec     hex filename
#    xxx      xx      xx     xxx     xxx build/firmware.elf
  1. 验证反汇编(可选):
arm-none-eabi-objdump -d build/firmware.elf | head -20

应能看到main和gpio_init的ARM Thumb指令。

  1. 清理验证:执行make clean后build目录被删除,再次make可完整重建。

关键说明

  • -MMD -MP自动生成头文件依赖,修改common.h后make会重新编译受影响文件。
  • 链接脚本中.data段使用AT> FLASH将初始化数据存储在Flash,运行时由启动代码拷贝到RAM(本例未包含启动代码,仅演示链接过程)。
  • 若需烧录,使用stm32flash或OpenOCD将firmware.bin写入0x08000000即可。