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

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,3 +0,0 @@
CompileFlags:
Add:
- -Iinclude

View File

@@ -1,38 +1,56 @@
# 指定 CMake 最低版本要求
cmake_minimum_required(VERSION 3.10) cmake_minimum_required(VERSION 3.10)
# 定义工程名(用于生成解决方案/项目名称)
project(DTU_HMI) project(DTU_HMI)
# 统一使用 C99 标准编译 C 代码
set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD 99)
# 导出编译数据库,供 Cursor/clangd 使用 # 导出 compile_commands.json,供 Cursor/clangd/静态分析工具使用
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# MSVC 编译器下强制使用 UTF-8 源文件编码,避免中文注释/字符串乱码
# 同时关闭 C4819部分环境即使是 UTF-8 文件仍会误报代码页告警)
if(MSVC) if(MSVC)
add_compile_options(/utf-8) add_compile_options(/utf-8 /wd4819)
endif() endif()
# 可选:启用 DEBUG 宏,用于条件编译输出调试信息cmake -DENABLE_DEBUG=ON .. # 可选开关:启用 DEBUG 宏,用于条件编译调试日志
# 用法cmake -DENABLE_DEBUG=ON ..
option(ENABLE_DEBUG "Enable debug printf output" OFF) option(ENABLE_DEBUG "Enable debug printf output" OFF)
if(ENABLE_DEBUG) if(ENABLE_DEBUG)
add_compile_definitions(DEBUG) add_compile_definitions(DEBUG)
endif() endif()
# 主可执行程序 DTU-HMI 及其源码列表
add_executable(DTU-HMI add_executable(DTU-HMI
src/main.c src/main.c
src/thread_utils.c src/thread_utils.c
src/remoteDisplay.c src/remoteDisplay.c
src/Drv/menu.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/key.c
src/Drv/Ascii.c src/Drv/lcd/ascii.c
src/Drv/display.c src/Drv/display.c
src/TCP/tcp.c src/TCP/tcp.c
) )
# 可执行程序头文件搜索路径
target_include_directories(DTU-HMI PRIVATE include src src/TCP) target_include_directories(DTU-HMI PRIVATE include src src/TCP)
# 按平台链接系统库:
# - Windows 需要 Winsock2ws2_32
# - Linux/macOS 使用 pthread
if(WIN32) if(WIN32)
target_link_libraries(DTU-HMI PRIVATE ws2_32) target_link_libraries(DTU-HMI PRIVATE ws2_32)
else() else()
target_link_libraries(DTU-HMI PRIVATE pthread) target_link_libraries(DTU-HMI PRIVATE pthread)
endif() endif()
# 开启 CTest 测试支持,并加载 tests 子目录中的测试目标
enable_testing()
add_subdirectory(tests)

View File

@@ -55,8 +55,12 @@ DTU-HMI/
│ └── Drv/ │ └── Drv/
│ ├── menu.c/h │ ├── menu.c/h
│ ├── display.c/h │ ├── display.c/h
│ ├── lcd.c/h │ ├── lcd/
├── Ascii.c/h │ ├── lcd.c/h
│ │ ├── lcd_draw.c/h
│ │ ├── lcd_text.c/h
│ │ ├── text_codec.c/h
│ │ └── ascii.c/h
│ └── utf8_hz12_data.c/h # 由脚本生成 │ └── utf8_hz12_data.c/h # 由脚本生成
└── build/ └── build/
``` ```
@@ -657,8 +661,11 @@ void Menu_Route(void)
具体使用方式可以参考: 具体使用方式可以参考:
- `src/Drv/Ascii.c`ASCII/字母数字字符绘制 - `src/Drv/lcd/ascii.c`ASCII/字母数字字符绘制
- `src/Drv/lcd.c`LCD 基础绘制接口 - `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` 包裹的日志 - 在关键流程(如 `Menu_Route`、`Menu_Show_Proc`、`remoteDisplay` 收发函数)增加 `#ifdef DEBUG` 包裹的日志
- 使用 GDB / VS 调试器在 `main.c`/`menu.c` 等处打断点,单步查看菜单树和坐标计算过程 - 使用 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 模块可按迭代逐步提升,不要求一次到位。

View File

@@ -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 章 测试架构”

View File

