在学习C语言的时候,我们会写个程序来入门,当我们写ARM程序,也该有一个简单的程序引领我们入门,这个程序就是点亮LED。
查看原理图,确定控制LED的引脚:

如上图是四种常见的LED驱动电路:
- 有的芯片为了省电等原因,其引脚驱动能力不足,这时可以使用三极管驱动。
但是对于我们写程序来说,不用关心输出的是3.3V还是1.2V,只需要知道引脚输出的是高电平还是低电平,简称输出1或0。
- 逻辑1–>高电平
- 逻辑0–>低电平
芯片操作引脚:

如上图,本喵的上,控制引脚输出0点亮LED灯,这里仅控制Red的LED灯,操作的引脚是PB0。
- 使能GPIOB组引脚:
从芯片手册上查找相关寄存器:

如上图是寄存器,用来控制不同组GPIO的使能,将该寄存器的bit3设置为1就使能了GPIOB。
那我们怎么找到这个寄存器呢?

如上图是不同寄存器所在的地址范围。是用来使能外设时钟的,它的基地址是。
又因为寄存器的偏移地址是,所以该寄存器的绝对地址就是。
- 设置GPIOB0为输出模式:

如上图,再从这张表中找到GPIOB的基地址是。

如上图所示是寄存器,x是引脚编号,本喵这里使用的是PB0,所以x就是0,需要配置和。该寄存器的偏移量是,所以该寄存器的绝对地址就是。
的两个比特位配置为11,表示输出,并且输出速度设置最大,此时电平变化最快。
的两个比特位是配置输出模式的,这里仅是点灯,使用默认值即可。
- 设置引脚电平:

如上图是寄存器,用来控制引脚的输出电平,根据偏移地址得到它的绝对地址是。
由于是PB0,所以控制它的bit0即可,该位是1,输出1,该为是0,输出0。
要实现或者,不能直接,这样虽然能让bit0为1,但是该寄存器的其他位被置0了。
- 操作寄存器的某一位时,不能影响其他位。
这样的方式就仅在操作bit0,其他位并不影响。

如上图所示寄存器,它的绝对地址是,对于PB0,只需要操作位和位。
写1,输出1,写1,输出0,这些位写0没有任何影响,此时就可以仅操作这一个寄存器即可,效率较高。
- 编程:

如上图所示启动文件中的汇编代码,蓝色框中的是语法规定,必须有的,暂时不用管它。
板子一上电以后会去向量表处开始执行代码,执行到后会开始调用我们自己写的函数,在调用之前需要设置一下栈顶,然后跳转到我们自己写的函数中去执行。

如上图本喵自己实现的函数中,先创建一个32位的指针变量,用来访问寄存器。
- 让指针指向寄存器的地址,将该寄存器的bit3置一,使能GPIOB。
- 让指针指向寄存器的地址,将该寄存器的bit0置一,设置PB0为输出模式。
- 让指针指向寄存器的地址,将该寄存器的bit0设置为1,让PB0输出1,然后延时,再将bit0设置为0,然后再延时,如此反复。
然后编译工程,并将程序烧录到开发板中,可以看到板子上红色的LED灯在闪烁,本喵这里就不贴图了。
ARM架构:
在上面点灯的过程中,本喵在访问寄存器的时候,完全就是在使用C语言的指针来访问内存地址,为什么这样做就可以访问到寄存器呢?

如上图示意图所示,在ARM架构的CPU中,内存,各种片内外设,如,控制器等都是统一编址的,它们的地址是连续的,从到。
CPU在访问不同的地址时,会将地址先发给内存控制器,由内存控制器去访问地址读取数据。
- 所以在CPU眼里,这些外设以及内存的访问方法都是一样的。
x86架构:

如上图所示是x86架构CPU访问内存和外设的示意图,这里的内存和IO空间中的外设就不是统一编址的,而是分隔开的。
内存的地址范围是,IO空间的范围是,这两个空间在的地址范围是重复的,CPU通过不同指令来访问不同的空间。
当CPU要访问内存空间的时候,就使用指令,当CPU要访问IO空间的时候,就使用指令。
精简指令集计算机:

如上图所示的乘法运算a = a * b,RISC中要使用4条汇编指令:
复杂指令集计算机:
但是对于程序员来说,他看不到“微程序”,他好像用一条指令就搞定了这一切!
这里提到x86架构以及CISC仅仅是为了和ARM架构以及RISC作一个对比,我们使用的是ARM架构以及RISC。
ARM架构中对于数据的运算是在CPU内部实现的,在内部用什么来保存上面乘法运算中的a,b,以及a * b的结果呢?
cortex-M3/M4中寄存器示意图:

