w~嵌入式C语言~合集4

我自己的原文哦~      https://blog.51cto.com/whaosoft/13870376

一、STM32怎么选型

什么是 STM32

    STM32,从字面上来理解,ST是意法半导体,M是Microelectronics的缩写,32表示32位,合起来理解,STM32就是指ST公司开发的32位微控制器。在如今的32位控制器当中,STM32可以说是最璀璨的新星,它受宠若娇,大受工程师和市场的青睐,无芯能出其右。

    STM32属于一个微控制器,自带了各种常用通信接口,比如USART、I2C、SPI等,可接非常多的传感器,可以控制很多的设备。现实生活中,我们接触到的很多电器产品都有STM32的身影,比如智能手环,微型四轴飞行器,平衡车、移动POST机,智能电饭锅,3D打印机等等。

    现在无人机非常火热,高端的无人机用STM32做不来,但是小型的四轴飞行器用STM32还是绰绰有余的。

STM32 分类

    STM32有很多系列,可以满足市场的各种需求,从内核上分有Cortex-M0、M3、M4和M7这几种,每个内核又大概分为主流、高性能和低功耗。具体如下表所示。

   单纯从学习的角度出发,可以选择F1和F4,F1代表了基础型,基于Cortex-M3内核,主频为72MHZ,F4代表了高性能,基于Cortex-M4内核,主频180M。之于F1,F4(429系列以上)除了内核不同和主频的提升外,升级的明显特色就是带了LCD控制器和摄像头接口,支持SDRAM,这个区别在项目选型上会被优先考虑。但是从大学教学和用户初学来说,还是首选F1系列,目前在市场上资料最多,产品占有量最多的就是F1系列的STM32。

更详细的命名方法说明,见下图。

选择合适的 MCU

    了解了STM32的分类和命名方法之后,就可以根据项目的具体需求先大概选择哪类内核的MCU,普通应用,不需要接大屏幕的一般选择Cortex-M3内核的F1系列,如果要追求高性能,需要大量的数据运算,且需要外接RGB大屏幕的则选择Cortex-M4内核的F429系列。明确了大方向之后,接下来就是细分选型,先确定引脚,引脚多的功能就多,价格也贵,具体得根据实际项目中需要使用到什么功能,够用就好。确定好了引脚数目之后再选择FLASH大小,相同引脚数的MCU会有不同的FLASH大小可供选择,这个也是根据实际需要选择,程序大的就选择大点的FLASH,要是产品一量产,这些省下来的都是钱啊。有些月出货量以KK(百万数量级)为单位的产品,不仅是MCU,连电阻电容能少用就少用,更甚者连PCB的过孔的多少都有讲究。项目中的元器件的选型有很多学问。

二、C语言内存泄漏问题及其检视方法

   本文通过介绍内存泄漏问题原理及检视方法,希望后续能够从编码检视环节就杜绝此类问题发生。

    预防内存泄漏问题有多种方法,如加强代码检视、工具检测和内存测试等,本文聚集于开发人员能力提升方面。

内存泄漏问题原理

1 堆内存在C代码中的存储方式

    内存泄漏问题只有在使用堆内存的时候才会出现,栈内存不存在内存泄漏问题,因为栈内存会自动分配和释放。C语言代码中堆内存的申请函数是malloc,常见的内存申请代码如下:

由于malloc函数返回的实际上是一个内存地址,所以保存堆内存的变量一定是一个指针(除非代码编写极其不规范)。再重复一遍,保存堆内存的变量一定是一个指针,这对本文主旨的理解很重要。当然,这个指针可以是单指针,也可以是多重指针。

    malloc函数有很多变种或封装,如g_malloc、g_malloc0、VOS_Malloc等,这些函数最终都会调用malloc函数。

2 堆内存的获取方法

    看到本小节标题,可能有些同学有疑惑,上一小节中的malloc函数,不就是堆内存的获取方法吗?的确是,通过malloc函数申请是最直接的获取方法,如果只知道这种堆内存获取方法,就容易掉到坑里了。

    一般的来讲,堆内存有如下两种获取方法:

「方法一:将函数返回值直接赋给指针,一般表现形式如下:」

char *local_pointer_xx = NULL;
local_pointer_xx = (char*)function_xx(para_xx, …);

    该类涉及到内存申请的函数,返回值一般都指针类型,例如:

GSList* g_slist_append (GSList   *list, gpointer  data)

「方法二:将指针地址作为函数返回参数,通过返回参数保存堆内存地址,一般表现形式如下:」

int ret;
char *local_pointer_xx = NULL;    /**转换后的字符串**/
ret = (char*)function_xx(..., &local_pointer_xx, ...);

    该类涉及到内存申请的函数,一般都有一个入参是双重指针,例如:

__STDIO_INLINE _IO_ssize_t
getline (char **__lineptr, size_t *__n, FILE *__stream)

    前面说通过malloc申请内存,就属于方法一的一个具体表现形式。其实这两类方法的本质是一样的,都是函数内部间接申请了内存,只是传递内存的方法不一样,方法一通过返回值传递内存指针,方法二通过参数传递内存指针。   

3 内存泄漏三要素

    最常见的内存泄漏问题,包含以下三个要素:

**要素一:**函数内有局部指针变量定义;

**要素二:**对该局部指针有通过上一小节中“两种堆内存获取方法”之一获取内存;

**要素三:**在函数返回前(含正常分支和异常分支)未释放该内存,也未保存到其它全局变量或返回给上一级函数。

4 内存释放误区

    稍微使用过C语言编写代码的人,都应该知道堆内存申请之后是需要释放的。但为何还这么容易出现内存泄漏问题呢?一方面,是开发人员经验不足、意识不到位或一时疏忽导致;另一方面,是内存释放误区导致。很多开发人员,认为要释放的内存应该局限于以下两种:

1)直接使用内存申请函数申请出来的内存,如malloc、g_malloc等;

2)该开发人员熟悉的接口中,存在内存申请的情况,如iBMC的兄弟,都应该知道调用如下接口需要释放list指向的内存:

dfl_get_object_list(const char* class_name, GSList **list)

    按照以上思维编写代码,一旦遇到不熟悉的接口中需要释放内存的问题,就完全没有释放内存的意识,内存泄漏问题就自然产生了。

内存泄漏问题检视方法

    检视内存泄漏问题,关键还是要养成良好的编码检视习惯。与内存泄漏三要素对应,需

    要做到如下三点:

(1)在函数中看到有局部指针,就要警惕内存泄漏问题,养成进一步排查的习惯

(2)分析对局部指针的赋值操作,是否属于前面所说的“两种堆内存获取方法”之一,如果是,就要分析函数返回的指针到底指向啥?是全局数据、静态数据还是堆内存?对于不熟悉的接口,要找到对应的接口文档或源代码分析;又或者看看代码中其它地方对该接口的引用,是否进行了内存释放;

(3)如果确认对局部指针存在内存申请操作,就需要分析该内存的去向,是会被保存在全局变量吗?又或者会被作为函数返回值吗?如果都不是,就需要排查函数所有有”return“的地方,保证内存被正确释放。

三、.h文件与.c文件

.h文件与.c文件的关系

    参考高手的程序时,发现别人写的严格的程序都带有一个“KEY.H”,里面定义了.C文件里用到的自己写的函数,如Keyhit()、Keyscan()等。.H文件就是头文件,估计就是Head的意思吧,这是规范程序结构化设计的需要,既可以实现大型程序的模块化,又可以实现根各模块的连接调试。

.H文件介绍:

    在单片机嵌入式C程序设计中,项目一般按功能模块化进行结构化设计。将一个项目划分为多个功能,每个功能的相关程序放在一个C程序文档中,称之为一个模块,对应的文件名即为模块名。一个模块通常由两个文档组成,一个为头文件*.h,对模块中的数据结构和函数原型进行描述;另一个则为C文件*.c ,对数据实例或对象定义,以及函数算法具体实现。

