RP2040驱动SPI LCD:从硬件连接到LVGL图形界面的全流程解析
2026/8/2 15:39:21 网站建设 项目流程

1. 项目概述:当Pico遇上1.8寸LCD

如果你手头有一块树莓派Pico或者兼容的RP2040开发板,又恰好对嵌入式图形显示感兴趣,那么“Pico LCD 1.8”这个组合绝对值得你花时间折腾一下。这不仅仅是在一个微小的屏幕上点亮几个像素那么简单,它背后涉及的是如何在资源极其有限的微控制器(MCU)上,高效地驱动一块SPI接口的彩色LCD,并实现流畅的图形界面。最近在社区里,围绕RP2040和LCD的讨论热度不减,从“luckfox pico焊上一块lcd”的硬件改造,到“stm32h750 dma 驱动 spi lcd 问题”的性能优化,再到“基于stm32的lcd信号波形和fft频谱显示”的应用创意,都说明了小型LCD在嵌入式项目中的巨大潜力。而“Pico LCD 1.8”正是踏入这个领域一个非常经典且友好的起点。

这个项目适合谁呢?首先是嵌入式开发的新手,想通过一个具体的硬件项目来学习SPI通信、帧缓冲(Framebuffer)和基本图形绘制。其次是电子爱好者或创客,希望为自己的Pico项目增加一个直观的人机交互界面,比如显示传感器数据、制作一个迷你游戏机或者简单的状态显示器。最后,即使是经验丰富的开发者,在面对如何优化RP2040的双核和PIO(可编程IO)来极致压榨SPI LCD的刷新率时,也能找到不少挑战和乐趣。接下来,我将从一个完整的项目流程出发,拆解从硬件连接到软件驱动,再到图形应用开发的每一个环节,分享我实际调试中的经验和踩过的坑。

2. 硬件选型与连接:不只是插上线那么简单

2.1 认识你的LCD模块

市面上常见的“1.8寸LCD”模块,大多指的是对角线为1.8英寸的TFT液晶屏。它们通常以模块的形式出售,已经集成了驱动芯片(最常见的是ST7735S或ST7789V)、背光电路以及一个FPC排线或插针。对于Pico项目,选择带有SPI接口的模块是最方便、最主流的选择。

核心驱动芯片解析: ST7735S和ST7789V是两款非常相似的控制器,都支持262K色(18位RGB,6位每色)。ST7789V通常用于分辨率稍高的屏幕(比如240x320),而ST7735S则常见于128x160或132x162分辨率的1.8寸屏。两者都通过SPI接口接受命令和数据,其内部有一个显存(GRAM),我们通过SPI写入的数据就存储在这里,控制器会自动按帧率将GRAM中的数据转换成LCD的驱动信号。

注意:购买时一定要确认模块的驱动芯片型号和分辨率。这直接关系到后续驱动库的选择和初始化序列的配置。我最初就曾误将ST7789的初始化代码用于ST7735的屏幕,导致花屏,排查了半天。

2.2. Pico引脚分配与连接方案

树莓派Pico的RP2040芯片有丰富的GPIO,但并非所有都适合高速SPI。为了获得最佳性能,我们应使用硬件SPI外设。Pico有两个硬件SPI:spi0spi1

一个典型的连接方案如下(以spi0为例):

LCD引脚功能Pico GPIO备注
VCC电源正极VSYS 或 3V3(OUT)接VSYS可承受5V,接3V3则为3.3V
GND电源地GND
SCL / SCKSPI时钟线GPIO 2 (SPI0 SCK)硬件SPI时钟,必须连接指定引脚
SDA / MOSISPI数据线(主出从入)GPIO 3 (SPI0 TX)硬件SPI数据输出,必须连接指定引脚
RES / RST复位线GPIO 4任何GPIO均可,用于硬件复位
DC / RS数据/命令选择线GPIO 5任何GPIO均可,关键控制线
CS片选线GPIO 1 (SPI0 CSn)硬件SPI片选,或任何GPIO
BL / LED背光控制GPIO 6接PWM引脚可实现亮度调节

