1. PCI寻址空间
1.1 PCI配置空间
内核为驱动提供的函数:
pci_read_config_byte/word/dword(struct pci_dev *pdev, int offset, int *value)
pci_write_config_byte/word/dwrod(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
pci_resource_len(struct pci_dev *dev, int bar)
- 从配置区相应寄存器得到I/O区域的内存的相关标志
pci_resource_flags(struct pci_dev *dev, int bar) // bar的范围0-5
2. PCI总线支持的设备
PCI驱动程序向PCI子系统注册其支持的厂家ID、设备ID和设备类编码。使用这个数据库,插入的卡通过配置空间被识别后,PCI子系统把插入的卡和对应的驱动程序绑定。
-
PCI设备列表
struct pci_device_id { __u32 vendor, device; __u32 subvendor, subdevice; __u32 class, class_mask; kernel_ulong_t driver_data; }注意:如果可以处理任何情况,可将相应的寄存器设置为PCI_ANY_ID。 -
一般使用如下:
#define TSR_VENDER 0x8848 #define TSR_NORMAL_DEVICE 0x5C30 #define TSR_VF_DEVICE 0xDC30 static const struct pci_device_id tsr_ids[] = { { PCI_DEVICE(TSR_VENDER, TSR_NORMAL_DEVICE) }, { 0 }, }; MODULE_DEVICE_TABLE(pci, tsr_ids);
3. PCI驱动其他API
3.1 PCI驱动注册API
// struct pci_driver
static struct pci_driver tsr_drv = {
.name = "SC30", // 驱动程序名
.id_table = tsr_ids, // pci设备配置信息
.probe = tsr_probe, // 设备插入内核时调用
.remove = tsr_remove,// 设备从内核移除时调用
.sriov_configure = tsr_sriov_configure, // 虚拟配置信息
};
pci_register_driver(struct pci_driver *drv);
// 取消注册
pci_unregister_driver(struct pci_drvier *drv);
3.2 私有数据
// 设置驱动私有数据
pci_set_drdata(struct pci_dev *pdev, void *data);
// 获取驱动私有数据
pci_get_drdate(struct pci_dev *pdev);
3.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 offet, int *value);
3.4 PIC的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);
// 释放I/O端口
release_mem_region(io_base, length, name);
// 读写I/O端口
inb() inw() inl() outb() outw() outl()
// 启用设备的I/O
// 和内存资源,分配不足的资源,如果需要,还要唤醒一个处于暂停状态的设备。
// 需要注意的是,这个操作可能会失败。
pci_enable_device(struct pci_dev *pdev);
// 禁止设备
pci_disable_device(struct pci_dev *pdev);
// 设定设备工作在总线主设备模式,设置主总线为DMA模式
pci_set_master(struct pci_dev *pdev);
// 辅助函数用于检查总线是否可以接收给定大小的总线地址(mask)。
// 如果可以,则通知总线层给定的外围设备将使用该大小的总线地址。
pci_set_dma_mask(struct pci_dev *pdev, DMA_BIT_MASK(48));
// 返回一致性dma映射缓冲区的虚拟地址
// pci_alloc_consistent
static inline void *pci_alloc_consistent(struct pci_dev *pdev, size_t size,
dma_addr_t *dma_handle);
// 释放一致性dma缓冲区映射
void pci_free_consistent(struct pci_dev *pdev, size_t size, void *cpu_addr,
dma_addr_t dma_addr);
// 配置空间(包括PCI、PCI-X、PCI-E)存到pci_dev里头
// 用于保存设备的状态,可以将设备的寄存器状态保存到内存中
pci_save_state(struct pci_dev *pdev);
// 从pci_dev里头保存的值来恢复配置空间的状态
// 用于恢复设备的状态,可以将内存中保存的状态恢复到设备中
pci_restore_state(struct pci_dev *pdev);
3.4.1 pci_alloc_consistent
- DMA是由pci_alloc_consistent申请的,那内存到硬件设备的物理地址是如何映射的,大小是如何决定的?
static inline void *pci_alloc_consistent(struct pci_dev *hwdev, size_t size, dma_addr_t *dma_handle) {
return dma_alloc_coherent(hwdev == NULL ? NULL : &hwdev->dev, size, dma_handle, GFP_ATOMIC);
}
// dma-mapping.h
static inline void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flag) {
return dma_alloc_attrs(dev, size, dma_handle, flag, NULL);
}
static inline void *dma_alloc_attrs(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flag, struct dma_attrs *attrs) {
struct dma_map_ops *ops = get_dma_ops(dev);
void *cpu_addr;
BUG_ON(!ops);
if (dma_alloc_from_coherent(dev, size, dma_handle, &cpu_addr))
return cpu_addr;
if (!arch_dma_alloc_attrs(&dev, &flag))
return NULL;
if (!ops->alloc)
return NULL;
cpu_addr = ops->alloc(dev, size, dma_handle, flag, attrs);
debug_dma_alloc_coherent(dev, size, *dma_handle, cpu_addr);
return cpu_addr;
}
extern struct dma_map_ops bad_dma_ops;
static inline struct dma_map_ops *get_dma_ops(struct device *dev)
{
return &bad_dma_ops;
}
dma_noop.c实现了对bad_dma_ops和dma_noop_alloc定义和初始化。
struct dma_map_ops dma_noop_ops = {
.alloc = dma_noop_alloc,
.free = dma_noop_free,
.map_page = dma_noop_map_page,
.map_sg = dma_noop_map_sg,
.mapping_error = dma_noop_mapping_error,
.dma_supported = dma_noop_supported,
};
void *dma_noop_alloc(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t gfp, struct dma_attrs *attrs)
{
void *ret;
ret = (void *)__get_free_pages(gfp, get_order(size)); //最终走向mm内存管理的函数 get_page_from_freelist
if (ret)
*dma_handle = virt_to_phys(ret); //根据虚拟内存转换转换出物理地址
return ret;
}
结论: pci_alloc_consistent函数其实就做了两件事儿
- 从
mm内存管理中获取对应大小的页内存 - 获取
mm获取的页内存的物理地址
**关于虚拟地址和物理地址的转换关系**
// ./arch/arm/include/asm/memory.h
/*
* The limitation of user task size can grow up to the end of free ram region.
* It is difficult to define and perhaps will never meet the original meaning
* of this define that was meant to.
* Fortunately, there is no reference for this in noMMU mode, for now.
*/
// 上述的解释在这里的意思大概也就是说用户可以申请到所有空闲的内存地址
#define TASK_SIZE (CONFIG_DRAM_SIZE)
#define END_MEM (UL(CONFIG_DRAM_BASE) + CONFIG_DRAM_SIZE)
// UL(CONFIG_DRAM_BASE) 可以理解为 数值+UL, 比如12456UL, 就是 123456为无符号长整型
#define PHYS_OFFSET UL(CONFIG_DRAM_BASE)
#define PAGE_OFFSET (PHYS_OFFSET)
#define __virt_to_phys(x) ((x) - PAGE_OFFSET + PHYS_OFFSET)
#define __phys_to_virt(x) ((x) - PHYS_OFFSET + PAGE_OFFSET)
static inline phys_addr_t virt_to_phys(const volatile void *x){return __virt_to_phys((unsigned long)(x));}
static inline void *phys_to_virt(phys_addr_t x) {return (void *)(__phys_to_virt((unsigned long)(x)));}
- 数据到底是怎么从设备传输到主设备的,DMA起到什么作用?
- 多个设备的物理内存如何和CPU内存映射的?
3.5 原子操作
/*
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值
*/
3.6 字符设备注册
/* 为一个字符驱动获取一个或多个设备编号来使用。用于分配指定的设备编号范围。
如果申请的设备编号范围跨越了主设备号,它会把分配范围内的编号按主设备号分割成较小的子范围,
并在每个子范围上调用 __register_chrdev_region() 。
如果其中有一次分配失败的话,那会把之前成功分配的都全部退回。
*/
int register_chrdev_region(dev_t from, unsigned count, const char *name);
/* 用于动态申请设备编号范围,这个函数好像并没有检查范围过大的情况,
不过动态分配总是找个空的散列桶,所以问题也不大。
通过指针参数返回实际获得的起始设备编号。
*/
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);
struct cdev *cdev_alloc(void);
int cdev_add(struct cdev *cdev, dev_t dev, unsigned count);
void cdev_del(struct cdev *cdev);
4. 申请内存
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)
5. 内核链表
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);
…;
}
6. 访问寄存器
// 把物理地址映射成虚拟地址
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);
7. 内核自旋锁
/*
(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中
8. 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);
9. 信号量
/*
semaphore(信号量)
在现在的内核中,基本上已经不用semaphore来保护临界区。
如果内核中某些资源限定了访问人数(比如只允许3个人同时访问),这时候可以用semaphore进行保护。
*/
// 按照资源的限定初始化信号量
struct semaphore mysem;
sema_init(&mysem, 3);
down(&mysem);
ret = down_interruptible(&mysem);
…//访问受限资源的代码
up(&mysem);
10. 内核队列
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);
11. 定时器
// 声明定时器
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,在卸载之前,必须删除所有没执行的定时器