led汇编点灯
阅读原文时间:2023年07月12日阅读:2

1. 汇编LED原理

为什么使用Cortex-A汇编

  • 使用汇编初始化soc外设
  • 使用汇编初始化DDR,I.MX不需要,因为它内部的96k ROM中存放了自己编写的启动代码,这些代码可以读取DDR配置信息
  • 设置sp指针,一般指向ddr,设置好C语言的运行环境

Alpah开发板原理硬件分析

LED0为低电平,DS0就会亮,再看一下LED0接到哪里

由图可知,LED0接到GPIO 3,可以查找参考手册了

2. 初始化流程

stm32初始化流程

  • 使能GPIO时钟
  • 设置复用,但是32的LED灯默认就是GPIO的功能
  • 配置GPIO的电气属性
  • 使用GPIO,输出高低电平

I.MX6ULL初始化流程

  1. 首先找到时钟控制模块CCM

这七个寄存器控制6ULL所有时钟外设使能

可以看到11就是任何模式下都使能,除了STOP

可以把这七个时钟都使能一遍(0xFFFFFFFF)(32位)

  1. MUX复用,首先找到GPIO1_IO3的复用

看一下前四位,是复用为什么管脚

由图中可以看到,需要设为GPIO的是0101

  1. PAD电气属性

  • SRE(0):压摆率,0为低压摆率,1为高压摆率。压摆率是指电平跳变所需时间。时间越小波形越抖,压摆率过高。如果你使用的产品要过EMC(电磁兼容性,定义为设备和系统在其电磁环境中能正常工作且不对环境中任何事物构成不能承受的电磁骚扰的能力)的话就要使用低压摆率,如果要是高速通信就是高压摆率。
  • 1-2:保留,没用
  • DSE(3-5) : 当IO用作输出的时候用来设置IO的驱动能力,001是R0,3.3V下R0是260欧姆,1.8v是150欧姆,DDR是240欧姆, 010电阻减半,以后会越来越小,电阻越来越小,驱动能力越来越强。内阻小,那分压就会小,这样外部负载的分压就会大,外部负载的电阻就会大,这样选择性比较好,又可以大又可以小。
  • SPEED(6.7) : 速度,没什么可说的
  • 8-10: 没用
  • ODE : 开路输出,1开启0关闭
  • PKE : 用来使能禁止上下拉保持
  • PUE : 0保持,1上下拉
  • PUS (14.15): 上下拉配置
  • HYS :电气属性寄存器
  1. 配置GPIO功能,设置输入输出

都是32位,每一个位代表一个GPIO的输入输出

  • DR:可以向DR的指定bit来实现高低电平
  • GDIR:控制设置GPIO是输入还是输出
  • PSR:只读状态寄存器
  • ICR1:中断,设置中断的触发电平,低电平,高电平,上升沿下降沿
  • ICR2:中断,因为是两个位控制一个GPIO,所以两个ICR
  • IMR:中断掩码,通过它控制中断的使能关闭
  • ISR:中断状态寄存器,判断GPIO对应的中断有没有发生

综上,对于点灯,就是GDIR的bit3设置为输出模式,DR控制高低电平。

3. ARM汇编基础

作用:

  • 恢复现场保护现场sp指针
  • 初始化DDR寄存器

我们使用的GCC交叉编译器,汇编需要符合GNU语法,GNU汇编语法适用于所有的架构,每条语句有三个可选地方:

label: instruction @ comment

label即标号,表示地址位置,有些指令前面可能会有标号,标号也可以用来表示数据地址,后面以:结尾。任何以:结尾的标志符都是一个标号

instruction是指令,也就是汇编指令或者伪指令

@标号,表示后面的是注释,comment是注释内容。

比如:

add:
    MOVS R0,#0X12 @设置R0 = 0X12

add就是标号,下面就是指令和注释

会变系统预定义了一些段名:

  • .text代码段
  • .data初始化的数据段
  • .bss未初始化的数据段
  • .rodata只读数据段

我们也可以自己使用.section来定义一个段,每个段以段名开始,以下一段名或者文件结尾结束

.section .testsection @定义一个testsection段

汇编程序的默认入口标号是_start,也可以在链接脚本中ENTRY来指明入口点,比如

.global _start

_start:
ldr r0,=0x12 @r0 = 0x12

.global是伪操作,表示_start是一个全局标号,类似于C语言里面的全局变量一样,常见的伪操作有:

  • .byte 定义单字节数据,比如 .byte 0x12
  • .short 定义双字节数据, 比如 .short 0x1234
  • .long 四字节 .long 0x12345678
  • .equ 赋值语句,.equ num,0x12 等于 num = 0x12
  • .align 数据字节对齐, 比如 .align 4字节对齐
  • .end 表示源文件结束
  • .global 定义一个全局标号

