项目场景:

在嵌入式显示设备开发中,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, &reg, 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, &reg, 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); // 打开显示
}
Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