嵌入式linux驱动之路05:LED灯实验
LED灯汇遍实验
I.MX6U GPIO
I.MX6ULL 的 IO 分为两类:SNVS 域的和通用的,形如“IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO00”的就是 GPIO 命名,命名形式就是“IOMUXC_SW_MUC_CTL_PAD_XX_XX”,后面的“XX_XX”就是 GPIO 命名,比如:GPIO1_IO01、UART1_TX_DATA、JTAG_MOD 等等。,I.MX6U 的 GPIO 不止 GPIO1_IO00~GPIO1_IO09 这 10 个,其它的 IO 都可以复用为 GPIO 来使用。GPIO 是一个 IO 众多复用功能中的一种。和 STM32一样,如果某个 IO 要作为某个外设引脚使用的话,是需要配置复用寄存器的。
左 下 角 的 IOMUXC 框 图 里 面 就 有 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 个寄存器。
此 DR 寄存器中的每个位都对应一个 GPIO。当 GPIO 被配置为输出功能以后,向指定的位写入数据那么相应的 IO 就会输出相应的高低电平,比如要设置 GPIO1_IO00 输出高电平,那么就应该设置 GPIO1.DR=1。当 GPIO被配置为输入模式以后,此寄存器就保存着对应 IO 的电平值,每个位对对应一个 GPIO,例如,当 GPIO1_IO00 这个引脚接地的话,那么 GPIO1.DR 的 bit0 就是 0。
GDIR 寄存器,这是方向寄存器,用来设置某个 GPIO 的工作方向的,即输入/输出,如果要设置 GPIO 为输入的话就设置相应的位为 0,如果要设置为输出的话就设置为 1。比如要设置 GPIO1_IO00 为输入,那么 GPIO1.GDIR=0。
PSR 寄存器也是一个 GPIO 对应一个位,读取相应的位即可获取对应的 GPIO 的状态,也就是 GPIO 的高低电平值。功能和输入状态下的 DR 寄存器一样。
ICR1和ICR2这两个寄存器,都是中断控制寄存器,ICR1用于配置低16个GPIO,ICR2 用于配置高 16 个 GPIO,。ICR1 寄存器中一个 GPIO 用两个位,这两个位用来配置中断的触发方式。
IMR 寄存器也是一个 GPIO 对应一个位,IMR 寄存器用来控制 GPIO 的中断禁止和使能,如果使能某个 GPIO 的中断,那么设置相应的位为 1 即可,反之,如果要禁止中断,那么就设置相应的位为 0 即可。
ISR 寄存器也是 32 位寄存器,一个 GPIO 对应一个位,只要某个 GPIO 的中断发生,那么ISR 中相应的位就会被置 1。所以,我们可以通过读取 ISR 寄存器来判断 GPIO 中断是否发生,相当于 ISR 中的这些位就是中断标志位。当我们处理完中断以后,必须清除中断标志位,清除方法就是向 ISR 中相应的位写 1,也就是写 1 清零。
EDGE_SEL 寄存器用来设置边沿中断,这个寄存器会覆盖 ICR1 和 ICR2 的设置,同样是一个 GPIO 对应一个位。如果相应的位被置 1,那么就相当与设置了对应的 GPIO 是上升沿和下降沿(双边沿)触发。
要使用某个外设,必须要先使能对应的时钟。 CMM 有
CCM_CCGR0~CCM_CCGR6 这 7 个寄存器,这 7 个寄存器控制着 I.MX6U 的所有外设时钟开关,CCM_CCGR0 是个 32 位寄存器,其中每 2 位控制一个外设的时钟。
总结一下,要将 I.MX6U 的 IO 作为 GPIO 使用,我们需要一下几步:
①、使能 GPIO 对应的时钟。
②、设置寄存器 IOMUXC_SW_MUX_CTL_PAD_XX_XX,设置 IO 的复用功能,使其复用为 GPIO 功能。
③、设置寄存器 IOMUXC_SW_PAD_CTL_PAD_XX_XX,设置 IO 的上下拉、速度等等。
④、第②步已经将 IO 复用为了 GPIO 功能,所以需要配置 GPIO,设置输入/输出、是否使用中断、默认输出电平等。
实验步骤
1、使能 GPIO1 时钟
GPIO1 的时钟由 CCM_CCGR1 的 bit27 和 bit26 这两个位控制,将这两个位都设置位 11 即可。
2、设置 GPIO1_IO03 的复用功能
找到 GPIO1_IO03 的复用寄存器“IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03”的地址为
0X020E0068,然后设置此寄存器,将 GPIO1_IO03 这个 IO 复用为 GPIO 功能,也就是 ALT5。
3、配置 GPIO1_IO03
找到配置寄存器“IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03”的地址为0X020E02F4,根据实际使用情况,配置此寄存器。
4、设置 GPIO
GPIO1_IO03 是作为输出功能的,因此 GPIO1_GDIR 的 bit3 要设置为 1,表示输出。
5、控制 GPIO 的输出电平
向 GPIO1_DR 寄存器的 bit3 写入 0 即可控制 GPIO1_IO03 输出低电平。
编译代码
我们是要编译出在 ARM 开发板上运行的可执行文件,所以要使用交叉编译器 arm-linux-gnueabihf-gcc 来编译。因为先将 led.s 编译为对应的.o 文件,在终端中输入如下命令:
arm-linux-gnueabihf-gcc -g -c led.s -o led.o
上述命令就是将 led.s 编译为 led.o,其中“-g”选项是产生调试信息,-c”选项是编译源文件,但是不链接。“-o”选项是指定编译产生的文件名字,这里我们指定 led.s 编译完成以后的文件名字为 led.o。执行上述命令以后就会编译生成一个 led.o 文件。一个工程中所有的 C文件和汇编文件都会编译生成一个对应的.o 文件,我们需要将这.o 文件链接起来组合成可执行文件。
arm-linux-gnueabihf-ld 用来将众多的.o 文件链接到一个指定的链接位置。这些.o 文件就是这个链接地址开始
依次存放,最终生成一个可以下载的 hex 或者 bin 文件。
因此我们现在需要做的就是确定一下本试验最终的可执行文件其运行起始地址,也就是链接地址。这里我们要区分“存储地址”和“运行地址”这两个概念,“存储地址”就是可执行文件存储在哪里,可执行文件的存储地址可以随意选择。“运行地址”就是代码运行的时候所处的地址,这个我们在链接的时候就已经确定好了,代码要运行,那就必须处于运行地址处,否则代码肯定运行出错。比如 I.MX6U 支持 SD 卡、EMMC、NAND 启动,因此代码可以存储到 SD 卡、EMMC 或者 NAND 中,但是要运行的话就必须将代码从 SD 卡、EMMC 或者NAND 中拷贝到其运行地址(链接地址)处,“存储地址”和“运行地址”可以一样。
比如例程烧写到 SD 卡中,上电以后 I.MX6U 的内部 boot rom 程序会将可执行文件拷贝到链接地址处,这个链接地址可以在 I.MX6U 的内部 128KB RAM 中(0X900000~0X91FFFF),也可以在外部的 DDR 中。链接起始地址为 0X87800000。确定了链接地址以后就可以使用 arm-linux-gnueabihf-ld 来将前面编译出来的 led.o 文件链接到 0X87800000 这个地址,使用如下命令:
arm-linux-gnueabihf-ld -Ttext 0X87800000 led.o -o led.elf
上述命令中-Ttext 就是指定链接地址,“-o”选项指定链接生成的 elf 文件名。
arm-linux-gnueabihf-objcopy 更像一个格式转换工具,我们需要用它将 led.elf 文件转换为led.bin 文件,命令如下:
arm-linux-gnueabihf-objcopy -O binary -S -g led.elf led.bin
上述命令中,“-O”选项指定以什么格式输出,后面的“binary”表示以二进制格式输出,选项“-S”表示不要复制源文件中的重定位信息和符号信息,“-g”表示不复制源文件中的调试信息。
大多数情况下我们都是用 C 语言写试验例程的,有时候需要查看其汇编代码来调试代码,因此就需要进行反汇编,一般可以将 elf 文件反汇编,比如如下命令:
arm-linux-gnueabihf-objdump -D led.elf > led.dis
上述代码中的“-D”选项表示反汇编所有的段,反汇编完成以后就会在当前目录下出现一个名为 led.dis 文件。
总结一下我们为了编译 ARM 开发板上运行的 led.o 这个文件使用了如下命令:
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
代码烧写
I.MX6U 支持从外置的 NOR Flash、NAND Flash、SD/EMMC、SPI NOR Flash和 QSPI Flash 这些存储介质中启动,所以我们可以将代码烧写到这些存储介质中中。将前面编译出来的 led.bin 烧写到 SD 卡中,烧写完成以后会在当前工程目录下生成一个 load.imx 的文件,load.imx 这个文件就是软件 imxdownload 根据 NXP 官方启动方式介绍的内容,在 led.bin 文件前面添加了一些数据头以后生成的。最终烧写到 SD 卡里面的就是这个 load.imx 文件,而非led.bin。
汇编部分实验
就几行代码,用来设置处理器运行到 SVC 模式下、然后初始化 SP 指针、最终跳转到 C 文件的 main 函数中。没有初始化 DDR3 的代码,但是却将 SVC 模式下的 SP 指针设置到了 DDR3 的地址范围中,这不会出问题吗?肯定不会的,DDR3 肯定是要初始化的,但是不需要在 start.s 文件中完成。DCD 数据包含了 DDR 配置参数,I.MX6U 内部的 Boot ROM 会读取 DCD 数据中的 DDR 配置参数然后完成 DDR 初始化的。
链接脚本
arm-linux-gnueabihf-ld -Ttext 0X87800000 -o ledc.elf $^
上面语句中我们是通过“-Ttext”来指定链接地址是0X87800000 的,这样的话所有的文件都会链接到以 0X87800000 为起始地址的区域。但是有时候我们很多文件需要链接到指定的区域,或者叫做段里面,比如在 Linux 里面初始化函数就会放到 init 段里面。因此我们需要能够自定义一些段,这些段的起始地址我们可以自由指定,同样的我们也可以指定一个文件或者函数应该存放到哪个段里面去。要完成这个功能我们就需要使用到链接脚本。其主要目的是描述输入文件中的段如何被映射到输出文件中,并且控制输出文件中的内存排布。。我们一般编译出来的代码都包含在 text、data、bss 和 rodata 这四个段内,假设现在的代码要被链接到 0X10000000 这个地址,数据要被链接到 0X30000000 这个地方,下面就是完成此功能的最简单的链接脚本:
1 SECTIONS{
2 . = 0X10000000;
3 .text : {*(.text)}
4 . = 0X30000000;
5 .data ALIGN(4) : { *(.data) }
6 .bss ALIGN(4) : { *(.bss) }
7 }
“.”在链接脚本里面叫做定位计数器,表示以 0X10000000 开始,后面的文件或者段都会以 0X10000000 为起始地址开始链接。
第 3 行的“.text”是段名,后面的冒号是语法要求,冒号后面的大括号里面可以填上要链接到“.text”这个段里面的所有文件。
第 4 行,我们的要求是数据放到 0X30000000 开始的地方,所以我们需要重新设置定位计数器“.”,将其改为 0X30000000。如果不重新设置的话会怎么样?假设“.text”段大小为 0X10000,那么接下来的.data 段开始地址就是 0X10000000+0X10000=0X10010000,这明显不符合我们的要求。
第 5 行跟第 3 行一样,定义了一个名为“.data”的段,然后所有文件的“.data”段都放到这里面。但是这一行多了一个“ALIGN(4)”,这是用来对“.data”这个段的起始地址做字节对齐的,ALIGN(4)表示 4 字节对齐。也就是说段“.data”的起始地址要能被 4 整除
第 6 行定义了一个“.bss”段,所有文件中的“.bss”数据都会被放到这个里面,“.bss”数据就是那些定义了但是没有被初始化的变量。
编写好了链接脚本文件:imx6ul.lds,我们肯定是要使用这个链接脚本文件的,
arm-linux-gnueabihf-ld -Timx6ul.lds -o ledc.elf $^
其实就是将-T 后面的 0X87800000 改为 imx6ul.lds,表示使用 imx6ul.lds 这个链接脚本文件。
模仿 STM32 驱动开发
属于一个外设的所有寄存器地址基本是相邻的
(有些会有保留寄存器)。因此我们可以借助 C 语言里面的结构体成员地址递增的特点来将某个外设的所有寄存器写入到一个结构体里面,然后定义一个结构体指针指向这个外设的寄存器基地址,这样我们就可以通过这个结构体指针来访问这个外设的所有寄存器。同理,I.MX6U 也可以使用这种方法来定义外设寄存器。
I.MX6U 寄存器定义
1.先将同属于一个外设的所有寄存器编写到一个结构体里面如 IO 复用寄存器组的结构体。,成员变量是每个 IO 对应的复用寄存器,每个寄存器的地址是 32 位,每个成员都使用“volatile”进行了修饰,目的是防止编译器优化。
2.定义 IO 复用寄存器组的基地址,根据结构体 IOMUX_SW_MUX_Type 的定义,其第一个成员变量为 BOOT_MODE0,也就是 BOOT_MODE0 这个 IO 的 IO 复用寄存器,查找 I.MX6U 的参考手册可以得知其地址为
0X020E0014,所以 IO 复用寄存器组的基地址就是 0X020E0014
3.定义访问指针
#define GPIOE ((GPIO_TypeDef *) GPIOE_BASE)“GPIOE”是个宏定义,是一个指向地址 GPIOE_BASE 的结构体指针,结构体为GPIO_TypeDef。GPIOE_BASE 的基地址为 0x40011800,宏 GPIOE 指向这个地址,因此 GPIOE 的寄存器CRL 的地址就是 0X40011800,寄存器 CRH 的地址就是 0X40011800+4=0X40011804,其他寄存
器地址以此类推。我们要操作 GPIOE 的 ODR 寄存器的话就可以通过“GPIOE->ODR”来实现,这个方法是借助了结构体成员地址连续递增的原理。
在编写寄存器组结构体的时候注意寄存器的地址是否连续,有些外设的寄存器地址可能不是连续的,会有一些保留地址,因此我们需要在结构体中留出这些保留的寄存器。