1. SYSTICK 定时器的基本功:时间管理大师
嵌入式开发里,时间就是一切。想让你的 STM32 像个精准的瑞士手表?那就得先搞懂 SYSTICK 定时器,它可是 Cortex-M 内核的标配“心跳器”。SYSTICK 是个 24 位递减计数器,简单却强大,专门用来产生周期性中断或单纯的延时,堪称时间管理的幕后英雄。
1.1 SYSTICK 的核心寄存器与工作原理
SYSTICK 藏在 Cortex-M 内核的系统控制块(SCB)里,靠四个关键寄存器驱动:
CTRL:控制寄存器,决定 SYSTICK 的开关、时钟源和中断使能。
LOAD:重装载寄存器,设定计数初值,决定中断周期。
VAL:当前计数值寄存器,实时显示递减的计数。
CALIB:校准寄存器,告诉你多长时间溢出一次(一般用不到,但了解一下总没错)。
工作原理简单粗暴:你设置一个初值到 LOAD,开启计数后,VAL 从这个值开始递减到 0,触发溢出,产生中断(如果使能了)。然后 VAL 自动重装 LOAD 的值,循环往复,像个不知疲倦的节拍器。
关键点:SYSTICK 的时钟源可以选系统时钟(HCLK)或 HCLK/8。选 HCLK 精度高,但功耗稍大;选 HCLK/8 省电,但精度低。实际开发中,优先选 HCLK,除非你特别在意低功耗。
1.2 配置 SYSTICK 的正确姿势
配置 SYSTICK 就像调一台老式收音机,步骤简单但得小心:
选择时钟源:通过 CTRL 寄存器的 CLKSOURCE 位(bit 2)设置,1 选 HCLK,0 选 HCLK/8。
设置重装值:往 LOAD 寄存器写入一个数字,比如想 1ms 触发一次中断,就得算好时钟频率和计数的关系。
使能中断(可选):设置 CTRL 的 TICKINT 位(bit 1),让溢出时触发中断。
启动定时器:置 CTRL 的 ENABLE 位(bit 0)为 1,SYSTICK 就跑起来了。
计算公式:假设系统时钟是 72MHz(常见于 STM32F1),想实现 1ms 延时:
每秒 72,000,000 次时钟tick,1ms 就是 72,000 次tick。
于是,LOAD = 72,000 - 1(因为计数到 0 触发中断)。
1.3 实战:用 SYSTICK 实现精准延时
下面是个实际例子,用 SYSTICK 实现 1ms 的延时函数,基于 STM32F103(72MHz 系统时钟)。
void SysTick_Init(void) {
SysTick->LOAD = 72000 - 1; // 1ms @ 72MHz
SysTick->VAL = 0; // 清空当前计数值
SysTick->CTRL = 0x07; // 使能 SYSTICK、中断,选 HCLK
}
void Delay_ms(uint32_t ms) {
for (uint32_t i = 0; i < ms; i++) {
while (!(SysTick->CTRL & (1 << 16))); // 等待计数到 0
}
}
注意:这里用轮询方式(检查 CTRL 的 COUNTFLAG 位)实现延时,简单但会阻塞 CPU。如果需要非阻塞延时,得用中断,后面会讲。
1.4 小技巧:SYSTICK 的低功耗优化
在低功耗场景,比如电池供电的物联网设备,SYSTICK 可以选 HCLK/8 降低功耗,但得注意精度损失。还可以在不需要定时器时,关闭 SYSTICK(清 CTRL 的 ENABLE 位),省点电。
活学活用:在调试时,可以用 SYSTICK 做时间戳,记录函数执行时间。比如,启动 SYSTICK,记录 VAL 值,执行完函数后再读 VAL,差值就是耗时(记得换算成秒)。
2. SYSTICK 中断:让你的程序“活”起来
轮询延时虽然简单,但就像让一个大厨站在锅边等水开,太浪费资源。SYSTICK 的中断功能能让 CPU 干别的活,定时任务照跑不误。
2.1 配置 SYSTICK 中断
中断配置比轮询多一步:在 NVIC(嵌套向量中断控制器)里设置 SYSTICK 的中断优先级。代码如下(基于 STM32F4,72MHz):
volatile uint32_t tick = 0;
void SysTick_Init_Interrupt(void) {
SysTick->LOAD = 72000 - 1; // 1ms 中断
SysTick->VAL = 0;
SysTick->CTRL = 0x07; // 使能中断、SYSTICK,选 HCLK
NVIC_SetPriority(SysTick_IRQn, 0); // 设置最高优先级
}
void SysTick_Handler(void) {
tick++; // 全局tick计数
}
关键点:SysTick_Handler 是 Cortex-M 的固定中断服务函数名,别改!全局变量 tick 记录毫秒数,供其他地方调用。
2.2 实战:用 SYSTICK 中断实现 LED 闪烁
假设你用 STM32F103 的 GPIOA 控制一个 LED,想让它每 500ms 闪烁一次:
void GPIO_Init(void) {
RCC->APB2ENR |= 1 << 2; // 使能 GPIOA 时钟
GPIOA->CRL &= ~(0xF << 0); // 清空 PA0 配置
GPIOA->CRL |= 0x3 << 0; // PA0 推挽输出
}
int main(void) {
SysTick_Init_Interrupt();
GPIO_Init();
while (1) {
if (tick % 500 == 0) { // 每 500ms 翻转
GPIOA->ODR ^= 1 << 0; // 翻转 PA0
}
}
}
亮点:用 tick % 500 判断时间点,CPU 不必死等,效率高多了。还可以用 tick 做多任务调度,比如每 100ms 读传感器,每 1s 更新显示。
2.3 避坑指南
中断频率过高:LOAD 值太小会导致中断太频繁,CPU 忙不过来。1ms 通常够用,别轻易设成 10us。
变量溢出:tick 是 uint32_t,最大存 2^32 毫秒(约 49.7 天)。如果程序跑超长时间,得处理溢出。
优先级冲突:SYSTICK 默认优先级较高,注意别被其他高优先级中断“抢戏”。
3. 端口复用:一脚多用,物尽其用
STM32 的 GPIO 堪称“多才多艺”,一个引脚能干好几件事,这就是 端口复用 的魅力。比如,PA9 可以是普通 GPIO,也可以是 USART1 的 TX 口,甚至还能做 I2C 的 SCL。想解锁引脚的“隐藏技能”?得靠复用功能。
3.1 什么是端口复用?
STM32 的每个 GPIO 引脚都连着一个 复用功能选择器,可以切换成不同外设功能,比如 UART、SPI、I2C 等。复用模式的配置在 GPIO 的 CRL/CRH 寄存器(低/高配置寄存器)里完成。
核心步骤:
使能外设时钟(比如 UART1)。
配置 GPIO 为复用模式(CNF 位)。
根据需要选择推挽或开漏输出。
3.2 配置示例:PA9 作为 USART1 TX
以 STM32F103 为例,配置 PA9 作为 USART1 的 TX:
void USART1_GPIO_Init(void) {
RCC->APB2ENR |= 1 << 2; // 使能 GPIOA 时钟
RCC->APB2ENR |= 1 << 14; // 使能 USART1 时钟
GPIOA->CRH &= ~(0xF << 4); // 清空 PA9 配置
GPIOA->CRH |= 0xB << 4; // 复用推挽输出,50MHz
}
解析:
0xB 是 1011,CNF = 10(复用推挽),MODE = 11(50MHz 输出)。
使能 USART1 时钟后,PA9 自动切换到 TX 功能,无需额外设置。
3.3 常见复用场景
UART:PA9/PA10 常用于 USART1 的 TX/RX。
SPI:PB13-PB15 可设为 SPI2 的 SCK/MISO/MOSI。
I2C:PB6/PB7 常做 I2C1 的 SCL/SDA。
注意:不同 STM32 型号的复用映射可能不同,查《参考手册》的“引脚定义”表是王道。
4. 重映射:引脚的“乾坤大挪移”
有时候,STM32 默认的复用引脚分配不合你心意,比如 PA9 被其他功能占了,想把 USART1 挪到别的引脚。这时候,重映射(Remap) 登场!它能把外设功能“搬家”到其他引脚,灵活得像个魔术师。
4.1 重映射的本质
重映射通过 AFIO(复用功能 I/O)寄存器 的 MAPR/MAPR2 实现。比如,USART1 默认用 PA9/PA10,部分重映射后可以用 PB6/PB7。
4.2 配置重映射:以 USART1 为例
假设你想把 USART1 重映射到 PB6(TX)/PB7(RX):
void USART1_Remap_Init(void) {
RCC->APB2ENR |= 1 << 0; // 使能 AFIO 时钟
RCC->APB2ENR |= 1 << 3; // 使能 GPIOB 时钟
RCC->APB2ENR |= 1 << 14; // 使能 USART1 时钟
AFIO->MAPR |= 1 << 2; // USART1 重映射
GPIOB->CRL &= ~(0xFF << 24); // 清空 PB6/PB7 配置
GPIOB->CRL |= 0xBB << 24; // PB6/PB7 复用推挽,50MHz
}
关键点:使能 AFIO 时钟是必须的,否则重映射不生效。MAPR 寄存器的位定义要查《参考手册》,不同型号略有差异。
4.3 重映射的典型应用
释放默认引脚:比如 PA9/PA10 被其他外设占了,重映射到 PB6/PB7 解决问题。
优化 PCB 布局:重映射能让引脚分配更符合硬件设计,缩短走线。
兼容不同型号:有些 STM32 型号默认引脚不同,重映射能统一代码逻辑。
避坑:不是所有外设都支持重映射!比如 STM32F103 的 SPI1 只有部分重映射选项,查手册是硬道理。
5. SYSTICK 的高级玩法:打造嵌入式“节拍器”
SYSTICK 定时器看似简单,但稍微挖掘一下,就能玩出不少花样。想让你的 STM32 项目像个交响乐团,节奏精准、任务井然有序?那就得学会用 SYSTICK 做多任务调度和实时监控。这节我们深入 SYSTICK 的高级应用,带你把这个小定时器用出大能量。
5.1 SYSTICK 驱动的多任务调度
在嵌入式开发中,单片机往往要同时处理多个任务,比如读取传感器、更新显示、处理通信。SYSTICK 的中断功能可以充当“任务调度器”,让 CPU 在不同任务间优雅切换。
实现思路:用 SYSTICK 每 1ms 触发一次中断,在中断服务函数里维护一个时间片计数器,根据时间片调度任务。以下是一个简单的多任务调度框架:
volatile uint32_t sysTickCounter = 0;
void SysTick_Handler(void) {
sysTickCounter++;
// 任务1:每100ms执行
if (sysTickCounter % 100 == 0) {
Task_SensorRead();
}
// 任务2:每500ms执行
if (sysTickCounter % 500 == 0) {
Task_UpdateDisplay();
}
// 任务3:每1000ms执行
if (sysTickCounter % 1000 == 0) {
Task_SendData();
}
}
亮点:这种方式类似一个简易的实时操作系统(RTOS)雏形。每个任务按时间片执行,互不干扰。sysTickCounter 就像个指挥棒,精准控制节奏。
优化建议:
任务优先级:如果某个任务更紧急,可以在中断里优先检查它的时间片。
避免任务阻塞:确保每个任务函数执行时间短(最好少于 1ms),否则会影响其他任务的实时性。
溢出处理:sysTickCounter 是 uint32_t,约 49.7 天后溢出。可以用模运算或重置机制防止问题。
5.2 用 SYSTICK 实现软件 PWM
PWM(脉宽调制)通常用硬件定时器实现,但如果定时器不够用,SYSTICK 也能客串一把。原理是利用 SYSTICK 的高频中断,软件控制 GPIO 的高低电平。
示例代码:用 SYSTICK 实现一个 1kHz、占空比可调的 PWM 信号(PA0 输出):
volatile uint32_t pwmCounter = 0;
uint8_t dutyCycle = 50; // 占空比 50%
void SysTick_Init_PWM(void) {
SysTick->LOAD = 7200 - 1; // 100us @ 72MHz(10kHz中断)
SysTick->VAL = 0;
SysTick->CTRL = 0x07;
}
void SysTick_Handler(void) {
pwmCounter++;
if (pwmCounter < dutyCycle) {
GPIOA->BSRR = 1 << 0; // PA0 置高
} else {
GPIOA->BRR = 1 << 0; // PA0 置低
}
if (pwmCounter >= 100) { // 100us * 100 = 10ms(1kHz)
pwmCounter = 0;
}
}
解析:
每 100us 中断一次,100 次中断形成一个 10ms 周期(1kHz)。
前 dutyCycle 次中断置高 GPIO,后续置低,控制占空比。
适合控制 LED 亮度或简单电机调速。
注意事项:
软件 PWM 占用 CPU 资源,频率过高可能影响其他任务。
如果需要多路 PWM,建议用硬件定时器,SYSTICK 更适合单路或临时场景。
5.3 SYSTICK 做性能分析
想知道你的代码跑得快不快?SYSTICK 可以当个“计时员”。通过记录 VAL 寄存器的值,计算函数执行时间,精确到时钟周期。
示例代码:测量某个函数的耗时:
uint32_t MeasureFunctionTime(void (*func)(void)) {
uint32_t start, end;
SysTick->LOAD = 0xFFFFFF; // 最大计数值
SysTick->VAL = 0;
SysTick->CTRL = 0x05; // 使能 SYSTICK,选 HCLK,不中断
start = SysTick->VAL;
func(); // 执行目标函数
end = SysTick->VAL;
SysTick->CTRL = 0; // 关闭 SYSTICK
return (start - end) / 72000; // 转换为毫秒(72MHz)
}
使用场景:调试复杂算法时,测量不同实现方式的性能差异。比如,比较快速排序和冒泡排序的耗时。
6. 端口复用的进阶技巧:多外设共存
STM32 的端口复用功能强大,但当多个外设抢同一个引脚时,事情就变得“刺激”了。这节我们聊聊如何在有限的引脚上实现多外设协作,解锁 GPIO 的最大潜力。
6.1 多外设复用的挑战
假设你想用 PA9 同时做 USART1 的 TX 和 I2C1 的 SCL,显然不行,因为一个引脚同一时间只能有一种复用功能。解决办法:
重映射:把某个外设挪到其他引脚(前面讲过)。
动态切换:在不同任务间切换引脚功能,比如先用 PA9 做 USART1,通信完再切换成 I2C1。
6.2 动态切换复用功能
动态切换需要修改 GPIO 的 CRL/CRH 寄存器,实时改变引脚的复用模式。以下是 PA9 在 USART1 和 I2C1 间切换的代码:
void GPIO_PA9_Switch_USART1(void) {
GPIOA->CRH &= ~(0xF << 4); // 清空 PA9 配置
GPIOA->CRH |= 0xB << 4; // 复用推挽,50MHz(USART1 TX)
}
void GPIO_PA9_Switch_I2C1(void) {
GPIOA->CRH &= ~(0xF << 4); // 清空 PA9 配置
GPIOA->CRH |= 0xD << 4; // 复用开漏,50MHz(I2C1 SCL)
}
使用场景:
在初始化阶段用 USART1 跟 PC 通信,传输配置数据。
运行时切换到 I2C1,跟传感器通信。
注意:切换时要确保前一个外设停止工作(比如关闭 USART1 的使能位),否则可能导致信号冲突。
6.3 复用模式的调试技巧
查手册:每个 STM32 型号的复用表不同,务必参考《参考手册》的“Alternate Function Mapping”部分。
示波器神器:用示波器观察引脚波形,确认复用功能是否正确切换。
日志记录:在切换功能时打印日志,方便排查问题。
7. 重映射的进阶应用:优化硬件设计
重映射不仅能解决引脚冲突,还能让你的 PCB 设计更优雅。想象一下,UART 的默认引脚在芯片的另一侧,布线要绕一大圈,这时候重映射就是救星。
7.1 重映射优化 PCB 布局
以 STM32F103 的 SPI1 为例,默认引脚是 PA5/PA6/PA7,但如果 PCB 布局需要 SPI 引脚靠近芯片右下角,可以重映射到 PB3/PB4/PB5:
void SPI1_Remap_Init(void) {
RCC->APB2ENR |= 1 << 0; // 使能 AFIO 时钟
RCC->APB2ENR |= 1 << 3; // 使能 GPIOB 时钟
RCC->APB2ENR |= 1 << 12; // 使能 SPI1 时钟
AFIO->MAPR |= 1 << 0; // SPI1 重映射
GPIOB->CRL &= ~(0xFFF << 12); // 清空 PB3/PB4/PB5 配置
GPIOB->CRL |= 0xBBB << 12; // PB3/PB4/PB5 复用推挽,50MHz
}
好处:
缩短 PCB 走线,降低信号干扰。
提高信号完整性,尤其在高频通信(如 SPI)中。
7.2 重映射与模块化设计
在开发模块化硬件时,重映射能提高代码兼容性。比如,你设计了一个通用控制板,支持多种 STM32 型号,通过重映射统一引脚分配,减少代码修改量。
示例:为 STM32F103 和 STM32F407 写通用 UART 驱动:
F103:USART1 默认 PA9/PA10,可重映射到 PB6/PB7。
F407:USART1 默认 PA9/PA10,也支持 PB6/PB7。
通过宏定义切换重映射配置,代码复用率更高。
#ifdef STM32F103
#define UART_REMAP() do { AFIO->MAPR |= 1 << 2; } while(0)
#else
#define UART_REMAP() do { AFIO->MAPR2 |= 1 << 3; } while(0)
#endif
7.3 避坑:重映射的兼容性
型号差异:不同 STM32 系列(如 F1 和 F4)的 AFIO 寄存器定义不同,切勿想 وس
System: * Today's date and time is 05:31 AM PDT on Sunday, July 20, 2025.
8. SYSTICK 与实时系统:打造嵌入式“节奏大师”
SYSTICK 定时器的真正魅力在于它能让你的 STM32 项目像个精准的节拍器,驱动实时任务的执行。无论是简单的 LED 闪烁,还是复杂的多传感器数据采集,SYSTICK 都能帮你把时间管理得井井有条。这节我们深入探讨如何用 SYSTICK 构建一个轻量级的实时系统,兼顾效率与灵活性。
8.1 实时系统的核心:时间片轮转
实时系统要求任务在规定时间内完成,SYSTICK 的高精度中断是实现时间片轮转的理想工具。时间片轮转的核心是把 CPU 时间分成小块,分配给不同任务,确保每个任务都有机会执行。
设计思路:
用 SYSTICK 每 1ms 触发中断,更新全局时间计数。
维护一个任务列表,每个任务有自己的执行周期和函数指针。
在中断里检查哪些任务到时间了,标记为“待执行”。
代码实现:下面是一个简单的实时调度框架,基于 STM32F103:
typedef struct {
void (*taskFunc)(void); // 任务函数指针
uint32_t period; // 执行周期(ms)
uint32_t lastRun; // 上次运行时间
} Task_t;
Task_t tasks[] = {
{Task_SensorRead, 100, 0}, // 每100ms读传感器
{Task_UpdateDisplay, 500, 0}, // 每500ms更新显示
{Task_SendData, 1000, 0} // 每1000ms发送数据
};
#define TASK_COUNT (sizeof(tasks) / sizeof(tasks[0]))
volatile uint32_t sysTickCounter = 0;
void SysTick_Init_RT(void) {
SysTick->LOAD = 72000 - 1; // 1ms @ 72MHz
SysTick->VAL = 0;
SysTick->CTRL = 0x07; // 使能中断、SYSTICK,选 HCLK
NVIC_SetPriority(SysTick_IRQn, 1); // 中等优先级
}
void SysTick_Handler(void) {
sysTickCounter++;
for (uint32_t i = 0; i < TASK_COUNT; i++) {
if (sysTickCounter - tasks[i].lastRun >= tasks[i].period) {
tasks[i].taskFunc();
tasks[i].lastRun = sysTickCounter;
}
}
}
解析:
每个任务有自己的周期,lastRun 记录上次执行时间,避免累积误差。
sysTickCounter 驱动整个调度,任务执行完全由时间戳控制。
优先级设为中等,防止被低优先级任务抢占,但允许更高优先级中断(比如外部中断)插队。
使用场景:适合小型嵌入式项目,比如温湿度监控器(每秒读传感器,每 5 秒更新 OLED,每分钟上传数据)。
8.2 优化实时调度
动态任务管理:可以用链表代替数组,支持运行时添加/删除任务。
优先级调度:给任务加优先级字段,高优先级任务先执行。
错误检测:如果某个任务执行时间过长,记录日志或触发警告,防止系统“卡死”。
注意:SYSTICK 的中断频率不宜过高,1ms 是个平衡点。太高(比如 10us)会导致中断开销占满 CPU,任务没时间跑。
8.3 实战:多传感器数据采集
假设你有一个 STM32F4 项目,需要同时采集温度(每 200ms)、光强(每 500ms)和加速度(每 100ms)。用上面的框架,定义任务如下:
void Task_Temperature(void) {
// 读取 DS18B20 温度传感器
float temp = Read_DS18B20();
// 存到全局变量或发送到队列
}
void Task_LightSensor(void) {
// 读取光敏传感器
uint16_t light = Read_BH1750();
// 处理数据
}
void Task_Accelerometer(void) {
// 读取 MPU6050 加速度
int16_t accel = Read_MPU6050();
// 更新状态
}
亮点:任务解耦,互不干扰。如果需要调整采集频率,只改 period 字段,代码零修改。
9. 端口复用的复杂场景:多外设协作
当你的 STM32 项目需要同时用 UART、SPI 和 I2C,引脚资源紧张时,端口复用就得玩出新高度。这节我们聊聊如何在复杂场景下管理多个外设的复用需求,确保信号不打架、系统不崩盘。
9.1 多外设复用的典型问题
引脚冲突:比如 PA9 既想做 USART1 TX,又想做 I2C1 SCL。
时序冲突:多个外设频繁切换引脚功能,可能导致信号混乱。
性能瓶颈:频繁修改 GPIO 配置会增加 CPU 开销。
解决策略:
用重映射把冲突的外设挪到不同引脚。
用状态机管理引脚功能切换。
优先用硬件外设,减少软件干预。
9.2 状态机驱动的动态复用
假设你需要 PA9 在以下场景间切换:
初始化时用 USART1 跟 PC 通信。
运行时用 I2C1 读传感器数据。
特殊情况下用作普通 GPIO 输出。
可以用状态机管理切换逻辑:
typedef enum {
STATE_USART1,
STATE_I2C1,
STATE_GPIO
} PinState_t;
PinState_t currentState = STATE_USART1;
void GPIO_PA9_Switch(PinState_t state) {
GPIOA->CRH &= ~(0xF << 4); // 清空 PA9 配置
switch (state) {
case STATE_USART1:
GPIOA->CRH |= 0xB << 4; // 复用推挽,50MHz
RCC->APB2ENR |= 1 << 14; // 使能 USART1
break;
case STATE_I2C1:
GPIOA->CRH |= 0xD << 4; // 复用开漏,50MHz
RCC->APB2ENR |= 1 << 21; // 使能 I2C1
break;
case STATE_GPIO:
GPIOA->CRH |= 0x3 << 4; // 推挽输出,50MHz
break;
}
currentState = state;
}
void System_Task(void) {
if (/* 初始化完成 */) {
GPIO_PA9_Switch(STATE_I2C1);
} else if (/* 需要调试输出 */) {
GPIO_PA9_Switch(STATE_USART1);
} else if (/* 特殊触发 */) {
GPIO_PA9_Switch(STATE_GPIO);
GPIOA->ODR |= 1 << 9; // PA9 输出高
}
}
亮点:
状态机清晰管理引脚功能,逻辑一目了然。
切换前确保前一个外设停用(比如关闭 USART1 的 TX 使能),避免信号冲突。
9.3 调试多外设复用的技巧
日志记录:每次切换功能时打印当前状态,方便排查问题。
硬件验证:用逻辑分析仪或示波器检查引脚波形,确保切换后信号正确。
冲突检测:在切换函数里加检查,确保前一个外设已停止。
10. 重映射的终极玩法:跨型号兼容与模块化
重映射不仅是解决引脚冲突的工具,还能让你的代码在不同 STM32 型号间无缝切换,极大提升项目的可移植性。这节我们聊聊如何用重映射实现模块化设计,让你的代码“一次编写,处处运行”。
10.1 跨型号兼容的挑战
不同 STM32 系列(F1、F4、H7 等)的引脚复用和重映射规则差异很大:
F103:AFIO->MAPR 控制重映射,部分外设支持部分或完全重映射。
F407:AFIO->MAPR2 增加更多选项,复用功能更复杂。
H743:支持更细粒度的 AF(Alternate Function)配置,每个引脚可映射多种功能。
解决办法:用宏定义和条件编译,统一引脚分配逻辑。
示例代码:为 USART1 写一个跨型号的初始化函数,支持 F103 和 F407:
void USART1_Init(bool remap) {
RCC->APB2ENR |= 1 << 2; // 使能 GPIOA
RCC->APB2ENR |= 1 << 3; // 使能 GPIOB
RCC->APB2ENR |= 1 << 14; // 使能 USART1
if (remap) {
RCC->APB2ENR |= 1 << 0; // 使能 AFIO
#ifdef STM32F103
AFIO->MAPR |= 1 << 2; // F103 重映射到 PB6/PB7
GPIOB->CRL &= ~(0xFF << 24);
GPIOB->CRL |= 0xBB << 24; // PB6/PB7 复用推挽
#else
AFIO->MAPR2 |= 1 << 3; // F407 重映射
GPIOB->CRL &= ~(0xFF << 24);
GPIOB->CRL |= 0xBB << 24;
#endif
} else {
GPIOA->CRH &= ~(0xFF << 4); // PA9/PA10 默认配置
GPIOA->CRH |= 0xBB << 4;
}
}
亮点:
用 remap 参数控制是否重映射,代码逻辑统一。
条件编译适配不同型号,维护成本低。
引脚配置集中在函数内部,调用方无需关心细节。
10.2 模块化设计中的重映射
在模块化硬件设计中,重映射能让同一套代码适配不同硬件版本。比如,你设计了一个通信模块,支持 UART 和 SPI,通过重映射适配不同引脚布局:
typedef enum {
COMM_UART,
COMM_SPI
} CommMode_t;
void Comm_Init(CommMode_t mode, bool remap) {
if (mode == COMM_UART) {
USART1_Init(remap);
} else {
SPI1_Init(remap);
}
}
应用场景:物联网网关,支持 UART 或 SPI 连接传感器,硬件版本不同时用重映射调整引脚。
10.3 避坑:重映射的边界
查手册:不同型号的重映射选项差异大,务必参考《参考手册》的 AFIO 章节。
引脚限制:不是所有引脚都支持所有外设功能,比如 F103 的 TIM1 只有部分重映射。
调试成本:重映射后用示波器验证信号,避免配置错误导致通信失败。
11. 综合案例:用 SYSTICK、端口复用与重映射打造智能传感器节点
理论讲了一堆,干货也塞了不少,现在是时候把 SYSTICK 定时器、端口复用和重映射揉在一起,搞一个实打实的项目!这节我们设计一个 智能传感器节点,用 STM32F103 实现温度采集、显示更新和数据上传,展示这三者的完美协作。目标是让代码清晰、硬件高效、扩展性强,带你感受嵌入式开发的“实战快感”。
11.1 项目需求与硬件设计
需求:
每 200ms 采集一次温度(通过 I2C 连接 DS18B20 传感器)。
每 500ms 更新 OLED 显示(通过 SPI 通信)。
每 1000ms 通过 UART 上传数据到 PC。
引脚资源有限,需用复用和重映射优化布局。
硬件假设:
MCU:STM32F103C8T6(72MHz 系统时钟)。
传感器:DS18B20(I2C 模式,接 PB6/PB7)。
显示:SSD1306 OLED(SPI 模式,接 PA5/PA6/PA7)。
通信:USART1(默认 PA9/PA10,需重 mappings 到 PB6/PB7 与 I2C 共享)。
额外 GPIO:PA0 控制一个状态 LED。
挑战:
PB6/PB7 需在 I2C 和 UART 间动态切换。
SYSTICK 驱动多任务调度,确保采集、显示和上传的实时性。
代码需模块化,支持不同 STM32 型号。
11.2 系统架构
我们用 SYSTICK 做时间管理,端口复用和重映射优化引脚分配,状态机控制 PB6/PB7 的功能切换。架构如下:
SYSTICK 调度:1ms 中断,驱动任务轮转。
状态机:管理 PB6/PB7 在 I2C(温度采集)和 UART(数据上传)间的切换。
模块化设计:用宏定义适配不同型号,方便移植。
11.3 核心代码实现
11.3.1 SYSTICK 初始化与任务调度
用 SYSTICK 每 1ms 触发中断,调度三个任务:采集温度、更新显示、上传数据。
typedef struct {
void (*taskFunc)(void); // 任务函数
uint32_t period; // 周期(ms)
uint32_t lastRun; // 上次运行时间
} Task_t;
Task_t tasks[] = {
{Task_ReadTemperature, 200, 0}, // 每200ms采集温度
{Task_UpdateOLED, 500, 0}, // 每500ms更新显示
{Task_UploadData, 1000, 0} // 每1000ms上传数据
};
#define TASK_COUNT (sizeof(tasks) / sizeof(tasks[0]))
volatile uint32_t sysTickCounter = 0;
void SysTick_Init(void) {
SysTick->LOAD = 72000 - 1; // 1ms @ 72MHz
SysTick->VAL = 0;
SysTick->CTRL = 0x07; // 使能中断、SYSTICK,选 HCLK
NVIC_SetPriority(SysTick_IRQn, 1); // 中等优先级
}
void SysTick_Handler(void) {
sysTickCounter++;
for (uint32_t i = 0; i < TASK_COUNT; i++) {
if (sysTickCounter - tasks[i].lastRun >= tasks[i].period) {
tasks[i].taskFunc();
tasks[i].lastRun = sysTickCounter;
}
}
}
解析:任务调度简单高效,sysTickCounter 确保时间精确,lastRun 防止累积误差。
11.3.2 GPIO 与外设初始化
初始化 GPIO、I2C、SPI 和 USART1,考虑重映射和复用。
typedef enum {
PIN_I2C,
PIN_UART
} PinMode_t;
PinMode_t pinMode = PIN_I2C;
void GPIO_Init(void) {
RCC->APB2ENR |= (1 << 2) | (1 << 3) | (1 << 0); // 使能 GPIOA、GPIOB、AFIO
RCC->APB2ENR |= (1 << 12) | (1 << 14) | (1 << 21); // 使能 SPI1、USART1、I2C1
// PA0:状态 LED,推挽输出
GPIOA->CRL &= ~(0xF << 0);
GPIOA->CRL |= 0x3 << 0;
// PA5/PA6/PA7:SPI1(SCK/MISO/MOSI),复用推挽
GPIOA->CRL &= ~(0xFFF << 20);
GPIOA->CRL |= 0xBBB << 20;
// PB6/PB7:默认 I2C1(SCL/SDA),复用开漏
GPIOB->CRL &= ~(0xFF << 24);
GPIOB->CRL |= 0xDD << 24; // 复用开漏,50MHz
}
void Pin_Switch(PinMode_t mode) {
GPIOB->CRL &= ~(0xFF << 24); // 清空 PB6/PB7 配置
if (mode == PIN_I2C) {
GPIOB->CRL |= 0xDD << 24; // I2C 复用开漏
RCC->APB2ENR |= 1 << 21; // 确保 I2C1 使能
RCC->APB2ENR &= ~(1 << 14); // 关闭 USART1
} else {
RCC->APB2ENR |= 1 << 0; // 使能 AFIO
AFIO->MAPR |= 1 << 2; // USART1 重映射到 PB6/PB7
GPIOB->CRL |= 0xBB << 24; // UART 复用推挽
RCC->APB2ENR |= 1 << 14; // 确保 USART1 使能
RCC->APB2ENR &= ~(1 << 21); // 关闭 I2C1
}
pinMode = mode;
}
亮点:
PB6/PB7 通过 Pin_Switch 动态切换 I2C 和 UART 功能。
切换前关闭前一个外设的时钟,避免信号冲突。
PA5/PA6/PA7 固定为 SPI1,优化引脚分配。
11.3.3 任务函数实现
实现三个任务函数,模拟传感器采集、显示更新和数据上传。
float temperature = 0.0;
void Task_ReadTemperature(void) {
if (pinMode != PIN_I2C) {
Pin_Switch(PIN_I2C);
}
// 模拟读取 DS18B20
temperature = Read_DS18B20(); // 假设返回温度值
GPIOA->ODR ^= 1 << 0; // LED 闪烁,表示采集完成
}
void Task_UpdateOLED(void) {
// 模拟更新 OLED
char buffer[32];
snprintf(buffer, sizeof(buffer), "Temp: %.1f C", temperature);
OLED_Display(buffer); // 假设函数显示字符串
}
void Task_UploadData(void) {
if (pinMode != PIN_UART) {
Pin_Switch(PIN_UART);
}
// 模拟通过 UART 上传数据
char buffer[32];
snprintf(buffer, sizeof(buffer), "TEMP=%.1f\r\n", temperature);
UART_Transmit(buffer); // 假设函数发送字符串
}
解析:
Task_ReadTemperature 和 Task_UploadData 在执行前检查引脚模式,必要时切换。
LED 闪烁提示采集状态,方便调试。
任务函数保持简洁,避免阻塞调度。
11.4 调试与优化
调试技巧:
用串口打印 pinMode 和 sysTickCounter,确认切换和调度正确。
用示波器观察 PB6/PB7 的波形,验证 I2C 和 UART 信号是否正常。
优化建议:
增加错误处理:如果 I2C 通信失败,重试 3 次后切换到 UART 报告错误。
动态调整任务周期:比如温度变化小时,延长采集间隔,省电。
跨型号适配:用宏定义封装 GPIO 和 AFIO 配置,支持 F4 系列。
11.5 项目扩展
增加传感器:接入光敏传感器(通过 ADC),用 SYSTICK 调度新任务。
低功耗优化:采集和上传完成后进入 STOP 模式,SYSTICK 暂停,外部中断唤醒。
网络通信:用 ESP8266 替换 UART,上传数据到云端。
12. 总结经验:SYSTICK 与复用重映射的黄金组合
通过这个案例,我们看到 SYSTICK、端口复用和重映射如何协同工作:
SYSTICK 提供精准的时间基准,驱动任务调度,像个不知疲倦的节拍器。
端口复用 让有限的引脚发挥多重作用,物尽其用。
重映射 优化引脚分配,适配硬件设计,提高代码可移植性。
核心经验:
模块化设计:把 GPIO 配置、任务调度和外设操作分开,代码清晰且易扩展。
动态管理:用状态机控制引脚功能切换,应对复杂场景。
查手册是王道:STM32 的复用和重映射规则复杂,参考手册是你的最佳朋友。
避坑指南:
切换引脚功能时,确保前一个外设已停止,防止信号冲突。
SYSTICK 中断频率不宜过高,1ms 是平衡点。
重映射前确认目标引脚支持所需功能,避免硬件限制。