PCI设备简介和PCI驱动使用函数

一,PCI设备简介

  1.  PCI总线的特点:
    

(1)速度上快,时钟频率提高到33M,而且还为进一步把时钟频率提高到66MHZ、总线带宽提高到64位留下了余地。(2)对于地址的分配和设置,系统软件课自动设置,每块外设通过某种途径告诉系统该外设有几个存储区间和I/O地址区间,每个区间的大小以及本地地址。系统软件知道了总共有多少外设以及各种的存储空间后就会统一为外设分配物理地址。(3)对于总线的竞争,PCI总线上配备了一个仲裁器,遇到冲突,仲裁器会选择其中之一暂时成为当前的主设备,而其他只能等待。同时考虑到这样的效率问题,PCI总线为写提纲了缓冲,故写比读会快。(4)对于总线扩充问题,PCI总线引入HOST-PCI桥(北桥),PCI-PCI桥,PCI-ISA桥(南桥)。CPU与内存通过系统总线连接,北桥连接内存控制器与主PCI总线,南桥连接主PCI总线和ISA总线,PCI-PCI桥连接主PCI总和次层PCI总线。

  1.  PCI设备概述
    

每个PCI设备有许多地址配置的寄存器,初始化时要通过这些寄存器来配置该设备的总线地址,一旦完成配置以后,CPU就可以访问该设备的各项资源了。PCI标准规定每个设备的配置寄存器组最多可以有256个连续的字节空间,开头64个字节叫头部,分为0型(PCI设备)和1型(PCI桥)头部,头部开头16个字节是设备的类型、型号和厂商等。这些头部寄存器除了地址配置的作用,还能使CPU能够探测到相应设备的存在,这样就不需要用户告诉系统都有哪些设备了,而是改由CPU通过一个号称枚举的过程自动扫描探测所有挂接在PCI总线上的设备。

设备的配置寄存器组采用相同的地址,由所在总线的PCI桥在访问时附加上其他条件区分,对于I386处理器,有两个32位寄存器,0XCF8为地址寄存器,0XCFC为数据寄存器。地址寄存器写入的内容包括总线号,设备号,功能号。逻辑地址(XX:YY.Z),XX表示PCI总线号,最多256个总线。YY表示PCI设备号,最多32个设备。Z表示PCI设备功能号,最多8个功能。

  1.  查询PCI总线和设备的命令
    

    查看PCI总线和PCI设备组成的树状图 lspci –t

    查看配置区的情况 lspci –x,注意PCI寄存器是小端格式

  2.  PCI总线架构
    

所有的根总线都链接在pci_root_buses链表中。Pci_bus ->device链表链接着该总线下的所有设备。而pci_bus->children链表链接着它的下层总线,对于pci_dev来说,pci_dev->bus指向它所属的pci_bus. Pci_dev->bus_list链接在它所属bus的device链表上。此外,所有pci设备都链接在pci_device链表中。

二.PCI驱动

  1.  PCI寻找空间
    
    PCI设备包括杀个寻址空间:配置空间,I/O端口空间,内存空间。

1.1 PCI配置空间:

内核为驱动提供的函数:

pci_read_config_byte/word/dword(struct pci_dev *pdev, int offset, int *value)

pci_write_config_byte/word/dword(struct pci_dev *pdev, int offset, int *value)

配置空间的偏移定义在include/linux/pci_regs.h

1.2 PCI的I/O和内存空间:

从配置区相应寄存器得到I/O区域的基址:

pci_resource_start(struct pci_dev *dev, int bar) Bar值的范围为0-5。

从配置区相应寄存器得到I/O区域的内存区域长度:

pci_resource_length(struct pci_dev *dev, int bar) Bar值的范围为0-5。

从配置区相应寄存器得到I/O区域的内存的相关标志:

pci_resource_flags(struct pci_dev *dev, int bar) Bar值的范围为0-5。

申请I/O端口:

request_mem_region(io_base, length, name)

