Qt 嵌入式开发实战
1. 概述
1.1 功能简介
Qt 是跨平台的 C++ GUI 开发框架,广泛应用于嵌入式设备的图形界面开发。在 RK3576 平台上,Qt 5.15.x 支持 OpenGL ES 3.2 GPU 加速渲染、QML 声明式 UI、串口/I2C/SPI 通信、虚拟键盘输入等完整功能。RK3576 搭载 Mali-G52 MC3 GPU,为 Qt Quick/QML 提供硬件加速的 2D/3D 渲染能力,适合工业控制面板、车载仪表、智能家居中控等嵌入式场景。
1.2 应用场景
- 工业 HMI:触摸屏人机交互界面、设备控制面板
- 车载显示:仪表盘、中控娱乐系统
- 智能家居:中控屏、门锁面板
- 医疗设备:监护仪界面、超声显示终端
- 物联网终端:数据可视化看板、信息展示屏
1.3 工作原理
RK3576 Qt 嵌入式开发架构:
Qt 应用层
├── Qt Widgets — 传统 C++ 控件
├── Qt Quick / QML — 声明式 UI(GPU 加速)
├── QSerialPort — 串口通信
├── Qt Virtual Keyboard — 虚拟键盘
└── QSystemTrayIcon — 系统托盘
Qt 平台插件层
├── QLinuxFbIntegration — LinuxFramebuffer
├── QEglFSIntegration — EGLFS(推荐,GPU 加速)
├── QEvdevKeyboardDriver — 键盘输入
└── QEvdevMouseDriver — 触摸/鼠标输入
EGL/OpenGL ES 3.2
├── Mali-G52 MC3 驱动
├── EGL 上下文管理
└── OpenGL ES 3.2 渲染管线
操作系统层(Linux 6.1)
├── DRM/KMS 显示框架
├── /dev/dri/card0 — GPU 设备节点
├── /dev/tty* — 串口设备
└── /dev/i2c-* /dev/spidev* — 总线设备
硬件层(RK3576)
├── Mali-G52 MC3 GPU(3 核)
├── VOP2 显示控制器
├── HDMI 输出
└── UART / I2C / SPI 控制器2. 硬件准备
2.1 硬件清单
| 组件 | 型号/规格 | 数量 | 备注 |
|---|---|---|---|
| 主控板 | C-3576BQ 核心板 + M4-R2 底板 | 1 | |
| HDMI 显示器 | 1080p 或以上 | 1 | 用于 Qt GUI 输出 |
| 调试串口线 | USB 转 TTL(3.3V) | 1 | 波特率 1500000 |
| 网线 | 标准 RJ45 | 1 | SSH 远程部署 |
| USB 键盘(可选) | 标准 USB 键盘 | 1 | Qt 输入测试 |
| USB 鼠标(可选) | 标准 USB 鼠标 | 1 | Qt 指针输入 |
| 电源适配器 | 5V/2A DC(Type-C 或 6pin 供电) | 1 | 空载最小 1000mA |
2.2 硬件连接
- 显示连接:通过 HDMI 线将底板 HDMI 接口连接至显示器
- 调试串口:USB 转 TTL 模块连接底板 UART 调试口(TX/RX/GND)
- 网络:网线连接底板 RJ45 至局域网交换机
- 电源:5V DC 电源接入
2.3 硬件确认
# 通过调试串口或 SSH 登录后,确认 GPU 设备节点
ls -la /dev/dri/
# 预期输出:
# card0
# renderD128
# 确认 Mali GPU 驱动加载
dmesg | grep -i mali
# 预期输出包含:Mali-G52
# 确认显示控制器
cat /sys/class/drm/card*/status
# HDMI 接口应显示 "connected"
# 确认串口设备
ls /dev/ttyS*3. 软件准备
3.1 内核配置
RK3576 Linux 6.1 内核需要启用以下配置以支持 Qt EGLFS 渲染:
# 进入内核源码目录
cd $SDK_DIR/kernel
# 打开内核配置
make menuconfig关键内核配置项:
# DRM 显示框架
CONFIG_DRM=y
CONFIG_DRM_ROCKCHIP=y
CONFIG_ROCKCHIP_VOP2=y
# Mali GPU 驱动
CONFIG_MALI_MIDGARD=y
CONFIG_MALI_PLATFORM_NAME="rk"
CONFIG_MALI_GROWABLE_PAGE_TABLE=y
CONFIG_MALI_BIFROST=y
# EGLFS 所需的 framebuffer 支持
CONFIG_FB=y
CONFIG_FB_VIRTUAL=y
# 输入设备(Evdev)
CONFIG_INPUT_EVDEV=y
# I2C 支持
CONFIG_I2C_CHARDEV=y
# SPI 支持
CONFIG_SPI=y
CONFIG_SPI_SPIDEV=y
# 串口
CONFIG_SERIAL_8250=y# 编译并安装内核
make -j$(nproc)
# 将 Image 和 .dtb 拷贝到目标板对应位置3.2 设备树配置
确保设备树中启用以下节点(寄存器基地址与中断号为 RK 平台通用结构示例,具体以实际发布的 RK3576 SDK 设备树为准):
/* GPU 节点(示例结构) */
gpu: gpu@fde00000 {
compatible = "arm,mali-bifrost", "arm,mali-timer";
reg = <0x0 0xfde00000 0x0 0x40000>;
interrupts = <GIC_SPI 90 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 91 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 92 IRQ_TYPE_LEVEL_HIGH>;
interrupt-names = "job", "mmu", "gpu";
clocks = <&cru ACLK_GPU>, <&cru PCLK_GPU>;
clock-names = "core", "bus";
operating-points-v2 = <&gpu_opp_table>;
power-domains = <&power RK3576_PD_GPU>;
status = "okay";
};
/* VOP2 显示控制器(示例结构) */
vop2: vop2@fe040000 {
compatible = "rockchip,rk3576-vop2";
reg = <0x0 0xfe040000 0x0 0x2000>,
<0x0 0xfe044000 0x0 0x1000>;
interrupts = <GIC_SPI 148 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru ACLK_VOP2>, <&cru HCLK_VOP2>,
<&cru DCLK_VP0>, <&cru DCLK_VP1>;
power-domains = <&power RK3576_PD_VOP>;
status = "okay";
};
/* HDMI 接口(示例结构) */
hdmi: hdmi@fe0a0000 {
compatible = "rockchip,rk3576-dw-hdmi";
reg = <0x0 0xfe0a0000 0x0 0x20000>;
interrupts = <GIC_SPI 169 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru PCLK_HDMI>,
<&cru HCLK_VO>,
<&cru DCLK_VOP1>;
status = "okay";
};以上寄存器基地址、中断号与时钟标识为 RK 平台通用结构示例,具体数值以实际发布的 RK3576 SDK 设备树为准(待板上确认)。
4. 操作步骤
4.1 交叉编译 Qt 5.15.x
4.1.1 准备交叉编译环境
# 设置工具链路径
export SDK_DIR=/path/to/rk3576sdk
export TOOLCHAIN=$SDK_DIR/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu
export PATH=$TOOLCHAIN/bin:$PATH
# 验证编译器
aarch64-linux-gnu-g++ --version
# 输出:aarch64-linux-gnu-g++ (GCC) 10.3.1 20210621
# 创建 Qt 构建目录
mkdir -p $HOME/qt5-build && cd $HOME/qt5-build4.1.2 配置 Qt 源码
# 解压 Qt 源码(以 5.15.13 为例)
tar xf qt-everywhere-src-5.15.13.tar.xz
cd qt-everywhere-src-5.15.13
# 创建 mkspecs 交叉编译配置
mkdir -p qtbase/mkspecs/devices/linux-rk3576-g++
cat > qtbase/mkspecs/devices/linux-rk3576-g++/qmake.conf << 'EOF'
#
# qmake configuration for RK3576 with Mali-G52 MC3 (EGLFS)
#
MAKEFILE_GENERATOR = UNIX
CONFIG += incremental
QMAKE_INCREMENTAL_STYLE = subdirs
QT_HOST_PATH = /usr
QT_TARGET_ARCH = aarch64
include(../common/linux_device_pre.conf)
# 交叉编译工具链
CROSS_COMPILE = aarch64-linux-gnu-
LIBC = g++
QMAKE_CC = $${CROSS_COMPILE}gcc
QMAKE_CXX = $${CROSS_COMPILE}g++
QMAKE_LINK = $${CROSS_COMPILE}g++
QMAKE_LINK_SHLIB = $${CROSS_COMPILE}g++
# sysroot 路径(如有)
# QMAKE_SYSROOT = /path/to/sysroot
# Mali-G52 MC3 EGLFS 配置
QMAKE_INCDIR_EGL = /usr/include
QMAKE_LIBDIR_EGL = /usr/lib
QMAKE_LIBS_EGL = -lEGL
QMAKE_LIBS_OPENGL_ES2 = -lGLESv2 -lEGL -lmali
# DRM/KMS 支持(EGLFS 依赖)
QMAKE_LIBS_DRM = -ldrm
# 输入设备
QMAKE_LIBS_EVDEV = -levdev
# 硬件优化
QMAKE_CFLAGS += -march=armv8-a -mtune=cortex-a72.cortex-a53
QMAKE_CXXFLAGS += -march=armv8-a -mtune=cortex-a72.cortex-a53
include(../common/linux_device_post.conf)
EOF4.1.3 编译 Qt
./configure \
-prefix /opt/qt5-rk3576 \
-opensource -confirm-license \
-xplatform devices/linux-rk3576-g++ \
-device-option CROSS_COMPILE=aarch64-linux-gnu- \
-sysroot $TOOLCHAIN/aarch64-none-linux-gnu/ \
-no-rpath \
-skip qtwebengine \
-nomake examples \
-nomake tests \
-opensource -confirm-license \
-qt-zlib \
-qt-libjpeg \
-qt-libpng \
-qt-freetype \
-qt-harfbuzz \
-qt-pcre \
-qt-xcb \
-eglfs \
-kms \
-gbm \
-linuxfb \
-no-opengl \
-opengl es2 \
-egl -gbm
# 使用 EGLFS + KMS/GBM 后端
# Mali-G52 MC3 通过 GBM 分配缓冲区,DRM/KMS 进行显示# 编译(使用全部 8 核)
make -j$(nproc)
# 安装到本地目录
make install
# 默认安装到 /opt/qt5-rk35764.2 Qt Creator 远程部署
4.2.1 配置交叉编译 Kit
在 Qt Creator 中配置 RK3576 交叉编译 Kit:
- 打开 Tools > Options > Kits
- 点击 Add 新增 Kit
- 配置如下:
- Name:
RK3576 - Compiler:
aarch64-linux-gnu-g++ - Qt Version: 选择 4.1.1 中安装的 Qt 5.15.x
- Debugger:
aarch64-linux-gnu-gdb - Device Type:
Generic Linux Device
- Name:
4.2.2 配置远程设备
- 打开 Tools > Options > Devices
- 点击 Add > Generic Linux Device
- 填写:
- Name:
RK3576 Board - Host name: 目标板 IP(如
192.168.1.100) - Port: 22
- User name:
root
- Name:
- 选择 Authentication type: Password 或 SSH Key
- 点击 Test 验证连接
4.2.3 部署与运行
# 在 Qt Creator 中创建项目后:
# 1. 选择 RK3576 Kit
# 2. 选择远程设备
# 3. 点击 Run(绿色三角按钮)
# Qt Creator 自动通过 SSH 部署可执行文件并运行
# 手动部署方式
scp myapp root@192.168.1.100:/root/
ssh root@192.168.1.100 "export LD_LIBRARY_PATH=/opt/qt5-rk3576/lib:\$LD_LIBRARY_PATH; /root/myapp"4.3 EGLFS 后端配置
RK3576 Mali-G52 MC3 使用 EGLFS 后端实现 GPU 加速渲染。
4.3.1 环境变量配置
# 在目标板上设置 EGLFS 环境变量
export QT_QPA_PLATFORM=eglfs
export QT_QPA_EGLFS_INTEGRATION=eglfs_kms
export QT_QPA_EGLFS_KMS_CONFIG=/etc/eglfs_kms.json
# 可选:指定 DRM 设备
export QT_QPA_EGLFS_KMS_DEVICE=/dev/dri/card0
# 可选:设置屏幕分辨率
export QT_QPA_EGLFS_KMS_CONFIG_WIDTH=1920
export QT_QPA_EGLFS_KMS_CONFIG_HEIGHT=1080
# 运行 Qt 应用
./myapp4.3.2 EGLFS KMS 配置文件
// /etc/eglfs_kms.json
{
"device": "/dev/dri/card0",
"outputs": [
{
"name": "HDMI-A-1",
"mode": "1920x1080",
"refresh": 60
}
],
"hwcursor": true,
"pbuffers": true
}4.3.3 验证 EGLFS 渲染
# 测试 OpenGL ES 3.2 渲染
export QT_QPA_PLATFORM=eglfs
export QT_QPA_EGLFS_INTEGRATION=eglfs_kms
export QT_QPA_EGLFS_KMS_CONFIG=/etc/eglfs_kms.json
# 运行 Qt 自带的几何图形示例
/usr/local/qt5/examples/opengl/cube/cube
# 验证 GPU 信息
cat /sys/class/misc/mali0/device/gpuinfo4.4 串口通信
Qt 使用 QSerialPort 模块进行串口通信。
4.4.1 启用 QSerialPort
# myapp.pro
QT += serialport4.4.2 串口通信代码
// serial_comm.h
#ifndef SERIAL_COMM_H
#define SERIAL_COMM_H
#include <QObject>
#include <QSerialPort>
#include <QSerialPortInfo>
#include <QByteArray>
class SerialComm : public QObject
{
Q_OBJECT
Q_PROPERTY(QString portName READ portName WRITE setPortName NOTIFY portNameChanged)
Q_PROPERTY(int baudRate READ baudRate WRITE setBaudRate NOTIFY baudRateChanged)
Q_PROPERTY(bool isOpen READ isOpen NOTIFY isOpenChanged)
public:
explicit SerialComm(QObject *parent = nullptr);
~SerialComm();
QString portName() const { return m_portName; }
void setPortName(const QString &name);
int baudRate() const { return m_baudRate; }
void setBaudRate(int rate);
bool isOpen() const { return m_serial.isOpen(); }
// 操作接口
Q_INVOKABLE bool openPort();
Q_INVOKABLE void closePort();
Q_INVOKABLE bool writeData(const QByteArray &data);
Q_INVOKABLE QStringList availablePorts();
signals:
void portNameChanged();
void baudRateChanged();
void isOpenChanged();
void dataReceived(const QByteArray &data);
void errorOccurred(const QString &errorString);
private slots:
void onReadyRead();
void onSerialError(QSerialPort::SerialPortError error);
private:
QSerialPort m_serial;
QString m_portName;
int m_baudRate = QSerialPort::Baud115200;
};
#endif // SERIAL_COMM_H// serial_comm.cpp
#include "serial_comm.h"
SerialComm::SerialComm(QObject *parent)
: QObject(parent)
{
connect(&m_serial, &QSerialPort::readyRead, this, &SerialComm::onReadyRead);
connect(&m_serial, &QSerialPort::errorOccurred, this, &SerialComm::onSerialError);
}
SerialComm::~SerialComm()
{
closePort();
}
void SerialComm::setPortName(const QString &name)
{
if (m_portName != name) {
m_portName = name;
m_serial.setPortName(name);
emit portNameChanged();
}
}
void SerialComm::setBaudRate(int rate)
{
if (m_baudRate != rate) {
m_baudRate = rate;
m_serial.setBaudRate(static_cast<QSerialPort::BaudRate>(rate));
emit baudRateChanged();
}
}
bool SerialComm::openPort()
{
if (m_serial.isOpen()) {
m_serial.close();
}
m_serial.setPortName(m_portName);
m_serial.setBaudRate(static_cast<QSerialPort::BaudRate>(m_baudRate));
m_serial.setDataBits(QSerialPort::Data8);
m_serial.setParity(QSerialPort::NoParity);
m_serial.setStopBits(QSerialPort::OneStop);
m_serial.setFlowControl(QSerialPort::NoFlowControl);
if (m_serial.open(QIODevice::ReadWrite)) {
emit isOpenChanged();
return true;
}
return false;
}
void SerialComm::closePort()
{
if (m_serial.isOpen()) {
m_serial.close();
emit isOpenChanged();
}
}
bool SerialComm::writeData(const QByteArray &data)
{
if (!m_serial.isOpen())
return false;
return m_serial.write(data) == data.size();
}
QStringList SerialComm::availablePorts()
{
QStringList ports;
foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts()) {
ports << info.portName();
}
return ports;
}
void SerialComm::onReadyRead()
{
QByteArray data = m_serial.readAll();
emit dataReceived(data);
}
void SerialComm::onSerialError(QSerialPort::SerialPortError error)
{
if (error == QSerialPort::NoError)
return;
emit errorOccurred(m_serial.errorString());
}4.4.3 QML 中使用串口
// SerialPortPage.qml
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import SerialComm 1.0
Page {
title: "串口通信"
SerialComm {
id: serial
portName: "/dev/ttyS2"
baudRate: 115200
onDataReceived: {
receivedText.append(data)
}
onErrorOccurred: {
statusLabel.text = "错误: " + errorString
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
RowLayout {
Label { text: "串口:" }
ComboBox {
id: portCombo
model: serial.availablePorts()
}
Label { text: "波特率:" }
ComboBox {
id: baudCombo
model: [9600, 19200, 38400, 57600, 115200, 230400, 460800]
currentIndex: 4
}
Button {
text: serial.isOpen ? "断开" : "连接"
onClicked: {
if (serial.isOpen) {
serial.closePort()
} else {
serial.portName = portCombo.currentText
serial.baudRate = parseInt(baudCombo.currentText)
serial.openPort()
}
}
}
}
TextArea {
id: sendArea
Layout.fillWidth: true
Layout.preferredHeight: 100
placeholderText: "输入要发送的数据..."
}
Button {
text: "发送"
enabled: serial.isOpen
onClicked: serial.writeData(sendArea.text)
}
ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
TextArea {
id: receivedText
readOnly: true
placeholderText: "接收到的数据..."
}
}
Label {
id: statusLabel
Layout.fillWidth: true
text: serial.isOpen ? "已连接: " + portCombo.currentText : "未连接"
}
}
}4.5 I2C 通信
Qt 未提供原生 I2C 模块,需通过 Linux 文件 I/O 访问。
// i2c_comm.h
#ifndef I2C_COMM_H
#define I2C_COMM_H
#include <QObject>
#include <QString>
#include <QByteArray>
#include <cstdint>
class I2CComm : public QObject
{
Q_OBJECT
Q_PROPERTY(int bus READ bus WRITE setBus NOTIFY busChanged)
Q_PROPERTY(int address READ address WRITE setAddress NOTIFY addressChanged)
public:
explicit I2CComm(QObject *parent = nullptr);
~I2CComm();
int bus() const { return m_bus; }
void setBus(int bus);
int address() const { return m_address; }
void setAddress(int addr);
Q_INVOKABLE bool openBus();
Q_INVOKABLE void closeBus();
Q_INVOKABLE quint8 readByte(quint8 reg);
Q_INVOKABLE QByteArray readBytes(quint8 reg, int length);
Q_INVOKABLE bool writeByte(quint8 reg, quint8 value);
Q_INVOKABLE bool writeBytes(quint8 reg, const QByteArray &data);
signals:
void busChanged();
void addressChanged();
private:
int m_fd = -1;
int m_bus = 1;
int m_address = 0x50;
};
#endif // I2C_COMM_H// i2c_comm.cpp
#include "i2c_comm.h"
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/i2c-dev.h>
#include <linux/i2c.h>
I2CComm::I2CComm(QObject *parent) : QObject(parent) {}
I2CComm::~I2CComm() { closeBus(); }
void I2CComm::setBus(int bus)
{
if (m_bus != bus) { m_bus = bus; emit busChanged(); }
}
void I2CComm::setAddress(int addr)
{
if (m_address != addr) { m_address = addr; emit addressChanged(); }
}
bool I2CComm::openBus()
{
QString devPath = QString("/dev/i2c-%1").arg(m_bus);
m_fd = ::open(devPath.toStdString().c_str(), O_RDWR);
if (m_fd < 0)
return false;
if (ioctl(m_fd, I2C_SLAVE, m_address) < 0) {
close(m_fd);
m_fd = -1;
return false;
}
return true;
}
void I2CComm::closeBus()
{
if (m_fd >= 0) { close(m_fd); m_fd = -1; }
}
quint8 I2CComm::readByte(quint8 reg)
{
if (m_fd < 0) return 0;
quint8 buf[1] = { reg };
if (write(m_fd, buf, 1) != 1) return 0;
if (read(m_fd, buf, 1) != 1) return 0;
return buf[0];
}
QByteArray I2CComm::readBytes(quint8 reg, int length)
{
QByteArray result;
if (m_fd < 0) return result;
quint8 buf[1] = { reg };
if (write(m_fd, buf, 1) != 1) return result;
result.resize(length);
if (read(m_fd, result.data(), length) != length) result.clear();
return result;
}
bool I2CComm::writeByte(quint8 reg, quint8 value)
{
if (m_fd < 0) return false;
quint8 buf[2] = { reg, value };
return write(m_fd, buf, 2) == 2;
}
bool I2CComm::writeBytes(quint8 reg, const QByteArray &data)
{
if (m_fd < 0) return false;
QByteArray buf = QByteArray(1, static_cast<char>(reg)) + data;
return write(m_fd, buf.data(), buf.size()) == buf.size();
}4.6 SPI 通信
Qt 同样未提供原生 SPI 模块,通过 /dev/spidev 文件接口实现。
// spi_comm.h
#ifndef SPI_COMM_H
#define SPI_COMM_H
#include <QObject>
#include <QByteArray>
class SPIComm : public QObject
{
Q_OBJECT
Q_PROPERTY(int bus READ bus WRITE setBus NOTIFY busChanged)
Q_PROPERTY(int chipSelect READ chipSelect WRITE setChipSelect NOTIFY chipSelectChanged)
public:
explicit SPIComm(QObject *parent = nullptr);
~SPIComm();
int bus() const { return m_bus; }
void setBus(int b);
int chipSelect() const { return m_chipSelect; }
void setChipSelect(int cs);
Q_INVOKABLE bool openDevice(int mode = 0, int speed = 1000000, int bitsPerWord = 8);
Q_INVOKABLE void closeDevice();
Q_INVOKABLE QByteArray transfer(const QByteArray &txData);
signals:
void busChanged();
void chipSelectChanged();
private:
int m_fd = -1;
int m_bus = 0;
int m_chipSelect = 0;
};
#endif // SPI_COMM_H// spi_comm.cpp
#include "spi_comm.h"
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/spi/spidev.h>
SPIComm::SPIComm(QObject *parent) : QObject(parent) {}
SPIComm::~SPIComm() { closeDevice(); }
void SPIComm::setBus(int b) { if (m_bus != b) { m_bus = b; emit busChanged(); } }
void SPIComm::setChipSelect(int cs) { if (m_chipSelect != cs) { m_chipSelect = cs; emit chipSelectChanged(); } }
bool SPIComm::openDevice(int mode, int speed, int bitsPerWord)
{
QString devPath = QString("/dev/spidev%1.%2").arg(m_bus).arg(m_chipSelect);
m_fd = ::open(devPath.toStdString().c_str(), O_RDWR);
if (m_fd < 0) return false;
ioctl(m_fd, SPI_IOC_WR_MODE, &mode);
ioctl(m_fd, SPI_IOC_WR_BITS_PER_WORD, &bitsPerWord);
ioctl(m_fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
return true;
}
void SPIComm::closeDevice()
{
if (m_fd >= 0) { close(m_fd); m_fd = -1; }
}
QByteArray SPIComm::transfer(const QByteArray &txData)
{
QByteArray rxData(txData.size(), 0);
struct spi_ioc_transfer xfer = {};
xfer.tx_buf = reinterpret_cast<unsigned long>(txData.constData());
xfer.rx_buf = reinterpret_cast<unsigned long>(rxData.data());
xfer.len = txData.size();
if (ioctl(m_fd, SPI_IOC_MESSAGE(1), &xfer) < 0)
rxData.clear();
return rxData;
}4.7 虚拟键盘
RK3576 触摸屏设备使用 Qt Virtual Keyboard 实现软键盘输入。
4.7.1 启用 Virtual Keyboard
# myapp.pro
QT += virtualkeyboard4.7.2 配置虚拟键盘
# 设置虚拟键盘环境变量
export QT_IM_MODULE=qtvirtualkeyboard
export QT_VIRTUALKEYBOARD_LANGUAGE=zh_CN4.7.3 QML 集成
// MainWindow.qml
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.VirtualKeyboard 2.15
ApplicationWindow {
visible: true
width: 1920
height: 1080
title: "RK3576 Qt Demo"
ColumnLayout {
anchors.fill: parent
TextField {
id: searchField
Layout.fillWidth: true
placeholderText: "点击输入搜索内容"
inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText
}
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
model: ListModel {
ListElement { name: "项目 A"; status: "进行中" }
ListElement { name: "项目 B"; status: "已完成" }
ListElement { name: "项目 C"; status: "待启动" }
}
delegate: ItemDelegate {
width: parent.width
height: 60
contentItem: RowLayout {
Label { text: model.name; font.bold: true }
Label { text: model.status }
}
}
}
// 虚拟键盘
InputPanel {
id: inputPanel
Layout.fillWidth: true
Layout.preferredHeight: 300
visible: Qt.inputMethod && Qt.inputMethod.visible
z: 10
}
}
}4.8 系统托盘
// main.cpp
#include <QApplication>
#include <QSystemTrayIcon>
#include <QMenu>
#include <QAction>
#include <QMessageBox>
#include <QIcon>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
// 系统托盘
QSystemTrayIcon trayIcon(QIcon(":/icons/app.png"), &app);
QMenu trayMenu;
QAction showAction("显示主窗口", &tray);
QAction settingsAction("设置", &tray);
QAction quitAction("退出", &tray);
QObject::connect(&showAction, &QAction::triggered, [&]() {
// 显示主窗口
});
QObject::connect(&quitAction, &QAction::triggered, &app, &QApplication::quit);
trayMenu.addAction(&showAction);
trayMenu.addAction(&settingsAction);
trayMenu.addSeparator();
trayMenu.addAction(&quitAction);
trayIcon.setContextMenu(&trayMenu);
trayIcon.setToolTip("RK3576 Qt Application");
trayIcon.show();
return app.exec();
}4.9 QML 性能优化
4.9.1 启用 QML Profiler
# 运行时启用 QML 性能分析
export QML_TRACE=1
export QML_COMPILER_TRACE=1
# 启动应用
./myapp
# 另一终端查看性能数据
# 使用 Qt Creator 的 QML Profiler 工具连接4.9.2 Scene Graph 调试
# 启用 Scene Graph 渲染调试
export QSG_INFO=1
export QT_LOGGING_RULES="qt.qpa.*=true"
# 查看渲染信息
# 输出应包含:
# - 使用的渲染后端(OpenGL ES 3.2)
# - GPU 信息(Mali-G52 MC3)
# - Scene Graph 线程模式4.9.3 性能优化要点
// 使用 Repeater 替代动态创建
Repeater {
model: 1000
Rectangle {
width: 100
height: 100
color: index % 2 ? "red" : "blue"
}
}
// 避免在 QML 中使用 JavaScript 做密集计算
// 推荐:将数据处理放在 C++ 层
// 使用 Loader 按需加载
Loader {
active: isPageVisible
sourceComponent: heavyComponent
}
// 启用 ShaderEffect 的 OpenVG 后端(如支持)4.9.4 GPU 加速验证
# 在 Mali-G52 MC3 上验证 OpenGL ES 3.2 渲染
# 运行 Qt 提供的 OpenGL 示例
cd /opt/qt5-rk3576/examples/opengl/cube
export QT_QPA_PLATFORM=eglfs
export QT_QPA_EGLFS_INTEGRATION=eglfs_kms
export QT_QPA_EGLFS_KMS_CONFIG=/etc/eglfs_kms.json
./cube
# 使用 glxinfo 查看 OpenGL ES 版本
apt-get install mesa-utils
# 目标板上
glxinfo | grep "OpenGL ES"
# 输出应包含:OpenGL ES 3.2 ...5. 代码详解
5.1 Qt 交叉编译架构
x86_64 主机(开发端)
├── Qt Creator(IDE)
│ ├── qmake / CMake 构建
│ ├── 交叉编译器 (aarch64-linux-gnu-g++)
│ └── 远程部署(SSH + scp)
└── 交叉编译 Qt 5.15.x
├── qmake 配置(mkspecs/devices/linux-rk3576-g++)
├── host tools(uic, moc, rcc — x86 版本)
└── target libs(aarch64 版本)
RK3576 目标板
├── Qt 运行时库(libQt5Core, libQt5Gui, libQt5Widgets...)
├── Qt 平台插件
│ ├── libqeglfs.so — EGLFS 平台插件
│ ├── libqlinuxfb.so — LinuxFramebuffer 插件
│ ├── libqevdevkeyboardplugin.so — 键盘驱动
│ └── libqevdevmouseplugin.so — 鼠标驱动
├── EGL + Mali-G52 MC3 驱动
│ ├── libEGL.so
│ └── libGLESv2.so(含 libmali.so)
└── DRM/KMS 显示框架5.2 EGLFS 渲染管线
QML/Qt Quick 界面
↓
Scene Graph(场景图渲染)
↓
OpenGL ES 3.2 调用
↓
Mali-G52 MC3 GPU 驱动(libmali.so)
↓
GBM 缓冲区分配
↓
DRM/KMS 显示提交
↓
VOP2 显示控制器
↓
HDMI / MIPI DSI 输出5.3 输入事件处理
硬件输入事件
├── 键盘 → /dev/input/eventX → evdev → Qt Key Events
├── 触摸屏 → /dev/input/eventX → evdev → Qt Touch Events
└── 鼠标 → /dev/input/eventX → evdev → Qt Mouse Events
Qt 输入处理
├── QKeyEvent — 键盘事件
├── QTouchEvent — 触摸事件
├── QMouseEvent — 鼠标事件
└── QInputMethodEvent — 输入法事件6. 常见问题
6.1 EGLFS 无法初始化
现象: 运行 Qt 应用报 Could not initialize EGL display 或 EGLFS: eglGetDisplay() failed
解决方案:
# 1. 确认 GPU 设备节点存在
ls -la /dev/dri/
# 2. 确认 Mali 驱动加载
dmesg | grep -i mali
cat /sys/class/misc/mali0/device/gpuinfo 2>/dev/null
# 3. 检查 EGL 库路径
ls /usr/lib/libEGL.so*
ls /usr/lib/libmali.so*
# 4. 手动测试 EGL
# 安装 mesa-utils
apt-get install mesa-utils
glxinfo | grep "OpenGL ES"6.2 屏幕无输出
现象: Qt 应用运行正常但显示器无画面
解决方案:
# 1. 检查 HDMI 连接状态
cat /sys/class/drm/card0-HDMI-A-1/status
# 应显示 "connected"
# 2. 检查显示分辨率
cat /sys/class/drm/card0-HDMI-A-1/modes
# 3. 设置正确的输出分辨率
export QT_QPA_EGLFS_KMS_CONFIG=/etc/eglfs_kms.json
# 确保 JSON 中的分辨率与显示器支持的分辨率匹配
# 4. 尝试 LinuxFramebuffer 模式
export QT_QPA_PLATFORM=linuxfb
export QT_QPA_LINUXFB_FB=/dev/fb0
./myapp6.3 触摸屏无响应
现象: Qt 应用能显示但无法响应触摸操作
解决方案:
# 1. 检查触摸设备
cat /proc/bus/input/devices | grep -A4 -i touch
# 2. 检查输入设备节点
ls -la /dev/input/event*
evtest /dev/input/eventX # 手动测试触摸事件
# 3. 配置触摸设备名
export QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS=/dev/input/event3:rotate=0
# 4. 启用 evdev 触摸驱动
export QT_QPA_EGLFS_INTEGRATION=eglfs_kms_egldevice6.4 虚拟键盘不弹出
现象: 点击 TextField 但虚拟键盘不显示
解决方案:
# 1. 确认已设置输入法模块
export QT_IM_MODULE=qtvirtualkeyboard
# 2. 确认语言设置
export QT_VIRTUALKEYBOARD_LANGUAGE=zh_CN
# 3. 在 QML 中确认 InputPanel 可见
# 确保 InputPanel 的 visible 属性绑定了 Qt.inputMethod.visible6.5 串口权限问题
现象: Permission denied: /dev/ttyS2
解决方案:
# 1. 添加用户到 dialout 组
sudo usermod -a -G dialout root
# 2. 或直接修改设备权限
sudo chmod 666 /dev/ttyS2
# 3. 验证串口设备
ls -la /dev/ttyS*
dmesg | grep ttyS6.6 应用启动报 "could not find or load the Qt platform plugin"
现象: qt.qpa.plugin: Could not load the Qt platform plugin "eglfs"
解决方案:
# 1. 设置插件路径
export QT_PLUGIN_PATH=/opt/qt5-rk3576/plugins
# 2. 或指定平台插件
export QT_QPA_PLATFORM=eglfs
# 3. 调试插件加载
export QT_DEBUG_PLUGINS=1
./myapp
# 输出将显示插件搜索路径和加载失败原因相关文档
- C/C++应用开发入门 — C/C++ 基础开发
- Python应用开发 — Python 应用开发
- GStreamer多媒体开发 — 多媒体处理
- 设备树详解与实践 — 设备树配置
- 显示DRM驱动 — GPU/显示驱动
