上篇文章(【i.MX6ULL】驅動開發3--GPIO寄存器配置原理),介紹了i.MX6ULL芯片的GPIO的工作原理與寄存器配置。
本篇,就要來實際操作一下GPIO,實現板子上LED燈的亮滅控制。
在介紹如何通過寄存器來控制LED之前,需要先來了解一下有關Linux地址映射相關的知識。
1 地址映射
Linux或是STM32,對于硬件的控制,本質都是操作寄存器,在對應的地址進行數據的讀寫。若是在裸機開發中,可以控制CPU直接操作寄存器的地址,實現相應的功能,其過程是這樣的:

linux環境,一般是不會直接訪問物理內存,因為如果用戶不小心修改了內存中的數據,很有可能造成錯誤甚至系統崩潰。為了避免這些問題,linux內核便引入了MMU和TLB進行內存地址映射,通過訪問虛擬地址實現對實際物理地址的讀寫:

1.1 MMU介紹
MMU,Memory Manage Unit,即內存管理單元,它提供統一的內存空間抽象,程序通過訪問虛擬內存中的地址,MMU將虛擬地址(Virtual Address)翻譯成實際的物理地址(Physical Address) ,之后CPU即可操作實際的物理地址。
MMU具有如下功能:
保護內存: MMU給一些指定的內存塊設置了讀、寫以及可執行的權限,這些權限存儲在頁表當中,MMU會檢查CPU當前所處的是特權模式還是用戶模式,只有和操作系統所設置的權限匹配才可以訪問。
提供方便統一的內存空間抽象,實現虛擬地址到物理地址的轉換:CPU可以運行在虛擬的內存當中,虛擬內存一般要比實際的物理內存大很多,使得CPU可以運行比較大的應用程序。
1.2 TLB介紹
TLB,Translation Lookaside Buffer,即轉譯后備緩沖器,也稱頁表緩存,里面存放的是一些頁表文件(虛擬地址到物理地址的轉換表),又稱為快表技術。
當CPU第一次查找一個虛擬地址時,硬件通過3級頁表(page table)得到最終的PPN(Physical Page Number),TLB會保存虛擬地址到物理地址的映射關系。這樣在下一次訪問同一個虛擬地址時,處理器通過查看TLB來直接返回物理地址,而不需要通過page table得到結果,從而提高地址轉換的效率。
1.3 I/O映射函數
Linux內核啟動的時候會初始化MMU,設置好內存映射,設置好以后CPU訪問的都是虛擬地址。
那在程序編寫的時候,如何進行物理內存和虛擬內存之間的轉換呢?這就需要用到兩個函數:ioremap和iounmap。
ioremap()
ioremap函數用將物理地址映射為虛擬地址。
#define ioremap(cookie,size) __arm_ioremap((cookie), (size), MT_DEVICE)
/**
* paddr: 被映射的 IO 起始地址(物理地址)
* size: 需要映射的空間大小,以字節為單位
* return: 一個指向__iomem類型的指針,映射成功后便返回一段虛擬地址空間的起始地址
*/
void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size, unsigned int mtype)
{
return arch_ioremap_caller(phys_addr, size, mtype, __builtin_return_address(0));
}
iounmap()
iounmap函數的作用是釋放掉ioremap函數所做的映射,即反向操作,在卸載驅動的時候需要調用。
/**
* addr: 要取消映射的虛擬地址空間首地址
* return: void
*/
void iounmap (volatile void __iomem *addr)
1.4 I/O內存訪問函數
在使用ioremap函數將物理地址轉換成虛擬地址之后,理論上我們便可以直接讀寫 I/O 內存,但是為了符合驅動的跨平臺以及可移植性,我們應該使用 linux 中指定的函數(如:iowrite8()、iowrite16()、iowrite32()、ioread8()、ioread16()、ioread32() 等)去讀寫 I/O 內存,而非直接通過映射后的指向虛擬地址的指針進行訪問。讀寫 I/O 內存的函數如下:
unsigned int ioread8(void __iomem *addr); /*讀取一個字節*/
unsigned int ioread16(void __iomem *addr); /*讀取一個字*/
unsigned int ioread32(void __iomem *addr); /*讀取一個雙字*/
void iowrite8(u8 b, void __iomem *addr); /*寫入一個字節*/
void iowrite16(u16 b, void __iomem *addr); /*寫入一個字*/
void iowrite32(u32 b, void __iomem *addr); /*寫入一個雙字*/
對于讀I/O而言,他們都只有一個 __iomem 類型指針的參數,指向被映射后的地址,返回值為讀取到的數據;
對于寫I/O而言他們都有兩個參數,第一個為要寫入的數據,第二個參數為要寫入的地址,返回值為空。
與這些函數相似的還有writeb、writew、writel、readb、readw、readl 等
u8 readb(const volatile void __iomem *addr);
u16 readw(const volatile void __iomem *addr);
u32 readl(const volatile void __iomem *addr);
void writeb(u8 value, volatile void __iomem *addr);
void writew(u16 value, volatile void __iomem *addr);
void writel(u32 value, volatile void __iomem *addr);
在 ARM 架構下,writex(readx)函數與 iowritex(ioreadx)有一些區別,writex(readx)不進行端序的檢查,而 iowritex(ioreadx)會進行端序的檢查。
2 程序編寫
2.1 LED驅動程序
led驅動也是屬于字符設備驅動的,之前介紹了新舊兩種字符驅動的寫法,本篇led驅動就按照新字符設置驅動的寫法來編寫。
關于新字符設備的驅動模塊,可參考之前的文章:【i.MX6ULL】驅動開發2--新字符設備開發模板
這里再放一張新字符設備開發的模板框架

2.1.1 字符設備的基本框架
//字符設備結構體
struct newchrled_dev{
dev_t devid; /* 設備號 */
struct cdev cdev; /* cdev */
struct class *class; /* 類 */
struct device *device; /* 設備 */
int major; /* 主設備號 */
int minor; /* 次設備號 */
};
struct newchrled_dev chrdevled; /* led設備 */
//打開 讀取 寫入 釋放, 基礎文件操作函數
static int chrdevled_open(struct inode *inode, struct file *filp)
{
/*設置chrdevled為私有數據*/
return 0;
}
static ssize_t chrdevled_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
static ssize_t chrdevled_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
static int chrdevled_release(struct inode *inode, struct file *filp)
{
return 0;
}
//設備操作函數結構體
static struct file_operations chrdevled_fops = {
.owner = THIS_MODULE,
.open = chrdevled_open,
.read = chrdevled_read,
.write = chrdevled_write,
.release = chrdevled_release,
};
//驅動入口函數
static int __init chrdevled_init(void)
{
/* 初始化LED */
/* 注冊字符設備驅動(操作chrdevled_fops) */
return 0;
}
//驅動出口函數
static void __exit chrdevled_exit(void)
{
/* 取消IO映射 */
/* 注銷字符設備驅動 */
}
//驅動的入口和出口函數
module_init(chrdevled_init);
module_exit(chrdevled_exit);
//LICENSE和作者信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xxpcb");
2.1.2 具體完善
1)GPIO寄存器宏定義
需要配置相關的寄存器,就要對照著LED這個GPIO的硬件按需配置。
有關GPIO的各種寄存器的使用原理介紹,請參考上篇文章的介紹。

