「野火i.MX6ULL 開發板連載」GPIO開發之點燈

本文是在字元驅動的基礎上,再進行的GPIO控制,所以本篇文章不再詳細敘述申請及釋放裝置號、 新增以及登出裝置,初始化、新增與刪除cdev結構體等操作。本篇的主要目的是控制GPIO,原始碼全部貼出來。

裸機開發與linux驅動開發區別

在裸機操作下,裸機驅動一般針對沒有作業系統支援的層面,不用考慮作業系統對它的呼叫。

Linux驅動是在裸機驅動基礎上,按照一定的規範來實現, 雖然實現的都是同一個東西,不過你發現在 Linux驅動攙雜了許多維護資訊。Linux裝置驅動就是比裸機驅動多了一些框架。

在裸機方式下,ARM的軟體整合開發環境就顯得極為重要,因為在這種方式下可以把所有程式碼都放在這個環境裡面編寫、編譯和除錯。在這種方式下測試驅動程式,首先要完成CPU的初始化,然後把需要測試的程式裝載到系統的RAM區/或者SDRAM中。當然,如果需要處理一些複雜的中斷處理的話,最好也把CPU的復位向量表放到RAM區中。把所有程式都除錯好之後,再把最後的程式燒寫到Flash裡面去執行。

有linux作業系統的存在大大降低了應用軟體與硬體平臺的耦合度,它充當了我們硬體與應用軟體之間的紐帶, 使得應用軟體只需要呼叫驅動程式介面API就可以讓硬體去完成要求的開發,而應用軟體則不需要關心硬體到底是如何工作的。 這將大大提高我們應用程式的可移植性和開發效率。

作業系統能夠帶來多工併發機制。

傳說中的MMU

說到MMU,大家都不陌生,相對於計算機來說,這個東西就是我們說的記憶體,在linux環境直接訪問物理記憶體是很危險的,如果使用者不小心修改了記憶體中的資料,很有可能造成錯誤甚至系統崩潰。

MMU是 MemoryManagementUnit 的縮寫即,記憶體管理單元。針對各種CPU, MMU是個可選的配件。MMU負責的是虛擬地址與物理地址的轉換。提供硬體機制的記憶體訪問授權。

MMU 的作用:

1。 將虛擬地址翻譯成為物理地址,然後訪問實際的物理地址;2。 訪問許可權控制。

當沒有啟用MMU的時候,CPU在讀取指令或者訪問記憶體時便會將地址直接輸出到晶片的引腳上,此地址直接被記憶體接收,這段地址稱為物理地址。當CPU開啟了MMU時,CPU發出的地址將被送入到MMU,被送入到MMU的這段地址稱為虛擬地址, 之後MMU會根據去訪問頁表地址暫存器然後去記憶體中找到頁表(假設只有一級頁表)的條目,從而翻譯出實際的物理地址。

地址轉換

裝置通常會提供一組暫存器來用於控制裝置、讀寫裝置和獲取裝置狀態,即控制暫存器、資料暫存器和狀態暫存器。這些暫存器可能位於 I/O 空間,也可能位於記憶體空間。當位於 I/O 空間時,通常被稱為 I/O 埠,位於記憶體空間時,對應的記憶體空間被稱為 I/O 記憶體。

記憶體管理單元(MMU)通常以頁為單位進行處理,而不是位元組,ioremap函式也同樣屬於頁對映。

ioremap()函式

在核心中訪問 I/O 記憶體之前,需首先使用 ioremap()函式將裝置所處的物理地址對映到虛擬地址。ioremap()的原型如下:

void *ioremap(unsigned long offset, unsigned long size);

引數:1。物理地址2。要對映的空間的大小返回值:頁對映,返回虛擬地址。

同一物理地址可以多次ioremap對映,分配的虛擬空間地址各部相同,iounmap互不影響。

訪問I/O記憶體的正確方式是透過一系列專用於此目的的函式(在中定義的):

unsigned int ioread8(void*addr); unsigned int ioread16(void*addr);unsigned int ioread32(void*addr);

/@@*addr是從ioremap獲得的地址(可能包含一個整型偏移量),返回值是從給定I/O記憶體讀取的值*//@@*對應的I/O記憶體寫函式*/void iowrite8(u8value,void*addr); void iowrite16(u16value,void*addr);void iowrite32(u32value,void*addr);

/@@*讀和寫一系列值到一個給定的I/O記憶體地址,從給定的buf讀或寫count個值到給定的addr*/void ioread8_rep(void *addr, void *buf, unsigned long count); void ioread16_rep(void *addr, void *buf, unsigned long count); void ioread32_rep(void *addr, void *buf, unsigned long count); void iowrite8_rep(void *addr, const void *buf, unsigned long count); void iowrite16_rep(void *addr, const void *buf, unsigned long count); void iowrite32_rep(void *addr, const void *buf,unsigned long count);

