嵌入式面经

嵌入式面经

    • 1.关键字static的作用是什么
    • 2.关键字const是什么含意?
    • 3.const和宏定义的区别
    • 4.关键字volatile有什么含意 并给出三个不同的例子。
    • 5.引用和指针有什么区别
    • 6. .h头文件中的ifndef/define/endif 的作用?
    • 7.描述实时系统的基本特性
    • 8.全局变量和局部变量的区别
    • 9.全局变量和静态全局变量的区别
    • 9.1.static函数与普通函数
    • 10.什么是平衡二叉树?
    • 11.堆栈溢出一般是由什么原因导致的?
    • 12.什么函数不能声明为虚函数?
    • 13.不能做switch()的参数类型
    • 14.程序的内存分配
    • 14.1堆与栈的区别
    • 14.2描述内存分配方式以及它们的区别
    • 14.3 malloc和new的区别是什么?
    • 15.进程与线程的区别
    • 15.1多进程和多线程的区别
    • 15.2 信号量
    • 16. 什么是预编译,何时需要预编译
    • 17. 三种基本的数据模型
    • 18. 简述数组与指针的区别?
    • 19.位操作
    • 20.访问固定的内存位置(Accessing fixed memory locations)
    • 21.中断与异常的区别
    • 22.变量的定义总结
    • 23. 为什么要使用宏,宏有什么优缺点?
    • 23.1 内联函数及与宏的区别
    • 24. bootloader
    • 25. MCU启动过程
    • 26. Arm体系结构
    • 27. 什么是嵌入式?
    • 28. 进程与线程中的通信方式
    • 29. 如何将PC上的程序移植到嵌入式系统上,需要注意些什么?
    • 30 . 设计一种通信方式,从一台主机向另外一台主机传递数据,那么应该怎么选择。
    • 31. FreeRTOS之全配置项详解、裁剪(FreeRTOSConfig.h)
    • 32. DMA为什么能提高效率?
    • 33.优先级反转以及解决方法
    • 34. 信号量及信号量与自旋锁的区别
    • 35. strcpy和strncpy的缺陷
    • 36. sizeof与strlen有以下区别
    • 37. 哈希函数及哈希冲突的定义
    • 37.1 哈希函数的构造方法
    • 37.2 处理哈希冲突的几种方法

1.关键字static的作用是什么

全局静态变量
在全局变量前加上关键字static,全局变量就定义成一个全局静态变量.
静态存储区,在整个程序运行期间一直存在。
初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);
作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。

局部静态变量
在局部变量之前加上关键字static,局部变量就成为一个局部静态变量。
内存中的位置:静态存储区
初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);
作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;

静态函数
在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。
函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突;
warning:不要再头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰;

类的静态成员
在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用

类的静态函数
静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。
在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);

2.关键字const是什么含意?

答:我只要一听到被面试者说:“const意味着常数”,我就知道我正在和一个业余者打交道。去年Dan Saks已经在他的文章里完全概括了const的所有用法,因此ESP(译者:Embedded Systems Programming)的每一位读者应该非常熟悉const能做什么和不能做什么.如果你从没有读到那篇文章,只要能说出const意味着“只读”就可以了。尽管这个答案不是完全的答案,但我接受它作为一个正确的答案。(如果你想知道更详细的答案,仔细读一下Saks的文章吧。)如果应试者能正确回答这个问题,我将问他一个附加的问题:下面的声明都是什么意思?

const int a;
int const a;
const int *a;
int * const a;
int const * a const;

前两个的作用是一样,a是一个常整型数。
第三个意味着a是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以)。
第四个意思a是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。
最后一个意味着a是一个指向常整型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)。
如果应试者能正确回答这些问题,那么他就给我留下了一个好印象。顺带提一句,也许你可能会问,即使不用关键字 const,也还是能很容易写出功能正确的程序,那么我为什么还要如此看重关键字const呢?我也如下的几下理由:

