嵌入式微处理器结构和上电启动到开始运行程序的过程讲解

版权声明:本文为博主原创文章,未经博主允许不得转载。欢迎联系我qq2488890051 https://blog.csdn.net/kangkanglhb88008/article/details/89605844

我们首先来了解一下微处理器的结构:(原谅我把微处理器称为单片机,叫法有问题,我明天改,里面最核心运算单元称为cpu,可能这样术语不是很准确或者比较俗气,但是我都是为了我更加通熟易懂的解释清楚这些问题)

现在的处理器内核基本上都是arm公司设计的,主要分为三种:

context-A系列(属于应用处理器内核,主频很高,1Ghz左右(即每秒可以计算1亿次),运算性能很强,多核,但是中断响应慢毫秒级别,实时性很差(接电话,快一毫秒慢一毫秒反正你也感觉不出来),比如主要是主要是应用于多媒体消费娱乐电子等等方面,比如手机,平板,电脑的cpu。这些芯片价格贵,几百到两千的都有)

context-M系列(主要是微控制器内核,比如单片机的内核,主频中等,200MHZ左右,用于工业工业控制(机器人电机控制器等),需要实时处理和低功耗(蓝牙手环),稳定性要求高的场合,这个内核具有较强的抗干扰能力,具有快速的中断响应速度微秒级别,其中分为M0,M1,M3,M4,M7内核,主频和性能依次升高。这些芯片价格便宜,几块到几十块钱的都有)

context-R系列(非常低调的arm内核,主打实时处理器,具有最快的中断响应速度,Cortex-R处理器针对高性能实时应用,例如硬盘控制器(或固态驱动控制器)、企业中的网络设备和打印机、消费电子设备(例如蓝光播放器和媒体播放器)、以及汽车应用(例如安全气囊、制动系统和发动机管理)。Cortex-R系列在某些方面与高端微控制器(MCU)类似,但是,针对的是比通常使用标准MCU的系统还要大型的系统。这些芯片价格巨贵,几百到几千的都有,属于ARM内核中的最强王者了)

从fpga如何构建一个微处理的过程可以看出来,一个单片机芯片内部包含了cpu(专门执行运算和控制),RAM(内存,存程序变量),ROM(硬盘,存程序代码),片上外设(指的是这个单片内部的可用资源,定时器,GPIO,AD,DMA,FPU,串口UART通信接口等等),芯片内部结构如下所示:

ARM公司是设计内核的(相当于是处理器中负责运算和控制的那部分的电路图纸和方案,也是最关键的),他们自己不生产芯片,其它芯片厂商(比如,st,nxp,Ti等等还有一些全世界最出名的芯片厂商)购买了arm公司的内核方案后,就自己在这个内核周围加一些存储器,通信接口,定时器,硬件解码器啥的(有个专有名词,IP核),然后通过通信总线与内核连接起来即可。如上图所示,就是nxp公司生成的conte-M7内核的rt1052单片机内部构造,中间的内核由ARM公司设计得,周围的外设芯片厂商自己设计,然后通过一些通信总线连接起来即可。下面是野火的一个1052处理器核心板,实际上就是一个微型计算机的主板

cpu分为8,16,32,64位类型,指的是通信总线宽度(这里以32位处理器为例),32位cpu的每次参与运算的数据量是4个字节(每个字节由8个二进制位组成),其寄存器(就是一些用于控制和设置处理器运行状态的高速存储器)的位数也32位的。

那我们现在来算一下,因为32位处理器的地址线是32根,每根线只有两个状态,高电平(3.3v),低电平(0v),高电平代表二进制位1,低电平代表0(处理器只有0,1两个数字),那么这32根线都是低电平时候,即0000 0000 0000。。。0000,这个32位的二进制串就是十进制的0,而最大的数就是全部是1,即1111 1111。。。1111,这个是十进制的多少呢,

