VL53L0X 测距传感器使用记录
VL53L0X 测距传感器测试使用说明...... by 矜辰所致
前言
最近代理商告知以前使用的测距传感器 VL6180 公司已经宣告停产了,那么咱就得找一款替代品作为测距产品的探头了,推荐了 VL53L4 和 VL53L0X 系列,考虑到功耗问题,决定选用低功耗的 VL53L0X 。
那本文的我们的内容就是来说明一下 VL53L0X 的测试使用情况。
以前关于 TOF 测距传感器相关的文章有:
ToF 测距传感器 VL6180 使用踩坑记(软件 I2C)
ToF 测距传感器 VL6180 测量范围修改(软件 I2C)
ToF 测距传感器 VL53L5CX 使用记录
使用 C# 设计ToF测距传感器 VL53L5CX 上位机软件
.
我是矜辰所致,全网同名,尽量用心写好每一系列文章,不浮夸,不将就,认真对待学知识的我们,矜辰所致,金石为开!
目录
- 前言
- 一、 基本准备
- 1.1 传感器板子设计
- 1.2 设计上的一点小纠结
- 1.3 MCU底板设计
- 二、程序设计
- 2.1 STM32CubeMX 设置
- 2.2 ☆ API 移植 ☆
- 2.2.1 通用驱动
- 2.2.2 vl53l0x_i2c_platform.c
- 2.2.3 vl53l0x_platform.c
- 三、测试
- 3.1 测试效果展示
- 3.2 一些使用说明
- 3.2.1 校准问题
- 3.2.2 使用环境问题
- 3.2.3 测量盲区
- 结语
一、 基本准备
其实在此之前我已经使用过 VL6180 和 VL53L5CX 系列,我们知道对于这些传感器, ST 官方都已经给出了 SDK 包,有些甚至可以直接使用 STM32CUBEMX 就可以一键生成测试代码。
本次的测试首先肯定是在 STM32 上面进行基本的测试,方便调试。 但是因为后期需要在 51 单片机上使用这个传感器,所以还必须研究下怎么移植一下代码。
因为有了前面使用传感器的经验,所以我们也不去买某宝现成的模块了,因为本来也不复杂,直接就结合自己产品需要,直接自己画板子测试了。
1.1 传感器板子设计
根据 VL53L0X 的使用手册,有使用原理图:
这里实际上和以前我们画过的 VL6180 也差不多:
所以我们直接在以前传感器小板子的原理图上修改一下:
改完以后看了下资料发现有点小纠结。
1.2 设计上的一点小纠结
为什么会有一点小纠结,因为我测试使用 STM32 ,用的是 3.3V 的系统,而我最终要用的平台是一款 51单片机,1.8V 的系统。
我们可以看到以前的VL6180 的原理图推荐:
在 VL53L0X 的推荐图上用写的是 IOVDD,然后通过搜索找到 IOVDD 的相关说明,发现默认为 1.8V 模式,感觉上来说,GPIO1 和 XSHUT 处于默认上拉倒 1.8V 模式,如果上拉倒2.8V还需要额外设置:
通过 VL53L0X 的手册来说,SDA,SCL,XSHUT 和 GPIO1 好像需要保存在同一电平,要么都是 2.8V 要么都是 1.8V ?
看上去和 VL6180 差不多,但是在以前 VL6180 的手册上面好像没有特别的说明,要保持在同一电平,所以在上面原理图可以看到 SDA SCL 小板子上是直接输出的,没有上拉,而 GPIO1 和 XSHUT 是上拉到 2.8V 的,因为以前的 VL6180 是这么设计来用的,在底板上,以前的VL6180 的 SDA 和 SCL 是上拉到 1.8V 的,也能正常使用。
某宝买的 VL53L0 小模块他都是上拉到 2.8V ,所以它上面会做好电平转换,它做的电平转换,主要是针对大部分 3.3V 的供电系统的。但是对于主要需要设计的产品,我使用的 MCU 的 IO 口是1.8V的, 即便做电平转换,方向与某宝的模块也是反的,因为我的 MCU 这边是电压低的一边,无法直接用某宝的模块。 而且我的 MCU 电压而且我也可以提供 1.8V 的电源以供 SDA 和 SCL 上拉,而且如果使用 1.8V 做 VL53L0 的 IOVDD ,我都可以省去电平转换。
想来想去,干脆小板子传感器 IO 都不上拉,在MCU 端自己做上拉和根据需要做电平转换。
所以图直接画成这样:
最后
1.3 MCU底板设计
传感器小板子简单的搞好了,那么我们测试板子还是用最常用的 STM32F103系列,那整体来说就是下面的图片,因为最小系统图我以前方案都上过了,这里简单看看:
其中与 VL53L0X 有关的部分我放大如下图,做了下电平转换,唯一需要注意的就是 电平转换的方向问题:
其中I2C接口连接到了硬件 I2C 的 PB6、PB7引脚上,这样我们还开始可以直接使用 ST 提供的软件包直接测试。
当然,我们除了使用标准的 SDK 包测试,我们还需要实现软件 I2C 的测试,因为我还需要一直到 51 单片机上去使用。
二、程序设计
传感器的测试,我们首先是通过 ST 官方提供的软件包测试的,我的计划是参考 VL53L5CX 使用记录 来进行。
2.1 STM32CubeMX 设置
基本设置我们就不在赘述了,以前好些文章都讲得很详细,时钟,调试接口,串口,根据原理图需要用到的 IO 口,硬件 I2C 接口等等。
完成上述步骤,我本想着直接在 CubeMX 里面找软件包,但是发现居然没有:
没有软件包的话,查了一下资料,只能自己移植官方的 API了。
既然要移植的话,那我们得修改一下 I2C 接口,不使用硬件 I2C ,直接把 SDA 和 SCL 设置为开漏输出,同时把 XSHUT 的 IO 口也定义一下,GPIO1 是传感器采集成功后的中断输出 IO ,我们移植包括后期使用基本都是单次测量,所以也不用。
修改完成后先生成工程,博主采用的是 GCC + VScode 开发 STM32 ,所以最后生成工程,稍微改一下 Makefile ,然后编译一下:
OK,程序基本框架准备好了,下面要开始官方 API 移植。
2.2 ☆ API 移植 ☆
我们先下载 VL53L0 软件包,ST官方的下载地址:ST官方 VL53L0 传感器API
下载好 解压出来有一个 VL53L0X_1.0.4
文件夹,这些年 API 都是这个版本没变过。
移植的装备工作其实我也参考了很多文章和视频,这里细节就不多赘述,我们直接开始尝试。
第一步,把 Api 文件夹拷贝至我们的工程,当然你可以重命名一下,我这里就直接拷贝过来:
然后我们添加文件尝试编译一下,因为已经参考过资料,所以先把 core 下面的文件添加,按理来说,这部分是直接编译没有问题的,如下图:
添加完以后编译一下,有警告,但是能够编译通过:
接下来就是来添加看看 platform 下面的东西了,根据参考过的资料,所谓移植,也就是我们要将 platform 下面的东西根据自己的硬件调整。 对于上面 通用的 API ,我们不用去修改,到时候直接调用。
移植的时候我也参考过很多文章和下载了很多的代码,都有可以直接用的,但是确实没有一步一步怎么操作来说明的,所以我这边还是根据自己的想法来走了一遍,当然,有些例程其实稍微修改一下 IO 口,直接跑人家的示例出结果还是不难的。只是我要考虑到后期,我还得放在 51 平台,必须得自己走一遍整的。
自己也走了一些弯路,中间一些过程省略,我们直接删除 platform 下面的几个文件,只留 2 个,其实一个不留也是可以,完全按照我们自己的来也没问题,但是这里考虑到和 API 的连接性,我们还是按照 platform 下面的函数原型来修改。
如下图,我们只留下2个文件, 其中vl53l0x_i2c_platform.c
是操作 I2C 读取相关 和一些延时什么的,而 vl53l0x_platform.c
主要用于提供 API 库通过 IIC 访问 VL53L0X 的操作函数 ,它会调用到vl53l0x_i2c_platform.c
里面的一些函数。
- 我们直接把
vl53l0x_i2c_platform.c
所有的函数内容都删除(不是删除函数,是直接返回值,当然记得要备份,后期还要一点一点加上去)防止报错,主要修改的就是这个文件,我后面直接上代码; - 不需要的那些头文件,比如那个什么
Windows.h
都去掉。 - 当然头文件路径什么的不要忘了添加,我这里在 Makefile 里面添加。
在vl53l0x_platform.c
里面,去掉了 Windows.h
,去掉了打印 LOG 相关,还有一个延时函数,也给他改掉:
保证编译没有错误了,我们再开始添加代码:
在添加代码之前我们要明白,我们使用软件 I2C 通讯,我们分为 I2C 通讯的通用驱动 和 与传感器有关的驱动,通用驱动就包括了 ,I2C 启动,结束,发送等待 ACK ,发送不等待 ACK ,读取,等待 ACK 等等这些函数,这些在我以前一直 I2C 通讯的文章中都有提到,当然,通用驱动与我们的硬件是有关系的,直接操作 硬件的 IO 口实现的。
大家手头肯定也有以往的可以参考的 I2C 驱动代码,就不太多说,我们直接新建 2 个文件,放 I2C 通用驱动。
放在 API 文件夹下面:
这里就直接上一下源码,其实基本和以前的一样。
2.2.1 通用驱动
my_i2c_com.c
#include "my_i2c_com.h"void delay_us(uint32_t Delay)
{uint32_t cnt = Delay * 12; // 72Mhz ,其他频率其他倍数uint32_t i = 0;for(i = 0; i < cnt; i++)__NOP();
}// ------------------------------------------------------------------
void i2c_init(void) {// the SDA and SCL pins are defined as input with pull up enabled// pins are initialized as inputs, ext. pull => SDA and SCL = high}
// ------------------------------------------------------------------
// send start sequence (S)void i2c_start(void) { SDA_HIGH; delay_us(5);SCL_HIGH; delay_us(5);SDA_LOW; delay_us(5);SCL_LOW; delay_us(5);
}// ------------------------------------------------------------------
// send stop sequence (P)
void i2c_stop(void) { SCL_LOW; SDA_LOW; // delay_us(5); delay_us(5); SCL_HIGH; delay_us(5); SDA_HIGH;delay_us(5);
}/******************************************************************* 函 数 返 回:0有应答 1超时无应答
******************************************************************/
u8 i2c_wait_ack(void)
{u8 ucErrTime=0;SDA_HIGH;delay_us(5); //MCU DATA 置高,外面高就是高,外面低就是低SCL_HIGH;delay_us(5); //CLK 高电平期间数据有效while(IIC_DATA_STATE) //低电平为有应答,高电平无应答 {ucErrTime++;if(ucErrTime>250){i2c_stop();return 1;} }SCL_LOW;delay_us(5);return 0;
}void iic_ack(void)
{SCL_LOW; //SCL为低,SDA为低,SCL为高,SDA为低,应答低电平有效,SCL为低,产生应答信号// MYSDA_OUT;SDA_LOW;delay_us(5);SCL_HIGH;delay_us(5);SCL_LOW;delay_us(5);SDA_HIGH;
}void iic_nack(void)
{SCL_LOW; //SCL为低,SDA为高,SCL为高,SCL为低// MYSDA_OUT;SDA_HIGH;delay_us(5);SCL_HIGH;delay_us(5);SCL_LOW;
}//读1个字节,ack=1时,发送ACK,ack=0,发送nACK
u8 i2c_read_byte(unsigned char ack)
{unsigned char i,receive=0;// MYSDA_IN;//SDA设置为输入for(i=0;i<8;i++){SCL_LOW; //SCL为由低变高,在SCL高的时候去读 SDA的数据delay_us(4);SCL_HIGH;receive<<=1; //第一次这里还是0,第二次开始每次接收的数据做移动一位,从高位开始接收if(IIC_DATA_STATE)receive++; //如果数据为1,++以后就是1,数据为0,不执行就是0; delay_us(5); } if (!ack)iic_nack();//发送nACKelseiic_ack(); //发送ACK return receive;
}//IIC发送一个字节void i2c_send_byte(u8 txd)
{ u8 t; // MYSDA_OUT; SCL_LOW; //拉低时钟开始数据传输 ,SCL为低,SDA变高或者变低(数据位),SCL变高,SCL变低,期间SDA为1既1,为0既0for(t=0;t<8;t++) //一个字节8位,一位一位发送{ SCL_LOW;if((txd&0x80)>>7) //从最高位开始发送,如果是1,发送高电平SDA_HIGH;elseSDA_LOW;delay_us(5); //对TEA5767这三个延时都是必须的SCL_HIGH;delay_us(5); SCL_LOW; delay_us(5);txd<<=1; //SDA处理完毕,此时可以将SCL拉高接受数据,拉高以后延时拉低}
}
my_i2c_com.h
#ifndef _I2C_H_INCLUDED
#define _I2C_H_INCLUDED#include "main.h"
#include "Datadef.h"// ------------------------
#define DONOTHING() {;}// ------------------------
// command's
#define I2C_WRITE 0
#define I2C_READ 1
#define I2C_ACK 0
#define I2C_NACK 1#define SCL_HIGH HAL_GPIO_WritePin(SCL_GPIO_Port,SCL_Pin,GPIO_PIN_SET)
#define SCL_LOW HAL_GPIO_WritePin(SCL_GPIO_Port,SCL_Pin,GPIO_PIN_RESET) #define SDA_HIGH HAL_GPIO_WritePin(SDA_GPIO_Port,SDA_Pin,GPIO_PIN_SET)
#define SDA_LOW HAL_GPIO_WritePin(SDA_GPIO_Port,SDA_Pin,GPIO_PIN_RESET)
#define IIC_DATA_STATE HAL_GPIO_ReadPin(SDA_GPIO_Port,SDA_Pin)
#define sda_read() HAL_GPIO_ReadPin(SDA_GPIO_Port,SDA_Pin)void delay_us(uint32_t Delay);void i2c_init(void);
void i2c_start(void);
void i2c_stop(void);u8 i2c_wait_ack(void);
void iic_nack(void);
void iic_ack(void);
void i2c_send_byte(u8 txd);
u8 i2c_read_byte(unsigned char ack);#endif
2.2.2 vl53l0x_i2c_platform.c
写好通用的 I2C 驱动以后,我们还需要使用我们的通用驱动把 vl53l0x_i2c_platform.c
里面的函数补充完整。
这里有个问题需要说明,就是 地址变量 address 在函数中是否要左移一位的问题,这个要注意自己移植的程序把 VL53L0X 的地址定义为 0x29 还是定义为 0x52 ,因为不同人移植的驱动习惯不一样,要不要把读写位定义在地址上这个看自己,不要盲目的直接移植而忽略这个地方导致移植不成功 。
直接上一下 vl53l0x_i2c_platform.c
代码 :
/**/#include <stdio.h> // sprintf(), vsnprintf(), printf()#ifdef _MSC_VER
#define snprintf _snprintf
#endif#include "vl53l0x_i2c_platform.h"
#include "vl53l0x_def.h"
#include "my_i2c_com.h"char debug_string[VL53L0X_MAX_STRING_LENGTH_PLT];#define MIN_COMMS_VERSION_MAJOR 1
#define MIN_COMMS_VERSION_MINOR 8
#define MIN_COMMS_VERSION_BUILD 1
#define MIN_COMMS_VERSION_REVISION 0#define STATUS_OK 0x00
#define STATUS_FAIL 0x01int32_t VL53L0X_comms_initialise(uint8_t comms_type, uint16_t comms_speed_khz)
{// 这里是硬件初始化,我们在 GPIO 口初始化的时候已经把这个完成了,这里就空着return STATUS_OK;
}int32_t VL53L0X_comms_close(void)
{return STATUS_OK;
}int32_t VL53L0X_write_multi(uint8_t address, uint8_t index, uint8_t *pdata, int32_t count)
{int32_t i; i2c_start();// i2c_send_byte((address << 1) | 0);i2c_send_byte(address| 0);if (i2c_wait_ack() == 1){i2c_stop();return STATUS_FAIL;}i2c_send_byte(index);if (i2c_wait_ack() == 1){i2c_stop();return STATUS_FAIL;}for (i=0; i<count; i++){i2c_send_byte(pdata[i]);if (i2c_wait_ack() == 1){i2c_stop();return STATUS_FAIL;}}i2c_stop();return STATUS_OK;
}int32_t VL53L0X_read_multi(uint8_t address, uint8_t index, uint8_t *pdata, int32_t count)
{i2c_start();// i2c_send_byte((address << 1) | 0);i2c_send_byte(address| 0);if (i2c_wait_ack() == 1){i2c_stop();return STATUS_FAIL;}i2c_send_byte(index);if (i2c_wait_ack() == 1){i2c_stop();return STATUS_FAIL;}i2c_start();// i2c_send_byte((address << 1) | 1);i2c_send_byte(address| 1);if (i2c_wait_ack() == 1){i2c_stop();return STATUS_FAIL;}while (count){*pdata = i2c_read_byte((count > 1) ? 1 : 0);count--;pdata++;}i2c_stop();return STATUS_OK;
}int32_t VL53L0X_write_byte(uint8_t address, uint8_t index, uint8_t data)
{int32_t status = STATUS_OK;const int32_t cbyte_count = 1;status = VL53L0X_write_multi(address, index, &data, cbyte_count);return status;}int32_t VL53L0X_write_word(uint8_t address, uint8_t index, uint16_t data)
{int32_t status = STATUS_OK;uint8_t buffer[BYTES_PER_WORD];// Split 16-bit word into MS and LS uint8_tbuffer[0] = (uint8_t)(data >> 8);buffer[1] = (uint8_t)(data & 0x00FF);status = VL53L0X_write_multi(address, index, buffer, BYTES_PER_WORD);return status;
}int32_t VL53L0X_write_dword(uint8_t address, uint8_t index, uint32_t data)
{int32_t status = STATUS_OK;uint8_t buffer[BYTES_PER_DWORD];// Split 32-bit word into MS ... LS bytesbuffer[0] = (uint8_t) (data >> 24);buffer[1] = (uint8_t)((data & 0x00FF0000) >> 16);buffer[2] = (uint8_t)((data & 0x0000FF00) >> 8);buffer[3] = (uint8_t) (data & 0x000000FF);status = VL53L0X_write_multi(address, index, buffer, BYTES_PER_DWORD);return status;
}int32_t VL53L0X_read_byte(uint8_t address, uint8_t index, uint8_t *pdata)
{int32_t status = STATUS_OK;int32_t cbyte_count = 1;status = VL53L0X_read_multi(address, index, pdata, cbyte_count);return status;
}int32_t VL53L0X_read_word(uint8_t address, uint8_t index, uint16_t *pdata)
{int32_t status = STATUS_OK;uint8_t buffer[BYTES_PER_WORD];status = VL53L0X_read_multi(address, index, buffer, BYTES_PER_WORD);*pdata = ((uint16_t)buffer[0]<<8) + (uint16_t)buffer[1];return status;
}int32_t VL53L0X_read_dword(uint8_t address, uint8_t index, uint32_t *pdata)
{int32_t status = STATUS_OK;uint8_t buffer[BYTES_PER_DWORD];status = VL53L0X_read_multi(address, index, buffer, BYTES_PER_DWORD);*pdata = ((uint32_t)buffer[0]<<24) + ((uint32_t)buffer[1]<<16) + ((uint32_t)buffer[2]<<8) + (uint32_t)buffer[3];return status;
}int32_t VL53L0X_platform_wait_us(int32_t wait_us)
{delay_us(wait_us);return STATUS_OK;
}int32_t VL53L0X_wait_ms(int32_t wait_ms)
{HAL_Delay(wait_ms);return STATUS_OK;
}int32_t VL53L0X_set_gpio(uint8_t level)
{return STATUS_OK;
}int32_t VL53L0X_get_gpio(uint8_t *plevel)
{return STATUS_OK;
}int32_t VL53L0X_release_gpio(void)
{return STATUS_OK;
}int32_t VL53L0X_cycle_power(void)
{return STATUS_OK;
}int32_t VL53L0X_get_timer_frequency(int32_t *ptimer_freq_hz)
{*ptimer_freq_hz = 0;return STATUS_FAIL;
}int32_t VL53L0X_get_timer_value(int32_t *ptimer_count)
{*ptimer_count = 0;return STATUS_FAIL;
}
2.2.3 vl53l0x_platform.c
前面也说到过 vl53l0x_platform.c
大多都是对 vl53l0x_i2c_platform.c
函数的应用,只需要修改下头文件包含,和一个延时函数,其他的大家可以自己修改也没什么问题。
到这里,基本上驱动移植算是完成了,但是呢,我们也要知道怎么用起来。
三、测试
实际上,怎么用起来应该是驱动移植前要了解的事情,要说起来,其实细节也挺多的,大家可以自己参考官方的文档研究,也可以去网上找找前人的细节分析,这里有一篇文章讲解得比较细,大家可以参考一下:
激光测距芯片VL53L0X的使用与代码
对于我们测试来说,这里有两个图说明一下,一个是传感器的工作流程图,如下图:
还有一个是传感器工作模式,如下图:
上面两个都关系到我们程序中的配置,所以确定了这个,我们在调用函数的时候,有一些参数也知道怎么设置,也知道什么意思了。
但是我测试的时候,参考了网上很多版本的代码,刚开始也遇到了莫民奇妙的问题,本来想着自己根据流程理解,一步一步的写一下,发现花费了好些精力却没成功… 这个过程其实也没太大意义,自己花费时间自己琢磨,这里就不细说了。
后来一不做二不休,直接在别人代码上面修改测试成功的。
基本上网上所有有关 VL53L0X 有关能用的示例我都下载了 = =! 正点原子,野火,包括一些博客论坛上人家推荐的,基本上我都测试过,最后我成功的测试移植了 “ 表面上看起来4个不同的示例 ”。
于是我的工程目录变成这样:
用起来的话,直接调用示例函数即可(整个工程包,我会上传至资源,大家可以下载):
3.1 测试效果展示
下面上一下我测试成功过的不同的示例效果:
vl53l0_demo1:
vl53l0_demo2:
vl53l0_demo3:
vl53l0_demo4:
3.2 一些使用说明
3.2.1 校准问题
上面的示例只要能够读到数据,还是很准确的,我也没有校准 。
那这里正好说明一个问题,不校准能不能测量? 答案当然是可以的。
ST 官方出厂的时候就做好了校准,实际使用中会有偏差是因为我们的被测物体的面不是那么完美,比如表面的反射率,和颜色等问题都会影响测量。
但是如果现场环境误差过大,还是需要校准的,具体校准的步骤,我这边也没有研究过,大家可以根据上面推荐的博文自行研究。
根据我实际测试的情况来看,出厂的校准已经是很好的。
3.2.2 使用环境问题
对于 VL53L0X 来说,他的使用环境也是有一定要求的,具体如下:
- 黑暗无红外,目标为白色,且被测物面积要覆盖整个测量区域;
- 在室内裸露测量,环境内的红外光会影响测量精度;
- 不是随便找个物体就能测的,目标为白色时误差最小,颜色越深误差越大;
- datasheet 上注明了测量角度是25°,1m 远的白色物体要求面积在 0.45㎡ 以上才能保证测量精度,当然面积不够也能测,就是误差大一点,在被测区域内最好没有其他物体干扰。
3.2.3 测量盲区
此类测距的传感器都会有一个测量盲区,就是小于多少距离是测量不出来的(距离太小,激光无法走一个正常的来回),以前的 VL6180 的盲区大概在 4mm 以内。
VL53L0X 的测量盲区是 在 40mm 以内。
结语
本文跨度时间有点久,因为从一开始的硬件设计就开始写了,然后等到板子到了以后的测试,都花了一定的时间,不过最终也算是测试成功了。
虽然测试完了,但是有些小细节其实也没有搞明白,比如工作 1.8V 模式,还是 2.8V 模式的问题,在一直期间去看过源码,是根据是否有宏定义来判断工作在什么模式,但是测试起来,并没有注意到哪里有特意的去修改,或许是因为网络上绝大多数的例程和大多数人的使用场景,基本都是2.8V 模式,所以没有特意的去说明。
还有一个因为我目前的使用场景需要的距离不远,所以我只测试了小于 500mm 的距离,对于再远一点的距离,因为没有用到,也没有特意的去测量。
那么这些细节,如果在以后有遇到再来补充说明,好歹 VL53L0X 传感器我们也成功用起来了。
另外,最后整个工程我上传到了资源,大家有需要自行下载:
VL53L0X 测试项目(by 矜辰所致)
好了,那本文就到这里把,谢谢大家!