前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >5_LED程序涉及的编程知识

5_LED程序涉及的编程知识

作者头像
韦东山
发布2022-05-05 18:19:12
4860
发布2022-05-05 18:19:12
举报
文章被收录于专栏:韦东山嵌入式韦东山嵌入式

第五章 LED程序涉及的编程知识

5.1 ARM架构的简单介绍

​ 目前IMX6UL是使用Cortex-A7架构,本小节简单介绍一下Cortex-A7架构的基础知识,比如运行模式、寄存器组等。

​ 参考资料:

  • 文件原名DEN0013D_cortex_a_series_PG.pdf
  • 文档全名ARM® Cortex™-A Series Version: 4.0 Programmer’s Guide.pdf
  • 文档所在目录: 资料光盘 00_UserManual\参考资料\Arm架构参考资料\ARMv7编程手册(DEN0013D_cortex_a_series_PG).pdf
  • 参考章节: 3: ARM Processor Modes and Registers

5.1.1 运行模式

​ Cortex-A7架构的运行模式有9种,分别为User、Sys(System)、FIQ、IRQ、ABT(Abort)、SVC(Supervisor)、UND(Undef)、MON(Monitor)、Hyp模式,如下表:

模式

描述

User

用户模式,非特权模式,大部分程序运行的 时候就处于此模式

Sys(System)

系统模式,用于运行特权级的操作系统任务

FIQ

快速中断模式,进入 FIQ 中断异常

IRQ

一般中断模式

ABT(Abort)

数据访问终止模式,用于虚拟存储以及存储保护

SVC(Supervisor)

超级管理员模式,供操作系统使用

UND(Undef)

未定义指令终止模式

MON(Monitor)

用于安全扩展模式

Hyp

用于虚拟化扩展

​ 除了User模式属于非特权模式,其它8种处理器模式都是特权模式。

​ 运行模式可以通过软件进行任意切换,也可以通过中断或者异常来进行切换。大多数的程序都运行在用户模式,用户模式下是不能访问系统所有资源的,有些资源是受限的,要想访问这些受限的资源就必须进行模式切换。但是用户模式是不能直接进行切换的,用户模式下需要借助异常来完成模式切换,当要切换模式的时候,应用程序可以产生异常,在异常的处理过程中完成处理器模式切换。

5.1.2 寄存器组

​ 本节我们要讲的是 Cortex-A7 内核寄存器组,而不是芯片外设寄存器。

​ 上一小节我们讲了 Cortex-A7 有 9 种运行模式,每一种运行模式都有一组与之对应的寄存器组,如下图:

​ 浅色字体是与 User 模式所共有的寄存器,浅蓝色背景是各个模式所独有的寄存器,即在所有的模式中,低寄存器组(R0~R7)是共享同一组物理寄存器的,只是一些高寄存器组在不同的模式有自己独有的寄存器,比如 FIQ 模式下 R8~R14 是独立的物理寄存器。

​ 如果某个程序处于 FIQ 模式下访问寄存器 R13(SP),那它实际访问的是寄存器 SP_fiq

​ 如果某个程序处于 SVC 模式下访问寄存器 R13(SP),那它实际访问的是寄存器 SP_svc

9 种运行模式的寄存器合计有34个,可以分为:

  1. 未备份寄存器,即 R0~R7
  2. 备份寄存器,即 R8~R14
  3. 程序计数器 ,即 R15
  4. 程序状态寄存器

下面一一介绍以上4类寄存器。

5.1.2.1 未备份寄存器

​ 未备份寄存器指的是 R0R7,因为在所有的运行模式下R0R7寄存器都是同一个物理寄存器,在不同的模式下,R0R7寄存器中的数据就会被破坏,所以R0R7寄存器并没有被用作特殊用途。

5.1.2.2 备份寄存器

​ 备份寄存器中的 R8~R12 寄存器有两种物理寄存器,在快速中断模式下(FIQ)它们对应着Rx_irq(x=8~12)物理寄存器,其他模式下对应着 Rx(8~12)物理寄存器。FIQ 是快速中断模式,这个中断模式要求快速执行!因为 FIQ 模式下的 R8~R12 是独立的,因此中断处理程序可以不用执行保存和恢复中断现场的指令,从而加速中断的执行过程。

​ 备份寄存器 R13(SP) ,也叫栈指针,有 8 个物理寄存器,其中一个是User和Sys模式共用的,剩下的 7 个分别对应 7 种不同的模式。

​ 备份寄存器 R14(LR) ,也叫链接寄存器,有 7 个物理寄存器,其中一个是User、Sys和Hyp模式所共有的,剩下的 6 个分别对应 6 种不同的模式,主要有如下用途:

​ 使用 R14(LR)来存放当前子程序的返回地址,如果使用 BL 或者 BLX来调用子函数的话,R14(LR)被设置成该子函数的返回地址,在子函数中,将 R14(LR)中的值赋给 R15(PC)即可完成子函数返回,如mov pc,lr

