重构代码的架构设计,增加测试单元,提高代码可靠性

This commit is contained in:
2026-03-23 20:40:04 +08:00
parent c2ce221691
commit a4bf0962b2
31 changed files with 2084 additions and 703 deletions

View File

@@ -1,7 +1,7 @@
#ifndef __DISPLAY__H__
#define __DISPLAY__H__
#include "../../include/types.h"
#include "types.h"
/* 静态菜定义需要的属性 */

View File

@@ -1,7 +1,7 @@
#ifndef __KEY_H__
#define __KEY_H__
#include "../../include/types.h"
#include "types.h"

View File

@@ -1,602 +0,0 @@
#include "lcd.h"
#include <string.h>
#include "ascii.h"
#include <math.h>
#ifdef DEBUG
#include <stdio.h>
#endif
tagScreenControl g_tCVsr; // 当前界面结构指针
tagDspAttrib g_tDspAttrib; // 显示属性
void Lcd_Init(void)
{
/* 清空显存 */
memset(g_tCVsr.pwbyLCDMemory, 0, sizeof(g_tCVsr.pwbyLCDMemory));
/* 设置默认颜色 */
g_tCVsr.wFontColor = LCD_COLOR_WHITE;
g_tCVsr.wBackColor = LCD_COLOR_BLACK;
/* 设置默认字体 */
//g_tCVsr.pwLibHZ = (uint16_t*)HZK12;
/* 字体的大小需要调试 */
g_tCVsr.wGBFontWidth = 13;
g_tCVsr.wGBFontHeight = 12;
/* 设置默认ASCII字体 */
g_tCVsr.pbyLibAscii = g_abyASCII126[0];
g_tCVsr.wASCIIFontWidth = 7;
g_tCVsr.wASCIIFontHeight = 12;
g_tDspAttrib.rowSpace = 2;
}
void Lcd_SetPixel(uint16_t x, uint16_t y, uint8_t color)
{
if (x >= LCD_SIZE_X || y >= LCD_SIZE_Y)
{
return;
}
/* 一个字节一个像素点 */
g_tCVsr.pwbyLCDMemory[y * LCD_LINE_SIZE + x] = color;
}
uint16_t Lcd_GetPixel(uint16_t x, uint16_t y)
{
if (x >= LCD_SIZE_X || y >= LCD_SIZE_Y)
{
return 0;
}
/* 一个字节一个像素点 */
return g_tCVsr.pwbyLCDMemory[y * LCD_LINE_SIZE + x];
}
/******************************************************************************
* 函数名: Lcd_FillRect
* 功能: 在显存中用指定颜色填充一个矩形区域
* 参数: lx - 左上角 X 坐标 (left x)
* ty - 左上角 Y 坐标 (top y)
* rx - 右下角 X 坐标 (right x)
* by - 右下角 Y 坐标 (bottom y)
* color- 填充颜色
* 返回: 无
* 说明: 对 [left_x, right_x] × [top_y, bottom_y] 区域内的每个像素逐点调用 Lcd_SetPixel 进行填充,
* 仅操作显存,不负责刷屏输出,由上层根据需要统一刷新。
*****************************************************************************/
void Lcd_FillRect(uint16_t left_x, uint16_t top_y, uint16_t right_x, uint16_t bottom_y, uint32_t color)
{
for(uint16_t y = top_y; y <= bottom_y; y++ )
{
for(uint16_t x = left_x; x <= right_x; x++ )
{
Lcd_SetPixel(x, y, color);
}
}
}
/******************************************************************************
* 宏名: M_GuiSwap
* 功能: 交换两个整型变量的值(按位异或方式)
* 说明: 要求 a、b 为同一类型的可写左值,且 a、b 不能是同一地址
*****************************************************************************/
#define M_GuiSwap(a, b) { a^=b; b^=a; a^=b; }
/******************************************************************************
* 函数名: Lcd_LineH
* 功能: 在显存中绘制一条水平实线(可指定线宽)
* 参数: wXStart - 线段起始 X 坐标(左端)
* wXEnd - 线段结束 X 坐标(右端,若小于起始则自动交换)
* wYStart - 线段上边缘 Y 坐标
* wWidth - 线宽(沿 Y 方向的像素高度)
* 返回: 无
* 说明: 1. 若 wXEnd < wXStart会先交换保证从左向右绘制
* 2. 实际绘制区域为 [wXStart, wXEnd) × [wYStart, wYStart + wWidth)
* 3. 颜色使用全局当前字体颜色 g_tCVsr.wFontColor
*****************************************************************************/
void Lcd_LineH(uint16_t wXStart, uint16_t wXEnd, uint16_t wYStart, uint16_t wWidth, uint8_t color)
{
uint16_t wYEnd = wYStart + wWidth; /* 计算水平线在 Y 方向的结束位置 = 起始 Y + 线宽 */
if( wXEnd < wXStart ) /* 若结束 X 小于起始 X说明调用参数顺序反了需要交换 */
{
M_GuiSwap(wXEnd, wXStart); /* 使用异或交换宏,将 wXStart 与 wXEnd 的值互换 */
}
for(uint16_t x = wXStart; x < wXEnd; x++ ) /* 从左到右,遍历线段覆盖的每一列 X 坐标 */
{
for(uint16_t y = wYStart; y < wYEnd; y++ ) /* 在当前 X 列内,从上到下按线宽遍历每个像素行 */
{
Lcd_SetPixel(x, y, color); /* 将当前像素点设置为当前字体颜色,实现实心线绘制 */
}
}
}
/******************************************************************************
* 函数名: Lcd_LineV
* 功能: 在显存中绘制一条垂直实线(可指定线宽)
* 参数: wYStart - 线段起始 Y 坐标(上端)
* wYEnd - 线段结束 Y 坐标(下端,若小于起始则自动交换)
* wXStart - 线段左边缘 X 坐标
* wWidth - 线宽(沿 X 方向的像素宽度)
* 返回: 无
* 说明: 1. 若 wYEnd < wYStart会先交换保证从上向下绘制
* 2. 实际绘制区域为 [wXStart, wXStart + wWidth) × [wYStart, wYEnd)
* 3. 颜色使用全局当前字体颜色 g_tCVsr.wFontColor
*****************************************************************************/
void Lcd_LineV(uint16_t wYStart, uint16_t wYEnd, uint16_t wXStart, uint16_t wWidth, uint8_t color)
{
uint16_t wXEnd = wXStart + wWidth;
if( wXEnd < wXStart ) /* 若结束 X 小于起始 X说明调用参数顺序反了需要交换 */
{
M_GuiSwap(wYEnd, wYStart); /* 使用异或交换宏,将 wYStart 与 wYEnd 的值互换 */
}
for(uint16_t x = wXStart; x < wXEnd; x++) /* 从左到右,遍历线段覆盖的每一列 X 坐标 */
{
for(uint16_t y = wYStart; y < wYEnd; y++) /* 从上到下,遍历线段覆盖的每一行 Y 坐标 */
{
Lcd_SetPixel(x, y, color); /* 将当前像素点设置为当前字体颜色,实现实心线绘制 */
}
}
}
/******************************************************************************
* 函数名: Lcd_Line
* 功能: 在显存中绘制一条任意斜率的直线,并支持指定线宽
* 参数: wXstart - 起点 X 坐标
* wYstart - 起点 Y 坐标
* wXend - 终点 X 坐标
* wYend - 终点 Y 坐标
* wWidth - 线宽(像素),以线段中轴为中心向两侧扩展
* 返回: 无
* 说明: 1. 对水平线/垂直线分别调用 Lcd_LineH / Lcd_LineV 进行优化绘制
* 2. 其它情况使用类 Bresenham 算法,从两端向中间对称绘制,并按线宽加粗
* 3. 颜色使用全局当前字体颜色 g_tCVsr.wFontColor
*****************************************************************************/
void Lcd_Line(uint16_t wXstart, uint16_t wYstart, uint16_t wXend, uint16_t wYend, uint16_t wWidth, uint8_t color)
{
uint16_t wDX; /* X 方向差值(水平偏移量的绝对值) */
uint16_t wDY; /* Y 方向差值(垂直偏移量的绝对值) */
uint16_t wSignY; /* 纵向增量符号(+1 或 -1决定向上/向下绘制 */
uint16_t wSignX; /* 横向增量符号(+1 或 -1决定向左/向右绘制 */
uint16_t wDecision; /* 误差累积量,类似 Bresenham 算法中的判别值 */
uint16_t wCurx, wCury, wNextx, wNexty, wPY, wPX; /* 当前端点、对称端点以及循环中使用的临时坐标 */
/* 若起点和终点 Y 相同,说明是水平直线,直接调用水平画线函数即可 */
if( wYstart == wYend )
{
Lcd_LineH(wXstart, wXend, wYstart, wWidth, color);
return;
}
/* 若起点和终点 X 相同,说明是垂直直线,直接调用垂直画线函数即可 */
if( wXstart == wXend )
{
Lcd_LineV(wYstart, wYend, wXstart, wWidth, color);
return;
}
/* 计算水平和垂直方向的距离,后续根据 wDX / wDY 决定主增量方向 */
wDX = abs(wXstart - wXend);
wDY = abs(wYstart - wYend);
/* 为了统一从“左到右 / 上到下”的绘制方向,对某些象限的线段进行起终点交换 */
if (((wDX >= wDY && (wXstart > wXend)) || /* 以 X 为主增量,且起点在右侧,则交换 */
((wDY > wDX) && (wYstart > wYend)))) /* 以 Y 为主增量,且起点在下侧,则交换 */
{
M_GuiSwap(wXend, wXstart); /* 交换 X 坐标,使起点在左/上 */
M_GuiSwap(wYend, wYstart); /* 交换 Y 坐标,对应调整终点 */
}
/* 计算每一步在 Y 方向上的符号:向下(+1) 或向上(-1) */
wSignY = (wYend - wYstart) / wDY;
/* 计算每一步在 X 方向上的符号:向右(+1) 或向左(-1) */
wSignX = (wXend - wXstart) / wDX;
/* 情况一X 方向偏移大于等于 Y 方向(线更“横向”) */
if (wDX >= wDY)
{
wCurx = wXstart; /* 当前点 X 从起点开始 */
wCury = wYstart; /* 当前点 Y 从起点开始 */
wNextx = wXend; /* 对称点 X 从终点开始(用于加粗两端) */
wNexty = wYend; /* 对称点 Y 从终点开始 */
wDecision = (wDX >> 1); /* 初始化判别值为一半的 wDX */
for (; wCurx <= wNextx; ) /* 从两端向中间画,直到两个端点相遇或交叉 */
{
/* 累积的误差超过一条“格子宽度”时,说明需要在 Y 方向跨一格 */
if (wDecision >= wDX)
{
wDecision -= wDX; /* 误差回退一个 wDX保持在合理范围内 */
wCury += wSignY; /* 当前端点 Y 沿着符号方向移动一格 */
wNexty -= wSignY; /* 对称端点 Y 反向移动一格,保持对称 */
}
/* 以当前端点 (wCurx, wCury) 为中心,按线宽在 Y 方向“扩粗”填充像素 */
for (wPY = wCury - wWidth / 2; wPY <= wCury + wWidth / 2; wPY++)
{
Lcd_SetPixel(wCurx, wPY, color);
}
/* 以对称端点 (wNextx, wNexty) 为中心,同样按线宽在 Y 方向填充,实现两端对称绘制 */
for (wPY = wNexty - wWidth / 2; wPY <= wNexty + wWidth / 2; wPY++)
{
Lcd_SetPixel(wNextx, wPY, color);
}
wCurx++; /* 当前端点 X 向右移动一格 */
wNextx--; /* 对称端点 X 向左移动一格 */
wDecision += wDY; /* 增加误差值,下一轮判断是否需要在 Y 方向跨格 */
}
}
/* 情况二Y 方向偏移大于 X 方向(线更“竖向”) */
else
{
wCurx = wXstart; /* 当前点 X 从起点开始 */
wCury = wYstart; /* 当前点 Y 从起点开始 */
wNextx = wXend; /* 对称点 X 从终点开始 */
wNexty = wYend; /* 对称点 Y 从终点开始 */
wDecision = (wDY >> 1); /* 初始化判别值为一半的 wDY */
for (; wCury <= wNexty; ) /* 从两端向中间画,直到在 Y 方向相遇 */
{
/* 累积误差超过一条“格子高度”时,说明需要在 X 方向跨一格 */
if (wDecision >= wDY)
{
wDecision -= wDY; /* 回退一个 wDY避免误差过大 */
wCurx += wSignX; /* 当前端点 X 沿符号方向移动一格 */
wNextx -= wSignX; /* 对称端点 X 反向移动一格 */
}
/* 以当前端点 (wCurx, wCury) 为中心,在 X 方向按线宽“扩粗”填充像素 */
for (wPX = wCurx - wWidth / 2; wPX <= wCurx + wWidth / 2; wPX++)
{
Lcd_SetPixel(wPX, wCury, color);
}
/* 以对称端点 (wNextx, wNexty) 为中心,同样在 X 方向扩粗填充,实现两端对称绘制 */
for (wPX = wNextx - wWidth / 2; wPX <= wNextx + wWidth / 2; wPX++)
{
Lcd_SetPixel(wPX, wNexty, color);
}
wCury++; /* 当前端点 Y 向下移动一格 */
wNexty--; /* 对称端点 Y 向上移动一格 */
wDecision += wDX; /* 增加误差值,下一轮判断是否需要在 X 方向跨格 */
}
}
}
/******************************************************************************
* 函数名: Lcd_MeiTouPic
* 功能: 在指定 Y 位置绘制一条带左右“眉头”装饰的水平线(中间直线 + 两端斜线)
* 参数: wYStart - 中间水平线的 Y 坐标
* wWidth - 线宽(像素),传递给 Lcd_LineH / Lcd_Line
* 返回: 无
* 说明: 1. 中间部分为 X=16..144 的水平粗线
* 2. 左端在 (16, wYStart) 位置向左上方连一条斜线到 (8, wYStart-8)
* 3. 右端在 (144, wYStart) 位置向右上方连一条斜线到 (152, wYStart-8)
*****************************************************************************/
void Lcd_MeiTouPic(uint16_t wYStart, uint16_t wWidth)
{
Lcd_LineH(16, 144, wYStart, wWidth, g_tCVsr.wFontColor); /* 中间水平粗线段 */
Lcd_Line(8, wYStart - 8, 16, wYStart, wWidth, g_tCVsr.wFontColor);/* 左端向上的斜线“眉头” */
Lcd_Line(144, wYStart, 152, wYStart - 8, wWidth, g_tCVsr.wFontColor);/* 右端向上的斜线“眉头” */
}
void Lcd_ButtonPush(uint16_t left_x, uint16_t top_y, uint16_t right_x, uint16_t bottom_y)
{
Lcd_LineH(left_x, right_x,top_y, 1, g_tCVsr.wFontColor);
Lcd_LineV(top_y, bottom_y,left_x, 1, g_tCVsr.wFontColor);
Lcd_LineH(left_x, right_x,bottom_y, 1, g_tCVsr.wFontColor);
Lcd_LineV(top_y, bottom_y,right_x, 1, g_tCVsr.wFontColor);
}
/******************************************************************************
* 宏名: M_Max / M_Min
* 功能: 返回两个数中的较大值 / 较小值(简单三目运算宏)
*****************************************************************************/
#define M_Max( a, b ) ( ((a) > (b)) ? (a) : (b) )
#define M_Min( a, b ) ( ((a) < (b)) ? (a) : (b) )
/******************************************************************************
* 函数名: Lcd_Invert
* 功能: 对指定矩形区域内的像素进行“反相”操作(黑变白、白变黑)
* 参数: wXstart - 矩形左上角 X 坐标
* wYstart - 矩形左上角 Y 坐标
* wXend - 矩形右下角 X 坐标
* wYend - 矩形右下角 Y 坐标
* 返回: 无
* 说明: 1. 首先进行边界检查,防止坐标越界访问显存
* 2. 使用 M_Min/M_Max 规范化矩形对角坐标为 (xMin,yMin)-(xMax,yMax)
* 3. 遍历矩形区域,每个像素值按位取反写回,实现反白/反显效果
*****************************************************************************/
void Lcd_Invert(uint16_t wXstart, uint16_t wYstart, uint16_t wXend, uint16_t wYend)
{
uint16_t xMin, xMax, yMin, yMax; /* 归一化后的矩形边界坐标 */
uint8_t wColor; /* 当前像素原始色值8bit 单色) */
/* 边界检查:若任意一端 X 超出屏幕宽度,则直接返回,不做处理 */
if ((wXstart > LCD_SIZE_X) || (wXend > LCD_SIZE_X))
return;
/* 边界检查:若任意一端 Y 超出屏幕高度,则直接返回 */
if ((wYstart > LCD_SIZE_Y) || (wYend > LCD_SIZE_Y))
return;
/* 规范化矩形xMin/xMax 为左右边界yMin/yMax 为上下边界(不关心调用时 start/end 的大小关系) */
xMin = M_Min(wXstart, wXend);
yMin = M_Min(wYstart, wYend);
xMax = M_Max(wXstart, wXend);
yMax = M_Max(wYstart, wYend);
/* 双重循环遍历矩形区域内的每个像素点(不包含 xMax/yMax 边界本身) */
for (uint16_t y = yMin; y < yMax; y++)
{
for (uint16_t x = xMin; x < xMax; x++)
{
/* 从显存中读取当前像素值,逐位取反后写回,实现黑白反转/反显效果 */
wColor = g_tCVsr.pwbyLCDMemory[y * LCD_SIZE_X + x];
g_tCVsr.pwbyLCDMemory[y * LCD_SIZE_X + x] = (uint8_t)~wColor;
}
}
return;
}
//==============================================================================
// 功能说明 : 在指定屏幕坐标处显示一个 ASCII 字符
// 设计说明 : 从 ASCII 字库中取出点阵, 按当前显示属性(正显/反显、旋转 90° 与否)
// 逐点调用画点函数进行显示
// 参数说明 : byScreen - 屏幕号
// x, y - 字符左上角基准坐标(若 Rotate!=0, 实际显示会旋转 90°)
// byAscii - 要显示的 ASCII 码
// bTR - TRUE : 透明显示, 点阵为 0 时保留原背景
// FALSE : 非透明, 点阵为 0 时用背景色重绘
// 返回说明 : 0 - 正常
// -1 - Y 方向越界
// -2 - X 方向越界
//==============================================================================
inline uint16_t Lcd_Pub_Ascii(uint16_t x, uint16_t y, uint8_t byAscii)
{
uint8_t i, j;
uint8_t byLine, *pbyFontLib;
uint8_t on_color, off_color;
/* 从 ASCII 字库中取得当前字符的点阵数据首地址
每个字符占用 wASCIIFontHeight 个字节, 按行存储 */
pbyFontLib = &g_tCVsr.pbyLibAscii[byAscii * g_tCVsr.wASCIIFontHeight];
/* 边界检查:根据旋转与字符宽高判断是否越界 */
if (0 == g_tDspAttrib.Rotate) {
if ((x + g_tCVsr.wASCIIFontWidth) > LCD_SIZE_X) return (uint16_t)-2; /* X 越界 */
if ((y + g_tCVsr.wASCIIFontHeight) > LCD_SIZE_Y) return (uint16_t)-1; /* Y 越界 */
}
else
{
/* 旋转后宽高互换, 重新按宽高做边界检查 */
if ((x + g_tCVsr.wASCIIFontHeight) > LCD_SIZE_X) return (uint16_t)-2; /* X 越界 */
if ((y + 1) < g_tCVsr.wASCIIFontWidth) return (uint16_t)-1; /* Y 越界(向上旋转) */
}
/* 根据正显/反显选择“点阵为 1/0 时用的颜色” */
if (0 == g_tDspAttrib.style) {
/* 正显1 = 前景色0 = 背景色 */
on_color = g_tCVsr.wFontColor;
off_color = g_tCVsr.wBackColor;
} else {
/* 反显1 = 背景色0 = 前景色 */
on_color = g_tCVsr.wBackColor;
off_color = g_tCVsr.wFontColor;
}
for (j = 0; j < g_tCVsr.wASCIIFontHeight; j++)
{
byLine = pbyFontLib[j]; /* 第 j 行的 8bit 点阵 */
for (i = 0; i < g_tCVsr.wASCIIFontWidth; i++)
{
uint8_t bit_on = ((byLine << i) & 0x80) != 0;
uint8_t color;
uint16_t px, py;
if (bit_on)
{
color = on_color;
}
else
{
color = off_color;
}
if (0 == g_tDspAttrib.Rotate)
{
/* 不旋转: (x+i, y+j) */
px = x + i;
py = y + j;
}
else
{
/* 旋转 90°: 将 (i,j) 映射到 (x+j, y-i) */
px = x + j;
py = y - i;
}
Lcd_SetPixel(px, py, color);
}
}
return 0;
}
/**
* 从 UTF-8 字节流中解析下一个字符的 Unicode 码点,并返回该字符占用的字节数。
*
* UTF-8 编码规则简要:
* - 1 字节0xxxxxxxASCII0x00..0x7F
* - 2 字节110xxxxx 10xxxxxxU+0080..U+07FF
* - 3 字节1110xxxx 10xxxxxx 10xxxxxxU+0800..U+FFFF含常用汉字
* - 4 字节11110xxx 10xxxxxx 10xxxxxx 10xxxxxxU+10000..,本函数不处理)
*
* @param utf8 指向当前 UTF-8 字节的指针(可含多字节序列)
* @param out_unicode 输出该字符的 Unicode 码点U+0000..U+FFFF
* @return 该字符占用的字节数 1/2/30 表示结束、无效或无法解析
*/
uint8_t utf8_next(const unsigned char *utf8, uint32_t *out_unicode)
{
unsigned char c = utf8[0];
/* 字符串结束:'\0' 不算一个可解析字符,返回 0 表示“无更多字符” */
if (c == 0) {
*out_unicode = 0;
return 0;
}
/* -----------------------------------------------------------------------
* 1 字节ASCIIU+0000..U+007F
* 格式0xxxxxxx即 c < 0x80。码点等于该字节的数值。
* ----------------------------------------------------------------------- */
if (c < 0x80) {
*out_unicode = c;
return 1;
}
/* -----------------------------------------------------------------------
* 2 字节U+0080..U+07FF如拉丁扩展、希腊文等
* 格式:首字节 110xxxxx0xC0..0xDF),次字节 10xxxxxx0x80..0xBF)。
* 码点 = (首字节低 5 位)<<6 | (次字节低 6 位) = (c&0x1F)<<6 | (utf8[1]&0x3F)。
* ----------------------------------------------------------------------- */
if ((c & 0xE0) == 0xC0) {
if (utf8[1] == 0)
return 0; /* 首字节后无后续字节,非法 UTF-8 序列 */
*out_unicode = (uint32_t)((c & 0x1F) << 6 | (utf8[1] & 0x3F));
return 2;
}
/* -----------------------------------------------------------------------
* 3 字节U+0800..U+FFFF含常用汉字、日韩等
* 格式:首字节 1110xxxx0xE0..0xEF),后两字节均为 10xxxxxx。
* 码点 = (首字节低 4 位)<<12 | (第2字节低6位)<<6 | (第3字节低6位)。
* 例如 “你” 的 UTF-8 为 E4 BD A0 → U+4F60。
* ----------------------------------------------------------------------- */
if ((c & 0xF0) == 0xE0) {
if (utf8[1] == 0 || utf8[2] == 0)
return 0; /* 缺少第二或第三字节,非法序列 */
*out_unicode = (uint32_t)((c & 0x0F) << 12 | (utf8[1] & 0x3F) << 6 | (utf8[2] & 0x3F));
return 3;
}
/* 4 字节U+10000 及以上)或非法首字节(如 10xxxxxx 单独出现):本实现不解析 */
*out_unicode = 0;
return 0;
}
void Lcd_Pub_UTF8(uint16_t x, uint16_t y, uint32_t unicode )
{
const uint8_t *bitmap = utf8_hz12_get(unicode);
uint16_t word = 0;
if (bitmap == NULL)
{
#ifdef DEBUG
printf("Lcd_Pub_UTF8: unicode = %d, bitmap = NULL\n", unicode);
#endif
return;
}
for (uint8_t j = 0; j < g_tCVsr.wGBFontHeight; j++)
{
word = (uint16_t)((bitmap[j*2] << 8) | bitmap[j*2+1]);
for (uint8_t i = 0; i < g_tCVsr.wGBFontWidth; i++)
{
if ((word >> (15 - i)) & 1)
{
Lcd_SetPixel(x + i, y + j, g_tCVsr.wFontColor);
}
else
{
Lcd_SetPixel(x + i, y + j, g_tCVsr.wBackColor);
}
}
}
}
/**
* 在指定坐标起逐字显示 UTF-8 字符串支持汉字UTF-8 字库、ASCII 与换行。
* 字符串以 \\0 结尾;遇换行符 0x0A 则换到下一行;本行放不下时擦除本行剩余部分。
*
* @param x, y 起始坐标(首个字符左上角)
* @param pcString UTF-8 字符串
* @return 0 成功;-1 起始 X 越界;-2 起始 Y 越界;<0 其它错误(如找不到换行导致换行失败)
*/
int8_t Lcd_ShowStr(uint16_t x, uint16_t y, uint8_t *pcString)
{
uint16_t bakx, baky; /* 当前行行首坐标,换行时 x 回到 bakx */
uint32_t unicode; /* utf8_next 解析出的当前字符码点 */
uint16_t index = 0; /* 当前字符在 pcString 中的字节下标 */
uint8_t n = 0;
bakx = x;
baky = y;
/* 起始坐标合法性:至少能放下一个 ASCII 宽、一行汉字高 */
if (x > LCD_SIZE_X - g_tCVsr.wASCIIFontWidth)
return -1;
if (y >= LCD_SIZE_Y - g_tCVsr.wGBFontHeight)
return -2;
while (pcString[index] != 0x0)
{
/* 解析当前字符n = 占用字节数1=ASCII2/3=多字节unicode = 码点 */
n = utf8_next(pcString + index, &unicode);
if (n <= 0)
break;
/* ---------- 多字节字符如汉字n=2 或 3用 UTF-8 字库绘制 ---------- */
if (n > 1)
{
/* 本行剩余宽度放不下一个汉字:向后查找换行符并换行 */
if (x > LCD_SIZE_X - g_tCVsr.wGBFontWidth)
{
/*擦除本行剩余部分*/
for(uint16_t j = y; j < y + g_tCVsr.wGBFontHeight; j++)
{
for(uint16_t i = x; i < LCD_SIZE_X; i++)
{
Lcd_SetPixel(i, j, g_tCVsr.wBackColor);
}
}
return -1;
}
else
{
Lcd_Pub_UTF8(x, y, unicode);
x += g_tCVsr.wGBFontWidth;
}
}
/* ---------- 单字节字符ASCIIn=1 ---------- */
else
{
if (unicode == 0x0a)
{
/* 换行符x 回到行首y 下移一行高度 + 行距 */
x = bakx;
y += g_tCVsr.wGBFontHeight + g_tDspAttrib.rowSpace;
}
else
{
/* 控制字符0x00..0x0F 且非 0x0A不绘制仅跳过 */
if (unicode >= 0x10)
{
/* 本行放不下一个 ASCII 时,向后查找换行符并换行 */
if (x > LCD_SIZE_X - g_tCVsr.wASCIIFontWidth)
{
/*擦除本行剩余部分*/
for(uint16_t j = y; j < y + g_tCVsr.wASCIIFontHeight; j++)
{
for(uint16_t i = x; i < LCD_SIZE_X; i++)
{
Lcd_SetPixel(i, j, g_tCVsr.wBackColor);
}
}
return -1;
}
else
{
Lcd_Pub_Ascii(x, y, (uint8_t)unicode);
x += g_tCVsr.wASCIIFontWidth;
}
}
}
}
index += n; /* 已处理完当前字符,下标移到下一字符 */
}
return 0;
}
int8_t Lcd_ShowTest(uint16_t x, uint16_t y, uint8_t *pcString)
{
return 0;
}

