ESP32驱动GC9A01圆形屏实现模拟VU表:从音频采集到图形渲染全解析

📅 2026/8/19 2:06:24
ESP32驱动GC9A01圆形屏实现模拟VU表:从音频采集到图形渲染全解析
1. 项目概述当复古指针遇上数字核心最近在捣鼓一个挺有意思的小玩意儿用ESP32驱动一块圆形的GC9A01显示屏来模拟一个经典的模拟VU表。VU表就是那种在旧式录音设备、功放上常见的随着音乐节奏左右摆动的指针表头它那种机械的、带点延迟的摆动感是数字进度条永远给不了的“味道”。这个项目的核心就是把这种模拟的“灵魂”注入到一块全彩的圆形数字屏幕里。听起来可能有点“多此一举”——明明有现成的LED频谱灯条为啥还要在屏幕上画个指针但玩硬件的乐趣就在于此不是为了解决一个刚需而是为了复现一种感觉探索一种可能性。ESP32作为主控提供了强大的计算和Wi-Fi/蓝牙能力而GC9A01这块圆形IPS屏240x240的分辨率、鲜艳的色彩和极高的刷新率是呈现平滑指针动画的绝佳画布。组合起来它不仅能作为一个酷炫的桌面音频可视化摆件更能作为一个深入学习SPI通信、图形渲染、音频处理以及ESP32双核任务管理的综合性练手项目。无论你是刚接触ESP32和Arduino想找一个有视觉反馈、成就感强的项目入门还是已经玩腻了点亮LED想挑战一下更复杂的图形和实时处理这个“模拟风格VU表”都能给你带来从硬件连接到软件调试再到算法优化的完整体验。接下来我就把自己从零搭建、调试到优化的全过程包括踩过的坑和总结的技巧详细拆解一遍。2. 硬件选型与核心思路解析2.1 为什么是ESP32 GC9A01这个组合不是随便选的背后有很强的功能匹配和性价比考量。首先看主控ESP32。我们需要一个能实时处理音频数据、驱动显示屏并流畅渲染动画的芯片。ESP32的双核Xtensa处理器通常一核运行Wi-Fi/蓝牙协议栈另一核处理用户程序为多任务并行提供了基础。更重要的是它内置了高速SPI和I2S接口。SPI用来驱动屏幕要求高速的数据吞吐以保证刷新率I2S则可以直接连接数字麦克风如INMP441或音频解码芯片如MAX98357获取高质量的音频流这是实现精准VU值计算的前提。相比之下普通的Arduino Uno在速度和接口上就捉襟见肘了。然后是显示屏GC9A01。市面上圆形屏不少但GC9A01有几个突出优点一是驱动芯片性能强支持最高SPI时钟频率配合ESP32的80MHz主频SPI能轻松实现60fps以上的全屏刷新这对于指针的丝滑移动至关重要二是它的IPS面板视角广、色彩好显示效果远超普通的TN屏三是它的分辨率是240x240对于绘制一个带刻度的圆形表盘来说像素密度足够细节可以很丰富。选择圆形屏而非方形就是为了完美还原传统VU表的视觉形态减少“黑边”沉浸感更强。2.2 系统架构与数据流设计整个系统的工作流可以清晰地分为几个阶段理解这个数据流对后续编程和调试至关重要。音频采集音频信号输入是源头。有两种主流方案数字麦克风如INMP441通过I2S接口直接输出数字音频流无需额外的ADC抗干扰能力强适合采集环境声音。ESP32的I2S外设可以配置为接收模式以固定的采样率如44.1kHz和位宽如16位读取数据。模拟麦克风ADC使用ESP32内置的ADC衰减后电压范围0-3.3V读取模拟麦克风的信号。这种方式成本低但易受电源噪声干扰且ESP32的ADC在高速采样时线性度一般需要仔细校准。 本项目为了追求更好的效果和简化电路更推荐使用I2S数字麦克风方案。信号处理与VU值计算获取到原始的PCM音频数据后不能直接用来驱动指针。需要经过处理预处理可能包括去除直流偏移、加窗函数如汉宁窗以减少频谱泄漏。计算幅度对于时域信号VU表反映的是声音的平均功率或准峰值。一个经典方法是计算一段时间内如50ms音频样本的均方根值。更简单的方法是取绝对值的平均值。这个计算出的数值就是当前音频的“强度”。映射与滤波计算出的强度值是一个变化的数字需要映射到屏幕指针的摆动角度例如-40dB到0dB对应屏幕上的-30度到30度。直接映射会导致指针抖动剧烈毫无“模拟感”。因此必须加入一个低通滤波器让指针的移动具有惯性和阻尼感。通常用一个一阶IIR滤波器来实现filtered_value alpha * new_value (1 - alpha) * filtered_value。alpha值越小惯性越大指针摆动越缓慢、平滑。图形渲染这是最体现视觉效果的一环。在GC9A01上我们需要绘制两部分静态背景表盘、刻度线、数字标签。这部分只在初始化时绘制一次或当设置改变时重绘以节省CPU时间。动态指针根据滤波后的VU值实时计算指针角度并绘制。这里的关键是双缓冲或局部刷新。如果每帧都重绘整个圆盘效率低下。高效的做法是只擦除上一帧指针所在的一个扇形区域或线段然后在新位置上绘制新的指针。这需要一些图形学计算但能极大提升帧率。任务调度在ESP32上合理的任务划分能充分利用双核。可以设计两个主要任务音频处理任务运行在一个核心上高优先级阻塞式地读取I2S数据进行VU值计算和滤波。显示刷新任务运行在另一个核心上以固定的频率如30Hz或60Hz读取滤波后的VU值并更新屏幕显示。 两个任务之间通过线程安全的队列如FreeRTOS的Queue或全局变量配合信号量来传递数据。注意GC9A01屏幕的驱动电压通常是3.3V与ESP32逻辑电平完美匹配。但它的背光可能需要单独的供电有些模块集成了背光驱动电路。接线时务必确认模块的引脚定义尤其是复位RST和命令/数据选择DC引脚它们必须连接到ESP32的GPIO上而不仅仅是电源。3. 核心细节解析与实操要点3.1 GC9A01圆形屏的驱动与初始化GC9A01虽然性能不错但初始化序列相对固定且稍显复杂。市面上常见的Arduino驱动库如TFT_eSPI通常已经支持但我们需要针对圆形屏和SPI高速模式进行特别配置。首先你需要一个支持GC9A01的图形库。TFT_eSPI库是首选它高度优化支持ESP32的硬件SPI并且可以通过用户配置文件灵活适配不同屏幕。关键配置步骤在TFT_eSPI的User_Setup.h文件中选择驱动芯片取消注释#define GC9A01_DRIVER。定义屏幕尺寸设置#define TFT_WIDTH 240和#define TFT_HEIGHT 240。配置ESP32引脚这是最容易出错的地方。你需要根据你的模块和接线定义以下引脚#define TFT_MOSI 23 // SPI数据输出 #define TFT_SCLK 18 // SPI时钟 #define TFT_CS 5 // 片选低电平有效 #define TFT_DC 2 // 数据/命令选择 #define TFT_RST 4 // 硬件复位如果模块有复位引脚且已接则定义否则置为-1使用软件复位。 #define TFT_BL 12 // 背光控制引脚可选注意TFT_MOSI和TFT_SCLK必须使用ESP32指定的SPI引脚VSPI默认是GPIO 23和18。CS、DC、RST可以自由选择任意GPIO。启用硬件SPI确保#define USE_HSPI_PORT被定义这样库会使用ESP32的HSPI即VSPI硬件外设速度远超软件模拟SPI。优化SPI时钟在代码中初始化后可以尝试提高SPI频率。但要注意过高的频率可能导致屏幕花屏或通信失败这与布线质量有关。通常可以设置到40MHz甚至80MHz。#include TFT_eSPI.h TFT_eSPI tft TFT_eSPI(); void setup() { tft.init(); tft.setRotation(0); // 根据屏幕方向调整 SPI.beginTransaction(SPISettings(80000000, MSBFIRST, SPI_MODE0)); // 尝试80MHz // ... 其他初始化 }圆形区域处理TFT_eSPI库提供了drawCircle和fillCircle函数但为了高效绘制表盘和实现指针的局部刷新我们更需要掌握setClipRect函数。通过将裁剪区域设置为一个围绕指针运动范围的小矩形或扇形可以确保绘图操作只影响屏幕的特定部分这是实现高帧率动态效果的关键技巧。3.2 音频输入与VU算法实现如前所述我们以I2S数字麦克风INMP441为例。硬件连接INMP441的SCK(BCLK),WS(LRCLK),SD(DOUT) 分别连接ESP32的GPIO 26, 25, 33这些是I2S的常用引脚也可自定义。注意INMP441需要3.3V供电且L/R引脚通常接地选择左声道输出。软件配置使用Arduino的I2S库#include driver/i2s.h #define I2S_SAMPLE_RATE 44100 #define I2S_BUFFER_SIZE 1024 // 缓冲区大小 int16_t i2s_buffer[I2S_BUFFER_SIZE]; // 16位音频数据缓冲区 void setup_audio() { i2s_config_t i2s_config { .mode (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX), .sample_rate I2S_SAMPLE_RATE, .bits_per_sample I2S_BITS_PER_SAMPLE_16BIT, .channel_format I2S_CHANNEL_FMT_ONLY_LEFT, // 单声道 .communication_format I2S_COMM_FORMAT_STAND_I2S, .intr_alloc_flags ESP_INTR_FLAG_LEVEL1, .dma_buf_count 4, .dma_buf_len I2S_BUFFER_SIZE, .use_apll false, .tx_desc_auto_clear false, .fixed_mclk 0 }; i2s_pin_config_t pin_config { .bck_io_num 26, .ws_io_num 25, .data_out_num I2S_PIN_NO_CHANGE, .data_in_num 33 }; i2s_driver_install(I2S_NUM_0, i2s_config, 0, NULL); i2s_set_pin(I2S_NUM_0, pin_config); }VU值计算与滤波算法 在loop或一个独立任务中不断读取I2S数据并计算。float vu_value_filtered 0.0; // 滤波后的值 float alpha 0.1; // 滤波系数越小越平滑但延迟越大。0.05-0.2是常用范围。 void calculate_vu() { size_t bytes_read; i2s_read(I2S_NUM_0, (void*)i2s_buffer, sizeof(i2s_buffer), bytes_read, portMAX_DELAY); int samples_read bytes_read / sizeof(int16_t); long sum 0; for (int i 0; i samples_read; i) { // 取绝对值并累加计算平均幅度 sum abs(i2s_buffer[i]); } float vu_raw (float)sum / samples_read; // 原始平均幅度 vu_raw vu_raw / 32768.0; // 归一化到[0, 1]假设16位有符号数 // 应用一阶低通滤波这是模拟感的核心 vu_value_filtered alpha * vu_raw (1 - alpha) * vu_value_filtered; }这个vu_value_filtered就是经过平滑处理、带有“惯性”的VU值可以直接用于驱动指针。3.3 表盘与指针的图形学绘制绘制一个美观的模拟表盘需要一些基本的几何计算。绘制静态表盘 在setup()中一次性绘制。包括同心圆、刻度线和文字。void draw_dial() { tft.fillScreen(TFT_BLACK); int center_x 120; int center_y 120; int radius 110; // 1. 绘制外圆和内圆 tft.drawCircle(center_x, center_y, radius, TFT_WHITE); tft.drawCircle(center_x, center_y, radius - 10, TFT_DARKGREY); // 2. 绘制刻度线 float start_angle -150; // 假设表盘从-30度到30度对应-150度到-30度数学极坐标 float end_angle -30; for (int db -40; db 0; db 5) { // 从-40dB到0dB float angle map(db, -40, 0, start_angle, end_angle); float rad angle * PI / 180.0; // 计算刻度线起点和终点 int x1 center_x (radius - 15) * cos(rad); int y1 center_y (radius - 15) * sin(rad); int x2 center_x (radius - 5) * cos(rad); int y2 center_y (radius - 5) * sin(rad); tft.drawLine(x1, y1, x2, y2, TFT_WHITE); // 每10dB添加数字标签 if (db % 10 0) { int tx center_x (radius - 25) * cos(rad); int ty center_y (radius - 25) * sin(rad); tft.setTextColor(TFT_WHITE); tft.drawNumber(db, tx - 10, ty - 5, 2); // 使用合适的字体 } } }绘制动态指针局部刷新 这是性能关键。我们需要记录上一帧指针的位置在绘制新指针前先擦除旧指针。float last_angle -150; // 上一帧指针角度 void draw_needle(float vu_normalized) { // vu_normalized 是[0,1]的滤波后值 int center_x 120; int center_y 120; int needle_len 90; // 1. 将VU值映射到角度例如0 - -150度 1 - -30度 float new_angle map_f(vu_normalized, 0, 1, -150, -30); // 自定义的浮点数map函数 // 2. 计算新旧指针的端点坐标 float last_rad last_angle * PI / 180.0; int last_x center_x needle_len * cos(last_rad); int last_y center_y needle_len * sin(last_rad); float new_rad new_angle * PI / 180.0; int new_x center_x needle_len * cos(new_rad); int new_y center_y needle_len * sin(new_rad); // 3. 设置裁剪区为包含新旧指针的一个稍大矩形避免全屏刷新 int min_x min(center_x, min(last_x, new_x)) - 2; int min_y min(center_y, min(last_y, new_y)) - 2; int max_x max(center_x, max(last_x, new_x)) 2; int max_y max(center_y, max(last_y, new_y)) 2; tft.setClipRect(min_x, min_y, max_x - min_x 1, max_y - min_y 1); // 4. 用背景色黑色重绘裁剪区内的表盘部分这里简化直接填充黑色矩形会覆盖部分背景但速度快 // 更精细的做法是重绘该区域内的背景图形。这里为简单起见填充矩形。 tft.fillRect(min_x, min_y, max_x-min_x1, max_y-min_y1, TFT_BLACK); // 5. 在裁剪区内绘制新指针 tft.drawLine(center_x, center_y, new_x, new_y, TFT_RED); // 画指针 tft.fillCircle(center_x, center_y, 5, TFT_SILVER); // 画指针轴心 // 6. 取消裁剪区 tft.setClipRect(0, 0, 0, 0); // 传入0,0,0,0取消裁剪 // 7. 更新角度记录 last_angle new_angle; }实操心得setClipRect是提升帧率的利器但使用不当会导致图形错乱。务必确保在绘制完局部内容后取消裁剪。另外对于复杂的背景局部擦除后需要重绘的背景部分也更复杂可能需要预先将背景的局部区域保存为位图或者更精细地计算需要重绘的图形元素。对于这个VU表指针运动范围有限用填充矩形覆盖一个稍大区域是简单有效的折中方案。4. 实操过程与核心环节实现4.1 完整代码框架与双核任务设计将上述模块组合起来并利用FreeRTOS实现双核任务调度可以获得更稳定的性能。#include TFT_eSPI.h #include driver/i2s.h // 硬件引脚定义 #define I2S_BCLK 26 #define I2S_LRC 25 #define I2S_DIN 33 // 全局变量 TFT_eSPI tft; float vu_filtered 0.0; const float alpha 0.08; float needle_angle -150.0; SemaphoreHandle_t xMutex; // 用于保护共享变量vu_filtered // 音频采集任务运行在核心0 void audioTask(void *parameter) { const i2s_port_t i2s_num I2S_NUM_0; const int buffer_size 1024; int16_t *buffer (int16_t*)malloc(buffer_size * sizeof(int16_t)); while(1) { size_t bytes_read; i2s_read(i2s_num, buffer, buffer_size * sizeof(int16_t), bytes_read, portMAX_DELAY); int samples bytes_read / sizeof(int16_t); long sum 0; for(int i0; isamples; i) { sum abs(buffer[i]); } float vu_raw (float)sum / samples / 32768.0; // 使用互斥锁更新共享变量 xSemaphoreTake(xMutex, portMAX_DELAY); vu_filtered alpha * vu_raw (1 - alpha) * vu_filtered; xSemaphoreGive(xMutex); // 任务延时控制采样处理周期 vTaskDelay(10 / portTICK_PERIOD_MS); // 约100Hz } free(buffer); vTaskDelete(NULL); } // 显示刷新任务运行在核心1 void displayTask(void *parameter) { float last_angle -150.0; int center_x tft.width() / 2; int center_y tft.height() / 2; int needle_len 90; // 绘制静态表盘仅一次 draw_dial(); while(1) { float current_vu; xSemaphoreTake(xMutex, portMAX_DELAY); current_vu vu_filtered; xSemaphoreGive(xMutex); // 映射VU值到角度并限制范围 float target_angle map_f(current_vu, 0.0, 1.0, -150.0, -30.0); target_angle constrain(target_angle, -150.0, -30.0); // 调用局部刷新函数绘制指针 draw_needle_local(target_angle, last_angle, center_x, center_y, needle_len); last_angle target_angle; // 控制刷新率30-60Hz为宜 vTaskDelay(20 / portTICK_PERIOD_MS); // 50Hz } vTaskDelete(NULL); } void setup() { Serial.begin(115200); tft.init(); tft.setRotation(0); tft.fillScreen(TFT_BLACK); // 初始化I2S setup_i2s(); // 创建互斥锁 xMutex xSemaphoreCreateMutex(); // 创建任务 xTaskCreatePinnedToCore( audioTask, // 任务函数 AudioTask, // 任务名 4096, // 栈大小 NULL, // 参数 2, // 优先级高于显示任务 NULL, // 任务句柄 0 // 运行在核心0 ); xTaskCreatePinnedToCore( displayTask, DisplayTask, 4096, NULL, 1, // 优先级略低 NULL, 1 // 运行在核心1 ); // 删除默认的loop任务 vTaskDelete(NULL); } void loop() { // FreeRTOS任务调度器已接管loop为空 }这个框架清晰地将音频处理和显示渲染解耦分别运行在不同的核心上通过互斥锁安全地共享VU数据。音频任务以较高优先级运行确保数据采集的实时性显示任务以固定频率刷新保证动画流畅。4.2 性能优化与视觉增强技巧当基础功能实现后可以进一步优化体验。SPI DMA传输TFT_eSPI库在ESP32上默认使用了SPI的DMA直接内存访问功能这已经极大地解放了CPU。我们无需手动干预但确保在User_Setup.h中#define ESP32_DMA被启用即可。帧率与平滑度的权衡显示任务的延迟vTaskDelay决定了帧率。帧率太高如100Hz可能增加不必要的CPU负载且受限于SPI写入速度帧率太低如20Hz则动画卡顿。30-60Hz是甜点区。滤波系数alpha同样影响“模拟感”。alpha大如0.3指针响应快但抖动alpha小如0.05指针平滑但延迟明显。需要根据音频类型和个人喜好调整。视觉增强指针样式不止画一条线。可以画一个三角形或添加一个“尾翼”使其更像真实的指针。// 绘制一个三角形指针 void draw_fancy_needle(float angle, int cx, int cy, int len) { float rad angle * PI / 180.0; int tip_x cx len * cos(rad); int tip_y cy len * sin(rad); // 计算指针两侧的偏移点形成宽度 float perp_rad rad PI/2; int width 3; int dx width * cos(perp_rad); int dy width * sin(perp_rad); int base_x1 cx 5 * cos(rad); // 指针根部内缩一点 int base_y1 cy 5 * sin(rad); // 填充三角形 tft.fillTriangle(tip_x, tip_y, base_x1dx, base_y1dy, base_x1-dx, base_y1-dy, TFT_RED); }峰值保持模拟一些高级VU表可以增加一个峰值指示条短暂停留在最高VU值处然后缓慢回落。颜色渐变根据VU值改变指针或背景颜色如绿色安全区黄色警告区红色过载区。抗锯齿TFT_eSPI库支持一些基本抗锯齿。对于斜线启用#define SMOOTH_FONT或使用库的drawLine函数本身有一定优化比直接操作像素更平滑。5. 常见问题与排查技巧实录在制作过程中你几乎一定会遇到下面这些问题。这里是我的排查记录和解决方案。5.1 屏幕无显示或花屏这是最常见的问题90%以上是接线或配置错误。排查清单电源确保屏幕的VCC和GND正确连接且电源能提供足够电流通常需200mA以上。尝试单独给屏幕供电。引脚连接再三核对MOSI,SCK,CS,DC,RST的接线是否与代码定义一致。RST引脚如果未使用在代码中要设置为-1。SPI频率过高的SPI频率可能导致数据错误。尝试在初始化时降低SPI时钟例如从80MHz降到40MHz或20MHz。SPISettings(40000000, MSBFIRST, SPI_MODE0)。初始化顺序确保在tft.init()之前SPI总线已经正确开始。有些库内部会处理但检查无妨。屏幕型号确认你的屏幕驱动芯片确实是GC9A01而非ST7789或ILI9341。错误的驱动定义会导致初始化失败。一个快速测试方法编写一个最简单的程序只做tft.init()和tft.fillScreen(TFT_RED)。如果屏幕能显示纯红色说明硬件连接和基础驱动是好的问题出在后续的绘图代码或任务调度上。5.2 指针跳动剧烈毫无“模拟感”这通常是VU值计算或滤波环节出了问题。原因1原始音频数据噪声大。如果是模拟麦克风检查供电是否稳定尝试在麦克风输出端加一个简单的RC低通滤波电路如1kΩ电阻串联0.1uF电容对地。如果是数字麦克风检查I2S接线是否远离干扰源。原因2滤波系数alpha太大。alpha越接近1滤波效果越弱。尝试将其减小到0.05或0.02观察指针是否会变得过于“迟钝”在响应速度和平滑度之间找到平衡点。原因3计算VU值的窗口太短。如果你每次只处理几十个样本就计算一次平均值结果自然会波动很大。确保你累计足够多的样本对应至少20-50ms的音频时长再进行计算。例如44.1kHz采样率下50ms对应2205个样本。调试技巧通过串口打印出vu_raw和vu_filtered的值。观察vu_raw是否本身就在剧烈跳动。如果是问题在采集端如果vu_raw相对平稳而vu_filtered跳动则检查滤波计算代码是否有误。5.3 动画卡顿帧率低下表现为指针移动不连贯有跳跃感。首要原因局部刷新未生效或效率低。确认你的draw_needle函数确实使用了setClipRect来限制绘制区域并且裁剪区大小合理。用millis()函数计算两次刷新之间的时间打印出实际帧率。如果远低于预期如低于20Hz检查是否在循环中不小心调用了重绘整个背景的函数如draw_dial()。SPI时钟是否设置得太低。图形操作如画线、填充是否过于复杂。任务阻塞如果音频处理任务计算量巨大例如做了FFT可能会长时间占用CPU导致显示任务无法及时执行。确保音频任务中有适当的vTaskDelay或使用事件驱动如I2S DMA中断来释放CPU。内存碎片在长时间运行后如果频繁动态分配内存如malloc可能导致内存碎片化影响性能。对于实时音频缓冲区尽量使用静态数组或一次分配、重复使用的内存池。5.4 音频输入无反应或声音失真I2S无数据使用i2s_read并检查bytes_read。如果始终为0检查I2S引脚配置是否正确。麦克风模块是否需要特定初始化命令INMP441通常不需要。I2S驱动是否成功安装i2s_driver_install返回值。声音失真破音** clipping削顶**原始音频信号幅度超过ADC或I2S接收范围。检查麦克风增益是否过高或者音频源音量过大。在代码中对读取的样本值进行限幅处理。采样率不匹配确保I2S配置的采样率与麦克风模块支持的采样率一致。INMP441通常支持多种采样率但需要正确的MCLK主时钟频率ESP32的I2S的APLL可以生成精确的时钟尝试启用use_apll true。电源噪声这是模拟输入方案的常见问题。为模拟部分使用线性稳压器LDO单独供电并增加滤波电容。5.5 ESP32重启或看门狗超时看门狗WDT超时FreeRTOS任务如果长时间阻塞而不释放CPU例如在任务中执行while(1)死循环而没有延时会触发看门狗定时器重启。确保每个任务中都有vTaskDelay或等待信号量、队列等阻塞操作。堆栈溢出任务分配的堆栈大小4096可能不足特别是如果使用了较大的局部数组或递归。如果遇到随机重启尝试增大堆栈大小或使用uxTaskGetStackHighWaterMark函数监控堆栈使用情况。内存不足GC9A01的帧缓冲区如果使用全屏2402402字节115200字节会占用大量内存。TFT_eSPI库默认使用部分缓冲区或直接写屏模式。如果启用了全缓冲区确保ESP32的可用内存足够选择PSRAM型号或优化内存使用。这个项目从硬件连接到软件调试涵盖了嵌入式开发的多个核心环节。当看到那个红色的指针随着音乐的节奏平滑而又有力地在精致的圆形表盘上摆动时所有的调试和折腾都值了。它不仅仅是一个复刻的仪表更是你对实时系统、数字信号处理和计算机图形学一次生动的实践。你可以在此基础上继续扩展比如加入Wi-Fi连接让它显示网络电台的音频或者增加触摸功能来调整参数乐趣无穷。