用户工具

站点工具


侧边栏

CC2640R2&BLE5.0开发

关于我们

入门开始

视频教程

外设驱动

开发BLE应用

工具集

其他

cc2640r2f:peripheral_driver:spi:spi

SPI 驱动

这一节详细讲解 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 驱动的分层实现

虽然在应用层直接调用几个驱动接口就可以在 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 的配置数组 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
    .fxnTablePtr 里面是驱动具体的实现函数,这些驱动函数就是来自业务逻辑层。它被赋值成 SPICC26XXDMA_fxnTable 的指针,这个结构体就是清单 1 中所示的业务逻辑层的驱动函数列表。在这里进行赋值配置之后,中间层的接口函数就可以链接使用它们了。
  • .object
    .object 是用来存放 SPI 的各种参数数据的。例如控制参数、传输参数等。
  • .hwAttrs
    .hwAttrs 是用来存放 SPI 硬件配置参数的数组。如清单 3 所示,这些硬件参数需要在使用 SPI 之前需要配置好。

SPI 驱动接口

在 SPI 的中间件文件 SPI.c 和 SPI.h 中,一共给出了 7 个接口函数。下面分别看一下每个接口函数的功能、形参、返回值以及注意事项。

  1. void SPI_init(void)
  • 函数功能:利用配置文件中设置的配置参数初始化 SPI 模块
  • 形参:无
  • 返回值:无
  • 注意事项:在调用此函数之前 SPI_config 结构体中的参数必须配置好,此函数必须先于其他任何 SPI 接口函数调用。
  1. void SPI_Params_init( SPI_Params *params )
  • 函数功能:将 SPI_Params 结构体中的参数初始化为默认值
  • 形参:params 需要进行初始化的 SPI_Params 结构体的指针
  • 返回值:无
  • 注意事项:初始化 SPI_Params 结构体的默认值如清单 4 所示

清单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 */
};
  1. SPI_Handle SPI_open(uint_least8_t index, SPI_Params *params)
  • 函数功能:打开给定的 SPI 外设
  • 形参:index 编入 SPI_config 中的外设索引。params 指向 SPI_Params 参数块的指针,如果为 NULL 就使用默认值
  • 返回值:SPI_Handle 如果成功打开了 SPI 接口就返回此接口的参数配置数组(SPI_config)的句柄,如果发生错误则返回 NULL
  • 注意事项:在调用 SPI_open()之前,应先调用 SPI_init()进行了初始化。
  1. bool SPI_transfer(SPI_Handle handle, SPI_Transaction *transaction)
  • 函数功能:执行 SPI 事务,发送/接收数据
  • 形参:handle SPI_open()中返回的句柄。transaction 指向传输数据结构
  • 返回值:bool SPI_transfer 是否启动成功。启动成功返回 true,否则返回 false
  • 注意事项:执行 SPI 事务有两种模式: SPI_MODE_BLOCKING SPI_MODE_CALLBACK 。在 SPI_MODE_BLOCKING 模式下, SPI_transfer 将阻止任务的执行,直达事务完成。在 SPI_MODE_CALLBACK 模式下,SPI_transfer 会调用 SPI_CallbackFxn ,不会阻止任务执行。
  1. void SPI_transferCancel(SPI_Handle handle)
  • 函数功能:取消执行 SPI 事务
  • 形参:handle SPI_open() 中返回的句柄
  • 返回值:无
  • 注意事项:在 SPI_MODE_BLOCKING 中,SPI_transferCancel()不起作用。在 SPI_MODE_CALLBACK 中,如果 SPI_transfer()正在进行中,SPI_transferCancel()将停止 SPI 传输。
  1. int_fast16_t SPI_control(SPI_Handle handle, uint_fast16_t cmd, void *controlArg)
  • 函数功能:在给定的 SPI 接口上执行特定的操作
  • 形参:handle SPI_open()中返回的句柄。cmd 特定的处理命令(该命令可以来自 SPI.h 或者逻辑应用层的 .h 文件,例如 SPICC26XX.h )。 controlArg 伴随 cmd 的可选择的读写命令
  • 返回值:不同命令处理的特定返回值,如果为负值表示操作不成功
  • 注意事项:在调用该函数之前必须先调用 SPI_open()
  1. void SPI_close(SPI_Handle handle)
  • 函数功能:关闭指定的 SPI 外设接口
  • 形参:handle SPI_open() 中返回的句柄
  • 返回值:无
  • 注意事项:在调用该函数之前,必须先调用 SPI_close()

SPI 数据传输的程序实现

下面调用 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.
}
  1. 可以看到程序首先调用 SPI_init(),利用 SPI_config 中的配置参数对 SPI 进行初始化。这一步函数的调用必须要在其他 SPI 接口函数调用之前进行,且 SPI_config 中的配置参数已经设置完成。
  2. 调用 SPI_Params_init() 将 SPI_Params 中的参数全部初始化为默认值,这些默认值在该接口函数说明中已经给出。
  3. 各自应用的 SPI_Params 结构体中的参数值可能和默认值不同,接下来可以对它们进行相应的修改。
  4. 调用 SPI_open() 打开对应的 SPI 外设接口
  5. 对数据传输结构体 SPI_Transaction 进行赋值,设置数据接收和传输缓冲区、以及传输事务的帧数( MSGSIZE )。
  6. 调用 SPI_transfer() 开始进行数据传输。结构体 SPI_Transaction 中有两个参数,分别是 *txBuf (输入缓冲区)和 *rxBuf (输出缓冲区)。当调用 SPI_transfer() 进行数据输出的时候,只需要将数据放入 *txBuf 中,调用函数之后数据就会传输出去。当调用 SPI_transfer() 进行数据接收的时候更加方便,调用函数之后,接收到的数据直接就会放入 *rxBuf 中。至此就完成了 SPI 接口的打开与数据传输。
  7. 根据以上接口的调用在例程 SPIFlash.c 中又写了一下函数,有了上面的调用实现,下面的代码应该不难理解。
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 从 Flash 中读取数据示例

