课时04 嵌入式技术应用设计实训 | 定时器:定时中断

嵌入式技术应用设计实训 | 定时器:定时中断

本课程开源地址(Gitee) :https://gitee.com/fujianxinxi/qianrushishixundianzi.git

课件、示例代码与验收脚本都在该仓库,可直接 git clone 或下载 ZIP 使用。

B站配套视频合集 :https://space.bilibili.com/572740825/lists?sid=9228122

合集《嵌入式技术应用设计实训|STM32F103 从点灯到智能家居》:每课配一节视频,可先看现象演示,再照着做。

开发板资料A盘(百度网盘) :https://pan.baidu.com/s/1_EXrjRUewwmGMgcy38zEZg(提取码:zbri)

正点原子官方资料下载 :http://www.openedv.com/docs/boards/stm32/zdyz_stm32f103_nano.html
模块一 基础外设开发 · 2 学时 · 环境:Keil MDK5 + VS Code + 正点原子 STM32F103 Nano V1.3

一句话概述 :从"延时不准、还占着 CPU"入手,把 TIM3 这条链路整条写在 main.c 里(开时钟 → 配 arr/psc → 开中断通道 → 启动),讲清定时时间怎么算(Tout=(arr+1)×(psc+1)/Ft,TIM3 的 Ft = 72MHz);中断入口与回调沿用课时02/03 的套路,中断里只加一个 volatile 计秒变量、翻一个灯,主循环照跑 LED 花样并用课时03 的 printf 打印秒数。

本节目标

  1. 能算出定时时间:说清 Tout=(arr+1)×(psc+1)/Ft 里三个量各是什么,并算出本课的 500 ms 是怎么来的(TIM3 的 Ft = 72MHz)
  2. 能照着 TIM3_Init() 说出四步(开时钟 → 配参数 → 开中断通道 → 启动),并把现象跑通:DS1 每 0.5 秒翻转一次、串口每秒打印一行秒数
  3. 能说出"中断入口 + 回调"这套结构和课时02/03 是同一个套路,并解释为什么中断里只加一个数、为什么计秒变量必须加 volatile

一、原理说明

课时01 用的 delay_ms() 是让 CPU 原地数数:数够时间才往下走,这段时间它什么也干不了;换个芯片、改个主频,同样的参数还不准了。定时器不一样------它是硬件里的一个计数器,由时钟自己数,数满了主动"敲门"(产生中断),CPU 在此之前爱干什么干什么。所以用定时器只要回答两个问题:"数多快、数多少个" (预分频 psc 和自动重装值 arr,两者一起决定定时时间),和**"敲了门之后干什么"**(中断回调里那一小段代码)。本课把这条链路整条写在 main.c 里,一眼能看全;中断入口与回调刻意沿用课时02 按键、课时03 串口那套"入口 + 回调"写法------这已经是你第三次见到它了,该记住它的样子了。

二、讲解内容

2.1 知识讲解

1)为什么不能一直用 delay_ms()

软件延时 delay_ms()(课时01 用过) 硬件定时器 + 中断(本课)
谁在数时间 CPU 自己原地数 硬件计数器,时钟自己在数
这段时间 CPU 能干什么 什么都不能干 想干什么干什么,到点被"敲门"
准不准 跟主频、编译器优化有关,改一处就可能偏 由时钟频率决定,准
同时干几件事 一件都干不了 主循环一件、定时器一件,并行
典型用途 闪灯、等器件稳定这种"短而糙"的等待 计秒、采样、刷新显示这种"要准、要周期"的活

2)定时时间是怎么算出来的

text 复制代码
Tout = (arr + 1) × (psc + 1) / Ft
符号 是什么 本课取值
Ft 定时器的时钟频率 72 MHz(见下面的"为什么是 72M")
psc 预分频(Prescaler):先把时钟"降速" 7200 - 1
arr 自动重装值(Period):降速后数多少个数 5000 - 1

代入:Ft / (psc+1) = 72MHz / 7200 = 10kHz(每 0.1 ms 计一个数);(arr+1) = 5000 → 5000 × 0.1ms = 500ms。这就是本课"0.5 秒"的来历------课堂上要求你自己把这行算式算一遍。

**为什么 Ft 是 72 MHz 而不是 36 MHz?**TIM3 挂在 APB1 上,本板 PCLK1 = 36 MHz;但 STM32 有一条规则:当 APB 的分频不为 1 时,接在它上面的定时器时钟 = 2 × PCLK1,所以 TIM3 拿到的是 72 MHz。这条规则不看图也能记:定时器总线上的定时器,时钟永远是 72 MHz。

厂家的注释写着"定时器时钟为 84M,分频系数为 8400-1",是错的 (那是别的芯片/别的例程抄来的),代码里传的是 7200-1。以代码和上面的算式为准------这也是提醒你:注释和代码不一致时,先自己算一遍。

3)定时器要配哪几样

字段 本课取值 说明
Instance TIM3 用哪个定时器;F103 的 TIM2~TIM7 都是通用定时器
Prescaler psc 预分频:0 表示不分频,n 表示 n+1 分频
CounterMode TIM_COUNTERMODE_UP 向上计数:0 → arr
Period arr 自动重装值:数到它就溢出(并产生更新中断)
ClockDivision TIM_CLOCKDIVISION_DIV1 时钟分频,只影响采样与死区,不影响计数周期
AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE 使能自动重载

配完这六样,HAL_TIM_Base_Init() 把它们写进寄存器;再 HAL_TIM_Base_Start_IT() 启动计数并打开"更新中断"。

