一、嵌入式开发C语言中的uint8_t

  在嵌入式开发中的C语言代码中,经常可以看到类似uint8_t、uint16_t、uint32_t、uint64_t这种数据类型,在教材中却从来没见过。实际上这些数据类型都是某种数据类型的别名。

    比如,在定义函数时用到了uint8_t。

51c~嵌入式C语言~合集1_嵌入式开发

    右键“uint8_t”,单击“Go To Definition Of 'uint8_t'”,可以在在工程文件stdint.h中看到下图代码。

51c~嵌入式C语言~合集1_嵌入式开发_02

    从上面代码可以知道,在这里uint8_t被定义为unsigned char的别名,也就是说uint_8t代指unsigned char(无符号字符型)。

    为什么要这样做呢?直观上来看是可以少打很多字母。实际上,这也是一种良好的编程习惯,让你的代码优美如诗。

    如下代码是类似的别名,其中signed就是有符号的意思:

/* exact-width signed integer types */
typedef   signed          char int8_t;
typedef   signed short     int int16_t;
typedef   signed           int int32_t;
typedef   signed       __INT64 int64_t;


/* exact-width unsigned integer types */
typedef unsigned          char uint8_t;
typedef unsigned short     int uint16_t;
typedef unsigned           int uint32_t;
typedef unsigned       __INT64 uint64_t;










二、总结嵌入式C语言知识点

  怎么才能做好嵌入式开发?学好C语言吧!今天就来推荐一篇大佬写的嵌入式C语言知识点总结。

C语言中的关键字

    C语言中的关键字按照功能分为:

  • 数据类型(常用char, short, int, long, unsigned, float, double)
  • 运算和表达式( =, +, -, *, while, do-while, if, goto, switch-case)
  • 数据存储(auto, static, extern,const, register,volatile,restricted),
  • 结构(struct, enum, union,typedef),
  • 位操作和逻辑运算(<<, >>, &, |, ~,^, &&),
  • 预处理(#define, #include, #error,#if...#elif...#else...#endif等),
  • 平台扩展关键字(__asm, __inline,__syscall)

    这些关键字共同构成了嵌入式平台的C语言语法。嵌入式的应用从逻辑上可以抽象为三个部分:

  • 数据的输入,如传感器,信号,接口输入
  • 数据的处理,如协议的解码和封包,AD采样值的转换等
  • 数据的输出,如GUI的显示,输出的引脚状态,DA的输出控制电压,PWM波的占空比等

    对于数据的管理就贯穿着整个嵌入式应用的开发,它包含数据类型,存储空间管理,位和逻辑操作,以及数据结构,C语言从语法上支撑上述功能的实现,并提供相应的优化机制,以应对嵌入式下更受限的资源环境。

数据类型

    C语言支持常用的字符型,整型,浮点型变量,有些编译器如keil还扩展支持bit(位)和sfr(寄存器)等数据类型来满足特殊的地址操作。C语言只规定了每种基本数据类型的最小取值范围,因此在不同芯片平台上相同类型可能占用不同长度的存储空间,这就需要在代码实现时考虑后续移植的兼容性,而C语言提供的typedef就是用于处理这种情况的关键字,在大部分支持跨平台的软件项目中被采用,典型的如下:

typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
......
typedef signed int int32_t;

    既然不同平台的基本数据宽度不同,那么如何确定当前平台的基础数据类型如int的宽度,这就需要C语言提供的接口sizeof,实现如下。

printf("int size:%d, short size:%d, char size:%d\n", sizeof(int), sizeof(char), sizeof(short));

    这里还有重要的知识点,就是指针的宽度,如:

char *p;
printf("point p size:%d\n", sizeof(p));

    其实这就和芯片的可寻址宽度有关,如32位MCU的宽度就是4,64位MCU的宽度就是8,在有些时候这也是查看MCU位宽比较简单的方式。

内存管理和存储架构

    C语言允许程序变量在定义时就确定内存地址,通过作用域,以及关键字extern,static,实现了精细的处理机制,按照在硬件的区域不同,内存分配有三种方式(节选自C++高质量编程):

  • 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。
  • 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中 ,效率很高,但是分配的内存容量有限。
  • 从堆上分配,亦称动态内存分配。程序在运行的时候用 malloc 或 new 申请任意多少的内存,程序员自己负责在何时用 free 或 delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活,但同时遇到问题也最多。

    这里先看个简单的C语言实例。

//main.c#include <stdio.h>#include <stdlib.h>


static int st_val;                   //静态全局变量 -- 静态存储区
int ex_val;                           //全局变量 -- 静态存储区int main(void)
{
   int a = 0;                         //局部变量 -- 栈上申请
   int *ptr = NULL;                   //指针变量
   static int local_st_val = 0;       //静态变量
   local_st_val += 1;
   a = local_st_val;
   ptr = (int *)malloc(sizeof(int)); //从堆上申请空间
   if(ptr != NULL)
   {      
    printf("*p value:%d", *ptr);
    free(ptr);      
    ptr = NULL;      
    //free后需要将ptr置空,否则会导致后续ptr的校验失效,出现野指针   
    }            
}

    C语言的作用域不仅描述了标识符的可访问的区域,其实也规定了变量的存储区域,在文件作用域的变量st_val和ex_val被分配到静态存储区,其中static关键字主要限定变量能否被其它文件访问,而代码块作用域中的变量a, ptr和local_st_val则要根据类型的不同,分配到不同的区域,其中a是局部变量,被分配到栈中,ptr作为指针,由malloc分配空间,因此定义在堆中,而local_st_val则被关键字限定,表示分配到静态存储区,这里就涉及到重要知识点,static在文件作用域和代码块作用域的意义是不同的:在文件作用域用于限定函数和变量的外部链接性(能否被其它文件访问), 在代码块作用域则用于将变量分配到静态存储区。

    对于C语言,如果理解上述知识对于内存管理基本就足够,但对于嵌入式C来说,定义一个变量,它不一定在内存(SRAM)中,也有可能在FLASH空间,或直接由寄存器存储(register定义变量或者高优化等级下的部分局部变量),如定义为const的全局变量定义在FLASH中,定义为register的局部变量会被优化到直接放在通用寄存器中,在优化运行速度,或者存储受限时,理解这部分知识对于代码的维护就很有意义。此外,嵌入式C语言的编译器中会扩展内存管理机制,如支持分散加载机制和__attribute__((section("用户定义区域"))),允许指定变量存储在特殊的区域如(SDRAM, SQI FLASH), 这强化了对内存的管理,以适应复杂的应用环境场景和需求。

LD_ROM 0x00800000 0x10000 { ;load region size_region
    EX_ROM 0x00800000 0x10000 { ;load address = execution address
  *.o (RESET, +First)
  *(InRoot$$Sections)
  .ANY (+RO)
  }
  EX_RAM 0x20000000 0xC000 { ;rw Data
    .ANY (+RW +ZI)
  }
  EX_RAM1 0x2000C000 0x2000 {
    .ANY(MySection)
   }
  EX_RAM2 0x40000000 0x20000{
    .ANY(Sdram)
  }
}


int a[10] __attribute__((section("Mysection")));
int b[100] __attribute__((section("Sdram")));

    采用这种方式,我们就可以将变量指定到需要的区域,这在某些情况下是必须的,如做GUI或者网页时因为要存储大量图片和文档,内部FLASH空间可能不足,这时就可以将变量声明到外部区域,另外内存中某些部分的数据比较重要,为了避免被其它内容覆盖,可能需要单独划分SRAM区域,避免被误修改导致致命性的错误,这些经验在实际的产品开发中是常用且重要,不过因为篇幅原因,这里只简略的提供例子,如果工作中遇到这种需求,建议详细去了解下。

    至于堆的使用,对于嵌入式Linux来说,使用起来和标准C语言一致,注意malloc后的检查,释放后记得置空,避免"野指针“,不过对于资源受限的单片机来说,使用malloc的场景一般较少,如果需要频繁申请内存块的场景,都会构建基于静态存储区和内存块分割的一套内存管理机制,一方面效率会更高(用固定大小的块提前分割,在使用时直接查找编号处理),另一方面对于内存块的使用可控,可以有效避免内存碎片的问题,常见的如RTOS和网络LWIP都是采用这种机制,我个人习惯也采用这种方式,所以关于堆的细节不在描述,如果希望了解,可以参考<C Primer Plus>中关于存储相关的说明。

指针和数组

    数组和指针往往是引起程序bug的主要原因,如数组越界,指针越界,非法地址访问,非对齐访问,这些问题背后往往都有指针和数组的影子,因此理解和掌握指针和数组,是成为合格C语言开发者的必经之路,相关视频推荐:动画讲解C语言中的指针,从未如此简单!

    数组是由相同类型元素构成,当它被声明时,编译器就根据内部元素的特性在内存中分配一段空间,另外C语言也提供多维数组,以应对特殊场景的需求,而指针则是提供使用地址的符号方法,只有指向具体的地址才有意义,C语言的指针具有最大的灵活性,在被访问前,可以指向任何地址,这大大方便了对硬件的操作,但同时也对开发者有了更高的要求。参考如下代码。

int main(void)
{
char cval[] = "hello";
int i;
int ival[] = {1, 2, 3, 4};
int arr_val[][2] = {{1, 2}, {3, 4}};
const char *pconst = "hello";
char *p;
int *pi;
int *pa;
int **par;


  p = cval;
  p++;            //addr增加1
  pi = ival;
  pi+=1;          //addr增加4
  pa = arr_val[0];
  pa+=1;          //addr增加4
  par = arr_val;
  par++;         //addr增加8
for(i=0; i<sizeof(cval); i++)
  {
printf("%d ", cval[i]);
  }
printf("\n");
printf("pconst:%s\n", pconst);
printf("addr:%d, %d\n", cval, p);
printf("addr:%d, %d\n", icval, pi);
printf("addr:%d, %d\n", arr_val, pa);
printf("addr:%d, %d\n", arr_val, par);
}


/* PC端64位系统下运行结果
0x68 0x65 0x6c 0x6c 0x6f 0x0
pconst:hello
addr:6421994, 6421995
addr:6421968, 6421972
addr:6421936, 6421940
addr:6421936, 6421944 */

    对于数组来说,一般从0开始获取值,以length-1作为结束,通过[0, length)半开半闭区间访问,这一般不会出问题,但是某些时候,我们需要倒着读取数组时,有可能错误的将length作为起始点,从而导致访问越界,另外在操作数组时,有时为了节省空间,将访问的下标变量i定义为unsigned char类型,而C语言中unsigned char类型的范围是0~255,如果数组较大,会导致数组超过时无法截止,从而陷入死循环,这种在最初代码构建时很容易避免,但后期如果更改需求,在加大数组后,在使用数组的其它地方都会有隐患,需要特别注意。

    由于,指针占有的空间与芯片的寻址宽度有关,32位平台为4字节,64位为8字节,而指针的加减运算中的长度又与它的类型相关,如char类型为1,int类型为4,如果你仔细观察上面的代码就会发现par的值增加了8,这是因为指向指针的指针,对应的变量是指针,也就是长度就是指针类型的长度,在64位平台下为8,如果在32位平台则为4,这些知识理解起来并不困难,但是这些特性在工程运用中稍有不慎,就会埋下不易察觉的问题。另外指针还支持强制转换,这在某些情况下相当有用,参考如下代码:

#include <stdio.h>


typedef struct
{
int b;
int a;
}STRUCT_VAL;
static __align(4) char arr[8] = {0x12, 0x23, 0x34, 0x45, 0x56, 0x12, 0x24, 0x53};
int main(void)
{
    STRUCT_VAL *pval;
int *ptr;
    pval = (STRUCT_VAL *)arr;
    ptr = (int *)&arr[4];
printf("val:%d, %d", pval->a, pval->b);
printf("val:%d,", *ptr);
}
//0x45342312 0x53241256
//0x53241256

    基于指针的强制转换,在协议解析,数据存储管理中高效快捷的解决了数据解析的问题,但是在处理过程中涉及的数据对齐,大小端,是常见且十分易错的问题,如上面arr字符数组,通过__align(4)强制定义为4字节对齐是必要的,这里可以保证后续转换成int指针访问时,不会触发非对齐访问异常,如果没有强制定义,char默认是1字节对齐的,当然这并不就是一定触发异常(由整个内存的布局决定arr的地址,也与实际使用的空间是否支持非对齐访问有关,如部分SDRAM使用非对齐访问时,会触发异常), 这就导致可能增减其它变量,就可能触发这种异常,而出异常的地方往往和添加的变量毫无关系,而且代码在某些平台运行正常,切换平台后触发异常,这种隐蔽的现象是嵌入式中很难查找解决的问题。另外,C语言指针还有特殊的用法就是通过强制转换给特定的物理地址访问,通过函数指针实现回调,如下:

#include <stdio.h>


typedef int (*pfunc)(int, int);
int func_add(int a, int b){
return a+b;
}
int main(void)
{
    pfunc *func_ptr;
    *(volatile uint32_t *)0x20001000 = 0x01a23131;
    func_ptr = func_add;
printf("%d\n", func_ptr(1, 2));
}

    这里说明下,volatile易变的,可变的,一般用于以下几种状况:

  • 并行设备的硬件寄存器,如:状态寄存器)
  • 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
  • 多线程应用中被几个任务共享的变量

    volatile可以解决用户模式和异常中断访问同一个变量时,出现的不同步问题,另外在访问硬件地址时,volatile也阻止对地址访问的优化,从而确保访问的实际的地址,精通volatile的运用,在嵌入式底层中十分重要,也是嵌入式C从业者的基本要求之一。函数指针在一般嵌入式软件的开发中并不常见,但对许多重要的实现如异步回调,驱动模块,使用函数指针就可以利用简单的方式实现很多应用,当然我这里只能说是抛砖引玉,许多细节知识是值得详细去了解掌握的。

结构类型和对齐

    C语言提供自定义数据类型来描述一类具有相同特征点的事务,主要支持的有结构体,枚举和联合体。其中枚举通过别名限制数据的访问,可以让数据更直观,易读,实现如下:

typedef enum {spring=1, summer, autumn, winter }season;
season s1 = summer;

    联合体的是能在同一个存储空间里存储不同类型数据的数据类型,对于联合体的占用空间,则是以其中占用空间最大的变量为准,如下:

typedef union{     
  char c;     
  short s;     
  int i; 
}UNION_VAL;


UNION_VAL val; 
int main(void) 
{     
  printf("addr:0x%x, 0x%x, 0x%x\n",            
         (int)(&(val.c)), (int)(&(val.s)), (int)(&(val.i)));     
  val.i = 0x12345678;     
  if(val.s == 0x5678)         
    printf("小端模式\n");       
  else         
    printf("大端模式\n");     
} 
/*
addr:0x407970, 0x407970, 0x407970 
小端模式
*/

    联合体的用途主要通过共享内存地址的方式,实现对数据内部段的访问,这在解析某些变量时,提供了更为简便的方式,此外测试芯片的大小端模式也是联合体的常见应用,当然利用指针强制转换,也能实现该目的,实现如下:

int data = 0x12345678; 
short *pdata = (short *)&data; 
if(*pdata = 0x5678)     
  printf("%s\n", "小端模式"); 
else   
  printf("%s\n", "大端模式");

    可以看出使用联合体在某些情况下可以避免对指针的滥用。结构体则是将具有共通特征的变量组成的集合,比起C++的类来说,它没有安全访问的限制,不支持直接内部带函数,但通过自定义数据类型,函数指针,仍然能够实现很多类似于类的操作,对于大部分嵌入式项目来说,结构化处理数据对于优化整体架构以及后期维护大有便利。

    C语言的结构体支持指针和变量的方式访问,通过转换可以解析任意内存的数据,如我们之前提到的通过指针强制转换解析协议。另外通过将数据和函数指针打包,在通过指针传递,是实现驱动层实接口切换的重要基础,有着重要的实践意义,另外基于位域,联合体,结构体,可以实现另一种位操作,这对于封装底层硬件寄存器具有重要意义。通过联合体和位域操作,可以实现对数据内bit的访问,这在寄存器以及内存受限的平台,提供了简便且直观的处理方式,另外对于结构体的另一个重要知识点就是对齐了,通过对齐访问,可以大幅度提高运行效率,但是因为对齐引入的存储长度问题,也是容易出错的问题,对于对齐的理解,可以分类为如下说明。

  • 基础数据类型:以默认的的长度对齐,如char以1字节对齐,short以2字节对齐等
  • 数组 :按照基本数据类型对齐,第一个对齐了后面的自然也就对齐了。
  • 联合体 :按其包含的长度最大的数据类型对齐。
  • 结构体:结构体中每个数据类型都要对齐,结构体本身以内部最大数据类型长度对齐

  其中union联合体的大小与内部最大的变量int一致,为4字节,根据读取的值,就知道实际内存布局和填充的位置是一致,事实上学会通过填充来理解C语言的对齐机制,是有效且快捷的方式。

