基于ESP32-S2与RDA5807M的双模收音机设计与实现
1. 项目概述从零打造一台ESP32-S2双模收音机作为一名嵌入式开发工程师我手头总有些“不务正业”的小想法。这次我想抛开复杂的物联网协议和云端服务回归一个更纯粹、更“复古”的需求听广播。但我不想只做一个传统的FM收音机而是希望它能连接网络收听世界各地的网络电台。于是基于ESP32-S2这款高性价比的Wi-Fi MCU我设计并实现了一台集成了传统FM收音机与网络收音机功能的双模设备。这个项目不仅是对ESP32-S2外设应用的一次深度探索也涉及了Arduino生态下的多库整合、UI交互设计以及一些实际开发中绕不开的“坑”。如果你也对硬件DIY、嵌入式音频应用或者想找一个综合性的Arduino实战项目感兴趣那么这篇从硬件选型到软件调试的全程记录或许能给你带来一些直接的参考和启发。整个设备的核心功能很清晰通过一个OLED屏幕和四个物理按键作为人机界面用户可以自由在“FM收音机”、“网络收音机”和“时钟显示”三个页面间切换。FM部分能自动搜台并存储网络部分则通过Wi-Fi连接指定的流媒体服务器获取音频流。此外设备开机后会自动从网络同步精确时间。下面我就将整个设计思路、实现细节、踩过的坑以及优化方向毫无保留地分享出来。2. 核心硬件选型与电路设计解析硬件是项目的骨架合理的选型是稳定运行的基础。我的核心思路是在满足功能的前提下追求极致的性价比和易用性同时为后续可能的扩展留出空间。2.1 主控芯片为什么是ESP32-S2在众多MCU中我最终选择了ESP32-S2-MINI-1这款模组。它基于ESP32-S2FH4芯片内置320KB SRAM搭载4MB Flash且不含PSRAM。选择它主要基于以下几点考量成本与功能平衡ESP32-S2是乐鑫推出的单核Wi-Fi MCU相较于双核的ESP32它砍掉了蓝牙功能但保留了强大的Wi-Fi性能和外设接口。对于这个不需要蓝牙的收音机项目来说它提供了最具性价比的Wi-Fi解决方案。其内置的硬件加速器也能很好地处理网络音频流的解码后续软件部分会详述。充足的IO与存储项目需要驱动I2C接口的OLED屏和FM模块连接多个按键还需要足够的程序空间来容纳网络协议栈、音频解码库和图形库。ESP32-S2的GPIO数量和4MB Flash完全满足需求320KB的RAM在精心管理下也足够运行。开发生态成熟得益于乐鑫官方的arduino-esp32支持我们可以直接在Arduino IDE或VS Code中使用熟悉的Arduino框架进行开发极大地降低了开发门槛避免了从零搭建ESP-IDF环境的复杂性。注意ESP32-S2系列有多个子型号主要区别在于封装和Flash大小。MINI-1是表面贴装模组集成天线开箱即用非常适合手工焊接或使用转接板。如果你的设计对体积有要求需要注意其尺寸。2.2 收音机模块RDA5807M的稳定之选FM收音机功能我交给了专门的芯片RDA5807M。这是一款非常流行的单芯片FM立体声收音机模块通过I2C接口控制。为什么不直接用ESP32-S2的ADC去采样原因如下专业性FM调频信号的接收、解调、立体声解码是一个复杂的射频模拟过程RDA5807M作为专用芯片其接收灵敏度、抗干扰能力和音质远非MCU通过软件模拟可比。简化设计该模块将射频前端、中频放大、锁相环、音频输出等所有电路集成在一颗芯片内外围电路极其简单通常只需几个电容、电感和一个晶振大大降低了硬件设计和调试难度。控制简单通过标准的I2C协议我们可以轻松地设置频率、音量、静音、搜索电台等所有功能开发者只需关注应用逻辑。2.3 人机交互与辅助电路显示单元选用一款0.96英寸的I2C接口OLED屏幕SSD1306驱动。I2C只需两根数据线节省GPIOOLED自发光的特性使其显示效果清晰尤其在光线较暗的环境下比如夜晚听广播体验很好。输入单元四个轻触按键分别定义为“模式切换”、“频率/频道切换”、“音量加”、“音量减”。所有按键均配置为内部上拉输入模式按下时接地电路简单可靠。音频输出RDA5807M模块本身提供耳机接口输出。对于网络音频ESP32-S2内置了一个8位的DACGPIO17, GPIO18但性能一般。为了获得更好的音质我外接了一颗PCM5102A这类I2S接口的音频解码芯片来播放网络流媒体。这样FM走耳机孔网络音频走外接DAC和功放互不干扰。如果对音质要求不高也可以直接用ESP32-S2的DAC驱动耳机。电源管理整个系统由USB 5V供电。使用一颗AMS1117-3.3稳压芯片将5V转为3.3V为ESP32-S2、OLED屏和RDA5807M模块供电。如果考虑便携性增加电池则需要加入锂电池充电管理电路如TP4056和升压电路将电池电压升至5V或3.3V。核心电路连接表元件连接至ESP32-S2引脚功能说明RDA5807M SDAGPIO8I2C数据线RDA5807M SCLGPIO9I2C时钟线OLED SDAGPIO8与FM模块共用I2C总线OLED SCLGPIO9与FM模块共用I2C总线按键1 (模式)GPIO38内部上拉低电平触发按键2 (切换)GPIO39内部上拉低电平触发按键3 (音量)GPIO40内部上拉低电平触发按键4 (音量-)GPIO41内部上拉低电平触发PCM5102A BCKGPIO42I2S位时钟PCM5102A DINGPIO41I2S数据输入 (注意与按键4引脚冲突需调整)PCM5102A LCKGPIO40I2S字时钟 (注意与按键3引脚冲突需调整)实操心得ESP32-S2的引脚功能复用非常灵活但需要注意冲突。例如我最初将I2S和按键分配到了同一组引脚导致冲突。在规划引脚时最好先查阅官方引脚定义图明确每个引脚默认功能如GPIO40、41常用于ADC但也可作普通IO并优先确保I2C、I2S等专用外设引脚分配到正确的硬件通道上。3. 软件开发环境搭建与核心库配置软件环境是项目的神经中枢。我选择在VS Code中使用PlatformIO插件进行开发它比原生Arduino IDE更强大但为了兼容原项目描述这里同时介绍Arduino IDE和VS Code两种方式。3.1 基础环境部署Arduino框架与ESP32支持无论使用哪种编辑器首先需要安装Arduino框架和ESP32开发板支持。安装Arduino IDE从Arduino官网下载安装包。安装后打开“文件”-“首选项”在“附加开发板管理器网址”中添加ESP32的板支持网址https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json。安装ESP32开发板打开“工具”-“开发板”-“开发板管理器”搜索“esp32”找到由“Espressif Systems”提供的版本并安装。这个过程需要下载大量文件网络稳定性是关键。VS Code方案推荐安装VS Code。安装“PlatformIO IDE”扩展。PlatformIO是一个专业的嵌入式开发平台内置了构建系统、库管理和调试工具对ESP32的支持非常完善。在PlatformIO中新建项目选择开发板为“Espressif ESP32-S2-Saola-1”或其他ESP32-S2板型框架选择“Arduino”。PlatformIO会自动下载所有必要的工具链和框架比手动配置Arduino环境更省心。3.2 核心依赖库的安装与选择本项目主要依赖以下几个库这些库在Arduino库管理器或PlatformIO的库搜索中都能找到U8g2用于驱动OLED显示屏。它支持海量的显示控制器包括我们使用的SSD1306且提供了丰富的绘图和字体函数。在库管理器中搜索“U8g2”并安装。RDA5807用于控制FM收音机模块。搜索“RDA5807”或“RDA5807M”选择评价较高的库安装。确保其支持I2C通信和完整的频率控制功能。WiFi HTTPClient Audio这些是arduino-esp32框架内置的库。对于网络收音机我们需要WiFi连接网络用HTTPClient获取音频流。最关键的是音频播放我们可以使用Audio库来自arduino-esp32或更强大的ESP32-audioI2S库。后者功能更全对网络流媒体支持更好我最终选择了它。NTPClient用于网络对时。这是一个轻量级的库可以方便地从NTP服务器获取并同步时间。踩坑记录库版本与网络问题在安装arduino-esp32支持包时最大的障碍是网络。由于资源托管在GitHub下载可能非常缓慢甚至失败。解决方案一Arduino IDE如果安装失败可以手动下载。根据IDE控制台输出的错误信息找到需要下载的zip包URL。使用能稳定下载GitHub资源的工具如某些代理或镜像站下载后将其解压到Arduino的staging/packages目录路径类似C:\Users\[用户名]\AppData\Local\Arduino15\staging\packages然后重启Arduino IDE再次尝试安装IDE会识别本地文件。解决方案二PlatformIOPlatformIO的下载通常更稳定因为它有内置的镜像源。如果遇到问题可以在VS Code的设置中配置PlatformIO的custom_download_url指向国内镜像源。版本问题确保所有库的版本兼容。例如新的arduino-esp32版本如2.0.x的API可能与旧版略有不同。建议在项目开始时明确记录所用库的版本号。3.3 项目源码结构解析我的项目源代码主要分为以下几个模块这种结构清晰便于维护ESP32_Radio/ ├── src/ │ ├── main.cpp // 主程序入口初始化与主循环 │ ├── radio_fm.h/cpp // FM收音机功能封装RDA5807控制 │ ├── radio_network.h/cpp // 网络收音机功能封装WiFi连接、音频流处理 │ ├── display.h/cpp // OLED显示控制封装U8g2调用 │ ├── input.h/cpp // 按键扫描与去抖逻辑 │ └── ntp_time.h/cpp // 网络时间同步功能 ├── lib/ // 可能放一些自定义或修改过的第三方库 ├── include/ // 全局配置文件如WiFi密码、服务器地址、引脚定义 │ └── config.h └── platformio.ini // PlatformIO项目配置文件在config.h中集中管理所有配置方便修改// WiFi配置 const char* WIFI_SSID Your_WiFi_SSID; const char* WIFI_PASS Your_WiFi_Password; // 网络收音机服务器配置 const char* AUDIO_STREAM_URL http://your-stream-server:port/stream; // NTP服务器配置 const char* NTP_SERVER pool.ntp.org; const long GMT_OFFSET_SEC 8 * 3600; // 东八区 const int DAYLIGHT_OFFSET_SEC 0; // 硬件引脚定义 #define PIN_I2C_SDA 8 #define PIN_I2C_SCL 9 #define PIN_BTN_MODE 38 // ... 其他引脚4. 核心功能模块实现详解有了硬件和软件框架接下来就是逐个击破功能模块。每个模块的实现都包含了一些值得细说的逻辑和技巧。4.1 FM收音机模块自动搜台与存储FM功能的核心是初始化RDA5807模块、执行自动搜台、将搜到的电台频率存入数组并提供切换和播放功能。初始化与搜台流程// 在 radio_fm.cpp 中 bool RadioFM::begin() { Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL); // 初始化I2C if (!rx.begin(Wire)) { // rx是RDA5807库的实例 Serial.println(FM Module not found!); return false; } rx.setVolume(10); // 设置初始音量0-15 rx.setMono(false); // 设置为立体声 return true; } void RadioFM::scanStations() { uint16_t currentFreq 8700; // 起始频率87.0MHz (以10kHz为单位即8700) uint8_t stationIndex 0; memset(stationList, 0, sizeof(stationList)); // 清空旧列表 rx.setFrequency(currentFreq); delay(100); // 等待频率稳定 for (int i 0; i 200; i) { // 搜索87.0MHz到108.0MHz if (rx.getRssi() 20 rx.isStereo()) { // 信号强度20且为立体声认为是有效电台 // 精细调谐在当前频率点附近小范围寻找最强信号点 uint16_t fineFreq fineTune(currentFreq); stationList[stationIndex] fineFreq; Serial.printf(Found station at %.1f MHz\n, fineFreq / 100.0); if (stationIndex MAX_STATIONS) break; // 跳转到下一个可能频点避免相邻强台干扰 currentFreq 300; // 跳转300kHz (30个单位) } else { currentFreq 10; // 步进10kHz继续搜索 } rx.setFrequency(currentFreq); delay(50); // 每次调频后短暂延迟 } totalStations stationIndex; }关键点解析频率单位RDA5807库通常以10kHz为单位。8700代表87.00MHz。搜台策略简单的逐点扫描步进10kHz太慢。我的策略是当找到一个有效电台后直接跳过300kHzcurrentFreq 300因为FM电台频率间隔通常至少0.2MHz200kHz这样可以大幅加快搜台速度。信号判别通过getRssi()获取信号强度isStereo()判断是否为立体声。强度阈值如20需要根据实际环境和天线调整。精细调谐fineTune()函数是我自己加的。因为在粗略找到电台后频率可能不在信号最强的中心点。这个函数会在找到的频率附近如±50kHz微调寻找RSSI最大的点确保收听效果最佳。电台存储与切换搜到的频率会存入数组stationList。通过按键切换时只需从数组中按索引取出频率调用rx.setFrequency()即可。4.2 网络收音机模块Wi-Fi连接与音频流播放这是项目的技术难点涉及网络连接、流媒体协议解析和音频解码输出。网络连接与音频流处理// 在 radio_network.cpp 中 bool RadioNetwork::connectWiFi() { WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASS); Serial.print(Connecting to WiFi); int attempts 0; while (WiFi.status() ! WL_CONNECTED attempts 30) { // 超时约15秒 delay(500); Serial.print(.); attempts; } if (WiFi.status() WL_CONNECTED) { Serial.println(\nConnected! IP: WiFi.localIP().toString()); return true; } else { Serial.println(\nConnection failed!); return false; } } void RadioNetwork::playStream() { if (!audioClient.connect(AUDIO_STREAM_URL)) { // audioClient是Audio库的实例 Serial.println(Failed to connect to stream server); return; } // 配置I2S音频输出参数 audioClient.setI2SCommFMT_LSB(); // 数据格式 audioClient.setPinout(PIN_I2S_BCLK, PIN_I2S_LRC, PIN_I2S_DOUT); audioClient.setVolume(10); // 设置音量 // 开始播放流媒体。库内部会处理HTTP连接、数据接收和解码。 audioClient.begin(AUDIO_STREAM_URL, AUDIO_OUTPUT_I2S); }关键点解析Wi-Fi连接稳定性加入了超时重试机制避免因网络问题卡死在连接过程中。在实际产品中还需要考虑连接失败后的处理逻辑比如进入配网模式SmartConfig或Web配网。音频库的选择与使用我使用了ESP32-audioI2S库即Audio库。它封装了HTTP/HTTPS流媒体接收、MP3/AAC解码和I2S输出等一系列复杂操作我们只需调用begin()并传入流媒体URL即可。它支持断线重连、缓冲管理非常强大。流媒体服务器你需要一个提供MP3或AAC格式音频流的网络电台URL。格式通常是http://server:port/path。注意ESP32的内存有限无法处理高码率如320kbps的流一般128kbps以下的MP3流比较稳定。I2S输出配置确保I2S的引脚BCLK, LRC, DOUT与硬件连接一致并且数据格式LSB或MSB与外接DAC芯片如PCM5102A的要求匹配。4.3 显示与用户界面设计显示部分负责将系统状态当前模式、FM频率/网络电台信息、时间直观地呈现出来。我使用了U8g2库它支持多种字体和图形绘制。多页面显示管理// 在 display.cpp 中 void DisplayManager::showPage(PageType page) { u8g2.clearBuffer(); // 清除显示缓冲区 switch(page) { case PAGE_FM: drawFMPage(); break; case PAGE_NETWORK: drawNetworkPage(); break; case PAGE_CLOCK: drawClockPage(); break; case PAGE_LOGO: drawLogo(); break; } u8g2.sendBuffer(); // 将缓冲区内容发送到屏幕显示 } void DisplayManager::drawFMPage() { u8g2.setFont(u8g2_font_6x10_tf); // 设置字体 u8g2.drawStr(0, 12, FM Radio); // 绘制频率如“98.5 MHz” char freqStr[20]; sprintf(freqStr, %.1f MHz, currentFreq / 100.0); u8g2.setFont(u8g2_font_10x20_tf); // 大字体显示频率 u8g2.drawStr(centerX(freqStr), 40, freqStr); // centerX是居中对齐函数 // 绘制信号强度条 drawSignalStrength(rssi); }UI设计要点页面切换流畅性在主循环中根据currentPage变量调用showPage()。切换页面时先清空缓冲区再绘制避免残影。信息布局清晰每个页面只显示最关键的信息。FM页面突出频率和信号强度网络页面显示IP和流媒体状态时钟页面清晰显示年月日时分秒。避免频繁刷新OLED屏幕频繁全屏刷新会影响寿命且可能闪烁。我只在信息发生变化如频率改变、时间跳秒时才更新对应的显示区域而不是整个屏幕。4.4 按键交互与系统状态管理四个按键构成了整个设备的控制核心。我需要实现按键去抖、长短按识别并管理一个清晰的系统状态机。按键扫描与去抖// 在 input.cpp 中 ButtonState InputManager::readButton(uint8_t pin) { int reading digitalRead(pin); if (reading ! lastButtonState[pin]) { lastDebounceTime[pin] millis(); } if ((millis() - lastDebounceTime[pin]) DEBOUNCE_DELAY) { if (reading ! buttonState[pin]) { buttonState[pin] reading; if (buttonState[pin] LOW) { return BTN_PRESSED; } else { return BTN_RELEASED; } } } lastButtonState[pin] reading; return BTN_NONE; }系统状态机 系统的核心状态变量包括currentMode:FM_MODE,NETWORK_MODE,CLOCK_MODE。fmStationIndex: 当前FM电台在列表中的索引。volume: 全局音量。在主循环中不断扫描按键并根据currentMode执行不同的响应逻辑void loop() { ButtonState btn1 input.readButton(PIN_BTN_MODE); ButtonState btn2 input.readButton(PIN_BTN_SWITCH); if (btn1 BTN_PRESSED) { // 短按模式键切换 FM - NETWORK - CLOCK - FM... currentMode (currentMode 1) % 3; display.showPage(getPageFromMode(currentMode)); // 切换到新模式时执行相应初始化如停止之前的音频播放 switchMode(currentMode); } if (btn2 BTN_PRESSED currentMode FM_MODE) { // 在FM模式下切换键用于换台 fmStationIndex (fmStationIndex 1) % totalStations; radioFM.setFrequency(stationList[fmStationIndex]); display.updateFMFrequency(...); } // ... 处理音量键等其他按键 // 更新时钟显示每秒一次 updateClockDisplay(); }这种基于状态机的设计使得代码逻辑清晰各功能模块解耦易于维护和扩展。5. 系统集成、调试与已知问题解决将各个模块组合在一起后真正的挑战才开始让它们稳定、协调地工作。这个过程中遇到了几个典型问题。5.1 编译与烧录优化如项目笔记所述在Windows上使用Arduino或VS Code编译ESP32项目速度很慢。我的解决方案是使用PlatformIOPlatformIO的编译缓存机制比原生Arduino IDE更高效二次编译速度显著提升。迁移到Linux子系统WSL或纯Linux这是效果最明显的一步。在WSL2中安装PlatformIO编译速度相比Windows原生环境有数倍的提升因为Linux的文件系统性能更优。对于ESP32这种需要编译大量核心库和框架的项目开发体验改善巨大。优化platformio.ini配置在配置文件中可以设置构建标志例如只对当前项目文件进行编译避免每次重新编译所有库。[env:esp32-s2-saola-1] platform espressif32 board esp32-s2-saola-1 framework arduino monitor_speed 115200 build_flags -Wl,-gc-sections # 链接时移除未使用的代码段 build_unflags -Os # 可以尝试替换为 -O2 以获得更优性能但体积稍大 lib_deps olikraus/U8g2^2.34.2 athombv/RDA5807^1.0.7 schreibfaul1/ESP32-audioI2S^2.0.75.2 运行时问题与修复项目原文列出了几个已知BUG我在实现过程中也遇到了类似问题并找到了解决方案上电Wi-Fi/时间服务器连接失败卡Logo问题初始化时如果Wi-Fi连接或NTP对时失败程序会阻塞在while循环中。解决为连接操作增加超时机制。例如Wi-Fi连接尝试30秒后若仍未成功则跳过网络部分进入离线模式仅FM功能可用。同时在屏幕上显示错误状态如“WiFi Fail”让用户知晓。NTP对时失败则使用ESP32的RTC维持一个粗略的时间。bool connectWiFiWithTimeout(int timeoutSeconds) { WiFi.begin(ssid, password); unsigned long start millis(); while (WiFi.status() ! WL_CONNECTED (millis() - start) timeoutSeconds * 1000) { delay(500); Serial.print(.); } return (WiFi.status() WL_CONNECTED); }按键长按连发与界面乱跳问题机械按键在按下时会产生抖动长按则会被识别为多次快速按下。解决硬件去抖在按键两端并联一个0.1uF的电容可以滤除大部分毛刺。软件去抖如上文所述采用状态检测法只有电平稳定超过一定时间如50ms才认为是一次有效的按键事件。长按与短按区分记录按键按下的时长。如果按下时间超过500ms则触发“长按”事件例如长按模式键进入设置菜单否则触发“短按”事件。这需要更精细的状态机来跟踪每个按键的PRESSED、HOLD、RELEASED状态。FM音频与网络音频输出干扰问题即使软件上停止了网络音频播放FM的声音有时仍会从I2S DAC的路径中微弱输出反之亦然。解决这是硬件上的信号串扰或软件控制不彻底。软件上在切换模式时不仅要在应用层停止播放如调用audioClient.stop()最好还能关闭对应的硬件外设。对于I2S可以调用i2s_driver_uninstall()来彻底释放资源。切换到FM时确保I2S DAC被静音或关闭。硬件上在音频切换处如使用模拟开关芯片进行物理隔离是最彻底的方案。例如使用一颗CD4066模拟开关由MCU的一个GPIO控制来选择是FM模块的音频信号还是I2S DAC的信号通向最终的功放。5.3 功耗考量与便携化改进原项目笔记提到了增加电池和蓝牙的改进方向这里展开说一下低功耗设计Wi-Fi休眠在纯FM模式下可以彻底关闭Wi-Fi模块WiFi.mode(WIFI_OFF)。CPU降频在时钟显示等非音视频处理场景可以调用setCpuFrequencyMhz(80)将ESP32-S2的CPU频率从240MHz降至80MHz显著降低功耗。屏幕控制OLED屏幕是耗电大户。长时间不操作时可以降低亮度或进入睡眠模式U8g2库支持setPowerSave(1)。优化电源电路选用高效率的DC-DC降压芯片如SY8089替代传统的LDO如AMS1117特别是在电池供电下能大幅提升续航。增加蓝牙音频可以集成一颗蓝牙音频接收模块如JDY-31支持BLE音频通过UART与ESP32-S2通信。这样设备就变成了“FM收音机网络收音机蓝牙音箱”三合一产品。软件上需要增加一个蓝牙控制模式并处理来自UART的蓝牙模块状态和音频控制指令。6. 项目总结与进阶思考回顾这个ESP32-S2双模收音机项目它成功地将传统的FM接收与现代的网络流媒体播放结合在一起提供了一个兼具怀旧感和科技感的音频终端。从技术层面它综合运用了MCU编程、外设驱动I2C, I2S、网络通信、实时音频处理和基本的UI设计是一个非常好的嵌入式系统综合实践案例。我个人在完成这个项目后最深的体会是嵌入式开发中硬件与软件的协同调试能力往往比单纯编写代码更重要。比如FM搜台阈值的设定、I2S时钟的配置、按键去抖参数的调整都需要在真实的硬件上反复测试才能找到最优值。另外对内存和性能的持续关注是保证ESP32这类资源受限设备稳定运行的关键。例如网络音频缓冲区的设置、全局变量和栈空间的使用情况都需要仔细权衡。这个项目还有很大的扩展空间加入音频均衡器EQESP32-S2的处理器性能足以在软件层面实现一个多段均衡器让用户自定义音效。实现录音功能将收听到的节目录制到SD卡中。开发手机App配网与控制通过BLE或Wi-Fi Web Server让用户更方便地配置网络和收藏电台。外壳与产品化设计一个精美的3D打印外壳将电路板、电池、扬声器集成进去就是一个完整的桌面小产品。最后所有的项目源码、原理图、PCB设计文件如果后续画了板子以及详细的搭建指南我都已经开源。希望这份详尽的开发笔记能为你启动自己的智能硬件项目提供一块坚实的垫脚石。嵌入式开发的乐趣正是在于这种从想法到实物的完整创造过程。如果在复现过程中遇到任何问题欢迎在项目仓库的Issue中讨论。