有一些人虽然工作了很多年,但工作表现就像刚入行的新人。他们几乎不学习软件开发的基础知识 。除了最初几年有所成长,后期一直停滞不前,而且他们不明白为什么。 与此同时,我也曾与一些只有几年工作经验的开发人员共事,他们表现出惊人的增长潜力。他们工作态度端正,并且明白如何避免不称职的行为。 根据开发人员的某些习惯,可以非常明显地分辨出谁更专业,谁更业余。让我们深入剖析下业余程序开发人员的几种表现,每个程序开发人员都应该引以为戒,这些错误会阻碍我们的职业发展。 一次性提交大量代码 回忆下,你是否碰到过一次性提交大量代码的人,你都不想给他做代码评审。是的,不专业的开发人员就会这样做。他们会在一次代码评审请求中包含多个模块的修改,而且会催促你优先评审他们的代码。 是啊,能不急吗,排到后边,还需要解决代码冲突的问题。这个问题在很多高级开发工程师中也存在,他们在功能开发期间不做任何提交,只有在功能彻底完工后,才会提交所有修改,于是代码评审中的任何意见都会引起大量的修改。 当我碰到这种代码评审请求时,我首先做的是要求提交者按功能模块将其拆分成多个小的请求。 我只会对 issues(任务管理系统)中的第一个功能需求评审,然后将其转回提交者。如果我有时间,我会和提交者连线进行代码实时评审。 你能做什么? 进行小的代码提交。一个好的做法是:每个工作日都进行代码提交。 不要提交没有编译或者会导致构建失败的代码。 代码写的很烂 缺乏经验的开发人员写不出漂亮的代码,他们写出的代码会很混乱,而且分布在代码库的各个部分。 当你尝试阅读这类代码时,会感觉自己身处一座迷宫之中。你会逐渐忘记自己是从什么地方开始的,要寻找什么以及这段代码完成了什么功能。 有经验的开发人员知道代码如何设计。除非要开发的功能显而易见,首先需要在纸上写出你对需求的理解并画出流程图(简化版的规格需求说明书),在脑海里对这段代码进行一个完整的构思。除非你彻底弄清楚了如何修改,否则不要开始代码编写。 如果你不遵守以上的规则,当你回顾自己完成的代码时会非常痛苦。以后如果需要修正问题或者增加功能,也会变得非常棘手。 你能做什么? 编写代码之前,对你要实现的功能有个清晰的了解。为了清楚地理解需求,你需要尽量多问问题。 让你的代码简洁而优雅。其他团队成员可以读懂代码并理解它打算做什么。 同时开展多项工作 缺乏经验的开发人员不知道什么时候开始一项任务、如何推进、什么时候结束。他们试图并行处理多项任务。他们不知道如何将一项大任务分解为小的模块,从而减轻实现的难度。 当他们收到一项任务时,并不是第一时间和上级确认需求,而是立刻就开始编程,而且在做任务期间,也不会和上级就任务进度进行沟通。 只有当任务完成时,他们才会向你反馈。到那个时候,你只能祈祷他们完成的功能就是你想要的。 缺乏经验的开发人员的另一个表现是同时推进多项任务,他们会同时处理多项事情,如:实现多个没有太大联系的功能点、解决生产环境问题、协助其他同事工作等。 最终,从他们那里得不到有效的产出。虽然他们的态度和出发点是好的,但对整个团队造成的后果是灾难性的,浪费了很多的时间,导致团队得日夜赶工。 你能做什么? 专注完成小的任务。将收到的任务分解为小块,明确需求的优先级,一小块一小块地完成。 领取一项任务,完成后再开始新的任务。 性格傲慢 对于缺乏经验的开发人员,傲慢是非常致命的。傲慢会导致他们不能接受别人的批评和建议。当你对他们的代码或者陈述给出意见时,他们会认为你是在质疑他们的能力。 许多新人由于无知,都会表现出这种傲慢。刚走出校门的他们充满自信,并没有意识到他们在学校学到的东西离社会要求还有很大差距。这些人中的聪明者会很快调整自己,以归零的心态,努力学习并适应公司文化。 其实不只是新人——一些有几年工作经验的开发人员也会表现出这种傲慢,一部分原因是其满足于个人获得的专业成就,另一部分可能的原因是其缺乏和优秀的人共事的机会,有点坐井观天。 此外,傲慢的行为也从另一方面证明这样的开发人员确实缺乏经验。这样的行为会对他们的职业发展造成很多阻碍,因为没有人喜欢和一个傲慢的人共事。当成长变慢时,他们不会从自身找原因,而是更多的归罪于别人。 你能做什么 在前行的路上保持谦卑。礼貌地对待别人会让你在软件开发职业生涯中走得更远。 尊重每一个人。出现分歧后,在你发表意见时,不管对方是什么身份,都要尊重对方。 不能从之前的错误中学到经验 我一直认为,对于软件开发人员,反馈机制是一个很有效的工具。来自他人的反馈,会让我们明白自己的短板是什么以及如何去改进。一个聪明的开发人员明白如何借助他人反馈来促进自己的成长。 根据一个开发人员对建设性意见的反应,你可以判断出他是否缺乏经验。缺乏经验的开发人员不接受任何建设性的建议,甚至代码评审中的评论,他都会认为是对他个人的一种攻击。 很多年前,我有一个同事给我写了很长的一封邮件,教我如何来评审代码,他对我给他代码的评论感到愤怒。他的主要观点是我不应该关注编码标准,因为他知道如何编码,我应该只关注代码能否满足功能需求。 如果一个开发人员因为别人对他代码给出的评论,而感觉被冒犯,只能表明他不具有真正的开发经验。他抱着做一天和尚撞一天钟的态度工作,却感慨没有遇到赏识自己的伯乐。 你能做什么? 对每个反馈保持积极的态度。对于每个反馈,你可以选择是接受还是拒绝,但拒绝之前要保持心平气和的态度。 从错误中学习。没有人能永远正确,保持终身学习才能让自己持续强大。 工作时间处理私人事务 日常工作中,总是发现团队里的一些成员在工作时间处理私人事务,如:看社交媒体,浏览购物网站,玩游戏。 我之前还有个团队成员,上班时间炒股。因为他需要不时地关注股票的 K 线走势,造成个人的产出质量不高。其他同事对他很有意见,因为他们需要花费更多的时间去赶工期。 当开发经理和这个开发人员谈话之后,他改变了一段时间,但是很快就故态复萌。最终,公司只能把他开除了。 工作时间处理私人事务,这是违反商业道德,并且表现了你的不专业。我们需要对工作敬业,毕竟我们要靠它谋生。 你能做什么? 工作时间尽量不要处理私人事务。当你需要离开几个小时去处理个人事情时,请向你的管理者请假。 使用休息时间浏览你的社交媒体。如果必须要点外卖或炒股,请利用午休时间。 盲目追逐技术潮流 开发人员缺乏经验的另一个表现是面对技术潮流的态度。你会发现他们总是在谈论技术潮流,当有一个新的潮流出现时,他们会立刻丢弃原来的潮流,投入新的怀抱。 缺乏经验的开发人员总是在学习教程。毫无疑问,教程是很有用的学习工具,但是,不进行任何实践而只是按照教程一步步操作无疑是浪费时间。 它会让你虚幻地觉得自己好像都掌握了,但是知识是否掌握了,需要通过真实的项目进行检验。 开发人员很少会用热门技术或者从教程中学到的知识来实现新的东西,他们学习热门技术或者教程很多是为了满足自己的虚荣心,或者担心自己会错过什么。 你能做什么? 花费时间和精力学习那些能在工作中或者实际项目中真正用到的技术。 从教程中学习并及时练习,相对于新手教程,自己实现一个功能能学到更多的东西。 缺乏经验的开发人员会因为自己的效率低下进而降低整个团队的效率。他对待自己工作的错误态度,会让其在职业发展中错失很多机会。 了解并避免这种错误的态度和工作方式,是聪明人的做法。如果你不幸染上了这些坏习惯,随着时间的推移,你会越来越难以摆脱。
以下是GObject的一些核心概念和使用方法。 源码:https://gitlab.gnome.org/GNOME/glib/ 教程:https://docs.gtk.org/gobject/index.html 1. GObject的核心概念 动态类型系统:GObject允许程序在运行时进行类型注册,这意味着可以使用纯C语言设计一整套面向对象的软件模块。 内存管理:GObject实现了基于引用计数的内存管理,这简化了内存管理的复杂性。 属性系统:GObject提供了通用的set/get属性获取方法,使得属性管理变得更加简单。 信号机制:GObject内置了简单易用的信号机制,允许对象之间进行通信。 2. GObject的使用示例 在GObject中,类和实例是两个结构体的组合。类结构体初始化函数一般被调用一次,而实例结构体的初始化函数的调用次数等于对象实例化的次数。 所有实例共享的数据保存在类结构体中,而对象私有的数据保存在实例结构体中。 GObject实例的结构体定义如下: typedef struct _GObject GObject; struct _GObject { GTypeInstance g_type_instance; /*< private >*/ guint ref_count; /* (atomic) */ GData *qdata; }; GObject类的结构体定义如下: struct _GObjectClass { GTypeClass g_type_class; /*< private >*/ GSList *construct_properties; /*< public >*/ /* seldom overridden */ GObject* (*constructor) (GType type, guint n_construct_properties, GObjectConstructParam *construct_properties); /* overridable methods */ void (*set_property) (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec); void (*get_property) (GObject *object, guint property_id, GValue *value, GParamSpec *pspec); void (*dispose) (GObject *object); void (*finalize) (GObject *object); /* seldom overridden */ void (*dispatch_properties_changed) (GObject *object, guint n_pspecs, GParamSpec **pspecs); /* signals */ void (*notify) (GObject *object, GParamSpec *pspec); /* called when done constructing */ void (*constructed) (GObject *object); /*< private >*/ gsize flags; gsize n_construct_properties; gpointer pspecs; gsize n_pspecs; /* padding */ gpointer pdummy[3]; }; 以下是一个简单的示例,展示了如何创建和使用GObject实例: #include int main (int argc, char **argv) { GObject* instance1, *instance2; // 指向实例的指针 GObjectClass* class1, *class2; // 指向类的指针 instance1 = g_object_new (G_TYPE_OBJECT, NULL); instance2 = g_object_new (G_TYPE_OBJECT, NULL); g_print ("The address of instance1 is %p\n", instance1); g_print ("The address of instance2 is %p\n", instance2); class1 = G_OBJECT_GET_CLASS (instance1); class2 = G_OBJECT_GET_CLASS (instance2); g_print ("The address of the class of instance1 is %p\n", class1); g_print ("The address of the class of instance2 is %p\n", class2); g_object_unref (instance1); g_object_unref (instance2); return 0; } The address of instance1 is 0x55fb9141ad20 The address of instance2 is 0x55fb9141ad40 The address of the class of instance1 is 0x55fb9141a350 The address of the class of instance2 is 0x55fb9141a350 在这个示例中,g_object_new函数用于创建GObject实例,并返回指向它的指针。 G_TYPE_OBJECT是GObject基类的类型标识符,所有其他GObject类型都从这个基类型派生。 宏G_OBJECT_GET_CLASS返回指向参数所属类变量的指针。g_object_unref用于销毁实例变量并释放内存。 实例1与实例2的存储空间是不同的,每个实例都有自己的空间。 两个类的存储空间是相同的,两个GObject实例共享同一个类。 3. GObject的信号机制 GObject允许定义和使用属性,以及发出和连接信号。 这些特性使得GObject非常适合用于构建复杂的软件系统,尤其是在需要组件间通信和属性管理的场景中。 信号最基本的用途是实现事件通知。例如:创建一个信号,当调用文件写方法时,触发文件变化信号。 创建信号: file_signals[CHANGED] = g_signal_newv ("changed", G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, NULL /* closure */, NULL /* accumulator */, NULL /* accumulator data */, NULL /* C marshaller */, G_TYPE_NONE /* return_type */, 0 /* n_params */, NULL /* param_types */); 带信号机制的文件写方法: void viewer_file_write (ViewerFile *self, const guint8 *buffer, gsize size) { g_return_if_fail (VIEWER_IS_FILE (self)); g_return_if_fail (buffer != NULL || size == 0); /* First write data. */ /* Then, notify user of data written. */ g_signal_emit (self, file_signals[CHANGED], 0 /* details */); } 用户回调处理函数连接到信号: g_signal_connect (file, "changed", (GCallback) changed_event, NULL); 4. 跨语言互通性 GObject被设计为可以直接使用在C程序中,并且可以封装至其他语言,如C++、Java、Ruby、Python和.NET/Mono等,这使得GObject具有很好的跨语言互通性。
一、异常处理实践在编写 C++ 代码时会遇到不可预期的错误和异常情况。为了让我们的代码更健壮和可靠,我们需要使用异常处理机制来处理这些情况。
01 为什么要基于UDS搞Bootloader 假如你的控制器有外壳,却没有设计bootloader的话,每次更新ECU的程序,你都需要把外壳拆开,用烧写器来更新程序。有了bootloader,你就可以通过CAN线来更新程序了。更方便些的话,甚至可以通过OTA进行远程升级。 那为什么使用UDS呢?主要是为了规范bootloader的全过程。比如烧写小明牌ECU时,我们肯定希望其他牌子的ECU处于一个静默的状态,都歇一歇,这就需要一个大家共同执行的标准来进行规范,什么时候停发数据,什么时候不能再储存DTC了等等。 又比如在调试时,大家肯定希望你的控制器经由CAN烧写的过程是大家都能看得懂的,是满足于某种规范的。由此,UDS在设计时考虑了bootloader的需求,专门为bootloader设计了几个服务,供大家使用。主机厂在发需求时自然就要求大家要在UDS规范的基础上完成bootloader功能了。 02 Bootloader应支持的UDS服务 显然bootloader不需要支持19/14等故障类服务。 在boot程序中,10/27/11/3E这样的基础诊断服务需要支持,22/2E读写DID的服务需要支持,31/34/36/37这4个bootloader主打服务需要支持,共10个。 在app段程序中,85和28服务需要支持,保证暂停CAN正常通信,暂停记录DTC,让被升级设备专心升级。 10种boot段服务+2种app段服务 03 测试设备在线Bootloader——三段式 (1)预编程阶段 1. 3E TP报文。 2. 10服务切换到03扩展模式。 3. 85服务和28服务,关DTC和非诊断报文。使整个CAN网络处于安静的状态。这是对整车网络进行操作的,一般都是以功能寻址的方式来发送。注意先用85服务关闭DTC,再使用28服务关报文。 (2)主编程阶段 1. 10服务切换到编程模式,这里要注意,正确的方式是App段程序回复0x78 NRC,接下来跳转到boot段程序,最后由Boot段程序来回复10 02的肯定响应。错误的方式是由App段回复10 02的肯定响应,再进行跳转。 2. 读取一个DID,tester要判断一下返回值。返回值里面可能包含密钥的一部分信息。 3. 27服务,解锁,通过安全验证。 注意10 02服务不应直接进行肯定响应,存在风险 4. 写DID指纹,标记写软件人的身份,ECU回复写指纹成功。(根据OEM要求来执行) 5. 31服务-擦除Flash。ECU肯定响应,擦除成功。 6. 34服务,请求数据下载,ECU回复确认最大块大小。 7. 36服务,开始传输数据。每个块传输完成后,ECU肯定响应。判断是否还有更多块需要下载。最多可以支持255个块。 8. 37服务,请求退出传输。ECU肯定响应。 9. 31服务-校验APP段程序,检查编程一致性/完整性。ECU肯定响应。校验成功。 10. 若有更多块需要下载,重新执行31(擦除Flash区域)-34-36-37-31(校验)服务。若无,往下执行。 11. 11服务,ECU复位。之后应直接跳转到新下载的APP段程序中。 31(擦Flash)-34-36 36-37-31(校验) (3)后编程状态 1. 10服务切换到03扩展会话。 2. 执行28服务和85服务,使能非诊断报文和DTC。这是对整车网络进行操作的,一般都是以功能寻址的方式来发送。注意先执行28,后执行85,避免DTC误报。 3. 27服务,安全校验,准备写入数据。 4. 2E服务,将编程信息写入到ECU中。 5. 10服务,退回01默认会话。结束。 04 BootLoader的启动顺序与转换流程 1. ECU上电或复位后,先进入Boot段。从Flash/EEPROM中读取 App有效标志,运行boot标志 。 2.判断 运行boot标志 ,若为1,则进入Boot段的编程会话(安全等级为上锁),之后写Flash/EEPROM(不安全操作), 运行boot标志 清零。若S3定时器超时则退回Boot段默认会话。 3. 经过安全访问进入Level2解锁状态,开始执行App内存擦除,擦除后 App有效标志 清零(不安全操作)。 4. 开始烧写。烧写成功后 运行boot标志 写0,App有效标志 写1。 2*. 判断 运行boot标志 ,若为0,则进入Boot段的默认会话。 3*. 50ms后判断 App有效标志 ,若为1,则 跳转到 App段默认会话。实现时使用汇编指令执行APP段程序;若为0,退回Boot段默认会话,且不再判断 App有效标志,不会再尝试进入App段。 4*. App段程序若收到了编程会话请求, 运行boot标志写1 ,随即执行ECU复位,这样会重新进入boot段程序。 注:从BOOT跳入APP前需要判断APP的数据完整性,例如进行CRC校验。
在工业自动化的繁忙现场,PLC(可编程逻辑控制器)如同大脑般指挥着各类设备的运作。然而,一旦其通讯模块出现故障,整个生产线可能会瞬间陷入瘫痪,带来不可估量的损失。你是否也曾遭遇过这样的困境,却束手无策?别担心,今天我们就来一场实战演练,教你如何快速判断PLC通讯模块的故障,并掌握更换技巧,让你的工业自动化系统畅通无阻! 通讯模块故障,你中招了吗? 想象一下,当PLC通讯模块突然“罢工”,生产线上的设备开始各自为政,数据无法上传、指令无法下达,整个系统陷入一片混乱。这样的场景,是否让你不寒而栗?别担心,我们这就为你揭开通讯模块故障的神秘面纱,助你轻松应对! 一、通讯模块的重要性与故障后果 PLC通讯模块,作为PLC与外界设备沟通的桥梁,其重要性不言而喻。一旦出现故障,可能会带来数据无法传输、生产停滞、误报漏报等严重后果。因此,掌握通讯模块的故障诊断与更换技能,对于确保工业自动化系统的稳定运行至关重要。 你的工厂是否也出现过PLC通讯模块故障的情况?你是如何处理的?欢迎在评论区分享你的经验和教训! 二、故障诊断前的准备 在进行故障诊断前,我们需要准备以下工具和设备:PLC主机及通讯模块(用于故障测试和替换)、编程软件及编程电缆、备用通讯模块、万用表、电脑或hmi设备等。这些工具将助我们快速定位故障,确保更换过程的顺利进行。 三、故障诊断的逻辑步骤 要快速判断PLC通讯模块是否故障,我们可以按照以下逻辑步骤进行: 排除非硬件问题:先确认通讯模块的供电、连接线缆、网络设置等是否正确。 检查硬件状态:通过观察通讯模块上的指示灯状态,判断是否正常工作。 替换法验证:将故障模块替换为备用模块,观察系统是否恢复正常。 记录和分析日志:利用PLC编程软件查看通讯错误代码或系统日志,进一步确认故障原因。 现在,请你试着根据以上步骤,对你工厂中的PLC通讯模块进行一次初步检查。你是否发现了潜在的问题?如果有,请记录下来,并在评论区分享你的发现! 四、具体操作步骤与细节 接下来,我们将详细介绍故障诊断与更换的具体操作步骤: 观察指示灯状态:根据指示灯的状态,我们可以初步判断通讯模块的工作状态。例如,电源灯不亮可能是模块未供电或电源故障;通讯灯不闪可能是通讯中断或模块未正常工作。 练习题:请查阅你所使用的PLC通讯模块的指示灯说明,记住各状态灯的含义。 检查供电和线缆:使用万用表检查模块的供电电压是否符合要求,线缆连接是否牢固。对于以太网通讯模块,可以使用网络测试工具检查网线是否通畅。 小提示:在振动较大的设备旁,线缆容易磨损或松动,务必加强检查。 使用编程软件诊断:通过PLC编程软件查看通讯模块的状态和错误代码。常见的错误包括通讯超时、地址冲突、参数错误等。 常见错误提醒:初学者容易忽略通讯协议和波特率的匹配问题,务必重点检查。 替换法验证:如果通过以上步骤仍然无法确认故障,可以直接将通讯模块替换为备用模块。替换后,观察系统是否恢复正常。 练习题:尝试从你的PLC中拔下通讯模块,记录其安装方式和固定方法。 记录和分析日志:许多PLC系统会记录故障日志或报警信息,这些信息有助于定位问题。例如,“模块脱离”或“通讯失败”等提示。 学习技巧:养成查看系统日志的习惯,这将让你更快地找到问题所在。 五、功能扩展与调试方法 除了基本的故障诊断和更换操作外,我们还可以对系统进行以下改进: 冗余设计:为关键通讯模块配置备份模块,当主模块故障时自动切换。 远程监控:通过将PLC接入云平台,实时监控通讯状态,提前预警故障。 优化布线:采用屏蔽线缆,减少工业环境中的电磁干扰对通讯的影响。 完成模块更换后,务必按照以下步骤进行调试: 检查模块安装是否牢固。 重新上电,观察通讯模块的状态指示灯是否恢复正常。 测试通讯功能,确保数据能够正常传输。 模拟故障场景,测试系统的故障响应能力。 六、注意事项与应用场景 在更换PLC通讯模块时,我们需要注意以下几点: 备件管理:现场应常备通讯模块,以便快速更换。 数据备份:更换模块前,确保PLC程序和参数已备份。 防静电操作:更换模块时应佩戴防静电手环,避免损坏硬件。 标记模块:为模块做好标识,避免误用或安装错误。 PLC通讯模块的故障诊断和更换方法不仅适用于工业生产线,还可以应用于智能楼宇控制、仓储物流系统、能源管理系统等多个场景。 七、常见问题及其解决方法 以下是一些常见问题及其解决方法: 通讯模块电源灯不亮:检查电源接线是否牢固,必要时更换模块。 通讯数据丢失或中断:更换线缆,优化布线,减少干扰。 模块通讯指示灯不闪:检查通讯协议和波特率是否匹配,必要时更换模块。 更换模块后仍无法通讯:更新PLC程序中的模块参数,确保配置正确。 八、总结与行动建议 PLC通讯模块是工业自动化系统中的重要组成部分,其故障可能会严重影响设备运行。通过本次学习,相信你已经掌握了判断通讯模块故障的基本方法以及更换模块的具体步骤。然而,知识只有付诸实践才能真正转化为技能。因此,我们建议你回到工作现场,尝试观察PLC通讯模块的状态灯并熟悉模块的拆装方法。相信你会发现,动手实践是最好的学习方式!
大家好!我是付工。西门子、三菱、欧姆龙是我们自动化行业使用最多的三个PLC品牌。今天跟大家分享一下三菱PLC的MC通信协议分析。大家在学习MC通信协议时,建议先熟练掌握Modbus通信协议。三菱PLC的MC协议是一种数据通信协议,它用于在计算机和三菱PLC之间传输数据。MC协议是三菱公司独有的一种协议,主要用于控制三菱PLC。MC协议是Melsec协议的简称。 一、通信帧类型 很多人在学习三菱PLC通信协议时,很头疼的是三菱PLC有很多种通信帧类型,每种通信帧又有ASCII和二进制两种编码格式。在我们实际开发中,以太网通信主要使用的是QnA兼容3E帧(FX5U系列/Q系列/Qna系列/L系列/R系列),如果是FX3U通过加装Fx3U-ENET-ADP,会使用到A兼容1E帧。对于串口设备,一般会使用QnA兼容2C帧或QnA兼容4C帧。通信编码格式有ASCII和二进制两种方式,通过二进制编码数据进行的通信与通过ASCII编码数据进行的通信相比,前者的通信数据量约为后者的二分之一,因此二进制编码的方式通信效率更高。 二、PLC配置 三菱PLC与西门子PLC有所不同。 西门子PLC是固定端口102,一个端口支持多个连接。 三菱PLC需要手动添加端口,一个端口只支持一个连接。 因此三菱PLC需要手动配置,这里以三菱R系列为例: 1、在导航中,通过参数>>R08ENCPU>> 模块参数,双击在打开的界面中设置好PLC的IP地址信息,将通信数据代码改成二进制,然后找到【对象设备连接配置设置】: 2、点击设置,可以拖入多个SLMP连接设备,端口号根据自己需求设置,然后反应设置并关闭设置结束,设置完成后,重新下载PLC程序,断电重启PLC。 SLMP(Seamless Message Protocol)是在以太网中使用的协议。MC协议则包含了串口以及以太网的通信协议,范围更广。 三、读取协议帧 接下来我们来分析一下三菱的通信报文,以QnA兼容3E帧为例,其他通信帧大同小异。 协议帧一般分为请求帧、响应帧及异常帧。 请求帧:表示发送请求的报文。 响应帧:如果请求正确,PLC会以响应帧进行返回。 异常帧:如果请求错误,CPU会以异常帧返回。 读取请求帧报文格式: 读取响应帧报文格式: 读取异常帧报文格式: 说明:以上三种协议帧是参考三菱官方文档总结而成,其中头部指的是TCP头部,我们可以不用管。 四、写入协议帧 写入请求帧报文格式: 写入响应帧报文格式: 写入异常帧报文格式: 说明:如果我们学过Modbus,可以看到,协议都是相通的,MC只是比Modbus报文结构更复杂而已。 五、通信测试 我们以读取D0开始的5个寄存器为例,结合协议文档,来进行报文拼接。 发送报文如下: 副头部:0x50 0x00 网络编号:0x00 PLC编号:0xFF 请求目标模块I/O编号:0xFF 0x03 请求目标模块站号:0x00 请求数据长度:0x0C 0x00 CPU监视定时器:0x0A 0x00 指令:0x01 0x04 子指令:0x00 0x00 起始软元件:0x00 0x00 0x00 软元件代码:0xA8 软元件点数:0x05 0x00 我们通过网络调试助手发送这个报文,观察一下返回的报文: 响应报文如下: 副头部:0xD0 0x00 网络编号:0x00 PLC编号:0xFF 请求目标模块I/O编号:0xFF 0x03 请求目标模块站号:0x00 响应数据长度:0x0C 0x00 结束代码:0x00 0x00 软元件数据:0x0A 0x00 0x14 0x00 0x1E 0x00 0x28 0x00 0x32 0x00 其中0x0A 0x00 0x14 0x00 0x1E 0x00 0x28 0x00 0x32 0x00即表示D0-D4的值,进行数据解析处理后的值分别为10、20、30、40、50,与PLC数据一致。 六、代码实现 我们也可以编写C#程序来实现整个过程。 这里测试为主,代码相对简单,实际应用时可进一步封装,代码如下: static void Main(string[] args){ // 连接 Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.Connect("192.168.2.144", 4096); byte[] bytes = new byte[] { 0x50,0x00,//副头部,固定50 00 0x00,// 网络编号 0xFF,//PLC编号 0xFF,0x03,//目标模块IO编号,固定FF 03 0x00,// 目标模块站号 0x0C,0x00, // 字节长度,当前字节往后 0x0A,0x00,//PLC响应超时时间,以250ms为单位计算 0x01,0x04,// 成批读出,主命令 0x00,0x00,// 字操作,子命令 0x00,0x00,0x00,// 起始地址 0xA8,// 区域代码 0x05,0x00 //读取长度 }; socket.Send(bytes); byte[] respBytes = new byte[1024]; int count = socket.Receive(respBytes); if (count == 21) { for (int i = 11; i < count; i+=2) { // 小端处理,每2个字节作为一个数据 byte[] dataBytes = new byte[2]; dataBytes[0] = respBytes[i]; dataBytes[1] = respBytes[i+1]; Console.WriteLine(BitConverter.ToInt16(dataBytes, 0)); } }} 输出结果如下:
在多线程编程中, 有两个需要注意的问题, 一个是数据竞争, 另一个是内存执行顺序. 什么是数据竞争(Data Racing) 我们先来看什么是数据竞争(Data Racing), 数据竞争会导致什么问题. #include #include int counter = 0;void increment() { for (int i = 0; i < 100000; ++i) { // ++counter实际上3条指令 // 1. int tmp = counter; // 2. tmp = tmp + 1; // 3. counter = tmp; ++counter; }}int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "Counter = " << counter << "\n"; return 0;} 在这个例子中, 我们有一个全局变量counter, 两个线程同时对它进行增加操作. 理论上, counter的最终值应该是200000. 然而, 由于数据竞争的存在, counter的实际值可能会小于200000. 这是因为, ++counter并不是一个原子操作, CPU会将++counter分成3条指令来执行, 先读取counter的值, 增加它, 然后再将新值写回counter. 示意图如下: Thread 1 Thread 2--------- ---------int tmp = counter; int tmp = counter;tmp = tmp + 1; tmp = tmp + 1;counter = tmp; counter = tmp; 两个线程可能会读取到相同的counter值, 然后都将它增加1, 然后将新值写回counter. 这样, 两个线程实际上只完成了一次+操作, 这就是数据竞争. 如何解决数据竞争 c++11引入了std::atomic, 将某个变量声明为std::atomic后, 通过std::atomic的相关接口即可实现原子性的读写操作. 现在我们用std::atomic来修改上面的counter例子, 以解决数据竞争的问题. #include #include #include // 只能用bruce-initialization的方式来初始化std::atomic.// std::atomiccounter{0} is ok, // std::atomiccounter(0) is NOT ok.std::atomic<int> counter{0}; void increment() { for (int i = 0; i < 100000; ++i) { ++counter; }}int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "Counter = " << counter << "\n"; return 0;} 我们将int counter改成了std::atomiccounter, 使用std::atomic的++操作符来实现原子性的自增操作. std::atomic提供了以下几个常用接口来实现原子性的读写操作, memory_order用于指定内存顺序, 我们稍后再讲. // 原子性的写入值std::atomic::store(T val, memory_order sync = memory_order_seq_cst);// 原子性的读取值std::atomic::load(memory_order sync = memory_order_seq_cst);// 原子性的增加// counter.fetch_add(1)等价于++counterstd::atomic::fetch_add(T val, memory_order sync = memory_order_seq_cst);// 原子性的减少// counter.fetch_sub(1)等价于--counterstd::atomic::fetch_sub(T val, memory_order sync = memory_order_seq_cst);// 原子性的按位与// counter.fetch_and(1)等价于counter &= 1std::atomic::fetch_and(T val, memory_order sync = memory_order_seq_cst);// 原子性的按位或// counter.fetch_or(1)等价于counter |= 1std::atomic::fetch_or(T val, memory_order sync = memory_order_seq_cst);// 原子性的按位异或// counter.fetch_xor(1)等价于counter ^= 1std::atomic::fetch_xor(T val, memory_order sync = memory_order_seq_cst); 什么是内存顺序(Memory Order) 内存顺序是指在并发编程中, 对内存读写操作的执行顺序. 这个顺序可以被编译器和处理器进行优化, 可能会与代码中的顺序不同, 这被称为指令重排. 我们先举例说明什么是指令重排, 假设有以下代码: int a = 0, b = 0, c = 0, d = 0;void func() { a = b + 1; c = d + 1;} 在这个例子中, a = b + 1和c = d + 1这两条语句是独立的, 它们没有数据依赖关系. 如果处理器按照代码的顺序执行这两条语句, 那么它必须等待a = b + 1执行完毕后, 才能开始执行c = d + 1. 但是, 如果处理器可以重排这两条语句的执行顺序, 那么它就可以同时执行这两条语句, 从而提高程序的执行效率. 例如, 处理器有两个执行单元, 那么它就可以将a = b + 1分配给第一个执行单元, 将c = d + 1分配给第二个执行单元, 这样就可以同时执行这两条语句. 这就是指令重排的好处:它可以充分利用处理器的执行流水线, 提高程序的执行效率. 但是在多线程的场景下, 指令重排可能会引起一些问题, 我们看个下面的例子: #include #include #include std::atomic<bool> ready{false};std::atomic<int> data{0};void producer() { data.store(42, std::memory_order_relaxed); // 原子性的更新data的值, 但是不保证内存顺序 ready.store(true, std::memory_order_relaxed); // 原子性的更新ready的值, 但是不保证内存顺序}void consumer() { // 原子性的读取ready的值, 但是不保证内存顺序 while (!ready.load(memory_order_relaxed)) { std::this_thread::yield(); // 啥也不做, 只是让出CPU时间片 } // 当ready为true时, 再原子性的读取data的值 std::cout << data.load(memory_order_relaxed); // 4. 消费者线程使用数据}int main() { // launch一个生产者线程 std::thread t1(producer); // launch一个消费者线程 std::thread t2(consumer); t1.join(); t2.join(); return 0;} 在这个例子中, 我们有两个线程:一个生产者(producer)和一个消费者(consumer). 生产者先将data的值修改为一个有效值, 比如42, 然后再将ready的值设置为true, 通知消费者可以读取data的值了. 我们预期的效果应该是当消费者看到ready为true时, 此时再去读取data的值, 应该是个有效值了, 对于本例来说即为42. 但实际的情况是, 消费者看到ready为true后, 读取到的data值可能仍然是0. 为什么呢? 一方面可能是指令重排引起的. 在producer线程里, data和store是两个不相干的变量, 所以编译器或者处理器可能会将data.store(42, std::memory_order_relaxed);重排到ready.store(true, std::memory_order_relaxed);之后执行, 这样consumer线程就会先读取到ready为true, 但是data仍然是0. 另一方面可能是内存顺序不一致引起的. 即使producer线程中的指令没有被重排, 但CPU的多级缓存会导致consumer线程看到的data值仍然是0. 我们通过下面这张示意图来说明这和CPU多级缓存有毛关系. 每个CPU核心都有自己的L1 Cache与L2 Cache. producer线程修改了data和ready的值, 但修改的是L1 Cache中的值, producer线程和consumer线程的L1 Cache并不是共享的, 所以consumer线程不一定能及时的看到producer线程修改的值. CPU Cache的同步是件很复杂的事情, 生产者更新了data和ready后, 还需要根据MESI协议将值写回内存,并且同步更新其他CPU核心Cache里data和ready的值, 这样才能确保每个CPU核心看到的data和ready的值是一致的. 而data和ready同步到其他CPU Cache的顺序也是不固定的, 可能先同步ready, 再同步data, 这样的话consumer线程就会先看到ready为true, 但data还没来得及同步, 所以看到的仍然是0. 这就是我们所说的内存顺序不一致问题. 为了避免这个问题, 我们需要在producer线程中, 在data和ready的更新操作之间插入一个内存屏障(Memory Barrier), 保证data和ready的更新操作不会被重排, 并且保证data的更新操作先于ready的更新操作. 需要C/C++ Linux服务器架构师学习资料加qun579733396获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享 现在我们来修改上述的例子, 来解决内存顺序不一致的问题. #include #include #include std::atomic<bool> ready{false};std::atomic<int> data{0};void producer() { data.store(42, std::memory_order_relaxed); // 原子性的更新data的值, 但是不保证内存顺序 ready.store(true, std::memory_order_released); // 保证data的更新操作先于ready的更新操作}void consumer() { // 保证先读取ready的值, 再读取data的值 while (!ready.load(memory_order_acquire)) { std::this_thread::yield(); // 啥也不做, 只是让出CPU时间片 } // 当ready为true时, 再原子性的读取data的值 std::cout << data.load(memory_order_relaxed); // 4. 消费者线程使用数据}int main() { // launch一个生产者线程 std::thread t1(producer); // launch一个消费者线程 std::thread t2(consumer); t1.join(); t2.join(); return 0;} 我们只是做了两处改动, 将producer线程里的ready.store(true, std::memory_order_relaxed);改为ready.store(true, std::memory_order_released);, 一方面限制ready之前的所有操作不得重排到ready之后, 以保证先完成data的写操作, 再完成ready的写操作. 另一方面保证先完成data的内存同步, 再完成ready的内存同步, 以保证consumer线程看到ready新值的时候, 一定也能看到data的新值. 将consumer线程里的while (!ready.load(memory_order_relaxed))改为while (!ready.load(memory_order_acquire)), 限制ready之后的所有操作不得重排到ready之前, 以保证先完成读ready操作, 再完成data的读操作; 现在我们再来看看memory_order的所有取值与作用: memory_order_relaxed: 只确保操作是原子性的, 不对内存顺序做任何保证, 会带来上述produer-consumer例子中的问题. memory_order_release: 用于写操作, 比如std::atomic::store(T, memory_order_release), 会在写操作之前插入一个StoreStore屏障, 确保屏障之前的所有操作不会重排到屏障之后, 如下图所示 + | | | No Moving Down |+-----v---------------------+| StoreStore Barrier |+---------------------------+ memory_order_acquire: 用于读操作, 比如std::atomic::load(memory_order_acquire), 会在读操作之后插入一个LoadLoad屏障, 确保屏障之后的所有操作不会重排到屏障之前, 如下图所示: +---------------------------+| LoadLoad Barrier |+-----^---------------------+ | | | No Moving Up | | + memory_order_acq_rel: 等效于memory_order_acquire和memory_order_release的组合, 同时插入一个StoreStore屏障与LoadLoad屏障. 用于读写操作, 比如std::atomic::fetch_add(T, memory_order_acq_rel). memory_order_seq_cst: 最严格的内存顺序, 在memory_order_acq_rel的基础上, 保证所有线程看到的内存操作顺序是一致的. 这个可能不太好理解, 我们看个例子, 什么情况下需要用到memory_order_seq_cst: #include#include#include std::atomic<bool> x = {false};std::atomic<bool> y = {false};std::atomic<int> z = {0}; void write_x(){ x.store(true, std::memory_order_seq_cst);} void write_y(){ y.store(true, std::memory_order_seq_cst);} void read_x_then_y(){ while (!x.load(std::memory_order_seq_cst)) ; if (y.load(std::memory_order_seq_cst)) { ++z; }} void read_y_then_x(){ while (!y.load(std::memory_order_seq_cst)) ; if (x.load(std::memory_order_seq_cst)) { ++z; }} int main(){ std::thread a(write_x); std::thread b(write_y); std::thread c(read_x_then_y); std::thread d(read_y_then_x); a.join(); b.join(); c.join(); d.join(); assert(z.load() != 0); // will never happen} 以上这个例子中, a, c, c, d四个线程运行完毕后, 我们期望z的值一定是不等于0的, 也就是read_x_then_y和read_y_then_x两个线程至少有一个会执行++z. 要保证这个期望成立, 我们必须对x和y的读写操作都使用memory_order_seq_cst, 以保证所有线程看到的内存操作顺序是一致的. 如何理解呢,我们假设将所有的 memory_order_seq_cst 替换为 memory_order_relaxed,则可能的执行顺序如下: 线程 A:write_x()执行时,x 被设为 true。 线程 B:write_y()执行时,y 被设为 true。 线程 C:read_x_then_y() 里面读取到 x 是 true,但由于 memory_order_relaxed 不保证全局可见性,y.load()可能仍然读取到 false。 线程 D:read_y_then_x() 里面读取到 y 是 true,但同样,x.load()可能仍然读取到 false。 如果我们使用的是memory_order_seq_cst,就不会遇到这种情况。线程C和线程D的看到的x和y的值就会是一致的。 这样说可能还是会比较抽象, 我们可以直接理解下memory_order_seq_cst是怎么实现的. 被memory_order_seq_cst标记的写操作, 会立马将新值写回内存, 而不仅仅只是写到Cache里就结束了; 被memory_order_seq_cst标记的读操作, 会立马从内存中读取新值, 而不是直接从Cache里读取. 这样相当于write_x, write_y, read_x_then_y, read_y_then_x四个线程都是在同一个内存中读写x和y, 也就不存在Cache同步的顺序不一致问题了. 理解memory_order_seq_cst的实现后,我们再回过头来就比较好理解为什么使用memory_order_relaxed 时线程C和线程D里看到的x和y的值会不一样了。 我们来看下面这张图,如果我们使用的是memory_order_relaxed ,则: write_x()与read_y_then_x()放到 Core1 执行 write_y()与read_x_then_y()放到 Core2 上执行。 结果就会是: write_x只会把自己 Core 里 Cache 的 x 更新为 true,也就只有 Core1 能看到 x为 true,而 Core2 里的 x 还是 false。 write_y只会把自己 Core 里 Cache 的 y 更新为 true,也就只有 Core2 能看到 y为 true,而 Core1 里的 y 还是 false。 而如果我们用的是memory_order_seq_cst,那么write_x()和write_y()都会把 Cache 与 Memory 里的值一起更新,read_x_then_y()与read_y_then_x()都会从 Memory 里读值,而不是从自己 Core 里的 Cache 里读,这样它们看到的 x和y的值就是一样的了。 所以我们也就能理解为什么memory_order_seq_cst会带来最大的性能开销了, 相比其他的memory_order来说, 因为相当于禁用了CPU Cache. memory_order_seq_cst常用于multi producer - multi consumer的场景, 比如上述例子里的write_x和write_y都是producer, 会同时修改x和y, read_x_then_y和read_y_then_x两个线程, 都是consumer, 会同时读取x和y, 并且这两个consumer需要按照相同的内存顺序来同步x和y.
在PLC编程中,一般把停止按钮物理接点接成常闭,程序中写成常开,这样的停止按钮在这套设备中只起到信号作用。 为什么PLC编程中急停要和普通急停按钮相反呢?其实写成常开的不仅仅是急停,还有热继、温控等等带有保护功能的接点,具体原因见下文: 首先急停按钮接入PLC的DI点中也是以常闭点接入: 上图就是急停按钮接入PLC中的接线图的一部分,可以看到是常闭触点的状态接入!以常闭触点接入的好处是,当急停按钮所在的线路断路了,程序中也能立马反映出来,或者说相当于急停按钮被按下去,常闭触点变成了常开触点,实现了对断线状态的监控! 然后在PLC程序中看一下: 这段程序,启动条件1(I0.2)和启动条件2(I0.3)都是以常开状态接入PLC的两个按钮,而急停按钮(E_StopPB)则是以常闭状态接入PLC的按钮!---从图片上的圆圈①可以看到,3个按钮都没有按下的时候,只有E_StopPB是接通的,当三个按钮都按下时即圆圈②处,启动条件1和2的两个状态接通了,而E_StopPB的状态则是显示断开!这两种状态的不同就解释了,为何急停按钮在PLC硬件处接线要用常闭,而PLC程序中要用常开!---是因为PLC模块的数字量DI点,是外围电路接通程序内部就显示接通,而外围电路断开则程序内部就显示断开,也就是说PLC的DI点硬件电路设计造成的!另外,因为急停信号是很重要的点,所以人们利用PLC数字量DI点的硬件特性,人为的规定急停按钮接线应该是常闭点接入!---因为常闭点造成电路一直接通,所以程序中就要用常开点,这样才能保证不急停的时候,程序逻辑能接通!
1. 如图4.13.1所示,P0端口接动态数码管的字形码笔段,P2端口接动态数码管的数位选择端,P1.7接一个开关,当开关接高电平时,显示“12345”字样;当开关接低电平时,显示“HELLO”字样。 2. 电路原理图 图4.13.1 3. 系...
1. 如图4.3.1所示,AT89S51单片机的P1.0-P1.3接四个发光二极管L1-L4,P1.4-P1.7接了四个开关K1-K4,编程将开关的状态反映到发光二极管上。(开关闭合,对应的灯亮,开关断开,对应的灯灭)。 2. 电路原理图 图4.3.1...