从SPI协议学习PX4源码

news/2024/7/24 8:03:16 标签: c语言, 无人机

一、SPI类 

SPI类的参数:设备名称,devname设备节点名称,总线,device片选信号线,SPI模式,时钟频率,中断。SPI类继承VDev类。

SPI协议在spi.cpp文件中,涉及到了cdev和device的操作。cdev字符设备是linux系统设备之一。还有块设备,网络设备。cdev是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据。字符设备是面向流的设备,包括键盘,显示屏,串口。linux用户程序通过设备文件来使用驱动程序操作字符设备。cdev与incode的关系:incode成员含有cdev结构体成员的指针。cdev与file_operations的关系:cdev_init()建立cdev与file_operations之间的连接,为字符设备驱动提供接口函数,比如open,read,write等。

字符设备驱动结构cdev介绍 - 知乎 (zhihu.com)

SPI初始化:

连接总线_dev = up_spiinitialize(_bus),取消选择设备使得引脚电平由高变低(取消片选信号),检查设备是否在线(默认在线),初始化cdev(SPI类是基于cdev类的派生类,初始化cdev会创建设备节点)。

SPI传输:

	case LOCK_PREEMPTION: {
			irqstate_t state = irqsave();
			result = _transfer(send, recv, len);
			irqrestore(state);
		}
		break;    
    case LOCK_THREADS:
        SPI_LOCK(_dev, true);
        result = _transfer(send, recv, len);
        SPI_LOCK(_dev, false);
        break;

int SPI::_transfer(uint8_t *send, uint8_t *recv, unsigned len)
{
	SPI_SETFREQUENCY(_dev, _frequency);
	SPI_SETMODE(_dev, _mode);
	SPI_SETBITS(_dev, 8);
	SPI_SELECT(_dev, _device, true);

	/* do the transfer */
	SPI_EXCHANGE(_dev, send, recv, len);

	/* and clean up */
	SPI_SELECT(_dev, _device, false);

	return OK;
}

反复出现的LOCK是用来干什么的?SPI传输时,接收数据是在中断中,这个接口不会锁住总线,可能会扰乱非中断调用者。中断和非中断混合配置的设备要确保合适的互锁。发送和接收至少一个是非空。当lockmode为preemption时,锁住全部;为threads时,为spi_lock,锁住其他的进程。

SPI_EXCHANGE()前后出现的SELECT函数先开后关是为什么? select函数是控制片选信号,表示当前设备被选中或者被释放。
 

二、HMC_5883_SPI类

在HMC5883_spi.cpp文件中,涉及到ioctl函数。ioctl函数在cdev和Device类中也出现了。那么这个函数有什么意义和作用呢?ioctl函数是内核设计着希望将用户空间和内核空间的驱动模块的交互分成两部分,数据读写以及状态控制,互不干扰。在使用时,要求用户按照内核特定的方式进行命令码的封装和解析,实现应用层和内核空间更好的对接。ioctl函数本质上就是用户空间向内核空间提交一段具有特定含义的命令码,内核空间根据内核规定好的方式,对命令码进行解析,执行底层的操作。在arm架构下的linux内核中,每个命令码由32bit组成。功能码+设备类型码+数据传输大小+数据传输方向。数据传输方向:_IO,_IOR,_IOW,_IOWR;数据传递大小:使用宏定义进行命令码封装时,必须填写数据类型;设备类型码:每一个驱动通过一个唯一的字符来代表;功能码:由自己指定。每次自己写32位的命令码比较麻烦,内核中定义好的宏定义可以简化这一过程。ioctl函数有cmd和arg两个参数,cmd就是命令码,arg就是数据地址,fd是文件描述符。

HMC5883_spi.cpp

device::Device *HMC5883_SPI_interface( int bus) 这是HMC的SPI接口函数。函数体是创建HMC_SPI类的一个实例。

HMC_SPI类是SPI的派生类。在HMC_SPI的构造函数中设定SPI的各个参数值。在这个类中,有init,read,write,ioctl四个虚函数。

HMC_SPI::init()先调用了SPI::init(),然后在指定地址读取ID,查看是否正确。

HMC_SPI::ioctl()如果操作是magiocg_external返回0(即使这个传感器是在外部SPI总线上,它仍然是飞控的内部组件,所以总是返回0表示内部),如果操作是deviocg_deviceid返回CDev::ioctl (nullptr, operation, arg)。

HMC_SPI::write和read都是在SPI::transfer进一步封装。需要注意的是,SPI::transfer是将发送和接收合为一个函数。这是因为SPI协议中,主从机的数据交互不需要应答位。当接收缓冲区满了之后或者发送缓冲区空了之后,都会触发同一个中断服务子函数。transfer函数只需要提供发数据的地址和接收数据的地址以及数据长度即可。transfer函数里调用的实际是firmware/nuttx/nuttx/arch /arm/src/stm32/stm32_spi.c的底层SPI库。

