12864zw C语言程序如何实现?

99ANYc3cd6
预计阅读时长 41 分钟
位置: 首页 C语言 正文

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

12864zw c语言程序
(图片来源网络,侵删)

由于 串行方式 使用引脚最少,接线简单,是目前最流行和推荐的方式,我将重点介绍 基于 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,通过旋转电位器可以调节屏幕的对比度,直到字符清晰可见。

12864zw c语言程序
(图片来源网络,侵删)

ST7920 控制器核心原理

ST7920 有两种工作模式:并行模式串行模式,我们使用串行模式。

1 串行指令格式

在串行模式下,所有数据和指令都是通过 SID (数据线)SCLK (时钟线) 串行传输的,传输的基本单位是 9 位

  1. 第 1 位 (起始位): 必须为 0
  2. 第 2-5 位 (指令/数据位): 1 表示接下来发送的是 显示数据0 表示接下来发送的是 控制指令
  3. 第 6-9 位: 实际的数据或指令码。
  4. 第 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 关绘图

内存结构:

12864zw c语言程序
(图片来源网络,侵删)
  • 文本显示区: 80H ~ 9FH (共 128 字节),屏幕分为 4 行,每行 16 个字符,每行占用 32 字节。
    • 第1行: 80H ~ 9FH
    • 第2行: 90H ~ 9FH
    • ...以此类推
  • 绘图显示区: 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 代码讲解

  1. 引脚定义: 在程序开头,我们使用 #define 宏定义了连接到 LCD 的三个引脚,方便修改。
  2. LcdWriteByte(): 这是 最核心的函数,它严格按照 ST7920 的 9 位串行协议发送数据或指令。isData 参数决定了第 2-5 位是 1 (数据) 还是 0 (指令)。
  3. LcdCommand()LcdWriteData(): 这两个是封装函数,分别用于发送指令和数据,它们都调用核心的 LcdWriteByte()
  4. LcdInit(): 初始化函数,初始化过程必须严格按照数据手册时序来:
    • 首先进入 基本指令集
    • 设置显示开关(开显示,关光标)。
    • 切换到 扩展指令集
    • 开启 绘图显示 功能(这是画图的前提!)。
    • 设置绘图 RAM 的起始地址。
    • 切换回 基本指令集
    • 最后执行清屏操作。
  5. LcdSetCursor(): 在文本模式下设置光标位置,它通过计算 SET_DDRAM_ADDR 指令的参数来实现。
  6. LcdPrint(): 一个简单的字符串打印函数,循环调用 LcdWriteData() 发送每个字符的 ASCII 码。
  7. LcdDrawPoint(): 画点函数。
    • 它首先需要进入 扩展指令集,因为要操作绘图区的地址。
    • 根据 y 坐标判断是在屏幕上半部分还是下半部分,计算出对应的 RAM 地址。
    • 发送 SET_DDRAM_ADDR 指令来设置地址。
    • 发送 8 位数据,其中只有 1 位是有效的(由 color 决定),其他 7 位为 0。bitWrite(temp, y % 8, color) 这行代码巧妙地将颜色位放置到了正确的字节位置。
    • 操作完成后,切回 基本指令集,以便后续的文本显示。
  8. LcdDrawLine()LcdDrawRect(): 这些是更高阶的图形函数,它们通过调用 LcdDrawPoint() 来实现,这里使用了 Bresenham 直线算法来画线,效率较高。

进阶:移植到其他平台 (如 STM32)

如果你想在 STM32 上使用,核心逻辑是完全一样的,只是底层 I/O 操作需要换成 STM32 的 HAL 库或寄存器操作。

主要改动点:

  1. 引脚定义: 改为 STM32 的 GPIO 引脚号。
  2. pinMode(): 在 STM32 中,你需要使用 HAL_GPIO_Init() 函数来配置 GPIO 为推挽输出模式。
  3. 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,对于精确延时可能需要用 DWTTIM 实现,但对于 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 液晶屏的关键在于:

  1. 正确接线,特别是对比度调节电位器。
  2. 理解 ST7920 的串行通信协议,尤其是 9 位数据格式和指令/数据位的区别。
  3. 掌握基本指令和扩展指令的切换,这是区分文本操作和绘图操作的核心。
  4. 理解内存映射,知道文本和图形数据分别存在哪里,地址如何计算。

希望这份详细的指南能帮助你成功地在 C 语言项目中使用 12864 液晶屏!

-- 展开阅读全文 --
头像
dede如何控制上传图片的大小尺寸?
« 上一篇 今天
dede如何循环调用二级栏目?
下一篇 » 今天

相关文章

取消
微信二维码
支付宝二维码

目录[+]