跳到主要内容

【九】定时器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

外设资源

外设引脚功能说明
LED1PB0数字输出发光二极管1
LED2PB1数字输出发光二极管2
LED3PB2数字输出发光二极管3
LED4PB3数字输出发光二极管4
LED5PB4数字输出发光二极管5
LED6PB5数字输出发光二极管6
LED7PB6数字输出发光二极管7
LED8PB7数字输出发光二极管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状态

返回主循环

功能特点

  1. 中断驱动: 采用中断方式实现定时控制,主循环无需执行任何操作
  2. 精确定时: 使用硬件定时器,定时精度高,不受软件延时影响
  3. 低功耗: 主循环空闲,可进入低功耗模式(本例未实现)
  4. 易于扩展: 可在中断中添加更多功能,或修改定时周期

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 00 位4 位无抢占,0-15级子优先级
Group 11 位3 位2级抢占,0-7级子优先级
Group 22 位2 位4级抢占,0-3级子优先级 ⬅️ 本例使用
Group 33 位1 位8级抢占,0-1级子优先级
Group 44 位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_Period4999计数器自动重装载值,计数到4999后产生更新事件
TIM_Prescaler7199预分频器值,72MHz ÷ 7200 = 10KHz
TIM_ClockDivisionTIM_CKD_DIV1时钟分频因子,此处不分频
TIM_CounterModeTIM_CounterMode_Up向上计数模式
TIM_RepetitionCounter0重复计数器(仅高级定时器),0表示每次更新都触发中断

初始化步骤:

  1. 使能TIM1时钟(APB2总线)
  2. 配置定时器参数(周期、预分频等)
  3. 清除可能存在的中断标志位(避免启动时立即触发中断)
  4. 使能定时器更新中断
  5. 启动定时器计数
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

退出中断,返回主程序

注意事项:

  1. ⚠️ 必须清除中断标志位: TIM_ClearITPendingBit(),否则会一直触发中断
  2. 使用标志位翻转实现LED状态切换
  3. 中断服务函数应尽量简短,避免长时间占用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 ms71999(72M/72) × 1000 = 1ms
10 ms719999(72M/720) × 1000 = 10ms
100 ms7199999(72M/7200) × 1000 = 100ms
500 ms71994999(72M/7200) × 5000 = 500ms ⬅️ 本例
1 s71999999(72M/7200) × 10000 = 1s
2 s719919999(72M/7200) × 20000 = 2s

7.4 定时器中断频率限制

理论上,定时器可以配置的最短周期为:

T_min = 1 / 72MHz = 13.9 纳秒

但实际应用中,需考虑:

  1. 中断处理时间: 中断服务函数的执行时间不能超过定时周期
  2. CPU占用率: 频繁中断会占用大量 CPU 资源
  3. 系统响应性: 高频中断可能影响其他任务的执行

建议:

  • 定时周期 ≥ 100μs(10KHz)
  • 中断处理时间 < 定时周期的 50%
  • 多个中断时,合理分配优先级

8. 中断机制

8.1 STM32 中断系统架构

外设产生中断请求

NVIC(嵌套向量中断控制器)

判断中断优先级

保存当前上下文

跳转到中断向量表

执行中断服务函数

恢复上下文

返回主程序

8.2 NVIC 优先级配置

中断优先级分组 (本例使用 Group 2):

NVIC_PriorityGroup_2:
- 抢占优先级: 2 位 (0-3)
- 子优先级: 2 位 (0-3)