.H文件的作用

    作为项目设计,除了对项目总体功能进行详细描述外,就是对每个模块进行详细定义,也就是给出所有模块的头文件。通常H头文件要定义模块中各函数的功能,以及输入和输出参数的要求。模块的具体实现,由项目组成根据H文件进行设计、编程、调试完成。为了保密和安全,模块实现后以可连接文件OBJ、或库文件LIB的方式提供给项目其他成员使用。由于不用提供源程序文档,一方面可以公开发行,保证开发人员的所有权;另一方面可以防止别人有意或无意修改产生非一致性,造成版本混乱。所以H头文件是项目的详细设计和团队工作划分的依据,也是对模块进行测试的功能说明。要引用模块内的数据或算法,只要用包含include指定模块H头文件即可。

.H文件的基本组成

/*如下为键盘驱动的头文档*/
#ifndef _KEY_H_ //防重复引用,如果没有定义过_KEY_H_,则编译下句
#define _KEY_H_ //此符号唯一, 表示只要引用过一次,即#i nclude,则定义符号_KEY_H_
/char keyhit( void ); //击键否unsigned char Keyscan( void ); //取键值/
#endif

尽量使用宏定义#define

    开始看别人的程序时,发现程序开头,在文件包含后面有很多#define语句,当时就想,搞这么多标示符替换来替换去的,麻不麻烦啊,完全没有理解这种写法的好处。原来,用一个标示符表示常数,有利于以后的修改和维护,修改时只要在程序开头改一下,程序中所有用到的地方就全部修改,节省时间。

#define KEYNUM 65//按键数量,用于Keycode[KEYNUM]
#define LINENUM 8//键盘行数
#define ROWNUM 8//键盘列数

    注意的地方:

  • 宏名一般用大写
  • 宏定义不是C语句,结尾不加分号

不要乱定义变量类型

    以前写程序,当需要一个新的变量时,不管函数内还是函数外的,直接在程序开头定义,虽然不是原则上的错误,但是很不可取的作法。下面说一下,C语言中变量类型的有关概念。从变量的作用范围来分,分为局部变量和全局变量:

  • 全局变量:是在函数外定义的变量,全局变量在程序全部执行过程中都占用资源,全局变量过多使程序的通用性变差,因为全局变量是模块间耦合的原因之一。
  • 局部变量:在函数内部定义的变量,只在函数内部有效。

    从变量的变量值存在的时间分为两种:

  • 静态存储变量:程序运行期间分配固定的存储空间。
  • 动态存储变量:程序运行期间根据需要动态地分配存储空间。

    具体又包括四种存储方式:

  • auto
  • static
  • register
  • extern

    不加说明默认为auto型,即动态存储,如果不赋初值,将是一个不确定的值。而将局部变量定义为static型的话,则它的值在函数内是不变的,且初值默认为0。编译时分配为静态存储区,可以被本文件中的各个函数引用。如果是多个文件的话,如果在一个文件中引用另外文件中的变量,在此文件中要用extern说明。不过如果一个全局变量定义为static的话,就只能在此一个文件中使用。register定义寄存器变量,请求编译器将这个变量保存在CPU的寄存器中,从而加快程序的运行。

特殊关键字const volatile的使用

const

    const用于声明一个只读的变量。

const unsigned char a=1;//定义a=1,编译器不允许修改a的值

    作用:保护不希望被修改的参数。

volatile

    一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。

static int i=0;
int main(void)
{
...
while (1)
{
if (i)
dosomething();
}
}
/* Interrupt service routine. */
void ISR_2(void)
{
i=1;
}

    程序的本意是希望ISR_2中断产生时,在main当中调用dosomething函数,但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远也不会被调用。如果将将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。

    一般说来,volatile用在如下的几个地方:

  • 中断服务程序中修改的供其它程序检测的变量需要加volatile;
  • 多任务环境下各任务间共享的标志应该加volatile;
  • 存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义。
四、嵌入式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_regionEX_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语言的指针具有最大的灵活性,在被访问前,可以指向任何地址,这大大方便了对硬件的操作,但同时也对开发者有了更高的要求。参考如下代码。

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增加1pi = ival;pi+=1;          //addr增加4pa = arr_val[0];pa+=1;          //addr增加4par = 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语言指针还有特殊的用法就是通过强制转换给特定的物理地址访问,通过函数指针实现回调,如下:

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

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

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

结构类型和对齐

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

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

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

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

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

六、嵌入式C语言知识点2

1 位操作

    位操作与位带操作并不相同,位操作就是对一个变量的每一位做运算,而逻辑位操作是对这个变量整体进行运算。

    下面是六种常用的操作运算符:

按位取反

void test01()
{int num = 7;printf("~num = %d\n", ~num);//-8// 0111  按位取反   1000   机器中存放的都是补码   
//补码转换原码需要分有符号数和无符号数两种
}

按位与

void test02()
{int num = 128;
//换算为八位,1换算就是00000001, 这样只要所给数字的二进制最后一位是1.那么就是奇数,否则就是偶数if ( (num & 1) == 0)    {printf("num为偶数\n");}else{printf("num为奇数\n");}
}

按位异或

void test03()
{//按位异或的意思是,两个数字相同为0,不同为1。我们可以利用按位异或实现两个数的交换num01 = 1; // 0001num02 = 4; // 0100printf("num01 ^ num02 = %d", num01 ^ num02); // 5  两个二进制按位异或之后是: 0101printf("交换前\n");printf("num01 = %d\n", num1);printf("num02 = %d\n", num2);num01 = num01 ^  num02;num02 = num01 ^  num02;num01 = num01 ^  num02;//不用临时数字实现两个变量交换printf("交换后\n");printf("num01 = %d\n", num1);printf("num02 = %d\n", num2);
}

按位或

    计算方法:
    参加运算的两个数,换算为二进制(0、1)后,进行与运算。只有当 相应位上全部为1时取1, 存在0时为0。

    printf是格式化输出函数,它可以直接打印十进制,八进制,十六进制,输出控制符分别为%d, %o, %x, 但是它不存在二进制,如果输出二进制,可以手写,但是也可以调用stdlib.h里面的itoa函数,他不是标准库里面的函数,但是大多数编译器里面都有这个函数。

#include <stdio.h>
#include <stdlib.h>int main()
{test04();    
}int test04()
{int a = 6;                  //二进制0110int b = 3;                  //二进制0011int c = a | b;              //a、b按位或,结果8,二进制111,赋值给cchar s[10];itoa(c, s, 2);printf("二进制 --> %s\n", s);//输出:二进制 -->111
}

左移运算符

void test05()
{int num = 6;printf("%d\n", num << 3);//左移三位,就是0000
}

右移运算符

void test06()
{int num = 6; //0110printf("%d\n", num >> 1); //右移一位,就是0011,输出3
}

    上面是用普通c代码举得栗子,下面我们看一下STM32中操作通常用的代码:

    (1)比如我要改变 GPIOA-> BSRRL 的状态,可以先对寄存器的值进行& 清零操作

GPIOA-> BSRRL &= 0xFF0F; //将第4位到第7位清零(注意编号是从0开始的)

    然后再与需要设置的值进行|或运算:

GPIOA-> BSRRL |= 0x0040; //将第4位到第7位设置为我们需要的数字

    (2)通过位移操作提高代码的可读性:

GPIOx->ODR = (((uint32_t)0x01) << pinpos);

    上面这行代码的意思就是,先将"0x01"这个八位十六进制转换为三十二位二进制,然后左移"pinpos"位,这个"pinpos"就是一个变量,其值就是要移动的位数。也就是将ODR寄存器的第pinpos位设置为1。

    (3)取反操作使用:

    SR寄存器的每一位代表一个状态,如果某个时刻我们想设置一个位的值为0,与此同时,其它位置都为1,简单的作法是直接给寄存器设置一个值:

TIMx->SR=0xFFF7;

    这样的作法设置第 3 位为 0,但是这样的作法可读性较差。看看库函数代码中怎样使用的:

TIMx->SR = (uint16_t)~TIM_FLAG;

    而 TIM_FLAG 是通过宏定义定义的值:

#define TIM_FLAG_Update                    ((uint16_t)0x0001) 
#define TIM_FLAG_CC1                       ((uint16_t)0x0002)

2 define宏定义

    define 是 C 语言中的预处理命令,它用于宏定义,可以提高源代码的可读性,为编程提供 方便。

    常见的格式:

#define 标识符 字符串

    标识符意思是所定义的宏名,字符串可以是常数、表达式或者格式串等,例如:

