Pico 2W网络收音机实战:I2S音频与FreeRTOS旋钮交互

发布时间:2026/9/17 9:39:55
Pico 2W网络收音机实战:I2S音频与FreeRTOS旋钮交互 1. 为什么我盯上了 Pico 2W 做网络收音机我玩树莓派有些年头了从最早的 3B 到后来的 4B、5家里抽屉里塞满了各种型号。但真正让我眼前一亮的是去年拿到Pico 2W的时候。这是一块带无线功能的微控制器开发板双核 ARM Cortex-M33 加上无线模块价格便宜到可以随便造功耗低到能用充电宝带一整天。当时我就想这玩意儿不做点小而美的东西可惜了。做智能网络收音机这个念头其实来自我家里那台老式 FM 收音机。那机器音质还行但频道就那么几个广告还特别多。我就琢磨现在网络电台资源这么丰富能不能用 Pico 2W 抓流媒体音频推给一个功放和喇叭做成一个复古外壳、现代内核的收音机它不需要屏幕、不需要操作系统、开机即用旋钮一转就换台。这才是收音机该有的样子。这篇文章适合谁看如果你手上有 Pico 2W想找个不太难但又有成就感的项目如果你做过 Pico 基础的点灯、按键实验想进阶到网络通信和音频处理如果你是电子爱好者喜欢把软件和硬件结合起来做点实用小物件——那这篇内容基本就是为你写的。我会把整个设计和踩坑过程摊开讲包括为什么选这些器件、参数怎么算、代码怎么写、出问题怎么查全部基于我实际跑通的方案。先说说这个智能到底智能在哪。市面上很多所谓智能音箱本质是把语音识别、云端处理那一套堆上去结果是延迟高、隐私堪忧、还得联网验证。我这台收音机的智能体现在三个方面一是自动连网获取电台列表我预先在固件里写好了几十个国内外公开的网络电台地址二是断线自动重连WiFi 掉线了它会自己找回来不用你管三是旋钮无极调节频道和音量用旋转编码器实现手感和老式收音机一模一样。没有花哨的语音助手就是纯粹地把听广播这件事做好。选 Pico 2W 而不是零号或者普通 Pico关键就在那个 W。它内置了无线模块官方 SDK 对网络连接的支持很完整省掉了我外挂 WiFi 模块的麻烦和额外的 GPIO 占用。另外 Pico 2W 的 PIO可编程 I/O非常灵活处理 I2S 音频这种对时序敏感的总线时特别省心——这个后面会详细说。至于为什么不直接用树莓派 4B 或 5原因也简单那玩意儿跑 Linux开机要几十秒功耗好几瓦还得配个像样的电源和散热做成收音机这个形态就太重了。Pico 2W 上电到出声大概两三秒功耗在播放时也就 0.5W 上下用五号电池盒都能撑很久。2. 整机方案设计从一个想法到一张能落地的框图2.1 核心链路怎么拆一台网络收音机本质上就是把网络上的音频数据变成喇叭里的声音。这条链路上有四个关键环节网络接入、音频流解码、数字音频输出、模拟功放。网络接入这块Pico 2W 的 CYW43 无线芯片负责连 WiFi拿到 IP 之后通过 HTTP 或者直接 socket 去拉音频流。这里有个坑很多网络电台是MP3 或 AAC 编码的流媒体不是原始 PCMPico 直接播不了必须解码。解码这部分我选择把解码工作交给一块专门的音频解码模块而不是让 Pico 自己算。原因很直接Cortex-M33 主频 150MHz跑个 MP3 软解码虽然勉强能行但会占掉大量 CPU导致网络接收和界面响应卡顿。用硬件解码芯片Pico 只负责把压缩数据搬过去CPU 占用率能降到 10% 以下。解码之后的数字音频通过I2S 总线送给 DAC再经过功放放大驱动喇叭。I2S 是三线制位时钟 BCK、左右声道时钟 LRCK、数据 SDATAPico 2W 的 PIO 可以精确产生这三路信号抖动很小音质有保障。功放我用了常见的PAM8403小功放板3W 双声道5V 供电推一个 4 欧 3W 的小喇叭绰绰有余音量在卧室里开一半就够响了。整个链路的信号流向可以这样理解WiFi 进来的是压缩包解码芯片负责拆包I2S 负责搬运拆好的数据DAC 负责翻译成模拟电压功放负责把电压放大到能推动喇叭喇叭负责震动空气。每一环都不能少每一环的选择都会影响最终听感。2.2 为什么不用 Pico 直接软解音频有人可能会问Pico 2W 双核 150MHz性能不算差为什么不自己软解 MP3我实测过用 Helix 这类的定点 MP3 解码库跑在 Pico 上单核解码 128kbps 的 MP3 大概占用 60%~70% 的 CPU。听着好像还行但别忘了同时还要跑网络接收、缓冲管理、编码器读取、LED 指示一叠加就吃满了。更麻烦的是一旦网络有抖动、缓冲区管理没做好就会爆音、卡顿。把解码交给专门的芯片等于把最重的活外包出去Pico 只当调度员系统稳定性和音质都上了一个台阶。另外硬件解码模块通常自带 DAC输出的是模拟信号直接接功放就行省掉了单独的 DAC 芯片和 I2S 走线的麻烦。不过我用的是带 I2S 输出的解码模块音质会更好一些而且数字信号抗干扰能力强走线长一点也没关系。这里要根据你的模块型号来定市面上常见的VS1053模块是 SPI 接口的也能解码 MP3但没有 I2S 输出PCM5102是纯 DAC需要配合软解或外部解码芯片。我最后用的是支持 I2S 输出的解码方案具体型号这里不展开思路是通用的。2.3 交互方式的选择旋钮、按键还是屏幕老式收音机最迷人的地方就是那个调频旋钮转动时有段落感和阻尼。我不想让这台机器变成按键菜单党所以交互设计上做了取舍一个旋转编码器负责调台和音量一个按键负责切换模式和开关机一个 OLED 小屏显示当前电台名和音量。旋转编码器选的是常见的EC11带按压功能转动时输出两路相位差 90 度的脉冲Pico 通过判断哪个信号先跳变来确定转向。这个器件很便宜一块钱一个但用起来特别有质感。OLED 屏用的是 0.96 寸的 SSD1306I2C 接口只占两个 GPIO显示电台名、音量、WiFi 状态足够了。屏幕这东西不是必须的但没有它你根本不知道现在在放什么台体验会打折。这里要提醒一句EC11 的机械触点有抖动直接用中断读会误触发必须加软件消抖或者用 RC 滤波。我一开始偷懒没消抖结果转一下跳好几个台后来加了 5ms 的软件消抖才稳定。这个小细节后面在代码部分会详细说。3. 硬件选型与接线细节3.1 物料清单与成本核算先把东西列清楚免得你做到一半发现少零件。下面这张表是我这台机器的实际用料价格是这段时间的参考价各地会有浮动。器件型号/规格数量参考单价用途主控板树莓派 Pico 2W1约 45 元主控与联网音频解码模块I2S 输出型解码板1约 25 元MP3/AAC 解码功放板PAM84031约 5 元音频放大喇叭4 欧 3W 全频2约 8 元/个发声旋转编码器EC11 带按压2约 2 元/个调台、调音量OLED 屏0.96 寸 SSD13061约 12 元状态显示按键6x6 轻触2约 0.5 元/个模式、开关电源5V 2A USB 适配器1约 10 元供电外壳木质/3D 打印1约 30 元装机杂项杜邦线、电阻、电容若干约 15 元连接与滤波算下来整机成本大概在160 元到 180 元之间比买一台现成的网络收音机便宜不了太多但胜在可玩、可改、完全自主。你要是手里已经有部分器件成本还能再压一压。关于喇叭的选择这里多说一句。4 欧 3W 的规格匹配 PAM8403 很合适如果你换 8 欧的喇叭音量会小一些但失真更低。我个人偏好人声清晰的调音选了 4 欧的听新闻和谈话类节目很舒服。如果你想低频更足可以选带音腔的喇叭别用裸喇叭否则声音干瘪。3.2 GPIO 分配与引脚规划Pico 2W 一共 26 个可用 GPIO除去内部占用的规划不好到后面改起来很烦。我在纸上画了好几版最后定下来的分配是这样的功能GPIO 引脚接口类型说明I2S 位时钟 BCKGP16PIO高频时钟I2S 左右时钟 LRCKGP17PIO声道选择I2S 数据 SDATAGP18PIO音频数据编码器 A 相GP10中断调台编码器 B 相GP11中断调台编码器按压GP12中断模式切换音量编码器 AGP13中断调音量音量编码器 BGP14中断调音量OLED SDAGP4I2C0显示数据OLED SCLGP5I2C0显示时钟功能按键GP6中断电源/静音状态 LEDGP15GPIO联网指示I2S 我放在 GP16~GP18 这一组是因为它们物理上挨着走线短、干扰小。I2C 用 GP4/GP5 是 Pico 默认的 I2C0 引脚很多库直接支持省事。两个编码器的四个相位脚分开放在 GP10~GP14避免互相干扰也方便中断处理时按组区分。3.3 I2S 音频总线的硬件要点I2S 是这个项目里最娇气的部分值得单独拎出来讲。I2S 通过三根线传输BCK是位时钟频率等于采样率乘以位深乘以声道数比如 44.1kHz、16 位、双声道BCK 就是 1.4112MHzLRCK是左右声道时钟频率等于采样率也就是 44.1kHzSDATA是按位串行的音频数据。三根线必须严格同步时钟歪一点数据就错位听到的就是刺耳的噪声。Pico 2W 的 PIO 特别适合干这个活。PIO 是一小块可编程的状态机能独立于 CPU 运行精确产生时钟翻转。我用的 I2S 方案基于 PIO 的移位寄存器输出抖动在纳秒级远好于软件模拟。接线的时候要注意三点第一线尽量短最好控制在 10 厘米以内长了容易引入干扰第二GND 一定要共地Pico 和解码模块的地线要连在一起否则信号没有参考电平第三电源去耦解码模块和功放的电源脚旁边各并一个 100nF 电容能显著减少底噪。注意I2S 和 I2C 的线不要并排捆在一起走I2S 的高频时钟会串扰到 I2C 上导致 OLED 花屏。我第一次布线图省事把它们绑一起结果屏幕一直闪分开走线后问题消失。4. 软件架构与核心代码拆解4.1 用 FreeRTOS 还是裸机循环Pico 2W 是双核不充分利用就浪费了。我的方案用了FreeRTOS 的多任务模型把工作拆成四个任务分配到两个核上核 0 任务 A网络与缓冲管理。负责连 WiFi、拉音频流、把数据填进环形缓冲区。核 0 任务 B音频喂数据。从缓冲区取数据通过 I2S 推给解码模块。核 1 任务 CUI 与交互。读编码器、更新 OLED、处理按键。核 1 任务 D状态监控。监控 WiFi 状态、缓冲区水位异常时触发重连或报警。为什么这么分因为网络任务和音频任务都跟时间强相关放一个核上容易互相抢占UI 任务优先级低放另一个核上不影响音频流畅度。缓冲区的做法也很关键网络来得快音频取得慢用环形缓冲能吸收网络抖动只要缓冲区不空声音就不会断。下面是我写的任务创建部分的骨架代码基于 Pico SDK 加 FreeRTOS#include pico/stdlib.h #include FreeRTOS.h #include task.h // 环形缓冲区句柄大小为 64KB能缓存约 4 秒的 128kbps 音频 static ring_buffer_t audio_rb; void network_task(void *p) { wifi_connect(你的SSID, 你的密码); stream_open(RADIO_URLS[current_station]); while (1) { // 从 socket 读取数据块写入环形缓冲 uint8_t buf[1024]; int n stream_read(buf, sizeof(buf)); if (n 0) { ring_buffer_write(audio_rb, buf, n); } else { stream_reconnect(); // 出错就重连 } vTaskDelay(pdMS_TO_TICKS(1)); } } void audio_task(void *p) { decoder_init(); while (1) { uint8_t buf[512]; size_t got ring_buffer_read(audio_rb, buf, sizeof(buf)); if (got 0) { i2s_write(buf, got); // 通过 PIO 推给解码板 } else { vTaskDelay(pdMS_TO_TICKS(5)); // 缓冲空了就歇一下 } } } void ui_task(void *p) { oled_init(); encoder_init(); while (1) { int delta encoder_read_delta(tuner_enc); if (delta ! 0) { current_station clamp(current_station delta, 0, RADIO_COUNT - 1); stream_switch(RADIO_URLS[current_station]); oled_show_station(current_station); } vTaskDelay(pdMS_TO_TICKS(20)); } } int main() { stdio_init_all(); ring_buffer_init(audio_rb, 65536); xTaskCreate(network_task, NET, 4096, NULL, 3, NULL); xTaskCreate(audio_task, AUD, 2048, NULL, 4, NULL); xTaskCreate(ui_task, UI, 2048, NULL, 1, NULL); vTaskStartScheduler(); return 0; }这段代码里几个点值得说。第一任务栈大小别设太小网络任务里要开 TLS 的话栈至少 4KB我一开始设 2KB连 HTTPS 电台直接崩了。第二优先级的安排是音频最高、网络次之、UI 最低保证声音不卡。第三vTaskDelay在空转时让出 CPU不做无谓的忙等。4.2 网络音频流的接收与缓冲策略网络电台的流媒体协议主要有两种HTTP 渐进式流和Icecast/Shoutcast 流。前者就像一个很长的 HTTP 响应一直往下传数据后者在响应头里带icy-metaint字段每隔固定字节数插入一段元数据比如当前歌曲名。处理起来要注意解析时必须跳过元数据块否则会把标题文字当音频数据推给解码器出来的是刺耳噪声。我处理的方式是读到响应头后解析出icy-metaint的值之后每读metaint字节就再读一个长度字节如果非零就跳过长度 x 16字节的元数据。下面是简化的处理逻辑// 假设 header 中解析出 icy_metaint int metaint parse_icy_metaint(header); int bytes_until_meta metaint; uint8_t meta_len 0; while (1) { if (bytes_until_meta 0) { // 读元数据长度字节单位是 16 字节 stream_read(meta_len, 1); if (meta_len 0) { uint8_t meta[meta_len * 16]; stream_read(meta, sizeof(meta)); update_song_title((char *)meta); // 更新显示 } bytes_until_meta metaint; } else { int to_read min(sizeof(buf), bytes_until_meta); int n stream_read(buf, to_read); ring_buffer_write(audio_rb, buf, n); bytes_until_meta - n; } }缓冲策略上我设了64KB 的环形缓冲按 128kbps 的码率算大约能存 4 秒音频。为什么不设更大因为 Pico 2W 的 RAM 有限总共 520KB还要留给网络栈和任务栈。4 秒的缓冲在实际网络环境下够用了偶尔的网络抖动吸收得掉。如果你的网络特别不稳定可以适当加大但别超过 128KB否则其他任务容易内存不够。提示加入缓冲水位监控当缓冲低于 1 秒约 16KB时主动降低读取节奏或临时切到低码率流能减少爆音概率。这个逻辑我在状态监控任务里做了。4.3 旋转编码器的读取与消抖处理EC11 编码器输出的是两路正交信号A 相和 B 相差 90 度。判断转向的方法当 A 相下降沿来临时读 B 相的电平如果是低就是顺时针高就是逆时针。听起来简单但机械编码器有抖动一个物理转动会触发好几次中断导致一次转动跳很多格。我的做法是状态机加时间窗滤波。记录上一次状态变化的时间只有距离上次超过 5ms 才认为是有效转动。同时在中断里只记录相位变化实际计数放到 UI 任务里做避免中断里做复杂运算。核心代码如下static volatile int enc_delta 0; static volatile uint32_t last_ms 0; void encoder_irq(uint gpio, uint32_t events) { uint32_t now to_ms_since_boot(get_absolute_time()); if (now - last_ms 5) return; // 5ms 消抖窗口 last_ms now; int a gpio_get(ENC_A); int b gpio_get(ENC_B); // 正交解码根据两相电平组合判断方向 if (a b) enc_delta; // 顺时针 else enc_delta--; // 逆时针 } int encoder_read_delta(encoder_t *e) { int d enc_delta; enc_delta 0; // 读取后清零 return d; }这个方案实测下来很稳转一格就是一格不会跳。注意enc_delta要加volatile否则编译器优化可能导致读取到旧值。还有两个编码器最好用不同的变量和中断处理别想着用同一个函数处理所有引脚容易乱。5. 常见问题与排查实录5.1 没有声音或者只有噪声这是最高频的问题我把它拆成几种情况。第一种是完全没声先查电源解码模块和功放有没有独立供电Pico 的 3.3V 输出带不动功放必须用 5V 供电。再查接线I2S 三根线有没有接对、GND 有没有共地。第二种是有噪声但听不清内容多半是 I2S 时序问题或者采样率不匹配检查解码模块的采样率配置和 Pico 输出的是否一致44.1kHz 和 48kHz 弄混了就是一片沙沙声。第三种是断断续续爆音这是缓冲区管理的问题检查环形缓冲是否太小、网络任务是否被饿死。我整理了一张排查速查表现象可能原因解决方法完全没声功放未供电、GND 未共地检查 5V 供电和共地沙沙噪声I2S 时钟/采样率不匹配核对采样率配置一致断续爆音缓冲区太小、网络卡加大缓冲、检查 WiFi 信号只响一个声道LRCK 接错或声道数据问题检查 LRCK 接线与数据格式音量很小喇叭阻抗不匹配换 4 欧喇叭或加前级OLED 花屏I2C 被 I2S 干扰分开走线、加去耦电容5.2 WiFi 连不上或者频繁掉线Pico 2W 的无线是 2.4GHz 单频不支持 5GHz这是第一个要确认的——你家路由器如果 5GHz 和 2.4GHz 用了同一个名字可能会连到 5GHz 上导致失败。解决办法是给 2.4GHz 单独设一个明确的 SSID。第二个原因是信号太弱Pico 的板载天线增益一般离路由器太远或者中间隔墙就会掉线可以外接一根小天线或者把机器放在信号好点的地方。第三个原因是DHCP 超时路由器分配 IP 慢的时候连接会失败代码里把重试次数和超时时间设长一点我设的是重试 5 次、每次等待 10 秒。还有一种情况是连上了但拉流失败这往往是电台地址的问题。有些电台需要 HTTPS有些需要特定的 User-Agent 头有些干脆已经下线了。我的建议是准备一个电台列表逐个测试把能用的留下来。测试方法很简单用电脑上的播放器先播一遍确认地址有效再写进固件。5.3 音频延迟和音画不同步的困扰网络收音机有个固有的延迟从电台服务器到你的喇叭至少几百毫秒这是缓冲和网络传输带来的。对纯听广播来说这无所谓但如果你想要实时感比如听体育直播延迟就会有点难受。降低延迟的办法是缩小缓冲但代价是抗抖动能力下降鱼和熊掌不可兼得。我的折中方案是动态缓冲网络好的时候缓冲缩到 1 秒网络差的时候自动扩到 4 秒。实现方式是监控缓冲水位和丢包率动态调整读取节奏。这个逻辑不算复杂但确实能明显改善体验。另外如果你对延迟特别敏感可以选择码率低一点的电台比如 64kbps数据量小缓冲填得快延迟自然低。5.4 整机发热和电源噪声做到最后装机的时候我发现一个问题功放和 Pico 挤在同一个盒子里时间长了盒子有点温。这不是故障PAM8403 工作时本身会发热3W 输出时效率大概 80% 多剩下的变成热。解决办法是开散热孔或者把功放贴在外壳内壁上帮助散热别用完全密封的盒子。电源噪声是另一个隐形杀手。如果 Pico 和功放共用一路 5V功放的大电流波动会通过电源线串到 Pico 上导致 WiFi 不稳或者 I2S 抖动。我的做法是电源分离一路 5V 给功放一路经过 LC 滤波给 Pico 和解码模块。实在只有一路电源就在 Pico 的电源脚旁边并一个大电容比如 470uF做储能能压住瞬态波动。注意不要把功放的输出线和解码模块的信号线捆在一起走输出线里是大电流音频信号会耦合到输入端形成正反馈严重时产生啸叫。我第一次布线没注意开机就啸叫分开后才干净。6. 装机、调试与使用体会6.1 外壳设计与组装顺序外壳我是用 3D 打印做的前面板开孔给 OLED、两个旋钮和一个按键后面板开孔给电源和喇叭线。打印参数用的是 0.2mm 层高、20% 填充强度足够。如果你没有 3D 打印机用现成的塑料项目盒也行开孔用美工刀和电钻慢慢修。木质外壳音质会温暖一些但加工难度大看你的工具条件。组装顺序很重要别想着一次装好。我的顺序是先在面包板上把整机跑通确认能出声、能换台再焊接到洞洞板或者定制 PCB 上。焊接完再做一次通断测试最后才装外壳。这个顺序能帮你把电气问题和机械问题分开排查不至于装好了发现不工作又全部拆开。走线的时候音频信号线用带屏蔽的电源线用粗一点的信号和电源分开走。6.2 电台列表的维护与扩展固件里的电台列表是我自己整理的大概三十几个覆盖新闻、音乐、谈话几类。维护这个列表有个小技巧把电台信息写成结构体数组每条包含名称、URL、编码格式、码率方便以后增删。typedef struct { const char *name; const char *url; const char *codec; // mp3 或 aac int bitrate; // kbps } radio_station_t; static const radio_station_t stations[] { {新闻台, http://example.com/news.mp3, mp3, 128}, {音乐台, http://example.com/music.aac, aac, 96}, {谈话台, http://example.com/talk.mp3, mp3, 64}, // ... 更多电台 };这里要提醒的是电台地址会失效互联网上的免费电台经常换域名或者关停。我的做法是每季度检查一次把失效的换掉。另外有些电台有地域限制或者需要特定协议遇到播不了的直接跳过别在单个台上耗太久。6.3 实际使用的几点体会用了一段时间之后我对这台小机器有几个真实的感受。首先是开机速度从插电到出声大概 3 秒比我预期的还快这个体验很接近传统收音机按一下就有声音。其次是音质在卧室环境下人声清晰、底噪很低比我那台老 FM 好不少当然跟正经 HiFi 没法比但对得起这个成本。第三是稳定性连续播放一整天没出过问题WiFi 掉线也能自己重连这一点比我之前做的一些项目靠谱。也有不满意的地方。旋钮的手感依赖编码器质量便宜的 EC11 转起来有点松我后来换了质量好一点的才好。OLED 在强光下看不清这个没办法是屏幕本身的问题。还有就是电台切换有个一两秒的缓冲时间做不到传统收音机那种瞬时切换这是网络流的固有特性接受就好。如果让我给想复刻的朋友一句建议那就是先跑通最小系统再从网络到音频逐段验证。不要一上来就把所有模块接上出了问题你都不知道是哪一段的锅。我见过太多人卡在没声音上其实往往就是某一根线或者某一个配置的小问题。分段调、逐段测做硬件项目永远是这句话最管用。后续我打算给这台机器加两个功能一个是定时开关机早上自动打开放新闻另一个是记忆上次电台断电重启后接着放。这两个功能都不难加个 RTC 模块或者利用 Pico 的内部时钟加上一个小电池就行。等做完了再跟大家分享。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询