这一篇在干嘛?
前面驱动的 TFT 彩屏「只认像素」,而 LCD12864 是一块自带中文字库的字符型液晶:你只要把汉字的国标编码写进去,它自己就能把字画出来。本实验用 ST7920 控制器的 CM12864-12 屏,通过 8 位并口显示四行内容(汉字、英文网址、电话号码),还演示了如何用 CGRAM 自定义一个字库里没有的图标。这是一篇实践性极强、坑也极多的驱动实验。
实验目标与原理
认识 LCD12864 与 ST7920
LCD12864 带字库,型号 CM12864-12。其相关数据手册为「ST7920 系列中文图形液晶模块使用说明书」,里面有详细的介绍,这里就不在多描述。其原理简图如下(我们只需关心接口部分):
系列原理简图

12864 指的是 128×64 的点阵分辨率。因为自带 16×16 中文字库(CGROM),按一个汉字占 16×16 点阵算,整屏可以显示 4 行、每行 8 个汉字——这就是它比「纯点阵屏」省事的地方:显示「杭州」两个字,不需要你提供 32×2 字节的字模,只需写两个字节编码。
接口定义与引脚表
接口定义中要注意 V0:原文作者的经验是「我一开始就栽倒这,网上搜到的资料中,这 PIN 可以悬空或接个滑动电阻,程序检查好多遍,没有发现问题,就是不显示,接到 3.3V 上也不行,后来把 V0 直接接到 5V 上,就可以正常显示。在这提醒大家数据手册仅供参考,具体还是以自己的实物为主。避免走没必要的弯路。」
| 管脚号 | 管脚名称 | 电平 | 管脚功能描述 |
| 1 | VSS | 0V | 电源地 |
| 2 | VCC | 5V | 电源正 |
| 3 | V0 | - | 对比度(亮度)调整.(直接接到5V上最靠谱) |
| 4 | RS(CS) | H/L | RS=“H”,表示DB7——DB0为显示数据RS=“L”,表示DB7——DB0为显示指令数据 |
| 5 | R/W(SID) | H/L | R/W=“H”,E=“H”,数据被读到DB7——DB0R/W=“L”,E=“H→L”,DB7——DB0的数据被写到IR或DR |
| 6 | E(SCLK) | H/L | 使能信号 |
| 7 | DB0 | H/L | 三态数据线 |
| 8 | DB1 | H/L | 三态数据线 |
| 9 | DB2 | H/L | 三态数据线 |
| 10 | DB3 | H/L | 三态数据线 |
| 11 | DB4 | H/L | 三态数据线 |
| 12 | DB5 | H/L | 三态数据线 |
| 13 | DB6 | H/L | 三态数据线 |
| 14 | DB7 | H/L | 三态数据线 |
| 15 | PSB | H/L | H: 8位或4位并口方式,L:串口方式 |
| 16 | NC | - | 空脚 |
| 17 | /RESET | H/L | 复位端,低电平有效(悬空或接到5V都可以) |
| 18 | VOUT | - | LCD驱动电压输出端(悬空) |
| 19 | A | VDD | 背光源正端(+5V) |
| 20 | K | VSS | 背光源负端(0V) |
几个关键控制线:RS 区分数据和命令(高电平是数据,低电平是命令);R/W 区分读和写(本实验固定写,接地);E 是使能,数据在 E 的上升沿被锁存;PSB 选择并口或串口模式(本实验固定接高,用 8 位并口)。
时序:用 6ms 慢时钟避开「忙检测」
ST7920 数据手册中提到,每次在写命令之前都要判断 LCD 是否处于空闲状态(忙标志为 0 表示空闲,为 1 表示忙)。本次实验全过程只写不读,为了避开 LCD 忙的状态,可以采取隔段时间写一次命令或数据。
在指令表中可以看到每个命令执行的时间。这里注意,除清除显示和地址归位(网上有不同说法,有的说是 1.6ms,有的说 4.6ms)外,其他命令执行时间都是 72us。不管怎么样,我们还是取最大值更为靠谱:4.6ms 取个整,取 6ms。当然可以取比这个更大——LCD 在显示数据时是一个接一个地显示出来的,可以察觉到取的越大显示速度就越慢,看自己的决定了。
利用 FPGA 分频,产生一个周期 6ms 的时钟,给 LCD 的 E 管脚,进行控制写数据或命令。在 8 位并口写操作时序图中,DB0~DB7 是在 E 为高时写入数据。
8 位并口写操作时序图:

