打印
[APM32F4]

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

[复制链接]
50|1
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主

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 函数示例

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

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) 的配置需要和我们的代码输出相匹配。

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

PixPin_2025-06-28_10-32-32.png

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

PixPin_2025-06-28_13-52-04.gif

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

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

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

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

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

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

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

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

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

#define MA_WINDOW_SIZE 5

typedef struct
{
    float buffer[MA_WINDOW_SIZE];
    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[i] = 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[filter->index];
    }
    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[filter->index] = 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) 表示平滑系数。α 大——反应积极,α 小——稳重平滑。 • 同样也有滞后,但只需存上一次滤波值就行,代码很精简。

简易示例:

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),是个“调参玄学”,一不留神数据就抖成筛子或者变得超级迟缓。

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

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 之间把关系调和好,否则效果可能“翻车”。

波形输出:

PixPin_2025-06-28_13-56-00.gif

局部对比:

PixPin_2025-06-28_13-56-41.png

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

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

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

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

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

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

使用特权

评论回复
沙发
kai迪皮|  楼主 | 2025-6-28 14:31 | 只看该作者
本帖最后由 kai迪皮 于 2025-6-28 14:40 编辑

#申请原创# @21小跑堂  

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

34

主题

227

帖子

11

粉丝