按照思路之一的大概想法-----分享一个最简单的Modbus多参数读写例程,12个轮询单字寄存器任务,每个任务轮询时间测试在10ms以内。匆忙写的子程序,错漏难免,仅供参考.
类似比它优秀的例程太多,带着批判性观点随便看看,不一定对,更不一定有用.
接口如下:
函数指针 IN DWORD 接口变量-指针
从控触发 TEMP BOOL 公共变量-读写
从控完成 TEMP BOOL 公共变量-读写
从控完缓 TEMP BOOL 公共变量-读写
从控完沿 TEMP BOOL 公共变量-读写
从控收发 TEMP BOOL 公共变量-读写
从控复位 TEMP BOOL 公共变量-读写
从控复缓 TEMP BOOL 公共变量-读写
从控错误 TEMP BYTE 公共变量-读写
全局循计 TEMP DWORD 公共变量-读写
全局时钟 TEMP DWORD 公共变量-只读
全局循环 TEMP DWORD 公共变量-只读
恢复毫秒 TEMP DWORD 公共变量-只读
从波特率 TEMP DWORD 公共变量-只读
从校验码 TEMP BYTE 公共变量-只读
从端口号 TEMP BYTE 公共变量-只读
从超毫秒 TEMP WORD 公共变量-只读
从读写完 TEMP BOOL 私有变量-读写
从读写缓 TEMP BOOL 私有变量-读写
从读写沿 TEMD BOOL 私有变量-读写
从站触发 TEMP BOOL 私有变量-读写
从跳标志 TEMP BOOL 私有变量-读写
从跳标缓 TEMP BOOL 私有变量-读写
从读写错 TEMP BYTE 私有变量-读写
从超动时 TEMP DWORD 私有变量-读写
从优先级 TEMP BYTE 私有变量-只读
从站地址 TEMP BYTE 私有变量-只读
从读写控 TEMP BYTE 私有变量-只读
从寄存址 TEMP DWORD 私有变量-只读
从寄存数 TEMP INT 私有变量-只读
从数据址 TEMP DWORD 私有变量-只读
函数缓针 TEMP DWORD 临时变量-缓存
跳站循环 TEMP INT 临时变量-缓存
开始例程:
ModbusRTU多参数读写.zip
结束例程:
Smart_MP_V1.6.zip
数据块
![](/club/bbs/upload/image/20220808/6379559202671962068753198.png)
程序块
![](/club/bbs/upload/image/20220808/6379557714886490318262184.png)
![](/club/bbs/upload/image/20220808/6379557716510565802964384.png)
![](/club/bbs/upload/image/20220808/6379557716782442517234248.png)
![](/club/bbs/upload/image/20220808/6379557717042785174073805.png)
![](/club/bbs/upload/image/20220808/6379557717296835539629282.png)
![](/club/bbs/upload/image/20220808/6379557717645927646499262.png)
![](/club/bbs/upload/image/20220808/6379559184174701639063564.png)
![](/club/bbs/upload/image/20220808/6379557718003997318639229.png)
![](/club/bbs/upload/image/20220808/6379557721319837067671930.png)
![](/club/bbs/upload/image/20220808/6379557731010311795770156.gif)
具体时序以及原理分析见88楼/70楼/61楼/60楼/58楼/53楼/51楼/46楼/09楼.
V1.6最终版MBUS_CTRL按Zane版讲的全扫描!抢占优先级需要简单的双向链表以及耗费占用时序和空间,并且要对例程的数据结构重新组织,虽然简单但没有什么动力去做,如有新的例程会贴在新帖里("新帖"绣罗襦,双双金鹧鸪.)
结帖: 在"一"中:一些资料说指针和Char类型(8位Byte)是C的灵魂.而在Smart里面由于编译器的原因可以没有类似于C语言的非常复杂的类型声明/内存管理/文件处理等,没有C强大,但C语言和Smart编程语言也有一些相似处,并且在Smart擅长的逻辑控制上也不必处处和C语言保持一致:一般来讲Smart的指针基本可以看成是void*型的"万能指针":void*指针是一个可以指向任何数据类型的指针,但是我们暂时先不明确的声明定义它指向的是什么类型;在解引用取其值的时候,先进行类型转化到我们想要的类型!这也类似于C++的泛型模板,它也可能对应着一些环境中的不定长结构体数组/不定长UDT数组/不定长的引用,C#的泛型集合里氏转换......;而且Smart几乎每一条指令基本都带有将void*型的指针指向的内容进行强制类型转换的作用:就是这些指令在解引用取内容时以指令操作数类型来确定void*型的指针指向的数据类型:比如我用到浮点加法:就把取到的指针内容强制的看成是浮点数,至于正不正确取决于用这条指令的人.
把 "动作数据化"是很多编程语言加强的方向之一:它要求把函数(方法)这些有"动作的过程"完全看作成数据,而中级C语言的函数指针(类似委托)初步具备"动作数据化"的可能,因此它也是很重要的一个技巧.
以上参考<C程序设计语言><C和指针><C缺陷与陷阱><C专家编程>
按照Smart官方手册间接寻址的介绍以及大部分指令操作数支持的类型来看,帖中V1.6版本例程里面的内容也可以看出类似于void*指针的用法以及类似于函数指针的用法:子程序Smart_MP用"函数指针"来组织内部数据和动作,且指针偏移到位解引用取内容后强制性的按照所用指令的数据类型来运算;子程序Ch_Write也类似于把"函数指针"看作参数直接调用.当然这些都可以用非定长的数组/引用等结构来变通实现.
"函数指针"类似于"快递员"在数据间,进程间,线程间......跑腿传参的,减少了成员直接耦合,让成员间接耦合;且不丧失成员之间的流动性(交流接触和动态变化).
类比"隔离有效的减少了部分感染,但隔离的前提是必须要有效的解决两个问题:(1)"物理实体传递者"-快递员跑腿传递物理实体;(2)"虚拟信息传递者"-以太网传递非实体性的信息;因此规范的声明使用"函数指针"让它成为合格的"快递员",往往是编程中要优先封装解决好的问题.
多用L区容器空间则有可能减少了指针移来移去跑腿的时间;
少用L区容器空间则要函数指针反复移来移去跑腿满足传参需求,有可能增加了跑腿的时间;
释放L并非目的,关键是解除一部分数据的直接耦合;变成利用”函数指针”的间接耦合(起到数据隔离的作用)
间接耦合的好处是显而易见的类比于硬件:多变比的隔离变压器的作用,既减少了部分干扰,又使对象运行在不同的电压等级,并形成自己独立的电压参考点,到达现场后只要分配一片新的场地,大概合规的输入电压就可以放心的运行起来了.
一个项目需求/实体等一旦确定下来,则整体的"耦合量"基本无法降低到那里去,因此整体的低耦合是不现实的,局部的低耦合是可能的,但做好间接("简洁")耦合的分类规划,可能比局部分散降低耦合还要关键.
函数指针的好处还体现在:
01:我暂时忙于主任务(一个函数),分不开身,委托一部分任务给你,请快速帮我完成(另一个函数);
02:给你一个声明定义为私有或公共的地址,并限定传参类型;返回值类型,属性,修饰符;请按我的格式来;
03:请在某个条件满足时,某个时序下严格的按照条件和时序完成我的委托任务(函数指针初步具备委托类似最重要的延迟执行,正逆双向传递,隔离数据干扰,虚拟接口等特性),不得有误;
04:如果给你或让你返回void*型指针,说明我暂时不想让你将参数"写死",我可能用不同数据类型,不同数据长度,所以先请你暂时按我的void*型规则来运行,等我具体引用到你或实例化时再给你具体参数类型;
05:时间换空间,空间换时间在编程时处处存在,比如(1)利用历史数据区和现实数据区的动态比较:即空间换时间来实现"变化才写",进而减少部分时间-空间换时间;(2)将函数指针指向的GET/SET型公共数据和私有数据复制拷贝到L容器,空间换时间进而减少函数指针移来移去的时间-空间换时间;(3)限制L区容器用户可用区为60个Byte(64字节其中4字节用户无法使用,类似传统64字节的Cache line),这有效地解决了必须要解决和面对的空间不足的问题,但会使得"函数指针"作为"快递员"移来移去,或者作为委托对象传来传去,牺牲部分必要的时间以达到程序紧凑和节省空间的目的,这时候时间消耗要做出必要的妥协-时间换空间;(4)再大的内存都要省着用(假设32G内存),比如复制一个文件流,必须限定容器的大小,以及每次操作的字节数组的大小-时间换空间.所以编程的时候对于时间和空间必须平衡/辩证的考量,以无限的去趋于最优,但可能永远达不到;
06:"函数指针"把字段+属性+构造+析构+方法....这些集总性的看成一个班级来管理,它把一"类"事物都纳入自己的声明管理范围内;
07:指针除了本身是个唯一占据物理实体内存的二进制整数外(方便硬件总线寻址,其不一定是一片连续的物理内存,需要软件编译器把它整理的像整数一样连续),还带有类型信息(大概指的是长度信息,长度信息最重要,还有属性,适用的方法等);类型信息的声明,识别,适用的方法的不断丰富体现了编译器越来越智能;Smart的编译器目前应该无法识别类型信息,可以像C一样美其名曰是信任"程序员",它也确实做到了信任"程序员".并且类型信息越复杂,占用的资源也越多,越难满足实时性,适应的场景也越复杂.
08:工业传感器,电机,阀门,按钮等这些结构不太复杂,数据量不大,但需要实时的场景不一定迫切的需要非常复杂的类型声明和解读.但工业场景随着视觉,雷达,AI,光学器件等复杂硬件的加入,有越来越需要复杂类型声明及解读的趋势.
09:双字指针和一个default缺省型的char型指针显然是有区别的,而char型指针无疑是最基本的,把char指针封装一定的属性和方法以及反射等机制后可以类似于object类型.
10:功能引脚内部即使是指针类型,一般也应该是可以填入一个VDxx或者AC1,2,3的,但这个VDxx或AC1,2,3在这个引脚被扫到引用的时候应时时刻刻保持一个合法的指针值.
这些呆板套路本身可以被熟练者直接忽略,不必考虑,但当你暂时没有太好的方法时,也可以简单的/批判性的/选择性的/套用.
但是在Smart捉襟见肘的V区上是否要这样"折腾",这是要权衡后再决定的,毕竟Smart可编程逻辑控制器可能最"适合"做逻辑控制,可能最"适合"用全局变量编程.
S7-300/400的pointer,any指针分别进化成6字节/10字节的指针,很好的诠释了编译器对指针以及引用类型声明的逐渐复杂化,智能化;比4字节的S7-200"野蛮"指针要智能很多;
个人感觉"DB"的设计思路只是栈优化,堆未完全优化的过渡产物;
类比pointer/any,6字节/10字节指针对"野蛮"的,无组织,无纪律的4字节原始指针的优化;"DB"对"野蛮"的无组织,无纪律的"V"区优化,更进一步是TIA-Vxx
当然任何事物都是昨天的发展,今天的存在,明天的过渡.
但是无论如何优化:子类还是继承父类的一点点特性的,想找到所有事物的"父类",只能逆着方向往源头找,即使找到了源头,彼此之间也只是存在极少的共同语言/方法/修饰符/属性/特征;也没法交流,相顾无言,唯有类(泪)千行......
但是如果只在语法糖层面:你想妥协一部分时间,空间上达到一部分通用性,你的变量只能是源头类型;你想挽回一部分时间,空间上达到专用性,你只能用更智能的进化类型;但是如果你想在整个层面都有提升,只能抛弃语法糖,重构整个软硬件最底层架构.....
感觉无为而治即是大治.
有时候无组织,无纪律的,野蛮自由的4字节指针以及一大片荒芜的V区何尝不是你向往的画笔和画布;又有时候有组织,有纪律的,智能的引用类型以及合理的数据结构何尝不是你快速准确的实现梦想的博途......
仅就例程:
(01)为优化掉站时间,恢复时间应做成变量,根据算法"实时性","概率性"的变化,并和实际掉站数呈大概反比关系或S指数关系,而这个算法做好了可能也要费点时间.
(02)某个从站的任意一个任务,比如读任务主动检测到掉站,可能也要将这个站号的所有写任务的"#从变才写"都置位,因为"#从变才写==1不仅仅只是代表是数据变化才去写,还可以主动的让写任务有了一次可以写的机会,即使该写任务的现实数据没变"这样可能比较合适,不这样应该也是没问题.
(03)例程中"函数指针"经常加减某个固定的数字,移来移去,这看似"不规范",但可能是时序较优;规范的方法是把不同数据结构封装提取出来,做成带有长度信息的引用类型,这样函数指针就可以不需要移来移去,而只需要赋值即可(即使需要加减运算也应该加上一个长度信息变量),但这样的话,除了规范外,应该还会增加时间和空间消耗.仅仅就这个(03)点可以看出"标准"和"非标"都是"双刃剑".
(04)跳站和恢复基本是一回事:就是主动检测到掉站的那个任务,主动通知所有和自己相同站号的站点:”你们被跳站了,原因是XXX,你们可以在XXX时刻尝试恢复通讯,在这期间,你们有权保持沉默,并且最好保持沉默,但你们说的每一句话都会被跳过忽略以及作为"逻辑程序"出问题的呈堂证供而记录下来”,这样即使有N个相同站号的任务掉站,也只有一个任务在尝试恢复通讯,节省时序和调度.
(05)通讯程序(非逻辑程序)不应干涉逻辑程序写过来的数据.无论逻辑程序写过来的数据有多么”不安全”,通讯程序必须没有任何折扣100%的执行,这样即使真的因为例程出问题导致事故倒查,也不会是封装好的通讯程序的问题,而是倒查到逻辑程序里面去.
(06)为实现'从变才写",数据组织方面:历史数据紧跟着现实数据存储---因为历史数据就像紧紧的跟着现实数据的影子一样,如影随形,密不可分,而且可变的长度,数据格式都是近似的.
(07)子程序的入口只是一个可变的接口指针:它本身的二进制数值存储在LD0里面,而它指向内容的第一个字节和子程序LB4开始的字节一一对应。子程序里面的L声明内容仅仅是实现函数指针指向堆区的临时缓存. 在Smart中VB0的地址&VB0==16#8000000==134217728,所以其余所有地址的偏移值都可以以此做依据,比如引用地址指针类型需要在数据块里填写&VB6000---则可以在数据块里填写6000,而在程序中处理时&VB0+6000就可以寻址到对应的引用地址.
(08)如果有N个相同自由口协议的硬件设备,当针对这个通用的自由口协议封装好一个“自由口函数”,可以根据思路二把这个“自由口函数”再次组织封装成对这N个相同自由口协议的通讯.
(09)函数指针本身简单体现了虚拟接口,隔离干扰,正逆传递,延迟满足......后续的思路可能涉及函数指针+自动分配内存,但在非系统级别的调度且内存量级低于GB级别时,自动分配内存在时序和空间上并不一定优于手动分配内存,更何况Smart的可用区域是KB级别.
(10)如下动图:给出了所有从站都没电气连接(RS-485口拔掉),都掉站时,主站调度程序尝试恢复通讯的运行情况.
![](/club/bbs/upload/image/20230219/6381243680646435677137898.GIF)
附:
微软推荐的一本编程书前言中写道:
“正因为有了委托......这几乎完全消除了写轮询例程的必要.”
感觉委托的时序优化以及内存占用并不一定比原始的函数指针/模板灵活优越。虽然是“类型安全的、可靠的”;虽然宣称CLR底层是超越C的汇编级优化。汇编级优化可能就是类似STL的基址偏移的编程方法。
《C和指针》关于指针和整数转换的描述:“K&R C 当混用指针和整型值时,旧式C编译器并不会发出抱怨。但是,我们现在对这方面的知识知道的更透彻一些了。把整型值转换为指针或把指针转换成整型值是极为罕见的,通常这类转换属于无意识的错误。”