ARTICLE DETAIL

资讯详情

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

嵌入式实时系统中断、任务与调度抖动治理实战

嵌入式实时系统中断、任务与调度抖动治理实战 1. 先搞清楚抖动到底指什么嵌入式控制系统里中断、任务、抖动这三个词几乎天天被人挂在嘴边但真正让系统跑不稳的往往是这三种东西混在一起之后产生的时间不确定性。我做过的板子里有电机电流环、有温湿度闭环、有交通灯时序最后让人半夜爬起来改代码的基本都是同一类问题该在第 N 个微秒发生的动作跑到了第 N 加几十个微秒的地方。它不一定崩但控制效果会飘波形会毛参数怎么调都调不好。这里必须先把抖动这个词拆开。它在两拨人嘴里是完全不同的东西搞信号链的人说抖动指的是皮秒级的时钟抖动搞实时系统的人说抖动指的是微秒到毫秒级的调度抖动。这两者成因、量级、治理手段全都不一样混在一起讨论就是鸡同鸭讲。我见过有人在 100 kHz 采样率的 ADC 前面纠结 20 ps 的时钟相位噪声结果系统真正的抖动来源是 RTOS 的 1 ms tick 带来的周期性延迟——后者比前者大了整整六个数量级。这篇文章适合谁看如果你正在做带闭环的嵌入式控制系统用着 STM32 这类 MCU跑着 FreeRTOS 或者裸机前后台并且发现参数调好了但效果不稳偶尔丢一帧数据输出有周期性毛刺那这里讲的东西基本能对上你的症状。纯做上位机的同学也能看但说实话这里的坑你不踩。1.1 时钟抖动皮秒级的战场时钟抖动指的是时钟信号边沿相对于理想位置的随机偏移通常用 RMS 值和峰峰值来描述单位是 ps 或 fs。它的危害主要在两个地方高速串行链路的误码率以及 ADC/DAC 的采样孔径误差。对于 ADC有一个非常经典的关系式可以直接估算抖动对信噪比的影响SNR_jitter -20 × log10(2π × f_in × t_jitter)拿实际数字算一下。假设你的时钟 RMS 抖动是 100 ps输入信号频率 10 kHz2π × 10^4 × 1×10^-10 6.28×10^-6-20 × log10(6.28×10^-6) ≈ 104 dB也就是说10 kHz 信号下 100 ps 抖动几乎不构成瓶颈104 dB 的信噪比远高于绝大多数 12 位 ADC 的 74 dB。但如果输入频率提高到 1 MHz同样的 100 ps 抖动就会把极限压到约 64 dB这就开始成为系统瓶颈了。结论很清楚低频信号链不需要为抖动过度设计高频采样才需要认真对待。抖动又分随机抖动和确定性抖动。随机抖动来自热噪声服从高斯分布只能用统计方式描述确定性抖动来自电源纹波、串扰、占空比失真通常有明确的周期性来源可以定位、可以消除。实践中排查时钟抖动第一步永远是看电源看参考时钟源附近有没有开关电源的开关频率分量耦合进去。1.2 调度抖动微秒到毫秒级的战场大多数嵌入式控制系统的实际问题出在这里。调度抖动指的是任务或中断实际执行时刻相对于理想时刻的偏差典型量级场景典型抖动主要来源裸机 硬件定时器触发 1 us中断延迟、当前指令完成裸机 主循环轮询几十 us ~ 几 ms循环体内其他耗时操作RTOS 硬件定时器 高优先级任务3 ~ 20 us调度器开销、临界区RTOS SysTick 软定时器1 ~ 2 mstick 分辨率、tick 中断被延迟带 Flash 擦写 / 文件系统10 ~ 500 ms总线 stall、写放大这张表是我自己项目里实测汇总的不是手册数据但它能解释很多玄学现象。比如为什么把控制环从主循环搬到定时器中断后效果立刻变好为什么加了日志文件系统之后闭环开始出现低频振荡。一个特别容易被忽略的点SysTick 驱动的软件定时器其抖动下限就是一个 tick。FreeRTOS 里vTaskDelay(1)的实际延时可能是 1 ms也可能是接近 2 ms取决于调用时刻落在 tick 相位的哪个位置。如果你的控制周期必须有确定的相位绝对不能用vTaskDelay来定时。1.3 控制系统的抖动预算怎么分工程上要做的是把总抖动指标分配到各个环节。假设你做一个电流环周期 100 us要求周期抖动不超过 5%也就是 5 us那么预算可以这样切硬件触发到中断入口允许 1 usISR 入口到采样值被读取允许 1 us计算完成到 PWM 寄存器更新允许 2 us余量1 us这个预算表写出来贴在看板上比任何架构文档都有用。因为后续每加一个功能你都能立刻判断这一下吃掉了多少预算。我见过最典型的翻车是把一段浮点 FFT 塞进和电流环同一个 ISR 里单次执行 30 us直接把 100 us 的环搞成了不确定周期。预算分完之后要留一个越界检测机制在控制输出前读一次时间戳如果本次周期偏离超过阈值就置一个计数器这个计数器能在调试阶段告诉你抖动到底发生了多少次、发生在什么负载条件下。这比事后拿示波器去找毛刺高效得多。2. 中断离硬件最近的那一层时间中断是整个系统里时间精度最高的一层也是最容易被写坏的一层。它的延迟决定了控制系统的响应下限它的执行时间决定了同优先级和低优先级中断被推迟多久。2.1 从中断标志置位到你第一行代码中间发生了什么以 Cortex-M4 为例这条链路大概是这样外设置位中断标志位NVIC 检查该中断是否使能、优先级是否高于当前门限CPU 等当前指令执行完如果是 LDM、STM、除法、浮点等长指令要多等几个周期硬件自动压栈xPSR、PC、LR、R12、R3~R0共 8 个寄存器、32 字节从向量表取中断服务函数地址跳转执行编译器生成的寄存器保存代码你的 C 代码第一行真正开始跑在 168 MHz 的 M4 上硬件压栈加取向量大约是 12 个周期也就是 71 ns 量级。加上长指令延迟、Flash 等待周期、总线竞争实测从标志置位到 ISR 第一行代码通常在 0.3~1 us 之间。这里有两个细节值得单独说。第一是尾链tail-chaining。当一个 ISR 返回时正好有另一个中断挂起CPU 不需要重新压栈和出栈只花 6 个周期就能跳过去。这在多外设同时中断的场景下能省下大量时间但也意味着如果你有多个同优先级中断同时来它们的执行顺序是按中断号排的不是按到达时间。需要严格时序的场合必须用优先级区分。第二是迟到的中断late-arriving。如果压栈阶段来了一个更高优先级的中断CPU 会直接改取那个更高优先级中断的向量省掉一次出栈入栈。这个特性对高优先级控制环有利但也让最坏情况分析变得复杂。2.2 ISR 写法的三条红线我总结过很多次线上事故ISR 的问题基本集中在三条线上。红线一ISR 里不允许阻塞。任何形式的延时、等待、信号量获取超时都不能出现在 ISR 里。在 RTOS 环境下ISR 里只能调xxxFromISR版本的内核 API普通版本内部可能进临界区甚至触发调度行为未定义。红线二ISR 要短且长度可预期。短不只是说平均执行时间短更重要的是最坏执行时间稳定。一个有 if 分支的 ISR如果某个分支会触发一串 SPI 传输那这个分支就是整个系统抖动的源头。我的习惯是 ISR 里只做三件事清标志、取数据、置事件其他全部推到任务里做。红线三ISR 里不要用浮点除非你很清楚代价。M4F 这类带 FPU 的核默认开启惰性压栈lazy stacking第一次用浮点会额外压栈 17 个寄存器那一拍就是几十个周期。更麻烦的是你很难预测某个编译器优化会不会把整数运算悄悄换成浮点。定点化在 ISR 里永远是更稳的选择。/* ISR 里只做取数和打时间戳计算推到任务 */ volatile uint32_t g_adc_ts; volatile uint16_t g_adc_raw; void ADC1_2_IRQHandler(void) { if (ADC1-SR ADC_SR_EOC) { g_adc_raw (uint16_t)ADC1-DR; /* 读 DR 同时清标志 */ g_adc_ts DWT_CYCCNT; /* 记录到达时刻 */ BaseType_t woken pdFALSE; vTaskNotifyGiveFromISR(g_ctrl_task, woken); portYIELD_FROM_ISR(woken); } }这段代码的总执行时间在 1 us 以内而且是常数时间不随数据变化——这就是可预期的意思。2.3 优先级与嵌套不是越高越好Cortex-M 的 NVIC 支持优先级分组抢占优先级决定能不能嵌套子优先级只决定同时挂起时的执行顺序。一个常见的误区是把主控制环设成最高抢占优先级觉得这样最稳。实际后果是它一旦开始执行所有通信中断都被推迟串口开始丢字节CAN 开始报错。正确的做法是按截止时间排优先级而不是按重要性排。截止时间越短、越硬的优先级越高中断源周期/频率截止时间建议抢占优先级电流环 ADC 完成100 us20 us0最高编码器定时器1 ms200 us1CAN 接收事件驱动1 ms2串口 DMA 空闲事件驱动10 ms3按键20 ms50 ms4最低另外要留意RTOS 会占用最低的几档优先级比如 FreeRTOS 的configMAX_SYSCALL_INTERRUPT_PRIORITY高于这个门限的中断不能调用内核 API。硬实时中断就放这一档以上纯硬件处理不碰内核需要通知任务的中断放门限以下。2.4 DMA 与中断的分工谁该干重活数据搬运这件事DMA 永远比中断便宜。以串口接收为例每字节一次中断115200 bps 下就是每秒 11520 次中断每次进出 ISR 的开销就要几个微秒累加起来占掉 CPU 好几个百分点还全是随机分布的高频抖动源。用 DMA 加空闲中断一帧数据只打断一次/* 串口 DMA 循环接收 空闲中断判定帧尾 */ void USART1_IRQHandler(void) { if (USART1-SR USART_SR_IDLE) { (void)USART1-DR; /* 读 DR 清 IDLE 标志 */ uint16_t remain DMA1_Channel5-CNDTR; uint16_t len RX_BUF_SIZE - remain; if (len 0) { q_push_frame(RX_BUF, len); /* 只做入队 */ } DMA1_Channel5-CNDTR RX_BUF_SIZE; /* 重置计数 */ } }CAN 的情况稍微不同。500 kbps 下标准帧最长约 130 位满负载也就每秒几千帧。单路 CAN 用接收中断完全够用而且中断方式能拿到精确的到达时间戳对周期分析更友好。只有在多路 CAN 或者总线负载超过 50% 的时候才考虑 DMA 环形接收代价是丢失每帧的到达时刻。要按需选不是 DMA 就一定好。3. 任务周期、优先级与它们的时间账中断解决了第一时间拿到数据任务解决的是用这些数据算出结果并输出。任务层的时间问题主要是周期准确性、优先级反转和调度开销。3.1 三类任务的划分方式我的习惯是把所有任务先分成三类分类本身就是一种降抖动手段周期性硬实时任务控制环、PWM 更新。这类任务绝对不能挂在内核 tick 上必须由硬件定时器中断或定时器触发的 DMA 唤醒任务本身用vTaskNotifyGiveFromISR或者直接是 ISR 加低优先级任务处理的形式。事件驱动任务按键、告警、协议响应。执行时间不固定但对相位不敏感可以接受几十毫秒的抖动。有意思的是这类任务恰恰是很多系统抖动的真正来源——处理一个按键事件如果顺手做了 EEPROM 写入那就是几十毫秒的总线阻塞。后台任务日志、LCD 刷新、OTA 分包、数据上报。优先级最低允许被无限推迟。但要给它们设一个饥饿保护否则在事件风暴时永远轮不到执行日志缓冲会满。3.2 优先级定法与 RMS 的实用判断实时系统有一个经典的可调度性判据速率单调调度RMS下的充分条件U Σ(Ci / Ti) ≤ n × (2^(1/n) - 1)其中 Ci 是任务最坏执行时间Ti 是周期n 是任务数。当 n 趋于无穷时这个界收敛到 ln2 ≈ 0.693。举个实际例子四个任务的参数如下任务 AC 1 msT 10 ms任务 BC 2 msT 20 ms任务 CC 3 msT 100 ms任务 DC 10 msT 1000 ms计算利用率U 0.1 0.1 0.03 0.01 0.24n 4 时的 RMS 界限是 4 × (2^0.25 - 1) 4 × 0.1892 0.757。0.24 远低于界限说明只要按周期升序分配优先级A 最高D 最低系统就是可调度的。这个公式的价值不在于精确而在于它逼你去测量每个任务的最坏执行时间。我见过太多项目从头到尾没人测过C全靠感觉这个任务挺快的。而一旦你开始测往往能发现某个任务的 C 是最初估计值的五倍。优先级排定之后还有一个必须处理的细节临界区时长。关中断或者关调度的时间必须远小于最高优先级任务周期。我一般要求所有临界区小于最高优先级任务周期的 5%。100 us 的电流环临界区就要小于 5 us。像 FreeRTOS 的taskENTER_CRITICAL关的是配置的最大系统调用优先级如果你的临界区里有串口打印那 5 us 早就爆了。3.3 任务级抖动的五个常见来源我把这些年遇到的任务抖动来源列一下基本能覆盖八成情况。动态内存分配。malloc的执行时间不确定取决于堆的碎片状态和搜索策略最坏情况可能是均值的几十倍。硬实时路径上必须用静态分配或者固定大小内存池。优先级反转。低优先级任务持有互斥量高优先级任务等它释放。FreeRTOS 的互斥量支持优先级继承二值信号量不支持。用错类型就会产生无上限的等待。Flash 擦写引起的总线 stall。这是最隐蔽的一个。STM32 在擦写内部 Flash 时CPU 取指会被挂起代码从 Flash 执行的话会直接停顿几十到几百毫秒。如果控制环的代码也在同一片 Flash 上那就是灾难。解决办法是把实时 ISR 放到 RAM 里执行用链接脚本或者__attribute__((section(.RamFunc)))或者确保擦写只发生在系统空闲时。日志输出。printf重定向到阻塞式串口一帧几十字节就是几百微秒到几毫秒。改成 DMA 或者环形缓冲非阻塞发送抖动立刻下降一个数量级。Cache 未命中。M7 这类带 Cache 的核第一次执行某段代码会因为取指未命中而变慢。中断处理函数最好在初始化阶段人为跑一遍把代码预热进 Cache避免第一次真实中断时的额外延迟。3.4 tick 与时间片那些看起来无害的设置RTOS 的 tick 频率是个需要认真选的参数。1 kHz 是默认值看起来很合理但它意味着任何基于 tick 的延时都有 1 ms 的量化误差。提到 10 kHz误差降到 0.1 ms代价是 tick 中断每秒一万次CPU 开销上升。一个折中方案是 tickless 模式。FreeRTOS 的 tickless idle 在空闲时关掉 tick 中断等下一个任务到期前再唤醒。这能让低功耗和抖动同时改善但要注意tickless 状态下xTaskGetTickCount()的返回值是补算出来的不能当作高精度时间戳用。至于时间片轮转configUSE_TIME_SLICING我的建议是在控制系统里关掉它。时间片让同优先级任务轮流执行每次切换都引入随机延迟。把同优先级任务数量控制在 1 个或者干脆让它们优先级不同系统的时间行为会好预测得多。4. 一个温湿度闭环系统的完整时间安排抽象讲完拿个具体的东西落地。做一个基于 STM32 的环境温湿度监测控制系统SHT30 采集温湿度根据偏差驱动加热片和风扇OLED 显示串口上报按键设定目标值。4.1 需求与时间指标温度控制精度±0.3 摄氏度采样周期500 ms受 SHT30 转换时间限制控制输出更新100 msPWM 占空比按键响应 100 ms上报周期1000 ms系统必须能连续跑 30 天不出时间漂移这里的控制对象是热惯性系统时间常数通常在几十秒到几分钟。按经验采样周期取对象时间常数的 1/10 到 1/20 就够了。如果加热片的热时间常数是 20 s采样周期 1~2 s 完全够用我这里取 500 ms 是为了留够余量也方便观察阶跃响应。反过来说如果这是个电机电流环对象时间常数是毫秒级采样周期就必须上到几十微秒。采样周期不是越快越好快到超过对象带宽只会把噪声放大进控制量。4.2 中断层设计中断层只保留必要的东西TIM3 更新中断1 kHz作为整个系统的时基基准只做一件事给任务发通知I2C1 事件中断用于 SHT30 读取完成USART1 空闲中断 DMA接收上报命令EXTI 按键中断只记录时间戳不做任何处理TIM3 时基用一个 32 位计数器累计毫秒数比HAL_GetTick()更可控因为后者可能被其他模块修改重载值。volatile uint32_t g_ms_tick; void TIM3_IRQHandler(void) { if (TIM3-SR TIM_SR_UIF) { TIM3-SR ~TIM_SR_UIF; g_ms_tick; /* 100 ms 分频通知控制任务 */ if ((g_ms_tick % 100) 0) { BaseType_t woken pdFALSE; vTaskNotifyGiveFromISR(g_ctrl_task, woken); portYIELD_FROM_ISR(woken); } } }按键中断里只记时间戳判断和去抖都放到任务里volatile uint32_t g_key_ts; volatile uint8_t g_key_flag; void EXTI0_IRQHandler(void) { if (EXTI-PR EXTI_PR_PR0) { EXTI-PR EXTI_PR_PR0; g_key_ts g_ms_tick; g_key_flag 1; } }机械按键的抖动时间是 5~20 ms在中断里做HAL_Delay(20)去抖是最差的做法因为它会阻塞整个中断通道 20 ms。正确做法是中断只记录任务里等到 20 ms 后读一次电平确认。4.3 任务层设计与时间预算表四个任务优先级按周期升序排任务周期最坏执行时间截止时间利用率控制任务100 ms3 ms100 ms3.0%采集任务500 ms8 ms500 ms1.6%按键任务20 ms0.5 ms100 ms2.5%通信任务1000 ms15 ms1000 ms1.5%合计———8.6%总利用率 8.6%远低于 RMS 界限余量充足。这里要注意采集任务的 8 ms 最坏执行时间主要来自 I2C 读取 SHT30单次高重复性测量的转换时间是 15 ms用非阻塞方式发起测量后等待或者用时钟拉伸配合超时处理。如果用阻塞式HAL_I2C_Mem_Read加 100 ms 超时那么采集任务的 C 就是 100 ms利用率直接飙到 20%而且这个 100 ms 是不可预测的。通信任务的 15 ms 里包含了 CRC 校验和组包这部分用定长缓冲避免任何动态分配。4.4 关键代码骨架采集任务的写法重点是把等待做成不占 CPU 的形式static void task_sample(void *arg) { uint8_t raw[6]; for (;;) { ulTaskNotifyTake(pdTRUE, portMAX_DELAY); /* 触发一次高重复性测量 */ uint8_t cmd[2] {0x2C, 0x06}; HAL_I2C_Master_Transmit(hi2c1, 0x44 1, cmd, 2, 10); /* 转换需要 15 ms用 delay 让出 CPU */ vTaskDelay(pdMS_TO_TICKS(20)); if (HAL_I2C_Master_Receive(hi2c1, 0x44 1, raw, 6, 10) HAL_OK) { if (crc8(raw, 2) raw[2] crc8(raw 3, 2) raw[5]) { float t -45.0f 175.0f * ((raw[0] 8) | raw[1]) / 65535.0f; xQueueOverwrite(g_temp_q, t); } else { g_crc_err_cnt; } } else { g_i2c_err_cnt; } } }两个错误计数器是我强烈建议加的。温湿度传感器在潮湿环境下会偶发 CRC 错误不做统计的话你根本不知道数据质量在恶化。控制任务用增量式 PID输出限幅加抗积分饱和static void task_ctrl(void *arg) { const float Kp 6.0f, Ki 0.15f, Kd 0.8f; float target 25.0f, integral 0.0f, prev_err 0.0f; for (;;) { ulTaskNotifyTake(pdTRUE, portMAX_DELAY); float t; if (xQueuePeek(g_temp_q, t, 0) ! pdTRUE) { continue; } float err target - t; integral err; /* 抗饱和输出限幅后回滚积分 */ if (integral 50.0f) integral 50.0f; if (integral -50.0f) integral -50.0f; float out Kp * err Ki * integral Kd * (err - prev_err); prev_err err; if (out 0.0f) out 0.0f; if (out 100.0f) out 100.0f; __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, (uint32_t)(out * 10)); } }这里用 100 ms 的固定周期因为 PWM 更新周期是它决定的。整个控制环的时间精度取决于 TIM3 时基的精度而 TIM3 挂在晶振上抖动是纳秒级的——这就是把时间交给硬件的价值。5. 抖动测量怎么把感觉变成数据不测就没法优化。抖动测量其实不需要很贵的设备两种方法就够用。5.1 GPIO 翻转法最简单的方法在任务或中断开始处把一个空闲 GPIO 拉高结束处拉低用示波器看这个方波。周期反映调度周期脉冲宽反映执行时间。/* 在控制任务开头 */ GPIOC-BSRR (1U 13); /* 置高 PC13 */ /* ... 控制计算 ... */ GPIOC-BSRR (1U 29); /* 复位 PC13写高 16 位 */注意 BSRR 的用法低 16 位写 1 是置位高 16 位写 1 是复位。这是原子操作不需要关中断。用这个方法能直接看到几件事周期是否稳定、有没有周期性的额外脉冲说明有高优先级任务在抢占、最坏脉冲宽度是多少。我一般会把示波器设成余辉模式跑十分钟脉冲宽度的分布一目了然。5.2 用 DWT 周期计数器做软件内测没有示波器的时候用 Cortex-M 的 DWT 周期计数器也能做#define DEMCR (*(volatile uint32_t *)0xE000EDFC) #define DWT_CTRL (*(volatile uint32_t *)0xE0001000) #define DWT_CYCCNT (*(volatile uint32_t *)0xE0001004) static inline void dwt_init(void) { DEMCR | (1U 24); /* 使能 TRCENA */ DWT_CYCCNT 0; DWT_CTRL | (1U 0); /* 使能 CYCCNT */ } static inline uint32_t dwt_now(void) { return DWT_CYCCNT; }在周期起点和终点分别取值相减就得到本次周期消耗的 CPU 周期数。把这个值和周期标称值一起存进一个数组跑够几千次之后统一算最大值、最小值、标准差。这个小工具我几乎每个项目都会加成本几十字节代码收益极大。要注意 CYCCNT 是 32 位的168 MHz 下大约 25.6 s 溢出一次。做差值计算时用无符号减法就能正确处理溢出回绕。5.3 数据分析与判据拿到数据之后怎么判断是否合格我的三条经验判据第一看最坏值而不是平均值。平均抖动 2 us、最坏抖动 200 us 的系统问题一定出在那 200 us 上。实时系统的意义就在最坏情况。第二看是否与负载相关。如果抖动只在通信任务活跃时出现那问题在临界区或者优先级配置不在控制算法。第三看抖动是否有周期性。与 tick 周期一致的抖动来源是调度器与工频 50 Hz 一致的抖动来源是电源或信号耦合与某个任务的周期一致的抖动来源是那个任务里的临界区。6. 常见问题排查速查6.1 现象、原因、手段对照表现象可能原因排查手段控制输出有周期性毛刺ISR 内有分支导致执行时间不均GPIO 翻转看脉冲宽度分布参数怎么调都不稳采样周期抖动大相位不准DWT 测周期标准差检查是否用 tick 定时通信偶发丢帧中断被高优先级长时间阻塞统计最高优先级 ISR 最坏耗时系统跑几小时后变慢堆碎片或内存泄漏定期打印xPortGetFreeHeapSizeFlash 写入时控制失控取指 stall 或临界区过长把 ISR 搬到 RAM或者写入只在空闲做上电正常加负载后异常优先级反转检查互斥量类型和持有时间输出低频振荡控制周期与对象时间常数不匹配检查采样周期是否慢于系统带宽6.2 我踩过的几个坑坑一在中断里调用HAL_Delay。早期做按键去抖直接在 EXTI 里写HAL_Delay(20)。结果串口开始丢数据因为中断通道被堵死了 20 ms。改成记录时间戳、任务里判断问题立刻消失。坑二把日志写入放在了控制任务的临界区里。用内部 Flash 模拟 EEPROM 存参数写入时关中断 40 ms电流环直接乱掉。后来改成写入前先把 ISR 复制到 RAM 运行再缩短单次擦写粒度。坑三误以为vTaskDelay(1)就是 1 ms。实际在 1 kHz tick 下调用时刻在 tick 相位后 0.1 ms实际延时就是 1.9 ms。做数据上报的节拍时这个偏差累积起来一分钟就差了几十毫秒。后来改成用硬件定时器计数判断误差降到微秒级。坑四优先级的重要性排序。曾经把屏幕刷新任务设得比通信任务还高觉得界面卡顿更影响体验。结果通信任务被挤得频繁超时。按截止时间重排之后一切正常。6.3 上线前的时间自检清单每个硬实时任务的触发源是不是硬件定时器而不是 RTOS 的 tick 延时最坏执行时间是否实测过是否留了 30% 以上的余量所有临界区的时长是否小于最高优先级任务周期的 5%是否存在动态内存分配位于实时路径上ISR 是否全部为常数时间没有分支依赖数据断点调试用的 GPIO 翻转代码是否已经移除或者用宏隔离是否有越界检测计数器用于记录周期超限次数时钟源是内部 RC 还是外部晶振长期漂移是否可接受内部 RC 振荡器这一条值得单独提。它省了两个电容和一个晶振但精度和温漂差得多室温下可能有 1% 到 3% 的偏差温度变化时更明显。做需要长时间累计计时的系统外部晶振省不得——一个月下来1% 的偏差就是七个多小时。我自己调这类问题的顺序很固定先用 DWT 把每个任务的周期分布打出来找到最坏值最大的那个任务再用 GPIO 翻转确认是执行时间长还是启动时刻晚如果是启动晚就往上看谁在它前面占了 CPU如果执行时间长就往里看临界区和等待。这个流程基本能覆盖我遇到的所有抖动问题比漫无目的地试参数有效得多。
返回列表