这一节详细讲解 TI CC13x0/CC26x0 SDK 开发平台,基于 TI-RTOS 的 SPI 驱动实现。主要了解 SPI 驱动的分层实现、驱动接口以及结合开发板板载 SPI Flash 调试。
SPI (Serial Perripheral Interface)串行外设接口,是 Motorala 公司推出的一种同步串行接口技术。它能够使 MCU 以全双工(数据能够同时进行双向传输)的同步串行方式与各种外围设备进行高速数据通信。
SPI 主要用在 EEPROM 、 Flash 、实时时钟( RTC )、数模转换器( ADC )、数字信号处理器( DSP )以及数字信号解码器之间。只占用芯片的 4 根管脚来实现控制和数据传输,现在很多芯片上也集成了 SPI 技术。SPI 有时也称为 SSI(同步串行接口),SPI 的控制方式采用主-从模式( Master - Slave )。
SPI 驱动程序能够驱动芯片与外围设备在 SPI 总线上进行数据的传输和接收。SPI 驱动程序接口是典型的 RTOS 应用程序调用接口,它们被重定向到 SPI_FxnTalble
中指针指定的特定驱动程序,增强了程序的移植性。
虽然在应用层直接调用几个驱动接口就可以在 SPI 总线上发送数据,但驱动程序内部从接口函数到底层硬件的操作是多层封装的。如图 1 所示是SPI驱动程序的分层实现图。
图1. SPI 驱动程序的分层实现
图 1 表明开发者只需要直接调用中间件层的驱动接口(例如:SPI_init、SPI_open 等等)就可以实现 SPI 的驱动功能。中间件层就是程序中的 SPI.c 和 SPI.h 所在层。
中间件层规范统一了应用程序的调用接口,对于 TI 不同类型的芯片平台在这一层给出的接口是一样的,应用层都是调用相同的接口来实现 SPI 功能。这样做的好处是增强了程序的可移植性,不管你的平台怎么换,应用程序都是不变的。
中间件层往下就是业务逻辑层,从业务逻辑层开始往下根据不同的芯片平台其接口封装实现就不尽相同了。以CC26XX芯片平台为例,业务逻辑层就位于 SPICC26XXDMA.c 和 SPICC26XXDMA.h 所在的层。
业务逻辑层采用 DMA 的数据传输方式,主要操控 DMA 进行数据传输以及调用驱动库中的一些函数实现相应功能。需要注意的是:这一层封装的驱动接口函数被全部放在一个函数指针结构体中。
如清单 1 所示,中间件层不直接调用这些驱动接口,而是通过一个配置文件( CC2640R2_LAUNCHXL.c )将装有驱动接口指针的结构体指针注册到 SPI_config 中。
如清单 2 所示,这样中间件层通过调用 SPI_config 中的结构体指针就可以指定使用业务逻辑层的驱动接口了。
清单1.业务逻辑层驱动接口指针结构体
const SPI_FxnTable SPICC26XXDMA_fxnTable = {
SPICC26XXDMA_close,
SPICC26XXDMA_control,
SPICC26XXDMA_init,
SPICC26XXDMA_open,
SPICC26XXDMA_transfer,
SPICC26XXDMA_transferCancel
};
清单2. SPI_config 中的驱动接口结构体指针注册
const SPI_Config SPI_config[CC2640R2_LAUNCHXL_SPICOUNT] = {
{
.fxnTablePtr = &SPICC26XXDMA_fxnTable,
.object = &spiCC26XXDMAObjects[CC2640R2_LAUNCHXL_SPI0],
.hwAttrs = &spiCC26XXDMAHWAttrs[CC2640R2_LAUNCHXL_SPI0]
},
{
.fxnTablePtr = &SPICC26XXDMA_fxnTable,
.object = &spiCC26XXDMAObjects[CC2640R2_LAUNCHXL_SPI1],
.hwAttrs = &spiCC26XXDMAHWAttrs[CC2640R2_LAUNCHXL_SPI1]
},
};
业务逻辑层再往下就是驱动库层( driver library ),业务逻辑层直接调用这一层的接口函数进行相应逻辑操作。驱动库层位于 ssi.c 和 ssi.h 所在的层,这一层就开始与硬件接触,进行相应寄存器操作来实现 SPI 驱动了。
之前已经提到 SPI 的配置数组 SPI_config[],它位于相应芯片平台的配置文件中。以 CC26XX 芯片平台为例,其配置文件为 CC2640R2_LAUNCHXL.c 。清单 3 是 CC2640R2_LAUNCHXL.c 中关于 SPI 的配置代码段。
清单3. UART 的配置代码段
/*
* =============================== SPI DMA ===============================
*/
#include <ti/drivers/SPI.h>
#include <ti/drivers/spi/SPICC26XXDMA.h>
SPICC26XXDMA_Object spiCC26XXDMAObjects[CC2640R2_LAUNCHXL_SPICOUNT];
const SPICC26XXDMA_HWAttrsV1 spiCC26XXDMAHWAttrs[CC2640R2_LAUNCHXL_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 = CC2640R2_LAUNCHXL_SPI0_MOSI,
.misoPin = CC2640R2_LAUNCHXL_SPI0_MISO,
.clkPin = CC2640R2_LAUNCHXL_SPI0_CLK,
.csnPin = CC2640R2_LAUNCHXL_SPI0_CSN
},
};
const SPI_Config SPI_config[CC2640R2_LAUNCHXL_SPICOUNT] = {
{
.fxnTablePtr = &SPICC26XXDMA_fxnTable,
.object = &spiCC26XXDMAObjects[CC2640R2_LAUNCHXL_SPI0],
.hwAttrs = &spiCC26XXDMAHWAttrs[CC2640R2_LAUNCHXL_SPI0]
},
};
const uint_least8_t SPI_count = CC2640R2_LAUNCHXL_SPICOUNT;
可以看到 SPI_config[] 数组中的元素有三个参数:分别是 .fxnTablePtr
、 .object
、 .hwAttrs
。
下面分别来看一下这三个参数的意义。
.fxnTablePtr
里面是驱动具体的实现函数,这些驱动函数就是来自业务逻辑层。它被赋值成 SPICC26XXDMA_fxnTable
的指针,这个结构体就是清单 1 中所示的业务逻辑层的驱动函数列表。在这里进行赋值配置之后,中间层的接口函数就可以链接使用它们了。.object
是用来存放 SPI 的各种参数数据的。例如控制参数、传输参数等。.hwAttrs
是用来存放 SPI 硬件配置参数的数组。如清单 3 所示,这些硬件参数需要在使用 SPI 之前需要配置好。在 SPI 的中间件文件 SPI.c 和 SPI.h 中,一共给出了 7 个接口函数。下面分别看一下每个接口函数的功能、形参、返回值以及注意事项。
void SPI_init(void)
void SPI_Params_init( SPI_Params *params )
params
需要进行初始化的 SPI_Params 结构体的指针清单4. 初始化 SPI_Params 结构体的默认值
/*!< 默认的 SPI parameters 结构体 */
const SPI_Params SPI_defaultParams = {
SPI_MODE_BLOCKING, /* transferMode */
SPI_WAIT_FOREVER, /* transferTimeout */
NULL, /* transferCallbackFxn */
SPI_MASTER, /* mode */
1000000, /* bitRate */
8, /* dataSize */
SPI_POL0_PHA0, /* frameFormat */
NULL /* custom */
};
SPI_Handle SPI_open(uint_least8_t index, SPI_Params *params)
index
编入 SPI_config 中的外设索引。params
指向 SPI_Params 参数块的指针,如果为 NULL 就使用默认值SPI_Handle
如果成功打开了 SPI 接口就返回此接口的参数配置数组(SPI_config)的句柄,如果发生错误则返回 NULLbool SPI_transfer(SPI_Handle handle, SPI_Transaction *transaction)
handle
SPI_open()中返回的句柄。transaction
指向传输数据结构bool
SPI_transfer 是否启动成功。启动成功返回 true,否则返回 falsevoid SPI_transferCancel(SPI_Handle handle)
handle
SPI_open() 中返回的句柄int_fast16_t SPI_control(SPI_Handle handle, uint_fast16_t cmd, void *controlArg)
handle
SPI_open()中返回的句柄。cmd
特定的处理命令(该命令可以来自 SPI.h 或者逻辑应用层的 .h 文件,例如 SPICC26XX.h )。 controlArg
伴随 cmd
的可选择的读写命令void SPI_close(SPI_Handle handle)
handle
SPI_open() 中返回的句柄下面调用 SPI 接口函数,将 SPI 设置为 Master,实现 SPI 的数据传输。
SPI_Handle spi;
SPI_Params spiParams;
SPI_Transaction spiTransaction;
uint8_t transmitBuffer[MSGSIZE];
uint8_t receiveBuffer[MSGSIZE];
bool transferOK;
SPI_init(); // Initialize the SPI driver
SPI_Params_init(&spiParams); // Initialize SPI parameters
spiParams.dataSize = 8; // 8-bit data size
spi = SPI_open(Board_SPI0, &spiParams);
if (spi == NULL) {
while (1); // SPI_open() failed
}
// Fill in transmitBuffer
spiTransaction.count = MSGSIZE;
spiTransaction.txBuf = transmitBuffer;
spiTransaction.rxBuf = receiveBuffer;
transferOK = SPI_transfer(spi, &spiTransaction);
if (!transferOK) {
// Error in SPI or transfer already in progress.
}
SPI_init()
,利用 SPI_config 中的配置参数对 SPI 进行初始化。这一步函数的调用必须要在其他 SPI 接口函数调用之前进行,且 SPI_config 中的配置参数已经设置完成。SPI_Params_init()
将 SPI_Params 中的参数全部初始化为默认值,这些默认值在该接口函数说明中已经给出。SPI_open()
打开对应的 SPI 外设接口SPI_Transaction
进行赋值,设置数据接收和传输缓冲区、以及传输事务的帧数( MSGSIZE )。SPI_transfer()
开始进行数据传输。结构体 SPI_Transaction
中有两个参数,分别是 *txBuf
(输入缓冲区)和 *rxBuf
(输出缓冲区)。当调用 SPI_transfer()
进行数据输出的时候,只需要将数据放入 *txBuf
中,调用函数之后数据就会传输出去。当调用 SPI_transfer()
进行数据接收的时候更加方便,调用函数之后,接收到的数据直接就会放入 *rxBuf
中。至此就完成了 SPI 接口的打开与数据传输。static bool Spi_Open(uint32_t bitRate) {
SPI_Params spiParams;
/* Configure SPI as master */
SPI_Params_init(&spiParams);
spiParams.bitRate = bitRate;
spiParams.mode = SPI_MASTER;
spiParams.transferMode = SPI_MODE_BLOCKING;
/* Attempt to open SPI. */
spiHandle = SPI_open(Board_SPI0, &spiParams);
return spiHandle != NULL;
}
static bool Spi_Write(const uint8_t *buf, size_t len) {
SPI_Transaction masterTransaction;
masterTransaction.count = len;
masterTransaction.txBuf = (void*)buf;
masterTransaction.arg = NULL;
masterTransaction.rxBuf = NULL;
return SPI_transfer(spiHandle, &masterTransaction) ? 1 : 0;
}
static bool Spi_Read(uint8_t *buf, size_t len) {
SPI_Transaction masterTransaction;
masterTransaction.count = len;
masterTransaction.rxBuf = buf;
masterTransaction.txBuf = NULL;
masterTransaction.arg = NULL;
return SPI_transfer(spiHandle, &masterTransaction) ? 1 : 0;
}
芯片与外围设备之间的 SPI 接口是通过 4 个管脚连接进行控制和数据交换的,如果想采用 SPI 进行数据传输就必须有效控制这些管脚,打通数据交换链路。其次,要根据外围设备自己的特性进行相关处理命令控制以读取写入数据。
图 2 展示了数据通过 SPI 在 Master 和 Slave 之间的传输示意图。
图2. Master 和 Slave 数据通过 SPI 传输过程
调用 SPI 接口已经能够驱动芯片进行数据传输了,下面来讲讲要使 Flash 能够被写入和读出数据需要做哪些设置。
以成都乐控畅联科技有限公司自主研发的 CC13X0/CC26X0 Evaluation Board 上搭载的 SPI Flash W25Q80BV 为例来进行讲解如何读出其 Manufacturer ID 和 Device ID 。你需要下载 W25Q80BV 的 datasheet ,上面关于 W25Q80BV 的很多信息对开发至关重要。
W25Q80BV 的 datasheet 管脚封装图如图 3 所示。
图3. 管脚封装图
它与 CC2640R2F 之间的硬件连接已按照图 2 的原理连接好。管脚 1 是 CS 端,用于使能选中该 Flash。在每次传输数据的时候首先需要将该管脚置为低电平来使能选中该 Flash,不进行数传输的时候置为高电平,使其失能。
在示例程序中,清单 1 和清单 2 就是在做这件事。
清单1.使能 Flash
static void flashSelect(void){
PIN_setOutputValue(hFlashPin,Board_SPI_FLASH_CS,Board_FLASH_CS_ON);
}
清单2.使能 Flash
static void flashDeSelect(void){
PIN_setOutputValue(hFlashPin,Board_SPI_FLASH_CS,Board_FLASH_CS_OFF);
}
阅读 W25Q80BV 的 datasheet 了解到:如果进行读写操作是要写入相应指令的,每条指令都有其时序图。表 1 列出了部分指令,完整的指令集请查看其 datasheet 。
表1. W25Q80BV 部分指令
指令名 | 指令码 |
---|---|
写使能 | 06h |
写失能 | 04h |
写状态寄存器 | 01h |
页编程 | 02h |
扇区擦除 | 20h |
读数据 | 03h |
读Manufacturer/Device ID | 90h |
清单 3 展示了程序中对这些指令的宏定义。
清单3. W25Q80BV 部分指令的宏定义
#define FLASH_WRITE_ENABLE 0x06
#define FLASH_WRITE_DISABLE 0x04
#define FLASH_WRITE_STATUS_REG 0x01
#define FLASH_WRITE_DATA 0x02
#define FLASH_SECTOR_ERASE 0x20
#define FLASH_READ_DATA 0x03
#define FLASH_READ_MANUDEVID 0x90
看完指令再来看看相应指令的时序图。
图4.写使能时序图
图5.写使能时序图
图6.读数据时序图
图7.页编程时序图
图8.读 readManufacturerDeviceID 时序图
如果想读出该 Flash 的 Manufacturer/Device ID ,要进行以下步骤:
清单4.写使能函数
static bool Flash_WriteEnable(void) {
bool flag;
const uint8_t comAdd[] = {FLASH_WRITE_ENABLE};//!< 将指令“写使能”放入数组中
FlashSelect();
flag = Spi_Write(comAdd,sizeof(comAdd));//!< 调用spi_write写入指令
FlashDeSelect();
return flag;
}
清单 5 展示了将读 Manufacturer/Device ID 指令和地址按时序写入,读出 Manufacturer/Device ID 的代码实现
清单5. 读 Manufacturer/Device ID
static bool Flash_GetInfo(void) {
bool flag;
const uint8_t comAdd[] = {FLASH_READ_MANUDEVID, 0x00, 0x00, 0x00};//!< 将读 Manufacturer/Device ID 指令以及地址放在数组中待写入
FlashSelect();//!< 选中使能 Flash
flag = Spi_Write(comAdd, sizeof(comAdd));//!< 调用 spi_write 写入指令和地址
if(!flag) {
FlashDeSelect();
return false;
}
flag = Spi_Read(devInfoBuf, sizeof(devInfoBuf));//!< 调用 spi_read 读出 Manufacturer/Device ID
FlashDeSelect();//!< 使能 Flash
return flag;
}
完整的代码实现文件可以在 SPIFlash.c 中查看。编译程序时利用 CC13X0/CC26X0 Evaluation Board 下载到 CC2640R2F 模块中中,然后利用逻辑分析仪和串口工具查看对 Flash 的读写效果。
C:\ti\simplelink_cc2640r2_sdk_1_35_00_33\examples\rtos\CC2640R2_LAUNCHXL\drivers\uartecho
中C:\ti\simplelink_cc2640r2_sdk_1_35_00_33\examples\rtos\CC2640R2_LAUNCHXL\drivers\uartecho\tirtos\iar
文件夹下打开 uartecho.eww
IAR 工程,可以看到如图 9 所示的工程目录。图9. IAR 工程目录
uartcho.c
文件,点击右键,选择 remove ,这时 uartcho.c
文件被移出工程。source files
文件夹,选择 Add 条目下的 Add Files... ,然后将 SPIFlash.c
添加进工程项目,如图 10 所示。图10. 将文件添加进 IAR 工程目录
uartecho-Debug
点击右键,选择 Rebuild All
编译工程。图11. Manufacturer/Device ID 被打印出来
图12. 逻辑分析仪的通道连接
图13. SPI 接口的配置
图14. 逻辑分析仪的配置界面
图15. Start 界面
图16. 弹出的界面
图17. Flash 输入数据、读出数据的时序图
文章所有代码、工具、文档开源。加入我们QQ群 591679055获取更多支持,共同研究CC2640R2F&BLE5.0。