读写:

inb() inw() inl() outb() outw() outl()

2. PCI总线支持的设备

PCI驱动程序向PCI子系统注册其支持的厂家ID,设备ID和设备类编码。使用这个数据库,插入的卡通过配置空间被识别后,PCI子系统把插入的卡和对应的驱动程序绑定。

PCI设备列表

struct pci_device_id {
__u32 vendor, device; /* Vendor and device ID or PCI_ANY_ID*/

   __u32 subvendor, subdevice; /* Subsystem ID's or PCI_ANY_ID */

   __u32 class, class_mask;      /* (class,subclass,prog-if) triplet */

   kernel_ulong_t driver_data;    /* Data private to the driver */

};

   注意:如果可以处理任何情况,可将相应的寄存器设置为PCI_ANY_ID。

3. PCI驱动其他API

获取驱动私有数据:pci_get_drvdata();

使能PCI设备:pci_enable_device()

总线主DMA模式设置:pci_set_master()

1.pci驱动注册

pci_register_driver(struct pci_driver *drv)

static struct pci_driver hamachi_driver = {
.name = DRV_NAME,
.id_table = hamachi_pci_tbl,
.probe = hamachi_init_one,
.remove = __devexit_p(hamachi_remove_one),
};

2.私有数据

pci_set_drvdata 设置驱动私有数据

pci_get_drvdata 获取驱动私有数据

3.PCI配置空间

pci_read_config_byte/word/dword(struct pci_dev *pdev, int offset, int *value)

pci_write_config_byte/word/dword(struct pci_dev *pdev, int offset, int *value)

4.PCI的I/O和内存空间

pci_resource_start(struct pci_dev *dev, int bar) Bar值的范围为0-5 ;从配置区相应寄存器得到I/O区域的基址:

pci_resource_length(struct pci_dev *dev, int bar) Bar值的范围为0-5;从配置区相应寄存器得到I/O区域的内存区域长度

pci_resource_flags(struct pci_dev *dev, int bar) Bar值的范围为0-5;从配置区相应寄存器得到I/O区域的内存的相关标志

request_mem_region(io_base, length, name) 申请I/O端口

release_mem_region(io_base, length, name) 释放I/O端口
inb() inw() inl() outb() outw() outl() 读写I/O端口

pci_enable_device 启用设备的I/O
和内存资源,分配不足的资源,如果需要,还要唤醒一个处于暂停状态的设备。需要注意的是,这个操作可能会失败。

pci_set_master 设定设备工作在总线主设备模式

ioremap_nocache 让CPU 可以访问设备的内存

pci_dma_supported

pci_set_dma_mask()和dma_set_mask()辅助函数用于检查总线是否可以接收给定大小的总线地址(mask),如果可以,则通知总线层给定的外围设备将使用该大小的总线地址。

pci_alloc_consistent 返回一致性dma映射缓冲区的虚拟地址

upci_free_consistent 释放一致性dma缓冲区映射

pci_save_state 配置空间(包括PCI、PCI-X、PCI-E)存到pci_dev里头

pci_restore_state 从pci_dev里头保存的值来恢复配置空间的状态

原子操作

Atomic_read(v) 返回原子变量的值

atomic_set(v,i) 设置原子变量的值

Atomic_add(int i, atomic_t *v) 原子的递增计数的值

static inline int atomic_add_return(int i, atomic_t *v) 只不过将变量v的最新值返回

Atomic_sub(int i, atomic_t *v) 原子的递减计数的值

static inline int atomic_sub_return(int i, atomic_t *v) 只不过将变量v的最新值返回

atomic_cmpxchg(atomic_t *ptr, int old, int new) 比较old和原子变量ptr中的值,如果相等,那么就把new值赋给原子变量。 返回旧的原子变量ptr中的值

atomic_clear_mask 原子的清除掩码

atomic_inc(v) 原子变量的值加一

atomic_inc_return(v) 同上,只不过将变量v的最新值返回

