ST7302 显示模块调试与验证记录

1. 项目概述

本文档记录 ST7302 显示模块从 ATmega2560 平台迁移到 ESP32-S3 平台后的调试过程、硬件连接、软件配置、显示坐标映射、初始化参数以及最终验证程序。

本次调试的主要目标不是完成完整 UI,而是首先确认以下底层链路能够稳定工作:

  1. ESP32-S3 与 ST7302 的 SPI 通信正常;
  2. ST7302 复位、初始化时序正常;
  3. 显存写入和显示刷新正常;
  4. 屏幕坐标与实际像素位置对应正确;
  5. 能够绘制基础几何图形;
  6. 能够通过 ST7302 对比度相关寄存器进行参数调整;
  7. 建立一份后续开发可以直接复用的基础程序。

最终测试结果:

  • SPI 通信正常;
  • 屏幕初始化正常;
  • 显示刷新正常;
  • 坐标映射正确;
  • 外框、内框、中心横线、中心竖线、中心矩形及角点标记均能够正确显示;
  • 屏幕能够正常显示测试文字;
  • 对比度参数可以动态修改;
  • 当前基础参数下显示效果正常,但不同参数组之间的视觉差异有限,因此最终保留当前基础参数作为后续开发起点。

2. 硬件平台

2.1 主控

本次测试使用:

4D Systems gen4-ESP32 S3 N16R8(arduino板子名字)

主控为 ESP32-S3。
请添加图片描述
同时使用过:

YD-ESP32-S3 2022 V1.3

作为 ESP32-S3 的开发与测试平台。


请添加图片描述

3. ST7302 模块连接方式

本次最终验证使用 ESP32-S3 的硬件 SPI 信号连接 ST7302。

3.1 引脚连接

ST7302 引脚ESP32-S3 GPIO功能
VCC3.3 V电源
GNDGND地
SCL / SCKGPIO12SPI 时钟
SDA / MOSIGPIO11SPI 数据
CSGPIO10片选
DCGPIO9数据/命令选择
RESGPIO8复位

连接关系可以简化表示为:

ESP32-S3                     ST7302
------------------------------------------------
3.3 V       ---------------> VCC
GND         ---------------> GND

GPIO12      ---------------> SCL / SCK
GPIO11      ---------------> SDA / MOSI
GPIO10      ---------------> CS
GPIO9       ---------------> DC
GPIO8       ---------------> RES

3.2 电平说明

之前使用 ATmega2560 进行测试时,主控 GPIO 为 5 V 电平。

本次改用 ESP32-S3 后,直接采用 3.3 V 供电和 3.3 V GPIO 电平。

因此,本次测试不再存在 ATmega2560 的 5 V GPIO 与 ST7302 接口电平之间的兼容性疑问。

实际调试过程中,ESP32-S3 连接 ST7302 后能够正常显示,说明当前硬件连接和逻辑电平配置可以正常工作。


4. ST7302 屏幕基本参数

本次使用的显示区域为:

宽度:122 pixel
高度:250 pixel

程序中的定义:

#define LCD_WIDTH       122
#define LCD_HEIGHT      250

ST7302 的显示数据并不是简单按照:

一个像素 = 一个 bit

的方式直接排列。

本次实际调试中最终确认的数据组织方式为:

  • 水平方向 4 个逻辑像素对应 1 个字节的列数据;
  • 垂直方向 2 个逻辑像素共享 1 个字节;
  • 实际使用的 framebuffer 为:
4125 bytes

对应:

#define BYTES_PER_ROW   33
#define BUFFER_ROWS     125

计算关系:

33 × 125 = 4125 bytes

5. 最关键的坐标映射

本次调试过程中,坐标映射是最重要的调试内容之一。

最初如果直接按照普通单色 LCD 的:

index = y * width / 8 + x / 8

方式组织数据,屏幕容易出现:

  • 雪花;
  • 图形位置错误;
  • 横纵坐标错位;
  • 图形压缩或拉伸;
  • 某些区域无法正确显示。

经过实际图形测试后,最终确认当前 ST7302 模块使用下面的映射方式。

5.1 X 方向偏移

程序中:

#define X_OFFSET 10

实际 X 坐标:

realX = x + X_OFFSET;

5.2 Y 方向映射

byteY = y / 2;

5.3 X 方向映射

byteX = realX / 4;

5.4 一个字节内部的 bit 位置

