【ARM Cortex-M 开发实战指南(入门篇)】第4章 Cortex-M新建工程及配置(标准版)

Cortex-M的开发环境有很多种,可以使用集成开发环境(IED),比如Keil,IAR等;也可使用GCC,使用GCC编译需要编写Makefile,比较麻烦,但是该方式能深入理解编译过程。另外ST开发了很多工具,也有自己的IDE,笔者不打算使用,但是笔者会使用STM32CubeMX软件开发STM32,这部分将在下一章讲解,本文将基于Keil和GCC开发,关于开发环境的搭建请参看上一章,笔者这里就不在赘述了。

笔者本文将以STM32为例,为了方便学习,笔者也会给出GD32和CH32的实例,请自行获取学习。
笔者使用的开发板是STM32F103ZET6。

4.1 Keil新建工程与配置

4.1.1文件准备

1.新建文件夹

新建STM32_Project目录,在STM32_Project文件夹下,我们新建7个文件夹,分别为CMSIS、FWLib、Listing、Output、Project、Readme、User。CMSIS 用来存放库为我们自带的启动文件和一些 Cortex-M系列的通用文件,CMSIS文件里存放的文件适合任何Cortex-M内核的单片机,CMSIS 的 缩写为:Cortex Microcontroller Software Interface Standard,是 ARM Cortex 微控制器软件接口标准,是 ARM 公司为芯片厂商提供的一套通用的且独立于芯片厂商 的处理器软件接口;FWlib 用来存放芯片厂家的库文件里面的 inc 和 src 这两个文件,这两个文件包含了芯片上的所有驱动。Listing 用来存放一些编译过程中产生的文件。Output用来保存软件编译后输出的文件。User用来存放工程文件和用户代码,包括主函数main.c。另外,再把文件名为keilkilll的文件放到STM32_Project文件夹用来清除不必要的文件。新建文件夹如下图所示:

LCfjmR.md.png

2.添加文件

在STM32F10x_StdPeriph_Lib_V3.5.0目录下

 STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\STM32F10x_StdPeriph_Driver 的 inc 跟 src 这两个文件夹拷贝到 STM32_Project\FWlib 文件夹中。

LChyH1.md.png

 STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\startup\arm 的全部文件拷贝到 STM32_Project\CMSIS\startup(需先在 CMSIS 新建好 startup 文件夹)文件夹下。这些是用汇编写的启动文件。开发板用的 CPU 是 STM32F103ZE,V表示100脚,E = 512K字节的闪存存储器;其中 512KFlash,属于大容量的,所以等下我们把startup_stm32f10x_hd.s添加到我们的工程中。根据 ST 的官方资料:Flash 在 16 ~32 Kbytes 为小容量,64 ~128 Kbytes为 中容量,256 ~512 Kbytes为大容量,不同大小的 Flash 对应的启动文件不一 样,这点要注意。

STM32 各类产品对应的启动文件后缀名含义。

LChIud.jpg

 STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\CoreSupport 的 core_cm3.c 和 core_cm3.h 也拷贝到 STM32_Project\CMSIS 文件夹下。

 STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x 的stm32f10x.h、system_stm32f10x.c、system_stm32f10x.h拷贝到STM32_Project\CMSIS 文件夹下。

LChOC8.md.png

 STM32F10x_StdPeriph_Lib_V3.5.0\Project\STM32F10x_StdPeriph_Template下的 main.c、stm32f10x_conf.h、 stm32f10x_it.h、 stm32f10x_it.c 、system_stm32f10x.c 拷贝到STM32_Project\User目录下,再在STM32_Project\User目录下新建YS_STM32,其中YS_STM32用来存放用户自定义文件。

LC438O.md.png

4.1.2新建的MDK 工程

4.1.2.1新建工程

1.新建工程

启动Keil 5软件,在工具栏 Project->New μVision Project…新建我们的工程文件,我们将新建的工程文件保存在STM32_Project\Project文件夹下,文件名取为YS_STM32,名字可以随便取,点击保存。

LChzuj.md.png

