1. 程式人生 > >嵌入式Linux學習筆記(三) 字元型裝置驅動--LED的驅動開發

嵌入式Linux學習筆記(三) 字元型裝置驅動--LED的驅動開發

  在成功構建了一個能夠執行在開發板平臺的系統後,下一步就要正式開始應用的開發(這裡前提是有一定的C語言基礎,對ARM體系的軟/硬體,這部分有疑問可能要參考其它教程),根據需求仔細分解任務,可以發現包含的外設有LED,BEEP,RS232,六軸感測(SPI介面),光環境感測器(I2C),音訊輸出, RTC等,如果按照這個順序去實現驅動,一定程度其實又迴歸最初的模組學習的策略,不過既然是從應用的角度,先實現應用框架,來驗證是否符合預期,這比測試模組驅動的更重要,也更容易有產出感。 按照這個需求,就可以先把實際工作分解為如下幾個步驟:

  1.完成LED驅動,能夠正常控制LED的點亮和關閉(本節完成)

  2.完成RS232的驅動,能夠實現串列埠的通訊

  3.定義一套上位機、下位機之間的通訊協議(也可以使用主流工業協議如Modbus), 並在上位機和下位機編碼實現通訊協議的組包和解包

  4.實現一套介面化的上位機工具,帶有除錯功能和控制功能

  既然初步的工作已經清晰,就可以開始第一步的工作,完成LED的驅動。

參考資料

  1. 開發板原理圖 《IMX6UL_ALPHA_V2.0(底板原理圖)》 《IMX6ULL_CORE_V1.4(核心板原理圖)》 

  2. 正點原子《Linux驅動開發指南說明V1.0》 第四十章 字元驅動裝置開發

  3. 宋寶華 《Linux裝置驅動開發詳解:基於最新的Linux 4.0核心》 第六章 字元驅動裝置

  4. 恩智浦官方手冊 《IMX6ULL參考手冊》Chapter 18:Clock Controller Module(CCM)/Chapter 28:General Purpose Input/Output (GPIO)

LED硬體配置實現

  首先當然要確定原理圖,下圖來自底板和核心板原理圖。

  通過追蹤就可以檢視當前使用LED的引腳為GPIO1_IO3。

  確定硬體後,第一步就是配置GPIO需要使用的暫存器了,對於使用過微控制器的使用者來說,對於GPIO這類外設,一般包含以下步驟:

  1. 使能模組時鐘

  2. 配置模組或者相關模組的暫存器,使模組複用到需要的功能

  3. 提供對外訪問的介面

  對於嵌入式Linux來說,這部分也沒有區別,硬體初始化介面(具體暫存器可使用《IMX6ULL參考手冊》查詢)

 1 /**
 2  * LED硬體初始化,引腳GPIO1_IO03
 3  * 
 4  * @param NULL
 5  *
 6  * @return NULL
 7  */
 8 static void led_gpio_init(void)
 9 {
10     u32  value;
11 
12     /*1. 暫存器地址對映*/
13     IMX6U_CCM_CCGR1 = ioremap(0X020C406C, 4);     //時鐘使能 
14     SW_MUX_GPIO1_IO03 = ioremap(0X020E0068, 4);   //複用功能設定
15     SW_PAD_GPIO1_IO03 = ioremap(0X020E02F4, 4);   //設定PAD的輸出狀態
16     GPIO1_DR = ioremap(0X0209C000, 4);            //設定LED輸出
17     GPIO1_GDIR = ioremap(0X0209C004, 4);          //設定GPIO的狀態
18 
19     /*2.時鐘使能*/
20     value = readl(IMX6U_CCM_CCGR1);
21     value &= ~(3 << 26);    
22     value |= (3 << 26);
23     writel(value, IMX6U_CCM_CCGR1);
24     printk("led write 0");
25 
26     /*3.複用功能設定*/
27     writel(5, SW_MUX_GPIO1_IO03);
28 
29     /*4.引腳IO功能設定*/
30     writel(0x10B0, SW_PAD_GPIO1_IO03);
31 
32     /*5.引腳輸出功能配置*/
33     value = readl(GPIO1_GDIR);
34     value |= (1 << 3);    /* 設定新值 */
35     writel(value, GPIO1_GDIR); 
36 
37     /*5.關閉LED顯示,高電平關閉*/
38     value = readl(GPIO1_DR);
39     value |= (1 << 3);    
40     writel(value, GPIO1_DR);
41 
42     printk(KERN_INFO"led hardware init ok\r\n");
43 }

  硬體資源釋放.

 1 /**
 2  * 釋放硬體資源
 3  * 
 4  * @param NULL
 5  *
 6  * @return NULL
 7  */
 8 static void led_gpio_release(void)
 9 {
10     iounmap(IMX6U_CCM_CCGR1);
11     iounmap(SW_MUX_GPIO1_IO03);
12     iounmap(SW_PAD_GPIO1_IO03);
13     iounmap(GPIO1_DR);
14     iounmap(GPIO1_GDIR);
15 }

  硬體裝置管理

 1 /**
 2  *LED燈開關切換
 3  * 
 4  * @param status  LED開關狀態,1開啟,0關閉
 5  *
 6  * @return NULL
 7  */
 8 static void led_switch(u8 status)
 9 {
10     u32 value;
11     value = readl(GPIO1_DR);
12 
13     switch(status)
14     {
15         case LED_OFF:
16             printk(KERN_INFO"led off\r\n");
17             value |= (1 << 3);    
18             writel(value, GPIO1_DR);
19             break;
20         case LED_ON:
21             printk(KERN_INFO"led on\r\n");
22             value &= ~(1 << 3);    
23             writel(value, GPIO1_DR);
24             break;
25         default:
26             printk(KERN_INFO"Invalid LED Set");
27             break;
28     }
29 }

