Linux驱动学习day3
字符驱动另一种注册方式cdev_init
在注册驱动的时候直接使用register_chrdev()的时候,不用指定主设备号,也可以不指定次设备号,这样会霸占一个主设备号下所有的次设备号,为了解决这个问题,使用alloc_chrdev_region(),该函数可以获得主设备号和可以指定次设备号的个数,并且使用cdev_init()和cdev_add()函数和重要的file_operations结构体挂钩。在入口函数和出口函数指定。
int alloc_chrdev_region(dev *dev , unsigned baseminor , unsigned count , const char *name);/* dev : 传出参数,传出设备号(包含主次设备号) baseminor :次设备号从哪开始count :多少个次设备号name : 名字
*/
void cdev_init(struct cdev *cdev , const struct file_operations *fops);
/* cdev: struct cdev类型fops: file_operations结构体*/
int cdev_add(struct cdev *p , dev_t dev , unsigned count);
/* p: cdev dev: 设备号count:之前指定的数
*/
入口函数
static dev_t hello_dev;
static struct cdev hello_cdev;static int __init hello_init(void)
{int ret = 0;ret = alloc_chrdev_region(&hello_dev , 0 , 1 , "hello_drv");if(ret < 0){printk(KERN_ERR "alloc_chrdev_region() failed for hello_drv\n");return -EINVAL;}cdev_init(&hello_cdev , &hello_drv);ret = cdev_add(&hello_cdev , hello_dev , 1);if(ret){printk(KERN_ERR "cdev_add() failed for hello_drv\n");return -EINVAL;}hello_class = class_create(THIS_MODULE , "hello_class"); device_create(hello_class , NULL , hello_dev , NULL , "hello");return 0;
}
出口函数
static void __exit hello_exit(void)
{device_destroy(hello_class , hello_dev);class_destroy(hello_class);cdev_del(&hello_cdev);unregister_chrdev_region(hello_drv , 1);return;
}
这样做的好处就是,比如设备号1(234 , 0)和设备号2(234 , 1)是隔绝的,这时候我mknod /dev/abc -c 234 1 , 使用./hello_test /dev/abc ,会返回can not open file,但是如果我将上述的1改为2,也就是说占用两个次设备号,我使用./hello_test /dev/abc 会返回/dev/hello驱动程序中的hello_buf中的数据。结果如下图。
GPIO子系统
引脚编号查询
查询引脚编号有两种方法
方法1:cat /sys/kernel/debug/gpio,这里的gpiochip是从0开始编号的,而芯片手册有些是从1开始编号的,所以有时候芯片手册上的gpio4就相当于这里面的gpiochip3。
方法2:cd /sys/class/gpio -ls,使用该方法进入该目录下使用ls,可以看到下面的内容,这里面的32,64,128等是第某个gpio模块引脚的起始位置。比如gpiochip32的32就是第二个gpio模块引脚的起始位置。
接着我们可以进入某个文件 ,有下面这些内容
其中base就是起始编号,可以用cat命令查看,ngpio为引脚个数,原子的板子label就是gpio编号,但是韦东山老师的板子是现实的物理地址。
这样我们可以算出GPIO4_IO14这个引脚的编号,32×3+14 = 100. 得到编号之后我们便可以对其进行操作。下面是操作行命令对应的背后的驱动函数。
echo 110 > /sys/class/gpio/export -->gpio_request
echo in > /sys/class/gpio/gpio110/diection -->gpio_direction_input
cat /sys/class/gpio/gpio110/value -->gpio_get_value
echo 110 > /sys/class/gpio/unexport -->gpio_free
中断函数
驱动中使用中断函数的流程(如果设备简单可以不用清除中断):
- 确定中断号(irq = gpio_to_irq())
- 注册中断处理函数(request_irq())
- 在中断处理函数中分辨/处理/清除中断
拆分通用驱动程序框架
1、file_operations结构体
static struct file_operations gpio_key_drv =
{.owner = THIS_MODULE,.read = gpio_drv_read, .write = gpio_drv_write,.poll = gpio_drv_poll, .fasync = gpio_drv_fasync /* 异步通知 */
};
在入口函数中注册。
major = register_chrdev(0 , "gpio_key" , &gpio_key_drv);gpio_class = class_create(THIS_MODULE , "gpio_key_class");device_create(gpio_class , NULL , MKDEV(major , 0) , NULL , "gpio_key"); /* /dev/gpio_kev */
2、GPIO操作
static struct gpio_desc /* 使用结构体来描述引脚 */
{int gpio;int irq;char *name;int key;struct timer_list key_timer;/* 定时器超时时长 */
};static struct gpio_desc gpio[2] =
{{131 , 0 , "gpio_1" , },{132 , 0 , "gpio_2" , }
}static int __init gpio_init(void)
{int err;int i;int count = sizeof(gpios)/sizeof(gpios[0]);for(i = 0 ; i < count ; i++){gpios[i].irp = gpio_to_irq(gpios[i].gpio); /* 将引脚编号转换成中断号 */err = request_irq(gpios[i].irq , gpio_key_isr , IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING , "gpio_key" , &gpios[0]); /* 注册中断号,中断触发方式为上升沿触发|下降沿触发,即双边沿触发 */}major = register_chrdev(0 , "gpio_key" , &gpio_key_drv);gpio_class = class_create(THIS_MODULE , "gpio_key_class");device_create(gpio_class , NULL , MKDEV(major , 0) , NULL , "gpio_key"); /* /dev/gpio_kev */
}
3、中断处理函数
/* 发生中断的时候,会进入到该函数,这个函数主要作用是设置定时器超时时间
这样操作可以消除抖动,具体看上述图*/
static irqreturn_t gpio_key_isr(int irq , void *dev_id)
{struct gpio_desc *gpio_desc = dev_id; /* 强转 */printk("gpio_key_isr key %d irq happened\n" , gpio_desc->gpio);mod_timer(&gpio_desc->key_timer , jiffies + HZ/5); /* 中断中主要执行的操作 */return IRQ_HANDLED;/* 如果不能处理中断返回 IRQ_NONE */
}
4、定时器
static void key_timer_expire(unsigned long data)
{/* data->gpio */struct gpio_desc *gpio_desc = (struct gpio_desc *)data;int val;int key;/* 读引脚值 */val = gpio_get_value(gpio_desc->gpio); key = (gpio_desc->key) | (val << 8);put_key(key); /* 记录按键值 */wake_up_interruptible(&gpio_wait); /* 去等待队列中将等待队列的事件唤醒 */kill_fasync(&button_fasync , SIGIO , POLL_IN); /* 异步通知,发信号给某个线程 */
}static int __init gpio_init(void)
{int err;int i;int count = sizeof(gpios)/sizeof(gpios[0]);for(i = 0 ; i < count ; i++){gpios[i].irp = gpio_to_irq(gpios[i].gpio); /* 将引脚编号转换成中断号 */setup_timer(&gpios[i].key_timer , key_timer_expire , (unsigned long)&gpio[i]);gpios[i].key_timer.expires = -0; /* 设置定时器时间为无穷 */add_timer(&gpios[i].key_timer); /* 启动定时器 */err = request_irq(gpios[i].irq , gpio_key_isr , IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING , "gpio_key" , &gpios[0]); /* 注册中断号,中断触发方式为上升沿触发|下降沿触发,即双边沿触发 */}major = register_chrdev(0 , "gpio_key" , &gpio_key_drv);gpio_class = class_create(THIS_MODULE , "gpio_key_class");device_create(gpio_class , NULL , MKDEV(major , 0) , NULL , "gpio_key"); /* /dev/gpio_kev */
}
val = gpio_get_value(gpio_desc->gpio); key = (gpio_desc->key) | (val << 8);假设gpio_desc->key = 0x1A(按键编号 26),val = 1(引脚为高电平)
key = 0x1A | (1 << 8) = 0x1A | 0x100 = 0x11Aval = 0
key = 0x1A | (0 << 8) = 0x1A
低八位是按键的编号这段代码将“哪个键(key)”和“当前按下还是松开(val)”编码到一个 int 值中,方便传递和处理。
应用程序和驱动交互
a、非阻塞
在应用程序代码中使用非阻塞的方式打开驱动程序。如果应用程序调用了读函数,会导致驱动调用自己相应的读函数,这时候驱动程序会去判断是否是以非阻塞的方式打开。
fd = open(argv[1] , O_RDWR | O_NONBLOCK);
/* 驱动程序读操作 */
static ssize_t gpio_drv_read(struct file *file , char __user *buf , size_t size , loft_t *offset)
{int err;int key;if(is_key_buf_empty() && (file->f_flags & O_NONBLOCK))return -EAGAIN;wait_event_interruptible(gpio_wait , !is_key_buf_empty());key = get_key();err = copy_to_user(buf , &key , 4);return 4;
}
b、阻塞
代码执行到下面这行,会进去判断!is_key_buf_empty()是否成立。如果空的话,该函数会进入休眠。其实就是该函数会等待!is_key_buf_empty()为真。休眠的时候会做下面两件事:改变程序的状态,不会被调度。记录在gpio_wait队列里面。
wait_event_interruptible(gpio_wait , !is_key_buf_empty());
流程
流程:当按键被按下/松开-->会产生中断-->执行中断处理函数-->设置定时器(主要是为了消除抖动)-->定时器事件超时会执行定时器函数-->唤醒上面休眠的函数。
c、POLL
在用户程序中open,open之后使用poll函数,判断poll函数的返回值,在返回值里面执行read函数。poll函数返回值有三种情况1.已经有数据,直接返回,2.无数据,休眠,3.被唤醒(按下按键、超时)。
fd = open(argv[1] , O_RDWR);fds[0].fd = fd;
fds[0].events = POLLIN;while(1)
{ret = poll(fds , 1 , timeout_ms);if(ret == 1 && (fds[0].revents & POLLIN)){ read(fd , &val , 4);}else{printf("time out\n");}
}
static unsigned int gpio_drv_poll(struct file *fp , poll_table *wait)
{poll_wait(fp , &gpio_wait , wait);return is_key_buf_empty() ? 0 : POLLIN | POLLRANDORM;
}
流程
APP调用poll函数-->内核有个sys_poll,里面含有一个循环-->调用驱动程序中的poll函数-->将进程记录在gpio_wait队列里,并没有休眠。 返回当前的状态-->按键按下/松开-->发送中断 -->执行中断处理函数,判断是否有数据-->有数据返回POLLIN/无数据返回0-->返回sys_poll退出循环/继续休眠or超时-->返回到APP读数据。
4、异步通知
借用老师上课所说的,儿子醒了主动告诉妈妈。将其的想法转移到驱动中就是,中断or定时器函数主动发送信号告诉APP有数据。
APP给信号提供处理函数,读取驱动程序
static void sig_func(int sig)
{int val;read(fd , &val , 4);printf("get butten : 0x%x\n" , val);
}fd = open(argv[1] , O_RDWR);/* 告诉驱动需要给谁发信号 PID */
fcntl(fd , F_SETOWN , getpid());
flags = fcntl(fd , F_GETFL);/* 设置其状态为原始状态加上异步通知 */
fcntl(fd , F_SETFL , flags | FASYNC); while(1){printf("wait \n");sleep(2);
}
驱动 (可以从button_fasync 结构体里面找到PID )
key_timer_expire()
{kill_fasync(&button_fasync , SIGIO , POLL_IN);/* 发送信号 SIGIO 给进程*/
}static int gpio_drv_fasync(int fd , struct file *file , int on)
{/* fasync_helper 函数会帮助构造 button_fasync 结构体*/if(fasync_helper(fd , file , on , &button_fasync) >= 0)return 0;elsereturn -EIO;
}
流程
APP注册信号处理函数-->F_SETOWN记录当前进程的PID-->使能异步通知-->进入循环-->按键按下\松开-->发生中断-->设置定时器时间(消除抖动)-->定时器时间超时-->执行定时器函数-->kill_fasync发送信号给button_fasync记录的PID-->APP执行信号处理函数,读取数据。
LED硬件相关知识
如何点亮一个LED灯?三步骤
- 看原理图确定控制LED的引脚
补充一下电路的知识,当NPN三极管的时候,需要P点电压大于下面那个N点电压即可导通,当PNP三极管的时候,需要下面那点P的电压高于N点电压即可导通。
- 看主芯片手册确定如何设置/控制引脚
- 写程序
正点原子RK3568板子上的LED原理图,如图可以知道LED由板子上引脚WORKING_LEDEN_H操作,可以查到 WORKING_LEDEN_H对应的是引脚 GPIO0 C0。这样只有当GPIO0 C0引脚为高电平的时候,三极管是通的,LED亮,当引脚为低电平的时候,LED灭。
文章写道:
PMU_GRF_GPIO0C_IOMUX_L 寄存器分为 2 部分:
① 、bit31:16:低 16 位写使能位,这 16 个 bit 控制着寄存器的低 16 位写使能。比如 bit16
就对应着 bit0 的写使能,如要要写 bit0,那么 bit16 要置 1,也就是允许对 bit0 进行写
操作。
② 、bit15:0:功能设置位。
一开始真的没有理解这句话,但是后来想一想还是能够理解。由于该口支持4种方法的IO复用,对应编码 000 001 010 011四种,所以需要bit2:0,代表的就是使用三位来编码。如果想让第一位变成1,则必须要让第16位先置为1(后面16位控制着是否能对前面每个位置进行写操作),这样才能对bit0进行写操作。
eg. val = (1 << 16) | (1 << 0);表示0位可写,并且将0位置为1。
明天开始要写论文了,要快点把论文写完投出去!然后继续学习驱动!