汇编LED驱动实验

跟着正点原子视频学

万事都从点灯起!

类似于stm32的启动文件:startup_stm32f10x_hd.s

它的作用是:

  1. 初始化堆栈指针 SP
  2. 初始化程序计数器指针 PC
  3. 设置堆、栈的大小
  4. 设置异常向量表的入表地址
  5. 配置外部 SRAM作为数据存储器(这个由用户配置,一般的开发板可没有外部 SRAM)
  6. 设置 C库的分支入口 _main (最终用来调用 main函数)
  7. 在3.5版的启动文件还调用了在 system_stm32f10x.c文件中的SystemInit()函数配置系统时钟

汇编LED原理分析

为什么要学习 Cortex-A 汇编:

  1. 需要用汇编初始化一些SOC外设
  2. 使用汇编初始化 DDR、I.MX6U 不需要
  3. 设置 SP指针、一边指向 DDR、设置好C语言运行环境

STM32 IO 初始化流程:

  1. 使能 GPIO时钟
  2. 设置 IO 复用,将其复用为 GPIO
  3. 配置 GPIO的电气属性
  4. 使用 GPIO,输出高/低电平

I.MX6ULL IO 初始化:

  1. 使能时钟,CCGR0-CCGR6这7个寄存器控制着 6ULL 所有外设时钟的使能。为了简单,设置 CCGR0-CCGR6 这7个寄存器全部为 0xFFFFFFFF,相当于使能所有外设时钟
  2. IO 复用,将寄存器 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 的 bit3-0 设置为0101=5,这样 GPIO1_IO03 就复用为 GPIO
  3. 寄存器 IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 是设置 GPIO1_IO03 的电气属性。也包括压摆率、速度、驱动能力、开漏、上下拉等。
  4. 配置 GPIO 功能,设置输入输出。设置 GPIO1_GDIR 寄存器bit3为1,也就是设置 GPIO1_IO03为输出模式。设置 GPIO1_DR 寄存器的bit3,为1表示输出高电平,为0表示输出低电平。

对于Cortex-A芯片来讲,大部分芯片在上电以后 C语言 环境还没准备好,所以第一行程序肯定是汇编的,至于要写多少汇编程序,那就看你能在哪一步把C语言环境准备好。所谓的C语言环境就是保证C语言能够正常运行。C语言中的函数调用涉及到出栈入栈,出栈入栈就要对堆栈进行操作,所谓的堆栈就是一段内存,这段内存比较特殊,由SP指针访问,SP指针指向栈顶。芯片一上电SP指针还没有初始化,所以 C语言无法运行,对于有些芯片还需要初始化DDR,因此一开始要用汇编来初始化DDR控制器。后面学习Uboot和Linux内核的时候汇编是必须要会的,是不是觉得很难?还要会汇编!前面都说了只是在芯片上电以后用汇编来初始化一些外设,不会涉及到复杂的代码,而且使用到的指令都是很简单的,用到的就那么十几个指令。所以,不要看到汇编就觉得复杂,打击学习信心。


汇编简介

存储器访问指令:

LDR指令:

LDR主要用于 从存储加载数据到寄存器 Rx中,LDR也可以将一个立即数加载到寄存器Rx中,LDR加载立即数的时候要使用 “=” ,而不是 “#” 。在嵌入式开发中,LDR最常用的就是读取CPU的寄存器值,比如 I.MX6UL 有个寄存器 GPIO1_GDIR,其地址为 0x0209C004,我们现在要读取这个寄存器中的数据

// 将寄存器地址 0X0209C004 加载到 R0 中,即 R0=0X0209C004
LDR R0, =0X0209C004

// 读取地址 0X0209C004 中的数据到 R1 寄存器中
LDR R1, [R0]

上述代码就是读取寄存器 GPIO_GDIR 中的值,读取到的寄存器值保存在 R1 寄存器中,上面代码 offset 是0,也就是没有用到 offset

STR指令:

LDR是从存储器读取数据,STR就是将数据写入到寄存器中,同样以 I.MX6UL 寄存器 GPIO1_GDIR 为例,现在我们要配置寄存器 GPIO1_GDIR 的值为 0X2000002

// 将寄存器地址 0X0209C004 加载到R0中,即R0=0X0209C004
LDR R0, =0X0209C004