至此,我們就實現了和硬體執行的介面

led_gpio_init()/led_gpio_release()/led_switch(n)

嵌入式核心模組實現

  嵌入式核心模組的參考本系列的第一篇檔案,主要提供載入到Linux核心,用於insmod和rmmod訪問的介面,這部分因為已經講過,如果希望理解就去看第一節內容,或者參考上面提供的資料。

  Linux載入的介面:

 1 /**
 2  * 驅動入口函式
 3  * 
 4  * @param NULL
 5  *
 6  * @return the error code, 0 on initialization successfully.
 7  */
 8 static int __init led_module_init(void)
 9 {
10     //此處新增設備註冊的實現
11     //......
12 }
13 module_init(led_module_init); 

  Linux釋放的介面:

 1 /**
 2  * 驅動釋放函式
 3  * 
 4  * @param NULL
 5  *
 6  * @return the error code, 0 on release successfully.
 7  */
 8 static void __exit led_module_exit(void)
 9 {
10    //此處新增設備註銷的實現
11    //......    
12 }
13 module_exit(led_module_exit);

  此外,在新增驅動說明,如作者,許可證和驅動說明等

1 MODULE_AUTHOR("zc");                          //模組作者
2 MODULE_LICENSE("GPL v2");                     //模組許可協議
3 MODULE_DESCRIPTION("led driver");             //模組許描述
4 MODULE_ALIAS("led_driver");                   //模組別名

  至此本節的準備工作全部完成,下面就開始完成總線上裝置的建立,這也是本章最核心的特徵。

裝置建立和釋放

  裝置建立如果按照固定的結構,使用起來雖然有些困難,如果按照官方流程來實現,是有跡可循的。但是如何從應用層的訪問介面open,read,write,close到底層驅動的xxx_open, xxx_read, xxx_write, xxxx_close的呼叫,這部分的理解在整個驅動機制的重要部分,這部分的難度當然不是一次可以講清楚的,這裡先拋磚引玉,在後面驅動的實踐中會步步深入去理解。

  作為熟悉C語言知識的開發者來說,可以很清楚open這一類介面是用來訪問檔案的,而在Linux中,字元型裝置和塊裝置就體現了"一切都是檔案"的思想,參考《Linux裝置驅動開發詳解:基於最新的Linux 4.0核心》第5章的說明,

