去年秋天,朋友的父亲脑梗出院后,人醒过来了,话却说不出来。医生说这叫运动性失语,听力和理解力大多还在,只是嘴和脑子的连线断了。那段时间,家里全靠按铃呼叫护士,但护士不可能时刻盯着,家人又听不懂那些含糊的“啊啊”到底是想喝水、想翻身,还是肚子疼。我帮他们做了一个“Stroke Communication Aid”的树莓派语音沟通板——准确说,是一台负责发声的树莓派加上一个负责按键输入的Pico面板,患者只要按下大按钮,就能把“我想喝水”“请帮我翻身”“我腿疼”这些话通过扬声器清楚地说出去。这个项目非常适合家有失语长辈的护理者、想用低成本硬件解决实际问题的创客朋友参考,核心就三件事:按键、菜单、播报。
1. 为什么做“树莓派语音沟通板”:一次真实的护理困境
1.1 失语不等于失能,需求一直都在
脑卒中后失语的患者,最痛苦的不是肢体不便,而是“心里有,嘴上说不出”。口渴、疼痛、想上厕所、被子太薄、药该吃了——这些需求每时每刻都在产生,但家人必须靠猜。我朋友最初买过商业的病床呼叫器,只有一两个按键,支持录制固定语音,功能倒是没问题,价格却让普通家庭犹豫:稍微做得像样一点的医用餐桌沟通器要上千元,而且多数按键少、无法扩展,后期想换语音还得联系厂家。
这个需求本质上是:把高频的、可枚举的日常短语固化成按钮,让患者用最少的手指动作完成表达。注意,这里不是做复杂的语义理解,而是做一个“词汇表面板”。临床上很多失语患者对简单文字、数字、颜色仍然有辨识能力,所以我们做的是“结构化选择”,不是AI对话。这个定位决定了整个项目的复杂度上限——它不需要自然语言处理,不需要摄像头,不需要云端,一台便宜的树莓派加一个单片机面板就足够了。
1.2 为什么是树莓派而不是买成品
我当时的思考逻辑很简单:成品沟通器贵、封闭、不好改,而树莓派生态成熟,GPIO、音频、WiFi、Python一把抓。选树莓派而不是普通单片机的主要原因有三点:
- 音频播放能力强。树莓派接上I2S功放模块,能播放清晰的WAV/MP3,音量可调,树莓派Zero 2 W的价格也才一百出头。
- 文件管理方便。语音素材存在SD卡里,家人随时可以通过WiFi或U盘增删,不用重新烧录芯片。
- 系统容错好。哪怕程序崩了,SSH进去重启服务就行;单片机固件一旦跑飞,患者家属可不会重新烧录。
我还对比过“树莓派Zero 2 W + Pico单片机”和“单块树莓派直连按钮”两种路线,后面详细说。如果你家里已经有树莓派4B或树莓派5,也可以直接按本文思路复刻,只是体积大一点、功耗高一点而已。
2. 双板分工:树莓派管声音,RP2040管交互
2.1 一开始我也打算一块树莓派干完所有事……
最开始我图省事,想用树莓派Zero 2 W的GPIO直接接按钮和OLED屏,Python里同时轮询按键、刷新屏幕、调aplay播放语音。原型跑通后发现问题很明显:树莓派是跑Linux的重负载设备,启动慢(十几秒到几十秒),系统更新、WiFi闪断、Python进程卡死都会让按钮失灵;而且GPIO轮询在Linux下延迟不稳定,按键按下去偶尔要等一两百毫秒才响应,老人会以为没按上,再按一下,结果播了两遍。
更关键的是,树莓派Zero 2 W没有板载3.5mm耳机孔,音频得走USB声卡或I2S功放,这倒是其次;真正的问题是它不适合做“实时的人机交互面板”。所以后来我把输入面板拆出去,用了一块树莓派Pico(RP2040芯片)专门负责按钮扫描和OLED显示,树莓派只负责一件事:收到命令后播放对应的语音文件。Pico的GPIO是裸金属级别的,按键响应可以做到毫秒级,而且它不吃Linux那一整套系统,断电重启也是秒开。两块板子各干各的活,稳定得多。
2.2 硬件选型与成本清单
以下是实际下单的硬件清单,价格按2024年国内常见渠道参考,会有浮动:
| 硬件 | 型号/规格 | 参考价 | 作用 |
|---|---|---|---|
| 树莓派 | Raspberry Pi Zero 2 W | 约100元 | 主控,播放语音、WiFi管理 |
| 单片机 | Raspberry Pi Pico(RP2040) | 约15元 | 按键扫描、OLED显示、串口通信 |
| OLED屏 | 0.96寸 I2C SSD1306 128x64 | 约15元 | 显示当前菜单页和选中项 |
| I2S功放 | MAX98357A模块 | 约12元 | 树莓派音频输出到扬声器 |
| 扬声器 | 3W 8Ω小喇叭,直径4cm | 约8元 | 发声 |
| 大按钮 | 彩色带帽按钮 12mm或更大 | 约10元 | 患者按压,防误触 |
| 锂电池 | 支持2A输出的移动电源/充电宝 | 约40元 | 整体供电 |
| 外壳 | 亚克力板或3D打印盒 | 约20元 | 固定面板,绝缘 |
| 杂项 | 杜邦线、热熔胶、纸质标签 | 约10元 | 连接与标识 |
总成本控制在220元左右,比商业成品低一个数量级,而且所有零件坏了都能单独换。
2.3 连接关系与通信协议
整个系统的连接非常清晰:
- 树莓派Zero 2 W的USB口给树莓派供电,电池用移动电源;
- 树莓派的5V和GND引脚给Pico供电(也可以Pico用独立USB口,但没必要);
- Pico接OLED屏和几个大按钮;
- Pico的GPIO UART接到树莓派的GPIO串口(UART),用于发送“播放编号”;
- 树莓派的I2S引脚接MAX98357A功放,功放接扬声器。
通信协议我设计得很简单,每行一条命令,格式就是:
PLAY|编号比如Pico检测到用户按下“喝水”按钮,就通过串口发一行PLAY|12,树莓派收到后查表,播放12_want_water.wav这个文件。为什么用串口文本而不是直接在GPIO上拉电平?因为按钮不止一个,未来要扩展短语库,文本协议比一堆引脚编码好维护得多,而且串口线就两根,不容易接错。
3. 端到端跑通:从按键到扬声器出声
3.1 树莓派端系统与依赖
我用的是Raspberry Pi OS Lite(不带桌面版),理由很简单:这个项目不需要图形界面,桌面环境会吃掉内存和CPU,还有一堆后台更新,没必要。刷写系统时在rpi-imager里直接开启SSH并预配好WiFi,这样第一次通电就能远程登录。
系统装好后,需要安装两个关键依赖:
sudo apt update sudo apt install -y python3-serial alsa-utilspython3-serial用来读取Pico发来的串口数据;alsa-utils提供aplay命令,负责播放WAV文件。我没用pygame或mpv,因为aplay是最轻量的播放器,一条命令就能播完退出,而且退出后立刻能响应下一条指令,不会出现Python非阻塞播放导致队列积压的问题。
音频输出配置我放在了/boot/firmware/config.txt(老系统是/boot/config.txt),加上一行:
dtoverlay=googlevoicehat-codec这是让树莓派把I2S引脚映射给MAX98357A功放。不加这一行,功放不会出声。改完重启,用aplay -l能看到声卡设备。然后调节音量:
sudo amixer set 'DAC' 80%这里有个坑:MAX98357A默认的DAC可能被静音,导致怎么播放都没有声音。我第一次调通时卡在“功放通电了但完全不响”,后来发现是音量被设为0%。你如果复刻,务必在配置后跑一句speaker-test -t wav -c 1验证。
3.2 RP2040面板:按键扫描与OLED显示
Pico端我用MicroPython写固件,简单、易改。按键布局上,我做的是“一键直达”方案,比多级菜单更适合患者:一块面板上排列5个大按钮,其中4个是常用短语(喝水、翻身、热了、盖被子),一个红色的是紧急呼叫“请马上到我身边来”。另外加一个翻页按钮,按一下切换第二组4个短语。
OLED屏显示当前是第几组、每个按钮对应的编号。这里我必须诚实说明:0.96寸OLED默认MicroPython的ssd1306库不支持中文显示,显示中文需要额外烧录字形数据,成本很高。我的第一版方案是“OLED显示页码和编号 + 按钮旁边贴打印好的中文标签”,实际使用证明够用了。如果你想把中文直接显示在OLED上,那属于V2的改进项,后面我会提到。
Pico端的核心代码骨架如下:
from machine import Pin, UART, I2C import ssd1306, time # OLED 0.96寸,I2C0:SDA=GP8,SCL=GP9 i2c = I2C(0, scl=Pin(9), sda=Pin(8), freq=400000) oled = ssd1306.SSD1306_I2C(128, 64, i2c) # UART0:TX=GP0,RX=GP1,接树莓派GPIO14/GPIO15 uart = UART(0, baudrate=9600, tx=Pin(0), rx=Pin(1)) # 按键:使用内部下拉,按下去为高电平 btn_page = Pin(16, Pin.IN, Pin.PULL_DOWN) # 翻页 btn_call = Pin(17, Pin.IN, Pin.PULL_DOWN) # 紧急呼叫 btn_say = Pin(18, Pin.IN, Pin.PULL_DOWN) # 当前选中项确认播放 # 页面内容示例:每页4个短语ID pages = [ [1, 2, 3, 4], # 第一页:1喝水 2翻身 3热了 4盖被子 [5, 6, 7, 8] # 第二页:5想上厕所 6肚子疼 7累了 8坐起来 ] current_page = 0 #############################看后续补充#############################上面有一段被我故意截断了,因为实际按键扫描、OLED刷新、防抖逻辑加在一起会占不少篇幅。这里说几个关键点:按键扫描必须带防抖,否则Pico这种高速轮询会导致一次按压被识别成多次,嘭嘭嘭连着播好几遍;我的做法是判断电平变化后延迟20毫秒再读一次,确认稳定才生效。紧急呼叫按钮则单独处理,它不参与翻页,无论在哪个页面按下,都立即发送PLAY|99。
Pico与树莓派之间的串口通信,波特率选9600就够,因为命令只有一行“PLAY|编号”,9600波特每秒能传约960字节,一帧命令才十几字节,完全没有压力。波特率太高反而容易受杜邦线干扰。
3.3 串口命令转播放:树莓派服务脚本
树莓派端的服务脚本我用Python写,结构大概是:
import serial, subprocess, time, os SOUND_DIR = "/home/pi/stroke_voice/audio" SOUND_MAP = { 1: "01_want_water.wav", 2: "02_turn_over.wav", 3: "03_too_hot.wav", 4: "04_cover_blanket.wav", 99: "99_help.wav", } ser = serial.Serial("/dev/serial0", 9600, timeout=0.5) ser.reset_input_buffer() def play(index): filename = SOUND_MAP.get(int(index)) if not filename: return path = os.path.join(SOUND_DIR, filename) if os.path.exists(path): subprocess.run(["aplay", "-q", path]) while True: line = ser.readline() if line: try: cmd = line.decode().strip() if cmd.startswith("PLAY|"): play(cmd.split("|")[1]) except Exception as e: print("error:", e)几个细节值得解释。subprocess.run是阻塞式的,播完这一条才继续读下一行,好处是避免两句语音叠在一起,坏处是如果老人连续按键,后面的指令会排队。所以我在Pico端做了限制:每次播放开始后1.2秒内忽略普通按键,防止误触和连按。紧急呼叫按钮不受这个限制,这很重要——紧急需求应该永远可以打断普通播报。
树莓派的串口默认可能被系统登录控制台占用,需要编辑/boot/cmdline.txt,把其中console=serial0,115200这段删掉,否则Pico发来的数据会被内核当成登录输入吃掉。删掉后重启,/dev/serial0就干净了。
3.4 开机自启与掉电恢复
这设备是给患者用的,不可能指望他们每次开机去终端敲命令。我把它做成systemd服务,开机自动运行,程序崩溃了还会自动重启:
[Unit] Description=Stroke Voice Aid Server After=multi-user.target [Service] User=pi ExecStart=/usr/bin/python3 /home/pi/stroke_voice/server.py Restart=always RestartSec=5 [Install] WantedBy=multi-user.target把文件放到/etc/systemd/system/stroke-vaid.service,然后:
sudo systemctl enable --now stroke-vaid还有一个权限坑:如果脚本读不到/dev/serial0,提示Permission denied,多半是用户不在dialout组。执行sudo usermod -aG dialout pi后重新登录即可。
4. 语音素材:用TTS还是录家人的声音
4.1 两种路线对比
语音文件是这个项目的灵魂。同样是“我想喝水”,用电子合成音说出来,和用家人自己的声音说出来,对患者是完全不同的感受。我把两条路线都试过:
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| TTS合成(espeak-ng/piper等) | 快速批量生成,改文案方便 | 机械感强,语句平淡,老人反感 | 快速原型验证、大量短句 |
| 录制家人声音 | 亲切、有情感、辨识度高 | 录制耗时,改一次文案要重录 | 长期给同一患者使用 |
我个人强烈推荐第二条路线。脑卒中失语患者往往对熟悉的声音更敏感,家人录的“爸,我在呢,别着急”和机器人说的“别着急”完全是两回事。朋友后来录完素材给他父亲试听时,老人眼睛明显亮了——这个细节不是技术参数能替代的。
4.2 实用录音规范
如果走真人录音路线,请遵守一套简单的录音规范,后期会省很多事:
- 用手机录音即可,但要在安静房间、同一位置、同一音量下录完所有句子;
- 句子尽量短,一句话控制在6~10个字,比如“我想喝水”而不是“爸你现在想不想要喝一点水”;
- 语速比平时慢20%,每个字咬清楚,方便机器播放时听清;
- 每句话之间停顿2秒,方便后期切分;
- 用Audacity做降噪和归一化,把每段音频振幅统一到-3dB左右,避免“翻身”声音小得像蚊子叫,“肚子疼”又轰得人一激灵。
导出格式我建议用WAV,采样率22050Hz、16bit、单声道。这个规格比CD音质低,但人声清晰度足够,文件大小只有44.1kHz的一半,一分钟约2.6MB,100条短语也就260MB,SD卡完全装得下。有人会说MP3更省空间,但aplay对WAV支持最直接,不用额外解码,启动播放几乎没有延迟,这在交互体验上很重要。
4.3 生成文件的命名与映射
文件命名我采用“两位序号_语义.wav”的格式,并把编号固化在Pico端和树莓派端两处:
01_want_water.wav 02_turn_over.wav 03_too_hot.wav ...为什么不用中文文件名?因为树莓派系统的locale有时候对UTF-8文件名不敏感,SSH传输也容易乱码,全用拼音或英文最稳。Pico端按钮对应的编号、OLED上显示的编号、树莓派端SOUND_MAP里的编号,三者必须一致。建议做一张Excel对照表:编号、按钮位置、OLED显示、音频文件名、具体语音内容,五列。没有这张表,后期改起来一定会漏。
5. 实测踩坑:三个差点劝退我的问题
5.1 供电:树莓派Zero 2 W的“隐形杀手”
第一版调试时,我在实验室用电脑USB口给树莓派供电,Pico也从树莓派取电,功放一播放声频就会随机重启。查了半天,发现是电流不够。树莓派Zero 2 W官方推荐供电能力是2.5A,MAX98357A播放瞬间的电流会突然拉高,USB口根本喂不饱。后来换成额定2A以上的移动电源,并且用粗短线连接,再也没出现过播放时重启。这里有个隐藏经验:不要用“标称2A但实际线材很细”的劣质充电线,线压降会让树莓派检测到欠压(红灯常亮、性能下降)。排查这类问题,可以在树莓派上跑vcgencmd get_throttled,如果返回0x1,就意味着曾发生过欠压。
5.2 OLED不亮或花屏
Pico连接0.96寸OLED,最常见的两个现象:完全不亮、显示雪花。完全不亮先查I2C地址,SSD1306常见地址是0x3C或0x3D,MicroPython里可以用一行代码确认:
print(i2c.scan())如果扫描得到地址,但屏幕还是白的,多半是VCC供电不稳或者SDA/SCL线太长。杜邦线超过20cm就容易出这个问题,解决办法是缩短排线、把电源线和信号线分开走,不要在OLED下方捋成一团。花屏则大概率是屏幕初始化前I2C总线被干扰,加一句time.sleep(0.1)等电源稳定后再初始化,一般能解决。
5.3 串口数据“发过去没反应”的排查链路
这是最磨人的坑。现象是Pico端明明发PLAY|12,树莓派服务端却毫无反应。我按这个顺序排查:
首先,确认/dev/serial0是否存在且未被控制台占用。执行:
ls -l /dev/serial0如果串口被登录控制台占用,Pico发来的数据会显示成一堆乱码。这时去/boot/cmdline.txt删掉console=serial0,115200,重启。
其次,确认Pico的TX/RX和树莓派交叉连接。UART是交叉的:Pico的TX(GP0)接树莓派的RX(GPIO15),Pico的RX(GP1)接树莓派的TX(GPIO14),必须共地。接错线收不到,还少共地也收不到。
最后做最小回归测试:在Pico端临时写个死循环uart.write("PLAY|12\n"),树莓派上cat /dev/serial0,能看到PLAY|12说明链路通了,再回头查自己的Python服务是不是没启动或读错了波特率。这套“最小链路验证法”在排查串口问题时百试百灵,不要一上来就改代码。
5.4 老人第一次用,按键设计其实比代码更重要
朋友把设备带回家,第一周他父亲基本不用。不是设备坏了,而是老人觉得“按错会麻烦别人”。这个反馈让我意识到,这套设备真正的难点不是技术,是交互设计。后来我们做了三处改进,效果立竿见影:
- 紧急按钮单独用红色大按钮,和其他按钮明显区分,且按下有咔哒声反馈;
- 每个按钮旁边贴带大字的打印标签,字高不低于8mm,黑体加粗;
- 刚开始只启用“喝水”“翻身”“热了”“紧急”四个按钮,等老人用顺手了,再把翻页功能和第二组短语加进去。
不要一上来就给人二十个短语,患者认知负荷会瞬间爆炸。从四个词开始,让设备变成习惯,再逐步扩展,这才是这类护理硬件真正该有的思路。
6. 如果再做一个V2,我会优先改这几件事
6.1 中文字库与提示图标
第一版的OLED只能显示页码和数字,按钮标签靠贴纸。如果做V2,我会在0.96寸OLED上显示中文。实现方式是在Pico端烧录一个简单的汉字点阵字库,比如16x16像素的常用汉字,再配合自绘的简单图标(水滴代表喝水、箭头代表翻身),屏幕虽小但信息密度会高很多。这需要额外写一版字库生成工具,但效果值得。
6.2 低电量和故障自检语音
树莓派可以通过vcgencmd get_throttled检测是否欠压,如果检测到欠压或者电量低,就播一句“电量不足,请帮我充一下电”。对长期卧床的患者来说,设备没电而自己说不出来,是一种被剥夺感。这个功能虽然只有几行代码,但实用性极高,属于“少了自己才知道多重要”的那种细节。
6.3 网页后台管理短语库
目前改短语需要拔SD卡或者SSH进去改文件,对家人还是太硬核。V2我打算在树莓派上跑一个小Web服务,家人手机连同一局域网就能打开页面,增删短语、上传录音、调整编号,实时生效。通信链路不变,只是把“更新音频映射”这件事从命令行变到浏览器。
6.4 独立后备播放通道
说到底,树莓派挂了,整个板就哑了。V2的终级形态是让Pico直接驱动一个DFPlayer Mini MP3模块,把最常用的几条紧急语音预存在独立TF卡里,树莓派只是扩充通道。树莓派正常时用树莓派的高音质播报;树莓派异常时,Pico仍然能独立播放“紧急、喝水、翻身”这三条保命级别的指令。对医疗护理场景来说,冗余比功能多更值钱。
这个项目做完,我最大的体会是:硬件选型其实没有那么多玄学,树莓派负责它擅长的生态和音频,Pico负责它擅长的实时交互,各司其职;而真正决定设备能不能被用起来的,永远是那些容易被忽略的细节——声音是不是家人的、按钮是不是够大、开机是不是稳、没电了会不会说话。如果你也要复刻这个东西,我的建议是:先剪二十句核心短语,先跑通按键到喇叭这条链路,再谈OLED菜单和花哨功能。先把“我渴了”这句播出来——比什么架构设计都重要。