admin管理员组

文章数量:1563235

0 写在前面

       后续会不断更新,大家可以先关注我,点收藏!

       开头先分享一下自己的一些笔记,欢迎至下图的水印.....嵌入式求职之路

        我本科是普通双非院校毕业,硕士就读于某个中游211院校,本科和硕士专业都是机械工程

        然后在2024届秋招中,从8.15号开始准备秋招投递简历,到9.20号成功拿到5个嵌入式软件工程师的offer,最低的offer是在二线省会城市一个做工业机器人的企业,总包22万最高的offer是做新能源存储设备,总包36万,其他三个offer薪资总包在25万-30万之间。最后选择新能源行业做嵌入式电力设备开发。

        从机械-->嵌入式,相当于是跨半个专业,整个过程全靠自学,然后自己独立完成几个项目,从stm32到RTOS操作系统,再到Linux应用开发,中间也涉及到了驱动、QT上位机等等,中间遇到了很多困难,然后摸索出很多学习思路和解决办法。

        这一篇八股文文章,是我自己结合网络上他人的经验,自己详细整理出来的,我秋招过程中也是使用了自己这篇八股文文章,把它写出来,分享给大家。

        这篇文章包含的内容很多(会不断更新):

        第一部分(纯八股):C语言、C++、数据结构与算法、操作系统(RTOS)、ARM基础与架构、通讯协议、Linux应用知识点、Linux驱动知识点

        第二部分(个人简历):简历模板、做一份好的嵌入式简历、如何突出重点

        第三部分(经验分享):面试经历、面试经验、面试技巧

第二部分(个人简历)

1、个人简历中项目之一

1.1项目在简历中的描述(图片中水印处获取)

1.2项目资料简介

第一部分(纯八股)

1 C语言/C++

1.1 面试常问关键字

        面试不是死记硬背,你死记硬背,技术面试官能一下就瞧出来,所以要从本质上去理解一些内容。所以,八股有些地方我写的很详细,不要嫌我啰嗦,因为这些地方的八股必须要真的去理解,而不单单是背诵,相信大家看完后,就能理解相应的知识点了,而不仅仅是死记硬背。

1.1.1 volatile关键字

        volatile的意思是”易变的”,这个关键字主要是防止编译器对变量进行优化。即告诉编译器每次存取该变量的时候都要从内存去存取而不是使用它之前在寄存器中的备份。详细分析一下什么是编译器优化,以及为什么使用这个关键字。

(a)关于编译器优化

        首先理解CPU(寄存器)读取规则:

        如下面程序段:

int a, b; // 为a,b申请内存
a = 1;    // 1 -> CPU
          // CPU -> 内存(&a)
b = a;    // 内存(&a) -> CPU
          // CPU -> 内存(&b)

        如上图代码所示,a = 1这个程序,先将1写入CPU,再从CPU中将1写入a所在的内存地址中; b = a是先从内存中将a的值取出到CPU,再从CPU将值存入b的内存地址中。

int a = 1, b, c; // 为a,b,c申请内存并初始化
b = a;    // 内存(&a) -> CPU
          // CPU -> 内存(&b)
c = a;    // * 内存(&a) -> CPU *
          // CPU -> 内存(&c)

        如上图代码所示,上边的程序如果按第一段代码所说的顺序执行,则c = a语句在编译时是可以被编译器优化的,即注释部分(* 内存(&a) -> CPU *)的内容不被执行,因为在b = a这个语句中,a已经被移入过寄存器(CPU),那么在执行c = a时,就直接将a在寄存器(CPU)中传递给c。这样就减少了一次指令的执行,就完成了优化。
        上面就是编译器优化的原理过程,但是这个过程,有时会出现问题,而这个问题也就volatile存在的意义! 

(b)volatile的引入

        上边程序中,如果在执行完b = a后,a此时的值存放在CPU中。但是a在内存中又发生了变化(比如中断改变了a的值),但是存在CPU中的a是原来未变的a,按理应该是已经变化后的a赋值给c,但是此时却导致未变化的a赋值给了c

        这种问题,就是编译器自身优化而导致的。为了防止编译器优化变量a,引入了volatile关键字,使用该关键字后,程序在执行时c = a时,就会先去a的地址读出a到CPU,再从CPU将a的值赋予给c。这样就防止了被优化。

volatile int a = 1, b, c; // 为a,b,c申请内存并初始化
b = a;    // 内存(&a) -> CPU
          // CPU -> 内存(&b)
c = a;    // 内存(&a) -> CPU
          // CPU -> 内存(&c)
(c)哪些情况下使用volatile

        (1)并行设备的硬件寄存器。存储器映射的硬件寄存器通常加volatile,因为寄存器随时可以被外设硬件修改。当声明指向设备寄存器的指针时一定要用volatile,它会告诉编译器不要对存储在这个地址的数据进行假设。

        (2) 一个中断服务程序中修改的供其他程序检测的变量。volatile提醒编译器,它后面所定义的变量随时都有可能改变。因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。

        (3)多线程应用中被几个任务共享的变量。

1.1.2 static关键字

        面试问题1:static关键词的作用?

        static是被声明为静态类型的变量,存储在静态区(全局区)中,其生命周期为整个程序,如果是静态局部变量,其作用域为一对{  }内,如果是静态全局变量,其作用域为当前文件。静态变量如果没有被初始化,则自动初始化为0。

        面试问题2:为什么 static变量只初始化一次?

        对于所有的对象(不仅仅是静态对象),初始化都只有一次,而由于静态变量具有“记忆”功能,初始化后,一直都没有被销毁,都会保存在内存区域中,所以不会再次初始化。存放在静态区的变量的生命周期一般比较长,它与整个程序“同生死、共存亡”,所以它只需初始化一次。而auto变量,即自动变量,由于它存放在栈区,一旦函数调用结束,就会立刻被销毁。

        static修饰的全局变量,只能在本文件被调用;修饰的函数也只能在本文件调用。

1.1.3 const关键字

(a)定义变量(局部变量或全局变量)为常量,例如:

        图片在CSDN上带有水印,不太方便观看,后续会分享便于观看的电子版PDF文档 

 (b)修饰指针

         第一种和第二种是常量指针;第三种是指针常量;第四种是指向常量的常指针。

(b1)面试问题1:什么是常量指针?

        (1)常量指针说的是不能通过这个指针改变变量的值,但是还是可以通过其他的方式来改变变量的值的。
        (2)常量指针指向的值不能改变,但是这并不是意味着指针本身不能改变,常量指针可以指向其他的地址。

         上图中,p1是定义的常量指针,p1指向a的地址,*p1 = 15是不行的,因为不能通过常量指针去改变变量的值,如果去掉const则是可以的。

        没有const时,利用*p1可以去对a的值进行修改,如下图所示。

(b2)面试问题2:什么是指针常量? 

        指针常量是指指针本身是个常量,不能在指向其他的地址,需要注意的是,指针常量指向的地址不能改变,但是地址中保存的数值是可以改变的,可以通过其他指向该地址的指针来修改。

 (b3)面试问题3:什么是指向常量的常指针? 

        是指针常量与常量指针的结合,指针指向的位置不能改变并且也不能通过这个指针改变变量的值,但是依然可以通过其他的普通指针改变变量的值。

 (c)修饰函数的参数

        表示在函数体内不能修改这个参数的值。

 (d)修饰函数的返回值

        (d1)如果给用const修饰返回值的类型为指针,那么函数返回值(即指针)的内容是不能被修改的,而且这个返回值只能赋给被const修饰的指针。例如:

        (d2)如果用const修饰普通的返回值,如返回int变量,由于这个返回值是一个临时变量,在函数调用结束后这个临时变量的生命周期也就结束了,因此把这些返回值修饰为const是没有意义的。

1.1.4 typedef和 define有什么区别?

        typedef与define都是替一个对象取一个别名,以此来增强程序的可读性,但是它们在使用和作用上也存在着以下4个方面的不同。

(a)原理不同

        #define是C语言中定义的语法,它是预处理指令,在预处理时进行简单而机械的字符串替换,不做正确性检査,不管含义是否正确照样代入,只有在编译已被展开的源程序时,才会发现可能的错误并报错。 例如, # define Pl3.1415926 ,当程序执行 area=Pr * r 语句时,PI会被替换为3.1415926。于是该 语句被替换为 area=3.1415926*r*r 。如果把# define语句中的数字9写成了g,预处理也照样代入,而不去检查其是否合理、合法。 typedef是关键字,它在编译时处理,所以 typedef具有类型检查的功能。它在自己的作用域内给一个已经存在的类型一个别名,但是不能在一个函数定义里面使用标识符 typedef。例如,typedef int INTEGER ,这以后就可用 INTEGER来代替int作整型变量的类型说明了,例如:INTEGER a,b; 用 typedef定义数组、指针、结构等类型将带来很大的方便,不仅使程序书写简单而且使意义更为明确,因而增强了可读性。例如: typedef int a[10]; 表示a是整型数组类型,数组长度为10。然后就可用a说明变量,例如:语句a s1,s2;完全等效于语句 int s1[10],s2[10].同理, typedef void(*p)(void)表示p是一种指向void型的指针类型。

(b)功能不同

        typedef用来定义类型的别名,这些类型不仅包含内部类型(int、char等),还包括自定义类型(如 struct),可以起到使类型易于记忆的功能。

        例如: typedef int (*PF)(const char *, const char*) 定义一个指向函数的指针的数据类型PF,其中函数返回值为int,参数为 const char*。typedef还有另外一个重要的用途,那就是定义机器无关的类型。例如,可以定义一个叫REAL的浮点类型,在目标机器上它可以获得最高的精度: typedef long double REAL ,在不支持 long double的机器上,该 typedef 看起来会是下面这样: typedef double real ,在 double都不支持的机器上,该 typedef看起来会是这样: typedef float REAL 。 #define不只是可以为类型取别名,还可以定义常量、变量、编译开关等。

(c)作用域不同

        #define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而 typedef有自己的作用域。

(d)对指针的操作不同

        INTPTR1 pl, p2和INTPTR2 p3,p4的效果截然不同。 INTPTR1 pl, p2进行字符串替换后变成 int*p1,p2 ,要表达的意义是声明一个指针变量p1和一个整型变量p2。

        而INTPTR2 p3,p4,由于 INTPTR2是具有含义的,告诉我们是一个指向整型数据的指针,那么p3和p4都为指针变量,这句相当于 int*pl,*p2 .从这里可以看出,进行宏替换是不含任何意义的替换,仅仅为字符串替换;而用 typedef 为一种数据类型起的别名是带有一定含义的。

        上述代码中, const INTPTR1 p1表示p1是一个常量指针,即不可以通过p1去修改p1指向的内容,但是 p1可以指向其他内容。而对于 const INTPTR2 p2,由于 INTPTR2表示的是个指针类型,因此用 const去 限定,表示封锁了这个指针类型,因此p2是一个指针常量,不可使p2再指向其他内容,但可以通过p2修 改其当前指向的内容。 INTPTR2 const p3同样声明的是一个指针常量。

