Linux驅動開發野火實戰(一):LED控制驅動詳解
文章目錄
- Linux驅動開發野火實戰(一):LED控制驅動詳解
- 引言
- 一、基礎知識
- 1.1 什么是字符設備驅動
- 1.2 重要的數據結構
- read 函數
- write 函數
- open 函數
- release 函數
- 二、 驅動程序實現
- 2.1 完整的驅動代碼示例
- 2.2 整體流程(圖解)
- 2.3 用戶空間與內核空間交互(圖解)
- 2.4 驅動模塊初始化
- 虛擬地址映射
- 2.5 拷貝數據
- 2.6 控制GPIO輸出的LED開關狀態
- 2.7 LED驅動程序的退出函數
- 三、實驗過程
- 項目編譯
- 連接開發板
- 掛載NFS文件系統
- 加載驅動(點燈!)
- 總結
引言
在Linux設備驅動開發中,字符設備驅動是最基礎也是最常見的驅動類型。本文將從理論到實踐,詳細講解字符設備驅動的開發流程,幫助讀者掌握驅動開發的核心知識
一、基礎知識
1.1 什么是字符設備驅動
字符設備(Character Device)是Linux中最基本的設備類型之一,它的特點是數據以字符流的方式被訪問,像串口、鍵盤、LED等都屬于字符設備。與塊設備不同,字符設備不能隨機訪問,只能順序讀寫。
1.2 重要的數據結構
在開發字符設備驅動之前,我們需要了解幾個關鍵的數據結構:
struct file_operations {struct module *owner;ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);int (*open) (struct inode *, struct file *);int (*release) (struct inode *, struct file *);...
};
這個結構體定義了驅動程序需要實現的接口函數。
read 函數
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
作用: 響應用戶空間的讀取請求
參數:
- filp:文件結構體指針
- buf:用戶空間緩沖區指針
- cnt:要讀取的字節數
- offt:文件位置指針
返回值: - 成功返回實際讀取的字節數
- 失敗返回負錯誤碼
- 使用場景:
- 讀取設備狀態
- 獲取設備數據
- 將數據從內核空間復制到用戶空間
write 函數
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
作用: 響應用戶空間的寫入請求
參數:
- filp:文件結構體指針
- buf:用戶空間數據緩沖區指針
- cnt:要寫入的字節數
- offt:文件位置指針
返回值: - 成功返回實際寫入的字節數
- 失敗返回負錯誤碼
使用場景: - 向設備發送控制命令
- 更新設備狀態
- 將數據從用戶空間復制到內核空間
open 函數
static int led_open(struct inode *inode, struct file *filp)
作用: 當用戶空間調用 open() 打開設備文件時被調用
參數:
- inode:包含文件系統信息的結構體,如設備號等
- filp:文件結構體,包含文件操作方法、私有數據等
返回值: - 成功返回0
- 失敗返回負錯誤碼
使用場景: - 初始化設備
- 檢查設備狀態
- 分配資源
- 增加使用計數
release 函數
static int led_release(struct inode *inode, struct file *filp)
作用: 當最后一個打開的文件被關閉時調用
參數:
- inode:索引節點結構體指針
- filp:文件結構體指針
返回值: - 成功返回0
- 失敗返回負錯誤碼
使用場景: - 釋放資源
- 清理設備狀態
- 減少使用計數
二、 驅動程序實現
2.1 完整的驅動代碼示例
代碼如下:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>#include <linux/fs.h>
#include <linux/uaccess.h>
#include <asm/io.h>#define DEV_MAJOR 0 /* 動態申請主設備號 */
#define DEV_NAME "red_led" /*led設備名字 *//* GPIO虛擬地址指針 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO04;
static void __iomem *SW_PAD_GPIO1_IO04;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;static int led_open(struct inode *inode, struct file *filp)
{return 0;
}static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{return -EFAULT;
}static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{unsigned char databuf[10];if (cnt > 10)cnt = 10;/*從用戶空間拷貝數據到內核空間*/if (copy_from_user(databuf, buf, cnt)){return -EIO;}if (!memcmp(databuf, "on", 2)){iowrite32(0 << 4, GPIO1_DR);}else if (!memcmp(databuf, "off", 3)){iowrite32(1 << 4, GPIO1_DR);}/*寫成功后,返回寫入的字數*/return cnt;
}static int led_release(struct inode *inode, struct file *filp)
{return 0;
}/* 自定義led的file_operations 接口*/
static struct file_operations led_fops = {.owner = THIS_MODULE,.open = led_open,.read = led_read,.write = led_write,.release = led_release,
};int major = 0;
static int __init led_init(void)
{/* GPIO相關寄存器映射 */IMX6U_CCM_CCGR1 = ioremap(0x20c406c, 4);SW_MUX_GPIO1_IO04 = ioremap(0x20e006c, 4);SW_PAD_GPIO1_IO04 = ioremap(0x20e02f8, 4);GPIO1_GDIR = ioremap(0x0209c004, 4);GPIO1_DR = ioremap(0x0209c000, 4);/* 使能GPIO1時鐘 */iowrite32(0xffffffff, IMX6U_CCM_CCGR1);/* 設置GPIO1_IO04復用為普通GPIO*/iowrite32(5, SW_MUX_GPIO1_IO04);/*設置GPIO屬性*/iowrite32(0x10B0, SW_PAD_GPIO1_IO04);/* 設置GPIO1_IO04為輸出功能 */iowrite32(1 << 4, GPIO1_GDIR);/* LED輸出高電平 */iowrite32(1 << 4, GPIO1_DR);/* 注冊字符設備驅動 */major = register_chrdev(DEV_MAJOR, DEV_NAME, &led_fops);printk(KERN_ALERT "led major:%d\n", major);return 0;
}static void __exit led_exit(void)
{/* 取消映射 */iounmap(IMX6U_CCM_CCGR1);iounmap(SW_MUX_GPIO1_IO04);iounmap(SW_PAD_GPIO1_IO04);iounmap(GPIO1_DR);iounmap(GPIO1_GDIR);/* 注銷字符設備驅動 */unregister_chrdev(major, DEV_NAME);
}module_init(led_init);
module_exit(led_exit);MODULE_LICENSE("GPL2");
MODULE_AUTHOR("embedfire ");
MODULE_DESCRIPTION("led_module");
MODULE_ALIAS("led_module");
2.2 整體流程(圖解)
2.3 用戶空間與內核空間交互(圖解)
2.4 驅動模塊初始化
虛擬地址映射
- ioremap 函數
void __iomem *ioremap(unsigned long phys_addr, unsigned long size);
作用: 將物理地址映射到虛擬地址空間
參數:
- phys_addr:物理地址
- size:映射的大小(字節數)
返回值: 映射后的虛擬地址指針 - void * 類型的指針,指向被映射的虛擬地址
- __iomem 主要是用于編譯器的檢查地址在內核空間的有效性
為什么要用: Linux內核出于安全考慮,不允許直接訪問物理地址,必須先映射到虛擬地址
GPIO相關寄存器映射
IMX6U_CCM_CCGR1 = ioremap(0x20c406c, 4);SW_MUX_GPIO1_IO04 = ioremap(0x20e006c, 4);SW_PAD_GPIO1_IO04 = ioremap(0x20e02f8, 4);GPIO1_GDIR = ioremap(0x0209c004, 4);GPIO1_DR = ioremap(0x0209c000, 4);
- 虛擬地址讀寫
void iowrite32(u32 b, void __iomem *addr) //寫入一個雙字(32bit)unsigned int ioread32(void __iomem *addr) //讀取一個雙字(32bit)
/* 使能GPIO1時鐘 */iowrite32(0xffffffff, IMX6U_CCM_CCGR1);/* 設置GPIO1_IO04復用為普通GPIO*/iowrite32(5, SW_MUX_GPIO1_IO04);/*設置GPIO屬性*/iowrite32(0x10B0, SW_PAD_GPIO1_IO04);/* 設置GPIO1_IO04為輸出功能 */iowrite32(1 << 4, GPIO1_GDIR);/* LED輸出高電平 */iowrite32(1 << 4, GPIO1_DR);
- register_chrdev 函數
int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);
作用: 注冊字符設備驅動
參數:
major:主設備號(0表示動態分配)
name:設備名稱
fops:文件操作結構體
次設備號為0,次設備號數量為256
返回值: 成功返回主設備號,失敗返回負值
為什么要用: 向Linux系統注冊一個字符設備,使系統能夠識別和管理該設備
2.5 拷貝數據
copy_from_user函數
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
作用: 將數據從用戶空間復制到內核空間
參數:
- to:內核空間目標地址
- from:用戶空間源地址
- n:復制的字節數
返回值: 成功返回0,失敗返回未復制的字節數
為什么要用: 內核空間和用戶空間是隔離的,需要專門的函數來安全地傳輸數據,要是有野指針會導致整個系統的崩潰,所以是起到一個安全的作用。
2.6 控制GPIO輸出的LED開關狀態
if (!memcmp(databuf, "on", 2)) // 比較是否接收到"on"命令
{iowrite32(0 << 4, GPIO1_DR); // GPIO1_4輸出低電平,LED亮
}
else if (!memcmp(databuf, "off", 3)) // 比較是否接收到"off"命令
{iowrite32(1 << 4, GPIO1_DR); // GPIO1_4輸出高電平,LED滅
}
- memcmp()函數:
int memcmp(const void *str1, const void *str2, size_t n)
// 比較兩個內存區域的前n個字節
// 返回0表示相等
2.7 LED驅動程序的退出函數
static void __exit led_exit(void)
{// 1. 取消IO內存映射iounmap(IMX6U_CCM_CCGR1);iounmap(SW_MUX_GPIO1_IO04);iounmap(SW_PAD_GPIO1_IO04);iounmap(GPIO1_DR);iounmap(GPIO1_GDIR);// 2. 注銷字符設備unregister_chrdev(major, DEV_NAME);
}
- iounmap 函數
void iounmap(void __iomem *addr);
作用: 解除I/O內存映射
參數:
addr: 要解除映射的虛擬地址
為什么要用: 釋放ioremap占用的資源,防止內存泄漏
- unregister_chrdev 函數
void unregister_chrdev(unsigned int major, const char *name);
作用: 注銷字符設備驅動
參數:
major:設備的主設備號
name:設備名稱
為什么要用: 在驅動卸載時清理系統資源
三、實驗過程
項目編譯
然后make
連接開發板
打開手機熱點并連上
讓電腦跟手機在同一個局域網內
-
ubuntu端
-
開發板端
掛載NFS文件系統
sudo mount -t nfs ”NFS服務端IP”:/home/embedfire/workdir /mnt
我們ubuntu的IP為192.168.46.118
所以為
sudo mount -t nfs 192.168.46.118:/home/embedfire/workdir /mnt
掛載成功后進入共享文件夾查看
ubuntu把ko文件復制到共享文件夾中
我們到共享文件夾ls查看
加載驅動(點燈!)
244是設備號(動態分配)
0是次設備號
為什么是0
因為在register_chrdev函數定義了
次設備號在(0~256之間隨便選)
ebf-buster-linux/include/linux/fs.h
static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)
{return __register_chrdev(major, 0, 256, name, fops);
}
創建設備文件
利用echo應用打開燈的命令
利用echo應用關燈的命令
卸載模塊
總結
本文詳細介紹了Linux字符設備驅動的開發流程,包括:
- 基本概念和原理
- 完整的代碼實現
- 詳細的流程圖解
- 實際操作
通過本文的學習,大家應該能夠掌握字符設備驅動的開發方法,并能夠開發簡單的字符設備驅動程序。