目录
- 项目概述与目标
- 硬件系统设计
2.1 硬件组合框图
2.2 核心组件选型与特性
2.3 电路原理图设计
2.4 硬件调试要点 - 软件系统设计
3.1 软件架构与组合框图
3.2 UML 建模
3.3 模块划分与接口定义
3.4 数据结构设计 - 敏捷开发流程实施
4.1 迭代计划与任务分解
4.2 每日站会与进度跟踪
4.3 迭代评审与回顾
4.4 版本控制与文档管理 - 核心技术实现
5.1 u8g2 图形引擎移植
5.2 SSD1327 的 SPI 驱动实现
5.3 汉字显示模块开发
5.4 图形元素绘制实现
5.5 动画效果实现
5.6 遥控器界面布局设计 - 测试验证方案
6.1 单元测试用例与结果
6.2 集成测试流程与数据
6.3 性能测试指标与分析
6.4 用户体验测试反馈 - 性能优化策略
7.1 关键性能指标定义
7.2 代码优化方法
7.3 显示效率提升技巧
7.4 优化前后对比数据 - 结论与展望
1. 项目概述与目标
本项目旨在开发一款基于 STM32F405 微控制器和 SSD1327 OLED 显示屏的无人机遥控器界面系统。通过 SPI 接口实现微控制器与显示屏的通信,利用 u8g2 图形引擎构建直观、响应迅速的用户界面,满足无人机飞行控制的实时信息展示需求。
项目核心目标
- 实现高清晰度的 OLED 显示界面,支持汉字、ASCII 字符、数字、图形和简单动画
- 构建符合无人机操控习惯的界面布局,包含飞行参数、电池状态、信号强度等关键信息
- 保证界面刷新速率≥10fps,满足实时性要求
- 采用敏捷开发方法,确保软件质量和开发效率
- 建立完善的测试验证体系,确保系统稳定性和可靠性
应用场景
该系统可应用于消费级和工业级无人机遥控器,为用户提供直观的飞行状态反馈,包括:
- 飞行高度、速度、航向等关键参数显示
- 电池电量、信号强度实时监控
- 飞行模式切换与状态指示
- 故障告警与提示信息展示
2. 硬件系统设计
2.1 硬件组合框图
无人机遥控器 OLED 界面的硬件系统采用模块化设计,主要由以下部分组成:
plaintext
┌─────────────────────────────────────────────────────────┐
│ 无人机遥控器主系统 │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │
│ │ STM32F405 │ │ SSD1327 │ │ 按键矩阵 │ │
│ │ 微控制器 │◄────►│ OLED显示屏 │ │ │ │
│ └──────┬──────┘ └─────────────┘ └─────┬─────┘ │
│ │ │ │
│ │ ┌─────────────┐ ┌───────────┐ │ │
│ ├─────►│ nRF24L01 │ │ 电池管理 │◄┘ │
│ │ │ 无线模块 │ │ 电路 │ │
│ │ └─────────────┘ └───────────┘ │
│ │ │
│ │ ┌─────────────┐ │
│ └─────►│ 陀螺仪/加速度计 │ │
│ │ (MPU6050) │ │
│ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
- 核心控制模块:STM32F405 微控制器,负责整个系统的控制与数据处理
- 显示模块:SSD1327 驱动的 OLED 显示屏,通过 SPI 接口与主控制器通信
- 输入模块:按键矩阵,用于用户输入与操作
- 无线通信模块:nRF24L01 无线模块,实现与无人机的数据交互
- 传感器模块:MPU6050 陀螺仪与加速度计,用于遥控器姿态检测(可选)
- 电源管理模块:负责系统供电与电池状态监测
2.2 核心组件选型与特性
STM32F405 微控制器
- 核心特性:32 位 ARM Cortex-M4 内核,工作频率 168MHz,内置 FPU(浮点运算单元)
- 存储器:1MB Flash,192KB RAM,满足复杂界面渲染需求
- 外设资源:3 个 SPI 接口,多个 UART、I2C 接口,丰富的定时器资源
- 性能优势:强大的运算能力支持复杂图形渲染,充足的外设资源满足多模块通信需求
- 功耗特性:多种低功耗模式,适合电池供电的遥控器系统
SSD1327 OLED 显示屏
- 显示特性:256×64 分辨率,16 级灰度,4 位色深,对比度可调
- 接口方式:支持 SPI 和 I2C 两种通信方式,本设计采用 SPI 接口以提高数据传输速率
- 功耗表现:工作电流约 20mA,休眠电流 < 1μA,适合低功耗应用
- 尺寸规格:常见 1.5 英寸和 2.4 英寸版本,可根据遥控器设计选择
- 温度范围:-40℃~85℃,满足各种环境下的使用需求
硬件特性对比表
组件 | 关键参数 | 优势 | 应用价值 |
---|---|---|---|
STM32F405 | 168MHz, 1MB Flash, 192KB RAM | 高性能,丰富外设 | 支持复杂界面渲染和多模块控制 |
SSD1327 | 256×64, 16 级灰度,SPI | 高对比度,低功耗 | 清晰显示各类信息,延长续航 |
nRF24L01 | 2.4GHz, 1Mbps, 100 米距离 | 低功耗,高速率 | 实时传输飞行数据 |
MPU6050 | 3 轴 gyro, 3 轴 accel | 高精度,小尺寸 | 支持体感控制(可选功能) |
2.3 电路原理图设计
SPI 通信电路设计
SSD1327 与 STM32F405 的 SPI 连接是系统的关键部分,原理图设计如下:
plaintext
STM32F405 SSD1327
| |
PB13(SPI2_SCK) -----------> SCK
| |
PB15(SPI2_MOSI) -----------> SDA
| |
PB12(GPIO) ---------------> CS
| |
PB14(GPIO) ---------------> DC
| |
PB11(GPIO) ---------------> RST
| |
3.3V ----------------------> VCC
| |
GND ----------------------> GND
- SCK:SPI 时钟线,由 STM32 提供时钟信号
- SDA:SPI 数据线,用于发送显示数据
- CS:片选信号,低电平有效,用于选择 SSD1327
- DC:数据 / 命令选择信号,高电平表示数据,低电平表示命令
- RST:复位信号,低电平复位显示屏
电源电路设计
为保证系统稳定工作,设计了专门的电源管理电路:
- 采用 3.7V 锂电池供电
- 通过 LDO 稳压芯片(如 AMS1117-3.3)提供稳定的 3.3V 电压
- 设计电池电压检测电路,通过 STM32 的 ADC 采集电池电压
- 包含电源指示 LED 和低电量告警电路
2.4 硬件调试要点
-
SPI 通信测试
- 先测试 SPI 基本通信功能,可通过读取 SSD1327 的 ID 寄存器验证
- 确保 SPI 时钟频率匹配(SSD1327 支持最高 10MHz)
- 检查 CS、DC 信号的时序是否正确
-
显示屏初始化测试
- 编写简单的初始化程序,测试显示屏是否能正常点亮
- 绘制简单图形(如矩形)验证显示功能
-
电源稳定性测试
- 测量不同工作状态下的电流消耗
- 测试电池电压下降时系统的稳定性
- 验证低电量检测功能是否准确
-
EMC 兼容性测试
- 测试无线模块工作时对显示屏的干扰
- 检查按键操作对显示稳定性的影响
3. 软件系统设计
3.1 软件架构与组合框图
软件系统采用分层架构设计,从上到下分为应用层、界面层、驱动层和硬件抽象层:
plaintext
┌─────────────────────────────────────────────────────┐
│ 应用层 (Application Layer) │
│ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │
│ │ 飞行参数显示 │ │ 系统设置模块 │ │ 告警提示模块 │ │
│ └────────────┘ └────────────┘ └──────────────┘ │
├─────────────────────────────────────────────────────┤
│ 界面层 (GUI Layer) │
│ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │
│ │ 界面布局管理 │ │ 图形绘制模块 │ │ 字体渲染模块 │ │
│ └────────────┘ └────────────┘ └──────────────┘ │
├─────────────────────────────────────────────────────┤
│ 驱动层 (Driver Layer) │
│ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │
│ │ SSD1327驱动 │ │ SPI接口驱动 │ │ 按键输入驱动 │ │
│ └────────────┘ └────────────┘ └──────────────┘ │
├─────────────────────────────────────────────────────┤
│ 硬件抽象层 (HAL Layer) │
│ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │
│ │ GPIO抽象接口 │ │ 定时器抽象接口 │ │ 中断抽象接口 │ │
│ └────────────┘ └────────────┘ └──────────────┘ │
├─────────────────────────────────────────────────────┤
│ 硬件 (Hardware) │
│ STM32F405 | SSD1327 OLED | 按键矩阵 | 其他外设 │
└─────────────────────────────────────────────────────┘
- 硬件抽象层:提供统一的硬件访问接口,屏蔽不同硬件的差异
- 驱动层:实现各外设的具体驱动,包括 SSD1327 显示屏驱动、SPI 接口驱动等
- 界面层:负责图形用户界面的绘制与管理,基于 u8g2 图形引擎实现
- 应用层:实现具体的业务逻辑,包括飞行参数显示、系统设置等功能
3.2 UML 建模
用例图 (Use Case Diagram)
plaintext
┌───────────┐ ┌─────────────────────────────────┐
│ 用户 │ │ 无人机遥控器 │
└─────┬─────┘ └───────────────┬─────────────────┘
│ │
│ ┌─────────────────────┐ │
├─►│ 查看飞行参数 │◄───┘
│ └─────────────────────┘
│ ┌─────────────────────┐
├─►│ 切换飞行模式 │
│ └─────────────────────┘
│ ┌─────────────────────┐
├─►│ 调整系统设置 │
│ └─────────────────────┘
│ ┌─────────────────────┐
└─►│ 查看告警信息 │
└─────────────────────┘
类图 (Class Diagram)
plaintext
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ U8G2Engine │ │ SSD1327Driver │ │ KeyInput │
├──────────────┤ ├──────────────┤ ├──────────────┤
│ -u8g2: u8g2_t│ │ -spiHandle: │ │ -keyState: │
│ │ │ SPI_HandleTypeDef│ │ uint16_t │
├──────────────┤ ├──────────────┤ ├──────────────┤
│ +init(): void│◄──────┤ +init(): void │ │ +scan(): void│
│ +clear():void│ │ +sendCmd(): │ │ +getKey(): │
│ +drawPixel():│ │ void │ │ uint8_t │
│ void │ │ +sendData(): │ └──────────────┘
│ +drawLine(): void │ void │ ▲
│ +drawRect(): void └──────────────┘ │
└──────────────┘ │
▲ │
│ │
┌───────┴───────┐ ┌───────┴───────┐
│ DisplayUI │ │ InputHandler │
├───────────────┤ ├───────────────┤
│ -u8g2Engine: │ │ -keyInput: │
│ U8G2Engine │ │ KeyInput │
│ -currentPage: │ │ -ui: │
│ uint8_t │◄──────────────────────────┤ DisplayUI* │
├───────────────┤ ├───────────────┤
│ +init(): void │ │ +handleInput():│
│ +showPage(): │ │ void │
│ void │ └───────────────┘
│ +update(): │
│ void │
└───────────────┘
▲
│
┌───────┴───────┐ ┌───────────────┐ ┌───────────────┐
│ FlightPage │ │ SettingPage │ │ AlarmPage │
├───────────────┤ ├───────────────┤ ├───────────────┤
│ -altitude: │ │ -brightness: │ │ -alarmType: │
│ float │ │ uint8_t │ │ uint8_t │
│ -speed: float │ │ -sound: │ │ -alarmMsg: │
├───────────────┤ │ bool │ │ char* │
│ +updateData():│ ├───────────────┤ ├───────────────┤
│ void │ │ +saveSetting():│ │ +showAlarm(): │
│ +draw(): void │ │ void │ │ void │
└───────────────┘ │ +draw(): void │ └───────────────┘
└───────────────┘
时序图 (Sequence Diagram) - 界面刷新流程
plaintext
用户 InputHandler DisplayUI FlightPage U8G2Engine
│ │ │ │ │
│ 按键操作 │ │ │ │
│─────────►│ │ │ │
│ │ 触发刷新 │ │ │
│ │────────────►│ │ │
│ │ │ 请求更新数据 │ │
│ │ │────────────►│ │
│ │ │ │ 返回数据 │
│ │ │ │────────────►│
│ │ │ 绘制界面 │ │
│ │ │────────────────────────►│
│ │ │ │ │ 更新显示
│ │ │ │ │───────────┐
│ │ │ │ │ │
│ │ │ │ │◄──────────┘
│ │ │ │ │
│ │ │ │ │
3.3 模块划分与接口定义
核心模块划分
-
驱动模块
- SSD1327 驱动:负责 OLED 显示屏的初始化和数据传输
- SPI 驱动:实现 SPI 通信功能
- 按键驱动:负责按键扫描和状态检测
-
界面模块
- 图形引擎封装:封装 u8g2 库的功能
- 界面管理:负责不同页面的切换和管理
- 字体管理:处理汉字和 ASCII 字符的显示
-
应用模块
- 飞行参数显示:处理和显示飞行相关数据
- 系统设置:管理系统参数和配置
- 告警处理:处理和显示告警信息
关键接口定义
- U8G2Engine 类接口
c
运行
/**
* @brief 初始化图形引擎
*/
void U8G2Engine::init();
/**
* @brief 清屏操作
*/
void U8G2Engine::clear();
/**
* @brief 绘制像素点
* @param x: X坐标
* @param y: Y坐标
* @param color: 颜色(0-15)
*/
void U8G2Engine::drawPixel(uint16_t x, uint16_t y, uint8_t color);
/**
* @brief 绘制直线
* @param x0: 起点X坐标
* @param y0: 起点Y坐标
* @param x1: 终点X坐标
* @param y1: 终点Y坐标
* @param color: 颜色(0-15)
*/
void U8G2Engine::drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint8_t color);
/**
* @brief 绘制矩形
* @param x: 左上角X坐标
* @param y: 左上角Y坐标
* @param width: 宽度
* @param height: 高度
* @param color: 颜色(0-15)
* @param filled: 是否填充
*/
void U8G2Engine::drawRect(uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint8_t color, bool filled);
/**
* @brief 绘制字符
* @param x: X坐标
* @param y: Y坐标
* @param c: 字符
* @param size: 字体大小
* @param color: 颜色(0-15)
*/
void U8G2Engine::drawChar(uint16_t x, uint16_t y, char c, uint8_t size, uint8_t color);
/**
* @brief 绘制字符串
* @param x: X坐标
* @param y: Y坐标
* @param str: 字符串
* @param size: 字体大小
* @param color: 颜色(0-15)
*/
void U8G2Engine::drawString(uint16_t x, uint16_t y, const char* str, uint8_t size, uint8_t color);
/**
* @brief 绘制汉字
* @param x: X坐标
* @param y: Y坐标
* @param hanzi: 汉字编码
* @param size: 字体大小
* @param color: 颜色(0-15)
*/
void U8G2Engine::drawHanzi(uint16_t x, uint16_t y, uint16_t hanzi, uint8_t size, uint8_t color);
- DisplayUI 类接口
c
运行
/**
* @brief 初始化显示界面
*/
void DisplayUI::init();
/**
* @brief 显示指定页面
* @param page: 页面编号
*/
void DisplayUI::showPage(uint8_t page);
/**
* @brief 更新飞行数据
* @param data: 飞行数据结构体
*/
void DisplayUI::updateFlightData(FlightData data);
/**
* @brief 更新系统状态
* @param state: 系统状态结构体
*/
void DisplayUI::updateSystemState(SystemState state);
/**
* @brief 显示告警信息
* @param alarm: 告警信息结构体
*/
void DisplayUI::showAlarm(AlarmInfo alarm);
/**
* @brief 刷新显示
*/
void DisplayUI::refresh();
- InputHandler 类接口
c
运行
/**
* @brief 初始化输入处理
* @param ui: 显示界面指针
*/
void InputHandler::init(DisplayUI* ui);
/**
* @brief 处理输入
*/
void InputHandler::handleInput();
/**
* @brief 设置按键回调函数
* @param callback: 回调函数指针
*/
void InputHandler::setCallback(InputCallback callback);
3.4 数据结构设计
- 飞行数据结构体
c
运行
/**
* @brief 飞行数据结构体
*/
typedef struct {
float altitude; // 高度,单位:米
float speed; // 速度,单位:米/秒
float heading; // 航向,单位:度
float pitch; // 俯仰角,单位:度
float roll; // 横滚角,单位:度
float batteryVoltage; // 电池电压,单位:伏特
uint8_t signalStrength; // 信号强度,0-100
uint8_t flightMode; // 飞行模式
} FlightData;
- 系统状态结构体
c
运行
/**
* @brief 系统状态结构体
*/
typedef struct {
uint8_t brightness; // 亮度,0-100
uint8_t volume; // 音量,0-100
uint8_t language; // 语言设置
uint32_t runtime; // 运行时间,单位:秒
uint8_t lowBatteryThreshold; // 低电量阈值
} SystemState;
- 告警信息结构体
c
运行
/**
* @brief 告警信息结构体
*/
typedef struct {
uint8_t type; // 告警类型
uint8_t level; // 告警级别
char message[32]; // 告警消息
uint32_t timestamp; // 时间戳
bool active; // 是否为活跃告警
} AlarmInfo;
- 页面定义枚举
c
运行
/**
* @brief 页面枚举
*/
typedef enum {
PAGE_FLIGHT = 0, // 飞行页面
PAGE_SETTINGS, // 设置页面
PAGE_ALARM, // 告警页面
PAGE_INFO, // 信息页面
PAGE_COUNT // 页面总数
} PageType;
- 飞行模式枚举
c
运行
/**
* @brief 飞行模式枚举
*/
typedef enum {
MODE_MANUAL = 0, // 手动模式
MODE_ATTITUDE, // 姿态模式
MODE_POSITION, // 位置模式
MODE_HOME, // 返航模式
MODE_FOLLOW, // 跟随模式
MODE_LANDING // 降落模式
} FlightMode;
4. 敏捷开发流程实施
4.1 迭代计划与任务分解
本项目采用敏捷开发方法,将整个开发过程分为 4 个迭代周期,每个迭代周期为 2 周:
迭代 1:基础功能实现(第 1-2 周)
- 硬件环境搭建与调试
- u8g2 图形引擎移植
- SSD1327 显示屏驱动实现
- 基本图形元素绘制功能开发
迭代 2:界面框架开发(第 3-4 周)
- 界面布局管理模块开发
- 汉字显示功能实现
- 基本页面切换功能开发
- 按键输入处理模块开发
迭代 3:功能完善(第 5-6 周)
- 飞行参数显示页面开发
- 系统设置页面开发
- 告警信息显示功能开发
- 动画效果实现
迭代 4:优化与测试(第 7-8 周)
- 系统性能优化
- 全面测试与 bug 修复
- 用户体验改进
- 文档完善与系统部署
任务分解示例(迭代 1)
任务 ID | 任务描述 | 负责人 | 预估工时 | 优先级 | 状态 |
---|---|---|---|---|---|
T1.1 | 搭建 STM32 开发环境 | 开发者 A | 8 小时 | 高 | 未开始 |
T1.2 | 设计硬件原理图 | 硬件工程师 | 16 小时 | 高 | 未开始 |
T1.3 | 制作测试电路板 | 硬件工程师 | 24 小时 | 高 | 未开始 |
T1.4 | 移植 u8g2 库到 STM32 | 开发者 B | 16 小时 | 高 | 未开始 |
T1.5 | 实现 SSD1327 初始化 | 开发者 B | 8 小时 | 高 | 未开始 |
T1.6 | 实现基本图形绘制功能 | 开发者 A | 16 小时 | 中 | 未开始 |
T1.7 | 编写迭代 1 测试用例 | 测试工程师 | 8 小时 | 中 | 未开始 |
4.2 每日站会与进度跟踪
采用每日 15 分钟站会的方式跟踪项目进度,团队成员需回答以下三个问题:
- 昨天完成了什么?
- 今天计划做什么?
- 遇到了什么障碍?
进度跟踪工具:
- 使用 JIRA 管理任务和缺陷
- 采用燃尽图 (Burn-down Chart) 可视化迭代进度
- 每周生成进度报告
燃尽图示例:
plaintext
迭代1燃尽图
工时
80 | ────
| / \
60 | / \
| / \
40 |/ \
| \
20 | \
| \
0 |_______________\_____
第1天 第3天 第5天 第7天
日期
4.3 迭代评审与回顾
每个迭代结束后进行迭代评审和回顾会议:
迭代评审会议:
- 向产品负责人展示当前迭代完成的功能
- 收集反馈意见
- 确认功能是否符合需求
迭代回顾会议:
- 讨论迭代过程中做得好的方面
- 分析遇到的问题和改进措施
- 制定下一个迭代的改进计划
回顾会议记录示例:
做得好的方面 | 需要改进的方面 | 改进措施 |
---|---|---|
1. 硬件驱动开发进度超前 2. 团队沟通顺畅 3. 测试用例设计全面 | 1. 汉字显示功能遇到技术难题 2. 文档更新不及时 3. 部分任务预估工时不准确 | 1. 安排技术调研,解决汉字显示问题 2. 每天预留 30 分钟更新文档 3. 参考历史数据,改进工时预估 |
4.4 版本控制与文档管理
版本控制策略:
- 采用 Git 进行源代码管理
- 主分支:master(稳定版本)
- 开发分支:develop(开发版本)
- 特性分支:feature/*(新功能开发)
- 发布分支:release/*(版本发布准备)
- 热修复分支:hotfix/*(紧急 bug 修复)
提交规范:
plaintext
<类型>[可选作用域]: <描述>
[可选正文]
[可选脚注]
类型包括:feat (新功能)、fix (修复)、docs (文档)、style (格式)、refactor (重构)、test (测试)、chore (构建过程或辅助工具变动)
文档管理:
- 需求文档:记录系统需求和功能规格
- 设计文档:包含硬件设计和软件设计
- 用户手册:指导用户使用系统
- 开发手册:包含开发环境搭建和编码规范
- 测试文档:包含测试用例和测试报告
文档采用 Markdown 格式编写,存储在 Git 仓库中,与代码保持同步更新。
5. 核心技术实现
5.1 u8g2 图形引擎移植
u8g2 是一款开源的单色图形库,支持多种 OLED 和 LCD 显示屏,非常适合嵌入式系统使用。将 u8g2 移植到 STM32F405 平台需要以下步骤:
步骤 1:获取 u8g2 库
从 u8g2 官方仓库 (https://github.com/olikraus/u8g2) 获取最新版本的库文件,主要包括:
- u8g2.h:头文件
- u8g2.c:核心实现
- u8x8.h:底层驱动头文件
- u8x8.c:底层驱动实现
步骤 2:配置 u8g2
根据硬件配置修改 u8g2 的配置:
- 在 u8g2.h 中定义使用的显示屏型号:
c
运行
#define U8G2_SSD1327_MIDAS_128X128_1_HW_I2C u8g2_cb_r0
// 我们使用SPI接口,所以需要定义SPI相关的配置
#define U8G2_SSD1327_256X64_1_HW_SPI u8g2_cb_r0
- 实现 u8g2 的硬件接口函数:
c
运行
/**
* @brief 初始化u8g2
*/
void u8g2Init(u8g2_t *u8g2) {
// 初始化u8g2结构体
u8g2_Setup_ssd1327_256x64_1(u8g2, U8G2_R0,
u8x8_byte_4wire_hw_spi,
u8x8_gpio_and_delay_stm32);
// 初始化显示屏
u8g2_InitDisplay(u8g2);
u8g2_SetPowerSave(u8g2, 0); // 打开显示
u8g2_ClearBuffer(u8g2);
}
- 实现 SPI 通信函数:
c
运行
/**
* @brief 通过SPI发送数据
* @param u8x8: u8x8结构体指针
* @param msg: 数据指针
* @param len: 数据长度
*/
uint8_t u8x8_byte_4wire_hw_spi(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) {
switch(msg) {
case U8X8_MSG_BYTE_SEND:
// 通过SPI发送数据
HAL_SPI_Transmit(&hspi2, arg_ptr, arg_int, 100);
break;
case U8X8_MSG_BYTE_INIT:
// 初始化SPI和GPIO
MX_SPI2_Init();
// 初始化CS, DC, RST引脚
init_gpio();
break;
case U8X8_MSG_BYTE_SET_DC:
// 设置DC引脚状态
HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, arg_int);
break;
case U8X8_MSG_BYTE_SET_CS:
// 设置CS引脚状态
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, arg_int);
break;
case U8X8_MSG_DELAY_MILLI:
// 延时
HAL_Delay(arg_int);
break;
default:
return 0;
}
return 1;
}
步骤 3:测试 u8g2 功能
编写简单的测试代码验证 u8g2 是否正常工作:
c
运行
/**
* @brief 测试u8g2功能
*/
void testU8g2() {
u8g2_t u8g2;
// 初始化u8g2
u8g2Init(&u8g2);
// 绘制测试图形
u8g2_ClearBuffer(&u8g2);
// 绘制矩形
u8g2_DrawFrame(&u8g2, 0, 0, 255, 63);
// 绘制直线
u8g2_DrawLine(&u8g2, 0, 0, 255, 63);
// 绘制文本
u8g2_SetFont(&u8g2, u8g2_font_ncenB14_tr);
u8g2_DrawStr(&u8g2, 30, 30, "U8G2 Test");
// 刷新显示
u8g2_SendBuffer(&u8g2);
}
5.2 SSD1327 的 SPI 驱动实现
SSD1327 是一款用于 OLED 显示屏的驱动芯片,支持 SPI 和 I2C 两种通信方式。本项目采用 SPI 接口以提高数据传输速度,实现如下:
步骤 1:SPI 接口初始化
c
运行
SPI_HandleTypeDef hspi2;
/**
* @brief SPI2初始化函数
*/
void MX_SPI2_Init(void) {
hspi2.Instance = SPI2;
hspi2.Init.Mode = SPI_MODE_MASTER;
hspi2.Init.Direction = SPI_DIRECTION_1LINE;
hspi2.Init.DataSize = SPI_DATASIZE_8BIT;
hspi2.Init.CLKPolarity = SPI_POLARITY_LOW;
hspi2.Init.CLKPhase = SPI_PHASE_1EDGE;
hspi2.Init.NSS = SPI_NSS_SOFT;
hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2; // 84MHz / 2 = 42MHz,实际使用10MHz以下
hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi2.Init.TIMode = SPI_TIMODE_DISABLE;
hspi2.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
hspi2.Init.CRCPolynomial = 10;
if (HAL_SPI_Init(&hspi2) != HAL_OK) {
Error_Handler();
}
}
/**
* @brief SPI GPIO初始化
*/
void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(spiHandle->Instance==SPI2) {
// 使能时钟
__HAL_RCC_SPI2_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
// SPI2引脚配置:SCK=PB13, MOSI=PB15
GPIO_InitStruct.Pin = GPIO_PIN_13|GPIO_PIN_15;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF5_SPI2;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// CS=PB12, DC=PB14, RST=PB11
GPIO_InitStruct.Pin = GPIO_PIN_12|GPIO_PIN_14|GPIO_PIN_11;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// 初始化引脚状态
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET); // CS高电平
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_RESET); // DC低电平
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_SET); // RST高电平
}
}
步骤 2:SSD1327 初始化
SSD1327 需要发送一系列初始化命令才能正常工作:
c
运行
/**
* @brief 初始化SSD1327
*/
void SSD1327_Init() {
// 复位显示屏
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_RESET);
HAL_Delay(10);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_SET);
HAL_Delay(100);
// 发送初始化命令
SSD1327_SendCmd(0xAE); // 关闭显示
SSD1327_SendCmd(0x15); // 设置列地址
SSD1327_SendCmd(0x00); // 起始列
SSD1327_SendCmd(0x7F); // 结束列
SSD1327_SendCmd(0x75); // 设置行地址
SSD1327_SendCmd(0x00); // 起始行
SSD1327_SendCmd(0x3F); // 结束行
SSD1327_SendCmd(0x81); // 设置对比度
SSD1327_SendCmd(0x80); // 对比度值
SSD1327_SendCmd(0xA0); // 设置段重映射
SSD1327_SendCmd(0x51); // 水平翻转
SSD1327_SendCmd(0xA1); // 设置起始行
SSD1327_SendCmd(0x00); // 起始行0
SSD1327_SendCmd(0xA2); // 设置显示偏移
SSD1327_SendCmd(0x00); // 无偏移
SSD1327_SendCmd(0xA4); // 正常显示
SSD1327_SendCmd(0xA8); // 设置多路复用比
SSD1327_SendCmd(0x3F); // 64行
SSD1327_SendCmd(0xB1); // 设置相位长度
SSD1327_SendCmd(0xF1); // 相位1=15DCLK, 相位2=1DCLK
SSD1327_SendCmd(0xB3); // 设置显示时钟分频
SSD1327_SendCmd(0x00); // 分频=0, 时钟=OSC/1
SSD1327_SendCmd(0xAB); // 启用内部稳压器
SSD1327_SendCmd(0x01); //
SSD1327_SendCmd(0xB6); // 设置预充电周期
SSD1327_SendCmd(0x0F); //
SSD1327_SendCmd(0xBE); // 设置VCOMH
SSD1327_SendCmd(0x04); // VCOMH=0.83*VCC
SSD1327_SendCmd(0xBC); // 设置预充电电压
SSD1327_SendCmd(0x08); //
SSD1327_SendCmd(0xD5); // 禁用命令锁存
SSD1327_SendCmd(0x00); //
SSD1327_SendCmd(0xAF); // 打开显示
// 清屏
SSD1327_Clear();
}
步骤 3:实现命令和数据发送函数
c
运行
/**
* @brief 向SSD1327发送命令
* @param cmd: 命令字节
*/
void SSD1327_SendCmd(uint8_t cmd) {
// 拉低CS
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET);
// DC=0表示命令
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_RESET);
// 发送命令
HAL_SPI_Transmit(&hspi2, &cmd, 1, 100);
// 拉高CS
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET);
}
/**
* @brief 向SSD1327发送数据
* @param data: 数据指针
* @param len: 数据长度
*/
void SSD1327_SendData(uint8_t* data, uint32_t len) {
// 拉低CS
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET);
// DC=1表示数据
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_SET);
// 发送数据
HAL_SPI_Transmit(&hspi2, data, len, 100);
// 拉高CS
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET);
}
/**
* @brief 清屏
*/
void SSD1327_Clear() {
uint8_t data[128];
memset(data, 0x00, 128);
for(uint8_t page = 0; page < 8; page++) {
// 设置页地址
SSD1327_SendCmd(0xB0 + page);
// 设置列地址
SSD1327_SendCmd(0x00);
SSD1327_SendCmd(0x10);
// 发送数据
SSD1327_SendData(data, 128);
}
}
步骤 4:实现基本绘图函数
c
运行
/**
* @brief 设置显示窗口
* @param x: X坐标
* @param y: Y坐标
* @param width: 宽度
* @param height: 高度
*/
void SSD1327_SetWindow(uint8_t x, uint8_t y, uint8_t width, uint8_t height) {
// 转换为SSD1327的列地址格式
uint8_t start_col = x + 0x1C; // SSD1327的列偏移
uint8_t end_col = start_col + width - 1;
// 设置列地址
SSD1327_SendCmd(0x15);
SSD1327_SendCmd(start_col & 0x7F);
SSD1327_SendCmd(end_col & 0x7F);
// 设置行地址
uint8_t start_row = y;
uint8_t end_row = y + height - 1;
SSD1327_SendCmd(0x75);
SSD1327_SendCmd(start_row & 0x7F);
SSD1327_SendCmd(end_row & 0x7F);
}
/**
* @brief 绘制像素点
* @param x: X坐标
* @param y: Y坐标
* @param color: 颜色(0-15)
*/
void SSD1327_DrawPixel(uint8_t x, uint8_t y, uint8_t color) {
if(x >= 256 || y >= 64) return; // 超出范围
// 计算页和位位置
uint8_t page = y / 8;
uint8_t bit = y % 8;
// 设置地址
SSD1327_SendCmd(0xB0 + page);
SSD1327_SendCmd((x & 0x0F) + 0x1C); // 低4位
SSD1327_SendCmd((x >> 4) & 0x0F); // 高4位
// 读取当前数据
// 注意:SSD1327不支持读操作,需要软件缓存来实现像素级操作
// 这里简化处理,实际应用中需要维护一个显存缓存
}
5.3 汉字显示模块开发
在 OLED 显示屏上显示汉字是本项目的一个关键功能,需要解决汉字编码、字库存储和汉字绘制等问题。
步骤 1:汉字编码与字库选择
采用 GB2312 编码作为汉字编码标准,选择 16×16 和 24×24 两种点阵字库:
- 16×16 字库:用于一般文本显示
- 24×24 字库:用于标题和重要信息显示
字库存储方式:
- 将常用汉字的点阵数据存储在 STM32 的 Flash 中
- 采用压缩存储方式减少存储空间占用
步骤 2:字库文件生成
使用字库生成工具(如 PCtoLCD2002)生成汉字点阵数据:
- 选择 GB2312 编码
- 设置点阵大小(16×16 或 24×24)
- 选择横向取模,字节倒序
- 导出 C 语言数组格式的字库文件
示例 16×16 汉字 "飞" 的点阵数据:
c
运行
// "飞"字的16×16点阵数据
const uint8_t hanzi_fei[32] = {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xF8,0x00,0x08,0x00,0x08,0x00,0x08,0x00,0x08,
0x00,0x08,0x00,0x08,0x00,0x08,0x00,0x08,0x00,0xF8,0x00,0x00,0x00,0x00,0x00,0x00
};
步骤 3:实现汉字显示功能
c
运行
/**
* @brief 绘制16×16汉字
* @param x: X坐标
* @param y: Y坐标
* @param hanzi: 汉字点阵数据指针
* @param color: 颜色(0-15)
*/
void U8G2Engine::drawHanzi16x16(uint16_t x, uint16_t y, const uint8_t* hanzi, uint8_t color) {
// 设置字体颜色对应的灰度值
uint8_t gray = (color & 0x0F) << 4; // SSD1327使用高4位表示灰度
for(uint8_t row = 0; row < 16; row++) {
for(uint8_t col = 0; col < 2; col++) {
uint8_t data = hanzi[row * 2 + col];
for(uint8_t bit = 0; bit < 8; bit++) {
if(data & (1 << (7 - bit))) {
// 绘制像素点
u8g2_DrawPixel(&u8g2, x + col * 8 + bit, y + row);
}
}
}
}
}
/**
* @brief 根据GB2312编码查找汉字
* @param code: GB2312编码
* @return 汉字点阵数据指针,找不到返回NULL
*/
const uint8_t* findHanzi(uint16_t code) {
// 计算在字库中的索引
// GB2312编码分为区(0xA1-0xF7)和位(0xA1-0xFE)
uint8_t area = (code >> 8) - 0xA0;
uint8_t pos = (code & 0xFF) - 0xA0;
// 检查是否在有效范围内
if(area < 1 || area > 94 || pos < 1 || pos > 94) {
return NULL;
}
// 计算偏移量
uint32_t index = (area - 1) * 94 + (pos - 1);
// 检查是否超出字库范围
if(index >= HANZI_COUNT) {
return NULL;
}
// 返回对应的点阵数据
return &hanzi_font16x16[index * 32];
}
/**
* @brief 绘制GB2312编码的汉字
* @param x: X坐标
* @param y: Y坐标
* @param code: GB2312编码
* @param size: 字体大小(1:16x16, 2:24x24)
* @param color: 颜色(0-15)
*/
void U8G2Engine::drawHanzi(uint16_t x, uint16_t y, uint16_t code, uint8_t size, uint8_t color) {
const uint8_t* hanzi;
switch(size) {
case 1: // 16x16
hanzi = findHanzi(code);
if(hanzi) {
drawHanzi16x16(x, y, hanzi, color);
}
break;
case 2: // 24x24
hanzi = findHanzi24(code);
if(hanzi) {
drawHanzi24x24(x, y, hanzi, color);
}
break;
default:
break;
}
}
步骤 4:实现字符串显示功能
c
运行
/**
* @brief 绘制中文字符串
* @param x: X坐标
* @param y: Y坐标
* @param str: 字符串(GB2312编码)
* @param size: 字体大小
* @param color: 颜色
* @return 字符串宽度
*/
uint16_t U8G2Engine::drawHanziString(uint16_t x, uint16_t y, const char* str, uint8_t size, uint8_t color) {
uint16_t currentX = x;
uint8_t charWidth = (size == 1) ? 16 : 24;
while(*str) {
// 判断是否为汉字(高字节大于0x80)
if((uint8_t)*str > 0x80) {
// 汉字由两个字节组成
uint16_t code = (*str << 8) | *(str + 1);
drawHanzi(currentX, y, code, size, color);
currentX += charWidth;
str += 2;
} else {
// ASCII字符
u8g2_DrawGlyph(&u8g2, currentX, y + charWidth - 2, *str);
currentX += charWidth / 2;
str++;
}
}
return currentX - x;
}
5.4 图形元素绘制实现
遥控器界面需要多种图形元素来直观展示信息,如箭头、进度条、仪表盘等,实现如下:
1. 基本图形元素
c
运行
/**
* @brief 绘制箭头
* @param x: 中心点X坐标
* @param y: 中心点Y坐标
* @param length: 箭头长度
* @param angle: 角度(度)
* @param color: 颜色
*/
void U8G2Engine::drawArrow(uint16_t x, uint16_t y, uint16_t length, float angle, uint8_t color) {
// 将角度转换为弧度
float rad = angle * 3.1415926 / 180.0;
// 计算箭头尖端坐标
int16_t x1 = x + (int16_t)(cos(rad) * length);
int16_t y1 = y - (int16_t)(sin(rad) * length);
// 计算箭头两翼坐标
float rad1 = rad + 150.0 * 3.1415926 / 180.0;
float rad2 = rad - 150.0 * 3.1415926 / 180.0;
int16_t x2 = x + (int16_t)(cos(rad1) * length / 2);
int16_t y2 = y - (int16_t)(sin(rad1) * length / 2);
int16_t x3 = x + (int16_t)(cos(rad2) * length / 2);
int16_t y3 = y - (int16_t)(sin(rad2) * length / 2);
// 绘制箭头
u8g2_DrawLine(&u8g2, x, y, x1, y1);
u8g2_DrawLine(&u8g2, x1, y1, x2, y2);
u8g2_DrawLine(&u8g2, x1, y1, x3, y3);
}
/**
* @brief 绘制进度条
* @param x: X坐标
* @param y: Y坐标
* @param width: 宽度
* @param height: 高度
* @param value: 进度值(0-100)
* @param color: 颜色
*/
void U8G2Engine::drawProgressBar(uint16_t x, uint16_t y, uint16_t width, uint16_t height,
uint8_t value, uint8_t color) {
// 绘制边框
u8g2_DrawFrame(&u8g2, x, y, width, height);
// 计算进度宽度
uint16_t progressWidth = (uint16_t)(width * value / 100.0);
// 绘制进度
if(progressWidth > 0) {
u8g2_DrawBox(&u8g2, x + 1, y + 1, progressWidth - 1, height - 1);
}
}
/**
* @brief 绘制仪表盘
* @param x: 中心X坐标
* @param y: 中心Y坐标
* @param radius: 半径
* @param value: 数值(0-100)
* @param min: 最小值
* @param max: 最大值
* @param color: 颜色
*/
void U8G2Engine::drawGauge(uint16_t x, uint16_t y, uint16_t radius,
float value, float min, float max, uint8_t color) {
// 绘制外圆
u8g2_DrawCircle(&u8g2, x, y, radius, U8G2_DRAW_ALL);
// 计算角度范围(-135度到+135度)
float angleRange = 270.0; // 总共270度
float angle = -135.0 + angleRange * (value - min) / (max - min);
// 限制角度范围
if(angle < -135.0) angle = -135.0;
if(angle > 135.0) angle = 135.0;
// 绘制指针
float rad = angle * 3.1415926 / 180.0;
int16_t x1 = x + (int16_t)(cos(rad) * radius * 0.8);
int16_t y1 = y - (int16_t)(sin(rad) * radius * 0.8);
u8g2_DrawLine(&u8g2, x, y, x1, y1);
// 绘制中心
u8g2_DrawDisc(&u8g2, x, y, 3, U8G2_DRAW_ALL);
}
/**
* @brief 绘制电池图标
* @param x: X坐标
* @param y: Y坐标
* @param width: 宽度
* @param height: 高度
* @param level: 电量(0-100)
* @param color: 颜色
*/
void U8G2Engine::drawBattery(uint16_t x, uint16_t y, uint16_t width, uint16_t height,
uint8_t level, uint8_t color) {
// 绘制电池主体
u8g2_DrawFrame(&u8g2, x, y, width, height);
// 绘制电池正极
u8g2_DrawBox(&u8g2, x + width, y + height / 4, width / 5, height / 2);
// 绘制电量
uint16_t batteryLevel = (uint16_t)(width * 0.8 * level / 100.0);
if(batteryLevel > 0) {
u8g2_DrawBox(&u8g2, x + width / 10, y + height / 10,
batteryLevel, height * 0.8);
}
// 低电量警告
if(level < 20) {
u8g2_DrawLine(&u8g2, x + width / 10, y + height / 10,
x + width * 0.9, y + height * 0.9);
u8g2_DrawLine(&u8g2, x + width * 0.9, y + height / 10,
x + width / 10, y + height * 0.9);
}
}
/**
* @brief 绘制信号强度图标
* @param x: X坐标
* @param y: Y坐标
* @param size: 大小
* @param strength: 信号强度(0-100)
* @param color: 颜色
*/
void U8G2Engine::drawSignalStrength(uint16_t x, uint16_t y, uint16_t size,
uint8_t strength, uint8_t color) {
uint16_t barWidth = size / 5;
uint16_t maxBarHeight = size;
// 绘制5个信号条
for(uint8_t i = 0; i < 5; i++) {
// 计算每个条的高度
uint8_t barHeight = (uint8_t)(maxBarHeight * (i + 1) / 5);
// 根据信号强度决定是否填充
if(strength >= (i + 1) * 20) {
u8g2_DrawBox(&u8g2, x + i * (barWidth + 2), y + (maxBarHeight - barHeight),
barWidth, barHeight);
} else {
u8g2_DrawFrame(&u8g2, x + i * (barWidth + 2), y + (maxBarHeight - barHeight),
barWidth, barHeight);
}
}
}
5.5 动画效果实现
为提升用户体验,实现简单的动画效果,如页面切换、数据更新动画等:
c
运行
/**
* @brief 淡入动画
* @param duration: 动画持续时间(毫秒)
*/
void U8G2Engine::fadeIn(uint32_t duration) {
uint32_t startTime = HAL_GetTick();
uint32_t frameTime = duration / 16; // 16级灰度
for(uint8_t contrast = 0; contrast <= 0xFF; contrast += 0x11) {
// 设置对比度
u8g2_SetContrast(&u8g2, contrast);
// 延时
while(HAL_GetTick() - startTime < (contrast / 0x11 + 1) * frameTime);
}
}
/**
* @brief 淡出动画
* @param duration: 动画持续时间(毫秒)
*/
void U8G2Engine::fadeOut(uint32_t duration) {
uint32_t startTime = HAL_GetTick();
uint32_t frameTime = duration / 16;
for(uint8_t contrast = 0xFF; contrast > 0; contrast -= 0x11) {
// 设置对比度
u8g2_SetContrast(&u8g2, contrast);
// 延时
while(HAL_GetTick() - startTime < (16 - contrast / 0x11) * frameTime);
}
// 最后关闭显示
u8g2_SetContrast(&u8g2, 0);
}
/**
* @brief 滑动动画
* @param direction: 方向(0:左,1:右,2:上,3:下)
* @param duration: 动画持续时间(毫秒)
* @param newPage: 新页面绘制函数
*/
void DisplayUI::slideAnimation(uint8_t direction, uint32_t duration, void (*newPage)()) {
uint32_t startTime = HAL_GetTick();
uint32_t frameCount = 20; // 20帧动画
uint32_t frameTime = duration / frameCount;
// 获取当前屏幕缓冲区
uint8_t* buffer = u8g2_GetBufferPtr(&u8g2Engine.u8g2);
uint8_t* tempBuffer = (uint8_t*)malloc(256 * 64 / 8);
memcpy(tempBuffer, buffer, 256 * 64 / 8);
// 绘制新页面到缓冲区
u8g2_ClearBuffer(&u8g2Engine.u8g2);
newPage();
// 动画帧循环
for(uint8_t i = 0; i < frameCount; i++) {
uint32_t currentTime = HAL_GetTick();
// 计算偏移量
int16_t offset;
switch(direction) {
case 0: // 左滑
offset = (int16_t)(256 * (frameCount - i) / frameCount);
break;
case 1: // 右滑
offset = (int16_t)(-256 * (frameCount - i) / frameCount);
break;
case 2: // 上滑
offset = (int16_t)(64 * (frameCount - i) / frameCount);
break;
case 3: // 下滑
offset = (int16_t)(-64 * (frameCount - i) / frameCount);
break;
default:
offset = 0;
break;
}
// 绘制当前帧
u8g2_ClearBuffer(&u8g2Engine.u8g2);
if(direction < 2) { // 左右滑动
// 绘制旧页面
u8g2_DrawXBM(&u8g2Engine.u8g2, offset, 0, 256, 64, tempBuffer);
// 绘制新页面
u8g2_DrawXBM(&u8g2Engine.u8g2, offset - 256 * direction + 256 * (direction == 0),
0, 256, 64, u8g2_GetBufferPtr(&u8g2Engine.u8g2));
} else { // 上下滑动
// 绘制旧页面
u8g2_DrawXBM(&u8g2Engine.u8g2, 0, offset, 256, 64, tempBuffer);
// 绘制新页面
u8g2_DrawXBM(&u8g2Engine.u8g2, 0, offset - 64 * (direction - 2) + 64 * (direction == 2),
256, 64, u8g2_GetBufferPtr(&u8g2Engine.u8g2));
}
// 刷新显示
u8g2_SendBuffer(&u8g2Engine.u8g2);
// 延时
if(i < frameCount - 1) {
while(HAL_GetTick() - currentTime < frameTime);
}
}
free(tempBuffer);
}
/**
* @brief 数值变化动画
* @param start: 起始值
* @param end: 结束值
* @param duration: 动画持续时间(毫秒)
* @param drawFunc: 绘制函数
*/
void DisplayUI::valueAnimation(float start, float end, uint32_t duration,
void (*drawFunc)(float value)) {
uint32_t startTime = HAL_GetTick();
uint32_t frameCount = 30;
uint32_t frameTime = duration / frameCount;
for(uint8_t i = 0; i < frameCount; i++) {
// 计算当前值
float progress = (float)i / (frameCount - 1);
float currentValue = start + (end - start) * progress;
// 绘制当前值
drawFunc(currentValue);
// 刷新显示
u8g2_SendBuffer(&u8g2Engine.u8g2);
// 延时
if(i < frameCount - 1) {
HAL_Delay(frameTime);
}
}
}
5.6 遥控器界面布局设计
无人机遥控器界面采用多页面设计,包括飞行数据页、设置页和告警页:
1. 飞行数据页面
飞行数据页面显示无人机的关键飞行参数,布局如下:
c
运行
/**
* @brief 绘制飞行数据页面
*/
void FlightPage::draw() {
// 清屏
u8g2_ClearBuffer(&u8g2);
// 绘制标题
u8g2_SetFont(&u8g2, u8g2_font_ncenB14_tr);
u8g2_DrawStr(&u8g2, 100, 15, "飞行数据");
// 绘制高度和速度
char buf[16];
u8g2_SetFont(&u8g2, u8g2_font_ncenB12_tr);
sprintf(buf, "高度: %.1fm", flightData.altitude);
u8g2_DrawStr(&u8g2, 10, 35, buf);
sprintf(buf, "速度: %.1fm/s", flightData.speed);
u8g2_DrawStr(&u8g2, 10, 55, buf);
// 绘制航向角
u8g2_DrawStr(&u8g2, 150, 35, "航向:");
sprintf(buf, "%.0f°", flightData.heading);
u8g2_DrawStr(&u8g2, 200, 35, buf);
// 绘制航向箭头
u8g2Engine.drawArrow(2