连接真实世界的传感器

对于 BLE 从设备执行任何有用的工作,无线 MCU 的 GPIO 几乎总是参与其中。例如,要从外部传感器读取温度,可能需要 GPIO 引脚的 ADC 功能。TI 的 CC2640 MCU 具有最多 31 个 GPIO,具有不同的封装类型。

在硬件方面,CC2640 提供丰富的外设功能,如 ADC,UARTS,SPI,SSI,I2C 等。在软件方面,TI 的 BLE 堆栈试图为不同的外设提供统一的独立于器件的驱动器接口。统一的驱动程序接口可以提高代码重用性的可能性,但另一方面,它也会增加学习曲线的斜率。在本文中,我们以 SPI 控制器为例,说明如何将软件驱动程序集成到用户应用程序中。

基本 SPI 驱动程序流程

在 TI 的 BLE 堆栈中,外设驱动程序通常由三部分组成:独立于设备的驱动程序 API 规范; 驱动程序 API 的设备特定实现和硬件资源的映射。

对于 SPI 控制器,其驱动程序实现涉及三个文件:

  • <ti / drivers / SPI.h> - 这是与设备无关的 API 规范
  • <ti / drivers / spi / SPICC26XXDMA.h> - 这是 CC2640 特定的 API 实现
  • <ti / drivers / dma / UDMACC26XX.h> - 这是 SPI 驱动程序所需的 uDMA 驱动程序

(注意:TI BLE 堆栈外设驱动程序的最佳文档大多可以在其头文件中找到,例如本例中的 SPICC26XXDMA.h)

要开始使用 SPI 控制器,我们首先创建一个自定义 c 文件,即 sbp_spi.c,其中包含上面的三个头文件。自然的下一步是创建驱动程序的实例并启动它。驱动程序实例封装在数据结构中 –SPI_Handle。另一种数据结构 –SPI_Params 用于指定 SPI 控制器的关键参数,如比特率,传输模式等。

#include <ti/drivers/SPI.h>
#include <ti/drivers/spi/SPICC26XXDMA.h>
#include <ti/drivers/dma/UDMACC26XX.h>

static void sbp_spiInit();

static SPI_Handle spiHandle;
static SPI_Params spiParams;

void sbp_spiInit(){
    SPI_init();
    SPI_Params_init(&spiParams);
    spiParams.mode                     = SPI_MASTER;
    spiParams.transferMode             = SPI_MODE_CALLBACK;
    spiParams.transferCallbackFxn      = sbp_spiCallback;
    spiParams.bitRate                  = 800000;
    spiParams.frameFormat              = SPI_POL0_PHA0;
    spiHandle = SPI_open(CC2650DK_7ID_SPI0, &spiParams);
}

上面的示例代码举例说明了如何初始化 SPI_Handle 实例。必须首先调用 API SPI_init() 来初始化内部数据结构。函数调用 SPI_Params_init(&spiParams)将 SPI_Params 结构的所有字段设置为默认值。然后开发人员可以修改关键参数以适应其特定情况。例如,上面的代码将 SPI 控制器设置为主模式,比特率为 800kbps,并使用非阻塞方法处理每个事务,这样当事务完成时,将调用回调函数 sbp_spiCallback。

最后,调用 SPI_open() 会打开硬件 SPI 控制器并返回一个句柄,以便以后进行 SPI 事务处理。SPI_open() 有两个参数,第一个是 SPI 控制器的 ID。CC2640 具有片上两个硬件 SPI 控制器,因此该 ID 参数将为 0 或 1,如下所述。第二个参数是 SPI 控制器的所需参数。

/*!
 *  @def    CC2650DK_7ID_SPIName
 *  @brief  Enum of SPI names on the CC2650 dev board
 */
typedef enum CC2650DK_7ID_SPIName {
    CC2650DK_7ID_SPI0 = 0,
    CC2650DK_7ID_SPI1,
    CC2650DK_7ID_SPICOUNT
} CC2650DK_7ID_SPIName;

成功打开 SPI_Handle 后,开发人员可以立即启动 SPI 事务。使用数据结构 - SPI_Transaction 描述每个 SPI 事务。

/*!
 *  @brief
 *  A ::SPI_Transaction data structure is used with SPI_transfer(). It indicates
 *  how many ::SPI_FrameFormat frames are sent and received from the buffers
 *  pointed to txBuf and rxBuf.
 *  The arg variable is an user-definable argument which gets passed to the
 *  ::SPI_CallbackFxn when the SPI driver is in ::SPI_MODE_CALLBACK.
 */
typedef struct SPI_Transaction {
    /* User input (write-only) fields */
    size_t     count;      /*!< Number of frames for this transaction */
    void      *txBuf;      /*!< void * to a buffer with data to be transmitted */
    void      *rxBuf;      /*!< void * to a buffer to receive data */
    void      *arg;        /*!< Argument to be passed to the callback function */

    /* User output (read-only) fields */
    SPI_Status status;     /*!< Status code set by SPI_transfer */

    /* Driver-use only fields */
} SPI_Transaction;

例如,要在 SPI 总线上启动写事务,开发人员需要准备一个填充了要传输的数据的’txBuf’,并将’count’变量设置为要发送的数据字节的长度。最后,调用 SPI_transfer(spiHandle, spiTrans)向 SPI 控制器发出信号以启动事务。

