摘要:前面已經介紹了使用裸機點燈,今天使用驅動開發的方式點亮一個LED燈。看看兩者有啥區別不?
一、先看原理圖
首先查看原理圖,看看我們的板子上的LED等接在哪一個IO口上面。
好了,看原理圖我們知道LED燈接在芯片的GPIO1的第三個引腳上面,也就是GPIO1_IO03。
二、IMX6UL的GPIO操作方法
先掌握三個名詞
CCM: Clock Controller Module (時鐘控制模塊)
IOMUXC : IOMUX Controller,IO復用控制器
GPIO: General-purpose input/output,通用的輸入輸出口
2.1 GPIO模塊結構
參考芯片手冊《Chapter 26: General Purpose Input/Output (GPIO)》我們知道了IMX6UL一共有有5組GPIO(GPIO1~GPIO5),每組引腳最多有32個,但是可能實際上并沒有那么多。
?
GPIO1有32個引腳:GPIO1_IO0~GPIO1_IO31; GPIO2有22個引腳:GPIO2_IO0~GPIO2_IO21; GPIO3有29個引腳:GPIO3_IO0~GPIO3_IO28; GPIO4有29個引腳:GPIO4_IO0~GPIO4_IO28; GPIO5有12個引腳:GPIO5_IO0~GPIO5_IO11;
?
我們知道IM6ULL有很多的引腳IO,但是并不是每一個引腳都能當做GPIO使用,它可以復用為其他模式的,比如作為I2C的時鐘線I2C2_SCL等其他的用處。所以要向把某一IO當做GPIO使用需要將其復用,在linux中負責復用功能的寄存器IOMUXC_SW_MUX。還有要打開這個GPIO的時鐘,在linux中叫做CCM,跟STM32一樣還要設置它的IO口速度、上下拉電阻啊、驅動能力啊、壓擺率(就是 IO 電平跳變所需要的時間,比如從0到1需要多少時間,時間越小波形就越陡,說明壓擺率越高)啊等這些,在linux中是用IOMUXC_SW_PAD。
因此如果想要使用某一組GPIO,比如GPIO1_IO03。首先要打開GPIO1的時鐘,然后將GPIO1_IO03設置為GPIO模式,而不是IIC模式。然后在設置一下GPIO1_IO03這個引腳的模式,速度、上下拉電阻、壓擺率等。然后再設置GPIO1_IO03為輸出模式。最后我們就可以向GPIO1_IO03的DR寄存器也就是數據寄存器寫入0或者1,就可以輸出高低電平來控制LED等的亮滅了。
2.2 打開的時鐘
根據芯片手冊我們可以看到,要想打開GPIO1_IO03的時鐘就需要要去配置CCGR1這個寄存器的CG13這個位
而且我還知道了這個寄存器的地址是20C406CH,因此我們可以寫一個宏定義。
?
#define?CCM_CCGR1_BASE????(0X020C406C)//這個寄存器用來打開GPIO1的時鐘的
/*?1、使能GPIO1時鐘?*/ val?=?readl(IMX6U_CCM_CCGR1); val?&=?~(3?<26);?/*?清楚以前的設置?*/ val?|=?(3?<26);?/*?設置新值?*/ writel(val,?IMX6U_CCM_CCGR1);
?
2.3 IOMUXC引腳復用和模式配置
參考資料:芯片手冊《Chapter 32: IOMUX Controller (IOMUXC)》。對于某個/某組引腳,IOMUXC中有2個寄存器用來設置它。
IOMUXC_SW_MUX_CTL_PAD_pad-name
?
IOMUXC_SW_MUX_CTL_PAD_?:Mux pad xxx,選擇某個pad的功能 IOMUXC_SW_MUX_CTL_GRP_ :Mux grp xxx,選擇某組引腳的功能
?
某個引腳,或是某組預設的引腳,都有8個可選的模式(alternate (ALT) MUX_MODE),
比如我們要把這個GPIO1_IO03設置為GPIO模式,就要將這個寄存器的bit[0..3]設置為0101,也就是5.
然后也看到這個寄存器的地址位Address: 20E_0000h base + 68h offset = 20E_0068h
?
#define?SW_MUX_GPIO1_IO03_BASE??(0X020E0068)//這個寄存器是將GPIO1_IO03復用為GPIO的
/*?2、設置GPIO1_IO03的復用功能,將其復用為 ?*??? GPIO1_IO03,最后設置IO屬性。 ?*/ writel(5,?SW_MUX_GPIO1_IO03);
?
IOMUXC_SW_MUX_CTL_GRP_group-name
?
IOMUXC_SW_PAD_CTL_PAD_:pad pad xxx,設置某個pad的參數 IOMUXC_SW_PAD_CTL_GRP_ :pad grp xxx,設置某組引腳的參數
?
比如:
2.4 GPIO模塊內部
框圖如下:
我們暫時只需要關心3個寄存器:
① GPIOx_GDIR:設置引腳方向,每位對應一個引腳,1-output,0-input
② GPIOx_DR:設置輸出引腳的電平,每位對應一個引腳,1-高電平,0-低電平
③ GPIOx_PSR:讀取引腳的電平,每位對應一個引腳,1-高電平,0-低電平
三、怎么編程
3.1 讀GPIO
① 設置CCM_CCGRx寄存器中某位使能對應的GPIO模塊,默認是使能的。② 設置IOMUX來選擇引腳用于GPIO。③ 設置GPIOx_GDIR中某位為0,把該引腳設置為輸入功能。④ 讀GPIOx_DR或GPIOx_PSR得到某位的值(讀GPIOx_DR返回的是GPIOx_PSR的值)
3.2 寫GPIO
① 設置CCM_CCGRx寄存器中某位使能對應的GPIO模塊,默認是使能的。② 設置IOMUX來選擇引腳用于GPIO。③ 設置GPIOx_GDIR中某位為1,把該引腳設置為輸出功能。④ 寫GPIOx_DR某位的值。
需要注意的是,你可以設置該引腳的loopback功能,這樣就可以從GPIOx_PSR中讀到引腳的有實電平;你從GPIOx_DR中讀回的只是上次設置的值,它并不能反應引腳的真實電平,比如可能因為硬件故障導致該引腳跟地短路了,你通過設置GPIOx_DR讓它輸出高電平并不會起效果。
有了上面的知識,我們點亮led燈的流程基本就了解了。
四、GPIO寄存器操作方法
原則:不能影響到其他位。
4.1 直接讀寫
讀出、修改對應位、寫入
要設置bit n
?
val?=?data_reg;//讀出 val?=?val?|?(1<?
要清除bit n
?
val?=?data_reg;//讀出 val?=?val?&?~(1<?
4.2 set-and-clear protocol
set_reg,clr_reg,data_reg 三個寄存器對應的是同一個物理寄存器
要設置 bit n:set_reg = (1<
要清除 bit n:clr_reg = (1<
五、編寫驅動程序的套路
1、確定主設備號,也可以讓內核分配。
2、定義自己的file_operations結構體。
3、實現對應的drv_open/drv_read/drv_write等函數,填入file_operations結構體。
4、把file_operations結構體告訴內核:register_chrdev。
5、誰來注冊驅動程序啊?得有一個入口函數:安裝驅動程序時,就會去調用這個入口函數。
6、有入口函數就應該有出口函數:卸載驅動程序時,出口函數調用unregister_chrdev。
7、其他完善:提供設備信息,自動創建設備節點:class_create,device_create。
驅動怎么操作硬件?
通過ioremap映射寄存器的物理地址得到虛擬地址,讀寫虛擬地址。
驅動怎么和APP傳輸數據?
通過copy_to_user、copy_from_user這 2 個函數。
六、地址映射
在編寫驅動之前,我們需要先簡單了解一下 MMU 這個神器,MMU全稱叫做 Memory Manage Unit,也就是內存管理單元。在老版本的Linux中要求處理器必須有MMU,但是現在Linux內核已經支持無MMU的處理器了。MMU主要完成的功能如下:
①、完成虛擬空間到物理空間的映射。
②、內存保護,設置存儲器的訪問權限,設置虛擬存儲空間的緩沖特性。
我們重點來看一下第①點,也就是虛擬空間到物理空間的映射,也叫做地址映射。首先了解兩個地址概念:虛擬地址(VA,Virtual Address)、物理地址(PA,Physcical Address)。對于 32 位的處理器來說,虛擬地址范圍是2^32=4GB,我們的開發板上有512MB的DDR3,這512MB的內存就是物理內存,經過MMU可以將其映射到整個4GB的虛擬空間
內存映射
物理內存只有512MB,虛擬內存有4GB,那么肯定存在多個虛擬地址映射到同一個物理地址上去,虛擬地址范圍比物理地址范圍大的問題處理器自會處理。
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。
6.1 ioremap函數
ioremap函 數用于獲取指定物理地址空間對應的虛擬地址空間,定義在arch/arm/include/asm/io.h文件中,定義如下:
?
#include#define?ioremap(cookie,size)?__arm_ioremap((cookie),?(size),MT_DEVICE) 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)); } ?
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進行讀寫操作即可。實際上,它是按頁(4096 字節)進行映射的,是整頁整頁地映射的。所以說雖然映射的是4字節,實際上映射的是4096字節。
6.2 iounmap函數
卸載驅動的時候需要使用iounmap函數釋放掉ioremap函數所做的映射,iounmap函數原型如下:
?
void?iounmap?(volatile?void?__iomem?*addr)?
iounmap只有一個參數addr,此參數就是要取消映射的虛擬地址空間首地址。假如我們現在要取消掉IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03寄存器的地址映射,使用如下代碼即可:
?
iounmap(SW_MUX_GPIO1_IO03);?
6.3 volatile的使用
① 編譯器很聰明,會幫我們做些優化,比如:
?
int?a; a?=?0;?//?這句話可以優化掉,不影響?a?的結果 a?=?1;?
② 有時候編譯器會自作聰明,比如:
?
int?*p?=?ioremap(xxxx,?4);?//?GPIO?寄存器的地址 *p?=?0;?//?點燈,但是這句話被優化掉了 *p?=?1;?//?滅燈?
③ 對于上面的情況,為了避免編譯器自動優化,需要加上 volatile,告訴它這是容易出錯的,別亂優化:
?
volatile?int?*p?=?ioremap(xxxx,?4);?//?GPIO?寄存器的地址 *p?=?0;?//?點燈,這句話不會被優化掉 *p?=?1;?//?滅燈?
七、I/O內存訪問函數
這里說的I/O是輸入/輸出的意思,并不是我們學習單片機的時候講的GPIO引腳。這里涉及到兩個概念:I/O端口和I/O內存。
當外部寄存器或內存映射到IO空間時,稱為I/O端口。當外部寄存器或內存映射到內存空間時,稱為I/O內存。
但是對于ARM來說沒有 I/O 空間這個概念,因此ARM體系下只有I/O內存(可以直接理解為內存)。使用ioremap函數將寄存器的物理地址映射到虛擬地址以后,我們就可以直接通過指針訪問這些地址,但是Linux內核不建議這么做,而是推薦使用一組操作函數來對映射后的內存進行讀寫操作。
上面的話是啥意思呢?
我說通俗一點就是:我現在知道了GPIO1_IO03它的時鐘寄存器地址是0X020C406C,但是你不能直接操作它
?
#define?CCM_CCGR1_BASE????(0X020C406C)?
0X020C406C是它實際存在的也就是物理地址,但是呢在Linux內核啟動的時候會初始化MMU,設置好內存映射,設置好以后CPU訪問的都是虛擬地址,我們就不能操作實際的物理地址了。怎么辦呢?不用怕,Linux提供了ioremap內存映射函數,我知道了實際的物理地址,只要通過這個函數我們就自動的獲取到了這個物理地址對應的虛擬地址了
?
IMX6U_CCM_CCGR1?=?ioremap(CCM_CCGR1_BASE,?4);?
現在我們就得到了0X020C406C對應的虛擬地址IMX6U_CCM_CCGR1 ,但是呢,現在我們還不能直接操作這個虛擬地址。這又為啥呢?因為使用ioremap函數將寄存器的物理地址映射到虛擬地址以后,按說我們就可以直接通過指針訪問這些地址,但是Linux內核不建議這么做,而是推薦使用一組操作函數來對映射后的內存進行讀寫操作。好家伙,Linux內核它不建議這樣做,它又提供了讀寫函數對這個虛擬地址進行操作。那么我們用戶只能按照它建議的這樣做了。比如我想操作這個地址后4個字節的某幾個位,就需要下面這樣做,先把這個地址對應的內存空間讀出來,然后修改,最后再把修改好的數據寫入就可以了。
?
val?=?readl(IMX6U_CCM_CCGR1); val?&=?~(3?<26);?/*?清楚以前的設置?*/ val?|=?(3?<26);?/*?設置新值?*/ writel(val,?IMX6U_CCM_CCGR1);?
具體的讀操作和寫操作函數如下:
1 、讀操作函數
讀操作函數有如下幾個:
?
u8?readb(const?volatile?void?__iomem?*addr)//讀8bit u16?readw(const?volatile?void?__iomem?*addr)//讀16bit u32?readl(const?volatile?void?__iomem?*addr)//讀32bit?
readb、readw 和readl這三個函數分別對應 8bit、16bit 和 32bit讀操作,參數addr就是要讀取寫內存地址,返回值就是讀取到的數據。
2 、寫操作函數
寫操作函數有如下幾個:
?
void?writeb(u8?value,?volatile?void?__iomem?*addr)//寫8bit void?writew(u16?value,?volatile?void?__iomem?*addr)//寫16bit void?writel(u32?value,?volatile?void?__iomem?*addr)//寫32bit?
writeb、writew 和 writel 這三個函數分別對應 8bit、16bit 和 32bit 寫操作,參數value是要寫入的數值,addr是要寫入的地址。
八、程序編寫
8.1 編寫驅動程序
?
#include?#include? #include? #include? #include? #include? #include? #include? #include? #include? #include? /* 我們要配置某一個GPIO的引腳 1、先打開這個GPIO的時鐘 2、在講這個GPIO復用為GPIO功能 3、設置這個GPIO的參數等 4、設置這個GPIO是輸入還是輸出 5、向這個GPIO的數據寄存器寫數據就可以了 */ #define?LED_MAJOR??200??/*?主設備號?*/ #define?LED_NAME??"led"??/*?設備名字?*/ #define?LEDOFF??0????/*?關燈?*/ #define?LEDON??1????/*?開燈?*/ ? /*?寄存器物理地址?*/ #define?CCM_CCGR1_BASE????(0X020C406C)//這個寄存器用來打開GPIO1的時鐘的 #define?SW_MUX_GPIO1_IO03_BASE??(0X020E0068)//這個寄存器是將GPIO1_IO03復用為GPIO的 #define?SW_PAD_GPIO1_IO03_BASE??(0X020E02F4)//這個寄存器是配置GPIO1_IO03的速度、驅動能力、壓擺率等 #define?GPIO1_DR_BASE????(0X0209C000)//這個寄存器是GPIO1_IO03的數據寄存器 #define?GPIO1_GDIR_BASE????(0X0209C004)//這個寄存器是設置GPIO1_IO03的方向,輸入還是輸出 /*?映射后的寄存器虛擬地址指針?*/ static?void?__iomem?*IMX6U_CCM_CCGR1; static?void?__iomem?*SW_MUX_GPIO1_IO03; static?void?__iomem?*SW_PAD_GPIO1_IO03; static?void?__iomem?*GPIO1_DR; static?void?__iomem?*GPIO1_GDIR; /* ?*?@description??:?LED打開/關閉 ?*?@param?-?sta??:?LEDON(0)?打開LED,LEDOFF(1)?關閉LED ?*?@return????:?無 ?*/ void?led_switch(u8?sta) { ?u32?val?=?0; ?if(sta?==?LEDON)?{ ??val?=?readl(GPIO1_DR); ??val?&=?~(1?<3);? ??writel(val,?GPIO1_DR); ?}else?if(sta?==?LEDOFF)?{ ??val?=?readl(GPIO1_DR); ??val|=?(1?<3);? ??writel(val,?GPIO1_DR); ?}? } /* ?*?@description??:?打開設備 ?*?@param?-?inode??:?傳遞給驅動的inode ?*?@param?-?filp??:?設備文件,file結構體有個叫做private_data的成員變量 ?*????????一般在open的時候將private_data指向設備結構體。 ?*?@return????:?0?成功;其他?失敗 ?*/ static?int?led_open(struct?inode?*inode,?struct?file?*filp) { ?return?0; } /* ?*?@description??:?從設備讀取數據? ?*?@param?-?filp??:?要打開的設備文件(文件描述符) ?*?@param?-?buf??:?返回給用戶空間的數據緩沖區 ?*?@param?-?cnt??:?要讀取的數據長度 ?*?@param?-?offt??:?相對于文件首地址的偏移 ?*?@return????:?讀取的字節數,如果為負值,表示讀取失敗 ?*/ static?ssize_t?led_read(struct?file?*filp,?char?__user?*buf,?size_t?cnt,?loff_t?*offt) { ?return?0; } /* ?*?@description??:?向設備寫數據? ?*?@param?-?filp??:?設備文件,表示打開的文件描述符 ?*?@param?-?buf??:?要寫給設備寫入的數據 ?*?@param?-?cnt??:?要寫入的數據長度 ?*?@param?-?offt??:?相對于文件首地址的偏移 ?*?@return????:?寫入的字節數,如果為負值,表示寫入失敗 ?*/ static?ssize_t?led_write(struct?file?*filp,?const?char?__user?*buf,?size_t?cnt,?loff_t?*offt) { ?int?retvalue; ?unsigned?char?databuf[1]; ?unsigned?char?ledstat; ?retvalue?=?copy_from_user(databuf,?buf,?cnt); ?if(retvalue?0)?{ ??printk("kernel?write?failed! "); ??return?-EFAULT; ?} ?ledstat?=?databuf[0];??/*?獲取狀態值?*/ ?if(ledstat?==?LEDON)?{? ??led_switch(LEDON);??/*?打開LED燈?*/ ?}?else?if(ledstat?==?LEDOFF)?{ ??led_switch(LEDOFF);?/*?關閉LED燈?*/ ?} ?return?0; } /* ?*?@description??:?關閉/釋放設備 ?*?@param?-?filp??:?要關閉的設備文件(文件描述符) ?*?@return????:?0?成功;其他?失敗 ?*/ static?int?led_release(struct?inode?*inode,?struct?file?*filp) { ?return?0; } /*?設備操作函數?*/ static?struct?file_operations?led_fops?=?{ ?.owner???=?THIS_MODULE, ?.open???=?led_open, ?.read???=?led_read, ?.write???=?led_write, ?.release?=?led_release, }; /* ?*?@description?:?驅動出口函數 ?*?@param???:?無 ?*?@return???:?無 ?*/ static?int?__init?led_init(void) { ?int?retvalue?=?0; ?u32?val?=?0; ?/*?初始化LED?*/ ?/*?1、寄存器地址映射?*/ ?IMX6U_CCM_CCGR1?=?ioremap(CCM_CCGR1_BASE,?4); ?SW_MUX_GPIO1_IO03?=?ioremap(SW_MUX_GPIO1_IO03_BASE,?4); ?SW_PAD_GPIO1_IO03?=?ioremap(SW_PAD_GPIO1_IO03_BASE,?4); ?GPIO1_DR?=?ioremap(GPIO1_DR_BASE,?4); ?GPIO1_GDIR?=?ioremap(GPIO1_GDIR_BASE,?4); ?/*?2、使能GPIO1時鐘?*/ ?val?=?readl(IMX6U_CCM_CCGR1); ?val?&=?~(3?<26);?/*?清楚以前的設置?*/ ?val?|=?(3?<26);?/*?設置新值?*/ ?writel(val,?IMX6U_CCM_CCGR1); ?/*?3、設置GPIO1_IO03的復用功能,將其復用為 ??*??? GPIO1_IO03,最后設置IO屬性。 ??*/ ?writel(5,?SW_MUX_GPIO1_IO03); ? ?/*寄存器SW_PAD_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?低轉換率 ??*/ ?writel(0x10B0,?SW_PAD_GPIO1_IO03); ?/*?4、設置GPIO1_IO03為輸出功能?*/ ?val?=?readl(GPIO1_GDIR); ?val?&=?~(1?<3);?/*?清除以前的設置?*/ ?val?|=?(1?<3);?/*?設置為輸出?*/ ?writel(val,?GPIO1_GDIR); ?/*?5、默認關閉LED?*/ ?val?=?readl(GPIO1_DR); ?val?|=?(1?<3);? ?writel(val,?GPIO1_DR); ?/*?6、注冊字符設備驅動?*/ ?retvalue?=?register_chrdev(LED_MAJOR,?LED_NAME,?&led_fops); ?if(retvalue?0){ ??printk("register?chrdev?failed! "); ??return?-EIO; ?} ?return?0; } /* ?*?@description?:?驅動出口函數 ?*?@param???:?無 ?*?@return???:?無 ?*/ static?void?__exit?led_exit(void) { ?/*?取消映射?*/ ?iounmap(IMX6U_CCM_CCGR1); ?iounmap(SW_MUX_GPIO1_IO03); ?iounmap(SW_PAD_GPIO1_IO03); ?iounmap(GPIO1_DR); ?iounmap(GPIO1_GDIR); ?/*?注銷字符設備驅動?*/ ?unregister_chrdev(LED_MAJOR,?LED_NAME); } module_init(led_init); module_exit(led_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("zhiguoxin"); ?
有了上面的講解,代碼很簡單就不用多說了,就是按照那7步來操作的。
8.2 編寫測試程序
?
#include?"stdio.h" #include?"unistd.h" #include?"sys/types.h" #include?"sys/stat.h" #include?"fcntl.h" #include?"stdlib.h" #include?"string.h" /*************************************************************** 使用方法??: ./ledtest?/dev/led??0???關閉LED ./ledtest?/dev/led??1???打開LED?? ***************************************************************/ #define?LEDOFF??0 #define?LEDON??1 /* ?*?@description??:?main主程序 ?*?@param?-?argc??:?argv數組元素個數 ?*?@param?-?argv??:?具體參數 ?*?@return????:?0?成功;其他?失敗 ?*/ int?main(int?argc,?char?*argv[]) { ?int?fd,?retvalue; ?char?*filename; ?unsigned?char?databuf[1]; ? ?if(argc?!=?3){ ??printf("Error?Usage! "); ??return?-1; ?} ?filename?=?argv[1]; ?/*?打開led驅動?*/ ?fd?=?open(filename,?O_RDWR); ?if(fd?0){ ??printf("file?%s?open?failed! ",?argv[1]); ??return?-1; ?} ?databuf[0]?=?atoi(argv[2]);?/*?要執行的操作:打開或關閉?*/ ?/*?向/dev/led文件寫入數據?*/ ?retvalue?=?write(fd,?databuf,?sizeof(databuf)); ?if(retvalue?0){ ??printf("LED?Control?Failed! "); ??close(fd); ??return?-1; ?} ?retvalue?=?close(fd);?/*?關閉文件?*/ ?if(retvalue?0){ ??printf("file?%s?close?failed! ",?argv[1]); ??return?-1; ?} ?return?0; }?
測試程序就很簡單了,不用多說。
3 8.編寫Makefile
?
KERNELDIR?:=?/home/zhiguoxin/linux/IMX6ULL/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek CURRENT_PATH?:=?$(shell?pwd) obj-m?:=?led.o build:?kernel_modules kernel_modules: ?$(MAKE)?-C?$(KERNELDIR)?M=$(CURRENT_PATH)?modules ?$(CROSS_COMPILE)arm-linux-gnueabihf-gcc?-o?ledApp?ledApp.c? clean: ?$(MAKE)?-C?$(KERNELDIR)?M=$(CURRENT_PATH)?clean?
第1行,KERNELDIR表示開發板所使用的Linux內核源碼目錄,使用絕對路徑,大家根據自己的實際情況填寫。
第2行,CURRENT_PATH表示當前路徑,直接通過運行pwd命令來獲取當前所處路徑。
第3行,obj-m表示將led.c這個文件編譯為led.ko模塊。
第8行,具體的編譯命令,后面的modules表示編譯模塊,-C表示將當前的工作目錄切換到指定目錄中,也就是KERNERLDIR目錄。M表示模塊源碼目錄,make modules命令中加入M=dir以后程序會自動到指定的 dir 目錄中讀取模塊的源碼并將其編譯為.ko 文件。
第9行,使用交叉編譯工具鏈將ledApp.c編譯成可以在arm板子上運行的ledApp可執行文件。
Makefile 編寫好以后輸入make命令編譯驅動模塊,編譯過程如圖所示
九、運行測試
9.1 上傳程序到開發板執行
開發板啟動后通過NFS掛載Ubuntu目錄的方式,將相應的文件拷貝到開發板上。簡單來說,就是通過NFS在開發板上通過網絡直接訪問ubuntu虛擬機上的文件,并且就相當于自己本地的文件一樣。
因為我的代碼都放在/home/zhiguoxin/myproject/alientek_drv_development_source這個目錄下,所以我們將這個目錄作為NFS共享文件夾。
Ubuntu IP為192.168.10.100,一般都是掛載在開發板的mnt目錄下,這個目錄是專門用來給我們作為臨時掛載的目錄。
文件系統目錄簡介
然后使用MobaXterm軟件通過SSH訪問開發板。
?
ubuntu?ip:192.168.10.100 windows?ip:192.168.10.200 開發板ip:192.168.10.50?
在開發板上執行以下命令就可以實現掛載了:
?
mount?-t?nfs?-o?nolock,vers=3?192.168.10.100:/home/zhiguoxin/myproject/alientek_drv_development_source?/mnt?
就將開飯的mnt目錄掛載在ubuntu的/home/zhiguoxin/myproject/alientek_drv_development_source目錄下了。這樣我們就可以在Ubuntu下修改文件,然后可以直接在開發板上執行可執行文件了。當然我這里的/home/zhiguoxin/myproject/和windows之間是一個共享目錄,我也可以直接在windows上面修改文件,然后ubuntu和開發板直接進行文件同步了。
9.2 加載驅動模塊
驅動模塊led.ko和ledApp可執行文件都已經準備好了,接下來就是運行測試。這里我是用掛載的方式將服務端的項目文件夾掛載到arm板的mnt目錄,進入到/mnt/02_led目錄輸入如下命令加載led.ko驅動文件:
?
insmod?led.ko?
9.3 創建設備節點文件
驅動加載成功需要在/dev目錄下創建一個與之對應的設備節點文件,應用程序就是通過操作這個設備節點文件來完成對具體設備的操作。輸入如下命令創建/dev/led這個設備節點文件:
?
mknod?/dev/led?c?200?0?
其中mknod是創建節點命令,/dev/hello_drv 是要創建的節點文件,c表示這是個字符設備,200是設備的主設備號,0是設備的次設備號。創建完成以后就會存在/dev/led這個文件,可以使用ls /dev/led-l命令查看。
9.3 led設備操作測試
一切準備就緒。使用ledtest 軟件操作led這個設備,看看是否可以正常打開或關閉led。
?
./ledApp?/dev/led??0???關閉LED ./ledApp?/dev/led??1???打開LED??
9.4 ?卸載驅動模塊
如果不再使用某個設備的話可以將其驅動卸載掉,比如輸入如下命令卸載掉hello_drv這個設備:
?
rmmod?led.ko?
卸載以后使用lsmod命令查看led這個模塊還存不存在:
可以看出,此時系統已經沒有任何模塊了,led這個模塊也不存在了,說明模塊卸載成功。而且系統中也沒有了led這個設備。
至此,led這個設備的整個驅動就驗證完成了,驅動工作正常。以后的字符設備驅動實驗基本都可以此為模板進行編寫。
?
審核編輯:湯梓紅
評論
查看更多