早年间在济南钢厂焦炉四大机车自动化项目中,我们已通过 OPC 技术实现远程数据交互,借助 RSLink 服务器软件,基于 OPC DA 规范完成对罗克韦尔 Logix 5500 系列 PLC 的间接控制,这也是 OPC 技术在工业场景的典型早期应用。
回溯OPC的发展历程,1995 年Rockwell Automation、Siemens、Honeywell 等工业巨头联合成立 OPC 基金会,同年完成首个 OPC 数据访问规范(OPC DA)草案,该规范基于微软 COM/DCOM 技术构建,为工业数据传输确立了统一标准。2001 年,规范家族进一步扩展,新增历史数据访问(HDA)、报警与事件(AE)及安全规范,OPC 技术迅速普及,成为工业自动化领域的事实标准,会员企业规模突破百家。不过,以 OPC DA 为核心的 OPC Classic 存在明显局限,因依赖 Windows COM/DCOM 技术,无法适配 Linux 或嵌入式系统,限制了其应用场景的拓展。
为突破这一技术瓶颈,2003 年 11 月,OPC UA 工作组在德国启动开发工作,核心目标是摆脱 COM/DCOM 依赖,采用面向服务架构(SOA)重塑技术体系。同年 8 月,OPC UA 1.0 规范正式发布,标志着 OPC 技术进入跨平台时代。2008 年,OPC UA 被国际电工委员会(IEC)采纳为 IEC 62541 标准,完成国际标准化认证;2010 年,首个嵌入式 OPC UA 设备问世,实现了无操作系统 "裸奔" 运行,彻底打破了传统 OPC 的部署限制。2017 年 11 月,OPC UA 1.04 版本发布,引入发布 - 订阅(PubSub)机制,大幅提升了数据传输效率与实时性,更好地满足了大规模工业数据交互需求。
2022 年,OPC UA 技术进一步深化,新增现场交换(UAFX)功能,专为控制器间(C2C)通信设计,支持离线配置与高实时性传输,完美适配工业机器人、CNC 等高精度控制场景。如今,OPC UA 已成为工业 4.0 与工业物联网(IIoT)的核心通信标准,全球 90% 以上的工业自动化厂商均提供支持,真正实现了跨平台、跨设备、跨行业的无缝数据互联。

睿擎派是睿赛德公司推出的一款工业开发板,以瑞芯微 RK3506/RK3563 为主控芯片,底层搭载 RT-Thread 操作系统,基于专为工业场景打造的睿擎工业平台进行开发,该平台是全栈自主可控的软硬件一体化解决方案,整合了数据采集、通信、控制、工业协议、AI、显示六大核心功能,精准适配工业应用需求。所以常见的工业总线通信协议,比如CAN Open、EtherCAT和OPC UA都是支持的。
另外国内工控领域,相对于AB的PLC,西门子的PLC反而应用更为广泛,我们早期就采用S7-200/300/400系列进行工业自动化系统开发。当下西门子主推S7-200Smart,S7-1200和S7-1500。其中S7-1200 固件V4.4版本以上开始支持OPC Server UA。
由此为了深度评测睿擎派的OPC UA能力,我们选用了S7-1200与之对接,实现远程操控PLC的目的。
一、S7-1200 开发环境搭建、编码和OPC 配置
S7-1200是德国西门子公司的PLC产品,采用TIA博途软件进行开发。从2009年推出以来,基本上每一两年就推出一个新版本,当前最新版本是V21,考虑到安装包大小和常用功能,我选择的是2022年推出的V18版本进行安装。
安装成功以后,在配置OPC Server的时候发现,一旦开启OPC Server功能,部署的时候就会异常,如下网页有相关的描述。
如果是V18版本,安装如下补丁,就可以解决相关问题。
https://pan.baidu.com/s/1NIjENUkc2GurhxkbKQQJDg  提取码:pfy6
为了便于测试,我们用梯形图在PLC中编写如下功能:
(1)开关量输入I0.0和继电器Q0.0联动(为了便于中间可控制,通过上升沿和下降沿信号置位和复位的方式来操控继电器)

(2)开启一个定时器,以5秒为间隔,打开和关闭继电器Q0.1

(3)实现计数器功能,便于OPC客户端连续显示一个不断变化的数字

程序编写完毕后,在PLC变量表里会显示相关的变量,当然我们也可以自行增加PLC变量,便于和OPC客户端交互。

这些工作完成后,我们就可以开始进行OPC UA相关的变量配置了。

