news 2026/9/9 23:47:31

STM32驱动19264液晶屏实战:从时序原理到汉字显示与排障

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32驱动19264液晶屏实战:从时序原理到汉字显示与排障

简介:这是一套基于STM32F03RBT6微控制器的19264点阵LCD驱动工程,面向嵌入式开发者和电子爱好者,完整演示了KS0108(兼容KS0107)控制芯片的8位并行接口驱动方案。工程包共151个文件、2.36MB,其中包含36个.h头文件与36个.c源代码文件,覆盖LCD驱动的主要逻辑;同时附有Keil MDK工程文件(uvproj/uvopt)、链接脚本sct、启动文件s以及编译过程生成的crf、o、axf、hex、map等文件,从源码到可执行文件一应俱全,便于逐步分析和验证。

目前已有745人浏览学习,对希望深入理解MCU底层驱动和LCD工作原理的读者是很好的案例。代码实现了LCD初始化、显示开关控制、清屏、像素点绘、字符与字符串显示等基础功能,并设计了显示缓冲区批量刷新机制,减少扫描通信次数;还预留了画直线、矩形等绘图函数的扩展空间。整个工程注释清晰,GPIO配置和时序逻辑均给出较完整说明,可直接编译烧录到STM32F0系列,也能迁移到其他MCU平台,是学习并口液晶驱动不可多得的参考项目。 192×64点阵液晶屏在嵌入式圈子里是个老面孔了,但直到现在我还能在各个工业仪表、设备面板、车载终端上看到它。STM32驱动19264 LCD这活儿,听起来不复杂——不就是写命令、写数据、刷个屏嘛,可真要把它做稳定,能显示汉字、能画图形、能长时间跑不死机,还是会踩到不少坑。这篇文章是我实际调试19264驱动工程的完整记录,从屏的接口类型、时序原理、硬件接线,到驱动代码编写、汉字取模显示,再到高频故障排查,一步步拆开讲清楚,适合刚用STM32做显示类项目的同学,也适合想把手头屏幕驱动写得更规范的人参考。

1. 19264LCD到底是什么屏?项目拆解先行

1.1 19264的硬件特性与常用接口

19264这个名字其实已经把核心参数写在脸上了:192列,64行,也就是一共有192×64个点,属于单色点阵屏。相比常见的12864,它横向点数更多,一行能排下的字符和汉字更多,所以在需要显示较长信息、曲线、表格的场景里很吃香。屏幕本身不带显存管理,必须依赖外部的LCD控制器芯片,常见的控制器有ST7565、ST7567、NT7534、ST7529等。ST7567和NT7534基本可以按ST7565的指令集兼容处理,但具体到某块屏,初始化序列多多少少会有差异,这一点后面我会单独说。

接口方面,19264屏的常见接法分两类:一类是并口,以8080时序为主,也有少部分6800时序;另一类是串口SPI。并口屏引脚多,D0到D7八根数据线,加上CS、RS、WR、RD、RST、PSB等控制脚,占用的MCU引脚比较多。SPI屏就省脚得多,MOSI、SCK、CS、DC、RST五根线搞定。做工程时选哪种接口,取决于你的MCU引脚余量和对刷新速度的要求。如果只做简单显示,SPI够用;如果要做曲线、图片这类大数据量刷新,并口优先。

1.2 为什么选择STM32来驱动它

现在很多人一上来就推荐用带GUI硬件加速的高端MCU或者直接上串口屏,但STM32驱动19264仍然是非常经典的组合,原因很朴素:第一,STM32引脚多且电平是3.3V,跟常见19264模组的逻辑电平天然匹配,不需要额外电平转换;第二,芯片性价比高,哪怕用一片STM32F103C8T6这种几块钱的片子,也能一边刷屏一边跑ADC采样、按键扫描、串口协议栈,互不干扰;第三,开发资料极其丰富,标准库、HAL库、寄存器版本都有人用过,遇到问题容易检索到答案。

在我实际做的这个项目里,主控用的就是STM32F103系列,标准库开发,屏幕走的是8080并口。之所以没用FSMC硬件总线,是因为当时这款屏的引脚排列比较特殊,FSMC映射起来不顺手,而GPIO模拟并口逻辑简单、实现直观,调试起来也方便。如果你对速度没有极端要求,GPIO模拟并口完全够用,这也是这套驱动工程最通用的做法。

2. 驱动方案选型与底层原理

2.1 三种主流的驱动方式怎么选

先说结论:STM32驱动并口19264的方式主要有三种,各有利弊。

第一种是GPIO模拟并口,也就是用普通IO口逐位去操作数据线和控制线。它的优势是引脚任意分配,代码逻辑完全可控,出问题好排查;劣势是刷屏速度受限于GPIO翻转频率,全屏刷一帧大约要几毫秒到十几毫秒。对于菜单、仪表盘这类界面,完全够用。

第二种是FSMC硬件总线。STM32的FSMC可以把外部设备映射到内存地址,你往某个地址写数据,硬件自己就把时序生成了。它最大的优势是刷屏速度快得多,CPU只需要做数据搬运,剩下的时序交给硬件。劣势是引脚基本固定,D0到D15、NOE、NWE、NE1、A0这些都要接到特定位置,PCB布线时比较别扭,而且不是所有型号都有FSMC外设。

第三种是SPI接口。如果你买到的19264本身就是SPI版本,或者模组上的PSB引脚可以切到串口模式,那么用STM32的硬件SPI驱动最省引脚,速率也不低。需要留意的是,SPI模式下时钟极性和相位必须跟屏幕控制器的要求对上,常见的配置是CPOL=0、CPHA=0,但最好以手册为准。

我最终选了GPIO模拟并口,主要考虑到项目里屏幕数据读取(读状态、读显存)也要用,GPIO方式实现起来最直接,不依赖任何总线的怪异映射。

2.2 8080时序到底在讲什么

很多初学者卡在驱动写不出来,其实不是不会写GPIO,而是没看懂时序图。8080并口的写时序,简单说就是“选设备、选通道、放数据、打拍子”。

具体操作顺序是:先把CS拉低,选中这片LCD;再设置RS(也叫A0)的电平,表示接下来传输的是指令还是数据;然后把要发送的字节放到D0到D7上;最后给WR一个低脉冲。数据是在WR上升沿被锁存进去的。写数据和写指令唯一的区别,就是RS电平不同——RS低是写命令,RS高是写数据。读时序类似,把WR改成RD,数据从屏里读出来,但读操作比写操作慢,中间要加一点延时,否则数据不稳定。

这里有个特别容易犯的错:有人先拉WR再放数据,或者RS和数据同时变化,结果是数据线还没稳定,控制器已经采样了,屏幕上就出现乱码。正确的做法永远是先准备好RS和数据线电平,再给WR脉冲。时序上虽然数据建立时间(tDS)只有几十纳秒,但对于低速的软件模拟来说,你只要保证“先设置后打拍子”这个顺序,就永远不会错。

2.3 硬件接线设计注意事项

硬件接线看着简单,实际坑不少。我习惯把所有引脚选在同一个IO口组上,比如数据线D0到D7放在PC0到PC7,控制线放在PC8到PC12,这样操作时可以按位操作端口寄存器,代码效率更高,也方便以后换FSMC方案时调整。

选引脚时第一禁忌是占用JTAG/SWD调试口。STM32上PA13、PA14、PA15、PB3、PB4默认是调试接口,如果你把LCD数据线挂在这些脚上,程序一运行,下载器直接断开,连程序都烧不进去。别问我是怎么知道的,这个错我早期至少踩过两回。

背光引脚不要直接怼MCU的3.3V,更不要直接接5V。19264的背光一般要串一个限流电阻,阻值根据屏的背光电流算,通常几十欧到一两百欧。如果要做背光开关控制,用三极管或者MOS管做开关,不要让背光电流流过MCU引脚。

对比度调节引脚V0是另一个关键点。V0电压决定了屏幕上像素的深浅,调低了花屏,调高了鬼影。最省事的做法是V0接一个10K电位器,一端接VEE,一端接地,中间抽头接V0,上电后肉眼调到显示最清晰即可。

3. 驱动代码核心实现(标准库+GPIO模拟并口)

3.1 引脚初始化与基础读写函数

我用的标准库版本,引脚初始化没有特别之处,就是把控制引脚和数据引脚统一配置成推挽输出,速率设为50MHz。如果不需要读屏,RD可以留空;但强烈建议把读函数也写了,后面调试和读显存都方便。