1.2 变量、数组、指针

1.2.1变量

(a)定义常量谁更好?# define还是 const?

        尺有所短,寸有所长, define与 const都能定义常量,效果虽然一样,但是各有侧重。

        define既可以替代常数值,又可以替代表达式,甚至是代码段,但是容易出错,而 const的引入可以增强程序的可读性,它使程序的维护与调试变得更加方便。具体而言,它们的差异主要表现在以下3个方面。

        (a1)define只是用来进行单纯的文本替换,define常量的生命周期止于编译期,不分配内存空间,它存在于程序的代码段,在实际程序中,它只是一个常数;而const常量存在于程序的数据段,并在堆栈中分配了空间,const常量在程序中确确实实存在,并且可以被调用、传递

        (a2)const常量有数据类型,而define常量没有数据类型。编译器可以对const常量进行类型安全检査,如类型、语句结构等,而define不行。

        (a3)很多IDE支持调试 const定义的常量,而不支持 define定义的常量由于const修饰的变量可以排除 程序之间的不安全性因素,保护程序中的常量不被修改,而且对数据类型也会进行相应的检查,极大地提高了程序的健壮性,所以一般更加倾向于用const来定义常量类型。

(b)全局变量和局部变量的区别是什么?

        (b1)全局变量的作用域为程序块,而局部变量的作用域为当前函数。

        (b2)内存存储方式不同,全局变量(静态全局变量,静态局部变量)分配在全局数据区(静态存储空间),后者分配在栈区。

        (b3)生命周期不同。全局变量随主程序创建而创建,随主程序销毁而销毁,局部变量在局部函数内部,甚至局部循环体等内部存在,退出就不存在了。

        (b4)使用方式不同。通过声明为全局变量,程序的各个部分都可以用到,而局部变量只能在局部使用。

(c)全局变量可不可以定义在可被多个.C文件包含的头文件中?为什么?

        可以,在不同的C文件中以static形式来声明同名全局变量

        可以在不同的C文件中声明同名的全局变量,前提是其中只能有一个C文件中对此变量赋初值,此时连接不会出错。

(d)局部变量能否和全局变量重名?

        能,局部会屏蔽全局。

        局部变量可以与全局变量同名,在函数内引用这个变量时,会用到同名的局部变量,而不会用到全局变量。 对于有些编译器而言,在同一个函数内可以定义多个同名的局部变量,比如在两个循环体内都定义一个同名的局部变量,而那个局部变量的作用域就在那个循环体内。

1.2.2 数组

(a)数组指针

        数组指针就是指向数组的指针,它表示的是一个指针,这个指针指向的是一个数组,它的重点是指针。 例如, int(*pa)[8] 声明了一个指针,该指针指向了一个有8个int型元素的数组。下面给出一个数组 指针的示例。

        程序的输出结果为 5。

        上例中,p是一个数组指针,它指向一个包含有4个int类型数组的指针,刚开始p被初始化为指向数组b 的首地址,++p相当于把p所指向的地址向后移动4个int所占用的空间,此时p指向数组{5,6,7,8},语句 *(++p); 表示的是这个数组中第一个元素的地址(可以理解p为指向二维数组的指针,{1,2,3,4}, {5,6,7,8},{9,10,11,12}。p指向的就是{1,2,3,4}的地址, *p 就是指向元素,{1,2,3,4}, **p 指向的就是1,语句**(++p)会输出这个数组的第一个元素5。

(b)指针数组

        指针数组表示的是一个数组,而数组中的元素是指针。下面给出另外一个指针数组的示例:

        程序的输出结果为1234。

(c)数组下标可以为负数吗?

        可以,因为下标只是给出了一个与当前地址的偏移量而已,只要根据这个偏移量能定位得到目标地址即可。

1.2.3 指针

(a)函数指针

        如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空 间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。

        这个语句就定义了一个指向函数的指针变量 p。首先它是一个指针变量,所以要有一个“*”,即 (*p); 其次前面的 int 表示这个指针变量可以指向返回值类型为 int 型的函数;后面括号中的两个 int 表示这个 指针变量可以指向有两个参数且都是 int 型的函数。所以合起来这个语句的意思就是:定义了一个指针变量 p,该指针变量可以指向返回值类型为 int 型,且有两个整型参数的函数。p 的类型为 int(*) (int,int) 。

        我们看到,函数指针的定义就是将“函数声明”中的“函数名”改成“(指针变量名)”。但是这里需要注意的是:“(指针变量名)”两端的括号不能省略,括号改变了运算符的优先级。如果省略了括号,就不是定义函数指针而是一个函数声明了,即声明了一个返回值类型为指针型的函数。

        重要:最后需要注意的是,指向函数的指针变量没有 ++ 和 -- 运算。

(b)指针函数

        首先它是一个函数,只不过这个函数的返回值是一个地址值。函数返回值必须用同类型的指针变量来接受,也就是说,指针函数一定有“函数返回值”,而且,在主调函数中,函数返回值必须赋给同类型的指针变量。

       类型名 *函数名(函数参数列表)

        其中,后缀运算符括号“()”表示这是一个函数,其前缀运算符星号“*”表示此函数为指针型函数,其函数值为指针,即它带回来的值的类型为指针,当调用这个函数后,将得到一个“指向返回值为…的指针(地址),“类型名”表示函数返回的指针指向的类型”。

        “(函数参数列表)”中的括号为函数调用运算符,在调用语句中,即使函数不带参数,其参数表的一对括号也不能省略。其示例如下:

        由于 “*” 的优先级低于“()”的优先级,因而pfun首先和后面的“()”结合,也就意味着,pfun是一个函数。 即:

        接着再和前面的 “*” 结合,说明这个函数的返回值是一个指针。由于前面还有一个int,也就是说,pfun是一个返回值为整型指针的函数。

        共有三个学生的成绩,函数find()被定义为指针函数,其形参pointer是指针指向包含4个元素的一维数组 的指针变量。pointer+n指向score的第n+1行。*(pointer+1)指向第一行的第0个元素。pt是一个指针变 量,它指向浮点型变量。main()函数中调用find()函数,将score数组的首地址传给pointer。

(c)数组和指针的区别与联系是什么?
        (c1)存储方式

        数组通常存储在静态存储区或栈上;指针可以随时随地地指向任意类型的内存块。

        数组在内存中是连续存放的,开辟一块连续的内存空间。数组是根据数组的下标进行访问的;指针很灵活,它可以指向任意类型的数据。指针的类型说明了它所指向地址空间的内存。

        (c2)求sizeof
        数组:

                数组所占存储空间的内存sizeof(数组名)

                数组的大小:sizeof(数组名)/sizeof(数据类型)

        指针:

                在32位平台下,无论指针的类型是什么,sizeof(指针名)都是4,在64位平台下,无论指针的类型是什么,sizeof(指针名)都是8

        (c3)数据访问方面

        指针对数据的访问方式是间接访问,需要用到解引用符号(*数组名)。

        数组对数据的访问则是直接访问,可通过下标访问或数组名+元素偏移量的方式

        (c4)使用环境

        指针多用于动态数据结构(如链表,等等)和动态内存开辟。

        数组多用于存储固定个数且类型统一的数据结构(如线性表等等)和隐式分配。

(d)指针进行强制类型转换后与地址进行加法运算,结果是什么?

        假设在32位机器上,在对齐为4的情况下,sizeof(long)的结果为4字节,sizeof(char*)的结果为4字节, sizeof(short int)的结果与 sizeof(short)的结果都为2字节, sizeof(char)的结果为1字节, sizeof(int)的结果为4字节,由于32位机器上是4字节对齐,以如下结构体为例:

        当p=0x100000; 则 p+0×200=? (ulong)p+0x200=? (char*)p+0x200=? 其实,在32位机器下, sizeof(struct BBB)=sizeof(*p)=4+4+2+2+1+3/*补齐*/+2*5+2/*补齐*/=24字节,而 p=0x100000 ,那么 p+0x200=0x1000000+0x200*24 指针加法,加出来的是指针所指类型的字节长度的整倍数,就是p偏移sizeof(p)*0x200。

        (ulong)p+0x200=0x10000010+0x200经过ulong后,已经不再是指针加法,而变成一个数值加法了。(char*)p+0x200=0x1000000+0×200*sizeof(char) 结果类型是char*。

(e)指针常量,常量指针,指向常量的常量指针有什么区别?
        (e1)指针常量

        先看const再看 * ,p是一个常量类型的指针,不能修改这个指针的指向,就是指针指向的地址不能修改,但是这个指针所指向的地址上存储的值可以修改。

        (e2)常量指针

先看*再看const,定义一个指针指向一个常量,不能通过指针来修改这个指针指向的值

        (e3)指向常量的常量指针

对于“指向常量的常量指针”,就必须同时满足上述1和2中的内容,既不可以修改指针的值,也不可以修改指针指向的值

(f)指针和引用的异同是什么?如何相互转换?(C++)

        (f1)相同

  1. 都是地址的概念,指针指向某一内存、内容是所指内存的地址;引用则是某块内存的别名
  2. 从内存分配上看:两者都占内存,程序为指针会分配内存,一般是4个字节;而引用的本质是指针常量,指向对象不能变,但指向对象的值可以变。两者都是地址概念,所以本身都会占用内存。

        (f2)区别

        1. 指针是实体,而引用是别名。

        2. 指针和引用的自增(++)运算符意义不同,指针是对内存地址自增,引用是对值的自增。

        3. 引用使用时无需解引用(*),指针需要解引用;

        4. 引用只能在定义时被初始化一次,之后不可变;指针可变。

        5. 引用不能为空,指针可以为空。

        6. “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小,在32 位系统指针变量一般占用4字节内存。

        由结果可知,引用使用时无需解引用(*),指针需要解引用;我用的是64位操作系统,“sizeof 指针”得到 的是指针本身的大小,及8个字节。而“sizeof 引用”得到的是的对象本身的大小及int的大小,4个字节。

       (f3) 转换

        1. 指针转引用:把指针用*就可以转换成对象,可以用在引用参数当中。

        2. 引用转指针:把引用类型的对象用&取地址就获得指针了。

(g)野指针是什么?

        (g1) 野指针是指向不可用内存的指针,当指针被创建时,指针不可能自动指向NULL,这时,默认值是随机的,此时的指针成为野指针。

        (g2) 当指针被free或delete释放掉时,如果没有把指针设置为NULL,则会产生野指针,因为释放掉的仅仅是指针指向的内存,并没有把指针本身释放掉。

        (g3) 第三个造成野指针的原因是指针操作超越了变量的作用范围。

(h)如何避免野指针?
        (h1)对指针进行初始化。

        (h2)指针用完后释放内存,将指针赋NULL。

        注:malloc函数分配完内存后需注意:

        1. 检查是否分配成功(若分配成功,返回内存的首地址;分配不成功,返回NULL。可以通过if语句来判断)

        2. 清空内存中的数据(malloc分配的空间里可能存在垃圾值,用memset或bzero 函数清空内存)

(i)C++中的智能指针是什么?

        智能指针是一个类,用来存储指针(指向动态分配对象的指针)。

        C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。

(j)智能指针的内存泄漏如何解决?

        为了解决循环引用导致的内存泄漏,引入了弱指针 weak_ptr , weak_ptr 的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但是不会指向引用计数的共享内存, 但是可以检测到所管理的对象是否已经被释放,从而避免非法访问。

(k)this指针是什么?

        this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针

        (k1)this指针指向当前对象,可以访问当前对象的所有成员变量。包括private、protected、public。

        (k2)this指针是const指针,一切企图修改该指针的操作,如赋值(改变指向)、增减都是不允许的!

        (k3)this指针只有在成员函数中才有定义。因此,在创建一个对象后,也不能通过对象使用this指针。所以,我们也无法知道一个对象的this指针的位置(只有在成员函数里才有this指针的位置)。当然,在成员函数里,你是可以知道this指针的位置的(可以&this获得),也可以直接使用的。

        (k4)只有创建对象后,this指针才有意义。

        (k5)static静态成员函数不能使用this指针。原因静态成员函数属于类,而不属于某个对象,所以static静态成员函数压根就没有this指针。

        (k6)this在成员函数的开始执行前构造的,在成员函数的执行结束后清除。至于如何清除的,由编译器实现,程序员不关心。this是通过函数参数的首参数来传递的。

1.3 内存

1.3.1 C语言中内存分配的方式有几种?

(a)静态存储区分配

内存分配在程序编译之前完成,且在程序的整个运行期间都存在,例如全局变量、静态变量等。 (b)栈上分配

在函数执行时,函数内的局部变量的存储单元在栈上创建,函数执行结束时这些存储单元自动释放。局部变量、函数内参数都在栈上。

(c) 堆上分配  New开辟的空间在堆上

1.3.2 堆与栈有什么区别?

(a)申请方式

        栈的空间由操作系统自动分配/释放,堆上的空间手动分配/释放。

(b)申请大小的限制栈空间有限。

        在Wind  ows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是 一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示 overflow。因此,能从栈获得的空间较小堆是很大的自由存储区。堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用 链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

(c)申请效率

        栈由系统自动分配,速度较快。但程序员是无法控制的。 堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.

1.3.3 栈在C语言中有什么作用?

(a)C语言中栈用来存储临时变量,临时变量包括函数参数和函数内部定义的临时变量。函数调用中和函数调用相关的函数返回地址,函数中的临时变量,寄存器等均保存在栈中,函数调动返回后从栈中恢复寄存器和临时变量等函数运行场景。

(b)多线程编程的基础是栈,栈是多线程编程的基石,每一个线程都最少有一个自己专属的栈,用来存储本线程运行时各个函数的临时变量和维系函数调用和函数返回时的函数调用关系和函数运行场景。 操作系统最基本的功能是支持多线程编程,支持中断和异常处理,每个线程都有专属的栈,中断和异常处理也具有专属的栈,栈是操作系统多线程管理的基石。

1.3.4 C语言函数参数压栈顺序是怎样的?

先理解入栈和出栈:

栈的范围是由   ss * 10H   至   ss * 10H + sp

(ss)指堆栈寄存器:存放堆栈段起始地址的高16位(即16进制下五个数的前四个数)。

(sp)指堆栈指针:用于存放栈顶的逻辑偏移地址。

栈的栈底指针不变,栈顶的指针随sp的改变而改变。由于栈的栈底地址是高地址,栈顶地址是低地址。所以当栈存入数据时,会先将sp减去存入数据的字节数,然后再将数据存入。反之,当栈取出数据时,会将数据取出后将sp加上取出数据的字节数。(例如,当sp=0800H,ss=2360H时,若此时加入20个字节的数据,那么就要将sp - 20,此时的栈顶就是ss * 10H + sp)。

注:所谓高地址与低地址,前面的地址称为低地址,后面的地址称为高地址,例如23600H23E00H,此时23600H为低地址,23E00H为高地址

回答问题:从右至左。

        C语言参数入栈顺序的好处就是可以动态变化参数个数。自左向右的入栈方式,最前面的参数被压在栈底。除非知道参数个数,否则是无法通过栈指针的相对位移求得最左边的参数。这样就变成了左边参数的个数不确定,正好和动态参数个数的方向相反。因此,C语言函数参数采用自右向左的入栈顺序,主要原因是为了支持可变长参数形式

例如:    printfconst char* format,…

  1. printf函数是一个不定参函数。
  2. 编译器通过format的%占位符的个数来获取参数的个数。
  3. 假设函数压栈顺序是从左至右,format先入栈,各个参数再入栈,最后pc入栈。入栈完之后,想知道参数的个数就要读取format,但要读取format就得知道参数的个数,陷入了一个死循环。
  4. 但是,如果函数压栈顺序是从右至左,未知个数的参数先入栈,format再入栈,最后压pc入栈。这时候要想知道参数的个数只需要将栈顶指针加2即可读取到format

1.3.5 C++的内存管理是怎样的?

        在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。

        代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。

        数据段:存储程序中已初始化的全局变量和静态变量

        BSS段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。

        堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。

        映射区:存储动态链接库以及调用mmap函数进行的文件映射

        栈:使用栈空间存储函数的返回地址、参数、局部变量、返回值

1.3.6 什么是内存泄漏?

        简单地说就是申请了一块内存空间,使用完毕后没有释放掉。

        它的一般表现方式是程序运行时间越长,占用内存越多,最终用尽全部内存,整个系统崩溃。由程序申请的一块内存,且没有任何一个指针指向它,那么这块内存就泄露了。

1.3.7 如何判断内存泄漏?

        1. 良好的编码习惯,尽量在涉及内存的程序段,检测出内存泄露。当程式稳定之后,在来检测内存泄露时,无疑增加了排除的困难和复杂度。使用了内存分配的函数,一旦使用完毕,要记得要使用其相应的函数释放掉。

        2. 将分配的内存的指针以链表的形式自行管理,使用完毕之后从链表中删除,程序结束时可检查改链表。

        3. Boost 中的smart pointer。

        4. 一些常见的工具插件,如ccmalloc、Dmalloc、Leaky等等

1.3.8  new/delete与malloc/free的区别是什么?

        在C++中,申请动态内存和释放动态内存,用new/delete 和 malloc/free都可以,new和malloc动态申请的内存都位于堆中,无法被操作系统回收,需要对应的delete/free来释放空间。

        void *malloc(int size);

        说明:malloc向系统申请分配指定size个字节的内存空间。返回类型是 void* 类型。void* 表示未确定类型的指针。C,C++规定,void* 类型可以强制转换为任何其它类型的指针。

        对于类的对象而言,malloc/free无法满足动态对象的要求,对象在创建时要自动执行构造函数,在对象消亡之前要自动执行析构函数,而malloc/free 不在编译器控制权限之内,无法执行构造函数和析构函数。

        当然对于没有资源要清理的类,不调用析构函数也没有太大的问题,即使用free或delete没有区别。但万一有一些类的成员是指针,而这个指针又在堆上开辟了空间,这时不调用析构函数去释放这个指针指向的这段空间,就会造成内存泄漏。delete会调用析构函数,释放指针成员变量的空间,再销毁对象本身的空间;而free只释放了对象本身的空间,而指针成员所指向的空间没有被释放

        1)new 能够自动计算需要分配的内存空间,而malloc需要手工计算字节数。

        2) new与delete带具体类型的指针,malloc与free返回void类型的指针。

        3)new 将调用构造函数,而malloc不能;delete将调用析构函数,而free不能。

        4)malloc/free 需要库文件<stdlib.h>支持,而new/delete不需要库文件支持。

        5)new操作可以重载,可以自定义内存分配策略,不做内存分配,或者分配到非内存设备上。而malloc不能。

        delete和free被调用后,内存不会不会立即收回,指针也不会指向空,delete或free仅仅是告诉操作系统,这一块内存被释放啦,还可以做其他用途。由于没有对这块内存进行写操作,所以内存中的变量数值并没有发生变化,出现野指针的情况,因此,释放完内存后需要将指针向量置为空。