注意,在linux下有以下舊的介面,雖然仍然能夠使用,但是不建議用,比如如下api:

unsigned readb(address); unsigned readw(address); unsigned readl(address); void writeb(unsigned value,address); void writew(unsigned value,address); void writel(unsigned value,address);

iounmap函式

iounmap函式用於取消ioremap()所做的對映,原型如下:

void iounmap(void *addr)

函式引數和返回值如下:

void iounmap(void *addr)

引數:addr: 需要取消ioremap對映之後的起始地址(虛擬地址)。返回值: 無

技術手冊資料

本次使用開發板上的三色燈進行控制。

硬體電路

查閱硬體電路圖,電路圖如下

「野火i.MX6ULL 開發板連載」GPIO開發之點燈

「野火i.MX6ULL 開發板連載」GPIO開發之點燈

「野火i.MX6ULL 開發板連載」GPIO開發之點燈

對GPIO操作來說,主要分為如下幾個步驟:

1。 使能GPIO時鐘2。 設定引腳功能複用為GPIO3。 設定引腳的上下拉,速率,驅動能力等4。 控制GPIO引腳的輸出電平,高或者低

暫存器地址

這裡就不詳細敘述如何查閱資料尋找暫存器地址了,下面直接給出暫存器地址:

使能GPIO時鐘,暫存器地址0x20C406C,該暫存器的27-26位設定如下可獲取不同屬性:

00:所有模式下都關閉外設時鐘

01:只有在執行模式下開啟外設時鐘

10:保留

11:除了停止模式以外,該外設時鐘全程使能

程式碼中該部分實現如下圖

「野火i.MX6ULL 開發板連載」GPIO開發之點燈

設定引腳功能複用為GPIO,暫存器地址0x20E006C,關於該複用的功能如下圖

「野火i.MX6ULL 開發板連載」GPIO開發之點燈

由此可見,需要配置成GPIO,需要設定模式ALT5。該部分程式碼實現如下

「野火i.MX6ULL 開發板連載」GPIO開發之點燈

設定引腳的上下拉,速率,驅動能力等,暫存器地址0x20E02F8,該部分功能較多

HYS(bit16):用來使能遲滯比較器 。

PUS(bit15-bit14):用來設定上下拉電阻大小。

PUE(bit13):當 IO 作為輸入的時候,這個位用來設定 IO 使用上下拉還是狀態保持器。

PKE(bit12):用來使能或者禁止上下拉/狀態保持器功能。

ODE(bit11):IO 作為輸出的時候,此位用來禁止或者使能開漏輸出。

SPEED(bit7-bit6):當 IO 用作輸出的時候,此位用來設定 IO 速度。

DSE(bit5-bit3):當 IO 用作輸出的時候用來設定 IO 的驅動能力。

SRE(bit0):設定壓擺率

我們對該暫存器寫入0x1F838。

程式碼如下

「野火i.MX6ULL 開發板連載」GPIO開發之點燈

控制GPIO引腳的輸出電平的暫存器地址為baseaddr+0x04,直接寫入0表示輸入,寫入1表示輸出。

「野火i.MX6ULL 開發板連載」GPIO開發之點燈

實際操作下,在/dev/下出現多個led裝置節點,如下圖

「野火i.MX6ULL 開發板連載」GPIO開發之點燈

執行後的效果如下圖

「野火i.MX6ULL 開發板連載」GPIO開發之點燈

程式碼

如下是led驅動程式碼

