diff --git a/.clangd b/.clangd deleted file mode 100644 index 49801fb..0000000 --- a/.clangd +++ /dev/null @@ -1,3 +0,0 @@ -CompileFlags: - Add: - - -Iinclude diff --git a/CMakeLists.txt b/CMakeLists.txt index 1682443..4f08c6b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,38 +1,56 @@ +# 指定 CMake 最低版本要求 cmake_minimum_required(VERSION 3.10) + +# 定义工程名(用于生成解决方案/项目名称) project(DTU_HMI) +# 统一使用 C99 标准编译 C 代码 set(CMAKE_C_STANDARD 99) -# 导出编译数据库,供 Cursor/clangd 使用 +# 导出 compile_commands.json,供 Cursor/clangd/静态分析工具使用 set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +# MSVC 编译器下强制使用 UTF-8 源文件编码,避免中文注释/字符串乱码 +# 同时关闭 C4819(部分环境即使是 UTF-8 文件仍会误报代码页告警) if(MSVC) - add_compile_options(/utf-8) + add_compile_options(/utf-8 /wd4819) endif() -# 可选:启用 DEBUG 宏,用于条件编译输出调试信息(cmake -DENABLE_DEBUG=ON ..) +# 可选开关:启用 DEBUG 宏,用于条件编译调试日志 +# 用法:cmake -DENABLE_DEBUG=ON .. option(ENABLE_DEBUG "Enable debug printf output" OFF) if(ENABLE_DEBUG) add_compile_definitions(DEBUG) endif() +# 主可执行程序 DTU-HMI 及其源码列表 add_executable(DTU-HMI src/main.c src/thread_utils.c src/remoteDisplay.c src/Drv/menu.c - src/Drv/lcd.c + src/Drv/lcd/lcd.c + src/Drv/lcd/lcd_draw.c + src/Drv/lcd/lcd_text.c src/Drv/key.c - src/Drv/Ascii.c + src/Drv/lcd/ascii.c src/Drv/display.c src/TCP/tcp.c ) +# 可执行程序头文件搜索路径 target_include_directories(DTU-HMI PRIVATE include src src/TCP) +# 按平台链接系统库: +# - Windows 需要 Winsock2(ws2_32) +# - Linux/macOS 使用 pthread if(WIN32) target_link_libraries(DTU-HMI PRIVATE ws2_32) else() target_link_libraries(DTU-HMI PRIVATE pthread) endif() +# 开启 CTest 测试支持,并加载 tests 子目录中的测试目标 +enable_testing() +add_subdirectory(tests) + diff --git a/README.md b/README.md index 1eed707..4e12bd0 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,12 @@ DTU-HMI/ │ └── Drv/ │ ├── menu.c/h │ ├── display.c/h -│ ├── lcd.c/h -│ ├── Ascii.c/h +│ ├── lcd/ +│ │ ├── lcd.c/h +│ │ ├── lcd_draw.c/h +│ │ ├── lcd_text.c/h +│ │ ├── text_codec.c/h +│ │ └── ascii.c/h │ └── utf8_hz12_data.c/h # 由脚本生成 └── build/ ``` @@ -657,8 +661,11 @@ void Menu_Route(void) 具体使用方式可以参考: -- `src/Drv/Ascii.c`:ASCII/字母数字字符绘制 -- `src/Drv/lcd.c`:LCD 基础绘制接口 +- `src/Drv/lcd/ascii.c`:ASCII/字母数字字符绘制 +- `src/Drv/lcd/lcd.c`:LCD 核心上下文与像素接口 +- `src/Drv/lcd/lcd_draw.c`:图元绘制接口 +- `src/Drv/lcd/lcd_text.c`:文本渲染接口 +- `src/Drv/lcd/text_codec.c`:UTF-8 解码接口 --- @@ -677,3 +684,45 @@ void Menu_Route(void) - 在关键流程(如 `Menu_Route`、`Menu_Show_Proc`、`remoteDisplay` 收发函数)增加 `#ifdef DEBUG` 包裹的日志 - 使用 GDB / VS 调试器在 `main.c`/`menu.c` 等处打断点,单步查看菜单树和坐标计算过程 + +--- + +## 11. 单元测试与门禁 + +### 11.1 运行单元测试 + +```powershell +# 1. 配置:生成构建文件(如 Makefile)到 build 目录 +cmake -S . -B build +# 2. 编译:真正编译代码 +cmake --build build +# 3. 进行测试 +# ctest 用 CTest 测试运行器(CMake 自带) +# --test-dir build 指定测试目录是 build。 +# -C Debug 指定运行 Debug 配置下编译出的测试程序。 +# --output-on-failure 只有测试失败时,才输出该测试的 stdout/stderr 详细信息 +ctest --test-dir build -C Debug --output-on-failure +``` + +当前已落地测试集合: + +- `test_smoke`:测试框架烟雾验证 +- `test_p0_remote_display`:`calc_crc`、`parse_frame` 协议解析核心 +- `test_p0_utf8_next`:UTF-8 解码边界场景 +- `test_p0_utf8_hz12_get`:汉字字库二分查找命中/未命中 +- `test_p1_key`:按键状态消费语义 +- `test_p1_lcd_basic`:像素读写、填充、反显 +- `test_p1_menu`:菜单长度计算、层级初始化 +- `test_p2_tcp_loopback`:TCP 本机回环收发(集成) + +### 11.2 质量门禁(建议) + +- 提交前最低门禁:`ctest --test-dir build -C Debug --output-on-failure` 必须全绿。 +- 新增或修改纯逻辑函数时,必须同时补充对应单元测试。 +- 修复缺陷时,必须附带回归测试用例,避免问题再次出现。 + +### 11.3 覆盖率目标(阶段性) + +- P0 核心模块(协议解析/字符解析)行覆盖率建议 >= 70%。 +- P0 核心模块分支覆盖率建议 >= 60%。 +- P1/P2 模块可按迭代逐步提升,不要求一次到位。 diff --git a/docs/系统架构设计文档.md b/docs/系统架构设计文档.md new file mode 100644 index 0000000..1563f61 --- /dev/null +++ b/docs/系统架构设计文档.md @@ -0,0 +1,244 @@ +# DTU-HMI 系统架构设计文档 + +## 1. 文档目的 + +本文档用于描述 `DTU-HMI` 工程的整体架构、核心模块职责、关键数据流、线程与通信模型、构建与测试体系,作为后续开发、联调、测试与维护的统一基线。 + +## 2. 系统概述 + +`DTU-HMI` 是一个基于纯 C 实现的 PC 端 HMI 菜单逻辑模拟系统,目标是复现现场 DTU 设备的人机界面行为。 +系统支持本地菜单交互与远程显示协议(RemoDispBus)通信,主要包含: + +- 菜单树构建与路由(多级菜单、同级循环导航) +- LCD 显存与绘制(160x160 单色像素缓冲) +- 按键输入(本地/远程按键注入) +- TCP 服务(远程显示数据交互) +- 跨平台线程与网络适配(Windows/Linux) + +## 3. 架构目标与设计原则 + +- **可移植性**:通过 `tcp.c`、`thread_utils.c` 封装平台差异。 +- **可维护性**:按模块职责划分(菜单/显示/网络/线程/输入)。 +- **可测试性**:优先抽取并覆盖纯逻辑函数,逐步推进集成测试。 +- **低耦合高内聚**:上层业务通过明确接口调用下层能力。 + +## 4. 系统分层架构 + +```text + +----------------------+ + | AppLayer | + | main.c | + +----------+-----------+ + | + +--------------+--------------+ + | | + v v + +---------+----------+ +---------+------------------+ + | MenuLayer | | RemoteDisplayLayer | + | menu.c | | remoteDisplay.c | + +----+-----------+---+ +-----+-----------+----------+ + | | | | + v v v v + +------+-----+ +--+---------+ +---+----+ +---+------------------+ + |DisplayLayer| |InputLayer | |Input | |PlatformLayer | + |lcd/Ascii/ | |key.c | |key.c | |tcp.c + thread_utils.c | + |display.c | +------------+ +-------+ +------------------------+ + +------------+ +``` + +### 4.1 应用层 + +- 文件:`src/main.c` +- 职责: + - 系统初始化(菜单、按键、TCP 线程) + - 主循环调度(菜单路由、周期显示刷新) + - 生命周期管理(线程退出、网络清理) + +### 4.2 菜单业务层 + +- 文件:`src/Drv/menu.c`、`src/Drv/menu.h` +- 职责: + - 基于静态菜单模型构建运行时菜单树 + - 处理按键驱动的菜单状态迁移 + - 执行菜单显示坐标计算与多级菜单渲染调度 + +### 4.3 显示层 + +- 文件:`src/Drv/lcd/lcd.c`、`src/Drv/lcd/lcd_draw.c`、`src/Drv/lcd/lcd_text.c`、`src/Drv/lcd/text_codec.c`、`src/Drv/lcd/ascii.c`、`src/Drv/display.c` +- 职责: + - 管理 LCD 显存 `g_tCVsr` 与像素绘制 + - 提供 ASCII/UTF-8 字符显示能力 + - 提供静态菜单模型定义 `g_tMenuModelTab` + +### 4.4 输入层 + +- 文件:`src/Drv/key.c`、`src/Drv/key.h` +- 职责: + - 提供按键读写状态控制(消费式读取) + - 接收远程模块写入的按键事件并供菜单模块读取 + +### 4.5 远程显示通信层 + +- 文件:`src/remoteDisplay.c`、`src/remoteDisplay.h` +- 职责: + - 实现 RemoDispBus 协议解析与回复 + - 提供 TCP 服务器线程入口与启动逻辑 + - 处理保活、初始化、按键下发、显存上传等命令 + +### 4.6 平台适配层 + +- 文件:`src/TCP/tcp.c`、`src/thread_utils.c` +- 职责: + - 提供跨平台 socket 与线程封装 + - 隔离 Windows/Linux API 差异 + +## 5. 核心数据结构设计 + +### 5.1 菜单模型与菜单树 + +- 静态菜单模型:`tagMenuModel`(定义于 `display.h`,数据在 `display.c`) +- 运行时菜单项:`tagMenuItem`(定义于 `menu.c` 内) +- 全局控制:`g_tMenuCtrl`、`g_tDspCtrl` + +关键关系: + +- `ptHigher` / `ptLower`:父子层级关系 +- `ptBefore` / `ptBehind`:同级双向关系(首尾成环) +- `ptRoute[]`:当前路径缓存(0~3 级) + +### 5.2 显示控制结构 + +- `tagScreenControl g_tCVsr`: + - 显存缓冲 `pwbyLCDMemory` + - 前景/背景色 + - ASCII 与汉字字体参数 + +### 5.3 远程按键结构 + +- `tagRKeyCtrl g_tRemoteKey`: + - `byKeyValid`:是否有新按键 + - `byKeyValue`:按键值 + - `bUseRkey`:远程按键开关(当前实现中初始化为启用) + +## 6. 关键业务流程 + +### 6.1 主循环流程 + +```text +[系统初始化] + | + v +[Menu_Route] + | + v +[Sleep 20ms] + | + v +[计数器累加] + | + v +[是否到刷新周期?] --否--> [Menu_Route] + | + +--是--> [Menu_Show_Proc] --> [Menu_Route] +``` + +### 6.2 菜单交互流程 + +- 输入来源:`Key_Read()`(含远程写入按键) +- 行为: + - 上/下:同级循环移动 + - 左/ESC:回退上级或退回主层 + - 右/确认:进入子级或执行叶子回调 +- 渲染: + - `Menu_Show_Proc` 根据路径增量刷新或全量刷新 + +### 6.3 远程显示协议流程 + +```text +[Accept客户端] + | + v +[接收缓冲区累积] + | + v +[parse_frame 校验解析] + | + +--成功--------> [按 cmd 分发] --> [send_reply 回包] --> [接收缓冲区累积] + | + +--失败/不完整--> [接收缓冲区累积] +``` + +命令语义(RemoDispBus): + +- `CMD_INIT`:返回 LCD 宽高与显存尺寸 +- `CMD_LCDMEM`:返回显存数据(支持起始地址) +- `CMD_KEY`:注入远程按键到 `g_tRemoteKey` +- `CMD_KEEPLIVE`:保活响应 + +## 7. 并发与线程模型 + +- 主线程: + - 负责菜单路由与本地显示刷新 +- TCP 服务器线程: + - 监听连接、解析协议、处理远程请求 + +共享状态: + +- `g_tCVsr.pwbyLCDMemory`(远程读取 + 本地写入) +- `g_tRemoteKey`(远程写入 + 菜单读取) + +当前实现未使用锁机制,依赖业务访问模式降低冲突风险。 +后续若并发复杂度提升,建议引入细粒度互斥或无锁缓冲策略。 + +## 8. 构建与运行架构 + +- 构建系统:CMake(`C_STANDARD 99`) +- 可执行目标:`DTU-HMI` +- 平台链接: + - Windows:`ws2_32` + - Linux/macOS:`pthread` +- 可选调试:`ENABLE_DEBUG=ON` 自动定义 `DEBUG` 宏 + +## 9. 测试架构 + +测试目录:`tests/` + +- 框架:`ctest + 自定义断言宏` +- 分层策略: + - P0:纯逻辑单元测试(协议解析、UTF-8 解析、字库查找) + - P1:状态/计算单测(按键、菜单、LCD 基础像素操作) + - P2:集成测试(TCP 回环) + +建议执行命令: + +```bash +cmake -S . -B build +cmake --build build +ctest --test-dir build -C Debug --output-on-failure +``` + +## 10. 模块依赖关系(代码级) + +- `main.c` 依赖:`menu`、`key`、`remoteDisplay`、`tcp`、`thread_utils` +- `menu.c` 依赖:`lcd`、`display`、`key` +- `remoteDisplay.c` 依赖:`lcd`、`key`、`tcp`、`thread_utils` +- `lcd.c` 依赖:`ascii` +- `display.c` 提供:静态菜单表(被 `menu.c` 使用) + +## 11. 已知风险与改进建议 + +- **并发一致性风险**:远程线程与主线程共享状态无锁访问。 + - 建议:为显存快照与按键事件引入互斥保护或双缓冲。 +- **协议缓冲鲁棒性**:当前异常数据采用清空缓冲策略,存在丢包窗口。 + - 建议:增加更精细的帧边界恢复策略与统计日志。 +- **可测试性边界**:部分逻辑仍与全局状态耦合较深。 + - 建议:逐步引入接口注入(如 `TcpOps`、`delay_ms`)降低耦合。 + +## 12. 版本与维护 + +- 文档版本:v1.0 +- 适配代码基线:当前 `DTU-HMI` 仓库主干实现 +- 维护建议: + - 每次新增模块或调整主流程时同步更新本文档 + - 测试策略更新需同步维护“第 9 章 测试架构” + diff --git a/docs/通信协议设计文档.md b/docs/通信协议设计文档.md new file mode 100644 index 0000000..d109b8a --- /dev/null +++ b/docs/通信协议设计文档.md @@ -0,0 +1,264 @@ +# DTU-HMI 通信协议设计文档 + +## 1. 文档说明 + +- 文档名称:`DTU-HMI` 通信协议设计文档 +- 协议名称:RemoDispBus(项目内实现) +- 适用范围:`DTU-HMI` 与远程显示上位机(如 `remo_disp_server.py`)之间的 TCP 通信 +- 对应实现:`src/remoteDisplay.c`、`src/remoteDisplay.h` + +## 2. 协议目标 + +本协议用于实现以下能力: + +- 上位机与设备端建立会话并获取显示参数 +- 上位机按需读取 LCD 显存内容用于渲染 +- 上位机向设备端下发按键事件 +- 维持连接活性(保活) + +## 3. 传输层与连接模型 + +- 传输层:TCP +- 服务器角色:`DTU-HMI`(设备端) +- 客户端角色:远程显示上位机 +- 默认监听端口:`7003` +- 连接模式:单连接处理(当前连接断开后继续接受下一连接) + +连接与处理流程(文字图): + +```text +[TcpServer_Listen:7003] + | + v +[Accept 客户端连接] + | + v +[接收并累积缓冲区数据] + | + v +[解析完整协议帧] + | 成功 | 不完整/非法 + v v +[命令分发与处理] [继续接收数据] + | + v +[发送应答帧] + | + v +[继续处理当前连接,直到断开] +``` + +## 4. 帧结构定义 + +### 4.1 通用帧格式 + +协议帧格式如下: + +```text +[TAG][CMD][LEN_H][LEN_L][DATA...][CRC] +``` + +字段说明: + +- `TAG`:1 字节,报文方向标记 +- `CMD`:1 字节,命令码 +- `LEN_H` + `LEN_L`:2 字节,大端,表示 `DATA` 长度 +- `DATA`:可变长,长度由 `LEN` 指定 +- `CRC`:1 字节,`DATA` 区逐字节异或 + +### 4.2 TAG 约定 + +- 客户端 -> 设备端:`0xAA` +- 设备端 -> 客户端:`0xBB` + +### 4.3 CRC 算法 + +- 初值:`0x00` +- 计算范围:仅 `DATA` 字段 +- 算法:`crc = data[0] ^ data[1] ^ ... ^ data[n-1]` +- `DATA` 长度为 0 时,CRC 结果为 `0x00` + +## 5. 命令字定义 + +当前实现支持 4 个命令: + +- `0x00`:`CMD_KEEPLIVE` +- `0x01`:`CMD_INIT` +- `0x02`:`CMD_KEY` +- `0x03`:`CMD_LCDMEM` + +## 6. 命令详细设计 + +### 6.1 CMD_KEEPLIVE(0x00) + +#### 请求 + +- `DATA`:空(长度 0) + +#### 响应 + +- `CMD`:`0x00` +- `DATA`:空(长度 0) +- 用于连接保活与链路探测 + +--- + +### 6.2 CMD_INIT(0x01) + +#### 请求 + +- `DATA`:空(长度 0) + +#### 响应 + +- `DATA` 长度:8 字节 +- 格式: + +```text +[LCD_W_H][LCD_W_L][LCD_H_H][LCD_H_L][MEM_B3][MEM_B2][MEM_B1][MEM_B0] +``` + +字段含义: + +- `LCD_W`:屏幕宽度(当前为 `160`) +- `LCD_H`:屏幕高度(当前为 `160`) +- `MEM`:显存总字节数(当前为 `160 * 160 = 25600`) + +字节序:全部为大端编码 + +--- + +### 6.3 CMD_KEY(0x02) + +#### 请求 + +- `DATA`:至少 1 字节 +- `DATA[0]`:按键值(如 `KEY_U/KEY_D/KEY_L/KEY_R/KEY_ENT/KEY_ESC`) + +#### 处理行为 + +- 设备端将按键写入: + - `g_tRemoteKey.byKeyValid = EN_KEY_FLAG_NEW` + - `g_tRemoteKey.byKeyValue = DATA[0]` + +#### 响应 + +- 当前实现:不发送显式响应帧 +- 建议:后续版本增加 ACK,以便上位机确认按键注入结果 + +--- + +### 6.4 CMD_LCDMEM(0x03) + +#### 请求 + +- `DATA` 长度:建议 4 字节 +- 格式:`[ADDR_B3][ADDR_B2][ADDR_B1][ADDR_B0]`(大端起始地址) + +若请求长度小于 4,设备端默认起始地址为 0。 + +#### 响应 + +- `DATA` 格式: + +```text +[ADDR_B3][ADDR_B2][ADDR_B1][ADDR_B0][LCD_MEM_SLICE...] +``` + +- 前 4 字节回显起始地址 +- 后续为显存片段: + - 若 `start_addr < LCD_DISPLAYMEMORYSIZE`,返回从该地址到末尾的全部显存 + - 若 `start_addr >= LCD_DISPLAYMEMORYSIZE`,仅返回 4 字节地址(无显存数据) + +## 7. 帧解析与容错策略 + +设备端接收缓冲解析规则: + +1. 至少 5 字节才可判定为候选帧(最小帧) +2. 首字节必须是 `TAG_CLIENT(0xAA)` +3. 根据 `LEN` 计算总帧长:`4 + len + 1` +4. 缓冲长度不足总帧长时,继续接收 +5. CRC 不匹配则视为非法帧 +6. 成功解析后按 `consume` 字节从缓冲区移除 + +异常处理策略: + +- 长时间无法成帧且缓冲接近上限(`4096-256`)时,清空缓冲防止越界 +- `recv` 返回 `0` 或 `<0`,认为连接结束,关闭当前客户端 +- 未知命令:回空应答(同命令码,空 `DATA`) + +## 8. 协议示例报文 + +说明:以下示例均为十六进制字节流。 + +### 8.1 KEEPLIVE 请求/响应 + +- 请求:`AA 00 00 00 00` + - `CRC=00`(空数据) +- 响应:`BB 00 00 00 00` + +### 8.2 INIT 请求/响应(示意) + +- 请求:`AA 01 00 00 00` +- 响应头:`BB 01 00 08 ... CRC` +- 响应 `DATA` 示例(160x160,25600): + - `00 A0 00 A0 00 00 64 00` + +### 8.3 KEY 请求(上键示例) + +- 若上键值为 `0x02`,请求可为: + - `AA 02 00 01 02 02` + - 其中末尾 CRC=`0x02` + +### 8.4 LCDMEM 请求(从 0 地址读取) + +- 请求:`AA 03 00 04 00 00 00 00 00` + - `DATA` 为 4 字节地址 `0x00000000` + - CRC=`00` +- 响应:`BB 03 LEN_H LEN_L [00 00 00 00][显存数据...] CRC` + +## 9. 状态与时序约定 + +推荐交互顺序: + +```text +1) 连接 TCP 7003 +2) 发送 CMD_INIT 获取屏幕参数 +3) 周期发送 CMD_LCDMEM 拉取显存 +4) 有用户操作时发送 CMD_KEY +5) 周期发送 CMD_KEEPLIVE 保活 +``` + +## 10. 安全性与边界约束 + +当前协议属于内网轻量协议,未设计鉴权与加密机制。建议在生产化场景补充: + +- 连接鉴权(口令/Token) +- 传输加密(TLS 或应用层加密) +- 命令频率限制与异常连接清理 + +## 11. 兼容性与扩展建议 + +- 保留 `CMD` 空间用于后续扩展 +- 建议新增统一 ACK/NACK 机制(含错误码) +- 建议引入协议版本字段(可放在 `CMD_INIT` 响应或扩展头中) +- 建议为 `CMD_KEY` 增加长度校验(当前默认读取 `DATA[0]`) + +## 12. 与代码映射关系 + +- 帧解析:`parse_frame` +- CRC 计算:`calc_crc` +- 应答构造发送:`send_reply` +- 命令处理:`handle_cmd_keeplive` / `handle_cmd_init` / `handle_cmd_key` / `handle_cmd_lcdmem` +- 线程入口:`tcp_server_thread_fn` +- 服务启动:`StartTcpServerThread` + +## 13. 测试建议(协议方向) + +建议将以下场景纳入自动化测试: + +- 正常帧:4 类命令全部覆盖 +- 异常帧:错误 TAG、错误 CRC、截断帧、超长无效数据 +- 边界值:`LEN=0`、`start_addr=0`、`start_addr=LCD_DISPLAYMEMORYSIZE-1`、`start_addr>=LCD_DISPLAYMEMORYSIZE` +- 连接稳定性:频繁重连、并发请求(若后续支持) + diff --git a/src/Drv/display.h b/src/Drv/display.h index d49a08a..7ee8be1 100644 --- a/src/Drv/display.h +++ b/src/Drv/display.h @@ -1,7 +1,7 @@ #ifndef __DISPLAY__H__ #define __DISPLAY__H__ -#include "../../include/types.h" +#include "types.h" /* 静态菜定义需要的属性 */ diff --git a/src/Drv/key.h b/src/Drv/key.h index e672ea0..37d194d 100644 --- a/src/Drv/key.h +++ b/src/Drv/key.h @@ -1,7 +1,7 @@ #ifndef __KEY_H__ #define __KEY_H__ -#include "../../include/types.h" +#include "types.h" diff --git a/src/Drv/lcd.c b/src/Drv/lcd.c deleted file mode 100644 index c82f66f..0000000 --- a/src/Drv/lcd.c +++ /dev/null @@ -1,602 +0,0 @@ - -#include "lcd.h" -#include -#include "ascii.h" -#include -#ifdef DEBUG -#include -#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 字节:0xxxxxxx(ASCII,0x00..0x7F) - * - 2 字节:110xxxxx 10xxxxxx(U+0080..U+07FF) - * - 3 字节:1110xxxx 10xxxxxx 10xxxxxx(U+0800..U+FFFF,含常用汉字) - * - 4 字节:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx(U+10000..,本函数不处理) - * - * @param utf8 指向当前 UTF-8 字节的指针(可含多字节序列) - * @param out_unicode 输出该字符的 Unicode 码点(U+0000..U+FFFF) - * @return 该字符占用的字节数 1/2/3;0 表示结束、无效或无法解析 - */ -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 字节:ASCII(U+0000..U+007F) - * 格式:0xxxxxxx,即 c < 0x80。码点等于该字节的数值。 - * ----------------------------------------------------------------------- */ - if (c < 0x80) { - *out_unicode = c; - return 1; - } - - /* ----------------------------------------------------------------------- - * 2 字节:U+0080..U+07FF(如拉丁扩展、希腊文等) - * 格式:首字节 110xxxxx(0xC0..0xDF),次字节 10xxxxxx(0x80..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(含常用汉字、日韩等) - * 格式:首字节 1110xxxx(0xE0..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=ASCII,2/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; - } - } - /* ---------- 单字节字符(ASCII):n=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; -} \ No newline at end of file diff --git a/src/Drv/lcd.h b/src/Drv/lcd.h deleted file mode 100644 index 7816f86..0000000 --- a/src/Drv/lcd.h +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/Drv/Ascii.c b/src/Drv/lcd/ascii.c similarity index 100% rename from src/Drv/Ascii.c rename to src/Drv/lcd/ascii.c diff --git a/src/Drv/ascii.h b/src/Drv/lcd/ascii.h similarity index 93% rename from src/Drv/ascii.h rename to src/Drv/lcd/ascii.h index 99af45c..6f144fe 100644 --- a/src/Drv/ascii.h +++ b/src/Drv/lcd/ascii.h @@ -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 */ \ No newline at end of file diff --git a/src/Drv/lcd/lcd.c b/src/Drv/lcd/lcd.c new file mode 100644 index 0000000..39a1b7c --- /dev/null +++ b/src/Drv/lcd/lcd.c @@ -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 + +/* 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; +} diff --git a/src/Drv/lcd/lcd.h b/src/Drv/lcd/lcd.h new file mode 100644 index 0000000..2bc2846 --- /dev/null +++ b/src/Drv/lcd/lcd.h @@ -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 diff --git a/src/Drv/lcd/lcd_draw.c b/src/Drv/lcd/lcd_draw.c new file mode 100644 index 0000000..9de6b8a --- /dev/null +++ b/src/Drv/lcd/lcd_draw.c @@ -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 +#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; +} diff --git a/src/Drv/lcd/lcd_draw.h b/src/Drv/lcd/lcd_draw.h new file mode 100644 index 0000000..210de97 --- /dev/null +++ b/src/Drv/lcd/lcd_draw.h @@ -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 diff --git a/src/Drv/lcd/lcd_text.c b/src/Drv/lcd/lcd_text.c new file mode 100644 index 0000000..031c0b8 --- /dev/null +++ b/src/Drv/lcd/lcd_text.c @@ -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 + +#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; +} diff --git a/src/Drv/lcd/lcd_text.h b/src/Drv/lcd/lcd_text.h new file mode 100644 index 0000000..06b736c --- /dev/null +++ b/src/Drv/lcd/lcd_text.h @@ -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 diff --git a/src/Drv/menu.c b/src/Drv/menu.c index a88b25f..bbd7e49 100644 --- a/src/Drv/menu.c +++ b/src/Drv/menu.c @@ -1,8 +1,11 @@ #include "menu.h" +#include #include #include #include -#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; /* 无论何种情况,更新备份指针为最新当前结点 */ diff --git a/src/Drv/menu.h b/src/Drv/menu.h index 2fb209e..d382d98 100644 --- a/src/Drv/menu.h +++ b/src/Drv/menu.h @@ -12,7 +12,7 @@ #include -#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 // 菜单字符行间隔 diff --git a/src/main.c b/src/main.c index c47571a..83382f9 100644 --- a/src/main.c +++ b/src/main.c @@ -1,4 +1,4 @@ -/* ============================================================================ +/* ============================================================================ * main.c - PC 端 HMI 菜单主程序 * 功能:菜单交互(主线程)+ TCP 服务器(独立线程),按 Q 退出 * ========================================================================== */ diff --git a/src/remoteDisplay.c b/src/remoteDisplay.c index e143278..5d3fafe 100644 --- a/src/remoteDisplay.c +++ b/src/remoteDisplay.c @@ -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 #include -#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 { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..79cd86b --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,108 @@ +# tests 子工程最低 CMake 版本要求(与主工程保持一致) +cmake_minimum_required(VERSION 3.10) + +# 预留:测试通用源码列表(当前未直接使用,可用于后续统一链接) +set(DTU_TEST_COMMON_SOURCES + "${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c" + "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c" + "${CMAKE_SOURCE_DIR}/src/Drv/key.c" + "${CMAKE_SOURCE_DIR}/src/Drv/menu.c" + "${CMAKE_SOURCE_DIR}/src/Drv/display.c" + "${CMAKE_SOURCE_DIR}/src/TCP/tcp.c" + "${CMAKE_SOURCE_DIR}/src/thread_utils.c" + "${CMAKE_SOURCE_DIR}/src/remoteDisplay.c" +) + +# 封装测试目标创建逻辑: +# - add_executable: 生成测试可执行文件 +# - target_include_directories: 注入项目头文件路径 +# - target_link_libraries: 按平台链接系统库 +# - add_test: 注册到 CTest,支持 ctest 统一执行 +function(add_dtu_test test_name) + add_executable(${test_name} ${ARGN}) + # 测试目标可见的头文件目录 + target_include_directories(${test_name} PRIVATE + "${CMAKE_SOURCE_DIR}/include" + "${CMAKE_SOURCE_DIR}/src" + "${CMAKE_SOURCE_DIR}/src/TCP" + "${CMAKE_SOURCE_DIR}/src/Drv" + ) + # 平台差异:Windows 需要 ws2_32,非 Windows 使用 pthread + if(WIN32) + target_link_libraries(${test_name} PRIVATE ws2_32) + else() + target_link_libraries(${test_name} PRIVATE pthread) + endif() + # 将测试程序注册为 CTest 用例,名称与目标名一致 + add_test(NAME ${test_name} COMMAND ${test_name}) +endfunction() + +# ------------------------------------------------------------ +# Smoke 测试:验证测试框架本身可用 +# ------------------------------------------------------------ +add_dtu_test(test_smoke tests_smoke.c) + +# ------------------------------------------------------------ +# P0:高价值纯逻辑测试 +# ------------------------------------------------------------ +add_dtu_test( + test_p0_remote_display + test_p0_remote_display.c + "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c" + "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_draw.c" + "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_text.c" + "${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c" + "${CMAKE_SOURCE_DIR}/src/Drv/key.c" + "${CMAKE_SOURCE_DIR}/src/TCP/tcp.c" + "${CMAKE_SOURCE_DIR}/src/thread_utils.c" +) +add_dtu_test( + test_p0_utf8_next + test_p0_utf8_next.c + "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c" + "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_draw.c" + "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_text.c" + "${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c" +) +add_dtu_test( + test_p0_utf8_hz12_get + test_p0_utf8_hz12_get.c + "${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c" +) + +# ------------------------------------------------------------ +# P1:业务核心计算/状态流转测试 +# ------------------------------------------------------------ +add_dtu_test( + test_p1_key + test_p1_key.c + "${CMAKE_SOURCE_DIR}/src/Drv/key.c" +) +add_dtu_test( + test_p1_lcd_basic + test_p1_lcd_basic.c + "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c" + "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_draw.c" + "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_text.c" + "${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c" +) +add_dtu_test( + test_p1_menu + test_p1_menu.c + "${CMAKE_SOURCE_DIR}/src/Drv/display.c" + "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c" + "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_draw.c" + "${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_text.c" + "${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c" + "${CMAKE_SOURCE_DIR}/src/Drv/key.c" +) + +# ------------------------------------------------------------ +# P2:集成测试(网络回环等) +# ------------------------------------------------------------ +add_dtu_test( + test_p2_tcp_loopback + test_p2_tcp_loopback.c + "${CMAKE_SOURCE_DIR}/src/TCP/tcp.c" + "${CMAKE_SOURCE_DIR}/src/thread_utils.c" +) diff --git a/tests/test_common.h b/tests/test_common.h new file mode 100644 index 0000000..5e7b383 --- /dev/null +++ b/tests/test_common.h @@ -0,0 +1,66 @@ +/* 统一测试辅助头: + * - 提供轻量断言宏,失败时打印上下文并返回 1 终止当前测试程序 + * - 所有测试文件可直接 include 本头,减少重复样板代码 + */ +#ifndef DTU_TEST_COMMON_H +#define DTU_TEST_COMMON_H + +/* 标准库依赖: + * - stdio.h : fprintf + * - stdlib.h : 通用工具(当前宏未直接使用,预留) + * - string.h : strcmp + */ +#include +#include +#include + +/* 断言:表达式为真 + * 失败输出:失败表达式 + 文件 + 行号 + * 失败返回:1(约定 main 返回非 0 表示测试失败) + */ +#define ASSERT_TRUE(expr) \ + do { \ + if (!(expr)) { \ + fprintf(stderr, "ASSERT_TRUE failed: %s (%s:%d)\n", #expr, __FILE__,\ + __LINE__); \ + return 1; \ + } \ + } while (0) + +/* 断言:两个整数值相等(按 int 比较) */ +#define ASSERT_EQ_INT(expected, actual) \ + do { \ + int _exp = (int)(expected); \ + int _act = (int)(actual); \ + if (_exp != _act) { \ + fprintf(stderr, \ + "ASSERT_EQ_INT failed: expected=%d actual=%d (%s:%d)\n", \ + _exp, _act, __FILE__, __LINE__); \ + return 1; \ + } \ + } while (0) + +/* 断言:两个无符号 32 位值相等(按 unsigned int 比较) */ +#define ASSERT_EQ_U32(expected, actual) \ + do { \ + unsigned int _exp = (unsigned int)(expected); \ + unsigned int _act = (unsigned int)(actual); \ + if (_exp != _act) { \ + fprintf(stderr, \ + "ASSERT_EQ_U32 failed: expected=%u actual=%u (%s:%d)\n", \ + _exp, _act, __FILE__, __LINE__); \ + return 1; \ + } \ + } while (0) + +/* 断言:两个 C 字符串内容相等(区分大小写) */ +#define ASSERT_STREQ(expected, actual) \ + do { \ + if (strcmp((expected), (actual)) != 0) { \ + fprintf(stderr, "ASSERT_STREQ failed: expected=\"%s\" actual=\"%s\" (%s:%d)\n", \ + (expected), (actual), __FILE__, __LINE__); \ + return 1; \ + } \ + } while (0) + +#endif diff --git a/tests/test_p0_remote_display.c b/tests/test_p0_remote_display.c new file mode 100644 index 0000000..138ba4d --- /dev/null +++ b/tests/test_p0_remote_display.c @@ -0,0 +1,77 @@ +/* P0 测试:remoteDisplay 协议核心 + * 目标: + * - 校验 CRC 纯函数行为(calc_crc) + * - 校验 RemoDispBus 帧解析边界(parse_frame) + */ +#include + +#include "test_common.h" +/* 直接包含 .c 以访问 static 函数(calc_crc / parse_frame) */ +#include "../src/remoteDisplay.c" + +/* 用例1:CRC 计算 + * - 空数据长度应返回 0 + * - 常规 payload 应返回逐字节异或结果 + */ +static int test_calc_crc(void) +{ + const uint8_t payload[] = {0x12, 0x34, 0x56}; + ASSERT_EQ_INT(0x00, calc_crc(NULL, 0)); + ASSERT_EQ_INT((0x12 ^ 0x34 ^ 0x56), calc_crc(payload, 3)); + return 0; +} + +/* 用例2:合法帧解析 + * 验证: + * - parse_frame 返回成功 + * - cmd/data/data_len/consume 与期望一致 + */ +static int test_parse_frame_ok(void) +{ + uint8_t cmd = 0; + const uint8_t* data = NULL; + unsigned int data_len = 0; + unsigned int consume = 0; + const uint8_t payload[] = {0x10, 0x20}; + const uint8_t frame[] = {0xAA, 0x03, 0x00, 0x02, 0x10, 0x20, (uint8_t)(0x10 ^ 0x20)}; + + ASSERT_EQ_INT(1, parse_frame(frame, sizeof(frame), &cmd, &data, &data_len, &consume)); + ASSERT_EQ_INT(0x03, cmd); + ASSERT_EQ_INT(2, (int)data_len); + ASSERT_EQ_INT((int)sizeof(frame), (int)consume); + ASSERT_EQ_INT(payload[0], data[0]); + ASSERT_EQ_INT(payload[1], data[1]); + + return 0; +} + +/* 用例3:非法输入集合 + * - TAG 非客户端 TAG + * - 帧长度不足(截断) + * - CRC 错误 + * 期望均解析失败(返回 0) + */ +static int test_parse_frame_invalid_cases(void) +{ + uint8_t cmd = 0; + const uint8_t* data = NULL; + unsigned int data_len = 0; + unsigned int consume = 0; + const uint8_t bad_tag[] = {0xBB, 0x03, 0x00, 0x00, 0x00}; + const uint8_t short_buf[] = {0xAA, 0x03, 0x00, 0x01}; + const uint8_t bad_crc[] = {0xAA, 0x02, 0x00, 0x01, 0x55, 0x00}; + + ASSERT_EQ_INT(0, parse_frame(bad_tag, sizeof(bad_tag), &cmd, &data, &data_len, &consume)); + ASSERT_EQ_INT(0, parse_frame(short_buf, sizeof(short_buf), &cmd, &data, &data_len, &consume)); + ASSERT_EQ_INT(0, parse_frame(bad_crc, sizeof(bad_crc), &cmd, &data, &data_len, &consume)); + return 0; +} + +/* 测试入口:按顺序执行各子用例,任一失败即返回非 0 */ +int main(void) +{ + if (test_calc_crc() != 0) return 1; + if (test_parse_frame_ok() != 0) return 1; + if (test_parse_frame_invalid_cases() != 0) return 1; + return 0; +} diff --git a/tests/test_p0_utf8_hz12_get.c b/tests/test_p0_utf8_hz12_get.c new file mode 100644 index 0000000..c5a4e2b --- /dev/null +++ b/tests/test_p0_utf8_hz12_get.c @@ -0,0 +1,14 @@ +#include "../src/Drv/lcd/ascii.h" +#include "test_common.h" + +int main(void) +{ + const uint8_t* hit = utf8_hz12_get(0x4F60u); /* 你 */ + const uint8_t* miss_low = utf8_hz12_get(0x0001u); + const uint8_t* miss_high = utf8_hz12_get(0x10FFFFu); + + ASSERT_TRUE(hit != NULL); + ASSERT_TRUE(miss_low == NULL); + ASSERT_TRUE(miss_high == NULL); + return 0; +} diff --git a/tests/test_p0_utf8_next.c b/tests/test_p0_utf8_next.c new file mode 100644 index 0000000..4764017 --- /dev/null +++ b/tests/test_p0_utf8_next.c @@ -0,0 +1,27 @@ +#include "../src/Drv/lcd/lcd_text.h" +#include "test_common.h" + +int main(void) +{ + uint32_t unicode = 0; + const unsigned char ascii[] = "A"; + const unsigned char u2[] = {0xC2, 0xA2, 0x00}; /* U+00A2 */ + const unsigned char u3[] = {0xE4, 0xBD, 0xA0, 0x00}; /* U+4F60 */ + const unsigned char invalid[] = {0xF0, 0x00}; /* 4-byte start not supported */ + const unsigned char truncated2[] = {0xC2, 0x00}; + const unsigned char truncated3[] = {0xE4, 0xBD, 0x00}; + + ASSERT_EQ_INT(1, utf8_next(ascii, &unicode)); + ASSERT_EQ_U32(0x41u, unicode); + + ASSERT_EQ_INT(2, utf8_next(u2, &unicode)); + ASSERT_EQ_U32(0x00A2u, unicode); + + ASSERT_EQ_INT(3, utf8_next(u3, &unicode)); + ASSERT_EQ_U32(0x4F60u, unicode); + + ASSERT_EQ_INT(0, utf8_next(invalid, &unicode)); + ASSERT_EQ_INT(0, utf8_next(truncated2, &unicode)); + ASSERT_EQ_INT(0, utf8_next(truncated3, &unicode)); + return 0; +} diff --git a/tests/test_p1_key.c b/tests/test_p1_key.c new file mode 100644 index 0000000..5594897 --- /dev/null +++ b/tests/test_p1_key.c @@ -0,0 +1,16 @@ +#include "../src/Drv/key.h" +#include "test_common.h" + +int main(void) +{ + Key_Init(); + ASSERT_EQ_INT(EN_KEY_FLAG_NULL, g_tRemoteKey.byKeyValid); + ASSERT_EQ_INT(KEY_NONE, g_tRemoteKey.byKeyValue); + + g_tRemoteKey.byKeyValid = EN_KEY_FLAG_NEW; + g_tRemoteKey.byKeyValue = KEY_U; + ASSERT_EQ_INT(KEY_U, Key_Read()); + ASSERT_EQ_INT(EN_KEY_FLAG_NULL, g_tRemoteKey.byKeyValid); + ASSERT_EQ_INT(KEY_NONE, Key_Read()); + return 0; +} diff --git a/tests/test_p1_lcd_basic.c b/tests/test_p1_lcd_basic.c new file mode 100644 index 0000000..3304bd5 --- /dev/null +++ b/tests/test_p1_lcd_basic.c @@ -0,0 +1,289 @@ +/* ------------------------------------------------------------------------- + * 文件名: test_p1_lcd_basic.c + * 作用: + * 对 LCD 基础模块进行单元测试,覆盖以下三个子模块: + * 1) lcd.c : 显存初始化、像素读写、边界保护 + * 2) lcd_draw.c : 填充、反显、水平线/垂直线绘制 + * 3) lcd_text.c : UTF-8 解码、字符串显示(ASCII/中文/换行)与边界返回值 + * + * 测试策略: + * - 每个 test_* 函数仅验证一个明确行为域,失败即立即返回非 0。 + * - 使用 ASSERT_* 宏统一断言风格,失败时打印文件+行号。 + * - main() 串行执行所有子用例,便于定位首个失败点。 + * + * 设计说明: + * - 本文件只依赖公开头文件,不包含被测 .c,实现黑盒+灰盒结合验证。 + * - 通过“像素点结果”验证绘图函数,不依赖外设或真实 LCD 硬件。 + * ------------------------------------------------------------------------- */ +#include "../src/Drv/lcd/lcd.h" +#include "../src/Drv/lcd/lcd_draw.h" +#include "../src/Drv/lcd/lcd_text.h" +#include "test_common.h" + +/* ------------------------------------------------------------------------- + * 用例: test_lcd_init_and_pixel_rw + * 目标: + * - 验证 Lcd_Init() 会把显存清零(黑色) + * - 验证 Lcd_SetPixel/Lcd_GetPixel 的基本读写正确性 + * - 验证越界写入/读取均返回错误码 + * + * 覆盖函数: + * - Lcd_Init + * - Lcd_SetPixel + * - Lcd_GetPixel + * ------------------------------------------------------------------------- */ +static int test_lcd_init_and_pixel_rw(void) +{ + /* 初始化后显存应为全黑(0x00) */ + Lcd_Init(); + /* 左上角像素校验 */ + ASSERT_EQ_INT(LCD_COLOR_BLACK, Lcd_GetPixel(0, 0)); + /* 右下角像素校验,确认全屏范围都被正确初始化 */ + ASSERT_EQ_INT(LCD_COLOR_BLACK, Lcd_GetPixel(LCD_SIZE_X - 1, LCD_SIZE_Y - 1)); + + /* 在合法坐标写白点并回读 */ + ASSERT_EQ_INT(LCD_OK, Lcd_SetPixel(1, 1, LCD_COLOR_WHITE)); + ASSERT_EQ_INT(LCD_COLOR_WHITE, Lcd_GetPixel(1, 1)); + + /* 非法颜色应返回错误码,且不改变原有像素值 */ + ASSERT_EQ_INT(LCD_ERR_INVALID_COLOR, Lcd_SetPixel(1, 1, 0x7F)); + ASSERT_EQ_INT(LCD_COLOR_WHITE, Lcd_GetPixel(1, 1)); + + /* 越界写入应返回错误码:x == LCD_SIZE_X 已越界 */ + ASSERT_EQ_INT(LCD_ERR_OUT_OF_RANGE, Lcd_SetPixel(LCD_SIZE_X, 1, LCD_COLOR_WHITE)); + /* 越界写入应返回错误码:y == LCD_SIZE_Y 已越界 */ + ASSERT_EQ_INT(LCD_ERR_OUT_OF_RANGE, Lcd_SetPixel(1, LCD_SIZE_Y, LCD_COLOR_WHITE)); + /* 越界读取应返回错误码 */ + ASSERT_EQ_INT(LCD_ERR_OUT_OF_RANGE, Lcd_GetPixel(LCD_SIZE_X, 1)); + ASSERT_EQ_INT(LCD_ERR_OUT_OF_RANGE, Lcd_GetPixel(1, LCD_SIZE_Y)); + + /* 附加点位校验,确保越界写没有污染邻近合法区域 */ + ASSERT_EQ_INT(LCD_COLOR_BLACK, Lcd_GetPixel(2, 2)); + return 0; +} + +/* ------------------------------------------------------------------------- + * 用例: test_fillrect_and_invert + * 目标: + * - 验证 Lcd_FillRect 的闭区间填充行为(包含 right_x/bottom_y)与错误码 + * - 验证 Lcd_Invert 的半开区间行为([min, max))与错误码 + * - 验证 Lcd_Invert 支持反向坐标输入(起点大于终点) + * + * 覆盖函数: + * - Lcd_FillRect + * - Lcd_Invert + * ------------------------------------------------------------------------- */ +static int test_fillrect_and_invert(void) +{ + Lcd_Init(); + + /* 填充 3x3 白色区域:x=[2,4], y=[2,4](闭区间) */ + ASSERT_EQ_INT(LCD_OK, Lcd_FillRect(2, 2, 4, 4, LCD_COLOR_WHITE)); + /* 左上角在填充区内 */ + ASSERT_EQ_INT(LCD_COLOR_WHITE, Lcd_GetPixel(2, 2)); + /* 右下角也在填充区内,验证 FillRect 含边界 */ + ASSERT_EQ_INT(LCD_COLOR_WHITE, Lcd_GetPixel(4, 4)); + /* 区域外点保持黑色 */ + ASSERT_EQ_INT(LCD_COLOR_BLACK, Lcd_GetPixel(5, 5)); + + /* Invert 采用 [min, max) 半开区间:会翻转 x=2..3, y=2..3 */ + ASSERT_EQ_INT(LCD_OK, Lcd_Invert(2, 2, 4, 4)); + /* 区域内像素从白翻转成 ~白 */ + ASSERT_EQ_INT((uint8_t)~LCD_COLOR_WHITE, Lcd_GetPixel(2, 2)); + ASSERT_EQ_INT((uint8_t)~LCD_COLOR_WHITE, Lcd_GetPixel(3, 3)); + /* x=4/y=4 在半开区间外,不应被翻转 */ + ASSERT_EQ_INT(LCD_COLOR_WHITE, Lcd_GetPixel(4, 4)); + + /* 反向坐标也应可处理:再次翻转回白色 */ + ASSERT_EQ_INT(LCD_OK, Lcd_Invert(4, 4, 2, 2)); + ASSERT_EQ_INT(LCD_COLOR_WHITE, Lcd_GetPixel(2, 2)); + ASSERT_EQ_INT(LCD_COLOR_WHITE, Lcd_GetPixel(3, 3)); + + /* 越界参数应返回错误码 */ + ASSERT_EQ_INT(LCD_ERR_OUT_OF_RANGE, Lcd_FillRect(0, 0, LCD_SIZE_X, 4, LCD_COLOR_WHITE)); + ASSERT_EQ_INT(LCD_ERR_INVALID_COLOR, Lcd_FillRect(0, 0, 1, 1, 0x7F)); + ASSERT_EQ_INT(LCD_ERR_OUT_OF_RANGE, Lcd_Invert(0, 0, LCD_SIZE_X + 1, 4)); + + return 0; +} + +/* ------------------------------------------------------------------------- + * 用例: test_lineh_linev + * 目标: + * - 验证 Lcd_LineH/Lcd_LineV 的绘制范围与线宽、错误码 + * - 验证水平/垂直线的“结束坐标不包含”语义 + * - 验证 Lcd_LineH 的反向坐标能力 + * + * 覆盖函数: + * - Lcd_LineH + * - Lcd_LineV + * ------------------------------------------------------------------------- */ +static int test_lineh_linev(void) +{ + Lcd_Init(); + + /* 水平线:x=[10,14), y=[20,22);线宽=2 */ + ASSERT_EQ_INT(LCD_OK, Lcd_LineH(10, 14, 20, 2, LCD_COLOR_WHITE)); + /* 起点应该被绘制 */ + ASSERT_EQ_INT(LCD_COLOR_WHITE, Lcd_GetPixel(10, 20)); + /* 范围内末端附近点(x=13,y=21)应为白 */ + ASSERT_EQ_INT(LCD_COLOR_WHITE, Lcd_GetPixel(13, 21)); + /* xEnd 不包含:x=14 不应被画 */ + ASSERT_EQ_INT(LCD_COLOR_BLACK, Lcd_GetPixel(14, 20)); /* xEnd 不包含 */ + + /* 垂直线:x=[40,42), y=[30,35);线宽=2 */ + ASSERT_EQ_INT(LCD_OK, Lcd_LineV(30, 35, 40, 2, LCD_COLOR_WHITE)); + /* 起点应被绘制 */ + ASSERT_EQ_INT(LCD_COLOR_WHITE, Lcd_GetPixel(40, 30)); + /* 范围内末端附近点应为白 */ + ASSERT_EQ_INT(LCD_COLOR_WHITE, Lcd_GetPixel(41, 34)); + /* yEnd 不包含:y=35 不应被画 */ + ASSERT_EQ_INT(LCD_COLOR_BLACK, Lcd_GetPixel(40, 35)); /* yEnd 不包含 */ + + /* 水平线支持反向 X:函数内部应自动交换起止坐标 */ + ASSERT_EQ_INT(LCD_OK, Lcd_LineH(14, 10, 50, 1, LCD_COLOR_WHITE)); + ASSERT_EQ_INT(LCD_COLOR_WHITE, Lcd_GetPixel(10, 50)); + ASSERT_EQ_INT(LCD_COLOR_WHITE, Lcd_GetPixel(13, 50)); + ASSERT_EQ_INT(LCD_COLOR_BLACK, Lcd_GetPixel(14, 50)); + + ASSERT_EQ_INT(LCD_ERR_INVALID_COLOR, Lcd_LineH(10, 14, 20, 1, 0x7F)); + ASSERT_EQ_INT(LCD_ERR_OUT_OF_RANGE, Lcd_LineH(10, 14, LCD_SIZE_Y, 1, LCD_COLOR_WHITE)); + ASSERT_EQ_INT(LCD_ERR_OUT_OF_RANGE, Lcd_LineV(10, 14, LCD_SIZE_X, 1, LCD_COLOR_WHITE)); + + return 0; +} + +/* ------------------------------------------------------------------------- + * 工具函数: count_white_in_rect + * 作用: + * 统计指定闭区间矩形内白色像素数量。 + * 用途: + * 文本渲染(尤其字形)不便逐像素写期望值时,用“白点数 > 0”验证 + * “确实有内容被渲染”这一最小正确性。 + * + * 参数: + * x0,y0 : 左上角(包含) + * x1,y1 : 右下角(包含) + * + * 返回: + * 区域内等于 LCD_COLOR_WHITE 的像素个数。 + * ------------------------------------------------------------------------- */ +static int count_white_in_rect(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) +{ + int count = 0; + for (uint16_t y = y0; y <= y1; y++) { + for (uint16_t x = x0; x <= x1; x++) { + if (Lcd_GetPixel(x, y) == LCD_COLOR_WHITE) { + count++; + } + } + } + return count; +} + +/* ------------------------------------------------------------------------- + * 用例: test_utf8_next_decode + * 目标: + * - 覆盖 utf8_next 对 1/2/3 字节 UTF-8 的解码路径 + * - 验证不支持或非法输入时返回 0 + * + * 覆盖函数: + * - utf8_next + * ------------------------------------------------------------------------- */ +static int test_utf8_next_decode(void) +{ + uint32_t unicode = 0; + const unsigned char ascii[] = "A"; /* 单字节 ASCII */ + const unsigned char u2[] = {0xC2, 0xA2, 0x00}; /* 两字节: U+00A2 */ + const unsigned char u3[] = {0xE4, 0xBD, 0xA0, 0x00}; /* 三字节: U+4F60 */ + const unsigned char invalid[] = {0xF0, 0x00}; /* 四字节起始(当前实现不支持) */ + const unsigned char trunc2[] = {0xC2, 0x00}; /* 两字节截断 */ + const unsigned char trunc3[] = {0xE4, 0xBD, 0x00}; /* 三字节截断 */ + + /* ASCII: 返回长度 1,码点 0x41 */ + ASSERT_EQ_INT(1, utf8_next(ascii, &unicode)); + ASSERT_EQ_U32(0x41u, unicode); + + /* 两字节 UTF-8 */ + ASSERT_EQ_INT(2, utf8_next(u2, &unicode)); + ASSERT_EQ_U32(0x00A2u, unicode); + + /* 三字节 UTF-8 */ + ASSERT_EQ_INT(3, utf8_next(u3, &unicode)); + ASSERT_EQ_U32(0x4F60u, unicode); + + /* 非支持输入返回 0 */ + ASSERT_EQ_INT(0, utf8_next(invalid, &unicode)); + ASSERT_EQ_INT(0, utf8_next(trunc2, &unicode)); + ASSERT_EQ_INT(0, utf8_next(trunc3, &unicode)); + return 0; +} + +/* ------------------------------------------------------------------------- + * 用例: test_showstr_ascii_utf8_newline_and_bounds + * 目标: + * - 验证 Lcd_ShowStr 对 ASCII/中文渲染均生效 + * - 验证换行逻辑(行高 + 行间距)正确 + * - 验证越界起始坐标时返回错误码 + * + * 覆盖函数: + * - Lcd_ShowStr + * ------------------------------------------------------------------------- */ +static int test_showstr_ascii_utf8_newline_and_bounds(void) +{ + uint8_t text_a[] = "A"; + uint8_t text_cn[] = "你"; + uint8_t text_nl[] = "A\nB"; + uint8_t text_cn_overflow_x[] = "你你"; + uint8_t text_nl_overflow_y[] = "A\nB"; + + /* ASCII 渲染: 返回 0 且字形区域应存在白点 */ + Lcd_Init(); + ASSERT_EQ_INT(0, Lcd_ShowStr(0, 0, text_a)); + ASSERT_TRUE(count_white_in_rect(0, 0, 6, 11) > 0); + + /* 中文渲染: 13x12 区域内应存在白点 */ + Lcd_Init(); + ASSERT_EQ_INT(0, Lcd_ShowStr(10, 10, text_cn)); + ASSERT_TRUE(count_white_in_rect(10, 10, 22, 21) > 0); + + /* 换行渲染: "A\nB" 中 B 应下移到 y = 12 + rowSpace(2) = 14 */ + Lcd_Init(); + ASSERT_EQ_INT(0, Lcd_ShowStr(0, 0, text_nl)); + ASSERT_TRUE(count_white_in_rect(0, 0, 6, 11) > 0); /* A */ + ASSERT_TRUE(count_white_in_rect(0, 14, 6, 25) > 0); /* B: 12 高 + 2 行距 */ + + /* 起始越界:x 方向越界应返回 -1 */ + ASSERT_EQ_INT(-1, Lcd_ShowStr(LCD_SIZE_X - 6, 0, text_a)); + /* 起始越界:y 方向越界应返回 -2 */ + ASSERT_EQ_INT(-2, Lcd_ShowStr(0, LCD_SIZE_Y - 12, text_a)); + + /* 中文第 2 个字符越过右边界时应返回 -1 */ + Lcd_Init(); + ASSERT_EQ_INT(-1, Lcd_ShowStr(LCD_SIZE_X - 13, 0, text_cn_overflow_x)); + + /* 换行后若超出底部边界,应返回 -2 */ + Lcd_Init(); + ASSERT_EQ_INT(-2, Lcd_ShowStr(0, LCD_SIZE_Y - 13, text_nl_overflow_y)); + + /* 预留接口目前为桩函数,返回 0 */ + ASSERT_EQ_INT(0, Lcd_ShowTest(0, 0, text_a)); + + return 0; +} + +/* ------------------------------------------------------------------------- + * 测试入口: + * 按固定顺序执行所有子用例,任一失败立即返回 1。 + * 约定: + * 返回 0 表示该测试程序通过。 + * ------------------------------------------------------------------------- */ +int main(void) +{ + if (test_lcd_init_and_pixel_rw() != 0) return 1; + if (test_fillrect_and_invert() != 0) return 1; + if (test_lineh_linev() != 0) return 1; + if (test_utf8_next_decode() != 0) return 1; + if (test_showstr_ascii_utf8_newline_and_bounds() != 0) return 1; + return 0; +} diff --git a/tests/test_p1_menu.c b/tests/test_p1_menu.c new file mode 100644 index 0000000..01aa99f --- /dev/null +++ b/tests/test_p1_menu.c @@ -0,0 +1,28 @@ +#include + +#include "test_common.h" +#include "../src/Drv/menu.c" + +int main(void) +{ + uint8_t menu_num[4] = {0}; + tagPMenuItem first[4] = {0}; + tagPMenuItem index[4] = {0}; + uint8_t max_len; + + Menu_Init(); + ASSERT_TRUE(g_tMenuCtrl.by0LevelNum > 0); + ASSERT_TRUE(g_tMenuCtrl.ptHead != NULL); + ASSERT_TRUE(g_tMenuCtrl.ptCurrent != NULL); + + ASSERT_EQ_INT(3, utf8_len_cal((uint8_t*)"ABC")); + ASSERT_EQ_INT(2, utf8_len_cal((uint8_t*)"你")); + + first[0] = g_tMenuCtrl.ptHead; + index[0] = g_tMenuCtrl.ptHead; + max_len = Menu_charLenCal(0, menu_num, first, index); + ASSERT_TRUE(max_len > 0); + ASSERT_TRUE(menu_num[1] > 0); + + return 0; +} diff --git a/tests/test_p2_tcp_loopback.c b/tests/test_p2_tcp_loopback.c new file mode 100644 index 0000000..ffc99a5 --- /dev/null +++ b/tests/test_p2_tcp_loopback.c @@ -0,0 +1,81 @@ +#include + +#include "../src/TCP/tcp.h" +#include "../src/thread_utils.h" +#include "test_common.h" + +typedef struct { + int port; + volatile int ready; + volatile int done; + int result; +} loopback_ctx_t; + +static void server_fn(void* arg) +{ + loopback_ctx_t* ctx = (loopback_ctx_t*)arg; + int server = TcpServer_Listen((uint16_t)ctx->port); + char buf[32] = {0}; + int client; + int n; + + if (server == TCP_INVALID_SOCKET) { + ctx->result = 1; + ctx->done = 1; + return; + } + + ctx->ready = 1; + client = TcpServer_Accept(server); + if (client == TCP_INVALID_SOCKET) { + ctx->result = 2; + TcpServer_Close(server); + ctx->done = 1; + return; + } + + n = TcpClient_Recv(client, buf, sizeof(buf)); + if (n <= 0 || strncmp(buf, "ping", 4) != 0) { + ctx->result = 3; + } else { + TcpClient_Send(client, "pong", 4); + ctx->result = 0; + } + + TcpClient_Close(client); + TcpServer_Close(server); + ctx->done = 1; +} + +int main(void) +{ + loopback_ctx_t ctx; + thread_handle_t th; + int client; + char recv_buf[8] = {0}; + int n; + + memset(&ctx, 0, sizeof(ctx)); + ctx.port = 7013; + + ASSERT_EQ_INT(0, Tcp_Init()); + ASSERT_EQ_INT(0, Thread_Create(server_fn, &ctx, &th)); + + while (!ctx.ready) { + /* wait */ + } + + client = TcpClient_Connect("127.0.0.1", (uint16_t)ctx.port); + ASSERT_TRUE(client != TCP_INVALID_SOCKET); + ASSERT_EQ_INT(4, TcpClient_Send(client, "ping", 4)); + + n = TcpClient_Recv(client, recv_buf, sizeof(recv_buf)); + ASSERT_EQ_INT(4, n); + ASSERT_TRUE(strncmp(recv_buf, "pong", 4) == 0); + TcpClient_Close(client); + + Thread_Join(th); + ASSERT_EQ_INT(0, ctx.result); + Tcp_Cleanup(); + return 0; +} diff --git a/tests/tests_smoke.c b/tests/tests_smoke.c new file mode 100644 index 0000000..7c1cfa4 --- /dev/null +++ b/tests/tests_smoke.c @@ -0,0 +1,10 @@ +#include "test_common.h" + +int main(void) +{ + ASSERT_TRUE(1); + ASSERT_EQ_INT(4, 2 + 2); + ASSERT_EQ_U32(0x12345678, 0x12345678); + ASSERT_STREQ("Hello, World!", "Hello, World!"); + return 0; +}