1.4 预处理

 1.4.1 预处理器标识#error的目的是什么?

        #error预处理指令的作用是,编译程序时,只要遇到#error就会生成一个编译错误提示消息,并停止编译。其语法格式为:#error error-message。

        下面举个例子: 程序中往往有很多的预处理指令

        当程序比较大时,往往有些宏定义是在外部指定的(如makefile),或是在系统头文件中指定的,当你 不太确定当前是否定义了 XXX 时,就可以改成如下这样进行编译:

        这样,如果编译时出现错误,输出了XXX has been defined,表明宏XXX已经被定义了。

1.4.2 如何使用 define声明个常数,用以表明1年中有多少秒(忽略闰年问题)

        考虑到可能存在数据溢出问题,更加规范化的写法是使用长整型类型,即UL类型,告诉编译器这个常数是长整型数。

1.4.3  # include< filename. h>和#include" filename. h"有什么区别?

        对于 include< filename. h>,编译器先从标准库路径开始搜索filename.h,使得系统文件调用较快。而 对于# include“ filename.h”,编译器先从用户的工作路径开始搜索filename.h,然后去寻找系统路径,使得自定义文件较快。

1.4.4 头文件的作用有哪些?

        头文件的作用主要表现为以下两个方面:

        1. 通过头文件来调用库功能。出于对源代码保密的考虑,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口是怎么实现的。编译器会从库中提取相应的代码。

        2. 头文件能加强类型安全检查。当某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,大大减轻程序员调试、改错的负担。

1.4.5 在头文件中定义静态变量是否可行,为什么?

        不可行,如果在头文件中定义静态变量,会造成资源浪费的问题,同时也可能引起程序错误。因为如果 在使用了该头文件的每个C语言文件中定义静态变量,按照编译的步骤,在每个头文件中都会单独存在一个静态变量,从而会引起空间浪费或者程序错误所以,不推荐在头文件中定义任何变量,当然也包括静态变量

1.4.6 写一个"标准"宏MIN ,这个宏输入两个参数并返回较小的一个

1.5 其他C语言面试问题

1.5.1 C语言宏中“#”和“##”的用法

(a)“#”字符串化操作符

        作用:#可以把一个宏参数直接转换成相应的字符串。比如有下面这个宏:

        则进行如下调用:

        最后其执行效果如下面程序一样:

        也就是说,最后输出的是宏参数的参数名。即:将宏参数直接转换成相应得字符串

(b)“##”符号连接操作符

作用:将宏定义的多个形参转换成一个实际参数名。

则下面第一个图的代码和第二个图的代码等价:

1.5.2 extern”C” 的作用是什么?

        extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。

