问题:newlib-nano 砍掉了 %f

STM32 工程为了减小固件体积,通常使用 newlib-nano 运行时库。newlib-nano 默认不支持 printf%f 格式符——调用 rt_snprintf(buf, sz, "%.3f", value) 会输出空字符串或者直接崩溃。

但 PipeMonitor 需要在 LCD 和 JSON 里显示浮点数:流量 12.345 L/min、压力 0.520 MPa、温度 23.1 ℃。

方案:定点格式化

思路很简单:把浮点数拆成整数部分和小数部分,分别用 %lu%0*lu 输出。

static void app_format_fixed(char *buffer, rt_size_t size,
                             float value, rt_uint8_t decimals)
{
    long scale = 1;
    for (rt_uint8_t i = 0; i < decimals; i++)
        scale *= 10;

    /* 放大、四舍五入 */
    long scaled = (long)(value * (float)scale +
                   ((value >= 0.0f) ? 0.5f : -0.5f));

    unsigned long magnitude = (scaled < 0) ? (unsigned long)(-scaled)
                                           : (unsigned long)scaled;
    unsigned long integer_part = magnitude / (unsigned long)scale;
    unsigned long fraction_part = magnitude % (unsigned long)scale;

    if (decimals == 0)
        rt_snprintf(buffer, size, "%s%lu",
                    (scaled < 0) ? "-" : "", integer_part);
    else
        rt_snprintf(buffer, size, "%s%lu.%0*lu",
                    (scaled < 0) ? "-" : "", integer_part,
                    decimals, fraction_part);
}

调用示例:

app_format_fixed(buf, sz, 12.345f, 3);  /* → "12.345" */
app_format_fixed(buf, sz, 0.520f, 3);   /* → "0.520" */
app_format_fixed(buf, sz, 23.1f, 1);    /* → "23.1" */
app_format_fixed(buf, sz, -5.67f, 2);   /* → "-5.67" */

四舍五入的细节

直接截断会丢失精度:12.3456 * 1000 = 12345.6,截断得 12345,显示 12.345,少了 0.0006

加 0.5 再截断就是四舍五入:

long scaled = (long)(value * (float)scale + 0.5f);

但要注意负数:-5.678 * 1000 + 0.5 = -5677.5,截断得 -5677,不对。所以负数要减 0.5:

long scaled = (long)(value * (float)scale +
               ((value >= 0.0f) ? 0.5f : -0.5f));

JSON 编码器里的变体

上行 JSON 编码器需要处理”无效值输出 null”的情况,所以封装了带 valid 参数的版本:

static int fmt_fixed3(char *buf, int buf_sz, rt_uint8_t valid, float value)
{
    if (!valid)
        return rt_snprintf(buf, buf_sz, "null");

    value += 0.0005f;  /* 四舍五入 */
    long whole = (long)value;
    long frac = (long)((value - (float)whole) * 1000.0f);

    return rt_snprintf(buf, buf_sz, "%ld.%03ld", whole, frac);
}

PT100 的特殊处理

PT100 原始值是 *10 ℃ 的有符号整数(如 231 表示 23.1℃),不需要浮点运算:

static int fmt_pt100(char *buf, int buf_sz, int16_t raw)
{
    if (raw == INT16_MIN)
        return rt_snprintf(buf, buf_sz, "null");

    int whole = raw / 10;
    int frac = raw % 10;
    return rt_snprintf(buf, buf_sz, "%s%d.%d",
                       (raw < 0) ? "-" : "", whole, frac);
}

直接用整数除法和取模,比浮点转换更快更小。

LCD 显示的对齐问题

LCD 上显示的数据行需要对齐:

FLOW      : 12.345 L/min
TOTAL     : 1234.500 L
VELOCITY  : 0.850 m/s
PXW_PRES  : 0.520 MPa

如果用 %f,不同值的小数位数可能不同(12.345 vs 0.85),导致列不对齐。

定点格式化可以精确控制小数位数:%.3f 对应 decimals=3,保证所有值都是 3 位小数,自然对齐。

单位换算的精度

流量计返回 m³/h,业务层需要 L/min

handle->flow_lpm = instant_flow_m3h * (1000.0f / 60.0f);

这个乘法因子 16.666... 是无理数,浮点精度有限。但对工业监测来说,0.001 的精度损失完全可以接受。

压力的原始值是 kPa * 1000,显示时除以 1000 得到 MPa

float pres_mpa = m->pressure / 1000.0f;

这些换算都在采集时一次性完成,显示和上行层直接用换算后的值。

性能对比

方法代码体积执行时间
rt_snprintf("%.3f")~2KB(如果启用)~50μs
app_format_fixed~200B~10μs
fmt_pt100(纯整数)~50B~2μs

在 2 秒采样周期里,这点时间差异可以忽略。但在高频场景(比如 10ms 刷新率)下,定点格式化的优势会很明显。

浮点数的 IEEE754 解析

流量计返回的 REAL4 是 IEEE754 32 位浮点,两个 16 位寄存器拼起来就是浮点的位模式:

static float Flowmeter_RegsToFloat(uint16_t reg0, uint16_t reg1)
{
    float value;
    uint32_t raw = Flowmeter_CombineU32(reg0, reg1);
    memcpy(&value, &raw, sizeof(value));  /* 位模式转换 */
    return value;
}

这里用 memcpy 而不是强制类型转换,因为 C 标准不允许直接把 uint32_t 转成 float(虽然大多数编译器支持,但 UB 还是能避则避)。

小结

嵌入式浮点处理的核心原则:

  • 避免 %f:newlib-nano 不支持,体积也大
  • 定点格式化:用整数运算拆分整数和小数部分
  • 精确控制小数位:保证 LCD 对齐和 JSON 一致性
  • 无效值用 null:JSON 里无效通道输出 null 而不是 0.000

后续阅读