如上图所示是CPU中寄存器示意图。
CPU内部都有R0、R1、……、R15寄存器,它们可以用来“暂存”数据。
对于R13、R14、R15,还另有用途:
- R13:别名SP(Stack Pointer),栈指针。
- R14:别名LR(Link Register),用来保存返回地址。
- R15:别名PC(Program Counter),程序计数器,表示当前指令地址,写入新值即可跳转。
其中R13就是汇编指令里使用的,但是它有两个寄存器,一般情况下使用的是寄存器,运行RTOS的时候,任务使用的是寄存器。
- 在编程的时候直接使用SP,根据不同情况会自动调用相应的栈寄存器。

如上图所示,在程序执行的过程中,PC寄存器会按照顺序读取指令并去执行。
这16个寄存器之外还有一个寄存器,用来保存程序状态,保存上一条指令的执行结果,比如比较结果。还有一些控制作用,比如屏蔽中断、使能中断。
对于cortex-M3/M4来说,实际上对应3个寄存器:

这3个寄存器的含义如上图所示,其实就是3个寄存器使用32位中的不同比特位,所以就用一个程序状态寄存器来表示了3个寄存器。

如上图所示就是组后和的真实寄存器。

如上图所示是寄存器中不同位所表示的意义。
这3个寄存器,可以单独访问,使用下面汇编指令:
- MRS R0, APSR :读APSR
- MRS R0, IPSR :读IPSR
- MSR APSR, R0 :写APSR
这3个寄存器,也可以一次性访问:
- MRS R0, PSR :读组合程序状态
- MSR PSR, R0 :写组合程序状态
一开始,ARM公司发布两类指令集:
要节省空间时用Thumb指令,要效率时用ARM指令。
一个CPU既可以运行Thumb指令,也能运行ARM指令。通过程序状态寄存器中有一位,名为“T”,它等于1时表示当前运行的是Thumb指令。
现在有一种情况,函数A是使用Thumb指令写的,函数B是使用ARM指令写的,可以往PC寄存器里写入函数A或B的地址,就可以调用A或B。
但是怎么让CPU在执行A函数是进入Thumb状态,在执行B函数时进入ARM状态?
- 调用函数A时,让PC寄存器的BIT0等于1,即:PC=函数A地址+(1<<0);
- 调用函数B时,让PC寄存器的BIT0等于0:,即:PC=函数B地址
根据函数地址的bit0位来判断这是用Thumb指令写的还是用ARM指令写的。
这样做非常的麻烦,所以后来又引入了指令集,它支持16位指令、32位指令混合编程。
有那么多指令集:ARM、Thumb、Thumb2,难道都要记住它们的指令吗?当然不会,ARM公司推出了(Unified Assembly Language),统一汇编语言,你不需要去区分这些指令集。
在程序前面用CODE32/CODE16/THUMB表示指令集:ARM/Thumb/Thumb2
我们在使用中不需要记住多少汇编指令,没必要写很复杂的汇编程序,因为在设置栈后就用C语言来写函数了。
常用的汇编指令只有几类:内存访问、数据处理、跳转、其他指令。
以“数据处理”指令为例,UAL汇编格式为:

- Operation表示各类汇编指令,比如ADD、MOV;
- cond表示conditon,即该指令执行的条件,条件符合就执行,不符合就不执行,该选项可写可不写,条件有:

如上图,这些条件都和程序状态寄存器中的值有关,使用条件的指令之前一定得有改变寄存器的指令。
表示该指令执行后会去修改程序状态寄存器,也是可写可不写。
- Rd为目的寄存器,用来存储运算的结果;
- Rn、Operand2是两个源操作数
下面本喵用一款神器来讲解一下常用汇编指令的用法,这是一款ARM汇编模拟器。
LDR:Load Register

如上图所示是该指令的用法,作用就是从内存中读取数据到寄存器中,其中表示读取数据的类型,如B就是无符号的一个字节数据,该选项可写可不写。
STR:Store Register

如上图所示就是该指令的用法,作用就是将数据从寄存器中写入到内存中。

如上图所示汇编代码,在执行的过程中在右侧的红色框中可以看到寄存器中值的变化,在下侧的框中可以看到内存中的值。根据回调代码中的注释很容易看懂意思。
是一个最基本的汇编指令,表示数据的移动,从源操作数移动到目的操作数,如上面中的中,将移动到寄存器中。
表示R0=R0+8,就是运算完以后要改变寄存器中的值。
是一个数据左移指令,就相当于C语言中的操作符,如上面的表示将中的值左移4位然后加到上,最后将寄存器中的值存放到中值所代表的地址处。
LDM:Load Multiple Register

