简介:这份资源面向嵌入式开发初学者与需要快速上手的工程师,提供STM32F103驱动VL53L0X飞行时间测距传感器的完整IIC方案,解决从硬件接口到数据读取的落地问题。压缩包共238个文件,约8.28MB,以c源码、h头文件、o目标文件与d依赖文件为主,另含uvprojx工程、sct链接脚本、hex烧录文件及map映射文件,覆盖编译、链接与调试全流程。资源包内包含VL53L0X_API函数库、IIC驱动、HAL库文件、技术文档与用户示例,工程可直接在KEIL UV5中编译运行。已有1724人学习下载,读者可借此掌握IIC总线初始化、测距启动与距离获取等API调用,理解驱动分层与工程组织方式,并积累传感器调试与排错经验,便于迁移到其他ToF或IIC外设项目。
1. 从一次“翻车”说起:为什么这颗 2 块钱的激光测距芯片值得单独写驱动
去年帮一个做智能小车的老哥调避障,他图省事买了一批 VL53L0X 模块,结果 IIC 死活读不出稳定数据,串口打印的距离值在 0 和 8190 之间反复横跳,换了两块板子都一样。他一度怀疑是芯片坏了,后来发现是上电后没等内部固件加载完就开始读,时序全乱。这个场景我后来在论坛上至少见过二十次——VL53L0X 本身不难,难的是它跟普通 IIC 传感器不一样,它内部跑着一颗小 MCU,有固件、有状态机、有校准流程,你把它当 MPU6050 那样直接读寄存器,大概率翻车。
这篇笔记拆的就是基于 STM32F103 的 VL53L0X IIC 驱动资源包。它解决的核心问题很具体:用标准库或 HAL 库在 F103 上把 VL53L0X 跑通,拿到毫米级距离数据,并且能稳定连续输出。适合正在做避障小车、液位检测、手势识别或者任何需要短距测距的嵌入式开发者。如果你手里正好有蓝 pill 板或者最小系统板,跟着走一遍,半小时内能看到串口出数。
2. VL53L0X 的 IIC 不是“标准” IIC:先搞懂它到底特殊在哪
2.1 它内部有一颗 MCU,寄存器只是门面
VL53L0X 是 ST 的 ToF 激光测距芯片,测量范围标称 2 米,实际在室内白墙环境下 1.2 米以内比较稳。它跟普通 IIC 传感器最大的区别在于:芯片内部跑着一套完整的测距固件,你通过 IIC 读到的 0x14 寄存器里的距离值,是内部 MCU 算完之后放到那儿的。上电后它需要一段时间加载固件和初始化,这个时间手册里写的是 1.2ms 左右,但实际用 F103 的硬件 IIC 跑,如果上电就立刻发 START,经常拿到 0xFF 或者 NACK。
常见做法是在初始化函数里加一个 2ms 的延时,然后再去读 0xC0 寄存器的值,确认它等于 0xEE。这个值相当于“我准备好了”的信号。很多网上流传的驱动代码省略了这一步,导致部分批次的模块时好时坏,这就是玄学来源之一。
2.2 写寄存器要 16 位地址,读寄存器是另一套时序
VL53L0X 的寄存器地址是 8 位的,但写的时候要拆成两个字节发:先发高 8 位地址,再发低 8 位地址,然后才是数据。读的时候更绕:先写地址,再 restart,再读数据。F103 的硬件 IIC 在处理这种“写地址-重启-读”的时序时,如果直接用 HAL_I2C_Mem_Read 有时候会卡在 BUSY 状态。我一般会建议用软件模拟 IIC,时序完全可控,GPIO 随便选,移植起来反而省心。
资源包里给的是软件 IIC 版本,SDA 接 PB7,SCL 接 PB6,这个组合在 F103 上很常见,也方便你改成其他引脚。软件 IIC 的延时用 __NOP() 或者简单的 for 循环都行,F103 跑 72MHz 的时候,延时 2 微秒左右就能跑到 100kHz 左右,VL53L0X 支持到 400kHz,但软件模拟没必要跑那么快,稳定优先。
2.3 测距模式决定速度和精度,不是越快越好
VL53L0X 有三种测距模式:单次、连续、定时。资源包里默认用的是单次测量,每次调用测距函数触发一次,然后等中断或者轮询状态位。单次模式最稳,适合低速采集。如果你要做连续测距,需要配置 0x00 寄存器的值,并且注意测量周期不能小于 20ms,否则内部固件会返回上一帧的旧数据,看起来就像“数据没更新”。
提示:连续模式下如果周期设得太短,读到的距离值会重复,这不是 IIC 的问题,是芯片内部还没算完。
3. 把驱动移植到 F103:从 GPIO 到第一个距离值的完整步骤
3.1 软件 IIC 的底层时序实现
资源包里的 IIC 底层是独立的 iic.c 和 iic.h,不依赖 HAL 的 I2C 外设,只用 GPIO。先看起始和停止信号的写法:
// iic.c 软件 IIC 起始信号 void IIC_Start(void) { SDA_OUT(); // SDA 切换为输出 IIC_SDA = 1; // 拉高 SDA IIC_SCL = 1; // 拉高 SCL IIC_Delay(2); // 延时约 2us,对应 100kHz 左右 IIC_SDA = 0; // SDA 拉低,产生起始条件 IIC_Delay(2); IIC_SCL = 0; // 钳住 SCL,准备发送数据 IIC_Delay(2); }逻辑说明:起始信号要求在 SCL 高电平期间 SDA 从高变低。这里先拉高 SDA 和 SCL,延时后拉低 SDA,再拉低 SCL 准备后续数据传输。参数 IIC_Delay(2) 里的 2 是循环次数,实际延时跟主频有关,F103 在 72MHz 下大约 2 微秒。如果你超频或者降频,这个值要跟着调,否则时序会偏。
发送一个字节和等待 ACK 的代码:
// 发送一个字节并等待应答 uint8_t IIC_SendByte(uint8_t txd) { uint8_t t; SDA_OUT(); IIC_SCL = 0; for(t = 0; t < 8; t++) { IIC_SDA = (txd & 0x80) >> 7; // 先发最高位 txd <<= 1; IIC_Delay(2); IIC_SCL = 1; // 拉高 SCL,从机采样 IIC_Delay(2); IIC_SCL = 0; // 拉低 SCL,准备下一位 IIC_Delay(2); } SDA_IN(); // SDA 切输入,准备读 ACK IIC_SDA = 1; // 释放 SDA IIC_Delay(2); IIC_SCL = 1; // 拉高 SCL,从机拉低 SDA 表示应答 IIC_Delay(2); t = IIC_SDA; // 读 SDA 电平 IIC_SCL = 0; IIC_Delay(2); return t; // 0 表示收到 ACK,1 表示 NACK }参数说明:txd 是要发送的字节,函数返回 0 表示从机应答,返回 1 表示无应答。如果返回 1,常见原因是地址不对或者从机没上电。VL53L0X 的 7 位地址是 0x52,左移一位变成 0xA4 写地址,0xA5 读地址。
3.2 VL53L0X 初始化:写配置和读 ID 的顺序不能乱
初始化函数是资源包里最关键的部分,顺序错了后面全白搭。我一般按这个流程走:
// vl53l0x.c 初始化 uint8_t VL53L0X_Init(void) { IIC_Init(); // 先初始化 GPIO delay_ms(2); // 等芯片内部固件加载 if(VL53L0X_Read_ID() != 0xEE) // 读 0xC0 寄存器确认 ID return 1; // 返回 1 表示初始化失败 VL53L0X_Write_Reg(0x88, 0x00); // 设置测量模式相关寄存器 VL53L0X_Write_Reg(0x80, 0x01); VL53L0X_Write_Reg(0xFF, 0x01); VL53L0X_Write_Reg(0x00, 0x00); // ... 省略部分配置,资源包里有完整序列 VL53L0X_Write_Reg(0x91, 0x3C); // 设置测量周期上限 VL53L0X_Write_Reg(0x00, 0x01); // 触发单次测量 return 0; }逻辑说明:先延时 2ms 等固件就绪,然后读 0xC0 寄存器,正常返回 0xEE。如果读不到,后面所有配置都是无效的。配置序列里 0x88 和 0x80 是设置测量模式,0xFF 和 0x00 是进入默认配置,0x91 设置周期上限。这些值来自 ST 官方 API 的移植,资源包里已经整理成表格,照着写就行。
注意:读 ID 之前一定要先执行 IIC_Init(),GPIO 没配好就读,SDA 可能被拉死,读出来永远是 0。
3.3 触发测距和读取距离值的完整流程
单次测距的流程是:写 0x00 寄存器触发,然后轮询 0x13 寄存器的 bit0,等它变成 1 表示测量完成,再去读 0x1E 和 0x1F 两个字节拼成 16 位距离值。
// 单次测距,返回距离值,单位 mm uint16_t VL53L0X_GetDistance(void) { uint8_t val = 0; uint16_t distance = 0; VL53L0X_Write_Reg(0x00, 0x01); // 触发单次测量 while(1) { VL53L0X_Read_Reg(0x13, &val); // 读状态寄存器 if(val & 0x01) break; // bit0 为 1 表示测量完成 } VL53L0X_Read_Reg(0x1E, &val); // 读距离高字节 distance = val << 8; VL53L0X_Read_Reg(0x1F, &val); // 读距离低字节 distance |= val; return distance; }参数说明:0x13 是中断状态寄存器,bit0 是“测量完成”标志。0x1E 和 0x1F 分别是距离值的高 8 位和低 8 位。返回值的单位是毫米,量程内有效。如果返回 8190 或者 8191,表示测量超时或者信号太弱,不是有效距离。
4. 避坑与排查:这五个问题我几乎每次都能遇到
4.1 读出来永远是 0 或者 8190
现象:串口打印距离值,要么全是 0,要么全是 8190,偶尔跳一下。 原因:8190 是 VL53L0X 内部固件在测量失败时返回的默认值,通常是因为目标太远、太暗或者反射率太低。0 则多半是 IIC 通信根本没成功,读到的寄存器值全是 0xFF 或者 0x00。 解决:先确认 IIC 地址对不对,用逻辑分析仪抓一下波形,看从机有没有 ACK。如果 ACK 正常,再检查测量模式配置,把 0x91 的值改大一点,延长测量周期。室内测白墙,距离控制在 1 米以内。
4.2 初始化时读 ID 返回 0xEE 但后续配置写不进去
现象:读 0xC0 能拿到 0xEE,但写 0x88 之后再读回来还是旧值。 原因:VL53L0X 的寄存器写操作需要先发高 8 位地址再发低 8 位地址,如果地址发送顺序反了,写进去的就是错误位置。 解决:检查 Write_Reg 函数里地址的拆分顺序,资源包里是IIC_SendByte(reg >> 8); IIC_SendByte(reg & 0xFF);,先高后低。有些网上代码写成先低后高,在 F103 上跑就会出问题。
4.3 连续测量时数据不更新
现象:连续模式下读到的距离值一直不变,像卡住了一样。 原因:测量周期设得太短,内部固件还没算完就触发了下一次,读到的还是上一帧的旧数据。 解决:把 0x91 寄存器的值从 0x3C 改成 0x50 或者更大,给固件留够计算时间。或者干脆用单次模式,每次触发后等中断标志再读,最稳。
4.4 软件 IIC 的延时不够导致 NACK
现象:逻辑分析仪上看波形,SCL 频率跑到 200kHz 以上,从机不响应。 原因:IIC_Delay 的循环次数太少,F103 主频高的时候,几个 NOP 就过去了,实际频率远超 100kHz。 解决:把 IIC_Delay 的参数从 2 改成 5 或者 10,用示波器或者逻辑分析仪确认 SCL 频率在 100kHz 左右。软件 IIC 没必要追求速度,稳定第一。
4.5 上电后第一次测距总是失败
现象:每次上电后第一次调用 GetDistance 返回 8190,第二次开始正常。 原因:芯片内部固件加载需要时间,虽然初始化里加了 2ms 延时,但第一次触发测量时内部状态机还没完全就绪。 解决:在初始化函数最后加一次“空测”,触发一次测量但不读结果,等 10ms 后再开始正式采集。这个习惯我后来一直保留,能省掉很多“第一次不准”的麻烦。
5. 进阶技巧:用中断代替轮询,把 CPU 时间省出来
轮询 0x13 寄存器的方式简单,但会占着 CPU 空转。F103 虽然不差这点时间,但如果你同时要跑电机控制或者串口协议,轮询就会拖慢主循环。VL53L0X 的 GPIO1 引脚可以配置成测量完成中断输出,接在 F103 的 EXTI 上,测距完成触发外部中断,在中断里读距离值,主循环该干嘛干嘛。
资源包里预留了 GPIO1 的配置接口,默认没启用。我一般会这样改:
// 在初始化里配置 GPIO1 为中断输出 VL53L0X_Write_Reg(0x0A, 0x04); // 设置 GPIO1 为“新测量完成”中断 // 然后在 F103 侧配置 EXTI 下降沿触发 EXTI_Init(GPIO_Pin_0, EXTI_Trigger_Falling);中断服务函数里只做一件事:读 0x1E 和 0x1F,把距离值存到全局变量,清中断标志。主循环里直接读全局变量就行,不用等。
验证方法很简单:用示波器同时抓 GPIO1 和 SCL,看每次测量完成后 GPIO1 是不是拉低一次。如果 GPIO1 一直不动,检查 0x0A 寄存器的值有没有写对,有些模块的 GPIO1 默认是开漏输出,需要外部上拉电阻。
还有一个技巧是校准。VL53L0X 出厂有偏移,不同模块之间差个几毫米很正常。如果你做液位检测这种对精度要求高的场景,可以拿卷尺量一个固定距离,比如 100mm,然后读回来是多少,把差值写进代码里做补偿。我一般会在初始化后加一个校准函数,把偏移量存到 F103 的 Flash 里,下次上电直接读。
从那以后我每次调 VL53L0X,都强制走一遍“读 ID → 空测 → 校准 → 正式采集”的流程,再也没出现过上电第一次不准的情况。希望帮到你。
本文还有配套的精品资源,点击获取