嵌入式软件工程师笔试题笔记



  • 大小端问题

    A=0x12345678存入地址1000H~10003H中,

    小端模式:1000H=78 1001H=56 1002H=34 1003H=12

    大端模式:1000H=12 1001H=34 1002H=56 1003H=78

    //利用联合体这种巧妙地存储结构就可以轻松的将数据拆分出来
    #define _CRT_SECURE_NO_WARNINGS
    typedef struct S2{
    	unsigned char a;
    	unsigned char b;
    	unsigned char c;
    	unsigned char d;
    }S2;
    typedef union S{
    	long num;
    	S2 s1;
    }S;
    
    int main(void){
    	S s;
    	s.num = 2378912378;
    
    	printf("%d.%d.%d.%d\n", s.s1.a, s.s1.b, s.s1.c, s.s1.d );
    	system("pause");
    	return 0;
    }
    


  • 带参数宏定义

    程序按64位编译,运行下列程序代码,打印输出结果是多少

    #define CALC(x,y)  (x*y)
     
    int main(void) { 
        int i=3;
        int calc;
        char **a[5][6];
     
    	calc = CALC(i++, sizeof(a)+5);
    	printf("i=%d, calc=%d\n", i, calc);
    	return 0;
    }
    

    输出结果为:i=4, calc=725

    注意在宏定义中带参数时括号的用法,在本题中#define CALC(x, y) (x*y)的结果是725,但是如果这样写:#define CALC(x,y) (x)*(y) 的结果就是735

    一般32位机器就是564 = 120,64位则是568=240 ,char *a是字符型指针,char **a是指针的指针,在64位和32位中指针的大小是不一样的

    #define  PRODUCT (x) (x*x)
    
    int main()
    {
    	int a,b=3;
    	a=PRODUCT(b+2);
    }
    b+2*b+2=3+2*3+2=11
    


  • i2c

    I2C仅需两根线就可以支持一主多从或者多主连接,I2C使用两个双向开漏线,配合上拉电阻进行连接,关于上拉电阻阻值大小有最大值和最小值的限制,具体计算

    Rp(min)= ( VDD - Vol(max))/ Iol

    对于RC曲线:V(t) = VDD (1 - e-t / RC)
    V(t1) = 0.3 × VDD = VDD (1 - e-t1 / RC) ,t1 = 0.3566749 × RC
    V(t2) = 0.7 × VDD = VDD (1 - e-t2 / RC) ,t2 = 1.2039729 × RC
    T = t2 - t1 = 0.8473 × RC
    所以Rp(max)与最大上升时间(tr)和负载电容(Cb)有关,计算公式:
    Rp(max)=tr /(0.8437*Cb)

    双向传输总线:

    • 标准模式(Standard-mode):速率高达100kbit/s
    • 快速模式(Fast-mode):速率高达400kbit/s
    • 快速模式+(Fast-mode Plus):速率高达1Mbit/s。
    • 高速模式(High-speed mode):速率高达3.4Mbit/s

    单向传输总线:

    • 超快速模式(Ultra Fast-mode):速率高达5Mbit/s

    起始和终止条件都是由主机(master)发起产生。总线在起始条件之后处于忙碌状态,在停止条件之后又处于空闲状态。

    • 起始条件:SCL线是高电平时,SDA线从高电平向低电平切换。
    • 停止条件:SCL线是高电平时,SDA线从低电平向高电平切换。
    • 重复起始条件:和起始条件相似,重复起始条件发生在停止条件之前。主机想继续给从机发送消息时,一个字节传输完成后可以发送重复起始条件,而不是产生停止条件。

    0_1603445943156_1597237651236.png

    SDA数据线上的每个字节必须是8位,每次传输的字节数量没有限制。每个字节后必须跟一个响应位(ACK)。首先传输的数据是最高位(MSB),SDA上的数据必须在SCL高电平周期时保持稳定,数据的高低电平翻转变化发生在SCL低电平时期

    0_1603445954324_1597237829210.png

    8字节后主机释放SDA,从机下拉应答ACK,默认上拉NACK

    停止条件:SCL高时SDA变高

    0_1603445966502_1597238096482.png

    7-bit 地址格式和读写位
    一个7-bit的地址是从最高位(MSB) 开始发送的,这个地址后面会紧跟1-bit(R/W)的操作符,1表示读操作,0表示写操作。 接下来的一个bit是NACK/ACK,当这个帧中前面8 bit发送完后,接收端的设备获得SDA控制权,此时接收设备应该在第9个时钟脉冲之前回复一个ACK(将SDA拉低)以表示接收正常,如果接收设备没有将SDA拉低,则说明接收设备可能没有收到数据(如寻址的设备不存在或设备忙)或无法解析收到的消息,如果是这样,则由master来决定如何处理(stop或repeated start condition)。



  • SPI

    什么是SPI?

    ​ SPI是串行外设接口(Serial Peripheral Interface)的缩写。是 Motorola 公司推出的一种同步串行接口技术,是一种高速的,全双工,同步的通信总线。

    ​ 优点:支持全双工通信、通信简单、数据传输速率块

    ​ 缺点:没有指定的流控制,没有应答机制确认是否接收到数据,所以跟IIC总线协议比较在数据,可靠性上有一定的缺陷。

    ​ 特点(1):高速、同步、全双工、非差分、总线式(2):主从机通信模式

    ​ 它以主从方式工作,这种模式通常有一个主设备和一个或多个从设备,需要至少4根线,事实上3根也可以(单向传输时)。也是所有基于SPI的设备共有的,它们是SDI(数据输入)、SDO(数据输出)、SCLK(时钟)、CS(片选)。
    ​ (1)SDO/MOSI – 主设备数据输出,从设备数据输入;
    ​ (2)SDI/MISO – 主设备数据输入,从设备数据输出;
    ​ (3)SCLK – 时钟信号,由主设备产生;
    ​ (4)CS/SS – 从设备使能信号,由主设备控制。当有多个从设备的时候,因为每个从设备上都有一个片选引脚接入到主设备机中,当我们的主设备和某个从设备通信时将需要将从设备对应的片选引脚电平拉低或者是拉高。

    4种不同的模式

    ​ 我们SPI通信有4种不同的模式,不同的从设备可能在出厂是就是配置为某种模式,这是不能改变的;但我们的通信双方必须是工作在同一模式下,所以我们可以对我们的主设备的SPI模式进行配置,通过CPOL(时钟极性)和CPHA(时钟相位)来控制我们主设备的通信模式,具体如下:
    Mode0:CPOL=0,CPHA=0
    Mode1:CPOL=0,CPHA=1
    Mode2:CPOL=1,CPHA=0
    Mode3:CPOL=1,CPHA=1

    ​ 时钟极性CPOL是用来配置SCLK的电平出于哪种状态时是空闲态或者有效态,时钟相位CPHA是用来配置数据采样是在第几个边沿:
    CPOL=0,表示当SCLK=0时处于空闲态,所以有效状态就是SCLK处于高电平时
    CPOL=1,表示当SCLK=1时处于空闲态,所以有效状态就是SCLK处于低电平时
    CPHA=0,表示数据采样是在第1个边沿,数据发送在第2个边沿
    CPHA=1,表示数据采样是在第2个边沿,数据发送在第1个边沿

    ​ 需要注意的是:我们的主设备能够控制时钟,因为我们的SPI通信并不像UART或者IIC通信那样有专门的通信周期,有专门的通信起始信号,有专门的通信结束信号;所以我们的SPI协议能够通过控制时钟信号线,当没有数据交流的时候我们的时钟线要么是保持高电平要么是保持低电平



  • UART

    UART作为异步串口通信协议的一种,工作原理是将数据的字节一位接一位地传输。协议如下:
    在这里插入图片描述

    空闲位:
    UART协议规定,当总线处于空闲状态时信号线的状态为‘1’即高电平
    起始位:
    开始进行数据传输时发送方要先发出一个低电平’0’来表示传输字符的开始。因为空闲位一直是高电平所以开始第一次通讯时先发送一个明显区别于空闲状态的信号即为低电平。
    数据位:
    起始位之后就是要传输的数据,数据可以是5,6,7,8,9位,构成一个字符,一般都是8位。先发送最低位最后发送最高位
    奇偶校验位:
    数据位传送完成后,要进行奇偶校验,校验位其实是调整个数,串口校验分几种方式:
    1.无校验(no parity)
    2.奇校验(odd parity):如果数据位中’1’的数目是偶数,则校验位为’1’,如果’1’的数目是奇数,校验位为’0’。
    3.偶校验(even parity):如果数据为中’1’的数目是偶数,则校验位为’0’,如果为奇数,校验位为’1’。
    4.mark parity:校验位始终为1
    5.space parity:校验位始终为0
    停止位:
    数据结束标志,可以是1位,1.5位,2位的高电平。
    波特率:
    数据传输速率使用波特率来表示,单位bps(bits per second),常见的波特率9600bps,115200bps等等,其他标准的波特率是1200,2400,4800,19200,38400,57600。举个例子,如果串口波特率设置为9600bps,那么传输一个比特需要的时间是1/9600≈104.2us。
    在这里插入图片描述
    以9600 8N1(9600波特率,8个数据位,没有校验位,1位停止位)为例,这是目前最常用的串口配置,现在我们传输’O’'K’两个ASCII值,'O’的ASCII为79,对应的二进制数据为01001111 ,'K’对应的二进制数据为01001011 ,传输的格式数据如下图所示:
    在这里插入图片描述

    串口波特率为9600,1bit传输时间大约为104us,传送一个数据实际是10个比特(开始位,8个数据位,停止位),一个bytes传输速率实际为9600*8/10=7680bps。



  • 一个宏定义题目

    (1)x对a向下取整数倍的宏定义ALIGN_DOWN(x, a) 例子(65,3)->63
    (2)x对a向上取整数倍的宏定义ALIGN_UP(x, a) 例子(65,3)->66
    (3)x对a向下取整数倍的宏定义ALIGN_2N_DOWN(x, a) ,a是2的n次幂例子(65,4)->64
    (4)x对a向上取整数倍的宏定义ALIGN_2N_UP(x, a) 例子,a是2的n次幂例子(65,4)->68

    #define ALIGN_DOWN(x, a) ((x) - (x) % (a))
    #define ALIGN_UP(x, a) ((x) + a - (x) % (a)
    #define ALIGN_2N_DOWN(x, a) ((x)&~(a - 1))
    #define ALIGN_2N_UP(x, a) ((x + a - 1)&~(a - 1))
    


  • assert、volatile、const

    assert宏的原型定义在<assert.h>中,其作用是如果它的条件返回错误,则终止程序执行

    关键字volatile和const是完全相反的。它表明变量可能会通过某种方式发生改变,而这种方式是你通过分析正常的程序流程完全预测不出来的。(例如,一个变量可能被中断处理程序修改)。关键字使用语法如下:

    volatile data-definition;
    

    每次对变量内容的引用会重新从内存中加载而不是从变量在寄存器里面的拷贝加载。

    volatile关键词影响编译器编译的结果,用volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,以免出错

    有时候我们希望定义这样一种变量,它的值不能被改变,在整个作用域中都保持固定。例如,用一个变量来表示班级的最大人数,或者表示缓冲区的大小。为了满足这一要求,可以使用const关键字对变量加以限定:

    ​ const int MaxNum = 100; //班级的最大人数

    这样 MaxNum 的值就不能被修改了,任何对 MaxNum 赋值的行为都将引发错误

    所以常量必须在定义的同时赋值(初始化),后面的任何赋值行为都将引发错误

    const int *p1;
    int const *p2;
    int * const p3;
    

    在最后一种情况下,指针是只读的,也就是 p3 本身的值不能被修改;在前面两种情况下,指针所指向的数据是只读的,也就是 p1、p2 本身的值可以修改(指向不同的数据),但它们指向的数据不能被修改。

    当然,指针本身和它指向的数据都有可能是只读的,下面的两种写法能够做到这一点:

    const int * const p4;
    int const * const p5;
    

    const 和指针结合的写法多少有点让初学者摸不着头脑,大家可以这样来记忆:const 离变量名近就是用来修饰指针变量的,离变量名远就是用来修饰指针指向的数据,如果近的和远的都有,那么就同时修饰指针变量以及它指向的数据。

    在C语言中,单独定义 const 变量没有明显的优势,完全可以使用#define命令代替。const 通常用在函数形参中,如果形参是一个指针,为了防止在函数内部修改指针指向的数据,就可以用 const 来限制。



  • C/C++程序内存的各种变量存储区域和各个区域详解

    静态局部变量
    作用域:当前文件

    Text & Data & Bss
    • .text: 也称为代码段(Code),用来存放程序执行代码,同时也可能会包含一些常量(如一些字符串常量等)。该段内存为静态分配,只读(某些架构可能允许修改)。
      这块内存是共享的,当有多个相同进程(Process)存在时,共用同一个text段。
    • .data: 也有的地方叫GVAR(global value),用来存放程序中已经初始化的非零全局变量。静态分配。
      • data又可分为读写(RW)区域和只读(RO)区域。
        -> RO段保存常量所以也被称为.constdata
        -> RW段则是普通非零全局变量,静态变量就在其中
    • .bss: 存放程序中未初始化的和零值全局变量。静态分配,在程序开始时通常会被清零。

    text和data段都在可执行文件中,由系统从可执行文件中加载;而bss段不在可执行文件中,由系统初始化。
    这三段内存就组成了我们编写的程序的本体,但是一个程序运行起来,还需要更多的数据和数据间的交互,否则这个程序就是死的,无用的。所以我们还需要为更多的数据和数据交互提供一块内存——堆栈。

    堆栈(Heap& Stack)

    堆和栈都是动态分配内存,两者空间大小都是可变的。

    • Stack: 栈,存放Automatic Variables,按内存地址由高到低方向生长,其最大大小由编译时确定,速度快,但自由性差,最大空间不大。
    • Heap: 堆,自由申请的空间,按内存地址由低到高方向生长,其大小由系统内存/虚拟内存上限决定,速度较慢,但自由性大,可用空间大。
      每个线程都会有自己的栈,但是堆空间是共用的

    0_1603446174938_1597494108699.png



  • C语言编译过程中,volatile关键字和extern关键字分别在哪个阶段起作用?

    解答:volatile应该是在编译阶段,extern在链接阶段

    预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件。
    编译阶段:将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件
    汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件
    链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件

    一、编译

    ​ 1、编译

    ​ 编译是读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码

    (1)预处理:在正式的编译阶段之前进行。预处理阶段将根据已放置在文件中的预处理指令来修改源文件的内容

    ​ <1>宏定义指令,如 #define a b

    ​ <2>条件编译指令,如#ifdef,#ifndef,#else,#elif,#endif等

    ​ <3> 头文件包含指令,如#include 'FileName'或者#include 等

    ​ <4>特殊符号,预编译程序可以识别一些特殊的符号。例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。

    (2)编译、优化阶段:编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。

    ​ 优化处理是编译系统中一项比较艰深的技术。它涉及到的问题不仅同编译技术本身有关,而且同机器的硬件环境也有很大的关系。优化一部分是对中间代码的优化。这种优化不依赖于具体的计算机。另一种优化则主要针对目标代码的生成而进行的。

    ​ 2、汇编

    ​ 汇编实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。

    二、链接

    ​ 链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够诶操作系统装入执行的统一整体



  • 进程通信

    1.管道:速度慢,容量有限,只有父子进程能通讯

    2.FIFO:任何进程间都能通讯,但速度慢

    3.消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题

    4.信号量:不能传递复杂消息,只能用来同步

    5.共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存



  • 无锁编程

    ​ 在并发编程上按照同步的维护划分,可以分为阻塞的编程方式(Block)和非阻塞的编程方式(Non-blocking Synchronization)。阻塞的编程方式基本是基于锁的(lock-based)。 其中无锁编程(Lock-free)属于非阻塞同步(Non-blocking Synchronization)中的一种情况,实现非阻塞同步的算法方案按照效果要求不同可以粗略的分为:

    ​ Wait-free: 满足等待无关的程序,任何线程可以在有限步之内结束,不管其它线程的执行速度和进度如何

    ​ Lock-free:锁无关的程序,一个锁无关的程序能够确保它所有线程中至少有一个能够继续往下执行,而有些线程可能会被的延迟。然而在整体上,在某个时刻至少有一个线程能够执行下去。作为整体进程总是在前进的,尽管有些线程的进度可能没有其它线程进行的快。

    ​ Obstruction-free:在任何时间点,一个孤立运行线程的每一个操作可以在有限步之内结束。只要没有竞争,线程就可以持续运行。一旦共享数据被修改,Obstruction-free 要求中止已经完成的部分操作进行回滚。

    无锁编程具体使用和考虑到的技术方法包括:原子操作(atomic operations), 内存栅栏(memory barriers), 内存顺序冲突(memory order), 指令序列一致性(sequential consistency)和顺ABA现象等等

    ​ **对于原子操作的实现机制,在硬件层面上CPU处理器会默认保证基本的内存操作的原子性,CPU保证从系统内存当中读取或者写入一个字节的行为肯定是原子的,当一个处理器读取一个字节时,其他CPU处理器不能访问这个字节的内存地址。**但是对于复杂的内存操作CPU处理器不能自动保证其原子性,比如跨总线宽度或者跨多个缓存行(Cache Line),跨页表的访问等。这个时候就需要用到CPU指令集中设计的原子操作指令,现在大部分CPU指令集都会支持一系列的原子操作。

    ​ 而在无锁编程中经常用到的原子操作是Read-Modify-Write (RMW)这种类型的,这其中最常用的原子操作又是 COMPARE AND SWAP(CAS),几乎所有的CPU指令集都支持CAS的原子操作,比如X86平台下中的是 CMPXCHG(Compare Are Exchange)。

    1. x86/64 和 Itanium 架构通过 Compare-And-Swap (CAS) 方式来实现
    2. PowerPC、MIPS 和 ARM 架构通过 Load-Link/Store-Conditional (LL/SC) 方式来实现

    ​ 继续说一下CAS,**CAS操作行为是比较某个内存地址处的内容是否和期望值一致,如果一致则将该地址处的数值替换为一个新值。CAS操作具体的实现原理主要是两种方式:总线锁定和缓存锁定。**所谓总线锁定,就是CPU执行某条指令的时候先锁住数据总线的, 使用同一条数据总线的CPU就无法访问内存了,在指令执行完成后再释放锁住的数据总线。锁住数据总线的方式系统开销很大,限制了访问内存的效率,所以又有了基于CPU缓存一致性来保持操作原子性作的方法作为补充,简单来说就是用CPU的缓存一致性的机制来防止内存区域的数据被两个以上的处理器修改。

    ​ 最后这里随便说一下CAS操作的ABA的问题,所谓的ABA的问题简要的说就是,线程a先读取了要对比的值v后,被线程b抢占了,线程b对v进行了修改后又改会v原来的值,线程1继续运行执行CAS操作的时候,无法判断出v的值被改过又改回来。

    解决ABA的问题的一种方法是,一次用CAS检查双倍长度的值,前半部是指针,后半部分是一个计数器;或者对CAS的数值加上版本号。



  • static

    局部变量

    静态局部变量使用static修饰符定义,即使在声明时未赋初值,编译器也会把它初始化为0。且静态局部变量存储于进程的全局数据区,即使函数返回,它的值也会保持不变

    全局变量

    在定义不需要与其他文件共享的全局变量时,加上static关键字能够有效地降低程序模块之间的耦合,避免不同文件同名变量的冲突,且不会误使用

    函数

    函数的使用方式与全局变量类似,在函数的返回类型前加上static,就是静态函数。其特性如下:

    • 静态函数只能在声明它的文件中可见,其他文件不能引用该函数
    • 不同的文件可以使用相同名字的静态函数,互不影响


  • RTOS

    ​ 嵌入式linux 是将日益流行的Linux操作系统进行裁剪修改,使之能在嵌入式计算机系统上运行。它性能优异,软件移植容易,代码开放,有许多应用软件支持,应用产品开发周期短,新产品上市迅速,所以在不同行业,尤其是消费类电子产品中广泛使用。

    然而即便如此,嵌入式Linux操作系统也有其难以弥补的缺陷:

    ​ Linux操作系统有庞大的内核,对任何中断指令的响应都需要一个复杂的处理过程,对一些需要快速响应的场合显得有些力不从心。

    ​ 软硬件成本较高,需要功能强劲的MCU和外部资源,不适用于低成本的产品

    ​ 相对而言,配备嵌入式Linux会导致功耗较高,不适用于功耗要求严格应用场合

    原生Linux系统是分时操作系统,一些衍生的嵌入式Linux进行了优化和改进,也能做到很高的实时性,也可以认为是RTOS

    ​ 几种RTOS:uc/OS、FreeRTOS、TI DSP/BIOS、RT-Thread、VxWorks

    ​ 抢占式(UCos、FreeRTOS)和非抢占式(Linux)


 

Copyright © 2018 bbs.dian.org.cn All rights reserved.

与 Dian 的连接断开,我们正在尝试重连,请耐心等待