
去年我去看望一位因中风住院的长辈病房墙边挂着一块白板上面写着“水”“疼”“尿”“谢谢”几个词。需要的时候家里人会把白板递过去让他用手指。这个方法确实管用可一旦只有护工在或者老人手没有力气抬不起来白板就成了一种负担。回家以后我一直放不下这个画面那些心里明白却说不出来的患者真正需要的可能只是一个能替他们开口的小盒子。于是就有了这个项目——用树莓派Pico做一台脑卒中沟通辅助发声设备配合0.96寸OLED显示屏、几个大按钮和一个小喇叭让患者按一下就能说出想要的话。这篇文章会从硬件选型、接线细节、MicroPython固件、实测翻车点再到长期使用的改进方向把我从原型到可上手使用的完整折腾过程写下来。如果你正准备做辅助沟通装置、康复护理玩具或者单纯想用raspberry pi 2040加oled 0.96做点有人情味的东西这篇应该能帮你少走不少弯路。先说清楚它不是一个医疗器械也不能替代康复治疗师它能做的是在患者还能控制手指或头部的情况下把“我想喝水”变成一句清晰的人声。1. 为什么沟通板首选树莓派Pico而不是桌面派从病房场景反推硬件选型1.1 病房里真正需要的是什么脑卒中后语言障碍主要分两类。一类是失语症患者大脑语言中枢受损听、说、读、写能力下降心里明白但找不到词或者听不懂别人说话另一类是构音障碍肌肉控制出问题舌头嘴唇不听使唤说话含糊不清。两类患者都可能出现“表达需求失败”的瞬间然后变成急躁、拍床、甚至拒食。在病房里观察两天就会发现沟通需求其实可以被压缩成几大类基本生理需求喝水、吃饭、上厕所、冷热、疼痛与不适头疼、肚子疼、难受、情绪表达谢谢、想见家人、别担心、医疗请求叫护士、吃药。患者需要的不是一个能上网、能跑App的平板而是“看一眼、按一下、能出声”的确定性工具。辅助沟通设备的核心要求其实是四个直观患者不用学就会用可靠每一次按键都有反馈便携床头柜和轮椅小桌板放得下可维护护工和家属能换短语、能充电。这四个要求直接排除了功能复杂的桌面设备。1.2 为什么不是树莓派4、平板或Arduino我在最初选型时列了一张表逐个比过方案优点在病床场景的麻烦树莓派4性能强、屏幕大、能跑语音识别开机慢、功耗大、怕系统更新后菜单变了患者误触容易退出界面安卓平板通用App多电容屏对手指不灵活的人太灵敏误触率高界面容易被弹窗打断Arduino简单稳定、上电秒开flash太小存不了几条录音音频和菜单做起来繁琐树莓派Pico RP2040上电秒开、microPython开发快、有大容量flash、硬件I2S需要自己接OLED、功放和喇叭动手量稍大我的结论是沟通辅助这块系统越简单越好。树莓派4和平板会带来很多“不必要的能力”这些能力在患者手抖或误触时就变成了风险。Pico这种单片机没有操作系统、没有弹窗、没有待机唤醒按一下就做一件事反而最合适。Pico还有一个加分项它的RP2040无论是用MicroPython还是C SDK社区里驱动OLED 0.96的方案都非常成熟事实上“raspberry pi 2040加oled 0.96”本来就是很多小型交互项目的第一步素材和经验都好找。1.3 硬件清单从屏到喇叭都按“少而稳”选最终确定的硬件清单如下零件型号/规格数量选型理由主控Raspberry Pi Pico 开发板RP20401成本低、GPIO多、有硬件I2S显示屏0.96寸OLEDSSD1306I2C接口128x641功耗低、对比度高、点阵驱动成熟按钮大尺寸轻触开关带键帽直径12mm以上4~6患者手指动作精度差需要大面积触发音频功放MAX98357A模块I2S输入单声道1三根信号线接好就能出声自带DAC和功放扬声器3W、8Ω、直径40mm内磁喇叭1体积小装进外壳还有余量电源5V 2A USB电源或10000mAh充电宝1充电宝便宜内置过压保护电容470uF电解电容 0.1uF瓷片电容各2解决播放时电源跌落导致重启选OLED 0.96而不是LCD1602是因为OLED没有背光整体功耗低黑色底加白色字在病房昏暗环境下反而更清楚。但它的缺点是分辨率只有128x64一行塞不了太多字所以菜单不能做深两级最合适。这个限制反而是好事逼着我把沟通动作控制在三次按键以内。2. OLED、按钮和音频放大三件套的接线细节引脚、电平和电源环境2.1 引脚分配速查表一套成熟的接线方案应该一开始就定死引脚避免后面改线。我的分配如下功能Pico GPIO引脚外设端备注OLED SDAGPIO0OLED SDAI2C0数据线需要上拉OLED SCLGPIO1OLED SCLI2C0时钟线上一条GPIO2按钮到GND不用外部上拉内部上拉即可下一条GPIO3按钮到GND内部上拉确认/播放GPIO4按钮到GND内部上拉返回/长按返回GPIO5按钮到GND内部上拉I2S BCLKGPIO18MAX98357A BCLK数字音频位时钟I2S LRCGPIO19MAX98357A LRC左右声道时钟I2S DINGPIO20MAX98357A DIN数字音频数据MAX98357A电源5V / GNDVIN / GND用5V供电避免用3.3V注意I2S的这三个引脚一定不能接错顺序接反了不会烧但会完全无声。我调试时踩过这个坑BCLK和LRC接反后喇叭里只有电流底噪检查了半天才发现是杜邦线插错位。2.2 按钮接法为什么用“按下接地加软件去抖”Pico的GPIO可以开内部上拉所以按钮的一端接到GPIO另一端接GND代码里把引脚配置为Pin.IN, Pin.PULL_UP。平时引脚被上拉到高电平按下后变成低电平程序读到低就是触发。这个接法的好处是节省板子空间不用每个按键都加外部10k电阻。但要注意两点手边如果用的是长杜邦线线缆容易感应噪声那种情况下我会给每路再加上10k上拉电阻到3.3V让电平更稳定第二是Pico的部分GPIO在启动时不能直接接5V所以按钮这一侧只能接GND不要接3.3V以外的高电平。机械开关都有抖动问题按下去的一瞬间会有几毫秒的开闭震荡不做处理会出现一次按键触发两三次。最省事的软件去抖是from machine import Pin import time def debounce(pin): if pin.value() 0: time.sleep_ms(20) if pin.value() 0: return True return False这个函数在第一次检测到低电平后等20ms再去读一次如果还是低电平就确认是有效按键。20ms这个值对机械开关足够又不会让人觉得迟钝。2.3 OLED 0.96的I2C接线和上拉注意市面上0.96寸OLED大多数是四脚模块GND、VCC、SCL、SDA。VCC供电看模块说明大多数支持3.3V到5V但保险起见我都用3.3V。SCL接GPIO1SDA接GPIO0I2C地址一般是0x3C如果屏幕没反应可以试0x3D。有些模块板载了上拉电阻有些没有。I2C总线上拉电阻范围是2.2k到10k模块没有自带的话我们在SCL和SDA上各接一个4.7k电阻到3.3V。Pico自己也可以启用引脚上拉但软件上拉阻值偏大在长线通信时不够稳外接电阻更靠谱。点亮SSD1306的MicroPython代码很简单from machine import Pin, I2C import ssd1306 i2c I2C(0, sclPin(1), sdaPin(0)) oled ssd1306.SSD1306_I2C(128, 64, i2c) oled.text(Hello, 0, 0) oled.show()跑通这一步等于把这块小屏作为项目的“脸”先立起来了。2.4 选择I2S功放而不是PWM直推喇叭Pico可以仅用GPIO的PWM模拟音频输出接一个低通滤波和喇叭也能响但这种方式有两个麻烦PWM音频对CPU实时性要求高菜单刷新和文件读取稍微慢一点声音就会嘶哑而且输出的功率很小推到3W喇叭基本只能自己听见。所以我在项目里直接用MAX98357A它是专门为I2S数字音频设计的单声道D类功放支持3.2V到5.5V供电3W输出芯片内部自带DAC和解码把Pico的I2S信号直接变成能驱动喇叭的模拟输出。MAX98357A模块上有一个重要的GAIN引脚它决定功放增益GAIN引脚连接增益GAIN接VIN3dBGAIN悬空15dBGAIN接GND9dB我第一次直接把GAIN悬空结果音量最大声音一响电池供电的Pico立刻重启。后来改成GAIN接VIN也就是3dB音量调到中等整个系统才稳定下来。经验是辅助沟通设备不需要震天响3dB增益加上外壳共鸣腔完全够病房里听清宁可留音量余量也不要一开始就拉满。接线时一定要把MAX98357A的电源接到5V而不是Pico的3.3V因为播放瞬间电流会突然增加3.3V稳压扛不住。同时在VIN和GND之间并上470uF电解电容和0.1uF瓷片电容这个组合能吸收大部分瞬态跌落。2.5 供电规划充电宝比电池更适合原型原型阶段最省心的供电方式是直接插一个5V 2A的充电宝而不是自己做锂电池。Pico的VBUS脚接5VMAX98357A的VIN也接5VGND共地。充电宝自带电量显示和保护板电压输出足够稳定唯一要注意的是USB线要尽量短线材电阻太大在播放瞬间会造成压降。如果后面要把设备做成便携版就需要一个3.7V锂电池加5V升压模块再加TP4056充电板。但这里有个坑播放瞬间电流可能到400mA甚至更高升压模块的动态响应如果不好电压会瞬间跌到4V以下Pico虽然是3.3V逻辑也会跟着复位。所以在便携版里音频功放和大电容要尽量靠近升压模块输出端地线用粗线单独拉不能和OLED这些低速器件共一根细地线。3. 两级菜单的MicroPython实现从JSON短语表到按钮状态机3.1 用MicroPython的原因Pico官方支持C SDK性能上更好但我的项目要频繁调整短语录音文件也要重新生成C编译一次来回至少十分钟。MicroPython的好处是直接改文件就能跑短语列表可以放在JSON文件里连程序都不用动只改数据。对于一台辅助沟通设备来说这种“数据与逻辑分离”的设计非常实用。最终固件用MicroPython主循环里做三件事处理按钮、刷新OLED、按下确认键时用I2S播放WAV文件。菜单交互响应要求不高按钮轮询频率50Hz足够。3.2 短语表组织JSON设计短语表我放在phrases.json里结构是二级目录一级分类二级具体短语。{ categories: [ { label: 吃饭喝水, items: [我想喝水, 我想吃点东西, 我还不饿, 今天吃什么] }, { label: 身体不适, items: [我不舒服, 我头疼, 我肚子疼, 帮我把枕头垫高] }, { label: 情绪交流, items: [谢谢你, 我想见家人, 别担心, 我有点着急] }, { label: 医护人员, items: [请叫护士, 我需要吃药, 我要量体温, 请帮我翻身] } ] }分类控制在4个每个分类下4到6条短语这样从开机到发声最少两次按键最多三次。OLED只有128x64一行显示16x16的汉字最多8个但为了看清我会把字号放大到实际能用的程度一行最多显示四五个字所以短语务必短。3.3 菜单状态机和按键处理代码程序状态我定义成三个STATE_CATEGORY在一级菜单、STATE_ITEM在二级菜单、STATE_PLAYING正在播放语音。用两个变量cat_idx和item_idx记录当前位置。import json, time from machine import Pin, I2C import ssd1306 i2c I2C(0, sclPin(1), sdaPin(0)) oled ssd1306.SSD1306_I2C(128, 64, i2c) BTN_UP Pin(2, Pin.IN, Pin.PULL_UP) BTN_DOWN Pin(3, Pin.IN, Pin.PULL_UP) BTN_CONFIRM Pin(4, Pin.IN, Pin.PULL_UP) BTN_BACK Pin(5, Pin.IN, Pin.PULL_UP) STATE_CATEGORY 0 STATE_ITEM 1 STATE_PLAYING 2 state STATE_CATEGORY cat_idx 0 item_idx 0 def debounce(pin): if pin.value() 0: time.sleep_ms(20) if pin.value() 0: return True return False def draw_menu(categories, state, cat_idx, item_idx): oled.fill(0) if state STATE_CATEGORY: start cat_idx for i, cat in enumerate(categories): y i * 16 if i cat_idx: oled.text(, 0, y) oled.text(cat[label], 12, y) elif state STATE_ITEM: items categories[cat_idx][items] for i, item in enumerate(items): y i * 16 if i item_idx: oled.text(, 0, y) oled.text(item, 12, y) oled.show() with open(phrases.json, r) as f: phrases json.load(f) while True: draw_menu(phrases[categories], state, cat_idx, item_idx) if state STATE_CATEGORY: if debounce(BTN_UP): cat_idx max(0, cat_idx - 1) if debounce(BTN_DOWN): cat_idx min(len(phrases[categories]) - 1, cat_idx 1) if debounce(BTN_CONFIRM): state STATE_ITEM item_idx 0 elif state STATE_ITEM: if debounce(BTN_UP): item_idx max(0, item_idx - 1) if debounce(BTN_DOWN): items phrases[categories][cat_idx][items] item_idx min(len(items) - 1, item_idx 1) if debounce(BTN_CONFIRM): state STATE_PLAYING if debounce(BTN_BACK): state STATE_CATEGORY elif state STATE_PLAYING: # 播放完成后回到二级菜单 state STATE_ITEM这段逻辑很直白一级菜单用上下选择分类确认后进入二级二级再选具体短语确认后播放。播放完自动回到二级菜单方便患者连续说多句话。注意debounce函数处理的是低电平有效的按键。3.4 长按返回和播放中锁键在中风患者里很多人手抖或者无法精准控制手指需要额外的防误触设计。我在STATE_PLAYING时只处理“长按返回”这一个操作其他按钮全部忽略防止患者无意间连按一次发出好几句话。“长按返回”是一个安全出口无论患者在哪一层菜单只要按住返回键超过1秒就回到一级菜单首页。这个设计比做一个单独的“取消”按钮更合适因为取消按钮可能被误触而长按需要人主动持续压住误触概率小很多。def wait_short_or_long(pin): t0 time.ticks_ms() while pin.value() 0: if time.ticks_diff(time.ticks_ms(), t0) 1000: return long time.sleep_ms(10) return short在主循环里如果是长按就把state重置为STATE_CATEGORY两个索引都清零。3.5 预录真人声和WAV播放沟通辅助设备的语音我坚持用真人预录而不是文字转语音。原因很简单病房里响起一句陌生的机器合成声患者和家人都会有距离感如果是患者老伴录的“我想喝水”听起来就像家里人在说话情绪的安抚作用远比清晰度重要。录音用手机或电脑麦克风先录成48kHz 16bit单声道再用Audacity压成22050Hz 16bit单声道WAV。采样率降到22050是因为Pico的处理器和I2S功放都够用而且文件体积小一半。一段两秒的WAV大约88KBPico的2MB flash去掉固件和字库图片放15句常用短语完全够。播放前要处理WAV文件头部。简单做法是直接跳过前44字节from machine import I2S, Pin import struct audio_out I2S(0, sckPin(18), wsPin(19), sdPin(20), modeI2S.TX, bits16, formatI2S.MONO, rate22050, ibuf8192) def play_wav(filename, volume0.7): with open(filename, rb) as f: f.seek(44) while True: chunk f.read(4096) if not chunk: break chunk chunk[:len(chunk) - (len(chunk) % 2)] if chunk: samples bytearray(chunk) for i in range(0, len(samples), 2): raw struct.unpack_from(h, samples, i)[0] raw int(raw * volume) raw max(-32768, min(32767, raw)) struct.pack_into(h, samples, i, raw) audio_out.write(samples)这里用struct模块把每两个字节当成一个16bit有符号数乘以音量后写回缓冲再整块交给I2S。音量控制放在播放端而不是录音端是为了以后能用按键实时调整。实际使用中0.75左右的音量在病房最合适既能唤醒注意力又不会把隔床病人吓着。3.6 中文显示避坑别用MicroPython内置字体硬刚中文SSD1306库里自带的oled.text()只支持ASCII字符中文直接传入会显示乱码或空白。这是很多新手第一次做中文菜单卡住的地方。绕开办法有三个一是用拼音或英文标签但面向老年人不够友好二是在电脑上把每一条菜单预渲染成128x64的1bit位图存成字节数组运行时用oled.blit()整块显示三是烧录一个GB2312点阵字库用文件偏移方式访问工作量比较大。我最终选的是位图方案。在电脑上用Python的Pillow库生成from PIL import Image, ImageDraw, ImageFont font ImageFont.truetype(simhei.ttf, 16) img Image.new(1, (128, 64), 0) draw ImageDraw.Draw(img) draw.text((12, 24), 我想喝水, fontfont, fill1) img.save(want_water.bin)生成后的want_water.bin体积只有1KB几行字也就几KB塞进Pico的flash没有任何压力。运行时with open(want_water.bin, rb) as f: oled.blit(f.read(), 0, 0) oled.show()这样做的好处是“所见即所得”我在电脑上想用什么字体、什么排版就在电脑上排好设备端不做任何文字渲染。3.7 死机保护看门狗和异常兜底辅助沟通设备最怕“无声死机”患者按了没反应家属会以为设备坏了。MicroPython虽然稳定但我在程序里加了看门狗要求主循环必须定期喂狗try: from machine import WDT wdt WDT(timeout8000) while True: # 主循环 wdt.feed() except Exception: import machine machine.reset()WDT超时设为8秒即使主循环卡死设备也会自动重启。另外用try/except接住所有异常出错后直接调用machine.reset()复位保证最多8秒内恢复可用。这功能平时没有存在感但在长期使用中非常重要。4. 实测阶段最容易翻车的三件事播放重启、中文乱码和误触4.1 播放瞬间整个设备重启我第一版原型装好后遇到一个非常头疼的问题按下确认键喇叭刚响一下OLED屏幕闪白整机重启。用万用表测MAX98357A在播放瞬间电流从30mA跳到300mA以上如果供电线又细又长Pico VBUS端的电压会被拉低到复位阈值以下。排查路径大概这样先用示波器或万用表测Pico的VBUS电压播放瞬间如果有明显凹陷就说明电源不够硬。接下来做三件事解决给MAX98357A的VIN和GND之间并上470uF电解电容位置尽量靠近功放引脚把GAIN引脚从悬空改成接VIN把增益从15dB降到3dB播放电流峰值立刻小了换一条粗短USB线充电宝到设备之间的线材电阻不能忽视。改完之后播放电流峰值控制在200mA以内连续循环播放十句也没有再重启过。这个教训说明软件写得再稳电源设计有问题一切都白搭。4.2 声音播放有咔哒断续声第一次加入OLED刷新后播放WAV时“咔哒咔哒”的断续声特别明显。原因有两个一是I2S的缓冲区太小只有4096字节文件读取稍微卡顿就断流二是我在播放过程中还在同时调用oled.show()刷新菜单I2C虽然只占几毫秒但刚好打断了连续写入I2S的节奏。解决办法是把ibuf从4096提高到8192播放语音前先停掉LED和OLED的无效刷新保留最后一张菜单画面不重绘等播放完成后再恢复刷新。这个改法很简单但声音明显连续了很多。凡是遇到I2S有杂音优先查“谁在抢总线时间”。4.3 OLED中文乱码和显示不全接好OLED第一次运行就把我整懵了菜单上显示的“吃饭喝水”变成了每一位象。后来查明白SSD1306自带字库只覆盖ASCII中文字节被拆开自然全是乱码。我没有在设备端做字库而是提前把每条菜单渲染成128x64的位图存到flash里。这个方案要付出一点劳动每条短语生成一个bin文件并在JSON里加上对应的文件名。菜单表就变成{ label: 吃饭喝水, image: menu_eat.bin, items: [ {text: 我想喝水, sound: want_water.wav, image: item_water.bin}, {text: 我想吃点东西, sound: want_food.wav, image: item_food.bin} ] }菜单显示时先根据下标读取对应的bin文件用oled.blit()显示确认键触发时播放对应的WAV文件。虽然多一步预处理但中文显示效果非常干净字体和排版可以自由控制。4.4 手抖患者的误触“误触”这件事在我做真实样机以前完全没概念。面包板阶段我用普通轻触开关测试的是健康人手指按键精准、反应快。但当我拿给一位手部控制不太好的试用者时手指搭在按键上轻微抖动机器就一连串说了一堆话场面一度很尴尬。最终方案是给所有按钮加“触发持续时间”判断按下后必须持续0.4秒才确认触发而不是检测到低电平就立刻响应。0.4秒对正常人来说几乎感知不到延迟但能过滤掉大部分抖动和扫过按钮的误触。敏感短语后面再加一层确认患者按完确认键屏幕显示“确定要播放这句话吗”再按一次确认才出声。对于手抖得特别厉害的人还有更极端方案把“敏感项”放到一个需要长按才能进入的隐藏菜单里平时主菜单只有最基本的喝水、进食、叫护士这些需求。这项改动是临床试验反馈后的最优解强烈建议大家在做类似辅助项目时提前和真实使用者聊一聊。5. 从原型到能长期使用的设备外壳、紧急呼叫与数据记录5.1 外壳设计让按键放得稳让声音传得出来面包板版本没法放到病床边上芯片裸露、按钮歪斜家属看了也不敢用。后期我用3D打印做了一个小盒子尺寸定成120mm长、80mm宽、30mm高内部刚好放Pico、MAX98357A和40mm喇叭。盒子的设计要点有三个面板向上倾斜30度OLED嵌在斜面上患者躺着也能看到四个按钮用大号圆形按钮帽间距至少20mm防止胖手指一次碰到两个喇叭开口开在前面板下方而不是底面否则放在床单上声音会被捂住。如果没有3D打印机可以用木盒或亚克力粘一个。但要注意按键背面的固定柱轻触开关的引脚要焊接牢固长期按压下松脱是很常见的故障。5.2 短语列表必须让康复师参与我个人做技术出身一开始列菜单时想的都是“我饿了”“我疼”这种抽象描述结果康复师一句点醒了我患者说“我不舒服”家属根本不知道是哪里不舒服。后来我把短语表改了按康复思维分成更具体的项一级类别典型短语使用场景基本需求我想喝水、我想上厕所、我冷、我热最高频放首位疼痛不适我头疼、我肚子疼、我肩膀疼、帮我把枕头垫高要描述部位最好配图情绪社交谢谢你、我想见家人、别担心、我有点着急缓解沟通挫败感医疗请求请叫护士、我需要吃药、我要量体温、请帮我翻身紧急情况下能快速触发身体部位描述对失语症患者特别重要因为我发现很多人能听懂“哪里疼”但说不清“哪个位置”。如果OLED空间允许应该把疼痛类短语做成“身体部位图加对应按钮”不过这就属于第二版迭代了。5.3 紧急呼叫按钮和自动记录长期使用后我发现最常用的其实不是“我想喝水”这种日常短语而是一句“请叫护士”。所以我专门在机器侧面加了一个红色大按钮和普通菜单完全独立红色按钮通过单独GPIO接到一个独立中断处理函数里。无论设备在哪个状态只要红色按钮被按下并保持1秒就立即播放急促的“我需要帮助请叫护士”同时驱动一个小LED闪烁。我还在程序里到所有播放行为都记时间戳简单存储在CSV文件里def log_play(tag): with open(log.csv, a) as f: f.write({},{}\n.format(time.localtime(), tag))这条记录对康复师评估患者的表达意愿很有参考价值。比如全天只按过三次说明患者沟通需求不多或者按钮位置不合适护理人员可以依据这个调整设备摆放和菜单内容。5.4 长期使用的三个维护细节长期放在病房的设备会面临一些原型阶段想不到的维护问题。经验总结下来有三条开机音量固定为中等档位不要把上次调节的音量存为默认否则有人试过音量调最大后下一位患者被吓一跳电源采用USB适配器固定供电时要加一个防脱落的固定夹病人翻身时扯掉电源设备失灵会非常糟糕语音文件重新录制后所有WAV统一过一遍响度标准化不要一条响亮一条微弱切换播放时体验会差很多。这三个细节看着小但在护理现场都会变成“这个设备不好用”的直接原因。做辅助设备最怕的就是“能用”和“好用”之间的缝隙。如果让我重做一遍这个项目我会先把大按钮买回来用硬纸板做一个比真实尺寸略大的模型推到目标使用者的床头试摆两天。沟通辅助的瓶颈从来不是树莓派解码语音的速度也不是OLED显示汉字的方案而是按钮大小、颜色、位置、按压反馈这些最土的事情。技术上的路我已经踩平了Pico驱动OLED 0.96很成熟I2S功放接好电源就稳定MicroPython的菜单状态机几个小时就能写完。真正让设备从原型变成“有人愿意用”的是尽可能早地让患者、家属、护工参与进来。一句由家人提前录好的“我想喝水”比任何高端语音合成都更接近“沟通”的本意。