被 Gemini 稳稳的接住的一天
开始
起因很简单,需要用测量一个物体的厚度并将数据传到单片机,自然而然的想到买一个数字百分表,简单检索资料发现网上有相关资料,于是自信满满买回来了,然后掉一天头发的故事
踩坑
考虑价格,直接淘宝买了一个50块钱的百分表,microusb接口便于接线
百分表接口很简单,除去microusb的VCC,GND,只有两根有效信号线,其中一根为CLK,一根为DATA,表本身主动输出数据,让单片机接受即可
按照网上检索的资料,都表述基本采用:
3V 供电
28bit 串行输出
前 24bit 是 BCD 数据
后面几位为单位和正负号
每 4bit 表示一个十进制数字
但实际如果按照这个来,只会一直陷入无穷无尽的自我怀疑
分析
电压
注意这个表的电压是 1.5V,输入 3.3V 会让屏幕全部亮起看不清,因此要使用电池(LR44)或者接 1.5V 电源使用
我搜索到的资料基本都是直连GPIO的,电平也是,但我的表1.5v,输出的电压对于用的单片机(RP2040)来说都是低电平,所以需要进行一个转换,我的做法是使用了两个NPN型的三极管,然后基极接百分表输出控制。
想法有了,那么直接开始验证
简单测试电路经过测试输出的可靠没有问题。只是实现不那么优雅。
需要注意的是,NPN 共射极接法会导致电平反相,所以程序里的有效边沿以 MCU GPIO 实际看到的波形为准
通信
由于不能放弃必须解决,于是便要自己开始分析通信协议然后解决
首先使用示波器看看这个表的输出
示波器分析.jpg
很好看不太懂,但大概能知道和网上表的输出其实差不多,然后手上没有逻辑分析仪,于是拿出手上的Pico
通过开源项目 Pico逻辑分析仪 ,本质就是RP2040,借助强力的 PIO 能力直接当逻辑分析用,效果很不错,只要刷入 uf2 固件,装上上位机直接简单看看输出,和示波器差不多。
0mm.png
17.69mm.png然后就是尝试分析逻辑和数据,先把图片和对应的高低电平数据整理出来,丢给各家AI
首先是被我抱以厚望的 ChatGPT,开启 Thinking,浪费无数 Token 和时间也没能解决问题,稳重,保守但解决不了问题
交给 DeepSeek,专家思考情况下,成功思考中断,思考过程表述:“好像没有更多办法了~”
至于豆包、智谱、Kimi我连尝试的想法都没有了

但问题没有解决,观察到这些AI似乎都陷入了执着于解决BCD码,觉得问题出现在边沿触发或者延迟,觉得抓到的数据是错的,但又都解决不了问题
实在抓狂,抱着试一试的心态交给 Gemini,并指出不要死磕BCD,看看能不能暴力穷举出结果
说大话,但是说的是真话
成功了!!!
所以最后用 Gemini 的思路轻易解决了问题,这波真给他装到了:)
结论
供参考,具体以实际情况修改,仅代表我的测试结果以及数据
这块百分表协议如下:
帧长度:24bit
位序:LSB first
采样边沿:GPIO 端 FALL 边沿
编码方式:24bit 反码二进制
帧间隔:约 100~120ms
然后再测试切换单位和符号,简单分析得到正确解码逻辑:
解码:
raw24 = 接收到的 24bit 原始数据
inv24 = (~raw24) & 0xFFFFFF
字段定义:
bit0~bit19 数值
bit20 符号位,1=负数,0=正数
bit21~bit22 暂未确认,保留
bit23 单位位,1=inch,0=mm
单位:
mm 模式:
value_mm = value_raw × 0.01
inch 模式:
value_inch = value_raw × 0.0005

