Linux编程入门--正点原子Linux驱动开发指南学习2021W23
Posted cheney
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux编程入门--正点原子Linux驱动开发指南学习2021W23相关的知识,希望对你有一定的参考价值。
五、 ARM汇编基础
对于 Cortex-A 芯片来讲,大部分芯片在上电以后 C 语言环境还没准备好,所以前一部分程序肯定是汇编的。这部分汇编用来初始化堆栈(即堆栈指针SP),对于某些芯片本身没有 RAM,或者内部 RAM 不开放给用户使用,用户代码需要在 DDR 中运行,因此一开始要用汇编来初始化 DDR 控制器。这部分涉及到的指令不是很复杂,可能就十几个指令。
(1) GNU汇编语法
不同编译器使用的汇编语法略有不同,我们要编写的是 ARM 汇编,编译使用的 GCC 交叉编译器,所以我们的汇编代码要符合 GNU 语法。GNU 汇编语法适用于所有的架构,并不是 ARM 独享的, GNU 汇编由一系列的语句组成,每行一条语句,每条语句有三个可选部分:label: instruction @ comment
- label 即标号,表示地址位置,有些指令前面可能会有标号,这样就可以通过这个标号得到指令的地址,标号也可以用来表示数据地址。注意 label 后面的“:”,任何以“:”结尾的标识符都会被识别为一个标号。
- instruction 即指令,也就是汇编指令或伪指令。
- @符号,表示后面的是注释,就跟 C 语言里面的“/”和“/”一样,其实在 GNU 汇编文件中我们也可以使用“/”和“/”来注释。
- comment 就是注释内容。
注意! ARM 中的指令、伪指令、伪操作、寄存器名等可以全部使用大写,也可以全部使用小写,但是不能大小写混用。
用户可以使用.section 伪操作来定义一个段,格式如下.section .testsection @定义一个 testsetcion 段
另外汇编系统预定义了一些段名,
- .text 表示代码段。
- .data 初始化的数据段。
- .bss 未初始化的数据段。
- .rodata 只读数据段。
汇编程序的默认入口标号是 _start ,不过我们也可以在链接脚本中使用 ENTRY 来指明其它的入口点,下面的代码就是使用_start 作为入口标号:
global _start
_start:
ldr r0, =0x12 @r0=0x12
上面代码中 .global 是伪操作,表示 _start 是一个全局标号,类似 C 语言里面的全局变量一样,常见的伪操作有:
- .byte 定义单字节数据,比如.byte 0x12。
- .short 定义双字节数据,比如.short 0x1234。
- .long 定义一个 4 字节数据,比如.long 0x12345678。
- .equ 赋值语句,格式为: .equ 变量名,表达式,比如.equ num, 0x12,表示 num=0x12。
- .align 数据字节对齐,比如: .align 4 表示 4 字节对齐。
- .end 表示源文件结束。
- .global 定义一个全局符号,格式为: .global symbol,比如: .global _start。
GNU 汇编同样也支持函数,函数格式如下:
函数名:
函数体
返回语句 @ 返回语句不是必须
/*SVC 中断*/
SVC_Handler:
ldr r0, =SVC_Handler
bx r0
(2) ARM常用汇编指令
处理器内部数据传输指令
@ MOV 指令用于将数据从一个寄存器拷贝到另外一个寄存器,或者将一个立即数传递到寄存器里面 MOV R0, R1 @将寄存器 R1 中的数据传递给 R0,即 R0=R1 MOV R0, #0X12 @将立即数 0X12 传递给 R0 寄存器,即R0=0X12 @MRS 指令用于将特殊寄存器(如 CPSR 和 SPSR)中的数据传递给通用寄存器,要读取特殊寄存器的数据只能使用 MRS 指令 MRS R0, CPSR @将特殊寄存器 CPSR 里面的数据传递给 R0,即 R0=CPSR @MSR 指令用来将普通寄存器的数据传递给特殊寄存器,也就是写特殊寄存器,写特殊寄存器只能使用 MSR MSR CPSR, R0 @将 R0 中的数据复制到 CPSR 中,即 CPSR=R0
储存器访问指令
ARM 不能直接访问存储器(如RAM中的数据),需要借用Rx(x=0~12)寄存器中转实现访问存储器。LDR 加载(Load) 和 STR 存储(Store) 都是按照字进行读取和写入的,也就是操作的 32 位数据,如果要按照字节、半字进行操作的话可以在指令“LDR”后面加上 B 或 H,比如按字节操作的指令就是 LDRB 和 STRB,按半字操作的指令就是 LDRH 和 STRH。@LDR 主要用于从存储加载数据到寄存器 Rx 中,LDR 也可以将一个立即数加载到寄存器 Rx中, LDR 加载立即数的时候要使用“=”,而不是“#”。 LDR Rd, [Rn , #offset] 从存储器 Rn+offset 的位置读取数据存放到 Rd 中。 LDR R0, =0X0209C004 @将寄存器地址 0X0209C004 加载到 R0 中,即 R0=0X0209C004 LDR R1, [R0] @读取地址 0X0209C004 中的数据到 R1 寄存器中 @STR 就是将数据写入到存储器中 STR Rd, [Rn, #offset] 将 Rd 中的数据写入到存储器中的 Rn+offset 位置。 LDR R0, =0X0209C004 @将寄存器地址 0X0209C004 加载到 R0 中,即 R0=0X0209C004 LDR R1, =0X20000002 @R1 保存要写入到寄存器的值,即 R1=0X20000002 STR R1, [R0] @将 R1 中的值写入到 R0 中所保存的地址中
压栈和出栈指令
注意处理器的堆栈是向下增长的。常用的 PUSH 和 POP 指令;另外还可以用多重存储器的访问指令 “ STMFD SP!” 和 “ LDMFD SP! ”。其中的 “ STM ” 和 “ LDM ” 就是多重存储器的访问方式;而 “ FD ” 表示 “ Full Descending” ,是满减栈标示,告诉编译器这是针对这样类型的堆栈操作,而不是朝什么方向,编译器会翻译适当的操作指令 ;其中的 “!” 表示要自增或自减 SP 的值(字为单位,4个字节)。STM 和 LDM 的指令寄存器列表中编号小的对应低地址,编号高的对应高地址。@ PUSH <reg list> 将寄存器列表存入栈中。 @ POP <reg list> 从栈中恢复寄存器列表。 PUSH {R0~R3, R12} @将 R0~R3 和 R12 压栈 PUSH {LR} @将 LR 进行压栈 POP {LR} @先恢复 LR POP {R0~R3,R12} @再恢复 R0~R3,R12 @ 上面的汇编也可以改为 STMFD SP!,{R0~R3, R12} @R0~R3,R12 入栈 STMFD SP!,{LR} @LR 入栈 LDMFD SP!, {LR} @先恢复 LR LDMFD SP!, {R0~R3, R12} @再恢复 R0~R3, R12
- 跳转指令
有多种跳转指令,直接使用跳转指令 B、 BL、 BX 等;或者直接向 PC 寄存器里面写入数据。
指令 | 描述 |
---|---|
B <label> | 跳转到 label,如果跳转范围超过了+/-2KB,可以指定 B.W <label>使用 32 位版本的跳转指令, 这样可以得到较大范围的跳转 |
BX <Rm> | 间接跳转,跳转到存放于 Rm 中的地址处,并且切换指令集 |
BL <label> | 跳转到标号地址,并将返回地址保存在 LR 中。常用于子程序调用。 |
BLX <Rm> | 结合 BX 和 BL 的特点,跳转到 Rm 指定的地址,并将返回地址保存在 LR 中,切换指令集。 |
_start:
ldr sp,=0X80200000 @设置栈指针
b main @跳转到 main 函数
@ BL例子
push {r0, r1} @保存 r0,r1
cps #0x13 @进入 SVC 模式,允许其他中断再次进去
bl system_irqhandler @加载 C 语言中断处理函数到 r2 寄存器中
cps #0x12 @进入 IRQ 模式
pop {r0, r1}
str r0, [r1, #0X10] @中断执行完成,写 EOIR
- 算术运算指令
指令 | 计算公式 | 备注 |
---|---|---|
ADD Rd, Rn, Rm | Rd = Rn + Rm | |
ADD Rd, Rn, #immed | Rd = Rn + #immed | 加法运算,指令为 ADD |
ADC Rd, Rn, Rm | Rd = Rn + Rm + 进位 | |
ADC Rd, Rn, #immed | Rd = Rn + #immed +进位 | 带进位的加法运算,指令为 ADC |
SUB Rd, Rn, Rm | Rd = Rn – Rm | |
SUB Rd, #immed | Rd = Rd - #immed | |
SUB Rd, Rn, #immed | Rd = Rn - #immed | 减法 |
SBC Rd, Rn, #immed | Rd = Rn – 借位 | |
SBC Rd, Rn ,Rm | Rd = Rn – Rm – 借位 | 带借位的减法 |
MUL Rd, Rn, Rm | Rd = Rn * Rm | 乘法(32 位) |
UDIV Rd, Rn, Rm | Rd = Rn / Rm | 无符号除法 |
SDIV Rd, Rn, Rm | Rd = Rn / Rm | 有符号除法 |
- 逻辑运算指令
指令 | 计算公式 | 备注 |
---|---|---|
AND Rd, Rn | Rd = Rd & Rn | |
AND Rd, Rn, #immed | Rd = Rn & #immed | |
AND Rd, Rn, Rm | Rd = Rn & Rm | 按位与 |
ORR Rd, Rn | Rd = Rd \\ Rn | |
ORR Rd, Rn, #immed | Rd = Rn \\ #immed | |
ORR Rd, Rn, Rm | Rd = Rn \\ Rm | 按位或 |
BIC Rd, Rn | Rd = Rd & (~Rn) | |
BIC Rd, Rn, #immed | Rd = Rn & (~#immed) | |
BIC Rd, Rn, Rm | Rd = Rn & (~Rm) | 位清除 |
ORN Rd, Rn, #immed | Rd = Rn \\ (~#immed) | |
ORN Rd, Rn, Rm | Rd = Rn \\ (~Rm) | 按位或非 |
EOR Rd, Rn | Rd = Rd ^ Rn | |
EOR Rd, Rn, #immed | Rd = Rn ^ #immed | |
EOR Rd, Rn, Rm | Rd = Rn ^ Rm | 按位异或 |
六、 I.MX6U 汇编实验
在正点原子的学习指南中,汇编LED实验主要完全是使用了 LDR 和 STR 指令给 MX6U 的寄存器赋值,然后编译下载。使用的指令如下:
arm-linux-gnueabihf-gcc -g -c led.s -o led.o
其中“-g”选项是产生调试信息, GDB 能够使用这些调试信息进行代码调试。“-c”选项是编译源文件,但是不链接。“-o”选项是指定编译产生的文件名字,这里我们指定 led.s 编译完成以后的文件名字为 led.o。arm-linux-gnueabihf-ld -Ttext 0X87800000 led.o -o led.elf
-Ttext 就是指定链接地址,“-o”选项指定链接生成的 elf 文件名,这里我们命名为 led.elf。本教程中用的 MX6U 的DDR SDRAM 起始地址为 0X80000000 ,但是 Uboot 的链接地址为 0X87800000 ,于是都统一使用 0X87800000 。arm-linux-gnueabihf-objcopy -O binary -S -g led.elf led.bin
“-O”选项指定以什么格式输出,后面的“binary”表示以二进制格式输出,选项“-S”表示不要复制源文件中的重定位信息和符号信息,“-g”表示不复制源文件中的调试信息。arm-linux-gnueabihf-objdump -D led.elf > led.dis
“-D”选项表示反汇编所有的段,反汇编完成以后就会在当前目录下出现一个名为 led.dis 文件。
最后再写一个Makefile。
led.bin:led.s
arm-linux-gnueabihf-gcc -g -c led.s -o led.o
arm-linux-gnueabihf-ld -Ttext 0x87800000 led.o -o led.elf
arm-linux-gnueabihf-objcopy -O binary -S -g led.elf led.bin
arm-linux-gnueabihf-objdump -D led.elf > led.dis
.PHONY:clean
clean:
rm -rf *.o *.bin *.elf *.dis
代码的烧写过程就不操作了, I.MX6U 没有内部的flash,需要把代码放到外置的存储介质中,生成的bin文件不能直接烧录,要按照特定的方式放置,启动过程后面再看。
七、 I.MX6U 启动
(1)启动模式选择
I.MX6U 上电后会根据 BOOT_MODE[1:0] 的值来选择不同启动模式,BOOT_MODE[1:0]的值是可以改变的,有两种方式,一种是改写 eFUSE(熔丝),一种是修改相应的 GPIO 高低电平。这里介绍两种启动方式。
- 串行下载
串行下载的意思就是可以通过 USB 或者 UART 将代码下载到板子上的外置存储设备中,我们可以使用 OTG1 这个 USB口向开发板上的 SD/EMMC、 NAND 等存储设备下载代码。这个下载是需要用到 NXP 提供的一个软件,一般用于量产。 - 内部 BOOT 模式
在此模式下,芯片会执行内部的 boot ROM 代码,这段 boot ROM 代码会进行硬件初始化(一部分外设),然后从 boot 设备(就是存放代码的设备、比如 SD/EMMC、 NAND)中将代码拷贝出来复制到指定的 RAM 中,一般是 DDR。
(2)boot rom 初始化内容(内部 BOOT 模式)
首先初始化时钟。
内部 boot ROM 为了加快执行速度会打开 MMU 和 Cache,下载镜像的时候 L1 ICache 会打开,验证镜像的时候 L1 DCache、 L2 Cache 和 MMU 都会打开。一旦镜像验证完成, boot ROM就会关闭 L1 DCache、 L2 Cache 和 MMU。
中断向量偏移会被设置到 boot ROM 的起始位置,当 boot ROM 启动了用户代码以后就可以重新设置中断向量偏移了。一般是重新设置到我们用户代码的开始地方。
(3)启动设备
当 BOOT_MODE 设置为内部 BOOT 模式以后,接下来选择从设备中启动,I.MX6U 同样提供了 eFUSE 和 GPIO 配置两种方式启动设备,由这24个IO口BOOT_CFG1[7:0]、 BOOT_CFG2[7:0]和 BOOT_CFG4[7:0] 选择启动的设备,启动完成后,这些IO口便可作为LCD的数据线使用了。可已选择的设备如下:
- 接到 EIM 接口的 CS0 上的 16 位 NOR Flash。
- 接到 EIM 接口的 CS0 上的 OneNAND Flash。
- 接到 GPMI 接口上的 MLC/SLC NAND Flash, NAND Flash 页大小支持 2KByte、 4KByte和 8KByte, 8 位宽。
- Quad SPI Flash。
- 接到 USDHC 接口上的 SD/MMC/eSD/SDXC/eMMC 等设备。
- SPI 接口的 EEPROM。
(4)镜像烧写
led.bin 不能直接烧写,需要由 imxdownload 将 led.bin 打包成 load.imx 。I.MX6U 的最终可烧写文件组成如下:
- Image vector table,简称 IVT, IVT 里面包含了一系列的地址信息,这些地址信息在ROM 中按照固定的地址存放着。
- Boot data,启动数据,包含了镜像要拷贝到哪个地址,拷贝的大小是多少等等。
- Device configuration data,简称 DCD,设备配置信息,重点是 DDR3 的初始化配置。
- 用户代码可执行文件,比如 led.bin。
load.imx 最终组成就是 IVT + Boot data + Device configuration data + bin文件。load.imx 在用户代码前面又有 3KByte 的 IVT+Boot Data+DCD 数据,因此 load.imx 在 DDR 中的起始地址就是 0X87800000-3072=0X877FF400
八、C语言版LED试验
(1)代码解析
C语言版本的LED试验基本上也就是直接给相应的寄存器赋值,比较简单,但是奇怪的是多了一个汇编关于启动的,主要是设置了MCU运行的模式,还有初始化了堆栈。这两部之前的汇编程序并没有。
.global _start /* 全局标号 */
/*
* 描述: _start函数,程序从此函数开始执行,此函数主要功能是设置C
* 运行环境。
*/
_start:
/* 进入SVC模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x13 /* r0或上0x13,表示使用SVC模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */
ldr sp, =0X80200000 /* 设置栈指针 */
b main /* 跳转到main函数 */
值得注意的是,再C语言中,往指定的地址写数据,方法如下:
#define GPIO1_GDIR *((volatile unsigned int *)0X0209C004)
GPIO1_GDIR = 0X0000008; /* GPIO1_IO03设置为输出 */
(volatile unsigned int *)
就将 0X0209C004 变成了一个地址,然后前面再加一个 * ,就是指向该地址。
(2)链接脚本
链接脚本主要用于链接时使用的,用于描述文件应该如何被链接在一起形成最终的可执行文件。其主要目的是描述输入文件中的段如何被映射到输出文件中,并且控制输出文件中的内存排布。链接脚本的语法很简单,就是编写一系列的命令,这些命令组成了链接脚本,每个命令(如SECTIONS)是一个带有参数的关键字或者一个对符号的赋值,可以使用分号分隔命令。
在ELF格式的可执行文件中,全局内存包括三种:bss、data和 rodata 。bss是指那些没有初始化的和初始化为0的全局变量,bss类型的全局变量只占运行时的内存空间,而不占文件空间。data指那些初始化过(非零)的非const的全局变量,data类型的全局变量是即占文件空间,又占用运行时内存空间的。rodata 的意义同样明显,ro代表read only,即只读数据(const)。
在与Makefile相同的文件夹里面新建一个名为“imx6ul.lds”的文件,内容如下:
SECTIONS{
. = 0X87800000;
.text :
{
start.o
main.o
*(.text)
}
.rodata ALIGN(4) : {*(.rodata*)}
.data ALIGN(4) : { *(.data) }
__bss_start = .;
.bss ALIGN(4) : { *(.bss) *(COMMON) }
__bss_end = .;
}
SECTIONS
是链接脚本的命令,里面的内容用花括号括起来。第 2 行对一个特殊符号“.”进行赋值,“.”在链接脚本里面叫做定位计数器,默认的定位计数器为 0,这里我们给它赋值为 0x87800000 。.text
是段名,后面的冒号是语法要求,冒号后面的大括号里面可以填上要链接到“.text”这个段里面的所有文件,“(.text)”中的“”是通配符,表示所有输入文件的.text段都放到“.text”中。第 5行设置链接到开始位置的文件为 start.o,因为 start.o 里面包含着第一个要执行的指令,所以一定要链接到最开始的地方。接着就是 .rodata 段和 .data 段,这两段都用 ALIGN(4)表示 4 字节对齐。__bss_start = . 表示把当前的定位计数器赋值给__bss_start,后面的 __bss_end 类似。这样在汇编或C文件里面我们可以直接使用这两个标志来对 .bss 段内存进行赋0完成初始化。
(3)使用自动变量的Makefile
接下来针对C语言本版的ledc,再编写一个Makefile,这里要注意 start.o 一定要放到最前面!因为在后面链接的时候 start.o 要在最前面,因为 start.o 是最先要执行的文件。
objs := start.o main.o
# 生成ledc.bin 依赖是 start.o main.o
ledc.bin:$(objs)
arm-linux-gnueabihf-ld -Timx6ul.lds -o ledc.elf $^ # $^ 是所有依赖文件,这里就是 $(objs) ,按照链接脚本imx6ul.lds 进行链接
arm-linux-gnueabihf-objcopy -O binary -S ledc.elf $@ # $@ 是目标文件,这里就是 ledc.bin ,将 ledc.elf 转换成 ledc.bin
arm-linux-gnueabihf-objdump -D -m arm ledc.elf > ledc.dis
%.o:%.s
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $< # $@ 目标文件,这里是 %.o (start.o),$< 是第一个依赖文件,这里是 %.s (start.s)
%.o:%.S
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $<
%.o:%.c
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $<
clean:
rm -rf *.o ledc.bin ledc.elf ledc.dis
以上是关于Linux编程入门--正点原子Linux驱动开发指南学习2021W23的主要内容,如果未能解决你的问题,请参考以下文章
Linux编程入门--正点原子Linux驱动开发指南学习2021W23
正点原子I.MX6U-MINI驱动篇1字符设备驱动开发-Hello驱动(不涉及硬件操作)
正点原子Linux阿尔法开发板4.3 寸多点电容触摸屏测试问题