尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32驱动LCD12864串行显示:ST7920协议与实战避坑指南

STM32驱动LCD12864串行显示:ST7920协议与实战避坑指南 简介STM32与LCD12864串行驱动工程资源面向嵌入式开发者和电子爱好者解决基于STM32微控制器以串行方式驱动LCD12864液晶显示模块的问题。压缩包含188个文件约5.26MB核心包括多个c/h源文件与头文件、o/axf/hex等编译输出文件、uvprojx工程配置以及PDF说明等覆盖标准外设库与完整项目工程可直接在Keil等环境编译研究。已有344人学习。资源不仅给出串行接口初始化、命令发送与数据显示代码还包含字符与图形显示控制示例以及通过库函数、中断和帧缓冲区提升显示效率的思路适合希望系统掌握SPI/I2C串行驱动LCD12864的开发者参考学习。1. 串行驱动的第一句话省下的8根IO花在协议上接手一个小仪表项目面板上要放一块12864点阵液晶MCU用STM32F103C8T6。第一反应是“直接怼并行总线”但算完引脚就心疼了DB0~DB7、RS、RW、E、PSB再算上背光十几根IO直接被吃掉一块留给按键、传感器和通讯的余量所剩无几。把PSB拉低切到串行模式之后数据通道只剩SCLK、SID和CS三根线连上RS一共四个GPIO这就是标题里“stm32 lcd12864串行驱动”的核心诉求用最少的引脚把这块屏幕跑起来。市面上绝大多数12864屏用的都是ST7920控制器内置GB2312简体中文字库串行协议本质上是SPI的变体。串行驱动对5年以上经验的人来说难点不在“点亮”而在协议帧的结构、硬件SPI与模拟SPI的坑、初始化时序和DDRAM地址映射。这篇顺着“协议→配置→代码→排错”把整套方案讲清楚新手可以直接抄熟手可以对照着避坑。2. ST7920串行协议PSB拉低之后三个引脚怎么把数据发进去理解串行驱动先要明白ST7920在串行模式下到底接收什么。网上一搜lcd12864串行驱动代码满天飞但很多人直接把并行初始化命令搬过来屏幕不亮就怪接线——其实是没搞懂一帧数据的组成。这一节把协议拆开讲。2.1 串行模式引脚映射与模块丝印对照不同厂商的12864模块丝印差别很大有的直接标CS、SID、SCLK有的沿用并行时代的RS、RW、E。串行模式下ST7920的引脚功能会重新分配对照关系如下模块丝印并行习惯串行模式角色STM32 GPIO方向RS命令/数据选择串行标志位的一部分输出RWSID串行数据输入输出ESCLK串行时钟输出CS有的模块叫CS1/CS2ST7920只有一个CS片选低电平有效输出PSB接GND切换到串行模式接GND接线时先把PSB有的丝印叫PSB/PS接低这一步漏了后面所有代码都白写。若用的模块自带转接板丝印已经简化成CS、SID、SCLK就不用管RS/RW/E那套旧名字了。串行模式下的电源和对比度引脚照常接。注意串行模式只支持“写”不支持读所以不需要MISO这也是为什么用硬件SPI传输时可以只打开MOSI而不关心MISO的电平。2.2 0xF8/0xFA控制字节与一帧的组成ST7920串行传输的基本单位不是普通SPI里的一个字节而是一个“控制字节 数据字节”的组合。控制字节的格式是先发5个连续的1作为同步位紧接着是RW写方向恒为0、RS命令/数据选择、一个固定的0。拼起来就是写命令11111000即 0xF8写数据11111010即 0xFA所以每次写操作要发16个SCLK时钟前8个时钟是控制字节后8个时钟是真正的命令或数据。很多驱动代码里写成先write_byte(0xF8)再write_byte(cmd)就是干这个事。为什么前面要有5个1ST7920没有独立的片选握手信号接收端靠这串1来同步位边界然后从第6个时钟开始解析方向位。CS在整个传输过程中必须保持低电平两个字节之间不能把CS拉高否则控制器会认为这是一次新传输前面的同步就白做了。2.3 用GPIO先写一个不依赖SPI的发送函数在引入硬件SPI之前先实现一个最朴素的模拟SPI发送函数。好处有两个一是逻辑清晰二是后面排错时可以直接用GPIO点灯的方式验证时序。直接贴一个最小实现// 引脚宏定义按实际接线修改 #define LCD_CS_GPIO GPIOB #define LCD_CS_PIN GPIO_Pin_12 #define LCD_SCLK_GPIO GPIOB #define LCD_SCLK_PIN GPIO_Pin_13 #define LCD_SID_GPIO GPIOB #define LCD_SID_PIN GPIO_Pin_15 #define LCD_CS_L() GPIO_ResetBits(LCD_CS_GPIO, LCD_CS_PIN) #define LCD_CS_H() GPIO_SetBits(LCD_CS_GPIO, LCD_CS_PIN) #define LCD_SCLK_L() GPIO_ResetBits(LCD_SCLK_GPIO, LCD_SCLK_PIN) #define LCD_SCLK_H() GPIO_SetBits(LCD_SCLK_GPIO, LCD_SCLK_PIN) #define LCD_SID_L() GPIO_ResetBits(LCD_SID_GPIO, LCD_SID_PIN) #define LCD_SID_H() GPIO_SetBits(LCD_SID_GPIO, LCD_SID_PIN) static void lcd_delay_us(volatile uint32_t us) { // 72MHz主频下粗略延时每个循环约6~8个周期 for (volatile uint32_t i 0; i us * 12; i) { __NOP(); } } static void lcd_write_byte(uint8_t dat) { // 高位在前一个数据位一个时钟周期 for (uint8_t i 0; i 8; i) { if (dat 0x80) { LCD_SID_H(); } else { LCD_SID_L(); } LCD_SCLK_H(); LCD_SCLK_L(); dat 1; } } static void lcd_write_cmd(uint8_t cmd) { LCD_CS_L(); lcd_write_byte(0xF8); lcd_write_byte(cmd); LCD_CS_H(); } static void lcd_write_data(uint8_t dat) { LCD_CS_L(); lcd_write_byte(0xFA); lcd_write_byte(dat); LCD_CS_H(); }代码里lcd_write_byte的工作方式是把字节最高位移到SID然后SCLK拉高再拉低形成一个上升沿用采集数据。ST7920在SCLK上升沿采样所以先摆数据再给时钟。lcd_delay_us用一条粗略循环实现72MHz下每个迭代大约几个周期实际值不精确但够用。真正需要关注的是SCLK频率不能太高手册上串行时钟建议不超过2MHz所以模拟方式下每周期加一点延时是值得的。这里有个常见误用有人把lcd_write_cmd和lcd_write_data里的CS拉高放在函数最末尾没问题但如果在两个lcd_write_byte之间插了CS拉高的操作屏幕就会收到一个不完整的帧表现是花屏或者某条指令偶尔不生效。CS在整帧传输内必须保持低。3. STM32硬件SPI驱动配置模式0、分频比和CS时序GPIO模拟方式灵活但占CPU、耗时而且在72MHz主频下写的延时循环换个编译器水平就变了。跑熟了协议之后更干净的方案是直接用STM32的硬件SPI。ST7920的串行时序跟SPI模式0高度吻合但有几个细节要提前处理否则硬件SPI反而比GPIO模拟更让人头疼。3.1 SPI模式0的CubeMX参数表打开STM32CubeMX选好芯片把SPI1的SCK、MOSI引脚分配出来再把片选CS配成普通GPIO输出。ST7920串行模式下只收不发MISO引脚可以不接。核心参数如下参数项值说明ModeTransmit Only Master只发送不需要接收Hardware NSS SignalDisableCS用普通GPIO控制拉低拉高时机由自己掌握Data Size8 Bits每次发一个字节控制字节和数据字节分两次发First BitMSB FirstST7920要求高位先出Clock Polarity (CPOL)LowSPI模式0空闲时SCLK为低Clock Phase (CPHA)1 Edge第一个边沿采样即上升沿Baud Rate Prescaler16 或 3272MHz下对应4.5MHz或2.25MHz建议从32起步CRCDisable不需要分频这里多说一句ST7920手册给出的串行时钟上限约2MHz。F103的SPI1挂APB2总线72MHz主频下Prescaler16得到4.5MHz超了Prescaler32得到2.25MHz仍然略超但实测大多数屏没问题Prescaler64是1.125MHz绝对稳只是刷屏速度慢一点。做产品建议把分频留足余量跑demo图快可以用除以16。3.2 标准库/寄存器下的SPI发送实现CubeMX生成的是HAL库代码直接往里填一个发送函数即可void lcd_spi_send_byte(uint8_t dat) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI1, dat); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) SET); } void lcd_write_cmd(uint8_t cmd) { LCD_CS_L(); lcd_spi_send_byte(0xF8); lcd_spi_send_byte(cmd); LCD_CS_H(); } void lcd_write_data(uint8_t dat) { LCD_CS_L(); lcd_spi_send_byte(0xFA); lcd_spi_send_byte(dat); LCD_CS_H(); }第一个while等发送缓冲区空第二个while等总线不忙。两个都要只看TXE的话最后一个字节可能还没移出去就把CS拉高了导致帧尾被截断。如果用的是HAL库对应的是HAL_SPI_Transmit但要注意每次调用都有额外开销连续发两个字节时中间可能产生几个微秒的间隔这对ST7920完全没问题它只是采样慢不怕间隔。3.3 硬件SPI的拆帧问题与模拟SPI的取舍硬件SPI最大的坑不在参配而在CS和SPI字节帧的对齐。前面说了一个完整的ST7920写操作包含两个8位字节CS在整帧内必须保持低。如果用硬件NSS它会在每次SPI_I2S_SendData传输完成后自动拉高正好把帧拆断。所以这里必须禁用硬NSS用普通GPIO当CS自己控制拉低和拉高的时机。另一个坑是引脚复用冲突。F103的SPI1_SCK、SPI1_MOSI分别是PA5、PA7这两个引脚同时也被用作普通GPIO。程序里如果用到JTAG相关的PB3/PB4还涉及复用重映射。建议优先用SPI1的标准引脚省去重映射的麻烦。那么到底用模拟还是硬件SPI我的判断标准是项目里SPI总线空闲、引脚充裕就上硬件SPI省CPU如果SCLK引脚被别的外设占用或者板子走线把SID接到了某个不支持硬件SPI的引脚就老老实实用GPIO模拟。模拟方式的代码可移植性更好换GD32、APM32这类国产芯片时几乎不用改这也是为什么很多开源项目的lcd12864串行驱动默认就是模拟SPI。4. 初始化序列与显示代码从点屏到第一行汉字协议通了接下来就是让屏幕出字。ST7920初始化不复杂但顺序敏感上电后要先给控制器留出复位时间然后按固定顺序写入功能设置、显示开关、清屏等指令。很多“按下复位能亮、断电再上电就白屏”的怪现象都出在初始化时序上。4.1 上电延时与命令表ST7920数据手册要求上电后等待至少40ms再发第一条命令实际工程里这个时间还要多留一点尤其是电源用了DC-DC升压的板子液晶供电起来得更慢。初始化命令序列如下步骤命令含义10x308位接口基本指令集20x30重复发送确保控制器完成内部复位30x30再发一次三次更稳40x0C显示开光标关反白关50x01清屏60x06光标右移DDRAM地址自动加1第一到第三条0x30是“唤醒”过程。晶振起振、内部状态机复位都需要时间连续发三次是行业里通行的做法不能省。0x0C是开显示但不显示光标0x06决定后续写字符串时地址往哪个方向走。清屏0x01要额外注意它的执行时间比普通指令长得多。4.2 写命令、写数据与内部的1.6ms长操作清屏命令发出后ST7920内部要逐字把显示RAM清空这个过程约1.6ms。如果紧接着就发下一条命令控制器还没忙完命令会被丢掉。初始化代码里清屏后必须插入延时void lcd_init(void) { lcd_delay_ms(50); // 上电稳定 lcd_write_cmd(0x30); lcd_delay_ms(5); lcd_write_cmd(0x30); lcd_delay_ms(5); lcd_write_cmd(0x30); lcd_delay_ms(5); lcd_write_cmd(0x0C); // 显示开光标关 lcd_delay_ms(2); lcd_write_cmd(0x01); // 清屏 lcd_delay_ms(5); // 清屏执行时间约1.6ms延时留余量 lcd_write_cmd(0x06); // 地址自动加1 lcd_delay_ms(2); }注意0x30、0x0C、0x01这些命令后面的延时长度并不一样。功能设置和显示开关这类指令执行时间在微秒级延时可短清屏是唯一一个必须等毫秒级的。如果初始化完发现屏幕亮但字是乱的先检查清屏后的延时够不够。4.3 显示字符串GB2312中文与ASCII混排初始化完成就能写字了。写一个字符前要先设置DDRAM地址用0x80加上列偏移量。ST7920内置GB2312字库中文两个字节一个汉字ASCII一个字节一个半角字符。写字符串的代码要按字节流判断遇到小于0x80的按ASCII处理遇到大于等于0x80的按汉字处理取两个字节一起发void lcd_show_string(uint8_t row, uint8_t col, const char *str) { // row取值范围0~3对应4行col取值范围0~15 static const uint8_t row_addr[4] {0x80, 0x90, 0xA0, 0xB0}; lcd_write_cmd(row_addr[row] col); while (*str) { uint8_t c (uint8_t)(*str); if (c 0x80) { lcd_write_data(c); // ASCII字符 str; } else { lcd_write_data(c); // 汉字高字节 lcd_write_data((uint8_t)(*(str 1))); // 汉字低字节 str 2; } } }这个函数配合前面的lcd_write_cmd、lcd_write_data就能直接显示字符串。调用示例lcd_init(); lcd_show_string(0, 0, 串行驱动OK 123);要点在row_addr这张表第0行映射到0x80第1行映射到0x90第2行映射到0xA0第3行映射到0xB0列偏移量直接往上加。字符串里混排中文和数字时字符宽度不一样但对DDRAM地址来说每个ASCII字符占1个地址每个汉字也占1个地址只是视觉上汉字比ASCII宽一倍。所以一行16个地址能放16个汉字或32个ASCII字符混排时以实际地址步进为准。5. 坐标、字模与对比度把“能显示”变成“显示得对”屏幕点亮、字符串能出来只是第一步。实际项目里要排版、要对齐、要调可视角度这些涉及到DDRAM地址映射的细节和模拟电位器的调校。5.1 4行DDRAM地址表与x坐标换算ST7920的DDRAM地址并不是按物理行从上到下连续排列的。12864屏在ST7920内部被分成上下两个半屏每个半屏有2行字符区物理第3行在内存里的地址反而在第1行附近。完整映射如下物理行上位机视觉DDRAM起始地址列偏移范围第1行上半屏第一行0x800x80 ~ 0x8F第2行上半屏第二行0xA00xA0 ~ 0xAF第3行下半屏第一行0x900x90 ~ 0x9F第4行下半屏第二行0xB00xB0 ~ 0xBF注意第2行和第3行的地址顺序和物理顺序不一致。写代码时最容易错的就是这个想在第2行显示结果写到0x90出来发现去了下半屏。上面lcd_show_string里的row_addr表就是按“物理行”顺序排列的直接用即可不要自己去推0x80偏移。列偏移x的取值范围是0~15对应屏幕最左到最右的16个汉字宽度。有的代码里出现“第2行使0x88、第4行使0x98”那是另一种地址解释方式本质是把一个物理行拆成了两段各8列来看容易把人绕晕。这里统一采用4行16列的线性映射简单可靠。5.2 中英文字符宽度与布局对齐做菜单时中英文混排的对齐是个高频需求。ST7920的ASCII码表其实仍然以16x16点阵的显示空间为基准半角字符只占左半边8像素右半边保持空白。因此视觉上一个ASCII字符占8像素一个汉字占16像素。设计界面时同样10个字符的宽度中英文差一倍一行16个地址纯中文可放16字纯ASCII可放32字符。如果居中显示中文字符串“系统正常”占4字宽英文“OK”占2字宽。计算偏移时按字符数算不能按字节数算。数字和英文字母混排时每个字符宽度相同一个地址存一个字符不用特殊处理。真正麻烦的是要显示“半个字”的画面比如“正在加载”这类进度条。ASCII字符画进度条只能画左半边右半边是空的想填满就得用图形方式。ST7920的扩展指令集里有一条0x34切换命令可以进入扩展模式直接操作GDRAM写自定义图形或进度条。这部分驱动代码要另起一套读改写逻辑这里不展开但要知道内置字库模式下做不了任意像素级图形。5.3 对比度、背光与3.3V供电的取舍对比度调节在ST7920上不是软件指令能解决的。VO引脚需要一个可调电压典型值在1~5V之间。常见接法是VO接10K电位器中间抽头两端分别接VCC和GND调到屏上出现清晰黑字且背景不残留重影为止。如果VO悬空屏幕可能全黑或全白不是代码问题。3.3V供电的STM32驱动5V供电的12864屏这是最常见的配置。逻辑电平上STM32的GPIO输出高电平是3.3VST7920的输入高电平门槛约0.7×VDD5V供电下VIH约3.5V刚好卡在边缘。所以要么选3.3V兼容版本的模块要么在SID和SCLK上加电平转换。很多标称5V的模块实测3.3V直接驱动也能稳定跑因为ST7920的输入缓冲器实际门槛没有手册那么苛刻但量产产品不建议赌这一点加个74LVC245最稳妥。背光电流一般20~60mA限流电阻根据背光LED正向压降算。注意3.3V系统里如果模块带5V转3.3V的稳压芯片很多模块上有AMS1117-3.3供电要按模块丝印要求接不要想当然只给3.3V。6. 时序验证与花屏排错用逻辑分析仪看一帧数据驱动写完、屏幕亮了不代表万事大吉。换一块屏、换一个芯片型号、把主频从72MHz改成108MHzF103超频场景都可能冒出奇奇怪怪的显示问题。这一章把验证方法和高频故障点列出来照着排查能省半天时间。6.1 抓取CS低电平内的SCLK脉冲调试串行驱动最直接的工具是逻辑分析仪几十块钱的8通道就够用。把探针接到CS、SCLK、SID三个引脚抓一次写命令操作看波形是否符合预期CS先拉低然后SCLK连续输出16个脉冲SID在第一个字节里是按0xF8排列的高位在前第二个字节是命令内容。重点看三处CS低电平时SCLK的脉冲数是不是16SID在SCLK上升沿时是否稳定CS拉高的时刻是不是在最后一个SCLK下降沿之后。如果脉冲数不足16说明SPI配置里数据位设成了16位或者CS在中间被硬件NSS拉高了。如果SID电平在上升沿附近跳变说明GPIO模拟代码里先拉时钟后摆数据的顺序写反了。提示写命令和写数据在波形上的区别只有第一个字节的bit10xF8和0xFA差的就是这一位。抓波形时先写0x30命令再写0x30数据对比两次波形就能确认RS位是否正确。6.2 花屏的三个高频原因花屏、乱码、显示残影九成是下面三个原因之一第一初始化时序太急。上电后立刻发命令或者清屏后延时不足最常见的表现是屏上出现随机亮块。把开头lcd_delay_ms(50)加到100ms清屏后延时加到10ms多数问题直接消失。第二CS没有整帧保持低电平。用硬件SPI时如果开了NSS硬件控制每个字节传完CS自动拉高ST7920会把一个命令拆成两半处理表现出来就是某些字符位置错乱。解决方法是CS必须用软件GPIO控制这一点在3.3节已经强调过。第三电平不匹配或电源纹波过大。5V供电的屏用3.3V驱动时SID高电平可能刚好低于门槛。观察现象屏幕能初始化、能显示背光但所有字符都是乱的或者同一帧里前半段正常后半段花掉。这种问题用示波器量SID的高电平电压就能确认低于2.5V基本就是逻辑电平问题。纹波大的场合SCLK边沿会产生毛刺导致ST7920误采样可以在SCLK引脚串联一个33Ω电阻。6.3 一个自检技巧整屏刷白测试代码驱动基本正常但显示不满意时我习惯先做一个“整屏刷白”自检切到扩展指令集把整个GDRAM写成固定数据让屏幕全亮或全灭。这一步能快速区分是控制器问题、显示RAM问题还是字库映射问题。// 进入扩展指令集 lcd_write_cmd(0x34); // 依次写8行GDRAM每行16字节 for (uint8_t page 0; page 8; page) { lcd_write_cmd(0x80 page); // 设置GDRAM垂直地址 lcd_write_cmd(0x80); // 设置GDRAM水平地址首列 for (uint8_t col 0; col 16; col) { lcd_write_data(0xFF); // 全亮0xFF逐位对应8个像素 lcd_write_data(0xFF); } } // 回到基本指令集 lcd_write_cmd(0x30);这段代码会分8页把整个屏幕刷成点亮状态。注意扩展指令集下写入GDRAM需要连续发两个数据字节低字节在前、高字节在后地址会自动累加。刷白正常而写中文异常说明问题出在字库编码或DDRAM地址映射刷白直接花屏问题基本在控制器的时序或供电上。用这个方法把屏幕本身的读写逻辑验证干净再回头查字符串驱动排查范围就小很多。最后留一个实际操作上的建议CS拉高后下一帧之前不要做多余的空操作直接发下一帧就行。ST7920对CS高电平的保持时间没有严格要求真正要盯住的只有CS低电平时那16个SCLK脉冲。把这条记进代码注释以后再调这个屏照着波形对一行一行查不会迷路。本文还有配套的精品资源点击获取
返回列表