4)中断的"入口 + 回调",你已经见过两次了

课时 中断入口(写在 main.c) 干活的地方(回调)
课时02 按键 EXTI0/2/9_5_IRQHandler() HAL_GPIO_EXTI_Callback()
课时03 串口 USART1_IRQHandler() HAL_UART_RxCpltCallback()
课时04 定时器 TIM3_IRQHandler() HAL_TIM_PeriodElapsedCallback()

三者是同一个套路,分三步走:

text 复制代码
硬件到点(或引脚变化、或收到字节)
        │  硬件自动
        ▼
TIM3_IRQHandler()                 ← 中断入口,向量表按这个名字找它
        │  我们调用
        ▼
HAL_TIM_IRQHandler(&TIM3_Handler) ← HAL 帮你判断是哪种中断、清掉标志位
        │  它再调用
        ▼
HAL_TIM_PeriodElapsedCallback()   ← 我们干活的地方(本课:计一个数 + 翻一个灯)

HAL_TIM_PeriodElapsedCallback() 在 HAL 库里是弱函数 (__weak),我们在 main.c 里重写一份就被调用,不用注册。

5)中断里为什么只能干"最少的活"

中断一进来,主循环就停在原地等着 :这里干得越久,整个系统响应越迟钝。本课的回报里只有两件事------g_tick++ 和 LED1 = !LED1,加起来几微秒,主循环几乎感觉不到。

反过来说,下面这些绝对不能放进回调:

别放进去 为什么
printf(...) 一个字符在 115200 下要 ~87 µs,打印 20 个字符就是 1.7 ms,而这 1.7 ms 里主循环全停着
delay_ms() 直接在中断里"睡"几百毫秒,主循环彻底卡死(课时02 已经强调过一次)
复杂计算 / 大数组搬运 中断里应该"只置标志、只记账",重活留给主循环

6)volatile:被中断改过的变量必须加

g_tick 的定义是这样的:

c 复制代码
volatile u32 g_tick = 0;      // 中断里 +1、主循环里读 → 必须 volatile

道理很直白:编译器的优化是"假设这个变量没别人动过"。在它眼里,主循环里的 g_tick 从来没被赋值,于是可能只在第一次把它读进寄存器,之后一直用那个旧值 ------现象就是:灯在闪(中断确实在跑),但串口打印的秒数永远不动。

volatile 就是告诉编译器:"这个变量可能被别人(中断)改,每次用都要老老实实回内存里重新读 "。记住一条规矩:中断里写、主循环里读的变量,一律加 volatile。

7)中断优先级

课时 外设 抢占优先级 子优先级
课时02 按键 EXTI 2 0
课时03 串口 USART1 3 3
课时04 定时器 TIM3 1 3

数值越小,优先级越高。本课串口只用来发送(阻塞式等发完,不用中断),所以 TIM3 的中断独占,不用担心被谁抢。等以后真的要用两个中断干活,就要回来想清楚"谁可以打断谁"。

8)分工:中断负责"数拍子",主循环负责"换算和显示"

text 复制代码
中断(每 0.5 秒一次):g_tick++             ← 只记账,一个数
主循环(每 200 ms 一圈):g_tick / 2 → 秒 → printf   ← 换算 + 显示

为什么不让中断直接把秒算出来、甚至直接打印?因为中断里越少干活越好(见第 5 条)。这个"中断只置标志/只记账,主循环取数据干活"的分工,是嵌入式里最常见的写法之一,实验四的加分项就是让你亲手体会一遍。

9)本课为什么把定时器代码全部写在 main.c

看哪一处 厂家写法 本课写法
定时器初始化 HARDWARE/TIMER/timer.c 的 TIM3_Init() main.c 的 TIM3_Init()
开时钟、设中断优先级 藏在 HAL_TIM_Base_MspInit()(HAL 的隐式回调)里 TIM3_Init() 里明写
中断入口 timer.c 的 TIM3_IRQHandler() main.c 的 TIM3_IRQHandler()
到点回调 timer.c 的 HAL_TIM_PeriodElapsedCallback() main.c 的回调
打开 main.c 能看到什么 一句 TIM3_Init() + 主循环 整条链路:初始化 → 中断入口 → 回调 → 主循环

代价是 HARDWARE/TIMER/timer.c 暂时空了(只留一段说明),好处是一节课就能看全,而且与课时02、课时03(中断都写在 main.c)保持一致。实验四如果想把定时器再封装成自己的库,正好练一次"分层"。

2.2 代码示例

本课改两个文件,另外两个文件"搬空" (都在 课时文档/代码/课时04-定时器:定时中断/定时器中断实验/ 下):

text 复制代码
USER/main.c                       ← 本课主角:UART1_Init / TIM3_Init / TIM3_IRQHandler / 回调 / main
SYSTEM/usart/usart.c、usart.h      ← 沿用课时03 的"只发送"版本,供 printf 使用
HARDWARE/TIMER/timer.c、timer.h    ← 搬空了,只留一段说明(代码已全部进 main.c)

工程里其余文件(HARDWARE/LED、HARDWARE/KEY、SYSTEM/sys、SYSTEM/delay、HALLIB、CORE)都是现成的,不用动。

SYSTEM/usart/usart.h(完整代码)

