嵌入式Linux學習筆記(三) 字符型設備驅動–LED的驅動開發
- 2020 年 5 月 3 日
- 筆記
- 嵌入式Linux學習筆記
在成功構建了一個能夠運行在開發板平台的系統後,下一步就要正式開始應用的開發(這裡前提是有一定的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/*下的設備文件,最後訪問到驅動內部註冊的實際操作硬件的接口。
想理解這部分知識,就需要理解應用層接口做了什麼工作,參考這篇文章,//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驅動的編寫實現,整個流程算走上了正軌,不過我本身還要工作,這是因為五一才有這種效率更新,不過我已經制定了計劃,希望能夠順利的去學習吧。