5_LED程序涉及的编程知识

Posted 韦东山

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了5_LED程序涉及的编程知识相关的知识,希望对你有一定的参考价值。

第五章 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指令,如表

JT描述
00ARM
01Thumb
11ThumbEE
10Jazelle

​ 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]运行模式
10000User 模式
10001FIQ 模式
10010IRQ 模式
10011Supervisor(SVC)模式
10110Monitor(MON)模式
10111Abort(ABT)模式
11010Hyp(HYP)模式
11011Undef(UND)模式
11111System(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从内存里把数据读进来?

    比如:

mov r0, #addr_a // 把变量a的地址传给CPU寄存器r0
ldr r1, [r0]    // 从r0所指的内存把数值读进CPU寄存器r1
  1. 读进来后,这个数保存在哪里?

    当然是保存在CPU内部了,存在某个寄存器里,上面的代码用寄存器r1来保存该值

  2. 如何处理数据?

    CPU执行加法指令,比如:

add r1, r1, r2 // 在CPU内部,r1=r1+r2
  1. 最终数据怎么写入内存?

    CPU执行指令,比如:

str r1, [r0]   // 将r1的值写入r0所指的内存

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

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

condop1op指令类型
not 111100x-数据处理和杂项指令,如MOV
not 1111010-加载/存储指令,如LDR/STR
not 11110110加载/存储指令,如LDR/STR
not 11110111媒体指令(英文:Media instructions)
not 111110x-分支指令,如B、BL; 块数据传输指令,如
LDM/STM、POP/PUSH
not 111111x-协处理器指令
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 汇编指令

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

label:                  
	instruction @ comment

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

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

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

​ 比如:

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

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

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

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

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

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;

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,多数据存储,将某寄存器的值存储到某地址

​ 格式:

ldmcond Rn!, reglist
stmcond 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

​ 数据块操作:

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)]

​ 堆栈操作:满递减堆栈

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指令完成入栈,出栈,如下

1 ldr sp,=0x80200000
2
3 push r0-r2 @ 入栈
4 pop r0-r2  @ 出栈

5.3 进制

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

5.3.1 如何理解它们的区别?

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

0xA或AH

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

1010D

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

01212O

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

0b1010或1010B

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

十六进制: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+30x780x12
addr+20x560x34
addr+10x340x56
addr0x120x78

5.4.2 位操作

5.4.2.1 移位

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 取反

1 int a = 0x6; // 二进制是0b0110
2 int b = ~a;

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

5.4.2.3 位与

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

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

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 置位

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 清位

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

volatile unsigned int *CCM_CCGR1 = (volatile unsigned int *)(0x20C406C);

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

val = *CCM_CCGR1;       // 读寄存器
*CCM_CCGR1 |= (3<<30);  // 写寄存器,将CCM_CCGR1寄存器的[31:30]位置1

5.7 start.S解析

2 .text
3 .global _start
4 _start:
  • 第2行,.text表示代码段,汇编系统预定义段名,说明下面的汇编是代码段

  • 第3行,.global表示_start是一个全局符号

  • 第4行,标签_ start,汇编程序的默认入口是 _ start,也可以在链接脚本中使用ENTRY来指明其它的入口点,类似C语言main()函数,_ start是整个程序的入口,即程序执行的第一条指令

@ 相当于一个函数,_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指令执行
@ 相当于一个函数,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)的存储形式:

地址机器码
0000000028
00000001d0
000000029f
00000003e5

​ 没错,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解析》完全理解懂,不然阅读此小节,有点云里雾里。

    1)  CPU执行的第一条机器码就是内存地址0x80100000存储的e59fd028机器码,对应的指令是ldr sp, [pc, #40],相当于Start.S文件的ldr sp,=0x80200000指令,寄存器SP的值等于0x80200000
80100000: e59fd028 ldr sp, [pc, #40] ; 80100030 <clean+0x14>
  1. 每执行完一条机器码,会自动执行下一条内存地址0x80100004存储的eb000001机器码,对应的指令是bl 80100010,相当于Start.S文件的bl clean_bss指令。
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()函数的汇编指令。

以上是关于5_LED程序涉及的编程知识的主要内容,如果未能解决你的问题,请参考以下文章

ws2812 LED灯编程

STM32 学习5 寄存器编程LED显示数字

STM32 学习5 寄存器编程LED显示数字

驱动编程思想之初体验 --------------- 嵌入式linux驱动开发之点亮LED

基于AT89C51的十字路口交通灯proteus仿真 _紧急+夜间

六个LED从全灭一个一个亮 汇编语言编程+电路图