(1)const可以将一个对象变成一个常量,不可被修改,所以定义的时候必须进行初始化
(2)可以修饰函数的参数、返回值、甚至函数的定义体。被const修改时的东西受到强制保护,可以预防意外的变动,提高程序的健壮性。

3.const和宏定义的区别

编译器处理不同
宏定义是一个“编译时”概念,在预处理阶段展开(在编译时把所有用到宏定义值的地方用宏定义常量替换),不能对宏定义进行调试,生命周期结束于编译时期;
const常量是一个“运行时”概念,在程序运行使用,类似于一个只读行数据

存储方式不同
宏定义是直接替换,不会分配内存,存储与程序的代码段中;
const常量需要进行内存分配

类型和安全检查不同
宏定义是字符替换,没有数据类型的区别,同时这种替换没有类型安全检查,可能产生边际效应等错误;
const常量是常量的声明,有类型区别,需要在编译阶段进行类型检查

4.关键字volatile有什么含意 并给出三个不同的例子。

volatile原理:

Volatile意思是“易变的”,应该解释为“直接存取原始内存地址”比较合适。 “易变”是因为外在因素引起的,像多线程,中断等;

C语言书籍这样定义volatile关键字:volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,告诉编译器对该变量不做优化,都会直接从变量内存地址中读取数据,从而可以提供对特殊地址的稳定访问。。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。(简洁的说就是:volatile关键词影响编译器编译的结果,用volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,以免出错)

下面是volatile变量的几个例子:

1). 并行设备的硬件寄存器(如:状态寄存器)
2). 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3). 多线程应用中被几个任务共享的变量
回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。嵌入式系统程序员经常同硬件、中断、RTOS等等打交道,所用这些都要求volatile变量。不懂得volatile内容将会带来灾难。

几个问题:
  1)一个参数既可以是const还可以是volatile吗?
  可以的,例如只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
  2) 一个指针可以是volatile 吗?
  可以,当一个中服务子程序修改一个指向buffer的指针时。
  3). 下面的函数有什么错误:

  int square(volatile int *ptr)
{
return *ptr * *ptr;
}

这段代码的目的是用来返指针ptr指向值的平方,但是,由于ptr指向一个volatile型参数,编译器将产生类似下面的代码:

int square(volatile int *ptr)
{
int a,b;
a = *ptr;
b = *ptr;
return a * b;
}

由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:

long square(volatile int *ptr)
{
int a;
a = *ptr;
return a * a;
}

注意:频繁地使用volatile很可能会增加代码尺寸和降低性能,因此要合理的使用volatile。

5.引用和指针有什么区别

1)引用必须被初始化,指针不必。
2)引用初始化以后不能被改变,指针可以改变所指的对象。
3)不存在指向空值的引用,但是存在指向空值的指针。

6. .h头文件中的ifndef/define/endif 的作用?

防止该头文件被重复引用。

7.描述实时系统的基本特性

在特定时间内完成特定的任务,实时性与可靠性。

8.全局变量和局部变量的区别

1.全局变量储存在静态数据区,局部变量在堆栈中。
2.全局变量的生命周期是整个函数区间,局部变量的生命周期是声明该变量的函数区间。

9.全局变量和静态全局变量的区别

1.全局变量和静态全局变量都存储在静态数据区
2.全局变量的作用域是整个函数,静态全局变量的作用域是**声明该变量的模块*
3.static全局变量只初使化一次*

9.1.static函数与普通函数

static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝
问:A.c 和B.c两个c文件中使用了两个相同名字的static变量,编译的时候会不会有问题?这两个static变量会保存到哪里(栈还是堆或者其他的)?
答:static的全局变量,表明这个变量仅在本模块中有意义,不会影响其他模块。
他们都放在数据区,但是编译器对他们的命名是不同的。
如果要使变量在其他模块也有意义的话,需要使用extern关键字。

10.什么是平衡二叉树?

左右子树都是平衡二叉树 且左右子树的深度差值的绝对值不大于1。

11.堆栈溢出一般是由什么原因导致的?

