STM32F103驱动ST7789实现多方向TFT-LCD屏幕显示优化
1. 从零开始认识你的硬件伙伴大家好我是老张一个在嵌入式显示领域摸爬滚打了十来年的工程师。今天想和大家聊聊一个非常经典且实用的组合用STM32F103这块“国民MCU”去驱动ST7789控制的TFT-LCD屏幕并且实现灵活的多方向显示。你可能在玩智能手表、小型手持设备或者DIY一个状态显示器时遇到过这样的需求屏幕的物理安装方向是固定的但你想让显示内容能根据设备姿态或用户习惯旋转90度、180度甚至270度。如果每次都要重新计算坐标、重写显示函数那工作量可就太大了。而通过合理配置ST7789的寄存器我们只需要修改几个宏定义就能轻松实现屏幕显示的“乾坤大挪移”。我们先来认识一下今天的主角们。STM32F103C8T6大家亲切地叫它“蓝桥杯最小系统板”或者“核心板”价格亲民资源够用是无数嵌入式爱好者和工程师的入门首选。它的SPI接口速度足够驱动一块中小尺寸的屏幕。另一位主角是ST7789这是一款非常流行的TFT-LCD驱动芯片支持最高240x320的分辨率内部集成了显存GRAM我们通过SPI或者并口把图像数据发给它它就会老老实实地控制屏幕上的每一个像素点亮起来。我手头这块屏是1.69英寸、240x280分辨率的驱动芯片正是ST7789通信方式选择了SPI这样可以节省宝贵的IO口。接线是第一步也是最容易出错的一步。根据我的经验一定要对照屏幕的引脚定义和单片机手册仔细连接。通常ST7789的SPI接口需要这几根线SCLK时钟、SDAMOSI主机输出数据、CS片选低电平有效、DC数据/命令选择这个很重要决定了你发的是命令还是像素数据、RST复位用于硬件复位屏幕、BLK背光控制。在我的配置里SCLK接PA5SDA接PA6CS接PA4DC接PA2RST接PA3背光接PA1。这里有个小细节有些屏幕的背光控制是低电平点亮有些是高电平点亮拿到屏幕最好先测一下或者看数据手册。硬件连接好比盖房子的地基地基稳了后面的代码才能跑得顺畅。2. 打通任督二脉SPI与屏幕初始化详解硬件连好后我们要让单片机和屏幕“对上话”这就是初始化的过程。这个过程就像是给两个讲不同方言的人配备一个翻译并约定好通信规则。对于STM32F103我们首先要初始化用到的GPIO口。上面提到的SCLK、SDA等引脚我们都需要配置为推挽输出模式并把速度设置为50MHz这样才能满足SPI通信的速率要求。片选CS引脚在空闲时要拉高只有在通信时才拉低。背光引脚初始化后可以直接拉高点亮屏幕当然你也可以用PWM来控制亮度实现呼吸灯效果。接下来是重头戏SPI初始化。STM32F103的SPI1挂载在APB2总线上速度可以设得比较高。在初始化SPI时我们需要配置几个关键参数时钟极性CPOL和时钟相位CPHA这决定了数据在时钟的哪个边沿被采样。对于ST7789通常模式0CPOL0 CPHA0或模式3CPOL1 CPHA1都可以具体要看屏幕的数据手册。我习惯用模式0。然后要设置数据帧格式为8位主机模式以及通信的波特率预分频。一开始调试时建议把波特率设低一点比如系统时钟的256分频确保通信稳定等所有功能都调通了再尝试提高速度。void SPI1_Init(void) { SPI_InitTypeDef SPI_InitStructure; // 使能SPI1和GPIOA时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1 | RCC_APB2Periph_GPIOA, ENABLE); // 配置SPI引脚 (PA5-SCK, PA6-MOSI, PA7-未用PA4-NSS自定义为GPIO) GPIO_InitStructure.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 配置PA4为CS引脚普通推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_SetBits(GPIOA, GPIO_Pin_4); // CS拉高 SPI_InitStructure.SPI_Direction SPI_Direction_1Line_Tx; // 单线发送 SPI_InitStructure.SPI_Mode SPI_Mode_Master; // 主机模式 SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; // 8位数据 SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; // 时钟极性低 SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; // 第一个边沿采样 SPI_InitStructure.SPI_NSS SPI_NSS_Soft; // 软件控制NSS 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); }SPI初始化完成后就要开始和ST7789芯片“握手”了。这个过程是通过发送一系列特定的命令Command和数据Data来完成的。首先我们需要拉低RST引脚至少10ms然后再拉高完成一次硬件复位。紧接着发送“睡眠退出”命令0x11并等待120ms让屏幕稳定。之后就是一连串的配置命令用来设置颜色格式、伽马值、电源控制等。这里我强烈建议大家准备一份ST7789的数据手册对照着看每个命令的含义这样出了问题才知道从哪里查起。3. 核心魔法用三个宏定义玩转屏幕方向屏幕能正常点亮显示颜色后我们终于可以进入今天最有趣的部分了控制显示方向。ST7789内部有一个非常重要的寄存器叫做“内存数据访问控制”Memory Data Access Control, MADCTL地址是0x36。通过配置这个寄存器里的几个位我们可以控制屏幕的扫描方向、颜色顺序和行列交换从而实现0°、90°、180°、270°四种显示方向。为了方便在代码中灵活切换我定义了三个宏而不是直接写死寄存器的值。这三个宏分别控制三个关键的位DISPLAY_OVERTURN控制行地址顺序。0表示从上到下扫描1表示从下到上扫描。DISPLAY_BOTTOM_TO_TOP控制列地址顺序。0表示从左到右扫描1表示从右到左扫描。DISPLAY_RIGHT_TO_LEFT控制行列交换。0表示正常行列映射1表示交换行和列的地址。通过这三个宏的0/1组合我们可以定义出四种常用的方向模式// 模式一正常方向0度 #define DISPLAY_OVERTURN 0 #define DISPLAY_BOTTOM_TO_TOP 0 #define DISPLAY_RIGHT_TO_LEFT 0 // 模式二旋转180度 #define DISPLAY_OVERTURN 0 #define DISPLAY_BOTTOM_TO_TOP 1 #define DISPLAY_RIGHT_TO_LEFT 1 // 模式三顺时针旋转90度 #define DISPLAY_OVERTURN 1 #define DISPLAY_BOTTOM_TO_TOP 0 #define DISPLAY_RIGHT_TO_LEFT 1 // 模式四逆时针旋转90度 #define DISPLAY_OVERTURN 1 #define DISPLAY_BOTTOM_TO_TOP 1 #define DISPLAY_RIGHT_TO_LEFT 0在初始化函数中我们根据这些宏的定义来组装发送给0x36寄存器的数据TFT_SEND_CMD(0x36); // 发送内存访问控制命令 uint8_t lcd_data 0; // 根据宏定义设置寄存器位 #if DISPLAY_BOTTOM_TO_TOP lcd_data | (17); // 设置列地址顺序 #else lcd_data | (07); #endif #if DISPLAY_RIGHT_TO_LEFT lcd_data | (16); // 设置行地址顺序 #else lcd_data | (06); #endif #if DISPLAY_OVERTURN lcd_data | (15); // 设置行列交换 #else lcd_data | (05); #endif // 还可以在这里设置RGB/BGR顺序例如 lcd_data | (13) 表示BGR顺序 TFT_SEND_DATA(lcd_data);这样做的好处是显而易见的。当你的项目需要切换屏幕方向时你不需要去修改底层驱动函数只需要在头文件里改一下这三个宏的定义重新编译即可。整个显示逻辑包括画点、画线、显示图片和文字的函数都无需改动因为ST7789芯片已经在硬件层面帮你完成了坐标的转换。这大大提高了代码的复用性和可维护性。我遇到过一些朋友他们为每个方向都写了一套显示函数不仅代码冗余调试起来也异常痛苦。采用这种宏定义控制寄存器的方式可以说是“一劳永逸”。4. 让屏幕“说话”图片与文字的显示实战屏幕方向调好了接下来就是让它显示内容了。最常见的需求就是显示图片和文字。ST7789支持RGB565颜色格式即每个像素用16位数据表示红色占5位绿色占6位蓝色占5位。所以我们要显示的任何素材最终都要转换成RGB565的数组。首先是图片显示。电脑上常见的图片格式如BMP、JPEG通常是RGB88824位色或ARGB32位色我们需要将其转换为RGB565。我最初是用一些现成的电脑端工具转换但总觉得不够灵活后来干脆用QT自己写了一个转换工具。这个工具的核心函数是RGB888toRGB565它提取RGB888颜色中R的高5位、G的高6位和B的高5位拼成一个16位的数。这里有个大坑需要注意Windows系统的BMP文件其像素数据存储顺序是从图片的最后一行、最左边的像素开始的而且是BGR顺序而不是RGB。如果你按顺序读取并转换会发现图片是上下颠倒的。在我的QT工具里读取数据时特意做了行序的反转处理。// 关键代码从下到上读取BMP数据以纠正上下颠倒的问题 for(int i 0; i header.bmpHeight; i) { // 定位到当前行的起始位置从最后一行开始读 file.seek((header.bmpHeight - i - 1) * header.bmpWidth * 3 header.bfOffBits); for(int j 0; j header.bmpWidth; j) { file.read((char*)bmpPixel, sizeof(Pixel)); // 读取BGR数据 // 转换为RGB565 rgb565Data ((bmpPixel.red 3) 11) | ((bmpPixel.green 2) 5) | (bmpPixel.blue 3); // 保存到数组... } }转换得到的数组我会在开头加上两个16位的数分别表示图片的宽度和高度。这样在单片机代码里就可以动态获取图片尺寸而不需要写死。显示图片的函数TFT_display_image逻辑很清晰先读取数组前两个数据得到宽高然后设置显示窗口0x2A和0x2B命令最后发送写GRAM命令0x2C并将整个像素数据数组循环发送出去。然后是文字显示。显示文字的本质是显示一个由“点阵”构成的小图片。我们需要先用取模软件如PCtoLCD2002将汉字或字符转换成点阵数据。取模时要注意设置阴码点亮为1、逐行式、顺向高位在前、C51格式。这样得到的数组每一位就代表屏幕上的一个像素是否需要点亮。显示文字函数的核心是一个二重循环遍历点阵数据的每一个字节的每一个位。如果该位是1就向屏幕发送文字颜色如果是0则发送背景颜色。这里我优化了两次第一次是发现如果背景不是黑色文字周围会有难看的黑色边框。于是我写了第二个函数TFT_display_char16_16_noBackColor当点阵位为0时我不做任何操作不画点这样就实现了“透明”文字效果。但它的缺点是速度慢因为需要调用画点函数而画点函数每次都要设置一次坐标窗口。第二次优化是针对大量文字显示我改回了第一个函数但将背景色设置为当前屏幕的背景色这样虽然牺牲了真正的透明但换来了飞快的显示速度。在实际项目中你需要根据场景在效果和性能之间做权衡。5. 避坑指南与性能优化心得搞嵌入式显示踩坑是家常便饭。我总结了几条特别容易出错的地方希望能帮你节省时间。第一坑SPI通信时序。这是最基础的也最容易出问题。确保你的SPI时钟极性和相位与屏幕要求一致。如果屏幕完全没反应先用逻辑分析仪或者示波器抓一下CS、DC、SCLK和SDA的波形看看命令和数据有没有正确发出。有时候屏幕初始化命令序列中某个延时不够长也会导致初始化失败。ST7789的 datasheet 里对关键命令后的延时都有建议值务必遵守。第二坑坐标系统混乱。这是实现多方向显示后最容易晕的地方。当你通过0x36寄存器旋转了屏幕后屏幕的物理坐标原点0,0可能就跑到另一个角上了。比如顺时针旋转90度后原本屏幕的左上角0,0变成了物理上的左下角。但你的画点函数传入的坐标x,y逻辑上应该还是基于“左上角为原点”的坐标系。好在ST7789帮我们做了映射你只需要用同一套逻辑坐标去画图硬件会帮你转换到正确的物理位置。但设置显示窗口0x2A/0x2B时传入的起始和结束坐标必须是物理坐标。我建议写一个统一的TFT_SetWindows函数在这个函数内部根据当前方向宏的定义对传入的逻辑坐标进行转换再发送给屏幕。第三坑图片数据格式与对齐。前面提到BMP文件的坑上下颠倒、BGR顺序已经让很多人中招了。此外Windows系统下BMP文件的数据行会有“4字节对齐”的规则。如果图片的宽度像素数乘以每个像素的字节数例如3不是4的整数倍系统会在每行末尾自动补零。如果你的转换程序没有跳过这些填充字节显示出来的图片就会错位、倾斜。在读取数据时需要计算每行实际的数据长度并跳过填充部分。关于性能优化对于STM32F103这类主频72MHz的芯片驱动240*280的屏幕全屏刷新图片速度是够用的但追求更流畅的体验可以试试这几招提升SPI速率在初始化稳定后逐步提高SPI的波特率预分频比如从256分频提高到8分频传输速度会快很多。使用DMA传输这是终极提速方案。将需要发送的图片数据数组通过DMA搬运到SPI的数据寄存器CPU在此期间可以处理其他任务实现“后台刷新”。这对于需要频繁更新显示内容或做动画的应用至关重要。局部刷新如果只是更新屏幕的一小块区域比如一个数字只刷新那一部分的窗口而不是全屏刷新可以极大减少数据量。双缓冲机制在内存中开辟两块显存缓冲区CPU在“后台缓冲区”绘制下一帧图像绘制完成后通过DMA快速搬运到屏幕。这可以避免画面撕裂实现更流畅的动画效果不过对单片机RAM是个考验。最后分享一个我调试时的小习惯我会写一个简单的测试函数用不同颜色交替填充屏幕的四个象限或者画一个移动的小方块。这能非常直观地验证屏幕初始化、坐标系统和基本绘图函数是否正确比直接上复杂界面要高效得多。嵌入式显示开发就是这样一点点搭建一步步测试看到屏幕最终亮起并显示出你想要的内容时那种成就感就是最好的回报。希望这些经验能让你在驱动ST7789屏幕的路上少走些弯路。