STM32从零出发.工具链完成开发编译和烧录:(一)核心文件移植
为了能够理解单片机底层运行原理,故写下笔记记录从零搭建和学习过程,依赖AI提供的帮助,过程中使用VScode+arm-none-eabi-gcc+CMake+mingw+openocd工具链完成开发编译和烧录。
一、HAL库生成文件目录结构
我们可以先找一个Cube生成的工程查看其目录结构,如图1,在生成目录下有几个主要的文件夹,该目录下Core属于工程业务核心目录,存放应用层代码,如main.c,与Driver/CMSIS/Core的内核目录不同。
图1
主要看Drivers/CMSIS,图2中是 STM32Cube 固件里标准 CMSIS 文件夹分层,CMSIS 全称 Cortex Microcontroller Software Interface Standard(ARM Cortex 内核微控制器软件接口标准),是 ARM 制定的标准化底层软件库,统一了 Cortex-M 内核操作、芯片寄存器、DSP 算法、RTOS 接口,从零搭建最底层需要的文件都在这。

图2
1、Core
该文件夹由ARM提供,与芯片厂商(stm32)无关,文件用于操作Cortex-M 内核硬件(NVIC 中断、SysTick、内核寄存器、开关中断、休眠指令等)
- Include
内核头文件:core_cm3.h(STM32F1 是 Cortex-M3)、cmsis_gcc.h、cmsis_armcc.h等。 提供:__enable_irq()开中断、SysTick_Config()滴答定时器、NVIC_SetPriority()中断优先级、__WFI()休眠等内核函数。
- Template
工程启动模板文件:startup_stm32f1xx.s(汇编启动文件,中断向量表、堆栈初始化)、system_stm32f1xx.c模板,新建工程复制使用。
2、Device\ST\STM32F1xx
这个目录就是由芯片厂商(ST)提供了,绑定STM32F1 全系列芯片硬件寄存器、中断、系统初始化,是芯片与 CMSIS 内核的桥梁。
- Include
stm32f1xx.h:芯片所有外设寄存器地址、位定义、中断号、外设宏(GPIO/USART/ADC 等寄存器定义,寄存器开发必引); system_stm32f1xx.h:系统时钟初始化函数声明。
- Source
system_stm32f1xx.c:核心文件,SystemInit()上电时钟初始化(配置 HSE/HSI/PLL 主频 72M); 可选启动文件配套 C 辅助代码。
- LICENSE.txt
ST 的 CMSIS 开源授权协议(BSD 开源协议)。
- docs
STM32F1 系列 CMSIS 说明文档、寄存器手册索引。
3、Core_A
对应 Cortex-A 系列内核(STM32F1 是 M3内核,不用,仅给 MPU 芯片预留)。其余(DSP。Lib等)就是一些拓展需要的目录了,这里先不关心。
二、核心文件
在Core目录下有几个核心文件搭建时需要复制使用:
1、core_cm3.h
是stm32f1最核心文的内核层文件,其定义了处理器核心寄存器和外设的标准接口,如NVIC (嵌套向量中断控制器)、SCB (系统控制块)、SysTick (系统定时器)、调试相关模块,核心寄存器包括:
-
APSR (Application Program Status Register) - 应用程序状态寄存器
-
IPSR (Interrupt Program Status Register) - 中断程序状态寄存器
-
xPSR (Special-Purpose Program Status Register) - 特殊用途程序状态寄存器
-
CONTROL - 控制寄存器
2、cmsis_gcc.h
cmsis_gcc.h 是 CMSIS 框架中专门为 GCC 编译器及其兼容编译器(如 Clang)提供的一个适配层头文件。它的核心作用,是抹平不同编译器之间的语法差异,让你在 GCC 环境下也能使用 CMSIS 标准的函数和宏来操作 ARM Cortex-M 内核。我们平常在keil上编译时使用的又是cmsis_armcc.h(是ARM 官方为 Keil 的 ARM 编译器(armcc /armclang)提供的编译器内置函数、汇编指令、特殊关键字封装,只给 Keil 使用)。而本次使用的是arm-none-eabi-gcc+CMake+mingw编译,故使用cmsis_gcc.h。
3、cmsis_compiler.h
在core_cm3.h文件中存在cmsis_compiler.h调用,cmsis_compiler.h 定义了标准的编译器属性宏,这些宏在不同的编译器实现文件中会被映射为对应的编译器特定语法,如:
#ifndef __ASM
#define __ASM __asm
#endif
#ifndef __INLINE
#define __INLINE inline
#endif
#ifndef __STATIC_INLINE
#define __STATIC_INLINE static inline
#endif
#ifndef __STATIC_FORCEINLINE
#define __STATIC_FORCEINLINE __STATIC_INLINE
#endif
#ifndef __NO_RETURN
#define __NO_RETURN __attribute__((noreturn))
#endif
#ifndef __USED
#define __USED __attribute__((used))
#endif
#ifndef __WEAK
#define __WEAK __attribute__((weak))
#endif
#ifndef __PACKED
#define __PACKED __packed__
#endif
#ifndef __PACKED_STRUCT
#define __PACKED_STRUCT struct __packed__
#endif
#ifndef __PACKED_UNION
#define __PACKED_UNION union __packed__
#endif
4、cmsis_version.h
cmsis_version.h 是CMSIS框架中一个非常简洁但重要的头文件,作用是定义CMSIS-Core的版本号。
内核层的核心文件介绍完之后,在Device\ST\STM32F1xx目录下也有几个核心文件搭建时需要复制使用:
1、startup_stm32f103xb.s启动文件
因为芯片上电是由硬件复位至指定地址执行的(地址重映射由boot引脚确定,如从0x00000000重定向至flash0x08000000开始),startup_stm32f103xb.s启动文件中定义了各个中断向量表的位置,芯片上电后通过启动文件进行软件复位等并跳转至main()函数。
启动文件被放在Device/source目录下如图3,其中有arm、gcc、iar三个目录,每个目录下的启动文件不一样,核心原因在于不同工具链对C运行时环境初始化的分工和理念不同。

