最近接触PX4平台比较多。
记录一下在PX4软件上对于pixhawk的spi的一点工作。
我的PX4软件版本是1.11.3

首先聊一下硬件抽象的概念

在PX4代码里,用的是nuttx操作系统,它最大的特点就是代码接口严格遵守posix标准。
这使得,在不同的底层框架里,它的上层程序是可以共用的。PX4程序可以被编译成在nuttx操作系统下运行的程序,并打包成固件在pixhawk当中运行;也可以被编译成在Linux操作系统下运行的程序,并在Linux操作系统上运行(比如树莓派);或者在装有Linux操作系统的个人电脑上运行,进行半物理仿真和模拟飞行(比如gazebo和javsim)。
硬件抽象主要是为了隔离底层驱动与应用程序的。对于一个嵌入式操作系统来讲,硬件抽象的确是很重要的一部分,而很遗憾,我之前只接触过freertos,对于这个不是很了解。但是我们的国产RTOS rttread足够完善,对于这个也是完全支持滴。嗯,nuttx当然也选择这样子的接口。
其实PX4的整个代码框架,都在强调抽象与独立这样的概念。将飞控逻辑与具体的底层控制器指令实现进行了解耦合。我在很长一段时间里在做单片机方面的学习,倾向于直接使用底层控制协议来控制飞控板或者其它作业机构,但实际上PX4架构已经在更高的抽象层面上提供了更好的选择,无论是代码维护成本、开发效率、硬件兼容性都能显著高于前者。
其实,关于单片机操作系统的硬件抽象的一些概念,我比较推荐去看rttread的官方说明,对于nutxx的资料和说明实在是太少了(更别提中文的了)。


先找一个示例程序

我这里参考ICM20602程序

在PX4-Autopilot/src/drivers/imu/invensense/icm20602文件夹下面。
ICM20602类定义:

class ICM20602 : public device::SPI, public I2CSPIDriver<ICM20602>

继承device::SPI来自/src/lib/drivers/device/nuttx/SPI.hpp
继承I2CSPIDriver来自/platforms/common/include/px4_platform_common/i2c_spi_buses.h
/src/lib/drivers/device/nuttx/SPI.hpp主要是对nuttx硬件抽象接口的进一步封装,这个比较重要。
/platforms/common/include/px4_platform_common/i2c_spi_buses.h主要是对设备任务的一些接口封装,比如任务队列、初始化等接口,我们可以理解它就是个模子,不用也行(当然建议还是用这个)。


所以,硬件驱动方面,我们先看/src/lib/drivers/device/nuttx/SPI这个文件夹下面的程序
这个程序里的SPI类被分配到namespace device命名空间里,其实看这个命名空间就晓得它是用来做硬件层面的驱动接口用的。同时它继承了public CDev这个类,对应PX4-Autopilot/src/lib/drivers/device/CDev.cpp这个文件。
而CDev这个类主要的两个成员函数 virtual int init();和virtual int ioctl(file_t *filep, int cmd, unsigned long arg);这两个函数都被定义为virtual虚函数,所以它们大致会在后面被重新定义为不同的功能。
其中virtual int init() override;在SPI类里被覆盖,ioctl在spi驱动里并未被用到(用的是read()和write()函数)。
所以我一开始以为CDev就是一个没什么大用的模板类,其实不然,对于硬件抽象的注册等操作还是会通过它调用。
先回到SPI类,SPI::init()完成对SPI设备的初始化,我们看看SPI::init()干了什么,

