从零开始写一个基于Hi3516EV200平台的spi设备驱动框架demo(使用SPI子系统和设备树)

OpenHarmony硬件调试三板斧:基于RK3568的日志、状态与信号实战 嵌入式系统开发中,硬件调试是绕不开的关键环节,无论是工控网关还是智能终端,系统启动异常、外设无响应等问题常让开发者陷入困境。而RK3568作为OpenHarmony生态的典型参考平台,其调试思路与传统嵌入式Linux一脉相承,核心在于建立“现象-原因”的映射链。本文从日志分析、状态查询、信号测量三个递进的维度展开:先借助dmesg与hilog建立全局视野,再通过hdc及sysfs节点核实设备树绑定与驱动状态,最后用万用表与逻辑分析仪验证电气信号。这套方法能覆盖绝大多数硬件问题,尤其适用于设备树选择与调试工具 阅读详情

0.测试环境说明

环境参数
海思芯片型号Hi3516EV200
Kernel Version4.9.37
SDKHi3516EV200_SDK_V1.0.1.1

1.SPI设备和字符设备之间的关系

Linux的三大设备:字符设备、块设备和网络设备对知道Linux的人都不会陌生,在Linux内部,对这些基本的设备类型做了更进一步的封装,正如《Linux设备驱动开发详解》一书中所说,Linux虽然是用C语言写的,但是处处都有面向对象编程的思想。借由这一思想,Linux2.6之后提出了platform设备驱动模型,在此驱动模型中,有总线、设备和驱动三个实体,总线将设备和驱动绑定。

platform设备驱动模型+面向对象思想两者结合在一起会发生什么呢?那就是,对每一类相似的设备,都有一套独立的子系统,比如input子系统、RTC子系统、帧缓冲、终端、misc、MMC、USB、I2C以及本次要说的SPI等等。

所以从本质上说,SPI子系统同属于platform设备驱动模型,在SPI子系统中,涉及4个软件模块:(1)主机端驱动;(2)连接主机和外设的纽带;(3)外设驱动;(4)板级逻辑。

本博文测试环境使用的Kernel版本为4.9.37,并且打了海思官方提供的SDK内核patch,该版本内核使用设备树描述板级逻辑,板级逻辑即描述了SOC内部资源分布和分配。主机端驱动则使用海思默认配置中所选择的SSP-PL022。

PL022

上图为内核菜单配置界面,可以看到,我去掉了“User mode SPI device driver support”一项,该项勾选会在/dev目录下生成可以供应用层操作的spidev0.0、spidev1.0、spidev1.1节点,我没有测试勾选此项后后文的设备驱动挂载测试能否成功,此处声明。

连接主机和外设的纽带是内核的任务,所以我们本文要关注的主要是第三方面——外设驱动的大致架构。

2.字符设备驱动框架

前面说过,SPI设备模型是基于三大基本设备类型扩展的,所以如果想要实现一个字符型SPI设备驱动,首先要实现一个字符型的设备驱动框架。这一类的教程网上很多,此处借用了《Linux设备驱动开发详解》书中提供的架构逻辑,并增加了自动在/dev目录下生成节点的功能。

/* Copyright (c) 2020 Turix.
 *
 * 该程序用于Turix中心(虚拟)的程序学习和实验。
 * 并做程序参考模板。
 *
 */
#include <linux/module.h>//使用模块相关功能
#include <linux/kernel.h>//使用printk
#include <linux/cdev.h>  //提供cdev结构体
#include <linux/fs.h>    //提供字符设备注册函数
#include <linux/slab.h>  //提供kzalloc和kfree
#include <linux/device.h>//提供设备注册相关函数

#define DRIVER_MAJOR 0
#define DEVICE_NUMBER 1

static int device_number=DEVICE_NUMBER;

//****************************************
//[注意]这里要保证编译器将未初始化全局变量
//设置为0
//****************************************
static int driver_major=DRIVER_MAJOR;
//****************************************
//在载入模块的时候,指定参数值(主设备号):
//insmod module.ko driver_major=xxx
//否则自动分配
//****************************************
module_param(driver_major,int,S_IRUGO);

//****************************************
//该结构体用于设置每一个(子)设备的设备参量
//****************************************
struct turix_dev_t{
    struct cdev cdev;
    
};

struct turix_dev_t *pturix_dev_t;

//****************************************
//设置的类用于驱动程序自动生成设备节点
//设备类的结构体类型新旧版本系统不同。
//在旧版本系统为:struct class_device
//在新版本系统为:struct device
//****************************************
static struct class *turixdev_class;
static struct device *turixdev_class_device[DEVICE_NUMBER];

/****************************************
函数名称:turix_open
功能描述:
	设备打开函数。
其它:
****************************************/
static int turix_open(struct inode *inode,struct file *filp)
{
    struct turix_dev_t *dev=container_of(inode->i_cdev,struct turix_dev_t,cdev);
    filp->private_data=dev;
    return 0;
}

/****************************************
函数名称:turix_release
功能描述:
	设备释放(关闭)函数。
其它:
****************************************/
static int turix_release(struct inode *inode,struct file *filp)
{
    return 0;
}

/****************************************
函数名称:turix_read
功能描述:
	设备读函数。
其它:
****************************************/
static ssize_t turix_read(struct file *filp,char __user *buf,size_t count,loff_t *f_pos)
{
    //struct turix_dev_t *dev=filp->private_data;
    
    return 0;
}

