PX4通过I2C方式添加自定义传感器(2)

PX4 I2C通信方式传感器驱动分析(以ets_airspeed为例)

1、说明

这篇文章我们就来看看I2C传感器的驱动过程,当然里面也有很多东西我不是很理解,所以仅谈我领悟的一些东西。我就以etc_airspeed.c这个传感器的代码来分析一下,这个代码在src->drivers文件夹下,但是不同的固件位置不一定相同,比如有的就在src\driversdifferential_pressure\ets文件夹下,需要自己找一下。简单说下airspeed这个传感器,就是空速计,核心就是两部分:空速管(也称皮托管)和两片气压传感器,正好手头上有这个玩意,如下图:
皮托管
气压传感器
皮托管由两个同心圆管组成,内圆管为总压管,外套管为静压管(如图侧面开了小孔与大气相通)。空速管测量飞机速度的原理是这样的当飞机向前飞行时,气流便冲进空速管,在管子末端的气压传感器会感受到气流的冲击力量,即动压。飞机飞得越快,动压就越大。如果将空气静止时的压力即静压和动压相比就可以知道冲进来的空气有多快,也就是飞机飞得有多快。


2、 类的继承和定义

我们先来看一下关于ETSAirspeed这个类的定义

class ETSAirspeed : public Airspeed
{
public:
    ETSAirspeed(int bus, int address = I2C_ADDRESS, const char *path = ETS_PATH);

protected:

    /**
    * Perform a poll cycle; collect from the previous measurement
    * and start a new one.
    */
    virtual void    cycle();
    virtual int measure();
    virtual int collect();

};

从第一行代码可以看到,这个类是对Airspeed类的public继承方式(这里需要对C++有一定了解),所以我们再来追寻一下Airspeed这个类的定义(建议使用Source Insight来看代码,这个软件对代码的搜索功能很是强大),好了我们追寻到了src\drivers\airspeed文件夹下,这里有一个Airspeed.h,这里就对Airspeed类进行了定义如下(我们只简单看一小段):

class __EXPORT Airspeed : public device::I2C
{
public:
    Airspeed(int bus, int address, unsigned conversion_interval, const char *path);
    virtual ~Airspeed();

可以看到仍然是对其他类的继承,我们继续追寻,最后追寻到src\drivers\device\nuttx文件夹下有一个I2C.hpp文件

class __EXPORT I2C : public CDev
{

public:

    static int  set_bus_clock(unsigned bus, unsigned clock_hz);

    static unsigned int _bus_clocks[BOARD_NUMBER_I2C_BUSES];

protected:

到这里我们终于不用继续追寻了,我们看到我们想要的I2C这个类了,这个类可以好好看看,因为这里面对使用I2C所用到的各种函数都进行了申明,比如设置总线频率的函数:

static int  set_bus_clock(unsigned bus, unsigned clock_hz);

再比如transfer这个函数,这里也有比较详细的介绍:

    /**
     * Perform an I2C transaction to the device.
     *
     * At least one of send_len and recv_len must be non-zero.
     *
     * @param send      Pointer to bytes to send.
     * @param send_len  Number of bytes to send.
     * @param recv      Pointer to buffer for bytes received.
     * @param recv_len  Number of bytes to receive.
     * @return      OK if the transfer was successful, -errno
     *          otherwise.
     */
    int     transfer(const uint8_t *send, unsigned send_len, uint8_t *recv, unsigned recv_len);

所以总结一下就是,任何的以I2C通信方式的传感器,在写驱动的时候归根结底都是对基类I2C的继承。 有了基类的继承我们在用这些函数的时候就可以直接使用了,而不必重新申明和定义。

3、几个重要函数的介绍

3.1 transfer()函数介绍

先来看代码,这个函数的代码在I2C.cpp中:

int
I2C::transfer(const uint8_t *send, unsigned send_len, uint8_t *recv, unsigned recv_len)
{
    px4_i2c_msg_t msgv[2];
    unsigned msgs;
    int ret = PX4_ERROR;
    unsigned retry_count = 0;

    if (_dev == nullptr) {
        PX4_ERR("I2C device not opened");
        return 1;
    }

    do {
        DEVICE_DEBUG("transfer out %p/%u  in %p/%u", send, send_len, recv, recv_len);
        msgs = 0;

        if (send_len > 0) {
            msgv[msgs].frequency = _bus_clocks[get_device_bus() - 1];
            msgv[msgs].addr = get_device_address();
            msgv[msgs].flags = 0;
            msgv[msgs].buffer = const_cast<uint8_t *>(send);
            msgv[msgs].length = send_len;
            msgs++;
        }

        if (recv_len > 0) {
            msgv[msgs].frequency = _bus_clocks[get_device_bus() - 1];
            msgv[msgs].addr = get_device_address();
            msgv[msgs].flags = I2C_M_READ;
            msgv[msgs].buffer = recv;
            msgv[msgs].length = recv_len;
            msgs++;
        }

        if (msgs == 0) {
            return -EINVAL;
        }

        ret = I2C_TRANSFER(_dev, &msgv[0], msgs);

        /* success */
        if (ret == PX4_OK) {
            break;
        }

        /* if we have already retried once, or we are going to give up, then reset the bus */
        if ((retry_count >= 1) || (retry_count >= _retries)) {
            I2C_RESET(_dev);
        }

    } while (retry_count++ < _retries);

    return ret;
}

先对这里的几个参数简单说一下,

