前言
本文基于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告诉ST7735R要在哪个坐标显示什么颜色,ST7735R负责把数据存进显存,再不停扫描显存点亮屏幕像素。
二、硬件接线说明
硬件连接表
| VCC | 5V | LCD模块供电 |
| GND | GND | 电源地,必须和STM32共地 |
| SCL | PA1 | 软件SPI时钟线 |
| SDA | PA2 | 软件SPI单向数据线,STM32向LCD发送数据 |
| RES | PA3 | LCD硬件复位,低电平有效 |
| DC | PA4 | 命令/数据选择,低电平=命令,高电平=数据 |
| CS | PA5 | LCD片选,低电平有效 |

接线关键点说明
供电与逻辑电平 本工程使用的成品LCD模块接5V供电,模块板载稳压电路。STM32的GPIO输出是3.3V逻辑电平,可以正常驱动该模块。
注意:只有标识支持5V供电的成品模块才可以这样接线,裸屏不要直接接5V,以模块丝印和手册为准。
三、1.8寸TFT‑LCD显示基础原理
1.8寸指屏幕对角线物理尺寸,屏幕物理像素128列×160行。屏幕横屏使用时程序逻辑分辨率为160×128。
每一个彩色像素由红、绿、蓝3个子像素构成。
- TFT即薄膜晶体管,每一个子像素搭配独立晶体管,用来控制液晶单元透光程度;
- 背光源提供白光;
- 液晶单元调整光线透过率;
- 再经过RGB彩色滤光片,混合得到人眼看到的彩色。
重点:LCD屏幕本身不认识“画线、写汉字、字符串”。所有文字、图形,全部需要拆解成一个个像素点颜色,交给显示控制器,再输出到面板。
四、ST7735R控制器作用
ST7735R是LCD模块板载驱动芯片,是STM32单片机和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字节数据逻辑:
发送字节顺序:bit7最高位优先发送,最后发送bit0最低位。
DC引脚区分命令与数据
命令和数据共用SDA数据线,依靠DC引脚电平区分:
- DC=0,CS=0:发送命令字节
- DC=1,CS=0:发送命令参数、像素颜色数据
- CS=1:结束本次数据传输
举例子:设置RGB565颜色格式,先DC拉低发送命令0x3A,再DC拉高发送参数0x05。
软件SPI vs 硬件SPI对比
| 引脚选择 | 非常灵活,普通GPIO即可 | 只能使用芯片指定SPI复用引脚 |
| 学习理解 | 时序清晰,适合教学学习 | 需要操作外设寄存器,抽象 |
| 运行速度 | 速度慢,占用CPU资源 | 速度快,可以搭配DMA搬运数据 |
| 本工程实现 | PA1 PA2模拟SPI | 没有使用片上SPI1、SPI2外设 |
GPIO配置为GPIO_Speed_50MHz只是设置GPIO最大输出能力,不等于软件SPI时钟能跑到50M,实际通信速度取决于CPU执行循环、翻转IO的速度。
六、LCD初始化流程
LCD上电后不能直接显示画面,需要硬件复位 + 向ST7735R发送一整套初始化参数。工程内LCD_Init()函数执行顺序:
复位、退出睡眠之后必须留出足够延时等待。工程通过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组命令限定要绘制的矩形范围:
注意:结束坐标是包含在绘制区域内。 举例:窗口(10,20)~(19,29) 宽度 =19‑10+1 =10像素 高度 =29‑20+1 =10像素 总像素数量=10×10=100个像素。
设置完窗口后,连续发送100个RGB565颜色值,ST7735内部写指针会自动递增,不需要每一个像素重复发送坐标命令,极大提升绘图效率。
2. RGB565颜色编码
16bit表示一个像素,分配规则:5bit红色,6bit绿色,5bit蓝色。
人眼对绿色亮度更敏感,所以绿色分配更多位数。
| 黑色 | 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
绿色进度条会持续变长、变短,下方数字同步跟随进度更新。

演示验证点
实操小实验:修改进度条颜色、修改屏幕方向宏定义,烧录观察屏幕画面变化,理解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需要更换初始化序列 |
十二、学习实操建议
总结
STM32F103C8T6利用PA1‑PA5普通GPIO模拟SPI通信,把显示命令、RGB565像素数据发送给ST7735R控制器;ST7735R接收数据存入内部GRAM显存,不断扫描显存驱动128×160 TFT面板,最终输出彩色文字与图形画面。




