嵌入式系统

概述

嵌入式系统是以应用为中心,以计算机技术为基础,并且软硬件可裁剪,适用于应用系统对功能、可靠性、成本、体积、功耗有严格要求的专用计算机系统

计算机系统

当下两大主流的计算机组成架构,分别是 冯.诺伊曼体系结构 和 哈佛 架构 这两大结构都是确定了计算机系统的组成框架

冯诺依曼结构(存储程序型电脑)

早期的计算机仅内核特定的程序==》计算机仅内含一个数学计算程序,不能处理文字数据、图像数据更不能玩游戏

冯.诺伊曼的两大观点:

1.程序应当被存储

2.计算机数据的最基本单位应该 是二进制 数据

同时,冯.诺伊曼体系结构将计算机分为以下几个部分:

  1. 算术逻辑单元

  2. 控制电路(位于中央处理器中) =》用来操作哪些组件被 enable 或 disable

    ==>CPU = 运算器 ALU + 控制器 Controller

  3. 存储器:存储程序(指令)和数据

    指令和数据以同等的地位存储在存储器中,并且可以按照地址寻访 ===> 程序是按顺序执行

  4. 输入/输出设备:IO

如图:

冯诺依曼体体系结构

哈佛结构

有人就提出:指令和数据虽然本质上都是"数据"(1/0)但是,指令和数据的属性是不一样的==> 逻辑层面的特性

指令===》不可修改的(CPU):Read Only

数据===》可读可写的

因此,数据和指令在存储时,也应当按其属性分开存储,有些存储器存储只读数据(程序、指令)===》ROM:Read Only Memory

有些存储器存储可读可写的数据 ===》RAM:Random Access Memory

ROM 是只读存储器,用来存储程序,既然是只读的,那程序又是如何写入的呢?

==》只读 是针对 CPU,ROM 需要特定的电路用于写入数据 这种电路 称为 烧写电路,往 ROM 中写入数据的操作 称为 烧机/刷机

上述这种将数据和指令分开存储的结构被称为哈佛结构

如图:

哈佛架构

总线

既然一个计算机是由上述多个组件构成,那组件与组件之间又是如何进行数据传递的呢??

===> 总线

总线(BUS):本质就是多根“电线”,是计算机中用于数据传递的最基本的构成原件

总线有两个特点:

  1. 多个部件可以同时从总线上接受相同的信息 -> 广播式
  2. 任意时刻只能有一个设备向总线发送信息 -> 系统瓶颈

你往总线上发一个高电平,我发一个低电平 -> 总线上状态,到底是高还是低呢? 不能确定 => "信息误码"

按功能可以分为:

  • 地址总线(AB):单向总线,由 CPU 发出地址进行寻址,宽度与寻址空间相关

    比如 4G 寻址空间 =》4G 个地址 0x0000 0000~0xffff ffff=》232=》32 根地址总线

  • 数据总线(DB):双向总线,宽度差别被称为“带宽”,用于读写数据

  • 控制总线(CB):用于传递命令或状态的总线,比如确定器件的读写状态

按位置可以分为:

  • 片内总线:位于 CPU 内部总线
  • 系统总线:芯片与其它芯片之间的通信总线
  • 通信总线:IO 总线 CPU 与外设控制器

总线

在上图中,DB 向所有器件发送信息,AB 负责选定接收方,其它者对此信息不予理会,CB 负责确定 CPU 对该接收方的操作是读出 or 写入数据。

存储器的逻辑结构和操作

通过一个例子来了解

假设 有一个 64 * 64 Byte 存储器,请问该存储器需要多少根地址总线?

64 * 64 Byte = 2 ^12;

bit :最小的存储单元,只能存储一个二进制状态,实质是一个简单时序逻辑电路,只包含存储电路=》触发器/锁存器

锁存器

Byte:由 8bits 组成的存储单元,可以存储 8 个二进制状态,也是计算机存储空间的最小单位,存储器的地址分配也是以 Byte 为单位

存储器

