树莓派1.44寸LCD HAT驱动全解析:从SPI通信到Python实战应用
2026/8/1 10:38:10 网站建设 项目流程

1. 项目缘起:为什么是1.44寸LCD HAT?

如果你玩过树莓派,大概率会和我有一样的感受:这块小小的开发板功能强大,但缺了块屏幕,总感觉少了点什么。调试代码时得连着SSH,跑个图形界面还得外接个大显示器,既不优雅,也失去了“嵌入式”那种小巧便携的精髓。所以,给树莓派找个合适的“眼睛”,就成了很多玩家的刚需。

市面上树莓派的屏幕模块不少,从几寸到十几寸都有。但1.44寸这个尺寸,在我看来是个非常有趣的平衡点。它足够小,小到可以轻松集成到任何外壳里,甚至可以直接“戴”在树莓派头上,这就是HAT(Hardware Attached on Top)形式的由来。同时,它又足够大,能显示几行文字、简单的图形界面或者传感器数据,对于状态监控、小型仪表盘、便携式设备UI来说,信息量刚刚好。最关键的是,这类屏幕通常采用SPI接口,只需要占用树莓派GPIO口上的少数几根线,接线极其简单,对系统资源占用也极低,非常适合作为辅助显示屏或者低功耗应用的核心部件。

我手头这块1.44寸LCD HAT,就是一个典型的代表。它直接扣在树莓派的40针GPIO排针上,通过SPI通信,驱动一块128x128像素的彩色LCD。没有复杂的驱动板,没有额外的电源接口,即插即用,可以说是把“简洁”做到了极致。但正是这种简洁,背后却涉及了SPI通信协议、帧缓冲区(Framebuffer)管理、Python图像库操作等一系列知识点。这次,我就来详细拆解一下这块小屏幕从硬件连接到软件驱动的全过程,分享如何让它真正“亮”起来,并显示我们想要的内容。

2. 硬件初探:接口、引脚与驱动芯片

拿到这块1.44寸LCD HAT,第一件事就是搞清楚它的硬件底细。虽然卖家通常会提供简单的说明,但理解其工作原理,才能更好地驾驭它。

2.1 核心组件与接口

这块HAT的核心是一块1.44英寸的TFT液晶屏,分辨率是128x128。这个分辨率对于显示图标、文字和简单动画已经足够。屏幕本身是被动器件,需要驱动芯片来控制每一个像素点的亮灭与颜色。我手上这块板子使用的驱动芯片是ST7735S,这是一款在小型彩色LCD中非常常见的驱动IC。

为什么是SPI接口?这是由应用场景决定的。对于这种低分辨率、刷新率要求不高的屏幕,并行总线(如8080或RGB接口)虽然速度快,但需要占用大量GPIO引脚(通常16根数据线加若干控制线),对于引脚资源紧张的树莓派Zero或普通型号来说过于奢侈。而SPI(Serial Peripheral Interface)是一种高速、全双工、同步的串行通信总线,只需要四根线(有时三根)就能完成数据交换,极大地节省了硬件资源。ST7735S支持SPI通信模式,正好契合了树莓派HAT模块“简洁至上”的设计哲学。

2.2 引脚定义与连接

作为一块HAT,它的连接方式就是直接插在树莓派的40针GPIO排座上。我们需要关注的是它具体占用了哪些GPIO引脚。通过查看板子背面或者原理图(如果卖家提供),可以确定其引脚定义。通常,这类屏幕的接线是固定的:

  • SPI0 MOSI (GPIO10): 主设备输出,从设备输入。树莓派通过这根线向屏幕发送显示数据。
  • SPI0 SCLK (GPIO11): 串行时钟线,由树莓派产生,用于同步数据位。
  • DC (Data/Command, GPIO24): 这是一个非常重要的引脚。它用于告诉驱动芯片,当前通过SPI发送的数据是“命令”还是“显示数据”。拉高时表示数据,拉低时表示命令。初始化屏幕、设置参数需要发送命令,而更新屏幕内容则需要发送像素数据。
  • RST (Reset, GPIO25): 复位引脚。拉低一段时间可以强制驱动芯片复位,回到初始状态。通常在上电初始化时使用。
  • CS (Chip Select, GPIO8): 片选引脚。在SPI总线上可以挂载多个设备,通过片选信号来选择与哪个设备通信。对于单设备,可以将其永久拉低(使能),但规范做法还是控制它。
  • BL (Backlight, GPIO18): 背光控制。可以通过PWM信号来控制背光亮度,实现调光效果。如果直接接VCC,则背光常亮。

