一、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_MPU6000device::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 valueuint8_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博客