右侧窗口中的OPC UA元素可以拖动进入到中间的OPC UA服务器接口窗口,会自动添加一条变量。这个顺序很重要,是后续OPC UA客户端读取数据的一个根据(不过如果中间有删除,OPC中的索引顺序并不连续)。
配置完毕后,连带程序代码就可以一起部署到PLC了。
(注意:S7-1200 V4.4以上才支持OPC Server UA。 我当下S7-1200的版本是V4.2.3,所以不支持OPC Server。需要在西门子官方网站下载最新的固件,然后升级PLC即可。当前最新的版本为V4.6.1)

部署成功后,转至在线状态,然后让PLC进入运行状态(RUN)。
二、OPC UA 客户端软件UaExpert 对接测试
由于OPC UA客户端是通过命名空间索引和变量索引获取数据的,所以我们先用OPC UA客户端工具定位相关变量的索引,顺便也测试一下OPC Server是否可以正常工作。
RTT官方示例中有相关工具的下载和使用说明。
https://www.rt-thread.com/ruiching/document/site/rc3506/qy1k2kok/#运行-opc-ua-示例
OPC UA客户端:https://www.unified-automation.com/downloads/opc-ua-clients.html

我们PLC的OPC UA Server IP是192.168.1.200,端口是 4840,按上图添加。

连接成功后,把右侧的服务器接口_1(这个名字,其实是PLC中定义的)标签可以拖动到中间的窗口,则所有的变量会呈现出来。其中如下红框里面的部分,对后续的数据读取格外重要。

三、睿擎派测试代码开发
我们有睿擎派RK3506的开发板和一个4.3英寸的MIPI-DSV的LCD显示触摸屏,所以采lvgl和opc ua技术栈来实现对S7-1200远程操控的功能。
所参考的官方示例分别为:
(1)03_network_opc_ua
示例有opc ua客户端和服务端功能,我们只参考客户端功能即可。
(2)05_gui_lvgl_ethercat_motor_control_7in_1024_600
虽然是7寸屏的功能示例,但是动态显示电机状态的代码我们可以参考。
(3)05_gui_lvgl_mipi_ruiching_4_3in_480_800。
LVGL图形界面完整的示例,可以参考各种控件的功能实现。

