简介:STM32F103C8T6配套程序源码是一份以LCD1602字符液晶显示为核心的嵌入式开发例程,附带C/C++环境下的彩屏驱动思路,适合刚接触STM32的入门者,也适合需要复用显示代码的进阶开发者。压缩包共121个文件,以29个h头文件、28个c源文件为主,另含uvproj工程文件、hex/axf编译结果、备份调试文件及辅助配置,整体仅1.3MB,结构紧凑、便于快速定位。目前已有2671人学习下载,具备较高的参考热度。源码覆盖GPIO推挽输出配置、RS/E/D0-D7控制时序、液晶初始化序列、字符编码映射等LCD1602驱动全流程;彩屏部分则涉及SPI/I2C接口选择、帧缓冲组织与DMA刷新,可帮助学习者系统建立显示类外设的开发框架。配套工程可直接打开编译烧录,适合按步骤对照学习,能有效缩短从原理到落地的调试周期。
1. 一块蓝板子,从字符屏到彩屏的完整驱动链路
常见做法,拿到STM32F103C8T6最小系统板,第一步不外乎点个灯、跑个串口,再往前就是驱动屏。LCD1602是字符屏里的入门钉子户,TFT彩屏则是图形界面的第一个台阶。标题里的这套配套源码,本质上是把两种屏幕的驱动路径都铺好了:底层用C操作寄存器与管脚时序,应用层用C++封装成可复用的类。适合刚踩进嵌入式门槛、手上有蓝板子但不知道代码怎么组织的开发者,也适合想从裸机过渡到FreeRTOS的人——两个屏都跑通之后,任务的划分、锁的保护、中断的归属就都有载体可以练了。
这套代码最值得拆的不是“能显示”,而是C与C++如何在同一工程里分工:驱动层必须贴着硬件、用C表达时序和寄存器操作,应用层则可以用C++的类把“清屏、写字符串、画矩形”这些动作收拢。本文按显示驱动的最小原理、LCD1602的GPIO时序实现、TFT彩屏的SPI与DMA路径、工程组织与FreeRTOS整合、最后是调试技巧的顺序展开,每一段都带可抄的参数表和代码。
2. LCD1602驱动:GPIO模拟时序与HD44780协议
2.1 为什么LCD1602还在用 GPIO 模拟,而不是走总线
LCD1602内部的HD44780控制器是1970年代末的设计,接口是8位或4位并行数据总线,加RS(寄存器选择)、RW(读/写)、E(使能)三条控制线。STM32F103C8T6没有专门的LCD并行总线外设,最可靠的办法就是拿GPIO去模拟时序。这也是配套源码里最底层的部分——它不依赖任何库函数,直接操作GPIO的ODR和IDR寄存器。
模拟时序的原理并不复杂:把数据放到并行引脚上,拉高E,保持一个最小宽度,再拉低E。芯片在E的下降沿锁存数据。难点在于HD44780对最小脉宽有硬性要求,E的高电平持续时间不能低于450ns(VDD=5V时),而STM32F103C8T6跑在72MHz,一条普通IO操作大约十几纳秒,如果代码不做点空转,一个周期会压到几百纳秒以内,部分LCD模块会偶发漏字。
#define LCD_D4_PORT GPIOB #define LCD_D4_PIN GPIO_Pin_0 #define LCD_D5_PIN GPIO_Pin_1 #define LCD_D6_PIN GPIO_Pin_10 #define LCD_D7_PIN GPIO_Pin_11 #define LCD_RS_PIN GPIO_Pin_12 #define LCD_E_PIN GPIO_Pin_13 void LCD_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = LCD_D4_PIN | LCD_D5_PIN | LCD_D6_PIN | LCD_D7_PIN | LCD_RS_PIN | LCD_E_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); }逻辑说明:这是4位模式的GPIO初始化,只用PB0、PB1、PB10、PB11做数据,PB12做RS,PB13做E。为什么选4位模式?因为8位模式要占8根数据线加3根控制线共11个引脚,F103C8T6虽然引脚够,但留给其他外设的空间就少了。4位模式只需7个引脚,代价是每次传输要拆成高4位、低4位两次发送,速度降一半——对于1602这种刷新率只有几十赫兹的字符屏,完全够用。参数方面,GPIO_Mode_Out_PP是推挽输出,GPIO_Speed_50MHz保证翻转速率足够;两条数据线复用了PB0、PB1这两个默认的JTAG引脚,如果下载调试异常,方案是换到PB3~PB7,或者直接在代码里关掉JTAG复用。
2.2 写命令与写数据的时序实现
HD44780的两条核心操作是写命令和写数据。区别只在于RS的电平:RS=0写命令(清屏、光标归位、设置模式),RS=1写数据(要显示的字符ASCII码)。写命令/数据的关键步骤一致:先把RS拉到目标电平,把数据送到D4~D7,然后拉高E,延时,拉低E。旁边配一个RW引脚永远接低,让它始终处于写状态,省掉读忙标志的逻辑。
void LCD_WriteNibble(uint8_t nibble) { GPIO_WriteBit(LCD_D4_PORT, LCD_D4_PIN, (nibble >> 0) & 0x01); GPIO_WriteBit(LCD_D4_PORT, LCD_D5_PIN, (nibble >> 1) & 0x01); GPIO_WriteBit(LCD_D4_PORT, LCD_D6_PIN, (nibble >> 2) & 0x01); GPIO_WriteBit(LCD_D4_PORT, LCD_D7_PIN, (nibble >> 3) & 0x01); GPIO_SetBits(LCD_D4_PORT, LCD_E_PIN); delay_us(1); GPIO_ResetBits(LCD_D4_PORT, LCD_E_PIN); } void LCD_WriteByte(uint8_t rs_level, uint8_t data) { GPIO_WriteBit(LCD_D4_PORT, LCD_RS_PIN, rs_level); LCD_WriteNibble(data >> 4); LCD_WriteNibble(data & 0x0F); }逻辑说明:LCD_WriteNibble先把4位数据摆到数据引脚上,然后拉高E并延时1微秒,最后拉低E。这个1微秒的延时远大于HD44780要求的450ns最小高电平脉宽,多出来的部分是给模块内部电平转换电路留的余量,特别是当LCD模块是5V供电而STM32是3.3V时,电平转换芯片会引入额外延迟。LCD_WriteByte先写高4位再写低4位,完成一个字节传输。rs_level参数为0表示写命令,为1表示写数据,这个参数控制RS引脚的电平状态。延时函数delay_us(1)用SysTick实现,注意如果使用FreeRTOS,这里要换成vTaskDelay或者关掉调度器,否则SysTick中断优先级低于LCD时序时会出现毛刺。
2.3 LCD1602初始化序列的执行顺序
HD44780的初始化是一个多步序列,顺序不能颠倒。上电后芯片可能处于8位或4位未知状态,第一步要发三次0x03,让芯片确认进入4位模式,再发0x02正式切到4位模式,之后才能设置显示参数。配套源码里的初始化序列是标准做法,完整复制即可用,但每步之间的延时参数值得留意。
void LCD_Init(void) { delay_ms(50); LCD_WriteNibble(0x03); delay_ms(5); LCD_WriteNibble(0x03); delay_us(150); LCD_WriteNibble(0x03); delay_ms(5); LCD_WriteNibble(0x02); // 进入4位模式 LCD_WriteByte(0, 0x28); // 2行、5x7点阵、4位模式 LCD_WriteByte(0, 0x0C); // 显示开、光标关、闪烁关 LCD_WriteByte(0, 0x06); // 写入后地址自动加1 LCD_WriteByte(0, 0x01); // 清屏 delay_ms(5); }初始化参数表:
| 命令 | 二进制 | 作用 | 备注 |
|---|---|---|---|
| 0x28 | 0010 1000 | 4位模式、2行、5x7点阵 | 必须在0x02之后发送 |
| 0x0C | 0000 1100 | 显示开,光标关,闪烁关 | 改成0x0F可显示光标 |
| 0x06 | 0000 0110 | 写入后地址自动加1,整体不移动 | 适合连续字符串输出 |
| 0x01 | 0000 0001 | 清屏,光标回原点 | 清屏后需等待1.52ms |
逻辑说明:0x28的每一位都有实际对应关系——bit5是4位/8位模式选择(0为4位),bit4是行数选择(1为2行),bit3是字形大小(0为5x8)。初始化序列中前三次发送0x03是HD44780规定必须存在的“唤醒”步骤,跳过会导致屏幕显示乱码或者完全不亮。上电后的延时50毫秒是给LCD模块内部的DC-DC升压电路稳定电压用的,如果用5V供电、模块自带负压生成,这个时间可缩短到20毫秒。清屏命令的5毫秒延时不是拍脑袋写的,HD44780数据手册上清屏指令的典型执行时间是1.52毫秒,这里给3倍余量。
3. TFT彩屏驱动:SPI通信、初始化序列与DMA优化
3.1 从LCD1602到TFT彩屏的架构变化
LCD1602只能显示ASCII字符和少量日本假名,且需要手动维护坐标,而TFT彩屏以像素为单位刷新。ST7735和ILI9341是入门彩屏最常见的两种控制芯片,ST7735常见于1.8寸128x160的小屏,ILI9341则对应2.8寸和3.2寸的320x240大屏。两者的驱动接口都是SPI,数据位宽等于像素色深——16位RGB565时,一个像素就是两字节。
这套配套源码最值得关注的设计,是把LCD1602的GPIO模拟思路直接切到了SPI外设上。STM32F103C8T6的SPI1支持18Mbit/s的时钟,比GPIO模拟快一个数量级,根本原因是SPI外设的移位寄存器在硬件层面工作,CPU只需往DR寄存器里写数据就能触发发送,不需要逐位翻转引脚。结构上,TFT屏有5条控制线:SCLK(时钟)、MOSI(主出从入)、CS(片选)、DC(数据/命令)、RST(复位)。
void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); 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_4 | GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOA, &GPIO_InitStructure); 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_4; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }逻辑说明:PA5(SCLK)和PA7(MOSI)配置为复用推挽,交给SPI1外设控制;PA4(CS)和PA6(DC)配置为普通输出,由代码手动控制。SPI_Direction_2Lines_FullDuplex意味着同时使用MOSI和MISO,但很多TFT屏的MISO引脚悬空或者没有引出,实际只用到了发送方向。SPI_CPOL_Low和SPI_CPHA_1Edge组合成SPI Mode 0,这是ST7735和ILI9341数据手册里标注的时序要求,SPI模式不匹配的典型症状是显示花屏或者颜色错乱。SPI_BaudRatePrescaler_4把72MHz的APB2时钟降到18MHz,这是ST7735能接受的极限时钟频率。如果配到2分频(36MHz),一部分屏会丢像素,尤其是供电不稳时。
3.2 彩屏初始化序列的“三板斧”
TFT彩屏的驱动比LCD1602复杂在于:上电后芯片默认处于Sleep模式,要经过一串寄存器配置序列才能正常显示。这个序列通常有几十条指令,源码里提供的初始化序列可以直接使用,但需要理解三条最关键的指令。
void LCD_Init(void) { LCD_Reset(); LCD_WriteCmd(0x11); // 退出睡眠模式 delay_ms(120); LCD_WriteCmd(0x36); // 设置扫描方向和RGB顺序 LCD_WriteData(0x00); LCD_WriteCmd(0x3A); // 设置像素格式 LCD_WriteData(0x05); // 16位色 LCD_WriteCmd(0x29); // 开启显示 delay_ms(20); } void LCD_Reset(void) { GPIO_ResetBits(GPIOA, GPIO_Pin_3); delay_ms(20); GPIO_SetBits(GPIOA, GPIO_Pin_3); delay_ms(120); }逻辑说明:0x11退出睡眠模式是最关键的一步,上电后不执行它屏幕就是黑的,这个指令后的120毫秒延时是芯片内部震荡器启动时间,缩短到50毫秒以下大概率出现显示异常。0x36的bit6和bit7控制扫描方向,0x00对应默认的从左到右、从上到下;如果想翻转屏幕,改成0xC0。0x3A设置像素格式,0x05是16位RGB565,0x06是18位RGB666,注意如果选择18位,后续写像素时的数据组织和RGB565完全不同。LCD_Reset在硬件复位引脚上拉低至少20毫秒再拉高,这是芯片数据手册规定的复位脉宽下限。
3.3 像素写入与DMA搬运
彩屏的日常操作中,写像素占了大头。一个320x240的ILI9341屏幕,刷一帧要写入3202402 = 153600字节。如果用阻塞式SPI发送,按18MHz时钟、每字节8个时钟周期算,理论极限是每秒2.25MB,但每发一个字节都要判断SPI发送完成标志,实际吞吐量会掉一半以上。解决办法是用DMA搬数据。
void LCD_DrawPixel(uint16_t x, uint16_t y, uint16_t color) { LCD_SetWindow(x, y, x, y); LCD_CS_LOW(); LCD_WriteCmd(0x2C); LCD_DC_HIGH(); SPI_I2S_SendData(SPI1, color >> 8); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(SPI1, color & 0xFF); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); LCD_CS_HIGH(); } void LCD_DrawBitmap_DMA(uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint16_t *bitmap) { uint32_t pixel_count = (uint32_t)w * h; LCD_SetWindow(x, y, x + w - 1, y + h - 1); LCD_CS_LOW(); LCD_WriteCmd(0x2C); LCD_DC_HIGH(); SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE); DMA_Cmd(DMA1_Channel3, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel3, pixel_count * 2); DMA_Cmd(DMA1_Channel3, ENABLE); }逻辑说明:单点写像素时,先设置窗口地址,然后发送0x2C(内存写)命令,最后分两次发送高字节和低字节。注意RGB565的字节序——先发高字节,这里不是随便定的,取决于芯片内部的像素格式,顺序反了会导致红蓝颜色互换。LCD_DrawBitmap_DMA把整个位图交给DMA1通道3搬运,DMA在内存和SPI外设之间直接传输,不经过CPU,CPU可以腾出来做其他事情。pixel_count * 2是因为一个像素两个字节。使用时需要先初始化DMA1 Channel3,源地址是bitmap指针,目的地址是&SPI1->DR。DMA传输完成后,标志位在DMA ISR里处理。
SPI与GPIO模拟两种方案对比(这个挂在第2章和第3章之间衔接更自然):
| 对比项 | LCD1602 GPIO模拟 | TFT彩屏 SPI |
|---|---|---|
| 数据宽度 | 4位并行 | 串行8位 |
| 控制线数量 | 6~7根 | 5根(含CS、DC) |
| 刷新一屏 | ~50ms(16字符) | 320x240全屏约68ms |
| CPU占用 | 全程占用 | 单点低,DMA下近零 |
| 适用场景 | 字符菜单、调试信息 | 图形界面、图像显示 |
4. C与C++的工程组织:给裸机代码做分层的正确姿势
4.1 为什么LED秒闪的代码用C,UI逻辑用C++
标题里写了C和C++,说明这套配套源码不是纯C裸机工程,而是C与C++混编。常见做法的边界划分是:硬件驱动层用C文件实现,对外暴露C接口函数;上层应用用C++的类封装,把多个驱动组合成带状态的对象。分层的基本原理是——C能精确控制内存布局,适合处理寄存器映射、中断回调、硬件时序;C++的类、继承、重载能把UI逻辑写得可维护,尤其是多个屏幕之间切换状态机时,用C做状态机得写一堆switch-case嵌套,用C++直接抽象成状态类。
class LCD1602 { public: LCD1602(GPIO_TypeDef *port, uint16_t rs, uint16_t e, uint16_t d4, uint16_t d5, uint16_t d6, uint16_t d7); void Init(); void Print(const char *str, uint8_t row, uint8_t col); void Clear(); private: void WriteCmd(uint8_t cmd); void WriteData(uint8_t data); void WriteNibble(uint8_t nibble); TIM_TypeDef *delay_timer; GPIO_TypeDef *gpio_port; uint16_t rs_pin, e_pin; uint16_t d4_pin, d5_pin, d6_pin, d7_pin; };逻辑说明:这个类的成员变量保存了LCD的GPIO配置,构造函数传入的端口和引脚地址在初始化时赋值给类内私有成员。WriteCmd和WriteData是私有方法,对应上一章的C接口;Print是公开方法,内部按行列地址换算成HD44780的DDRAM地址,再逐个写字符。C++类的好处是复用性——同一套代码实例化两个对象时,只需要传入不同的GPIO配置,不会像C版本那样全局变量互相污染。混编工程里,C文件(如system_stm32f10x.c、stm32f10x_gpio.c)不能直接编译成C++,必须用extern "C"包裹头文件,否则链接阶段会报未定义符号错误。
4.2 FreeRTOS移植与C++析构函数的坑
配套源码如果放到FreeRTOS环境下,有C和C++的内存管理冲突问题。FreeRTOS的pvPortMalloc默认实现不提供free函数,而C++的new/delete依赖标准库的堆管理。如果有C++对象在动态创建和销毁,常见做法是重载类的operator new和operator delete让它们走FreeRTOS的堆分配器:
#include "FreeRTOS.h" #include "task.h" void* operator new(size_t size) { return pvPortMalloc(size); } void operator delete(void* ptr) { vPortFree(ptr); } void vTaskLCDTask(void *pvParameters) { LCD1602 *lcd = new LCD1602(GPIOB, GPIO_Pin_12, GPIO_Pin_13, GPIO_Pin_0, GPIO_Pin_1, GPIO_Pin_10, GPIO_Pin_11); lcd->Init(); for (;;) { lcd->Print("FreeRTOS OK!", 0, 0); vTaskDelay(pdMS_TO_TICKS(1000)); } }逻辑说明:全局重载operator new和operator delete后,任务里new LCD1602的分配动作走FreeRTOS的pvPortMalloc,好处是内存申请进入临界区,避免两个任务同时new对象时数据竞争。注意这里的delete不需要——FreeRTOS任务函数结束后任务本身会被删除,对象留在堆上没问题。另一个关键点是任务栈大小。C++对象构造时若有虚函数表,会在堆上额外分配vptr,这不算栈开销,但如果任务栈默认给128字(512字节),加上LCD操作时层层调用,很容易溢出。F103C8T6被视为国产替代热门芯片的原因是同封装下有20KB SRAM,任务栈可以给到256字甚至512字,一个LCD显示任务实际占用约200字栈空间。
4.3 Keil/IAR双环境的工程文件组织
源码里应同时保留Keil(MDK-ARM)和IAR两种工程文件。两个IDE处理C++混编的方式有细微差别。Keil的AC5编译器需要手动把源文件扩展名改成.cpp,并且在Options - C/C++里勾选--cpp11或--cpp;IAR则在Project - Options - C/C++ Compiler里把语言模式切换到C++。两种环境下都建议把C文件的头文件包含路径用extern "C"包裹一次:
#ifdef __cplusplus extern "C" { #endif #include "lcd1602.h" #include "tft_st7735.h" #include "spi_dma.h" #ifdef __cplusplus } #endif逻辑说明:这段代码放在C头文件的末尾或者C++文件包含它们的地方。__cplusplus宏在C++编译器下自动定义,C编译器不定义,所以C文件包含时不变,C++文件包含时自动套上extern "C",确保链接器按C符号规则找函数名。这样设计的目的是让C++代码能直接调用LCD_Init()这种C函数,而不用在C++源文件里每个函数都重写一遍声明。工程文件路径上,Keil的uvprojx和IAR的ewp放在不同目录,避免IDE互相覆盖配置。F103C8T6的Flash只有64KB,C++的标准库如果全量链接会超过这个数,解决方案是使用--no_exceptions或--no_rtti关闭C++的异常和运行时类型识别——嵌入式上的C++一般不启用这两个特性。
// lcd1602.h 核心C接口 void LCD_Init(void); void LCD_Clear(void); void LCD_PrintChar(char c); void LCD_PrintString(char *str); void LCD_SetCursor(uint8_t row, uint8_t col); void LCD1602_WriteCmd(uint8_t cmd); void LCD1602_WriteData(uint8_t data);逻辑说明:C接口的设计原则是只暴露操作必需的最小集合——LCD_Init做初始化,LCD_Clear清屏,LCD_PrintString输出字符串,LCD_SetCursor设置光标位置。LCD1602_WriteCmd和LCD1602_WriteData虽然是对外的,但主要给C++类内部调用。每个函数都加LCD1602_前缀,防止多个驱动文件的符号在链接时冲突。
5. LCD1602与TFT彩屏共存:源码复用的边界与调试手法
5.1 两个屏同时接的系统,IO资源怎么分配,冲突在哪
当LCD1602和TFT彩屏同时接入F103C8T6最小系统板时,引脚分配的优先级是:先SPI(彩屏的时钟线不能换)、再LCD1602的数据线、最后才是控制线。标准接法是SPI1用PA5/PA6/PA7,LCD1602用PB0~PB13缺口处的一组,但PB2(BOOT1)默认是启动配置脚,不能拿来当普通IO,所以LCD1602的6根线要避开PB2。如果发现TFT屏幕花屏、LCD1602无显示同时发生,先查两个屏的CS片选是否接有独立GPIO,是否存在共用引脚导致片选信号交叉——这个问题在一个屏单独跑的时候永远不会暴露。
5.2 逻辑分析仪抓时序站位,比猜EFAST(E脉冲)靠谱
LCD1602驱动里最隐蔽的坑是E脉冲的宽度不够。逻辑分析仪接在E引脚上,抓一条写命令波形,查E的高电平时间是否大于450ns。如果小于这个值,很可能是编译器O3优化把delay_us里的空循环删掉了——一个没有副作用的延时函数,优化掉了等于没有延时。对策是把延时函数声明为volatile或者用__NOP()指令填满循环体,这也是为什么源码里要留一个看起来多余的for(i=0; i<10; i++);而不是直接调官方库函数:
void delay_us(uint32_t us) { volatile uint32_t i; for (i = 0; i < us * 8; i++) { __NOP(); } }参数说明:us * 8不是随手写的。F103C8T6主频72MHz时,一条__NOP()加一条循环分支判断大约需要8个时钟周期,所以延时时间约等于8 * 循环次数 / 72MHz,单位正好是微秒。如果用volatile uint32_t声明循环变量,编译器不会把循环优化掉。这个方法不如SysTick精确,但胜在轻量,适合在GPIO模拟时序里用。
5.3 电平不匹配:5V的LCD1602和3.3V的STM32怎么共存
LCD1602的典型供电是5V,而STM32F103C8T6的GPIO输出是3.3V。直接把3.3V信号接到5V的LCD模块IO上,逻辑高电平阈值是2.0V,3.3V确实高于阈值,大部分模块能正常工作。但反向问题更大:LCD模块的上拉电阻可能会往STM32的GPIO灌入超过数据手册限值的电流,长期跑下来PA引脚有可能会损坏。源码里的标准配置是LCD1602的VDD接5V,而数据线和控制线串330欧姆限流电阻再连到STM32。TFT彩屏则完全没有这个问题,ST7735和ILI9341都支持3.3V供电和3.3V逻辑电平,直接共用电源轨即可。
5.4 用OLED替代彩屏时的驱动节省量,验证初始化的抽象正确性
如果这套源码已经跑通LCD1602和TFT彩屏,再换一个SSD1306的OLED时,会发现接口层可以复用——OLED是I2C接口但同样有命令/数据区分,SPI版本的TFT驱动代码里把写命令和写数据抽成两个函数以后,I2C驱动只需实现同样的WriteCmd和WriteData函数签名,上层OLED_ShowString、OLED_DrawLine这些API就能原封不动搬过来。验证这套代码抽象质量的快速方法是:只改驱动层,不动应用层调用代码,看看还能不能显示完整画面。如果改完一处就要动上层三处,说明分层边界划错了。
本文还有配套的精品资源,点击获取