从零搭建Cortex-M4工程

考察一个嵌入式工程师能力的重要指标就是,是否具备架构大型工程的能力。作为一个合格的嵌入式工程师,在职业生涯中一定有需要架构一个全新的工程。
废话不多说,接下来我们以一款Cortex-M4的MUC为例,给大家展示如何搭建一个的嵌入式系统的工程框架。
1. 环境
(1) 硬件:STM32F407核心板,只有基础的供电和晶振电路,没有外围器件;
选择这块板子作为例子有三个重要的原因:
其一,意法的MCU应用很广泛,比较具有代表性;
其二,官网支持比较好,开发资料很容易找;
其三,也是最重要的,我手里刚好有一块;
(2) 工具链:gcc-arm-none-eabi
我用过很多嵌入式开发环境,ADS、keil、IAR等等,但开发32位单片机最喜欢的还是GNU的这套工具链,而且使用这套工具链还有一个重要的原因,相比其他开发环境,由于GNU与脚本可以紧密结合,使得搭建持续集成服务器会容易得多。
(3) 第三方库:STM32Cube_FW_F4_V1.14.0
意法官网提供的驱动包:
注:启动代码一定要使用gcc目录下的:
Drivers\CMSIS\Device\ST\STM32F4xx\Source\Templates\gcc\startup_stm32f407xx.s
2. 目录划分
根据嵌入式系统层次顶层划分了五个目录:
|– apps # 应用层程序路径
|– boot # 启动代码路径
|– build # 编译根目录
   |– bin # bin文件输出路径
   |– objs # obj文件输出路径
|– drivers # 驱动层程序路径
|– libs # 第三方库(在本例中存放意法官网的驱动库文件)
   |– inc 库的头文件
   |– src 库的源文件