c 复制代码
#ifndef __USART_H
#define __USART_H
#include "stdio.h"
#include "sys.h"
//////////////////////////////////////////////////////////////////////////////////
// 《嵌入式技术应用设计实训》课时04 定时器:定时中断 ------ 串口(只用发送)
// 本文件基于正点原子 ALIENTEK NANO STM32F103 厂家例程改写,
// 原例程版权归广州市星翼电子科技有限公司所有。
//
// 本课只把课时03 里"发送"那一条路搬过来,用来 printf 打印秒数:
//      printf  ──> fputc() ──> uart_putc() ──> USART1
//      uart_send() ──────────────┘
// 课时03 的"接收 + 二进制命令"本课用不上,已经删掉;串口初始化写在 USER/main.c。
//////////////////////////////////////////////////////////////////////////////////

extern UART_HandleTypeDef UART1_Handler;        // USART1 句柄:usart.c 里定义、main.c 里初始化

void uart_send(const u8 *buf, u16 len);         // 本课唯一的发送出口
#endif

SYSTEM/usart/usart.c(完整代码)

c 复制代码
#include "sys.h"
#include "usart.h"
//////////////////////////////////////////////////////////////////////////////////
// 《嵌入式技术应用设计实训》课时04 定时器:定时中断 ------ 串口底层(只用发送)
// 本文件基于正点原子 ALIENTEK NANO STM32F103 厂家例程改写,
// 原例程版权归广州市星翼电子科技有限公司所有。
//
// 只留一条发送路径(沿用课时03 的做法):
//     ① uart_putc()  最底层:等上一个字节发完,再把一个字节写进数据寄存器
//     ② uart_send()  对外唯一出口
//     ③ fputc()      把 printf 接到 uart_putc() 上
// 串口初始化不在这里,写在 USER/main.c 的 UART1_Init() 里,看得见、不是黑盒。
//////////////////////////////////////////////////////////////////////////////////

UART_HandleTypeDef UART1_Handler;               // USART1 句柄

// 最底层:发 1 个字节
// SR 的 bit6 是 TC(发送完成标志),等它为 1 说明上一个字节发完了,再写 DR 才不会冲掉数据
static void uart_putc(u8 ch)
{
    while ((USART1->SR & 0x40) == 0);           // 等 TC = 1
    USART1->DR = ch;                            // 写数据寄存器,硬件自动一个位一个位发出去
}

// 对外唯一出口:发一串字节
void uart_send(const u8 *buf, u16 len)
{
    while (len--) uart_putc(*buf++);
    while ((USART1->SR & 0x40) == 0);           // 等最后一个字节真正发完再返回
}

// printf 重定向:printf 自己不懂串口,它把内容拼成字符串后,一个字符一个字符地调 fputc()
// Keil 里必须勾选 Options for Target → Target → Use MicroLIB,否则 printf 不出声
#pragma import(__use_no_semihosting)            // 不使用半主机模式,免得卡在 BKPT 上
struct __FILE                                   // 标准库要求有这个类型
{
    int handle;
};
FILE __stdout;
void _sys_exit(int x)                           // 屏蔽半主机相关的退出函数
{
    x = x;
}
int fputc(int ch, FILE *f)
{
    uart_putc((u8)ch);                          // 和 uart_send() 走同一条路
    return ch;
}

USER/main.c(完整代码)

c 复制代码
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
//////////////////////////////////////////////////////////////////////////////////
// 《嵌入式技术应用设计实训》课时04 定时器:定时中断
// 本文件基于正点原子 ALIENTEK NANO STM32F103 厂家例程(定时器中断实验-HAL库版本)改写,
// 原例程版权归广州市星翼电子科技有限公司所有。
//
// 硬件:正点原子 STM32F103 Nano V1.3(全部板载,不用额外接线)
//     DS0~DS7:PC0~PC7,低电平点亮
//     USART1 :PA9 = TX、PA10 = RX,板上已连到板载 ST-Link 虚拟串口,本课只用来 printf 打印秒数
//
// 本课把"定时器"这条链路全部写在 main.c 里,一节课看得全,不用在文件之间跳:
//     UART1_Init()                    串口初始化(沿用课时03,只为 printf 打印)
//     TIM3_Init()                     定时器初始化:开时钟 → 配参数 → 开中断通道 → 启动
//     TIM3_IRQHandler()               中断入口(向量表按名字找它)
//     HAL_TIM_PeriodElapsedCallback() 到点后干活的地方(本课:计一个数 + 翻一个灯)
//
// 现象:
//     DS1 每 0.5 秒翻转一次 ------ 硬件定时器中断驱动,CPU 不用等
//     DS0 每 0.2 秒翻转一次 ------ 主循环 delay_ms() 驱动,CPU 原地等 200ms
//     串口每 1 秒打印一行"已经过 n 秒",秒数由中断在后台累加
//////////////////////////////////////////////////////////////////////////////////

TIM_HandleTypeDef TIM3_Handler;                 // TIM3 句柄
volatile u32 g_tick = 0;                        // 定时器节拍:中断里 +1、主循环里读
                                                // 被中断改的变量必须加 volatile,
                                                // 否则编译器可能认为它"永远不会变"而优化掉