在上图中,每一个存储单元有八个 bit 位,地址总线分为行地址和列地址,并共同选择唯一的一个存储单元,由控制总线确定数据总线对该存储单元的读 or 写操作

CPU 的工作原理

CPU 内部有以下组件:

ALU:算术运算单元,早期的 CPU 是一个简单的 加法器作为 ALU

Control Unit:控制单元

R0,R1,R2,...Rn 寄存器(CPU 内部的数据寄存器)

Program Counter 程序计数器 PC,用来保存 CPU 下一条要执行的指令的地址

Instr Register 指令寄存器

Instr Decoder 指令译码器

Internal Bus:内部总线

其中:寄存器 Register 是指 CPU 内部用来临时存储参与运算的数据或运算结果的存储单元

如图:

CPU 的工作原理 1

上图中执行 ADD R1,R2,R3 指令,指将 R2 与 R3 相加,结果保存在 R1 里面。

CPU 中控制单元首先将 R2 使能,加法器 Input1 使能,然后将 R2 寄存器内容读入 Input1 中,同理将 R3 寄存器内容读入 Input2 中,将结果放入 Output 中,最后再将结果放入 R1 中。

CPU 工作原理

上图中 CPU 与外部存储器通信读取并执行指令,程序计数器 PC 将保存的地址对应指令存入指令寄存器,通过指令译码器将指令译码结果传入控制单元控制,后续过程重复上上图

几个基本概念

机器字长:机器字长是指 CPU 一次能处理数据的位数,通常与 CPU 寄存器位数(总线位数)有关。字长越大,数的表示范围就大,精度也越高,处理速度也就越快。

int 与机器字长 有没有关系?
并没有明文规定有关系。
机器字长是一个 CPU 内部结构相关的一个概念,
int 软件上编译器相关的一个概念。

  		int类型一般为机器中最自然的长度。  
  		最自然 -> 寄存器的位数

  		unsigned long

芯片:芯片 = CPU + 总线 + 各硬件控制器

STM32F407(芯片型号)采用内核 ARM Cortex M4

ARM:一个公司,只设计 CPU,不生产芯片,同时也是一个 CPU 型号的名字

		ARM7  
		ARM9 -> 三星的S3C2410, S3C2440

		ARM10  
		ARM11 -〉三星的S3C6410,S3C6440

Cortex:

			Cortex A  
				Application(应用级产品),对性能要求比较高的产品  
				A7 A8 A9 A53...

			Cortex R  
				Realtime(实时性),对实时要求比较高的产品,  
					工业级,军工类..  
				R3 R4 R6 R7  
			Cortex M  
				MCU(微处理器),“单片机”  
				M0 M1  
				M3 -> STM32F103

				M4 -> STM32F407  
						NXP MK60  
						...

ARM Cortex M4 体系结构

Cortex M4 总线接口

ARM Cortex M4 采用哈佛架构,为系统提供了三套总线

  1. Icode 总线,用于访问代码空间的指令, 32bits

    访问的空间为: 0x0000 0000 ~ 0x1FFF FFFF (512M).
    每次取 4 字节 指令,只读数据

  2. Dcode 总线,用于访问代码空间的数据, 32bits

    访问的空间为: 0x0000 0000 ~ 0x1FFF FFFF(512M)
    非对齐的访问会被总线分割成几个对齐的访问。

      	“4字节对齐”:地址必须为4倍数。
    
      	从最底层来讲: 4字节对齐,总线的最后两bit固定为0  
      				可以“另作他用”. 总线的资源是很宝贵的
    
      	0 :	00 00  
      	4:		01 00  
      	8:		10 00  
      	12:		11 00  
      	16:	   100 00  
      	……
    
      	如果底层采用 "4字节对齐"  => CPU写出的地址必须为4的倍数
    
      	地址 00 01 02 03 04 05 06 07
    
      	如果软件上(编译器)的对象地址不是4字节对齐,则效率会变低!!!
    
      	so,如果底层硬件(体系结构)规定是4字节对齐,相应的编译器也会  
      	考虑到对齐的问题.
    
  3. System 总线,用于访问其他系统空间。如: 各硬件控制器,GPIO……。

    访问空间为 0x2000 0000 ~ 0xDFFF FFFF 和 0XE010 0000 ~ 0xFFFF FFFF
    非对齐的访问会被总线分割成几个对齐的访问

      	I/O总线,用于访问芯片上的各外设寄存器的地址空间的,……。
    

