旧字符设备驱动之LED点灯
led.c
/*
* 根据linux内核的程序查找所使用函数的对应头文件。
*/
#include <linux/module.h> // MODULE_LICENSE,MODULE_AUTHOR
#include <linux/init.h> // module_init,module_exit
#include <linux/kernel.h> // printk
#include <linux/fs.h> // struct file_operations
#include <linux/uaccess.h> // copy_to_user,copy_from_user
#include <linux/io.h> //ioremap,iounmap
/*
* 为了方便管理, Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分。
* 组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。
* 注意:旧字符设备驱动在使用主设备号时会将其对应下的所有次设备号使用掉,
* 因此不存在哪一类(主设备号)驱动程序的哪一个(次设备号)驱动程序。
*/
#define LED_MAJOR 200 //主设备号
#define LED_NAME "led" //设备名称
/********************************* 1.1、寄存器物理地址定义 ********************************/
#define CCM_CCGR1_BASE (0x020C406C) //对应IO的时钟寄存器地址
#define SW_MUX_GPIO1_IO03_BASE (0x020E0068) //对应IO的复用寄存器地址
#define SW_PAD_GPIO1_IO03_BASE (0x020E02F4) //对应IO的电气属性寄存器地址
#define GPIO1_GDIR_BASE (0x0209C004) //对应IO的输出方向寄存器地址
#define GPIO1_DR_BASE (0x0209C000) //对应IO的输出电平寄存器地址
/*********************************************************************************************/
/****** 1.2、物理地址映射后定义虚拟地址的指针,其类型是根据ioremap函数返回值类型定义的 ******/
static void __iomem *IMX6U_CCM_CCGR1; //对应IO的时钟寄存器的映射虚拟地址
static void __iomem *SW_MUX_GPIO1_IO03; //对应IO的复用寄存器的映射虚拟地址
static void __iomem *SW_PAD_GPIO1_IO03; //对应IO的电气属性寄存器的映射虚拟地址
static void __iomem *GPIO1_GDIR; //对应IO的输出方向寄存器的映射虚拟地址
static void __iomem *GPIO1_DR; //对应IO的输出电平寄存器的映射虚拟地址
/*********************************************************************************************/
#define LEDOFF 0 //关灯
#define LEDON 1 //开灯
/* 开/关灯的函数 */
void led_switch(u8 state)
{
u32 val; //操作的是32位的寄存器
if(state == LEDON) {
/* 开灯 */
val = readl(GPIO1_DR); //读取寄存器
val &= ~(1 << 3); //清零
writel(val, GPIO1_DR); //写入寄存器
} else if(state == LEDOFF) {
/* 关灯 */
val = readl(GPIO1_DR); //读取寄存器
val |= 1 << 3;
writel(val, GPIO1_DR); //写入寄存器
}
}
/*
* 基本的字符设备驱动函数需要实现最基本的open、release、read、write设备操作函数。
* 具体编写可以通过linux内核的程序进行参考。
*/
/* 设备打开函数 */
static int led_open(struct inode *inode, struct file *filp)
{
return 0;
}
/* 设备关闭函数 */
static int led_release(struct inode *inode, struct file *filp)
{
return 0;
}
/* 向设备写入数据 */
static ssize_t led_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
int ret;
unsigned char databuf[1];
ret = copy_from_user(databuf, buf, count); //将用户传入的buf数据写入databuf内核缓冲区
if(ret < 0) {
printk("write kernel failed!\r\n");
return -EFAULT;
}
/* 判断开关灯 */
led_switch(databuf[0]); //调用开关的函数
return 0;
}
/*
* 字符设备的操作集合:是将上面的具体设备操作函数进行初始化。
* owner:拥有该结构体的模块的指针,这里指向当前驱动模块。
* open、release、read、write是指向上面已经实现的设备操作函数。
* 这里只是控制led等,所以没有使用读取设备的操作函数
*/
static const struct file_operations led_fops = {
.owner = THIS_MODULE,
.write = led_write,
.open = led_open,
.release= led_release,
};
/*
* 因为这里需要将驱动编译成模块(Linux 下模块扩展名为.ko),这样的话修改驱动以后只需要编译一下驱动代码即可,
* 不需要编译整个 Linux 代码。而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。
* 这里需要编写驱动加载函数和驱动卸载函数,并且需要将这驱动加载函数和驱动卸载函数进行注册。
* 对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。
*/
/* 驱动模块入口函数 */
static int __init led_init(void)
{
int ret = 0; //保存调用函数返回值的临时变量
unsigned int val = 0; //操作寄存器是保存的临时变量
/***************** 1、初始化led,先地址映射 *****************/
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_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
/***********************************************************/
/****** 2、初始化时钟,一般操作寄存器使用读改写操作步骤 ******/
val = readl(IMX6U_CCM_CCGR1); //读取寄存器
val &= ~(3 << 26); //bit26:27清零
val |= 3 << 26; //bit26:27置一
writel(val, IMX6U_CCM_CCGR1); //写入寄存器,时钟
writel(0x5,SW_MUX_GPIO1_IO03); //复用
writel(0x10B0,SW_PAD_GPIO1_IO03); //电气属性
val = readl(GPIO1_GDIR); //读取寄存器
val |= 1 << 3;
writel(val, GPIO1_GDIR); //写入寄存器,输出方向
/* 开灯 */
val = readl(GPIO1_DR); //读取寄存器
val &= ~(1 << 3); //清零
writel(val, GPIO1_DR); //写入寄存器
/***********************************************************/
/* 注册字符设备:设备号、设备名、结构体 file_operations 类型指针,指向设备的操作函数集合变量 */
ret = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
if(ret < 0) {
printk("register chrdev failed!\r\n");
return -EIO; //返回IO操作错误标识
}
printk("led_init\r\n");
return 0;
}
/* 驱动模块出口函数 */
static void __exit led_exit(void)
{
/* 关灯 */
unsigned int val = 0;
val = readl(GPIO1_DR); //读取寄存器
val |= 1 << 3;
writel(val, GPIO1_DR); //写入寄存器
/******************** 3、注销地址映射 **********************/
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_GDIR);
iounmap(GPIO1_DR);
/***********************************************************/
/* 注销字符设备 */
unregister_chrdev(LED_MAJOR, LED_NAME);
printk("led_exit\r\n");
}
/*
* 向Linux内核注册模块入口与出口函数,分别对应在终端输入“ismod或modprobe”和“rmmod”
* ismod不能解决模块的依赖关系,modprobe 会分析模块的依赖关系,然后会将所有的依赖模块都加载到内核中。
*/
/* 注册驱动模块加载与卸载函数 */
module_init(led_init);
module_exit(led_exit);
/* 模块必须通过MODULE_LICENSE宏声明此模块的许可证,否则在加载此模块时,会收到内核被污染 “kernel tainted” 的警告 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("DJW"); //作者信息
ledAPP.c
/*
* 头文件可以通过linux手册查找,man no <xx>
* 1是普通的命令
* 2是系统调用,如open,write之类的(通过这个,至少可以很方便的查到调用这个函数,需要加什么头文件)
* 3是库函数,如printf,fread
* 4是特殊文件,也就是/dev下的各种设备文件
* 5是指文件的格式,比如passwd, 就会说明这个文件中各个字段的含义
* 6是给游戏留的,由各个游戏自己定义
* 7是附件还有一些变量,比如向environ这种全局变量在这里就有说明
* 8是系统管理用的命令,这些命令只能由root使用,如ifconfig
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
/*
* argc:应用程序参数个数
* argv[]:具体打参数内容,字符串形式
* ./ledAPP <filename> <0:1> 0为关,1为开
* ./ledAPP /dev/led 0 表示关灯
* ./ledAPP /dev/led 1 表示开灯
*/
int main(int argc, char *argv[])
{
int fd, ret; //fd保存调用文件操作函数的文件描述符的临时变量,ret保存调用函数返回值临时变量
char *filename; //保存文件名信息的指针变量
unsigned char databuf[1]; //保存终端输入的开关灯变量
/* 判断终端输入的参数个数是否为3个 */
if(argc != 3) {
printf("ERROR USAGE!\r\n");
return -1;
}
/* 保存第二个输入的参数,文件名信息,此参数是保存对应创建文件节点的绝对路径的 */
filename = argv[1];
fd = open(filename,O_RDWR); //使用读写方式打开文件
if(fd < 0) {
printf("file %s open failed!\r\n",filename);
return -1;
}
/* 保存第三个输入参数,也就是led的状态参数 */
databuf[0] = atoi(argv[2]);
/* 将led的状态参数写入fd设备文件中,其会在驱动程序中以调用函数buf形参写入驱动程序的变量中 */
ret = write(fd, databuf, sizeof(databuf));
if(ret < 0) {
printf("LED control failed!\r\n");
close(fd); //如果写入操作失败了,那么就关闭设备文件
return -1;
}
close(fd); //程序运行完成,关闭设备文件
return 0;
}
步骤:
1、将驱动程序进行make操作,编译生成xxx.ko文件
2、使用交叉编译器将应用程序编译成可执行文件,如:我这里使用 arm-linux-gnueabihf-gcc xxxAPP.c -o xxxAPP
3、将xxx.ko和xxxAPP两个文件拷贝到存放驱动模块的目录中。
4、打开开发板并使用Linux系统选择通过TFTP从网络启动和使用NFS挂载网络根文件系统。
5、在PC机的串口终端中先输入depmod,后输入modprobe xxx.ko加载驱动文件,最后输入lsmod查看当前系统中存在的模块。
6、在PC机的串口终端中输入cat /proc/devices查看当前系统的所有设备的对应的设备号及名称。
7、驱动加载成功需要在/dev目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。
在PC机的串口终端中输入mknod /dev/xxx c 200 0,其中“mknod”是创建节点命令,“/dev/xxx”是要创建的节点文件,“c”表示这是个
字符设备,“ 200”是设备的主设备号,“ 0”是设备的次设备号。可以通过ls /dev/xxx -l查看该文件详情。
8、在PC机的串口终端中输入./xxxApp /dev/xxx 1或./xxxApp /dev/xxx 2指令来使用应用程序对驱动程序进行读写等操作。
9、在PC机的串口终端中输入rmmod xxx.ko卸载驱动模块。