【STM32开发笔记】移植AI框架TensorFlow到STM32单片机【下篇】

news2024/10/3 11:45:25

【STM32开发笔记】移植AI框架TensorFlow到STM32单片机【下篇】

    • 一、上篇回顾
    • 二、项目准备
      • 2.1 准备模板项目
      • 2.2 支持计时功能
      • 2.3 配置UART4引脚
      • 2.4 支持printf重定向到UART4
      • 2.5 支持printf输出浮点数
      • 2.6 支持printf不带`\r`的换行
      • 2.7 支持ccache编译缓存
    • 三、TFLM集成
      • 3.1 添加tflite-micro源码
      • 3.2 修正micro_time.cc代码
      • 3.3 构建micro_time.cc的规则
      • 3.4 添加TFLM构建规则
      • 3.5 添加TFLM函数调用
      • 3.6 添加TFLM依赖关系
    • 四、TFLM测试
      • 4.1 编译TFLM和Appli项目
      • 4.2 下载Boot代码
      • 4.3 下载Appli代码
      • 4.3 运行TFLM基准测试
    • 五、问题解决
      • 5.1 benchmark编译失败
      • 5.2 Appli链接报错
      • 5.3 benchmark无法正常开始
      • 5.4 Release版无法正常返回
    • 六、源码分享
    • 七、参考链接

本文将会继续介绍——如何为STM32H7S78-DK开发板准备CMake项目、如何将TFLM集成到基于CMake的STM32项目中、如何在STM32H7S78-DK开发板上运行TFLM基准测试,具体包括如何支持计时和printf输出、如何集成TFLM到基于CMake的STM32项目,以及解决过程中遇到的一些问题。

一、上篇回顾

书接上回,上篇文章主要分为TFLM是什么、TFLM初步体验、TFLM源码浅析、TFLM主体移植几个部分。其中,TFLM初步体验部分将会介绍如何在PC上运行TFLM基准测试,TFLM源码浅析部分主要介绍TFLM源码是如何进行构建的,TFLM主体移植主要介绍如何在基于CMake的STM32项目中构建TFLM库和基准测试。

上篇链接: https://blog.csdn.net/xusiwei1236/article/details/142467410

二、项目准备

2.1 准备模板项目

项目模板采用基于CMake的STM32H7S78-DK项目,代码仓为:

https://gitcode.com/xusiwei1236/STM32H7S78-DK-XIP

该项的ioc文件来自官方STM32CubeH7RS软件包Template_XIP项目,修改了部分配置,项目类型改为了CMake;然后使用CubeMX生成的项目代码即为本项目的主要代码。

2.2 支持计时功能

STM32上,使用HAL库记录耗时非常简单,只需要用:

  • HAL_GetTick() 获取Tick数即可,默认的Tick频率是1000Hz;
  • 需要注意的是: HAL_GetTickFreq() 返回的枚举值,并不是实际的频率(例如默认的HAL_TICK_FREQ_1KHZ,其值为1,而不是1000)。

因此,记录使用HAL_GetTick记录耗时,代码类似:

uint32_t start = HAL_GetTick();

// 需要记录耗时的代码

uint32_t end = HAL_GetTick();
float cost_s = (end - start) / 1000.0f;  // 实际耗时(单位:秒)

这部模板本身已经支持了,不需要额外的工作。

2.3 配置UART4引脚

开发板上自带了ST-Link V3调试器,该调试器带有虚拟串口功能。通过查阅原理图,我们知道主控MCU和ST-Link之间的连接关系如下图:

202408282134895

可以看到,ST-Link的虚拟串口和主控芯片的连接关系为:

  • VCP_RX连接到主控芯片的 PD0上;
  • VCP_TX连接到主控芯片的 PD1上;

接下来,需要修改这两个引脚的功能:

image-20240923214140259

启用UART4功能:

image-20240923214457990

完成上述修改后,Ctrl+S保存,然后重新生成项目代码。

2.4 支持printf重定向到UART4

CubeMX选择CMake项目后,默认已经生成了 syscalls.c文件,已经实现了支持gcc工具链的printf输出的一半功能:

stm32_syscall_write

