资讯动态

ESP32纯网络DMR终端:RoIP实现与硬件设计

发布时间:2026/9/26 1:32:46 来源:尧图企业网站定制
说句实话第一眼看到这个标题我自己都愣了一下。“ESP32纯网络DMR终端”听起来像在否定DMR的命根子毕竟DMR之所以是数字移动无线电靠的就是UHF/VHF频段上的4FSK调制信号没有射频还把什么通联但我做完这个项目后反而不是这么想了。这个开源硬件项目的核心思路非常简单DMR中继台之间本来就是靠IP网络互联的那我能不能把终端的另一半也搬上网络手里这台设备不再承担任何VHF/UHF频段发射只负责采集语音、走WiFi或以太网上网剩下的交给DMR网络侧的桥接服务去处理。做出来之后我最大的感受是——它确实不适合所有人但对于像我这样主要玩联网通话、又不想折腾天线和执照相关问题的爱好者这种“告别射频”的形态反而干净利落。1. 不发射射频DMR 还能怎么通1.1 先弄清楚 DMR 协议里哪些是“数字”哪些是“射频”想把这个项目讲明白要先分清DMR系统里的两层东西。我们平时说的DMR空中接口部分确实非常“射频”它采用4FSK调制在12.5kHz信道带宽里跑9.6kbps的码流而且帧结构还包含30ms一个TDMA帧、两个时隙这些规则都是为了在射频链路上高效传输。但问题也在这里——DMR中继台之间、中继台和核心网之间用的早就是IP网络了。你去翻任何一个DMR核心网服务商的文档就会发现中继台接入网络时通过固定端口建立TCP连接然后用专用数据帧把呼叫请求、语音帧、信令都打包进去。也就是说DMR实际上天生就有“可脱离射频”的一部分。只不过传统终端为了和本地中继台通信必须先把语音编码成DMR空中接口格式再调制上射频。而纯网络终端要做的就是把这个“编码成空中接口格式再上射频”的步骤省掉直接让自己变成一个IP节点。这是最核心的一个认知转变DMR不再必须是“电台”的形态。它可以是一块ESP32开发板带着麦克风、喇叭和一个PTT按键通过网络连接到一个处理DMR协议转换的桥最终进入DMR核心网完成呼叫。1.2 纯网络终端的技术路线RoIP 而不是“魔改空中接口”那问题来了ESP32能不能直接把语音打包成DMR的空中接口数据帧发给服务器严格说这是最“正宗”的纯网络DMR但现实很骨感。DMR语音用的AMBE编码器是专有算法尽管网上有一些反向工程资料想在ESP32这种资源有限的MCU上跑完整AMBE编解码器既不轻松也不稳定。我评估过几条路线之后决定采用更务实的方案——RoIPRadio over IP即“语音走IP、协议转换交给网关”。所谓RoIP简单说就是把对讲机的语音和PTT控制信号数字化之后放到IP网络上传输另一端的网关再把它翻译成DMR网络需要的东西。这样一来终端这边的任务被大大简化采集麦克风音频、压缩、通过网络发送同时接收网络来的音频流、解码、播放。至于DMR真正关心的时隙、通话组ID、私呼号码这些信令可以由网关侧统一处理由网关把这些信令包装成DMR核心网认得的协议包。我选的网关是树莓派上跑的MMDVMHost把它配置成“纯网络模式”当作一个虚拟热点中继来使用。这样整个链路是ESP32终端 —— WiFi/以太网 —— MMDVMHost —— DMR核心网 —— 远端中继台或另一个纯网络终端。1.3 我用到了哪些现成组件与协议整个项目的软件协议栈其实没有想象中复杂。终端和网关之间我用的是最原始的UDP音频流原因后面讲延迟时再说语音编码先用PCM 8kHz 16bit 单声道跑通之后再换成更省带宽的Opus或者Codec2。网关侧MMDVMHost负责把收到的音频转成DMR数据帧并发送到BrandMeister等核心网。组件清单如下终端主控ESP32 DevKitC V4双核240MHz带WiFi和蓝牙最主要是有两个独立的I2S外设可以一条I2S给麦克风、另一条给功放数字麦克风INMP441I2S输出省掉模拟前端的偏置电路和放大电路功放MAX98357A同样走I2S输入直接驱动一个小喇叭输出功率1W左右足够手持机用显示器SSD1306 0.96寸OLED用来显示连接状态、IP地址和当前通话组网络模块W5500或者LAN8720以太网模块如果走有线模式的话按键一个轻触开关做PTT串接一个LED指示发射中状态。这套组合的关键优势是全部数字链路麦克风采集到的声音不经过模拟放大直接I2S进ESP32天然抗干扰。项目复现时也不需要采购那些专用对讲机芯片大部分料都是常见模块。2. 核心硬件选型与引脚分配2.1 主控为什么我选 ESP32 而不是树莓派 Pico 或 STM32这块纯网络终端芯片最先筛掉的是STM32F103频率太低且没有WiFi树莓派Pico要另外接WiFi模块整机做起来像积木拼接ESP32就是我手里最能打的选手。它内置WiFi和蓝牙240MHz双核其中一个核可以专职跑音频I2S另外一个核跑网络协议栈和主逻辑天然适合对讲机这种“边说话边收发”的场景。具体型号上原版ESP32和ESP32-S3都可以。我手上的是经典ESP32 DevKitCI2S外设够用。如果重新选型我更推荐ESP32-S3它的I2S实现更完善且原生支持更大的PSRAM后续如果要在终端里跑Codec2编码器会更从容。注意不要选ESP32-C3虽然便宜但只有一个核、I2S外设也不是全功能的处理连续音频流时容易卡顿。2.2 语音输入输出INMP441 MAX98357 的组合传统模拟麦克风方案需要运放、偏置电阻、ADC采样噪声控制很琐碎。我这次直接用INMP441它本身就是一个完整的数字麦克风内部有MEMS传感、放大和ADC输出的是24bit I2S数据。你只需要给它3.3V供电、I2S时钟和数据线它就能吐出干净的数字音频。对讲机应用里8kHz采样率已经足够传输语音INMP441最高支持到48kHz我用的是16kHz采样避免音频太“闷”。输出侧MAX98357A更省事它也是I2S输入内部集成了D类功率放大器和输出滤波。从ESP32引三根线——BCLK、LRCLK、DIN——出来功放那边就能直接驱动喇叭。这两个模块都有I2S接口对ESP32来说就等于两个数字音频端点软件实现时把输入数据挪到输出端点很容易。功耗方面INMP441工作电流约1.4mAMAX98357在待机模式下不到1mA说话时根据音量大小差异较大但整机平均电流控制在150mA以内500mAh锂电池也能撑一个下午的断续通联。2.3 外设细节PTT、OLED 和电池管理PTT按键接一个GPIO上拉输入按下去为发射状态。这块有个细节我选GPIO4作为PTT输入而不是常见的GPIO0原因放在后面“以太网坑”一节说。OLED用的是I2C接口接GPIO21和GPIO22地址0x3C显示当前IP地址和通话组ID方便确认终端是否连上网关。电池部分我用了一个TP4056充电模块加18650电池因为纯网络终端不像传统对讲机那样依赖恒定射频功率电流波动没那么凶。锂电池正极经过一个LDOAMS1117-3.3给ESP32和传感器供电注意LDO输入耐压要选对。实际上ESP32 DevKitC板载了USB转串口芯片直接用USB供电调试也行我接电池是为了模拟手持机场景。2.4 完整引脚分配参考表功能外设ESP32 引脚I2S 麦克风数据INMP441 SDGPIO32I2S 麦克风时钟INMP441 SCKGPIO26I2S 麦克风帧同步INMP441 WSGPIO25I2S 功放数据MAX98357 DINGPIO22I2S 功放时钟MAX98357 BLCKGPIO21I2S 功放帧同步MAX98357 LRCGPIO19PTT 输入轻触开关GPIO4PTT 状态灯LEDGPIO2OLED I2CSSD1306GPIO18(SDA) / GPIO23(SCL)以太网复位LAN8720 nRSTGPIO5以太网中断LAN8720 INTGPIO16这个表格是我已经跑通过的一版如果你电脑串口芯片占用不同可以整体平移几个GPIO但要注意I2S的数据线、时钟线最好都用硬件外设映射不要用软件模拟。3. 软件任务与音频流设计3.1 I2S 音频任务采样率、缓冲区怎么定软件设计第一件事是定采样参数。对讲机语音带宽有效范围是300Hz到3.4kHz奈奎斯特采样率8kHz够用但我用16kHz是为了让Codec2和Opus编码器有更好的语音质量。I2S每次读取也很有讲究ESP32的I2S驱动按字节对齐我一次读取32字节的音频数据相当于8ms的一小包正好匹配后续UDP包的长度。主循环用FreeRTOS双任务结构。一个任务负责音频采集它永远在阻塞状态等I2S DMA数据到来另一个任务负责网络收发和UI更新。两个任务之间用一个环形缓冲区交换音频数据。我这里没有用队列而是环形缓冲区原因是音频数据量大、频率高队列的拷贝开销太大环形区互斥锁就足够了。关键实现是开启CONFIG_SPIRAM这个选项没有哦不对我先把代码塞进Web浪费。如果你用的是Arduino框架直接调用installDriver时设置buffer数量为8每个buffer定义成64字节左右基本就不会产生欠载断裂音。伪代码形如// 音频采集任务优先级高 void audioTask(void*) { i2s_read(I2S_NUM_0, sampleBuffer, 64, bytesRead, portMAX_DELAY); xRingbufferSend(audioRing, sampleBuffer, bytesRead, pdMS_TO_TICKS(20)); } // 网络任务 void networkTask(void*) { // 从环形缓冲读音频打包成UDP报文发到网关 xRingbufferReceive(audioRing, ...); udpSend(packet); // 同时接收网关来的UDP包解码后送I2S功放 }3.2 网络线程UDP 包格式和抖动缓冲终端与网关的通信我用UDP报文固定分三类音频数据包、PTT状态包、心跳包。音频包格式是“0xAA 2字节序列号 音频数据”。序列号相当重要没有它就没法判断丢包网关侧也无法应对乱序。接收侧必须做抖动缓冲。UDP在网络里延迟不稳定可能这一包20ms、下一包120ms直接把包送去I2S播放就会产生声卡卡顿。我实现了一个简单的缓冲池收到包后按序列号插入有序链表每5ms检查一次是否有可以解码的连续包连续满三包才播放播放线程从缓冲池取数据。这个缓冲策略虽然增加约40ms延迟但对讲机这类应用中完全可接受换来的是相当平滑的语音不会出现“机器人音”。对讲机是半双工模式我设计成收信和发信共用同一个UDP端口。按下PTT时终端先发送一个PTT-on控制包网关收到后进入“占机”状态这时候终端持续发送音频包。松开PTT时发送PTT-off包。采用控制包和语音包分离的好处是网关可以准确判断超时就算最后一包语音丢了PTT-off也一定能触发释放流程。3.3 为什么选了半双工而不是全双工很多人觉得纯网络终端既然都上网了为什么不直接做成全双工体验接近打电话多好。我后面实测发现半双工确实更适合DMR场景。一方面DMR网络本身就是TDMA半双工协议中继台同一时间只在一个时隙上传一个源另一方面就是全双工需要回声消除ESP32上做AEC不是不行但调试量和延迟都会明显增加容易把本来不复杂的项目拖垮。这里给新手一个建议先把半双工跑通再去想进阶PTT一按一放这种操作方式本身就很有对讲机的仪式感也符合DMR用户的使用习惯。4. 以太网模块 LAN8720三类典型坑与完整接线4.1 为什么纯网络项目要考虑以太网WiFi省事但问题也不少2.4GHz频段环境一复杂延迟就抖动而且ESP32的WiFi和I2S音频同时跑的时候偶尔会出现音频DMA中断被WiFi任务抢占导致爆音。我的终端既然主打“告别射频”那用有线以太网把网络侧彻底稳定下来逻辑上才更自洽。于是我去买了块经典的LAN8720以太网模块RJ45口自带网变那种打算给终端插根网线。结果这一接就接出了本文的标题来源。LAN8720这个名字和水很深网上资料混乱模块版本又多以下三个坑是我几乎每块板子都会踩一遍的。4.2 坑一REF_CLK 不要乱接分清谁是时钟源LAN8720是RMII接口的PHY芯片RMII对时钟有硬性要求发送和接收必须共用一个50MHz参考时钟。很多模块出厂时设置成“有源晶振自给时钟”板上有一个50M晶振REF_CLK由LAN8720自己产生输出给MCU而有些模块是空晶振焊盘需要外部提供50MHz时钟进去。我第一次接的时候就搞混了看到网上一篇文章说“REF_CLK接GPIO0”我就把LAN8720的CLK_OUT接到ESP32的GPIO0上但模块是自给时钟版本结果GPIO0根本没有输入以太网初始化时esp_eth_phy_new_lan8720直接返回PHY地址不对。后来我拿示波器量板上晶振位置才发现这个模块自带晶振应该用的是CLK_OUT方向。正确做法看清你买的是“带晶振”还是“不带晶振”版本。带晶振的模块LAN8720的REF_CLK输出给ESP32的GPIO0输入然后配置ESP32的以太网时钟模式为ETH_CLOCK_GPIO0_IN不带晶振的模块则需要你额外从GPIO0输出50MHz时钟到LAN8720此时配置为ETH_CLOCK_GPIO0_OUT。两套接线完全不能混用。4.3 坑二PHY 地址寄存器冲突PHYAD引脚决定命运LAN8720的PHY地址由PHYAD0引脚的上下拉决定默认模块电路把这个引脚拉低PHY地址为0x00。表面上看这没问题但很多模块又同时把另一个地址选择焊盘做出来了拿跳线帽短路后PHY地址变成0x01。如果你的代码写死phy_addr 0而板子上跳线帽默认短接成1初始化就会失败。麻烦的是报错很不明显ESP-IDF日志只会显示“phy register not found”这种让人一脸懵的信息。第一次遇到时我反复确认供电和接线最后用循环扫描代码把所有可能地址打印一遍才定位到地址是1。建议在初始化代码里做一个简单的地址探测esp_eth_mac_t *mac; esp_eth_phy_t *phy; // 先尝试地址0 esp_eth_phy_new_lan8720(phy_config0, phy); // 如果mdio read失败尝试地址1如果项目里只有一颗LAN8720干脆按模块实际默认地址写死更省心懒得在运行时探测。4.4 坑三GPIO0既要进LinuxBoot又要做RMII时钟启动冲突这才是最坑的一个。ESP32的GPIO0脚粉身碎骨既是下载模式选择脚又是RMII参考时钟输入脚。如果你的电路让LAN8720的REF_CLK直接常连接GPIO0而且这个50MHz时钟在ESP32上电时就在运行那么GPIO0会处于强制输出状态导致芯片固件烧录时无法进入下载模式或者烧录后一上电就随机进入下载模式表现为程序完全跑不起来、串口打印乱码。解决思路有二一是LAN8720的nRST在ESP32上电初期保持低电平直到主控初始化以太网时才释放复位这样GPIO0就不会提前收到时钟二是如果你不需要以太网这种确认神坑就老老实实从板上焊接一个跳线让时钟输出在调试阶段可以断开。我最后选择的是方法一GPIO5控制LAN8720 nRST并设置为开漏输出。ESP32启动时先把GPIO5拉低等系统完成启动、开始初始化以太网驱动时再置高释放复位。经过这样处理下载烧录和以太网功能完全互不干扰。4.5 完整接线参考表LAN8720 版本LAN8720 引脚ESP32 引脚备注TXD0GPIO19RMII 发送数据0TXD1GPIO22RMII 发送数据1TX_ENGPIO21发送使能RXD0GPIO27接收数据0RXD1GPIO25接收数据1CRS_DVGPIO26载波/数据有效REF_CLKGPIO0必须看模块晶振方向MDCGPIO23管理时钟MDIOGPIO18管理数据nRSTGPIO5我额外接带RC复位延时INTGPIO16可选中断引脚跑通以太网之后我明显感觉音频包的抖动小了一个数量级终端开机后秒连服务器不像WiFi还要等握手和DHCP。这个优化对“纯网络”这三个字的体验提升巨大。5. 接入 DMR 网络的桥接实践5.1 MMDVMHost 到底是干什么的现在终端已经能把语音塞进UDP包发给电脑了但电脑收到之后要把这些语音变成“DMR核心网能懂的东西”。这里我用的桥接是MMDVMHost它原本是配合MMDVM板子驱动射频调制解调器的程序但它也支持纯网络模式可以模拟一个虚拟中继台。我在树莓派上刷好系统后把一个MMDVMHost实例配置成ModeMMDVM但不开射频调制解调器开启网络功能让它作为一个虚拟热点中继连接远端DMR核心网。这样ES

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