5.1.2.3 程序计数器

​ 程序计数器 R15(PC),保存着当前执行指令地址值加 8 个字节

​ 因为ARM处理器是三级流水线:取指->译码->执行,循环执行。比如当前正在执行第一条指令的同时也对第二条指令进行译码,第三条指令也同时被取出存放在 R15(PC)中,即 R15(PC)总是指向当前正在执行指令地址再加上 2 条指令的地址,对于 32 位的 ARM 处理器,每条指令是 4 个字节,

​ 所以R15(PC) = 当前执行指令地址 + 8个字节

5.1.2.4 程序状态寄存器

​ 程序状态寄存器PSR可以分成当前程序状态寄存器CPSR与备份程序状态寄存器SPSR。

​ 所有运行模式都共用一个 CPSR 物理寄存器,因此 CPSR 可以在任何模式下被访问,该寄存器包含条件标志位、中断禁止位、当前运行模式标志等一些状态位以及一些控制位。但是所有运行模式都共用一个 CPSR 必然会导致冲突,因此除了 User 和 Sys 模式以外,其他 7 个模式都配备一个专用的物理状态寄存器,叫做 备份程序状态寄存器(SPSR),当特定异常中断发生时,SPSR用来保存CPSR的值,当异常退出以后可以用 SPSR 中保存的值来恢复 CPSR。

​ 由于 SPSR 是 CPSR 的备份,因此 SPSR 和 CPSR 的寄存器结构相同,如下图:

​ N(bit31):当两个有符号整数运算(补码表示)时,结果用N表示,N=1/0 表示 负数/正数

​ Z(bit30):对于 CMP 指令,Z=1 表示进行比较的两个数大小相等

​ C(bit29):

​ 在加法指令中,当结果产生了进位,则C=1,表示无符号数运算发生上溢,其它情况下 C=0

​ 在减法指令中,当运算中发生借位,则C=0,表示无符号数运算发生下溢,其它情况下 C=1

​ 对于包含移位操作的非加/减法运算指令,C 中包含最后一次溢出的位的数值

​ 对于其它非加/减运算指令,C 位的值通常不受影响

​ V(bit28):对于加/减法运算指令,当操作数和运算结果表示为二进制的补码表示的带符号数时,V=1 表示符号位溢出,通常其他位不影响 V 位

​ Q(bit27):仅 ARM v5TE_J 架构支持,表示饱和状态,Q=1/0 表示累积饱和/累积不饱和

​ IT1:0 和 IT7:2一起组成 IT[7:0],作为 IF-THEN 指令执行状态

​ J(bit24)和T(bit5):控制指令执行状态,表明本指令是ARM指令还是Thumb指令,如表

J

T

描述

0

0

ARM

0

1

Thumb

1

1

ThumbEE

1

0

Jazelle

​ GE3:0:SIMD 指令有效,大于或等于

​ E(bit9):大小端控制位,E=1/0 表示大/小端模式

​ A(bit8):禁止异步中断位,A=1 表示禁止异步中断

​ I(bit7):I=1/0 代表 禁止/使能 IRQ

​ F(bit6):F=1/0 代表 禁止/使能 FIQ

​ M[4:0]:运行模式控制位,如表

M[4:0]

运行模式

10000

User 模式

10001

FIQ 模式

10010

IRQ 模式

10011

Supervisor(SVC)模式

10110

Monitor(MON)模式

10111

Abort(ABT)模式

11010

Hyp(HYP)模式

11011

Undef(UND)模式

11111

System(SYS)模式

5.2 汇编与机器码、汇编指令

参考资料:

  • 文件原名DDI0406C_d_armv7ar_arm.pdf
  • 文档全名ARM® Architecture Reference Manual ARMv7-A and ARMv7-R edition
  • 文档所在目录: 资料光盘 00_UserManual\参考资料\Arm架构参考资料\ armv7 ar架构参考手册 学习CPU架构、内存及系统架构(DDI0406C_d_armv7ar_arm).pdf
  • 参考章节: A5: ARM Instruction Set Encoding

根据指令复杂度来区分,所有CPU可以分为2类:

  1. CISC 复杂指令集计算机,Complex Instruction Set Computer,比如x86
  2. RISC 精简指令集计算机,Reduced Instruction Set Computing,比如ARM,RISC-V

比如,对于加法运算:a = a + b,它涉及4个步骤的操作:读出a,读出b,计算a+b,把结果写回a。

  1. 使用CISC(复杂指令集计算机,比如x86)提供的加法指令,只需要一条指令即可完成这4步操作。当然,这一个指令需要多个CPU周期才可以完成。
  2. 而RISC不提供“一站式”的加法指令,需调用四条单CPU周期指令完成两数相加:内存a加载到寄存器,内存b加载到寄存器,两个寄存器中数相加,寄存器结果存入内存a

