十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32驱动TDC-GP22:硬件SPI与模拟SPI的时序选型实践

STM32驱动TDC-GP22:硬件SPI与模拟SPI的时序选型实践 简介这份压缩包提供了一套基于STM32的GP22外设SPI通信示例工程面向需要移植GP22驱动或调试SPI接口的嵌入式开发者尤其适合希望弄清硬件SPI与模拟SPI差异的读者。整个RAR压缩包仅7KB内含1个C语言源文件代码精简但覆盖SPI初始化、从设备选择、数据收发等核心流程方便直接阅读和二次开发。资源已有446人学习是理解GP22控制与STM32硬件资源配合的实用参考。源码展示了硬件SPI与模拟SPI的配置区别并解释了官方例程采用硬件SPI以避免模拟方式限制GP22处理速度的原因通过分析GP22寄存器配置与读写时序还可以掌握时钟极性、数据采样等SPI协议要点并将其迁移到其他SPI外设的开发中缩短同类外设的调试周期。1. GP22 这颗时间数字转换芯片为什么非要用 STM32 的 SPI 去读一个激光测距模块的接收端如果只靠 MCU 的定时器去抓信号边沿分辨率做到纳秒级就已经很吃力了。TDC-GP22 这类时间数字转换芯片把「时间到数字」这件事变成一颗芯片的专职工作内部延迟链能把时间差量化到几十皮秒测量结束之后通过 SPI 把结果交给主控。STM32 在这里扮演的角色很纯粹下发配置寄存器、发启动命令、等中断、搬数据。真正的精度来自 GP22 的模拟前端和延迟链STM32 只要保证 SPI 时序不捣乱就行。这个标题里最有意思的是「硬件 SPI 和模拟 SPI 并存」。GP22 的命令字只有 8 位看起来任何一片带 SPI 外设的 MCU 都能应付但在实际项目中软件模拟 SPI 反而是排查问题时的第一选择。原因是 GP22 的从机时序对 CS 释放时间、命令与数据之间的间隔有要求硬件 SPI 的连续传输往往会让 CS 保持低电平时间过长导致 GP22 把下一次的命令字节误判成上一次的数据。先把两种方式都跑通再根据测量频率决定用哪一种是这套方案里最务实的路径。这篇文章围绕 STM32 驱动 GP22 的完整链路展开GP22 的 SPI 从机特性、硬件 SPI 的最小配置、软件模拟 SPI 的时序实现以及最终测量结果的换算和调试手段。适合正在做超声波流量计、激光测距或者 AnyWay 类高精度时间测量的工程师也适合第一次接触 TDC-GP22、被 AN034 那份应用笔记折腾得无处下手的读者。2. GP22 的 SPI 从机协议与硬件、模拟两种读法的选型边界2.1 GP22 的 SPI 接口到底特殊在哪里GP22 是一颗 SPI 从机但它跟普通的 Flash 或传感器有一个明显区别命令方向和数据方向是同一个引脚对而且 CS 的时序被严格定义。发送 8 位命令字之后如果是写寄存器主控要立刻跟上数据如果是读寄存器主控要继续给出时钟GP22 在 SO 引脚上把数据吐回来。也就是说MOSI 和 MISO 的职责在一条完整的传输事务里是不对称的命令阶段走 MOSI读数据阶段走 MISO。GP22 的命令字定义不算复杂常用的是这么几组命令含义后续动作0x80写配置寄存器 0主控继续发送 32 bit 配置数据0x40读配置寄存器 0主控给时钟GP22 返回 32 bit 数据0x82写配置寄存器 1主控继续发送 32 bit 配置数据0x42读配置寄存器 1主控给时钟GP22 返回 32 bit 数据0x70初始化所有寄存器立即执行无需数据0x02启动单次测量立即执行GP22 随后拉低 INTN 表示完成这里的命令字值是常见做法AN034 应用笔记里的寄存器位定义比这复杂得多但只要命令字这个入口是对的后续读写寄存器位的操作就能按部就班地推进。GP22 的 SPI 时钟相位建议配置成 CPOL0、CPHA1也就是时钟空闲为低、数据在第二个边沿下降沿被采样。这一点如果搞错读回来的所有比特位都是错位的而且这种错误在逻辑分析仪上很难一眼看出来得逐个 bit 对照。2.2 硬件 SPI 与软件模拟 SPI 的取舍硬件 SPI 的优势是快STM32 的 SPI 外设配合 DMA 可以做到几 Mbps 的时钟速率GP22 的配置阶段这么快的速率并不是必要的。软件模拟 SPI 的优势是每一个边沿都受代码控制CS、SCLK、MOSI 三个引脚的跳变顺序可以精确安排排查问题时可以随时在中间加延时观测信号。实际项目中我一般这样选如果 GP22 只做单次测量、每次测量间隔数百毫秒以上直接用模拟 SPI省去 SPI 外设的配置和中断复杂度如果要做连续测量或者需要配合 DMA 搬运数据再切到硬件 SPI。用一张表格可以看清两者的边界对比项硬件 SPI模拟 SPI最大速率可达 18 MHz 以上通常做到 1 MHz 以内引脚占用4 个含 CS 可选4 个时序可控性受外设状态机约束完全受代码控制调试便利性需要逻辑分析仪可随时插入延时观察适合场景连续测量、DMA 搬运初始化配置、单次测量需要注意一点GP22 的测量精度与 SPI 速率无关SPI 只负责搬运配置和结果。因此把模拟 SPI 作为默认方案把硬件 SPI 当成性能优化项是更稳妥的思路也正好落入「软件模拟 SPI」这个搜索意图的覆盖范围。2.3 STM32 引脚规划与最小连接GP22 与 STM32 之间的最小连接需要五根线SCLK、MOSI、CS、INTN再加上一根可选的中断输入。GP22 的 SO 在写命令阶段是高阻态读数据阶段由 GP22 驱动主控这边只需要一个 SPI_MISO 引脚或者普通 GPIO 输入就可以。STM32 的引脚分配可以这样规划GP22 引脚STM32 引脚GPIO 模式说明SCLKPA5复用推挽输出SPI1_SCKMOSIPA7复用推挽输出SPI1_MOSICSPA4普通推挽输出软件控制片选INTNPB0外部中断输入下降沿触发RSTNPB1普通推挽输出复位引脚低有效CS 用普通 GPIO 而不是硬件 NSS 是几乎所有 GP22 项目的共同选择因为硬件 NSS 在多字节传输时的自动拉低拉高时机跟 GP22 期望的时序不匹配。INTN 是开漏输出必须在 STM32 这边配置上拉否则电平无法被正确识别。RSTN 也不是必须的但保留它可以让调试时快速把 GP22 恢复到已知状态。3. 用 STM32CubeMX 配置硬件 SPI 驱动 GP22 的最小步骤3.1 CubeMX 里 SPI 参数的具体设置用 STM32CubeMX 生成工程时SPI 参数里最容易被忽略的是 Clock Phase 和 NSS 的设置。GP22 要求在下降沿采样数据因此必须把 CPOL 设为 Low、CPHA 设为 2 Edge同时把 NSS 设为 Software。如果只改了 CPOL 没改 CPHAGP22 能收到命令但读回来的数据会出现整体移位而且状态寄存器读出来的值也会是乱的。CubeMX 里的重点配置项如下配置项值原因ModeMasterSTM32 作为主控Direction2 Lines Full Duplex读写共用一个外设Data Size8 BitGP22 命令字和数据字节均为 8 bitClock PolarityLow空闲时钟为低电平Clock Phase2 Edge第二个边沿下降沿采样NSSSoftware避免硬件 NSS 自动控制 CSBaudRate Prescaler16初始速率放慢稳定后再提速First BitMSB FirstGP22 要求高位先出生成代码后SPI 初始化函数大致是这样void MX_SPI1_Init(void) { hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; hspi1.Init.CLKPhase SPI_PHASE_2EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_16; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; HAL_SPI_Init(hspi1); }配置完成后最重要的一步是把 CS 初始化成高电平。GP22 在 CS 为高时处于待机状态如果 CS 默认低电平GP22 会误以为有命令进入状态机的行为不可预期。CubeMX 里把 PA4 初始化为 Output Push Pull 且默认 High是一个容易被忽略但必须检查的细节。3.2 写配置寄存器命令字与 32 位数据的组合GP22 的配置寄存器是 32 位宽的一次写入需要发送 5 个字节第一个字节是命令字后面四个字节是要写入的寄存器值MSB 在前。用 HAL 库实现时必须自己控制 CS 的电平不能让 SPI 外设连续传输时把 CS 一直拉低。void GP22_WriteRegister(uint8_t reg_cmd, uint32_t reg_val) { uint8_t buf[5]; buf[0] reg_cmd; buf[1] (uint8_t)(reg_val 24); buf[2] (uint8_t)(reg_val 16); buf[3] (uint8_t)(reg_val 8); buf[4] (uint8_t)(reg_val); GP22_CS_LOW(); HAL_SPI_Transmit(hspi1, buf, 5, HAL_MAX_DELAY); GP22_CS_HIGH(); }这里把命令字和数据字节打包成一个 buffer 一次发送是为了避免多次调用 HAL_SPI_Transmit 时 CS 中间出现不可控的抖动。CS 拉低后立刻发数据全部字节发送完再拉高 CSGP22 会在 CS 上升沿锁存这次事务的内容。如果需要在命令和数据字节之间插入延时必须用模拟 SPI 来实现硬件 SPI 做不到这一点。3.3 读寄存器与启动测量的实际代码读寄存器的过程是先发命令字再连续接收 4 个字节。HAL 库的 HAL_SPI_Receive 在接收时会同步发送时钟GP22 正是依靠这些时钟把数据从 SO 引脚推出来。uint32_t GP22_ReadRegister(uint8_t reg_cmd) { uint8_t tx_data reg_cmd; uint8_t rx_buf[4] {0}; uint32_t result 0; GP22_CS_LOW(); HAL_SPI_Transmit(hspi1, tx_data, 1, HAL_MAX_DELAY); HAL_SPI_Receive(hspi1, rx_buf, 4, HAL_MAX_DELAY); GP22_CS_HIGH(); result ((uint32_t)rx_buf[0] 24) | ((uint32_t)rx_buf[1] 16) | ((uint32_t)rx_buf[2] 8) | ((uint32_t)rx_buf[3]); return result; }读操作里容易踩的坑是 CS 拉低之后立刻调 HAL_SPI_Transmit此时 GP22 的 SPI 从机状态机可能还没准备好。稳妥的做法是在 CS 拉低后加一个极短的延时一般 1 微秒以内就够取决于 STM32 的系统时钟频率和 GPIO 翻转速度。启动单次测量则是把 CS 拉低、发一个 0x02、再拉高 CS 即可不需要后续数据。中断等待部分用外部中断下降沿触发 INTN回调函数里只需要置一个标志位void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin INTN_Pin) { measurement_done 1; } }GP22 测量完成时 INTN 拉低主控在 while 循环里等这个标志位即可。注意如果读取结果之前没有等待 INTN直接去读寄存器可能读到的是上一次测量的旧数据。这个时序问题在后续的高速测量中会被无限放大。4. 软件模拟 SPI 驱动 GP22 的完整实现与时序控制4.1 为什么模拟 SPI 更容易把 GP22 调通GP22 的 AN034 应用笔记里提到的很多时序指标例如 CS 释放时间、命令与数据之间的间隔在硬件 SPI 的连续传输模式下并不容易满足。HAL_SPI_Transmit 一旦开始SCLK 会连续翻转如果 GP22 规定命令字节后需要几个时钟周期的静默期硬件 SPI 在这种连续传输的架构里就无能为力了。软件模拟 SPI 则完全不同。每个字节的发送、每个时钟沿的翻转、CS 的拉低与拉高都由代码直接控制想在哪里停就可以在哪里停。对于 GP22 这种测量精度极高、但对配置时序容忍度不算宽的芯片来说模拟 SPI 是把它调通的最短路径。尤其是当读回来的状态寄存器值与预期不符时模拟 SPI 可以在关键边沿插入延迟快速定位是建立时间不足还是保持时间不足。4.2 GPIO 模拟 SPI 的初始化代码模拟 SPI 不需要配置 SPI 外设只需要把四个 GPIO 设为输出模式其中读取数据时要切换引脚方向。GP22 需要 CPOL0、CPHA1也就是 SCLK 空闲为低数据在下降沿被采样因此代码里要在 SCLK 拉高时设置 MOSI 数据在 SCLK 拉低后读取 MISO。#define GP22_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define GP22_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) #define GP22_SCLK_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET) #define GP22_SCLK_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET) #define GP22_MOSI_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_SET) #define GP22_MOSI_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_RESET) void GP22_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); GPIO_InitStruct.Pin GPIO_PIN_0; GPIO_InitStruct.Mode GPIO_MODE_IT_FALLING; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); HAL_NVIC_SetPriority(EXTI0_IRQn, 2, 0); HAL_NVIC_EnableIRQ(EXTI0_IRQn); GP22_SCLK_LOW(); GP22_CS_HIGH(); }这段初始化的关键点是必须先让 SCLK 处于低电平再拉高 CS否则 CS 上升沿和 SCLK 的毛刺重叠GP22 可能错误地解析成一个命令字节。GPIO 速度设为 HIGH 是为了让翻转沿更陡减少边沿抖动对时序的影响。INTN 配置为下降沿中断并启用内部上拉匹配 GP22 开漏输出的特性。4.3 模拟 SPI 的核心读写函数模拟 SPI 的字节发送函数把每个 bit 依次放到 MOSI 上然后产生一个完整的时钟周期void GP22_SPI_WriteByte(uint8_t data) { for (int i 7; i 0; i--) { if (data (1 i)) { GP22_MOSI_HIGH(); } else { GP22_MOSI_LOW(); } GP22_SCLK_HIGH(); delay_us(1); GP22_SCLK_LOW(); delay_us(1); } }这里在 SCLK 高电平期间保持 MOSI 数据不变等待 GP22 在下降沿采样。1 微秒的延时对应 500 kHz 的 SPI 时钟对 GP22 来说完全够用而且留出了足够的建立与保持时间。如果板子上的走线较长或者使用了杜邦线连接可以把这个延时提高到 5 微秒先把功能跑通再追求速率。读字节的函数需要在 SCLK 低电平时读取 MISO 引脚因为下降沿采样后数据已经稳定uint8_t GP22_SPI_ReadByte(void) { uint8_t rx_data 0; for (int i 7; i 0; i--) { GP22_SCLK_HIGH(); delay_us(1); GP22_SCLK_LOW(); delay_us(1); if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_6) ! GPIO_PIN_RESET) { rx_data | (1 i); } } return rx_data; }注意读字节时 GP22 的 SO 引脚连接在 PA6而 PA6 在这个函数执行期间必须是输入模式。如果在初始化时把 PA6 配成了推挽输出读回来的数据会一直为固定电平。一个常见的做法是在读寄存器函数里临时把 PA6 切换为输入读完再切回输出但更推荐的方案是把 PA6 始终配置为输入因为 GP22 的 SO 在没有数据输出时是高阻态不会对总线造成冲突。4.4 用模拟 SPI 完成一次完整测量事务一次完整的 GP22 测量事务包括写配置寄存器、启动测量、等待中断、读取测量结果最后把 CS 拉高结束事务。这三个步骤放在一个函数里可以避免外部调用者因为时序交错导致 CS 状态错乱uint32_t GP22_MeasureOnce(uint32_t config_reg0, uint32_t config_reg1) { uint32_t result 0; GP22_WriteRegister(0x80, config_reg0, 5); GP22_WriteRegister(0x82, config_reg1, 5); GP22_CS_LOW(); GP22_SPI_WriteByte(0x02); GP22_CS_HIGH(); for (int i 0; i 1000; i) { if (measurement_done) { break; } delay_ms(1); } if (measurement_done) { measurement_done 0; GP22_CS_LOW(); GP22_SPI_WriteByte(0x40); result | ((uint32_t)GP22_SPI_ReadByte() 24); result | ((uint32_t)GP22_SPI_ReadByte() 16); result | ((uint32_t)GP22_SPI_ReadByte() 8); result | ((uint32_t)GP22_SPI_ReadByte()); GP22_CS_HIGH(); } return result; }这段代码在命令字节和读取数据之间没有插入额外延时因为 GP22 在读命令发出后立即开始驱动 SO 引脚。如果读回来的数据全是 0xFF 或者 0x00优先检查 4.2 节的 GPIO 初始化和 CS 时序不要急着改命令字。5. 测量结果换算、睡眠唤醒与三个高频排错技巧5.1 32 位原始值换算成实际时间差GP22 返回的 32 位数值不是纳秒也不是皮秒而是内部环形振荡器的计数结果。换算公式常见的是float time_ns (float)raw_value * 0.09f;其中 0.09 表示在每个测量分辨率配置下一个 LSB 对应的典型时间值单位是纳秒。AN034 中提到的典型配置下 GP22 的分辨率约为 90 ps也就是原始计数值每增加 1时间差增加 0.09 ns。如果配置了内部预分频器或者使用了不同的测量范围这个系数要相应调整具体数值必须从配置寄存器的位定义推导。换算后的时间差在激光测距里要除以 2 才是单程距离对应的时间因为光脉冲走了往返路径。这个除法千万不要忘记否则测出来的距离会整整翻一倍。5.2 睡眠模式下的唤醒与重新初始化顺序GP22 支持睡眠模式来降低功耗进入睡眠的常用方式是配置寄存器中的相关位。睡眠状态下 SPI 接口仍然保持但测量单元完全断电。唤醒后不要直接发启动测量命令因为内部延迟链和校准电路的温度状态已经发生变化直接测量可能导致结果偏差。可靠的唤醒流程是先复位 GP22再重新写一遍两份配置寄存器然后等待一小段时间让内部参考稳定最后才启动测量。这个重新初始化的顺序可以用模拟 SPI 完整复现因为它保证了 CS 的每次释放都在正确的字节边界上。硬件 SPI 在睡眠唤醒场景下的表现不如模拟 SPI 稳定这也是很多量产固件保留模拟 SPI 的原因。5.3 高频排错技巧状态寄存器回读、示波器触发与 CS 边沿遇到 GP22 读不到数据时下面三个手段按顺序使用基本能把问题范围缩小到具体引脚或时序参数读状态寄存器是最快的自检手段。GP22 的状态寄存器反映了当前的工作状态和校准完成标志如果读回的值全为 0说明命令字都没有被正确接收问题出在 SCLK 极性或者 MOSI 数据线上。如果读回的值有合理变化但测量结果始终为 0说明命令通路正常问题在测量配置寄存器上。这一步用 3.3 节的读寄存器函数即可完成。示波器抓时序时把触发条件设为 CS 下降沿单次触发模式下查看 CS 拉低到第一个 SCLK 上升沿之间的间隔。GP22 对 CS 建立时间有要求如果这个间隔太短GP22 的片选逻辑可能不会正确启动。同时观察最后一个 SCLK 下降沿到 CS 上升沿的持续时间这个保持时间不足会导致数据锁存失败。遇到 SPI 接口调试异常先看这两个时间再谈其他。最后一个技巧是给 CS 加宽释放时间。GP22 的 CS 在两次事务之间必须保持高电平一段时间常见的实现是在每次 CS 拉高后加 10 微秒延时。如果连续快速读写导致 GP22 状态错乱优先排查这个时间是否足够。把 CS 释放延时作为参数独立封装调试时就能快速区分是 GP22 状态机问题还是 STM32 时钟配置问题。本文还有配套的精品资源点击获取
返回列表