使用的开发板为正点原子STM32mp157。
参考资料为正点原子驱动开发教程,以及STM32mp芯片手册等。
之前已经学会了如何直接操作寄存器进行led的驱动开发。
那么在此基础之上,引入设备树进行修改。
回忆以下之前的流程。
- 驱动的注册与卸载
- 分别编写入口函数和出口函数
- 注册函数与卸载函数(重点是file_operations函数)
- 地址映射以及led初始化
- led相关程序的功能实现
新字符设备驱动
之前分配和释放设备号,使用的是register_chrdev这个函数,只需要给定一个主设备号即可。
但是会有两个问题,我们需要知道哪些设备号没有被使用,同时会将一个主设备号下的次设备号都使用掉。
1 构造设备号
这里引入新的设备号申请。
如果没有指定设备号的话:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
如果指定了设备号的话:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
释放设备号:
void unregister_chrdev_region(dev_t from, unsigned count)
知道了这几个函数之后,我们进入内核源文件中,找到相应的文件,查看使用方法。
可以看出有两种方法,一种是判断有没有指定主设备号,指定了设备号怎么申请和没有指定设备号怎么申请。另一种则是直接申请设备号。
这里我们是初学者,为了保险一点,选用第一种方法。
在此之前,我们定义一个结构体。
//newchrled设备结构体
struct newchrled_dev
{
dev_t devid; //设备号
int major; //主设备号
int minor; //次设备号
};
将设备号,主设备号,次设备号定义好。
//构造设备号
if(newchrled.major) //如果指定了设备号
{
newchrled.devid = MKDEV(newchrled.major,0);
ret = register_chrdev_region(newchrled.devid,NEWCHRLED_CNT,NEWCHRLED_NAME);
if(ret<0){
printk("chrdevbase driver register failed!\r\n");
goto fail_map;
}
}else{
ret = alloc_chrdev_region(&newchrled.devid,0,NEWCHRLED_CNT,NEWCHRLED_NAME);//从内核申请
if(ret<0){
printk("chrdevbase driver register failed!\r\n");
goto fail_map;
}
}
仿照上面第一种形式进行编写,然后使用goto语句进行错误处理。
fail_map:
led_iounmap();
return -EIO;
发生了错误,则释放地址映射,避免其他出问题。
2 注册字符设备
引入cdev结构体表示字符设备,我们可以在内核文件中找到这个结构体,它的定义如下:
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
} __randomize_layout;
我们主要关注ops和dev就行。
这个跟之前的类似,重点在于ops的编写。
观察内核文件中,该结构体的用法。
可以看到都是先进行初始化,然后添加字符设备。
对于初始化函数。
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
我们首先要定义一个cdev结构体变量,这里直接添加进之前定义的设备结构体中。
//newchrled设备结构体
struct newchrled_dev
{
dev_t devid; //设备号
struct cdev cdev; //字符设备
int major; //主设备号
int minor; //次设备号
};
fop函数在之前已经定义过,这里不用修改。
再看添加函数。
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
注意,这个有返回值。基本上里面的变量我们都已经定义过了。
开始写代码。
//注册字符设备
newchrled.cdev.owner = THIS_MODULE;
cdev_init(&newchrled.cdev,&led_fops);
ret = cdev_add(&newchrled.cdev,newchrled.devid,NEWCHRLED_CNT);
if(ret<0){
goto del_unregister;
}
同样的,跟前面一样,goto语句处理错误,由于上一步是申请设备号,所以我们这里goto语句转的就是释放设备号这个命令。
3 自动创建设备节点
之前我们都是用mknod来手动创建设备节点,这一次我们直接在文件里实现自动创建设备节点。
可以看到跟前面的写法类似。
struct class *class_create (struct module *owner, const char *name)
//自动创建设备节点
newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);
if (IS_ERR(newchrled.class)) {
pr_err("QAT: class_create failed for adf_ctl\n");
goto del_unregister;
}
4 创建设备
跟之前的写法类似,同样也是要定义一个结构体在设备结构体中。
//newchrled设备结构体
struct newchrled_dev
{
dev_t devid; //设备号
struct cdev cdev; //字符设备
struct device *device;//设备,注意是指针类型
struct class *class; //设备节点,注意是指针类型
int major; //主设备号
int minor; //次设备号
};
//创建设备
newchrled.device = device_create(newchrled.class, NULL,
newchrled.devid,
NULL, NEWCHRLED_NAME);
if (IS_ERR( newchrled.device)) {
pr_err("QAT: failed to create device\n");
goto err_cdev_del;
}
如此,我们基本的注册就完成了,接下来引进设备树。
设备树下的led驱动
简单来说,就是我们在设备树下定义一个节点,然后通过此节点获取各寄存器的物理地址信息。
/*dada*/
stm32mp1_led {
compatible = "atkstm32mp1-led";
status = "okay";
reg = <0X50000A28 0X04 /* RCC_MP_AHB4ENSETR */
0X5000A000 0X04 /* GPIOI_MODER */
0X5000A004 0X04 /* GPIOI_OTYPER */
0X5000A008 0X04 /* GPIOI_OSPEEDR */
0X5000A00C 0X04 /* GPIOI_PUPDR */
0X5000A018 0X04 >; /* GPIOI_BSRR */
};
在根节点下面创建,并且标记好一些个人信息,方便后面修改,我写了dada。
这里主要定义了三个变量,从原子的教程中可以具体知道这三个变量的定义,reg里面定义的寄存器地址和它们的长度。
修改之后保存,然后编译我们的dtb文件,得到新的dtb文件,拷贝进tftproot中,重新加载内核,就可以发现stm32mp1_led这个节点已经存在了。
然后注意,使用OF函数之前,我们需要在文件里添加头文件。
#include <linux/of.h>
#include <linux/of_address.h>
不然就会报错,别问,问就是我遇到过。
定义一个设备节点结构体变量,也是在我们的设备结构体中添加。
//定义dtsled设备结构体
struct dtsled_dev{
dev_t devid;//设备号
int major; //主设备号
int minor; //次设备号
struct cdev cdev;//注册函数
struct class *class;//节点
struct device *device;//设备
struct device_node *nd;//设备节点
};
然后获取设备节点。
//获取设备树属性内容
//获取设备节点
dtsled.nd = of_find_node_by_path("/stm32mp1_led");//前面要多加一个/
if(dtsled.nd == NULL){
ret = -EINVAL;
goto fail_findnd;
}
这里特别要注意的是,stm32mp1_led之前要加 / 。
如果没有加的话,编译会顺利通过,但是你在加载驱动的时候,会有几十行的报错,让你无从下手!!
后面基本就没什么问题了,只要知道了函数用法按部就班的编写就行,跟之前一样,有返回值的要注意利用这个返回值判断一下是否发生错误。
//获取compatible属性
proper = of_find_property(dtsled.nd, "compatible", NULL);
if (proper == NULL)
{
printk("compatible property find failed\r\n");
}
else
{
printk("compatible = %s\r\n", (char *)proper->value);
}
//获取status属性
ret = of_property_read_string(dtsled.nd, "status", &str);
if(ret < 0){
printk("status read failed!\r\n");
}else{
printk("status = %s\r\n",str);
}
//获取 reg 属性内容,获取数组
ret = of_property_read_u32_array(dtsled.nd, "reg", regdata, 12);
if (ret < 0)
{
printk("reg property read failed!\r\n");
}
else
{
u8 i = 0;
printk("reg data:\r\n");
for (i = 0; i < 12; i++)
printk("%#X ", regdata[i]);
printk("\r\n");
}
获取到了之后就将我们的物理地址进行映射。
可以使用regdata这个数组进行映射,还是使用ioremap函数。
我们这里只是看一下获取的物理地址对不对,但其实可以直接使用of_iomap函数直接利用节点进行映射。
//初始化 LED
//寄存器地址映射
RCC_MP_AHB4ENSETR_PI = of_iomap(dtsled.nd, 0);
GPIOI_MODER_PI = of_iomap(dtsled.nd, 1);
GPIOI_OTYPER_PI =of_iomap(dtsled.nd, 2);
GPIOI_OSPEEDR_PI = of_iomap(dtsled.nd, 3);
GPIOI_PUPDR_PI = of_iomap(dtsled.nd, 4);
GPIOI_BSRR_PI = of_iomap(dtsled.nd, 5);
最后,要注意的是,我们在进行获取属性,映射完地址之后,再进行LED的初始化,然后再开始构建我们的设备号,注册字符设备等。基本上编写就是这么个顺序。
完整代码
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/cdev.h>
#include <linux/of.h>
#include <linux/of_address.h>
#define LED_ON 1
#define LED_OFF 0
#define DTSLED_CNT 1
#define DTSLED_NAME "dtsled"
//映射后的寄存器虚拟地址指针
static void __iomem *RCC_MP_AHB4ENSETR_PI;
static void __iomem *GPIOI_MODER_PI;
static void __iomem *GPIOI_OTYPER_PI;
static void __iomem *GPIOI_OSPEEDR_PI;
static void __iomem *GPIOI_PUPDR_PI;
static void __iomem *GPIOI_BSRR_PI;
//定义dtsled设备结构体
struct dtsled_dev{
dev_t devid;//设备号
int major; //主设备号
int minor; //次设备号
struct cdev cdev;//注册函数
struct class *class;//节点
struct device *device;//设备
struct device_node *nd;//设备节点
};
struct dtsled_dev dtsled; //led设备
//取消映射
static void led_iounmap(void)
{
iounmap(RCC_MP_AHB4ENSETR_PI);
iounmap(GPIOI_MODER_PI);
iounmap(GPIOI_OTYPER_PI);
iounmap(GPIOI_OSPEEDR_PI);
iounmap(GPIOI_PUPDR_PI);
iounmap(GPIOI_BSRR_PI);
}
void led_switch(u8 sta)
{
u32 val = 0;
if (sta == LED_ON)
{
val = readl(GPIOI_BSRR_PI);
val &= ~(0x1 << 16);
val |= (0x1 << 16); // 设置为1
writel(val, GPIOI_BSRR_PI);
}
else if (sta == LED_OFF)
{
val = readl(GPIOI_BSRR_PI);
val &= ~(0x1 << 0);
val |= (0x1 << 0); // 设置为1
writel(val, GPIOI_BSRR_PI);
}
}
static int led_open(struct inode *inode, struct file *filp)
{
int ret = 0;
return ret;
}
static int led_release(struct inode *inode, struct file *filp)
{
int ret = 0;
return ret;
}
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt,
loff_t *offt)
{
int ret = 0;
return ret;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t led_write(struct file *filp, const char __user *buf,
size_t cnt, loff_t *offt)
{
int ret = 0;
unsigned char databuf[1];//数据缓冲区
unsigned char ledstat;//灯的状态
ret = copy_from_user(databuf,buf,cnt);
if (ret < 0)
{
printk("kernel receviedata failed!\r\n");
ret = -EFAULT;
}
ledstat = databuf[0];//传进来灯的开关状态
if(ledstat == LED_ON) //开灯
{
led_switch(LED_ON);
}else if(ledstat == LED_OFF)//关灯
{
led_switch(LED_OFF);
}
return ret;
}
const struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.release = led_release,
.write = led_write,
.read = led_read,
};
//入口函数
static int __init dtsled_init(void)
{
int ret = 0;
int val =0;
u32 regdata[12];
const char *str;
struct property *proper;
//获取设备树属性内容
//获取设备节点
dtsled.nd = of_find_node_by_path("/stm32mp1_led");
if(dtsled.nd == NULL){
ret = -EINVAL;
goto fail_findnd;
}
//获取compatible属性
proper = of_find_property(dtsled.nd, "compatible", NULL);
if (proper == NULL)
{
printk("compatible property find failed\r\n");
}
else
{
printk("compatible = %s\r\n", (char *)proper->value);
}
//获取status属性
ret = of_property_read_string(dtsled.nd, "status", &str);
if(ret < 0){
printk("status read failed!\r\n");
}else{
printk("status = %s\r\n",str);
}
//获取 reg 属性内容,获取数组
ret = of_property_read_u32_array(dtsled.nd, "reg", regdata, 12);
if (ret < 0)
{
printk("reg property read failed!\r\n");
}
else
{
u8 i = 0;
printk("reg data:\r\n");
for (i = 0; i < 12; i++)
printk("%#X ", regdata[i]);
printk("\r\n");
}
//初始化 LED
//寄存器地址映射
RCC_MP_AHB4ENSETR_PI = of_iomap(dtsled.nd, 0);
GPIOI_MODER_PI = of_iomap(dtsled.nd, 1);
GPIOI_OTYPER_PI =of_iomap(dtsled.nd, 2);
GPIOI_OSPEEDR_PI = of_iomap(dtsled.nd, 3);
GPIOI_PUPDR_PI = of_iomap(dtsled.nd, 4);
GPIOI_BSRR_PI = of_iomap(dtsled.nd, 5);
//使能GPIO时钟
val = readl(RCC_MP_AHB4ENSETR_PI);
val &= ~(0x1 << 8);//将bit8位清0
val |=(0x1 << 8);//将bit8置1
writel(val,RCC_MP_AHB4ENSETR_PI);
//将GPIOI设置为输出
val = readl(GPIOI_MODER_PI);
val &= ~(0x3 << 0);//将bit0和1位清0s
val |=(0x1 << 0);//将bit0和1置为01
writel(val,GPIOI_MODER_PI);
//将输出设置为推挽输出
val = readl(GPIOI_OTYPER_PI);
val &= ~(0x1 << 0);//将bit0清0,设置为推挽输出
writel(val,GPIOI_OTYPER_PI);
//设置速度
val = readl(GPIOI_OSPEEDR_PI);
val &= ~(0x3 << 0);
val |= (0x3<<0);//设置为超高速
writel(val,GPIOI_OSPEEDR_PI);
//设置上拉下拉
val = readl(GPIOI_PUPDR_PI);
val &= ~(0x3 << 0);
val |= (0x1<<0);//设置为上拉
writel(val,GPIOI_PUPDR_PI);
//将GPIOI默认设置为1,关闭状态
val = readl(GPIOI_BSRR_PI);
val &= ~(0x1 << 0);
val |= (0x1<<0);//设置为1
writel(val,GPIOI_BSRR_PI);
//注册字符
//构造设备号
if(dtsled.major)//定义了主设备号
{
dtsled.devid = MKDEV(dtsled.major,0);
ret = register_chrdev_region(dtsled.devid,DTSLED_CNT,DTSLED_NAME);
if(ret<0){
printk("dtsled driver register failed!\r\n");
goto fail_map;
}
}else {
ret = alloc_chrdev_region(&dtsled.devid,0,DTSLED_CNT,DTSLED_NAME);//没有定义就从内核申请
if(ret<0){
printk("dtsled driver register failed!\r\n");
goto fail_map;
}
}
//添加字符设备
dtsled.cdev.owner = THIS_MODULE;
cdev_init(&dtsled.cdev,&led_fops);//初始化
ret = cdev_add(&dtsled.cdev,dtsled.devid,DTSLED_CNT);
if(ret<0){
goto del_unregister;
}
//自动创建设备节点
dtsled.class = class_create(THIS_MODULE, DTSLED_NAME);
if (IS_ERR(dtsled.class)) {
pr_err("QAT: class_create failed for adf_ctl\n");
goto del_unregister;
}
//创建设备
dtsled.device = device_create(dtsled.class, NULL,
dtsled.devid,
NULL, DTSLED_NAME);
if (IS_ERR( dtsled.device)) {
pr_err("QAT: failed to create device\n");
goto err_cdev_del;
}
printk("led_init\r\n");
return 0;
fail_findnd:
device_destroy(dtsled.class,dtsled.devid);
err_cdev_del:
cdev_del(&dtsled.cdev);
del_unregister:
unregister_chrdev_region(dtsled.devid, DTSLED_CNT);
fail_map:
led_iounmap();
return -EIO;
}
//出口函数
static void __exit dtsled_fini(void)
{
led_iounmap();
device_destroy(dtsled.class,dtsled.devid);//删掉设备
cdev_del(&dtsled.cdev);//注销字符设备
class_destroy(dtsled.class);//删除设备节点
unregister_chrdev_region(dtsled.devid, DTSLED_CNT);//注销设备号
printk("led_exit\r\n");
}
//注册驱动和卸载驱动
module_init(dtsled_init);
module_exit(dtsled_fini);
// 添加作者信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("dada");
MODULE_INFO(intree, "Y");
测试
应用程序之前已经编写过,因为完全没有变动,所以不需要重新编写,直接上开发板测试。
可以看到,已经成功获取到了设备节点的各个属性,并且成功初始化。
测试一下。
发现可以实现灯的亮灭。
实验成功!!!
继续加油!!!