int
SPI::init()
{
    /* attach to the spi bus */
    if (_dev == nullptr) {
        int bus = get_device_bus();

        if (!board_has_bus(BOARD_SPI_BUS, bus)) {
            return -ENOENT;
        }

        _dev = px4_spibus_initialize(bus);
    }

    if (_dev == nullptr) {
        DEVICE_DEBUG("failed to init SPI");
        return -ENOENT;
    }

    /* deselect device to ensure high to low transition of pin select */
    SPI_SELECT(_dev, _device, false);

    /* call the probe function to check whether the device is present */
    int ret = probe();

    if (ret != OK) {
        DEVICE_DEBUG("probe failed");
        return ret;
    }

    /* do base class init, which will create the device node, etc. */
    ret = CDev::init();

    if (ret != OK) {
        DEVICE_DEBUG("cdev init failed");
        return ret;
    }

    /* tell the world where we are */
    DEVICE_DEBUG("on SPI bus %d at %d (%u KHz)", get_device_bus(), PX4_SPI_DEV_ID(_device), _frequency / 1000);

    return PX4_OK;
}

其实也就是简单的初始化,需要注意的就是两个地方

    /* call the probe function to check whether the device is present */
    int ret = probe();
    /* do base class init, which will create the device node, etc. */
    ret = CDev::init();

virtual int probe() { return PX4_OK; }被定义为了虚函数,在icm20602的类里面被定义为如下,其实这个我们不重写也行,直接让它返回OK也未尝不可(我自己的想法,其实安全起见加上这个还是有好处)。

int ICM20602::probe()
{
    const uint8_t whoami = RegisterRead(Register::WHO_AM_I);

    if (whoami != WHOAMI) {
        DEVICE_DEBUG("unexpected WHO_AM_I 0x%02x", whoami);
        return PX4_ERROR;
    }

    return PX4_OK;
}

ret = CDev::init();则是我之前说的:所以我一开始以为CDev就是一个没什么大用的模板类,其实不然,对于硬件抽象的注册等操作还是会通过它调用。这个成员函数虽然被子类覆盖了,但是子类也直接调用了它。

int CDev::init()
{
    PX4_DEBUG("CDev::init");

    // base class init first
    int ret = Device::init();

    if (ret != PX4_OK) {
        goto out;
    }

    // now register the driver
    if (get_devname() != nullptr) {
        ret = cdev::CDev::init();

        if (ret != PX4_OK) {
            goto out;
        }
    }

out:
    return ret;
}

它主要运行了_dev = px4_spibus_initialize(bus);和int ret = Device::init();和ret = cdev::CDev::init();
其中px4_spibus_initialize(bus);负责spi总线的初始化。
Device::init()直接返回了OK,但是这个文件PX4-Autopilot/src/lib/drivers/device/Device.hpp值得注意,它提供的是硬件抽象层的所有接口,当然都是虚函数,等着被继承覆盖的那种(说白了就是个模板)。
cdev::CDev::init()目标文件在PX4-Autopilot/src/lib/cdev/CDev.cpp,用于注册设备。
再次回到SPI类,三个虚函数virtual ~SPI();virtual int init() override;virtual int probe() { return PX4_OK; }
其中 ~SPI()对应~ICM20602()
init()对应ICM20602::init()
probe()对应ICM20602::probe()
其它的SPI类成员函数都被直接继承给了ICM20602类。
比较重要的像transfer(uint8_t _send, uint8_t _recv, unsigned len)
其它细节,比如一些成员变量就不看了,有点绕多了。
总之,这个SPI类PX4-Autopilot/src/lib/drivers/device/nuttx/SPI.cpp负责了SPI通信接口函数实现,从注册初始化到数据收发。


接下来是class I2CSPIDriver : public I2CSPIDriverBase这个类
前面的SPI类相当于是协议方法,这个类就是调用方法,是在任务队列的基础上封装的,说实话我很难理解它封装这么个玩意儿有什么大的好处,我读起来麻烦的要死。。。
它首先继承了public I2CSPIDriverBase这个类,其实主要的是I2CSPIDriverBase这个类。
先回到I2CSPIDriver这个类,里面有一个比较重要的成员函数

    void Run() final
    {
        static_cast<T *>(this)->RunImpl();

        if (should_exit()) {
            exit_and_cleanup();
        }
    }