View File

@@ -1,63 +0,0 @@
#ifndef __LCD__H__
#define __LCD__H__
#include "../../include/types.h"
/* 单色液晶屏幕 160*160
一个 byte 代表一个像素点
0xFF 代表白色
0x00 代表黑色
*/
#define LCD_SIZE_X 160
#define LCD_SIZE_Y 160
#define LCD_LINE_SIZE LCD_SIZE_X
#define LCD_DISPLAYMEMORYSIZE (LCD_SIZE_X * LCD_SIZE_Y)
#define LCD_COLOR_WHITE 0xFF
#define LCD_COLOR_BLACK 0x00
typedef struct
{
uint8_t pwbyLCDMemory[LCD_DISPLAYMEMORYSIZE]; //定义显存
uint8_t wFontColor; // 字体颜色
uint8_t wBackColor; // 字符显示背景颜色
uint8_t wGBFontWidth; // 汉字字体宽度
uint8_t wGBFontHeight; // 汉字字体高度
uint8_t wASCIIFontWidth; // 字符字体宽度
uint8_t wASCIIFontHeight; // 字符字体高度
uint16_t *pwLibHZ; // 汉字库地址
uint8_t *pbyLibAscii; // ASCII库地址
} tagScreenControl;
/* 当前界面/显存lcd.c 中定义,供 remoteDisplay 等模块读取显存) */
extern tagScreenControl g_tCVsr;
typedef struct { // 显示属性数据结构
uint16_t style:1; // 显示方式 <0=正常显示; 1=返显>
uint16_t StringDirect:1; // 字符显示方向<0=横向显示, 1=竖向显示>
uint16_t fillZero:1; // 10进制数显示前面是否补0。0=不补0,1=补0;
uint16_t Rotate:1; // 字符是否旋转显示目前只支持逆时针转90度
uint16_t res:6; // 保留
uint16_t rowSpace:4; // 行距
}tagDspAttrib, *tagPDspAttrib;
void Lcd_Init(void);
void Lcd_SetPixel(uint16_t x, uint16_t y, uint8_t color);
uint16_t Lcd_GetPixel(uint16_t x, uint16_t y);
void Lcd_FillRect(uint16_t left_x, uint16_t top_y, uint16_t right_x, uint16_t bottom_y, uint32_t color);
void Lcd_MeiTouPic(uint16_t wYStart, uint16_t wWidth);
void Lcd_ButtonPush(uint16_t left_x, uint16_t top_y, uint16_t right_x, uint16_t bottom_y);
void Lcd_Invert(uint16_t wXstart, uint16_t wYstart, uint16_t wXend, uint16_t wYend);
void Lcd_LineH(uint16_t wXStart, uint16_t wXEnd, uint16_t wYStart, uint16_t wWidth, uint8_t color);
void Lcd_LineV(uint16_t wYStart, uint16_t wYEnd, uint16_t wXStart, uint16_t wWidth, uint8_t color);
uint8_t utf8_next(const unsigned char *utf8, uint32_t *out_unicode);
int8_t Lcd_ShowStr(uint16_t x, uint16_t y, uint8_t *pcString);
int8_t Lcd_ShowTest(uint16_t x, uint16_t y, uint8_t *pcString);
#endif

