目录
引言
无人机地面多功能显示器(MFD)是飞行控制的核心交互设备,需实时呈现关键飞行参数并支持快速操作。本方案采用 STM32 微处理器直接通过 QSPI 接口驱动 7 寸液晶屏,集成 CAN FD 总线实现与无人机飞控系统的高速数据交互,通过物理按键完成操作,专注于无人机专用参数显示(剩余电量、电机转速、悬停高度等),兼顾可靠性与实时性。
项目概述
核心目标
硬件选型与设计
核心控制器选型
选用STM32H743VIT6作为主控制器,关键参数:
内核 | ARM Cortex-M7,主频 400MHz | 高性能处理能力,支持复杂图形渲染与 CAN FD 实时处理 |
外设 | 2x QSPI 接口(支持 800*480 屏驱动)、2x CAN FD 控制器 | 原生 QSPI 与 CAN FD 外设,无需额外扩展 |
内存 | 1MB SRAM、2MB Flash | 满足 LVGL 图形缓存(800*480@16bit 需约 768KB)与程序存储 |
工作温度 | -40℃~85℃ | 适应无人机户外作业环境 |
显示设备选型
7 寸 800*480 QSPI 接口液晶屏参数:
尺寸 | 7 英寸 | 平衡显示面积与便携性 |
分辨率 | 800*480(4:3) | 适合多参数分区显示 |
接口 | QSPI(4 线模式,支持双向数据传输) | 直接与 STM32 QSPI 外设连接,传输速率最高 80MHz |
亮度 | 400cd/m² | 户外阳光下可视 |
背光 | LED(支持 PWM 亮度调节) | 可通过 STM32 GPIO 控制功耗 |
工作电压 | 3.3V | 与 STM32 供电兼容 |
关键外设模块
CAN FD 收发器:
- 型号:TJA1044(支持 CAN FD,最高 8Mbps)
- 功能:将 STM32 的 CAN FD 信号转换为差分信号,与无人机飞控 CAN 总线连接。
功能按键:
- 数量:5 个(电源键、菜单键、上 / 下选择键、确认键)
- 类型:轻触按键(带硬件消抖),下拉输入模式。
电源模块:
- 输入:DC 12V(无人机供电系统常见电压)
- 输出:3.3V/2A(给 STM32、液晶屏供电),5V/1A(备用)。
硬件连接设计
1. STM32 与 QSPI 液晶屏连接
PB2(QSPI_CLK) | 时钟线 | CLK | QSPI 时钟信号(最高 80MHz) |
PD11(QSPI_NCS) | 片选 | CS | 低电平有效,选中液晶屏 |
PF8(QSPI_D0) | 数据输入 | D0 | 主机接收 / 从机发送数据 |
PF9(QSPI_D1) | 数据输出 | D1 | 主机发送 / 从机接收数据 |
PF10(QSPI_D2) | 数据输入 2 | D2 | 可选,4 线模式增强传输(本方案启用) |
PG12(QSPI_D3) | 数据输出 2 | D3 | 可选,4 线模式增强传输(本方案启用) |
PA0 | 复位 | RST | 低电平复位液晶屏(默认上拉) |
PA1 | 背光控制 | BL | PWM 输出调节亮度(高电平使能) |
3.3V | 电源 | VCC | 液晶屏供电 |
GND | 接地 | GND | 共地连接 |
2. STM32 与 CAN FD 总线连接
PD0(CAN1_RX) | 接收数据 | RXD | CAN FD 接收信号 |
PD1(CAN1_TX) | 发送数据 | TXD | CAN FD 发送信号 |
3.3V | 电源 | VCC | 收发器供电 |
GND | 接地 | GND | 共地连接 |
– | 总线接口 | CAN_H/CAN_L | 连接至无人机飞控 CAN 总线 |
3. 功能按键连接
电源键 | PC0 | 下拉输入,按下接 3.3V |
菜单键 | PC1 | 下拉输入,按下接 3.3V |
上选择键 | PC2 | 下拉输入,按下接 3.3V |
下选择键 | PC3 | 下拉输入,按下接 3.3V |
确认键 | PC4 | 下拉输入,按下接 3.3V |
开发环境搭建
核心工具
STM32CubeIDE | 1.14.0 | 代码开发、编译、调试一体化环境 |
STM32CubeMX | 6.10.0 | 外设配置(QSPI、CAN FD、GPIO 等) |
LVGL | v8.3 | 嵌入式图形库,适配 800*480 分辨率 |
CANoe | 11.0 | CAN FD 总线数据仿真与测试 |
ST-Link V3 | – | 程序下载与硬件调试 |
环境配置步骤
1. STM32CubeMX 配置
选择芯片 “STM32H743VITx”,配置核心外设:
- QSPI:模式 “Memory-mapped”,时钟 80MHz,数据宽度 4 线(D0-D3)。
- CAN FD:波特率 “500kbps(仲裁段)+8Mbps(数据段)”,使能中断。
- GPIO:配置 5 个按键引脚为 “输入下拉”,PA1(背光)为 “TIM2_CH1 PWM 输出”。
- RCC:外部高速时钟(25MHz),系统时钟 400MHz。
- TIM:TIM6 作为 LVGL 时基(1ms 中断)。
生成工程(STM32CubeIDE 格式),启用 FreeRTOS(用于任务调度)。
2. LVGL 移植配置
c
运行
#define LV_HOR_RES_MAX 800
#define LV_VER_RES_MAX 480
#define LV_COLOR_DEPTH 16 // 16位色(RGB565)
#define LV_MEM_SIZE (768 * 1024) // 768KB缓存(刚好容纳一帧800*480@16bit)
#define LV_TICK_CUSTOM 1 // 使用TIM6作为时基
#define LV_USE_GAUGE 1 // 启用仪表组件
#define LV_USE_BAR 1 // 启用进度条组件
- 实现disp_init():初始化 QSPI 接口与液晶屏(发送初始化指令)。
- 实现disp_flush_cb():将 LVGL 的图像数据通过 QSPI 写入液晶屏指定区域。
界面设计与实现(800*480)
整体布局设计
针对无人机专用参数,界面划分为 5 个区域:
状态栏 | 800*30 | 顶部(0,0) | 系统时间、飞行模式(手动 / 自动)、GPS 状态 |
主参数区 | 400*300 | 左侧(0,30) | 悬停高度(大字体)、剩余电量(百分比 + 进度条) |
电机转速区 | 400*300 | 右侧(400,30) | 4 路电机转速表(圆形仪表,0-100%) |
辅助参数区 | 800*120 | 底部(0,330) | 飞行速度、电池电压、续航时间预估 |
按键提示区 | 800*30 | 最底部(0,450) | 当前界面可用按键功能提示(如 “菜单键:切换视图”) |
核心仪表实现
1. 悬停高度显示(主参数区)
c
运行
void altitude_display_init(lv_obj_t *parent) {
// 创建容器(400*300)
lv_obj_t *container = lv_obj_create(parent);
lv_obj_set_size(container, 400, 300);
lv_obj_align(container, LV_ALIGN_TOP_LEFT, 0, 30);
lv_obj_set_style_bg_color(container, lv_color_hex(0x000000), LV_PART_MAIN);
// 高度值标签(大字体)
lv_obj_t *alt_label = lv_label_create(container);
lv_obj_set_style_text_font(alt_label, &lv_font_montserrat_60, LV_PART_MAIN);
lv_obj_set_style_text_color(alt_label, lv_color_hex(0x00FF00), LV_PART_MAIN);
lv_label_set_text(alt_label, "0.0m");
lv_obj_align(alt_label, LV_ALIGN_CENTER, 0, -30);
// 高度单位标签
lv_obj_t *unit_label = lv_label_create(container);
lv_obj_set_style_text_font(unit_label, &lv_font_montserrat_24, LV_PART_MAIN);
lv_label_set_text(unit_label, "悬停高度");
lv_obj_align(unit_label, LV_ALIGN_CENTER, 0, 30);
}
// 更新高度值
void altitude_update(float height) {
char buf[10];
sprintf(buf, "%.1fm", height);
lv_label_set_text(alt_label, buf);
// 高度异常(>50m或<0)时变红
if (height < 0 || height > 50) {
lv_obj_set_style_text_color(alt_label, lv_color_hex(0xFF0000), LV_PART_MAIN);
} else {
lv_obj_set_style_text_color(alt_label, lv_color_hex(0x00FF00), LV_PART_MAIN);
}
}
2. 剩余电量显示(主参数区下方)
c
运行
void battery_display_init(lv_obj_t *parent) {
// 创建电池容器(400*50)
lv_obj_t *container = lv_obj_create(parent);
lv_obj_set_size(container, 400, 50);
lv_obj_align(container, LV_ALIGN_TOP_LEFT, 0, 330-50); // 主参数区下方
// 电量进度条
lv_obj_t *bat_bar = lv_bar_create(container);
lv_obj_set_size(bat_bar, 200, 20);
lv_obj_align(bat_bar, LV_ALIGN_CENTER, -50, 0);
lv_bar_set_range(bat_bar, 0, 100);
// 电量百分比标签
lv_obj_t *bat_label = lv_label_create(container);
lv_obj_set_style_text_font(bat_label, &lv_font_montserrat_20, LV_PART_MAIN);
lv_label_set_text(bat_label, "100%");
lv_obj_align(bat_label, LV_ALIGN_CENTER, 80, 0);
}
// 更新电量
void battery_update(uint8_t percentage) {
lv_bar_set_value(bat_bar, percentage, LV_ANIM_ON);
char buf[5];
sprintf(buf, "%d%%", percentage);
lv_label_set_text(bat_label, buf);
// 低电量(<20%)时进度条变红
if (percentage < 20) {
lv_obj_set_style_bg_color(bat_bar, lv_color_hex(0xFF0000), LV_PART_INDICATOR);
} else {
lv_obj_set_style_bg_color(bat_bar, lv_color_hex(0x00FF00), LV_PART_INDICATOR);
}
}
3. 电机转速表(右侧 400*300 区域)
c
运行
void motor_gauge_init(lv_obj_t *parent) {
// 创建4个电机仪表(2×2布局)
for (int i = 0; i < 4; i++) {
lv_obj_t *gauge = lv_gauge_create(parent);
lv_obj_set_size(gauge, 180, 180);
// 设置位置(2×2网格)
int x = (i%2) * 200 + 10;
int y = (i/2) * 150 + 10;
lv_obj_align(gauge, LV_ALIGN_TOP_LEFT, 400 + x, 30 + y);
// 配置仪表范围(0-100%)
lv_gauge_set_range(gauge, 0, 100);
lv_gauge_set_critical_value(gauge, 90); // 90%以上为警戒值
lv_gauge_add_needle_line(gauge, lv_color_hex(0x00FF00), 3, 50); // 绿色指针
// 电机编号标签
lv_obj_t *label = lv_label_create(gauge);
lv_label_set_text_fmt(label, "电机%d", i+1);
lv_obj_align(label, LV_ALIGN_BOTTOM_MID, 0, -10);
}
}
// 更新电机转速(0-100%)
void motor_speed_update(uint8_t motor_id, uint8_t speed) {
if (motor_id >= 4) return;
lv_gauge_set_value(motors[motor_id], 0, speed);
// 超警戒值(>90%)时指针变红
if (speed > 90) {
lv_gauge_set_needle_color(motors[motor_id], 0, lv_color_hex(0xFF0000));
} else {
lv_gauge_set_needle_color(motors[motor_id], 0, lv_color_hex(0x00FF00));
}
}
核心功能开发
1. QSPI 液晶屏驱动
c
运行
// 液晶屏初始化(发送QSPI命令)
void qspi_lcd_init(void) {
// 复位液晶屏
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);
HAL_Delay(100);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);
HAL_Delay(100);
// 初始化QSPI接口(STM32CubeMX已配置,此处发送屏初始化指令)
qspi_send_cmd(0x11); // 退出睡眠模式
HAL_Delay(120);
qspi_send_cmd(0x29); // 开启显示
qspi_send_cmd(0x36, 0x00); // 设置显示方向
}
// 通过QSPI发送命令
static void qspi_send_cmd(uint8_t cmd, uint8_t data) {
// 片选使能
HAL_QSPI_Command(&hqspi, &(QSPI_CommandTypeDef){
.Instruction = cmd,
.InstructionMode = QSPI_INSTRUCTION_1_LINE,
.AddressMode = QSPI_ADDRESS_NONE,
.DataMode = QSPI_DATA_1_LINE,
.DummyCycles = 0,
.NbData = 1,
.DdrMode = QSPI_DDR_MODE_DISABLE,
.DdrHoldHalfCycle = QSPI_DDR_HHC_DISABLE,
.SIOOMode = QSPI_SIOO_INST_EVERY_CMD
}, HAL_QPSI_TIMEOUT_DEFAULT_VALUE);
// 发送数据
HAL_QSPI_Transmit(&hqspi, &data, HAL_QPSI_TIMEOUT_DEFAULT_VALUE);
// 片选禁用
HAL_QSPI_Abort(&hqspi);
}
// LVGL刷新回调(将图像数据写入液晶屏)
static void disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) {
// 设置液晶屏显示区域(area->x1, area->y1到area->x2, area->y2)
lcd_set_window(area->x1, area->y1, area->x2, area->y2);
// 计算数据长度(像素数×2字节/像素)
uint32_t length = (area->x2 – area->x1 + 1) * (area->y2 – area->y1 + 1) * 2;
// 通过QSPI发送图像数据
HAL_QSPI_Command(&hqspi, &(QSPI_CommandTypeDef){
.Instruction = 0x2C, // 写入GRAM命令
.InstructionMode = QSPI_INSTRUCTION_1_LINE,
.AddressMode = QSPI_ADDRESS_NONE,
.DataMode = QSPI_DATA_4_LINES, // 4线模式加速传输
.DummyCycles = 0,
.NbData = length,
.DdrMode = QSPI_DDR_MODE_DISABLE,
.DdrHoldHalfCycle = QSPI_DDR_HHC_DISABLE,
.SIOOMode = QSPI_SIOO_INST_EVERY_CMD
}, HAL_QPSI_TIMEOUT_DEFAULT_VALUE);
HAL_QSPI_Transmit(&hqspi, (uint8_t*)color_p, HAL_QPSI_TIMEOUT_DEFAULT_VALUE);
HAL_QSPI_Abort(&hqspi);
// 通知LVGL刷新完成
lv_disp_flush_ready(disp);
}
2. CAN FD 数据接收与解析
c
运行
// CAN FD初始化
void can_fd_init(void) {
// 过滤器配置(接收所有无人机数据帧)
CAN_FilterTypeDef filter = {
.FilterActivation = ENABLE,
.FilterBank = 0,
.FilterMode = CAN_FILTERMODE_IDMASK,
.FilterScale = CAN_FILTERSCALE_32BIT,
.FilterIdHigh = 0x0000,
.FilterIdLow = 0x0000,
.FilterMaskIdHigh = 0x0000,
.FilterMaskIdLow = 0x0000,
.FilterFIFOAssignment = CAN_RX_FIFO0
};
HAL_CAN_ConfigFilter(&hcan1, &filter);
// 启动CAN FD并使能接收中断
HAL_CAN_Start(&hcan1);
HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);
}
// CAN FD接收中断回调
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
CAN_RxHeaderTypeDef rx_header;
uint8_t rx_data[64]; // CAN FD最大数据长度64字节
if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rx_header, rx_data) == HAL_OK) {
// 根据ID解析数据(无人机自定义协议)
switch (rx_header.StdId) {
case 0x101: // 飞行状态帧(高度、电量等)
parse_flight_status(rx_data, rx_header.DLC);
break;
case 0x102: // 电机状态帧
parse_motor_status(rx_data, rx_header.DLC);
break;
// 其他帧类型…
}
}
}
// 解析飞行状态帧(示例:rx_data[0-3]为高度,rx_data[4]为电量)
static void parse_flight_status(uint8_t *data, uint8_t len) {
if (len < 5) return;
// 高度(float类型,小端格式)
float height;
memcpy(&height, data, 4);
altitude_update(height);
// 电量(百分比)
battery_update(data[4]);
}
// 解析电机状态帧(示例:rx_data[0-3]为4路电机转速百分比)
static void parse_motor_status(uint8_t *data, uint8_t len) {
if (len < 4) return;
for (int i = 0; i < 4; i++) {
motor_speed_update(i, data[i]);
}
}
3. 按键处理
c
运行
// 按键扫描任务(FreeRTOS任务)
void key_scan_task(void *arg) {
uint8_t key_state[5] = {1,1,1,1,1}; // 初始状态(未按下)
uint8_t key_prev[5] = {1,1,1,1,1}; // 上一状态
while (1) {
// 读取5个按键状态(0=按下,1=未按下)
key_state[0] = HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_0); // 电源键
key_state[1] = HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_1); // 菜单键
key_state[2] = HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_2); // 上选择键
key_state[3] = HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_3); // 下选择键
key_state[4] = HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_4); // 确认键
// 检测按键下降沿(按下事件)
for (int i = 0; i < 5; i++) {
if (key_state[i] == 0 && key_prev[i] == 1) {
key_handler(i); // 处理按键事件
}
key_prev[i] = key_state[i];
}
osDelay(20); // 20ms扫描一次,去抖
}
}
// 按键事件处理
static void key_handler(uint8_t key_id) {
switch (key_id) {
case 1: // 菜单键
ui_switch_view(); // 切换界面视图
break;
case 2: // 上选择键
ui_param_increase(); // 参数增加
break;
case 3: // 下选择键
ui_param_decrease(); // 参数减少
break;
case 4: // 确认键
ui_param_confirm(); // 确认参数
break;
// 电源键处理(略)
}
}
系统集成与主程序
c
运行
// 主程序入口
int main(void) {
// 硬件初始化
HAL_Init();
SystemClock_Config(); // 配置400MHz系统时钟
MX_GPIO_Init();
MX_QSPI_Init();
MX_CAN1_Init();
MX_TIM6_Init(); // LVGL时基
MX_FREERTOS_Init(); // 初始化FreeRTOS
// 外设初始化
qspi_lcd_init(); // QSPI液晶屏初始化
can_fd_init(); // CAN FD初始化
// LVGL初始化
lv_init();
lv_port_disp_init(); // 绑定显示驱动
ui_init(); // 初始化界面元素
// 创建任务
osThreadNew(key_scan_task, NULL, &key_scan_task_attributes);
osThreadNew(ui_refresh_task, NULL, &ui_refresh_task_attributes);
// 启动调度器
osKernelStart();
while (1) {
// 主循环(FreeRTOS调度器运行后不会执行到这里)
}
}
// 界面刷新任务(100ms一次)
void ui_refresh_task(void *arg) {
while (1) {
lv_task_handler(); // 处理LVGL任务
osDelay(10); // 10ms一次,保证界面流畅
}
}
测试与调试
关键测试项
QSPI 显示测试:
- 测试内容:全屏纯色填充(红 / 绿 / 蓝)、灰度渐变、字符显示。
- 验收标准:无花屏、无拖影,颜色过渡均匀。
CAN FD 通信测试:
- 测试工具:CANoe 仿真无人机数据帧(0x101、0x102 等 ID)。
- 验收标准:接收成功率 100%,数据解析正确,界面更新延迟 < 100ms。
按键功能测试:
- 测试内容:连续按键 5000 次,检查是否有误触发或失效。
- 验收标准:响应率 100%,无粘连现象。
系统稳定性测试:
- 测试方法:连续运行 24 小时,模拟振动(可选)。
- 验收标准:无死机、无数据丢失,界面正常刷新。
常见问题解决
QSPI 传输数据错误 | 时钟频率过高(>80MHz)或布线不良 | 降低 QSPI 时钟至 60MHz,优化 PCB 布线(缩短线长,阻抗匹配) |
CAN FD 接收丢帧 | 波特率配置错误或 FIFO 溢出 | 重新校准 CAN FD 波特率,在中断中及时处理数据(减少耗时操作) |
界面卡顿 | LVGL 缓存不足或刷新频率过高 | 确保 LV_MEM_SIZE≥768KB,限制界面元素数量(减少重绘区域) |
按键误触发 | 无硬件消抖或扫描频率过低 | 增加 10KΩ 下拉电阻 + 100nF 电容消抖,提高扫描频率至 50Hz |
总结与展望
本方案基于 STM32 与 QSPI 屏构建了无人机专用 MFD,通过 CAN FD 总线实现高速数据交互,物理按键操作简单可靠,重点呈现了剩余电量、电机转速、悬停高度等核心参数。方案优势在于:
未来可扩展方向:
- 增加数据记录功能(通过 SD 卡存储飞行参数)。
- 开发多视图切换(如增加姿态仪表、航线规划视图)。
- 集成无线数传模块(如 4G),支持远程监控。
评论前必须登录!
注册