这里通过模板类访问了子类的RunImpl()函数,也就是void ICM20602::RunImpl()。
这个Run()后面有一个 final关键字,它是被禁用重写的,这个函数是从任务队列里继承过来的,具体可以参考例子PX4-Autopilot/src/examples/work_item/WorkItemExample.cpp
这里粗略的说一下,运行ScheduleNow、ScheduleOnInterval这样的接口后会将当前任务加入到任务队列当中,然后进入/PX4-Autopilot/platforms/common/include/px4_platform_common/px4_work_queue/WorkItem.hpp里的virtual void Run() = 0;
这个run函数被继承到class ScheduledWorkItem : public WorkItem然后被继承到class I2CSPIDriverBase : public px4::ScheduledWorkItem, public I2CSPIInstance最后被继承到class I2CSPIDriver : public I2CSPIDriverBase。这个就是被覆盖的void Run() final。
这里面弯弯绕绕我真的挺无语,就当个模板来套。
具体流程我说一下:
跟随start指令进入ThisDriver::module_start(cli, iterator);
对应PX4-Autopilot/platforms/common/include/px4_platform_common/i2c_spi_buses.h

    static int module_start(const BusCLIArguments &cli, BusInstanceIterator &iterator)
    {
        return I2CSPIDriverBase::module_start(cli, iterator, &T::print_usage, &T::instantiate);
    }

对应/platforms/common/i2c_spi_buses.cpp

int I2CSPIDriverBase::module_start(const BusCLIArguments &cli, BusInstanceIterator &iterator,
                   void(*print_usage)(), instantiate_method instantiate)

这段代码我有些看不懂,只知道它大概是通过I2CSPIDriverBase _instance = initializer_data.instance;调用了子类的_instance。
进入的是icm20602程序

static I2CSPIDriverBase *instantiate(const BusCLIArguments &cli, const BusInstanceIterator &iterator, int runtime_instance);

对应PX4-Autopilot/src/drivers/imu/invensense/icm20602/icm20602_main.cpp

I2CSPIDriverBase *ICM20602::instantiate(const BusCLIArguments &cli, const BusInstanceIterator &iterator,
                    int runtime_instance)
{
    ICM20602 *instance = new ICM20602(iterator.configuredBusOption(), iterator.bus(), iterator.devid(), cli.rotation,
                      cli.bus_frequency, cli.spi_mode, iterator.DRDYGPIO());

    if (!instance) {
        PX4_ERR("alloc failed");
        return nullptr;
    }

    if (OK != instance->init()) {
        delete instance;
        return nullptr;
    }

    return instance;
}

实例化ICM20602,设置关于设备、加速度计与陀螺仪的参数,进入instance->init()
执行return Reset() ? 0 : -1;进入bool ICM20602::Reset()运行ScheduleNow();然后工作队列就会自动运行Run()这个函数了。


再来好好看看nuttx的硬件抽象

在SPI类里面我们看到了

    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);

这几个接口被指向了nuttx底层,PX4-Autopilot/platforms/nuttx/NuttX/nuttx/include/nuttx/spi/spi.h
比如

#define SPI_SELECT(d,id,s) ((d)->ops->select(d,id,s))

我们也可以看到第一个参数比较_dev比较重要。
它被定义在SPI类里struct spi_dev_s *_dev {nullptr};
好了,也就是实现方法都在spi_dev_s这个结构体里面。
对应PX4-Autopilot/platforms/nuttx/NuttX/nuttx/include/nuttx/spi/spi.h

struct spi_dev_s
{
  FAR const struct spi_ops_s *ops;
};

嗯,对上了,再看spi_ops_s这个结构体