预处理机制

    C语言提供了丰富的预处理机制,方便了跨平台的代码的实现,此外C语言通过宏机制实现的数据和代码块替换,字符串格式化,代码段切换,对于工程应用具有重要意义,下面按照功能需求,描述在C语言运用中的常用预处理机制。

    #include 包含文件命令,在C语言中,它执行的效果是将包含文件中的所有内容插入到当前位置,这不只包含头文件,一些参数文件,配置文件,也可以使用该文件插入到当前代码的指定位置。其中<>和""分别表示从标准库路径还是用户自定义路径开始检索。

    #define宏定义,常见的用法包含定义常量或者代码段别名,当然某些情况下配合##格式化字符串,可以实现接口的统一化处理,实例如下:

#define MAX_SIZE  10
#define MODULE_ON  1
#define ERROR_LOOP() do{\
                     printf("error loop\n");\
                   }while(0);
#define global(val) g_##val
int global(v) = 10;
int global(add)(int a, int b)
{
return a+b;
}

    #if..#elif...#else...#endif, #ifdef..#endif, #ifndef...#endif条件选择判断,条件选择主要用于切换代码块,这种综合性项目和跨平台项目中为了满足多种情况下的需求往往会被使用。

    #undef 取消定义的参数,避免重定义问题。

    #error,#warning用于用户自定义的告警信息,配合#if,#ifdef使用,可以限制错误的预定义配置。

    #pragma 带参数的预定义处理,常见的#pragma pack(1), 不过使用后会导致后续的整个文件都以设置的字节对齐,配合push和pop可以解决这种问题,代码如下:

#pragma pack(push)
#pragma pack(1)
struct TestA
{
char i;
int b;
}A;
#pragma pack(pop); //注意要调用pop,否则会导致后续文件都以pack定义值对齐,执行不符合预期
//等同于
struct _TestB{
char i;
int b;
 }__attribute__((packed))A;

总结

    嵌入式C语言在处理硬件物理地址、位操作、内存访问方面都给予开发者了充分的自由,相关文章:STM32开发中的位运算以及位带操作。通过数组,指针以及强制转换的技巧,可以有效减少数据处理中的复制过程,这对于底层是必要的,也方便了整个架构的开发。对于任何嵌入式C语言开发的从业者,清晰的掌握这些基础的知识是必要的。








三、STM32开发中的位运算以及位带操作

 为了像51单片机一样能够对某个管脚单独操作,引入了位带操作这样的操作机制。

区就是就是你想单独操作的IO的区域,比如PA1、PA2。而位带别名区就是你给每一位重新起了个名字的那一片地址区域。可以看下表,M3内核存储器映射表,你能看到1M内存的BitBand区,还有与之对应的32M内存的BitBand别名区,因为你将每一位膨胀成为了一个32位的地址,所以相应的别名区的内存也会是位带区的32倍。

51c~嵌入式C语言~合集1_嵌入式开发_03

    想进行位带操作,应该先去找该位对应的别名区的地址,找到了这个地址,对这个地址进行操作,那么实际上也就是对该位进行操作了。

    官方给出了如下相应的计算公式:

AliasAddr
=0x42000000+((A‐0x40000000)*8+n)*4
=0x42000000+ (A‐0x40000000)*32 + n*4

    其中,AliasAddr是别名区的地址,A是GPIOA->ODR的地址,n是该端口的上的某一位。

    0x42000000是位带别名区域的起始地址,A是输出数据寄存器GPIOA->ODR的地址,A的地址先减去位带区基地址,得到的是相对于位带区基地址的偏移地址,那么膨胀之后还是一个偏移地址,是相对于位带别名区基地址的偏移量,加上位带别名区域基地址,就得到了其对应的别名区地址。


    多数情况下,大家见到的代码,应该是以下这个样子,一共分为三步:

#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2))
#define MEM_ADDR(addr)  *((volatile unsigned long  *(addr))                                                                       
#define BIT_ADDR(addr, bitnum)   MEM_ADDR(BITBAND(addr, bitnum))

    第一步,就是我们上面分析的,得到位带别名区域的32位地址。

就是将第一步得到的32位地址,给转换成一个指针变量,并且操作这个地址里的值,唯一的区别,就是由于安全的考虑,多加了一个volatile 这样的关键字。

举个例子

    如下,想直接访问0x00000001这个地址,并且给这个地址写1,该怎么做呢?

# define ADDR 0x00000001
*(int *)ADDR = 1;

    第三步,就是将前两部,结合在一起,根据传入的addr和bit计算得到32位的地址,然后强制类型转换,使得我们可以去操作这个地址里的值。

    提示:bitnum<<2相当于bitnum2乘以4,实际上在计算机底层乘法也是基于位运算实现的。









四、嵌入式开发中的C语言编译器

如果你和一个优秀的程序员共事,你会发现他对他使用的工具非常熟悉,就像一个画家了解他的画具一样。----比尔.盖茨1 不能简单的认为是个工具

  • 嵌入式程序开发跟硬件密切相关,需要使用C语言来读写底层寄存器、存取数据、控制硬件等,C语言和硬件之间由编译器来联系,一些C标准不支持的硬件特性操作,由编译器提供。
  • 汇编可以很轻易的读写指定RAM地址、可以将代码段放入指定的Flash地址、可以精确的设置变量在RAM中分布等等,所有这些操作,在深入了解编译器后,也可以使用C语言实现。
  • C语言标准并非完美,有着数目繁多的未定义行为,这些未定义行为完全由编译器自主决定,了解你所用的编译器对这些未定义行为的处理,是必要的。
  • 嵌入式编译器对调试做了优化,会提供一些工具,可以分析代码性能,查看外设组件等,了解编译器的这些特性有助于提高在线调试的效率。
  • 此外,堆栈操作、代码优化、数据类型的范围等等,都是要深入了解编译器的理由。
  • 如果之前你认为编译器只是个工具,能够编译就好。那么,是时候改变这种思想了。

2 不能依赖编译器的语义检查

    编译器的语义检查很弱小,甚至还会“掩盖”错误。现代的编译器设计是件浩瀚的工程,为了让编译器设计简单一些,目前几乎所有编译器的语义检查都比较弱小。为了获得更快的执行效率,C语言被设计的足够灵活且几乎不进行任何运行时检查,比如数组越界、指针是否合法、运算结果是否溢出等等。这就造成了很多编译正确但执行奇怪的程序。

    C语言足够灵活,对于一个数组test[30],它允许使用像test[-1]这样的形式来快速获取数组首元素所在地址前面的数据;允许将一个常数强制转换为函数指针,使用代码(((void()())0))()来调用位于0地址的函数。C语言给了程序员足够的自由,但也由程序员承担滥用自由带来的责任。

2.1莫名的死机

    下面的两个例子都是死循环,如果在不常用分支中出现类似代码,将会造成看似莫名其妙的死机或者重启。

unsigned char i;    //例程1 
 for(i=0;i<256;i++)
 {
     //其它代码  
 }
unsigned char i;     //例程2 
 for(i=10;i>=0;i--)
 {
     //其它代码  
 }

    对于无符号char类型,表示的范围为0~255,所以无符号char类型变量i永远小于256(第一个for循环无限执行),永远大于等于0(第二个for循环无限执行)。需要说明的是,赋值代码i=256是被C语言允许的,即使这个初值已经超出了变量i可以表示的范围。C语言会千方百计的为程序员创造出错的机会,可见一斑。

2.2不起眼的改变

    假如你在if语句后误加了一个分号,可能会完全改变了程序逻辑。编译器也会很配合的帮忙掩盖,甚至连警告都不提示。代码如下:

if(a>b);           //这里误加了一个分号  
 a=b;              //这句代码一直被执行

    不但如此,编译器还会忽略掉多余的空格符和换行符,就像下面的代码也不会给出足够提示:

51c~嵌入式C语言~合集1_c语言_04

    这段代码的本意是n<3时程序直接返回,由于程序员的失误,return少了一个结束分号。编译器将它翻译成返回表达式logrec.data=x[0]的结果,return后面即使是一个表达式也是C语言允许的。这样当n>=3时,表达式logrec.data=x[0];就不会被执行,给程序埋下了隐患。

2.3 难查的数组越界

    上文曾提到数组常常是引起程序不稳定的重要因素,程序员往往不经意间就会写数组越界。

    一位同事的代码在硬件上运行,一段时间后就会发现LCD显示屏上的一个数字不正常的被改变。经过一段时间的调试,问题被定位到下面的一段代码中:

int SensorData[30];
 //其他代码 
 for(i=30;i>0;i--)
 {
      SensorData[i]=…;
      //其他代码   
 }

    这里声明了拥有30个元素的数组,不幸的是for循环代码中误用了本不存在的数组元素SensorData[30],但C语言却默许这么使用,并欣然的按照代码改变了数组元素SensorData[30]所在位置的值, SensorData[30]所在的位置原本是一个LCD显示变量,这正是显示屏上的那个值不正常被改变的原因。真庆幸这么轻而易举的发现了这个Bug。

    其实很多编译器会对上述代码产生一个警告:赋值超出数组界限。但并非所有程序员都对编译器警告保持足够敏感,况且,编译器也并不能检查出数组越界的所有情况。比如下面的例子:

    你在模块A中定义数组:

int SensorData[30];

    在模块B中引用该数组,但由于你引用代码并不规范,这里没有显示声明数组大小,但编译器也允许这么做:

extern int SensorData[];

    这次,编译器不会给出警告信息,因为编译器压根就不知道数组的元素个数。所以,当一个数组声明为具有外部链接,它的大小应该显式声明。

    再举一个编译器检查不出数组越界的例子。函数func()的形参是一个数组形式,函数代码简化如下所示:

51c~嵌入式C语言~合集1_c语言_05

    这个给SensorData[30]赋初值的语句,编译器也是不给任何警告的。实际上,编译器是将数组名Sensor隐含的转化为指向数组第一个元素的指针,函数体是使用指针的形式来访问数组的,它当然也不会知道数组元素的个数了。造成这种局面的原因之一是C编译器的作者们认为指针代替数组可以提高程序效率,而且,可以简化编译器的复杂度。

    指针和数组是容易给程序造成混乱的,我们有必要仔细的区分它们的不同。其实换一个角度想想,它们也是容易区分的:可以将数组名等同于指针的情况有且只有一处,就是上面例子提到的数组作为函数形参时。其它时候,数组名是数组名,指针是指针。

    下面的例子编译器同样检查不出数组越界。

    我们常常用数组来缓存通讯中的一帧数据。在通讯中断中将接收的数据保存到数组中,直到一帧数据完全接收后再进行处理。即使定义的数组长度足够长,接收数据的过程中也可能发生数组越界,特别是干扰严重时。

    这是由于外界的干扰破坏了数据帧的某些位,对一帧的数据长度判断错误,接收的数据超出数组范围,多余的数据改写与数组相邻的变量,造成系统崩溃。由于中断事件的异步性,这类数组越界编译器无法检查到。

    如果局部数组越界,可能引发ARM架构硬件异常。

    同事的一个设备用于接收无线传感器的数据,一次软件升级后,发现接收设备工作一段时间后会死机。调试表明ARM7处理器发生了硬件异常,异常处理代码是一段死循环(死机的直接原因)。接收设备有一个硬件模块用于接收无线传感器的整包数据并存在自己的缓冲区中,当硬件模块接收数据完成后,使用外部中断通知设备取数据,外部中断服务程序精简后如下所示: 

__irq ExintHandler(void)  
 {
      unsignedchar DataBuf[50];
      GetData(DataBug);        //从硬件缓冲区取一帧数据  
      //其他代码 
 }

    由于存在多个无线传感器近乎同时发送数据的可能加之GetData()函数保护力度不够,数组DataBuf在取数据过程中发生越界。由于数组DataBuf为局部变量,被分配在堆栈中,同在此堆栈中的还有中断发生时的运行环境以及中断返回地址。溢出的数据将这些数据破坏掉,中断返回时PC指针可能变成一个不合法值,硬件异常由此产生。

    如果我们精心设计溢出部分的数据,化数据为指令,就可以利用数组越界来修改PC指针的值,使之指向我们希望执行的代码。

    1988年,第一个网络蠕虫在一天之内感染了2000到6000台计算机,这个蠕虫程序利用的正是一个标准输入库函数的数组越界Bug。起因是一个标准输入输出库函数gets(),原来设计为从数据流中获取一段文本,遗憾的是,gets()函数没有规定输入文本的长度。

    gets()函数内部定义了一个500字节的数组,攻击者发送了大于500字节的数据,利用溢出的数据修改了堆栈中的PC指针,从而获取了系统权限。目前,虽然有更好的库函数来代替gets函数,但gets函数仍然存在着。

2.4神奇的volatile

    做嵌入式设备开发,如果不对volatile修饰符具有足够了解,实在是说不过去。volatile是C语言32个关键字中的一个,属于类型限定符,常用的const关键字也属于类型限定符。

    volatile限定符用来告诉编译器,该对象的值无任何持久性,不要对它进行任何优化;它迫使编译器每次需要该对象数据内容时都必须读该对象,而不是只读一次数据并将它放在寄存器中以便后续访问之用(这样的优化可以提高系统速度)。

    这个特性在嵌入式应用中很有用,比如你的IO口的数据不知道什么时候就会改变,这就要求编译器每次都必须真正的读取该IO端口。这里使用了词语“真正的读”,是因为由于编译器的优化,你的逻辑反应到代码上是对的,但是代码经过编译器翻译后,有可能与你的逻辑不符。

    你的代码逻辑可能是每次都会读取IO端口数据,但实际上编译器将代码翻译成汇编时,可能只是读一次IO端口数据并保存到寄存器中,接下来的多次读IO口都是使用寄存器中的值来进行处理。因为读写寄存器是最快的,这样可以优化程序效率。与之类似的,中断里的变量、多线程中的共享变量等都存在这样的问题。

    不使用volatile,可能造成运行逻辑错误,但是不必要的使用volatile会造成代码效率低下(编译器不优化volatile限定的变量),因此清楚的知道何处该使用volatile限定符,是一个嵌入式程序员的必修内容。

    一个程序模块通常由两个文件组成,源文件和头文件。如果你在源文件定义变量:

unsigned int test;

    并在头文件中声明该变量:

extern unsigned long test;

    编译器会提示一个语法错误:变量’ test’声明类型不一致。但如果你在源文件定义变量:

volatile unsigned int test;

    在头文件中这样声明变量:

extern unsigned int test;     /*缺少volatile限定符*/

    编译器却不会给出错误信息(有些编译器仅给出一条警告)。当你在另外一个模块(该模块包含声明变量test的头文件)使用变量test时,它已经不再具有volatile限定,这样很可能造成一些重大错误。比如下面的例子,注意该例子是为了说明volatile限定符而专门构造出的,因为现实中的volatile使用Bug大都隐含,并且难以理解。

    在模块A的源文件中,定义变量:

volatile unsigned int TimerCount=0;

    该变量用来在一个定时器中断服务程序中进行软件计时:

TimerCount++;

    在模块A的头文件中,声明变量:

extern unsigned int TimerCount;   //这里漏掉了类型限定符volatile

    在模块B中,要使用TimerCount变量进行精确的软件延时:

#include “…A.h”                     //首先包含模块A的头文件  
 //其他代码  
 TimerCount=0;
 while(TimerCount<=TIMER_VALUE);   //延时一段时间(感谢网友chhfish指这里的逻辑错误)  
 //其他代码

    实际上,这是一个死循环。由于模块A头文件中声明变量TimerCount时漏掉了volatile限定符,在模块B中,变量TimerCount是被当作unsigned int类型变量。由于寄存器速度远快于RAM,编译器在使用非volatile限定变量时是先将变量从RAM中拷贝到寄存器中,如果同一个代码块再次用到该变量,就不再从RAM中拷贝数据而是直接使用之前寄存器备份值。

    代码while(TimerCount<=TIMER_VALUE)中,变量TimerCount仅第一次执行时被使用,之后都是使用的寄存器备份值,而这个寄存器值一直为0,所以程序无限循环。下面的流程图说明了程序使用限定符volatile和不使用volatile的执行过程。

