Newhor

  • 2025-02-21
  • 回复了主题帖: DIY 耳机盒爆改随身音响(三)

    okhxyyo 发表于 2025-2-21 09:03 重心问题这个可以补救下。买个那种手机支架环,贴在背后,要放桌面上的时候就支起来下。 这个想法感觉可以

  • 2025-02-07
  • 发表了主题帖: DIY 耳机盒爆改随身音响(三)

    本帖最后由 Newhor 于 2025-2-21 08:44 编辑 DIY 耳机盒爆改随身音响(三)   上一篇文章介绍了硬件以及结构方面的设计,这次给分享软件功能、视频演示和成本统计几个方面 历史分享见以下链接 DIY 耳机盒爆改随身音响(一) DIY 耳机盒爆改随身音响(二)   一、软件设计 MP3模块资料可以参考以下链接:DFPlayer Mini Mp3 Player - DFRobot Wiki 串口控制指令   CMD命令(指令) 对应的功能 参数(16位) 0x01 下一曲   0x02 上一曲   0x03 指定曲目(NUM) 1-2999 0x04 音量+   0x05 音量-   0x06 指定音量 0-30 0x07 指定EQ 0/1/2/3/4/5 Normal/Pop/Rock/Jazz/Classic/Bass 0x08 单曲循环指定曲目播放 1-2999 0x09 指定播放设备 1/2/3/4/5 U盘/SD/AUX/SLEEP/FLASH 0x0A 进入休眠——低功耗   0x0B 保留   0x0C 模块复位   0x0D 播放   0x0E 暂停   0x0F 指定文件夹播放 1-10(需要自己设定) 0x10 扩音设置(无) [DH=1:开扩音][DL:设置增益0-31] 0x11 全部循环播放 [1:循环播放][0:停止循环播放] 0x12 指定MP3文件夹曲目 1-9999 0x13 插播广告 1-9999 0x14 支持15个文件夹 见下面的详细说明 0x15 停止播放,播放背景   0x16 停止播放     基于以上的协议,并借鉴了dcexpert大佬编写的库如下: from machine import Timer from binascii import hexlify class miniplayer(): def __init__(self, uart): self.u = uart # 串口对象 self.txbuf = bytearray(10) # 串口发送缓冲区 self.rxbuf = bytearray(10) # 串口接收缓冲区 self._dcnt = 0 self._dcmd = 0 self._dparam = 0 self.rxp = 0 # 串口缓冲区位置 self.rxflag = 0 # 串口33接收标志位 self.checksum = 0 # 校验值 self._repaet = True # 重复模式 self.card_in = False # TF 存在 self.usb_in = False # usb 存在 self.device = 0 # 当前设备 0: TF 1: usb self.tf_filecount = 0 # TF 卡文件数 self.usb_filecount = 0 # usb 文件数 self.tf_index = 0 # TF 当前曲目 self.usb_index = 0 # usb 当前曲目 self.message = ['','','','',''] # 设备消息列表 self.ack = 0 # 设备响应标志 self._volume = 10 # 音量 self._tm = Timer(-1) self._tm.init(mode=Timer.PERIODIC, period = 100, callback=self._tm_irq) def status(self): r = '' for i in range(len(self.message)): if self.message[i] != '': r = r + self.message[i] + '\n' self.message[i] = '' return r def cmd_delay(self, cnt, cmd, param=0): self._dcnt = cnt self._dcmd = cmd self._dparam = param def _tm_irq(self, t): if self._dcnt > 0: self._dcnt -= 1 if self._dcnt == 0: self.cmd(self._dcmd, self._dparam) while self.u.any() > 0: self.rxbuf[self.rxp] = self.u.read(1)[0] if self.rxflag == 1: # 已经收到帧头 self.rxp += 1 if self.rxp > 9: # 已经收到 10 个字节 self.rxflag = 2 # 数据接收完成 for i in range(len(self.message)-1): # 消息滚动 self.message[i] = self.message[i+1] try: #print(hexlify(self.rxbuf, ' ')) r = '' if self.rxbuf[2] != 6: # 长度错误 return c = self.rxbuf[3] p = self.rxbuf[5]*256+self.rxbuf[6] # 分析消息 if c == 0x3A: # TF 插入 if p == 0x01: r = 'USB 插入' self.usb_in = True self.device = 1 self.cmd_delay(6, 0x47) # 延时后查询文件数(直接查询无返回) if p == 0x02: r = 'TF 卡插入' self.card_in = True self.device = 0 self.cmd(0x48) # 查询 TF 文件数 elif c == 0x3B: # TF 弹出 if p == 1: r = 'USB 弹出' self.usb_in = False self.device = 0 self.usb_filecount = 0 if p == 2: r = 'TF 卡弹出' self.card_in = False self.device = 1 self.tf_filecount = 0 elif c == 0x3C: # usb 播放完成 r= 'USB 卡播放完第 {} 曲'.format(p) elif c == 0x3D: # tf 播放完成 r= 'TF 卡播放完第 {} 曲'.format(p) elif c == 0x3F: # 初始化参数 if p == 0x01: r = 'USB 已安装' self.usb_in = True self.device = 1 self.cmd_delay(6, 0x47) # 延时后查询文件数 if p == 0x02: r = 'TF 卡已安装' self.card_in = True self.device = 0 self.cmd(0x48) # 查询TF文件数 elif c == 0x40: # 错误 if p == 1: r = '返回忙' elif p == 2: r = '当前处于睡眠模式' elif p == 3: r = '串口接收错误' elif p == 4: r = '校验错误' elif p == 5: r = '指定文件超出范围' elif p == 6: r = '未找到指定的文件' elif p == 7: r = '插播指令错误' else: r = '未知错误 {}'.format(p) elif c == 0x41: # 应答 r = '应答' elif c == 0x42: # 查询播放状态 r = '播放状态: ' if p == 0: r += '停止播放' elif p == 1: r += '正在播放' elif p == 2: r += '暂停播放' elif p == 8: r += '睡眠状态' else: r += '未知' elif c == 0x43: # 查询音量 self.ack = 1 # 设置响应标志 r = '当前音量 {}'.format(p) elif c == 0x44: # 查询EQ r = '当前 EQ: ' if p == 0: r += 'Normal' elif p == 1: r += 'Pop' elif p == 2: r += 'Rock' elif p == 3: r += 'Jazz' elif p == 4: r += 'Classic' elif p == 5: r += 'Bass' else: r += '未知' elif c == 0x45: # 查询当前播放模式 r = f'查询当前播放模式 {p}' elif c == 0x46: # 查询当前软件版本 r = f'查询当前软件版本 {p}' elif c == 0x47: # 查询 usb 文件数 r = f'USB 文件数 {p}' self.usb_filecount = p elif c == 0x48: # 查询 TF 卡的总文件数 r = f'TF 卡文件数 {p}' self.tf_filecount = p print('tf_filecount',tf_filecount) elif c == 0x4B: # usb 当前曲目 r = f'USB 当前曲目 {p}' self.usb_index = p elif c == 0x4C: # 查询 TF 卡的当前曲目 r = f'TF 卡当前曲目 {p}' self.tf_index = p else: r = f'未知命令 {c} {p}' finally: self.message[-1] = r self.rxflag = 0 # 开始接收新数据 self.rxp = 0 return else: if self.rxbuf[self.rxp] == 0x7E: # 收到帧头 self.rxflag = 1 self.rxp = 1 def cmd(self, CMD, PARA=0): self.txbuf[0]=0x7E self.txbuf[1]=0xFF self.txbuf[2]=6 self.txbuf[3]=CMD self.txbuf[4]=0 self.txbuf[5]=PARA//256 self.txbuf[6]=PARA self.checksum=0x10000-self.txbuf[1]-self.txbuf[2]-self.txbuf[3]-self.txbuf[4]-self.txbuf[5]-self.txbuf[6] self.txbuf[7]=self.checksum//256 self.txbuf[8]=self.checksum self.txbuf[9]=0xEF print('uart.wite.buf',hexlify(self.txbuf)) self.u.write(self.txbuf) def stat(self): self.cmd(0x42) def volume(self, vol=None): if vol == None: self.ack = 0 # 清除标志位 self.cmd(0x43) return self._volume else: self._volume = vol self.cmd(6, vol) def play(self, n=None): if n == None: self.cmd(0x0D) else: self.cmd(3, n) def pause(self): self.cmd(0x0E) def stop(self): self.cmd(0x16) def next(self): self.cmd(0x01) def prev(self): self.cmd(0x02) def random(self): self.cmd(0x18) def filecount(self): if self.device == 0: return self.tf_filecount else: return self.usb_filecount def index(self): if self.device == 0: return self.tf_index else: return self.usb_index def sleep(self): self.cmd(0x0A) def reset(self): self.cmd(0xC) def repaet(self, mode=None): if mode==None: return self._repaet else: self._repaet = (mode == True) self.cmd(0x11, self._repaet) 主程序如下 from machine import Pin, UART, ADC, freq, reset, Signal, PWM, TouchPad from neopixel import NeoPixel import dfminiplayer import asyncio import json import random from binascii import hexlify import gc, os, sys # 系统常数 MAX_HISFILE_COUNT = const(36) # 引脚定义 PIN_RX = 10 PIN_TX = 8 PIN_BATV = 7 PIN_LED = 2 PIN_BUSY = 4 PIN_SWPREV = 1 PIN_SWMODE = 3 PIN_SWNEXT = 5 uartplay=UART(1, 9600, tx=Pin(PIN_TX), rx=Pin(PIN_RX)) uartplay.read() LED = NeoPixel(Pin(PIN_LED), 1) swp_pin = TouchPad(Pin(PIN_SWPREV)) swm_pin = TouchPad(Pin(PIN_SWMODE)) swn_pin = TouchPad(Pin(PIN_SWNEXT)) BUSY = Pin(PIN_BUSY, Pin.IN) adc_batv = ADC(PIN_BATV, atten=ADC.ATTN_11DB) class SYS_STAT: def __init__(self): self.volume = 10 # 音量 self.filecount = 740 # 文件数 self.his = [] # 历史文件序号 self.device = 0 # 播放设备 class global_var: stat = 1 # 播放状态 1: 播放 0: 停止 pause = False # 1:暂停 0:未暂停 batv = 4000 # 电池电压 mp = None # 播放器对象 sav_stat = 0 # 保存状态到文件标志 BAT_LOW = 0 # 电池低压标志,需要充电或重新复位后才能清除 cnt = 0 # 内部计数器 PREV = 9556 MODE = 9560 NEXT = 10360 class Switch: cnt = 0 # 按下计数器 last = 0 # 上次按下状态 flag = 0 # 按键标志 0: 未按下 1: 短按 2: 长按 init_press = 0 def __init__(self, Tpin): self.Tpin = Tpin gv = global_var() stat = SYS_STAT() # 创建按键对象 sw_p = Switch(swp_pin) sw_m = Switch(swm_pin) sw_n = Switch(swn_pin) # 创建player对象 gv.mp = dfminiplayer.miniplayer(uartplay) def load_stat(): try: print('从文件载入参数') f = open('stat.cfg', 'rt') d = json.load(f) stat.volume = d['volume'] # stat.filecount = d['filecount'] stat.his = d['his'] stat.device = d['device'] f.close() except: print('load config error, usin default value') def save_stat(): try: print('保存参数到文件') f = open('stat.cfg', 'wt') d = stat.__dict__ json.dump(d, f) f.close() except: print('# save stat to file error!') # 保存参数到文件 async def SAV_TASK(): while True: await asyncio.sleep_ms(1000) if gv.sav_stat > 0: gv.sav_stat -= 1 if gv.sav_stat == 0: save_stat() async def init(): print('device initial') # 设置频率为80M以降低功耗 freq(80000000) load_stat() await asyncio.sleep_ms(200) gv.batv = adc_batv.read_uv()//500 print('gv.batv:',gv.batv) # 查询 await asyncio.sleep_ms(200) # 发送复位 print('reset player') gv.mp.u.read() gv.mp.reset() await asyncio.sleep_ms(1200) def sw_check(sw): if sw==sw_m: sw.init_press=gv.MODE elif sw==sw_p: sw.init_press=gv.PREV elif sw==sw_n: sw.init_press=gv.NEXT if sw.Tpin.read()-sw.init_press >=300: # 键按下 print(sw.Tpin.read()) LED[0] = (150,150,150) #白 LED.write() if sw.last: # 按键已经按下 if sw.cnt < 55: # 按下时间计数 sw.cnt += 1 else: sw.flag = 2 else: sw.cnt = 0 # 开始计数 else: # 键未按下 if sw.last: # 按键释放 if sw.cnt > 50: # 大于1秒,长按 sw.flag = 2 elif sw.cnt > 0: sw.flag = 1 else: sw.flag = 0 sw.cnt = 0 else: sw.flag = 0 if sw.Tpin.read()-sw.init_press >=300: sw.last = 1 else: sw.last = 0 # 按键检测任务 async def SW_TASK(): print('按键任务启动') while True: sw_check(sw_m) if sw_m.flag > 0: print('play 键按下', sw_m.flag) gv.stat = not gv.stat if not gv.stat: # 暂停播放 gv.pause = True gv.mp.pause() print('暂停') sw_m.flag = 0 sw_check(sw_p) if sw_p.flag > 0: if sw_p.flag == 1: # 上一首 print('上一首') if len(stat.his) > 1: gv.stat = 0 stat.his.pop() # 删除最后一个历史文件 print('历史文件', stat.his) gv.mp.play(stat.his[-1]) # 播放上一首 gv.pause = False print('播放文件', stat.his[-1]) await asyncio.sleep_ms(800) else: print('无更多历史文件') if len(stat.his) > 0: stat.his.pop() # 删除最后一个历史文件 gv.mp.pause() # 利用暂停播放,自动开始播放新的文件 gv.stat = 1 else: # 音量减 if stat.volume > 1: stat.volume -= 1 gv.mp.volume(stat.volume) gv.sav_stat = 10 print('音量减', stat.volume) await asyncio.sleep_ms(100) sw_p.flag = 0 await asyncio.sleep_ms(100) sw_check(sw_n) if sw_n.flag > 0: if sw_n.flag == 1: # 下一首 gv.stat = 1 gv.pause = False gv.mp.pause() # 利用暂停播放,自动开始播放新的文件 print('下一首') else: # 音量加 if stat.volume < 30: stat.volume += 1 gv.mp.volume(stat.volume) gv.sav_stat = 10 print('音量加', stat.volume) await asyncio.sleep_ms(100) sw_n.flag = 0 await asyncio.sleep_ms(100) if sw_m.cnt > 50 or sw_p.cnt > 50 or sw_n.cnt > 50: LED[0] = (15,0,15) #紫色 LED.write() await asyncio.sleep_ms(20) # 检查 mini player 状态 async def player_stat(): global stat print('监听播放器任务启动') while True: r = gv.mp.status() if r != '': print('>', r) if r == 'USB \u63d2\u5165\n' or r == 'USB \u5df2\u5b89\u88c5\n' or (r == 'TF \u5361\u5f39\u51fa\n' and gv.mp.usb_in): await asyncio.sleep_ms(1000) if gv.mp.usb_filecount > 0: print('# 切换到 USB 设备') stat.his = [] gv.mp.cmd(9, 1) await asyncio.sleep_ms(200) play_new() if r == 'TF \u5361\u63d2\u5165\n' or r == 'TF \u5361\u5df2\u5b89\u88c5\n' or (r == 'USB \u5f39\u51fa\n' and gv.mp.card_in): await asyncio.sleep_ms(500) if gv.mp.tf_filecount > 0: print('# 切换到 TF 卡') stat.his = [] gv.mp.cmd(9, 2) await asyncio.sleep_ms(200) play_new() await asyncio.sleep_ms(200) # 播放一个新文件 def play_new(): # 无文件 print('stat.filecount:',stat.filecount) if stat.filecount == 0: print('无文件') return if stat.filecount > MAX_HISFILE_COUNT: while True: n = random.randrange(stat.filecount) if not n in stat.his: # 保证 n 不在历史列表中 break else: n = random.randrange(stat.filecount) # 添加到历史列表 stat.his.append(n) if len(stat.his) > MAX_HISFILE_COUNT: stat.his.pop(0) print('历史播放列表', stat.his) gv.mp.play(n) print('play new file', n) gv.sav_stat = 10 # 播放任务 async def player(): print('播放任务启动') print('历史文件', stat.his) await asyncio.sleep_ms(200) print('设置音量', stat.volume) gv.mp.volume(stat.volume) await asyncio.sleep_ms(200) # 上电后如果电压超过4.5V(插入USB充电),则不自动播放 if len(stat.his) > 0: # 继续播放上次最后文件 gv.mp.play(stat.his[-1]) await asyncio.sleep_ms(800) if gv.batv > 4500: print('充电模式,暂停播放') gv.stat = 0 gv.mp.pause() gv.pause = True while True: if BUSY()==1 and gv.stat and (gv.mp.card_in or gv.mp.usb_in): cnt = 0 while True: # 检查播放器响应状态,因为连接pc时无返回 gv.mp.volume() await asyncio.sleep_ms(200) if gv.mp.ack: break if cnt < 20: cnt += 1 if cnt == 10: print('设备无响应,可能是连接到计算机') if gv.pause: gv.pause = False gv.mp.play() print('继续播放文件', stat.his[-1]) else: play_new() await asyncio.sleep_ms(600) gv.mp.volume(stat.volume) # 插入卡后有时会自动重置模块音量,因此每次播放前设置音量 await asyncio.sleep_ms(200) await asyncio.sleep_ms(200) async def LED_TASK(): c = bytearray(1) while True: if gv.BAT_LOW: LED[0] = (30,0,0)#红 if gv.stat: LED[0] = (0,0,100) #蓝 else: LED[0] = (0,30,0) #绿 if not (gv.mp.card_in or gv.mp.usb_in): LED[0]=(30,30,0)#黄 c[0] += 1 LED.write() await asyncio.sleep_ms(20) def system_info(): print(f'硬件信息: {sys.platform}') print(f'固件版本: {sys.version}') try: f = round(freq()/1000000, 1) except: f = round(freq()[0]/1000000, 1) print(f'系统频率: {f} MHz') r= gc.mem_free()+gc.mem_alloc() print(f'内存大小: {r}') r = os.statvfs('/') disksize = round(r[0]*r[2]/1024**2, 1) freesize = round(r[0]*r[3]/1024**2, 1) print(f'磁盘大小: {disksize} M') print(f'剩余空间: {freesize} M') buf = bytearray(16) bdev.readblocks(0, buf) r = 'LittleFS' if buf[8:16] == b"littlefs" else 'FAT' print(f'文件系统: {r}') # 主任务,同时检查系统电压 async def main(): print('power on\n') # 系统信息 system_info() print('\ncreate tasks') asyncio.create_task(player_stat()) await init() asyncio.create_task(player()) asyncio.create_task(SW_TASK()) asyncio.create_task(SAV_TASK()) asyncio.create_task(LED_TASK()) print('设置音量', stat.volume) gv.mp.volume(stat.volume) await asyncio.sleep_ms(200) print('run') v = 0 gv.cnt = 0 while True: # 每100毫秒采样一次,每6.4秒计算一次电池电压 v += adc_batv.read_uv()//500 gv.cnt += 1 if gv.cnt > 63: gv.batv = v//gv.cnt gv.cnt = 0 v = 0 if gv.batv < 3400: gv.BAT_LOW = 1 if gv.BAT_LOW: if gv.batv > 3600: gv.BAT_LOW = 0 print(f'{gv.batv:04}', end='\x08'*4) await asyncio.sleep_ms(100) # 创建主任务 asyncio.create_task(main()) # 运行 while True: try: asyncio.Loop.run_forever() except KeyboardInterrupt: print('Keyboard int') break except MemoryError: print('memory error') reset() except exception as e: print(e) reset()   以下是程序的主要功能描述: 硬件功能 触摸按键: 播放/暂停键(PIN_SWMODE):     短按:播放或暂停当前曲目。 上一曲/音量减键(PIN_SWPREV):  短按:切换到上一首曲目。  长按:降低音量。 下一曲/音量加键(PIN_SWNEXT):  短按:切换到下一首曲目。  长按:增加音量。 音频播放器: 通过 UART 与 DFMiniPlayer 模块通信,控制音频播放。 支持从 TF 卡或 USB 设备播放音频文件。 电池电压检测: 通过ADC 检测电池电压,当电压低于阈值时进入低电量模式,通过2812闪烁红灯。  指示灯: 通过设置2812不同的颜色,来代表显示系统多个状态(如播放、暂停、加减音量、低电量提醒等)。   软件功能 随机播放: 默认开启随机播放模式,确保每次播放的曲目顺序不同。 通过 random.randrange 随机产生编号,选择曲目序号。 播放历史记录: 已播放的曲目会被记录到 stat.his 列表中,防止重复播放。 音量调节: 音量范围:1 到 30。 通过长按上一曲/下一曲键调节音量。 播放器状态检测: 检测播放器是否处于忙碌状态(BUSY 引脚)。 支持自动切换播放设备(TF 卡或 USB)。 低电量保护: 当电池电压低于 3.4V 时,进入低电量模式,LED 显示红灯闪烁。 当电压恢复到 3.6V 以上时,退出低电量模式。 参数保存与加载: 系统参数(如音量、播放历史、设备状态)会保存到 stat.cfg 文件中。 每次启动时从文件加载参数,确保状态持久化。   任务与异步处理 异步任务: SW_TASK:检测触摸按键状态,处理短按和长按事件。 player_stat:监听播放器状态,处理设备切换(TF 卡/USB)。 player:控制音频播放逻辑,包括随机播放、暂停、继续播放等。 SAV_TASK:定期保存系统参数到文件。 LED_TASK:控制 LED 指示灯,显示系统状态。 main:主任务,负责初始化系统并运行其他任务。 异步框架: 使用 asyncio 实现多任务并发,确保系统响应迅速且高效。   系统信息与调试 系统信息: 打印硬件信息(如系统频率、内存大小、磁盘空间等)。 显示文件系统类型(LittleFS 或 FAT)。 调试信息: 通过串口打印调试信息,如按键状态、播放器响应、电池电压等。   续航与功耗优化 续航能力: 设备搭载 500mAh 电池,在默认音量(10)下实测续航时间为 5 小时。 功耗优化: 系统频率设置为 80MHz,以降低功耗。   异常处理 异常捕获: 捕获 MemoryError、KeyboardInterrupt 等异常,防止系统崩溃。 发生异常时,系统会自动复位。 该程序实现了一个功能完善的音频播放器控制系统,支持触摸按键控制、随机播放、音量调节、播放历史记录、低电量提醒等功能。通过异步任务和多任务并发,系统能够高效运行并实时响应用户操作。   二、功能演示视频: [localvideo]653085064ff19b8413a658a236fde712[/localvideo]   三、主要物料成本: ESP32-S2MINI:9.5 MP3miniplayer:4.5 耳机仓:0.8 迷你扬声器:3 USB接口板:0.5 PCB:嘉立创免费 其余电子元器件物料是常用且成本很少的,就不一一列举了 合计:18.3   四、总结 通过此次的DIY,让我对产品的设计有了新的认识,尤其是锻炼了动手能力,该mini音响还是有很多不够完善的地方,例如: 1.结构上未考虑大重心问题,导致开盖后无法直立于桌面播放,影响使用观感 2.未添加蓝牙板,没有蓝牙播放功能 计划在下一次改进,并考虑两路音频切换的设计。        

  • 2025-01-24
  • 发表了主题帖: DIY 耳机盒爆改随身音响(二)

    本帖最后由 Newhor 于 2025-1-24 09:07 编辑 在上一篇分享DIY 耳机盒爆改随身音响(一)中介绍了大致的思路,这次在完成原理设计的过程中,考虑到了更多的细节,具体硬件以及结构设计如下   硬件设计 1. 硬件框图   主要包含主控MCU、MP3模块、电池、电池充电管理以及采样电路、霍尔传感器开、灯、喇叭等 2. 原理图   MCU采用的时ESP32-S2MINI模块,主要功能用于与MP3模块通信、控制灯的不同颜色亮度、采样电池以及触摸按钮采集,考虑到该模块体积小以及拥有触摸按键功能。可以看到整个电路其实只用到了几个引脚就够了,如果使用物理按键的话,这里采用引脚更少的ESP32-C3MINI也是够用的,同时体积和成本更好。 MP3模块采用的是DFPlayer Mini,可以直接驱动扬声器,可以通过串口控制。模块集成了MP3、WAV、WMA的硬解码,同时软件支持TF卡驱动,支持FAT16、FAT32文件系统,最大支持32G内存卡。通过简单的串口指令即可完成播放指定的音乐。 充电管理芯片使用经典TP4054,给500mah的小电池充电也够用了 CH213K在霍尔开关电路中起到防反的作用,防止充电时的5V直接给电池充电,损坏电池,此元件相比与传统二极管,有着更低的导通压降和功耗。 3. PCB设计       PCB绘制是感觉整个制作过程中最繁琐的一步,主要是结构上要考虑的东西很多,包括各个地方的尺寸,还有各个接模块摆放的位置,对精度都有一定的要求,所以这个PCB打了第二次板才实现了稍微合理的布局。     接下来看看实板效果吧     MP3模块和ESP32采用的正反面重叠的方式布置,考虑到这样会有一个模块的一边引脚在另一个模块的底部,焊接会有冲突,所以ESP32在原理图设计时就只使用了一边的引脚,放弃另一边引脚的使用。   反面的Type-c接口采用了叠加一个接口板的形式,这样是为了可以微调接口的位置,使其对准底部的预留Type-C孔位,在飞线焊接后,使用热熔胶固定     在安装完Type-c接口板的基础上,再将ESP32堆叠焊接上去   最后将其他剩余元器件焊接上去,总的效果如下图   4. 安装结构 首先上盖挖出大小合适的空槽,确保喇叭刚好可以卡进去,喇叭线从上盖旋转结构处横穿至下层,确保盖子的每次开合使得喇叭线弯折的弧度较小,降低折损     三个触摸按键的使用胶带从盒子内侧,粘贴至壳上     在上盖内部安装一块磁铁,并调整霍尔传感器的位置,使盖下来的时候磁铁能够对准霍尔传感器的位置           基本硬件设计和结构就是这些了,这个结构设计感觉是相对困难且非常容易出错的一步,需要考虑位置的地方很多,接下来就可以开始编写程序,实现各种功能控制了,这次的分享就到这里了,下次分享软件代码和效果演示

  • 2025-01-22
  • 回复了主题帖: DIY 耳机盒爆改随身音响(一)

    lkh747566933 发表于 2025-1-21 15:16 真是牛人啊,这么厉害。功放电路能装进去吗? 这款MP3模块自带驱动喔,可以装得下,硬件设计已经完成了,这两天会更新分享

  • 2025-01-15
  • 回复了主题帖: DIY 耳机盒爆改随身音响(一)

    御坂10032号 发表于 2025-1-14 22:37 0.8元爆炸了我都夸它炸的响 加上其他的模块,估计总成本得二十多了,我后面会整理一个带价格的物料清单

  • 回复了主题帖: DIY 耳机盒爆改随身音响(一)

    tagetage 发表于 2025-1-14 21:32 这0.8的价格打破了我的认知。。 就是运费要6元

  • 回复了主题帖: DIY 耳机盒爆改随身音响(一)

    秦天qintian0303 发表于 2025-1-14 21:53 主要是这样的音腔,喇叭的音质也不会很好,最好来那种自带音腔的  主要是考虑到安装空间的限制,没找到其他合适大小的,这个还是带一点音腔的,安静环境下试听了一下,感觉还行

  • 回复了主题帖: DIY 耳机盒爆改随身音响(一)

    oxlm_1 发表于 2025-1-14 21:16 貌似没上功放板,esp直驱喇叭嘛? MP3模块上有接口,可以驱动的

  • 2025-01-14
  • 发表了主题帖: DIY 耳机盒爆改随身音响(一)

    本帖最后由 Newhor 于 2025-1-14 19:59 编辑   在淘宝上购花0.8买的耳机盒,外壳成色还不错,拆开看了,除了没有耳机以外,空间大小还不错,想着利用上里面面原来的电池和霍尔传感器,再加一个MP3模块、一个主控模块和一个小喇叭就能变成小的随身音响了。                    1.将耳机仓部分的塑料部分剪掉,取出内部原来的电路板,里面的电池以及霍尔传感器可以留下来使用 2.随后大致量了一下内部空间尺寸,47X35X38mm,内部的纵向空间还是不错的,估摸着前后堆叠着放,可以放得下一个MP3 模块、一个ESP32主控模块、一个电池等主要元器件           3.MP3模块打算使用MP3mini player,比较经典的一款,体积比较小,功能也比较齐全;主控板打算采用esp32-s2mini,主要是想利用它的触摸按键功能,安装位置比较好找,看着也比较高端一些,这里如果使用物理按键的话,就建议采用S3mini,体积更小,价格也更实惠。   4.喇叭打算安装在上盖,测量尺寸后,在网上找了一款体积和上盖差不多小喇叭,测量了一下尺寸,后面将上盖内部塑料挖空后刚好能放下。           初步想法是实现以下功能: 通过耳机盒自带的霍尔传感器实现耳机盒开盖自动上电播放音乐,闭盖断电 通过三个触摸按键的短按和长按,实现音乐上一曲、下一曲和音量的加减 结合mini MP3 player模块指令集,通过ESP32编程来实现随机播放 底部Type-c接口保留作为充电以及程序调试     后续将持续更新硬件、软件设计以及成品效果展示                                

  • 2024-11-27
  • 回复了主题帖: 【2024 DigiKey 创意大赛】自行车智能骑行助手

    wangerxian 发表于 2024-11-27 09:02 原来如此,这样确实也能检测胎压,不过在骑行的过程压力会变化吧 分为了两个阶段,初始未骑行的状态的测量值转换为胎压,骑行中的值转换为体重,用了一些算法对进行骑行时的数据进行滤波处理

  • 2024-11-26
  • 回复了主题帖: 【2024 DigiKey 创意大赛】自行车智能骑行助手

    wangerxian 发表于 2024-11-26 18:47 可能传感器是夹在内胎和外胎之间 先将轮胎气放光,然后夹在钢圈和内胎之间,然后再充气固定的  

  • 2024-11-25
  • 发表了主题帖: 【2024 DigiKey 创意大赛】自行车智能骑行助手

    本帖最后由 Newhor 于 2024-11-27 09:38 编辑 自行车智能骑行助手 作者:newhor   一、作品简介   简介         自行车智能骑行助手的想法是填补骑行配件功能的空缺,为户外骑行爱好者提供一个集胎压测量、体重监测分析及环境感知预警于一体的综合智能解决方案 市面上现有的骑行配件中,很少有监测骑行者长途骑行下的体重变化,胎压预警以及环境感知预警等功能。本作品主要特点如下: ①在骑行前,安装在自行车轮胎中的压力传感器采集的压力数据,可以监测轮胎的初始压力值,并通过ESP-NOW无线通信将数据传送到主控MCU,再通过前期的数据标定确定的函数关系,MCU将压力数据转换为胎压后,显示在屏幕上。当检测到胎压低于预设的安全范围(例如25PSI)时,智能助手会立即通过显示器提示骑行者进行胎压补充,确保骑行安全。具体实现包括:压力传感器安装在轮胎钢圈与内胎之间,测量初始压力值;ESP-NOW模块负责将传感器数据无线传输到主控MCU;主控MCU处理接收到的数据,并通过显示器显示给骑行者。   ②在长时间骑行时,系统通过采集安装在轮胎内的压力传感器的压力值,通过前期的标定,将其转化为体重值。结合骑行时间和BME680传感器提供的环境温度数据,系统通过算法计算一段时间内体重的变化量,如果体重变化量超过预设阈值,会触发警报,通过显示屏提醒用户,同时提供休息和补水建议。   ③通过拓展的BME680环境传感器,给骑行者提供户外骑行时的空气质量预警和高温预警。一旦发现空气质量下降或温度过高,智能助手将通过显示屏及时提醒骑行者采取相应措施,如减少运动强度或寻找遮阴处休息,确保骑行者的健康和安全。   ④通过车把端的MCU采集并记录BME680传感器在一段时间内的气压和湿度数据,MCU对这些数据进行分析,当检测到气压持续降低且空气湿度升高超过一定阈值时,提供下雨预警,通过显示器告警骑行者,建议提前返回或寻找避雨场所,确保骑行安全。   ⑤可记录骑行过程中的体重、胎压、环境变化数据保存在本地(后续添加手机端无线接收功能),并提供长期趋势分析,帮助骑行者更好地了解自身状况和改善骑行计划。 此外,该还系统具有强大的可拓展性,以后可以集成更多好玩的功能进来。   主要特点: 胎压监测 实时监测体重变化 智能骑行建议 恶劣天气预警 可存储运动数据,分析长期数据 硬件模功能块化设计,功能易扩展 可太阳能充电 开源   照片 (1)车把数据接收显示端        (2)轮胎数据采集发送端             二、系统框图 硬件说明   主要物料 控制器:ESP32-C6,ESP32-S2MINI 加速度传感器:4554、MMA8452 湿度空气传感器:BME680 显示屏幕:1.8寸TFT 充电管理芯片:TP4054 薄膜式压力传感器 锂电池、太阳能板   硬件框图 主要分为车轮压力数据采集端、以及车把数据接收显示端,采用模块集成方式、方便更换与拓展。         2.软件说明       软件分为4个部分: MCU数据处理 ESPNOW数据传输 传感器数据采样控制 液晶显示   三、各部分功能说明 硬件功能 (1)轮胎端:主要完成胎压数据采集、ESPnow数据发送等功能   轮胎端主控使用ESP32-S2MINI,这是一款乐鑫一款入门级开发板,搭载Xtensa®32位LX7单核处理器,工作频率高达240MHz。可深度休眠,利用低功耗协处理器监测外设的状态变化或某些模拟量多的阈值,可通过外部中断唤醒,常用于可穿戴电子设备、智能家居等场景常用于物联网设备开发。在该作品中,利用其ADC功能,采集安装于轮胎内壁的薄膜式压力传感器的电压值,并通过ESPnow无线功能,实时将数据上传到车把端的MCU进行处理。 加速度传感器采用了基础型号MMA8452,加速度传感器的中断输出引脚,连接到轮胎端ESP32MINI上可支持休眠唤醒的IO口,并在采样程序中配置了休唤醒中断。当自行车静止一段时间后,ESP32MINI由于未检测到加速度传感器的运动中断,会进入深度休眠降低功耗,当检测到轮胎震动时,加速度传感器会立即产生运动中断唤醒MCU,开始采集压力数据,实现了轮胎端采样系统的低功耗。     压力传感器采用薄膜式,方便安装于轮胎与钢圈之间,当轮胎由于胎压或骑行者体重产生形变时,薄膜式压力传感器受到压力产生相应的形变,其电阻值产生相应变化,MCU通过电阻分压电路的方式进行ADC采样,将压力值转换为电压值。   供电部分,配备了电池,并基于TP4054芯片设计了可多路供电的防反充电电路,通过添加多个二极管防止反充,实现开发板的USB接口以及太阳能板双充电模式。       (2)车把端:主要完成数据接收,处理、以及数据显示等功能   车把端主控使用大赛提供的ESP32-C6开发板,是一款高性能、低功耗的单芯片解决方案。多达 22 个 GPIO 引脚,支持 SPI、I2C、UART 等多种通信协议,具有 Wi-Fi 6 和 蓝牙低能耗(Bluetooth LE 5.2) 的支持,主要面向现代无线通信需求。该作品采用该模块作为主控接收端MCU,能满足ESPNOW无线功能高频的数据接收和处理需求,并同时实现了对多个传感器的数据采集处理,以及显示。 显示器使用ST7735驱动TFT屏,支持全彩显示、内容方向可调整,数据显示量够大,且观感更好。 加速度传感器模块使用的是大赛提供的4554,ICM-20948 是一款高性能的9轴运动传感器,具有高精度与低噪声等特点,在该作品中,该模块用于判断自行车实时的骑行状态,是加减速还是匀速,帮助MCU处理数据,消除加减速带来的轮胎形变时产生的误差数据。 环境传感器使用的是大赛提供的BME680模块,具有环境参数丰富,精度较高等特点。在该作品中,主要用于采样环境参数,包括温湿度、空气质量、大气压等。其中温度数据,用于辅助长时间骑行时体重变化量供相应的休息和补水建议;通过分析湿度和大气压数据下雨前的变化趋势,提前预测降雨的可能性,并通过屏幕向骑行者发出提醒;空气质量数据,实时转换为空气质量IAQ,给户外骑者相应的户外骑行建议。 供电电路与轮胎端一致,采用电池供电,并支持USB与太阳能板双充电。 2.软件功能     (1)数据标定以及转换部分 在胎压数据标定时,由于传感器的精度以及受到安装位置的影响,会出现相同胎压下前后轮的压力传感器初始压力不一致的现象,故前后轮的压力数据分开进行标定。压力传感器安装于车轮内部后,通过带压力显示的打气筒给轮胎补充到多组不同的胎压值,得到对应胎压下的传感器压力数值,作为空载状态下,不同胎压与采集电压的数据组合,通过数据拟合得到相应的函数关系后,实现通过空载状态下电压计算出当前胎压数值的功能。 在体重数据标定时,通过记录多个不同体重骑行者,骑行时与空载的压力差值,与相应体重进行数据组合,然后进行数据拟合,得到电压差与骑行者体重函数关系。     (2)数据滤波部分           骑行时压力传感器由于轮胎转动到不同的位置、路面颠簸、急加减速等因素,会产生部分误差数据。采用滑动平均法,对一段时间窗口内的数据进行平均,从而减少噪声或波动,使数据更加平滑。   四、作品源码   自行车智能骑行助手——作品源码-嵌入式开发相关资料下载-EEWORLD下载中心     系统的硬件部分软件使用 micropython 进行开发,采用异步多任务形式。部分源码如下:       文件:task_TFTshow.py import asyncio from machine import Pin, SoftSPI from time import sleep_ms from global_var import * from cfg import * async def TFTshow(): while True: if gv.int_flag == 1: # pass lcd.clear(0) lcd.rotate(180) lcd.drawText('系统初始化中'+str(gv.int_cnt*10)+'%',0,50,font,0x0f0) lcd.drawText('请在10S内推动自',0,70,font,0xff0) lcd.drawText('行车采集初始数据',0,90,font,0xff0) lcd.show() elif gv.str_flag == 1 or 2 or 3: lcd.clear(0) lcd.rotate(180) lcd.drawText('温度:'+str(round(gv.tmp,1))+'°',0,0,font,0x0f0) lcd.drawText('湿度:'+str(round(gv.hum,1))+'%',0,20,font,0xff0) lcd.drawText('气压:'+str(round(gv.pre,1))+'Pa',0,40,font,0x00f) lcd.drawText('IAQ:'+str(gv.IAQ)+gv.iaq_leve,0,60,font,0xff0) lcd.show() await asyncio.sleep(2) if gv.weight_int !=0: lcd.clear(0) lcd.rotate(180) lcd.drawText('初始胎压psi:',0,0,font,0x00f) lcd.drawText('前:'+str(gv.P1_int)+'后:'+str(gv.P2_int),0,20,font,0x00f) lcd.drawText('初始体重:'+str(gv.weight_int)+'kg',0,40,font,0x0f0) lcd.drawText('实时体重:'+str(gv.weight)+'kg',0,60,font,0x0f0) # lcd.drawText('体重差值:'+str(gv.weight_int-gv.weight)+'kg',0,80,font,0x00f) lcd.drawText('运动时长:'+str(round(gv.SportTime/60+0.7,1))+'min',0,100,font,0x00f) lcd.show() await asyncio.sleep(2) gv.SportTime+=2 gv.SportTime+=2.1 if gv.end_flag ==1:#运动结束 lcd.clear(0) lcd.rotate(180) lcd.drawText('运动已结束',0,0,font,0x0f0) lcd.drawText('该次运动时长为',0,50,font,0xff0) lcd.drawText(str(round(gv.SportTime/60,1))+'min',0,70,font,0xff0) lcd.drawText('该次运动减重;',0,90,font,0xff0) lcd.drawText(str(gv.weight_int-gv.weightmin)+'kg',0,110,font,0xff0) lcd.show() import machine machine.reset() await asyncio.sleep(0.1) # asyncio.run(TFTshow())     文件:task_ESPnow.py import network import aioespnow import asyncio import neopixel import binascii,time from machine import Pin from neopixel import NeoPixel from time import localtime from global_var import * from cfg import * np = neopixel.NeoPixel(Pin(8), 1) # WLAN接口必须处于活动状态才能 send()/recv() sta = network.WLAN(network.STA_IF) # 或 network.AP_IF sta.active(True) sta.disconnect() # 对于 ESP8266 e = aioespnow.AIOESPNow() e.active(True) peer = b'\xAA\xBB\xCC\xDD\xEE\xFF' # 配对设备wifi接口的MAC地址 e.add_peer(peer) # 发送前必须 add_peer() # e.send(peer, '123') def Pcvt1(x): if x>=0 and x<0.76: return 35 if x>=0.76 and x<0.92: return 25 if x>=0.92 and x<1.0: return 30 if x>=1.0 and x<1.08: return 35 if x>=1.08 and x<1.3: return 40 if x>=1.3 and x<1.4: return 45 if x>=1.4: return 50 def Pcvt2(x): if x>=0 and x<1.12: return 20 if x>=1.12 and x<1.19: return 25 if x>=1.19 and x<1.24: return 30 if x>=1.24 and x<1.26: return 35 if x>=1.26 and x<1.28: return 40 if x>=1.28 and x<1.32: return 45 if x>=1.32: return 50 def P_Vdata_int(r):# 初始电压平均 global v1,v2,m,n print('接收的r[4:9]字节数据为:',r[4:9],type(r[4:9])) if r[2] == 49: gv.V1 = float(r[4:9].decode('utf-8')) print('转换后的整数数据为V1:',gv.V1) v1 = gv.V1+v1 n=n+1 gv.Vint1 = round(v1/n,3) print('gv.Vint1',gv.Vint1 ) elif r[2] == 50: gv.V2 = float(r[4:9].decode('utf-8')) print('转换后的整数数据为V2:',gv.V2) v2 = gv.V2+v2 m=m+1 gv.Vint2 = round(v2/m,3) print('gv.Vint2',gv.Vint2) def P_Vdata(x):#取峰值 global r gv.st_cnt+=1 if gv.st_cnt<=x: if r[2] == 49: gv.V1 = float(r[4:9].decode('utf-8')) elif r[2] == 50: gv.V2 = float(r[4:9].decode('utf-8')) if gv.V1 > gv.V1max: gv.V1max = gv.V1 if gv.V2 > gv.V2max: gv.V2max = gv.V2 else: gv.st_cnt = 0 gv.V = gv.V1+gv.V2-gv.Vint1-gv.Vint2 print('========gv.V:',gv.V) gv.weight = WGTcvt(gv.V) if gv.weight<gv.weightmin: gv.weightmin = gv.weight def WGTcvt(y): if y>0.1: t = 57+y*12 elif y< 0.1 and y >0.05: t = 57+y*21 elif y <0.05 and y>0.01: t = 57+y*31 elif y <0.01: t = 57+y*61 print('^^^^^^^^^t',t) return t async def e_recv(): global r while True: if e.any(): host, msg = e.recv() if len(msg)>=5 and len(msg)<=15: if msg[:2] == b'cc': print('msg',msg) np[0] = (0, 30, 0) np.write() gv.int_cnt +=1 print('gv.int_cnt',gv.int_cnt) if gv.int_cnt <= 10:#初始化时间10S #gv.int_flag = 1 P_Vdata_int(msg) print('gv.int_flag :',gv.int_flag) elif gv.int_cnt>10 and gv.str_flag ==0: gv.int_flag = 0 gv.str_flag = 1 print('gv.str_flag:',gv.str_flag) else: pass if gv.str_flag == 1:#初始化结束,胎压转换 gv.P1_int = Pcvt1(gv.Vint1) print('gv.P1_int',gv.P1_int) gv.P2_int = Pcvt2(gv.Vint2) print('gv.P2_int',gv.P2_int) gv.str_flag = 2 print('gv.str_flag :',gv.str_flag) if gv.str_flag == 2:#开始测量初始体重 print('---------------gv.str_flag :',gv.str_flag) gv.waitcnt+=1 if gv.waitcnt>=100: r = msg P_Vdata(10)#取50次数据的峰值 if gv.weight>0 and gv.st_cnt ==0: gv.weight_int = gv.weight print('gv.weight_int',gv.weight_int) gv.weight = 0 gv.str_flag = 3 if gv.str_flag == 3:#开始测量运动后体重 print('```````````gv.str_flag:',gv.str_flag) r = msg P_Vdata(20)#每100次数据更新一次体重 r = localtime() s = '{:04}-{:02}-{:02} {:02}:{:02}:{:02} > {}'.format(r[0], r[1], r[2], r[3], r[4], r[5], str(msg)) f = open('称重数据1.txt', "a", encoding="UTF-8") f.write( s+'\n') f.close() await asyncio.sleep(0.05) np[0] = (0, 0, 0) np.write() else: print('msg head error') else: print(' msg len error') else: print('等待数据,请在5秒内启动车辆') gv.endcnt+=1 if gv.endcnt>=100:#(5秒内未收到数据,运动结束) gv.endcnt = 0 gv.end_flag = 1 await asyncio.sleep(0.05) # asyncio.run(e_recv())   五、作品功能演示视频 自行车智能骑行助手——得捷大赛作品演示视频_哔哩哔哩_bilibili [localvideo]7cc8994b5fdfb34a822f81dc96acf77b[/localvideo]   六、项目总结     参加这次活动之前,刚好买了一辆自行车,加入了骑行者的队伍,然后计划购买一些配件来改装车辆,和同事讨论时,想到胎压测量以及骑行时的体重监测等场景,在市面上没有找到类似功能的产品,所以想尝试做一下这个功能的作品,看看是否能够实现预期的效果。     此次作品将最初想法能大致实现了,可以完成测量胎压和体重等功能,但由于各方面的限制,数据精度上但是还有很多不足,例如传感器精度不足以及线性度不够、安装位置无法保证受力均衡、轮胎的自然转动带来的数据波动,都会影响最后得到的数据精度。由于时间关系,还有运动数据上传到手机功能暂未实现,这个后续再添加。     通过参加本次活动,收获还是不小的,真真切切的感受到了从一个想法到一个作品做好的难度,需要考虑的东西太多了,做到一半才发现很多东西与预想的差别很大,可能写程序、画板子还是其中最容易的环节,例如此次作品中,一个合适的传感器的选择和安装固定的方式,在前期就花了很多时间。     最后,感谢得捷和EEworld,组织了这次活动,让大家的想法可以有个很好的机会来实现,并且能学习到很多其他人的有趣想法。     【2024 DigiKey 创意大赛】物料开箱帖 - DigiKey得捷技术专区 - 电子工程世界-论坛 【2024 DigiKey 创意大赛】自行车智能骑行助手 - DigiKey得捷技术专区 - 电子工程世界-论坛   https://gitee.com/newhor/zhinengqixing.git  

  • 2024-11-24
  • 上传了资料: 自行车智能骑行助手——作品源码

  • 2024-11-05
  • 加入了学习《【2024 DigiKey 创意大赛】AI全功能环境监测站作品功能演示视频》,观看 【2024 DigiKey 创意大赛】AI全功能环境监测站作品功能演示视频

  • 2024-08-11
  • 发表了主题帖: 【2024 DigiKey 创意大赛】物料开箱帖

    本帖最后由 Newhor 于 2024-8-11 11:38 编辑   非常荣幸能入选这次2024 DigiKey 创意大赛“大赛,这次我想做的是一个山地车胎压测量以及称重系统,本次我选择的物料是一块ESP32-C6开发板、BME680传感器和4554加速度传感器。接下来先来看一下物料   包装很严实   一个esp32 c6的开发板,作为主控MCU,感觉包装盒还不错   BME680用来采集温度,用来做胎压的算法的温度校准,它的VOC、湿度、气压这几个检测功能也可以为户外骑行条件提供一些参考,功能很齐全     一个4554用来做运动状态检测    

最近访客

< 1/2 >

统计信息

已有13人来访过

  • 芯积分:183
  • 好友:1
  • 主题:9
  • 回复:30

留言

你需要登录后才可以留言 登录 | 注册


现在还没有留言