自制踏频传感器
我从朋友那里收了一辆自行车(巨人逃跑叁辛丑年版)。那辆车可以变速,但我不会换挡。上网查了下,说合适的挡位是让踏频保持90rpm的挡位。我尝试过边数数边骑车,可很难持续数一分钟。正好最近发现小巧可爱的Raspberry Pi Pico W可以连接各种传感器,能用MicroPython编程,还具备蓝牙通讯能力,就萌生了用它搭配磁强计做踏频器的想法。
目录
大致思路
貌似市面上卖的踏频器都是绑在曲柄上,通过陀螺仪检测旋转。老式的踏频器是通过磁铁和霍尔元件来检测。我觉得磁力方案要比陀螺仪方案简单,所以选择在牙盘内侧吸上磁铁(下图黄色部分);在立管最靠近牙盘的部分放上ICM-20948传感器(下图蓝色部分);最后把Pico W放在比较平坦的下管(下图红色部分)。
之所以把磁铁吸在牙盘上,因为全车只有这里是钢制的。如果可以的话,我想把它们放在没有机械结构的左边。
购物清单
- 带Qwiic口、JST-PH口的Pico W(图中紫色的);
- 带Qwiic口的磁强计(图中树莓上面的);
- Qwiic连接线(图中左侧);
- JST-PH口的电池盒(图中右侧);
- 磁铁(太常见了,就没有照)。
我害怕触发烟雾警报器,不敢用电烙铁,所以我想要留好接口的Pico W。手头上虽然有符合要求的Badger 2040 W,但我打算拿它干别的。最后我相中了引出三个Qwiic接口、附带多个LED和蜂鸣器的Maker Pi Pico Mini(紫色板子后面是预先焊好的Pico W)——回过头来看,这个选择似乎是个败笔。
选择ICM-20948是因为GitHub上有使用Python读取数据的库。我只需要把其中的I2C库SMBus库替换成MicroPython的machine.I2C
就能用了。
电池方面,我不敢用裸露的锂电池,更倾向使用威力较小的普通电池。具体规格上我没多想,随便买了容纳两块七号电池的盒子。
如果我是学物理的,也许会通过ICM-20948的Datasheets、牙盘到立柱的距离和各种磁铁的磁力来计算出我需要多大、多强的磁铁。但我的物理已经快忘光了,所以凭着感觉买了一些直径6毫米,厚度3毫米,号称N45的铷磁铁。
收集数据
等硬件到货了,首先要验证能不能仅用它们检测踏频。而验证的第一步,当然是收集牙盘转动的数据。
把各个零件在预想的位置上固定好。然后用手倒转曲柄来假装骑车,记录磁强计X、Y和Z轴上的读数。
作图
一块磁铁
当然,我不能仅从数字看出什么规律。我得把它(Z轴读数和X、Y、Z轴合成的向量长度)画成图像才行。
这里我只截取了两个波峰,代表曲柄转了两圈。可以看出磁铁远离传感器时,大概有-50到50µT的读数;当磁铁离传感器最近时,磁感应强度能飙升到1700µT。不过大部分数据集中在0到250µT之内——这很好理解,因为磁铁大部分时间远离传感器。这似乎带来了新的问题——为了省电,势必要增大采样间隔,可间隔太大又有可能会漏过峰值。也许可以换更大更强的磁铁,或者用更多的磁铁来抵消掉增大间隔带来的问题;也许增大间隔带来的问题不会大到需要解决。
我不太清楚为什么Z轴读数在曲柄刚转走时会出现负值。
X、Y轴
我有点好奇把X、Y轴的读数加入考虑范围是什么样子的,所以用三个轴的分量合成了总量,但和Z轴趋势区别不大。那单独画X和Y的数据呢?
两个轴的数值接近水平镜像,这是因为我在固定传感器时恰好让X轴Y轴都45度斜着指向了天空;如果我让它俩都指向后轮的话,就会同正同负;要是让其中一个竖直指向天空,那估计都读不出什么变化。
这种偶然的安装方式启发我以X、Y轴数据颠倒正负号的时机来检测周期。不过我不希望在安装时要同时考虑三个轴的方向,所以还是打算以合成的总量为主。
更多磁铁
画完单个磁铁的图后,我觉得峰谷值有些小,所以又在已有磁铁上加了一块。由于两次实验的转速不同,所以请大家在对比时忽略横坐标。单就峰值来看,两块磁铁已经快到ICM-20948能检测的最大磁感应强度4912µT了。
目前我倾向于用两块磁铁,因为我一下买了40块,害怕用不完。而且我出去骑了一圈,快的时候有30km/h,两块磁铁仍然牢牢地固定在原地,没有被甩出去。所以两块磁铁即使不是浑然一体,其连接的牢固程度不用担心。
峰值检测
Eli Billaue的peakdet
已知曲柄转一圈,会产生一个峰值。所以检测转圈的问题可以转换成检测峰值的问题。峰值么,就是比左边、右边的点都高,所以可以用max(data[i-1], data[i+1]) < data[i]
来检测,结果如下图的橙叉:
可以看出有些微小的凸起也被当成峰值了。如果某点只有比后续点高1000µT(设为Δ)的才算峰值,这样又能标出上图的绿十字——比橙叉准多了。
什么?你说差的也不多?那我们可以拿波动更大的单个磁铁的数据作比较:
可以看出,加了阈值的检测方法确实比朴素的比大小更准确。
其实这种加阈值的峰值检测方法来源于Eli Billaue的博客。这个算法的大意就是遍历所有数据,记录遇到的最大值。假设新遇到的点小于最大值减阈值,就意味着先前遇到的最大值是峰值。这也是为什么上图中最后一个峰值没有被这种方法找到:因为它需要差距足够大的后续值(大过Δ),才能确定前面遇到的是峰值——这对我们来说是好事,想象你在等红灯时磁铁正好在ICM-20948旁边,造成持续偏高的读数。那时你一定不想让其中任意一个读数被当成转了一圈才应该出现的峰值。
这条StackOverflow也讨论了峰值检测的算法,我觉得相比Eli Billaue的方法有些复杂,所以没有采用。
检测上升边?
其实我最开始想到的检测曲柄转圈的方法不是检测峰值,而是检测上升边。大概方法是记住前面的最小值,如果此时碰到了比最小值大大约½个峰谷值的点,则记为一次上升。
从需要手动输入一个值的角度来看,这方法看起来和Eli Billaue的峰值检测算法有些相似。但输入的值恰好要½个峰谷值:大了有可能检测不到上升边;小了又可能把一条边当好几条边。Eli Billaue的方法就对要输入的值(Δ)没什么限制,因为他的方法在检测到峰值以后,会去寻找谷值,找到谷值才会再找峰值。而判定谷值这个动作是在遇到比最小值大Δ的后续点时才完成的,也就是说,在真正走过波谷之前不会再检测峰值。所以,Eli Billaue需要的Δ不用特意设成½个峰谷值,因为他的算法强调了两个波峰之间一定有个波谷(假设找完峰值不找谷值直接找下一个峰值,就需要设成½个峰谷值了)。
峰值转踏频
踏频的单位是圈每分钟(rpm),所以最正确的计算方式当然是维护一个过去一分钟峰值时间点的队列,队列长度就是踏频。不过这种方法不能很好地反映突变情况——想象维持59秒高踏频,在最后一秒突然慢了下来的情景。
那只记录前一秒的数据可以么?我们可以在每次达到峰值时给steps加一,然后设置一个每秒的时钟中断,在中断处理程序里根据steps计算踏频,再清零steps。其实也不行,因为我只在牙盘的一个点放有磁铁,这一秒能检测到的要么没转圈,要么转一圈(我估计我不会有120rpm的踏频)。所以计算出的踏频一定只有0或60rpm。
给牙盘上平均放置n块磁铁,可以在一秒内检测到最小1/n的转动。我个人觉得这样比较麻烦,所以还是只保留一块(叠加的)磁铁,用上一次和这次峰值之间相距多少微秒,来计算保持这个节奏一分钟的踏频。
具体地说,是在每次峰值时调用utime.ticks_ms
,再用utime.ticks_diff
得出时间差。不过这有个问题:utime.ticks_ms
只能返回0到TICKS_PERIOD区间的值,utime.ticks_diff
也只保证差距在TICKS_PERIOD/2之间才有效。那TICKS_PERIOD到底是多少?文档说:
The wrap-around value is not explicitly exposed.
我尝试从源码里推导出TICKS_PERIOD,只能确定它是MICROPY_PY_TIME_TICKS_PERIOD。但具体是多少,我没法拿肉眼看,还是等哪天需要自个编译MicroPython时加个printf来看吧。不过,经测试,一两秒的间隔还是容得下的。
蓝牙
用Pico W发蓝牙
Pico W的W,是Wireless的W。这代表它既支持Wi-Fi,又支持蓝牙。根据官方的教程,很快就可以用Pico W进行蓝牙advertise、notify。官方的示例程序基本可用,我们需要改的只有两项:
- 用于踏频传感器的service、characteristic编号;
- 确定notify要发送多长的字节。
前者需要上蓝牙的官方仓库里找。其中service id好找,搜cycling就能找到org.bluetooth.service.cycling_speed_and_cadence及其对应的0x1816;我是死活找不到——后来才发现他们在列举characteristic时把cycling_speed_and_cadence简写成CSC了,所以我要找的是对应0x2A5B的CSC Measurement。
编号有了,接下来确定要传输多少字节。由于人类踏频不会超过256rpm,所以长达一字节的无符号数即可容纳踏频信息。
用浏览器收蓝牙
发完了,谁来收呢?Chromium系的浏览器实现了实验性的Web Bluetooth API,允许你通过JavaScript来操纵蓝牙配对、消息传递。Chrome的开发者还分享了示例程序。我把那段程序改了改,让它在收到蓝牙通知时打印当前时间和转速。下面是演示视频:
这段不小心把我的传家宝拖鞋也录进去了。本来想重录一遍更漂亮的,电池却在关键时刻没电了——那是我最后的七号电池了!
我的代码是一刻不停地读取数据,也许可以适当降低频率来减少功耗,但目前阶段还是买新电池麻烦更少。
电池插曲
我查了下哪家充电电池又便宜又好,发现大家比较推荐宜家的LADDA(听起来很像俄罗斯破烂汽车)。正巧我想买张桌子,就找了个周末上宜家扛回来了一张桌板、四条桌腿和四节电池。
回到家,给Maker Pi Pico Mini安上新电池。通电就亮的小LED马上点亮——又马上熄灭。奇了怪了,同样的电池,抠下来给附带屏幕的Badger 2040 W(也是在背后焊了一个Pico W)换上就能照常使用;给Maker Pi Pico Mini接上连灯都亮不了。
从Badger 2040 W的教程上看到,两节充电电池的电压(各1.2V)对Pico W的无线功能来说不够。可是实际测试时发现,两节LADDA完全可以让Badger 2040 W连上Wi-Fi。我问了两个相关专业的朋友,西工大的告诉我可以把两个七号电池盒串联起来,用LDO、DCDC boost把电池盒的电压规定在某个区间;哈工大的则推荐我去买个小一点的移动电源,他说移动电源把哪些工作都做好了。
为什么2.4V的电压能带动有屏幕的Badger 2040 W,却点不亮没屏幕的Maker Pi Pico Mini?我怀疑是后者电路设计有问题。当然,这就不是我能理解的了。我对这个问题的解决方式是:买一个三块电池的电池盒,在到货之前给LADDA充满电再接到Maker Pi Pico Mini上。
实地测试
我的原计划是把Badger 2040 W装在车把上,实时显示我的踏频。不过安卓上的Chrome浏览器也支持Web Bluetooth API,所以只要把我的测试HTML传到VPS上,再用手机打开就可以显示、记录踏频了。实际测试时担心了一路,害怕自己没把各个部件固定牢:掉到地上还好,要是被卷进齿轮里可就不好了。好消息是:一路下来有惊无险,也得到了足够的数据——可以说,我的自制踏频器已经通过阶段性成功了。
剩下的工作
虽说目前已经可以使用,但还有许多改进的空间。其中最重要的就是外壳——有了外壳,我就不用担心下雨了,而且安装、拆卸时也要方便许多。目前我得知图书馆里有3D打印机,我也短暂地学习过《自动桌子 发明家
有了外壳,就可以随时使用了。等数据多了应该会微调读取磁强计数据的间隔、发送蓝牙消息的时机等等。也说不定会利用ICM-20948的陀螺仪和加速计去测量些别的什么东西——我可以用这两个来推算出当前速度么?如果能推出速度,想必连里程都可以估算出来。
另外,我还有一块BME688。之前尝试过用它的气压计估计当前海拔。也许会把它放到自行车上,看看我什么时候能累计爬完一个珠峰。
其实,能玩的很多,只是缺少连续的大块时间。