好不容易搞定了VL53L1X的移植,结果立创那边告诉我它的移植已经有人做了,气死我了。看了看CSDN上似乎还没有多少人细讲VL53L1X的移植,之前我也踩了很长一段时间的坑,那就在CSDN上发一篇文档吧。
一、模块简述
VL53L1X是由ST推出的新一代TOF测距模块,属于VL53L0X的升级版本,其测距距离和测量频率由VL53L0X的1.2m/50Hz或2.0m/33Hz提升至2.0m/100Hz或4.0m/50Hz,且保留了VL53L0X小体积和廉价的优势,其引脚也完全兼容VL53L0X(注意,仅仅是引脚兼容)。
二、驱动介绍
VL53L1X通过IIC总线与主机进行通讯,ST官方并未公开VL53L1X的寄存器,而是提供了一整套函数库,以实现对VL53L1X的配置/读取,用户仅需实现最底层的一些操作,例如毫米级延时、IIC发送接收等。这个库有Full和ULD(ultra lite driver)两个版本,编号分别为IMG007和IMG009,如下图所示:
Full版本(IMG007)是VL53L1X驱动的完全体版本,其可以使用VL53L1X的所有功能,且拥有最高100Hz的测距频率,但也正因如此,其底层操作多且复杂,包含IIC读写、引脚操作、引脚中断、延时等,占用flash较大,移植难度也相对较高。为解决上述问题,ST推出了简化后的ULD版本驱动(IMG009),其大幅精简了文件数量,占用flash空间由9KB下降至2.3KB,且移植时仅需实现IIC读写与延时即可,移植难度低了许多,代价是VL53L1X的GPIO相关功能无法使用,且最高测距频率仅支持66Hz。本次移植采用ULD版本驱动。
三、驱动移植
硬件:正点原子ATK-MS53L1M模块(工作于IIC模式,等效于一块VL53L1X传感器)
梁山派GD32F470开发板
软件:MDK v5.18
接线:模块 开发板
VCC ------------ 5V
GND ----------- GND
SCL ------------ PB8
SDA ------------- PB7
1. 下载驱动
首先前往ST官网下载DataSheet和ULD版本驱动,完成后打开驱动文件夹,子文件夹如下所示:
其中API文件夹是驱动所在位置,Example文件夹则是ST基于X-NUCLEO-53L1A1评估板实现的移植示例,包含MDK、IAR、cubeIDE等多个版本,README提供了关于本库文件的一些信息。打开API文件夹,可以发现core和platform两个文件夹,这便是驱动库的本体了,其中core文件夹提供了驱动VL53L1X所需的API,而platform文件夹中便是驱动库的底层操作了,其中的platform.h文件定义了需要实现的底层操作函数,在platform.c中实现即可完成移植。
2. 建立工程
在MDK中建立工程,并将core和platform中的文件包含进工程中,如下所示:
3. 函数实现
platform.c中需要实现的函数包括延时和IIC读写两类,数量不多,接下来讲解如何实现。
3.1 延时函数
与完整版驱动需要提供微秒级和毫秒级两种延时不同,此处仅需提供毫秒级延时函数即可,因此其实没什么好说的,直接将systick.c中已有的延时函数拷贝进去即可,代码如下:
int8_t VL53L1_WaitMs(uint16_t dev, int32_t wait_ms){
int8_t status = 0;
delay_1ms((uint32_t)wait_ms);
return status; // to be implemented
}
3.2 IIC读写
3.2.1 IIC介绍
IIC(Inter-Integrated Circuit),中文名集成电路总线,它是一种串行通信总线,使用多主从架构,由飞利浦公司在1980年代为了让高速主机用以连接低速周边设备而发展。其硬件特点为一般仅有两根线,分别传输时钟信号(SCL)和数据(SDA),其拥有3种传输模式:标准模式(100Kb/s)、快速模式(400Kb/s)和高速模式(3.4Mb/s)。
数据传输
IIC为同步通讯接口,其帧格式为MSB优先,时钟高电平与数据位一一对应,一个高电平时钟对应一个数据位,只有在SCL为低电平期间,才允许 SDA上的电平改变状态。在发送完8位(1字节)数据后需要接收方发送ACK信号进行应答,在读操作时,如果主机停止读取数据,由主机发送给从机非应答信号NACK。
传输起始与结束
在SCL保持高电平时拉低SDA,表示通讯开始,在SDA保持低电平时拉高SCL,表示通讯结束。
设备地址
在进行读写之前,需要先发送设备地址来呼叫对应设备,IIC设备拥有7位设备地址+1位读写标志位,0为写1为读,帧格式如下:
可以理解为一个设备拥有2个地址,一个为读地址一个为写地址。
3.2.2 代码实现
首先打开VL53L1X的Datasheet,跳转至第四章"Control interface",其中详细讲述了VL53L1X的读写时序与步骤,其中最关键的是这两张图:
以上为VL53L1X的连续读/写数据帧图,由上可见, 在连续写入时,仅需向传感器传输写入的起始寄存器地址,然后写入数据即可,传感器会自动进行地址递增。在连续读取时则需先发起一次写入通讯,向寄存器中写入需要读取的寄存器地址,结束本次通讯后立刻再发起一次读取通讯,由传感器持续向主机传输数据,但值得注意的是,连续读取示意图最后一部分的示意是错误的,主机在终止通讯之前必须先停止发送应答位以停止从机的数据发送,如下所示:
具体的连续读/写代码实现如下所示:
/*
* 功能 将提供的字节缓冲区写入设备,用于对接platform.c中相关函数
* 参数 dev_addr: 7位设备地址
* reg_addr: 16位寄存器地址
* *pdata: 数据缓冲区指针
* len: 数据长度
* 返回 操作是否成功
*/
int8_t vl53_writeBytes(uint8_t dev_addr, uint16_t reg_addr, uint8_t *pdata, uint32_t len)
{
uint32_t i = 0;
uint8_t *ptr = pdata;
i2c_start_on_bus(VL53_IIC);/* 生成起始信号 */
while(RESET == i2c_flag_get(VL53_IIC, I2C_FLAG_SBSEND));/* 等待SBSEND标志位置位 */
i2c_master_addressing(VL53_IIC, dev_addr, I2C_TRANSMITTER);/* 发送地址与写指令 */
while(RESET == i2c_flag_get(VL53_IIC, I2C_FLAG_ADDSEND));/* 等待ADDSEND置位 */
i2c_flag_clear(VL53_IIC, I2C_FLAG_ADDSEND);/* 清除ADDSEND标志位 */
while(RESET == i2c_flag_get(VL53_IIC, I2C_FLAG_TBE));/* 等待发送缓冲区清零 */
i2c_data_transmit(VL53_IIC, (uint8_t)((reg_addr & 0xff00) >> 8));/* 发送高8位寄存器地址 */
while(RESET == i2c_flag_get(VL53_IIC, I2C_FLAG_TBE));/* 等待发送缓冲区清零 */
i2c_data_transmit(VL53_IIC, (uint8_t)(reg_addr & 0x00ff));/* 发送低8位寄存器地址 */
while(RESET == i2c_flag_get(VL53_IIC, I2C_FLAG_TBE));/* 等待发送缓冲区清零 */
while(i < len)/* 循环发送需要写入的数据 */
{
i2c_data_transmit(VL53_IIC, *ptr);/* 发送数据缓冲区指针指向的数据 */
while(RESET == i2c_flag_get(VL53_IIC, I2C_FLAG_TBE));/* 等待发送缓冲区清零 */
ptr++;
i++;
}
while(RESET == i2c_flag_get(VL53_IIC, I2C_FLAG_BTC));/* 等待最后一个字节发送完成 */
i2c_stop_on_bus(VL53_IIC);/* 生成停止信号 */
return 0;
}
/*
* 功能 从设备读取请求的字节数,用于对接platform.c中相关函数
* 参数 dev_addr: 7位设备地址
* reg_addr: 16位寄存器地址
* *pdata: 数据缓冲区指针,用于存储读出的数据
* len: 数据长度
* 返回 操作是否成功
*/
int8_t vl53_readBytes(uint8_t dev_addr, uint16_t reg_addr, uint8_t *pdata, uint32_t len)
{
uint32_t i = 0;
uint8_t *ptr = pdata;
i2c_start_on_bus(VL53_IIC);/* 生成起始信号 */
while(RESET == i2c_flag_get(VL53_IIC, I2C_FLAG_SBSEND));/* 等待SBSEND标志位置位 */
i2c_master_addressing(VL53_IIC, dev_addr, I2C_TRANSMITTER);/* 发送地址与写指令 */
while(RESET == i2c_flag_get(VL53_IIC, I2C_FLAG_ADDSEND));/* 等待ADDSEND置位 */
i2c_flag_clear(VL53_IIC, I2C_FLAG_ADDSEND);/* 清除ADDSEND标志位 */
while(RESET == i2c_flag_get(VL53_IIC, I2C_FLAG_TBE));/* 等待发送缓冲区清零 */
i2c_data_transmit(VL53_IIC, (uint8_t)((reg_addr & 0xff00) >> 8));/* 发送高8位寄存器地址 */
while(RESET == i2c_flag_get(VL53_IIC, I2C_FLAG_TBE));/* 等待发送缓冲区清零 */
i2c_data_transmit(VL53_IIC, (uint8_t)(reg_addr & 0x00ff));/* 发送低8位寄存器地址 */
while(RESET == i2c_flag_get(VL53_IIC, I2C_FLAG_TBE));/* 等待发送缓冲区清零 */
while(RESET == i2c_flag_get(VL53_IIC, I2C_FLAG_BTC));/* 等待最后一个字节发送完成 */
i2c_stop_on_bus(VL53_IIC);/* 生成停止信号 */
i2c_start_on_bus(VL53_IIC);/* 再次生成起始信号 */
while(RESET == i2c_flag_get(VL53_IIC, I2C_FLAG_SBSEND));/* 等待SBSEND标志位置位 */
i2c_master_addressing(VL53_IIC, dev_addr, I2C_RECEIVER);/* 发送地址与读指令 */
if(SET == i2c_flag_get(VL53_IIC, I2C_FLAG_RBNE))
i2c_data_receive(VL53_IIC);/* 读取接收缓冲区的数据以清空接收缓冲区 */
while(RESET == i2c_flag_get(VL53_IIC, I2C_FLAG_ADDSEND));/* 等待ADDSEND置位 */
i2c_flag_clear(VL53_IIC, I2C_FLAG_ADDSEND);/* 清除ADDSEND标志位 */
while(i < len)
{
if(i == len - 1)
i2c_ack_config(VL53_IIC, I2C_ACK_DISABLE);/* 在接收最后一个数据前失能应答发送功能 */
while(RESET == i2c_flag_get(VL53_IIC, I2C_FLAG_RBNE));/* 等待接收缓冲区收到数据 */
*ptr = i2c_data_receive(VL53_IIC);/* 读取接收缓冲区的数据 */
ptr++;
i++;
}
i2c_stop_on_bus(VL53_IIC);/* 生成停止信号 */
i2c_ack_config(VL53_IIC, I2C_ACK_ENABLE);/* 在接收完成后重新使能应答发送功能 */
return 0;
}
其与platform.c中的函数对接如下:
int8_t VL53L1_WriteMulti( uint16_t dev, uint16_t index, uint8_t *pdata, uint32_t count) {
int8_t status;
status = vl53_writeBytes((uint8_t)dev, index, pdata, count);
return status; // to be implemented
}
int8_t VL53L1_ReadMulti(uint16_t dev, uint16_t index, uint8_t *pdata, uint32_t count){
int8_t status;
status = vl53_readBytes((uint8_t)dev, index, pdata, count);
return status; // to be implemented
}
int8_t VL53L1_WrByte(uint16_t dev, uint16_t index, uint8_t data) {
int8_t status;
status = vl53_writeBytes((uint8_t)dev, index, &data, 1);
return status; // to be implemented
}
int8_t VL53L1_WrWord(uint16_t dev, uint16_t index, uint16_t data) {
int8_t status;
uint8_t buffer[2];
// Split 16-bit word into MS and LS uint8_t
buffer[0] = (uint8_t)(data >> 8);
buffer[1] = (uint8_t)(data & 0x00FF);
status = vl53_writeBytes((uint8_t)dev, index, buffer, 2);
return status; // to be implemented
}
int8_t VL53L1_WrDWord(uint16_t dev, uint16_t index, uint32_t data) {
int8_t status;
uint8_t buffer[4];
// Split 32-bit word into MS ... LS bytes
buffer[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 = vl53_writeBytes((uint8_t)dev, index, buffer, 4);
return status; // to be implemented
}
int8_t VL53L1_RdByte(uint16_t dev, uint16_t index, uint8_t *data) {
int8_t status;
status = vl53_readBytes((uint8_t)dev, index, data, 1);
return status; // to be implemented
}
int8_t VL53L1_RdWord(uint16_t dev, uint16_t index, uint16_t *data) {
int8_t status;
uint8_t buffer[2];
status = vl53_readBytes((uint8_t)dev, index, buffer, 2);
*data = (uint16_t)(((uint16_t)(buffer[0])<<8) + (uint16_t)buffer[1]);
return status; // to be implemented
}
int8_t VL53L1_RdDWord(uint16_t dev, uint16_t index, uint32_t *data) {
int8_t status;
uint8_t buffer[4];
status = vl53_readBytes((uint8_t)dev, index, buffer, 4);
*data = ((uint32_t)buffer[0]<<24) + ((uint32_t)buffer[1]<<16) + ((uint32_t)buffer[2]<<8) + (uint32_t)buffer[3];
return status; // to be implemented
}
至此,VL53L1X的驱动移植完成,接下来根据API手册调用API进行初始化即可完成传感器初始化与读取。完整代码将会在下一篇文章中开源。