2024艾迈斯欧司朗竞赛 - 基于TMF8821的角度测量
该项目使用了艾迈斯欧司朗dToF传感器TMF8821,实现了测量距离,和小角度测量的设计,它的主要功能为:使用艾迈斯欧司朗dToF传感器TMF8821,通过激光多点测距,实现测量距离,和小角度感知。。
标签
RP2040
mpy
TMF8821
艾迈斯欧司朗dToF传感器
aramy
更新2025-03-04
48

1、硬件介绍:

dToF模块是基于 TMF8821 设计的直接飞行时间 (dToF) 传感器模块。dToF 设备基于 SPAD、TDC 和直方图技术,可实现 5000 mm 的检测范围。支持 3x3、4x4 和 3x6 多区域输出数据以及宽广的、动态可调的视野。TMF8821在其 I2C 接口上提供距离信息和置信度值。测量频率可达10Hz。

主控使用RP2040 Game Kit。这是基于树莓派RP2040的嵌入式系统学习平台,USB Type-C供电,采用RP2040作为主控,支持MicroPython、C/C++编程,性能强大。

主控与传感器之间使用I2C连接通讯。

2、任务选择:

这次任务选择的是任务1:将板卡组合后固定,并保证与面前的平面存在一定夹角,通过程序测算出板卡距离屏幕的夹角和垂直最小距离

3、设备驱动

拿到硬件,可以看见传感器非常小巧,外围元件也很少,电子森林这里设计了一个排母的插座,正好和RP2040 Game Kit贴合。在官网可以找到TMF8821的说明文档,和Arduino的例程,拿到手很方便地就利用官方例程将传感器驱动起来了。然后老师又添加了新的规定,不准使用Arduino,这可是把我愁死了。只能参考着官方文档,和网上一点点资料,慢慢地啃起来这个芯片的驱动方法了。

