12864 是一种非常常见的图形点阵液晶模块,通常指的是分辨率为 128x64 像素的屏幕,它有多种控制方式,其中最主流的是 并行 8 位/4 位总线 和 串行(SPI) 方式。

(图片来源网络,侵删)
由于 串行方式 使用引脚最少,接线简单,是目前最流行和推荐的方式,我将重点介绍 基于 ST7920 控制器的串行(SPI)驱动,市面上绝大多数 12864 模块(特别是带中文字库的)都使用 ST7920 芯片。
硬件准备与接线
1 硬件清单
- 主控: Arduino UNO / STM32 / ESP32 / 树莓派 Pico 等(这里以 Arduino 为例)。
- 模块: 12864 液晶屏(ST7920 控制器)。
- 杜邦线: 若干。
2 模块引脚定义
标准的 12864 模块通常有 20 个引脚,但我们只关心以下几个关键引脚:
| 引脚名 | 功能 | 说明 |
|---|---|---|
| VSS | GND (0V) | 电源地 |
| VDD | +5V | 电源正极 (部分模块支持 3.3V) |
| V0 | 对比度调节 | 连接一个 10K 电位器中间脚,用于调节屏幕对比度 |
| RS / CS | 片选/寄存器选择 | 串行模式下为 CS,低电平有效 |
| R/W | 读写选择 | 串行模式下为 SID,数据输入线 |
| E | 使能 | 串行模式下为 SCLK,时钟线 |
| A | 背光正极 | 通常接 5V |
| K | 背光负极 | 接 GND |
3 接线图 (Arduino UNO)
这是最常用的接线方式,采用 串行 3 线 模式。
| 12864 模块引脚 | Arduino UNO 引脚 | 功能 |
|---|---|---|
| VSS | GND | 电源地 |
| VDD | 5V | 电源正极 |
| V0 | 电位器中间脚 | 对比度调节 |
| CS | Pin 10 | 片选 |
| SID | Pin 11 | 数据线 |
| SCLK | Pin 13 | 时钟线 |
| A | 5V | 背光正极 |
| K | GND | 背光负极 |
注意: V0 脚的接法非常重要,你需要一个 10KΩ 的电位器,将其两端分别接到 VDD (5V) 和 VSS (GND),中间的脚接到 V0,通过旋转电位器可以调节屏幕的对比度,直到字符清晰可见。

(图片来源网络,侵删)
ST7920 控制器核心原理
ST7920 有两种工作模式:并行模式 和 串行模式,我们使用串行模式。
1 串行指令格式
在串行模式下,所有数据和指令都是通过 SID (数据线) 和 SCLK (时钟线) 串行传输的,传输的基本单位是 9 位。
- 第 1 位 (起始位): 必须为
0。 - 第 2-5 位 (指令/数据位):
1表示接下来发送的是 显示数据,0表示接下来发送的是 控制指令。 - 第 6-9 位: 实际的数据或指令码。
- 第 9 位后:
SCLK线上需要产生一个额外的上升沿来锁存数据。
2 关键指令列表
我们需要掌握几个核心指令来控制屏幕。
| 指令名称 | 指令码 (二进制) | 说明 |
|---|---|---|
| 清除显示 | 0000 0001 |
清空屏幕,光标回到左上角 |
| 地址归位 | 0000 0010 |
光标/光标地址归位,不擦除内容 |
| 进入基本指令集 | 0000 0010 |
设置为基本指令模式 (如清屏、光标移动等) |
| 进入扩展指令集 | 0000 1110 |
重要! 设置为扩展指令模式 (用于开/关绘图、设置地址等) |
| 开/关整体显示 | 0000 1000 |
D=1 开显示, D=0 关显示 |
| 设定显示地址 | 1XXXXXXX |
扩展指令。XXXXXXX 是要写入的地址 (0x00-0x7F)。 |
| 读取/写入数据 | 1XXXXXXX |
扩展指令。XXXXXXX 是要读/写的地址 (0x80-0xFF)。 |
| 开/关绘图显示 | 0001 0001 |
扩展指令。D1=1 开绘图, D1=0 关绘图 |
内存结构:

(图片来源网络,侵删)
- 文本显示区:
80H~9FH(共 128 字节),屏幕分为 4 行,每行 16 个字符,每行占用 32 字节。- 第1行:
80H~9FH - 第2行:
90H~9FH - ...以此类推
- 第1行:
- 绘图显示区:
80H~9FH(共 128 字节),这 128 字节对应屏幕的 上半部分 (64行中的前32行),每个字节控制 8 个像素点,从上到下,下半部分的绘图区地址是88H~9FH。
C 语言程序实现 (Arduino IDE)
下面是一个完整的 C 语言程序,基于 Arduino 环境,它包含了初始化、显示字符串、画点和画图等基本功能。
1 完整代码
/*
* 12864 (ST7920) 串行驱动程序
* 硬件接线:
* 12864 CS -> Arduino Pin 10
* 12864 SID -> Arduino Pin 11 (MOSI)
* 12864 SCLK-> Arduino Pin 13 (SCK)
*/
// 定义引脚
#define LCD_CS 10
#define LCD_SID 11
#define LCD_SCLK 13
// --- ST7920 指令定义 ---
#define CLEAR_DISPLAY 0x01
#define RETURN_HOME 0x02
#define ENTRY_MODE_SET 0x04
#define DISPLAY_CONTROL 0x08
#define CURSOR_SHIFT 0x10
#define FUNCTION_SET 0x20
#define SET_CGRAM_ADDR 0x40
#define SET_DDRAM_ADDR 0x80
#define EXTENDED_INSTRUCT 0x08 // 用于判断是否是扩展指令
// --- 指令参数 ---
// FUNCTION_SET
#define BIT_8BIT 0x10
#define FONT_5X10 0x04
#define FONT_5X7 0x00
#define REVERSE 0x04
#define NORMAL 0x00
#define BASIC_INSTRUCT 0x00
#define EXTENDED_INSTRUCT 0x04
// DISPLAY_CONTROL
#define DISPLAY_ON 0x04
#define DISPLAY_OFF 0x00
#define CURSOR_ON 0x02
#define CURSOR_OFF 0x00
#define BLINK_ON 0x01
#define BLINK_OFF 0x00
// ENTRY_MODE_SET
#define ENTRY_LEFT 0x02
#define ENTRY_RIGHT 0x00
#define ENTRY_NO_SHIFT 0x00
#define ENTRY_SHIFT 0x01
// --- 函数声明 ---
void LcdWriteByte(bool isData, byte value);
void LcdCommand(byte cmd);
void LcdWriteData(byte dat);
void LcdInit();
void LcdClear();
void LcdSetCursor(byte x, byte y);
void LcdPrint(const char *str);
void LcdDrawPoint(byte x, byte y, byte color);
void LcdDrawLine(byte x1, byte y1, byte x2, byte y2, byte color);
void LcdDrawRect(byte x, byte y, byte width, byte height, byte color);
void LcdFillRect(byte x, byte y, byte width, byte height, byte color);
void setup() {
Serial.begin(9600);
Serial.println("Initializing LCD...");
// 设置引脚为输出模式
pinMode(LCD_CS, OUTPUT);
pinMode(LCD_SID, OUTPUT);
pinMode(LCD_SCLK, OUTPUT);
// 初始化 LCD
LcdInit();
// 显示测试信息
LcdClear();
LcdSetCursor(0, 0);
LcdPrint("Hello, World!");
LcdSetCursor(0, 1);
LcdPrint("ST7920 Test");
LcdSetCursor(0, 2);
LcdPrint("Draw a square:");
// 画一个矩形
LcdDrawRect(10, 30, 100, 30, 1); // (x, y, width, height, color)
Serial.println("LCD Test Complete.");
}
void loop() {
// 主循环中不做任何事
}
/**
* @brief 向 LCD 发送一个字节 (9位)
* @param isData 1=数据, 0=指令
* @param value 要发送的数据或指令
*/
void LcdWriteByte(bool isData, byte value) {
digitalWrite(LCD_CS, LOW); // 拉低片选,开始传输
// 发送起始位 (0)
digitalWrite(LCD_SID, LOW);
digitalWrite(LCD_SCLK, HIGH);
digitalWrite(LCD_SCLK, LOW);
// 发送指令/数据位 (1 for data, 0 for command)
digitalWrite(LCD_SID, isData ? HIGH : LOW);
digitalWrite(LCD_SCLK, HIGH);
digitalWrite(LCD_SCLK, LOW);
// 发送 8 位数据
for (int i = 0; i < 8; i++) {
digitalWrite(LCD_SID, (value >> (7 - i)) & 0x01); // MSB first
digitalWrite(LCD_SCLK, HIGH);
digitalWrite(LCD_SCLK, LOW);
}
// 发送第9位 (锁存脉冲)
digitalWrite(LCD_SID, LOW);
digitalWrite(LCD_SCLK, HIGH);
digitalWrite(LCD_SCLK, LOW);
digitalWrite(LCD_CS, HIGH); // 拉高片选,结束传输
}
/**
* @brief 发送一个指令
*/
void LcdCommand(byte cmd) {
LcdWriteByte(false, cmd);
delayMicroseconds(100); // 指令执行需要时间
}
/**
* @brief 发送一个数据
*/
void LcdWriteData(byte dat) {
LcdWriteByte(true, dat);
delayMicroseconds(50);
}
/**
* @brief 初始化 LCD
*/
void LcdInit() {
delay(50); // 等待 LCD 上电稳定
// 进入基本指令集
LcdCommand(FUNCTION_SET | BIT_8BIT | FONT_5X7 | BASIC_INSTRUCT);
delay(5);
// 显示开关设置
LcdCommand(DISPLAY_CONTROL | DISPLAY_ON | CURSOR_OFF | BLINK_OFF);
delay(5);
// 进入扩展指令集
LcdCommand(FUNCTION_SET | BIT_8BIT | FONT_5X7 | EXTENDED_INSTRUCT);
delay(5);
// 开启绘图显示
LcdCommand(DISPLAY_CONTROL | DISPLAY_ON); // 这里的指令码是 0x09 (0000 1001)
delay(5);
// 设定绘图RAM地址
LcdCommand(SET_DDRAM_ADDR | 0x80); // 从绘图区开始
delay(5);
// 回到基本指令集
LcdCommand(FUNCTION_SET | BIT_8BIT | FONT_5X7 | BASIC_INSTRUCT);
delay(5);
// 清屏
LcdClear();
}
/**
* @brief 清屏
*/
void LcdClear() {
LcdCommand(CLEAR_DISPLAY);
delay(2); // 清屏指令耗时较长
}
/**
* @brief 设置光标位置 (文本模式)
* @param x 列 (0-15)
* @param y 行 (0-3)
*/
void LcdSetCursor(byte x, byte y) {
byte addresses[] = {0x80, 0x90, 0x88, 0x98};
LcdCommand(SET_DDRAM_ADDR | (addresses[y] + x));
}
/**
* @brief 打印字符串
*/
void LcdPrint(const char *str) {
while (*str) {
LcdWriteData(*str++);
}
}
/**
* @brief 画一个点
* @param x 列 (0-127)
* @param y 行 (0-63)
* @param color 1=点亮, 0=熄灭
*/
void LcdDrawPoint(byte x, byte y, byte color) {
if (x > 127 || y > 63) return;
byte addr, temp;
bitWrite(temp, y % 8, color); // 将颜色位放到正确的位置
// 进入扩展指令集
LcdCommand(FUNCTION_SET | BIT_8BIT | FONT_5X7 | EXTENDED_INSTRUCT);
// 设定地址
if (y < 32) {
addr = 0x80 + x; // 上半部分绘图区
} else {
addr = 0x88 + x; // 下半部分绘图区
}
LcdCommand(SET_DDRAM_ADDR | addr);
// 写入数据
LcdWriteData(temp);
// 回到基本指令集
LcdCommand(FUNCTION_SET | BIT_8BIT | FONT_5X7 | BASIC_INSTRUCT);
}
/**
* @brief 画一条线 (使用 Bresenham 算法)
*/
void LcdDrawLine(byte x1, byte y1, byte x2, byte y2, byte color) {
int dx = abs(x2 - x1);
int dy = abs(y2 - y1);
int sx = (x1 < x2) ? 1 : -1;
int sy = (y1 < y2) ? 1 : -1;
int err = dx - dy;
while (1) {
LcdDrawPoint(x1, y1, color);
if (x1 == x2 && y1 == y2) break;
int e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x1 += sx;
}
if (e2 < dx) {
err += dx;
y1 += sy;
}
}
}
/**
* @brief 画一个空心矩形
*/
void LcdDrawRect(byte x, byte y, byte width, byte height, byte color) {
LcdDrawLine(x, y, x + width - 1, y, color);
LcdDrawLine(x + width - 1, y, x + width - 1, y + height - 1, color);
LcdDrawLine(x + width - 1, y + height - 1, x, y + height - 1, color);
LcdDrawLine(x, y + height - 1, x, y, color);
}
/**
* @brief 画一个实心矩形
*/
void LcdFillRect(byte x, byte y, byte width, byte height, byte color) {
for (byte i = y; i < y + height; i++) {
for (byte j = x; j < x + width; j++) {
LcdDrawPoint(j, i, color);
}
}
}
2 代码讲解
- 引脚定义: 在程序开头,我们使用
#define宏定义了连接到 LCD 的三个引脚,方便修改。 LcdWriteByte(): 这是 最核心的函数,它严格按照 ST7920 的 9 位串行协议发送数据或指令。isData参数决定了第 2-5 位是1(数据) 还是0(指令)。LcdCommand()和LcdWriteData(): 这两个是封装函数,分别用于发送指令和数据,它们都调用核心的LcdWriteByte()。LcdInit(): 初始化函数,初始化过程必须严格按照数据手册时序来:- 首先进入 基本指令集。
- 设置显示开关(开显示,关光标)。
- 切换到 扩展指令集。
- 开启 绘图显示 功能(这是画图的前提!)。
- 设置绘图 RAM 的起始地址。
- 切换回 基本指令集。
- 最后执行清屏操作。
LcdSetCursor(): 在文本模式下设置光标位置,它通过计算SET_DDRAM_ADDR指令的参数来实现。LcdPrint(): 一个简单的字符串打印函数,循环调用LcdWriteData()发送每个字符的 ASCII 码。LcdDrawPoint(): 画点函数。- 它首先需要进入 扩展指令集,因为要操作绘图区的地址。
- 根据 y 坐标判断是在屏幕上半部分还是下半部分,计算出对应的 RAM 地址。
- 发送
SET_DDRAM_ADDR指令来设置地址。 - 发送 8 位数据,其中只有 1 位是有效的(由
color决定),其他 7 位为 0。bitWrite(temp, y % 8, color)这行代码巧妙地将颜色位放置到了正确的字节位置。 - 操作完成后,切回 基本指令集,以便后续的文本显示。
LcdDrawLine()和LcdDrawRect(): 这些是更高阶的图形函数,它们通过调用LcdDrawPoint()来实现,这里使用了 Bresenham 直线算法来画线,效率较高。
进阶:移植到其他平台 (如 STM32)
如果你想在 STM32 上使用,核心逻辑是完全一样的,只是底层 I/O 操作需要换成 STM32 的 HAL 库或寄存器操作。
主要改动点:
- 引脚定义: 改为 STM32 的 GPIO 引脚号。
pinMode(): 在 STM32 中,你需要使用HAL_GPIO_Init()函数来配置 GPIO 为推挽输出模式。digitalWrite()和delayMicroseconds(): 这些 Arduino 的函数需要替换为 STM32 的等效函数。digitalWrite(pin, HIGH/LOW)->HAL_GPIO_WritePin(GPIOx, GPIO_Pin, GPIO_Pin_Set/Reset)delayMicroseconds(us)->HAL_Delay(us / 1000)(HAL 库的最小单位是 ms,对于精确延时可能需要用DWT或TIM实现,但对于 LCD 这种不要求纳秒级精度的场景,HAL_Delay通常足够)。
STM32 示例片段 (LcdWriteByte 函数改造):
// 假设你已经定义了 GPIO 句柄
// GPIO_HandleTypeDef hGPIO_lcd;
void LcdWriteByte(bool isData, uint8_t value) {
// CS拉低
HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_RESET);
// 起始位
HAL_GPIO_WritePin(LCD_SID_GPIO_Port, LCD_SID_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(LCD_SCLK_GPIO_Port, LCD_SCLK_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(LCD_SCLK_GPIO_Port, LCD_SCLK_Pin, GPIO_PIN_RESET);
// 指令/数据位
HAL_GPIO_WritePin(LCD_SID_GPIO_Port, LCD_SID_Pin, isData ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(LCD_SCLK_GPIO_Port, LCD_SCLK_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(LCD_SCLK_GPIO_Port, LCD_SCLK_Pin, GPIO_PIN_RESET);
// 8位数据
for (int i = 0; i < 8; i++) {
HAL_GPIO_WritePin(LCD_SID_GPIO_Port, LCD_SID_Pin, (value >> (7 - i)) & 0x01 ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(LCD_SCLK_GPIO_Port, LCD_SCLK_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(LCD_SCLK_GPIO_Port, LCD_SCLK_Pin, GPIO_PIN_RESET);
}
// 锁存脉冲
HAL_GPIO_WritePin(LCD_SID_GPIO_Port, LCD_SID_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(LCD_SCLK_GPIO_Port, LCD_SCLK_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(LCD_SCLK_GPIO_Port, LCD_SCLK_Pin, GPIO_PIN_RESET);
// CS拉高
HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_SET);
}
驱动 12864 液晶屏的关键在于:
- 正确接线,特别是对比度调节电位器。
- 理解 ST7920 的串行通信协议,尤其是 9 位数据格式和指令/数据位的区别。
- 掌握基本指令和扩展指令的切换,这是区分文本操作和绘图操作的核心。
- 理解内存映射,知道文本和图形数据分别存在哪里,地址如何计算。
希望这份详细的指南能帮助你成功地在 C 语言项目中使用 12864 液晶屏!