struct spi_ops_s
{
  CODE int      (*lock)(FAR struct spi_dev_s *dev, bool lock);
  CODE void     (*select)(FAR struct spi_dev_s *dev, uint32_t devid,
                  bool selected);
  CODE uint32_t (*setfrequency)(FAR struct spi_dev_s *dev, uint32_t frequency);
#ifdef CONFIG_SPI_CS_DELAY_CONTROL
  CODE int      (*setdelay)(FAR struct spi_dev_s *dev, uint32_t a, uint32_t b,
                  uint32_t c);
#endif
  CODE void     (*setmode)(FAR struct spi_dev_s *dev, enum spi_mode_e mode);
  CODE void     (*setbits)(FAR struct spi_dev_s *dev, int nbits);
#ifdef CONFIG_SPI_HWFEATURES
  CODE int      (*hwfeatures)(FAR struct spi_dev_s *dev,
                  spi_hwfeatures_t features);
#endif
  CODE uint8_t  (*status)(FAR struct spi_dev_s *dev, uint32_t devid);
#ifdef CONFIG_SPI_CMDDATA
  CODE int      (*cmddata)(FAR struct spi_dev_s *dev, uint32_t devid,
                  bool cmd);
#endif
  CODE uint16_t (*send)(FAR struct spi_dev_s *dev, uint16_t wd);
#ifdef CONFIG_SPI_EXCHANGE
  CODE void     (*exchange)(FAR struct spi_dev_s *dev,
                  FAR const void *txbuffer, FAR void *rxbuffer,
                  size_t nwords);
#else
  CODE void     (*sndblock)(FAR struct spi_dev_s *dev,
                  FAR const void *buffer, size_t nwords);
  CODE void     (*recvblock)(FAR struct spi_dev_s *dev, FAR void *buffer,
                  size_t nwords);
#endif
#ifdef CONFIG_SPI_TRIGGER
  CODE int      (*trigger)(FAR struct spi_dev_s *dev);
#endif
  CODE int      (*registercallback)(FAR struct spi_dev_s *dev,
                  spi_mediachange_t callback, void *arg);
};

嗯,结构体定义是找到了,结构体变量的定义在哪里呢?

我们回到SPI类的程序,PX4-Autopilot/src/lib/drivers/device/nuttx/SPI.hpp结构体变量被定义:

struct spi_dev_s    *_dev {nullptr};

在PX4-Autopilot/src/lib/drivers/device/nuttx/SPI.cpp里面有

_dev = px4_spibus_initialize(bus);

好的,_dev = px4_spibus_initialize(bus);就返回了硬件抽象的具体实现。
对应PX4-Autopilot/platforms/nuttx/src/px4/stm/stm32_common/include/px4_arch/micro_hal.h

#define px4_spibus_initialize(bus_num_1based)   stm32_spibus_initialize(bus_num_1based)

对应PX4-Autopilot/platforms/nuttx/NuttX/nuttx/arch/arm/src/stm32/stm32_spi.c
然后定义

FAR struct stm32_spidev_s *priv = NULL;

stm32_spidev_s这个结构体里包含了struct spi_dev_s spidev;
以SPI4为例(pixhawk的外部SPI是SPI4)

priv = &g_spi4dev;

再返回

return (FAR struct spi_dev_s *)priv;

所以具体的实现是在g_spi4dev

static struct stm32_spidev_s g_spi4dev =
{
  .spidev   = { &g_sp4iops },
  .spibase  = STM32_SPI4_BASE,
  .spiclock = STM32_PCLK2_FREQUENCY,
#ifdef CONFIG_STM32_SPI_INTERRUPTS
  .spiirq   = STM32_IRQ_SPI4,
#endif
#ifdef CONFIG_STM32_SPI_DMA
#  ifdef CONFIG_STM32_SPI4_DMA
  .rxch     = DMACHAN_SPI4_RX,
  .txch     = DMACHAN_SPI4_TX,
#if defined(SPI4_DMABUFSIZE_ADJUSTED)
  .rxbuf    = g_spi4_rxbuf,
  .txbuf    = g_spi4_txbuf,
  .buflen   = SPI4_DMABUFSIZE_ADJUSTED,
#    endif
#  else
  .rxch     = 0,
  .txch     = 0,
#  endif
#endif
};

所以我们转而看.spidev = { &g_sp4iops },的定义

