编写优质的嵌入式C程序代码(下)

一起学嵌入式 2023-03-22 11:16

点击下方一起学嵌入式】关注,一起学习,一起成长

由于文章比较长,分为两篇来发。上篇的文章链接如下:
编写优质的嵌入式 C 程序代码(上)
本文是第二篇(下篇)。继续!

4. 防御性编程

嵌入式产品的可靠性自然与硬件密不可分,但在硬件确定、并且没有第三方测试的前提下,使用防御性编程思想写出的代码,往往具有更高的稳定性。
防御性编程首先需要认清 C 语言的种种缺陷和陷阱,C 语言对于运行时的检查十分弱小,需要程序员谨慎的考虑代码,在必要的时候增加判断;防御性编程的另一个核心思想是假设代码运行在并不可靠的硬件上,外接干扰有可能会打乱程序执行顺序、更改 RAM 存储数据等等。

4.1 具有形参的函数,需判断传递来的实参是否合法。

程序员可能无意识的传递了错误参数;外界的强干扰可能将传递的参数修改掉,或者使用随机参数意外的调用函数,因此在执行函数主体前,需要先确定实参是否合法。

1. int exam_fununsigned 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. }
在使用一些库函数时,同样需要对边界进行检查,比如下面的memset(RecBuf,0,len) 函数把 RecBuf 指指向的内存区的前 len 个字节用 0 填充,如果不注意 len 的长度,就会将数组 RecBuf 之外的内存区清零:
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 除法运算,只检测除数为零就可靠吗?

除法运算前,检查除数是否为零几乎已经成为共识,但是仅检查除数是否为零就够了吗?

考虑两个整数相除,对于一个 signed long 类型变量,它能表示的数值范围为:-2147483648 ~+2147483647,如果让-2147483648/ -1,那么结果应该是+2147483648,但是这个结果已经超出了signedlong所能表示的范围了。所以,在这种情况下,除了要检测除数是否为零外,还要检测除法是否溢出。

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 如果有硬件看门狗,则使用它

在其它一切措施都失效的情况下,看门狗可能是最后的防线。它的原理特别简单,但却能大大提高设备的可靠性。如果设备有硬件看门狗,一定要为它编写驱动程序。

  • 要尽可能早的开启看门狗

这是因为从上电复位结束到开启看门狗的这段时间内,设备有可能被干扰而跳过看门狗初始化程序,导致看门狗失效。尽可能早的开启看门狗,可以降低这种概率;

  • 不要在中断中喂狗,除非有其他联动措施

在中断程序喂狗,由于干扰的存在,程序可能一直处于中断之中,这样会导致看门狗失效。如果在主程序中设置标志位,中断程序喂狗时与这个标志位联合判断,也是允许的;

  • 喂狗间隔跟产品需求有关,并非特定的时间

产品的特性决定了喂狗间隔。对于不涉及安全性、实时性的设备,喂狗间隔比较宽松,但间隔时间不宜过长,否则被用户感知到,是影响用户体验的。对于设计安全性、有实时控制类的设备,原则是尽可能快的复位,否则会造成事故。

克莱门汀号在进行第二阶段的任务时,原本预订要从月球飞行到太空深处的Geographos 小行星进行探勘,然而这艘太空探测器在飞向小行星时却由于一个软件缺陷而使其中断运作 20 分钟,不但未能到达小行星,也因为控制喷嘴燃烧了 11 分钟使电力供应降低,无法再透过远端控制探测器,最终结束这项任务,但也导致了资源与资金的浪费。
“克莱门汀太空任务失败这件事让我感到十分震惊,它其实可以透过硬件中一款简单的看门狗计时器避免掉这项意外,但由于当时的开发时间相当紧缩,程序设计人员没时间编写程序来启动它,”Ganssle说。

遗憾的是,1998年发射的近地号太空船(NEAR)也遇到了相同的问题。由于编程人员并未采纳建议,因此,当推进器减速器系统故障时,29公斤的储备燃料也随之报销──这同样是一个本来可经由看门狗定时器编程而避免的问题,同时也证明要从其他程序设计人员的错误中学习并不容易。

