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
}
运行与验证
- 将上述文件按目录结构放置,在项目根目录执行:
make
- 通过条件:终端无报错,且生成以下文件:
build/firmware.elf # ELF调试文件
build/firmware.bin # 可烧录的二进制文件
- 验证bin文件有效性(预期输出,非实测):
arm-none-eabi-size build/firmware.elf
# 预期输出示例(数值取决于编译结果):
# text data bss dec hex filename
# xxx xx xx xxx xxx build/firmware.elf
- 验证反汇编(可选):
arm-none-eabi-objdump -d build/firmware.elf | head -20
应能看到main和gpio_init的ARM Thumb指令。
- 清理验证:执行
make clean后build目录被删除,再次make可完整重建。
关键说明
-MMD -MP自动生成头文件依赖,修改common.h后make会重新编译受影响文件。- 链接脚本中
.data段使用AT> FLASH将初始化数据存储在Flash,运行时由启动代码拷贝到RAM(本例未包含启动代码,仅演示链接过程)。 - 若需烧录,使用
stm32flash或OpenOCD将firmware.bin写入0x08000000即可。