51c~嵌入式C语言~合集1_c语言_06

    为了更容易的理解编译器如何处理volatile限定符,这里给出未使用volatile限定符和使用volatile限定符程序的反汇编代码:

  • 没有使用关键字volatile,在keil MDK V4.54下编译,默认优化级别,如下所示(注意最后两行):
122:     unIdleCount=0;
    123:
 0x00002E10  E59F11D4  LDR       R1,[PC,#0x01D4]
 0x00002E14  E3A05000  MOV       R5,#key1(0x00000000)
 0x00002E18  E1A00005  MOV       R0,R5
 0x00002E1C  E5815000  STR       R5,[R1]
    124:     while(unIdleCount!=200);   //延时2S钟   
    125:
      0x00002E20  E35000C8  CMP       R0,#0x000000C8  
 0x00002E24  1AFFFFFD  BNE       0x00002E20</span>
  • 使用关键字volatile,在keil MDK V4.54下编译,默认优化级别,如下所示(注意最后三行):
122:     unIdleCount=0;
    123:
 0x00002E10  E59F01D4  LDR       R0,[PC,#0x01D4]
 0x00002E14  E3A05000  MOV       R5,#key1(0x00000000)
 0x00002E18  E5805000  STR       R5,[R0]
    124:     while(unIdleCount!=200);   //延时2S钟   
    125:
 0x00002E1C  E5901000  LDR       R1,[R0]
 0x00002E20  E35100C8  CMP       R1,#0x000000C8  
 0x00002E24  1AFFFFFC  BNE       0x00002E1C

    可以看到,如果没有使用volatile关键字,程序一直比较R0内数据与0xC8是否相等,但R0中的数据是0,所以程序会一直在这里循环比较(死循环);再看使用了volatile关键字的反汇编代码,程序会先从变量中读出数据放到R1寄存器中,然后再让R1内数据与0xC8相比较,这才是我们C代码的正确逻辑!

2.5局部变量

    ARM架构下的编译器会频繁的使用堆栈,堆栈用于存储函数的返回值、AAPCS规定的必须保护的寄存器以及局部变量,包括局部数组、结构体、联合体和C++的类。默认情况下,堆栈的位置、初始值都是由编译器设置,因此需要对编译器的堆栈有一定了解。

    从堆栈中分配的局部变量的初值是不确定的,因此需要运行时显式初始化该变量。一旦离开局部变量的作用域,这个变量立即被释放,其它代码也就可以使用它,因此堆栈中的一个内存位置可能对应整个程序的多个变量。

    局部变量必须显式初始化,除非你确定知道你要做什么。下面的代码得到的温度值跟预期会有很大差别,因为在使用局部变量sum时,并不能保证它的初值为0。编译器会在第一次运行时清零堆栈区域,这加重了此类Bug的隐蔽性。

51c~嵌入式C语言~合集1_c语言_07

    由于一旦程序离开局部变量的作用域即被释放,所以下面代码返回指向局部变量的指针是没有实际意义的,该指针指向的区域可能会被其它程序使用,其值会被改变。

char * GetData(void)  
 {
      char buffer[100];      //局部数组  
      …
      return buffer;
 }

2.6使用外部工具

    由于编译器的语义检查比较弱,我们可以使用第三方代码分析工具,使用这些工具来发现潜在的问题,这里介绍其中比较著名的是PC-Lint。

    PC-Lint由Gimpel Software公司开发,可以检查C代码的语法和语义并给出潜在的BUG报告。PC-Lint可以显著降低调试时间。

    目前公司ARM7和Cortex-M3内核多是使用Keil MDK编译器来开发程序,通过简单配置,PC-Lint可以被集成到MDK上,以便更方便的检查代码。MDK已经提供了PC-Lint的配置模板,所以整个配置过程十分简单,Keil MDK开发套件并不包含PC-Lint程序,在此之前,需要预先安装可用的PC-Lint程序,配置过程如下:

  1. 点击菜单Tools---Set-up PC-Lint…

51c~嵌入式C语言~合集1_c语言_08

    PC-Lint Include Folders:该列表路径下的文件才会被PC-Lint检查,此外,这些路径下的文件内使用#include包含的文件也会被检查;

    Lint Executable:指定PC-Lint程序的路径

    Configuration File:指定配置文件的路径,该配置文件由MDK编译器提供。

  1. 菜单Tools---Lint 文件路径.c/.h

    检查当前文件。

  1. 菜单Tools---Lint All C-Source Files

    检查所有C源文件。

    PC-Lint的输出信息显示在MDK编译器的Build Output窗口中,双击其中的一条信息可以跳转到源文件所在位置。

    编译器语义检查的弱小在很大程度上助长了不可靠代码的广泛存在。随着时代的进步,现在越来越多的编译器开发商意识到了语义检查的重要性,编译器的语义检查也越来越强大,比如公司使用的Keil MDK编译器,虽然它的编辑器依然不尽人意,但在其V4.47及以上版本中增加了动态语法检查并加强了语义检查,可以友好的提示更多警告信息。建议经常关注编译器官方网站并将编译器升级到V4.47或以上版本,升级的另一个好处是这些版本的编辑器增加了标识符自动补全功能,可以大大节省编码的时间。

3 你觉得有意义的代码未必正确

    C语言标准特别的规定某些行为是未定义的,编写未定义行为的代码,其输出结果由编译器决定!C标准委员会定义未定义行为的原因如下:

  • 简化标准,并给予实现一定的灵活性,比如不捕捉那些难以诊断的程序错误;
  • 编译器开发商可以通过未定义行为对语言进行扩展
    C语言的未定义行为,使得C极度高效灵活并且给编译器实现带来了方便,但这并不利于优质嵌入式C程序的编写。因为许多 C 语言中看起来有意义的东西都是未定义的,并且这也容易使你的代码埋下隐患,并且不利于跨编译器移植。Java程序会极力避免未定义行为,并用一系列手段进行运行时检查,使用Java可以相对容易的写出安全代码,但体积庞大效率低下。作为嵌入式程序员,我们需要了解这些未定义行为,利用C语言的灵活性,写出比Java更安全、效率更高的代码来。

3.1常见的未定义行为

  1. 自增自减在表达式中连续出现并作用于同一变量或者自增自减在表达式中出现一次,但作用的变量多次出现

    自增(++)和自减(--)这一动作发生在表达式的哪个时刻是由编译器决定的,比如:

r = 1 * a[i++] + 2 * a[i++] + 3 * a[i++];

    不同的编译器可能有着不同的汇编代码,可能是先执行i++再进行乘法和加法运行,也可能是先进行加法和乘法运算,再执行i++,因为这句代码在一个表达式中出现了连续的自增并作用于同一变量。更加隐蔽的是自增自减在表达式中出现一次,但作用的变量多次出现,比如:

a[i] = i++; /* 未定义行为 */

    先执行i++再赋值,还是先赋值再执行i++是由编译器决定的,而两种不同的执行顺序的结果差别是巨大的。

  1. 函数实参被求值的顺序

    函数如果有多个实参,这些实参的求值顺序是由编译器决定的,比如:

printf("%d %d\n", ++n, power(2, n));    /* 未定义行为 */

    是先执行++n还是先执行power(2,n)是由编译器决定的。

  1. 有符号整数溢出

    有符号整数溢出是未定义的行为,编译器决定有符号整数溢出按照哪种方式取值。比如下面代码:

int value1,value2,sum
 //其它操作  
 sum=value1+value;    /*sum可能发生溢出*/
  1. 有符号数右移、移位的数量是负值或者大于操作数的位数
  2. 除数为零
  3. malloc()、calloc()或realloc()分配零字节内存

3.2如何避免C语言未定义行为

    代码中引入未定义行为会为代码埋下隐患,防止代码中出现未定义行为是困难的,我们总能不经意间就会在代码中引入未定义行为。但是还是有一些方法可以降低这种事件,总结如下:

  • 了解C语言未定义行为

    标准C99附录J.2“未定义行为”列举了C99中的显式未定义行为,通过查看该文档,了解那些行为是未定义的,并在编码中时刻保持警惕;

  • 寻求工具帮助

    编译器警告信息以及PC-Lint等静态检查工具能够发现很多未定义行为并警告,要时刻关注这些工具反馈的信息;

  • 总结并使用一些编码标准

    1)避免构造复杂的自增或者自减表达式,实际上,应该避免构造所有复杂表达式;

比如a[i] = i++;语句可以改为a[i] = i; i++;这两句代码。

    2)只对无符号操作数使用位操作;

  • 必要的运行时检查

    检查是否溢出、除数是否为零,申请的内存数量是否为零等等,比如上面的有符号整数溢出例子,可以按照如下方式编写,以消除未定义特性:

int value1,value2,sum;
 //其它代码  
 if((value1>0 && value2>0 && value1>(INT_MAX-value2))||
    (value1<0 && value2<0 && value1<(INT_MIN-value2)))
 {
     //处理错误  
 }
 else  
 {
     sum=value1+value2;
 }

    上面的代码是通用的,不依赖于任何CPU架构,但是代码效率很低。如果是有符号数使用补码的CPU架构(目前常见CPU绝大多数都是使用补码),还可以用下面的代码来做溢出检查:

int value1, value2, sum;
unsigned int usum = (unsigned int)value1 + value2;


if((usum ^ value1) & (usum ^ value2) & INT_MIN)
{
 /*处理溢出情况*/
}
else
{
 sum = value1 + value2;
}

    使用的原理解释一下,因为在加法运算中,操作数value1和value2只有符号相同时,才可能发生溢出,所以我们先将这两个数转换为无符号类型,两个数的和保存在变量usum中。如果发生溢出,则value1、value2和usum的最高位(符号位)一定不同,表达式(usum ^ value1) & (usum ^ value2) 的最高位一定为1,这个表达式位与(&)上INT_MIN是为了将最高位之外的其它位设置为0。

  • 了解你所用的编译器对未定义行为的处理策略

    很多引入了未定义行为的程序也能运行良好,这要归功于编译器处理未定义行为的策略。不是你的代码写的正确,而是恰好编译器处理策略跟你需要的逻辑相同。了解编译器的未定义行为处理策略,可以让你更清楚的认识到那些引入了未定义行为程序能够运行良好是多么幸运的事,不然多换几个编译器试试!

    以Keil MDK为例,列举常用的处理策略如下:

1) 有符号量的右移是算术移位,即移位时要保证符号位不改变。

2)对于int类的值:超过31位的左移结果为零;无符号值或正的有符号值超过31位的右移结果为零。负的有符号值移位结果为-1。

3)整型数除以零返回零

4 了解你的编译器

    在嵌入式开发过程中,我们需要经常和编译器打交道,只有深入了解编译器,才能用好它,编写更高效代码,更灵活的操作硬件,实现一些高级功能。下面以公司最常用的Keil MDK为例,来描述一下编译器的细节。4.1编译器的一些小知识

  1. 默认情况下,char类型的数据项是无符号的,所以它的取值范围是0~255;
  2. 在所有的内部和外部标识符中,大写和小写字符不同;
  3. 通常局部变量保存在寄存器中,但当局部变量太多放到栈里的时候,它们总是字对齐的。
  4. 压缩类型的自然对齐方式为1。使用关键字__packed来压缩特定结构,将所有有效类型的对齐边界设置为1;
  5. 整数以二进制补码形式表示;浮点量按IEEE格式存储;
  6. 整数除法的余数的符号于被除数相同,由ISO C90标准得出;
  7. 如果整型值被截断为短的有符号整型,则通过放弃适当数目的最高有效位来得到结果。如果原始数是太大的正或负数,对于新的类型,无法保证结果的符号将于原始数相同。
  8. 整型数超界不引发异常;像unsigned char test; test=1000;这类是不会报错的;
  9. 在严格C中,枚举值必须被表示为整型。例如,必须在‑2147483648 到+2147483647的范围内。但MDK自动使用对象包含enum范围的最小整型来实现(比如char类型),除非使用编译器命令‑‑enum_is_int 来强制将enum的基础类型设为至少和整型一样宽。超出范围的枚举值默认仅产生警告:#66:enumeration value is out of "int" range;
  10. 对于结构体填充,根据定义结构的方式,keil MDK编译器用以下方式的一种来填充结构:

I> 定义为static或者extern的结构用零填充;

II> 栈或堆上的结构,例如,用malloc()或者auto定义的结构,使用先前存储在那些存储器位置的任何内容进行填充。不能使用memcmp()来比较以这种方式定义的填充结构!

  1. 编译器不对声明为volatile类型的数据进行优化;
  2. __nop():延时一个指令周期,编译器绝不会优化它。如果硬件支持NOP指令,则该句被替换为NOP指令,如果硬件不支持NOP指令,编译器将它替换为一个等效于NOP的指令,具体指令由编译器自己决定;
  3. __align(n):指示编译器在n 字节边界上对齐变量。对于局部变量,n的值为1、2、4、8;
  4. attribute((at(address))):可以使用此变量属性指定变量的绝对地址;
  5. __inline:提示编译器在合理的情况下内联编译C或C++ 函数;

4.2初始化的全局变量和静态变量的初始值被放到了哪里?

    我们程序中的一些全局变量和静态变量在定义时进行了初始化,经过编译器编译后,这些初始值被存放在了代码的哪里?我们举个例子说明:

unsigned int g_unRunFlag=0xA5;
 static unsigned int s_unCountFlag=0x5A;

    我曾做过一个项目,项目中的一个设备需要在线编程,也就是通过协议,将上位机发给设备的数据通过在应用编程(IAP)技术写入到设备的内部Flash中。我将内部Flash做了划分,一小部分运行程序,大部分用来存储上位机发来的数据。随着程序量的增加,在一次更新程序后发现,在线编程之后,设备运行正常,但是重启设备后,运行出现了故障!经过一系列排查,发现故障的原因是一个全局变量的初值被改变了。

    这是件很不可思议的事情,你在定义这个变量的时候指定了初始值,当你在第一次使用这个变量时却发现这个初值已经被改掉了!这中间没有对这个变量做任何赋值操作,其它变量也没有任何溢出,并且多次在线调试表明,进入main函数的时候,该变量的初值已经被改为一个恒定值。

    要想知道为什么全局变量的初值被改变,就要了解这些初值编译后被放到了二进制文件的哪里。在此之前,需要先了解一点链接原理。

    ARM映象文件各组成部分在存储系统中的地址有两种:一种是映象文件位于存储器时(通俗的说就是存储在Flash中的二进制代码)的地址,称为加载地址;一种是映象文件运行时(通俗的说就是给板子上电,开始运行Flash中的程序了)的地址,称为运行时地址。

    赋初值的全局变量和静态变量在程序还没运行的时候,初值是被放在Flash中的,这个时候他们的地址称为加载地址,当程序运行后,这些初值会从Flash中拷贝到RAM中,这时候就是运行时地址了。

    原来,对于在程序中赋初值的全局变量和静态变量,程序编译后,MDK将这些初值放到Flash中,位于紧靠在可执行代码的后面。在程序进入main函数前,会运行一段库代码,将这部分数据拷贝至相应RAM位置。

    由于我的设备程序量不断增加,超过了为设备程序预留的Flash空间,在线编程时,将一部分存储全局变量和静态变量初值的Flash给重新编程了。在重启设备前,初值已经被拷贝到RAM中,所以这个时候程序运行是正常的,但重新上电后,这部分初值实际上是在线编程的数据,自然与初值不同了。

4.3在C代码中使用的变量,编译器将他们分配到RAM的哪里?

    我们会在代码中使用各种变量,比如全局变量、静态变量、局部变量,并且这些变量时由编译器统一管理的,有时候我们需要知道变量用掉了多少RAM,以及这些变量在RAM中的具体位置。

    这是一个经常会遇到的事情,举一个例子,程序中的一个变量在运行时总是不正常的被改变,那么有理由怀疑它临近的变量或数组溢出了,溢出的数据更改了这个变量值。要排查掉这个可能性,就必须知道该变量被分配到RAM的哪里、这个位置附近是什么变量,以便针对性的做跟踪。

    其实MDK编译器的输出文件中有一个“工程名.map”文件,里面记录了代码、变量、堆栈的存储位置,通过这个文件,可以查看使用的变量被分配到RAM的哪个位置。要生成这个文件,需要在Options for Targer窗口,Listing标签栏下,勾选Linker Listing前的复选框,如下图所示。