2.选择芯片
新建工程后,会弹出窗口,这让我们选择片型号,我们用的芯片是 ST 公司的STM32F103ZE,有64K SRAM,512K Flash,属于高集成度的芯片。按如下选择即可。

LC4trd.md.png

接下来会弹出工程管理器,这里可以选择启动文件和CMSIS,这里就不选择了,我们这里用的是 ST 的库,库文件里面也自带了这一份启动代码,所以为了保持库的完整性,我们就不需要开发环境为我们自带的启动代码了,稍后我们自己手动添加。

LC4RZn.md.png

3.工程管理
点击Keil的文件管理器,将 Target 改为 YS_STM32(不改也行),新建五个组,分别命名为STARTUP、FWlib、CMSIS、USER、Readme。STARTUP 从名字就可以看得出我们是用它来放我们的启动代码的,FWlib 用来存放库文件,CMSIS 用来存放 M系列单片机通用的文件,USER用来存放用户自定义的应用程序,Readme存放工程文档。然后添加相应文件。

LC4fI0.md.png

修改main.c的代码。

/**
  ******************************************************************************
  * @file                main.c
  * @author              BruceOu
  * @lib version         V3.5.0
  * @version             V1.0
  * @date                2021-02-06
  * @blog                https://blog.bruceou.cn/
  * @Official Accounts   嵌入式实验楼
  * @brief               STM32F103ZET6工程模板
  ******************************************************************************
  */
/* Includes*********************************************************************/
#include "./LED/stm32f103_led.h"

/*延时函数*/
void Delay(u32 xms); 

/**
  * @brief     主函数
  * @param     None
  * @retval    int
  */
int main(void)
{   
    /* LED 初始化 */
    LED_GPIO_Config();   

    while(1)
    {
        // ODR GPIOB
        // 15 14 13 12     11 10 9 8    7 6 5 4    3 2 1 0
        // 0  0  0  0      0  0  0 0    0 0 0 0    0 0 0 0 (复位值)
        // 1  1  1  1      1  1  1 1    1 1 1 1    1 1 1 0
        GPIOB->ODR = 0XFFFE;         //低电平,GPIOB0(LED1)灯灭
        Delay(0x4FFFFF);
        GPIOB->ODR = 0XFFFF;     //高电平,GPIOB0(LED1)灯亮

        Delay(0x4FFFFF); 
    }
}

/**
  * @brief  延时函数
  * @param  
            xms 延时长度
  * @retval None
  */
void Delay( u32 xms)    
{
    //for(; nCount != 0; nCount--);(方法一)
    while(xms--);//(方法二)
}

/**********************************END OF FILE*******************************/

为了方便观察现象,这里写了个控制LED的实例,具体如何配置将在后文详细,当然啦,如果有一定基础,直接根据自己的板子修改GPIO的即可。

LC4XIx.md.png

4.1.2.2配置 MDK 的配置选项

点击工具栏中的魔术棒按钮

LC4zRO.md.png

在弹出来的窗口中选中Output点击 Select Folder for Objects… 设置编译后输出文件保存的位置(放在STM32_Project\Output文件夹下)。同时把 Create HEX File 和 Browse information 这两个选项框也选上。

LC5Aot.md.png

同样在 Listing 这个选项卡中,我们也点击 Select Folder listings…定位到模板中的 Listing 文件夹。
在C/C++选项卡,在 Define 里面输入添加 USE_STDPERIPH_DRIVER,STM32F10X_HD。添加 USE_STDPERIPH_DRIVER 是为了屏蔽编译器的默认搜索路径,转而使用我们添加到工程中的 ST 的库,添加 STM32F10X_HD 是因为我们用的芯片是大容量的,添加了STM32F10X_HD 这个宏之后,库文件里面为大容量定义的寄存器我们就可以用了。芯片是小或中容量的时候宏要换成STM32F10X_LD 或者 STM32F10X_MD。其实不管是什么容量的,我们只要添加上STM32F10X_HD 这个宏即可,当你用小或者中容量的芯片时,那些为大容量定义的寄存器我不去访问就是了,反正也访问不了,关于这两个宏的详细解释参看后文的小贴士。在 Include Paths 栏点击,在这里添加库文件的搜索路径,这样就可以屏蔽掉默认的搜索路径。