/****************************************
函数名称:turix_write
功能描述:
	设备写函数。
其它:
****************************************/
static ssize_t turix_write(struct file *filp,const char __user *buf,size_t count,loff_t *f_pos)
{
    //struct turix_dev_t *dev=filp->private_data;
    
    return 0;
}

/****************************************
函数名称:turix_ioctl
功能描述:
	设备控制函数。
其它:
****************************************/
static long turix_ioctl(struct file *filp,unsigned int cmd,unsigned long arg)
{
    //struct turix_dev_t *dev=filp->private_data;
    switch(cmd)
    {
        default:return -EINVAL;
    }
    
    return 0;
}

static const struct file_operations turix_fops=
{
    .owner=THIS_MODULE,
    .read=turix_read,
    .write=turix_write,
    .unlocked_ioctl=turix_ioctl,
    .open=turix_open,
    .release=turix_release,
};

/****************************************
函数名称:turix_setup
功能描述:
	该函数用于子设备注册。
其它:
****************************************/
static int turix_setup(struct turix_dev_t *dev,int index)
{
    //****************************************
    //为每个子设备生成设备号
    //****************************************
    int err,devno=MKDEV(driver_major,index);
    
    cdev_init(&dev->cdev,&turix_fops);
    dev->cdev.owner=THIS_MODULE;
    
    err=cdev_add(&dev->cdev,devno,1);
    if(err<0)
    {
        printk(KERN_NOTICE "Turix:Add turixdev%d failed with %d!\n",index,err);
        return err;
    }
    
    return 0;
}

/****************************************
函数名称:turix_init
功能描述:
	该函数用于模块整体的初始化。
其它:
****************************************/
static int __init turix_init(void)
{
    int i,ret;
    //****************************************
    //MKDEV(dev_t dev)通过主次设备号生成设备号
    //****************************************
    dev_t devno=MKDEV(driver_major,0);
    
    printk(KERN_INFO "Turix: module init.\n");
    
    if(driver_major)
    {
        //****************************************
        //register_chrdev_region用于向系统注册设备号
        //这里注册的为主设备号,对不同数量的设备,
        //在配置函数turix_setup中设置次设备号。
        //****************************************
        ret=register_chrdev_region(devno,device_number,"turixdev");
    }
    else
    {
        //****************************************
        //alloc_chrdev_region用于向系统申请设备号
        //****************************************
        ret=alloc_chrdev_region(&devno,0,device_number,"turixdev");
        //****************************************
        //MAJOR(dev_t dev)宏用于提取主设备号
        //****************************************
        driver_major=MAJOR(devno);
    }
    if(ret<0)
    {
        goto FAIL;
    }
    printk(KERN_INFO "Turix: turixdev devno_major:%d.\n",driver_major);
    printk(KERN_INFO "Turix: The devices number is %d.\n",device_number);
    
    //****************************************
    //此处为每个设备申请的结构体会分别被每个
    //设备调用,其占用的内存在模块存在期间始终
    //存在,模块卸载时被释放。
    //****************************************
    pturix_dev_t=kzalloc(sizeof(struct turix_dev_t)*device_number,GFP_KERNEL);
    if(!pturix_dev_t)
    {
        ret=-ENOMEM;
        //****************************************
        //当内存申请不足时,异常退出。
        //****************************************
        unregister_chrdev_region(devno,device_number);
        goto FAIL;
    }
    
    //****************************************
    //创建类设备,会在sys/class/turixdev_class
    //类下创建设备,udev通过这个自动创建
    ///dev/turixdev这个设备节点。
    //****************************************
    turixdev_class=class_create(THIS_MODULE,"turixclass");
    
    /*运行子设备注册函数*/
    for(i=0;i<device_number;i++)
    {
        if(turix_setup(pturix_dev_t+i,i)==0)
        {
            turixdev_class_device[i]=device_create(turixdev_class,NULL,MKDEV(driver_major,i),NULL,"turixdev%d",i);
            printk(KERN_INFO "Turix: Add turixdev%d for major:%d-sub:%d.\n",i,driver_major,i);
        }
    }
    
	return 0;
FAIL:
    return ret;
}
module_init(turix_init);

/****************************************
函数名称:turix_exit
功能描述:
	该函数用于模块整体的退出操作。
其它:
****************************************/
static void __exit turix_exit(void)
{
    int i;
    
    for(i=0;i<device_number;i++)
    {
        cdev_del(&(pturix_dev_t+i)->cdev);
        device_unregister(turixdev_class_device[i]);
        printk(KERN_INFO "Turix: Remove turixdev%d for major:%d-sub:%d.\n",i,driver_major,i);
    }
    class_destroy(turixdev_class);
    
    kfree(pturix_dev_t);
    unregister_chrdev_region(MKDEV(driver_major,0),1);
    
    printk(KERN_INFO "Turix: module unload.\n");
}
module_exit(turix_exit);

MODULE_AUTHOR("Turix");
MODULE_DESCRIPTION("Turix test driver.");
MODULE_LICENSE("GPL");

这个框架看起来如此简单,字符设备驱动模型理解起来的话,除了设备的注册和注销、注册设备号以及节点生成之外,就只剩下下面的文件操作结构体了:

static const struct file_operations turix_fops=
{
.owner=THIS_MODULE,
.read=turix_read,
.write=turix_write,
.unlocked_ioctl=turix_ioctl,
.open=turix_open,
.release=turix_release,
};

它们的大致实现函数在上面的程序中已经有了,但是并没有对读写做更多的操作,因为这些读写我们并不打算使用GPIO进行模拟。即便如此,上面的程序也可以进行编译和挂载,Makefile为:

CC = arm-himix100-linux-gcc
obj-m:=test.o

CURRENT_PATH:=$(shell pwd)

LINUX_KERNEL_PATH:=/home/wind/Documents/osdrv/opensource/kernel/linux-4.9.y
#complie object
default:
	make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
clean:
	make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean
endif

其中,CC为交叉编译工具,test.o为对应于测试文件名的输出文件,LINUX_KERNEL_PATH为目标内核的源码目录,需要根据需要修改。挂载之后的现象是,在/dev下出现turixdev0的设备。

字符设备示例

3.SPI字符设备驱动框架

3.1SPI设备的struct driver结构体

该结构体在文件 linux源码/include/linux/spi/spi.h 文件中定义:

/**
 * struct spi_driver-主机端“协议”驱动程序
 * @id_table:该驱动程序支持的SPI设备列表
 * @probe: 将此驱动程序绑定到spi设备。 
 * 驱动程序可以验证设备是否确实存在,并且可能需要配置
 * 系统设置过程中进行初始配置时不需要的特征(例如bits_per_word)。
 * @remove:将此驱动程序与spi设备解除绑定
 * @shutdown:在系统状态转换期间使用的标准关闭回调,例如powerdown / halt和kexec
 * @suspend:系统状态转换期间使用的标准暂停回调
 * @resume:系统状态转换期间使用的标准恢复回调
 * @driver:SPI设备驱动程序应初始化此结构的名称和所有者字段。
 *
 * 这表示使用SPI消息与SPI链路另一端的硬件进行交互的设备驱动程序。 
 * 之所以称为“协议”驱动程序,是因为它通过消息工作,
 * 而不是直接与SPI硬件通信(这是底层SPI控制器
 * 驱动程序用来传递这些消息的方式)。
 * 这些协议在驱动程序支持的设备的规范中定义。
 *
 * 通常,这些设备协议代表驱动程序支持的最低级别的接口,
 * 它也将支持较高级别的接口。这种高层的示例包括诸如MTD,
 * 网络,MMC,RTC,文件系统字符设备节点和硬件监视之类的框架。
 */
struct spi_driver {
	const struct spi_device_id *id_table;
	int			(*probe)   (struct spi_device *spi);
	int			(*remove)  (struct spi_device *spi);
	void		(*shutdown)(struct spi_device *spi);
	int			(*suspend) (struct spi_device *spi, pm_message_t mesg);
	int			(*resume)  (struct spi_device *spi);
	struct device_driver	driver;
};

probe为主机端和外设匹配成功之后执行的操作,remove则是设备移除后的操作,其它的回调函数和PM有关,暂不考虑。

这样一来,对字符设备的改动应该在以下几个方面:

1.设备的注册和节点的生成应该放在probe中,因为只有外设和主机都存在且匹配的时候,设备节点的操作才有意义且合法。

2.因为所要编写的SPI设备(其实我本意是想写2401的驱动的,基于spi而非gpio模拟的,但是未完成之前先整理一下才写的博文)并不具备热插拔检测能力,所以匹配的操作在模块挂载时完成,而移除的操作由模块卸载函数进行。

3.为了指定设备绑在那个SPI上,需要修改设备树。

3.2设备树的修改

设备树的修改

修改对应于板的dts/dtsi文件,(dtsi被包含在dts中,只修改dts即可,重复设置的项会被新设置覆盖),在compatible中添加一项,这个项目将在后续驱动尝试匹配时发挥作用。

当然,SPI设备的match并不只有这一种操作。比如在参考2中,作者使用spi_new_device动态增加一个SPI设备(但是我在使用的时候,他的操作没有复现成功,device和ssp-pl022冲突,并且dts中删除spibus1之后也不成,不排除我操作不对,但还是…心累)。此处把修改该作者使用的device端板信息注册模块代码放上,有兴趣测试的自取:

#include <linux/kernel.h> //提供printk
#include <linux/module.h> //使用模块相关功能
#include <linux/spi/spi.h>//提供SPI设备模型API

#define DEVICENAME "turixSPIdev"
#define SPIBUS 1

static struct spi_board_info dev_info =
{
	.modalias        = DEVICENAME,  //初始化设备的名称
	.platform_data   = NULL,        //平台文件信息
	.max_speed_hz    = 10*1000*1000,//初始化传输速率
	.bus_num         = SPIBUS,      //控制器编号
	.chip_select     = 1,           //控制器片选的编号
	.mode            = SPI_MODE_0,  //spi的模式
	.controller_data = NULL,
};
static struct spi_device* turix_spi_device; 

static int __init turix_init(void)
{
	struct spi_master* turix_spi_master;
	turix_spi_master=spi_busnum_to_master(dev_info.bus_num);
	turix_spi_device=spi_new_device(turix_spi_master,&dev_info);
    return 0;
}

static void __exit turix_exit(void)
{ 
    spi_unregister_device(turix_spi_device);
}

module_init(turix_init);
module_exit(turix_exit);