「降速换简单」是 LCD 驱动的常用思路
用一个 6ms 周期的慢时钟直接驱动整个状态机,每一步操作之间的间隔天然满足最慢命令(清屏 4.6ms)的执行时间要求,就完全不需要读忙标志、不需要每条命令单独延时。代价是整个初始化加显示要几秒,但换来的是极简的时序控制——这种思路在低速外设驱动里非常实用。
显示什么、显示在哪
知道了何时给 LCD 写数据或命令,还得知道如何把自己想要显示的东西显示在 LCD 屏幕上,这一点也不难:
第一,得知道中文字符的编码。可以查国标 GB2312 来调用 CGROM 字库,比如「文:CEC4、少:C9D9、清:C7E5」。或者可以下载「汉字十六进制转换工具」软件直接生成。
第二,得知道显示文字的地址该如何设置,也就是一个字我可以摆在第一行的开头或结尾或其他行,如下图示:
| 80H | 81H | 82H | 83H | 84H | 85H | 86H | 87H |
| 90H | 91H | 92H | 93H | 94H | 95H | 96H | 97H |
| 88H | 89H | 8AH | 8BH | 8CH | 8DH | 8EH | 8FH |
| 98H | 99H | 9AH | 9BH | 9CH | 9DH | 9EH | 9FH |
这张 DDRAM 地址表有个著名的「陷阱」:第一行是 80H87H,第三行却是 88H8FH(地址是接着第一行排的),第二行 90H97H、第四行 98H9FH。背光上每行实际可显 16 个半宽字符(8 个汉字),但 DDRAM 每行预留了 16 字节,所以换行时地址不是简单加 8。代码中第一行用 80H、第二行用 91H、第三行用 88H、第四行用 98H,正是对照这张表得出的。
三种字型与自定义字符(CGRAM)
显示 RAM(DDRAM)提供 64×2 字节的空间,最多可以控制 4 行 16 字的中文字型显示。当写入显示资料 RAM 时,可以分别显示 CGROM、HCGROM 及 CGRAM 的字型。
三种字型的选择:
1)显示半宽字型:将一个字节的编码写入 DDRAM 中,范围是 02~7FH;
2)显示 CGRAM 字型:将 2 个字节的编码写入 DDRAM 中,共有 0000H、0002H、0004H 及 0006H 四种编码;
3)显示中文字型:将 2 字节的编码写入 DDRAM 中,先写高 8 位,后写低 8 位,范围是 A140H~D75FH(BIG5),A1A0H~F7FFH(GB)。
若自带的库中没有你想要的东西时,可以自己定义,比如代码中的星星月亮组合图标,库中没有,那么我们可以自己定义(组合图标的编码可以用字模软件进行提取)。自定义字符具体操作:
- 先设定 CGRAM 存储地址;
- 把该组合图标的编码数据放置到该地址中;
- 再设置该图标在 LCD 屏幕显示的位置;
- 最后连续写两次数据(注意写的是数据不是命令,原文作者也被这东西栽倒一次),先写高 8bit,后写低 8bit,就可以了。
此时要知道写这数据到底是啥数据,那么就得理解 0x0000、0x0002、0x0004、0x0006 这四种编码。至于这四种编码有什么区别,原文作者也不是很明白:在程序中,0x0000 和 0x0006 没发现有区别,显示正常;0x0002 在星星的左上角有个小点;0x0004 显示乱码。
第四种是半宽字型:数据手册中写的是「半宽字型 ROM(HCGROM),总共提供 126 个西文字型(16×8 点阵)」,应该就是英文字符,程序中的「LCD12864」即属此类。
Verilog 实现
在写代码之前理清思路,程序的流程该如何跑,写起代码来也比较顺手,如下图。前面设置好频率后,每步中的延时就不用再考虑了。

