STM32学习笔记#1-回调函数
既然你正在学习STM32标准库(Standard Peripheral Library),我会用标准库的架构设计三个回调函数的例子,由浅入深,帮助你理解回调函数在底层开发中的应用逻辑。通过这些例子,你可以看到回调函数在标准库中如何实现硬件事件与用户逻辑的解耦。:如果回调函数可能被多个中断同时触发,需考虑数据保护的机制(如关闭中断或使用互斥锁)。中断发生时自动调用回调函数,实现**硬件事件与业务逻辑
好的!既然你正在学习STM32标准库(Standard Peripheral Library),我会用标准库的架构设计三个回调函数的例子,由浅入深,帮助你理解回调函数在底层开发中的应用逻辑。
例1:GPIO外部中断回调(基础)
场景:按键按下触发外部中断,通过回调函数控制LED。
1.1 标准库的中断处理流程
在标准库中,回调函数需要手动实现(不像HAL库有预定义的回调接口)。以下是实现步骤:
按下PA0按键→触发外部中断→进入EXTI0_IRQHandler→清除中断标志→调用注册的LED_Toggle函数→LED状态翻转
// 定义回调函数类型,使用函数指针保存你的函数地址。
typedef void (*GPIO_Callback)(void);//函数指针,指向无参无返回值的函数
// 用上面定义的函数指针类型来定义:全局回调函数指针EXTI0_Callback
GPIO_Callback EXTI0_Callback = NULL;
// 配置外部中断(按键PA0)
void EXTI0_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
EXTI_InitTypeDef EXTI_InitStruct;
// 使能GPIOA和SYSCFG时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
// 配置PA0为输入模式
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置EXTI0线路
EXTI_InitStruct.EXTI_Line = EXTI_Line0;
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发
EXTI_InitStruct.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStruct);
// 注册中断服务函数
NVIC_EnableIRQ(EXTI0_IRQn);
}
// 用户注册回调函数
void EXTI0_RegisterCallback(GPIO_Callback callback)
{
EXTI0_Callback = callback;//参数 callback 是用户定义的函数地址。通过赋值实现动态绑定,支持运行时更换回调。
}
// 中断服务函数(固定名称)
void EXTI0_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line0) != RESET)
{
EXTI_ClearITPendingBit(EXTI_Line0); // 清除中断标志
if (EXTI0_Callback != NULL)
{
EXTI0_Callback(); // 调用用户注册的回调函数,安全写法(推荐)
}
}
}
// 用户业务逻辑(回调函数实现)
void LED_Toggle(void)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_0, (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_0)));
}
int main(void)
{
// 初始化LED和EXTI
LED_Init(); // 假设LED在PB0
EXTI0_Init();
// 注册回调函数
EXTI0_RegisterCallback(LED_Toggle);//用户调用这个函数,将自己的函数(如 LED_Toggle)的地址传给 EXTI0_Callback。
while (1)
{
// 主循环空闲
}
}
关键点:
-
通过
EXTI0_RegisterCallback将用户函数LED_Toggle注册为回调。 -
中断发生时自动调用回调函数,实现**硬件事件与业务逻辑解耦**。
问题:
1.进入EXTI0_IRQHandler中断的逻辑。
2.EXTI0_Callback()和EXTI0_Callback的区别
3. if (EXTI0_Callback != NULL) { EXTI0_Callback(); // 调用用户注册的回调函数 }
是固定格式吗
例2:定时器中断回调(进阶)
场景:使用定时器TIM2每隔1秒触发中断,通过回调函数执行周期性任务。
2.1 标准库的定时器配置
// 定义回调函数类型和全局指针
typedef void (*TIM_Callback)(void);//函数指针,指向无参无返回值的函数
TIM_Callback TIM2_Callback = NULL;//初始化指针指向地址0000 0000
// 初始化TIM2
void TIM2_Init(void)
{
TIM_TimeBaseInitTypeDef TIM_InitStruct;
// 使能TIM2时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
// 配置定时器参数
TIM_InitStruct.TIM_Period = 9999; // 自动重装载值
TIM_InitStruct.TIM_Prescaler = 7199; // 预分频值(72MHz/(7200)=10kHz)
TIM_InitStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_InitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM2, &TIM_InitStruct);
// 使能更新中断
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
// 注册中断服务函数
NVIC_EnableIRQ(TIM2_IRQn);
// 启动定时器
TIM_Cmd(TIM2, ENABLE);
}
// 用户注册回调函数
void TIM2_RegisterCallback(TIM_Callback callback)
{
TIM2_Callback = callback;
}
// 中断服务函数
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
if (TIM2_Callback != NULL)
{
TIM2_Callback(); // 调用用户回调
}
}
}
// 用户业务逻辑
void Task_EverySecond(void)
{
static uint32_t counter = 0;
printf("Second: %lu\n", counter++);
}
int main(void)
{
// 初始化定时器和串口
TIM2_Init();
USART_Init(); // 假设串口已初始化
// 注册回调
TIM2_RegisterCallback(Task_EverySecond);
while (1)
{
// 主循环空闲
}
}
关键点:
-
定时器中断通过回调函数执行周期性任务(如打印计数)。
-
用户无需关注定时器中断的底层细节,只需实现业务逻辑。
例3:USART接收中断回调(高级)
场景:串口接收数据时触发中断,通过回调函数处理接收到的数据。
3.1 标准库的串口接收配置
// 定义回调函数类型和全局指针
typedef void (*USART_RxCallback)(uint8_t data);
USART_RxCallback USART1_RxCallback = NULL;
// 初始化USART1
void USART1_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct;
USART_InitTypeDef USART_InitStruct;
// 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
// 配置TX(PA9)和RX(PA10)
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置USART1参数
USART_InitStruct.USART_BaudRate = 115200;
USART_InitStruct.USART_WordLength = USART_WordLength_8b;
USART_InitStruct.USART_StopBits = USART_StopBits_1;
USART_InitStruct.USART_Parity = USART_Parity_No;
USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStruct);
// 使能接收中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
// 注册中断服务函数
NVIC_EnableIRQ(USART1_IRQn);
// 启动USART1
USART_Cmd(USART1, ENABLE);
}
// 用户注册回调函数
void USART1_RegisterRxCallback(USART_RxCallback callback)
{
USART1_RxCallback = callback;
}
// 中断服务函数
void USART1_IRQHandler(void) {
if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
uint8_t data = USART_ReceiveData(USART1); // 读取数据
if (USART1_RxCallback != NULL)
{
USART1_RxCallback(data); // 传递数据到回调函数
}
}
}
// 用户业务逻辑:回显接收到的字符
void Echo_ReceivedData(uint8_t data) {
USART_SendData(USART1, data); // 回显数据
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); // 等待发送完成
}
int main(void) {
USART1_Init();
USART1_RegisterRxCallback(Echo_ReceivedData); // 注册回显回调
while (1) {
// 主循环空闲
}
}
关键点:
-
串口接收中断触发时,通过回调函数
Echo_ReceivedData回显数据。 -
用户无需阻塞等待数据接收,实现**非阻塞式通信**。
回调函数在标准库中的设计总结
|
设计模式 |
说明 |
|
自定义回调类型 |
使用函数指针类型(如 |
|
注册机制 |
提供注册函数(如 |
|
中断服务函数解耦 |
在中断服务函数中调用回调函数,分离硬件操作与业务逻辑。 |
|
数据传递 |
可通过参数传递数据(如USART接收的数据)。 |
对比HAL库与标准库的回调实现
|
特性 |
标准库 |
HAL库 |
|
回调接口定义 |
需手动定义函数指针和注册机制 |
预定义弱函数(如 |
|
中断处理 |
需手动编写中断服务函数 |
HAL库封装通用中断服务函数 |
|
灵活性 |
更高(完全自定义) |
较低(需遵循HAL库规范) |
|
开发效率 |
较低(需更多底层代码) |
较高(快速原型开发) |
实际开发中的建议
-
保持回调函数简洁:避免在回调函数中执行耗时操作(如复杂计算或阻塞等待)。
-
注意重入问题:如果回调函数可能被多个中断同时触发,需考虑数据保护的机制(如关闭中断或使用互斥锁)。
-
模块化设计:将回调函数与硬件初始化分离,提高代码可维护性。
通过这些例子,你可以看到回调函数在标准库中如何实现硬件事件与用户逻辑的解耦。这种设计模式是嵌入式系统的核心思想之一,后续在RTOS或复杂协议栈中也会频繁用到。
更多推荐



所有评论(0)