1.没有回收垃圾资源
2.层次太深的递归调用

12.什么函数不能声明为虚函数?

1.只有类的成员函数才能说明为虚函数
2.静态成员函数不能是虚函数
3.内联函数不能为虚函数
4.构造函数不能是虚函数
5.析构函数可以是虚函数,而且通常声明为虚函数

13.不能做switch()的参数类型

不支持float,double,string

14.程序的内存分配

一个由c/C++编译的程序占用的内存分为以下几个部分:
1、栈区(stack)—由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap)—一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表
3、全局区(静态区)(static)—全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。
4、文字常量区—常量字符串就是放在这里的。程序结束后由系统释放。
5、程序代码区—存放函数体的二进制代码

例子程序
//main.cpp
  int a=0;    //全局初始化区
  char *p1;   //全局未初始化区
  main()
  {
   intb;             //栈
   char s[]="abc";   //栈
   char *p2;         //栈
   char *p3="123456";   //123456\0在常量区,p3在栈上。
   static int c=0//全局(静态)初始化区
   p1 = (char*)malloc(10);
   p2 = (char*)malloc(20);   //分配得来得10和20字节的区域就在堆区。
   strcpy(p1,"123456");   //123456\0放在常量区,编译器可能会将它与p3所向"123456"优化成一个地方。
}

14.1堆与栈的区别

(1)管理方式:堆中资源由程序员控制(通过malloc/free、new/delete,容易产生memory leak),栈资源由编译器自动管理。
(2)系统响应:对于堆,系统有一个记录空闲内存地址的链表,当系统收到程序申请时,遍历该链表,寻找第一个大于所申请空间的空间的堆结点,删除空闲结点链表中的该结点,并将该结点空间分配给程序(大多数系统会在这块内存空间首地址记录本次分配的大小,这样delete才能正确释放本内存空间,另外,系统会将多余的部分重新放入空闲链表中)。对于栈,只要栈的剩余空间大于所申请空间,系统就会为程序分配内存,否则报异常出现栈空间溢出错误。
(3)空间大小:堆是不连续的内存区域(因为系统是用链表来存储空闲内存地址的,自然不是连续),堆的大小受限于计算机系统中有效的虚拟内存(32位机器上理论上是4G大小),所以堆的空间比较灵活,比较大。栈是一块连续的内存区域,大小是操作系统预定好的,windows下栈大小是2M(也有是1M,在编译时确定,VC中可设置)。
(4)碎片问题:对于堆,频繁的new/delete会造成大量内存碎片,降低程序效率。对于栈,它是一个先进后出(first-in-last-out)的结构,进出一一对应,不会产生碎片。
(5)生长方向:堆向上,向高地址方向增长;栈向下,向低地址方向增长。
(6)分配方式:堆是动态分配(没有静态分配的堆)。栈有静态分配和动态分配,静态分配由编译器完成(如函数局部变量),动态分配由alloca函数分配,但栈的动态分配资源由编译器自动释放,无需程序员实现。
(7)分配效率:堆由C/C++函数库提供,机制很复杂,因此堆的效率比栈低很多。栈是机器系统提供的数据结构,计算机在底层对栈提供支持,分配专门的寄存器存放栈地址,提供栈操作专门的指令。

14.2描述内存分配方式以及它们的区别

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

14.3 malloc和new的区别是什么?

属性:
new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持。
特征:
嵌入式面经_第1张图片

15.进程与线程的区别

进程:一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。进程是拥有资源的基本单位,线程是调度和分配的基本单位线程和进程都可以并发执行。

线程:线程是进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。

根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位

资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的

影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行

15.1多进程和多线程的区别

多进程:操作系统中同时运行的多个程序

多线程:在同一个进程中同时运行的多个任务