atomic_dec(v) 原子变量的值减去一

atomic_dec_return(v) 同上,只不过将变量v的最新值返回

atomic_sub_and_test(i, v) 给一个原子变量v减去i,并判断变量v的最新值是否等于0

atomic_add_negative(i,v) 给一个原子变量v增加i,并判断变量v的最新值是否是负数

static inline int atomic_add_unless(atomic_t *v, int a, int u) 只要原子变量v不等于u,那么就执行原子变量v加a的操作。 如果v不等于u,返回非0值,否则返回0值

char设备注册

1.int register_chrdev_region(dev_t from, unsigned count, const char *name)

 为一个字符驱动获取一个或多个设备编号来使用。用于分配指定的设备编号范围。如果申请的设备编号范围跨越了主设备号,它会把分配范围内的编号按主设备号分割成较小的子范围,并在每个子范围上调用 __register_chrdev_region() 。如果其中有一次分配失败的话,那会把之前成功分配的都全部退回。

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name)

用于动态申请设备编号范围,这个函数好像并没有检查范围过大的情况,不过动态分配总是找个空的散列桶,所以问题也不大。通过指针参数返回实际获得的起始设备编号。

void unregister_chrdev_region(dev_t from, unsigned count)

void cdev_init(struct cdev *cdev, const struct file_operations *fops) cdev 静态内存定义初始化

struct cdev *cdev_alloc(void) cdev动态内存定义初始化

int cdev_add(struct cdev *p, dev_t dev, unsigned count)初始化 cdev 后,把它添加到系统中去

void cdev_del(struct cdev *p)释放 cdev 占用的内存

2.misc注册cdev

static struct file_operations led_fops;

static struct miscdevice up4412_led_dev = {
.minor = MISC_DYNAMIC_MINOR,
.name = “abc”,
.fops = &led_fops,
};

注册(返回0成功):
ret = misc_register(&up4412_led_dev);
注销:
misc_deregister(&up4412_led_dev);

申请内存

get_zeroed_page(unsigned int flags);
返回一个指向新页的指针并且用零填充了该页.
__get_free_page(unsigned int flags);

类似于 get_zeroed_page, 但是没有清零该页.
__get_free_pages(unsigned int flags, unsigned int order);

分配并返回一个指向一个内存区第一个字节的指针, 内存区可能是几个(物理上连续)页长但是没有清零.

kmalloc()分配连续的物理地址,用于小内存分配

void *vmalloc(unsigned long size)在虚拟内存空间分配一块连续的内存区(虚拟空间连续但是物理空间不一定连续)

void vfree(void *addr)

__pa( address )转换虚拟地址到物理地址

__va(addrress ) 将物理地址转换为虚拟地址

#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))

void free_page(unsigned long addr)
void free_pages(unsigned long addr, unsigned long order)

内核链表

struct list_head my_head;

(1)初始化
INIT_LIST_HEAD(&my_head);
每个list_head在加入链表之前,都要先初始化一下

(2)添加
list_add(new, &my_head);
list_add_tail(new, &my_head);
向链表中加入新成员

(3)删除
list_del(new);

(4)通过list_head的指针找到外部数据结构体的指针
利用container_of宏

struct b {
long test;
char ch;
struct list_head list;

};

void my_test(struct list_head *entry)
{
//通过entry找到外部的结构体b
struct b *tmp = container_of(entry, struct b, list);
printk("%d:%c\n", tmp->test, tmp->ch);

}

(5)链表的遍历

struct list_head *pos;
struct b *tmp;
list_for_each(pos, &my_head) {
tmp = container_of(pos, struct b, list);

}

list_for_each_entry(…);

struct b *tmp;
list_for_each_entry(tmp, &my_head, list) {
printk("%d\n", tmp->test);

}

如果遍历链表的目的是释放链表,推荐使用:
list_for_each_entry_safe(…);