优先级判断规则:

  1. 抢占优先级高的中断可以打断正在执行的低抢占优先级中断
  2. 抢占优先级相同时,子优先级高的先响应
  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 中断编程注意事项

  1. 必须清除中断标志位

    TIM_ClearITPendingBit(TIM1, TIM_IT_Update);  // ⚠️ 必须执行
    • 否则会一直触发中断,导致程序卡死
  2. 中断函数应尽量短小

    // ❌ 不推荐:在中断中执行耗时操作
    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; // 快速翻转标志位
    }
  3. 避免在中断中调用阻塞函数

    // ❌ 禁止:在中断中使用延时
    void TIM1_UP_IRQHandler(void) {
    delay_ms(100); // 错误!
    }
  4. 共享变量使用 volatile 关键字

    volatile unsigned char flag1 = 0;  // 防止编译器优化
  5. 关键代码段保护

    // 主程序中读取多字节变量时,禁用中断
    __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 编译步骤

  1. 打开工程

    • 双击 MDK/TEST_CODE.uvproj 打开 Keil 工程
  2. 选择目标芯片

    • 确认芯片型号为 STM32F103C8(64K Flash)
  3. 配置编译选项

    • Target: STM32F103C8
    • Crystal: 8.0 MHz (外部晶振)
  4. 编译工程

    • 点击 ProjectBuild Target (F7)
    • 或点击工具栏编译按钮
  5. 查看编译结果

    Program Size: Code=xxxx RO-data=xxx RW-data=xx ZI-data=xxx
    "TEST_CODE" - 0 Error(s), 0 Warning(s).

9.3 下载与调试

  1. 连接硬件

    J-Link          STM32F103C8T6
    VTref ---- 3.3V
    GND ---- GND
    SWDIO ---- PA13 (SWDIO)
    SWCLK ---- PA14 (SWCLK)
  2. 配置下载选项

    • Options for TargetDebug → 选择 J-Link/J-Trace Cortex
    • Settings → 确认芯片检测成功
  3. 下载程序

    • 点击 FlashDownload (F8)
    • 等待下载完成
  1. 连接硬件

    ST-Link         STM32F103C8T6
    VDD ---- 3.3V
    GND ---- GND
    SWDIO ---- PA13 (SWDIO)
    SWCLK ---- PA14 (SWCLK)
  2. 配置下载选项

    • Options for TargetDebug → 选择 ST-Link Debugger
    • Settings → 确认芯片检测成功
  3. 下载程序

    • 点击 FlashDownload (F8)

9.4 运行效果

预期现象:

  1. 程序下载完成后,LED1 开始闪烁
  2. 闪烁周期: 亮 500ms → 灭 500ms → 亮 500ms ...
  3. 闪烁频率: 1Hz(每秒闪烁1次)

效果验证:

  • 使用秒表测量 LED 闪烁周期,应为 1.0 秒 ± 0.01 秒
  • 长时间运行不应有累积误差(硬件定时器精度高)

9.5 调试方法

在线调试

  1. 进入调试模式

    • 点击 DebugStart/Stop Debug Session (Ctrl+F5)
  2. 设置断点

    • TIM1_UP_IRQHandler 函数中设置断点
    • 点击行号左侧,出现红点表示断点设置成功
  3. 运行程序

    • 点击 DebugRun (F5)
    • 程序会在断点处暂停
  4. 查看变量

    • Watch 窗口添加 flag1 变量
    • 每次中断触发时,观察 flag1 的值变化
  5. 单步执行

    • 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 不闪烁

可能原因:

  1. ❌ 定时器时钟未使能
  2. ❌ 定时器中断未使能
  3. ❌ NVIC 中断未配置
  4. ❌ 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. ❌ 中断标志位未清除
  2. ❌ 定时器参数配置错误
  3. ❌ 时钟频率设置错误

排查步骤:

// 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: 下载后程序不运行

可能原因:

  1. ❌ BOOT0 引脚配置错误
  2. ❌ 程序跳转到错误地址
  3. ❌ Flash 未写入成功

排查步骤:

BOOT0BOOT1启动模式说明
0X从Flash启动正常运行模式 ⬅️ 应使用此模式
10从系统存储器启动用于ISP下载
11从SRAM启动用于调试

解决方案:

  1. 确保 BOOT0 接 GND(或拨码开关置0)
  2. 重新上电或按复位键
  3. 使用 STM32 ST-Link Utility 检查 Flash 内容

问题 5: 定时不准确

可能原因:

  1. ❌ 外部晶振频率错误
  2. ❌ 系统时钟配置错误
  3. ❌ 定时器参数计算错误

排查步骤:

// 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);
}
}