注意:不同厂商生产的1.44寸LCD HAT,其引脚定义可能不同,尤其是DC、RST、CS这几个控制引脚。务必以你手中模块的说明书或原理图为准。接错线可能导致屏幕不亮甚至损坏树莓派GPIO(虽然概率低)。

2.3 电源考量

屏幕的供电直接来自树莓派的GPIO排针的3.3V和GND引脚。树莓派的3.3V引脚输出电流能力有限(通常标称~50mA)。一块小尺寸LCD屏幕的工作电流一般在几十毫安级别,背光全亮时可能达到100mA以上。虽然对于树莓派3B+/4B来说问题不大,但如果你使用的是树莓派Zero,或者同时连接了其他高耗电的HAT,就需要留意总电流是否超限。一个简单的判断方法是:如果屏幕亮度不足或闪烁,或者树莓派出现不稳定重启,可能就是供电不足的表现。此时可以考虑为屏幕背光单独供电,或者使用外部供电的USB HUB来为树莓派提供更充沛的电力。

3. 软件驱动:从SPI通信到像素绘制

硬件连接妥当后,下一步就是让系统识别并驱动这块屏幕。在树莓派上,我们通常有几种选择:直接操作GPIO模拟SPI、使用Linux内核的SPI设备接口、或者使用高级别的图形库。这里我推荐并详细讲解最常用、最灵活的方式:通过Python库PIL(Pillow) 和spidev/RPi.GPIO来控制。

3.1 启用树莓派SPI接口

树莓派的SPI接口默认是关闭的,需要手动开启。

  1. 在终端运行sudo raspi-config
  2. 选择Interface Options->SPI
  3. 选择Yes以启用SPI接口。
  4. 重启树莓派 (sudo reboot)。

重启后,你可以通过检查/dev/目录下是否存在spidev0.0spidev0.1设备文件来确认SPI是否启用成功:ls /dev/spi*

3.2 安装必要的Python库

我们需要安装几个关键的Python库:

sudo apt update sudo apt install python3-pip python3-pil python3-numpy sudo pip3 install spidev RPi.GPIO
  • spidev: 提供了Python访问Linux SPI设备接口的能力,让我们可以方便地读写SPI数据。
  • RPi.GPIO: 用于控制GPIO引脚(DC, RST, CS, BL),设置输入输出模式和电平。
  • PIL(Pillow): 强大的图像处理库。我们将用它来创建图像、绘制图形和文字,然后提取图像的像素数据发送给屏幕。
  • numpy: 虽然不是必须,但在处理图像像素数据数组时,能极大提升效率。

3.3 理解驱动流程:初始化与双缓冲

驱动ST7735S这类屏幕,软件上的核心流程可以分为两部分:初始化和数据刷新。

初始化过程:这是一系列预先定义好的命令序列。通过SPI总线,在DC引脚为低电平(命令模式)时,向屏幕驱动芯片发送这些命令。这些命令用于设置屏幕的各种参数,例如:

  • 扫描方向(GRAM的写入顺序,决定了屏幕旋转)。
  • 颜色格式(RGB565, RGB666等)。
  • 显示开/关。
  • 睡眠模式进入/退出。
  • 等等。 每个命令可能后面还跟着参数。ST7735S有详细的命令集文档,但幸运的是,开源社区已经有成熟的驱动代码,我们不需要从头编写这些晦涩的十六进制命令码。

数据刷新过程:当需要更新屏幕内容时,流程如下:

  1. 设置DC引脚为高电平(数据模式)。
  2. 通过SPI,一次性发送整个帧缓冲区(framebuffer)的数据。对于128x128的RGB565屏幕,一帧图像的数据量是 128 * 128 * 2 bytes = 32768 bytes。
  3. 发送完成后,屏幕驱动芯片会自动将这些数据写入其内部的GRAM(图形存储器),并显示出来。

这里有一个重要的概念:双缓冲。如果我们直接在代表屏幕的帧缓冲区上绘图,那么绘图过程中(比如画一个圆需要很多步),屏幕可能会显示中间状态,造成闪烁。为了避免这个问题,常见的做法是:

  1. 在内存中创建一个“离屏”缓冲区(一个PIL的Image对象)。
  2. 所有的绘图操作都在这个离屏缓冲区上进行。
  3. 绘图完成后,将这个离屏缓冲区的像素数据,通过SPI一次性、快速地发送到屏幕。 这样,屏幕的更新是瞬间完成的,用户看到的就是完整的、无闪烁的新画面。