代码(使用并口,只有写):
LCD12864.v
module LCD_12864(
//input
sys_clk,
rst_n,
//output
lcd_rs,
lcd_rw,
lcd_en,
lcd_data, endcase
lcd_psb
);
input sys_clk;// 50MHZ
input rst_n;
output lcd_rs;//H:data L:command
output lcd_rw;//H:read module L:write module
output lcd_en;//H active
output [7:0] lcd_data;
output lcd_psb;//H:parallel module L:SPI module
/*****
parameter T3MS = 18'd149_999;
parameter IDLE = 4'd0,
INIT_FUN_SET1 = 4'd1,
INIT_FUN_SET2 = 4'd2,
INIT_DISPLAY = 4'd3,
INIT_CLEAR = 4'd4,
INIT_DOT_SET = 4'd5,
SET_DDRAM = 4'd6,
WRITE_DATA1 = 4'd7,
INIT_FUN_SET3 = 4'd8,
SET_CGRAM = 4'd9,
WRITE_DATA2 = 4'd10,
SET_DDRAM2 = 4'd11,
SET_CUSTOM_L = 4'd12,
SET_CUSTOM_H = 4'd13,
STOP = 4'd14;
/*****
//产生周期为 6MS 的 lcd_clk 给 LCD
reg [17:0] cnt;
reg lcd_clk;
always @(posedge sys_clk or negedge rst_n)
if(!rst_n) begincnt <= 18'd0;
lcd_clk <= 1'b0;
end
else if(cnt == T3MS)begin
cnt <= 18'd0;
lcd_clk <= ~lcd_clk;
end
else
cnt <= cnt + 1'b1;
/*****
reg lcd_rs;
always @(posedge lcd_clk or negedge rst_n)
if(!rst_n)
lcd_rs <= 1'b0;
else if((state == WRITE_DATA1) || (state == WRITE_DATA2)|| (state == SET_CUSTOM_H) || (state == SET_CUSTOM_L))
lcd_rs <= 1'b1; //写数据模式
else
lcd_rs <= 1'b0; //写命令模式
/*****
reg [3:0] state;
reg [7:0] lcd_data;
reg [6:0] num;
reg en;
always @(posedge lcd_clk or negedge rst_n)
if(!rst_n) begin
state <= IDLE;
lcd_data <= 8'hzz;
en <= 1'b1;
num <= 6'd0;
end
else
case(state)
IDLE:begin
state <= INIT_FUN_SET1;
lcd_data <= 8'hzz;
en <= 1'b1;
end
INIT_FUN_SET1:
begin
lcd_data <= 8'h30; //功能设定
state <= INIT_FUN_SET2;
end
INIT_FUN_SET2:
begin
lcd_data <= 8'h30; //功能设定
state <= INIT_DISPLAY;
end
INIT_DISPLAY:
begin
lcd_data <= 8'h0c; //显示设定
state <= INIT_CLEAR;
end
INIT_CLEAR:
begin
lcd_data <= 8'h01; //清屏
state <= INIT_DOT_SET;
end
INIT_DOT_SET:
begin
lcd_data <= 8'h06; //进入点设定
state <= SET_DDRAM;
end
SET_DDRAM:
beginif(num == 7'd0)
lcd_data <= 8'h80;//1 line
else if(num == 7'd16)
lcd_data <= 8'h91;//2 line
else if(num == 7'd28)
lcd_data <= 8'h88;//3 line
else if(num == 7'd42)
lcd_data <= 8'h98;//4 line
state <= WRITE_DATA1;
end
WRITE_DATA1:
begin
if(num <= 7'd15) begin
num <= num + 1'b1;
lcd_data <= dis_data;
if(num == 7'd15)
state <= SET_DDRAM;
else
state <= WRITE_DATA1;
end
else if( num > 7'd15 && num <= 7'd27) begin
num <= num + 1'b1;
lcd_data <= dis_data;
if(num == 7'd27)
state <= SET_DDRAM;
else
state <= WRITE_DATA1;
end
else if(num > 7'd27 && num <= 7'd41) begin
num <= num + 1'b1;
lcd_data <= dis_data;
if(num == 7'd41)
state <= SET_DDRAM;
elsestate <= WRITE_DATA1;
end
else if(num > 7'd41 && num <= 7'd57) begin
num <= num + 1'b1;
lcd_data <= dis_data;
if(num == 7'd57)
state <= INIT_FUN_SET3;
else
state <= WRITE_DATA1;
end
end
INIT_FUN_SET3:
begin
lcd_data <= 8'h30; // 功能设定
state <= SET_CGRAM;
end
SET_CGRAM:
begin
lcd_data <= 8'h40; // 设定 CGRAM 字符的位置
state <= WRITE_DATA2;
end
WRITE_DATA2:
begin
if(num >= 7'd42 && num <= 7'd57) begin
num <= num + 1'b1;
lcd_data <= dis_data;
state <= STOP;
end
else begin
num <= 7'd0;
state <= STOP;
end
end
// SET_DDRAM2:// begin
// lcd_data <= 8'h9e; //4line
// state <= SET_CUSTOM_H; //设置自定义显示字符编码
// end
//
// SET_CUSTOM_H:
// begin
// lcd_data <= 8'h00; //高 8bit
// state <= SET_CUSTOM_L;
// end
//
// SET_CUSTOM_L:
// begin
// lcd_data <= 8'h00; //低 8bit 00:正常 02:星星左上角多了一小点 04:显示的是乱码 06:显示也正常
// state <= STOP;
// end
STOP:
begin
en <= 1'b0; //显示完了,lcd_e 就一直拉为低
state <= STOP;
end
default: state <= IDLE;
endcase
/*****
reg [7:0] dis_data;
always @(posedge sys_clk or negedge rst_n)
if(!rst_n)
dis_data <= 8'hzz;
else
case(num)
//1 Line 80 位置显示 杭州康芯电子公司7'd0 : dis_data <= 8'hBA; //显示第一行 // 杭
7'd1 : dis_data <= 8'hBC;
7'd2 : dis_data <= 8'hD6; //州
7'd3 : dis_data <= 8'hDD;
7'd4 : dis_data <= 8'hBF; //康
7'd5 : dis_data <= 8'hB5;
7'd6 : dis_data <= 8'hD0; // 芯
7'd7 : dis_data <= 8'hBE;
7'd8 : dis_data <= 8'hB5; //电
7'd9 : dis_data <= 8'hE7;
7'd10 : dis_data <= 8'hD7; //子
7'd11 : dis_data <= 8'hD3;
7'd12 : dis_data <= 8'hB9; // 公
7'd13 : dis_data <= 8'hAB;
7'd14 : dis_data <= 8'hCB; //司
7'd15 : dis_data <= 8'hBE;
//2Line 91 位置显示 热情为您服务
7'd16 : dis_data <= 8'hC8; //热
7'd17 : dis_data <= 8'hC8;
7'd18 : dis_data <= 8'hC7; //情
7'd19 : dis_data <= 8'hE9;
7'd20 : dis_data <= 8'hCE; //为
7'd21 : dis_data <= 8'hAA;
7'd22 : dis_data <= 8'hC4; //您
7'd23 : dis_data <= 8'hFA;
7'd24 : dis_data <= 8'hB7; //服
7'd25 : dis_data <= 8'hFE;
7'd26 : dis_data <= 8'hCE; //务7'd27 : dis_data <= 8'hF1; //3Line 88 位置显示 www.kx-so
7'd28 : dis_data <= "w"; 7'd29 : dis_data <= "w"; 7'd30 : dis_data <= "w"; 7'd31 : dis_data <= "."; 7'd32 : dis_data <= "k"; 7'd33 : dis_data <= "x"; 7'd34 : dis_data <= "-""; 7'd35 : dis_data <= "s"; 7'd36 : dis_data <= "o"; 7'd37 : dis_data <= "c"; 7'd38 : dis_data <= "."; 7'd39 : dis_data <= "c"; 7'd40 : dis_data <= "o"; 7'd41 : dis_data <= "m"; //4Line 98 位置显示电话号!
7'd42 : dis_data <= "T"; 7'd43 : dis_data <= "E"; 7'd44 : dis_data <= "L"; 7'd45 : dis_data <= "0"; 7'd46 : dis_data <= "5"; 7'd47 : dis_data <= "7"; 7'd48 : dis_data <= "1"; 7'd49 : dis_data <= "-""; 7'd50 : dis_data <= "8"; 7'd51 : dis_data <= "8"; 7'd52 : dis_data <= "9"; 7'd53 : dis_data <= "1"; 7'd54 : dis_data <= "4"; 7'd55 : dis_data <= "5"; 7'd56 : dis_data <= "8"; 7'd57 : dis_data <= "7"; default: dis_data <= 8'h00;//4Line 99 位置显示 谢谢!
/******************************************************************************************
assign lcd_rw = 1’b0; // 只有写模式
assign lcd_psb = 1’b1; // 并口模式
assign lcd_en = en ? lcd_clk : 1’b0;
/******************************************************************************************
endmodule
代码读法上抓住四个要点:
lcd_clk分频:T3MS = 149_999,即 50MHz 下计满 15 万个周期翻转一次,周期正好 6ms(3ms 高 + 3ms 低),这个慢时钟就是全部操作的节拍;- 状态机主线:IDLE → 功能设定(两次 8’h30,设定为 8 位并口、基本指令集)→ 显示开(8’h0C)→ 清屏(8’h01)→ 进入点设定(8’h06)→ 逐行设置 DDRAM 地址并写数据(SET_DDRAM/WRITE_DATA1 反复切换)→ CGRAM 自定义流程(SET_CGRAM/WRITE_DATA2)→ STOP(拉低 E 停止);
num双重身份:它既是当前写到了第几个字节,也是dis_data查表的索引,还兼职判断「何时该换行设地址」(num==0/16/28/42 时下一状态是 SET_DDRAM 而不是继续写);- 注释掉的自定义字符流程:SET_DDRAM2 → SET_CUSTOM_H(写高 8bit)→ SET_CUSTOM_L(写低 8bit),保留在代码里作为参考,注释中也记录了 0x00/02/04/06 四种编码的实测现象。
三个最容易栽跟头的地方
- V0 对比度脚:直接接 5V 最靠谱,接 3.3V 或悬空可能整屏不显示;
- 自定义字符最后要写两次「数据」:RS 必须为高,写成命令就前功尽弃;
- 清屏和地址归位要给足时间:它们比其他命令慢一个数量级(毫秒级 vs 72us),这就是代码取 6ms 慢时钟的原因。
上板流程
按引脚表把 LCD12864 接入扩展 IO:8 位数据线 DB0~DB7、RS、RW(固定写)、E、PSB(固定并口)依次接好,V0 接 5V,背光 A/K 接 5V/GND。综合工程并下载程序到核心板,即可看到硬件验证效果:


现象与总结
屏幕四行依次显示:
杭州康芯电子公司
热情为您服务
TEL0571-88914587
第一、二行是 GB2312 编码调用 CGROM 字库显示的汉字,第三行是 HCGROM 半宽字型显示的西文网址,第四行是电话号码。由于每个操作间隔 6ms,可以明显看到字符一个接一个「打字机式」地出现——这正是用慢时钟换时序简单的直接体现。
本实验的通用收获有三条:其一,字符型液晶驱动是「查表写编码」的模式,先备好编码表(GB2312 或字模工具生成)再动笔;其二,DDRAM 地址表要对照手册逐行确认,不能想当然线性递增;其三,只写不读时用足够长的操作间隔替代忙检测,是初学者最稳的路线。
自测
自测
- ST7920 的 RS、R/W、E 三根控制线分别起什么作用?本实验中 R/W 和 PSB 固定接什么电平?
- 为什么要用 6ms 周期的慢时钟驱动状态机?不这样做的话标准做法是什么?
- 要在第二行开头显示汉字,应设置哪个 DDRAM 地址?为什么第一行 80H 之后第三行是 88H?
- 自定义 CGRAM 字符写入 DDRAM 时要注意什么?先写高字节还是低字节?
- 屏幕完全不显示(背光正常)时,除了检查程序,还应优先检查哪个硬件引脚?