// R1保存要写入到寄存器的值,即 R1=X20000002
LDR R1, =0X20000002

// 将R1中的值写入到R0中所保存的地址中
STR R1, [R0]

LDR 和 STR 都是按照字进行读取和写入的,也就是操作的 32位数据,如果要按照字节、半字节进行操作的话可以在指令 “LDR” 后面加上 B 或 H,比如按字节操作的指令就是 LDRB 和 STRB,按照半字操作的指令就是 LDRH 和STRH


汇编由一条一条指令构成,指令就涉及到汇编指令。

int a, b;
a = b;
假设a地址为0x20,b地址为0x30

LDR R0,=0x30 // R0=b
LDR R1,[R0] // R1=R0 -> R1=b

LDR R0,=0x20 // R0=a
STR R1,[R0] // R0=R1 -> a=R0=R1=b

我们在使用汇编编写驱动的时候最常用的就是 LDR 和 STR 这两个指令


压栈和出栈指令:

我们通常会在 A 函数中调用 B 函数,当 B 函数执行完后再回到 A 函数继续执行。要想在跳回 A 函数以后代码能够接着正常运行,那就必须在跳到 B 函数之前将当前处理器状态保存起来(就是保存 R0~R15 这些寄存器值),当 B 函数执行完成后再用前面保存的寄存器恢复 R0~R15 即可。保存 R0~R15 寄存器的操作就叫做现场保护,恢复 R0~R15 寄存器的操作就叫做恢复现场。在进行现场保护的时候需要进行压栈(入栈)操作,恢复现场就要进行出栈操作。压栈的指令为 PUSH ,出栈的指令为 POP ,PUSH 和 POP 是一种多存储和多加载指令,即可以一次操作多个寄存器数据,他们利用当前的栈指针 SP 来生成地址, PUSH 和 POP 的用法如下表:

指令 描述
PUSH <.reg list> 将寄存器列表存入栈中。
POP <.reg.list> 从栈中恢复寄存器列表

http://www.360doc.com/content/20/0423/21/277688_907959744.shtml

上面网址内容很好的讲解了出栈和入栈


Cortex-A 处理器运行模型

以前的 ARM 处理器有 7 种运行模型:User、FIQ、IRQ、Supervisor(SVC)、Abort、Undef 和 System,其中 User 是非特权模式。但新的 Cortex-A 架构加入了 TrustZone 安全拓展,所以就新加了一种运行模式:Monitor,新的处理器架构还支持虚拟机扩展,因此又加入了另一种运行模式:Hyp,所以 Cortex-A 处理器有 9 种处理模式

模式 描述
User(USR) 用户模式,非特权模式,大部分程序运行的时候就处于此模式
FIQ 快速中断模式,进入FIQ中断异常
IRQ 一般中断模式
Supervisor(SVC) 超级管理员模式,特权模式,供操作系统使用
Monitor(MON) 监视模式,这个模式用于安全扩展模式,只用户安全
Abort(ABT) 数据访问终止模式,用于虚拟存储以及存储保护
Hyp(HYP) 超级监视模式,用于虚拟化扩展
Undef(UND) 未定义指令终止模式
System 系统模式,用于运行特权级的操作系统任务

在表中,除了User(USR)用户模式之外,其他8种运行模式都是特权模式,在特权模式下,程序可以访问所有的系统资源。


寄存器组

ARM架构提供了 16个 32位的通用寄存器(R0~R15)供软件使用,前 15个(R0~R14)可以用作通用的数据存储,R15 是程序计数器 PC,用来保存将要执行的指令。ARM还提供了一个当前程序状态寄存器 CPSR 和一个备份程序状态寄存器 SPSRSPSR寄存器就是 CPSR寄存器的备份

在这里插入图片描述


.global _start @全局标号