换算过来是4G(注:1G=1024M,1M=1024K,1K=1024),也就是4亿,也就是这32跟地址线最大可以直接访问的存储空间是4GB(因为是直接寻址,即cpu的32根地址线直接跟内存芯片的32跟地址线相连接的),这也就解释了32位的电脑为什么最大内存条只能装4GB的。补充一下,如果cpu不是32跟寻址地址线直接跟内存芯片相连,那么就属于间接寻址,比如在cpu和内存芯片之间加一个地址转换器电路啥的,这样cpu可以连续送出两个32位数据来拼接成一个64位数据的地址来用,那么就可以访问更加广阔的存储区域,但是这样每次访问内存读取数据就得用两个指令,损耗了cpu性能。比如51单片机是8位的,但是却不仅仅只能访问256字节的存储空间。同理,32位cpu访问外部存储器,比如硬盘,就不是直接32跟地址线直接跟硬盘相连,而是硬盘带有一个数据寻址和交换的控制器,cpu每次寻址硬盘的数据就需要多发几次的指令给这个控制器,控制器再去硬盘里面取出目标数据返回给cpu。

现在我们继续分析处理器的结构,32位单片机cpu为例,为了达到最佳性能,往往跟内存,片上外设(定时器,串口等通信接口啥的啥的)都是32地址线直接相连,使得cpu跟存储器,外设之间交换数据(因为cpu要告诉这些外设你应该干嘛,就得给他们的寄存器发送数据去设置和控制他们,而这些外设又需要反馈数据给cpu,尤其是通信接口外设,需要频繁的跟cpu交换大量数据)需要的指令最少,性能最快。那么也就是说内存,定时器,通信口,GPIO引脚控制器,外存(存代码的flash芯片)的地址都得在这32根地址线的访问区域之内才行,我们再来看看nxp公司的rt1052这个单片机的地址分配吧:

前面我们说过了,32跟地址线的访问地址空间就是00000。。。。00000(32个0)—111111。。。。11111(32个1),写成十六进制形式就是0x00000000—0xffffffff

如上图,我们可以看到地址从下往上依次递增,最前面的0.5GB地址空间被分配给了代码存储器(比如flash芯片等),紧接着的0.5GB分配给了内存(存储程序变量的),再上面分配给了片上外设等等就结束了。芯片厂商在每个块的范围内设计各具特色的外设时并不一定都用得完,都是只用了其中的一部分而已,比如给ram(内存)分配了1.5GB空间,但是真的有必要连接一个这么大的内存条吗,没必要,一般嵌入式处理器仅需要几KB内存就可以了。

好,那我们现在就来具体看看cpu究竟是如何访问这些外设的

其实就是通过 C语言指针的操作方式,比如GPIO1(就是处理器芯片的引脚,要么输出高电平或者低电平)是片上的一个外设,假如GPIO1这个外设的地址是0x401B8000(已经写成16进制形式了),现在cpu想要访问这个GPIO1外设,就是通过地址访问它,

比如我们想要把这个外设全部设为1,也就是 GPIO1 端口(对应处理器芯片的引脚)全部输出 高电平,c语言代码就是:
*(unsigned int*)(0x401B8000) = 0xFFFFFFFF

*号是c语言中用于给某个地址对应的存储区域赋值的作用,上面就是把0x401B8000这个地址对应的区域(即GPIO1外设)全部赋值为1,所以用电压表测量芯片对应的引脚全部是高电平。因为0x401B8000这个地址难以记忆,那我们就给他取个名字,方便进行操作,就是用c语言中的宏定义,然后直接对这个新名字赋值就可以了,方便使用,即:
#define GPIO1_DR *(unsigned int*)(0x401B8000)
GPIOF_DR = 0xFFFFFFFF;

同理,假如我们在程序中声明了一个变量 int a;这个过程实际上是编译器会在内存条芯片中分配一个4字节存储区,同时把这个存储区取名为a,那cpu怎么找到这个存储区呢,这个名字为a的存储区(连续4字节空间)有个首地址吧(因为它是连续4个字节存储区域,那么每一个字节区域就会对应一个地址,那不就有4个地址了,这里我们只需要首地址就可以了(如果想了解具体原因可以看我这篇文章内存管理原理)),那cpu就是通过那32根地址线访问这个存储区首地址从而访问到这个存储区,那这个名字为a的存储区的首地址是多少呢,就是&a,这里用到了C语言的另外一个符号“&”取地址符,用于获取到一个存储区的首地址的,有了这个首地址后,根据上面说的,我们的cpu就可以通过“ * ”号访问和操作这块区域了,比如我们给这个名字为a的存储区写入一个1,就是  *(&a) = 1;但是这样c语言编译器开发者为了形式更简便,就直接可以对存储区的名字操作即可

