简介:本资源为2017年全国大学生电子设计竞赛(电赛)‘可见光室内定位’赛题的完整实战解决方案包,面向电子信息类、自动化、计算机等专业本科生,专为备赛训练、课程设计及嵌入式系统实践提供可运行参考。压缩包共280个文件,涵盖67个头文件(h)、60个C源码(c)、33个编译中间文件(o/d)及14份PDF技术文档,包含STM32F4平台下的TIM/RTC/RCC外设驱动、MPU姿态解算、LCD显示控制等核心模块代码,辅以Keil工程配置(uvprojx/uvoptx)、调试脚本(bat)及HEX/AXF可执行镜像,结构完整、即下即用。已有198人学习下载,所有程序均经实测验证,支持硬件快速部署与算法逻辑复现,特别适合电赛新手理解光信号调制解调、多基站时间差定位(TDOA)实现及嵌入式实时系统协同开发全流程。
1. 项目缘起:一个被低估的经典赛题
2017年的全国大学生电子设计竞赛,有一个题目在当年和之后很长一段时间里,都让很多参赛者和电子爱好者印象深刻,那就是“可见光室内定位装置”。这个赛题的核心,是要求参赛者利用可见光(通常是白光LED)作为信号载体,在室内实现一个简单的定位系统。题目文件通常以“2017年电赛-可见光室内定位.zip”这样的压缩包形式流传,里面包含了当年的赛题要求、评分标准等原始文档。
乍一看,这个题目有点“科幻”——用灯光就能知道自己在屋里的哪个位置?但深入下去,你会发现它巧妙地融合了通信、信号处理、单片机控制和几何算法等多个电子工程的核心领域。它不像一些纯算法或纯硬件的题目那样偏科,而是要求参赛者具备系统级的整合能力。从LED驱动电路的设计,到光信号的调制解发射,再到接收端的光电转换、信号解调,最后到定位算法的实现与显示,一整条链路都需要自己打通。这正是电赛题目的魅力所在,也是它成为经典的原因:它模拟了一个真实、完整的小型产品开发流程。
我之所以对这个项目念念不忘,是因为它揭示了一个在物联网和智能家居背景下非常实际的需求:低成本、高精度的室内定位。虽然我们现在有Wi-Fi、蓝牙信标等方案,但在一些对电磁敏感、或者需要极高更新频率、极低成本的场景下,可见光定位(Visible Light Positioning, VLP)依然有其独特的优势。通过复现和深化这个项目,我们不仅能重温经典,更能理解一种有别于无线电的定位技术思路,这对于拓宽技术视野非常有帮助。
2. 系统核心原理:光如何成为“坐标尺”
要理解可见光室内定位,首先要抛开“照明”的固有印象,把LED灯看作一个无线信号发射器。它的核心原理与无线电定位(如GPS)类似,都是利用信号传播的一些特性来反推位置,只不过载体从无线电波变成了光波。
2.1 三种主流的定位原理
在这个赛题语境下,通常有三种基础原理可供选择,它们各有优劣,也直接决定了后续硬件和软件的复杂度。
2.1.1 基于接收信号强度(RSSI)的定位
这是最直观,也是当年很多队伍首选的方法。其原理是:光在空气中传播时,强度会随着距离的增加而衰减(遵循平方反比定律)。如果我们已知发射灯的位置和其发射功率,通过测量接收端(如光电二极管)收到的光强,就能估算出接收端到该灯的距离。
但这里有个巨大的坑:理想很丰满,现实很骨感。光强的衰减模型在理想均匀介质中才成立。在实际房间里,墙壁、家具的反射(多径效应),人员走动造成的遮挡,甚至LED灯本身的老化、供电电压的波动,都会严重影响接收光强的读数。这就导致单纯靠RSSI算出的距离误差非常大,可能达到米级,很难用于精确定位。所以,纯RSSI方案往往需要极其复杂的现场校准和补偿算法,并不推荐作为唯一依据。
2.1.2 基于到达时间差(TDOA)的定位
这是一种更高精度的方法。其思路是:让多个已知位置的LED灯同步发射带有时间戳的光信号。接收端会先后收到这些信号,通过计算信号到达的时间差,可以推算出接收端到每两个灯之间的距离差。这就在空间中形成了一条双曲线,多条双曲线的交点就是接收端的位置。
TDOA的精度理论上可以做到厘米级,但它对硬件的要求极高。首先,需要所有发射灯之间实现高精度的时间同步(纳秒级),这本身就是一个挑战。其次,接收端需要能精确测量光信号到达的微小时间差,这对接收电路的时间分辨率(带宽)和时钟稳定性提出了苛刻要求。在当年的比赛时限和常用单片机(如STM32)的性能下,实现稳定可靠的TDOA非常困难。
2.1.3 基于身份编码(ID)与三角测量的定位
这是当年平衡了难度与精度之后,最可行、最主流的方案,也是我们今天重点讨论的。它的核心思想可以概括为“认灯”+“测角”。
- 身份编码(认灯):我们不在光信号里传递复杂的时间戳或强度信息,而是让每个LED灯发射一个唯一的、简单的数字ID编码(比如001, 010, 100)。这个编码通过一种叫“开关键控(OOK)”的方式调制到LED的亮灭上。接收端的光电传感器接收到明暗变化的光信号后,解调出这个ID,从而知道自己“看到”的是哪一盏灯。
- 角度感知(测角):如何通过“看到”了哪盏灯就知道位置呢?这里的关键是,接收端不是一个简单的“光强计”,而是一个有方向性的“光学探测器”。通常,我们会使用一个遮光筒套在光电二极管(或光敏电阻)外面。遮光筒就像一个望远镜的镜筒,只有正对光源方向的光线才能直射进来,侧面来的光大部分被筒壁遮挡。这样,当接收端旋转到信号强度最大时,遮光筒的轴向就大致指向了光源的方向。
- 三角测量:在室内固定位置安装至少两个(最好三个)已知坐标的LED信标灯。接收端通过旋转,分别找到对准每个灯的方向(即方位角)。知道了两个发射点的位置和接收端看向它们的两个方向线,这两条方向线的交点就是接收端自身的位置。这就是最简单的三角定位法。
这个方案巧妙规避了测距不准和同步难的痛点,将问题转化为相对容易实现的编码识别和方向寻找。精度主要取决于方向测量的精度,而这可以通过机械结构(旋转云台)和软件算法(峰值搜索)来优化。
2.2 系统整体架构设计
基于ID编码+三角测量的方案,整个系统可以划分为发射端、接收端和显示端三大部分。
- 发射端:由多个(通常2-4个)固定在室内已知坐标点的LED灯组成。每个灯由一个单片机(如51、STM32)控制,以不同的频率或编码循环发送自己唯一的ID。LED需要工作在一定的频率(几百Hz到几KHz),以避免与环境光(50/60Hz工频)干扰,同时要让人眼不易察觉闪烁。
- 接收端:这是系统的核心。主要包括:
- 光学接收头:光电二极管+遮光筒,负责将光信号转换为微弱的电信号。
- 信号调理电路:包括前置放大器、带通滤波器、比较器。用于放大微伏级的有用信号,滤除环境光和其他噪声,并将模拟信号整形成单片机可以识别的数字方波。
- 主控单片机:负责解码数字方波中的ID,并控制一个舵机(或步进电机)旋转接收头。通过扫描,找到信号最强的角度,即为对准某个灯的方向。同时,它还需要通过串口等通信方式将角度数据发送出去。
- 显示端:通常是一个上位机软件(如Qt、LabVIEW编写)或另一个带屏幕的单片机。它接收来自接收端的角度数据,根据已知的信标灯坐标,运行三角定位算法,实时计算出接收端的位置(X, Y坐标),并在平面图上动态显示出来。
3. 硬件设计与选型:从光电转换到信号整形
硬件是实现一切算法的基础,在这个项目中,硬件设计的优劣直接决定了系统能否稳定工作。下面我们拆解几个关键模块。
3.1 发射端:如何让LED“说话”
发射端的任务很简单:让LED灯以特定的频率和编码规律闪烁。但“简单”背后有细节。
3.1.1 LED驱动电路
不能直接用单片机的IO口驱动大功率LED。IO口的驱动能力有限(通常20mA左右),且压降会导致LED亮度不足、不稳定。我们需要一个简单的驱动电路。 一个经典的方案是使用NPN三极管(如S8050)或MOS管(如SI2302)作为开关。单片机IO输出高电平时,三极管导通,电流从VCC流经LED和限流电阻、三极管到地,LED点亮。限流电阻R的计算很关键:R = (VCC - Vf_led - Vce_sat) / I_led。其中Vf_led是LED正向压降(白光约3.0-3.4V),Vce_sat是三极管饱和压降(约0.2V),I_led是期望的LED工作电流(根据亮度需要,可选20-100mA)。例如,VCC=5V, Vf=3.2V, Vce=0.2V, I=50mA, 则R = (5-3.2-0.2)/0.05 = 32Ω, 可选择33Ω的电阻。确保LED在调制频率下能快速亮灭。
3.1.2 编码与调制
为了让每个灯有唯一身份,我们需要编码。最简单的就是用不同频率的方波。例如,灯A以1kHz闪烁,灯B以2kHz闪烁。接收端通过测量信号频率来区分它们。但频率容易受干扰,且需要较复杂的测频电路。 更可靠的方法是数字编码。例如,采用曼彻斯特编码或自定义的帧结构。一个简单的帧可以设计为:同步头(一段特定频率的方波,如10个1kHz脉冲) + 数据位(用脉冲宽度表示,如短宽代表0,长宽代表1) + 停止位。这样,每个灯循环发送自己的ID(如3位二进制)。单片机使用一个定时器产生PWM或精确的延时即可实现。
3.2 接收端:如何从噪声中提取“声音”
接收端是硬件设计的难点,也是最容易出问题的地方。它的任务是把微弱的、淹没在强环境光噪声中的有用光信号,变成干净的数字信号送给单片机。
3.2.1 光电传感器选型
常用有两种:光电二极管和光敏电阻。
- 光电二极管:响应速度快(可达微秒级),非常适合我们这种几百Hz到KHz的调制信号。它工作在反向偏压模式时,线性度好,噪声低。是首选。
- 光敏电阻:成本低,但响应速度慢(毫秒级),对于快速变化的信号会严重失真,导致解码失败。不推荐用于此项目。
我强烈建议使用光电二极管,型号如BPW34(硅PIN光电二极管),它价格便宜,响应快,光谱范围也覆盖可见光。
3.2.2 核心:跨阻放大器(TIA)设计
光电二极管输出的是微弱的电流信号(几微安到几十微安)。第一步就是把这个电流信号转换成电压信号,并且要放大。这里绝对不能使用普通的同相或反相电压放大器,因为光电二极管的高输出阻抗会导致信号严重衰减。 必须使用跨阻放大器。它的原理是将光电二极管接在运放的反相输入端和输出端之间,利用负反馈,将电流直接线性地转换为电压:Vout = -Iph * Rf。其中Iph是光电二极管产生的光电流,Rf是反馈电阻。
- 运放选型:必须选择低偏置电流、低噪声的运放。通用运放如LM358的偏置电流太大(纳安级),会淹没微安级的光信号。推荐使用JFET输入型或CMOS输入的运放,如TLV2462、OPA344、MCP6002等,它们的偏置电流在皮安级。
- 反馈电阻Rf:决定放大倍数。假设光电流最大50uA,我们希望输出电压最大2.5V,则Rf = 2.5V / 50uA = 50kΩ。可以先用一个50kΩ的可调电阻调试,确定最佳值后换为固定电阻。
- 反馈电容Cf:这是一个关键且易忽略的元件。它并联在Rf上,用于抑制高频噪声和防止振荡。其值很小,通常在几皮法到几十皮法。没有它,电路可能自激振荡,输出杂乱无章。可以通过实验确定:用示波器观察输出,调整Cf直到信号清晰稳定。
3.2.3 滤波与整形电路
TIA输出的电压信号仍然叠加着大量的噪声(环境光、工频干扰、电路噪声)。我们需要滤波。
- 带通滤波器:因为我们知道发射信号的频率范围(比如500Hz-3kHz),可以设计一个带通滤波器,只让这个频段的信号通过,极大地抑制带外噪声。可以使用一个高速滤波器(截止频率500Hz)和一个低通滤波器(截止频率3kHz)级联构成。运放可以复用TIA后的另一路。
- 电压比较器:经过放大滤波后的信号是模拟的正弦波或类正弦波。我们需要将其转换为单片机可以读取的数字方波(0或3.3V)。这就是比较器的作用。将滤波后的信号接入比较器同相端,反相端接一个可调的参考电压(通过电位器分压得到)。当信号电压高于参考电压时,输出高电平;反之输出低电平。这样,光信号的“有/无”或“强/弱”就被转化为了数字脉冲。
- 比较器滞回(施密特触发器):非常重要!如果比较器没有滞回,当输入信号在参考电压附近有微小波动时,输出会产生一连串抖动的毛刺,导致解码错误。我们需要引入正反馈,形成一个电压窗口(滞回电压)。例如,当输出为高时,比较阈值自动抬高一点;输出为低时,阈值自动降低一点。这样就能有效消除抖动。可以在比较器输出端和同相输入端之间连接一个较大的反馈电阻(如10MΩ)来实现。
3.2.4 方向性接收与机械结构
为了测量方向,我们必须让接收头有方向性。这就是“遮光筒”的作用。
- 材料:使用不透光的材料,如黑色亚克力管、涂黑的纸筒或金属管。
- 尺寸:长度和内径需要权衡。筒越长,方向性越好(波束越窄),定位越准,但接收到的光强也越弱,可能影响信噪比。筒内径越小,方向性也越好。一个常见的折中尺寸是:长度5-10cm,内径1-2cm。
- 安装:光电二极管必须安装在遮光筒的底部中心位置,确保只有正前方的光能直射到感光面。整个接收头需要牢固安装在一个舵机的摆臂上,由单片机控制其进行0-180度的扫描。
4. 软件算法实现:解码、寻向与定位
硬件提供了干净的脉冲信号,软件的任务是解读这些信号,并计算出最终位置。
4.1 发射端固件:生成可靠编码
发射端单片机程序相对简单,核心是利用定时器中断产生精确的时序。
// 伪代码示例 (以灯A, ID=001为例) #define ID_A 0b001 #define SYNC_PULSES 10 #define BIT_PERIOD_MS 5 // 每位持续5ms void Timer_IRQ_Handler() { static int state = 0; static int bit_count = 0; static int sync_count = 0; switch(state) { case 0: // 发送同步头 if(sync_count < SYNC_PULSES) { LED_TOGGLE(); // 以1kHz频率翻转LED sync_count++; } else { state = 1; sync_count = 0; bit_count = 0; } break; case 1: // 发送数据位 if(bit_count < 3) { // 假设ID为3位 int bit_val = (ID_A >> (2-bit_count)) & 0x01; // 从最高位开始发 if(bit_val == 1) { // 发“1”:高电平持续时间长,例如3ms高,2ms低 if(当前时间 < 3ms) LED_ON(); else LED_OFF(); } else { // 发“0”:高电平持续时间短,例如1ms高,4ms低 if(当前时间 < 1ms) LED_ON(); else LED_OFF(); } // 检查一位是否发完 if(当前时间 >= BIT_PERIOD_MS) { 重置计时; bit_count++; } } else { // ID发完,回到发送同步头状态 state = 0; } break; } }关键在于定时精准和抗干扰。可以在代码中加入看门狗,防止程序跑飞导致LED发送异常。
4.2 接收端固件:解码与角度测量
接收端程序是核心,主要包括信号解码和舵机控制扫描两大任务。
4.2.1 解码算法
比较器输出的数字脉冲信号接入单片机的外部中断引脚或带输入捕获功能的定时器引脚。
- 方法一:输入捕获+脉冲宽度测量。利用定时器的输入捕获功能,测量脉冲的高电平宽度。根据我们定义的编码规则(例如,同步头是固定数量的等宽脉冲,数据位“1”是宽脉冲,“0”是窄脉冲),来解析数据。
- 首先寻找同步头:连续检测到多个(如10个)宽度在预期范围内的脉冲。
- 同步头确认后,开始解析后续的数据位,测量每个脉冲周期内高电平的持续时间,与阈值比较,判断是“1”还是“0”。
- 组合几位数据,得到ID。进行简单的校验(如奇偶校验),通过后则认为成功解码一个灯的信号。
- 方法二:外部中断+软件计时。如果没有足够的输入捕获通道,可以用外部中断+普通定时器实现。在中断服务程序里记录引脚电平跳变的时间戳,然后在主循环中处理这些时间戳序列,同样通过判断脉冲宽度来解码。
解码程序必须非常健壮,要考虑信号丢失、噪声毛刺等情况。我的经验是:加入“超时重置”机制。如果在预期时间内没有收到完整的同步头或数据帧,就将解码状态机重置到初始搜索状态,避免“卡死”在某个错误状态。
4.2.2 舵机控制与角度搜索
解码成功后,我们需要知道当前接收头指向哪个角度时,信号最强(即成功解码)。这需要一个搜索过程。
- 初始化:舵机归零(指向一个起始角度,如0度)。
- 扫描:单片机控制舵机以固定的步进角度(如1度或2度)逐步旋转。
- 采样:在每个角度停留一小段时间(如100ms),在这段时间内尝试解码信号。如果成功解码到某个ID,则记录下当前角度和接收到的信号强度(可以通过ADC采样比较器前的模拟信号电压,或统计单位时间内解码成功的次数作为“强度”指标)。
- 峰值判断:通常,信号强度会随着指向角度偏离光源方向而衰减。我们可以记录下扫描整个范围内,信号强度最大时对应的角度,即为该光源的方位角。
- 优化策略:全范围(0-180度)扫描比较耗时。可以第一次粗扫(步进5度)找到信号大致区域,第二次在该区域附近细扫(步进1度),提高效率和精度。
4.3 上位机定位算法:从角度到坐标
接收端通过串口将测量到的角度数据(例如:ID:A, Angle:45.3)发送给上位机。上位机(或另一个负责显示的单片机)进行三角运算。
假设我们使用两个灯进行定位(二维平面最少需要两个参考点)。已知:
- 灯1坐标:(x1, y1)
- 灯2坐标:(x2, y2)
- 接收端测得的相对于正北(或某个参考方向)的方位角:θ1(指向灯1), θ2(指向灯2)。
求接收端坐标 (x, y)。
根据几何关系,对于每个灯,可以得到一条方向线方程:tan(θ1) = (y - y1) / (x - x1)tan(θ2) = (y - y2) / (x - x2)
这是一个二元一次方程组。理论上,只要θ1不等于θ2(即接收端不在两个灯的连线上),就能解出唯一的(x, y)。在实际编程中,直接解这个方程即可。使用三个灯时,可以通过最小二乘法求最优解,能有效降低单个角度测量误差的影响。
一个重要的细节:角度系统的统一。单片机测量的舵机角度是相对于其机械零点的。而上位机计算用的角度(如θ1, θ2)应该是相对于统一的坐标系(比如房间的东北方向)。因此,在安装发射灯时,需要记录下每个灯在房间坐标系中的坐标,同时还需要通过一次校准,确定接收头“零度”方向对应房间坐标系的那个方向,并在角度数据发送前进行换算。
5. 调试心得与避坑指南
这个项目从电路焊接、程序编写到系统联调,每一步都可能遇到意想不到的问题。下面分享几个我踩过的坑和总结的经验。
5.1 硬件调试:没有示波器,寸步难行
- 问题现象:接收端无论如何都解不出码,或者解码极不稳定。
- 排查步骤:
- 先查发射:用手机摄像头(很多手机CMOS有卷帘快门)对着LED灯看,应该能看到明显的暗条纹,说明LED在高速闪烁。这是最粗略的检查。
- 关键点测量:用示波器探头,从后往前查。
- 测比较器输出:看是否有规则的方波脉冲?如果没有,或者全是噪声,问题在前级。
- 测运放输出(带通滤波器后):看是否有清晰的正弦波?幅度是否足够?如果这里信号很好但比较器输出不对,调整比较器的参考电压,或者检查是否忘了加滞回。
- 测TIA输出:这里应该能看到被放大但依然混杂着低频噪声的信号。如果信号非常微弱,检查光电二极管是否接反(光电二极管有正负极),检查Rf阻值是否合适,检查运放供电是否正常。
- 测光电二极管两端电压:在无光时,反向偏压是否正常?
- 常见坑点:
- 电源噪声:模拟电路部分(尤其是TIA)对电源噪声非常敏感。一定要用线性稳压电源(如LM7805)为模拟部分单独供电,并与数字部分(单片机、舵机)的电源用磁珠或0Ω电阻隔离。舵机启停的瞬间会产生很大的电流毛刺,足以让整个模拟前端失灵。
- 虚焊与接触不良:特别是运放、比较器这种多引脚器件,以及反馈电阻、电容这类小元件,一定要焊接牢固。有时问题就是某个引脚虚焊导致的。
- 环境光干扰:虽然带通滤波器能滤除大部分,但强烈的日光灯或窗户自然光仍可能使光电二极管饱和。可以尝试在遮光筒前端加一个可见光透过、红外截止的滤光片,因为很多白光LED含有蓝光芯片激发荧光粉,其光谱与日光有一定区别,滤光片能提升一些信噪比。
5.2 软件调试:逻辑与时序的陷阱
- 问题现象:角度测量跳动大,定位点飘忽不定。
- 排查步骤:
- 固定测试:将接收头固定对准一个灯,看解码是否连续稳定。如果不稳定,检查解码程序的抗干扰处理(如脉宽容差范围是否设得太小)。
- 检查角度数据:让接收头缓慢匀速旋转,通过串口打印角度和对应的信号强度值。观察信号强度随角度变化的曲线是否平滑,峰值是否明显。如果曲线很平缓,说明遮光筒方向性不好,需要加长或减小内径。
- 舵机控制:舵机本身有回差,且不同舵机的中位脉宽可能有细微差异。确保你发送的PWM脉宽与舵机角度是线性对应的。最好对每个舵机进行校准:发送几个已知角度(0, 90, 180),用尺子测量实际转动角度是否吻合。
- 经验技巧:
- 数字滤波:对于解码得到的角度,不要直接用单次测量值。可以采用滑动平均滤波或卡尔曼滤波。例如,连续测量10次角度,去掉最大最小值后取平均,能有效平滑抖动。
- 异常值剔除:在三角定位计算时,如果某个角度值明显偏离历史值或物理上不可能(例如角度突变超过30度),应该将其视为无效数据丢弃,等待下一次有效测量。
- 分时处理:单片机同时要控制舵机、解码信号、通信,任务繁重。好的程序结构很重要。建议使用状态机+定时中断的方式。主循环负责调度,定时中断负责精确定时(如舵机PWM生成、解码时间窗口判断),避免使用
delay()这类阻塞函数。
5.3 系统集成:误差来源与精度提升
即使每个模块单独工作都正常,整个系统联调时精度可能还是不理想。误差主要来自几个方面:
- 机械误差:舵机安装不水平,遮光筒与舵机旋转轴不同心。
- 标定误差:发射灯在房间内的实际坐标测量不准;接收头“零度”方向与房间坐标系的对齐不准。
- 算法误差:三角定位公式在角度接近90度时,tan函数变化剧烈,会放大测量误差。因此,两个发射灯与待测点形成的夹角,最好在30度到150度之间,避免三点接近一条直线。
提升精度的小技巧:
- 多点测量取平均:让接收端在同一个位置多测几组角度数据,上位机计算多个定位结果后取平均。
- 引入高度信息:如果房间高度已知,且接收端高度固定(例如放在桌子上),可以将三维定位问题简化为二维,有时能减少一个参考点。
- 软件补偿:通过大量测试,建立一个“角度测量值-真实角度”的误差查找表,在软件中进行补偿。
回顾整个项目,从理解原理、设计电路、编写代码到调试优化,是一个完整的工程实践闭环。它锻炼的不仅仅是某个知识点,而是解决一个复杂、跨学科问题的系统能力。虽然可见光定位目前还不是消费市场的主流,但通过动手实现它,你对信号处理、嵌入式系统、控制算法的理解会上一个实实在在的台阶。那些调试电路时对着示波器波形苦思冥想的夜晚,那些修改代码后终于看到定位点稳定出现在屏幕上的瞬间,才是这个项目带给一名工程师最宝贵的财富。
本文还有配套的精品资源,点击获取