树莓派1.44寸LCD HAT驱动全解析:从SPI通信到Python实战应用
1. 项目缘起为什么是1.44寸LCD HAT如果你玩过树莓派大概率会和我有一样的感受这块小小的开发板功能强大但缺了块屏幕总感觉少了点什么。调试代码时得连着SSH跑个图形界面还得外接个大显示器既不优雅也失去了“嵌入式”那种小巧便携的精髓。所以给树莓派找个合适的“眼睛”就成了很多玩家的刚需。市面上树莓派的屏幕模块不少从几寸到十几寸都有。但1.44寸这个尺寸在我看来是个非常有趣的平衡点。它足够小小到可以轻松集成到任何外壳里甚至可以直接“戴”在树莓派头上这就是HATHardware 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或普通型号来说过于奢侈。而SPISerial 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接口默认是关闭的需要手动开启。在终端运行sudo raspi-config。选择Interface Options-SPI。选择Yes以启用SPI接口。重启树莓派 (sudo reboot)。重启后你可以通过检查/dev/目录下是否存在spidev0.0和spidev0.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.GPIOspidev: 提供了Python访问Linux SPI设备接口的能力让我们可以方便地读写SPI数据。RPi.GPIO: 用于控制GPIO引脚DC, RST, CS, BL设置输入输出模式和电平。PIL(Pillow): 强大的图像处理库。我们将用它来创建图像、绘制图形和文字然后提取图像的像素数据发送给屏幕。numpy: 虽然不是必须但在处理图像像素数据数组时能极大提升效率。3.3 理解驱动流程初始化与双缓冲驱动ST7735S这类屏幕软件上的核心流程可以分为两部分初始化和数据刷新。初始化过程这是一系列预先定义好的命令序列。通过SPI总线在DC引脚为低电平命令模式时向屏幕驱动芯片发送这些命令。这些命令用于设置屏幕的各种参数例如扫描方向GRAM的写入顺序决定了屏幕旋转。颜色格式RGB565, RGB666等。显示开/关。睡眠模式进入/退出。等等。 每个命令可能后面还跟着参数。ST7735S有详细的命令集文档但幸运的是开源社区已经有成熟的驱动代码我们不需要从头编写这些晦涩的十六进制命令码。数据刷新过程当需要更新屏幕内容时流程如下设置DC引脚为高电平数据模式。通过SPI一次性发送整个帧缓冲区framebuffer的数据。对于128x128的RGB565屏幕一帧图像的数据量是 128 * 128 * 2 bytes 32768 bytes。发送完成后屏幕驱动芯片会自动将这些数据写入其内部的GRAM图形存储器并显示出来。这里有一个重要的概念双缓冲。如果我们直接在代表屏幕的帧缓冲区上绘图那么绘图过程中比如画一个圆需要很多步屏幕可能会显示中间状态造成闪烁。为了避免这个问题常见的做法是在内存中创建一个“离屏”缓冲区一个PIL的Image对象。所有的绘图操作都在这个离屏缓冲区上进行。绘图完成后将这个离屏缓冲区的像素数据通过SPI一次性、快速地发送到屏幕。 这样屏幕的更新是瞬间完成的用户看到的就是完整的、无闪烁的新画面。3.4 编写核心驱动类下面是一个高度精简但功能完整的驱动类框架展示了上述原理的实现import spidev import RPi.GPIO as GPIO from PIL import Image, ImageDraw, ImageFont import time class ST7735: def __init__(self, width128, height128): 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[i1], img_data[i2] 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), fillcolor) 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 usageIP地址可以使用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 IP4.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, fontfont_small, fill(255, 255, 255)) # 绘制CPU温度 temp_text f{cpu_temp:.1f}°C bbox lcd.draw.textbbox((0,0), temp_text, fontfont_large) text_width bbox[2] - bbox[0] lcd.draw.text(( (lcd.width - text_width)//2, 30 ), temp_text, fontfont_large, fill(255, 100, 100)) # 绘制内存使用率 lcd.draw.text((10, 75), fMem: {mem_usage:.1f}%, fontfont_small, fill(200, 200, 255)) # 绘制IP地址 lcd.draw.text((10, 95), fIP: {ip_addr}, fontfont_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速度与数据转换默认的刷新率可能只有每秒几帧对于动态内容来说太慢。优化点有两个SPI时钟速度在spidev.SpiDev()初始化后可以尝试提高max_speed_hz。ST7735S通常最高支持几十MHz可以尝试80000000(80MHz)。但要注意过高的速度可能导致通信不稳定与线长、干扰有关需要测试。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 0CPOL0 CPHA0或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, x5, y5), 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) # 约50FPS6.2 结合传感器与硬件交互将屏幕作为交互界面的一部分。例如连接一个DHT11温湿度传感器在屏幕上实时显示环境温湿度或者连接一个按钮通过按钮切换屏幕显示不同的信息页面。这需要你同时处理GPIO输入按钮、I2C/SPI传感器读取和屏幕输出是综合性的嵌入式项目练习。6.3 集成到系统服务中你可以将上面的监控脚本设置为一个系统服务systemd service让它在树莓派启动时自动运行这样一开机就能看到状态屏。同时可以编写一个简单的Web接口或Socket服务接收来自其他程序的信息并显示在屏幕上将其变成一个通用的消息通知终端。从硬件连接到软件驱动从原理理解到实战优化这块小小的1.44寸LCD HAT就像一扇窗口透过它我们不仅能窥见树莓派系统的运行状态更能深入理解SPI通信、帧缓冲、嵌入式图形显示这些底层技术。它的价值不在于尺寸而在于其带来的无限可能性和学习的深度。