Files
stc32g128k/Drivers/lcd.c
edisondeng ec556fdd2a fix(boot): hold backlight off until first frame is drawn
SGM_CTRL was turned on at the very start of LCD_Init(), before the
ST7789V register sequence and first frame ran, showing garbage/blank
content briefly on every boot. GPIO_Init() now forces backlight off at
power-on, LCD_Init() no longer touches it, and main() only turns it on
right before entering the main loop (after HomeApp's first frame is
drawn). Wakeup_Restore()'s existing on-then-reinit sequence is
untouched since it sets the level itself before calling LCD_Init().
2026-07-31 18:09:39 +08:00

450 lines
13 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "lcd.h"
#include "lcd_font.h"
#include "../App/config.h"
#include "intrins.h"
// ====== LHS114TC-IF03 (ST7789V) 屏幕 SPI 引脚映射 ======
sbit LCD_RST = P1^0; // 屏幕硬件复位引脚 (RESET),低电平复位
sbit LCD_DCX = P1^1; // 串行数据 / 命令选择控制线 (1:数据, 0:指令)
sbit LCD_SDI = P1^4; // SPI 主机输出从机输入数据线 (MOSI)
sbit LCD_SCL = P1^5; // SPI 串行工作时钟线 (SCLK)
sbit LCD_CS = P1^6; // SPI 物理片选选择端 (CS),低电平选通屏幕
// ====== LHS114TC-IF03 LEDA 背光控制引脚 ======
sbit SGM_CTRL = P1^7; // LEDA 开启引脚 (0:开启背光, 1:关闭熄灭 - PMOS控制)
// 引用外部延时函数 (定义在 system.c 中)
extern void Delay_ms(u16 ms);
extern void Delay10us(void);
/**
* @brief 软件模拟 8 位串行 SPI 字节数据发送
* @param dat 待发送的 1 字节数据
*/
void SPI_SendData(u8 dat)
{
u8 i;
for(i = 0; i < 8; i++)
{
LCD_SCL = 0; // 拉低时钟,准备更新数据
if(dat & 0x80)
LCD_SDI = 1;
else
LCD_SDI = 0;
_nop_();
LCD_SCL = 1; // 拉高时钟,触发屏幕在上升沿采样数据位
_nop_();
dat <<= 1; // 左移一位,准备发送下一位
}
}
/**
* @brief 向屏幕控制器写入单字节控制指令 (DCX=0)
*/
void WriteComm(u8 cmd)
{
LCD_CS = 0; // 开启片选
LCD_DCX = 0; // 切换为指令传输模式
SPI_SendData(cmd);
LCD_CS = 1; // 关闭片选
}
/**
* @brief 向屏幕控制器写入单字节显示数据 (DCX=1)
*/
void WriteData(u8 dat)
{
LCD_CS = 0; // 开启片选
LCD_DCX = 1; // 切换为数据传输模式
SPI_SendData(dat);
LCD_CS = 1; // 关闭片选
}
/**
* @brief 按照物理时序执行屏幕硬件复位
*/
void LCD_Reset(void)
{
LCD_RST = 1;
Delay_ms(10);
LCD_RST = 0; // 拉低复位引脚进入硬件复位状态
Delay_ms(20);
LCD_RST = 1; // 重新拉高引脚
Delay_ms(120); // 必须等待至少 120ms 才能执行后续的初始化
}
/**
* @brief 初始化 LHS114TC-IF03 (ST7789V) 屏幕控制器
*/
void LCD_Init(void)
{
// 开启扩展 SFR 访问权限 (EAXFR = 1),确保可以操作 P1M1 和 P1M0 模式寄存器
P_SW2 |= 0x80;
// 配置 P1 端口的 LCD_RST(P1.0)/DCX(P1.1)/SDI(P1.4)/SCL(P1.5)/CS(P1.6)/LEDA(P1.7) 为推挽输出模式 (M1=0, M0=1)
P1M1 &= ~((1<<0) | (1<<1) | (1<<4) | (1<<5) | (1<<6) | (1<<7));
P1M0 |= ((1<<0) | (1<<1) | (1<<4) | (1<<5) | (1<<6) | (1<<7));
// 设置各控制引脚初始默认电平 (LEDA 背光电平不在这里设置——开机路径由 GPIO_Init() 提前
// 关闭、main() 即将进入主循环前才点亮;休眠唤醒路径由 Wakeup_Restore() 在调用本函数前
// 先行点亮两条路径对背光的时序要求不同LCD_Init() 本身不做任何背光电平的假设)
LCD_CS = 1;
LCD_SCL = 1;
LCD_RST = 1;
LCD_DCX = 1;
LCD_Reset(); // 硬件复位屏幕
// ====== 写入 ST7789V 原厂寄存器初始化指令序列 ======
WriteComm(0x11); // Sleep Out 退出睡眠
Delay_ms(120); // 退出睡眠后必须延时至少 120ms
// Memory Data Access Control (MADCTL) 扫描方向控制
// 0x00: 从上至下从左至右RGB 格式
WriteComm(0x36);
WriteData(0x00);
// Interface Pixel Format (COLMOD)
// 0x05 代表 16-bit/pixel (RGB565格式)
WriteComm(0x3A);
WriteData(0x05);
// Porch Setting (B2h)
WriteComm(0xB2);
WriteData(0x0C);
WriteData(0x0C);
WriteData(0x00);
WriteData(0x33);
WriteData(0x33);
// Gate Control (B7h)
// VGH=13.65V, VGL=-10.43V
WriteComm(0xB7);
WriteData(0x35);
// VCOM Setting (BBh)
// VCOM=1.175V
WriteComm(0xBB);
WriteData(0x19);
// LCM Control (C0h)
WriteComm(0xC0);
WriteData(0x2C);
// VDV and VRH Command Enable (C2h)
WriteComm(0xC2);
WriteData(0x01);
// VRH Set (C3h)
// VRH=4.3V
WriteComm(0xC3);
WriteData(0x12);
// VDV Set (C4h)
// VDV=0V
WriteComm(0xC4);
WriteData(0x20);
// Frame Rate Control in Normal Mode (C6h)
// 0x0F: 60Hz
WriteComm(0xC6);
WriteData(0x0F);
// Power Control 1 (D0h)
WriteComm(0xD0);
WriteData(0xA4);
WriteData(0xA1);
// Positive Gamma Correction (E0h) 正极性伽马
WriteComm(0xE0);
WriteData(0xD0); WriteData(0x04); WriteData(0x0D); WriteData(0x11);
WriteData(0x13); WriteData(0x2B); WriteData(0x3F); WriteData(0x54);
WriteData(0x4C); WriteData(0x18); WriteData(0x0D); WriteData(0x0B);
WriteData(0x1F); WriteData(0x23);
// Negative Gamma Correction (E1h) 负极性伽马
WriteComm(0xE1);
WriteData(0xD0); WriteData(0x04); WriteData(0x0C); WriteData(0x11);
WriteData(0x13); WriteData(0x2C); WriteData(0x3F); WriteData(0x44);
WriteData(0x51); WriteData(0x2F); WriteData(0x1F); WriteData(0x1F);
WriteData(0x20); WriteData(0x23);
// Display Inversion On (21h) 开启反显ST7789 IPS 屏常用
WriteComm(0x21);
// Display On (29h) 开启显示
WriteComm(0x29);
Delay_ms(20);
// 开启显示后立即进行全屏清屏,防止屏幕亮起瞬间呈现花屏随机噪点
LCD_Clear(COLOR_BLACK);
}
/**
* @brief 设定屏幕的数据更新绘制窗口区域
*/
void LCD_SetWindow(u16 x_start, u16 x_end, u16 y_start, u16 y_end)
{
// 在底层窗口设定中,统一进行物理偏置的透明校正,免除上层各个绘图函数的手工计算
x_start += LCD_X_OFFSET;
x_end += LCD_X_OFFSET;
y_start += LCD_Y_OFFSET;
y_end += LCD_Y_OFFSET;
// 写入 Column Address Set 列地址设置寄存器命令 (0x2A)
WriteComm(0x2A);
WriteData((u8)(x_start >> 8));
WriteData((u8)(x_start & 0xFF));
WriteData((u8)(x_end >> 8));
WriteData((u8)(x_end & 0xFF));
// 写入 Row Address Set 行地址设置寄存器命令 (0x2B)
WriteComm(0x2B);
WriteData((u8)(y_start >> 8));
WriteData((u8)(y_start & 0xFF));
WriteData((u8)(y_end >> 8));
WriteData((u8)(y_end & 0xFF));
}
/**
* @brief 用指定色彩刷写清空全屏幕
* @param color 16位 RGB565 颜色填充值
*/
void LCD_Clear(u16 color)
{
u16 i, j;
LCD_SetWindow(0, LCD_WIDTH - 1, 0, LCD_HEIGHT - 1);
WriteComm(0x2C); // 发送 RAM Write (0x2C) 显存写入命令
for(i = 0; i < LCD_WIDTH; i++)
{
for(j = 0; j < LCD_HEIGHT; j++)
{
WriteData((u8)(color >> 8)); // 发送 RGB565 高 8 位字节
WriteData((u8)(color & 0xFF)); // 发送 RGB565 低 8 位字节
}
}
}
/**
* @brief 在屏幕指定视口绘制实心矩形
*/
void LCD_FillRect(u16 x, u16 y, u16 w, u16 h, u16 color)
{
u16 i, count;
u8 ch = (u8)(color >> 8);
u8 cl = (u8)(color & 0xFF);
LCD_SetWindow(x, x + w - 1, y, y + h - 1);
WriteComm(0x2C); // 写入显存
count = w * h; // 实心矩形包含的总像素数
for(i = 0; i < count; i++)
{
WriteData(ch);
WriteData(cl);
}
}
/**
* @brief 绘制空心矩形边界框
* @details 通过组合调用四次 LCD_FillRect 实心线来绘制矩形的上、下、左、右四条边。
*/
void LCD_DrawRectBorder(u16 x, u16 y, u16 w, u16 h, u16 color)
{
LCD_FillRect(x, y, w, 1, color); // 上边框线
LCD_FillRect(x, y + h - 1, w, 1, color); // 下边框线
LCD_FillRect(x, y, 1, h, color); // 左边框线
LCD_FillRect(x + w - 1, y, 1, h, color); // 右边框线
}
/**
* @brief 渲染显示一个单色字模位图资源
* @param x 渲染起始坐标 X
* @param y 渲染起始坐标 Y
* @param w 位图宽度 (单位: 像素点数)
* @param h 位图高度 (单位: 像素点数)
* @param bmp FLASH 中的单色像素数组指针 (1表示画前景色0表示画背景色)
* @param color 16位前景色
* @param bg_color 16位背景色
*/
void LCD_DrawMonoBitmap(u16 x, u16 y, u16 w, u16 h, unsigned char code *bmp, u16 color, u16 bg_color)
{
u16 r, c;
u16 byte_width = (w + 7) / 8; // 计算在单色编码中,一行图像占用的实际字节数
u8 ch = (u8)(color >> 8);
u8 cl = (u8)(color & 0xFF);
u8 bgh = (u8)(bg_color >> 8);
u8 bgl = (u8)(bg_color & 0xFF);
LCD_SetWindow(x, x + w - 1, y, y + h - 1); // 限制绘制显存窗口
WriteComm(0x2C);
// 嵌套循环,从第 0 行第 0 列开始遍历每个像素位置
for(r = 0; r < h; r++)
{
for(c = 0; c < w; c++)
{
u16 byte_idx = r * byte_width + (c / 8); // 定位当前像素所在单色图数组中的字节索引
u8 bit_shift = 7 - (c % 8); // 定位该像素在该字节中对应的 bit 位置 (高位在前)
if(bmp[byte_idx] & (1 << bit_shift))
{
WriteData(ch); // 该位为 1发送前景色
WriteData(cl);
}
else
{
WriteData(bgh); // 该位为 0发送背景色
WriteData(bgl);
}
}
}
}
/**
* @brief 在指定坐标渲染输出一个 8x16 尺寸的普通英文 ASCII 字符
* @param ch ASCII 字符本身
*/
void LCD_ShowChar8x16(u16 x, u16 y, char ch, u16 color, u16 bg_color)
{
u8 r, c;
u8 char_idx = ch - 0x20; // 偏移计算ASCII 可打印字符从 0x20 (空格) 开始
u8 f_h = (u8)(color >> 8);
u8 f_l = (u8)(color & 0xFF);
u8 b_h = (u8)(bg_color >> 8);
u8 b_l = (u8)(bg_color & 0xFF);
if(ch < 0x20 || ch > 0x7E) return; // 过滤不可见控制字符
LCD_SetWindow(x, x + 8 - 1, y, y + 16 - 1);
WriteComm(0x2C);
// 循环 16 行,绘制 8x16 字型
for(r = 0; r < 16; r++)
{
u8 line = FONT_8x16[char_idx][r]; // 提取当前行对应的 8 位单色像素信息
for(c = 0; c < 8; c++)
{
// 从高到低依次提取单行中的像素点
if(line & (0x80 >> c))
{
WriteData(f_h);
WriteData(f_l);
}
else
{
WriteData(b_h);
WriteData(b_l);
}
}
}
}
/**
* @brief 在指定坐标打印一行普通英文 ASCII 字符串 (左对齐)
*/
void LCD_ShowString(u16 x, u16 y, char *str, u16 color, u16 bg_color)
{
while(*str)
{
LCD_ShowChar8x16(x, y, *str, color, bg_color); // 渲染当前字符
x += 8; // X 轴起始位置偏移 8 像素,准备绘制下一个字符
str++; // 移向下一个字符
}
}
/**
* @brief 在指定 Y 轴高度上,自动计算偏移以实现字符串水平居中显示
*/
void LCD_ShowStringCentered(u16 y, char *str, u16 color, u16 bg_color)
{
u16 len = 0;
char *p = str;
while(*p++) len++; // 计算字符串长度
if(len * 8 >= LCD_WIDTH)
// 若超出或等同于屏幕有效宽度,强制从最左侧 0 开始左对齐绘制
LCD_ShowString(0, y, str, color, bg_color);
else
// 否则通过公式 (LCD_WIDTH - 字符宽度) / 2 计算出水平居中的首字起始偏移 X 坐标并渲染
LCD_ShowString((LCD_WIDTH - len * 8) / 2, y, str, color, bg_color);
}
/**
* @brief 渲染大号特粗 16x32 尺寸的英文/数字字符
*/
void LCD_ShowChar16x32(u16 x, u16 y, char ch, u16 color, u16 bg_color)
{
u8 r, c, i, j;
u8 val_bit;
u8 char_idx = ch - 0x20;
u8 f_h = (u8)(color >> 8);
u8 f_l = (u8)(color & 0xFF);
u8 b_h = (u8)(bg_color >> 8);
u8 b_l = (u8)(bg_color & 0xFF);
if(ch < 0x20 || ch > 0x7E) return;
LCD_SetWindow(x, x + 16 - 1, y, y + 32 - 1); // 锁定 16x32 的绘图视窗
WriteComm(0x2C);
// 遍历字型的 16 个点阵行 data
for(r = 0; r < 16; r++)
{
u8 line = FONT_8x16[char_idx][r];
// 纵向插点翻倍:将当前行重复绘制两次 (i = 0 和 i = 1)
for(i = 0; i < 2; i++)
{
// 遍历单行内的 8 个 bit 像素点
for(c = 0; c < 8; c++)
{
val_bit = (line & (0x80 >> c));
// 横向插点翻倍:单个像素点重复发送写入 2 次 (j = 0 和 j = 1)
for(j = 0; j < 2; j++)
{
if(val_bit)
{
WriteData(f_h);
WriteData(f_l);
}
else
{
WriteData(b_h);
WriteData(b_l);
}
}
}
}
}
}
/**
* @brief 在指定坐标绘制特大 16x32 规格的数字/英文字串
*/
void LCD_ShowString16x32(u16 x, u16 y, char *str, u16 color, u16 bg_color)
{
while(*str)
{
LCD_ShowChar16x32(x, y, *str, color, bg_color);
x += 16; // 每次起始坐标偏移 16 像素
str++;
}
}
/**
* @brief 在指定 Y 轴高度上,水平居中渲染显示特大 16x32 规格字符串
*/
void LCD_ShowString16x32Centered(u16 y, char *str, u16 color, u16 bg_color)
{
u16 len = 0;
char *p = str;
while(*p++) len++; // 计算字数
if(len * 16 >= LCD_WIDTH)
// 超出屏幕宽度,左对齐
LCD_ShowString16x32(0, y, str, color, bg_color);
else
// 水平居中计算
LCD_ShowString16x32((LCD_WIDTH - len * 16) / 2, y, str, color, bg_color);
}