static const struct spi_ops_s g_sp4iops =
{
  .lock              = spi_lock,
  .select            = stm32_spi4select,
  .setfrequency      = spi_setfrequency,
  .setmode           = spi_setmode,
  .setbits           = spi_setbits,
#ifdef CONFIG_SPI_HWFEATURES
  .hwfeatures        = spi_hwfeatures,
#endif
  .status            = stm32_spi4status,
#ifdef CONFIG_SPI_CMDDATA
  .cmddata           = stm32_spi4cmddata,
#endif
  .send              = spi_send,
#ifdef CONFIG_SPI_EXCHANGE
  .exchange          = spi_exchange,
#else
  .sndblock          = spi_sndblock,
  .recvblock         = spi_recvblock,
#endif
#ifdef CONFIG_SPI_TRIGGER
  .trigger           = spi_trigger,
#endif
#ifdef CONFIG_SPI_CALLBACK
  .registercallback  = stm32_spi4register,  /* provided externally */
#else
  .registercallback  = 0,  /* not implemented */
#endif
};

然后就能看到各自的实现函数啦!
我是从上层往底层代码看的,这里有一篇文章讲的也非常棒,给了我很大的借鉴。https://blog.csdn.net/czyv587/article/details/53817154
不过PX4软件版本不同,而且他是从底层分析到上层的,很明显他的做法是对的,应该着力于从nuttx底层去了解这个。


再讲一下我自己做的两个事情

首先是关于片选的事情,我手上的板子是自制的,它的imu传感器的片选被做了改变,我需要去针对片选引脚的选择做一下调整,也就是修改对应的片选引脚。
在文件PX4-Autopilot/src/lib/drivers/device/nuttx/SPI.cpp里我看到了SPI_SELECT(_dev, _device, false);是操作片选拉低/拉高的。
pixhawk上一共使用了3路spi,其中spi1给了板上内部的imu传感器,spi2给了板上的FLASH(eeprom还是其它的?),spi4给了板子上的外部的spi接口。
所以我操作的应该是针对spi1,我决定跟踪SPI_SELECT这个函数

#define SPI_SELECT(d,id,s) ((d)->ops->select(d,id,s))

对应的是PX4-Autopilot/platforms/nuttx/NuttX/nuttx/arch/arm/src/stm32/stm32_spi.c里的

.select            = stm32_spi1select,

在PX4-Autopilot/platforms/nuttx/src/px4/stm/stm32_common/spi/spi.cpp里有定义

__EXPORT void stm32_spi1select(FAR struct spi_dev_s *dev, uint32_t devid, bool selected)
{
    stm32_spixselect(_spi_bus1, dev, devid, selected);
}

...
...
...

static inline void stm32_spixselect(const px4_spi_bus_t *bus, struct spi_dev_s *dev, uint32_t devid, bool selected)
{
    for (int i = 0; i < SPI_BUS_MAX_DEVICES; ++i) {
        if (bus->devices[i].cs_gpio == 0) {
            break;
        }

        if (devid == bus->devices[i].devid) {
            // SPI select is active low, so write !selected to select the device
            stm32_gpiowrite(bus->devices[i].cs_gpio, !selected);
        }
    }
}

所以要紧的是第一个参数,const px4_spi_bus_t *bus —> _spi_bus1
被定义在PX4-Autopilot/platforms/nuttx/src/px4/stm/stm32_common/spi/spi.cpp

static const px4_spi_bus_t *_spi_bus1;

这里是一个声明,所以_spi_bus1一定是在某个地方被定义了。我们需要找到_spi_bus1被赋值的地方。
全局搜索得到