struct b *tmp1, *tmp2;
list_for_each_entry_safe(tmp1, tmp2, &my_head, list) {
list_del(&tmp1->list);
kfree(tmp1);
…;
}

访问寄存器

把物理地址映射成虚拟地址

static void __iomem *vir_base;
vir_base = ioremap(GPIO_BASE, GPIO_SIZE);
if (!vir_base) {
printk(“Cannot ioremap\n”);
return -EIO;
}

访问寄存器,一般采用基地址加偏移的模式

/* 8位寄存器 */
char value;
value = readb(vir_base + offset);
writeb(value, (vir_base+offset));

/* 16位寄存器 */
short value;
value = readw(vir_base+offset);
writew(value, (vir_base+offset));

/* 32位寄存器 */
int value;
value = readl(vir_base+offset);
writel(value, (vir_base+offset));

/* 64位寄存器 */
u64 value;
value = readq(vir_base+offset);
writeq(value, (vir_base+offset));

//如果不再访问寄存器,应该取消映射
iounmap(vir_base);

gpio库的使用

在<mach/gpio.h>中获得GPIO的编号赋给gpio_num

向gpio库申请使用gpio
ret = gpio_request(gpio_num, “myio”)

对io进行配置
s3c_gpio_cfgpin(gpio_num, S3C_GPIO_OUTPUT)
还可以配置为S3C_GPIO_INPUT和S3C_GPIO_SFN(x)
上述宏定义在<plat/gpio-cfg.h>

设置gpio输出0或1
gpio_set_value(gpio_num, 0|1)

获得gpio输出的值(0或1)
int ret = gpio_get_value(gpio_num)

释放gpio
gpio_free(gpio_num)

蜂鸣器驱动

在用pwm库来控制GPIO之前,首先要将GPIO配置为PWM输出:
int gpio_num = EXYNOS4_GPD0(0)
gpio_request(…)
s3c_gpio_cfgpin(gpio_num, S3C_GPIO_SFN(2))

struct pwm_device *dev;

申请pwm通道(根据4412手册,id从0到3)
dev = pwm_request(pwd_id, “xxx”)

释放pwm通道
pwm_free(dev)

配置pwm的占空比和频率,时间以ns为单位
pwm_config(dev, 一个周期中高电平的ns数,整个周期的ns数)
内核中不要用浮点数,假如占空比为47%,则计算高电平的ns数可以
1周期的ns数/100*47

使能pwm
pwm_enable(dev)

关闭pwm
pwm_disable(dev)

内核自旋锁

(1)临界区中只能进入一个人
(2)等待的人忙等(只有SMP才会真正忙等)
(3)持有锁的人不能睡眠

使用之前先初始化

spinlock_t mylock;

spin_lock_init(&mylock);

普通加解锁
spin_lock(&mylock);
临界区
spin_unlock(&mylock);

如果临界区可能被中断处理函数打断,并影响到要保护的变量,则应该在加锁的同时关闭中断
unsigned long flags;
spin_lock_irqsave(&mylock, flags);加锁同时关中断,将CPSR的当前值存储到flags中
临界区
spin_unlock_irqrestore(&mylock, flags);解锁时打开中断,并将flags的值恢复到CPSR中

mutex互斥锁

mutex的特性:(等待锁的时间常常为ms级)
(1)临界区中一个人
(2)睡眠等
(3)持有锁时可以睡眠(必须确保能被唤醒)

用之前要先初始化

struct mutex mylock;

mutex_init(&mylock);

加锁和解锁
mutex_lock(&mylock);
ret = mutex_lock_interruptible(&mylock);
if (ret)
return -ERESTARTSYS;
临界区
mutex_unlock(&mylock);

semaphore(信号量)

在现在的内核中,基本上已经不用semaphore来保护临界区。如果内核中某些资源限定了访问人数(比如只允许3个人同时访问),这时候可以用semaphore进行保护。

按照资源的限定初始化信号量

struct semaphore mysem;

sema_init(&mysem, 3);

