ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

定时器原理深度解析:从51到STM32,避开PWM占空比与中断陷阱

定时器原理深度解析:从51到STM32,避开PWM占空比与中断陷阱 定时器这东西我接触得越深越觉得它是个“大坑套小坑”的活儿。单看一篇手册你以为自己懂了“计数、比较、中断”这几个词一上板子却发现要么定时不准、要么中断不进、要么占空比死活到不了100%。我最早从51的T0/T1开始玩后来切到STM32的TIM、再到Linux下的高精度定时器中间踩过的坑堆起来比我写过的代码行数还多。所以今天这篇我想换一个角度不谈某个芯片的某个寄存器怎么配而是把定时器的原理、工作模式和那些“看上去正常但实际有毛病”的细节一次性讲透。内容以51、STM32、555、SysTick和软件定时器为主线能帮你把底层逻辑串起来遇到问题的时候不至于只能靠试。1. 定时器的底层逻辑其实就是一个“数数”的计数器先说一个最容易被人忽略的事实无论叫什么名字——51的定时器、STM32的TIM、GD32的Timer、ARM的SysTick、Linux的hrtimer甚至桌面软件里用setTimeout实现的延时它们的内核只有一个就是“数数”。1.1 一个时钟源一个计数器一个比较器所有定时器都离不开三个关键角色时钟源决定你“数一下”要花多长时间。它可能来自芯片内部的RC振荡器、外部晶振分频后的时钟也可能是外部引脚输入的脉冲。计数器一个不断累加或递减的寄存器比如51的TH0/TL0STM32的CNT。它每来一个时钟边沿就加1或减1。比较器/溢出逻辑计数器累加到某个目标值时触发一个动作中断、翻转引脚、清0、重装载等。注意这个“数数”用生活比喻最好理解你手里有个秒表每秒钟秒针跳一格这就是时钟源秒表上显示的数字就是计数器你设定“跑到第10秒时闹铃响”这个“10”就是比较值或溢出值。为什么理解这个底层逻辑很重要因为后续再复杂的模式——PWM输出、输入捕获、编码器接口、多定时器同步——都是在这个“数数”的基础上加各种外设功能而来的。1.2 分频器从高频时钟“数”出低频时间芯片的主频往往很高比如STM32F103的TIM挂载时钟是72MHz。如果直接用72MHz去数数计数器从0加到65535只需要不到1ms你想定一个10ms的周期就非常困难因为周期计数次数/时钟频率65535次撑死了也只有0.91ms。所以定时器前面加了一个预分频器Prescaler。它就是把时钟先除以一个系数得到稍慢的时钟。这个系数在STM32里由PSC寄存器决定分频后的频率为定时器时钟频率 输入时钟 / (PSC 1)注意几乎所有的定时器分频寄存器配置值都要加1才是实际分频系数。因为寄存器从0开始写0表示不分频即除以1写71就是除以72。如果你用STM32F103想要定时1ms主频72MHz目标周期为1ms那计算思路是定时器输入 72MHz / 7200 10kHz此时CNT加1耗时0.1ms 再计100次 10ms不对10kHz下1次是0.1ms100次就是10ms这是错的这里我就不写错例了直接给出正确姿势选择 PSC 71分频后 72MHz / 72 1MHzCNT加1耗时 1us 若目标定时 1ms则 ARR 1000 - 1 999为什么是减1因为计数器是从0开始数到999这一共是1000个时钟周期正好1ms。这种“寄存器值期望次数-1”的规则在整个嵌入式体系中到处可见PWM占空比、比较器阈值、捕获值运算都要小心这个-1。1.3 和“时间”的对应关系周期与频率的换算很多时候我们习惯用“微妙、毫秒”来思考但定时器寄存器只认“时钟周期数”。中间桥接的就是频率和周期换算时间 计数值 / 定时器时钟频率反过来计数值 时间 * 定时器时钟频率比如GD32F450的定时器它的时钟树里有一个RCU_TIMER_PSC_MUL2配置意思是定时器时钟可以倍频到系统时钟的2倍。这时候你算PSC就不是按72MHz算而是按144MHz算。很多从STM32转到GD32的人第一个坑就在这里——沿用旧的分频系数结果时间快了正好一倍。所以在配任何定时器之前第一件事不是抄例程而是确认当前芯片的定时器时钟到底是多少再谈分频、装载值。2. 单片机定时器的核心工作模式拆解有了“数数”的基础框架再看工作模式就轻松多了。以STM32的通用定时器为例它的几种模式基本涵盖了MCU定时器绝大多数应用场景。2.1 定时模式最基础也是最容易出错的定时模式就是把计数器配置为“数到某个值就溢出/更新”并触发中断。最初学51的时候很多人用的是工作方式116位定时初始化就是填TMOD、TH0、TL0然后TR01开跑中断里记得重装初值。如果懒一点直接进中断不重装第二次定时时间会变成从0开始跑到65535误差大得离谱。到了STM32定时模式更灵活因为自动重装载寄存器ARR帮你做了重装的事计数器CNT从0向上递增当CNT ARR时产生更新事件CNT自动清零然后继续从头开始你可以选择在更新事件时进入中断。就这么简单的一个流程却有3个常见问题问题1清中断标志的时机错了。HAL库里中断回调是HAL_TIM_PeriodElapsedCallback但如果用了中断却没有及时清标志下一次更新事件永远进不来表现为“只有第一次中断”。其实STM32的更新中断标志UIF需要软件清零HAL库会在HAL_TIM_IRQHandler里自动处理但如果你自己写寄存器版漏了清SR的UIF位就是只能进一次中断。问题2CNT计数到ARR时不进中断。看热词里有“定时器cnt加到car为什么没有产生中断”这个问题我专门排查过一次根因通常是开了定时器但没在NVIC里使能对应的中断通道更新中断没有在DIER寄存器里使能也就是TIM_IT_Update中断回调里提前把更新标志位清了导致HAL_TIM_IRQHandler判断不到标志。排查思路很简单第一步直接在中断服务函数里打个断点或翻转个GPIO看能不能进第二步查NVIC和DIER第三步再看定时器是否有其他中断优先级把更新中断抢占了。多数情况第一步就能发现问题。问题3定时周期“差不多但不对”。比如你算好了1ms实测却是1.1ms。这种偏差的来源一是系统时钟本身的精度HSE晶振、PLL配置二是分了频之后计数粒度不够。较好做法是用逻辑分析仪或示波器实测一个引脚翻转周期反推实际定时器时钟再微调PSC或者ARR。2.2 计数模式数外部脉冲别忘滤波计数模式和定时模式的区别只有一个它数的不是内部时钟而是外部引脚的脉冲。同样一个TIM可以配置为外部时钟模式1数TIx引脚或者模式2数内部触发输入。这常用来做测速、测频率。实际项目里我比较多用的是输入捕获模式来测频率。它其实可以算作“计数器捕获寄存器”的组合拳配置定时器为输入捕获第一个上升沿到来时把CNT值锁存到捕获寄存器第二个上升沿再来一次两次捕获值之差就是信号的周期在CNT不溢出的前提下。用这个差值除以定时器时钟频率就能得到信号频率。STM32定时器捕获测频率的具体思路1. 选择输入通道比如TIM2_CH1PA0 2. 配置为上升沿捕获预分频PSC设置合理值 3. 使能捕获中断或者DMA 4. 在中断里读两次CCR1值相减得到周期计数值 5. 频率 定时器时钟 / 周期计数值需要注意两点。第一如果被测信号频率很低比如10Hz计数器很容易溢出这时需要开启从模式复位把另一个定时器或内部事件配置为复位源或者把定时器配置为外部时钟模式周期测量寄存器法让计数器更“耐数”。第二输入引脚上最好加滤波尤其是电机换向这种干扰多的环境不滤波的话毛刺也能触发捕获测量结果跳得厉害。STM32的输入滤波器ICF位可以配置为“连续N个有效电平才确认边沿”实践下来比单纯靠软件消抖靠谱得多。2.3 PWM模式从原理到100%占空比的坑PWM输出的本质是计数器循环运行输出引脚在一个比较值处翻转或置位到溢出时又复位。它的两个关键量是周期由ARR决定占空比由比较寄存器CCR决定。拿向上计数、PWM模式1来说当CNT CCR时输出有效电平当CNT CCR时输出无效电平。也就是说CCR ARR时理论上输出一直是高电平占空比100%。但热词里有一个非常典型的场景“STM32F103定时器输出PWM占空比到不了100%”。我一开始也很纳闷明明算好了CCR ARR示波器测出来却有很窄的低电平毛刺或者干脆最高只能到99.x%。后来排查发现原因有好几层第一层很多例程用的PWM频率较高比如20kHz你设置的CCR是1000ARR也是1000但CCR再怎么说也需要一个时钟周期来比较计数器到达ARR时可能直接触发更新事件随后的那个周期如果配置成PWM模式1输出会在下一次匹配时立即拉低导致实际上达不到绝对100%。第二层如果你用了互补输出和死区插入高级定时器TIM1/TIM8死区时间会强制插入一段低电平即使CCRARR也不能输出连续的100%高电平。解决思路也分两路。要是用高级定时器试一下把死区设成0并检查CCER里的极性配置要是用通用定时器还遇到这个问题就用逻辑分析仪抓波形看是否有个窄脉冲有就说明是边界比较时序问题可以考虑把输出极性反过来、或者把计数模式改成中心对齐模式来绕开。2.4 定时器的“一专多能”一个定时器能否同时收PWM和发PWM热词里有人问“一个定时器能不能同时接收PWM数据和输出PWM波形”。这个问题要分两个层面看如果你的MCU定时器有多个独立通道比如TIM3有CH1~CH4那完全可以让CH1输出PWMCH2配置为输入捕获来测外部PWM频率/占空比。因为通道之间相对独立可以同时工作。但如果你说的是某个通道既要输出又要采集同频率的PWM那就有问题。比如你希望用同一通道既输出PWM又测自己输出的PWM频率绝大多数MCU是不推荐这么接的因为内部反馈信号和外部输入信号可能冲突。实操中我常用做法是用定时器A的CH1输出PWM给设备再用同一个定时器的CH2做输入捕获间接检测设备是否有异常比如堵转导致反电动势频率变化。这种方案能省一个定时器逻辑上也没有太多麻烦。2.5 多个定时器如何同步启动多定时器同步最常见的场景是电机控制里需要两路或多路PWM严格同频同相或者需要同时启动多个定时器保证时间基准一致。常规办法有几种主从模式把某个定时器设为主模式把它的更新事件或触发输出TRGO连到另一个定时器的从模式输入端。例如TIM1的更新事件作为TRGO接到TIM2的从模式ITR0令TIM2在接收到触发时启动。这样只要TIM1一开始跑TIM2也就跟着跑误差在同一个时钟域内可以忽略。外部统一触发多个定时器都配置为“外部触发从模式”然后共用一个外部信号启动。这个方法的好处是启动时刻完全一致适合做多通道同步采集。软件同时置位最土的办法把几个定时器的CR1寄存器中的CEN位通过一次写操作同时置1。在Cortex-M内核上这不是原子操作除非你关中断所以启动时间可能会有几个周期的偏差低速应用可以凑合高速PWM同步就不太行了。我自己的体会是宁可多用一条内部触发连线去搞主从同步也别偷懒用软件启动。因为电机驱动这类场合对PWM相位一致性要求高软件方式抖一下电流波形就容易出现低频振荡。3. 那些“不算MCU定时器”的定时器如果只聊单片机这个题目就太窄了。工程里你一定会碰到SysTick、555这类经典芯片、以及Linux/Windows下的软件定时器它们的原理可以互相印证。3.1 SysTick一个和操作系统强绑定的定时器SysTick是Cortex-M内核自带的定时器它不依附于某个外设所以哪怕你换不同厂家的STM32或GD32SysTick的基础代码基本通用。它是一个24位递减计数器计数到0时可以触发异常中断并且可以自动重装载。它有两大典型用法提供RTOS的心跳时钟FreeRTOS的xPortSysTickHandler就是挂在SysTick中断里。此时SysTick的配置频率和系统时钟节拍通常1000Hz或100Hz直接挂钩。有人曾问“SysTick的时钟源是什么”答案在CoreDebug或SysTick控制寄存器里可以是处理器时钟HCLK也可以是HCLK/8具体看芯片实现。在部分低功耗场景为了让SysTick计数更稳定有人会选择HCLK/8但这样时间分辨率降低实时性要求高的系统一般不用。提供裸机延时如果你不想用定时器TIM拿SysTick做delay是最快的。经典实现为void delay_us(uint32_t us) { SysTick-LOAD us * (SystemCoreClock / 1000000) - 1; SysTick-VAL 0; SysTick-CTRL | SysTick_CTRL_ENABLE_Msk; while (!(SysTick-CTRL SysTick_CTRL_COUNTFLAG_Msk)); SysTick-CTRL ~SysTick_CTRL_ENABLE_Msk; }实际上这个写法对us比较大时并不严谨因为SysTick是24位SystemCoreClock如果是72MHz那么最大值只能定约233ms超过这个值的延时就不能一次性LOAD。所以长时间延时你得循环多次或者拆成ms级循环再补us。很多人在网上下载的延时函数一改主频就失效就是因为没算LOAD溢出。我个人建议如果你只做裸机延时直接用DWT数据观察点与跟踪单元里的CYCCNT做us级延时更好用它是32位的也不用和操作系统抢SysTick。这个思路比较冷门但实测下来非常稳。3.2 555定时器硬件定时器的老祖宗别看555是个1971年的老芯片它的内部结构放到今天依然是理解“比较器触发器”的最佳教材。555内部有三个5kΩ电阻分压组成两个阈值电压1/3 VCC、2/3 VCC两个比较器一个SR锁存器一个放电三极管。它最经典的是多谐振荡器无稳态模式用来产生方波电容通过R1和R2充电电压升到2/3 VCC时比较器翻转输出低电平内部放电管导通电容经R2放电电压降到1/3 VCC时再次翻转输出高电平放电管截止又开始充电。周期公式是很多电子入门课必背的T_high 0.693 * (R1 R2) * C T_low 0.693 * R2 * C注意如果R2为0T_low就是0但正常工作一般不这么接因为会过流损坏放电管。555定时器产生方波的频率范围宽、电路极度简化我在教学板或快速原型里仍然经常用。它给嵌入式工程师的启发是定时不一定非要用程序硬件定时器天然不受程序跑飞、中断阻塞的影响。3.3 Linux下的定时器与Cron表达式从内核态到用户态到了Linux定时器的概念更偏向“调度”而非“硬件计数”。早期Linux内核用“时间轮”timer wheel维护大量定时器现在还有高精度定时器hrtimer。用户态程序则常用timerfd、POSIX Timer、epoll配合来做一个“精准到毫秒”的定时循环。Linux C定时器的经典做法至少有三种setitimer发送SIGALRM信号周期触发。优点是简单缺点是信号处理函数里能干的事有限精度一般。timerfd_createpoll/epoll把定时器抽象成一个文件描述符到期后fd可读。这种非常适合写事件驱动的服务不会破坏现有的事件循环也可以精确到纳秒级别取决于内核配置和硬件。POSIX Timertimer_create更灵活可以选择信号通知或者线程通知精度高但API比前两个复杂。如果你的项目是在业务系统里写“每分钟执行一次报表生成”那直接用Cron表达式最省事。Cron表达式的核心是6~7个字段分别表示秒、分、时、日、月、周年可选前端做定时组件时最常见的问题是时区和下一次执行时间计算。如果把Cron解析放在前端要注意new Date()的时区偏移否则在UTC8环境下很容易差8小时。我见过不止一个定时任务组件部署到海外服务器上就偏了最后发现是前端把本地时间转成字符串解析时时区被吞了。从原理上讲Linux的软件定时器跟STM32定时器没有本质不同都是“一个任务队列每次tick时检查哪些任务到期”。区别只在于MCU的tick来自硬件中断Linux的tick来自内核时钟节拍或者高精度事件。理解了这一层你在任何平台上写定时器都不会慌。3.4 桌面端“定时器失效”的坑以Windows锁屏定时器为例热词里有一条“windows锁屏定时器失效”这个很有代表性。Windows下如果你用SetTimer它依赖消息循环锁屏后系统为了省电可能会挂起或降低计时精度如果用CreateWaitableTimer它理论上不受锁屏影响但如果你没有设置“唤醒”权限或者系统进入睡眠状态依然会不准。解决办法通常有两种用多媒体定时器timeSetEvent高精度或者CreateWaitableTimer并在电源管理里阻止系统进入睡眠或者注册PowerSettingNotification在睡眠后恢复定时。把定时逻辑放到服务程序里服务运行在Session 0不受当前用户锁屏影响。严格来说这不是定时器原理的问题而是操作系统的电源管理介入。嵌入式工程师做上位机的时候经常忽略这一点以为自己程序写得没问题半夜跑定时任务总是对不上点最终发现是笔记本合盖睡过去了。4. 常用定时器配置实操手把手走一遍前面讲了一大堆原理接下来给几个能直接抄作业的配置思路覆盖51、STM32/GD32和C#环境下的“精准定时”。4.1 51定时器从工作方式1到方式251单片机的定时器有两个T0和T1都是16位的。常见工作方式方式013位计数器老古董基本不用方式116位计数器方式28位自动重装载。方式2在串口波特率里用得很多因为它可以自动重装初值不需要中断里额外赋值。方式1则是通用定时的主流。写一个经典“定时1ms中断翻转LED”的初始化void Timer0_Init(void) { TMOD 0xF0; // 清除T0的配置位 TMOD | 0x01; // T0工作方式116位定时 TH0 (65536 - 1000) / 256; // 12MHz晶振12分频后1MHz1ms计1000次 TL0 (65536 - 1000) % 256; ET0 1; EA 1; TR0 1; } void Timer0_ISR(void) interrupt 1 { TH0 (65536 - 1000) / 256; // 重装初值 TL0 (65536 - 1000) % 256; LED !LED; }注意这里有个细节如果中断里重装初值后CNT已经从0开始跑了一会儿进中断也是要时间的中断入口要压栈这会导致时间略微偏长。高精度场合建议用“提前重装修正”的办法或者干脆换成自动重装载的方式2但方式2最多只能装8位初值对于1000us来说不够。4.2 STM32标准库/HAL库配定时器中断HAL库的套路比较固定拿TIM3做1ms更新中断来说TIM_HandleTypeDef htim3; void MX_TIM3_Init(void) { htim3.Instance TIM3; htim3.Init.Prescaler 72 - 1; // 72MHz/72 1MHz htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 1000 - 1; // 1MHz1000次 1ms htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_Base_Init(htim3); }关键点在于HAL_TIM_BASE_MSP_INIT回调里面会帮你做GPIO时钟、中断优先级、NVIC的配置。很多人忘写这个回调结果定时器初始化成功后中断就是进不去。检查办法在HAL_TIM_Base_Start_IT(htim3)之后看htim3-State是否正常再看NVIC里TIM3中断是否Enable。更新中断回调不要写在IRQHandler里要写在void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3) { // 你的定时逻辑 } }4.3 GD32的定时器配置差异GD32F450最近用的人多它的定时器和STM32神似但时钟树有差异。热词里的rcu_timer_clock_prescaler_config(RCU_TIMER_PSC_MUL2)就是把TIMER时钟倍频到2 * APBx时钟。配置步骤大致是rcu_periph_clock_enable(RCU_TIMER1); rcu_timer_clock_prescaler_config(RCU_TIMER_PSC_MUL2); // 定时器时钟倍频2倍 timer_deinit(TIMER1); timer_parameter_struct initpara; initpara.prescaler 144 - 1; // 144MHz/144 1MHz initpara.period 1000 - 1; // 1ms initpara.alignedmode TIMER_COUNTER_EDGE; initpara.counterdirection TIMER_COUNTER_UP; initpara.clockfilter 0; timer_init(TIMER1, initpara); timer_update_event_enable(TIMER1); // 注意GD32必须显式使能更新事件 timer_interrupt_enable(TIMER1, TIMER_INT_FLAG_UP); nvic_irq_enable(TIMER1_IRQn, 1, 0); timer_enable(TIMER1);如果你在GD32上套用STM32的PSC值因为GD32默认定时器时钟可能已经是倍频后的时间会比预期快一倍。所以拿到一款新MCU最先看时钟树图确认定时器所在总线频率以及能否倍频再写分频系数。4.4 用C#做1ms精准定时上位机里做高精度定时很多人直接用System.Windows.Forms.Timer但它依赖UI消息循环精度只有约15ms哪怕你设置Interval1实测也可能跳到15ms甚至更高。要做到1ms级别得用System.Threading.Timer或StopwatchThread.Sleep混合。我的做法是用Stopwatch自旋等待var sw System.Diagnostics.Stopwatch.StartNew(); while (sw.ElapsedMilliseconds 1) { } // 这段空转在Release下才有意义Debug下会被优化和调试器干扰但纯自旋会占满一个CPU核不适合多任务环境。更稳的是用CreateWaitableTimer或者timeBeginPeriod(1)提升系统定时分辨率。Win32 APItimeBeginPeriod可以把系统最小定时器周期调到1ms但用完必须timeEndPeriod还原否则系统整体功耗上升。[DllImport(winmm.dll)] static extern uint timeBeginPeriod(uint uMilliseconds); [DllImport(winmm.dll)] static extern uint timeEndPeriod(uint uMilliseconds);注意这个API只影响系统定时器分辨率不影响线程调度延迟。如果你在一个忙碌的UI线程里调用定时器照样可能卡顿。要精准建议把定时逻辑放独立线程或者后台任务里。4.5 用Cron表达式组件前端定时别踩时区前端定时组件最流行的方案是cron-parser、cronstrue这类库。开发一个定时任务管理页面时我的习惯是后端统一存储Cron表达式和预期时区前端只负责展示和编辑不做任何本地时间转换。为什么因为不同浏览器、不同操作系统的时区处理方式有差异你没法保证每个用户的本机时间都正确。如果非要前端做“下一次执行时间”的计算记住一点始终用UTC毫秒数做运算展示时才转本地时间。千万不要用new Date(2024-01-01 00:00:00)这种字符串解析因为不同浏览器对不带时区的ISO字符串解析规则不同轻则差8小时重则直接返回Invalid Date。5. 常见问题与排查技巧实录下面这些案例每一个都是我在实际项目里真正遇到的整理成速查表供参考。5.1 定时器CNT已经等于ARR为什么中断就是不进我先一步步复现排查过程。假设使用STM32F103TIM2配置为向上计数ARR999开了更新中断。第一步检查定时器是否真的跑起来了while(1) { uint16_t cnt TIM2-CNT; }看CNT是否在变化。如果CNT一直不动说明时钟没来查RCC里TIM2时钟是否使能或分频配置是否把时钟设成了0。第二步检查SR寄存器里UIF位是否在定期置1。如果置1说明比较/溢出已经发生只是没有进中断那就是NVIC或DIER的问题。第三步检查中断服务函数是否存在并且在启动文件里正确命名。我见过不少“换了芯片型号启动文件的IRQHandler名称对不上”导致程序HardFault的案例。5.2 定时器输出PWM占空比到不了100%排除法排序如下先看CCR是否真的等于ARR中间有没有被其他代码改动再看输出极性CCER里的CC1P位是否配置成高电平有效检查是否开启了死区高级定时器尤其明显最后用示波器看最小低电平时间。如果低电平只有几十ns大概率是边界比较时序问题可考虑把PWM频率降低或者改计数模式中心对齐模式下CCRARR更容易输出满占空比。5.3 一个定时器能同时读PWM、写PWM吗如果你指的是“同一个定时器的不同通道”完全可以。比如TIM4的CH1输出PWMCH2做输入捕获。要注意的是输入捕获和输出比较共用CCR寄存器的一部分资源具体到某个定时器型号可能有限制需要查手册确认捕获/比较通道是否支持同时独立工作。STM32F1系列通用定时器一般是支持的只要你不把同一个通道配成既输出又输入。5.4 三个定时器为什么不能同时启动很多人以为只要在同一行代码里写三个Enable就能同步但在Cortex-M里三次写TIMx-CR1是三条汇编指令中间完全可以被中断打断。如果中断里又恰好操作了定时器同步性就破坏了。想要严格同步优先用主从模式TIM1为主其它TIM为从触发源设为ITR。或者用一个外部GPIO统一触发。这样无论代码执行到哪里硬件上都是同一时刻启动。5.5 锁屏后定时器失效、Linux下定时器不准Windows锁屏失效的原因前面提到了主要和电源管理相关Linux下定时器不准则可能是内核CONFIG_HZ配置过低如100Hz精度10ms或者系统负载太高导致进程调度延迟。高精度场景要么用timerfdepoll要么考虑实时线程clock_nanosleep(TIMER_ABSTIME)。我用clock_nanosleep的绝对时间模式做过周期任务抖动明显比相对时间模式小因为它不会累积误差。6. 写在最后的经验和扩展定时器这个主题越细究越发现它连接了硬件和软件的很多底层机制。我个人在实际操作中的体会是遇到定时不准、中断不进、占空比不对先不要怀疑编译器先拿示波器/逻辑分析仪量引脚看看硬件行为到底是什么。软件层面的“我以为”常常是最大的坑而波形不会说谎。另外如果手头有带FPU和硬件定时器的新MCU建议从第一天就花半小时确认三件事定时器时钟源、分频寄存器是否要加1、中断标志是硬件自动清还是要软件清。这三件事能拦住80%的定时器问题。最后分享一个小技巧在调试PWM或定时器时永远先让定时器输出一个已知频率的方波比如1kHz用示波器确认精确无误后再去改复杂参数。这样一旦后面波形异常你能判断是配置问题还是负载问题而不是像无头苍蝇一样来回试寄存器值。如果你看完这篇能对定时器的“数数”本质和各大平台的差异有个整体把握以后再换任何一款芯片配置定时器时心里都会更有底。
返回列表