为什么这么连接?

  1. SPI引脚固定:RP2040的硬件SPI外设的SCK、TX、RX、CSn引脚是映射到固定GPIO的。对于spi0,SCK可以是GPIO 2或18,TX可以是GPIO 3或19。为了布线清晰和代码通用,我推荐使用GPIO 2和3这一组。CSn虽然硬件上也有限制,但在软件中我们可以用任何GPIO模拟片选,更灵活。
  2. DC线的重要性:这是驱动SPI LCD最关键的一根控制线。它告诉LCD控制器,接下来从SPI收到的一个字节是命令(Command)还是数据(Data)。例如,设置屏幕扫描方向是一个命令,而填充屏幕颜色的像素值则是数据。必须用一个独立的GPIO来控制它。
  3. 复位线(RST):虽然有些驱动库支持软件复位,但硬件复位更可靠。特别是在系统上电或程序跑飞后,一个硬复位能确保LCD控制器回到已知的初始状态。
  4. 背光控制(BL):直接接VCC背光会常亮。接一个GPIO可以通过输出高/低电平来控制开关。如果接在支持PWM的引脚上(如GPIO 6),还能用代码analogWrite来实现无极调光,这对电池供电的设备节省电量很有用。

实操心得:焊接或使用杜邦线连接前,最好用万用表蜂鸣档检查一下LCD模块的引脚定义是否与卖家描述一致。我曾遇到过一个模块,其GND和VCC的顺序印反了,盲目上电差点烧毁屏幕。

3. 软件驱动原理与底层代码解析

3.1. SPI通信时序与初始化序列

驱动LCD的第一步是与之建立正确的通信。SPI是一种同步、全双工的串行通信协议。对于LCD,我们通常只使用主设备(Pico)发送数据到从设备(LCD)的模式。

关键概念:

  • 时钟极性(CPOL)与相位(CPHA):这决定了数据在时钟信号的哪个边沿被采样。ST7735/ST7789通常工作在CPOL=0, CPHA=0CPOL=1, CPHA=1模式。具体需查阅数据手册,但常见驱动库中SPI_MODE0(CPOL=0, CPHA=0)是默认且广泛兼容的。如果屏幕显示乱码或完全无反应,检查SPI模式是首要的排查点。
  • 数据位顺序:通常是MSB(最高位)先行。
  • 时钟频率:初期调试建议先使用较低频率,如10-20 MHz,确保通信稳定。后续优化时可逐步提高。RP2040的SPI时钟最高可达系统时钟的一半。

初始化序列(Init Sequence): 这是一系列预先定义好的命令和数据的组合,用于配置LCD控制器的内部寄存器,比如设置颜色模式、扫描方向、伽马校正等。这个序列因屏幕型号、厂商甚至批次而异。通常,屏幕卖家会提供示例代码,或者我们可以在开源驱动库(如Adafruit_ST7735)中找到对应型号的序列。

一个典型的初始化流程包括:

  1. 执行硬件或软件复位。
  2. 发送SLPOUT(退出睡眠模式)命令。
  3. 发送COLMOD(接口像素格式)命令,设置为16位RGB565格式(这是最常用格式,平衡了色彩和传输量)。
  4. 发送MADCTL(内存访问控制)命令,设置扫描方向(横屏/竖屏,是否镜像)。
  5. 发送INVON/INVOFF(颜色反转)命令,调整显示颜色。
  6. 发送DISPON(打开显示)命令。

注意事项:初始化序列必须在屏幕供电稳定后发送。建议在setup()函数中,完成GPIO和SPI初始化后,先延时至少100ms,再进行复位和发送初始化命令。匆忙的通信是导致初始化失败的主要原因之一。

3.2. 核心驱动函数剖析

理解了通信基础后,我们来看驱动LCD最核心的三个函数:写命令、写数据和设置窗口。

// 伪代码示例,基于常见Arduino风格库 void writeCommand(uint8_t cmd) { digitalWrite(pinDC, LOW); // DC线拉低,表示接下来是命令 digitalWrite(pinCS, LOW); // 片选拉低,选中设备 spi.transfer(cmd); // 通过SPI发送命令字节 digitalWrite(pinCS, HIGH); // 释放片选 } void writeData(uint8_t data) { digitalWrite(pinDC, HIGH); // DC线拉高,表示接下来是数据 digitalWrite(pinCS, LOW); spi.transfer(data); digitalWrite(pinCS, HIGH); } void setAddrWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { // 设置将要写入数据的矩形区域 writeCommand(0x2A); // 列地址设置命令 (CASET) writeData(x0 >> 8); writeData(x0 & 0xFF); // 起始列高8位,低8位 writeData(x1 >> 8); writeData(x1 & 0xFF); // 结束列 writeCommand(0x2B); // 行地址设置命令 (RASET) writeData(y0 >> 8); writeData(y0 & 0xFF); // 起始行 writeData(y1 >> 8); writeData(y1 & 0xFF); // 结束行 writeCommand(0x2C); // 内存写命令 (RAMWR),接下来发送的将是像素数据 }