xInByte = realX % 4;
yInByte = y % 2;

bit = 7 - (xInByte * 2 + yInByte);

最终:

index = byteY * 33 + byteX;

因此一个像素最终对应:

realX = x + X_OFFSET;
byteX = realX / 4;
byteY = y / 2;

xInByte = realX % 4;
yInByte = y % 2;

index = byteY * 33 + byteX;
bit = 7 - (xInByte * 2 + yInByte);

这个映射已经通过实际图形验证,不建议后续开发时随意修改。


6. 为什么必须进行几何图形测试

在开始显示复杂 UI 之前,首先使用简单图形验证坐标系统,是本次调试中比较重要的一步。

最终测试图形包括:

+--------------------------+
|                          |
|      +------------+      |
|      |            |      |
|      |      +     |      |
|      |------|-----|      |
|      |      |     |      |
|      +------------+      |
|                          |
+--------------------------+

实际测试内容包括:

  1. 外框;
  2. 内框;
  3. 中心横线;
  4. 中心竖线;
  5. 屏幕中心矩形;
  6. 四个角附近的标记。

如果这些图形全部位置正确,基本可以确认:

  • X 方向没有整体偏移;
  • Y 方向没有整体偏移;
  • X/Y 没有交换;
  • 字节排列方向正确;
  • bit 排列方向正确;
  • framebuffer 行列关系正确。

本次测试最终确认上述内容均正常。


7. SPI 配置

最终测试使用:

SPI Mode:0
Bit Order:MSB First
SPI Clock:8 MHz

对应程序配置:

SPI.begin(SCLK_PIN, -1, MOSI_PIN, CS_PIN);
SPI.beginTransaction(
    SPISettings(8000000, MSBFIRST, SPI_MODE0)
);

其中:

SCLK = GPIO12
MOSI = GPIO11
CS   = GPIO10

ST7302 本次只使用 MOSI,因此没有使用 MISO。


8. ST7302 初始化过程

最终能够稳定显示的初始化序列中,比较关键的寄存器包括:

D6
D1
C0
C1
C2
C4
C5
D8
B2
B3
B4
62
B7
B0
11
C9
36
3A
B9
B8
2A
2B
35
D0
38
29
20
BB

其中对本次显示效果影响比较明显的是:

C1
C2
C4
C5

这些参数在本次调试中作为对比度相关参数进行实验。


9. 当前基础对比度参数

最终采用的基础参数为:

C1 = 3C 3E 3C 3C
C2 = 23 21 23 23
C4 = 5A 5C 5A 5A
C5 = 37 35 37 37

对应程序:

LCD_WriteCommand(0xC1);
LCD_WriteData(0x3C);
LCD_WriteData(0x3E);
LCD_WriteData(0x3C);
LCD_WriteData(0x3C);

LCD_WriteCommand(0xC2);
LCD_WriteData(0x23);
LCD_WriteData(0x21);
LCD_WriteData(0x23);
LCD_WriteData(0x23);

LCD_WriteCommand(0xC4);
LCD_WriteData(0x5A);
LCD_WriteData(0x5C);
LCD_WriteData(0x5A);
LCD_WriteData(0x5A);

LCD_WriteCommand(0xC5);
LCD_WriteData(0x37);
LCD_WriteData(0x35);
LCD_WriteData(0x37);
LCD_WriteData(0x37);

注意:

上述参数属于本次实际调试使用的工作参数,不应直接理解为 ST7302 所有面板均适用的通用推荐值。更换面板或 ST7302 配置后,应重新依据对应资料确认参数范围。


10. 对比度调试方法

本次对比度调试采用“小范围参数扫描”的方式,而不是一次大幅度修改参数。

测试共设置 9 组。

10.1 Group 1:基础参数

C1 = 3C 3E 3C 3C
C2 = 23 21 23 23
C4 = 5A 5C 5A 5A
C5 = 37 35 37 37

10.2 Group 2:C1 +2

C1 = 3E 40 3E 3E
C2 = 23 21 23 23
C4 = 5A 5C 5A 5A
C5 = 37 35 37 37

10.3 Group 3:C1 -2

C1 = 3A 3C 3A 3A
C2 = 23 21 23 23
C4 = 5A 5C 5A 5A
C5 = 37 35 37 37

10.4 Group 4:C2 +2

C1 = 3C 3E 3C 3C
C2 = 25 23 25 25
C4 = 5A 5C 5A 5A
C5 = 37 35 37 37