  • *send即为指针,指向需要发送到设备的数据
  • send_len即为需要发送的数据的长度
  • *recv即为指针,指向接收数据的变量
  • recv_len即为要接收数据的长度
    上一篇文章我们着重强调了这个transfer函数,为什么强调呢?假如我们如果要在单片机上进行实验,需要给这个设备发送一个控制指令(对于我的角度传感器,我需要发送一个通道选择指令为0x40|3),那通常我们会怎么做呢?(1)发送写命令0x90 (2)发送控制命令0x40|3。但是在飞控上使用transfer()函数我们就不能这么做了,只需要
    uint8_t cmd = SET_CMD;
    ret = transfer(&cmd, 1, nullptr, 0);
    if (OK != ret) {
        perf_count(_comms_errors);
    }

其中SET_CMD=0x40|3,可见不需要给器件先发送写指令0x90,这是为什么呢?我们回到刚刚那段transfer()函数代码,其中有一句:

msgv[msgs].addr = get_device_address();

由此可见,每一次使用transfer()函数都会自动地先发送写和读指令,而地址的定义是在最前面:

#define I2C_ADDRESS 0x75    /* 7-bit address. 8-bit address is 0xEA */

看到没有这里的地址是7位!!! 这也是我上一篇文章中强调的。

3.2 measurea() 函数介绍

这个函数比较简单,我们还是先来看代码:

int
ETSAirspeed::measure()
{
    int ret;

    /*
     * Send the command to begin a measurement.
     */
    uint8_t cmd = READ_CMD;
    ret = transfer(&cmd, 1, nullptr, 0);

    if (OK != ret) {
        perf_count(_comms_errors);
    }

    return ret;
}

说过了transfer()函数,这段代码就比较容易了,就是如果你的传感器在读取数据之前需要发一些控制的指令,就放在这一段就好了。

3.3 collect()函数介绍

先看代码:

int
ETSAirspeed::collect()
{
    int ret = -EIO;

    /* read from the sensor */
    uint8_t val[2] = {0, 0};

    perf_begin(_sample_perf);

    ret = transfer(nullptr, 0, &val[0], 2);

    if (ret < 0) {
        perf_count(_comms_errors);
        return ret;
    }

    float diff_pres_pa_raw = (float)(val[1] << 8 | val[0]);

    differential_pressure_s report;
    report.timestamp = hrt_absolute_time();

    if (diff_pres_pa_raw < FLT_EPSILON) {
        // a zero value indicates no measurement
        // since the noise floor has been arbitrarily killed
        // it defeats our stuck sensor detection - the best we
        // can do is to output some numerical noise to show
        // that we are still correctly sampling.
        diff_pres_pa_raw = 0.001f * (report.timestamp & 0x01);
    }

    // The raw value still should be compensated for the known offset
    diff_pres_pa_raw -= _diff_pres_offset;

    report.error_count = perf_event_count(_comms_errors);

    // XXX we may want to smooth out the readings to remove noise.
    report.differential_pressure_filtered_pa = diff_pres_pa_raw;
    report.differential_pressure_raw_pa = diff_pres_pa_raw;
    report.temperature = -1000.0f;
    report.device_id = _device_id.devid;

    if (_airspeed_pub != nullptr && !(_pub_blocked)) {
        /* publish it */
        orb_publish(ORB_ID(differential_pressure), _airspeed_pub, &report);
    }

    ret = OK;

    perf_end(_sample_perf);

    return ret;
}

这段代码很长,但是其实很简单,我们需要了解的就一部分。首先定义了一个数组val[2],用来存放接收的数据,可见它的数据长度是16位,接收完了就是对数据的处理,这段我们就不需要了解了。

4、用于nsh调试的函数介绍

4.1 nsh调试介绍

我们在编写完代码之后除了编译,还需要调试看能否达到我们期望的结果。比如我们要添加以I2C通信机制的传感器,我们需要通过调试来看,能否得到传感器的数据。NuttShell和Unix终端命令类似。NSH通过串口或者USB转串口来与PX4FMU交互,因此可以使用类似超级终端的串口软件来与FMU交互,在Pixhawk开发中有多种nsh调试的软件,我使用的是QGroundControl里面携带的nsh调试工具。

4.2 nsh调试函数建立

我们还是先看代码:

/**
 * Local functions in support of the shell command.
 */
namespace ets_airspeed
{

ETSAirspeed *g_dev;

int start(int i2c_bus);
int stop();
int test();
int reset();
int info();

使用namespace定义了ets_airspeed这个类似于类的东西,这里面所定义的函数都可以在nsh调试中使用,比如在这段代码中定义了start(),stop(),test()等函数,这些函数通过主函数

ets_airspeed_main(int argc, char *argv[])

进行调用,这些函数具体什么功能以及如何实现,后面如果有时间我再进行介绍。下一篇应该也会涉及到这些内容。

好了,这篇文章就到这里了。简单分析了ets_airspeed.c这个代码的架构和一些函数,有很多不足还望多多指教。下一篇我将通过自己添加的角度传感器来谈谈,对于添加我们自己的传感器具体我们应该怎么做。


版权声明:本文为Enoch_klb原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
THE END
< <上一篇
下一篇>>