LC5GF0.md.png

LC5dOJ.md.png

4.1.2.3硬件调试配置

接着以上操作,这个工程默认的是软件仿真,如果开发板要用 J-LINK (ST-LINK同理)调试的话,还需要在 开发环境中做如下修改。实际上,我们开发程序的时候 80%都是在硬件上调试的。

LC5yY6.md.png

然后设置下Port。

LC5WOH.md.png

一般都是SW接口,到此,常用的配置就完成了。

最后再来编译下。

LC540A.md.png

出现一下信息表示编译通过。

LC5okt.md.png

接下来就是可进行程序烧写和验证了。
当然啦,能下载的前提是需要安装J-link等设备,也是可以使用其他下载方式,后面的章节会详细描述。

4.2 GCC环境新建工程与配置

笔者已经在《STM32开发环境搭建》系列文章详细介绍了该方式,但是笔者使用STM32CubeMX生成的工程,Makefile也是STM32CubeMX生成的,笔者接下来要介绍如何在标准库的基础上使用GCC开发。

在Keil工程的基础上删除Listing、Output、Project目录,在顶层目录添加Makefile和链接文件STM32F103ZETx_FLASH.ld。

Makefile

##########################################################################################################################
# File automatically-generated by tool: [projectgenerator] version: [3.15.2] date: [Fri Apr 08 20:05:41 CST 2022]
##########################################################################################################################

# ------------------------------------------------
# Generic Makefile (based on gcc)
#
# ChangeLog :
#   2022-04-08 - first version
# ------------------------------------------------

######################################
# target
######################################
TARGET = YS_STM32

######################################
# building variables
######################################
# debug build?
DEBUG = 1
# optimization
OPT = -O0

#######################################
# paths
#######################################
# Build path
BUILD_DIR = build

######################################
# source
######################################
# C sources
C_SOURCES =  \
CMSIS/system_stm32f10x.c \
User/main.c \
User/stm32f10x_it.c \
FWLib/src/stm32f10x_rcc.c \
FWLib/src/stm32f10x_gpio.c \
User/YS_STM32/LED/stm32f103_led.c

# ASM sources
ASM_SOURCES =  \
CMSIS/startup/startup_stm32f10x_hd.s

#######################################
# binaries
#######################################
PREFIX = arm-none-eabi-
# The gcc compiler bin path can be either defined in make command via GCC_PATH variable (> make GCC_PATH=xxx)
# either it can be added to the PATH environment variable.
ifdef GCC_PATH
CC = $(GCC_PATH)/$(PREFIX)gcc
AS = $(GCC_PATH)/$(PREFIX)gcc -x assembler-with-cpp
CP = $(GCC_PATH)/$(PREFIX)objcopy
SZ = $(GCC_PATH)/$(PREFIX)size
else
CC = $(PREFIX)gcc
AS = $(PREFIX)gcc -x assembler-with-cpp
CP = $(PREFIX)objcopy
SZ = $(PREFIX)size
endif
HEX = $(CP) -O ihex
BIN = $(CP) -O binary -S

#######################################
# CFLAGS
#######################################
# cpu
CPU = -mcpu=cortex-m3

# fpu
# NONE for Cortex-M0/M0+/M3

# float-abi

# mcu
MCU = $(CPU) -mthumb $(FPU) $(FLOAT-ABI)

# macros for gcc
# AS defines
AS_DEFS = 

# C defines
C_DEFS =  \
-DUSE_STDPERIPH_DRIVER \
-DSTM32F10X_HD

# AS includes
AS_INCLUDES = 

# C includes
C_INCLUDES =  \
-ICMSIS \
-IFWLib/inc \
-IUser \
-IUser/YS_STM32

# compile gcc flags
ASFLAGS = $(MCU) $(AS_DEFS) $(AS_INCLUDES) $(OPT) -Wall -fdata-sections -ffunction-sections

CFLAGS = $(MCU) $(C_DEFS) $(C_INCLUDES) $(OPT) -Wall -fdata-sections -ffunction-sections