51c~嵌入式C语言~合集1_嵌入式开发_09

4.4默认情况下,栈被分配到RAM的哪个地方?

    MDK中,我们只需要在配置文件中定义堆栈大小,编译器会自动在RAM的空闲区域选择一块合适的地方来分配给我们定义的堆栈,这个地方位于RAM的那个地方呢?

    通过查看MAP文件,原来MDK将堆栈放到程序使用到的RAM空间的后面,比如你的RAM空间从0x4000 0000开始,你的程序用掉了0x200字节RAM,那么堆栈空间就从0x4000 0200处开始。

    使用了多少堆栈,是否溢出?

4.5 有多少RAM会被初始化?

    在进入main()函数之前,MDK会把未初始化的RAM给清零的,我们的RAM可能很大,只使用了其中一小部分,MDK会不会把所有RAM都初始化呢?

    答案是否定的,MDK只是把你的程序用到的RAM以及堆栈RAM给初始化,其它RAM的内容是不管的。如果你要使用绝对地址访问MDK未初始化的RAM,那就要小心翼翼的了,因为这些RAM上电时的内容很可能是随机的,每次上电都不同。

4.6 MDK编译器如何设置非零初始化变量?

    对于控制类产品,当系统复位后(非上电复位),可能要求保持住复位前RAM中的数据,用来快速恢复现场,或者不至于因瞬间复位而重启现场设备。而keil mdk在默认情况下,任何形式的复位都会将RAM区的非初始化变量数据清零。

    MDK编译程序生成的可执行文件中,每个输出段都最多有三个属性:RO属性、RW属性和ZI属性。对于一个全局变量或静态变量,用const修饰符修饰的变量最可能放在RO属性区,初始化的变量会放在RW属性区,那么剩下的变量就要放到ZI属性区了。

    默认情况下,ZI属性区的数据在每次复位后,程序执行main函数内的代码之前,由编译器“自作主张”的初始化为零。所以我们要在C代码中设置一些变量在复位后不被零初始化,那一定不能任由编译器“胡作非为”,我们要用一些规则,约束一下编译器。

    分散加载文件对于连接器来说至关重要,在分散加载文件中,使用UNINIT来修饰一个执行节,可以避免编译器对该区节的ZI数据进行零初始化。这是要解决非零初始化变量的关键。

    因此我们可以定义一个UNINIT修饰的数据节,然后将希望非零初始化的变量放入这个区域中。于是,就有了第一种方法:

  1. 修改分散加载文件,增加一个名为MYRAM的执行节,该执行节起始地址为0x1000A000,长度为0x2000字节(8KB),由UNINIT修饰:
LR_IROM1 0x00000000 0x00080000  {    ; load region size_region
   ER_IROM1 0x00000000 0x00080000  {  ; load address = execution address
    *.o (RESET, +First)
    *(InRoot$$Sections)
    .ANY (+RO)
   }
   RW_IRAM1 0x10000000 0x0000A000  {  ; RW data
    .ANY (+RW +ZI)
   }
   MYRAM 0x1000A000 UNINIT 0x00002000  {
    .ANY (NO_INIT)
   }
 }

    那么,如果在程序中有一个数组,你不想让它复位后零初始化,就可以这样来定义变量:

unsigned char  plc_eu_backup[32] __attribute__((at(0x1000A000)));

    变量属性修饰符__attribute__((at(adde)))用来将变量强制定位到adde所在地址处。由于地址0x1000A000开始的8KB区域ZI变量不会被零初始化,所以位于这一区域的数组plc_eu_backup也就不会被零初始化了。

    这种方法的缺点是显而易见的:要程序员手动分配变量的地址。如果非零初始化数据比较多,这将是件难以想象的大工程(以后的维护、增加、修改代码等等)。所以要找到一种办法,让编译器去自动分配这一区域的变量。

  1. 分散加载文件同方法1,如果还是定义一个数组,可以用下面方法:
unsigned char  plc_eu_backup[32] __attribute__((section("NO_INIT"),zero_init));

    变量属性修饰符__attribute__((section(“name”),zero_init))用于将变量强制定义到name属性数据节中,zero_init表示将未初始化的变量放到ZI数据节中。因为“NO_INIT”这显性命名的自定义节,具有UNINIT属性。

  1. 将一个模块内的非初始化变量都非零初始化

    假如该模块名字为test.c,修改分散加载文件如下所示:

LR_IROM1 0x00000000 0x00080000  {    ; load region size_region
   ER_IROM1 0x00000000 0x00080000  {  ; load address = execution address
    *.o (RESET, +First)
    *(InRoot$$Sections)
   }
   RW_IRAM1 0x10000000 0x0000A000  {  ; RW data
    .ANY (+RW +ZI)
   }
   RW_IRAM2 0x1000A000 UNINIT 0x00002000  {
    test.o (+ZI)
   }
 }

    在该模块定义时变量时使用如下方法:

    这里,变量属性修饰符__attribute__((zero_init))用于将未初始化的变量放到ZI数据节中变量,其实MDK默认情况下,未初始化的变量就是放在ZI数据区的。








五、嵌入式可靠性设计的编程要点

设备的可靠性涉及多个方面:稳定的硬件、优秀的软件架构、严格的测试以及市场和时间的检验等等。这里着重谈一下作者自己对嵌入式软件可靠性设计的一些理解,通过一定的技巧和方法提高软件可靠性。

1 判错

工欲善其事必先利其器。判错的最终目的是用来暴露设计中的Bug并加以改正,所以将错误信息提供给编程者是必要的。有时候需要将故障信息储存于非易失性存储器中,便于查看。这里以使用串口打印错误信息到PC显示屏为例,来说明一般需要显示什么信息。

编写或移植一个类似C标准库中的printf函数,可以格式化打印字符、字符串、十进制整数、十六进制整数。这里称为UARTprintf()。

unsigned int WriteData(unsigned int addr)
{
    if((addr>= BASE_ADDR)&&(addr<=END_ADDR)) 
    {
        …/*地址合法,进行处理*/
    } 
    else 
    { /*地址错误,打印错误信息*/
        UARTprintf ("文件%s的第 %d 行写数据时发生地址错误,错误地址为:0x%x\n",__FILE__,__LINE__,addr);
        …/*错误处理代码*/
    }

假设UARTprintf()函数位于main.c模块的第256行,并且WriteData()函数在读数据时传递了错误地址0x00000011,则会执行UARTprintf()函数,打印如下所示的信息:

文件main.c的第256行写数据时发生地址错误,错误地址为:0x00000011。 类似这样的信息会有助于程序员定位分析错误产生的根源,更快的消除Bug。

2 判断实参是否合法

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

int exam_fun( unsigned char *str ) 
{ 
    if( str != NULL )
    { //  检查“假设指针不为空”这个条件 


        ... //正常处理代码
    } 
    else 
    {
        UARTprintf(…); // 打印错误信息
        …//处理错误代码
    }
}

3 仔细检查函数的返回值

对函数返回的错误码,要进行全面仔细处理,必要时做错误记录。

char *DoSomething(…)
{
    char * p;
    p=malloc(1024);
    if(p==NULL) 
    { /*对函数返回值作出判断*/
        UARTprintf(…); /*打印错误信息*/
        return NULL;
    }
    retuen p;
}

4 防止指针越界

如果动态计算一个地址时,要保证被计算的地址是合理的并指向某个有意义的地方。特别对于指向一个结构或数组的内部的指针,当指针增加或者改变后仍然指向同一个结构或数组。

5 防止数组越界

数组越界的问题前文已经讲述的很多了,由于C不会对数组进行有效的检测,因此必须在应用中显式的检测数组越界问题。下面的例子可用于中断接收通讯数据。

#define REC_BUF_LEN 100
unsigned char RecBuf[REC_BUF_LEN];
… //其它代码
void Uart_IRQHandler(void)
{
    static RecCount=0;   //接收数据长度计数器
    …       //其它代码
    if(RecCount < REC_BUF_LEN)
    {
        RecBuf[RecCount]=…;  //从硬件取数据
        RecCount++;
        …      //其它代码
    } 
    else 
    {
        UARTprintf(…);   //打印错误信息
        …      //其它错误处理代码
    }
    …
}

在使用一些库函数时,同样需要对边界进行检查:

#define REC_BUF_LEN 100
unsigned char RecBuf[REC_BUF_LEN];
 
if(len< REC_BUF_LEN)
{
    memset(RecBuf,0,len);  //将数组RecBuf清零
} 
else 
{
    //处理错误
}

6 数学算数运算

  • • 检测除数是否为零
  • • 检测运算溢出情况

有符号整数除法,仅检测除数为零就够了吗?

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

#include <limits.h>
signed long sl1,sl2,result;
/*初始化sl1和sl2*/
if((sl2==0)||((sl1==LONG_MIN) && (sl2==-1)))
{
    //处理错误
} 
else 
{
    result = sl1 / sl2;
}

加法溢出检测:

a)无符号加法

#include <limits.h>
unsigned int a,b,result;
/*初始化a,b*/
if(UINT_MAX-a<b)
{
    //处理溢出
} 
else 
{
    result=a+b;
}

b)有符号加法

#include <limits.h>
signed int a,b,result;
/*初始化a,b */
if((a>0 && INT_MAX-a<b)||(a<0) && (INT_MIN-a>b))
{
    //处理溢出
} 
else 
{
    result=a+b;
}

乘法溢出检测:

a)无符号乘法

#include <limits.h>
unsigned int a,b,result;
/*初始化a,b*/
if((a!=0) && (UINT_MAX/a<b)) 
{
    //
} 
else 
{
    result=a*b;
}

b)有符号乘法

#include <limits.h>
signed int a,b,tmp,result;
/*初始化a,b*/
tmp=a * b;
if(a!=0 && tmp/a!=b)
{
//
} 
else 
{
    result=tmp;
}

7 其它可能出现运行时错误的地方

运行时错误检查是C 程序员需要加以特别的注意的,这是因为C语言在提供任何运行时检测方面能力较弱。对于要求可靠性较高的软件来说,动态检测是必需的。因此C 程序员需要谨慎考虑的问题是,在任何可能出现运行时错误的地方增加代码的动态检测。大多数的动态检测与应用紧密相关,在程序设计过程中要根据系统需求设置动态代码检测。

8 编译器语义检查

为了更简单的设计编译器,目前几乎所有编译器的语义检查都比较弱小,加之为了获得更快的执行效率,C语言被设计的足够灵活且几乎不进行任何运行时检查,比如数组越界、指针是否合法、运算结果是否溢出等等。

C语言足够灵活,对于一个数组a[30],它允许使用像a[-1]这样的形式来快速获取数组首元素所在地址前面的数据;允许将一个常数强制转换为函数指针,使用代码( * ((void( * )())0))()来调用位于0地址的函数。C语言给了程序员足够的自由,但也由程序员承担滥用自由带来的责任。下面的两个例子都是死循环,如果在不常用分支中出现类似代码,将会造成看似莫名其妙的死机或者重启。

a. unsigned char i;                  
   for(i=0;i<256;i++)  {… }              
b. unsigned chari;
   for(i=10;i>=0;i--) { … }

对于无符号char类型,表示的范围为0~255,所以无符号char类型变量i永远小于256(第一个for循环无限执行),永远大于等于0(第二个for循环无线执行)。需要说明的是,赋值代码i=256是被C语言允许的,即使这个初值已经超出了变量i可以表示的范围。C语言会千方百计的为程序员创造出错的机会,可见一斑。 假如你在if语句后误加了一个分号改变了程序逻辑,编译器也会很配合的帮忙掩盖,甚至连警告都不提示。代码如下:

if(a>b);          //这里误加了一个分号
a=b;               //这句代码一直被执行

不但如此,编译器还会忽略掉多余的空格符和换行符,就像下面的代码也不会给出足够提示:

if(n<3)
return    //这里少加了一个分号
logrec.data=x[0];
logrec.time=x[1];
logrec.code=x[2];

这段代码的本意是n<3时程序直接返回,由于程序员的失误,return少了一个结束分号。编译器将它翻译成返回表达式logrec.data=x[0]的结果,return后面即使是一个表达式也是C语言允许的。这样当n>=3时,表达式logrec.data=x[0];就不会被执行,给程序埋下了隐患。 可以毫不客气的说,弱小的编译器语义检查在很大程度上纵容了不可靠代码可以肆无忌惮的存在。

上文曾提到数组常常是引起程序不稳定的重要因素,程序员往往不经意间就会写数组越界。一位同事的代码在硬件上运行,一段时间后就会发现LCD显示屏上的一个数字不正常的被改变。经过一段时间的调试,问题被定位到下面的一段代码中:

int SensorData[30];
for(i=30;i>0;i--)
{
    SensorData[i]=…;
    …
}

这里声明了拥有30个元素的数组,不幸的是for循环代码中误用了本不存在的数组元素SensorData[30],但C语言却默许这么使用,并欣然的按照代码改变了数组元素SensorData[30]所在位置的值, SensorData[30]所在的位置原本是一个LCD显示变量,这正是显示屏上的那个值不正常被改变的原因。真庆幸这么轻而易举的发现了这个Bug。

9 关键数据多区备份,取数据采用“表决法”

RAM中的数据在受到干扰情况下有可能被改变,对于系统关键数据必须进行保护。关键数据包括全局变量、静态变量以及需要保护的数据区域。数据备份与原数据不应该处于相邻位置,因此不应由编译器默认分配备份数据位置,而应该由程序员指定区域存储。

可以将RAM分为3个区域,第一个区域保存原码,第二个区域保存反码,第三个区域保存异或码,区域之间预留一定量的“空白”RAM作为隔离。可以使用编译器的“分散加载”机制将变量分别存储在这些区域。需要进行读取时,同时读出3份数据并进行表决,取至少有两个相同的那个值。

假如设备的RAM从0x1000_0000开始,我需要在RAM的0x1000_00000x10007FFF内存储原码,在0x1000_90000x10009FFF内存储反码,在0x1000_B000~0x1000BFFF内存储0xAA的异或码,编译器的分散加载可以设置为:

LR_IROM1 0x00000000 0x00080000 { ; load region size_region
ER_IROM1 0x00000000 0x00080000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}


RW_IRAM1 0x10000000 0x00008000 { ;保存原码
.ANY (+RW +ZI )
}


RW_IRAM3 0x10009000 0x00001000{ ;保存反码
.ANY (MY_BK1)
}


RW_IRAM2 0x1000B000 0x00001000 { ;保存异或码
.ANY (MY_BK2)
}
}

如果一个关键变量需要多处备份,可以按照下面方式定义变量,将三个变量分别指定到三个不连续的RAM区中,并在定义时按照原码、反码、0xAA的异或码进行初始化。

uint32 plc_pc=0; //原码
__attribute__((section("MY_BK1"))) uint32 plc_pc_not=~0x0; //反码
__attribute__((section("MY_BK2"))) uint32 plc_pc_xor=0x0^0xAAAAAAAA; //异或码

当需要写这个变量时,这三个位置都要更新;读取变量时,读取三个值做判断,取至少有两个相同的那个值。

为什么选取异或码而不是补码?这是因为MDK的整数是按照补码存储的,正数的补码与原码相同,在这种情况下,原码和补码是一致的,不但起不到冗余作用,反而对可靠性有害。比如存储的一个非零整数区因为干扰,RAM都被清零,由于原码和补码一致,按照3取2的“表决法”,会将干扰值0当做正确的数据。

10 非易失性存储器的数据存储

非易失性存储器包括但不限于Flash、EEPROM、铁电。仅仅将写入非易失性存储器中的数据再读出校验是不够的。强干扰情况下可能导致非易失性存储器内的数据错误,在写非易失性存储器的期间系统掉电将导致数据丢失,因干扰导致程序跑飞到写非易失性存储器函数中,将导致数据存储紊乱。一种可靠的办法是将非易失性存储器分成多个区,每个数据都将按照不同的形式写入到这些分区中,需要进行读取时,同时读出多份数据并进行表决,取相同数目较多的那个值。

对于因干扰导致程序跑飞到写非易失性存储器函数,还应该配合软件锁以及严格的入口检验,单单依靠写数据到多个区是不够的也是不明智的,应该在源头进行阻截。

11 软件锁

