連線真實世界的感測器
對於 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 介面與外部感測器晶片進行通訊。