资讯动态

定时器输出翻转模式详解:从原理到GD32实战

发布时间:2026/8/30 2:43:41 来源:尧图企业网站定制
在嵌入式调试中定时器输出始终是绕不开的基础功能。但很多人一上来就是PWM模式遇到需要精确翻转电平的场景才发现PWM反而绕了远路。其实定时器还有一类容易被忽视的toggle mode输出翻转模式它能在不需要CPU干预的情况下让输出引脚在每次比较事件到达时自动翻转电平。这篇文章就围绕timer output channel toggle mode展开把原理、寄存器配置、实际波形验证以及相关的高频故障一次讲透。我最早接触这个功能是在做一个红外载波发射模块时。当时的需求很简单输出38kHz的方波驱动红外发射管占空比最好能控制CPU还要腾出来处理解码和协议栈。用普通PWM模式当然也可以但每次修改频率都要同时算ARR和CCR两组寄存器稍不注意极性就反了。后来换成toggle mode只改一个比较值就能微调频率波形质量也稳定得多。这篇文章就适合两类读者一类是刚入门、对定时器输出模式区分不清的初学者另一类是已经会用PWM、但想减少CPU开销或者需要生成特殊波形、正在寻找更优方案的开发者。1. 先搞清楚toggle mode到底在解决什么问题1.1 三种输出模式的本质区别MCU定时器的输出比较通道通常至少支持三种输出行为PWM模式、强制输出模式和toggle翻转模式。很多人对PWM很熟但对toggle模式的理解常常停留在“翻转电平”这个字面上没有深入想它背后的用途。从硬件角度看PWM模式是靠计数器CNT和比较寄存器CCR的持续比较在一个周期内自动拉高和拉低输出引脚占空比由CCR和ARR共同决定。强制输出模式则是直接把输出引脚强制置高或置低不受比较事件影响。而toggle模式是当计数器CNT的值等于比较寄存器CCR的值时产生一次比较事件输出引脚电平自动翻转一次翻转后继续计数等下一次比较事件再翻转一次。从波形上看toggle模式天然输出方波频率是计数频率的一半。因为一个完整方波周期需要翻转两次即CNT从0到CCR一次、再从CCR到下一个周期匹配点又一次。1.2 toggle mode的核心价值用最少的CPU介入生成精确方波我个人的理解是toggle mode的核心价值有两个第一CPU开销极低启动后完全不需要软件维护第二频率分辨率高因为修改输出频率只需要调整一个比较值而不像PWM那样要同时维护周期和占空比。举个例子使用GD32F303这种Cortex-M4内核的MCU主频可以跑到120MHz。如果定时器时钟直接走72MHz预分频设为1那么计数频率就是72MHz。想要输出一个1kHz的方波在toggle模式下只需要把CCR设置为72000000/2000 36000也就是每计数36000次翻转一次。因为翻转两次才是一个完整周期所以方波频率是72000000/(2*36000) 1kHz完全符合要求。如果改用PWM模式想输出1kHz、占空比50%的方波需要同时配置ARR71999、CCR36000。看起来也不复杂但是当你在运行中要动态改变频率时PWM模式必须同时更新ARR和CCR还要注意更新时刻避免毛刺而toggle模式只需更新CCR一个寄存器简单程度高了一个量级。1.3 适合toggle mode的典型场景从实际项目看toggle mode尤其适合以下几类场景红外载波发射比如38kHz或56kHz的载波用toggle模式生成方波再通过三极管驱动红外二极管频率稳定、代码量小。无源蜂鸣器驱动有些无源蜂鸣器需要特定频率的方波驱动才会响用toggle模式输出一个恰好落在谐振频率附近的方波效果比软件延时翻转稳定得多。步进电机脉冲输出步进电机驱动器需要脉冲信号脉冲频率决定转速。如果使用定时器PWM模式每个周期都要输出两个边沿频率计算稍微绕一点用toggle模式脉冲频率计算更直接。产生非对称脉冲让CCR不取周期的一半而是取一个较小或较大的值就能让翻转点不对称从而得到不同占空比的波形——这一点比PWM灵活因为PWM的极性行为相对固定。注意toggle模式对实时性没有要求因为电平翻转完全由硬件完成。真正要注意的是中断是否开启以及比较值是否被正确写入影子寄存器。2. 硬件原理拆解从定时器计数到引脚翻转2.1 抓住三个关键寄存器要让定时器输出通道工作在toggle mode软件上只需要配置好三个核心寄存器分别是控制寄存器CCER、捕获/比较模式寄存器CMOS或叫CCMR和捕获/比较寄存器CCR。以GD32系列为例它们的寄存器名和STM32风格很接近以下是核心配置位CCER的第0位CC1E输出通道1使能。只有这个位置1引脚才被连接到定时器输出功能上否则引脚保持GPIO状态。CMOS寄存器中的OC1M[2:0]位这是输出模式选择位。PWM模式一般是110PWM模式1或111PWM模式2而toggle模式是011。把这个值写成011通道就进入翻转模式。CCR寄存器设置比较值决定输出翻转的时刻。除了以上三个还有一个经常被忽略但很关键的寄存器位CMOS寄存器里的OC1PE输出比较预装载使能位。如果OC1PE置1CCR的写入会在更新事件时生效而不是立即生效如果清0则写入立即生效。对于toggle模式我一般建议开启预装载因为这样在运行中更新CCR时不会产生瞬间的毛刺或异常波形。GD32标准外设库有对应的函数比如timer_channel_output_mode_config()直接传入TIMER_OC_MODE_TOGGLE就能完成模式设置。2.2 单通道toggle与多通道联动的差异单通道toggle模式只影响一个输出引脚逻辑最简单。但很多MCU的定时器支持多个输出通道比如CH0、CH1、CH2、CH3它们可以独立配置成不同模式也可以组合使用。在GD32的通用定时器上每个通道都有独立的CCR和模式配置位。我实测过TIMER1的CH0和CH1同时工作在toggle模式两个通道各跑各的频率完全互不干扰。但要注意多通道toggle模式不会自动产生相位差相位差只能靠初始电平不同或者软件在启动时有意错开。什么叫有意错开就是让两个通道的CCR不同时匹配举例来说CH0的CCR设为1000CH1的CCR设为1500那么两个通道虽然都在翻转但因为比较值不同翻转时间点不同方波之间就有了固定的相位关系。这在生成两相或者三相脉冲时有用。另一个与多通道相关的功能是定时器主从联动。比如让TIMER0产生更新事件去触发TIMER1开始计数从而实现多个定时器输出的协调。这在一些需要多路不同频率方波的场合比单一定时器多通道更灵活。2.3 波形时序为什么toggle模式能精确控制频率和占空比拆解一段波形时序以计数器从0向上计数到ARR后溢出清零为例。假设计数器时钟频率为f_ck_cntARR寄存器值为NCCR寄存器值为M。toggle模式下输出引脚电平的翻转时刻是CNTM和CNTN1溢出清零后回到0进入下一轮之间的比较事件但每次匹配都会翻转一次。因此如果M不等于0也不等于N一个完整周期的前M个计数是高电平、后N-M1个计数是低电平方波频率为f_out f_ck_cnt / (2*(N1))。占空比 M / (N1)假设从0开始翻转也可以调整极性来实现反转。如果M恰好取(N1)/2那占空比就是50%也就是标准方波。用这个公式就能解释为什么toggle模式频率计算公式和PWM不同PWM一个完整周期内只有一个高电平段和一个低电平段由同一次周期匹配控制toggle模式一个周期内有两次匹配事件所以输出频率天然低一半。我个人的习惯是先在纸上画出CNT的锯齿波再标注出CCR匹配点最后把输出电平标在下面。波形一画出来CCR和频率的关系就一清二楚比死记公式可靠得多。3. 手把手配置GD32定时器输出翻转模式3.1 环境准备与引脚选择这里我以GD32F303为例使用TIMER1_CH0输出翻转波形。开发环境用的是Keil MDK配合GD32标准外设库。硬件上只需要一个示波器或者逻辑分析仪观察PB0引脚TIMER1_CH0的默认映射之一就行。选定时器之前需要确认两件事。第一定时器挂在哪个时钟总线上GD32F303的TIMER0和TIMER1挂在APB2上其他的通用定时器挂在APB1上第二定时器时钟倍频关系APB1和APB2的预分频不同定时器时钟可能被倍频不同的分频系数会直接影响到后续的PSC和CCR计算。GPIO配置方面TIMER1_CH0在PB0引脚上是复用功能输出需要配置为AFIO推挽输出模式并设置合适的输出速度。如果配置成普通GPIO或者模拟输入那无论定时器怎么翻转引脚都不会有信号出来。这是初学者最容易犯的错误之一。3.2 代码实现从初始化到启动的完整配置过程下面给出一段可直接使用的配置代码注释写得比较详细方便对照寄存器理解。#include gd32f30x.h void TIMER1_Toggle_Mode_Init(uint16_t arr, uint16_t psc, uint16_t ccr) { // 1. 开启定时器时钟和GPIO时钟 rcu_periph_clock_enable(RCU_TIMER1); rcu_periph_clock_enable(RCU_GPIOB); rcu_periph_clock_enable(RCU_AF); // 2. 配置PB0为TIMER1_CH0复用推挽输出 gpio_init(GPIOB, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_0); gpio_pin_remap_config(GPIO_TIMER1_PARTIAL_REMAP, ENABLE); // 注意如果TIMER1_CH0默认就在PB0上或者已经映射到需要的引脚可以不用重映射 // 3. 定时器复位到默认状态 timer_deinit(TIMER1); // 4. 设置定时器参数预分频、周期、计数方式 timer_parameter_struct timer_init_struct; timer_struct_para_init(timer_init_struct); timer_init_struct.prescaler psc - 1; // 预分频值实际分频为psc timer_init_struct.period arr - 1; // 自动重装载值 timer_init_struct.clockdivision TIMER_CKDIV_DIV1; timer_init_struct.counterdirection TIMER_COUNTER_UP; // 向上计数 timer_init_struct.alignedmode TIMER_COUNTER_EDGE; timer_init(TIMER1, timer_init_struct); // 5. 配置通道0为toggle模式 timer_channel_output_mode_config(TIMER1, TIMER_CH_0, TIMER_OC_MODE_TOGGLE); timer_channel_output_pulse_value_config(TIMER1, TIMER_CH_0, ccr); timer_channel_output_preload_config(TIMER1, TIMER_CH_0, TIMER_OC_PRELOAD_ENABLE); // 6. 使能通道输出 timer_channel_output_enable(TIMER1, TIMER_CH_0); // 7. 使能定时器自动重装载 timer_auto_reload_shadow_enable(TIMER1); timer_update_event_enable(TIMER1); // 8. 启动定时器 timer_enable(TIMER1); } int main(void) { // 假设系统时钟120MHzAPB2定时器时钟120MHz输出1kHz方波 // 计数频率 120MHz / 60 2MHz // 期望输出频率 2MHz / (2*1000) 1kHz TIMER1_Toggle_Mode_Init(1000, 60, 500); while(1) { // 主循环可以做其他事情方波由硬件持续输出 } }这段代码有几个容易出错的地方需要注意。第一prescaler和period都要减1因为寄存器值从0开始。第二timer_channel_output_pulse_value_config中的pulse值就是CCR它决定了翻转时刻我在这里设成500比周期1000小一半所以得到一个50%占空比方波。第三如果不开timer_channel_output_preload_config更新CCR时可能会在运行中产生瞬时异常波形开了预装载后新比较值会等待下一次更新事件统一生效。3.3 验证方法示波器波形实测配置完成后用示波器探头接PB0和GND应该能看到稳定的方波输出。测量频率时要注意示波器的探头补偿是否正确否则幅值和边沿可能有误差。我实测下来的波形频率精确度很高只要晶振准toggle模式输出的频率误差主要来自硬件时钟源而不是软件计算。在动态修改频率的场景下可以在主循环中用timer_channel_output_pulse_value_config()修改CCR。比如想从1kHz切换到2kHz在PSC不变的情况下需要把CCR从500改成250对应周期减半频率翻倍。实测修改瞬间波形会平滑过渡不会出现明显的频率跳变毛刺因为预装载在起作用。提示使用timer_channel_output_mode_config(TIMER1, TIMER_CH_0, TIMER_OC_MODE_TOGGLE)时确认库函数版本支持这个模式枚举值。有些旧版本的库可能把枚举命名成TIMER_OC_MODE_TOGGLE但内部实现不同如果编译报错直接查头文件里的枚举定义即可。4. timer相关的几个高频坑慢一倍、空指针与ADC联动4.1 定时器慢了一倍多半是时钟树配置的锅围绕热搜词中“定时器慢了一倍”的情况我复盘了之前一个GD32项目中的经历。当时用定时器产生一个1ms中断做软件计时结果实测中断间隔是2ms整整慢了一倍。排查下来问题出在定时器时钟源选择上。GD32的定时器时钟来源于APB1或APB2而APB预分频系数为1时定时器时钟直接等于系统时钟APB预分频系数大于1时定时器时钟通常会被倍频到2倍APB时钟。如果用错了预分频配置或者忽略了倍频关系实际计数频率就会和理论计算差一倍直接表现为定时器“慢了一倍”。遇到这种问题我的排查思路是先确认RCC配置里APB1和APB2的分频系数再查看当时用的定时器挂在哪个总线上最后用示波器测量一个已知延时来反推实际计数频率。如果反推出来正好差一倍九成是时钟倍频没算对。再加上SystemCoreClock或rcu_clock_freq_get这类函数打印出实际时钟频率就能快速定位。4.2 查询timer执行报空指针初始化顺序和回调上下文要留意热搜词里有“timer执行查询是报空指针”。在单片机环境里空指针通常出现在定时器中断回调函数中访问未初始化的全局指针或者PC上位机的WinForm Timer的事件处理函数中访问了尚未创建的对象。以WinForm开发为例很多人习惯在窗体构造函数里先创建Timer组件但在某个按钮事件中才初始化Timer用到的数据结构。如果Timer的Tick事件在数据结构初始化之前被触发访问空对象必然报空指针。解决办法很简单把数据结构的初始化放到窗体构造函数里或者给Timer设置一个“就绪标志位”Tick事件开头检查标志位未就绪就直接返回。在单片机上定时器中断里使用指针前也建议做有效性判断。比如在中断回调里访问一个环形缓冲区指针如果缓冲区还没初始化先判断指针是否为NULL再访问避免在中断上下文里直接崩溃。调试这类问题时可以用__get_IPSR()这类寄存器读取函数判断当前是否处于中断上下文再决定能否安全调用某些可能阻塞的函数。4.3 用定时器触发ADC采样时的联动配置要点GD32的ADC支持定时器触发常见做法是使用定时器的TRGO事件来触发ADC采样这能实现精确的采样频率而不依赖软件定时。配置ADC和定时器联动时有几个关键点定时器的TRGO事件必须通过timer_master_output_trigger_config()配置成更新事件。如果不配置TRGO可能保持默认值ADC永远等不到触发信号。ADC的触发源要设置为“外部触发”并且选择对应的定时器触发通道。采样时间不能太短否则ADC内部采样电容没有足够时间充电转换结果会偏小或波动。如果在while循环里用ADC查询标志位等待转换完成要确保触发间隔足够长否则上一次转换还没结束下一次触发就来了可能读到旧数据。我碰到过一个典型情况定时器已经触发了ADC但ADC读到的数据一直跳动后来发现是采样时间配置太短再加上信号源阻抗较高导致采样值不稳定。把采样时间从1.2周期改成7.5周期后数据立刻稳定了。这个经验也适用于任何用定时器做周期性采集的场景。5. 常见问题速查解决toggle mode相关的典型报错与异常为了查阅方便我把平时调试中遇到的与timer output toggle mode相关的典型问题整理成了表格大家可以直接对照排查。现象可能原因解决方案输出引脚无波形GPIO没有配置为复用功能检查GPIO模式是否为AF_PP或AF_OD输出引脚无波形定时器通道输出未使能调用timer_channel_output_enable()输出频率与预期差一倍误用了PWM频率公式toggle模式频率计数频率/(2*(ARR1))输出频率与预期差一倍时钟树倍频计算错误确认APB1/APB2分频系数及定时器倍频关系修改CCR后波形出现毛刺输出预装载未开启开启timer_channel_output_preload_config()中断里访问变量报空指针指针使用前未初始化初始化前置或增加空指针判断ADC采样数据跳动定时器触发配置不对或采样时间太短确认TRGO事件配置适当延长采样时间定时器不计数未调用timer_enable()确认定时器已启动波形频率抖动时钟源不稳定使用外部晶振或校准内部RC这个表格并不是全部但覆盖了大部分开发中遇到的问题。如果遇到表格之外的问题优先用一个最小规模的测试工程来复现这样能大大缩小排查范围。提示在调试toggle模式时我强烈建议先把中断关闭只用示波器看波形。等波形正确了再打开中断处理其他逻辑。这样可以把硬件配置问题和软件中断问题分开定位更快。6. 实操心得用toggle mode之后我改了哪些设计习惯最后分享一个我自己真实调整过的设计习惯。早期做脉冲输出类产品时我总是习惯用PWM模式加阻塞延时来模拟各种频率代码里到处都是延时函数CPU被占得死死的。后来在一次红外解码项目中认真使用了toggle mode才发现硬件定时器能帮我们做这么多事主循环几乎被彻底解放出来。从那以后只要需要周期性方波输出我都会先评估一下能不能用定时器输出比较翻转模式来实现能不能把频率计算改成单个CCR的调整如果答案是肯定的我基本不会去写软件翻转的延时逻辑。这不仅是代码量变少更重要的是系统的实时性和稳定性都提升了。硬件定时器一旦启动不受中断优先级、任务调度和代码执行时间的影响这在需要精确频率的场合是软件模拟完全无法替代的。另外我在设计基于GD32的ADC采样时也更倾向于用定时器触发代替软件延时采样。触发间隔精准、CPU不参与、数据一致性有保障。定时器输出比较模式不只是生成方波的小工具它其实是整个定时器系统的核心能力之一把主从联动、触发ADC、触发DMA这些都串联起来以后单片机的潜力会被充分释放。如果你正卡在某个定时器输出问题上不妨先停下来画一张CNT与CCR匹配的时序图再结合示波器波形去验证寄存器配置很多问题当场就能看出来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价