MODULE_AUTHOR("Turix");
MODULE_DESCRIPTION("Turix test driver.");
MODULE_LICENSE("GPL");

设备树的编译跟随内核进行,会生成dtb二进制文件并写入uImage,在内核启动时加载。

编译设备树

上图中的内核是经过少量再裁剪的,官方默认deconfig配置生成的大小约3.4MB,所以有差池正常。

3.2SPI设备的注册

和字符设备填充struct file_operations结构体相似,SPI设备要填充上面说到的struct spi_driver结构体:

#define DEVICENAME "turixSPIdev"
//****************************************
//此处的匹配要配套设备树中进行相应的修改
//****************************************
static const struct of_device_id spi_id[] = {
    { .compatible = "arm,turixSPIdev", .data = NULL },
    {}
};

static struct spi_driver turix_spi_driver =
{
    .driver =
    {
        .name  = DEVICENAME,
        .owner = THIS_MODULE,
        .of_match_table = spi_id,
    },
    .probe     = turix_spi_probe,
    .remove    = turix_spi_remove,
};

可以看到,struct of_device_id结构体中的信息正是之前我们在设备树中添加的项,这样两者一致就可以成功匹配进而执行probe函数注册设备了。按照3.1节所说,设备的注册和节点的生成应该放在probe中,这样一来模块挂载中要进行的操作就剩下spi设备的注册了,具体的匹配交给SPI子系统去做,挂载卸载函数为:

static int __init turix_init(void)
{
    spi_register_driver(&turix_spi_driver);
    return 0;
}
module_init(turix_init);

static void __exit turix_exit(void)
{
    /*先进行设备的注销、节点的删除操作*/
    spi_unregister_driver(&turix_spi_driver);
}
module_exit(turix_exit);

其中turix_spi_driver结构体即上面的struct spi_driver结构体。

3.4驱动框架demo

整个驱动程序如下, 其中,write/read函数未实现 (因为作者写此博文的时候作为当天工作总结,剩下的坑来日再踩):

/* Copyright (c) 2020 Turix.
 *
 * 该程序用于Turix中心(虚拟)的SPI设备驱动程序学习和实验。
 * 并做程序参考模板。
 *
 */

#include <linux/module.h>//使用模块相关功能
#include <linux/kernel.h>//使用printk
#include <linux/cdev.h>  //提供cdev结构体
#include <linux/fs.h>    //提供字符设备注册函数
#include <linux/slab.h>  //提供kzalloc和kfree
#include <linux/device.h>//提供设备注册相关函数
#include <linux/spi/spi.h>//提供SPI设备模型API

#define DEVICENAME "turixSPIdev"
#define DEVICE_NUMBER 1
static int device_number=DEVICE_NUMBER;

#define DRIVER_MAJOR 0
//****************************************
//[注意]这里要保证编译器将未初始化全局变量
//设置为0
//****************************************
static int driver_major=DRIVER_MAJOR;
//****************************************
//在载入模块的时候,指定参数值(主设备号):
//insmod module.ko driver_major=xxx
//否则自动分配
//****************************************
module_param(driver_major,int,S_IRUGO);

//****************************************
//该结构体用于设置每一个(子)设备的设备参量
//****************************************
struct turix_dev_t{
    struct cdev cdev;
    
};
struct turix_dev_t *pturix_dev_t;

//****************************************
//设置的类用于驱动程序自动生成设备节点
//设备类的结构体类型新旧版本系统不同。
//在旧版本系统为:struct class_device
//在新版本系统为:struct device
//****************************************
static struct class *turixdev_class;
static struct device *turixdev_class_device[DEVICE_NUMBER];

/****************************************
函数名称:turix_open
功能描述:
    设备打开函数。
其它:
****************************************/
static int turix_open(struct inode *inode,struct file *filp)
{
    struct turix_dev_t *dev=container_of(inode->i_cdev,struct turix_dev_t,cdev);
    filp->private_data=dev;
    printk(KERN_INFO "Turix: SPI Device open.\n");
    return 0;
}

/****************************************
函数名称:turix_release
功能描述:
    设备释放(关闭)函数。
其它:
****************************************/
static int turix_release(struct inode *inode,struct file *filp)
{
    printk(KERN_INFO "Turix: SPI Device release.\n");
    return 0;
}

/****************************************
函数名称:turix_read
功能描述:
    设备读函数。
其它:
****************************************/
static ssize_t turix_read(struct file *filp,char __user *buf,size_t count,loff_t *f_pos)
{
    //struct turix_dev_t *dev=filp->private_data;
    printk(KERN_INFO "Turix: Perform a read operation.\n");
    return 0;
}

/****************************************
函数名称:turix_write
功能描述:
    设备写函数。
其它:
****************************************/
static ssize_t turix_write(struct file *filp,const char __user *buf,size_t count,loff_t *f_pos)
{
    //struct turix_dev_t *dev=filp->private_data;
    printk(KERN_INFO "Turix: Perform a write operation.\n");
    return 0;
}

/****************************************
函数名称:turix_ioctl
功能描述:
    设备控制函数。
其它:
****************************************/
static long turix_ioctl(struct file *filp,unsigned int cmd,unsigned long arg)
{
    //struct turix_dev_t *dev=filp->private_data;
    switch(cmd)
    {
        default:return -EINVAL;
    }
    
    return 0;
}

