?前言:
本文是根據嗶哩嗶哩網站上“正點原子【第四期】手把手教你學Linux系列課程之 Linux驅動開發篇”視頻的學習筆記,該課程配套開發板為正點原子alpha/mini Linux開發板。在這里會記錄下正點原子 I.MX6ULL 開發板的配套視頻教程所作的實驗和學習筆記內容。本文大量引用了正點原子教學視頻和鏈接中的內容。
引用:
正點原子IMX6U倉庫 (GuangzhouXingyi) - Gitee.com
正點原子【第四期】手把手教你學 Linux之驅動開發篇_嗶哩嗶哩_bilibili
《【正點原子】I.MX6U嵌入式Linux驅動開發指南V1.5.2.pdf》
正點原子資料下載中心 — 正點原子資料下載中心 1.0.0 文檔
正點原子imx6ull-mini-Linux驅動之Linux I2C 驅動實驗(21)-CSDN博客
uboot移植(4)--在NXP官方uboot適配ALPHA開發板網絡_uboot sr8201f-CSDN博客
?
正文:
本文是 “正點原子【第四期】手把手教你學 Linux之驅動開發篇-1.1 Linux驅動開發與裸機開發的區別”。本節將參考正點原子的視頻教程和配套的正點原子開發指南文檔進行學習。
?
0. 概述
上一章我們詳細的講解了字符設備驅動開發步驟,并且用一個虛擬的 chrdevbase 設備為例帶領大家完成了第一個字符設備驅動的開發。本章我們就開始編寫第一個真正的 Linux 字符設備驅動。在 I.MX6U-ALPHA 開發板上有一個 LED 燈,我們在裸機篇中已經編寫過此 LED 燈的裸機驅動,本章我們就來學習一下如何編寫 Linux 下的 LED 燈驅動。
1 Linux 下 LED 燈驅動原理
Linux 下的任何外設驅動,最終都是要配置相應的硬件寄存器。所以本章的 LED 燈驅動最終也是對 I.MX6ULL 的 IO 口進行配置,與裸機實驗不同的是,在 Linux 下編寫驅動要符合 Linux的驅動框架。I.MX6U-ALPHA 開發板上的 LED 連接到 I.MX6ULL 的 GPIO1_IO03 這個引腳上,因此本章實驗的重點就是編寫 Linux 下 I.MX6UL 引腳控制驅動。關于 I.MX6ULL 的 GPIO 詳細講解請參考第八章。
1.1 地址映射
在編寫驅動之前,我們需要先簡單了解一下 MMU 這個神器, MMU 全稱叫做 MemoryManage Unit,也就是內存管理單元。在老版本的 Linux 中要求處理器必須有 MMU,但是現在Linux 內核已經支持無 MMU 的處理器了。 MMU 主要完成的功能如下:
①、完成虛擬空間到物理空間的映射。
②、內存保護,設置存儲器的訪問權限,設置虛擬存儲空間的緩沖特性。
我們重點來看一下第①點,也就是虛擬空間到物理空間的映射,也叫做地址映射。首先了解兩個地址概念:虛擬地址(VA,Virtual Address)、物理地址(PA, Physcical Address)。對于 32 位的處理器來說,虛擬地址范圍是 2^32=4GB,我們的開發板上有 512MB 的 DDR3,這 512MB 的內存就是物理內存,經過 MMU 可以將其映射到整個 4GB 的虛擬空間,如圖 41.1.1 所示
?
物理內存只有 512MB,虛擬內存有 4GB,那么肯定存在多個虛擬地址映射到同一個物理地址上去,虛擬地址范圍比物理地址范圍大的問題處理器自會處理,這里我們不要去深究,因為MMU 是很復雜的一個東西,后續有時間的話正點原子 Linux 團隊會專門做 MMU 專題教程。
Linux 內核啟動的時候會初始化 MMU,設置好內存映射,設置好以后 CPU 訪問的都是虛擬 地 址 。 比 如 I.MX6ULL 的 GPIO1_IO03 引 腳 的 復 用 寄 存 器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03? 的地址為 0X020E0068。如果沒有開啟 MMU 的直接向 0X020E0068 這個寄存器地址寫入數據就可以配置 GPIO1_IO03 的復用功能。現在開啟了 MMU,并且設置了內存映射,因此就不能直接向 0X020E0068 這個地址寫入數據了。我們必須得到 0X020E0068 這個物理地址在 Linux 系統里面對應的虛擬地址,這里就涉及到了物理內存和虛擬內存之間的轉換,需要用到兩個函數: ioremap 和 iounmap。
1、 ioremap 函數
ioremap 函 數 用 于 獲 取 指 定 物 理 地 址 空 間 對 應 的 虛 擬 地 址 空 間 , 定 義 在arch/arm/include/asm/io.h 文件中,定義如下:
arch/arm/include/asm/io.h
ioremap 是個宏,有兩個參數: cookie 和 size,真正起作用的是函數__arm_ioremap,此函數有三個參數和一個返回值,這些參數和返回值的含義如下:
- phys_addr:要映射的物理起始地址。
- size:要映射的內存空間大小。
- mtype: ioremap 的類型,可以選擇 MT_DEVICE、 MT_DEVICE_NONSHARED、MT_DEVICE_CACHED 和 MT_DEVICE_WC, ioremap 函數選擇 MT_DEVICE。
返回值: __iomem 類型的指針,指向映射后的虛擬空間首地址。
假如我們要獲取 I.MX6ULL 的 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 寄存器對應的虛擬地址,使用如下代碼即可:
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
static void __iomem* SW_MUX_GPIO1_IO03;
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
宏 SW_MUX_GPIO1_IO03_BASE 是寄存器物理地址, SW_MUX_GPIO1_IO03 是映射后的虛擬地址。對于 I.MX6ULL 來說一個寄存器是 4 字節(32 位)的,因此映射的內存長度為 4。映射完成以后直接對 SW_MUX_GPIO1_IO03 進行讀寫操作即可。
2、 iounmap 函數
卸載驅動的時候需要使用 iounmap 函數釋放掉 ioremap 函數所做的映射, iounmap 函數原型如下:
void iounmap (volatile void __iomem *addr)
1.2 I/O 內存訪問函數
這里說的 I/O 是輸入/輸出的意思,并不是我們學習單片機的時候講的 GPIO 引腳。這里涉及到兩個概念: I/O 端口和 I/O 內存。
當外部寄存器或內存映射到 IO 空間時,稱為 I/O 端口。當外部寄存器或內存映射到內存空間時,稱為 I/O 內存
。但是對于 ARM 來說沒有 I/O 空間這個概念,因此 ARM 體系下只有 I/O 內存(可以直接理解為內存)。使用 ioremap 函數將寄存器的物理地址映射到虛擬地址以后,我們就可以直接通過指針訪問這些地址,但是 Linux 內核不建議這么做,而是推薦使用一組操作函數來對映射后的內存進行讀寫操作。
1、讀操作函數
讀操作函數有如下幾個:
readb、 readw 和 readl 這三個函數分別對應 8bit、 16bit 和 32bit 讀操作,參數 addr 就是要讀取寫內存地址,返回值就是讀取到的數據。
2、寫操作函數
寫操作函數有如下幾個:
writeb、 writew 和 writel 這三個函數分別對應 8bit、 16bit 和 32bit 寫操作,參數 value 是要寫入的數值, addr 是要寫入的地址。
2.硬件原理圖分析
本章實驗硬件原理圖參考 8.3 小節即可
?
3.實驗程序編寫
本章實驗編寫 Linux 下的 LED 燈驅動,可以通過應用程序對 I.MX6U-ALPHA 開發板上的LED 燈進行開關操作。
新建名為“2_led”文件夾,然后在 2_led 文件夾里面創建 VSCode 工程,工作區命名為“led”。工程創建好以后新建 led.c 文件,此文件就是 led 的驅動文件,在 led.c 里面輸入如下內容:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/uaccess.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <asm/io.h>#define LED_MAJOR 200
#define LED_NAME "led"#define CCM_CCGR1_BASE (0x020C406C)
#define SW_MUX_CTL_PAD_GPIO1_IO03_BASE (0x020E0068)
#define SW_PAD_CTL_PAD_GPIO1_IO03_BASE (0x020E02F4)
#define GPIO1_DR_BASE (0x0209C000)
#define GPIO1_GDIR_BASE (0X0209C004)void __iomem * CCM_CCGR1 = NULL;
void __iomem * SW_MUX_CTL_GPIO1_IO03 = NULL;
void __iomem * SW_PAD_CTL_GPIO1_IO03 = NULL;
void __iomem * GPIO1_GDIR = NULL;
void __iomem * GPIO1_DR = NULL;static int led_open(struct inode *inode, struct file *filep) {return 0;
}static int led_release(struct inode *inode, struct file *filep) {return 0;
}static ssize_t led_read(struct file *filep, char __user *buf,size_t count, loff_t *ppos) {return 0;
}#define LED_OFF 0
#define LED_ON 1void led_switch(uint8_t sta) {u32 val = sta;if (sta == LED_ON) {val = readl(GPIO1_DR);val &= ~(1 << 3);writel(val, GPIO1_DR);}else if (sta == LED_OFF) {val = readl(GPIO1_DR);val |= (1 << 3);writel(val, GPIO1_DR);}
}static ssize_t led_write(struct file *filep, const char __user *buf,size_t count, loff_t *ppos)
{int retvalue;unsigned char databuf[1];retvalue = copy_from_user(databuf, buf, 1);if(retvalue < 0 ){printk("kernel write failed\n\n");return -EFAULT;}if (databuf[0] == 0) {led_switch(LED_OFF);}else if (databuf[0] == 1) {led_switch(LED_ON);}return 0;
}//
static const struct file_operations led_fopes = {.owner = THIS_MODULE,.read = led_read,.write = led_write,.open = led_open,.release = led_release,
};// entry
static int __init led_init(void) {int ret = 0;u32 val = 0;printk("led init\n");/**/// map gpio physical memory address to vitual addressCCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);SW_MUX_CTL_GPIO1_IO03 = ioremap(SW_MUX_CTL_PAD_GPIO1_IO03_BASE, 4);SW_PAD_CTL_GPIO1_IO03 = ioremap(SW_PAD_CTL_PAD_GPIO1_IO03_BASE, 4);GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);//gpio init//CCGR0 clockval = readl(CCM_CCGR1);val |= (3 << 26);writel(val, CCM_CCGR1);//GPIO1_IO03 MUXwritel(0x5, SW_MUX_CTL_GPIO1_IO03);writel(0x10B0, SW_PAD_CTL_GPIO1_IO03);//GPOI1 directionval = readl(GPIO1_GDIR);val |= (1 << 3);writel(val, GPIO1_GDIR);//GPIO1 dataval = readl(GPIO1_DR);val &= ~(1 << 3);writel(val, GPIO1_DR);// register chrdeviceret = register_chrdev(LED_MAJOR, LED_NAME, &led_fopes);if(ret < 0){printk("register chardev fail\r\n");return -EIO;}return 0;
}//exit
static void __exit led_exit(void) {u32 val;unregister_chrdev(LED_MAJOR, LED_NAME);//GPIO1 dataval = readl(GPIO1_DR);val |= (1 << 3);writel(val, GPIO1_DR);iounmap(CCM_CCGR1);iounmap(SW_MUX_CTL_GPIO1_IO03);iounmap(SW_PAD_CTL_GPIO1_IO03);iounmap(GPIO1_DR);iounmap(GPIO1_GDIR);printk("led exit\n");
}//retister .ko load and deload function
module_init(led_init);
module_exit(led_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("dimon.chen@163.com");
編寫測試 APP
編寫測試 APP, led 驅動加載成功以后手動創建/dev/led 節點,應用 APP 通過操作/dev/led文件來完成對 LED 設備的控制。向/dev/led 文件寫 0 表示關閉 LED 燈,寫 1 表示打開 LED 燈。新建 ledApp.c 文件,在里面輸入如下內容:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>/** ./ledApp <filename> <0|1> , 0:trun off led, 1:turn on led* ./ledApp /dev/led 0 * ./ledApp /dev/led 1*/
int main(int argc, char *argv[])
{int ret = 0;int fd = -1;char *filename = NULL;char read_data[100];char write_data[100];if(argc < 3) {printf("usate: %s <dev> <opt>\r\n", argv[0]);return -1;}filename = argv[1];fd = open(filename, O_RDWR);if(fd < 0) {printf("open %s fail\r\n");return -1;}char databuf[1];databuf[0] = atoi(argv[2]);ret = write(fd, databuf, sizeof(databuf));if(ret < 0 ){printf("write fail \n");close(fd);return -1;}close(fd);return 0;
}
4 運行測試
4.1 編譯驅動程序和測試 APP
1、編譯驅動程序
編寫 Makefile 文件,本章實驗的 Makefile 文件和第四十章實驗基本一樣,只是將 obj-m 變量的值改為 led.o, Makefile 內容如下所示:
obj-m := led.oPWD=$(shell pwd)
KDERDIR=/home/dimon/I.MX6ULL/linux_altek_drivermodules:$(MAKE) -C $(KDERDIR) M=$(PWD) modulesclear:$(MAKE) -C $(KDERDIR) M=$(PWD) clear
2、編譯測試 APP
輸入如下命令編譯測試 ledApp.c 這個測試程序:
arm-linux-gnueabihf-gcc ledApp.c -o ledApp
編譯成功以后就會生成 ledApp 這個應用程序。
4.2 運行測試
注意! 如果大家使用的正點原子出廠系統來做本實驗,那么會發現 LED 燈會一直閃爍。這是因為正點原子出廠系統默認將 LED 燈作為了心跳燈,因此系統啟動以后 LED 燈就會自動閃爍,這樣會影響大家做實驗。如果是完全按照本教程自行移植的內核和根文件系統,那么就不會遇到此問題。 如果直接使用出廠系統來做實驗,我們需要關閉 LED 燈的心跳功能,關閉方法參考《【正點原子】 I.MX6U 用戶快速體驗》第 3.1 小節,或者輸入如下命令即可:
echo none > /sys/class/leds/sys-led/trigger // 改變 LED 的觸發模式
創建設備節點
mknod /dev/led c 200 0
加載內核模塊
執行ledApp測試程序,控制LED亮滅
./ledApp /dev/led 1 //打開 LED 燈
輸入上述命令以后觀察 I.MX6U-ALPHA 開發板上的紅色 LED 燈是否點亮,如果點亮的話說明驅動工作正常。
在輸入如下命令關閉 LED 燈
./ledApp /dev/led 0 //關閉 LED 燈
輸入上述命令以后觀察 I.MX6U-ALPHA 開發板上的紅色 LED 燈是否熄滅,如果熄滅的話說明我們編寫的 LED 驅動工作完全正常!至此,我們成功編寫了第一個真正的 Linux 驅動設備程序。
如果要卸載驅動的話輸入如下命令即可:
rmmod led.ko
5. 總結
Linux中開啟了MMU則ARM 處理器訪問的所有地址都是虛擬內存地址,當在led.ko中需要對寄存器物理地址進行讀操作和寫操作時,需要先把寄存器物理內存地址映射為虛擬內存物理地址。從物理內存地址映射為虛擬物理內存地址使用的是 ioremap() 和 iounmap(),在Linux中類似這種ioremap()/iounmap()的函數必須成對的使用,有ioremap,在退出內核模塊時就需要做iounmap()。
在內核里對映射到虛擬內存地址的 void __iomem * xxx_addr 的地址做讀寫的時,需要使用Linux內核提供 void __iomem * 的讀寫函數,內核提供的讀函數有 readb(), readw(), readl() 分別是讀8bit, 16bit, 32bit,內核提供的寫函數有 writeb(), writew(), writel() 分別是寫 8bit, 16bit, 32bit。