基于ATtiny167的电蚊拍智能化改造:电流采样与信号处理实战
1. 项目概述与核心思路
手头这个电蚊拍,用了几年,除了偶尔“啪”的一声和一股焦糊味,似乎也没什么存在感。直到某个夏夜,看着它孤零零地躺在墙角,我突发奇想:如果能像打游戏一样,每次击杀蚊子都有音效和分数,这场“人蚊大战”会不会更有趣?这个念头,催生了这次改造。
本质上,这是一个典型的“传统设备智能化”项目。核心目标很明确:让一个纯机械/高压的电蚊拍,具备感知、计算、反馈和交互的能力。听起来复杂,但拆解开来,无非是几个成熟模块的组合拳:一个负责“大脑”运算的微控制器(我选择了ATtiny核心的Digispark Pro),一个负责“眼睛”观察的电流采样电路,一个负责“嘴巴”发声的DFPlayer MP3模块,一个负责“记分牌”显示的TM1637四位数码管,以及一套负责“后勤”的USB充电锂电池系统。整个项目的技术价值,不在于发明了多高深的电路,而在于如何巧妙地利用现有模块,低成本、高可靠地解决一个具体问题,并在这个过程中,将嵌入式开发中常见的信号处理、电源管理、模块集成等知识点串联起来,形成一个完整的、可复现的实践案例。
2. 核心硬件选型与电路设计解析
2.1 微控制器:为什么是Digispark Pro (ATtiny167)?
市面上Arduino板子很多,UNO、Nano、Pro Mini都很常见。选择Digispark Pro,主要基于几个现实考量。第一是尺寸,它的板载面积非常小,对于需要塞进电蚊拍狭小手柄的内部空间来说,这是决定性优势。第二是引脚数量,ATtiny167提供了足够多的I/O口,同时驱动TM1637(2个引脚)、DFPlayer(2个引脚)、读取模拟信号(1个引脚)、读取按钮(1个引脚)以及可能的其他功能(如控制充电芯片的使能引脚)绰绰有余。第三是成本与易用性,它价格低廉,且可以通过Arduino IDE进行编程,生态友好。当然,它的性能(16MHz主频,16KB Flash)对于处理我们这种级别的信号分析和控制任务,完全足够。
注意:Digispark Pro有两个版本,核心分别是ATtiny167和ATtiny85。务必选择ATtiny167版本,因为85的引脚和资源(特别是模拟输入引脚和内存)对于本项目来说会非常紧张,甚至可能无法实现所有功能。
2.2 击杀检测原理:从“猜”到“算”
这是整个项目的技术核心,也是最有意思的部分。最初的想法很直接:高压电弧产生时,电路里一定有剧烈的电压或电流变化,直接测量高压侧?不行,动辄几百上千伏,远超单片机承受范围。那就换个思路,从源头——电池供电回路入手。
当电蚊拍空载(未按下按钮或按下但未击中蚊子)时,整个升压电路工作在一个相对稳定的状态,消耗的电流很小。一旦高压电网间产生电弧(无论是击中蚊子还是其他导体),相当于负载突然加重,会从电池抽取一个瞬间的脉冲电流。这个电流变化,虽然幅度可能不大,但足以被捕捉。
如何捕捉?欧姆定律。我们在电池的正极和电蚊拍原电路的正极之间,串联一个非常小的采样电阻(我用了1Ω)。这样,流经电蚊拍的总电流I会在这个电阻R上产生一个微小的压降V = I * R。通过单片机的模拟输入引脚(我用了A12)测量这个电压V,就能反推出电流I的变化。
但问题来了,这个电压信号极其微弱且充满噪声。直接读取analogRead的值,波形就像一团乱麻,根本无法分辨哪里是“蚊子来了”。这时就需要信号处理。我的策略是“滑动窗口计算标准偏差”。简单来说,我不再关心电压的绝对高低,而是关心一小段时间内电压值的“波动剧烈程度”。在稳定状态下,电压值围绕一个均值小幅波动,标准偏差很小。当有电弧脉冲时,电压值剧烈跳变,这一小段时间内的标准偏差值会急剧升高。通过设定一个合适的阈值,当计算出的标准偏差平方值超过这个阈值时,我就认为一次有效的“击杀事件”发生了。
2.3 功能模块集成方案
1. 显示模块 (TM1637 四位数码管)选择它纯粹是因为省事。TM1637是一个集成了驱动和扫描的芯片,只需要两根线(CLK, DIO)就能控制一个4位数码管,极大节省了单片机的I/O资源和编程复杂度。直接使用现成的库(如TM1637Display),几行代码就能实现数字的显示和亮度控制。
2. 音频模块 (DFPlayer Mini)同样是一个“傻瓜式”模块。它支持直接读取SD卡或TF卡里的MP3文件,通过简单的串口指令(发送特定格式的字节)就能控制播放、暂停、选曲、音量等。我们将准备好的击杀音效、升级音效等存入SD卡,单片机在检测到击杀事件后,发送对应的曲目编号指令即可。它自带功放,可以直接驱动一个小型喇叭(8Ω 0.5W-1W足够)。
3. 电源系统改造原装电蚊拍使用两节AAA电池(3V),而我们的附加模块(单片机、显示、音频)都需要5V供电。因此,必须进行电源升级。方案是:使用一块3.7V的锂聚合物电池(常见手机充电宝拆机电池即可),搭配一个支持锂电池充电与5V升压输出的集成模块(如IP5306、TP4056+升压芯片组合模块)。这样,通过一个Micro USB口就能给电池充电,并且模块能持续输出稳定的5V电压给整个系统。这里的关键是,需要将原电蚊拍电路的供电,也从这两节AAA电池改为从这个5V系统取电(经过一个开关),实现统一供电。
4. 外围电路
- 音量调节:一个电位器连接至DFPlayer的VOL引脚,实现硬件音量调节。
- 重置按钮:用于将击杀计数器清零。接在单片机的一个数字引脚上,通过程序检测其状态。
- 总开关:控制整个系统(包括原电蚊拍高压电路和新增的5V电路)的电源通断。
2.4 整体电路连接图与要点
由于无法绘制图表,我用文字描述核心连接逻辑:
电源主干:
- 锂电池正负极接入充电/升压一体化模块的BAT+和BAT-。
- 模块的5V输出端(通常标5V或OUT+)连接到Digispark Pro的
5V引脚(注意不是VIN)。 - 从Digispark Pro的
5V引脚引出线,为DFPlayer模块的VCC、TM1637显示屏的VCC、电位器的一端供电。 - 所有模块的
GND(地线)最终都需要连接到充电模块的GND(OUT-)。
信号与数据线:
- 电流采样:在充电模块
5V输出端与原电蚊拍电路正极之间,串联1Ω采样电阻。用一根线连接该电阻与原电路正极的节点,到Digispark Pro的模拟引脚A12。 - 显示模块:TM1637的
CLK接Digispark Pro的P2,DIO接P1。 - 音频模块:DFPlayer的
RX接Digispark Pro的P0(用于发送串口指令),SPK1和SPK2接喇叭两极。 - 音量控制:电位器中间引脚接DFPlayer的
VOL,另外两脚分别接5V和GND。 - 重置按钮:按钮一端接
5V,另一端接Digispark Pro的P3引脚,同时在P3引脚与GND之间连接一个10kΩ的下拉电阻,确保空闲时为低电平。
- 电流采样:在充电模块
原电蚊拍电路改造:
- 移除原有的AAA电池。
- 将原电池仓的正负极触点,分别连接到充电模块的
5V输出和GND。务必确认原电蚊拍的开关是控制正极(火线)。如果是控制负极,则需要调整采样电阻的位置和代码逻辑。
实操心得:焊接前,最好用万用表蜂鸣档把所有计划连接的
GND点都测试一遍,确保它们最终是连通的。电源和地线的连接错误是导致模块不工作甚至烧毁的最常见原因。对于信号线,可以先不焊接,用杜邦线连接进行功能测试,确认无误后再固定。
3. 软件实现与信号处理算法深度剖析
3.1 开发环境搭建与核心库
编程在Arduino IDE中进行。首先需要安装Digispark Pro的板卡支持。在“文件”->“首选项”的“附加开发板管理器网址”中添加http://digistump.com/package_digistump_index.json,然后在“工具”->“开发板”->“开发板管理器”中搜索“Digistump”并安装。选择板卡为“Digispark (Default - 16.5mhz)”。
需要安装的库主要有:
TM1637Displayby Avishay Orpaz:用于驱动数码管。SoftwareSerial:Arduino自带,用于模拟一个串口与DFPlayer通信(因为Digispark Pro的硬件串口引脚可能被占用或不够用)。EEPROM:Arduino自带,用于将击杀计数存入非易失存储器,防止断电丢失。
3.2 击杀检测算法的代码实现
算法的核心在于process_average()和process_std_dev()这两个函数。下面我拆解关键部分:
// 定义与初始化 const int sensorPin = A12; // 电流采样引脚 const int numReadings = 10; // 单次读取时,模拟采样的次数,用于初步平滑 int readings[numReadings]; // 存储单次采样的数组 int readIndex = 0; long total = 0; int average = 0; // 这是经过numReadings次采样平均后的“瞬时”值 const int windowSize = 15; // 滑动窗口大小,用于计算标准偏差 int dataWindow[windowSize]; // 存储最近windowSize个“瞬时平均值” int windowIndex = 0; long sum = 0; long sumOfSquares = 0; int stdDevSquared = 0; // 标准偏差的平方,避免开方运算 // 阈值 const int killThreshold = 110; // 判定为击杀的阈值 const int buttonThreshold = 250; // 判定为按下/松开电蚊拍按钮的阈值(更高) void process_average() { // 1. 移除旧的读数 total = total - readings[readIndex]; // 2. 读取新的模拟值 readings[readIndex] = analogRead(sensorPin); // 3. 加入新的读数到总和 total = total + readings[readIndex]; // 4. 移动索引 readIndex = (readIndex + 1) % numReadings; // 5. 计算当前的平均值 average = total / numReadings; // 6. 将这个平均值放入滑动窗口,用于后续标准偏差计算 update_std_dev_window(average); } void update_std_dev_window(int newValue) { // 取出即将被覆盖的旧值 int oldValue = dataWindow[windowIndex]; // 用新值覆盖旧值 dataWindow[windowIndex] = newValue; // 更新窗口索引 windowIndex = (windowIndex + 1) % windowSize; // 更新总和与平方和(高效计算,无需每次遍历整个数组) sum = sum - oldValue + newValue; sumOfSquares = sumOfSquares - (oldValue * oldValue) + (newValue * newValue); // 计算方差(标准偏差的平方) long mean = sum / windowSize; // 方差 = (平方和 / N) - (均值的平方) stdDevSquared = (sumOfSquares / windowSize) - (mean * mean); } void process_std_dev() { int currentStdDevSq = stdDevSquared; // 获取当前计算出的方差值 // 状态机:用于区分是按钮操作还是击杀 static int state = 0; // 0:空闲,1:检测到高波动(可能是按钮按下),2:确认按钮按下,3:按钮释放检测... // 此处省略状态机具体代码,其逻辑是: // 如果波动值超过buttonThreshold,则认为是人为按下了电蚊拍开关,忽略此次事件。 // 如果波动值在killThreshold和buttonThreshold之间,且不是紧接在按钮事件之后,则判定为一次击杀。 if (currentStdDevSq > killThreshold && currentStdDevSq < buttonThreshold) { // 符合击杀条件! if (/* 状态机判断不是按钮事件的余波 */) { killConfirmed(); } } } void killConfirmed() { score++; // 分数加1 EEPROM.put(0, score); // 存入EEPROM displayScore(score); // 更新显示 playSound(1); // 播放击杀音效,例如曲目1 // 每10次击杀播放一次“升级”音效 if (score % 10 == 0) { playSound(2); // 播放升级音效,例如曲目2 } }算法要点解析:
- 两级滤波:先通过
numReadings次采样取平均,消除一部分高频噪声;再通过windowSize大小的滑动窗口计算波动,关注中长期变化。 - 方差代替标准差:计算
stdDevSquared(方差)避免了耗时的开方运算,因为我们需要比较的是相对大小,平方后的值同样具有指示性。 - 状态机防误触:电蚊拍开关的按下和松开,也会引起巨大的电流变化,其波动幅度远大于蚊子击杀。通过一个简单的状态机逻辑和更高的
buttonThreshold,可以过滤掉这些人为操作,确保只对“小脉冲”(击杀)进行计数。 - 阈值调参:
killThreshold和buttonThreshold是经验值,需要根据你的具体硬件(采样电阻精度、电池电压、电蚊拍型号)在实验中调整。可以通过在代码中临时将stdDevSquared值显示在数码管上,观察正常波动、击杀波动和按钮波动的数值范围来确定。
3.3 功能模块的驱动与协同
数码管显示:初始化后,主要使用setSegments()函数显示数字,或showNumberDec()函数直接显示十进制数。注意TM1637库可能需要微调延时,如果显示乱码,尝试调整#define TM1637_DELAY的数值。
DFPlayer控制:通过SoftwareSerial向模块发送固定的指令帧。例如,发送{0x7E, 0xFF, 0x06, 0x03, 0x00, 0x00, 0x01, 0xEF}可以播放SD卡中序号为1的曲目。关键点:DFPlayer上电需要一定时间初始化(约200-500ms),在setup()函数中发送指令前务必先delay(500),否则指令可能无法被识别。
EEPROM存储:使用EEPROM.put(addr, score)存储分数,EEPROM.get(addr, score)读取。地址addr可以从0开始。注意ATtiny167的EEPROM有512字节,足够用。频繁写入会损耗EEPROM寿命,但击杀计数频率很低,无需担心。
4. 组装、调试与问题排查实录
4.1 机械结构改造与组装
- 拆解与评估:小心拧下电蚊拍手柄上的螺丝,打开外壳。仔细观察内部空间,特别是电池仓和高压电路板附近的区域,规划各个模块(锂电池、充电板、Digispark、DFPlayer、喇叭)的摆放位置。用游标卡尺测量关键尺寸。
- 电池仓改造:原AAA电池仓通常空间浪费严重。可以小心地用钳子和烙铁拆除内部的塑料隔断,为18650或软包锂电池腾出空间。保留原有的电池弹片,将其焊接引线出来连接到我们的5V电源系统。
- 开孔与固定:
- USB充电口:在手柄末端或侧面,用烙铁或小电钻开一个Micro USB口大小的方孔,让充电模块的接口露出来。
- 喇叭出声孔:在手柄侧面或正面,钻一系列小孔作为喇叭的出声孔。可以用热熔胶将喇叭固定在内部对应位置。
- 数码管窗口:在手柄正面开一个矩形窗口,用于嵌入数码管。可以用砂纸慢慢打磨,或者用小型雕刻机。确保窗口大小与数码管严丝合缝,后期可以用热熔胶从内部固定。
- 模块固定:所有电路板尽量不要悬空。使用尼龙柱、螺丝或者大量的热熔胶进行固定,防止在挥舞拍子时内部元件晃动、脱落导致短路。
- 电磁屏蔽:这是一个极易忽略但至关重要的步骤。高压电弧会产生强烈的电磁干扰,可能让数码管显示乱码甚至单片机死机。我的解决方案是:用铝箔胶带将TM1637显示模块的背面和侧面包裹起来(注意不要碰到任何焊点和引脚),然后将铝箔接地(连接到系统的GND)。这相当于给显示模块加了一个法拉第笼。效果立竿见影。
4.2 系统上电调试流程
- 分模块测试:不要一次性焊接所有线路。建议顺序如下:
- 先只连接充电模块和锂电池,测试USB充电是否正常,5V输出是否稳定。
- 然后连接Digispark和5V电源,上传一个最简单的Blink程序,测试单片机核心是否工作。
- 接着单独连接TM1637显示屏,上传一个显示固定数字的程序,测试显示和通信是否正常。
- 再单独连接DFPlayer和喇叭,上传一个播放指定曲目的程序,测试音频系统。
- 最后连接采样电阻和原电蚊拍电路。
- 电流采样调试:这是最需要耐心的一步。上传一个只读取A12引脚并打印平均值
average和方差stdDevSquared到数码管(或通过USB-CDC虚拟串口,如果开启了的话)的程序。然后:- 不按按钮,观察稳定状态下的数值。
- 按下电蚊拍按钮(不要电到任何东西),观察数值的剧烈跳变,记录峰值。这就是
buttonThreshold的参考。 - 用镊子或螺丝刀瞬间短路一下电蚊拍电网(模拟电弧),观察产生的脉冲数值。这就是
killThreshold的参考。 - 根据记录的数据,在代码中调整两个阈值。目标是:按钮操作产生的数值远大于
killThreshold,而模拟击杀的数值稳定地落在killThreshold之上、buttonThreshold之下。
4.3 常见问题与解决方案速查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 上电无任何反应 | 1. 电源未接通 2. 电池没电 3. 短路保护 | 1. 检查总开关、所有焊接点,用万用表测量5V和GND之间电压。 2. 连接USB充电器,看充电指示灯是否亮起。 3. 断开所有负载,单独测试充电模块5V输出。检查是否有元件发烫。 |
| 数码管不亮或乱码 | 1. 供电不足 2. 通信引脚接错 3. 电磁干扰 | 1. 测量数码管VCC电压是否在4.5-5.5V之间。 2. 确认CLK和DIO引脚连接正确,代码中引脚定义一致。 3.进行电磁屏蔽(铝箔包裹并接地)。 4. 在TM1637的VCC和GND之间并联一个10uF-100uF的电解电容。 |
| 没有声音 | 1. DFPlayer未初始化 2. 串口通信问题 3. SD卡或文件问题 | 1. 确保setup()中给DFPlayer足够的初始化延时(delay(1000))。2. 确认TX/RX接线正确(Digispark的TX接DFPlayer的RX)。 3. 检查SD卡格式(FAT32),音频文件是否为MP3格式,文件名是否简单(如 0001.mp3),并放置在根目录。尝试用playTrack(1)播放第一首。 |
| 击杀计数不准(漏计或多计) | 1. 阈值设置不当 2. 采样电阻值不合适 3. 电源噪声大 | 1. 重新进行“电流采样调试”,精细调整killThreshold和buttonThreshold。2. 尝试更换采样电阻(如0.5Ω或2Ω),观察信号幅度变化。 3. 在电蚊拍原电路的电源输入端(即采样电阻之后)并联一个470μF的电解电容,可以稳定电压,吸收脉冲,极大改善信号质量。 |
| 设备运行一段时间后自动关机 | 充电模块自动进入待机模式 | 许多集成充电升压模块(如IP5306)有轻载关机功能。解决方案: 1. 在代码中定期(如每15秒)模拟一个微小负载(如让一个IO口短暂输出高电平)。 2. 如果模块有“KEY”引脚,将其连接到单片机的一个IO口,并通过程序定期给一个脉冲信号“唤醒”模块。 |
| 挥舞拍子时显示闪烁或复位 | 1. 电池接触不良 2. 电源线虚焊 3. 元件松动 | 1. 加固电池的连接,如果是18650电池仓,确保弹簧片压力足够。 2. 重新焊接所有电源线(5V和GND),尤其是电流较大的路径。 3. 用热熔胶或硅胶固定所有模块和重要焊点。 |
实操心得:调试信号处理部分时,最忌讳“我觉得”。一定要让数据说话。把关键变量(如
average,stdDevSquared)实时显示出来,你才能准确知道系统“眼”中的世界是什么样的。另外,那个470μF的电容几乎是本项目的“神器”,它并接在电蚊拍电路两端,不仅能滤除高频噪声,还能在电弧发生时提供瞬间大电流,稳定了系统电压,让采样信号干净了很多,强烈建议加上。
5. 功能扩展与优化思路
完成基础功能后,这个平台还有很大的可玩性:
- 多音效与随机播放:在SD卡里多存几个击杀音效,代码中检测到击杀时,从列表中随机选择一个播放,增加趣味性。DFPlayer支持直接播放指定文件夹下的随机文件。
- 连击与分数加成:在代码中增加计时器,如果在一定时间间隔内(比如2秒)连续检测到多次击杀,可以判定为“连击”,额外加分并播放特殊音效。
- 低电量指示:锂电池电压会随着放电下降。虽然升压模块能在电池电压较低时仍输出5V,但我们可以通过单片机的模拟引脚(需分压电路)监测电池电压,当电压低于3.5V左右时,让数码管闪烁显示或显示“Lo”提示充电。
- 无线数据传输:增加一个超低功耗的蓝牙模块(如HC-08)或Wi-Fi模块(如ESP-01S),每次击杀后将数据发送到手机App或云端,实现击杀数据的统计、排行榜甚至地理标记。
- 改进检测算法:当前的方差检测法虽然有效,但仍有优化空间。可以尝试更先进的算法,如基于短时能量和过零率的音频检测思路(将电流信号视为一种特殊“音频”),或者简单的机器学习分类(需更强大的MCU),来进一步区分蚊子击杀与其他干扰(如轻微触碰电网)。
这个项目从想法到实现,最大的收获不是做出了一个会计分的电蚊拍,而是完整地走通了一个嵌入式产品从需求分析、方案设计、硬件选型、电路搭建、信号处理算法实现、调试排错到最终封装的全过程。每一个环节踩的坑,都是宝贵的经验。当你终于看到数码管上的数字随着那一声清脆的“啪”而跳动,并伴随着熟悉的游戏音效时,那种成就感,远比单纯买一个成品玩具要强烈得多。硬件改造的魅力,就在于此。