图3
-
ARM Compiler (如Keil MDK):启动文件(
.s)在设置好堆栈指针后,直接跳转到一个由编译器提供的特殊函数__main。这个函数会自动调用__scatterload等内部子程序,来完成上述的数据搬运和清零工作,然后再跳转到用户的main()函数。这个过程对开发者是透明的,像个“黑盒子”。 -
GCC:GCC工具链通常不会自动链接一个像
__main这样功能复杂的入口函数。因此,大部分的初始化工作,包括从Flash复制.data和清零.bss,都需要在启动文件(.s)中通过汇编代码显式地、一步一步地完成。GCC的启动文件就像一个“说明书”,把所有的准备工作都写在了明面上。
并且对比arm(keil使用)和gcc的启动文件发现,arm的启动文件有堆栈空间设置的过程,但gcc的启动文件并没有,但在该启动文件中有
; 在向量表中引用 _estack(外部符号)
g_pfnVectors:
.word _estack ; <--- 这个符号在链接脚本(.ld)中定义
_estack 是在链接脚本(.ld文件)中定义的符号,定义了栈顶地址,或许堆栈空间将由链接脚本定义?因为我们需要使用gcc编译,于是启动文件选择gcc的。
我们还发现同一个目录下,存在多个不同的启动文件,有各种各样的后缀如xb、xe、x6等。其也和具体芯片型号有关如图4。使用的f103c8t6,故使用xb后缀的。
图4
2、STM32F103C8Tx_FLASH.ld
在gcc目录下并没有找到配套的链接脚本,于是找ai帮忙生成了一份
/* Entry Point */
ENTRY(Reset_Handler)
/* 定义内存区域 */
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 64K
}
/* 栈顶地址 = RAM 起始地址 + RAM 大小 */
_estack = ORIGIN(RAM) + LENGTH(RAM);
/* 定义堆和栈的最小大小 */
_Min_Heap_Size = 0x200;
_Min_Stack_Size = 0x400;
/* 定义各个段在内存中的布局 */
SECTIONS
{
/* 中断向量表,必须放在 Flash 的开头 */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} >FLASH
/* 代码段 */
.text :
{
. = ALIGN(4);
*(.text)
*(.text*)
*(.glue_7)
*(.glue_7t)
*(.eh_frame)
KEEP (*(.init))
KEEP (*(.fini))
. = ALIGN(4);
_etext = .;
} >FLASH
/* 只读数据段 */
.rodata :
{
. = ALIGN(4);
*(.rodata)
*(.rodata*)
. = ALIGN(4);
} >FLASH
/* 已初始化的全局变量,存放在 Flash,启动时拷贝到 RAM */
.data :
{
. = ALIGN(4);
_sdata = .;
*(.data)
*(.data*)
. = ALIGN(4);
_edata = .;
} >RAM AT> FLASH
_sidata = LOADADDR(.data);
/* 未初始化的全局变量,在 RAM 中 */
.bss :
{
. = ALIGN(4);
_sbss = .;
__bss_start__ = _sbss;
*(.bss)
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
__bss_end__ = _ebss;
} >RAM
/* 用于检查堆栈是否溢出 */
._user_heap_stack :
{
. = ALIGN(8);
PROVIDE ( end = . );
PROVIDE ( _end = . );
. = . + _Min_Heap_Size;
. = . + _Min_Stack_Size;
. = ALIGN(8);
} >RAM
/* 丢弃标准库中无用的信息 */
/DISCARD/ :
{
libc.a ( * )
libm.a ( * )
libgcc.a ( * )
}
}
分析下其实现了什么
ENTRY(Reset_Handler)
告诉链接器,程序的入口点(Entry Point)是 Reset_Handler 这个符号
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 64K
}
MEMORY为链接器定义目标芯片的物理内存空间。
RAM:起始地址0x20000000,大小20KB;
FLASH:起始地址0x08000000,大小64KB;
属性:(xrw) 表示可执行、可读、可写;(rx) 表示可执行、可读、不可写(Flash的特性)。
/* 栈顶地址 = RAM 起始地址 + RAM 大小 */
_estack = ORIGIN(RAM) + LENGTH(RAM);
/* 定义堆和栈的最小大小 */
_Min_Heap_Size = 0x200;
_Min_Stack_Size = 0x400;
该部分即定义了栈顶位置,指向RAM的末尾,同时定义了堆栈空间大小,在后续在ram中段分配时进行分配。
/* 定义各个段在内存中的布局 */
SECTIONS
{
/* 中断向量表,必须放在 Flash 的开头 */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} >FLASH
/* 代码段 */
.text :
{
. = ALIGN(4);
*(.text)
*(.text*)
*(.glue_7)
*(.glue_7t)
*(.eh_frame)
KEEP (*(.init))
KEEP (*(.fini))
. = ALIGN(4);
_etext = .;
} >FLASH
/* 只读数据段 */
.rodata :
{
. = ALIGN(4);
*(.rodata)
*(.rodata*)
. = ALIGN(4);
} >FLASH
/* 已初始化的全局变量,存放在 Flash,启动时拷贝到 RAM */
.data :
{
. = ALIGN(4);
_sdata = .;
*(.data)
*(.data*)
. = ALIGN(4);
_edata = .;
} >RAM AT> FLASH
_sidata = LOADADDR(.data);
/* 未初始化的全局变量,在 RAM 中 */
.bss :
{
. = ALIGN(4);
_sbss = .;
__bss_start__ = _sbss;
*(.bss)
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
__bss_end__ = _ebss;
} >RAM
/* 用于检查堆栈是否溢出 */
._user_heap_stack :
{
. = ALIGN(8);
PROVIDE ( end = . );
PROVIDE ( _end = . );
. = . + _Min_Heap_Size;
. = . + _Min_Stack_Size;
. = ALIGN(8);
} >RAM
这是链接脚本最核心的部分,定义了所有段如何分布到Flash和RAM中。下面分析一下此部分几个重要的地方做了什么。
.isr_vector 这类为段名,. = ALIGN(4)则是完成一个四字节对齐的操作,因为Cortex-M3 的向量表要求必须4字节对齐(每个向量是32位/4字节)。如果地址不对齐,CPU在读取向量时会触发硬件错误。如上一段的末尾地址为0x8000002,使用. = ALIGN(4)后下一段的开始为0x8000004。
我们看到在一些段分配中有KEEP(*(.isr_vector))/*(.data)/*(.data*)等,发现有*(.段名)这个相似的格式,该作用是从所有目标文件中收集为该段名的段,KEEP表示强制保留,否则可能被编译器有优化,*(.段名*)表示从所有目标文件中收集为该段名为开头的段。
还有一个很重要的部分,即每段末尾的 >FLASH, >RAM, >RAM AT> FLASH,我们不知道其原理也能知道它的目的,即将该段分配至RAM还是FLASH。 而>RAM AT> FLASH其含义为:
1) >RAM (指定 VMA 虚拟内存地址):冒号前的 >RAM,指定了该段的 VMA(运行地址)。告诉链接器,这个段的内容在程序运行时,必须位于 RAM 内存区域。
2)AT> FLASH (指定 LMA 加载内存地址):AT> FLASH 指定了该段的 LMA(加载地址)。告诉链接器,这个段的原始数据(初始值)应该存放在 FLASH 内存区域。
._user_heap_stack段分配了堆栈空间,并使用了8地址对齐,ARM架构的ARM Architecture Procedure Call Standard (AAPCS,即ARM架构过程调用标准) 明确规定栈必须保持8字节对齐(在函数入口处),例如ARM的 LDRD / STRD(双字加载/存储)指令要求8字节对齐。
3、system_stm32f1xx.c/system_stm32f1xx.h
在mcu上电后首先执行Reset_Handler,需要执行跳转至SystemInit()函数,完成配置 FSMC 总线、对应 GPIO、系统时钟,驱动 Bank1-NOR/SRAM3 外部静态 SRAM等操作。SystemInit()函数定义在system_stm32f1xx.c中,并由system_stm32f1xx.h提供外部调用接口。由于从cubemx生成工程中移植,其系统配置是由cubemx配置输出的。
Reset_Handler:
/* Call the clock system initialization function.*/
bl SystemInit
/* Copy the data segment initializers from flash to SRAM */
ldr r0, =_sdata
ldr r1, =_edata
ldr r2, =_sidata
movs r3, #0
b LoopCopyDataInit
4、stm32f1xx.h
在system_stm32f1xx.c中发现引入了stm32f1xx.h该头文件如图5,这是干嘛的?