// GPIO初始化(以PC口为例) void LCD_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10 | GPIO_Pin_11 | GPIO_Pin_12; GPIO_Init(GPIOC, &GPIO_InitStructure); }

写一个字节的函数是整个驱动的心脏。为了效率,我直接用寄存器操作,不调用库函数。

#define LCD_CS(x) GPIOC->BSRR = x ? GPIO_Pin_8 : (uint32_t)GPIO_Pin_8 << 16 #define LCD_RS(x) GPIOC->BSRR = x ? GPIO_Pin_9 : (uint32_t)GPIO_Pin_9 << 16 #define LCD_WR(x) GPIOC->BSRR = x ? GPIO_Pin_10 : (uint32_t)GPIO_Pin_10 << 16 #define LCD_RD(x) GPIOC->BSRR = x ? GPIO_Pin_11 : (uint32_t)GPIO_Pin_11 << 16 #define LCD_RST(x) GPIOC->BSRR = x ? GPIO_Pin_12 : (uint32_t)GPIO_Pin_12 << 16 // 8080写字节,isData为1写数据,为0写命令 void LCD_WriteByte(uint8_t data, uint8_t isData) { LCD_CS(0); LCD_RS(isData); // 数据放到D0~D7 GPIOC->ODR = (GPIOC->ODR & 0xFF00) | data; LCD_WR(0); LCD_WR(1); // WR上升沿锁存数据 LCD_CS(1); }

这段代码里有个细节:先设置RS和数据,再操作WR。这就是我前面强调的“先准备后打拍子”。对于初学标准库的人来说,可能会疑惑为什么用BSRR寄存器而不是GPIO_SetBits,这里直接操作寄存器一是减少函数调用开销,二是在刷屏时可以明显提升速度。

3.2 初始化序列与清屏、坐标定位

19264初始化看起来代码不长,但每条命令都有讲究。下面是一段适用于ST7565系列控制器的初始化序列,基本覆盖了偏压设置、ADC方向、扫描方向、对比度等关键项。