@@ -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_KEEPLIVE0x00
#### 请求
- `DATA`:空(长度 0
#### 响应
- `CMD``0x00`
- `DATA`:空(长度 0
- 用于连接保活与链路探测
---
### 6.2 CMD_INIT0x01
#### 请求
- `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_KEY0x02
#### 请求
- `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_LCDMEM0x03
#### 请求
- `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` 示例160x16025600
- `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`
- 连接稳定性:频繁重连、并发请求(若后续支持)

View File

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

View File

@@ -1,7 +1,7 @@
#ifndef __KEY_H__ #ifndef __KEY_H__
#define __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__ #ifndef __ASCII_H__
#define __ASCII_H__ #define __ASCII_H__
#include "../../include/types.h" #include "types.h"
/* 8x8 ASCII 点阵表 */ /* 8x8 ASCII 点阵表 */
extern const uint8_t g_abyASCII88[][8]; extern const uint8_t g_abyASCII88[][8];
@@ -12,13 +12,10 @@ extern uint8_t g_abyASCII126[][12];
/* 16x8 ASCII 点阵表 */ /* 16x8 ASCII 点阵表 */
extern uint8_t g_abyASCII168[][16]; extern uint8_t g_abyASCII168[][16];
#define UTF8_HZ12_BYTES_PER_CHAR 24 #define UTF8_HZ12_BYTES_PER_CHAR 24
#define UTF8_HZ12_NUM_CHARS 7038 #define UTF8_HZ12_NUM_CHARS 7038
/* 按 Unicode 码点查找点阵,返回 24 字节指针,未找到返回 NULL */ /* 按 Unicode 码点查找点阵,返回 24 字节指针,未找到返回 NULL */
const uint8_t* utf8_hz12_get(uint32_t unicode); const uint8_t* utf8_hz12_get(uint32_t unicode);
#endif /* ASCII_FONT_TABLES_H */ #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 "menu.h"
#include <stdlib.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <string.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 "display.h"
#include "key.h" #include "key.h"
@@ -99,6 +102,98 @@ tagMenuCtrl g_tMenuCtrl; /* 菜单全局
tagMenuItem g_tMenuItem[300]; // 菜单链表空间定义 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) void Menu_0LevelNumCal(void)
{ {
tagPMenuModel ptModelIndex; /* 当前遍历到的菜单表项指针 */ tagPMenuModel ptModelIndex; /* 当前遍历到的菜单表项指针 */
@@ -614,17 +709,17 @@ void Menu_PositionCal(tagPMenuItem ptMenuHead)
{ {
for (uint16_t x = left_x; x < right_x; x++) 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 像素 */ /* 绘制上边框:从 (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 像素 */ /* 绘制左边框:从 (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 像素防止边界漏点 */ /* 绘制下边框:从 (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 像素 */ /* 绘制右边框:从 (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; byFirstPos = 1;
wPosY = wEPosY - LINE_HEIGHT; 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*)"↓"); // 第一页尾显示↓ Lcd_ShowStr(wPosX, wPosY, (uint8_t*)"↓"); // 第一页尾显示↓
} }
else if ((byMenuPos < (byMaxNum * 2 - 2)) && (byPage > 2)) // 当前位置在第二页 else if ((byMenuPos < (byMaxNum * 2 - 2)) && (byPage > 2)) // 当前位置在第二页
@@ -667,12 +762,12 @@ else if (byMenuNum > 20)
byFirstPos = byMaxNum; byFirstPos = byMaxNum;
wPosY = wSPosY + 2; 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*)"↑"); // 第二页头显示↑ Lcd_ShowStr(wPosX, wPosY, (uint8_t*)"↑"); // 第二页头显示↑
wPosY = wEPosY - LINE_HEIGHT; 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*)"↓"); // 第一页尾显示↓ Lcd_ShowStr(wPosX, wPosY, (uint8_t*)"↓"); // 第一页尾显示↓
} }
else else
@@ -690,7 +785,7 @@ else if (byMenuNum > 20)
} }
wPosY = wSPosY + 2; 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*)"↑"); // 第二页头显示↑ Lcd_ShowStr(wPosX, wPosY, (uint8_t*)"↑"); // 第二页头显示↑
} }
@@ -809,15 +904,15 @@ void Menu_Show_Other(uint8_t bylevel)
* 说明: 1. 所有 0 级菜单在 X 方向上按等间距分布 * 说明: 1. 所有 0 级菜单在 X 方向上按等间距分布
* 2. 每次循环仅在菜单项仍在屏幕可见区域时才绘制对应标题文本 * 2. 每次循环仅在菜单项仍在屏幕可见区域时才绘制对应标题文本
* 3. 顶部 0~32 像素区域作为 0 级菜单标题栏背景,会被统一清屏并重绘 * 3. 顶部 0~32 像素区域作为 0 级菜单标题栏背景,会被统一清屏并重绘
* 4. 调用 Lcd_MeiTouPic 绘制“眉头”装饰线条,增强标题栏的视觉效果 * 4. 调用 Menu_MeiTouPic 绘制“眉头”装饰线条,增强标题栏的视觉效果
* 5. 示例中固定显示 “当前模式: 无模式”,后续可替换为实际运行模式文本 * 5. 示例中固定显示 “当前模式: 无模式”,后续可替换为实际运行模式文本
*****************************************************************************/ *****************************************************************************/
void Menu_Show_0Level() void Menu_Show_0Level()
{ {
/* 先清除顶部 0~32 像素高度区域,作为 0 级菜单标题栏背景 */ /* 先清除顶部 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 级菜单栏更加立体 */ /* 绘制“眉头”装饰,使 0 级菜单栏更加立体 */
Lcd_MeiTouPic(16, 2 ); Menu_MeiTouPic(16, 2 );
Lcd_ShowStr(16, 20, (uint8_t*)"当前模式: 无模式" ); Lcd_ShowStr(16, 20, (uint8_t*)"当前模式: 无模式" );
} }
/****************************************************************************** /******************************************************************************
@@ -853,7 +948,7 @@ void Menu_Show_Proc(void)
if (g_tMenuCtrl.pt0Level != g_tMenuCtrl.ptRoute[0]) if (g_tMenuCtrl.pt0Level != g_tMenuCtrl.ptRoute[0])
{ {
bNeedFresh = 1; /* 需要整体刷新 */ 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.pt0Level = g_tMenuCtrl.ptRoute[0]; /* 更新 0 级路径起点 */
g_tMenuCtrl.ptCurBak = g_tMenuCtrl.ptCurrent; /* 更新备份指针 */ g_tMenuCtrl.ptCurBak = g_tMenuCtrl.ptCurrent; /* 更新备份指针 */
} }
@@ -872,7 +967,7 @@ void Menu_Show_Proc(void)
#ifdef DEBUG #ifdef DEBUG
printf("退层:从 %d 级退回 %d 级\n", g_tMenuCtrl.ptCurBak->byClass, g_tMenuCtrl.ptCurrent->byClass); printf("退层:从 %d 级退回 %d 级\n", g_tMenuCtrl.ptCurBak->byClass, g_tMenuCtrl.ptCurrent->byClass);
#endif #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; bNeedFresh = 1;
} }
g_tMenuCtrl.ptCurBak = g_tMenuCtrl.ptCurrent; /* 无论何种情况,更新备份指针为最新当前结点 */ g_tMenuCtrl.ptCurBak = g_tMenuCtrl.ptCurrent; /* 无论何种情况,更新备份指针为最新当前结点 */

