文章目錄
- 1.背景
- 2.結構體從CodeSys導出后導入到C++
- 2.1.將結構體從CodeSys中導出
- 2.2.將結構體從m4文件提取翻譯成c++格式
- 3.添加RTTR注冊信息
- 4.讀取PLC變量值
- 5.更改PLC變量值
- 6.Qt讀寫CodeSys的共享內存
1.背景
在文章【基于RTTR在C++中實現結構體數據的多層級動態讀寫】中,我們實現了通過字符串讀寫結構體中的變量。那么接下來我們開始與CodeSys來進行交互。
由于我們是基于共享內存來通訊的,那么我們需要對共享的內存定義一個數據結構,也就是一個結構體。
假如我們和PLC的通訊只是簡單的一個結構體,結構體中都是一些POD(Plain Old Data),那可以直接和PLC程序編寫人員協商溝通好,讓他把結構的定義代碼發給你,你再根據ST代碼寫出結構體的C++代碼。
但是,在實際的項目中,要到使用到的結構體往往是多種類型的結構體互相嵌套的結果。不僅結構體多、數據多,而且還存在數組、嵌套的方式,單純靠手工來拷貝ST代碼-》轉C++代碼必定是繁瑣且容易出錯的。因此,必須得搞一套穩定可靠的導出導入機制。
這里我選擇通過利用CodeSys的機制+python腳本來實現
2.結構體從CodeSys導出后導入到C++
要想將Application的結構體數據直接導出,貌似是不行的,但是可以先把結構體數據復制到一個Library工程,然后導出m4文件,最后利用python腳本翻譯(處理)成我們需要的代碼。
2.1.將結構體從CodeSys中導出
我這里有一個Application工程,里面定義了若干結構體
假如想將其導出,那么可以新建一個Library工程,然后將結構體復制過去(直接在左側的樹狀列表中選擇、復制,而不是直接復制代碼)。
然后選擇 編譯–》生成運行時系統文件
然后勾選M4接口文件
然后點擊確定、生成M4文件。
如此,便完成了結構體的導出。
2.2.將結構體從m4文件提取翻譯成c++格式
其實打開M4文件看一下,可以發現,導出數據已經是c語言格式的結構體了,基本都可以拿來直接用了,但是由于后面要和RTTR結合使用,必須還得清洗處理一下。
M4文件的清洗處理我們需要用到clang(LLVM)庫。
我們是在python中使用clang,因此我們需要在python中安裝此工具包,我安裝的是20.1.0:
但是在python中安裝了還不行,還得去官網將依賴的庫及程序文件下載下來
【llvm github】
下載之后,解壓到某個路徑下即可,不用安裝
然后就可以使用腳本了,這是我的腳本
在腳本中指定好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.spellingchild_name = child.spellingif child.type.get_array_size() != -1: # 數組需要特殊處理prefix = child_type.split('[')[0]child_type = prefixchild_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++格式的代碼文件了
腳本先將M4文件中的主要內容提取出來,然后添加一個頭文件保存為一個中間文件。此時這個中間文件的結構體的定義還是c風格的。
然后將此中間文件交給clang解析,將結構體的內容分析出來,然后再將結構體的名稱由原來的帶tag的替換成沒有帶tag的。最后將所有結構體的內容保存成一個cpp風格的h文件。
需要注意的是,生成的頭文件中有很多不必要的信息,自己手動刪除即可。
3.添加RTTR注冊信息
從我們前一篇文章可以知道,要使用RTTR的功能,必須要對每一個結構體進行注冊處理。我們結構體這么多,一個個手動寫代碼,不現實。我們還是用腳本來自動處理吧,這個腳本輸入的是前面腳本生成的頭文件:
import sys
import clang.cindex
from clang.cindex import CursorKindclang.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 membersdef 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 codedef 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.spellingif 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注冊的代碼
這個處理后生成的代碼,就保存成cpp文件。只要將前面生成的h文件一起加到我們自己的工程,我們就可以對PLC放在共享內存上的結構體全知全曉了。
4.讀取PLC變量值
讀取變量直接將結構體指針指向約定好的那一塊共享內存,然后讀即可。
可以選擇直接用變量名讀,也可以通過RTTR的字符串屬性來讀,選擇你喜歡的方式就好。
5.更改PLC變量值
這個稍微復雜一些。
首先,在我們已經在【基于RTTR在C++中實現結構體數據的多層級動態讀寫】中實現了獲取某個子成員地址相對于主數據的地址的偏移,而經過測試、CodeSys上的數據結構及結構體的對齊策略是與Qt這邊是一致的。
因此,我們完全可以將要寫的變量的值+類型+地址的偏移發送給PLC,PLC收到之后,按照偏移來對變量賦值。
要實現這個功能,得靈活使用結構體、指針和共用體。
更加具體的代碼就不詳述了。
6.Qt讀寫CodeSys的共享內存
直接用QSharedMemory來讀寫的話,無論在Windows下還是Linux下,都是不行的。
在Windows報權限不足,哪怕你加了Global\\
也不行;
因此,在Windows下使用系統api;在Linux下,用QFile讀寫或者QFile::map來操作/dev/shm
下的對應的文件。
#include <windows.h>---// 讀共享內存{// 打開已存在的共享內存HANDLE hMapFile = OpenFileMapping(FILE_MAP_WRITE | FILE_MAP_READ, // 讀寫權限FALSE, // 不繼承句柄L"Global\\PLC_MEMORY_WRITE");if (hMapFile == NULL) {std::cerr << "OpenFileMapping fail: " << GetLastError() << std::endl;return 1;}// 映射內存視圖LPVOID pBuffer = MapViewOfFile(hMapFile,FILE_MAP_ALL_ACCESS,0,0,1024);if (pBuffer == NULL) {std::cerr << "MapViewOfFile失敗: " << GetLastError() << std::endl;CloseHandle(hMapFile);return 1;}qDebug() << "the read pointer:" << pBuffer;}// 寫共享內存{// 打開已存在的共享內存HANDLE hMapFile = OpenFileMapping(FILE_MAP_WRITE | FILE_MAP_READ, // 讀寫權限FALSE, // 不繼承句柄L"Global\\PLC_MEMORY_READ");if (hMapFile == NULL) {std::cerr << "OpenFileMapping fail: " << GetLastError() << std::endl;return 1;}// 映射內存視圖LPVOID pBuffer = MapViewOfFile(hMapFile,FILE_MAP_ALL_ACCESS,0,0,1024);if (pBuffer == NULL) {std::cerr << "MapViewOfFile失敗: " << GetLastError() << std::endl;CloseHandle(hMapFile);return 1;}qDebug() << "the write pointer:" << pBuffer;}
參考:
【基于RTTR在C++中實現結構體數據的多層級動態讀寫】
【共享內存 - C#與CoDeSys通訊】
【clang 在 Windows 下的安裝教學】
【CodeSys平臺ST語言編程】