这是系列文章的第一篇,介绍如何用C语言为PLC编程。适合希望提升编程技能的中级程序员,重点关注机器控制与安全性。如图1所示的Arduino Opta将作为开发平台使用。购买信息及相关TechForum文章列表请参阅本页面。
主要目标是通过减少程序冗余来简化逻辑结构。此代码片段展示了一个示例。if语句会评估选择开关、红色按钮和绿色按钮的状态。根据状态结果,它将设置运行状态及红绿面板灯的颜色。该代码技术可描述为:遵循传统PLC程序扫描方式(在超级循环顶部读取输入输出),通过指针注入进行增强。
代码下载:DemoIOUtil.zip (2.0 KB)
if (localSelectorSwitch.value && localStartPB.value && !localStopPB.value) {
runState = true;
localPLRed.value = false;
localPLGreen.value = true;
}
技术提示 :关于嵌入式系统该用C还是C++的争论持续存在。Arduino代码库大部分采用C++编写。但这并非绝对共识,因为C语言的速度与低开销,与C++的额外保护、抽象性和优雅特性需要权衡。具体适用性取决于嵌入式项目的实现方式。
本文面向正从易用的Arduino转向嵌入式编程挑战的学员。无论优劣,文中采用了类C结构。建议您进一步将例程转换为C++,自行判断哪种方法更适合您的嵌入式项目。
图1 :安装在DigiKey训练器上的Arduino Opta图示。它支持IEC 61131-3标准的C或C++语言编程。
技术提示 :建议从IEC 61131-3梯形图语言开始学习PLC编程。您将发现PLC的精髓不仅在于编程,更在于其与物理世界的实时交互能力。梯形图能有效培养对PLC主要控制与接口条件开关特性的思维方式。
突破 Arduino 舒适区
图1所示的Opta是个被低估的竞争者。虽然Opta本身只有8个输入和4个继电器输出,但请注意它具有扩展性。例如可添加数字和模拟扩展模块。设备还配备Modbus通信端口和以太网端口,通过外接硬件可大幅扩展总输入输出数量。
技术提示 :切勿将Opta视为普通小型PLC或"智能继电器"。当我们利用Modbus和以太网连接时,搭载32位双ARM处理器(Cortex-M7和Cortex-M4)的多语言编程能力将得到充分释放。结合外部配套硬件使用,Arduino Opta为现代边缘计算提供了独特的社区支持解决方案。但首先需要建立编程结构来简化代码库。
本文撰写时已考虑到这种扩展需求。为Opta编程实现实时响应并非易事,尤其当涉及数十个输入输出连接时。当复杂度达到某个临界点时,抽象化和编程结构就成为了必需品。缺乏这些要素会导致代码难以管理且几乎无法排错。
接下来请做好准备,我们将深入软件开发领域,学习如何创建结构和抽象化方法。本文如同桥梁,连接着Arduino草图的平静水域与模块化编程的结构化世界,并展望安全关键系统中代码安全性、可移植性和可靠性的未来。
结构体是程序优化的关键组件
如我前文所述,结构体是程序组织架构的核心要素。前文中我们使用结构体整合了RC机械臂中各伺服电机的相关变量和常量。虽然整合过程可能需要适应,但最终效果证明其价值。最终我们能将伺服电机作为单一实体处理,这使得HomeAllServos()等函数的实现变得异常简单。
1) 结构体描述
现在我们可以将这个封装理念扩展到Opta的输入输出引脚,使用LocalInputPin和LocalOutputPin结构体。
typedef struct {
uint8_t pin; // Arduino physical pin number
const char *name; // Descriptive name (e.g., "Start Pushbutton")
bool inverted; // True for invert e.g., a normally closed pushbutton
bool value; // Current state (updated by LocalPinUtil.cpp)
} LocalInputPin;
typedef struct {
uint8_t pin; // Arduino pin number
const char *name; // Descriptive name (e.g., "Motor Enable")
bool value; // Current state
} LocalOutputPin;
2) 为每个输入输出实例化结构体
当我们在主.ino文件中为输入输出引脚实例化结构体时,魔法就发生了。这段代码显示所有I/O引脚相关变量和常量都被添加到结构体中。包括物理Arduino I/O引脚和描述I/O功能的人类可读字符串。还设有参数可反转输入引脚逻辑,自然适配停止按钮等常闭开关。输出引脚结构体与输入结构体类似。您可以选择添加额外代码来反转输出,以处理负逻辑情况。
LocalInputPin localSelectorSwitch = { A0, "Local Switch", 0, 0 };
LocalInputPin localStopPB = { A1, "Stop Switch", 1, 0 };
LocalInputPin localStartPB = { A2, "Start Switch", 0, 0 };
LocalInputPin localProxSensor = { A3, "Prox Sensor", 0, 0 };
LocalOutputPin localPLGreen = { 0, "Green Panel Lamp", 0 };
LocalOutputPin localPLRed = { 1, "Red Panel Lamp", 1 };
LocalOutputPin localCR1 = { 2, "Control Relay", 0 };
LocalOutputPin localFan = { 3, "Cooling Fan", 0 };
技术提示 :常闭停止按钮是防止断线的传统方法。一般而言,启动PLC的设备应使用常开开关。停止机器的设备应为常闭状态。
务必评估机器的安全性能。根据需要添加冗余电路,以保护机械设备,更重要的是保护操作人员和技术人员。
3) 构建指针数组
接下来,我们构建一个指针数组,其中包含一个用于输入引脚和一个用于输出引脚的数组。
LocalInputPin* inputPinList[] = {
&localSelectorSwitch,
&localStopPB,
&localStartPB,
&localProxSensor,
};
LocalOutputPin* outputPinList[] = {
&localPLGreen,
&localPLRed,
&localCR1,
&localFan,
};
4) 自动确定指针数组的大小
通过将结构体的总大小(字节)除以结构体中的元素数量,自动计算每个结构体中输入和输出引脚的数量。
static const uint8_t numInputPins = sizeof(inputPinList) / sizeof(inputPinList[0]);
static const uint8_t numOutputPins = sizeof(outputPinList) / sizeof(outputPinList[0]);
技术提示 :我们可以手动硬编码输入和输出指针的数量。但是,当输入和输出的数量发生变化时,这可能会导致错误。
5) 注册结构体数组并初始化 I/O
下一步是将结构体数组注册到LocalPinUtil.cpp中。这种“指针注入”是简化代码的关键步骤。它允许主.ino文件拥有原始的输入和输出结构体。然而,它将责任转移到LocalPinUtil.c以对这些结构体进行操作。例如,执行pinMode()、digitalWrite()和digitalRead()操作。
整个系统依赖于LocalPinUtil.c维护指向主.ino文件中结构体的静态指针。这些指针处理了困难的部分,而主.ino文件中的程序员可以愉快地访问输入和输出结构体的成员。
void setup() {
registerLocalInputPins(inputPinList, numInputPins);
registerLocalOutputPins(outputPinList, numOutputPins);
initLocalInOut();
}
6) 每次循环迭代调用更新函数
最后一步是简单地请求LocalPinUtil.cpp在每次循环迭代时更新I/O。这种方法类似于PLC的程序扫描。回想一下,PLC程序扫描由几个阶段组成:
-
内务处理活动
-
读取输入
-
执行用户程序
-
设置输出
用户程序从不直接操作I/O引脚。而是执行内存到内存的操作,直接修改关联结构体的value成员。
void loop() {
LocalInOutUpdate();
// My program
}
技术提示 :传统PLC程序扫描确实会引入延迟,因为输入输出不会立即更新。这种实时性能表现已被现场数百万台PLC广泛采用。
对于需要更快响应时间的应用,可采用专用硬件或支持中断的PLC。
7) 使用变量
完成基础工作后,现在可以在程序中使用这些变量。本示例中,我们根据运行状态和接近传感器的值控制冷却风扇。回到最初的结构体定义,可见每个输入输出结构体都有名为value的成员。我们使用点标记法访问目标value成员。
这是处理PLC程序逻辑近乎理想的方式。最大优势在于清晰性——digitalRead()和digitalWrite()函数已被归入utilities.c文件。另请注意,Arduino特有功能已移至LocalPinUtil.c。若决定将代码移植到其他环境,这可能成为优势。
if (runState && localProxSensor.value) { //Turn selector switch off and press the red button to clear fault state.
localFan.value = true;
} else {
localFan.value = false;
}
结语
该代码清单采用C语言编程风格,而非C++面向对象风格。指针注入过程相对简单。速度也很快,因为直接访问内存位置,而非使用缓冲区堆栈传递数值。
接下来何去何从?
很简单,全文都聚焦于本地I/O。我们需要讨论Modbus工具文件等本地I/O。若精心构建程序,应能无缝集成远程与本地I/O。
请分享您对这个主题的看法。我们很期待听到您使用这种指针注入技术的编程经验。或者,您有更高效的方法吗?
欢迎对C语言优于C++或反之的原因发表评论——需附带说明。
相关资料
请访问以下链接获取关联实用信息:
