嵌入式内核及驱动开发(初级)
一、设备环境搭建
1.交叉开发模式
2.目标机开发方式
3.tftp服务器与nfs服务器配置
/*Linux驱动开发环境搭建流程*/
1,ubuntu中配置编译环境
设置交叉工具链:
tar -xvf gcc-4.6.4.tar.xz -C ~/Linux_4412/toolchain
设置环境变量:
vim ~/.bashrc 最后面添加
export PATH=$PATH:/home/george/Linux_4412/toolchain/gcc-4.6.4/bin
更新脚本:
source ~/.bashrc
arm-none-linux-gnueabi-gcc -v
Using built-in specs.
COLLECT_GCC=arm-none-linux-gnueabi-gcc
COLLECT_LTO_WRAPPER=/home/george/Linux_4412/toolchain/gcc-4.6.4/bin/
../libexec/gcc/arm-arm1176jzfssf-linux-gnueabi/4.6.4/lto-wrapper
2,运行开发
a,通过tftp去启动内核
1,将uImage和dtb文件放入到ubuntu中/tftpboot
2,在开发板中设置uboot参数,使其能够去加载内核
set ipaddr 192.168.7.22
set serverip 192.168.7.21
set bootcmd tftp 0x41000000 uImage \; tftp 0x42000000 exynos4412-fs4412.dtb \; bootm 0x41000000 - 0x42000000
save
b,通过nfs去挂载rootfs
1,需要一个跟文件系统目录--rootfs.tar.xz,需要解压到ubuntu
sudo tar -xvf rootfs.tar.xz -C /opt/4412/
2, 配置nfs服务器(需要安装),让/opt/4412/rootfs可以被挂载
sudo vim /etc/exports
/opt/4412/rootfs *(subtree_check,rw,no_root_squash,async)
sudo service nfs-kernel-server restart //重启nfs服务器
测试:
sudo mount -t nfs localhost:/opt/4412/rootfs /mnt
3,在开发中去指定内核要挂载/opt/4412/rootfs--切换到开发操作
set bootargs console=ttySAC2,115200 init=/linuxrc root=/dev/nfs rw nfsroot=192.168.7.21:/opt/4412/rootfs ip=192.168.7.22
save
解释:
bootargs 是uboot传递给内核到启动参数,是一个字符串
console=xxx: 告诉内核启动时候到调试信息是从哪个设备输出
init=xxx: 告诉内核linux到第一个用户进程是什么
root=xxx : 告诉内核根文件系统在哪里
root=/dev/nfs 表示根文件系统在网路远端
nfsroot=ip:path
ip=xxx :告诉内核开机的时候内核的ip地址是多少(静态分配ip)
3,可以开始去编写代码--开发驱动
a, 编译内核
tar -xvf linux-3.14.tar.xz
步骤:
1,设置交叉工具链--uImage也运行arm开发板
vim Makefile
ARCH = arm
CROSS_COMPILE = arm-none-linux-gnueabi-
2, 选择一个soc ,可以支持很多到soc,所以必须挑出针对我们到平台到代码
make exynos_defconfig
// cp -raf arch/arm/configs/exynos_defconfig .config
3,make menuconfig 内核裁剪,产生一个图像界面
System Type --->
(2) S3C UART to use for low-level messages
4,make uImage : 编译内核
//如果编译报错:缺mkimage
sudo cp -raf mkimage /usr/bin/
sudo chmod 777 /usr/bin/
重新在make uImage
5,编译设备树文件--描述设备信息--最终要编译成dtb
以一个默认到dts为参考,变成我们自己想要的dts
arch/arm/boot/dts$ cp exynos4412-origen.dts exynos4412-fs4412.dts
arch/arm/boot/dts$ vim Makefile
70行 exynos4412-fs4412.dtb \
回到内核源码顶层目录:
george@ubuntu:~/Linux_4412/kernel/linux-3.14$ make dtbs
DTC arch/arm/boot/dts/exynos4210-origen.dtb
DTC arch/arm/boot/dts/exynos4210-smdkv310.dtb
DTC arch/arm/boot/dts/exynos4210-trats.dtb
DTC arch/arm/boot/dts/exynos4210-universal_c210.dtb
DTC arch/arm/boot/dts/exynos4412-odroidx.dtb
DTC arch/arm/boot/dts/exynos4412-origen.dtb
DTC arch/arm/boot/dts/exynos4412-fs4412.dtb
DTC arch/arm/boot/dts/exynos4412-smdk4412.dtb
DTC arch/arm/boot/dts/exynos4412-tiny4412.dtb
DTC arch/arm/boot/dts/exynos4412-trats2.dtb
DTC arch/arm/boot/dts/exynos5250-arndale.dtb
DTC arch/arm/boot/dts/exynos5250-smdk5250.dtb
DTC arch/arm/boot/dts/exynos5250-snow.dtb
DTC arch/arm/boot/dts/exynos5420-arndale-octa.dtb
DTC arch/arm/boot/dts/exynos5420-smdk5420.dtb
DTC arch/arm/boot/dts/exynos5440-sd5v1.dtb
DTC arch/arm/boot/dts/exynos5440-ssdk5440.dtb
使用uImag和dtb文件
cp -raf arch/arm/boot/uImage /tftpboot
cp -raf arch/arm/boot/dts/exynos4412-fs4412.dtb /tftpboot
==============================================
移植dm9000
实际是设备树文件修改
vim arch/arm/boot/dts/exynos4412-fs4412.dts
添加如下内容:
srom-cs1@5000000 {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x5000000 0x1000000>;
ranges;
ethernet@5000000 {
compatible = "davicom,dm9000";
reg = <0x5000000 0x2 0x5000004 0x2>;
interrupt-parent = <&gpx0>;
interrupts = <6 4>;
davicom,no-eeprom;
mac-address = [00 0a 2d a6 55 a2];
};
};
保存退出后,需要再次编译dts文件
make dtbs
配置内核:
make menuconfig
[*] Networking support --->
Networking options --->
<*> Packet socket
<*> Unix domain sockets
[*] TCP/IP networking
[*] IP: kernel level autoconfiguration
[*] IP: BOOTP support
Device Drivers --->
[*] Network device support --->
[*] Ethernet driver support (NEW) --->
<*> DM9000 support
File systems --->
[*] Network File Systems (NEW) --->
<*> NFS client support
[*] NFS client support for NFS version 2
[*] NFS client support for NFS version 3
[*] NFS client support for the NFSv3 ACL protocol extension
[*] Root file system on NFS
退出到时候要保存:
再次编译内核:
make uImage -j2
cp -raf arch/arm/boot/uImage /tftpboot
cp -raf arch/arm/boot/dts/exynos4412-fs4412.dtb /tftpboot
在开发板中到uboot设置中,添加一个参数 clk_ignore_unused
set bootargs clk_ignore_unused console=ttySAC2,115200 init=/linuxrc root=/dev/nfs rw nfsroot=192.168.7.21:/opt/4412/rootfs ip=192.168.7.22
重新启动开发板
b, 编写驱动代码
1,用什么工具去写---source insight(看代码的工具)
环境搭建\烧录镜像和工具\si_linux3.14-ori.tgz
解压到内核源码的顶层目录:
tar -xvf si_linux3.14-ori.tgz
2,怎么写
在souceinsght去写
驱动代码需要有四个部分
1,头文件
#include <linux/init.h>
#include <linux/module.h>
2,驱动模块装载和卸载函数入口到声明
module_init(hello_drv_init);
module_exit(hello_drv_exit);
3,实现模块装载和卸载函数入口
static int __init hello_drv_init(void)
{
return 0;
}
static void __exit hello_drv_exit(void)
{
}
4,GPL声明
MODULE_LICENSE("GPL");
c,编译驱动代码--Makefile(被读取两次: make 2,内核源码中Makefile)
ROOTFS_DIR = /opt/4412/rootfs
ifeq ($(KERNELRELEASE), )
#内核源码到路径,不同环境会不一样,内核源码一定要先编译
KERNEL_DIR = /home/george/Linux_4412/kernel/linux-3.14
CUR_DIR = $(shell pwd)
all :
make -C $(KERNEL_DIR) M=$(CUR_DIR) modules
clean :
make -C $(KERNEL_DIR) M=$(CUR_DIR) clean
install:
cp -raf *.ko $(ROOTFS_DIR)/drv_module
else
#用于指定到底编译哪个代码--hello.c
obj-m += hello.o
endif
d,加载与卸载ko
[root@farsight drv_module]# insmod hello.ko
[ 2789.700000] -------hello_drv_init-------------
[root@farsight drv_module]# lsmod
hello 805 0 - Live 0xbf000000 (O)
[root@farsight drv_module]# rmmod hello
rmmod: can't change directory to '/lib/modules': No such file or directory
[root@farsight drv_module]# mkdir /lib/modules
[root@farsight drv_module]# rmmod hello
rmmod: can't change directory to '3.14.0': No such file or directory
[root@farsight drv_module]# mkidr /lib/modules/3.14.0
[root@farsight drv_module]# rmmod hello
[ 2903.230000] -------hello_drv_exit-------------
(二三)、驱动模块开发
1. 参数传递
加载ko: insmod hello.ko myname="george" myvalue=33
用途: wifi驱动,wifi硬件中内部也运行内部代码,原厂开发,这些代码叫做固件--firmware.bin
装载wifi驱动,必须告诉固件到文件在哪里
insmod rtxxx.ko path=/lib/modules/firmware/xxx.bin
在代码如何处理参数:
module_param(name, type, perm)
参数1:表示参数到名字,比如myname, myvalue
参数2:参数到类型, charp, int
参数3: /sys/modules/表示文件到权限: 0666
用法:
module_param(myvalue, int, 0666);
module_param(myname, charp, S_IRUGO|S_IWUGO|S_IXUGO);
2,符号导出
#include <linux/module.h>
#include <linux/init.h>
//不需要模块加载和卸载到入口声明,直接定义好一些封装的函数
int my_add(int a, int b)
{
return a+b;
}
EXPORT_SYMBOL(my_add);
int my_sub(int a, int b)
{
return a-b;
}
EXPORT_SYMBOL(my_sub);
MODULE_LICENSE("GPL");
(四五六)、字符设备驱动
1. 字符设备驱动开发框架
- 作为字符设备驱动要素:
1,必须有一个设备号,用在众多到设备驱动中进行区分
2,用户必须知道设备驱动对应到设备节点(设备文件)
linux把所有到设备都看成文件
crw-r----- 1 root root 13, 64 Mar 28 20:14 event0
crw-r----- 1 root root 13, 65 Mar 28 20:14 event1
crw-r----- 1 root root 13, 66 Mar 28 20:14 event2
3,对设备操作其实就是对文件操作,应用空间操作open,read,write的时候
实际在驱动代码有对应到open, read,write
2. 申请设备号和创建设备节点
1,作为驱动必须有一个主设备号–向系统申请
- int register_chrdev(unsigned int major, const char * name, const struct file_operations * fops)
参数1:主设备号
设备号(32bit–dev_t)==主设备号(12bit) + 次设备号(20bit)
主设备号:表示一类设备–camera
次设备号: 表示一类设备中某一个:前置,后置
给定到方式有两种:
1,动态–参数1直接填0
2,静态–指定一个整数,250
参数2: 描述一个设备信息,可以自定义
/proc/devices列举出所有到已经注册的设备
参数3: 文件操作对象–提供open, read,write
返回值: 正确返回0,错误返回负数 - void unregister_chrdev(unsigned int major, const char * name)
参数1:主设备号
参数2: 描述一个设备信息,可以自定义
2,创建设备节点:
1,手动创建–缺点/dev/目录中文件都是在内存中,断电后/dev/文件就会消失
mknod /dev/设备名 类型 主设备号 次设备号
比如:
mknod /dev/chr0 c 250 0
[root@farsight drv_module]# ls /dev/chr0 -l
crw-r--r-- 1 0 0 250, 0 Jan 1 00:33 /dev/chr0
2,自动创建(通过udev/mdev机制)
//创建一个类
- struct class *class_create(owner, name)
参数1: THIS_MODULE
参数2: 字符串名字,自定义
返回一个class指针
//创建一个设备文件
- struct device *device_create(struct class * class, struct device * parent, dev_t devt,
void * drvdata, const char * fmt,…)
参数1: class结构体,class_create调用之后到返回值
参数2:表示父亲,一般直接填NULL
参数3: 设备号类型 dev_t
dev_t devt
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
参数4:私有数据,一般直接填NULL
参数5和6:表示可变参数,字符串,表示设备节点名字
//销毁动作:
- void device_destroy(devcls, MKDEV(dev_major, 0));
参数1: class结构体,class_create调用之后到返回值
参数2: 设备号类型 dev_t - void class_destroy(devcls);
参数1: class结构体,class_create调用之后到返回值
3,在驱动中实现文件io的接口,应用程序可以调用文件io
a,驱动中实现文件io操作接口:struct file_operations
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
int (*show_fdinfo)(struct seq_file *m, struct file *f);
}; //函数指针的集合,其实就是接口,我们写驱动到时候需要去实现
const struct file_operations my_fops = {
.open = chr_drv_open,
.read = chr_drv_read,
.write = chr_drv_write,
.release = chr_drv_close,
};
b,应用程序如何去调用文件io去控制驱动–open,read,…
fd = open("/dev/chr2", O_RDWR);
if(fd < 0)
{
perror("open");
exit(1);
}
read(fd, &value, 4);
write(fd, &value, 4);
close(fd);
3. 用户控制驱动和驱动控制硬件方式
1,应用程序需要传递数据给驱动
- int copy_to_user(void __user * to, const void * from, unsigned long n)
//将数据从内核空间拷贝到用户空间,一般是在驱动中chr_drv_read()用
参数1:应用驱动中的一个buffer
参数2:内核空间到一个buffer
参数3:个数
返回值:大于0,表示出错,剩下多少个没有拷贝成功
等于0,表示正确 - int copy_from_user(void * to, const void __user * from, unsigned long n)
//将数据从用户空间拷贝到内核空间,一般是在驱动中chr_drv_write()用
参数1:内核驱动中的一个buffer
参数2:应用空间到一个buffer
参数3:个数
2, 控制外设,其实就是控制地址,内核驱动中是通过虚拟地址操作
- void *ioremap(cookie, size)
参数1: 物理地址
参数2: 长度
返回值: 虚拟地址
去映射–解除映射 - void iounmap(void __iomem *addr)
参数1: 映射之后到虚拟地址
3, 通过驱动控制led灯:
led— GPX2_7 — GPX2CON ==0x11000C40
GPX2DAT ==0x11000C44
将0x11000C40映射成虚拟地址
对虚拟地址中到[32:28] = 0x1
4,应用程序和驱动扮演的是什么角色
- 用户态:应用程序
玩策略: 怎么去做
1, 一闪一闪
2,10s闪一次,也可以1s闪一次
3,一直亮
4,跑马灯
控制权是在应用程序(程序员) - 内核态:驱动
玩机制: 能做什么
led:亮 和 灭
5,编写字符设备驱动到步骤和规范 - 步骤:
1,实现模块加载和卸载入口函数
module_init(chr_dev_init);
module_exit(chr_dev_exit);
2,在模块加载入口函数中
a, 申请主设备号 (内核中用于区分和管理不同字符设备)
register_chrdev(dev_major, “chr_dev_test”, &my_fops);
b,创建设备节点文件 (为用户提供一个可操作到文件接口–open())
struct class *class_create(THIS_MODULE, “chr_cls”);
struct device *device_create(devcls, NULL, MKDEV(dev_major, 0), NULL, “chr2”);
c, 硬件的初始化
1,地址的映射
gpx2conf = ioremap(GPX2_CON, GPX2_SIZE);
2,中断到申请
3,实现硬件的寄存器到初始化
// 需要配置gpio功能为输出
*gpx2conf &= ~(0xf<<28);
*gpx2conf |= (0x1<<28);
e,实现file_operations
const struct file_operations my_fops = {
.open = chr_drv_open,
.read = chr_drv_read,
.write = chr_drv_write,
.release = chr_drv_close,
}; - 规范:
1,面向对象编程思想
用一个结构体来表示一个对象
//设计一个类型,描述一个设备的信息
struct led_desc{
unsigned int dev_major; //设备号
struct class *cls;
struct device *dev; //创建设备文件
void *reg_virt_base;
};
struct led_desc *led_dev;//表示一个全局的设备对象
// 0, 实例化全局的设备对象–分配空间
// GFP_KERNEL 如果当前内存不够用到时候,该函数会一直阻塞(休眠)
// #include <linux/slab.h>
led_dev = kmalloc(sizeof(struct led_desc), GFP_KERNEL);
if(led_dev == NULL)
{
printk(KERN_ERR "malloc error\n");
return -ENOMEM;
}
led_dev->dev_major = 250;
2,做出错处理
在某个位置出错了,要将之前申请到资源进行释放
led_dev = kmalloc(sizeof(struct led_desc), GFP_KERNEL);
led_dev->dev_major = register_chrdev(0, "led_dev_test", &my_fops);
if(led_dev->dev_major < 0)
{
printk(KERN_ERR "register_chrdev error\n");
ret = -ENODEV;
goto err_0;
}
err_0:
kfree(led_dev);
return ret;
6, 操作寄存器地址到方式:
1, volatile unsigned long *gpxcon;
*gpxcon &= ~(0xf<<28);
2, readl/writel();
u32 readl(const volatile void __iomem *addr)//从地址中读取地址空间到值
void writel(unsigned long value , const volatile void __iomem *add)
// 将value的值写入到addr地址
例子:
// gpio的输出功能的配置
u32 value = readl(led_dev->reg_virt_base);
value &= ~(0xf<<28);
value |= (0x1<<28)
writel(value, led_dev->reg_virt_bas);
或者:
*gpx2dat |= (1<<7);
替换成:
writel( readl(led_dev->reg_virt_base + 4) | (1<<7), led_dev->reg_virt_base + 4 );
(七~十一)、中断编程
1、中断号和中断申请
1,中断号–就是一个号码,需要通过一定的方式去获取到
在3.14.0内核中,从设备树中获取
获取中断号到方法:
1, 宏定义
IRQ_EINT(号码)
2,设备树文件中
arch/arm/boot/dts/exynos4412-fs4412.dts
硬件连接:
key ---- gpx1_2— EINT10
设备树文件:arch/arm/boot/dts/exynos4x12-pinctrl.dtsi
gpx1: gpx1 {
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
interrupt-parent = <&gic>;
interrupts = <0 24 0>, <0 25 0>, <0 26 0>, <0 27 0>,
<0 28 0>, <0 29 0>, <0 30 0>, <0 31 0>;
#interrupt-cells = <2>;
};
在编程过程中,需要定义自己的节点–描述当前设备用的中断号
arch/arm/boot/dts/exynos4412-fs4412.dts +51
key_int_node{
compatible = "test_key";
interrupt-parent = <&gpx1>;
interrupts = <2 4>;
};
编译设备树文件:
make dtbs
更新dtbs文件:
cp -raf arch/arm/boot/dts/exynos4412-fs4412.dtb /tftpboot/
2,在驱动中去通过代码获取到中断号,并且申请中断(实现中断处理方法)
a,获取到中断号码:
int get_irqno_from_node(void)
{
// 获取到设备树中到节点
struct device_node *np = of_find_node_by_path("/key_int_node");
if(np){
printk("find node ok\n");
}else{
printk("find node failed\n");
}
// 通过节点去获取到中断号码
int irqno = irq_of_parse_and_map(np, 0);
printk("irqno = %d\n", irqno);
return irqno;
}
b,申请中断
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char * name, void * dev)
参数1: 设备对应的中断号
参数2: 中断的处理函数
typedef irqreturn_t (*irq_handler_t)(int, void *);
参数3:触发方式
#define IRQF_TRIGGER_NONE 0x00000000 //内部控制器触发中断的时候的标志
#define IRQF_TRIGGER_RISING 0x00000001 //上升沿
#define IRQF_TRIGGER_FALLING 0x00000002 //下降沿
#define IRQF_TRIGGER_HIGH 0x00000004 // 高点平
#define IRQF_TRIGGER_LOW 0x00000008 //低电平触发
参数4:中断的描述,自定义,主要是给用户查看的
/proc/interrupts
参数5:传递给参数2中函数指针的值
返回值: 正确为0,错误非0
参数2的赋值:
irqreturn_t key_irq_handler(int irqno, void *devid)
{
return IRQ_HANDLED;
}
释放中断:
void free_irq(unsigned int irq, void *dev_id)
参数1: 设备对应的中断号
参数2:与request_irq中第5个参数保持一致
3,实现字符设备驱动的框架
// 1,设定一个全局的设备对象
key_dev = kzalloc(sizeof(struct key_desc), GFP_KERNEL);
// 2,申请主设备号
key_dev->dev_major = register_chrdev(0, "key_drv", &key_fops);
// 3,创建设备节点文件
key_dev->cls = class_create(THIS_MODULE, "key_cls");
key_dev->dev = device_create(key_dev->cls, NULL,
MKDEV(key_dev->dev_major,0), NULL, "key0");
4,驱动中将硬件所产生的数据传递给用户
a,硬件如何获取数据
key: 按下和抬起: 1/0
读取key对应的gpio的状态,可以判断按下还是抬起
读取key对应gpio的寄存器--数据寄存器
//读取数据寄存器
int value = readl(key_dev->reg_base + 4) & (1<<2);
b,驱动如何传递给用户
在中断处理中填充数据:
key_dev->event.code = KEY_ENTER;
key_dev->event.value = 0;
在xxx_read中奖数据传递给用户
ret = copy_to_user(buf, &key_dev->event, count);
c,用户如何拿到--编写应用程序
while(1)
{
read(fd, &event, sizeof(struct key_event));
if(event.code == KEY_ENTER)
{
if(event.value)
{
printf("APP__ key enter pressed\n");
}else{
printf("APP__ key enter up\n");
}
}
}
2、IO模型
1,实现文件IO模型之一阻塞,等同于休眠
文件io模型:
1,非阻塞
2,阻塞
3,多路复用--select/poll
4, 异步信号通知faync
阻塞: 当进程在读取外部设备的资源(数据),资源没有准备好,进程就会休眠
linux应用中,大部分的函数接口都是阻塞
scanf();
read();
write();
accept();
驱动中需要调用
1,将当前进程加入到等待队列头中
add_wait_queue(wait_queue_head_t * q, wait_queue_t * wait)
2,将当前进程状态设置成TASK_INTERRUPTIBLE
set_current_state(TASK_INTERRUPTIBLE)
3,让出调度--休眠
schedule(void)
更加智能的接口,等同于上面的三个接口:
wait_event_interruptible(wq, condition)
驱动如何去写代码
1,等待队列头
wait_queue_head_t
init_waitqueue_head(wait_queue_head_t *q);
2,在需要等待(没有数据)的时候,进行休眠
wait_event_interruptible(wait_queue_head_t wq, condition) // 内部会构建一个等待队列项/节点wait_queue_t
参数1: 等待队列头
参数2: 条件,如果是为假,就会等待,如果为真,就不会等待
可以用一标志位,来表示是否有数据
3,在一个合适的时候(有数据),会将进程唤醒
wake_up_interruptible(wait_queue_head_t *q)
用法:
wake_up_interruptible(&key_dev->wq_head);
//同时设置标志位
key_dev->key_state = 1;
2, 非阻塞: 在读写的时候,如果没有数据,立刻返回,并且返回一个出错码
用的会比较少,因为比较耗资源
open("/dev/key0", O_RDWR|O_NONBLOCK);
------------------------------------
驱动中需要去区分,当前模式是阻塞还是非阻塞
//如果当前是非阻塞模式,并且没有数据,立马返回一个出错码
if(filp->f_flags & O_NONBLOCK && !key_dev->key_state)
return -EAGAIN;
3,多路复用–select和poll
poll的应用:
1, 需要打开多个文件(多个设备)
2, 利用poll来实现监控fd的读,写,出错
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数1: 表示多个文件描述符集合
struct pollfd描述的是文件描述符到信息
struct pollfd {
int fd; //文件描述符
short events; //希望监控fd的什么事件:读,写,出错
POLLIN 读,
POLLOUT 写,
POLLERR出错
short revents; //结果描述,表示当前的fd是否有读,写,出错
//用于判断,是内核自动赋值
POLLIN 读,
POLLOUT 写,
POLLERR出错
};
参数2:被监控到fd的个数
参数3: 监控的时间:
正: 表示监控多少ms
负数: 无限的时间去监控
0: 等待0ms,类似于非阻赛
返回值: 负数:出错
大于0,表示fd中有数据
等于0: 时间到
4,如果应用中使用poll对设备文件进行了监控,那么设备驱动就必须实现poll接口
unsigned int key_drv_poll(struct file *filp, struct poll_table_struct *pts)
{
// 返回一个mask值
unsigned int mask;
// 调用poll_wait,将当前到等待队列注册系统中
poll_wait(filp, &key_dev->wq_head, pts);
// 1,当没有数据到时候返回一个0
if(!key_dev->key_state)
mask = 0;
// 2,有数据返回一个POLLIN
if(key_dev->key_state)
mask |= POLLIN;
return mask;
}
const struct file_operations key_fops = {
.poll = key_drv_poll,
};
5,异步信号通知: 当有数据到时候,驱动会发送信号(SIGIO)给应用,就可以异步去读写数据,不用主动去读写
a,应用–处理信号,主要是读写数据
void catch_signale(int signo)
{
if(signo == SIGIO)
{
printf("we got sigal SIGIO");
// 读取数据
read(fd, &event, sizeof(struct key_event));
if(event.code == KEY_ENTER)
{
if(event.value)
{
printf("APP__ key enter pressed\n");
}else
{
printf("APP__ key enter up\n");
}
}
}
}
// 1,设置信号处理方法
signal(SIGIO,catch_signale);
// 2,将当前进程设置成SIGIO的属主进程
fcntl(fd, F_SETOWN, getpid());
// 3,将io模式设置成异步模式
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | FASYNC );
b,驱动--发送信号
1,需要和进程进行关联--记录信号该发送给谁
实现一个fasync的接口
int key_drv_fasync(int fd, struct file *filp, int on)
{
//只需要调用一个函数记录信号该发送给谁
return fasync_helper(fd, filp, on, &key_dev->faysnc);
}
2,在某个特定的时候去发送信号,在有数据的时候
//发送信号
kill_fasync(&key_dev->faysnc, SIGIO, POLLIN);
3、中断下半部和timer定时器的实现
1、中断的下半部
1,softirq: 处理比较快,但是内核级别的机制,需要修改整个内核源码,不推荐也不常用
2,tasklet: 内部实现实际调用了softirq
3, workqueue: 工作队列
1,tasklet:
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long); // 下半部的实现逻辑
unsigned long data; // 传递给func
};
a, 初始化
struct tasklet_struct mytasklet;
tasklet_init(struct tasklet_struct * t, void(* func)(unsigned long), unsigned long data)
例子:
void key_tasklet_half_irq(unsigned long data)
{
// 表示有数据,需要去唤醒整个进程/等待队列
wake_up_interruptible(&key_dev->wq_head);
//同时设置标志位
key_dev->key_state = 1;
//发送信号
kill_fasync(&key_dev->faysnc, SIGIO, POLLIN);
}
tasklet_init(&key_dev->mytasklet, key_tasklet_half_irq, 45);
b,在上半部中放入到内核线程中--启动
// 启动下半步
tasklet_schedule(&key_dev->mytasklet);
c,模块卸载的时候:
tasklet_kill(&key_dev->mytasklet);
2,工作队列和工作
typedef void (*work_func_t)(struct work_struct *work);
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func;
};
a, 初始化
void work_irq_half(struct work_struct *work)
{
printk("-------%s-------------\n", __FUNCTION__);
// 表示有数据,需要去唤醒整个进程/等待队列
wake_up_interruptible(&key_dev->wq_head);
//同时设置标志位
key_dev->key_state = 1;
//发送信号
kill_fasync(&key_dev->faysnc, SIGIO, POLLIN);
}
struct work_struct mywork;
INIT_WORK(struct work_struct *work, work_func_t func);
b, 在上半部中放入到内核线程中--启动
schedule_work(&key_dev->mywork);
(十二~十五)、平台总线开发
- 设备驱动模型:bus, driver, device struct bus_type :总线对象,描述一个总线,管理device和driver,完成匹配
• struct bus_type {
const char *name;
int (*match)(struct device *dev, struct device_driver *drv);
}
- 注册和注销
• int bus_register(struct bus_type *bus)
void bus_unregister(struct bus_type *bus)
device对象:设备对象,描述设备信息,包括地址,中断号,甚至其他自定义的数据
struct device {
struct kobject kobj; //所有对象的父类
const char *init_name;
// 在总线中会有一个名字,用于做匹配,在/sys/bus/mybus/devices/名字
struct bus_type *bus; //指向该device对象依附于总线的对象
void *platform_data; // 自定义的数据,指向任何类型数据
}
注册和注销的方法:
int device_register(struct device *dev)
void device_unregister(struct device *dev)driver对象:描述设备驱动的方法(代码逻辑)
struct device_driver {
const char *name;
// 在总线中会有一个名字,用于做匹配,在/sys/bus/mybus/drivers/名字
struct bus_type *bus;//指向该driver对象依附于总线的对象
int (*probe) (struct device *dev); // 如果device和driver匹配之后,driver要做的事情
int (*remove) (struct device *dev); // 如果device和driver从总线移除之后,driver要做的事情
}
注册和注销:
int driver_register(struct device_driver *drv)
void driver_unregister(struct device_driver *drv)
如何实现总线匹配,匹配成功之后会自动调用driver的probe方法:
1, 实现bus对象中 match方法
2, 保证driver和device中名字要一样
====================================================================
- 平台总线模型:
为什么会有平台总线:
用于平台升级:三星: 2410, 2440, 6410, s5pc100 s5pv210 4412
硬件平台升级的时候,部分的模块的控制方式,基本上是类似的
但是模块的地址是不一样
gpio控制逻辑: 1, 配置gpio的输入输出功能: gpxxconf
2, 给gpio的数据寄存器设置高低电平: gpxxdata
逻辑操作基本上是一样的
但是地址不一样
uart控制:1,设置8n1,115200, no AFC
UCON,ULCON, UMODOEN, UDIV
逻辑基本上是一样的
但是地址不一样
问题:
当soc升级的时候, 对于相似的设备驱动,需要编写很多次(如果不用平台总线)
但是会有大部分重复代码
解决:引入平台总线
device(中断/地址)和driver(操作逻辑) 分离
在升级的时候,只需要修改device中信息即可(中断/地址)
实现一个driver代码能够驱动多个平台相似的模块,并且修改的代码量很少
- 平台总线中的三元素: 1, bus platform_bus:不需要自己创建,开机的时候自动创建
struct bus_type platform_bus_type = {
.name = “platform”,
.dev_groups = platform_dev_groups,
.match = platform_match,
.uevent = platform_uevent,
.pm = &platform_dev_pm_ops,
};
匹配方法:
1,优先匹配pdriver中的id_table,里面包含了支持不同的平台的名字
2,直接匹配driver中名字和device中名字
struct platform_device *pdev = to_platform_device(dev);
struct platform_driver *pdrv = to_platform_driver(drv);
if (pdrv->id_table)// 如果pdrv中有idtable,平台列表名字和pdev中的名字
return platform_match_id(pdrv->id_table, pdev) != NULL;
/* fall-back to driver name match */
return (strcmp(pdev->name, drv->name) == 0);
2,device对象:
struct platform_device {
const char *name; //用于做匹配
int id; // 一般都是直接给-1
struct device dev; // 继承了device父类
u32 num_resources; // 资源的个数
struct resource *resource; // 资源:包括了一个设备的地址和中断
}
注册和注销
int platform_device_register(struct platform_device * pdev);
void platform_device_unregister(struct platform_device * pdev)
3,driver对象
struct platform_driver {
int (*probe)(struct platform_device *); //匹配成功之后被调用的函数
int (*remove)(struct platform_device *);//device移除的时候调用的函数
struct device_driver driver; //继承了driver父类
|
const char *name;
const struct platform_device_id *id_table; //如果driver支持多个平台,在列表中写出来
}
注册和注销
int platform_driver_register(struct platform_driver *drv);
void platform_driver_unregister(struct platform_driver *drv)
==========================================
编写代码: 编写一个能在多个平台下使用的led驱动
1,注册一个platform_device,定义资源:地址和中断
struct resource {
resource_size_t start; // 开始
resource_size_t end; //结束
const char *name; //描述,自定义
unsigned long flags; //区分当前资源描述的是中断(IORESOURCE_IRQ)还是内存(IORESOURCE_MEM)
struct resource *parent, *sibling, *child;
};
2,注册一个platform_driver,实现操作设备的代码
注册完毕,同时如果和pdev匹配成功,自动调用probe方法:
probe方法: 对硬件进行操作
a,注册设备号,并且注册fops–为用户提供一个设备标示,同时提供文件操作io接口
b, 创建设备节点
c, 初始化硬件
ioremap(地址); //地址从pdev需要获取
readl/writle();
d,实现各种io接口: xxx_open, xxx_read, …
获取资源的方式:
//获取资源
// 参数1: 从哪个pdev中获取资源
// 参数2: 资源类型
// 参数3: 表示获取同种资源的第几个
struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num)