// ------------------------------------------------------------------------------
// 串口 1 初始化(沿用课时03):本课只用来 printf 打印秒数
//     ① 开时钟 ② 配 PA9/PA10 复用 ③ 配 115200-8-N-1
// ------------------------------------------------------------------------------
static void UART1_Init(u32 bound)
{
    GPIO_InitTypeDef GPIO_Initure;

    /* ① 开时钟(不开时钟,后面写寄存器全部无效,而且不报错) */
    __HAL_RCC_GPIOA_CLK_ENABLE();               // PA9 / PA10 在 GPIOA
    __HAL_RCC_USART1_CLK_ENABLE();              // USART1 自己
    __HAL_RCC_AFIO_CLK_ENABLE();                // STM32F1 的复用功能要经过 AFIO

    /* ② 配引脚:把 PA9、PA10 交给 USART1 去管 */
    GPIO_Initure.Pin   = GPIO_PIN_9;            // PA9 = TX
    GPIO_Initure.Mode  = GPIO_MODE_AF_PP;       // 复用推挽输出
    GPIO_Initure.Pull  = GPIO_PULLUP;
    GPIO_Initure.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOA, &GPIO_Initure);

    GPIO_Initure.Pin  = GPIO_PIN_10;            // PA10 = RX
    GPIO_Initure.Mode = GPIO_MODE_AF_INPUT;     // 复用输入
    HAL_GPIO_Init(GPIOA, &GPIO_Initure);

    /* ③ 配参数:两边必须一模一样,否则收到的全是乱码 */
    UART1_Handler.Instance        = USART1;
    UART1_Handler.Init.BaudRate   = bound;                  // 115200
    UART1_Handler.Init.WordLength = UART_WORDLENGTH_8B;     // 8 位数据位
    UART1_Handler.Init.StopBits   = UART_STOPBITS_1;        // 1 位停止位
    UART1_Handler.Init.Parity     = UART_PARITY_NONE;       // 无校验
    UART1_Handler.Init.HwFlowCtl  = UART_HWCONTROL_NONE;    // 无硬件流控
    UART1_Handler.Init.Mode       = UART_MODE_TX_RX;        // 收发都开
    HAL_UART_Init(&UART1_Handler);
}

// ------------------------------------------------------------------------------
// 定时器 3 初始化
//
// 定时时间怎么算: Tout = (arr + 1) * (psc + 1) / Ft
//     Ft = 定时器的时钟。TIM3 挂在 APB1 上,而当 APB1 的分频不为 1 时,
//          定时器时钟 = 2 × PCLK1 ------ 本板 PCLK1 = 36MHz,所以 Ft = 72MHz
//     本课取 psc = 7200 - 1:72MHz / 7200 = 10kHz,即每 0.1ms 计一个数
//             arr = 5000 - 1:数满 5000 个数 = 5000 × 0.1ms = 500ms = 0.5 秒
//     想改快改慢:只动 arr 就行(arr 减半 = 时间减半)
// ------------------------------------------------------------------------------
static void TIM3_Init(u16 arr, u16 psc)
{
    /* ① 开时钟 */
    __HAL_RCC_TIM3_CLK_ENABLE();                // 给 TIM3 使能时钟

    /* ② 配参数:预分频、计数方向、自动重装值、时钟分频、自动重载 */
    TIM3_Handler.Instance               = TIM3;                          // 用通用定时器 3
    TIM3_Handler.Init.Prescaler         = psc;                           // 预分频
    TIM3_Handler.Init.CounterMode       = TIM_COUNTERMODE_UP;            // 向上计数
    TIM3_Handler.Init.Period            = arr;                           // 自动重装值
    TIM3_Handler.Init.ClockDivision     = TIM_CLOCKDIVISION_DIV1;        // 时钟分频:不分
    TIM3_Handler.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; // 使能自动重载
    HAL_TIM_Base_Init(&TIM3_Handler);           // 把上面参数写进寄存器

    /* ③ 开中断:放行 TIM3 的中断通道(抢占 1、子 3,数值越大优先级越低) */
    HAL_NVIC_SetPriority(TIM3_IRQn, 1, 3);      // 抢占 1、子 3(数值越大优先级越低)
    HAL_NVIC_EnableIRQ(TIM3_IRQn);              // 放行中断通道

    /* ④ 启动:定时器开始计数,数满了就产生"更新中断" */
    HAL_TIM_Base_Start_IT(&TIM3_Handler);
}

// ------------------------------------------------------------------------------
// TIM3 中断入口:向量表按这个名字找它
// (和课时02 的 EXTIx_IRQHandler、课时03 的 USART1_IRQHandler 是同一个套路)
// ------------------------------------------------------------------------------
void TIM3_IRQHandler(void)
{
    HAL_TIM_IRQHandler(&TIM3_Handler);          // HAL 判断是哪种中断、清标志位,再调回调
}

// ------------------------------------------------------------------------------
// 到点回调:中断里只干"最少的活"
// 只加一个数、翻一个灯;绝不在这里 printf、绝不 delay_ms------
// 中断一进来,主循环就停在原地了,这里干得越久,整个系统响应越迟钝。
// ------------------------------------------------------------------------------
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM3)                 // 确认是 TIM3 的更新中断(多个定时器会共用这个回调)
    {
        g_tick++;                               // 0.5 秒一个"节拍",真正的秒数在 main 里换算
        LED1 = !LED1;                           // 翻一下 DS1,让人"看见"中断在跑
    }
}