10.5 Group 5:C2 -2

C1 = 3C 3E 3C 3C
C2 = 21 1F 21 21
C4 = 5A 5C 5A 5A
C5 = 37 35 37 37

10.6 Group 6:C4 +2

C1 = 3C 3E 3C 3C
C2 = 23 21 23 23
C4 = 5C 5E 5C 5C
C5 = 37 35 37 37

10.7 Group 7:C4 -2

C1 = 3C 3E 3C 3C
C2 = 23 21 23 23
C4 = 58 5A 58 58
C5 = 37 35 37 37

10.8 Group 8:C5 +2

C1 = 3C 3E 3C 3C
C2 = 23 21 23 23
C4 = 5A 5C 5A 5A
C5 = 39 37 39 39

10.9 Group 9:C5 -2

C1 = 3C 3E 3C 3C
C2 = 23 21 23 23
C4 = 5A 5C 5A 5A
C5 = 35 33 35 35

每组显示约:

4 s

如果肉眼比较困难,可以改为:

#define GROUP_TIME_MS 8000

让每组保持 8 s。


11. 对比度观察标准

对比度调试不能简单以“黑得越深越好”作为唯一标准。

建议同时观察:

  1. 黑色像素的深度;
  2. 空白区域是否保持均匀;
  3. 图形边缘是否清晰;
  4. 是否出现残影;
  5. 是否出现局部发灰;
  6. 是否存在闪烁;
  7. 刷新过程中是否出现明显的瞬态变化;
  8. 大面积黑色区域是否均匀。

如果某一参数让黑色明显变深,但同时产生明显残影、闪烁或背景不均匀,则不应仅因为“更黑”而选择该参数。

本次测试最终认为不同参数组之间的差异没有明显到需要进一步大范围扫描,因此以基础参数作为后续程序开发的默认值。


12. 最终测试程序

下面程序用于后续重新验证 ST7302。

程序功能:

  • 初始化 ST7302;
  • 设置对比度;
  • 自动切换 9 组参数;
  • 屏幕显示当前测试组;
  • 屏幕显示 C1/C2/C4/C5 的参数;
  • 绘制测试几何图形;
  • 串口输出当前参数;
  • 每组保持 4 s。

注意:程序中的字体只用于调试显示,不是正式 UI 字库。

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

// =========================================================
// ST7302 GPIO
// =========================================================

#define PIN_SCLK    12
#define PIN_MOSI    11
#define PIN_CS      10
#define PIN_DC       9
#define PIN_RES      8

// =========================================================
// LCD
// =========================================================

#define LCD_WIDTH       122
#define LCD_HEIGHT      250

#define BYTES_PER_ROW   33
#define BUFFER_ROWS     125

#define FRAMEBUFFER_SIZE (BYTES_PER_ROW * BUFFER_ROWS)

#define X_OFFSET         10

uint8_t framebuffer[FRAMEBUFFER_SIZE];

// =========================================================
// Contrast test
// =========================================================

#define GROUP_TIME_MS   4000

struct ContrastGroup
{
    uint8_t c1[4];
    uint8_t c2[4];
    uint8_t c4[4];
    uint8_t c5[4];
};

