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 软件后,可参照以下步骤完成基础配置,确保软件能正确识别我们通过串口输出的数据,并绘制成波形:
- 选择正确的 COM 端口:在 SerialPlot 主界面,先下拉选择与板子连接的那个串口设备(比如 COM3 或 COM4 等)。
- 配置 Port 设置:将波特率(常见 115200)、数据位(8)、停止位(1)以及奇偶校验(None)等与我们在代码里配置的 USART 参数保持一致。
- 切换到“数据格式 (Data Format)”选项卡:
步骤 (4)~(6) 的配置需要和我们的代码输出相匹配。
- 数据格式设置:选择 ASCII(因为我们的代码通过 printf("%f...\n") 输出文本数据)。
- 通道数量 (Channels) 设置:目前配置为 1,其余的通道我们后续再加(因为我们现在只输出单一通道的数据)。
- 数据分隔符 (Delimiter) 选择:设为 “comma”,与我们代码里用逗号输出时保持一致(如果只是单通道输出,也可使用默认换行分隔,但为兼容后面多通道最好统一用逗号)。
- 切换到 “Plot” 选项卡:在这里可以对绘制参数进行个性化调整,让图像更易于读取。
- 通道名称配置:给当前单通道起个简明易懂的名字,例如 “rawDist”。调整线条颜色或其他可视化选项,不同颜色能让你在后续多通道时更容易区分。
- 最后,点击 “Open” 打开COM

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

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 上:
- 原始距离 rawDist
- 卡尔曼滤波 kalmanDist
- 均值滤波 maDist
- 指数平滑 esDist
然后你会看到:
• rawDist 曲线上下跳动最欢快,偶尔还跑出离群值;
• maDist 明显平滑了一些,但在突然改变距离时会慢一拍;
• esDist 也能提供平滑效果,调 α 大小还可以自行微调它对新数据的敏感程;
• kalmanDist 在参数合适时,多数情况下兼具抑制噪声和快速响应的效果,但需要在 Q/R 之间把关系调和好,否则效果可能“翻车”。
波形输出:

局部对比:

6 结语:滤波要巧,内核更要“给力”
APM32F402 采用了 Arm® Cortex®-M4F 内核,最高主频可达 120MHz,并且内置 FPU(Floating Point Unit)和 DSP 指令集,这让它在需要频繁浮点运算或数字信号处理(如滤波、傅里叶变换、控制算法)时比 Cortex®-M3、M0+ 更胜一筹。换句话说,在这颗“内芯”里跑各种滤波算法,可谓是事半功倍,既能提高实时性,也能减少软件浮点的额外开销。
• 如果只是小型电子制作,对精度和实时响应要求不算苛刻,均值滤波或指数平滑滤波就能应付绝大多数噪声场景;
• 如果项目场合复杂,既要实时跟踪又怕离群值捣乱,并且对系统运动或噪声分布有一定了解,那卡尔曼滤波当仁不让;
• 最终选择何种滤波,还是要结合具体需求、资源限制以及个人调参习惯。在嵌入式开发的世界里,“合用”往往胜过“一味追求顶配”。
以上便是本次分享的全部内容(这里是
附件:APM32F402_403_SDK_V1.0.1_HC_SR04_Filter.zip代码)。希望能给你一点启发,让超声数据“乖乖听话”,也让你在面对跳变值时不再焦头烂额。你觉得那个滤波方式最好呢?欢迎在评论区留下你的观点。