setAddrWindow是关键:LCD控制器内部GRAM的访问是基于“窗口”的。我们首先通过CASETRASET命令划定一个矩形区域,然后发送RAMWR命令,之后所有通过writeData发送的数据都会被填充到这个窗口内,并自动更新行和列指针。这避免了为每个像素都重复发送地址信息,极大地提高了填充矩形或绘制位图的速度。

3.3. 色彩格式:RGB565详解

为了节省带宽和内存,微控制器驱动LCD最常用的色彩格式是RGB565。它用一个16位(2字节)的无符号整数来表示一个像素的颜色。

  • 结构:红色(R)占5位,绿色(G)占6位,蓝色(B)占5位。RRRRRGGG GGGBBBBB
  • 优势:相比24位真彩色(RGB888,3字节),它节省了1/3的传输数据和内存占用,这对Pico只有264KB的RAM来说至关重要。
  • 转换:如果你有24位颜色值(如#FF8800),需要将其转换为RGB565。
    // 将8位R,G,B值转换为16位RGB565 uint16_t color = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
    在填充颜色时,我们需要依次发送这个16位值的高字节和低字节。

4. 高级优化:利用RP2040的独门绝技

4.1. 使用DMA解放CPU

“stm32h750 dma 驱动 spi lcd 问题”这个热词点出了驱动LCD的一个高级话题——使用DMA(直接内存访问)。RP2040同样拥有强大的DMA控制器。其原理是:CPU只需要配置好DMA通道的源地址(存放像素数据的数组)、目标地址(SPI发送数据寄存器)、传输数据量,然后启动DMA。DMA控制器会在后台自动将数据从内存搬运到SPI外设,无需CPU干预。在此期间,CPU可以执行其他任务,比如处理传感器数据、运行游戏逻辑等,从而实现“并行”操作。

Pico SDK中的DMA-SPI配置思路

  1. 初始化SPI为DMA模式。
  2. 准备一个像素数据缓冲区(uint16_t buffer[SCREEN_HEIGHT][SCREEN_WIDTH]),这就是我们的帧缓冲(Framebuffer)。
  3. 当一帧图像准备好后,配置DMA,源地址指向buffer,目标地址指向SPI_DR(数据寄存器),数据量是width * height * 2字节(因为每个像素2字节)。
  4. 启动DMA传输。
  5. 在DMA传输完成中断中,准备下一帧数据或进行其他同步操作。

实操心得:使用DMA时,要确保像素数据缓冲区位于物理连续的内存中,并且对齐到合适的边界(通常4字节或32字节对齐),这能提升DMA效率。Pico SDK的malloc默认分配的内存可能不保证强对齐,可以使用aligned_alloc函数。

4.2. 探索PIO(可编程IO)的极限

RP2040最革命性的特性是PIO。我们可以用PIO状态机来模拟或增强外设功能。对于驱动LCD,一个有趣的想法是:用PIO状态机来生成SPI时钟和数据信号,甚至实现“位拆裂”(Bit Banging)协议。

为什么用PIO?

  1. 极致时序控制:PIO指令的执行周期是确定的(系统时钟分频后)。你可以编写精确到纳秒级别的波形,这对于驱动一些时序苛刻的非标准SPI变种或8080并行接口屏幕非常有用。
  2. 减轻CPU负担:与DMA类似,一旦PIO程序启动,状态机会自动运行,CPU可以去做别的事。甚至可以将PIO与DMA结合,让DMA自动喂数据给PIO状态机,实现完全硬件加速的图形数据流。
  3. 灵活性:如果你的项目需要驱动多个SPI设备,而硬件SPI外设不够用,用PIO可以“创造”出额外的软件SPI,且性能远高于用CPU循环模拟。

一个简单的PIO SPI发送程序可能包含:循环从FIFO(先入先出队列)取出数据字节,然后通过一个循环,在8个时钟周期内,依次将数据的每个位移出到MOSI引脚,同时在SCK引脚产生时钟脉冲。

注意事项:PIO编程需要学习其独特的汇编指令集。对于标准SPI驱动,使用硬件SPI+DMA通常已经足够高效且简单。PIO更适合作为解决特殊协议需求或性能瓶颈时的进阶工具。不建议初学者一开始就深陷PIO,先从标准的Arduino库或Pico SDK的硬件SPI入手更稳妥。

5. 图形库与用户界面构建

5.1. 选择合适的图形库

直接操作帧缓冲来画点、线、矩形和文字是非常底层的。为了高效开发,我们应引入图形库。对于Pico,有几个主流选择:

  1. LVGL:一个功能极其强大的开源嵌入式图形库,支持动画、抗锯齿、多种字体、主题样式、触摸事件等,资源消耗相对较大,但对于创建复杂的用户界面(UI)是首选。它需要你提供一个“刷屏”函数(flush_cb),在这个函数里,LVGL会将需要更新的区域像素数据传递给你,由你负责通过SPI发送到LCD。这正是前面讲的setAddrWindow和DMA用武之地。
  2. U8g2:专注于单色显示屏,但通过其“全缓冲模式”也能驱动彩色LCD。它更轻量,内置了大量字体和绘图函数,适合显示文本、简单图标和图表。如果你的项目主要是数据展示,U8g2是个好选择。
  3. 自定义轻量级库:如果你只需要画点、线、矩形、圆形和显示位图,完全可以自己封装一个。例如,实现一个drawPixel(x, y, color)函数,它会更新帧缓冲数组;再实现一个updateScreen()refreshArea(x, y, w, h)函数,负责将帧缓冲的特定区域或全部数据发送到LCD。

选型建议

  • 追求丰富UI和交互:选择LVGL。需要仔细配置其内存池(用于图形对象、样式等)、帧缓冲,并实现刷屏和触摸(如果有)驱动。
  • 专注数据监控和文本显示:选择U8g2。配置简单,API直观。
  • 学习底层或需求极简:自己实现基础绘图函数。这是理解图形显示原理的最佳途径。

5.2. 实现帧缓冲与局部刷新

无论用哪个库,帧缓冲(Framebuffer)的概念都至关重要。它是一个存储在RAM中的二维数组,大小与屏幕分辨率一致,每个元素对应屏幕上一个像素的RGB565值。

双缓冲(Double Buffering): 这是实现流畅动画的关键技术。我们创建两个帧缓冲:front_buffer(前台缓冲)和back_buffer(后台缓冲)。

  1. CPU在back_buffer上绘制下一帧图像。
  2. 绘制完成后,启动DMA将back_buffer的数据传输到LCD。
  3. 在DMA传输期间,CPU可以开始准备下一帧到另一个缓冲(此时角色互换)。 这样可以避免屏幕在绘制过程中出现撕裂(一部分是上一帧,一部分是下一帧)。

局部刷新(Partial Update): 全屏刷新(即使只改了一个像素)传输数据量是320*240*2 = 153600字节,在10MHz SPI下需要约123ms,帧率不到10FPS。如果UI只有一小部分变化(如一个按钮高亮),我们可以只刷新变化的矩形区域。这就是setAddrWindow的价值所在。LVGL等高级库内部就实现了局部刷新,它只将脏区(dirty area)的像素数据传给刷屏回调函数,极大地提升了效率。

实操心得:在资源紧张的Pico上,全尺寸的双帧缓冲(对于240x320屏幕约300KB)会占用几乎所有RAM,导致程序无法运行。此时可以采用折中方案:使用单帧缓冲,并结合LVGL的局部刷新机制;或者降低显示分辨率;或者使用“行缓冲”+DMA乒乓传输等更复杂的技术。

6. 实战项目:构建一个系统监视器

让我们将以上所有知识融合,创建一个实用的项目:一个基于Pico和1.8寸LCD的系统监视器,实时显示Pico的CPU温度、核心电压以及自定义的模拟信号波形(类似“基于stm32的lcd信号波形和fft频谱显示”的简易版)。

6.1. 硬件与软件架构

硬件:Pico开发板,1.8寸 SPI LCD(ST7789V,240x320),一个电位器(用于模拟信号输入)。软件架构

  1. 底层驱动层:基于Pico SDK或Arduino框架,实现SPI初始化、LCD初始化、setAddrWindow、DMA传输函数。
  2. 图形库层:集成LVGL。配置其内存、实现刷屏驱动(调用我们的DMA函数)、实现触摸驱动(本项目暂不需要)。
  3. 应用层
    • 任务A(核心0):负责数据采集。循环读取Pico内部ADC的温度传感器和电压传感器,读取连接电位器的ADC引脚值,计算波形数据(或简单的FFT,如果计算量允许)。
    • 任务B(核心1或主循环):负责UI渲染。运行LVGL任务处理器(lv_timer_handler),根据采集到的数据更新UI上的标签、进度条和波形图控件。

6.2. 关键代码片段与解析

1. 刷屏回调函数(LVGL驱动核心)

static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) { // area: 需要更新的区域 // color_p: LVGL已经渲染好的该区域的像素数据数组 uint32_t width = lv_area_get_width(area); uint32_t height = lv_area_get_height(area); uint32_t num_pixels = width * height; // 1. 设置LCD的更新窗口为这个区域 setAddrWindow(area->x1, area->y1, area->x2, area->y2); // 2. 启动DMA,将color_p中的数据发送到SPI start_spi_dma_transfer((uint8_t*)color_p, num_pixels * 2); // 每个像素2字节 // 3. 告诉LVGL刷新已完成(需要在DMA传输完成中断中调用) // lv_disp_flush_ready(disp_drv); }

注意:lv_disp_flush_ready必须在像素数据真正被发送完毕后才能调用,通常放在DMA传输完成中断服务程序(ISR)中。如果立即调用,LVGL可能会覆盖还在传输中的数据。

2. 数据采集与UI更新

void data_acquisition_task() { while (1) { // 读取CPU温度 float temp = read_onboard_temperature(); // 读取核心电压 float voltage = read_core_voltage(); // 读取ADC电位器值 uint16_t adc_val = read_potentiometer(); // 将数据存入全局变量或队列 current_temperature = temp; current_voltage = voltage; adc_buffer[adc_index] = adc_val; adc_index = (adc_index + 1) % ADC_BUFFER_SIZE; // 唤醒UI任务或设置标志位 update_ui_flag = true; sleep_ms(50); // 20Hz采样率 } } void ui_task() { lv_init(); // ... 初始化显示驱动和输入设备驱动 ... lv_disp_t * disp = lv_disp_drv_register(&disp_drv); // 创建UI:标签、进度条、图表 lv_obj_t * temp_label = lv_label_create(lv_scr_act()); lv_obj_t * volt_label = lv_label_create(lv_scr_act()); lv_obj_t * waveform_chart = lv_chart_create(lv_scr_act()); lv_chart_set_point_count(waveform_chart, ADC_BUFFER_SIZE); while (1) { lv_timer_handler(); // 处理LVGL定时器和任务 sleep_ms(5); if (update_ui_flag) { update_ui_flag = false; // 更新温度标签 lv_label_set_text_fmt(temp_label, "Temp: %.1f C", current_temperature); // 更新电压标签 lv_label_set_text_fmt(volt_label, "Volt: %.2f V", current_voltage); // 更新波形图 lv_chart_set_next_value(waveform_chart, lv_chart_get_series_next(waveform_chart, NULL), adc_buffer[adc_index]); } } }

6.3. 性能优化与调试

在这个项目中,性能瓶颈可能出现在:

  1. SPI刷新率:使用DMA,并尽可能提高SPI时钟(在稳定前提下),是提升帧率的关键。实测在62.5MHz系统时钟下,SPI时钟设为31.25MHz,驱动240x320屏幕局部刷新小区域,UI可以非常流畅。
  2. LVGL刷新策略:确保lv_disp_flush_ready在DMA完成后及时调用,避免LVGL任务阻塞。可以适当增加LVGL的任务处理周期(但不要低于5ms)。
  3. 双核利用:正如上面架构所示,将数据采集(可能涉及ADC和计算)放在一个核心,UI渲染放在另一个核心或主循环,能有效避免任务相互阻塞。
  4. 内存管理:LVGL对象创建和删除要小心,避免内存碎片。对于不变的UI部分,在初始化时创建好即可。

常见问题排查:如果屏幕出现花屏、错位或颜色异常,按以下顺序检查:

  1. 电源和连接:确保VCC和GND稳定,所有信号线连接牢固。
  2. 初始化序列:确认使用的序列与你的屏幕型号(ST7735S/ST7789V)和具体变体(如偏移、扫描方向)匹配。很多问题源于错误的MADCTL参数。
  3. SPI配置:检查时钟极性/相位(CPOL/CPHA)、位顺序(MSB/LSB First)和时钟频率。
  4. 色彩格式:确认你发送的是RGB565格式(16位),并且高低字节顺序是否正确。有些屏幕驱动芯片需要先高字节后低字节,有些则相反。
  5. DMA/PIO配置:如果使用了DMA,检查缓冲区地址、传输大小是否正确,是否发生了内存越界。如果使用了PIO,用逻辑分析仪抓取SPI波形是最直接的调试手段。

通过这个完整的“Pico LCD 1.8”项目,我们从硬件连接到底层驱动,再到高级优化和图形应用开发,走通了一个嵌入式图形界面项目的全流程。最关键的不是记住每一个命令,而是理解其背后的原理:SPI通信、帧缓冲、DMA加速、任务拆分。掌握了这些,你就能灵活应对不同的屏幕和更复杂的应用需求,真正让这块小屏幕在你的项目中焕发光彩。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询