CortexM4 工作状态(处理器状态)

指令集

ARM 公司设计的 CPU,可以支持多种指令集:

  • ARM 指令集:32bits,功能比较强大,通用

  • Thumb 指令集:

    • thumb 16bits, 功能也强大
    • thumb-2 32bits,功能强大,增加了不少专用的 DSP 指令

同一个应用你既可以用 ARM 指令来写,还可以用 Thumb 指令来写,或者混合写也可以。
但是这样就会对 CPU 造成一个困扰:
ARM 32bits
thumb 16bits
..
我(CPU)下一条指令,我到底是取 32bits,还是 16bits 呢
即使是取回来了,我是按 ARM 指令去译码,还是按 thumb 指令去译码呢
你要给 cpu 一个 hint(暗示),到底现在执行的 ARM 还是 thumb

处理器状态:我们把 CPU 正在执行何种指令集,称为处理器的状态。

  • ARM 状态:CPU 正在执行 ARM 指令集
  • Thumb 状态:CPU 正在执行 thumb 指令 <---- Cortem M4 只支持 Thumb 指令

在一个状态寄存器中,专门有一个 bit 为用来表示处理器的状态
T:
1 -> Thumb
0 -> ARM

CortexM4 寄存器

通用寄存器

没有特殊用途,你想怎么用就怎么用,没有禁忌

R0~R7: 所有 thumb thumb-2 都可以访问

R8~R12: 只有少量的 thumb 指令可以访问,thumb-2 都可以访问

受 bits 位数的限制,thumb 有些可以访问,有些不可以,而 thumb-2 都可以

如:
MOV R0, #3 ; 3 -> R0
MOV R1, #4 ; 4 -> R1
ADD R2, R1, R0 ; => R0 + R1 -> R2

  			----  
  			MOV R3, #3  
  			MOV R4, #4  
  			ADD R7, R3, R4;

专用寄存器

有专门的用途,不能乱用

R13 R14 R15 xPSR

R13(SP)

Stack Pointer 保存堆栈的栈顶地址

“堆栈(Stack)” 是用“栈的思想”来管理的一段内存。
“栈的思想”:先进后出

  		为什么需要“堆栈”?为了支持过程调用(函数)。  
  				“现场保护”  
  				函数的具体功能的代码  
  				“现场恢复”

  		过程调用?跳转  
  			一个过程还没有结束,就去调用另外一个过程。  
  			所有指令的操作数和结果都需要在寄存器中,CPU才能访问  
  			一个过程的一些中间结果,保存在寄存器中的,  
  			如果一个过程,去调用另外一段代码(过程调用),另外的那段代码  
  			势必也要修改寄存器,如果另外那段代码不事先保存寄存器里面的  
  			内容,在代码结束,返回到前面的那个过程时,结果就会不对。

  			过程调用(函数):  
  				“现场保护”:把寄存器里面的内容保存到内存中去

  				“现场恢复”:把原先保存的内容还原到相应的寄存器中去。

  				这个过程,正好是 “先进后出”

在 C 语言完成一个功能,我们可以定义一个宏,也可以定义成一个函数,二者有何区别

#define MIN(a,b) ((a) < (b) ? (a) : (b))
			
int min(int a, int b)
{
	return ((a) < (b) ? (a) : (b));
}

main()
{
	int m;
	m = MIN(3,4); // => m = ((3) < (4) ? (3) : (4))
	m = min(3,4);
}

区别:min函数有额外的开销,宏没有:
							现场保护
							...
							现场恢复

inline在 c/c++ 中,为了解决一些频繁调用小函数大量消耗栈空间(栈内存)的问题,特别的引入了 inline 修饰符,表示为内联函数

inline:建议编译器,将它修饰的函数展开。

#include <stdio.h>
 