//****************************************
//字符设备操作
//****************************************
static const struct file_operations turix_fops=
{
    .owner         =THIS_MODULE,
    .read          =turix_read,
    .write         =turix_write,
    .unlocked_ioctl=turix_ioctl,
    .open          =turix_open,
    .release       =turix_release,
};

/****************************************
函数名称:turix_setup
功能描述:
	该函数用于子设备注册。
其它:
****************************************/
static int turix_setup(struct turix_dev_t *dev,int index)
{
    //****************************************
    //为每个子设备生成设备号
    //****************************************
    int err,devno=MKDEV(driver_major,index);
    
    cdev_init(&dev->cdev,&turix_fops);
    dev->cdev.owner=THIS_MODULE;
    
    err=cdev_add(&dev->cdev,devno,1);
    if(err<0)
    {
        printk(KERN_NOTICE "Turix:Add node turixdev%d failed with %d!\n",index,err);
        return err;
    }
    
    return 0;
}

//****************************************
//probe执行标志,用于执行失败时模块可以正常卸载
//****************************************
static int flag_devprobed=0;
/****************************************
函数名称:turix_spi_probe
功能描述:
    设备挂载函数。
其它:
****************************************/
int turix_spi_probe(struct spi_device *spi)
{
    int i,ret;
    //****************************************
    //MKDEV(dev_t dev)通过主次设备号生成设备号
    //****************************************
    dev_t devno=MKDEV(driver_major,0);
    
    printk(KERN_INFO "Turix: SPI Device probe.\n");
    
    if(driver_major)
    {
        //****************************************
        //register_chrdev_region用于向系统注册设备号
        //这里注册的为主设备号,对不同数量的设备,
        //在配置函数turix_setup中设置次设备号。
        //****************************************
        ret=register_chrdev_region(devno,device_number,"turixdev");
    }
    else
    {
        //****************************************
        //alloc_chrdev_region用于向系统申请设备号
        //****************************************
        ret=alloc_chrdev_region(&devno,0,device_number,"turixdev");
        //****************************************
        //MAJOR(dev_t dev)宏用于提取主设备号
        //****************************************
        driver_major=MAJOR(devno);
    }
    if(ret<0)
    {
        goto FAIL;
    }
    printk(KERN_INFO "Turix: turixdev devno_major:%d.\n",driver_major);
    printk(KERN_INFO "Turix: The devices number is %d.\n",device_number);
    
    //****************************************
    //此处为每个设备申请的结构体会分别被每个
    //设备调用,其占用的内存在模块存在期间始终
    //存在,模块卸载时被释放。
    //****************************************
    pturix_dev_t=kzalloc(sizeof(struct turix_dev_t)*device_number,GFP_KERNEL);
    if(!pturix_dev_t)
    {
        ret=-ENOMEM;
        //****************************************
        //当内存申请不足时,异常退出。
        //****************************************
        unregister_chrdev_region(devno,device_number);
        goto FAIL;
    }
    
    //****************************************
    //创建类设备,会在sys/class/turixdev_class
    //类下创建设备,udev通过这个自动创建
    ///dev/turixdev这个设备节点。
    //****************************************
    turixdev_class=class_create(THIS_MODULE,"turixclass");
    
    /*运行子设备注册函数*/
    for(i=0;i<device_number;i++)
    {
        if(turix_setup(pturix_dev_t+i,i)==0)
        {
            turixdev_class_device[i]=device_create(turixdev_class,NULL,MKDEV(driver_major,i),NULL,"turixdev%d",i);
            printk(KERN_INFO "Turix: Add node turixdev%d for major:%d-sub:%d.\n",i,driver_major,i);
        }
    }
    
    flag_devprobed=1;
	return 0;
FAIL:
    printk(KERN_INFO "Turix: SPI Device probe failed.\n");
    return ret;
}

/****************************************
函数名称:turix_spi_remove
功能描述:
    设备卸载函数。
其它:
****************************************/
int turix_spi_remove(struct spi_device *spi)
{
    printk(KERN_INFO "Turix: SPI Device remove.\n");
    return 0;
}

//****************************************
//此处的匹配要配套设备树中进行相应的修改
//****************************************
static const struct of_device_id spi_id[] = {
    { .compatible = "arm,turixSPIdev", .data = NULL },
    {}
};

static struct spi_driver turix_spi_driver =
{
    .driver =
    {
        .name  = DEVICENAME,
        .owner = THIS_MODULE,
        .of_match_table = spi_id,
    },
    .probe     = turix_spi_probe,
    .remove    = turix_spi_remove,
};

/****************************************
函数名称:turix_init
功能描述:
    该函数用于模块整体的初始化。
其它:
****************************************/
static int __init turix_init(void)
{
    printk(KERN_INFO "Turix: module init.\n");
    spi_register_driver(&turix_spi_driver);
    
    return 0;
}
module_init(turix_init);

/****************************************
函数名称:turix_exit
功能描述:
    该函数用于模块整体的退出操作。
其它:
****************************************/
static void __exit turix_exit(void)
{
    int i;
    
    if(flag_devprobed==1)
    {
        for(i=0;i<device_number;i++)
        {
            cdev_del(&(pturix_dev_t+i)->cdev);
            device_unregister(turixdev_class_device[i]);
            printk(KERN_INFO "Turix: Remove turixdev%d for major:%d-sub:%d.\n",i,driver_major,i);
        }
        class_destroy(turixdev_class);
        
        kfree(pturix_dev_t);
        unregister_chrdev_region(MKDEV(driver_major,0),1);
        
    }
    spi_unregister_driver(&turix_spi_driver);
    
    printk(KERN_INFO "Turix: module unload.\n");
}
module_exit(turix_exit);