ContrastGroup groups[] =
{
    // 1 Base
    {
        {0x3C, 0x3E, 0x3C, 0x3C},
        {0x23, 0x21, 0x23, 0x23},
        {0x5A, 0x5C, 0x5A, 0x5A},
        {0x37, 0x35, 0x37, 0x37}
    },

    // 2 C1 +2
    {
        {0x3E, 0x40, 0x3E, 0x3E},
        {0x23, 0x21, 0x23, 0x23},
        {0x5A, 0x5C, 0x5A, 0x5A},
        {0x37, 0x35, 0x37, 0x37}
    },

    // 3 C1 -2
    {
        {0x3A, 0x3C, 0x3A, 0x3A},
        {0x23, 0x21, 0x23, 0x23},
        {0x5A, 0x5C, 0x5A, 0x5A},
        {0x37, 0x35, 0x37, 0x37}
    },

    // 4 C2 +2
    {
        {0x3C, 0x3E, 0x3C, 0x3C},
        {0x25, 0x23, 0x25, 0x25},
        {0x5A, 0x5C, 0x5A, 0x5A},
        {0x37, 0x35, 0x37, 0x37}
    },

    // 5 C2 -2
    {
        {0x3C, 0x3E, 0x3C, 0x3C},
        {0x21, 0x1F, 0x21, 0x21},
        {0x5A, 0x5C, 0x5A, 0x5A},
        {0x37, 0x35, 0x37, 0x37}
    },

    // 6 C4 +2
    {
        {0x3C, 0x3E, 0x3C, 0x3C},
        {0x23, 0x21, 0x23, 0x23},
        {0x5C, 0x5E, 0x5C, 0x5C},
        {0x37, 0x35, 0x37, 0x37}
    },

    // 7 C4 -2
    {
        {0x3C, 0x3E, 0x3C, 0x3C},
        {0x23, 0x21, 0x23, 0x23},
        {0x58, 0x5A, 0x58, 0x58},
        {0x37, 0x35, 0x37, 0x37}
    },

    // 8 C5 +2
    {
        {0x3C, 0x3E, 0x3C, 0x3C},
        {0x23, 0x21, 0x23, 0x23},
        {0x5A, 0x5C, 0x5A, 0x5A},
        {0x39, 0x37, 0x39, 0x39}
    },

    // 9 C5 -2
    {
        {0x3C, 0x3E, 0x3C, 0x3C},
        {0x23, 0x21, 0x23, 0x23},
        {0x5A, 0x5C, 0x5A, 0x5A},
        {0x35, 0x33, 0x35, 0x35}
    }
};

#define GROUP_COUNT (sizeof(groups) / sizeof(groups[0]))

// =========================================================
// SPI / command
// =========================================================

void LCD_Select()
{
    digitalWrite(PIN_CS, LOW);
}

void LCD_Unselect()
{
    digitalWrite(PIN_CS, HIGH);
}

void LCD_CommandMode()
{
    digitalWrite(PIN_DC, LOW);
}

void LCD_DataMode()
{
    digitalWrite(PIN_DC, HIGH);
}

void LCD_WriteCommand(uint8_t cmd)
{
    SPI.beginTransaction(
        SPISettings(8000000, MSBFIRST, SPI_MODE0)
    );

    LCD_Select();
    LCD_CommandMode();

    SPI.transfer(cmd);

    LCD_Unselect();

    SPI.endTransaction();
}

void LCD_WriteData(uint8_t data)
{
    SPI.beginTransaction(
        SPISettings(8000000, MSBFIRST, SPI_MODE0)
    );

    LCD_Select();
    LCD_DataMode();

    SPI.transfer(data);

    LCD_Unselect();

    SPI.endTransaction();
}

void LCD_WriteDataBuffer(const uint8_t *data, size_t len)
{
    SPI.beginTransaction(
        SPISettings(8000000, MSBFIRST, SPI_MODE0)
    );

    LCD_Select();
    LCD_DataMode();

    SPI.writeBytes(data, len);

    LCD_Unselect();

    SPI.endTransaction();
}

// =========================================================
// Contrast
// =========================================================

void LCD_WriteReg4(uint8_t cmd, const uint8_t data[4])
{
    LCD_WriteCommand(cmd);

    for (int i = 0; i < 4; i++)
    {
        LCD_WriteData(data[i]);
    }
}

void LCD_SetContrast(const ContrastGroup &g)
{
    LCD_WriteReg4(0xC1, g.c1);
    LCD_WriteReg4(0xC2, g.c2);
    LCD_WriteReg4(0xC4, g.c4);
    LCD_WriteReg4(0xC5, g.c5);

    LCD_WriteCommand(0x38);
}

// =========================================================
// Reset
// =========================================================

void LCD_Reset()
{
    digitalWrite(PIN_RES, LOW);
    delay(20);

    digitalWrite(PIN_RES, HIGH);
    delay(120);
}

// =========================================================
// ST7302 initialization
// =========================================================