inline const char *num_check(int v)
{
    return (v % 2 > 0) ? "奇" : "偶";
}
 
int main(void)
{
    int i;
    for (i = 0; i < 100; i++)
        printf("%02d   %s\n", i, num_check(i));
    return 0;
}

Cortex M4 有两个堆栈,双堆栈
MSP 主堆栈指针

  		**PSP 进程堆栈指针**

  			为什么需要双堆栈呢?  
  			为了支持操作系统。把**操作系统**用的堆栈和**用户进程**用的堆栈**分开**。

R14(LR)

Linked Register 链接寄存器

在执行过程调用的指令的时候,我们需要保存该指令的下一条指针的地址
因为这个地址,就是我过程结束后,需要返回的地址

  		有一个专门的寄存器,用来保存过程调用调用的返回地址。->LR(R14)  
  		例子:

  			MOV R0, #3  
  			MOV R1, #4  
  			BL sum   ;  
  					 // BL:把下一条指令的地址(如下的: (A))存放在LR中  
  					 // 跳转是通过把:要跳到的那个地址,直接赋值给PC  
  					 //  sum -> PC

  		(A)	ADD R0, R1,

  		sum:  
  			ADD R0,R0,R1  
  			MOV PC, LR  ;  -> return 函数返回,过程返回。

R15(PC)

Program Counter 程序计数器。

保存下一条要执行的指令的地址。

PC 会在取指后,会自动增加指令所占的 bits 位数。

在 ARM Cortex M4, PC + 4

  			在有“指令流水线”情况下,PC的值会有所不同?  
  				三级流水线  
  				五级流水线

  				(1) 为什么需要指令流水线  
  				(2) 指令流水线是什么东西  
  				(3) 如何达到想有的效果

  			在程序实现的跳转,就是往PC中赋一个跳转的地址  
  			“跳转”? 就是不按顺序执行,就是跳转。

xPSR

Program Status Register 程序状态寄存器。

保存程序运行过程中的一些状态。

这些要保存的状态分为三类:

  • 应用状态寄存器 APSR: 计算结果的标志
    N Z C V Q
  • 中断状态寄存器 IPSR: 中断的编号 Exception Number 8bits
  • 执行状态寄存器 EPSR: 执行状态,如: Thumb/ARM

组合一个 32bits 的 xPSR1

中断屏蔽寄存器

			中断  
			异常

		PRIMASK[0] 片上外设的总中断开关  
				1  屏蔽所有的片上外设中断  
				0  响应所有外设的中断

		FAULTMASK[0] 系统错误异常的中断总开关  
				1	屏蔽所有异常  
				0	响应所有异常

		BASEPRI	为使中断屏蔽更加灵活,该寄存器根据中断优先级来屏蔽中断或异常。  
				当被设置为一个非0的数值时,它就会屏蔽所有相同或更低优先级的  
				异常(包括中断),则更高优先级的中断或异常还会被响应。

				每一个异常或中断,都会有一个优先级。

CONTROL 寄存器

		用来控制选择哪个堆栈(主堆栈/进程堆栈)  
			选择线程模式的访问等级(特权等级/非特权等级)  
				特权等级可以访问所有东西;  
				非特权等级只能有限访问

		CONTROL[0] :线程模式的访问等级  
				1  非特权等级  
				0 特权等级  
		CONTROL[1] :堆栈的选择  
				1  进程堆栈 PSP  
				0 主堆栈 MSP

Cortex M4 工作模式

"模式": 不同环境,不同的角色

ARM cortex M4 有两种工作模式:

  • Thread Mode: 线程模式
  • Handler Mode: 处理模式(异常中断模式)

