简介面向嵌入式初学者与单片机开发者的12864显示屏驱动资料包围绕ST7920控制器实现自定义字符、反白显示、字体与菜单图片的显示需求。压缩包共14个文件约43KB以Keil工程文件、C语言源码、HEX烧录文件和编译中间文件为主另含少量备份与项目配置记录可直接用于梳理ST7920的SPI通信、字符发生器RAM写入、ASCII码与位图映射、指定坐标显示字符串等实现思路。已有1123人学习下载适合需要快速搭建12864显示实验环境、对照工程结构理解驱动分层与命令调用的读者。包内工程可直接编译或烧录验证便于在自定义字符模板、字体查找表、反白模式切换、位图逐像素发送以及菜单按钮与选择框绘制之间建立完整参照减少从零调试显示效果与排查接口问题的时间。1. 带字库 12864 的 ST7920自定义字符和图片为什么总在两个 RAM 里打架手上这块带字库的 12864 用的是 ST7920最近被问得最多的不是怎么显示汉字而是怎么显示自定义字符、怎么再显示图片。它内置 GB2312 字库写两个字节就能出汉字看起来省事但自定义字符在 CGRAM图片在 GDRAM文本模式又在 DDRAM三块地址各管一段。很多人把 CGRAM 字模写进去了屏幕上却是空白或内置汉字图片取模也对显示出来却错位、上下颠倒。问题通常不在代码量而在模式切换和地址映射。下面按并口/串口、内置字库、CGRAM、GDRAM、避坑、上板验收的顺序拆适合拿单片机驱动 12864 做小设备界面的从业者也适合第一次碰 ST7920 的新手照着复现。2. 接口与初始化并口 8 位、串口 3 线和忙标志的实测顺序2.1 并口 8 位RS/RW/E 与数据口的输入输出切换ST7920 模块上一般有个 PSB 引脚接高电平走并口接低电平走串口。并口模式下RS 选指令或数据RW 选读或写E 是使能脉冲数据口 D0-D7 在写的时候输出、读忙的时候必须切成输入。写一个字节的顺序是RS 和 RW 先稳定数据放到 D0-D7E 拉高保持一个窄脉冲再拉低模块在 E 的下降沿把数据取走。读忙标志则是 RS0、RW1、E 拉高后读 DB7DB7 为 1 表示忙。这里最容易翻车的是数据口方向很多单片机 GPIO 默认输出读忙时没有切输入读回来永远是 0xFF程序就一直等。我一般把并口底层收成三个函数写命令、写数据、读忙。命令和数据的区别只在 RS写时序完全一样。E 脉冲不用太宽但也不能太窄按手册的几百纳秒量级留 1us 比较稳普通指令执行时间按 72us 留清屏指令单独留 2ms。下面这段是 8 位并口的骨架引脚宏按你手头模块改。// 8 位并口RS0 指令RS1 数据RW0 写RW1 读 #define LCD_RS_H() GPIO_SetBits(GPIOA, GPIO_Pin_1) #define LCD_RS_L() GPIO_ResetBits(GPIOA, GPIO_Pin_1) #define LCD_RW_H() GPIO_SetBits(GPIOA, GPIO_Pin_2) #define LCD_RW_L() GPIO_ResetBits(GPIOA, GPIO_Pin_2) #define LCD_E_H() GPIO_SetBits(GPIOA, GPIO_Pin_3) #define LCD_E_L() GPIO_ResetBits(GPIOA, GPIO_Pin_3) #define LCD_DATA_OUT(d) GPIO_Write(GPIOB, (uint16_t)(d)) #define LCD_DATA_IN() GPIO_ReadInputData(GPIOB) static void lcd_delay_us(uint32_t us) { // 按主频实现 1us 级延时不要用空循环硬猜 } static void lcd_write_cmd(uint8_t cmd) { LCD_RS_L(); LCD_RW_L(); LCD_DATA_OUT(cmd); LCD_E_H(); lcd_delay_us(1); LCD_E_L(); lcd_delay_us(72); // 普通指令约 72us } static void lcd_write_data(uint8_t dat) { LCD_RS_H(); LCD_RW_L(); LCD_DATA_OUT(dat); LCD_E_H(); lcd_delay_us(1); LCD_E_L(); lcd_delay_us(72); } static uint8_t lcd_read_busy(void) { uint8_t sta; LCD_RS_L(); LCD_RW_H(); LCD_DATA_IN(); // 数据口必须切输入 LCD_E_H(); lcd_delay_us(1); sta (uint8_t)LCD_DATA_IN(); LCD_E_L(); return (sta 0x80); // DB71 忙 }这段代码里lcd_delay_us(72)是给普通写指令和写数据留的执行时间不是给 E 脉冲留的。E 脉冲本身只占 1us 左右真正要等的是模块内部把数据落到 RAM。清屏指令 0x01 的执行时间明显更长写完要单独等 2ms 左右否则紧接着写地址会丢。读忙函数只读 DB7不读其他位因为其他位在忙的时候没有意义。数据口切输入这一步不能省尤其是用 8051 这类准双向口时要先写 0xFF 再读。2.2 串口模式省 IO 的代价是读不到忙标志模块引脚少的时候会走串口模式PSB 接低只用 CS、SCLK、SID 三根线。ST7920 的串口不是标准 SPI每个 8 位字节要拆成三个字节发第一个字节是起始字节格式为11111 RW RS 0写操作 RW0RS 决定是命令还是数据第二个字节放数据的高 4 位低 4 位补 0第三个字节放数据的低 4 位左移到高 4 位。也就是说发一个命令要发三个字节发一个数据也要发三个字节。模块在 SCLK 的上升沿取样CS 的有效电平要看模块手册常见是低有效但有些底板加了反相不能照抄。串口模式最大的坑是读不到忙标志。并口可以读 DB7串口没有读通道只能靠固定延时等指令执行完。普通指令留 72us清屏留 2ms这个时间不能省。另一个坑是 SCLK 空闲电平如果空闲时是高电平第一个上升沿可能被模块当成有效边沿花屏概率会上去。我一般让 SCLK 空闲为低发数据前先把 CS 拉有效发完再拉无效。static void lcd_spi_write_byte(uint8_t b) { for (uint8_t i 0; i 8; i) { LCD_SCLK_L(); if (b 0x80) LCD_SID_H(); else LCD_SID_L(); b 1; LCD_SCLK_H(); // 上升沿取样 } } static void lcd_serial_write(uint8_t rs, uint8_t data) { uint8_t start 0xF8 | (rs ? 0x02 : 0x00); // 11111 RW RS 0RW0 LCD_CS_H(); // 按模块有效电平改这里假设高有效 lcd_spi_write_byte(start); lcd_spi_write_byte(data 0xF0); lcd_spi_write_byte((uint8_t)((data 4) 0xF0)); LCD_CS_L(); lcd_delay_us(72); // 串口读不到忙只能等 }start字节里的0xF8是写操作的基础值rs ? 0x02 : 0x00把 RS 位带进去。第二个字节只取高 4 位第三个字节把低 4 位挪到高 4 位低 4 位补 0。这个拆法和并口写一个字节的效果一样只是传输时间更长。串口模式下不要频繁清屏清屏一次要等 2ms界面刷新快的时候会明显拖慢。2.3 初始化序列0x30、0x0C、0x01 谁先谁后上电后不要立刻写指令模块内部电源和复位需要时间我一般先等 50ms。第一条指令发 0x30意思是 8 位并口、基本指令集如果模块之前被别的程序切到扩展指令集这一条会把它拉回来。接着发 0x0C开显示、关光标、关闪烁再发 0x01 清屏等 2ms最后发 0x06光标右移、地址加一。这个顺序不要颠倒清屏放在显示开之后问题不大但清屏后一定要等够时间。void lcd_init(void) { lcd_delay_ms(50); // 上电等待别省 lcd_write_cmd(0x30); // 8 位并口基本指令集 lcd_delay_ms(2); lcd_write_cmd(0x30); // 有些模块上电状态不稳补一条 lcd_write_cmd(0x0C); // 开显示关光标 lcd_write_cmd(0x01); // 清屏 lcd_delay_ms(2); lcd_write_cmd(0x06); // 光标右移地址加一 }0x30 是功能设定0x0C 是显示控制0x01 是清屏0x06 是进入点设置。清屏后 DDRAM 全是空格但 GDRAM 不一定清图片显示前最好自己填 0。初始化里补一条 0x30 是血泪经验有些模块上电时电源爬升慢第一条指令没收到后面全乱。等到第 6 章验收时如果屏幕一片黑或全亮先回头看这段延时和 0x30 有没有发进去。3. 内置字库与 CGRAMGB2312 寻址、半宽 ASCII 和 32 字节自定义字模3.1 文本模式 DDRAM 的行地址不是 0x80 到 0x9F 顺序排ST7920 的文本模式 DDRAM 地址不是从头到尾线性排列的。12864 屏幕显示 4 行汉字每行 8 个 16x16 汉字但行基地址是 0x80、0x90、0x88、0x98不是 0x80、0x90、0xA0、0xB0。第一行从 0x80 开始第二行从 0x90 开始第三行回到 0x88第四行是 0x98。列地址以字节为单位一行最多 16 个字节地址一个 16x16 汉字占两个字节地址所以一行放 8 个汉字。写字符串时如果按 0x80 开始一直加写到第一行末尾会跳到第二行的隐藏区域屏幕上看起来就是文字跑位。行号行基地址可写列地址16x16 汉字个数第 1 行0x800x80-0x8F8第 2 行0x900x90-0x9F8第 3 行0x880x88-0x8F8第 4 行0x980x98-0x9F8我一般把行基地址做成常量数组显示前先设 DDRAM 地址再连续写数据。lcd_set_ddram(row, col)里的col用字节列不是汉字列写一个汉字要占两个字节列所以混排时列计数要按实际写入的字节数走。static void lcd_set_ddram(uint8_t row, uint8_t col) { static const uint8_t row_base[4] {0x80, 0x90, 0x88, 0x98}; lcd_write_cmd(row_base[row 0x03] (col 0x0F)); }row取 0-3col取 0-15row_base就是上面表里的四个基地址。col 0x0F是防止越界真正写之前最好自己判断列有没有超。这个函数只设地址不写数据写数据由后面的字符串函数做。3.2 GB2312 直接写两字节不要在单片机里做 UTF-8 转码ST7920 内置字库按 GB2312 编码寻址显示一个汉字就往 DDRAM 写两个字节第一个字节是高字节第二个字节是低字节。比如“中”的 GB2312 编码是 0xD6 0xD0直接写这两个字节就能出字不需要先算区位码再查表。区位码和 GB 码的关系是“高字节减 0xA0、低字节减 0xA0”但 ST7920 内部认的是 GB 码本身写区位码反而不对。ASCII 半宽字符只写一个字节范围 0x20-0x7E0x80 以上按双字节处理。不要在单片机里做 UTF-8 到 GB2312 的转换。单片机 Flash 和 RAM 都紧转换表一放就是几 KB还容易把双字节截断。常见做法是在 PC 端把要显示的汉字转成 GB 码数组或者直接用 GB2312 编码保存字符串编译进固件。下面这个显示函数按字节判断小于 0x80 的当 ASCII 写一个字节大于等于 0x80 的连续写两个字节。void lcd_show_text(uint8_t row, uint8_t col, const uint8_t *s) { lcd_set_ddram(row, col); while (*s) { if (*s 0x80) { lcd_write_data(*s); // ASCII 半宽占一个字节地址 } else { lcd_write_data(*s); // GB2312 高字节 lcd_write_data(*s); // GB2312 低字节 } } }这个函数没有做列边界检查实际用的时候要么保证字符串不超行要么在写之前算好长度。ASCII 字符在 ST7920 上显示为半宽占一个字节地址汉字占两个字节地址。混排时“ABC中”会占 3 个 ASCII 地址加 2 个汉字地址共 5 个字节列。行地址自动加一写满一行不会自动换行会继续往当前行的隐藏地址走所以长字符串要自己拆行。3.3 CGRAM 的 0x40 起始地址与显示码 0x00/0x02ST7920 带字库版本留了 64 字节 CGRAM刚好放两个 16x16 自定义字符或者四个 16x8 半宽字符。每个 16x16 字符占 32 字节排列是 16 行每行两个字节第一个字节是左半 8 像素第二个字节是右半 8 像素MSB 在左。CGRAM 地址从 0x40 开始第一个 16x16 字符占 0x40-0x5F对应显示码 0x00 和 0x01第二个占 0x60-0x7F对应显示码 0x02 和 0x03。写 CGRAM 之前要先切扩展指令集 0x34写完再切回基本指令集 0x30。static void lcd_write_cgram(uint8_t code, const uint8_t glyph[32]) { lcd_write_cmd(0x34); // 扩展指令集 lcd_write_cmd(0x40 | (code 4)); // code0x00 - 0x40code0x02 - 0x60 for (uint8_t i 0; i 32; i) { lcd_write_data(glyph[i]); } lcd_write_cmd(0x30); // 回基本指令集 } void lcd_show_cgram_char(uint8_t row, uint8_t col, uint8_t code) { lcd_set_ddram(row, col); lcd_write_data(code); // 写显示码不是 CGRAM 地址 lcd_write_data(code 1); // 16x16 占两个字符码 }code只取 0x00 或 0x02code 4把第二个字符的起始地址推到 0x60。写 32 字节时地址会自动加不需要每写一个字节重设。显示的时候写的是字符码 0x00/0x01 或 0x02/0x03不是 CGRAM 地址 0x40/0x60这一点最容易搞混。取模软件里的参数按下面这张表设字模排列才对得上。取模选项建议值说明字宽×字高16×16对应一个汉字取模方式本文还有配套的精品资源点击获取