#define PLL_Q 7  //注意,这个定义语句的最后不需要加分号

3 ifdef条件编译

    在程序开发的过程中,经常会用到这种条件编译:

#ifdef PLL_Q程序段1
#else程序段2
#endif

    上面这段代码作用就是当这个标识符已经被定义过,那么就进行程序程序段1,如果没有则进行程序段2。当然,和我们设计普通的c代码是一样的,"#else"也可以没有,就是上面的代码减去"#else"和程序段2。

#ifndef PLL_Q    //意思就是如果没有定义这个标识符

4 extern变量申明

    C 语言中 extern 可以置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义(一个变量只能定义一次,而extern可以申明很多次)使用例子如下:

extern u16 USART_RX_STA;

    上面例子意思就是申明 “USART_RX_STA” 这个变量在其他文件中已经定义了,"u16"的意思是16位的。

5 结构体

    定义一个结构体的一般形式为:

struct 结构名
{成员列表
};

    成员列表由若干个成员组成,每个成员都是该结构体的一个组成部分。对每个成员也必须作类型说明,其形式:

类型说明符  成员名;//比如:int num;

    结合上面的说明,我们可以构建一个简单的结构体例子:

struct sutdent
{int num;char name[20];  //20个字节长的字符char sex;int age;float score;char addr[30]; //30个字节长的字符
}

    而如果我们想定义结构体变量,那么我们在定义这个结构体的时候直接定义,或者定义完结构体再另外定义结构体变量,比如:

struct sutdent
{int num;char name[20];  //20个字节长的字符char sex;int age;float score;char addr[30]; //30个字节长的字符
}student01,student02; //变量名表列(如果由结构体变量名,那么我们可以不写结构体名称)

    有时候我们可能需要用到结构体的嵌套,比如:

struct date
{int year, month,day;
};
struct sutdent
{int num;char name[20];  //20个字节长的字符char sex;struct date birthday; //这里就用到了结构体的嵌套int age;float score;char addr[30]; //30个字节长的字符
}student01,student02; //变量名表列(如果由结构体变量名,那么我们可以不写结构体名称)

    如果需要引用结构体里面的成员内容,可以使用下面的方式:

student01.name = 小李; 
// 结构体变量名.成员名(注意这里用的是点),这里是对这个成员的赋值

    结构指针变量说明的一般形式为:

struct 结构名 *结构指针变量名

    假如说我们想定义一个指向结构体"student"的指针变量pstu,那么我们可以使用如下代码:

struct student *pstu;

    如果我们要给一个结构体指针变量赋初值,那么我们可以使用如下的方式:

struct student
{char name[66];int num;char sex;
}stu;
pstu = &stu;

    注意上边的赋值方式,我们如果要进行赋值,那必须使用结构体变量,而不能使用结构体名,像下边这样就是错误的。

struct student
{char name[66];int num;char sex;
}stu;pstu = &student;

   这是因为结构名和结构体变量是两个不同的概念,结构名只能表示一个结构形式,编译系统并不会给它分配内存空间(就是说不会给它分配地址),而结构体变量作为一个变量,编译系统会给它分配一个内存空间来存储。

访问结构体成员的一般形式:

(*pstu).name;   //(1)(*结构指针变量).成员名;pstu->name;   //(2)结构指针变量->成员名

    结构体的知识就简单说上边这些。

6 typedef类型别名

    typedef用来为现有类型创建一个新的名字,或者称为类型别名,用来简化变量的定义(上边extern变量申明的例子中,"u16"就是对"uint16_t"类型名称的简化)。typedef在MDK中用得最多的就是定义结构体的类型别名和枚举类型。

    我们定义一个结构体GPIO:

struct _GPIO
{_IO uint32_t MODER;_IO uint32_tOTYPER;...
};

    定义这样一个结构体以后,如果我们想定义一个结构体变量比如"GPIOA",那么我们需要使用这样的代码:

struct _GPIO GPIOA;

    虽然也可以达到我们的目的,但是这样会比较麻烦,而且在MDK中会有很多地方用到,所以,我们可以使用"typedef"为其定义一个别名,这样直接通过这个别名就可以定义结构体变量,来达到我们的目的:

typedef struct
{_IO uint32_t MODER;_IO uint32_t OTYPER;
}GPIO_typedef;

    这样定义完成之后,如果我们需要定义结构体变量,那么我们只需要这样:

GPIO_typedef _GPIOA,_GPIOB;

七、嵌入式开发中的编译器

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

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;              //这句代码一直被执行

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

  这段代码的本意是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()的形参是一个数组形式,函数代码简化如下所示:

 这个给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的执行过程。

 为了更容易的理解编译器如何处理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,R50x00002E1C  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的隐蔽性。

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

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…

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前的复选框,如下图所示。

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_regionER_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_regionER_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 内存的基本概念

内存是计算机系统中除了处理器以外最重要的资源,用于存储当前正在执行的程序和数据。内存是相对于CPU来说的,CPU可以直接寻址的存储空间叫做内存,CPU需要通过驱动才能访问的叫做外存。

2 ROM&RAM&Flash

内存一般采用半导体存储单元,分为只读存储器(ROM,Read Only Memory)、随机存储器(RAM,Random Access Memory)ROM一般只能读取不能写入,掉电后其中的数据也不会丢失。RAM既可以从中读取也可以写入,但是掉电后其中的数据会丢失。内存一般指的就是RAM。

ROM在嵌入式系统中一般用于存储BootLoader以及操作系统或者程序代码或者直接当硬盘使用。近年来闪存(Flash)已经全面代替了ROM在嵌入式系统中的地位,它结合了ROM和RAM的长处,不仅具备电子可擦除可编程的特性,而且断电也不会丢失数据,同时可以快速读取数据。

3 两类内存管理方式

内存管理模块管理系统的内存资源,它是操作系统的核心模块之一。主要包括内存的初始化、分配以及释放。

从分配内存是否连续,可以分为两大类。

  • 连续内存管理

为进程分配的内存空间是连续的,但这种分配方式容易形成内存碎片(碎片是难以利用的空闲内存,通常是小内存),降低内存利用率。连续内存管理主要分为单一连续内存管理和分区式内存管理两种。

  • 非连续内存管理

将进程分散到多个不连续的内存空间中,可以减少内存碎片,内存使用率更高。如果分配的基本单位是页,则称为分页内存管理;如果基本单位是段,则称为分段内存管理。

当前的操作系统,普遍采用非连续内存管理方式。不过因为分配粒度较大,对于内存较小的嵌入式系统,一般采用连续内存管理。本文主要对嵌入式系统中常用的连续内存管理的分区式内存管理进行介绍。

4 分区式内存管理

分区式内存管理分为固定分区和动态分区。

  • 固定分区
    事先就把内存划分为若干个固定大小的区域。分区大小既可以相等也可以不等。固定分区易于实现,但是会造成分区内碎片浪费,而且分区总数固定,限制了可以并发执行的进程数量。
  • 动态分区

根据进程的实际需要,动态地给进程分配所需内存。

5 动态分区内存管理

运作机制

动态分区管理一般采用空闲链表法,即基于一个双向链表来保存空闲分区。对于初始状态,整个内存块都会被作为一个大的空闲分区加入到空闲链表中。当进程申请内存时,将会从这个空闲链表中找到一个大小满足要求的空闲分区。如果分区大于所需内存,则从该分区中拆分出需求大小的内存交给进程,并将此拆分出的内存从空闲链表中移除,剩下的内存仍然是一个挂在空闲链表中的空闲分区。

数据结构

空闲链表法有多种数据结构实现,这里介绍一种较为简单的数据结构。每个空闲分区的数据结构中包含分区的大小,以及指向前一个分区和后一个分区的指针,这样就能将各个空闲分区链接成一个双向链表。

内存分配算法

  • First Fit(首次适应算法)

First Fit要求空闲分区链表以地址从小到大的顺序链接。分配内存时,从链表的第一个空闲分区开始查找,将最先能够满足要求的空闲分区分配给进程。

  • Next Fit(循环首次适应算法)

