目录

Qt程序基于共享内存读写CodeSys的变量

Qt程序基于共享内存读写CodeSys的变量

1.背景

在文章 中,我们实现了通过字符串读写结构体中的变量。那么接下来我们开始与CodeSys来进行交互。

由于我们是基于共享内存来通讯的,那么我们需要对共享的内存定义一个数据结构,也就是一个结构体。

假如我们和PLC的通讯只是简单的一个结构体,结构体中都是一些POD(Plain Old Data),那可以直接和PLC程序编写人员协商沟通好,让他把结构的定义代码发给你,你再根据ST代码写出结构体的C++代码。

但是,在实际的项目中,要到使用到的结构体往往是多种类型的结构体互相嵌套的结果。不仅结构体多、数据多,而且还存在数组、嵌套的方式,单纯靠手工来拷贝ST代码-》转C++代码必定是繁琐且容易出错的。因此,必须得搞一套稳定可靠的导出导入机制。

这里我选择通过利用CodeSys的机制+python脚本来实现

2.结构体从CodeSys导出后导入到C++

要想将Application的结构体数据直接导出,貌似是不行的,但是可以先把结构体数据复制到一个Library工程,然后导出m4文件,最后利用python脚本翻译(处理)成我们需要的代码。

2.1.将结构体从CodeSys中导出

我这里有一个Application工程,里面定义了若干结构体

https://i-blog.csdnimg.cn/direct/75a6e442751b474f8fd7fb2cbe4bd605.png

假如想将其导出,那么可以新建一个Library工程,然后将结构体复制过去(直接在左侧的树状列表中选择、复制,而不是直接复制代码)。

https://i-blog.csdnimg.cn/direct/b3847c4b535842d89516f75d59a20e06.png

然后选择 编译–》生成运行时系统文件

https://i-blog.csdnimg.cn/direct/83163eec897a4ee1bbfc30a4fd9204b5.png

然后勾选M4接口文件

https://i-blog.csdnimg.cn/direct/1d3e1b78e1164a2c99b6db940a21ed71.png

然后点击确定、生成M4文件。

如此,便完成了结构体的导出。

2.2.将结构体从m4文件提取翻译成c++格式

其实打开M4文件看一下,可以发现,导出数据已经是c语言格式的结构体了,基本都可以拿来直接用了,但是由于后面要和RTTR结合使用,必须还得清洗处理一下。

https://i-blog.csdnimg.cn/direct/3fc90a180ed64c5e9b7b563c7e930ea9.png M4文件的清洗处理我们需要用到clang(LLVM)库。

我们是在python中使用clang,因此我们需要在python中安装此工具包,我安装的是20.1.0:

https://i-blog.csdnimg.cn/direct/f3ec2e9b72ee4fb491d939624389f15f.png 但是在python中安装了还不行,还得去官网将依赖的库及程序文件下载下来

https://i-blog.csdnimg.cn/direct/909a14bab7964843b057f61985e316eb.png 下载之后,解压到某个路径下即可,不用安装

https://i-blog.csdnimg.cn/direct/c0ccb4f05bdd4a74afd0045f03daa88b.png

然后就可以使用脚本了,这是我的脚本

在脚本中指定好M4文件所在路径、中间文件保存路径、最终文件保存路径,运行即可

import sys
import clang.cindex
from clang.cindex import CursorKind, TypeKind, Config
import os

# 前面提到的clang压缩包的解压的路径,根据自己的路径指定
Config.set_library_path("D:/Qt/clang+llvm-20.1.0-x86_64-pc-windows-msvc/bin")

# M4文件位置
m4FilePath = r'C:/Users/Administrator/Desktop/stateTest/StructOutputItf.m4'
# 中间文件位置
middleFilePath = "./output/tmpFile.h"
# 处理后的文件位置
outputFilePath = "./output/memorydefine.h"