这个_write支持printf和fprintf调用__io_puchar进行输出。

另外一半功能——实现__io_puchar输出到UART即可实现printf输出到UART。

需要手动修改main.c文件,实现__io_puchar函数:

stm32_io_putchar

2.5 支持printf输出浮点数

默认生成的CMake项目不支持浮点数打印,需要修改链接选项,修改文件Appli\CMakeLists.txt

在末尾添加如下代码片段:

target_link_options(${CMAKE_PROJECT_NAME} PRIVATE
    -u _printf_float
)

之后,再次编译,就可以输出浮点数了。

2.6 支持printf不带\r的换行

大部分串口终端工具,例如MobaXterm,换行需要收到\r\n两个字符才能正常换行。通过修改代码,可以让测试代码输出\n结尾也能和\r\n一样自动换行,具体实现方式为:

stm32_syscall_write_endl

这样修改之后,printf就同时支持了\r\n\n两种换行符。

2.7 支持ccache编译缓存

修改CMake有时候需要清理build目录才能正常出发重新配置和构建,但清理了build目录后重新构建的过程非常耗时。为了解决这个问题,我们可以使用ccache进行加速。

ccache下载链接: Ccache — Download

Windows平台的ccache是压缩包,解压到合适的目录后,将其配置到PATH环境变量,即可在任意位置使用ccache命令。

完成ccache配置之后,可以咋CMake代码中加入如下片段,实现CMake支持ccache加速:

find_program(CCACHE_PROGRAM ccache)
if (CCACHE_PROGRAM)
    set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
    set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
    message(STATUS "Ccache found: ${CCACHE_PROGRAM}!")
else ()
    message(STATUS "Ccache not found!")
endif ()

三、TFLM集成

3.1 添加tflite-micro源码

首先,将PC上运行过基准测试的TFLM代码拷贝到CubeMX生成的基于CMake的STM32H7S78-DK项目中,并放在如下目录:

Middlewares/tensorflow

然后,将前“TFLM移植”章节编写完成的CMakeLists.txt文件、micro_time.cc文件,也放到这个目录中。另外,将后下一步需要修改的generate_cc_arrays.py文件也拷贝一份放到该目录中。

完成上述操作后,项目文件布局如下:

image-20240928003233905

3.2 修正micro_time.cc代码

TFLM默认的micro_time.cc文件在STM32上不能正常工作,上篇文章已经给出了一个版本的实现,经过测试上篇文章的代码不能实现预期。

实际需要修改为使用调用HAL库的代码:

TFLM-micro_time_stm32_hal

这里的ticks_per_second函数用于返回ticks频率,GetCurrentTimeTicks函数用于返回当前的ticks数;

3.3 构建micro_time.cc的规则

为了正确的构建micro_time.cc,需要将TFLM默认的micro_time.cc文件过滤掉,因此需要修改上一篇文章中我们实现的CMakeLists.txt代码,具体修改为:

image-20240927220255886

这里,默认的micro_time.cc由306行代码匹配上,314行实现了将其从MICROLITE_CC_BASE_SRCS中过滤掉。

3.4 添加TFLM构建规则

接下来修改顶层的CMakeLists.txt文件,在最后追加两行:

image-20240928003722101

这样,就将TFLM的构建规则添加到了CubeMX生成的CMake项目中了。

3.5 添加TFLM函数调用

上篇文章中,我们实现的TFLM的CMakeLists.txt以及完成了对TFLM库和基准测试库的构建,并且最终会生成三个静态库:

  • TFLM库,包含TFLM所有类和函数实现代码;
  • keyword_benchmark 库,包含keyword_benchmark函数实现代码;
  • person_detection_benchmark 库,包含person_detection_benchmark函数实现代码;

接着,在我们的Appli子项目的代码中,添加对TFLM的调用。

由于C++函数支持参数重载,编译器生成C++函数会经过名称修饰。同样的函数声明代码,放在C++代码文件中和C代码文件中经过编译生成的二进制符号不同。这导致,在C代码中,我们无法通过声明函数或包含头文件的方式,直接调用C++函数;否则,链接时报告符号找不到。