举个例子,多线程下载软件,可以同时运行多个线程,但是通过程序运行的结果发现,每一次结果都不一致。 因为多线程存在一个特性:随机性。造成的原因:CPU在瞬间不断切换去处理各个线程而导致的,可以理解成多个线程在抢CPU资源。
嵌入式面经_第2张图片
多线程并不能提高运行速度,但可以提高运行效率,让CPU的使用率更高。但是如果多线程有安全问题或出现频繁的上下文切换时,运算速度可能反而更低。

15.2 信号量

信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。

16. 什么是预编译,何时需要预编译

预编译又称为预处理,是做些代码文本的替换工作。处理#开头的指令,比如拷贝#include包含的文件代码,#define宏定义的替换,条件编译等,就是为编译做的预备工作的阶段,主要处理#开始的预编译指令,预编译指令指示了在程序正式编译前就由编译器进行的操作,可以放在程序中的任何位置。

c编译系统在对程序进行通常的编译之前,先进行预处理。c提供的预处理功能主要有以下三种:1)宏定义 2)文件包含 3)条件编译

1、 总是使用不经常改动的大型代码体。

2、程序由多个模块组成,所有模块都使用一组标准的包含文件和相同的编译选项。在这种情况下,可以将所有包含文件预编译为一个预编译头。

17. 三种基本的数据模型

按照数据结构类型的不同,将数据模型划分为层次模型网状模型关系模型

18. 简述数组与指针的区别?

数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。指针可以随时指向任意类型的内存块。
(1)修改内容上的差别

char a[] = “hello”;
a[0] = ‘X’;
char *p = “world”; // 注意p 指向常量字符串
p[0] = ‘X’; // 编译器不能发现该错误,运行时错误

(2) 用运算符sizeof 可以计算出数组的容量(字节数)。sizeof§ 为指针得到的是一个 指针变量的字节数,而不是p所指的内存容量。C++/C 语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。

char a[] = “hello world”;
char *p = a;
cout<< sizeof(a) << endl; // 12 字节
cout<< sizeof(p)  << endl; // 4 字节
计算数组和指针的内存容量
void Func(char a[100])
{
cout<< sizeof(a) << endl; // 4 字节而不是100 字节
}

19.位操作

嵌入式系统总是要用户对变量或寄存器进行位操作。给定一个整型变量a,写两段代码,第一个设置a的bit 3,第二个清除a 的bit 3。在以上两个操作中,要保持其它位不变。
对这个问题有三种基本的反应
1)不知道如何下手。该被面者从没做过任何嵌入式系统的工作。
2) 用bit fields(字段)。Bit fields是被扔到C语言死角的东西,它保证你的代码在不同编译器之间是不可移植的,同时也保证了的你的代码是不可重用的。我最近不幸看到 Infineon为其较复杂的通信芯片写的驱动程序,它用到了bit fields因此完全对我无用,因为我的编译器用其它的方式来实现bit fields的。从道德讲:永远不要让一个非嵌入式的家伙粘实际硬件的边。
3) 用 #defines 和 bit masks 操作。这是一个有极高可移植性的方法,是应该被用到的方法。最佳的解决方案如下:

#define BIT3 (0x1 << 3)
static int a;
void set_bit3(void)
{
a |= BIT3;
}
void clear_bit3(void)
{
a &= ~BIT3;
}

一些人喜欢为设置和清除值而定义一个掩码同时定义一些说明常数,这也是可以接受的。我希望看到几个要点:说明常数、|=和&=~操作。

20.访问固定的内存位置(Accessing fixed memory locations)

嵌入式系统经常具有要求程序员去访问某特定的内存位置的特点。在某工程中,要求设置一绝对地址为0x67a9的整型变量的值为0xaa66。编译器是一个纯粹的ANSI编译器。写代码去完成这一任务。
这一问题测试你是否知道为了访问一绝对地址把一个整型数强制转换(typecast)为一指针是合法的。这一问题的实现方式随着个人风格不同而不同。典型的类似代码如下:
int *ptr;
ptr = (int *)0x67a9;//先取出地址de值付给指针
*ptr = 0xaa66; //再把指针解引用
A more obscure approach is:
一个较晦涩的方法是:
*(int * const)(0x67a9) = 0xaa55;
即使你的品味更接近第二种方案,但我建议你在面试时使用第一种方案。

