【Linux】遇事不决,可先点灯,LED驱动的进化之路---1
前言:
一、最简单的LED驱动程序
1.1 字符设备驱动程序框架
1.2 程序实战
1.2.1 驱动程序(led_drive_simple.c)
1.2.2 应用程序(led_test_simple.c)
1.2.3 Makefile代码
1.3 运行测试
1.3.1 首先编译内核(如果没编译过)
1.3.2 设置交叉编译工具链(Ubuntu)
1.3.3 编译(Ubuntu)
1.3.4 上机测试(开发板)
二、LED驱动程序(分层)
2.1 分层设计思想
2.2 程序实战
2.2.1 头文件(led_opr.h)
2.2.2 驱动程序(board_imx6ull.c)
2.2.3 驱动程序(led_drive.c)
2.2.4 应用程序(led_test.c)
2.2.5 Makefile代码
2.3 运行测试
2.3.1 上机测试(开发板)
2.4 总结
三、LED驱动程序(分离)
3.1 分离设计思想
3.2 程序实战
3.2.1 头文件(led_resource.h)
3.2.2 驱动程序(board_A_led.c)
3.2.3 驱动程序(chip_imx6ull_gpio.c)
3.2.4 Makefile代码
3.3 运行测试
3.4 总结
四、LED驱动程序(总线设备驱动模型)
4.1 总线设备驱动模型
4.2 程序实战
4.2.1 头文件(led_drive.h)
4.2.2 驱动程序(led_drive.c)
4.2.3 驱动程序(board_A_led.c)
4.2.4 驱动程序(chip_imx6ull_gpio.c)
4.2.5 Makefile代码
4.3 测试运行
4.4 总结
五、LED驱动程序(设备树)
5.1 设备树
5.1.1 设备树背景
5.1.2 设备树简述
5.2 程序实战
5.2.1 设备树dts文件(100ask_led.dts)
5.2.2 驱动程序(chip_imx6ull_gpio.c)
5.2.3 Makefile代码
5.3 总结
本文展示LED驱动进化升级化蝶的过程,并由浅入深的对驱动程序框架的理念作进一步阐述。遇到搞不明白的,就不妨先点个灯吧。
LED驱动进化之路:(层次递进)
- 最简单的LED驱动程序
- 加入分层思想的LED驱动程序
- 加入分离思想的LED驱动程序
- 总线设备驱动模型下的LED驱动程序
- 加入设备树的LED驱动程序
参考:韦老师课程,Linux笔记老师课程(设备树部分)
https://blog.csdn.net/qq_33487044/article/details/126325656
https://www.bilibili.com/video/BV14f4y1Q7ti?p=12&spm_id_from=pageDriver&vd_source=cf66c4035cd726f1d3cb6a42cfd6da5f
过一遍驱动框架后,有了大体的认知,但还需要进一步的实践感受。
https://blog.csdn.net/weixin_42373086/article/details/130521999
最简单的驱动程序,一个设备app调用open时就提供给驱动程序里的drv_open,read→drv_read,write→drv_write,ioctrl→drv_ioctl。
编写驱动程序的步骤:跟hello驱动程序框架完全一致
具体的注释和解析都在代码中:
/* 说明:
*1,本代码是跟学韦老师课程所编写,增加了注释和分析
*2,采用的是UTF-8编码格式
*3,简单LED驱动程序 led_drive_simple.c
*4,参照内核字符设备驱动程序cm4040_cs.c
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/*registers*/
//IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3:0x02290000 + 0x14
static volatile unsigned int* IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
//GPIO5_GDIR:0x20AC004
static volatile unsigned int* GPIO5_GDIR;
//GPIO5_DR:0x020AC000
static volatile unsigned int* GPIO5_DR;
static struct class *led_class;
/*第一步:确定主设备号,这里由内核分配*/
static int major = 0;
/*第二步:定义自己的 file_operations 结构体,
实现对应的 drv_open/drv_write 等函数,填入 file_operations 结构体*/
/*
*函数: led_drv_open
*功能: enable gpio 使能gpio
configure pin as gpio 设置引脚为GPIO
configure gpio as output 设置引脚为GPIO输出引脚
*传入参数:
*flip:要打开的文件
*返回参数: 如果成功返回0
*/
static int led_drv_open(struct inode *inode, struct file *filp)
{
//imx6ull默认使能GPIO5.
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 &= ~0xf;
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 |= 0x5;
*GPIO5_GDIR |= (1<<3);
return 0;
}
/*
*函数: led_drv_write
*功能: copy_from_user:get data form app 获取app的数据,并设置gpio的寄存器
set gpio register: out 1/0
*传入参数:
*flip:要写的文件
*buf: 写的数据来自于buf
*size:写多大的数据
*offset:偏移值
*返回参数: 写的数据长度
*/
static ssize_t led_drv_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
char val;
int err;
err = copy_from_user(&val, buf, 1);
if(val)
{
*GPIO5_DR &= ~(1<<3);
}else
{
*GPIO5_DR |= (1<<3);
}
return -1;
}
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.write = led_drv_write,
.open = led_drv_open,
};
/*第三步:定义一个入口函数, 调用register_chrdev(把 file_operations 结构体告诉内核)*/
/*
*函数: led_init
*功能: ①注册主设备号
②获取寄存器物理地址映射过来的虚拟地址
③辅助完善:提供设备信息,自动创建设备节点:class_create、device_create
*/
static int __init led_init(void)
{
/*
*printk:判断一下是否调用了入口函数
*__FILE__ :表示文件
*__FUNCTION__ :当前函数名
*__LINE__ :在文件的哪一行
*/
printk("%s %s %d\n",__FILE__,__FUNCTION__,__LINE__);
//ioremap:物理地址映射到虚拟地址
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x02290000 + 0x14, 4);
GPIO5_GDIR = ioremap(0x020AC004, 4);
GPIO5_DR = ioremap(0x020AC000, 4);
//0:由内核分配主设备号,名称,指定函数结构体led_fops,返回值为主设备号
major = register_chrdev(0,"xixiwuli_led", &led_fops);
led_class = class_create(THIS_MODULE, "led_2345");
if (IS_ERR(led_class))
return PTR_ERR(led_class);
if (major < 0) {
class_destroy(led_class);
return major;
}
//创建/dev/myled的设备节点
device_create(led_class, NULL, MKDEV(major, 0), NULL, "myled");
return 0;
}
/*第四步:定义一个出口函数,出口函数调用unregister_chrdev*/
static void __exit led_exit(void)
{
iounmap(IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3);
iounmap(GPIO5_GDIR);
iounmap(GPIO5_DR);
device_destroy(led_class, MKDEV(major, 0));
class_destroy(led_class);
unregister_chrdev(major,"xixiwuli_led");
}
//把函数修饰成入口函数和出口函数
module_init(led_init);
module_exit(led_exit);
//内核遵循GPL协议,所以我们也要遵循GPL协议,才能够使用内核里的一些函数
MODULE_LICENSE("Dual BSD/GPL");
具体的注释和解析都在代码中:
/* 说明:
*1,本代码是跟学韦老师课程所编,增加了注释和理解
*2,采用的是UTF-8编码格式
*3,简单LED应用程序 led_test_simple.c
*/
#include
#include
#include
#include
#include
#include
//led_test_simple /dev/myled on
//led_test_simple /dev/myled off
int main(int argc, char** argv)
{
int fd;
char status;
if(argc != 3)
{
printf("usage: %s \n", argv[0]);
printf("eg: %s /dev/myled on\n", argv[0]);
printf("eg: %s /dev/myled off\n", argv[0]);
}
//open
fd = open(argv[1], O_RDWR);
if(fd < 0)
{
printf("can not open%s\n",argv[1]);
return -1;
}
//write
if(strcmp(argv[2], "on") == 0)
{
status = 1;
write(fd, &status, 1);
}
if(strcmp(argv[2], "off") == 0)
{
status = 0;
write(fd, &status, 1);
}
return 0;
}
具体的注释和解析都在代码中:
# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH, 比如: export ARCH=arm
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=arm-buildroot-linux-gnueabihf-
# 2.3 PATH, 比如: export PATH=$PATH:/home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
# 请参考各开发板的高级用户使用手册
KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88
all:
make -C $(KERN_DIR) M=`pwd` modules
$(CROSS_COMPILE)gcc -o led_test_simple led_test_simple.c
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
rm -f led_test_simple
obj-m += led_drive_simple.o
参照这篇文章https://blog.csdn.net/weixin_42373086/article/details/129796348?spm=1001.2014.3001.5501
export ARCH=arm
export CROSS_COMPILE=arm-buildroot-linux-gnueabihf-
export PATH=$PATH:/home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin
make编译后,将.ko文件和 led_test_simple复制到nfs挂载的文件夹下。
make
cp *.ko led_test_simple ~/nfs_rootfs/
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
//复制到开发板上
cp /mnt/led_drive_simple.ko ./
cp /mnt/led_test_simple ./
//安装驱动模块
insmod led_drive_simple.ko
//查询是否有我们的hello程序
cat /proc/devices
lsmod
//查询是否有我们的设备节点
ls /dev/myled -l
//打开
./led_test_simple /dev/myled on
//关闭
./led_test_simple /dev/myled off
查询设备结果:
点灯和关灯:
如果对于多个板子,去驱动同一种设备(例如去点个灯),都要像上面的方式从头到尾写一个对应的驱动程序,是件非常麻烦的事情。
简而言之,这里LED驱动是否能支持多个板子?如何实现呢?
针对上述的情况,就要应用到分层的思想,我们先要将驱动拆为通用的框架(leddrv.c)、具体的硬件操作(board_X.c),如下图所示:
这里再进一步,以面向对象的思想,抽象出一个结构体,每个单板相关的boardX.c实现自己的led_operations结构体,供上层的leddrv.c调用。
struct led_operations
{
int (*init) (int which); /*初始化LED,which---哪个LED*/
int (*ctl) (int which, int status);/*控制LED,which-哪个LED,status:1/0亮灭*/
};
相较最简框架下的驱动程序,代码里注释差异点。
这个头文件是作为board_imx6ull.c和led_drive.c之间的桥梁。
//定义这个宏,防止二次调用
//例子LED调用LEDA,LEDA调用这个头文件,未加时,会二次调用这个头文件
//加了之后,第二次调用时,就不再其效用了。
#ifndef _LED_OPR_H
#define _LED_OPR_H
struct led_operations
{
int num;
int (*init) (int which); /*初始化LED,which---哪个LED*/
int (*ctl) (int which, char status);/*控制LED,which-哪个LED,status:1/0亮灭*/
};
struct led_operations *get_board_led_opr(void);
#endif
从这里我们可以看到,对应板子上的硬件操作都在这个程序(这层)里去实现了。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "led_opr.h"
/*registers*/
//CCM_CCGR1:0x20C406C
static volatile unsigned int* CCM_CCGR1;
//IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3:0x02290000 + 0x14
static volatile unsigned int* IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
//GPIO5_GDIR:0x20AC004
static volatile unsigned int* GPIO5_GDIR;
//GPIO5_DR:0x020AC000
static volatile unsigned int* GPIO5_DR;
/*
*函数: board_demo_led_init
*功能: enable gpio 使能gpio
configure pin as gpio 设置引脚为GPIO
configure gpio as output 设置引脚为GPIO输出引脚
*传入参数:
*which:哪个LED
*返回参数:如果成功返回0
*/
static int board_demo_led_init(int which)
{
unsigned int val;
//printk("%s %s line %d, led %d\n",__FILE__,__FUNCTION__,__LINE__,which);
if(which == 0)
{
if(!CCM_CCGR1)
{
//ioremap:物理地址映射到虚拟地址
CCM_CCGR1 = ioremap(0x20C406C, 4);
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x02290000 + 0x14, 4);
GPIO5_GDIR = ioremap(0x020AC004, 4);
GPIO5_DR = ioremap(0x020AC000, 4);
}
*CCM_CCGR1 |= (3<<30);
val = *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
val &= ~0xf;
val |= 0x5;
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = val;
*GPIO5_GDIR |= (1<<3);
}
return 0;
}
/*
*函数: board_demo_led_init
*功能: set gpio register: out 1/0
*传入参数:
*which:哪个LED
*status: 状态1/0,亮灭
*返回参数:如果成功返回0
*/
static int board_demo_led_ctl(int which, char status)
{
if(which == 0)
{
if(status)
{
*GPIO5_DR &= ~(1<<3);
}else
{
*GPIO5_DR |= (1<<3);
}
}
return 0;
}
static struct led_operations board_demo_led_opr =
{
.num = 1,
.init = board_demo_led_init,
.ctl = board_demo_led_ctl,
};
struct led_operations *get_board_led_opr(void)
{
return &board_demo_led_opr;
}
之前的框架是在这里实现设备注册和硬件操作,现在只去实现注册设备驱动等通用功能。
/* 说明:
*1,本代码是跟学韦老师课程所编写,增加了注释和分析
*2,采用的是UTF-8编码格式
*3,LED驱动程序(分层思想) led_drive.c
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "led_opr.h"
static struct class *led_class;
struct led_operations *p_ledopr;
/*第一步:确定主设备号,这里由内核分配*/
static int major = 0;
/*第二步:定义自己的 file_operations 结构体,
实现对应的 drv_open/drv_write 等函数,填入 file_operations 结构体*/
/*
*函数: led_drv_open
*差异点: 根据次设备号初始化LED
*传入参数:
*flip:要打开的文件
*返回参数:如果成功返回0
*/
static int led_drv_open(struct inode *inode, struct file *filp)
{
int minor = iminor(inode);/*次设备号*/
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
p_ledopr->init(minor);
return 0;
}
/*
*函数: led_drv_write
*差异点: 根据次设备号和status控制LED
*功能: copy_from_user,从app中获取数据
*传入参数:
*flip:要写的文件
*buf: 写的数据来自于buf
*size:写多大的数据
*offset:偏移值
*返回参数: 写的数据长度
*/
static ssize_t led_drv_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
char status;
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = copy_from_user(&status, buf, 1);
/*根据次设备号控制LED*/
struct inode* inode = file_inode(filp);
int minor = iminor(inode) & 0x0f;
p_ledopr->ctl(minor, status);
return -1;
}
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.write = led_drv_write,
.open = led_drv_open,
};
/*第三步:定义一个入口函数, 调用register_chrdev(把 file_operations 结构体告诉内核)*/
/*
*函数: led_init
*差异点: 有多个次设备号
*功能: ①注册主设备号
②辅助完善:提供设备信息,自动创建设备节点:class_create、device_create
*/
static int __init led_init(void)
{
/*
*printk:判断一下是否调用了入口函数
*__FILE__ :表示文件
*__FUNCTION__ :当前函数名
*__LINE__ :在文件的哪一行
*/
printk("%s %s %d\n",__FILE__,__FUNCTION__,__LINE__);
//0:由内核分配主设备号,名称,指定函数结构体led_fops,返回值为主设备号
major = register_chrdev(0,"xixiwuli_led", &led_fops);
led_class = class_create(THIS_MODULE, "led_2345_class");
int i;
if (IS_ERR(led_class))
return PTR_ERR(led_class);
if (major < 0) {
class_destroy(led_class);
return major;
}
//通过p_ledopr,可以操作调用单板相关的代码
p_ledopr = get_board_led_opr();
//创建/dev/myled2的设备节点,多个次设备号控制多个LED
for(i = 0;i < p_ledopr->num; i++)
{
device_create(led_class, NULL, MKDEV(major, i), NULL, "myled%d",i);
}
return 0;
}
/*第四步:定义一个出口函数,出口函数调用unregister_chrdev*/
static void __exit led_exit(void)
{
int i;
for(i = 0; i < p_ledopr->num; i++)
{
device_destroy(led_class, MKDEV(major, i));
}
class_destroy(led_class);
unregister_chrdev(major,"xixiwuli_led");
}
//把函数修饰成入口函数和出口函数
module_init(led_init);
module_exit(led_exit);
//内核遵循GPL协议,所以我们也要遵循GPL协议,才能够使用内核里的一些函数
MODULE_LICENSE("Dual BSD/GPL");
#include
#include
#include
#include
#include
#include
//led_test /dev/myled0 on
//led_test /dev/myled0 off
int main(int argc, char** argv)
{
int fd;
char status = 0;
if(argc != 3)
{
printf("usage: %s \n", argv[0]);
printf("eg: %s /dev/myled on\n", argv[0]);
printf("eg: %s /dev/myled off\n", argv[0]);
}
//open
fd = open(argv[1], O_RDWR);
if(fd < 0)
{
printf("can not open %s\n",argv[1]);
return -1;
}
//write
if (strcmp(argv[2], "on") == 0)
{
status = 1;
}
write(fd, &status, 1);
close(fd);
return 0;
}
这里有小的变动,将board_imx6ull.c和led_drive.c编译在一起。
# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH, 比如: export ARCH=arm
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=arm-buildroot-linux-gnueabihf-
# 2.3 PATH, 比如: export PATH=$PATH:/home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
# 请参考各开发板的高级用户使用手册
KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88
all:
make -C $(KERN_DIR) M=`pwd` modules
$(CROSS_COMPILE)gcc -o led_test led_test.c
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
rm -f led_test
#led_drive.c和board_demo.c编译成xixiwuli_led.ko
xixiwuli_led-y := led_drive.o board_imx6ull.o
obj-m += xixiwuli_led.o
前三个步骤跟上述完全一致,就不再赘述。
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
//复制到开发板上
cp /mnt/xixiwuli_led.ko ./
cp /mnt/led_test ./
//安装驱动模块
insmod xixiwuli_led.ko
//查询是否有我们的hello程序
cat /proc/devices
lsmod
//查询是否有我们的设备节点
ls /dev/myled0 -l
//打开
./led_test /dev/myled0 on
//关闭
./led_test /dev/myled0 off
//程序运行完,可以卸载相应的模块
rmmod xixiwuli_led.ko
查询设备结果:
点灯和关灯:
从结果上来看,相较于简单的驱动框架下的驱动程序,也能够很好的实现点灯关灯。
但结构上,他很好的将通用功能和硬件操作部分做了分离,分别对应led_drive_stra.c和board_imx6ull.c,再加入一个设备时,也只需要创建一个board_X.c就可以了。
这样的驱动程序有了更好的拓展性,可以支持多个板子。也从这里我们能更好的理解了Linux驱动 = 驱动框架 + 硬件操作,相信大家也开始慢慢感受到驱动框架的魅力。
上述的方式,我们能够发现board_X.c里跟芯片硬件绑定的太死,如果我们要换个灯点亮,就需要修改代码,重新编译,那么如何解决呢?
简而言之,如何解决硬件操作不灵活的问题?
针对上述的问题, 就要应用到分离的设计思想。对于某一款芯片,引脚操作是类似的,可以写出一个通用的驱动程序,进行一个左右分离,一个定义资源(board_X.c),一个定义硬件的通用操作(chipY_gpio.c),如下图所示:
这里就抽象出一个led_resource结构体,来表达具体资源是怎么样的。(沿用面向对象的思想)
这里相较于第二节的程序,board_imx6ull.c文件要分成两个文件board_A_led.c和chip_imx6ull_gpio.c。所以这里主要阐述led_resource.h、board_A_led.c、chip_imx6ull_gpio.c以及Makefile代码。具体实现框架如下:
led_resource.h里的结构体led_resource定义资源。
#ifndef _LED_RE_H
#define _LED_RE_H
/*GPIO5*/
/*bit[31:16] = group*/
/*bit[15:0] = pin*/
//可以用以下的宏,表示GPIO引脚
#define GROUP(x) (x>>16)
#define PIN(x) (x&0xFFFF)
#define GROUP_PIN(g,p) ((g<<16) | (p))
struct led_resource
{
int pin;
};
struct led_resource *get_led_resource(void);
#endif
这里的资源:GPIO引脚,这里初始设置为GPIO5_3。
#include "led_resource.h"
static struct led_resource board_A_led = {
.pin = GROUP_PIN(5,3),
};
struct led_resource * get_led_resource(void)
{
return &board_A_led;
};
这里具体实现GPIO通用硬件操作,以GPIO5_3为例,先不涉及繁杂的硬件操作。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "led_opr.h"
#include "led_resource.h"
static struct led_resource *led_rsc;
/*阐述说明:
*现阶段仅以展示分离设计思想
*以下的为GPIO5_3需要设置的寄存器绝对物理地址,后续可以按照基址表示GPIO组内多引脚。
*定义好多个基址,可以实现表示多个GPIO组
*/
/*
//CCM_CCGR1:0x20C406C
static volatile unsigned int* CCM_CCGR1;
//IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3:0x02290000 + 0x14
static volatile unsigned int* IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
//GPIO5_GDIR:0x20AC004
static volatile unsigned int* GPIO5_GDIR;
//GPIO5_DR:0x020AC000
static volatile unsigned int* GPIO5_DR;
*/
/*
*函数: board_demo_led_init
*功能: enable gpio 使能gpio
configure pin as gpio 设置引脚为GPIO
configure gpio as output 设置引脚为GPIO输出引脚
*传入参数:
*which:哪个LED
*返回参数:如果成功返回0
*/
static int board_demo_led_init(int which)
{
unsigned int val;
//printk("%s %s line %d, led %d\n",__FILE__,__FUNCTION__,__LINE__,which);
if(!led_rsc)
{
led_rsc = get_led_resource();
}
printk("init gpio: group %d, pin %d\n", GROUP(led_rsc->pin), PIN(led_rsc->pin));
switch(GROUP(led_rsc->pin))
{
case 0:
{
printk("init pin of group 0 ...\n");
break;
}
case 1:
{
printk("init pin of group 1 ...\n");
break;
}
case 2:
{
printk("init pin of group 2 ...\n");
break;
}
case 3:
{
printk("init pin of group 3 ...\n");
break;
}
}
/*
if(which == 0)
{
if(!CCM_CCGR1)
{
//ioremap:物理地址映射到虚拟地址
CCM_CCGR1 = ioremap(0x20C406C, 4);
IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x02290000 + 0x14, 4);
GPIO5_GDIR = ioremap(0x020AC004, 4);
GPIO5_DR = ioremap(0x020AC000, 4);
}
*CCM_CCGR1 |= (3<<30);
val = *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
val &= ~0xf;
val |= 0x5;
*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = val;
*GPIO5_GDIR |= (1<<3);
}
*/
return 0;
}
/*
*函数: board_demo_led_init
*功能: set gpio register: out 1/0
*传入参数:
*which:哪个LED
*status: 状态1/0,亮灭
*返回参数:如果成功返回0
*/
static int board_demo_led_ctl(int which, char status)
{
printk("set led %s: group %d, pin %d\n", status ? "on" : "off", GROUP(led_rsc->pin), PIN(led_rsc->pin));
switch(GROUP(led_rsc->pin))
{
case 0:
{
printk("set pin of group 0 ...\n");
break;
}
case 1:
{
printk("set pin of group 1 ...\n");
break;
}
case 2:
{
printk("set pin of group 2 ...\n");
break;
}
case 3:
{
printk("set pin of group 3 ...\n");
break;
}
}
/*
if(which == 0)
{
if(status)
{
*GPIO5_DR &= ~(1<<3);
}else
{
*GPIO5_DR |= (1<<3);
}
}
*/
return 0;
}
static struct led_operations board_demo_led_opr =
{
.num = 1,
.init = board_demo_led_init,
.ctl = board_demo_led_ctl,
};
struct led_operations *get_board_led_opr(void)
{
return &board_demo_led_opr;
}
这里有小的变动,将led_drive.c、board_A_led.c以及chip_imx6ull_gpio.c编译在一起。
# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH, 比如: export ARCH=arm
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=arm-buildroot-linux-gnueabihf-
# 2.3 PATH, 比如: export PATH=$PATH:/home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
# 请参考各开发板的高级用户使用手册
KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88
all:
make -C $(KERN_DIR) M=`pwd` modules
$(CROSS_COMPILE)gcc -o led_test led_test.c
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
rm -f led_test
#led_drive.c、board_A_led.c、chip_imx6ull_gpio.c编译成xixiwuli.ko
xixiwuli_led-y := led_drive.o board_A_led.o chip_imx6ull_gpio.o
obj-m += xixiwuli_led.o
跟上面所述基本一致。
//打开
./led_test /dev/myled0 on
//关闭
./led_test /dev/myled0 off
dmesg
打印演示:
经过上述分离设计思想的实践,我们会发现在需要变更要控制的引脚时(打开另一个LED时),我们只需要改动board_A_led.c就可以了。
相较于第二节的部分,拓展性和灵活性又得到了进一步的提升。
上一节的内容里,我们可以进一步发现如果我们处理多个不同的设备,例如LED、LCD等等,我们都要再定义一个相应的resource.h,这个是不现实的。
基于上述的问题,提出了总线设备驱动模型,它是分离思想的进一步实现。
后续进一步采用bus总线来管理platform_device /platform_driver。如下图所示:
在第三节程序基础上进一步进阶到总线设备驱动框架,需要抽象出platform_device和platform_driver结构体,并实现它们之间的匹配配对。
具体的各个步骤如下:
1.分配/设置/注册 platform_device结构体
- 在里面定义所用资源,指定设备名字
2. 分配/设置/注册 platform_driver结构体
- 在其中probe函数里,分配/设置/注册file_operations结构体
- 并从platform_device中确定所用硬件资源,动态实现device_create
- 指定platform_driver的名字
相较第三节内容,展示内容改动的部分。
声明底层到上层的注册函数。
#ifndef _LEDDRV_H
#define _LEDDRV_H
#include "led_opr.h"
void led_device_create(int minor);
void led_device_destory(int minor);
void register_led_operations(struct led_operations *opr);
#endif /* _LEDDRV_H */
这里属于驱动程序的最上层,具体阐述和分析,详见代码。
/* 说明:
*1,本代码是跟学韦老师课程所编写,增加了注释和分析
*2,采用的是UTF-8编码格式
*3,LED驱动程序(总线设备驱动模型) led_drive.c
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "led_opr.h"
/*第一步:确定主设备号,这里由内核分配*/
static int major = 0;
static struct class *led_class;
struct led_operations *p_ledopr;
/*
*阐述说明
*定义给底层(chip_imx6ull_gpio.c)去调用的
*/
void led_device_create(int minor)
{
device_create(led_class, NULL, MKDEV(major, minor), NULL, "myled%d", minor);
}
void led_device_destory(int minor)
{
device_destroy(led_class, MKDEV(major, minor));
}
//定义底层向上层注册函数,避免交叉编译
void register_led_operations(struct led_operations *opr)
{
p_ledopr = opr;
}
EXPORT_SYMBOL(led_device_create);
EXPORT_SYMBOL(led_device_destory);
EXPORT_SYMBOL(register_led_operations);
/*第二步:定义自己的 file_operations 结构体,
实现对应的 drv_open/drv_write 等函数,填入 file_operations 结构体*/
/*
*函数: led_drv_open
*差异点: 根据次设备号初始化LED
*传入参数:
*flip:要打开的文件
*返回参数:如果成功返回0
*/
static int led_drv_open(struct inode *inode, struct file *filp)
{
int minor = iminor(inode);/*次设备号*/
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
p_ledopr->init(minor);
return 0;
}
/*
*函数: led_drv_write
*差异点: 根据次设备号和status控制LED
*功能: copy_from_user,从app中获取数据
*传入参数:
*flip:要写的文件
*buf: 写的数据来自于buf
*size:写多大的数据
*offset:偏移值
*返回参数: 写的数据长度
*/
static ssize_t led_drv_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
char status;
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = copy_from_user(&status, buf, 1);
/*根据次设备号控制LED*/
struct inode* inode = file_inode(filp);
int minor = iminor(inode) & 0x0f;
p_ledopr->ctl(minor, status);
return 1;
}
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.write = led_drv_write,
.open = led_drv_open,
};
/*第三步:定义一个入口函数, 调用register_chrdev(把 file_operations 结构体告诉内核)*/
/*
*函数: led_init
*功能: ①注册主设备号
②辅助完善:提供设备信息,自动创建设备节点:class_create、device_create
*/
static int __init led_init(void)
{
/*
*printk:判断一下是否调用了入口函数
*__FILE__ :表示文件
*__FUNCTION__ :当前函数名
*__LINE__ :在文件的哪一行
*/
printk("%s %s %d\n",__FILE__,__FUNCTION__,__LINE__);
//0:由内核分配主设备号,名称,指定函数结构体led_fops,返回值为主设备号
major = register_chrdev(0,"100ask_led", &led_fops);
led_class = class_create(THIS_MODULE, "led_2345_class");
int i;
if (IS_ERR(led_class))
return PTR_ERR(led_class);
if (major < 0) {
class_destroy(led_class);
return major;
}
return 0;
}
/*第四步:定义一个出口函数,出口函数调用unregister_chrdev*/
static void __exit led_exit(void)
{
class_destroy(led_class);
unregister_chrdev(major,"100ask_led");
}
//把函数修饰成入口函数和出口函数
module_init(led_init);
module_exit(led_exit);
//内核遵循GPL协议,所以我们也要遵循GPL协议,才能够使用内核里的一些函数
MODULE_LICENSE("Dual BSD/GPL");
本程序主要定义一些资源,编写实现platform_device结构体。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "led_resource.h"
static void led_dev_release(struct device *dev)
{
}
//定义一个资源数组
static struct resource resources[] = {
{
.start = GROUP_PIN(5,3),
.flags = IORESOURCE_IRQ,
},
{
.start = GROUP_PIN(3,1),
.flags = IORESOURCE_IRQ,
},
};
/*
*name:平台名称
*num_resources:资源个数
*resource:引入资源数组
*/
static struct platform_device board_A_led_dev =
{
.name = "100ask_led",
.num_resources = ARRAY_SIZE(resources),
.resource = resources,
.dev = {
.release = led_dev_release,
},
};
/*入口函数,patform_device
*功能:注册设备
*/
static int led_dev_init(void)
{
int err;
err = platform_device_register(&board_A_led_dev);
return 0;
}
/*出口函数,platform_device*/
static void led_dev_exit(void)
{
platform_device_unregister(&board_A_led_dev);
}
module_init(led_dev_init);
module_exit(led_dev_exit);
MODULE_LICENSE("GPL");
这里主体实现platform_driver结构体。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "led_drive.h"
#include "led_opr.h"
#include "led_resource.h"
static int g_ledpins[100];
static int g_ledcnt = 0;
/*阐述说明:
*现阶段仅以示意总线设备驱动模型框架
*/
/*
*函数: board_imx6ull_led_init
*功能: 获取gpio引脚信息
*输入参数:which---哪个引脚
*返回参数:如果成功返回0
*/
static int board_imx6ull_led_init(int which)
{
//printk("%s %s line %d, led %d\n",__FILE__,__FUNCTION__,__LINE__,which);
printk("init gpio: group %d, pin %d\n", GROUP(g_ledpins[which]), PIN(g_ledpins[which]));
switch(GROUP(g_ledpins[which]))
{
case 0:
{
printk("init pin of group 0 ...\n");
break;
}
case 1:
{
printk("init pin of group 1 ...\n");
break;
}
case 2:
{
printk("init pin of group 2 ...\n");
break;
}
case 3:
{
printk("init pin of group 3 ...\n");
break;
}
}
return 0;
}
/*
*函数: board_imx6ull_led_init
*功能: 打印某个gpio引脚设置信息
*输入参数:which---哪一个引脚
status:LED状态,1/0亮灭
*返回参数:如果成功返回0
*/
static int board_imx6ull_led_ctl(int which, char status)
{
printk("set led %s: group %d, pin %d\n", status ? "on" : "off", GROUP(g_ledpins[which]), PIN(g_ledpins[which]));
switch(GROUP(g_ledpins[which]))
{
case 0:
{
printk("set pin of group 0 ...\n");
break;
}
case 1:
{
printk("set pin of group 1 ...\n");
break;
}
case 2:
{
printk("set pin of group 2 ...\n");
break;
}
case 3:
{
printk("set pin of group 3 ...\n");
break;
}
}
return 0;
}
static struct led_operations board_imx6ull_led_opr =
{
.init = board_imx6ull_led_init,
.ctl = board_imx6ull_led_ctl,
};
struct led_operations *get_board_led_opr(void)
{
return &board_imx6ull_led_opr;
}
/*
*函数:chip_imx6ull_gpio_led_probe
*功能:记录引脚
创建设备,device_create
*/
static int chip_imx6ull_gpio_led_probe(struct platform_device *dev)
{
int i = 0;
struct resource *res;
while(1)
{
//设备、哪一类资源、第几个资源
res = platform_get_resource(dev,IORESOURCE_IRQ, i++);
if(!res)
break;
g_ledpins[g_ledcnt] = res->start;
/*创建设备*/
led_device_create(g_ledcnt);
g_ledcnt++;
}
return 0;
}
/*
*函数:chip_imx6ull_gpio_led_remove
*功能:注销设备,device_destory
*/
static int chip_imx6ull_gpio_led_remove(struct platform_device *dev)
{
int i;
for(i = 0; i < g_ledcnt; i++)
{
led_device_destory(i);
}
g_ledcnt = 0;
return 0;
}
static struct platform_driver chip_imx6ull_gpio_driver = {
.probe = chip_imx6ull_gpio_led_probe,
.remove = chip_imx6ull_gpio_led_remove,
.driver = {
.name = "100ask_led",
},
};
/*入口函数,platform_driver
*功能:注册设备
底层到上层的注册
*/
static int chip_imx6ull_gpio_drv_init(void)
{
int err;
err = platform_driver_register(&chip_imx6ull_gpio_driver);
register_led_operations(&board_imx6ull_led_opr);
return 0;
}
/*出口函数,paltform_driver*/
static void chip_imx6ull_gpio_drv_exit(void)
{
platform_driver_unregister(&chip_imx6ull_gpio_driver);
}
module_init(chip_imx6ull_gpio_drv_init);
module_exit(chip_imx6ull_gpio_drv_exit);
MODULE_LICENSE("Dual BSD/GPL");
上述程序的变动,Makefile也有一定的改动。
# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH, 比如: export ARCH=arm
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=arm-buildroot-linux-gnueabihf-
# 2.3 PATH, 比如: export PATH=$PATH:/home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
# 请参考各开发板的高级用户使用手册
KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88
all:
make -C $(KERN_DIR) M=`pwd` modules
$(CROSS_COMPILE)gcc -o led_test led_test.c
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
rm -f led_test
# 参考内核源码drivers/char/ipmi/Makefile
# 要想把a.c, b.c编译成ab.ko, 可以这样指定:
# ab-y := a.o b.o
# obj-m += ab.o
obj-m += led_drive.o chip_imx6ull_gpio.o board_A_led.o
//复制到开发板上
cp /mnt/board_A_led.ko ./
cp /mnt/chip_imx6ull_gpio.ko ./
cp /mnt/led_drive.ko ./
cp /mnt/led_test ./
//安装驱动模块
insmod board_A_led.ko
insmod led_drive.ko
insmod chip_imx6ull_gpio.ko
//GPIO5_3
./led_test /dev/myled0 on
./led_test /dev/myled0 off
//GPIO3_1
./led_test /dev/myled1 on
./led_test /dev/myled1 off
dmesg
打印结果:
相较第三节上代码上的变化,可以感受到总线设备驱动模型框架的优点,在原有基础上进一步实现了对device和driver的分离。
在应对处理不同的设备时,这种方式就能更好的进行管理。
我们从第四节的内容中发现,如果我们修改LED所用的GPIO引脚,我们需要修改board_A_led.c代码,之后重新编译加载驱动。
随着ARM芯片的流行,内核中针对不同厂商的开发保存里大量类似,没有技术含量的文件。
那么针对上述的问题,有没有好的解决方案呢?
上述问题的核心,就是在于是用.c文件来配置资源。这里就引入了专门的配置文件,这里就是用设备树来实现这一点。
采用设备树后,许多硬件的细节可以直接通过它传递给Linux,而不再需要在内核中进行大量的冗余编码,它通过bootloader将硬件资源传给内核,使得内核和文件资源描述相对独立。
最终的效果,设备在脚本里,驱动在c里。
设备树包含DTC (device tree compiler),DTS (device tree source) 和DTB (device tree blob)。
dtc、dts/dtsi和dtb的关系:
dts和dtsi源文件会经过dtc编译器编译成dtb二进制文件,dtb文件最后会被放到系统中被内核解析。
具体使用的语法和设备树解析,详细参照:
https://blog.csdn.net/qq_33487044/article/details/126325656
怎么使用设备树写驱动程序?
注意的地方:
设备树内容繁杂,不在这里详细阐述。这里的程序实现简单功能和走完整个流程。
后面主要展示改动的部分程序,设备树文件、驱动程序(chip_imx6ull_gpio.c),board_A_led.c文件则是不需要了。
修改设备树,添加设备节点100ask_led@0和100ask_led@1。
#define GROUP_PIN(g,p) ((g<<16) | (p))
/ {
100ask_led@0 {
compatible = "100as,leddrv";
pin = ;
};
100ask_led@1 {
compatible = "100as,leddrv";
pin = ;
};
};
imx6ull pro开发板里内核源码目录中arch/arm/boot/dts/100ask_imx6ull-14x14.dts,修改编译后得到arch/arm/boot/dts/100ask_imx6ull-14x14.dtb。
详细步骤:(PC端)
这里主要设置of_match_table成员,用于设备树节点和platform_driver匹配之后。probe函数里获取资源的方式转变,通过读取设备树文件获取资源。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "led_drive.h"
#include "led_opr.h"
#include "led_resource.h"
static int g_ledpins[100];
static int g_ledcnt = 0;
/*阐述说明:
*现阶段仅以示意总线设备驱动模型框架
*/
/*
*函数: board_imx6ull_led_init
*功能: 获取gpio引脚信息
*输入参数:which---哪个引脚
*返回参数:如果成功返回0
*/
static int board_imx6ull_led_init(int which)
{
//printk("%s %s line %d, led %d\n",__FILE__,__FUNCTION__,__LINE__,which);
printk("init gpio: group %d, pin %d\n", GROUP(g_ledpins[which]), PIN(g_ledpins[which]));
switch(GROUP(g_ledpins[which]))
{
case 0:
{
printk("init pin of group 0 ...\n");
break;
}
case 1:
{
printk("init pin of group 1 ...\n");
break;
}
case 2:
{
printk("init pin of group 2 ...\n");
break;
}
case 3:
{
printk("init pin of group 3 ...\n");
break;
}
}
return 0;
}
/*
*函数: board_imx6ull_led_init
*功能: 打印某个gpio引脚设置信息
*输入参数:which---哪一个引脚
status:LED状态,1/0亮灭
*返回参数:如果成功返回0
*/
static int board_imx6ull_led_ctl(int which, char status)
{
printk("set led %s: group %d, pin %d\n", status ? "on" : "off", GROUP(g_ledpins[which]), PIN(g_ledpins[which]));
switch(GROUP(g_ledpins[which]))
{
case 0:
{
printk("set pin of group 0 ...\n");
break;
}
case 1:
{
printk("set pin of group 1 ...\n");
break;
}
case 2:
{
printk("set pin of group 2 ...\n");
break;
}
case 3:
{
printk("set pin of group 3 ...\n");
break;
}
}
return 0;
}
static struct led_operations board_imx6ull_led_opr =
{
.init = board_imx6ull_led_init,
.ctl = board_imx6ull_led_ctl,
};
struct led_operations *get_board_led_opr(void)
{
return &board_imx6ull_led_opr;
}
/*
*函数:chip_imx6ull_gpio_led_probe
*功能:记录引脚
创建设备,device_create
*/
static int chip_imx6ull_gpio_led_probe(struct platform_device *dev)
{
int i = 0;
struct resource *res;
struct device_node *p;
int led_pin;
p = dev->dev.of_node;
if(!p)
return -1;
//读取pin属性的值保存在led_pin变量里
int err = of_property_read_u32(p, "pin", &led_pin);
g_ledpins[g_ledcnt] = led_pin;
/*创建设备*/
led_device_create(g_ledcnt);
g_ledcnt++;
return 0;
}
/*
*函数:chip_imx6ull_gpio_led_remove
*功能:注销设备,device_destory
*/
static int chip_imx6ull_gpio_led_remove(struct platform_device *dev)
{
int i;
for(i = 0; i < g_ledcnt; i++)
{
led_device_destory(i);
}
g_ledcnt = 0;
return 0;
}
//of_match_table成员,用于设备树节点和platform_driver的匹配上
static const struct of_device_id ask100_leds[] = {
{ .compatible = "100as,leddrv" },
{ },
};
static struct platform_driver chip_imx6ull_gpio_driver = {
.probe = chip_imx6ull_gpio_led_probe,
.remove = chip_imx6ull_gpio_led_remove,
.driver = {
.name = "100ask_led",
.of_match_table = ask100_leds,
},
};
/*入口函数,platform_driver
*功能:注册设备
底层到上层的注册
*/
static int chip_imx6ull_gpio_drv_init(void)
{
int err;
err = platform_driver_register(&chip_imx6ull_gpio_driver);
register_led_operations(&board_imx6ull_led_opr);
return 0;
}
/*出口函数,paltform_driver*/
static void chip_imx6ull_gpio_drv_exit(void)
{
platform_driver_unregister(&chip_imx6ull_gpio_driver);
}
module_init(chip_imx6ull_gpio_drv_init);
module_exit(chip_imx6ull_gpio_drv_exit);
MODULE_LICENSE("Dual BSD/GPL");
相较上节的内容,这里不再需要board_A_led.c。
# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH, 比如: export ARCH=arm
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=arm-buildroot-linux-gnueabihf-
# 2.3 PATH, 比如: export PATH=$PATH:/home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
# 请参考各开发板的高级用户使用手册
KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88
all:
make -C $(KERN_DIR) M=`pwd` modules
$(CROSS_COMPILE)gcc -o led_test led_test.c
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
rm -f led_test
# 参考内核源码drivers/char/ipmi/Makefile
# 要想把a.c, b.c编译成ab.ko, 可以这样指定:
# ab-y := a.o b.o
# obj-m += ab.o
obj-m += led_drive.o chip_imx6ull_gpio.o
测试程序:(设备树加载情况)
//加载设备树文件
cp /mnt/100ask_imx6ull-14x14.dtb /boot/
reboot
cd /sys/firmware/devicetree/base/
ls -ld *100ask*
cd 100ask_led@0
ls
cat compatible
hexdump pin
后续led_test测试程序的运行结果跟上一章节完全一致,这里是管理资源的方式发生了转变。
相信大家在过完上述LED的驱动进化之路,会有一种酣畅淋漓的感觉。