4.7 关键数据储存多个备份,取数据采用“表决法”

RAM 中的数据在受到干扰情况下有可能被改变,对于系统关键数据应该进行保护。关键数据包括全局变量、静态变量以及需要保护的数据区域。备份数据与原数据不应该处于相邻位置,因此不应由编译器默认分配备份数据位置,而应该由程序员指定区域存储。可以将 RAM 分为 3 个区域,第一个区域保存原码,第二个区域保存反码,第三个区域保存异或码,区域之间预留一定量的“空白 ”RAM 作为隔离。可以使用编译器的“分散加载”机制将变量分别存储在这些区域。需要进行读取时,同时读出3份数据并进行表决,取至少有两个相同的那个值。

假如设备的 RAM 从 0x1000_0000 开始,我需要在 RAM 的0x1000_0000~0x10007FFF 内存储原码,在 0x1000_9000~0x10009FFF 内存储反码,在 0x1000_B000~0x1000BFFF 内存储 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、EEPROM、铁电。仅仅将写入非易失性存储器中的数据再读出校验是不够的。强干扰情况下可能导致非易失性存储器内的数据错误,在写非易失性存储器的期间系统掉电将导致数据丢失,因干扰导致程序跑飞到写非易失性存储器函数中,将导致数据存储紊乱。一种可靠的办法是将非易失性存储器分成多个区,每个数据都将按照不同的形式写入到这些分区中,需要进行读取时,同时读出多份数据并进行表决,取相同数目较多的那个值。

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. }
该程序段是编程 lpc1778 内部Flash,其中调用 IAP 程序的函数iap_entry(paramin, paramout) 是关键安全代码,所以在执行该代码前,先判断一个特定设置的安全锁标志 ProgStart,只有这个标志符合设定值,才会执行编程 Flash 操作。如果因为意外程序跑飞到该函数,由于 ProgStart 标志不正确,是不会对 Flash 进行编程的。

4.10 通信

通讯线上的数据误码相对严重,通讯线越长,所处的环境越恶劣,误码会越严重。抛开硬件和环境的作用,我们的软件应能识别错误的通讯数据。对此有一些应用措施:
  • 制定协议时,限制每帧的字节数;

每帧字节数越多,发生误码的可能性就越大,无效的数据也会越多。对此以太网规定每帧数据不大于1500字节,高可靠性的 CAN 收发器规定每帧数据不得多于 8 字节,对于 RS485,基于 RS485 链路应用最广泛的 Modbus 协议一帧数据规定不超过 256 字节。因此,建议制定内部通讯协议时,使用 RS485 时规定每帧数据不超过 256 字节;

  • 使用多种校验

编写程序时应使能奇偶校验,每帧超过 16 字节的应用,建议至少编写 CRC16 校验程序;

  • 增加额外判断

1) 增加缓冲区溢出判断。这是因为数据接收多是在中断中完成,编译器检测不出缓冲区是否溢出,需要手动检查,在上文介绍数据溢出一节中已经详细说明。

2) 增加超时判断。当一帧数据接收到一半,长时间接收不到剩余数据,则认为这帧数据无效,重新开始接收。可选,跟不同的协议有关,但缓冲区溢出判断必须实现。这是因为对于需要帧头判断的协议,上位机可能发送完帧头后突然断电,重启后上位机是从新的帧开始发送的,但是下位机已经接收到了上次未发送完的帧头,所以上位机的这次帧头会被下位机当成正常数据接收。这有可能造成数据长度字段为一个很大的值,填满该长度的缓冲区需要相当多的数据(比如一帧可能1000字节),影响响应时间;另一方面,如果程序没有缓冲区溢出判断,那么缓冲区很可能溢出,后果是灾难性的。

  • 重传机制

如果检测到通讯数据发生了错误,则要有重传机制重新发送出错的帧。

4.11 开关量输入的检测、确认