1.5.3 strlen("\0") =? sizeof("\0")=? 两者结果与区别

        strlen("\0") =0,sizeof("\0")=2。

        strlen用来计算字符串的长度(在C/C++中,字符串是以"\0"作为结束符的),它从内存的某个位置(可以是字符串开头,中间某个位置,甚至是某个不确定的内存区域)开始扫描直到碰到第一个字符串结束符\0为止,然后返回计数器值。sizeof是C语言的关键字,它以字节的形式给出了其操作数的存储大小,操作数可以是一个表达式或括在括号内的类型名,操作数的存储大小由操作数的类型决定。

1.5.4 C语言中 struct与 union的区别是什么?

        struct(结构体)与 union(联合体)是C语言中两种不同的数据结构,两者都是常见的复合结构,其区 别主要表现在以下两个方面。

        (a)结构体与联合体虽然都是由多个不同的数据类型成员组成的,但不同之处在于联合体中所有成员共 用一块地址空间,即联合体只存放了一个被选中的成员,而结构体中所有成员占用空间是累加的, 其所有成员都存在,不同成员会存放在不同的地址。在计算一个结构型变量的总长度时,其内存空 间大小等于所有成员长度之和(需要考虑字节对齐),而在联合体中,所有成员不能同时占用内存空间,它们不能同时存在,所以一个联合型变量的长度等于其最长的成员的长度。

        (b)对于联合体的不同成员赋值,将会对它的其他成员重写,原来成员的值就不存在了,而对结构体的 不同成员赋值是互不影响的。

        假设为32位机器,int型占4个字节, double型占8个字节,char型占1个字节,而DATE是一个联合型变 量,联合型变量共用空间,uion里面最大的变量类型是int[5],所以占用20个字节,它的大小是20,而 由于 union中 double占了8个字节,因此 union是要8个字节对齐,所占内存空间为8的倍数。为了实现 8个字节对齐,所占空间为24.而data是一个结构体变量,每个变量分开占用空间,依次为 sizeof(int) + sizeof(DATE)+ sizeof( double)=4+24+8=36按照8字节对齐,占用空间为40,所以结果为 40+24=64。

1.5.5 左值和右值是什么?

        左值是指可以出现在等号左边的变量或表达式,它最重要的特点就是可写(可寻址)。也就是说,它的值可以被修改,如果一个变量或表达式的值不能被修改,那么它就不能作为左值。

        右值是指只可以出现在等号右边的变量或表达式。它最重要的特点是可读。一般的使用场景都是把一个右值赋值给一个左值。通常,左值可以作为右值,但是右值不一定是左值。

1.5.6 有符号数和无符号数的运算?   

        int a = -20, unsigned int b = 6,a+b是否大于6?

       有符号和无符号运算,强制转换为无符号,所有a+b会变成(unsigned int)a+b;

       (unsigned int)a 就会相当于无符号最大值-20,那么是一个非常大的值,这个值加上6,那么肯定是大于6的;

       最后的值是2^32-20+6=4294967282,肯定大于6

1.5.7 什么是短路求值?

        输出结果为1。输出为什么不是2,而是1呢?

        其实,这里就涉及一个短路计算的问题。由于i语句是个条件判断语句,里 面是有两个简单语句进行或运算组合的复合语句,因为或运算中,只要参与或运算的两个表达式的值都为真,则整个运算结果为真,而由于变量i的值为6,已经大于0了,而该语句已经为true,则不需要执行后续的j+操作来判断真假,所以后续的j++操作不需要执行,j的值仍然为1。

        因为短路计算的问题,对于&&操作,由于在两个表达式的返回值中,如果有一个为假则整个表达式的值 都为假,如果前一个语句的返回值为 false,则无论后一个语句的返回值是真是假,整个条件判断都为 假,不用执行后一个语句,而a>b的返回值为 false,程序不执行表达式n=c>d,所以,n的值保持为初值2。

1.5.8 什么是大端和小端?

        大端:高地址存低字节,低地址存高字节
        小端:低地址存低字节,高地址存高字节

1.5.9 ++a和a++有什么区别?两者是如何实现的?

        a++的具体运算过程为: 先用a,再执行++

        ++a的具体运算过程为:  先执行++,再用a

1.6 C++部分