ifeq ($(DEBUG), 1)
CFLAGS += -g -gdwarf-2
endif

# Generate dependency information
CFLAGS += -MMD -MP -MF"$(@:%.o=%.d)"

#######################################
# LDFLAGS
#######################################
# link script
LDSCRIPT = STM32F103ZETx_FLASH.ld

# libraries
LIBS = -lc -lm -lnosys 
LIBDIR = 
LDFLAGS = $(MCU) -specs=nano.specs -T$(LDSCRIPT) $(LIBDIR) $(LIBS) -Wl,-Map=$(BUILD_DIR)/$(TARGET).map,--cref -Wl,--gc-sections

# default action: build all
all: $(BUILD_DIR)/$(TARGET).elf $(BUILD_DIR)/$(TARGET).hex $(BUILD_DIR)/$(TARGET).bin

#######################################
# build the application
#######################################
# list of objects
OBJECTS = $(addprefix $(BUILD_DIR)/,$(notdir $(C_SOURCES:.c=.o)))
vpath %.c $(sort $(dir $(C_SOURCES)))
# list of ASM program objects
OBJECTS += $(addprefix $(BUILD_DIR)/,$(notdir $(ASM_SOURCES:.s=.o)))
vpath %.s $(sort $(dir $(ASM_SOURCES)))

$(BUILD_DIR)/%.o: %.c Makefile | $(BUILD_DIR) 
    $(CC) -c $(CFLAGS) -Wa,-a,-ad,-alms=$(BUILD_DIR)/$(notdir $(<:.c=.lst)) $< -o $@

$(BUILD_DIR)/%.o: %.s Makefile | $(BUILD_DIR)
    $(AS) -c $(CFLAGS) $< -o $@

$(BUILD_DIR)/$(TARGET).elf: $(OBJECTS) Makefile
    $(CC) $(OBJECTS) $(LDFLAGS) -o $@
    $(SZ) $@

$(BUILD_DIR)/%.hex: $(BUILD_DIR)/%.elf | $(BUILD_DIR)
    $(HEX) $< $@

$(BUILD_DIR)/%.bin: $(BUILD_DIR)/%.elf | $(BUILD_DIR)
    $(BIN) $< $@ 

$(BUILD_DIR):
    mkdir $@        

#######################################
# clean up
#######################################
clean:
    -rm -fR $(BUILD_DIR)