int main(void)
{
    u32 sec = 0;                                // 已经打印过的秒数

    HAL_Init();                                 // 初始化 HAL 库:顺带配好 1ms 时基
    Stm32_Clock_Init(RCC_PLL_MUL9);             // 系统时钟:倍频到 72MHz
    delay_init(72);                             // 初始化延时函数
    LED_Init();                                 // 初始化 8 个 LED(PC0~PC7),上电默认全灭
    UART1_Init(115200);                         // 串口 1:只用来 printf 打印
    TIM3_Init(5000 - 1, 7200 - 1);              // TIM3:72MHz/7200=10kHz,数满 5000 个 → 0.5 秒一次中断

    printf("\r\n");
    printf("嵌入式技术应用设计实训 | 课时04 定时器:定时中断\r\n");
    printf("DS1 由 TIM3 中断每 0.5 秒翻转;DS0 由主循环 delay_ms(200) 翻转\r\n\r\n");

    while (1)
    {
        LED0 = !LED0;                           // DS0:靠软件延时闪(CPU 原地等 200ms)
        delay_ms(200);

        if (g_tick / 2 != sec)                  // 中断每 0.5 秒 +1,所以 2 个节拍 = 1 秒
        {
            sec = g_tick / 2;                   // 主循环只负责"把节拍换算成人看的秒"
            printf("已经过 %u 秒\r\n", sec);     // printf → fputc → uart_putc,和课时03 同一条路
        }
    }
}

HARDWARE/TIMER/timer.c、timer.h(本课为"空壳")

厂家把定时器代码放在这两个文件里,本课把它们搬进了 main.c,所以这两个文件只剩一段说明(原文见附录 B)。工程里保留它们,是为了不动 .uvprojx;实验四若想把定时器封装成自己的库,就从这里开始写。

代码里最值得盯住的 4 处

位置 为什么这么写
TIM3_Init() 的四步 开时钟 → 配参数 → 开中断通道 → 启动;和课时01 点灯、课时03 串口一个套路,看得见
volatile u32 g_tick 中断里 +1、主循环里读;不加 volatile 可能永远读到旧值(秒数不动)
HAL_TIM_PeriodElapsedCallback() 里只有两行 中断里只加一个数、翻一个灯,几微秒就出来;printf/delay_ms 一律不许进
g_tick / 2 放在主循环 中断只"数拍子",换算和打印交给主循环------中断里越少干活越好

三、实验操作与现象

3.1 接线与操作步骤

本课不用接杜邦线:LED 在板上,串口走板载 ST-Link 的虚拟串口(和课时03 一样,下载与串口共用那根 USB 线)。

设备 接到哪 说明
板载 ST-Link(下载 + 串口一体) 板上标着 ST-Link 的 USB 口 → 电脑 USB 口 一根线管两件事:下载程序 + 串口收发;本课只用串口看 printf 打印的秒数
板上的 SWD 排针 (本课程不接) 备用口:只有换外接 下载器时才接 SWDIO / SWCLK / GND

操作步骤(照着做就能跑):

  1. 上电接线 :板上标着 ST-Link 的 USB 口用一根线插到电脑(供电 + 下载 + 串口都走它),不用再接别的线
  2. Keil 打开工程 :课时文档/代码/课时04-定时器:定时中断/定时器中断实验/USER/TIMER.uvprojx;Options for Target → Debug 选 ST-Link Debugger;到 Target 勾上 Use MicroLIB (不勾,printf 不出声)
  3. 先自己算一遍 (这是本课的重点练习):TIM3_Init(5000 - 1, 7200 - 1) → 72MHz / 7200 = 10kHz → 5000 × 0.1ms = 500ms
  4. 编译 :Build(F7)确认 0 Error(s), 0 Warning(s)
  5. 下载并连串口 :Download(F8);串口助手选课时03 记下的那个 COM 号(STLink Virtual COM Port),115200-8-N-1,打开串口
  6. 按复位键看现象 :对照 3.2 逐条核对------重点看两个灯频率不一样 、秒数在递增

3.2 观察到的现象

本课现象以随课视频演示为准;文档不逐条配截图,你自己做完请拍一张或录一段,作为提交物。

看哪里 看到什么算成功 怎么核对
板上 DS1 每 0.5 秒翻转一次 (约每秒明暗各一次);DS0 每 0.2 秒翻转一次(闪得快得多) 两个灯频率明显不同、互不影响------这就是"中断在后台跑、主循环在台上跑"
串口 上电两行欢迎语,随后每秒一行 :已经过 0 秒、已经过 1 秒、已经过 2 秒...... 秒数连续递增、不跳号、不重复;哪怕主循环在忙着闪灯,秒数照样走

复位后的时间轴:

时刻 看到
0 ms 串口出现两行欢迎语;DS0、DS1 都开始翻转
每 200 ms DS0 翻转一次(软件延时驱动)
每 500 ms DS1 翻转一次(TIM3 中断驱动)
每 1 s 串口多一行"已经过 n 秒"(主循环把节拍换算出来打印)

图注:串口每秒打印一行秒数,秒数由定时器中断在后台累加(截图待补)

看不到现象怎么办(按最可能的顺序查):

现象 最可能的原因 怎么处理
两个灯都不动、串口也不打印 定时器没启动,或中断通道没放行 检查 TIM3_Init() 里 HAL_NVIC_EnableIRQ(TIM3_IRQn) 和 HAL_TIM_Base_Start_IT() 都在
DS1 不动,但 DS0 在闪 中断根本没进来 核对入口函数名 TIM3_IRQHandler 拼写(向量表按名字找它,写错就永远进不来)
灯在闪,但秒数一直停在 0 g_tick 没加 volatile,被编译器优化成"只读一次" 补上 volatile 重新编译
串口一个字都没有 没勾 Use MicroLIB;或 COM 口 / 波特率选错 先确认 MicroLIB,再核对 COM 与 115200-8-N-1(详见课时03 排查表)
灯闪的频率和算出来的不一样 arr / psc 算错,或改完没重新编译下载 按 Tout=(arr+1)×(psc+1)/Ft 重算,Build+Download
程序像卡住了、串口输出断断续续 在回调里加了 printf 或 delay_ms 回调里只留 g_tick++ 和翻灯

