ARTICLE DETAIL

资讯详情

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

嵌入式信号捕捉与中断处理:从原理到优化的实战指南

嵌入式信号捕捉与中断处理:从原理到优化的实战指南 做嵌入式这些年信号捕捉和中断这两个词几乎天天碰面。最近在调一块电机控制板的编码器测速又一次被“中断穿插”这个老问题折腾得够呛。说它老是因为原理谁都懂——事件来了触发中断CPU放下手里的活去处理处理完再回来继续但实际代码跑起来各种莫名其妙的现象全冒出来了标志位被覆盖、中断响应变慢、偶发丢事件、临界区没关好导致数据错乱。这一篇我就以“信号捕捉”和“穿插中断”为主线把从原理到实现、从踩坑到优化的完整思路整理出来希望对正在啃单片机中断部分的读者有实际帮助。这套内容不只适用于STM32也适用于其他Cortex-M、51、AVR这类带中断系统的单片机只要理解了机制换芯片只是换寄存器名的问题。适合正在做定时器输入捕获、外部脉冲计数、通信协议解析、传感器信号同步这类需求的开发者参考。如果你被“中断里到底能不能做耗时操作”“为什么我开了中断还是丢信号”这类问题折磨过这篇文章就是给你的。1. 信号捕捉的本质为什么轮询永远低人一等1.1 信号捕捉不是“检测”而是“记录时机”很多人刚接触单片机时对信号捕捉的理解就是“用IO口读电平”。这个理解没错但做出来的东西往往只能在实验室里转一旦上到真实工况就露馅儿。我习惯把信号捕捉拆成两层来看信号层外部事件产生的物理变化比如编码器A相的电平翻转、按键按下、脉冲宽度变化。时间层这个事件发生的时刻或者相邻事件之间的时间差。轮询方式读IO口其实只完成了信号层的一部分——你能读到当前电平是高还是低但无法精准知道“什么时候”发生的变化。原因是轮询周期受主循环代码长度影响主循环里只要多跑几条耗时指令你对边沿的感知误差就可能拉到几十微秒甚至几百微秒。试想一个测速场景编码器每圈输出1000个脉冲电机转速3000转/分那脉冲频率是50kHz一个周期20微秒。如果用主循环轮询循环体稍微长一点MCU还没转完一轮呢几个脉冲就过去了丢脉冲几乎无法避免。而中断方式完全不同边沿触发后CPU暂停当前工作立即记录现场、打上时间戳、把计数加一再回去继续原先的事情。这个“记录时机”的能力是中断相对于轮询最核心的优势。1.2 信号捕捉的“三维指标”我在实际选型时会从三个维度衡量一套信号捕捉方案是否合格指标含义轮询方案表现中断方案表现实时性从事件发生到CPU感知的时间微秒到毫秒级不定纳秒到微秒级取决于中断响应完整性单位时间内捕获事件的比例高频时明显丢失合理设计下基本不丢精准度时间戳记录与真实时刻的偏差偏差随主循环波动由硬件定时器保证偏差极小这里的关键是“合理设计”四个字。中断方案如果设计不好完整性反而比精心调校的轮询还要差。最典型的反面案例就是外部中断服务函数里塞了一个串口发送函数打算把每次捕捉到的事件实时发出去。结果一条115200波特率的串口发一个字节就要将近87微秒如果发10个字节中断被占用将近1毫秒。这个期间如果又来了10个脉冲中断标志位可能只留下一个剩下的全部丢失。所以信号捕捉的底层逻辑很清楚把“感知事件”和“处理事件”拆开。感知必须由硬件中断完成处理可以放到主循环或者更合适的时机去做。这也就引出了下面“穿插中断”这个话题。2. 穿插中断的设计边界哪些活能留在中断里哪些必须赶出去2.1 中断服务函数的三条铁律“穿插中断”这个说法我理解有两层含义中断打断主程序的执行流把一段代码“穿插”到当前工作中。多个中断源之间互相穿插低优先级被高优先级打断。很多人对第一层含义不以为然觉得中断嘛就是临时跳到另一个函数跑一圈而已。但跳转这件事本身是有代价的函数内容越复杂代价越明显。我给自己定过三条铁律做嵌入式这些年基本没出过大问题中断服务函数里只做“读取保存置标志”。任何可能阻塞的操作一律不放中断里。中断服务函数执行时间必须控制在微秒级别。展开说。所谓“读取保存置标志”就是先读硬件寄存器拿到需要的数据把它保存到全局变量或者缓冲区里然后置一个标志位通知主循环“有事情发生了”。至于读取之后要不要滤波、要不要处理数据、要不要发消息这些全部放到主循环里去。为什么不推荐在中断里做“完整处理”有两个原因。第一个是实时性反噬中断处理时间越长CPU响应其他中断的延迟就越大等于你自己把系统的实时性拖垮了。第二个是重入风险如果一个低优先级中断正在处理同类型的更高优先级中断来了会打断它如果你的代码依赖某个静态变量的连续状态就很容易出bug。2.2 关中断与临界区的粒度控制穿插中断里最微妙的问题是主程序和中断服务函数共享数据时的保护。初学者最容易犯的错误是想保护一段代码于是把中断关了执行完再打开。如果这段代码比较长关中断时间就特别长。我见过一个具体案例一个同事在主程序里为了读取编码器计数值直接用了__disable_irq()然后执行了一条32位的内存读取再__enable_irq()。这个操作本身问题不大但如果他在关中断期间执行了浮点运算、延时等待甚至printf问题就来了。正确的粒度控制思路是这样的只保护“共享变量访问”那几行代码。关中断时间必须短到可以忽略不计。如果要访问的数据量较大用“先拷贝再处理”的方式关中断把硬件寄存器或共享缓冲区的数据拷贝到局部变量开中断然后处理局部变量。以编码器计数为例如果主循环要计算速度做法是volatile uint32_t encoder_count; uint32_t safe_snapshot; __disable_irq(); safe_snapshot encoder_count; __enable_irq(); // 在 safe_snapshot 基础上计算速度这段代码的中断关闭时间只有几条指令对系统实时性影响微乎其微却保证了数据的一致性。这就是“该关就关关完立刻开”的典型实践。2.3 中断服务函数的标准模板基于上面的原则我给自己定了一个中断服务函数模板各平台通用void EXTI_IRQHandler(void) { uint32_t status; status get_interrupt_status(); // 1.读取硬件状态 clear_interrupt_flag(status); // 2.清除中断标志 g_signal_flag 1; // 3.置主循环标志 g_signal_data status; // 4.保存必要数据 }这个模板看起来简单但有几个细节很重要。第一先读状态再清标志防止边沿触发期间再次到达的信号被误清。第二标志位和数据要定义成volatile否则编译器优化可能导致主循环读不到最新值。第三如果中断服务函数里需要判断不同信号源判断分支也要尽量合并减少分支跳转时间。3. 定时器输入捕获与外部中断两种最常用的信号捕捉手段3.1 定时器输入捕获自带硬件时间戳测频测宽都靠它如果只是数脉冲个数外部中断就够用但如果你需要测量脉冲的频率或者高电平持续时间那就得靠定时器输入捕获了。定时器输入捕获的原理很巧妙定时器始终在自由计数当检测到预设边沿到来时硬件会把当前计数器的值“锁存”到捕获寄存器里同时产生捕获中断。这个值就是事件发生时刻的硬件时间戳精度跟定时器时钟直接相关。假设定时器时钟是72MHz计数器是16位那么计数器从0到65535走一遍的时间是910微秒左右。测脉宽时如果被测信号的周期超过这个范围捕获值就会产生回绕。处理办法是在中断里判断新捕获值是否小于上一次捕获值如果小于说明定时器已经回绕过要额外加上一个计数周期。这是新手最容易漏掉的细节。来看一个具体的STM32定时器输入捕获示例测量外部信号的频率volatile uint32_t capture_buffer 0; volatile uint8_t capture_flag 0; void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_CC1)) { TIM_ClearITPendingBit(TIM3, TIM_IT_CC1); capture_buffer TIM_GetCapture1(TIM3); capture_flag 1; } }主循环里读取capture_buffer通过两次相邻捕获值的差值delta current - previous计算出脉冲周期。如果定时器时钟频率为timer_clock那么信号频率就是f timer_clock / delta。这里有个非常坑的点如果你在中断里直接读捕获值而不考虑上一轮的值是否被处理完那么当两个脉冲间隔极短时第二次捕获会覆盖第一次尚未被主循环消费的值。此时信号没有丢但信息丢了。所以我通常的做法是在中断里维护一个深度为4或8的环形缓冲区把捕获值依次放进去主循环再从缓冲区里取出来计算。缓冲区填满时意味着主循环处理不过来这时才真有资格说“信号捕捉不过来”。3.2 外部中断脉冲计数的简单粗暴方案外部中断适合做低速脉冲计数比如按键检测、光电开关计数、编码器测速低速场景。STM32的外部中断引脚通常要配置成边沿触发模式上升沿、下降沿、双边沿都行。具体触发方式取决于传感器输出信号特性。按键消抖这块也是个经典问题。硬件上加了RC滤波或者施密特触发器的话代码里可以少写消抖逻辑如果什么都没加就得在外部中断里做“时间窗消抖”也就是触发中断后读取当前系统时间与上一次触发时间比较间隔小于设定值就判定为抖动直接忽略。我把消抖逻辑放在外部中断服务函数里没有放到主循环原因是主循环的调度时延不稳定消抖判断放在主循环里会导致抖动窗口忽长忽短反而容易误判。放在中断里用硬件定时器的计数值做时间基准每次判断的精度是一致的。一个典型的按键计数外部中断示例volatile uint32_t key_count 0; volatile uint32_t last_trigger_time 0; volatile uint32_t current_time; void EXTI0_IRQHandler(void) { current_time TIM_GetCounter(TIM2); // 读取硬件时间戳 if ((current_time - last_trigger_time) DEBOUNCE_TICKS) { key_count; last_trigger_time current_time; } EXTI_ClearITPendingBit(EXTI_Line0); }这段代码里面有个很容易被忽视的点current_time - last_trigger_time这个减法不需要判断谁大谁小因为定时器是回绕计数的无符号减法本身就具备模运算的特性。这个技巧在处理时间戳差值时非常实用前提是定时器计数类型必须使用无符号类型。3.3 用DMA配合中断捕捉批量信号当信号速率再往上升外部中断加上读取寄存器的开销就可能顶不住了。这时候可以开启DMA让DMA自动把外设数据搬运到内存缓冲区搬完一整批后再中断通知CPU。比如用ADC连续采集传感器波形每次ADC转换完成都会触发DMA请求DMA把结果循环写入缓冲区当缓冲区的传输计数减到一半或者全部完成时触发DMA中断。CPU在中断里只需要处理“半满”和“全满”两个事件大大降低中断频率同时保证每个数据都被存下来了。“半满中断”这个玩法很适合做音频采集、振动信号分析这类需要连续数据流的场景。它的巧妙之处在于CPU处理前半段缓冲区的数据时DMA还在往后半段写数据两个区域互不冲突CPU和DMA就像流水线的两道工序谁也不用等谁。4. 实测中的中断优先级陷阱与信号丢失排查4.1 优先级配置不合理高中断率信号把低中断率任务饿死中断优先级这东西看着就几个数字配错之后的后果可能非常隐蔽。我遇到过一个大无语场景外部中断负责编码器计数定时器中断负责1ms系统心跳。我把外部中断优先级配得比定时器高结果编码器转速一上去外部中断几乎霸占了CPU定时器中断一直被推迟系统心跳抖动从1ms拉到了将近5ms直接导致靠心跳做时间基准的控制逻辑全部乱套。反过来如果定时器中断优先级远高于外部中断编码器脉冲又比较密集外部中断可能在排队期间就被新脉冲覆盖造成计数丢失。所以优先级的配置原则不是“重要信号给高优先级”这么简单而是要看中断发生频率和可容忍延迟的乘积。频率高且对实时性要求极高的才配最高优先级频率高但不敏感的配中优先级即可频率低但必须立即响应的可以配高优先级。我常用的一个配置思路系统心跳通常是定时器高优先级因为所有时间相关的逻辑都依赖它。外部信号捕捉中到高优先级取决于信号密度。串口收发中优先级配合DMA使用时不占太多时间。按键等低频事件低优先级。4.2 标志位被覆盖是“丢信号”的最大元凶很多人遇到“中断明明触发了主循环却没反应”的情况第一反应是中断没配置好。其实大部分时候中断确实触发了问题出在标志位只有一个bit而事件来了两次。假设外部中断里这样写volatile uint8_t pulse_flag 0; void EXTI_IRQHandler(void) { pulse_flag 1; }如果两个脉冲相隔非常短第一次中断把pulse_flag置1第二次中断再次置1。主循环处理完第一次之后把pulse_flag清0。这时候你根本不知道两次脉冲已经把标志位置了两次第二次事件的信息在“置1-清零-置1-清零”的竞态中丢失了。正确做法是使用计数器而不是布尔标志volatile uint32_t pulse_count 0; void EXTI_IRQHandler(void) { pulse_count; }主循环读取并清零时要使用“先读后清”加临界区保护uint32_t snapshot; __disable_irq(); snapshot pulse_count; pulse_count 0; __enable_irq();这样即使中断频繁触发计数也不会丢。用计数器代替标志位是我做信号捕捉以来最重要的经验之一。4.3 信号线毛刺导致的中断风暴还有一种情况特别折磨人信号源本身不干净。没有上拉电阻的悬空引脚或者长线传输的传感器信号很容易产生毛刺。一个毛刺就是一个上升沿触发频率可能高达几兆赫兹瞬间塞满中断队列把CPU活活拖死。解决思路有三层按成本从低到高排在信号线和地之间加RC滤波在引脚附近放一个0.1微法电容滤除高频毛刺。在代码里做“连续边沿确认”也就是检测到第一次边沿后不立即计数而是延时几十微秒再次读取引脚电平确认稳定后才算有效边沿。进入外部中断后重设引脚触发方式先改成上升沿触发处理完再改回下降沿触发这招可以解决部分抖动场景但不太推荐因为处理期间可能会漏事件。4.4 排查信号丢失的完整流程我在现场排查信号丢失问题时一般不急着改代码而是按以下顺序走一遍用逻辑分析仪抓信号线波形确认信号本身频率、脉宽、毛刺情况。这一步能排除信号源问题。查看中断服务函数执行时间。在中断入口和出口各反转一次GPIO引脚用示波器测量高电平宽度就能知道中断到底占用多长时间。检查中断优先级配置看高优先级中断是否频繁抢占低优先级。确认所有共享变量是否加了临界区保护是否有编译器优化导致的值不同步。检查中断标志位的清除位置和时机。这套流程走下来大部分丢失问题都能定位到具体环节。最怕的就是跳过波形检查直接改代码改来改去都不知道信号本身根本没进到MCU里。5. 穿插中断的进阶优化环形队列、双缓冲与运行时序分析5.1 中断与主循环通过环形队列解耦信号捕捉频率高了以后简单计数器已经不够用了。如果每个信号事件还附带时间戳或其他信息就得用队列把这些信息暂存起来让主循环慢慢消化。环形队列是最经典的做法。核心思路是中断服务函数只负责往队列尾部写入数据主循环从队列头部读出数据满时丢弃或者置溢出标志。关键点在于读指针和写指针的操作必须考虑竞争。下面是一个精简的环形队列实现#define QUEUE_SIZE 64 typedef struct { uint32_t data[QUEUE_SIZE]; volatile uint8_t head; volatile uint8_t tail; } ring_queue_t; void queue_init(ring_queue_t *q) { q-head 0; q-tail 0; } uint8_t queue_is_full(ring_queue_t *q) { return (uint8_t)((q-head 1) % QUEUE_SIZE) q-tail; } uint8_t queue_is_empty(ring_queue_t *q) { return q-head q-tail; } void queue_push(ring_queue_t *q, uint32_t value) { if (queue_is_full(q)) { return; // 队列满丢弃 } q-data[q-head] value; q-head (q-head 1) % QUEUE_SIZE; } uint32_t queue_pop(ring_queue_t *q) { uint32_t value q-data[q-tail]; q-tail (q-tail 1) % QUEUE_SIZE; return value; }中断里调用queue_push主循环里先判断非空再queue_pop。这里有个需要特别注意的地方单消费者单生产者的场景下入队操作不需要关中断因为主循环不会往队列里写数据出队操作也不需要关中断因为中断不会从队列里取数据。只有当“一个读一个写”都涉及同一块数据时临界区保护才有必要。很多初学者一上来就给所有队列操作加关中断反而把简单问题复杂化了。5.2 双缓冲采集与计算并行不悖如果信号捕捉的目的是得到一段连续波形环形队列还不够因为主循环计算的时候新数据可能把还没算完的数据覆盖掉。双缓冲能很好地解决这个问题。双缓冲就是准备两块缓冲区一块给中断采集写入一块给主循环计算读取。采集写满了切换主循环的读写角色。关键在于切换时要保证当前缓冲区写完了然后原子地交换两个指针。我之前用双缓冲处理光电传感器的脉冲宽度统计结构大概是这样的#define BUF_SIZE 256 static uint16_t buf_a[BUF_SIZE]; static uint16_t buf_b[BUF_SIZE]; static uint16_t *write_buf buf_a; static uint16_t *read_buf buf_b; static volatile uint8_t write_index 0; static volatile uint8_t buf_ready 0; void TIM_IRQHandler(void) { write_buf[write_index] TIM_GetCapture1(TIMx); if (write_index BUF_SIZE) { uint16_t *tmp write_buf; write_buf read_buf; read_buf tmp; write_index 0; buf_ready 1; } }主循环发现buf_ready被置1就知道read_buf里有一整批完整数据可以处理处理完后将buf_ready清零。这种设计的优势在于计算过程不会影响采集过程数据不会互相踩踏主循环处理多慢都没关系只要平均速度赶得上数据产生速度就行。5.3 穿插中断的时序分析把时间轴画出来做中断优化时我强烈建议把中断时序图画在纸上或者白板上不要只在脑子里想。因为中断嵌套、排队、共享变量这几件事叠加在一起脑子的短期记忆很容易漏掉某个边界条件。我一般这样画时序图先画一条主程序执行的时间线。在主时间线上标出各个中断触发点。用竖直箭头表示中断进入用矩形块表示中断服务函数的执行区间。如果存在嵌套矩形块套矩形块。在矩形块旁边标注这段时间内其他中断是否被挂起。这个画法看起来很原始但特别有效。几乎每次排查“偶发性问题”画完时序图以后都能发现一两处之前没注意到的问题。比如两个中断几乎同时触发但优先级高的那一个执行时间过长导致低优先级中断在排队期间被后续同类型事件覆盖。5.4 用GPIO翻转法测量中断执行时间很多开发者觉得自己写的代码执行时间很短不需要测量。但“觉得”和“实测”之间的差距往往就是线上事故的根源。测量中断服务函数执行时间最简单的方法就是翻转引脚。在中断服务函数的第一行把某个空闲GPIO拉高最后一行拉低然后用示波器观察这个引脚的高电平宽度。得到的时间就是中断服务函数从开始到结束的实际执行时间。我实测过一个极端案例一个外部中断服务函数里加了按键消抖延时用了delay_us(10000)也就是10毫秒。当时代码跑起来现象是“按键偶尔没反应”用GPIO翻转法一测发现中断占用了10毫秒这期间编码器脉冲来了几百个全丢了。所以后来我对“中断里能不能放延时”这类问题只有一个回答不能除非你确定这个延时不会影响其他任何中断。6. 中断与主循环的数据同步关于volatile、内存屏障与编译优化6.1 volatile必须加但只靠volatile不够volatile是嵌入式面试题里的常客但在实际代码里我发现很多人对这个关键字的理解只停留在“告诉编译器不要优化”这个层面。实际上volatile确实能防止编译器把变量优化到寄存器里也能防止编译器对读取次数做优化但它不能防止CPU乱序执行也不能解决多核场景下的缓存一致性问题。在单核单片机上volatile加上临界区保护基本能解决绝大多数共享变量的问题。但前提是你必须把共享变量声明成volatile。一个真实案例我在中断里更新了一个全局变量g_angle主循环里判断g_angle threshold后执行动作。加了volatile后编译器每次都从内存读取没问题。但如果我忘了在头文件里加extern volatile而是在另一个.c文件里重新定义一个普通变量编译器就可能在优化时把这个变量放到寄存器里导致主循环永远读不到中断更新的值。这种问题非常隐蔽因为它不是每次编译都会出现取决于优化级别和代码量。6.2 读改写操作的原子性问题嵌入式开发中非常容易踩的坑是“读改写”操作被中断打断。比如主循环里有这样一段代码g_count;看起来是一句话但编译成汇编后通常是三条指令把g_count从内存读到寄存器。把寄存器加1。把寄存器写回内存。如果在第二步执行时来了一个中断而中断服务函数里也对g_count做了操作那么中断退出后主循环的第三步会把旧值加1写回去覆盖掉中断函数里更新的结果。解决办法有两种一是用临界区保护关中断-修改-开中断二是想办法用硬件原子指令。很多Cortex-M0内核没有硬件原子操作指令只能靠临界区。Cortex-M3/M4则有LDREX/STREX指令但标准C语言里没有直接暴露通常要通过编译器内建函数调用。在应用层我倾向于用“先读快照再对快照操作最后写回”的方式同时在外围加上关中断保护逻辑清晰也不容易错。6.3 从实际经验谈中断服务函数的代码风格写了这么多年代码我总结了一套适合中断服务函数的代码风格分享给大家参考中断服务函数命名规范统一一眼就能看出属于哪个外设。函数体内不允许出现任何阻塞循环。函数体内不允许调用打印函数。函数体内尽量只操作volatile变量和缓冲区。一行代码只做一件事降低阅读成本。在函数顶部注释里写明执行的预期时间。这套风格最大的价值在于当线上出问题时你能快速通过审查中断服务函数排除或者确认问题而不是在成百上千行代码里找来找去。7. 穿插中断项目中的调试工具与常用技巧7.1 示波器、逻辑分析仪比仿真器更靠谱嵌入式调试中我越来越依赖示波器和逻辑分析仪而不是仿真器。因为中断是异步的仿真器单步执行时中断照样可能到来反而干扰了对程序流程的观察。示波器抓物理信号逻辑分析仪抓多路信号时序关系这两个工具足以覆盖大部分信号捕捉和中断调试的需求。比如你要确认外部中断边沿触发配置是否正确直接看信号波形和MCU引脚处是否有毛刺比反复读寄存器更直观。7.2 在中断里维护“调试计数器”线上跑的时候没法接示波器怎么办我常用的一个办法是在中断服务函数里维护几个调试计数器中断总触发次数、成功入队次数、队列满丢弃次数、标志位重复次数。这些计数器通过串口周期性打印出来就能判断系统运行时信号捕捉是否正常。一个典型的调试计数器结构volatile uint32_t dbg_exti_count 0; // 外部中断触发总次数 volatile uint32_t dbg_queue_full_count 0; // 队列满丢弃次数 volatile uint32_t dbg_timer_capture_count 0; // 定时器捕获次数如果发现dbg_exti_count的增量和实际信号不符说明信号在硬件层面就已经丢了如果dbg_exti_count增量正常但dbg_queue_full_count很大说明主循环处理速度跟不上需要优化主循环或者加大缓冲区。这套方法调试“偶发问题”尤其有效。因为偶发问题经常你盯着的时候不出现你一走就出现。有了计数器就能事后通过打印数据分析当时系统的运行状况。7.3 仿真器断点调试中断时的小心机非要用仿真器打断点时我建议不要直接在主循环里打断点因为中断来了会乱跳你在主循环打断点其实很难看清全局。更好的办法是在中断服务函数入口设置条件断点条件是某个调试计数器达到一定值。这样断点只在满足特定条件时触发能帮你定位特定次数的信号事件。另一个技巧是观察窗口里监控共享变量时记得勾选“按实时刷新”否则仿真器暂停时你看到的只是暂停瞬间的值不代表运行中的实时状态。8. 一套可复用的信号捕捉框架思考我这几年做信号捕捉相关项目最终沉淀出一套可以复用的框架不管MCU型号怎么换思路都不变。框架分四层硬件抽象层封装定时器输入捕获配置、外部中断触发方式、GPIO翻转调试接口。信号采集层负责中断服务函数将信号事件写入环形队列或者双缓冲。数据处理层在主循环里读取队列数据做滤波、计算、统计。输出层通过串口、显示模块或者控制指令对外输出结果。在这个框架里信号采集层是唯一跟中断强相关的部分也是我花心思最多的地方。它必须做到中断服务函数极简。队列操作无锁单生产者单消费者。缓冲区大小可配置能覆盖主循环最大处理延迟下的数据量。提供溢出计数便于在线判断采集是否饱和。有了这样的框架每次接到一个新需求我只需要替换硬件抽象层重新配置定时器和外部中断再修改数据处理层的算法上层代码基本不用动。就拿这次编码器测速来说信号层代码跟之前做的脉冲宽度测量项目几乎完全一样区别只在数据处理层把“捕获差值”换算成了“速度值”。这种复用带来的效率提升是实实在在的。最后再分享一个我实测过的小技巧调试信号捕捉问题时可以把中断服务函数入口的GPIO翻转测量作为一个固定配置留着不要删。线上如果需要快速确认中断是否在跑示波器一夹就知道了不用重新编译烧录。这个习惯帮我省了很多来回烧录的时间。
返回列表