跳转至

附录:MCU 代码风格

算法能跑起来只是第一步。真正的工程代码还要考虑初始化、溢出、单位、异常输入和可测试性。

每个滤波器都应该有状态结构体

不要把所有变量写成散乱的全局变量。更推荐:

#include <stdint.h>

typedef struct {
    float y;       /* 上一次滤波输出 */
    uint8_t init;  /* 是否已经用第一笔数据初始化 */
} Lowpass1st;

float lowpass1st_update(Lowpass1st *f, float x, float alpha)
{
    if (!f->init) {
        /* 第一次调用时直接采用输入值,避免从 0 慢慢爬升。 */
        f->y = x;
        f->init = 1;
        return x;
    }

    /* 一阶滞后更新:只修正当前误差的一部分。 */
    f->y += alpha * (x - f->y);
    return f->y;
}

这样一个项目里可以同时创建多个滤波器实例,互不干扰。

初始化很重要

很多滤波器第一次输出不能默认从 0 开始,否则会产生很长的启动过渡。通常第一次采样时直接让输出等于输入。

if (!init) {
    y = x;
    init = 1;
}

注意整数溢出

ADC 是整数,累加时很容易溢出。

例如 12 位 ADC 最大值 4095,累加 1000 次最大约 4095000,已经超过 16 位整数范围。

#include <stdint.h>

uint32_t sum = 0;
for (uint16_t i = 0; i < n; i++) sum += adc[i];

定点数思想

没有 FPU 的 MCU 上,可以把小数放大成整数。例如把电压单位从 V 改成 mV,或者把系数放大 1024 倍。

一阶滞后可以写成:

y = y + ((alpha_q * (x - y)) >> 10);

其中 alpha_q = alpha * 1024

定点实现要特别注意符号、移位和中间变量位宽。

单位要统一

滤波前后最好明确单位:

  • ADC 原始码值
  • mV
  • 摄氏度
  • Pa
  • g
  • deg/s

同一个滤波器内部不要混用单位。单位混乱会让阈值和参数失去意义。

保留原始数据

调试和写教材示例时,不要只保留滤波后的值。至少同时记录:

timestamp, raw, filtered

如果是报警逻辑,还应记录阈值、状态和消抖计数。记录方式可以是数组、日志文件、仿真表格或调试输出,不必把调试输出作为主线。

参数要集中管理

不要把 0.1、20、5.0 这样的数字散落在代码中。

#define TEMP_LPF_ALPHA      0.08f
#define KEY_DEBOUNCE_MS     20
#define BAT_LOW_ON_MV       3300
#define BAT_LOW_OFF_MV      3450

参数名本身就是文档。半年后回来看,也能知道每个数字的意义。