void LCD_Init()
{
    pinMode(PIN_CS, OUTPUT);
    pinMode(PIN_DC, OUTPUT);
    pinMode(PIN_RES, OUTPUT);

    digitalWrite(PIN_CS, HIGH);
    digitalWrite(PIN_DC, HIGH);

    SPI.begin(PIN_SCLK, -1, PIN_MOSI, PIN_CS);

    LCD_Reset();

    LCD_WriteCommand(0xD6);
    LCD_WriteData(0x17);
    LCD_WriteData(0x02);

    LCD_WriteCommand(0xD1);
    LCD_WriteData(0x01);

    LCD_WriteCommand(0xC0);
    LCD_WriteData(0x0E);
    LCD_WriteData(0x05);

    LCD_WriteCommand(0xC1);
    LCD_WriteData(0x3C);
    LCD_WriteData(0x3E);
    LCD_WriteData(0x3C);
    LCD_WriteData(0x3C);

    LCD_WriteCommand(0xC2);
    LCD_WriteData(0x23);
    LCD_WriteData(0x21);
    LCD_WriteData(0x23);
    LCD_WriteData(0x23);

    LCD_WriteCommand(0xC4);
    LCD_WriteData(0x5A);
    LCD_WriteData(0x5C);
    LCD_WriteData(0x5A);
    LCD_WriteData(0x5A);

    LCD_WriteCommand(0xC5);
    LCD_WriteData(0x37);
    LCD_WriteData(0x35);
    LCD_WriteData(0x37);
    LCD_WriteData(0x37);

    LCD_WriteCommand(0xD8);
    LCD_WriteData(0xA6);
    LCD_WriteData(0xE9);

    LCD_WriteCommand(0xB2);
    LCD_WriteData(0x15);

    LCD_WriteCommand(0xB3);

    uint8_t b3[] =
    {
        0xE5, 0xF6, 0x17, 0x77, 0x77,
        0x77, 0x77, 0x77, 0x77, 0x71
    };

    LCD_WriteDataBuffer(b3, sizeof(b3));

    LCD_WriteCommand(0xB4);

    uint8_t b4[] =
    {
        0x05, 0x46, 0x77, 0x77,
        0x77, 0x77, 0x76, 0x45
    };

    LCD_WriteDataBuffer(b4, sizeof(b4));

    LCD_WriteCommand(0x62);
    LCD_WriteData(0x32);
    LCD_WriteData(0x03);
    LCD_WriteData(0x1F);

    LCD_WriteCommand(0xB7);
    LCD_WriteData(0x13);

    LCD_WriteCommand(0xB0);
    LCD_WriteData(0x3F);

    LCD_WriteCommand(0x11);
    delay(120);

    LCD_WriteCommand(0xC9);
    LCD_WriteData(0x00);

    LCD_WriteCommand(0x36);
    LCD_WriteData(0x48);

    LCD_WriteCommand(0x3A);
    LCD_WriteData(0x11);

    LCD_WriteCommand(0xB9);
    LCD_WriteData(0x20);

    LCD_WriteCommand(0xB8);
    LCD_WriteData(0x29);

    LCD_WriteCommand(0x2A);
    LCD_WriteData(0x19);
    LCD_WriteData(0x23);

    LCD_WriteCommand(0x2B);
    LCD_WriteData(0x00);
    LCD_WriteData(0x7C);

    LCD_WriteCommand(0x35);
    LCD_WriteData(0x00);

    LCD_WriteCommand(0xD0);
    LCD_WriteData(0xFF);

    LCD_WriteCommand(0x38);

    LCD_WriteCommand(0x29);

    LCD_WriteCommand(0x20);

    LCD_WriteCommand(0xBB);
    LCD_WriteData(0x4F);

    LCD_WriteCommand(0x38);

    delay(300);

    // Re-apply contrast parameters
    LCD_WriteCommand(0xC1);
    LCD_WriteData(0x3C);
    LCD_WriteData(0x3E);
    LCD_WriteData(0x3C);
    LCD_WriteData(0x3C);

    LCD_WriteCommand(0xC2);
    LCD_WriteData(0x23);
    LCD_WriteData(0x21);
    LCD_WriteData(0x23);
    LCD_WriteData(0x23);

    LCD_WriteCommand(0xC4);
    LCD_WriteData(0x5A);
    LCD_WriteData(0x5C);
    LCD_WriteData(0x5A);
    LCD_WriteData(0x5A);

    LCD_WriteCommand(0xC5);
    LCD_WriteData(0x37);
    LCD_WriteData(0x35);
    LCD_WriteData(0x37);
    LCD_WriteData(0x37);

    LCD_WriteCommand(0xC9);
    LCD_WriteData(0x00);

    LCD_WriteCommand(0x29);

    delay(100);
}

// =========================================================
// Framebuffer
// =========================================================

void LCD_Clear()
{
    memset(framebuffer, 0x00, sizeof(framebuffer));
}