21.中断与异常的区别

中断是指 CPU 对系统发生某事件时的这样一种响应:
CPU 暂停正在执行的程序,在保留现场后自动地转去执行该事件的中断处理程序;执行完后,再返回到原程序的断点处继续执行。
还可进一步把中断分为外中断和内中断:

外中断——就是我们指的中断——是指由于外部设备事件所引起的中断,
如通常的磁盘中断、打印机中断等;

内中断——就是异常——是指由于 CPU 内部事件所引起的中断,
如程序出错(非法指令、地址越界)。
内中断(trap)也被译为“捕获”或“陷入”。

异常是由于执行了现行指令所引起的。由于系统调用引起的中断属于异常。
中断则是由于系统中某事件引起的,该事件与现行指令无关。

相同点:都是CPU对系统发生的某个事情做出的一种反应。
 区别:中断由外因引起,异常由CPU本身原因引起。

22.变量的定义总结

a) 一个整型数(An integer)
b) 一个指向整型数的指针(A pointer to an integer)
c) 一个指向指针的的指针,它指向的指针是指向一个整型数(A pointer to a pointer to an integer)
d) 一个有10个整型数的数组(An array of 10 integers)
e) 一个有10个指针的数组,该指针是指向一个整型数的(An array of 10 pointers to integers)
f) 一个指向有10个整型数数组的指针(A pointer to an array of 10 integers)
g) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数(A pointer to a function that takes an integer as an argument and returns an integer)
h) 一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数( An array of ten pointers to functions that take an integerargument and return an integer )

答案是:

a) int a; // An integer
b) int *a; // A pointer to an integer
c) int **a; // A pointer to a pointer to an integer
d) int a[10]; // An array of 10 integers
e) int *a[10]; // An array of 10 pointers to integers
f) int (*a)[10]; // A pointer to an array of 10 integers
g) int (*a)(int); // A pointer to a function a that takes an integer argument and returns an integer
h) int (*a[10])(int); // An array of 10 pointers to functions that take an integer argument and return an integer

23. 为什么要使用宏,宏有什么优缺点?

优点:因为函数的调用必须要将程序执行的顺序转移到函数所存放在内存中的某个地址,将函数的内容执行完后再返回到转去执行该函数前的地方。这种转移操作要求在转去执行前要保存现场并记忆执行的地址,转回后要恢复现场,并按原来保存地址继续执行。因此函数调用要有一定的时间和空间方面的开销,于是将影响其效率。而宏只是在预处理的地方将代码展开,不需要额外的空间和时间方面的开销,所以调用一个宏比调用一个函数更有效率。
缺点:宏的二意性

23.1 内联函数及与宏的区别

宏是由预处理器对宏进行替换的,而内联函数是通过编译器控制实现的。
而且内联函数是真正的函数,只是在需要用到的时候内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。所以可以像调用函数一样来调用内联函数,而不必担心会产生像宏出现的问题。

24. bootloader

什么是bootloader?
定义一:初始化开发板上主要硬件(时钟,内存,硬盘),把操作系统从硬盘拷贝到内存,然后让CPU跳到内存中执行操作系统。
定义二:芯片上电后运行的一段裸机程序,功能是:初始化DDR(内存控制寄存器)等外设,然后将linux内核从flash(SD卡、NAND、EMMC)拷贝到DDR中,最后启动linux内核。
BootLoader和linux系统的关系就如同一BIOS和Windows的关系

25. MCU启动过程

一个工程代码总是以main函数作为入口,芯片上电时完成软硬件初始化工作,为执行main函数作准备
MCU复位时,完成以下工作:(见下图)
1) 从中断向量表的第一项(0x00000000)中取出MSP值(main stack pointer);
2) 从复位向量(0x00000004)中取出Reset_Handler函数地址;
3) 跳转到Reset_Handler位置并执行
在Reset_Handler函数中,MCU执行软硬件初始化,包括Flash和RAM位置、时钟和PLL(锁相环)、静态和全局变量等。
嵌入式面经_第3张图片