执行前后对比(改一个数,看一眼差别)

操作 改之前 改之后
TIM3_Init(5000 - 1, 7200 - 1) → (10000 - 1, 7200 - 1) DS1 每 0.5 秒翻一次 每 1 秒翻一次(慢一倍)------证明"时间是算出来的"
→ (1000 - 1, 7200 - 1) 0.5 秒 0.1 秒(快得几乎看不清)
psc 从 7200 - 1 改成 720 - 1(arr 不动) 计数频率 10kHz 计数频率 100kHz → 同样 5000 个数只要 50ms
把 volatile 去掉、优化等级调到 Level 3 秒数正常递增 秒数可能一直不动 (灯照闪)------这就是 volatile 的作用

四、学生练习

课堂上按顺序做,一条一条改、一条一条看现象:

编号 任务 提示 预期结果
P4.1 改时间 把 TIM3_Init(5000 - 1, 7200 - 1) 改成 (10000 - 1, 7200 - 1),再改成 (1000 - 1, 7200 - 1),各编译下载一次 DS1 分别是 1 秒、0.1 秒翻一次;能用公式把两次都算通
P4.2 自己算一遍 要求 DS1 每 2 秒 翻转一次,arr 该填多少?(psc 仍用 7200 - 1) 算得 arr = 20000 - 1(10kHz 下数 20000 个数 = 2 秒),改完现象对上
P4.3 亲眼见证 volatile 把 volatile 去掉,再把 Options for Target → C/C++ → Optimization 调到 Level 3,重新编译下载;看完记得改回来 灯还在闪,但串口秒数可能一直是 0 ------这就是不加 volatile 的后果
P4.4 改成毫秒表 把 g_tick / 2 改成 g_tick * 500,打印改成"已经过 n 毫秒" 串口每 0.5 秒打印一个 500 的倍数:0 / 500 / 1000 / 1500...
P4.5 亲手体验"中断里不能干重活" 在回调里加一句 printf("tick\r\n");,重新编译下载,盯住主循环打印的节奏和灯闪 主循环明显被拖慢、秒数开始"跳号",灯闪也变怪;看完把这句删掉
P4.6 挑战:中断只置标志 加 volatile u8 g_flag = 0;,回调里只写 g_flag = 1;(不再直接 g_tick++),主循环里 if (g_flag) { g_flag = 0; g_tick++; } 现象和原来一样,但"中断只置标志、主循环干活"的分工更彻底------实验四的加分项就是这个

五、大作业(实验四:定时器秒表与 8LED 花样)

本作业对应课程大纲里的实验四「定时器秒表与 8LED 花样」,当堂完成、当堂验收;做不完的课后接着做,下次课开课先收。

5.1 任务书

把课上的"一个秒表 + 两个灯",做成"一个秒表 + 8 个灯的花样",两件事同时进行:

  1. TIM3 每 1 秒 进一次中断:arr / psc 自己算,把算式写在注释里 ;中断里只做 sec++(可以顺便翻一个灯),不许 printf、不许 delay_ms
  2. 主循环跑 8LED 花样 :DS0→DS7 依次点亮(每步 200 ms),再从 DS7→DS0 折回来,来回不停------就是课时01 大作业里那个 led_show(dir, ms)
  3. 每跑完一个来回,用串口 printf 打印一行 sec = n(秒数来自中断里累加的那个变量)
  4. 两件事互不干扰:花样照跑(不被秒表打断),秒数也照走(不因花样变慢,±1 秒以内)
  5. 中断里改、主循环里读的变量必须加 volatile
  6. 编译输出 0 Error(s), 0 Warning(s)

加分项(任选,做几项算几项):

  • "中断只置标志"写法 :回调里只写 g_flag = 1;,主循环看到 g_flag 才取数据、才打印(也就是 P4.6 那一套)
  • 用课时02 的按键做清零 / 暂停秒表:按 KEY0 把秒表归零,再打印一行"秒表已清零"
  • 中断周期改成 0.1 秒 ,打印带一位小数的秒表:10.3 秒 这种

挑战项:

  • 把 8LED 花样搬进中断(每 10 ms 中断换一个灯),主循环只剩打印------然后观察串口打印是否被打断、是否有字符丢失,并写一句话说明**"为什么重活不能放进中断"**

5.2 分步提示

  1. 先把秒表单独做出来 :把 TIM3_Init(5000 - 1, 7200 - 1) 改成 (10000 - 1, 7200 - 1)(1 秒),确认串口每秒打印一行
  2. 再接花样 :把课时01 大作业写的 led_show(dir, ms) 放进主循环(led_show(0, 200); led_show(1, 200);),一圈来回约 3.2 秒
  3. 打印时机放在主循环 :每跑完一个来回打印一次 sec;千万不要在回调里打印
  4. 按键清零(加分) :把课时02 的 KEY_EXTI_Init() 和 HAL_GPIO_EXTI_Callback() 一起搬过来------注意它和 HAL_TIM_PeriodElapsedCallback() 是两个不同名字的弱函数,不会打架;回调里只做 sec = 0;
  5. 最后自检 :volatile 加了没有?回调里是不是只有一两行?0 Warning 有没有守住?

