STM32F103通过SPI驱动ST7735s TFT彩屏:从CubeMX HAL库到标准库的移植与优化实践
1. 硬件准备与环境搭建第一次接触STM32驱动TFT彩屏时我对着淘宝买来的1.44寸ST7735s屏幕发了半天呆。这块128*128分辨率的小屏虽然只有拇指大小但引脚密密麻麻排了两排。先别急着写代码硬件连接是第一步这里有几个容易踩坑的地方开发板我用的是最常见的STM32F103C8T6最小系统板和屏幕通过杜邦线连接。具体接线时要注意ST7735s的SPI接口有硬件和软件两种接法。硬件SPI需要用到单片机特定的SPI引脚PA5/PA6/PA7而软件SPI可以任意指定GPIO。我建议新手先用硬件SPI稳定性更好。接线表示例SCK → PA5 (硬件SPI时钟)MOSI → PA7 (硬件SPI数据输出)DC → PB0 (数据/命令切换)RST → PB1 (复位控制)CS → PB10 (片选)VCC → 3.3VGND → GND注意ST7735s的工作电压是3.3V直接接5V可能会烧毁屏幕。背光控制如果不需要调光可以直接接3.3V常亮。开发环境我选择了Keil MDK CubeMX的组合。CubeMX可以快速生成初始化代码特别适合不熟悉底层寄存器的开发者。新建工程时记得选择正确的芯片型号在Connectivity中启用SPI1配置为全双工主机模式时钟分频先设为256后续可以调整。2. CubeMX HAL库驱动实现用HAL库开发就像用自动挡开车省去了很多底层操作。在CubeMX中配置好SPI后重点要关注几个参数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; hspi1.Init.CLKPhase SPI_PHASE_1EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_256; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB;屏幕初始化需要按照ST7735s的时序要求发送一系列命令。我参考数据手册整理了一个初始化序列void LCD_Init(void) { LCD_RST_LOW(); HAL_Delay(100); LCD_RST_HIGH(); HAL_Delay(120); LCD_Write_Cmd(0x11); // Sleep out HAL_Delay(120); LCD_Write_Cmd(0xB1); // FRMCTR1 LCD_Write_Data(0x05); LCD_Write_Data(0x3C); LCD_Write_Data(0x3C); // 更多初始化命令... LCD_Write_Cmd(0x29); // Display on }实际测试发现HAL库的SPI传输函数HAL_SPI_Transmit()在高速时会有明显延迟。经过多次尝试我改用寄存器直接操作的方式优化了写数据函数void LCD_Write_Data(uint8_t data) { LCD_DC_HIGH(); SPI1-DR data; while(!(SPI1-SR SPI_SR_TXE)); while((SPI1-SR SPI_SR_BSY)); }3. 标准库移植关键步骤当项目需要从HAL库迁移到标准库时最大的变化在于SPI和GPIO的底层驱动方式。标准库更接近硬件需要手动配置更多寄存器。首先重写SPI初始化函数void SPI1_Init(void) { SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_256; SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_InitStructure.SPI_CRCPolynomial 7; SPI_Init(SPI1, SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }标准库的GPIO配置也需要特别注意void GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; // SPI引脚配置 GPIO_InitStructure.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 控制引脚配置 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOB, GPIO_InitStructure); }4. 显示优化与性能提升驱动调通只是第一步要让显示效果更好还需要一些技巧。我发现ST7735s的刷新率直接影响了显示流畅度。通过调整SPI时钟分频可以提升刷新速度// 在初始化后将分频从256改为8 SPI1-CR1 ~SPI_CR1_BR; SPI1-CR1 | SPI_BAUDRATEPRESCALER_8;但要注意过高的SPI速度可能导致屏幕无法正常响应。经过测试STM32F103在72MHz主频下SPI分频设为8约9MHz是稳定工作的上限。区域刷新是另一个优化点。全屏刷新速度慢且没必要可以通过设置窗口只刷新特定区域void LCD_SetWindow(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { LCD_Write_Cmd(0x2A); // 列地址设置 LCD_Write_Data(x18); LCD_Write_Data(x10xFF); LCD_Write_Data(x28); LCD_Write_Data(x20xFF); LCD_Write_Cmd(0x2B); // 行地址设置 LCD_Write_Data(y18); LCD_Write_Data(y10xFF); LCD_Write_Data(y28); LCD_Write_Data(y20xFF); LCD_Write_Cmd(0x2C); // 写入RAM }对于需要频繁更新的内容如动态波形可以预先分配缓冲区uint16_t buffer[128]; // 一行像素的缓冲区 void LCD_UpdateLine(uint16_t y, uint16_t color) { LCD_SetWindow(0, y, 127, y); for(int x0; x128; x) { buffer[x] color; } LCD_Write_Bulk(buffer, sizeof(buffer)); }5. 图形显示实战技巧显示文字和图片是TFT屏的常见应用。ST7735s使用16位RGB565格式每个像素用2字节表示。汉字显示需要先取模。我用PCtoLCD2002软件生成32x32点阵的字模// 电字的点阵数据示例 const uint8_t font_dian[] { 0x00,0x00,0x00,0x00,0x03,0xFF,0x02,0x01, 0x02,0x01,0x02,0x01,0x02,0x01,0x03,0xFF, // 更多数据... };显示函数需要处理像素映射void LCD_ShowFont(uint16_t x, uint16_t y, uint16_t fc, uint16_t bc, uint8_t *font) { uint16_t i,j; uint8_t temp; for(i0; i32; i) { temp font[i]; for(j0; j8; j) { if(temp 0x80) LCD_DrawPoint(xj, yi, fc); else LCD_DrawPoint(xj, yi, bc); temp 1; } } }图片显示则需要先将图片转为C数组。使用Img2Lcd软件转换时要注意图片尺寸调整为128x128输出格式选择16位真彩色扫描模式选择垂直扫描// 图片数据示例 const uint16_t gImage_test[16384] { 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 更多数据... }; void LCD_ShowPicture(uint16_t x, uint16_t y, uint16_t width, uint16_t height, const uint16_t *pic) { LCD_SetWindow(x, y, xwidth-1, yheight-1); for(uint32_t i0; iwidth*height; i) { LCD_Write_Data(pic[i]8); LCD_Write_Data(pic[i]0xFF); } }6. 常见问题排查调试过程中我遇到过不少问题这里分享几个典型案例屏幕白屏无显示检查背光是否开启测量各引脚电压是否正常确认复位时序是否正确复位脉冲宽度需大于10μs显示颜色异常确认RGB颜色格式设置ST7735s常用BGR顺序检查SPI数据位顺序MSB/LSB验证初始化命令是否完整发送显示内容错位检查屏幕扫描方向设置0x36命令确认窗口设置范围是否正确验证坐标系统是否与屏幕物理布局匹配SPI通信失败用逻辑分析仪抓取波形检查时钟极性和相位设置降低SPI速度测试稳定性记得在代码中添加调试信息输出方便定位问题printf(SPI Init Complete\n); printf(LCD ID: 0x%04X\n, LCD_Read_ID());7. 工程架构建议随着功能增加代码需要更好的组织。我推荐这样的文件结构/Driverslcd.c/lcd.h - 屏幕驱动spi.c/spi.h - SPI底层font.c/font.h - 字库/Applicationui.c/ui.h - 界面逻辑main.c - 主程序关键数据结构设计typedef struct { uint16_t width; uint16_t height; uint8_t dir; uint16_t id; } LCD_Dev; typedef struct { uint8_t *table; uint16_t width; uint16_t height; } FontDef;对于需要频繁调用的函数可以封装成宏提高效率#define LCD_WR_DATA(data) do { \ LCD_DC_HIGH(); \ SPI1-DR (data); \ while(!(SPI1-SR SPI_SR_TXE)); \ } while(0)移植到其他平台时只需要替换底层驱动即可。这种分层设计让代码更易维护和扩展。