重构代码的架构设计,增加测试单元,提高代码可靠性
This commit is contained in:
@@ -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 需要 Winsock2(ws2_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)
|
||||||
|
|
||||||
|
|||||||
57
README.md
57
README.md
@@ -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 模块可按迭代逐步提升,不要求一次到位。
|
||||||
|
|||||||
244
docs/系统架构设计文档.md
Normal file
244
docs/系统架构设计文档.md
Normal 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 章 测试架构”
|
||||||
|
|
||||||
264
docs/通信协议设计文档.md
Normal file
264
docs/通信协议设计文档.md
Normal 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_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`
|
||||||
|
- 连接稳定性:频繁重连、并发请求(若后续支持)
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#ifndef __DISPLAY__H__
|
#ifndef __DISPLAY__H__
|
||||||
#define __DISPLAY__H__
|
#define __DISPLAY__H__
|
||||||
|
|
||||||
#include "../../include/types.h"
|
#include "types.h"
|
||||||
|
|
||||||
|
|
||||||
/* 静态菜定义需要的属性 */
|
/* 静态菜定义需要的属性 */
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#ifndef __KEY_H__
|
#ifndef __KEY_H__
|
||||||
#define __KEY_H__
|
#define __KEY_H__
|
||||||
|
|
||||||
#include "../../include/types.h"
|
#include "types.h"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
602
src/Drv/lcd.c
602
src/Drv/lcd.c
@@ -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 字节: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;
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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
133
src/Drv/lcd/lcd.c
Normal 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
31
src/Drv/lcd/lcd.h
Normal 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
225
src/Drv/lcd/lcd_draw.c
Normal 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
13
src/Drv/lcd/lcd_draw.h
Normal 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
244
src/Drv/lcd/lcd_text.c
Normal 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
19
src/Drv/lcd/lcd_text.h
Normal 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
|
||||||
125
src/Drv/menu.c
125
src/Drv/menu.c
@@ -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; /* 无论何种情况,更新备份指针为最新当前结点 */
|
||||||
|
|||||||
@@ -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 // 菜单字符行间隔
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
* main.c - PC 端 HMI 菜单主程序
|
* main.c - PC 端 HMI 菜单主程序
|
||||||
* 功能:菜单交互(主线程)+ TCP 服务器(独立线程),按 Q 退出
|
* 功能:菜单交互(主线程)+ TCP 服务器(独立线程),按 Q 退出
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|||||||
@@ -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
108
tests/CMakeLists.txt
Normal 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
66
tests/test_common.h
Normal 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
|
||||||
77
tests/test_p0_remote_display.c
Normal file
77
tests/test_p0_remote_display.c
Normal 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"
|
||||||
|
|
||||||
|
/* 用例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;
|
||||||
|
}
|
||||||
14
tests/test_p0_utf8_hz12_get.c
Normal file
14
tests/test_p0_utf8_hz12_get.c
Normal 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
27
tests/test_p0_utf8_next.c
Normal 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
16
tests/test_p1_key.c
Normal 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
289
tests/test_p1_lcd_basic.c
Normal 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
28
tests/test_p1_menu.c
Normal 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;
|
||||||
|
}
|
||||||
81
tests/test_p2_tcp_loopback.c
Normal file
81
tests/test_p2_tcp_loopback.c
Normal 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
10
tests/tests_smoke.c
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user