被 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
示波器分析.jpg

很好看不太懂,但大概能知道和网上表的输出其实差不多,然后手上没有逻辑分析仪,于是拿出手上的Pico
通过开源项目 Pico逻辑分析仪 ,本质就是RP2040,借助强力的 PIO 能力直接当逻辑分析用,效果很不错,只要刷入 uf2 固件,装上上位机直接简单看看输出,和示波器差不多。

0mm.png
0mm.png

17.69mm.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封装起来,方便后期使用,我的代码如下:

#ifndef DIAL_INDICATOR_DECODER_H
#define DIAL_INDICATOR_DECODER_H

#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif

#define DIAL_INDICATOR_FRAME_BITS 24

#define DIAL_INDICATOR_VALUE_MASK 0x0FFFFFUL
#define DIAL_INDICATOR_SIGN_MASK  0x100000UL
#define DIAL_INDICATOR_UNIT_MASK  0x800000UL

#define DIAL_INDICATOR_INVALID_GPIO 0xFFu
#define DIAL_INDICATOR_DEFAULT_FRAME_GAP_US 5000u

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
);

#ifdef __cplusplus
}
#endif

#endif

#include "dial_indicator_decoder.h"

#include "hardware/gpio.h"
#include "pico/time.h"

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;

    /* Common 24-bit dial-indicator streams are sampled on clock falling edge. */
    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 吧

查阅的一些参考资料:

  1. https://blog.csdn.net/shujian123/article/details/131103194
  2. https://blog.csdn.net/weixin_44481398/article/details/121110336
  3. https://bbs.elecfans.com/jishu_465931_1_1.html