Linux入门第二部分(ARM裸机篇) 第六章 驱动开发
参考教程:
正点原子【第二期】手把手教你学Linux之ARM(MX6U)裸机篇_哔哩哔哩_bilibili
一、I.MX6U寄存器定义与封装
1、外设寄存器组结构体
(1)需要说明的是,驱动配置文件一般没有固定的写法,但是会有相对比较高效的写法是值得参考的,本章介绍的驱动配置文件写法主要参考STM32标准库。
(2)一个外设基本上都会牵扯若干寄存器,可以先将同属于一个外设的所有寄存器编写到一个结构体里面,如下所示,IOMUX_SW_MUX_Type就是IO复用寄存器组,成员变量是每个IO对应的复用寄存器,每个寄存器的地址是32位,每个成员都使用“volatile”进行修饰,目的是防止编译器优化。
// IOMUX 寄存器组 typedef struct{ volatile unsigned int BOOT_MODE0; volatile unsigned int BOOT_MODE1; volatile unsigned int SNVS_TAMPER0; volatile unsigned int SNVS_TAMPER1; // ……… volatile unsigned int CSI_DATA00; volatile unsigned int CSI_DATA01; volatile unsigned int CSI_DATA02; volatile unsigned int CSI_DATA03; volatile unsigned int CSI_DATA04; volatile unsigned int CSI_DATA05; volatile unsigned int CSI_DATA06; volatile unsigned int CSI_DATA07; /* 为了缩短代码,其余 IO 复用寄存器省略 */ }IOMUX_SW_MUX_Tpye;2、定义IO复用寄存器组的基地址
(1)一个外设的寄存器组,各寄存器的地址往往是连续的(不一定全部都是连续的,只是绝大部分),可以将第一个寄存器的绝对地址作为基地址,后续的寄存器,只需要记相对于基地址的偏移地址即可,这样,只需要知道基地址和每个寄存器的偏移地址,就能得到每个寄存器的绝对地址。
(2)在定义外设寄存器组结构体时,成员变量的摆放顺序也要与寄存器在存储空间中的排布顺序一致(如果寄存地的排布不连续,中间有空位,那么定义结构体时也要定义占位预留的成员变量),这样,成员变量的偏移地址,就是寄存器相对于寄存器组基地址的偏移地址。
(3)根据上一小节中结构体IOMUX_SW_MUX_Type的定义,其第一个成员变量为BOOT_MODE0,也就是BOOT_MODE0这个IO的IO复用寄存器,查阅I.MX6U的参考手册可以得知其地址为0x020E0014,所以IO复用寄存器组的基地址就是0x020E0014,定义如下:
#define IOMUX_SW_MUX_BASE (0x020E0014)3、定义寄存器组访问指针
(1)如上小节所述,寄存器相对于寄存器组基地址的偏移地址,就是寄存器组结构体中成员变量的偏移地址,也就是说,实际物理存储空间中地址连续的寄存器,存储结构与所定义的寄存器组结构体一致,那么C代码就可以按照访问结构体成员的方式,直接访问这些寄存器。
(2)如果将实际物理存储空间中地址连续的寄存器抽象为所定义的寄存器组结构体,那么寄存器组结构体的首成员地址就是寄存器组的基地址,因此需要定义访问指针,比如:
#define IOMUX_SW_MUX ((IOMUX_SW_MUX_Type *)IOMUX_SW_MUX_BASE)(3)定义访问指针后,这个指针指向实际物理存储空间中的寄存器组结构体,C代码可以通过该指针直接访问寄存器组的寄存器,比如“IOMUX_SW_MUX->BOOT_MODE1”。
二、程序编写与验证
1、启动配置文件编写
(1)在Board_Drivers文件夹中新建文件夹“3_ledc_plus”,然后在该文件夹中新建start.s文件,I.MX启动配置(构建C语言运行环境)的汇编代码将写在该文件中。
(2)在start.s文件中写入如下内容(基于上一章的start.s,增加了清除bss段的操作):
.global _start .global _bss_start _bss_start: .word __bss_start @ 存放bss段起始地址(__bss_start 在lds链接脚本定义) .global _bss_end _bss_end: .word __bss_end @ 存放bss段结束地址(__bss_end 在lds链接脚本定义) @ _start 函数,程序从此函数开始执行,此函数主要功能是设置C运行环境 _start: @ 进入 SVC 模式 mrs r0, cpsr bic r0, r0, #0x1f @ 将r0的低5位清零,也就是CPSR的M0~M4 orr r0, r0, #0x13 @ 将r0或上0x13(仅修改M[4:0]),表示使用SVC模式 msr cpsr, r0 @ 将r0的数据写入CPSR中 @ 清除 bss 段 ldr r0, _bss_start @ r0 = bss段起始地址 ldr r1, _bss_end @ r1 = bss段结束地址 mov r2, #0 @ r2 = 清零填充值0 bss_loop: stmia r0!, {r2} @ 把r2的值写入r0指向内存,写完r0自增(4字节) cmp r0, r1 @ 比较当前地址是否到达bss末尾(该指令会操作CSPR的Z) ble bss_loop @ 没到末尾就继续循环(根据CSPR的Z判断CMP比较结果) ldr sp, =0X80200000 @ 设置栈指针 b main @ 跳转到main函数2、外设配置文件编写
(1)在文件夹3_ledc_plus中继续新建imx6ul.h文件,外设配置的定义写在imx6ul.h中。
(2)在imx6ul.h文件中写入如下内容:
①防止重复编译框架:
#ifndef __IMX6UL_H #define __IMX6UL_H #endif②外设寄存器组基地址定义:
// 外设寄存器组的基地址 #define CCM_BASE (0X020C4000) #define CCM_ANALOG_BASE (0X020C8000) #define IOMUX_SW_MUX_BASE (0X020E0044) #define IOMUX_SW_PAD_BASE (0X020E0204) #define GPIO1_BASE (0x0209C000) #define GPIO2_BASE (0x020A0000) #define GPIO3_BASE (0x020A4000) #define GPIO4_BASE (0x020A8000) #define GPIO5_BASE (0x020AC000)③CCM寄存器结构体定义:
typedef struct { volatile unsigned int CCR; volatile unsigned int CCDR; volatile unsigned int CSR; // ......省略中间寄存器(需要自行补全,否则可能无法正常工作) volatile unsigned int CCGR6; volatile unsigned int RESERVED_3[1]; // 预留占位 volatile unsigned int CMEOR; } CCM_Type; typedef struct { volatile unsigned int PLL_ARM; volatile unsigned int PLL_ARM_SET; volatile unsigned int PLL_ARM_CLR; volatile unsigned int PLL_ARM_TOG; // ......省略中间寄存器 volatile unsigned int MISC2; volatile unsigned int MISC2_SET; volatile unsigned int MISC2_CLR; volatile unsigned int MISC2_TOG; } CCM_ANALOG_Type;④IOMUX寄存器结构体定义:
typedef struct { volatile unsigned int JTAG_MOD; volatile unsigned int JTAG_TMS; volatile unsigned int JTAG_TDO; // ......省略中间寄存器(需要自行补全,否则可能无法正常工作) volatile unsigned int CSI_DATA04; volatile unsigned int CSI_DATA05; volatile unsigned int CSI_DATA06; volatile unsigned int CSI_DATA07; } IOMUX_SW_MUX_Type; typedef struct { volatile unsigned int DRAM_ADDR00; volatile unsigned int DRAM_ADDR01; // ......省略中间寄存器(需要自行补全,否则可能无法正常工作) volatile unsigned int GRP_DDRPKE; volatile unsigned int GRP_DDRMODE; volatile unsigned int GRP_DDR_TYPE; } IOMUX_SW_PAD_Type;⑤GPIO寄存器结构体定义:
typedef struct { volatile unsigned int DR; // GPIO数据寄存器 volatile unsigned int GDIR; // GPIO方向寄存器 volatile unsigned int PSR; // GPIO状态寄存器 volatile unsigned int ICR1; // 中断配置寄存器1 volatile unsigned int ICR2; // 中断配置寄存器2 volatile unsigned int IMR; // 中断屏蔽寄存器 volatile unsigned int ISR; // 中断状态寄存器 volatile unsigned int EDGE_SEL; // 边沿选择寄存器 } GPIO_Type;⑥寄存器组访问指针定义:
// 外设指针 #define CCM ((CCM_Type *)CCM_BASE) #define CCM_ANALOG ((CCM_ANALOG_Type *)CCM_ANALOG_BASE) #define IOMUX_SW_MUX ((IOMUX_SW_MUX_Type *)IOMUX_SW_MUX_BASE) #define IOMUX_SW_PAD ((IOMUX_SW_PAD_Type *)IOMUX_SW_PAD_BASE) #define GPIO1 ((GPIO_Type *)GPIO1_BASE) #define GPIO2 ((GPIO_Type *)GPIO2_BASE) #define GPIO3 ((GPIO_Type *)GPIO3_BASE) #define GPIO4 ((GPIO_Type *)GPIO4_BASE) #define GPIO5 ((GPIO_Type *)GPIO5_BASE)3、C语言点灯程序编写
(1)在文件夹3_ledc_plus中继续新建main.c文件,将点灯的业务代码和LED外设初始化的代码写在main.c文件中。
(2)在main.c文件中写入如下内容:
①先将程序基本框架搭起来,如下所示:
#include "imx6ul.h" int main(void) { return 0; }②添加外设时钟初始化函数,如下所示:
/* * @description : 使能 I.MX6U 所有外设时钟 * @param : 无 * @return : 无 */ 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; }③添加LED对应GPIO的初始化函数,如下所示:
/* * @description : 初始化 LED 对应的 GPIO * @param : 无 * @return : 无 */ void led_init(void){ /* 1、初始化 IO 复用, 复用为 GPIO1_IO03 */ IOMUX_SW_MUX->GPIO1_IO03 = 0X5; /* 2、配置 GPIO1_IO03 的 IO 属性 *bit 16:0 HYS 关闭 *bit [15:14]: 00 默认下拉 *bit [13]: 0 kepper 功能 *bit [12]: 1 pull/keeper 使能 *bit [11]: 0 关闭开路输出 *bit [7:6]: 10 速度 100Mhz *bit [5:3]: 110 R0/6 驱动能力 *bit [0]: 0 低转换率 */ IOMUX_SW_PAD->GPIO1_IO03 = 0X10B0; /* 3、初始化 GPIO, GPIO1_IO03 设置为输出 */ GPIO1->GDIR = 0X0000008; /* 4、设置 GPIO1_IO03 输出低电平,打开 LED0 */ GPIO1->DR &= ~(1 << 3); }④添加LED灯开启和关闭函数,如下所示:
/* * @description : 打开 LED 灯 * @param : 无 * @return : 无 */ void led_on(void){ // 将 GPIO1_DR 的 bit3 清零 GPIO1->DR &= ~(1<<3); } /* * @description : 关闭 LED 灯 * @param : 无 * @return : 无 */ void led_off(void){ // 将 GPIO1_DR 的 bit3 置 1 GPIO1->DR |= (1<<3); }⑤添加延时函数,如下所示:
/* * @description : 短时间延时函数 * @param - n : 要延时循环次数(空操作循环次数,模式延时) * @return : 无 */ void delay_short(volatile unsigned int n){ while(n--){} } /* * @description : 延时函数,在 396Mhz 的主频下延时时间大约为 1ms * @param - n : 要延时的 ms 数 * @return : 无 */ void delay(volatile unsigned int n){ while(n--) { delay_short(0x7ff); } }⑥在main函数中完成LED灯闪烁的业务逻辑,如下所示:
int main(void) { clk_enable(); // 使能所有的时钟 led_init(); // 初始化 LED while(1) { led_off(); // 关闭 LED delay(500); // 延时大约 500ms led_on(); // 打开 LED delay(500); // 延时大约 500ms } return 0; }4、Makefile文件与链接脚本编写
(1)由于本章例程的架构与上一章类似,故可直接复用上一章的Makefile,将其复制到本工程的根目录下即可。
(2)除了Makefile以外,链接脚本也可直接复用,将上一章的imx6u.lds复制到本工程的根目录下即可。
5、程序烧写验证
(1)将imxdownload拷贝到工程的根目录下,也就是和ledc.bin处于同一个文件夹下,并赋予其可执行权限。
(2)将SD卡接入虚拟机,然后在当前目录运行命令“./imxdownload ledc.bin /dev/sdb”即可将固件刷入SD卡(设备名需要核对,与/dev下的设备名一致),然后在开发板上验证即可。