void LCD_SetPixel(int x, int y, bool on)
{
    if (x < 0 || x >= LCD_WIDTH ||
        y < 0 || y >= LCD_HEIGHT)
    {
        return;
    }

    int realX = x + X_OFFSET;

    int byteX = realX / 4;
    int byteY = y / 2;

    if (byteX < 0 || byteX >= BYTES_PER_ROW ||
        byteY < 0 || byteY >= BUFFER_ROWS)
    {
        return;
    }

    int xInByte = realX % 4;
    int yInByte = y % 2;

    int bit = 7 - (xInByte * 2 + yInByte);

    uint8_t mask = 1 << bit;

    int index = byteY * BYTES_PER_ROW + byteX;

    if (on)
        framebuffer[index] |= mask;
    else
        framebuffer[index] &= ~mask;
}

// =========================================================
// Drawing
// =========================================================

void LCD_DrawHLine(int x1, int x2, int y)
{
    if (x1 > x2)
    {
        int t = x1;
        x1 = x2;
        x2 = t;
    }

    for (int x = x1; x <= x2; x++)
    {
        LCD_SetPixel(x, y, true);
    }
}

void LCD_DrawVLine(int x, int y1, int y2)
{
    if (y1 > y2)
    {
        int t = y1;
        y1 = y2;
        y2 = t;
    }

    for (int y = y1; y <= y2; y++)
    {
        LCD_SetPixel(x, y, true);
    }
}

void LCD_DrawRect(int x, int y, int w, int h)
{
    LCD_DrawHLine(x, x + w - 1, y);
    LCD_DrawHLine(x, x + w - 1, y + h - 1);

    LCD_DrawVLine(x, y, y + h - 1);
    LCD_DrawVLine(x + w - 1, y, y + h - 1);
}

void LCD_FillRect(int x, int y, int w, int h)
{
    for (int yy = y; yy < y + h; yy++)
    {
        for (int xx = x; xx < x + w; xx++)
        {
            LCD_SetPixel(xx, yy, true);
        }
    }
}

// =========================================================
// 5x7 font
// =========================================================

const uint8_t font5x7[][5] =
{
    {0x3E,0x51,0x49,0x45,0x3E}, // 0
    {0x00,0x42,0x7F,0x40,0x00}, // 1
    {0x42,0x61,0x51,0x49,0x46}, // 2
    {0x21,0x41,0x45,0x4B,0x31}, // 3
    {0x18,0x14,0x12,0x7F,0x10}, // 4
    {0x27,0x45,0x45,0x45,0x39}, // 5
    {0x3C,0x4A,0x49,0x49,0x30}, // 6
    {0x01,0x71,0x09,0x05,0x03}, // 7
    {0x36,0x49,0x49,0x49,0x36}, // 8
    {0x06,0x49,0x49,0x29,0x1E}, // 9

    {0x7E,0x11,0x11,0x11,0x7E}, // A
    {0x7F,0x49,0x49,0x49,0x36}, // B
    {0x3E,0x41,0x41,0x41,0x22}, // C
    {0x7F,0x41,0x41,0x22,0x1C}, // D
    {0x7F,0x49,0x49,0x49,0x41}, // E
    {0x7F,0x09,0x09,0x09,0x01}  // F
};

int FontIndex(char c)
{
    if (c >= '0' && c <= '9')
        return c - '0';

    if (c >= 'A' && c <= 'F')
        return 10 + (c - 'A');

    return -1;
}

void LCD_DrawChar(int x, int y, char c, int scale)
{
    int index = FontIndex(c);

    if (index < 0)
        return;

    for (int col = 0; col < 5; col++)
    {
        uint8_t bits = font5x7[index][col];

        for (int row = 0; row < 7; row++)
        {
            if (bits & (1 << row))
            {
                for (int dx = 0; dx < scale; dx++)
                {
                    for (int dy = 0; dy < scale; dy++)
                    {
                        LCD_SetPixel(
                            x + col * scale + dx,
                            y + row * scale + dy,
                            true
                        );
                    }
                }
            }
        }
    }
}

void LCD_DrawString(int x, int y, const char *str, int scale)
{
    while (*str)
    {
        LCD_DrawChar(x, y, *str, scale);

        x += 6 * scale;
        str++;
    }
}

// =========================================================
// Hex display
// =========================================================

char HexChar(uint8_t v)
{
    v &= 0x0F;

    if (v < 10)
        return '0' + v;

    return 'A' + (v - 10);
}