_start:
    /*使能所有外设时钟*/
    ldr r0, =0x020c4068 @ CCGR0
    ldr r1, =0xffffffff @ 要向CCGR0写入打数据
    str r1, [r0]        @ 将0xffffffff写入到CCGR0中
    
    ldr r0, =0x020c406c @ CCGR1
    str r1, [r0]        @ 将0xffffffff写入到CCGR1中

    ldr r0, =0x020c4070 @ CCGR2
    str r1, [r0]        @ 将0xffffffff写入到CCGR2中

    ldr r0, =0x020c4074 @ CCGR3
    str r1, [r0]        @ 将0xffffffff写入到CCGR3中

    ldr r0, =0x020c4078 @ CCGR4
    str r1, [r0]        @ 将0xffffffff写入到CCGR4中

    ldr r0, =0x020c407c @ CCGR5
    str r1, [r0]        @ 将0xffffffff写入到CCGR5中

    ldr r0, =0x020c4080 @ CCGR6
    str r1, [r0]        @ 将0xffffffff写入到CCGR6中

    /*配置 GPIO1——IO03 PIN的复用为GPIO,也就是设置
        IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03=5
        IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03寄存器地址为0X020E0068
    */
    ldr r0, =0x020E0068 @ IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03
    ldr r1, =0x5        @ 要写入到数据
    str r1, [r0]        @ 将5写入到IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03中

    /*配置GPIO1——IO03到电气属性 也就是寄存器:
        IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03
        IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03寄存器地址为0x020e02f4
        bit0: 0 低速率
        bit2-1:  保留
        bit5-3: 110 R0/6驱动能力
        bit7-6: 10  100MHz速度
        bit10-8:   保留
        bit11:  0   关闭开路输出
        bit12:  1   使能pull/keeper
        bit13:  0   keeper
        bit15-14:   00  100k下拉
        bit16:  0   关闭HYS
        bit31-17:   保留
     */
     ldr r0, =0x020e02f4
     ldr r1, =0x10b0
     str r1, [r0]

     /* 设置GPIO
        设置GPIO1_GDIR寄存器,设置GPIO1_GPIO03为输出
        GPIO1_GDIR寄存器地址为0x0209c004,设置GPIO1_GDIR寄存器bit3为1
        也就是设置GPIO1_GPIO03为输出
      */
      ldr r0, =0x0209c004
      ldr r1, =0x8
      str r1, [r0]

      /* 打开LED,也就是设置GPIO1_IO03为0
         GPIO1_DR寄存器地址为0x0209c000
       */
       ldr r0, =0x0209c000
       ldr r1, =0
       str r1, [r0]

loop:
    b loop

编写驱动文件

1、编译程序:

  • 使用 arm-linux-gnueabihf-gcc,将 .c .s 文件变为 .o
  • 将所有的 .o 文件连接为 elf 格式的可执行文件
  • 将 elf 文件转为 bin 文件
  • 将 elf 文件转为汇编,反汇编

链接:

  • 链接就是将所有 .o 文件链接在一起,并且链接到指定的地方。本实验链接的时候要指定链接起始地址。链接起始地址就是代码运行的起始地址。
  • 对于 6ULL 来说,链接起始地址应该指向 RAM 地址。RAM分为内部RAM和外部RAM,也就是 DDR。6ULL内部RAM地址范围 0X900000~0X91FFFFF。也可以放到外部 DDR中,对于 LMX6U~ALPHA开发板,512MB 字节 DDR 版本的核心板,DDR 范围就是 0X80000000~0X9FFFFFFF。对于256MB的DDR来说,那就是 0X80000000~0X8FFFFFFF
  • 本系列视频,裸机代码的链接起始地址为 0X87800000。要使用 DDR,那么必须要初始化 DDR,bin文件不能直接烧写到SD卡、EMMC、NAND等外置存储中,然后从这些外置存储中启动运行。而不是bin文件不能直接运行,使用JLINK将bin文件直接下载到内部RAM中还是可以运行的
  • Bin的运行地址一定要和链接地址一致。位置无关代码除外。

烧写 bin 文件

STM32烧写到内部 FLASH

6ULL 支持 SD卡,EMMC、NAND、nor、SPI flash 等等启动。裸机例程选择烧写到SD卡里面。

在Ubuntu下向SD卡烧写裸机 bin文件。烧写不是将 bin文件 拷贝到 SD卡中,而是将 bin文件烧写到SD卡 绝对地址上。而且对于 LMX 而言,不能直接烧写bin文件,比如先在bin文件前面添加头部。完成这个工作,需要正点原子提供的 imxdownload软件。