最后再用.c/.h封装起来,方便后期使用,我的代码如下:
extern "C" {
typedef enum {
DIAL_INDICATOR_UNIT_MM = 0,
DIAL_INDICATOR_UNIT_INCH = 1
} dial_indicator_unit_t;
typedef struct {
uint32_t raw24;
uint32_t inv24;
uint32_t value_raw;
uint8_t negative;
uint8_t reserved_bits;
dial_indicator_unit_t unit;
int32_t mm_x100;
int32_t inch_x10000;
uint8_t valid;
} dial_indicator_value_t;
typedef struct {
volatile uint32_t raw;
volatile uint8_t bit_count;
volatile uint32_t last_edge_us;
uint32_t frame_gap_us;
volatile uint8_t frame_ready;
volatile uint32_t ready_raw;
volatile uint8_t ignore_until_gap;
uint8_t clock_gpio;
uint8_t data_gpio;
uint8_t last_clock_high;
} dial_indicator_decoder_t;
void dial_indicator_init(
dial_indicator_decoder_t *dec,
uint32_t frame_gap_us
);
void dial_indicator_init_gpio(
dial_indicator_decoder_t *dec,
uint8_t clock_gpio,
uint8_t data_gpio,
uint32_t frame_gap_us
);
int dial_indicator_feed_bit(
dial_indicator_decoder_t *dec,
uint32_t now_us,
int data_bit
);
int dial_indicator_poll_gpio(
dial_indicator_decoder_t *dec
);
void dial_indicator_start_gpio_irq(
dial_indicator_decoder_t *dec
);
void dial_indicator_stop_gpio_irq(
dial_indicator_decoder_t *dec
);
int dial_indicator_poll_read_gpio(
dial_indicator_decoder_t *dec,
dial_indicator_value_t *out
);
int dial_indicator_available(
const dial_indicator_decoder_t *dec
);
dial_indicator_value_t dial_indicator_read(
dial_indicator_decoder_t *dec
);
dial_indicator_value_t dial_indicator_decode_raw24(
uint32_t raw24
);
}
static dial_indicator_decoder_t *s_irq_decoder = 0;
static dial_indicator_value_t dial_indicator_invalid_value(void) {
dial_indicator_value_t value;
value.raw24 = 0;
value.inv24 = 0;
value.value_raw = 0;
value.negative = 0;
value.reserved_bits = 0;
value.unit = DIAL_INDICATOR_UNIT_MM;
value.mm_x100 = 0;
value.inch_x10000 = 0;
value.valid = 0;
return value;
}
static void dial_indicator_gpio_irq_callback(uint gpio, uint32_t events) {
dial_indicator_decoder_t *dec = s_irq_decoder;
if (dec == 0 || gpio != dec->clock_gpio ||
(events & GPIO_IRQ_EDGE_FALL) == 0) {
return;
}
(void)dial_indicator_feed_bit(
dec,
time_us_32(),
gpio_get(dec->data_gpio) ? 1 : 0
);
}
void dial_indicator_init(
dial_indicator_decoder_t *dec,
uint32_t frame_gap_us
) {
if (dec == 0) {
return;
}
dec->raw = 0;
dec->bit_count = 0;
dec->last_edge_us = 0;
dec->frame_gap_us = frame_gap_us ? frame_gap_us
: DIAL_INDICATOR_DEFAULT_FRAME_GAP_US;
dec->frame_ready = 0;
dec->ready_raw = 0;
dec->ignore_until_gap = 0;
dec->clock_gpio = DIAL_INDICATOR_INVALID_GPIO;
dec->data_gpio = DIAL_INDICATOR_INVALID_GPIO;
dec->last_clock_high = 1;
}
void dial_indicator_init_gpio(
dial_indicator_decoder_t *dec,
uint8_t clock_gpio,
uint8_t data_gpio,
uint32_t frame_gap_us
) {
dial_indicator_init(dec, frame_gap_us);
if (dec == 0) {
return;
}
dec->clock_gpio = clock_gpio;
dec->data_gpio = data_gpio;
gpio_init(clock_gpio);
gpio_set_dir(clock_gpio, GPIO_IN);
gpio_pull_up(clock_gpio);
gpio_init(data_gpio);
gpio_set_dir(data_gpio, GPIO_IN);
gpio_pull_up(data_gpio);
dec->last_clock_high = gpio_get(clock_gpio) ? 1 : 0;
}
int dial_indicator_feed_bit(
dial_indicator_decoder_t *dec,
uint32_t now_us,
int data_bit
) {
uint32_t gap = 0;
if (dec == 0) {
return 0;
}
if (dec->last_edge_us != 0) {
gap = now_us - dec->last_edge_us;
}
dec->last_edge_us = now_us;
if (gap > dec->frame_gap_us) {
dec->raw = 0;
dec->bit_count = 0;
dec->ignore_until_gap = 0;
}
if (dec->ignore_until_gap) {
return 0;
}
if (dec->bit_count < DIAL_INDICATOR_FRAME_BITS) {
if (data_bit) {
dec->raw |= (1UL << dec->bit_count);
}
dec->bit_count++;
if (dec->bit_count == DIAL_INDICATOR_FRAME_BITS) {
dec->ready_raw = dec->raw & 0x00FFFFFFUL;
dec->frame_ready = 1;
dec->ignore_until_gap = 1;
return 1;
}
} else {
dec->raw = 0;
dec->bit_count = 0;
dec->ignore_until_gap = 1;
}
return 0;
}
int dial_indicator_poll_gpio(
dial_indicator_decoder_t *dec
) {
uint8_t clock_high;
int frame_ready;
if (dec == 0 ||
dec->clock_gpio == DIAL_INDICATOR_INVALID_GPIO ||
dec->data_gpio == DIAL_INDICATOR_INVALID_GPIO) {
return 0;
}
clock_high = gpio_get(dec->clock_gpio) ? 1 : 0;
frame_ready = 0;
if (dec->last_clock_high && !clock_high) {
int data_bit = gpio_get(dec->data_gpio) ? 1 : 0;
frame_ready = dial_indicator_feed_bit(dec, time_us_32(), data_bit);
}
dec->last_clock_high = clock_high;
return frame_ready;
}
void dial_indicator_start_gpio_irq(
dial_indicator_decoder_t *dec
) {
if (dec == 0 ||
dec->clock_gpio == DIAL_INDICATOR_INVALID_GPIO ||
dec->data_gpio == DIAL_INDICATOR_INVALID_GPIO) {
return;
}
s_irq_decoder = dec;
gpio_set_irq_enabled_with_callback(
dec->clock_gpio,
GPIO_IRQ_EDGE_FALL,
true,
&dial_indicator_gpio_irq_callback
);
}
void dial_indicator_stop_gpio_irq(
dial_indicator_decoder_t *dec
) {
if (dec == 0 ||
dec->clock_gpio == DIAL_INDICATOR_INVALID_GPIO) {
return;
}
gpio_set_irq_enabled(dec->clock_gpio, GPIO_IRQ_EDGE_FALL, false);
if (s_irq_decoder == dec) {
s_irq_decoder = 0;
}
}
int dial_indicator_poll_read_gpio(
dial_indicator_decoder_t *dec,
dial_indicator_value_t *out
) {
if (out == 0) {
return 0;
}
if (!dial_indicator_poll_gpio(dec)) {
return 0;
}
*out = dial_indicator_read(dec);
return out->valid ? 1 : 0;
}
int dial_indicator_available(
const dial_indicator_decoder_t *dec
) {
if (dec == 0) {
return 0;
}
return dec->frame_ready ? 1 : 0;
}
dial_indicator_value_t dial_indicator_decode_raw24(
uint32_t raw24
) {
dial_indicator_value_t out;
out.valid = 0;
out.raw24 = raw24 & 0x00FFFFFFUL;
* 核心协议:
* 24bit 反码二进制
*/
out.inv24 = (~out.raw24) & 0x00FFFFFFUL;
out.unit = (out.inv24 & DIAL_INDICATOR_UNIT_MASK)
? DIAL_INDICATOR_UNIT_INCH
: DIAL_INDICATOR_UNIT_MM;
out.negative = (out.inv24 & DIAL_INDICATOR_SIGN_MASK) ? 1 : 0;
out.reserved_bits = (uint8_t)((out.inv24 >> 21) & 0x03);
out.value_raw = out.inv24 & DIAL_INDICATOR_VALUE_MASK;
out.mm_x100 = 0;
out.inch_x10000 = 0;
if (out.unit == DIAL_INDICATOR_UNIT_MM) {
out.mm_x100 = (int32_t)out.value_raw;
if (out.negative && out.mm_x100 != 0) {
out.mm_x100 = -out.mm_x100;
}
} else {
out.inch_x10000 = (int32_t)(out.value_raw * 5UL);
if (out.negative && out.inch_x10000 != 0) {
out.inch_x10000 = -out.inch_x10000;
}
}
out.valid = 1;
return out;
}
dial_indicator_value_t dial_indicator_read(
dial_indicator_decoder_t *dec
) {
dial_indicator_value_t value;
if (dec == 0) {
return dial_indicator_invalid_value();
}
value = dial_indicator_decode_raw24(dec->ready_raw);
dec->frame_ready = 0;
return value;
}
希望能帮到后来的人,因为这东西折腾起来实在是闹心,每个卖家都说自己的百分表支持数据输出,然后卖着一根根两百块钱的USB转RS232的线,没招
如果文章中遇到看不太懂的代码、示例,以实际情况为准,实在弄不清楚就交给 AI 吧
查阅的一些参考资料:
- https://blog.csdn.net/shujian123/article/details/131103194
- https://blog.csdn.net/weixin_44481398/article/details/121110336
- https://bbs.elecfans.com/jishu_465931_1_1.html