引言
我家客厅的电子钟右上角显示客厅温度。有一天温度变成了”25.10℃”。
小数不可能是 10。这说明我的显示代码有 bug,而且是个藏了很久、只在特定数值下才现形的 bug。排查过程不算复杂,但这个坑的根源——线段绘制函数含两端点——值得单独记一笔,因为大多数教程不会讲。
背景:为什么小数用”算盘”表示
硬件是 ESP8266 D1 Mini 驱动 4 片 MAX7219 点阵屏,横向拼接成 32×8。时间占左侧 25 列,右侧只剩 7 列给温度。温度是个值如 25.9 的浮点数,7 列宽塞不下”25.9”这几个字符,于是我用了一种类似算盘的编码:
- 小数部分 ≥5 时,在 x=25 画一个亮点(代表 5,相当于算盘的上珠)
- 小数低位 1~5 用一条竖线表示,长度就是数值(相当于下珠)
.6 = 高位点 + 1px 竖线,.9 = 高位点 + 4px 竖线。8px 高的屏幕,够用。
温度由 MQTT 下发,display lambda 每秒重绘。

现象:像 5+5=10
某天温度 25.9,屏幕上出现”一个亮点 + 连续 5 个点亮”的组合。按我的编码,应该是高位点(5) + 4px 竖线,屏幕上却多出来一个点,观感像 5+5=10。
定位:一行代码的嫌疑最大
打开电子钟的 display lambda,小数绘制就三行:
temp_decimal = (int)(id(living_room_temp)*10 + 0.5) % 10; // 取十分位 0~9
if(temp_decimal > 0)
{
if(temp_decimal > 5) {
it.line(25, 0, 25, 0); // 高位点 = 代表 5
}
it.line(31 - ((temp_decimal-1)%5+1), 0, 31, 0); // 低位竖线 ← 嫌疑在这行
}
根因:line() 含两端点
ESPHome 的 it.line(x1, y1, x2, y2) 画线段时两个端点都点亮,水平方向实际点亮 x2 - x1 + 1 个点。这是 Bresenham 线段的标准语义,Adafruit GFX 的 drawLine 也一样。
我的起点公式 31 - ((temp_decimal-1)%5+1) 算的是”想要的长度 L”,但 line 实际点亮 L+1 个点:
| 小数 d | 设计点数 | 实际点数 | 观感 |
|---|---|---|---|
| 1 | 1 | 2 | 不明显 |
| 4 | 4 | 5 | 和高位点拼成 10 ⚠️ |
| 9 | 4 (+高位5) | 5 (+高位5) | 像 5+5=10 |
d=9 时(比如 25.9℃),低位恰好凑满 5 个点,跟高位点拼在一起,才暴露出这个一直存在、但平时不显眼的 off-by-one。
修复:一行,但先算边界值
// ❌ 旧:31 - ((temp_decimal-1)%5+1) → 点亮 5 点,比设计多 1
// ✅ 新:31 - ((temp_decimal-1)%5) → 起点右移 1,点数正确
it.line(31 - ((temp_decimal-1)%5), 0, 31, 0);
动手前先把边界值手算一遍(1/5/6/9),确认不会修出回归:
| 小数 | 新起点 | line 范围 | 点数 | 合计(含高位点) |
|---|---|---|---|---|
| 1 | 31-0=31 | line(31..31) | 1 | 1 ✓ |
| 5 | 31-4=27 | line(27..31) | 5 | 5 ✓ |
| 6 | 31-0=31 | line(31..31) | 1 | 5+1=6 ✓ |
| 9 | 31-3=28 | line(28..31) | 4 | 5+4=9 ✓ |
新起点 31 - ((d-1)%5) 算的是”倒数第 L 个像素的索引”,配合含端点的 line,正好点亮 L 个点。想清楚端点语义之后,起点应该用像素索引来算,而不是用长度。
OTA 烧录的坑(顺带踩的)
docker exec esphome esphome upload /config/esp32d1.yaml
非交互 shell 下这条命令会挂,因为扫描到两个 OTA 设备要你选:
Found multiple options for uploading, please choose one:
[1] Over The Air (<device-ip>)
[2] Over The Air (MQTT IP lookup)
(number): EOFError: EOF when reading a line
-d <ip> 不识别,正确参数是 --device:
docker exec esphome esphome upload /config/esp32d1.yaml --device <device-ip>
编译约 10 秒通过,OTA 5.7 秒完成,设备下一秒刷新就显示修正后的温度。
可以带走的三条经验
- 线段函数含端点:line / drawLine 这类 API 的点数 = x2-x1+1,和 fillRect 收”宽高”的心智模型不一样。用长度当坐标差,就会多画一个点。
- 差 1 的 bug 用边界值表现形:盯着代码看不出 off-by-one,把 1/4/5/9 这类极端值代入公式手算一遍,立刻现形。修复后也不只看目标值,相邻值要一起验。
- 文档里的公式可能是错的:这个公式最初是按”line 只画 L 点”写的,实际实现含端点。排障时以代码行为准,不盲信文档注释。
如果你在点阵屏上遇到”数字多一个点、平时没事一到边界就露馅”的显示 bug,先查绘制函数是不是含端点,再手算一遍边界值。这次修的是 line(),你下次遇到的可能是另一个有同样语义的 API。