三、仿写

在PX4固件中,使用SPI协议的除了HMC磁力计还有MPU陀螺仪。所有SPI协议的传感器都是继承SPI这个类来实现SPI传感器驱动程序。构造函数,ioctl,read,write,init虚函数在类里重写。虚函数的重写要参照具体的硬件手册。

参照HMC的SPI文件,写MPU6000的SPI文件:

#ifdef PX4_SPIDEV_MPU6000



device::Device *MPU6000_SPI_interface(int bus);

class MPU6000_SPI : public device::SPI
{
public:
	MPU6000_SPI(int bus, spi_dev_e device);
	virtual ~MPU6000_SPI();

	virtual int	init();
	virtual int	read(unsigned address, void *data, unsigned count);
	virtual int	write(unsigned address, void *data, unsigned count);

	virtual int	ioctl(unsigned operation, unsigned &arg);

};

device::Device *
MPU6000_SPI_interface(int bus)
{
	return new MPU6000_SPI(bus, (spi_dev_e)PX4_SPIDEV_MPU6000);
}

HMC5883_SPI::MPU6000_SPI(int bus, spi_dev_e device) :
	SPI("MPU6000_SPI", nullptr, bus, device, SPIDEV_MODE3, 11 * 1000 * 1000 )
{
	_device_id.devid_s.devtype = DRV_MAG_DEVTYPE_MPU6000;
}


MPU6000_SPI::~MPU6000_SPI()
{
}

int
MPU6000_SPI::init()
{
	int ret;

	ret = SPI::init();

	if (ret != OK) {
		DEVICE_DEBUG("SPI init failed");
		return -EIO;
	}

	// read WHO_AM_I value
	uint8_t data[3] = {0, 0, 0};

	if (read(ADDR_ID_A, &data[0], 1) ||
	    read(ADDR_ID_B, &data[1], 1) ||
	    read(ADDR_ID_C, &data[2], 1)) {
		DEVICE_DEBUG("read_reg fail");
	}

	if ((data[0] != ID_A_WHO_AM_I) ||
	    (data[1] != ID_B_WHO_AM_I) ||
	    (data[2] != ID_C_WHO_AM_I)) {
		DEVICE_DEBUG("ID byte mismatch (%02x,%02x,%02x)", data[0], data[1], data[2]);
		return -EIO;
	}

	return OK;
}

int
MPU6000_SPI::ioctl(unsigned operation, unsigned &arg)
{
	int ret;

	switch (operation) {

	case MAGIOCGEXTERNAL:
		return 0;

	case DEVIOCGDEVICEID:
		return CDev::ioctl(nullptr, operation, arg);

	default: {
			ret = -EINVAL;
		}
	}

	return ret;
}

int
MPU6000_SPI::write(unsigned address, void *data, unsigned count)
{
	uint8_t buf[32];

	if (sizeof(buf) < (count + 1)) {
		return -EIO;
	}

	buf[0] = address | DIR_WRITE;
	memcpy(&buf[1], data, count);

	return transfer(&buf[0], &buf[0], count + 1);
}

int
MPU6000_SPI::read(unsigned address, void *data, unsigned count)
{
	uint8_t buf[32];

	if (sizeof(buf) < (count + 1)) {
		return -EIO;
	}

	buf[0] = address | DIR_READ | ADDR_INCREMENT;

	int ret = transfer(&buf[0], &buf[0], count + 1);
	memcpy(data, &buf[1], count);
	return ret;
}

#endif 

在MPU6000的构造函数中,与SPI一样,参数有bus,device,device_type,还有mode和频率。mode和频率要看传感器的手册。还有在后面的检查whoami时的ID号码。device是片选信号线,P4X4_SPIDEV_MPU6000。bus是SPI的总线。查看PX4的总线接口:PIXHAWK有三路SPI总线接口,一个给铁电存储器,一路给内置IMU,一路给外置的SPI。先看一下这个内置的SPI总线(给IMU,包括磁力计,陀螺仪,他们总线相同,但是片选信号不同,参考SPI的通讯图)。在使用SPI类来派生新类时,特别注意的是类的构造函数中的参数,要参考传感器的手册和硬件电路图,PX4的SPI外接的总线。

四、补充:底层的关系

HMC5883_SPI类里面开启工作队列work_queue或者定时回调函数来读取传感器的值,然后通过ourb把数据发送出去。