GNU也支持函数:

函数名称:

​ 函数体

​ 返回语句

例如:

/*未定义中断*/
Undefined_Handler:
    ldr r0,=Undefined_Handler
    bx r0 @返回指令

/*SVC*/
SVC_Handler:
    ldr r0,=SVC_Handler
    bx r0

3.1 处理器内部数据传输指令

常见的操作有:

  • 将数据从一个寄存器传递到另一个寄存器
  • 将数据从一个寄存器传递到特殊寄存器,如CPSR和SPSR
  • 将立即数传递到寄存器

MOV指令

将R1里面的数据复制到R0中,或者一个立即数(常数)复制到R0中

MOV R0 R1
MOV R0 #0x12 @立即数前面加一个#

MRS指令

用于将特殊寄存器(如CPSR和SPSR)中的数据传递到通用寄存器,即要读取通用寄存器里面的数据必须用MRS

MRS R0,CPSR @将CPSR里面的数据读取到R0中

MSR指令

用于将通用寄存器的数据传递给特殊寄存器(如CPSR和SPSR)

MSR CPSR,R0  @将CPSR里面的数据读取到R0中

3.2 存储器访问指令

ARM中不能直接访问存储器,比如RAM中的数据,我们用汇编来配置寄存器的时候需要借助存储器访问指令,一般先写到通用寄存器中,然后借助存储器访问指令将通用寄存器中的数据写入到寄存器中,读取寄存器也是一样。

LDR

LDR主要用于存储加载数据到寄存器Rx中,LDR也可以将一个立即数加载到寄存器Rx中, LDR加载立即数的时候要用等于不是#

LDR R0, =0x0209C004 @将存储器地址加载到R0中,即R0 = 0x0209C004
LDR R1, [R0] @读取地址的数据到R1中

STR

LDR是从存储器读取数据,STR是将数据写入存储器中。

LDR R0, =0x0209C004
LDR R1, =0x20000002 @R1 = 0x20000002
STR R1, [R0]        @将R1中的值写入到R0保存的地址中

如果按照字节半字就在后面加上B或者H

例子 a = b, a的地址为0x20, b为0x30

LDR R0, =0x30
LDR R1, [R0]

LDR R0, =0x20
STR R1, [R0]

3.3 压栈和出栈指令

通常在A函数中调用B函数,当B函数执行完之后继续A,所以需要保存现场和恢复现场,现场就是寄存器的值,用到POP和PUSH,可以一次性操作多个存储器,他们利用当前的栈指针来生成地址

如果当前SP指针指向0x80000000

PUSH {R0~R3, R12}

此时SP就是原来-4*5 0x7FFFFFEC。

出栈也是从栈顶出栈,原理类似。

也可以使用STMFD SP!和LDMFD SP!代替上面的指令。

STM和LDM对应STR和LDR,就是多寄存器的,多个连续数据。

FD是满递减的意思。 地址是向下增长的,编号小对应低地址,编号大对应高地址。

3.4 跳转指令

  • 直接使用跳转指令B,BL,BX等
  • 直接向PC寄存器里面写入数据

B

最简单的跳转指令,B指令会将PC寄存器的值设置为跳转目标地址,一旦执行B指令,ARM就会立即跳转到指定的目标地址。如果调用的函数不会再返回到原来执行地方,可以用B指令

_start:

    ldr sp,=0x80200000
    b main

常用的初始化C语言环境

BL

BL在跳转之前会在寄存器LR中保存当前PC的值,所以可以通过将LR的值重新加载到PC寄存器中来恢复现场,即保护现场,跳转到C中处理中断,再恢复现场。

push {r0, r1}
cps #0x13  @进入SVC模式,允许其他中断再次进去