5.3 现象与验收标准

验收项 标准
秒表 串口每秒打印一行 sec = n,n 连续递增、不跳号、不错乱
花样 DS0→DS7→DS0 来回流水,任意时刻只有 1 个灯亮,每步 200 ms
并行 花样不停顿、秒数不变慢(两个来回之间秒数应增加 3~4 秒)
中断里干净 回调里只有 sec++(和可选的翻灯 / 置标志),没有 printf、没有 delay_ms
volatile 中断里改、主循环里读的变量都加了 volatile
编译下载 0 Error(s), 0 Warning(s);Programming Done. Verify OK.
加分 / 挑战 按 5.1 里的规定逐项现场演示(做多少算多少)

5.4 提交物

  • 代码:课时文档/代码/课时04-定时器:定时中断/定时器中断实验/USER/main.c
  • 现象:一段 20 秒以内的视频(能同时看到 8 灯流水和串口秒数)
  • 一句话说明:"你的秒表为什么准?中断里为什么不打印?"

六、本节小结与下节预告

本节把定时器这条线完整走了一遍:先看清 delay_ms() 与硬件定时的区别 (一个让 CPU 原地数、一个让硬件自己数);再学会算定时时间 ------Tout = (arr+1)×(psc+1)/Ft,本课 psc = 7200-1 得 10 kHz、arr = 5000-1 得 500 ms ,而 TIM3 的 Ft 之所以是 72 MHz,是因为"APB1 分频不为 1 时,定时器时钟 = 2 × PCLK1";然后把中断入口 + 回调 这套写法第三次用了一遍(TIM3_IRQHandler → HAL_TIM_IRQHandler → HAL_TIM_PeriodElapsedCallback);最后把"中断只数拍子、主循环读数据干活 "这个分工跑成了现象:DS1 每 0.5 秒由中断翻转、DS0 每 0.2 秒由主循环翻转、串口每秒打印一行秒数。三句话记住本课:时间是算出来的 (arr/psc/Ft 三个量);中断里要短 (只加一个数,绝不 printf、绝不 delay_ms);中断改的变量要 volatile。下节进入 PWM:用定时器的比较输出,让灯的亮度"随手可调"(呼吸灯)。

本节函数与宏速查

名称 一句话作用
TIM3_Init(arr, psc) 定时器初始化(本课写在 main.c):开时钟 → 配参数 → 开中断通道 → 启动
__HAL_RCC_TIM3_CLK_ENABLE() 给 TIM3 使能时钟(不开时钟,后面全白写)
HAL_TIM_Base_Init(&htim) 把 Init.* 里的参数(psc / arr / 计数方向...)写进寄存器
HAL_TIM_Base_Start_IT(&htim) 启动计数,并打开"更新中断"(到点就来敲门)
HAL_NVIC_SetPriority(TIM3_IRQn, 1, 3) 给 TIM3 中断排优先级:抢占 1、子 3(数值越大越低)
HAL_NVIC_EnableIRQ(TIM3_IRQn) 放行 TIM3 的中断通道
TIM3_IRQHandler() 中断入口函数(向量表按名字找它),里面调 HAL_TIM_IRQHandler()
HAL_TIM_IRQHandler(&htim) HAL 提供的"判断中断来源 + 清标志 + 转回调"
HAL_TIM_PeriodElapsedCallback(htim) 弱函数,定时器数满一圈时被调用;本课在这里 g_tick++ 和翻灯
volatile 告诉编译器"这个变量可能被中断改,每次都要回内存重新读"
delay_ms(n) 软件延时(CPU 原地等),不能放进中断

附录

A. 怎么编译下载(照着做就能跑)

text 复制代码
1. 接线:板子上标着 ST-Link 的 USB 口用一根线插电脑(供电 + 下载 + 串口都走它)
2. Keil 打开:课时文档/代码/课时04-定时器:定时中断/定时器中断实验/USER/TIMER.uvprojx
3. 选下载器:Options for Target → Debug → ST-Link Debugger → OK
4. 勾 MicroLIB:Options for Target → Target → 勾 Use MicroLIB(printf 才出声)
5. 编译    :Build(F7),确认 0 Error(s), 0 Warning(s)
6. 下载    :Download(F8),确认 Programming Done. Verify OK.
7. 看现象  :串口助手选对 COM 口、115200-8-N-1 并打开串口,按 RST:
             DS1 每 0.5 秒翻转(TIM3 中断);DS0 每 0.2 秒翻转(主循环 delay_ms);
             串口每秒打印一行"已经过 n 秒"

B. 本课代码

本课改两个文件、搬空两个文件(完整代码见 2.2 节)。工程里其余文件都是厂家例程现成的,不用动:

文件 作用
USER/TIMER.uvprojx Keil 工程文件(用 Keil 打开这个)
USER/main.c 本课主角 :UART1_Init()、TIM3_Init()、TIM3_IRQHandler()、HAL_TIM_PeriodElapsedCallback()、main()
SYSTEM/usart/usart.h 串口(只发送)接口:uart_send() 声明、UART1_Handler 外部声明
SYSTEM/usart/usart.c 串口底层:uart_putc() / uart_send() / fputc()(沿用课时03 的"只发送"版本)
HARDWARE/TIMER/timer.h、timer.c 搬空了 ,只剩一段说明(代码已进 main.c);保留是为了不动 .uvprojx
HARDWARE/LED/led.c、led.h LED 驱动:PC0~PC7 配成推挽输出并上电置全灭,提供 LED0~LED7 宏
HARDWARE/KEY/ 厂家按键驱动(本课不用;实验四的加分项要按键清零时可以用课时02 的写法)
SYSTEM/sys、SYSTEM/delay 时钟配置、SysTick 延时(delay_ms)
HALLIB/、CORE/ STM32F1 HAL 库与启动文件
OBJ/ 编译输出(已加入 .gitignore,不进仓库)