SPI类里面的函数SPI_SETMODE,SPI_SELECT,SPI_EXCHANGE在底层驱动stm32_spi.c里面。底层的关系:在CDev::init()函数中,调用了int register_driver(const char *path, const struct file_operations*fops, mode_t mode, void *priv)之后就可以使用用户接口open,close,read,write等。每个字符设备驱动程序必须实现struct file_operation的实例;每个串口设备驱动程序必须实现struct spi_ops_s的实例。spi_ops_s结构体的成员是指向函数的指针。static   struct  stm32_spidev_sg_spi1dev 结构体会把 static const  struct spi_ops_s g_sp1iops包含在内,这样g_spi1dev就可以代表一个spi端口了,然后利用up_spiinitialize就可以初始化spi端口了

主要过程就是:每个spi端口都会有structspi_ops_s的实例,spi_ops_s结构体的成员是指向函数的指针,这样g_spi1dev就可以代表一个spi端口了;然后利用up_spiinitialize就可以初始化spi端口了;之后使用spi端口的传感器在初始化中都会调用SPI::init(),从而调用up_spiinitialize。可以发现spi的操作没有register(),SPI驱动程序通常不由用户代码直接访问,但通常绑定到另一个更高级别的设备驱动程序(例如mpu6000),绑定的顺序是:从硬件特定的SPI设备驱动程序获取struct spi_dev_s的实例,将该实例提供给较高级别设备驱动程序的初始化方法。(这部分自己还没搞懂,先搁到这里吧)

pixhawk px4 spi设备驱动_pixhawk驱动下载-CSDN博客


http://www.niftyadmin.cn/n/5423872.html

相关文章

【每日八股】Java基础中面试你必须要掌握问题1

&#x1f525; 个人主页: 黑洞晓威 &#x1f600;你不必等到非常厉害&#xff0c;才敢开始&#xff0c;你需要开始&#xff0c;才会变的非常厉害。## 如何解决浮点数运算的精度丢失问题&#xff1f; BigDecimal 可以解决精度问题的原因在于它是一个精确的十进制数学运算类&…

Spring Cloud Alibaba微服务从入门到进阶(二)

Spring Boot配置管理 1、application.properties 2、application.yml 1.内容格式比较&#xff1a; .properties文件&#xff0c;通过 . 来连接&#xff0c;通过 来赋值&#xff0c;结构上&#xff0c;没有分层的感觉&#xff0c;但比较直接。 .yml文件&#xff0c;通过 &…

个人商城系统开源(配置支付宝支付!)

原文地址&#xff1a;个人商城系统开源&#xff08;配置支付宝支付&#xff01;&#xff09; - Pleasure的博客 下面是正文内容&#xff1a; 前言 由于近期实在没有什么话题可写和一些有趣的项目教程可以分享。所以我只能决定将我自己亲手编写的一个迷你迷你商城系统进行开源…

面试官:js需要同时发起百条接口请求怎么办?--通过Promise实现分批处理接口请求

如何通过 Promise 实现百条接口请求&#xff1f; 实际项目中遇到需要批量发起上百条接口请求怎么办&#xff1f; 前言 不知你项目中有没有遇到过这样的情况&#xff0c;反正我的实际工作项目中真的遇到了这种玩意&#xff0c;一个接口获取一份列表&#xff0c;列表中的每一项…

打卡--MySQL8.0 二 (用户权限管理)

一、mysql8修改了安全规则&#xff0c;不能像mysql5.7 一次性创建用户并授权&#xff0c;需要分批创建。 1、注意在MySQL8.0版本中创建用户一定要在配置文件中增加如下内容&#xff0c;来兼容旧的程序运行。 default_authentication_pluginmysql_native_password 2、创建用户…

鸿蒙Harmony应用开发—ArkTS声明式开发(基础手势:Progress)

进度条组件&#xff0c;用于显示内容加载或操作处理等进度。 说明&#xff1a; 该组件从API version 7开始支持。后续版本如有新增内容&#xff0c;则采用上角标单独标记该内容的起始版本。 子组件 无 接口 Progress(options: ProgressOptions<Type>) 创建进度组件&a…

Java通过Excel批量上传数据!!!

一、首先在前端写一个上传功能。 <template><!-- 文件上传 --><el-upload class"upload-demo" drag action"" :on-change"onChange" :auto-upload"false"><el-icon class"el-icon--upload"><up…

解决docker通过volumes挂载文件,宿主机修改后容器内不同步,重启服务才能同步

将文件的权限改为777&#xff0c;即chmod 777 filename。 详细解释在该文章&#xff1a; https://huaweicloud.csdn.net/633114e5d3efff3090b51a5a.html 说明&#xff1a; 这是由于linux系统文件挂载机制导致的。 docker通过volumes挂载文件到容器中&#xff0c;有以下两种方式…