基于STM32的LCD ST7796驱动优化:SPI+DMA提高刷新率
·
项目场景:
在嵌入式显示设备开发中,ST7796 TFT-LCD显示屏广泛应用于智能家居控制面板、工业HMI等场景。原厂提供的SPI驱动代码在低性能MCU(如STM32F103)上运行时,存在刷新速率低和CPU占用率高的问题,导致系统响应缓慢且无法实现动态效果。
问题描述
在实现480x320分辨率全屏刷新时,发现以下问题:
1.刷新速率慢:上电的时刷新至少要1s。
2.UI动画卡顿:快速滑动UI元素时出现断层。
3.占用CPU资源:没有使用DMA搬运SPI数据
原厂代码如下
/******************************************************************************
函数说明:LCD串行数据写入函数
入口数据:dat 要写入的串行数据
返回值: 无
******************************************************************************/
void LCD_Writ_Bus(u8 dat)
{
LCD_CS_Clr();
SPI1_ReadWriteByte(dat);
LCD_CS_Set();
}
/******************************************************************************
函数说明:LCD写入数据
入口数据:dat 写入的数据
返回值: 无
******************************************************************************/
void LCD_WR_DATA8(u8 dat)
{
LCD_Writ_Bus(dat);
}
/******************************************************************************
函数说明:LCD写入数据
入口数据:dat 写入的数据
返回值: 无
******************************************************************************/
void LCD_WR_DATA(u16 dat)
{
LCD_Writ_Bus(dat >> 8);
LCD_Writ_Bus(dat);
}
/******************************************************************************
函数说明:LCD写入命令
入口数据:dat 写入的命令
返回值: 无
******************************************************************************/
void LCD_WR_REG(u8 dat)
{
LCD_DC_Clr(); // 写命令
LCD_Writ_Bus(dat);
LCD_DC_Set(); // 写数据
}
/******************************************************************************
函数说明:设置起始和结束地址
入口数据:x1,x2 设置列的起始和结束地址
y1,y2 设置行的起始和结束地址
返回值: 无
******************************************************************************/
void LCD_Address_Set(u16 x1, u16 y1, u16 x2, u16 y2)
{
LCD_WR_REG(0x2a); // 列地址设置
LCD_WR_DATA(x1);
LCD_WR_DATA(x2);
LCD_WR_REG(0x2b); // 行地址设置
LCD_WR_DATA(y1);
LCD_WR_DATA(y2);
LCD_WR_REG(0x2c); // 储存器写
}
void LCD_Init(void)
{
SPI1_Init();
LCD_GPIO_Init(); // 初始化GPIO
LCD_RES_Clr(); // 复位
delay_ms(100);
LCD_RES_Set();
delay_ms(100);
LCD_BLK_Set(); // 打开背光
delay_ms(100);
//************* Start Initial Sequence **********//
LCD_WR_REG(0x11); // Sleep out
delay_ms(120); // Delay 120ms
//************* Start Initial Sequence **********//
LCD_WR_REG(0Xf0);
LCD_WR_DATA8(0xc3);
LCD_WR_REG(0Xf0);
LCD_WR_DATA8(0x96);
LCD_WR_REG(0x36); // Memory Access Control
if (USE_HORIZONTAL == 0)
LCD_WR_DATA8(0x48);
else if (USE_HORIZONTAL == 1)
LCD_WR_DATA8(0x88);
else if (USE_HORIZONTAL == 2)
LCD_WR_DATA8(0x28);
else
LCD_WR_DATA8(0xE8);
LCD_WR_REG(0x3A);
LCD_WR_DATA8(0x05);
LCD_WR_REG(0Xe8);
LCD_WR_DATA8(0x40);
LCD_WR_DATA8(0x82);
LCD_WR_DATA8(0x07);
LCD_WR_DATA8(0x18);
LCD_WR_DATA8(0x27);
LCD_WR_DATA8(0x0a);
LCD_WR_DATA8(0xb6);
LCD_WR_DATA8(0x33);
// ...后续多个参数写入
}
原因分析:
| 问题点 | 具体表现 |
|---|---|
| SPI传输效率低 | 每个字节都产生CS片选信号 |
| 无DMA支持 | CPU全程参与数据传输,无法并行处理 |
| 代码冗余 | 多次重复执行DC线切换操作 |
| 内存碎片 | 频繁调用单字节写入函数产生堆栈压力 |
其中写入一个坐标点产生了多达11次片选CS信号!!!
void LCD_Address_Set(u16 x1, u16 y1, u16 x2, u16 y2)
{
LCD_WR_REG(0x2a); // 1次
LCD_WR_DATA(x1); // 2次
LCD_WR_DATA(x2); // 2次
LCD_WR_REG(0x2b); // 1次
LCD_WR_DATA(y1); // 2次
LCD_WR_DATA(y2); // 2次
LCD_WR_REG(0x2c); // 1次
}
解决方案:
1.DMA批量传输优化
// 新增DMA批量写入函数
void LCD_WR_DATA_BUF(const uint8_t* data, size_t len) {
LCD_CS_Clr();
spi_dma_tx_complete = 0;
HAL_SPI_Transmit_DMA(&hspi1, data, len); // 启动DMA传输
while (!spi_dma_tx_complete); // 等待传输完成
LCD_CS_Set();
}
2.寄存器配置协议重构
// 统一寄存器+数据写入接口
static void LCD_WR_REG_AND_DATA(uint8_t reg, const uint8_t* data, size_t data_len) {
LCD_CS_Clr();
LCD_DC_Clr(); // 写寄存器
HAL_SPI_Transmit_DMA(&hspi1, ®, 1);
while (!spi_dma_tx_complete);
LCD_DC_Set(); // 写数据
HAL_SPI_Transmit_DMA(&hspi1, data, data_len);
while (!spi_dma_tx_complete);
LCD_CS_Set();
}
下列是使用HAL库优化的方案
1.开启SPI+MDA传输
2.添加DMA中断回调函数
volatile uint8_t spi_dma_tx_complete;
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef* hspi)
{
if (hspi->Instance == SPI1) {
spi_dma_tx_complete = 1;
}
}
3.LCD整体优化后的代码
/**
* @brief 连续写入数据
* @param data: 数据指针
* @param len: 数据长度
* @retval None
* */
void LCD_WR_DATA_BUF(const uint8_t* data, size_t len)
{
LCD_CS_Clr();
spi_dma_tx_complete = 0;
HAL_SPI_Transmit_DMA(&hspi1, data, len);
while (!spi_dma_tx_complete)//等待DMA数据搬运完成后再拉高片选
;
LCD_CS_Set();
}
/**
* @brief 写入一个16位数据
* @param dat: 数据
* @retval None
* */
void LCD_WR_DATA(uint16_t dat)
{
uint8_t data[2] = { (dat >> 8), (dat & 0xFF) };
LCD_WR_DATA_BUF(data, sizeof(data));
}
/**
* @brief 写入寄存器地址
* @param dat: 寄存器地址
* @retval None
* */
static void LCD_WR_REG(uint8_t dat)
{
LCD_DC_Clr();
LCD_WR_DATA_BUF(&dat, 1);
LCD_DC_Set();
}
/**
* @brief 写入一个寄存器地址和对应的数据
* @param reg: 寄存器地址
* @param data: 数据指针
* @param data_len: 数据长度
* @retval None
* */
static void LCD_WR_REG_AND_DATA(uint8_t reg, const uint8_t* data, size_t data_len)
{
LCD_CS_Clr();
// 发送寄存器地址
LCD_DC_Clr();
spi_dma_tx_complete = 0;
HAL_SPI_Transmit_DMA(&hspi1, ®, 1);
while (!spi_dma_tx_complete)
;
// 发送数据
LCD_DC_Set();
LCD_WR_DATA_BUF(data, data_len);
LCD_CS_Set();
}
/**
* @brief 设置光标位置
* @param x1: 起始x坐标
* @param y1: 起始y坐标
* @param x2: 结束x坐标
* @param y2: 结束y坐标
* @retval None
* */
void LCD_Address_Set(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2)
{
uint8_t x_params[] = { x1 >> 8, x1 & 0xFF, x2 >> 8, x2 & 0xFF };
LCD_WR_REG_AND_DATA(0x2a, x_params, sizeof(x_params));
uint8_t y_params[] = { y1 >> 8, y1 & 0xFF, y2 >> 8, y2 & 0xFF };
LCD_WR_REG_AND_DATA(0x2b, y_params, sizeof(y_params));
LCD_WR_REG(0x2c);
}
void LCD_Init(void)
{
LCD_RES_Clr(); // 复位LCD
HAL_Delay(100); // 延时100毫秒
LCD_RES_Set(); // 恢复LCD复位
HAL_Delay(100); // 延时100毫秒
LCD_BLK_Set(); // 打开LCD背光
HAL_Delay(100); // 延时100毫秒
//************* 初始化序列 **********//
LCD_WR_REG(0x11); // 退出睡眠模式
HAL_Delay(120); // 延时120毫秒
// 配置寄存器
LCD_WR_REG(0xF0);
LCD_WR_DATA_BUF((uint8_t[]) { 0xC3 }, 1);
LCD_WR_REG(0xF0);
LCD_WR_DATA_BUF((uint8_t[]) { 0x96 }, 1);
// 内存访问控制
const uint8_t madctl_values[] = { 0x48, 0x88, 0x28, 0xE8 };
uint8_t madctl = madctl_values[USE_HORIZONTAL < 3 ? USE_HORIZONTAL : 3];
LCD_WR_REG_AND_DATA(0x36, &madctl, 1);
LCD_WR_REG_AND_DATA(0x3A, (uint8_t[]) { 0x05 }, 1);
// E8 配置
LCD_WR_REG_AND_DATA(0xE8, (uint8_t[]) { 0x40, 0x82, 0x07, 0x18, 0x27, 0x0A, 0xB6, 0x33 }, 8);
LCD_WR_REG_AND_DATA(0xC5, (uint8_t[]) { 0x27 }, 1);
LCD_WR_REG_AND_DATA(0xC2, (uint8_t[]) { 0xA7 }, 1);
// Gamma 校准
LCD_WR_REG_AND_DATA(0xE0, (uint8_t[]) { 0xF0, 0x01, 0x06, 0x0F, 0x12, 0x1D, 0x36, 0x54, 0x44, 0x0C, 0x18, 0x16, 0x13, 0x15 }, 14);
LCD_WR_REG_AND_DATA(0xE1, (uint8_t[]) { 0xF0, 0x01, 0x05, 0x0A, 0x0B, 0x07, 0x32, 0x44, 0x44, 0x0C, 0x18, 0x17, 0x13, 0x16 }, 14);
LCD_WR_REG_AND_DATA(0xF0, (uint8_t[]) { 0x3C }, 1);
LCD_WR_REG_AND_DATA(0xF0, (uint8_t[]) { 0x69 }, 1);
LCD_WR_REG(0x29); // 打开显示
}
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)