
1. 项目概述与核心思路最近在带新人做项目发现很多刚接触STM32的朋友对定时器和PWM的使用总是一头雾水寄存器配置看得眼花缭乱。正好手边有块经典的万利EK-STM32F开发板我就以“用定时器3产生1秒周期、50%和10%占空比的PWM信号并驱动板载LEDLD1、LD2闪烁”这个具体任务为例把整个流程掰开揉碎了讲一遍。这不仅仅是调通一个例程更重要的是理解STM32定时器产生PWM的底层机制和配置逻辑以后无论换哪个型号、哪个定时器你都能自己推算出来。这个任务的核心目标很明确让连接到PC6和PC7引脚的两个LED分别以1秒为周期、50%和10%的亮度严格说是占空比闪烁。选择定时器3TIM3是因为在EK-STM32F这块板上当定时器3的通道进行“完全重映射”后其通道1CH1和通道2CH2恰好对应到PC6和PC7而这两个引脚又直接连着LD2和LD1硬件连线完美匹配省去了外加电路的麻烦。整个实现的关键在于理解几个核心寄存器TIMx_ARR自动重装载寄存器决定PWM的周期TIMx_CCRx捕获/比较寄存器决定PWM的占空比而TIMx_PSC预分频器则和系统时钟一起决定了计数器的计数频率。我们的配置工作本质上就是根据想要的PWM周期和系统时钟频率倒推出这几个寄存器的值并正确设置定时器的工作模式。下面我们就从时钟树分析开始一步步算出来。2. 时钟配置与参数计算详解在动手写代码之前我们必须先把账算清楚。STM32的定时器不是凭空工作的它的“心跳”来源于系统时钟。在EK-STM32F开发板上通常使用内部8MHz的HSI RC振荡器经过PLL倍频到72MHz作为系统时钟SYSCLK。理解时钟路径是正确计算定时器参数的前提。2.1 时钟树分析与定时器时钟源对于STM32F103系列定时器的时钟源有讲究。高级定时器TIM1, TIM8挂在APB2总线上而通用定时器TIM2, TIM3, TIM4, TIM5则挂在APB1总线上。我们用的TIM3就属于APB1总线。这里有个关键点当APB1的预分频系数为1时定时器时钟CK_INT直接等于APB1总线时钟PCLK1如果APB1预分频系数不为1比如是2、4、8等那么定时器时钟会是PCLK1的2倍。这是STM32内部的一个倍频机制目的是确保即使APB1总线频率较低定时器也能有较高的时间分辨率。在常见的72MHz系统时钟配置下AHB总线HCLK通常设为72MHz。APB1总线PCLK1的最高频率是36MHz所以通常设置其预分频系数为2即PCLK1 HCLK / 2 72MHz / 2 36MHz。由于此时APB1预分频系数2不等于1因此定时器时钟CK_INT PCLK1 * 2 36MHz * 2 72MHz。这就是为什么我们常说“TIM3的时钟是72MHz”尽管它的总线是36MHz。这个72MHz就是我们计算分频和周期的基准频率。注意务必通过读取RCC相关寄存器或查看初始化代码来确认你的系统实际时钟配置而不是盲目相信“默认72MHz”。时钟配置错误会导致所有时间相关的计算全部出错。2.2 PWM频率与占空比参数计算我们要产生周期T为1秒的PWM波。PWM频率f是周期的倒数即f 1 / T 1 Hz。定时器产生PWM的原理是计数器从0开始向上计数每来一个时钟脉冲就加1一直计到我们设定的重装载值ARR然后产生一个更新事件计数器归零重新开始。同时在计数过程中会不断与捕获/比较寄存器CCR的值进行比较。当计数值小于CCR时输出一种电平例如高电平当计数值大于等于CCR时输出另一种电平例如低电平。这样ARR决定了计数周期即PWM周期CCR决定了高电平持续时间即PWM脉宽。因此PWM周期公式为T (ARR 1) * (PSC 1) / CK_INT。 其中CK_INT定时器时钟频率72,000,000 HzPSC预分频器寄存器值0-65535ARR自动重装载寄存器值0-65535公式中的1是因为寄存器值PSC和ARR是分频系数和计数值实际分频比是PSC1计数周期是ARR1。我们的目标是T 1秒。两个未知数PSC和ARR一个方程有无穷多解。我们需要选择合适的值通常遵循两个原则1. 让ARR尽可能大以获得更精细的占空比调节分辨率2.PSC和ARR的值都在0-65535的合法范围内。我们来尝试计算如果令PSC 7200 - 1 7199那么预分频后的计数器时钟频率为CK_CNT CK_INT / (PSC1) 72,000,000 / 7200 10,000 Hz。这意味着计数器每秒钟计数10000次。要产生1秒的周期我们需要计数器计满10000个数后溢出。所以ARR 10000 - 1 9999。验证T (9999 1) * (7199 1) / 72,000,000 10000 * 7200 / 72,000,000 1秒。完美。接下来是占空比。占空比Duty Cycle定义为高电平时间与整个周期的比值。在向上计数PWM模式1下当计数值小于CCR时输出有效电平我们设为高电平。所以占空比D CCR / (ARR 1)。对于50%占空比CCR1 (ARR 1) * 50% 10000 * 0.5 5000。对于10%占空比CCR2 (ARR 1) * 10% 10000 * 0.1 1000。至此核心参数全部确定TIM3_PSC 7199代码中常写为7200-1TIM3_ARR 9999TIM3_CCR1 5000通道1PC6LD250%TIM3_CCR2 1000通道2PC7LD110%3. 硬件连接与GPIO复用配置参数算好了接下来要让信号从定时器的内部逻辑单元真正输出到物理引脚上驱动LED。这就涉及到GPIO的复用功能配置。3.1 引脚复用与重映射分析查看STM32F103的数据手册可以知道定时器3的各个通道默认映射到特定的引脚例如TIM3_CH1默认是PA6。但是芯片设计者提供了“重映射”功能可以将定时器通道映射到其他备用引脚上。对于TIM3重映射分为“部分重映射”和“完全重映射”两种。我们需要使用的是完全重映射因为在这种模式下TIM3_CH1 重映射到 PC6TIM3_CH2 重映射到 PC7TIM3_CH3 重映射到 PC8TIM3_CH4 重映射到 PC9而万利EK-STM32F开发板的原理图显示LD2连接在PC6LD1连接在PC7。这简直是“天作之合”无需任何飞线直接通过配置就能将PWM信号输送到LED上。重映射功能由“复用功能I/O和调试配置寄存器”AFIO_MAPR控制。我们需要设置其中的TIM3_REMAP[1:0]位为11即选择完全重映射。3.2 GPIO模式配置详解引脚确定了接下来要配置GPIO的工作模式。我们的目的是让定时器内部控制输出波形所以GPIO必须设置为“复用推挽输出”模式。推挽输出可以提供较强的驱动能力高电平时由内部PMOS管将引脚上拉到VDD低电平时由内部NMOS管将引脚下拉到GND非常适合驱动LED这种负载。具体到PC6和PC7它们属于GPIOC的低8位配置寄存器是GPIOC_CRL。每个引脚占用4个bit位分别是CNFy[1:0]配置位和MODEy[1:0]模式位。对于复用推挽输出模式CNF[1:0]应设置为10即“复用功能输出模式”。MODE[1:0]决定输出速度。对于频率仅为1Hz的PWM低速就足够了。但通常为了保险和统一我们会选择11最大50MHz速度。这并不会增加功耗只是驱动器的响应速度上限实际翻转速度由定时器决定。所以我们需要将GPIOC_CRL寄存器中对应PC6和PC7的字段分别设置为0b1011即CNF10,MODE11。在提供的代码中是通过两个位域CNF6、MODE6等来分别设置的效果是一样的。实操心得在配置复用功能时一定要遵循“先开启外设时钟再配置引脚”的原则。虽然代码片段里没有体现但在完整的工程中务必在配置GPIO前通过RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC | RCC_APB2Periph_AFIO, ENABLE)来开启GPIOC和AFIO的时钟。时钟没开所有配置都不会生效这是新手最常踩的坑之一。4. 定时器核心功能模块配置硬件通路打通后我们来配置定时器3本身。这是整个任务的核心分为时基单元配置和输出比较通道配置两部分。4.1 时基单元初始化时基单元是定时器的心脏负责产生最基础的计数节拍。配置它就是配置我们之前计算好的PSC和ARR并设定计数模式。首先我们需要使能TIM3的时钟。TIM3挂在APB1总线上所以使能命令是RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE)。在寄存器版本中就是置位RCC-APB1ENR中的TIM3EN位。接着配置关键寄存器预分频器TIM3_PSC写入我们计算好的值7199。这个值会在下一次更新事件时被加载进实际的影子寄存器从而生效。自动重装载寄存器TIM3_ARR写入9999。同样这个值也会在更新事件时加载。计数模式TIM3_CR1.DIR设置为0代表向上计数。从0计数到ARR然后溢出归零。时钟分频TIM3_CR1.CKD设置为0代表tDTS tCK_INT即死区时间和数字滤波的时钟与定时器时钟一致。在简单的PWM输出中这个参数影响不大。自动重装载预装载使能TIM3_CR1.ARPE强烈建议设置为1使能。这意味着对ARR的写操作是写入一个预装载寄存器只有在下次更新事件时才会真正更新ARR的影子寄存器。这可以防止在运行中修改ARR时当前计数周期被意外切断导致输出波形出现毛刺。对于要求波形稳定的PWM这个必须开启。在提供的代码中还有一步STM32_Tim3_Regs-egr.bit.UG1;。这是通过软件产生一个更新事件UG1目的是立即将我们刚刚写入PSC和ARR寄存器的值加载到它们对应的影子寄存器中让新配置立刻生效。同时这个操作也会清空计数器让它从0开始计数。这是一个好习惯。4.2 输出比较与PWM模式配置时基单元产生了稳定的计数节拍输出比较通道则负责根据这个节拍来塑造波形。我们需要将通道1和通道2配置为PWM模式1输出。主要配置在以下几个寄存器中完成捕获/比较模式寄存器1TIM3_CCMR1CCxS[1:0]位对于通道1CC1S和通道2CC2S必须设置为00表示该通道被配置为输出模式。OCxM[2:0]位对于通道1OC1M和通道2OC2M设置为110即PWM模式1。在这个模式下在向上计数时当TIMx_CNT TIMx_CCRx时通道输出有效电平由极性位决定当TIMx_CNT TIMx_CCRx时输出无效电平。在向下计数时逻辑相反。OCxPE位输出比较预装载使能。建议设置为1使能。和ARPE类似它使得对CCRx寄存器的写入操作先进入预装载寄存器在下次更新事件时才生效。这样可以同步更新占空比避免在PWM周期中间改变占空比导致波形出现“断裂”。捕获/比较使能寄存器TIM3_CCERCCxE位通道输出使能。必须设置为1才能将比较输出的信号送到对应的引脚。CCxP位输出极性。设置为0表示“高电平有效”。即我们定义的有效电平是高电平无效电平是低电平。这意味着当计数值小于CCRx时引脚输出高电平LED点亮大于等于CCRx时输出低电平LED熄灭。如果你想实现“低电平点亮LED”共阳接法只需将此位设为1即可。捕获/比较寄存器TIM3_CCR1 TIM3_CCR2这就是决定占空比的关键寄存器。将我们计算好的值写入CCR1 5000,CCR2 1000。4.3 代码逐行解析与注意事项结合提供的代码片段我们逐行分析关键操作// 1. 重映射配置 STM32_Afio_Regs-mapr.bit.TIM3_REMAP3; // 3 即二进制11完全重映射这行代码必须在AFIO时钟开启后执行。它告诉芯片TIM3的通道1/2/3/4请使用PC6/7/8/9这组备用引脚。// 2. GPIO配置 STM32_Gpioc_Regs-crl.bit.CNF6Output_Af_push_pull; // 复用推挽输出 STM32_Gpioc_Regs-crl.bit.MODE6Output_Mode_50mhz; // 输出速度50MHz // ... PC7配置同理这里通过位域直接操作寄存器清晰明了。Output_Af_push_pull和Output_Mode_50mhz应该是开发者定义的宏分别对应0x2和0x3。// 3. 复位与时钟使能 STM32_Rcc_Regs-apb1rstr.all | RCC_TIM3RST; // 复位TIM3 STM32_Rcc_Regs-apb1rstr.all ~RCC_TIM3RST; // 结束复位 STM32_Rcc_Regs-apb1enr.all |RCC_TIM3EN; // 使能TIM3时钟先复位外设到一个确定状态再开启时钟是一个良好的初始化习惯。// 4. 时基单元配置 STM32_Tim3_Regs-arr.all9999; STM32_Tim3_Regs-psc.all720; // 注意这里是720不是7200这里出现了一个关键分歧根据我们前面的计算要得到10kHz的计数器时钟72MHz / 7200PSC应该设置为7199。但代码中写的是720。如果PSC720那么分频系数是721计数器时钟CK_CNT 72MHz / 721 ≈ 99.86kHz。此时ARR9999周期T 10000 * 721 / 72MHz ≈ 0.1001秒即约100ms而不是1秒。这会导致LED闪烁频率快10倍。我怀疑代码中的720可能是笔误或者是基于另一个不同的时钟配置比如APB1没有倍频定时器时钟就是36MHz。在实际项目中务必以你自己的时钟配置和计算公式为准。如果系统时钟是72MHz要达到1秒周期PSC应为7199。// 5. PWM通道配置 STM32_Tim3_Regs-ccmr1.ou_bit.OC1M6; // PWM模式1 STM32_Tim3_Regs-ccr1.all5000; // 50%占空比 STM32_Tim3_Regs-ccmr1.ou_bit.OC1PE1; // 使能CCR1预装载 // ... 通道2配置同理 STM32_Tim3_Regs-cr1.bit.ARPE1; // 使能ARR预装载这里正确配置了PWM模式和占空比并开启了预装载功能保证了波形切换时的稳定性。// 6. 启动定时器 STM32_Tim3_Regs-cr1.bit.CEN1; // 使能计数器最后一步拉下闸门计数器开始奔跑PWM波形随之输出。5. 完整代码整合与工程实践要点将上述所有配置步骤整合到一个有序的初始化函数中是工程化的关键。顺序很重要错误的顺序可能导致配置不生效或出现短暂错误输出。一个推荐的初始化流程如下开启时钟开启GPIO端口GPIOC、AFIO、TIM3所在总线的时钟。配置引脚重映射如果需要在开启AFIO时钟后配置AFIO_MAPR寄存器。配置GPIO为复用功能将PC6、PC7设置为复用推挽输出模式。复位并初始化定时器时基单元可选执行定时器软件复位TIM3-EGR TIM_EGR_UG以确保干净的状态。配置PSC、ARR、计数模式向上、时钟分频。务必设置ARPE1。配置PWM输出通道设置通道为输出模式CCxS00。设置PWM模式OCxM110。设置输出极性CCxP。使能输出CCxE1。设置占空比写入CCRx。务必设置OCxPE1。使能定时器设置CEN1计数器开始运行。可选产生软件更新事件在计数器使能前或后设置UG1可以立即加载所有预装载寄存器并清零计数器让新配置从第一个周期就开始完美运行。这里提供一个更符合常见库函数风格的逻辑框架并融入寄存器操作的思路void PWM_LED_Init(void) { // 1. 开启时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 2. 引脚重映射 GPIO_PinRemapConfig(GPIO_FullRemap_TIM3, ENABLE); // 库函数方式 // 3. 配置GPIO GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOC, GPIO_InitStructure); // 4. 配置定时器时基 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period 9999; // ARR TIM_TimeBaseStructure.TIM_Prescaler 7199; // PSC 72MHz/(71991)10kHz TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; // 时钟分频 TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; // 向上计数 TIM_TimeBaseStructure.TIM_RepetitionCounter 0; // 高级定时器用通用定时器忽略 TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); // 5. 配置PWM输出通道 TIM_OCInitTypeDef TIM_OCInitStructure; TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; // PWM模式1 TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; // 输出使能 TIM_OCInitStructure.TIM_Pulse 5000; // CCR1值决定占空比 TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; // 输出极性高 TIM_OC1Init(TIM3, TIM_OCInitStructure); // 初始化通道1 TIM_OCInitStructure.TIM_Pulse 1000; // CCR2值 TIM_OC2Init(TIM3, TIM_OCInitStructure); // 初始化通道2 // 6. 使能预装载寄存器关键 TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM3, ENABLE); // 7. 启动定时器 TIM_Cmd(TIM3, ENABLE); }实操心得使用标准外设库StdPeriph_Lib或HAL库时库函数会自动帮你设置很多关联的位。例如TIM_OCxInit()函数在设置PWM模式时会自动将CCxS位设为00输出模式。但用寄存器直接操作时你必须自己关注每一个细节位。两种方式没有绝对优劣寄存器操作让你对硬件了如指掌库函数则提升开发效率。建议初学者先从库函数入手理解了原理后再尝试寄存器操作这对调试底层问题有巨大帮助。6. 调试技巧与常见问题排查实录即使代码逻辑正确第一次上手也难免遇到LED不亮、闪烁频率不对、亮度异常等问题。下面是我在多年调试中总结的一些“踩坑”经验和排查清单。6.1 LED完全不亮这是最常见的问题。请按照以下顺序排查检查硬件连接首先确认开发板供电正常。用万用表测量PC6/PC7引脚对地电压。如果PWM输出正常你应该能看到电压在0V和3.3V之间快速变化对于1Hz信号用万用表直流电压档可能显示一个中间值如1.65V。如果一直是0V或3.3V说明软件配置有问题。确认时钟是否开启这是最容易被忽略的一步。确保RCC-APB2ENR中的IOPCEN和AFIOEN位为1RCC-APB1ENR中的TIM3EN位为1。可以在调试器中查看这些寄存器的值或者在代码初始化最开始添加RCC_Configuration()函数并确保它被调用。检查GPIO模式确认GPIOC_CRL寄存器中PC6和PC7对应的4位被正确设置为0b1011复用推挽输出50MHz。如果被错误地设置为输入模式或模拟模式信号是无法输出的。检查重映射配置确认AFIO_MAPR寄存器中的TIM3_REMAP位被设置为11完全重映射。如果没设置或设置错误PWM信号会输出到默认的PA6/PA7引脚PC6/PC7上自然没信号。检查定时器是否使能确认TIM3_CR1寄存器中的CEN位为1。如果为0计数器根本没启动。检查输出使能确认TIM3_CCER寄存器中的CC1E和CC2E位为1。如果为0比较输出单元到引脚的通路被关闭。6.2 LED常亮或常灭如果LED一直亮或一直灭说明PWM没有在高低电平之间切换。检查PWM模式与极性常亮检查TIM3_CCER.CCxP位。如果设置为0高电平有效但LED是共阳接法阳极接VCC阴极接IO那么高电平反而会让LED熄灭。你需要确认硬件电路。更可能的原因是占空比设置成了100%CCRx ARR1或0%CCRx 0。检查你的CCRx值。常灭检查TIM3_CCER.CCxP位。如果设置为1低电平有效而你的LED是共阴接法阴极接地阳极接IO那么低电平有效意味着CCRx比较有效时输出低电平LED不亮。同样需要确认硬件。也可能是CCRx值设为0在PWM模式1下计数器永远不小于CCRx0输出永远为无效电平如果极性为高有效则是低电平。检查ARR和CCRx的值在调试器中查看TIM3_ARR和TIM3_CCRx寄存器的值。确保它们符合你的计算。一个常见的错误是ARR设置得太小比如71而CCRx设置得很大比如50导致占空比很大在1Hz的频率下人眼几乎分辨不出熄灭的瞬间感觉像是常亮。用示波器观察引脚波形是最直接的判断方法。检查计数器是否在运行查看TIM3_CNT寄存器的值它应该在0到ARR之间循环变化。如果不变说明计数器没跑起来回头检查时钟和使能位。6.3 闪烁频率周期不对如果LED闪烁明显比1秒快或慢问题出在时基单元的计算上。确认系统时钟频率这是所有计算的基石。通过SystemCoreClock变量库函数或检查RCC_CFGR寄存器确认你的SYSCLK、HCLK、PCLK1以及定时器时钟CK_INT到底是多少。不要想当然地认为是72MHz。重新计算PSC和ARR根据确认后的CK_INT使用公式T (ARR1)*(PSC1)/CK_INT重新计算。确保计算过程没有错误。特别注意寄存器写入的值是分频系数减1和计数值减1。检查预分频器是否生效PSC寄存器有缓冲如果ARPE1写入后需要一次更新事件或设置UG1才会真正加载到影子寄存器。确保你在配置完成后要么等待一次更新事件要么手动产生一次更新事件TIM3-EGR | TIM_EGR_UG。检查是否有其他中断或代码干扰如果定时器配置为在更新事件时产生中断并且中断服务程序执行时间过长可能会影响定时器的正常计数。但这种情况对1Hz这种低频PWM影响微乎其微。6.4 亮度占空比不对如果闪烁频率正确但亮灭时间比例不对问题集中在CCRx寄存器。检查CCRx计算确认CCRx (ARR1) * DutyCycle。例如对于50%占空比和ARR9999CCR1应该是10000 * 0.5 5000。检查PWM模式确认TIM3_CCMR1中的OC1M/OC2M位设置为110PWM模式1。如果错误地设置为其他模式如冻结、强制输出等输出行为会完全不同。检查预装载如果你在定时器运行中动态修改CCRx的值但没有使能预装载OCxPE0那么新值会立即生效可能会打断当前PWM周期造成波形畸变。对于需要平滑改变占空比的应用如呼吸灯务必使能预装载并在修改后手动或等待更新事件。6.5 高级调试工具的使用逻辑分析仪/示波器这是最强大的调试工具。直接连接到PC6/PC7引脚可以直观地看到PWM波的频率、占空比、上升沿时间等所有信息。一看波形所有问题都无所遁形。调试器如ST-Link配合IDE如Keil MDK、IAR EWARM、STM32CubeIDE使用可以实时查看和修改所有寄存器的值。你可以单步执行初始化代码观察每一步操作后寄存器的变化确保配置按预期进行。串口打印虽然原始但有效。可以在代码关键点通过串口打印出寄存器值或状态信息辅助判断程序执行流程。最后分享一个我个人的调试习惯模块化测试。不要试图一次性写完所有代码并期望它工作。你可以先写一个简单的GPIO测试程序让LED以软件延时的方式闪烁确保硬件通路是好的。然后再单独测试定时器让它产生一个更新中断在中断里翻转一个测试引脚用示波器看中断是否精确地1秒一次。最后再把定时器和PWM输出功能结合起来。这样当问题出现时你就能快速定位到是哪个模块配置出错。