3.4 编写核心驱动类

下面是一个高度精简但功能完整的驱动类框架,展示了上述原理的实现:

import spidev import RPi.GPIO as GPIO from PIL import Image, ImageDraw, ImageFont import time class ST7735: def __init__(self, width=128, height=128): self.width = width self.height = height self.spi = spidev.SpiDev() self.spi.open(0, 0) # 使用 SPI0, CE0 self.spi.max_speed_hz = 40000000 # 设置SPI速度,40MHz,可根据屏幕调整 self.spi.mode = 0b00 # SPI模式0 # 定义GPIO引脚(根据你的HAT实际接线修改!) self.dc_pin = 24 self.rst_pin = 25 self.cs_pin = 8 self.bl_pin = 18 GPIO.setmode(GPIO.BCM) GPIO.setup(self.dc_pin, GPIO.OUT) GPIO.setup(self.rst_pin, GPIO.OUT) GPIO.setup(self.cs_pin, GPIO.OUT) GPIO.setup(self.bl_pin, GPIO.OUT) GPIO.output(self.cs_pin, GPIO.HIGH) self.reset() self.init_display() # 这里需要填入具体的ST7735S初始化命令序列 self.clear() GPIO.output(self.bl_pin, GPIO.HIGH) # 打开背光 # 创建离屏缓冲区 self.buffer = Image.new('RGB', (width, height), color=(0, 0, 0)) self.draw = ImageDraw.Draw(self.buffer) def write_cmd(self, cmd): GPIO.output(self.dc_pin, GPIO.LOW) # 命令模式 GPIO.output(self.cs_pin, GPIO.LOW) # 选中设备 self.spi.xfer2([cmd]) GPIO.output(self.cs_pin, GPIO.HIGH) def write_data(self, data): GPIO.output(self.dc_pin, GPIO.HIGH) # 数据模式 GPIO.output(self.cs_pin, GPIO.LOW) if isinstance(data, list): self.spi.xfer2(data) else: self.spi.xfer2([data]) GPIO.output(self.cs_pin, GPIO.HIGH) def reset(self): GPIO.output(self.rst_pin, GPIO.LOW) time.sleep(0.1) GPIO.output(self.rst_pin, GPIO.HIGH) time.sleep(0.1) def set_window(self, x0, y0, x1, y1): # 设置要更新的屏幕区域,这里简化处理为全屏 self.write_cmd(0x2A) # 列地址设置命令 self.write_data([0x00, x0, 0x00, x1]) self.write_cmd(0x2B) # 行地址设置命令 self.write_data([0x00, y0, 0x00, y1]) self.write_cmd(0x2C) # 存储器写命令,接下来发送的都是像素数据 def display(self): """将离屏缓冲区的内容刷新到屏幕""" # 1. 设置更新区域为全屏 self.set_window(0, 0, self.width-1, self.height-1) # 2. 将PIL Image的RGB数据转换为屏幕需要的RGB565格式字节流 # 这是一个关键的性能点,使用numpy可以极大加速 img_data = self.buffer.convert('RGB').tobytes() rgb565_data = [] # 这是一个简单的转换示例,实际应用应使用numpy优化 for i in range(0, len(img_data), 3): r, g, b = img_data[i], img_data[i+1], img_data[i+2] rgb565 = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3) rgb565_data.extend([(rgb565 >> 8) & 0xFF, rgb565 & 0xFF]) # 3. 通过SPI发送数据 self.write_data(rgb565_data) def clear(self, color=(0,0,0)): self.draw.rectangle((0, 0, self.width, self.height), fill=color) self.display() # 可以在此类中添加更多便捷绘图方法,如画线、画圆、显示文字等

这个类封装了基本的操作。init_display方法需要填入完整的ST7735S初始化序列,这通常是一长串固定的命令和参数,可以从开源项目(如Adafruit的ST7735库)中获取。display方法中的RGB到RGB565的转换是性能瓶颈,在正式项目中务必使用numpy进行向量化优化,否则刷新帧率会非常低。

4. 实战应用:打造一个系统状态监控屏

理解了驱动原理后,我们可以做一个有趣的项目:将这块1.44寸屏幕变成一个实时系统状态监控器,显示CPU温度、内存使用率、IP地址等信息。

4.1 设计显示界面

