方案核心概述
你是否曾面临这样的困境:一个系统既要处理复杂、响应迅速的图形界面,又要确保底层电机控制的硬实时性能?传统的单系统架构往往难以两全其美。高性能实时控制与丰富人机交互的需求矛盾,是嵌入式系统开发中一个常见的挑战。
针对此问题,我们采用了一种混合部署的架构方案。该方案的核心思想是“各司其职”,将系统清晰地划分为两个层面:
- 实时层:部署在基于瑞芯微RK3576芯片的睿擎派设备上,运行 RT-Thread 实时操作系统。这一层专攻高频率、高确定性的核心任务,例如执行核心控制算法、驱动 EtherCAT 等工业总线通信,以及实施毫秒级的故障保护等硬实时操作。
- 交互层:基于强大的 Qt Framework 进行开发,运行在标准的 Linux 操作系统之上。这一层负责所有图形化用户界面(GUI)相关任务,包括参数配置、状态监控、实时曲线显示、数据记录、报警管理以及网络通信等软实时或非实时任务。
那么,如何实现这种高效的数据交换呢?答案就是高速的核间通信机制。通过该机制,两个层面可以无缝地进行数据交换和指令传递,最终形成一个兼顾极致实时性能与高效开发流程的完整解决方案。
睿擎派混合部署方案详解
混合部署,或称非对称多处理(AMP),指的是在同一颗芯片上同时运行两个独立的操作系统。在我们的实践中,睿擎派RC3506开发板(基于RK3576芯片)实现了:
- Linux系统:运行在芯片的应用核心(Cortex-A系列),负责承载Qt图形界面和处理所有非实时任务,提供了丰富的生态和开发便利性。
- RT-Thread系统:运行在芯片的实时核心(Cortex-R/M系列),专责电机控制、EtherCAT主站通信等对时间确定性要求极高的实时任务。
- 核间通信通道:基于
rpmsg(Remote Processor Messaging)机制实现两个异构系统间的高速、低延迟数据交换。这是整个方案能够成功运转的技术基石。
启动设备后,我们可以通过不同的串口分别观察到两个系统的运行日志,直观感受AMP架构的运行状态。
睿擎派RC3506上运行Linux系统(通过串口1查看的启动日志):

睿擎派RC3506上运行RT-Thread系统(通过串口3查看的启动日志):

Linux侧:Qt应用与核间通信实现
1. UI界面设计与效果
用户交互的核心是一个由Qt开发的电机控制界面。该界面实现了电机启停、方向控制、速度设定、实时位置与期望位置曲线对比等关键功能,所有控件状态都与底层实时系统同步。

