本文主要是介绍【c5】proc文件系统,内存分配,数据类型/移植/对齐,内核中断,通过IO内存访问外设,PCI,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
文章目录
- 1.创建proc文件系统接口:之前调试内核时都是通过prink打印内核信息,通过dmesg查看输出的信息。新调试方法:利用proc文件系统在pro文件夹下创建接口,读写这个接口就可实现对内核的调试
- 2.内核内存分配函数:top,free,cat /pro/meminfo查看内存使用情况,cat /pro/slabinfo,cat /pro/buddyinfo,proc/sys/vm/下文件是虚拟内存更详细信息
- 2.1 hello.c:和硬件有关,需要用到物理地址的,都不能使用vmalloc
- 3.内核基础数据类型/移植性/数据对齐:页大小为PAGE_SIZE,不要假设4K,保证可移植性
- 3.1 kdatasize.c:不同的架构(x86_64,arm),基础类型大小可能不同,主要区别在long和指针
- 3.2 kdataalign.c:数据存储时没有特殊指定会自然对齐:在数据项大小的整数倍的地址处存储数据项,如数据项为short类型即2个字节(32位是4个字节),那就要存在地址能整除2的位置,字节对齐可以提高CPU的访问效率
- 4.内核中断的使用,顶半部和底半部:使用中断可实现内核和外设的异步处理,提高通讯效率,降低系统功耗
- 5.通过IO内存访问外设:有的外设将自己的寄存器映射到了物理内存某个区域,那这个区域叫做io内存区域,linux内核访问这个区域能实现对外设控制和读写
- 6.PCI设备驱动:pci是一种标准总线可实现块/网络/字符设备驱动,一部分是pci总线部分,另一是设备业务部分(和具体设备有关不讨论)
1.创建proc文件系统接口:之前调试内核时都是通过prink打印内核信息,通过dmesg查看输出的信息。新调试方法:利用proc文件系统在pro文件夹下创建接口,读写这个接口就可实现对内核的调试
/*
struct proc_ops //pro文件夹下创建接口第一种方式
proc_create()struct seq_operations //第二种方式
proc_create_seq()remove_proc_entry //移除接口
*/
#include<linux/module.h>
#include<linux/uaccess.h>
#include<linux/string.h>
#define PROC_DEBUG
#ifdef PROC_DEBUG
#include<linux/proc_fs.h> //传统第一种方式
#include<linux/seq_file.h> //seq第二种方式
#endifchar * str = "hello proc\n";
#ifdef PROC_DEBUG //由于proc一般用于调试,通常定义一个宏,将proc对应代码包起来,不需要这调试接口时,就可把这个宏注释掉,这样这个宏包含代码不会编译到内核中了。
int hp_open(struct inode * inode, struct file * filp)
{printk(KERN_INFO"open %ld\n",strlen(str));return 0;
}ssize_t hp_read(struct file * filp, char __user * buff, size_t count, loff_t * f_pos)
{ssize_t retval=0;int n = strlen(str);if(*f_pos >= n)goto out;if(*f_pos + count > n)count = n - *f_pos;//如上是判断读的内容是否有效if(copy_to_user(buff,str,count)) //将字符串str赋值到buff用户空间{retval = -EFAULT;goto out;}*f_pos += count;return count;
out:return retval;
}struct proc_ops hp_ops = { //下面 __init函数中proc_create调用hp_ops创建接口文件.proc_open = hp_open,.proc_read = hp_read,
};//1111111111111111111111111111111111111111111111111111111111111111111111111111111111111
void * hp_seq_start (struct seq_file *m, loff_t *pos) //pos表示当前读到哪个位置或写到哪个位置了
{printk(KERN_INFO"seq start\n");if(*pos >= strlen(str)) //pos指当前读到或写到哪个位置,索引return NULL;return &str[*pos]; //拿出字符串中字符,将地址返回,这返回值将来作为下面其他函数的v传入
}void hp_seq_stop(struct seq_file *m, void *v)
{printk(KERN_INFO"seq stop\n"); //清除start函数一些工作,start里开辟一些空间或申请一些锁,这里清除
}void * hp_seq_next (struct seq_file *m, void *v, loff_t *pos) //改变索引值
{printk(KERN_INFO"seq next\n");(*pos)++;if(*pos >= strlen(str))return NULL;return &str[*pos];
}int hp_seq_show (struct seq_file *m, void *v)
{printk(KERN_INFO"seq show\n");seq_putc(m,*(char*)v); //将获得到的字符一个一个打印出return 0;
}const struct seq_operations seq_ops={ //构建这结构体.start = hp_seq_start,.stop = hp_seq_stop,.next = hp_seq_next,.show = hp_seq_show,
};
#endif//11111111111111111111111111111111111111111111111111111111111111111111111111111111111
static int __init hello_init(void)
{printk(KERN_INFO "HELLO LINUX MODULE\n");
#ifdef PROC_DEBUGproc_create("hello_proc",0,NULL,&hp_ops); //第一个参数即显示在pro目录下文件名称,//第二个参数默认0只读权限。第三个参数父节点,null默认pro目录。最后一个参数是操作的结构体地址。proc_create_seq("hello_seq_proc",0,NULL,&seq_ops); //就可在pro目录下创建对应节点
#endifreturn 0;
}static void __exit hello_exit(void)
{
#ifdef PROC_DEBUGremove_proc_entry("hello_proc",NULL); //第二个参数是父节点remove_proc_entry("hello_seq_proc",NULL);
#endifprintk(KERN_INFO "GOODBYE LINUX\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("KGZ");
MODULE_VERSION("V1.0");
rmmod就没有/proc/下接口了,"hello proc"有11个字符(连空格和换行符),所以show next调用了11次,最后一次返回null会调用stop。cat又会再调用一次,调start后直接返回null到stop。
2.内核内存分配函数:top,free,cat /pro/meminfo查看内存使用情况,cat /pro/slabinfo,cat /pro/buddyinfo,proc/sys/vm/下文件是虚拟内存更详细信息
2.1 hello.c:和硬件有关,需要用到物理地址的,都不能使用vmalloc
/*
1.如下两个一般千字节以下空间
kmalloc() 分配空间不清0
kzalloc() 分配空间并清0
kfree()2.如下对于某些应用需要频繁分配或释放固定大小空间,如下可提前创建一个高速缓冲区,从高速缓冲区中分配空间,这样运行速度会快,内存使用效率也会高
struct kmem_cache //slab分配器/专用高速缓存 速度快 利用率高
kmem_cache_create() //创建高速缓冲区,返回地址保存在上面一行的结构指针中,然后可调用下行函数分配空间,使用完后,free释放
kmem_cache_alloc()
kmem_cache_free()
kmem_cache_destroy() //清除高速缓冲区3.大块内存
__get_free_page() //按页分配,单独一页
__get_free_pages() //多页
get_zeroed_page() //清0
free_page()
free_pages()vmalloc() / vfree() //分配的虚拟地址连续,物理地址不连续,效率不高,用在分配大的连续的、只在软件中使用的、用于缓存的内存区域, 和硬件有关和需用到物理地址的都不能用
*/
#include<linux/module.h>
#include<linux/slab.h>
#include<linux/gfp.h> //按页分配__get_free_page(),包含在slab.h中
#include<linux/vmalloc.h>
char * kmlcp;
struct kmem_cache *h_cache;
char * kmemcp;
char * frpgp;
char * vmlcp;static int hello_init(void)
{printk(KERN_INFO "HELLO LINUX MODULE\n");
//111111111111111111111111111111111111 1kmlcp = kmalloc(1024,GFP_KERNEL); //第一个参数:分配空间的大小,第二个参数:常用flag有GFP_KERNEL(kmalloc可休眠)和GFP_ATOMIC(kmalloc不可休眠),中断中分配空间用GFP_ATOMICif(!kmlcp) //不休眠可能会失败,所以这里判断下{return -ENOMEM;}printk(KERN_INFO"kmalloc get addr:%p\n",kmlcp); //分配成功,将地址打印出//111111111111111111111111111111111111 2h_cache = kmem_cache_create("h_cache",512,0,SLAB_HWCACHE_ALIGN|SLAB_POISON,NULL);if(!h_cache){kfree(kmlcp);return -ENOMEM;}kmemcp = kmem_cache_alloc(h_cache,GFP_KERNEL); //第一个参数就是kmem_cache_create返回发的地址if(!kmemcp){//清除分配的资源return -ENOMEM;}printk(KERN_INFO"kmem_cache get addr:%p\n",kmemcp);//111111111111111111111111111111111111 3frpgp =(void *) __get_free_pages(GFP_KERNEL,0); //第二个参数是页面数,以2为底的对数值,0:1 1:2 2:4 3:8 ,填的0分配1个页,填的1分配2个页,填的2分配4个页if(!frpgp){//清除分配的资源return -ENOMEM;}printk(KERN_INFO"free pages get addr:%p\n",frpgp);//111111111111111111111111111111111111 4vmlcp = vmalloc(PAGE_SIZE<<4); //大空间,大于一个页,这里分配16个页空间if(!vmlcp){//清除分配的资源return -ENOMEM;}printk(KERN_INFO"vmalloc get addr:%p\n",vmlcp);return 0;
}static void __exit hello_exit(void)
{printk(KERN_INFO "GOODBYE LINUX\n");
// 1 kfree(kmlcp);
// 2kmem_cache_free(h_cache,kmemcp);kmem_cache_destroy(h_cache);
// 3free_pages((unsigned long)frpgp,0);
// 4 vfree(vmlcp);
}module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("KGZ");
MODULE_VERSION("V1.0");
3.内核基础数据类型/移植性/数据对齐:页大小为PAGE_SIZE,不要假设4K,保证可移植性
3.1 kdatasize.c:不同的架构(x86_64,arm),基础类型大小可能不同,主要区别在long和指针
/*linux内核基础数据类型分三大类:C标准(int,long,char等),linux内核特有大小确定(u32,u16等),特定内核对象(pid_t,ssize_t,size_t等)由于不同平台数据类型大小有区别,要考虑程序可移植性:-Wall(编译时使用这个标志会检查所有不兼容的问题),消除所有警告就可保证程序可移植性。如果编译器支持uint32_t,则不使用u32,使用uint32_t等标准类型基础数据类型除了不同大小外还有存储方式不同,有的系统是大端存储方式,有的是小端,内核提供如下函数进行转换: cpu_to_le32() le32_to_cpu() (小端32位转换为cpu存储类型)cpu_to_be32() be32_to_cpu() (大....)......htonl() (host主机转换为network的long类型) ntohl() (network转主机)htons() (.................short..) ntohs()
*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/utsname.h>
#include <linux/errno.h>static void data_cleanup(void)
{/* never called */
}int data_init(void)
{ssize_t n=90888;printk("arch Size: char short int long ptr long-long "" u8 u16 u32 u64\n");printk("%-12s %3i %3i %3i %3i %3i %3i ""%3i %3i %3i %3i\n",init_uts_ns.name.machine,(int)sizeof(char), (int)sizeof(short), (int)sizeof(int),(int)sizeof(long),(int)sizeof(void *), (int)sizeof(long long), (int)sizeof(__u8),(int)sizeof(__u16), (int)sizeof(__u32), (int)sizeof(__u64));printk("%i, %li, %i, %li\n",(int)sizeof(pid_t),(long)current->pid,(int)sizeof(ssize_t),(long)n);printk("le32:%x be32:%x htonl:%x ntohl:%x\n", cpu_to_le32(0x1234abcd),cpu_to_be32(0x1234abcd),htonl(0x1234abcd),ntohl(0x1234abcd));return -ENODEV;
}module_init(data_init);
module_exit(data_cleanup);
MODULE_LICENSE("Dual BSD/GPL");
如下host是小端,network网络存储都是大端。
3.2 kdataalign.c:数据存储时没有特殊指定会自然对齐:在数据项大小的整数倍的地址处存储数据项,如数据项为short类型即2个字节(32位是4个字节),那就要存在地址能整除2的位置,字节对齐可以提高CPU的访问效率
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/utsname.h>
#include <linux/errno.h>struct c {char c; char t;} c; // 如果不对齐,第二个成员地址减去结构体地址都=1(第一个成员char)
struct s {char c; short t;} s;
struct i {char c; int t;} i;
struct l {char c; long t;} l;
struct ll {char c; long long t;} ll; //long long是8个字节
struct p {char c; void * t;} p;
struct u1b {char c; __u8 t;} u1b;
struct u2b {char c; __u16 t;} u2b;
struct u4b {char c; __u32 t;} u4b;
struct u8b {char c; __u64 t;} u8b;struct {u16 id;u8 a;u64 lun;u16 reserved1;u32 reserved2;
}__attribute__((packed)) scsi; // 属性:不用对齐,每个数据紧挨着struct {u16 id;u8 a;u64 lun;u16 reserved1;u32 reserved2;
} scsi1;static void data_cleanup(void)
{/* never called */
}static int data_init(void)
{printk("arch Align: char short int long ptr long-long "" u8 u16 u32 u64\n");printk("%-12s %3i %3i %3i %3i %3i %3i ""%3i %3i %3i %3i\n",init_uts_ns.name.machine,(int)((void *)(&c.t) - (void *)&c), //第二个成员地址 - 结构体地址 = 地址差即偏移的字节数(int)((void *)(&s.t) - (void *)&s),(int)((void *)(&i.t) - (void *)&i),(int)((void *)(&l.t) - (void *)&l),(int)((void *)(&p.t) - (void *)&p),(int)((void *)(&ll.t) - (void *)&ll),(int)((void *)(&u1b.t) - (void *)&u1b),(int)((void *)(&u2b.t) - (void *)&u2b),(int)((void *)(&u4b.t) - (void *)&u4b),(int)((void *)(&u8b.t) - (void *)&u8b));// 如下打印对齐和不对齐的每个成员地址printk("packed %i unpacked %i\n",(int)sizeof(scsi),(int)sizeof(scsi1));printk(" id a lun reserved1 reserved2\n");printk("scsi %lx %lx %lx %lx %lx",(unsigned long)&scsi.id,(unsigned long)&scsi.a,(unsigned long)&scsi.lun,(unsigned long)&scsi.reserved1,(unsigned long)&scsi.reserved2);printk("scsi1 %lx %lx %lx %lx %lx\n",(unsigned long)&scsi1.id,(unsigned long)&scsi1.a,(unsigned long)&scsi1.lun,(unsigned long)&scsi1.reserved1,(unsigned long)&scsi1.reserved2);return -ENODEV;
}module_init(data_init);
module_exit(data_cleanup);
MODULE_LICENSE("Dual BSD/GPL");
x86_64 / unpacked / scsi1一行都是对齐的,char在地址0上,short在地址2上(因为1不能被2整除),
4.内核中断的使用,顶半部和底半部:使用中断可实现内核和外设的异步处理,提高通讯效率,降低系统功耗
// hello.c
/*
request_irq() //申请中断,申请成功后就可以使用这个中断,一旦中断触发就会调用注册的回调函数
free_irq()
typedef irqreturn_t (*irq_handler_t)(int, void *); //中断回调函数,第一个参数是中断号int类型
enable_irq() //打开指定的中断
disable_irq()
*/
#include<linux/module.h>
#include<linux/gpio.h> //测试中断需用外设,用到了树莓派的gpio
#include<linux/interrupt.h> //中断
#include<linux/proc_fs.h> //用到了proc文件系统
#include<linux/uaccess.h> //用到了内核空间与用户空间数据交互对应函数static struct work_struct work;
unsigned long flags;
void workqueue_fn(struct work_struct *work) //工作队列回调函数 ,//下/底半部(不紧急且耗时)
{printk("hello workqueue\n");
}static irqreturn_t irq_handler(int irq,void *dev) //中断回调函数,第一个参数中断号,第二个参数设备结构地址 , //上/顶半部(紧急工作)
{static int n=0;printk("get irq%d int %d\n",irq,++n); //打印中断号和中断次数schedule_work(&work); //把工作放到内核默认的工作队列中运行return IRQ_HANDLED; //此处退出后,workqueue_fn就会运行
}ssize_t hp_write(struct file * filp, const char __user * buff, size_t count, loff_t * f_pos)
{char a;get_user(a,buff);if(a=='0') //给proc文件写入的是0,关闭中断{printk("disable irq\n");disable_irq(gpio_to_irq(12));//如下不起作用:可能内核不允许关闭所有中断,也可能这里关了中断,在其它地方被打开了//local_irq_disable(); //local_irq_save(flags);}else{printk("enable irq\n");enable_irq(gpio_to_irq(12)); //local_irq_enable();//local_irq_restore(flags);}return count;
}struct file_operations hp_ops = { //proc_create接口,通过proc控制中断开闭.write = hp_write,
};//11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
static int __init hello_init(void)
{int err;printk(KERN_INFO "HELLO LINUX MODULE\n");proc_create("hello_proc",0777,NULL,&hp_ops); //可读可写可执行,主要用到可写INIT_WORK(&work,workqueue_fn); //初始化一个work,将底半部操作放在工作队列中去执行err = request_irq(gpio_to_irq(12),irq_handler,IRQ_TYPE_EDGE_BOTH,"hello-int",NULL);
//上行第一个参数:通过gpio号12获取中断号。第二个参数:中断回调函数irq_handler。第三个参数:触发方式:上升沿下降沿都会触发中断
//第四个参数:hello-int名称会在proc文件系统中显示,第五个参数:指针参数,这个参数会在中断触发后通过irq_handler中一个参数传入if(err<0){printk("irq_request failed\n");remove_proc_entry("hello_proc",NULL);return err;} return 0;
}static void __exit hello_exit(void)
{printk(KERN_INFO "GOODBYE LINUX\n");free_irq(gpio_to_irq(12),NULL); //释放request_irq申请的中断remove_proc_entry("hello_proc",NULL);
}module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("KGZ");
MODULE_VERSION("V1.0");
如下中断号,cpu0核心中断次数,cpu1…,中断控制器pinctrl-bcm2835。
// gpioout.c // 测试中断运行状态
#include <bcm2835.h>
#include<unistd.h>int main(int argc ,char* argv[])
{int n = atoi(argv[1]);bcm2835_init();bcm2835_gpio_fsel(21,BCM2835_GPIO_FSEL_OUTP); //树莓派21号引脚和之前树莓派12号引脚硬件连在一起,这样可通过控制21号引脚高低电平触发12号引脚中断while(n--){bcm2835_gpio_set(21); //高电平sleep(1);bcm2835_gpio_clr(21); //低电平sleep(1);}return 0;
}
如下循环一次(参数是1)中触发两次中断,上升沿一次,下降沿一次。
如下dmesg先是中断回调,再是工作队列。cat /proc/interrupts看出在CPU0上发生2次中断。
如下关闭中断(非0是开中断),再次运行./gpioout 1,dmesg看没有信息更新。
5.通过IO内存访问外设:有的外设将自己的寄存器映射到了物理内存某个区域,那这个区域叫做io内存区域,linux内核访问这个区域能实现对外设控制和读写
// hello.c
/*
request_mem_region() //访问外设前需要先申请这片io内存区域
release_mem_region()
ioremap() //io内存区域(上行申请的)是物理地址,内核使用的是虚拟地址,ioremap将物理地址映射为虚拟地址
iounmap()
ioread32()/ioread8()/ioread16() //读取io内存 //硬件是树莓派,四字节对齐地址读写的话8/16/32位都能读到正常值
iowrite32()/iowrite8()/iowrite16()
*/
#include<linux/module.h>
#include<linux/io.h>unsigned long gpio_base = 0x3f200000; //树莓派gpio基地址
int gpio_len =0xb3; //寄存器范围
struct timer_list t1; //内核定时器,让1s开一次灯,1s关一次灯
int tdelay;
uint8_t flag=0;void timer_fn(struct timer_list *t) //定时器回调函数
{if(flag)iowrite32(ioread32((void *)(gpio_base+0x1c))|(1<<4),(void*)(gpio_base+0x1c)); //1c寄存器将gpio置为高电平elseiowrite32(ioread32((void *)(gpio_base+0x28))|1<<4,(void*)(gpio_base+0x28)); //28寄存器将gpio置为低电平flag=!flag;mod_timer(&t1,jiffies+msecs_to_jiffies(1000)); //gpio4接了一个led灯,以1s频率亮灭
}//11111111111111111111111111111111111111111111111111111111111111111111111111111111
static int __init hello_init(void)
{printk(KERN_INFO "HELLO LINUX MODULE\n");// if (! request_mem_region(gpio_base,gpio_len , "gpio")) { //理论上先申请这片区域,不过树莓派已经将这片区域申请好了,可通过cat /proc/iomem了解i/o内存分配情况(gpio....)// printk(KERN_INFO " can't get I/O mem address 0x%lx\n",// gpio_base);// return -ENODEV;// }gpio_base = (unsigned long)ioremap(gpio_base,gpio_len);//下行将基地址内容读出来或上要改变的值,再写回去。iowrite32第一个参数是写的值,第二个参数是写的地址iowrite32(ioread32((void *)gpio_base)|(1<<12),(void*)gpio_base); //这一整行代码意思是将pin4设置为输出,具体寄存器含义下载树莓派芯片手册查看printk(KERN_INFO"gpio remap base:0x%lx\n",gpio_base); //打印地址//如下gpio地址是4字节对齐的,可以用如下8 16 32读, 如果将gpio_base+1,+2,+3就不对了printk(KERN_INFO"read %x %x %x\n",ioread8((void *)(gpio_base)),ioread16((void *)(gpio_base)),ioread32((void *)(gpio_base)));timer_setup(&t1,timer_fn,0); //初始化定时器mod_timer(&t1,jiffies+msecs_to_jiffies(1000)); //设置溢出时间1s return 0;
}static void __exit hello_exit(void)
{printk(KERN_INFO "GOODBYE LINUX\n");//release_mem_region(gpio_base,gpio_len);del_timer(&t1); //删除定时器iounmap((void *)gpio_base);
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("Dual BSD/GPL");//许可 GPL、GPL v2、Dual MPL/GPL、Proprietary(专有)等,没有内核会提示
MODULE_AUTHOR("KGZ"); //作者
MODULE_VERSION("V1.0"); //版本
如下make,insmod,打印的地址是虚拟地址,8位读到的是0,16位读到的是1900。
6.PCI设备驱动:pci是一种标准总线可实现块/网络/字符设备驱动,一部分是pci总线部分,另一是设备业务部分(和具体设备有关不讨论)
如下是PCI设备的配置寄存器值:每个PCI设备中都有一个配置区域,这个区域保存了PCI设备信息,下图是前64字节内容(标准化的)。
// pci_skel.c
/*
struct pci_device_id 用这结构体构造一个数组,数组中包含驱动支持的所有设备
PCI_DEVICE() 这个宏通过vendor-id和device-id填充上面pci_device_id结构体内容
PCI_DEVICE_CLASS() 通过class类填充pci_device_id结构体内容
MODULE_DEVICE_TABLE() 上面填充好结构体构造的数组后,调用MODULE_DEVICE_TABLE()宏,导出pci_device_id结构体到用户空间,使热插拔和模块装载系统知道什么模块针对什么硬件设备struct pci_driver 利用这结构体将驱动注册到内核中
pci_register_driver() 注册
pci_unregister_driver() 注销在读取pci设备的配置寄存器或io空间/io地址时,需要先如下调用:
pci_enable_device() 激活/初始化pci设备,比如唤醒设备、读写配置信息等
pci_disable_device() 关闭设备如下内核提供一系列函数读取pci设备配置信息
pci_read_config_byte() 8位
pci_read_config_word() 16位
pci_read_config_dword() 32位
pci_resource_start() 获取区域信息(bar info) pci支持6个区域(io端口/io内存),获取io空间起始地址
pci_resource_end() 获取io空间结束地址
pci_resource_flags() 获取io空间标志信息pci_request_regions() 获得io空间地址后,调用这行函数申请这片区域,跟request_mem_region()一样
pci_release_regions()pci_ioremap_bar() 物理地址映射到虚拟地址空间,跟ioremap一样,作了必要的检查pci_set_drvdata() 设置驱动私有数据
pci_get_drvdata() 获取驱动私有数据
*/
#include <linux/module.h>
#include <linux/pci.h>struct pci_card //私有数据
{//端口读写变量resource_size_t io; //io空间起始地址long range,flags; //空间大小,空间标志void __iomem *ioaddr; //地址被映射后的虚拟地址int irq; //pci设备中断号
};static struct pci_device_id ids[] = { //pci_device_id里面包含这驱动支持的所有pci设备{ PCI_DEVICE(PCI_VENDOR_ID_INTEL, 0x100e) }, //第一个参数:厂商号。第二个参数:设备id{ PCI_DEVICE(PCI_VENDOR_ID_INTEL,PCI_DEVICE_ID_INTEL_80332_0) },{ 0, } //最后一组是0,表示结束
};
MODULE_DEVICE_TABLE(pci, ids); //导出到用户空间:第一个参数:总线类型。第二个参数:上面数组名称。void skel_get_configs(struct pci_dev *dev) //测试读写配置空间
{uint8_t val1;uint16_t val2;uint32_t val4;pci_read_config_word(dev,PCI_VENDOR_ID, &val2);printk("vendorID:%x",val2);pci_read_config_word(dev,PCI_DEVICE_ID, &val2);printk("deviceID:%x",val2);pci_read_config_byte(dev, PCI_REVISION_ID, &val1);printk("revisionID:%x",val1);pci_read_config_dword(dev,PCI_CLASS_REVISION, &val4);printk("class:%x",val4);
}/* 设备中断服务*/
static irqreturn_t mypci_interrupt(int irq, void *dev_id)
{struct pci_card *mypci = (struct pci_card *)dev_id;printk("irq = %d,mypci_irq = %d\n",irq,mypci->irq);return IRQ_HANDLED;
}//11111111111111111111111111111111111111111111111111111111111111111111111
static int probe(struct pci_dev *dev, const struct pci_device_id *id) // 第一个参数pci设备结构体,第二个参数匹配的pci_device_id
{int retval = 0;struct pci_card *mypci;printk("probe func\n"); if(pci_enable_device(dev)) //激活pci设备{printk (KERN_ERR "IO Error.\n");return -EIO;}mypci = kmalloc(sizeof(struct pci_card),GFP_KERNEL); //私有数据分配一空间if(!mypci){printk("In %s,kmalloc err!",__func__);return -ENOMEM;}//如下是给私有数据的属性赋值mypci->irq = dev->irq; //给私有数据中断号赋值,内核启动时扫描pci设备,给pci设备分配中断号获取基本信息 if(mypci->irq < 0){printk("IRQ is %d, it's invalid!\n",mypci->irq);goto out_mypci;}mypci->io = pci_resource_start(dev, 0); //获得区域0的开始地址mypci->range = pci_resource_end(dev, 0) - mypci->io + 1; //结束地址 - 开始地址 + 1 : 就是空间大小mypci->flags = pci_resource_flags(dev,0); //获取区域0标志,这标志会指示这区域是io内存还是io端口printk("start %llx %lx %lx\n",mypci->io,mypci->range,mypci->flags);printk("PCI base addr 0 is io%s.\n",(mypci->flags & IORESOURCE_MEM)? "mem":"port"); //判断是io内存还是io端口//retval=request_mem_region(mypci->io,mypci->range, "pci_skel");retval = pci_request_regions(dev,"pci_skel"); //要操作这内存区域,首先要分配这内存区,作用同上行if(retval){printk("PCI request regions err!\n");goto out_mypci;}mypci->ioaddr = pci_ioremap_bar(dev,0); //分配成功,就将物理地址映射到内核的虚拟地址中,作用同下行,不过pci.h提供pci_ioremap_bar就用这个//mypci->ioaddr = ioremap(mypci->io,mypci->range); 这里变量的类型与函数参数的类型必须一致,否则会出错if(!mypci->ioaddr){printk("ioremap err!\n");retval = -ENOMEM;goto out_regions;}//申请中断IRQ并给中断号绑定中断服务子函数pci_ioremap_barretval = request_irq(mypci->irq, mypci_interrupt, IRQF_SHARED, "pci_skel", mypci);if(retval){printk (KERN_ERR "Can't get assigned IRQ %d.\n",mypci->irq);goto out_iounmap;}pci_set_drvdata(dev,mypci); //将私有数据保存到pci设备结构体中printk("Probe succeeds.PCIE ioport addr start at %llX, mypci->ioaddr is 0x%p,interrupt No. %d.\n",mypci->io,mypci->ioaddr,mypci->irq);skel_get_configs(dev); //测试读写配置空间return 0;out_iounmap:iounmap(mypci->ioaddr);
out_regions:pci_release_regions(dev);
out_mypci:kfree(mypci);return retval;
} //当probe函数结束后就拿到了pci设备io空间地址,之后业务逻辑代码操作这io地址进行static void remove(struct pci_dev *dev) //移除PCI设备,清除在prob函数中做的工作
{struct pci_card *mypci = pci_get_drvdata(dev); //获得私有数据free_irq (mypci->irq, mypci); //释放中断号iounmap(mypci->ioaddr); //取消地址映射//release_mem_region(mypci->io,mypci->range);pci_release_regions(dev); //释放申请的空间kfree(mypci); //释放私有数据pci_disable_device(dev); //关闭pci设备printk("Device is removed successfully.\n");
}//11111111111111111111111111111111111111111111111111111111111111111111
static struct pci_driver pci_driver = {.name = "pci_skel", //一般和模块名称一样即本文件名称.id_table = ids, //支持的所有设备结构体数组的名称.probe = probe, //当内核检测到和驱动匹配后会调用probe.remove = remove,
};static int __init pci_skel_init(void)
{printk("HELLO PCI\n");return pci_register_driver(&pci_driver); //上面结构体地址
}static void __exit pci_skel_exit(void)
{printk("GOODBYE PCI\n");pci_unregister_driver(&pci_driver); //注销驱动程序
}MODULE_LICENSE("GPL");
module_init(pci_skel_init);
module_exit(pci_skel_exit);
如下网卡驱动在设备启动时就加载了,需先将模块驱动移除rmmod。显示probe func说明调用了probe函数,发现了匹配的设备。mypci->ioaddr
是映射后的虚拟地址。
lspci:列出系统中所有pci的简略信息,总线:设备.功能
。
如下还有一个pci网卡信息。cat /pro/bus/pci/devices也会列出pci设备详细信息。
如下进入目录得到pci设备信息文件。
这篇关于【c5】proc文件系统,内存分配,数据类型/移植/对齐,内核中断,通过IO内存访问外设,PCI的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!