• 为什么要基于UDS搞Bootloader

    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校验。

    04-09 88浏览
  • 通讯模块故障,你中招了吗?

    在工业自动化的繁忙现场,PLC(可编程逻辑控制器)如同大脑般指挥着各类设备的运作。然而,一旦其通讯模块出现故障,整个生产线可能会瞬间陷入瘫痪,带来不可估量的损失。你是否也曾遭遇过这样的困境,却束手无策?别担心,今天我们就来一场实战演练,教你如何快速判断PLC通讯模块的故障,并掌握更换技巧,让你的工业自动化系统畅通无阻! 通讯模块故障,你中招了吗? 想象一下,当PLC通讯模块突然“罢工”,生产线上的设备开始各自为政,数据无法上传、指令无法下达,整个系统陷入一片混乱。这样的场景,是否让你不寒而栗?别担心,我们这就为你揭开通讯模块故障的神秘面纱,助你轻松应对! 一、通讯模块的重要性与故障后果 PLC通讯模块,作为PLC与外界设备沟通的桥梁,其重要性不言而喻。一旦出现故障,可能会带来数据无法传输、生产停滞、误报漏报等严重后果。因此,掌握通讯模块的故障诊断与更换技能,对于确保工业自动化系统的稳定运行至关重要。 你的工厂是否也出现过PLC通讯模块故障的情况?你是如何处理的?欢迎在评论区分享你的经验和教训! 二、故障诊断前的准备 在进行故障诊断前,我们需要准备以下工具和设备:PLC主机及通讯模块(用于故障测试和替换)、编程软件及编程电缆、备用通讯模块、万用表、电脑或hmi设备等。这些工具将助我们快速定位故障,确保更换过程的顺利进行。 三、故障诊断的逻辑步骤 要快速判断PLC通讯模块是否故障,我们可以按照以下逻辑步骤进行: 排除非硬件问题:先确认通讯模块的供电、连接线缆、网络设置等是否正确。 检查硬件状态:通过观察通讯模块上的指示灯状态,判断是否正常工作。 替换法验证:将故障模块替换为备用模块,观察系统是否恢复正常。 记录和分析日志:利用PLC编程软件查看通讯错误代码或系统日志,进一步确认故障原因。 现在,请你试着根据以上步骤,对你工厂中的PLC通讯模块进行一次初步检查。你是否发现了潜在的问题?如果有,请记录下来,并在评论区分享你的发现! 四、具体操作步骤与细节 接下来,我们将详细介绍故障诊断与更换的具体操作步骤: 观察指示灯状态:根据指示灯的状态,我们可以初步判断通讯模块的工作状态。例如,电源灯不亮可能是模块未供电或电源故障;通讯灯不闪可能是通讯中断或模块未正常工作。 练习题:请查阅你所使用的PLC通讯模块的指示灯说明,记住各状态灯的含义。 检查供电和线缆:使用万用表检查模块的供电电压是否符合要求,线缆连接是否牢固。对于以太网通讯模块,可以使用网络测试工具检查网线是否通畅。 小提示:在振动较大的设备旁,线缆容易磨损或松动,务必加强检查。 使用编程软件诊断:通过PLC编程软件查看通讯模块的状态和错误代码。常见的错误包括通讯超时、地址冲突、参数错误等。 常见错误提醒:初学者容易忽略通讯协议和波特率的匹配问题,务必重点检查。 替换法验证:如果通过以上步骤仍然无法确认故障,可以直接将通讯模块替换为备用模块。替换后,观察系统是否恢复正常。 练习题:尝试从你的PLC中拔下通讯模块,记录其安装方式和固定方法。 记录和分析日志:许多PLC系统会记录故障日志或报警信息,这些信息有助于定位问题。例如,“模块脱离”或“通讯失败”等提示。 学习技巧:养成查看系统日志的习惯,这将让你更快地找到问题所在。 五、功能扩展与调试方法 除了基本的故障诊断和更换操作外,我们还可以对系统进行以下改进: 冗余设计:为关键通讯模块配置备份模块,当主模块故障时自动切换。 远程监控:通过将PLC接入云平台,实时监控通讯状态,提前预警故障。 优化布线:采用屏蔽线缆,减少工业环境中的电磁干扰对通讯的影响。 完成模块更换后,务必按照以下步骤进行调试: 检查模块安装是否牢固。 重新上电,观察通讯模块的状态指示灯是否恢复正常。 测试通讯功能,确保数据能够正常传输。 模拟故障场景,测试系统的故障响应能力。 六、注意事项与应用场景 在更换PLC通讯模块时,我们需要注意以下几点: 备件管理:现场应常备通讯模块,以便快速更换。 数据备份:更换模块前,确保PLC程序和参数已备份。 防静电操作:更换模块时应佩戴防静电手环,避免损坏硬件。 标记模块:为模块做好标识,避免误用或安装错误。 PLC通讯模块的故障诊断和更换方法不仅适用于工业生产线,还可以应用于智能楼宇控制、仓储物流系统、能源管理系统等多个场景。 七、常见问题及其解决方法 以下是一些常见问题及其解决方法: 通讯模块电源灯不亮:检查电源接线是否牢固,必要时更换模块。 通讯数据丢失或中断:更换线缆,优化布线,减少干扰。 模块通讯指示灯不闪:检查通讯协议和波特率是否匹配,必要时更换模块。 更换模块后仍无法通讯:更新PLC程序中的模块参数,确保配置正确。 八、总结与行动建议 PLC通讯模块是工业自动化系统中的重要组成部分,其故障可能会严重影响设备运行。通过本次学习,相信你已经掌握了判断通讯模块故障的基本方法以及更换模块的具体步骤。然而,知识只有付诸实践才能真正转化为技能。因此,我们建议你回到工作现场,尝试观察PLC通讯模块的状态灯并熟悉模块的拆装方法。相信你会发现,动手实践是最好的学习方式!

    03-14 177浏览
  • 三菱PLC的MC通信协议报文解析

    大家好!我是付工。西门子、三菱、欧姆龙是我们自动化行业使用最多的三个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)); } }} 输出结果如下:

    03-14 162浏览
  • C++多线程内存模型(MemoryOrder)详解

    在多线程编程中, 有两个需要注意的问题, 一个是数据竞争, 另一个是内存执行顺序. 什么是数据竞争(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.

    03-06 174浏览
  • PLC程序中急停为什么要写成常开?

    在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点的硬件特性,人为的规定急停按钮接线应该是常闭点接入!---因为常闭点造成电路一直接通,所以程序中就要用常开点,这样才能保证不急停的时候,程序逻辑能接通!

    03-05 155浏览
  • 动态数码显示设计:灵动数字的视觉盛宴

    1. 如图4.13.1所示,P0端口接动态数码管的字形码笔段,P2端口接动态数码管的数位选择端,P1.7接一个开关,当开关接高电平时,显示“12345”字样;当开关接低电平时,显示“HELLO”字样。 2. 电路原理图   图4.13.1 3. 系...

    02-27 150浏览
  • 多路开关状态指示设计:掌控电路的智能之眼

    1. 如图4.3.1所示,AT89S51单片机的P1.0-P1.3接四个发光二极管L1-L4,P1.4-P1.7接了四个开关K1-K4,编程将开关的状态反映到发光二极管上。(开关闭合,对应的灯亮,开关断开,对应的灯灭)。 2. 电路原理图   图4.3.1...

    02-27 98浏览
  • 单片机C语言:程序在内存中的分布

    单片机C语言:程序在内存中的分布情况

    02-18 107浏览
  • 嵌入式软件为何要用状态机架构

    一、提高CPU使用效率 话说我只要见到满篇都是delay_ms()的程序就会头疼,动辄十几个ms几十个ms的软件延时是对CPU资源的巨大浪费,宝贵的CPU时间都浪费在了NOP指令上。 那种为了等待一个管脚电平跳变或者一个串口数据,让整个程序都不动的情况也让我非常纠结,如果事件一直不发生电平跳变,你要等到世界末日么? 如果应用状态机编程思想,程序只需要用全局变量记录下工作状态,就可以转头去干别的工作了,当然忙完那些活儿之后要再看看工作状态有没有变化。 只要目标事件(定时未到、电平没跳变、串口数据没收完)还没发生,工作状态就不会改变,程序就一直重复着“查询—干别的—查询—干别的”这样的循环,这样CPU就闲不下来了。 这种处理方法的实质就是在程序等待事件的过程中间隔性地插入一些有意义的工作,好让CPU不是一直无谓地等待。 二、逻辑完备性 逻辑完备性是状态机编程最大的优点。 不知道大家有没有用C语言写过计算器的小程序,我很早以前写过,写出来一测试,那个惨不忍睹啊! 当我规规矩矩的输入算式的时候,程序可以得到正确的计算结果,但要是故意输入数字和运算符号的随意组合,程序总是得出莫名其妙的结果。 后来我试着思维模拟一下程序的工作过程,正确的算式思路清晰,流程顺畅,可要碰上了不规矩的式子,走着走着我就晕菜了,那么多的标志位,那么多的变量,变来变去,最后直接分析不下去了。 很久之后我认识了状态机,才恍然明白,当时的程序是有逻辑漏洞的。 如果把这个计算器程序当做是一个反应式系统,那么一个数字或者运算符就可以看做一个事件,一个算式就是一组事件组合。 对于一个逻辑完备的反应式系统,不管什么样的事件组合,系统都能正确处理事件,而且系统自身的工作状态也一直处在可知可控的状态中。 反过来,如果一个系统的逻辑功能不完备,在某些特定事件组合的驱动下,系统就会进入一个不可知不可控的状态,与设计者的意图相悖。 状态机就能解决逻辑完备性的问题。 状态机是一种以系统状态为中心,以事件为变量的设计方法,它专注于各个状态的特点以及状态之间相互转换的关系。 状态的转换恰恰是事件引起的,那么在研究某个具体状态的时候,我们自然而然地会考虑任何一个事件对这个状态有什么样的影响。 这样,每一个状态中发生的每一个事件都会在我们的考虑之中,也就不会留下逻辑漏洞。 这样说也许大家会觉得太空洞,实践出真知,某天如果你真的要设计一个逻辑复杂的程序,会觉得状态机真香! 三、程序结构清晰 用状态机写出来的程序的结构是非常清晰的。 程序员最痛苦的事儿莫过于读别人写的代码,如果代码不是很规范,而且手里还没有流程图,读代码会让人晕了又晕,只有顺着程序一遍又一遍的看,很多遍之后才能隐约地明白程序大体的工作过程。 有流程图会好一点,但是如果程序比较大,流程图也不会画得多详细,很多细节上的过程还是要从代码中理解。 相比之下,用状态机写的程序要好很多,拿一张标准的UML状态转换图,再配上一些简明的文字说明,程序中的各个要素一览无余。 程序中有哪些状态,会发生哪些事件,状态机如何响应,响应之后跳转到哪个状态,这些都十分明朗,甚至许多动作细节都能从状态转换图中找到。 可以毫不夸张的说,有了UML状态转换图,程序流程图写都不用写。

    01-14 119浏览
  • 用状态机在STM32不同按键方式的应用

    常见的按键判定程序,如正点原子按键例程,只能判定单击事件,对于双击、长按等的判定逻辑较复杂,且使用main函数循环扫描的方式,容易被阻塞,或按键扫描函数会阻塞其他程序的执行。 使用定时器设计状态机可以规避这一问题。 功能介绍 本程序功能: 使用定时器状态机实现按键单击、双击、长按、连按功能。 消抖时间可调,长按时间可调,双击判定时间可调,连按单击间隔可调,可选择使能长按、连按、双击功能,无延时不阻塞,稳定触发。 移植只需修改读IO函数,结构体初始化和宏定义时间参数即可。 注: 在定时器状态机判定产生事件标志,在主函数处理并清除事件标志。 单击是最基本事件,除以下情况外,经过消抖后,在按键释放时触发单击事件。 使能长按后,若按键按下时间大于长按判定时间,则释放时触发长按事件,若不使能,释放时触发单击事件。 使能连按后,按住按键时持续触发连按事件,可自定义等效为单击事件。 无论是否使能长按,按键长按不释放,先经过长按判定时间触发第一次连按事件,然后循环进行连按计时,每次计时结束后都会触发一次连按事件,直到按键释放,触发长按事件(使能长按),或单击事件(不使能长按)。 使能双击后,若两次单击行为之间,由释放到按下的时间小于双击判定时间,则第一次单击行为释放时不触发单击事件,第二次单击行为在释放时触发双击事件。 一次单击行为在双击判定时间内无按键按下动作,之后才触发单击事件。 无论是否使能长按,若上述第二次行为是长按,则第二次释放时不会触发双击事件,而是到达长按判定时间后先触发属于第一次的单击事件,然后在第二次释放按键时触发长按事件(使能长按),或单击事件(不使能长按)。 代码 头文件 my_key.h #ifndef ___MY_KEY_H__#define ___MY_KEY_H__#include "main.h"#define ARR_LEN(arr) ((sizeof(arr)) / (sizeof(arr[0]))) //数组大小宏函数 #define KEY_DEBOUNCE_TIME 10 //消抖时间#define KEY_LONG_PRESS_TIME 500 //长按判定时间#define KEY_QUICK_CLICK_TIME 100 //连按时间间隔#define KEY_DOUBLE_CLICK_TIME 200 //双击判定时间#define KEY_PRESSED_LEVEL 0 //按键被按下时的电平 //按键动作typedef enum{ KEY_Action_Press, //按住 KEY_Action_Release, //松开} KEY_Action_TypeDef; //按键状态typedef enum{ KEY_Status_Idle, //空闲 KEY_Status_Debounce, //消抖 KEY_Status_ConfirmPress, //确认按下 KEY_Status_ConfirmPressLong, //确认长按 KEY_Status_WaitSecondPress, //等待再次按下 KEY_Status_SecondDebounce, //再次消抖 KEY_Status_SecondPress, //再次按下} KEY_Status_TypeDef; //按键事件typedef enum{ KEY_Event_Null, //空事件 KEY_Event_SingleClick, //单击 KEY_Event_LongPress, //长按 KEY_Event_QuickClick, //连击 KEY_Event_DoubleClick, //双击} KEY_Event_TypeDef; //按键模式使能选择typedef enum{ KEY_Mode_OnlySinge = 0x00, //只有单击 KEY_Mode_Long = 0x01, //单击长按 KEY_Mode_Quick = 0x02, //单击连按 KEY_Mode_Long_Quick = 0x03, //单击长按连按 KEY_Mode_Double = 0x04, //单击双击 KEY_Mode_Long_Double = 0x05, //单击长按双击 KEY_Mode_Quick_Double = 0x06, //单击连按双击 KEY_Mode_Long_Quick_Double = 0x07, //单击长按连按双击} KEY_Mode_TypeDef; //按键配置typedef struct{ uint8_t KEY_Label; //按键标号 KEY_Mode_TypeDef KEY_Mode; //按键模式 uint16_t KEY_Count; //按键按下计时 KEY_Action_TypeDef KEY_Action; //按键动作,按下或释放 KEY_Status_TypeDef KEY_Status; //按键状态 KEY_Event_TypeDef KEY_Event; //按键事件} KEY_Configure_TypeDef; extern KEY_Configure_TypeDef KeyConfig[];extern KEY_Event_TypeDef key_event[]; void KEY_ReadStateMachine(KEY_Configure_TypeDef *KeyCfg); #endif ‍源文件 my_key.c #include "my_key.h" static uint8_t KEY_ReadPin(uint8_t key_label){ switch (key_label) { case 0: return (uint8_t)HAL_GPIO_ReadPin(K0_GPIO_Port, K0_Pin); case 1: return (uint8_t)HAL_GPIO_ReadPin(K1_GPIO_Port, K1_Pin); case 2: return (uint8_t)HAL_GPIO_ReadPin(K2_GPIO_Port, K2_Pin); case 3: return (uint8_t)HAL_GPIO_ReadPin(K3_GPIO_Port, K3_Pin); case 4: return (uint8_t)HAL_GPIO_ReadPin(K4_GPIO_Port, K4_Pin); // case X: // return (uint8_t)HAL_GPIO_ReadPin(KX_GPIO_Port, KX_Pin); } return 0;} KEY_Configure_TypeDef KeyConfig[] = { {0, KEY_Mode_Long_Quick_Double, 0, KEY_Action_Release, KEY_Status_Idle, KEY_Event_Null}, {1, KEY_Mode_Long_Quick_Double, 0, KEY_Action_Release, KEY_Status_Idle, KEY_Event_Null}, {2, KEY_Mode_Long_Quick_Double, 0, KEY_Action_Release, KEY_Status_Idle, KEY_Event_Null}, {3, KEY_Mode_Long_Quick_Double, 0, KEY_Action_Release, KEY_Status_Idle, KEY_Event_Null}, {4, KEY_Mode_Long_Quick_Double, 0, KEY_Action_Release, KEY_Status_Idle, KEY_Event_Null}, // {X, KEY_Mode_Long_Quick_Double, 0, KEY_Action_Release, KEY_Status_Idle, KEY_Event_Null},}; KEY_Event_TypeDef key_event[ARR_LEN(KeyConfig)] = {KEY_Event_Null}; //按键事件//按键状态处理void KEY_ReadStateMachine(KEY_Configure_TypeDef *KeyCfg){ static uint16_t tmpcnt[ARR_LEN(KeyConfig)] = {0}; //按键动作读取 if (KEY_ReadPin(KeyCfg->KEY_Label) == KEY_PRESSED_LEVEL) KeyCfg->KEY_Action = KEY_Action_Press; else KeyCfg->KEY_Action = KEY_Action_Release; //状态机 switch (KeyCfg->KEY_Status) { //状态:空闲 case KEY_Status_Idle: if (KeyCfg->KEY_Action == KEY_Action_Press) //动作:按下 { KeyCfg->KEY_Status = KEY_Status_Debounce; //状态->消抖 KeyCfg->KEY_Event = KEY_Event_Null; //事件->无 } else //动作:默认动作,释放 { KeyCfg->KEY_Status = KEY_Status_Idle; //状态->维持 KeyCfg->KEY_Event = KEY_Event_Null; //事件->无 } break; //状态:消抖 case KEY_Status_Debounce: if ((KeyCfg->KEY_Action == KEY_Action_Press) && (KeyCfg->KEY_Count >= KEY_DEBOUNCE_TIME)) //动作:保持按下,消抖时间已到 { KeyCfg->KEY_Count = 0; //计数清零 KeyCfg->KEY_Status = KEY_Status_ConfirmPress; //状态->确认按下 KeyCfg->KEY_Event = KEY_Event_Null; //事件->无 } else if ((KeyCfg->KEY_Action == KEY_Action_Press) && (KeyCfg->KEY_Count < KEY_DEBOUNCE_TIME)) //动作:保持按下,消抖时间未到 { KeyCfg->KEY_Count++; //消抖计数 KeyCfg->KEY_Status = KEY_Status_Debounce; //状态->维持 KeyCfg->KEY_Event = KEY_Event_Null; //事件->无 } else //动作:释放,消抖时间未到,判定为抖动 { KeyCfg->KEY_Count = 0; //计数清零 KeyCfg->KEY_Status = KEY_Status_Idle; //状态->空闲 KeyCfg->KEY_Event = KEY_Event_Null; //事件->无 } break; //状态:确认按下 case KEY_Status_ConfirmPress: if ((KeyCfg->KEY_Action == KEY_Action_Press) && (KeyCfg->KEY_Count >= KEY_LONG_PRESS_TIME)) //动作:保持按下,长按时间已到 { KeyCfg->KEY_Count = KEY_QUICK_CLICK_TIME; //计数置数,生成第一次连按事件 KeyCfg->KEY_Status = KEY_Status_ConfirmPressLong; //状态->确认长按 KeyCfg->KEY_Event = KEY_Event_Null; //事件->无 } else if ((KeyCfg->KEY_Action == KEY_Action_Press) && (KeyCfg->KEY_Count < KEY_LONG_PRESS_TIME)) //动作:保持按下,长按时间未到 { KeyCfg->KEY_Count++; //长按计数 KeyCfg->KEY_Status = KEY_Status_ConfirmPress; //状态->维持 KeyCfg->KEY_Event = KEY_Event_Null; //事件->无 } else //动作:长按时间未到,释放 { if ((uint8_t)(KeyCfg->KEY_Mode) & 0x04) //双击模式 { KeyCfg->KEY_Count = 0; //计数清零 KeyCfg->KEY_Status = KEY_Status_WaitSecondPress; //状态->等待再按 KeyCfg->KEY_Event = KEY_Event_Null; //事件->无 } else //非双击模式 { KeyCfg->KEY_Count = 0; //计数清零 KeyCfg->KEY_Status = KEY_Status_Idle; //状态->空闲 KeyCfg->KEY_Event = KEY_Event_SingleClick; //事件->单击**** } } break; //状态:确认长按 case KEY_Status_ConfirmPressLong: if (KeyCfg->KEY_Action == KEY_Action_Press) //动作:保持按下 { if ((uint8_t)KeyCfg->KEY_Mode & 0x02) //连按模式 { if (KeyCfg->KEY_Count >= KEY_QUICK_CLICK_TIME) //连按间隔时间已到 { KeyCfg->KEY_Count = 0; //计数清零 KeyCfg->KEY_Status = KEY_Status_ConfirmPressLong; //状态->维持 KeyCfg->KEY_Event = KEY_Event_QuickClick; //事件->连按**** } else //连按间隔时间未到 { KeyCfg->KEY_Count++; //连按计数 KeyCfg->KEY_Status = KEY_Status_ConfirmPressLong; //状态->维持 KeyCfg->KEY_Event = KEY_Event_Null; //事件->无 } } else //非连按模式 { KeyCfg->KEY_Count = 0; //计数清零 KeyCfg->KEY_Status = KEY_Status_ConfirmPressLong; //状态->维持 KeyCfg->KEY_Event = KEY_Event_Null; //事件->无 } } else //动作:长按下后释放 { if ((uint8_t)KeyCfg->KEY_Mode & 0x01) //长按模式 { KeyCfg->KEY_Count = 0; //计数清零 KeyCfg->KEY_Status = KEY_Status_Idle; //状态->空闲 KeyCfg->KEY_Event = KEY_Event_LongPress; //事件->长按**** } else //非长按模式 { KeyCfg->KEY_Count = 0; //计数清零 KeyCfg->KEY_Status = KEY_Status_Idle; //状态->空闲 KeyCfg->KEY_Event = KEY_Event_SingleClick; //事件->单击**** } } break; //状态:等待是否再次按下 case KEY_Status_WaitSecondPress: if ((KeyCfg->KEY_Action != KEY_Action_Press) && (KeyCfg->KEY_Count >= KEY_DOUBLE_CLICK_TIME)) //动作:保持释放,双击等待时间已到 { KeyCfg->KEY_Count = 0; //计数清零 KeyCfg->KEY_Status = KEY_Status_Idle; //状态->空闲 KeyCfg->KEY_Event = KEY_Event_SingleClick; //事件->单击**** } else if ((KeyCfg->KEY_Action != KEY_Action_Press) && (KeyCfg->KEY_Count < KEY_DOUBLE_CLICK_TIME)) //动作:保持释放,双击等待时间未到 { KeyCfg->KEY_Count++; //双击等待计数 KeyCfg->KEY_Status = KEY_Status_WaitSecondPress; //状态->维持 KeyCfg->KEY_Event = KEY_Event_Null; //事件->无 } else //动作:双击等待时间内,再次按下 { tmpcnt[KeyCfg->KEY_Label] = KeyCfg->KEY_Count; //计数保存 KeyCfg->KEY_Count = 0; //计数清零 KeyCfg->KEY_Status = KEY_Status_SecondDebounce; //状态->再次消抖 KeyCfg->KEY_Event = KEY_Event_Null; //事件->无 } break; //状态:再次消抖 case KEY_Status_SecondDebounce: if ((KeyCfg->KEY_Action == KEY_Action_Press) && (KeyCfg->KEY_Count >= KEY_DEBOUNCE_TIME)) //动作:保持按下,消抖时间已到 { KeyCfg->KEY_Count = 0; //计数清零 KeyCfg->KEY_Status = KEY_Status_SecondPress; //状态->确认再次按下 KeyCfg->KEY_Event = KEY_Event_Null; //事件->无 } else if ((KeyCfg->KEY_Action == KEY_Action_Press) && (KeyCfg->KEY_Count < KEY_DEBOUNCE_TIME)) //动作:保持按下,消抖时间未到 { KeyCfg->KEY_Count++; //消抖计数 KeyCfg->KEY_Status = KEY_Status_SecondDebounce; //状态->维持 KeyCfg->KEY_Event = KEY_Event_Null; //事件->无 } else //动作:释放,消抖时间未到,判定为抖动 { KeyCfg->KEY_Count = KeyCfg->KEY_Count + tmpcnt[KeyCfg->KEY_Label]; //计数置数 KeyCfg->KEY_Status = KEY_Status_WaitSecondPress; //状态->等待再按 KeyCfg->KEY_Event = KEY_Event_Null; //事件->无 } break; //状态:再次按下 case KEY_Status_SecondPress: if ((KeyCfg->KEY_Action == KEY_Action_Press) && (KeyCfg->KEY_Count >= KEY_LONG_PRESS_TIME)) //动作:保持按下,长按时间已到 { KeyCfg->KEY_Count = 0; //计数清零 KeyCfg->KEY_Status = KEY_Status_ConfirmPressLong; //状态->确认长按 KeyCfg->KEY_Event = KEY_Event_SingleClick; //事件->先响应单击 } else if ((KeyCfg->KEY_Action == KEY_Action_Press) && (KeyCfg->KEY_Count < KEY_LONG_PRESS_TIME)) //动作:保持按下,长按时间未到 { KeyCfg->KEY_Count++; //计数 KeyCfg->KEY_Status = KEY_Status_SecondPress; //状态->维持 KeyCfg->KEY_Event = KEY_Event_Null; //事件->无 } else //动作:释放,长按时间未到 { KeyCfg->KEY_Count = 0; //计数清零 KeyCfg->KEY_Status = KEY_Status_Idle; //状态->空闲 KeyCfg->KEY_Event = KEY_Event_DoubleClick; //事件->双击 } break; } if (KeyCfg->KEY_Event != KEY_Event_Null) //事件记录 key_event[KeyCfg->KEY_Label] = KeyCfg->KEY_Event;} 定时器中断调用和主函数使用 中断周期为1ms //调用uint32_t tim_cnt = 0;void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){ if (htim->Instance == htim1.Instance) { tim_cnt++; if (tim_cnt % 1 == 0) // 1ms { KEY_ReadStateMachine(&KeyConfig[0]); KEY_ReadStateMachine(&KeyConfig[1]); KEY_ReadStateMachine(&KeyConfig[2]); KEY_ReadStateMachine(&KeyConfig[3]); KEY_ReadStateMachine(&KeyConfig[4]); } }} int main(void){ while (1) { if (key_event[1] == KEY_Event_SingleClick) //单击 { something1(); } if (key_event[2] == KEY_Event_LongPress) //长按 { something2(); } if ((key_event[3] == KEY_Event_QuickClick) || (key_event[3] == KEY_Event_SingleClick)) //连按 { something3(); } if (key_event[4] == KEY_Event_DoubleClick) //双击 { something4(); } memset(key_event, KEY_Event_Null, sizeof(key_event)); //清除事件 }}

    01-09 196浏览
正在努力加载更多...
广告