1. extern unsigned int TimerCount; //这里漏掉了类型限定符volatile
在模块 B 中,变量用于精确的软件延迟:
1. #include “…A.h” //首先包含模块A的头文件
2. //其他代码
3. TimerCount=0;
4. while(TimerCount<=TIMER_VALUE); //延时一段时间(感谢网友chhfish指出这里的逻辑错误)
5. //其他代码
事实上,这是一个无限循环。 由于在模块A的头文件中声明变量时缺少限定符,因此在模块B中,该变量被视为int类型变量。 由于寄存器比 RAM 快得多,因此在使用非限定变量时,编译器首先将变量从 RAM 复制到寄存器。 如果在同一个代码块中再次使用该变量,则不再从 RAM 中复制数据,而是直接注册备份值后再使用。 代码 while( 结构定义为 或 用零填充;
II> 堆栈或堆上的结构,例如用() 或auto 定义的结构,将填充先前存储在这些内存位置中的任何内容。 您不能使用 () 来比较以这种方式定义的填充结构!
编译器不会优化声明为类型的数据;
__nop():延迟一个指令周期,编译器永远不会对其进行优化。 如果硬件支持NOP指令,则该语句被替换为NOP指令。 如果硬件不支持NOP指令,编译器会将其替换为与NOP等效的指令。 具体指令由编译器自己决定;
(n):指示编译器在 n 字节边界上对齐变量。 对于局部变量,n的值为1、2、4、8;
((at())):可以使用该变量属性来指定变量的绝对地址;
:在合理情况下提示编译器内联编译C或C++函数;
3.4.2 初始化的全局变量和静态变量的初始值放在哪里?
我们程序中的一些全局变量和静态变量在定义的时候就被初始化了。 经过编译器编译后,这些初始值存储在代码中的哪里呢? 让我们举个例子:
1. unsigned int g_unRunFlag=0xA5;
2. static unsigned int s_unCountFlag=0x5A;
我曾经做过一个项目,项目中的一个设备需要在线编程,即通过协议,将上位机发送给设备的数据通过在应用编程(IAP ) 技术。 我对内部Flash进行了划分,一小部分运行程序,大部分用来存储上位机发来的数据。 随着程序量的增加,更新程序后发现,在线编程后,设备运行正常,但重启设备后,出现故障! 经过一系列排查,发现失败的原因是更改了一个全局变量的初始值。 这是一件非常不可思议的事情。 你在定义这个变量的时候就指定了初始值,但是当你第一次使用这个变量的时候,你发现初始值已经改变了! 没有对该变量进行赋值操作,其他变量也没有溢出。 多次在线调试发现,进入main函数时,变量的初始值已经改为常量值。
如果想知道全局变量的初始值为什么会改变,就需要了解编译后这些初始值放在了二进制文件的什么位置。 在此之前,您需要了解一点链接原理。
ARM镜像文件的各个组成部分在存储系统中的地址有两种:一种是镜像文件位于内存时的地址(通俗地说就是存储在Flash中的二进制代码),称为加载地址; 镜像文件运行时(通俗的说就是板子上电,开始运行Flash中的程序)时的地址,称为运行时地址。 全局变量和被赋予初始值的静态变量的初始值在程序运行前就被放置在Flash中。 此时它们的地址称为加载地址。 当程序运行时,这些初始值将从Flash中复制。 到 RAM 中,这是运行时地址。
事实证明,对于程序中被赋予初始值的全局变量和静态变量,程序编译后,MDK将这些初始值放在Flash中,位于紧邻可执行代码的后面。 在程序进入main函数之前,会运行一段库代码,将这部分数据复制到相应的RAM位置。 由于我的设备程序数量不断增加,超过了为设备程序保留的Flash空间。 在线编程时,对Flash中存储全局变量和静态变量初始值的部分进行了重新编程。 在重启设备之前,初始值已经被复制到RAM中,因此此时程序运行正常。 但再次上电后,这部分初始值实际上是在线编程数据,自然与初始值不同。
3.4.3 对于C代码中使用的变量,编译器将它们分配到RAM的哪里?
我们在代码中会用到各种变量,比如全局变量、静态变量、局部变量,而这些变量都是由编译器统一管理的。 有时我们需要知道变量使用了多少RAM,以及这些变量在RAM中的位置。 具体位置。 这是经常遇到的事情。 例如,如果程序中的某个变量在运行时总是发生异常变化,那么就有理由怀疑其相邻的变量或数组发生了溢出,而溢出的数据改变了该变量。 价值。 为了排除这种可能性,你必须知道变量在RAM中分配的位置以及该位置附近有哪些变量,以便你可以有针对性地跟踪它。
其实MDK编译器的输出文件中有一个“项目名.map”文件,记录了代码、变量、堆栈的存储位置。 通过该文件,您可以检查所使用的变量在 RAM 中的分配位置。 要生成该文件,需要勾选for窗口标签栏下的前面的复选框,如图3-1所示。
图像
图 3-1 设置编译器生成 MAP 文件
3.4.4 默认情况下,堆栈分配在 RAM 的什么位置?
在MDK中,我们只需要在配置文件中定义堆栈大小,编译器就会自动在RAM的空闲区域中选择一个合适的地方来分配我们定义的堆栈。 这个地方位于 RAM 的什么位置?
通过查看MAP文件,可以发现MDK将堆栈放置在程序使用的RAM空间后面。 例如,如果您的 RAM 空间从 0000 开始,并且您的程序使用 0x200 字节的 RAM,则堆栈空间将从 0200 开始。
使用了多少堆栈以及是否溢出?
2.4.5 将初始化多少RAM?
在进入main()函数之前,MDK会清除未初始化的RAM。 我们的RAM可能非常大,但只使用了其中的一小部分。 MDK会初始化所有RAM吗?
答案是不。 MDK只初始化你的程序使用的RAM和堆栈RAM,并不关心其他RAM的内容。 如果你想使用绝对地址访问MDK未初始化的RAM,你需要小心,因为这些RAM上电时的内容很可能是随机的,并且每次上电时都不同。
3.4.6 MDK编译器如何设置非零初始化变量?
对于控制类产品,系统复位(非上电复位)后,可能需要保留复位前RAM中的数据,以快速恢复场景,或者避免因瞬间复位而重新启动现场设备。 默认情况下,keil mdk中任何形式的复位都会清除RAM区域中未初始化的变量数据。
MDK编译器生成的可执行文件中,每个输出节最多具有三个属性:RO属性、RW属性和ZI属性。 对于全局变量或静态变量,用const修饰符修饰的变量最有可能被放置在RO属性区,初始化的变量将被放置在RW属性区,其余变量将被放置在ZI属性区区域。 默认情况下,在每次复位之后以及程序执行主函数中的代码之前,编译器“主动”将 ZI 属性区域中的数据初始化为零。 因此,我们需要在C代码中设置一些变量在复位后不被初始化为零。 那么我们一定不能让编译器“胡作非为”。 我们需要使用一些规则来约束编译器。
分散的加载文件对于连接器至关重要。 在分散加载文件中,使用修改执行节可以避免编译器对该节的ZI数据进行零初始化。 这是解决非零初始化变量的关键。 所以我们可以定义一个装饰数据区,并将我们想要非零初始化的变量放入该区域。 所以,就有了第一种方法:
修改分散加载文件并添加名为 MYRAM 的执行部分。 执行段的起始地址为 ,长度为字节(8KB),修改为:
1: LR_IROM1 0x00000000 0x00080000 { ; load region size_region
2: ER_IROM1 0x00000000 0x00080000 { ; load address = execution address
3: *.o (RESET, +First)
4: *(InRoot$$Sections)
5: .ANY (+RO)
6: }
7: RW_IRAM1 0x10000000 0x0000A000 { ; RW data
8: .ANY (+RW +ZI)
9: }
10: MYRAM 0x1000A000 UNINIT 0x00002000 {
11: .ANY (NO_INIT)
12: }
13: }
因此,如果你的程序中有一个数组,并且你不希望它在重置后被初始化为零,你可以像这样定义变量:
1. unsigned char plc_eu_backup[32] __attribute__((at(0x1000A000)));
变量属性修饰符((at(adde)))用于强制变量指向adde所在的地址。 由于从该地址开始的8KB区域中的ZI变量不会被零初始化,因此位于该区域的数组也不会被零初始化。
这种方法的缺点很明显:程序员需要手动分配变量的地址。 如果有大量的非零初始化数据,这将是一个难以想象的大工程(以后的维护、添加、修改代码等)。 所以我们需要想办法让编译器自动在这个区域分配变量。
分散加载文件与方法1相同,如果仍然定义数组,可以使用以下方法:
unsigned char plc_eu_backup[32] __attribute__((section("NO_INIT"),zero_init));
变量属性修饰符(((“name”),))用于强制将变量定义在name属性数据节中,这意味着未初始化的变量被放置在ZI数据节中。 因为“”这个明确命名的自定义节具有属性。
将模块中所有未初始化的变量初始化为非零
如果模块名为test.c,则分散加载文件修改如下:
1: LR_IROM1 0x00000000 0x00080000 { ; load region size_region
2: ER_IROM1 0x00000000 0x00080000 { ; load address = execution address
3: *.o (RESET, +First)
4: *(InRoot$$Sections)
5: .ANY (+RO)
6: }
7: RW_IRAM1 0x10000000 0x0000A000 { ; RW data
8: .ANY (+RW +ZI)
9: }
10: RW_IRAM2 0x1000A000 UNINIT 0x00002000 {
11: test.o (+ZI)
12: }
13: }
在该模块中定义时间变量时使用以下方法:
这里,变量属性修饰符(())用于将未初始化的变量放置在ZI数据部分中。 事实上,默认情况下,MDK 将未初始化的变量放置在 ZI 数据区中。
4. 防御性编程
嵌入式产品的可靠性自然离不开硬件,但当硬件确定且没有第三方测试时,采用防御性编程思想编写的代码往往具有更高的稳定性。
防御性编程首先需要清楚地了解C语言的各种缺陷和陷阱。 C语言在运行时检查方面非常薄弱,需要程序员仔细考虑代码,必要时添加判断; 防御性编程的另一个核心思想是假设代码运行在不可靠的硬件上,外部干扰可能会扰乱程序执行顺序、改变RAM存储数据等。
4.1 对于有形式参数的函数,需要判断传递的实参是否合法。
程序员可能会无意识地传递错误的参数; 强烈的外部干扰可能会修改传递的参数,或者意外地调用带有随机参数的函数。 因此,在执行函数体之前,首先需要判断实参是否合法。
1. int exam_fun( unsigned char *str )
2. {
3. if( str != NULL ) // 检查“假设指针不为空”这个条件
4. {
5. //正常处理代码
6. }
7. else
8. {
9. //处理错误代码
10. }
11. }
4.2 仔细检查函数的返回值
必须对函数返回的错误码进行全面、仔细的处理,必要时进行错误记录。
1. char *DoSomething(…)
2. {
3. char * p;
4. p=malloc(1024);
5. if(p==NULL) /*对函数返回值作出判断*/
6. {
7. UARTprintf(…); /*打印错误信息*/
8. return NULL;
9. }
10. retuen p;
11. }
4.3 防止指针越界
如果动态计算地址,请确保计算出的地址是合理的,并且指向有意义的地方。 特别是对于指向结构体或数组内部的指针,当指针增加或改变时,它仍然指向同一个结构体或数组。
4.4 防止数组越界
数组越界问题在之前的文章中已经讨论过很多了。 由于 C 不能有效地检测数组,因此必须在应用程序中显式检测数组越界问题。 以下示例可用于中断通信数据的接收。
1. #define REC_BUF_LEN 100
2. unsigned char RecBuf[REC_BUF_LEN];
3. //其它代码
4. void Uart_IRQHandler(void)
5. {
6. static RecCount=0; //接收数据长度计数器
7. //其它代码
8. if(RecCount< REC_BUF_LEN) //判断数组是否越界
9. {
10. RecBuf[RecCount]=…; //从硬件取数据
11. RecCount++;
12. //其它代码
13. }
14. else
15. {
16. //错误处理代码
17. }
18. //其它代码
19. }
在使用一些库函数时,还需要检查边界。 例如下面的(,0,len)函数将指向的内存区域的前len个字节用0填充,如果不注意len的长度,就会将数组外的内存区域清空:
1. #define REC_BUF_LEN 100
2. unsigned char RecBuf[REC_BUF_LEN];
3.
4. if(len< REC_BUF_LEN)
5. {
6. memset(RecBuf,0,len); //将数组RecBuf清零
7. }
8. else
9. {
10. //处理错误
11. }
4.5 数学算术运算 4.5.1 除法运算,只检测除数为零可靠吗?
在除法运算之前检查除数是否为零几乎已成为共识,但仅检查除数是否为零就足够了吗?
考虑两个整数的除法。 对于long类型变量来说,它可以表示的取值范围是:-~+。 如果使用-/-1,结果应该是+,但是这个结果超出了可以表示的范围。 。 所以,在这种情况下,除了检查除数是否为零之外,我们还需要检查除法是否溢出。
1. #include
2. signed long sl1,sl2,result;
3. /*初始化sl1和sl2*/
4. if((sl2==0)||(sl1==LONG_MIN && sl2==-1))
5. {
6. //处理错误
7. }
8. else
9. {
10. result = sl1 / sl2;
11. }
4.5.2 检测运算溢出
整数加法、减法和乘法运算可能会溢出。 在讨论未定义行为时,给出了有符号整数加法溢出判断代码。 下面是一段无符号整数加法溢出判断代码段:
1. #include
2. unsigned int a,b,result;
3. /*初始化a,b*/
4. if(UINT_MAX-a5. {
6. //处理溢出
7. }
8. else
9. {
10. result=a+b;
11. }
嵌入式硬件一般没有浮点处理器,浮点运算在嵌入式系统中比较少见,溢出判断很大程度上依赖于C库的支持,这里不再讨论。
4.5.3 检测移位
在讨论未定义行为时,提到有符号数右移、移位次数为负、或者大于操作数的位数都是未定义行为。 还提到,不对有符号数执行位操作,但必须检测移位。 位数是否大于操作数的数量。 以下是无符号整数左移检测的代码片段:
1. unsigned int ui1;
2. unsigned int ui2;
3. unsigned int uresult;
4.
5. /*初始化ui1,ui2*/
6. if(ui2>=sizeof(unsigned int)*CHAR_BIT)
7. {
8. //处理错误
9. }
10. else
11. {
12. uresult=ui1< 13. }
4.6 如果有硬件看门狗,则使用它
当其他一切都失败时,看门狗可能是最后一道防线。 它的原理非常简单,但却可以大大提高设备的可靠性。 如果设备有硬件看门狗,请务必为其编写驱动程序。
这是因为在上电复位结束到看门狗开启期间,器件可能会受到干扰而跳过看门狗初始化过程,导致看门狗失效。 尽早开启看门狗可以降低这种概率;
在中断程序中喂狗,由于干扰的存在,程序可能一直被中断,从而导致看门狗失效。 如果主程序中设置了该标志位,也允许在中断程序喂狗时与该标志位联合判断;
产品的特性决定了狗的喂养间隔。 对于不涉及安全性或实时性的设备,喂狗间隔相对宽松,但间隔不宜过长,否则会被用户感知,影响用户体验。 对于安全设计、实时控制的设备,原则是尽快复位,否则可能会发生事故。
克莱门汀号执行第二阶段任务时,原计划从月球出发,探索深空小行星。 然而,当太空探测器飞向小行星时,由于软件缺陷而中断。 手术中断了20分钟。 不但没能到达小行星,而且因为控制喷嘴烧毁了11分钟而导致供电减少,探测器无法再远程控制。 任务最终结束了,但也造成了资源和金钱的浪费。 。
“克莱门汀任务失败令我感到震惊。 本来可以通过硬件中的一个简单的看门狗定时器来避免这种情况,但由于当时的开发时间非常紧张,编程人员没有时间编写程序来激活它。”
不幸的是,1998年发射的近地航天器(NEAR)也遇到了同样的问题。 由于程序员没有遵循建议,当推进器减速器系统发生故障时,29公斤的储备燃料丢失了——这又是一个可以通过编程看门狗定时器来避免的问题,而且事实证明,从其他程序员的错误中吸取教训不简单。
4.7 存储关键数据的多个备份,并使用“投票法”找回数据
RAM中的数据如果受到干扰可能会被更改,因此应该保护关键的系统数据。 关键数据包括全局变量、静态变量和需要保护的数据区。 备份数据和原始数据不应该位于相邻的位置,因此备份数据的位置不应该由编译器默认分配,而应该由程序员存储在指定的区域。 RAM 可分为 3 个区域。 第一区域存储原码,第二区域存储反码,第三区域存储异或码。 区域之间保留一定量的“空白”RAM 作为隔离。 可以使用编译器的“分散加载”机制将变量单独存储在这些区域中。 需要读取时,同时读取3条数据并投票,至少取两条相同的值。
如果设备的RAM是从头开始的,我需要将原码存储在RAM的~中,补码存储在~中,0xAA的异或码存储在~中。 编译器的分散加载可以设置为:
1. LR_IROM1 0x00000000 0x00080000 { ; load region size_region
2. ER_IROM1 0x00000000 0x00080000 { ; load address = execution address
3. *.o (RESET, +First)
4. *(InRoot$$Sections)
5. .ANY (+RO)
6. }
7. RW_IRAM1 0x10000000 0x00008000 { ;保存原码
8. .ANY (+RW +ZI )
9. }
10.
11. RW_IRAM3 0x10009000 0x00001000{ ;保存反码
12. .ANY (MY_BK1)
13. }
14.
15. RW_IRAM2 0x1000B000 0x00001000 { ;保存异或码
16. .ANY (MY_BK2)
17. }
18. }
如果某个关键变量需要多处备份,可以如下定义该变量,将这三个变量分配到三个不连续的RAM区域,并根据0xAA的原码、反码、异或码进行初始化定义时。 。
1. uint32 plc_pc=0; //原码
2. __attribute__((section("MY_BK1"))) uint32 plc_pc_not=~0x0; //反码
3. __attribute__((section("MY_BK2"))) uint32 plc_pc_xor=0x0^0xAAAAAAAA; //异或码
当需要写入变量时,必须更新这三个位置; 读取变量时,读取三个值进行判断,至少取两个相同的值。
为什么选择异或码而不是补码? 这是因为MDK的整数是按照补码存储的。 正数的补码与原码相同。 在这种情况下,原码和补码是一致的。 不但起不到冗余作用,而且还影响可靠性。 有害。 例如,当存储非零整数区域时,RAM会因干扰而被清除。 由于原码和补码一致,按照3取2的“投票法”,干扰值0将被视为正确数据。
4.8 非易失性存储器的备份存储
非易失性存储器包括但不限于Flash、铁电体。 仅仅读取写入非易失性存储器的数据并验证它是不够的。 强干扰可能会导致非易失性存储器中的数据错误。 写入非易失性存储器期间系统断电将导致数据丢失。 由于干扰,程序会遇到非易失性存储器写入函数,从而导致数据存储混乱。 一种可靠的方法是将非易失性存储器划分为多个区域。 每个数据都会以不同的形式写入这些分区。 当需要读取时,会同时读取多份数据并进行投票。 相同数字中较大数字的值。
4.9 软件锁
对于一定顺序的初始化序列或者函数调用,为了保证调用顺序或者保证每个函数都被调用,我们可以使用互锁,互锁本质上是一种软件锁。 另外,对于一些安全关键的代码语句(语句,而不是函数),可以为其设置软件锁。 只有拥有特定密钥的人才能访问这些密钥代码。 也可以通俗地理解,关键的安全代码不能根据单一条件执行,必须设置一个额外的标志。
例如,向Flash写入数据时,我们会判断数据是否合法,写入的地址是否合法,并计算要写入的扇区。 然后,调用写Flash子程序。 该子程序中判断扇区地址是否合法以及数据长度是否合法,然后将数据写入Flash。 由于写入 Flash 语句是安全关键代码,因此程序会锁定这些语句:您必须拥有正确的密钥才能写入 Flash。 这样,即使程序跑掉,写入了Flash子程序,也能大大降低误写的风险。
1. /****************************************************************************
2. * 名称:RamToFlash()
3. * 功能:复制RAM的数据到FLASH,命令代码51。
4. * 入口参数:dst 目标地址,即FLASH起始地址。以512字节为分界
5. * src 源地址,即RAM地址。地址必须字对齐
6. * no 复制字节个数,为512/1024/4096/8192
7. * ProgStart 软件锁标志
8. * 出口参数:IAP返回值(paramout缓冲区) CMD_SUCCESS,SRC_ADDR_ERROR,DST_ADDR_ERROR,
9. SRC_ADDR_NOT_MAPPED,DST_ADDR_NOT_MAPPED,COUNT_ERROR,BUSY,未选择扇区
10. ****************************************************************************/
11. void RamToFlash(uint32 dst, uint32 src, uint32 no,uint8 ProgStart)
12. {
13. PLC_ASSERT("Sector number",(dst>=0x00040000)&&(dst<=0x0007FFFF));
14. PLC_ASSERT("Copy bytes number is 512",(no==512));
15. PLC_ASSERT("ProgStart==0xA5",(ProgStart==0xA5));
16.
17. paramin[0] = IAP_RAMTOFLASH; // 设置命令字
18. paramin[1] = dst; // 设置参数
19. paramin[2] = src;
20. paramin[3] = no;
21. paramin[4] = Fcclk/1000;
22. if(ProgStart==0xA5) //只有软件锁标志正确时,才执行关键代码
23. {
24. iap_entry(paramin, paramout); // 调用IAP服务程序
25. ProgStart=0;
26. }
27. else
28. {
29. paramout[0]=PROG_UNSTART;
30. }
31. }
该程序段是对内部Flash进行编程,其中调用IAP程序的函数(,)是关键的安全代码。 因此,在执行代码之前,首先要判断一个专门设置的安全锁标志。 只有当该标志满足设定值时才会被执行。 对闪存操作进行编程。 如果程序意外运行到该函数,则由于标志不正确,Flash 将不会被编程。
4.10 通讯
通信线路上的数据错误是比较严重的。 通信线路越长、环境越恶劣,误差就越严重。 无论硬件和环境的影响如何,我们的软件都应该能够识别错误的通信数据。 有一些应用程序可以做到这一点:
每帧字节数越多,出现误码的可能性就越大,无效数据也就越多。 对此,以太网规定每帧数据不得超过1500字节。 高可靠性CAN收发器规定每帧数据不得超过8个字节。 对于RS485,基于RS485链路使用最广泛的协议规定一帧数据不得超过256字节。 因此,建议在制定内部通信协议时,使用RS485时,每帧数据不要超过256字节;
编写程序时应启用奇偶校验。 对于每帧超过16字节的应用,建议至少编写CRC16校验程序;
1) 增加缓冲区溢出判断。 这是因为数据接收大多在中断中完成,编译器无法检测缓冲区是否溢出,需要手动检查。 这在上面的数据溢出部分已经详细解释了。
2)增加超时判断。 当一帧数据接收到一半,而剩余数据长时间无法接收时,则认为该帧数据无效,重新开始接收。 可选,与不同协议相关,但必须实现缓冲区溢出判断。 这是因为对于需要判断帧头的协议,上位机发送帧头后可能会突然断电。 重启后,上位机从新的帧开始发送,但下位机已收到最后一个未发送的帧头。 因此,上位机的这个帧头将被下位机作为正常数据接收。 这可能会导致数据长度字段是一个非常大的值,而填充这个长度的缓冲区需要相当大量的数据(例如一帧可能是1000字节),从而影响响应时间; 另一方面,如果程序没有对缓冲区溢出进行判断,那么缓冲区很可能会溢出,后果将是灾难性的。
如果通信数据出现错误,则需要重传机制来重新发送错误的帧。
4.11 开关量输入检测与确认
开关量容易受到尖峰干扰,如果不进行滤波,可能会导致故障。 一般情况下,需要对开关输入信号进行多次采样并进行逻辑判断,直至确认信号正确。
4.12 开关量输出
简单地一次性输出开关信号是不安全的,干扰信号可能会翻转开关输出的状态。 反复刷新输出可以有效防止电平翻转。
4.13 保存和恢复初始化信息
微处理器的寄存器值也可能因外部干扰而改变。 外设初始化值需要长期保存在寄存器中,最容易被破坏。 由于Flash中的数据比较难损坏,因此可以提前将初始化信息写入Flash中,在程序空闲时,比较与初始化相关的寄存器值是否被改变。 如果发现非法更改,则使用Flash中的值进行恢复。
该公司目前使用的4.3英寸液晶显示屏,抗干扰能力一般。 如果显示器和控制器之间的电缆距离太长,或者使用显示器的设备受到静电或脉冲串的影响,显示器可能会变得模糊或发白。 对此,我们可以将初始化显示屏的数据保存在Flash中。 程序运行后,每隔一定时间从显示屏的寄存器中读取当前值,并与Flash中存储的值进行比较。 如果发现两者不同,请重新初始化。 展示。 下面给出验证源码,仅供参考。
定义数据结构:
1. typedef struct {
2. uint8_t lcd_command; //LCD寄存器
3. uint8_t lcd_get_value[8]; //初始化时写入寄存器的值
4. uint8_t lcd_value_num; //初始化时写入寄存器值的数目
5. }lcd_redu_list_struct;
定义一个const修饰的结构变量来存储一些LCD寄存器的初始值。 这个初始值与具体应用初始化有关,不一定是表中的数据。 通常,该结构体变量存储在Flash中。
1. /*LCD部分寄存器设置值列表*/
2. lcd_redu_list_struct const lcd_redu_list_str[]=
3. {
4. {SSD1963_Get_Address_Mode,{0x20} ,1}, /*1*/
5. {SSD1963_Get_Pll_Mn ,{0x3b,0x02,0x04} ,3}, /*2*/
6. {SSD1963_Get_Pll_Status ,{0x04} ,1}, /*3*/
7. {SSD1963_Get_Lcd_Mode ,{0x24,0x20,0x01,0xdf,0x01,0x0f,0x00} ,7}, /*4*/
8. {SSD1963_Get_Hori_Period ,{0x02,0x0c,0x00,0x2a,0x07,0x00,0x00,0x00},8}, /*5*/
9. {SSD1963_Get_Vert_Period ,{0x01,0x1d,0x00,0x0b,0x09,0x00,0x00} ,7}, /*6*/
10. {SSD1963_Get_Power_Mode ,{0x1c} ,1}, /*7*/
11. {SSD1963_Get_Display_Mode,{0x03} ,1}, /*8*/
12. {SSD1963_Get_Gpio_Conf ,{0x0F,0x01} ,2}, /*9*/
13. {SSD1963_Get_Lshift_Freq ,{0x00,0xb8} ,2}, /*10*/
14. };
实现函数如下。 函数会遍历结构体变量中的每条命令以及每条命令下的初始值。 如果其中一条命令不正确,就会跳出循环,执行重新初始化和恢复措施。 这个函数中的宏是我自己的调试函数,使用串口打印调试信息,在接下来的第五部分会详细介绍。 通过这个功能,我可以长时间监控显示的哪些命令、哪些位容易受到干扰。 该程序使用了一个妖魔化的关键字:goto。 大多数 C 语言书籍对 goto 关键字的描述很差,但是您应该使用自己的判断。 除了goto关键字之外,还有什么方法可以如此简单高效地跳出函数内部的多个循环呢!
1. /**
2. * lcd 显示冗余
3. * 每隔一段时间调用该程序一次
4. */
5. void lcd_redu(void)
6. {
7. uint8_t tmp[8];
8. uint32_t i,j;
9. uint32_t lcd_init_flag;
10.
11. lcd_init_flag =0;
12. for(i=0;i<sizeof(lcd_redu_list_str)/sizeof(lcd_redu_list_str[0]);i++)
13. {
14. LCD_SendCommand(lcd_redu_list_str[i].lcd_command);
15. uyDelay(10);
16. for(j=0;j 17. {
18. tmp[j]=LCD_ReadData();
19. if(tmp[j]!=lcd_redu_list_str[i].lcd_get_value[j])
20. {
21. lcd_init_flag=0x55;
22. MY_DEBUGF(MENU_DEBUG,("读lcd寄存器值与预期不符,命令为:0x%x,第%d个参数,
23. 该参数正确值为:0x%x,实际读出值为:0x%x\n",lcd_redu_list_str[i].lcd_command,j+1,
24. lcd_redu_list_str[i].lcd_get_value[j],tmp[j]));
25. goto handle_lcd_init;
26. }
27. }
28. }
29.
30. handle_lcd_init:
31. if(lcd_init_flag==0x55)
32. {
33. //重新初始化LCD
34. //一些必要的恢复措施
35. }
36. }
4.14 陷阱
对于8051核心的单片机,由于没有相应的硬件支持,所以可以采用纯软件的方式设置软件陷阱来拦截一些程序跑掉。 对于ARM7或-M系列微控制器,硬件内置了多个异常。 软件需要根据硬件异常编写trap程序来快速定位甚至恢复错误。
4.15 阻塞处理
有时程序员会使用 while(!flag); 语句阻塞并等待标志改变。 例如通过串口发送时,用于等待一字节数据发送完成。 这样的代码是有风险的。 如果由于某种原因该标志没有改变,就会导致系统崩溃。
一个好的冗余程序是设置一个超时定时器,强制程序在一定时间后退出while循环。
2003年8月11日发生的W32..蠕虫事件造成全球经济损失高达5亿美元。 该漏洞利用了分布式组件对象模型的远程过程调用接口中的逻辑缺陷:调用 () 函数时,循环仅设置不充分的结束条件。
原代码简化如下:
1. HRESULT GetMachineName ( WCHAR *pwszPath,
2. WCHARwszMachineName[MAX_COMPUTTERNAME_LENGTH_FQDN+1])
3. {
4. WCHAR *pwszServerName = wszMachineName;
5. WCHAR *pwszTemp = pwszPath + 2;
6. while ( *pwszTemp != L’\\’ ) /* 这句代码循环结束条件不充分 */
7. *pwszServerName++= *pwszTemp++;
8. /*… */
9. }
微软发布的安全补丁MS03-026解决了这个问题,并为()函数设置了足够的终止条件。 简化的解决方案代码如下(不是微软补丁代码):
1. HRESULT GetMachineName( WCHAR *pwszPath,
2. WCHARwszMachineName[MAX_COMPUTTERNAME_LENGTH_FQDN+1])
3. {
4. WCHAR *pwszServerName = wszMachineName;
5. WCHAR *pwszTemp = pwszPath + 2;
6. WCHAR *end_addr = pwszServerName +MAX_COMPUTTERNAME_LENGTH_FQDN;
7. while ((*pwszTemp != L’\\’ ) && (*pwszTemp != L’\0’)
8. && (pwszServerName/*充分终止条件*/
9. *pwszServerName++= *pwszTemp++;
10. /*… */
11. }
5.测试,再测试
无论程序员多么细心,都不可能编写出完全没有缺陷的程序。 测试的目的是发现尽可能多的缺陷并纠正它们。 这里所说的测试是指程序员的自我测试。 及早自检可以更早地发现错误,相应的修复成本也会很低。 如果你不彻底测试你的代码,恐怕你开发的不仅仅是代码,而且你还可能会得到不好的名声。
高质量的嵌入式C程序与高质量的基本要素密切相关。 函数可以用作基本元素。 我们的测试是从最基本的功能开始的。 判断哪些功能需要测试需要一定的经验。 尽管代码行的数量与逻辑复杂性不成比例,但是如果您不能判断是否需要测试函数,则简单而粗糙的方法是:当函数的有效代码超过20行时,只需对其进行测试。
程序员对他们的代码和逻辑关系非常清楚。 测试时,他们会彻底测试每个逻辑分支。 在我们认为它们不会出错的地方发生了许多错误,因此,即使逻辑分支很简单,也建议对其进行测试。 第一个原因是,当我们查看自己的代码时,发现错误并不容易,并且测试可以暴露这些错误。 另一方面,在使用正确的语法和正确逻辑的代码由编译器编辑后,生成的汇编代码可能与您的逻辑不在基础上相同。 例如,在使用上面提到的关键字的情况下编译后生成的汇编代码,或以低优化级别编译后生成的汇编代码,并以高优化级别进行编译,可能会大不相同。 在实际的运行测试中,您可以暴露这些隐藏的错误。 最后,尽管非常不可能,但编译器本身可能会有错误,尤其是在构造复杂表达式时(应尽可能避免复杂表达式)。
5.1使用硬件调试器测试
使用硬件调试器(例如J-Link)进行测试是最常见的方法。 您可以运行单步,设置断点,并轻松查看当前寄存器和变量的值。 在寻找缺陷时,使用硬件调试器进行测试是最简单但最有效的方法。
硬件调试者已经常用于公司。 测试的这一方面将不会引入。 我相信每个人都已经熟悉了。
5.2一些缺陷很难解决
就像没有一种方法可以完美地解决所有问题一样,在实际项目中,硬件调试器也很难到达。 可以给出一些例子:
例如,如果公司使用LWIP协议堆栈,则如果跟踪数据处理过程,则需要从接收数据到应用程序层以处理数据。 它将穿过驱动程序层,IP层,TCP层和应用程序层,并将通过数十个文件和数十个文件。 使用硬件调试器跟踪的功能是耗时和费力的。
可能会不时出现一些缺陷,这些缺陷可能会在几分钟或几个小时甚至几天内发生。 这样的缺陷很难用硬件调试器捕获。
当我们第一次学习C语言时,我们都与功能接触。 此功能可以方便地输出信息,并以指定格式将各种变量格式化为字符串。 我们应该提供类似的功能;
在编码阶段,我们可能会在程序中添加大量调试语句,但是当发布该程序时,这些调试语句需要从代码中删除,这将是一个可怕的过程。 我们必须提供一种策略,以轻松删除这些调试声明。
5.2.1简单易用的调试功能
使用库功能。 以MDK为例,该方法如下:
我>初始化串行端口
II>重建FPUTC函数。 该函数将调用FPUTC函数以执行在基础串行端口上发送数据。
1. /**
2. * @brief 将C库中的printf函数重定向到指定的串口.
3. * @param ch:要发送的字符
4. * @param f :文件指针
5. */
6. int fputc(int ch, FILE *f)
7. {
8.
9. /*这里是一个跟硬件相关函数,将一个字符写到UART */
10. //举例:USART_SendData(UART_COM1, (uint8_t) ch);
11.
12. return ch;
13. }
iii>在for窗口中,在“选项卡”栏下,在使用前检查复选框,以避免使用半功能。 (注意:标准C库功能默认启用半托管功能。如果您必须使用标准C库,请亲自查阅信息)
构建自己的调试功能
使用库功能更方便,但也缺乏一些灵活性,并且不利于根据自己的方式自定义输出格式。 自己编写类似的功能将更加灵活,并且不依赖任何编译器。 下面给出了完整的类功能实现。 此函数支持有限的格式参数,其用法与库函数一致。 与库函数类似,这也需要提供底部级别的串行端口发送函数(原型:(const *pcbuf,ullen)),该功能用于发送指定数量的字符,并返回发送的字符的最终数。
1. #include /*支持函数接收不定量参数*/
2.
3. const char * const g_pcHex = "0123456789abcdef";
4.
5. /**
6. * 简介: 一个简单的printf函数,支持\%c, \%d, \%p, \%s, \%u,\%x, and \%X.
7. */
8. void UARTprintf(const uint8_t *pcString, ...)
9. {
10. uint32_t ulIdx;
11. uint32_t ulValue; //保存从不定量参数堆栈中取出的数值型变量
12. uint32_t ulPos, ulCount;
13. uint32_t ulBase; //保存进制基数,如十进制则为10,十六进制数则为16
14. uint32_t ulNeg; //为1表示从变量为负数
15. uint8_t *pcStr; //保存从不定量参数堆栈中取出的字符型变量
16. uint8_t pcBuf[32]; //保存数值型变量字符化后的字符
17. uint8_t cFill; //'x'->不足8个字符用'0'填充,cFill='0';
18. //'%8x '->不足8个字符用空格填充,cFill=' '
19. va_list vaArgP;
20.
21. va_start(vaArgP, pcString);
22. while(*pcString)
23. {
24. // 首先搜寻非%核字符串结束字符
25. for(ulIdx = 0; (pcString[ulIdx] != '%') && (pcString[ulIdx] != '\0'); ulIdx++)
26. { }
27. UARTwrite(pcString, ulIdx);
28.
29. pcString += ulIdx;
30. if(*pcString == '%')
31. {
32. pcString++;
33.
34. ulCount = 0;
35. cFill = ' ';
36. again:
37. switch(*pcString++)
38. {
39. case '0': case '1': case '2': case '3': case '4':
40. case '5': case '6': case '7': case '8': case '9':
41. {
42. // 如果第一个数字为0, 则使用0做填充,则用空格填充)
43. if((pcString[-1] == '0') && (ulCount == 0))
44. {
45. cFill = '0';
46. }
47. ulCount *= 10;
48. ulCount += pcString[-1] - '0';
49. goto again;
50. }
51. case 'c':
52. {
53. ulValue = va_arg(vaArgP, unsigned long);
54. UARTwrite((unsigned char *)&ulValue, 1);
55. break;
56. }
57. case 'd':
58. {
59. ulValue = va_arg(vaArgP, unsigned long);
60. ulPos = 0;
61.
62. if((long)ulValue < 0)
63. {
64. ulValue = -(long)ulValue;
65. ulNeg = 1;
66. }
67. else
68. {
69. ulNeg = 0;
70. }
71. ulBase = 10;
72. goto convert;
73. }
74. case 's':
75. {
76. pcStr = va_arg(vaArgP, unsigned char *);
77.
78. for(ulIdx = 0; pcStr[ulIdx] != '\0'; ulIdx++)
79. {
80. }
81. UARTwrite(pcStr, ulIdx);
82.
83. if(ulCount > ulIdx)
84. {
85. ulCount -= ulIdx;
86. while(ulCount--)
87. {
88. UARTwrite(" ", 1);
89. }
90. }
91. break;
92. }
93. case 'u':
94. {
95. ulValue = va_arg(vaArgP, unsigned long);
96. ulPos = 0;
97. ulBase = 10;
98. ulNeg = 0;
99. goto convert;
100. }
101. case 'x': case 'X': case 'p':
102. {
103. ulValue = va_arg(vaArgP, unsigned long);
104. ulPos = 0;
105. ulBase = 16;
106. ulNeg = 0;
107. convert: //将数值转换成字符
108. for(ulIdx = 1; (((ulIdx * ulBase) <= ulValue) &&(((ulIdx * ulBase) / ulBase) == ulIdx)); ulIdx *= ulBase, ulCount--)
109. { }
110. if(ulNeg)
111. {
112. ulCount--;
113. }
114. if(ulNeg && (cFill == '0'))
115. {
116. pcBuf[ulPos++] = '-';
117. ulNeg = 0;
118. }
119. if((ulCount > 1) && (ulCount < 16))
120. {
121. for(ulCount--; ulCount; ulCount--)
122. {
123. pcBuf[ulPos++] = cFill;
124. }
125. }
126.
127. if(ulNeg)
128. {
129. pcBuf[ulPos++] = '-';
130. }
131.
132. for(; ulIdx; ulIdx /= ulBase)
133. {
134. pcBuf[ulPos++] = g_pcHex[(ulValue / ulIdx) % ulBase];
135. }
136. UARTwrite(pcBuf, ulPos);
137. break;
138. }
139. case '%':
140. {
141. UARTwrite(pcString - 1, 1);
142. break;
143. }
144. default:
145. {
146. UARTwrite("ERROR", 5);
147. break;
148. }
149. }
150. }
151. }
152. //可变参数处理结束
153. va_end(vaArgP);
154. }
5.2.2进一步封装调试功能
如上所述,我们添加的调试语句应轻松从最终版本中删除。 因此,我们不能直接调用或自定义功能。 我们需要封装这些调试功能,以便可以随时将它们从代码中删除。 调试语句。 参考方法如下:
1. #ifdef MY_DEBUG
2. #define MY_DEBUGF(message) do { \
3. {UARTprintf message;} \
4. } while(0)
5. #else
6. #define MY_DEBUGF(message)
7. #endif /* PLC_DEBUG */
在我们的编码测试中,定义宏并使用宏(请注意额外的f'比以前的宏)输出调试信息。 预处理后,将更换宏(),从而实现调试信息的输出; 正式发布时,您只需要评论宏即可。 预处理后,所有()语句将被空间替换,并将启用调试。 信息从代码中删除。
6.编程思想6.1编程样式
“计算机程序的构建和解释”一书开始写:程序是为人们提供的,可以在机器上运行。
6.1.1干净样式
要使用的编码样式,例如凹痕和支撑位置,一直存在争议。 由于编码样式也会影响程序的可读性,因此我们很难有兴趣阅读具有随机括号和不一致对齐的源代码。 我们总是必须研究别人的计划。 如果编码样式相似,我们会更愿意阅读源代码。 但是编码样式的问题是主观的,并且永远不可能就编码方式提出统一的意见。 因此,只要您的编码样式干净并且结构清晰,就足够了。 除此之外,没有其他对编码样式的要求。
提议匈牙利命名法和微软前首席建筑师的程序员说:我认为,代码清单使人们和一个整洁的家一样愉悦。 您可以一目了然地告诉您的房屋是混乱还是原始。 这可能没有多大意义。 因为只是一个干净的房子并不意味着太多,所以它仍然可能藏有污垢! 但是第一印象很重要,并且至少反映了该计划的某些方面。 我敢打赌,我可以判断一个程序是否从3米之外进行了操作。 我可能无法保证它是好的,但是如果在3米外看起来很糟糕,我可以保证不会仔细编写该程序。 如果没有仔细编写,它在逻辑上可能不会很漂亮。
6.1.2清晰的命名
变量,功能,宏等。都需要命名。 清晰的命名是出色代码的特征之一。 命名的要点之一是该名称应清楚地描述对象,以便初级程序员可以轻松理解代码的逻辑。 您需要考虑我们写的代码:将自己,给编译器或其他人? 我认为该代码最重要的是向他人展示,遵循自己。 如果没有明确的名字,那么其他人在维护程序时很难在整个图片上看到代码,因为很难记住十几个或更多的坏名称; 查看自己的代码,可能不记得坏名称的含义。
获得对象的清晰名称并不简单。 首先,您可以意识到该名称的重要性需要一个过程。 这可能与棕褐色式C程序教科书的广泛使用有关:a,b,c,x,y和z变量名称。 在此阶段,它传达了出色的编程思想。 其次,如何适当地命名对象也非常具有挑战性。 有必要准确,不道德,不要大声。 这个非常困难; 此外,命名需要考虑整体一致性。 它必须在同一项目中具有统一的样式,并且坚持这种风格并不容易。
关于如何命名,例如:面对具有某些属性的结构,不要随便地取名,然后让每个人都考虑名称和属性之间的关联。 您应该将属性本身用作结构的结构。 姓名。
6.1.3适当的注释
笔记一直是争议之一,我反对没有注释和过度注释。 没有注释的代码显然很不好,但是过多的注释也会阻碍该程序的可读性。 由于可能存在周年纪念日,因此可能会误解该计划的真正意图。 此外,过多的注释将增加程序员不必要的时间。 如果您的编码样式整洁,命名和清晰,那么您的代码可读性就不会更糟,并且注释的原始含义是促进理解该程序。
建议使用良好的编码样式和清晰的命名来减少注释。 应该注释模块,功能,变量,数据结构,算法和密钥代码。 应注意注释的质量。 如果您需要大量的注释来清楚程序的作用,那么您应该注意:程序变量名称不够清晰,还是代码逻辑过于混乱。 此时此程序。
6.2数据结构
数据结构是编程的基础。 在设计程序之前,您应该考虑所需的数据结构。
前微软首席建筑师:编程的第一步是想象力。 在您脑海中,要清楚地掌握龙的来龙去脉。 在这个初始阶段,我将使用纸和铅笔。 我只是相信涂鸦,不要编写代码。 我可能会画一些盒子或箭头,但基本上只是涂鸦,因为真实的想法在我心中。 我喜欢想象需要维护的那些结构,这些结构代表了我想编码的现实世界。 一旦这种结构被认为是严格而清晰的,我就会开始编写代码。 我将坐在终端前面或更改为之前,我将Zhang Bai纸开始编写代码。 这很容易。 只要我改变了脑海中写下的思想,我就会知道结果应该是什么样子。 大多数代码都将完成,但是我维护的数据结构是关键。 我将首先考虑数据结构,并在整个代码中牢记它们。
以太网和操作系统SDS 940的开发最重要的质量:(程序员)最重要的质量是,它可以将问题解决方案组织到易于控制的结构中。
开发CP/M操作系统的Gary.A:如果无法确认数据结构,我将永远不会开始编码。 我将首先绘制数据结构,然后花很长时间思考数据结构。 确定数据结构后,我开始编写一些小型代码,并不断改进和监视。 编码过程中的测试可以确保其修改是本地的,如果有任何问题,您可以立即找到它。
创始人Bill **·**门:编写程序的最重要部分是设计数据结构。 下一个重要的部分是分解各种代码块。
丹(Dan)写了世界上第一个电子仪表软件:我认为,编写程序的最重要部分是设计数据结构。 此外,您必须知道人类机器接口的外观。
让我们以一个例子来解释。 在引入防御性编程时,提到公司的LCD显示反干扰能力。 为了提高LCD的稳定性,需要定期读取LCD中的密钥寄存器,然后将其与Flash存在的初始值进行比较。 需要读取十个以上的LCD寄存器,并且每个寄存器中读取的值都不相同。 可能从1到8个字节。 如果不考虑数据结构,则编写的程序将非常漫长。
1. void lcd_redu(void)
2. {
3. 读第一个寄存器值;
4. if(第一个寄存器值==Flash存储值)
5. {
6. 读第二个寄存器值;
7. if(第二个寄存器值==Flash存储值)
8. {
9. ...
10.
11. 读第十个寄存器值;
12. if(第十个寄存器值==Flash存储值)
13. {
14. 返回;
15. }
16. else
17. {
18. 重新初始化LCD;
19. }
20. }
21. else
22. {
23. 重新初始化LCD;
24. }
25. }
26. else
27. {
28. 重新初始化LCD;
29. }
30. }
我们分析了这一过程,发现可以提取许多相同的元素。 例如,每当LCD寄存器读取寄存器的命令号时,它将通过寄存器读取,判断值相同,并且处理异常。 因此,我们可以提取一些相同的元素,将其组织到数据结构中,使用统一的处理这些数据,并将数据与处理过程分开。
我们可以首先提取相同的元素并将其组织到数据结构中:
1. typedef struct {
2. uint8_t lcd_command; //LCD寄存器
3. uint8_t lcd_get_value[8]; //初始化时写入寄存器的值
4. uint8_t lcd_value_num; //初始化时写入寄存器值的数目
5. }lcd_redu_list_struct;
这是LCD寄存器命令号; 这是一个数组,指示要初始化的寄存器的值。 这是因为对于LCD寄存器,它可以由多个字节初始化。 字节的初始值是因为每个寄存器的初始值不同。 当我们使用相同的方法处理数据时,我们需要此信息。
就此而言,我们将处理的数据已提前固定,因此在定义数据结构后,我们可以将这些数据组织成一种形式:
1. /*LCD部分寄存器设置值列表*/
2. lcd_redu_list_struct const lcd_redu_list_str[]=
3. {
4. {SSD1963_Get_Address_Mode,{0x20} ,1}, /*1*/
5. {SSD1963_Get_Pll_Mn ,{0x3b,0x02,0x04} ,3}, /*2*/
6. {SSD1963_Get_Pll_Status ,{0x04} ,1}, /*3*
7. {SSD1963_Get_Lcd_Mode ,{0x24,0x20,0x01,0xdf,0x01,0x0f,0x00} ,7}, /*4*/
8. {SSD1963_Get_Hori_Period ,{0x02,0x0c,0x00,0x2a,0x07,0x00,0x00,0x00},8}, /*5*/
9. {SSD1963_Get_Vert_Period ,{0x01,0x1d,0x00,0x0b,0x09,0x00,0x00} ,7}, /*6*/
10. {SSD1963_Get_Power_Mode ,{0x1c} ,1}, /*7*/
11. {SSD1963_Get_Display_Mode,{0x03} ,1}, /*8*/
12. {SSD1963_Get_Gpio_Conf ,{0x0F,0x01} ,2}, /*9*/
13. {SSD1963_Get_Lshift_Freq ,{0x00,0xb8} ,2}, /*10*
14. };
此时,我们可以使用处理过程来完成数十个LCD寄存器的阅读,判断和异常处理:
1. /**
2. * lcd 显示冗余
3. * 每隔一段时间调用该程序一次
4. */
5. void lcd_redu(void)
6. {
7. uint8_t tmp[8];
8. uint32_t i,j;
9. uint32_t lcd_init_flag;
10.
11. lcd_init_flag =0;
12. for(i=0;i<sizeof(lcd_redu_list_str)/sizeof(lcd_redu_list_str[0]);i++)
13. {
14. LCD_SendCommand(lcd_redu_list_str[i].lcd_command);
15. uyDelay(10);
16. for(j=0;j 17. {
18. tmp[j]=LCD_ReadData();
19. if(tmp[j]!=lcd_redu_list_str[i].lcd_get_value[j])
20. {
21. lcd_init_flag=0x55;
22. //一些调试语句,打印出错的具体信息
23. goto handle_lcd_init;
24. }
25. }
26. }
27.
28. handle_lcd_init:
29. if(lcd_init_flag==0x55)
30. {
31. //重新初始化LCD
32. //一些必要的恢复措施
33. }
34. }
通过合理的数据结构,我们可以将数据与处理过程分开,并且可以使用非常简单的代码来实现LCD冗余判断过程。 更重要的是,分开数据和处理过程更有利于维护代码。 例如,通过实验,我们还需要为法官添加LCD寄存器的值。 目前,我们只需要将新添加的注册信息以数据结构格式放置,然后将其放在LCD寄存器设置值列表中的任何位置。 无需添加任何额外的添加处理代码! 这只是数据结构的优势之一。 数据结构的使用还可以简化编程,并使复杂的过程变得更简单。 实际编程后,这只会有更深入的了解。
7.摘要和阅读书籍
本文介绍了编写高质量嵌入式C程序所涉及的多个方面。 每年,数以千计的C计划都在微处理器上运行,例如单芯片微型计算机,ARM7和-M3,但几乎没有关于如何在这些处理器上编写高质量和有效的C程序的书籍。 本文试图在这方面做出一些努力。 编写高质量的嵌入式C程序需要很多专业知识。 尽管本文试图描述编写嵌入式C程序所需的各种技能,但本文无法描述各个方面的各个方面,因此本文最终将列出一些阅读书籍,其中大多数书籍都是真正的大师的经验。 站在巨人的肩膀上,您可以进一步看到。
7.1关于语言特征7.2关于编译器7.3关于防御性编程7.4关于编程想法