如上图所示是的用法,作用是从多个地址处将数据读取到多个寄存器中。
: 会影响CPSR, 在讲异常时再细讲
这里的表示地址,如表示将R0,R0+4,R0+8地址处的数据读取到R1,R2,R3寄存器中。
STM:Store Multiple Register

如上图所示是的用法,作用是将多个寄存器中的值写到多个地址处。这里也表示地址。选项和的用法一样。

如上图所示是将寄存器中R1,R2,R3中的1,2,3放入到R0中的起始地址处时,使用的四种方式。
上面是汇编代码,下面是执行完毕后内存中的值,这个过程中,高地址放编号高的寄存器中值。
四种栈
根据栈指针指向,可分为满(Full)/空(Empty):
- 满SP指向最后一个入栈的数据,需要先修改SP再入栈。

- 空SP指向下一个空位置,先入栈再修改SP。

根据压栈时SP的增长方向,可分为增/减:
- 增(Ascending):SP变大。
- 减(Descending):SP变小。
- 组合后,就有4种栈:满增、满减,空增,空减。
常用的的栈为“满减”:
- 入栈时用STMDB,也可以用STMFD,作用一样,表示入栈之前先减小SP。
- 出栈时用LDMIA,也可以用LDMFD,作用一样,表示出栈之后再增加SP。

如上图代码所示,使用将数字1,2,3入栈,此时R13也就是SP寄存器的值是,因为从开始减了12个字节,此时内存中的值也符合。
然后将R1,R2,R3寄存器中的值清0,然后再使用将栈中的1,2,放入到寄存器中。
加法指令:
减法指令:
进行减法运算的时候,发生借位时会改变程序状态寄存器中的N位。
位操作:
VisUAL里不支持(1<<4)这样的写法,写成。
比较:
比较的本质就是在做减法,用第一个操作数减去第二个操作数,比较的结果会改变程序状态寄存器中的N位和Z位。
C程序中,函数A调用函数B的实质是:跳转去执行函数B的代码,函数B执行完后,还要回到函数A继续执行后面的代码。
对应的汇编指令就是跳转指令:
- B:Branch,跳转
- BL:Branch with Link,跳转前先把返回地址保持在LR寄存器中
- BX:Branch and eXchange,根据跳转地址的BIT0切换为ARM或Thumb状态(0:ARM状态,1:Thumb状态)。
- BLX:Branch with Link and eXchange,跳转前先把返回地址保持在LR寄存器中,根据跳转地址的BIT0切换为ARM或Thumb状态(0:ARM状态,1:Thumb状态)。
由于使用的是Thumb2指令集,所以只使用和两条跳转指令。

如上图,跳转时可以加条件,不用写。

如上图,使用B跳转指令跳转到延时函数中,然后让R0减1,再使用来判断跳转到哪里,当R0不为0时,在和之间执行。
在减法指令的基础上增加了,表示执行完后会影响程序状态寄存器的值。

在运行过程中,中的Z位始终为0,表示R0的值不为0。是在跳转指令的基础上增加了条件,代表的条件就是中的Z为0。
此时符合条件,所以跳转到继续执行,从而实现延时。但是这个延时函数执行完毕后无法获得返回地址,因为跳转指令不会保存返回地址到寄存器中。

如上图所示,使用跳转指令,在跳转之前会将返回地址存入到寄存器中,如上图所示,中的值是,由于当前地址是第一行,返回地址就是下一条指令的地址,也就是第2行,又因为指令是32位指令,所以增加4。
当延时结束以后,将中的返回地址直接赋值给寄存器,程序从第2行开始执行。
这样一条指令:意图是把这个值存入R0寄存器,那么可以是任意值吗?不可以
- 直接给寄存器赋值的数必须是立即数。
假设可以是任意数,本身是16位或32位,哪来的空间保存任意数值的VAL?所以,VAL必须符合立即数的规定:

如上图是立即数必须符合的规则,但是由我们去判断一个数是否是立即数会比较麻烦,并且我就想把任意数值赋值给R0,这时就可以使用伪指令。
LDR伪指令:
“伪指令”,就是假的、不存在的指令。编译器会把“伪指令”替换成真实的指令,比如:
- 中是立即数,那么替换为:。
- 中不是立即数,那么替换为:使用读内存指令读出值,offset是链接程序时确定的。
编译器在程序某个地方保存有这个非立即数的值,需要赋值的时候就来这个地府读取。
- 注意作为“伪指令”时,指令中有一个“=”,否则它就是真实的LDR(load regisgter)指令了。
ADR伪指令:
比如,要将标号的地址读取到R0中,它是伪指令,会被转换成真实的指令,在连接的时候确定。

