众所周知,GNU/GCC在标准的C/C++基础上做了有实用性的扩展, 零长度数组(Arrays of Length Zero) 就是其中一个知名的扩展. 多数情况下, 其应用在变长数组中, 其定义如下: struct Packet { int state; int len; char cData[0]; //这里的0长结构体就为变长结构体提供了非常好的支持 }; 首先对 0 长度数组, 也叫柔性数组,做一个解释 : 用途 : 长度为0的数组的主要用途是为了满足需要变长度的结构体; 用法 : 在一个结构体的最后,声明一个长度为 0 的数组, 就可以使得这个结构体是可变长的. 对于编译器来说, 此时长度为 0 的数组并不占用空间, 因为数组名本身不占空间, 它只是一个偏移量, 数组名这个符号本身代表了一个不可修改的地址常量 (注意 : 数组名永远都不会是指针!), 但对于这个数组的大小, 我们可以进行动态分配。 注意 :如果结构体是通过calloc、malloc或 者new等动态分配方式生成,在不需要时要释放相应的空间。 优点 :比起在结构体中声明一个指针变量、再进行动态分 配的办法,这种方法效率要高。因为在访问数组内容时,不需要间接访问,避免了两次访存。 缺点 :在结构体中,数组为 0 的数组必须在最后声明,使用上有一定限制。 对于编译器而言, 数组名仅仅是一个符号, 它不会占用任何空间, 它在结构体中, 只是代表了一个偏移量, 代表一个不可修改的地址常量! 0 长度数组的用途: 我们设想这样一个场景, 我们在网络通信过程中使用的数据缓冲区, 缓冲区包括一个len字段和data字段, 分别标识数据的长度和传输的数据, 我们常见的有几种设计思路: 定长数据缓冲区, 设置一个足够大小MAX_LENGTH的数据缓冲区 设置一个指向实际数据的指针, 每次使用时, 按照数据的长度动态的开辟数据缓冲区的空间 我们从实际场景中应用的设计来考虑他们的优劣. 主要考虑的有, 缓冲区空间的开辟、释放和访问。 1、定长包(开辟空间, 释放, 访问): 比如我要发送 1024 字节的数据, 如果用定长包, 假设定长包的长度MAX_LENGTH为 2048, 就会浪费 1024 个字节的空间, 也会造成不必要的流量浪费: 数据结构定义: // 定长缓冲区 struct max_buffer { int len; char data[MAX_LENGTH]; }; 数据结构大小:考虑对齐, 那么数据结构的大小 >=sizeof(int) + sizeof(char) * MAX_LENGTH 由于考虑到数据的溢出, 变长数据包中的data数组长度一般会设置得足够长足以容纳最大的数据, 因此max_buffer中的 data 数组很多情况下都没有填满数据, 因此造成了浪费。 数据包的构造:假如我们要发送CURR_LENGTH = 1024个字节, 我们如何构造这个数据包呢;一般来说, 我们会返回一个指向缓冲区数据结构max_buffer的指针: // 开辟 if ((mbuffer = (struct max_buffer *)malloc(sizeof(struct max_buffer))) != NULL) { mbuffer->len = CURR_LENGTH; memcpy(mbuffer->data, "Hello World", CURR_LENGTH); printf("%d, %s\n", mbuffer->len, mbuffer->data); } 访问:这段内存要分两部分使用;前部分 4 个字节p->len, 作为包头(就是多出来的那部分),这个包头是用来描述紧接着包头后面的数据部分的长度,这里是 1024, 所以前四个字节赋值为 1024 (既然我们要构造不定长数据包,那么这个包到底有多长呢,因此,我们就必须通过一个变量来表明这个数据包的长度,这就是len的作用);而紧接其后的内存是真正的数据部分, 通过p->data, 最后, 进行一个memcpy()内存拷贝, 把要发送的数据填入到这段内存当中 释放:那么当使用完毕释放数据的空间的时候, 直接释放就可以了 // 销毁 free(mbuffer); mbuffer = NULL; 2、小结: 使用定长数组, 作为数据缓冲区, 为了避免造成缓冲区溢出, 数组的大小一般设为足够的空间MAX_LENGTH, 而实际使用过程中, 达到MAX_LENGTH长度的数据很少, 那么多数情况下, 缓冲区的大部分空间都是浪费掉的 但是使用过程很简单, 数据空间的开辟和释放简单, 无需程序员考虑额外的操作 3、 指针数据包(开辟空间, 释放, 访问): 如果你将上面的长度为MAX_LENGTH的定长数组换为指针, 每次使用时动态的开辟CURR_LENGTH大小的空间, 那么就避免造成MAX_LENGTH - CURR_LENGTH空间的浪费, 只浪费了一个指针域的空间: 数据包定义: struct point_buffer { int len; char *data; }; 数据结构大小:考虑对齐, 那么数据结构的大小 >=sizeof(int) + sizeof(char *) 空间分配:但是也造成了使用在分配内存时,需采用两步 // ===================== // 指针数组 占用-开辟-销毁 // ===================== /// 占用 printf("the length of struct test3:%d\n",sizeof(struct point_buffer)); /// 开辟 if ((pbuffer = (struct point_buffer *)malloc(sizeof(struct point_buffer))) != NULL) { pbuffer->len = CURR_LENGTH; if ((pbuffer->data = (char *)malloc(sizeof(char) * CURR_LENGTH)) != NULL) { memcpy(pbuffer->data, "Hello World", CURR_LENGTH); printf("%d, %s\n", pbuffer->len, pbuffer->data); } } 首先, 需为结构体分配一块内存空间;其次再为结构体中的成员变量分配内存空间。 这样两次分配的内存是不连续的, 需要分别对其进行管理. 当使用长度为的数组时, 则是采用一次分配的原则, 一次性将所需的内存全部分配给它。 释放:相反, 释放时也是一样的: /// 销毁 free(pbuffer->data); free(pbuffer); pbuffer = NULL; 小结: - 使用指针结果作为缓冲区, 只多使用了一个指针大小的空间, 无需使用 MAX_LENGTH 长度的数组, 不会造成空间的大量浪费。 但那是开辟空间时, 需要额外开辟数据域的空间, 施放时候也需要显示释放数据域的空间, 但是实际使用过程中, 往往在函数中开辟空间, 然后返回给使用者指向struct point_buffer的指针, 这时候我们并不能假定使用者了解我们开辟的细节, 并按照约定的操作释放空间, 因此使用起来多有不便, 甚至造成内存泄漏。 4、变长数据缓冲区(开辟空间, 释放, 访问) 定长数组使用方便, 但是却浪费空间, 指针形式只多使用了一个指针的空间, 不会造成大量空间分浪费, 但是使用起来需要多次分配, 多次释放, 那么有没有一种实现方式能够既不浪费空间, 又使用方便的呢? GNU C的 0 长度数组, 也叫变长数组, 柔性数组就是这样一个扩展。对于 0 长数组的这个特点,很容易构造出变成结构体,如缓冲区,数据包等等: 数据结构定义: // 0长度数组 struct zero_buffer { int len; char data[0]; }; 数据结构大小:这样的变长数组常用于网络通信中构造不定长数据包, 不会浪费空间浪费网络流量, 因为char data[0];只是个数组名, 是不占用存储空间的: sizeof(struct zero_buffer) = sizeof(int) 开辟空间:那么我们使用的时候, 只需要开辟一次空间即可 /// 开辟 if ((zbuffer = (struct zero_buffer *)malloc(sizeof(struct zero_buffer) + sizeof(char) * CURR_LENGTH)) != NULL) { zbuffer->len = CURR_LENGTH; memcpy(zbuffer->data, "Hello World", CURR_LENGTH); printf("%d, %s\n", zbuffer->len, zbuffer->data); } 释放空间:释放空间也是一样的, 一次释放即可 /// 销毁 free(zbuffer); zbuffer = NULL; 总结: // zero_length_array.c #include #include #define MAX_LENGTH 1024 #define CURR_LENGTH 512 // 0长度数组 struct zero_buffer { int len; char data[0]; }__attribute((packed)); // 定长数组 struct max_buffer { int len; char data[MAX_LENGTH]; }__attribute((packed)); // 指针数组 struct point_buffer { int len; char *data; }__attribute((packed)); int main(void) { struct zero_buffer *zbuffer = NULL; struct max_buffer *mbuffer = NULL; struct point_buffer *pbuffer = NULL; // ===================== // 0长度数组 占用-开辟-销毁 // ===================== /// 占用 printf("the length of struct test1:%d\n",sizeof(struct zero_buffer)); /// 开辟 if ((zbuffer = (struct zero_buffer *)malloc(sizeof(struct zero_buffer) + sizeof(char) * CURR_LENGTH)) != NULL) { zbuffer->len = CURR_LENGTH; memcpy(zbuffer->data, "Hello World", CURR_LENGTH); printf("%d, %s\n", zbuffer->len, zbuffer->data); } /// 销毁 free(zbuffer); zbuffer = NULL; // ===================== // 定长数组 占用-开辟-销毁 // ===================== /// 占用 printf("the length of struct test2:%d\n",sizeof(struct max_buffer)); /// 开辟 if ((mbuffer = (struct max_buffer *)malloc(sizeof(struct max_buffer))) != NULL) { mbuffer->len = CURR_LENGTH; memcpy(mbuffer->data, "Hello World", CURR_LENGTH); printf("%d, %s\n", mbuffer->len, mbuffer->data); } /// 销毁 free(mbuffer); mbuffer = NULL; // ===================== // 指针数组 占用-开辟-销毁 // ===================== /// 占用 printf("the length of struct test3:%d\n",sizeof(struct point_buffer)); /// 开辟 if ((pbuffer = (struct point_buffer *)malloc(sizeof(struct point_buffer))) != NULL) { pbuffer->len = CURR_LENGTH; if ((pbuffer->data = (char *)malloc(sizeof(char) * CURR_LENGTH)) != NULL) { memcpy(pbuffer->data, "Hello World", CURR_LENGTH); printf("%d, %s\n", pbuffer->len, pbuffer->data); } } /// 销毁 free(pbuffer->data); free(pbuffer); pbuffer = NULL; return EXIT_SUCCESS; } 长度为 0 的数组并不占有内存空间, 而指针方式需要占用内存空间. 对于长度为 0 数组, 在申请内存空间时, 采用一次性分配的原则进行; 对于包含指针的结构体, 在申请空间时需分别进行, 释放时也需分别释放. 对于长度为 0 的数组的访问可采用数组方式进行 GNU Document中 变长数组的支持: 参考: 6.17 Arrays of Length Zero C Struct Hack – Structure with variable length array 在C90之前, 并不支持 0 长度的数组, 0 长度数组是GNU C的一个扩展, 因此早期的编译器中是无法通过编译的;对于GNU C增加的扩展,GCC提供了编译选项来明确的标识出他们: -pedantic选项,那么使用了扩展语法的地方将产生相应的警告信息 -Wall使用它能够使GCC产生尽可能多的警告信息 -Werror, 它要求GCC将所有的警告当成错误进行处理 // 1.c #include #include int main(void) { char a[0]; printf("%ld", sizeof(a)); return EXIT_SUCCESS; } 我们来编译: # 显示所有警告 gcc 1.c -Wall #none warning and error # 对GNU C的扩展显示警告 gcc 1.c -Wall -pedantic 1.c: In function ‘main’: 1.c:7: warning: ISO C forbids zero-size array ‘a’ # 显示所有警告同时GNU C的扩展显示警告, 将警告用 error 显示 gcc 1.c -Werror -Wall -pedantic cc1: warnings being treated as errors 1.c: In function ‘main’: 1.c:7: error: ISO C forbids zero-size array ‘a’ 0长度数组其实就是灵活地运用数组指向的是其后面连续的内存空间: struct buffer { int len; char data[0]; }; 在早期没引入 0 长度数组的时候, 大家是通过定长数组和指针的方式来解决的, 但是: 定长数组定义了一个足够大的缓冲区, 这样使用方便, 但是每次都造成空间的浪费 指针的方式, 要求程序员在释放空间时必须进行多次的free操作, 而我们在使用的过程中往往在函数中返回了指向缓冲区的指针, 我们并不能保证每个人都理解并遵从我们的释放方式。 所以GNU就对其进行了 0 长度数组的扩展. 当使用data[0]的时候, 也就是 0 长度数组的时候,0长度数组作为数组名, 并不占用存储空间。 在C99之后,也加了类似的扩展,只不过用的是char payload[]这种形式(所以如果你在编译的时候确实需要用到-pedantic参数,那么你可以将char payload[0]类型改成char payload[], 这样就可以编译通过了,当然你的编译器必须支持C99标准的,如果太古老的编译器,那可能不支持了) // 2.c payload #include # include struct payload { int len; char data[]; }; int main(void) { struct payload pay; printf("%ld", sizeof(pay)); return EXIT_SUCCESS; } 使用-pedantic编译后, 不出现警告, 说明这种语法是 C 标准的 gcc 2.c -pedantic -std=c99 所以结构体的末尾, 就是指向了其后面的内存数据。因此我们可以很好的将该类型的结构体作为数据报文的头格式,并且最后一个成员变量,也就刚好是数据内容了. GNU 手册还提供了另外两个结构体来说明,更容易看懂意思: struct f1 { int x; int y[]; } f1 = { 1, { 2, 3, 4 } }; struct f2 { struct f1 f1; int data[3]; } f2 = { { 1 }, { 5, 6, 7 } }; 我把 f2 里面的 2,3,4 改成了 5,6,7 以示区分。如果你把数据打出来。即如下的信息: f1.x = 1 f1.y[0] = 2 f1.y[1] = 3 f1.y[2] = 4 也就是f1.y指向的是{2,3,4}这块内存中的数据。所以我们就可以轻易的得到,f2.f1.y指向的数据也就是正好f2.data的内容了。打印出来的数据: f2.f1.x = 1 f2.f1.y[0] = 5 f2.f1.y[1] = 6 f2.f1.y[2] = 7 如果你不是很确认其是否占用空间. 你可以用sizeof来计算一下。就可以知道sizeof(struct f1)=4,也就是int y[]其实是不占用空间的。但是这个 0 长度的数组,必须放在结构体的末尾。如果你没有把它放在末尾的话。编译的时候,会有如下的错误: main.c:37:9: error: flexible array member not at end of struct int y[]; ^ 到这边,你可能会有疑问,如果将struct f1中的int y[]替换成int *y,又会是如何?这就涉及到数组和指针的问题了. 有时候吧,这两个是一样的,有时候又有区别。 首先要说明的是,支持 0 长度数组的扩展,重点在数组,也就是不能用int *y指针来替换。sizeof的长度就不一样了。把struct f1改成这样: struct f3 { int x; int *y; }; 在 32/64 位下, int 均是 4 个字节,sizeof(struct f1)=4,而sizeof(struct f3)=16 因为int *y是指针, 指针在 64 位下, 是 64 位的,sizeof(struct f3) = 16;如果在32位环境的话,sizeof(struct f3)则是 8 了,sizeof(struct f1)不变. 所以int *y是不能替代int y[]的; 代码如下: // 3.c #include #include struct f1 { int x; int y[]; } f1 = { 1, { 2, 3, 4 } }; struct f2 { struct f1 f1; int data[3]; } f2 = { { 1 }, { 5, 6, 7 } }; struct f3 { int x; int *y; }; int main(void) { printf("sizeof(f1) = %d\n", sizeof(struct f1)); printf("sizeof(f2) = %d\n", sizeof(struct f2)); printf("szieof(f3) = %d\n\n", sizeof(struct f3)); printf("f1.x = %d\n", f1.x); printf("f1.y[0] = %d\n", f1.y[0]); printf("f1.y[1] = %d\n", f1.y[1]); printf("f1.y[2] = %d\n", f1.y[2]); printf("f2.f1.x = %d\n", f1.x); printf("f2.f1.y[0] = %d\n", f2.f1.y[0]); printf("f2.f1.y[1] = %d\n", f2.f1.y[1]); printf("f2.f1.y[2] = %d\n", f2.f1.y[2]); return EXIT_SUCCESS; } 0 长度数组的其他特征: 1、为什么 0 长度数组不占用存储空间: 0 长度数组与指针实现有什么区别呢, 为什么0长度数组不占用存储空间呢? 其实本质上涉及到的是一个 C 语言里面的数组和指针的区别问题。char a[1]里面的a和char *b的b相同吗? 《 Programming Abstractions in C》(Roberts, E. S.,机械工业出版社,2004.6)82页里面说: “arr is defined to be identical to &arr[0]”. 也就是说,char a[1]里面的a实际是一个常量,等于&a[0]。而char *b是有一个实实在在的指针变量b存在。所以,a=b是不允许的,而b=a是允许的。两种变量都支持下标式的访问,那么对于a[0]和b[0]本质上是否有区别?我们可以通过一个例子来说明。 参见如下两个程序gdb_zero_length_array.c和gdb_zero_length_array.c: // gdb_zero_length_array.c #include #include struct str { int len; char s[0]; }; struct foo { struct str *a; }; int main(void) { struct foo f = { NULL }; printf("sizeof(struct str) = %d\n", sizeof(struct str)); printf("before f.a->s.\n"); if(f.a->s) { printf("before printf f.a->s.\n"); printf(f.a->s); printf("before printf f.a->s.\n"); } return EXIT_SUCCESS; } \ // gdb_pzero_length_array.c #include # include struct str { int len; char *s; }; struct foo { struct str *a; }; int main(void) { struct foo f = { NULL }; printf("sizeof(struct str) = %d\n", sizeof(struct str)); printf("before f.a->s.\n"); if (f.a->s) { printf("before printf f.a->s.\n"); printf(f.a->s); printf("before printf f.a->s.\n"); } return EXIT_SUCCESS; } 可以看到这两个程序虽然都存在访问异常, 但是段错误的位置却不同 我们将两个程序编译成汇编, 然后diff查看他们的汇编代码有何不同 gcc -S gdb_zero_length_array.c -o gdb_test.s gcc -S gdb_pzero_length_array.c -o gdb_ptest diff gdb_test.s gdb_ptest.s 1c1 < .file "gdb_zero_length_array.c" --- > .file "gdb_pzero_length_array.c" 23c23 < movl $4, %esi --- > movl $16, %esi 30c30 < addq $4, %rax --- > movq 8(%rax), %rax 36c36 < addq $4, %rax --- > movq 8(%rax), %rax # printf("sizeof(struct str) = %d\n", sizeof(struct str)); 23c23 < movl $4, %esi #printf("sizeof(struct str) = %d\n", sizeof(struct str)); --- > movl $16, %esi #printf("sizeof(struct str) = %d\n", sizeof(struct str)); 从 64 位系统中, 汇编我们看出, 变长数组结构的大小为 4, 而指针形式的结构大小为 16: f.a->s 30c30/36c36 < addq $4, %rax --- > movq 8(%rax), %rax 可以看到有: 对于char s[0]来说, 汇编代码用了addq指令,addq $4, %rax 对于char *s来说,汇编代码用了movq指令,movq 8(%rax), %rax addq对%rax + sizeof(struct str), 即str结构的末尾即是char s[0]的地址, 这一步只是拿到了其地址, 而movq则是把地址里的内容放进去, 因此有时也被翻译为leap指令, 参见下一例子 从这里可以看到, 访问成员数组名其实得到的是数组的相对地址, 而访问成员指针其实是相对地址里的内容(这和访问其它非指针或数组的变量是一样的): 访问相对地址,程序不会crash,但是,访问一个非法的地址中的内容,程序就会crash。 // 4-1.c #include #include int main(void) { char *a; printf("%p\n", a); return EXIT_SUCCESS; } 4-2.c #include #include int main(void) { char a[0]; printf("%p\n", a); return EXIT_SUCCESS; } $ diff 4-1.s 4-2.s 1c1 < .file "4-1.c" --- > .file "4-2.c" 13c13 < subl $16, %esp --- > subl $32, %esp 15c15 < leal 16(%esp), %eax --- > movl 28(%esp), %eax 对于char a[0]来说, 汇编代码用了leal指令,leal 16(%esp), %eax: 对于char *a来说,汇编代码用了movl指令,movl 28(%esp), %eax 2、地址优化: // 5-1.c #include #include int main(void) { char a[0]; printf("%p\n", a); char b[0]; printf("%p\n", b); return EXIT_SUCCESS; } img 由于 0 长度数组是 GNU C 的扩展, 不被标准库任可, 那么一些巧妙编写的诡异代码, 其执行结果就是依赖于编译器和优化策略的实现的. 比如上面的代码, a 和 b 的地址就会被编译器优化到一处, 因为a[0]和b[0]对于程序来说是无法使用的, 这让我们想到了什么? 编译器对于相同字符串常量, 往往地址也是优化到一处, 减少空间占用: // 5-2.c #include #include int main(void) { const char *a = "Hello"; printf("%p\n", a); const char *b = "Hello"; printf("%p\n", b); const char c[] = "Hello"; printf("%p\n", c); return EXIT_SUCCESS; }
1、程序框架的重要性 很多人尤其是初学者在写代码的时候往往都是想一点写一点,最开始没有一个整体的规划,导致后面代码越写越乱,bug不断。 最终代码跑起来看似没有问题(有可能也真的没有问题),但是要加一个功能的时候会浪费大量的时间,甚至导致整个代码的崩溃。 所以,在一个项目开始的时候多花一些时间在代码的架构设计上是十分有必要的。 代码架构确定好了之后你会发现敲代码的时候会特别快,并且在后期调试的时候也不会像无头苍蝇一样胡乱找问题。当然,调试也是一门技术。 在学习实时操作系统的过程中,发现实时操作系统框架与个人的业务代码之间的耦合性就非常低,都是只需要将业务代码通过一定的接口函数注册好后就交给操作系统托管了,十分方便。 但是操作系统的调度过于复杂,这里就使用操作系统的思维方式来重构这个时间片轮询框架。 实现该框架的完全解耦,用户只需要包含头文件,并且在使用过程中不需要改动已经写好的库文件。 2、程序实例 首先来个demo,该demo是使用电脑开两个线程: 一个线程模拟单片机的定时器中断产生时间片轮询个时钟,另一个线程则模拟主函数中一直运行的时间片轮询调度程序。 #include #include #include #include "timeslice.h" // 创建5个任务对象TimesilceTaskObj task_1, task_2, task_3, task_4, task_5; // 具体的任务函数void task1_hdl(){ printf(">> task 1 is running ...\n");} void task2_hdl(){ printf(">> task 2 is running ...\n");} void task3_hdl(){ printf(">> task 3 is running ...\n");} void task4_hdl(){ printf(">> task 4 is running ...\n");} void task5_hdl(){ printf(">> task 5 is running ...\n");} // 初始化任务对象,并且将任务添加到时间片轮询调度中void task_init(){ timeslice_task_init(&task_1, task1_hdl, 1, 10); timeslice_task_init(&task_2, task2_hdl, 2, 20); timeslice_task_init(&task_3, task3_hdl, 3, 30); timeslice_task_init(&task_4, task4_hdl, 4, 40); timeslice_task_init(&task_5, task5_hdl, 5, 50); timeslice_task_add(&task_1); timeslice_task_add(&task_2); timeslice_task_add(&task_3); timeslice_task_add(&task_4); timeslice_task_add(&task_5);} // 开两个线程模拟在单片机上的运行过程void timeslice_exec_thread(){ while (true) { timeslice_exec(); }} void timeslice_tick_thread(){ while (true) { timeslice_tick(); Sleep(10); }} int main(){ task_init(); printf(">> task num: %d\n", timeslice_get_task_num()); printf(">> task len: %d\n", timeslice_get_task_timeslice_len(&task_3)); timeslice_task_del(&task_2); printf(">> delet task 2\n"); printf(">> task 2 is exist: %d\n", timeslice_task_isexist(&task_2)); printf(">> task num: %d\n", timeslice_get_task_num()); timeslice_task_del(&task_5); printf(">> delet task 5\n"); printf(">> task num: %d\n", timeslice_get_task_num()); printf(">> task 3 is exist: %d\n", timeslice_task_isexist(&task_3)); timeslice_task_add(&task_2); printf(">> add task 2\n"); printf(">> task 2 is exist: %d\n", timeslice_task_isexist(&task_2)); timeslice_task_add(&task_5); printf(">> add task 5\n"); printf(">> task num: %d\n", timeslice_get_task_num()); printf("\n\n========timeslice running===========\n"); std::thread thread_1(timeslice_exec_thread); std::thread thread_2(timeslice_tick_thread); thread_1.join(); thread_2.join(); return 0;} 运行结果如下: 由以上例子可见,这个框架使用十分方便,甚至可以完全不知道其原理,仅仅通过几个简单的接口就可以迅速创建任务并加入到时间片轮询的框架中,十分好用。 3、时间片轮询框架 其实该部分主要使用了面向对象的思维,使用结构体作为对象,并使用结构体指针作为参数传递,这样作可以节省资源,并且有着极高的运行效率。 其中最难的部分是侵入式链表的使用,这种链表在一些操作系统内核中使用十分广泛,这里是参考RT-Thread实时操作系统中的侵入式链表实现。h文件: #ifndef _TIMESLICE_H#define _TIMESLICE_H #include "./list.h" typedef enum { TASK_STOP, TASK_RUN} IsTaskRun; typedef struct timesilce{ unsigned int id; void (*task_hdl)(void); IsTaskRun is_run; unsigned int timer; unsigned int timeslice_len; ListObj timeslice_task_list;} TimesilceTaskObj; void timeslice_exec(void);void timeslice_tick(void);void timeslice_task_init(TimesilceTaskObj* obj, void (*task_hdl)(void), unsigned int id, unsigned int timeslice_len);void timeslice_task_add(TimesilceTaskObj* obj);void timeslice_task_del(TimesilceTaskObj* obj);unsigned int timeslice_get_task_timeslice_len(TimesilceTaskObj* obj);unsigned int timeslice_get_task_num(void);unsigned char timeslice_task_isexist(TimesilceTaskObj* obj); #endif c文件: #include "./timeslice.h" static LIST_HEAD(timeslice_task_list); void timeslice_exec(){ ListObj* node; TimesilceTaskObj* task; list_for_each(node, ×lice_task_list) { task = list_entry(node, TimesilceTaskObj, timeslice_task_list); if (task->is_run == TASK_RUN) { task->task_hdl(); task->is_run = TASK_STOP; } }} void timeslice_tick(){ ListObj* node; TimesilceTaskObj* task; list_for_each(node, ×lice_task_list) { task = list_entry(node, TimesilceTaskObj, timeslice_task_list); if (task->timer != 0) { task->timer--; if (task->timer == 0) { task->is_run = TASK_RUN; task->timer = task->timeslice_len; } } }} unsigned int timeslice_get_task_num(){ return list_len(×lice_task_list);} void timeslice_task_init(TimesilceTaskObj* obj, void (*task_hdl)(void), unsigned int id, unsigned int timeslice_len){ obj->id = id; obj->is_run = TASK_STOP; obj->task_hdl = task_hdl; obj->timer = timeslice_len; obj->timeslice_len = timeslice_len;} void timeslice_task_add(TimesilceTaskObj* obj){ list_insert_before(×lice_task_list, &obj->timeslice_task_list);} void timeslice_task_del(TimesilceTaskObj* obj){ if (timeslice_task_isexist(obj)) list_remove(&obj->timeslice_task_list); else return;} unsigned char timeslice_task_isexist(TimesilceTaskObj* obj){ unsigned char isexist = 0; ListObj* node; TimesilceTaskObj* task; list_for_each(node, ×lice_task_list) { task = list_entry(node, TimesilceTaskObj, timeslice_task_list); if (obj->id == task->id) isexist = 1; } return isexist;} unsigned int timeslice_get_task_timeslice_len(TimesilceTaskObj* obj){ return obj->timeslice_len;} 4、底层侵入式双向链表 该链表是linux内核中使用十分广泛,也十分经典,其原理具体可以参考文章:https://www.cnblogs.com/skywang12345/p/3562146.htmlh文件: #ifndef _LIST_H#define _LIST_H #define offset_of(type, member) (unsigned long) &((type*)0)->member#define container_of(ptr, type, member) ((type *)((char *)(ptr) - offset_of(type, member))) typedef struct list_structure{ struct list_structure* next; struct list_structure* prev;} ListObj; #define LIST_HEAD_INIT(name) {&(name), &(name)}#define LIST_HEAD(name) ListObj name = LIST_HEAD_INIT(name) void list_init(ListObj* list);void list_insert_after(ListObj* list, ListObj* node);void list_insert_before(ListObj* list, ListObj* node);void list_remove(ListObj* node);int list_isempty(const ListObj* list);unsigned int list_len(const ListObj* list); #define list_entry(node, type, member) \ container_of(node, type, member) #define list_for_each(pos, head) \ for (pos = (head)->next; pos != (head); pos = pos->next) #define list_for_each_safe(pos, n, head) \ for (pos = (head)->next, n = pos->next; pos != (head); \ pos = n, n = pos->next) #endif c文件: #include "list.h" void list_init(ListObj* list){ list->next = list->prev = list;} void list_insert_after(ListObj* list, ListObj* node){ list->next->prev = node; node->next = list->next; list->next = node; node->prev = list;} void list_insert_before(ListObj* list, ListObj* node){ list->prev->next = node; node->prev = list->prev; list->prev = node; node->next = list;} void list_remove(ListObj* node){ node->next->prev = node->prev; node->prev->next = node->next; node->next = node->prev = node;} int list_isempty(const ListObj* list){ return list->next == list;} unsigned int list_len(const ListObj* list){ unsigned int len = 0; const ListObj* p = list; while (p->next != list) { p = p->next; len++; } return len;} 到此,一个全新的,完全解耦的,十分方便易用时间片轮询框架完成。
有一些人虽然工作了很多年,但工作表现就像刚入行的新人。他们几乎不学习软件开发的基础知识 。除了最初几年有所成长,后期一直停滞不前,而且他们不明白为什么。 与此同时,我也曾与一些只有几年工作经验的开发人员共事,他们表现出惊人的增长潜力。他们工作态度端正,并且明白如何避免不称职的行为。 根据开发人员的某些习惯,可以非常明显地分辨出谁更专业,谁更业余。让我们深入剖析下业余程序开发人员的几种表现,每个程序开发人员都应该引以为戒,这些错误会阻碍我们的职业发展。 一次性提交大量代码 回忆下,你是否碰到过一次性提交大量代码的人,你都不想给他做代码评审。是的,不专业的开发人员就会这样做。他们会在一次代码评审请求中包含多个模块的修改,而且会催促你优先评审他们的代码。 是啊,能不急吗,排到后边,还需要解决代码冲突的问题。这个问题在很多高级开发工程师中也存在,他们在功能开发期间不做任何提交,只有在功能彻底完工后,才会提交所有修改,于是代码评审中的任何意见都会引起大量的修改。 当我碰到这种代码评审请求时,我首先做的是要求提交者按功能模块将其拆分成多个小的请求。 我只会对 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通讯模块的状态灯并熟悉模块的拆装方法。相信你会发现,动手实践是最好的学习方式!