MODULE_AUTHOR("Turix");
MODULE_DESCRIPTION("Turix SPI test driver.");
MODULE_LICENSE("GPL");

模块编译Makefile为:

CC = arm-himix100-linux-gcc

obj-m:=spidriver.o
CURRENT_PATH:=$(shell pwd)

LINUX_KERNEL_PATH:=/home/wind/Documents/osdrv/opensource/kernel/linux-4.9.y
#complie object
default:
	make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
clean:
	make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean
endif

其中,CC为交叉编译工具,test.o为对应于测试文件名的输出文件,LINUX_KERNEL_PATH为目标内核的源码目录,需要根据需要修改。

3.5驱动框架验证

模块加载、节点查看、读取及卸载如图所示:

SPI设备驱动框架验证

在上图中,首先进行加载操作,可以看到,模块匹配后执行到probe,从系统申请的字符设备主设备号为252,设备数量为1null那里是我程序中有点瑕疵,传到博文的版本没有此问题。

使用 lsmod 可列出加载的驱动,同时在 /dev 目录下可以找到生成的设备节点 turixdev0 ,在 cd /sys/bus/spi/drivers/ 目录中有驱动匹配到的驱动名。

cat 操作可以对设备进行一次读操作,从打印信息来看,其依次执行了open、read、release操作。

最后模块卸载时会自动删除节点。

再次说明, 驱动中的write/read函数未实现 ,这是由于作者 太累了 想偷懒所致。

