一、什么是外设?
可以把STM32芯片想象成一台微型的电脑:
内核: 就像是电脑的CPU,负责执行代码和进行运算。
外设: 就像电脑的屏幕、键盘、风扇等,负责各种功能模块。
学习STM32开发,本质上就是学习如何配置和使用这些丰富的外设来实现你想要的功能。
二、STM32常见的外设分类与举例
STM32的外设非常丰富,可以大致分为以下几类:
外设类别 | 子类型 | 功能描述 |
通信接口 | UART | 异步串口通信,基础通信方式,用于连接GPS模块、蓝牙模块、电脑串口调试等场景。 |
I2C | 双向两线制串行总线,适用于连接多个低速设备,如温湿度传感器、加速度计、EEPROM存储器等。 | |
SPI | 高速全双工串行总线,支持高速数据传输,常用于连接Flash存储器、SD卡、显示屏、无线模块等。 | |
USB | 通用串行总线,支持USB设备(被电脑识别)、USB主机(连接U盘)或USB OTG(角色切换)模式。 | |
CAN | 控制器局域网总线,具备强抗干扰能力和可靠性,主要应用于汽车和工业领域。 | |
以太网 | 提供有线网络连接功能,满足需要网络通信的嵌入式设备需求。 | |
定时器 | 基本定时器 | 用于产生精确延时和定时中断。 |
通用定时器 | 功能丰富,支持基本定时、PWM输出(控制舵机、直流电机速度、LED亮度调节)、输入捕获(测量脉冲宽度、频率,如编码器信号)、输出比较(产生特定频率和占空比的波形)。 | |
高级定时器 | 功能最强大,适用于电机控制、数字电源等复杂领域,可生成带死区控制的互补PWM信号。 | |
模拟外设 | ADC | 模数转换器,将外部模拟电压信号(如传感器输出)转换为数字值,供内核处理,例如读取电位器位置、光照强度、温度传感器电压。 |
DAC | 数模转换器,将数字值转换为模拟电压输出,可用于生成特定波形(如正弦波、三角波)。 | |
比较器 | 快速比较两个模拟电压大小,输出数字信号(高/低),常用于过流保护、阈值检测等场景。 | |
数据管理 | DMA | 直接存储器访问,可在不占用CPU的情况下在外设和内存之间直接搬运数据,例如ADC转换完成后自动将数据传输至RAM数组,提升系统效率。 |
输入/输出 | GPIO | 通用输入输出端口,可配置为输入模式(读取按键状态)、输出模式(控制LED亮灭)或复用为其他外设功能(如UART的TX引脚)。 |
安全与保护 | 看门狗 | 独立定时器,用于监测程序运行状态。程序正常时需定期“喂狗”,若程序崩溃无法喂狗,看门狗会自动复位芯片,使系统恢复。 |
三、时钟树和系统结构
时钟树可以理解为一个“时钟信号的配送网络”。它有几个关键的“水源”(时钟源),通过一系列的“开关和阀门”(多路选择器、分频器、倍频器)将不同频率和特性的时钟信号“配送”到各个“用水单位”(内核、外设等),使这些“用水单位”(内核、外设等)能正常工作。控制这些“开关和阀门”的模块称之为时钟控制RCC。
在《STM32F10xxx参考手册中文版.pdf》第 56 页有对时钟树的详细介绍:
在我们使用 STM32CubeMX 新建工程时,其实已经对这个时钟树进行配置了。
下面为STM32CubeMX配置时钟树后生成的代码入口函数(了解就行)
四、STM32F10xxx系统结构框图
五、启动文件作用
六、启动文件的核心作用
- 初始化堆栈指针
- 构建中断向量表
- 执行系统初始化
- 跳转到主程序
- 提供中断服务程序的默认入口
七、详细代码分析
1. 堆栈配置
Stack_Size EQU 0x400 ; 定义栈大小为 1KB AREA STACK, NOINIT, READWRITE, ALIGN=3 Stack_Mem SPACE Stack_Size ; 栈顶地址,初始化MSP的值 __initial_sp- EQU: 类似于C语言的 #define,定义常量。
- AREA: 声明一个段。STACK是段名,NOINIT表示不初始化为0,READWRITE可读可写,ALIGN=3表示按8字节对齐。
- SPACE: 分配一段指定大小的内存空间。
- __initial_sp: 链接器会使用这个符号的值作为主堆栈指针的初始值。
Heap_Size EQU 0x200 ; 定义堆大小为 512字节 AREA HEAP, NOINIT, READWRITE, ALIGN=3 __heap_base Heap_Mem SPACE Heap_Size __heap_limit- 堆用于动态内存分配(如malloc、free)。
2. 中断向量表
AREA RESET, DATA, READONLY EXPORT __Vectors __Vectors DCD __initial_sp ; 地址0: 初始栈顶地址 DCD Reset_Handler ; 地址4: 复位中断服务程序 DCD NMI_Handler ; 地址8: NMI Handler DCD HardFault_Handler ; 地址12: Hard Fault Handler ... ; 更多系统异常 DCD WWDG_IRQHandler ; 外部中断0 (Window Watchdog) DCD PVD_IRQHandler ; 外部中断1 (PVD) ... ; 所有外部中断- AREA RESET, DATA, READONLY: 定义一个名为RESET的只读数据段,存放向量表。
- EXPORT __Vectors: 导出__Vectors符号,让链接器知道向量表的起始位置。
- DCD: 分配一个32位的字(4字节)内存,并初始化。
- 向量表结构:
- 第一个条目必须是初始栈顶指针。Cortex-M内核上电后,首先从地址0x00000000取出这个值赋给MSP。
- 第二个条目是复位向量,即Reset_Handler的地址。上电后,内核从地址0x00000004取出这个值,并跳转到该地址执行。
- 后续条目依次是各种异常和中断的入口地址。
3. 复位中断服务程序
Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT SystemInit IMPORT __main LDR R0, =SystemInit BLX R0 LDR R0, =__main BX R0 ENDP- PROC/ENDP: 定义一个过程(函数)。
- [WEAK]: 弱定义。如果别处没有定义同名的强符号,则使用此定义。这里允许用户在C代码中自己实现一个Reset_Handler。
- IMPORT: 声明外部符号,类似于C语言的extern。
- 执行流程:
- LDR R0, =SystemInit: 将SystemInit函数的地址加载到寄存器R0。
- BLX R0:跳转并执行SystemInit()。这个函数通常由ST提供,在system_stm32f1xx.c中,它负责配置时钟、初始化等关键系统配置。不过用STM32CubeMX生成工程时,时钟配置已经在SystemClock_Config()函数中实现,SystemInit()函数不做配置了。
- LDR R0, =__main: 将__main的地址加载到R0。注意:这个__main不是你的C代码里的main函数,它是C库中的一个函数,由编译器提供。
- BX R0: 跳转到__main。
- __main会完成以下工作:
- 复制初始化数据从Flash到SRAM(例如,初始化全局变量)。
- 将未初始化的内存区域清零(例如,.bss段)。
- 调用C库初始化(如果使用)。
- 最后,调用用户的main()函数。
- __main会完成以下工作:
4. 默认中断服务程序
NMI_Handler PROC EXPORT NMI_Handler [WEAK] B . ; 无限循环 ENDP HardFault_Handler\ PROC EXPORT HardFault_Handler [WEAK] B . ENDP ...- 对于所有异常和中断,这里都提供了一个默认实现:一个什么都不做的无限循环(B .)。
- 它们都被标记为[WEAK]。这意味着,如果你在C代码中自己实现了一个同名的函数(例如 void TIM2_IRQHandler(void) { ... }),链接器会优先使用你的函数,从而覆盖这个默认的空实现。这就是我们自定义中断服务函数的方式(函数名不能改,但是函数里面执行的代码我们可自定义)。
5. 堆栈初始化(用于非微库)
IF :DEF:__MICROLIB ... ; 如果使用微库,直接使用上面定义的符号 ELSE IMPORT __use_two_region_memory EXPORT __user_initial_stackheap __user_initial_stackheap LDR R0, = Heap_Mem ; 返回堆起始地址 LDR R1, =(Stack_Mem + Stack_Size) ; 返回栈结束地址 LDR R2, = (Heap_Mem + Heap_Size) ; 返回堆结束地址 LDR R3, = Stack_Mem ; 返回栈起始地址 BX LR ENDIF- 这部分是为了支持标准的C库(而非微库Microlib)进行堆栈初始化。
- __user_initial_stackheap 函数会被C库调用,它需要返回堆和栈的边界地址。