在128x128的像素范围内,我们需要合理布局信息。可以设计一个简单的界面:

  • 顶部:固定标题,如“RPi Monitor”。
  • 中部:用大字体显示CPU温度(例如“52.3°C”)。
  • 底部:分两行显示内存使用率(如“Mem: 45%”)和IP地址(如“IP: 192.168.1.100”)。
  • 背景可以使用渐变色或静态图片,但要注意过于复杂的背景会降低刷新速度。

使用PIL的ImageDraw可以轻松实现这些绘图功能。

4.2 获取系统状态信息

在Linux下,我们可以通过读取系统文件来获取这些信息。

  • CPU温度:树莓派的CPU温度可以从/sys/class/thermal/thermal_zone0/temp读取,数值需要除以1000得到摄氏度。
    def get_cpu_temp(): with open('/sys/class/thermal/thermal_zone0/temp', 'r') as f: temp = int(f.read()) / 1000.0 return temp
  • 内存使用率:通过解析/proc/meminfo文件计算。
    def get_mem_usage(): with open('/proc/meminfo', 'r') as f: lines = f.readlines() total = int(lines[0].split()[1]) available = int(lines[2].split()[1]) usage = (total - available) / total * 100 return usage
  • IP地址:可以使用Python的socket库获取。
    import socket def get_ip_address(): s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: # 不需要真正连接 s.connect(('10.255.255.255', 1)) IP = s.getsockname()[0] except Exception: IP = 'N/A' finally: s.close() return IP

4.3 整合与动态刷新

我们将驱动类、界面绘制和状态获取整合到一个主循环中。为了避免屏幕闪烁,我们始终遵循“离屏绘制-全屏刷新”的双缓冲原则。

def main(): lcd = ST7735() # 初始化屏幕 font_large = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 24) font_small = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 14) while True: # 1. 清空离屏缓冲区(或用背景图填充) lcd.draw.rectangle((0, 0, lcd.width, lcd.height), fill=(0, 20, 40)) # 深蓝背景 # 2. 获取当前系统状态 cpu_temp = get_cpu_temp() mem_usage = get_mem_usage() ip_addr = get_ip_address() # 3. 在离屏缓冲区上绘制 # 绘制标题 lcd.draw.text((10, 5), "RPi Status", font=font_small, fill=(255, 255, 255)) # 绘制CPU温度 temp_text = f"{cpu_temp:.1f}°C" bbox = lcd.draw.textbbox((0,0), temp_text, font=font_large) text_width = bbox[2] - bbox[0] lcd.draw.text(( (lcd.width - text_width)//2, 30 ), temp_text, font=font_large, fill=(255, 100, 100)) # 绘制内存使用率 lcd.draw.text((10, 75), f"Mem: {mem_usage:.1f}%", font=font_small, fill=(200, 200, 255)) # 绘制IP地址 lcd.draw.text((10, 95), f"IP: {ip_addr}", font=font_small, fill=(200, 255, 200)) # 4. 将缓冲区内容刷新到屏幕 lcd.display() # 5. 等待一段时间再更新,例如2秒 time.sleep(2) if __name__ == '__main__': try: main() except KeyboardInterrupt: lcd.clear() GPIO.cleanup()

运行这个脚本,你的1.44寸小屏幕就会开始周期性地显示树莓派的实时状态了。你可以根据需要调整刷新频率、显示的信息和界面样式。

5. 性能优化与常见问题排查

在实际使用中,你可能会遇到刷新慢、颜色不对、屏幕花屏等问题。这里分享一些优化和排查经验。

5.1 提升刷新率:SPI速度与数据转换

默认的刷新率可能只有每秒几帧,对于动态内容来说太慢。优化点有两个:

  1. SPI时钟速度:在spidev.SpiDev()初始化后,可以尝试提高max_speed_hz。ST7735S通常最高支持几十MHz,可以尝试80000000(80MHz)。但要注意,过高的速度可能导致通信不稳定(与线长、干扰有关),需要测试。
  2. RGB到RGB565的转换:这是最大的性能瓶颈。前面示例中的for循环在Python中非常慢。必须使用numpy进行优化:
    import numpy as np def display_numpy(self): self.set_window(0,0,self.width-1, self.height-1) # 将PIL Image转换为numpy数组 (height, width, 3) img_array = np.array(self.buffer) # 使用向量化运算一次性完成转换 r = (img_array[..., 0].astype(np.uint16) & 0xF8) << 8 g = (img_array[..., 1].astype(np.uint16) & 0xFC) << 3 b = (img_array[..., 2].astype(np.uint16) & 0xF8) >> 3 rgb565 = r | g | b # 将16位数据拆分为高8位和低8位,并展平 data = np.dstack(( (rgb565 >> 8).astype(np.uint8), (rgb565 & 0xFF).astype(np.uint8) )).flatten() # 转换为list并发送(spidev需要list或bytes) self.write_data(data.tolist())
    使用numpy后,刷新帧率可以有数量级的提升,足以满足大多数动画需求。

