写在正文之前
刚拿到任务列表的时候,液体种类识别这个题目看起来很有趣。在仔细阅读了数据手册之后, 灵感一闪,突然想到了解题思路。
首先,了解 TMF8821 这个传感器的工作原理,作为直接飞行时间(dToF)传感器,dToF是通过测量光脉冲从发射到反射回来的时间来计算距离的,所以如果光通过不同折射率的介质,传播速度会改变,导致飞行时间的变化,从而影响测距结果,这就是用来测量折射率的关键点。主要的解题思路有了之后,就准备着手行动,以往的项目在写总结报告的时候都流水地介绍使用哪些板卡实现什么功能,通过哪些方法完成了任务。
这次的报告想写得有趣些,从现象到本质地分析,希望可以把这篇作业做成科普文的类型,也当成自己的学习日记。
本项目主要使用了 LPC55S69 开发板做为主控板,配合 dToF 模块,通过测量光脉冲在液体介质中的传播时间差,实现对液体折射率的非接触式检测。
引言
在炎热的夏日,驾车行驶在平坦的公路上时,常会看到远处路面仿佛覆盖着一层“晃动的积水”,甚至能映出车辆的倒影。但当车辆靠近时,这片“水面”又会突然消失。这种现象的本质是热晕效应,是由于地面受热引发的空气密度不均匀导致。
当阳光炙烤柏油路面时,地表温度急剧升高,紧贴地面的空气因受热膨胀而密度降低,而上方的空气仍保持相对低温与高密度,形成垂直方向的密度梯度。光线在穿过这些密度连续变化的空气层时,遵循“从高密度到低密度介质中传播速度变快”的规律,这会导致光线发生弯曲。这就导致我们从远处看时,景物出现扭曲、模糊,甚至“抖动”或“闪烁”的效果。
热晕效应直观揭示了光在非均匀介质中的传播特性,其核心物理机制正是空气密度差异引发的折射率变化。
在热晕效应中,空气密度的不均匀性通过折射率变化直接影响了光路,而这种密度与折射率的关联性在更广泛的物质状态下(如液体或固体)同样存在一定规律。例如,在某些物质中,密度较大的物质往往会有较大的折射率。这是因为折射率和物质内部的电子密度有关,电子密度较高时,物质更能“折射”光线。
但是,折射率并非仅由密度单一决定。例如,水的密度(1 g/cm³)远大于乙醇(0.79 g/cm³),但水的折射率(1.33)却低于乙醇(1.36)。折射率还会受到物质的分子结构、电子云的极化能力、以及光的波长等因素的影响。
原理
折射是光从一种介质进入另一种介质时传播方向发生改变的现象,其偏转的方向和角度可以通过中学物理中由斯涅尔总结出的经验定律计算:
其中:和 分别是入射介质和折射介质的折射率,和分别为入射角和折射角。大学物理中讲到光学和折射现象时,会做一个这样的实验:
在玻璃的一侧加上待测溶液,使用带角度测量的观察镜
当视野中呈现半边黑半边亮时,记录当前角度,就可以测出溶液折射率
这里可以通过一个小实验来观察,在一个装满水的水槽侧边用激光笔进行照射,
当激光笔照射进水中时可以看见光线确实发生了偏转,并且三种颜色的偏转角度不同(发生了色散),其偏转的方向和角度亦可以通过更复杂的公式计算
(公式过于超纲不展开讨论,通常有经验方程Sellmeier 方程可以描述【注:这个经验方程并不能完美拟合结果,依然存在误差,除此之外还有其他的拟合方程】)。
Sellmeier 方程和实际测量结果的比较图
反应了折射率与波长的关系
但这并没有解释折射的本质。实际上发生折射和光速变化是密切相关的,折射现象实际上是由光速变化所导致的。那为什么光穿透介质时速度会变化,具体涉及到的物理知识过多不在这里展开(可以看电磁场与电磁波或者光学的教材),只说最核心的结论便是:
- 光是一种电磁波,且在真空中的速度是299792458m/s(这个数字是物理学定义的精确值,不是估算值)
- 电磁波在任何介质中速度都会略慢,这个数值受到物体材质影响
- 不同频率的电磁波受到介质影响的效果不同,频率越高的电磁波减速越明显(这也是彩虹形成的主要原因)
这里附一个有趣的仿真,展示了通过混合红色、橙色、黄色、绿色、青色、蓝色和紫色形成的白色, 在经过三棱柱后产生了色散的现象,仿真示意图
(关于折射率在生活中的另一个应用就是眼镜片,镜片的材料通常会选择密度更低且折射率更高,且色散更小的材料,这样镜片就能轻巧舒适)
各种玻璃的折射率随波长的变化,阴影区域表示可见光的范围
镜片使用的理想材料应该是曲线越高且平直(阿贝数越高越好)
正因为光进入介质时发生了减速,所以宏观上看起来光线进入水面后,方向发生了变化。所以折射率还满足了如下的关系:折射率(n)是光在真空中的传播速度和光在某种介质中的传播速度之间的比值。这个关系可以用以下公式表示:
其中:
- n是折射率。
- c是光在真空中的传播速度。
- v是光在该介质中的传播速度。
从这个公式可以看出,折射率越大,光在该介质中的传播速度越慢。也就是说,光在介质中的传播速度与折射率成反比。
金红石(色散0.28,宝石中最高)、金刚石、莫桑石等宝石都有非常高的折射率,
且色散系数高,所以光照射在宝石上会有美丽的火彩现象
例如,在空气中的折射率大约是 1.00029,几乎与真空相同,因此我们可以近似理解为光速几乎不受影响;而在水中,折射率大约是 1.33,光速会减慢,约为真空中光速的 3/4。弄清楚折射率的由来,就可以结合传感器设计实验了
实验
首先弄清传感器传感器的原理,需要了解一些传感器手册里的名词:
FoI (Field of Illumination) 照明场:传感器的VCSEL发射激光器主动照明的范围。
FoV (Field of View) 视野场:传感器的接收端透过入射镜片能够检测到范围。
SPAD (Single Photon Avalanche Photodiode) 单光子雪崩二极管:即为传感器接收端的敏感元件。
TDC(Time to Digital Converter) 时间-数字转换器:负责记录发射激光开始,直到SPAD接收到信号的时间,并转换为数字记录在芯片中。
测量原理是使用由 VCSEL 产生一系列的脉冲。这些脉冲使用 MLA(微透镜阵列)来照射 FoI 。物体将这些光线反射回 TMF8821 的接收器光学透镜到达 SPAD 阵列上。再由 TDC 测量这些脉冲从发射到返回命中接收器的时间,并将这些时间信息累积到直方图内的区间中。由于我们认为常见的身边环境中光速并不会变化,于是激光飞行的时间和距离就成了一个固定的关系。所以这就是传感器被称作 ToF(Time-of-Flight)飞行时间的由来。
当 TMF8821 以 550 kHz 频率发射脉冲时,DC生成的直方图会清晰显示不同距离物体的反射峰,如图下所示:
芯片内部会解读直方图信息,以上图的2号和3号区域直方图为例,原先直方图 50 cm 的位置有一个物体,然后在 50 cm 处添加了新的物体后,直方图的 25 号数据位置处就会产生一个尖峰,代表 25号数据(对应50cm) 处有一个物体反射了发出的光线。
接下来便是真正的解题思路:
TMF8821 这个传感器输出的信息是每个方向的距离,这些距离的前提是将传感器放在空气中做测量,并利用每个方向光线照射到障碍物后返回的时间的一半乘以光在空气中传播的速度计算出物体的距离。反过来,将距离除以光速,就可以得到光子飞行的时间。
将传感器成功驱动后,通过串口可以看见面前的物体放的远近和数值一一对应。
但是我做的题目恰好利用了“常见的身边环境中光速并不会变化”的特殊情况,正如理论篇所述,在一些介质中,光速并不是以空气中的 米/秒速度前进。传感器实际上并不会知道面前的介质不是空气,光线被介质减速后,仍然以标准光速进行计算,所以传感器透过一定厚度的水测出的距离会比同样厚度的空气中距离更远。
实际上距离没有发生变化,是光速变化导致的飞行时间变化,进行一些简单的数学计算可以知道,
光线在介质中真实的速度,是光速乘以真实距离和透过介质测出的距离的比值。有了真实速度就可以计算出介质的折射率。
所以最终绕了一大圈之后求得了计算介质折射率的算式,接下来只需要找到一个合适的容器,足够厚且外壁足够薄以减少容器壁的干扰。我选择用薄亚克力组成的水槽。
为了增加反射的信噪比,我在水槽的一侧内壁贴上了一层黑色哑光塑料(过白或者镜面反射会增大误差)。
首先是测量空水槽的前后厚度,记为。
接下来将水槽中加满水,再次测量,记下距离为。
根据公式计算可以计算出,水的折射率约为 1.35 和理论值 1.33 接近。
测量结果\介质种类 | 空气 | 纯净水 | 饱和食盐水 | 食用油 |
---|---|---|---|---|
测量出的距离 | 118 | 159 | 164 | 174 |
计算得到的折射率 | 1 | 1.35 | 1.39 | 1.47 |
将液体换为饱和盐水(约1.39)、食用油(约1.47)重复实验,数据都与理论值接近。这部分的实验在视频中有介绍,感兴趣的朋友可以看视频内容。
项目设计思路
硬件方面:
LPC55S69主控:负责驱动dToF模块、高精度计时及数据处理;(因为实验室里正好有这个LPC55S69-EVK的板子,也比较熟悉,所以主控选择了它)
LPC55S69-EVK是恩智浦半导体推出的基于LPC5500系列双核Arm Cortex-M33微控制器的评估开发平台,运行频率高达150MHz。板卡上的扩展接口也与Arduino兼容,方便后续扩展更多功能,本次使用的就是Arduino扩展接口,按照上图进行连接。
dToF模块:发射/接收光脉冲,测量空气与液体中的飞行时间;
光学结构:设计透明容器固定液体,确保光脉冲垂直穿透介质。(此处有个小插曲,这里想要的其实就是一个前后壁平行装水的容器,想到最合适的就是购买一个鱼缸,但是玻璃制品快递发货不方便。于是在网上定制了一个正方体的亚克力盒子,想要和店家要求使用最薄的亚克力,但是很容易破裂,就折中选择了3mm的壁厚)
安装方式如上图所示,由于传感器端需要靠在容器上,不可避免会与液体接触,为了保证安全将传感器板和控制器的板卡分来固定,中间使用导线加固连接。
本次没有限制主控制器,所以使用了更顺手的 LPC55S69 做开发,为了方便实时查看测量的数据,使用 VOFA+ 作为电脑端的可视化工具。
static void log_result(struct tmf882x_msg_meas_results *result_msg) {
uint32_t distances_cm[TMF882X_MAX_MEAS_RESULTS] = { 0 };
if (!result_msg)
return;
for (uint32_t hit = 0, idx = 0; hit < result_msg->num_results; ++hit)
{
idx = result_msg->results[hit].channel - 1; // channel '0' is the reference channel
idx += result_msg->results[hit].sub_capture * 8;
distances_cm[idx] = result_msg->results[hit].distance_mm;
}
// Print distance results of the example channels
for (uint32_t idx = 0; idx < 9; ++idx)
{
PRINTF("%u,", result_msg->results[idx].distance_mm);
}
PRINTF("%1.2f\n", (result_msg->results[4].distance_mm)/118.0);//118是由传感器测量出的数据计算得来
return;
}
通信代码
传感器与主板之间通过I2C进行交互通信,在移植库函数中只需要完成使用I2C读写一段可变长度的寄存器即可,所以设计的读写函数具有以下传参:
从机地址、从机寄存器地址、收/发的缓存区指针、收/发的数据长度。
int32_t ams_i2c_write_block(uint8_t slave_addr, uint8_t reg, const uint8_t * buf, uint32_t len)
{
i2c_master_transfer_t masterXfer = {0};
status_t reVal = kStatus_Fail;
ams_i2c_reinit();
/* subAddress = 0x01, data = g_master_txBuff - write to slave.
start + slaveaddress(w) + subAddress + length of data buffer + data buffer + stop*/
masterXfer.slaveAddress = slave_addr;
masterXfer.direction = kI2C_Write;
masterXfer.subaddress = (uint32_t)reg;
masterXfer.subaddressSize = 1;
masterXfer.data = (uint8_t *)buf;
masterXfer.dataSize = len;
masterXfer.flags = kI2C_TransferDefaultFlag;
/* Send master non-blocking data to slave */
reVal = I2C_MasterTransferBlocking(EXAMPLE_I2C_MASTER, &masterXfer);
return reVal != kStatus_Success;
}
int32_t ams_i2c_read_block(uint8_t slave_addr, uint8_t reg, uint8_t * buf, uint32_t len)
{
i2c_master_transfer_t masterXfer = {0};
status_t reVal = kStatus_Fail;
ams_i2c_reinit();
/* subAddress = 0x01, data = g_master_txBuff - write to slave.
start + slaveaddress(w) + subAddress + length of data buffer + data buffer + stop*/
masterXfer.slaveAddress = slave_addr;
masterXfer.direction = kI2C_Read;
masterXfer.subaddress = (uint32_t)reg;
masterXfer.subaddressSize = 1;
masterXfer.data = buf;
masterXfer.dataSize = len;
masterXfer.flags = kI2C_TransferDefaultFlag;
/* Send master non-blocking data to slave */
reVal = I2C_MasterTransferBlocking(EXAMPLE_I2C_MASTER, &masterXfer);
return reVal != kStatus_Success;
}
传感器功能关键代码
在直播中听到老师介绍过,为了对不同应用场景,芯片可以加载不同的初始固件,以达到理想的运行效果,比如测量距离从0mm开始而不是从10mm开始。不过每次芯片上电后都需要把固件加载到传感器内置的处理器中,具体流程可以按照《TMF882X-Host-Driver-Communication》手册中所写:
使用的库函数中也与此段流程一一对应(下方代码为真正通信的部分,有一些输出log的函数已略去),需要注意的是要注意数据手册中有要求相关的时序,在程序中也要记得加上对应的延时才行。
static int32_t tmf882x_mode_bl_fwdl(struct tmf882x_mode *self, int32_t fwdl_type, const uint8_t *buf, size_t len)
{
int32_t rc = -1;
struct tmf882x_mode_bl *bl;
if (!verify_mode(self)) return -1;
bl = member_of(self, struct tmf882x_mode_bl, mode);
if (TMF882X_BL_ENCRYPT_FLAG) {
rc = tmf882x_mode_bl_upload_init(bl, BL_DEFAULT_SALT);
//此处发从第一个DOWNLOAD_INIT初始化命令,并接收到返回成功
}
switch (fwdl_type) {
case FWDL_TYPE_BIN://此处进入下载状态,开始循环发送BIN固件
rc = bin_fwdl(bl, buf, len);
break;
}
if (0 == rc)
// 退出BOOT加载模式,进入APP模式
tmf882x_mode_bl_close(self);
return rc;
}
static int32_t bin_fwdl(struct tmf882x_mode_bl *bl, const uint8_t *buf, size_t len)
{
tmf882x_mode_bl_addr_ram(bl, BL_DEFAULT_BIN_START_ADDR);//设置写入的初始地址
tmf882x_mode_bl_write_ram(bl, buf, len);//
tmf882x_mode_bl_ram_remap(bl);//写入完成后,将RAM指针复原,传感器进执行程序阶段
return 0;
}
int32_t tmf882x_mode_bl_write_ram(struct tmf882x_mode_bl *bl, const uint8_t *buf, int32_t len)
{
struct tmf882x_mode_bl_write_ram_cmd *cmd = &(bl->bl_command.write_ram_cmd);
int32_t idx = 0;
int32_t num = 0;
uint8_t chunk_bytes = 0;
int32_t error = 0;
int32_t rc;
if (!verify_mode(&bl->mode)) return -1;
do {
cmd->command = BL_CMD_WR_RAM;//设置命令为写入模式
chunk_bytes = ((len - num) > BL_MAX_DATA_SZ) ? BL_MAX_DATA_SZ : (uint8_t) (len - num);
cmd->size = chunk_bytes;
for(idx = 0; idx < cmd->size; idx++) {
cmd->data[idx] = buf[num + idx];
}
/* add chksum to end */
cmd->data[(uint8_t)cmd->size] = tmf882x_calc_chksum(get_bl_cmd_buf(bl), BL_CALC_CHKSUM_SIZE(cmd->size));
rc = tmf882x_mode_bl_send_rcv_cmd(bl);
if (!rc)
num += chunk_bytes;
error = error ? error : (rc ? rc : 0);
} while ((num < len) && !rc);//循环发送固件片段
return error;
}
初始化完成后需要配置SDAP为3*3模式,这样正中心的距离就是垂直距离,我只需要取出最中心的数值作为距离带入计算即可。
对着手册设置对应的寄存器,如图将SPAD_MAP_ID寄存器的数值设置为1。
tofcfg.spad_map_id = TMF8828_COM_SPAD_MAP_ID__spad_map_id__map_no_1;
tmf882x_ioctl(&tof, IOCAPP_SET_CFG, &tofcfg, NULL);
完成配置之后就可以进入正式的程序,需要注意的是要注意数据手册中有要求相关的时序,在程序中也要记得加上对应的延时才行。
主程序中等待传感器采集完成后会触发中断,在中断里更新采样的结果,具体的工作流程在代码中详细展示。
主要程序流程图
功能展示
按照上图进行实现,由于每次实验使用的是同一个容器,且传感器固定在了侧面,便将容器的前后厚度直接固定在了代码中,下图依次是纯水,食用油,饱和食盐水(1.35,1.47,1.39)
只需要提前根据溶液的种类,确定折射率范围,做成查找表固化在程序中,最终由程序进行分类,就可以做到自动测量与分类。
PS:传感器也有基于直方图直接读取的模式,如果直接使用直方图,应该可以解决一些反射造成的伪影,可以作为后期的尝试方向。
总结
总结一下,实验结果的误差主要来源于两方面:一是测量精度的限制,二是为简化计算未扣除容器壁的折射影响(如亚克力材质对光路的干扰,下图官方的光学设计指导中也提到了容器壁以及空气间隙对结果的影响)。
此外,实验中材料和反射条件的选择也需优化——例如白色反光贴纸可能导致信号过饱和,改用灰色材料可平衡信噪比。
在测试不同液体时发现:
1、饱和食盐水折射率与水接近
虽然氯化钠溶解度不高(常温下约36g/100g水),但实际配制时可能因溶解不充分或浓度不足,导致离子密度提升有限,折射率变化未显著体现。
2、食用油测量偏差较大
推测主要原因是买来的稻米油里有悬浮杂质。倒油时,油在容器中产生气泡,气泡也会散射光线,干扰 SPAD 接收信号;而杂质则导致介质折射率局部不均匀,影响光路稳定性。
3、溶剂选择局限性
理论上,使用高溶解度溶质(如蔗糖)可更显著改变液体折射率,但因时间限制未进一步验证。(要是有一块10cm厚的金刚石,拿来测折射率肯定现象更加明显)
4、传感器未对准导致的误差
在前期测试时发现,如果传感器并没有平行贴在侧面,有一些误差的话,会出现距离变化。
改进方向:
预处理样品:通过离心或者过滤布去除油中气泡与悬浮物,或选用高纯度试剂级食用油;
优化溶质:选择折射率梯度大的溶质(如丙三醇),或者折射率很高且水中溶解度很大的材料,配制不同浓度溶液进行对比;如下图就是常见盐的溶解度表,可以按照这个图挑选易溶的物质。
容器校准:单独测量空槽壁厚度,从总光程中扣除其折射效应。或者更换更大的容器以降低容器壁的影响(如使用深的水桶,从水面上进行测量)PS:在小卖部看到了一个饮料瓶子,整体是长方体的,也许这样的瓶子更能减少容器壁的误差。
算法升级:猜想如果板卡没有平行贴紧,就会和对侧的容器壁有夹角,那么应该可以利用第一题的原理,消除夹角带来的误差,即使有夹角也可以自动消除,计算出更准确的结果。
发散一下,如果已知测量的溶质和标准折射率,可以将这个项目扩展一下功能,通过测量不同的折射率,反算出溶质的浓度,应该会在工业生产上有较好的应用。
心得体会
本次活动的出题很有趣,是一个非常好的学科交叉。看完数据手册之后有了解题思路,设计实验时想把这次活动当成一次科普来做。本次活动将物理现象和理论结合起来,是一次非常好的融合学科的尝试,在这次实验的过程中,我复习到多年前学习的物理知识,又将物理知识和电子知识结合,真是一次非常有意义的开发活动,感谢官方大大,祝硬禾活动越办越好!