基于 STM32 与LVGL, 7寸QSPI 屏的无人机 MFD 仪表方案实践

目录

  1. 引言
  2. 项目概述
  3. 硬件选型与设计
  4. 开发环境搭建
  5. 界面设计与实现
  6. 核心功能开发
  7. 系统集成与主程序
  8. 测试与调试
  9. 总结与展望

引言

无人机地面多功能显示器(MFD)是飞行控制的核心交互设备,需实时呈现关键飞行参数并支持快速操作。本方案采用 STM32 微处理器直接通过 QSPI 接口驱动 7 寸液晶屏,集成 CAN FD 总线实现与无人机飞控系统的高速数据交互,通过物理按键完成操作,专注于无人机专用参数显示(剩余电量、电机转速、悬停高度等),兼顾可靠性与实时性。

项目概述

核心目标

  1. 硬件架构:STM32 通过 QSPI 接口直接驱动 7 寸 800*480 液晶屏,省去 RGB 转换环节,简化电路。
  2. 数据交互:通过 CAN FD 总线接收无人机飞控数据(传输速率最高 8Mbps),满足高速数据需求。
  3. 显示功能:实现无人机剩余电量、4 路电机转速、悬停高度、飞行模式、GPS 状态等核心参数的可视化显示。
  4. 操作方式:通过 3-5 个物理功能按键完成界面切换、参数设置、紧急操作等功能。
  5. 性能指标:数据刷新率≥10Hz,界面响应时间≤100ms,连续运行无故障时间≥1000 小时。

硬件选型与设计

核心控制器选型

选用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 供电兼容

关键外设模块

  1. CAN FD 收发器

    • 型号:TJA1044(支持 CAN FD,最高 8Mbps)
    • 功能:将 STM32 的 CAN FD 信号转换为差分信号,与无人机飞控 CAN 总线连接。
  2. 功能按键

    • 数量:5 个(电源键、菜单键、上 / 下选择键、确认键)
    • 类型:轻触按键(带硬件消抖),下拉输入模式。
  3. 电源模块

    • 输入:DC 12V(无人机供电系统常见电压)
    • 输出:3.3V/2A(给 STM32、液晶屏供电),5V/1A(备用)。

硬件连接设计

1. STM32 与 QSPI 液晶屏连接
STM32 引脚功能液晶屏引脚说明
PB2(QSPI_CLK)时钟线CLKQSPI 时钟信号(最高 80MHz)
PD11(QSPI_NCS)片选CS低电平有效,选中液晶屏
PF8(QSPI_D0)数据输入D0主机接收 / 从机发送数据
PF9(QSPI_D1)数据输出D1主机发送 / 从机接收数据
PF10(QSPI_D2)数据输入 2D2可选,4 线模式增强传输(本方案启用)
PG12(QSPI_D3)数据输出 2D3可选,4 线模式增强传输(本方案启用)
PA0复位RST低电平复位液晶屏(默认上拉)
PA1背光控制BLPWM 输出调节亮度(高电平使能)
3.3V电源VCC液晶屏供电
GND接地GND共地连接
2. STM32 与 CAN FD 总线连接
STM32 引脚功能TJA1044 引脚说明
PD0(CAN1_RX)接收数据RXDCAN FD 接收信号
PD1(CAN1_TX)发送数据TXDCAN FD 发送信号
3.3V电源VCC收发器供电
GND接地GND共地连接
-总线接口CAN_H/CAN_L连接至无人机飞控 CAN 总线
3. 功能按键连接
按键功能STM32 引脚电路设计
电源键PC0下拉输入,按下接 3.3V
菜单键PC1下拉输入,按下接 3.3V
上选择键PC2下拉输入,按下接 3.3V
下选择键PC3下拉输入,按下接 3.3V
确认键PC4下拉输入,按下接 3.3V

开发环境搭建

核心工具

工具名称版本用途
STM32CubeIDE1.14.0代码开发、编译、调试一体化环境
STM32CubeMX6.10.0外设配置(QSPI、CAN FD、GPIO 等)
LVGLv8.3嵌入式图形库,适配 800*480 分辨率
CANoe11.0CAN FD 总线数据仿真与测试
ST-Link V3-程序下载与硬件调试

环境配置步骤

1. STM32CubeMX 配置
  1. 选择芯片 “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 中断)。
  2. 生成工程(STM32CubeIDE 格式),启用 FreeRTOS(用于任务调度)。

2. LVGL 移植配置
  1. 下载 LVGL v8.3 源码,复制至工程 “Middlewares/LVGL” 目录。
  2. 配置lv_conf.h关键参数:

    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  // 启用进度条组件
    
  3. 编写 QSPI 显示驱动(lv_port_disp.c):
    • 实现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个电机仪表(2x2布局)
    for (int i = 0; i < 4; i++) {
        lv_obj_t *gauge = lv_gauge_create(parent);
        lv_obj_set_size(gauge, 180, 180);
        // 设置位置(2x2网格)
        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一次,保证界面流畅
    }
}

测试与调试

关键测试项

  1. QSPI 显示测试

    • 测试内容:全屏纯色填充(红 / 绿 / 蓝)、灰度渐变、字符显示。
    • 验收标准:无花屏、无拖影,颜色过渡均匀。
  2. CAN FD 通信测试

    • 测试工具:CANoe 仿真无人机数据帧(0x101、0x102 等 ID)。
    • 验收标准:接收成功率 100%,数据解析正确,界面更新延迟 < 100ms。
  3. 按键功能测试

    • 测试内容:连续按键 5000 次,检查是否有误触发或失效。
    • 验收标准:响应率 100%,无粘连现象。
  4. 系统稳定性测试

    • 测试方法:连续运行 24 小时,模拟振动(可选)。
    • 验收标准:无死机、无数据丢失,界面正常刷新。

常见问题解决

问题原因解决方案
QSPI 传输数据错误时钟频率过高(>80MHz)或布线不良降低 QSPI 时钟至 60MHz,优化 PCB 布线(缩短线长,阻抗匹配)
CAN FD 接收丢帧波特率配置错误或 FIFO 溢出重新校准 CAN FD 波特率,在中断中及时处理数据(减少耗时操作)
界面卡顿LVGL 缓存不足或刷新频率过高确保 LV_MEM_SIZE≥768KB,限制界面元素数量(减少重绘区域)
按键误触发无硬件消抖或扫描频率过低增加 10KΩ 下拉电阻 + 100nF 电容消抖,提高扫描频率至 50Hz

总结与展望

本方案基于 STM32 与 QSPI 屏构建了无人机专用 MFD,通过 CAN FD 总线实现高速数据交互,物理按键操作简单可靠,重点呈现了剩余电量、电机转速、悬停高度等核心参数。方案优势在于:

  1. 硬件精简:直接 QSPI 驱动液晶屏,省去 RGB 转换模块,降低成本与故障率。
  2. 实时性强:CAN FD 总线支持 8Mbps 传输速率,配合 10Hz 界面刷新率,满足无人机数据实时性需求。
  3. 专用性突出:界面布局针对无人机参数优化,关键信息一目了然。

未来可扩展方向:

  • 增加数据记录功能(通过 SD 卡存储飞行参数)。
  • 开发多视图切换(如增加姿态仪表、航线规划视图)。
  • 集成无线数传模块(如 4G),支持远程监控。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值