树莓派Pico驱动电子墨水屏:SPI通信协议与低功耗显示实战 1. 项目概述当Pico遇上电子墨水屏最近在捣鼓一个桌面小摆件想让它能显示点天气、日程或者名言警句之类的信息但又不想让它像个普通屏幕一样一直亮着耗电还刺眼。这时候电子墨水屏e-Paper就成了不二之选。它只在刷新内容时耗电显示静态内容时完全零功耗而且视觉上接近纸质印刷在阳光下也清晰可见简直是信息展示的绝配。我手头正好有一块树莓派 Pico 和一块 2.7 英寸的三色电子墨水屏模块。Pico 以其双核 RP2040 芯片、丰富的 GPIO 和极低的功耗在嵌入式开发圈子里备受欢迎。而这块 2.7 寸的 e-Paper 屏分辨率是 264x176支持黑、白、红三色显示尺寸小巧很适合做桌面显示终端。把它们俩结合起来的核心任务就是通过 SPI 接口让 Pico 去驱动这块屏幕。SPISerial Peripheral Interface是一种高速、全双工、同步的串行通信总线协议在驱动显示屏、存储器、传感器等外设时非常常见。它比 I2C 速度更快比 UART 连线更规范是连接微控制器和这类显示模块的“标准语言”。这个项目的核心就是理解并实现这套“语言”的对话规则让 Pico 能准确无误地把我们想要的图像数据“画”到墨水屏上。2. 核心硬件与通信协议解析2.1 硬件连接与引脚定义首先得把硬件连起来。这块 2.7 寸 e-Paper 模块通常采用标准的 SPI 接口并需要一些额外的控制引脚。以常见的 Waveshare 或类似模块为例其引脚定义和与 Pico 的连接方式如下e-Paper 引脚Pico GPIO 引脚功能说明VCCVSYS 或 3V3(OUT)电源正极接 3.3VGNDGND电源地DINGPIO 11 (SPI0 TX)SPI 数据输入主出从入MOSICLKGPIO 10 (SPI0 SCK)SPI 时钟信号CSGPIO 9片选信号低电平有效DCGPIO 8数据/命令选择高电平数据低电平命令RSTGPIO 12复位引脚低电平复位BUSYGPIO 13忙状态指示高电平/低电平表示忙依具体屏而定注意这里的 GPIO 编号是 Pico 的物理引脚编号对应 RP2040 的 GP11、GP10 等。SPI0 的默认引脚是 GP10(SCK), GP11(MOSI), GP12(CSn)但为了灵活性我们通常将 CS、DC、RST、BUSY 映射到其他通用 GPIO 上。务必根据你屏幕的具体型号和数据手册确认 BUSY 引脚的电平逻辑这点非常关键否则程序会卡在等待屏幕就绪的循环里。连接时建议使用杜邦线在面包板上进行或者直接焊接一个简单的转接板确保连接牢固。电源一定要稳定可以单独从 Pico 的 3.3V 输出引脚取电。2.2 SPI 通信协议深度剖析驱动 e-Paper 屏本质上是 Pico 作为 SPI 主机Master向作为从机Slave的屏幕控制器发送一系列命令和数据。这里涉及几个关键概念1. SPI 模式 (Mode)SPI 有四种模式由时钟极性CPOL和时钟相位CPHA决定。CPOL 决定空闲时时钟线的电平0低电平1高电平CPHA 决定数据在时钟的哪个边沿被采样0第一个边沿1第二个边沿。大多数 e-Paper 屏控制器如 SSD1675、IL0373工作在Mode 0即 CPOL0, CPHA0。这意味着时钟空闲时为低电平。数据在时钟的上升沿被采样捕获在下降沿发生变化。在 Pico 的 SDK 中初始化 SPI 时需要正确配置这个参数。2. 片选 (CS)、数据/命令选择 (DC) 与忙信号 (BUSY)CS (Chip Select)当我们需要与屏幕通信时先将此引脚拉低通信结束后再拉高。它告诉屏幕“现在主机要和你说话了”。DC (Data/Command)这是 e-Paper 驱动中的一个关键引脚。屏幕控制器需要区分接收到的是命令用于设置寄存器、启动刷新等还是显示数据实际的像素点阵。DC 拉低表示发送的是命令字节拉高表示发送的是数据字节。一次通信中可能需要频繁切换 DC 引脚的状态。BUSY电子墨水屏在执行内部刷新、温度传感等操作时需要一定时间几十毫秒到几秒不等。在此期间它会通过 BUSY 引脚告知主机“我正忙别打扰”。主程序必须检测这个信号并等待否则后续发送的数据可能会丢失。有些屏是忙时为高电平有些是忙时为低电平。3. 数据帧与字节顺序SPI 通信以字节为单位。对于单色或三色屏每个像素通常用若干位bit来表示。例如对于黑、白、红三色屏可能用 2 个 bit 表示一个像素00为白色01为黑色10为红色。屏幕的驱动芯片会规定数据的传输顺序是高位MSB先送还是低位LSB先送以及一帧图像的数据是按行扫描还是按列扫描。这些信息都在屏幕的规格书或驱动芯片数据手册里必须严格遵守。3. 驱动程序设计思路与关键函数3.1 初始化流程唤醒沉睡的屏幕驱动 e-Paper 的第一步是进行正确的硬件和软件初始化。这个过程就像给屏幕做一次“开机体检”和“参数设置”。void epd_init(void) { // 1. 初始化GPIO gpio_init(EPD_RST_PIN); gpio_set_dir(EPD_RST_PIN, GPIO_OUT); gpio_init(EPD_DC_PIN); gpio_set_dir(EPD_DC_PIN, GPIO_OUT); gpio_init(EPD_CS_PIN); gpio_set_dir(EPD_CS_PIN, GPIO_OUT); gpio_init(EPD_BUSY_PIN); gpio_set_dir(EPD_BUSY_PIN, GPIO_IN); // 2. 初始化SPI接口 spi_init(SPI_PORT, 4000 * 1000); // 初始化SPI设置波特率4MHz gpio_set_function(EPD_SCK_PIN, GPIO_FUNC_SPI); gpio_set_function(EPD_MOSI_PIN, GPIO_FUNC_SPI); spi_set_format(SPI_PORT, 8, SPI_CPOL_0, SPI_CPHA_0, SPI_MSB_FIRST); // 3. 硬件复位 gpio_put(EPD_RST_PIN, 0); sleep_ms(10); gpio_put(EPD_RST_PIN, 1); sleep_ms(10); // 4. 发送一系列初始化命令序列 epd_send_command(0x12); // 软复位 epd_busy_wait(); epd_send_command(0x01); // 驱动器输出控制 epd_send_data(0x27); epd_send_data(0x01); epd_send_data(0x00); // ... 更多初始化命令具体取决于屏幕型号 epd_send_command(0x11); // 数据输入模式 epd_send_data(0x03); epd_send_command(0x3C); // 边框波形控制 epd_send_data(0x05); // ... 设置完毕 }关键点解析复位操作硬件复位拉低 RST是必须的它能确保屏幕控制器从一个已知的、确定的状态开始工作。命令序列初始化命令序列是屏幕厂商提供的“启动密码”顺序和参数都不能错。这些命令通常包括设置分辨率、扫描方式、电源配置、波形参数LUT查找表等。波形参数尤其重要它决定了像素从一种颜色切换到另一种颜色时所施加电压的时序和大小直接影响刷新效果和残影控制。忙等待在发送某些关键命令如软复位、开始刷新后必须调用epd_busy_wait()函数等待屏幕内部操作完成。这个函数内部就是一个循环检测 BUSY 引脚状态的逻辑。3.2 核心通信函数与屏幕对话的基础所有与屏幕的交互都建立在两个最基本的函数上发送命令和发送数据。static void epd_send_command(uint8_t cmd) { gpio_put(EPD_DC_PIN, 0); // DC置低表示发送命令 gpio_put(EPD_CS_PIN, 0); // 片选拉低开始通信 spi_write_blocking(SPI_PORT, cmd, 1); gpio_put(EPD_CS_PIN, 1); // 片选拉高结束通信 } static void epd_send_data(uint8_t data) { gpio_put(EPD_DC_PIN, 1); // DC置高表示发送数据 gpio_put(EPD_CS_PIN, 0); spi_write_blocking(SPI_PORT, data, 1); gpio_put(EPD_CS_PIN, 1); }实操心得在调试初期我建议将这两个函数以及epd_busy_wait函数都加上调试输出通过串口打印记录每次发送的命令和数据。当屏幕没反应时这是排查通信是否成功建立的最直接手段。另外注意spi_write_blocking是阻塞式写入在发送大量图像数据时它会占用较长时间。对于更复杂的应用可以考虑使用 DMA直接存储器访问来传输数据解放 CPU。3.3 图像数据准备与传输把画面“画”上去这是最核心的部分。我们需要在内存中构建一个代表整个屏幕图像的缓冲区Frame Buffer然后将其发送给屏幕。1. 构建帧缓冲区对于 264x176 的三色屏假设每个像素用 2 bits 表示00白01黑10红11保留。那么一帧图像需要的总比特数是264 * 176 * 2 92928 bits换算成字节是92928 / 8 11616 字节。我们可以定义一个这么大的数组#define EPD_WIDTH 264 #define EPD_HEIGHT 176 #define EPD_BUFFER_SIZE ((EPD_WIDTH * EPD_HEIGHT) / 4) // 因为每像素2bit每字节存4个像素 uint8_t frame_buffer[EPD_BUFFER_SIZE];接下来我们需要编写函数来操作这个缓冲区比如设置某个像素点的颜色。void epd_set_pixel(int x, int y, uint8_t color) { if (x EPD_WIDTH || y EPD_HEIGHT || x 0 || y 0) return; // 计算该像素在缓冲区中的位置 uint32_t addr (x y * EPD_WIDTH) 2; // 除以4得到字节索引 uint8_t shift (x 0x3) 1; // 在一个字节内每像素占2bit计算偏移 (0, 2, 4, 6) // 先清空该像素原来的2个bit frame_buffer[addr] ~(0x03 shift); // 再设置新的颜色值 (color应为0, 1, 2) frame_buffer[addr] | ((color 0x03) shift); }2. 传输图像数据缓冲区准备好后通过 SPI 将其发送到屏幕。屏幕通常有专门的命令来启动数据写入和后续的刷新显示。void epd_display_frame(void) { // 1. 发送命令告知屏幕准备接收图像数据 epd_send_command(0x24); // 写入黑/白图像数据具体命令码需查手册 for (uint32_t i 0; i EPD_BUFFER_SIZE; i) { epd_send_data(frame_buffer[i]); } // 2. 如果有红色通道可能需要发送另一组数据对于三色屏 epd_send_command(0x26); // 写入红色图像数据 for (uint32_t i 0; i EPD_BUFFER_SIZE; i) { // 这里需要根据你的颜色编码规则从frame_buffer中提取或计算红色通道数据 uint8_t red_data extract_red_data(frame_buffer, i); epd_send_data(red_data); } // 3. 发送刷新显示命令 epd_send_command(0x22); epd_send_data(0xC7); // 触发全屏刷新 epd_send_command(0x20); epd_busy_wait(); // 等待刷新完成这是最耗时的步骤可能持续数秒 // 4. 刷新完成后发送休眠命令以省电 epd_send_command(0x10); epd_send_data(0x01); }注意事项电子墨水屏的全屏刷新速度很慢2-3秒是常事期间 BUSY 引脚会一直保持忙状态。刷新过程中切勿断电或强行复位否则可能导致屏幕损坏或出现永久性残影。对于局部刷新Partial Refresh虽然速度快但多次局部刷新后必须进行一次全刷否则残影会累积。具体支持哪种刷新模式需要查阅屏幕规格书。4. 高级功能实现与优化技巧4.1 字体与图形库的集成在帧缓冲区里直接操作像素点来画图或写字效率太低。我们需要引入字体和基本图形绘制函数。1. 使用位图字体我们可以将字体预先转换成位图格式。例如一个 8x16 像素的 ASCII 字符可以用 16 个字节表示每行一个字节。定义一个字体数组然后编写一个epd_draw_char函数将字符位图“贴”到帧缓冲区的指定位置。// 简化的8x16字体示例仅显示部分 const uint8_t font_8x16[][16] { {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 空格 {0x00, 0x00, 0x18, 0x3C, 0x3C, 0x3C, 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00}, // ! // ... 更多字符 }; void epd_draw_char(int x, int y, char c, uint8_t color) { uint8_t const *bitmap font_8x16[c - ]; // 假设数组从空格开始 for (int row 0; row 16; row) { uint8_t row_data bitmap[row]; for (int col 0; col 8; col) { if (row_data (0x80 col)) { // 判断每个bit是否为1即需要绘制的点 epd_set_pixel(x col, y row, color); } } } }基于此可以很容易地实现字符串绘制函数epd_draw_string。2. 基本图形绘制实现画线、画矩形、画圆等函数可以大大丰富显示内容。例如画线可以使用 Bresenham 算法这是一个仅使用整数加减法和位运算的高效算法非常适合在单片机上运行。void epd_draw_line(int x0, int y0, int x1, int y1, uint8_t color) { int dx abs(x1 - x0); int dy -abs(y1 - y0); int sx (x0 x1) ? 1 : -1; int sy (y0 y1) ? 1 : -1; int err dx dy; while (1) { epd_set_pixel(x0, y0, color); if (x0 x1 y0 y1) break; int e2 2 * err; if (e2 dy) { err dy; x0 sx; } if (e2 dx) { err dx; y0 sy; } } }4.2 多级灰度与抖动算法标准的黑白红三色屏只有两种或三种颜色。但通过抖动算法Dithering我们可以模拟出更多灰度层次让显示的图片看起来更细腻。例如Floyd-Steinberg 误差扩散算法是一种经典的抖动算法。其原理是将当前像素的量化误差实际灰度与输出二值之间的差值按一定比例分配到它右侧、右下、下方、左下的像素上从而在整体上保持平均灰度。在资源有限的 Pico 上运行完整的误差扩散算法可能比较吃力但我们可以预先在 PC 上对图片进行处理将处理后的二值位图数据直接存入 Pico 的 Flash 中或者使用更简单的有序抖动阈值矩阵算法在 Pico 上实时计算。4.3 低功耗与睡眠管理电子墨水屏最大的优势是静态显示不耗电但 Pico 本身仍在运行。为了制作一个真正的低功耗设备我们需要让 Pico 在刷新完屏幕后也进入睡眠状态。RP2040 支持多种睡眠模式。最简单的做法是在完成一次显示更新后让核心进入深度睡眠Dormant Mode并通过一个外部中断比如连接一个按键到某个 GPIO来唤醒。在深度睡眠下Pico 的功耗可以降到极低的水平。#include hardware/sleep.h void enter_deep_sleep(void) { // 1. 配置一个GPIO例如GPIO0作为唤醒源上升沿或下降沿触发 gpio_set_irq_enabled_with_callback(WAKE_PIN, GPIO_IRQ_EDGE_RISE, true, wake_handler); // 2. 保存必要状态如果有的话然后关闭外设 // ... // 3. 进入深度睡眠 sleep_run_from_xosc(); sleep_goto_dormant_until_edge_high(WAKE_PIN); // 等待指定引脚高电平唤醒 // 4. 唤醒后程序会从这里继续执行需要重新初始化外设 // ... }这样你的桌面显示终端就可以做到只在按下按钮更新内容时消耗能量其余时间几乎零功耗。5. 项目实战打造一个网络天气站有了驱动屏幕和显示图文的基础我们就可以做一个有趣的项目一个通过 WiFi 获取天气信息并显示的桌面站。5.1 系统架构设计这个项目需要Pico W因为普通的 Pico 没有无线功能我们需要使用 Pico W它集成了 Infineon CYW43439 WiFi 芯片。e-Paper 2.7显示屏作为输出设备。天气 API选择一个免费的天气服务提供商如 OpenWeatherMap。电源可以使用 USB 供电或者搭配一个小型锂电池和充电管理电路实现便携。工作流程是Pico W 上电后连接 WiFi向天气 API 发送 HTTP 请求解析返回的 JSON 数据提取温度、天气状况晴、雨等、图标代码等信息然后在本地将这些信息绘制成界面最后刷新到 e-Paper 屏幕上。更新完成后系统进入深度睡眠定时例如每半小时由 RTC 或简单的软件定时器唤醒重复上述过程。5.2 关键代码模块1. WiFi 连接与 HTTP 请求使用 Pico SDK 的cyw43-driver和lwIP库来实现网络功能。#include pico/cyw43_arch.h #include lwip/apps/http_client.h bool connect_to_wifi(const char *ssid, const char *pass) { if (cyw43_arch_init()) { printf(WiFi init failed\n); return false; } cyw43_arch_enable_sta_mode(); if (cyw43_arch_wifi_connect_timeout_ms(ssid, pass, CYW43_AUTH_WPA2_AES_PSK, 10000)) { printf(Failed to connect to WiFi\n); return false; } printf(Connected to WiFi\n); return true; } // 使用lwIP的httpcHTTP Client来发起请求获取JSON数据2. JSON 解析返回的天气数据是 JSON 格式。我们可以使用一个轻量级的 JSON 解析库如jsmn或cJSON虽然稍重但更易用。在 Pico 上需要根据内存情况谨慎选择。3. 信息整合与显示解析出数据后我们设计一个简单的界面顶部城市名称、日期时间可以从网络获取或使用 RTC。中部大型温度数字、天气状况文字描述。下部一个简单的天气图标用我们自己的图形函数绘制比如太阳、云朵、雨滴的轮廓以及湿度、风速等信息。绘制完成后调用epd_display_frame()刷新屏幕。5.3 优化与调试心得内存管理这个项目对内存RAM比较敏感。帧缓冲区11KB、网络缓冲区、JSON 解析缓冲区都会占用 RAM。Pico 只有 264KB RAM需要精打细算。可以考虑将部分数据如图标位图、字体存放在 Flash 中使用时再读取。连接稳定性在实际环境中WiFi 连接可能不稳定。代码中需要增加重试机制并处理连接失败的情况例如显示一个“无网络”的图标。API 调用频率免费的天气 API 通常有调用次数限制。不要设置太短的更新间隔如每分钟每 30 分钟或 1 小时更新一次完全足够也更省电。功耗实测用万用表测量整个系统在深度睡眠时的电流。理想情况下应低于 1mA。如果偏高检查是否有没有必要的外设如调试用的 LED仍在耗电。6. 常见问题与深度排查指南在驱动 e-Paper 屏的过程中你几乎一定会遇到下面这些问题。这里我把踩过的坑和解决方法整理出来。6.1 屏幕完全无反应一片空白这是最让人头疼的情况。请按照以下步骤系统排查检查硬件连接三遍这是最常见的原因。用万用表通断档逐根线检查从 Pico 到屏幕的每个引脚是否连通特别是 VCC 和 GND。确保电源电压是稳定的 3.3V。确认电源电流e-Paper 在刷新瞬间需要较大的电流可能超过 100mA。确保你的电源比如电脑 USB 口或充电头能提供足够的电流。Pico 的 3.3V 线性稳压器输出能力有限如果电流不足会导致屏幕复位或工作异常。验证 SPI 信号如果你有逻辑分析仪甚至示波器这是最好的调试工具。连接 SCK、MOSI、CS、DC 引脚抓取上电初始化阶段的波形。看 CS 和 DC它们是否在正确的时间点被拉低或拉高CS 应在每个字节传输前后有跳变。DC 应在发送命令和数据时正确切换。看 SCK 和 MOSISCK 是否有规律的时钟脉冲MOSI 上是否有随 SCK 变化的数据对照你代码里发送的命令码通常是十六进制看看波形上的数据是否一致。特别注意数据是在 SCK 的哪个边沿稳定的这决定了 SPI 模式CPHA是否正确。检查复位和忙信号RST用逻辑分析仪看上电后是否有一个从低到高的跳变低电平至少保持 10msBUSY在发送刷新命令后这个引脚的电平是否发生变化如果一直是固定电平可能是屏幕根本没收到命令或者屏幕本身已损坏。务必查阅数据手册确认 BUSY 的有效电平是“忙时为高”还是“忙时为低”你的等待函数逻辑写反了就会死循环。简化测试代码写一个最简单的测试程序只做三件事初始化 GPIO 和 SPI执行硬件复位然后发送一个最简单的命令比如读取屏幕 ID 的命令 0x71。通过逻辑分析仪观察这最基本的通信是否成功。6.2 屏幕有反应但显示乱码、错位或残影严重这说明通信基本通了但数据或时序有问题。帧缓冲区格式错误这是乱码和错位的首要怀疑对象。逐字节核对你的帧缓冲区数据格式是否与屏幕控制器要求完全一致。包括像素位顺序一个字节内的 8 个 bit是代表横向连续的 4 个像素每像素 2bit还是其他排列方式扫描方向数据是先从左到右填满第一行再第二行行扫描还是先从上到下填满第一列再第二列列扫描颜色映射你代码里00代表白色屏幕控制器是否也这样认为有时需要取反。缓冲区大小计算是否正确(宽度 * 高度 * 每像素位数) / 8。可以尝试发送一屏全黑或全白的数据来测试。初始化命令序列错误或不全不同批次的屏幕甚至同一型号不同厂商的屏幕其驱动芯片所需的初始化序列可能有细微差别。一定要找到与你手中屏幕完全匹配的示例代码或数据手册。一个命令或一个参数的差异就可能导致显示异常。刷新波形LUT问题残影严重特别是局部刷新后往往与查找表LUT设置有关。LUT 定义了从一种灰度到另一种灰度转换所需的电压脉冲序列。全刷和局刷的 LUT 不同。确保你使用的 LUT 数据是针对你这款屏幕、当前温度很多屏需要温度补偿和所需刷新模式全刷/局刷的。不要混用不同屏幕型号的 LUT 数据。供电不稳导致刷新中断在屏幕刷新BUSY 为高期间如果电源电压有大的跌落或毛刺可能导致刷新过程出错留下残影。确保电源线路有足够的去耦电容例如在屏幕的 VCC 和 GND 之间并联一个 100uF 的电解电容和一个 0.1uF 的陶瓷电容。6.3 刷新速度慢或刷新时系统卡死检查忙等待函数epd_busy_wait()函数是否实现正确是否因为电平逻辑弄反而陷入了无限循环在函数内部加一个超时机制是很好的做法比如循环等待最多 10 秒超时则报错退出避免永久卡死。void epd_busy_wait(void) { uint32_t timeout 10000; // 10秒超时 while (gpio_get(EPD_BUSY_PIN) BUSY_LEVEL) { // BUSY_LEVEL 需根据手册定义 sleep_ms(1); if (--timeout 0) { printf(Error: EPD busy timeout!\n); break; } } }SPI 时钟速度SPI 时钟设得太低会导致数据传输变慢。在确保屏幕能稳定接收的前提下参考屏幕数据手册的最大 SPI 时钟可以适当提高spi_init中的频率比如从 1MHz 提高到 4MHz 或更高。优化数据传输spi_write_blocking是逐字节发送的。对于长达上万字节的图像数据可以改用spi_write_blocking一次性发送整个缓冲区或者探索使用 DMA 进行传输。使用 DMA 时CPU 可以在数据传输的同时去处理其他任务如准备下一帧数据但对于简单的天气站项目阻塞式传输通常可以接受。6.4 显示内容闪烁或“鬼影”这不是程序 bug而是电子墨水屏的物理特性。墨水颗粒在电场作用下移动需要时间且从一个状态切换到另一个状态尤其是黑白反转的路径不同可能导致上一次图像的“残影”短暂可见。解决方法使用正确的刷新模式对于从白到黑或从黑到白的更新使用“全刷新”模式虽然慢但干净。对于文本更新等少量变化可以尝试“局部刷新”但记得在数次局部刷新后强制进行一次全刷。优化显示内容避免大面积、高频次的黑白反转。比如设计 UI 时采用深色背景上的浅色文字更新时只更新文字区域背景保持不变。启用“闪屏”有些屏幕驱动芯片支持在刷新前先进行一次“全白-全黑-全白”的快速闪烁以清除残影。这会在命令序列中体现。驱动一块 e-Paper 屏幕就像是在和一位反应稍慢但记忆力超群的伙伴合作。你需要清晰地告诉它每一步该做什么并耐心等待它完成。一旦掌握了它的“语言”SPI 协议和“习惯”正确的时序与命令它就能稳定可靠地呈现出你想要的内容。从点亮第一句“Hello, E-Paper!”到完成一个自动更新的天气站这个过程充满了嵌入式开发特有的挑战与乐趣。最关键的是每次看到屏幕上的信息在零功耗下持续显示那种成就感是驱动普通 LCD 屏幕无法比拟的。