开关量容易受到尖脉冲干扰,如果不进行滤除,可能会造成误动作。一般情况下,需要对开关量输入信号进行多次采样,并进行逻辑判断直到确认信号无误为止。

4.12 开关量输出

开关信号简单的一次输出是不安全的,干扰信号可能会翻转开关量输出的状态。采取重复刷新输出可以有效防止电平的翻转。

4.13 初始化信息的保存和恢复

微处理器的寄存器值也可能会因外界干扰而改变,外设初始化值需要在寄存器中长期保存,最容易被破坏。由于 Flash 中的数据相对不易被破坏,可以将初始化信息预先写入 Flash,待程序空闲时比较与初始化相关的寄存器值是否被更改,如果发现非法更改则使用 Flash 中的值进行恢复。

公司目前使用的 4.3 寸 LCD 显示屏抗干扰能力一般。如果显示屏与控制器之间的排线距离过长或者对使用该显示屏的设备打静电或者脉冲群,显示屏有可能会花屏或者白屏。对此,我们可以将初始化显示屏的数据保存在 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. };
实现函数如下所示,函数会遍历结构体变量中的每一个命令,以及每一个命令下的初始值,如果有一个不正确,则跳出循环,执行重新初始化和恢复措施。这个函数中的 MY_DEBUGF 宏是我自己的调试函数,使用串口打印调试信息,在接下来的第五部分将详细叙述。通过这个函数,我可以长时间监控显示屏的哪些命令、哪些位容易被干扰。程序里使用了一个被妖魔化的关键字: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;j17.         {
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 或者 Cortex-M 系列单片机,硬件已经内建了多种异常,软件需要根据硬件异常来编写陷阱程序,用来快速定位甚至恢复错误。

4.15 阻塞处理

有时候程序员会使用 while(!flag); 语句阻塞在此等待标志 flag 改变,比如串口发送时用来等待一字节数据发送完成。这样的代码时存在风险的,如果因为某些原因标志位一直不改变则会造成系统死机。

一个良好冗余的程序是设置一个超时定时器,超过一定时间后,强制程序退出while循环。

2003年8月11日发生的 W32.Blaster.Worm 蠕虫事件导致全球经济损失高达5亿美元,这个漏洞是利用了 Windows 分布式组件对象模型的远程过程调用接口中的一个逻辑缺陷:在调用 GetMachineName() 函数时,循环只设置了一个不充分的结束条件。

原代码简化如下所示:

1. HRESULT GetMachineName ( WCHAR *pwszPath,  
2. WCHAR wszMachineName[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 解决了这个问题,为 GetMachineName()函数设置了充分终止条件。一个解决代码简化如下所示(并非微软补丁代码):

1. HRESULT GetMachineName( WCHAR *pwszPath,  
2. WCHAR wszMachineName[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 行,就测试它。

程序员对自己的代码以及逻辑关系十分清楚,测试时,按照每一个逻辑分支全面测试。很多错误发生在我们认为不会出错的地方,所以即便某个逻辑分支很简单,也建议测试一遍。第一个原因是我们自己看自己的代码总是不容易发现错误,而测试能暴露这些错误;另一方面,语法正确、逻辑正确的代码,经过编译器编译后,生成的汇编代码很可能与你的逻辑相差甚远。

比如我们前文提及的使用 volatile 以及不使用 volatile 关键字编译后生成的汇编代码,再比如我们用低优化级别编译和使用高优化级别编译后生成的汇编代码,都可能相差很大,实际运行测试,可以暴漏这些隐含错误。最后,虽然可能性极小,编译器本身也可能有 BUG,特别是构造复杂表达式的情况下(应极力避免复杂表达式)。

5.1 使用硬件调试器测试

使用硬件调试器(比如 J-link)测试是最通用的手段。可以单步运行、设置断点,可以很方便的查看当前寄存器、变量的值。在寻找缺陷方面,使用硬件调试器测试是最简单却又最有效的手段。

硬件调试器已经在公司普遍使用,这方面的测试不做介绍,想必大家都已经很熟悉了。

5.2 有些缺陷很难缠

就像没有一种方法能完美解决所有问题,在实际项目中,硬件调试器也有难以触及的地方。可以举几个例子说明:

  • 使用了比较大的协议栈,需要跟进到协议栈内部调试的缺陷

比如公司使用 lwIP 协议栈,如果跟踪数据的处理过程,需要从接收数据开始一直到应用层处理数据,之间会经过驱动层、IP层、TCP层和应用层,会经过十几个文件几十个函数,使用硬件调试器跟踪费时费力;

  • 具有随机性的缺陷

有一些缺陷,可能是不定时出现的,有可能是几分钟出现,也有可能是几个小时甚至几天才出现,像这样的缺陷很难用硬件调试器捕捉到;

  • 需要外界一系列有时间限制的输入条件触发,但这一过程中有缺陷
    比如我们用组合键来完成某个功能,规定按下按键 1 不小于3秒后松开,然后在 6 秒内分别按下按键2、按键3、按键4这三个按键来执行我们的特定程序,要测试类似这种过程,硬件调试器很难做到;
    除了测试缺陷需要,有时候我们在做稳定性测试时,需要知道软件每时每刻运行到那些分支、执行了哪些操作、我们关心的变量当前值是什么等等,这些都表明,我们还需要一种和硬件调试器互补的测试手段。
    这个测试手段就是在程序中增加额外调试语句,当程序运行时,通过这些调试语句将运行信息输出到可以方便查看的设备上,可以是PC机、LCD显示屏、存储卡等等。
    以串口输出到PC机为例,下面提供完整的测试思路。在此之前,我们先对这种测试手段提一些要求:
  • 必须简单易用

我们在初学C语言的时候,都接触过printf函数,这个函数可以方便的输出信息,并可以将各种变量格式化为指定格式的字符串,我们应当提供类似的函数;

  • 调试语句必须方便的从代码中移除

在编码阶段,我们可能会往程序中加入大量的调试语句,但是程序发布时,需要将这些调试语句从代码中移除,这将是件恐怖的过程。我们必须提供一种策略,可以方便的移除这些调试语句。

5.2.1 简单易用的调试函数

  1. 使用库函数 printf。以 MDK 为例,方法如下:

I> 初始化串口

II> 重构 fputc 函数,printf 函数会调用 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>  在 Options for Targer 窗口,Targer 标签栏下,勾选 Use MicroLIB 前的复选框以便避免使用半主机功能。(注:标准 C 库 printf 函数默认开启半主机功能,如果非要使用标准 C 库,请自行查阅资料)
  1. 构建自己的调试函数
使用库函数比较方便,但也少了一些灵活性,不利于随心所欲的定制输出格式。自己编写类似 printf 函数则会更灵活一些,而且不依赖任何编译器。下面给出一个完整的类 printf 函数实现,该函数支持有限的格式参数,使用方法与库函数一致。同库函数类似,该也需要提供一个底层串口发送函数(原型为:int32_t UARTwrite(const uint8_t *pcBuf, uint32_t 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;          //'%08x'->不足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 - 11);
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 对调试函数进一步封装

上文说到,我们增加的调试语句应能很方便的从最终发行版中去掉,因此我们不能直接调用 printf 或者自定义的 UARTprintf 函数,需要将这些调试函数做一层封装,以便随时从代码中去除这些调试语句。参考方法如下:

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 */

在我们编码测试期间,定义宏 MY_DEBUG,并使用宏 MY_DEBUGF(注意比前面那个宏多了一个‘F’)输出调试信息。经过预处理后,宏MY_DEBUGF(message) 会被 UARTprintf message 代替,从而实现了调试信息的输出;当正式发布时,只需要将宏 MY_DEBUG 注释掉,经过预处理后,所有MY_DEBUGF(message) 语句都会被空格代替,而从将调试信息从代码中去除掉。

6. 编程思想

6.1 编程风格

《计算机程序的构造和解释》一书在开篇写到:程序写出来是给人看的,附带能在机器上运行。

6.1.1 整洁的样式

使用什么样的编码样式一直都颇具争议性的,比如缩进和大括号的位置。因为编码的样式也会影响程序的可读性,面对一个乱放括号、对齐都不一致的源码,我们很难提起阅读它的兴趣。我们总要看别人的程序,如果彼此编码样式相近,读起源码来会觉得比较舒适。但是编码风格的问题是主观的,永远不可能在编码风格上达成统一意见。因此只要你的编码样式整洁、结构清晰就足够了。除此之外,对编码样式再没有其它要求。

提出匈牙利命名法的程序员、前微软首席架构师 Charles Simonyi 说:我觉得代码清单带给人的愉快同整洁的家差不多。你一眼就能分辨出家里是杂乱无章还是整洁如新。这也许意义不大。因为光是房子整洁说明不了什么,它仍可能藏污纳垢!但是第一印象很重要,它至少反映了程序的某些方面。我敢打赌,我在 3 米开外就能看出程序拙劣与否。我也许没法保证它很不错,但如果从 3 米外看起来就很糟,我敢保证这程序写得不用心。如果写得不用心,那它在逻辑上也许就不会优美。

6.1.2 清晰的命名

变量、函数、宏等等都需要命名,清晰的命名是优秀代码的特点之一。命名的要点之一是名称应能清晰的描述这个对象,以至于一个初级程序员也能不费力的读懂你的代码逻辑。我们写的代码主要给谁看是需要思考的:给自己、给编译器还是给别人看?我觉得代码最主要的是给别人看,其次是给自己看。如果没有一个清晰的命名,别人在维护你的程序时很难在整个全貌上看清代码,因为要记住十多个以上的糟糕命名的变量是件非常困难的事;而且一段时间之后你回过头来看自己的代码,很有可能不记得那些糟糕命名的变量是什么意思。
为对象起一个清晰的名字并不是简单的事情。首先能认识到名称的重要性需要有一个过程,这也许跟谭式 C 程序教材被大学广泛使用有关:满书的 a、b、c、x、y、z 变量名是很难在关键的初学阶段给人传达优秀编程思想的;其次如何恰当的为对象命名也很有挑战性,要准确、无歧义、不罗嗦,要对英文有一定水平,所有这些都要满足时,就会变得很困难;此外,命名还需要考虑整体一致性,在同一个项目中要有统一的风格,坚持这种风格也并不容易。

关于如何命名,Charles Simonyi 说:面对一个具备某些属性的结构,不要随随便便地取个名字,然后让所有人去琢磨名字和属性之间有什么关联,你应该把属性本身,用作结构的名字。

6.1.3 恰当的注释

注释向来也是争议之一。不加注释和过多的注释,我都是反对的。不加注释的代码显然是很糟糕的,但过多的注释也会妨碍程序的可读性,由于注释可能存在的歧义,有可能会误解程序真实意图,此外,过多的注释会增加程序员不必要的时间。如果你的编码样式整洁、命名又很清晰,那么,你的代码可读性不会差到哪去,而注释的本意就是为了便于理解程序。

这里建议使用良好的编码样式和清晰的命名来减少注释,对模块、函数、变量、数据结构、算法和关键代码做注释,应重视注释的质量而不是数量。如果你需要一大段注释才能说清楚程序做什么,那么你应该注意了:是否是因为程序变量命名不够清晰,或者代码逻辑过于混乱,这个时候你应该考虑的可能就不是注释,而是如何精简这个程序了。

6.2 数据结构

数据结构是程序设计的基础。在设计程序之前,应该先考虑好所需要的数据结构。

前微软首席架构师 Charles Simonyi:编程的第一步是想象。就是要在脑海中对来龙去脉有极为清晰的把握。在这个初始阶段,我会使用纸和铅笔。我只是信手涂鸦,并不写代码。我也许会画些方框或箭头,但基本上只是涂鸦,因为真正的想法在我脑海里。我喜欢想象那些有待维护的结构,那些结构代表着我想编码的真实世界。一旦这个结构考虑得相当严谨和明确,我便开始写代码。我会坐到终端前,或者换在以前的话,就会拿张白纸,开始写代码。这相当容易。我只要把头脑中的想法变换成代码写下来,我知道结果应该是什么样的。大部分代码会水到渠成,不过我维护的那些数据结构才是关键。我会先想好数据结构,并在整个编码过程中将它们牢记于心。

开发过以太网和操作系统 SDS 940 的 Butler Lampson:(程序员)最重要的素质是能够把问题的解决方案组织成容易操控的结构。

开发 CP/M 操作系统的 Gary.A:如果不能确认数据结构是正确的,我是决不会开始编码的。我会先画数据结构,然后花很长时间思考数据结构。在确定数据结构之后我就开始写一些小段的代码,并不断地改善和监测。在编码过程中进行测试可以确保所做的修改是局部的,并且如果有什么问题的话,能够马上发现。

微软创始人比尔·盖茨:编写程序最重要的部分是设计数据结构。接下来重要的部分是分解各种代码块。

编写世界上第一个电子表格软件的 Dan Bricklin:在我看来,写程序最重要的部分是设计数据结构,此外,你还必须知道人机界面会是什么样的。

我们举个例子来说明。在介绍防御性编程的时候,提到公司使用的 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_command 表示的是LCD寄存器命令号;lcd_get_value是一个数组,表示寄存器要初始化的值,这是因为对于一个LCD寄存器,可能要初始化多个字节,这是硬件特性决定的;lcd_value_num是指一个寄存器要多少个字节的初值,这是因为每一个寄存器的初值数目是不同的,我们用同一个方法处理数据时,是需要这个信息的。

就本例而言,我们将要处理的数据都是事先固定的,所以定义好数据结构后,我们可以将这些数据组织成表格:

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;j17.         {
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、Cortex-M3 这些微处理器上,但在这些处理器上如何编写优质高效的 C 程序,几乎没有书籍做专门介绍。本文试图在这方面做一些努力。编写优质嵌入式 C 程序需要大量的专业知识,本文虽尽力描述编写嵌入式 C程序所需要的各种技能,但本文却无力将每一个方面都面面俱到的描述出来,所以本文最后会列举一些阅读书目,这些书大多都是真正大师的经验之谈。站在巨人的肩膀上,可以看的更远。

7.1 关于语言特性

  • Stephen Prata 著 云巅工作室 译 《C Primer Plus(第五版)中文版》
  • Andrew Koenig 著 高巍 译 《C陷阱与缺陷》
  • Peter Van Der Linden 著 徐波 译 《C专家编程》
  • 陈正冲 编著 《C语言深度解剖》

7.2 关于编译器

  • 杜春雷 编著 《ARM体系结构与编程》
  • Keil MDK 编译器帮助手册

7.3 关于防御性编程

  • MISRA-C-:2004 Guidelines for the use of the C language in criticalsystems
  • Robert C.Seacord 著 徐波 译 《C安全编码标准》

7.4 关于编程思想

  • Pete Goodliffe 著 韩江、陈玉 译 《编程匠艺---编写卓越的代码》
  • Susan Lammers 著 李琳骁、吴咏炜、张菁《编程大师访谈录》

原文:http://blog.csdn.net/zhzht19861011/article/details/45508029

文章来源于网络,版权归原作者所有,如有侵权,请联系删除。



关注我【一起学嵌入式】,一起学习,一起成长。



觉得文章不错,点击“分享”、“”、“在看” 呗!

一起学嵌入式 公众号【一起学嵌入式】,RTOS、Linux编程、C/C++,以及经验分享、行业资讯、物联网等技术知
评论
  • 戴上XR眼镜去“追龙”是种什么体验?2024年11月30日,由上海自然博物馆(上海科技馆分馆)与三湘印象联合出品、三湘印象旗下观印象艺术发展有限公司(下简称“观印象”)承制的《又见恐龙》XR嘉年华在上海自然博物馆重磅开幕。该体验项目将于12月1日正式对公众开放,持续至2025年3月30日。双向奔赴,恐龙IP撞上元宇宙不久前,上海市经济和信息化委员会等部门联合印发了《上海市超高清视听产业发展行动方案》,特别提到“支持博物馆、主题乐园等场所推动超高清视听技术应用,丰富线下文旅消费体验”。作为上海自然
    电子与消费 2024-11-30 22:03 107浏览
  • TOF多区传感器: ND06   ND06是一款微型多区高集成度ToF测距传感器,其支持24个区域(6 x 4)同步测距,测距范围远达5m,具有测距范围广、精度高、测距稳定等特点。适用于投影仪的无感自动对焦和梯形校正、AIoT、手势识别、智能面板和智能灯具等多种场景。                 如果用ND06进行手势识别,只需要经过三个步骤: 第一步&
    esad0 2024-12-04 11:20 103浏览
  • 光伏逆变器是一种高效的能量转换设备,它能够将光伏太阳能板(PV)产生的不稳定的直流电压转换成与市电频率同步的交流电。这种转换后的电能不仅可以回馈至商用输电网络,还能供独立电网系统使用。光伏逆变器在商业光伏储能电站和家庭独立储能系统等应用领域中得到了广泛的应用。光耦合器,以其高速信号传输、出色的共模抑制比以及单向信号传输和光电隔离的特性,在光伏逆变器中扮演着至关重要的角色。它确保了系统的安全隔离、干扰的有效隔离以及通信信号的精准传输。光耦合器的使用不仅提高了系统的稳定性和安全性,而且由于其低功耗的
    晶台光耦 2024-12-02 10:40 143浏览
  • 艾迈斯欧司朗全新“样片申请”小程序,逾160种LED、传感器、多芯片组合等产品样片一触即达。轻松3步完成申请,境内免费包邮到家!本期热荐性能显著提升的OSLON® Optimal,GF CSSRML.24ams OSRAM 基于最新芯片技术推出全新LED产品OSLON® Optimal系列,实现了显著的性能升级。该系列提供五种不同颜色的光源选项,包括Hyper Red(660 nm,PDN)、Red(640 nm)、Deep Blue(450 nm,PDN)、Far Red(730 nm)及Ho
    艾迈斯欧司朗 2024-11-29 16:55 186浏览
  • 学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习笔记&记录学习习笔记&记学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&
    youyeye 2024-11-30 14:30 85浏览
  • 11-29学习笔记11-29学习笔记习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习笔记&记录学习习笔记&记学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&学习学习笔记&记录学习学习笔记&记录学习学习笔记&记
    youyeye 2024-12-02 23:58 92浏览
  •         温度传感器的精度受哪些因素影响,要先看所用的温度传感器输出哪种信号,不同信号输出的温度传感器影响精度的因素也不同。        现在常用的温度传感器输出信号有以下几种:电阻信号、电流信号、电压信号、数字信号等。以输出电阻信号的温度传感器为例,还细分为正温度系数温度传感器和负温度系数温度传感器,常用的铂电阻PT100/1000温度传感器就是正温度系数,就是说随着温度的升高,输出的电阻值会增大。对于输出
    锦正茂科技 2024-12-03 11:50 141浏览
  • 最近几年,新能源汽车愈发受到消费者的青睐,其销量也是一路走高。据中汽协公布的数据显示,2024年10月,新能源汽车产销分别完成146.3万辆和143万辆,同比分别增长48%和49.6%。而结合各家新能源车企所公布的销量数据来看,比亚迪再度夺得了销冠宝座,其10月新能源汽车销量达到了502657辆,同比增长66.53%。众所周知,比亚迪是新能源汽车领域的重要参与者,其一举一动向来为外界所关注。日前,比亚迪汽车旗下品牌方程豹汽车推出了新车方程豹豹8,该款车型一上市就迅速吸引了消费者的目光,成为SUV
    刘旷 2024-12-02 09:32 138浏览
  • 当前,智能汽车产业迎来重大变局,随着人工智能、5G、大数据等新一代信息技术的迅猛发展,智能网联汽车正呈现强劲发展势头。11月26日,在2024紫光展锐全球合作伙伴大会汽车电子生态论坛上,紫光展锐与上汽海外出行联合发布搭载紫光展锐A7870的上汽海外MG量产车型,并发布A7710系列UWB数字钥匙解决方案平台,可应用于数字钥匙、活体检测、脚踢雷达、自动泊车等多种智能汽车场景。 联合发布量产车型,推动汽车智能化出海紫光展锐与上汽海外出行达成战略合作,联合发布搭载紫光展锐A7870的量产车型
    紫光展锐 2024-12-03 11:38 126浏览
  • 作为优秀工程师的你,已身经百战、阅板无数!请先醒醒,新的项目来了,这是一个既要、又要、还要的产品需求,ARM核心板中一个处理器怎么能实现这么丰富的外围接口?踌躇之际,你偶阅此文。于是,“潘多拉”的魔盒打开了!没错,USB资源就是你打开新世界得钥匙,它能做哪些扩展呢?1.1  USB扩网口通用ARM处理器大多带两路网口,如果项目中有多路网路接口的需求,一般会选择在主板外部加交换机/路由器。当然,出于成本考虑,也可以将Switch芯片集成到ARM核心板或底板上,如KSZ9897、
    万象奥科 2024-12-03 10:24 90浏览
  • 《高速PCB设计经验规则应用实践》+PCB绘制学习与验证读书首先看目录,我感兴趣的是这一节;作者在书中列举了一条经典规则,然后进行详细分析,通过公式推导图表列举说明了传统的这一规则是受到电容加工特点影响的,在使用了MLCC陶瓷电容后这一条规则已经不再实用了。图书还列举了高速PCB设计需要的专业工具和仿真软件,当然由于篇幅所限,只是介绍了一点点设计步骤;我最感兴趣的部分还是元件布局的经验规则,在这里列举如下:在这里,演示一下,我根据书本知识进行电机驱动的布局:这也算知行合一吧。对于布局书中有一句:
    wuyu2009 2024-11-30 20:30 142浏览
  • 遇到部分串口工具不支持1500000波特率,这时候就需要进行修改,本文以触觉智能RK3562开发板修改系统波特率为115200为例,介绍瑞芯微方案主板Linux修改系统串口波特率教程。温馨提示:瑞芯微方案主板/开发板串口波特率只支持115200或1500000。修改Loader打印波特率查看对应芯片的MINIALL.ini确定要修改的bin文件#查看对应芯片的MINIALL.ini cat rkbin/RKBOOT/RK3562MINIALL.ini修改uart baudrate参数修改以下目
    Industio_触觉智能 2024-12-03 11:28 109浏览
  • 概述 说明(三)探讨的是比较器一般带有滞回(Hysteresis)功能,为了解决输入信号转换速率不够的问题。前文还提到,即便使能滞回(Hysteresis)功能,还是无法解决SiPM读出测试系统需要解决的问题。本文在说明(三)的基础上,继续探讨为SiPM读出测试系统寻求合适的模拟脉冲检出方案。前四代SiPM使用的高速比较器指标缺陷 由于前端模拟信号属于典型的指数脉冲,所以下降沿转换速率(Slew Rate)过慢,导致比较器检出出现不必要的问题。尽管比较器可以使能滞回(Hysteresis)模块功
    coyoo 2024-12-03 12:20 170浏览
  • RDDI-DAP错误通常与调试接口相关,特别是在使用CMSIS-DAP协议进行嵌入式系统开发时。以下是一些可能的原因和解决方法: 1. 硬件连接问题:     检查调试器(如ST-Link)与目标板之间的连接是否牢固。     确保所有必要的引脚都已正确连接,没有松动或短路。 2. 电源问题:     确保目标板和调试器都有足够的电源供应。     检查电源电压是否符合目标板的规格要求。 3. 固件问题: &n
    丙丁先生 2024-12-01 17:37 114浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