static SPI_Transaction spiTrans;
bool sbp_spiTransfer(uint8_t len, uint8_t * txBuf, uint8_t rxBuf, uint8_t * args)
{    
    spiTrans.count = len;
    spiTrans.txBuf = txBuf;
    spiTrans.rxBuf = rxBuf;
    spiTrans.arg   = args;

    return SPI_transfer(spiHandle, &spiTrans);
}

由于 SPI 是一种双工协议,发送和接收同时发生,因此当写事务完成时,其相应的响应数据已在’rxBuf’中可用。

由于我们将传输模式设置为回调模式,因此每当事务完成时,将调用已注册的回调函数。这是我们处理响应数据或启动下一个事务的地方。 (注意:永远记住不要在回调函数内做更多必要的 API 调用)。

void sbp_spiCallback(SPI_Handle handle, SPI_Transaction * transaction){
    uint8_t * args = (uint8_t *)transaction->arg;
    
    // may want to disable the interrupt first
    key = Hwi_disable();    
    if(transaction->status == SPI_TRANSFER_COMPLETED){
        // do something here for successful transaction...
    }
    Hwi_restore(key);
}

I / O 引脚配置

到目前为止,使用 SPI 驱动程序似乎相当简单。但是等等,如何将软件 API 调用连接到物理 SPI 信号?这是通过三种数据结构完成的:SPICC26XXDMA_Object,SPICC26XXDMA_HWAttrsV1 和 SPI_Config。它们通常在’board.c’之类的不同位置实例化。

/* SPI objects */
SPICC26XXDMA_Object spiCC26XXDMAObjects[CC2650DK_7ID_SPICOUNT];

/* SPI configuration structure, describing which pins are to be used */
const SPICC26XXDMA_HWAttrsV1 spiCC26XXDMAHWAttrs[CC2650DK_7ID_SPICOUNT] = {
    {
        .baseAddr           = SSI0_BASE,
        .intNum             = INT_SSI0_COMB,
        .intPriority        = ~0,
        .swiPriority        = 0,
        .powerMngrId        = PowerCC26XX_PERIPH_SSI0,
        .defaultTxBufValue  = 0,
        .rxChannelBitMask   = 1<<UDMA_CHAN_SSI0_RX,
        .txChannelBitMask   = 1<<UDMA_CHAN_SSI0_TX,
        .mosiPin            = ADC_MOSI_0,
        .misoPin            = ADC_MISO_0,
        .clkPin             = ADC_SCK_0,
        .csnPin             = ADC_CSN_0
    },
    {
        .baseAddr           = SSI1_BASE,
        .intNum             = INT_SSI1_COMB,
        .intPriority        = ~0,
        .swiPriority        = 0,
        .powerMngrId        = PowerCC26XX_PERIPH_SSI1,
        .defaultTxBufValue  = 0,
        .rxChannelBitMask   = 1<<UDMA_CHAN_SSI1_RX,
        .txChannelBitMask   = 1<<UDMA_CHAN_SSI1_TX,
        .mosiPin            = ADC_MOSI_1,
        .misoPin            = ADC_MISO_1,
        .clkPin             = ADC_SCK_1,
        .csnPin             = ADC_CSN_1
    }
};

/* SPI configuration structure */
const SPI_Config SPI_config[] = {
    {
         .fxnTablePtr = &SPICC26XXDMA_fxnTable,
         .object      = &spiCC26XXDMAObjects[0],
         .hwAttrs     = &spiCC26XXDMAHWAttrs[0]
    },
    {
         .fxnTablePtr = &SPICC26XXDMA_fxnTable,
         .object      = &spiCC26XXDMAObjects[1],
         .hwAttrs     = &spiCC26XXDMAHWAttrs[1]
    },
    {NULL, NULL, NULL}
};

SPI_Config 阵列为每个硬件 SPI 控制器都有一个单独的条目。每个条目都有三个字段:fxnTablePtr,object 和 hwAttrs。 ‘fxnTablePtr’是一个点表,指向驱动程序 API 的特定于设备的实现。

对象跟踪驱动程序状态,传输模式,驱动程序的回调函数等信息。这个对象由驱动程序自动维护。

‘hwAttrs’存储实际的硬件资源映射数据,例如 SPI 信号的 IO 引脚,硬件中断号,SPI 控制器的基地址等 .‘hwAttrs’的大多数字段是预定义的,不能修改。而接口的 IO 引脚可以根据用户情况自由分配。注意:CC26XX MCU 将 IO 引脚与特定外设功能分离,任何 IO 引脚都可以分配给任何外设功能。

当然,必须首先在’board.h’中定义实际的 IO 引脚。

#define ADC_CSN_1                             IOID_1
#define ADC_SCK_1                             IOID_2
#define ADC_MISO_1                            IOID_3
#define ADC_MOSI_1                            IOID_4
#define ADC_CSN_0                             IOID_5
#define ADC_SCK_0                             IOID_6
#define ADC_MISO_0                            IOID_7
#define ADC_MOSI_0                            IOID_8

因此,在配置硬件资源映射后,开发人员最终可以通过 SPI 接口与外部传感器芯片进行通信。