【九】定时器1(TIM1)LED闪烁实验
1. 项目概述
本项目演示如何使用 STM32F103C8T6 微控制器的 TIM1 定时器,通过定时器中断实现 LED 的定时闪烁功能。LED 每隔 500ms 翻转一次状态(亮/灭),整个过程由定时器中断自动完成,无需在主循环中执行任何操作。
技术要点
- ✅ TIM1 定时器初始化与配置
- ✅ 定时器中断服务程序(ISR)
- ✅ NVIC 中断优先级配置
- ✅ GPIO 输出控制 LED
- ✅ JTAG 引脚重映射
2. 硬件平台
主控芯片
- 型号: STM32F103C8T6
- 内核: ARM Cortex-M3
- 主频: 72 MHz
- Flash: 64 KB
- SRAM: 20 KB
外设资源
| 外设 | 引脚 | 功能 | 说明 |
|---|---|---|---|
| LED1 | PB0 | 数字输出 | 发光二极管1 |
| LED2 | PB1 | 数字输出 | 发光二极管2 |
| LED3 | PB2 | 数字输出 | 发光二极管3 |
| LED4 | PB3 | 数字输出 | 发光二极管4 |
| LED5 | PB4 | 数字输出 | 发光二极管5 |
| LED6 | PB5 | 数字输出 | 发光二极管6 |
| LED7 | PB6 | 数字输出 | 发光二极管7 |
| LED8 | PB7 | 数字输出 | 发光二极管8 |
| TIM1 | - | 定时器 | 用于产生500ms定时中断 |
硬件连接
STM32F103C8T6 LED模块
PB0 ----------> LED1 (正极)
PB1 ----------> LED2 (正极)
PB2 ----------> LED3 (正极)
PB3 ----------> LED4 (正极)
PB4 ----------> LED5 (正极)
PB5 ----------> LED6 (正极)
PB6 ----------> LED7 (正极)
PB7 ----------> LED8 (正极)
LED负极 ----------> GND (通过限流电阻)
注意:
- LED 为共阴接法,GPIO 输出高电平时 LED 点亮
- 每个 LED 需串联适当的限流电阻(建议 330Ω - 1KΩ)
3. 项目结构
6A_TIM1/
├── Libraries/ # STM32标准外设库
│ ├── CMSIS/ # Cortex微控制器软件接口标准
│ └── STM32F10x_StdPeriph_Driver/ # STM32F10x标准外设驱动
├── MDK/ # Keil工程文件
│ ├── TEST_CODE.uvproj # Keil项目文件
│ ├── TEST_CODE.uvopt # 项目配置选项
│ ├── Output/ # 编译输出目录
│ └── list/ # 列表文件目录
├── Readme/ # 说明文档目录
└── User/ # 用户代码目录
├── main.c # 主程序入口
├── stm32f10x_conf.h # 外设库配置文件
├── stm32f10x_it.c # 中断服务函数(标准)
├── stm32f10x_it.h # 中断服务函数头文件
├── system_stm32f10x.c # 系统初始化文件
└── TF_APP/ # 应用层驱动
├── TF_LED.c # LED驱动实现
├── TF_LED.h # LED驱动头文件
├── TF_Timer.c # 定时器驱动实现
└── TF_Timer.h # 定时器驱动头文件
4. 功能说明
系统工作流程
系统上电
↓
NVIC中断配置
↓
TIM1定时器初始化
(设置500ms定时)
↓
LED GPIO初始化
↓
TIM1开始计数
↓
主循环等待
↓
每隔500ms触发中断
↓
TIM1中断服务函数
↓
翻转LED1状态
↓
返回主循环
功能特点
- 中断驱动: 采用中断方式实现定时控制,主循环无需执行任何操作
- 精确定时: 使用硬件定时器,定时精度高,不受软件延时影响
- 低功耗: 主循环空闲,可进入低功耗模式(本例未实现)
- 易于扩展: 可在中断中添加更多功能,或修改定时周期
5. 定时器原理
STM32 定时器简介
STM32F103C8T6 具有多个定时器资源:
- TIM1: 高级定时器(16位),支持PWM、输入捕获、输出比较等
- TIM2-TIM4: 通用定时器(16位)
- 其他: 基本定时器、看门狗定时器等
TIM1 特点
| 特性 | 说明 |
|---|---|
| 类型 | 高级定时器 |
| 位数 | 16位 |
| 时钟源 | APB2总线时钟(72MHz) |
| 计数模式 | 向上计数、向下计数、中央对齐 |
| 预分频器 | 16位(0-65535) |
| 自动重装载 | 16位(0-65535) |
| 重复计数器 | 8位(TIM1特有) |
| 中断类型 | 更新中断、捕获/比较中断等 |
定时器计数原理
定时器的定时时间计算公式:
T = ((1 + TIM_Prescaler) / FCLK) × (1 + TIM_Period)
参数说明:
T: 定时时间(秒)TIM_Prescaler: 预分频器值(0-65535)FCLK: 定时器时钟频率(Hz)TIM_Period: 自动重装载值(0-65535)
本项目配置:
FCLK = 72,000,000 Hz (72MHz)
TIM_Prescaler = 7199 (实际分频系数 = 7200)
TIM_Period = 4999 (实际计数值 = 5000)
T = ((1 + 7199) / 72,000,000) × (1 + 4999)
= (7200 / 72,000,000) × 5000
= 0.0001 × 5000
= 0.5 秒
= 500 毫秒
定时器工作过程
TIM1时钟使能
↓
预分频器分频
(72MHz → 10KHz)
↓
计数器从0开始向上计数
↓
计数到4999时产生更新事件
↓
触发更新中断
↓
计数器清零,重新开始计数
6. 代码详解
6.1 主程序 (main.c)
完整代码
#include "TF_LED.h"
#include "TF_Timer.h"
/************************************************************************************************/
int main (void)
{
NVIC_Configuration(); // 配置中断优先级
TIM1_Configuration(); // 初始化定时器1,设定定时500ms
LED_Init(); // 初始化LED所对应IO口
while(1) // 程序循环
{
// 主循环中不需要做任何事情,中断服务函数会自动处理LED翻转
}
}
代码说明
| 函数调用 | 功能 | 说明 |
|---|---|---|
NVIC_Configuration() | 配置中断优先级 | 设置TIM1中断优先级分组和抢占优先级 |
TIM1_Configuration() | 初始化定时器 | 配置TIM1为500ms定时中断 |
LED_Init() | 初始化LED | 配置GPIO为推挽输出模式 |
while(1) | 主循环 | 空循环,所有工作由中断完成 |
设计思想:
- 采用事件驱动架构,主循环空闲
- 所有定时任务在中断服务函数中完成
- 便于后续添加其他功能或进入低功耗模式
6.2 定时器驱动 (TF_Timer)
TF_Timer.h - 头文件
#ifndef __TF_TIMER_H__
#define __TF_TIMER_H__
#ifdef __cplusplus
extern "C"
{
#endif
#include "stm32f10x_rcc.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_tim.h"
#include "misc.h"
// 函数声明
extern void NVIC_Configuration(void); // 配置TIM1中断优先级
extern void TIM1_Configuration(void); // 初始化TIM1--设定定时500ms
#ifdef __cplusplus
}
#endif
#endif
TF_Timer.c - 实现文件
1. 中断优先级配置
void NVIC_Configuration(void) // 配置TIM1中断优先级
{
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 设置NVIC中断分组2
NVIC_InitStructure.NVIC_IRQChannel = TIM1_UP_IRQn; // TIM1更新中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 抢占优先级0
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 子优先级0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能IRQ通道
NVIC_Init(&NVIC_InitStructure); // 初始化NVIC寄存器
}
NVIC 中断优先级分组说明:
| 分组 | 抢占优先级位 | 子优先级位 | 说明 |
|---|---|---|---|
| Group 0 | 0 位 | 4 位 | 无抢占,0-15级子优先级 |
| Group 1 | 1 位 | 3 位 | 2级抢占,0-7级子优先级 |
| Group 2 | 2 位 | 2 位 | 4级抢占,0-3级子优先级 ⬅️ 本例使用 |
| Group 3 | 3 位 | 1 位 | 8级抢占,0-1级子优先级 |
| Group 4 | 4 位 | 0 位 | 16级抢占,无子优先级 |
本项目配置:
- 抢占优先级: 0(最高优先级)
- 子优先级: 0(最高优先级)
- 中断通道: TIM1_UP_IRQn(TIM1更新中断)
2. 定时器初始化
void TIM1_Configuration(void) // 初始化TIM1--设定定时500ms
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);
// T=((1+TIM_Prescaler )/72M)*(1+TIM_Period )=((1+7199)/72M)*(1+4999)=0.5秒
TIM_TimeBaseStructure.TIM_Period = (5000-1); // 自动重装载值
TIM_TimeBaseStructure.TIM_Prescaler = (7200-1); // 预分频器值
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟分频系数,不分频
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式
TIM_TimeBaseStructure.TIM_RepetitionCounter = 0; // 重复计数器值(TIM1特有)
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); // 初始化TIMx
TIM_ClearITPendingBit(TIM1, TIM_IT_Update); // 预先清除中断标志位
TIM_ITConfig(TIM1, TIM_IT_Update, ENABLE); // 使能TIM1更新中断
TIM_Cmd(TIM1, ENABLE); // 启动TIM1计数
}
参数详解:
| 参数 | 值 | 说明 |
|---|---|---|
TIM_Period | 4999 | 计数器自动重装载值,计数到4999后产生更新事件 |
TIM_Prescaler | 7199 | 预分频器值,72MHz ÷ 7200 = 10KHz |
TIM_ClockDivision | TIM_CKD_DIV1 | 时钟分频因子,此处不分频 |
TIM_CounterMode | TIM_CounterMode_Up | 向上计数模式 |
TIM_RepetitionCounter | 0 | 重复计数器(仅高级定时器),0表示每次更新都触发中断 |
初始化步骤:
- 使能TIM1时钟(APB2总线)
- 配置定时器参数(周期、预分频等)
- 清除可能存在的中断标志位(避免启动时立即触发中断)
- 使能定时器更新中断
- 启动定时器计数
3. 中断服务函数
unsigned char flag1 = 0; // LED状态标志位
void TIM1_UP_IRQHandler(void) // TIM1更新中断服务函数
{
if(TIM_GetITStatus(TIM1, TIM_IT_Update) != RESET)
{
TIM_ClearITPendingBit(TIM1, TIM_IT_Update); // 清除中断标志位
flag1 = ~flag1; // 翻转标志位
if(flag1 == 0) // 判断标志
{
LED_On(1); // 点亮LED1
}
else
{
LED_Off(1); // 熄灭LED1
}
}
}
中断处理流程:
中断触发
↓
检查中断标志位
↓
清除中断标志位 ⚠️ 必须执行,否则一直中断
↓
翻转标志位 (0 ⇄ 0xFF)
↓
根据标志位控制LED
↓
退出中断,返回主程序
注意事项:
- ⚠️ 必须清除中断标志位:
TIM_ClearITPendingBit(),否则会一直触发中断 - 使用标志位翻转实现LED状态切换
- 中断服务函数应尽量简短,避免长时间占用CPU
6.3 LED驱动 (TF_LED)
TF_LED.h - 头文件
#ifndef __TF_LED_H__
#define __TF_LED_H__
#ifdef __cplusplus
extern "C"
{
#endif
#include "stm32f10x_gpio.h"
// LED1 定义
#define LED1_PIN GPIO_Pin_0
#define LED1_PORT GPIOB
#define LED1_CLK RCC_APB2Periph_GPIOC
// LED2 定义
#define LED2_PIN GPIO_Pin_1
#define LED2_PORT GPIOB
#define LED2_CLK RCC_APB2Periph_GPIOC
// LED3 定义
#define LED3_PIN GPIO_Pin_2
#define LED3_PORT GPIOB
#define LED3_CLK RCC_APB2Periph_GPIOC
// LED4 定义
#define LED4_PIN GPIO_Pin_3
#define LED4_PORT GPIOB
#define LED4_CLK RCC_APB2Periph_GPIOC
// LED5 定义
#define LED5_PIN GPIO_Pin_4
#define LED5_PORT GPIOB
#define LED5_CLK RCC_APB2Periph_GPIOC
// LED6 定义
#define LED6_PIN GPIO_Pin_5
#define LED6_PORT GPIOB
#define LED6_CLK RCC_APB2Periph_GPIOC
// LED7 定义
#define LED7_PIN GPIO_Pin_6
#define LED7_PORT GPIOB
#define LED7_CLK RCC_APB2Periph_GPIOC
// LED8 定义
#define LED8_PIN GPIO_Pin_7
#define LED8_PORT GPIOB
#define LED8_CLK RCC_APB2Periph_GPIOC
// 所有LED组合定义
#define LED_PIN GPIO_Pin_0| GPIO_Pin_1| GPIO_Pin_2|GPIO_Pin_3| \
GPIO_Pin_4| GPIO_Pin_5|GPIO_Pin_6| GPIO_Pin_7
#define LED_PORT GPIOB
#define LED_CLK RCC_APB2Periph_GPIOB
// 函数声明
extern void LED_Init(void);
void LED_On(unsigned char Led_PIN);
void LED_Off(unsigned char Led_PIN);
void LED_Covert(unsigned char Led_PIN);
#ifdef __cplusplus
}
#endif
#endif
TF_LED.c - 实现文件
1. LED初始化
void LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(LED_CLK, ENABLE); // 使能GPIOB时钟
// JTAG重映射:禁用JTAG,保留SWD调试功能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // 开启AFIO时钟
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); // 禁用JTAG,使能SWD
GPIO_InitStructure.GPIO_Pin = LED_PIN; // PB0-PB7
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz; // 输出速度10MHz
GPIO_Init(LED_PORT, &GPIO_InitStructure); // 初始化GPIOB
GPIO_ResetBits(LED_PORT, LED_PIN); // 所有LED初始化为熄灭状态
}
JTAG 重映射说明:
STM32 的 PB3、PB4 引脚默认被 JTAG 调试接口占用:
- PB3: JTAG_TDO
- PB4: JTAG_NTRST
如果要使用这些引脚作为普通 GPIO,需要重映射 JTAG 功能:
| 重映射模式 | JTAG功能 | SWD功能 | 说明 |
|---|---|---|---|
| 无重映射 | ✅ 使能 | ✅ 使能 | 默认状态,JTAG和SWD都可用 |
| JTAGDisable | ❌ 禁用 | ✅ 使能 | 禁用JTAG,保留SWD ⬅️ 本例使用 |
| 完全禁用 | ❌ 禁用 | ❌ 禁用 | 释放所有调试引脚(不推荐) |
本项目配置:
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);
- 禁用 JTAG 功能,释放 PB3、PB4 引脚
- 保留 SWD 调试功能(PA13/SWDIO、PA14/SWCLK)
- ✅ 推荐配置: 既能使用 PB3/PB4,又能保留调试功能
GPIO 模式说明:
| 模式 | 说明 | 适用场景 |
|---|---|---|
GPIO_Mode_Out_PP | 推挽输出 | LED、继电器等需要高/低电平驱动的场景 ⬅️ 本例使用 |
GPIO_Mode_Out_OD | 开漏输出 | I2C、单总线等需要外部上拉的场景 |
GPIO_Mode_IN_FLOATING | 浮空输入 | 按键检测(配合上拉/下拉电阻) |
GPIO_Mode_IPU | 上拉输入 | 按键检测(内部上拉) |
GPIO_Mode_IPD | 下拉输入 | 按键检测(内部下拉) |
2. LED 点亮函数
void LED_On(unsigned char Led_PIN)
{
switch(Led_PIN)
{
case 1: GPIO_SetBits(LED1_PORT, LED1_PIN); break;
case 2: GPIO_SetBits(LED2_PORT, LED2_PIN); break;
case 3: GPIO_SetBits(LED3_PORT, LED3_PIN); break;
case 4: GPIO_SetBits(LED4_PORT, LED4_PIN); break;
case 5: GPIO_SetBits(LED5_PORT, LED5_PIN); break;
case 6: GPIO_SetBits(LED6_PORT, LED6_PIN); break;
case 7: GPIO_SetBits(LED7_PORT, LED7_PIN); break;
case 8: GPIO_SetBits(LED8_PORT, LED8_PIN); break;
default: GPIO_SetBits(LED_PORT, LED_PIN); break; // 点亮所有LED
}
}
函数说明:
- 输入参数: 1-8 表示单个 LED,其他值点亮所有 LED
- 使用
GPIO_SetBits()将对应引脚设为高电平 - 共阴接法: 高电平点亮 LED
3. LED 熄灭函数
void LED_Off(unsigned char Led_PIN)
{
switch(Led_PIN)
{
case 1: GPIO_ResetBits(LED1_PORT, LED1_PIN); break;
case 2: GPIO_ResetBits(LED2_PORT, LED2_PIN); break;
case 3: GPIO_ResetBits(LED3_PORT, LED3_PIN); break;
case 4: GPIO_ResetBits(LED4_PORT, LED4_PIN); break;
case 5: GPIO_ResetBits(LED5_PORT, LED5_PIN); break;
case 6: GPIO_ResetBits(LED6_PORT, LED6_PIN); break;
case 7: GPIO_ResetBits(LED7_PORT, LED7_PIN); break;
case 8: GPIO_ResetBits(LED8_PORT, LED8_PIN); break;
default: GPIO_ResetBits(LED_PORT, LED_PIN); break; // 熄灭所有LED
}
}
函数说明:
- 输入参数: 1-8 表示单个 LED,其他值熄灭所有 LED
- 使用
GPIO_ResetBits()将对应引脚设为低电平 - 共阴接法: 低电平熄灭 LED
7. 定时器配置详解
7.1 预分频器计算
STM32F103 的 TIM1 时钟来源于 APB2 总线,频率为 72MHz。
预分频作用: 将高频时钟分频为较低频率,便于计数
分频后频率 = 时钟频率 / (预分频器 + 1)
= 72,000,000 / (7199 + 1)
= 72,000,000 / 7200
= 10,000 Hz
= 10 KHz
此时计数器每计一次的时间间隔为:
T_count = 1 / 10,000 = 0.0001 秒 = 0.1 毫秒
7.2 自动重装载值计算
重装载值作用: 设定计数器的计数范围
定时时间 = T_count × (重装载值 + 1)
= 0.0001 × (4999 + 1)
= 0.0001 × 5000
= 0.5 秒
= 500 毫秒
7.3 定时周期调整方法
如需修改定时周期,可以通过以下方式:
方法1: 修改预分频器(推荐用于大范围调整)
// 定时1秒(1000ms)
TIM_TimeBaseStructure.TIM_Prescaler = (7200-1); // 保持不变
TIM_TimeBaseStructure.TIM_Period = (10000-1); // 修改为9999
// 定时100ms
TIM_TimeBaseStructure.TIM_Prescaler = (7200-1); // 保持不变
TIM_TimeBaseStructure.TIM_Period = (1000-1); // 修改为999
方法2: 修改自动重装载值(推荐用于小范围调整)
// 定时250ms
TIM_TimeBaseStructure.TIM_Prescaler = (7200-1); // 保持不变
TIM_TimeBaseStructure.TIM_Period = (2500-1); // 修改为2499
// 定时1ms
TIM_TimeBaseStructure.TIM_Prescaler = (72-1); // 修改为71(1MHz)
TIM_TimeBaseStructure.TIM_Period = (1000-1); // 修改为999
常用定时周期配置表
| 定时周期 | 预分频器 | 自动重装载值 | 计算公式 |
|---|---|---|---|
| 1 ms | 71 | 999 | (72M/72) × 1000 = 1ms |
| 10 ms | 719 | 999 | (72M/720) × 1000 = 10ms |
| 100 ms | 7199 | 999 | (72M/7200) × 1000 = 100ms |
| 500 ms | 7199 | 4999 | (72M/7200) × 5000 = 500ms ⬅️ 本例 |
| 1 s | 7199 | 9999 | (72M/7200) × 10000 = 1s |
| 2 s | 7199 | 19999 | (72M/7200) × 20000 = 2s |
7.4 定时器中断频率限制
理论上,定时器可以配置的最短周期为:
T_min = 1 / 72MHz = 13.9 纳秒
但实际应用中,需考虑:
- 中断处理时间: 中断服务函数的执行时间不能超过定时周期
- CPU占用率: 频繁中断会占用大量 CPU 资源
- 系统响应性: 高频中断可能影响其他任务的执行
建议:
- 定时周期 ≥ 100μs(10KHz)
- 中断处理时间 < 定时周期的 50%
- 多个中断时,合理分配优先级
8. 中断机制
8.1 STM32 中断系统架构
外设产生中断请求
↓
NVIC(嵌套向量中断控制器)
↓
判断中断优先级
↓
保存当前上下文
↓
跳转到中断向量表
↓
执行中断服务函数
↓
恢复上下文
↓
返回主程序
8.2 NVIC 优先级配置
中断优先级分组 (本例使用 Group 2):
NVIC_PriorityGroup_2:
- 抢占优先级: 2 位 (0-3)
- 子优先级: 2 位 (0-3)
优先级判断规则:
- 抢占优先级高的中断可以打断正在执行的低抢占优先级中断
- 抢占优先级相同时,子优先级高的先响应
- 抢占和子优先级都相同时,按中断号决定(号小的先响应)
优先级配置示例:
// TIM1 中断(本例)
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 最高抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 最高子优先级
// 假设系统中还有其他中断
// USART1 中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 次高抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 最高子优先级
// EXTI 外部中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; // 较低抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 次高子优先级
中断响应顺序:
TIM1 (抢占0, 子0) > USART1 (抢占1, 子0) > EXTI (抢占2, 子1)
8.3 中断嵌套场景
场景 1: TIM1 中断执行时,USART1 中断到来
主程序运行
↓
TIM1中断触发 (抢占0)
↓
正在执行TIM1_UP_IRQHandler
↓
USART1中断触发 (抢占1)
↓
❌ 抢占优先级低,等待TIM1中断完成
↓
TIM1中断返回
↓
立即响应USART1中断
↓
返回主程序
场景 2: USART1 中断执行时,TIM1 中断到来
主程序运行
↓
USART1中断触发 (抢占1)
↓
正在执行USART1_IRQHandler
↓
TIM1中断触发 (抢占0)
↓
✅ 抢占优先级高,立即打断USART1中断
↓
执行TIM1_UP_IRQHandler
↓
TIM1中断返回
↓
继续执行USART1中断
↓
USART1中断返回
↓
返回主程序
8.4 中断编程注意事项
-
必须清除中断标志位
TIM_ClearITPendingBit(TIM1, TIM_IT_Update); // ⚠️ 必须执行- 否则会一直触发中断,导致程序卡死
-
中断函数应尽量短小
// ❌ 不推荐:在中断中执行耗时操作
void TIM1_UP_IRQHandler(void) {
for(int i=0; i<10000; i++) { /* 长时间循环 */ }
}
// ✅ 推荐:只做必要的操作
void TIM1_UP_IRQHandler(void) {
TIM_ClearITPendingBit(TIM1, TIM_IT_Update);
flag1 = ~flag1; // 快速翻转标志位
} -
避免在中断中调用阻塞函数
// ❌ 禁止:在中断中使用延时
void TIM1_UP_IRQHandler(void) {
delay_ms(100); // 错误!
} -
共享变量使用 volatile 关键字
volatile unsigned char flag1 = 0; // 防止编译器优化 -
关键代码段保护
// 主程序中读取多字节变量时,禁用中断
__disable_irq(); // 关闭全局中断
uint32_t temp = counter; // 读取共享变量
__enable_irq(); // 恢复全局中断
9. 使用说明
9.1 开发环境
- IDE: Keil MDK-ARM V5.x
- 编译器: ARMCC V5.06 或更高版本
- 调试器: J-Link / ST-Link
- 下载工具: J-Link / ST-Link / 串口ISP
9.2 编译步骤
-
打开工程
- 双击
MDK/TEST_CODE.uvproj打开 Keil 工程
- 双击
-
选择目标芯片
- 确认芯片型号为 STM32F103C8(64K Flash)
-
配置编译选项
- Target: STM32F103C8
- Crystal: 8.0 MHz (外部晶振)
-
编译工程
- 点击
Project→Build Target(F7) - 或点击工具栏编译按钮
- 点击
-
查看编译结果
Program Size: Code=xxxx RO-data=xxx RW-data=xx ZI-data=xxx
"TEST_CODE" - 0 Error(s), 0 Warning(s).
9.3 下载与调试
使用 J-Link 下载
-
连接硬件
J-Link STM32F103C8T6
VTref ---- 3.3V
GND ---- GND
SWDIO ---- PA13 (SWDIO)
SWCLK ---- PA14 (SWCLK) -
配置下载选项
Options for Target→Debug→ 选择J-Link/J-Trace CortexSettings→ 确认芯片检测成功
-
下载程序
- 点击
Flash→Download(F8) - 等待下载完成
- 点击
使用 ST-Link 下载
-
连接硬件
ST-Link STM32F103C8T6
VDD ---- 3.3V
GND ---- GND
SWDIO ---- PA13 (SWDIO)
SWCLK ---- PA14 (SWCLK) -
配置下载选项
Options for Target→Debug→ 选择ST-Link DebuggerSettings→ 确认芯片检测成功
-
下载程序
- 点击
Flash→Download(F8)
- 点击
9.4 运行效果
预期现象:
- 程序下载完成后,LED1 开始闪烁
- 闪烁周期: 亮 500ms → 灭 500ms → 亮 500ms ...
- 闪烁频率: 1Hz(每秒闪烁1次)
效果验证:
- 使用秒表测量 LED 闪烁周期,应为 1.0 秒 ± 0.01 秒
- 长时间运行不应有累积误差(硬件定时器精度高)
9.5 调试方法
在线调试
-
进入调试模式
- 点击
Debug→Start/Stop Debug Session(Ctrl+F5)
- 点击
-
设置断点
- 在
TIM1_UP_IRQHandler函数中设置断点 - 点击行号左侧,出现红点表示断点设置成功
- 在
-
运行程序
- 点击
Debug→Run(F5) - 程序会在断点处暂停
- 点击
-
查看变量
- 在
Watch窗口添加flag1变量 - 每次中断触发时,观察
flag1的值变化
- 在
-
单步执行
- F10: 单步跳过(Step Over)
- F11: 单步进入(Step Into)
- Shift+F11: 跳出函数(Step Out)
逻辑分析仪调试
// 在中断中翻转额外的GPIO,用于测量定时精度
void TIM1_UP_IRQHandler(void) {
if(TIM_GetITStatus(TIM1, TIM_IT_Update) != RESET) {
TIM_ClearITPendingBit(TIM1, TIM_IT_Update);
GPIO_WriteBit(GPIOA, GPIO_Pin_0, !GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_0));
flag1 = ~flag1;
if(flag1 == 0) {
LED_On(1);
} else {
LED_Off(1);
}
}
}
使用逻辑分析仪连接 PA0,测量翻转周期,应为 500ms。
10. 常见问题
问题 1: LED 不闪烁
可能原因:
- ❌ 定时器时钟未使能
- ❌ 定时器中断未使能
- ❌ NVIC 中断未配置
- ❌ LED 硬件连接错误
排查步骤:
// 1. 检查时钟使能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); // 必须有
// 2. 检查中断使能
TIM_ITConfig(TIM1, TIM_IT_Update, ENABLE); // 必须有
// 3. 检查定时器启动
TIM_Cmd(TIM1, ENABLE); // 必须有
// 4. 检查NVIC配置
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 必须有
解决方案:
- 在调试模式下,在
TIM1_UP_IRQHandler设置断点 - 如果断点未触发,说明中断配置有问题
- 如果断点触发,但 LED 不亮,检查 LED 硬件连接
问题 2: LED 闪烁不规律
可能原因:
- ❌ 中断标志位未清除
- ❌ 定时器参数配置错误
- ❌ 时钟频率设置错误
排查步骤:
// 1. 必须清除中断标志位
void TIM1_UP_IRQHandler(void) {
if(TIM_GetITStatus(TIM1, TIM_IT_Update) != RESET) {
TIM_ClearITPendingBit(TIM1, TIM_IT_Update); // ⚠️ 关键!
// ...
}
}
// 2. 检查系统时钟配置(在 system_stm32f10x.c)
// 确保 SYSCLK = 72MHz, APB2 = 72MHz
问题 3: PB3、PB4 引脚无法控制 LED
可能原因: ❌ JTAG 功能未重映射,PB3/PB4 被 JTAG 占用
解决方案:
void LED_Init(void) {
// 必须添加以下代码
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); // ⚠️ 关键!
// ... 其他初始化代码
}
问题 4: 下载后程序不运行
可能原因:
- ❌ BOOT0 引脚配置错误
- ❌ 程序跳转到错误地址
- ❌ Flash 未写入成功
排查步骤:
| BOOT0 | BOOT1 | 启动模式 | 说明 |
|---|---|---|---|
| 0 | X | 从Flash启动 | 正常运行模式 ⬅️ 应使用此模式 |
| 1 | 0 | 从系统存储器启动 | 用于ISP下载 |
| 1 | 1 | 从SRAM启动 | 用于调试 |
解决方案:
- 确保 BOOT0 接 GND(或拨码开关置0)
- 重新上电或按复位键
- 使用 STM32 ST-Link Utility 检查 Flash 内容
问题 5: 定时不准确
可能原因:
- ❌ 外部晶振频率错误
- ❌ 系统时钟配置错误
- ❌ 定时器参数计算错误
排查步骤:
// 1. 检查外部晶振频率(通常为 8MHz)
// 在 stm32f10x.h 中确认:
#define HSE_VALUE ((uint32_t)8000000) // 外部高速晶振频率
// 2. 检查系统时钟配置
// 在 SystemInit() 函数中确认 SYSCLK = 72MHz
// 3. 重新计算定时器参数
// T = ((1+Prescaler)/72MHz) × (1+Period)
问题 6: 程序卡在中断中
可能原因: ❌ 中断标志位未清除,导致一直重复进入中断
解决方案:
void TIM1_UP_IRQHandler(void) {
// ⚠️ 第一时间清除中断标志位
TIM_ClearITPendingBit(TIM1, TIM_IT_Update); // 必须放在最前面!
// 然后再处理其他逻辑
flag1 = ~flag1;
if(flag1 == 0) {
LED_On(1);
} else {
LED_Off(1);
}
}