1. 项目缘起为什么是Pico-LLC-1.14最近在捣鼓一些嵌入式的小玩意儿手头正好有一块树莓派Pico想着给它配个屏幕让它能“开口说话”显示点信息。市面上给Pico用的LCD屏模块不少但“Pico-LCD-1.14”这个型号以其小巧的体积和相对友好的价格在创客圈里热度一直不低。它指的通常是一款1.14英寸、分辨率为240x135像素的IPS彩色LCD屏幕通过SPI接口与Pico连接。你可能会问为什么选它而不是那些0.96寸或者更大的屏幕这里面的考量其实挺实际的。0.96寸的OLED虽然更省电、对比度高但显示彩色内容受限且可视角度通常不如IPS。而更大的屏幕比如1.3寸、1.8寸固然显示效果更好但功耗、价格和占用GPIO的数量也会相应增加。对于Pico这种主打小巧、低功耗、高性价比的微控制器来说1.14寸的IPS屏是一个很好的平衡点它足够显示几行文字、简单的图标、甚至是一些基础的游戏画面它的SPI接口通常只需要占用4-5个GPIODC、CS、SCK、MOSI、背光控制有时还有RST不会过度消耗Pico宝贵的IO资源同时IPS技术带来了不错的可视角度和色彩表现让做出来的小项目看起来更“精致”。我最初的想法很简单就是想做一个能显示系统状态比如CPU温度、网络状态或者滚动显示一些自定义信息的小桌面摆件。但深入了解后发现围绕这块小屏幕能玩的花样远比想象的多。从最基础的“Hello World”显示到驱动复杂的图形界面GUI再到结合传感器做数据可视化甚至有人用它来复刻经典的游戏机界面。网络上相关的讨论和项目也印证了它的热度比如有人探讨如何用DMA优化SPI驱动以提高刷新率这直接关联到STM32H750 DMA驱动SPI LCD的问题也有人尝试在更强大的平台上如Luckfox Pico焊接这块屏幕探索其性能上限。这些热词背后反映的正是开发者们对“如何在资源受限的嵌入式设备上实现更好的人机交互”这一核心问题的持续探索。2. 硬件连接与核心驱动原理拿到Pico-LCD-1.14模块第一步就是把它和树莓派Pico正确地连接起来。虽然模块的引脚定义可能因生产商略有不同但核心的SPI接口是标准化的。下面是一个典型的连接方式也是大多数开源驱动库默认的引脚映射Pico-LCD-1.14 引脚树莓派Pico GPIO功能说明VCCVSYS 或 3V3(OUT)电源接3.3VGNDGND地DIN (MOSI)GPIO11 (SPI1 TX)SPI主设备输出从设备输入CLK (SCK)GPIO10 (SPI1 SCK)SPI时钟CS (Chip Select)GPIO9片选低电平有效DC (Data/Command)GPIO8数据/命令选择高电平为数据低电平为命令RST (Reset)GPIO12复位低电平复位有些模块可接固定高电平BL (Backlight)GPIO13背光控制高电平点亮注意务必确认你的模块引脚顺序最好查阅随模块的资料或卖家提供的说明。电源一定要接3.3VPico的GPIO口耐受电压也是3.3V接5V大概率会损坏屏幕或Pico。连接好硬件我们来聊聊驱动它的核心原理。这块屏幕的驱动芯片常见的是ST7789或类似的型号。驱动它本质上就是通过SPI总线向这块驱动芯片发送一系列的命令和数据。这里的关键在于理解“命令”和“数据”的区别也就是DC引脚的作用。当DC引脚为低电平0时此时通过SPI发送的字节会被屏幕驱动芯片解释为命令。命令用于设置屏幕的工作模式比如打开/关闭显示、设置扫描方向、定义内存写入的区域等。你可以把命令理解为对屏幕驱动芯片的“配置指令”。当DC引脚为高电平1时此时通过SPI发送的字节会被解释为数据。数据就是要实际显示在屏幕上的像素颜色值。对于这块16位色的屏幕RGB565格式每个像素需要2个字节16位的数据来描述其颜色。因此显示任何内容的基本流程都是先发送命令配置好要写入的显示区域即告诉芯片“接下来我要往屏幕的哪个矩形区域填充颜色”然后将DC引脚拉高再连续发送该区域内所有像素的颜色数据。刷新率帧率是衡量显示流畅度的关键指标。对于SPI接口刷新率受限于SPI时钟频率和总数据量。全屏刷新一帧240*13532400个像素每个像素2字节需要传输约64.8KB的数据。在Pico的SPI时钟达到几十MHz时理论帧率可以做到十几到几十帧每秒这对于大多数信息显示应用足够了。但如果要实现动画或游戏就需要优化。这就是为什么“STM32H750 DMA驱动SPI LCD”会成为热词——DMA直接内存访问可以让CPU在配置好传输后就去处理其他任务数据搬运由DMA控制器完成极大地解放了CPU并且能实现更稳定、更高带宽的数据流从而提升刷新率和系统效率。虽然Pico的RP2040芯片也有DMA但其在超高频SPI下的优化和STM32H750这类高性能MCU的侧重点不同但优化思路是相通的减少CPU干预提高总线利用率。3. 软件环境搭建与基础显示硬件连好了原理也清楚了接下来就是让屏幕亮起来。我们选择在树莓派Pico上使用MicroPython进行开发因为它上手快生态丰富。当然如果你追求极致性能用C/C和官方的Pico SDK是更好的选择。首先你需要准备MicroPython环境给Pico刷入最新的MicroPython固件从树莓派基金会官网下载.uf2文件按住Pico上的BOOTSEL按钮上电将其拖入出现的U盘即可。使用Thonny、VS Code with Pico-W-Go插件等IDE连接到Pico。驱动屏幕我们通常不需要从零开始写底层SPI命令。社区已经有非常成熟的库比如st7789_mpy或者pico_st7789。这里以st7789_mpy为例因为它功能全面且维护活跃。步骤一上传驱动库文件你需要将驱动库的.py文件通常是st7789.py上传到Pico的文件系统中。在Thonny中你可以通过“文件”-“打开”找到本地的库文件然后选择“文件”-“另存为”保存到Pico设备上。步骤二编写第一个显示程序创建一个新的main.py文件写入以下代码import machine import st7789 import time # 1. 初始化SPI总线 spi machine.SPI(1, baudrate40000000, polarity1, phase0, sckmachine.Pin(10), mosimachine.Pin(11)) # 参数说明SPI(1)表示使用SPI1baudrate是时钟频率40MHzpolarity和phase是SPI模式根据屏幕驱动芯片手册通常为1,0或0,0 # 2. 初始化屏幕对象按照我们的硬件连接定义引脚 tft st7789.ST7789(spi, 240, 135, resetmachine.Pin(12, machine.Pin.OUT), dcmachine.Pin(8, machine.Pin.OUT), csmachine.Pin(9, machine.Pin.OUT), backlightmachine.Pin(13, machine.Pin.OUT), rotation1) # rotation表示旋转方向可以填0,1,2,3 # 3. 激活背光 tft.backlight(True) # 4. 填充屏幕颜色 tft.fill(st7789.RED) # 填充红色 time.sleep(1) tft.fill(st7789.GREEN) # 填充绿色 time.sleep(1) tft.fill(st7789.BLUE) # 填充蓝色 time.sleep(1) # 5. 在指定位置显示文字 tft.text(“Hello Pico-LCD!”, 10, 50, st7789.WHITE, st7789.BLACK) # 参数字符串, x坐标, y坐标, 文字颜色, 背景颜色运行这段代码你应该能看到屏幕依次变红、变绿、变蓝最后在中间位置显示出“Hello Pico-LCD!”的白字。实操心得与避坑点SPI频率baudrate不是越高越好。虽然Pico的SPI最高可达125MHz但屏幕模块、连接线质量都可能成为瓶颈。过高的频率会导致数据错误屏幕出现花屏或条纹。建议从20MHz或30MHz开始测试逐步提高直到稳定工作的最高频率。我实测中40MHz在短线连接下通常很稳定。初始化顺序有些屏幕模块对初始化时序有要求。如果屏幕点亮后白屏或乱码可以尝试在初始化tft对象后增加一个短暂的延时time.sleep_ms(100)再执行tft.init()如果库提供该方法或直接进行填充操作。确保电源稳定上电后再进行通信。旋转方向rotation参数非常实用它决定了屏幕的物理朝向。如果你发现文字是横着的或者倒着的调整这个参数即可无需修改坐标计算逻辑。4. 进阶图形绘制与界面构建能显示文字和纯色块只是第一步。一个有用的项目通常需要绘制图形、进度条、图标甚至更复杂的界面。st7789_mpy库提供了一系列基础的绘图函数。4.1 基本图形绘制# 绘制矩形边框 tft.rect(20, 20, 100, 50, st7789.YELLOW) # (x, y, width, height, color) # 绘制填充矩形 tft.fill_rect(20, 80, 100, 30, st7789.CYAN) # 绘制直线 tft.line(0, 0, 239, 134, st7789.MAGENTA) # 从左上角到右下角的对角线 # 绘制单个像素点 tft.pixel(120, 67, st7789.WHITE) # 在屏幕中心点一个白点4.2 显示位图图像在嵌入式设备上显示图片通常需要先将图片转换为C语言数组或Python的字节数组。一个常用的工具是LCD Image Converter或在线转换工具。转换时需注意颜色格式必须转换为RGB565格式。尺寸不能超过屏幕分辨率240x135。存储转换后的数据会很大一张全屏的RGB565图片需要约64KB需要考虑Pico的RAM264KB是否够用。通常只存储小图标。假设你有一个转换好的logo.rgb文件包含RGB565原始数据可以这样显示def show_image(filename, x, y, width, height): with open(filename, ‘rb’) as f: buffer f.read() # 将数据按行写入屏幕指定区域 for row in range(height): tft.write_data(buffer[row*width*2:(row1)*width*2]) # 注意此函数为示意实际库可能提供更高效的blit方法。st7789_mpy的 blit_buffer 方法更专业。更高效的方式是使用库自带的blit_buffer方法它可以直接将一块内存缓冲区bytearray刷到屏幕的指定区域。4.3 构建简单用户界面对于复杂的界面手动计算每个元素的位置非常繁琐。你可以引入简单的UI框架比如micropython-ulab的图形部分或者自己实现一个轻量级的控件系统。一个常见的模式是定义控件基类包含位置、大小、重绘(draw)方法、处理事件(handle)方法。实现具体控件如标签(Label)、按钮(Button)、进度条(ProgressBar)。维护控件列表在主循环中遍历列表检查是否需要重绘脏矩形机制或者处理触摸/按键事件。例如一个简单的进度条可以这样绘制class ProgressBar: def __init__(self, x, y, width, height, color_fg, color_bg): self.x x self.y y self.width width self.height height self.fg_color color_fg self.bg_color color_bg self.value 0 # 0.0 to 1.0 def set_value(self, val): self.value max(0, min(1, val)) # 限制在0-1之间 self.draw() def draw(self): # 画背景 tft.fill_rect(self.x, self.y, self.width, self.height, self.bg_color) # 画前景进度部分 fill_width int(self.width * self.value) if fill_width 0: tft.fill_rect(self.x, self.y, fill_width, self.height, self.fg_color)在主循环中更新进度条就能实现动态效果。这种方式虽然简单但对于很多小项目来说已经足够。5. 性能优化与高级技巧当你的项目需要显示动态内容如动画、实时数据曲线时性能就变得至关重要。全屏刷新频率低会导致明显的卡顿。以下是几个关键的优化方向5.1 局部刷新与脏矩形这是最重要的优化策略。不要每次都重绘整个屏幕。只重绘内容发生变化的区域。上面的ProgressBar.draw()方法就隐含了这个思想——它只重绘了进度条自身所在的矩形区域。你需要为你的界面元素设计“脏”标志。当某个元素的状态改变时将其标记为“脏”并在下一次主循环中只重绘所有“脏”的区域。对于复杂的重叠界面可能需要合并脏矩形区域以避免重复绘制。5.2 使用帧缓冲区st7789_mpy库支持帧缓冲区Framebuffer。你可以创建一个与屏幕分辨率匹配的帧缓冲区对象所有的绘图操作都先在内存中的这个缓冲区里完成最后一次性将整个缓冲区通过SPI发送到屏幕。这样做的好处是避免闪烁绘图过程在后台完成用户看到的是完整的、一次性更新的画面。简化逻辑无需自己管理脏矩形。库函数优化很多图形库的绘图函数针对帧缓冲区有高度优化。但缺点也很明显极其消耗内存。一个240x135的RGB565帧缓冲区需要240*135*2 64800字节约63.3KB。这对于只有264KB RAM的Pico来说占了将近四分之一。如果同时还需要其他内存如网络缓冲区、传感器数据就可能捉襟见肘。import framebuf # 创建一个帧缓冲区 buffer bytearray(240 * 135 * 2) fb framebuf.FrameBuffer(buffer, 240, 135, framebuf.RGB565) # 在fb上绘图 fb.fill(0) # 黑色背景 fb.text(“Framebuffer”, 30, 60, 0xffff) # 白色文字 # 将fb内容一次性刷到屏幕 tft.blit(fb, 0, 0)是否使用帧缓冲区需要在效果和资源之间做权衡。对于静态界面居多的应用局部刷新更经济。对于需要复杂动画或全屏游戏的应用帧缓冲区可能是必需品。5.3 优化SPI传输即使使用了局部刷新SPI传输本身也是瓶颈。提高SPI时钟在稳定的前提下尽量使用更高的baudrate。使用DMARP2040的DMA可以用于SPI传输。这需要更底层的编程通常在使用C/C和Pico SDK时才能充分发挥威力。其原理是CPU设置好源地址图形数据、目标地址SPI发送数据寄存器和传输量启动DMA后DMA控制器会自动搬运数据CPU可以同时执行其他任务如计算下一帧。MicroPython层面对DMA的支持有限但一些优化的C语言驱动库会集成DMA支持。减少通信开销每次发送数据都有命令开销拉低CS、设置DC等。批量发送数据时确保一次性发送尽可能多的连续数据而不是分多次发送小数据包。5.4 应对内存不足如果项目复杂内存不足是常态。使用gc.collect()MicroPython有垃圾回收器。在创建大对象如图片缓冲区后可以手动调用gc.collect()来回收内存碎片。将资源存放到FlashMicroPython可以冻结freeze模块到固件中或者将只读数据如图标、字体以const形式存储它们会占用Flash空间而非RAM。对于图片可以考虑使用压缩格式如RLE并在显示时动态解压到一个小缓冲区。流式处理对于显示大图片不要一次性读入整个文件。可以分块读取、分块解码、分块显示。6. 项目实战打造一个系统状态显示器现在我们把所有知识融合起来做一个实用的项目一个基于Pico-LCD-1.14的系统状态显示器。它将显示当前时间从网络获取或RTC。Pico的内部温度传感器读数。一个模拟风格的CPU使用率指示器用进度条模拟。滚动显示的励志标语。我们将采用局部刷新的策略并尽量节省内存。步骤1硬件与网络连接假设我们使用Pico W带Wi-Fi除了连接LCD无需其他硬件。我们需要连接Wi-Fi以获取网络时间NTP。步骤2软件结构main.py lib/ ├── st7789.py # 屏幕驱动 ├── config.py # Wi-Fi配置等 └── widgets.py # 自定义控件时间标签、温度标签、进度条等步骤3核心代码逻辑main.py负责初始化和主循环调度。import machine import network import time import ntptime from lib import st7789 from lib.widgets import Label, ProgressBar, ScrollingText import config # 初始化硬件 spi machine.SPI(1, baudrate40000000, polarity1, phase0, sckmachine.Pin(10), mosimachine.Pin(11)) tft st7789.ST7789(spi, 240, 135, resetmachine.Pin(12, machine.Pin.OUT), dcmachine.Pin(8, machine.Pin.OUT), csmachine.Pin(9, machine.Pin.OUT), backlightmachine.Pin(13, machine.Pin.OUT), rotation1) tft.backlight(True) # 连接Wi-Fi wlan network.WLAN(network.STA_IF) wlan.active(True) wlan.connect(config.WIFI_SSID, config.WIFI_PASSWORD) while not wlan.isconnected(): time.sleep(0.5) print(“Connected to Wi-Fi”) # 同步时间 try: ntptime.settime() except: print(“NTP sync failed”) # 创建界面控件 time_label Label(tft, 10, 10, “–:–:–“, st7789.WHITE, st7789.BLACK) temp_label Label(tft, 10, 40, “Temp: –.-C”, st7789.YELLOW, st7789.BLACK) cpu_bar ProgressBar(tft, 10, 70, 200, 15, st7789.GREEN, st7789.DARKGREY) scroll_text ScrollingText(tft, 10, 100, 220, 20, “Stay hungry, stay foolish. “, st7789.CYAN, st7789.BLACK, speed2) # 内部温度传感器 sensor_temp machine.ADC(4) conversion_factor 3.3 / (65535) # 主循环 last_second time.time() last_temp_update time.time() cpu_usage 0.0 while True: now time.time() # 每秒更新时间 if now - last_second 1: last_second now local_time time.localtime() time_str “{:02d}:{:02d}:{:02d}”.format(local_time[3], local_time[4], local_time[5]) time_label.set_text(time_str) # 模拟CPU使用率变化实际项目中可从系统负载获取 import random cpu_usage (cpu_usage random.uniform(-0.1, 0.1)) cpu_usage max(0, min(1, cpu_usage)) cpu_bar.set_value(cpu_usage) # 每5秒更新温度 if now - last_temp_update 5: last_temp_update now reading sensor_temp.read_u16() * conversion_factor temperature 27 - (reading - 0.706)/0.001721 # 根据RP2040数据手册的公式计算 temp_label.set_text(“Temp: {:.1f}C”.format(temperature)) # 更新滚动文本每次循环移动一点 scroll_text.update() # 微秒级延时让出CPU time.sleep_ms(50)widgets.py中实现了支持局部刷新的控件。每个控件自己管理是否需要重绘dirty标志并在draw_if_dirty()方法中完成重绘。主循环只需调用每个控件的更新和绘制方法即可。项目总结与踩坑实录时间同步NTP同步可能失败需要增加重试逻辑和离线备用使用RTC或维护一个软件时钟。温度读数RP2040的内部温度传感器精度一般且读数受芯片自身发热影响较大适合看趋势而非绝对值。滚动文本实现时要注意文本滚出屏幕左侧和从右侧进入的边界处理以及重绘区域的计算避免闪烁。功耗如果希望设备长期电池供电需要优化降低SPI频率、周期性地关闭背光tft.backlight(False)、让Pico进入休眠模式machine.lightsleep()并在定时器中断中唤醒进行更新。抗锯齿MicroPython原生的绘图函数通常不支持抗锯齿绘制斜线或曲线时会有锯齿感。如果需要更精美的图形可以考虑使用预先渲染好的位图或者实现一个简单的手工抗锯齿算法如Wu算法但这会消耗更多CPU资源。通过这个项目你不仅驱动了屏幕还综合运用了网络、传感器、定时任务和UI构建的知识。Pico-LCD-1.14就像一扇窗口让原本“沉默”的Pico能够生动地展示其内部世界和外部交互的结果。从点亮第一像素到构建一个动态的信息面板这个过程充满了嵌入式开发特有的、在有限资源下创造无限可能的乐趣。