​ ARM芯片属于精简指令集计算机(RISC:Reduced Instruction Set Computing),它所用的指令比较简单,有如下特点:

  1. 对内存只有读、写指令
  2. 对于数据的运算是在CPU内部实现
  3. 使用RISC指令的CPU复杂度小一点,易于设计

5.2.1 汇编与机器码

​ 上面的例子中,数值a原来是保存在内存里的,执行了某条指令后,它的值被读入内存,那问题来了:

  1. 什么指令,可以让CPU从内存里把数据读进来? 比如:
代码语言:javascript
复制
mov r0, #addr_a // 把变量a的地址传给CPU寄存器r0
ldr r1, [r0]    // 从r0所指的内存把数值读进CPU寄存器r1
  1. 读进来后,这个数保存在哪里? 当然是保存在CPU内部了,存在某个寄存器里,上面的代码用寄存器r1来保存该值
  2. 如何处理数据? CPU执行加法指令,比如:
代码语言:javascript
复制
add r1, r1, r2 // 在CPU内部,r1=r1+r2
  1. 最终数据怎么写入内存? CPU执行指令,比如:
代码语言:javascript
复制
str r1, [r0]   // 将r1的值写入r0所指的内存

​ 上面例子中,mov、add、ldr、str等都是汇编指令,或者说它们是“助记符”──帮助我们记忆的。记忆什么呢?这些指令其实是一个一个数值,我们去记这些数值有难度,所以就用mov表示某个指令的数值,用add表示某个指令的数值,对应的,这些指令的数值就是机器码,即汇编指令是机器码的助记符

ARM指令机器码是有一定格式,如下:

cond

op1

op

指令类型

not 1111

00x

-

数据处理和杂项指令,如MOV

not 1111

010

-

加载/存储指令,如LDR/STR

not 1111

011

0

加载/存储指令,如LDR/STR

not 1111

011

1

媒体指令(英文:Media instructions)

not 1111

10x

-

分支指令,如B、BL; 块数据传输指令,如 LDM/STM、POP/PUSH

not 1111

11x

-

协处理器指令

1111

-

-

无条件指令,如BL

​ 下面讲解几种常用的汇编指令。

​ 参考资料: ARM® and Thumb®-2 Instruction Set Quick Reference Card.pdf (ARM指令快速参考卡)

​ 文档所在目录: 资料光盘 00_UserManual\参考资料\Arm架构参考资料\ ARM® and Thumb®-2 Instruction Set Quick Reference Card.pdf

5.2.2 汇编指令

​ 汇编指令的格式,如下:

代码语言:javascript
复制
label:                  
	instruction @ comment

label,即标签,表示地址位置,可以通过label得到指令/数据地址

instruction,即指令,表示汇编指令或伪指令

@ comment,@表示后面是注释,comment表示注释内容

​ 比如:

代码语言:javascript
复制
add:                                
	mov r0, #0 @ 将R0寄存器设置成0

​ 上面汇编代码中,add表示标签,mov r0, #0表示指令,@ 将R0寄存器设置成0 表示 注释

​ 常用的汇编指令一般有mov、bl/b、add/sub、ldm/stm、push/pop等等,下面一一介绍。

5.2.2.1 mov
代码语言:javascript
复制
mov r1, #10  @ 将10赋值给寄存器r1,即r1=10

​ 指令执行过程,如下:

  1. 取指 ​ 假设从内存的addrA地址取机器码e3a0100a(即mov r1, #10指令)
  2. 译码 ​ 原来是MOV指令
  3. 执行 ​ CPU内部寄存器R1等于10 ​ 其中,机器码e3a0100a,MOV指令各位的解析如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-klSoVHWv-1642059925428)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/LED_Program_Knowdge_image006.png)]

​ [31:28]位是条件码0xe;[15:12]位是寄存器R1,即0x1;[12:0]位是立即数10,即0x00a

5.2.2.2 bl
代码语言:javascript
复制
1 bl test_tag
2 mov r1, #10
3 
4 test_tag:
5 	mov r3, #0
6 	mov pc, lr

​ 第1行,跳转到test_tag标签处执行mov r3, #0指令,并且将mov r1, #10指令的地址存储到 LR 寄存器

​ 第6行,返回到mov r1, #10指令地址,并且执行mov r1, #10指令