如上图,之前的延时程序可以使用伪指令直接将返回地址赋值给寄存器,将函数的地址直接赋值给寄存器,去执行延时函数。
我们的第1个LED程序涉及2个文件:start.s、main.c,它们的处理过程如下:

如上图所示是程序编译的步骤,最后面的红色框是反汇编,就是将生成的可执行二进制文件变成汇编代码。
- 汇编:汇编文件转换为目标文件(里面是机器码)。
- 反汇编:可执行文件(目标文件,里面是机器码),转换为汇编文件。
KEIL中反汇编:
在KEIL的User选项中,如下图添加这两项:

然后重新编译,即可得到二进制文件led.bin(以后用到)、反汇编文件led.dis。

如上图,只截取led.dis中前面一小段,第一列是地址,第二列是机器码,第三列是汇编代码。

如上图所示是指令集中指令的机器码生成规则,和前面反汇编文件中对应的机器码做对比,可以发现,完全可以对的上。
汇编代码中调用C函数时使用,那如果我想给函数传参呢?在前面编译过程中可以看到,源文件也会被编译成汇编文件,然后所有汇编文件再进行汇编生成目标文件,然后再进行连接。
此时中调用中的函数,这两个文件都是汇编文件,汇编调用汇编传参就容易实现了。在ARM中使用寄存器来传参:

如上图,用于调用者和被调用者之间传参数。
用来保存局部变量,函数可能使用它们,所以在函数的入口保存它们,在函数的出口恢复它们。
是特殊用途的寄存器。
上面的C代码转换成汇编后调用delay时如下:
可以看到,在调用之前,直接将赋给寄存器,然后使用调用,此时就通过R0进行了传参。
函数调用结束后,函数的返回值也保存在寄存器中。

如上图是我们生成的反汇编文件,其中机器码是烧写到Flash上的,汇编码只是为了方便我们阅读。
每条指令会对应一个地址,如上图中的,这个地址在Flash中是真实存在的,Flash中的地址也是按照上图中指令的地址这样分布的。
烧到Flash上
如上表所示,烧到Flash上的内容只有机器码,它自动放在与每条指令相对应的地址上。
启动流程:
上电后:
- 设置栈:CPU会从0x0读取值,用来设置SP(我们的程序里在调用mymain之前设置了SP)
- 跳转:CPU从0x0得到地址值,根据它的BIT0切换为ARM状态或Thumb状态,然后跳转
- 对于cortex M3/M4,它只支持Thumb状态,所以0x0上的值bit0必定是1
- 0x0上的值 = Reset_Handler + 1
- 从Reset_Handler继续执行,使用BL调用我们的mymain函数开始执行C代码。

如上图所示汇编代码,上电后,程序会执行处,开始执行汇编代码,步骤和C语言的一样。
- 将RCC_APB2ENR寄存器的绝对地址赋值给R0,然后将bit3置为1,使能GPIOB。
- 将GPIOx_CRL寄存器的绝对地址赋值给R0,然后将bit0置位1,设置PB0为输出模式。
- 将GPIOx_ODR寄存器的绝对地址赋值给R2,然后控制它的bit0位来控制引脚输出0和1。
- 将bit0设置成1,LED灯点亮,然后延时
- 将bit0设置成0,熄灭LED灯,然后延时
- 再使用指令跳转回Loop处,循环点亮
在调用延时函数时使用的是指令,在延时函数中,使用判断R0中的值是否为0,延时结束后将LR中的返回地址赋值给PC寄存器。
调用延时函数时,通过寄存器R0传参。
将程序编译并烧录到开发板中,可以看到LED灯在闪烁,本喵这里也不贴图了。
这篇文章中,要对ARM架构有一个框架性的认识,知道CPU是怎么访问内存的,还有要记住这几条常用的汇编指令,其他复杂的指令遇到时自行百度查阅即可。
要明白调用函数是如何传参的,以及板子上电后,程序的执行流程,包括Flash中存放的是什么。
到此这篇ldr指令和ldr伪指令有什么不同(ldr指令什么意思)的文章就介绍到这了,更多相关内容请继续浏览下面的相关推荐文章,希望大家都能在编程的领域有一番成就!版权声明:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权、违法违规、事实不符,请将相关资料发送至xkadmin@xkablog.com进行投诉反馈,一经查实,立即处理!
转载请注明出处,原文链接:https://www.xkablog.com/rfx/11906.html