5.2 屏幕显示异常排查

  • 白屏或全亮/全暗:首先检查背光(BL)引脚是否已正确设置为高电平。然后检查复位(RST)时序是否正确,确保复位后有足够的延时。最后,最可能的原因是初始化命令序列错误或缺失。务必使用针对你屏幕型号(ST7735S可能有细微变种)的正确初始化代码。
  • 花屏、错位、颜色怪异
    • 扫描方向/偏移设置错误:ST7735S初始化命令中包含了设置扫描方向和GRAM原点的参数。如果设置不对,图像可能旋转、镜像或显示在错误的位置。需要根据你的屏幕物理安装方向来调整这些参数。通常驱动库会提供rotation参数。
    • 颜色格式不匹配:确保初始化命令中设置的颜色格式(如RGB565)与你发送的数据格式完全一致。RGB565是每个像素2字节(5位红+6位绿+5位蓝),如果你的数据是3字节的RGB,就会导致颜色错乱。
    • SPI模式或相位错误spidev.mode需要与驱动芯片要求的一致。ST7735S通常使用Mode 0(CPOL=0, CPHA=0)或Mode 3。如果不对,数据根本无法被正确采样。
  • 屏幕只有一部分更新:检查set_window函数是否正确设置了全屏区域。有时为了局部更新优化,可能会错误地设置了固定的窗口大小。
  • 树莓派运行卡顿:如果刷新率设置过高(比如在循环中不加延时地全速刷新),SPI总线会持续高负荷工作,可能影响树莓派其他任务的性能。合理的做法是控制刷新频率,例如对于状态监控,1-2秒刷新一次足矣;对于动画,30-60Hz也完全足够,无需盲目追求极限帧率。

5.3 降低功耗与发热

这个小屏幕本身功耗不高,但如果你希望设备长时间电池供电,可以做一些优化:

  • 动态控制背光:在不需要看屏幕的时候,通过GPIO将背光引脚(BL)拉低,关闭背光。背光是屏幕最主要的耗电部分。
  • 利用屏幕的睡眠模式:ST7735S支持睡眠命令。在长时间不更新时,可以发送睡眠命令让驱动芯片进入低功耗模式,需要显示时再唤醒。这比只关背光更省电。
  • 降低刷新率:如前所述,根据应用需求降低刷新频率。

6. 进阶玩法:超越静态显示

掌握了基础显示后,这块小屏幕的潜力远不止于此。

6.1 显示动态图形与动画

利用双缓冲和优化后的刷新函数,你可以轻松实现平滑的动画。例如,画一个移动的小球:

x, y = 20, 20 dx, dy = 2, 1 while True: # 清屏或擦除上一帧 lcd.draw.rectangle((0,0,lcd.width, lcd.height), fill=(0,0,0)) # 在新位置画球 lcd.draw.ellipse((x-5, y-5, x+5, y+5), fill=(255,0,0)) # 更新位置 x += dx y += dy # 边界碰撞检测 if x <=5 or x >= lcd.width-5: dx = -dx if y <=5 or y >= lcd.height-5: dy = -dy # 刷新屏幕 lcd.display() time.sleep(0.02) # 约50FPS

6.2 结合传感器与硬件交互

将屏幕作为交互界面的一部分。例如,连接一个DHT11温湿度传感器,在屏幕上实时显示环境温湿度;或者连接一个按钮,通过按钮切换屏幕显示不同的信息页面。这需要你同时处理GPIO输入(按钮)、I2C/SPI传感器读取和屏幕输出,是综合性的嵌入式项目练习。

6.3 集成到系统服务中

你可以将上面的监控脚本设置为一个系统服务(systemd service),让它在树莓派启动时自动运行,这样一开机就能看到状态屏。同时,可以编写一个简单的Web接口或Socket服务,接收来自其他程序的信息并显示在屏幕上,将其变成一个通用的消息通知终端。

从硬件连接到软件驱动,从原理理解到实战优化,这块小小的1.44寸LCD HAT就像一扇窗口,透过它我们不仅能窥见树莓派系统的运行状态,更能深入理解SPI通信、帧缓冲、嵌入式图形显示这些底层技术。它的价值不在于尺寸,而在于其带来的无限可能性和学习的深度。

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

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

立即咨询