连接真实世界的传感器
对于 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 接口与外部传感器芯片进行通信。