Makefile 学习
kconfig
make menuconfig 进入图形化编译界面,build中有一个总的kconfig,各个子kconfig 通过source 导入到顶层的kconfig
Makefile
执行目标与伪目标
程序中默认执行第一个伪目标作为执行目标
.PHONY: default all install clean testx viewx flint 声明所有的伪目标 default all install clean testx viewx flint
变量赋值
CPU := arm720t 直接把这个值付给cpu
CPU ?:= arm720t 要是没有赋值就将这个值给他
驱动编译进入内核与编译为ko
Obj-m 编译成ko
Obj-y 编译进内核
子makefile 编译
跳转到子目录下编译makefile
$(MAKE) -C $(KERNELDIR) $(MODULE_NAME_CAVALRY)-objs="$(OBJS_ALL_CAVALRY)" M=$(PWD) EXTRA_CFLAGS="$(EXTRA_CFLAGS)" modules
1、$(MAKE) -C $(KERNELDIR) 表示切换到KERNELDIR 目录,执行make 命令($(MAKE) )
2、指定模块的源文件对象。$(MODULE_NAME_CAVALRY) 是模块的名称,$(OBJS_ALL_CAVALRY) 是一个变量,包含所有需要编译的对象文件。这个选项告诉内核的 make 命令哪些对象文件需要编译成模块
基础知识
怎么写一个驱动程序
在设备树中设置硬件信息: 设备树中包含了硬件平台和设备的描述信息,包括设备的寄存器地址、中断号、资源大小等。驱动程序在设备树中查找并解析这些信息来了解硬件的配置和功能。
在驱动程序中定义涉及的寄存器: 驱动程序需要定义与硬件设备相关的寄存器,通常使用C语言的结构体或宏来表示寄存器。这些结构体或宏包含了寄存器的地址、偏移量以及相关的位字段等信息。
在驱动程序中进行寄存器地址映射: 驱动程序在初始化过程中会将涉及的寄存器地址映射到内核地址空间,这样内核就可以直接通过访问这些映射地址来与硬件设备进行通信。
使用函数获得设备树中的寄存器设置赋值给映射好的寄存器: 驱动程序在初始化过程中会通过设备树获取硬件配置信息,如设备树节点中的寄存器地址、寄存器位字段的设置等。然后,驱动程序会将这些设备树中的设置赋值给映射好的寄存器,以正确地配置硬件设备。
简而言之,设备树提供了硬件平台和设备的描述信息,驱动程序会解析这些信息,初始化并配置硬件设备。在这个过程中,驱动程序需要定义寄存器的表示方式,并将硬件的寄存器地址映射到内核地址空间,然后使用设备树中的信息来正确配置这些寄存器。这样,硬件设备就能够在操作系统中正常工作了。
编译成内核、编译成ko
注意
将驱动编译到内核中 make zImage
将驱动编译成ko模块 make modules
在driver/xzj 目录新建一个ap3216c驱动
一、修改makeflie 、图像界面kconifg
二、在顶层kconfig source 导入子kconfig
三、编译命令
将驱动编译到内核中 make zImage
将驱动编译成ko模块 make modules
不使用kconfig 文件,将驱动编译到一起,新增一个驱动文件(vbp)
在顶层build 目录下,找到makefile ,将驱动文件夹目录写入
build.inc : 添加新文件查找路径
build.src 添加新增需要编译的文件夹
PDC中全部驱动编译成一个pdc.ko
全部的驱动都在顶层makefile 中添加目标源文件
OBJS_DRIVER += $(DRIVER_DIR)/mcu/pdc_mcuUart.o
OBJS_DRIVER += $(DRIVER_DIR)/mcu/pdc_mcuSPI.o
OBJS_DRIVER += $(DRIVER_DIR)/mcu/pdc_mcuI2c.o
OBJS_DRIVER += $(DRIVER_DIR)/mcu/pdc_mcu.o
OBJS_CFG := $(PER_CONFIG_DIR)/pdccfg/pdc_cfg.o
怎么修改代码中库的路径
一、设备树
一个存放硬件配置信息的数据结构
设备树就是将关于硬件配置信息的文件独立出去,驱动程序中只留下关于硬件的操作
修改设备树
1、添加pinctrl节点
2、添加设备节点
二、深入理解pinctrl 和gpio子系统
将硬件配置存放在设备树中的一个节点中,这些特殊节点叫做pinctl子系统、gpio子系统节点,然后驱动中通过gpio子系统的一些特定函数,获取这些特殊节点的配置信息,做出具体操作
pinctrl子系统
绑定文档,官网为了不同的芯片的pinctrl规范写了一个模板
linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/Documentation/devicetree/bindings/pinctrl$
imx芯片的文档是 fsl,imx-pinctrl.txt
将一个硬件设备需要的引脚配置都写好,放在一个节点下
以imx芯片系列为例,公共的设备树文件是imx.dts,然后使用&在其他设备树文件中追加代码
引脚复用:一个引脚可以用作很多功能,比如i2c1的SCL、SDA 也可以作为UART4的接收、发送引脚,只需要将这个两个引脚连接到串口控制的引脚上就可以实现,pinctrl子系统将各种复用引脚设置好了,我们只需要输入参数就可以设置成我们想要的配置
借助pinctrl子系统来设置一个PIN的复用(用作什么功能和电气属性)
GPIO子系统
使用gpio子系统来控制gpio,控制gpio的输入输出
在设备树中添加设备节点
gpioled {
#address-cells = <1>;
#size-cells = <1>;
compatible = "atkalpha-gpioled";
//下面这个是pinctl子系统
pinctrl-names = "default";
// pinctrl-0 属性设置 LED 灯所使用的 PIN 对应的 pinctrl 节点
pinctrl-0 = <&pinctrl_led>;
下面这个与gpio子系统有关
//led-gpio 属性指定了 LED 灯所使用的 GPIO,在这里就是 GPIO1这一组第四个引脚,低电平有效(也急速GPIO1d第三个引脚)
led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
status = "okay";
};
1
三、在驱动程序中操作GPIO
1、先从设备树中获得GPIO
2、配置为输出引脚
三、平台总线框架
主机驱动:一般负责对具体设备进行硬件级别的操作,然后向外提交api函数接口,主机驱动一般都是开发板厂家写好的
设备驱动:所有的设备驱动都可以调用主机驱动的api函数去实现与外界交互功能
当我们向系统注册一个驱动的时候,总线就会在右侧的设备中查找,看看是有没有设备与之匹配的设备,如果有的话就将二者联系起来,同样的,当向系统中注册一个设备的时候,总线就会在左侧的驱动中查找有没有与之匹配的设备
驱动与设备分离
我们知道设备驱动的分离,并且引出了总线、驱动、设备的模型,比如i2c、spi、usb等总线。但是在soc中有些外设没有这个概念,但是我们有想使用这个模型,Linux提出了platform这虚拟总线,于是就对应的platfor_driver和platform_device
当我们向系统注册一个驱动的时候,总线就会在右侧的设备中查找,看看有没有与之匹配
的设备,如果有的话就将两者联系起来。同样的,当向系统中注册一个设备的时候,总线就会在左侧的驱动中查找看有没有与之匹配的设备,有的话也联系起来。Linux 内核中大量的驱动程序都采用总线、驱动和设备模式,我们一会要重点讲解的 platform 驱动就是这一思想下的产物。
1、platform总线
Linux系统内核用bus_type结构体表示总线
bus_type结构体中 platform_match函数负责驱动与设备的匹配
struct bus_type platform_bus_type = {
.name = "platform",
.dev_groups = platform_dev_groups,
.match = platform_match,
.uevent = platform_uevent,
.pm = &platform_dev_pm_ops,
}
2、platform驱动
struct platform_driver {
int (*probe)(struct platform_device *);
int (*remove)(struct platform_device *);
void (*shutdown)(struct platform_device *);
int (*suspend)(struct platform_device *, pm_message_t state);
int (*resume)(struct platform_device *);
struct device_driver driver; //这个结构体变量中包含了设备树匹配的of_match_tableha函数
const struct platform_device_id *id_table; //无设备树匹配
bool prevent_deferred_probe;
}
第二行: probe 函数,当驱动与设备匹配成功以后 probe 函数就会执行,probe函数就是负 责注册驱动设备到内核的哪些东西
第 七 行 driver 成员,该结构体中包含了设备树匹配的of_match_table函数
第八行 :id_table 用在与无设备树匹配
注意:在无设备树的时候platform_driver name 和 platfor_device name 对应就可以匹配上
struct device_driver
{
const struct of_device_id *of_match_table; //设备树匹配
}
驱动入口函数里面调用的platform_driver_register函数向Linux内核注册一个platform驱动
驱动卸载函数里面调用platform_driver_unregister函数卸载platform驱动
编写platform驱动需要的一些东西
0、寄存器地址定义、因为这里是用地址映射,用虚拟地址进行操作 //传统字符设备驱动
1、设备结构体
2、设备具体操作函数
3、字符设备驱动操作集(file_operations)
4、platform驱动的probe函数,驱动与设备匹配后此函数就会执行(注册字符设备驱动,初始化设备(寄存器地址映射、设备))
5、remove()(卸载字符设备驱动,取消寄存器地址映射)
6、匹配列表(如果使用设备树的话通过此匹配表进行驱动匹配)
7、platform平台驱动结构体(其中包含name(其中name移动要和设备字段相对应),匹配列表,probe和remove)
8、驱动模块的加载/卸载
三、设备树
platform_devices和设备里面具体的节点功能是一样的,所以说如果有设备树,就没必要整个platform_devices。在编写基于设备树的platform驱动我们需要注意以下几点:
1.在设备树中创建设备节点
2.编写platform要注意兼容属性
这里需要注意,我们就是用
设备节点的compatible属性:“atkalpha-gpioled”和platform驱动的of_match_table属性表中compatible属性:”atkalpha-gpioled"来进行匹配的
驱动与设备匹配
在平台框架下,总线结构体里面有一个platform_match函数,里面定义了几种匹配方法函数
1、有设备树,驱动结构体中有一个of_match _table 表中的compatible属性与设备树的compatible匹配
2、无设备树,一般看设备结构体和驱动结构体的name属性
static struct i2c_driver ap3216c_driver = {
.probe = ap3216c_probe,
.remove = ap3216c_remove,
.driver = {
.owner = THIS_MODULE,
.name = "ap3216c" //无设备树
.of_match_table = ap3216c_of_match, //设备树匹配函数
},
};
/* 设备树匹配列表 */
static const struct of_device_id ap3216c_of_match[] = {
{ .compatible = "alientek,ap3216c" },
{ /* Sentinel */ }
};
字符设备驱动
使用了设备树的pinctrl和gpio子系统
应用层访问驱动层过程
过程大致如下:
在虚拟文件系统VFS中查找对应与字符设备对应struct inode节点
遍历散列表cdev_map,根据inod节点中的cdev_t设备号找到cdev对象
创建struct file对象
初始化struct file对象,将struct file对象中的file_operations成员指向struct cdev对象中的file_operation成员
回调open函数
简介
1、字符设备是Linux驱动中最基本的一类设备驱动,字符设备就是一个个字节,按照字节流进行读写操作的设备。(例:按键,电池等,IIC,SPI,LCD)。这些设备的驱动就叫字符设备驱动。
在Linux下一切皆为文件,驱动加载成功以后会在“/dev”目录下生成一个相对应的设备节点(文件),应用程序通过对这个“/dev/xxx”的文件进行操作,这个xxx是具体的驱动文件名字。比如/dev/led,可以通过read来读取当前灯的状态(开或者关),write可以写数据,用来控制灯开或者关,open和close就是打开或者关闭这个led驱动
在 Linux 内核文件 include/linux/fs.h 中
有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合(上面那几个函数都在里面)
字符设备驱动框架
加载流程
odule_init
是驱动模块的“大门”,而 cdev——add 是门后初始化流程中的“一步操作”。
详细流程
一、注册模块加载与卸载函数
module_exit(leddriver_exit);//注册模块卸载函数
static void __exit leddriver_exit(void)
{
i2c_del_driver(&leddriver_driver);//关联到驱动结构体
}
module_init(xxx_init); //注册模块加载函数
static int __init xxx_init(void)//入口函数关联到驱动结构体
{
int ret = 0;
ret = i2c_add_driver(ap3216c_driver);
}
insmod drv.ko //加载驱动模块,系统调用module_init
rmmod drv.ko //卸载驱动模块
后面使用这两个将之加载
用来指定加载驱动时要执行的模块加载函数;xxx_init
module_init
宏用于定义模块加载时要执行的初始化函数。当模块加载到内核中时,内核会在初始化过程中调用这个初始化函数,以执行特定的设置、分配资源、注册设备等操作。
module_i2c_driver
这个函数在驱动加载调用i2c-add_driver在卸载的时候调用i2c_deletc_driver
i2c_add_driver
函数是i2c子系统注册的一个函数,匹配成功会调用probe 然后使cdev_add 将驱动注册到内核(实际是将ops指针注册)
spi_register_driver
函数是spi子系统注册的一个函数,匹配成功会调用probe 然后使cdev_add 将驱动注册到内核(实际是将ops指针注册
二、 (内存映射)
将物理地址转换成虚拟地址
三、.注册字符设备驱动
1、注册驱动设备号到内核(设备号注册内核中,/proc/)
2、生成驱动设备节点(与用户交互,/dev/)
简要流程
1、申请设备号register_chrdev_regio、alloc_chrdev_region2、cdev_init 初始化cdev 建立cdev 与file_operation 之间的联系(cdev 描述字符设备结构体)
2、cdev_add 将操作结构体指针写入一个哈希表,主设备号一样的在同一链表中
3、
1、创建设备号
静态创建(设置好设备号)
register_chrdev_region(dev_t from, unsigned count, const char *name);
from: 自定义的 dev_t 类型设备号 表示设备号的起始值(主+次)
count: 申请设备的数量(次设备)
name: 申请的设备名称
函数返回值:申请成功返回 0,申请失败返回负数
int major = 200;
int minor = 0;
dev_t devid = MKDEV(major, minor); // 第一个参数是主设备号,第二个参数是次设备号
register_chrdev_region(devid, 1, "test");
动态创建(主设备号自动分配,次设备号指定起始范围)
alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);
dev 是设备号(主+次)
baseminor 次设备的起始地址
count 注册数量
name 设备的名字
可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以baseminor为起始地址地址开始递增。一般baseminor为0,也就是说次设备号从0开始。
2、字符设备结构体初始化
cdev两个重要结构体
ops设备操作结构体
设备号
cdev_init(&newchrled.cdev, &newchrled_fops)
第一个参数是字符设备结构体(也就是字符设备的操作函数)
第二个参数是操作函数结构体
3、字符设备注册进内核
(cdev添加进内核并且绑定设备号)
cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);
//第一个参数是设备结构体字符设备指针
//第二个参数是设备号
//第三个是设备个数
本质是将cdev写入内核中的一个哈希表中,表中同一个主设备号的会在一条链表上
cat /proc/devices
:可以查看已经注册的驱动程序,但是只显示主设备号
4、自动创建设备节点
下面是创建设备文件,先创建出一个类,再调用device_create 函数创建出/dev/目录下对应的设备节点,这样在加载模块的时候,用户空间的udev就会自动响应device_create函数去创建设备节点
也就是/dev/led这种
创建出一个逻辑类
newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);
创建出设备节点
newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, NEWCHRLED_NAME);
1、每个设备节点都有一个innode,indode是linux文件管理系统维护的一个结构体
i_rdev是该设备的设备号,i_cdev是该设备的cdev结构体
2、每一个打开的文件都会有一个file结构体,要是一个文件打开多次就会有多个file结构体,所有的file都会指向一个inode结构体
5、几个重要的结构体
1、字符设备结构体
内核用struct cdev结构体来描述一个字符设备,并通过struct kobj_map类型的散列表cdev_map来管理当前系统中的所有字符设备
//字符设备结构体
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
设备结构体,存放一些很杂的东西
struct gpioled_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev 字符设备结构体*/
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
struct device_node *nd; /* 设备节点 */
int led_gpio; /* led所使用的GPIO编号 */
};
2、设备操作结构体
应用层代码的write之后的函数会通过系统函数调用到设备操作结构体中的write函数
static struct file_operations gpioled_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
驱动层次的操作硬件函数
tatic ssize_t led_write(struct file *filp, const char __user *buf,
size_t cnt, loff_t *offt)
{
int retvalue;
unsigned char databuf[1];
unsigned char ledstat;
retvalue = copy_from_user(databuf, buf, cnt);
if(retvalue < 0) {
printk("kernel write failed!\r\n");
return -EFAULT;
}
ledstat = databuf[0]; /* 获取状态