关于linux编程:Linux编程入门正点原子Linux驱动开发指南学习2021W23

13次阅读

共计 8147 个字符,预计需要花费 21 分钟才能阅读完成。

五、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, 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 有符号除法
  1. 逻辑运算指令
指令 计算公式 备注
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 高低电平。这里介绍两种启动形式。

  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
    
正文完
 0