View File

@@ -1,7 +1,7 @@
#ifndef __ASCII_H__
#define __ASCII_H__
#include "../../include/types.h"
#include "types.h"
/* 8x8 ASCII 点阵表 */
extern const uint8_t g_abyASCII88[][8];
@@ -12,13 +12,10 @@ extern uint8_t g_abyASCII126[][12];
/* 16x8 ASCII 点阵表 */
extern uint8_t g_abyASCII168[][16];
#define UTF8_HZ12_BYTES_PER_CHAR 24
#define UTF8_HZ12_NUM_CHARS 7038
/* 按 Unicode 码点查找点阵,返回 24 字节指针,未找到返回 NULL */
const uint8_t* utf8_hz12_get(uint32_t unicode);
#endif /* ASCII_FONT_TABLES_H */

133
src/Drv/lcd/lcd.c Normal file
View File

@@ -0,0 +1,133 @@
/* -------------------------------------------------------------------------
* 文件名: lcd.c
* 模块职责:
* 提供 LCD 显存framebuffer的最小抽象接口
* 1) 初始化显存
* 2) 单像素写入
* 3) 单像素读取
* 4) 获取显存首地址(供上层批量处理)
*
* 设计说明:
* - 当前实现是“内存模拟 LCD”用一段线性数组表示屏幕像素。
* - 坐标系采用左上角原点:
* x: [0, LCD_SIZE_X-1],向右递增
* y: [0, LCD_SIZE_Y-1],向下递增
* - 像素索引映射关系:
* index = y * LCD_LINE_SIZE + x
* - 越界策略:
* 写操作:直接忽略(安全返回)
* 读操作:返回 0黑色/默认值)
* ------------------------------------------------------------------------- */
#include "lcd.h"
#include <string.h>
/* LCD 显存数组(模块私有):
* - static 限定作用域在本翻译单元内,避免外部直接访问破坏封装
* - 每个元素对应一个像素点,大小为 LCD_DISPLAYMEMORYSIZE
*/
static uint8_t lcd_memory[LCD_DISPLAYMEMORYSIZE];
/* -------------------------------------------------------------------------
* 函数名: Lcd_Init
* 功能:
* 初始化 LCD 显存,将所有像素清为 0黑色背景
*
* 实现细节:
* - 使用 memset 一次性清零整个显存,时间复杂度 O(N)。
* - sizeof(lcd_memory) 保证长度与数组实际大小一致,避免硬编码。
*
* 输入参数:
* 无
* 返回值:
* 无
* ------------------------------------------------------------------------- */
void Lcd_Init(void)
{
memset(lcd_memory, 0, sizeof(lcd_memory));
}
/* -------------------------------------------------------------------------
* 函数名: Lcd_SetPixel
* 功能:
* 设置指定坐标 (x, y) 的像素值。
*
* 参数:
* x - 像素横坐标
* y - 像素纵坐标
* color - 要写入的颜色值(单字节)
*
* 边界处理:
* - 若 x 或 y 越界,函数返回 LCD_ERR_OUT_OF_RANGE不做任何写入。
* - 这样可防止数组越界访问导致内存破坏,并允许上层检测错误。
*
* 说明:
* - 本函数对颜色值做白名单校验,仅允许 LCD_FONT / LCD_BACK。
* 返回值:
* - LCD_OK (0): 写入成功
* - LCD_ERR_OUT_OF_RANGE (-1): 坐标越界
* - LCD_ERR_INVALID_COLOR (-2): 非法颜色值
* ------------------------------------------------------------------------- */
int8_t Lcd_SetPixel(uint16_t x, uint16_t y, uint8_t color)
{
/* 坐标越界保护:超出屏幕范围返回错误码 */
if (x >= LCD_SIZE_X || y >= LCD_SIZE_Y)
{
return LCD_ERR_OUT_OF_RANGE;
}
/* 颜色值白名单:仅允许前景色与背景色 */
if (color != LCD_FONT && color != LCD_BACK)
{
return LCD_ERR_INVALID_COLOR;
}
/* 二维坐标转线性索引并写入像素 */
lcd_memory[y * LCD_LINE_SIZE + x] = color;
return LCD_OK;
}
/* -------------------------------------------------------------------------
* 函数名: Lcd_GetPixel
* 功能:
* 读取指定坐标 (x, y) 的像素值。
*
* 参数:
* x - 像素横坐标
* y - 像素纵坐标
*
* 返回值:
* - 0~255: 合法坐标对应的像素值
* - LCD_ERR_OUT_OF_RANGE (-1): 坐标越界
*
* 设计考虑:
* - 使用错误码让调用方可显式区分“黑色像素(0)”与“越界读取(-1)”。
* - 与 Lcd_SetPixel 的错误码风格保持一致,便于统一测试和上层处理。
* ------------------------------------------------------------------------- */
int16_t Lcd_GetPixel(uint16_t x, uint16_t y)
{
/* 越界读取保护:返回错误码 */
if (x >= LCD_SIZE_X || y >= LCD_SIZE_Y)
{
return LCD_ERR_OUT_OF_RANGE;
}
/* 二维坐标转线性索引并返回像素值 */
return lcd_memory[y * LCD_LINE_SIZE + x];
}
/* -------------------------------------------------------------------------
* 函数名: Lcd_GetFrameBuffer
* 功能:
* 返回 LCD 显存首地址,供上层进行批量处理(如反显、整屏传输等)。
*
* 返回值:
* uint8_t* - 指向 lcd_memory[0] 的指针
*
* 使用约束:
* - 调用方应保证访问范围不超过 LCD_DISPLAYMEMORYSIZE。
* - 该接口暴露底层内存,适合高性能场景,但需调用方自律维护边界。
* ------------------------------------------------------------------------- */
uint8_t* Lcd_GetFrameBuffer(void)
{
return lcd_memory;
}