imxdownload 使用方法,确定要烧写的SD卡文件,我的是 /dev/sdb

给予 imxdownload 可执行权限:chmod 777 imxdownload

烧写:/imxdownload led.bin /dev/sdb

imxdownload 会向 led.bin 添加一个头部,生成新的 load.imx文件,这个 load.imx 文件就是最终烧写到SD卡里面去的。

JLINK:

  1. 6ULL支持JTAG,因为没有烧写算法,所以无法烧写。
  2. 但是可以通过JTAG将bin文件下载到内部RAM。
  3. 奇葩的问题,6ULL的JTAG口竟然和SAI复用,SAI连接了 WM8960 音频 DAC。
  4. 在嵌入式Linux开发中基本不使用JLINK,普通开发者。
  5. 点灯和串口调试程序。

一、硬件启动方式选择

1、启动方式的选择:

  • LED灯实验,是从 SD 卡读取 bin 文件并启动,说明 6ULL支持从 SD 卡启动。6ULL 支持多种启动方式。6ULL是怎么支持从多种外置 flash 启动程序的。

    • BOOT_MODE0 和 BOOT_MODE1,这两个是由两个 IO 来控制的。选择从 USB 启动还是内部 BOOT 启动。如果要烧写系统到开发板中可以选择从 USB 下载,下载到 SD 卡,EMMC,NAND等外置存储中。烧写完成设置从内部 BOOT 启动,然后从相应的外置存储中启动。

2、启动设备的选择:

  • 选择启动设备:前提是,你设置 MODE1 和 MODE0 是从内部 BOOT 启动的,也就是 MODE1=1,MODE0=0
  • 支持哪些设备:NOR flash,OneNAND、NAND Flash、QSPI Flash、SD/EMMC、EEPROM。我们最常用的就是 NAND、SD、EMMC 甚至 QSPI Flash。
  • 如何选择启动设备:通过 BOOT_CFG 选择,有 BOOT_CFG1,2,4,每个8位。BOOT_CFG 是由 LCD_DATA0~23 来设置的。在ALPHA开发板上,大部分默认都 47k下拉电阻 接地,BOOT_CFG4的8根线全部接地。BOOT_CFG2全部接地,除了 BOOT_CFG2[3],此位用来选择 SD 卡启动接口。BOOT_CFG1,0、1、2都是定死的。3、4、5、6、7是可以设置的。
  • 正点原子开发板 BOOT 电路设置。核心板 LCD_DATA0~23基本 47K下拉

1、Boot Rom做的事情

  • 设置内核时钟为 396MHz。使能 MMU 和 Cache,使能 L1cache L2 cache MMU,目的就是为了加速启动。
  • Boot_CFG设置的外置存储中,读取 image,然后做相应的处理。

2、IVT 和 Boot Data数据

  • Bin文件前面要添加头部。可以得到,我们烧写到 SD卡中的 load.imx 文件在 SD卡中的起始地址是 0x400,也就是 1024
  • 头部大小为 3KB,加上偏移的 1KB,一共是 4KB,因此在SD卡中bin文件起始地址为 4096
  • IVT 大小为 32B/4=8条
  • IVT + Boot Data的数据,很多是我从 NXP官方 u-boot.imx文件中提取出来的。

3、DCD数据(Device Configuration Data)

  • DCD数据就是配置 6ULL 内部寄存器的
  • 首先,将 CCR0~CCR6 全部写为 0XFFFFFFFF,表示打开所有外设时钟。然后就是 DDR 初始化参数。设置 DDR 控制器,也就是初始化 DDR。

C语言版本LED驱动实验

在前面我们用汇编的形式来编写LED灯驱动,但由于汇编太难,而且写出来也不好理解,所以大部分情况下汇编就是在工程开始时初始化一下C语言环境,比如初始化 DDR ,设置堆栈指针 SP 等等,当这些工作都做完以后就可以进入C语言环境,也就是运行C语言代码,一般是进入main函数,所以我们有两部分文件要做:

  • ①、汇编文件:汇编文件只是用来完成C语言环境搭建。
  • ②、C语言文件:C语言文件就是完成我们的业务层代码的,其实就是我们实际例程要完成的功能。

猜你喜欢

转载自blog.csdn.net/xuexiwd/article/details/119851676