__EXPORT void stm32_spiinitialize()
{
    px4_set_spi_buses_from_hw_version();
    board_control_spi_sensors_power_configgpio();
    board_control_spi_sensors_power(true, 0xffff);

    for (int i = 0; i < SPI_BUS_MAX_BUS_ITEMS; ++i) {
        switch (px4_spi_buses[i].bus) {
        case 1: _spi_bus1 = &px4_spi_buses[i]; break;

        case 2: _spi_bus2 = &px4_spi_buses[i]; break;

        case 3: _spi_bus3 = &px4_spi_buses[i]; break;

        case 4: _spi_bus4 = &px4_spi_buses[i]; break;

        case 5: _spi_bus5 = &px4_spi_buses[i]; break;

        case 6: _spi_bus6 = &px4_spi_buses[i]; break;
        }
    }

stm32_spiinitialize()这个函数是伴随nuttx系统初始化被调用的。
看到px4_set_spi_buses_from_hw_version();

void px4_set_spi_buses_from_hw_version()
{
    int hw_version = board_get_hw_version();

    for (int i = 0; i < BOARD_NUM_SPI_CFG_HW_VERSIONS; ++i) {
        if (!px4_spi_buses && px4_spi_buses_all_hw[i].board_hw_version == 0) {
            px4_spi_buses = px4_spi_buses_all_hw[i].buses;
        }

        if (px4_spi_buses_all_hw[i].board_hw_version == hw_version) {
            px4_spi_buses = px4_spi_buses_all_hw[i].buses;
        }
    }

    if (!px4_spi_buses) { // fallback
        px4_spi_buses = px4_spi_buses_all_hw[0].buses;
    }
}

在这里,case 1: _spi_bus1 = &px4_spi_buses[i]; break;
所以转而找px4_spi_buses的声明和定义,见PX4-Autopilot/platforms/common/spi.cpp

void px4_set_spi_buses_from_hw_version()
{
    int hw_version = board_get_hw_version();

    for (int i = 0; i < BOARD_NUM_SPI_CFG_HW_VERSIONS; ++i) {
        if (!px4_spi_buses && px4_spi_buses_all_hw[i].board_hw_version == 0) {
            px4_spi_buses = px4_spi_buses_all_hw[i].buses;
        }

        if (px4_spi_buses_all_hw[i].board_hw_version == hw_version) {
            px4_spi_buses = px4_spi_buses_all_hw[i].buses;
        }
    }

    if (!px4_spi_buses) { // fallback
        px4_spi_buses = px4_spi_buses_all_hw[0].buses;
    }
}

const px4_spi_bus_t *px4_spi_buses{};

这里看到了一个很重要的变量px4_spi_buses_all_hw,搜索看到对应PX4-Autopilot/boards/px4/fmu-v3/src/spi.cpp里面定义

constexpr px4_spi_bus_all_hw_t px4_spi_buses_all_hw[BOARD_NUM_SPI_CFG_HW_VERSIONS] = {

好了!我知道了!
在那个的基础上我改动如下

其中//////////////////ljjljjljj//////////////////////后两行就是我自己添加的,驱动成功!
当然任务还没结束,因为这个板子上的imu是安装在下方的,所以在启动的时候要在PX4-Autopilot/boards/px4/fmu-v3/init/rc.board_sensors里添加
icm20602 -s -R 14 start
icm20689 -s -R 14 start

加上-R 14表示旋转,这个参数怎么确定看PX4-Autopilot/src/lib/conversion/rotation.h


第二个工作是利用外部的spi(即SPI4)与单片机通信,代码我上传到码云。
https://gitee.com/TerryAAA/px4-ljj_spi-for-pixhawkmini
需要注意几点:

  1. 如果是pixhawk mini的外部SPI,我们需要在PX4-Autopilot/boards/px4/fmu-v2/src/spi.cpp里面加上

    initSPIBusExternal(SPI::Bus::SPI4, { // unused, but we must at least define it here
         }),
    

    这段代码里面加上如图所示的内容,因为mini板子没有外部spi接口默认不开放SPI4(我这个自制的有)。

  2. 这个代码里面我定义了一个参数,叫LJJ_SPI_SENDONLY,具体什么意思可以看代码,关于自定义参数的操作后续更新。

  3. ljj_spi文件夹内容是PX4代码,把这个文件夹放置到源码的位置PX4-Autopilot/src/drivers/

  4. 与pixhawk通信的测试芯片为stm32f103c8t6,注意LJJ_SPI_SENDONLY参数与stm32工程里的RECIVEONLY对应。


记录完毕~