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常用汇编指令

  1. 处理器内部数据传输指令

    @ 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
  2. 储存器访问指令
    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 中所保存的地址中
  3. 压栈和出栈指令
    注意处理器的堆栈是向下增长的。常用的 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
  4. 跳转指令
    有多种跳转指令,直接使用跳转指令 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
  1. 算术运算指令
指令计算公式备注
ADD Rd, Rn, RmRd = Rn + Rm
ADD Rd, Rn, #immedRd = Rn + #immed加法运算,指令为 ADD
ADC Rd, Rn, RmRd = Rn + Rm + 进位
ADC Rd, Rn, #immedRd = Rn + #immed +进位带进位的加法运算,指令为 ADC
SUB Rd, Rn, RmRd = Rn – Rm
SUB Rd, #immedRd = Rd - #immed
SUB Rd, Rn, #immedRd = Rn - #immed减法
SBC Rd, Rn, #immedRd = Rn – 借位
SBC Rd, Rn ,RmRd = Rn – Rm – 借位带借位的减法
MUL Rd, Rn, RmRd = Rn * Rm乘法(32 位)
UDIV Rd, Rn, RmRd = Rn / Rm无符号除法
SDIV Rd, Rn, RmRd = Rn / Rm有符号除法
  1. 逻辑运算指令
指令计算公式备注
AND Rd, RnRd = Rd & Rn
AND Rd, Rn, #immedRd = Rn & #immed
AND Rd, Rn, RmRd = Rn & Rm按位与
ORR Rd, RnRd = Rd \\ Rn
ORR Rd, Rn, #immedRd = Rn \\ #immed
ORR Rd, Rn, RmRd = Rn \\ Rm按位或
BIC Rd, RnRd = Rd & (~Rn)
BIC Rd, Rn, #immedRd = Rn & (~#immed)
BIC Rd, Rn, RmRd = Rn & (~Rm)位清除
ORN Rd, Rn, #immedRd = Rn \\ (~#immed)
ORN Rd, Rn, RmRd = Rn \\ (~Rm)按位或非
EOR Rd, RnRd = Rd ^ Rn
EOR Rd, Rn, #immedRd = Rn ^ #immed
EOR Rd, Rn, RmRd = 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 高低电平。这里介绍两种启动方式。

  1. 串行下载
    串行下载的意思就是可以通过 USB 或者 UART 将代码下载到板子上的外置存储设备中,我们可以使用 OTG1 这个 USB口向开发板上的 SD/EMMC、 NAND 等存储设备下载代码。这个下载是需要用到 NXP 提供的一个软件,一般用于量产。
  2. 内部 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驱动篇4Linux设备树详解

正点原子I.MX6U-MINI驱动篇1字符设备驱动开发-Hello驱动(不涉及硬件操作)

正点原子Linux阿尔法开发板4.3 寸多点电容触摸屏测试问题

正点原子Linux阿尔法开发板4.3 寸多点电容触摸屏测试问题

正点原子I.MX6U-MINI驱动篇2嵌入式 Linux驱动开发之点灯大法