即:a = 1;这两个是一样的作用,都是对名字为a的存储区进行赋值

假如我们要读取这个首地址对应的存储区域,就是这样 int b =  *(&a);同理,简便形式就是 int b = a;

假如我们要读取存储区a的下一个存储区,就int b =  *(&a + 1)即可,就是得到了区域a的地址后,再把这个地址加1,然后再用“ * ”号指向这个新的存储区域即可访问到了。我们感觉这样还是有点死板,有没有更加灵活一点的方法,当然有,

这里我们声明一个新的变量(就是开辟一个新的存储区域)取名为p,专门用来存放这32跟地址线马上要访问的地址,这样p存放的这个地址值就可以随时更换(那么这个p就叫做指针变量),cpu就可以灵活的访问任意存储区域了,即:

int *p ; // 声明一个指针变量

p = &a;  // 取变量a的首地址值放入指针变量p中

*p = 1  // 然后指向变量p里面存储的地址所对应的存储区域,不就是变量a了嘛,这样也是实现 a = 1;

int b = *(p+1)// 这样还是实现了:p的值(就是存储区名为a的首地址,简便来说,就是变量a的地址)加1然后对应的存储区内容赋值给了新定义的变量b;可以看出用指针变量来存储地址值,然后操作这个指针变量p即可读取和写入某个存储区,这样就使得程序很灵活了,操作也更简便。这里我们再来思考这样一个问题,假如我的指针变量p赋值任意一个地址值,

比如0x000.。。0000011(这个地址对应的区域很可能是操作系统或者其他软件的存储区域),现在我们指向这个存储区域且给这个区域赋值任意一个数,那岂不是可能会破坏系统正常的工作状态呢,从c语言的角度来说这个是完全可能的,但是计算机针对这个隐患又有了另外一个保护机制,就是在cpu内部加了一个访问控制电路,内存保护单元(memory protection unit),简称:MPU,用于防止越界访问非法区域,有兴趣具体了解的可以看我这篇文章。

关于地址操作的例子还有很多很多,但是都是cpu在和存储区或者外设的地址不断打交道的过程

可以看出c语言为什么这么强大了吧,其实c语言是最接近底层的高级语言了,因为它有指针,可以直接操作内存哪个哪个区域,所以一般底层驱动开发,嵌入式软件开发等等都是用c语言的多。只要合理的利用指针,从而程序更加灵活高效(但是也容易出错),所以对程序员的要求也会更高。而其他高级编程语言比如java,Python等等已经摒弃了指针的概念,降低了编程难度和灵活度,也降低了程序运行效率,但是反而提高了开发效率。

接下来就是通电后,cpu开始执行启动文件的代码

启动文件由汇编编写,是系统上电复位后第一个执行的程序。主要做了以下工作: 
1、设置栈指针SP=_initial_sp 
2、设置PC指针=Reset_Handler // 复位中断函数里会进行所有寄存器等的初始化
3、配置系统时钟
4、调用C库函数__main函数,最终调用用户编写的main函数

此时来到了bootloader引导程序的main函数了(关于bootloader程序的具体过程可以看我这篇文章,嵌入式处理器中Bootloader程序是什么以及IAP设备固件更新原理),在这个函数里会进行系统的选择,win8还是Ubuntu等等,如果不选择,默认就进入第一个系统。假如是嵌入式处理器,我们选择了ucos操作系统为例(所有操作系统原理都是一样的),里面会进行快速任务调度和切换,根据任务的优先级不同,cpu快速在各个任务之间依次切换执行,实现了多个软件任务宏观上的并行执行的效果,但是微观来看,cpu还是一个一个软件任务执行的,只是切换速度太快(叫做时间片,一般为一毫秒),给我们的感觉是多个软件任务同时运行罢了。

当我们按下某个按键时,触发cpu的外部io中断,根据启动文件中设置好的中断向量表找到对应的中断处理函数,进行相应的处理,如果是跑了操作系统,系统会发送出一个某某按键按下的消息,那么我们在自己的软件任务程序中只需要获取这个消息,从而执行对应的操作,比如移动游戏人物前进等等。

猜你喜欢

转载自blog.csdn.net/kangkanglhb88008/article/details/89605844
今日推荐