void LCD_DrawHexByte(int x, int y, uint8_t value)
{
    char text[3];

    text[0] = HexChar(value >> 4);
    text[1] = HexChar(value);
    text[2] = '\0';

    LCD_DrawString(x, y, text, 1);
}

// =========================================================
// Display framebuffer
// =========================================================

void LCD_Update()
{
    LCD_WriteCommand(0x2A);
    LCD_WriteData(0x19);
    LCD_WriteData(0x23);

    LCD_WriteCommand(0x2B);
    LCD_WriteData(0x00);
    LCD_WriteData(0x7C);

    LCD_WriteCommand(0x2C);

    LCD_WriteDataBuffer(
        framebuffer,
        sizeof(framebuffer)
    );
}

// =========================================================
// Draw contrast test page
// =========================================================

void DrawContrastPage(
    int groupIndex,
    const ContrastGroup &g
)
{
    LCD_Clear();

    // Outer frame
    LCD_DrawRect(
        0,
        0,
        LCD_WIDTH,
        LCD_HEIGHT
    );

    // Inner frame
    LCD_DrawRect(
        20,
        85,
        82,
        80
    );

    // Center horizontal line
    LCD_DrawHLine(
        0,
        LCD_WIDTH - 1,
        LCD_HEIGHT / 2
    );

    // Center vertical line
    LCD_DrawVLine(
        LCD_WIDTH / 2,
        0,
        LCD_HEIGHT - 1
    );

    // Center rectangle
    LCD_FillRect(
        55,
        115,
        12,
        20
    );

    // Corner markers
    LCD_FillRect(4, 4, 5, 5);
    LCD_FillRect(LCD_WIDTH - 9, 4, 5, 5);
    LCD_FillRect(4, LCD_HEIGHT - 9, 5, 5);
    LCD_FillRect(
        LCD_WIDTH - 9,
        LCD_HEIGHT - 9,
        5,
        5
    );

    // Group number
    char groupText[3];

    groupText[0] = '0' + (groupIndex + 1) / 10;
    groupText[1] = '0' + (groupIndex + 1) % 10;
    groupText[2] = '\0';

    LCD_DrawString(
        35,
        15,
        groupText,
        2
    );

    // Register names
    LCD_DrawString(5, 45, "C1", 1);
    LCD_DrawString(5, 55, "C2", 1);
    LCD_DrawString(5, 65, "C4", 1);
    LCD_DrawString(5, 75, "C5", 1);

    // First byte of each register
    LCD_DrawHexByte(25, 45, g.c1[0]);
    LCD_DrawHexByte(25, 55, g.c2[0]);
    LCD_DrawHexByte(25, 65, g.c4[0]);
    LCD_DrawHexByte(25, 75, g.c5[0]);

    LCD_Update();
}

// =========================================================
// Serial print
// =========================================================

void PrintHex4(const uint8_t data[4])
{
    for (int i = 0; i < 4; i++)
    {
        if (data[i] < 0x10)
            Serial.print('0');

        Serial.print(data[i], HEX);

        if (i != 3)
            Serial.print(' ');
    }
}

void PrintContrastGroup(
    int groupIndex,
    const ContrastGroup &g
)
{
    Serial.println();
    Serial.println(
        "======================================"
    );

    Serial.print("Contrast Group: ");
    Serial.println(groupIndex + 1);

    Serial.print("C1: ");
    PrintHex4(g.c1);
    Serial.println();

    Serial.print("C2: ");
    PrintHex4(g.c2);
    Serial.println();

    Serial.print("C4: ");
    PrintHex4(g.c4);
    Serial.println();

    Serial.print("C5: ");
    PrintHex4(g.c5);
    Serial.println();
}

// =========================================================
// Setup
// =========================================================

void setup()
{
    Serial.begin(115200);

    delay(500);

    Serial.println();
    Serial.println(
        "ST7302 Contrast Test"
    );

    LCD_Init();

    delay(200);
}

// =========================================================
// Loop
// =========================================================

void loop()
{
    static int groupIndex = 0;

    const ContrastGroup &g =
        groups[groupIndex];

    PrintContrastGroup(
        groupIndex,
        g
    );

    LCD_SetContrast(g);

    DrawContrastPage(
        groupIndex,
        g
    );

    delay(GROUP_TIME_MS);

    groupIndex++;

    if (groupIndex >= GROUP_COUNT)
    {
        groupIndex = 0;
    }
}