bl system_irqhandler @加载函数到r2中
cps #0x12 @进入IRQ模式
pop {r0, r1}
str r0,[r1, #0x10] @写EDIR

3.5 其他运算

算数运算

逻辑运算

4. Cortex-A内核寄存器组

除了R0到R14,还有R15(PC)程序计数器,CPSR当前状态寄存器和SPSR备用状态寄存器。R13是sp,R14是LR寄存器。

LR寄存器

恢复现场:

MOV PC, LR @将LR保存的值传递给PC

或者:

PUSH {LR} @压栈
POP {PC} @弹栈,并将值赋给PC

当异常发生以后,该异常模式对应的 R14 寄存器被设置成该异常模式将要返回的地址,R14 也可以当作普通寄存器使用。

程序计数器PC

pc = 当前执行位置+8个字节

解释一下:

5. 汇编代码

.global _start 

_start:
    @ 使能所有时钟
    @ CCM_CCGR0的内存地址为20C_4068
    ldr r0,=0x020c4068 @取得时钟0的地址
    ldr r1,=0xffffffff @要赋的值
    str r1, [r0]       @赋值

    ldr r0,=0x020c406c @取得时钟1的地址
    @或者 add r0, r0, #0x04
    str r1, [r0]       @赋值

    ldr r0,=0x020c4070 @取得时钟2的地址
    str r1, [r0]       @赋值

    ldr r0,=0x020c4074 @取得时钟3的地址
    str r1, [r0]       @赋值

    ldr r0,=0x020c4078 @取得时钟4的地址
    str r1, [r0]       @赋值

    ldr r0,=0x020c407c @取得时钟5的地址
    str r1, [r0]       @赋值

    ldr r0,=0x020c4080 @取得时钟6的地址
    str r1, [r0]       @赋值

    /* 配置IO复用 */  @ ctrl+shift+A
    ldr r0,=0x020e0068 @MUX复用地址
    ldr r1,=0x5        @要赋的值,16进制,0101是二进制
    str r1, [r0]       @赋值

    /* 电气属性
     * bit0     : 0   低速率
     * bit5-3   : 110 R0/6驱动能力
     * bit7-6   : 10  100MHz速度
     * bit11    : 0   关闭开路输出
     * bit12    : 1   使能上拉保持
     * bit13    : 0   保持
     * bit15-14 : 00  默认100K下拉
     * bit16    : 0   关闭hys
     * 综上:
     * 0000_0000_0000_0000_0001_0000_1011_0000
     * 0x000010C0
     */
    ldr r0,=0x020e02f4 @电气属性地址
    ldr r1,=0x10b0
    str r1, [r0]

    /* 设置GPIO */
    @ GPIO1_GDIR的bit3
    ldr r0,=0x0209c004 @GDIR
    ldr r1,=0x8        @8是1000, 输入输出设置
    str r1, [r0]

    /* 打开LED GPIO1_IO03为0 */
    ldr r0,=0x0209c000 @DR
    ldr r1,=0xfffffff7 @默认全是1,要配成0
    str r1, [r0]

/* 程序执行到这的时候,不知道会有什么事情,所以要有个死循环 */
loop:
    b loop

6. 编译程序

使用arm-linux-gnueabihf-gcc把所有的.s文件编译成点o

arm-linux-gnueabihf-gcc -g -c led.s -o led.o
  • -g产生调试信息,

  • -c是选择编译源文件,并且不链接

使用arm-linux-gnueabihf-ld将所有的.o文件链接为elf格式的可执行文件

把所有的o链接到一起,要有起始位置。本实验链接的时候要链接起始地址,链接起始地址就是代码运行的起始地址,或者保存的起始地址(一定是运行的,一般运行的和保存的是一个地址)

对于6ULL,链接起始地址应该指向RAM地址。

RAM分为内部RAM和外部RAM,DDR。内部为0x90000-0x91FFFFF,外部DDR中, 对于开发板0x80000000-0xA0000000(512MB)。

起始地址为0x8780000,后面UBOOT也是这个,所以要统一起来。要使用DDR必须要初始化DDR,bin文件不能直接烧写到mmc等外置存储中启动运行,需要添加一个头部,头部包含了DDR初始化参数。bootrom会从SD卡,EMMC等外置存储中读取头部信息,初始化DDr,并且将bin拷贝到链接起始地址,bin的运行地址一定要和链接起始地址一致。位置无关代码除外。

arm-linux-gnueabihf-ld -Ttext 0x87800000 led.o -o led.elf
  • -Ttext指定链接起始地址

使用arm-linux-gnueabihf-objcopy将elf文件转化为bin文件

arm-linux-gnueabihf-objcopy -O binary -S -g led.elf led.bin
  • -O 以什么形式输出
  • -S 表示不要复制源文件的重定位信息和符号信息
  • -g 不要复制源文件的调试信息

使用arm-linux-gnueabihf-objdump elf转为汇编,反汇编

arm-linux-gnueabihf-objdump -D led.elf > led.dis
  • -D表示反汇编所有的段

7. 烧写

将bin文件烧写到mmc中。

在Ubuntu下烧写SD卡:

插入U盘链接

将bin文件烧写到SD卡绝对地址上,对于I.MX而言必须在bin文件添加头部,使用imxdownload

两次插拔可知

烧在sdb1上

先给imx可执行权限

烧写成功

添加了头部的.bin文件就是load.imx

8.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

clean:
    rm -rf *.o *.bin *.elf *.dis

9.download.sh

chmod 777 imxdownload
./imxdownload led.bin /dev/sdb

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章