26. Arm体系结构

1) arm处理器有7种模式
嵌入式面经_第4张图片
除了用户模式外,其它6种为特殊模式,这些模式下,程序可以访问系统所有资源;这六种特殊模式中,除了系统模式,其它5种又称为异常模式。

2) arm处理器介绍
arm处理器共有37个寄存器,其中31个通用寄存器,6个状态寄存器。
任意时刻可见的寄存器组包括15个通用寄存器(R0-R14),一个或两个状态寄存器及程序计数器(PC),其中通用寄存器中:
a) R0-R7为未备份寄存器,未备份处理器在所有的处理模式下指的都是同一个物理寄存器,在异常中断造成的处理器模式切换时,由于不同的处理器模式使用的都是相同的物理寄存器,可能造成寄存器中的数据被破坏,未备份处理器没有用于特别的用途,任何可采用通用寄存器的场合都可以使用未备份处理器;
b) R8-R14为备份寄存器
c) 程序计数器PC,即R15
由于ARM采用了流水线机制,当正确读取了PC的值时,改值为当前指令地址值加8个字节,其实最终是加8个字节还是12个字节,取决于芯片的流水线级数。
d) 状态寄存器CPSR
CPSR可以在任何处理器模式下被访问,它包含了标志位,中断禁止位,当前处理器模式标志以及其他的一些控制和状态位,每一种处理器模式下都有一个专用的物理状态寄存器称为SPSR(备份状态寄存器)。当特定的异常中断发生时,这个寄存器用于存放当前程序状态寄存器的内容。在异常中断程序退出时,可以用SPSR中保存的值来恢复CPSR。

27. 什么是嵌入式?

嵌入式是一种专用的计算机系统;国内普遍认同的嵌入式系统定义是以应用为中心,以计算机技术为基础,软硬件可裁剪,适应应用系统对功能、可靠性、成本、体积、功耗等严格要求的专用计算机系统;从应用对象上加以定义来说,嵌入式系统是软件和硬件的综合体,还可以涵盖机械等附属装置。

嵌入式系统组成:一个嵌入式系统装置一般都由嵌入式计算机系统和执行装置组成,嵌入式计算机系统是整个嵌入式系统的核心,由硬件层、中间层、系统软件层和应用软件层组成。执行装置也称为被控对象,它可以接受嵌入式计算机系统发出的控制命令,执行所规定的操作或任务。

执行装置可以很简单,如手机上的一个微小型的电机,当手机处于震动接收状态时打开;也可以很复杂,如SONY 智能机器狗,上面集成了多个微小型控制电机和多种传感器,从而可以执行各种复杂的动作和感受各种状态信息。

28. 进程与线程中的通信方式

进程间的通信方式:
管道( pipe ):
管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

有名管道 (namedpipe) :
有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。

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

消息队列( messagequeue ) :
消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

信号 (sinal ) :
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

共享内存(shared memory ) :
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。

套接字(socket ) :
套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同设备及其间的进程通信。

线程间的通信方式:

锁机制:包括互斥锁、条件变量、读写锁
互斥锁提供了以排他方式防止数据结构被并发修改的方法。
读写锁允许多个线程同时读共享数据,而对写操作是互斥的。
条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。

信号量机制(Semaphore):包括无名线程信号量和命名线程信号量

信号机制(Signal):类似进程间的信号处理
线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。

29. 如何将PC上的程序移植到嵌入式系统上,需要注意些什么?

先查看PC程序运行,需要哪些lib库。ldd 命令查看。
然后从你的交叉编译器目录中,找到对应的相同名字的库,拷贝到板子/lib中。
交叉编译器简介:在一种计算机环境中运行的编译程序,能编译出在另外一种环境下运行的代码

30 . 设计一种通信方式,从一台主机向另外一台主机传递数据,那么应该怎么选择。

