GT20L16S1Y字库芯片比较常用,现把调试通过的Arduino代码记录一下。以下代码需使用U8G2图形库。
接线方面字库芯片和LCD屏共用SPI接口,以CS引脚区分数据传输。

#include <Arduino.h>
#include <U8g2lib.h>
#include <SPI.h>

// --- 硬件定义 ---
#define PIN_LCD_CS   7
#define PIN_LCD_DC   6
#define PIN_LCD_RST  11
#define PIN_FONT_CS  5

// --- U8g2 初始化 ---
// 你的屏幕构造函数
U8G2_ST7565_NHD_C12864_1_4W_HW_SPI u8g2(U8G2_R0, PIN_LCD_CS, PIN_LCD_DC, PIN_LCD_RST);

// 汉字点阵缓冲区 (16x16点阵 = 32字节)
uint8_t fontBuffer[32];
// 英文字符点阵缓冲8*16
uint8_t asciiBuffer[16];

void setup() {
  // 初始化串口 (调试用)
  Serial.begin(115200);
  // 初始化字库芯片引脚
  pinMode(PIN_FONT_CS, OUTPUT);
  digitalWrite(PIN_FONT_CS, HIGH); // 默认拉高,取消选中
  // 初始化 SPI
  SPI.begin();
  // 初始化屏幕
  u8g2.begin();
}

// 从GT20L16S1Y读取16x16汉字点阵数据
void readFontData(uint8_t msb, uint8_t lsb, uint8_t *buffer) {
  unsigned long address;
  // GT20L16S1Y GB2312 地址计算公式:
  // BaseAddr + ((MSB - 0xB0) * 94 + (LSB - 0xA1)) * 32
  if (msb >= 0xA1 && msb <= 0xA3 && lsb >= 0xA1) {
     address = ((unsigned long)(msb - 0xA1) * 94 + (lsb - 0xA1)) * 32;
  } else if (msb >= 0xB0 && msb <= 0xF7 && lsb >= 0xA1) {
     address = ((unsigned long)(msb - 0xB0) * 94 + (lsb - 0xA1) + 846) * 32;
  } else if (msb == 0xA9 && lsb >= 0xA1) {
     address = (282 + (lsb - 0xA1)) * 32; 
  } else {
     address = 0; 
  }
  // 开始SPI事务读取字库
  SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); // GT20L16S1Y支持高达30MHz+
  digitalWrite(PIN_FONT_CS, LOW);
  // 发送读取命令 0x03
  SPI.transfer(0x03);
  // 发送地址 (24位地址,高位在前)
  SPI.transfer((address >> 16) & 0xFF);
  SPI.transfer((address >> 8) & 0xFF);
  SPI.transfer(address & 0xFF);
  // 读取32个字节的点阵数据
  for (int i = 0; i < 32; i++) {
    buffer[i] = SPI.transfer(0x00);
  }
  digitalWrite(PIN_FONT_CS, HIGH);
  SPI.endTransaction();
}

// 从GT20L16S1Y读取ASCII8x16点阵数据
void readASCIIData(uint8_t asciichar, uint8_t *buffer) {
  unsigned long address = (asciichar-0x20)*16 + 0x3cf80;
  // 开始SPI事务读取字库
  SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); // GT20L16S1Y支持高达30MHz+
  digitalWrite(PIN_FONT_CS, LOW);
  // 发送读取命令 0x03
  SPI.transfer(0x03);
  // 发送地址 (24位地址,高位在前)
  SPI.transfer((address >> 16) & 0xFF);
  SPI.transfer((address >> 8) & 0xFF);
  SPI.transfer(address & 0xFF);
  // 读取32个字节的点阵数据
  for (int i = 0; i < 16; i++) {
    buffer[i] = SPI.transfer(0x00);
  }
  digitalWrite(PIN_FONT_CS, HIGH);
  SPI.endTransaction();
}

// 手动打点绘制汉字
void drawChineseChar(int x, int y, uint8_t msb, uint8_t lsb) {
  readFontData(msb, lsb, fontBuffer);
  for (int i = 0; i < 32; i++) {
    uint8_t byteData = fontBuffer[i];
    for (int bit = 0; bit < 8; bit++) {
       if (byteData & (0x01 << bit)) {
         // 根据具体的排列方式计算 x, y
         u8g2.drawPixel(x + (i % 16) , y + (i / 16) * 8 + bit); 
       }
    }
  }
}

// 手动打点绘制ascii字符
void drawASCIIChar(int x, int y, uint8_t asciichar) {
  readASCIIData(asciichar, asciiBuffer);
  for (int i = 0; i < 16; i++) {
    uint8_t byteData = asciiBuffer[i];
    for (int bit = 0; bit < 8; bit++) {
      if (byteData & (0x01 << bit)) {
        // 根据具体的排列方式计算 x, y
        u8g2.drawPixel(x + (i % 8) , y + (i / 8) * 8 + bit); 
      }
    }
  }
}

void drawExternalFontString(int x, int y, const uint8_t *str) {
  int cursorX = x;
  while (*str) {
    if (*str < 128) {
      // ASCII
      // 英文字体向下压2个像素,与汉字基本取齐。
      drawASCIIChar(cursorX, y + 2 , *str); 
      cursorX += 8; 
      str++;
    } else {
      // 中文
      uint8_t msb = *str;
      uint8_t lsb = *(str + 1);
      if (lsb == 0) break;
      drawChineseChar(cursorX, y, msb, lsb);
      cursorX += 16;
      str += 2;
    }
  }
}

void loop() {
  u8g2.firstPage();
  do {
    // 由于 Arduino IDE 是 UTF-8,我们不能直接写 "你好World"。
    // 我们需要用 16进制 来表示 GB2312 编码。
    const uint8_t textCN[] = {0xB5, 0xB7, 0xB9, 0xC4, 0xD2, 0xD7, 0xD7, 0xE5, 0x00}; //捣鼓易族
    const uint8_t textMix[] = {'H', 'e', 'l', 'l', 'o', 0xCA, 0xC0, 0xBD, 0xE7, 0xA1, 0xA3, 0x00}; //Hello世界
    // 显示
    drawExternalFontString(0, 0, textCN);
    drawExternalFontString(0, 20, textMix);
  } while (u8g2.nextPage());
  delay(1000);
}

效果如下: