• 正文
  • 相关推荐
申请入驻 产业图谱

STM32中的ADC和DMA妙用:数据故障前的黑盒子

1小时前
36
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

前言

做电机控制或者传感器采集,除了看当前数值,有时还需要知道一次异常前后发生了什么。比如电机启动时电源短暂掉压,随后传感器报错;等程序打印当前电压,数值可能已经恢复正常。

日志只能告诉我们“刚才出过问题”。但是问题发生之前,电源是怎么掉的、信号有没有先抖一下、故障持续了多长时间,基本都看不到。

示波器有一个很实用的功能,叫预触发。平时先把波形留在缓存里,触发发生以后,再把触发前后的数据一起保存下来。

博主觉得,这个功能也很适合放进 STM32 的项目里。用 ADC、DMA 和一块 RAM,就可以设计一个小型信号黑匣子:平时持续保留历史,检测到异常以后,再把异常前后的那段波形一起导出来。本期就介绍一下它的实现思路和关键代码。

1历史缓存

这里按一路 ADC 来设计,目标采样率设成 20 kS/s,也就是每 50 μs 采一个点。

最后需要保存 2048 个点,其中 1536 个点放在触发之前,剩下 512 个点包含触发样本和后续数据。换算一下,就是大约 76.8 ms 的历史数据,再加 25.6 ms 的触发段。

2048 个 uint16_t 样本需要 4096 字节。DMA 接收数组再放 256 个样本,占 512 字节。两组数组一共占 4.5 KiB,芯片选型时还要给程序其他变量和栈留出空间。

这里的关键不是数组有多大,而是它要一直滚动。数组写到末尾以后,重新回到开头,把最早的数据覆盖掉。这样无论设备运行多久,RAM 里留下的始终是“最近这一段”。

2采样配置

要把数据还原成波形,采样间隔就要固定。如果在主循环里一边做其他任务,一边启动 ADC 采样,间隔容易跟着任务耗时变化。这里让定时器负责节拍,ADC 接到外部触发以后转换,DMA 自动把结果搬进数组,数据流就是下面这样:

定时器触发 → ADC 转换 → DMA 写入数组                         ↓                 半满 / 全满回调                         ↓                   保存和检查样本

在 CubeMX 里,ADC 使用外部触发,DMA 设为循环模式,打开半传输和传输完成中断。定时器选择哪个触发源,要看具体 STM32 型号的 ADC 触发映射。

测电源电压时,前端需要按 ADC 的允许输入范围做好分压,采样时间也要结合分压电阻和输入阻抗来配置。

下面是两个回调的用法。DMA 收满前半段时处理 adc_dma[0],收满后半段时处理 adc_dma[128]:

staticuint16_t adc_dma[256];voidHAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc){    if (hadc == &hadc1)    {        capture_feed(&adc_dma[0], 128U);    }}
voidHAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc){    if (hadc == &hadc1)    {        capture_feed(&adc_dma[128], 128U);    }}

20 kS/s 下,128 个点就是 6.4 ms。也就是说,前一个半区交给 CPU 以后,要在 DMA 再次写到它之前处理完。capture_feed() 里面只做样本搬运和触发判断,不在中断里打印串口,也不做复杂计算。

3触发捕获

假设我要抓的是电压突然下降。ADC 原始值从阈值上方掉到阈值下方时,就认为事件发生:

if (previous >= CAPTURE_LEVEL && x < CAPTURE_LEVEL){    triggered = 1U;    remaining = CAPTURE_POST - 1U;}

previous 是前一个样本,x 是当前样本。这样判断的是一次“向下穿过阈值”,不会因为设备启动时电压本来就低,就马上触发。

平时还没触发时,样本一边进来,一边写入环形缓冲区。等历史数据攒够 1536 个点,才允许触发。触发的这个样本算作后段的第一个点,后面再继续收 511 个点,刚好凑够 512 个后段样本。

这里有一个容易忽略的地方:CPU 是收齐半块数据才开始检查的,发现异常会比信号变化晚一些。但是只要 DMA 交给 CPU 的数据还没有被覆盖,我们仍然可以在这一批样本里找到触发点。它定位到的是采样点,两个采样点之间更短的变化仍可能漏掉。

捕获完成以后,历史数组就不再改写。主循环再停止 ADC 触发和 DMA,把数据整理出来。这样做比在中断里立刻停外设更容易控制,也不会把串口发送塞进采样中断。

4数据导出

环形数组存储时是绕圈的,不能从 history[0] 直接开始画图。窗口完成后,wr 指向下一次要写入的位置,也就是当前窗口里最旧的样本:

uint16_tcapture_read(uint32_t i){  return history[(wr + i) % CAPTURE_N];}

导出数据时,可以把触发样本设为 t = 0。当前参数下,前面第一个样本对应 -76800 μs,触发点对应 0,最后一个样本对应 +25550 μs:

for (uint32_t i = 0U; i < CAPTURE_N; ++i){    int32_t t_us = ((int32_t)i - (int32_t)CAPTURE_PRE) * 50;    uint16_t raw = capture_read(i);    /* 输出 t_us 和 raw,具体接口按工程使用 UART 或 USB。 */}

以前排查问题,程序通常只在出错以后打印一行日志。加上这个小黑匣子以后,设备可以把出错前的一段过程也保留下来。

它不一定要用来抓电压。电机电流或者传感器突变,也可以按同样的思路记录。对于不容易反复出现的问题,提前给它留一段历史,比出错以后再打开串口要方便得多。以后再给设备加告警功能时,大伙也可以顺手考虑一下:除了一个故障码,要不要把它前后的那段波形一起留下来。

相关推荐

嵌入式开发兴趣爱好者,热衷于电子信息技术,嵌入式开发,物联网技术等领域开发。