Next Fit由First Fit算法演变而来。分配内存时,从上一次刚分配过的空闲分区的下一个开始查找,直至找到能满足要求的空闲分区。查找时会采用循环查找的方式,即如果直到链表最后一个空闲分区都不能满足要求,则返回到第一个空闲分区开始查找。

  • Best Fit(最佳适应算法)

从所有空闲分区中找出能满足要求的、且大小最小的空闲分区。为了加快查找速度,Best Fit算法会把所有空闲分区按其容量从小到大的顺序链接起来,这样第一次找到的满足大小要求的内存必然是最小的空闲分区。

  • Worst Fit(最坏适应算法)

从所有空闲分区中找出能满足要求的、且大小最大的空闲分区。Worst Fit算法按其容量从大到小的顺序链接所有空闲分区。

  • Two LevelSegregated Fit(TLSF)

使用两层链表来管理空闲内存,将空闲分区大小进行分类,每一类用一个空闲链表表示,其中的空闲内存大小都在某个特定值或者某个范围内。这样存在多个空闲链表,所以又用一个索引链表来管理这些空闲链表,该表的每一项都对应一种空闲链表,并记录该类空闲链表的表头指针。

图中,第一层链表将空闲内存块的大小根据2的幂进行分类。第二层链表是具体的每一类空闲内存块按照一定的范围进行线性分段。比如25这一类,以23即8分为4个内存区间【25,25+8),【25+8,25+16),【25+16,25+24),【25+24,25+32);216这一类,以214分为4个小区间【216,216+214),【216+214,216+2*214),【216+2*214,216+3*214),【216+3*214,216+4*214)。同时为了快速检索到空闲块,每一层链表都有一个bitmap用于标记对应的链表中是否有空闲块,比如第一层bitmap后3位010,表示25这一类内存区间有空闲块。对应的第二层bitmap为0100表示【25+16,25+24)这个区间有空闲块,即下面的52Byte。

  • Buddysystems(伙伴算法)

Segregated Fit算法的变种,具有更好的内存拆分和回收合并效率。伙伴算法有很多种类,比如BinaryBuddies,Fibonacci Buddies等。Binary Buddies是最简单也是最流行的一种,将所有空闲分区根据分区的大小进行分类,每一类都是具有相同大小的空闲分区的集合,使用一个空闲双向链表表示。BinaryBuddies中所有的内存分区都是2的幂次方。

因为无论是已分配的或是空闲的分区,其大小均为 2 的幂次方,即使进程申请的内存小于分配给它的内存块,多余的内存也不会再拆分出来给其他进程使用,这样就容易造成内部碎片。

当进程申请一块大小为n的内存时的分配步骤为:

1、计算一个i值,使得2i-1<n≤2i

2、在空闲分区大小为2i的空闲链表中查找

3、如果找到空闲块,则分配给进程

4、如果2i的空闲分区已经耗尽,则在分区大小为2i+1的空闲链表中查找

5、如果存在2i+1的空闲分区,则将此空闲块分为相等的两个分区,这两分区就是一对伙伴,其中一块分配给进程,另一块挂到分区大小为2i的空闲链表中

6、如果2i+1的空闲分区还是不存在,则继续查找大小为2i+2的空闲分区。如果找到,需要进行两次拆分。第一次拆分为两块大小为2i+1的分区,一块分区挂到大小为2i+1的空闲链表中,另一块分区继续拆分为两块大小为2i的空闲分区,一块分配给进程,另一块挂到大小为2i的空闲链表中

7、如果2i+2的空闲分区也找不到,则继续查找2i+3,以此类推

在内存回收时,如果待回收的内存块与空闲链表中的一块内存互为伙伴,则将它们合并为一块更大的内存块,如果合并后的内存块在空闲链表中还有伙伴,则继续合并到不能合并为止,并将合并后的内存块挂到对应的空闲链表中。

下面的表格对上面6种算法的优缺点进行了比较:

九、详解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的启动文件开始,有一段汇编代码是分配堆栈大小的。

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

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大小信息:

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

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

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

十、STM32F10x中,一些专业术语

GPIO(General Purpose Input Output)是通用输入/输出端口;每个GPIO端口可通过软件分别配置成输入或输出;输出又分为推挽式(Push-Pull)和开漏式(Open-Drain)。

    USART(Universal Synchronous/Asynchronous Receiver/Transmitter)是通用同步/异步串行接收/发送器,支持全双工操作;可设置波特率,数据位,停止位,校验位等。
    PWM(Pulse Width Modulation)是脉冲宽度调制,简称脉宽调制,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。简单一点,就是对脉冲宽度的控制。

    OLED(Organic Light-Emitting Diode)即有机发光二极管;具备自发光,不需背光源、对比度高、厚度薄、视角广、反应速度快、可用于挠曲性面板、使用温度范围广、构造及制程较简单等优异之特性,被认为是下一代的平面显示器新兴应用技术。LCD都需要背光,而OLED不需要,因为它是自发光的,因此,OLED效果要来得好一些。OLED的尺寸难以大型化,但是分辨率确可以做到很高。

    TFT-LCD(Thin Film Transistor-Liquid Crystal Display)即薄膜晶体管液晶显示器;它在液晶显示屏的每一个象素上都设置有一个薄膜晶体管(TFT),可有效地克服非选通时的串扰,使显示液晶屏的静态特性与扫描线数无关,因此大大提高了图像质量。TFT-LCD也被叫做真彩液晶显示器。

    RTC(Real Time Clock)即实时时钟,是一个独立的定时器。RTC模块拥有一组连续计数的计数器,在相应软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统当前的时间和日期。

    ADC(Analog-to-Digital Converter)指模拟/数字转换器。是指将连续变量的模拟信号转换为离散的数字信号的器件。真实世界的模拟信号,例如温度、压力、声音或者图像等,需要转换成更容易储存、处理和发射的数字形式。模/数转换器可以实现这个功能,在各种不同的产品中都可以找到它的身影

    DMA(Direct Memory Access)即直接存储器访问。DMA传输方式无需 CPU直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为RAM与I/O设备开辟一条直接传送数据的通路,能使 CPU的效率大为提高。

    I2C(Inter-Integrated Circuit)即集成电路总线,它用于连接微控制器及其外围设备。它是由数据线 SDA 和时钟 SCL 构成的串行总线,可发送和接收数据。

    SPI(Serial Peripheral Interface)是串行外围设备接口。SPI接口主要应用在FLASH,EEPROM(Electrically Erasable Programmable Read-Only Memory),RTC(Real Time Clock),ADC(Analog to Digital Converter),还有数字信号处理器和数字信号解码器之间。SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB(Printed Circuit Board)的布局上节省空间,提供方便,正是出于这种简单易用的特性,现在越来越多的芯片集成了这种通信协议。

    PS/2是电脑上常见的接口之一,用于鼠标、键盘等设备。,PS/2接口的鼠标为绿色,键盘为紫色。PS/2接口是输入装置接口,而不是传输接口。所以PS2口根本没有传输速率的概念,只有扫描速率。在Windows环境下,ps/2鼠标的采样率默认为60次/秒,USB鼠标的采样率为120次/秒。较高的采样率理论上可以提高鼠标的移动精度。

    USB(Universal Serial BUS)即通用串行总线;它是一个外部总线标准,用于规范电脑与外部设备的连接和通讯。它是应用在PC领域的接口技术。USB接口支持设备的即插即用和热插拔功能。

    SD(Secure Digital Memory Card)即安全数码存储卡,是一种基于半导体快闪记忆器的新一代记忆设备,它被广泛地于便携式装置上使用,例如数码相机、多媒体播放器等。

十一、STM32启动过程

1 概述

说明

    每一款芯片的启动文件都值得去研究,因为它可是你的程序跑的最初一段路,不可以不知道。通过了解启动文件,我们可以体会到处理器的架构、指令集、中断向量安排等内容,是非常值得玩味的。

    STM32作为一款高端 Cortex-M3系列单片机,有必要了解它的启动文件。打好基础,为以后优化程序,写出高质量的代码最准备。

    本文以一个实际测试代码--START_TEST为例进行阐述。

整体过程

    STM32整个启动过程是指从上电开始,一直到运行到 main函数之间的这段过程,步骤为(以使用微库为例):

①上电后硬件设置SP、PC

②设置系统时钟