#include #include #include #include #include #include “linux/cdev。h”#include #include #include #include /@@*使能GPIO時鐘設定引腳複用為GPIO設定引腳屬性(上下拉、速率、驅動能力)控制GPIO引腳輸出高低電平GPIO1_IO04:0x020C406C,0x20E006C,0x020E02F8,0x0209C004GPIO4_IO20:GPIO4_IO19:*/#define BUFFSIZE 64#define DEV_CNT 3int major = 0;int minor = 0;dev_t devno = 0;char buf[BUFFSIZE] = {0};static struct class *led_class;struct led_chardev{ struct cdev dev; unsigned int __iomem *va_dr; unsigned int __iomem *va_gdir; unsigned int __iomem *va_iomuxc_mux; unsigned int __iomem *va_ccm_ccgrx; unsigned int __iomem *va_iomux_pad; unsigned long pa_dr; unsigned long pa_gdir; unsigned long pa_iomuxc_mux; unsigned long pa_ccm_ccgrx; unsigned long pa_iomux_pad; unsigned int led_pin; unsigned int clock_offset;};static struct led_chardev led_cdev[DEV_CNT] = { { 。pa_dr = 0x0209C000, 。pa_gdir = 0x0209C004,//輸出電平,-0:輸入 -1:輸出 。pa_iomuxc_mux =0x20E006C,//引腳複用GPIO 。pa_ccm_ccgrx = 0x20C406C,//gpio1時鐘 。pa_iomux_pad =0x20E02F8,//引腳屬性 。led_pin = 4, 。clock_offset = 26 }, { 。pa_dr = 0x20A8000, 。pa_gdir = 0x20A8004, 。pa_iomuxc_mux =0x20E01E0, 。pa_ccm_ccgrx = 0x20C4074, 。pa_iomux_pad =0x20E046C, 。led_pin = 20, 。clock_offset = 12 }, { 。pa_dr = 0x20A8000, 。pa_gdir = 0x20A8004, 。pa_iomuxc_mux =0x20E01DC, 。pa_ccm_ccgrx = 0x20C4074, 。pa_iomux_pad =0x20E0468, 。led_pin = 19, 。clock_offset = 12 },};ssize_t led_read(struct file *file, char __user *data, size_t length, loff_t *loff){ int ret = 0; printk(KERN_EMERG “[ KERN_EMERG ] led Module led_read\n”); for(ret=0;retprivate_data; val = ioread32(led_cdev->va_dr); if (ret == 0) val &= ~(0x01 << led_cdev->led_pin); else val |= (0x01 << led_cdev->led_pin); iowrite32(val, led_cdev->va_dr); *loff += tmp; return tmp;}int led_open(struct inode *inode, struct file *file){ unsigned int val = 0; printk(KERN_EMERG “[ KERN_EMERG ] led Module led_open\n”); struct led_chardev *led_cdv = (struct led_chardev*)container_of(inode->i_cdev,struct led_chardev,dev); file->private_data = led_cdev; /@@* 實現地址對映 */ led_cdev->va_dr = ioremap(led_cdev->pa_dr, 4);//資料暫存器對映,將led_cdev->va_dr指標指向對映後的虛擬地址起始處,這段地址大小為4個位元組 led_cdev->va_gdir = ioremap(led_cdev->pa_gdir, 4);//方向暫存器對映 led_cdev->va_iomuxc_mux = ioremap(led_cdev->pa_iomuxc_mux, 4);//埠複用功能暫存器對映 led_cdev->va_ccm_ccgrx = ioremap(led_cdev->pa_ccm_ccgrx, 4);//時鐘控制暫存器對映 led_cdev->va_iomux_pad = ioremap(led_cdev->pa_iomux_pad, 4);//電氣屬性配置暫存器對映 val = ioread32(led_cdev->va_ccm_ccgrx); val = val | (3 << led_cdev->clock_offset);//置位對應的時鐘位 iowrite32(val,led_cdev->va_ccm_ccgrx); iowrite32(5,led_cdev->va_iomuxc_mux);//複用為GPIO val = ioread32(led_cdev->va_gdir); val &= ~(1 << led_cdev->led_pin); val |= (1 << led_cdev->led_pin); iowrite32(val, led_cdev->va_gdir); //配置位輸出模式 val = ioread32(led_cdev->va_dr); val |= (0x01 << led_cdev->led_pin); iowrite32(val, led_cdev->va_dr); //輸出高電平 return 0;}int led_release(struct inode *inode, struct file *file){ printk(KERN_EMERG “[ KERN_EMERG ] led Module led_release\n”); struct led_chardev *led_cdev = (struct led_chardev *)container_of(inode->i_cdev, struct led_chardev, dev); /@@* 釋放ioremap後的虛擬地址空間 */ iounmap(led_cdev->va_dr); //釋放資料暫存器虛擬地址 iounmap(led_cdev->va_gdir); //釋放輸入輸出方向暫存器虛擬地址 iounmap(led_cdev->va_iomuxc_mux); //釋放I/O複用暫存器虛擬地址 iounmap(led_cdev->va_ccm_ccgrx); //釋放時鐘控制暫存器虛擬地址 iounmap(led_cdev->va_iomux_pad); //釋放埠電氣屬性暫存器虛擬地址 return 0;}struct file_operations fops = { 。owner = THIS_MODULE, 。open = led_open, 。release = led_release, 。write = led_write, 。read = led_read,};static int __init led_init(void) { int ret = 0; int i = 0; int j = 0; printk(KERN_EMERG “[ KERN_EMERG ] led Module Init\n”); ret = alloc_chrdev_region(&devno, 0,DEV_CNT, “led”); if(ret == 0) printk(KERN_EMERG “[ KERN_EMERG ] baseminor ok\n”); else goto out; major = MAJOR(devno); minor = MINOR(devno); led_class = class_create(THIS_MODULE,“led”); if(led_class < 0) goto out; for(i=0;i

如下是應用層程式碼,一個簡單的測試程式碼

#include “stdio。h”#include “unistd。h”#include #include #include int main(){ char buf[6] = {0}; int val = 0; char *hello = “/dev/led_chrdev1”; int fd = open(hello,O_RDWR|O_NDELAY); if(fd < 0) { printf(“open error\n”); return 0; } while(1) { write(fd,&val,5); sleep(1); val = !val; } close(fd); return 0;}