最终工程目录划分如下图所示:
.
├── apps
│   ├── main.c
│   └── Makefile
├── boot
│   ├── Makefile
│   └── startup.s
├── build
│   ├── bin
│   ├── Makefile
│   ├── objs
│   └── stm32f407vet6.ld
├── drivers
│   ├── inc
│   ├── Makefile
│   └── src
├── libs
│   ├── inc
│   ├── Makefile
│   └── src
└── Makefile
3. 编写Makefile
对于多文件夹的工程,不仅需要在根目录编写Makefile,而且需要为每个子文件夹单独编写Makefile文件,并在根目录的文件夹中遍历所有子文件夹并调用其中的Makefile。
(1) 根目录Makefile
对于循环调用子文件夹中的Makefile,一个简单的办法是利用awk命令和make命令的-C参数:
SUBDIRS =$(shell ls -l | grep ^d | awk ‘{if($$9 != “build”) print $$9}’)
$(SUBDIRS):ECHO
make -C $@
ECHO:
@echo $(SUBDIRS)
其中:
“grep ^d”表示ls -l命令后首行以‘d’字母开头,既文件夹;
“$$9”表示第九列的值
由于我们需要在build文件夹中进行最后的链接工作,所以在遍历子目录的时候排除‘build’文件夹。
除此之外,我们还需要记录几个固定的目录:
ROOT_DIR=$(shell pwd)
OBJS_DIR=build/obj
BIN_DIR =build/bin
头文件路径:
INCS+=-I$(ROOT_DIR)/stm32f407_lib/inc
INCS+=-I$(ROOT_DIR)/drivers/inc
编译选项:
CFLAGS+= -mcpu=cortex-m4 -mthumb -Wall
CFLAGS+= -mfloat-abi=hard -mfpu=fpv4-sp-d16
CFLAGS+= -Os
CFLAGS+= -ffunction-sections -fdata-sections
链接选项:
LFLAGS+= -mcpu=cortex-m4 -mthumb
LFLAGS+= -mfloat-abi=hard -mfpu=fpv4-sp-d16
LFLAGS+= -Wl,–gc-sections
最终将这些宏导出,以供子目录编译时使用:
export BIN OBJS_DIR BIN_DIR ROOT_DIR DEFS INCS CFLAGS LFLAGS
(2) 子目录Makefile
子目录中Makefile文件的编写通常需要使用到三个十分常用的Makefile函数:wildcard、notdir和patsubst。
其中:
wildcard:用于搜索路径中的文件,并以空格间隔每个文件从而形成列表的形式返回
例如,本工程drives路径下的src目录中有两个C文件,那么编写如下Makefile:
CUR_SOURCE=$(wildcard *.c ./src/*.c)
all: $(CUR_SOURCE)
@echo $(CUR_SOURCE)
执行后输出结果为:
./src/key.c ./src/led.c
notdir:用于去掉列表中的文件夹路径
还以上面的Makefile为例,如果再增加一行:
CUR_SOURCE=$(wildcard *.c ./src/*.c)
NOT_SOURCE=$(notdir $(CUR_SOURCE))
all: $(CUR_SOURCE)
@echo $(CUR_SOURCE)
@echo $(NOT_SOURCE)
执行后输出结果为:
./src/key.c ./src/led.c
key.c led.c
patsubst:用于匹配替换
在上文的Makefile中继续增加一行:
CUR_SOURCE=$(wildcard *.c ./src/*.c)
NOT_SOURCE=$(notdir $(CUR_SOURCE))
PAT_SOURCE=$(patsubst %.c, %.o, $(NOT_SOURCE))
all: $(CUR_SOURCE)
@echo $(CUR_SOURCE)
@echo $(NOT_SOURCE)
@echo $(PAT_SOURCE)
最终输出结果为:
./src/key.c ./src/led.c
key.c led.c
key.o led.o
对于子目录中不存在下级目录的情况,我们可以简单地编写Makefile文件如下:
CUR_SOURCE =$(wildcard *.c)
CUR_OBJS =$(patsubst %.c, %.o, $(CUR_SOURCE))
all:$(CUR_OBJS)
$(CUR_OBJS):%.o:%.c
@echo $^
@$(CC) $(CFLAGS) $(DEFS) $(INCS) -c $^ -o $(ROOT_DIR)/$(OBJS_DIR)/$@
而对于子目录中还有下级src路径的情况,可以简单地编写Makefile如下:
CUR_SOURCE=$(wildcard ./src/*.c)
CUR_OBJS=$(patsubst %.c, %.o, $(notdir $(CUR_SOURCE)))
all:$(CUR_OBJS)
$(CUR_OBJS):%.o:$(DIR_SRC)/%.c
@echo $^
@$(CC) $(CFLAGS) $(DEFS) $(INCS) -c $^ -o $(ROOT_DIR)/$(OBJS_DIR)/$@
(3) build目录Makefile
最终所有子目录中的程序都编译为.o文件后,需要在build文件夹中进行链接,链接的过程分为两步:
生成elf格式的文件:
$(ROOT_DIR)/$(BIN_DIR)/$(OBJ_ELF):$(OBJS)
@$(CC) $(LFLAGS) -Tstm32.ld -o $@ $^
转为可以直接烧写到Flash中的bin文件:
$(ROOT_DIR)/$(BIN_DIR)/$(BIN):$(ROOT_DIR)/$(BIN_DIR)/$(OBJ_ELF)
@arm-none-eabi-objcopy -O binary -S $< $@
4. 链接脚本
意法的官方库中提供了链接脚本的样例,但在具体的工程中需要根据不同的MCU以及RAM、Flash划分进行改写,例如,本例中使用的STM32F407后缀名为‘VET6’具有512K Flash,192K RAM,则修改:
/* Highest address of the user mode stack */
_estack = 0x20020000;    /* end of RAM */
/* Specify the memory areas */
MEMORY
{
FLASH (rx)      : ORIGIN = 0x8000000, LENGTH = 512K
RAM (xrw)      : ORIGIN = 0x20000000, LENGTH = 128K
CCMRAM (rw)      : ORIGIN = 0x10000000, LENGTH = 64K
}

发表评论