软件锁可以实现但不局限于环环相扣。对于初始化序列或者有一定先后顺序的函数调用,为了保证调用顺序或者确保每个函数都被调用,我们可以使用环环相扣,实质上这也是一种软件锁。此外对于一些安全关键代码语句(是语句,而不是函数),可以给它们设置软件锁,只有持有特定钥匙的,才可以访问这些关键代码。

比如,向Flash写一个数据,我们会判断数据是否合法、写入的地址是否合法,计算要写入的扇区。之后调用写Flash子程序,在这个子程序中,判断扇区地址是否合法、数据长度是否合法,之后就要将数据写入Flash。由于写Flash语句是安全关键代码,所以程序给这些语句上锁:必须具有正确的钥匙才可以写Flash。这样即使是程序跑飞到写Flash子程序,也能大大降低误写的风险。

/***************************************************************
* 名称:RamToFlash()
* 功能:复制RAM的数据到FLASH,命令代码51。
* 入口参数:dst 目标地址,即FLASH起始地址。以512字节为分界
* src 源地址,即RAM地址。地址必须字对齐
* no 复制字节个数,为512/1024/4096/8192
* ProgStart 软件锁标志
* 出口参数:IAP返回值(paramout缓冲区) CMD_SUCCESS,SRC_ADDR_ERROR,DST_ADDR_ERROR,
SRC_ADDR_NOT_MAPPED,DST_ADDR_NOT_MAPPED,COUNT_ERROR,BUSY,未选择扇区
****************************************************************/
void RamToFlash(uint32 dst, uint32 src, uint32 no,uint8 ProgStart)
{
    PLC_ASSERT("Sector number",(dst>=0x00040000)&&(dst<=0x0007FFFF));
    PLC_ASSERT("Copy bytes number is 512",(no==512));
    PLC_ASSERT("ProgStart==0xA5",(ProgStart==0xA5));
    paramin[0] = IAP_RAMTOFLASH; // 设置命令字
    paramin[1] = dst; // 设置参数
    paramin[2] = src;
    paramin[3] = no;
    paramin[4] = Fcclk/1000;
    if(ProgStart==0xA5) //只有软件锁标志正确时,才执行关键代码
    {
        iap_entry(paramin, paramout); // 调用IAP服务程序
        ProgStart=0;
    }
    else
    {
        paramout[0]=PROG_UNSTART;
    }


}

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

12 通信数据的检错

通讯线上的数据误码相对严重,通讯线越长,所处的环境越恶劣,误码会越严重。抛开硬件和环境的作用,我们的软件应能识别错误的通讯数据。对此有一些应用措施:

  • 制定协议时,限制每帧的字节数;

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

  • 使用多种校验

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

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

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

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

开关量容易受到尖脉冲干扰,如果不进行滤除,可能会造成误动作。一般情况下,需要对开关量输入信号进行多次采样,并进行逻辑判断直到确认信号无误为止。多次采样之间需要有一定时间间隔,具体跟开关量的最大切换频率有关,一般不小于1ms。

14 开关量输出

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

15 初始化信息的保存与恢复

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

16 while循环

有时候程序员会使用while(!flag);语句来等待标志flag改变,比如串口发送时用来等待一字节数据发送完成。这样的代码时存在风险的,如果因为某些原因标志位一直不改变则会造成系统死机。良好冗余的程序是设置一个超时定时器,超过一定时间后,强制程序退出while循环。

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

原代码简化如下所示:

HRESULT GetMachineName ( WCHAR *pwszPath,
WCHARwszMachineName[MAX_COMPUTTERNAME_LENGTH_FQDN+1])
{
    WCHAR *pwszServerName = wszMachineName;
    WCHAR *pwszTemp = pwszPath + 2;
    while ( *pwszTemp != L’\\’ )               /* 这句代码循环结束条件不充分 */
        *pwszServerName++= *pwszTemp++;
    /*… */
}

微软发布的安全补丁MS03-026解决了这个问题,为GetMachineName()函数设置了充分终止条件。一个解决代码简化如下所示(并非微软补丁代码):

HRESULT GetMachineName( WCHAR *pwszPath,
WCHARwszMachineName[MAX_COMPUTTERNAME_LENGTH_FQDN+1])
{
    WCHAR *pwszServerName = wszMachineName;
    WCHAR *pwszTemp = pwszPath + 2;
    WCHAR *end_addr = pwszServerName +MAX_COMPUTTERNAME_LENGTH_FQDN;
    while ((*pwszTemp != L’\\’ ) && (*pwszTemp != L’\0’)
            && (pwszServerName<end_addr))  /*充分终止条件*/
            *pwszServerName++= *pwszTemp++;
    /*… */
}

17 系统自检

对CPU、RAM、Flash、外部掉电保存存储器以及其他线路自检。

18 其它一些编程建议

  • 深入理解嵌入式C语言以及编译器
  • 细致、谨慎的编程
  • 使用好的风格和合理的设计
  • 不要仓促编写代码,写每一行的代码时都要三思而后行:可能会出现什么样的错误?是否考虑了所有的逻辑分支?
  • 打开编译器所有警告开关
  • 使用静态分析工具分析代码
  • 安全的读写数据(检查所有数组边界…)
  • 检查指针的合法性
  • 检查函数入口参数合法性
  • 检查所有返回值
  • 在声明变量位置初始化所有变量
  • 合理的使用括号
  • 谨慎的进行强制转换
  • 使用好的诊断信息日志和工具


六、嵌入式开发中的C语言特性

   本文面向的,正是使用单片机、ARM7、Cortex-M3这类微控制器的编程人员。

    C语言诡异且有种种陷阱和缺陷,需要程序员多年历练才能达到较为完善的地步。

    总是有大批的初学者,前仆后继的倒在这些陷阱和缺陷上,民用设备、工业设备甚至是航天设备都不例外。本文将结合具体例子再次审视它们,希望引起足够重视。深入理解C语言特性,是编写优质嵌入式C程序的基础。

    由于篇幅限制,后续再推送编译器、防御性编程、测试和编程思想这几个方面的内容,来讨论如何编写优质嵌入式C程序。

1 处处都是陷阱

1.1 无心之过

1) “=”和”==”

    将比较运算符”==”误写成赋值运算符”=”,可能是绝大多数人都遇到过的,比如下面代码:

if(x=5)
{
//其它代码   
}

    代码的本意是比较变量x是否等于常量5,但是误将”==”写成了”=”,if语句恒为真。如果在逻辑判断表达式中出现赋值运算符,现在的大多数编译器会给出警告信息。比如keil MDK会给出警告提示:“warning: #187-D: use of "=" where"==" may have been intended”,但并非所有程序员都会注意到这类警告,因此有经验的程序员使用下面的代码来避免此类错误:

if(5==x)
 {
     //其它代码   
 }

    将常量放在变量x的左边,即使程序员误将’==’写成了’=’,编译器会产生一个任谁也不能无视的语法错误信息:不可给常量赋值!

2) 复合赋值运算符

    复合赋值运算符(+=、*=等等)虽然可以使表达式更加简洁并有可能产生更高效的机器代码,但某些复合赋值运算符也会给程序带来隐含Bug,比如”+=”容易误写成”=+”,代码如下:

tmp=+1;

    代码本意是想表达tmp=tmp+1,但是将复合赋值运算符”+=”误写成”=+”:将正整数常量1赋值给变量tmp。编译器会欣然接受这类代码,连警告都不会产生。

    如果你能在调试阶段就发现这个Bug,真应该庆祝一下,否则这很可能会成为一个重大隐含Bug,且不易被察觉。

    复合赋值运算符”-=”也有类似问题存在。

3) 其它容易误写

  • 使用了中文标点
  • 头文件声明语句最后忘记结束分号
  • 逻辑与&&和位与&、逻辑或||和位或|、逻辑非!和位取反~
  • 字母l和数字1、字母O和数字0

    这些误写其实容易被编译器检测出,只需要关注编译器对此的提示信息,就能很快解决。

1.2 数组下标

    数组常常也是引起程序不稳定的重要因素,C语言数组的迷惑性与数组下标从0开始密不可分,你可以定义int test[30],但是你绝不可以使用数组元素test [30],除非你自己明确知道在做什么。

1.3 容易被忽略的break关键字

1) 不能漏加的break

    switch…case语句可以很方便的实现多分支结构,但要注意在合适的位置添加break关键字。程序员往往容易漏加break从而引起顺序执行多个case语句,这也许是C的一个缺陷之处。

    对于switch…case语句,从概率论上说,绝大多数程序一次只需执行一个匹配的case语句,而每一个这样的case语句后都必须跟一个break。去复杂化大概率事件,这多少有些不合常情。

2) 不能乱加的break

    break关键字用于跳出最近的那层循环语句或者switch语句,但程序员往往不够重视这一点。

    1990年1月15日,AT&T电话网络位于纽约的一台交换机宕机并且重启,引起它邻近交换机瘫痪,由此及彼,一个连着一个,很快,114型交换机每六秒宕机重启一次,六万人九小时内不能打长途电话。

    当时的解决方式:工程师重装了以前的软件版本。。。事后的事故调查发现,这是break关键字误用造成的。《C专家编程》提供了一个简化版的问题源码:

51c~嵌入式C语言~合集1_嵌入式开发_10

    那个程序员希望从if语句跳出,但他却忘记了break关键字实际上跳出最近的那层循环语句或者switch语句。现在它跳出了switch语句,执行了use_modes_pointer()函数。但必要的初始化工作并未完成,为将来程序的失败埋下了伏笔。

1.4 意想不到的八进制

    将一个整形常量赋值给变量,代码如下所示:

int a=34, b=034;

    变量a和b相等吗?

    答案是不相等的。我们知道,16进制常量以’0x’为前缀,10进制常量不需要前缀,那么8进制呢?它与10进制和16进制表示方法都不相同,它以数字’0’为前缀,这多少有点奇葩:三种进制的表示方法完全不相同。

    如果8进制也像16进制那样以数字和字母表示前缀的话,或许更有利于减少软件Bug,毕竟你使用8进制的次数可能都不会有误使用的次数多!下面展示一个误用8进制的例子,最后一个数组元素赋值错误:

a[0]=106;       /*十进制数106*/
 a[1]=112;      /*十进制数112*/
 a[2]=052;       /*实际为十进制数42,本意为十进制52*/

1.5指针加减运算

    **指针的加减运算是特殊的。**下面的代码运行在32位ARM架构上,执行之后,a和p的值分别是多少?

int a=1;
int *p=(int *)0x00001000;
a=a+1;
p=p+1;

    对于a的值很容判断出结果为2,但是p的结果却是0x00001004。指针p加1后,p的值增加了4,这是为什么呢?原因是指针做加减运算时是以指针的数据类型为单位。p+1实际上是按照公式p+1*sizeof(int)来计算的。不理解这一点,在使用指针直接操作数据时极易犯错。

    某项目使用下面代码对连续RAM初始化零操作,但运行发现有些RAM并没有被真正清零。

51c~嵌入式C语言~合集1_嵌入式开发_11

    通过分析我们发现,由于pRAMaddr是一个无符号int型指针变量,所以pRAMaddr+=4代码其实使pRAMaddr偏移了4*sizeof(int)=16个字节,所以每执行一次for循环,会使变量pRAMaddr偏移16个字节空间,但只有4字节空间被初始化为零。其它的12字节数据的内容,在大多数架构处理器中都会是随机数。

1.6关键字sizeof

    不知道有多少人最初认为sizeof是一个函数。其实它是一个关键字,其作用是返回一个对象或者类型所占的内存字节数,对绝大多数编译器而言,返回值为无符号整形数据。需要注意的是,使用sizeof获取数组长度时,不要对指针应用sizeof操作符,比如下面的例子:

void ClearRAM(char array[])
{
int i ;
     for(i=0;i<sizeof(array)/sizeof(array[0]);i++)     //这里用法错误,array实际上是指针  
     {
array[i]=0x00;
     }
 }


int main(void)
{
char Fle[20];


     ClearRAM(Fle);          //只能清除数组Fle中的前四个元素  
 }

    我们知道,对于一个数组array[20],我们使用代码sizeof(array)/sizeof(array[0])可以获得数组的元素(这里为20),但数组名和指针往往是容易混淆的,有且只有一种情况下数组名是可以当做指针的,那就是**数组名作为函数形参时,数组名被认为是指针,同时,它不能再兼任数组名。

    **注意只有这种情况下,数组名才可以当做指针,但不幸的是这种情况下容易引发风险。在ClearRAM函数内,作为形参的array[]不再是数组名了,而成了指针。sizeof(array)相当于求指针变量占用的字节数,在32位系统下,该值为4,sizeof(array)/sizeof(array[0])的运算结果也为4。所以在main函数中调用ClearRAM(Fle),也只能清除数组Fle中的前四个元素了。

1.7增量运算符’++’和减量运算符‘--‘

    增量运算符”++”和减量运算符”--“既可以做前缀也可以做后缀。**前缀和后缀的区别在于值的增加或减少这一动作发生的时间是不同的。**作为前缀是先自加或自减然后做别的运算,作为后缀时,是先做运算,之后再自加或自减。许多程序员对此认识不够,就容易埋下隐患。下面的例子可以很好的解释前缀和后缀的区别。

int a=8,b=2,y;
y=a+++--b;

    代码执行后,y的值是多少?

    这个例子并非是挖空心思设计出来专门让你绞尽脑汁的C难题(如果你觉得自己对C细节掌握很有信心,做一些C难题检验一下是个不错的选择。那么,《The C Puzzle Book》这本书一定不要错过),你甚至可以将这个难懂的语句作为不友好代码的例子。但是它也可以让你更好的理解C语言。根据运算符优先级以及编译器识别字符的贪心法原则,第二句代码可以写成更明确的形式:

y=(a++)+(--b);

    当赋值给变量y时,a的值为8,b的值为1,所以变量y的值为9;赋值完成后,变量a自加,a的值变为9,千万不要以为y的值为10。这条赋值语句相当于下面的两条语句:

y=a+(--b);
a=a+1;

1.8逻辑与’&&’和逻辑或’||’的陷阱

    为了提高系统效率,逻辑与和逻辑或操作的规定如下:**如果对第一个操作数求值后就可以推断出最终结果,第二个操作数就不会进行求值!**比如下面代码:

if((i>=0)&&(i++ <=max))
 {
//其它代码  
 }

    在这个代码中,只有当i>=0时,i++才会被执行。这样,i是否自增是不够明确的,这可能会埋下隐患。逻辑或与之类似。

1.9结构体的填充

    结构体可能产生填充,因为对大多数处理器而言,访问按字或者半字对齐的数据速度更快,当定义结构体时,编译器为了性能优化,可能会将它们按照半字或字对齐,这样会带来填充问题。比如以下两个个结构体:

    第一个结构体:

struct {
char  c;
short s;
int   x;
 }str_test1;

    第二个结构体: 

struct {
char  c;
int   x;
short s;
 }str_test2;

    这两个结构体元素都是相同的变量,只是元素换了下位置,那么这两个结构体变量占用的内存大小相同吗?

    其实这两个结构体变量占用的内存是不同的,对于Keil MDK编译器,默认情况下第一个结构体变量占用8个字节,第二个结构体占用12个字节,差别很大。第一个结构体变量在内存中的存储格式如下图所示:

51c~嵌入式C语言~合集1_嵌入式开发_12

    第二个结构体变量在内存中的存储格式如下图所示。对比两个图可以看出MDK编译器是是怎么将数据对齐的,这其中的填充内容是之前内存中的数据,是随机的,所以不能在结构之间逐字节比较;另外,合理的排布结构体内的元素位置,可以最大限度减少填充,节省RAM。

51c~嵌入式C语言~合集1_嵌入式开发_13

2 不可轻视的优先级

    C语言有32个关键字,却有34个运算符。要记住所有运算符的优先级是困难的。稍不注意,你的代码逻辑和实际执行就会有很大出入。

    比如下面将BCD码转换为十六进制数的代码:

result=(uTimeValue>>4)*10+uTimeValue&0x0F;

    这里uTimeValue存放的BCD码,想要转换成16进制数据,实际运行发现,如果uTimeValue的值为0x23,按照我设定的逻辑,result的值应该是0x17,但运算结果却是0x07。经过种种排查后,才发现’+’的优先级是大于’&’的,相当于(uTimeValue>>4)*10+uTimeValue与0x0F位与,结果自然与逻辑不符。符合逻辑的代码应该是:

result=(uTimeValue>>4)*10+(uTimeValue&0x0F);

    不合理的#define会加重优先级问题,让问题变得更加隐蔽。

51c~嵌入式C语言~合集1_c语言_14

    编译器在编译后将宏带入,原代码语句变为:

if(IO0PIN&(1<<11) ==(1<<11))
 {
//其它代码   
 }

    运算符'=='的优先级是大于'&'的,代码IO0PIN&(1<<11) ==(1<<11))等效为IO0PIN&0x00000001:判断端口P0.0是否为高电平,这与原意相差甚远。因此,使用宏定义的时候,最好将被定义的内容用括号括起来。

    按照常规方式使用时,可能引起误会的运算符还有很多,如下表所示。C语言的运算符当然不会只止步于数目繁多!

51c~嵌入式C语言~合集1_c语言_15

    有一个简便方法可以避免优先级问题:不清楚的优先级就加上”()”,但这样至少有会带来两个问题:

  • 过多的括号影响代码的可读性,包括自己和以后的维护人员
  • 别人的代码不一定用括号来解决优先级问题,但你总要读别人的代码

    无论如何,在嵌入式编程方面,该掌握的基础知识,偷巧不得。建议花一些时间,将优先级顺序以及容易出错的优先级运算符理清几遍。

隐式转换

    C语言的设计理念一直被人吐槽,因为它认为C程序员完全清楚自己在做什么,其中一个证据就是隐式转换。C语言规定,**不同类型的数据(比如char和int型数据)需要转换成同一类型后,才可进行计算。

    **如果你混合使用类型,比如用char类型数据和int类型数据做减法,C使用一个规则集合来自动(隐式的)完成类型转换。这可能很方便,但也很危险。

    这就要求我们理解这个转换规则并且能应用到程序中去!

  1. 当出现在表达式里时,有符号和无符号的char和short类型都将自动被转换为int类型,在需要的情况下,将自动被转换为unsigned int(在short和int具有相同大小时)。这称为类型提升。

    提升在算数运算中通常不会有什么大的坏处,但如果位运算符 ~ 和 << 应用在基本类型为unsigned char或unsigned short 的操作数,结果应该立即强制转换为unsigned char或者unsigned short类型(取决于操作时使用的类型)。

uint8_t  port =0x5aU;
uint8_t  result_8;
result_8= (~port) >> 4;

    假如我们不了解表达式里的类型提升,认为在运算过程中变量port一直是unsigned char类型的。我们来看一下运算过程:~port结果为0xa5,0xa5>>4结果为0x0a,这是我们期望的值。

    但实际上,result_8的结果却是0xfa!在ARM结构下,int类型为32位。变量port在运算前被提升为int类型:~port结果为0xffffffa5,0xa5>>4结果为0x0ffffffa,赋值给变量result_8,发生类型截断(这也是隐式的!),result_8=0xfa。经过这么诡异的隐式转换,结果跟我们期望的值,已经大相径庭!正确的表达式语句应该为:

result_8=(unsigned char) (~port) >> 4;             /*强制转换*/
  1. 在包含两种数据类型的任何运算里,两个值都会被转换成两种类型里较高的级别。类型级别从高到低的顺序是long double、double、float、unsigned long long、long long、unsigned long、long、unsigned int、int。

    这种类型提升通常都是件好事,但往往有很多程序员不能真正理解这句话,比如下面的例子(int类型表示16位)。

uint16_t  u16a = 40000;             /* 16位无符号变量*/
uint16_t  u16b= 30000;           /*16位无符号变量*/
uint32_t  u32x;                   /*32位无符号变量 */
uint32_t  u32y;
 u32x = u16a +u16b;                 /* u32x = 70000还是4464 ? */
 u32y =(uint32_t)(u16a + u16b);    /* u32y = 70000 还是4464 ? */

    u32x和u32y的结果都是4464(70000%65536)!不要认为表达式中有一个高类别uint32_t类型变量,编译器都会帮你把所有其他低类别都提升到uint32_t类型。正确的书写方式:

u32x = (uint32_t)u16a +(uint32_t)u16b;      
//或者:
 u32x = (uint32_t)u16a + u16b;

    后一种写法在本表达式中是正确的,但是在其它表达式中不一定正确,比如:

uint16_t u16a,u16b,u16c;
uint32_t  u32x;
u32x= u16a + u16b + (uint32_t)u16c;/*错误写法,u16a+ u16b仍可能溢出 */
  1. 在赋值语句里,计算的最后结果被转换成将要被赋予值的那个变量的类型。这一过程可能导致类型提升也可能导致类型降级。降级可能会导致问题。比如将运算结果为321的值赋值给8位char类型变量。程序必须对运算时的数据溢出做合理的处理。很多其他语言,像Pascal(C语言设计者之一曾撰文狠狠批评过Pascal语言),都不允许混合使用类型,但C语言不会限制你的自由,即便这经常引起Bug。
  2. 当作为函数的参数被传递时,char和short会被转换为int,float会被转换为double。

    当不得已混合使用类型时,一个比较好的习惯是使用类型强制转换。强制类型转换可以避免编译器隐式转换带来的错误,同时也向以后的维护人员传递一些有用信息。这有个前提:你要对强制类型转换有足够的了解!下面总结一些规则:

  • 并非所有强制类型转换都是由风险的,把一个整数值转换为一种具有相同符号的更宽类型时,是绝对安全的。
  • 精度高的类型强制转换为精度低的类型时,通过丢弃适当数量的最高有效位来获取结果,也就是说会发生数据截断,并且可能改变数据的符号位。
  • 精度低的类型强制转换为精度高的类型时,如果两种类型具有相同的符号,那么没什么问题;需要注意的是负的有符号精度低类型强制转换为无符号精度高类型时,会不直观的执行符号扩展,例如:
unsigned int bob;
signed char fred = -1;
bob=(unsigned int )fred;    /*发生符号扩展,此时bob为0xFFFFFFFF*/








七、STM32单片机的堆栈

学习STM32单片机的时候,总是能遇到“堆栈”这个概念。分享本文,希望对你理解堆栈有帮助。

    对于了解一点汇编编程的人,就可以知道,堆栈是内存中一段连续的存储区域,用来保存一些临时数据。堆栈操作由PUSH、POP两条指令来完成。而程序内存可以分为几个区:

  • 栈区(stack)
  • 堆区(Heap)
  • 全局区(static)
  • 文字常亮区程序代码区

    程序编译之后,全局变量,静态变量已经分配好内存空间,在函数运行时,程序需要为局部变量分配栈空间,当中断来时,也需要将函数指针入栈,保护现场,以便于中断处理完之后再回到之前执行的函数。
    栈是从高到低分配,堆是从低到高分配。

普通单片机与STM32单片机中堆栈的区别   

 普通单片机启动时,不需要用bootloader将代码从ROM搬移到RAM。

    但是STM32单片机需要。

    这里我们可以先看看单片机程序执行的过程,单片机执行分三个步骤:

  • 取指令
  • 分析指令
  • 执行指令

    根据PC的值从程序存储器读出指令,送到指令寄存器。然后分析执行执行。这样单片机就从内部程序存储器去代码指令,从RAM存取相关数据。

    RAM取数的速度是远高于ROM的,但是普通单片机因为本身运行频率不高,所以从ROM取指令慢并不影响。

    而STM32的CPU运行的频率高,远大于从ROM读写的速度。所以需要用bootloader将代码从ROM搬移到RAM。

    使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。

    其实堆栈就是单片机中的一些存储单元,这些存储单元被指定保存一些特殊信息,比如地址(保护断点)和数据(保护现场)。

    如果非要给他加几个特点的话那就是:

  • 这些存储单元中的内容都是程序执行过程中被中断打断时,事故现场的一些相关参数。如果不保存这些参数,单片机执行完中断函数后就无法回到主程序继续执行了。
  • 这些存储单元的地址被记在了一个叫做堆栈指针(SP)的地方。

结合STM32的开发讲述堆栈

    从上面的描述可以看得出来,在代码中是如何占用堆和栈的。可能很多人还是无法理解,这里再结合STM32的开发过程中与堆栈相关的内容来进行讲述。

    如何设置STM32的堆栈大小?

    在基于MDK的启动文件开始,有一段汇编代码是分配堆栈大小的。

51c~嵌入式C语言~合集1_c语言_16

    这里重点知道堆栈数值大小就行。还有一段AREA(区域),表示分配一段堆栈数据段。数值大小可以自己修改,也可以使用STM32CubeMX数值大小配置,如下图所示。

51c~嵌入式C语言~合集1_嵌入式开发_17

    STM32F1默认设置值0x400,也就是1K大小。

Stack_Size EQU 0x400

    函数体内局部变量:

void Fun(void){ char i; int Tmp[256]; //...}

    局部变量总共占用了256*4 + 1字节的栈空间。所以,在函数内有较多局部变量时,就需要注意是否超过我们配置的堆栈大小。

    函数参数:

void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init)

    这里要强调一点:传递指针只占4字节,如果传递的是结构体,就会占用结构大小空间。提示:在函数嵌套,递归时,系统仍会占用栈空间。

    堆(Heap)的默认设置0x200(512)字节。

Heap_Size EQU 0x200

    大部分人应该很少使用malloc来分配堆空间。虽然堆上的数据只要程序员不释放空间就可以一直访问,但是,如果忘记了释放堆内存,那么将会造成内存泄漏,甚至致命的潜在错误。

MDK中RAM占用大小分析

    经常在线调试的人,可能会分析一些底层的内容。这里结合MDK-ARM来分析一下RAM占用大小的问题。在MDK编译之后,会有一段RAM大小信息:

51c~嵌入式C语言~合集1_嵌入式开发_18

    这里4+6=1640,转换成16进制就是0x668,在进行在调试时,会出现:

51c~嵌入式C语言~合集1_嵌入式开发_19

    这个MSP就是主堆栈指针,一般我们复位之后指向的位置,复位指向的其实是栈顶:

51c~嵌入式C语言~合集1_嵌入式开发_20

    而MSP指向地址0x20000668是0x20000000偏移0x668而得来。具体哪些地方占用了RAM,可以参看map文件中【Image Symbol Table】处的内容:

51c~嵌入式C语言~合集1_c语言_21









八、嵌入式开发中更接近底层的汇编与C语言

本文介绍下更接近硬件底层的C语言与汇编,解释 CPU 如何执行代码。

高级语言与低级语言

    学习编程其实就是学习与计算机交流的语言。因为计算机不理解人类语言,通过编译器把人类写的代码转成二进制代码,才能在机器上运行。掌握了高级语言,并不等于理解计算机实际的运行步骤,还需要对C语言甚至是汇编有所了解才行。编程语言从低级到高级,如下图所示。其中,Assembly Language也就是我们说的汇编,在机器语言Machine Language与高级语言之间。

51c~嵌入式C语言~合集1_嵌入式开发_22

    然而,计算机只能理解低级语言,它专门用来控制硬件。

    汇编语言就是一种低级语言,直接描述或者控制CPU的运行。通过学习汇编语言,可以了解CPU到底干了些什么,相关文章推荐:CPU怎么识别我们写的代码?

    汇编语言不容易学习,大多数的嵌入式开发用C语言就能做得很好。用C语言开发效率更高,程序运行效率并不会大打折扣。为什么还要学习汇编呢?权当是为了更接近真相吧!

汇编语言怎么来的

    作为智能设备核心的CPU只负责计算,本身不具备智能,只会按照指令要求去执行相应动作。

    这些指令都是二进制的,称为操作码(opcode),比如加法指令就是00000011。编译器的作用,就是将高级语言写好的程序,翻译成一条条操作码。

    最早的时候,编写程序就是手写二进制指令,程序就是一串0或1。据说在上世纪,世界上只有为数不多的天才可以做到。写完一连串01程序之后,通过各种开关输入计算机,比如要做加法了,就按一下加法开关,相关文章:用一堆开关做成一个CPU?后来,发明了纸带打孔机,通过在纸带上打孔,将二进制指令自动输入计算机。如下图,就可能是一段计算机指令。

    但是,这种反人类的二进制程序难以理解,可读性极差,换人来维护基本上等于从头再来!根本看不出来机器干了什么。为了解决可读性的问题,以及偶尔的编辑需求,汇编语言应运而生。

    早期,为了解决二进制指令的可读性问题,工程师曾经将那些二进制指令写成了八进制,但是八进制的可读性也不行。很自然地,最后还是用文字表达。汇编语言是二进制指令的文本形式,与指令是一一对应的关系。比如,加法指令00000011写成汇编语言就是 ADD。内存地址也不再直接引用,而是用标签表示。

    把这些文字指令翻译成二进制,这个步骤就称为汇编assembling,完成这个步骤的程序就叫做汇编器assembler。它处理的文本,标准化以后称为汇编语言Assembly Language,缩写为asm,文件名后缀为s。 

寄存器与内存模型

寄存器

    每一种CPU 的机器指令都是不一样的,因此对应的汇编语言也不一样。本文介绍的是最常见的Intel 公司CPU使用的那种x86汇编语言。

    学习汇编语言要熟悉两个知识点:寄存器和内存模型。先来说一下寄存器。

    CPU本身只负责运算,不负责储存数据。数据一般都储存在内存之中,CPU要用的时候就去内存读写数据。但是,CPU的运算速度远高于内存的读写速度,为了避免被拖慢,CPU都自带一级缓存和二级缓存。基本上,CPU缓存可以看作是读写速度较快的内存。

    由于CPU缓存还是不够快,另外数据在缓存里面的地址是不固定的,CPU每次读写都要寻址也会拖慢速度。因此,除了缓存之外,CPU还自带了寄存器(register),用来储存最常用的数据。也就是说,像循环变量那种最频繁读写的数据都会放在寄存器里面,CPU优先读写寄存器,再由寄存器跟内存交换数据。如下图,按速度排序,从上到下依次降低。

51c~嵌入式C语言~合集1_嵌入式开发_23

    寄存器不依靠地址区分数据,而依靠名称。每一个寄存器都有自己的名称,我们告诉CPU去具体的哪一个寄存器拿数据,这样的速度是最快的。有人比喻寄存器是CPU的零级缓存。

    早期的x86CPU只有8个寄存器,而且每个都有不同的用途。现在的寄存器已经有100多个了,都变成通用寄存器,不特别指定用途了,但是早期寄存器的名字都被保存了下来。

EAX
EBX
ECX
EDX
EDI
ESI
EBP
ESP

    上面这8个寄存器之中,前面七个都是通用的。ESP 寄存器有特定用途,保存当前 Stack 的地址。

51c~嵌入式C语言~合集1_c语言_24

    常常看到的32位 CPU、64位 CPU 这样的名称,其实指的就是寄存器的大小。32 位 CPU 的寄存器大小就是4个字节。

内存模型:Heap

    寄存器只能存放很少量的数据,大多数时候,CPU 要指挥寄存器,直接跟内存交换数据。所以,除了寄存器,还必须了解内存怎么储存数据。程序运行的时候,操作系统会给它分配一段内存,用来储存程序和运行产生的数据。这段内存有起始地址和结束地址,比如从0x1000到0x8000,起始地址是较小的那个地址,结束地址是较大的那个地址。

51c~嵌入式C语言~合集1_嵌入式开发_25

    程序运行过程中,对于动态的内存占用请求(比如新建对象,或者使用malloc命令),系统就会从预先分配好的那段内存之中,划出一部分给用户,具体规则是从起始地址开始划分(实际上,起始地址会有一段静态数据,这里忽略)。举例来说,用户要求得到10个字节内存,那么从起始地址0x1000开始给他分配,一直分配到地址0x100A,如果再要求得到22个字节,那么就分配到0x1020。

51c~嵌入式C语言~合集1_c语言_26

    这种因为用户主动请求而划分出来的内存区域,叫做 Heap(堆)。它由起始地址开始,从低位(地址)向高位(地址)增长。Heap 的一个重要特点就是不会自动消失,必须手动释放,或者由垃圾回收机制来回收。

内存模型:Stack

    除了 Heap 以外,其他的内存占用叫做 Stack(栈)。简单说,Stack 是由于函数运行而临时占用的内存区域。

51c~嵌入式C语言~合集1_嵌入式开发_27

    请看下面的例子。

int main() {
   int a = 2;
   int b = 3;
}


    上面代码中,系统开始执行main函数时,会为它在内存里面建立一个帧(frame),所有main的内部变量(比如a和b)都保存在这个帧里面。main函数执行结束后,该帧就会被回收,释放所有的内部变量,不再占用空间。