芯片与外围设备之间的 SPI 接口是通过 4 个管脚连接进行控制和数据交换的,如果想采用 SPI 进行数据传输就必须有效控制这些管脚,打通数据交换链路。其次,要根据外围设备自己的特性进行相关处理命令控制以读取写入数据。

图 2 展示了数据通过 SPI 在 Master 和 Slave 之间的传输示意图。

图2. Master 和 Slave 数据通过 SPI 传输过程

  • SS/CS ( Slave Select/Chip Select ), 用于 Master 设备片选Slave设备, 使被选中的 Slave 设备能够被 Master 设备所访问;
  • SCK ( Serial Clock ),用于 Master 设备往 Slave 设备传输时钟信号, 控制数据交换的时机以及速率;
  • SDO/MOSI ( Serial Data Output/Master Out Slave In )在 Master 上面也被称为 Tx-Channel ,作为数据的出口,主要用于 SPI 设备发送数据;
  • SDI/MISO ( Serial Data Input/Master In Slave Out )在 Master 上面也被称为 Rx-Channel ,作为数据的入口,主要用于 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 ,要进行以下步骤:

  1. 写入写使能指令,保证可以向 Flash 中写指令和数据。清单 4 是这一步的代码实现。

清单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;
}
  1. 写入读 Manufacturer/Device ID 指令。
  2. 写入地址 000000h 。这在图 8 读 readManufacturerDeviceID 时序图中可以看到,在写入读 Manufacturer/Device ID 指令后,紧接着有 3 个字节的地址( Address ) 000000h 需要写入。
  3. 读取两个字节的 Manufacturer/Device ID 。这在图 8 的时序图中也可以看到,在指令和地址写入之后,后面紧接着的两个字节就是 Manufacturer/Device ID 。

清单 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 的读写效果。

  1. 将源文件 SPIFlash.c 放入 SDK 安装目录 C:\ti\simplelink_cc2640r2_sdk_1_35_00_33\examples\rtos\CC2640R2_LAUNCHXL\drivers\uartecho
  2. C:\ti\simplelink_cc2640r2_sdk_1_35_00_33\examples\rtos\CC2640R2_LAUNCHXL\drivers\uartecho\tirtos\iar 文件夹下打开 uartecho.eww IAR 工程,可以看到如图 9 所示的工程目录。

图9. IAR 工程目录

  1. 选中 uartcho.c 文件,点击右键,选择 remove ,这时 uartcho.c 文件被移出工程。
  2. 在工程目录下选中 source files 文件夹,选择 Add 条目下的 Add Files... ,然后将 SPIFlash.c 添加进工程项目,如图 10 所示。

图10. 将文件添加进 IAR 工程目录

  1. 选中工程文件 uartecho-Debug 点击右键,选择 Rebuild All 编译工程。
  2. 保证已经下载了蓝牙协议栈镜像文件的调试板接入电脑,选择 Project -> Download -> Download Active Application 。如图 10 所示将程序下载到调试板中。
  3. 打开串口调试助手,连接上对应的端口,在 CC13X0/CC26X0 Evaluation Board 上按下复位键,可以看到 Manufacturer/Device ID 被打印到串口调试工具上,如图 11 所示。

图11. Manufacturer/Device ID 被打印出来

  1. 如图 12 所示,将逻辑分析仪的各个通道分别连接 CC13X0/CC26X0 Evaluation Board 上对应的 SPI 四个端口 SS/CS (DIO20)、 SCK (DIO10)、 SDO/MOSI (DIO9)、 SDI/MISO (DIO8),对于具体是哪几个 IO 口可以在文档 CC13X0CC26X0EvaluationBoard 中查看。

图12. 逻辑分析仪的通道连接

  1. 打开逻辑分析仪( Saleae Logic 1.1.15 ),首先配置好 SPI 接口,软件右侧 Analyzers 下的 SPI 选项下配置,如图13所示。

图13. SPI 接口的配置

  1. 选择之后你可以看到如图 14 所示的配置界面,将各个端口对应的通道配置好,点击 Save 保存。

图14. 逻辑分析仪的配置界面

  1. 如图 15 所示点击界面上方的 Start 按钮,会弹出如图 16 所示的界面。

图15. Start 界面

图16. 弹出的界面

  1. CC13X0/CC26X0 Evaluation Board 上按下复位键,点击图 16 所示界面的 stop 按钮。
  2. 这样就可以看到向 Flash 输入数据、读出数据的时序图。如图17所示,输入的指令与程序是完全符合的,读出 Manufacturer/Device ID 与串口打印的结果也是完全符合的。

图17. Flash 输入数据、读出数据的时序图

加入我们

文章所有代码、工具、文档开源。加入我们QQ群 591679055获取更多支持,共同研究CC2640R2F&BLE5.0。

CC2640R2F&BLE5.0-乐控畅联 © Copyright 2017, 成都乐控畅联科技有限公司.

cc2640r2f/peripheral_driver/spi/spi.txt · 最后更改: 2017/09/09 17:03 由 wuyl