HARDWARE/TIMER/timer.h(完整代码)

c 复制代码
#ifndef _TIMER_H
#define _TIMER_H
//////////////////////////////////////////////////////////////////////////////////
// 《嵌入式技术应用设计实训》课时04 定时器:定时中断
//
// 说明:本课把定时器的代码全部写在 USER/main.c 里------
//         TIM3_Init()                     定时器初始化(开时钟 → 配参数 → 开中断 → 启动)
//         TIM3_IRQHandler()               中断入口
//         HAL_TIM_PeriodElapsedCallback() 到点后干活的地方
//       一节课就能看全"初始化 → 中断 → 干什么",不用在文件之间跳。
//
//       所以本文件暂时只留这段说明,没有任何声明。
//       工程里保留 HARDWARE/TIMER 目录,是为了和厂家例程的工程结构一致(.uvprojx 不用改);
//       做实验四时若想把定时器封装成自己的库,就从这里开始加。
//////////////////////////////////////////////////////////////////////////////////
#endif

HARDWARE/TIMER/timer.c(完整代码)

c 复制代码
//////////////////////////////////////////////////////////////////////////////////
// 《嵌入式技术应用设计实训》课时04 定时器:定时中断
//
// 本文件是厂家例程(定时器中断实验-HAL库版)里放定时器代码的地方,厂家写法是:
//         TIM3_Init()                     初始化
//         HAL_TIM_Base_MspInit()          开时钟、设中断优先级(藏在 HAL 回调里)
//         TIM3_IRQHandler()               中断入口
//         HAL_TIM_PeriodElapsedCallback() 到点后翻 LED1
// 四个东西全在这个文件里,学生打开 main.c 只能看到一句 TIM3_Init(),
// "中断在哪、中断里干什么"完全看不见------课时02、课时03 已经踩过这个坑。
//
// 本课改法(与课时02、课时03 一致):把它们全部搬进 USER/main.c,看得见全链路:
//         USER/main.c:UART1_Init() / TIM3_Init() / TIM3_IRQHandler() /
//                      HAL_TIM_PeriodElapsedCallback() / main()
//
// 所以本文件暂时为空,只留这段说明。工程里保留它,是为了不动 .uvprojx;
// 做实验四时如果想把定时器封装成自己的库(比如加 TIM3_GetTick()),就从这里开始写。
//////////////////////////////////////////////////////////////////////////////////

C. 定时器参数与时间速查(TIM3,Ft = 72 MHz)

psc 计数频率 arr 定时时间
7200 - 1 10 kHz(每个数 0.1 ms) 1000 - 1 100 ms
7200 - 1 10 kHz 2000 - 1 200 ms
7200 - 1 10 kHz 5000 - 1 500 ms(本课)
7200 - 1 10 kHz 10000 - 1 1 s
7200 - 1 10 kHz 20000 - 1 2 s
720 - 1 100 kHz(每个数 0.01 ms) 5000 - 1 50 ms
36000 - 1 2 kHz(每个数 0.5 ms) 2000 - 1 1 s

提醒 :psc 和 arr 都是 16 位 (0 ~ 65535),也就是"最多数 65536 个数"。要定更长的时间,先用 psc 把频率降下来(本课要 1 秒,10 kHz 下 arr = 10000 - 1 就够了,不需要硬撑到 65535)。

相关文档

相关推荐
阿钱真强道9 小时前
课时03 嵌入式技术应用设计实训 | 串口通信:USART 收发二进制 与 printf
串口·usart·二进制协议·printf重定向·stm32f103·嵌入式入门·中断接收
阿钱真强道1 天前
课时02 嵌入式技术应用设计实训 | GPIO 输入与外部中断:按键
外部中断·gpio·按键·nvic·stm32f103·嵌入式入门·消抖
Thinker QAQ3 天前
并发编程(七):volatile——从语言规则到 CPU
java·python·go·并发编程·volatile·memory model
阿钱真强道4 天前
课时00 嵌入式技术应用设计实训 | 开发环境搭建与开发板认识
git·hal库·keil·vs code·开发环境搭建·stm32f103·st-link
阿钱真强道4 天前
课时01 嵌入式技术应用设计实训 | GPIO 输出:跑马灯与蜂鸣器
hal库·keil·gpio·蜂鸣器·跑马灯·推挽输出·stm32f103
阿钱真强道9 天前
18 嵌入式操作系统 | uloop 定时器:周期任务与网络上报
c语言·定时器·周期任务·uloop
晊晌_h10 天前
IMX6ULL裸机开发:定时器(GPT/EPIT)与UART串口
arm·定时器·arm裸机开发
AAA代码批发商15 天前
Days 50 51 单片机与 i.MX6ULL 定时器对比:Timer、EPIT、GPT 原理与实验
51单片机·定时器·imx6ull·裸机开发
捷瑞电子工坊16 天前
蓝桥杯嵌入式STM32G431定时器入门:从原理到CubeMX配置(LL库)
cubemx·定时器·定时中断·led闪烁·蓝桥杯嵌入式·stm32g431·ll库