31
src/Drv/lcd/lcd.h Normal file
View File

@@ -0,0 +1,31 @@
#ifndef __LCD__H__
#define __LCD__H__
#include "types.h"
/* 单色液晶屏幕 160*160
一个 byte 代表一个像素点
0xFF 代表白色
0x00 代表黑色
*/
#define LCD_SIZE_X 160
#define LCD_SIZE_Y 160
#define LCD_LINE_SIZE LCD_SIZE_X
#define LCD_DISPLAYMEMORYSIZE (LCD_SIZE_X * LCD_SIZE_Y)
#define LCD_COLOR_WHITE 0xFF
#define LCD_COLOR_BLACK 0x00
#define LCD_FONT LCD_COLOR_WHITE
#define LCD_BACK LCD_COLOR_BLACK
#define LCD_OK 0
#define LCD_ERR_OUT_OF_RANGE -1
#define LCD_ERR_INVALID_COLOR -2
void Lcd_Init(void);
int8_t Lcd_SetPixel(uint16_t x, uint16_t y, uint8_t color);
int16_t Lcd_GetPixel(uint16_t x, uint16_t y);
uint8_t* Lcd_GetFrameBuffer(void);
#endif

225
src/Drv/lcd/lcd_draw.c Normal file
View File

@@ -0,0 +1,225 @@
/* -------------------------------------------------------------------------
* 文件名: lcd_draw.c
* 模块职责:
* 提供基于像素接口的基础绘图能力,包含:
* 1) 矩形填充Lcd_FillRect
* 2) 水平线绘制Lcd_LineH
* 3) 垂直线绘制Lcd_LineV
* 4) 区域反显Lcd_Invert
*
* 设计说明:
* - 本模块是 lcd.c 的上层,复用 Lcd_SetPixel/Lcd_GetFrameBuffer。
* - 当前模块在入口做参数合法性检查,发生越界/非法颜色时直接返回错误码。
* - 坐标区间语义在不同函数中不同,使用时需特别注意(见各函数注释)。
* ------------------------------------------------------------------------- */
#include "lcd_draw.h"
#ifdef DEBUG
#include <stdio.h>
#endif
static int8_t Lcd_ColorCheck(uint32_t color)
{
if ((color != LCD_FONT) && (color != LCD_BACK)) {
return LCD_ERR_INVALID_COLOR;
}
return LCD_OK;
}
/* -------------------------------------------------------------------------
* 函数名: Lcd_FillRect
* 功能:
* 用指定颜色填充一个矩形区域。
*
* 参数:
* left_x - 左上角 x包含
* top_y - 左上角 y包含
* right_x - 右下角 x包含
* bottom_y - 右下角 y包含
* color - 填充颜色
*
* 区间语义:
* - 采用“闭区间”遍历:
* x: [left_x, right_x]
* y: [top_y, bottom_y]
*
* 注意:
* - 若 right_x < left_x 或 bottom_y < top_y当前实现不会进入循环或可能因无符号导致异常遍历风险
* 调用方应保证坐标顺序合法。
* 返回值:
* - LCD_OK: 参数合法且绘制完成
* - LCD_ERR_OUT_OF_RANGE: 坐标越界或顺序非法
* - LCD_ERR_INVALID_COLOR: 颜色非法
* ------------------------------------------------------------------------- */
int8_t Lcd_FillRect(uint16_t left_x, uint16_t top_y, uint16_t right_x, uint16_t bottom_y, uint32_t color)
{
if (Lcd_ColorCheck(color) != LCD_OK) return LCD_ERR_INVALID_COLOR;
if ((right_x < left_x) || (bottom_y < top_y)) return LCD_ERR_OUT_OF_RANGE;
if ((right_x >= LCD_SIZE_X) || (bottom_y >= LCD_SIZE_Y)) return LCD_ERR_OUT_OF_RANGE;
for (uint16_t y = top_y; y <= bottom_y; y++) {
for (uint16_t x = left_x; x <= right_x; x++) {
Lcd_SetPixel(x, y, color);
}
}
return LCD_OK;
}
/* 宏: M_GuiSwap
* 作用:
* 使用异或交换法交换两个同类型变量的值。
* 使用场景:
* 在起止坐标反向时进行修正(如 xStart > xEnd
* 注意:
* 该写法要求 a 与 b 指向不同变量;若传入同一变量会把值清零。
*/
#define M_GuiSwap(a, b) { a ^= b; b ^= a; a ^= b; }
/* -------------------------------------------------------------------------
* 函数名: Lcd_LineH
* 功能:
* 绘制水平线(可设置线宽)。
*
* 参数:
* wXStart - 起始 x
* wXEnd - 结束 x
* wYStart - 起始 y
* wWidth - 线宽(沿 y 方向扩展)
* color - 线条颜色
*
* 区间语义:
* - x 方向采用半开区间 [wXStart, wXEnd)
* - y 方向采用半开区间 [wYStart, wYStart + wWidth)
*
* 说明:
* - 当 wXEnd < wXStart 时会自动交换两者,支持反向输入。
* 返回值:
* - LCD_OK: 参数合法且绘制完成
* - LCD_ERR_OUT_OF_RANGE: 坐标越界
* - LCD_ERR_INVALID_COLOR: 颜色非法
* ------------------------------------------------------------------------- */
int8_t Lcd_LineH(uint16_t wXStart, uint16_t wXEnd, uint16_t wYStart, uint16_t wWidth, uint8_t color)
{
uint32_t wYEnd32;
uint16_t wYEnd;
if (Lcd_ColorCheck(color) != LCD_OK) return LCD_ERR_INVALID_COLOR;
if (wXEnd < wXStart) {
M_GuiSwap(wXEnd, wXStart);
}
wYEnd32 = (uint32_t)wYStart + (uint32_t)wWidth;
if (wXStart >= LCD_SIZE_X) return LCD_ERR_OUT_OF_RANGE;
if (wXEnd > LCD_SIZE_X) return LCD_ERR_OUT_OF_RANGE; /* 半开区间end 可等于边界 */
if (wYStart >= LCD_SIZE_Y) return LCD_ERR_OUT_OF_RANGE;
if (wYEnd32 > LCD_SIZE_Y) return LCD_ERR_OUT_OF_RANGE; /* 半开区间end 可等于边界 */
wYEnd = (uint16_t)wYEnd32;
for (uint16_t x = wXStart; x < wXEnd; x++) {
for (uint16_t y = wYStart; y < wYEnd; y++) {
Lcd_SetPixel(x, y, color);
}
}
return LCD_OK;
}
/* -------------------------------------------------------------------------
* 函数名: Lcd_LineV
* 功能:
* 绘制垂直线(可设置线宽)。
*
* 参数:
* wYStart - 起始 y
* wYEnd - 结束 y
* wXStart - 起始 x
* wWidth - 线宽(沿 x 方向扩展)
* color - 线条颜色
*
* 区间语义:
* - y 方向采用半开区间 [wYStart, wYEnd)
* - x 方向采用半开区间 [wXStart, wXStart + wWidth)
*
* 说明:
* - 当 wYEnd < wYStart 时会自动交换两者,支持反向输入。
*
* 返回值:
* - LCD_OK: 参数合法且绘制完成
* - LCD_ERR_OUT_OF_RANGE: 坐标越界
* - LCD_ERR_INVALID_COLOR: 颜色非法
* ------------------------------------------------------------------------- */
int8_t Lcd_LineV(uint16_t wYStart, uint16_t wYEnd, uint16_t wXStart, uint16_t wWidth, uint8_t color)
{
uint32_t wXEnd32;
uint16_t wXEnd;
if (Lcd_ColorCheck(color) != LCD_OK) return LCD_ERR_INVALID_COLOR;
if (wYEnd < wYStart) {
M_GuiSwap(wYEnd, wYStart);
}
wXEnd32 = (uint32_t)wXStart + (uint32_t)wWidth;
if (wYStart >= LCD_SIZE_Y) return LCD_ERR_OUT_OF_RANGE;
if (wYEnd > LCD_SIZE_Y) return LCD_ERR_OUT_OF_RANGE; /* 半开区间end 可等于边界 */
if (wXStart >= LCD_SIZE_X) return LCD_ERR_OUT_OF_RANGE;
if (wXEnd32 > LCD_SIZE_X) return LCD_ERR_OUT_OF_RANGE; /* 半开区间end 可等于边界 */
wXEnd = (uint16_t)wXEnd32;
for (uint16_t x = wXStart; x < wXEnd; x++) {
for (uint16_t y = wYStart; y < wYEnd; y++) {
Lcd_SetPixel(x, y, color);
}
}
return LCD_OK;
}
/* 宏: M_Max / M_Min
* 作用:
* 计算两个值的较大/较小值,用于将任意方向输入规范化为最小/最大边界。
*/
#define M_Max(a, b) (((a) > (b)) ? (a) : (b))
#define M_Min(a, b) (((a) < (b)) ? (a) : (b))
/* -------------------------------------------------------------------------
* 函数名: Lcd_Invert
* 功能:
* 对指定矩形区域像素执行按位取反,实现反显效果。
*
* 参数:
* wXstart, wYstart - 第一个角点
* wXend, wYend - 第二个角点
*
* 区间语义:
* - 先通过 min/max 规范化坐标后,按半开区间处理:
* x: [xMin, xMax)
* y: [yMin, yMax)
*
* 边界处理:
* - 若任一端点超出屏幕范围(> LCD_SIZE_X / > LCD_SIZE_Y函数返回错误码。
* - 这里通过直接访问 framebuffer 提升效率,不逐点调用 Lcd_SetPixel。
*
* 注意:
* - 本函数使用线性地址 `y * LCD_SIZE_X + x` 访问显存,默认与 lcd.c 的布局一致。
* 返回值:
* - LCD_OK: 参数合法且处理完成
* - LCD_ERR_OUT_OF_RANGE: 坐标越界
* ------------------------------------------------------------------------- */
int8_t Lcd_Invert(uint16_t wXstart, uint16_t wYstart, uint16_t wXend, uint16_t wYend)
{
uint16_t xMin, xMax, yMin, yMax;
uint8_t wColor;
uint8_t *framebuffer = Lcd_GetFrameBuffer();
if ((wXstart > LCD_SIZE_X) || (wXend > LCD_SIZE_X)) return LCD_ERR_OUT_OF_RANGE;
if ((wYstart > LCD_SIZE_Y) || (wYend > LCD_SIZE_Y)) return LCD_ERR_OUT_OF_RANGE;
xMin = M_Min(wXstart, wXend);
yMin = M_Min(wYstart, wYend);
xMax = M_Max(wXstart, wXend);
yMax = M_Max(wYstart, wYend);
for (uint16_t y = yMin; y < yMax; y++) {
for (uint16_t x = xMin; x < xMax; x++) {
wColor = framebuffer[y * LCD_SIZE_X + x];
framebuffer[y * LCD_SIZE_X + x] = (uint8_t)~wColor;
}
}
return LCD_OK;
}

