基于 STM32F405 与 SSD1327 的无人机遥控器 OLED 界面开发指南

目录

  1. 项目概述与目标
  2. 硬件系统设计
    2.1 硬件组合框图
    2.2 核心组件选型与特性
    2.3 电路原理图设计
    2.4 硬件调试要点
  3. 软件系统设计
    3.1 软件架构与组合框图
    3.2 UML 建模
    3.3 模块划分与接口定义
    3.4 数据结构设计
  4. 敏捷开发流程实施
    4.1 迭代计划与任务分解
    4.2 每日站会与进度跟踪
    4.3 迭代评审与回顾
    4.4 版本控制与文档管理
  5. 核心技术实现
    5.1 u8g2 图形引擎移植
    5.2 SSD1327 的 SPI 驱动实现
    5.3 汉字显示模块开发
    5.4 图形元素绘制实现
    5.5 动画效果实现
    5.6 遥控器界面布局设计
  6. 测试验证方案
    6.1 单元测试用例与结果
    6.2 集成测试流程与数据
    6.3 性能测试指标与分析
    6.4 用户体验测试反馈
  7. 性能优化策略
    7.1 关键性能指标定义
    7.2 代码优化方法
    7.3 显示效率提升技巧
    7.4 优化前后对比数据
  8. 结论与展望

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℃,满足各种环境下的使用需求
硬件特性对比表
组件关键参数优势应用价值
STM32F405168MHz, 1MB Flash, 192KB RAM高性能,丰富外设支持复杂界面渲染和多模块控制
SSD1327256×64, 16 级灰度,SPI高对比度,低功耗清晰显示各类信息,延长续航
nRF24L012.4GHz, 1Mbps, 100 米距离低功耗,高速率实时传输飞行数据
MPU60503 轴 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 硬件调试要点

  1. SPI 通信测试

    • 先测试 SPI 基本通信功能,可通过读取 SSD1327 的 ID 寄存器验证
    • 确保 SPI 时钟频率匹配(SSD1327 支持最高 10MHz)
    • 检查 CS、DC 信号的时序是否正确
  2. 显示屏初始化测试

    • 编写简单的初始化程序,测试显示屏是否能正常点亮
    • 绘制简单图形(如矩形)验证显示功能
  3. 电源稳定性测试

    • 测量不同工作状态下的电流消耗
    • 测试电池电压下降时系统的稳定性
    • 验证低电量检测功能是否准确
  4. 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 模块划分与接口定义

核心模块划分
  1. 驱动模块

    • SSD1327 驱动:负责 OLED 显示屏的初始化和数据传输
    • SPI 驱动:实现 SPI 通信功能
    • 按键驱动:负责按键扫描和状态检测
  2. 界面模块

    • 图形引擎封装:封装 u8g2 库的功能
    • 界面管理:负责不同页面的切换和管理
    • 字体管理:处理汉字和 ASCII 字符的显示
  3. 应用模块

    • 飞行参数显示:处理和显示飞行相关数据
    • 系统设置:管理系统参数和配置
    • 告警处理:处理和显示告警信息
关键接口定义
  1. 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);
  1. 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();
  1. InputHandler 类接口

c

运行

/**
 * @brief 初始化输入处理
 * @param ui: 显示界面指针
 */
void InputHandler::init(DisplayUI* ui);

/**
 * @brief 处理输入
 */
void InputHandler::handleInput();

/**
 * @brief 设置按键回调函数
 * @param callback: 回调函数指针
 */
void InputHandler::setCallback(InputCallback callback);

3.4 数据结构设计

  1. 飞行数据结构体

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;
  1. 系统状态结构体

c

运行

/**
 * @brief 系统状态结构体
 */
typedef struct {
    uint8_t brightness;   // 亮度,0-100
    uint8_t volume;       // 音量,0-100
    uint8_t language;     // 语言设置
    uint32_t runtime;     // 运行时间,单位:秒
    uint8_t lowBatteryThreshold; // 低电量阈值
} SystemState;
  1. 告警信息结构体

c

运行

/**
 * @brief 告警信息结构体
 */
typedef struct {
    uint8_t type;         // 告警类型
    uint8_t level;        // 告警级别
    char message[32];     // 告警消息
    uint32_t timestamp;   // 时间戳
    bool active;          // 是否为活跃告警
} AlarmInfo;
  1. 页面定义枚举

c

运行

/**
 * @brief 页面枚举
 */
typedef enum {
    PAGE_FLIGHT = 0,      // 飞行页面
    PAGE_SETTINGS,        // 设置页面
    PAGE_ALARM,           // 告警页面
    PAGE_INFO,            // 信息页面
    PAGE_COUNT            // 页面总数
} PageType;
  1. 飞行模式枚举

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 开发环境开发者 A8 小时未开始
T1.2设计硬件原理图硬件工程师16 小时未开始
T1.3制作测试电路板硬件工程师24 小时未开始
T1.4移植 u8g2 库到 STM32开发者 B16 小时未开始
T1.5实现 SSD1327 初始化开发者 B8 小时未开始
T1.6实现基本图形绘制功能开发者 A16 小时未开始
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 的配置:

  1. 在 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
  1. 实现 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);
}
  1. 实现 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)生成汉字点阵数据:

  1. 选择 GB2312 编码
  2. 设置点阵大小(16×16 或 24×24)
  3. 选择横向取模,字节倒序
  4. 导出 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值