​ 指令执行过程,如下:

  1. CPU从内存的addrA地址取机器码eb000000(即bl test_tag指令),执行后,PC跳转到test_tag标签位置,即内存的addrA+8地址,从上图可知,其实test_tag标签的地址是mov r3, #0指令的地址。同时自动将内存的addrA+4地址存储在寄存器LR中
  2. CPU从内存的addrA+8地址取机器码e3a03000(即mov r3, #0指令),执行,CPU内部寄存器R3等于0
  3. CPU从内存的addrA+12地址取机器码e1a0f00e(即mov pc, lr指令),执行,PC跳转到内存的addrA+4地址
  4. CPU从内存的addrA+4地址取机器码e3a0100a(即mov r1, #10指令),执行,CPU内部寄存器R1等于10 其中,机器码eb000000,BL指令各位的解析如下:

​ imm[23:0]是PC值与标签的偏移值除以4,但是此处的偏移值是0,为什么尼?这是因为ARM采用三级流水线的方法,即取指、译码、执行指令。所以当ARM执行addrA地址的bl test_tag指令时,但是PC已经指向addrA+8地址进行取mov r3, #0指令,所以此处的偏移值是0

5.2.2.3 b
代码语言:javascript
复制
1 b test_tag
2 mov r1, #10
3
4 test_tag:
5 	mov r3, #0

​ 第1行,只是跳转到test_tag标签处执行mov r3, #0指令,没有跳转回去执行mov r1, #10指令

​ 指令B与指令BL,大同小异,此处就不一一分析了,可以参数指令BL,它们的区别:是否将B/BL指令的下一条指令的地址存储到寄存器LR,BL指令会存储,B指令不会存储。

5.2.2.4 add/sub
代码语言:javascript
复制
1 mov r1, #10
2 add r2, r1, #4
3 sub r2, r1, #4

​ 第1行,将寄存器r1加上4后,赋值给寄存器r2

​ 第2行,将寄存器r1减去4后,赋值给寄存器r2

​ 指令执行过程,如下:

​ CPU从内存的addrA+4地址取机器码e2812004(即add r2, r1, #4指令),执行后,CPU内部寄存器R2等于14

​ CPU从内存的addrA+8地址取机器码e2412004(即sub r2, r1, #4指令),执行后,CPU内部寄存器R2等于6

​ 其中,机器码e2812004,ADD指令各位的解析如下:

​ [19: 16]位是源寄存器R1,即1;[15: 12]位是目标寄存器R2,即2;[11: 0]位是立即数4,即0x004;

​ 其中,机器码e2412004,SUB指令各位的解析如下:

​ [19: 16]位是源寄存器R1,即1;[15: 12]位是目标寄存器R2,即2;[11: 0]位是立即数4,即0x004;

5.2.2.5 ldr/str
代码语言:javascript
复制
1 mov r0, #400H @ 0x400
2 mov r1, #aH   @ 0xa
3 str r1, [r0]
4 ldr r2, [r0]

​ 第3行,将寄存器R1的值0xa存储到寄存器R0指向的地址0x400

​ 第4行,将寄存器R0指向地址0x400的数据赋值给寄存器R2

​ 指令执行过程,如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o1GEe5XB-1642059925431)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/LED_Program_Knowdge_image012.png)]

  1. CPU从内存的addrA地址取机器码e3a00b01(即mov r0, #400H指令),执行后,CPU内部寄存器R0等于0x400
  2. CPU从内存的addrA+4地址取机器码e3a0100a(即mov r1, #aH指令),执行后,CPU内部寄存器R1等于0xa
  3. CPU从内存的addrA+8地址取机器码e5801000(即str r1, [r0]指令),执行后,寄存器R1的0xa数据存储到寄存器R0指向的地址0x400,即内存的0x400地址的值为0xa
  4. CPU从内存的addrA+12地址取机器码e5902000(即ldr r2, [r0]指令),执行后,寄存器R0指向的地址0x400的数据存储到CPU内部寄存器R2,即CPU内部寄存器R2等于0xa 其中,机器码e5801000,STR指令各位的解析如下:

​ [19: 16]位是目标寄存器R0,即0;[15: 12]位是源寄存器R1,即1;

​ 其中,机器码e5902000,LDR指令各位的解析如下:

​ [19: 16]位是源寄存器R0,即0;[15: 12]位是目标寄存器R2,即2;

代码语言:javascript
复制
ldr sp,=0x80200000

​ 这个是一条伪指令,即实际中并不存在这个指令,它会被拆分成几个真正的ARM指令,实现一样的效果,将0x80200000赋值给寄存器sp,即sp=0x80200000

​ 指令执行过程,如下:

​ ldr sp,=0x80200000这条伪指令,被翻译成两条指令来执行,先将0x80200000存储到内存地址addrA+4处,然后通过LDR指令把寄存器SP设置成0x80200000。

​ 如何分析ldr sp, [pc, #-4]指令的机器码e51fd004?读者可以根据上图LDR指令机器码的格式,自行进行分析。温馨提示:imm12[11: 0]位是源寄存器Rn的偏移值。

5.2.2.6 ldm/stm

​ ldm,多数据加载,将某地址的值赋值给某寄存器

​ stm,多数据存储,将某寄存器的值存储到某地址

​ 格式:

代码语言:javascript
复制
ldm{cond} Rn{!}, reglist
stm{cond} Rn{!}, reglist

参数说明:

​ cond:前四个条件是用于数据块操作,后四个条件是用于堆栈操作

​ IA : 每次传送后地址加4,其中寄存器从左到右执行,例如:STMIA R0,{R1,LR} 先存R1,再存LR

​ IB : 每次传送前地址加4,同上

​ DA : 每次传送后地址减4,其中寄存器从右到左执行,例如:STMDA R0,{R1,LR} 先存LR,再存R1

​ DB : 每次传送前地址减4,同上

​ FD : 满递减堆栈

​ FA : 满递增堆栈

​ ED : 空递减堆栈

​ EA : 空递增堆栈

​ Rn:基址寄存器,即寄存器的值是起始地址

​ !:表示最后的地址写回到Rn中

​ reglist:表示寄存器范围,用 , 隔开,如{R1,R2,R6-R9}

​ 数据块操作:

代码语言:javascript
复制
1 ldr r1,=0x10000000     
2 
3 ldmib r1!, {r0,r4-r6}
4 stmda r1!, {r0,r4-r6}

​ 第1行,将起始地址0x10000000赋值给r1

​ 第3行,因为使用ib,所以每次传送前地址加4,具体操作如下:

​ 将0X10000004地址的内容赋值给R0

​ 将0X10000008地址的内容赋值给R4

​ 将0X1000000C地址的内容赋值给R5

​ 将0X10000010地址的内容赋值给R6

​ 由于!,最后的地址写回到R1中,R1=0X10000010

​ 第4行,因为使用da,所以每次传送后地址减4,具体操作如下:

​ 将R6存储到0X10000010地址

​ 将R5存储到0X1000000C地址

​ 将R4存储到0X10000008地址

​ 将R0存储到0X10000004地址

​ 由于!,最后的地址写回到R1中,R1=0X10000000

​ 如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PI07EznT-1642059925434)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/LED_Program_Knowdge_image016.png)]

​ 堆栈操作:满递减堆栈

代码语言:javascript
复制
1 ldr sp,=0x80200000
2
3 stmfd sp!, {r0-r2} @ 入栈
4 ldmfd sp!, {r0-r2} @ 出栈

​ 第1行,将0x80200000赋值给sp,作为堆栈的起始地址

​ 第3行,入栈,具体操作如下:

​ 将R2存储到0X80200000地址

​ 将R1存储到0X801FFFFC地址

​ 将R0存储到0X801FFFF8地址

​ 第4行,出栈,具体操作如下:

​ 将0X801FFFF8地址的内容赋值给R0

​ 将0X801FFFFC地址的内容赋值给R1

​ 将0X80200000地址的内容赋值给R2

​ 如下图所示:

​ 上述第3,4行汇编代码,就是所谓的入栈,出栈。也可以用push,pop指令完成入栈,出栈,如下

代码语言:javascript
复制
1 ldr sp,=0x80200000
2
3 push {r0-r2} @ 入栈
4 pop {r0-r2}  @ 出栈

5.3 进制

​ 目前计算机对数据的表示方式,有十六进制、十进制、八进制与二进制。

5.3.1 如何理解它们的区别?

​ 十六进制,逢十六进一,每一位由0~F组成,习惯用0x前缀表示或用H后缀表示

代码语言:javascript
复制
0xA或AH

​ 十进制,逢十进一,每一位由0~9组成,无前缀或用D后缀表示

代码语言:javascript
复制
10或10D

​ 八进制,逢八进一,每一位由0~7组成,习惯用0前缀表示或用O后缀表示

代码语言:javascript
复制
012或12O

​ 二进制,逢二进一,每一位由0~1组成,习惯用0b前缀表示或用B后缀表示

代码语言:javascript
复制
0b1010或1010B

5.3.2 在C语言中怎么表示这些进制呢?

代码语言:javascript
复制
十六进制:int a = 0xA;   // 0x前缀
十进制:  int a = 10;
八进制:  int a = 012;   // 0前缀
二进制:  int a = 0b1010;// 0b前缀

5.3.3 十六进制与二进制转换关系

​ 在嵌入式开发中经常需要对十六进制与二进制进行转换

​ 如何快速的转换2/16进制? 首先记住8 4 2 1 ——>二进制权重

​ 将二进制0b01101110101转换成十六进制:将二进制从右到左,每四个分成一组:

​ 结果就是0x375

​ 将十六进制0xABC1转换成二进制:将十六进制从右到左,每个分成四位:

​ 结果就是1010 1011 1100 0001

5.4 大/小端模式与位操作

5.4.1 大/小端模式

​ 大端模式(Big-endian),是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中

​ 小端模式(Little-endian),是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中

​ 比如:0x12345678,在大/小端模式的存储位置如下:

内存地址

大端模式

小端模式

addr+3

0x78

0x12

addr+2

0x56

0x34

addr+1

0x34

0x56

addr

0x12

0x78

5.4.2 位操作

5.4.2.1 移位
代码语言:javascript
复制
1 int a = 0x6; // 二进制是0b0110
2 int b = a<<1;
3 int c = a>>1;

​ 第2行,对a左移一位,从0b0110->0b1100,即b=0xC

​ 第3行,对a右移一位,从0b0110->0b0011,即b=0x3

5.4.2.2 取反
代码语言:javascript
复制
1 int a = 0x6; // 二进制是0b0110
2 int b = ~a;

​ 第2行,对a按位取反,从0b0110->0b1001,即b=0x9

5.4.2.3 位与

​ 只有对应的两个二进位都为1时,结果位才为1

代码语言:javascript
复制
1 int a = 0x6; // 二进制是0b0110
2 int b = 0x7; // 二进制是0b0111
3 
4 int c = a&b;

​ 第4行,a&b,二进制是0b0110,即c=0x6

5.4.2.4 位或

​ 只要对应的二个二进位有一个为1时,结果位就为1

代码语言:javascript
复制
1 int a = 0x6; // 二进制是0b0110
2 int b = 0x7; // 二进制是0b0111
3 
4 int c = a|b;

​ 第4行,a|b,二进制是0b0111,即c=0x7

5.4.2.5 置位
代码语言:javascript
复制
1 int a = 0x6;     // 二进制是0b0110
2 
3 int a |= (1<<3);

​ 第3行,将变量a的bit3置1。1<<3 = 0b1000,然后0b1000|0b0110=0b1110,即a=0xe

5.4.2.6 清位
代码语言:javascript
复制
1 int a = 0x6;     // 二进制是0b0110
2 
3 int a &= ~(1<<2);

​ 第3行,将变量a的bit2清位。~(1<<2) = 0b1011,然后0b1011&0b0110=0b0010,即a=0x2

5.5 汇编程序调用C程序

​ 在C程序和ARM汇编程序之间相互调用时必须遵守ATPCS规则,ATPCS规定了一些函数间调用的基本规则。

​ 参考资料:

  • 文件原名ATPCS.pdf
  • 文档全名The ARM-THUMB Procedure Call Standard
  • 文档所在目录: 资料光盘 00_UserManual\参考资料\Arm架构参考资料\ ATPCS(ATM-Thumb指令调用标准).pdf
  • 参考章节: 所有

5.5.1 ATPCS规则

​ ATPCS即ARM-THUMB procedure call standard(ARM-Thumb过程调用标准)的简称,是基于ARM指令集和THUMB指令集过程调用的规范,规定了调用函数如何传递参数,被调用函数如何获取参数,以何种方式传递函数返回值。

​ 寄存器R0~R15在ATPCS规则的使用:

  • 在函数中,通过寄存器R0R3来传递参数,被调用的函数在返回前无需恢复寄存器R0R3的内容
  • 在函数中,通过寄存器R4~R11来保存局部变量
  • 寄存器R12用作函数间scratch寄存器
  • 寄存器R13用作栈指针,记作SP,在函数中寄存器R13不能用做其他用途,寄存器SP在进入函数时的值和退出函数时的值必须相等
  • 寄存器R14用作链接寄存器,记作LR,它用于保存函数的返回地址,如果在函数中保存了返回地址,则R14可用作其它的用途
  • 寄存器R15是程序计数器,记作PC,它不能用作其他用途

5.5.2 汇编程序如何向C程序的函数传递参数

  • 当参数小于等下4个时,使用寄存器R0~R3来进行参数传递
  • 当参数大于4个时,前四个参数按照上面方法传递,剩余参数传送到栈中,入栈的顺序与参数顺序相反,即最后一个参数先入栈

5.5.3 C程序如何返回结果给汇编程序

  • 结果为一个32位的整数时,通过寄存器R0返回
  • 结果为一个64位整数时,通过R0和R1返回,依此类推.
  • 结果为一个浮点数时,通过浮点运算部件的寄存器f0,d0或s0返回
  • 结果为一个复合的浮点数时,通过寄存器f0-fN或者d0~dN返回
  • 对于位数更多的结果,通过调用内存来传递

5.5.4 C函数为何要用栈

​ 总的来说,栈的作用就是:保存现场/上下文,传递参数

  • 保存现场/上下文

​ 保存现场,也叫保存上下文

​ 现场,相当于案发现场,总有一些现场的情况,要记录下来的,否则被别人破坏掉之后,你就无法恢 复现场了。而此处说的现场,就是指CPU运行的时候,用到了一些寄存器,比如R0~R3,LR等等,对于这些寄存器的值,如果你不保存而直接跳转到函数中去执行,那么很可能会被破坏了,因为函数执行需要用到这些寄存器。

​ 因此在函数调用之前,应该将这些寄存器等现场,暂时保持起来,等调用函数执行完毕返回后,再恢复现场,这样CPU就可以正确的继续执行了。

​ 保存寄存器的值,一般用的是push指令,将对应的某些寄存器的值,一个个放到栈中,即所谓的入栈

​ 然后待被调用的子函数执行完毕的时候,再调用pop,把栈中的一个个的值,赋值给对应的入栈的寄存器,即所谓的出栈

  • 传递参数

​ 当函数被调用并且参数大于4个时,(不包括第4个参数)第4个参数后面的参数就保存在栈中。

5.6 C语言中读写寄存器

​ 每一个寄存器都有一个地址,只要找到寄存器地址,通过指针指向寄存器地址单元,通过读写指针值,就可以获得寄存器值。

​ 首先,定义一个指针,指针类型根据寄存器大小决定,同时需要加上volatile关键字让编译器不要优化此指针,比如,CCM_CCGR1寄存器值是32位,此处定义为unsigned int *指针类型,寄存器地址为0x20C406C

代码语言:javascript
复制
volatile unsigned int *CCM_CCGR1 = (volatile unsigned int *)(0x20C406C);

​ 然后,对寄存器进行读写操作

代码语言:javascript
复制
val = *CCM_CCGR1;       // 读寄存器
*CCM_CCGR1 |= (3<<30);  // 写寄存器,将CCM_CCGR1寄存器的[31:30]位置1

5.7 start.S解析

代码语言:javascript
复制
2 .text
3 .global _start
4 _start:
  • 第2行,.text表示代码段,汇编系统预定义段名,说明下面的汇编是代码段
  • 第3行,.global表示_start是一个全局符号
  • 第4行,标签_ start,汇编程序的默认入口是 _ start,也可以在链接脚本中使用ENTRY来指明其它的入口点,类似C语言main()函数,_ start是整个程序的入口,即程序执行的第一条指令
代码语言:javascript
复制
@ 相当于一个函数,_start是函数名,下面汇编指令是函数内容
4 _start:
5
6  //设置栈
7  ldr sp,=0x80200000
8
9  bl clean_bss
10
11 bl main
12
13 halt:
14 b halt
  • 第7行,将0x80200000赋值给寄存器sp,即设置栈地址,因为C语言函数调用时,保存现场/上下文和传递参数需要用到栈
  • 第9行,跳转到标签clean_bss,相当于调用clean_bss函数,并将bl main指令地址存储到寄存器lr中
  • 第11行,进入C语言的main()函数,并将b halt指令地址存储到寄存器lr中
  • 第13行,标签halt
  • 第14行,只跳转到标签halt,循环执行b halt指令执行
代码语言:javascript
复制
@ 相当于一个函数,clean_bss是函数名,下面汇编指令是函数内容
16 clean_bss:
17 /* 清除BSS段 */
18 ldr r1, =__bss_start
19 ldr r2, =__bss_end
20 mov r3, #0
21 clean:       @ 下面汇编指令相当于循环体,直到R1与R2相等
22 str r3, [r1]
23 add r1, r1, #4
24 cmp r1, r2
25 bne clean
26
27 	mov pc, lr @ 函数执行完毕,返回
  • 第16行,标签clean_bss,下面汇编代码是清除BSS段,将BSS段都设置成0的作用
  • 第18行,将链接脚本定义的bss起始地址赋值给寄存器r1
  • 第19行,将链接脚本定义的bss结束地址赋值给寄存器r2
  • 第20行,将0赋值给寄存器r3,即r3=0
  • 第21行,标签clean
  • 第22行,将寄存器r3的值存储到寄存器r1的值对应地址中
  • 第23行,将寄存器r1的值加上4,赋值给寄存器r1,即r1 = r1+4
  • 第24行,比较寄存器r1的值与寄存器r2的值
  • 第25行,如果寄存器r1的值与寄存器r2的值不相等,跳转到标签clean
  • 第26行,如果寄存器r1的值与寄存器r2的值相等,就执行此行,返回到 bl main 处,继续执行

5.8 根据led.dis分析代码的整体运行流程

​ 在分析led.dis文件前,我们再把imx6ull芯片如何将led.bin文件复制到内存DDR中过程,简单整体过一篇。

​ 如下图,imx6ull芯片一上电后,会先执行bootRom程序,此程序是芯片出厂时已经固定的程序,除了芯片原厂,咱们是无法修改的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uvCdRLif-1642059925437)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/LED_Program_Knowdge_image020.png)]