图5
在生成的工程目录下寻找该文件,如图6,同级目录下还有更具体型号的头文件。

图6
在stm32f1xx.h下我们很明显地看到这一段代码,其作用是根据不同的宏引入不同的头文件
#if defined(STM32F100xB)
#include "stm32f100xb.h"
#elif defined(STM32F100xE)
#include "stm32f100xe.h"
#elif defined(STM32F101x6)
#include "stm32f101x6.h"
#elif defined(STM32F101xB)
#include "stm32f101xb.h"
#elif defined(STM32F101xE)
#include "stm32f101xe.h"
#elif defined(STM32F101xG)
#include "stm32f101xg.h"
#elif defined(STM32F102x6)
#include "stm32f102x6.h"
#elif defined(STM32F102xB)
#include "stm32f102xb.h"
#elif defined(STM32F103x6)
#include "stm32f103x6.h"
#elif defined(STM32F103xB)
#include "stm32f103xb.h"
#elif defined(STM32F103xE)
#include "stm32f103xe.h"
#elif defined(STM32F103xG)
#include "stm32f103xg.h"
#elif defined(STM32F105xC)
#include "stm32f105xc.h"
#elif defined(STM32F107xC)
#include "stm32f107xc.h"
#else
#error "Please select first the target STM32F1xx device used in your application (in stm32f1xx.h file)"
#endif
此外该头文件还配套了寄存器操作工具宏,提供操作单片机所有硬件的基础。有之前的了解,我们最终引入的头文件应该是stm32f103xb.h,宏之后将在cmake文件中定义。
5、stm32f103xb.h
既然stm32f1xx.h需要引入这些头文件,那这些头文件作用是啥?
我们知道,操作外设的本质就是操作寄存器,而操作需要通过向特定寄存器地址写入完成,
该头文件就是定义了各类寄存器的基地址,以及外设寄存器结构体封装等。
三、目录结构设计
至此所需的一些文件介绍完毕,将其复制移植到自定义的文件夹中,并整理成几个目录,如图8。

图8
这里我将内核头文件和芯片厂商的文件区分开,ld目录i单独存放链接文件,Usr存放用户定义文件,即main函数在此目录下。
四、一些补充
更多推荐

所有评论(0)