View File

@@ -12,7 +12,7 @@
#include <stdint.h> #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_HEIGHT 12 /* 菜单汉字高 */
#define CN_ROWSPACE 2 // 菜单字符行间隔 #define CN_ROWSPACE 2 // 菜单字符行间隔

View File

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

View File

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

108
tests/CMakeLists.txt Normal file
View File

@@ -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"
)

66
tests/test_common.h Normal file
View File

@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 断言:表达式为真
* 失败输出:失败表达式 + 文件 + 行号
* 失败返回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

View File

@@ -0,0 +1,77 @@
/* P0 测试remoteDisplay 协议核心
* 目标:
* - 校验 CRC 纯函数行为calc_crc
* - 校验 RemoDispBus 帧解析边界parse_frame
*/
#include <stdint.h>
#include "test_common.h"
/* 直接包含 .c 以访问 static 函数calc_crc / parse_frame */
#include "../src/remoteDisplay.c"
/* 用例1CRC 计算
* - 空数据长度应返回 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;
}

View File

@@ -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;
}

27
tests/test_p0_utf8_next.c Normal file
View File

@@ -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;
}

16
tests/test_p1_key.c Normal file
View File

@@ -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;
}

289
tests/test_p1_lcd_basic.c Normal file
View File

@@ -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;
}

28
tests/test_p1_menu.c Normal file
View File

@@ -0,0 +1,28 @@
#include <string.h>
#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;
}

View File

@@ -0,0 +1,81 @@
#include <string.h>
#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;
}

10
tests/tests_smoke.c Normal file
View File

@@ -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;
}