③软件设置SP

④加载.data、.bss,并初始化栈区

⑤跳转到C文件的main函数

代码

    启动过程涉及的文件不仅包含 startup_stm32f10x_hd.s,还涉及到了MDK自带的连接库文件 entry.o、entry2.o、entry5.o、entry7.o等(从生成的 map文件可以看出来)。

2 程序在Flash上的存储结构

    在真正讲解启动过程之前,先要讲解程序下载到 Flash上的结构和程序运行时(执行到main函数)时的SRAM数据结构。程序在用户Flash上的结构如下图所示。下图是通过阅读hex文件和在MDK下调试综合提炼出来的。

  上图中:

  • MSP初始值由编译器生成,是主堆栈的初始值。
  • 初始化数据段是.data
  • 未初始化数据段是.bss

    .data和.bss是在__main里进行初始化的,对于ARM Compiler,__main主要执行以下函数:

  其中__scatterload会对.data和.bss进行初始化。

加载数据段和初始化栈的参数

    加载数据段和初始化栈的参数分别有4个,这里只讲解加载数据段的参数,至于初始化栈的参数类似。

0x0800033c  Flash上的数据段(初始化数据段和未初始化数据段)起始地址
0x20000000  加载到SRAM上的目的地址
0x0000000c  数据段的总大小
0x080002f4  调用函数_scatterload_copy

    需要说明的是初始化栈的函数-- 0x08000304与加载数据段的函数不一样,为 _scatterload_zeroinit,它的目的就是将栈空间清零。

3 数据在SRAM上的结构

    程序运行时(执行到main函数)时的SRAM数据结构

4 详细过程分析

    有了以上的基础,现在详细分析启动过程

上电后硬件设置SP、PC

    刚上电复位后,硬件会自动根据向量表偏移地址找到向量表,向量表偏移地址的定义如下:

    调试现象如下:

    看看我们的向量表内容(通过J-Flash打开hex文件)

  硬件这时自动从0x0800 0000位置处读取数据赋给栈指针SP,然后自动从0x0800 0004位置处读取数据赋给PC,完成复位,结果为:

SP = 0x02000810
PC = 0x08000145

设置系统时钟

    上一步中令 PC=0x08000145的地址没有对齐,硬件自动对齐到 0x08000144,执行 SystemInit函数初始化系统时钟。

软件设置SP

