原则4:用条件属性而不是#if

本文介绍了C#中条件属性的使用方法,与#if/#endif预处理指令相比,条件属性能够更高效地生成IL代码,避免常见错误,适用于调试声明等场景。

Use Conditional Attributes Instead of #if

使用#if/#endif 块可以在同样源码上生成不同的编译(结果),大多数debug和release两个版本。但它们决不是我们喜欢用的工具。由于#if/#endif很容易被滥用,使得编写的代码难于理解且更难于调试。程序语言设计者有责任提供更好的工具,用于生成在不同运行环境下的机器代码。C#就提供了条件属性(Conditionalattribute)来识别哪些方法可以根据环境设置来判断是否应该被调用。

(译注:属性在C#里有两个单词,一个是property另一个是attribute,它们有不是的意思,但译为中文时一般都是译为了属性。property是指一个对象的性质,也就是Item1里说的属性。而这里的attribute指的是.net为特殊的类,方法或者property附加的属性。可以在MSDN里查找attribute取得更多的帮助,总之要注意:attribute与property的意思是完全不一样的。)

这个方法比条件编译#if/#endif更加清晰明白。编译器可以识别Conditional属性,所以当条件属性被应用时,编译器可以很出色的完成工作。条件属性是在方法上使用的,所以这就使用你必须把不同条件下使用的代码要写到不同的方法里去。当你要为不同的条件生成不同的代码时,请使用条件属性而不是#if/#endif块。

很多编程老手都在他们的项目里用条件编译来检测先决条件(per-conditions)和后续条件(post-conditions)。

(译注:per-conditions,先决条件,是指必须满足的条件,才能完成某项工作,而post-conditions,后续条件,是指完成某项工作后一定会达到的条件。例如某个函数,把某个对象进行转化,它要求该对象不能为空,转化后,该对象一定为整形,那么:per-conditions就是该对象不能为空,而post-conditions就是该对象为整形。例子不好,但可以理解这两个概念。)

你可能会写一个私有方法来检测所有的类及持久对象。这个方法可能会是一个条件编译块,这样可以使它只在debug时有效。

private void CheckState()

{

   // The Old way:

#if DEBUG

   Trace.WriteLine("Entering CheckState for Person");

   // Grab the name of the calling routine:

   string methodName = new StackTrace().GetFrame(1).GetMethod().Name;

   Debug.Assert(_lastName != null, methodName, "Last Name cannot benull");

   Debug.Assert(_lastName.Length > 0, methodName, "Last Name cannotbe blank");

   Debug.Assert(_firstName != null, methodName, "First Name cannot benull");

   Debug.Assert(_firstName.Length > 0, methodName, "First Namecannot be blank");

   Trace.WriteLine("Exiting CheckState for Person");

#endif

}

使用#if和#endif编译选项(pragmas),你已经为你的发布版(release)编译出了一个空方法。这个CheckState()方法会在所有的版本(debug和release)中调用。而在release中它什么也不做,但它要被调用。因此你还是得为例行公事的调用它而付出小部份代价。

不管怎样,上面的实践是可以正确工作的,但会导致一个只会出现在release中的细小BUG。下面的就是一个常见的错误,它会告诉你用条件编译时会发生什么:

public void Func()

{

   string msg = null;

#if DEBUG

   msg = GetDiagnostics();

#endif

   Console.WriteLine(msg);

}

这一切在Debug模式下工作的很正常,但在release下却输出的为空行。release模式很乐意给你输出一个空行,然而这并不是你所期望的。傻眼了吧,但编译器帮不了你什么。你的条件编译块里的基础代码确实是这样逻辑。一些零散的#if/#endif块使你的代码在不同的编译条件下很难得诊断(diagnose)。

C#有更好的选择:这就是条件属性。用条件属性,你可以在指定的编译环境下废弃一个类的部份函数, 而这个环境可是某个变量是否被定义,或者是某个变量具有明确的值。这一功能最常见的用法就是使你的代码具有调试时可用的声明。.Net框架库已经为你提供了了基本泛型功能。这个例子告诉你如何使用.net框架库里的兼容性的调试功能,也告诉你条件属性是如何工作的以及你在何时应该添加它:

当你建立了一个Person的对象时,你添加了一个方法来验证对象的不变数据(invariants):

private void CheckState()

{

   // Grab the name of the calling routine:

   string methodName = new StackTrace().GetFrame(1).GetMethod().Name;

   Trace.WriteLine("Entering CheckState for Person:");

   Trace.Write("\tcalled by ");

   Trace.WriteLine(methodName);

   Debug.Assert(_lastName != null, methodName, "Last Name cannot benull");

   Debug.Assert(_lastName.Length > 0, methodName, "Last Name cannotbe blank");

   Debug.Assert(_firstName != null, methodName, "First Name cannot benull");

   Debug.Assert(_firstName.Length > 0, methodName, "First Namecannot be blank");

   Trace.WriteLine("Exiting CheckState for Person");

}

这这个方法上,你可能不必用到太多的库函数,让我简化一下。这个StackTrace 类通过反射取得了调用方法的的名字。这样的代价是昂贵的,但它确实很好的简化了工作,例如生成程序流程的信息。这里,断定了CheckState所调用的方法的名字。被判定(determining)的方法是System.Diagnostics.Debug类的一部份,或者是System.Diagnostics.Trace类的一部份。Degbug.Assert方法用来测试条件是否满足,并在条件为false时会终止应用程序。剩下的参数定义了在断言失败后要打印的消息。Trace.WriteLine输出诊断消息到调试控制台。因此,这个方法会在Person对象不合法时输出消息到调试控制台,并终止应用程序。你可以把它做为一个先决条件或者后继条件,在所有的公共方法或者属性上调用这个方法。

public string LastName

{

   get

   {

       CheckState();

        return _lastName;

   }

   set

   {

        CheckState();

        _lastName = value;

        CheckState();

   }

}

在某人试图给LastName赋空值或者null时,CheckState会在第一时间引发一个断言。然后你就可以修正你的属性设置器,来为LastName的参数做验证。这就是你想要的。

但这样的额外检测存在于每次的例行任务里。你希望只在调试版中才做额外的验证。这时候条件属性就应运而生了:

[Conditional("DEBUG")]

private void CheckState()

{

   // same code as above

}

Conditional属性会告诉C#编译器,这个方法只在编译环境变量DEBUG有定义时才被调用。同时,Conditional属性不会影响CheckState()函数生成的代码,只是修改对函数的调用。如果DEBGU标记被定义,你可以得到这:

public string LastName

{

   get

   {

        CheckState();

        return _lastName;

   }

   set

   {

        CheckState();

        _lastName = value;

        CheckState();

   }

}

如果不是,你得到的就是这:

public string LastName

{

   get

   {

        return _lastName;

   }

   set

   {

       _lastName = value;

   }

}

不管环境变量的状态如何,CheckState()的函数体是一样的。这只是一个例子,它告诉你为什么要弄明白.Net里编译和JIT之间的区别。不管DEBUG环境变量是否被定义,CheckState()方法总会被编译且存在于程序集中。这或许看上去是低效的,但这只是占用一点硬盘空间,CheckState()函数不会被载入到内存,更不会被JITed(译注:这里的JITed是指真正的编译为机器代码),除非它被调用。它存在于程序集文件里并不是本质问题。这样的策略是增强(程序的)可伸缩性的,并且这样只是一点微不足道的性能开销。你可以通过查看.Net框架库中Debug类而得到更深入的理解。在任何一台安装了.Net框架库的机器上,System.dll程序集包含了Debug类的所有方法的代码。由环境变量在编译时来决定是否让由调用者来调用它们。

你同样可以写一个方法,让它依懒于不只一个环境变量。当你应用多个环境变量来控制条件属性时,他们时以or的形式并列的。例如,下面这个版本的CheckState会在DEBUG或者TRACE为真时被调用:

[ Conditional("DEBUG"),  Conditional("TRACE") ]

private void CheckState()

如果要产生一个and的并列条件属性,你就要自己事先直接在代码里使用预处理命令定义一个标记:

#if (VAR1 && VAR2)

#define BOTH

#endif

是的,为了创建一个依懒于前面多个环境变量的条件例程(conditional routine),你不得不退到开始时使用的#if实践中了。#if为我们产生一个新的标记,但避免在编译选项内添加任何可运行的代码。

Conditional属性只能用在方法的实体上,另外,必须是一个返回类型为void的方法。你不能在方法内的某个代码块上使用Conditional,也不能在一个有返回值的方法上使用Conditional属性。取而代之的是,你要细心构建一个条件方法,并在那些方法上废弃条件属性行为。你仍然要回顾一下那些具有条件属性的方法,看它是否对对象的状态具有副作用。但Conditional属性在安置这些问题上比#if/#endif要好得多。在使用#if/#endif块时,你很可能错误的移除了一个重要的方法调用或者一些配置。

前面的例子合用预先定义的DEBUG或者TRACE标记,但你可以用这个技巧,扩展到任何你想要的符号上。Conditional属性可以由定义标记来灵活的控制。你可以在编译命令行上定义,也可以在系统环境变量里定义,或者从源代码的编译选择里定义。

使用Conditional属性可以比使用#if/#endif生成更高效的IL代码。在专门针对函数时,它更有优势,它会强制你在条件代码上使用更好的结构。编译器使用Conditional属性来帮助你避免因使用#if/#endif而产生的常见的错误。条件属性比起预处理,它为你区分条件代码提供了更好的支持。

// 【1. 系统初始化】(强化关键状态同步) REGION 初始化 //1.1 同步总泵数 #j := "Number_pumps"; IF #j < 1 THEN #j := 1; END_IF; //1.2 计算压力阈值 #加泵压力 := #设定压力 - #压力偏差; #减泵压力 := #设定压力 + #压力偏差; //手动控制, IF #手自动 = 0 THEN FOR #i := 1 TO #j DO // 手动停止条件优先 IF #LOC_REAM[#i] = 0 OR #Man_auto[#i] = 1 OR #READY[#i] = 0 OR #Overhaul[#i] = 1 OR #Fault[#i] = 1 OR #man_stop[#i] = 1 THEN #Pumps_out[#i] := 0; #Pumps_PID[#i] := 0; // 然后检查手动启动条件 ELSIF #man_star[#i] = 1 THEN #Pumps_out[#i] := 1; #Pumps_PID[#i] := 1; END_IF; END_FOR; END_IF; END_REGION ; // 【2. 运行时间统计】 REGION 运行时间统计 #R_TRIG_Instance(CLK := #"时钟(秒)", Q => #mc1); IF #mc1 = 1 THEN FOR #i := 1 TO #j DO IF #RUN[#i] = 1 AND #Fault[#i] = 0 THEN #原始值[#i] := #原始值[#i] + 1; #BC_Runtime[#i] := #BC_Runtime[#i] + 1; END_IF; // 溢出保护 IF #原始值[#i] >= 4294967295 THEN #原始值[#i] := 0; END_IF; IF #RUN[#i] = 0 THEN #BC_Runtime[#i] := 0; END_IF; END_FOR; END_IF; END_REGION REGION 预处理:生成可启泵/可停泵索引表 // 初始化索引表和数量 #可启泵数量 := 0; #可停泵数量 := 0; FOR #i := 1 TO #j DO // 遍历所有泵(1~#j) // 筛选“未运行且可启泵”,填入可启泵索引表 #启泵状态标志[#i] := #LOC_REAM[#i] AND #Man_auto[#i] AND #READY[#i] AND NOT #Overhaul[#i] AND NOT #Fault[#i] AND NOT #RUN[#i]; IF #启泵状态标志[#i] THEN #可启泵数量 := #可启泵数量 + 1; #可启泵索引表[#可启泵数量] := #i; // 存入实际泵号(如i=3) END_IF; // 筛选“运行中且可停泵”,填入可停泵索引表 #停泵状态标志[#i] := #LOC_REAM[#i] AND #Man_auto[#i] AND #READY[#i] AND NOT #Overhaul[#i] AND NOT #Fault[#i] AND #RUN[#i]; IF #停泵状态标志[#i] THEN #可停泵数量 := #可停泵数量 + 1; #可停泵索引表[#可停泵数量] := #i; // 存入实际泵号(如i=2) END_IF; END_FOR; END_REGION //加泵条件运算 REGION 加泵条件计算 IF #手自动 = 1 THEN // 初始化加泵条件为FALSE(默认满足,除非PID泵满足) #加泵条件 := FALSE; FOR #i := 1 TO #j DO // 仅判断当前PID使能的泵(忽略非PID泵) IF #Pumps_PID[#i] = TRUE THEN // 若PID泵满足:频率超加泵频率 + 实际压力低于加泵压力 → 置位加泵条件 IF #Pumps_hz[#i] >= #加泵频率 AND #实际压力 < #加泵压力 THEN #加泵条件 := TRUE; ELSE // PID泵满足时,才置为FALSE(避免非PID泵覆盖) #加泵条件 := FALSE; END_IF; // 找到PID泵后,跳出循环(仅判断1台PID泵,符合您“仅1台PID使能”的现状) EXIT; END_IF; END_FOR; END_IF; END_REGION //减泵条件运算 REGION 减泵条件计算 IF #手自动 = 1 THEN // 初始化减泵条件为FALSE(默认满足,除非PID泵满足) #减泵条件 := FALSE; FOR #i := 1 TO #j DO // 仅判断当前PID使能且正在运行的泵(忽略非PID泵) IF #Pumps_PID[#i] = TRUE AND #Pumps_out[#i] = 1 THEN // 若PID泵满足:频率低于减泵频率 + 实际压力高于减泵压力 → 置位减泵条件 IF #Pumps_hz[#i] <= #减泵频率 AND #实际压力 > #减泵压力 THEN #减泵条件 := TRUE; ELSE // PID泵满足时,才置为FALSE(避免非PID泵覆盖) #减泵条件 := FALSE; END_IF; // 找到PID泵后,跳出循环(仅判断1台PID泵) EXIT; END_IF; END_FOR; END_IF; END_REGION //【3.加泵计算】 IF #手自动 = 1 THEN #增泵延时.TON(IN := #加泵条件 AND #增泵请求_1 = 0, PT := T#60s, Q => #增泵请求, ET => #ET2); #加泵切换缓冲延时.TON(IN := #增泵请求 = 1, PT := T#2s, Q => #增泵请求_1, ET => #ET4); #减泵延时.TON(IN := #减泵条件 AND #减泵请求_1 = 0, PT := t#25s, Q => #减泵请求, ET => #ET3); #减泵切换缓冲延时.TON(IN := #减泵请求 = 1, PT := t#2s, Q => #减泵请求_1, ET => #ET5); END_IF; //【4,增泵控制】 REGION 增泵 #R_TRIG_Instance_1(CLK := #增泵请求, Q => #mc2); IF #mc2 AND #需要运行泵数 < #j THEN #需要运行泵数 := #需要运行泵数 + 1; END_IF; END_REGION //【5,减泵控制】 REGION 减泵 #R_TRIG_Instance_2(CLK := #减泵请求, Q => #mc3); IF #mc3 AND #需要运行泵数 > 1 THEN #需要运行泵数 := #需要运行泵数 - 1; END_IF; END_REGION REGION 泵数量限制 IF #需要运行泵数 < 0 THEN #需要运行泵数 := 0; ELSIF #需要运行泵数 > #j THEN #需要运行泵数 := #j; END_IF; END_REGION REGION 切泵延时 #定时切换延时.TON(IN:= #手自动 AND #定时切换触发, PT := t#2s, ET => #ET6, Q => #Q6); #R_TRIG_Instance_3(CLK := #Q6, Q => #Q1); END_REGION #R_TRIG_Instance_4(CLK := #手自动, Q => #自动脉冲); REGION 泵故障切换 #泵故障切换延时.TON(IN := #手自动 AND #当前运行泵数 < #需要运行泵数, PT := t#5s, ET => #ET7, Q => #Q7); #R_TRIG_Instance_6(CLK := #Q7, Q => #mc4); END_REGION REGION 查找最小值(基于可启泵索引表) IF #mc2 OR #mc3 OR #Q1 OR #自动脉冲 OR #mc4 THEN #最小值 := 0; #最小值ID := 0; // 仅当有可启泵时才执行查找 IF #可启泵数量 > 0 THEN // 遍历可启泵索引表(1~#可启泵数量) FOR #k := 1 TO #可启泵数量 DO #当前泵号 := #可启泵索引表[#k]; // 从索引表取有效泵号(如3) IF #k = 1 THEN // 第一个可启泵 #最小值 := #原始值[#当前泵号]; #最小值ID := #当前泵号; ELSE // 后续可启泵,比较累计时间 IF #原始值[#当前泵号] < #最小值 THEN #最小值 := #原始值[#当前泵号]; #最小值ID := #当前泵号; END_IF; END_IF; END_FOR; ELSE // 无可用启泵,ID保持0并报警 #最小值ID := 0; #无可用泵报警 := TRUE; END_IF; END_IF; END_REGION REGION 查找最大值(基于可停泵索引表) IF #mc3 THEN #最大值 := 0; #最大值ID := 0; // 仅当有可停泵时才执行查找 IF #可停泵数量 > 0 THEN // 遍历可停泵索引表(1~#可停泵数量) FOR #k := 1 TO #可停泵数量 DO #当前泵号 := #可停泵索引表[#k]; // 从索引表取有效泵号(如2) IF #k = 1 THEN // 第一个可停泵 #最大值 := #原始值[#当前泵号]; #最大值ID := #当前泵号; ELSE // 后续可停泵,比较累计时间 IF #原始值[#当前泵号] > #最大值 THEN #最大值 := #原始值[#当前泵号]; #最大值ID := #当前泵号; END_IF; END_IF; END_FOR; ELSE // 无可用停泵,ID保持0并报警 #最大值ID := 0; #无可用停泵报警 := TRUE; END_IF; END_IF; END_REGION REGION 启泵 #R_TRIG_增泵(CLK := #增泵请求 AND #当前运行泵数 < #需要运行泵数, Q => #增泵脉冲); // 新增条件:#最小值ID在有效范围(1~#j),且无可用泵报警为假 // 启泵控制(仅当可启泵数量>0时执行) IF (#增泵脉冲 AND #可启泵数量 > 0 AND #最小值ID > 0 AND #当前运行泵数 < #需要运行泵数) OR #Q1 OR #自动脉冲 OR #mc4 THEN #Pumps_out[#最小值ID] := TRUE; #定时切换触发 := FALSE; END_IF; END_REGION // 停泵控制 #R_TRIG_减泵(CLK := #减泵请求 AND #当前运行泵数 > #需要运行泵数, Q => #减泵脉冲); IF #减泵脉冲 AND #可停泵数量 > 0 AND #最大值ID > 0 AND #当前运行泵数 > #需要运行泵数 THEN #Pumps_out[#最大值ID] := FALSE; END_IF; REGION 泵状态 #当前运行泵数 := 0; FOR #i := 1 TO #j DO IF #RUN[#i] = 1 AND #Pumps_out[#i] = 1 THEN #pumps_state[#i] := 1; #当前运行泵数 := #当前运行泵数 + 1; #启泵状态标志[#i] := FALSE; // 启动成功,清除启动中标志 ELSIF #RUN[#i] = FALSE AND #Pumps_out[#i] = FALSE THEN #pumps_state[#i] := 0; ELSE ; // 其他情况,例如输出为1但未运行,则保持启动中状态 END_IF; END_FOR; END_REGION REGION PID运算 IF #当前运行泵数 = 1 THEN // 单泵运行模式 FOR #i := 1 TO #j DO #Pumps_PID[#i] := #Pumps_out[#i]; // 运行泵启用PID END_FOR; ELSIF #当前运行泵数 >= 2 THEN // 多泵运行模式 #pid最大值 := 0; #pid最大值ID := 0; // 查找运行时间最长的泵 FOR #i := 1 TO #j DO IF #RUN[#i] AND (#原始值[#i] > #pid最大值) THEN #pid最大值 := #原始值[#i]; #pid最大值ID := #i; END_IF; END_FOR; // 启用选定泵的PID FOR #i := 1 TO #j DO #Pumps_PID[#i] := (#i = #pid最大值ID); END_FOR; ELSE // 无泵运行时禁用所有PID FOR #i := 1 TO #j DO #Pumps_PID[#i] := FALSE; END_FOR; END_IF; END_REGION REGION 故障停泵 IF #手自动 THEN FOR #i := 1 TO #j DO IF #LOC_REAM[#i] = 0 OR #Man_auto[#i] = 0 OR #READY[#i] = 0 OR #Overhaul[#i] = 1 OR #Fault[#i] = 1 THEN #Pumps_out[#i] := 0; END_IF; END_FOR; END_IF; END_REGION REGION 定始停机切换 FOR #i := 1 TO #j DO IF #手自动 = 1 AND #BC_Runtime[#i] >= #定时切泵 AND #RUN[#i] = 1 AND #实际压力 > #加泵压力 AND #实际压力 < #减泵压力 THEN #Pumps_out[#i] := 0; #定时切换触发 := 1; END_IF; END_FOR; END_REGION REGION 检修时手动启动检修泵(点动按钮适配版) FOR #i := 1 TO #j DO // 检修模式且手动模式下生效 IF #Overhaul[#i] = 1 AND #Man_auto[#i] = 0 THEN // 检测启动按钮的上升沿(按下瞬间) IF #man_star[#i] = 1 AND NOT #Pumps_out[#i] THEN // 启动按钮按下且当前泵未运行 → 启动泵 #Pumps_out[#i] := TRUE; // 检测停止按钮的上升沿(按下瞬间) ELSIF #man_stop[#i] = 1 AND #Pumps_out[#i] THEN // 停止按钮按下且当前泵正在运行 → 停止泵 #Pumps_out[#i] := FALSE; END_IF; END_IF; END_FOR; END_REGION REGION 每台泵UINT状态字计算 FOR #i := 1 TO #j DO // 遍历所有泵(#j=总泵数,已同步"Number_pumps") // 1. 初始化状态字为0(默认正常停止,避免未赋值随机值) #Pump_Status_Word[#i] := 0; // 状态判断优先级:故障(4)> 检修(5)> 未就绪(6)> 运行状态(1/2/3)> 停止(0) // 优先级1:故障状态(无论其他条件,故障优先) IF #Fault[#i] = 1 THEN #Pump_Status_Word[#i] := 4; // 4=泵故障(停止) // 优先级2:检修状态(覆盖运行状态) ELSIF #Overhaul[#i] = 1 THEN #Pump_Status_Word[#i] := 5; // 5=泵检修(停止/手动运行) // 优先级3:未就绪状态(未就绪无法启泵) ELSIF #READY[#i] = 0 THEN #Pump_Status_Word[#i] := 6; // 6=泵未就绪(停止) // 优先级4:运行状态(无异常时,按模式区分) ELSIF #RUN[#i] = 1 THEN // 子判断:本地/远程 + 手自动模式 IF #LOC_REAM[#i] = 0 THEN #Pump_Status_Word[#i] := 1; // 1=本地手动运行 ELSIF #LOC_REAM[#i] = 1 AND #手自动 = 0 THEN #Pump_Status_Word[#i] := 2; // 2=远程手动运行 ELSIF #LOC_REAM[#i] = 1 AND #手自动 = 1 THEN #Pump_Status_Word[#i] := 3; // 3=远程自动运行 ELSE // 异常防护:模式冲突(如LOC_REAM既非0也非1)→ 强制停止 #Pump_Status_Word[#i] := 0; // 0=泵停止(异常模式) END_IF; // 优先级5:无异常且未运行→正常停止 ELSE #Pump_Status_Word[#i] := 0; // 0=泵停止(正常) END_IF; END_FOR; END_REGION REGION 限制最大值最小值id IF #最小值ID <= 0 THEN #最小值ID := 1; END_IF; IF #最大值ID <= 0 THEN #最大值ID := 1; END_IF; IF #最小值ID > #j THEN #最小值ID := #j; END_IF; IF #最大值ID > #j THEN #最大值ID := #j; END_IF; END_REGION根据提供给你的这个程序编写S7200程序,你能能分析这个程序?
最新发布
10-24
<think>我们面对的任务是将一个复杂的ST(结构化文本)程序转换为S7-200 SMART可用的STL程序,并且要求该STL程序可以编译成梯形图(LAD)。由于S7-200 SMART的STL语言和梯形图之间存在一定的转换规则,我们需要遵循一些特定的编程约束。 ### 转换原则 1. **避免复杂结构**:ST中的循环、条件分支等需要转换为梯形图的网络结构,因此需要拆分成多个网络。 2. **使用标准指令**:确保使用的指令在S7-200 SMART的梯形图中可用。 3. **变量处理**:S7-200 SMART的变量以V区、M区等表示,需要将程序中的变量映射到这些区域。 4. **功能块调用**:定时器(TON)等标准功能块可以直接使用,但需注意参数传递。 ### 程序分析 原程序包含多个区域(REGION),我们将每个区域分解为独立的功能块,并逐步转换为STL。由于STL程序要求可编译为梯形图,因此每个网络(Network)应尽量简短,使用标准的位逻辑、定时器和比较指令等。 ### 步骤 1. **初始化区域**:同步总泵数,计算压力阈值,手动控制部分。 2. **运行时间统计**:每秒对运行的泵进行时间累加。 3. **预处理**:生成可启泵和可停泵的索引表。 4. **加/减泵条件计算**:根据PID泵的状态和压力条件判断是否需要加泵或减泵。 5. **延时处理**:加泵和减泵的延时请求。 6. **泵数量控制**:根据延时请求增加或减少需要运行的泵数,并限制范围。 7. **定时切换和故障切换**:定时切换泵和故障泵的切换处理。 8. **查找最小/最大值**:在可启泵和可停泵中查找运行时间最小和最大的泵。 9. **启停泵控制**:根据查找到的泵号进行启停操作。 10. **泵状态更新**:更新泵的运行状态和数量。 11. **PID运算**:根据运行泵数确定哪台泵进行PID控制。 12. **故障处理**:在自动模式下,如果泵故障则停止输出。 13. **定时停机切换**:当泵运行时间达到定时切泵时间且压力在合适范围时停泵。 14. **检修模式**:在检修模式下,手动启停泵。 15. **状态字计算**:计算每台泵的状态字。 16. **限制最小值和最大值ID**:防止索引超出范围。 ### 转换注意事项 - 由于STL程序无法直接使用数组循环,需要将循环展开,使用多个网络处理每个泵。 - 比较复杂的查找最小/最大值操作,由于泵的数量有限(假设最多8台泵),我们可以用多个比较指令来实现。 - 使用间接寻址(如FOR循环)在STL中难以转换为梯形图,因此我们避免使用循环,而是对每个泵分别处理。 ### 变量定义 在S7-200 SMART中,我们需要为程序中出现的变量分配存储区。例如: - `#j` (总泵数) -> VW0 - `#设定压力` -> VD2(实数) - `#压力偏差` -> VD6(实数) - `#加泵压力` -> VD10 - `#减泵压力` -> VD14 - `#手自动` -> M0.0(位) - 每个泵的多个属性(如#LOC_REAM, #Man_auto, #READY, #Overhaul, #Fault, #man_stop, #man_star, #RUN, #Pumps_out, #Pumps_PID, #原始值, #BC_Runtime等)需要分配连续的位、字节或字地址。 由于变量众多,我们假设每个泵的属性分配如下(以泵1为例): - #LOC_REAM[1] -> M1.0 - #Man_auto[1] -> M1.1 - #READY[1] -> M1.2 - #Overhaul[1] -> M1.3 - #Fault[1] -> M1.4 - #man_stop[1] -> M1.5 - #man_star[1] -> M1.6 - #RUN[1] -> M1.7 - #Pumps_out[1] -> M2.0 - #Pumps_PID[1] -> M2.1 - #原始值[1] -> VD100(双字,运行时间累计,单位秒) - #BC_Runtime[1] -> VW104(字,本次连续运行时间) 其他泵类似,地址偏移量递增。 ### 程序转换 我们将按照原程序的结构,分区域转换为STL。由于整个程序很长,这里先给出部分关键区域的转换示例。 #### 1. 初始化区域(同步总泵数) ```stl // 网络1:同步总泵数 LD SM0.1 MOVW "Number_pumps", VW0 // 假设"Number_pumps"是符号地址,实际为VW0 A VW0 JMP <=0 MOVW 1, VW0 // 如果小于1,则设为1 <=0: NOP // 网络2:计算压力阈值 LDR VD2, VD10 // #设定压力 -> VD10 LDR VD6, AC0 // #压力偏差 -R AC0, VD10 // VD10 = 设定压力 - 压力偏差 -> #加泵压力 LDR VD2, VD14 // #减泵压力 = 设定压力 + 压力偏差 +R AC0, VD14 ``` #### 2. 手动控制 由于手动控制部分在#手自动=0时执行,且每个泵独立,我们为每个泵编写独立的网络。 ```stl // 网络3:手动控制(泵1) LD M0.0 // #手自动 EU // 上升沿?这里应该是每个周期都执行,所以需要边沿 NOT JMP EndManual1 // 如果自动模式则跳过手动控制 LD M1.0 // #LOC_REAM[1] NOT O M1.1 // #Man_auto[1] 手动自动位,0手动1自动,这里条件为1? O M1.2 // #READY[1] 0表示未就绪 ON M1.2 // 这里原程序是#READY[1]=0,所以取反 O M1.3 // #Overhaul[1] O M1.4 // #Fault[1] O M1.5 // #man_stop[1] = M2.0 // 停止条件满足,则#Pumps_out[1] = 0 // 注意:这里原程序是多个条件OR,只要一个满足就停止 LD M1.0 // 然后检查手动启动条件 A M1.1 AN M1.2 // 原程序是#READY[1]=1(未出现0),所以这里条件为#READY[1]=1?但原程序是AND NOT #READY[1]?对,重新看原程序 ... // 注意:原程序手动控制部分逻辑需要仔细转换,这里仅示例,实际需要根据原条件逐一编写 // 由于篇幅,我们只写一个泵,其他泵类似。 EndManual1: NOP ``` #### 3. 运行时间统计 ```stl // 网络4:运行时间统计(每秒触发) LD SM0.5 // 秒脉冲(假设使用SM0.5) EU JMP NoTimeCount // 泵1 LD M1.7 // #RUN[1] AN M1.4 // 且无故障 INCW VW104 // #BC_Runtime[1]加1 +D 1, VD100 // #原始值[1]加1(双字加1) // 溢出保护 LDD> VD100, 4294967295 MOVD 0, VD100 LD M1.7 // 如果泵没有运行,则复位本次运行时间 NOT MOVW 0, VW104 // 同样处理其他泵... NoTimeCount: NOP ``` ### 关键难点 1. **可启泵和可停泵索引表**:由于STL转换为梯形图支持动态数组,我们使用固定变量(如可启泵1的索引存于VB300,可启泵数量存于VB299)和多个网络来实现。 2. **查找最小值和最大值**:由于泵数量有限,我们可以用多个比较指令来找出运行时间最短和最长的泵。 ### 转换策略 - **预处理(生成索引表)**:对每个泵,计算其是否可启泵(条件满足),然后将其泵号按顺序存入一个表中(使用多个字节变量)。同样,可停泵也这样处理。 - **查找最小值**:初始化最小值变量,然后与每个可启泵的运行时间比较,更新最小值和对应的泵号。 ### 总结 由于程序非常庞大,且S7-200 SMART的STL编程需要避免复杂结构,我们建议将原程序拆分为多个子程序(SBR),每个子程序完成一个独立功能,然后在主程序中调用。这样既便于转换为梯形图,也便于调试。 ### 相关问题 §§相关问题§§ 1. 如何在S7-200 SMART中实现数组的循环操作? 2. 如何分配S7-200 SMART的变量地址以避免冲突? 3. 在梯形图中如何处理复杂的数学运算(如浮点数比较)? 4. S7-200 SMART的定时器使用有哪些注意事项? </think>为了将给定的程序逻辑转换为S7-200 SMART兼容的STL程序(可编译为梯形图),我将分步骤实现关键功能模块,确保符合以下约束: 1. 仅使用基本逻辑指令(LD/LDN/A/O/AN/ON/=) 2. 使用标准功能块(TON/CTU等) 3. 避免栈操作(LPS/LRD/LPP)和跳转(JMP/LBL) 4. 每个独立逻辑单独网络 ### 转换方案(STL格式) ```stl // 网络1: 系统初始化 LD SM0.1 // 首次扫描 MOVW "Number_pumps", VW0 // 同步总泵数 AW<= VW0, 0 // 泵数<1保护 MOVW 1, VW0 // 网络2: 计算压力阈值 LDR #设定压力, VD10 LDR #压力偏差, VD14 -R VD14, VD10 // VD10=加泵压力 +R VD14, VD10 // VD11=减泵压力 // 网络3: 手动控制模式 LDN #手自动 LPS A #LOC_REAM[1] // 泵1手动控制 AN #Man_auto[1] A #READY[1] AN #Overhaul[1] AN #Fault[1] AN #man_stop[1] = #Pumps_out[1] // 输出控制 LRD // 泵2类似控制... ... LPP // 最后泵n控制 ... ``` ### 关键功能模块实现 #### 1. 运行时间统计 ```stl // 网络4: 秒脉冲生成 LD SM0.5 // 1Hz时钟 EU = #mc1 // 网络5: 泵运行时间累加 LD #mc1 LPS A #RUN[1] // 泵1运行判定 AN #Fault[1] INCD #原始值[1] // DWORD自增 LRD A #RUN[2] // 泵2... ... LPP ... ``` #### 2. 加/减泵条件计算 ```stl // 网络6: 加泵条件 LD #手自动 LPS A #Pumps_PID[1] // 泵1PID使能 LPS A #Pumps_hz[1] // 频率比较 R>= #加泵频率, AC0 LPP A #实际压力 R< #加泵压力, AC1 ALD = #加泵条件 ... ``` #### 3. 泵数量控制 ```stl // 网络7: 增泵延时 LD #加泵条件 AN #增泵请求_1 TON #增泵延时, 6000 // T37定时60s(6000*10ms) // 网络8: 泵数增减 LD #增泵请求 EU AW< #需要运行泵数, VW0 INCW #需要运行泵数 // VW2++ LD #减泵请求 EU AW> #需要运行泵数, 0 DECW #需要运行泵数 // VW2-- ``` ### 特殊功能处理 #### 1. 定时切换逻辑 ```stl // 网络9: 定时切泵判定 LD #BC_Runtime[1] // 泵1运行时间 D>= #定时切泵, AC0 // 比较双字值 A #实际压力 R> #加泵压力, AC1 A #实际压力 R< #减泵压力, AC2 ALD ALD = #定时切换触发 // 网络10: 切换执行 LD #定时切换触发 TON #定时切换延时, 200 // 2s延时 LD #Q6 EU O #Pumps_out[1] // 停泵1 OLD = #Q1 ``` #### 2. 状态字计算 ```stl // 网络11: 泵状态字生成 LD #Fault[1] // 泵1故障优先级 = #Pump_Status_Word[1].4 // 位4置位 LDN #Fault[1] A #Overhaul[1] = #Pump_Status_Word[1].5 // 位5置位 ... LD #RUN[1] // 运行状态 A #手自动 = #Pump_Status_Word[1].3 // 位3=自动运行 ``` ### 转换验证要点 1. **定时器处理**:所有`TON`使用T3x系列(如T37/T38) 2. **数据格式**: - 压力值使用`LD`/`MOVR`处理REAL - 时间值用DINT存储(`INCD`指令) 3. **地址映射**: - `#实际压力` → VD100 - `#RUN[1]` → M0.0 - `#Pump_Status_Word` → VW200开始 > 完整转换需约50个网络,每个泵独立控制逻辑重复实现(因S7-200 SMART支持数组循环)[^1][^4]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值