STM32mp157字符设备实验—设备树下led驱动

使用的开发板为正点原子STM32mp157。

参考资料为正点原子驱动开发教程,以及STM32mp芯片手册等。


之前已经学会了如何直接操作寄存器进行led的驱动开发。

那么在此基础之上,引入设备树进行修改。

回忆以下之前的流程。

  1. 驱动的注册与卸载
  2. 分别编写入口函数和出口函数
  3. 注册函数与卸载函数(重点是file_operations函数)
  4. 地址映射以及led初始化
  5. 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;

我们主要关注opsdev就行。

这个跟之前的类似,重点在于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来手动创建设备节点,这一次我们直接在文件里实现自动创建设备节点。

自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在cdev_add函数后面,首先要创建一个class类,class是个结构体。
观察一下内核文件里的代码。

可以看到跟前面的写法类似。

struct class *class_create (struct module *owner, const char *name)
一共有两个参数,参数 owner 一般为 THIS_MODULE,参数 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");

测试

应用程序之前已经编写过,因为完全没有变动,所以不需要重新编写,直接上开发板测试。

 可以看到,已经成功获取到了设备节点的各个属性,并且成功初始化。

 测试一下。

发现可以实现灯的亮灭。

实验成功!!!

继续加油!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值