LDR   R0,=__mainBX   R0

    执行上两条之类,跳转到 __main程序段运行,注意不是main函数, ___main的地址是0x0800 0130。

    可以看到指令LDR.W sp,[pc,#12],结果SP=0x2000 0810。

加载.data、.bss,并初始化栈区

BL.W     __scatterload_rt2

    进入 __scatterload_rt2代码段。

__scatterload_rt2:
0x080001684C06      LDR      r4,[pc,#24]  ; @0x08000184
0x0800016A4D07      LDR      r5,[pc,#28]  ; @0x08000188
0x0800016C E006      B        0x0800017C
0x0800016E68E0      LDR      r0,[r4,#0x0C]
0x08000170 F0400301  ORR      r3,r0,#0x01
0x08000174 E8940007  LDM      r4,{r0-r2}
0x080001784798      BLX      r3
0x0800017A3410      ADDS     r4,r4,#0x10
0x0800017C42AC      CMP      r4,r5
0x0800017E D3F6      BCC      0x0800016E
0x08000180 F7FFFFDA  BL.W     _main_init (0x08000138)

    这段代码是个循环 (BCC0x0800016e),实际运行时候循环了两次。第一次运行的时候,读取“加载数据段的函数 (_scatterload_copy)”的地址并跳转到该函数处运行(注意加载已初始化数据段和未初始化数据段用的是同一个函数);第二次运行的时候,读取“初始化栈的函数 (_scatterload_zeroinit)”的地址并跳转到该函数处运行。相应的代码如下:

0x0800016E68E0      LDR      r0,[r4,#0x0C]
0x08000170 F0400301  ORR      r3,r0,#0x01
0x08000174
0x080001784798      BLX      r3

    当然执行这两个函数的时候,还需要传入参数。至于参数,我们在“加载数据段和初始化栈的参数”环节已经阐述过了。当这两个函数都执行完后,结果就是“数据在SRAM上的结构”所展示的图。最后,也把事实加载和初始化的两个函数代码奉上如下:

__scatterload_copy:
0x080002F4 E002      B        0x080002FC
0x080002F6 C808      LDM      r0!,{r3}
0x080002F81F12      SUBS     r2,r2,#4
0x080002FA C108      STM      r1!,{r3}
0x080002FC2A00      CMP      r2,#0x00
0x080002FE D1FA      BNE      0x080002F6
0x080003004770      BX       lr
__scatterload_null:
0x080003024770      BX       lr
__scatterload_zeroinit:
0x080003042000      MOVS     r0,#0x00
0x08000306 E001      B        0x0800030C
0x08000308 C101      STM      r1!,{r0}
0x0800030A1F12      SUBS     r2,r2,#4
0x0800030C2A00      CMP      r2,#0x00
0x0800030E D1FB      BNE      0x08000308
0x080003104770      BX       lr

跳转到C文件的main函数

_main_init:
0x080001384800      LDR      r0,[pc,#0]  ; @0x0800013C
0x0800013A4700      BX       r0

5 异常向量与中断向量表

; VectorTableMapped to Address0 at Reset
AREA    RESET, DATA, READONLY
EXPORT  __Vectors
EXPORT  __Vectors_End
EXPORT  __Vectors_Size__Vectors       DCD     __initial_sp               ; Top of Stack
DCD     Reset_Handler; ResetHandler
DCD     NMI_Handler                ; NMI Handler
DCD     HardFault_Handler; HardFaultHandler
DCD     MemManage_Handler; MPU FaultHandler
DCD     BusFault_Handler; BusFaultHandler
DCD     UsageFault_Handler; UsageFaultHandler
DCD     0; Reserved
DCD     0; Reserved
DCD     0; Reserved
DCD     0; Reserved
DCD     SVC_Handler                ; SVCallHandler
DCD     DebugMon_Handler; DebugMonitorHandler
DCD     0; Reserved
DCD     PendSV_Handler; PendSVHandler
DCD     SysTick_Handler; SysTickHandler; ExternalInterrupts
DCD     WWDG_IRQHandler            ; WindowWatchdog
DCD     PVD_IRQHandler             ; PVD through EXTI Line detect
DCD     TAMPER_IRQHandler          ; Tamper
DCD     RTC_IRQHandler             ; RTC
DCD     FLASH_IRQHandler           ; Flash
DCD     RCC_IRQHandler             ; RCC
DCD     EXTI0_IRQHandler           ; EXTI Line0
DCD     EXTI1_IRQHandler           ; EXTI Line1
DCD     EXTI2_IRQHandler           ; EXTI Line2
DCD     EXTI3_IRQHandler           ; EXTI Line3
DCD     EXTI4_IRQHandler           ; EXTI Line4
DCD     DMA1_Channel1_IRQHandler   ; DMA1 Channel1
DCD     DMA1_Channel2_IRQHandler   ; DMA1 Channel2
DCD     DMA1_Channel3_IRQHandler   ; DMA1 Channel3
DCD     DMA1_Channel4_IRQHandler   ; DMA1 Channel4
DCD     DMA1_Channel5_IRQHandler   ; DMA1 Channel5
DCD     DMA1_Channel6_IRQHandler   ; DMA1 Channel6
DCD     DMA1_Channel7_IRQHandler   ; DMA1 Channel7
DCD     ADC1_2_IRQHandler          ; ADC1 & ADC2
DCD     USB_HP_CAN1_TX_IRQHandler  ; USB HighPriority or CAN1 TX
DCD     USB_LP_CAN1_RX0_IRQHandler ; USB LowPriority or CAN1 RX0
DCD     CAN1_RX1_IRQHandler        ; CAN1 RX1
DCD     CAN1_SCE_IRQHandler        ; CAN1 SCE
DCD     EXTI9_5_IRQHandler         ; EXTI Line9..5
DCD     TIM1_BRK_IRQHandler        ; TIM1 Break
DCD     TIM1_UP_IRQHandler         ; TIM1 Update
DCD     TIM1_TRG_COM_IRQHandler    ; TIM1 Trigger and Commutation
DCD     TIM1_CC_IRQHandler         ; TIM1 CaptureCompare
DCD     TIM2_IRQHandler            ; TIM2
DCD     TIM3_IRQHandler            ; TIM3
DCD     TIM4_IRQHandler            ; TIM4
DCD     I2C1_EV_IRQHandler         ; I2C1 Event
DCD     I2C1_ER_IRQHandler         ; I2C1 Error
DCD     I2C2_EV_IRQHandler         ; I2C2 Event
DCD     I2C2_ER_IRQHandler         ; I2C2 Error
DCD     SPI1_IRQHandler            ; SPI1
DCD     SPI2_IRQHandler            ; SPI2
DCD     USART1_IRQHandler          ; USART1
DCD     USART2_IRQHandler          ; USART2
DCD     USART3_IRQHandler          ; USART3
DCD     EXTI15_10_IRQHandler       ; EXTI Line15..10
DCD     RTCAlarm_IRQHandler; RTC Alarm through EXTI Line
DCD     USBWakeUp_IRQHandler; USB Wakeup from suspend
DCD     TIM8_BRK_IRQHandler        ; TIM8 Break
DCD     TIM8_UP_IRQHandler         ; TIM8 Update
DCD     TIM8_TRG_COM_IRQHandler    ; TIM8 Trigger and Commutation
DCD     TIM8_CC_IRQHandler         ; TIM8 CaptureCompare
DCD     ADC3_IRQHandler            ; ADC3
DCD     FSMC_IRQHandler            ; FSMC
DCD     SDIO_IRQHandler            ; SDIO
DCD     TIM5_IRQHandler            ; TIM5
DCD     SPI3_IRQHandler            ; SPI3
DCD     UART4_IRQHandler           ; UART4
DCD     UART5_IRQHandler           ; UART5
DCD     TIM6_IRQHandler            ; TIM6
DCD     TIM7_IRQHandler            ; TIM7
DCD     DMA2_Channel1_IRQHandler   ; DMA2 Channel1
DCD     DMA2_Channel2_IRQHandler   ; DMA2 Channel2
DCD     DMA2_Channel3_IRQHandler   ; DMA2 Channel3
DCD     DMA2_Channel4_5_IRQHandler ; DMA2 Channel4& Channel5
__Vectors_End

  这段代码就是定义异常向量表,在之前有一个“J-Flash打开hex文件”的图片跟这个表格是一一对应的。编译器根据我们定义的函数 Reset_Handler、NMI_Handler等,在连接程序阶段将这个向量表填入这些函数的地址。

    startup_stm32f10x_hd.s内容:

NMI_Handler     PROC
EXPORT  NMI_Handler                [WEAK]
B       .
ENDP

  stm32f10x_it.c中内容:

void NMI_Handler(void)
{
}

  在启动汇编文件中已经定义了函数 NMI_Handler,但是使用了“弱”,它允许我们再重新定义一个 NMI_Handler函数,程序在编译的时候会将汇编文件中的弱函数“覆盖掉”--两个函数的代码在连接后都存在,只是在中断向量表中的地址填入的是我们重新定义函数的地址。

6 使用微库与不使用微库的区别

 使用微库就意味着我们不想使用MDK提供的库函数,而想用自己定义的库函数,比如说printf函数。那么这一点是怎样实现的呢?我们以printf函数为例进行说明。

不使用微库而使用系统库

    在连接程序时,肯定会把系统中包含printf函数的库拿来调用参与连接,即代码段有系统库的参与。

    在启动过程中,不使用微库而使用系统库在初始化栈的时候,还需要初始化堆(猜测系统库需要用到堆),而使用微库则是不需要的。

IF      :DEF:__MICROLIBEXPORT  __initial_sp
EXPORT  __heap_base
EXPORT  __heap_limitELSEIMPORT  __use_two_region_memory
EXPORT  __user_initial_stackheap__user_initial_stackheapLDR     R0, =  Heap_Mem
LDR     R1, =(Stack_Mem+ Stack_Size)
LDR     R2, = (Heap_Mem+  Heap_Size)
LDR     R3, = Stack_Mem
BX      LRALIGNENDIF

    另外,在执行 __main函数的过程中,不仅需要完成“使用微库”情况下的所有工作,额外的工作还需要进行库的初始化,才能使用系统库(这一部分我还没有深入探讨)。附上 __main函数的内容:       

__main:
0x08000130 F000F802  BL.W     __scatterload_rt2_thumb_only (0x08000138)
0x08000134 F000F83C  BL.W     __rt_entry_sh (0x080001B0)
__scatterload_rt2_thumb_only:
0x08000138 A00A      ADR      r0,{pc}+4; @0x08000164
0x0800013A E8900C00  LDM      r0,{r10-r11}
0x0800013E4482      ADD      r10,r10,r0
0x080001404483      ADD      r11,r11,r0
0x08000142 F1AA0701  SUB      r7,r10,#0x01
__scatterload_null:
0x0800014645DA      CMP      r10,r11
0x08000148 D101      BNE      0x0800014E
0x0800014A F000F831  BL.W     __rt_entry_sh (0x080001B0)
0x0800014E F2AF0E09  ADR.W    lr,{pc}-0x07; @0x08000147
0x08000152 E8BA000F  LDM      r10!,{r0-r3}
0x08000156 F0130F01  TST      r3,#0x01
0x0800015A BF18      IT       NE
0x0800015C1AFB      SUBNE    r3,r7,r3
0x0800015E F0430301  ORR      r3,r3,#0x01
0x080001624718      BX       r3
0x080001640298      LSLS     r0,r3,#10
0x080001660000      MOVS     r0,r0
0x0800016802B8      LSLS     r0,r7,#10
0x0800016A0000      MOVS     r0,r0
__scatterload_copy:
0x0800016C3A10      SUBS     r2,r2,#0x10
0x0800016E BF24      ITT      CS
0x08000170 C878      LDMCS    r0!,{r3-r6}
0x08000172 C178      STMCS    r1!,{r3-r6}
0x08000174 D8FA      BHI      __scatterload_copy (0x0800016C)
0x080001760752      LSLS     r2,r2,#29
0x08000178 BF24      ITT      CS
0x0800017A C830      LDMCS    r0!,{r4-r5}
0x0800017C C130      STMCS    r1!,{r4-r5}
0x0800017E BF44      ITT      MI
0x080001806804      LDRMI    r4,[r0,#0x00]
0x08000182600C      STRMI    r4,[r1,#0x00]
0x080001844770      BX       lr
0x080001860000      MOVS     r0,r0
__scatterload_zeroinit:
0x080001882300      MOVS     r3,#0x00
0x0800018A2400      MOVS     r4,#0x00
0x0800018C2500      MOVS     r5,#0x00
0x0800018E2600      MOVS     r6,#0x00
0x080001903A10      SUBS     r2,r2,#0x10
0x08000192 BF28      IT       CS
0x08000194 C178      STMCS    r1!,{r3-r6}
0x08000196 D8FB      BHI      0x08000190
0x080001980752      LSLS     r2,r2,#29
0x0800019A BF28      IT       CS
0x0800019C C130      STMCS    r1!,{r4-r5}
0x0800019E BF48      IT       MI
0x080001A0600B      STRMI    r3,[r1,#0x00]
0x080001A24770      BX       lr
__rt_lib_init:
0x080001A4 B51F      PUSH     {r0-r4,lr}
0x080001A6 F3AF8000  NOP.W
__rt_lib_init_user_alloc_1:
0x080001AA BD1F      POP      {r0-r4,pc}
__rt_lib_shutdown:
0x080001AC B510      PUSH     {r4,lr}
__rt_lib_shutdown_user_alloc_1:
0x080001AE BD10      POP      {r4,pc}
__rt_entry_sh:
0x080001B0 F000F82F  BL.W     __user_setup_stackheap (0x08000212)
0x080001B44611      MOV      r1,r2
__rt_entry_postsh_1:
0x080001B6 F7FFFFF5  BL.W     __rt_lib_init (0x080001A4)
__rt_entry_postli_1:
0x080001BA F000F919  BL.W     main (0x080003F0)

使用微库而不使用系统库

    在程序连接时,不会把包含printf函数的库连接到终极目标文件中,而使用我们定义的库。

    启动时需要完成的工作就是之前论述的步骤1、2、3、4、5,相比使用系统库,启动过程步骤更少。

十二、如何中断单片机的中断?

如果外部中断来的频率足够快,上一个中断没有处理完成,新来的中断该如何处理?

    中断一般是由硬件(例如外设、外部引脚)产生,当某种内部或外部事件发生时,MCU的中断系统将迫使 CPU 暂停正在执行的程序,转而去进行中断事件的处理,中断处理完毕后,又返回被中断的程序处,继续执行下去,所有的Cortex-M 内核系统都有一个用于中断处理的组件NVIC,主要负责处理中断,还处理其他需要服务的事件。嵌套向量式中断控制器(NVIC: Nested Vectored Interrupt Controller)集成在Cortex-M0处理器里,它与处理器内核紧密相连,并且提供了中断控制功能以及对系统异常的支持。

    处理器中的NVIC能够处理多个可屏蔽中断通道和可编程优先级,中断输入请求可以是电平触发,也可以是最小的一个时钟周期的脉冲信号。每一个外部中断线都可以独立的使能、清除或挂起,并且挂起状态也可以手动地设置和清除。

    主程序正在执行,当遇到中断请求(Interrupt Request)时,暂停主程序的执行转而去执行中断服务例程(Interrupt Service Routine,ISR),称为响应,中断服务例程执行完毕后返回到主程序断点处并继续执行主程序。多个中断是可以进行嵌套的。正在执行的较低优先级中断可以被较高优先级的中断所打断,在执行完高级中断后返回到低级中断里继续执行,采用“咬尾中断”机制。

 内核中断(异常管理和休眠模式等),其中断优先级则由SCB寄存器来管理,IRQ的中断优先级是由NVIC来管理。

    NVIC的寄存器经过了存储器映射,其寄存器的起始地址为0xE000E100,对其访问必须是每次32bit。

    SCB寄存器的起始地址:0xE000ED00,也是每次32bit访问,SCB寄存器主要包含SysTick操作、异常管理和休眠模式控制。

    NVIC具有以下特性:

  • 灵活的中断管理:使能\清除、优先级配置
  • 硬件嵌套中断支持
  • 向量化的异常入口
  • 中断屏蔽

1 中断使能和清除使能

    ARM将处理器的中断使能设置和清除设置寄存器分在两个不同的地址,这种设计主要有如下优势:一方面这种方式减少了使能中断所需要的步骤,使能一个中断NVIC只需要访问一次,同时也减少了程序代码并且降低了执行时间,另一方面当多个应用程序进程同时访问寄存器或者在读写操作寄存器时有操作其他的中断使能位,这样就有可能导致寄存器丢失,设置和清除分成两个寄存器能够有效防止控制信号丢失。

 因此我可以独立的操作每一个中断的使能和清除设置。

1.1 C代码

*(volatile unsigned long) (0xE000E100) = 0x4 ; //使能#2中断
*(volatile unsigned long) (0xE000E180) = 0x4 ; //清除#2中断

1.2 汇编代码

__asm void Interrupt_Enable()
{LDR R0, =0xE000E100  ;  //ISER寄存器的地址MOVS R1, #04         ;  //设置#2中断STR R1, [R0]         ;  //使能中断#2
}__asm void Interrupt_Disable()
{LDR R0, =0xE000E180  ;  //ICER寄存器的地址MOVS R1, #04         ;  //设置#2中断STR R1, [R0]         ;  //使能中断#2
}

1.3 CMSIS标准设备驱动函数

//使能中断#IRQn
__STATIC_INLINE void __NVIC_EnableIRQ(IRQn_Type IRQn) 
{if ((int32_t)(IRQn) >= 0) {NVIC->ISER[0U] = (uint32_t)(1UL << (((uint32_t)(int32_t)IRQn) & 0x1FUL));}
}
//清除中断#IRQn
__STATIC_INLINE void __NVIC_DisableIRQ(IRQn_Type IRQn) 
{if ((int32_t)(IRQn) >= 0) {NVIC->ICER[0U] = (uint32_t)(1UL << (((uint32_t)(int32_t)IRQn) & 0x1FUL));__DSB();__ISB();}
}
//读取使能中断#IRQn
__STATIC_INLINE uint32_t __NVIC_GetEnableIRQ(IRQn_Type IRQn)
{if ((int32_t)(IRQn) >= 0) {return((uint32_t)(((NVIC->ISER[0U] & (1UL << (((uint32_t)(int32_t)IRQn) & 0x1FUL))) != 0UL) ? 1UL : 0UL));}else {return(0U);}
}

2 中断挂起和清除挂起

    如果一个中断发生了,却无法立即处理,这个中断请求将会被挂起。挂起状态保存在一个寄存器中,如果处理器的当前优先级还没有降低到可以处理挂起的请求,并且没有手动清除挂起状态,该状态将会一直保持。

    可以通过操作中断设置挂起和中断清除挂起两个独立的寄存器来访问或者修改中断挂起状态,中断挂起寄存器也是通过两个地址来实现设置和清除相关位。这使得每一个位都可以独立修改,并且无需担心在两个应用程序进程竞争访问时出现的数据丢失。

  中断挂起状态寄存器允许使用软件来触发中断。如果中断已经使能并且没有被屏蔽掉,当前还没有更高优先级的中断在运行,这时中断的服务程序就会立即得以执行。

2.1 C代码

*(volatile unsigned long)(0xE000E100) = 0x4 ; //使能中断#2
*(volatile unsigned long)(0xE000E200) = 0x4 ; //挂起中断#2
*(volatile unsigned long)(0xE000E280) = 0x4 ; //清除中断#2的挂起状态

2.2 汇编代码

__asm void Interrupt_Set_Pending()
{LDR R0, =0xE000E100   ;  //设置使能中断寄存器地址MOVS R1, #0x4         ;  //中断#2STR R1, [R0]          ;  //使能#2中断LDR R0, =0xE000E200   ; //设置挂起中断寄存器地址MOVS R1, #0x4         ;  //中断#2STR R1, [R0]          ;  //挂起#2中断
}__asm void Interrupt_Clear_Pending()
{LDR R0, =0xE000E100   ;  //设置使能中断寄存器地址MOVS R1, #0x4         ;  //中断#2STR R1, [R0]          ;  //使能#2中断LDR R0, =0xE000E280   ; //设置清除中断挂起寄存器地址MOVS R1, #0x4         ;  //中断#2STR R1, [R0]          ;  //清除#2的挂起状态
}

2.3 CMSIS标准设备驱动函数

//设置一个中断挂起
__STATIC_INLINE void __NVIC_SetPendingIRQ(IRQn_Type IRQn) 
{if ((int32_t)(IRQn) >= 0) {NVIC->ISPR[0U] = (uint32_t)(1UL << (((uint32_t)(int32_t)IRQn) & 0x1FUL));}
}//清除中断挂起
__STATIC_INLINE void __NVIC_ClearPendingIRQ(IRQn_Type IRQn) 
{if ((int32_t)(IRQn) >= 0) {NVIC->ICPR[0U] = (uint32_t)(1UL << (((uint32_t)(int32_t)IRQn) & 0x1FUL));}
}//读取中断挂起状态
__STATIC_INLINE uint32_t __NVIC_GetPendingIRQ(IRQn_Type IRQn) 
{if ((int32_t)(IRQn) >= 0) {return((uint32_t)(((NVIC->ISPR[0U] & (1UL << (((uint32_t)(int32_t)IRQn) & 0x1FUL))) != 0UL) ? 1UL : 0UL));}else {return(0U);}
}

    NVIC属于处理器内核部分,因此在MM32 MCU芯片的用户手册中只有简单的提及,没有重点讲述,需要深入了解相关寄存器和功能需要参考《Cortex-M0技术参考手册》。

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

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

功能:

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

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

#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数据类型。

#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 创建文件并填充固定数据

功能:

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

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <ctype.h>//#define FILL_DATA_VALUE  0xff
#define FILL_DATA_VALUE  0x30 //char 0int 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 批量处理图片

功能:

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

#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.jpg2 directories, 54 files
biao@ubuntu:~/test/photo$

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

5 IO控制小程序

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

#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  0x2700000int 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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/78441.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Multisim使用教程详尽版--(2025最新版)

一、Multisim14前言 1.1、主流电路仿真软件 1. Multisim&#xff1a;NI开发的SPICE标准仿真工具&#xff0c;支持模拟/数字电路混合仿真&#xff0c;内置丰富的元件库和虚拟仪器&#xff08;示波器、频谱仪等&#xff09;&#xff0c;适合教学和竞赛设计。官网&#xff1a;艾…

分布式理论和事务

微服务和分布式 微服务 是一种软件架构风格&#xff0c;它将应用程序拆分成一系列小型、独立的服务&#xff0c;每个服务专注于单一功能&#xff0c;彼此通过轻量级通信机制&#xff08;如 API&#xff09;进行交互。微服务通常是松耦合的&#xff0c;可以独立开发、部署和扩展…

JAVA:红黑树应用的技术指南

&#x1f333; 1、简述 红黑树是一种自平衡二叉查找树&#xff08;Self-Balancing Binary Search Tree&#xff09;&#xff0c;被广泛应用于操作系统调度、Java集合、数据库索引等核心模块中。本文将从 基本原理 入手&#xff0c;结合 实际应用场景与代码实例&#xff0c;带你…

【Pandas】pandas DataFrame rfloordiv

Pandas2.2 DataFrame Binary operator functions 方法描述DataFrame.add(other)用于执行 DataFrame 与另一个对象&#xff08;如 DataFrame、Series 或标量&#xff09;的逐元素加法操作DataFrame.add(other[, axis, level, fill_value])用于执行 DataFrame 与另一个对象&…

【数据可视化-26】基于人口统计与社会经济数据的多维度可视化分析

🧑 博主简介:曾任某智慧城市类企业算法总监,目前在美国市场的物流公司从事高级算法工程师一职,深耕人工智能领域,精通python数据挖掘、可视化、机器学习等,发表过AI相关的专利并多次在AI类比赛中获奖。CSDN人工智能领域的优质创作者,提供AI相关的技术咨询、项目开发和个…

WinForm真入门(18)——DateTimePicker‌控件解析

一、基本概念‌ ‌DateTimePicker‌ 是 Windows 窗体中用于选择日期和时间的控件&#xff0c;支持以下交互方式&#xff1a; 通过下拉日历选择日期通过上下按钮调整时间直接输入日期或时间 适用于需要规范日期格式、限制日期范围或快速输入的场景&#xff08;如预约系统、数据…

AVFormatContext 再分析

说明 &#xff1a;将 avfromatContext 的变量依次打印分析&#xff0c;根据ffmpeg 给的说明&#xff0c;猜测&#xff0c;结合网上的文章字节写测试代码分析。 从常用到不常用依次分析 1. unsigned int nb_streams; 代表 avfromatContext 中 AVStream **streams 的个数 /** …

计算机网络-运输层(1)

计算机网络-运输层(1) 文章目录 计算机网络-运输层(1)5.1 运输层概述5.2 运输层端口号、复用与分用端口号基本概念端口号特性端口号分类重要说明 5.3 UDP与TCP协议对比关键区别说明 5.1 运输层概述 计算机网络体系结构中的物理层、数据链路层以及网络层共同解决了主机通过异构…

2025 FIC wp

这次比赛计算机和手机大部分题目都比较常规 第一和第四部分有点让人摸不着头脑 比赛的时候第一部分有四个题没出 第四部分基本都没怎么出 现在复盘一下 把我当时做题的心得和获取的新知识记录一下 互联网取证的部分就先学习一下别的师傅 检材 链接&#xff1a;https://pan.bai…

【大数据技术-联邦集群RBF】DFSRouter日志一直打印修改Membership为EXPIRED状态的日志分析

生产环境遇到下面报错 2025-04-23 17:44:15,780 INFO store.CachedRecordStore (CachedRecordStore.java:overrideExpiredRecords(192)) - Override State Store record MembershipState: router1:8888->hh-fed-sub25:nn2:nn2:8020-EXPIRED 2025-04-23 17:44:15,781 INFO …

【HarmonyOS 5】鸿蒙检测系统完整性

【HarmonyOS 5】鸿蒙检测系统完整性 一、前言 从现实安全威胁来看&#xff0c;设备系统完整性风险已影响至移动应用的各个场景。不少用户因使用越狱设备&#xff08;Jailbreak&#xff09;或非真实设备&#xff08;Emulator&#xff09;&#xff0c;导致应用安全防护机制失效…

学习spark-streaming收获

1.流处理的核心概念 •实时 vs微批处理&#xff1a;理解了 Spark Streaming 的微批处理&#xff08;Micro-Batch&#xff09;模型&#xff0c;将流数据切分为小批次&#xff08;如1秒间隔&#xff09;进行处理&#xff0c;与真正的流处理&#xff08;如Flink&#xff09;的区…

Redis一些小记录

Redis一些小记录 SpringData Redis&#xff1a;RedisTemplate配置与数据操作 操作String类型数据 String是Redis中最基本的数据类型&#xff0c;可以存储字符串、整数或浮点数。RedisTemplate提供了ValueOperations接口来操作String类型的数据&#xff0c;支持设置值、获取值、…

5G融合消息PaaS项目深度解析 - Java架构师面试实战

5G融合消息PaaS项目深度解析 - Java架构师面试实战 场景&#xff1a;互联网大厂Java求职者面试&#xff0c;面试官针对5G融合消息PaaS项目进行提问。 第一轮提问 面试官&#xff1a;马架构&#xff0c;请简要介绍5G融合消息PaaS平台的核心功能和应用场景。 马架构&#xff…

【C语言极简自学笔记】C 语言数组详解:一维数组与二维数组

在 C 语言中&#xff0c;数组是一种非常重要的数据结构&#xff0c;它可以将多个相同类型的元素组织在一起&#xff0c;以便于我们进行批量处理和操作。本文将详细介绍 C 语言中的一维数组和二维数组&#xff0c;包括它们的定义、初始化、元素访问以及内存存储等方面的内容。 …

04.通过OpenAPI-Swagger规范让Dify玩转Agent

dify安装 cd dify cd docker cp .env.example .env docker compose up -d准备自定义工具 我自建的PowerDNS&#xff0c;它的swagger如下&#xff1a; https://github.com/PowerDNS/pdns/blob/master/docs/http-api/swagger/authoritative-api-swagger.yaml 但需要加上&#x…

汽车产业链主表及类别表设计

&#xff08;提前设计&#xff0c;备用&#xff09; 一、汽车产业链类别表&#xff08;industry_chain_category&#xff09; 设计要点 1、核心字段&#xff1a;定义产业链分类&#xff08;如零部件、整车制造、销售服务等&#xff09; 2、主键约束&#xff1a;自增ID作为唯一标…

‌RISC-V架构的低功耗MCU多电压域优化设计

RISC-V核低功耗MCU的多电压域设计是一种优化电源管理以降低功耗的技术方案。该设计通过电源域划分、电压转换和时序管理等手段&#xff0c;有效降低了系统功耗并提升能效&#xff0c;适用于物联网和嵌入式系统等场景。 多电压域设计的基本原理是将芯片划分为多个独立供电区域&…

基于STM32、HAL库的AD7616BSTZ模数转换器ADC驱动程序设计

一、简介: AD7616BSTZ是Analog Devices公司生产的一款16位、双通道、同步采样SAR型ADC芯片,主要特点包括: 16位分辨率 双通道同步采样 最高采样率:1MSPS/通道 输入范围:10V, 5V或2.5V(软件可编程) 串行(SPI)和并行接口选项 低功耗:典型值100mW 工作温度范围:-40C至+8…

CUDA Stream 回调函数示例代码

文章目录 CUDA Stream 回调函数示例代码基本概念示例代码代码解释回调函数的特点更复杂的示例&#xff1a;多个回调注意事项 CUDA Stream 回调函数中使用 MPI 或 NCCL示例程序注意事项 CUDA Stream 回调函数示例代码 CUDA 中的流回调函数(stream callback)是一种在 CUDA 流中插…