avatar🌌
零醇零醇の小屋

樱花树下,再续前缘

鹤望兰无凋零之时,比翼鸟永世长存

STM32 HAL库入门教程(六):定时器定时中断 —— 彻底摆脱被 HAL_Delay 绑架的 CPU!

还记得第一篇教程里 我们是怎么让小灯泡每隔一秒闪烁一次的吗? 没错 靠的是无脑调用 HAL_Delay(1000)( 但是到了第三篇讲中断的时候 大家就发现了这个函数的毒瘤之处: 它是“阻塞延时”!在这整整一秒钟里 cpu 就像被施了定身咒一样在原地傻站着 别的代码一行也跑不了 如果这时候有按键按下了、串口来数据了 cpu 全都装聋作哑假装看不见 简直是摸鱼界的终极摆烂王!哼(`へ´)

这就像你定了个 3 分钟的泡面定时 但是在这 3 分钟里 你两只眼睛死死盯着泡面桶发呆 手机不敢看 游戏不敢玩 啥事都耽误了( 那能不能给单片机装一个“后台小闹钟”? 平时让 cpu 开开心心去打游戏算数据 只要时间一到 闹钟“叮——”地响一下通知 cpu 过来翻转一下灯泡 翻转完继续回去打游戏? 今天 我们就来认识这个终极摸鱼神器——定时器定时中断!(。・ω・。)ノ♡

肯定有小伙伴举手提问啦:“哎等等!上一篇不是刚讲过定时器 PWM 吗?怎么这篇又是定时器?这俩到底有什么区别呀喵?头好痒感觉要长脑子了(” 别慌别慌!为了防止大家把这两样东西搞混 我们先用大白话彻底把它们捋清楚!

看图 图0 图片仅为示意 7200和10000没办法画 其实“定时器(TIM)”就像是一个自带发条的“齿轮小秒表” 它在后台唯一的工作就是数数(从 0 数到 ARR 不停循环) 而我们这两期学的 相当于给这个小秒表接了两种完全不同的输出方式:

  1. “定时器中断(这节课的主角)”: 它是一个【闹钟】! 当秒表数到头的时候 这个信号会直接打断正在摸鱼的 cpu 把它拖进中断函数里去! 也就是说:“定时中断是需要 cpu 亲自下场干活的!” 所以定时中断一般不用太快(比如 1 秒响一次、或者 10ms 响一次) 要是一秒钟响上万次 cpu 光顾着跑中断了 主循环就寄了(

  2. “PWM 输出(上一节课的内容)”: 它是一个【自动开关】! 它是把秒表直接连到了芯片内部的一个电平比较器上 硬件自己拿着计数值去跟我们设定的脉冲值做对比 到了时间硬件自己把 PA0 引脚拉高或拉低,实现输出亮度变化的效果。

简单来说一句话: pwm调亮度 定时器中断是设置闹钟时间 理解了吗,喵~ (=´ω`=)

理清了区别 我们再来看看定时中断的计算公式 其实跟上一篇一模一样: 定时时间 T = ((PSC + 1) * (ARR + 1)) / 72MHz

这次我们想定一个“正好 1 秒(1000ms)”响一次的小闹钟 怎么凑整数组合呢? 我们来做个简单的小学乘除法: 把 PSC 设成 7200 - 1: 心跳频率变成 72MHz / (7199 + 1) = 10kHz(也就是一秒钟跳 10000 下 每一个刻度正好是 0.1ms) 把 ARR 设成 10000 - 1: 让计数器从 0 数到 9999 刚好数了 10000 下! 10000 * 0.1ms = 1000ms = 1秒! 完美凑出 1 秒钟的整数闹钟 强迫症患者当场感动落泪喵~

原理搞明白了 打开我们熟悉的 cubemx! 选好 c8t6 老规矩配好时钟和调试口 顺便把我们板载的 PC13 配成 GPIO_Output(这次可以直接用板载小灯泡啦 不需要外接面包板哦!) 在左侧找到 Connectivity 也可以顺手把 USART1 开上(波特率 115200 待会可以用上一篇刚学的 printf 报时!)

重点来啦!在左侧找到 Timers -> 点击 TIM2: 把 Clock Source 选为 Internal Clock(内部时钟) 在下面的参数配置里: Prescaler 填 7199 Counter Period 填 9999

图1

还没完哦!既然是定时“中断” 那就必须让大管家 NVIC 给小闹钟通电! 切换到 TIM2 的 NVIC Settings 选项卡: 把“TIM2 global interrupt”右边的 Enabled 勾上! 忘勾的话 定时器数到时间也触发不了中断 闹钟就变成哑巴啦喵~

图2

配置好工程 点击生成代码 打开 Keil 工程!

写代码前 别忘了上一篇说的:如果要用 printf 记得在魔术棒 Target 勾上 Use MicroLIB 哦! 打开 main.c 先在 USER CODE BEGIN 2 里面启动定时器中断:

c
/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim2);
printf("喵~ 定时器闹钟已经启动啦!\r\n");
/* USER CODE END 2 */

注意看!这个函数叫 HAL_TIM_Base_Start_IT 名字后面带了个“_IT” 这就代表不仅启动定时器 还会自动开启中断哦!

那闹钟响了之后 单片机会去哪里执行代码呢? 当当当当!又到了熟悉的“弱定义(__weak)空盘子”环节啦(星星眼) 官方在底层帮我们写好了定时器中断回调函数: void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) 我们在 main.c 最下面找到 USER CODE BEGIN 4 把我们自己的回调函数端上来:

c
/* USER CODE BEGIN 4 */
uint32_t seconds = 0;

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    // 判断是不是 TIM2 响的闹钟
    if (htim->Instance == TIM2)
    {
        HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); // 翻转板载小灯泡
        seconds++;
        printf("叮咚~ 闹钟响啦!当前运行时间:%d 秒 喵~\r\n", seconds);
    }
}
/* USER CODE END 4 */

图3-1图3-2

代码写完了!看一眼上面的 while(1) 主循环:

c
/* USER CODE BEGIN WHILE */
while (1)
{
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    // 里面空空如也!一行阻塞延时都没有!
}
/* USER CODE END 3 */
图4

编译 下载 烧录! 插上串口助手 打开波特率 115200:

图5

看!板子上的 PC13 蓝灯极其规律地每隔一秒翻转一次 电脑屏幕上一秒钟精准跳出来一行报时消息! 最爽的是 此时的 while(1) 主循环完全是自由的!你想在主循环里算数学题也好、检测按键也罢 都不用再被恶心的 HAL_Delay 卡脖子啦!

完结撒花!(=^・ω・^=)! ​

💻 本教程完整工程源码已开源:
👉 TomSorachan/stm32-hal-tutorials

STM32 HAL库入门教程(七):ADC 模数转换 —— 测电压与读取真实世界模拟量!
STM32 HAL库入门教程(五):串口通信(UART)与 printf 重定向