13
src/Drv/lcd/lcd_draw.h Normal file
View File

@@ -0,0 +1,13 @@
#ifndef __LCD_DRAW_H__
#define __LCD_DRAW_H__
#include "lcd.h"
int8_t Lcd_FillRect(uint16_t left_x, uint16_t top_y, uint16_t right_x, uint16_t bottom_y, uint32_t color);
int8_t Lcd_Invert(uint16_t wXstart, uint16_t wYstart, uint16_t wXend, uint16_t wYend);
int8_t Lcd_LineH(uint16_t wXStart, uint16_t wXEnd, uint16_t wYStart, uint16_t wWidth, uint8_t color);
int8_t Lcd_LineV(uint16_t wYStart, uint16_t wYEnd, uint16_t wXStart, uint16_t wWidth, uint8_t color);
#endif

244
src/Drv/lcd/lcd_text.c Normal file
View File

@@ -0,0 +1,244 @@
/* -------------------------------------------------------------------------
* 文件名: lcd_text.c
* 模块职责:
* 提供文本渲染与 UTF-8 解析能力,包含:
* 1) UTF-8 单字符解码utf8_next
* 2) ASCII 字符绘制(内部函数 Lcd_Pub_Ascii
* 3) 中文位图绘制(内部函数 Lcd_Pub_UTF8
* 4) 字符串渲染入口Lcd_ShowStr
*
* 设计说明:
* - 显示像素最终通过 Lcd_SetPixel 写入显存。
* - 当前字体配置固定为:
* ASCII: 7x12
* 中文: 13x12来自 utf8_hz12_get
* - 对越界场景采用“显式错误返回”,便于上层与测试捕获。
* ------------------------------------------------------------------------- */
#include "lcd_text.h"
#include <stddef.h>
#include "ascii.h"
#include "lcd_draw.h"
static textConfig text_cfg = {
.wGBFontWidth = 13,
.wGBFontHeight = 12,
.wASCIIFontWidth = 7,
.wASCIIFontHeight = 12,
.pbyLibAscii = g_abyASCII126[0],
.rowSpace = 2
};
/* -------------------------------------------------------------------------
* 函数名: utf8_next
* 功能:
* 从 UTF-8 字节流当前位置解析“一个字符”,输出 Unicode 码点与字节长度。
*
* 参数:
* utf8 - 指向当前待解析字节
* out_unicode - 输出解析得到的 Unicode 码点
*
* 返回值:
* 0 : 解析失败/字符串结束
* 1 : ASCII 单字节
* 2 : 两字节 UTF-8
* 3 : 三字节 UTF-8
*
* 注意:
* - 当前实现不支持 4 字节 UTF-8如 emoji
* - 对截断序列(缺少后续字节)返回 0。
* ------------------------------------------------------------------------- */
uint8_t utf8_next(const unsigned char *utf8, uint32_t *out_unicode)
{
unsigned char c = utf8[0];
if (c == 0) {
*out_unicode = 0;
return 0;
}
if (c < 0x80) {
*out_unicode = c;
return 1;
}
if ((c & 0xE0) == 0xC0) {
if (utf8[1] == 0) return 0;
*out_unicode = (uint32_t)((c & 0x1F) << 6 | (utf8[1] & 0x3F));
return 2;
}
if ((c & 0xF0) == 0xE0) {
if (utf8[1] == 0 || utf8[2] == 0) return 0;
*out_unicode = (uint32_t)((c & 0x0F) << 12 | (utf8[1] & 0x3F) << 6 | (utf8[2] & 0x3F));
return 3;
}
*out_unicode = 0;
return 0;
}
/* -------------------------------------------------------------------------
* 函数名: Lcd_Pub_Ascii内部
* 功能:
* 按 ASCII 点阵库将单个 ASCII 字符绘制到指定坐标。
*
* 参数:
* x, y - 字符左上角坐标
* byAscii - ASCII 码值
*
* 返回值:
* LCD_OK / LCD_ERR_OUT_OF_RANGE
* ------------------------------------------------------------------------- */
static int8_t Lcd_Pub_Ascii(uint16_t x, uint16_t y, uint8_t byAscii)
{
const textConfig *cfg = &text_cfg;
uint8_t i, j;
uint8_t byLine, *pbyFontLib;
/* 按字库行高定位字符位图起始地址 */
pbyFontLib = &cfg->pbyLibAscii[byAscii * cfg->wASCIIFontHeight];
if ((x + cfg->wASCIIFontWidth) > LCD_SIZE_X) return LCD_ERR_OUT_OF_RANGE;
if ((y + cfg->wASCIIFontHeight) > LCD_SIZE_Y) return LCD_ERR_OUT_OF_RANGE;
for (j = 0; j < cfg->wASCIIFontHeight; j++) {
byLine = pbyFontLib[j];
for (i = 0; i < cfg->wASCIIFontWidth; i++) {
uint8_t bit_on = ((byLine << i) & 0x80) != 0;
uint16_t px, py;
uint8_t color = bit_on ? LCD_FONT : LCD_BACK;
px = x + i;
py = y + j;
if (Lcd_SetPixel(px, py, color) != LCD_OK) {
return LCD_ERR_OUT_OF_RANGE;
}
}
}
return LCD_OK;
}
/* -------------------------------------------------------------------------
* 函数名: Lcd_Pub_UTF8内部
* 功能:
* 将一个 Unicode 字符(中文)按 13x12 位图绘制到指定坐标。
*
* 参数:
* x, y - 字符左上角坐标
* unicode - Unicode 码点
*
* 返回值:
* LCD_OK / LCD_ERR_OUT_OF_RANGE
*
* 注意:
* - 若字库中找不到该字符位图,当前实现按“空操作成功”处理(返回 LCD_OK
* ------------------------------------------------------------------------- */
static int8_t Lcd_Pub_UTF8(uint16_t x, uint16_t y, uint32_t unicode)
{
const textConfig *cfg = &text_cfg;
const uint8_t *bitmap = utf8_hz12_get(unicode);
uint16_t word = 0;
if ((x + cfg->wGBFontWidth) > LCD_SIZE_X) return LCD_ERR_OUT_OF_RANGE;
if ((y + cfg->wGBFontHeight) > LCD_SIZE_Y) return LCD_ERR_OUT_OF_RANGE;
if (bitmap == NULL) return LCD_OK;
for (uint8_t j = 0; j < cfg->wGBFontHeight; j++) {
word = (uint16_t)((bitmap[j * 2] << 8) | bitmap[j * 2 + 1]);
for (uint8_t i = 0; i < cfg->wGBFontWidth; i++) {
if ((word >> (15 - i)) & 1) {
if (Lcd_SetPixel(x + i, y + j, LCD_FONT) != LCD_OK) {
return LCD_ERR_OUT_OF_RANGE;
}
} else {
if (Lcd_SetPixel(x + i, y + j, LCD_BACK) != LCD_OK) {
return LCD_ERR_OUT_OF_RANGE;
}
}
}
}
return LCD_OK;
}
/* -------------------------------------------------------------------------
* 函数名: Lcd_ShowStr
* 功能:
* 在指定坐标绘制 UTF-8 字符串,支持 ASCII、中文与换行。
*
* 参数:
* x, y - 首字符左上角坐标
* pcString - UTF-8 字符串(以 '\0' 结尾)
*
* 返回值:
* 0 : 绘制成功
* -1 : x 方向越界
* -2 : y 方向越界
*
* 说明:
* - 与历史行为兼容,保留 -1/-2 两种错误码语义。
* - 内部会在渲染前做边界预判,并在底层写像素失败时转化为越界错误返回。
* ------------------------------------------------------------------------- */
int8_t Lcd_ShowStr(uint16_t x, uint16_t y, uint8_t *pcString)
{
const textConfig *cfg = &text_cfg;
uint16_t bakx = x;
uint32_t unicode;
uint16_t index = 0;
uint8_t n = 0;
if (x > LCD_SIZE_X - cfg->wASCIIFontWidth) return -1;
if (y >= LCD_SIZE_Y - cfg->wGBFontHeight) return -2;
while (pcString[index] != 0x0) {
n = utf8_next(pcString + index, &unicode);
if (n <= 0) break;
if (n > 1) {
/* 中文字符渲染路径13x12 */
if (x > LCD_SIZE_X - cfg->wGBFontWidth) {
for (uint16_t j = y; j < y + cfg->wGBFontHeight; j++) {
for (uint16_t i = x; i < LCD_SIZE_X; i++) {
Lcd_SetPixel(i, j, LCD_BACK);
}
}
return -1;
} else {
if (Lcd_Pub_UTF8(x, y, unicode) != LCD_OK) {
return -2;
}
x += cfg->wGBFontWidth;
}
} else {
/* ASCII / 控制字符渲染路径 */
if (unicode == 0x0a) {
/* 换行x 回到行首y 下移一行(字高 + 行距) */
x = bakx;
y += cfg->wGBFontHeight + cfg->rowSpace;
if (y >= LCD_SIZE_Y - cfg->wGBFontHeight) {
return -2;
}
} else if (unicode >= 0x10) {
if (x > LCD_SIZE_X - cfg->wASCIIFontWidth) {
for (uint16_t j = y; j < y + cfg->wASCIIFontHeight; j++) {
for (uint16_t i = x; i < LCD_SIZE_X; i++) {
Lcd_SetPixel(i, j, LCD_BACK);
}
}
return -1;
} else {
if (Lcd_Pub_Ascii(x, y, (uint8_t)unicode) != LCD_OK) {
return -1;
}
x += cfg->wASCIIFontWidth;
}
}
}
index += n;
}
return 0;
}
/* -------------------------------------------------------------------------
* 函数名: Lcd_ShowTest
* 功能:
* 预留测试接口(当前为桩实现)。
* ------------------------------------------------------------------------- */
int8_t Lcd_ShowTest(uint16_t x, uint16_t y, uint8_t *pcString)
{
(void)x;
(void)y;
(void)pcString;
return 0;
}