def convert_c_struct_to_cpp(input_file, output_file):
    index = clang.cindex.Index.create()

    # tu = index.parse(input_file, args=['-std=c++11'])

    # Windows特定参数
    args = [
        '-finput-charset=UTF-8',
        '-std=c++11',
        '-x', 'c++',  # 强制按C++模式解析
        # r'-IC:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\include',  # MSVC头文件路径
        # r'-IC:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\ucrt',  # Windows SDK路径,
        r'-IC:\Program Files\CODESYS 3.5.19.60\CODESYS\CODESYS Control SL Extension Package\4.10.0.0\ExtensionSDK\include',
        # Windows SDK路径,
        # r'-ID:\Qt5.15\5.15.2\msvc2019_64\include\QtCore',
    ]
    tu = index.parse(input_file, args=args)

    struct_defs = []

    def analyze_typedef(node):
        if node.kind == CursorKind.TYPEDEF_DECL:
            canonical_type = node.type.get_canonical()
            # print("--", canonical_type.spelling)
            # if canonical_type.spelling.startswith("tag"):
            #     print("--", canonical_type.spelling)

            if canonical_type.kind == TypeKind.RECORD:
                struct_decl = canonical_type.get_declaration()
                struct_defs.append({
                    'new_name': node.spelling,
                    'members': list(get_struct_members(struct_decl))
                })

    def get_struct_members(struct_decl):
        for child in struct_decl.get_children():
            if child.kind == CursorKind.FIELD_DECL:
                # print("type:", child.type.spelling, child.type.get_array_size(), child.type.get_canonical().spelling)
                child_type = child.type.spelling
                child_name = child.spelling
                if child.type.get_array_size() != -1:  # 数组需要特殊处理
                    prefix = child_type.split('[')[0]
                    child_type = prefix
                    child_name += child.type.spelling.replace(prefix, "")
                    # print("----", child_type, child_name)

                if child_type == 'int' or child_type == 'int *':
                    child_type = '没定义_自己处理'

                yield {
                    'type': child_type,
                    'name': child_name
                }

    def generate_cpp_struct(def_info):
        lines = [
            f"struct {def_info['new_name']}",
            "{"
        ]
        for member in def_info['members']:
            lines.append(f"    {member['type']} {member['name']};")
        lines.append("};\n")
        return '\n'.join(lines)

    # AST遍历
    for node in tu.cursor.get_children():
        analyze_typedef(node)

    # 生成纯净CPP代码
    output_content = """#ifndef MEMORYDEFINE_H
#define MEMORYDEFINE_H
#include "CmpStd.h"
// ST语言的数据类型所占用的字节数:https://blog.csdn.net/u013186651/article/details/135324625
// 默认string类型的字节为:80 + 1
// 转换之后,假如出现了 int ,那么这个类型应该就是没有被正确识别,需要手动替换处理

// 有很多系统的第三方的库结构是没办法导出,因此需要自己在PLC系统中测量,然后自行用数组类型替换
// 替换的目的是内存对齐
// SMC_POS_REF -->48 Byte
// MC_KIN_REF_SM3 --> 8 Byte
// Kin_ArticulatedRobot_6DOF --> 760 Byte

"""
    output_content += '\r\n'.join([generate_cpp_struct(d) for d in struct_defs])

    output_content += '\r\n#endif // MEMORYDEFINE_H'

    # 写入文件
    os.makedirs(os.path.dirname(output_file), exist_ok=True)
    with open(output_file, 'w', encoding='utf-8') as f:
        f.write(output_content)


if __name__ == "__main__":
    source_code = """
    #include "CmpStd.h"
    
    // 系统未定义或者M4文件没有导出的类型,然后又通过PLC程序知道其长度
    struct SMC_POS_REF{int8_t data[48];};
    struct MC_KIN_REF_SM3{int8_t data[8];};
    struct Kin_ArticulatedRobot_6DOF{int8_t data[760];};
    
    """

    # 读取m4文件内容
    with open(m4FilePath, 'r', encoding='utf-8') as m4File:
        content = m4File.read()

    # 找到开始和结束标记的位置
    start_marker = """
#ifdef __cplusplus
extern "C" {
#endif
"""
    end_marker = """
#ifdef __cplusplus
}
#endif
"""
    start_index = content.find(start_marker) + len(start_marker)
    end_index = content.find(end_marker)

    print(start_index, end_index)
    # 检查是否找到了开始和结束标记
    if start_index != -1 and end_index != -1:
        # 截取标记之间的内容
        extracted_content = content[start_index:end_index]

        source_code += extracted_content

        # 创建一个临时文件
        with open(middleFilePath, 'w', encoding='utf-8') as middleFile:
            # 将源代码字符串写入文件
            middleFile.write(source_code)
            # 确保内容被写入磁盘
            middleFile.flush()

        print("开始转换")
        convert_c_struct_to_cpp(middleFilePath, outputFilePath)
        print("操作完成-----》")
    else:
        print("m4文件内容有误,无法提取")

脚本的一些注意事项已经在代码中注释了,就不另外说明了。

运行完脚本,就可以得到了符合我们需求的c++格式的代码文件了

https://i-blog.csdnimg.cn/direct/33a8ce55b2764321af5f1304f3b07791.png 脚本先将M4文件中的主要内容提取出来,然后添加一个头文件保存为一个中间文件。此时这个中间文件的结构体的定义还是c风格的。