#######################################
# dependencies
#######################################
-include $(wildcard $(BUILD_DIR)/*.d)

# *** EOF ***

STM32F103ZETx_FLASH.ld

/*
******************************************************************************
**

**  File        : LinkerScript.ld
**
**
**  Abstract    : Linker script for STM32F103ZETx series
**                512Kbytes FLASH and 64Kbytes RAM
**
**                Set heap size, stack size and stack location according
**                to application requirements.
**
**                Set memory bank area and size if external memory is used.
**
**  Target      : STMicroelectronics STM32
**
**  Distribution: The file is distributed “as is,” without any warranty
**                of any kind.
**
*****************************************************************************
*/

/* Entry Point */
ENTRY(Reset_Handler)

/* Highest address of the user mode stack */
_estack = 0x20010000;    /* end of RAM */
/* Generate a link error if heap and stack don't fit into RAM */
_Min_Heap_Size = 0x200;      /* required amount of heap  */
_Min_Stack_Size = 0x400; /* required amount of stack */

/* Specify the memory areas */
MEMORY
{
RAM (xrw)      : ORIGIN = 0x20000000, LENGTH = 64K
FLASH (rx)      : ORIGIN = 0x8000000, LENGTH = 512K
}

/* Define output sections */
SECTIONS
{
  /* The startup code goes first into FLASH */
  .isr_vector :
  {
    . = ALIGN(4);
    KEEP(*(.isr_vector)) /* Startup code */
    . = ALIGN(4);
  } >FLASH

  /* The program code and other data goes into FLASH */
  .text :
  {
    . = ALIGN(4);
    *(.text)           /* .text sections (code) */
    *(.text*)          /* .text* sections (code) */
    *(.glue_7)         /* glue arm to thumb code */
    *(.glue_7t)        /* glue thumb to arm code */
    *(.eh_frame)

    KEEP (*(.init))
    KEEP (*(.fini))

    . = ALIGN(4);
    _etext = .;        /* define a global symbols at end of code */
  } >FLASH

  /* Constant data goes into FLASH */
  .rodata :
  {
    . = ALIGN(4);
    *(.rodata)         /* .rodata sections (constants, strings, etc.) */
    *(.rodata*)        /* .rodata* sections (constants, strings, etc.) */
    . = ALIGN(4);
  } >FLASH

  .ARM.extab   : { *(.ARM.extab* .gnu.linkonce.armextab.*) } >FLASH
  .ARM : {
    __exidx_start = .;
    *(.ARM.exidx*)
    __exidx_end = .;
  } >FLASH

  .preinit_array     :
  {
    PROVIDE_HIDDEN (__preinit_array_start = .);
    KEEP (*(.preinit_array*))
    PROVIDE_HIDDEN (__preinit_array_end = .);
  } >FLASH
  .init_array :
  {
    PROVIDE_HIDDEN (__init_array_start = .);
    KEEP (*(SORT(.init_array.*)))
    KEEP (*(.init_array*))
    PROVIDE_HIDDEN (__init_array_end = .);
  } >FLASH
  .fini_array :
  {
    PROVIDE_HIDDEN (__fini_array_start = .);
    KEEP (*(SORT(.fini_array.*)))
    KEEP (*(.fini_array*))
    PROVIDE_HIDDEN (__fini_array_end = .);
  } >FLASH

  /* used by the startup to initialize data */
  _sidata = LOADADDR(.data);

  /* Initialized data sections goes into RAM, load LMA copy after code */
  .data : 
  {
    . = ALIGN(4);
    _sdata = .;        /* create a global symbol at data start */
    *(.data)           /* .data sections */
    *(.data*)          /* .data* sections */

    . = ALIGN(4);
    _edata = .;        /* define a global symbol at data end */
  } >RAM AT> FLASH

  /* Uninitialized data section */
  . = ALIGN(4);
  .bss :
  {
    /* This is used by the startup in order to initialize the .bss secion */
    _sbss = .;         /* define a global symbol at bss start */
    __bss_start__ = _sbss;
    *(.bss)
    *(.bss*)
    *(COMMON)

    . = ALIGN(4);
    _ebss = .;         /* define a global symbol at bss end */
    __bss_end__ = _ebss;
  } >RAM

  /* User_heap_stack section, used to check that there is enough RAM left */
  ._user_heap_stack :
  {
    . = ALIGN(8);
    PROVIDE ( end = . );
    PROVIDE ( _end = . );
    . = . + _Min_Heap_Size;
    . = . + _Min_Stack_Size;
    . = ALIGN(8);
  } >RAM

  /* Remove information from the standard libraries */
  /DISCARD/ :
  {
    libc.a ( * )
    libm.a ( * )
    libgcc.a ( * )
  }

  .ARM.attributes 0 : { *(.ARM.attributes) }
}

然后修改启动文件,将STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\DeviceSupport\ ST\STM32F10x\startup\gcc_ride7 的全部文件拷贝到 STM32_Project\CMSIS\startup目录下,当前的启动文件就使用startup_stm32f10x_hd.s。

接下来就可以编译了。

关于GCC环境的搭建参看笔者前面的文章就可以了,Linux和Windows板本的都有。

笔者是在Windows开发,这里贴出GCC编译的环境搭建,为了方便查看。

下载地址

LC5v0s.md.png

【注】这里要选择GUN-RM下的工具,GUN-A是Cortex-A系列的交叉编译工具。
下载后解压,并把安装目录下的bin文件夹添加到环境变量:

LCISkq.md.png

然后在命令窗口中输入下面的命令验证安装是否成功:

arm-none-eabi-gcc -v

如果有信息输出,那就是装好了。

LCIkX4.md.png

GCC编译工程是需要编写Makefile,这里笔者已经编写好了,直接用就行。那么就需要一个工具来识别Makefile文件,也就是make工具,在Linux中已经自带make了,这一步就可以省了。
本文的make工具是依赖Git工具的,我相信很多朋友都用过Git,但是很少使用Git的make等功能。
Git的bash实际上也就是一个mingw,是可以支持部分Linux指令的,但是只有少部分。在编译代码的时候经常会使用make命令反而在bash下默认是不支持的。

Make工具下载地址

LCIVB9.md.png

下载make-4.1-2-without-guile-w32-bin.zip 文件。
把该文件进行解压,把解压出来的文件全部拷贝的git的安装目录下:
C:\Program Files\Git\mingw64

把文件夹进行合并,如果跳出来需要替换的文件要选择不替换。

LCIuh6.md.png

这样在git bash窗口下就可以执行make了。

LCIQ1O.md.png

没有安装Git先安装Git工具。
Git下载地址:https://git-scm.com/download/win
最后我们使用make编译下前文新建的工程,编译通过显示如下:

LCINNt.md.png

编译成功就会在build目录下生成固件。

值得注意的是,国产的很多厂家都没有提供GCC版本的启动文件,需要自行修改,笔者在后面会介绍如何编写启动文件。

在后面的章节笔者讲解程序的下载与调试,尽请期待!

小贴士

关于STM32 MDK中USE_STDPERIPH_DRIVER, STM32F10X_HD问题的解释
初学STM32,在MDK 环境中使用STM32固件库建立工程时,初学者可能会遇到编译不通过的问题。出现如下警告或错误提示:

warning: #223-D: function "assert_param" declared implicitly;assert_param(IS_GPIO_ALL_PERIPH(GPIOx));

这时候我们需要在“Target Options”中的“C/C++”选项卡中如下图所示红框中添加USE_STDPERIPH_DRIVER、STM32F10X_HD。这样才能使编顺利通过。

LCI0gS.md.png

知其然了,我们还得知其所以然。下面就笔者给大家一一道来。我们知道,程序的执行是从“main.c”文件开始的,其中必须包含有头文件“stm32f10x.h”。我们打开“stm32f10x.h”,按下“Ctrl+F”键,查找USE_STDPERIPH_DRIVER,在“Find What”栏中输入“USE_STDPERIPH_DRIVER”。值得注意的是在查找之前工程必须是编译过了的。如下图所示。

LCIBjg.png

点击“Find Next”,出现“USE_STDPERIPH_DRIVER”对应的代码行,我们能在第8296-8298行找到如下图所示代码段。

LCIsBj.png

这段代码的意思是,只有用预编译指令预定义了“USE_STDPERIPH_DRIVER”,才会将"stm32f10x_conf.h"包含进“stm32f10x.h”中,从而被"main.c"用到。这就解释了,为什么我们没有在“main.c”中包含"stm32f10x_conf.h",而在编译之后却被包含进了"main.c"中,出现如图-5所示的情况。"stm32f10x_conf.h"文件相当于一个开关文件,如果要用到STM32固件库驱动标准外设,则外设驱动头文件是必不可少的,如“stm32f10x_gpio.h”。在"stm32f10x_conf.h"中我们通过代码#include "stm32f10x_gpio.h"来实现这个操作。

说到这儿估计大家已经对“USE_STDPERIPH_DRIVER”的来龙去脉有个清晰的认识了吧?其实单从字面意思理解就是“使用标准外设驱动”,在C/C++预定义中加“USE_STDPERIPH_DRIVER”就是允许“使用标准外设驱动”了。至于加入的“STM32F10X_HD“同样能在文件”stm32f10x.h“通过如上所述的方法通过功能查找来进行解释,其实它也是对于对应硬件的某些定义起开关作用罢了。其实写在这里,他的名字叫define。也就跟你在工程里面写define XXX是一样的。只不过写在这里的话,是全局的而已。


欢迎访问我的网站

BruceOu的哔哩哔哩
BruceOu的主页
BruceOu的博客
BruceOu的CSDN博客
BruceOu的简书
BruceOu的知乎

资源获取方式

1.关注公众号[嵌入式实验楼]
2.在公众号回复关键词[Cortex-M]获取资料提取码

Related posts

Leave a Comment