因此,我们无法通过在Appli子项目的main.c中直接调用keyword_benchmark函数或者person_detection_benchmark函数的方式实现基准测试的集成。

为了解决C代码中不能直接调用C++函数的问题,我们需要引入一个中间层。这里需要用到几个C++相关的知识:

  • 使用extern "C"修饰C++函数,可以让编译器将该C++函数按照C函数的方式生成符号,不进行名称修饰;
  • C代码中可以调用由名称修饰的C++函数,就像调用其他C函数一样;
  • C++有默认的内置宏,__cplusplus用于只是当前C++编译器支持的语言标准版本,例如201103L表示C++11;

Appli\Src目录中,创建tflm_benchmark.h文件,内容如下:

tflm_benchmark_h

与之对应的tflm_benchmark.cc文件,内容为:

tflm_benchmark_cc

这里,通过KEYWORD_BENCHMARKPERSON_DETECTION_BENCHMARK两个宏,实现两个基准测试的开关。

接下来就可以修改Appli子项目的main.c,调用这个tflm_benchmark函数了:

image-20240927231439544

3.6 添加TFLM依赖关系

接下来,需要为我们的Appli添加对TFLM的依赖关系,包括对TFLM库和基准测试的依赖,具体修改的代码为:

image-20240927230012557

左侧行号标记绿色的即为新增代码行,一共六处,作用分别为:

  • 60~64行,定义了几个CMake变量,后面会用到;
  • 68~69行,为当前Appli子项目的构建目标,添加KEYWORD_BENCHMARK(或PERSON_DETECTION_BENCHMARK)宏;
  • 75行,为当前Appli子项目的构建目标,添加头文件搜索目录;
  • 81~84行,为当前Appli子项目的构建目标,添加三个源代码文件;
  • 90行,为当前Appli子项目的构建目标,添加库文件搜索目录;
  • 96~98行,为当前Appli子项目的构建目标,添加链接keyword_benchmark(或person_detection_benchmark)库、tflite-micro库;

完成以上全部修改后,就完成了对TFLM库和基准测试的集成工作。

四、TFLM测试

好了,万事俱备,只欠东风!

完成前面的所有工作后,就可以准备在我们的STM32H7S78-DK上进行TFLM基准测试了。

4.1 编译TFLM和Appli项目

编译构建,主要使用VSCode的CMake插件工具栏,具体方法不再赘述,感兴趣的可以参考我之前发的帖子: 【STM32H7S78-DK评测】搭建基于ST官方VSCode扩展的STM32开发环境 - STM32团队 ST意法半导体中文论坛 (stmicroelectronics.cn)

编译之前,先清理一下项目:

image-20240927231911537

接着,构建TFLM核心库:

image-20240927231940611

继续,生成基准测试库keyword_benchmark

image-20240927232008752

以及基准测试库person_detection_benchmark

image-20240927232338456

紧接着,构建Appli项目:

image-20240927232441027

构建完成后,可以看到RAM、Flash占用信息:

image-20240927232835085

例如,图中的Flash占用为115244 B

Appli类似的方式,进行Boot子项目的构建,不再赘述。

4.2 下载Boot代码

由于Appli代码需要使用Boot代码进行跳转,因此,下载Appli代码之前,需要线下载Boot代码到开发板上。

下载之前,先将STM32H7S78-DK开发板和PC通过USB线连接好,板子由三个USB口,注意连接到标有STLK的。

接着,VSCode上上操作:

image-20240927234045648

终端子窗口可以看到输出:

image-20240927234154457

4.3 下载Appli代码

接下来,将我们的STM32H7S78-DK开发板和PC通过USB线连接好,板子由三个USB口,注意连接到标有STLK的。

然后,在VSCode上操作:

image-20240927233206011

终端子窗口可以看到输出:

image-20240927234238275

4.3 运行TFLM基准测试

打开MobaXterm,添加会话,选择STLink的虚拟串口设备,参数如下:

image-20240927234401111

连接设备之后,按下开发板上的NRST按钮,重启设备,可以看到串口输出如下:

生成的图片20240927235823

可以看到,keyword模型初始化耗时3毫秒,单独运行一次耗时2毫秒,连续运行10次耗时10毫秒,速度还是可以的。

