最近給朋友調試了STM32F407驅動VL53L0的激光測距,安裝在機器人上的,遇到一些問題,這里發帖紀錄一下。
關于VL53L0的資料和代碼在正點原子那里都有,但是正點原子只是驅動了一路VL53L0,很多問題都需要我們自己解決,一路的VL53L0非常簡單,隨便參考一下例程就能完美解決,但是一旦涉及到多路設備,就會出現一堆問題,最突出最主要的就是多個VL53L0的地址設置,把握不住就會出現只有一路能正常使用的問題。
VL53L0X 是 ST 公司推出的新一代 ToF 激光測距傳感器,采用了第二代 FlightSenseTM技術,利用飛行時間(ToF)原理,通過光子的飛行來回時間與光速的計算,實現測距應用。較比上一代 VL6180X,新的器件將飛行時間測距長度擴展至 2 米,測量速度更快,能效更高。除此之外,為使集成度過程更加快捷方便, ST 公司為此也提供了 VL53L0X 軟件 API(應用編程接口)以及完整的技術文檔,通過主 IIC 接口,向應用端輸出測距的數據,大大降低了開發難度。
①, 使用 940nm 無紅光閃爍激光器,該頻段的激光為不可見光,且不危害人眼。
②,系統視野角度(FOV)可達 25 度,傳感器的感測有效工作直徑擴展到 90 厘米。
③,采用脈沖式測距技術,避免相位式測距檢測峰值的誤差,利用了相位式檢測中除波峰以外的光子。
④,多種精度測量和工作模式的選擇。
⑤,測距距離能擴至到 2 米。
⑥, 正常工作模式下功耗僅 20mW,待機功耗只有 5uA。
⑦,高達 400Khz 的 IIC 通信接口。
⑧,超小的封裝尺寸: 2.4mm × 4.4mm × 1mm。
VL53L0X 傳感器提供了 3 種測量模式, Single ranging(單次測量)、 Continuous ranging(連續測量)、以及 Timed ranging(定時測量),下面我們將簡單介紹下:
(1) Single ranging(單次測量),在該模式下只觸發執行一次測距測量,測量結束后,VL53L0X 傳感器會返回待機狀態,等待下一次觸發。
(2) Continuous ranging(連續測量),在該模式下會以連續的方式執行測距測量。一旦測量結束,下一次測量就會立即啟動,用戶必須停止測距才能返回到待機狀態,最后的一次測量在停止前完成。
(3) Timed ranging(定時測量),在該模式下會以連續的方式執行測距測量。測量結束后,在用戶定義的延遲時間之后,才會啟動下一次測量。用戶必須停止測距才能返回到待機狀態,最后的一次測量在停機前完成。根據以上的測量模式, ST 官方提供了 4 種不同的精度模式,如表格所示:
從表格可以看到,針對不同的精度模式,測量時間也是有所區別的,測量時間最快為高速模式,只需 20ms 內就可以采樣一次,但精度確存在有±5%的誤差范圍。而在長距離精度模式下,測距距離能達到 2m,測量時間在 33ms 內,但測量時需在黑暗條件(無紅外線)的環境下。所以在實際的應用中,需根據當前的要求去選擇合適的精度模式,以達到最佳的測量效果。
以上資料來源于正點原子的《AN1703C ATK-VL53L0X 激光測距模塊使用說明》。這里摘錄一部分,方便進入主題。
因為今天是調試多路的VL53L0X設備,這里不完全借鑒正點原子的例程,但是官方提供的驅動我們還是必須要用的。
如果想要快速上手,文末直接下載我的代碼,我的驅動庫經過自己的修改,和正點原子有些不同。
在初始化VL53L0X之前,我們必須初始化IIC外設,此次遵循正點原子的方法,用模擬IIC。
復制
#ifndef _VL53L0X_I2C_H
#define _VL53L0X_I2C_H
#include "stm32f10x.h"
#include "stm32f10x_i2c.h"
//四個VL53L0掛載在同一個IIC總線下,所以使用四個片選信號--2019/10/30
//!!!!!!!注意:重新使能設備后,設備iic的地址會恢復為默認值0x52--2019/10/30
//VL53L0 0
#define I2C_SCL_GPIO GPIOB
#define I2C_PIN_SCL GPIO_Pin_8
#define I2C_SCL_HIGH() GPIO_SetBits(I2C_SCL_GPIO,I2C_PIN_SCL)
#define I2C_SCL_LOW() GPIO_ResetBits(I2C_SCL_GPIO,I2C_PIN_SCL)
#define I2C_SDA_GPIO GPIOB
#define I2C_PIN_SDA GPIO_Pin_9
#define I2C_SDA_HIGH() GPIO_SetBits(I2C_SDA_GPIO,I2C_PIN_SDA)
#define I2C_SDA_LOW() GPIO_ResetBits(I2C_SDA_GPIO,I2C_PIN_SDA)
#define I2C_SDA_STATE GPIO_ReadInputDataBit(I2C_SDA_GPIO,I2C_PIN_SDA)
//片選使能--2019/10/30
#define I2C_X_GPIO GPIOB
#define I2C_PIN_X0 GPIO_Pin_12
#define I2C_X0_HIGH() GPIO_SetBits(I2C_X_GPIO,I2C_PIN_X0)
#define I2C_X0_LOW() GPIO_ResetBits(I2C_X_GPIO,I2C_PIN_X0)
#define I2C_PIN_X1 GPIO_Pin_13
#define I2C_X1_HIGH() GPIO_SetBits(I2C_X_GPIO,I2C_PIN_X1)
#define I2C_X1_LOW() GPIO_ResetBits(I2C_X_GPIO,I2C_PIN_X1)
#define I2C_PIN_X2 GPIO_Pin_14
#define I2C_X2_HIGH() GPIO_SetBits(I2C_X_GPIO,I2C_PIN_X2)
#define I2C_X2_LOW() GPIO_ResetBits(I2C_X_GPIO,I2C_PIN_X2)
#define I2C_PIN_X3 GPIO_Pin_15
#define I2C_X3_HIGH() GPIO_SetBits(I2C_X_GPIO,I2C_PIN_X3)
#define I2C_X3_LOW() GPIO_ResetBits(I2C_X_GPIO,I2C_PIN_X3)
void i2c_init(void);
uint8_t i2c_write(uint8_t addr, uint8_t reg, uint32_t len, uint8_t * data);
uint8_t i2c_read(uint8_t addr, uint8_t reg, uint32_t len, uint8_t *buf);
#endif
復制
void i2c_init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
//模擬iic配置
GPIO_InitStructure.GPIO_Pin=I2C_PIN_SCL;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_2MHz;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
GPIO_Init(I2C_SCL_GPIO, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin=I2C_PIN_SDA;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_2MHz;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_OD;
GPIO_Init(I2C_SDA_GPIO, &GPIO_InitStructure);
//片選使能配置
GPIO_InitStructure.GPIO_Pin=I2C_PIN_X0;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_2MHz;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
GPIO_Init(I2C_X_GPIO, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin=I2C_PIN_X1;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_2MHz;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
GPIO_Init(I2C_X_GPIO, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin=I2C_PIN_X2;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_2MHz;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
GPIO_Init(I2C_X_GPIO, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin=I2C_PIN_X3;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_2MHz;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
GPIO_Init(I2C_X_GPIO, &GPIO_InitStructure);
I2C_X0_LOW();
I2C_X1_LOW();
I2C_X2_LOW();
I2C_X3_LOW();
delay_ms(20);
}
在模塊初始化時調用IIC外設初始化,同時初始化4個測距模塊。
復制
VL53L0X_Error vl53l0x_init(void)
{
VL53L0X_Error Status=VL53L0X_ERROR_NONE; //初始值賦值為0
//初始化一定按照這個順序執行,否則不成功
VL53L0X_i2c_init();
vl53l0x_initX(&vl53l0x_dev0,0);
vl53l0x_initX(&vl53l0x_dev1,1);
vl53l0x_initX(&vl53l0x_dev2,2);
vl53l0x_initX(&vl53l0x_dev3,3);
return Status; //返回0
}
在vl53l0x_initX()函數便去別去正點原子的驅動,這里是全文的重點,很多單設備發展到多設備這里都會出問題,在初始化設備時一定要設置設備的IIC地址。
復制
//單個VL53L0初始化
VL53L0X_Error vl53l0x_initX( VL53L0X_Dev_t *pMyDevice ,u8 vl53l0_x_id)
{
VL53L0X_Error Status=VL53L0X_ERROR_NONE; //初始值賦值為0
pMyDevice->I2cDevAddr=0x52; //iic地址 0x52是默認地址,要初始化必須先寫0x52,才能初始化,之后再通過軟件修改
pMyDevice->comms_type=1; //選擇IIC還是SPI iic=1;SPI=0
pMyDevice->comms_speed_khz=400; //iic速率
//正點原子的VL53L0用戶手冊上寫明了再次使能時地址會恢復為0x52,所以只能使能一次,設置好地址即可,這里是核心
switch(vl53l0_x_id)
{
case 0:
I2C_X0_HIGH();
delay_ms(20);
vl53l0x_Addr_set(pMyDevice,0x60);//設置第一個VL53L0X傳感器I2C地址
break;
case 1:
I2C_X1_HIGH();
delay_ms(20);
vl53l0x_Addr_set(pMyDevice,0x62);//設置第一個VL53L0X傳感器I2C地址
break;
case 2:
I2C_X2_HIGH();
delay_ms(20);
vl53l0x_Addr_set(pMyDevice,0x64);
break;
case 3:
I2C_X3_HIGH();
delay_ms(20);
vl53l0x_Addr_set(pMyDevice,0x66);
break;
}
Status=VL53L0X_DataInit(pMyDevice); // Data initialization //VL53L0X_DataInit:一次設備的初始化,初始化成功返回0
if(Status !=VL53L0X_ERROR_NONE){ //判斷如果狀態不為0 打印錯誤信息
print_pal_error(Status);
return Status; // 返回錯誤值 可通過此值DEBUG查找錯誤位置
}
Status=VL53L0X_GetDeviceInfo(pMyDevice, &vl53l0x_dev_info); //讀取給定設備的設備信息
if(Status !=VL53L0X_ERROR_NONE){
print_pal_error(Status);
return Status;
}
printf("VL53L0X_GetDeviceInfo:\n");
printf("Device Name : %s\n", vl53l0x_dev_info.Name); //設備名
printf("Device Type : %s\n", vl53l0x_dev_info.Type); //產品類型VL53L0X=1, VL53L1=2
printf("Device ID : %s\n", vl53l0x_dev_info.ProductId); // 設備ID
printf("ProductRevisionMajor : %d\n", vl53l0x_dev_info.ProductRevisionMajor);
printf("ProductRevisionMinor : %d\n", vl53l0x_dev_info.ProductRevisionMinor);
if ((vl53l0x_dev_info.ProductRevisionMajor !=1) && (vl53l0x_dev_info.ProductRevisionMinor !=1)){
printf("Error expected cut 1.1 but found cut %d.%d\n",
vl53l0x_dev_info.ProductRevisionMajor, vl53l0x_dev_info.ProductRevisionMinor);
Status=VL53L0X_ERROR_NOT_SUPPORTED;
print_pal_error(Status);
return Status;
}
Status=vl53l0x_measure_init(pMyDevice); //測量配置
vl53l0x_status=Status;
if(Status !=VL53L0X_ERROR_NONE){ //判斷如果不為0打印錯誤信息
print_pal_error(Status);
return Status;
}
}
模塊的初始化順序是:使用默認地址初始化設備---修改傳感器IIC地址---再次初始化---測量配置。
所以在這個傳感器的初始化中我們先用默認的0X52地址將VL53L0X模塊初始化,初始化完成后方可修改其地址,這里使用SWITCH函數判斷用戶配置的地址,避免函數重寫,減小代碼尺寸。修改完地址調用VL53L0X_DataInit()函數進行模塊的再次初始化,使修改生效。注意:VL53L0X不能保存地址,如果掉電后地址會恢復為默認的0X52,同時修改完地址后只能執行一次初始化,更多的初始化次數會也會導致地址復位。這在硬件的處理上要加倍注意。
在這里我翻車了,因為硬件不在我的手邊,我都是遠程幫助調試,沒看到硬件,我的朋友一直反應各種問題,最多的就是測距有問題,測出的數據都是錯的,或者只有一個傳感器可以使用。我檢查了很多遍的代碼,始終找不到原因,還好他自己也想到了硬件的問題(因為他們硬件干過很多錯事,都是一些小白容易犯的,但是那個老員工比較粗心,也會犯錯),最后發現是線的質量太差,線的長度太長,IVL53L0X模塊安裝的位置不好,因為模塊安裝在可動部件上的,導致每次移動都會導致模塊短暫的掉電,導致地址復位。后來加裝模塊的減震裝置更換屏蔽線解決問題。
復位完成便可以測試:
復制
VL53L0X_Error vl53l0x_start_single_test(VL53L0X_Dev_t *pdev, \
VL53L0X_RangingMeasurementData_t *pdata)
{
int i=0,j=0,sum=0;
VL53L0X_Error status=VL53L0X_ERROR_NONE;
if(vl53l0x_status !=VL53L0X_ERROR_NONE)
return vl53l0x_status;
status=VL53L0X_PerformSingleRangingMeasurement(pdev, pdata); ////執行單次測距并獲取測距測量數據
if(status !=VL53L0X_ERROR_NONE){
printf("error:Call of VL53L0X_PerformSingleRangingMeasurement\n");
return status;
}
for(i=0;i<5;i++)
sum+=pdata->RangeMilliMeter;
pdata->RangeMilliMeter=sum/5;
printf("%d\r\n",pdata->RangeMilliMeter);
return status;
}
打印測試結果,通過!
主函數循環測試,因為項目對代碼的速度要求不高,所以一些狀態判斷代碼中還有保留,這里跟著原子走,沒做太多改變。
因為這個項目是幫助朋友做的調試,而且他們的項目還在研發期,太多的東西不能介紹,照片啥的都放棄了。一個簡短的帖子,希望能幫到大家把握住該模塊,蟹蟹!
附件見論壇原帖。
原標題:【把握住了】STM32F4驅動4路VL53L0測距你把握不住
原作者:吶咯密密
本文為21ic有獎征文作品,詳情請見21ic論壇活動專區:第二屆萬元紅包——藍V達人有獎征文活動,如果您也有興趣參與征文,歡迎進入論壇參與活動~
今天是LeetCode專題的第40篇文章,我們一起來看的是LeetCode中的71題Simplify Path,中文名是簡化路徑。
這題的難度是Medium,通過率是1/3左右,也是一道踩多捧少的題,一共有737個點贊,1703個反對。老實講我覺得反對得不冤,我先賣個關子,等會來詳細聊聊它為什么會被踩。
題目會給定一個字符串,表示一個Unix系統下的文件路徑,這個路徑當中會包含一些路徑的計算, 要求我們返回簡化之后的結果。
在Unix系統下用/來分隔文件夾,比如/home/download/file.txt。在這個路徑當中支持簡單的運算,比如.表示當前文件夾。所以如果我們當前終端在download這個文件夾下,我們要訪問file.txt文件,可以使用相對路徑./file.txt即可。除此之外,還包括..操作。..表示當前文件夾的上層文件夾。
比如如果我們在download文件夾下,當我們運行cd ..,那么我們就會返回到download文件夾的上層,也就是home文件夾下。我們是可以把..和.嵌入在文件路徑中使用的。比如說/home/download/../download/file.txt也是合法的,中間由于我們嵌入了..所以會返回到download的上層也就是home,然后再進入download。雖然這樣很費勁,但是是合法的。只要你愿意,可以不停地利用..回到上層,來回穿梭。
我們要返回的是這個路徑簡化之后的版本也就是:/home/download/file.txt
我們來看幾個案例:
Input: "/home/"
Output: "/home"
Explanation: Note that there is no trailing slash after the last directory name.
Input: "/../"
Output: "/"
Explanation: Going one level up from the root directory is a no-op, as the root level is the highest level you can go.
Input: "/a/../../b/../c//.//"
Output: "/c"
這題其實也是模擬題,不過相比之前我們做過的模擬題難度要小上很多。這道題的思路還是蠻明顯的,由于存在..和.的操作,我們需要記錄下來訪問的路徑,在..向上移動的時候把之前的文件夾拋棄掉。
舉個例子,a/b/../b/d/e
我們在b之后使用了..回到了a,然后我們再次進入b往下。顯然這里由于..導致b在路徑當中出現了兩次,這是多余的。我們需要在..回到上層的時候把b拋棄掉。對于.操作來說,由于它就表示當前路徑,所以對于答案并不會影響,我們直接忽略它的存在即可。
理解了這個思路之后,實現是非常簡單的,我們只需要根據/將字符串分段。每一段當中除了.和..之外就是文件夾的名稱,我們用一個list去存儲從上到下的經過的文件夾,遇見..就將最后一個添加的元素拋棄。最后用/將它們join在一起即可,唯一需要注意的是,當我們已經到了頂層的時候,如果我們繼續執行..并不會報錯,而是會停留在原地。所以我們需要特殊判斷這種情況,除此之外就幾乎沒有難度了。
class Solution:
def simplifyPath(self, path: str) -> str:
folders=[]
# 按照/分割
fs=path.split("/")
for f in fs:
# .直接跳過即可,不會影響結果
if f=='.':
continue
# 如果是..需要判斷是否在頂層
# 不在頂層的話拋棄掉最后插入的文件夾
if f=='..':
if len(folders) > 0:
folders.pop()
elif f !='':
folders.append(f)
return '/' + '/'.join(folders)
代碼非常簡單,只有10行左右。
到這里,關于題解的部分就結束了。
我們回到標題當中的問題,為什么我會有這樣的感受呢?是因為這道題我做過兩次,上一次做的時候用的是C++。由于C++的string類型不支持split,所以我需要自己進行split處理。整個的計算過程要復雜得多,我放一下C++的AC代碼大家自己感受一下就知道了,簡直不是一個次元的。
class Solution {
public:
vector<string> split(string & path) {
vector<string> vt;
string cur="";
// 遍歷所有字符
for (int i=0; i < path.length(); i++) {
// 如果是/ 說明需要把之前的內容放入vector
if (path[i]=='/') {
// 如果是空或者是.就跳過,因為.沒有意義
if (cur !="" && cur !=".") {
vt.push_back(cur);
}
cur="";
}else cur=cur + path[i];
}
// 要注意最后遺留的字符串
if (cur !="" && cur !=".") vt.push_back(cur);
return vt;
}
string simplifyPath(string path) {
vector<string> dirs=split(path);
string ret="";
// 存儲文件的結構
vector<string> paths;
for (string str : dirs) {
// 如果是.. 則返回上級
if (str=="..") {
if (paths.size() > 0) {
paths.pop_back();
}
// 否則則填入vector,表示合法
}else paths.push_back(str);
}
for (string str : paths) ret=ret + "/" + str;
if (ret=="") return "/";
return ret;
}
};
我說這些的重點并不是吐槽C++這門語言有多么落后,或者是證明Python有多么強大。不同的語言有不同的誕生背景,也有不同的強項,這個是很自然的。這題最主要的問題是不應該出這種因為語言本身的特性帶來巨大差異的問題,在正規比賽當中出這樣的問題一定是會被瘋狂吐槽的。
舉個例子,比如Java當中有大整數類BigInter,可以用來代替高精度算法來處理超過int64范圍的大整數。如果有出題人出了一道非常復雜的大整數問題,那么使用Java的選手使用BigInter(算法比賽一般不允許使用Python),三兩行代碼就可以輕松AC,而C++選手卻需要些上百行代碼來實現高精度計算,這顯然是不公平的。所以acm比賽當中,出題人一定會盡量避免這種語言特性差異巨大的問題,大概這也是這題遭黑的原因吧。
這篇文章就到這里,如果喜歡本文,可以的話,請點個關注,給我一點鼓勵,也方便獲取更多文章。