回答由SPI、IIC和UART,吞吐量大的建议用SPI(可以达到5Mhz),但如果对线路资源紧张,建议用IIC,因为IIC两根线就可以,但SPI最少用3根。

31. FreeRTOS之全配置项详解、裁剪(FreeRTOSConfig.h)

首先,我们需要明确一个问题,FreeRTOSConfig.h是一个用户级别的文件,不属于内核文件。每个用户可以有不同的FreeRTOSConfig.h。
  FreeRTOS作为一个可高度配置的实时内核,其绝大多数配置选项都体现在FreeRTOS.h(注意是FreeRTOS.h不是FreeRTOSConfig.h)中。为什么这么说?打开FreeRTOS.h看看就知道了,这个文件唯一要干的活就是负责根据宏值来对FreeRTOS进行配置的。那么,如果用户想要根据自己的需要来配置FreeRTOS,那该怎么办呢?难道需要直接改内核头文件FreeRTOS.h?当然不是。FreeRTOS的设计者采用了一个很灵活的方式:FreeRTOS.h通过检查一个名为FreeRTOSConfig.h的用户级别的配置文件来实现对FreeRTOS的配置。这样,既实现了灵活配置,又保证了所有用户就只有FreeRTOSConfig.h不同,而不需要修改内核源码。
  那么用户具体如何配置呢?简单,用户只需要根据需要,在FreeRTOSConfig.h中以宏值的形式给出定义即可。那么,宏值如何来的呢?为什么用户定义了宏值就可以呢?首先,宏值全部来自FreeRTOS.h这个系统级头文件,用户需要什么功能,需要在该文件中查找对应的宏值;其次,FreeRTOS.h中会检查特定功能的宏值有没有定义(默认认为用户使用FreeRTOSConfig.h这个文件),如果用户定义了指定宏值,则FreeRTOS就根据用户的定义来实现。这样就实现了用户配置。
http://www.freertos.org/a00110.html

32. DMA为什么能提高效率?

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

33.优先级反转以及解决方法

优先级翻转是当一个高优先级任务通过信号量机制访问共享资源时,该信号量已被一低优先级任务占有,因此造成高优先级任务被许多具有较低优先级任务阻塞,实时性难以得到保证。

例如:有优先级为A、B和C三个任务,优先级A>B>C,任务A,B处于挂起状态,等待某一事件发生,任务C正在运行,此时任务C开始使用某一共享资源S。在使用中,任务A等待事件到来,任务A转为就绪态,因为它比任务C优先级高,所以立即执行。当任务A要使用共享资源S时,由于其正在被任务C使用,因此任务A被挂起,任务C开始运行。如果此时任务B等待事件到来,则任务B转为就绪态。由于任务B优先级比任务C高,因此任务B开始运行,直到其运行完毕,任务C才开始运行。直到任务C释放共享资源S后,任务A才得以执行。在这种情况下,优先级发生了翻转,任务B先于任务A运行。

解决优先级翻转问题有优先级天花板(priority ceiling)和优先级继承(priority inheritance)两种办法。

优先级天花板是当任务申请某资源时, 把该任务的优先级提升到可访问这个资源的所有任务中的最高优先级, 这个优先级称为该资源的优先级天花板。这种方法简单易行, 不必进行复杂的判断, 不管任务是否阻塞了高优先级任务的运行, 只要任务访问共享资源都会提升任务的优先级。

优先级继承是当任务A 申请共享资源S 时, 如果S正在被任务C 使用,通过比较任务C 与自身的优先级,如发现任务C 的优先级小于自身的优先级, 则将任务C的优先级提升到自身的优先级, 任务C 释放资源S 后,再恢复任务C 的原优先级。这种方法只在占有资源的低优先级任务阻塞了高优先级任务时才动态的改变任务的优先级,如果过程较复杂, 则需要进行判断。