13. 关于最终测试程序的一个注意点

当前程序为了方便肉眼快速识别测试组,屏幕上显示的是:

Group
C1
C2
C4
C5

其中 C1/C2/C4/C5 默认只显示该寄存器的第一个字节。

完整 4 字节参数会通过串口输出。

例如:

======================================
Contrast Group: 1
C1: 3C 3E 3C 3C
C2: 23 21 23 23
C4: 5A 5C 5A 5A
C5: 37 35 37 37

因此如果后续继续进行精细调试,可以直接根据串口日志记录对应组号,而不需要记忆参数。


14. 本次调试过程中的关键问题

14.1 初始显示为“雪花”

现象:

屏幕能够点亮,但显示内容类似雪花或随机噪点。

排查方向:

  1. SPI 模式;
  2. SPI 时钟;
  3. DC;
  4. CS;
  5. RES;
  6. 初始化命令;
  7. 显存数据格式;
  8. 坐标映射;
  9. X/Y 偏移。

最终确认核心问题不是简单的 SPI 通信失败,而是 ST7302 的显存数据组织方式和普通单色 LCD 并不相同。


14.2 图形能够显示,但位置不正确

通过绘制:

  • 横线;
  • 竖线;
  • 矩形;
  • 边框;
  • 角点;

逐步确认坐标映射。

最终得到:

realX = x + X_OFFSET;
byteX = realX / 4;
byteY = y / 2;

xInByte = realX % 4;
yInByte = y % 2;

bit = 7 - (xInByte * 2 + yInByte);

index = byteY * 33 + byteX;

该映射是后续所有 UI 绘制代码的基础。


15. 为什么最终确认显示正确

最终测试页面同时包含:

外框
内框
中心横线
中心竖线
中心矩形
四角标记

用户实际观察结果为:

  • 图形位置正确;
  • 中心横线位于屏幕中心;
  • 中心竖线位于屏幕中心;
  • 中心矩形位于中心;
  • 四角标记位置正确;
  • 没有出现明显整体偏移;
  • 没有出现 X/Y 方向错误。

因此可以认为当前:

硬件连接
   ↓
SPI
   ↓
ST7302 初始化
   ↓
显存写入
   ↓
坐标映射
   ↓
图形绘制
   ↓
显示刷新

整条基础显示链路已经打通。


16. 后续正式开发建议

当前调试程序已经可以作为 ST7302 的基础验证程序保存。

建议后续正式开发时,将程序拆分为:

st7302_driver.h
st7302_driver.cpp
        │
        ├── SPI
        ├── Reset
        ├── Command
        ├── Data
        ├── Init
        ├── SetPixel
        ├── Clear
        ├── Update
        └── 基础绘图

ui_xxx.cpp
        │
        ├── 状态栏
        ├── 参数显示
        ├── 波形
        ├── 图标
        └── 业务界面

不要在正式 UI 程序中继续把初始化命令、坐标映射和业务界面代码全部放在 setup() / loop() 中。

当前验证程序的主要作用是:

确认 ST7302 硬件、SPI、初始化、显存组织和像素坐标系统已经可靠工作。

后续开发的重点应该从“能不能显示”转移到:

显示驱动稳定性
        ↓
刷新效率
        ↓
局部刷新
        ↓
文字/图形绘制
        ↓
波形显示
        ↓
正式 UI

17. 调试结论

本次 ST7302 调试最终完成了从硬件连接到显示验证的完整闭环。

最终确认:

ESP32-S3
   │
   ├── GPIO12 → ST7302 SCL
   ├── GPIO11 → ST7302 SDA/MOSI
   ├── GPIO10 → ST7302 CS
   ├── GPIO9  → ST7302 DC
   ├── GPIO8  → ST7302 RES
   │
   └── 3.3 V / GND
            │
            ▼
          ST7302
            │
            ▼
        122 × 250

最终工作参数:

SPI Clock    = 8 MHz
SPI Mode     = 0
Bit Order    = MSB First

Framebuffer  = 4125 bytes
Bytes/Row    = 33
Buffer Rows  = 125
X Offset     = 10

最终基础对比度参数:

C1 = 3C 3E 3C 3C
C2 = 23 21 23 23
C4 = 5A 5C 5A 5A
C5 = 37 35 37 37

当前状态:

ST7302 已经可以作为 ESP32-S3 平台上的正常显示器件继续进行正式 UI 开发。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