bootRom有什么作用?下面一一讲解。

  1. bootRom会把EMMC或TF卡的前4K数据读入到芯片内部RAM运行
  2. bootRom根据DCD进行初始化DDR。
  3. bootRom根据IVT,从EMMC或TF卡中将led.bin读到DDR的0x80100000地址
  4. 跳转到DDR的0x80100000地址执行

​ 目前led.bin程序已经复制到内存中,CPU开始从内存0x80100000地址开始执行机器码,每一条机器码是32位/4字节,此处的机器码就是led.bin中的机器码,那我们能不能打开led.bin文件,看到里面的机器码?答案是可以的。如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dGwtyJsS-1642059925437)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/LED_Program_Knowdge_image021.png)]

​ 前面介绍过大/小端模式,你是否记得?如果忘记了,可以回头看一下。 ​ 此处可以看到机器码e59fd028(指令:ldr sp,=0x80200000)的存储形式:

地址

机器码

00000000

28

00000001

d0

00000002

9f

00000003

e5

​ 没错,imx6ull的存储方式是小端模式,换一句话说,ARM存储方式一般都是小端模式。 ​ 但是bin文件的机器码不方便阅读,所以我们一般会通过objdump进行反汇编,得到人类容易读的led.dis文件。 ​ 如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jcXyHisv-1642059925438)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/LED_Program_Knowdge_image022.png)]

