3、固件工程结构:创建第一个STM32CubeIDE工程
好,咱们正式开始动手了。这一章我会带你从零创建一个STM32CubeIDE工程,然后把这个工程的「骨架」拆开给你看。说白了,就是让你知道你的代码到底跑在什么环境里。
我记得我刚入行那会儿,拿到一个现成的工程,打开一看,一堆.s文件、.ld文件、启动文件……完全不知道干嘛用的。后来踩了不少坑,才慢慢摸清楚。今天咱们就把这些坑提前填上。
3.1 创建第一个STM32CubeIDE工程
打开STM32CubeIDE,咱们一步步来。
- 选择工作空间:启动时会让你选一个工作目录。我个人习惯把每个项目单独放一个文件夹,比如
D:\workspace\smartwatch_fw。 - 新建工程:点击
File → New → STM32 Project。 - 选择芯片型号:在弹窗里搜索你的芯片型号,比如
STM32L4R9ZI。选好后点Next。 - 工程命名:给工程起个名字,比如
smartwatch_fw。注意不要用中文路径。 - 初始化配置:IDE会问你要不要初始化所有外设到默认状态。我建议选
Yes,后面再按需裁剪。
创建完成后,你会看到这样一个工程结构:
smartwatch_fw/
├── Core/
│ ├── Inc/ // 头文件
│ ├── Src/ // 源文件
│ └── Startup/ // 启动文件
├── Drivers/
│ ├── CMSIS/ // ARM内核抽象层
│ └── STM32L4xx_HAL_Driver/ // HAL库
├── .cproject // Eclipse工程配置
├── .project // Eclipse项目文件
└── *.ioc // CubeMX图形化配置
嗯,这里要注意:.ioc文件是CubeMX的图形化配置文件。你双击它,就能用图形界面配置引脚、时钟、外设。配置完保存,IDE会自动生成代码。
3.2 工程目录结构解析
咱们把每个文件夹拆开看看,到底装了什么。
3.2.1 Core 目录
这是你写代码的主战场。
- Inc/:放头文件。比如
main.h、gpio.h、usart.h等。IDE会自动生成外设的头文件。 - Src/:放源文件。你的
main.c就在这里。还有各种外设的驱动文件,比如gpio.c、usart.c。 - Startup/:启动文件。这个很关键,后面单独讲。
3.2.2 Drivers 目录
这里装的是官方提供的驱动库。
- CMSIS/:ARM公司搞的一套标准接口。它定义了内核寄存器、中断向量表、系统时钟等底层东西。说白了,就是让不同厂家的ARM芯片能用同一套API。
- STM32L4xx_HAL_Driver/:ST官方的HAL库。HAL是 Hardware Abstraction Layer 的缩写。它把寄存器操作封装成了函数,比如
HAL_GPIO_WritePin()、HAL_UART_Transmit()。
我在项目中遇到过一个问题:HAL库虽然方便,但有些场景下效率不够高。比如你要在中断里快速翻转一个GPIO,用HAL函数可能多出几十个时钟周期的开销。这时候我会直接操作寄存器。
3.3 链接脚本详解
链接脚本,后缀是 .ld。它告诉链接器:你的代码该放到Flash的哪个位置,变量该放到RAM的哪个位置。
打开工程里的 STM32L4R9ZITx_FLASH.ld,你会看到类似这样的内容:
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 640K
}
SECTIONS
{
.text :
{
*(.isr_vector) // 中断向量表
*(.text) // 代码段
*(.rodata) // 只读数据
_etext = .; // 代码结束地址
} > FLASH
.data :
{
_sdata = .; // 数据段起始地址
*(.data) // 已初始化的全局变量
_edata = .; // 数据段结束地址
} > RAM AT> FLASH
.bss :
{
_sbss = .; // BSS段起始地址
*(.bss) // 未初始化的全局变量
_ebss = .; // BSS段结束地址
} > RAM
}
我来解释一下关键点:
- MEMORY:定义了两块物理存储。Flash是只读的,放代码;RAM是可读写的,放变量。
- .text:代码段。中断向量表、你的函数、常量字符串都放这里。
- .data:已初始化的全局变量。比如
int a = 10;,这个10就存在Flash里,启动时拷贝到RAM。 - .bss:未初始化的全局变量。比如
int b;,启动时清零。
AT> FLASH。它表示.data段虽然运行在RAM,但初始值存在Flash里。启动文件负责把这个初始值从Flash拷贝到RAM。如果你自己写启动代码,千万别忘了这一步。
我曾经在一个项目里,因为链接脚本写错了,导致全局变量初始值全是乱的。查了两天才发现是 AT> FLASH 这个属性没写对。嗯,从那以后我每次改链接脚本都会反复检查。
3.4 启动文件详解
启动文件,后缀是 .s。它是汇编写的,负责芯片上电后最开始的初始化工作。
打开 startup_stm32l4r9xx.s,你会看到:
; 中断向量表
g_pfnVectors:
.word _estack // 栈顶地址
.word Reset_Handler // 复位中断
.word NMI_Handler // 不可屏蔽中断
.word HardFault_Handler // 硬件错误中断
// ... 其他中断向量
; 复位中断处理函数
Reset_Handler:
ldr r0, =_estack
mov sp, r0 // 设置栈指针
ldr r0, =_sdata
ldr r1, =_edata
ldr r2, =_sidata
bl CopyDataInit // 拷贝.data段
ldr r0, =_sbss
ldr r1, =_ebss
mov r2, #0
bl FillZero // 清零.bss段
bl SystemInit // 系统时钟初始化
bl main // 跳转到main函数
启动文件干了这几件事:
- 定义中断向量表:芯片上电后,硬件会自动从Flash的0地址读取栈顶指针,然后跳转到复位中断。
- 设置栈指针:把
_estack的值赋给SP寄存器。这个_estack是在链接脚本里定义的。 - 拷贝.data段:把Flash里存着的初始值,拷贝到RAM里。
- 清零.bss段:把未初始化的全局变量全部清零。
- 调用SystemInit:配置系统时钟、Flash等待周期等。
- 跳转到main:终于,你的C代码开始执行了。
CopyDataInit 和 FillZero 有没有执行。我经常在这两个地方打断点。
你可能会问:为什么不用C语言写启动代码?因为芯片上电时,C语言运行环境还没准备好——RAM没初始化、栈指针没设置。所以必须用汇编来完成这些最底层的操作。
3.5 本章小结
这一章咱们干了三件事:
- 创建了第一个STM32CubeIDE工程
- 拆解了工程目录结构,知道每个文件夹是干嘛的
- 深入分析了链接脚本和启动文件,明白了芯片上电后到底发生了什么
下一章,咱们会开始写真正的固件代码——点亮智能手表的屏幕。到时候你会看到,今天学的这些底层知识,是怎么支撑起上层应用的。
嗯,今天就到这里。有什么问题,欢迎在评论区留言。