#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); }