void LCD_Init(void) { LCD_GPIO_Init(); LCD_RST(0); delay_ms(10); LCD_RST(1); delay_ms(10); LCD_WriteByte(0xA2, 0); // 1/9偏压 LCD_WriteByte(0xA0, 0); // 列地址从左到右 LCD_WriteByte(0xC8, 0); // 行扫描方向从上到下 LCD_WriteByte(0xA6, 0); // 正常显示 LCD_WriteByte(0x2F, 0); // 开启电源 LCD_WriteByte(0x27, 0); // 内部电阻调整 LCD_WriteByte(0x81, 0); // 对比度设置第一字节 LCD_WriteByte(0x1A, 0); // 对比度值,可调整 LCD_WriteByte(0xAF, 0); // 显示开 LCD_Clear(); }

这里特别提醒:0x81对比度设置是分两字节的,第一字节是命令,第二字节才是值。如果只写了0x81没写值,对比度会处在一个随机状态,屏幕要么全黑要么白屏。另外0x27这个内部电阻调整值在不同的屏上表现不一样,屏幕底色偏深就把值往小调,偏浅就往大调。

19264的显示RAM是按页划分的。纵向上64行被分成8页,每页8行;横向上192列就是192个字节列。也就是说,一个字节在屏幕上对应的是“某一列从上往下8个点”,而不是横向的8个点。这一点和很多人的直觉不一样,画点、画字符前一定要先搞清楚。

// 设置列地址和页地址 void LCD_SetPos(uint8_t col, uint8_t row) { LCD_WriteByte(0xB0 | (row >> 3), 0); // 页地址,row是0~63 LCD_WriteByte(0x10 | ((col >> 4) & 0x0F), 0); // 列高四位 LCD_WriteByte(0x00 | (col & 0x0F), 0); // 列低四位 }

清屏就是把DDRAM全部写0。按照页0到7、列0到191的顺序循环写数据即可,注意写完一页要重新设置下一页地址,计数器不会自动跨页。

3.3 画点、画线、画矩形、画圆的实现

画点是所有图形功能的基础。19264的显存是按列页组织,所以要画一个点,必须先把目标字节读出来,把对应位改成1(或0),再写回去。如果不读回直接整体赋值,就会把同一页同一列其他行已有的点全部抹掉。

void LCD_DrawPoint(uint8_t x, uint8_t y, uint8_t color) { uint8_t page = y >> 3; uint8_t bitPos = y & 0x07; uint8_t readData; if (x >= 192 || y >= 64) return; LCD_SetPos(x, y); LCD_ReadByte(&readData); // 读回原字节 if (color) { readData |= (uint8_t)(0x01 << bitPos); } else { readData &= (uint8_t)~(0x01 << bitPos); } LCD_SetPos(x, y); LCD_WriteByte(readData, 1); }

有了画点函数,画线用Bresenham算法,画矩形、填充矩形、画圆就都水到渠成了。工程里我把这些函数放在了graphic.c里,和底层驱动lcd.c分开,上层的菜单、状态机只调用graphic接口。这种分层写法,后期想换SPI屏或者FSMC屏,只需要改lcd.c,上层代码一行不用动。

4. 汉字与字符显示:字模与取模实践

4.1 字符与汉字显示的原理

19264本身不认识汉字,它只认显存里的点阵数据。所谓显示汉字,本质就是把一个汉字按16×16点阵拆成32个字节,逐字节写入DDRAM。字符更简单,比如8×16的ASCII码,一个字符16个字节,占用一页里的两列。

这里要理解“页”和“字高”的关系。16×16的汉字高度是16行,跨越两页:上半8行写到第n页,下半8行写到第n+1页。所以显示汉字时不能像显示字符一样一路顺写,而是要分别定位到两个页地址,再写入各自的两列数据。

4.2 取模软件与参数设置

取模软件我用的是PCtoLCD2002,网上到处有,免费好用。重点在参数设置,设置不对,显示屏上就会出现“躺倒”的汉字或者镜像文字。

我的配置是:取模方式选“横向取模”,也就是每行从左到右取8个点组成一个字节;字节内的位序选“高位在前”,这样字节最高位对应最左边的点。汉字是16×16,所以上半页是16个字节,下半页也是16个字节,总共32个字节。

生成出来的数据大概是这样的格式:

// “嵌”字 16×16 点阵,横向取模,高位在前 const uint8_t hanzi_qian[32] = { 0x08, 0x08, 0x08, 0x0B, 0x7C, 0x8A, 0x08, 0x88, 0x08, 0x88, 0x08, 0x0A, 0x28, 0x08, 0x08, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x02, 0x22, 0x22, 0x20, 0x20, 0x20, 0xE0, 0x20, 0x20, 0x20, 0x20, 0x00 };

特别强调一点:不同控制器支持的显示方向不同,取模方向必须和屏幕驱动方向一致。如果你设置的列地址是从左往右、扫描从上往下,那就用上面的配置;如果你的屏因为走线原因是从右往左扫,取模方向就要反过来。否则字会左右颠倒,很多人一上来就怀疑是硬件问题,其实是取模参数不对。

4.3 把汉字写进显存的思路

显示一个16×16汉字的流程,我封装成一个函数:

void LCD_ShowHz(uint8_t x, uint8_t y, const uint8_t *hzData) { uint8_t i, pageBase = y >> 3; // 上半页 LCD_SetPos(x, pageBase); for (i = 0; i < 16; i++) { LCD_WriteByte(hzData[i], 1); } // 下半页 LCD_SetPos(x, pageBase + 1); for (i = 16; i < 32; i++) { LCD_WriteByte(hzData[i], 1); } }

下一行画字符时x坐标加16。如果是混排,英文按8×16点阵处理,x就加8。为了让字库不占太多RAM,工程里我把字模数组放在const段,编译后直接进Flash。如果项目里要显示的汉字特别多,比如几百个,建议提前用工具把常用汉字提取成自己的字库文件,放到外部Flash或者SD卡里,按索引查询,这个项目规模不大,直接数组就够了。

5. 常见问题与排查技巧实录

这部分是干货中的干货。我把做这个项目时遇到以及帮别人排查过的高频问题整理成一张速查表,每一项都是真实踩过的坑。

故障现象可能原因排查与解决办法
屏幕全白或者全黑对比度V0没调对调电位器,如果没有电位器就改初始化里的0x81后面的参数
屏幕无任何反应复位引脚悬空或初始化没复位确认RST有上电拉低再拉高的动作,至少10ms延时
显示的字符颠倒取模方向与扫描方向不一致检查ACD命令设定的列地址方向,跟取模方式对齐
显示乱码、点错位页地址和列地址计算错误先用画点函数测试四个角,再显示简单字符
字体重叠写入前未关闭显示,或没读回字节直接整字节覆盖写字符前做整列清除,或者使用读改写模式
程序下载不了占用了PA13/14/15或PB3/PB4调试口换引脚,或者用ST-Link Utility清除读保护
刷新很慢GPIO模拟并口频繁调用库函数关键函数直接用寄存器操作,减少函数调用
背光不亮限流电阻太大或三极管接错测背光两端电压,正常应为2.8~3.3V

有两个排查方法非常管用。第一个是“点灯法”:上电后先调用一次清屏和画点函数,在屏幕四角各画一个点,如果四个点位置正确,说明坐标体系没问题,之后再显示字符、图形才可信。第二个是逻辑分析仪抓时序:如果屏完全没反应,用逻辑分析仪看CS、RS、WR和数据线的电平变化是否符合8080写时序。很多“屏坏了”的结论,最后都是时序参数差了一口气。

还有一个经验想专门说一下:调试带汉字显示的工程,不要最开始就把所有字库塞进去。先用单个汉字数组测试,确认取模方向正确了,再填充整个字库。因为字库错了的话看起来全是乱码,很难定位是取模问题还是坐标问题,分割变量才能快速定位。

写在最后的建议

如果这个驱动工程要复用,我强烈建议把底层封装做干净。当时我就是图省事,直接把画点函数写在lcd.c里,后来要换SPI串口屏,底层重写了一遍。后面我重新整理时,把底层接口统一成了“LCD_SetPos、LCD_ReadByte、LCD_WriteByte”三个函数,上层图形库只依赖这些接口。这样无论片内控制器换成ST7565还是ST7920,底层替换都不痛苦。

另外,如果你做的产品要长期跑,记得在初始化完成后做一次全屏填充0xAA的测试,也就是画棋盘格,肉眼检查有没有列短路或虚焊。很多焊接问题不是上电就能发现的,棋盘格一眼就能扫出来。

这个工程扩展起来也很有意思,现在这个驱动基础上,我做了一个简单的菜单框架:一个结构体数组存菜单项,一个光标位置变量控制高亮,按键中断改状态,屏幕只显示当前页。后面甚至加了一页实时数据曲线,就是画点函数加一个环形缓冲区而已。所以说,19264虽然老,但把它玩透,你收获的是一整套显存管理、坐标计算、字模处理的底层能力,这些东西到了任何一块LCD屏上都能复用。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 23:47:14

加密货币清算与爆仓机制:杠杆交易者的风险防范指南

1. 清算的底层逻辑&#xff1a;保证金交易里那把悬在头顶的刀 1.1 为什么会有清算&#xff1a;交易所的“风控底线”究竟是什么 很多人第一次接触“清算”这个词&#xff0c;是在某个凌晨看到自己账户的仓位突然消失&#xff0c;或者看见行情图上出现一根极长的影线。我当时第…

作者头像 李华
网站建设 2026/9/9 23:41:59

全家游北京一家一团怎么选?2026 北京一家一团服务标准及哪家好不拼陌生人深度指南

对于计划全家一起来北京旅行的家庭来说&#xff0c;选择合适的一家一团服务商是出行规划中最重要的决策。北京一家一团服务标准、北京一家一团哪家好、全家游北京一家一团、北京一家一团不拼陌生人&#xff0c;这四个关键词反映了家庭游客对服务品质、选择标准、全家游需求、团…

作者头像 李华
网站建设 2026/9/9 23:41:35

汉字与字母文字:两套操作系统的架构对比与演化逻辑

“字母文字的焦虑&#xff1a;当汉字成了文明发展的高效操作系统”——光看这个标题&#xff0c;我就知道这不是一个用来做情绪宣泄的选题&#xff0c;而是可以当成一套系统架构来聊的话题。我自己写文章、做产品、处理中英文信息有十几年了&#xff0c;频繁在两种文字系统之间…

作者头像 李华