与之对比的,在PC上运行keyword_benchmark的结果数据:

生成的图片20240928000642

可以看到,PC上模型初始化和单独运行一次耗时都不到1毫秒,连续运行10次耗时3毫秒。

同样,稍加修改Appli\CMakeLists.txt,我们可以编译person_detection_benchmark,并得到在开发板上运行结果数据:

生成的图片20240927235722

可以看到,开发板上,运行有人图像的人体检测耗时为993毫秒,没有人的耗时为994毫秒;连续运行10次的耗时分别为9938毫秒和9940毫秒,速度有点慢。

与之对应的,PC上运行person_detection_benchmark的结果数据为:

生成的图片20240928000424

可以看到,PC上运行有人图像的人体检测耗时为36毫秒,没有人的耗时为35毫秒;连续运行10次的耗时分别为9938毫秒和9940毫秒,

五、问题解决

在前面的第三章、第四章的实践过程中,我遇到了一些问题,为了保持主体部分的简洁清晰,没有将问题描述和解决方法写在第三章、第四章内容中。本章将介绍预提遇到的问题,以及问题的解决方法,如果你在实践过程中遇到类似的问题,可以参考本章的方法进行解决。

5.1 benchmark编译失败

【问题现象】 编译失败,报错信息:

image-20240928165305210

【问题原因】 直接原因是脚本生成代码中,数组和变量定义有问题(把路径带入进去了):

image-20240928165700113

【解决方法】 修改代码生成脚本:

image-20240928170346632

通过排查脚本生成代码,可以知道**【问题根因】**是Windows系统的路径分隔符不是/.split('/')失败。

根据走读代码,可以知道代码中的base_array_name是文件名的基础部分,也就是去掉路径和扩展名;重新实现一下就好了。

5.2 Appli链接报错

【问题现象】 Appli链接失败,报错信息:

image-20240928223506797

【问题原因】 Appli子项目使用了VFP寄存器参数(VFP register arguments),而libtflite-micro.a没有使用;

【解决方法】 顶层修改CMakeLists.txt,添加一行:

image-20240928223843589

5.3 benchmark无法正常开始

【问题现象】 无法正常运行benchmark;

【初步调试】 运行到benchmark入口函数之后,无法进入tflite::InitializeTarget函数;

image-20240928233048857

下一步直接进入HardFault_Handler;

【问题分析】 查看寄存器,发现栈指针位置异常:

image-20240928233805024

已经超出了链接脚本设置的栈范围:

image-20240928233921866

根据这段代码,可以知道正常的栈指针范围应该在[0x20000000, 0x20010000)64K范围内。

【代码排查】 反汇编查看benchmark入口代码:

image-20240928234355163

可以看到入口处申请的栈内存空间为81920(80K),超过链接脚本设置的64K栈空间,结合代码内容,可以知道是MicroProfiler对象占用的空间。

【解决方法】 修改MicroProfiler代码,具体修改如下:

image-20240928234743789

因为,这个常量是几个数组的大小:

image-20240928235038560

初步估算:5*4*4K正好是80K

因此,把这个常量的值改小即可解决该问题。

5.4 Release版无法正常返回

【问题现象】 Debug版本可以正常运行,Release版benchmark函数无法正常返回到main函数;

【问题原因】 经过排查, 发现原因是benchmark函数写了返回值类型int,但没有写return语句;

【解决方法】 修改代码,benchmark函数最后添加一行return 0;语句即可;

六、源码分享

本项目的所有代码已经开源到GitCode平台,感兴趣的小伙伴可以免费下载体验: https://gitcode.com/xusiwei1236/STM32H7S78-DK-TFLM.git

本代码仓使用了git submodule特性,需要用--recursive选项进行克隆:

git clone --recursive https://gitcode.com/xusiwei1236/STM32H7S78-DK-TFLM.git

另外,tflite-micro依赖的一些三方软件已经打包到了如下仓库:

https://gitcode.com/tflm/downloads.git

下载方法:

# 跳转到 tflite-micro 子目录
cd Middlewares/tensorflow/tflite-micro

