連線真實世界的感測器

對於 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 介面與外部感測器晶片進行通訊。