TIM - 通用定时器
更新于 2026年8月13日
本篇总结
使用 TIM3,输出 PWM 波形并用输入捕获测量信号脉宽。
本示例演示 W55MH32L 通用定时器(TIM2-5)的两项典型功能:PWM 输出与输入捕获。PWM 输出模式下,TIM3 在 PA7 产生固定频率与占空比的方波;输入捕获模式下,TIM3 通过通道 2 捕获外部信号的上升沿/下降沿,测量其高电平持续时间。
通用定时器简介
W55MH32 的通用定时器(TIM2-5)是一个由可编程预分频器驱动的 16 位自动装载计数器,每个定时器拥有 4 个独立通道,可分别配置为:输入捕获、输出比较、PWM 生成(边缘或中央对齐)、单脉冲模式输出。与基本定时器相比,通用定时器增加了通道逻辑;与高级定时器相比,它没有互补输出与死区插入。
| 通道模式 | 用途 | 本篇演示 |
|---|---|---|
| 输出比较 / PWM | 产生可调频率与占空比的方波 | 步骤 1-3:PA7 输出 10kHz/50% PWM |
| 输入捕获 | 测量外部信号电平跳变时间 | 步骤 4-5:测量 PA7 高电平脉宽 |
通用定时器的时基核心与基本定时器一致:更新周期 = (PSC+1) × (ARR+1) / TIMxCLK;PWM 输出在此基础上多了 TIMx_CCRx 比较寄存器来决定占空比:占空比 = CCR / (ARR+1)。
前置工具准备
开始本例程前,请确认以下工具已安装:
- WIZ UartTool V1.0:串口日志观察工具:点击下载
- 示波器或逻辑分析仪(可选):观察 PA7 的 PWM 波形;输入捕获实验需一个可产生方波的外部信号源
硬件连接
PWM 输出与输入捕获共用 PA7(TIM3_CH2),但二者分属两个独立工程,互不冲突。串口日志通过 USART1(PA9/PA10)的板载 USB 转串口输出,仅需一根 Micro USB 线连接 DEBUG USB 口到 PC。
| 引脚 | 功能 | 方向 | 模式 | 所属演示 |
|---|---|---|---|---|
| PA7 | TIM3_CH2 | 芯片 → 外部 | 复用推挽输出 (AF_PP) | PWM 输出 |
| PA7 | TIM3_CH2 | 外部 → 芯片 | 下拉输入 (IPD) | 输入捕获 |
| PA9 | USART1_TX | 开发板 → PC | 复用推挽输出 (AF_PP) | 两者共用 |
| PA10 | USART1_RX | PC → 开发板 | 浮空输入 (IN_FLOATING) | 两者共用 |
工程结构
工程位于 SDK 内 1.SDK\ModuleDemo\TIM\TIM_General\,核心文件如下:
TIM_General/
├── USER/
│ ├── main.c ← TIM初始化 + PWM输出/输入捕获 + 中断服务 + printf 重定向
│ ├── w55mh32_conf.h ← 外设头文件集中包含
│ ├── w55mh32_it.c/h ← 中断服务函数(系统异常部分)
│ ├── system_w55mh32.c/h← 系统初始化(时钟配置)
│ └── startup_w55mh32.s ← 启动文件
└── SYSTEM/
└── delay/
└── delay.c/h ← SysTick 延时函数
步骤1:TIM3 PWM 输出初始化
文件:USER/main.c | 函数:TIM_Configuration() — 配置 PA7 为复用推挽输出,TIM3 时基 1MHz、ARR=99(PWM 周期 100μs / 10kHz),CH2 输出 PWM2 模式、占空比 50%。
void TIM_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
/* 开启 TIM3 及 GPIOA/AFIO 时钟 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
/* PA7: TIM3_CH2 → 复用推挽输出 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* 时基:PSC 使 CK_CNT=1MHz,ARR=99 → PWM 周期 100μs(10kHz) */
TIM_TimeBaseStructure.TIM_Period = 99;
TIM_TimeBaseStructure.TIM_Prescaler = clocks.PCLK1_Frequency / 1000000 - 1;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
/* 输出比较:PWM2 模式,CH2,Pulse=49 → 占空比 49%,低电平有效 */
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Enable;
TIM_OCInitStructure.TIM_Pulse = 99 / 2;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;
TIM_OCInitStructure.TIM_OCNPolarity = TIM_OCNPolarity_Low;
TIM_OCInitStructure.TIM_OCIdleState = TIM_OCIdleState_Set;
TIM_OCInitStructure.TIM_OCNIdleState = TIM_OCIdleState_Reset;
TIM_OC2Init(TIM3, &TIM_OCInitStructure);
/* 使能预装载寄存器:运行中改 CCR/ARR 会在下个更新事件生效 */
TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable);
TIM_ARRPreloadConfig(TIM3, ENABLE);
/* 启动定时器 */
TIM_Cmd(TIM3, ENABLE);
TIM_CtrlPWMOutputs(TIM3, ENABLE);
}六步完成:开时钟 → GPIO 复用推挽 → 时基参数(PSC=71 使 CK_CNT=1MHz、ARR=99)→ 输出比较参数(PWM2、Pulse=49、低电平有效)→ 预装载使能 → 启动。PWM 周期 = (ARR+1)/CK_CNT = 100/1MHz = 100μs(10kHz),占空比 = Pulse/(ARR+1) = 49/100 = 49%。
如果你想自定义
- 改 PWM 频率 → 修改
TIM_Period(ARR)和TIM_Prescaler(PSC)。如要 1kHz:PSC=71(1MHz)、ARR=999 - 改占空比 → 修改
TIM_Pulse(CCR)。如要 30% 占空比:TIM_Pulse = 99 * 3 / 10(即 29) - 换用 CH1/CH3/CH4 → 同步修改 GPIO 引脚(CH1=PA6、CH3=PA8、CH4=PA9)和
TIM_OCxInit/TIM_OCxPreloadConfig的通道号
步骤2:主函数逻辑
函数:main() — 初始化延时与 UART(115200) → 打印时钟频率 → 配置 TIM3 PWM → 进入空循环,波形由硬件持续输出。
RCC_ClocksTypeDef clocks;
int main(void)
{
delay_init();
UART_Configuration(115200);
RCC_GetClocksFreq(&clocks);
printf("SYSCLK: %3.1fMhz, HCLK: %3.1fMhz, PCLK1: %3.1fMhz, "
"PCLK2: %3.1fMhz, ADCCLK: %3.1fMhz\n",
(float)clocks.SYSCLK_Frequency / 1000000,
(float)clocks.HCLK_Frequency / 1000000,
(float)clocks.PCLK1_Frequency / 1000000,
(float)clocks.PCLK2_Frequency / 1000000,
(float)clocks.ADCCLK_Frequency / 1000000);
printf("TIM General PWM Test.\n");
TIM_Configuration();
while (1);
}
PWM 一旦配置完成即由硬件持续输出,while(1) 中无需任何干预。clocks 为全局变量,供 TIM_Configuration() 计算 PSC 时引用 PCLK1 频率,保证换主频时无需改代码。
运行时观察占空比
在 while(1) 中加入以下代码可周期性打印当前占空比:
uint16_t v = TIM_GetCapture2(TIM3);
printf("Duty: %.1f%%\n", (float)v / 100 * 100);
delay_ms(1000);
步骤3:printf 重定向
重写 fputc() 将 printf 输出重定向到 USART1,并自动在 \n 前补 \r 确保串口工具正确换行。
int SER_PutChar(int ch)
{
while (!USART_GetFlagStatus(USART_TEST, USART_FLAG_TC));
USART_SendData(USART_TEST, (uint8_t)ch);
return ch;
}
int fputc(int c, FILE *f)
{
if (c == '\n')
SER_PutChar('\r'); // 补回车,确保串口工具正确换行
return (SER_PutChar(c));
}步骤4:输入捕获初始化(扩展功能)
以下为独立工程 TIM_InputCapture 的核心代码(与 PWM 输出工程互斥,不可同时烧录)。函数:TIM_ICConfiguration() — 将 PA7 配为下拉输入,TIM3 时基 1MHz(计数分辨率 1μs),CH2 上升沿捕获,并使能溢出 + 捕获中断。
void TIM_ICConfiguration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_ICInitTypeDef TIM_ICInitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
/* PA7: TIM3_CH2 → 下拉输入(捕获外部信号) */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* 时基:PSC=71(72MHz/72=1MHz,计数分辨率 1μs),ARR=0xFFFF */
TIM_TimeBaseStructure.TIM_Period = 0xFFFF;
TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
/* 通道2 上升沿捕获 */
TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_Div1;
TIM_ICInitStructure.TIM_ICFilter = 0x0;
TIM_ICInit(TIM3, &TIM_ICInitStructure);
/* NVIC:溢出 + 捕获中断 */
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
TIM_ITConfig(TIM3, TIM_IT_Update | TIM_IT_CC2, ENABLE);
TIM_Cmd(TIM3, ENABLE);
}
关键三点:① PSC=71 使计数器每 1μs 加 1,捕获值单位即 μs;② ARR=0xFFFF,单次最多计 65535μs,超出则靠溢出中断累计;③ 先上升沿捕获清零计数器,再切换为下降沿捕获读出高电平时长。全局变量 TIM3_CAPTURE_STA(位7完成 / 位6已捕获上升沿 / 位0-5溢出次数)与 TIM3_CAPTURE_VAL(捕获值)配合主循环输出结果。
如果你想自定义
- 改捕获精度 → 修改
TIM_Prescaler。如要 0.1ms 精度:PSC=7199(10kHz) - 改捕获通道 → 同步修改
TIM_Channel、GPIO 引脚和TIM_IT_CCx - 测低电平脉宽 → 将首次捕获极性改为
TIM_ICPolarity_Falling,第二次切换为Rising
步骤5:输入捕获中断服务函数
函数:TIM3_IRQHandler() — 上升沿清零计数器并切为下降沿捕获;下降沿读出计数值标记完成;溢出时累计溢出次数。主循环检测到完成标志后计算总时长。
void TIM3_IRQHandler(void)
{
if (!(TIM3_CAPTURE_STA & 0x80)) /* 未完成捕获 */
{
/* 处理溢出中断 */
if (TIM_GetITStatus(TIM3, TIM_IT_Update))
{
if (TIM3_CAPTURE_STA & 0x40) /* 已捕获上升沿,统计溢出 */
{
if ((TIM3_CAPTURE_STA & 0x3F) == 0x3F)
TIM3_CAPTURE_STA |= 0x80; /* 溢出 32 次,标记失败 */
else
TIM3_CAPTURE_STA++; /* 溢出次数 +1 */
}
}
/* 处理捕获中断(CH2) */
if (TIM_GetITStatus(TIM3, TIM_IT_CC2))
{
if (TIM3_CAPTURE_STA & 0x40) /* 已捕获上升沿,本次为下降沿 */
{
TIM3_CAPTURE_VAL = TIM_GetCapture2(TIM3); /* 读取计数值 */
TIM3_CAPTURE_STA |= 0x80; /* 标记完成 */
TIM_OC2PolarityConfig(TIM3, TIM_ICPolarity_Rising); /* 切回上升沿 */
}
else /* 首次捕获上升沿 */
{
TIM3_CAPTURE_STA = 0x40; /* 标记已捕获上升沿 */
TIM_SetCounter(TIM3, 0); /* 计数器清零 */
TIM_OC2PolarityConfig(TIM3, TIM_ICPolarity_Falling); /* 切为下降沿 */
}
}
}
TIM_ClearITPendingBit(TIM3, TIM_IT_CC2 | TIM_IT_Update); /* 清除中断标志 */
}
主循环中:总时长 = (溢出次数 × 65536 + 捕获值) μs。通过两次捕获(上升沿→下降沿)配合极性动态切换,用单通道实现了高电平脉宽测量。必须先清除中断标志,否则会反复进中断。
注意
TIM_OC2PolarityConfig 虽名为 OC(输出比较),但在输入捕获下用于动态切换 CC2 通道的捕获极性,这是标准库的惯用写法。若用 标准库则对应 HAL_TIM_IC_ConfigChannel 重新配置极性。
编译与下载
Keil MDK 中打开 TIM_General.uvprojx,F7 编译,确认 0 Error(s)。连接 DEBUG USB 口到 PC,F8 下载。下载成功后按 RESET 键启动新固件。PWM 输出工程与输入捕获工程分别编译烧录,不可同时运行。
运行验证
PWM 输出工程:打开 WIZ UartTool V1.0 ,选 择开发板对应 COM 口,波特率 115200 / 8N1。按 RESET 启动后,串口打印时钟信息与 "TIM General PWM Test.";用示波器探头接 PA7,可观察到 10kHz、50% 占空比的方波。
SYSCLK: 72.0Mhz, HCLK: 72.0Mhz, PCLK1: 36.0Mhz, PCLK2: 72.0Mhz, ADCCLK: 36.0Mhz
TIM General PWM Test.
输入捕获工程 :在 PA7 上输入一个方波信号(可由另一块开发板或信号发生器产生),串口将持续打印该信号的高电平时间,单位 μs。
HIGH:500 us
HIGH:501 us
HIGH:500 us
...
常见问题
Q: PA7 没有 PWM 波形输出
按顺序检查:① GPIO 是否配为 GPIO_Mode_AF_PP(复用推挽,不是普通推挽);② TIM_OC2Init 的 TIM_OutputState 是否为 Enable;③ TIM_Cmd(TIM3, ENABLE) 是否调用;④ 示波器探头地线是否接到开发板 GND。
Q: PWM 占空比与预期相反(该高的时候低)
PWM 模式与极性配合问题。本例用 PWM2 + OCPolarity_Low:CNT>CCR 时输出有效(低电平),所以 Pulse 越小、低电平时间越长。若想要"常规"高电平占空比,可改为 TIM_OCMode_PWM1 + TIM_OCPolarity_High,此时 Pulse/(ARR+1) 即为高电平占比。
Q: 输入捕获打印的脉宽值一直是 0 或乱跳
① 确认外部信号确实加到了 PA7 且共地;② 检查 GPIO 模式:无信号时应为 IPD(下拉),若信号是高电平有效则改为 IPU(上拉);③ 中断函数中极性切换是否正确(上升沿→下降沿→读值);④ TIM_ClearITPendingBit 是否放在中断函数末尾。
Q: 换用 CH1/CH3/CH4 后编译报错或无输出
通道号必须三处一致:① GPIO 引脚(CH1=PA6、CH2=PA7、CH3=PA8、CH4=PA9,注意 CH4 与 USART1_TX 冲突);② TIM_OCxInit / TIM_ICInit 的 TIM_Channel_x;③ 预装载 TIM_OCxPreloadConfig 与捕获读 TIM_GetCapturex。三处通道号不一致会导致编译通过但运行异常。
Q: 编译报错 "cannot open source file w55mh32.h"
检查 Project → Options → C/C++ → Include Paths,确保 SDK 的 Libraries 目录已添加。若 TIM_OCInitTypeDef 结构体报未知成员,确认包含的 w55mh32_tim.h 已启用对应定时器宏。
总结
本文基于 W55MH32L 的 TIM3 通用定时器演示了两大核心功能:
- PWM 输出:TIM3_CH2(PA7)配置为 PWM2 模式,PSC 使 CK_CNT=1MHz、ARR=99,输出 10kHz / 50% 占空比方波;
TIM_OC2PreloadConfig使能预装载,运行中可安全修改 CCR - 输入捕获:TIM3_CH2(PA7)配置为上升沿捕获,PSC=71 使计数分辨率 1μs,通过上升沿清零→下降沿读值的双沿切换测量高电平脉宽,溢出中断累计超长信号
- 核心公式:PWM 频率 = TIMxCLK / (PSC+1) / (ARR+1),占空比 = CCR / (ARR+1),捕获脉宽 = (溢出次数 × 65536 + 捕获值) × 计数周期
- 通用定时器的 4 个通道完全独立,可同时输出不同占空比的 PWM 或分别捕获多路信号,是电机控制与信号测量的基础