kai迪皮 发表于 2025-6-28 14:29

APM32F402 & HC-SR04超声测距经典操作:波形输出与滤波

# 1 背景:为什么要看“波形”而不是“数字”?

很多时候,搞单片机的小伙伴们会说:“我不就是想看个距离嘛?干嘛整那么复杂?”的确,HC-SR04 测距可以很轻松地用串口打印,随手用 printf("%f", dist) 就能搞定。然而,当我们认真追究测量稳定性时,光看数字列表经常会“眼花缭乱”,而且一旦出现个别极端测量值,我们也很难察觉到背后的规律。

因此,我们往往倾向于“把数据变成波形”。所谓“一图胜千言”,对连续采样的结果进行可视化,常常能“一眼”看出抖动程度、周期性趋势或离群点。

APM32F402 这颗微控制器具备丰富的外设资源和较高的主频,加上 HC-SR04 超声波模块,本身就能很好地完成中近距离测量任务。那接下来,就让我们先弄清楚如何把这些测距数据“画”出来吧。

# 2 波形输出:你只需要一个 SerialPlot

## 2.1 为什么选 SerialPlot?

• 免费又开源,安装便捷(https://bitbucket.org/hyOzd/serialplot/src/default/)。
• 能够实时读取串口并自带波形绘制功能,省去了对接 Matlab 或其他上位机软件的麻烦。
• 简单设置就能同时采集多通道数据,比如同时显示“原始测距”、“滤波后测距”等。

## 2.2 “三步走”操作流程

(1) 初始化串口:   在 APM32F402 上使用 USARTx (如 USART1),配置波特率、数据位、停止位等参数与 PC 端的 USB-UART 模块匹配。波特率常选用 115200。
(2) 发送数据:   主循环里,每隔 N 毫秒 (比如 20ms) 采集一次数据,用 printf() 。注意数据格式要简单统一,可用逗号或空格分隔多通道数值,然后以 \n 结尾;
(3) SerialPlot 收数:   打开 SerialPlot,选定对应的串口号和波特率后,就能愉快地看到数据曲线随时间跳动了。

## 2.3 Main 函数示例

下方给出一个“多通道”输出的示例,只要做过类似串口输出的同学都能了解这块逻辑。对比之前,你会发现多了对各种滤波结果的采集输出。等下一节我们再把滤波代码补充进来,这里先看大致用法。

```c
int main(void)
{
    USART_Config_T usartConfigStruct;
    float rawDist = 0.0f;// 原始测距值

    // (1) NVIC vector table and basic initialization
    NVIC_ConfigVectorTable(NVIC_VECT_TAB_FLASH, 0x0000);
    BOARD_LED_Config(LED3);
    BOARD_Delay_Config();

    /* (2) Configure USART */
    USART_ConfigStructInit(&usartConfigStruct);
    usartConfigStruct.baudRate      = 115200;
    usartConfigStruct.mode          = USART_MODE_TX_RX;
    usartConfigStruct.parity      = USART_PARITY_NONE;
    usartConfigStruct.stopBits      = USART_STOP_BIT_1;
    usartConfigStruct.wordLength    = USART_WORD_LEN_8B;
    usartConfigStruct.hardwareFlow= USART_HARDWARE_FLOW_NONE;
    BOARD_COM_Config(COM1, &usartConfigStruct);

    /* (3) Initialize HC-SR04 measurement module */
    TMR_HCSR04_Init();

    printf("APM32F402 & HC-SR04 Demo: Only rawDist output\r\n");

    while (1)
    {
      // (4) Get raw distance from HC-SR04
      rawDist = sonar_mm_tmr();

      // (5) Print only rawDist
      printf("%.2f\r\n", rawDist);

      // (6) Toggle LED3 to indicate activity and delay
      BOARD_LED_Toggle(LED3);
      BOARD_Delay_Ms(20);
    }
}
```

## 2.4 设置SerialPlot

在打开 SerialPlot 软件后,可参照以下步骤完成基础配置,确保软件能正确识别我们通过串口输出的数据,并绘制成波形:

1. 选择正确的 COM 端口:在 SerialPlot 主界面,先下拉选择与板子连接的那个串口设备(比如 COM3 或 COM4 等)。
2. 配置 Port 设置:将波特率(常见 115200)、数据位(8)、停止位(1)以及奇偶校验(None)等与我们在代码里配置的 USART 参数保持一致。
3. 切换到“数据格式 (Data Format)”选项卡:

步骤 (4)~(6) 的配置需要和我们的代码输出相匹配。

4. 数据格式设置:选择 ASCII(因为我们的代码通过 printf("%f...\n") 输出文本数据)。
5. 通道数量 (Channels) 设置:目前配置为 1,其余的通道我们后续再加(因为我们现在只输出单一通道的数据)。
6. 数据分隔符 (Delimiter) 选择:设为 “comma”,与我们代码里用逗号输出时保持一致(如果只是单通道输出,也可使用默认换行分隔,但为兼容后面多通道最好统一用逗号)。
7. 切换到 “Plot” 选项卡:在这里可以对绘制参数进行个性化调整,让图像更易于读取。
8. 通道名称配置:给当前单通道起个简明易懂的名字,例如 “rawDist”。调整线条颜色或其他可视化选项,不同颜色能让你在后续多通道时更容易区分。
9. 最后,点击 “Open” 打开COM

!(data/attachment/forum/202506/28/142100bboeb159199mur9e.png "PixPin_2025-06-28_10-32-32.png")

一旦与板子连通,并且板子上已烧录代码,SerialPlot 界面就会开始刷新波形啦!

!(data/attachment/forum/202506/28/142115iyg4g780gu1zbu77.gif "PixPin_2025-06-28_13-52-04.gif")

# 3 看波形时发现了离奇“跳变”:那它们从何而来?

在你喜滋滋打开 SerialPlot 观看血(bu)脉(tong)喷(qiang)张(li)的距离曲线时,可能会发现原以为会像中学数学课本那样“顺滑”的数据却经常出现±几毫米的抖动,有时甚至莫名地瞬间高或瞬间低。

**• 这是因为超声波在空气中的传播容易受到环境干扰,特别是气流变动、多重反射干扰
• 或者目标极近或极远,HC-SR04 本身难以准确捕捉回波
• 以及软件层面定时器捕获可能会有极端误差,总之挑战多多**

# 4 滤波的三“怪侠”:均值、指数平滑、卡尔曼

当你的系统需要更精确、更稳定的测量结果,就必须引入“滤波”来对抗这些噪声和跳变。紧随其后我们就来谈谈几种经典滤波方式——从最简单的滑动平均,到稍微聪明一点的指数平滑,再到优雅的卡尔曼滤波——它们各自有什么优缺点?

## 4.1 均值滤波:最朴实的“一锅大杂烩”

• 算法原理,也就是滑动平均(Moving Average):将最近 N 次测量值加起来除以 N。

• 优点:实现简单,短时间内的随机噪声会被有效抵消。• 缺点:突变信号时会出现延迟。窗口越大,延迟越明显;窗口过小又无法有效平滑。

简易示例代码(环形缓冲方式):

```c
#define MA_WINDOW_SIZE 5

typedef struct
{
    float buffer;
    uint32_t index;
    float sum;
    uint32_t count;
} MovingAverageFilter_t;

void MAFilter_Init(MovingAverageFilter_t* filter)
{
    filter->sum = 0.0f;
    filter->index = 0;
    filter->count = 0;
    // Initialize the buffer to 0
    for(uint32_t i = 0; i < MA_WINDOW_SIZE; i++)
    {
      filter->buffer = 0.0f;
    }
}

float MAFilter_Update(MovingAverageFilter_t* filter, float newSample)
{
    // Subtract the oldest sample from sum if buffer is full
    if(filter->count >= MA_WINDOW_SIZE)
    {
      filter->sum -= filter->buffer;
    }
    else
    {
      // If the buffer isn't full, just increase count
      filter->count++;
    }

    // Add new sample to sum
    filter->sum += newSample;

    // Put new sample into buffer
    filter->buffer = newSample;

    // Update index (circular)
    filter->index++;
    if(filter->index >= MA_WINDOW_SIZE)
    {
      filter->index = 0;
    }

    // Calculate the average
    float average = filter->sum / (float)(filter->count);
    return average;
}
```

## 4.2 指数平滑:给新数据一点“特殊照顾”

当我们想要占用更少的内存、并能灵活调节“新数据”和“旧数据”权重时,可以上“指数平滑 (Exponential Smoothing)”这把刀。它每次更新公式常写作:

filtered(k) = α × newSample + (1 - α) × filtered(k-1)

• α ∈ (0,1) 表示平滑系数。α 大——反应积极,α 小——稳重平滑。• 同样也有滞后,但只需存上一次滤波值就行,代码很精简。

简易示例:

```c
typedef struct
{
    float alpha;
    float prevFiltered;
    uint8_t initFlag;   
} ExpSmoothFilter_t;

void ExpSmoothFilter_Init(ExpSmoothFilter_t* filter, float alpha, float initialVal)
{
    filter->alpha = alpha;
    filter->prevFiltered = initialVal;
    filter->initFlag = 1;
}

float ExpSmoothFilter_Update(ExpSmoothFilter_t* filter, float newSample)
{
    if(!filter->initFlag)
    {
      // If not initialized properly, we do it on the fly
      filter->prevFiltered = newSample;
      filter->initFlag = 1;
      return newSample;
    }

    // filtered(k) = alpha * newSample + (1-alpha)*filtered(k-1)
    float currentFiltered = filter->alpha * newSample +
                            (1.0f - filter->alpha) * filter->prevFiltered;

    // Store the result for next iteration
    filter->prevFiltered = currentFiltered;

    // Return filtered output
    return currentFiltered;
}
```

## 4.3 卡尔曼滤波:让你的测距结果变得“华丽丽”

有些场景,只靠均值或指数平滑,还无法很好地兼顾平滑度与实时性,而卡尔曼滤波 (Kalman Filter) 正在此时闪亮登场——它在四轴飞行器、机器人定位、VR/AR 追踪等高精度领域大放异彩。

它是利用最优状态估计理论,在已知系统模型、噪声统计特性的前提下,能同时降低随机噪声干扰,又能对真实值变化保持快速跟踪。

• 优点:自适应性更强,对离群值有一定抑制效果。

• 缺点:需要适当的噪声模型(比如 Q, R),是个“调参玄学”,一不留神数据就抖成筛子或者变得超级迟缓。

示例代码(简易一维场景):

```c
void KalmanFilter_Init(KalmanFilter_t *kf, float initVal)
{
    /*
   * x = initVal
   * p = 10000.0f      (初始较大的不确定度)
   * Q = 5.0f            (过程噪声: 可适度调大/调小)
   * R = 50.0f         (测量噪声: 数值越大说明观测值不可靠)
   */
    kf->x = initVal;
    kf->p = 10000.0f;
    kf->Q = 5.0f;
    kf->R = 50.0f;
}

float KalmanFilter_Update(KalmanFilter_t *kf, float measurement)
{
    /* 1) 预测阶段:x' = x, p' = p + Q */
    float x_prime = kf->x;
    float p_prime = kf->p + kf->Q;

    /* 2) 更新阶段:
   *    K = p' / (p' + R)
   *    x = x' + K*(z - x')
   *    p = (1 - K)*p'
   */
    float K = p_prime / (p_prime + kf->R);
    kf->x   = x_prime + K * (measurement - x_prime);
    kf->p   = (1.0f - K) * p_prime;

    return kf->x;
}

float getFilteredDistance(float measurement)
{
    /* 首次调用时初始化卡尔曼滤波器 */
    if (!s_kfInitFlag)
    {
      KalmanFilter_Init(&s_filter, 0.0f);
      s_kfInitFlag = 1;
    }

    /* 使用传入的 measurement 完成卡尔曼滤波更新 */
    float filteredDist = KalmanFilter_Update(&s_filter, measurement);

    /* 返回滤波后的距离 */
    return filteredDist;
}
```

# 5 实战对比:四条波形,谁更平稳?

在前文的 main 函数循环里,只要我们分别调用均值滤波、指数平滑滤波与卡尔曼滤波,就能一口气输出四条通道数据到 SerialPlot 上:

1. 原始距离 rawDist
2. 卡尔曼滤波 kalmanDist
3. 均值滤波 maDist
4. 指数平滑 esDist

然后你会看到:

• rawDist 曲线上下跳动最欢快,偶尔还跑出离群值;

• maDist 明显平滑了一些,但在突然改变距离时会慢一拍;

• esDist 也能提供平滑效果,调 α 大小还可以自行微调它对新数据的敏感程;

• kalmanDist 在参数合适时,多数情况下兼具抑制噪声和快速响应的效果,但需要在 Q/R 之间把关系调和好,否则效果可能“翻车”。

波形输出:

!(data/attachment/forum/202506/28/142128bqfhhrr7yrgyshy7.gif "PixPin_2025-06-28_13-56-00.gif")

局部对比:

!(data/attachment/forum/202506/28/142137hek3kros67q54o4o.png "PixPin_2025-06-28_13-56-41.png")

# 6 结语:滤波要巧,内核更要“给力”

**APM32F402 采用了 Arm® Cortex®-M4F 内核,最高主频可达 120MHz,并且内置 FPU(Floating Point Unit)和 DSP 指令集,这让它在需要频繁浮点运算或数字信号处理(如滤波、傅里叶变换、控制算法)时比 Cortex®-M3、M0+ 更胜一筹。换句话说,在这颗“内芯”里跑各种滤波算法,可谓是事半功倍,既能提高实时性,也能减少软件浮点的额外开销。**

• 如果只是小型电子制作,对精度和实时响应要求不算苛刻,均值滤波或指数平滑滤波就能应付绝大多数噪声场景;

• 如果项目场合复杂,既要实时跟踪又怕离群值捣乱,并且对系统运动或噪声分布有一定了解,那卡尔曼滤波当仁不让;

• 最终选择何种滤波,还是要结合具体需求、资源限制以及个人调参习惯。在嵌入式开发的世界里,“合用”往往胜过“一味追求顶配”。

以上便是本次分享的全部内容(这里是[!(/source/plugin/zhanmishu_markdown/template/editor/images/upload.svg) 附件:APM32F402_403_SDK_V1.0.1_HC_SR04_Filter.zip](forum.php?mod=attachment&aid=2419192 "attachment")代码)。希望能给你一点启发,让超声数据“乖乖听话”,也让你在面对跳变值时不再焦头烂额。你觉得那个滤波方式最好呢?欢迎在评论区留下你的观点。

kai迪皮 发表于 2025-6-28 14:31

本帖最后由 kai迪皮 于 2025-6-28 14:40 编辑

#申请原创# @21小跑堂

转瞬回声 发表于 2025-6-30 20:32

楼主这动图实在太舒服了。
我们就使用最简单的中值滤波来平滑数据。

kai迪皮 发表于 2025-7-1 09:09

转瞬回声 发表于 2025-6-30 20:32
楼主这动图实在太舒服了。
我们就使用最简单的中值滤波来平滑数据。

{:lol:}感谢支持
页: [1]
查看完整版本: APM32F402 & HC-SR04超声测距经典操作:波形输出与滤波