2. Qt界面绘制及业务逻辑核心代码
界面的构建涉及到一系列Qt控件的创建、布局和信号槽连接。主要工作包括:
- 绘制电机启停控制按钮
- 设计电机正转/反转方向切换控件
- 实现电机期望位置和当前位置的数值与图表显示
- 创建用于实时显示位置跟踪曲线的图表组件
以下是一些关键的界面初始化代码片段:
// 在 motorcontrol.cpp 中创建控制按钮和标签
QLabel *motor_ctrl_widget = new QLabel();
M_adc1_gauge = new GaugeCtrl(motor_ctrl_widget);
M_adc1_gauge->setGeometry(QRect(0, 0, 180, 180));
this->plot_widget = new QCustomPlot(motor_ctrl_widget);
this->plot_widget->setGeometry(QRect(180, 0, 760, 400));
light_btn = new LightButton(50, 50, 80, 80);
light_btn->setGeometry(QRect(900, 100, 80, 80));
QObject::connect(light_btn, SIGNAL(clicked()), this,SLOT(on_light_btn_clicked()));
// ... 其他控件初始化,如滑块、标签等
slider = new QSlider(Motor_ctrl_widget);
slider->setGeometry(QRect(10, 320, 200, 50));
slider->setValue(1000);
slider->setMinimum(0);
slider->setMaximum(10000);
QObject::connect(slider, SIGNAL(valueChanged(int)), this,SLOT(ctrl_value_changed(int)));
代码节选1:Qt控件初始化
// 在 motorcontrol.cpp 中设置图表控件的属性
void MotorCtrlWin::setupPlotWidget()
{
plot_widget->legend()->setVisible(true);
QFont legend_font = QFont("Serif");
plot_widget->legend()->setFont(legend_font);
plot_widget->legend()->setBrush(QColor(255,255,255,80));
plot_widget->xAxis->setMin(0);
plot_widget->xAxis->setMax(100);
plot_widget->yAxis->setMin(0);
plot_widget->yAxis->setMax(100);
// 设置坐标轴画笔、网格等样式
plot_widget->xAxis->setBasePen(QPen(Qt::white, 1));
plot_widget->yAxis->setBasePen(QPen(Qt::white, 1));
plot_widget->xAxis->setGridPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine));
plot_widget->setAxisTitle(plot_widget->xAxis(), "Time (s)");
plot_widget->setAxisTitle(plot_widget->yAxis(), "Position");
plot_widget->setTitle("Motor Control Data");
plot_widget->replot();
}
代码节选2:Qt图表控件配置
3. Linux侧核间通讯实现
这是连接Qt应用与RT-Thread实时层的桥梁。我们通过Linux内核提供的rpmsg字符设备接口进行通信。
核心步骤包括:
- rpmsg端点管理:检查并创建通信端点。
- 数据发送:将UI产生的控制命令(如启停、目标位置)打包成结构体,通过
write系统调用发送给RT-Thread侧。
- 数据接收:创建一个独立线程,持续从rpmsg设备
read,接收RT-Thread侧反馈的电机实时状态(如当前位置、转速),并更新到UI。
// 定义通信双方的地址
#define RPMSG_MASTER_ADDR (0x1001) // Linux侧作为主端地址
#define RPMSG_REMOTE_ADDR (0x3001) // RT-Thread侧作为远端地址
int ret = 0;
char eptdev_name[32];
char dev_name[32];
int rpmsg_num = 32;
// 1. 检查并创建rpmsg端点设备
if (rpmsg_char_check_eptdev("/dev/rpmsg", &rpmsg_num, RPMSG_MASTER_ADDR, RPMSG_REMOTE_ADDR)) {
if (rpmsg_num < 0) {
fprintf(stderr, "No available rpmsg device number [n=%d, %s]\n", rpmsg_num, __func__);
return -1;
}
sprintf(eptdev_name, "/dev/rpmsg-%d", rpmsg_num);
ret = rpmsg_char_create_eptdev(eptdev_name, RPMSG_MASTER_ADDR, RPMSG_REMOTE_ADDR, &created_ept_num, rpmsg_num);
if (ret) {
fprintf(stderr, "Could not create end-point %s [n=%d, %s]\n", eptdev_name, rpmsg_num, __func__);
return ret;
}
} else {
sprintf(eptdev_name, "/dev/rpmsg-%d", rpmsg_num);
}
// 2. 打开设备节点
sprintf(dev_name, "/dev/rpmsg%d", rpmsg_num);
ctrl_efd = rpmsg_char_open_eptdev(eptdev_name, dev_name, 0);
if (ctrl_efd < 0) {
fprintf(stderr, "Could not open end-point %s [n=%d, %s]\n", dev_name, rpmsg_num, __func__);
return ctrl_efd;
}
// 3. 初始化控制数据结构
ctrl_data.slave = 0;
ctrl_data.run = 0;
ctrl_data.dir = 0;
ctrl_data.mode = 1;
ctrl_data.ctrl_value = 300;
// ... 初始化其他成员
代码节选3:Linux侧rpmsg设备初始化
当用户在Qt界面上进行操作(如点击启动按钮、拖动速度滑块)时,应用程序会调用类似下面的代码,将控制数据通过rpmsg发送出去:
if(ctrl_efd >= 0) {
send_msg(ctrl_efd, (char *)&ctrl_data, sizeof(ctrl_data));
}
// send_msg 内部实质是调用 write(ctrl_efd, ...)
代码节选4:Linux侧发送控制数据
RT-Thread侧:核间通信与实时控制
RT-Thread侧作为实时控制的核心,需要高效、可靠地处理来自Linux的命令,并实时反馈状态。
实现要点:
- 通信初始化:在系统启动时,通过
rt_device_find 查找并打开 rpmsg 虚拟设备。
- 双线程架构:创建独立的读线程和写线程,分别处理命令接收和状态发送,避免阻塞实时控制回路。
- 同步机制:使用信号量(
dynamic_sem)来同步控制数据的发送时机,确保只有在数据更新时才进行传输。
- 实时反馈:将电机驱动器读取到的实际位置、状态等信息实时更新到共享数据结构中,并通过写线程发送给Linux侧用于界面显示。
// 定义通信地址结构(与Linux侧对应)
static struct rt_rpmsg_ep_addr rpmsg_remote_echo = {"rpmsg_chrdev", 0x3001U, 0x1001U};
// 1. 查找并初始化rpmsg设备
rpmsg = rt_device_find("rpmsg");
if (rpmsg == RT_NULL) {
rt_kprintf("Unable to find rpmsg device.\n");
return RT_ERROR;
}
rt_device_open(rpmsg, RT_DEVICE_OFLAG_RDWR);
rt_device_control(rpmsg, RT_DEVICE_CTRL_CONFIG, &rpmsg_remote_echo);
// 2. 创建用于同步的信号量
dynamic_sem = rt_sem_create("dsem", 0, RT_IPC_FLAG_PRIO);
if (dynamic_sem == RT_NULL) {
return -1;
}
// 3. 创建并启动读线程
thread = rt_thread_create("rpmsg_ethercat_read", rpmsg_ethercat_read, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE);
if (thread != RT_NULL) rt_thread_startup(thread);
// 4. 创建并启动写线程
thread = rt_thread_create("rpmsg_ethercat_write", rpmsg_ethercat_write, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE);
if (thread != RT_NULL) rt_thread_startup(thread);
rt_kprintf("rpmsg ethercat init end.\n");
代码节选5:RT-Thread侧rpmsg设备与线程初始化
读线程负责接收来自Linux的控制命令:
static void rpmsg_ethercat_read(void *parameter) {
rt_uint32_t len = 0;
rt_uint8_t buff[BUFF_SIZE];
while (1) {
len = rt_device_read(rpmsg, rpmsg_remote_echo.src, buff, BUFF_SIZE - 1);
if (len > 0) {
motorctrl_data_t *data = (motorctrl_data_t*)buff;
// 解析并应用控制命令,例如更新目标位置、启停状态
ctrl_data.ctrl_value = data->ctrl_value;
ctrl_data.run = data->run;
ctrl_data.dir = data->dir;
// 触发信号量,通知写线程发送更新后的状态
rt_sem_release(dynamic_sem);
}
rt_thread_mdelay(1);
}
}
代码节选6:RT-Thread侧读线程(接收命令)
写线程负责将电机实时状态发送回Linux:
static void rpmsg_ethercat_write(void *parameter) {
while (1) {
// 等待数据更新的信号
if (rt_sem_take(dynamic_sem, RT_WAITING_FOREVER) == RT_EOK) {
// 从电机驱动器或传感器更新 ctrl_data 中的状态值(如 ctrl_data.cur_pos)
// ...
// 将状态数据写入rpmsg通道,发送给Linux
rt_device_write(rpmsg, rpmsg_remote_echo.dst, &ctrl_data, sizeof(motorctrl_data_t));
}
}
}
代码节选7:RT-Thread侧写线程(发送状态)
核间通讯技术总结
rpmsg 是Linux与RT-Thread这类实时操作系统间进行多核异构通信的标准化机制,也是本AMP方案得以实现的关键。
1. 通信架构清晰
- 主从定义:Linux端作为MASTER(地址0x1001),RT-Thread端作为REMOTE(地址0x3001)。
- 数据载体:两端约定使用统一的
motorctrl_data_t 结构体,确保了数据解析的一致性。
- 接口抽象:底层基于共享内存和处理器间中断,但对上层应用则提供了简单的字符设备
write/read接口,易于使用。
2. 核心优势显著
- 低延迟:内核态的通信机制避免了系统调用和上下文切换的部分开销,延迟极低,能满足微秒到毫秒级的实时控制需求。
- 高可靠性:通过信号量等同步原语确保数据在生产和消费过程中的完整性,避免数据覆盖或丢失。
- 双向全双工:天然支持命令下行与状态上行的同步传输,形成闭环控制。
- 易于集成:提供了清晰的API(创建端点、销毁端点、读写数据),方便在应用层快速集成。
3. 数据流闭环
整个系统形成了两个高效的数据流闭环:
- 控制命令流:
Qt界面 → Linux用户进程 → Linux内核rpmsg → RT-Thread内核rpmsg → RT-Thread电机控制任务。
- 状态反馈流:
电机驱动器/编码器 → RT-Thread数据采集 → RT-Thread内核rpmsg → Linux内核rpmsg → Linux用户进程 → Qt界面刷新。
这种双向、实时的数据流动,确保了操作指令能即刻下达,系统状态也能实时可视,为复杂的嵌入式控制系统提供了坚实的通信基础。这种将高性能计算与网络/系统通信、实时控制与计算机基础相结合的方法,在工业自动化、机器人等领域的应用前景非常广阔。如果你对 C/C++ 在嵌入式实时系统中的应用有更多疑问,或想探讨类似架构,欢迎在云栈社区交流分享你的实践经验。