基于加速度计与移动平均滤波的自行车智能刹车灯设计与实现

发布时间:2026/5/15 21:30:23

基于加速度计与移动平均滤波的自行车智能刹车灯设计与实现 1. 项目概述一个能感知减速的智能刹车灯如果你和我一样喜欢在晚上或者光线不好的时候骑车那你肯定对骑行安全有切身体会。一个明亮的尾灯是必需品但传统的刹车灯需要手动触发或者依赖额外的压力/拉线传感器安装麻烦还容易出故障。几年前我开始琢磨能不能做一个更“聪明”的灯让它自己知道什么时候该亮起来这个想法最终落地成了一个基于Adafruit Circuit Playground Bluefruit开发板的自动刹车灯项目。它的核心逻辑很简单利用板载的加速度传感器实时感知自行车运动状态的变化。当你捏下刹车车速开始降低时车体会因为减速而产生一个向前的惯性力或者说传感器会感受到一个向后的加速度。我们的代码就是捕捉这个微小的信号变化然后自动点亮板子周围那一圈炫酷的NeoPixelsLED发出高亮红光警示后方的车辆和行人。整个项目融合了嵌入式编程、传感器数据处理和3D打印外壳制作是一个典型的“软硬结合”的创客项目。无论你是想为爱车增添一个实用的安全装备还是想学习如何用CircuitPython快速玩转传感器和灯光控制这个项目都能给你带来不少乐趣和成就感。接下来我会把从硬件选型、代码原理到打印组装的完整过程以及我踩过的坑和优化心得毫无保留地分享给你。2. 核心硬件选型与设计思路解析2.1 为什么是Circuit Playground Bluefruit市面上微控制器开发板那么多为什么这个项目选择了Adafruit的Circuit Playground Bluefruit后面简称CPB这绝不是随意之举而是基于几个关键特性的考量首先高度集成与易用性。CPB在一块小巧的圆板上集成了我们所需的所有核心部件一个三轴加速度传感器LIS3DH、10颗可独立寻址的RGB NeoPixel LED、一个锂电池充电管理芯片、一个JST PH2电池接口甚至还有蓝牙功能本项目虽未使用但为未来扩展留了可能。这意味着我们不需要焊接任何额外的传感器或LED也不需要复杂的分压电路真正做到了开箱即用极大降低了硬件门槛。其次CircuitPython生态。CPB原生支持CircuitPython这是一种基于Python 3的微控制器编程语言。对于从软件转向硬件的开发者或者初学者来说Python语法远比C/C友好。你可以像在电脑上写脚本一样通过简单的串口连接直接编辑板载存储上的code.py文件来修改程序实现“秒级”迭代开发调试效率非常高。最后供电与便携性。板载的锂电池管理电路支持通过USB或JST接口直接为500mAh左右的锂聚合物电池充电。这对于一个需要移动使用的自行车配件来说至关重要。我们只需要一块小电池和一根带开关的延长线就能组成一个独立的供电系统无需依赖车载电源。注意原教程也提到了可以使用老款的Circuit Playground Express。两者主要区别在于主控芯片性能CPB的nRF52840更快和无线功能。对于这个刹车灯项目Express完全能跑但CPB更快的处理速度能让传感器数据采样和移动平均计算更流畅理论上响应会更灵敏一些。如果你的手头只有Express放心用代码完全兼容。2.2 传感器方案的深度思考为什么用加速度计怎么用自行车刹车检测常见思路有检测刹车线位移、检测刹把角度或者检测轮速变化。但这些方法都需要额外的机械或磁性传感器安装复杂且车型适配性差。我们采用的加速度计方案则是一种非接触式、通用性极强的方案。其物理原理是牛顿第二定律。当自行车匀速运动时加速度计测得的综合加速度主要包含重力分量始终指向地心和微小的路面振动噪声。当你开始刹车时车体减速会产生一个与运动方向相反的减速度在物理学上等同于正向的加速度。这个额外的加速度矢量会叠加在原有的传感器读数上。在这个项目中我们特别关注Z轴垂直于开发板平面当板子水平安装在坐垫下时Z轴大致指向地面的加速度变化。这是因为重力基准稳定在平地骑行时重力在Z轴的分量相对稳定提供了一个可靠的参考基线。减速信号明显刹车时车体因惯性有“前倾”趋势但更主要的是整体减速会在Z轴取决于安装角度上产生一个可被分解和识别的变化。简化算法专注于单轴数据可以简化数据处理逻辑降低对处理器算力的要求更利于在单片机上实现实时判断。当然只读原始数据是远远不够的。加速度计数据噪声很大一次颠簸、一次抬手都可能造成数据尖峰。因此我们需要通过软件算法来从噪声中提取出真实的“刹车信号”。这就是本项目代码的核心双移动平均滤波算法。用一个短窗口10个采样点快速捕捉变化趋势再用一个长窗口50个采样点表征慢速变化的基线两者之差就能有效放大“减速”这个瞬态事件同时过滤掉持续振动和噪声。我后面会详细拆解这段代码的每一个细节。2.3 结构设计两种安装方式的权衡原教程提供了两种3D打印的安装支架设计这体现了对不同车型和用户习惯的考虑。设计一坐垫导轨安装优点安装位置高灯光更易被后方察觉重心相对稳定受车轮直接振动影响稍小。缺点对坐垫导轨的形状有要求。标准的圆形导轨兼容性好但许多现代坐垫尤其是碳纤维材质的会使用椭圆形或异形导轨可能导致夹具无法夹紧或损伤导轨。我的经验是如果夹不紧可以剪一小段废旧内胎橡胶垫在夹具和导轨之间既能防滑又能保护漆面。适配性提醒打印前最好用卡尺量一下自己坐垫导轨的直径。虽然设计有一定通用性但微调模型教程提供了TinkerCAD源文件链接可以确保完美贴合。设计二坐管安装优点安装更牢固通过M5螺丝锁紧几乎适用于所有圆形坐管结构集成了滑动开关控制电源更方便。缺点位置可能较低容易被泥水溅到灯光可能被部分车架或行李遮挡。我的选择我最初使用的是设计一但在一次长途骑行中夹具因为持续振动发生了轻微松动旋转。后来我改用了设计二虽然安装稍麻烦但再也没有出现过位移问题。如果你的骑行路况比较颠簸我强烈推荐设计二。3. 软件核心代码逐行解析与算法优化把硬件组装好只是完成了骨架让这个刹车灯真正拥有“智能”的是运行在CPB里的CircuitPython代码。我们来深入看看code.py里的每一部分都在做什么以及为什么要这么做。3.1 初始化与变量定义import time import math from adafruit_circuitplayground import cp brightness 0 # 用于10点移动平均的列表 last10 [0] * 10 # 用于50点移动平均的列表 last50 [0] * 50 consecutive_triggers 0 cp.pixels.fill((255, 0, 0)) # 初始化所有NeoPixel为红色 light_on Falseimport部分导入了时间、数学运算和CPB的核心控制库。cp对象是我们控制板上所有功能灯光、传感器、按钮等的入口。brightness这个变量用于控制“呼吸灯”效果的亮度变化周期后面会看到一个巧妙的正弦波应用。last10和last50这是整个算法的数据基石。它们被初始化为全零列表分别用于存储最近10次和50次的Z轴加速度采样值。使用列表来实现一个滑动窗口是嵌入式系统中处理数据流非常经典且高效的方法。consecutive_triggers连续触发计数器。这是防误触的关键。它记录“疑似刹车”信号连续出现的次数。只有当这个次数超过阈值代码中是3才会真正点亮刹车灯。这有效过滤了因单次颠簸造成的偶然数据波动。cp.pixels.fill((255, 0, 0))一开始就把所有LED设置为红色RGB值255,0,0。这样我们后续只需要控制亮度(brightness)就能实现红灯的亮、灭和呼吸效果代码更简洁。light_on一个状态标志位。当它为True时表示刹车灯正处于高亮状态。这个标志用于优化程序逻辑避免在灯亮时重复执行不必要的计算。3.2 主循环与数据采集while True: x, y, z cp.acceleration主循环是一个永恒的while True。每次循环第一件事就是读取加速度计的当前三轴数据并赋值给x, y, z变量。我们只关心z。cp.acceleration返回的单位是米每二次方秒 (m/s²)。在静止状态下Z轴读数约为9.8 m/s²重力加速度。3.3 移动平均滤波从噪声中提取信号这是算法的核心预处理步骤。# 10点移动平均平滑度一般但能显著降低噪声 last10.append(z) last10.pop(0) avg10 sum(last10) / 10 # 50点移动平均非常平滑 last50.append(z) last50.pop(0) avg50 sum(last50) / 50移动平均的原理假设我们每秒采样50次由后面的time.sleep(0.02)决定。last10列表始终保存着最近0.2秒10个点的数据last50则保存着最近1秒50个点的数据。每次新数据z到来就把它追加到列表末尾同时移除列表最开头那个最旧的数据。然后计算列表内所有数据的平均值。为什么这样能滤波随机噪声的特点是时正时负短期波动大。当我们对一段时间内的数据求平均时这些正负起伏的噪声会相互抵消一部分从而使得结果更接近真实的趋势信号。avg10反应更灵敏能快速跟上变化avg50则更平滑代表了更长期的、缓慢变化的背景值可以近似理解为当前的重力分量加匀速运动状态。3.4 刹车判断逻辑多条件确保精准这是整个项目的“大脑”逻辑链环环相扣# 如果近期平均与长期平均的差值大于1 m/s²进入判断 if avg10 - avg50 1: if consecutive_triggers 3: # 连续多次满足条件提高置信度 # 防抖动判断检查是否处于剧烈晃动中如过减速带 if not cp.shake(shake_threshold10): # 条件全部满足触发高亮刹车灯 cp.pixels.brightness 1 # 亮度设为最大1.0 start time.monotonic() # 记录触发时刻 light_on True # 更新状态标志 consecutive_triggers 1 # 无论是否亮灯满足差值条件就计数第一层判断趋势判断if avg10 - avg50 1:。这是刹车检测的核心条件。avg10 - avg50这个差值本质上度量了“近期加速度”相对于“长期平均加速度”的突变程度。当自行车开始减速时Z轴加速度会有一个向上的突变具体符号取决于安装方向代码通过1来捕捉正向突变导致avg10快速增大而avg50变化缓慢于是差值迅速超过阈值1 m/s²。这个阈值是我经过多次路试调整出来的在平路骑行中能较好地平衡灵敏度和抗干扰性。第二层判断持续判断if consecutive_triggers 3:。仅一次突变可能是压到石子。这里要求这个突变信号必须连续出现至少4次计数器从0开始3即第4次。假设循环间隔20ms这相当于刹车信号需要持续至少80ms以上。这能过滤掉绝大多数瞬间的冲击。第三层判断排除干扰if not cp.shake(shake_threshold10):。这是最后的“安全阀”。cp.shake()函数检测板子是否处于剧烈晃动状态比如正在经过一段极其颠簸的烂路或者被人用手剧烈摇晃。shake_threshold10是敏感度值越小越敏感。当处于剧烈晃动时即使前两个条件都满足我们也不点亮刹车灯因为那很可能是路况导致的而非主动刹车。这是一个非常重要的防误触设计。触发动作当且仅当以上三层判断全部通过我们才执行刹车动作将LED亮度设为最大值1记录当前时间戳到start变量并将light_on标志设为True。3.5 灯光效果与状态恢复刹车灯不能一亮就永远亮着我们需要一个优雅的熄灭过程并回到待机状态。# 如果灯没亮或者高亮时间已超过0.4秒 elif not light_on or time.monotonic() - start 0.4: # 呼吸灯效果利用正弦函数产生周期性亮度变化 cp.pixels.brightness abs(math.sin(brightness)) * 0.5 brightness 0.05 consecutive_triggers 0 light_on False条件elif not light_on or time.monotonic() - start 0.4:。这个条件在两种情况下为真一是刹车灯根本没亮light_on为False这对应着正常骑行状态二是刹车灯已经高亮且高亮时间超过了0.4秒。呼吸灯效果这是待机状态的灯光效果。math.sin(brightness)产生一个介于-1到1之间的周期波形。abs()函数将其折叠到0到1之间。乘以0.5是将最大亮度设置为50%0.5这样待机时灯光柔和不刺眼也更省电。brightness 0.05每次循环增加一点点驱动正弦波缓慢前进从而形成亮度平滑起伏的“呼吸”效果。你可以通过调整0.05这个增量来改变呼吸的快慢调整0.5这个系数来改变呼吸灯的最大亮度。状态重置一旦进入这个分支无论是待机还是结束刹车就将consecutive_triggers计数器归零为下一次刹车检测做准备并将light_on标志设回False。3.6 循环延时与稳定性time.sleep(0.02)最后的sleep(0.02)让每次循环间隔大约20毫秒即采样率约为50Hz。这个值很重要不能太快如果太快比如sleep(0.001)循环执行频率过高移动平均列表更新太快可能无法有效平滑数据且会增加功耗。不能太慢如果太慢比如sleep(0.1)采样率只有10Hz可能会错过短暂的刹车动作导致检测延迟变大。20ms是一个经验值在数据平滑度、响应速度和系统功耗之间取得了很好的平衡。原代码作者提到如果没有这个延时串口监视器会出现异常导致电脑卡顿这其实是因为主循环疯狂运行占用了大量CPU时间与串口通信资源加上CircuitPython的垃圾回收机制可能引发不稳定。所以这个sleep不仅是控制节奏更是系统稳定的保障。4. 制作全流程从3D打印到上路测试4.1 3D打印模型处理与技巧拿到STL文件后直接丢进切片软件可能不是最佳选择。根据我的打印经验有几个细节需要注意对于设计一坐垫夹具支撑设置底部零件Bottom.stl需要生成支撑。在CURA中建议将“支撑悬垂角度”设置为80度。这样做的目的是避免在固定Circuit Playground的螺丝孔内部生成支撑。内部的支撑非常难清理容易损坏模型。设置更大的角度切片软件会认为这个孔洞的顶部不需要支撑。打印方向提供的STL文件已经优化了打印方向。务必不要旋转模型。现有的方向确保了夹具的卡扣部分有最大的接触面积打印出来强度最好。材料与填充使用PLA材料即可平衡了强度、成本和耐候性。填充率用30%足够既能保证结构强度又不会浪费太多时间和材料。层高0.1mm能获得更光滑的表面但0.2mm打印更快强度也不错你可以自行权衡。对于设计二坐管夹支撑这个设计是不需要支撑的所有悬空部分的角度都在可打印范围内。顶层数这是关键务必在切片设置中将“顶层层数”设置为5层。因为这个设计有一个集成滑动开关的“屋顶”结构足够的顶层数可以确保这个“屋顶”密实、牢固不会在开关多次拨动后破裂或变形。装配公差3D打印存在收缩率。如果发现开关装入卡槽太紧可以用小锉刀或砂纸轻轻打磨卡槽内侧。如果太松开关容易脱落可以在开关侧面涂一点氰基丙烯酸酯胶水俗称快干胶再装入注意不要流到开关的触点上。4.2 硬件组装与走线要点组装过程像拼乐高但顺序和细节决定成败。先固定电池对于设计一先将500mAh锂电池放入底座背部的卡槽。如果电池稍厚或卡槽稍紧不要用力硬塞。可以用吹风机热风稍微加热卡槽的塑料部分注意不要对着电池吹使其轻微软化膨胀后再放入。对于设计二电池是滑入侧面的口袋相对简单。安装支架到自行车设计一使用1/4-20的螺栓和尼龙防松螺母推荐将夹具锁紧在坐垫导轨上。拧紧前确保夹具水平。如果夹不紧按之前说的垫一小条橡胶。设计二将夹具开口对准坐管从上往下卡入。可以上下滑动调整到最佳位置通常建议在坐管中部偏下但不要低于坐垫高度。然后插入M5螺丝在另一侧用六角螺母拧紧。拧紧时最好用两个扳手一个固定螺母一个拧螺丝避免整个夹具旋转。连接Circuit Playground使用那颗短的1/4-20 x 1/2螺栓将CPB固定到支架顶部的螺丝孔上。关键细节螺栓可能会太长。你需要在螺栓头和支架之间添加垫片作为间隔。我试过普通的平垫片也用过自行车刹车片上的凸面垫片都可以。目标是拧紧后CPB不会晃动但也不要过紧导致塑料支架开裂。方向确认确保CPB板上的JST电池接口朝上。这样连接电池线时线材可以自然弯曲避免反向弯折导致线材疲劳断裂。电源连接与开关将带开关的JST延长线一端接电池另一端接CPB。开关位置确保开关放在你骑行时方便操作的地方比如贴在坐垫侧面或坐管上。我用了一小段尼龙扎带和双面胶来固定开关线。如果没有带开关的延长线可以自己制作。剪断一根普通的JST延长线剥开线皮将两根线分别焊接到一个微型拨动开关的两个端子上。务必注意焊接前先给线头和开关焊点上锡。焊接完成后用热缩管或电工胶布严密包裹每个焊点防止短路。这是一个非常实用的技能在很多DIY项目里都能用到。4.3 软件烧录与库文件管理安装CircuitPython固件去CircuitPython官网找到Circuit Playground Bluefruit的页面下载最新的.uf2固件文件。用一条可靠的数据线连接CPB和电脑。很多手机充电线只能充电不能传数据务必确认。快速双击CPB板中央的复位按钮。板载的NeoPixel会先变红再变绿此时电脑会出现一个名为CPLAYBTBOOT的U盘。把下载好的.uf2文件拖进去。U盘会自动弹出稍等几秒会出现一个名为CIRCUITPY的新U盘。这就表示固件刷写成功了。部署项目代码与库本项目幸运地不需要额外的库文件CPB板子的基础固件已经包含了adafruit_circuitplayground这个核心库。这大大简化了部署。你只需要将前面详细解析的那个code.py文件直接复制到CIRCUITPY盘的根目录下。板子会自动运行这个文件。验证复制完成后板子可能会自动复位。你应该看到板子上的10颗LED发出柔和的红色呼吸光。拿起板子轻轻向前快速移动模拟刹车减速LED应该瞬间变为高亮红色。这就成功了5. 路测调优与常见问题排查代码跑起来只是第一步真正好用还需要上路测试和微调。我在不同路况下骑行了大概50公里总结出以下问题和优化方案。5.1 灵敏度调校让刹车灯更“懂”你原代码的阈值avg10 - avg50 1和consecutive_triggers 3是一个通用起点。你可以根据个人骑行习惯和路况进行微调感觉刹车灯亮得太晚可以尝试减小第一个阈值例如将1改为0.8或0.7。这样对更轻微的减速也会更敏感。也可以减少连续触发次数将3改为2。频繁误亮过个小坎就亮这是最常见的问题。首先增大第一个阈值比如从1调到1.2或1.5。如果还不行增大连续触发次数比如从3调到4或5。这要求更持久、更明确的减速信号才会触发。在颠簸路面完全失灵或乱亮主要靠cp.shake()函数来抑制。可以尝试降低shake_threshold的值比如从10调到15或20让板子对“晃动”的判断更敏感从而在颠簸时更不容易误触发刹车灯。但注意别调得太高否则正常刹车也可能被抑制。调参方法论一次只修改一个参数然后进行固定路段的测试比如从一条平路起点刹车到终点。用手机录下骑行过程和灯光反应回来对照视频分析这样才能科学地找到最适合你的参数组合。5.2 常见问题与解决方案速查表问题现象可能原因排查与解决步骤上电后LED完全不亮1. 电池没电或开关未开。2. 代码未正确上传。3. 硬件连接问题。1. 检查开关用USB线连接电脑看是否能识别CIRCUITPY盘。2. 确认code.py文件在CIRCUITPY根目录且名称无误。3. 检查JST接口是否插紧电池线是否完好。LED常亮红色无呼吸效果code.py未运行或运行出错。连接电脑用串口工具如Mu编辑器查看错误信息。常见错误是缩进不正确或语法错误。呼吸灯正常但刹车时不触发高亮1. 灵敏度阈值设置过高。2. 安装方向错误。3. 移动平均窗口不匹配。1. 调低avg10 - avg50的阈值。2. 确保板子水平安装Z轴垂直。可修改代码打印z值静止时应接近9.8。3. 检查last10和last50列表初始化是否正确。轻微颠簸就误触发高亮1. 灵敏度阈值设置过低。2. 防抖(shake)阈值过高。3. 机械固定不牢板子自身晃动。1. 调高avg10 - avg50的阈值或增加consecutive_triggers。2. 调低shake_threshold值如从10改为5。3. 紧固安装螺栓在支架与车架间加橡胶垫减震。刹车触发后高亮时间太短或太长time.monotonic() - start 0.4这个时间阈值不合适。修改0.4这个值。改小如0.3高亮时间变短改大如0.6或1.0高亮时间变长。电池续航极短1. 电池容量不足或老化。2. NeoPixel亮度设置过高。1. 使用容量更大的电池如1000mAh并用扎带固定于坐垫下方。2. 在呼吸灯效果中降低系数如将* 0.5改为* 0.2。高亮亮度1不建议调低以保证警示效果。5.3 续航与可靠性提升实战心得续航焦虑原教程提到的500mAh电池约1.5小时续航在实测中是基本准确的。如果你夜骑时间长有两个升级方案一是换用1000mAh或更大容量的软包锂电池用双面胶或魔术贴固定在坐垫下方二是修改代码优化功耗。CircuitPython的time.sleep()本身功耗很低主要耗电大户是那10颗NeoPixel。在呼吸灯模式下将最大亮度从50% (*0.5) 降到20% (*0.2)能显著延长续航。防水防尘CPB板子本身不防水。虽然坐垫下方位置相对较好但雨天骑行或经过泥泞路段仍有风险。可以购买尺寸合适的圆形防水盒或者简单点用透明的热缩管整体包裹板子和部分线缆用热风枪吹缩既能防水又不影响灯光。冬季性能锂电池在低温下容量会下降。如果常在寒冷环境下骑行会发现续航缩短。这是物理特性尽量让电池贴近身体如放在骑行服口袋保温骑行前再安装能有所改善。这个自动刹车灯项目从构思到调试完成给我的最大启发是一个实用的智能硬件产品往往是简单原理、巧妙算法和扎实工艺的结合。它不需要多么高深的黑科技而是需要对应用场景的深刻理解比如如何区分刹车和颠簸以及将想法可靠实现的过程比如3D打印的适配和代码参数的微调。现在我的车上一直装着这个小设备它就像是一个沉默的守护者在每一次减速时替我向后方的世界发出明确的信号。这种自己动手创造安全、增添乐趣的过程或许就是创客精神最吸引人的地方。希望你的制作和骑行之旅也一样顺利愉快。

相关新闻