文章目錄
- 前言
- 一、設備驅動的作用與本質
- 1. 驅動的作用
- 2. 有無操作系統的區別
- 二、內存管理單元MMU
- 三、相關函數
- 1. ioremap( )
- 2. iounmap( )
- 3. class_create( )
- 4. class_destroy( )
- 四、GPIO的基本知識
- 1. GPIO的寄存器進行讀寫操作流程
- 2. 引腳復用
- 2. 定義GPIO寄存器物理地址
- 五、實驗代碼
- 1. 宏定義出需要的地址
- 2. 編寫LED字符設備結構體且初始化
- 3. container_of( )函數
- 4. file_operations結構體成員函數的實現
- 5. 實驗效果
前言
??前段時間我們學習了字符驅動,并實現了字符的回環發送,這部分我們將進行I/O的操作學習,以萬能的點亮LED為例。
一、設備驅動的作用與本質
??直接操作寄存器點亮LED和通過驅動程序點亮LED最本質的區別就是有無使用操作系統。 有操作系統的存在則大大降低了應用軟件與硬件平臺的耦合度,它充當了我們硬件與應用軟件之間的紐帶, 使得應用軟件只需要調用驅動程序接口API就可以讓硬件去完成要求的開發,而應用軟件則不需要關心硬件到底是如何工作的。
1. 驅動的作用
??設備驅動與底層硬件直接打交道,按照硬件設備的具體工作方式讀寫設備寄存器, 完成設備的輪詢、中斷處理、DMA通信,進行物理內存向虛擬內存的映射,最終使通信設備能夠收發數據, 使顯示設備能夠顯示文字和畫面,使存儲設備能夠記錄文件和數據。
2. 有無操作系統的區別
??無操作系統(即裸機)時的設備驅動也就是直接操作寄存器的方式控制硬件,在這樣的系統中,雖然不存在操作系統,但是設備驅動是必須存在的。 一般情況下,對每一種設備驅動都會定義為一個軟件模塊,包含.h文件和.c文件,前者定義該設備驅動的數據結構并聲明外部函數, 后者進行設備驅動的具體實現。其他模塊需要使用這個設備的時候,只需要包含設備驅動的頭文件然后調用其中的外部接口函數即可。 比如我們在51或者STM32中直接看手冊查找對應的寄存器,然后往寄存器相應的位寫入數據0或1便可以實現LED的亮滅。
??有操作系統時的設備驅動反觀有操作系統。首先,驅動硬件工作的的部分仍然是必不可少的,其次,我們還需要將設備驅動融入內核。 為了實現這種融合,必須在所有的設備驅動中設計面向操作系統內核的接口,這樣的接口由操作系統規定,對一類設備而言結構一致,獨立于具體的設備,還是以led為例,我們就要將LED燈引腳對應的數據寄存器(物理地址)映射到程序的虛擬地址空間當中,然后我們就可以像操作寄存器一樣去操作我們的虛擬地址啦!
二、內存管理單元MMU
??MMU是一個實際的硬件,為編程提供了方便統一的內存空間抽象,MMU內部有一個專門存放頁表的頁表地址寄存器,該寄存器存放著頁表的具體位置,這使得只要程序在被分配的虛擬地址范圍內進行讀寫操作,實際上就是對設備(寄存器)的訪問,如下圖所示。他的主要作用是將虛擬地址翻譯成真實的物理地址同時管理和保護內存, 不同的進程有各自的虛擬地址空間,某個進程中的程序不能修改另外一個進程所使用的物理地址,以此使得進程之間互不干擾,相互隔離。 總體而言MMU具有如下功能:
- 保護內存: MMU給一些指定的內存塊設置了讀、寫以及可執行的權限,這些權限存儲在頁表當中,MMU會檢查CPU當前所處的是特權模式還是用戶模式,如果和操作系統所設置的權限匹配則可以訪問,如果CPU要訪問一段虛擬地址,則將虛擬地址轉換成物理地址,否則將產生異常,防止內存被惡意地修改。
- 提供方便統一的內存空間抽象,實現虛擬地址到物理地址的轉換: CPU可以運行在虛擬的內存當中,虛擬內存一般要比實際的物理內存大很多,使得CPU可以運行比較大的應用程序。
三、相關函數
??上面提到了物理地址到虛擬地址的轉換函數。包括ioremap()地址映射和取消地址映射iounmap()函數。
1. ioremap( )
//用于將物理內存地址映射到內核的虛擬地址空間
void __iomem *ioremap(phys_addr_t phys_addr, unsigned long size)//定義寄存器物理地址
#define GPIO0_BASE (0xFDD60000)
#define GPIO0_DR (GPIO0_BASE+0x0000)va_dr = ioremap(GPIO0_DR, 4); // 將物理地址GPIO0_DR,映射給虛擬地址指針,這段地址大小為4個字節
val = ioread32(va_dr); //讀取該地址的值,保存到臨時變量,重新賦值
val |= (0x00400000); // 設置GPIO0_A6引腳低電平
writel(val, va_dr); //把值重新寫入到被映射后的虛擬地址當中,實際是往寄存器中寫入了數據
- 參數:
- phys_addr:要映射的物理地址的起始地址
- size:要映射的內存區域的大小(以字節為單位)
- 返回值:
- 如果成功,ioremap返回一個指向映射區域的虛擬地址的指針
- 如果失敗,返回NULL
??在使用ioremap函數將物理地址轉換成虛擬地址之后,理論上我們便可以直接讀寫I/O內存,但是為了符合驅動的跨平臺以及可移植性, 我們應該使用linux中指定的函數(如:iowrite8()、iowrite16()、iowrite32()、ioread8()、ioread16()、ioread32()等)去讀寫I/O內存,如下表所示:
函數名 | 功能 |
---|---|
unsigned int ioread8(void __iomem *addr) | 讀取一個字節(8bit) |
unsigned int ioread16(void __iomem *addr) | 讀取一個字(16bit) |
unsigned int ioread32(void __iomem *addr) | 讀取一個雙字(32bit) |
void iowrite8(u8 data, void __iomem *addr) | 寫入一個字節(8bit) |
void iowrite16(u16 data, void __iomem *addr) | 寫入一個字(16bit) |
void iowrite32(u32 data, void __iomem *addr) | 寫入一個雙字(32bit) |
2. iounmap( )
//取消地址映射
void iounmap(void *addr)iounmap(va_dr); //釋放掉ioremap映射之后的起始地址(虛擬地址)
- 參數
- addr: 需要取消ioremap映射之后的起始地址(虛擬地址)。
- 返回值: 無
3. class_create( )
//提交目錄信息
#define class_create(owner, name) \
({static struct lock_class_key _key; \_class_create(owner, name, &_key); \
})
- 參數
- owner:THIS_MODULE (struct module結構體的首地址這個結構體存放了驅動的出口入口)
- name:目錄名
- 返回值
- 成功:返回結構體首地址
- 失敗:返回錯誤碼指針
注:IS_ERR(cls); 判斷是否為錯誤指針
??PTR_ERR(cls); 將錯誤碼指針轉換為錯誤碼
4. class_destroy( )
//注銷目錄信息
void class_destroy(struct class *cls);
- 參數
- cls:結構體首地址
- 返回值:無
四、GPIO的基本知識
1. GPIO的寄存器進行讀寫操作流程
- 使能GPIO時鐘(默認開啟,不用設置)
- 設置引腳復用為GPIO(復位默認為GPIO,不用配置)
- 設置引腳屬性(上下拉、速率、驅動能力,默認)
- 控制GPIO引腳為輸出,并輸出高低電平
2. 引腳復用
??對于rockchip系類芯片,我們需要通過參考手冊以及數據手冊來確定引腳的復用功能。首先可以看到泰山派的小燈連接引腳,這里我們選擇GPIO1_B0_d。
??通過查詢rk3568官方資料,可以看到該引腳的復用功能如下所示。
??再查找其復用功能存在于SYS_GRF寄存器,和復用相關的總共8個寄存器,如下圖所示:
??查詢 Rockchip_RK3568_TRM_Part1 手冊,GRF_GPIO1B_IOMUX_L寄存器(由于GPIO1_b0是在低八位,下同),如下圖所示:
??寄存器總共32位,高16位都是使能位,控制低16位的寫使能,低16位對應4個引腳,每個引腳占用3bits,不同的值引腳復用為不同功能。與此同時由[14:12]進行具體功能的設定。
??我們可以查看到SYS_GRF寄存器的復用功能基地址為0xFDC60000。
??此時通過命令行輸入可以查詢到該寄存器的設置情況,可以看到這里默認是GPIO功能。
//目標地址為Address Base(0xfdc60000)+offset(0x0008)
io -r -4 0xfdc60008
2. 定義GPIO寄存器物理地址
??需要設置的寄存器的地址為base+offset,由下圖可以知道GPIO1的基地址為:0xFE740000
??接下來就是確定GPIO的是輸入還是輸出,我們這里需要的是GPIO_SWPORT_DDR_L。
??可以看到GPIO_SWPORT_DDR_L的定義情況,這里我們可以重復上面提到的命令行,查看寄存器的設置情況,我們的b0應當是第1x7+1=8位。
??數據寄存器選擇GPIO_SWPORT_DR_L,大致流程和上面一樣就不再贅述了。這里便完成了對GPIO的設置。
五、實驗代碼
1. 宏定義出需要的地址
#define GPIO1_BASE (0xFE740000)//一個寄存器32位,其中高16位都是寫使能位,控制低16位的寫使能;低16位對應16個引腳,控制引腳的輸出電平
#define GPIO1_DR_L (GPIO0_BASE + 0x0000) // GPIO0的低十六位引腳的數據寄存器地址
#define GPIO1_DR_H (GPIO0_BASE + 0x0004) // GPIO0的高十六位引腳的數據寄存器地址//一個寄存器32位,其中高16位都是寫使能位,控制低16位的寫使能;低16位對應16個引腳,控制引腳的輸入輸出模式
#define GPIO1_DDR_L (GPIO0_BASE + 0x0008) // GPIO0的低十六位引腳的數據方向寄存器地址
#define GPIO1_DDR_H (GPIO0_BASE + 0x000C) // GPIO0的低十六位引腳的數據方向寄存器地址
2. 編寫LED字符設備結構體且初始化
//led字符設備結構體
struct led_chrdev {struct cdev dev;unsigned int __iomem *va_dr; // 數據寄存器虛擬地址保存變量unsigned int __iomem *va_ddr; // 數據方向寄存器虛擬地址保存變量unsigned int led_pin; // 引腳
};static struct led_chrdev led_cdev[DEV_CNT] = {{.led_pin = 8 //CPIO1_B0的偏移為8+0=8},
};
3. container_of( )函數
??在Linux驅動編程當中我們會經常和container_of()這個函數打交道,其宏定義實現如下所示:
#define container_of(ptr, type, member) ({ \const typeof( ((type *)0)->member ) *__mptr = (ptr); \(type *)( (char *)__mptr - offsetof(type,member) );})
- 參數:
- ptr: 結構體變量中某個成員的地址
- type: 結構體類型
- member: 該結構體變量的具體名字
- 返回值: 結構體type的首地址
??原理其實很簡單,就是通過已知類型type的成員member的地址ptr,計算出結構體type的首地址。 type的首地址 = ptr - size ,需要注意的是它們的大小都是以字節為單位計算的,container_of( )函數的主要作用如下:
- 判斷ptr 與 member 是否為同一類型
- 計算size大小,結構體的起始地址 = (type *)((char *)ptr - size) (注:強轉為該結構體指針)
注:文件私有數據
??一般很多的linux驅動都會將文件的私有數據private_data指向設備結構體,其保存了用戶自定義設備結構體的地址。 自定義結構體的地址被保存在private_data后,可以通過讀、寫等操作通過該私有數據去訪問設備結構體中的成員, 這樣做體現了linux中面向對象的程序設計思想。
4. file_operations結構體成員函數的實現
static int led_chrdev_open(struct inode *inode, struct file *filp)
{unsigned int val = 0;struct led_chrdev *led_cdev = (struct led_chrdev *)container_of(inode->i_cdev, struct led_chrdev, dev);filp->private_data = container_of(inode->i_cdev, struct led_chrdev, dev);printk("open\n");//讀取數據方向寄存器val = ioread32(led_cdev->va_ddr);//設置數據方向寄存器為pin位可寫val |= ((unsigned int)0x1 << (led_cdev->led_pin+16)); //設置數據方向寄存器為pin位輸出val |= ((unsigned int)0X1 << (led_cdev->led_pin));//寫入數據方向寄存器iowrite32(val,led_cdev->va_ddr);//讀取數據寄存器val = ioread32(led_cdev->va_dr);//設置數據寄存器為pin位可寫val |= ((unsigned int)0x1 << (led_cdev->led_pin+16));//設置數據寄存器為pin位高電平val |= ((unsigned int)0x1 << (led_cdev->led_pin));//寫入數據寄存器iowrite32(val, led_cdev->va_dr);return 0;
}
??這部分代碼位open_operations結構體的設置,其中container_of()函數和寄存器設置部分需要聯系前節4.2的介紹反復理解(筆者這里看了很久才頓悟)。
5. 實驗效果
最近臨時變化地點,后續補上。
免責聲明:本程序參考了野火和北京訊為科技的部分視頻資料,不作商用僅供學習,若有侵權和錯誤請聯系筆者刪除