【基于Linux系统设备树的SPI驱动编写方法】


前言

在Linux系统(ubuntu18.04)下,基于设备树(device tree)而不是platform总线(驱动开发分platform总线与设备树),编写SPI驱动

  随着工作的需要,提升底层技术也越来越重要,很多人都开启了驱动学习,本文就介绍了SPI驱动学习的基础内容。


一、SPI驱动编写

  总体分为两个步骤修改设备树 + 编写驱动

1、修改设备树

 a、设备树文件是什么?

  用来描述硬件信息,硬件有哪些资源。
  驱动开发分platform总线与设备树,本文在这里介绍的是基于设备树的spi驱动编写方法,那么可能就有人要问了,这两种驱动开发的有什么区别?

  我们知道:Linux驱动 = 驱动框架 + 硬件操作

  在platform总线下,需要编写指定硬件描述信息(platform_device)的资源文件xxx_dev.c,编写驱动程序(platform_driver)的驱动文件xxx_drv.c

  在设备树下,资源用设备树指定,只需要编写驱动程序的驱动文件xxx_drv.c

  简而言之,设备树文件包含了硬件的描述信息。在设备树情况下:不需要一行行写硬件的描述信息即资源文件xxx_dev.c,需要的硬件信息在设备树文件里面找,传说Linux之父因嫌xxx_dev.c文件太多导致Linux臃肿而引入了DTS即由DeviceTree机制来负责传递硬件拓扑和硬件资源信息。

 b、设备树怎么改?

  通过模仿已经写好可以用的,对比我们自己的设备信息来修改

  • 原本的xxx.dts文件:

/dts-v1/;

#include <dt-bindings/input/input.h>
#include "xxx.dtsi"