异常/中断 是什么? 打断 CPU 指令执行顺序的事件,称为中断。

  为什么要支持两种模式呢? 为什么不只用一种模式呢? Thread Mode

  如果只用一种模式,thread mode,为了响应一些外部事件(比如说,用户是否  
  按下某个按键?):  
  	轮询:轮流询问。 通过轮询,CPU也可能 响应外部事件,但是  
  	轮询天生就有缺陷:  
  		(1) 浪费CPU  
  		(2) 占用总线, Bus is always busy.  
  		(3) 轮询有一个时间差,轮询的时间间隔。不及时!!!

  一些比较重要的事件,如果去轮询,不太合适,  
  所以,有人就提出,能不能在CPU内部设计一个 “中断模式”:  
  	不去轮询,来了事件,你中断我,。处理模式。

  	为了提高效率和响应速度。

  两种模式之间是怎么切换的呢? 很重要。如图:

  Handler Mode :  
  	中断模式,当一些比较重要的事件,产生时,CPU中止正在做的  
  	事情,切换到Handler Mode下去执行,此时 “特权等级”

  	中断处理完成后,再返回到断点处,继续Thread Mode运行。

  Thread Mode:  
  	线程模式。  
  		特权等级 : 可以跑一些如OS的代码  
  		非特权等级: 可以跑一些如 "用户态"的代码

  		特权等级 -> 非特权等级  
  	但是:  
  		非特权等级 不可以 切换到 特权事件,除非产生“中断”


  1. XPSR

    	N Z C V Q  
    				我们每一条指令的执行都可以影响这些状态标志位。  
    			N: 负数标志。 Negative is set o bit 31 of the result  
    				xPSR.N < Ret[31] 最高位是符号位 1 -> 负数, 0 -> 非负数
    
    				如果 xSPR.N == 1,表示上一条指令的操作结果为负数。  
    				N什么什么情况下会置位呢?  
    				如果一条指令,影响状态标志位  
    					指令可以不影响状态标志位,也可以设置为影响标志位
    
    					如:  
    						MOV R0, #3 ; ->不影响状态标志位,  
    							不管这条指令的执行结果如何,xPSR不变。  
    						MOVS R0, #3; ->影响状态标志位  
    				xPSR.N <- 结果寄存器[31]  
    						如:  
    							MOVS R0, #3  
    								R0[31] -> xSPR.N == 0
    
    			Z : Zero 。零标志。结果所有bit位都为0,则xPSR.Z == 1  
    				否则xPSR.Z == 0.前提也是指令结果影响状态标志。  
    					如:  
    						MOVS R0, #0;  
    								-> xPSR.Z == 1  
    						MOVS R1, #4 ;  
    								-> xPSR.Z == 0
    
    						MOV R2, #0 ;没加S,表示指令的执行,不影响状态寄存器  
    								-> xPSR.Z值不变。  
    			C: Carry 借位或进位标志。  
    				进位: 在做加法运算时,产生了进位。则C == 1,否则 C == 0  
    				借位: 在做减法运算时,没产生借位。则C == 1,否则 C == 0
    
    				ADC, ADD, CMN 加法。如果产生了进位,则C == 1,否则 C == 0  
    				SBC, SUB, CMP 减法。如果生生了借位,则C == 0,否则 C == 1
    
    				如:  
    					MOV R0, #3  
    					MOV R1, #4  
    					SUBS R2, R0, R1;  R0 - R1  -> R2  
    							R0 - R1 产生借位。 C == 0  
    								xPSR.C == 0
    
    					--------  
    					MOV R0, #5  
    					MOV R1, #4  
    					SUBS R2, R0, R1  
    							R0 - R1 -> R2  
    							R0 - R1没产生借位,C == 1  
    								xPSR.C == 1
    
    					-----  
    					A - B  
    						xPSR.C == 1  
    					说明: A >= B
    
    					A - B  
    						xPSR.C == 0  
    					说明: A < B
    
    						r0 r1
    
    					CMP R0, R1  
    						xPSR.C == 0  => R0 < R1  
    						xPSR.C == 1 => R0 >= R1
    
    					-----------  
    					A + B  
    						xPSR.C == 1  
    					说明产生了进位,是不是就说明 溢出啦?????  
    						不一定。因为进位与溢出没有半毛钱关系。
    
    			V: oVerflow 溢出标志。  
    				反映有符号数做加减运算所得结果是否溢出,如果运算结果超过当前  
    				运算位数所能表示的范围,则溢出 xPSR.V = 1, 否则为0.
    
    				在有符号的运算中,进位(借位,C)与溢出是两个完全不同的概念。  
    				例子: 假设寄存器是 8bits  
    					CASE 1: 有进位,结果溢出  
    							1011 0101  
    						+   1000 1111  
    						--------------  
    						  1 0100 0100  
    							溢出:  
    								负数 + 负数 = 正数(结果溢出)
    
    							C6 = 0  
    							C7 = 1  
    							V = C7 异或 C6 = 1 => 溢出
    
    					CASE 2: 有进位,结果未溢出  
    							0100 0010   (66)  
    						+   1100 1101   (-51)  
    						---------------------  
    						  1 0000 1111   (15)
    
    					CASE 3: 无进位,结果溢出  
    							0100 0010  
    						+	0110 0011  
    						----------------------  
    						    1010 0101  
    							正数+ 正数 = 负数(结果溢出)
    
    					CASE 4: 无进位,结果未溢出  
    							0100 1010  
    						+   0010 1101  
    						-----------------------  
    							0111 0111
    
    					V = Cn 异或 Cn-1  
    						Cn表示最高位的进位(或借位)标志  
    						Cn-1表示次高位的进位(或借位)标志
    
    			Q: 饱和标志  
    				饱和计算: 通过将数据强制置为最大(或最小)允许值,减小了  
    						数据畸变,当然畸变仍然存在,不过若数据没有超过最大  
    						允许范围太多,就不会有太大的问题。
    
    				8bits无符号的加法:  
    					普通计算:  
    						11111111 + 1 = 0  
    						11111111  
    					+   00000001  
    					--------------  
    					   100000000
    
    					饱和计算  
    						11111111 + 1 = 11111111
    
    				例子:  0111111111...11   => 0X7FFF FFFF  
    					   1000000000...00  =>  - (2^31)  
    					ADD 普通加法  
    					QADD 将两上有符号的32bits相加后,进行饱和操作
    
    					MOV R0, #0x7FFFFFFF  
    					MOV R1, #1  
    					ADD R2, R0, R1  
    					QADD R3, R0,R1
    
    			T : xPSR[24]  
    				表示处理器当前执行的状态。(工作状态/处理器状态)  
    					ARM状态 xPSR.T == 0  
    					Thumb状态 xPST.T == 1
    
    			IT  
    				26:25 -> 该条件所管的指令有多少条  0 1 2 3  
    				15:10 -> 执行的条件 GT或EQ ..
    
    					IF-THEN  位,它们是if-then指令的执行状态位  
    					包含了if then模块的指令数目和它们的执行条件
    
    					if (R0 > R1) //  CMP R0,R1 ;// if R0 > R1  xPSR.C == 1  => GT >  
    					{  
    						r2 = 2		MOVGT R2, #2  
    						r3 = 3		MOVGT R3, #3  
    						r4 = 4		MOVGT R4, #4  
    					}
    
    					=> IT  
    					CMP R0, R1  
    					ITGT TT   -> GT这个条件管三条语句  
    					MOV R2,#2						MOV R3,#​3  
    					MOV R4,#4
    
    			ICI  : Interruptible-Continuable Instrument 可中断-可继续指令位  
    				15:12  
    					如果执行LDM/STM操作(对内存进行批量拷贝)时,产生一个中断,  
    					中断的优先级很高,必须要打断这条指令的执行,那么你的数据  
    					有可能只拷贝了一部分,对不对?这种情况怎么办呢?  
    					这种情况,就需要记录被打断的寄存器的编号,在中断响应之后,  
    					处理器返回由该位指向的寄存器,并恢复操作,继续执行拷贝。
    
  • 待分类

    用户发帖时如果不填标签,则默认加上“待分类”。这样做是为了减少用户发帖的负担,同时也减少运营维护的工作量。具有帖子更新权限的用户可以帮助社区进行帖子整理,让大家可以更方便地找到所需内容。这里是关于这样设计的一些思考,欢迎讨论。

    2 引用 • -280 回帖 • 4 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...