# 下载 downloads 下的三方软件源码
git clone https://gitcode.com/tflm/downloads.git tensorflow/lite/micro/tools/make/downloads/

七、参考链接

  1. CCache下载页面: Ccache — Download
  2. CMake中使用CCache: Use Ccache with CMake | Lindevs
  3. 官方STM32CubeH7RS软件包的XIP项目模板: Template_XIP
  4. TensorFlow Lite for Microcontrollers介绍: TensorFlow Lite for Microcontrollers (google.cn)
  5. TensorFlow Lite for Microcontrollers入门: 微控制器入门 | TensorFlow (google.cn)
  6. tflite-micro 源码GitHub仓: https://github.com/tensorflow/tflite-micro
  7. CMake最新文档: CMake Reference Documentation — CMake 3.30.3 Documentation

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2186138.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

记录win11 蓝屏修复

1原因: win11 edge 的浏览器异常 打开新窗口的广告 打不开显示网络未连接下载驱动精灵 下载驱动要开会员 果断卸载 然后发现没有卸载干净 任务管理器 搜驱动 不小心干掉了win自带的文件win提示更新 更新重启就蓝屏随便点击一个新闻页面 解决办法: 在…

吴恩达深度学习笔记:卷积神经网络(Foundations of Convolutional Neural Networks)2.5-2.6