51c~嵌入式C语言~合集1_c语言_28

    如果函数内部调用了其他函数,会发生什么情况?

int main() {
   int a = 2;
   int b = 3;
   return add_a_and_b(a, b);
}

    上面代码中,main函数内部调用了add_a_and_b函数。执行到这一行的时候,系统也会为add_a_and_b新建一个帧,用来储存它的内部变量。也就是说,此时同时存在两个帧:main和add_a_and_b。一般来说,调用栈有多少层,就有多少帧。

51c~嵌入式C语言~合集1_c语言_29

    等到add_a_and_b运行结束,它的帧就会被回收,系统会回到函数main刚才中断执行的地方,继续往下执行。通过这种机制,就实现了函数的层层调用,并且每一层都能使用自己的本地变量。所有的帧都存放在 Stack,由于帧是一层层叠加的,所以 Stack 叫做栈。生成新的帧,叫做"入栈",英文是 push;栈的回收叫做"出栈",英文是 pop。Stack 的特点就是,最晚入栈的帧最早出栈(因为最内层的函数调用,最先结束运行),这就叫做"后进先出"的数据结构。每一次函数执行结束,就自动释放一个帧,所有函数执行结束,整个 Stack 就都释放了。

51c~嵌入式C语言~合集1_嵌入式开发_30

51c~嵌入式C语言~合集1_嵌入式开发_31

    Stack 是由内存区域的结束地址开始,从高位(地址)向低位(地址)分配。比如,内存区域的结束地址是0x8000,第一帧假定是16字节,那么下一次分配的地址就会从0x7FF0开始;第二帧假定需要64字节,那么地址就会移动到0x7FB0。

51c~嵌入式C语言~合集1_c语言_32

CPU 指令一个实例

    了解寄存器和内存模型以后,就可以来看汇编语言到底是什么了。下面是一个简单的程序example.c。

int add_a_and_b(int a, int b) {
   return a + b;
}


int main() {
   return add_a_and_b(2, 3);
}

    gcc 将这个程序转成汇编语言。

$ gcc -S example.c

    上面的命令执行以后,会生成一个文本文件example.s,里面就是汇编语言,包含了几十行指令。这么说吧,一个高级语言的简单操作,底层可能由几个,甚至几十个 CPU 指令构成。CPU 依次执行这些指令,完成这一步操作。

    example.s经过简化以后,大概是下面的样子。

_add_a_and_b:
   push   %ebx
   mov    %eax, [%esp+8] 
   mov    %ebx, [%esp+12]
   add    %eax, %ebx 
   pop    %ebx 
   ret  


_main:
   push   3
   push   2
   call   _add_a_and_b 
   add    %esp, 8
   ret

    可以看到,原程序的两个函数add_a_and_b和main,对应两个标签_add_a_and_b和_main。每个标签里面是该函数所转成的 CPU 运行流程。

    每一行就是 CPU 执行的一次操作。它又分成两部分,就以其中一行为例。

push   %ebx

    这一行里面,push是 CPU 指令,%ebx是该指令要用到的运算子。一个 CPU 指令可以有零个到多个运算子。下面我就一行一行讲解这个汇编程序,建议读者最好把这个程序,在另一个窗口拷贝一份,省得阅读的时候再把页面滚动上来。

push指令

    程序从_main标签开始执行,这时会在 Stack 上为main建立一个帧,并将 Stack 所指向的地址,写入 ESP 寄存器。后面如果有数据要写入main这个帧,就会写在 ESP 寄存器所保存的地址。然后,开始执行第一行代码。

push 3

    push指令用于将运算子放入 Stack,这里就是将3写入main这个帧。

    虽然看上去很简单,push指令其实有一个前置操作。它会先取出 ESP 寄存器里面的地址,将其减去4个字节,然后将新地址写入 ESP 寄存器。使用减法是因为 Stack 从高位向低位发展,4个字节则是因为3的类型是int,占用4个字节。得到新地址以后, 3 就会写入这个地址开始的四个字节。

push 2

    第二行也是一样,push指令将2写入main这个帧,位置紧贴着前面写入的3。这时,ESP 寄存器会再减去 4个字节(累计减去8)。

51c~嵌入式C语言~合集1_嵌入式开发_33

call指令

    第三行的call指令用来调用函数。

call _add_a_and_b

    上面的代码表示调用add_a_and_b函数。这时,程序就会去找_add_a_and_b标签,并为该函数建立一个新的帧。下面就开始执行_add_a_and_b的代码。

push %ebx

    这一行表示将 EBX 寄存器里面的值,写入_add_a_and_b这个帧。这是因为后面要用到这个寄存器,就先把里面的值取出来,用完后再写回去。这时,push指令会再将 ESP 寄存器里面的地址减去4个字节(累计减去12)。

mov指令

    mov指令用于将一个值写入某个寄存器。

mov    %eax, [%esp+8]

    这一行代码表示,先将 ESP 寄存器里面的地址加上8个字节,得到一个新的地址,然后按照这个地址在 Stack 取出数据。根据前面的步骤,可以推算出这里取出的是2,再将2写入 EAX 寄存器。下一行代码也是干同样的事情。

mov    %ebx, [%esp+12]

    上面的代码将 ESP 寄存器的值加12个字节,再按照这个地址在 Stack 取出数据,这次取出的是3,将其写入 EBX 寄存器。

add指令

    add指令用于将两个运算子相加,并将结果写入第一个运算子。

add    %eax, %ebx

    上面的代码将 EAX 寄存器的值(即2)加上 EBX 寄存器的值(即3),得到结果5,再将这个结果写入第一个运算子 EAX 寄存器。

pop指令

    pop指令用于取出 Stack 最近一个写入的值(即最低位地址的值),并将这个值写入运算子指定的位置。

pop    %ebx

    上面的代码表示,取出 Stack 最近写入的值(即 EBX 寄存器的原始值),再将这个值写回 EBX 寄存器(因为加法已经做完了,EBX 寄存器用不到了)。

注意,pop指令还会将 ESP 寄存器里面的地址加4,即回收4个字节。

ret指令

    ret指令用于终止当前函数的执行,将运行权交还给上层函数。也就是,当前函数的帧将被回收。如下,可以看到,该指令没有运算子。

ret

    随着add_a_and_b函数终止执行,系统就回到刚才main函数中断的地方,继续往下执行。

add    %esp, 8

    上面的代码表示,将 ESP 寄存器里面的地址,手动加上8个字节,再写回 ESP 寄存器。这是因为 ESP 寄存器的是 Stack 的写入开始地址,前面的pop操作已经回收了4个字节,这里再回收8个字节,等于全部回收。

ret

    最后,main函数运行结束,执行ret指令退出程序的执行。









九、几个实用的嵌入式C程序代码块

在学习和工作开发的时候,经常需要使用到各种各样不太常用的操作,这种情况一般是自己手动写一些小程序来处理。因为它们不太常用,所以经常用了又没保存,等到下一次在使用的时候又需要重写,这样的非常浪费时间和精力。

    所以想在这里统一记录一下,以备下次重新使用。代码以实用为主,如果缺陷,欢迎指出。

1 十六进制字符转整型数字

功能:

    将16进制的字符串转换为10进制的数字。我是没有找到相应的库函数,所以参考网上的代码自己手动写了个函数来实现。

    常用的函数有atoi,atol,他们都是将10进制的数字字符串转换为int或是long类型,所以在有些情况下不适用。

/*=============================================================================
#     FileName: hex2dec.cpp
#         Desc: Convert a hex string to a int number
#       Author: Caibiao Lee
#      Version: 
#   LastChange: 2018-11-26 
#      History:
=============================================================================*/


#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <ctype.h>


int c2i(char ch)  
{  
    // 如果是数字,则用数字的ASCII码减去48, 如果ch = '2' ,则 '2' - 48 = 2  
    if(isdigit(ch))  
            return ch - 48;  


    // 如果是字母,但不是A~F,a~f则返回  
    if( ch < 'A' || (ch > 'F' && ch < 'a') || ch > 'z' )  
            return -1;  


    // 如果是大写字母,则用数字的ASCII码减去55, 如果ch = 'A' ,则 'A' - 55 = 10  
    // 如果是小写字母,则用数字的ASCII码减去87, 如果ch = 'a' ,则 'a' - 87 = 10  
    if(isalpha(ch))  
            return isupper(ch) ? ch - 55 : ch - 87;  


    return -1;  
} 


int hex2dec(char *hex)  
{  
    int len;  
    int num = 0;  
    int temp;  
    int bits;  
    int i;  
    char str[64] = {0};


 if(NULL==hex)
 {
  printf("input para error \n");
  return 0;
 }




 if(('0'==hex[0])&&(('X'==hex[1])||('x'==hex[1])))
 {
  strcpy(str,&hex[2]);
 }else
 {
  strcpy(str,hex);
 }


 printf("input num = %s \n",str);


    // 此例中 str = "1de" 长度为3, hex是main函数传递的  
    len = strlen(str);  


    for (i=0, temp=0; i<len; i++, temp=0)  
    {  
            // 第一次:i=0, *(str + i) = *(str + 0) = '1', 即temp = 1  
            // 第二次:i=1, *(str + i) = *(str + 1) = 'd', 即temp = 13  
            // 第三次:i=2, *(str + i) = *(str + 2) = 'd', 即temp = 14  
            temp = c2i( *(str + i) );  
            // 总共3位,一个16进制位用 4 bit保存  
            // 第一次:'1'为最高位,所以temp左移 (len - i -1) * 4 = 2 * 4 = 8 位  
            // 第二次:'d'为次高位,所以temp左移 (len - i -1) * 4 = 1 * 4 = 4 位  
            // 第三次:'e'为最低位,所以temp左移 (len - i -1) * 4 = 0 * 4 = 0 位  
            bits = (len - i - 1) * 4;  
            temp = temp << bits;  


            // 此处也可以用 num += temp;进行累加  
            num = num | temp;  
    }  


    // 返回结果  
    return num;  
}  


int main(int argc, char **argv)
{
 int l_s32Ret = 0;


 if(2!=argc)
 {
  printf("=====ERROR!======\n");
  printf("usage: %s Num \n", argv[0]);
  printf("eg 1: %s 0x400\n", argv[0]);
  return 0;
 }


 l_s32Ret = hex2dec(argv[1]);
 printf("value hex = 0x%x \n",l_s32Ret);
 printf("value dec = %d \n",l_s32Ret);
 return 0;
}


运行结果:
biao@ubuntu:~/test/flash$ ./a.out 0x400
input num = 400 
value hex = 0x400 
value dec = 1024 
biao@ubuntu:~/test/flash$
2 字符串转整型

    功能:

    将正常输入的16进制或是10进制的字符串转换为int数据类型。

/*=============================================================================
#     FileName: hex2dec.cpp
#         Desc: Convert a hex/dec string to a int number
#       Author: Caibiao Lee
#      Version: 
#   LastChange: 2018-12-03 
#      History:
=============================================================================*/
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <ctype.h>


int String2int(char *strChar)
{
 int len=0;
 const char *pstrCmp1="0123456789ABCDEF";
 const char *pstrCmp2="0123456789abcdef";


 char *pstr=NULL;
 int uiValue=0;
 int j=0; 
 unsigned int t=0;
 int i=0;
 if(NULL==strChar)
  return -1;
 if(0>=(len=strlen((const char *)strChar)))
  return -1;
 if(NULL!=(pstr=strstr(strChar,"0x"))||NULL!=(pstr=strstr(strChar,"0X")))
 {
  pstr=(char *)strChar+2;


  if(0>=(len=strlen((const char *)pstr)))
   return -1;
  for(i=(len-1);i>=0;i--)
  {
   if(pstr[i]>'F')
   {
    for(t=0;t<strlen((const char *)pstrCmp2);t++)
    { 
     if(pstrCmp2[t]==pstr[i])
      uiValue|=(t<<(j++*4));
    }
   }
   else
   {
    for(t=0;t<strlen((const char *)pstrCmp1);t++)
    { 
     if(pstrCmp1[t]==pstr[i])
      uiValue|=(t<<(j++*4));
    }
   }
  }
 }
 else
 {
  uiValue=atoi((const char*)strChar);
 }
 return uiValue;
}


int main(int argc, char **argv)
{
 int l_s32Ret = 0;


 if(2!=argc)
 {
  printf("=====ERROR!======\n");
  printf("usage: %s Num \n", argv[0]);
  printf("eg 1: %s 0x400\n", argv[0]);
  return 0;
 }
 l_s32Ret = String2int(argv[1]);
 printf("value hex = 0x%x \n",l_s32Ret);
 printf("value dec = %d \n",l_s32Ret);
 return 0;
}
3 创建文件并填充固定数据

功能:

    创建固定大小的一个文件,并且把这个文件填充为固定的数据。

/*=============================================================================
#     FileName: CreateFile.cpp
#         Desc: 创建固定大小的文件,然后填充固定的数据
#       Author: Caibiao Lee
#      Version: 
#   LastChange: 2018-11-26 
#      History:
=============================================================================*/
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <ctype.h>


//#define FILL_DATA_VALUE  0xff
#define FILL_DATA_VALUE  0x30 //char 0


int c2i(char ch)  
{  
    if(isdigit(ch))  
            return ch - 48;  


    if( ch < 'A' || (ch > 'F' && ch < 'a') || ch > 'z' )  
            return -1;  


    if(isalpha(ch))  
            return isupper(ch) ? ch - 55 : ch - 87;  


    return -1;  
} 


int hex2dec(char *hex)  
{  
    int len;  
    int num = 0;  
    int temp;  
    int bits;  
    int i;  
    char str[64] = {0};


 if(NULL==hex)
 {
  printf("input para error \n");
  return 0;
 }


 if(('0'==hex[0])&&(('X'==hex[1])||('x'==hex[1])))
 {
  strcpy(str,&hex[2]);
 }else
 {
  strcpy(str,hex);
 }


 printf("input num = %s \n",str);


    len = strlen(str);  


    for (i=0, temp=0; i<len; i++, temp=0)  
    {  
            temp = c2i( *(str + i) );  


            bits = (len - i - 1) * 4;  
            temp = temp << bits;  


            num = num | temp;  
    }  
    return num;  
}  


int main(int argc, char **argv)
{
 FILE *l_pFile = NULL;
 int  l_s32Rest = 0;
 unsigned int l_WriteLen = 0;
 unsigned int l_FileLen = 0;
 unsigned char TempData[1024] = {FILL_DATA_VALUE};


 if(3!=argc)
 {
  printf("usage: %s FileName  FileLen \n ", argv[0]);
  printf("eg: %s ./Outfile.bin 0x400 \n ", argv[0]);
  return 0;
 };


 const char *l_pFileName = argv[1];
 if(NULL==l_pFileName)
 {
  printf("input file name is NULL \n");
  return -1;
 }


 if(('0'==argv[2][0])&&(('X'==argv[2][1])||('x'==argv[2][1])))
 {
  l_FileLen = hex2dec(argv[2]);


 }else
 {
  l_FileLen = atoi(argv[2]);
 }


 printf("Need To Write Data Len %d \n",l_FileLen);
 printf("Fill Data Vale = 0x%x \n",FILL_DATA_VALUE);


 for(int i=0;i<1024;i++)
 {
  TempData[i] = FILL_DATA_VALUE;
 }




 l_pFile = fopen(l_pFileName,"w+");
 if(l_pFile==NULL)
 {
  printf("open file %s error \n",l_pFileName);
  return -1;
 }




 while(l_WriteLen<l_FileLen)
 {
  if(l_FileLen<1024)
  {
   l_s32Rest = fwrite(TempData,1,l_FileLen,l_pFile);


  }
  else
  {
   l_s32Rest = fwrite(TempData,1,1024,l_pFile);
  }


  if(l_s32Rest <= 0)
  {
   break;
  };


  l_WriteLen +=l_s32Rest; 
 }


 if(NULL!=l_pFile)
 {
  fclose(l_pFile);
  l_pFile = NULL;
 }


 return 0;


}

    运行结果:

biao@ubuntu:~/test/flash$ gcc CreateFile.cpp 
biao@ubuntu:~/test/flash$ ls
a.out  CreateFile.cpp  hex2dec.cpp  main.cpp  out.bin
biao@ubuntu:~/test/flash$ ./a.out ./out.bin 0x10
input num = 10 
Need To Write Data Len 16 
Fill Data Vale = 0x30 
biao@ubuntu:~/test/flash$ ls
a.out  CreateFile.cpp  hex2dec.cpp  main.cpp  out.bin
biao@ubuntu:~/test/flash$ vim out.bin 
  1 0000000000000000