然后将此中间文件交给clang解析,将结构体的内容分析出来,然后再将结构体的名称由原来的带tag的替换成没有带tag的。最后将所有结构体的内容保存成一个cpp风格的h文件。

需要注意的是,生成的头文件中有很多不必要的信息,自己手动删除即可。

3.添加RTTR注册信息

从我们前一篇文章可以知道,要使用RTTR的功能,必须要对每一个结构体进行注册处理。我们结构体这么多,一个个手动写代码,不现实。我们还是用脚本来自动处理吧,这个脚本输入的是前面脚本生成的头文件:

import sys
import clang.cindex
from clang.cindex import CursorKind

clang.cindex.Config.set_library_path("D:/Qt/clang+llvm-20.1.0-x86_64-pc-windows-msvc/bin")

srcFilePath = "./output/memorydefine.h"
dstFilePath = "./output/memorydefine.cpp"

def get_struct_members(cursor):
    members = []
    for child in cursor.get_children():
        if child.kind == CursorKind.FIELD_DECL:
            member_type = child.type.spelling
            # 处理数组类型(保留方括号)
            if child.type.get_array_size() != -1:
                array_size = child.type.get_array_size()
                member_type = f"{child.type.element_type.spelling}[{array_size}]"
            members.append((child.spelling, member_type))

    return members


def generate_rttr_code(structs_map):
    code = "RTTR_REGISTRATION\n{\n"
    for struct_name, members in structs_map.items():
        code += f"    registration::class_<{struct_name}>(\"{struct_name}\")\n"
        for member_name, _ in members:
            code += f"    .property(\"{member_name}\", &{struct_name}::{member_name})(policy::prop::as_reference_wrapper)\n"
        code += "    ;\n\n"
    code += "}"
    return code


def analyze_header(file_path):
    index = clang.cindex.Index.create()

    # Windows特定参数
    args = [
        '-std=c++11',
        '-x', 'c++',  # 强制按C++模式解析
        # r'-IC:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\include',  # MSVC头文件路径
        # r'-IC:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\ucrt',  # Windows SDK路径,
        r'-IC:\Program Files\CODESYS 3.5.19.60\CODESYS\CODESYS Control SL Extension Package\4.10.0.0\ExtensionSDK\include',
        r'-ID:\Qt5.15\5.15.2\msvc2019_64\include\QtCore'
    ]
    tu = index.parse(file_path, args=args)

    structs = {}

    def visit_node(cursor):
        if cursor.kind == CursorKind.STRUCT_DECL and cursor.is_definition():
            struct_name = cursor.spelling
            if struct_name and not struct_name.startswith('_'):  # 忽略匿名结构体
                structs[struct_name] = get_struct_members(cursor)
        for child in cursor.get_children():
            visit_node(child)

    visit_node(tu.cursor)
    return generate_rttr_code(structs)


if __name__ == "__main__":
    # print(analyze_header("MyStruct.h"))
    # print(analyze_header("E:\zhongyong\zyQt\Robot\CommunicationTest\communication\sharedMemory\memorydefine.h"))

    fileContent = """#include "memorydefine.h"
    
#include <rttr/registration>
#include <rttr/type>
#include <vector>

using namespace rttr; 
"""
    fileContent += analyze_header(srcFilePath)
    with open(dstFilePath, 'w', encoding='utf-8') as f:
        f.write(fileContent)

处理完之后,我们就得到了RTTR注册的代码

https://i-blog.csdnimg.cn/direct/5075e46f49034d8eac1330e0ab5ceed8.png 这个处理后生成的代码,就保存成cpp文件。只要将前面生成的h文件一起加到我们自己的工程,我们就可以对PLC放在共享内存上的结构体全知全晓了。

4.读取PLC变量值

读取变量直接将结构体指针指向约定好的那一块共享内存,然后读即可。

可以选择直接用变量名读,也可以通过RTTR的字符串属性来读,选择你喜欢的方式就好。

5.更改PLC变量值

这个稍微复杂一些。

首先,在我们已经在 中实现了获取某个子成员地址相对于主数据的地址的偏移,而经过测试、CodeSys上的数据结构及结构体的对齐策略是与Qt这边是一致的。

因此,我们完全可以将要写的变量的值+类型+地址的偏移发送给PLC,PLC收到之后,按照偏移来对变量赋值

https://i-blog.csdnimg.cn/direct/989d3e64a8414868901f70299e3be774.png 要实现这个功能,得灵活使用结构体、指针和共用体。

更加具体的代码就不详述了。


参考: