欢迎光临
我们一直在努力

STM32驱动1.8寸TFT‑LCD(软件SPI)完整讲解

前言

本文基于STM32F103C8T6单片机,使用GPIO模拟SPI时序驱动1.8寸ST7735R彩色TFT‑LCD屏幕,实现文字绘制、基础图形、动态进度条等效果。全程使用软件SPI,不需要硬件SPI外设,引脚选择灵活,适合入门学习彩屏驱动原理。

屏幕硬件参数:物理分辨率128×160,颜色格式RGB565,本文工程默认配置为横屏模式,逻辑分辨率160×128。

视频讲解STM32驱动1.8寸TFT‑LCD(软件SPI)完整讲解https://www.bilibili.com/video/BV1CKYK6uEce/?share_source=copy_web&vd_source=91b967b41a17a5686913521545a8c3fb

一、整体工作流程

STM32不会直接控制液晶面板,而是把“位置、颜色”信息发送给ST7735R显示控制器,由控制器完成显存写入、面板扫描刷新。

整体数据流:

  • STM32准备要显示的内容(像素、文字、图形)
  • LCD驱动设置显示区域与颜色参数
  • PA1~PA5普通GPIO模拟SPI时序输出命令和像素数据
  • ST7735R接收命令与RGB565像素数据
  • 将像素数据写入控制器内部显存GRAM
  • ST7735R持续扫描GRAM,驱动TFT液晶面板输出画面
  • 通俗理解:STM32告诉ST7735R要在哪个坐标显示什么颜色,ST7735R负责把数据存进显存,再不停扫描显存点亮屏幕像素。

    二、硬件接线说明

    硬件连接表

    LCD模块引脚STM32F103C8T6引脚功能说明
    VCC 5V LCD模块供电
    GND GND 电源地,必须和STM32共地
    SCL PA1 软件SPI时钟线
    SDA PA2 软件SPI单向数据线,STM32向LCD发送数据
    RES PA3 LCD硬件复位,低电平有效
    DC PA4 命令/数据选择,低电平=命令,高电平=数据
    CS PA5 LCD片选,低电平有效

    接线关键点说明

  • 为什么一定要共地 SCL、SDA、RES、DC、CS的高低电平判断全部以GND作为参考基准。如果两者不共地,就算信号线接好,LCD也无法正确识别STM32输出高低电平,会出现白屏、花屏、不工作。
  • 供电与逻辑电平 本工程使用的成品LCD模块接5V供电,模块板载稳压电路。STM32的GPIO输出是3.3V逻辑电平,可以正常驱动该模块。

    注意:只有标识支持5V供电的成品模块才可以这样接线,裸屏不要直接接5V,以模块丝印和手册为准。

  • 为什么没有MISO引脚 标准SPI包含MOSI发送、MISO接收。本项目只需要STM32往LCD写命令、写像素,不需要读取LCD内部寄存器和显存,所以只接SDA(等效MOSI)。 优点:节省单片机引脚;缺点:无法通信读取屏幕状态,不能校验写入是否成功。
  • 三、1.8寸TFT‑LCD显示基础原理

    1.8寸指屏幕对角线物理尺寸,屏幕物理像素128列×160行。屏幕横屏使用时程序逻辑分辨率为160×128。

    每一个彩色像素由红、绿、蓝3个子像素构成。

    • TFT即薄膜晶体管,每一个子像素搭配独立晶体管,用来控制液晶单元透光程度;
    • 背光源提供白光;
    • 液晶单元调整光线透过率;
    • 再经过RGB彩色滤光片,混合得到人眼看到的彩色。

    重点:LCD屏幕本身不认识“画线、写汉字、字符串”。所有文字、图形,全部需要拆解成一个个像素点颜色,交给显示控制器,再输出到面板。

    四、ST7735R控制器作用

    ST7735R是LCD模块板载驱动芯片,是STM32单片机和TFT面板中间的桥梁,承担这些工作:

  • 接收STM32下发的命令和像素数据;
  • 保存屏幕方向、颜色格式、显示区域等配置参数;
  • 内部自带显存GRAM,保存每一帧画面所有像素颜色;
  • 按照固定时序读取GRAM,驱动TFT面板完成画面刷新;
  • 完成行列扫描、电源管理、伽马校正。
  • 计算全屏显存大小:128 × 160 × 2字节 = 40960字节,约40KB。 STM32F103C8T6单片机SRAM只有20KB,片内放不下一整幅全屏图像缓存。 所以本驱动不在单片机开辟全屏帧缓存,做法是:先设置LCD内部写入窗口,像素颜色直接源源不断写入ST7735的GRAM。 这和OLED驱动经常在单片机内部开辟全屏缓存有很大区别。

    五、软件SPI通信原理

    PA1‑PA5全部配置为推挽输出模式,程序手动翻转GPIO高低电平模拟SPI通信波形,叫软件SPI,也叫位模拟SPI。

    各信号有效状态

    信号有效状态功能
    CS 低电平 选中LCD模块,允许通信
    DC 低电平 当前发送内容为命令
    DC 高电平 当前发送的是命令参数或者像素数据
    SCL 上升沿 LCD锁存SDA上面的数据位
    SDA 高低电平 传输数据,高位优先发送
    RES 低电平 硬件复位ST7735R控制器

    发送1字节数据逻辑:

  • SCL时钟拉低
  • 根据当前要发送bit设置SDA电平
  • SCL拉高,产生上升沿,LCD锁存这一位数据
  • 数据变量左移,循环8次,完成一个字节发送
  • 发送字节顺序:bit7最高位优先发送,最后发送bit0最低位。

    DC引脚区分命令与数据

    命令和数据共用SDA数据线,依靠DC引脚电平区分:

    • DC=0,CS=0:发送命令字节
    • DC=1,CS=0:发送命令参数、像素颜色数据
    • CS=1:结束本次数据传输

    举例子:设置RGB565颜色格式,先DC拉低发送命令0x3A,再DC拉高发送参数0x05。

    软件SPI vs 硬件SPI对比

    对比项软件SPI硬件SPI
    引脚选择 非常灵活,普通GPIO即可 只能使用芯片指定SPI复用引脚
    学习理解 时序清晰,适合教学学习 需要操作外设寄存器,抽象
    运行速度 速度慢,占用CPU资源 速度快,可以搭配DMA搬运数据
    本工程实现 PA1 PA2模拟SPI 没有使用片上SPI1、SPI2外设

    GPIO配置为GPIO_Speed_50MHz只是设置GPIO最大输出能力,不等于软件SPI时钟能跑到50M,实际通信速度取决于CPU执行循环、翻转IO的速度。

    六、LCD初始化流程

    LCD上电后不能直接显示画面,需要硬件复位 + 向ST7735R发送一整套初始化参数。工程内LCD_Init()函数执行顺序:

  • 将PA1~PA5配置推挽输出,设置总线空闲电平;
  • RES引脚输出低电平完成硬件复位脉冲;
  • 发送软件复位命令SWRESET 0x01;
  • 退出睡眠模式 SLPOUT 0x11;
  • 配置屏幕帧率、像素反转方式;
  • 设置模块电源参数、公共电极电压;
  • MADCTL命令0x36配置屏幕扫描方向(横竖屏);
  • 配置正负伽马曲线;
  • COLMOD命令0x3A+参数0x05设置RGB565颜色模式;
  • NORON 0x13进入正常显示模式;
  • DISPON 0x29开启屏幕显示输出。
  • 复位、退出睡眠之后必须留出足够延时等待。工程通过LCD_SetDelayCallback(delay_ms)绑定系统毫秒延时函数,保证时序满足芯片手册。

    关键命令速查表

    命令命令名称功能描述
    0x01 SWRESET 软件复位控制器
    0x11 SLPOUT 退出睡眠模式
    0x13 NORON 正常显示模式
    0x28 DISPOFF 关闭屏幕显示
    0x29 DISPON 打开屏幕显示
    0x2A CASET 设置列地址(X坐标范围)
    0x2B RASET 设置行地址(Y坐标范围)
    0x2C RAMWR 开始向GRAM写入像素数据
    0x36 MADCTL 屏幕扫描方向、行列交换、RGB/BGR顺序
    0x3A COLMOD 设置像素颜色深度格式

    七、显存窗口与RGB565颜色格式

    1. 显存写入窗口机制

    绘制矩形区域之前,先发送3组命令限定要绘制的矩形范围:

  • CASET(0x2A):设置X轴起始、结束坐标
  • RASET(0x2B):设置Y轴起始、结束坐标
  • RAMWR(0x2C):启动显存写入
  • 注意:结束坐标是包含在绘制区域内。 举例:窗口(10,20)~(19,29) 宽度 =19‑10+1 =10像素 高度 =29‑20+1 =10像素 总像素数量=10×10=100个像素。

    设置完窗口后,连续发送100个RGB565颜色值,ST7735内部写指针会自动递增,不需要每一个像素重复发送坐标命令,极大提升绘图效率。

    2. RGB565颜色编码

    16bit表示一个像素,分配规则:5bit红色,6bit绿色,5bit蓝色。

    人眼对绿色亮度更敏感,所以绿色分配更多位数。

    颜色RGB565十六进制值
    黑色 0x0000
    白色 0xFFFF
    红色 0xF800
    绿色 0x07E0
    蓝色 0x001F
    黄色 0xFFE0
    青色 0x07FF
    品红色 0xF81F

    RGB888(0‑255)转为RGB565公式:

    uint16_t color;
    color = ((red & 0xF8) << 8)
    | ((green & 0xFC) << 3)
    | (blue >> 3);

    发送颜色时,先发送高字节,再发送低字节,符合ST7735R接收要求。

    八、驱动主要API接口说明

    驱动代码存放路径:HARDWARE/LCD

    文件函数名功能
    LCD.c LCD_PortInit() 初始化PA1‑PA5 GPIO,设置总线空闲电平
    LCD.c LCD_HardwareReset() RES引脚硬件复位LCD屏幕
    LCD.c LCD_Init() 完整ST7735R初始化
    LCD.c LCD_SetAddressWindow() 设置显存写入矩形窗口
    LCD.c LCD_DrawPixel() 绘制单个像素点
    LCD.c LCD_Clear() 全屏填充指定颜色
    LCD.c LCD_FillRect() 绘制实心矩形
    LCD.c LCD_DrawLine() Bresenham算法绘制直线
    LCD.c LCD_DrawCircle() 绘制空心圆
    LCD.c LCD_DrawRect() 绘制空心矩形框
    LCD.c LCD_DrawString() 5×7点阵显示ASCII字符串
    LCD.c LCD_DrawUInt() 显示固定宽度无符号数字
    LCD.h 宏定义 屏幕尺寸、屏幕方向、引脚、RGB565颜色宏
    main.c Demo_DrawLayout() 演示界面绘制函数

    补充:LCD本身不自带字符库。显示文字是驱动读取点阵字库,把每个点转换成像素颜色写进屏幕。还支持点阵放大显示。

    实心矩形绘制逻辑:调用LCD_FillRect之后,做参数校验、屏幕边界裁剪,设置CASET+RASET窗口,发送RAMWR命令,循环发送全部像素颜色,ST7735自动写入GRAM。

    九、源代码

    下面包含lcd.c、lcd.h、main.c代码

    工程调用主流程示例

    delay_init(); //系统延时初始化
    LCD_SetDelayCallback(delay_ms);//绑定毫秒延时回调
    LCD_Init(); //LCD屏幕初始化
    Demo_DrawLayout(); //绘制演示界面

    while(1)
    {
    //主循环:动态更新进度条、数字,局部刷新屏幕
    }

    main.c代码

    /*
    * STM32F103C8T6 + ST7735R LCD teaching demonstration
    *
    * LCD SCL -> PA1 LCD SDA -> PA2
    * LCD RES -> PA3 LCD DC -> PA4
    * LCD CS -> PA5
    */
    #include "delay.h"
    #include "LCD.h"

    #define DEMO_BAR_X 8u
    #define DEMO_BAR_Y 72u
    #define DEMO_BAR_WIDTH 144u
    #define DEMO_BAR_HEIGHT 14u
    #define DEMO_STEP_DELAY_MS 50u

    static void Demo_DrawLayout(void);

    int main(void)
    {
    uint16_t progress;
    int16_t direction;

    delay_init();

    /* Use the project's calibrated SysTick delay during LCD reset/start-up. */
    LCD_SetDelayCallback(delay_ms);
    LCD_Init();

    Demo_DrawLayout();

    progress = 0u;
    direction = 1;

    while (1)
    {
    /* Erase the old fill, then draw the new progress value. */
    LCD_FillRect(DEMO_BAR_X + 1u, DEMO_BAR_Y + 1u,
    DEMO_BAR_WIDTH – 2u, DEMO_BAR_HEIGHT – 2u,
    LCD_COLOR_BLACK);
    if (progress != 0u)
    {
    LCD_FillRect(DEMO_BAR_X + 1u, DEMO_BAR_Y + 1u,
    progress, DEMO_BAR_HEIGHT – 2u,
    LCD_COLOR_GREEN);
    }

    LCD_DrawUInt(74u, 94u, progress, 3u,
    LCD_COLOR_YELLOW, LCD_COLOR_BLACK, 16u);

    if (direction > 0)
    {
    if (progress >= DEMO_BAR_WIDTH – 2u)
    {
    direction = -1;
    }
    else
    {
    progress += 2u;
    }
    }
    else
    {
    if (progress == 0u)
    {
    direction = 1;
    }
    else
    {
    progress -= 2u;
    }
    }

    delay_ms(DEMO_STEP_DELAY_MS);
    }
    }

    static void Demo_DrawLayout(void)
    {
    LCD_Clear(LCD_COLOR_BLACK);

    LCD_DrawString(12u, 5u, "STM32 LCD DEMO",
    LCD_COLOR_YELLOW, LCD_COLOR_BLACK, 16u);
    LCD_DrawLine(8u, 23u, 151u, 23u, LCD_COLOR_BLUE);

    LCD_DrawString(8u, 29u, "SCL:PA1 SDA:PA2",
    LCD_COLOR_WHITE, LCD_COLOR_BLACK, 12u);
    LCD_DrawString(8u, 43u, "RES:PA3 DC :PA4",
    LCD_COLOR_WHITE, LCD_COLOR_BLACK, 12u);
    LCD_DrawString(8u, 57u, "CS :PA5 ST7735R",
    LCD_COLOR_CYAN, LCD_COLOR_BLACK, 12u);

    LCD_DrawRect(DEMO_BAR_X, DEMO_BAR_Y,
    DEMO_BAR_WIDTH, DEMO_BAR_HEIGHT, LCD_COLOR_WHITE);

    LCD_DrawString(8u, 94u, "PIXELS:",
    LCD_COLOR_GREEN, LCD_COLOR_BLACK, 16u);
    LCD_DrawString(8u, 112u, "LCD DRIVER RUNNING",
    LCD_COLOR_MAGENTA, LCD_COLOR_BLACK, 12u);
    }

    LCD.c代码

    #include "LCD.H"

    /**
    * @file LCD.c
    * @brief STM32F103C8T6 的 ST7735R 彩色 LCD 软件 SPI 驱动实现。
    *
    * 驱动不使用动态内存,也不依赖操作系统。默认端口适配基于
    * STM32F10x 标准外设库;其他平台可通过 LCD.h 中的宏完成移植。
    * 本文件采用 UTF-8 编码,所有说明性注释均使用中文。
    */

    /* ST7735R 命令。 */
    #define LCD_CMD_SWRESET 0x01u
    #define LCD_CMD_SLPOUT 0x11u
    #define LCD_CMD_NORON 0x13u
    #define LCD_CMD_INVOFF 0x20u
    #define LCD_CMD_DISPOFF 0x28u
    #define LCD_CMD_DISPON 0x29u
    #define LCD_CMD_CASET 0x2Au
    #define LCD_CMD_RASET 0x2Bu
    #define LCD_CMD_RAMWR 0x2Cu
    #define LCD_CMD_MADCTL 0x36u
    #define LCD_CMD_COLMOD 0x3Au

    /* 5×7 可打印 ASCII 字库,每个字节表示一列,最低位位于顶部。 */
    static const uint8_t lcd_font_5x7[96][5] = {
    {0x00,0x00,0x00,0x00,0x00}, {0x00,0x00,0x5F,0x00,0x00},
    {0x00,0x07,0x00,0x07,0x00}, {0x14,0x7F,0x14,0x7F,0x14},
    {0x24,0x2A,0x7F,0x2A,0x12}, {0x23,0x13,0x08,0x64,0x62},
    {0x36,0x49,0x55,0x22,0x50}, {0x00,0x05,0x03,0x00,0x00},
    {0x00,0x1C,0x22,0x41,0x00}, {0x00,0x41,0x22,0x1C,0x00},
    {0x14,0x08,0x3E,0x08,0x14}, {0x08,0x08,0x3E,0x08,0x08},
    {0x00,0x50,0x30,0x00,0x00}, {0x08,0x08,0x08,0x08,0x08},
    {0x00,0x60,0x60,0x00,0x00}, {0x20,0x10,0x08,0x04,0x02},
    {0x3E,0x51,0x49,0x45,0x3E}, {0x00,0x42,0x7F,0x40,0x00},
    {0x42,0x61,0x51,0x49,0x46}, {0x21,0x41,0x45,0x4B,0x31},
    {0x18,0x14,0x12,0x7F,0x10}, {0x27,0x45,0x45,0x45,0x39},
    {0x3C,0x4A,0x49,0x49,0x30}, {0x01,0x71,0x09,0x05,0x03},
    {0x36,0x49,0x49,0x49,0x36}, {0x06,0x49,0x49,0x29,0x1E},
    {0x00,0x36,0x36,0x00,0x00}, {0x00,0x56,0x36,0x00,0x00},
    {0x08,0x14,0x22,0x41,0x00}, {0x14,0x14,0x14,0x14,0x14},
    {0x00,0x41,0x22,0x14,0x08}, {0x02,0x01,0x51,0x09,0x06},
    {0x32,0x49,0x79,0x41,0x3E}, {0x7E,0x11,0x11,0x11,0x7E},
    {0x7F,0x49,0x49,0x49,0x36}, {0x3E,0x41,0x41,0x41,0x22},
    {0x7F,0x41,0x41,0x22,0x1C}, {0x7F,0x49,0x49,0x49,0x41},
    {0x7F,0x09,0x09,0x09,0x01}, {0x3E,0x41,0x49,0x49,0x7A},
    {0x7F,0x08,0x08,0x08,0x7F}, {0x00,0x41,0x7F,0x41,0x00},
    {0x20,0x40,0x41,0x3F,0x01}, {0x7F,0x08,0x14,0x22,0x41},
    {0x7F,0x40,0x40,0x40,0x40}, {0x7F,0x02,0x0C,0x02,0x7F},
    {0x7F,0x04,0x08,0x10,0x7F}, {0x3E,0x41,0x41,0x41,0x3E},
    {0x7F,0x09,0x09,0x09,0x06}, {0x3E,0x41,0x51,0x21,0x5E},
    {0x7F,0x09,0x19,0x29,0x46}, {0x46,0x49,0x49,0x49,0x31},
    {0x01,0x01,0x7F,0x01,0x01}, {0x3F,0x40,0x40,0x40,0x3F},
    {0x1F,0x20,0x40,0x20,0x1F}, {0x3F,0x40,0x38,0x40,0x3F},
    {0x63,0x14,0x08,0x14,0x63}, {0x07,0x08,0x70,0x08,0x07},
    {0x61,0x51,0x49,0x45,0x43}, {0x00,0x7F,0x41,0x41,0x00},
    {0x02,0x04,0x08,0x10,0x20}, {0x00,0x41,0x41,0x7F,0x00},
    {0x04,0x02,0x01,0x02,0x04}, {0x40,0x40,0x40,0x40,0x40},
    {0x00,0x01,0x02,0x04,0x00}, {0x20,0x54,0x54,0x54,0x78},
    {0x7F,0x48,0x44,0x44,0x38}, {0x38,0x44,0x44,0x44,0x20},
    {0x38,0x44,0x44,0x48,0x7F}, {0x38,0x54,0x54,0x54,0x18},
    {0x08,0x7E,0x09,0x01,0x02}, {0x0C,0x52,0x52,0x52,0x3E},
    {0x7F,0x08,0x04,0x04,0x78}, {0x00,0x44,0x7D,0x40,0x00},
    {0x20,0x40,0x44,0x3D,0x00}, {0x7F,0x10,0x28,0x44,0x00},
    {0x00,0x41,0x7F,0x40,0x00}, {0x7C,0x04,0x18,0x04,0x78},
    {0x7C,0x08,0x04,0x04,0x78}, {0x38,0x44,0x44,0x44,0x38},
    {0x7C,0x14,0x14,0x14,0x08}, {0x08,0x14,0x14,0x18,0x7C},
    {0x7C,0x08,0x04,0x04,0x08}, {0x48,0x54,0x54,0x54,0x20},
    {0x04,0x3F,0x44,0x40,0x20}, {0x3C,0x40,0x40,0x20,0x7C},
    {0x1C,0x20,0x40,0x20,0x1C}, {0x3C,0x40,0x30,0x40,0x3C},
    {0x44,0x28,0x10,0x28,0x44}, {0x0C,0x50,0x50,0x50,0x3C},
    {0x44,0x64,0x54,0x4C,0x44}, {0x00,0x08,0x36,0x41,0x00},
    {0x00,0x00,0x7F,0x00,0x00}, {0x00,0x41,0x36,0x08,0x00},
    {0x08,0x04,0x08,0x10,0x08}, {0x00,0x06,0x09,0x09,0x06}
    };

    /* 驱动运行状态与可选的工程延时回调。 */
    static uint8_t lcd_initialized = 0u;
    static LCD_DelayMsCallback lcd_delay_callback = 0;

    /**
    * 内部粗略毫秒延时。
    * 该实现只用于保证驱动复制后可以独立初始化;对时序精度有要求时,
    * 应在 LCD_Init() 前通过 LCD_SetDelayCallback() 传入工程延时函数。
    */
    static void LCD_DefaultDelayMs(uint16_t milliseconds)
    {
    volatile uint32_t count;

    while (milliseconds– != 0u) {
    count = LCD_DELAY_LOOP_COUNT_PER_MS;
    while (count– != 0u) {
    }
    }
    }

    static void LCD_DelayMs(uint16_t milliseconds)
    {
    /* 优先使用用户注册的精确延时,没有注册时才使用内部循环延时。 */
    if (lcd_delay_callback != 0) {
    lcd_delay_callback(milliseconds);
    } else {
    LCD_DefaultDelayMs(milliseconds);
    }
    }

    void LCD_SetDelayCallback(LCD_DelayMsCallback callback)
    {
    /* 保存工程提供的延时函数地址,允许传入空指针恢复默认延时。 */
    lcd_delay_callback = callback;
    }

    /**
    * 根据编译时选择的屏幕方向生成 MADCTL(显存访问控制)寄存器值。
    * 该寄存器决定显存的行列交换方向以及 X、Y 轴的扫描方向。
    */
    static uint8_t LCD_GetMadctl(void)
    {
    #if (LCD_DIRECTION == 0u)
    return 0x00u;
    #elif (LCD_DIRECTION == 1u)
    return 0xC0u;
    #elif (LCD_DIRECTION == 2u)
    return 0x70u;
    #else
    return 0xA0u;
    #endif
    }

    static void LCD_WriteByteRaw(uint8_t data)
    {
    uint8_t bit;

    /*
    * 软件模拟四线 SPI:数据从最高位开始发送。
    * 先拉低 SCL 并准备 SDA,随后在 SCL 上升沿让 LCD 锁存当前数据位。
    */
    for (bit = 0u; bit < 8u; bit++) {
    LCD_SCL_LOW();
    if ((data & 0x80u) != 0u) {
    LCD_SDA_HIGH();
    } else {
    LCD_SDA_LOW();
    }
    LCD_SCL_HIGH();
    data <<= 1;
    }
    LCD_SCL_LOW();
    }

    static void LCD_BeginData(void)
    {
    /* DC=1 表示后续字节为数据,CS=0 选中 LCD。 */
    LCD_DC_HIGH();
    LCD_CS_LOW();
    }

    static void LCD_EndTransfer(void)
    {
    /* 拉高片选线,结束本次命令或数据传输。 */
    LCD_CS_HIGH();
    }

    static void LCD_WriteColorRaw(uint16_t color)
    {
    /* ST7735R 接收 RGB565 时要求先发送高字节,再发送低字节。 */
    LCD_WriteByteRaw((uint8_t)(color >> 8));
    LCD_WriteByteRaw((uint8_t)color);
    }

    static void LCD_WriteColorRepeat(uint16_t color, uint32_t count)
    {
    /* 片选保持有效,连续写入相同颜色,供清屏和矩形填充使用。 */
    LCD_BeginData();
    while (count– != 0u) {
    LCD_WriteColorRaw(color);
    }
    LCD_EndTransfer();
    }

    static void LCD_WriteCommandData(uint8_t command,
    const uint8_t *data,
    uint8_t length)
    {
    uint8_t i;

    /* 先发送一字节命令,再连续发送该命令需要的全部参数。 */
    LCD_WriteCommand(command);
    if ((data == 0) || (length == 0u)) {
    return;
    }

    LCD_BeginData();
    for (i = 0u; i < length; i++) {
    LCD_WriteByteRaw(data[i]);
    }
    LCD_EndTransfer();
    }

    static uint8_t LCD_SetClippedRegion(uint16_t x_start, uint16_t y_start,
    uint16_t x_end, uint16_t y_end)
    {
    uint8_t address[4];
    uint16_t gram_start;
    uint16_t gram_end;

    /* 起点越界或矩形方向错误时拒绝设置,防止写入无效显存地址。 */
    if ((x_start >= LCD_WIDTH) || (y_start >= LCD_HEIGHT) ||
    (x_start > x_end) || (y_start > y_end)) {
    return 0u;
    }

    /* 终点超出屏幕时自动裁剪到最后一个有效像素。 */
    if (x_end >= LCD_WIDTH) {
    x_end = LCD_WIDTH – 1u;
    }
    if (y_end >= LCD_HEIGHT) {
    y_end = LCD_HEIGHT – 1u;
    }

    /* CASET 设置列地址范围,即逻辑坐标的 X 方向。 */
    gram_start = x_start + LCD_X_OFFSET;
    gram_end = x_end + LCD_X_OFFSET;
    address[0] = (uint8_t)(gram_start >> 8);
    address[1] = (uint8_t)gram_start;
    address[2] = (uint8_t)(gram_end >> 8);
    address[3] = (uint8_t)gram_end;
    LCD_WriteCommandData(LCD_CMD_CASET, address, 4u);

    /* RASET 设置行地址范围,即逻辑坐标的 Y 方向。 */
    gram_start = y_start + LCD_Y_OFFSET;
    gram_end = y_end + LCD_Y_OFFSET;
    address[0] = (uint8_t)(gram_start >> 8);
    address[1] = (uint8_t)gram_start;
    address[2] = (uint8_t)(gram_end >> 8);
    address[3] = (uint8_t)gram_end;
    LCD_WriteCommandData(LCD_CMD_RASET, address, 4u);

    /* RAMWR 命令之后即可按设定窗口顺序连续写入像素数据。 */
    LCD_WriteCommand(LCD_CMD_RAMWR);
    return 1u;
    }

    /* —————- GPIO 与底层通信接口 —————- */

    void LCD_PortInit(void)
    {
    #if (LCD_USE_CUSTOM_PORT == 0u)
    GPIO_InitTypeDef gpio;

    /* 打开 GPIOA 时钟,将 PA1~PA5 配置为 50 MHz 推挽输出。 */
    RCC_APB2PeriphClockCmd(LCD_GPIO_CLK, ENABLE);

    gpio.GPIO_Pin = LCD_SCL_PIN | LCD_SDA_PIN | LCD_RST_PIN |
    LCD_DC_PIN | LCD_CS_PIN;
    gpio.GPIO_Speed = GPIO_Speed_50MHz;
    gpio.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_Init(LCD_GPIO_PORT, &gpio);
    #else
    LCD_PORT_INIT();
    #endif

    /*
    * 设置总线空闲状态:CS、DC、RES、SDA 为高电平,SCL 为低电平。
    * 先建立确定的空闲电平,可以避免上电阶段产生误命令。
    */
    LCD_CS_HIGH();
    LCD_DC_HIGH();
    LCD_RST_HIGH();
    LCD_SDA_HIGH();
    LCD_SCL_LOW();
    }

    void LCD_WriteCommand(uint8_t command)
    {
    /* DC=0 表示命令,CS 在单字节命令发送期间保持低电平。 */
    LCD_DC_LOW();
    LCD_CS_LOW();
    LCD_WriteByteRaw(command);
    LCD_CS_HIGH();
    }

    void LCD_WriteData8(uint8_t data)
    {
    /* 发送一个 8 位命令参数或普通数据。 */
    LCD_BeginData();
    LCD_WriteByteRaw(data);
    LCD_EndTransfer();
    }

    void LCD_WriteData16(uint16_t data)
    {
    /* 发送一个 16 位 RGB565 像素。 */
    LCD_BeginData();
    LCD_WriteColorRaw(data);
    LCD_EndTransfer();
    }

    void LCD_WriteRegister(uint8_t command, uint8_t data)
    {
    /* 单参数寄存器写入由一个命令字节和一个数据字节组成。 */
    LCD_WriteCommand(command);
    LCD_WriteData8(data);
    }

    void LCD_HardwareReset(void)
    {
    /* RES 产生低电平复位脉冲,释放复位后等待控制器内部稳定。 */
    LCD_RST_HIGH();
    LCD_DelayMs(5u);
    LCD_RST_LOW();
    LCD_DelayMs(20u);
    LCD_RST_HIGH();
    LCD_DelayMs(120u);
    }

    void LCD_Init(void)
    {
    /* 帧率、供电和伽马曲线参数来自参考模组的 ST7735R 初始化序列。 */
    static const uint8_t frame_rate[] = {0x01u, 0x2Cu, 0x2Du};
    static const uint8_t frame_rate_partial[] = {
    0x01u, 0x2Cu, 0x2Du, 0x01u, 0x2Cu, 0x2Du
    };
    static const uint8_t power_1[] = {0xA2u, 0x02u, 0x84u};
    static const uint8_t power_3[] = {0x0Au, 0x00u};
    static const uint8_t power_4[] = {0x8Au, 0x2Au};
    static const uint8_t power_5[] = {0x8Au, 0xEEu};
    static const uint8_t gamma_positive[] = {
    0x0Fu,0x1Au,0x0Fu,0x18u,0x2Fu,0x28u,0x20u,0x22u,
    0x1Fu,0x1Bu,0x23u,0x37u,0x60u,0x07u,0x02u,0x10u
    };
    static const uint8_t gamma_negative[] = {
    0x0Fu,0x1Bu,0x0Fu,0x17u,0x33u,0x2Cu,0x29u,0x2Eu,
    0x30u,0x30u,0x39u,0x3Fu,0x00u,0x07u,0x03u,0x10u
    };
    uint8_t value;

    /* 第一步:初始化通信引脚并执行硬件复位。 */
    lcd_initialized = 0u;
    LCD_PortInit();
    LCD_HardwareReset();

    /* 第二步:软件复位并退出睡眠,退出睡眠后必须等待电源电路稳定。 */
    LCD_WriteCommand(LCD_CMD_SWRESET);
    LCD_DelayMs(10u);
    LCD_WriteCommand(LCD_CMD_SLPOUT);
    LCD_DelayMs(120u);

    /* 第三步:设置正常、空闲和局部显示模式下的帧率及反转方式。 */
    LCD_WriteCommandData(0xB1u, frame_rate, 3u);
    LCD_WriteCommandData(0xB2u, frame_rate, 3u);
    LCD_WriteCommandData(0xB3u, frame_rate_partial, 6u);
    value = 0x07u;
    LCD_WriteCommandData(0xB4u, &value, 1u);

    /* 第四步:配置内部升压、电源控制和公共电极电压。 */
    LCD_WriteCommandData(0xC0u, power_1, 3u);
    value = 0xC5u;
    LCD_WriteCommandData(0xC1u, &value, 1u);
    LCD_WriteCommandData(0xC2u, power_3, 2u);
    LCD_WriteCommandData(0xC3u, power_4, 2u);
    LCD_WriteCommandData(0xC4u, power_5, 2u);
    value = 0x0Eu;
    LCD_WriteCommandData(0xC5u, &value, 1u);

    /* 第五步:设置显示方向,并装载正、负极性伽马校正参数。 */
    value = LCD_GetMadctl();
    LCD_WriteCommandData(LCD_CMD_MADCTL, &value, 1u);
    LCD_WriteCommandData(0xE0u, gamma_positive, 16u);
    LCD_WriteCommandData(0xE1u, gamma_negative, 16u);

    /* 第六步:选择控制器测试参数,并设置 16 位 RGB565 像素格式。 */
    value = 0x01u;
    LCD_WriteCommandData(0xF0u, &value, 1u);
    value = 0x00u;
    LCD_WriteCommandData(0xF6u, &value, 1u);
    value = 0x05u;
    LCD_WriteCommandData(LCD_CMD_COLMOD, &value, 1u);

    /* 最后进入正常显示模式并开启面板输出。 */
    LCD_WriteCommand(LCD_CMD_INVOFF);
    LCD_WriteCommand(LCD_CMD_NORON);
    LCD_DelayMs(10u);
    LCD_WriteCommand(LCD_CMD_DISPON);
    LCD_DelayMs(20u);
    lcd_initialized = 1u;
    }

    /* —————- 显示控制与基础绘图 —————- */

    void LCD_DisplayOn(void)
    {
    /* 仅开启面板显示,不改变已经写入的显存。 */
    LCD_WriteCommand(LCD_CMD_DISPON);
    }

    void LCD_DisplayOff(void)
    {
    /* 仅关闭面板显示,不清除已经写入的显存。 */
    LCD_WriteCommand(LCD_CMD_DISPOFF);
    }

    void LCD_DrawPixel(uint16_t x, uint16_t y, uint16_t color)
    {
    /* 将写入窗口缩小为一个像素,然后发送该像素的 RGB565 颜色。 */
    if (LCD_SetClippedRegion(x, y, x, y) != 0u) {
    LCD_WriteColorRepeat(color, 1u);
    }
    }

    #if (LCD_ENABLE_LEGACY_API != 0u)

    void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t color)
    {
    LCD_DrawPixel(x, y, color);
    }

    uint16_t LCD_BGR2RGB(uint16_t color)
    {
    return (uint16_t)(((color & 0x001Fu) << 11) |
    (color & 0x07E0u) |
    ((color & 0xF800u) >> 11));
    }

    #endif /* LCD_ENABLE_LEGACY_API */

    void LCD_Clear(uint16_t color)
    {
    /* 清屏等价于用同一种颜色填充整个逻辑显示区域。 */
    LCD_FillRect(0u, 0u, LCD_WIDTH, LCD_HEIGHT, color);
    }

    /**
    * 绘制参考工程使用的低字节在前 RGB565 图片。
    * 发送给 LCD 时交换每个像素的两个字节,使其符合控制器的高字节优先格式。
    */
    void LCD_DrawImageLE(uint16_t x, uint16_t y,
    uint16_t width, uint16_t height,
    const uint8_t *picture)
    {
    uint16_t draw_width;
    uint16_t draw_height;
    uint16_t row;
    uint16_t column;
    uint32_t source_index;

    if ((picture == 0) || (width == 0u) || (height == 0u) ||
    (x >= LCD_WIDTH) || (y >= LCD_HEIGHT)) {
    return;
    }

    draw_width = width;
    draw_height = height;
    if ((uint32_t)x + draw_width > LCD_WIDTH) {
    draw_width = (uint16_t)(LCD_WIDTH – x);
    }
    if ((uint32_t)y + draw_height > LCD_HEIGHT) {
    draw_height = (uint16_t)(LCD_HEIGHT – y);
    }

    if (LCD_SetClippedRegion(x, y,
    (uint16_t)(x + draw_width – 1u),
    (uint16_t)(y + draw_height – 1u)) == 0u) {
    return;
    }

    LCD_BeginData();
    for (row = 0u; row < draw_height; row++) {
    source_index = (uint32_t)row * width * 2u;
    for (column = 0u; column < draw_width; column++) {
    LCD_WriteByteRaw(picture[source_index + 1u]);
    LCD_WriteByteRaw(picture[source_index]);
    source_index += 2u;
    }
    }
    LCD_EndTransfer();
    }

    void LCD_DrawLine(uint16_t x0, uint16_t y0,
    uint16_t x1, uint16_t y1, uint16_t color)
    {
    int32_t x;
    int32_t y;
    int32_t end_x;
    int32_t end_y;
    int32_t dx;
    int32_t dy;
    int32_t step_x;
    int32_t step_y;
    int32_t error;
    int32_t error2;

    /* 将异常端点限制在屏幕范围内,避免无效参数导致长时间空循环。 */
    if (x0 >= LCD_WIDTH) {
    x0 = LCD_WIDTH – 1u;
    }
    if (x1 >= LCD_WIDTH) {
    x1 = LCD_WIDTH – 1u;
    }
    if (y0 >= LCD_HEIGHT) {
    y0 = LCD_HEIGHT – 1u;
    }
    if (y1 >= LCD_HEIGHT) {
    y1 = LCD_HEIGHT – 1u;
    }

    x = x0;
    y = y0;
    end_x = x1;
    end_y = y1;
    dx = (x < end_x) ? (end_x – x) : (x – end_x);
    dy = (y < end_y) ? (end_y – y) : (y – end_y);
    step_x = (x < end_x) ? 1 : -1;
    step_y = (y < end_y) ? 1 : -1;
    error = dx – dy;

    /* Bresenham 整数直线算法,不使用浮点运算,适合单片机执行。 */
    for (;;) {
    if ((x >= 0) && (y >= 0) &&
    (x < (int32_t)LCD_WIDTH) && (y < (int32_t)LCD_HEIGHT)) {
    LCD_DrawPixel((uint16_t)x, (uint16_t)y, color);
    }
    if ((x == end_x) && (y == end_y)) {
    break;
    }
    error2 = error * 2;
    if (error2 > -dy) {
    error -= dy;
    x += step_x;
    }
    if (error2 < dx) {
    error += dx;
    y += step_y;
    }
    }
    }

    void LCD_DrawCircle(uint16_t x, uint16_t y,
    uint16_t radius, uint16_t color)
    {
    int32_t center_x;
    int32_t center_y;
    int32_t offset_x;
    int32_t offset_y;
    int32_t error;
    int32_t saved_error;

    /* 圆心必须在屏幕内,同时限制异常大的半径参数。 */
    if ((x >= LCD_WIDTH) || (y >= LCD_HEIGHT) ||
    (radius > (LCD_WIDTH + LCD_HEIGHT))) {
    return;
    }

    center_x = x;
    center_y = y;
    offset_x = -(int32_t)radius;
    offset_y = 0;
    error = 2 – ((int32_t)radius * 2);

    /* 利用圆的八向对称性,通过整数增量算法绘制圆周。 */
    do {
    if ((center_x – offset_x >= 0) && (center_y + offset_y >= 0)) {
    LCD_DrawPixel((uint16_t)(center_x – offset_x),
    (uint16_t)(center_y + offset_y), color);
    }
    if ((center_x – offset_y >= 0) && (center_y – offset_x >= 0)) {
    LCD_DrawPixel((uint16_t)(center_x – offset_y),
    (uint16_t)(center_y – offset_x), color);
    }
    if ((center_x + offset_x >= 0) && (center_y – offset_y >= 0)) {
    LCD_DrawPixel((uint16_t)(center_x + offset_x),
    (uint16_t)(center_y – offset_y), color);
    }
    if ((center_x + offset_y >= 0) && (center_y + offset_x >= 0)) {
    LCD_DrawPixel((uint16_t)(center_x + offset_y),
    (uint16_t)(center_y + offset_x), color);
    }

    saved_error = error;
    if (saved_error <= offset_y) {
    offset_y++;
    error += offset_y * 2 + 1;
    }
    if ((saved_error > offset_x) || (error > offset_y)) {
    offset_x++;
    error += offset_x * 2 + 1;
    }
    } while (offset_x < 0);
    }

    void LCD_DrawRect(uint16_t x, uint16_t y,
    uint16_t width, uint16_t height, uint16_t color)
    {
    uint32_t x_end;
    uint32_t y_end;

    if ((width == 0u) || (height == 0u) ||
    (x >= LCD_WIDTH) || (y >= LCD_HEIGHT)) {
    return;
    }

    /* 先计算并裁剪右下角,再用四条直线组成矩形边框。 */
    x_end = (uint32_t)x + width – 1u;
    y_end = (uint32_t)y + height – 1u;
    if (x_end >= LCD_WIDTH) {
    x_end = LCD_WIDTH – 1u;
    }
    if (y_end >= LCD_HEIGHT) {
    y_end = LCD_HEIGHT – 1u;
    }

    LCD_DrawLine(x, y, (uint16_t)x_end, y, color);
    LCD_DrawLine(x, y, x, (uint16_t)y_end, color);
    LCD_DrawLine((uint16_t)x_end, y,
    (uint16_t)x_end, (uint16_t)y_end, color);
    LCD_DrawLine(x, (uint16_t)y_end,
    (uint16_t)x_end, (uint16_t)y_end, color);
    }

    #if (LCD_ENABLE_LEGACY_API != 0u)

    /* 绘制具有明暗边缘的旧版按钮边框,用于兼容参考工程界面。 */
    void Gui_box2(uint16_t x, uint16_t y,
    uint16_t width, uint16_t height, uint8_t mode)
    {
    uint16_t top_left;
    uint16_t bottom_right;

    if (mode == 0u) {
    top_left = LCD_COLOR_GRAY0;
    bottom_right = LCD_COLOR_GRAY2;
    } else if (mode == 1u) {
    top_left = LCD_COLOR_GRAY2;
    bottom_right = LCD_COLOR_GRAY0;
    } else {
    top_left = LCD_COLOR_WHITE;
    bottom_right = LCD_COLOR_WHITE;
    }

    LCD_DrawLine(x, y, (uint16_t)(x + width), y, top_left);
    LCD_DrawLine(x, y, x, (uint16_t)(y + height), top_left);
    LCD_DrawLine((uint16_t)(x + width), y,
    (uint16_t)(x + width),
    (uint16_t)(y + height), bottom_right);
    LCD_DrawLine(x, (uint16_t)(y + height),
    (uint16_t)(x + width),
    (uint16_t)(y + height), bottom_right);
    }

    void DisplayButtonDown(uint16_t x1, uint16_t y1,
    uint16_t x2, uint16_t y2)
    {
    /* 左上使用深色、右下使用亮色,形成按钮按下的视觉效果。 */
    LCD_DrawLine(x1, y1, x2, y1, LCD_COLOR_GRAY2);
    LCD_DrawLine(x1, y1, x1, y2, LCD_COLOR_GRAY2);
    LCD_DrawLine(x1, y2, x2, y2, LCD_COLOR_WHITE);
    LCD_DrawLine(x2, y1, x2, y2, LCD_COLOR_WHITE);
    }

    void DisplayButtonUp(uint16_t x1, uint16_t y1,
    uint16_t x2, uint16_t y2)
    {
    /* 左上使用亮色、右下使用深色,形成按钮弹起的视觉效果。 */
    LCD_DrawLine(x1, y1, x2, y1, LCD_COLOR_WHITE);
    LCD_DrawLine(x1, y1, x1, y2, LCD_COLOR_WHITE);
    LCD_DrawLine(x1, y2, x2, y2, LCD_COLOR_GRAY2);
    LCD_DrawLine(x2, y1, x2, y2, LCD_COLOR_GRAY2);
    }

    #endif /* LCD_ENABLE_LEGACY_API */

    /* —————- ASCII 字符与数字显示 —————- */

    void LCD_DrawChar(uint16_t x, uint16_t y, char character,
    uint16_t foreground, uint16_t background,
    uint8_t font_height)
    {
    uint16_t size_x;
    uint16_t draw_width;
    uint16_t draw_height;
    uint16_t pixel_x;
    uint16_t pixel_y;
    uint8_t source_x;
    uint8_t source_y;
    uint8_t font_index;
    uint8_t pixel_on;
    uint8_t character_code;

    /* 字号或起点无效时直接返回,避免后续比例计算发生除零。 */
    if ((font_height < 2u) || (x >= LCD_WIDTH) || (y >= LCD_HEIGHT)) {
    return;
    }
    /* 字库覆盖 ASCII 码 32~127,范围外字符统一显示为问号。 */
    character_code = (uint8_t)character;
    if ((character_code < 32u) || (character_code > 127u)) {
    character_code = (uint8_t)'?';
    }

    /* 字符宽度约为高度的一半,并对超出屏幕的部分进行裁剪。 */
    size_x = (uint16_t)(font_height / 2u);
    draw_width = size_x;
    draw_height = font_height;
    if ((uint32_t)x + draw_width > LCD_WIDTH) {
    draw_width = (uint16_t)(LCD_WIDTH – x);
    }
    if ((uint32_t)y + draw_height > LCD_HEIGHT) {
    draw_height = (uint16_t)(LCD_HEIGHT – y);
    }

    if (LCD_SetClippedRegion(x, y,
    (uint16_t)(x + draw_width – 1u),
    (uint16_t)(y + draw_height – 1u)) == 0u) {
    return;
    }

    /*
    * 将输出区域中的每个像素反向映射到 5×7 点阵。
    * 字库第六列和第八行留作字符间距,使连续文本更加清晰。
    */
    font_index = (uint8_t)(character_code – 32u);
    LCD_BeginData();
    for (pixel_y = 0u; pixel_y < draw_height; pixel_y++) {
    source_y = (uint8_t)(((uint32_t)pixel_y * 8u) / font_height);
    for (pixel_x = 0u; pixel_x < draw_width; pixel_x++) {
    source_x = (uint8_t)(((uint32_t)pixel_x * 6u) / size_x);
    pixel_on = 0u;
    if (source_x < 5u) {
    pixel_on = (uint8_t)(lcd_font_5x7[font_index][source_x] &
    (uint8_t)(1u << source_y));
    }
    LCD_WriteColorRaw((pixel_on != 0u) ? foreground : background);
    }
    }
    LCD_EndTransfer();
    }

    void LCD_DrawString(uint16_t x, uint16_t y, const char *text,
    uint16_t foreground, uint16_t background,
    uint8_t font_height)
    {
    uint16_t origin_x;
    uint16_t size_x;

    if ((text == 0) || (font_height < 2u)) {
    return;
    }

    /* 保存首列坐标,显式换行或自动折行时从该位置继续显示。 */
    origin_x = x;
    size_x = (uint16_t)(font_height / 2u);
    while (*text != '\\0') {
    if (*text == '\\n') {
    x = origin_x;
    y = (uint16_t)(y + font_height);
    } else {
    if ((uint32_t)x + size_x > LCD_WIDTH) {
    x = origin_x;
    y = (uint16_t)(y + font_height);
    }
    if ((uint32_t)y + font_height > LCD_HEIGHT) {
    break;
    }
    LCD_DrawChar(x, y, *text,
    foreground, background, font_height);
    x = (uint16_t)(x + size_x);
    }
    text++;
    }
    }

    void LCD_DrawUInt(uint16_t x, uint16_t y, uint32_t number,
    uint8_t field_width, uint16_t foreground,
    uint16_t background, uint8_t font_height)
    {
    uint8_t buffer[10];
    uint8_t count;
    uint8_t i;
    uint8_t character;
    uint8_t leading;
    uint16_t size_x;

    if ((field_width == 0u) || (font_height < 2u)) {
    return;
    }
    if (field_width > 10u) {
    field_width = 10u;
    }

    /* 从个位开始提取十进制数字并暂存在逆序缓冲区中。 */
    count = 0u;
    do {
    buffer[count++] = (uint8_t)('0' + (number % 10u));
    number /= 10u;
    } while ((number != 0u) && (count < field_width));

    /* 按指定字段宽度从高位到低位输出,未使用的高位以空格填充。 */
    size_x = (uint16_t)(font_height / 2u);
    leading = (uint8_t)(field_width – count);
    for (i = 0u; i < field_width; i++) {
    character = (i < leading) ?
    (uint8_t)' ' : buffer[field_width – i – 1u];
    LCD_DrawChar((uint16_t)(x + ((uint16_t)i * size_x)), y,
    (char)character, foreground, background, font_height);
    }
    }

    /* —————- 状态查询与批量绘图 —————- */

    uint8_t LCD_IsInitialized(void)
    {
    /* 返回软件记录的初始化完成标志,不访问 LCD 硬件。 */
    return lcd_initialized;
    }

    uint16_t LCD_GetWidth(void)
    {
    /* 宽度由编译时选择的显示方向决定。 */
    return LCD_WIDTH;
    }

    uint16_t LCD_GetHeight(void)
    {
    /* 高度由编译时选择的显示方向决定。 */
    return LCD_HEIGHT;
    }

    uint8_t LCD_SetAddressWindow(uint16_t x_start, uint16_t y_start,
    uint16_t x_end, uint16_t y_end)
    {
    return LCD_SetClippedRegion(x_start, y_start, x_end, y_end);
    }

    void LCD_FillRect(uint16_t x, uint16_t y,
    uint16_t width, uint16_t height, uint16_t color)
    {
    uint16_t draw_width;
    uint16_t draw_height;

    if ((width == 0u) || (height == 0u) ||
    (x >= LCD_WIDTH) || (y >= LCD_HEIGHT)) {
    return;
    }

    /* 根据屏幕右边界和下边界裁剪实际填充宽高。 */
    draw_width = width;
    draw_height = height;
    if ((uint32_t)x + draw_width > LCD_WIDTH) {
    draw_width = (uint16_t)(LCD_WIDTH – x);
    }
    if ((uint32_t)y + draw_height > LCD_HEIGHT) {
    draw_height = (uint16_t)(LCD_HEIGHT – y);
    }

    if (LCD_SetClippedRegion(x, y,
    (uint16_t)(x + draw_width – 1u),
    (uint16_t)(y + draw_height – 1u)) != 0u) {
    /* 窗口设置完成后连续写入“实际宽度 × 实际高度”个像素。 */
    LCD_WriteColorRepeat(color,
    (uint32_t)draw_width * draw_height);
    }
    }

    void LCD_DrawImage(uint16_t x, uint16_t y,
    uint16_t width, uint16_t height,
    const uint16_t *pixels)
    {
    uint16_t draw_width;
    uint16_t draw_height;
    uint16_t row;
    uint16_t column;
    uint32_t source_index;

    if ((pixels == 0) || (width == 0u) || (height == 0u) ||
    (x >= LCD_WIDTH) || (y >= LCD_HEIGHT)) {
    return;
    }

    /* 图片可见区域超出屏幕时只绘制位于屏幕内的部分。 */
    draw_width = width;
    draw_height = height;
    if ((uint32_t)x + draw_width > LCD_WIDTH) {
    draw_width = (uint16_t)(LCD_WIDTH – x);
    }
    if ((uint32_t)y + draw_height > LCD_HEIGHT) {
    draw_height = (uint16_t)(LCD_HEIGHT – y);
    }

    if (LCD_SetClippedRegion(x, y,
    (uint16_t)(x + draw_width – 1u),
    (uint16_t)(y + draw_height – 1u)) == 0u) {
    return;
    }

    /* 原始像素数组按从左到右、从上到下的顺序连续存放。 */
    LCD_BeginData();
    for (row = 0u; row < draw_height; row++) {
    source_index = (uint32_t)row * width;
    for (column = 0u; column < draw_width; column++) {
    LCD_WriteColorRaw(pixels[source_index + column]);
    }
    }
    LCD_EndTransfer();
    }

    /* —————- 参考工程旧接口兼容包装 —————- */

    #if (LCD_ENABLE_LEGACY_API != 0u)

    /*
    * 以下函数只负责把旧接口转发到当前驱动实现,便于已有示例代码直接迁移。
    * 新项目建议调用 LCD_Init、LCD_Clear、LCD_DrawPixel 等统一命名的接口。
    */
    void LCD_GPIO_Init(void)
    {
    LCD_PortInit();
    }

    void SPI_WriteData(uint8_t data)
    {
    LCD_WriteByteRaw(data);
    }

    void Lcd_WriteIndex(uint8_t index)
    {
    LCD_WriteCommand(index);
    }

    void Lcd_WriteData(uint8_t data)
    {
    LCD_WriteData8(data);
    }

    void LCD_WriteData_16Bit(uint16_t data)
    {
    LCD_WriteData16(data);
    }

    void Lcd_WriteReg(uint8_t index, uint8_t data)
    {
    LCD_WriteRegister(index, data);
    }

    void Lcd_Reset(void)
    {
    LCD_HardwareReset();
    }

    void Lcd_Init(void)
    {
    LCD_Init();
    }

    void Lcd_SetRegion(uint16_t x_start, uint16_t y_start,
    uint16_t x_end, uint16_t y_end)
    {
    (void)LCD_SetAddressWindow(x_start, y_start, x_end, y_end);
    }

    void LCD_SetRegion(uint16_t x_start, uint16_t y_start,
    uint16_t x_end, uint16_t y_end)
    {
    (void)LCD_SetAddressWindow(x_start, y_start, x_end, y_end);
    }

    void Lcd_SetXY(uint16_t x, uint16_t y)
    {
    (void)LCD_SetAddressWindow(x, y, x, y);
    }

    void Gui_DrawPoint(uint16_t x, uint16_t y, uint16_t color)
    {
    LCD_DrawPixel(x, y, color);
    }

    void Lcd_Clear(uint16_t color)
    {
    LCD_Clear(color);
    }

    void LCD_Fill(uint16_t x_start, uint16_t y_start,
    uint16_t x_end, uint16_t y_end, uint16_t color)
    {
    uint16_t width;
    uint16_t height;

    if ((x_start >= LCD_WIDTH) || (y_start >= LCD_HEIGHT) ||
    (x_start > x_end) || (y_start > y_end)) {
    return;
    }

    if (x_end >= LCD_WIDTH) {
    x_end = LCD_WIDTH – 1u;
    }
    if (y_end >= LCD_HEIGHT) {
    y_end = LCD_HEIGHT – 1u;
    }

    width = (uint16_t)((uint32_t)x_end – x_start + 1u);
    height = (uint16_t)((uint32_t)y_end – y_start + 1u);
    LCD_FillRect(x_start, y_start, width, height, color);
    }

    void LCD_ShowPicture(uint16_t x, uint16_t y,
    uint16_t width, uint16_t height,
    const uint8_t *picture)
    {
    LCD_DrawImageLE(x, y, width, height, picture);
    }

    void Gui_Circle(uint16_t x, uint16_t y,
    uint16_t radius, uint16_t color)
    {
    LCD_DrawCircle(x, y, radius, color);
    }

    void Gui_DrawLine(uint16_t x0, uint16_t y0,
    uint16_t x1, uint16_t y1, uint16_t color)
    {
    LCD_DrawLine(x0, y0, x1, y1, color);
    }

    void Gui_box(uint16_t x, uint16_t y,
    uint16_t width, uint16_t height, uint16_t color)
    {
    LCD_DrawRect(x, y, width, height, color);
    }

    void LCD_ShowChar(uint16_t x, uint16_t y, uint8_t character,
    uint16_t foreground, uint16_t background,
    uint8_t size_y)
    {
    LCD_DrawChar(x, y, (char)character,
    foreground, background, size_y);
    }

    void LCD_ShowString(uint16_t x, uint16_t y, const uint8_t *text,
    uint16_t foreground, uint16_t background,
    uint8_t size_y)
    {
    LCD_DrawString(x, y, (const char *)text,
    foreground, background, size_y);
    }

    void LCD_ShowNum(uint16_t x, uint16_t y, uint32_t number,
    uint8_t digits, uint16_t foreground,
    uint16_t background, uint8_t size_y)
    {
    LCD_DrawUInt(x, y, number, digits,
    foreground, background, size_y);
    }

    #endif /* LCD_ENABLE_LEGACY_API */

    LCD.h代码

    #ifndef LCD_H
    #define LCD_H

    /**
    * @file LCD.h
    * @brief STM32F103C8T6 的 ST7735R 彩色 LCD 软件 SPI 驱动接口。
    *
    * 默认硬件连接(STM32F103C8T6):
    * – LCD SCL -> PA1
    * – LCD SDA -> PA2
    * – LCD RES -> PA3
    * – LCD DC -> PA4
    * – LCD CS -> PA5
    *
    * 移植方法:
    * 1. STM32F10x 标准外设库工程可直接复制 LCD.C 和 LCD.H。
    * 2. 其他平台将 LCD_USE_CUSTOM_PORT 设为 1,并实现下方列出的 GPIO 宏。
    * 3. 屏幕初始化前可调用 LCD_SetDelayCallback() 接入工程自身的毫秒延时。
    * 4. 屏幕出现整体偏移时,修改 LCD_X_OFFSET 和 LCD_Y_OFFSET。
    *
    * 坐标原点位于屏幕左上角,X 轴向右,Y 轴向下。颜色统一使用
    * RGB565 格式。当前接线没有 MISO,因此驱动只提供写操作,不读取显存。
    *
    * 本文件采用 UTF-8 编码,所有说明性注释均使用中文。
    */

    #include <stdint.h>

    /* 可选配置文件应只定义配置宏,不应包含驱动实现。 */
    #ifdef LCD_CONFIG_HEADER
    #include LCD_CONFIG_HEADER
    #endif

    #define LCD_DRIVER_VERSION_MAJOR 1u
    #define LCD_DRIVER_VERSION_MINOR 1u
    #define LCD_DRIVER_VERSION_PATCH 0u

    /* —————- 屏幕方向与尺寸参数 —————- */

    #define LCD_DIRECTION_PORTRAIT_0 0u
    #define LCD_DIRECTION_PORTRAIT_180 1u
    #define LCD_DIRECTION_LANDSCAPE_0 2u
    #define LCD_DIRECTION_LANDSCAPE_180 3u

    /** 屏幕默认采用横屏 180 度方向,对应逻辑分辨率 160×128。 */
    #ifndef LCD_DIRECTION
    #ifdef USE_HORIZONTAL
    #define LCD_DIRECTION USE_HORIZONTAL
    #else
    #define LCD_DIRECTION LCD_DIRECTION_LANDSCAPE_180
    #endif
    #endif

    #if (LCD_DIRECTION > LCD_DIRECTION_LANDSCAPE_180)
    #error "LCD_DIRECTION must be in the range 0..3."
    #endif

    #if ((LCD_DIRECTION == LCD_DIRECTION_PORTRAIT_0) || \\
    (LCD_DIRECTION == LCD_DIRECTION_PORTRAIT_180))
    #define LCD_WIDTH 128u
    #define LCD_HEIGHT 160u
    #else
    #define LCD_WIDTH 160u
    #define LCD_HEIGHT 128u
    #endif

    /**
    * 显存坐标偏移量。
    * 不同尺寸或批次的 ST7735R 模组可能存在 1~3 个像素的起点偏移,
    * 如画面整体偏移,可通过工程宏定义覆盖这两个默认值。
    */
    #ifndef LCD_X_OFFSET
    #define LCD_X_OFFSET 0u
    #endif
    #ifndef LCD_Y_OFFSET
    #define LCD_Y_OFFSET 0u
    #endif

    /**
    * 内部粗略毫秒延时的循环次数。
    * 本工程已经通过 LCD_SetDelayCallback() 接入 delay_ms(),此值仅作为
    * 驱动独立使用时的后备延时参数。
    */
    #ifndef LCD_DELAY_LOOP_COUNT_PER_MS
    #define LCD_DELAY_LOOP_COUNT_PER_MS 18000u
    #endif

    #if (LCD_DELAY_LOOP_COUNT_PER_MS == 0u)
    #error "LCD_DELAY_LOOP_COUNT_PER_MS must be greater than 0."
    #endif

    /** 默认保留参考代码中的旧接口名称,设为 0 可隐藏旧接口声明。 */
    #ifndef LCD_ENABLE_LEGACY_API
    #define LCD_ENABLE_LEGACY_API 1u
    #endif

    #if ((LCD_ENABLE_LEGACY_API != 0u) && (LCD_ENABLE_LEGACY_API != 1u))
    #error "LCD_ENABLE_LEGACY_API must be 0 or 1."
    #endif

    /* —————- 常用 RGB565 颜色值 —————- */

    #define LCD_COLOR_BLACK 0x0000u
    #define LCD_COLOR_NAVY 0x000Fu
    #define LCD_COLOR_BLUE 0x001Fu
    #define LCD_COLOR_BLUE2 0x022Fu
    #define LCD_COLOR_DARK_GREEN 0x0320u
    #define LCD_COLOR_GREEN 0x07E0u
    #define LCD_COLOR_CYAN 0x07FFu
    #define LCD_COLOR_RED 0xF800u
    #define LCD_COLOR_MAGENTA 0xF81Fu
    #define LCD_COLOR_CARMINE 0xE00Bu
    #define LCD_COLOR_VIOLETRED 0xC0B0u
    #define LCD_COLOR_ORANGE 0xFA20u
    #define LCD_COLOR_YELLOW 0xFFE0u
    #define LCD_COLOR_WHITE 0xFFFFu
    #define LCD_COLOR_GRAY0 0xEF7Du
    #define LCD_COLOR_GRAY1 0x8410u
    #define LCD_COLOR_GRAY2 0x4208u

    /* —————- GPIO 引脚及平台适配 —————- */

    #ifndef LCD_USE_CUSTOM_PORT
    #define LCD_USE_CUSTOM_PORT 0u
    #endif

    #if ((LCD_USE_CUSTOM_PORT != 0u) && (LCD_USE_CUSTOM_PORT != 1u))
    #error "LCD_USE_CUSTOM_PORT must be 0 or 1."
    #endif

    #if (LCD_USE_CUSTOM_PORT == 0u)

    #include "stm32f10x.h"

    #ifndef LCD_GPIO_PORT
    /** 五根控制线均连接到 GPIOA。 */
    #define LCD_GPIO_PORT GPIOA
    #endif
    #ifndef LCD_GPIO_CLK
    #define LCD_GPIO_CLK RCC_APB2Periph_GPIOA
    #endif
    #ifndef LCD_SCL_PIN
    /** 软件 SPI 时钟线:LCD SCL -> PA1。 */
    #define LCD_SCL_PIN GPIO_Pin_1
    #endif
    #ifndef LCD_SDA_PIN
    /** 软件 SPI 数据线:LCD SDA -> PA2。 */
    #define LCD_SDA_PIN GPIO_Pin_2
    #endif
    #ifndef LCD_RST_PIN
    /** LCD 硬件复位线:LCD RES -> PA3。 */
    #define LCD_RST_PIN GPIO_Pin_3
    #endif
    #ifndef LCD_DC_PIN
    /** 命令/数据选择线:LCD DC -> PA4。低电平为命令,高电平为数据。 */
    #define LCD_DC_PIN GPIO_Pin_4
    #endif
    #ifndef LCD_CS_PIN
    /** LCD 片选线:LCD CS -> PA5,低电平有效。 */
    #define LCD_CS_PIN GPIO_Pin_5
    #endif

    /* 使用置位/复位寄存器直接改变引脚电平,以缩短软件 SPI 的发送时间。 */
    #define LCD_SCL_HIGH() (LCD_GPIO_PORT->BSRR = LCD_SCL_PIN)
    #define LCD_SCL_LOW() (LCD_GPIO_PORT->BRR = LCD_SCL_PIN)
    #define LCD_SDA_HIGH() (LCD_GPIO_PORT->BSRR = LCD_SDA_PIN)
    #define LCD_SDA_LOW() (LCD_GPIO_PORT->BRR = LCD_SDA_PIN)
    #define LCD_RST_HIGH() (LCD_GPIO_PORT->BSRR = LCD_RST_PIN)
    #define LCD_RST_LOW() (LCD_GPIO_PORT->BRR = LCD_RST_PIN)
    #define LCD_DC_HIGH() (LCD_GPIO_PORT->BSRR = LCD_DC_PIN)
    #define LCD_DC_LOW() (LCD_GPIO_PORT->BRR = LCD_DC_PIN)
    #define LCD_CS_HIGH() (LCD_GPIO_PORT->BSRR = LCD_CS_PIN)
    #define LCD_CS_LOW() (LCD_GPIO_PORT->BRR = LCD_CS_PIN)

    #else

    /* 自定义平台必须提供端口初始化及 10 个电平操作宏。 */
    #ifndef LCD_PORT_INIT
    #error "LCD_PORT_INIT() must be defined when LCD_USE_CUSTOM_PORT is 1."
    #endif
    #ifndef LCD_SCL_HIGH
    #error "LCD_SCL_HIGH() must be defined when LCD_USE_CUSTOM_PORT is 1."
    #endif
    #ifndef LCD_SCL_LOW
    #error "LCD_SCL_LOW() must be defined when LCD_USE_CUSTOM_PORT is 1."
    #endif
    #ifndef LCD_SDA_HIGH
    #error "LCD_SDA_HIGH() must be defined when LCD_USE_CUSTOM_PORT is 1."
    #endif
    #ifndef LCD_SDA_LOW
    #error "LCD_SDA_LOW() must be defined when LCD_USE_CUSTOM_PORT is 1."
    #endif
    #ifndef LCD_RST_HIGH
    #error "LCD_RST_HIGH() must be defined when LCD_USE_CUSTOM_PORT is 1."
    #endif
    #ifndef LCD_RST_LOW
    #error "LCD_RST_LOW() must be defined when LCD_USE_CUSTOM_PORT is 1."
    #endif
    #ifndef LCD_DC_HIGH
    #error "LCD_DC_HIGH() must be defined when LCD_USE_CUSTOM_PORT is 1."
    #endif
    #ifndef LCD_DC_LOW
    #error "LCD_DC_LOW() must be defined when LCD_USE_CUSTOM_PORT is 1."
    #endif
    #ifndef LCD_CS_HIGH
    #error "LCD_CS_HIGH() must be defined when LCD_USE_CUSTOM_PORT is 1."
    #endif
    #ifndef LCD_CS_LOW
    #error "LCD_CS_LOW() must be defined when LCD_USE_CUSTOM_PORT is 1."
    #endif

    #endif /* LCD_USE_CUSTOM_PORT */

    #ifdef __cplusplus
    extern "C" {
    #endif

    /** 毫秒延时函数类型,参数表示需要延时的毫秒数。 */
    typedef void (*LCD_DelayMsCallback)(uint16_t milliseconds);

    /* —————- 初始化与底层写接口 —————- */

    /** 设置精确毫秒延时回调;传入空指针时恢复内部粗略延时。 */
    void LCD_SetDelayCallback(LCD_DelayMsCallback callback);

    /** 初始化 LCD 所用 GPIO,并设置总线空闲电平。 */
    void LCD_PortInit(void);

    /** 写入一个 ST7735R 命令。 */
    void LCD_WriteCommand(uint8_t command);

    /** 写入一个 8 位参数。 */
    void LCD_WriteData8(uint8_t data);

    /** 按高字节在前的顺序写入一个 RGB565 像素。 */
    void LCD_WriteData16(uint16_t data);

    /** 写入单字节寄存器。 */
    void LCD_WriteRegister(uint8_t command, uint8_t data);

    /** 执行 LCD 硬件复位。 */
    void LCD_HardwareReset(void);

    /** 完成 GPIO、硬件复位和 ST7735R 寄存器初始化;允许重复调用。 */
    void LCD_Init(void);

    /** 查询初始化状态:已完成初始化返回 1,否则返回 0。 */
    uint8_t LCD_IsInitialized(void);
    /** 获取当前显示方向下的逻辑宽度,单位为像素。 */
    uint16_t LCD_GetWidth(void);
    /** 获取当前显示方向下的逻辑高度,单位为像素。 */
    uint16_t LCD_GetHeight(void);
    /** 开启屏幕显示,显存内容保持不变。 */
    void LCD_DisplayOn(void);
    /** 关闭屏幕显示,显存内容保持不变。 */
    void LCD_DisplayOff(void);

    /* —————- 基础绘图接口 —————- */

    /** 设置包含终点在内的显存窗口,成功返回 1,参数无效返回 0。 */
    uint8_t LCD_SetAddressWindow(uint16_t x_start, uint16_t y_start,
    uint16_t x_end, uint16_t y_end);

    /** 在指定坐标绘制一个 RGB565 颜色像素,越界坐标将被忽略。 */
    void LCD_DrawPixel(uint16_t x, uint16_t y, uint16_t color);
    /** 使用指定 RGB565 颜色填充整个屏幕。 */
    void LCD_Clear(uint16_t color);

    /** 按“左上角 + 宽高”填充矩形,超出屏幕的部分会自动裁剪。 */
    void LCD_FillRect(uint16_t x, uint16_t y,
    uint16_t width, uint16_t height, uint16_t color);

    /** 使用 Bresenham 算法绘制一条直线。 */
    void LCD_DrawLine(uint16_t x0, uint16_t y0,
    uint16_t x1, uint16_t y1, uint16_t color);
    /** 以给定圆心和半径绘制空心圆。 */
    void LCD_DrawCircle(uint16_t x, uint16_t y,
    uint16_t radius, uint16_t color);
    /** 按“左上角 + 宽高”绘制空心矩形。 */
    void LCD_DrawRect(uint16_t x, uint16_t y,
    uint16_t width, uint16_t height, uint16_t color);

    /** 绘制原生 uint16_t RGB565 像素数组。 */
    void LCD_DrawImage(uint16_t x, uint16_t y,
    uint16_t width, uint16_t height,
    const uint16_t *pixels);

    /** 绘制“低字节在前”的 RGB565 字节数组,用于兼容参考工程图片。 */
    void LCD_DrawImageLE(uint16_t x, uint16_t y,
    uint16_t width, uint16_t height,
    const uint8_t *pixels);

    /* —————- ASCII 文本接口 —————- */

    /**
    * 绘制单个 ASCII 字符。
    * 内置 5×7 点阵字库会按 font_height 等比例缩放,foreground 为字色,
    * background 为背景色;无法显示的字符会用问号代替。
    */
    void LCD_DrawChar(uint16_t x, uint16_t y, char character,
    uint16_t foreground, uint16_t background,
    uint8_t font_height);
    /** 绘制以 '\\0' 结尾的 ASCII 字符串,支持换行和自动折行。 */
    void LCD_DrawString(uint16_t x, uint16_t y, const char *text,
    uint16_t foreground, uint16_t background,
    uint8_t font_height);
    /** 按固定字段宽度显示无符号整数,空余高位使用空格填充。 */
    void LCD_DrawUInt(uint16_t x, uint16_t y, uint32_t number,
    uint8_t field_width, uint16_t foreground,
    uint16_t background, uint8_t font_height);

    /* —————- 参考工程兼容接口 —————- */

    #if (LCD_ENABLE_LEGACY_API != 0u)

    /*
    * 以下名称用于兼容参考资料中的旧版驱动。新编写的代码建议优先使用
    * 上方以 LCD_ 开头的新接口,以便获得参数检查和越界裁剪能力。
    */

    #ifndef USE_HORIZONTAL
    #define USE_HORIZONTAL LCD_DIRECTION
    #endif
    #define X_MAX_PIXEL LCD_WIDTH
    #define Y_MAX_PIXEL LCD_HEIGHT

    #if (LCD_USE_CUSTOM_PORT == 0u)
    #define LCD_SCL LCD_SCL_PIN
    #define LCD_SDA LCD_SDA_PIN
    #define LCD_RST LCD_RST_PIN
    #define LCD_DC LCD_DC_PIN
    #define LCD_RS LCD_DC_PIN
    #define LCD_CS LCD_CS_PIN
    #endif

    #ifndef CARMINE
    #define CARMINE LCD_COLOR_CARMINE
    #endif
    #ifndef VIOLETRED
    #define VIOLETRED LCD_COLOR_VIOLETRED
    #endif
    #ifndef GREE2
    #define GREE2 LCD_COLOR_DARK_GREEN
    #endif
    #ifndef ORANGE
    #define ORANGE LCD_COLOR_ORANGE
    #endif
    #ifndef RED
    #define RED LCD_COLOR_RED
    #endif
    #ifndef GREEN
    #define GREEN LCD_COLOR_GREEN
    #endif
    #ifndef BLUE
    #define BLUE LCD_COLOR_BLUE
    #endif
    #ifndef BLUE2
    #define BLUE2 LCD_COLOR_BLUE2
    #endif
    #ifndef WHITE
    #define WHITE LCD_COLOR_WHITE
    #endif
    #ifndef BLACK
    #define BLACK LCD_COLOR_BLACK
    #endif
    #ifndef YELLOW
    #define YELLOW LCD_COLOR_YELLOW
    #endif
    #ifndef GRAY0
    #define GRAY0 LCD_COLOR_GRAY0
    #endif
    #ifndef GRAY1
    #define GRAY1 LCD_COLOR_GRAY1
    #endif
    #ifndef GRAY2
    #define GRAY2 LCD_COLOR_GRAY2
    #endif

    void LCD_GPIO_Init(void);
    void SPI_WriteData(uint8_t data);
    void Lcd_WriteIndex(uint8_t index);
    void Lcd_WriteData(uint8_t data);
    void LCD_WriteData_16Bit(uint16_t data);
    void Lcd_WriteReg(uint8_t index, uint8_t data);
    void Lcd_Reset(void);
    void Lcd_Init(void);
    void Lcd_SetRegion(uint16_t x_start, uint16_t y_start,
    uint16_t x_end, uint16_t y_end);
    void LCD_SetRegion(uint16_t x_start, uint16_t y_start,
    uint16_t x_end, uint16_t y_end);
    void Lcd_SetXY(uint16_t x, uint16_t y);
    void Gui_DrawPoint(uint16_t x, uint16_t y, uint16_t color);
    void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t color);
    uint16_t LCD_BGR2RGB(uint16_t color);
    void Lcd_Clear(uint16_t color);
    void LCD_Fill(uint16_t x_start, uint16_t y_start,
    uint16_t x_end, uint16_t y_end, uint16_t color);
    void LCD_ShowPicture(uint16_t x, uint16_t y,
    uint16_t width, uint16_t height,
    const uint8_t *picture);
    void Gui_Circle(uint16_t x, uint16_t y,
    uint16_t radius, uint16_t color);
    void Gui_DrawLine(uint16_t x0, uint16_t y0,
    uint16_t x1, uint16_t y1, uint16_t color);
    void Gui_box(uint16_t x, uint16_t y,
    uint16_t width, uint16_t height, uint16_t color);
    void Gui_box2(uint16_t x, uint16_t y,
    uint16_t width, uint16_t height, uint8_t mode);
    void DisplayButtonDown(uint16_t x1, uint16_t y1,
    uint16_t x2, uint16_t y2);
    void DisplayButtonUp(uint16_t x1, uint16_t y1,
    uint16_t x2, uint16_t y2);
    void LCD_ShowChar(uint16_t x, uint16_t y, uint8_t character,
    uint16_t foreground, uint16_t background,
    uint8_t size_y);
    void LCD_ShowString(uint16_t x, uint16_t y, const uint8_t *text,
    uint16_t foreground, uint16_t background,
    uint8_t size_y);
    void LCD_ShowNum(uint16_t x, uint16_t y, uint32_t number,
    uint8_t digits, uint16_t foreground,
    uint16_t background, uint8_t size_y);

    #endif /* LCD_ENABLE_LEGACY_API */

    #ifdef __cplusplus
    }
    #endif

    #endif /* LCD_H */

    十、实物演示章节

    演示现象

    单片机上电之后屏幕背光点亮,屏幕输出演示界面:

    STM32 LCD DEMO
    ——————
    SCL:PA1 SDA:PA2
    RES:PA3 DC :PA4
    CS :PA5 ST7735R
    [==== 动态进度条 ====]
    PIXELS: 000
    LCD DRIVER RUNNING

    绿色进度条会持续变长、变短,下方数字同步跟随进度更新。

    演示验证点

  • LCD初始化成功,屏幕正常点亮输出文字;
  • STM32可以持续向ST7735R发送像素数据;
  • 局部矩形刷新生效:只更新进度条、数字区域,没有全屏刷屏,画面几乎无闪烁,速度更快;
  • 屏幕方向、坐标体系、RGB565颜色配置工作正常。
  • 实操小实验:修改进度条颜色、修改屏幕方向宏定义,烧录观察屏幕画面变化,理解MADCTL扫描方向作用。

    十一、屏幕方向、显存偏移与故障排查

    1. 屏幕方向

    LCD.h中宏LCD_DIRECTION控制屏幕横竖屏。工程默认:

    #define LCD_DIRECTION LCD_DIRECTION_LANDSCAPE_180

    横屏模式逻辑宽160,高128。修改宏之后,LCD_GetWidth()、LCD_GetHeight()会返回对应逻辑分辨率。

    2. 显存偏移

    不同批次ST7735R模组显存起点不一样,画面完整但是整体偏移,修改下面两个宏补偿:

    #define LCD_X_OFFSET 0u
    #define LCD_Y_OFFSET 0u

    建议不要修改所有绘图坐标,统一使用偏移宏做补偿。

    常见故障排查表

    故障现象优先排查内容
    屏幕完全不亮 检查VCC、GND供电,确认背光供电正常
    背光点亮,一直白屏 RES、CS、SCL、SDA、DC接线,确认LCD_Init初始化函数被执行
    画面镜像、画面颠倒 修改LCD_DIRECTION屏幕方向宏
    画面整体偏移、边缘内容缺失 调整X/Y偏移宏
    红色蓝色颜色互换 MADCTL命令里面RGB/BGR位配置错误
    字符正常,刷新动画很慢 减少全屏Clear,尽量使用局部窗口刷新
    随机花屏、偶尔乱点 缩短杜邦线,确认单片机与LCD共地,检查软件SPI时序延时
    接线全部正确,完全不显示 确认模块控制器芯片是ST7735R,其他驱动IC需要更换初始化序列

    十二、学习实操建议

  • 先核对硬件接线,重点确认共地;
  • 观察屏幕像素点,理解RGB三原色子像素;
  • 理清STM32‑ST7735R‑GRAM‑TFT面板之间数据流;
  • 有条件用逻辑分析仪抓取SCL、SDA、CS、DC波形,理解软件SPI;
  • 重点理解CASET → RASET → RAMWR窗口写入机制;
  • 修改RGB565颜色值,观察屏幕颜色变化;
  • 修改进度条演示代码,体会局部刷新对比全屏刷屏差异;
  • 修改屏幕方向宏,观察坐标、画面旋转变化。
  • 总结

    STM32F103C8T6利用PA1‑PA5普通GPIO模拟SPI通信,把显示命令、RGB565像素数据发送给ST7735R控制器;ST7735R接收数据存入内部GRAM显存,不断扫描显存驱动128×160 TFT面板,最终输出彩色文字与图形画面。

    赞(0)
    未经允许不得转载:171主机测评 » STM32驱动1.8寸TFT‑LCD(软件SPI)完整讲解
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址