down(&mysem);
ret = down_interruptible(&mysem);
…//访问受限资源的代码
up(&mysem);

内核队列

struct workqueue_struct *create_workqueue(const char *name)用于创建一个workqueue队列,为系统中的每个CPU都创建一个内核线程

struct workqueue_struct *create_singlethread_workqueue(const char *name)创建workqueue,只创建一个内核线程

void destroy_workqueue(struct workqueue_struct *wq)释放workqueue队列

int schedule_work(struct work_struct *work)调度执行一个具体的任务,执行的任务将会被挂入Linux系统提供的workqueue

int schedule_delayed_work(struct delayed_work *work, unsigned long delay)延迟一定时间去执行一个具体的任务,功能与schedule_work类似,多了一个延迟时间

int queue_work(struct workqueue_struct *wq, struct work_struct *work)调度执行一个指定workqueue中的任务

int queue_delayed_work(struct workqueue_struct *wq, struct delayed_work *work, unsigned long delay)延迟调度执行一个指定workqueue中的任务,功能与queue_work类似

int cancel_delayed_work(struct delayed_work *work)在这个工作还未执行的时候就把它给取消掉

void flush_workqueue(struct workqueue_struct *wq);

void flush_scheduled_work(void)一般在调用cancel_delayed_work后都会继续调用flush_delayed_work这个是用来等到正在执行的队列执行完。实际上后者是为了解决cancel时的死锁问题。

要使用工作队列,首先要做的是创建一些需要推后完成的工作

DECLARE_WORK(name,void (*func) (void *), void *data)创建一个名为name,待执行函数为func,参数为data的work_struct结构

DECLARE_DELAYED_WORK(name, func);

INIT_WORK(structwork_struct *work, woid(*func) (void *), void *data)在运行时通过指针创建一个工作

PREPARE_WORK(struct work_struct work, work_func_t func);
INIT_DELAYED_WORK(struct delayed_work work, work_func_t func);
PREPARE_DELAYED_WORK(struct delayed_work work, work_func_t func);

wait_queue_head_t mywait;

队列头使用前要初始化
init_waitqueue_head(&mywait);

进入睡眠
wait_event(mywait, dev->wp!=dev->buf_size);
ret = wait_event_interruptible(mywait, dev-wp!=dev->buf_size);

唤醒等待队列中的睡眠进程

wake_up(&mywait);

wake_up_interruptible(&mywait);

ndelay(10); //延迟10ns
udelay(20); //延迟20us
mdelay(30); //延迟30ms

struct timeval tval;
struct timespec tspec;

调用内核的函数来获得绝对时间
do_gettimeofday(&tval);
getnstimeofday(&tspec);

定时器

声明定时器
struct timer_list mytimer;

定时器的执行函数,当定时器到期后,由硬件定时器中断执行一次
static void my_timer_func(unsigned long data)
{
…//不可睡眠
}

初始化定时器
setup_timer(&mytimer, my_timer_func, data);
初始化定时器时传入的参数为timer_list的指针;执行函数;传给执行函数的参数

启动定时器
mod_timer(&mytimer, jiffies+HZ);
定时器一旦启动,就会加入一个timer_list的链表,一旦到时,就会被执行。
启动定时器的人和执行的人不是一个。即使启动者退出,定时器仍然执行。

删除定时器
del_timer(&mytimer);
如果模块要rmmod,在卸载之前,必须删除所有没执行的定时器。

内核中断

中断号都定义在<mach/irqs.h>中,可以用两种不同的方法来查找中断号:

(1)芯片内部的外设
首先明确设备的名字,然后利用名字匹配,自行在irqs.h中找到对应的中断号;
比如看门狗设备对应的中断号为IRQ_WDT, rtc硬件对应的为IRQ_RTC_ALARAM/IRQ_RTC_TIC

(2)芯片外部连接的设备
由于设备的中断引脚都连接到GPIO,因此可以利用GPIO号来找到中断号
中断号 = gpio_to_irq(GPIO号)