因为官方文档全是英文的,读起来一知半解。在知乎上有位老师分享了一篇驱动的文章(https://zhuanlan.zhihu.com/p/18552342863),参考着各位老师的讲解,一点点地用micropython将芯片驱动起来。

image.png

首先上电,然后拉高传感器的en管脚,接着是向0xe0寄存器写入0x01,即 w(0xe0,0x01)再循环读使能寄存器0xe0,直至返回0x41,此时,传感器进入bootloader模式,读取0x00寄存器(APPID_REG),值应当为0x80。然后就是写入固件。这里其实挺不明白的,按厂家的说法,不同的固件对应不一样的算法,可以通过不同的固件实现特殊功能的测量,但是这个固件是不开放的,只有厂家提供,也就是说只能使用厂家提供的固定算法;但是这里的驱动厂家仅仅提供了Arduino下C语言的,没看见官方提供其它平台下的驱动代码,此次任务又不让使用Arduino,所以挺矛盾的。最终一知半解的情况下,还是成功读取出来了传感器提供的距离信息。

4、功能实现

本想着有了距离信息,计算个角度应该不难。上网找了一圈,两个平面的夹角,要使用法向量来求解,可是不知道是忘记了还是就没学过,对法向量完全没有概念。让孩子给我补习了一个下午的法向量,还是觉着无从下手。

image.png

最终简化了问题,将问题简化了。现在的思路是:假定待测平面与传感器所在平面都与地面垂直,且传感器只在水平面上旋转。首先获取待测平面AB上3X3个点阵到传感器O的距离,当这9个点的数据与均值偏差较小时认为此时传感器所处的平面,与待测平面平行,且此时中心点的距离OA就是这两个平面的距离,将这个距离作为直角三角形的一条直角边。当传感器水平旋转时,与待测平面距离必然拉长,此时距离OB作为直角三角形的斜边,∠AOB通过反余弦就可以计算出旋转角度值。

image.png

按官方说法,TMF8821可以提供10Hz的测量速度,为解决传感器获得数据跳动问题,使用50组数据进行均值,也就是当传感器稳定5秒,即可计算出距离和角度信息。mpy中不知道如何使用pandas,手工计算矩阵还是挺烦的。

from tmf8821_utility import Tmf8821Utility
from i2c_com import I2C_com, I2C_Settings
from tmf8821_device import Tmf8821Device
from tmf8821_app import Tmf8821App
import utime
import uos
import machine
import st7789 as st7789
from fonts import vga2_8x8 as font1
from fonts import vga1_bold_16x16 as font2
from fonts import vga1_16x32 as font3
import random
import framebuf
import math

#系统的初始化
st7789_res = 0#定义res引脚
st7789_dc = 1#定义dc引脚
disp_width = 240#设置显示屏的宽度
disp_height = 240#设置显示屏的高度
spi_sck=machine.Pin(2)#定义SPISCK引脚
spi_tx=machine.Pin(3)#定义SPIMOSI引脚
spi0=machine.SPI(0,baudrate=4000000, phase=1, polarity=1, sck=spi_sck, mosi=spi_tx)#初始化spi0
display = st7789.ST7789(spi0, disp_width, disp_width,
reset=machine.Pin(st7789_res, machine.Pin.OUT),
dc=machine.Pin(st7789_dc, machine.Pin.OUT),
xstart=0, ystart=0, rotation=1)#初始化我们的显示屏幕
display.fill(st7789.BLACK)

# 创建 I2C 通信对象
ic_com = I2C_com()
# 创建 Tmf8821Utility 实例
tof = Tmf8821Utility(ic_com)
if Tmf8821App.Status.OK != tof.open():
tof.error("Error open FTDI device")
raise RuntimeError("Error open FTDI device")
else:
tof.log("Opened connection")

tof.init_bootloader_check() #载入固件 使用3X3测量方式
#获得距离信息
groupnum=30 #均值组个数
allowdev=0.045 #允许偏差值
maxallowdev=0.08 #最大允许偏差
distancegroup=[] #距离信息数组,使用10组数据,后期做均值处理
distanceinfo={"distance":0} #距离信息

def getDistance():
frames = tof.measure_frame(1) # 获取帧数据
distance=[] #距离信息
framelist=[]
if frames:
for idx, frame in enumerate(frames):
# 获取前 9 个测量结果
for result_idx, result in enumerate(frame.results):
if result_idx < 9: # 只取前 9
framelist.append(result.distanceInMm)
#调整一下顺序,使数据顺序与实际情况相符
if len(framelist)==9:
distance.append(framelist[6])
distance.append(framelist[7])
distance.append(framelist[8])
distance.append(framelist[3])
distance.append(framelist[4])
distance.append(framelist[5])
distance.append(framelist[0])
distance.append(framelist[1])
distance.append(framelist[2])
if len(distancegroup)>=groupnum:
distancegroup.pop(0)
distancegroup.append(distance)

#显示距离信息
def dispDistance(offstat):
if len(distancegroup)==groupnum:
distance=[0,0,0,0,0,0,0,0,0] #计算一个均值矩阵 3X3
for pos in range(9):
for i in range(groupnum):
distance[pos]=distance[pos]+distancegroup[i][pos]
for pos in range(9):
distance[pos]=distance[pos]/groupnum

#串口显示 和 屏幕显示
for i in range(3):
for j in range(3):
print("%.1f" % (distance[i*3+j]), end=' ')
if offstat==0:
display.text(font2, "%3.0f" % (distance[i*3+j]/10), i*70+30, j*25+10,color=st7789.GREEN) #使用CM做单位
elif offstat==1:
display.text(font2, "%3.0f" % (distance[i*3+j]/10), i*70+30, j*25+10,color=st7789.YELLOW) #使用CM做单位
else:
display.text(font2, "%3.0f" % (distance[i*3+j]/10), i*70+30, j*25+10,color=st7789.RED) #使用CM做单位
print()
print()
print()



#当所有距离 小于偏差时 返回0 大于偏差小于最大偏差时 返回1 否则返回9
def distanceIsStand():
if len(distancegroup)>=groupnum:
distance=[0,0,0,0,0,0,0,0,0] #计算一个均值矩阵 3X3
for pos in range(9):
for i in range(groupnum):
distance[pos]=distance[pos]+distancegroup[i][pos]
for pos in range(9):
distance[pos]=distance[pos]/groupnum
#print(distancegroup)
#print(distance)
groupavg=sum(distance)/9 #距离均值
groupoffset=groupavg*allowdev #计算偏差值
maxgroupoffset=groupavg*maxallowdev #计算偏差值
print("offset %.1f,%.1f,%.1f" %(groupavg,groupoffset,maxgroupoffset))
#检查平均组里的每个数据 是否是落在偏差值内
echoval=0
for idx in range(9):
if abs(distance[idx]-groupavg)>maxgroupoffset :
distanceinfo["distance"]=0 #偏差太大时就 不可信距离值
distanceinfo["angle"]=0
return 9
if abs(distance[idx]-groupavg)>groupoffset:
echoval=1
#依据偏差值的结果来计算距离和角度
if echoval==0 : #当偏差小于指定值时,就取中心点的距离为距离
if distanceinfo["distance"]==0:
distanceinfo["distance"]=distance[4]
distanceinfo["angle"]=0
else:
if distanceinfo["distance"]<distance[4]:
distanceinfo["angle"]=math.degrees(math.acos(distanceinfo["distance"]/distance[4]))
else:
distanceinfo["distance"]=distance[4]
distanceinfo["angle"]=0

elif echoval==1 : #当偏差值较小,则计算偏角
if distanceinfo["distance"]<distance[4]:
distanceinfo["angle"]=math.degrees(math.acos(distanceinfo["distance"]/distance[4]))
else:
distanceinfo["angle"]=0
return echoval



#显示距离和角度信息
def dispinfo(offstat):
if offstat==0:
display.text(font3, "%3.0f CM" % ((distanceinfo["distance"])/10), 120, 100,color=st7789.GREEN) #使用CM做单位
display.text(font2, " DIST: " , 10, 105,color=st7789.WHITE)
display.text(font3, "%3.0f " % (distanceinfo["angle"]), 120, 160,color=st7789.GREEN)
display.text(font2, "ANGLE: " , 10, 160,color=st7789.WHITE)
elif offstat==1:
display.text(font3, "%3.0f CM" % ((distanceinfo["distance"]+5)/10), 120, 100,color=st7789.YELLOW) #使用CM做单位
display.text(font2, " DIST: " , 10, 105,color=st7789.WHITE)
if distanceinfo["angle"]==0:
display.text(font3, "---", 120, 160,color=st7789.YELLOW)
display.text(font2, "ANGLE: " , 10, 165,color=st7789.WHITE)
else:
display.text(font3, "%3.0f " % (distanceinfo["angle"]), 120, 160,color=st7789.YELLOW)
display.text(font2, "ANGLE: " , 10, 165,color=st7789.WHITE)
else:
display.text(font3, "--- " , 120, 100,color=st7789.RED) #使用CM做单位
display.text(font2, " DIST: " , 10, 105,color=st7789.WHITE)
display.text(font3, "---", 120, 160,color=st7789.YELLOW)
display.text(font2, "ANGLE: " , 10, 165,color=st7789.WHITE)

while True:
getDistance()
offstat=distanceIsStand()
print("echo= ",offstat)
dispDistance(offstat)
#如果计算距离小于预期偏差,则显示距离信息 以均值为距离信息,并且开始计算
dispinfo(offstat)
#print("distanceval=",distanceinfo)
utime.sleep_ms(100) # 延迟 1 秒,便于查看每次显示




5、效果展示

由于使用了50组数据做的均值,所以系统反应是比较缓慢的,需要缓慢移动,直至数据不再跳动,再继续旋转。

0d3bebe6aacb0e25b2b20fa9a015a98.jpg

小角度旋转,测量角度开始变化。小角度变化,感觉还是比较准确。

41e97dbe22a52ed3abc789b025657b1.jpg

继续旋转

e36d5ba440471bca73af7d9f7366c8c.jpg

当偏差角度过大时,就无法显示了。

09b4e5b3ebe13d64bbe1eb88fd3c115.jpg

6、心得体会:

超棒的激光测距模块,分辨率最高能到4X4,要是能再高点就好了。再高点就可以用图像方式展示距离信息了,就像热成像那样,实现个距离成像,一定很炫酷。通过这次接触硬件底层文档和驱动的编写,了解到了这颗dtof的好玩的用法,还有很多高级功能等待挖掘。

附件下载
dtof.zip
源码
mpy-sge+nes-v20220301.uf2
团队介绍
单片机业余爱好者,瞎捣鼓小能手。
团队成员
aramy
单片机业余爱好者,瞎捣鼓小能手。
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号