FEATURED · 精选文章

基于Wio Terminal的嵌入式MP3播放器开发:从硬件连接到软件解码全解析

发布时间 / 2026/8/19 13:57:46
来源 / 创域科博编辑部
栏目 / 资讯中心
基于Wio Terminal的嵌入式MP3播放器开发:从硬件连接到软件解码全解析 1. 项目概述当开发板遇上音乐Wio Terminal的MP3之旅几年前当我第一次拿到Seeed Studio的Wio Terminal时就被它那块2.4英寸的彩色LCD屏幕和内置的传感器给吸引了。这玩意儿本质上是一个基于ATSAMD51微控制器的开发板但官方给它配了个漂亮的壳让它看起来像个成品设备。当时我就在想除了做数据监控、物联网网关这些“正经”用途能不能用它干点更有趣的事比如做一个便携式的MP3播放器。这个想法其实挺自然的。Wio Terminal本身有TF卡槽可以存储音乐文件有扬声器接口通过Grove接口扩展可以输出音频有屏幕可以显示播放信息还有按键和摇杆能实现播放控制。硬件基础几乎是现成的缺的只是一个能把它们整合起来的软件。市面上当然有现成的MP3模块但直接用开发板解码播放更像是一个“从零造轮子”的挑战能让你深入理解音频解码、文件系统、实时控制这些嵌入式开发的核心概念。所以这个“Wio Terminal MP3 Player”项目就是一次软硬件的深度整合实践。它不仅仅是为了播放音乐更是为了探索如何在资源有限的微控制器上实现一个需要实时处理、内存管理和用户交互的完整应用。无论你是想学习嵌入式音频开发还是想为你的创客项目添加一点音乐元素这个项目都能提供一条清晰的路径。接下来我会拆解整个实现过程从硬件连接到软件解码从界面绘制到功能优化分享我踩过的坑和总结的技巧。2. 核心硬件选型与电路设计思路2.1 Wio Terminal的音频能力剖析Wio Terminal本身并不直接包含一个高质量的音频编解码芯片Codec。它的音频输出能力依赖于其微控制器MCU的I2S接口和数模转换器DAC。ATSAMD51芯片内置了一个I2SInter-IC Sound控制器这是一种专门用于传输数字音频数据的串行通信协议。同时芯片也包含一个真正的10位DAC数模转换器可以将数字信号转换为模拟电压信号。然而直接使用MCU的DAC输出音频存在两个主要限制一是输出驱动能力很弱无法直接推动扬声器二是没有功率放大音量会非常小。因此我们需要一个外部音频放大器。幸运的是Wio Terminal的设计者考虑到了这一点在板子的右侧有一个专用的“Grove - I2S Amp 3W”接口。这是一个标准的Grove四针接口VCC, GND, SDA, SCL但在这里被重定义为I2S信号线BCLK, LRCLK, DIN和电源。你可以直接插入Seeed Studio的Grove - I2S Audio Amp模块这是一个集成了MAX98357A I2S Class D放大器的微型板能提供3W的驱动功率完美匹配我们的需求。注意Wio Terminal的耳机插孔3.5mm是直接连接到这个I2S Amp接口的。当你插入耳机时音频信号会自动路由到耳机插孔并断开与外部放大器的连接。这是一个非常贴心的硬件设计。除了音频存储是关键。Wio Terminal有一个microSDTF卡槽支持SPI模式访问。我们需要将MP3文件存放在SD卡中。对于显示和交互2.4英寸的LCD320x240分辨率和五个物理按键三个在顶部一个摇杆按键一个复位键以及一个模拟摇杆构成了完美的用户界面基础。2.2 外围电路连接方案对于这个项目最简洁的硬件连接方案如下音频放大器将Grove - I2S Audio Amp模块直接插入Wio Terminal右侧标有“I2S Amp”的Grove接口。无需任何焊接或跳线。该模块自带一个微型扬声器连接座你可以焊接一个4Ω或8Ω的扬声器。存储设备将一张格式化为FAT32文件系统的microSD卡存入你的MP3文件然后插入Wio Terminal底部的卡槽。供电通过Wio Terminal的USB Type-C接口供电。在播放时尤其是推动扬声器电流消耗可能达到200-300mA建议使用能提供5V/1A以上的电源适配器或电脑USB口。这就是全部了。硬件连接极其简单这也是Wio Terminal作为一体化开发平台的优势。如果你的扬声器功率较大或者想获得更好的音质可以考虑以下增强方案使用更强大的功放你可以通过杜邦线将Wio Terminal的I2S信号对应引脚引出连接到其他I2S功放板比如PAM8403、PAM8302等这些功放通常有更高的输出功率或不同的接口。添加音频解码芯片虽然我们使用软件解码MP3但如果你追求极低的MCU占用率可以考虑使用VS1053、VS1003这类硬件解码芯片。这时你需要通过SPI将Wio Terminal与解码芯片连接解码芯片的模拟输出再接到功放。这会让项目复杂很多但能解放MCU去处理更复杂的图形或网络任务。对于大多数入门和中等需求方案一Grove I2S Amp完全足够。它稳定、简单且与硬件兼容性最好。3. 软件架构与核心库解析3.1 开发环境与核心库选择Wio Terminal支持Arduino框架和CircuitPython开发。考虑到性能MP3解码是计算密集型任务和社区资源Arduino框架是更合适的选择。你需要安装Arduino IDE或VS Code with PlatformIO并添加Seeed Studio的板支持包。实现MP3播放器的核心在于两个库Seeed_Arduino_FS这是Seeed为Wio Terminal优化的文件系统库用于访问SD卡。它比标准的SD库更稳定对长文件名支持更好。Seeed_Arduino_MP3这是一个至关重要的库它封装了Helix MP3解码器。Helix是一个开源的、高度优化的MP3解码器专为嵌入式系统设计它可以在像ATSAMD51这样的ARM Cortex-M4芯片上实时解码标准比特率的MP3文件。这个库提供了简单的API来初始化解码器、馈送数据并获取解码后的PCM音频样本。除了这两个核心库我们还会用到TFT_eSPI这是驱动Wio Terminal屏幕的事实标准库图形绘制功能强大。Seeed_Arduino_RTC用于获取当前时间可以为播放列表或文件添加时间戳可选。Button2一个优秀的按键处理库能简化摇杆和物理按键的消抖和事件处理。3.2 播放器软件工作流设计整个播放器的软件逻辑是一个典型的状态机围绕“读取文件 - 解码数据 - 输出音频”这个核心流水线展开。下面是大致的工作流程初始化初始化LCD屏幕绘制初始界面如播放器名称、等待提示。初始化文件系统SD.begin()并扫描指定目录如/MP3下的所有.mp3文件构建一个播放列表文件路径数组。初始化I2S音频输出。设置正确的采样率通常为44100Hz、位深16位和I2S引脚。初始化MP3解码器告诉它我们将使用I2S输出。初始化按键和摇杆绑定回调函数如播放/暂停、上一曲/下一曲、音量加减。主循环播放状态检查用户输入处理按键事件改变播放状态播放、暂停、停止、切换曲目或调节音量。如果状态是“播放” a. 从SD卡中读取一块MP3数据例如512字节或1024字节到缓冲区。不能一次读太多会占用大量内存也不能读太少会导致解码器饥饿。 b. 将这块MP3数据送入Seeed_Arduino_MP3库的解码函数。 c. 解码器内部工作将压缩的MP3数据解压为原始的PCM音频数据。 d. 解码器通过回调函数或直接写入的方式将PCM数据推送到I2S接口I2S控制器则自动将数据发送给外部放大器。重复这个过程直到当前文件结束或用户中断。用户界面更新在主循环中还需要定期比如每100毫秒更新屏幕显示。显示的信息包括当前播放的歌曲名、歌手可从ID3标签解析但较复杂通常只显示文件名、播放进度条、当前时间/总时长、音量图标等。进度条的计算需要知道文件总大小和当前读取的位置而总时长在MP3解码开始后才能从文件头信息中获取比特率 * 文件大小。这是一个需要注意的细节。这个流程的关键在于实时性。从SD卡读取、MP3解码、I2S数据传输这些操作必须在音频缓冲区清空之前完成否则就会产生卡顿或爆音。ATSAMD51120MHz的性能足以应对标准比特率128kbps的MP3解码但代码必须高效避免在音频输出关键路径上进行耗时的操作如复杂的屏幕绘制、文件系统遍历。4. 核心功能实现与代码详解4.1 MP3解码与I2S音频输出集成这是项目的核心引擎。首先在代码中引入必要的库并定义对象#include Seeed_Arduino_FS.h #include Seeed_Arduino_MP3.h #include TFT_eSPI.h #include Audio.h // Arduino的Audio库用于I2S设置 TFT_eSPI tft; Audio audio; // 使用Arduino Audio库管理I2S MP3 mp3; // MP3解码器对象 File currentFile;初始化阶段最关键的是正确设置I2S和MP3解码器void setup() { Serial.begin(115200); tft.init(); tft.setRotation(3); // 1. 初始化SD卡 if (!SD.begin(SDCARD_SS_PIN, SDCARD_SPI)) { tft.println(SD Card Mount Failed!); return; } // 2. 初始化I2S音频输出 audio.setPinout(BCLK, LRCLK, DIN); // 这些引脚在Wio Terminal上已内部连接至I2S Amp接口 audio.setVolume(10); // 初始音量范围0-21 audio.i2s_mclk_pin_select(I2S_MCLK); // 设置主时钟引脚如果需要 // 3. 初始化MP3解码器并关联输出回调 mp3.begin(); mp3.setOutput(audio); // 告诉解码器将解码后的PCM数据输出到audio对象管理的I2S流 }播放一个MP3文件的函数可能如下所示void playFile(const char* path) { if (currentFile) currentFile.close(); currentFile SD.open(path); if (!currentFile) { Serial.println(Failed to open file); return; } mp3.play(currentFile); // 开始解码播放。这是一个非阻塞调用解码在后台进行。 isPlaying true; }在loop()函数中你需要持续喂数据给解码器。但Seeed_Arduino_MP3库的play()方法通常内部已经处理了文件读取和解码循环你只需要定期调用mp3.loop()来处理后台任务和检查状态void loop() { mp3.loop(); // 必须定期调用以维持解码和播放 // 处理用户按键... // 更新UI... }实操心得mp3.loop()的调用频率至关重要。如果调用太慢音频缓冲区会下溢导致声音中断。最好在主循环中无条件且尽快地调用它。避免在mp3.loop()调用前执行可能阻塞的长时间操作如复杂的文件遍历或屏幕绘制。可以将UI更新放在一个由定时器控制的任务中与音频解码循环解耦。4.2 用户界面与交互设计实现一个友好的UI是播放器的灵魂。我们将利用TFT_eSPI库来绘制界面。界面可以划分为几个区域顶部状态栏显示项目标题“Wio MP3 Player”或当前时间。中部信息区显示歌曲名文件名、艺术家/专辑可预留、频谱动画高级功能或大封面图如果解析了专辑封面。底部控制区绘制播放/暂停按钮图标、上一曲/下一曲图标、进度条和音量条。由于Wio Terminal没有触摸屏所有交互通过顶部按键和摇杆完成。我们可以定义如下映射顶部按键1最左播放/暂停。顶部按键2中停止或切换播放模式如单曲循环/列表循环。顶部按键3最右返回/菜单在简单播放器中可能不用。摇杆上/下音量增加/减少。摇杆左/右上一曲/下一曲。摇杆按下可能用于确认选择或显示隐藏菜单。使用Button2库可以优雅地处理这些输入#include Button2.h Button2 btnPlayPause, btnStop, btnMenu; Button2 joystick; void setup() { // ... btnPlayPause.begin(TOP_BUTTON1_PIN); btnPlayPause.setTapHandler([](Button2 b) { if (isPlaying) { mp3.pause(); isPlaying false; } else { mp3.resume(); isPlaying true; } drawPlayPauseIcon(); // 更新UI图标 }); joystick.begin(JOYSTICK_PIN); // 摇杆通常是一个模拟引脚需要特殊处理 // 对于摇杆我们需要在loop中读取模拟值来判断方向 } void loop() { // ... btnPlayPause.loop(); btnStop.loop(); // 读取摇杆模拟值并判断方向 handleJoystick(); }绘制进度条是一个经典问题。你需要知道文件的总时长和当前播放位置。MP3解码库有时会提供getAudioCurrentTime()和getAudioFileDuration()这样的函数。如果没有你可以估算当前进度秒 ≈ (当前文件位置 / 文件总大小) * 总时长秒总时长可以从MP3文件的比特率和文件大小计算出来但这需要解析MP3帧头比较复杂。一个更简单但粗糙的方法是直接使用文件读取的进度。绘制时用tft.drawRect画一个背景框再根据进度比例用tft.fillRect画一个填充矩形。4.3 文件系统管理与播放列表构建一个基本的播放器需要能遍历SD卡上的MP3文件。我们通常将音乐文件放在一个特定的文件夹比如/MP3。String playlist[100]; // 假设最多100首歌 int playlistSize 0; int currentTrackIndex 0; void buildPlaylist(const char* directoryPath) { File dir SD.open(directoryPath); if (!dir || !dir.isDirectory()) { Serial.println(Failed to open directory); return; } playlistSize 0; File file dir.openNextFile(); while (file playlistSize 100) { if (!file.isDirectory()) { String filename file.name(); if (filename.endsWith(.mp3) || filename.endsWith(.MP3)) { // 存储完整路径 playlist[playlistSize] String(directoryPath) / filename; playlistSize; } } file dir.openNextFile(); } dir.close(); }构建好播放列表后切换曲目就很简单了void playNext() { currentTrackIndex (currentTrackIndex 1) % playlistSize; playFile(playlist[currentTrackIndex].c_str()); updateSongInfoOnScreen(playlist[currentTrackIndex]); } void playPrevious() { currentTrackIndex (currentTrackIndex - 1 playlistSize) % playlistSize; playFile(playlist[currentTrackIndex].c_str()); updateSongInfoOnScreen(playlist[currentTrackIndex]); }注意事项文件系统操作openNextFile相对较慢尤其是在SD卡上有大量文件时。不要在音频播放的关键路径比如每解码一帧就检查一次进行文件遍历。应该在启动时一次性构建好播放列表并将其保存在内存中。同时确保文件名路径不要过长以免导致字符串操作内存溢出。5. 性能优化与高级功能探索5.1 内存管理与缓冲区优化在嵌入式系统中内存是宝贵资源。Wio Terminal的ATSAMD51有192KB的RAM但系统、库和屏幕缓冲区会占用一部分。MP3解码和音频播放需要缓冲区MP3输入缓冲区用于存放从SD卡读取的原始压缩数据。通常8KB-16KB足够。PCM输出缓冲区解码后的PCM数据在送入I2S前的缓冲区。I2S通常使用DMA直接内存访问传输所以需要设置DMA缓冲区。Audio库和Seeed_Arduino_MP3库内部会管理这些缓冲区。你需要关注全局变量和栈的使用。避免在函数内定义大型数组如char str[256]这可能导致栈溢出。尽量使用动态内存malloc要谨慎因为容易产生内存碎片。对于已知大小的数据如播放列表使用全局或静态数组是更安全的选择。双缓冲区技术为了更流畅的音频播放可以考虑实现双缓冲区机制。一个缓冲区用于当前I2S DMA传输另一个缓冲区用于填充解码器新产出的PCM数据。当DMA传输完一个缓冲区后立即切换到另一个已填满的缓冲区同时解码器开始填充刚刚被传输完的空缓冲区。这能有效防止因解码或读取速度波动导致的音频中断。不过Seeed_Arduino_MP3和Audio库的底层实现可能已经采用了类似技术我们需要先验证是否仍有卡顿再决定是否深入优化。5.2 音效与音频处理可能性基础的播放实现了但音质可能比较“干”。我们可以探索一些简单的音频处理来增强体验软件音量控制除了I2S放大器自带的硬件音量控制我们可以在软件中对PCM数据进行缩放来实现更精细的音量调节。例如在将PCM样本送入I2S之前将每个16位样本乘以一个0.0到1.0之间的系数。但要注意过大的缩放可能导致裁剪失真超出-32768到32767的范围。int16_t applyVolume(int16_t sample, float volumeFactor) { int32_t scaled (int32_t)sample * volumeFactor; if (scaled 32767) scaled 32767; if (scaled -32768) scaled -32768; return (int16_t)scaled; }均衡器EQ实现一个多段软件均衡器是可能的但计算量较大。一个简化的方法是实现一个低音增强bass boost通过一个简单的低通滤波器来提升低频分量。这需要数字信号处理DSP知识如实现一个二阶IIR滤波器。对于120MHz的M4内核处理单声道的简单滤波器是可行的。频谱显示在屏幕上显示音乐频谱是视觉效果的一大提升。这需要对一小段PCM数据进行快速傅里叶变换FFT。Arduino上有一些轻量级的FFT库如arduinoFFT。你可以采集256或512个样本进行FFT计算然后将不同频率区间的幅度映射为屏幕上柱状图的高度。注意FFT计算量较大需要合理安排计算时机避免影响音频播放的实时性。最好在独立的低优先级任务或定时器中断中执行。5.3 扩展功能设想网络电台与蓝牙音频让播放器连接网络或蓝牙可玩性会大大增加。网络电台Wi-FiWio Terminal内置了Realtek RTL8720DN Wi-Fi/蓝牙双模芯片。你可以使用WiFi库连接网络然后通过HTTP或HTTPS协议从网络电台流媒体服务器获取音频流。网络流通常是MP3、AAC或OGG格式。你需要一个支持流媒体解码的库。Seeed_Arduino_MP3库可能支持从网络流解码或者你需要寻找其他支持流式解码的库如Audio库的audio.connecttohost()功能。这涉及到网络缓冲、流协议解析等更复杂的问题。蓝牙音频接收A2DP Sink将Wio Terminal变成一个蓝牙音箱接收手机或电脑的音频流。这需要实现A2DP高级音频分发配置文件Sink角色。Realtek芯片支持此功能但Arduino生态下的相关库和支持可能还不完善。你可能需要深入研究RTL8720DN的SDK或者使用MicroPython/Python等更高级语言的环境那里可能有更成熟的蓝牙音频库。这些高级功能每一个都可以作为一个独立的进阶项目。从本地MP3播放器起步逐步添加这些功能是学习嵌入式系统开发的绝佳路径。6. 常见问题排查与调试心得6.1 编译与上传问题库依赖冲突最常见的错误是库版本不兼容。确保你使用的是与Wio Terminal板包兼容的Seeed_Arduino_FS和Seeed_Arduino_MP3库版本。建议通过Arduino库管理器或Seeed的官方GitHub仓库获取最新稳定版。内存不足编译失败如果代码太大可能会遇到“region FLASH overflowed”的错误。尝试在platformio.ini如果使用PlatformIO中启用编译优化build_flags -Os。移除不用的库引用和代码。检查是否将大量字符串常量存储在Flash中而非RAM中使用F()宏如Serial.println(F(Hello))。上传失败确保选择了正确的板型Seeed Wio Terminal和端口。如果之前上传过CircuitPython可能需要先通过双击复位按钮进入bootloader模式屏幕显示“Arduino”或磁盘驱动器。6.2 运行时问题无声、卡顿、爆音问题现象可能原因排查步骤与解决方案完全没声音1. 硬件连接错误或接触不良。2. 音量设置为0或静音。3. I2S初始化失败。4. 扬声器损坏或未正确连接。1. 检查Grove I2S Amp模块是否插紧扬声器线是否焊牢。2. 在代码中检查并调高audio.setVolume()的值。3. 在setup()中检查I2S初始化函数的返回值并添加调试输出。4. 用耳机插入Wio Terminal的3.5mm口测试判断是功放问题还是源头问题。播放卡顿、断断续续1. SD卡读取速度慢或文件系统碎片多。2. MP3解码耗时过长导致音频缓冲区欠载。3. 主循环中有阻塞性操作如复杂图形绘制、文件遍历。4. 电源供电不足导致芯片降频。1. 换用Class 10或更高速度的SD卡并将MP3文件复制到卡上而非在卡内频繁移动删除。2. 尝试播放更低比特率如96kbps的MP3文件测试。3. 优化代码确保mp3.loop()调用频率最高将UI更新放在非关键路径使用millis()进行非阻塞延时。4. 使用可靠的5V/2A电源适配器供电避免使用老旧的电脑USB口。有爆音或杂音1. I2S时钟配置错误采样率、位深不匹配。2. 音频缓冲区设置过小。3. 电源噪声。4. 扬声器或功放质量问题。1. 确认代码中设置的采样率如44100与MP3文件的采样率一致。Audio库的setI2SConfig函数可以检查。2. 尝试在Audio库初始化时增加I2S缓冲区大小如果库支持配置。3. 为电源添加滤波电容在VCC和GND之间并联一个100uF电解电容和一个0.1uF陶瓷电容尽量让电源走线远离模拟音频部分。4. 更换扬声器或功放模块测试。只能播放一首歌无法切换1. 播放列表构建错误或切换逻辑有bug。2. 文件关闭/打开逻辑错误导致资源未释放。3. SD卡在播放过程中被意外卸载。1. 在切换歌曲时通过串口打印出playlist[currentTrackIndex]确认路径正确。2. 确保在打开新文件前调用currentFile.close()关闭旧文件。3. 避免在loop()中频繁调用SD.begin()初始化一次即可。确保SD卡插稳。6.3 屏幕显示与用户交互问题屏幕闪烁或残影频繁全屏刷新会导致闪烁。只刷新需要更新的区域局部刷新。TFT_eSPI库的setAddrWindow和pushColors函数可以用于高效更新局部区域。对于进度条可以只重画填充部分变化的矩形而不是整个进度条背景。按键响应不灵或连击这是机械按键的抖动问题。Button2库内部已经做了消抖处理。如果还有问题可以调整库的消抖时间参数setDebounceTime。对于摇杆因为是模拟输入需要设置一个死区dead zone比如中心值±10以内的变化视为无效防止轻微触碰导致的误触发。文件名显示乱码或截断如果MP3文件名包含中文或特殊字符可能需要处理编码问题。Arduino环境通常使用UTF-8但SD卡文件系统可能因操作系统而异。一个简单的方法是避免使用非ASCII字符命名文件。对于显示TFT_eSPI库的字体可能不支持中文需要加载中文字库但这会占用大量Flash空间。调试时串口打印是你的好朋友。在各个关键节点如文件打开成功/失败、解码开始/结束、按键触发添加Serial.print语句可以快速定位问题所在。当系统稳定后再移除或禁用这些调试输出以提升性能。7. 项目总结与迭代方向做完这个项目我最深的体会是嵌入式开发是一个在严格约束下寻求平衡的艺术。Wio Terminal的MP3播放器项目正好体现了这种平衡有限的CPU算力、有限的内存、有限的电源却要完成文件I/O、实时音频解码、图形渲染和用户交互等多个任务。任何一个环节处理不当都会直接反映在用户体验上——卡顿、爆音或是界面迟钝。从技术实现上看成功的关键在于理解并尊重各个子系统的时序和资源需求。音频播放是最高优先级的实时任务必须保证数据流的连续性。因此任何可能阻塞音频线程的操作如复杂的屏幕绘制、低效的文件查找都必须被拆解、优化或移到低优先级任务中。使用状态机来管理播放流程而非简单的线性代码让程序结构更清晰也更容易应对各种中断用户按键。这个基础版本已经是一个功能完整的播放器了但它还有巨大的进化潜力。我个人认为接下来可以沿着这几个方向迭代UI/UX深化实现一个真正的图形化菜单系统支持按文件夹浏览、创建播放列表、收藏歌曲。可以借鉴MP3播放器经典的九宫格或列表界面。元数据支持目前只显示了文件名。可以集成一个轻量级的ID3标签解析库在屏幕上显示歌曲名、艺术家、专辑信息甚至从文件中提取专辑封面JPEG并显示。这需要额外的解析和图像解码开销。低功耗优化如果想让其成为真正的便携设备需要研究休眠模式。在无操作一段时间后关闭屏幕、暂停解码让MCU进入低功耗休眠状态通过按键中断唤醒。这需要精细地管理外设时钟和电源。硬件扩展为其设计一个3D打印的外壳集成更大的电池、更好的扬声器单元甚至添加一个小型OLED作为副屏显示状态。硬件上的“完工度”会让项目成就感倍增。最后一个小技巧是关于SD卡的。我发现有些廉价的SD卡在SPI模式下的兼容性和稳定性不佳会导致随机读取错误或初始化失败。如果你的播放器偶尔出现无法读卡或播放中断第一个怀疑对象就是SD卡。换一张品牌可靠、速度等级为Class10或UHS-I的卡问题往往迎刃而解。这看似是个小细节但在嵌入式项目中存储设备的可靠性是系统稳定的基石。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