ARTICLE DETAIL

资讯详情

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

STM32H743 ADC DMA低CPU占用数据采集:定时器TRGO触发与Cache一致性详解

STM32H743 ADC DMA低CPU占用数据采集:定时器TRGO触发与Cache一致性详解 如果你现在还在用“定时器中断里读 ADC、算完再出去”的老套路我建议你把这篇看完。我上个月把一个 20kHz、4 通道同步采集的程序从“定时器中断 轮询读寄存器”改成 STM32H743 的定时器 TRGO 触发 DMA 循环搬运之后CPU 占用率直接从肉眼可见的卡顿降到了 3% 以内而且波形比之前干净不少。很多人拿到 H743 第一反应是“这核 480MHz 够快”但 ADC 这种高频数据流场景拼的不是 CPU 算得有多快而是数据能不能不经过 CPU 之手自动、连续、不失位地流入内存。这篇文章就把定时器触发、DMA 搬运和低 CPU 占用这条链路讲透适合正在做数据采集、电机控制、电源监测或者被 ADC 频繁中断搞到头大的开发者参考。1. 为什么“定时器中断里读 ADC”这条路会越走越窄1.1 你以为的“只是进个中断”很多入门教程教的是开一个定时器中断在中断回调里调用 HAL_ADC_Start、然后等转换完成再读结果。这个流程在单通道、几十赫兹的场景下完全没问题但一旦把频率拉到 kHz 级别问题就来了。以我做的 4 通道、20kHz 规则组扫描为例等效来看每 50 微秒就要触发一次转换序列。4 个通道串行转换每个通道即便按最短配置也要约 0.4 微秒一算四通道就是 1.6 微秒这还只是 ADC 转换本身。中断里还要做数据搬运、通道判断、标度换算甚至可能做一次简易滤波。整段中断处理随便写写就是 8 到 10 微秒20kHz 乘下来 CPU 占用就是 20% 上下。如果中间还跑着屏幕刷新、通信协议栈或者你打算把波形数据再塞进 FFT那基本没有余量了。再说另一个隐患规则组转换是硬件自动一轮一轮跑的如果你在中断里读取结果寄存器的时机和下一轮转换重叠读到的可能是新数据也可能是旧数据表现就是波形上有随机毛刺。你在时域看不出规律做频域分析时就会发现底噪高得离谱。1.2 固定采样间隔对数据质量的隐性作用软件触发还有个很容易被忽略的问题抖动。即使定时器中断优先级拉满中断响应也有延迟如果系统里同时跑着 EtherCAT、USB 这类对时序敏感的外设中断响应时间会飘。ADC 采样点之间的间隔一旦不稳定时域波形看不出什么但做频谱分析或者计算两路信号相位差时结果会很难看。比如你要用 H743 同时采集两路正弦波并计算相位差采样周期抖动会直接变成相位噪声。硬件定时器触发完全没有这个问题。定时器的输出比较或更新事件是纯硬件逻辑触发到 ADC 开始转换的延迟是固定的、确定的。数据流就变成了“定时器精准敲钟、ADC 按时执行、DMA 默默把结果搬走”CPU 从头到尾不参与每一次采样只在数据积攒成块之后做一次批量处理。这才是低 CPU 占用数据流的本质。1.3 链路全貌三个硬件各司其职这套方案的完整链路是定时器产生触发事件TRGO→ ADC 规则组收到外部触发开始转换 → 每转换完一个通道DMA 自动把结果寄存器里的值搬运到内存数组 → DMA 工作在循环模式数据填满半缓冲或全缓冲后产生中断通知 CPU 来取。这里 CPU 的角色从“搬运工”变成了“工头”只在缓冲区快满时去收一趟货。后面我会把每一环的参数选择、配置逻辑、以及我实际踩过的坑全部拆开讲。2. 动手前先看硬件H743 的 ADC 时钟、参考源与 PCB 布局讲配置之前必须先讲硬件因为 H7 系列的 ADC 和 F1/F4 不一样直接抄老工程的参数很容易翻车。2.1 ADC 时钟树与采样时间怎么配合STM32H743 的 ADC 时钟不是直接来自 APB2而是来自独立的 ADC kernel clock典型最高到 36MHz 左右。CubeMX 里可以选择 PLL2、PLL3 分频来生成这个时钟。不少人在 F1 上习惯把 ADC 时钟拉到最高以为越快越好但在 H7 上不能这么想。采样时间的计算公式是 Ttotal (SMP 转换周期) / fADC 12 位分辨率下转换周期固定为 12.5 个 ADC 时钟周期。假设 fADC 36MHzSMP 配置为 16.5 个周期那么单通道总耗时就是 (16.5 12.5) / 36MHz ≈ 0.81 微秒 如果是 4 通道规则组一轮扫描大约 3.2 微秒对应最高触发频率约 312kHz。做 20kHz 采样绰绰有余。但要注意采样时间不只是用来计算吞吐量的它直接影响采样电容的充电时间。如果你的信号源内阻比较大比如传感器输出串了一个 10k 电阻SMP 太短会导致采样电容还没充满就被切走读到的电压偏低而且这个误差会随温度漂。我在实践中通常先看信号源阻抗再决定 SMP 用 16.5 还是 32.5而不是一味追求高吞吐。2.2 参考电压和电源噪声是“看不见的坑”H7 的 VREF 如果直接接 VDDA那么 VDDA 上的任何纹波都会变成采样误差。如果你只是做内部温度检测或者粗略的电压监测可以这么用但如果你想做 16 位级别的采集必须给 VREF 单独供电或者加高精度基准源。这里有个容易被忽略的地方ADC 的电源引脚和 MCU 的 IO 电源一定要做好去耦。我见过一个项目模拟输入波形干净但采集结果总有周期性的小幅波动查了半天才发现是板载 DCDC 的开关频率串进了 VDDA纹波大约 20mV反映到 5V 采样上就是几十个 LSB 的跳动。2.3 PCB 布局上规避时钟抖动与电源噪声的 3 个要点结合我做过的几版采集板下面这三点比换芯片、换基准源都管用模拟地和数字地单点连接。不要大面积铺一块铜把模拟区和数字区完全混在一起而是分区铺铜在 ADC 芯片或 MCU 的 AGND 引脚附近用 0 欧电阻或磁珠做单点汇接。这样数字逻辑跳变产生的地弹噪声不会直接灌进模拟前端。去耦电容必须靠近 MCU 电源引脚。VREF、VDDA 引脚旁边的 100nF 1uF 组合要尽量贴近引脚过孔的位置也有讲究不要先打孔再走一段长线否则等效电感会把高频噪声留在电源引脚上。时钟抖动往往不是晶振的问题而是电源噪声耦合进时钟树造成采样时刻的微抖。模拟输入走线远离 PWM 和电感的开关节点。电机驱动板上的 PWM 信号上升沿能通过寄生电容耦合到 ADC 输入走线解决办法是模拟输入线尽量短、加 RC 低通滤波且电容尽量靠近 MCU 引脚如果空间允许在模拟输入下方铺一层完整的地铜皮做屏蔽。说白了DMA 搬得再快、定时器触得再准源端信号脏了后续软件再怎么救都有限。硬件布局这件事值得在开工前多花半天。3. 定时器触发选型与触发时刻控制从 TRGO 到中心对齐采样3.1 选哪个定时器通用定时器更新事件是最省事的入口从 CubeMX 里看定时器触发 ADC 其实就是一个选项把定时器的 TRGO触发输出接到 ADC 的外部触发输入。对绝大多数等间隔采样需求用通用定时器 TIM2、TIM3、TIM4、TIM5 的 Update Event 就够了配置简单触发间隔完全由 PSC 和 ARR 决定。我一般选 TIM2因为它是 32 位定时器虽然我们用不到那么大的计数范围但它的时钟源在 APB1 上触发延迟稳定不容易被其他高级定时器抢占。这里要讲清楚一个容易混淆的概念CubeMX 里 ADC 的外部触发源列表里会有 Timer 2 Trigger Out event、Timer 8 Trigger Out 等选项实际上就是对应定时器的 TRGO。通用定时器的 TRGO 可以选择 Update Event、Enable、OCx 等多种信号。做等间隔采样时选 Update Event 最简单因为 ARR 溢出周期就是你想要的采样周期。3.2 什么时候必须用高级定时器的 TRGO2 和中心对齐触发如果你做的是电机控制、数字电源这类和 PWM 强耦合的应用采样时刻必须和 PWM 中心对齐。比如三相电流采样业界标准做法是在 PWM 中心点采一次、边沿附近采一次保证电流环所需的相电流信息在正确时刻被 ADC 捕获。这种场景下通用定时器的 Update Event 就不够用了你得用 TIM1 或 TIM8 这类高级定时器因为它们有 TRGO2可以选择“中心对齐模式下向上或向下计数到匹配点”来触发 ADC。CubeMX 的 ADC 触发源列表里通常能看到 Timer 8 TRGO2配合高级定时器的重复计数器和 OC 配置可以精确控制每个 PWM 周期内的采样时刻。我在一个电机驱动项目里就是用 TIM8 发互补 PWM同时用 TIM8_TRGO2 在中心对齐点触发 ADC 采样电流波形里的毛刺比原来中断里读 ADC 少了一大截这就是触发相位固定带来的好处。3.3 采样率计算示例以 20kHz 为目标反推参数假设系统时钟 480MHzAPB1 120MHz但定时器时钟在 APB1 分频不为 1 时会自动倍频到 240MHz。CubeMX 时钟树里可以直接看到 TIM2 的时钟频率我按下表配参数值说明TIM2 时钟240MHz来自 APB1 Timer ClockPSC239240MHz / (2391) 1MHzARR491MHz / (491) 20kHzTRGOUpdate Event每溢出一次触发一次 ADC计算公式就是 f_trigger f_timer / ((PSC 1) * (ARR 1))验证一下240MHz / (240 * 50) 20kHz正确。这个触发频率对应每 50 微秒启动一轮 4 通道扫描单轮扫描约 3.2 微秒占比很低剩余时间 ADC 和 DMA 都在空闲等待下一次触发。4. DMA 搬运链路数据如何从结果寄存器“流”进内存不丢帧4.1 DMA 请求、循环模式和 Continuous Requests 到底怎么回事这是整篇文章最容易踩坑的地方。在 CubeMX 中配置 ADC1 时你需要开启 DMA 请求然后在 DMA Settings 里把 Mode 选为 Circular。同时 ADC 配置页面里有一个 “Continuous Requests” 选项必须设为 Enable。这两个是配合着用的。连续请求的意思是ADC 每完成一次规则组转换就向 DMA 发起一次传输请求。DMA 循环模式的意思是DMA 搬完一整轮数据后自动把地址计数器回绕到初始地址继续接收下一轮。如果你把 Continuous Requests 关掉会出现一个很诡异的现象第一轮 DMA 传输完成后第二次触发转换的数据不再被搬运数组里始终是第一批数据。因为 DMA 搬完一轮后停住了之后的请求没有生效。CubeMX 默认配置下这两个开关容易让人摸不清实际很多网上案例里“ADC DMA 只工作一次”就是这个原因。4.2 数据宽度选 Half Word 还是 WordH7 的 ADC 数据寄存器ADC_DR是 32 位的但 12 位采样结果只占低 16 位高 16 位在常规配置下是 0。因此 DMA 搬运时有两种常见选择外设和内存宽度都选 Word目标数组用 uint32_t。最直接一个采样点占 4 字节缺点是内存占用大。外设和内存宽度都选 Half Word目标数组用 uint16_t。省内存但要注意 DMA 每次从 32 位寄存器里读低 16 位只要你对齐没问题结果是正确的。我推荐新手用 Word 方式原因后面会讲和 DMA 非对齐访问有关。无论选哪种外设宽度和内存宽度必须一致否则 DMA 传输会报错或者数据错位。4.3 缓冲区位置H7 有个 D2 域 RAM 和 Cache 的隐藏坑H743 的内存布局和老的 F 系列不一样还多了 DTCM 和 AXI SRAM 的区别。如果你用 STM32CubeIDE 新建工程默认链接脚本会把不少 RAM 区间分配在 DTCM0x20000000 起始这个区域是 CPU 私有总线直连的DMA1/DMA2 根本访问不到。所以 DMA 缓冲区绝对不能放在 DTCM。我身边好几个同事第一次在 H7 上配 DMA 都栽在这一步程序编译运行不报错但 ADC 数据数组永远是 0后来打开 .map 文件一看缓冲区的地址落在 0x20000000 开头的 DTCMDMA 压根没碰过它。正确的做法是把缓冲区放在 D2 域 SRAM比如__attribute__((section(.RAM_D2))) uint32_t adc_raw[ADC_BUF_LEN];这里.RAM_D2是链接脚本里的一个区域名如果工程里没有这个 section你需要在自己工程的链接脚本里手动加一段内存区域。另一种更省事的方案是用 AXI SRAM0x24000000比如__attribute__((section(.axisram))) uint32_t adc_raw[ADC_BUF_LEN];但 AXI SRAM 在开启了 D-Cache 之后会有缓存一致性问题我在后面第 8 章专门讲。4.4 半传输完成中断和传输完成中断怎么用才不打架DMA 循环模式下最经典的“无锁数据处理”套路是利用 DMA 的半传输中断和传输完成中断把缓冲区分成两个半区。假设缓冲区长度是 256DMA 搬到第 128 个数据时触发 Half Transfer 中断CPU 处理前半区搬到第 256 个数据时触发 Transfer Complete 中断CPU 处理后半区。此时 CPU 永远只处理 DMA 已经写满的那一半不会和正在写入的区段冲突。在 HAL 库里对应两个回调HAL_ADC_ConvHalfCpltCallback 对应半传输中断HAL_ADC_ConvCpltCallback 对应传输完成中断这两个回调里只做一件事把对应半区的数据指针和长度记到全局变量置一个标志位真正处理放到主循环里。不要在中断回调里做 FFT、做字符串格式化、甚至 printf那会把中断执行时间拉长破坏定时器触发和 DMA 的流水线节奏。5. 数据落地之后通道对齐、数值滤波与漂移排查5.1 通道错位数组顺序和规则序列顺序的对应关系DMA 会把 ADC 规则组按转换完成顺序依次写入缓冲区所以缓冲区里的数据顺序一定和你在 CubeMX 里配置的通道顺序一致。比如规则序列是 Ch5、Ch6、Ch7、Ch8那么adc_raw[0]是 Ch5adc_raw[1]是 Ch6以此类推。听起来简单但实际做批量处理时很容易出边界错位。假设缓冲区一半是 4 个通道 × 32 轮处理函数里每 4 个数为一组如果取模操作写错一位整个数据块从第 33 轮开始就会串位表现为波形周期性错相。我在调试时有个土办法把 Ch5 接到 3.3VCh6 接地。跑起来后看数组里是否每隔 4 个出现一个接近满量程的值、再隔 4 个出现一个接近 0 的值如果模式乱了就是取模或者 DMA 缓冲区切半的 index 算错了。5.2 三个实用的 C 语言滤波函数热词里有“c语言adc值滤波函数”这块我直接给代码。第一种是滑动平均适合压掉高频随机噪声#define MAF_LEN 16 typedef struct { uint16_t buf[MAF_LEN]; uint8_t idx; uint32_t sum; } maf_t; uint16_t maf_process(maf_t *f, uint16_t x) { f-sum - f-buf[f-idx]; f-buf[f-idx] x; f-sum x; f-idx (f-idx 1) % MAF_LEN; return (uint16_t)(f-sum / MAF_LEN); }第二种是去极值平均适合信号中偶尔有尖峰脉冲干扰的场合比如电机启动瞬间的电磁串扰。思路是取 N 个数排序后去掉最小和最大剩下取平均。uint16_t mid_avg(uint16_t *arr, uint8_t n) { uint8_t i, j; uint16_t tmp, sum 0; for (i 0; i n - 1; i) { for (j i 1; j n; j) { if (arr[j] arr[i]) { tmp arr[i]; arr[i] arr[j]; arr[j] tmp; } } } for (i 1; i n - 1; i) sum arr[i]; return sum / (n - 2); }第三种是一阶低通滤波适合需要平滑又不希望延迟太大的控制类应用static float lp_y 0.0f; float lowpass(float x, float alpha) { lp_y alpha * (x - lp_y); return lp_y; }注意滑动平均初始阶段 sum 里是 0输出会有斜坡可以先把 buf 数组预填充为首个采样值再开始算。滤波器不是越多越好每一种都会带来相位延迟在闭环控制里尤其要谨慎。5.3 ADC 数据漂移先找原因再谈软件校准ADC 数据漂移这个话题经常被单独拎出来问。我在 H743 上遇到过的漂移分几类第一类是启动后整体偏大或偏小通常是没有做校准。H7 的 ADC 带有 ADCAL 校准机制我在第 6 章的启动顺序里会专门强调。第二类是温度漂移模拟前端和基准源受温度影响典型表现为采集值在开机一段时间后缓慢变化。这个是硬件问题软件上只能用校准表修正不能根治。第三类是采样时间不足造成的动态误差输入信号变化越快采样电容充电误差越大。排查方法很简单改变 SMP 从 16.5 拉长到 64.5如果读数明显变化说明原来采样时间不够。第四类是 DMA 缓冲区被 Cache“冻结”CPU 读到的都是旧数据波形看起来像“数据粘住了”这也是 H7 独有的坑后面单独讲。6. CubeMX 配置和启动顺序一张表讲清每个参数为什么这么填6.1 定时器参数配置速查以我常用的 TIM2 20kHz 为例CubeMX 里的参数填法如下配置项值理由Clock SourceInternal Clock使用内部定时器时钟Prescaler (PSC)239把 240MHz 降到 1MHzCounter ModeUp向上计数溢出周期即触发周期Counter Period (ARR)491MHz / 50 20kHzauto-reload preloadEnable避免运行中更新 ARR 时产生中间状态Trigger Output (TRGO)Update Event每次溢出触发一次 ADC如果你用高级定时器做中心对齐触发Counter Mode 要选 Center-AlignedARR 决定 PWM 周期TRGO2 按实际需要选择对应比较事件这里不展开细讲但逻辑是一样的触发频率最终都由 ARR 和预分频决定。6.2 ADC 参数配置速查以 ADC1、4 通道规则组为例配置项值理由Continuous ConversionDisable只由定时器触发不自动连续循环Continuous RequestsEnable确保每轮转换后 DMA 持续搬运DMA ModeCircular缓冲回绕形成环形数据流Resolution12 bit大多数场景够用Sampling Time16.5 cycles 或 32.5 cycles根据信号源阻抗选External Trigger SourceTimer 2 Trigger Out event对应 TIM2 的 TRGOExternal Trigger EdgeRising Edge上升沿开始一轮规则组转换规则组 Channel 顺序按实际需要填入注意 Rank 的顺序就是 DMA 写入缓冲区的顺序。选完采样时间后可以算一下一轮转换总耗时确保小于触发周期的一半留出余量。6.3 初始化顺序先校准、再启动 DMA、最后启动定时器很多人的问题出在启动顺序上我推荐固定用这个顺序MX_DMA_Init(); MX_ADC1_Init(); MX_TIM2_Init(); // 1. 校准 ADCH7 必须要调用 HAL_ADCEx_Calibration_Start(hadc1); // 2. 先使能 ADC 和 DMA数据链路先立起来 HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_raw, ADC_BUF_LEN); // 3. 最后启动定时器开始产生触发 HAL_TIM_Base_Start(htim2);为什么必须先HAL_ADC_Start_DMA再启动定时器因为定时器一旦启动触发事件马上就来了。如果 DMA 接收还没使能第一轮转换的数据就没有出口轻则丢第一帧重则触发条件错乱。校准必须先做否则 ADC 内部增益和偏移没校正后面采到的数全是偏的。6.4 HAL_ADC_Start_DMA 的坑第二个参数是数据长度HAL 库函数HAL_ADC_Start_DMA的参数很多人理解错HAL_StatusTypeDef HAL_ADC_Start_DMA(ADC_HandleTypeDef *hadc, uint32_t *pData, uint32_t Length);这里的 Length 是采样点个数不是字节数。如果你配置 DMA 宽度为 Half Word而传入的adc_raw是 uint32_t 数组那么 Length256 表示搬 256 个半字也就是 128 个 uint32_t。新手容易在这里把缓冲区和实际搬运数量搞拧导致一半数据全是 0。7. 实测复盘三个坑的完整排查链路7.1 坑一启动后 adc_raw 数组全零这个现象我调了一天。程序编译通过定时器在跑ADC 也初始化成功但读取 DMA 缓冲区256 个数据全是 0。排查链路是这样的先不开定时器手动调用一次HAL_ADC_Start(hadc1)并轮询转换完成发现 ADC 本身能出数据说明 ADC 和通道配置没问题接着确认 DMA 使能用调试器看 DMA 的 NDTR 寄存器发现每次定时器触发后 NDTR 确实在递减说明 DMA 收到请求了但内存数组就是不动。最后打开.map文件发现adc_raw数组被链接到了 0x20000000 附近的地址正是 D2 域的 DMAC 访问不到的位置。解决办法就是我在 4.3 节写的给数组加__attribute__((section(.RAM_D2)))把缓冲区挪到 DMA 能访问的 SRAM。从那以后我再也没有踩过这个坑。7.2 坑二DMA 半中断和全中断处理同一段数据我的程序最初只在 Full Transfer 中断里处理整个缓冲区发现每隔一轮会出现一次“半个区块数据看起来不对劲”的情况。查了很久才明白DMA 是全双工的流水线CPU 在处理全缓冲区时DMA 已经开始往缓冲区前半区写入新数据了。CPU 读到的是新旧混合的数据。解决办法就是 4.4 节说的半缓冲切块。把缓冲区从逻辑上分成前后两个独立的 blockHalf Transfer 只管前块Full Transfer 只管后块。处理函数的 index 计算一定要基于块首地址偏移而不是每次都从adc_raw[0]开始。7.3 坑三采样率算出来是对的实测却只有一半我用逻辑分析仪测定时器引脚发现频率正确但把数据导出来做频谱分析发现采样率只有预期的一半。排查后发现问题出在 ADC 配置里的 Continuous Conversion 选项。我以为把 Continuous Conversion 设成 Enable 更流畅实际上开启后 ADC 会在定时器触发的基础上自动连续扫描规则组转换请求变得“过密”而 DMA 每轮只按半缓冲区上报一次导致我在处理时用的是“两轮拼一轮”的假数据。把 Continuous Conversion 关掉只保留 Continuous Requests Enable数据流立刻恢复正常。这里给后来者一个经验定时器触发模式下ADC 的自动连续转换通常要关掉DMA 的连续请求要打开这两个名字很像的开关千万别弄反。8. H7 专属的 Cache 一致性问题以及这套方案的适用边界8.1 开启 D-Cache 后 DMA 缓冲区为什么会被“冻结”H7 默认或很多例程会开启 D-Cache。DMA 写数据是直接写到内存的不经过 Cache而 CPU 读数据时优先从 Cache 读。如果 Cache 里的对应行还是旧值CPU 读到的就是脏数据表现为波形“卡住不动”或者全是旧值。处理方案有几个按优先级排序把 DMA 缓冲区放到非 Cacheable 区域比如通过 MPU 配置 AXI SRAM 为 Non-Cacheable或者利用 D2 域 SRAM 的默认配置。每次读之前显式做 Cache 失效操作SCB_InvalidateDCache_by_Addr((uint32_t *)adc_raw, sizeof(adc_raw));注意地址必须 32 字节对齐长度也建议按 32 字节对齐。在 CubeMX 里检查 MPU 配置如果启用了 D-Cache 并且把 AXI SRAM 设为 Write-Back建议给 DMA 缓冲区单独建一个 Non-Cacheable 的 MPU region。这个坑在 H7 上几乎是绕不开的不是“可能遇到”而是只要你开 Cache 就一定会遇到。8.2 什么时候这套方案并不划算定时器触发 DMA 并不是银弹。如果你只是每秒采一次温度用阻塞模式轮询反而最简单如果采样频率很低中断负载可以忽略不计那也没必要为了“低 CPU 占用”把工程复杂化。另外如果信号本身是事件驱动的比如边沿到来才需要采一次那定时器触发反而别扭因为它不管你有没有事件都在那按固定节奏跑。事件驱动场景老老实实用外部中断启动单次转换就行。8.3 我的总体体会这套方案最适合的形态是“持续、周期性、多通道、需要批量处理”的数据流比如振动监测、电流环采样、功率分析。H743 这颗芯片性能很强但它的强大主要体现在能让你把数据链路用硬件串起来而不是让你用 CPU 硬扛高频中断。我最后再分享一个实际经验调试这种低 CPU 占用数据流时别一上来就优化代码先用示波器或逻辑分析仪确认三个信号——定时器触发是否等间隔、ADC 是否每次触发都产生了 EOC、DMA 是否按预期在递增地址搬运。这三条链路如果都干净软件处理只是最后一步收尾链路里任何一环松动CPU 写得再聪明也救不回数据质量。把硬件链路理顺了你会发现自己那套采集程序突然变得又稳又省剩下的 CPU 余量足够再跑一套算法或者通信栈这种感觉和我第一次把中断改成 DMA 时的体验一模一样真该早点这么干。
返回列表