1.6.1 C++中类成员的访问权限?

        无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。在类的外部 (定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员。

        (a)protected:受保护的,类内和子类可直接访问,也就是说,基类中有protected成员,子类继承于基类,那么也可以访问基类的protected成员,要是基类是private成员,则对于子类也是隐藏的,不可访问。

        (b)private:私有的,只有类内的成员函数才可以访问。

1.6.2 什么是构造函数?   

        构造函数是一种特殊的函数,用于创建和初始化对象。它在创建对象时被调用,用于设置对象的初始状态和属性。构造函数的名称通常与类的名称相同,且没有返回类型声明。

        构造函数可以有多个重载版本,每个版本允许接受不同类型和数量的参数。通过调用不同的构造函数,可以根据需要创建不同种类的对象。

        构造函数的主要功能包括:

        (a)分配内存空间:构造函数负责为对象分配足够的内存空间,以存储对象的数据成员。

        (b)初始化对象:构造函数可以对对象的数据成员进行初始化,确保对象的属性处于正确的初始状态。

        (c)设置默认值:构造函数可以为对象的属性设置默认值,以避免对象在创建时出现未定义的行为。

        在C++中,构造函数名称与类名称相同,没有返回类型声明,并且可以是公有、私有或受保护的。当创建对象时,会自动调用适当的构造函数来初始化对象。如果未明确定义构造函数,编译器将提供一个默认的无参数构造函数。

1.6.3 构造函数的分类是怎样的?

        (1)无参构造    Person( ) {}

        (2)有参构造      Person(int a) {}

        (3)拷贝构造函数     Person( const Person& p) {}

1.6.4 构造函数的调用规则是怎样的?

     C++编译器至少给一个类添加3个函数

        (a)默认构造函数(无参)

        (b)默认析构函数(无参)

        (c)默认拷贝构造函数,对属性进行值拷贝

                如果用户定义了有参构造函数,C++不再提供默认无参构造,但是会提供默认拷贝构造;如果用户定义拷贝构造函数,C++不会再提供其他构造函数。

1.6.5 什么是析构函数? 

        需要自己定义构造函数和析构函数的情况有以下几种:

        (a)当需要在对象创建时进行一些初始化操作时,可以定义构造函数来实现。比如,需要在对象创建时给成员变量赋初值或者打开一些资源。

        (b)当需要在对象销毁时进行一些清理操作时,可以定义析构函数来实现。比如,需要在对象销毁时释放一些资源或者关闭一些文件。

        (c)当需要控制对象的生命周期时,可以定义构造函数和析构函数来实现。比如,需要在对象创建时进行一些操作,在对象销毁时进行一些清理操作,这样可以确保对象的正确使用。

        总之,需要自己定义构造函数和析构函数的情况主要是为了实现一些特定的需求,比如初始化、清理、控制对象的生命周期等。

        注意:构造函数可以有参数,因此可以重载,析构函数不能有参数,因此不可以发生重载。

1.6.6 引用注意事项?       

        引用格式:   数据类型 &别名 = 原名

        引用必须初始化;引用在初始化后不可以改变;函数传参时,可以利用引用让形参修饰实参;引用可以作为函数的返回值,但是不要返回局部变量。引用的本质在C++内部实现一个指针常量。

1.6.7 函数重载是什么?

        重载满足条件:同一个作用域下;函数名称相同;函数参数类型不同或者个数不同或者顺序不同。

1.6.8 什么是深拷贝与浅拷贝?

浅拷贝:简单的赋值拷贝操作

深拷贝:在堆区重新申请空间,进行拷贝操作

       当在类里面涉及到指针操作时,如果采用浅拷贝,则执行拷贝构造函数后。会导致拷贝出两个指针指向同一个内存空间,则进行析构函数时,就会对该空间释放两次,然后导致报错。因此需要进行深拷贝,对于指针重新再开辟一段空间。

1.6.9 静态成员归纳

(a)静态成员变量:

       所有对象共享同一份数据; 在编译阶段分配内存;类内声明,类外初始化

(b)静态成员函数:

       所有对象共享同一个函数;静态成员函数只能访问静态成员变量

(c)关于两者内存:

       如果只声明了类而未定义对象,则类的一般成员变量是不占用内存空间的,只有在定义对象的时候,才为对象的成员变量分配空间。

       静态成员不占用类内空间;静态成员函数在类内声明,类外初始化。

1.6.10  继承是什么?

        语法:  class 子类 : 继承方式 父类     比如:class A :public B

       多继承语法:   

                  class 子类 : 继承方式   父类1,继承方式 父类1

       继承过程中,父类中的私有成员也被子类继承,只是由编译器隐藏后访问不到。

        继承同名成员处理方式:

        子类对象可以直接访问到子类中同名成员

       子类对象加作用域可以访问到父类同名成员

       当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数。

1.6.11 菱形继承是什么?

        两个派生类继承同一个基类;又有某个类同时继承了两个派生类;这种继承称为菱形继承。

       羊继承了动物数据;马继承了动物数据;草泥马继承了羊和马的数据,则动物数据被继承了两份。

       采用虚继承的方法解决该问题。

1.6.12 虚函数是什么?

        虚函数只能是类的成员函数, 而不能将类外的普通函数声明为虚函数. 虚函数的作用是允许在派生类中对基类的虚函数重新定义 (函数覆盖), 只能用于类的继承层次结构中.

        虚函数能有效减少空间开销. 当一个类带有虚函数时, 编译系统会为该类构造一个虚函数表 (一个指针数组), 用于存放每个虚函数的入口地址.

        什么时候应该使用虚函数:

        判断成员函数所在的类是不是基类, 非基类无需使用虚函数

        成员函数在类被继承后有没有可能被更改的功能, 如果希望修改成员函数功能, 一般在基类中将其声明为虚函数;

        我们会通过对象名还是基类指针访问成员函数, 如果通过基类指针过引用去访问, 则应当声明为虚函数

1.6.13 静态函数和虚函数的区别?

        多态分为两类:静态多态和动态多态

       静态多态:函数重载和运算符重载属于静态多态、

       虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销。

1.6.14 什么是多态?

        多态是指不同继承关系的类对象,去调用同一函数,产生了不同的行为。在继承中要想构成多态需要满足两个条件:

       (a)必须通过基类的指针或者引用调用虚函数

        (b)被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

1.6.15 纯虚函数是什么?

        纯虚函数不需要在父类中实现,必须在子类中实现

        在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数。

       纯虚函数语法:

                virtual 返回值类型 函数名 (参数列表) = 0

               当类中有了纯虚函数,这个类也称为抽象类

        抽象类特点:

               无法实例化对象

               子类必须重写抽象类中的纯虚函数,否则也属于抽象类。

1.6.16 重载和覆盖有什么区别?

        (a)覆盖是子类和父类之间的关系,垂直关系;重载同一个类之间方法之间的关系,是水平关系。

        (b)覆盖只能由一个方法或者只能由一对方法产生关系;重写是多个方法之间的关系。

        (c)覆盖是根据对象类型(对象对应存储空间类型)来决定的;而重载关系是根据调用的实参表和形参表来选择方法体的。

1.6.17 析构函数可以为 virtual 型,构造函数则不能,为什么?

        虚函数的主要意义在于被派生类继承从而产生多态。派生类的构造函数中,编译器会加入构造基类的代码,如果基类的构造函数用到参数,则派生类在其构造函 数的初始化列表中必须为基类给出参数,就是这个原因。虚函数的意思就是开启动态绑定,程序会根据对象的动态类型来选择要调用的方法。然而在构造函数运行的时候,这个对象的动态类型还不完整,没有办法确定它到底是什么类型,故构造函数不能动态绑定。

1.6.18 数组下标可以为负数吗

可以,因为下标只是给出了一个与当前地址的偏移量而已,只要根据这个偏移量能定位得到目标地址即可。

1.6.19 结构体和类的区别

​2. ARM

2.1 硬件基础

2.1.1 NAND FLASH 和NOR FLASH异同?

        我们使用的智能手机除了有一个可用的空间(如苹果8G、16G等),还有一个RAM容量,很多人都不是很清楚,为什么需要2个这样的芯片做存储呢,这就是我们下面要讲到的。这2种存储设备我们都统称为“FLASH”,FLASH是一种存储芯片,全名叫Flash EEPROM emory,通地过程序可以修改数据,即平时所说的“闪存”。Flash又分为NAND flash和NOR flash二种。

        许多业内人士也搞不清楚NAND闪存技术相对于NOR技术的优越之处,因为大多数情况下闪存只是用来存储少量的代码,这时NOR闪存更适合一些。而NAND则是高数据存储密度的理想解决方案。NOR Flash 的读取和我们常见的 SDRAM 的读取是一样,用户可以直接运行装载在 NOR Flash里面的代码,这样可以减少 SRAM 的容量从而节约了成本。 NAND Flash 没有采取内存的随机读取技术,它的读取是以一次读取一块的形式来进行的, 通常是一次读取 512 个字节,采用这种技术的 Flash 比较廉价。用户不能直接运行NAND Flash 上的代码,因此好多使用 NAND Flash 的开发板除了使用NAND Flash以外,还用了 一块小的 NOR Flash 来运行启动代码。

        NOR flash是intel公司1988年开发出了NOR flash技术。NOR的特点是芯片内执行(XIP, eXecute In Place),这样应用程序可以直接在flash闪存内运行,不必再把代码读到系统RAM中。NOR的传输效率很高,在1~4MB的小容量时具有很高的成本效益,但是很低的写入和擦除 速度大大影响了它的性能。

不同点:

注意:nandflash 和 norflash 的 0 地址是不冲突的,norflash 占用 BANK 地址,而 nandflash不占用 BANK 地址,它的 0 地址是内部的。

相同点:

2.1.2 CPU、MPU、MCU、SOC、SOPC 联系与差别?

  1. CPU(Central Processing Unit),是一台计算机的运算核心和控制核心。CPU由运算器、控制器和寄存器及实现它们之间联系的数据、控制及状态的总线构成。差不多所有的CPU的运作原理可分为四个阶 段:提取(Fetch)、解码(Decode)、执行(Execute)和写回(Writeback)。 CPU从存储器或高速缓冲存储器中取出指令,放入指令寄存器,并对指令译码,并执行指令。所谓的计算机的可编程性主要是指对CPU的编程。
  2. MPU (Micro Processor Unit),叫微处理器(不是微控制器),通常代表一个功能强大的CPU(暂且 理解为增强版的CPU吧),但不是为任何已有的特定计算目的而设计的芯片。这种芯片往往是个人计算机和高端工作站的核心CPU。最常见的微处理器是Motorola的68K系列和Intel的X86系列。
  3. MCU(Micro Control Unit),叫微控制器,是指随着大规模集成电路的出现及其发展,将计算机的 CPU、RAM、ROM、定时计数器和多种I/O接口集成在一片芯片上,形成芯片级的芯片,比如51,avr这些芯片,内部除了CPU外还有RAM,ROM,可以直接加简单的外围器件(电阻,电容)就可以运行代码 了,而MPU如x86,arm这些就不能直接放代码了,它只不过是增强版的CPU,所以得添加RAM,ROM。 MCU MPU 最主要的区别就是能否直接运行代码。MCU有内部的RAM ROM,而MPU是增强版的CPU,需要添加外部RAM ROM才可以运行代码。
  4. SOC(System on Chip),指的是片上系统,MCU只是芯片级的芯片,而SOC是系统级的芯片,它既MCU(51,avr)那样有内置RAM,ROM同时又像MPU(arm)那样强大的,不单单是放简单的代码,可以放系统级的代码,也就是说可以运行操作系统(将就认为是MCU集成化与MPU强处理力各优点二合一)。
  5. SOPC(System On a Programmable Chip)可编程片上系统(FPGA就是其中一种),上面4点的硬 件配置是固化的,就是说51单片机就是51单片机,不能变为avr,而avr就是avr不是51单片机,他们的硬件是一次性掩膜成型的,能改的就是软件配置,说白点就是改代码,本来是跑流水灯的,改下代码,变成数码管,而SOPC则是硬件配置,软件配置都可以修改,软件配置跟上面一样,没什么好说的,至于硬件,是可以自己构建的也就是说这个芯片是自己构造出来的,这颗芯片我们叫“白片”,什么芯片都不是,把硬件配置信息下载进去了,他就是相应的芯片了,可以让他变成51,也可以是avr,甚至arm,同时SOPC是在SOC基础上来的,所以他也是系统级的芯片,所以记得当把他变成arm时还得加外围ROM,RAM之类的,不然就是MPU了。

2.1.3 什么是交叉编译?

       在一种计算机环境中运行的编译程序,能编译出在另外一种环境下运行的代码,我们就称这种编译器支持交叉编译。这个编译过程就叫交叉编译。简单地说,就是在一个平台上生成另一个平台上的可执行代码。

      这里需要注意的是所谓平台,实际上包含两个概念:体系结构(Architecture)、操作系统(OperatingSystem)。同一个体系结构可以运行不同的操作系统;同样,同一个操作系统也可以在不同的体系结构上运行。举例来说,我们常说的x86 Linux平台实际上是Intel x86体系结构和Linux for x86操作系统的统称;而x86 WinNT平台实际上是Intel x86体系结构和Windows NT for x86操作系统的简称。

2.1.4  为什么需要交叉编译?

       有时是因为目的平台上不允许或不能够安装我们所需要的编译器,而我们又需要这个编译器的某些特征;有时是因为目的平台上的资源贫乏,无法运行我们所需要编译器;有时又是因为目的平台还没有建立,连操作系统都没有,根本谈不上运行什么编译器。

2.1.5 ROM和RAM的区别及类型

页(Page)< 扇区(Sector) < 块(Block)< 芯片(Chip)

一般1页是1k

ROM为只读存储器,RAM为读写存储器。

       存储器按其存储介质特性主要分为“易失性存储器”和“非易失性存储器”两大类。

       其中的“易失/非易失”是指存储器断电后,它存储的数据内容是否会丢失的特性。由于一般易失性存储器存取速度快,而非易失性存储器可长期保存数据,它们都在计算机中占据着重要角色。

      在计算机中易失性存储器最典型的代表是内存,非易失性存储器的代表则是硬盘。

关于RAM中,DRAM和SRAM对比:

       DRAM中代表1的电容会放电,因此需要定时刷新,刷新操作会对电容电量进行检查。而DRAM本身分为同步和异步两种通讯方式,其中同步通讯方式速度更快,应用广泛,同步通讯的方式叫做SDRAM。

       SRAM利用锁存器去存储数据,不需要定时刷新充电。所以被称为静态RAM,(static RAM)。而SRAM本身也分为同步和异步通讯,相对而言,异步SRAM用的比较广泛。

          对比 DRAM 与 SRAM 的结构,可知 DRAM 的结构简单得多,所以生产相同容量的存储器,DRAM的成本要更低,且集成度更高。而 DRAM 中的电容结构则决定了它的存取速度不如 SRAM,特性对比见表 DRAM 与 SRAM 对比。

       所以在实际应用场合中,SRAM 一般只用于 CPU 内部的高速缓存 (Cache),而外部扩展的内存一般使用 DRAM。在 STM32 系统的控制器中,只有STM32F429 型号或更高级的芯片才支持扩展SDRAM,其它型号如STM32F1、STM32F2 及 STM32F407 等型号只能扩展 SRAM。

关于ROM中:

       ROM中应用最广泛的是EEPROM。它可重复擦写,擦除和写入都是直接用外部设备来擦写,而且可以按字节为单位修改数据。

关于FLASH:

       FLASH 存储器又称 为闪存,它也是可重复擦写的储器,部分书籍会把FLASH 存储器称为 FLASH ROM,但它的容量一般比 EEPROM 大得多,且在擦除时,一般以多个字节为单位。如有的 FLASH 存储器以 4096 个字节为扇区,最小的擦除单位为一个扇区。

       NOR 与 NAND 的共性是在数据写入前都需要有擦除操作,而擦除操作一般是以“扇区/块”为单位的。

       而 NOR 与 NAND 特性的差别,主要是由于其内部“地址/数据线”是否分开导致的。

       由于 NOR 的地址线和数据线分开,它可以按“字节”读写数据;而由于 NAND 的数据和地址线共用,只能按“块”来读写数据。

       由于两种 FLASH 存储器特性的差异,NOR FLASH 一般应用在代码存储的场合,如嵌入式控制器内部的程序存储空间。而 NAND FLASH 一般应用在大数据量存储的场合,包括 SD 卡、U盘以及固态硬盘等,都是 NAND FLASH 类型的。

2.1.6 Cortex-M3寄存器组

2.1.7 保存、恢复现场的详细操作

       向量表从FLASH的0地址开始放置,以4个字节为一个单位,地址0存放的是栈顶地址,0X04存放的是复位程序的地址,以此类推。从代码上看,向量表中存放的都是中断服务函数的函数名,可我们知道C语言中的函数名就是一个地址

       首先是中断或异常产生,CPU去FLASH里面找对应的异常向量表,异常向量表找到对应的中断函数,执行中断函数前,要进行保护现场的操作,中断函数结束后,恢复现场。

       寄存器R13在ARM指令中常用作堆栈指针SP,但这只是一种习惯用法,用户也可使用其他的寄存器作为堆栈指针,而在Thumb指令集中,某些指令强制性的要求使用R13作为堆栈指针.

        由于处理器的每种运行模式均有自己独立的物理寄存器R13,在用户应用程序的初始化部分,一般都要初始化每种模式下的R13,使其指向该运行模式的栈空间。这样,当程序的运行进入异常模式时,可以将需要保护的寄存器放入R13所指向的堆栈,而当程序从异常模式返回时,则从对应的堆栈中恢复,采用这种方式可以保证异常发生后程序的正常执行。

        R14称为子程序链接寄存器LR(Link Register),当执行子程序调用指令(BL)时,R14可得到R15(程序计数器PC)的备份。

        保护现场:首先,将SP的值保存在IP寄存器中(此时IP中存的sp是未进行压栈的sp),然后让fp ip lr pc按照从右向左顺序入栈,入栈的过程中,让sp随着入栈而不断增长。入栈过程中,sp不断增加,从左图高地址处sp一直移动到fp处,然后将ip-4,将sp存入fp中(相当于fp中存的是pc当前地址)。此时,fp里面存入了栈顶sp。

       恢复现场:先将fp-12,此时fp内存的地址即为当前sp所在位置,然后将fp赋值给fp,sp赋值给ip,lr赋值给pc。其中lr为函数返回地址,而pc存储的是程序要执行的下一条指令。

2.1.8 单片机启动流程

2.1.8.1 单片机基础

从图中看出,代码一般存储在ROM区,然后一些局部变量等利用SRAM来进行存储。

2.1.8.2 启动流程

单片机上电后一直到准备好C语言运行环境并跳转到main函数执行总共经历了5个步骤:

1.内核初始化;

2.强制PC指针指向中断向量表的复位中断向量执行复位中断函数;

3.在复位中断函数中调用 SystemInit 函数,初始化时钟,配置中断向量表等

4.调用 __main 函数完成全局/静态变量/常量的初始化和重定位工作,初始化堆栈和库函数

5.跳转到main函数中执行

注意:

 2.1.8.3 重定位

PC指针:pc指针是指寄存器pc,里边的值总是指向当前程序运行点的地址。

SP指针:SP指针指堆栈指针,也是通用寄存器,用于入栈和出栈操作。​

2.1.9 异常和中断

       在处理器执行到因编译 错误而导致的错误指令时,或者在执行期间出现特殊错误,必须靠内核处理的时候,处理器就会产生一个异常。

2.1.10 中断和DMA的区别

DMA:是一种无须CPU的参与,就可以让外设与系统内存之间进行双向数据传输的硬件机制,使用 DMA可以使系统CPU从实际的I/O数据传输过程中摆脱出来,从而大大提高系统的吞吐率。

中断:是指CPU在执行程序的过程中,出现了某些突发事件时,CPU必须暂停执行当前的程序,转去处理突发事件,处理完毕后CPU又返回源程序被中断的位置并继续执行。

所以中断和DMA的区别就是:DMA不需CPU参与,而中断是需要CPU参与的

2.1.11  H中断能不能睡眠,为什么?下半部能不能睡眠?

2.1.12 中断响应执行流程

       中断的响应流程:cpu接受中断->保存中断上下文跳转到中断处理历程->执行中断上半部->执行中断下半部->恢复中断上下文。

2.1.13 中断和轮询哪个效率高?怎样决定是采用中断方式还是采用轮询方式去实现驱动?

      中断是CPU处于被动状态下来接受设备的信号,而轮询是CPU主动去查询该设备是否有请求。

      凡事都是两面性,所以,看效率不能简单的说那个效率高。如果是请求设备是一个频繁请求cpu的设备,或者有大量数据请求的网络设备,那么轮询的效率是比中断高。如果是一般设备,并且该设备请求 cpu的频率比较低,则用中断效率要高一些。主要是看请求频率。

2.1.14 当一个异常出现以后,ARM微处理器会执行哪几步操作?

1. 将下一条指令的地址存入相应连接寄存器LR,以便程序在处理异常返回时能从正确的位置重新开始执行。若异常是从ARM状态进入,则LR寄存器中保存的是下一条指令的地址(当前PC+4或PC+ 8,与异常的类型有关);若异常是从Thumb状态进入,则在LR寄存器中保存当前PC的偏移量,这样,异常处理程序就不需要确定异常是从何种状态进入的。例如:在软件中断异常SWI,指令 MOV PC,R14_svc总是返回到下一条指令,不管SWI是在ARM状态执行,还是在Thumb状态执行。

2. 将CPSR复制到相应的SPSR中。

3. 根据异常类型,强制设置CPSR的运行模式位。

4. 强制PC从相关的异常向量地址取下一条指令执行,从而跳转到相应的异常处理程序处

注意:

CPSR: 程序状态寄存器(当前程序状态寄存器),在任何处理器模式下被访问

SPSR:程序状态保存寄存器(saved programstatus register),每一种处理器模式下都有一个状态寄存器SPSR,SPSR用于保存CPSR的状态,以便异常返回后恢复异常发生时的工作状态

2.1.15 ARM内核架构

1. ICode总线:ICode 中的 I 表示 Instruction,即指令。我们写好的程序编译之后都是一条条指令,存放在 FLASH 中,内核要读取这些指令来执行程序就必须通过 ICode 总线,它几乎每时每刻 都需要被使用,它是专门用来取指的。

2、DCode总线:DCode 中的 D 表示 Data,即数据,那说明这条总线是用来取数的。我们在写程序的时候,数据有常量和变量两种,常量就是固定不变的,用 C 语言中的 const 关键字修饰,是放到内部的 FLASH 当中的,变量是可变的,不管是全局变量还是局部变量都放在内部的 SRAM。因为数据可以被 Dcode 总线和 DMA 总线访问,所以为了避免访问冲突,在取数的时候需要经过一个总线矩阵来仲裁,决定哪个总线在取数。

3、系统总线:系统总线主要是访问外设的寄存器,我们通常说的寄存器编程,即读写寄存器都是通 过这根系统总线来完成的

(1)FSMC的英文全称是 Flexible static memory controller,叫灵活的静态的存储器控制器, 是 STM32F10xx 中一个很有特色的外设,通过 FSMC,我们可以扩展内存,如外部的 SRAM,NANDFLASH 和 NORFLASH。

(2)内部的闪存存储器即 FLASH,我们编写好的程序就放在这个地方。内核通过 ICode 总 线来取里面的指令。

(3)DMA总线(Direct Memory Access)即直接存储器访问。主要用来传输数据,这个数据可以是某个外设的数据寄存存器,可以在SRAM,可以在内部的FLASH。

(4)AHB到APB的桥。AHB(Advanced High performance Bus)系统总线高级高性能总线

APB(Advance Peripheral BUS)外围总线。

(5)看门狗模块(watchdog timer):定期查看芯片内部的情况,一旦发生错误向芯片发出重启信号。看门狗命令在程序的中断中拥有最高的优先级。

​2.1.16 STM32是大端还是小端?

      STM32是小端模式

2.1.17 Linux系统启动流程?

2.1.17.1 执行ROM代码

它是处理器上电以后首先执行的程序,ROM代码的主要工作就是读取 STM32MP1的BOOT引脚电平,然后根据电平判断当前启动设备,最后从选定的启动设备里面读取FSBL代码,并将FSBL代码放到对应的RAM空间。(first stage boot loader 第一阶段加载程序)

2.1.17.2 FSBL

       FSBL 代码初始化时钟树、初始化外部RAM控制器,也就是DDR。最终FSBL将SSBL加载到DDR里面并运行SSBL代码。

DDR的全称其实是DDR SDRAM。所以在介绍DDR之前,得先了解什么是SDRAM。

SDRAM(同步动态随机存取内)可以看作一种特殊的DRAM(动态随机存取内存),我们平时说的计算机内存条就是一种DRAM。

       SDRAM除了异步接口,还多了一个同步接口。同步接口需要数据发送方和接收方有一个统一的时钟信号,技术相对复杂,相应的传输速率和并发度都更高。

2.1.17.3 SSBL

由于SSBL代码运行在DDR里面,无需担心空间不够,因此SSBL代码的功能就可以做的 很全面,比如使能USB、网络、显示等等。这样我们就可以在SSBL中灵活的加载linux内核, 比如从Flash设备上读取,或者通过网络下载下载等,用户使用起来也非常的友好。SSBL一般是Uboot,用来启动Linux内核。

2.1.17.4 Linux内核

       SSBL 部分的Uboot就一个使命,启动Linux内核,Uboot会将Linux内核加载到DDR上 并运行。Linux内核启动过程中会初始化板子上的各种外设。​

2.1.17.5 Linux用户空间

       系统启动的时候会通过init 进程切换到用户空间,在这个过程中会初始化根文件系统里面 的各种框架以及服务。

2.2 通讯协议:

不同双工通讯:

同步/异步通讯:

       时钟线,有时钟线那就是同步通信,否则就是异步通信。

2.2.1 串口通讯协议总结

2.2.1.1 RS232物理层

其中RS-232提高电压后,增加了串口通信的远距离传输能力和抗干扰能力。

2.2.1.2 RS485物理层

         RS-485 通讯网络的最大传输距离可达 1200 米,总线上可挂载 128 个通讯节点,而由于 RS-485 网络只有一对差分信号线,它使用差分信号来表达逻辑,当 AB 两线间的电压差为-6V~-2V 时表示逻辑 1,当电压差为 +2V~+6V 表示逻辑 0,在同一时刻只能表达一个信号,所以它的通讯是半双工形式的。



2.2.1.3 串口通讯协议

​起始位:为低电平;

停止信号:可由0.5、1、1.5、2个逻辑1的数据位表示

位0~位7:有效数据可被约定为 5—8位长

校验位:奇校验;偶校验

2.2.1.4 波特率

       fPLCK为usart时钟,USARTDIV是存放在波特率寄存器中的一个无符号定点数。其中 DIV_Mantissa[11:0]位定义USARTDIV 的整数部分,DIV_Fraction[3:0]位定义USARTDIV 的小数部分

       我们知道USART1使用APB2总线时钟,最高可达72MHz,其他USART的最高频率为36MHz。我们选取USART1作为实例讲解,即fPLCK=72MHz。为得到 115200bps 的波特率,

 此时:​

解得USARTDIV=39.0625,可算得 DIV_Fraction=0.0625*16=1=0x01,DIV_Mantissa=39=0x27,即 应该设置 USART_BRR 的值为 0x271。

2.2.1.5 波特率与比特率的区别

波特率是指单位时间内传送二进制数据的位数,单位用bps(位/秒)表示,记作波特

       比如:波特率为9600,则1秒传送9600位,也就是960个字节。

       比如在异步串行传输系统中,若字符格式:1位起始位,8位数据位,1个校验位,1个终止位,假设波特率为1200bps。

       有效数据位8位,传送一个字符为1+8+1+1=11,比特率为:1200*(8/11)

2.2.2 I2C通讯协议总结

2.2.2.1 物理层

注意:

开漏输出:高电平无驱动能力,需要借助外部上拉输出高电平。

       因为I2C协议是支持多个主设备与多个从设备在一条总线上的,此时就会有多个GPIO口连接在同一条总线上,就势必会出现输出高、低电平不统一的情况,如果采用推挽输出时,就可能会出现某个GPIO的Vcc和GND连接在一起造成短路的情况,当你采用开漏输出时,因为有上拉电阻的存在就可以避免这一问题的出现。

总线通过上拉电阻接到电源。当I2C设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。

多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线。

SDA线在输入时配置成上下拉输入模式。

数据传送过程中,先传送最高位(MSB),每一个被传送的字节后面都必须跟随着1位应答位(即一帧共有9位长)。

2.2.2.2 协议层

(1) 主机写数据给从机

       这些图表示的是主机和从机通讯时,SDA 线的数据包序列。

        其中S表示由主机的 I2C 接口产生的传输起始信号(S),这时连接到I2C总线上的所有从机都会接收到这个信号。

    起始信号产生后,所有从机就开始等待主机紧接下来广播的从机地址信号(SLAVE_ADDRESS)。

        在 I2C 总线上,每个设备的地址都是唯一的,当主机广播的地址与某个设备地址相同时,这个设备就被选中了,没被选中的设备将会忽略之后的数据信号。

        根据 I2C 协议,这个从机地址可以是7位或10位(7位用的更加广泛)在地址位之后,是传输方向的选择位

该位为0,主机向从机写数据。

       该位为1,主机由从机读数据。

       从机接收到匹配的地址后,主机或从机会返回一个应答(ACK)或非应答(NACK)信号,只有接收到应答信号后,主机才能继续发送或接收数据。

注意:

       每次读写数据,数据包为8位,即一个字节。

       主机向从机时,主机给从机发送一个字节,从机回复一个应答信号,然后主机再发送一个字节,不断重复这个过程。当数据传输结束时,主机给从机发送停止信号(P)。

       主机向从机时,从机给主机发送一个字节,主机回复一个应答信号,然后从机再发送一个字节,不断重复这个过程。当主机不想接收数据时,主机给从机返回一个非应答信号(NACK)。

(2) 读和写数据:

        除了基本的读写,I2C通讯更常用的是复合格式,即第三幅图的情况,该传输过程有两次起始信号(S)。一般在第一次传输中,主机通过SLAVE_ADDRESS寻找到从设备后,发送一段“数据”,这段数据通常用于表示从设备内部的寄存器或存储器地址(注意区分它与 SLAVE_ADDRESS 的区别);在第二次的传输中,对该地址的内容进行读或写。也就是说,第一次通讯是告诉从机读写地址,第二次则是读写的实际内容。

2.2.2.3 通讯信号电路定义:

起始信号/终止信号(一般由主机产生)

起始信号:SCL高,SDA由高变低

终止信号:SCL高,SDA由低变高

数据有效性:

响应信号:

传输时主机产生时钟,在第9个时钟时,数据发送端会释放SDA的控制权,由数据接收端控制SDA,若SDA为高电平,表示非应答信号 (NACK),低电平表示应答信号(ACK)。

2.2.2.4 硬件I2C和软件I2C的区别

2.2.3 SPI通讯协议总结

2.2.3.1 物理层

       CS线(NSS线):从设备选择线,也称为片选信号线。哪个NSS线设为低电平,就是选中对应的模块。NSS线为高电平时,为结束信号。

       SCK:时钟信号线,用于通讯主机产生,决定了通讯的速率,STM32的SPI时钟频率最大为fPCLK/2,一般通讯过程中,通讯速率受限于低速设备。

       MOSI:主机发送,从机接收端口。

       MISO:主机接收,从机发送端口。

2.2.3.2 协议层

起始信号:NSS由高变低       停止信号:NSS由低变高

注意:

       MOSI与MISO的信号只在NSS为低电平的时候才有效,在SCK的每个时钟周期MOSI和MISO传输一位数据。数据的输入和输出是同时进行的,在SCK下降沿进行数据采样,SPI每次数据传输可以以8位或16位为单位,每次传输单位数不受限制。

2.2.3.3 SPI通讯模式(四种)

       四种通讯模式最大的区别:总线空闲时 SCK 的时钟状态以及数据采样时刻。

       因此,在此引入“时钟极性CPOL”和“时钟相位CPHA”概念。

2.2.3.4 控制逻辑

主模式收发流程及事件说明如下:

(1) 控制NSS信号线,产生起始信号(图中没有画出);

(2) 把要发送的数据写入到“数据寄存器“DR”中,该数据会被存储到发送缓冲区;

(3) 通讯开始,SCK时钟开始运行。MOSI 把发送缓冲区中的数据一位一位地传输出去;MISO 则把数据一位一位地存储进接收缓冲区中;

(4) 当发送完一帧数据的时候,“状态寄存器 SR”中的“TXE 标志位”会被置 1,表示传输完一 帧,发送缓冲区已空;类似地,当接收完一帧数据的时候,“RXNE 标志位”会被置 1,表示传输完一帧,接收缓冲区非空;

(5) 等待到“TXE 标志位”为 1 时,若还要继续发送数据,则再次往“数据寄存器 DR”写入数据 即可;等待到“RXNE 标志位”为 1 时,通过读取“数据寄存器 DR”可以获取接收缓冲区中的 内容。

       假如我们使能了 TXE 或 RXNE 中断,TXE 或 RXNE 置 1 时会产生 SPI 中断信号,进入同一个中断服务函数,到 SPI 中断服务程序后,可通过检查寄存器位来了解是哪一个事件,再分别进行处理。也可以使用 DMA 方式来收发“数据寄存器 DR”中的数据。

2.2.4 CAN通讯协议总结

2.2.4.1 物理层

(1) 闭环网络

CAN 物理层的形式主要有两种,图 CAN 闭环总线通讯网络中的 CAN 通讯网络是一种遵循 ISO11898 标准的高速、短距离“闭环网络”,它的总线最大长度为 40m,通信速度最高为 1Mbps,总线的两端各要求有一个“120欧”的电阻。

(2) 开环网络

       图 CAN 开环总线通讯网络 中的是遵循 ISO11519-2 标准的低速、远距离“开环网络”,它的最大传输距离为 1km,最高通讯速率为 125kbps,两根总线是独立的、不形成闭环,要求每根总线上 各串联有一个“2.2千欧”的电阻。

       CAN 通讯节点由一个 CAN 控制器及 CAN 收发器组成,控制器与收发器之间通过 CAN_Tx 及 CAN_Rx 信号线相连,收发器与 CAN 总线之间使用 CAN_High 及 CAN_Low 信号线相连。其中 CAN_Tx 及 CAN_Rx 使用普通的类似 TTL 逻辑信号,而 CAN_High 及 CAN_Low 是一对差分信 号线,使用比较特别的差分信号,下一小节再详细说明。

2.3 其他类型

2.3.1 DMA存储

       DMA主要是用来搬运数据,但是不经过CPU,数据直接从外设到存储器,或者从存储器到存储器,存储器可以是SRAM或者FLASH。

       DMA包含DMA1(7通道)和DMA2(5通道)。每个通道可以接收多个外设请求,但是同一时间只能接收一个,不能同时接收多个。

2.3.1.1 仲裁器

      多个DMA通道请求,由仲裁器管理。分两个阶段,先软件阶段:根据优先级设置(4个优先级)判断,如果两个通道优先级一样,则进入硬件阶段,根据通道号决定谁优先级高,通道0高于通道1。

2.3.1.2 从哪来到哪里去

(1)外设到存储器

       以ADC采集为例,DMA外设寄存器的地址对应的就是ADC数据寄存器的地址,DMA存储器的地址就是我们自定义的变量(用来接收存储AD采集的数据)的地址。方向我们设置外设为源地址。

(2)存储器到外设

(3)存储器到存储器

2.3.1.3 DMA结构体

(1) 第一个参数:DMA_PeripheralBaseAddr 用来设置 DMA 传输的外设基地址,比如要进行串口 DMA 传输,那么外设基地址为串口接受发送数据存储器 USART1->DR 的地址,表示方法为 &USART1->DR。

(2) 第二个参数:DMA_MemoryBaseAddr为内存基地址,也就是我们存放DMA传输数据的内存地址。

(3) 第三个参数:DMA_DIR 设置数据传输方向,决定是从外设读取数据到内存还送从内存读取数据发送到外设,也就是外设是源地还是目的地,这里我们设置为从内存读取数据发送到串口,所以外设自然就是目的地了,所以选择值为 DMA_DIR_PeripheralDST。

(4) 第四个参数:DMA_BufferSize 设置一次传输数据量的大小,这个很容易理解。

(5) 第五个参数:DMA_PeripheralInc 设置传输数据的时候外设地址是不变还是递增。如果设置 为递增,那么下一次传输的时候地址加1,这里因为我们是一直往固定外设地址&USART1->DR发送数据,所以地址不递增,值为DMA_PeripheralInc_Disable。

(6) 第六个参数:DMA_MemoryInc 设置传输数据时候内存地址是否递增。这个参数和 DMA_PeripheralInc意思接近,只不过针对的是内存。这里我们的场景是将内存中连续存储单元的数据发送到串口,毫无疑问内存地址是需要递增的,所以值为 DMA_MemoryInc_Enable。

(7) 第七个参数:DMA_PeripheralDataSize 用来设置外设的数据长度是为字节传输(8bits),半 字传输(16bits)还是字传输(32bits),这里我们是8位字节传输,所以值设置为DMA_PeripheralDataSize_Byte。

(8) 第八个参数:DMA_MemoryDataSize是用来设置内存的数据长度,和第七个参数意思接近,这里我们同样设置为字节传输 DMA_MemoryDataSize_Byte。

2.3.2 SysTick部分总结

SysTick优先级和片上外设优先级

2.3.3 什么是哈佛结构和冯诺依曼结构?

       冯诺依曼结构主要用于通用计算机领域,需要对存储器中的代码和数据频繁的进行修改,统一编址有利于节约资源。

       哈佛结构主要用于嵌入式计算机,程序固化在硬件中,有较高的可靠性、运算速度和较大的吞吐。

2.3.4 ARM有几种工作模式?

2.3.4.1 用户模式(USR)

        用户模式是用户程序的工作模式,它运行在操作系统的用户态,它没有权限去操作其它硬件资源,只能执行处理自己的数据,也不能切换到其它模式下,要想访问硬件资源或切换到其它模式只能通过软中断或产生异常

2.3.4.2系统模式(SYS)

       系统模式是特权模式,不受用户模式的限制。用户模式和系统模式共用一套寄存器,操作系统在该模式下可以方便的访问用户模式的寄存器,而且操作系统的一些特权任务可以使用这个模式访问一些受控的资源

2.3.4.3 一般中断模式(IRQ)

       一般中断模式也叫普通中断模式,用于处理一般的中断请求,通常在硬件产生中断信号之后自动进 入该模式,该模式为特权模式,可以自由访问系统硬件资源。

2.3.4.4 快速中断模式(FIQ)

       快速中断模式是相对一般中断模式而言的,它是用来处理对时间要求比较紧急的中断请求,主要用于高速数据传输及通道处理中。

2.3.4.5 管理模式(SVC)

       管理模式是CPU上电后默认模式,因此,在该模式下主要用来做系统的初始化,软中断处理也在该模式下。当用户模式下的用户程序请求使用硬件资源时,通过软件中断进入该模式。

2.3.4.6 终止模式(ABT)

       中止模式用于支持虚拟内存或存储器保护,当用户程序访问非法地址,没有权限读取的内存地址时,会进入该模式,linux下编程时经常出现的segment fault通常都是在该模式下抛出返回的。

2.3.4.7 未定义模式(UND)

2.3.5 ARM指令集分为几类?

       2类,分别为Thumb指令集,ARM指令集。ARM指令长度为32位,Thumb指令长度为16位。这种特点使得ARM既能执行16位指令,又能执行32位指令,从而增强了ARM内核的功能。

3. RTOS

3.1 内存管理

3.1.1 关于heap_1.c

        它只实现了 pvPortMalloc,没有实现 vPortFree。 如果你的程序不需要删除内核对象,那么可以使用 heap_1。FreeRTOS 在创建任务时,需要 2 个内核对象:task control block(TCB)、stack。(TCB任务控制块和栈)。

3.2 RTOS移植与中断管理

1、修改sys.h文件,让它支持OS。

2、修改usart文件,更改中断。在uC/OS的时候,进入和退出中断需要添加OSIntEnter()和OSIntExit()两个函数,然后在FreeRTOS中并没有该机制,所以将这里的代码删除。

3、关于delay函数的修改,FreeRTOS中使用SysTick作为作为操作系统的心跳,所以需要将xPortSysTickHandler()添加,作为系统始终中断。

4、delay_init() 用于初始化SysTick,主要修改SysTick的重装载值,修改delay_ms和delay_us函数。

5、修改中断(SysTick中断、SVC中断、PendSV中断)。其中SysTick中断在delay.c文件中已经定义。

3.3 Systick中断和PendSV中断

       在Cortex-M内核上,FreeRTOS使用Systick定时器作为心跳时钟,一般默认心跳时钟为1ms,进入Systick中断后,内核会进入处理模式进行处理,在Systick中断处理中,系统会在 ReadList 就绪链表从高优先级到低优先找需要执行的任务,进行调度,如果有任务的状态发生了变化,改变了状态链表,就会产生一个pendSV异常,进入pendSV异常,通过改变进程栈指针(PSP)切换到不同的任务。

 对于相同优先级的任务,每隔一个Systick,运行过的任务被自动排放至该优先级链表的尾部(时间片调度)。

3.4 任务管理

       vTaskDelay()为相对延时函数,可以让任务进入阻塞状态。    

空闲任务

       FreeRTOS程序在任意时刻,必须至少有一个任务处于运行状态,为了达到这个要求,FreeRTOS使用了Idle任务:当vTaskStartScheduler调用后,调度器会自动创建Idle任务,这个任务的任务函数就是一个连续性工作的任务,所以他总是可以处于就绪态(在运行态和就绪态之间转换,没有其他状态)。由于Idle任务的优先级是最低的(优先级为0),所以Idle任务不会抢占用户任务的运行。当其他高优先级的任务需要运行时,他们会抢占Idle任务。

       Idle任务主要用于资源回收清理工作,例如当你在程序中删除一个任务后,就需要Idle任务去清理这个任务占用的资源。因此,不要让Idle任务“饿死”,具体而言,不要创建一个优先级比Idle任务优先级高,且连续性工作的任务。如果应用程序也需要一个在背后连续工作的任务,则应该设置其优先级和Idle任务相同。当然这个需求更好的实现方法是通过下面介绍的Idle钩子来完成。

4. Linux驱动

4.1 Linux系统移植流程

4.2 程序编译过程

5. 网络通信

5.1 三次握手

1、首先 Client 端发送连接请求报文

2、Server 段接受连接后回复 ACK 报文,并为这次连接分配资源。

3、Client 端接收到 ACK 报文后也向 Server 段发生 ACK 报文,并分配资源,这样 TCP 连接就建立了。

5.2 四次挥手

1、第一次挥手:Clien发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。

2、第二次挥手:Server收到FIN后,发送一个ACK给Client, Server进入CLOSE_WAIT状态。

3、第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。

4、第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,发送ACK给Server,Server进入CLOSED状态,完成四次挥手。

5.3 Linux下TCP的程序构建流程

5.4 TCP与UDP的区别

1、TCP面向连接,通过三次握手建立连接,四次挥手接除连接;UDP是无连接的,即发送数据之前不需要建立连接,这种方式为UDP带来了高效的传输效率,但也导致无法确保数据的发送成功。

2、TCP是可靠的通信方式。通过TCP连接传送的数据,TCP通过超时重传、 数据校验等方式来确保数据无差错,不丢失,不重复,且按序到达;而UDP由于无需连接的原因,将会以最大速度进行传输,但不保证可靠交付,也就是会出现丢失、重复等等问题。

3、TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流,由于连接的问题,当网络出现波动时,连接可能出现响应问题;UDP是面向报文的,UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低。

4、每一条TCP连接只能是点到点的;而UDP不建立连接,所以可以支持一对一,一对多,多对一和多对多的交互通信,也就是可以同时接受多个人的包。

5、TCP需要建立连接,首部开销20字节相比8个字节的UDP显得比较大。

6、TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道。

6. 操作系统

6.1 什么是进程?什么是线程?

       进程是资源分配的基本单位,它是程序执行时的一个实例,在程序运行时创建。

       线程是程序执行的最小单位,是进程的一个执行流,一个进程由多个线程组成的。

       协程是一种比线程更加轻量级的存在。一个线程也可以拥有多个协程。其执行过程更类似于子例程,或者说不带返回值的函数调用。

6.2 内核线程和用户线程的区别?

1. 内核支持线程是OS内核可感知的,而用户级线程是OS内核不可感知的。

2. 用户级线程的创建、撤消和调度不需要OS内核的支持,是在语言(如Java)这一级处理的;而内核支持线程的创建、撤消和调度都需OS内核提供支持,而且与进程的创建、撤消和调度大体是相同的。

3. 用户级线程执行系统调用指令时将导致其所属进程被中断,而内核支持线程执行系统调用指令时,只导致该线程被中断。

4. 在只有用户级线程的系统内,CPU调度还是以进程为单位,处于运行状态的进程中的多个线程,由用户程序控制线程的轮换运行;在有内核支持线程的系统内,CPU调度则以线程为单位,由OS的线程调度程序负责线程的调度。

5. 用户级线程的程序实体是运行在用户态下的程序,而内核支持线程的程序实体则是可以运行在任何状态下的程序。

6.3 进程和线程有什么区别?

1、根本区别

       进程是资源分配的基本单位,线程是程序执行的最小单位

2、资源开销

       进程有自己独立地址空间(代码空间和数据空间),每启动一个进程,系统会为它分配地址空间。

       线程没有自己独立的地址空间,线程共享进程中的数据,使用相同的地址空间。每个线程都有自己的堆栈。

       线程切换的资源开销要比进程小。涉及频繁切换,就选择线程。

       线程开销小,但是不利于进行资源包含;进程开销大,但是有利于资源保护。

3、关于通信

线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。(ftok函数)

但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也跟着死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。

4、执行过程

       每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

6.4 何时使用多进程,何时使用多线程?

       对资源的管理和保护要求高,不限制开销和效率时,使用多进程。

       要求效率高,频繁切换时,资源的保护管理要求不是很高时,使用多线程。

6.5 进程的几种状态?

​关于IO:

       用户程序进行IO的读写,会用到read&write两大系统调用。read系统调用,是把数据从内核缓冲区复制到进程缓冲区;而write系统调用,是把数据从进程缓冲区复制到内核缓冲区

 同步IO

是指用户空间线程是主动发起IO请求的一方,内核空间是被动接受方。

异步IO

是指内核kernel是主动发起IO请求的一方,用户线程是被动接受方。

6.6 进程创建的方式?

6.6.1 使用fork创建

       创建新进程在Linux的下是由父进程来完成的,创建完成的新进程是子进程。

       新进程的地址空间有两种可能性:

1、子进程是父进程的复制品(除了PID和task_struct是子进程自己的,其余的都从父进程复制而来

2、子进程装入另一个程序。

在Linux下的fork函数用于创建一个新的进程,使用fork函数来创建一个进程时,子进程只是完全复制父进程的资源。

6.6.2 fork和vfork的区别?

       void exit( int status) 结束当前进程并将status返回;exit结束进程会刷新缓冲区

       void _exit(int status)  这个不会进行缓冲区刷新

1. fork( )的子进程拷贝父进程的数据段和代码段;vfork( )的子进程与父进程共享数据段

2. fork( )的父子进程的执行次序不确定;vfork( )保证子进程先运行,在调用exec或exit之前与父进程数据是共享的,在它调用exec或exit之后父进程才可能被调度运行。

3. vfork( )保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。

4. 当需要改变共享数据段中变量的值,则拷贝父进程。

6.7 进程间通信方式有哪些?有什么优缺点?

6.7.1 管道(pipe)

       管道这种通讯方式有两种限制:

       一是半双工的通信,数据只能单向流动;

       二是只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

       缺点:速度慢,容量有限,只有父子进程能通讯。

6.7.2 流管道 s_pipe

       可以进行双向传输(全双工);允许具有亲缘关系进程间通信;也允许无亲缘关系进程间通信。

6.7.3 信号量

       信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

       缺点:只能用来同步,不能用来进行传递复杂信息。

6.7.4 消息队列

       容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题,消息队列可以不再局限于父子进程,而允许任意进程通过共享消息队列来实现进程间通信,并由系统调用函数来实现消息 发送和接收之间的同步,从而使得用户在使用消息缓冲进行通信时不再需要考虑同步问题,使用方便, 但是信息的复制需要额外消耗CPU的时间,不适宜于信息量大或操作频繁的场合。此种方法不太常用。

6.7.5 共享内存

       利用内存缓冲区直接交换信息,无须复制,快捷、信息量大是其优点。共享内存块提供了在任意数量的 进程之间进行高效双向通信的机制。每个使用者都可以读取写入数据,但是所有程序之间必须达成并遵 守一定的协议,以防止诸如在读取信息之前覆写内存空间等竞争状态的出现。

6.8 线程创建方式

6.9 僵尸进程、孤儿进程、守护进程是什么?

       僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。

       孤儿进程:父进程异常结束,子进程被进程Init所收养。

       守护进程:创建守护进程时,有意把父进程结束,然后被1号进程init收养

       区分:一个正常运行的子进程,如果此刻子进程退出,父进程没有及时调用wait或waitpid收回子进程的系统资源,该进程就是僵尸进程,如果系统收回了,就是正常退出,如果一个正常运行的子进程,父进程退出了但是子进程还在,该进程此刻是孤儿进程,被init收养,如果父进程是故意被杀掉,子进程做相应处理后就是守护进程

6.10 僵尸进程的危害?

        在进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息(包括进程号PID,退出状态,运行时间等)。直到父进程通过wait / waitpid 来取时才释放。 如果进程不调用 wait / waitpid 的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。

6.11 如何清理僵尸进程?

       当系统中出现了僵尸进程时,我们是无法通过 kill 命令把它清除掉的。但是我们可以杀死它的父进程, 让它变成孤儿进程,并进一步被系统中管理孤儿进程的进程收养并清理。  

本文标签: 嵌入式软件工程师详细