34. 信号量及信号量与自旋锁的区别

Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个不可用(已经被占用)的信号量时,信号量会将其推进一个等待队列,然后让其睡眠。这时处理器能重获自由,从而去执行其他代码。当持有的信号量可用(被释放)后,处于等待队列中的那个任务将被唤醒,并获得该信号量

信号量和自旋锁的区别

(1)信号量不会禁止内核抢占,持有信号量的代码可以抢占,而自旋锁不可以。这意味着信号量不会对调度的等待时间带来负面影响

(2)自旋锁只适合持锁时间短的,因为请求线程在此过程中有两次上下文的切换,被阻塞的线程要换出和换入,与实现自旋锁的少数几行代码相比,上下文切换当然代码多。相反,锁被短时间持有,使用信号量就不太合适了。因为睡眠、维护等待队列以及唤醒所花费的开销可能比锁被占用的全部时间还要长

(3)在你占用信号量的同时不能占用自旋锁。因为在你等待信号量时也可能会睡眠,而在持有自旋锁时是不允许睡眠的

(4)何时用自旋锁何时用信号量可以根据锁被持有的时间长短来判断

(5)信号量同时允许任意数量的锁持有者,而自旋锁在一个时刻最1多允许一个任务持有它

35. strcpy和strncpy的缺陷

1.存在潜在越界问题
当dest的长度 < src的长度的时候,由于无法根据指针判定其所指指针的长度,故数组内存边界不可知的。因此会导致内存越界,尤其是当数组是分配在栈空间的,其越界会进入你的程序代码区,将使你的程序出现非常隐晦的异常。

2.字符串结束标志服’\0’丢失
当dest所指对象的数组长度==count的时候,调用strncpy使得dest字符结束符’\0’丢失。

3.效率较低
当count > src所指对象的长度的时候,会继续填充’\0’知道count的长度为止。

4.不能处理内存覆盖问题
不能处理dest和src内存重叠的情况。

36. sizeof与strlen有以下区别

  1. sizeof是一个操作符,而strlen是库函数。
  2. sizeof的参数可以是数据的类型,也可以是变量,而strlen只能以结尾为’\0’的字符串作参数。
  3. 编译器在编译时就计算出了sizeof的结果,而strlen必须在运行时才能计算出来。
  4. sizeof计算数据类型占内存的大小,strlen计算字符串实际长度。

37. 哈希函数及哈希冲突的定义

1.哈希函数:

哈希法又称散列法、杂凑法以及关键字地址计算法等,相应的表成为哈希表。

基本思想:首先在元素的关键字K和元素的位置P之间建立一个对应关系f,使得P=f(K),其中f成为哈希函数。

创建哈希表时,把关键字K的元素直接存入地址为f(K)的单元;

查找关键字K的元素时利用哈希函数计算出该元素的存储位置P=f(K).

2.哈希冲突:

当关键字集合很大时,关键字值不同的元素可能会映像到哈希表的同一地址上,即K1!=K2,但f(K1)=f(K2),这种现象称为hash冲突,实际中冲突是不可避免的,只能通过改进哈希函数的性能来减少冲突。

37.1 哈希函数的构造方法

  1. 数字分析法
    如果事先知道关键字集合,并且每个关键字的位数比哈希表的地址码位数多时,可以从关键字中选出分布较均匀的若干位,构成哈希地址。

  2. 平方取中法
    如果事先知道关键字集合,并且每个关键字的位数比哈希表的地址码位数多时,可以从关键字中选出分布较均匀的若干位,构成哈希地址。

  3. 分段叠加法
    这种方法是按哈希表地址位数将关键字分成位数相等的几部分(最后一部分可以较短),然后将这几部分相加,舍弃最高进位后的结果就是该关键字的哈希地址。

  4. 除留余数法

  5. 伪随机数法

37.2 处理哈希冲突的几种方法

  1. 开放定址法(再散列法)
    当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其

  2. 再哈希法
    这种方法是同时构造多个不同的哈希函数,哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。

  3. 链地址法
    这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。

  4. 建立公共溢出区
    将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表

你可能感兴趣的:(嵌入式)