如果博文对您起到了哪怕一点点的帮助,可以给个赞嘛,ღ( ´・ᴗ・` )比心。

参考

1.《Linux设备驱动开发详解》宋宝华 著

2.Linux SPI 字符设备 驱动例子

3.AM335X——SPI设备驱动

————2020-5-13 @燕卫博————

工控Linux系统OverlayFS恢复出厂设计实战 在嵌入式Linux系统中,‘恢复出厂’并非简单重置,而是涉及文件系统架构、存储分区策略与容错机制的综合工程。OverlayFS作为核心技术,通过可写层(upperdir)与只读层(lowerdir)叠加,实现运行时配置隔离与原子化回滚,显著提升系统健壮性。结合squashfs只读镜像、A/B双分区升级及persistent持久化设计,可支撑工业场景下的无人值守、断电不丢数据、OTA安全回退等关键需求。本文聚焦工控单板落地实践,详解OverlayFS启用条件、分区规划、内核编译要点及三级恢复机制,覆盖i.MX 阅读详情

相关推荐

端侧AI开发板实战:从YOLOv5部署到硬件调试与软件开发

边缘AI设备正在从算力验证走向真实业务落地,而目标检测模型的端侧部署是其中最常见也最复杂的工程实践。嵌入式平台受限于内存与算力,通常需要借助NPU完成高效的神经网络推理,将训练好的YOLOv5模型经ONNX导出、算子适配与INT8量化,以压缩模型体积并提升推理速度。交叉编译环境的搭建、硬件调试与多线程软件开发,则进一步决定了这一系统能否稳定运行。该技术广泛应用于实时视频分析、智能安防、工业质检等场景。本文以Microduck-HD1910开发板为载体,完整梳理从模型转换、板端推理到摄像头接入、结果可视化的全

weixin_34367257的博客 345

从零开始创建Hi3516DV300在EMMC上的根文件系统

目录Hi3516DV300及测试环境介绍创建步骤1.busybox的编译注意事项本文共享资源 Hi3516DV300及测试环境介绍 海思Hi3516DV300使用arm-himix200-linux-工具链,内含双核 ARM Cortex-A7。 本文使用的测试硬件为 荣品Hi3516DV300开发板 ,淘宝有售,此处非广告。使用的SDK版本为 Hi3516CV500_SDK_V2.0.1.0 。...

Turix附属空间[已停用] 4331

海思hi3516ev200引脚GPIO直接输出高低电平

首先使用到海思自带寄存器配置工具devmem,本次操作的引脚为GPIO6_7。 1、查找引脚对应的寄存器,默认为普通引脚,本次不需要修改。 iocfg_reg41 12 Pin LCD_DATA3 IO Config Register. 0x112C0068 0x1000 31:11 保留。 10 电平转换速率控制,为0时电平转换速率快,为1时电平转换速率慢。 9 下拉电阻使能,高有效。 8 上拉电阻使能,高有效。 7:4 管脚

Sweets_JIE的博客 2356

海思3519Ademo-SAMPLE_VENC_4K30程序框架梳理

SAMPLE_VENC_4K30梳理 我用xmind对程序流程做了一个demo的梳理,传文件上来不太方便,我就导出成图片传上来吧。 我对海思的以及图像开发的入门就是从这里开始的,只要把上面的这些函数里面的东西搞懂,开发海思的图像项目基本上是没有什么特别的问题的了。 有问题可以相互讨论,我也是小白,微信 yucaohua001 ...

嵌入式小小白 416

海思3516DV300使用mipi_tx驱动st7701s屏幕显示

海思3516DV300使用MIPI_TX 驱动ST7701S

洱东 4705

海思平台gpio模拟spi驱动总结

1. 海思平台gpio如何映射到编号 #define GPIO_TO_PIN(bank, gpio) (8 * (bank) + (gpio)) 海思gpio引脚每组总是从0~7,而且引脚从头都有唯一编号;通过上面公式转换到对应编号; 2. spi引脚定义 引脚对应硬件原理图; //#define GPIO_TO_PIN(bank, gpio) (8 * (bank) + (gpio)) #include <linux/io.h> #include <asm/io.h> #de

qq_39048131的博客 4001

1.3寸SPI通讯TFT LCD显示屏(驱动笔记)

LCD的驱动为ST7789V,采用SPI单工通讯,只写不读,通过DC引脚来选择数据还是寄存器。

Lingtao_Chen的博客 1万+

Vivado静态时序分析学习笔记(1)

静态时序分析分为片内时序分析(寄存器与寄存器之间的时序关系)和片外时序分析(IO时序分析)。(1)建立时间 setup time在时钟事件(上升沿/下降沿)发生之前数据信号应保持稳定的时间。把实际电路抽象成前级D触发器+组合逻辑+后级D触发器。

Fantaasky的博客 4375

设备树学习——初窥设备树

一、设备树是什么 (1)device tree,设备树,是一种linux内核中采用的参数表示和传递技术 (2)dts,device tree source,设备树源文件 (3)dtc,device tree compiler,设备树编译/反编译/调试工具 (4)dtb,device tree blob,二进制设备树镜像 二、为什么需要设备树 (1)典型需求就是内核设备驱动,我们以驱动为例分析,但是不止可以用于驱动场景 (2)驱动源码包括2大部分:策略(操作函数)和数据(硬件信息) (3)早期解决方案:策略和

小嵌同学的博客 831

【实战解析】RTL8306MB交换芯片驱动开发中的RMII接口调试陷阱

本文通过一个RTL8306MB交换芯片驱动开发的实战案例,深入剖析了RMII接口调试中的经典陷阱。文章详细描述了因TX_EN与CRS_DV信号线交叉连接错误导致的“静默”故障,并分享了使用逻辑分析仪和示波器进行硬件排查的完整流程与核心技巧,为嵌入式网络驱动开发提供了宝贵的避坑指南。

weixin_29267307的博客 421

达梦数据守护集群监视器详解与应用指南

主库/备库:承担读写和只读服务的数据节点Redo 日志:记录数据库变更的核心日志Redo 日志传输:主备库间的日志同步机制Redo 日志重演:备库重做日志的应用过程守护进程 (dmwatcher):实例级别的监控和管理组件监视器 (dmmonitor):集群级别的监控和管理工具监视器作为数据守护系统的重要组成部分,提供了集群监控、故障管理和状态确认等关键功能。达梦数据守护集群监视器是维护数据库高可用性的关键工具。

huanhuanzhou blog 1058

在hi3516平台上使用i2C控制tmp117

在hi3516平台上使用i2C控制tmp117

qq_32012843的博客 791

jlink v9刷固件修改序列号来修复弹窗

jlink v9弹窗警告修复j

weixin_40500627的博客 9363

[HarmonyOS] HarmonyOS LiteOS-A 设备开发全流程指南

本文提供HarmonyOS LiteOS-A设备开发的完整流程指南,涵盖从环境搭建到量产的各个环节。首先介绍LiteOS-A作为HarmonyOS轻量级内核的定位,适用于带MMU的IoT设备(≥200MHz主控)。接着详细说明开发环境准备,包括硬件选型、源码获取、工具链配置等,并给出5步快速搭建方法。重点解析新板子适配的V模型流程,从需求分析、设备树编写、内核剪裁到驱动开发、系统服务部署和验证测试。最后分享量产注意事项和性能优化技巧,如安全启动、OTA升级方案等,并推荐官方文档和社区资源作为参考。

极客不孤独的博客 1977

SAP 对接CBS(http方式)

CLEAR:lv_xml_crc32_string. CONCATENATEcrc32_passwordcrc32_keyi_stringINTOlv_xml_crc32_string. CONDENSElv_xml_crc32_string. CALLFUNCTION'SCMS_STRING_TO_XSTRING' EXPORTING text=lv_xml_crc32_string encoding='8400' "GB...

3597

解决Home Assistant中安装的Advanced SSH & Web Terminal插件打开提示“502 Bad Gateway” 且弹窗提示“加载项似乎尚未准备就绪,它可能仍在启动中“问题

  在HomeAssistant的应用商店安装了【AdvancedSSH&WebTerminal】插件,但是打开这个终端连接到HomeAssistant的时候却提示“502BadGateway”且弹窗提示“加载项似乎尚未准备就绪,它可能仍在启动中,您想重试吗?”一直无法进入HomeAssistant的系统中

提供C#相关开发 、云计算、运维测试、网络安全相关技术分享与服务,有意向可联系。 2826

OpenHarmony I2C通信实战:从物理层排障到HDF驱动全链路解析

I2C总线是一种多主从、两线制、带地址寻址的同步串行通信协议,其核心在于起始/停止信号、7位设备地址与应答机制构成的可靠事务模型。理解SCL/SDA电平特性、上拉电阻选型与总线电容限制,是保障100kHz/400kHz稳定通信的物理基础;而OpenHarmony通过HDF框架抽象I2C控制器,依赖设备树(DTS)精准描述硬件资源,并要求应用层严格遵循多消息组合(如先写地址后读数据)完成寄存器访问。本文聚焦I2C在OpenHarmony嵌入式开发中的真实落地场景——涵盖SHT30温湿度传感器、GT911触摸芯

weixin_33884611的博客 482

uniapp(全端兼容) - 超详细接入百度统计功能,给uniapp全平台各端项目引入添加百度统计的插件统计流量详细的步骤教程(提供最新的百度统计源代码,详细的示例代码复制粘贴即可快速搞定!)

uniapp h5 app 使用加入接入百度统计,uniapp百度统计,uniapp详细引入百度统计的教程,uniapp接入百度统计,uniapp如何实现加入百度的统计代码,uniapp怎么植入百度统计详细教程,uniapp如何对接百度统计,uniapp百度统计不生效怎么办,uniapp使用百度统计不生效常见问题,uniapp引入百度统计后没有生效,uniapp h5网页接入百度统计代码后没有统计无效,uniapp最新接入使用百度统计功能,uniapp增加百度统计代码,uniapp app如何接入百度统计,

✅ 精选推荐文章 1万+

【海思篇】【Hi3516DV300】八、手撸一个海思SPI驱动

目的:使用SPI接口让LCD绘制LOGO;让更多的爱好者了解海思、加入海思。

cocoron的博客 6529

低功耗设计:如何计算power switch的数量?

power switch加多了漏电高,加少了ir drop会有问题,具体加多少合适要如何计算呢?这里可以给一个参考方式。 第一步,先选switch cell的类型 通常选用HVT类型的switch cell,优点是漏电更低,缺点也很明显,电阻大,压降也大,switch cell的大小选择,可以选择(开启电阻x面积)最小的switch cell,即单位面积提供最小并联电阻的cell类型。 第二步,计算数量 选好类型,确定了开启电阻,可以按照如下公式计算数量: Ron/Xnum • W/V = V•P

m0_61544122的博客 3570

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

51-20210316华为海思Hi3516DV300的linux系统编译1(SPI模式)

51-20210316华为海思Hi3516DV300的linux系统编译 2021/3/16 10:42 https://xueqiu.com/7970718062/159110439 官井想开挖掘机 来自iPhone发布于2020-09-13 15:18 $润和软件(SZ300339)$ HiSpark AI Camera套件 l 支持鸿蒙OS、LiteOS、Linux系统,方便进行产品的原型验证和快速开发 l 板载海思Hi3516DV300芯片,内置双核Cortex-A7,最高运行评率900M.

南岭笑笑生之家 3599

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

海思Hi3516EV200开发第三天

摄像模组测试例程

HappyPerry的博客 1285

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

2021-06-22 hi3516ev200 按键输入 gpio_key input 驱动 device tree 设备树方式

1.在内核文件目录下搜索关于HI3516EV200 的dts 文件: arch/arm/boot/dts/ hi3516ev200-demb.dtb hi3516ev200-demb.dts hi3516ev200.dtsi 有一个*.dtb文件明显设备用到了device tree 2.添加gpio6.4 在hi3516ev200-demb.dts 文件中 20 /dts-v1/; 21 #include "hi3516ev200.dtsi" 22 23 / { 24 mod

weixin_41740391的博客 757

Hi3516EV200 liteOs SDK搭建

一、操作环境 PC:win10 虚拟机:Ubuntu14.04 LTS 64bit 板端:DOPI 3516EV200 (Hi3516EV200)NAND 128MB 需要liteOs SDK(Hi3516EV200_SDK_V5.0.1.1.tgz)、以及海思的开发文档。 二、在linux环境下解压sdk tar -zxf Hi3516EV200_SDK_Vx.x.x.x.tgz 解压完毕后会有package 、scripts两个文件夹以及两个脚本:sdk.cleanup 、sdk.unp

nswcszmdfj的博客 3731

海思RTSP推流第二篇——海思平台和H264编码Demo

记得行内一个老师说过,基础不牢,地动山摇,对于海思的编码过程还是不是很熟悉所以回头把这个分析一遍。海思平台:官方手册:HiMPP IPC V2.0 媒体处理软件开发参考,里面有介绍海思IPC平台的架构和框架;这里贴几个图具体还得查看文档。Hi35xx 典型的系统层次图:海思媒体处理平台架构:分为好几个模块,视频输入( VI)、视频处理( VPSS)、视频编码( VENC)、视频解码( VDEC)、视频输出(VO)、视频侦测分。

hwf1877655的博客 2563

海思Hi3559(八)——简单Demo测试

参考资料 网址/文档名 1 https://blog.youkuaiyun.com/dong_beijing/article/details/96480219 测试前需先打开虚拟机,将网线、串口与电脑连接好通过Xshell6进行nfs挂载。将Hi3559AV100_SDK_V2.0.3.1目录下的mpp文件夹复制到nfs共享文件夹中。 本次将对mpp/sample目录下的snap进行测试。 1、编译snap 在虚拟机端进入共享文件夹下mpp/sample/snap,执行make命令,进行编译。 2...

qq_38108312的博客 2386
上一篇: 关于IRCUT切换的操作步骤
下一篇: 从零开始学习linux的I2C设备驱动框架——写一个简单的SHT20驱动
燕卫博
博客等级 码龄7年 312粉丝 49原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值