LED驱动开发
LED驱动开发
前置
真正的Linux驱动
要先实现MMU映射,用虚拟地址操控实际地址
//__iomem 是在编译的时候用的
/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;/* 1、寄存器地址映射 */
IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);//CCM_CCGR1_BASE将这个映射成IMX6U_CCM_CCGR1
val = readl(IMX6U_CCM_CCGR1);
/* 2、使能GPIO1时钟 */
writel(val, IMX6U_CCM_CCGR1); register_chrdev(LED_MAJOR, LED_NAME, &led_fops);把写的函数也指进去了
注意
vscode的c_cpp配置,要多加这几个防止报错
"includePath": [
"${workspaceFolder}/**",
"/home/hqyj/I.MX6ULL/linux_kernel/include",
"/home/hqyj/I.MX6ULL/linux_kernel/arch/arm/include",
"/home/hqyj/I.MX6ULL/linux_kernel/arch/arm/include/generated/",Linux 内核 VFS(虚拟文件系统)的基石
struct inode
inode(索引节点)是文件在磁盘上的静态信息。只要文件存在,它就有唯一的 inode
struct inode 里存的都是文件的“元数据”,比如:
i_mode:文件的类型和权限(是普通文件、目录还是设备文件?可读可写?)i_uid/i_gid:文件的所有者和所属组i_rdev:这是设备驱动的关键字段!如果文件是设备文件,i_rdev里存的就是设备号(主设备号+次设备号)。驱动需要通过它区分同类设备中的第几个(比如你用次设备号 0 还是 1)。i_fop:指向struct file_operations的指针,也就是你填的那张“合同”。i_cdev:对于字符设备,指向struct cdev结构体,直接关联到你的驱动对象。
它负责回答:“这是哪种文件?它对应哪个驱动?权限是什么
struct inode {
umode_t i_mode;
unsigned short i_opflags;
kuid_t i_uid;
kgid_t i_gid;
unsigned int i_flags;
#ifdef CONFIG_FS_POSIX_ACL
struct posix_acl *i_acl;
struct posix_acl *i_default_acl;
#endif
const struct inode_operations *i_op;
struct super_block *i_sb;
struct address_space *i_mapping;
#ifdef CONFIG_SECURITY
void *i_security;
#endif
/* Stat data, not accessed from path walking */
unsigned long i_ino;
/*
* Filesystems may only read i_nlink directly. They shall use the
* following functions for modification:
*
* (set|clear|inc|drop)_nlink
* inode_(inc|dec)_link_count
*/
union {
const unsigned int i_nlink;
unsigned int __i_nlink;
};
dev_t i_rdev;
loff_t i_size;
struct timespec i_atime;
struct timespec i_mtime;
struct timespec i_ctime;
spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */
unsigned short i_bytes;
unsigned int i_blkbits;
blkcnt_t i_blocks;
#ifdef __NEED_I_SIZE_ORDERED
seqcount_t i_size_seqcount;
#endif
/* Misc */
unsigned long i_state;
struct mutex i_mutex;
unsigned long dirtied_when; /* jiffies of first dirtying */
unsigned long dirtied_time_when;
struct hlist_node i_hash;
struct list_head i_wb_list; /* backing dev IO list */
struct list_head i_lru; /* inode LRU list */
struct list_head i_sb_list;
union {
struct hlist_head i_dentry;
struct rcu_head i_rcu;
};
u64 i_version;
atomic_t i_count;
atomic_t i_dio_count;
atomic_t i_writecount;
#ifdef CONFIG_IMA
atomic_t i_readcount; /* struct files open RO */
#endif
const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
struct file_lock_context *i_flctx;
struct address_space i_data;
struct list_head i_devices;
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev;
};
__u32 i_generation;
#ifdef CONFIG_FSNOTIFY
__u32 i_fsnotify_mask; /* all events this inode cares about */
struct hlist_head i_fsnotify_marks;
#endif
void *i_private; /* fs or device private pointer */
};struct file?
struct file 是每次打开文件时动态生成的“会话记录”。同一个文件可以被多个进程打开,产生多个 struct file。
struct file 里存的是“运行时的状态”,比如:
f_flags:打开文件时的标志,如O_RDONLY/O_RDWR。f_mode:通过f_mode & FMODE_READ可判断用户是否以读方式打开。f_pos:当前文件读写位置(lseek就是改这个)。f_op:通常和inode->i_fop指向同一个file_operations结构体。private_data:非常重要的“私有指针”。它是给驱动开发者保存每个会话专属数据的。在open函数里,可以把你的设备结构体地址赋值给filp->private_data,然后在read/write中取出来用。f_inode:反向指向对应的struct inode。
它负责记录:“谁打开的?读写到哪个位置了?驱动想记的临时信息是什么?”
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
/*
* Protects f_ep_links, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
struct mutex f_pos_lock;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;
u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;
#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
struct list_head f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
} __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */
struct file_handle {
__u32 handle_bytes;
int handle_type;
/* file identifier */
unsigned char f_handle[0];
};Linux 驱动 read 和 write 函数的标准接口
static ssize_t led_write(struct file *filp,const char __user *buf,size_t cnt,loff_t *offt)
{
}1. char __user *buf
- 含义:指向用户空间缓冲区的指针。
__user标记:这是一个编译器和静态检查工具的标记,表示这个指针指向的是用户空间地址,绝对不能在内核中直接解引用。如果直接访问(如*buf),轻则引发内核 oops,重则造成安全漏洞(恶意程序可以传入内核地址来读取内核数据)。- 正确操作:必须使用专用的跨空间拷贝函数:
- 读取用户数据:
copy_from_user(kernel_buf, buf, cnt) - 写入用户数据:
copy_to_user(buf, kernel_buf, cnt)
- 读取用户数据:
2.size_t cnt
- 含义:应用程序请求读/写的字节数。
- 来源:来自用户调用的
read(fd, buf, 100)中的100。 - 限制:你不能盲目使用
cnt。如果内核实际可用数据少于cnt,要返回实际读取/写入的字节数,不能越界。 - 常见操作:
- 读操作:
cnt = min(cnt, available_data_len);然后再拷贝给用户。 - 写操作:直接使用
cnt作为copy_from_user的长度参数,但可能需要判断是否溢出。
- 读操作:
3. loff_t *offt
- 含义:文件当前的读写位置偏移量的指针。
- 作用:类似于普通文件的“光标位置”。对于大多数字符设备(如串口、LED),这个参数通常不需要,因为它没有物理上的存储位置,可以直接忽略。但对于存储类设备(如 EEPROM、Flash),需要:
- 根据
*offt决定从设备哪个位置开始读写。 - 读写完成后更新
*offt(例如*offt += n),告诉内核当前读写位置移动了多少。
- 根据
4.__init __exit
__init 是 Linux 内核中的一个宏,用来标记只在驱动初始化阶段使用一次的函数或数据
内核模块加载成功后,初始化函数(chrdevbase_init)的代码就再也不会被调用了,但它仍然占用内存。
加上 __init 后,链接器会把这个函数的代码放到一个特殊的 .init.text 段里。
结果:内核在初始化阶段全部完成后,会把 .init 相关的段全部释放掉,回收内存。这在内核这种对内存寸土寸金的环境里非常重要。
同样,__exit 标记的函数(卸载函数)如果内核配置为不支持模块卸载,也会被自动丢弃;如果支持卸载,则保留在 .exit.text 段,模块卸载后才可能被释放。
注意:要注册到内核的函数的签名必须要和对应函数指针的签名一样

代码例子
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : led.c
作者 : 左忠凯
版本 : V1.0
描述 : LED驱动文件。
其他 : 无
论坛 : www.openedv.com
日志 : 初版V1.0 2019/1/30 左忠凯创建
***************************************************************/
#define LED_MAJOR 200 /* 主设备号 */
#define LED_NAME "led" /* 设备名字 */
#define LEDOFF 0 /* 关灯 */
#define LEDON 1 /* 开灯 */
/* 寄存器物理地址 */
#define CCM_CCGR1_BASE (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)
#define GPIO1_DR_BASE (0X0209C000)
#define GPIO1_GDIR_BASE (0X0209C004)
/* 映射后的寄存器虚拟地址指针 */
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!\r\n");
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!\r\n");
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("zuozhongkai");和字符设备一样的流程
先scp拷贝到板子,再加载,再创建驱动节点
depmod //第一次使用modprobe加载驱动的时候需要运行此命令modprobe led.ko //加载驱动驱动加载成功以后创建“/dev/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
