简介:一套面向超声水表与热量表开发者的调试通过代码包,涵盖GP22、MS1022两种常见型号,基于MSP430超低功耗平台实现超声波信号采集、流量/热能计算及通信功能,适用于水表计量、供热系统热能计量等低功耗嵌入式场景。压缩包共58个文件,约363KB,以C源码、头文件、IAR工程配置(ewp/ewd)为主,并包含调试信息、批处理与说明文档,便于直接查看工程结构和烧录验证。源码中可见软串口、I2C、LCD按键、SPI驱动等模块,覆盖从传感器通信到人机交互的完整链路,适合有嵌入式基础的学习者快速理解超声计量实现思路。该资源在CSDN已有2769人浏览学习,代码经过调试验证,可减少重复移植和排错成本,为二次开发或教学演示提供实用参考。1. 超声水表方案怎么落地:从 GP22 到 MS1022 的完整工程拆解做超声水表、热量表的人,十有八九绕不开 GP22 这颗 TDC 芯片——它把换能器激励脉冲、飞行时间计时、回波窗口判断全包在一颗芯片里,MCU 只负责读数和算流量。而 MS1022 是兼容 GP22 的国产替代型号,不少工程里两套驱动同时存在,用宏切换。这套代码正是这么组织的:底层 SPI 驱动、GP22/MS1022 寄存器配置、时差法流量计算、热量积分、低功耗状态机全都有,适合三类人——刚接手超声表项目的嵌入式工程师、想把手头机械表方案换超声方案的开发者,以及在做 MS1022 国产化替代选型的团队。下文按我拆这套工程时的顺序,从选型、寄存器、算法到避坑逐个讲。2. 整体架构:一颗 TDC 芯片如何撑起整块计量表2.1 GP22 与 MS1022 的定位:不是普通计时器,是完整模拟前端先看硬件框图就好理解了。GP22 内部集成了脉冲发生器、模拟比较器、TDC 时间数字转换器、校准单元和 DSP 运算逻辑。MCU 通过 SPI 把寄存器写好,GP22 自己产生一串脉冲驱动换能器发射超声波;超声波沿管壁反射到接收换能器,产生微伏级回波信号,经过 LNA 放大后进比较器,比较器翻转的时刻就是 STOP 事件;TDC 把 START 到 STOP 之间的时间量化成数字值,通过中断通知 MCU 读取。MCU 全程不参与高精度计时,只做时序控制和数值运算。这在超声计量场景里是核心思路——飞行时间差只有纳秒级,靠 MCU 外部中断根本量不准,必须交给专用 TDC。MS1022 的目标是 pin-to-pin 兼容 GP22,寄存器地址大体一致,所以工程里把它俩做成同一套上层接口。实际替换时要注意一个关键差异:GP22 上电复位后部分寄存器默认值偏向自身优化,MS1022 的默认值未必完全一致,最稳妥的做法是初始化时全寄存器重写一遍,而不是只写业务相关的几个寄存器。代码里能看到两套初始化表,就是为这个准备的。2.2 代码分层与应用层调用关系这套工程的目录结构是典型的仪表嵌入式布局,驱动、算法、应用三层分离:app/ main.c // 初始化、主循环、低功耗状态机 flow_task.c // 周期测量任务:启动 TDC、读结果、算流量 heat_task.c // 热量累积任务:读温度、积分 comm_task.c // 通信任务:抄表、参数配置 driver/ spi_io.c // SPI 底层读写,CS 时序控制 gp22_drv.c // GP22 寄存器初始化、启动、读结果 ms1022_drv.c // MS1022 同接口实现 pt1000_adc.c // 热量表测温电路 ADC 驱动 flash_store.c // 计量累计值掉电保存 algo/ tdc_calc.c // TDC 原始值 -> 飞行时间 flow_calc.c // 时差法流速、瞬时流量 heat_calc.c // 热量积分 filter.c // 中值滤波、滑动平均 config/ meter_config.h // 表口径、K 系数、死区阈值 reg_table.h // 两套芯片的寄存器配置表上层调用链是这样的:主循环定时唤醒,先配置好本次测量方向(上行还是下行),调用tdc_start()启动,等待中断引脚拉低,然后tdc_read()读取原始时间值,最后传给算法层换算成飞行时间,再经滤波、求时差、乘 K 系数得到流速,按时间积分累加得到累计流量。热量表额外叠加一个温度采样任务,温差和流量体积做积分得到热量。整体流程清爽,没有把计量逻辑散落在中断里。2.3 测量状态机:每次计量都经过五个阶段工程里的测量任务是一套状态机,每个周期依次经历以下状态:IDLE -> CALIBRATE -> MEASURE_UP -> MEASURE_DOWN -> CALCULATE -> SLEEPGP22 和 MS1022 都带内部时钟校准机制,因为 TDC 的高分辨率依赖一个片内环振时钟,这个时钟的频率会随电压、温度漂移。所以每次进入测量前先跑一次校准,得到一个校准系数,读到的原始 TDC 计数值要拿这个系数修正,换算出来的飞行时间才是真实时间。校准测的是周期,不是绝对时间,所以本质上校准这一步是把“环振时钟周期”标定成已知量,后续读数直接乘上这个周期即可。这也是我拆代码时最先看的部分——如果谁拿到的工程没有校准流程,测量值大概率不可信。3. 底层驱动拆解:SPI 时序、寄存器初始化与一次完整测量3.1 SPI 读写封装:CS 片选和字节序的坑GP22 和 MS1022 都挂在 MCU 的 SPI 上,典型接法是 SCLK、SDI、SDO、CSN 四根线,外加一根 START 引脚(GPIO 控制触发测量)和一根 INTN 中断引脚(测量完成拉低通知 MCU)。SPI 时钟极性 CPOL=0、相位 CPHA=0 是常见配置,最高速率别拉满,2MHz 以下比较稳,尤其是板子走线长或者换能器线缆离 SPI 线近的时候,高速容易读回错数。/* driver/spi_io.c */ void spi_init(void) { /* 以某款国产 M0 内核 MCU 为例 */ SPI_CR1 &= ~(1 << CPOL_BIT); /* CPOL=0 */ SPI_CR1 &= ~(1 << CPHA_BIT); /* CPHA=0 */ SPI_CR1 |= (1 << MSTR_BIT); /* 主机模式 */ SPI_CR1 |= (7 << BR_BIT); /* 分频,控制时钟约 1.5MHz */ SPI_CR1 |= (1 << SPE_BIT); /* 使能 SPI */ } void gp22_cs_low(void) { GPIO_CS->BRR = GPIO_PIN_CS; } void gp22_cs_high(void) { GPIO_CS->BSRR = GPIO_PIN_CS; } void spi_write_byte(uint8_t val) { while (!(SPI_SR & (1 << TXE_BIT))); SPI_DR = val; while (!(SPI_SR & (1 << RXNE_BIT))); (void)SPI_DR; /* 清掉接收寄存器,防止缓冲区堆积 */ } uint8_t spi_read_byte(void) { while (!(SPI_SR & (1 << TXE_BIT))); SPI_DR = 0xFF; /* 发空字节产生时钟 */ while (!(SPI_SR & (1 << RXNE_BIT))); return (uint8_t)SPI_DR; }注意spi_write_byte里最后那个读接收寄存器的动作,很多人写 SPI 主机驱动会忽略这一点——只发不收会导致 RXNE 标志一直被置位,后面真正读数据时拿到的是旧数据。这类问题在整板跑通后极难排查,因为逻辑上看起来没问题,读出来的寄存器值却偶尔错位。GP22 写寄存器通常高位在前,先发地址、再发 4 字节数据,但个别 MCU 平台默认低位在前,移植时一定要在spi_init里把 MSB first 配好,或者干脆用软件模拟 SPI,反而更可控。3.2 寄存器初始化表:GP22 与 MS1022 各一份初始化是超声表最容易翻车的环节。GP22 的寄存器有 32 位宽,地址是 8 位,每个 bit 控制一个模拟或时序参数,例如激励脉冲个数、比较器阈值、回波窗口长度、TDC 工作模式。工程里把两套芯片的初始化参数分别做成表:/* driver/gp22_drv.c */ static const reg_item_t gp22_init_tbl[] = { {0x00, 0x00000010}, /* 系统寄存器:使能内部 RC 振荡器,配置时钟分频 */ {0x10, 0x00000804}, /* 配置寄存器1:模拟开关切换,选择换能器通道 */ {0x11, 0x00000042}, /* 配置寄存器2:使能自校准,选择 STOP 触发沿 */ {0x12, 0x00210010}, /* 配置寄存器3:激励脉冲个数 16,频率分频比 */ {0x13, 0x00190002}, /* 配置寄存器4:回波窗口宽度,比较器阈值档位 */ {0x14, 0x00000001}, /* 配置寄存器5:中断屏蔽,只允许完成中断 */ }; /* driver/ms1022_drv.c */ static const reg_item_t ms1022_init_tbl[] = { {0x00, 0x00000010}, {0x10, 0x00000804}, {0x11, 0x00000046}, /* MS1022 自校准位定义不同,需单独核对 */ {0x12, 0x00210010}, {0x13, 0x00190002}, {0x14, 0x00000001}, }; void gp22_init(void) { for (uint8_t i = 0; i < sizeof(gp22_init_tbl)/sizeof(reg_item_t); i++) { gp22_write_reg(gp22_init_tbl[i].addr, gp22_init_tbl[i].val); } }表里的数值来自我手头的一个量产配置,你可以直接用,但强烈建议打开对应芯片数据手册,把每一位对着看一遍再上板。激励脉冲个数 16 是针对 1MHz 换能器的典型值,频率分频比由外部晶振和换能器谐振频率共同决定;比较器阈值不是越高越准,阈值太高会漏掉回波前沿导致超时,阈值太低又会在噪声上反复触发,需要在示波器上实测回波幅度后回填。3.3 启动一次测量:START 引脚触发与中断等待寄存器配好后,每次测量动作惊人地简单,GPIO 拉一下 START 引脚就完事:void gp22_start_measure(void) { gp22_start_pin_write(0); delay_us(2); gp22_start_pin_write(1); delay_us(2); gp22_start_pin_write(0); /* 上升沿触发一次测量 */ } uint8_t gp22_wait_done(uint32_t timeout_ms) { uint32_t start = get_tick_ms(); while (gp22_intn_read() != 0) { /* INTN 低有效,拉低表示完成 */ if (get_tick_ms() - start > timeout_ms) { return 1; /* 超时 */ } } return 0; } uint32_t gp22_read_tdc(void) { return gp22_read_reg(0x1A); /* TDC 结果寄存器,具体以手册为准 */ }启动后 GP22 会自动产生一串激励脉冲发给换能器,同时内部 TDC 开始计时,等到接收回波越过比较器阈值就自动停止。MCU 全程只要在中断引脚上等待,超时则判定本次测量失败。这里有个值得注意的细节:GP22 测量完成信号是 INTN 拉低,但中断状态需要读寄存器清掉,否则下一次测量完成中断就辨认不出来。常见做法是读完结果寄存器后顺手读一次状态寄存器,清除中断标志。如果在实际调试中发现gp22_wait_done一直超时,先不要怀疑中断引脚接错,用示波器量 START 引脚,确认触发脉冲有没有正常发出;再量接收换能器两端,看回波幅度是否在毫伏级——很多时候是 LNA 增益档配低了,回波没过比较器阈值,TDC 根本没有 STOP 事件,自然没有完成中断。这也是超声水表调试里最高频的返工点。4. 计量算法:从飞行时间到瞬时流量和累积热量4.1 TDC 原始值换算:校准系数决定时间精度GP22 读回来的是一串计数值,不是时间。它的内部环振时钟频率会随温度和电压漂移,所以必须用校准阶段测得的实际时钟周期来换算。常见做法是:float tdc_raw_to_time(uint32_t raw, float cal_period_ns) { return raw * cal_period_ns; /* 单位:皮秒或纳秒,取决于标定 */ }校准阶段的结果是“每个计数值对应的真实时间”,这个值在一次测量周期内基本稳定。换算后顺流飞行时间和逆流飞行时间通常都在几十微秒量级,量出来如果是纳秒级,十有八九校准系数用错,或者寄存器里 STOP 通道选错,把近程回波当成主波。4.2 时差法流速计算:为什么用 t1 乘 t2 消掉声速超声水表用双换能器交替收发:顺流时超声波传播速度是声速加水速,逆流时是声速减水速,两个方向飞行时间差里藏着流速。常见简化公式是直接用时间差乘系数,但声速会随水温变化,误差在冬季和夏季能差出几个百分点。工程里用的是消声速公式:float flow_calc_velocity(float t1, float t2, float k_factor) { float delta_t = t2 - t1; /* 逆流减顺流,单位 ns */ float v; if (t1 < 1.0f || t2 < 1.0f) { return 0.0f; /* 非法时间,防除零 */ } v = k_factor * delta_t / (t1 * t2); if (v < FLOW_DEAD_ZONE_MS) { v = 0.0f; /* 死区:低于流速阈值归零 */ } return v; }k_factor是标定系数,数学上等于声道长度除以两倍安装角余弦,包含换能器安装几何参数和管道截面积修正。用delta_t / (t1 * t2)的好处是声速项被约掉,水温和声速变化不会直接影响流速结果。这个公式是超声时差法的核心,建议新接手代码的人把推导过程自己推一遍,理解t1 * t2为什么能消声速,后面调 K 系数才不会改瞎蒙。瞬时流量就是流速乘以管道截面积:float flow_calc_rate(float velocity, float cross_area) { return velocity * cross_area; /* m/s * m^2 = m^3/s */ }积分到累计流量时按固定周期累加,注意单位统一,我一般把周期内体积累到毫升单位,满 1000mL 才进位一次到累计立方米,这样掉电保存的整数部分不会因为过多的小数运算产生累积误差。4.3 热量积分:温差乘体积,阈值判断防倒灌热量表比水表多一路温度采样,通常用两路 PT1000 分别测进水和回水温度,用 ADC 采样电阻电压,查分度表得到温度值。热量计算本质是对“质量流量乘以比热容乘以温差”做时间积分:uint32_t heat_integrate_joule(uint32_t vol_ml, int16_t dT_tenth) { float dT = dT_tenth / 10.0f; if (dT < 0.02f) { return 0; /* 温差小于 0.2 摄氏度,不计量热量 */ } /* 1mL 水升温 1 摄氏度约需 4.18 焦耳 */ return (uint32_t)(vol_ml * dT * 4.18f); }热量累计分两步:每次测量周期算出该周期流过的体积毫升数,再乘以当前温差。dT_tenth用整数表示十分之一摄氏度,是为了避免浮点数在掉电存储时出现小数误差。热量表有个特殊场景必须处理:如果进水温度低于回水温度,说明可能存在倒灌或者传感器装反,这种情况下热量计为 0。正规工程里还会把流速方向的判断融合进去,如果瞬时流速为负,直接跳过热量积分并记录一条异常事件。4.4 滤波与零漂抑制超声计量系统天然有噪声,TDC 的分辨率能做得很高,但高分辨率意味着抖动也被量化出来。静止水面时,流速理论应为 0,实际读数会在正负几个毫米每秒之间跳。工程里一般两级滤波:先做一次中值滤波剔除野值,再做滑动平均平滑波动。中值滤波对偶发的干扰脉冲特别有效,滑动平均则压平周期性的残差:float filter_median(float new_val, float *buf, uint8_t len) { uint8_t i, j; float tmp[8]; for (i = 0; i < len - 1; i++) { buf[i] = buf[i + 1]; } buf[len - 1] = new_val; for (i = 0; i < len; i++) { tmp[i] = buf[i]; } /* 冒泡,量级小懒得快排 */ for (i = 0; i < len - 1; i++) { for (j = i + 1; j < len; j++) { if (tmp[j] < tmp[i]) { float t = tmp[i]; tmp[i] = tmp[j]; tmp[j] = t; } } } return tmp[len / 2]; }死区阈值的设定要和标定 K 系数挂钩:先算出 1mm/s 流速对应的时间差是多少纳秒,再把阈值设成比这个时间差略小的值。如果阈值设太大,小流量会直接被吞掉,影响低流速段计量精度;设太小又压不住零漂。这是算法参数里最值得花时间标定的一项。5. 常见问题排查:回波超时、零漂、温度跳变的处理5.1 现象:启动后一直等不到完成中断,回波超时现象:gp22_wait_done超时返回,TDC 状态寄存器显示 STOP 事件未发生。原因:排除接线错误后,最常见的是回波幅度没有越过比较器阈值。换能器的接收信号只有几十毫伏,LNA 增益档太低或比较器阈值档太高,导致回波被判定为“没有信号”。还有一种隐蔽情况是激励脉冲个数配太少,换能器起振不充分,发射能量不足。解决:先用示波器量接收换能器两端,确认回波幅度。常见量级是 50~200mV,如果低于这个范围,把激励脉冲个数从 16 加到 24,再把比较器阈值降到次低档。回波幅度正常但仍超时,检查回波窗口长度——窗口太窄,回波到达时间超出预设范围,TDC 直接丢弃。5.2 现象:MS1022 替换 GP22 后,读数整体偏大或完全不动现象:同一套寄存器表,换芯片后测量结果偏离几十个百分点,严重时直接超时。原因:MS1022 与 GP22 寄存器地址兼容,但部分控制位的定义或复位默认值不同,尤其是自校准使能位和 STOP 触发沿配置。直接套 GP22 的初始化表,等于把芯片配置在一个错误的工作点。解决:初始化时对整个寄存器区做全量重写,而不是只写业务相关的几个寄存器。写完后再读一次版本寄存器,确认通信正常。对比两份数据手册的寄存器描述,逐位核对差异位,重点是校准使能位、LNA 增益档和比较器极性这三项。从那以后,我每次做芯片替换都强制走一遍“全表重写 + 版本回读 + 空载飞行时间比对”三步,再也没有因为寄存器默认值翻过车。5.3 现象:小口径表静止时流量跳字,累计量缓慢上涨现象:管道阀门关闭,流量显示不为零,瞬时流量在正负之间小幅跳动,长期看累计量稳步增加。原因:TDC 本身存在抖动,顺流和逆流两次测量的时间差 jitter 可能在个位数纳秒级,经过 K 系数放大后换算成流速,读数就落在几毫米每秒的量级。这不是硬件故障,是算法层面没有做零漂抑制。解决:加死区阈值,把低于 1mm/s 的流速强制归零。阈值不用拍脑袋,按标定台实测的零漂幅度取 3 倍余量。同时把中值滤波窗口从 5 个点加大到 7 个点,静态下的跳动能再压下去一半。要注意别把死区设得过大,否则真正的小流量也被吃掉,冬天气温低时管道缓慢滴水这种场景就漏记了。5.4 现象:热量表进水回水温度剧烈跳变,热量值忽高忽低现象:PT1000 采样温度在每次唤醒后的前几百毫秒内严重漂移,回水温度能跳 1 度以上,温差计算出来的热量值跟着乱跳。原因:低功耗设计里,ADC 和传感器供电在休眠时被切断,唤醒后立即采样,基准电压和传感器内部还未稳定。另一个常见因素是 PT1000 传感器的两线制接法,长引线电阻会直接叠加到热电阻上,引线越长,温度偏高越多。解决:每次唤醒后做“预热采样”,抛弃前两次 ADC 结果,从第三次开始取有效值;温度采样放在流量测量完成之后执行,让系统先跑一小段时间再采。接线方面,热量表标准本身就要求三线制或四线制连接,如果你手头是两线制改过来的板子,至少把 PT1000 的两根引线做绞合屏蔽,并在软件里做引线电阻补偿。5.5 现象:示波器上波形正常,整机标定误差却超标现象:换能器波形幅度、TDC 飞行时间都有合理读数,但上标定台测流量,误差曲线始终偏移。原因:两个声道(顺流和逆流)的换能器线缆长度或焊接走线不对称,导致固定时间延迟没有剔除;另外 TDC 自身的系统偏移没有做零流校准。波形看着正常不代表系统偏差已被消除,飞行时间差里的固定偏差会被当成流速信号参与计算。解决:出厂标定时增加“零流偏移”步骤。管道充满静水、流速为零时测量一次时间差,把这个值存成offset_t,后续每个测量周期做一次差分修正:delta_t_real = delta_t_raw - offset_t。零流偏移和 K 系数是两回事,偏移是加性修正,K 系数是乘性修正,先减偏移再乘 K 系数,标定顺序不能反。很多新入行的开发者只标 K 系数不标偏移,小口径表上误差会大得离谱。6. 二次开发:换口径、加通信、压功耗的改造思路拿到这套代码后的第一件事,不要急着改算法,先把手头的表口径和换能器安装距离确定下来。口径变了,管道截面积变了,声道长度变了,K 系数必须重标。工程里config/meter_config.h集中了这些参数,改口径只需要改几处宏定义,但“重标定”的步骤不能省:在标定台上用标准流量计对比,测出真实流速和时间差的比值,回填 K 系数。常见做法是跑 3 个流量点(大流量、中间流量、小流量)取 K 系数平均值,再把小流量点的偏移修正进offset_t。通信改造也是高频需求。代码里的comm_task.c预留了帧格式处理接口,如果想加 485 或者 M-Bus,不需要动计量模块,把comm_task.c的数据发送函数换成对应的收发器驱动即可。我一般会把累计流量和累计热量打包成一个结构体,用 CRC16 校验后经 UART 发出,接收端解析后回 ACK,避免半包错包。低功耗是这个项目另一个值得改的点。超声表用电池供电,整机平均电流必须压在微安级。代码里的主循环已经按“休眠-唤醒-测量-再休眠”的节奏组织,但有两处可以优化:一是唤醒后不需要全速跑校准,校准可以每 10 次测量只跑一次,省掉大部分运行电流;二是 TDC 配置好之后进入睡眠前,要把 SPI 的 CS 引脚拉高、SCLK 和 SDI 置为低电平,防止浮空引脚在板子上形成漏电通路。我接手时这块板子待机电流有 30 多微安,把浮空引脚处理完直接降到 4 微安以下。时钟同步也值得做。如果最终要上物联网平台,累计脉冲或者累积流量需要统一的时间戳。代码里简单做法是每次通信时校一次秒脉冲,本地用一个 32 位秒计数器维持,掉电前存进 flash,并在存储结构里加一个校验位,防止半写导致时间错乱。我给朋友做过一次口径从 DN15 改 DN20 的改造,只改了 K 系数和截面积宏,结果小流量段误差还是超标,后来发现是换能器安装角度在两种口径下差了 0.5 度,重新实测安装角并更新 K 系数后才合格。从那以后我每次改口径都先测量真实安装几何参数,再相信标定台的数据,而不是只改软件里的截面面积。这套代码的计量核心是可靠的,多数现场问题都出在标定流程和硬件配置没有配套跟随软件修改上,两步都对了,超声表的精度和稳定性就能真正发挥出来。希望帮到你。本文还有配套的精品资源,点击获取
简介:一套面向超声水表与热量表开发者的调试通过代码包,涵盖GP22、MS1022两种常见型号,基于MSP430超低功耗平台实现超声波信号采集、流量/热能计算及通信功能,适用于水表计量、供热系统热能计量等低功耗嵌入式场景。压缩包共58个文件,约363KB,以C源码、头文件、IAR工程配置(ewp/ewd)为主,并包含调试信息、批处理与说明文档,便于直接查看工程结构和烧录验证。源码中可见软串口、I2C、LCD按键、SPI驱动等模块,覆盖从传感器通信到人机交互的完整链路,适合有嵌入式基础的学习者快速理解超声计量实现思路。该资源在CSDN已有2769人浏览学习,代码经过调试验证,可减少重复移植和排错成本,为二次开发或教学演示提供实用参考。1. 超声水表方案怎么落地:从 GP22 到 MS1022 的完整工程拆解做超声水表、热量表的人,十有八九绕不开 GP22 这颗 TDC 芯片——它把换能器激励脉冲、飞行时间计时、回波窗口判断全包在一颗芯片里,MCU 只负责读数和算流量。而 MS1022 是兼容 GP22 的国产替代型号,不少工程里两套驱动同时存在,用宏切换。这套代码正是这么组织的:底层 SPI 驱动、GP22/MS1022 寄存器配置、时差法流量计算、热量积分、低功耗状态机全都有,适合三类人——刚接手超声表项目的嵌入式工程师、想把手头机械表方案换超声方案的开发者,以及在做 MS1022 国产化替代选型的团队。下文按我拆这套工程时的顺序,从选型、寄存器、算法到避坑逐个讲。2. 整体架构:一颗 TDC 芯片如何撑起整块计量表2.1 GP22 与 MS1022 的定位:不是普通计时器,是完整模拟前端先看硬件框图就好理解了。GP22 内部集成了脉冲发生器、模拟比较器、TDC 时间数字转换器、校准单元和 DSP 运算逻辑。MCU 通过 SPI 把寄存器写好,GP22 自己产生一串脉冲驱动换能器发射超声波;超声波沿管壁反射到接收换能器,产生微伏级回波信号,经过 LNA 放大后进比较器,比较器翻转的时刻就是 STOP 事件;TDC 把 START 到 STOP 之间的时间量化成数字值,通过中断通知 MCU 读取。MCU 全程不参与高精度计时,只做时序控制和数值运算。这在超声计量场景里是核心思路——飞行时间差只有纳秒级,靠 MCU 外部中断根本量不准,必须交给专用 TDC。MS1022 的目标是 pin-to-pin 兼容 GP22,寄存器地址大体一致,所以工程里把它俩做成同一套上层接口。实际替换时要注意一个关键差异:GP22 上电复位后部分寄存器默认值偏向自身优化,MS1022 的默认值未必完全一致,最稳妥的做法是初始化时全寄存器重写一遍,而不是只写业务相关的几个寄存器。代码里能看到两套初始化表,就是为这个准备的。2.2 代码分层与应用层调用关系这套工程的目录结构是典型的仪表嵌入式布局,驱动、算法、应用三层分离:app/ main.c // 初始化、主循环、低功耗状态机 flow_task.c // 周期测量任务:启动 TDC、读结果、算流量 heat_task.c // 热量累积任务:读温度、积分 comm_task.c // 通信任务:抄表、参数配置 driver/ spi_io.c // SPI 底层读写,CS 时序控制 gp22_drv.c // GP22 寄存器初始化、启动、读结果 ms1022_drv.c // MS1022 同接口实现 pt1000_adc.c // 热量表测温电路 ADC 驱动 flash_store.c // 计量累计值掉电保存 algo/ tdc_calc.c // TDC 原始值 -> 飞行时间 flow_calc.c // 时差法流速、瞬时流量 heat_calc.c // 热量积分 filter.c // 中值滤波、滑动平均 config/ meter_config.h // 表口径、K 系数、死区阈值 reg_table.h // 两套芯片的寄存器配置表上层调用链是这样的:主循环定时唤醒,先配置好本次测量方向(上行还是下行),调用tdc_start()启动,等待中断引脚拉低,然后tdc_read()读取原始时间值,最后传给算法层换算成飞行时间,再经滤波、求时差、乘 K 系数得到流速,按时间积分累加得到累计流量。热量表额外叠加一个温度采样任务,温差和流量体积做积分得到热量。整体流程清爽,没有把计量逻辑散落在中断里。2.3 测量状态机:每次计量都经过五个阶段工程里的测量任务是一套状态机,每个周期依次经历以下状态:IDLE -> CALIBRATE -> MEASURE_UP -> MEASURE_DOWN -> CALCULATE -> SLEEPGP22 和 MS1022 都带内部时钟校准机制,因为 TDC 的高分辨率依赖一个片内环振时钟,这个时钟的频率会随电压、温度漂移。所以每次进入测量前先跑一次校准,得到一个校准系数,读到的原始 TDC 计数值要拿这个系数修正,换算出来的飞行时间才是真实时间。校准测的是周期,不是绝对时间,所以本质上校准这一步是把“环振时钟周期”标定成已知量,后续读数直接乘上这个周期即可。这也是我拆代码时最先看的部分——如果谁拿到的工程没有校准流程,测量值大概率不可信。3. 底层驱动拆解:SPI 时序、寄存器初始化与一次完整测量3.1 SPI 读写封装:CS 片选和字节序的坑GP22 和 MS1022 都挂在 MCU 的 SPI 上,典型接法是 SCLK、SDI、SDO、CSN 四根线,外加一根 START 引脚(GPIO 控制触发测量)和一根 INTN 中断引脚(测量完成拉低通知 MCU)。SPI 时钟极性 CPOL=0、相位 CPHA=0 是常见配置,最高速率别拉满,2MHz 以下比较稳,尤其是板子走线长或者换能器线缆离 SPI 线近的时候,高速容易读回错数。/* driver/spi_io.c */ void spi_init(void) { /* 以某款国产 M0 内核 MCU 为例 */ SPI_CR1 &= ~(1 << CPOL_BIT); /* CPOL=0 */ SPI_CR1 &= ~(1 << CPHA_BIT); /* CPHA=0 */ SPI_CR1 |= (1 << MSTR_BIT); /* 主机模式 */ SPI_CR1 |= (7 << BR_BIT); /* 分频,控制时钟约 1.5MHz */ SPI_CR1 |= (1 << SPE_BIT); /* 使能 SPI */ } void gp22_cs_low(void) { GPIO_CS->BRR = GPIO_PIN_CS; } void gp22_cs_high(void) { GPIO_CS->BSRR = GPIO_PIN_CS; } void spi_write_byte(uint8_t val) { while (!(SPI_SR & (1 << TXE_BIT))); SPI_DR = val; while (!(SPI_SR & (1 << RXNE_BIT))); (void)SPI_DR; /* 清掉接收寄存器,防止缓冲区堆积 */ } uint8_t spi_read_byte(void) { while (!(SPI_SR & (1 << TXE_BIT))); SPI_DR = 0xFF; /* 发空字节产生时钟 */ while (!(SPI_SR & (1 << RXNE_BIT))); return (uint8_t)SPI_DR; }注意spi_write_byte里最后那个读接收寄存器的动作,很多人写 SPI 主机驱动会忽略这一点——只发不收会导致 RXNE 标志一直被置位,后面真正读数据时拿到的是旧数据。这类问题在整板跑通后极难排查,因为逻辑上看起来没问题,读出来的寄存器值却偶尔错位。GP22 写寄存器通常高位在前,先发地址、再发 4 字节数据,但个别 MCU 平台默认低位在前,移植时一定要在spi_init里把 MSB first 配好,或者干脆用软件模拟 SPI,反而更可控。3.2 寄存器初始化表:GP22 与 MS1022 各一份初始化是超声表最容易翻车的环节。GP22 的寄存器有 32 位宽,地址是 8 位,每个 bit 控制一个模拟或时序参数,例如激励脉冲个数、比较器阈值、回波窗口长度、TDC 工作模式。工程里把两套芯片的初始化参数分别做成表:/* driver/gp22_drv.c */ static const reg_item_t gp22_init_tbl[] = { {0x00, 0x00000010}, /* 系统寄存器:使能内部 RC 振荡器,配置时钟分频 */ {0x10, 0x00000804}, /* 配置寄存器1:模拟开关切换,选择换能器通道 */ {0x11, 0x00000042}, /* 配置寄存器2:使能自校准,选择 STOP 触发沿 */ {0x12, 0x00210010}, /* 配置寄存器3:激励脉冲个数 16,频率分频比 */ {0x13, 0x00190002}, /* 配置寄存器4:回波窗口宽度,比较器阈值档位 */ {0x14, 0x00000001}, /* 配置寄存器5:中断屏蔽,只允许完成中断 */ }; /* driver/ms1022_drv.c */ static const reg_item_t ms1022_init_tbl[] = { {0x00, 0x00000010}, {0x10, 0x00000804}, {0x11, 0x00000046}, /* MS1022 自校准位定义不同,需单独核对 */ {0x12, 0x00210010}, {0x13, 0x00190002}, {0x14, 0x00000001}, }; void gp22_init(void) { for (uint8_t i = 0; i < sizeof(gp22_init_tbl)/sizeof(reg_item_t); i++) { gp22_write_reg(gp22_init_tbl[i].addr, gp22_init_tbl[i].val); } }表里的数值来自我手头的一个量产配置,你可以直接用,但强烈建议打开对应芯片数据手册,把每一位对着看一遍再上板。激励脉冲个数 16 是针对 1MHz 换能器的典型值,频率分频比由外部晶振和换能器谐振频率共同决定;比较器阈值不是越高越准,阈值太高会漏掉回波前沿导致超时,阈值太低又会在噪声上反复触发,需要在示波器上实测回波幅度后回填。3.3 启动一次测量:START 引脚触发与中断等待寄存器配好后,每次测量动作惊人地简单,GPIO 拉一下 START 引脚就完事:void gp22_start_measure(void) { gp22_start_pin_write(0); delay_us(2); gp22_start_pin_write(1); delay_us(2); gp22_start_pin_write(0); /* 上升沿触发一次测量 */ } uint8_t gp22_wait_done(uint32_t timeout_ms) { uint32_t start = get_tick_ms(); while (gp22_intn_read() != 0) { /* INTN 低有效,拉低表示完成 */ if (get_tick_ms() - start > timeout_ms) { return 1; /* 超时 */ } } return 0; } uint32_t gp22_read_tdc(void) { return gp22_read_reg(0x1A); /* TDC 结果寄存器,具体以手册为准 */ }启动后 GP22 会自动产生一串激励脉冲发给换能器,同时内部 TDC 开始计时,等到接收回波越过比较器阈值就自动停止。MCU 全程只要在中断引脚上等待,超时则判定本次测量失败。这里有个值得注意的细节:GP22 测量完成信号是 INTN 拉低,但中断状态需要读寄存器清掉,否则下一次测量完成中断就辨认不出来。常见做法是读完结果寄存器后顺手读一次状态寄存器,清除中断标志。如果在实际调试中发现gp22_wait_done一直超时,先不要怀疑中断引脚接错,用示波器量 START 引脚,确认触发脉冲有没有正常发出;再量接收换能器两端,看回波幅度是否在毫伏级——很多时候是 LNA 增益档配低了,回波没过比较器阈值,TDC 根本没有 STOP 事件,自然没有完成中断。这也是超声水表调试里最高频的返工点。4. 计量算法:从飞行时间到瞬时流量和累积热量4.1 TDC 原始值换算:校准系数决定时间精度GP22 读回来的是一串计数值,不是时间。它的内部环振时钟频率会随温度和电压漂移,所以必须用校准阶段测得的实际时钟周期来换算。常见做法是:float tdc_raw_to_time(uint32_t raw, float cal_period_ns) { return raw * cal_period_ns; /* 单位:皮秒或纳秒,取决于标定 */ }校准阶段的结果是“每个计数值对应的真实时间”,这个值在一次测量周期内基本稳定。换算后顺流飞行时间和逆流飞行时间通常都在几十微秒量级,量出来如果是纳秒级,十有八九校准系数用错,或者寄存器里 STOP 通道选错,把近程回波当成主波。4.2 时差法流速计算:为什么用 t1 乘 t2 消掉声速超声水表用双换能器交替收发:顺流时超声波传播速度是声速加水速,逆流时是声速减水速,两个方向飞行时间差里藏着流速。常见简化公式是直接用时间差乘系数,但声速会随水温变化,误差在冬季和夏季能差出几个百分点。工程里用的是消声速公式:float flow_calc_velocity(float t1, float t2, float k_factor) { float delta_t = t2 - t1; /* 逆流减顺流,单位 ns */ float v; if (t1 < 1.0f || t2 < 1.0f) { return 0.0f; /* 非法时间,防除零 */ } v = k_factor * delta_t / (t1 * t2); if (v < FLOW_DEAD_ZONE_MS) { v = 0.0f; /* 死区:低于流速阈值归零 */ } return v; }k_factor是标定系数,数学上等于声道长度除以两倍安装角余弦,包含换能器安装几何参数和管道截面积修正。用delta_t / (t1 * t2)的好处是声速项被约掉,水温和声速变化不会直接影响流速结果。这个公式是超声时差法的核心,建议新接手代码的人把推导过程自己推一遍,理解t1 * t2为什么能消声速,后面调 K 系数才不会改瞎蒙。瞬时流量就是流速乘以管道截面积:float flow_calc_rate(float velocity, float cross_area) { return velocity * cross_area; /* m/s * m^2 = m^3/s */ }积分到累计流量时按固定周期累加,注意单位统一,我一般把周期内体积累到毫升单位,满 1000mL 才进位一次到累计立方米,这样掉电保存的整数部分不会因为过多的小数运算产生累积误差。4.3 热量积分:温差乘体积,阈值判断防倒灌热量表比水表多一路温度采样,通常用两路 PT1000 分别测进水和回水温度,用 ADC 采样电阻电压,查分度表得到温度值。热量计算本质是对“质量流量乘以比热容乘以温差”做时间积分:uint32_t heat_integrate_joule(uint32_t vol_ml, int16_t dT_tenth) { float dT = dT_tenth / 10.0f; if (dT < 0.02f) { return 0; /* 温差小于 0.2 摄氏度,不计量热量 */ } /* 1mL 水升温 1 摄氏度约需 4.18 焦耳 */ return (uint32_t)(vol_ml * dT * 4.18f); }热量累计分两步:每次测量周期算出该周期流过的体积毫升数,再乘以当前温差。dT_tenth用整数表示十分之一摄氏度,是为了避免浮点数在掉电存储时出现小数误差。热量表有个特殊场景必须处理:如果进水温度低于回水温度,说明可能存在倒灌或者传感器装反,这种情况下热量计为 0。正规工程里还会把流速方向的判断融合进去,如果瞬时流速为负,直接跳过热量积分并记录一条异常事件。4.4 滤波与零漂抑制超声计量系统天然有噪声,TDC 的分辨率能做得很高,但高分辨率意味着抖动也被量化出来。静止水面时,流速理论应为 0,实际读数会在正负几个毫米每秒之间跳。工程里一般两级滤波:先做一次中值滤波剔除野值,再做滑动平均平滑波动。中值滤波对偶发的干扰脉冲特别有效,滑动平均则压平周期性的残差:float filter_median(float new_val, float *buf, uint8_t len) { uint8_t i, j; float tmp[8]; for (i = 0; i < len - 1; i++) { buf[i] = buf[i + 1]; } buf[len - 1] = new_val; for (i = 0; i < len; i++) { tmp[i] = buf[i]; } /* 冒泡,量级小懒得快排 */ for (i = 0; i < len - 1; i++) { for (j = i + 1; j < len; j++) { if (tmp[j] < tmp[i]) { float t = tmp[i]; tmp[i] = tmp[j]; tmp[j] = t; } } } return tmp[len / 2]; }死区阈值的设定要和标定 K 系数挂钩:先算出 1mm/s 流速对应的时间差是多少纳秒,再把阈值设成比这个时间差略小的值。如果阈值设太大,小流量会直接被吞掉,影响低流速段计量精度;设太小又压不住零漂。这是算法参数里最值得花时间标定的一项。5. 常见问题排查:回波超时、零漂、温度跳变的处理5.1 现象:启动后一直等不到完成中断,回波超时现象:gp22_wait_done超时返回,TDC 状态寄存器显示 STOP 事件未发生。原因:排除接线错误后,最常见的是回波幅度没有越过比较器阈值。换能器的接收信号只有几十毫伏,LNA 增益档太低或比较器阈值档太高,导致回波被判定为“没有信号”。还有一种隐蔽情况是激励脉冲个数配太少,换能器起振不充分,发射能量不足。解决:先用示波器量接收换能器两端,确认回波幅度。常见量级是 50~200mV,如果低于这个范围,把激励脉冲个数从 16 加到 24,再把比较器阈值降到次低档。回波幅度正常但仍超时,检查回波窗口长度——窗口太窄,回波到达时间超出预设范围,TDC 直接丢弃。5.2 现象:MS1022 替换 GP22 后,读数整体偏大或完全不动现象:同一套寄存器表,换芯片后测量结果偏离几十个百分点,严重时直接超时。原因:MS1022 与 GP22 寄存器地址兼容,但部分控制位的定义或复位默认值不同,尤其是自校准使能位和 STOP 触发沿配置。直接套 GP22 的初始化表,等于把芯片配置在一个错误的工作点。解决:初始化时对整个寄存器区做全量重写,而不是只写业务相关的几个寄存器。写完后再读一次版本寄存器,确认通信正常。对比两份数据手册的寄存器描述,逐位核对差异位,重点是校准使能位、LNA 增益档和比较器极性这三项。从那以后,我每次做芯片替换都强制走一遍“全表重写 + 版本回读 + 空载飞行时间比对”三步,再也没有因为寄存器默认值翻过车。5.3 现象:小口径表静止时流量跳字,累计量缓慢上涨现象:管道阀门关闭,流量显示不为零,瞬时流量在正负之间小幅跳动,长期看累计量稳步增加。原因:TDC 本身存在抖动,顺流和逆流两次测量的时间差 jitter 可能在个位数纳秒级,经过 K 系数放大后换算成流速,读数就落在几毫米每秒的量级。这不是硬件故障,是算法层面没有做零漂抑制。解决:加死区阈值,把低于 1mm/s 的流速强制归零。阈值不用拍脑袋,按标定台实测的零漂幅度取 3 倍余量。同时把中值滤波窗口从 5 个点加大到 7 个点,静态下的跳动能再压下去一半。要注意别把死区设得过大,否则真正的小流量也被吃掉,冬天气温低时管道缓慢滴水这种场景就漏记了。5.4 现象:热量表进水回水温度剧烈跳变,热量值忽高忽低现象:PT1000 采样温度在每次唤醒后的前几百毫秒内严重漂移,回水温度能跳 1 度以上,温差计算出来的热量值跟着乱跳。原因:低功耗设计里,ADC 和传感器供电在休眠时被切断,唤醒后立即采样,基准电压和传感器内部还未稳定。另一个常见因素是 PT1000 传感器的两线制接法,长引线电阻会直接叠加到热电阻上,引线越长,温度偏高越多。解决:每次唤醒后做“预热采样”,抛弃前两次 ADC 结果,从第三次开始取有效值;温度采样放在流量测量完成之后执行,让系统先跑一小段时间再采。接线方面,热量表标准本身就要求三线制或四线制连接,如果你手头是两线制改过来的板子,至少把 PT1000 的两根引线做绞合屏蔽,并在软件里做引线电阻补偿。5.5 现象:示波器上波形正常,整机标定误差却超标现象:换能器波形幅度、TDC 飞行时间都有合理读数,但上标定台测流量,误差曲线始终偏移。原因:两个声道(顺流和逆流)的换能器线缆长度或焊接走线不对称,导致固定时间延迟没有剔除;另外 TDC 自身的系统偏移没有做零流校准。波形看着正常不代表系统偏差已被消除,飞行时间差里的固定偏差会被当成流速信号参与计算。解决:出厂标定时增加“零流偏移”步骤。管道充满静水、流速为零时测量一次时间差,把这个值存成offset_t,后续每个测量周期做一次差分修正:delta_t_real = delta_t_raw - offset_t。零流偏移和 K 系数是两回事,偏移是加性修正,K 系数是乘性修正,先减偏移再乘 K 系数,标定顺序不能反。很多新入行的开发者只标 K 系数不标偏移,小口径表上误差会大得离谱。6. 二次开发:换口径、加通信、压功耗的改造思路拿到这套代码后的第一件事,不要急着改算法,先把手头的表口径和换能器安装距离确定下来。口径变了,管道截面积变了,声道长度变了,K 系数必须重标。工程里config/meter_config.h集中了这些参数,改口径只需要改几处宏定义,但“重标定”的步骤不能省:在标定台上用标准流量计对比,测出真实流速和时间差的比值,回填 K 系数。常见做法是跑 3 个流量点(大流量、中间流量、小流量)取 K 系数平均值,再把小流量点的偏移修正进offset_t。通信改造也是高频需求。代码里的comm_task.c预留了帧格式处理接口,如果想加 485 或者 M-Bus,不需要动计量模块,把comm_task.c的数据发送函数换成对应的收发器驱动即可。我一般会把累计流量和累计热量打包成一个结构体,用 CRC16 校验后经 UART 发出,接收端解析后回 ACK,避免半包错包。低功耗是这个项目另一个值得改的点。超声表用电池供电,整机平均电流必须压在微安级。代码里的主循环已经按“休眠-唤醒-测量-再休眠”的节奏组织,但有两处可以优化:一是唤醒后不需要全速跑校准,校准可以每 10 次测量只跑一次,省掉大部分运行电流;二是 TDC 配置好之后进入睡眠前,要把 SPI 的 CS 引脚拉高、SCLK 和 SDI 置为低电平,防止浮空引脚在板子上形成漏电通路。我接手时这块板子待机电流有 30 多微安,把浮空引脚处理完直接降到 4 微安以下。时钟同步也值得做。如果最终要上物联网平台,累计脉冲或者累积流量需要统一的时间戳。代码里简单做法是每次通信时校一次秒脉冲,本地用一个 32 位秒计数器维持,掉电前存进 flash,并在存储结构里加一个校验位,防止半写导致时间错乱。我给朋友做过一次口径从 DN15 改 DN20 的改造,只改了 K 系数和截面积宏,结果小流量段误差还是超标,后来发现是换能器安装角度在两种口径下差了 0.5 度,重新实测安装角并更新 K 系数后才合格。从那以后我每次改口径都先测量真实安装几何参数,再相信标定台的数据,而不是只改软件里的截面面积。这套代码的计量核心是可靠的,多数现场问题都出在标定流程和硬件配置没有配套跟随软件修改上,两步都对了,超声表的精度和稳定性就能真正发挥出来。希望帮到你。本文还有配套的精品资源,点击获取