/ {
	xxxxxx
	xxxxxx
	
	xxx{
		xxxxxx
		......
	}
	......
	
&xxxspix {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_xxxspix>;

    fsl,spi-num-chipselects = <2>;
    cs-gpios = <&gpiox xx GPIO_ACTIVE_LOW>,  <&gpiox xx GPIO_ACTIVE_LOW>;
    status = "okay";
    
    spidev0: spi@0 {
        compatible = "rohm,dh2228fv";
        reg = <0>;
        spi-max-frequency = <5000000>;
    };

    spidev1: spi@1 {
        compatible = "rohm,dh2228fv";
        reg = <1>;
        spi-max-frequency = <5000000>;
    };

};

  • 修改后的xxx.dts文件:
/dts-v1/;

#include <dt-bindings/input/input.h>
#include "xxx.dtsi"

/ {	/* 最外面的大括号是根节点,我们需要在根节点下里面写子节点,第二层大括号中编写 */
	xxxxxx
	xxxxxx
	
	xxx{
		xxxxxx
		......
	}
	......
	
&ecspi1 {	/* 子节点 */
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_ecspi1>;

    fsl,spi-num-chipselects = <2>;
    cs-gpios = <&gpio4 26 GPIO_ACTIVE_LOW>;  //, <&gpio4 24 GPIO_ACTIVE_LOW>;
    status = "okay";	/* 使用子节点固定要写为okay */

    a5: a5@0 {
        compatible = "veb,a5";	/* dev 和 drv 匹配的关键点:name要改写得一样 */
        reg = <0>;
        spi-max-frequency = <2000000>;	        /* 根据spi设备的传输频率改写 */
    };

};

2、编写驱动

  写spi驱动之前想必已经入门简单驱动编写,像hello驱动和led驱动这种类型的驱动编写应该是小菜一碟。繁琐的话不多说,具体spi驱动代码如下


#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 <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/i2c.h>
#include <linux/spi/spi.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include "veba5_reg.h"

#define VEBA5_CNT	1
#define VEBA5_NAME	"a5"

struct veba5_dev {
	dev_t devid;				/* 设备号 	*/
	struct cdev cdev;			/* cdev 	*/
	struct class *class;		/* 类 		*/
	struct device *device;		/* 设备 	*/
	struct device_node	*nd; 	/* 设备节点 */
	int major;					/* 主设备号 */
	void *private_data;			/* 私有数据 */
};

static struct veba5_dev veba5dev;

void printmem( const void* mem, size_t size )
{
	int i;
    printk( "{");
    for( i=0; i!=size; ++i )
        printk( "%02hhX", ((const unsigned char*)mem)[i] );
    printk( "}\n" );
}

static int a5_spi_write(struct veba5_dev *dev, char *cmd, int len)
{    
	struct spi_device *spi = (struct spi_device *)dev->private_data;
	
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	printmem(cmd, DATALEN);

	spi_write(spi, cmd, DATALEN);	/* 写传输:系统自带的,是下面几行代码的封装 */
	//t[0].tx_buf = cmd; 		/* 要发送的数据 */
	//t[0].len = CMD_T_SIZE; 			/* t->len=发送的长度+读取的长度 */
	//spi_message_init(&m);		/* 初始化spi_message */
	//spi_message_add_tail(&t[0], &m);/* 将spi_transfer添加到spi_message队列 */
	//spi_sync(spi, &m);	/* 同步发送 */

	printk("%s %s %d OUT\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

static long veba5_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	int err;
	struct veba5_dev *dev = (struct veba5_dev *)filp->private_data;

	//char data[DATALEN] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x80, 0x40, 0x20, 0x10, 0xf0};
	char tmp_data[DATALEN] = {0};
	
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	
	/* data 和应用层传下来的数据一样,也可以换成结构体 */
	err = copy_from_user(&tmp_data, (const void __user *)arg, DATALEN);
		
	switch (cmd)
	{
		case CMD_INS:
		{				
			a5_spi_write(dev, tmp_data, DATALEN);
			break;
		}
		default:printk("veba5_ioctl cmd error\n");
	}
	printk("%s %s %d veba5_ioctl OUT\n", __FILE__, __FUNCTION__, __LINE__);

	return 0;
}
/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做pr似有ate_data的成员变量
 * 					  一般在open的时候将private_data似有向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int veba5_open(struct inode *inode, struct file *filp)
{
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	filp->private_data = &veba5dev; /* 设置私有数据 */ 

	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t veba5_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
	//struct veba5_dev *dev = (struct veba5_dev *)filp->private_data;
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);

	//err = copy_to_user(buf, data, sizeof(data));
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int veba5_close(struct inode *inode, struct file *filp)
{
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

/* veba5操作函数 */
static struct file_operations veba5_ops = {
	.owner 			= THIS_MODULE,
	.open 			= veba5_open,
	.read 			= veba5_read,
	.release 		= veba5_close,
	.unlocked_ioctl = veba5_ioctl,
};

/*
 * 内部寄存器初始化函数 
 * @param  	: 无
 * @return 	: 无
 */
void veba5_reginit(void)
{
	unsigned int val;
	//unsigned int i;
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	
    CCM_CCGR3                               = ioremap(0x020C4074, 4);
    IOMUXC_SW_MUX_CTL_PAD_CSI_DATA00        = ioremap(0x020E01E4, 4);
	IOMUXC_SW_MUX_CTL_PAD_CSI_DATA01        = ioremap(0x020E01E8, 4);
//	IOMUXC_SW_MUX_CTL_PAD_CSI_DATA02        = ioremap(0x020E01EC, 4);
    GPIO4_GDIR                              = ioremap(GPIO4_BASE_ADD + 0x4, 4);
    GPIO4_DR                                = ioremap(GPIO4_BASE_ADD + 0, 4);
	
	printk("iomap\n\r");
	
 	/* a. 使能GPIO4     */
    *CCM_CCGR3 |= (3<<13);
    
    /* b. 设置GPIO4_IO21-23用于GPIO     */
    val = *IOMUXC_SW_MUX_CTL_PAD_CSI_DATA00;
    val &= ~(0xf);
    val |= (5);
    *IOMUXC_SW_MUX_CTL_PAD_CSI_DATA00 = val;

	val = *IOMUXC_SW_MUX_CTL_PAD_CSI_DATA01;
    val &= ~(0xf);
    val |= (5);
    *IOMUXC_SW_MUX_CTL_PAD_CSI_DATA01 = val;
/*
    val = *IOMUXC_SW_MUX_CTL_PAD_CSI_DATA02;
    val &= ~(0xf);
    val |= (5);
    *IOMUXC_SW_MUX_CTL_PAD_CSI_DATA02 = val;
*/    
    
    /* c. 设置GPIO4_IO21-22作为output引脚       */
	*GPIO4_GDIR &= ~(1<<21);/* 设置GPIO4_IO21  input引脚,  准备读  */ 
    *GPIO4_GDIR |= (1<<22);	/* 设置GPIO4_IO22  output引脚, 准备写  */ 

	/* 配置个引脚 */
	*GPIO4_DR |= (1<<22);/* 设置GPIO4_IO22 , 默认高电平 */
}

 /*
  * @description     : spi驱动的probe函数,当驱动与
  *                    设备匹配以后此函数就会执行
  * @param - client  : spi设备
  * @param - id      : spi设备ID
  * 
  */	
static int veba5_probe(struct spi_device *spi)
{
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	/* 1、构建设备号 */
	if (veba5dev.major) {
		veba5dev.devid = MKDEV(veba5dev.major, 0);
		register_chrdev_region(veba5dev.devid, VEBA5_CNT, VEBA5_NAME);
	} else {
		alloc_chrdev_region(&veba5dev.devid, 0, VEBA5_CNT, VEBA5_NAME);
		veba5dev.major = MAJOR(veba5dev.devid);
	}

	/* 2、注册设备 */
	cdev_init(&veba5dev.cdev, &veba5_ops);
	cdev_add(&veba5dev.cdev, veba5dev.devid, VEBA5_CNT);

	/* 3、创建类 */
	veba5dev.class = class_create(THIS_MODULE, VEBA5_NAME);
	if (IS_ERR(veba5dev.class)) {
		return PTR_ERR(veba5dev.class);
	}

	/* 4、创建设备 */
	veba5dev.device = device_create(veba5dev.class, NULL, veba5dev.devid, NULL, VEBA5_NAME);
	if (IS_ERR(veba5dev.device)) {
		return PTR_ERR(veba5dev.device);
	}

	/*初始化spi_device */
	spi->mode = SPI_MODE_0;// | SPI_LSB_FIRST;	/*MODE0,CPOL=0,CPHA=0*/
	spi_setup(spi);
	veba5dev.private_data = spi; /* 设置私有数据 */
	
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);

	/* 初始化veba5内部寄存器 */
	veba5_reginit();		
	return 0;
}

/*
 * @description     : spi驱动的remove函数,移除spi驱动的时候此函数会执行
 * @param - client 	: spi设备
 * @return          : 0,成功;其他负值,失败
 */
static int veba5_remove(struct spi_device *spi)
{
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	/* 删除设备 */
	cdev_del(&veba5dev.cdev);
	unregister_chrdev_region(veba5dev.devid, VEBA5_CNT);

	/* 注销掉类和设备 */
	device_destroy(veba5dev.class, veba5dev.devid);
	class_destroy(veba5dev.class);
	return 0;
}

///* 传统匹配方式ID列表 */
//static const struct spi_device_id icm20608_id[] = {
//	{"100ask,icm20608", 0},  
//	{}
//};

/* 设备树匹配列表 */
static const struct of_device_id veba5_of_match[] = {
	{ .compatible = "veb,a5" },
	{ },
};

/* SPI驱动结构体 */	
static struct spi_driver veba5_driver = {
	.probe = veba5_probe,
	.remove = veba5_remove,
	.driver = {
			//.owner = THIS_MODULE,
		   	.name = "a5",
		   	.of_match_table = veba5_of_match, 
		   },
	//.id_table = icm20608_id,
};
		   
/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init veba5_init(void)
{
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	return spi_register_driver(&veba5_driver);
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit veba5_exit(void)
{
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	spi_unregister_driver(&veba5_driver);

	iounmap(CCM_CCGR3);
	iounmap(IOMUXC_SW_MUX_CTL_PAD_CSI_DATA00);
	iounmap(IOMUXC_SW_MUX_CTL_PAD_CSI_DATA01);
	//iounmap(IOMUXC_SW_MUX_CTL_PAD_CSI_DATA02);
	iounmap(GPIO4_GDIR);	
	iounmap(GPIO4_DR);
	
	printk("iounmap\n\r");
}

module_init(veba5_init);
module_exit(veba5_exit);
MODULE_LICENSE("GPL");


二、完善和测试

1、编译和应用程序

  • Linux环境:Ubuntu18.04、Linux4.9.88
  • 编译环境: ARCH=arm64、CROSS_COMPILE=aarch64-linux-gnu-
  • 开发板:imx6ull
      这个驱动代码是直接可以拿来用的,加上Makefile文件和应用程序文件挂个连接吧,有需要的请点击下方蓝色跳转哟

Linux设备树SPI驱动的打包文件下载跳转

 a、编译 && 拷贝到开发板命令

make
cp veba5_drv.ko veba5_app ~/nfs_rootfs/xxx_driver/

 b、操作过程截图

编译拷贝到开发板截图

2、加载和运行

 a、加载该驱动命令

	insmod  veba5drv.ko
  • 操作过程截图
    	加载驱动截图

 b、运行应用调用该驱动

	./veba5_app /dev/a5

应用运行截图

3、逻辑分析仪分析SPI的数据传输

  1. 逻辑分析仪分析spi传输的数据
  2. 和发下去的数据对的上,大致是成功了
    分析spi传输的数据波形图

还有一些问题可供思考,欢迎留言评论,我们一起来探讨

  • 问题1:
      设备树目前只改了只支持一个spi设备a5,如果在spi总线上挂载2个spi设备该怎么改设备树呢?
  • 问题2:
      逻辑分析仪采样频率2M是必须和设备树中的spi-max-frequency = <2000000>一样嘛,不一样又会出现什么结果,又代表了什么?

三、总结

  这里对文章进行总结:
  以上就是今天要讲的内容,本文仅仅简单介绍了Linux下SPI驱动的编写,而Linux提供了大量能使我们快速便捷地处理数据的函数和方法。

以上就是本篇博文的全部内容,如果本篇博文对您有帮助,或者有所启发,动动小手为我点个赞👍,让我知道我写的东西并不是一无是处,还是有那么一点作用滴!of course,我也会继续努力,写出更好的文章!!!

  • 13
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值