第4章:SPI接口驱动LCD:从协议到实战
说到驱动LCD,SPI接口绝对是个绕不开的话题。我个人觉得,SPI是嵌入式开发里最优雅的通信方式之一——简单、高效、够用。今天我们就来聊聊怎么用STM32的HAL库,通过SPI驱动一块1.8寸的TFT屏幕(ST7735控制器)。
本章核心脉络:SPI协议模式 → HAL库配置 → DMA加速 → 点亮屏幕
4.1 SPI协议基础:模式0/1/2/3到底怎么选?
SPI协议,说白了就是四根线的事:SCK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。但为什么会有四种模式?
关键在于时钟极性(CPOL)和时钟相位(CPHA)的组合。我刚开始学的时候也被绕晕过,后来发现一个记忆诀窍:
- CPOL = 0:空闲时时钟为低电平
- CPOL = 1:空闲时时钟为高电平
- CPHA = 0:在第一个时钟边沿采样数据
- CPHA = 1:在第二个时钟边沿采样数据
| 模式 | CPOL | CPHA | 采样边沿 | 常见器件 |
|---|---|---|---|---|
| 模式0 | 0 | 0 | 上升沿 | ST7735、W25Qxx |
| 模式1 | 0 | 1 | 下降沿 | 某些ADC |
| 模式2 | 1 | 0 | 下降沿 | 某些传感器 |
| 模式3 | 1 | 1 | 上升沿 | SD卡(SPI模式) |
个人经验:ST7735默认工作在模式0。我遇到过一块国产的ST7735兼容屏,手册写的是模式0,但实际要配成模式3才能正常工作。嗯,遇到这种情况别慌,先看数据手册的时序图,再不行就两个模式都试试。
4.2 HAL库SPI配置:三步搞定
用STM32CubeMX配置SPI其实很简单,但我建议你手动检查几个关键参数:
- 数据大小:8位还是16位?LCD通常用8位
- 时钟极性/相位:根据器件手册选,ST7735用模式0
- 波特率分频:别一上来就搞最高速,我吃过这个亏
避坑指南:我曾经把SPI时钟配到36MHz,结果屏幕花屏。后来发现ST7735的极限是15MHz左右。建议先从4分频开始,稳定后再慢慢提速度。
配置代码大概长这样:
// SPI句柄定义
SPI_HandleTypeDef hspi1;
// 初始化函数(CubeMX生成的,但我会手动微调)
void MX_SPI1_Init(void)
{
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_LOW; // CPOL=0
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA=0
hspi1.Init.NSS = SPI_NSS_SOFT; // 软件控制CS
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4;
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();
}
}
注意:NSS一定要设成软件模式。用硬件片选的话,每次传输完CS会自动拉高,但LCD需要你手动控制CS电平来区分命令和数据。
4.3 SPI DMA传输:让CPU喘口气
刷屏的时候,如果一帧一帧地用轮询方式发数据,CPU基本就废了。DMA就是干这个的——它帮你把数据从内存搬到SPI外设,CPU可以去做别的事。
配置DMA其实不复杂,但有几个坑:
- DMA方向:内存到外设(MEM_TO_PERIPH)
- 数据宽度:和SPI数据位一致,8位就设字节
- 循环模式:刷动画时有用,但要注意缓冲区管理
// DMA传输示例
void LCD_SendData_DMA(uint8_t *data, uint16_t len)
{
// 等待上一次传输完成
while(HAL_SPI_GetState(&hspi1) != HAL_SPI_STATE_READY);
// 启动DMA传输
HAL_SPI_Transmit_DMA(&hspi1, data, len);
}
// 传输完成回调
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
if(hspi->Instance == SPI1)
{
// 传输完成,可以准备下一帧数据
transfer_complete = 1;
}
}
我的习惯:用DMA刷全屏时,我会准备两个缓冲区(双缓冲)。一个在DMA传输,另一个在准备数据。这样刷屏速度能翻倍。
4.4 实战:用SPI驱动1.8寸TFT(ST7735)
好了,理论说完了,咱们直接上手。ST7735这块芯片,说白了就是个带GRAM的SPI从设备。你要做的就是:初始化它,然后往GRAM里填颜色数据。
初始化序列(关键步骤):
- 硬件复位(拉低RST引脚至少10ms)
- 发SLPOUT命令(退出睡眠模式)
- 等待120ms(让内部电路稳定)
- 发COLMOD命令(设置颜色格式,16位色)
- 发DISPON命令(打开显示)
// 写命令函数
void LCD_WriteCmd(uint8_t cmd)
{
LCD_CS_LOW(); // 片选拉低
LCD_DC_LOW(); // 命令模式
HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY);
LCD_CS_HIGH(); // 片选拉高
}
// 写数据函数
void LCD_WriteData(uint8_t data)
{
LCD_CS_LOW();
LCD_DC_HIGH(); // 数据模式
HAL_SPI_Transmit(&hspi1, &data, 1, HAL_MAX_DELAY);
LCD_CS_HIGH();
}
// 初始化函数(精简版)
void ST7735_Init(void)
{
// 硬件复位
LCD_RST_LOW();
HAL_Delay(10);
LCD_RST_HIGH();
HAL_Delay(120);
// 软件初始化序列
LCD_WriteCmd(0x01); // 软件复位
HAL_Delay(150);
LCD_WriteCmd(0x11); // 退出睡眠
HAL_Delay(120);
LCD_WriteCmd(0x3A); // 颜色格式
LCD_WriteData(0x05); // 16位色(RGB565)
LCD_WriteCmd(0x29); // 打开显示
HAL_Delay(10);
// 设置显示区域(全屏)
LCD_WriteCmd(0x2A); // 列地址
LCD_WriteData(0x00);
LCD_WriteData(0x00); // 起始列
LCD_WriteData(0x00);
LCD_WriteData(0x7F); // 结束列(128像素)
LCD_WriteCmd(0x2B); // 行地址
LCD_WriteData(0x00);
LCD_WriteData(0x00); // 起始行
LCD_WriteData(0x00);
LCD_WriteData(0x9F); // 结束行(160像素)
// 填充全屏为蓝色
LCD_WriteCmd(0x2C); // 写GRAM
for(uint16_t i = 0; i < 128*160; i++)
{
LCD_WriteData(0x00); // 蓝色低字节
LCD_WriteData(0x1F); // 蓝色高字节
}
}
我曾经踩过的坑:初始化序列里的延时不能省!特别是退出睡眠模式后的120ms,有些屏延时不够会直接黑屏。还有,颜色格式命令0x3A的参数,0x05是16位色,0x06是18位色,别搞混了。
如果你看到屏幕变成蓝色,恭喜你,SPI驱动LCD已经成功了。接下来就可以画点、画线、显示字符了——这些就是后面章节的内容了。
总结一下:SPI驱动LCD其实就三件事——配对模式、写好初始化序列、用DMA加速刷屏。掌握了这些,市面上大部分SPI接口的LCD你都能搞定。