1. 项目概述:一块3.5寸LCD屏的“树莓派”之旅
如果你手头恰好有一块标着“3.5inch RPi LCD (G)”的屏幕,正琢磨着怎么让它亮起来,那你来对地方了。这玩意儿在开源硬件圈里挺常见,一块3.5英寸、分辨率320x480的TFT LCD,自带电阻触摸,接口是标准的SPI。它的核心是ST7796S驱动芯片和XPT2046触摸控制器,名字里的“RPi”直指树莓派,意味着官方或社区提供了针对树莓派GPIO的即插即用驱动,让点亮它变得像搭积木一样简单。但它的价值远不止于此,ST7796S这颗驱动芯片性能不俗,支持RGB565/666接口,而SPI模式只是其“节能省引脚”的工作状态;XPT2046更是电阻触摸界的常青树。这意味着,这块屏不仅是树莓派的专属配件,更是我们深入理解SPI通信、LCD驱动乃至嵌入式GUI开发的绝佳实验平台。无论你是想快速在树莓派上做个显示终端,还是打算用STM32、ESP32甚至FPGA来驯服它,探索其底层时序,这篇文章都将为你拆解清楚从接线、驱动到深度定制的每一步。
2. 核心硬件与接口深度解析
2.1 屏幕模组本体:ST7796S驱动芯片探秘
这块3.5寸屏的核心是ST7796S。这是一款262K色(18位数据线)的TFT LCD控制器。它支持多种接口模式,包括并行的RGB接口和串行的SPI接口。在“3.5inch RPi LCD (G)”这个模组上,为了节省树莓派宝贵的GPIO引脚,设计者选择了SPI接口模式。需要注意的是,SPI模式通常无法达到RGB接口的全带宽,但对于320x480这种分辨率的静态图像或简单动画刷新,40MHz甚至更高的SPI时钟下,体验是足够流畅的。
ST7796S内部有独立的显存(GRAM),我们通过SPI发送的命令和数据,最终就是写入这片GRAM,控制器再自动将GRAM中的内容映射到屏幕的每一个像素上。它有一系列初始化命令(Initialization Command),用于设置颜色格式、扫描方向、电源控制等。市面上很多驱动代码,其核心就是按照ST7796S数据手册的时序要求,通过SPI发送这一连串的初始化命令序列。
注意:不同批次或供应商的“3.5inch RPi LCD”,其ST7796S的初始化序列可能有细微差别。如果发现屏幕点亮后颜色异常、花屏或只有背光亮,首先应怀疑初始化代码是否完全匹配你的屏幕。通常供应商会提供参考代码。
2.2 触摸功能核心:XPT2046电阻触摸控制器
屏幕下方的“G”通常代表带有电阻式触摸屏(Resistive Touch)。负责这项功能的是XPT2046芯片。它同样通过SPI接口与主控通信。与ST7796S不同,XPT2046的SPI通信是典型的“查询”模式:主控发起一次转换请求,XPT2046在忙碌(BUSY)期间完成模数转换,然后主控读取转换结果。
XPT2046可以测量触摸点的X、Y坐标,以及触摸压力(通过测量Z轴)。其精度通常能达到12位(4096级)。电阻屏的优势是成本低、抗干扰强,任何物体均可触控;缺点是透光率稍差,且需要轻微压力。驱动XPT2046的关键在于理解其命令字(Control Byte)的格式,以及如何将读取到的原始AD值,通过校准算法转换成屏幕上的像素坐标。
一个典型的坐标读取流程如下:
- 拉低片选(CS)。
- 发送控制字(例如,启动+通道选择+Y坐标读取)。
- 读取高字节(同时发送时钟)。
- 读取低字节(同时发送时钟)。
- 拉高片选。
- 对读取的12位数据进行处理(可能需丢弃最低几位)。
2.3 接口定义与电气连接
这块屏的接口通常是排针或排母,引脚定义大同小异。一个典型的40Pin引脚定义(适配树莓派)会包含以下关键信号:
| 引脚号 | 标号 | 功能 | 树莓派GPIO | 说明 |
|---|---|---|---|---|
| 1, 17 | 3.3V | 电源正极 | 3.3V | 逻辑与背光电源 |
| 2, 4 | 5V | 背光电源(可选) | 5V | 部分模组背光需5V驱动 |
| 6, 9, 14, 20, 25, 30, 34, 39 | GND | 电源地 | GND | 必须可靠连接 |
| 19 | MOSI | SPI主出从入 | GPIO10 (MOSI) | 主控数据输出到屏幕 |
| 21 | MISO | SPI主入从出 | GPIO9 (MISO) | 触摸芯片数据输出到主控 |
| 23 | SCLK | SPI时钟 | GPIO11 (SCLK) | 通信时钟 |
| 24 | CS | LCD片选 | GPIO8 (CE0) | 选择LCD驱动芯片 |
| 26 | DC | 数据/命令选择 | GPIO25 | 高电平=数据,低电平=命令 |
| 22 | RST | 复位 | GPIO27 | 硬件复位,低电平有效 |
| 18 | BL | 背光控制 | GPIO24 | PWM或高低电平控制亮度 |
| 13 | T_CS | 触摸片选 | GPIO7 (CE1) | 选择触摸控制器芯片 |
接线要点与避坑指南:
- 电源是关键:务必确保3.3V和GND连接牢固。电流不足会导致屏幕工作不稳定、花屏或无法启动。如果屏幕背光单独由5V供电,且电流需求较大(可能超过树莓派单引脚供电能力),建议使用外部5V电源,并与树莓派共地。
- SPI冲突:树莓派默认只有一个SPI总线(SPI0),其CE0和CE1是硬件片选。如果你同时连接了其他SPI设备(如RFID读卡器),需要注意片选冲突。LCD和触摸屏可以共享MOSI、MISO、SCLK,但必须使用不同的GPIO作为片选(如表中所示)。
- 复位与背光:RST引脚在上电后需要一个由低到高的跳变过程来完成复位。有些驱动代码会手动控制这个引脚。BL背光控制,如果接的是PWM引脚,则可以实现亮度调节;如果只是接普通GPIO,则只能开关背光。
3. 在树莓派上的快速点亮与驱动配置
3.1 启用树莓派SPI接口
树莓派系统默认可能未开启SPI。通过命令行即可轻松启用:
sudo raspi-config依次选择Interface Options->SPI->Yes启用。重启后,可以通过以下命令检查SPI设备是否出现:
ls /dev/spi*如果看到/dev/spidev0.0和/dev/spidev0.1,说明SPI0已启用,其中.0对应CE0,.1对应CE1。
3.2 使用预编译驱动(最简方法)
对于只是想快速用起来的用户,最方便的方法是使用屏幕供应商或社区提供的预编译驱动/配置文件。通常这会是一个脚本,执行后会自动:
- 下载并安装内核模块(如
fbtft)。 - 修改
/boot/config.txt,添加设备树覆盖(Device Tree Overlay)配置。 - 配置触摸屏的坐标映射,将
/dev/input/eventX关联到屏幕。
执行完驱动安装脚本并重启后,屏幕应该就能作为系统的第二个显示器(或主显示器)正常工作,触摸功能也即插即用。你可以通过xinput_calibrator工具进行触摸校准,以获得更准确的点击体验。
实操心得:使用预编译驱动虽然省事,但可能会遇到内核版本不兼容、与其他显示配置冲突等问题。如果安装后黑屏或无法启动,可以尝试在启动时按住
Shift键进入恢复模式,通过命令行回滚配置。更可控的方式是手动配置,这能让你更清楚底层发生了什么。
3.3 手动配置FBTFT驱动(进阶控制)
如果你想更深入地控制,或者预编译驱动不工作,可以尝试手动配置Linux内核的FBTFT框架。这需要编辑/boot/config.txt文件:
# 添加以下行 dtparam=spi=on dtoverlay=gpio-backlight # 关键:加载针对ST7796S SPI的FBTFT设备树覆盖 # 注意:覆盖层名称可能因内核版本和屏幕变体而异,例如“pitft35-resistive” dtoverlay=pitft35-resistive,speed=32000000,rotate=90 # 解释:speed设置SPI时钟频率(Hz),rotate设置旋转角度重启后,屏幕会作为一个帧缓冲设备(如/dev/fb1)出现。你可以使用fbi或自己编写的程序向/dev/fb1写入图像数据来显示。
手动配置触摸:触摸屏通常会生成一个输入设备,如/dev/input/event1。你需要使用evdev驱动,并通过xinput或修改X11配置将其映射到正确的屏幕。对于非桌面环境或嵌入式GUI(如Qt Embedded),则需要在应用层直接读取evdev事件并处理。
4. 基于MCU(如STM32)的底层驱动开发
将这块屏幕连接到STM32等单片机,是学习嵌入式GUI和SPI通信的经典实践。这里我们以STM32的HAL库为例,讲解驱动要点。
4.1 SPI外设初始化配置
首先,需要初始化两个SPI外设(或一个SPI分时复用):一个用于LCD(ST7796S),一个用于触摸(XPT2046)。它们的配置有共同点也有区别。
LCD用SPI配置要点:
- 模式:SPI_MODE3(CPOL=1, CPHA=1)或MODE0,具体需查ST7796S手册,常见为MODE3。
- 数据大小:8位(SPI_DATASIZE_8BIT)。
- 时钟极性与相位:根据数据手册设置。
- 时钟预分频器:决定SCLK频率。ST7796S的SPI接口最高时钟可达几十MHz。在STM32上,根据系统时钟(如72MHz)设置分频,如
SPI_BAUDRATEPRESCALER_2得到36MHz。初始化和写入GRAM时可以用较低速,刷图时再切换到高速。 - NSS(片选)管理:设置为软件管理(
SPI_NSS_SOFT),用普通GPIO口控制CS引脚。
触摸用SPI配置要点:
- XPT2046通常也支持MODE0或MODE3。
- 其时钟频率要求较低,一般2MHz左右即可。
- 关键是需要全双工模式,因为在发送控制字的同时,需要接收来自触摸芯片的转换数据。
// STM32 HAL库 SPI初始化示例片段(LCD) hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_HIGH; // CPOL=1 hspi1.Init.CLKPhase = SPI_PHASE_2EDGE; // CPHA=1 hspi1.Init.NSS = SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; // 初始用较低速 hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; hspi1.Init.TIMode = SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; hspi1.Init.CRCPolynomial = 10; if (HAL_SPI_Init(&hspi1) != HAL_OK) { Error_Handler(); }4.2 ST7796S的初始化与基本绘图函数
驱动LCD的核心是编写两个底层函数:写命令(LCD_Write_Cmd)和写数据(LCD_Write_Data)。通过控制DC(数据/命令选择)引脚的电平来区分。
void LCD_Write_Cmd(uint8_t cmd) { LCD_DC_CMD(); // 设置DC引脚为低电平 LCD_CS_LOW(); // 拉低片选 HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); LCD_CS_HIGH(); } void LCD_Write_Data(uint8_t dat) { LCD_DC_DATA(); // 设置DC引脚为高电平 LCD_CS_LOW(); HAL_SPI_Transmit(&hspi1, &dat, 1, HAL_MAX_DELAY); LCD_CS_HIGH(); } void LCD_Write_Data_16bit(uint16_t dat) { uint8_t buf[2]; buf[0] = dat >> 8; // 发送高字节 buf[1] = dat & 0xFF; // 发送低字节 LCD_DC_DATA(); LCD_CS_LOW(); HAL_SPI_Transmit(&hspi1, buf, 2, HAL_MAX_DELAY); LCD_CS_HIGH(); }有了这两个函数,就可以按照ST7796S数据手册提供的初始化序列,一步步发送命令和数据。初始化通常包括:软件复位、设置像素格式(如RGB565)、设置显示方向(横屏/竖屏)、打开显示等。
设置窗口与写GRAM:刷屏(更新图像)的效率是关键。ST7796S允许我们设置一个矩形窗口(通过CASET和RASET命令),然后连续向GRAM写入数据(RAMWR命令),数据会自动填充该窗口。这是实现局部刷新和快速全屏刷新的基础。
void LCD_SetWindow(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { LCD_Write_Cmd(0x2A); // CASET LCD_Write_Data(x1 >> 8); LCD_Write_Data(x1 & 0xFF); LCD_Write_Data(x2 >> 8); LCD_Write_Data(x2 & 0xFF); LCD_Write_Cmd(0x2B); // RASET LCD_Write_Data(y1 >> 8); LCD_Write_Data(y1 & 0xFF); LCD_Write_Data(y2 >> 8); LCD_Write_Data(y2 & 0xFF); LCD_Write_Cmd(0x2C); // RAMWR }调用LCD_SetWindow后,连续调用LCD_Write_Data_16bit(对于RGB565格式)即可填充窗口。为了极致优化,可以将SPI切换为DMA模式,并一次性发送整个帧缓冲区。
4.3 XPT2046触摸数据读取与校准
触摸驱动的核心是读取原始的AD值,并通过校准将其转换为屏幕坐标。
读取原始坐标函数:
uint16_t TP_Read_AD(uint8_t cmd) { uint8_t buf[3]; uint16_t ad_value = 0; TP_CS_LOW(); HAL_SPI_TransmitReceive(&hspi2, &cmd, buf, 3, HAL_MAX_DELAY); // 发送命令,同时接收3字节 TP_CS_HIGH(); ad_value = ((buf[1] << 8) | buf[2]) >> 3; // 组合有效数据位 return ad_value; } uint8_t TP_Read_XY(uint16_t *x, uint16_t *y) { uint16_t x_ad, y_ad; // 通常需要多次采样取平均以抗干扰 x_ad = TP_Read_AD(CMD_RDX); y_ad = TP_Read_AD(CMD_RDY); // 检查是否在合理范围内(非触摸时XPT2046会返回接近0或4095的值) if(x_ad > 100 && x_ad < 3900 && y_ad > 100 && y_ad < 3900) { *x = x_ad; *y = y_ad; return 1; // 触摸有效 } return 0; // 无触摸 }触摸校准:电阻屏必须校准。经典的四点校准法需要用户在屏幕四个角依次点击,程序记录下点击时读取的原始AD值(Xraw, Yraw)和对应的理论屏幕像素坐标(Xdisp, Ydisp)。 通过这两组映射关系,可以计算出校准参数(通常是一个仿射变换矩阵,包含缩放和偏移)。一个简化的线性模型如下:
Xdisp = A * Xraw + B Ydisp = C * Yraw + D通过解方程求出A、B、C、D四个参数,后续所有触摸点都通过这个公式转换。更精确的校准可以考虑更复杂的模型,以纠正屏幕的非线性误差。
5. 性能优化与高级应用探讨
5.1 使用DMA提升SPI刷屏效率
当需要刷新全屏或大块区域时,频繁调用HAL_SPI_Transmit会产生大量CPU中断开销,严重限制帧率。此时,直接内存访问(DMA)是救星。
配置SPI DMA:在CubeMX中,为SPI的TX流启用DMA(Memory-to-Peripheral)。然后,你可以准备一个完整的帧缓冲区(frame buffer),里面按顺序存放着屏幕上每个像素点的RGB565数据。
DMA刷屏流程:
- 设置好显示窗口(
LCD_SetWindow(0,0,width-1,height-1))。 - 启动SPI发送命令
0x2C(RAMWR)。 - 紧接着,不是用
HAL_SPI_Transmit发送数据,而是用HAL_SPI_Transmit_DMA,将帧缓冲区的地址和长度(像素数*2字节)传给它。 - DMA控制器会在后台自动将数据从内存搬运到SPI数据寄存器,无需CPU干预。CPU可以在此期间处理触摸、逻辑等任务。
- 通过DMA传输完成中断或回调函数,得知一帧数据发送完毕,可以准备下一帧。
踩坑记录:使用DMA时,务必确保帧缓冲区位于DMA可访问的内存区域(如STM32的普通SRAM),并且其生命周期覆盖整个DMA传输过程(不能是局部变量)。另外,SPI时钟频率和DMA带宽要匹配,过高的SPI速度可能导致DMA来不及供数,造成显示撕裂。此时可以尝试启用SPI的FIFO,或适当降低SPI时钟。
5.2 驱动常见问题与排查技巧
在驱动开发过程中,你几乎一定会遇到以下问题。这里提供一个速查表:
| 现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 白屏或背光亮但无显示 | 1. 初始化序列错误或缺失。 2. 电源不稳定。 3. 复位信号异常。 | 1. 用逻辑分析仪抓取SPI时序,核对发送的命令序列是否与数据手册一致。 2. 测量3.3V电压,检查电源电流是否足够(可并联电容试试)。 3. 检查RST引脚上电时序,确保有从低到高的跳变。 |
| 花屏、错位、颜色异常 | 1. 像素格式设置错误(如RGB565设成RGB666)。 2. 扫描方向(MADCTL)设置错误。 3. 显存(GRAM)写入地址错误。 | 1. 确认初始化命令中颜色格式设置字节。 2. 尝试修改MADCTL寄存器的值,旋转屏幕看是否正常。 3. 检查 CASET/RASET窗口设置函数,坐标计算是否正确。 |
| 触摸完全无反应 | 1. 触摸SPI通信失败。 2. 触摸芯片电源或基准电压异常。 3. 触摸屏物理损坏。 | 1. 用逻辑分析仪检查触摸SPI的CS、CLK、MOSI、MISO信号。 2. 测量XPT2046的VCC和VREF引脚电压。 3. 轻微按压屏幕四边,看是否有某个区域有反应。 |
| 触摸坐标不准、漂移 | 1. 未校准或校准参数错误。 2. 电源噪声干扰。 3. 采样次数不足,噪声大。 | 1. 重新执行四点校准程序。 2. 为触摸芯片的电源引脚增加滤波电容。 3. 增加单次坐标的采样次数(如8次)并取中值滤波。 |
| 刷屏速度慢,闪烁 | 1. 未使用DMA,CPU搬运数据耗时太长。 2. SPI时钟频率设置过低。 3. 刷新策略不佳,全屏刷新太频繁。 | 1. 启用SPI TX DMA。 2. 在屏幕和主控允许范围内,提高SPI波特率预分频。 3. 采用局部刷新,只更新变化区域。 |
| 与其它SPI设备冲突 | 片选(CS)信号管理混乱。 | 确保同一时间只有一个设备的CS引脚被拉低。仔细检查所有SPI设备的CS引脚GPIO初始化状态(默认应为高电平)。 |
5.3 超越基础:GUI集成与项目构想
当你成功驱动了屏幕和触摸,一个充满可能性的世界就打开了。你可以在此基础上集成轻量级GUI库。
- LVGL:这是一个高度可裁剪、硬件要求极低的开源嵌入式图形库。它提供了丰富的控件(按钮、图表、列表等)和动画效果。将LVGL的显示驱动回调函数指向你的
LCD_SetWindow和刷屏函数,将输入设备回调指向你的触摸坐标读取函数,就能快速构建出漂亮的交互界面。 - emWin/STemWin:如果你是STM32用户,并且芯片有足够的Flash/RAM,可以尝试Segger的emWin。它功能强大,但通常需要授权。ST为其部分MCU提供了免费的STemWin库。
- 自己实现:对于简单需求,如绘制几个按钮和进度条,完全可以自己用基础绘图函数(画线、矩形、填充、显示位图)和状态机逻辑来实现,这样代码最精简,控制力最强。
项目构想方向:
- 智能家居中控屏:用树莓派+这块屏,运行Kivy或PyQt做一个本地控制界面,连接MQTT获取传感器数据,控制智能设备。
- 嵌入式数据仪表:用STM32读取传感器(如温湿度、压力),在屏幕上实时绘制曲线图、仪表盘。
- 复古游戏机:利用STM32的性能,驱动屏幕显示,配合按键,实现一些简单的像素游戏。
- FPGA显示验证终端:通过FPGA模拟SPI主控,将内部逻辑分析仪的数据或图像处理结果实时显示在这块屏上,作为调试终端。
驱动一块SPI LCD屏,从点亮的瞬间到实现流畅的交互,整个过程是对嵌入式开发中硬件接口、驱动编写、性能优化和系统集成能力的综合锻炼。无论是用树莓派快速原型验证,还是用MCU深入底层,这块“3.5inch RPi LCD (G)”都是一个绝佳的起点。当你看到自己编写的代码让像素逐一亮起,触摸精准响应时,那种成就感正是嵌入式开发的乐趣所在。