Linux-ARM裸机开发(三)-汇编LED驱动实验
本文目录
一、LED驱动原理分析
1.1 为什么要学习Cortex-A汇编
①需要用汇编初始化一些SOC外设,片上系统(SoC,System on Chip)。
②使用汇编初始化DDR(DDR SDRAM),I.MX6ULL此款芯片不用使用汇编初始化DDR。因为NXP在I.MX6ULL 内部96KB的ROM中存放了自己编写的启动代码,这些启动代码可以读取DDR配置信息,并且完成DDR的初始化。
③设置sp指针,一般指向DDR(Cortex-A系列的内部RAM比较小,一般sp指针不会指向内部RAM),设置好C语言运行环境(C语言运行环境就是指设置sp指针,因为C语言运行环境中需要保护现场—入栈和出栈,而入栈和出栈就要用到sp指针)。
1.2 LED对应的引脚图
以下为LED对应的引脚图,图中LED0与GPIO3相连:
1.3 STM32 IO初始化流程
①、使能GPIO时钟。
②、设置IO复用,将其复用为GPIO。
③、配置GPIO的电气属性(上下拉,速度等)。
④、使用GPIO,输出高/低电平。
1.4 I.MX6ULL IO初始化流程(原理分析-重点)
①、使能时钟,CCGR0-CCGR6这7个寄存器控制着6ULL所有外设时钟的使能。(参考手册Chapter18)
每个模块由两个位表示,如下图所示:
下图CCGR0的30-31位控制着gpio2的时钟使能,28-29位控制着uart2的时钟使能。
将两个位置设为:
00:所有模式下时钟都关闭 01:只有运行模式下,时钟使能
10:还未使用(保留) 11:所有模式下时钟全部开启
为了简单,设置CCGR0~CCGR6这7个寄存器全部为0XFFFFFFFF,相当于使能所有外设时钟。
②、IO复用,配置MUX寄存器(参考手册Chapter32)
如下图所示,bit31-5位全部保留,只有bit4-0这五个位用到。
bit4=1:强制输入路径;bit4=0:路径由功能决定
bit3-0:设置IO复用模式。0000:IIC模式,0101为GPIO1_IO03复用为gpio功能。其他的如下图:
将寄存器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03的bit3~0设置为0101=5,这样GPIO1_IO03就复用为GPIO。
③、设置电气属性,配置PAD寄存器,包括压摆率、速度、驱动能力、开漏、上下拉等。(参考手册Chapter32)
需要配置HYS、PUS、PUE,PKE,ODE,SPEED,DSE,SRE块。
HYS(bit6):用来使能迟滞比较器,当 IO 作为输入功能的时候有效,用于设置输入接收器的施密特触发器是否使能。如果需要对输入波形进行整形的话可以使能此位。此位为 0 的时候禁止迟滞比较器,为 1 的时候使能迟滞比较器。
PUS(bit15:14): 用来设置上下拉电阻的,一共有四种选项可以选择:
00:100K下拉、01:47K上拉、10:100K上拉、11:22K上拉
PUE(bit13):当 IO 作为输入的时候,这个位用来设置 IO 使用上下拉还是状态保持器。当为 0 的时候使用状态保持器,当为 1 的时候使用上下拉。状态保持器在IO 作为输入的时候才有用,就是当外部电路断电以后此 IO 口可以保持住以前的状态。
PKE(bit12):此位用来使能或者禁止上下拉/状态保持器功能,为0 时禁止上下拉/状态保持器,为 1 时使能上下拉和状态保持器。
ODE(bit11):当 IO 作为输出的时候,此位用来禁止或者使能开路输出,此位为 0 的时候禁止开路输出,当此位为 1 的时候就使能开路输出功能。
SPEED(bit7:6):当 IO 用作输出的时候,此位用来设置 IO 速度,具体输出速度设置如下:
DSE(bit5:3):当 IO 用作输出的时候用来设置 IO 的驱动能力,如下图所示,电阻值由上至下减少,输出驱动能力上升:
SRE(bit0):设置压摆率,当此位为 0 的时候是低压摆率,当为 1的时候是高压摆率。这里压摆率就是 IO 电平跳变所需要的时间,比如从 0 到 1 需要多少时间,时间越小波形就越陡,说明压摆率越高;时间越长波形就越缓,压摆率就越低。如果你的产品要过 EMC 的话那就可以使用小的压摆率,因为波形缓和,如果使用 IO做高速通信的话就可以使用高压摆率。
IO功能图如下(我看不太懂):
寄存器IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03是设置GPIO1_IO03的电气属性。包括压摆率、速度、驱动能力、开漏、上下拉等。
④、配置GPIO功能,设置输入输出。(参考手册Chapter28)
**注意:**IOMUXC_SW_MUX_CTL_PAD_XX_XX和IOMUXC_SW_PAD_CTL_PAD_XX_XX 这两种寄存器都是配置 IO 的,注意是 IO!不是 GPIO,GPIO 是一个 IO 众多复用功能中的一种。比如 GPIO1_IO00 这个 IO 可以复为:I2C2_SCL、GPT1_CAPTURE1、ANATOP_OTG1_ID、ENET1_REF_CLK 、 MQS_RIGHT 、 GPIO1_IO00 、 ENET1_1588_EVENT0_IN 、SRC_SYSTEM_RESET 和 WDOG3_WDOG_B 这 9 个功能,GPIO1_IO00 是其中的一种,我们想要把 GPIO1_IO00 用作哪个外设就复用为哪个外设功能即可。
GPIO 结构如下图所示:
图中左下角的SW_MUX_CTL_PAD和SW_PAD_CTL_PAD这两种寄存器前面说了用来设置 IO 的复用功能和 IO 属性配置。左上角部分的 GPIO 框图就是,当 IO 用作 GPIO 的时候需要设置的寄存器,一共有八个:DR、GDIR、PSR、ICR1、ICR2、EDGE_SEL、IMR 和 ISR。 I.MX6U 一共有GPIO1~GPIO5 共五组 GPIO,每组 GPIO 都有这 8 个寄存器。
下面将介绍一下这8个寄存器:
1.DR寄存器,此寄存器为数据寄存器,其结构图如下:
该寄存器为32位,一个GPIO组最大只有32个IO,故DR寄存器每个位对应一个GPIO。此寄存器功能为:当GPIO被配置为输出模式后,若要GPIOx_IOx输出高电平,则设置GPIOx.DR对应的bitx=1,反之,若要GPIOx_IOx输出低电平,则设置GPIOx.DR对应的bitx=0;当GPIO被配置为输入模式后,该寄存器用于保存对应的IO的电平,如GPIOx-IOx引脚接地,则GPIOx.DR的bit0位为0。
2.GDIR 寄存器,此寄存器为方向寄存器,其结构图如下:
GDIR寄存器也是32位,该寄存器主要是设置IO的工作方向,输入或者输出。若要设置GPIOx_IOx为输入时,则设置GPIOx.GDIR对应的bitx=0;若要设置GPIOx_IOx为输出时,则设置GPIOx.GDIR对应的bitx=1;
3.PSR 寄存器, 此寄存器为GPIO 状态寄存器,其结构图如下:
同样的 PSR 寄存器也是一个 GPIO 对应一个位,读取相应的位即可获取对应的 GPIO 的状态,也就是 GPIO 的高低电平值。功能和输入状态下的 DR 寄存器一样。
4.ICR1和ICR2寄存器,两个寄存器都是中断控制寄存器,ICR1结构图如下:
ICR1 用于 IO0-15 的配置, ICR2 用于 IO16-31 的配置。ICR1 寄存器中一个 GPIO 用两个
位,这两个位用来配置中断的触发方式,配置表如下图所示:
5.IMR 寄存器,这是中断屏蔽寄存器,结构图如下:
IMR 寄存器也是一个 GPIO 对应一个位,IMR 寄存器用来控制 GPIO 的中断禁止和使能,如果某个 GPIO 使能中断,那么设置相应的位为 1 即可,反之,如果要禁止中断,那么就设置相应的位为 0 即可。
6.ISR寄存器 ,是中断状态寄存器,结构图如下:
ISR 寄存器也是 32 位寄存器,一个 GPIO 对应一个位,只要某个 GPIO 的中断发生,那么ISR 中相应的位就会被置1。所以,我们可以通过读取 ISR 寄存器来判断 GPIO 中断是否发生,相当于 ISR 中的这些位就是中断标志位。当我们处理完中断以后,必须清除中断标志位,清除方法就是向 ISR 中相应的位写1,也就是写1清零。
7.EDGE_SEL 寄存器,这是边沿选择寄存器,结构图如下:
EDGE_SEL 寄存器用来设置边沿中断,这个寄存器会覆盖 ICR1 和 ICR2 的设置,同样是一个GPIO 对应一个位。如果相应的位被置1,那么就相当与设置了对应的 GPIO 是上升沿和下降沿(双边沿)触发。也就是说如果EDGE_SEL对应IO的bit=1时,无论上面的ICR寄存器设置为什么触发,该IO都被设为双边沿触发;若为0时,就是ICR寄存器设置为什么中断触发,则为什么触发。
二、汇编LED驱动实验
2.1 驱动编写
①使能外设时钟
.global _start @全局标号
_start:
/*使能所有时钟 */
ldr r0, =0x020c4068 @CCGR0寄存器地址
ldr r1, =0xffffffff @要向CCGR0寄存器中写入的数据
str r1, [r0] @向CCGR0的地址写入0xfffffffff
ldr r0, =0x020c406c @CCGR1寄存器地址
ldr r1, =0xffffffff
str r1, [r0]
ldr r0, =0x020c4070 @CCGR2寄存器地址
ldr r1, =0xffffffff
str r1, [r0]
ldr r0, =0x020c4074 @CCGR3寄存器地址
ldr r1, =0xffffffff
str r1, [r0]
ldr r0, =0x020c4078 @CCGR4寄存器地址
ldr r1, =0xffffffff
str r1, [r0]
ldr r0, =0x020c407c @CCGR5寄存器地址
ldr r1, =0xffffffff
str r1, [r0]
ldr r0, =0x020c4080 @CCGR6寄存器地址
ldr r1, =0xffffffff
str r1, [r0]
②设置IO复用模式,配置MUX寄存器
ldr r0, =0x020e0068
ldr r1, =0x05
str r1, [r0]
③设置电气属性,配置PAD寄存器
/* bit0: 0 低速
* bit5:3 110 R0/6驱动能力
* bit7:6 10 100MHZ速度
* bit11 0 禁止开路输出
* bit12 1
* bit13 0
*bit15:14 00 100k下拉
* bit16 0 关闭hys
*/
ldr r0, =0x020e02f4
ldr r1, =0x10b0
str r1, [r0]
④ 配置GPIO为输出模式:GDIR寄存器配置bit3=1,开灯:DR寄存器配置bit3=0
ldr r0, =0x209c004
ldr r1, =0x08
str r1, [r0]
ldr r0, =0x209c000
ldr r1, =0x00
str r1, [r0]
⑤ 最后加个死循环
loop:
b loop
2.2 编译程序
①、使用arm-linux-gnueabihf-gcc,将.c .s文件变为.o
arm-linux-gnueabihf-gcc -g -c leds.s -o led.o
②、将所有的.o文件链接为elf格式的可执行文件。
链接就是将所有.o文件链接在一起,并且链接到指定的地方。本实验链接的时候要指定链接起始地址。链接起始地址就是代码运行的起始地址。
对于6ULL来说,链接起始地址应该指向RAM地址。RAM分为内部RAM和外部RAM,也就是 DDR。6ULL内部RAM地址范围0X900000-0X91FFFF。也可以放到外部DDR中,对于I.MX6U-ALPHA开发板,512MB字节DDR版本的核心板,DDR范围就是0X80000000-0X9FFFFFFF。对于256MB的DDR来说,那就是0X80000000-0X8FFFFFFF。
本系列视频,裸机代码的链接起始地址为0X87800000。要使用DDR,那么必须要初始化DDR,对于I.MX来说bin文件不能直接运行,需要添加一个头部,这个头部信息包含了DDR的初始化参数,I.MX系列SOC内部boot rom会从SD卡,EMMC等外置存储中读取头部信息,然后初始化DDR,并且将bin文件拷贝到指定的地方。
Bin的运行地址一定要和链接起始地址一致。位置无关代码除外。
确定了链接地址以后就可以使用 arm-linux-gnueabihf-ld 来将前面编译出来的 led.o 文件链接到 0X87800000 这个地址,使用如下命令:
arm-linux-gnueabihf-ld -Ttext 0X87800000 led.o -o led.elf
上述命令中-Ttext 就是指定链接地址,“-o”选项指定链接生成的 elf 文件名,这里我们命名为 led.elf。
③、将elf文件转为bin文件。
led.elf 文件也不是我们最终烧写到 SD 卡中的可执行文件,我们要烧写的.bin 文件,因此还需要将 led.elf 文件转换为.bin 文件。我们需要用arm-linux-gnueabihf-objcopy将 led.elf 文件转换为led.bin 文件,命令如下:
arm-linux-gnueabihf-objcopy -O binary -S -g led.elf led.bin
上述命令中,“-O”选项指定以什么格式输出,后面的“binary”表示以二进制格式输出,选项“-S”表示不要复制源文件中的重定位信息和符号信息,“-g”表示不复制源文件中的调试信息。
④、将elf文件转为汇编,反汇编。
大多数情况下我们都是用 C 语言写试验例程的,有时候需要查看其汇编代码来调试代码,因此就需要进行反汇编,一般可以将 elf 文件反汇编,比如如下命令:
arm-linux-gnueabihf-objdump -D led.elf > led.dis
上述代码中的“-D”选项表示反汇编所有的段,反汇编完成以后就会在当前目录下出现一个名为 led.dis 文件。
2.3 烧写代码
STM32烧写到内部FLASH。
6ULL支持SD卡、EMMC、NAND、nor、SPI flash等等启动。裸机例程选择烧写到SD卡里面。
在ubuntu下向SD卡烧写裸机bin文件。烧写不是将bin文件拷贝到SD卡中,而是将bin文件烧写到SD卡绝对地址上。而且对于I.MX而言,不能直接烧写bin文件,比如先在bin文件前面添加头部。完成这个工作,需要使用正点原子提供的imxdownload软件
① 给予 imxdownload 可执行权限
chmod 777 imxdownload
向 SD 卡烧写 bin 文件
./imxdownload led.bin /dev/sdb
Imxdownlaod会向led.bin添加一个头部,生成新的load.imx文件,这个load.imx文件就是最终烧写到SD卡里面去的。
三、汇编LED驱动实验
3.1 设置处理器模式
设置6ULL处于SVC模式(超级管理员模式,特权模式,供操作系统使用),将CPSR寄存器的bit4~0,也就是M[4:0],设置为10011=0x13。读写状态寄存器需要MRS和MSR指令。
MRS指令将CPSR寄存器数据读出到通用寄存器中,MSR指令将通用寄存器的值写入到CPSR寄存器当中。
3.2 设置SP指针
SP指针可以指向内部RAM,也可以指向DDR,我们将其指向到DDR上。512M的DDR范围0x80000000-0x9FFFFFFFF。栈大小,0x200000=2MB,处理器栈增长方式,A7是向下增长的。设置SP指向0x80200000。
3.3 跳转到C语言
使用b指令,跳转到C语言,比如main函数。
3.4 代码
start.s
.global _start
_start:
/* 设置处理器进入SVC模式 */
mrs r0, cpsr /* 读取cpsr的值到r0 */
bic r0, r0, #0x1f /* 将cpsr寄存器bit0:5清零 */
orr r0, r0, #0x13 /* 设置为SVC模式 */
msr cpsr, r0 /* 将r0写入到cpsr */
/* 设置SP指针 */
ldr sp, =0x80200000
b main
main.h
#ifndef __MAIN_H
#define __MAIN_H
/* 定义要使用的寄存器 */
#define CCM_CCGR0 *((volatile unsigned int *)0X020C4068)
#define CCM_CCGR1 *((volatile unsigned int *)0X020C406C)
#define CCM_CCGR2 *((volatile unsigned int *)0X020C4070)
#define CCM_CCGR3 *((volatile unsigned int *)0X020C4074)
#define CCM_CCGR4 *((volatile unsigned int *)0X020C4078)
#define CCM_CCGR5 *((volatile unsigned int *)0X020C407C)
#define CCM_CCGR6 *((volatile unsigned int *)0X020C4080)
#define SW_MUX_GPIO1_IO03 *((volatile unsigned int *)0X020E0068)
#define SW_PAD_GPIO1_IO03 *((volatile unsigned int *)0X020E02F4)
#define GPIO1_DR *((volatile unsigned int *)0X0209C000)
#define GPIO1_GDIR *((volatile unsigned int *)0X0209C004)
#define GPIO1_PSR *((volatile unsigned int *)0X0209C008)
#define GPIO1_ICR1 *((volatile unsigned int *)0X0209C00C)
#define GPIO1_ICR2 *((volatile unsigned int *)0X0209C010)
#define GPIO1_IMR *((volatile unsigned int *)0X0209C014)
#define GPIO1_ISR *((volatile unsigned int *)0X0209C018)
#define GPIO1_EDGE_SEL *((volatile unsigned int *)0X0209C01C)
#endif
main.c
#include "main.h"
/* 使能外设时钟 */
void clk_enable(void)
{
CCM_CCGR0 = 0xFFFFFFFF;
CCM_CCGR1 = 0xFFFFFFFF;
CCM_CCGR2 = 0xFFFFFFFF;
CCM_CCGR3 = 0xFFFFFFFF;
CCM_CCGR4 = 0xFFFFFFFF;
CCM_CCGR5 = 0xFFFFFFFF;
CCM_CCGR6 = 0xFFFFFFFF;
}
void led_init(void)
{
SW_MUX_GPIO1_IO03 = 0x05; /*复用为GPIO1_IO3*/
SW_PAD_GPIO1_IO03 = 0x10b0; /*设置电气属性*/
GPIO1_GDIR = 0x08;
GPIO1_DR = 0X00;
}
/*短延时*/
void delay_short(volatile unsigned int n)
{
while (n--){}
}
/*1ms延时 主频396MHz时*/
void delay(volatile unsigned int n)
{
while (n--)
{
delay_short(0x7ff);
}
}
void led_on(void)
{
GPIO1_DR &= ~(1<<3);
}
void led_off(void)
{
GPIO1_DR |= (1<<3);
}
int main(void)
{
/*使能外设时钟*/
clk_enable();
/* 初始化LED */
led_init();
/* 设置LED闪烁 */
while (1)
{
led_on();
delay(500);
led_off();
delay(500);
}
return 0;
}
Makefile
objs = start.o main.o
ledc.bin : $(objs)
arm-linux-gnueabihf-ld -Ttext 0X87800000 -o ledc.elf $^
arm-linux-gnueabihf-objcopy -O binary -S ledc.elf $@
arm-linux-gnueabihf-objdump -D -m arm ledc.elf > ledc.dis
%.o : %.c
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $<
%.o : %.s
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $<
clean:
rm -rf *.o ledc.bin ledc.elf ledc.dis
烧写代码同上面的汇编
3.5 链接脚本
SECTIONS{
. = 0X87800000;
.text :
{
start.o
*(.text)
}
.rodata ALIGN(4) : {*(.rodata*)}
.data ALIGN(4) : { *(.data) }
__bss_start = .;
.bss ALIGN(4) : { *(.bss) *(COMMON) }
__bss_end = .;
}
Makefile修改为如下
objs = start.o main.o
ledc.bin : $(objs)
arm-linux-gnueabihf-ld -Timx6u.ids $^ -o ledc.elf
arm-linux-gnueabihf-objcopy -O binary -S ledc.elf $@
arm-linux-gnueabihf-objdump -D -m arm ledc.elf > ledc.dis
%.o : %.c
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $<
%.o : %.s
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $<
clean:
rm -rf *.o ledc.bin ledc.elf ledc.dis