
简介面向STM32嵌入式开发者的硬件SPI驱动LCD示例演示如何通过STM32的硬件SPI接口完成LCD屏幕的初始化与显示刷新适合需要在工业控制、物联网终端中快速实现图形界面的中初级开发者。压缩包仅含1个C源码文件整体大小约2KB小巧精炼便于直接查阅移植。示例详细覆盖GPIO引脚配置、SPI主模式初始化、LCD初始化命令发送、数据透传以及基于中断的非阻塞传输实现代码段落清晰并给出HAL库函数调用流程可帮助理解SPI时序与LCD驱动底层逻辑与调试思路。已有305人学习下载适合作为竞赛练习、课设参考或快速上手的项目模板。1. 一块LCD的硬件SPI驱动比软件模拟省下的不只是CPU很多嵌入式新手第一次点亮LCD屏习惯直接用GPIO翻转电平来模拟时钟把像素数据一位一位“挤”到屏幕上。这个办法能亮起来但一旦遇到320x240这类TFT屏或者需要在界面上做流畅切换问题就立刻暴露软件模拟的SCLK频率上不去、时序受中断影响抖动大、传输期间CPU被完全占住。这个压缩包里的STM32的硬件SPI驱动LCD例子.c是一套把GPIO复用、SPI外设初始化、LCD命令序列和像素数据传输串起来的参考实现覆盖的正是从“能点灯”到“能刷屏”这一段路。适合刚学完GPIO输出、准备上手显示驱动的开发者也适合做带屏产品原型、需要稳定配置SPI的嵌入式工程师。硬件SPI的第一个优势在频率。STM32F103的SPI1挂在APB2总线上外设时钟最高72MHz分频后SCLK可以从几百kHz一路调到36MHz。软件模拟即使主频同样跑72MHz发送一个字节往往需要几十条指令实际SCLK很少能稳定超过2MHz。两种方案的SCLK差距在240x320 RGB565全屏刷新时直接体现为传输时间的数量级差异。如果再配合DMA搬运CPU占用率可以从接近100%降到几乎为零。不管是在搜索引擎里搜lcd_stm32还是翻论坛里的SPI驱动帖子绕不开的就是这套流程。当然硬件SPI不是配好就能跑通的最容易出问题的恰恰是它那组参数CPOL、CPHA、分频系数、数据帧长度和位顺序每一项配错的现象都不一样。下面先从SPI协议本身的时序参数说起这是后面代码配置的基础。2. SPI时序与引脚约定CLK极性、采样沿和分频选择2.1 四线分工与MISO的真实用途SPI通信的四条信号线各有分工。SCLK由主设备产生决定数据移位的节奏MOSI承载主设备发给从设备的数据MISO是从设备回传数据的通道CS是片选低电平有效只有被选中的从设备才会响应SCLK。驱动LCD时STM32作为主设备LCD作为从设备这是基本的主从关系。在纯刷屏场景里MISO往往整条线都不接。显示只需要单向写数据SCLK加MOSI两条线足以把命令和像素数据送进LCD。只有当LCD支持读取GRAM、或者模块上还集成了触摸屏控制芯片时MISO才真正参与通信。一个值得留意的点是SPI没有应答机制主设备把字节发出去了从设备到底有没有在正确的采样沿收到主设备是无从知晓的。所以显示一旦异常不要急着改上层代码先挂逻辑分析仪看波形用数据说话。2.2 CPOL与CPHA对采样沿的影响CPOL决定SCLK空闲电平CPHA决定数据在第一个还是第二个跳变沿被采样。两两组合形成SPI Mode 0到Mode 3四种模式。LCD控制器的数据手册里一般会标明支持的模式最常见的是Mode 0即CPOL0、CPHA0ST7735、ILI9341都明确支持有些型号同时列出Mode 3。关键是以手册为准不要因为上一块屏用Mode 0正常就默认所有LCD都这样。CPOL配错的典型现象是屏幕能收到命令但数据建立时间不够出现整体偏色或某一类颜色丢失。CPHA配错的感官表现更直接花屏尤其是渐变区域会出现斜纹或杂色。调试时先把模式锁定再对应设置HAL库里的CLKPolarity和CLKPhase可以省掉大量盲目试参数的时间。另外片选信号有硬件NSS和软件片选两种控制方式驱动LCD时普遍把NSS设为软件控制用普通GPIO拉CS这是为了避免硬件NSS在发送时自动切换电平干扰LCD的CS和DC时序配合。2.3 分频系数选择从可靠到高速的切换SPI1的外设时钟来自APB2总线72MHz下波特率发生器支持2到256分频。SCLK频率不是个固定值要结合LCD控制器的上限频率、PCB走线长度和刷新率需求来选。常用分频与场景对应关系如下分频系数SCLK频率适用场景236MHz极短走线、LCD手册明确支持实际工程很少用418MHz同板短走线追求刷新率时选用89MHz大多数TFT屏的默认选择稳定性和速度兼顾164.5MHz杜邦线跨板连接、时序余量不足时的稳妥值322.25MHz调试初期、走线较长或LCD本身时钟上限低分频系数不是越小越好。LCD内部的移位寄存器有建立时间约束SCLK太高时采样点会落到数据稳定区之外表现就是花屏和随机噪点。工程中的常见做法是初始化阶段用32分频保证命令序列被可靠接收初始化完成后重新配置BaudRatePrescaler并调用HAL_SPI_Init切到16分频或8分频。不少开发板的LCD驱动就是这么写的初始化阶段的稳定性和刷屏阶段的速度两头兼顾。除了分频FirstBit也要和LCD手册保持一致。绝大多数LCD控制器按MSB优先接收命令和数据HAL配置里写SPI_FIRSTBIT_MSB一般不会出问题。若换用LSB优先的屏而忘记改像素颜色会整体错乱因为RGB565的高低字节被掉转了。这类错误排查起来很隐蔽因为波形是完好的问题只在数据解释层。提示验证分频参数是否合理时不要只发几个字节就下结论。连续刷新几十帧渐变图观察有没有细小噪点或周期性杂色比单帧纯色测试可靠得多。3. 初始化代码拆解GPIO复用、SPI参数与LCD命令写入3.1 GPIO复用配置SCK和MOSI必须交给外设STM32的引脚要作为外设功能使用必须先配成复用模式。以SPI1为例常见接法是SCKPA5、MOSIPA7也有重映射到PB3/PB5的。如果GPIO_InitStruct.Mode写成了普通推挽输出而不是复用推挽SPI外设完全无法控制引脚电平发送函数返回正常但屏幕上不会有数据。这个错误在CubeMX生成的代码里不会出现但手写寄存器或移植旧工程时经常踩到。/* 使能GPIOA和SPI1时钟 */ __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_SPI1_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct {0}; /* SCKPA5, MOSIPA7 配置为复用推挽输出 */ GPIO_InitStruct.Pin GPIO_PIN_5 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); /* PA4 作为普通输出用于CS片选 */ GPIO_InitStruct.Pin GPIO_PIN_4; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); /* CS 初始置高 */代码里SCK和MOSI的Speed写HIGH在低频SPI下不影响功能真正关键的是复用模式和时钟使能的顺序。CS引脚配成普通输出后要立刻置高避免上电瞬间LCD被误选中。CS本身不要求高频翻转Speed用LOW即可反而能减少信号边沿对其他引脚的耦合干扰。3.2 SPI外设初始化每个字段的调整依据HAL库的SPI初始化集中在这个结构体赋值里每个参数都和LCD控制器型号强相关SPI_HandleTypeDef hspi1 {0}; hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; /* STM32做主机 */ hspi1.Init.Direction SPI_DIRECTION_2LINES; /* 全双工 */ hspi1.Init.DataSize SPI_DATASIZE_8BIT; /* LCD控制器以8位为命令帧 */ hspi1.Init.CLKPolarity SPI_POLARITY_LOW; /* CPOL0 */ hspi1.Init.CLKPhase SPI_PHASE_1EDGE; /* CPHA0Mode 0 */ hspi1.Init.NSS SPI_NSS_SOFT; /* 片选交给GPIO */ hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_16; /* 4.5MHz */ hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; /* 高位先出 */ HAL_SPI_Init(hspi1);SPI_DIRECTION_2LINES是全双工模式发送和接收各占一条线。有人会问只用发送为什么不用单线半双工答案很简单保留MISO通道后续如果要回读LCD状态寄存器或者接触摸屏不需要改初始化。SPI_NSS_SOFT是驱动LCD的惯例写法硬件NSS在每次传输时自动拉低拉高CS做不到和DC引脚逐字节配合软件片选则可以把CS的时序控制得更精细。分频系数按第2章的表格选型16分频大约4.5MHz适合初次上电调试。3.3 命令和数据怎么写DC引脚与CS的先后顺序LCD控制器区分命令和数据的唯一依据是DC引脚电平。DC低电平表示当前发送的是命令高电平表示数据。所以封装两个底层函数是必须的static void LCD_Write_Cmd(uint8_t cmd) { HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); /* 片选拉低 */ HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, GPIO_PIN_RESET); /* 命令模式 */ HAL_SPI_Transmit(hspi1, cmd, 1, 100); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); /* 片选拉高 */ } static void LCD_Write_Data(uint8_t data) { HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, GPIO_PIN_SET); /* 数据模式 */ HAL_SPI_Transmit(hspi1, data, 1, 100); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); }HAL_SPI_Transmit第三个参数是发送数据的字节数第四个是超时毫秒数轮询超时能避免SPI硬件异常时主程序卡死。DC的电平切换必须发生在CS拉低之后、发送函数调用之前。有些代码先切DC再拉CS大多数LCD控制器不报错但严格的时序手册会要求在CS有效后再改变DC状态否则第一个字节可能丢失。这种问题用逻辑分析仪才抓得到属于典型的“软故障”。3.4 LCD复位与初始化命令的组织顺序初始化命令不是随便发的。以ST7735为例标准顺序是软复位、退出睡眠、设置扫描方向和显存格式然后配置帧率、偏压和伽马最后开显示。各阶段的典型命令和注意事项整理如下阶段典型命令作用注意事项软件复位0x01复位内部逻辑发送后延时150ms退出睡眠0x11唤醒显示电路发送后延时120ms显示参数0xB1、0xB4等帧率、偏压、伽马参数随面板型号变化开显示0x29输出显示内容出现画面前最后一步硬件复位引脚RST同样不能省。常见做法是在LCD_Init()开头把RST拉低20ms再拉高再延时120ms让内部电源稳定然后才开始发送命令序列。RST的复位脉冲长度和SPI波特率无关这是LCD控制器本身的电源时序要求。若跳过复位部分LCD在异常掉电重启后会对初始化命令无响应屏幕保持白屏或黑屏。4. 窗口设置、显存分块与DMA搬运优化4.1 用窗口命令限定刷新区域TFT LCD的GRAM是一整块显存配合窗口命令可以让连续写入的像素数据自动落在指定矩形区域内地址自增不需要逐点切换行列。窗口设置的顺序是固定的先CASET列地址再RASET行地址最后RAMWR写显存LCD_Write_Cmd(0x2A); /* CASET: 列地址设置 */ LCD_Write_Data(0x00); LCD_Write_Data(0x00); /* 列起始 0 */ LCD_Write_Data(0x00); LCD_Write_Data(0xEF); /* 列结束 239 */ LCD_Write_Cmd(0x2B); /* RASET: 行地址设置 */ LCD_Write_Data(0x00); LCD_Write_Data(0x00); /* 行起始 0 */ LCD_Write_Data(0x00); LCD_Write_Data(0x13); /* 行结束 319 */ LCD_Write_Cmd(0x2C); /* RAMWR: 写显存命令 */四个地址字节按高字节在前发送这是绝大多数LCD控制器的约定。窗口设置完成后后续发送的每个字节都落在窗口内不需要反复发命令。如果只是重绘一个按钮或进度条窗口命令能把数据量从全屏的153600字节压缩到目标区域的大小效果非常明显。4.2 整帧缓冲放不下时的分块策略刷新整帧需要2403202共153600字节的像素数据。STM32F103C8T6的SRAM只有20KB放不下整帧所以现实做法是把显存拆成行缓冲分块发送。按16行一组是比较常见的取舍#define LCD_WIDTH 240 #define LCD_BUF_LINE 16 static uint16_t lcd_buf[LCD_WIDTH * LCD_BUF_LINE]; /* 240*16*27.5KB */ void LCD_FillRect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) { uint32_t i; LCD_SetWindow(x, y, x w - 1, y h - 1); LCD_Write_Cmd(0x2C); for (uint16_t row 0; row h; row LCD_BUF_LINE) { uint16_t lines (h - row LCD_BUF_LINE) ? LCD_BUF_LINE : (h - row); uint32_t words (uint32_t)LCD_WIDTH * lines; for (i 0; i words; i) lcd_buf[i] color; HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, GPIO_PIN_SET); HAL_SPI_Transmit(hspi1, (uint8_t *)lcd_buf, words * 2, 1000); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); } }这段代码把窗口按16行切块每块占7.5KB RAM在F103上能跑。块内行数LCD_BUF_LINE是调优的关键设太大接近整帧缓冲RAM不够设太小窗口切换和CS翻转次数增加传输效率下降。如果你用的是F407或更大内存的芯片直接加大这个值或者申请整帧缓冲能少写不少循环。4.3 DMA搬运的配置与完成回调阻塞式HAL_SPI_Transmit每次发送都轮询TXE标志CPU空转。换DMA后内存到SPI数据寄存器的搬运由DMA控制器完成CPU可以去处理触摸扫描或UI逻辑。DMA初始化代码如下__HAL_RCC_DMA1_CLK_ENABLE(); DMA_HandleTypeDef hdma_spi1_tx {0}; hdma_spi1_tx.Instance DMA1_Channel3; /* SPI1_TX 默认映射 */ hdma_spi1_tx.Init.Direction DMA_MEMORY_TO_PERIPH; hdma_spi1_tx.Init.PeriphInc DMA_PINC_DISABLE; /* 外设地址固定 */ hdma_spi1_tx.Init.MemInc DMA_MINC_ENABLE; /* 内存地址递增 */ hdma_spi1_tx.Init.PeriphDataAlignment DMA_PDATAALIGN_BYTE; hdma_spi1_tx.Init.MemDataAlignment DMA_MDATAALIGN_HALFWORD; hdma_spi1_tx.Init.Mode DMA_NORMAL; hdma_spi1_tx.Init.Priority DMA_PRIORITY_HIGH; HAL_DMA_Init(hdma_spi1_tx); __HAL_LINKDMA(hspi1, hdmatx, hdma_spi1_tx);注意PeriphDataAlignmentBYTE搭配MemDataAlignmentHALFWORD外设端SPI寄存器按8位写入内存端像素按16位读取。如果把内存对齐也设成BYTEDMA会把每个像素高低字节按小端序拆开搬运显示时蓝红互换。颜色错乱时这两行往往是最后查到的地方。另外对F103来说SPI1_TX占用DMA1_Channel3SPI1_RX是DMA1_Channel2发送和接收各走各的通道不是非要绑定在一起只写LCD的话一个TX方向就够。DMA发送完成后CS的拉高操作要放在传输完成回调中void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); } }如果不这样做而是调用完HAL_SPI_Transmit_DMA后立刻把CS拉高屏幕尾部会出现半截花屏。原因很简单函数返回时数据可能还在SPI移位寄存器里此时拉高CS会截断最后几个字节。这是DMA模式与阻塞模式在流程控制上最本质的差别。4.4 全屏刷新的理论耗时与实测差异刷新耗时可以做粗略估算。SCLK4.5MHz时153600字节的传输时间约为0.27秒9MHz时约0.14秒18MHz时约0.07秒。注意这是纯数据搬运时间实际还要算命令间隔、窗口设置和代码执行开销。SCLK提上去之后如果CS翻转太频繁刷新时间并不会线性下降。优化顺序应该是先把分块缓冲和DMA做好再考虑拉高SPI频率最后在UI层面缩小无效刷新区域。提示DMA发送期间不能修改正在被读取的缓冲区否则会出现画面撕裂。需要双缓冲时必须等上一次HAL_SPI_TxCpltCallback之后再切缓冲。5. 逻辑分析仪验证时序与排查信号故障5.1 用0x55做SPI自检波形初始化代码跑通之前可以先写几行循环发送0x55或0xAA测试字节把SCK和MOSI挂到逻辑分析仪抓波形。0x55的二进制是01010101分析仪上能看到规整的方波频率、极性和数据位顺序一眼就能确认。如果波形正常再接LCD做初始化排查范围就缩小到命令序列或硬件连接而不是把时间浪费在猜配置上。这个习惯在换新屏、换新板子时特别省时间。5.2 常见时序异常与定位方法异常现象可能原因排查思路CS有效期间无SCLKGPIO复用配置错误或SPI时钟未使能检查AF_PP设置和时钟使能SCLK频率与配置不符分频系数和外设时钟算错确认SPI挂在APB1还是APB2MOSI数据位反序FirstBit配成了LSB改为MSB后重新抓包花屏且颜色错乱CPHA/CPOL配错对照LCD手册确认Mode屏幕有细小噪点SCLK过高或走线过长降分频或者改短排线5.3 用sigrok命令行解析SPI抓包结果逻辑分析仪保存的采样文件可以用sigrok-cli在命令行下解析出SPI总线上的完整数据流sigrok-cli -i capture.sr -C D0SCK,D1MOSI,D2CS,D3DC \ -P spi:clkSCK:mosiMOSI:csCS \ -A spispi-packet-decode \ -o spi_packets.csv-C把物理通道编号映射成SCK、MOSI这些逻辑名-P spi指定SPI协议解码器-A是注解输出模式。导出的CSV里每一行能看到字节值和相对时间和代码里发送的命令逐条对照即可。采样率建议设为SCLK的4倍以上比如4.5MHz的SCLK用25MHz采样否则会出现混叠频率读数偏差一倍数。5.4 上电时序与CS初始状态最后说一个经常让人查半天的坑上电时序。MCU复位期间GPIO处于输入浮动状态CS电平不确定如果LCD模块的CS引脚恰好为低上电瞬间的随机波形会被当成命令或数据写入显存。解决方法是软硬结合硬件上在CS引脚加10k上拉电阻软件上GPIO配置完成后立刻把CS写高再拉低RST完成复位脉冲然后初始化SPI最后拉低CS开始发命令。顺序反过来一样会出问题——先初始化SPI再拉CSGPIO配置过程中产生的毛刺照样会误触发LCD。CS初始电平、RST复位脉冲、DC与CS的先后顺序、DMA完成回调这四个环节把LCD驱动里绝大多数隐性故障都覆盖了。配置完一轮时序参数逻辑分析仪挂在四条线上跑一帧数据哪里先错哪里后错基本一目了然。本文还有配套的精品资源点击获取