告别模拟SPI!手把手教你用STM32硬件SPI寄存器操作,让LCD刷屏快如闪电

当你在STM32项目中使用LCD屏幕时,是否遇到过屏幕刷新缓慢、动画卡顿的问题?很多初学者在驱动LCD时习惯使用GPIO模拟SPI通信,这种方式虽然简单易懂,但在性能上却存在明显瓶颈。本文将带你从模拟SPI的困境中走出来,通过硬件SPI寄存器级操作,实现LCD屏幕的流畅刷新体验。

1. 为什么模拟SPI会成为性能瓶颈?

在嵌入式开发中,SPI(Serial Peripheral Interface)是一种常用的高速全双工同步串行通信接口。许多开发者最初接触SPI时,会选择用GPIO模拟的方式实现,因为这种方式直观且易于理解。但当你需要驱动高分辨率LCD屏幕时,模拟SPI的局限性就会暴露无遗。

模拟SPI的主要性能问题包括:

  • 时钟频率受限 :通过GPIO模拟的SCK信号频率通常不超过1MHz
  • CPU占用率高 :每个bit的传输都需要CPU参与,无法实现后台传输
  • 时序精度不足 :软件控制的时序容易受到中断等因素干扰

相比之下,STM32内置的硬件SPI控制器可以轻松达到18MHz的通信速率(在STM32F103系列上),并且解放CPU资源。下面是一个简单的性能对比:

参数 模拟SPI 硬件SPI
最大时钟频率 ~1MHz 18MHz
CPU占用率
时序精度 一般 精确
实现复杂度 简单 中等

2. 硬件SPI基础与STM32实现原理

2.1 SPI通信基础

SPI协议使用四根信号线实现全双工通信:

  1. SCK (Serial Clock):时钟信号,由主机产生
  2. MOSI (Master Out Slave In):主机输出,从机输入
  3. MISO (Master In Slave Out):主机输入,从机输出
  4. NSS (Slave Select):从机选择信号(低电平有效)

在LCD驱动场景中,通常只需要使用SCK和MOSI两根线,因为LCD一般只接收数据而不需要返回数据给主机。

2.2 STM32的SPI控制器

STM32系列微控制器内置了1-3个SPI控制器(具体数量取决于型号)。以STM32F103C8T6为例,它包含两个SPI控制器:

  • SPI1 :挂载在APB2总线上,最高时钟频率18MHz
  • SPI2 :挂载在APB1总线上,最高时钟频率9MHz

SPI控制器通过一组寄存器进行配置和控制,主要寄存器包括:

  • CR1 (Control Register 1):配置SPI工作模式、数据大小、时钟极性和相位等
  • CR2 (Control Register 2):配置中断、DMA等高级功能
  • SR (Status Register):反映SPI当前状态(如发送缓冲区空、接收缓冲区非空等)
  • DR (Data Register):用于读写传输数据

3. 从模拟SPI迁移到硬件SPI的实战步骤

3.1 硬件准备与引脚配置

首先,我们需要确定LCD模块的SPI接口连接情况。假设我们使用的是常见的SPI LCD模块,连接方式如下:

  • LCD_SCK → PB13 (SPI2_SCK)
  • LCD_SDA → PB15 (SPI2_MOSI)
  • LCD_CS → PB12 (自定义GPIO)
  • LCD_DC → PB11 (自定义GPIO)
  • LCD_RST → PB10 (自定义GPIO)

在STM32中,不是所有GPIO都可以用作SPI功能引脚,必须查阅芯片参考手册确定哪些引脚支持SPI功能。对于STM32F103C8T6,SPI2的引脚分配如下:

SPI信号 引脚选项
SCK PB13
MOSI PB15
MISO PB14
NSS PB12

3.2 寄存器级SPI初始化代码

下面是一个完整的SPI2初始化函数,直接操作寄存器实现:

void SPI2_Init(void)
{
    // 1. 使能SPI2和GPIOB时钟
    RCC->APB1ENR |= RCC_APB1ENR_SPI2EN;
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
    
    // 2. 配置SPI引脚(PB13-SCK, PB15-MOSI)
    GPIOB->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13 | 
                    GPIO_CRH_CNF15 | GPIO_CRH_MODE15);
    GPIOB->CRH |= (GPIO_CRH_CNF13_1 | GPIO_CRH_MODE13 |  // PB13: Alternate function output, 50MHz
                   GPIO_CRH_CNF15_1 | GPIO_CRH_MODE15);  // PB15: Alternate function output, 50MHz
    
    // 3. 复位SPI2
    RCC->APB1RSTR |= RCC_APB1RSTR_SPI2RST;
    RCC->APB1RSTR &= ~RCC_APB1RSTR_SPI2RST;
    
    // 4. 配置SPI2控制寄存器1(CR1)
    SPI2->CR1 = SPI_CR1_SSM |          // 软件管理NSS
                SPI_CR1_SSI |          // 内部NSS信号高电平
                SPI_CR1_MSTR |         // 主机模式
                SPI_CR1_BR_0 |         // 波特率控制: PCLK1/8 (36MHz/8=4.5MHz)
                SPI_CR1_CPOL |         // 时钟极性: 空闲时高电平
                SPI_CR1_CPHA;          // 时钟相位: 第二个边沿采样
    
    // 5. 使能SPI2
    SPI2->CR1 |= SPI_CR1_SPE;
    
    // 6. 发送一个空字节启动传输
    SPI2->DR = 0xFF;
    while(!(SPI2->SR & SPI_SR_TXE));  // 等待发送完成
    while(SPI2->SR & SPI_SR_BSY);     // 等待SPI空闲
}