有了以上的代码参考,我们要实现的功能其实也蛮简单,就是显示一个S7-1200的PLC图片,IO状态灯和真实的PLC同步显示出来。然后增加四个Q继电器按钮,可以远程开关PLC的四个继电器。另外就是有一个标签,实时显示PLC里面的计数值。
(1) LVGL 界面实现
睿擎派RK3506 V1.7.2SDK集成的LVGL为V9.1.0版本,为2024年3月20日发布的。当前最新版本为V9.4.0于2025年10月16日发布。主要差异就是最新版本支持3D模型和GPU扩展支持。
关于图片,网上可以搜索S7-1200的正面图片,我实际搜索了一下,正面图很少(和我当前型号契合的),并且分辨率不高和模糊,下载后,只好用PS工具加工了一下。图片的尺寸需要和LCD显示器适配(480*800),所以图片我们设定为450*480。并保存为png格式。
打开LVGL官方图片在线工具:https://lvgl.io/tools/imageconverter
把图片导入,会生成对应的C代码文件。
同样我们还需要显示一些汉字内容,也需要用官方文字在线工具进行C代码文件生成
LVGL官方文字在线生成工具:https://lvgl.io/tools/fontconverter
以上的功能的主要实现代码如下:
//默认开启字体 14、18、22
font_large = &lv_font_montserrat_22;
font_normal = &lv_font_montserrat_14;
lv_obj_set_style_text_font(lv_screen_active(), font_normal, 0);
lv_obj_t *title_label = lv_label_create(lv_screen_active());
lv_obj_set_style_text_color(title_label, lv_color_hex(COLOR_DIALOG_TEXT), LV_PART_MAIN);
lv_obj_set_style_text_font(title_label, &YF32HZ, LV_PART_MAIN);
lv_label_set_text_fmt(title_label,"%s","睿擎派OPC-UA对接S7-1200演示");
lv_obj_set_pos(title_label, 20, 30);
num_label = lv_label_create(lv_screen_active());
lv_obj_set_style_text_color(num_label, lv_color_hex(COLOR_BTN_PRESS), LV_PART_MAIN);
lv_obj_set_style_text_font(num_label, font_large, LV_PART_MAIN);
lv_label_set_text_fmt(num_label,"NUM: %06d",0);
lv_obj_set_pos(num_label, 160, 100);
lv_obj_t *plc_img = lv_img_create(lv_screen_active());
lv_img_set_src(plc_img, &S71200W );
lv_obj_set_pos(plc_img, 15, 160);
四个按钮实现的代码如下:
void lv_create_do_button (void)
{
lv_color_t btn_bg_color = lv_color_hex(COLOR_BTN_BG);
lv_color_t btn_press_color = lv_color_hex(COLOR_BTN_PRESS);
for(uint8_t i = 0; i < BTN_CNT; i++)
{
lv_obj_t *btn = lv_btn_create(lv_screen_active());
lv_obj_set_size(btn, BTN_WIDTH, BTN_HEIGHT);
lv_coord_t curr_btn_x = BTN_START_X + i * (BTN_WIDTH + BTN_GAP);
lv_obj_set_pos(btn, curr_btn_x, BTN_START_Y);
lv_obj_set_style_bg_color(btn, btn_bg_color, LV_PART_MAIN); // 默认背景色
lv_obj_set_style_bg_color(btn, btn_press_color, LV_STATE_PRESSED); // 按下背景色
lv_obj_set_style_radius(btn, 4, LV_PART_MAIN); // 圆角
lv_obj_set_style_pad_all(btn, 0, LV_PART_MAIN); // 无内边距
lv_obj_set_style_border_width(btn, 0, LV_PART_MAIN); // 无边框
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER , LV_PART_MAIN); // 不透明
btn_label[i] = lv_label_create(btn);
lv_label_set_text(btn_label[i], btn_text_arr[i]);
lv_obj_set_style_text_color(btn_label[i], lv_color_hex(COLOR_BTN_TEXT), LV_PART_MAIN);
lv_obj_set_style_text_font(btn_label[i], font_large, LV_PART_MAIN); // 正确字体调用
lv_obj_center(btn_label[i]); // 文字水平+垂直绝对居中
lv_obj_add_event_cb(btn, btn_click_event_cb, LV_EVENT_CLICKED, NULL);
}
}
19个指示灯实现的代码如下:
void lv_draw_state_led (void)
{
// ===================== 全局固定参数定义 =====================
const lv_coord_t rect_w_h = 8; // 矩形尺寸:宽8px、高8px
const lv_coord_t rect_space = 7; // 矩形之间的物理间隔:7像素(核心要求)
// ===================== PLC 状态 =====================
// 起始坐标:x=15+37 y=160+178 | 数量:3个 | 间隔7px | 8*8 | 橙色
lv_coord_t plc_state_x_start = 15 + 37;
lv_coord_t plc_state_y_start = 160 + 178;
for(uint8_t i = 0; i < 3; i++)
{
// 1. 创建矩形对象(LVGL9.1 屏幕对象API:lv_screen_active())
plc_state[i] = lv_obj_create(lv_screen_active());
// 2. 设置矩形尺寸 8*8(固定)
lv_obj_set_size(plc_state[i], rect_w_h, rect_w_h);
// 3. 计算X坐标(核心:保证间距7px)、Y坐标固定
lv_coord_t curr_x = plc_state_x_start + i * (rect_w_h + rect_space);
lv_obj_set_pos(plc_state[i], curr_x, plc_state_y_start);
// 4. 样式配置:纯色填充、直角、无边框(小矩形标准样式)
lv_obj_set_style_bg_color(plc_state[i], lv_color_hex(COLOR_BLACK), LV_PART_MAIN);
lv_obj_set_style_radius(plc_state[i], 0, LV_PART_MAIN); // 圆角0 → 纯矩形
lv_obj_set_style_border_width(plc_state[i], 0, LV_PART_MAIN); // 无边框(无多余线条)
lv_obj_set_style_pad_all(plc_state[i], 0, LV_PART_MAIN); // 无内边距
}
// ===================== PLC DI =====================
// 起始坐标:x=15+298 y=160+177 | 数量:6个 | 间隔7px | 8*8 | 绿色
lv_coord_t plc_di_x_start = 15 + 298;
lv_coord_t plc_di_y_start = 160 + 177;
for(uint8_t i = 0; i < 8; i++)
{
plc_di[i] = lv_obj_create(lv_screen_active());
lv_obj_set_size(plc_di[i], rect_w_h, rect_w_h);
lv_coord_t curr_x = plc_di_x_start + i * (rect_w_h + rect_space);
lv_obj_set_pos(plc_di[i], curr_x, plc_di_y_start);
lv_obj_set_style_bg_color(plc_di[i], lv_color_hex(COLOR_BLACK), LV_PART_MAIN);
lv_obj_set_style_radius(plc_di[i], 0, LV_PART_MAIN);
lv_obj_set_style_border_width(plc_di[i], 0, LV_PART_MAIN);
lv_obj_set_style_pad_all(plc_di[i], 0, LV_PART_MAIN);
}
// ===================== PLC DQ =====================
// 起始坐标:x=15+298 y=160+299 | 数量:4个 | 间隔7px | 8*8 | 绿色
lv_coord_t plc_do_x_start = 15 + 298;
lv_coord_t plc_do_y_start = 160 + 299;
for(uint8_t i = 0; i < 8; i++)
{
plc_do[i] = lv_obj_create(lv_screen_active());
lv_obj_set_size(plc_do[i], rect_w_h, rect_w_h);
lv_coord_t curr_x = plc_do_x_start + i * (rect_w_h + rect_space);
lv_obj_set_pos(plc_do[i], curr_x, plc_do_y_start);
lv_obj_set_style_bg_color(plc_do[i], lv_color_hex(COLOR_BLACK), LV_PART_MAIN);
lv_obj_set_style_radius(plc_do[i], 0, LV_PART_MAIN);
lv_obj_set_style_border_width(plc_do[i], 0, LV_PART_MAIN);
lv_obj_set_style_pad_all(plc_do[i], 0, LV_PART_MAIN);
}
}
初始的时候,LED灯都显示黑色。OPC Server连接成功后,PLC RUN灯位绿色,否则为橙色。开关量输入和输出灯根据实际状态进行变化。
(2) OPC UA 客户端功能实现
睿擎派RK3506 V1.7.2 SDK集成的open62541为V1.2.2版本,2019年9月18日发布的,属于早期经典版,基础功能完善,封装相对简单。最新工业级稳定版本为V1.5.1,2024年11月12日发布。核心区别如下,V1.2.2没有批量读取变量的函数,无PubSub(发布-订阅)功能,V1.5.1原生适配RT-Thread,内存优化,但是需要C99编译支持。
OPC UA客户端有三个关键功能函数,我们专门创建open62541_client.c文件来实现,由于我们本代码示例是基于05_gui_lvgl_mipi_ruiching_4_3in_480_800创建的,所以需要双击"RunChing Setings"项,开启OPC UA功能,如下图所示:

注:开启OPC UA功能后,\opc_ua_lvgl_s7_1200\rt-thread\components\net_apps\open62541的目录并没有加入到Includes目录,记得要添加上,否则对应的头文件编译时会提示找不到。
1 opc ua server 连接
int open62541_connect (char *ip,int port)
{
char ip_data[128] = {0};
rt_sprintf(ip_data,"opc.tcp://%s:%d", ip, port);
if(client==NULL) {
client = UA_Client_new();
UA_ClientConfig_setDefault(UA_Client_getConfig(client));
}
UA_StatusCode retval = UA_Client_connect(client, ip_data);
if(retval != UA_STATUSCODE_GOOD)
{
UA_Client_delete(client);
client = NULL;
return (int)retval;
}
return (int)retval;
}
提供ip地址和端口即可,目前没有开启安全验证,所以相对简单。
2 读取变量
目前我们只读取了两种类型的变量,就是布尔型和整型,代码如下:
int open62541_get_value (int ns,int i, int *value)
{
if (client==NULL) return -1;
UA_Variant read_value;
UA_Variant_init(&read_value);
UA_StatusCode retval = UA_Client_readValueAttribute(client,UA_NODEID_NUMERIC(ns,i), &read_value);
if(retval == UA_STATUSCODE_GOOD)
{
UA_UInt32 type_num = -1;
// 提取类型编号
if(read_value.type != NULL) {
type_num = read_value.type->typeIndex;
}
//Boolean
if(type_num==0)
{
UA_Boolean *p = (UA_Boolean *)read_value.data;
*value = (int)*p;
}
//UInt32
else if(type_num==4)
{
UA_UInt32 *p = (UA_UInt32 *)read_value.data;
*value = (int)*p;
}
else {
rt_kprintf("[err]type_num=%d\n", type_num);
}
//rt_kprintf(" - 类型: %s (编号: %u) arrayLength = %d\n", ua_typeid_to_name(type_num), type_num,read_value.arrayLength);
}
else
{
rt_kprintf("get [%d.%d] failed, code: %d\n", ns,i, retval);
}
UA_Variant_clear(&read_value);
return retval;
}
3 写变量
我们目前是操作继电器Q变量,该变量是布尔型,所以代码仅支持该类型的写操作。
int open62541_set_value (int ns,int i,UA_Boolean value)
{
if (client==NULL) return -1;
UA_Variant write_value;
UA_Variant_setScalar(&write_value, &value, &UA_TYPES[UA_TYPES_BOOLEAN]);
UA_StatusCode retval = UA_Client_writeValueAttribute(client, UA_NODEID_NUMERIC(ns,i), &write_value);
if(retval != UA_STATUSCODE_GOOD)
{
rt_kprintf("set [%d.%d] failed retval = %d\n",ns,i,retval);
}
return (int)retval;
}
(3) 变量远程实时读写
opc读写函数中的参数int ns,int i 就对应了OPC客户端工具画红框的部分,比如"NS4|Numeric|8", 相应的ns=4,i=8;
LVGL不支持多线程操作,所以需要创建一个LVGL 定时器来定时刷新数据,另外由于定时器函数属于UI线程回调,如果里面做长时间操作,会堵塞UI线程,界面操作会很卡。所以需要新创建一个线程来实现OPC UA变量的实时读取。
/OPC-UA链接线程
static void opc_thread_entry (void *parameter)
{
int state[10];
int err_count = 0;
int idx[10]={4,5,6,7,8,10,11,12,13,14};
while(1)
{
if(plc_connect_state!=0)
{
if(open62541_connect("192.168.1.200",4840)==0)
{
plc_connect_state = 0;
}
}
else
{
for (int i=0;i<10;i++){
if(open62541_get_value(4,idx[i],&state[i])==0){
io_state[i] = state[i];
err_count = 0;
}
else {
rt_kprintf("open62541_get_value %d err!!\n",i);
if(err_count++>10)
{
plc_connect_state = -1;
}
}
}
rt_kprintf("I %d %d %d %d %d %d\n",io_state[4],io_state[5],io_state[6],io_state[7],io_state[8],io_state[9]);
rt_kprintf("Q %d %d %d %d\n\n",io_state[0],io_state[1],io_state[2],io_state[3]);
int temp_num = 0;
if(open62541_get_value(4,15,&temp_num)==0){
num = temp_num;
}
}
rt_thread_mdelay(100);
}
}
创建一个定时器,300毫秒执行一次,来进行界面刷新。
lv_timer_create(data_timer_cb, 300, NULL);
static void data_timer_cb(lv_timer_t *timer)
{
//PLC状态
if(plc_connect_state!=old_connect_state)
{
lv_obj_set_style_bg_color(plc_state[0], lv_color_hex(plc_connect_state==0?COLOR_GREEN:COLOR_ORANGE), LV_PART_MAIN);
old_connect_state = plc_connect_state;
}
//计数
lv_label_set_text_fmt(num_label,"NUM: %06d",num);
//IO状态
for (int i=0;i<10;i++){
if(i < 4)
{
lv_obj_set_style_bg_color(plc_do[i],lv_color_hex( io_state[i]!=1?COLOR_BLACK:COLOR_GREEN), LV_PART_MAIN);
lv_obj_set_style_text_color(btn_label[i], lv_color_hex(io_state[i]!=1?COLOR_BTN_TEXT:COLOR_GREEN), LV_PART_MAIN);
}
else
{
lv_obj_set_style_bg_color(plc_di[i-4],lv_color_hex( io_state[i]!=1?COLOR_BLACK:COLOR_GREEN), LV_PART_MAIN);
}
}
}
由于写变量操作,执行实现不长,所以直接在按钮回调事件里实现了。
static void btn_click_event_cb(lv_event_t *e)
{
lv_event_code_t code = lv_event_get_code(e);
lv_obj_t *btn = lv_event_get_target(e);
if (code == LV_EVENT_CLICKED)
{
const char *btn_name = lv_label_get_text(lv_obj_get_child(btn, 0));
int index = btn_name[3]-'0';
rt_kprintf("DO: %s %d\n", btn_name,index);
if(open62541_set_value(4,4+index,1-io_state[index])==0)
{
io_state[index] = 1-io_state[index];
}
}
}
四、操作演示视频
(1 )部署运行
部署成功后,程序会自动运行,连接成功后,会不断读取PLC的IO状态及计数器的值。

(2 )操作演示