4 批量处理图片

功能:

    批处理将图片前面固定的字节数删除。

/*=============================================================================
#     FileName: CutFile.cpp
#         Desc: 批量处理,将图片的前面固定字节删除
#       Author: Caibiao Lee
#      Version: 
#   LastChange: 2018-11-26 
#      History:
=============================================================================*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>




#define START_READ_POSITION  128
#define PHOTO_START_TIME  83641
//l_s32PhotoTime = 92809;


int Cut_file(char * InputFile)
{
 FILE *l_pFileInput = NULL;
 FILE *l_pFileOutput = NULL;
 char l_ars8OutputName[128] = {0};
 unsigned char l_arru8TempData[1024] = {0};
 int l_s32Ret = 0;
 static unsigned int ls_u32Num = 0;




 if(NULL== InputFile) 
 {
  goto ERROR;
 }


 //sprintf(l_ars8OutputName,"./outfile/_%s",&InputFile[8]);
 sprintf(l_ars8OutputName,"./outfile/00%d.jpg",ls_u32Num++);


 //printf("out file name %s \n",l_ars8OutputName);


 l_pFileInput = fopen(InputFile,"rb+");
 if(NULL==l_pFileInput)
 {
  printf("input file open error\n");
  goto ERROR;
 }


 l_pFileOutput = fopen(l_ars8OutputName,"w+");
 if(NULL==l_pFileOutput)
 {
  printf("out file open error\n");
  goto ERROR;
 }


 fseek(l_pFileInput,START_READ_POSITION,SEEK_SET);


 while(!feof(l_pFileInput))
 {
  l_s32Ret = fread(l_arru8TempData,1,1024,l_pFileInput);
  if(l_s32Ret<0)
  {
   break;
  }


  l_s32Ret = fwrite(l_arru8TempData,1,l_s32Ret,l_pFileOutput);
  if(l_s32Ret<0)
  {
   break;
  }
 }


ERROR:
 if(NULL!=l_pFileOutput)
 {
  fclose(l_pFileOutput);
  l_pFileOutput =NULL;
 };


 if(NULL !=l_pFileInput);
 {
  fclose(l_pFileInput);
  l_pFileInput =NULL;
 }
}


int main(void)
{
 char l_arrs8InputName[128] = {0};
 char l_s8PhotoChannel = 0;
 int  l_s32PhotoTime = 0;


 l_s8PhotoChannel = 3;
 l_s32PhotoTime = PHOTO_START_TIME;


 /**从第一通道开始**/
 for(int j=1;j<l_s8PhotoChannel;j++)
 {


  for(int i=l_s32PhotoTime;i<235959;i++)
  {
   memset(l_arrs8InputName,0,sizeof(l_arrs8InputName));
   sprintf(l_arrs8InputName,"./image/%dY%06d.jpg",j,i);


   if(0==access(l_arrs8InputName,F_OK))
   {
    printf("%s\n",l_arrs8InputName);
    Cut_file(l_arrs8InputName);    
   }
  }
 }
}

    运行结果:

biao@ubuntu:~/test/photo$ gcc CutFile.cpp 
biao@ubuntu:~/test/photo$ ls
a.out  CutFile.cpp  image  outfile
biao@ubuntu:~/test/photo$ ./a.out 
./image/1Y083642.jpg
./image/1Y083714.jpg
./image/1Y083747.jpg
./image/1Y083820.jpg
./image/1Y083853.jpg
./image/1Y083925.jpg
./image/1Y084157.jpg
./image/1Y084228.jpg
./image/1Y084301.jpg
./image/1Y084334.jpg
./image/1Y084406.jpg
./image/1Y084439.jpg
./image/1Y084711.jpg
./image/1Y084742.jpg
./image/1Y173524.jpg
./image/1Y173556.jpg
./image/1Y173629.jpg
./image/1Y173702.jpg
./image/1Y173933.jpg
./image/1Y174004.jpg
./image/1Y174244.jpg
./image/1Y174315.jpg
./image/1Y174348.jpg
./image/1Y174420.jpg
./image/1Y174454.jpg
./image/1Y174733.jpg
biao@ubuntu:~/test/photo$ tree
.
├── a.out
├── CutFile.cpp
├── image
│   ├── 1Y083642.jpg
│   ├── 1Y083714.jpg
│   ├── 1Y083747.jpg
│   ├── 1Y083820.jpg
│   ├── 1Y083853.jpg
│   ├── 1Y083925.jpg
│   ├── 1Y084157.jpg
│   ├── 1Y084228.jpg
│   ├── 1Y084301.jpg
│   ├── 1Y084334.jpg
│   ├── 1Y084406.jpg
│   ├── 1Y084439.jpg
│   ├── 1Y084711.jpg
│   ├── 1Y084742.jpg
│   ├── 1Y173524.jpg
│   ├── 1Y173556.jpg
│   ├── 1Y173629.jpg
│   ├── 1Y173702.jpg
│   ├── 1Y173933.jpg
│   ├── 1Y174004.jpg
│   ├── 1Y174244.jpg
│   ├── 1Y174315.jpg
│   ├── 1Y174348.jpg
│   ├── 1Y174420.jpg
│   ├── 1Y174454.jpg
│   └── 1Y174733.jpg
└── outfile
    ├── 000.jpg
    ├── 0010.jpg
    ├── 0011.jpg
    ├── 0012.jpg
    ├── 0013.jpg
    ├── 0014.jpg
    ├── 0015.jpg
    ├── 0016.jpg
    ├── 0017.jpg
    ├── 0018.jpg
    ├── 0019.jpg
    ├── 001.jpg
    ├── 0020.jpg
    ├── 0021.jpg
    ├── 0022.jpg
    ├── 0023.jpg
    ├── 0024.jpg
    ├── 0025.jpg
    ├── 002.jpg
    ├── 003.jpg
    ├── 004.jpg
    ├── 005.jpg
    ├── 006.jpg
    ├── 007.jpg
    ├── 008.jpg
    └── 009.jpg


2 directories, 54 files
biao@ubuntu:~/test/photo$

    运行前需要创建两个目录,image用来存放需要处理的图片,outfile用来存放处理过后的文件。这种处理文件批处理方式很暴力,偶尔用用还是可以的。

5 IO控制小程序

    嵌入式设备系统一般为了节省空间,一般都会对系统进行裁剪,所以很多有用的命令都会被删除。在嵌入式设备中要调试代码也是比较麻烦的,一般只能看串口打印。现在写了个小程序,专门用来查看和控制海思Hi3520DV300芯片的IO电平状态。

/*=============================================================================
#     FileName: Hi3520_IO_CTRL.cpp
#         Desc: Hi3520DV300 IO Write and  Read
#       Author: Caibiao Lee
#      Version: 
#   LastChange: 2018-11-30
#      History:
=============================================================================*/
#include <stdio.h>
#include <stdlib.h>
#include "hstGpioAL.h"


int PrintfInputTips(char *ps8Name)
{
 printf("=========== error!!! ========\n\n");
 printf("usage Write: %s GPIO bit value \n", ps8Name);
 printf("usage Read : %s GPIO bit \n", ps8Name);
 printf("eg Write 1 to GPIO1_bit02  :     %s 1 2 1\n", ps8Name);
 printf("eg Read  GPIO1_bit02 Value :     %s 1 2 \n\n", ps8Name);


 printf("=============BT20==================\n")
 printf("USB HUB    GPIO_0_2  1_UP; 0_Down \n");
 printf("RESET_HD   GPIO_13_0 0_EN; 1_disEN\n");
 printf("Power_HD   GPIO_13_3 1_UP; 0_Down \n");
 return 0;
}


int main(int argc, char **argv)
{
 if((3!=argc)&&(4!=argc))
 {
  PrintfInputTips(argv[0]);
  return -1;
 }


 unsigned char l_u8GPIONum = 0;
 unsigned char l_u8GPIOBit = 0;
 unsigned char l_u8SetValue = 0;


 GPIO_GROUP_E  l_eGpioGroup;
 GPIO_BIT_E   l_eBit;
 GPIO_DATA_E   l_eData;


 l_u8GPIONum   = atoi(argv[1]);
 l_u8GPIOBit   = atoi(argv[2]);


 if(l_u8GPIONum<14)
 {
  l_eGpioGroup = (GPIO_GROUP_E)l_u8GPIONum;
 }else
 {
  printf("l_u8GPIONum error l_u8GPIONum = %d\n",l_u8GPIONum);
  return -1;
 };


 if(l_u8GPIOBit<8)
 {
  l_eBit = (GPIO_BIT_E)l_u8GPIOBit;
 }else
 {
  printf("l_u8GPIOBit error l_u8GPIOBit = %d\n",l_u8GPIOBit);
  return -1;
 }


 if(NULL!=argv[3])
 {
  l_u8SetValue = atoi(argv[3]);
  if(0==l_u8SetValue)
  {
   l_eData = (GPIO_DATA_E)l_u8SetValue;
  }else if(1==l_u8SetValue)
  {
   l_eData = (GPIO_DATA_E)l_u8SetValue;
  }else
  {
   printf("l_u8SetValue error l_u8SetValue = %d\n",l_u8SetValue);
  }
 }


 if(3==argc)                                                       
 {/**read**/                                                                                                                                                      
     printf("read GPIO%d Bit%d \n",l_u8GPIONum,l_u8GPIOBit);           
        /**set input**/                                               
        HstGpio_Set_Direction(l_eGpioGroup, l_eBit, GPIO_INPUT);                        


     /**read **/                                                                               
     char l_s8bit_val = 0;                                                                     
     HstGpio_Get_Value(l_eGpioGroup, l_eBit, &l_s8bit_val);                                    


     printf("read Data = %d \n",l_s8bit_val);                                                  


   }else if(4==argc)                                                                             
   {/**write**/                                                                                                                                                                            
       printf("Write GPIO %d; Bit %d; Value %d\n",l_u8GPIONum,l_u8GPIOBit,l_u8SetValue);         


       /***set IO output*/                                                                       
       HstGpio_Set_Direction(l_eGpioGroup, l_eBit, GPIO_OUPUT);                                  


       /**Write To IO**/ 
    HstGpio_Set_Value(l_eGpioGroup,l_eBit,l_eData);
   }else                                            
   {                                                                                             


   }


 return 0;


}
6 文件固定位置插入数据

    在文件的固定位置插入固定的数据。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>


#define BASIC_FILE_NAME  "./nandflash.bin"
#define UBOOT_FILE_NAME  "./u-boot.bin"
#define KERNEL_FILE_NAME "./kernel.bin"
#define ROOTFS_FILE_NAME "./rootfs.bin"
#define APP_FILE_NAME  "./app.bin"




#define UBOOT_POSITION  0x00
#define KERNEL_POSITION  0x100000
#define ROOTFS_POSITION  0x500000
#define APP_POSITION  0x2700000






int InsertData(FILE *pfBasic,FILE *psInsert,int s32Position)
{
 int l_S32Ret = 0;
 unsigned char l_arru8Temp[1024] = {0xff};


 fseek(pfBasic,s32Position,SEEK_SET);
 fseek(psInsert,0,SEEK_SET);
 while(1)
 {
  l_S32Ret = fread(l_arru8Temp,1,1024,psInsert);
  if(l_S32Ret > 0)
  {
   l_S32Ret = fwrite(l_arru8Temp,1,l_S32Ret,pfBasic);
   if(l_S32Ret<=0)
   {
    printf("line %d error l_S32Ret = %d \n",__LINE__,l_S32Ret);
    return -1;
   }
  }else
  {
   break;
  }
 }


 return 0;
}






int main(void)
{
 int l_s32Ret = 0;
 FILE *l_pfBasec = NULL;
 FILE *l_pfUboot = NULL;
 FILE *l_pfKernel = NULL;
 FILE *l_pfRootfs = NULL;
 FILE *l_pfApp = NULL;




 l_pfBasec = fopen(BASIC_FILE_NAME,"r+");
 if(NULL==l_pfBasec)
 {
  printf("line %d error \n",__LINE__);
  goto ERROR;
 }


 l_pfUboot = fopen(UBOOT_FILE_NAME,"r");
 if(NULL==l_pfUboot)
 {
  printf("line %d error \n",__LINE__);
  goto ERROR;
 }


 l_pfKernel = fopen(KERNEL_FILE_NAME,"r");
 if(NULL==l_pfKernel)
 {
  printf("line %d error \n",__LINE__);
  goto ERROR;
 }


 l_pfRootfs = fopen(ROOTFS_FILE_NAME,"r");
 if(NULL==l_pfRootfs)
 {
  printf("line %d error \n",__LINE__);
  goto ERROR;
 }


 l_pfApp = fopen(APP_FILE_NAME,"r");
 if(NULL==l_pfApp)
 {
  printf("line %d error \n",__LINE__);
  goto ERROR;
 }


 if(0> InsertData(l_pfBasec,l_pfUboot,UBOOT_POSITION))
 {
  printf("line %d error \n",__LINE__);
  goto ERROR;
 }


 if(0> InsertData(l_pfBasec,l_pfKernel,KERNEL_POSITION))
 {
  printf("line %d error \n",__LINE__);
  goto ERROR;
 }


 if(0> InsertData(l_pfBasec,l_pfRootfs,ROOTFS_POSITION))
 {
  printf("line %d error \n",__LINE__);
  goto ERROR;
 }


 if(0> InsertData(l_pfBasec,l_pfApp,APP_POSITION))
 {
  printf("line %d error \n",__LINE__);
  goto ERROR;
 }




ERROR:
 if(NULL!=l_pfBasec)
 {
  fclose(l_pfBasec);
  l_pfBasec = NULL;
 }


 if(NULL!=l_pfUboot)
 {
  fclose(l_pfUboot);
  l_pfUboot = NULL;
 }


 if(NULL!=l_pfKernel)
 {
  fclose(l_pfKernel);
  l_pfKernel = NULL;
 }




 if(NULL!=l_pfRootfs)
 {
  fclose(l_pfRootfs);
  l_pfRootfs = NULL;
 }


 if(NULL!=l_pfApp)
 {
  fclose(l_pfApp);
  l_pfApp = NULL;
 }


 return 0;
}
7 获取本地IP地址

    在linux设备中获取本地IP地址可以使用下面的程序,支持最大主机有三个网口的设备,当然这个网卡数可以修改。

#include <stdio.h>
#include <ifaddrs.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>


int get_local_ip(char *ps8IpList)
{
    struct ifaddrs *ifAddrStruct;
    char l_s8IpAddr[INET_ADDRSTRLEN];
    void *tmpAddrPtr;
    int l_s32IPCount = 0;


    getifaddrs(&ifAddrStruct);
    while (ifAddrStruct != NULL) 
    {
        if (ifAddrStruct->ifa_addr->sa_family==AF_INET)
        {
            tmpAddrPtr=&((struct sockaddr_in *)ifAddrStruct->ifa_addr)->sin_addr;
            inet_ntop(AF_INET, tmpAddrPtr, l_s8IpAddr, INET_ADDRSTRLEN);
            if (strcmp(l_s8IpAddr, "127.0.0.1") != 0) 
            {
                if(l_s32IPCount == 0)
                {
                        memcpy(ps8IpList, l_s8IpAddr, INET_ADDRSTRLEN);
                } else 
                {
                        memcpy(ps8IpList+INET_ADDRSTRLEN, l_s8IpAddr, INET_ADDRSTRLEN);
                }
                l_s32IPCount++;
            }
        }
        ifAddrStruct=ifAddrStruct->ifa_next;
    }


    freeifaddrs(ifAddrStruct);
    return l_s32IPCount;
}


int main()
{
    char l_arrs8IpAddrList[3][INET_ADDRSTRLEN];
    int l_s32AddrCount;


    memset(l_arrs8IpAddrList, 0, sizeof(l_arrs8IpAddrList));


    l_s32AddrCount = get_local_ip(*l_arrs8IpAddrList);


    for(l_s32AddrCount;l_s32AddrCount>0;l_s32AddrCount--)
    {
        printf("Server Local IP%d: %s\n",l_s32AddrCount,l_arrs8IpAddrList[l_s32AddrCount-1]);
    }


 return 0;
}

来源:https://caibiao-lee.blog.csdn.net/?type=blog










十、xxx