目录 第四门课 卷积神经网络(Convolutional Neural Networks)第二周 深度卷积网络:实例探究(Deep convolutional models: case studies)2.5 网络中的网络以及 11 卷积(Network in Network and 11 convoluti…

利用Spring Boot开发“衣依”服装销售系统

1系统概述 1.1 研究背景 如今互联网高速发展,网络遍布全球,通过互联网发布的消息能快而方便的传播到世界每个角落,并且互联网上能传播的信息也很广,比如文字、图片、声音、视频等。从而,这种种好处使得互联网成了信息传…

如何从 PC 中检索已删除的文件?从 PC 恢复已删除的照片技巧

按 Shift Delete 以后后悔?想要恢复已删除的照片吗?好吧,如果是这样的话,那么您来对地方了。在本文中,我们将讨论如何从 PC 中检索已删除的文件。 自从摄影的概念被提出以来,人们就对它着迷。早期的照片保…

YOLO11改进|上采样篇|引入DySample轻量级动态上采样器

目录 一、DySample轻量级动态上采样器1.1DySample上采样模块介绍1.2DySample核心代码 五、添加DySample上采样器5.1STEP15.2STEP25.3STEP35.4STEP4 六、yaml文件与运行6.1yaml文件6.2运行成功截图 一、DySample轻量级动态上采样器 1.1DySample上采样模块介绍 DySample是一种基…

Koa2+Vue2的简书后台管理系统

文章目录 项目实战:前(vue)后(koa)端分离1、创建简书项目2、创建数据库2.1 创建数据库2.2 连接数据库3、模型对象3.1 设计用户模块的Schema3.2 实现用户增删改查3.2.1 增加用户3.2.2 修改用户3.2.3 删除用户3.2.4 查询用户4、封装业务逻辑层5、封装CRUD6、创建Vue项目7、配…

“衣依”服装销售平台:Spring Boot技术架构剖析

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统,它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等,非常适…

TCP四次挥手过程详解

TCP四次挥手全过程 有几点需要澄清: 1.首先,tcp四次挥手只有主动和被动方之分,没有客户端和服务端的概念 2.其次,发送报文段是tcp协议栈的行为,用户态调用close会陷入到内核态 3.再者,图中的情况前提是双…

【CKA】十、统计node节点ready状态的数量

10、统计node节点ready状态的数量 1. 考题内容: 2. 答题思路: 1、检查有多个node状态ready 2、去除有Taint和NoSchedule的节点数量 3、将结果写入到指定文件中 3. 官网地址: https://kubernetes.io/zh-cn/docs/reference/node/node-statu…

LeetCode[中等] 45. 跳跃游戏 II

给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说&#xff0c;如果你在 nums[i] 处&#xff0c;你可以跳转到任意 nums[i j] 处: 0 < j < nums[i] i j < n 返回到达 nums[n - 1] 的最…

在Docker中运行微服务注册中心Eureka

1、Docker简介&#xff1a; 作为开发者&#xff0c;经常遇到一个头大的问题&#xff1a;“在我机器上能运行”。而将SpringCloud微服务运行在Docker容器中&#xff0c;避免了因环境差异带来的兼容性问题&#xff0c;能够有效的解决此类问题。 通过Docker&#xff0c;开发者可…

五子棋双人对战项目(4)——匹配模块(解读代码)

目录 一、约定前后端交互接口的参数 1、websocket连接路径 2、构造请求、响应对象 二、用户在线状态管理 三、房间管理 1、房间类&#xff1a; 2、房间管理器&#xff1a; 四、匹配器(Matcher) 1、玩家实力划分 2、加入匹配队列&#xff08;add&#xff09; 3、移除…

golang grpc初体验

grpc 是一个高性能、开源和通用的 RPC 框架&#xff0c;面向服务端和移动端&#xff0c;基于 HTTP/2 设计。目前支持c、java和go&#xff0c;分别是grpc、grpc-java、grpc-go&#xff0c;目前c版本支持c、c、node.js、ruby、python、objective-c、php和c#。grpc官网 grpc-go P…

Linux相关概念和重要知识点(11)(进程调度、Linux内核链表)

1.Linux调度算法 上篇文章我粗略讲过queue[140]的结构&#xff0c;根据哈希表&#xff0c;我们可以将40个不同优先级的进程借助哈希桶链入queue[140]中。调度器会根据queue的下标来进行调度。但这个具体的调度过程是怎样的呢&#xff1f;以及runqueue和queue[140]的关系是什么…

谷歌给到的185个使用生成式AI的案例

很多公司从利用AI回答问题&#xff0c;进而使用AI进行预测&#xff0c;向使用生成式AI Agent转变。AI Agent的独特之处在于它们可以采取行动以实现特定目标&#xff0c;比如引导购物者找到合适的鞋子&#xff0c;帮助员工寻找合适的健康福利&#xff0c;或在护理人员交接班期间…

python之输入输出

1、输入 Python在控制台输入内容&#xff0c;需要使用input函数。input函数会在控制台等待用户输入&#xff0c;直到用户按下了回车键才算完成输入。 注意&#xff1a;input函数接收的内容为字符串。 str1 input("请输入内容\n") print(str1) print(type(str1))1…

Python酷库之旅-第三方库Pandas(132)

目录 一、用法精讲 591、pandas.DataFrame.plot方法 591-1、语法 591-2、参数 591-3、功能 591-4、返回值 591-5、说明 591-6、用法 591-6-1、数据准备 591-6-2、代码示例 591-6-3、结果输出 592、pandas.DataFrame.plot.area方法 592-1、语法 592-2、参数 592-…

9.28学习笔记

1.ping 网址 2.ssh nscc/l20 3.crtl,打开vscode的setting 4.win 10修改ssh配置文件及其密钥权限为600 - 晴云孤魂 - 博客园 整体来看&#xff1a; 使用transformer作为其主干网络&#xff0c;代替了原先的UNet 在latent space进行训练&#xff0c;通过transformer处理潜…

查缺补漏----该不该考虑不可屏蔽中断

可以看看这个视频&#xff1a; 讨论中断时&#xff0c;该不该考虑不可屏蔽中断&#xff1f;_哔哩哔哩_bilibili 首先要知道一个概念&#xff1a;可屏蔽中断和不可屏蔽中断 可屏蔽中断&#xff1a; 可屏蔽中断是可通过中断屏蔽字来启用或禁用的中断。对于多级中断而言&#…

①EtherCAT转ModbusTCP, EtherCAT/Ethernet/IP/Profinet/ModbusTCP协议互转工业串口网关

EtherCAT/Ethernet/IP/Profinet/ModbusTCP协议互转工业串口网关https://item.taobao.com/item.htm?ftt&id822721028899 协议转换通信网关 EtherCAT 转 ModbusTCP GW系列型号 MS-GW15 简介 MS-GW15 是 EtherCAT 和 Modbus TCP 协议转换网关&#xff0c;为用户提供一种 …