linux开发笔记
呼呼,这里记录一下前一段时间开发Linux环境下的Xilinx驱动的过程,由于中途有其他事情要做只能暂时搁置。这帖子也是用来记录下自己开发过程中的一些小坑。方便后续快速回忆起来。
第一次写博客,里面有些逻辑不顺的地方请见谅。
我选择的开发环境是Linux的4-15-0的内核,Ubuntu16.04LTS版本。在内核编译这块借鉴大佬的流程:
点这里
不过大佬的测试例子没有在/dev下创建设备节点,所以运行的时候有小问题,这些都在bug纪录里说吧。
在linux中,设备与驱动是分离的,两者通过总线进行连接。你不能直接访问设备的物理地址,只能访问经过CPU映射后的地址,其三者结构为:
设备<------------>总线<-------------->驱动进行连接
主设备号,次设备号
想要使用生成文件的函数如device_create_file()或者sysfs_create_file()的函数时需要在前面定义一个
#define DEVICE_ATTR(_name, _mode, _show, _store)
其中_mode的定义如下:
400 拥有者能够读,其他任何人不能进行任何操作;
644 拥有者都能够读,但只有拥有者可以编辑;
660 拥有者和组用户都可读和写,其他人不能进行任何操作;
664 所有人都可读,但只有拥有者和组用户可编辑;
700 拥有者能够读、写和执行,其他用户不能任何操作;
744 所有人都能读,但只有拥有者才能编辑和执行;
755 所有人都能读和执行,但只有拥有者才能编辑;
777 所有人都能读、写和执行
一般定义说:S_IWUSR(用户可写),S_IRUSR(用户可读)
bug纪录
1.每个驱动在写好后加入内核模块,若没有在驱动文件中进行设备类的创建即cdev_init,和device_creat_file等操作的话是无法自动在/dev文件夹中创建驱动文件的,用户层无法通过open操作访问该驱动,需要在首次加载内存后手动通过mknod /dev/设备名 c/b 设备号 次设备号 创建后才可以通过open调用该驱动,但是重启后又要重新创建设备节点
17号记录:
主要的一个流程。注册->使能->访问配置空间->访问IO和内存空间->PCI中断
今天bug,代码问题出在of_find_node_by_type,在设备树中没有找到pci这个类型的节点。但此时并未初始化完成,没有进入到creat文件的阶段,于是打算先从设备树开始手动寻找
未解决:这部分关于node节点的查找的,需要编译设备树,然后才能通过驱动访问到这个专有的设备节点。听同事说,如果当需要使用到这快硬件的额外资源的时候需要查阅手册然后编写设备树文件,然后编译进内核模块 (不清楚具体流程-后面要用到再去问问大佬)
pci设备的信息在sys/bus/pci/devices/0000:xx:xx.x后面是根据lspci查询到的地址,然后进入到相应的文件夹下面就可以看到
内核开发头文件
//这个头文件包含了许多符号与函数的定义,这些符号与函数多与加载模块有关
#include <linux/module.h>
//这个头文件包含了你的模块初始化与清除的函数
#include <linux/init.h>
//PCI设备的头文件
#include <linux/pci.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
内核开发相关函数
modult_init/modult_exit
这两个函数包含在module.h中,是写驱动必备的包含了驱动初始化以及卸载时需要的工作。
static int initialization_function(void)
{
//初始化代码
}
modult_init(initialization_function);
static void exit_function(void)
{
//释放代码
}
modult_exit(exit_function);
module LICENSE
LICENSE包括 GPL、GPL v2、GPL and additional rights、Dual BSD/GPL、Dual MPL/GPL、Proprietary
MODULE_LICENSE("GPL"); // "GPL" 是指明了 这是GNU General Public License的任意版本
// “GPL v2” 是指明 这仅声明为GPL的第二版本
// "GPL and addtional"
// "Dual BSD/GPL"
// "Dual MPL/GPL"
// "Proprietary" 私有的
// 除非你的模块显式地声明一个开源版本,否则内核会默认你这是一个私有的模块(Proprietary)。
MODULE_AUTHOR // 声明作者
MODULE_DESCRIPTION // 对这个模块作一个简单的描述,这个描述是"human-readable"的
MODULE_VERSION // 这个模块的版本
MODULE_ALIAS // 这个模块的别名
MODULE_DEVICE_TABLE // 告诉用户空间这个模块支持什么样的设备 USB PCI设备需要
其实还有很多东西没补全,后续再补,先给出一个PCI驱动的基本框架
PCI驱动基本框架
pci驱动需要包含这几个部分:初始化设备模块、设备打开模块、数据读写和控制模块、中断处理模块、设备释放模块、设备卸载模块
典型的PCI设备驱动程序的基本框架:
/* 指明该驱动程序适用于哪一些PCI设备 */
static struct pci_device_id demo_pci_tbl [] __initdata = {
{PCI_VENDOR_ID_DEMO, PCI_DEVICE_ID_DEMO,
PCI_ANY_ID, PCI_ANY_ID, 0, 0, DEMO},
{0,}
};
/* 定义文件操作结构体 */
struct file_operations DispatchTable;
/* 用于在/dev文件夹下创建设备文件 */
static ssize_t show_debug(struct device *dev,
struct device_attribute *da,
char *buf)
{
struct device *fdo = dev_get_drvdata(dev);
if (fdo != NULL)
{
return sprintf(buf, "%s\n", fdo->debug == 1 ? "enabled" : "disabled");
}
return sprintf(buf, "%s\n", "error");
}
static ssize_t set_debug(struct device *dev,
struct device_attribute *da,
const char *buf,
size_t count)
{
struct device *fdo = dev_get_drvdata(dev);
if (fdo != NULL)
{
if (strncmp(buf, "enabled", 7) == 0)
{
fdo->debug = 1;
}
else
{
fdo->debug = 0;
}
}
return count;
}
static DEVICE_ATTR(debug,
S_IWUSR | S_IRUGO,
show_debug,
set_debug);
/* open函数编写---难度不大 */
int Dispatch_open(struct inode *inode, struct file *filp)
{
return 0;
}
/* release函数编写 */
int Dispatch_release(
struct inode *inode,
struct file *filp
)
{
//这里要写当文件退出时的操作
//需要对申请的空间进行释放以及中断关闭
return 0;
}
/* read函数编写 */
ssize_t Dispatch_read(struct file *filp,
char *buf,
size_t count,
loff_t *f_pos)
{
return 0;
}
/* write函数编写 */
ssize_t Dispatch_write(struct file *filp, const char *buf,
size_t count, loff_t *f_pos)
{
return 0;
}
/* iocontrol函数编写---难度不大 */
long demo_IoControl(
// struct inode *inode,
struct file *filp,
unsigned int cmd,
unsigned long args
)
{
return 0;
}
/* mmap函数编写 */
int Dispatch_mmap(
struct file *filp,
struct vm_area_struct *vma
)
{
return 0;
}
/* dma申请 */
static int dma_init(参数)
{
/* ... */
}
/* 中断处理模块 */
static int demo_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
/* ... */
}
/* PCI设备初始模块 */
static int ads_pcie_dma_init_one (struct pci_dev *pdev, const struct pci_device_id *ent)
{
/* 在这里初始化PCIE设备功能 */
/* 可以在这里打印出该设备的配置信息,这样每次加在设备后可以在dmesg终端看到该设备的信息 */
/* 主要有几个,就是结构体内存空间申请,中断申请,DMA缓冲区申请,以及node节点申请,创建设备节点文件 */
int State;
State = demo_interrupt(参数);
if(State<0)
{
Printk("inerrupt fail return \n");
return -1;
}
State = dma_init(参数);
if(State<0)
{
Printk("inerrupt fail return \n");
return -1;
}
//启用设备
Status = pci_enable_device(pdev);
if ( Status != 0){
printk("ERROR - PCI device enable failed\n");
break;
}
//填写适当的调度处理程序
DispatchTable.owner = THIS_MODULE;
DispatchTable.unlocked_ioctl= demo_IoControl;
DispatchTable.mmap = demo_mmap;
DispatchTable.open = demo_open;
DispatchTable.release = demo_release;
DispatchTable.read = demo_read;
DispatchTable.write = demo_write;
//cdev三连 init add create
cdev_init(&pdx->cdev, &pdx->DispatchTable);
pdx->cdev.owner = THIS_MODULE;
pdx->cdev.ops = &pdx->DispatchTable;
status = cdev_add(&pdx->cdev, pdx->dev_num, 1);
if(Status != 0){
printk("ERROR - add char device failed\n");
break;
}
pdx->sysfs_class = class_create(THIS_MODULE, DMA_DRIVER_NAME);
if (IS_ERR(pdx->sysfs_class))
{
printk("ERROR - sysfs class create failed.\n");
break;
}
//每次加载设备就在/dev文件夹下创建节点文件---记得在release进行删除
int ret;
struct device *cre_dev = device_create(pdx->sysfs_class, NULL, pdx->dev_num, NULL, DMA_DRIVER_NAME);
dev_set_drvdata(cre_dev, fdo);
ret = device_create_file(cre_dev, &dev_attr_debug);
if (ret)
{
printk("ERROR - sysfs attribute create failed.\n");
break;
}
return 0;
}
/* 设备模块信息 */
static struct pci_driver demo_pci_driver = {
.name = demo_MODULE_NAME, /* 设备模块名称 */
.id_table= demo_pci_tbl, /* 能够驱动的设备列表 */
.probe = demo_init, /* 查找并初始化设备 */
.remove = demo_remove /* 卸载设备模块 */
/* ... */
};
static int demo_init_module (void)
{
if(pci_register_driver(&demo_pci_driver)==0)
{
printk(KERN_INFO"Driver-pci init OK");
}
return 0;
}
static void demo_cleanup_module (void)
{
printk(KERN_INFO"Driver-pci eixt OK");
pci_unregister_driver(&demo_pci_driver);
}
/* 加载驱动程序模块入口 */
module_init(demo_init_module);
/* 卸载驱动程序模块入口 */
module_exit(demo_cleanup_module);
pci设备初始化需要做到的事情
初始化设备模块
在Linux系统下,想要完成对一个PCI设备的初始化,需要完成以下工作:
- 检查PCI总线是否被Linux内核支持;
- 检查设备是否插在总线插槽上,如果在的话则保存它所占用的插槽的位置等信息。
- 读出配置头中的信息提供给驱动程序使用。
驱动程序首先调用函数**pci_present( )检查PCI总线是否已经被Linux内核支持,如果系统支持PCI总线结构,这个函数的返回值为0,如果驱动程序在调用这个函数时得到了一个非0的返回值,那么驱动程序就必须得中止自己的任务了。系统一般调用pci_register_driver( )**函数来注册PCI设备的驱动程序,此时需要提供一个pci_driver结构,在该结构中给出的probe探测例程将负责完成对硬件的检测工作。
启用设备模块
在这个模块里主要实现申请中断、检查读写模式以及申请对设备的控制权等。在申请控制权的时候,非阻塞方式遇忙返回,否则进程主动接受调度,进入睡眠状态,等待其它进程释放对设备的控制权。
在使用前需要在函数前面声明一下。
static int demo_open(struct inode *inode, struct file *file)
{
/* 申请中断,注册中断处理程序 */
request_irq(card->irq, &demo_interrupt, SA_SHIRQ,
card_names[pci_id->driver_data], card)) {
/* 检查读写模式 */
if(file->f_mode & FMODE_READ) {
/* ... */
}
if(file->f_mode & FMODE_WRITE) {
/* ... */
}
/* 申请对设备的控制权 */
down(&card->open_sem);
while(card->open_mode & file->f_mode) {
if (file->f_flags & O_NONBLOCK) {
/* NONBLOCK模式,返回-EBUSY */
up(&card->open_sem);
return -EBUSY;
} else {
/* 等待调度,获得控制权 */
card->open_mode |= f_mode & (FMODE_READ | FMODE_WRITE);
up(&card->open_sem);
/* 设备打开计数增1 */
MOD_INC_USE_COUNT;
/* ... */
}
}
}
控制设备操作模块
例如,通过它可以从I/O寄存器里读取一个数据,并传送到用户空间里:
static int demo_ioctl(struct inode *inode, struct file *file,
unsigned int cmd, unsigned long arg)
{
/* ... */
switch(cmd) {
case DEMO_RDATA:
/* 从I/O端口读取4字节的数据 */
val = inl(card->iobae + 0x10);
/* 将读取的数据传输到用户空间 */
return 0;
}
/* ... */
}
Linux内核源码中的driver目录里提供了许多设备驱动程序的源代码,找那里可以找到类似的例子。在对资源的访问方式上,除了有I/O指令以外,还有对外设I/O内存的访问。对这些内存的操作一方面可以通过把I/O内存重新映射后作为普通内存进行操作,另一方面也可以通过总线主DMA(Bus Master DMA)的方式让设备把数据通过DMA传送到系统内存中。
中断处理
PC的中断资源比较有限,只有0~15的中断号,因此大部分外部设备都是以共享的形式申请中断号的。当中断发生的时候,中断处理程序首先负责对中断进行识别,然后再做进一步的处理
static void demo_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
struct demo_card *card = (struct demo_card *)dev_id;
u32 status;
spin_lock(&card->lock);
/* 识别中断 */
status = inl(card->iobase + GLOB_STA);
if(!(status & INT_MASK))
{
spin_unlock(&card->lock);
return; /* not for us */
}
/* 告诉设备已经收到中断 */
outl(status & INT_MASK, card->iobase + GLOB_STA);
spin_unlock(&card->lock);
/* 其它进一步的处理,如更新DMA缓冲区指针等 */
}
释放设备模块
负责释放对设备的控制权,释放占用的内存和中断等
static int demo_release(struct inode *inode, struct file *file)
{
/* ... */
/* 释放对设备的控制权 */
card->open_mode &= (FMODE_READ | FMODE_WRITE);
/* 唤醒其它等待获取控制权的进程 */
wake_up(&card->open_wait);
up(&card->open_sem);
/* 释放中断 */
free_irq(card->irq, card);
/* 设备打开计数增1 */
MOD_DEC_USE_COUNT;
/* ... */
}
Makefile编写
ifneq ($(KERNELRELEASE),)
obj-m:=hello.o
else
#generate the path
CURRENT_PATH:=$(shell pwd)
#the absolute path
LINUX_KERNEL_PATH:=/lib/modules/$(shell uname -r)/build
#complie object
default:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean
endif
文件操作
open可以打开设备,驱动程序
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
//具体参数查阅Linux设备驱动开发详解 105页
文档里面的driver_moudles/driver.rst
主要结构体
PCI驱动相关
lspci指令
选项 | 含义 |
---|---|
-v | 显示PCI设备的详细信息 |
-vv | 显示PCI设备更加详细的信息 |
-vvv | 显示PCI设备所有可解析的信息 |
-s <域>:<总线>:<插槽>.<函数> | 只显示指定的PCI设备,需要输入总线、插槽和函数 |
-n | 显示PCI设备的相关标识符,包括总线、插槽、函数、设备类标识符、制造商标识符及PCI设备标识符 |
-d <制造商标识符>:<PCI设备标识符> | 只显示指定的PCI设备,需要输入制造商标识符和PCI设备标识符 |
-x | 以16进制显示配置空间的前64字节 |
-xxx | 以16进制显示整个配置空间 |
-xxxx | 以16进制显示4096字节扩展配置空间 |
-k | 显示处理每个设备的内核驱动程序 |
pci_driver这个数据结构在文件include/linux/pci.h里,这是Linux内核版本2.4之后为新型的PCI设备驱动程序所添加的,其中最主要的是用于识别设备的id_table结构,以及用于检测设备的函数probe( )和卸载设备的函数remove( ),在PCIE驱动开发就注册这个结构体。
struct pci_driver {
struct list_head node;
char *name;
const struct pci_device_id *id_table;
int (*probe) (struct pci_dev *dev, const struct pci_device_id *id);
void (*remove) (struct pci_dev *dev);
int (*save_state) (struct pci_dev *dev, u32 state);
int (*suspend)(struct pci_dev *dev, u32 state);
int (*resume) (struct pci_dev *dev);
int (*enable_wake) (struct pci_dev *dev, u32 state, int enable);
};
pci_dev这个数据结构也在文件include/linux/pci.h里,它详细描述了一个PCI设备几乎所有的硬件信息,包括厂商ID、设备ID、各种资源等,会在PCIdriver中作为参数传入到各个函数中。
struct pci_dev {
struct list_head global_list;
struct list_head bus_list;
struct pci_bus *bus;
struct pci_bus *subordinate;
void *sysdata;
struct proc_dir_entry *procent;
unsigned int devfn;
unsigned short vendor;
unsigned short device;
unsigned short subsystem_vendor;
unsigned short subsystem_device;
unsigned int class;
u8 hdr_type;
u8 rom_base_reg;
struct pci_driver *driver;
void *driver_data;
u64 dma_mask;
u32 current_state;
unsigned short vendor_compatible[DEVICE_COUNT_COMPATIBLE];
unsigned short device_compatible[DEVICE_COUNT_COMPATIBLE];
unsigned int irq;
struct resource resource[DEVICE_COUNT_RESOURCE];
struct resource dma_resource[DEVICE_COUNT_DMA];
struct resource irq_resource[DEVICE_COUNT_IRQ];
char name[80];
char slot_name[8];
int active;
int ro;
unsigned short regs;
int (*prepare)(struct pci_dev *dev);
int (*activate)(struct pci_dev *dev);
int (*deactivate)(struct pci_dev *dev);
};
device_node设备节点,如果你需要使用设备的额外功能,就需要编译设备树,通过of_find_node_by_type在设备树中找到你编译的节点文件,device_node结构提定义include/linux/of.h 中,定义如下:
struct device_node {
const char *name; /* 节点名字 */
const char *type; /* 设备类型 */
phandle phandle;
const char *full_name; /* 节点全名 */
struct fwnode_handle fwnode;
struct property *properties; /* 属性 */
struct property *deadprops; /* removed 属性 */
struct device_node *parent; /* 父节点 */
struct device_node *child; /* 子节点 */
struct device_node *sibling;
struct kobject kobj;
unsigned long _flags;
void *data;
#if defined(CONFIG_SPARC)
const char *path_component_name;
unsigned int unique_id;
struct of_irq_controller *irq_trans;
#endif
};
cdev结构体
在Linux内核中,使用cdev结构体来描述一个字符设备,cdev结构体的定义如下:
<include/linux/cdev.h>
struct cdev {
struct kobject kobj; //内嵌的内核对象.
struct module *owner; //该字符设备所在的内核模块的对象指针.
const struct file_operations *ops; //该结构描述了字符设备所能实现的方法,是极为关键的一个结构体.
struct list_head list; //用来将已经向内核注册的所有字符设备形成链表.
dev_t dev; //字符设备的设备号,由主设备号和次设备号构成.
unsigned int count; //隶属于同一主设备号的次设备号的个数.
};
cdev_init(struct cdev *, const struct file_operations *); 初始化
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}
struct cdev *cdev_alloc(void);
struct cdev *cdev_alloc(void)
{
struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
if (p) {
INIT_LIST_HEAD(&p->list);
kobject_init(&p->kobj, &ktype_cdev_dynamic);
}
return p;
}
owner成员的存在体现了驱动程序与内核模块间的亲密关系,struct module是内核对于一个模块的抽象,该成员在字符设备中可以体现该设备隶属于哪个模块,在驱动程序的编写中一般由用户显式的初始化 .owner = THIS_MODULE, 该成员可以防止设备的方法正在被使用时,设备所在模块被卸载。而dev成员和count成员则在cdev_add中才会赋上有效的值。
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
该函数向内核注册一个struct cdev结构,即正式通知内核由struct cdev *p代表的字符设备已经可以使用了。第一个参数是设备号dev,第二个参数是该设备关联的设备编号的数量
void cdev_del(struct cdev *p);
该函数向内核注销一个struct cdev结构
vm_area_struct结构体
内存映射信息放在vma参数中,它表示的是一块连续的虚拟地址空间区域。下面的这张图是借鉴别人的,由于忘记是谁的等找再贴上链接
/*
* 此结构定义了内存VMM内存区域。 每个VM区域/任务中有一个。 VM区域是进程虚拟内存空间的任何部分,
* 它具有页面错误处理程序的特殊规则(即共享库,可执行区域等)。
*/
struct vm_area_struct {
/* The first cache line has the info for VMA tree walking.
第一个缓存行具有VMA树移动的信息 */
unsigned long vm_start; /* Our start address within vm_mm. 我们的起始地址在vm_mm内*/
unsigned long vm_end; /* The first byte after our end address within vm_mm. 我们的结束地址在vm_mm之后的第一个字节*/
/* linked list of VM areas per task, sorted by address
每个任务的VM区域的链接列表,按地址排序 */
struct vm_area_struct *vm_next, *vm_prev;
struct rb_node vm_rb;
/*
* 此VMA左侧最大的可用内存间隙(以字节为单位)。在此VMA和vma-> vm_prev之间,
* 或者在VMA rbtree中我们下面的一个VMA与其->vm_prev之间。
* 这有助于get_unmapped_area找到合适大小的空闲区域。
*/
unsigned long rb_subtree_gap;
/* Second cache line starts here.
第二个缓存行从这里开始 */
struct mm_struct *vm_mm; /* The address space we belong to. 我们所属的address space*/
pgprot_t vm_page_prot; /* Access permissions of this VMA. 此VMA的访问权限 */
unsigned long vm_flags; /* Flags, see mm.h. */
/*
* 对于具有地址空间(address apace)和后备存储(backing store)的区域,
* 链接到address_space->i_mmap间隔树,或者链接到address_space-> i_mmap_nonlinear列表中的vma。
*/
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} shared;
/*
* 在其中一个文件页面的COW之后,文件的MAP_PRIVATE vma可以在i_mmap树和anon_vma列表中。
* MAP_SHARED vma只能位于i_mmap树中。
* 匿名MAP_PRIVATE,堆栈或brk vma(带有NULL文件)只能位于anon_vma列表中。
*/
struct list_head anon_vma_chain; /* Serialized by mmap_sem &
* page_table_lock 由mmap_sem和* page_table_lock序列化*/
struct anon_vma *anon_vma; /* Serialized by page_table_lock 由page_table_lock序列化*/
/* Function pointers to deal with this struct.
用于处理此结构体的函数指针 */
const struct vm_operations_struct *vm_ops;
/* Information about our backing store:
后备存储(backing store)的信息:*/
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE units 以PAGE_SIZE为单位的偏移量(在vm_file中)*/
struct file * vm_file; /* File we map to (can be NULL). 我们映射到文件(可以为NULL)*/
void * vm_private_data; /* was vm_pte (shared mem) 是vm_pte(共享内存) */
atomic_long_t swap_readahead_info;
#ifndef CONFIG_MMU
struct vm_region *vm_region; /* NOMMU mapping region NOMMU映射区域 */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* NUMA policy for the VMA 针对VMA的NUMA政策 */
#endif
struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
} __randomize_layout;