/* 寄存器物理地址 */
#define CCM_CCGR1_BASE (0X020C406C)
#define SW_MUX_SNVS_TAMPER3_BASE (0X02290014)
#define SW_PAD_SNVS_TAMPER3_BASE (0X02290058)
#define GPIO5_DR_BASE (0X020AC000)
#define GPIO5_GDIR_BASE (0X020AC004)
/* 映射后的寄存器虛擬地址指針 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_SNVS_TAMPER3;
static void __iomem *SW_PAD_SNVS_TAMPER3;
static void __iomem *GPIO5_DR;
static void __iomem *GPIO5_GDIR;
CCM 是用來進行時鐘的使能,其寄存器包括CCGR0~CCGR6,因為LED用到GPIO屬于GPIO5,它對應的時鐘配置寄存器就是CCM_CCGR1
MUX 是用來將IO復用為GPIO
PAD 是用來配置IO的基本參數(驅動能力、壓擺率、上下拉等)
GPIO5_DR 數據寄存器,當GPIO為輸出模式時,用來設置對應的高低電平
GPIO5_GDIR 方向寄存器,用來設置輸入還是輸出
以上是先對這些需要使用的寄存器的地址聲明宏定義(這些寄存器的地址可通過查閱i.MX6ULL數據手冊得到),然后再聲明對應的虛擬地址的指針,因為Linux開始MMU后,就不能直接對寄存器的地址直接操作了,需要使用映射后的虛擬地址。
2)GPIO硬件初始化
主要包括以下幾步:
寄存器地址映射:將需要用的寄存器的物理地址映射為虛擬地址
使能GPIO1時鐘:就是配置CCM_CCGR1寄存器
設置GPIO5_IO03的復用功能:配置MUX和PAD寄存器
設置GPIO5_IO03為輸出功能:配置GPIO5_GDIR方向寄存器
初始默認關閉LED:配置GPIO5_DR數據寄存器
具體配置過程如下,主要這里使用"與"和"或"的位運算操作,來配置寄存器中對應位的值。
static void led_hardware_init(void)
{
u32 val = 0;
/* 1、寄存器地址映射 */
IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
SW_MUX_SNVS_TAMPER3 = ioremap(SW_MUX_SNVS_TAMPER3_BASE, 4);
SW_PAD_SNVS_TAMPER3 = ioremap(SW_PAD_SNVS_TAMPER3_BASE, 4);
GPIO5_DR = ioremap(GPIO5_DR_BASE, 4);
GPIO5_GDIR = ioremap(GPIO5_GDIR_BASE, 4);
/* 2、使能GPIO1時鐘 */
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26); /* 清除以前的設置 */
val |= (3 << 26); /* 設置新值 */
writel(val, IMX6U_CCM_CCGR1);
/* 3、設置GPIO5_IO03的復用功能,將其復用為GPIO5_IO03,最后設置IO屬性 */
writel(5, SW_MUX_SNVS_TAMPER3);
/*寄存器SW_PAD_SNVS_TAMPER3設置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 低轉換率
*/
writel(0x10B0, SW_PAD_SNVS_TAMPER3);
/* 4、設置GPIO5_IO03為輸出功能 */
val = readl(GPIO5_GDIR);
val &= ~(1 << 3); /* 清除以前的設置 */
val |= (1 << 3); /* 設置為輸出 */
writel(val, GPIO5_GDIR);
/* 5、默認關閉LED */
val = readl(GPIO5_DR);
val |= (1 << 3);
writel(val, GPIO5_DR);
}
3)字符設備初始化
需要定義led字符設備結構體,來管理這個led設備。
/*newchr設備結構體 */
struct newchrled_dev{
dev_t devid; /* 設備號 */
struct cdev cdev; /* cdev */
struct class *class; /* 類 */
struct device *device; /* 設備 */
int major; /* 主設備號 */
int minor; /* 次設備號 */
};
struct newchrled_dev chrdevled; /* led設備 */
具體的led字符設備初始化流程:
初始化LED的GPIO(上面剛介紹)
創建設備號
初始化cdev字符設備
添加cdev字符設備
創建類
創建設備
static int __init chrdevled_init(void)
{
/* 初始化LED */
led_hardware_init();
/* 注冊字符設備驅動 */
/* 1、創建設備號 */
if (chrdevled.major) /* 定義了設備號 */
{
chrdevled.devid = MKDEV(chrdevled.major, 0);
register_chrdev_region(chrdevled.devid, chrdevled_CNT, chrdevled_NAME);
}
else /* 沒有定義設備號 */
{
alloc_chrdev_region(&chrdevled.devid, 0, chrdevled_CNT, chrdevled_NAME); /* 申請設備號 */
chrdevled.major = MAJOR(chrdevled.devid); /* 獲取分配號的主設備號 */
chrdevled.minor = MINOR(chrdevled.devid); /* 獲取分配號的次設備號 */
}
printk("chrdevled major=%d,minor=%d\n",chrdevled.major, chrdevled.minor);
/* 2、初始化cdev */
chrdevled.cdev.owner = THIS_MODULE;
cdev_init(&chrdevled.cdev, &chrdevled_fops);
/* 3、添加一個cdev */
cdev_add(&chrdevled.cdev, chrdevled.devid, chrdevled_CNT);
/* 4、創建類 */
chrdevled.class = class_create(THIS_MODULE, chrdevled_NAME);
if (IS_ERR(chrdevled.class))
{
return PTR_ERR(chrdevled.class);
}
/* 5、創建設備 */
chrdevled.device = device_create(chrdevled.class, NULL, chrdevled.devid, NULL, chrdevled_NAME);
if (IS_ERR(chrdevled.device))
{
return PTR_ERR(chrdevled.device);
}
printk("chrdevled init done!\n");
return 0;
}
4)LED亮滅控制
驅動程序中,對于LED的控制,可以分為兩步。
第一步是接收和解析應用層發來的控制數據(0或1來控制亮滅),將控制參數傳遞給具體的開關led的函數:
static ssize_t chrdevled_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
unsigned char databuf[1];
unsigned char ledstat;
/* 接收用戶空間傳遞給內核的數據并且打印出來 */
if(0 != copy_from_user(databuf, buf, cnt))
{
printk("kernel recevdata failed!\n");
return -EFAULT;
}
ledstat = databuf[0]; /* 獲取狀態值 */
if(ledstat == LEDON)
{
led_switch(LEDON); /* 打開LED燈 */
printk("led on!\n");
}
else if(ledstat == LEDOFF)
{
led_switch(LEDOFF); /* 關閉LED燈 */
printk("led off!\n");
}
return 0;
}
第二步就是根據指令參數,通過控制數據寄存器GPIO5_DR來實現GPIO的高低電平輸出,從而實現LED的亮滅:
void led_switch(u8 sta)
{
u32 val = 0;
if(sta == LEDON)
{
val = readl(GPIO5_DR);
val &= ~(1 << 3);
writel(val, GPIO5_DR);
}
else if(sta == LEDOFF)
{
val = readl(GPIO5_DR);
val|= (1 << 3);
writel(val, GPIO5_DR);
}
}
5)驅動退出
驅動不再使用時,需要注銷相關的設備:
static void led_hardware_exit(void)
{
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_SNVS_TAMPER3);
iounmap(SW_PAD_SNVS_TAMPER3);
iounmap(GPIO5_DR);
iounmap(GPIO5_GDIR);
}
首先釋放掉這些地址映射:
static void __exit chrdevled_exit(void)
{
/* 取消IO映射 */
led_hardware_exit();
/* 注銷字符設備驅動 */
cdev_del(&chrdevled.cdev);/* 刪除cdev */
unregister_chrdev_region(chrdevled.devid, chrdevled_CNT); /* 注銷設備號 */
device_destroy(chrdevled.class, chrdevled.devid);
class_destroy(chrdevled.class);
printk("chrdevled exit done!\n");
}
驅動程序基本就是這些,完整的程序見我的gitee倉庫:https://gitee.com/xxpcb/imx6ull
2.2 LED應用程序
寫完了驅動程序(BSP),還要寫對應的應用程序(APP)。
目前的應用程序比較簡短,因為在Linux中,一切皆文件,所以,對于LED的控制,就是通過向文件中寫入0或1來實現LED的亮滅。
先來對0和1進行宏定義:
#define LEDOFF 0 /*長滅*/
#define LEDON 1 /*長亮*/
然后就是main函數了:
int main(int argc, char *argv[])
{
int fd, retvalue;
char *filename;
unsigned char databuf[1];
if(argc != 3)
{
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
/* 打開led驅動文件 */
fd = open(filename, O_RDWR);
if(fd < 0)
{
printf("Can't open file %s\r\n", filename);
return -1;
}
/* 要執行的操作:打開或關閉 */
databuf[0] = atoi(argv[2]);
/* 向設備驅動(/dev/chrdevled)寫數據 */
retvalue = write(fd, databuf, sizeof(databuf));
if(retvalue < 0)
{
printf("write file %s failed!\r\n", filename);
close(fd);
return -1;
}
/* 關閉設備 */
retvalue = close(fd);
if(retvalue < 0)
{
printf("Can't close file %s\r\n", filename);
return -1;
}
return 0;
}
3 實驗測試
3.1 程序編譯與下載
再來復習一下基本步驟:
ubuntu中通過gcc交叉編譯器編譯出led的驅動程序和應用程序
搭建局域網環境(電腦和linux板子連接到同一個路由器下,Linux板子以及燒錄了鏡像文件,能夠正常運行)
通過tftp服務將兩個文件發送到linux板子的對應目錄中(/lib/modules/4.1.15目錄)
進行字符設備的加載,以及文件讀寫測試(控制led亮滅)

程序的具體編譯過程與之前的類似,這里不再贅述,可參考之前的文章(如這篇:【i.MX6ULL】驅動開發2--新字符設備開發模板)
3.2 實驗現象
首先來看一下板子上LED的位置,如下圖的電路上的標號D14處:

然后在串口中,按照之前介紹字符設備的加載流程,先加載led字符設備,然后就可以下向應用程序寫1或0來控制led的亮滅了。

led點亮的效果如下:

4 總結
本篇主要介紹了如何通過操作寄存器來點亮i.MX6ULL開發板上的led,通過編寫LED對應的驅動程序和應用程序,實現程序設計的分層。
因為Linux使用了MMU進行虛擬地址管理,因此在操作寄存器時,要進行地址映射后再操作。最后通過程序的實際測試,驗證了led的亮滅功能。
審核編輯:符乾江
-
嵌入式
+關注
關注
5103文章
19268瀏覽量
310017 -
驅動
+關注
關注
12文章
1866瀏覽量
85934 -
Linux
+關注
關注
87文章
11373瀏覽量
211293
發布評論請先 登錄
相關推薦
i.MX6ULL嵌入式Linux開發1-uboot移植初探

使用i.MX6ULL開發板進行Linux根文件系統的完善
I.MX6ULL終結者開發板裸機仿真jlink調試
i.MX6ULL開發板硬件資源
關于i.MX6ULL配置GPIO
飛凌i.MX6ULL開發板的評測,再次進階擁有更高的性價比

基于NXP i.MX6ULL處理器的FETMX6ULL-C核心板

基于i.MX6ULL點亮LED
使用pinctrl和gpio子系統實現LED燈驅動
基于i.MX6ULL的掉電檢測設計與軟件測試

評論