通關VFS(virtual Filesytem), 將上層介面操作/dev/*下的裝置檔案,最後訪問到驅動內部註冊的實際操作硬體的介面。

 

想理解這部分知識,就需要理解應用層介面做了什麼工作,參考這篇文章,https://www.jianshu.com/p/f3f5a33f2c59,以open為例。

open函式,這裡可以簡述步驟(下面所有實現在linux/fs/namei.c檔案中)

1.獲取一個可用的id,用於外部的記錄,如fd

2.根據name名稱如"/dev/led"獲取file指標資訊,包含裝置的實際資訊

3.將fd與file關聯起來,後續就可以通關fd直接訪問file指標的內容(裝置端資訊指標file),至此我們就獲取裝置端的資訊

4.建立inode型別的資料nd,這部分就是VFS中連結到真正驅動的位置資訊,其中包含的cdev *i_cdev即是和裝置相關的指標,至於這部分如何連結到實際裝置,等後續深入瞭解後在詳細瞭解。

5.file和nd的連結則依靠file->f_path.mnt和nd->path.mnt配置相等實現

到達這一步,當然還遠遠不夠,但目前只是初步入門,先不過度深入,下面開始驅動編寫。其中在module_init中主要完成註冊流程,module_exit中完成釋放流程,此外還要實現訪問LED的介面,具體如下:

  1.訪問LED的硬體介面連結

  1 /**
  2  * 獲取LED資源
  3  * 
  4  * @param inode    
  5  * @param filp
  6  *
  7  * @return the error code, 0 on initialization successfully.
  8  */
  9 int led_open(struct inode *inode, struct file *filp)
 10 {
 11     filp->private_data = &led_driver_info;
 12     return 0;
 13 }
 14 
 15 /**
 16  * 釋放LED裝置資源
 17  * 
 18  * @param inode
 19  * @param filp
 20  * 
 21  * @return the error code, 0 on initialization successfully.
 22  */
 23 int led_release(struct inode *inode, struct file *filp)
 24 {
 25     return 0;
 26 }
 27 
 28 /**
 29  * 從LED裝置讀取資料
 30  * 
 31  * @param filp
 32  * @param buf
 33  * @param count
 34  * @param f_ops
 35  *
 36  * @return the error code, 0 on initialization successfully.
 37  */
 38 ssize_t led_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
 39 {
 40     return 0;
 41 }
 42 
 43 /**
 44  * 向LED裝置寫入資料
 45  * 
 46  * @param filp
 47  * @param buf
 48  * @param count
 49  * @param f_ops
 50  *
 51  * @return the error code, 0 on initialization successfully.
 52  */
 53 ssize_t led_write(struct file *filp, const char __user *buf, size_t count,  loff_t *f_pos)
 54 {
 55     int result;
 56     u8 databuf[2];
 57 
 58     result = copy_from_user(databuf, buf, count);
 59     if(result < 0) {
 60         printk(KERN_INFO"kernel write failed!\r\n");
 61         return -EFAULT;
 62     }
 63     
 64     /*利用資料操作LED*/
 65     led_switch(databuf[0]);
 66     return 0;
 67 }
 68 
 69 /**
 70  * light從裝置讀取狀態
 71  * 
 72  * @param filp
 73  * @param cmd
 74  * @param arg
 75  *  
 76  * @return the error code, 0 on initialization successfully.
 77  */
 78 long led_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
 79 {
 80     switch(cmd){
 81         case 0:
 82             led_switch(0);
 83             break;
 84         case 1:
 85             led_switch(1);
 86             break;
 87         default:
 88             printk(KERN_INFO"Invalid Cmd!\r\n");
 89             return -ENOTTY;
 90     }
 91 
 92     return 0;
 93 }
 94 
 95 /* 裝置操作函式 */
 96 static struct file_operations led_fops = {
 97     .owner = THIS_MODULE,
 98     .open = led_open,
 99     .read = led_read,
100     .write = led_write,
101     .unlocked_ioctl = led_ioctl,
102     .release = led_release,
103 };

