
1. 为什么压力采集项目里ADC加DMA是绕不开的组合做过压力变送器、液位检测或者气路监控的朋友应该都有体会传感器输出的模拟信号本身并不难读难的是持续、稳定、不丢点地把数据搬进内存。XGZP6847A 这颗压力传感器在工业现场和消费类设备里出镜率很高它输出的是标准模拟电压信号量程覆盖从几 kPa 到几百 kPa 的多个档位供电和输出都做了温度补偿接线简单但真正把它用好的关键不在传感器本身而在 MCU 这一侧的采样链路设计。我最早接触这类项目时用的是最朴素的方式主循环里调一次HAL_ADC_GetValue()读一个值处理一下再进下一次循环。小批量验证阶段完全没问题数据看着也挺准。但一旦系统里同时跑着串口通信、按键扫描、OLED 刷新采样节奏立刻就被打乱了。压力值在屏幕上跳得厉害用示波器一对比才发现采样点之间的间隔忽长忽短根本不是等间隔采样。这就是典型的软件触发单次采样的毛病——采样时刻取决于主循环什么时候轮到 ADC而不是取决于你真正想要的采样率。后来换成定时器触发的 ADC 连续转换问题缓解了一部分但新的麻烦又来了每次转换完成都要进中断读数据采样率一高CPU 就被中断吃满了。我实测过在 72MHz 的 STM32F103 上如果采样率做到 10kHz 并且每个点都进中断光是进出中断的开销就能占到 CPU 时间的百分之十几这还没算中断里做滤波和打包的开销。对于需要同时处理其他任务的系统来说这个代价太高了。ADC DMA 的组合就是来解决这个矛盾的。ADC 负责按你设定的节奏把模拟量转成数字量DMA 负责在每次转换完成后自动把数据从 ADC 数据寄存器搬到内存数组里整个过程不需要 CPU 插手。CPU 只在攒够一批数据之后被通知一次去做滤波、换算、上报。这样采样时刻由硬件定时器精确控制数据搬运由 DMA 硬件完成CPU 占用率可以压到极低采样率也能做得更高更稳。XGZP6847A 这类压力传感器的应用场景往往对采样连续性有实际要求。比如做气密性检测需要观察压力随时间的变化曲线采样点稀疏或者间隔不均曲线就会失真做液位监测需要长时间稳定采样再取平均来抑制噪声采样链路不稳定平均值就没有意义。所以这套 ADC DMA 的方案不是炫技而是这类项目里真正能落地的工程做法。下面我会从硬件接线、CubeMX 配置、代码实现、数据换算、踩坑排查几个方面把整个链路完整走一遍。代码基于 STM32F103 HAL 库 CubeMX其他 F0/F4/G0 系列思路完全一致只是个别寄存器和时钟配置有差异。如果你手上正好有 XGZP6847A或者任何输出模拟电压的传感器这套流程可以直接套用。2. XGZP6847A 的电气特性与采样链路设计取舍2.1 先搞清楚传感器输出的是什么XGZP6847A 是一颗带温度补偿的模拟输出压力传感器常见的有 5V 供电和 3.3V 供电两个版本输出形式一般是比例电压输出也就是输出电压和供电电压成比例关系。这一点非常关键因为它直接决定了你 ADC 的参考电压该怎么选。举个具体的例子。假设你拿到的是量程 0 到 100kPa 的版本供电 3.3V输出范围是 0.5V 到 3.0V 对应 0 到满量程。那么压力 P 和输出电压 Vout 的关系就是P (Vout - 0.5) / (3.0 - 0.5) * 100kPa如果你用 STM32 的 3.3V 作为 ADC 参考电压那么 ADC 读到的原始值和电压的关系是Vout raw / 4095 * 3.3V把两个式子合起来就能从 ADC 原始值直接算出压力值。这里有个细节要注意传感器的供电和 ADC 的参考电压最好是同一个电源或者至少是同源、同精度的。因为比例输出的传感器输出会跟着供电电压浮动如果 ADC 参考电压是独立的、更精准的基准那供电波动就会直接变成测量误差。反过来如果两者同源供电波动在比例关系里会被抵消掉这就是所谓的比例式测量。2.2 参考电压的选择不是随便定的STM32F103 的 ADC 参考电压引脚是 VREF在 100 脚以上的封装里可以外接基准在 64 脚及以下的封装里通常内部连到 VDDA。很多人图省事直接用 VDDA 当参考这在小批量、常温、对精度要求不高的场合没问题但如果你要做的项目对精度有要求就得认真考虑基准源。我踩过的一个坑用 USB 供电给板子VDDA 实测在 3.28V 到 3.35V 之间波动ADC 读同一个稳定电压源原始值能差出十几个 LSB。换算成压力就是零点附近来回跳。后来换成一颗 3.0V 的专用基准芯片给 VREF 供电同时传感器也用这个 3.0V 供电波动立刻小了一个数量级。所以选型上的建议是如果传感器是比例输出优先让传感器供电和 ADC 参考同源如果传感器是绝对输出内部有基准那 ADC 侧就要用高精度基准。XGZP6847A 属于前者所以同源方案更划算。2.3 采样率和滤波的权衡压力信号本身是慢变量人的呼吸、气泵的脉动、液面的晃动频率一般都在几十赫兹以内。按照奈奎斯特定理采样率做到信号最高频率的 2 倍以上就能还原但工程上我们通常做得更高原因有两个一是为了用过采样加平均来压制随机噪声二是为了在时域上拿到更细的曲线。我的经验值是采样率做到 1kHz 到 10kHz 之间然后每 100 到 1000 个点做一次平均或者中值滤波输出 10Hz 到 100Hz 的有效数据。这样既压住了噪声又不会引入太大的滞后。采样率再高对压力这种慢信号意义不大反而增加 DMA 缓冲区的压力采样率太低平均的样本数不够噪声压不下去。这里有个容易忽略的点ADC 的采样时间要匹配信号源阻抗。XGZP6847A 的输出阻抗不高但如果你在 ADC 输入前面加了 RC 低通滤波电阻就会抬高源阻抗。STM32F103 的 ADC 采样时间可配置为 1.5、7.5、13.5、28.5、41.5、55.5、71.5、239.5 个 ADC 时钟周期。源阻抗越大需要的采样时间越长否则采样电容充不满读数会偏低。我一般会在输入加一个 1k 电阻加 100nF 电容的低通截止频率约 1.6kHz采样时间设成 55.5 周期实测很稳。3. CubeMX 里 ADC 与 DMA 的配置细节3.1 时钟树和 ADC 时钟分频STM32F103 的 ADC 时钟来自 PCLK2 经过 ADC 预分频器。ADC 时钟有上限F103 是 14MHz。假设系统主频 72MHzPCLK2 也是 72MHz那 ADC 预分频至少要 6 分频得到 12MHz符合要求。这个分频在 CubeMX 的 Clock Configuration 页面里设置很多人会漏掉结果 ADC 时钟超频读数飘忽不定。配置路径是Clock Configuration 页面找到 ADC Prescaler选 /6。然后在 ADC 配置页面确认 ADC 时钟显示为 12MHz。这一步做完后面所有采样时间的计算才有意义。3.2 ADC 模式选择独立模式加连续转换加 DMA在 CubeMX 的 ADC1 配置里需要改几个关键项ModeIndependent因为我们只用 ADC1。Scan Conversion ModeDisable。如果只采一个通道不需要扫描如果要多通道轮询才开。Continuous Conversion ModeEnable。这是让 ADC 在一次触发后连续转换配合 DMA 循环模式就能实现不间断采样。DMA Continuous RequestsEnable。这个选项让 ADC 每次转换完成都发 DMA 请求而不是只发一次。External Trigger Conversion Source如果要用定时器精确控制采样率这里选 Timer 的 Trigger Out 事件如果只是自由跑选软件触发也行但采样率就不精确了。我推荐用定时器触发因为采样率精确可控。具体做法是开一个定时器比如 TIM2设置成更新事件触发 ADC。定时器频率就是采样率。比如要 10kHz 采样定时器就配成 10kHz 更新。3.3 DMA 配置循环模式加半满中断DMA 这边在 DMA Settings 里给 ADC1 添加一个 DMA 请求方向是 Peripheral to Memory模式选Circular数据宽度 Peripheral 和 Memory 都选 Half Word因为 ADC 是 12 位存在 16 位变量里。关键在中断配置。DMA 有两个中断传输完成中断TC和半传输中断HT。如果只开 TC那 CPU 每攒满整个缓冲区才被通知一次处理延迟大如果同时开 HT 和 TC那缓冲区攒到一半和攒满时各通知一次CPU 的处理节奏更均匀缓冲区也可以做得更小。这就是所谓的双缓冲思路的简化版。我的做法是缓冲区开 256 个点开 HT 和 TC 两个中断。这样每 128 个点 CPU 处理一次10kHz 采样下就是每 12.8ms 处理一次节奏很舒服。3.4 定时器触发的配置以 TIM2 为例要让它以 10kHz 触发 ADC定时器时钟 72MHz预分频设为 0自动重装载值设为 7199这样更新频率就是 72MHz / (01) / (71991) 10kHz。在 TIM2 的配置里把Trigger Event Selection设为 Update Event。在 ADC 的 External Trigger 里选 TIM2 Trigger Out event触发边沿选上升沿。这样每次定时器更新就发一个触发信号给 ADCADC 启动一次转换转换完 DMA 搬走数据循环往复。整个过程硬件闭环CPU 完全不参与。4. 代码实现从初始化到数据搬运4.1 全局变量与缓冲区定义先定义 DMA 缓冲区和相关标志。缓冲区大小取 256用uint16_t类型因为 ADC 是 12 位。#define ADC_BUF_SIZE 256 uint16_t adc_dma_buf[ADC_BUF_SIZE]; volatile uint8_t adc_half_ready 0; volatile uint8_t adc_full_ready 0;这里用volatile修饰标志位因为它们在中断里被修改在主循环里被读取编译器不能优化掉。4.2 启动 ADC 与 DMA在main()里初始化完外设之后启动链路HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_dma_buf, ADC_BUF_SIZE); HAL_TIM_Base_Start(htim2);注意顺序先启动 ADC 的 DMA再启动定时器。如果反过来定时器已经开始触发但 DMA 还没准备好前几个触发可能会丢。虽然实际影响不大但养成好习惯。HAL_ADC_Start_DMA的第二个参数要强制转成uint32_t *这是 HAL 库的接口约定虽然实际数据是 16 位的但传参类型是 32 位指针。这个转换不会出问题因为 DMA 配置里数据宽度是 Half Word硬件只搬 16 位。4.3 DMA 中断回调的处理HAL 库把 DMA 中断统一到了HAL_ADC_ConvHalfCpltCallback和HAL_ADC_ConvCpltCallback两个回调里。我们在这两个回调里只置标志不做实际处理把处理放到主循环避免中断里耗时太长。void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { adc_half_ready 1; } } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { adc_full_ready 1; } }这里判断Instance是个好习惯如果系统里有多个 ADC不会误触发。4.4 主循环里的数据处理主循环里轮询标志位哪个半区准备好了就处理哪个半区while (1) { if (adc_half_ready) { adc_half_ready 0; process_adc_data(adc_dma_buf[0], ADC_BUF_SIZE / 2); } if (adc_full_ready) { adc_full_ready 0; process_adc_data(adc_dma_buf[ADC_BUF_SIZE / 2], ADC_BUF_SIZE / 2); } }process_adc_data里做滤波和换算。这里有个细节处理前半区的时候DMA 正在写后半区两边不冲突这就是双缓冲的意义。如果只用一个缓冲区并且处理时间和采样时间有重叠就会读到正在被改写的数。4.5 滤波与压力换算滤波我用的是去极值平均先把一批数据排序去掉最大和最小的若干个剩下的求平均。这样能有效抑制脉冲干扰。实现上为了省事可以用简单的冒泡排序128 个点排序在 72MHz 上也就几百微秒完全能接受。uint32_t sum 0; uint16_t temp[ADC_BUF_SIZE / 2]; memcpy(temp, buf, len * sizeof(uint16_t)); // 冒泡排序 for (int i 0; i len - 1; i) { for (int j 0; j len - 1 - i; j) { if (temp[j] temp[j 1]) { uint16_t t temp[j]; temp[j] temp[j 1]; temp[j 1] t; } } } // 去掉前后各 10 个求平均 for (int i 10; i len - 10; i) { sum temp[i]; } uint16_t avg sum / (len - 20);然后换算成电压和压力float voltage avg / 4095.0f * 3.3f; float pressure (voltage - 0.5f) / (3.0f - 0.5f) * 100.0f;这里的 0.5V、3.0V、100kPa 要按你手上传感器的实际规格改。规格书里一般会给出零点输出和满量程输出照着填就行。5. 实测中遇到的几个典型问题和排查过程5.1 数据全是一样的值或者全是零第一次跑通的时候串口打印出来全是 0。排查思路是这样的先用调试器看adc_dma_buf数组的内容发现确实全是 0说明 DMA 根本没搬数据。然后检查 DMA 配置发现HAL_ADC_Start_DMA调用之后ADC 的 CR2 寄存器里 DMA 使能位没置起来。再往前查发现是 ADC 校准没做。ADC 校准是必须的HAL 库提供了HAL_ADCEx_Calibration_Start在HAL_ADC_Start_DMA之前调用。不校准的话ADC 的偏移误差会比较大严重时读数完全不对。加上校准之后数据立刻正常了。5.2 数据每隔一段就跳一下跑起来之后发现压力值大部分时候很稳但每隔一两秒会跳一下。用逻辑分析仪抓 DMA 中断发现跳变的时刻正好对应串口发送数据。原因是串口发送用了阻塞方式发送期间 CPU 被占住主循环处理 DMA 数据的时机被推迟导致某一次处理的数据批次和上一次之间有重叠或者遗漏。解决办法是把串口发送改成 DMA 方式或者至少改成非阻塞。我用的是串口 DMA 发送发送请求丢给 DMA 就不管了CPU 立刻回来处理 ADC 数据跳变消失。5.3 采样率上不去想把采样率从 10kHz 提到 50kHz结果发现数据开始丢。查下来是两个原因一是 ADC 采样时间设得太长55.5 周期在 12MHz 时钟下就是 4.6 微秒加上转换时间 12.5 周期约 1 微秒一次转换要 5.6 微秒50kHz 需要 20 微秒一次理论上够但加上 DMA 开销就紧张了。二是 DMA 中断太频繁50kHz 下每 128 个点中断一次就是每 2.56ms 一次中断本身不慢但主循环里处理 128 个点的排序加平均要花时间处理不过来。后来把采样时间降到 13.5 周期排序改成只取平均不做排序采样率提到 30kHz 稳定运行。再高就没必要了压力信号用不上。5.4 零点漂移放置一段时间后零点慢慢往上飘。这个多半是传感器自发热或者参考电压漂移。XGZP6847A 功耗不高但长时间工作还是会有温升。我的处理办法是在软件里做零点跟踪设备启动时采一批数据作为零点运行过程中如果压力值长时间在零点附近小幅波动就缓慢更新零点基准。这样温漂被慢慢补偿掉又不会把真实的小压力信号当成漂移抹掉。零点跟踪的更新速度要慢我一般设成每 10 秒更新一次每次只把零点往当前值挪一点点比如挪 1%。这样既跟得上温漂又不会误伤真实信号。6. 几个让这套方案更稳的工程习惯6.1 缓冲区大小要匹配处理耗时缓冲区不是越大越好。缓冲区大中断频率低但每次处理的数据多处理耗时长实时性差缓冲区小实时性好但中断频繁。我的经验是让处理一次的时间不超过采样一批数据的时间的三分之一。比如 10kHz 采样128 个点一批是 12.8ms那处理时间控制在 4ms 以内留足余量。用这个标准去反推缓冲区大小和滤波算法复杂度。6.2 关键变量加 volatile 和临界区保护adc_dma_buf被 DMA 硬件写被主循环读虽然双缓冲避免了同一时刻读写同一区域但标志位的读写还是要小心。标志位置位和清零如果可能被打断就要用临界区保护。简单做法是关中断再操作__disable_irq(); adc_half_ready 0; __enable_irq();对于单个字节的标志位在 Cortex-M3 上读写是原子的其实不加也行但养成习惯没坏处。6.3 用调试器看 DMA 计数器排查 DMA 问题时直接看 DMA 的 CNDTR 寄存器剩余传输数量非常有用。如果这个值在变化说明 DMA 在搬数据如果不变说明 DMA 没启动或者已经停了。配合 ADC 的 DR 寄存器看能快速定位是 ADC 没转换还是 DMA 没搬。6.4 电源和地的处理模拟部分和数字部分的电源、地要分开走最后单点汇合。ADC 输入走线尽量短远离时钟线和开关电源。如果板子上有电机或者继电器ADC 输入一定要加 RC 滤波必要时加 TVS 管做保护。这些是硬件层面的基本功软件滤波只能补救不能替代。7. 从这套链路延伸出去还能做什么这套 ADC DMA 的框架搭好之后扩展性其实很强。比如你要同时采多路压力或者温度只要在 ADC 里开扫描模式把多个通道排进规则组DMA 缓冲区按通道数成倍扩大处理的时候按通道拆分就行。再比如你要做压力数据的实时曲线可以把处理后的数据通过串口 DMA 发给上位机或者存到 SD 卡里采样链路本身不用动。我还用这套结构做过气密性检测仪思路是在 DMA 缓冲区里做滑动窗口检测压力上升沿和下降沿的时间差从而算出泄漏率。因为采样是等间隔的时间差的计算非常准比软件触发采样的方案可靠得多。如果你手上的项目对成本敏感用 STM32F030 或者 GD32 的对应型号也能跑这套逻辑CubeMX 配置几乎一样只是时钟树和 DMA 通道映射有差别。核心思想不变定时器定节奏ADC 做转换DMA 搬数据CPU 只做后处理。把这四者的关系理顺压力采集这类项目基本就不会再被采样稳定性和 CPU 占用问题卡住了。