夜骑团练的时候,最怕的不是对向远光灯,而是前面车友的车灯突然切到爆闪——你根本判断不出来他是要减速、要拐弯,还是单纯误触了开关。我一脚重刹差点追尾,那天晚上回到家就开始琢磨一件事:车灯为什么不能像心率带和码表一样,接入我随身带着的那套骑行传感网?
这个项目就是我做的一个原型车灯:基于ANT+协议,让车灯实时接收心率、速度、踏频这些骑行数据,并根据骑行状态自动调整亮度。说直白一点,就是让灯“知道”你此刻是在爬坡、巡航、冲刺,还是已经停车休息,然后自己决定亮多少、闪不闪、切换成什么模式。ANT+协议是骑行生态里最常见的低功耗无线方案,码表、心率带、速度计、踏频计甚至功率计都靠它互联,车灯接入这个网络后,不再是一个孤立的“电动手电”,而是整个骑行数据链路上的又一个执行节点。
这个项目适合两类人:一类是玩骑行装备的发烧友,想知道“智能车灯”背后到底怎么工作;另一类是嵌入式或DIY方向的开发者,想用ANT+协议栈做一个能跑通的小设备。全文涉及硬件选型、协议配置、固件逻辑、实测结果和排错记录。我不堆概念,只讲在实际做这个灯的时候踩过的坑和验证过的路子。
1. 为什么车灯要成为骑行数据网的一个节点
1.1 传统车灯的痛点不是亮度,而是“无脑”
市面上大多数骑行灯只有三种控制方式:手动开关、震动感应、光感自动。手动开关在骑行过程中操作非常危险,尤其在冬季戴厚手套的时候,想切个模式都要停车。震动感应的问题是误判多,过减速带会亮,放在包里晃一下也会亮。光感自动只解决“天黑开灯”这个逻辑,它不知道你在平路还是下坡,不知道你的心率已经爆到175,更不知道你其实已经在路边停了五分钟。
真正让我下定决心改装的是一次晨骑:天色刚暗,我开着低亮度在城市路段溜车,心率不高,速度也不快,但刚好遇到一长段下坡,速度瞬间到了45km/h左右。此时低亮模式在高速场景下根本无法提供足够的视野,而我要分心去摸车灯按钮,手一离开车把,整辆车就开始发飘。那次之后我清楚了一件事:车灯的问题不是“不够亮”,而是“不知道什么时候该亮、该亮多少”。
ANT+协议恰好能解决这个信息缺失。骑行时身上已经有心率带,花鼓或曲柄上有速度/踏频传感器,车把上挂着码表——这些设备通过ANT+网络持续广播数据。车灯如果能监听这些广播,就等于有了“眼睛”和“感知器”,可以根据真实骑行状态做照明决策。
1.2 ANT+ 和 BLE 的关键区别:为什么是ANT+而不是蓝牙
很多人会问:现在蓝牙BLE这么普及,手机上都用BLE,为什么车灯不直接用BLE?
确实可以用,但从骑行场景来看,ANT+有几个BLE比不了的优势。ANT+网络是主从广播模式:一个传感器(比如心率带)可以同时向多个接收设备广播数据,码表能收到,车灯也能收到,健身房跑步机还能收到,不需要额外配对。BLE通常是一对一配对连接,链接建立后还要维持连接状态,连接被占用或断开会影响其他设备的交互。对于车灯这种“应该安静工作、不打扰你”的配件来说,ANT+这套无感接入的方式非常合适。
另外,ANT+的功耗控制做得相当极致。ANT+射频在0 dBm发射功率下,单次广播电流消耗大约只有十毫安级别,而且ANT协议栈支持极短的唤醒周期,设备大部分时间可以处于深度睡眠。对车灯这种由18650电池供电、本身又要带大功率LED的系统来说,无线部分的功耗几乎可以忽略不计。BLE在待机和广播功耗上其实也不差,但要维持一个连接状态,整体链路开销更高。
当然,BLE也有它的优势:有了BLE,灯就可以配合手机App做详细配置、固件升级。我的做法不是二选一,而是把ANT+作为主链路,后续考虑用双模芯片把BLE作为配置通道补上。前期跑通ANT+才是核心目标。
1.3 市面上的“智能车灯”给我们的参考
SIGMA的Sportlight系列是市面上较早把ANT+集成进车灯的产品,它能通过ANT+接收心率信号,在心率超过阈值时自动增强亮度。Lezyne也做过类似概念的灯。这些产品的存在说明了一个事实:ANT+车灯本身不是伪需求,而是产业界验证过方向但还没做到足够低成本和可玩性高的细分领域。
我查过一些拆解和用户反馈,发现这些成品灯有一个共性问题:它们只实现了“心率触发高亮”这一个逻辑,没有把速度、踏频、停车检测这些数据综合起来。你仍然需要一个码表去配对这些传感器。我的项目没有走“复刻成品”的路线,而是把灯作为一个独立的ANT+监听节点,自行融合多路数据,做更细分的照明策略。这个思路本质上是在底层协议上做文章,也是整篇文章最值得展开的部分。
2. 硬件选型与电路设计:从RF芯片到LED驱动的关键取舍
2.1 射频主控:从nRF24LE1到nRF51系列
整个项目的核心是选一颗能跑ANT+协议栈的射频SoC。ANT+联盟的芯片方案基本都集中在Nordic平台,市面上能买到的ANT+模块绝大多数用的是nRF24LE1或nRF51系列。nRF24LE1是Nordic早期的经典方案,内置8051内核和2.4GHz收发器,ANT协议栈以库的形式运行,CPU需要自己调度;nRF51系列(比如nRF51422)是后来的ARM Cortex-M0方案,协议栈更加成熟,支持并发多通道和更复杂的睡眠管理。
我最终选择了nRF24LE1模块来做第一版原型。原因有三个:第一,模块封装相对友好,市面上有大量带陶瓷天线的现成模组,焊接难度低;第二,nRF24LE1的ANT协议栈资料比较全,很多老外博客和GitHub项目都做过类似的车灯或传感器改造;第三,成本低,模块价格控制在二十元以内,前期就算烧掉几块也不心疼。
如果你准备直接做第二版,我建议考虑nRF51系列。原因在于nRF51支持BLE和ANT+双协议栈,后续想接手机App的时候不用重新做硬件。不过nRF51的下载调试需要SWD接口,需要一只J-Link,工具链门槛比nRF24LE1要高一截。新手从nRF24LE1起步,把整条链路跑通后再移植,这个路径更平滑。
2.2 LED驱动和电源架构:大电流和射频灵敏度不能打架
车灯的负载和射频模块是两套完全不同的电气系统。LED一颗就吃掉几百毫安到一两安培的电流,如果驱动电路设计不好,开关噪声会直接干扰2.4GHz射频前端的灵敏度,表现就是ANT+距离骤减、甚至完全连不上码表。
我选了一颗Cree XM-L2灯珠,额定功率10W,实际控制在5W左右,光通量大约500lm,足够城市夜骑和郊区照明。LED驱动用的是AMC7135线性恒流方案,一颗芯片恒流350mA,并联三颗就是1.05A,配合PWM调光。线性驱动的优势是电路简单、无电感辐射噪声,缺点是效率低,多出来的能量变成热量。好在车灯本身就是铝制外壳,灯珠贴在铝基板上,散热路径直接,5W级别的发热完全压得住。
电池方面,我用一节18650电芯(3400mAh),标称电压3.7V,直接给AMC7135供电。3.7V驱动一颗白光LED,导通压降约3.2V,AMC7135上压降约0.5V,这个电路刚好能工作。如果用两节18650串联成7.4V,就必须上降压恒流方案,比如PT4115,否则线性驱动会烧掉大量功率。
这里有个很多人忽略的点:PWM调光频率的选择会影响射频。我一开始用500Hz PWM做亮度调节,实测发现ANT+的通信距离明显变短,尤其亮度在30%到70%之间时,射频丢包率大幅上升。后来把PWM频率提高到2kHz,并且在LED驱动线上加了RC低通滤波,问题基本消失。这是整机调试里最坑的一个问题,后面我会在第六章详细展开排查过程。
2.3 天线和布局:不要为了省空间牺牲天线净空区
nRF24LE1模块通常自带陶瓷天线或者PCB天线,天线区域要求周围不能有大面积铜箔,最好留出净空。第一版PCB我为了省尺寸,把电池线和LED驱动线直接从天线下方穿过,结果ANT+通信距离只有1米多,码表和车灯相距不到30厘米都有偶发丢包。后来把天线下方的走线全部清空,重新铺地,距离立刻恢复到十几米。
另外,车灯内部空间小,电池、驱动、射频模块挤在一起,金属外壳还会形成一个半封闭的谐振腔。我的做法是把射频模块放在外壳尾部,天线朝外侧,电池放在中间位置,LED在前端。这样天线尽量远离金属和电源走线,效果比理论计算预想的好很多。ANT+的广播是单向的,码表和车灯都在车上,实际距离一般不会超过两米,所以天线性能压力没有手机那么大,但也不能完全忽略。
3. ANT+协议层:配置通道、收发广播数据包与解析数据页
3.1 ANT+通信模型:不是“连接”而是“广播”
我第一次接触ANT+的时候,总是用BLE的思路去理解,怎么想怎么别扭。ANT+本质上是一种非常轻量的周期性广播协议:传感器按照固定的时间间隔,比如每0.5秒或每1秒,把一个数据包广播出去,接收方只是监听这些广播,不需要建立一对一的连接。
这个模型的好处是,一个传感器可以同时被无数个接收端听,而且新设备接入时无需配对。你戴上一条ANT+心率带,附近的码表、车灯、跑步机都能收到同一个心率数据,这是ANT+生态最核心的特性。对车灯来说,它就只需要做一个“监听者”,去听心率带、速度计发出来的广播,这比做“发射者”简单很多。
ANT+链路层的数据帧结构相对固定,包含同步字节、消息ID、长度、数据负载和校验字节。普通广播数据包的消息ID是0x4E,负载长度通常是8个字节。ANT+协议的关键在于这8个字节的“数据页”定义,不同设备类型通过不同的数据页格式来传递各自的数据。
3.2 通道初始化配置:几行代码里的门道
ANT+的接收通道有发送窗口和接收窗口,配置通道时最关键的是频率、周期和通道ID。ANT+默认射频频率是2462MHz,这个基本不用改。通道周期决定了你多久接收一个包,单位是32768分之一秒,也就是说1Hz的周期值是32768,0.5Hz的周期值是65536。车灯作为接收端,需要同时监听心率通道和速度通道,每个通道独立配置。
下面给出一段基于nRF24LE1的ANT协议栈初始化伪代码,展示配置一个监听通道的核心流程:
// 分配通道 ANT_AssignChannel(0x00, ANT_CHANNEL_TYPE_SLAVE_RX_ONLY, 0x00, 0x00); // 设置通道ID:设备编号、设备类型、传输类型 // 监听心率设备,设备类型为0x78(心率) ANT_SetChannelId(0x00, 0x0000, 0x78, 0x01); // 设置射频频率:2462MHz ANT_SetRfFrequency(0x00, 0x46); // 设置通道周期:1Hz ANT_SetChannelPeriod(0x00, 0x8000); // 设置搜索超时时间 ANT_SetSearchTimeout(0x00, 0x00); // 打开通道 ANT_OpenChannel(0x00);分号前看起来只是几个API调用,但每个参数都有讲究。ANT通道类型里,接收通道要配成从接收模式,发射通道才是主发射。这个从/主不是设备角色,而是数据流向。车灯只订阅数据,不主动发射,所以要配置为从接收。
通道ID里的设备编号如果是0x0000,表示接收这个类型下所有设备的数据。如果你想只看某一条心率带的数据,可以把设备编号填成那条心率带的编号,不过车灯这个场景通常不需要这么精确。设备类型字段决定了你能收到什么设备的数据,心率是0x78,速度/踏频计是0x7A,功率计是0x0B,我同时开了三条不同的监听通道,分别处理心率、速度和踏频数据。
3.3 数据页解析:心率、速度、踏频分别怎么读
拿到ANT+广播包后,真正的解析逻辑在数据页上。心率设备的广播页是0x80,载荷里包含了即时心率和心率带状态。速度和踏频设备的广播页是0x19、0x1A或0x1B,分别对应速度、踏频和速度踏频组合页,里面是累计转数和事件时间。
心率页的解析相对简单,一个典型的心率广播包负载前两个字节是页面编号和页面类型,最后一个字节是即时心率值:
void parse_hrm_page(uint8_t *payload) { if (payload[0] == 0x80) { // 心率页 uint8_t heart_rate = payload[7]; // 即时心率 // 更新系统状态 bikelight_set_heart_rate(heart_rate); } }速度页比心率页复杂一些,因为ANT+不会直接给你“当前速度”,它给你的是“累计轮转数”和“最后一次轮转事件时间”,需要你自己根据两个相邻事件的时间差算出瞬时速度。做这个计算的时候,你的固件里必须配置轮周长参数,公路车一般是2100mm左右,具体要看轮胎尺寸。速度页的解析代码不复杂,但要处理“事件时间回绕”的边界情况,不能直接拿当前时间减上次时间。
我在项目里没有直接在中断里做数据页解析,而是把收到的原始载荷先存到一个环形缓冲区,主循环里再统一处理。这样做的原因是ANT+回调函数里做复杂运算容易阻塞射频收发,而车灯本身还有PWM控制和状态机需要跑,把数据解析放到主循环里更安全也更容易调试。
4. 灯光策略状态机:心率、速度、刹车信号如何映射成PWM输出
4.1 状态机的设计思路:一件事一个状态
自动车灯最忌讳的是把所有逻辑写在一大段if else里。比如“心率大于150且速度大于30且持续3秒且不在闪烁模式”这种条件组合,写多了你自己都记不住什么情况会触发什么效果。我在一开始就设计了明确的状态机,每个状态代表一种照明模式,状态之间通过明确的迁移条件切换。
状态的划分参考了实际骑行场景,一共五个主状态:
- 停车节能状态:速度低于2km/h持续2分钟以上,灯光降到10%亮度,并切换为缓慢呼吸模式,方便在路边停车时省电,同时让周围人知道这辆车不是完全熄火的。
- 城市低速状态:速度在2-25km/h之间,亮度控制在40%,固定常亮,不闪烁,尽量减少对对向行人和车辆的干扰。
- 郊外巡航状态:速度在25-40km/h之间,亮度提到80%,同样常亮,保证视野。
- 高速疾驰状态:速度超过40km/h,亮度直接拉满到100%。这个场景通常在长下坡或者冲刺,是视野需求最迫切的时候。
- 高强度警示状态:心率持续超过150bpm且速度低于25km/h,说明正在爬坡或高强度踩踏,灯光切换到高亮慢闪模式,让前后车辆更容易注意到你。
每个状态的迁移条件都要设置防抖时间,避免因为单次数据抖动导致状态反复横跳。比如速度从25.1km/h掉到24.9km/h,不应该立刻触发一次状态切换。我在固件里为每个迁移条件加了一个20秒的持续判定窗口,只有当条件在窗口内稳定持续成立时,才真正执行状态切换。
4.2 基于事件的处理:刹车和紧急状态需要“即时响应”
上面说的状态机是基于周期性数据的连续判断,适合亮度的平滑变化。但有些场景需要瞬时响应,比如急刹车。普通ANT+速度计的数据在低速下更新频率不够,靠速度差值判断刹车不够灵敏,所以我在车上额外加了一颗加速度计(三轴,通过I2C读取),在固件里实时计算负向加速度。
当检测到减速度低于-1m/s²并且持续300毫秒时,车灯会立即进入紧急爆闪状态,持续2秒后恢复原状态。这个功能在城市跟车场景里非常实用,后车看到前方突然爆闪,条件反射就会警惕,比起刹车灯这种不常见的配置,车灯爆闪的警示效果更明显。
另一个事件是停车状态下的自动休眠。我的第一版固件只做了速度判断,结果发现夜骑休息时只要车被风吹得晃一下,速度计可能产生一个瞬时数据,导致灯又切回高亮模式。后来我加入了加速度计的静止判断逻辑,只有当速度为零且加速度波动小于阈值持续3分钟,才进入深度节能的呼吸模式,这个误判问题才算解决。
4.3 PWM亮度映射:不要把电压当作亮度
车灯的调光是通过PWM控制AMC7135实现的,频率最终锁定在2kHz,占空比从0到100%对应0到最大亮度。但这里有个一般人容易踩的坑:人眼对亮度的感知不是线性的,而是接近对数曲线。如果直接把占空比按线性映射,你会觉得30%到60%之间亮度变化轻微,而90%到100%之间又突然亮了一大截。
我参照骑行灯的常见调光曲线,给PWM加了一层Gamma校正。实际的做法是维护一张32项亮度的查找表,占空比数值从表里查,表是按2.2的Gamma曲线生成的。这样在低亮度区域,调整步进更密集,高亮度区域步进更稀疏,观感上顺滑得多。别看这只是车灯,夜晚骑行时亮度如果突然跳变,对骑行者视觉的冲击是很大的,光线平滑过渡本身就是一种安全设计。
5. 实测数据与三周夜骑验证:距离、续航和干扰
5.1 夜骑实测:把数据拉出来看效果
整个原型车灯装车后,我连续骑了三周,累计夜骑里程400多公里,跑过城市道路、绕城公路、郊区爬坡路和一段隧道。为了让判断尽量客观,我用码表记录了每次骑行的日志,同时让车灯固件内部把状态切换事件存到EEPROM里,每次骑完导出,和码表数据做对比。
最满意的表现是心率联动。有一次团练爬坡,从心率130bpm匀速提升到160bpm的过程中,车灯在心率达到150bpm的阈值后约3秒内切换到了高亮慢闪模式。整个过程我没有做任何手动操作,后方的车友回来还专门问了一句“你那个灯怎么自己会闪”,这说明状态切换的触发是清晰可感知的。城市低速和高速疾驰两个状态之间的切换也稳定,没有出现反复横跳的情况。
隧道场景是意外之喜。当时没有装光线传感器,但隧道里我自然减速到30km/h左右,车灯自动从城市模式的40%亮度升到了巡航模式的80%,进入隧道前刚好完成切换,视觉上没有出现突然变暗的现象。
5.2 续航测试:功率数据说话
续航方面,我用了三节18650循环测试。在城市混合路况下,平均亮度约50%,等效电流大概450mA,3400mAh的电芯实测续航约6小时出头。如果是全程郊外巡航模式,80%亮度下等效电流约700mA,续航约4小时。全亮100%模式下,电流接近1.05A,续航只有3小时左右。
ANT+接收模块的功耗在整个系统里几乎可以忽略,实测射频部分平均电流约4mA,这还是在监听三路通道数据的情况下。也就是说,多花在“智能”上面的电量占比不到1%,为这个智能化付出的续航代价几乎可以不计。
下面这张表记录了三种典型场景下的实测参数:
| 场景 | 平均速度 | 平均心率 | 平均亮度 | 等效电流 | 实测续航 |
|---|---|---|---|---|---|
| 城市通勤 | 24km/h | 135bpm | 40% | 360mA | 约7小时 |
| 郊外训练 | 33km/h | 150bpm | 80% | 700mA | 约4小时 |
| 山地爬坡 | 18km/h | 172bpm | 100%(慢闪) | 800mA | 约3.5小时 |
考虑到夜骑一般不会连续超过4小时,这个续航在实用上完全够用。停车呼吸模式下的功耗更低,实测等效电流约120mA,相当于把那1%的智能成本进一步摊薄了。
5.3 干扰排查记录:一次“距离骤减”的完整定位过程
最想分享的实测经验,是那次“ANT+距离突然从十米以上骤减到一米左右”的定位过程。
现象是在一次夜骑中段发生的:码表开始间歇性丢失心率读数,车灯状态机也开始乱跳,一会儿高亮一会儿低亮,完全没有规律。回到家我先怀疑是心率带电池不够,换了新电池,问题依旧。又怀疑是射频硬件故障,把车灯拆下来放到桌面上测试,距离又恢复正常,只要装回车灯壳子里就复现。
排查到这里,我判断问题出在“车灯工作状态”和“装车状态”的共同作用,可能和电磁干扰有关。我首先把LED驱动断开,用外置电源给灯珠供电,ANT+距离恢复正常——说明干扰源在灯珠或驱动电路本身。然后我调整了PWM频率,把500Hz改成2kHz,距离恢复到了原来的水平。
后面又做了一轮对比:同样2kHz PWM,驱动线上没加RC滤波时,距离从十米掉到六米;加了RC滤波后,距离恢复到了十三米以上。基本可以确认,LED驱动PWM边沿的开关噪声通过走线辐射出来,正好落在2.4GHz频段附近,影响了射频前端的信噪比。这个问题的完整链路是:PWM频率低→基波和谐波落在射频段→天线近场耦合→ANT+灵敏度下降。
这个问题如果只在实验室里测功能正常,不到真实骑行场景里通电跑一圈,几乎不可能暴露。车灯和无线射频放在同一个壳子里,本身就是一种电磁兼容设计挑战,不是简单地把模块焊上去就能完事儿的。
6. 三个隐蔽的坑:通道冲突、LED驱动电磁干扰与天线净空
6.1 坑一:ANT+通道ID冲突导致码表不显示灯设备
第一次给车灯配上ANT+发送模块时,心率带、速度计、码表都能正常工作,但车灯的数据在码表上一直找不到。翻了很多文档后发现是通道ID的设备类型和传输类型组合和某个已有设备重了。
ANT+生态里,设备类型字段区分设备类别,但同一个设备类型下还有更细的通道ID组合规则。车灯当时用的是普通自定义类型,和码表内置配置表里某个未知设备撞了。解决方法是把设备类型改成ANT+联盟预留的照明类型,并且把设备编号设成一个不常见的自定义值。改完后,码表在下一次扫描中很快就发现了车灯设备。
这个坑给的经验是:在ANT+生态里加一个新设备,不能随便填设备类型,需要去查ANT+的设备profile定义,合规才能获得最好的兼容性。
6.2 坑二:LED驱动噪声污染射频(前面排查案例的复盘)
这个坑是整套系统里最隐蔽的一个。表面看是ANT+通信距离缩水,实际根因在驱动电路。PWM调光频率从500Hz提高到2kHz后,我原本以为问题已经解决了,但仔细测试仍然发现,距离从原来的十四米掉到十米左右,虽然没有影响实际使用,但对于严谨的项目来说还是不够好。
之后在驱动线上串联了一个100Ω电阻,并在灯珠两端并联了一颗47nF的MLCC电容,把PWM边沿的高频成分进一步抑制,距离才恢复到十三至十四米。这个经验说明,PWM调光本身不是问题,PWM边沿的过冲和振铃才是辐射源。开关速度越慢,辐射越小,但也不能慢到影响调光的线性度,这个平衡要靠实测来调整。
6.3 坑三:天线净空区被金属外壳和走线“吃掉”
这个坑在2.3节提到过,但值得单独说透。nRF24LE1模块的陶瓷天线对周围环境非常敏感,天线下方的铺铜、外壳的金属、电池的钢壳,都会改变天线阻抗和辐射方向。
我在第一版外壳设计里,把电池放在了模块正上方,中间只隔了一层塑料,结果ANT+距离只有1.2米。后来做了三版调整:把模块移到尾部,天线斜向上方;天线正下方禁布铜箔和走线;电池挪到LED和模块之间作为隔离块。三管齐下后,距离稳定在十三米以上。
对车灯来说,实际行驶时码表和车灯距离大约0.5到1.5米,这些距离余量完全够用。但如果你做的是尾灯,接收距离就变成了前车把到后坐管的距离,天线净空的要求会更高。给后来者一个直接建议:即使你觉得距离够用,也一定不要在天线下方走大电流线,否则某天电池电压波动或者环境湿度变化,距离会突然掉到一个你意想不到的底限。
6.4 测试方法:怎么判断是“真稳定”还是“运气好”
最后分享一个验证方法:不要只测一次距离就宣布稳定。我会在固定位置放一台接收器,让车灯分别以10%、30%、50%、70%、100%的亮度连续运行,每个亮度档位测试5分钟,统计全程丢包率。只有所有档位的丢包率都低于0.5%,我才会认为当前的电磁设计方案是可用的。
这个测试方法帮我发现了不少“间歇性”问题——有些亮度档位下距离正常,有些档位下偶尔丢包。这类问题在正常骑行中不容易发现,但一旦碰到信号受干扰的环境就会放大。对于车灯这种安全相关的设备,稳定的通信链路比峰值距离更重要。
整个项目做下来,我最大的体会是:车灯接入ANT+协议并不是为了炫技,而是让照明从“手动控制”进化到“骑行状态自适应”。三周夜骑测试之后,我越来越习惯不看码表也能从灯光的明暗变化里判断自己当前的状态——心率拉起来的时候灯会亮起警示,速度放慢的时候灯会柔和下去。这种“被设备理解”的感觉,比任何参数指标都更真实地说明问题了。