​ 下面我们就来分析一下led.dis文件,但是在阅读此小节前,尽量把前一小节《1.7 start.S解析》完全理解懂,不然阅读此小节,有点云里雾里。

代码语言:javascript
复制
    1)  CPU执行的第一条机器码就是内存地址0x80100000存储的e59fd028机器码,对应的指令是ldr sp, [pc, #40],相当于Start.S文件的ldr sp,=0x80200000指令,寄存器SP的值等于0x80200000
代码语言:javascript
复制
80100000: e59fd028 ldr sp, [pc, #40] ; 80100030 <clean+0x14>
  1. 每执行完一条机器码,会自动执行下一条内存地址0x80100004存储的eb000001机器码,对应的指令是bl 80100010,相当于Start.S文件的bl clean_bss指令。
代码语言:javascript
复制
80100004: eb000001 bl 80100010 <clean_bss>
....
80100010 <clean_bss>:
80100010: e59f101c ldr r1, [pc, #28] ; 80100034 <clean+0x18>
80100014: e59f201c ldr r2, [pc, #28] ; 80100038 <clean+0x1c>
  1. 跳转到内存地址0x80100010,执行e59f101c机器码,对应的指令是ldr r1, [pc, #28],相当于Start.S文件的ldr r1, =__bss_start指令。
  2. 此处clean_bss相当于一个函数体,CPU会自动让内存地址加4,向下执行机器码,直到执行mov pc, lr指令后,才返回内存地址0x80100008处执行fa000057机器码,对应的指令是blx 8010016c,相当于Start.S文件的bl main指令。 到此,CPU跳转到C语言的main()函数,继续执行。 为了让大家深入理解C语言函数的调用执行过程中,汇编指令如何执行,此处简单分析main()函数

​ 如上图所示

​ 1.进入main()函数后,先将寄存器R7、LR入栈,保存现场/上下文,方便main()函数执行完毕后返回,并且将当前栈指向的内存地址赋值给寄存器R7,如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VkW5Adpe-1642059925438)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/LED_Program_Knowdge_image024.png)]

​ 2. 调用led_init()函数,因为没有参数传递,所以直接调用BL指令进行跳转,即bl 8010003c指令

​ 3. 调用led_ctl(1)函数,此处只有一个参数,通过寄存器R0进行传递,即movs r0, #1指令,然后通过BL指令进行跳转,即bl 801000f8指令,关于参数传递问题,可以参考前面《5.5 汇编程序调用C程序》。

​ 4. 调用delay(1000000)函数,此处只有一个参数,通过寄存器R0进行传递,然后通过BL指令进行跳转

​ 5. 调用led_ctl(0)函数,此处只有一个参数,通过寄存器R0进行传递,然后通过BL指令进行跳转

​ 6. 调用delay(1000000)函数,此处只有一个参数,通过寄存器R0进行传递,然后通过BL指令进行跳转

​ 7. while(1)循环体到此已经结束,但是需要循环执行循环体的内容,通过B指令进行跳转到循环体开头,即b.n 80100174指令,执行内存地址0x80100174处的指令,也就是led_ctl(1)函数对应的汇编指令movs r0, #1

​ 到此,进入并执行main()函数对应的汇编指令分析已经结束,如果读者有兴趣可以分析一下,led_init()、led_ctl()与delay()函数的汇编指令。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-01-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 第五章 LED程序涉及的编程知识
    • 5.1 ARM架构的简单介绍
      • 5.1.1 运行模式
      • 5.1.2 寄存器组
    • 5.2 汇编与机器码、汇编指令
      • 5.2.1 汇编与机器码
      • 5.2.2 汇编指令
    • 5.3 进制
      • 5.3.1 如何理解它们的区别?
      • 5.3.2 在C语言中怎么表示这些进制呢?
      • 5.3.3 十六进制与二进制转换关系
    • 5.4 大/小端模式与位操作
      • 5.4.1 大/小端模式
      • 5.4.2 位操作
    • 5.5 汇编程序调用C程序
      • 5.5.1 ATPCS规则
      • 5.5.2 汇编程序如何向C程序的函数传递参数
      • 5.5.3 C程序如何返回结果给汇编程序
      • 5.5.4 C函数为何要用栈
    • 5.6 C语言中读写寄存器
      • 5.7 start.S解析
        • 5.8 根据led.dis分析代码的整体运行流程
        相关产品与服务
        数据保险箱
        数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档