19
src/Drv/lcd/lcd_text.h Normal file
View File

@@ -0,0 +1,19 @@
#ifndef __LCD_TEXT_H__
#define __LCD_TEXT_H__
#include "lcd.h"
typedef struct {
uint8_t wGBFontWidth;
uint8_t wGBFontHeight;
uint8_t wASCIIFontWidth;
uint8_t wASCIIFontHeight;
uint8_t *pbyLibAscii;
uint16_t rowSpace;
} textConfig;
uint8_t utf8_next(const unsigned char *utf8, uint32_t *out_unicode);
int8_t Lcd_ShowStr(uint16_t x, uint16_t y, uint8_t *pcString);
int8_t Lcd_ShowTest(uint16_t x, uint16_t y, uint8_t *pcString);
#endif

View File

@@ -1,8 +1,11 @@
#include "menu.h"
#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "lcd.h"
#include "lcd/lcd.h"
#include "lcd/lcd_draw.h"
#include "lcd/lcd_text.h"
#include "display.h"
#include "key.h"
@@ -99,6 +102,98 @@ tagMenuCtrl g_tMenuCtrl; /* 菜单全局
tagMenuItem g_tMenuItem[300]; // 菜单链表空间定义
#define MENU_GUI_SWAP(a, b) \
do { \
(a) ^= (b); \
(b) ^= (a); \
(a) ^= (b); \
} while (0)
static void Menu_Line(uint16_t wXstart, uint16_t wYstart, uint16_t wXend, uint16_t wYend, uint16_t wWidth, uint8_t color)
{
uint16_t wDX, wDY, wSignY, wSignX, wDecision;
uint16_t wCurx, wCury, wNextx, wNexty, wPY, wPX;
if (wYstart == wYend) {
Lcd_LineH(wXstart, wXend, wYstart, wWidth, color);
return;
}
if (wXstart == wXend) {
Lcd_LineV(wYstart, wYend, wXstart, wWidth, color);
return;
}
wDX = (uint16_t)abs((int)wXstart - (int)wXend);
wDY = (uint16_t)abs((int)wYstart - (int)wYend);
if (((wDX >= wDY && (wXstart > wXend)) || ((wDY > wDX) && (wYstart > wYend)))) {
MENU_GUI_SWAP(wXend, wXstart);
MENU_GUI_SWAP(wYend, wYstart);
}
wSignY = (wYend - wYstart) / wDY;
wSignX = (wXend - wXstart) / wDX;
if (wDX >= wDY) {
wCurx = wXstart;
wCury = wYstart;
wNextx = wXend;
wNexty = wYend;
wDecision = (wDX >> 1);
for (; wCurx <= wNextx;) {
if (wDecision >= wDX) {
wDecision -= wDX;
wCury += wSignY;
wNexty -= wSignY;
}
for (wPY = wCury - wWidth / 2; wPY <= wCury + wWidth / 2; wPY++) {
Lcd_SetPixel(wCurx, wPY, color);
}
for (wPY = wNexty - wWidth / 2; wPY <= wNexty + wWidth / 2; wPY++) {
Lcd_SetPixel(wNextx, wPY, color);
}
wCurx++;
wNextx--;
wDecision += wDY;
}
} else {
wCurx = wXstart;
wCury = wYstart;
wNextx = wXend;
wNexty = wYend;
wDecision = (wDY >> 1);
for (; wCury <= wNexty;) {
if (wDecision >= wDY) {
wDecision -= wDY;
wCurx += wSignX;
wNextx -= wSignX;
}
for (wPX = wCurx - wWidth / 2; wPX <= wCurx + wWidth / 2; wPX++) {
Lcd_SetPixel(wPX, wCury, color);
}
for (wPX = wNextx - wWidth / 2; wPX <= wNextx + wWidth / 2; wPX++) {
Lcd_SetPixel(wPX, wNexty, color);
}
wCury++;
wNexty--;
wDecision += wDX;
}
}
}
static void Menu_MeiTouPic(uint16_t wYStart, uint16_t wWidth)
{
Lcd_LineH(16, 144, wYStart, wWidth, LCD_FONT);
Menu_Line(8, wYStart - 8, 16, wYStart, wWidth, LCD_FONT);
Menu_Line(144, wYStart, 152, wYStart - 8, wWidth, LCD_FONT);
}
static void Menu_ButtonPush(uint16_t left_x, uint16_t top_y, uint16_t right_x, uint16_t bottom_y)
{
Lcd_LineH(left_x, right_x, top_y, 1, LCD_FONT);
Lcd_LineV(top_y, bottom_y, left_x, 1, LCD_FONT);
Lcd_LineH(left_x, right_x, bottom_y, 1, LCD_FONT);
Lcd_LineV(top_y, bottom_y, right_x, 1, LCD_FONT);
}
void Menu_0LevelNumCal(void)
{
tagPMenuModel ptModelIndex; /* 当前遍历到的菜单表项指针 */
@@ -614,17 +709,17 @@ void Menu_PositionCal(tagPMenuItem ptMenuHead)
{
for (uint16_t x = left_x; x < right_x; x++)
{
Lcd_SetPixel(x, y, g_tCVsr.wBackColor); /* 设置按钮内部像素为前景色,实现实心效果 */
Lcd_SetPixel(x, y, LCD_BACK); /* 设置按钮内部像素为前景色,实现实心效果 */
}
}
/* 绘制上边框:从 (left_x, top_y) 到 (right_x, top_y),线宽 1 像素 */
Lcd_LineH(left_x, right_x, top_y, 1, g_tCVsr.wFontColor);
Lcd_LineH(left_x, right_x, top_y, 1, LCD_FONT);
/* 绘制左边框:从 (left_x, top_y) 到 (left_x, bottom_y),线宽 1 像素 */
Lcd_LineV(top_y, bottom_y, left_x, 1, g_tCVsr.wFontColor);
Lcd_LineV(top_y, bottom_y, left_x, 1, LCD_FONT);
/* 绘制下边框:从 (left_x, bottom_y) 到 (right_x+1, bottom_y),稍微向右多画 1 像素防止边界漏点 */
Lcd_LineH(left_x, right_x + 1, bottom_y, 1, g_tCVsr.wFontColor);
Lcd_LineH(left_x, right_x + 1, bottom_y, 1, LCD_FONT);
/* 绘制右边框:从 (right_x, top_y) 到 (right_x, bottom_y),线宽 1 像素 */
Lcd_LineV(top_y, bottom_y, right_x, 1, g_tCVsr.wFontColor);
Lcd_LineV(top_y, bottom_y, right_x, 1, LCD_FONT);
}
/*
这是分页的逻辑,需要保存,不能删除
@@ -656,7 +751,7 @@ else if (byMenuNum > 20)
byFirstPos = 1;
wPosY = wEPosY - LINE_HEIGHT;
Lcd_ButtonPush(wSPosX + 3, wPosY - 2, wEPosX - 4, wPosY + 14);
Menu_ButtonPush(wSPosX + 3, wPosY - 2, wEPosX - 4, wPosY + 14);
Lcd_ShowStr(wPosX, wPosY, (uint8_t*)"↓"); // 第一页尾显示↓
}
else if ((byMenuPos < (byMaxNum * 2 - 2)) && (byPage > 2)) // 当前位置在第二页
@@ -667,12 +762,12 @@ else if (byMenuNum > 20)
byFirstPos = byMaxNum;
wPosY = wSPosY + 2;
Lcd_ButtonPush(wSPosX + 3, wPosY - 1, wEPosX - 4, wPosY + 13);
Menu_ButtonPush(wSPosX + 3, wPosY - 1, wEPosX - 4, wPosY + 13);
Lcd_ShowStr(wPosX, wPosY, (uint8_t*)"↑"); // 第二页头显示↑
wPosY = wEPosY - LINE_HEIGHT;
Lcd_ButtonPush(wSPosX + 3, wPosY - 2, wEPosX - 4, wPosY + 14);
Menu_ButtonPush(wSPosX + 3, wPosY - 2, wEPosX - 4, wPosY + 14);
Lcd_ShowStr(wPosX, wPosY, (uint8_t*)"↓"); // 第一页尾显示↓
}
else
@@ -690,7 +785,7 @@ else if (byMenuNum > 20)
}
wPosY = wSPosY + 2;
Lcd_ButtonPush(wSPosX + 3, wPosY - 1, wEPosX - 4, wPosY + 13);
Menu_ButtonPush(wSPosX + 3, wPosY - 1, wEPosX - 4, wPosY + 13);
Lcd_ShowStr(wPosX, wPosY, (uint8_t*)"↑"); // 第二页头显示↑
}
@@ -809,15 +904,15 @@ void Menu_Show_Other(uint8_t bylevel)
* 说明: 1. 所有 0 级菜单在 X 方向上按等间距分布
* 2. 每次循环仅在菜单项仍在屏幕可见区域时才绘制对应标题文本
* 3. 顶部 0~32 像素区域作为 0 级菜单标题栏背景,会被统一清屏并重绘
* 4. 调用 Lcd_MeiTouPic 绘制“眉头”装饰线条,增强标题栏的视觉效果
* 4. 调用 Menu_MeiTouPic 绘制“眉头”装饰线条,增强标题栏的视觉效果
* 5. 示例中固定显示 “当前模式: 无模式”,后续可替换为实际运行模式文本
*****************************************************************************/
void Menu_Show_0Level()
{
/* 先清除顶部 0~32 像素高度区域,作为 0 级菜单标题栏背景 */
Lcd_FillRect(0, 0, LCD_SIZE_X, 32, g_tCVsr.wBackColor);
Lcd_FillRect(0, 0, LCD_SIZE_X, 32, LCD_BACK);
/* 绘制“眉头”装饰,使 0 级菜单栏更加立体 */
Lcd_MeiTouPic(16, 2 );
Menu_MeiTouPic(16, 2 );
Lcd_ShowStr(16, 20, (uint8_t*)"当前模式: 无模式" );
}
/******************************************************************************
@@ -853,7 +948,7 @@ void Menu_Show_Proc(void)
if (g_tMenuCtrl.pt0Level != g_tMenuCtrl.ptRoute[0])
{
bNeedFresh = 1; /* 需要整体刷新 */
Lcd_FillRect(0, MENU_YMIN, LCD_SIZE_X, MENU_YMAX, g_tCVsr.wBackColor); /* 清除 1~3 级菜单区域 */
Lcd_FillRect(0, MENU_YMIN, LCD_SIZE_X, MENU_YMAX, LCD_BACK); /* 清除 1~3 级菜单区域 */
g_tMenuCtrl.pt0Level = g_tMenuCtrl.ptRoute[0]; /* 更新 0 级路径起点 */
g_tMenuCtrl.ptCurBak = g_tMenuCtrl.ptCurrent; /* 更新备份指针 */
}
@@ -872,7 +967,7 @@ void Menu_Show_Proc(void)
#ifdef DEBUG
printf("退层:从 %d 级退回 %d 级\n", g_tMenuCtrl.ptCurBak->byClass, g_tMenuCtrl.ptCurrent->byClass);
#endif
Lcd_FillRect(0, MENU_YMIN, LCD_SIZE_X, MENU_YMAX, g_tCVsr.wBackColor); /* 擦除 1~3 级区域(保留 0~32 像素标题栏) */
Lcd_FillRect(0, MENU_YMIN, LCD_SIZE_X, MENU_YMAX, LCD_BACK); /* 擦除 1~3 级区域(保留 0~32 像素标题栏) */
bNeedFresh = 1;
}
g_tMenuCtrl.ptCurBak = g_tMenuCtrl.ptCurrent; /* 无论何种情况,更新备份指针为最新当前结点 */