2.建立裝置,新增到裝置總線上,這裡要提到知識點,

  對於一個裝置的基本id,由主裝置號和子裝置號組成,其中主裝置就是掛載在/proc/devices下的裝置總線上,如果裝置已經存在,則可以用register_chdev_region直接生成裝置資訊,則需要使用alloc_chrdev_region申請新的裝置資訊。

  在獲取裝置資訊結構後,可通過cdev_init將cdev,裝置號以及上面的硬體操作介面函式連結起來。

  最後通過cdev_add將裝置資訊掛載到裝置總線上,這時通過cat /proc/devices就可以檢視裝置是否新增成功。

 1     int result;
 2 
 3     led_driver_info.major = DEFAULT_MAJOR;
 4     led_driver_info.minor = DEFAULT_MINOR;
 5 
 6     /*在總線上建立裝置*/    
 7     /*1.申請字元裝置號*/
 8     if(led_driver_info.major){
 9         led_driver_info.dev_id = MKDEV(led_driver_info.major, led_driver_info.minor);
10         result = register_chrdev_region(led_driver_info.dev_id, DEVICE_LED_CNT, DEVICE_LED_NAME);
11     }
12     else{
13         result = alloc_chrdev_region(&led_driver_info.dev_id, 0, DEVICE_LED_CNT, DEVICE_LED_NAME);
14         led_driver_info.major = MAJOR(led_driver_info.dev_id);
15         led_driver_info.minor = MINOR(led_driver_info.dev_id);
16     }
17     if(result < 0){
18         printk(KERN_INFO"dev alloc or set failed\r\n");    
19         return result;
20     }
21     else{
22         printk(KERN_INFO"dev alloc or set ok, major:%d, minor:%d\r\n", led_driver_info.major,  led_driver_info.minor);    
23     }
24     
25     /*2.新增裝置到相應總線上*/
26     cdev_init(&led_driver_info.cdev, &led_fops);
27     led_driver_info.cdev.owner = THIS_MODULE;
28     result = cdev_add(&led_driver_info.cdev, led_driver_info.dev_id, DEVICE_LED_CNT);
29     if(result != 0){
30         unregister_chrdev_region(led_driver_info.dev_id, DEVICE_LED_CNT);
31         printk(KERN_INFO"cdev add failed\r\n");
32         return result;
33     }else{
34         printk(KERN_INFO"device add Success!\r\n");    
35     }

3.在/dev/下根據裝置號建立裝置節點,用於應用上層介面的訪問,這部分和mknod /dev/led c 主裝置號 從裝置號功能一致,理論使用指令也可,具體如下。

 1 /* 4、建立類 */
 2     led_driver_info.class = class_create(THIS_MODULE, DEVICE_LED_NAME);
 3     if (IS_ERR(led_driver_info.class)) {
 4         printk(KERN_INFO"class create failed!\r\n");
 5         unregister_chrdev_region(led_driver_info.dev_id, DEVICE_LED_CNT);
 6         cdev_del(&led_driver_info.cdev);    
 7         return PTR_ERR(led_driver_info.class);
 8     }
 9     else{
10         printk(KERN_INFO"class create successed!\r\n");
11     }
12 
13     /* 5、建立裝置 */
14     led_driver_info.device = device_create(led_driver_info.class, NULL, led_driver_info.dev_id, NULL, DEVICE_LED_NAME);
15     if (IS_ERR(led_driver_info.device)) {
16         printk(KERN_INFO"device create failed!\r\n");
17                 unregister_chrdev_region(led_driver_info.dev_id, DEVICE_LED_CNT);       
18                 cdev_del(&led_driver_info.cdev);
19         
20         class_destroy(led_driver_info.class);
21         return PTR_ERR(led_driver_info.device);
22     }
23     else{
24         printk(KERN_INFO"device create successed!\r\n");
25     }
26 
27     /*硬體初始化*/
28     led_gpio_init();

  至此,建立裝置並新增到裝置匯流排的流程實現完畢,這就是module_init中需要的所有實現。