驱动人员在设计中断处理函数时,要遵循的要求是:
(1)可嵌套不可重入
(2)不能睡眠
(3)如果硬件有中断的状态寄存器,软件要负责清除中断的标志位。一般来说,如果不清除标志位,设备无法再次产生中断
kzalloc(size, GFP_KERNEL); //可能睡眠
kzalloc(size, GFP_ATOMIC); //不会睡眠

确定中断号
#define KEY_IRQ gpio_to_irq(gpio号);

中断处理函数
static irqreturn_t key_service(int irq, void *dev_id)
{

return IRQ_HANDLED 或 IRQ_NONE;
}

注册中断处理函数,必须检查返回值

u32 flags = IRQF_TRIGGER_FALLING
| IRQF_TRIGGER_RISING;

ret = request_irq(KEY_IRQ, /* 中断号 /
key_service, /
中断处理函数 /
flags, /
中断的标志 /
“xxx”, /
中断处理函数的名字 /
dev_id);
/
最后的参数dev_id为传给中断处理函数的参数,一般会设置为私有结构体的指针,不能为NULL */
实际上,如果是非共享的中断,dev_id可以为NULL

注销中断处理函数
free_irq(irq, dev_id) dev_id一定要和request_irq中的最后一个参数一致。

人为关闭(mask)/打开某个中断:
disable_irq(int irq);
enable_irq(int irq);
上面的两个函数支持嵌套,也就是说,如果调用了3次disable_irq,需要enable_irq3次,才能真正使能中断
要确保先调用disable_irq,再调用enable_irq;

如果要屏蔽整个cpu的中断,可以用:
local_irq_disable();
local_irq_enable();
实际上是将CPSR寄存器的I位置1或清0

中断下半部

在进入中断处理函数前,会默认关闭本中断。对于某些要求迅速响应或数据吞吐量很大的中断,要考虑将中断处理函数的工作分为两个部分,分别称为中断的上半部和下半部。
下半部的实现有多种方法,包括softirq,tasklet和工作队列(work queue)。对于驱动来说,只会使用tasklet和工作队列(work queue)

打开或关闭本cpu的下半部:
local_bh_enable();
local_bh_disable();

tasklet

(1)在上半部执行完后马上执行,但此时中断是全部打开的;
(2)执行tasklet时内核仍处于中断上下文,因此不能睡眠;
(3)tasklet的执行函数不会重入;
(4)如果在tasklet的执行期间再次发生调度,第二次调度无效;

声明tasklet结构体
struct tasklet_struct mytask;

tasklet的执行函数
void bo_service(unsigned long data)
{
}

上半部的执行函数
irqreturn_t up_service(int irq, void *dev_id)
{
//首先完成和硬件交互之类的重要工作
//触发tasklet下半部
tasklet_schedule(&mytask);
或tasklet_hi_schedule(&mytask);

}

初始化tasklet
tasklet_init(&mytask, bo_service, (unsigned long)dev);

工作队列

(1)推后到进程上下文执行,此时中断是全部打开的;
(2)执行work时处于进程上下文,因此可以睡眠;
(3)work的执行函数不会重入;
(4)如果在work的执行期间再次发生调度,第二次调度无效;

宏定义

ioctl()的cmd的大小为 32位,共分 4 个域:
_IOC_DIR

bit31~bit30 2位为 “区别读写” 区,作用是区分是读取命令还是写入命令。

_IOC_SIZE

bit29~bit15 14位为 “数据大小” 区,表示 ioctl() 中的 arg 变量传送的内存大小。

_IOC_TYPE

bit20~bit08 8位为 “魔数"(也称为"幻数")区,这个值用以与其它设备驱动程序的 ioctl 命令进行区别。

_IOC_NR()

bit07~bit00 8位为 “区别序号” 区,是区分命令的命令顺序序号。

猜你喜欢

转载自blog.csdn.net/qq_38350702/article/details/111997194
PCI