View File

@@ -12,7 +12,7 @@
#include <stdint.h>
#include "lcd.h" /* MENU_YMAX 依赖 LCD_SIZE_Y */
#include "lcd/lcd.h" /* MENU_YMAX 依赖 LCD_SIZE_Y */
#define CN_HEIGHT 12 /* 菜单汉字高 */
#define CN_ROWSPACE 2 // 菜单字符行间隔

View File

@@ -1,4 +1,4 @@
/* ============================================================================
/* ============================================================================
* main.c - PC 端 HMI 菜单主程序
* 功能:菜单交互(主线程)+ TCP 服务器(独立线程),按 Q 退出
* ========================================================================== */

View File

@@ -1,4 +1,4 @@
/*
/*
* remoteDisplay.c - TCP 服务器线程实现
* 实现 RemoDispBus 协议:解析 remo_disp_server.py 的请求,返回 lcd 显存数据等。
* 帧格式: [TAG][cmd][len_hi][len_lo][data][crc]CRC = data 区逐字节异或低 8 位。
@@ -8,7 +8,7 @@
#include <stdio.h>
#include <string.h>
#include "Drv/lcd.h"
#include "Drv/lcd/lcd.h"
#include "TCP/tcp.h"
#include "Drv/key.h"
#include "remoteDisplay.h"
@@ -78,6 +78,7 @@ static int send_reply(int client, uint8_t cmd, const uint8_t* data, unsigned int
/* 处理 CMD_LCDMEM请求 data 为 4 字节大端起始地址;回复 [4B 地址][显存数据] */
static void handle_cmd_lcdmem(int client, const uint8_t* req_data, unsigned int req_len)
{
const uint8_t *framebuffer = Lcd_GetFrameBuffer();
unsigned int start_addr = 0;
if (req_len >= 4)
@@ -96,7 +97,7 @@ static void handle_cmd_lcdmem(int client, const uint8_t* req_data, unsigned int
{
unsigned int offset = start_addr;
copy_len = LCD_DISPLAYMEMORYSIZE - offset;
memcpy(payload + 4, g_tCVsr.pwbyLCDMemory + offset, copy_len);
memcpy(payload + 4, framebuffer + offset, copy_len);
}
else
{