问题: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