尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

TDC-GP22高精度时间测量实战:从SPI配置到激光测距

TDC-GP22高精度时间测量实战:从SPI配置到激光测距 简介一套基于MSP430单片机与TDC-GP22高精度时间测量芯片的超声水表流量计算例程面向单片机与嵌入式硬件开发者适用于超声波测距、脉冲测速等需要精确时间间隔测量的场景。压缩包共158个文件、455KB以C源文件、头文件及IAR工程配置文件为主包含主程序、TDC-GP22驱动、LCD显示、RS485通信、定时器与功能函数等模块代码结构清晰便于查阅和移植。例程完整展示了TDC-GP22的初始化配置、超声波脉冲触发与回波捕获、时间差计算以及流量积分的实现流程可帮助开发者理解两芯片的协同工作机制并直接基于MSP430平台进行调试。已有2149人学习下载对正在开发低功耗流量计量或高精度测时方案的工程师具有直接参考价值也可依据实际需求进一步优化测量精度与通信逻辑。1. TDC-GP22到底香在哪先搞懂这颗芯片在测什么做激光测距、超声波流量计、热表水表的朋友大概率都听过TDC-GP22这个名字。它是德国ACAM公司现在归了ams推出的一款时间数字转换芯片核心能力就一句话把两个脉冲之间的时间间隔直接测出来精度能干到皮秒级。典型的22ps分辨率是什么概念光在1纳秒里走30厘米22ps对应的距离精度大概在毫米级这比普通单片机用定时器输入捕获硬憋出来的精度高了好几个数量级。所以TDC-GP22的典型应用场景非常好理解激光测距模块里你发一个激光脉冲打到目标反射回来光电二极管收到回波中间这段飞行时间Time of Flight用GP22来量配合光速就能算出距离。超声波流量计里顺流和逆流两个超声波信号的传播时间差决定了流体的流速这个时间差可能只有几百纳秒甚至几十纳秒MCU自己搞不定必须交给专用芯片。还有水表热表里用超声波测流量原理类似。我之前第一次接触这颗芯片的时候以为例程拿过来改改SPI配置就能跑结果被寄存器配置和校准流程折腾了好几轮。后来发现这颗芯片的硬件电路设计虽然不复杂但真正决定测量精度和稳定性的是软件里的各种细节。这篇文章就把我踩过的坑和经验整理出来从SPI读写、测量模式选择、校准流程到PCB注意点一次讲透。2. 上手第一步SPI通信和寄存器配置的底层逻辑2.1 SPI时序和地址映射先别急着写代码TDC-GP22是一个SPI从机支持最高20MHz的SPI时钟。它没有传统的寄存器地址数据这种通用读写模型而是定义了40条指令每条指令对应一个操作。比如0x00是初始化0x01是读取配置寄存器00x80开始是写入配置寄存器0的指令等等。指令和数据是连续发送的所以操作顺序要非常严格。硬件连接其实就五根线SPI的SCLK、MOSI、MISO、片选CSN再加上复位RSTN。注意GP22的MISO引脚是开漏输出外部必须接上拉电阻不然主机读不到数据。这个细节我一开始没注意结果读回来的数据永远是0xFF排查了半小时。指令集里最常用的是这几条指令说明0x00初始化复位芯片内部状态0x01读取配置寄存器00x02读取配置寄存器10x03读取配置寄存器20x04读取配置寄存器30x05读取状态寄存器0x10启动一次测量不含校准0x11启动一次测量含校准0x50读取结果寄存器00x51读取结果寄存器10x52读取结果寄存器20x53读取结果寄存器30xB0写配置寄存器00xB1写配置寄存器10xB2写配置寄存器20xB3写配置寄存器3读指令和写指令要分开看。写配置寄存器时指令是0xB0~0xB3后面跟32位数据4个字节。读配置寄存器时指令是0x01~0x04后面跟4个字节的填充读回来的第2到第5个字节就是寄存器内容。这里有个容易被坑的点GP22的位序不是常规的MSB先行而是LSB先行也就是低有效位在前。如果你用HAL库默认的MSB先行模式通信读回来的数据会整个反转看起来就像乱码一样。2.2 寄存器里每个位都是什么含义配置寄存器0是GP22最重要的一张配置表32位每bit都有含义我挑关键的几个字段说Bit 21~20HITIN1模式控制Hit1引脚的触发模式00是正沿触发01是负沿触发。激光测距一般用正沿触发。Bit 23~22HITIN2模式同理控制Hit2引脚。Bit 25~24START模式控制Start引脚的触发模式同样选正沿。Bit 9~8时钟分频器GP22内部PLL会把外部晶振频率倍频到几GHz作为时间戳的时钟源这两个位设置分频比影响测量的死区和测量范围。Bit 13校准是否启用自动校准。Bit 5~4通道选择设置使用哪个通道做测量测量模式1只能选一个通道测量模式2可以同时两个通道。配置寄存器1主要管ALU的重叠处理、FIFO溢出行为、中断引脚配置等。配置寄存器2管温度测量和时钟控制。配置寄存器3管SPI接口时序和一些测试模式。初次上手不需要把所有位都吃透先照着例程把寄存器的值算出来跑通测量再慢慢调参。2.3 一段可直接用的SPI底驱动GP22的SPI通信细节在于片选信号的控制。每次操作CSN拉低发送一个首字节指令然后根据是读还是写继续发送或接收数据最后CSN拉高。数据位宽设成8位比较顺手因为所有指令和寄存器都是按字节组织的。下面是一个基于STM32标准外设库的读写函数平台是STM32F103SPI1主模式时钟极性CPOL0相位CPHA1。注意GP22要求SPI模式是1CPOL0CPHA1这是很多人容易忽略的HAL库默认可能是模式0必须改过来。#define TDC_CSN_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_4) #define TDC_CSN_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_4) static uint8_t SPI1_SendByte(uint8_t byte) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET) {} SPI_I2S_SendData(SPI1, byte); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) RESET) {} return SPI_I2S_ReceiveData(SPI1); } static void TDC_WriteConfig(uint8_t reg_addr, uint32_t val) { uint8_t buf[4]; buf[0] (uint8_t)(val 0xFF); buf[1] (uint8_t)((val 8) 0xFF); buf[2] (uint8_t)((val 16) 0xFF); buf[3] (uint8_t)((val 24) 0xFF); TDC_CSN_LOW(); SPI1_SendByte(reg_addr); SPI1_SendByte(buf[0]); SPI1_SendByte(buf[1]); SPI1_SendByte(buf[2]); SPI1_SendByte(buf[3]); TDC_CSN_HIGH(); } static uint32_t TDC_ReadConfig(uint8_t reg_addr) { uint8_t buf[4]; uint32_t val 0; TDC_CSN_LOW(); SPI1_SendByte(reg_addr); // 读操作时后续需要发送填充字节从MISO读出数据 for (int i 0; i 4; i) { buf[i] SPI1_SendByte(0x00); } TDC_CSN_HIGH(); val (uint32_t)buf[0] | ((uint32_t)buf[1] 8) | ((uint32_t)buf[2] 16) | ((uint32_t)buf[3] 24); return val; }注意读配置寄存器的指令是0x01寄存器0到0x04寄存器3读结果寄存器是0x50~0x53。寄存器/结果数据的低位字节在前。收到数据后要按小端序拼装成32位整数。3. 测量模式1和测量模式2该用哪个以及为什么3.1 测量模式1最适合激光测距的单通道方案TDC-GP22有四种测量模式实际项目里最常用的是测量模式1Measurement Mode 1和测量模式2Measurement Mode 2。测量模式1适合单通道单次测量比如激光测距。它的工作逻辑是Start引脚收到一个上升沿芯片内部开始计数Hit1或Hit2引脚收到一个上升沿芯片停止计数。中间经过的时间就是你要测的时间间隔。这种模式的特点是分辨率和精度最高测量范围在0~1.8微秒左右配置成双通道后范围会更窄。激光测距里光速往返不需要测很长的距离这个量程完全够了。写配置寄存器0时把通道选择字段设为0Hit1通道HITIN1模式设为0正沿触发然后启动测量指令选0x11带校准或0x10不带校准。启动后芯片会等待Start信号的到来所以你的外部电路要保证Start引脚能收到激光发射的同步脉冲Hit1引脚收到回波信号。3.2 测量模式2双通道多Stop流量计场景的第一选择测量模式2适合需要同时记录多个stop信号的场景比如超声波流量计。在这种模式下一个Start脉冲开始计时最多可以依次记录多个Stop脉冲的时间戳而且可以同时使用两个停止通道。芯片内部有可配置的FIFO每个stop信号的时间戳会被依次保存下来。流量计里经常要测顺流和逆流两个方向的传播时间正好用两个通道分别对应。测量模式2的配置关键点在于寄存器0里要设置FIFO深度、重触发/非重触发模式、以及两个通道的触发极性。设置不当会导致数据串扰比如通道1的stop时间戳记录到了通道2的FIFO里。我用过一次因为FIFO深度设置成了单次模式结果第二次测量时FIFO没清空读出来的时间间隔总是带着上次的残留数据跳变得厉害。后来每次启动测量前先发一次0x00初始化指令问题就消失了。3.3 结果计算从原始数据到真实时间不管哪种测量模式读出来的原始数据都需要经过ALU换算才能变成实际时间。GP22内部有一个算术逻辑单元ALU它会把原始计数转换成以秒为单位的浮点数但前提是你得正确配置参考时钟频率。换算公式可以简化成实际时间 原始数据 / (参考时钟频率 * 时钟分频系数)比如你用4MHz的外部晶振PLL倍频到系统时钟ALU计算时会自动做归一化。如果你不想依赖ALU也可以直接用SPI读原始数据然后自己在MCU里算。算出来的值单位是秒激光测距场景再乘光速除以2就得到距离。需要特别注意不管是原始数据还是ALU结果都需要经过校准才能达到官方宣称的精度。校准分两种协方差校准Calibration of the Time Base和零漂校准Offset Calibration。前者消除的是时钟源频率偏差带来的误差后者消除的是内部比较器、施密特触发器等电路引起的固定偏移。校准操作不是可选项是必须做的我下一节展开说。4. 校准不是可选项协方差校准和零漂校准的完整流程4.1 为什么必须校准不校准的后果是什么GP22的测量原理本质上是靠内部环形振荡器的高速计数器对时间间隔计数。环形振荡器的频率会随温度和电压漂移所以原始计数值只能反映相对时间不能直接当作绝对时间用。校准单元的作用是在每次测量前或间隔一定次数后测出当前环形振荡器的实际频率然后计算出修正系数。如果不校准实测下来偏差有多大我做过一组对照实验同一块电路板同一组测量条件校准前后测同一个1纳秒左右的时间间隔结果差出好几十个皮秒。在测距场景里这个偏差对应的距离误差可能达到毫米到厘米级别。所以不用心存侥幸校准流程必须写进例程里。4.2 校准的软件操作流程校准操作在指令层面非常简单启动测量时不用0x10不带校准改用0x11带校准。但这只是表象真正的校准逻辑在芯片内部分了好几步芯片内部自动产生一个参考信号从校准单元开始经过已知延迟路径再回到停止通道。用参考时钟外部晶振和环形振荡器同时对这段已知延迟计时。比较两者的计数值算出环形振荡器的实际频率与期望频率的偏差系数。这个偏差系数会保存在芯片内部寄存器里后续的测量结果会自动应用这个修正。应用修正的开关在配置寄存器0的某个bit里我记得是Bit 13校准模式使能位。如果你启用了校准测量指令但寄存器里的使能位没设对校准单元压根不会工作校准指令只是空转了一下。4.3 校准后的精度验证方法验证校准是否生效最直接的办法是给GP22输入一个已知的时间间隔然后用示波器或信号发生器来对照。我没有高精度信号源时用了个土办法把GP22的Start和Hit1引脚分别接同一个方波信号的不同延时线一段同轴电缆电缆长度已知信号在电缆里的传播速度大约是0.66倍光速可以算出理论延时。然后对比GP22测出来的结果就能估算出校准后的实际误差。实测下来校准后单次测量的抖动jitter大概在正负几十个皮秒的范围内多次平均后稳定性会更好。如果你的项目对绝对精度要求高建议在每个测量周期都做一次校准代价是测量时间会变长一点点换来的是数据稳定性的提升这笔账是划算的。5. 硬件设计上最容易被忽视的几个坑5.1 晶振选型和布局直接决定测量精度上限GP22必须外接一个参考时钟源通常是4MHz的晶振也可以直接输入外部方波。晶振的精度直接传导到时间测量结果里。我踩过的坑是最初为了省成本用了精度只有50ppm的普通晶振结果测出来的时间基准整体偏移了0.005%左右换算成距离就是每100米偏5厘米这肯定不能接受。建议选温补晶振TCXO精度做到1ppm以内。如果预算允许可以考虑OCXO但功耗会大不少便携设备不太合适。晶振摆放要尽量靠近GP22的XIN/XOUT引脚走线短而直周围不要有高频数字信号穿过晶振下方最好铺一块完整的地铜皮做隔离。5.2 电源纹波对测量的影响比想象中大GP22的供电电压是2.5V~3.6V但内部把模拟和数字部分分开供电AVDD和DVDD。如果模拟电源的纹波过大会直接影响时间测量的稳定性。我遇到过一种情况电机启动的瞬间电源电压跌落几十毫伏GP22的测量结果顿时跳变了上百皮秒。解决办法AVDD要单独加一级LC滤波电感用磁珠就行电容加大容量的钽电容和100nF陶瓷电容并联。DVDD的纹波要求可以稍微宽松一些但也不能和电机驱动之类的大电流负载共用一路电源。另外注意GP22的VDD引脚要就近放去耦电容每个电源引脚至少一个0.1uF电容。5.3 SPI信号完整性和测量信号布线SPI的SCLK频率如果不高几MHz以内一般不用太担心信号完整性问题但有一条经验值得记住SPI的SCLK和MOSI走线不要离Start/Hit1的输入信号太近避免时钟信号串扰到测量通道上。如果布线空间受限至少在两种走线之间加一条地线做隔离。Start和Hit1信号本身对噪声非常敏感建议走微带线并且在靠近芯片引脚的地方加一个RC滤波器比如100欧姆电阻串联、10pF电容对地。不要盲目加大滤波电容因为电容会减缓信号边沿边沿变缓会让触发点位置抖动变大。触发点抖动最终会变成测量误差所以滤波要适可而止。6. 完整的TDC-GP22例程STM32F103驱动激光测距时间读取6.1 初始化流程和代码下面给出一个最精简的初始化流程适合快速验证GP22的基本功能。假设硬件连接SPI1PA5SCKPA6MISOPA7MOSICSNPA4RSTNPB0。晶振用4MHz测量模式1Hit1通道带校准。void TDC_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_SPI1, ENABLE); // SPI1 SCK, MOSI, CSN 配置为复用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // MISO 配置为上拉输入GP22的MISO是开漏 GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, GPIO_InitStructure); // 复位引脚 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_SetBits(GPIOB, GPIO_Pin_0); // RSTN拉高 GPIO_SetBits(GPIOA, GPIO_Pin_4); // CSN拉高 } void TDC_SPI_Init(void) { SPI_InitTypeDef SPI_InitStructure; SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; // CPOL0 SPI_InitStructure.SPI_CPHA SPI_CPHA_2Edge; // CPHA1 SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_16; // 72/164.5MHz SPI_InitStructure.SPI_FirstBit SPI_FirstBit_LSB; // 关键LSB先行 SPI_Init(SPI1, SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); } void TDC_Init(void) { uint32_t config0, config1; // 先复位 TDC_CSN_HIGH(); GPIO_ResetBits(GPIOB, GPIO_Pin_0); delay_ms(10); GPIO_SetBits(GPIOB, GPIO_Pin_0); delay_ms(10); // 发送初始化指令 TDC_CSN_LOW(); SPI1_SendByte(0x00); TDC_CSN_HIGH(); // 配置寄存器0 // 参考时钟4MHz测量模式1Hit1通道正沿触发启动信号正沿触发使能校准 // 具体bit值根据数据手册逐项拼凑这里给一个典型的合法值 config0 0xE0040001; // 这个值要按注释逐bit核对不要照抄 TDC_WriteConfig(0xB0, config0); // 配置寄存器1中断使能、ALU操作等初次先给一个保守值 config1 0x00000000; TDC_WriteConfig(0xB1, config1); // 可以在这里读取回寄存器验证配置是否正确写入 uint32_t chk0 TDC_ReadConfig(0x01); if (chk0 ! config0) { // 配置回读不一致多半是SPI模式或位序错了 while (1); } }6.2 单次测量与结果读取初始化完成后每次测量只需要三步发出启动测量指令带校准用0x11等待INT中断或者轮询状态寄存器读取结果并计算时间。下面是用查询方式实现的一个测量函数uint32_t TDC_Measure_TimeOfFlight(void) { uint32_t raw 0; uint32_t config0; // 确保上一次测量已结束必要时重新初始化 TDC_CSN_LOW(); SPI1_SendByte(0x00); // 重新初始化清空FIFO TDC_CSN_HIGH(); // 启动测量带校准 TDC_CSN_LOW(); SPI1_SendByte(0x11); TDC_CSN_HIGH(); // 等待测量完成最朴素的延时方式实际项目可以改成中断驱动 delay_ms(50); // 读取状态寄存器确认测量完成 uint32_t status TDC_ReadStatus(); if ((status 0x01) 0) { // BIT0 表示Hit1检测成功 return 0; // 没采到回波 } // 复位ALU后触发ALU计算 TDC_CSN_LOW(); SPI1_SendByte(0x03); // 复位ALU TDC_CSN_HIGH(); TDC_CSN_LOW(); SPI1_SendByte(0x05); // 触发ALU计算 TDC_CSN_HIGH(); // 读取结果寄存器0得到原始计数或ALU结果 raw TDC_ReadResult(0x50); return raw; }读回来的raw要根据配置换算成时间。假设使用了ALU结果模式raw本身就是以秒为单位的小数的定点表示。如果读的是原始计数公式如下时间(ps) raw * (1e12 / (晶振频率 * 分频系数 * 65536))不同固件版本和配置模式下这个换算系数不完全一样建议先用已知延时验证一下。我在实际项目里是先接一个已知长度的同轴电缆做延时校准把算出来的系数微调到一个稳定的值然后再固化到代码里。6.3 上位机联调时怎么快速验证数据调试阶段强烈建议把原始数据、换算后的时间值、距离值都通过串口打印出来。不要只打印最终距离因为一旦数据异常你没法判断是前端信号问题、时间测量问题还是换算公式问题。我习惯的格式是raw1234567, time987.65 ps, dist0.148 m这样一个格式里三个值都能看到排查效率会高很多。另外如果发现距离值有规律地跳变比如总是每隔几个数就跳一次多半不是随机噪声而是SPI读取时序问题或者测量周期里混入了多余的中断信号需要拿示波器去抓Start和Hit1的波形。7. 从例程到量产还有几件容易被忽略的事例程能跑通和产品能稳定运行之间还有一段路。我总结几个我交付项目时必做的事第一个是上电时序检查GP22的电源稳定之后必须等待晶振起振稳定一般要给个10ms以上的延时才发第一个SPI命令。如果上电就和GP22通信大概率读到0xFF或者配置不成功。第二个是看门狗策略如果使用了外部看门狗喂狗操作不要放在两个连续的测量周期之间否则会引入不确定的延时导致测量节奏错乱。第三个是温度补偿如果产品工作环境温度变化大建议在固件里做温度补偿GP22本身有温度测量通道可以读内部温度值然后针对这个温度值微调换算系数这样全温域下的精度会好看很多。最后一个经验有关调试工具。我在调GP22时非常依赖逻辑分析仪把SPI的四根线全部抓出来逐帧核对指令和数据的位序。因为GP22的LSB先行模式和大多数器件不一样光靠眼睛看代码很容易漏掉这个点。你把一个已知值的寄存器写进去再用分析仪抓到波形手动解出每一位和预期比对基本可以确认SPI这层没问题。这个习惯帮我省了大量排查时间也推荐给你。本文还有配套的精品资源点击获取
返回列表