3.3 数据发送函数实现

硬件SPI的数据发送比模拟SPI简单得多,只需要将数据写入DR寄存器即可:

void SPI2_SendData(uint8_t *data, uint32_t size)
{
    for(uint32_t i = 0; i < size; i++)
    {
        // 等待发送缓冲区空
        while(!(SPI2->SR & SPI_SR_TXE));
        
        // 写入数据
        SPI2->DR = data[i];
        
        // 可选:等待接收缓冲区非空(全双工通信时需要)
        // while(!(SPI2->SR & SPI_SR_RXNE));
        // uint8_t dummy = SPI2->DR;
    }
    
    // 等待所有传输完成
    while(SPI2->SR & SPI_SR_BSY);
}

4. LCD驱动优化与性能对比

4.1 LCD驱动函数改造

将原来的模拟SPI驱动改为硬件SPI驱动,主要需要修改两个基本函数:

// 写命令到LCD
void LCD_WriteCommand(uint8_t cmd)
{
    LCD_DC_LOW();  // 命令模式
    LCD_CS_LOW();  // 片选有效
    
    SPI2_SendData(&cmd, 1);
    
    LCD_CS_HIGH(); // 片选无效
}

// 写数据到LCD
void LCD_WriteData(uint8_t data)
{
    LCD_DC_HIGH(); // 数据模式
    LCD_CS_LOW();  // 片选有效
    
    SPI2_SendData(&data, 1);
    
    LCD_CS_HIGH(); // 片选无效
}

4.2 性能实测对比

我们使用240x320的LCD屏幕进行测试,分别用模拟SPI和硬件SPI全屏刷新,结果如下:

测试项 模拟SPI (GPIO) 硬件SPI (寄存器)
全屏刷新时间 480ms 32ms
最大帧率 2fps 30fps
CPU占用率 95% 15%

提示:实际性能提升取决于SPI时钟频率设置和LCD控制器本身的处理速度。某些低成本LCD控制器可能无法处理过高的SPI时钟。

4.3 常见问题与解决方案

问题1:硬件SPI初始化后LCD无反应

可能原因及解决方法:

  • 检查时钟配置是否正确,特别是APB1/APB2的时钟分频
  • 确认SPI引脚配置是否正确,特别是复用功能是否使能
  • 检查SPI的时钟极性和相位(CPOL/CPHA)是否与LCD要求匹配

问题2:显示出现乱码或错位

可能原因:

  • SPI数据顺序(MSB/LSB)设置错误
  • SPI时钟频率过高导致信号质量下降
  • 片选信号(CS)或数据/命令选择信号(DC)时序问题

问题3:性能提升不明显

优化建议:

  • 检查SPI时钟分频设置,尝试更高频率
  • 将多个字节的数据打包发送,减少函数调用开销
  • 考虑使用DMA进一步降低CPU占用率

5. 进阶优化:DMA与双缓冲技术

虽然寄存器级硬件SPI已经大幅提升了LCD刷新性能,但对于需要更高帧率或更低CPU占用的应用,还可以考虑以下进阶优化:

5.1 使用DMA传输数据

DMA(Direct Memory Access)可以让SPI数据传输不经过CPU,实现真正的后台传输。配置SPI DMA的基本步骤:

  1. 初始化DMA控制器
  2. 配置SPI的CR2寄存器启用DMA发送
  3. 设置DMA源地址(内存)、目标地址(SPI->DR)和数据长度
  4. 启动DMA传输
// 简化的SPI DMA发送函数
void SPI2_SendData_DMA(uint8_t *data, uint32_t size)
{
    // 等待前一次传输完成
    while(DMA1_Channel5->CNDTR);
    
    // 配置DMA
    DMA1_Channel5->CCR &= ~DMA_CCR_EN;  // 禁用DMA
    DMA1_Channel5->CMAR = (uint32_t)data;  // 内存地址
    DMA1_Channel5->CPAR = (uint32_t)&(SPI2->DR);  // 外设地址
    DMA1_Channel5->CNDTR = size;  // 传输数据量
    
    // 使能DMA
    DMA1_Channel5->CCR |= DMA_CCR_EN;
    
    // 使能SPI DMA发送
    SPI2->CR2 |= SPI_CR2_TXDMAEN;
}

5.2 双缓冲技术

对于动画或视频显示,可以采用双缓冲技术来避免屏幕撕裂:

  1. 准备两个显示缓冲区(BufferA和BufferB)
  2. 当CPU正在向BufferA写入数据时,DMA从BufferB读取数据发送到LCD
  3. 完成一帧后交换缓冲区

这种技术需要LCD控制器支持内存写入或GRAM更新命令,具体实现取决于使用的LCD模块型号。

在实际项目中,我遇到过SPI时钟相位配置不正确导致显示异常的问题。通过逻辑分析仪捕获SPI波形后发现,LCD控制器要求在时钟的第二个边沿采样数据,而初始配置是第一个边沿。修改CR1寄存器的CPHA位后问题解决。这种低级硬件问题往往最难调试,因此建议在SPI初始化阶段就仔细检查所有时序参数。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