概述
嵌入式系统是以应用为中心,以计算机技术为基础,并且软硬件可裁剪,适用于应用系统对功能、可靠性、成本、体积、功耗有严格要求的专用计算机系统
计算机系统
当下两大主流的计算机组成架构,分别是 冯.诺伊曼体系结构 和 哈佛 架构 这两大结构都是确定了计算机系统的组成框架
冯诺依曼结构(存储程序型电脑)
早期的计算机仅内核特定的程序==》计算机仅内含一个数学计算程序,不能处理文字数据、图像数据更不能玩游戏
冯.诺伊曼的两大观点:
1.程序应当被存储
2.计算机数据的最基本单位应该 是二进制 数据
同时,冯.诺伊曼体系结构将计算机分为以下几个部分:
算术逻辑单元
控制电路(位于中央处理器中) =》用来操作哪些组件被 enable 或 disable
==>CPU = 运算器 ALU + 控制器 Controller
存储器:存储程序(指令)和数据
指令和数据以同等的地位存储在存储器中,并且可以按照地址寻访 ===> 程序是按顺序执行
输入/输出设备:IO
如图:
哈佛结构
有人就提出:指令和数据虽然本质上都是"数据"(1/0)但是,指令和数据的属性是不一样的==> 逻辑层面的特性
指令===》不可修改的(CPU):Read Only
数据===》可读可写的
因此,数据和指令在存储时,也应当按其属性分开存储,有些存储器存储只读数据(程序、指令)===》ROM:Read Only Memory
有些存储器存储可读可写的数据 ===》RAM:Random Access Memory
ROM 是只读存储器,用来存储程序,既然是只读的,那程序又是如何写入的呢?
==》只读 是针对 CPU,ROM 需要特定的电路用于写入数据 这种电路 称为 烧写电路,往 ROM 中写入数据的操作 称为 烧机/刷机
上述这种将数据和指令分开存储的结构被称为哈佛结构
如图:
总线
既然一个计算机是由上述多个组件构成,那组件与组件之间又是如何进行数据传递的呢??
===> 总线
总线(BUS):本质就是多根“电线”,是计算机中用于数据传递的最基本的构成原件
总线有两个特点:
- 多个部件可以同时从总线上接受相同的信息 -> 广播式
- 任意时刻只能有一个设备向总线发送信息 -> 系统瓶颈
你往总线上发一个高电平,我发一个低电平 -> 总线上状态,到底是高还是低呢? 不能确定 => "信息误码"
按功能可以分为:
-
地址总线(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 内部用来临时存储参与运算的数据或运算结果的存储单元
如图:
上图中执行 ADD R1,R2,R3 指令,指将 R2 与 R3 相加,结果保存在 R1 里面。
CPU 中控制单元首先将 R2 使能,加法器 Input1 使能,然后将 R2 寄存器内容读入 Input1 中,同理将 R3 寄存器内容读入 Input2 中,将结果放入 Output 中,最后再将结果放入 R1 中。
上图中 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 采用哈佛架构,为系统提供了三套总线
-
Icode 总线,用于访问代码空间的指令, 32bits
访问的空间为: 0x0000 0000 ~ 0x1FFF FFFF (512M).
每次取 4 字节 指令,只读数据 -
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字节对齐,相应的编译器也会 考虑到对齐的问题.
-
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的代码 非特权等级: 可以跑一些如 "用户态"的代码 特权等级 -> 非特权等级 但是: 非特权等级 不可以 切换到 特权事件,除非产生“中断”
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操作(对内存进行批量拷贝)时,产生一个中断, 中断的优先级很高,必须要打断这条指令的执行,那么你的数据 有可能只拷贝了一部分,对不对?这种情况怎么办呢? 这种情况,就需要记录被打断的寄存器的编号,在中断响应之后, 处理器返回由该位指向的寄存器,并恢复操作,继续执行拷贝。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于