2.釋放模組

  在上面我們建立裝置,佔用了系統資源,在解除安裝模組的時候,這些都要全部釋放,不然就會造成記憶體的洩露,具體如下。

 1 /**
 2  * 驅動釋放函式
 3  * 
 4  * @param NULL
 5  *
 6  * @return the error code, 0 on release successfully.
 7  */
 8 static void __exit led_module_exit(void)
 9 {
10     /* 登出字元裝置驅動 */
11     device_destroy(led_driver_info.class, led_driver_info.dev_id);
12     class_destroy(led_driver_info.class);
13 
14     cdev_del(&led_driver_info.cdev);
15     unregister_chrdev_region(led_driver_info.dev_id, DEVICE_LED_CNT);
16 
17     /*硬體資源釋放*/
18     led_gpio_release();
19 }
20 module_exit(led_module_exit);

測試程式碼實現  

  在上面驅動程式碼就已經實現,但對於應用來說,實現驅動並不是結束,我們還要完成測試單元,但驅動的有效性進行測試,這部分因為並不是嚴格的工業化專案,所以只做簡單的測試,程式碼如下

 1 #include<unistd.h>
 2 #include<sys/types.h>
 3 #include<sys/stat.h>
 4 #include<fcntl.h>
 5 #include<stdio.h>
 6 
 7 /**
 8  * 測試LED工作
 9  * 
10  * @param NULL
11  *
12  * @return NULL
13  */
14 int main(int argc, const char *argv[])
15 {
16     unsigned char val = 1;
17     int fd;
18 
19     fd = open("/dev/led", O_RDWR | O_NDELAY);
20     if(fd == -1)
21     {
22         printf("/dev/led open error");
23         return -1;
24     }
25 
26     if(argc > 1){   
27         val = atoi(argv[1]);
28     }
29 
30     write(fd, &val, 1);
31 
32     close(fd);   
33 }

Makefile實現

  Makefile的語法也是嵌入式Linux開發中重要知識,如果沒有對bash語法有深刻的認識,且理解編譯原理的那部分知識,這部分其實也十分困難,這也不是三兩句可以說清楚的,等積累一段時間後專門用筆記講解這部分內容,初步能大致看懂,修改會編譯就夠了。

KERNELDIR := /usr/code/linux
CURRENT_PATH := $(shell pwd)
obj-m := led.o

build: kernel_modules

kernel_modules:
    $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
    $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

  儲存為Makefile後,使用make指令,就可以編譯生成需要的led.ko檔案,此外通過

  arm-linux-gnueabihf-gcc -o led_test led_test.c也可以生成我們需要的測試檔案。

檔案上傳和執行

  可通過sd卡,ssh或者nfs系統,將上述檔案新增到上章編譯完成的系統中,

  執行insmod /usr/driver/led.ko將驅動載入

  執行lsmod查詢當前載入的驅動

  通過./usr/app/led_test 1或者./usr/app/led_test 0控制LED的點亮和關閉,現象如下:

  

總結

  至此,關於LED的驅動開發基本講解完成,雖然開發參考了部分例程用了不到2個小時,但完成這篇文件用了4個小時,為了能夠將知識可以解決出來,去查詢書籍,以及去檢視核心程式碼,但是這是值得的,我感覺對驅動有了更深刻的認知,但我認為這是值得的,下節將開始Uart驅動的編寫實現,整個流程算走上了正軌,不過我本身還要工作,這是因為五一才有這種效率更新,不過我已經制定了計劃,希望能夠順利的去學習