RT-Thread学习笔记【通用总线SPI与IIC】

SPI设备

RTT的SPI设备驱动遵循SPI协议进行编写,一般为四线SPI(MOSI、MISO、CS、SCLK)或四线DSPI模式或六线QSPI模式

SPI工作在全双工模式,MISO和MOSI同时发送数据,一般在设备内部采用移位寄存器的方式实现;DSPI工作在半双工模式,MOSI和MISO换成SIO0、SIO1,在一个时钟周期内传输两个比特数据;QSPI也工作在半双工模式,设置SIO0、SIO1、SIO2、SIO3四条数据信号线和SCLK、CS两条控制信号线,在同一时钟周期内能传输4个比特数据

在相同时钟下,线数越多传输速率越高

SPI设备挂载和配置

使用SPI驱动程序将SPI总线注册,SPI设备需要挂载到已经注册好的SPI总线上,使用以下API将一个SPI设备挂载到指定的总线

rt_err_t rt_spi_bus_attach_device(struct rt_spi_device *device,//SPI设备句柄
                                  const char *name,//设备名
                                  const char *bus_name,//总线名
                                  void *user_data)//用户数据指针

user_data会被自动保存到对应SPI设备的设备控制块中

SPI总线命名为spix,SPI设备命名为spixy,user_data一般设为SPI设备的CS引脚指针,比如spi10表示挂载到spi1总线上的0号设备

针对不同设备可能API有所不同,如使用stm32对应的bsp

rt_hw_spi_device_attach(const char *bus_name, const char *device_name, GPIO_TypeDef* cs_gpiox, uint16_t cs_gpio_pin)

使用例如下:将SPI FLASH作为0号设备挂载到SPI1总线

static int rt_hw_spi_flash_init(void)
{
    __HAL_RCC_GPIOB_CLK_ENABLE();
    rt_hw_spi_device_attach("spi1", "spi10", GPIOB, GPIO_PIN_14);

    if (RT_NULL == rt_sfud_flash_probe("W25Q128", "spi10"))
        return -RT_ERROR;
    return RT_EOK;
}

使用以下API配置SPI设备的参数

rt_err_t rt_spi_configure(struct rt_spi_device *device,//SPI设备句柄
                          struct rt_spi_configuration *cfg)//SPI配置参数指针

//配置结构体如下
struct rt_spi_configuration
{
    rt_uint8_t mode; /* 模式:从下面的宏定义中选择,可以使用位与连接 */
    rt_uint8_t data_width; /* 数据宽度:可取8位、16位、32位 */
    rt_uint16_t reserved; /* 保留位 */
    rt_uint32_t max_hz; /* 最大频率:单位Hz */
};

//可使用【模式】设置如下
/* 设置数据传输顺序是MSB位在前还是LSB位在前 */
#define RT_SPI_LSB      (0<<2)                        /* bit[2]: 0-LSB */
#define RT_SPI_MSB      (1<<2)                        /* bit[2]: 1-MSB */

/* 设置SPI的主从模式 */
#define RT_SPI_MASTER   (0<<3)                        /* SPI主设备模式 */
#define RT_SPI_SLAVE    (1<<3)                        /* SPI从设备模式 */

/* 设置时钟极性和时钟相位 */
#define RT_SPI_MODE_0   (0 | 0)                       /* CPOL = 0, CPHA = 0 */
#define RT_SPI_MODE_1   (0 | RT_SPI_CPHA)             /* CPOL = 0, CPHA = 1 */
#define RT_SPI_MODE_2   (RT_SPI_CPOL | 0)             /* CPOL = 1, CPHA = 0 */
#define RT_SPI_MODE_3   (RT_SPI_CPOL | RT_SPI_CPHA)   /* CPOL = 1, CPHA = 1 */

#define RT_SPI_CS_HIGH  (1<<4)                        /* 片选信号高电平有效 */
#define RT_SPI_NO_CS    (1<<5)                        /* 无片选信号 */
#define RT_SPI_3WIRE    (1<<6)                        /* 3线SPI模式 */
#define RT_SPI_READY    (1<<7)                        /* 从设备发送低电平信号表示暂停 */

QSPI

QSPI协议一般用于片外SPI FLASH、SRAM等的操作

使用以下API配置QSPI设备的传输参数

rt_err_t rt_qspi_configure(struct rt_qspi_device *device,//QSPI设备句柄
                           struct rt_qspi_configuration *cfg);//QSPI配置参数指针

//QSPI参数配置结构体如下
struct rt_qspi_configuration
{
    struct rt_spi_configuration parent; /* 继承自 SPI 设备配置参数 */
    rt_uint32_t medium_size; /* 介质大小:外设存储器大小 */
    rt_uint8_t ddr_mode; /* 双倍速率模式 */
    rt_uint8_t qspi_dl_width ; /* QSPI总线位宽,单线模式 1 位、双线模式 2 位,4 线模式 4 位 */
};

访问SPI设备

注意:SPI数据传输相关接口会调用rt_mutex_take()(使用了互斥量),所以不能在中断服务程序中调用SPI传输相关API,否则会导致assertion报错

查找SPI设备

使用以下API通过设备名获取SPI设备句柄

rt_device_t rt_device_find(const char* name);

使用例如下:

#define W25Q_SPI_DEVICE_NAME     "qspi10"   /* SPI 设备名称 */
struct rt_spi_device *spi_dev_w25q;     /* SPI 设备句柄 */

/* 查找 spi 设备获取设备句柄 */
spi_dev_w25q = (struct rt_spi_device *)rt_device_find(W25Q_SPI_DEVICE_NAME);

自定义传输数据

通过以下API传输SPI数据

struct rt_spi_message *rt_spi_transfer_message(struct rt_spi_device *device,//设备句柄
                                               struct rt_spi_message *message)//SPI消息结构体指针
    
struct rt_spi_message
{
    const void *send_buf;           /* 发送缓冲区指针 */
    void *recv_buf;                 /* 接收缓冲区指针 */
    rt_size_t length;               /* 发送 / 接收 数据字节数,单位 word */
    struct rt_spi_message *next;    /* 指向继续发送的下一条消息的指针(消息链表的指针域) */
    unsigned cs_take    : 1;        /* 片选选中 */
    unsigned cs_release : 1;        /* 释放片选 */
};
//当send_buf=RT_NULL时,表示本次传输位只接收状态,不发送数据
//当recv_buf=RT_NULL时,表示本次传输位只发送状态,会将收到的数据直接丢弃
//如果只发送一条消息,应将next=NULL
//cs_take为1时表示传输数据前,设置对应的CS为有效状态;cs_release为1时表示传输数据结束后,释放对应的CS

注意:当send_buf或recv_buf不为空时,两者的可用空间必须大于等于length

一般传输第一条消息的cs_take和最后一条消息的cs_release要置1,中间的消息的cs_take和cs_release都置0

使用例如下:

/* 查找 spi 设备获取设备句柄 */
spi_dev_w25q = (struct rt_spi_device *)rt_device_find(W25Q_SPI_DEVICE_NAME);
struct rt_spi_message msg1, msg2;//实例化两个消息

msg1.send_buf   = &w25x_read_id;
msg1.recv_buf   = RT_NULL;
msg1.length     = 1;
msg1.cs_take    = 1;//第一个消息置1
msg1.cs_release = 0;
msg1.next       = &msg2;//连接到msg2

msg2.send_buf   = RT_NULL;
msg2.recv_buf   = id;
msg2.length     = 5;
msg2.cs_take    = 0;
msg2.cs_release = 1;//第二个消息置0
msg2.next       = RT_NULL;

rt_spi_transfer_message(spi_dev_w25q, &msg1);//只调用msg1,API会自动遍历SPI消息链表

单次传输数据

rt_size_t rt_spi_transfer(struct rt_spi_device *device,//设备句柄
                          const void *send_buf,//发送缓冲区指针
                          void *recv_buf,//接收缓冲区指针
                          rt_size_t length);//发送、接收数据字节数

此函数等同于调用上面的spi_transfer_message()传输一条消息,开始发送数据时片选选中,函数返回时释放片选

单次发送数据

使用以下API发送一次数据,忽略接收到的数据

rt_size_t rt_spi_send(struct rt_spi_device *device,//SPI设备句柄
                      const void *send_buf,//发送数据缓冲区指针
                      rt_size_t length)//发送数据字节数

此函数等同于调用上面的spi_transfer_message()传输一条消息,开始发送数据时片选选中,函数返回时释放片选(都是套皮封装)

单次接收数据

rt_size_t rt_spi_recv(struct rt_spi_device *device,//SPI设备句柄
                      void *recv_buf,//接收数据缓冲区指针
                      rt_size_t length);//接收数据字节数

此函数是对rt_spi_transfer()的封装,在接收数据时主设备会发送数据0xFF

连续两次发送数据

使用以下API先后连续发送 2 个缓冲区的数据,中间片选不释放

rt_err_t rt_spi_send_then_send(struct rt_spi_device *device,//SPI设备句柄
                               const void *send_buf1,//发送数据1缓冲区指针
                               rt_size_t send_length1,//发送数据1字节数
                               const void *send_buf2,//发送数据2缓冲区指针
                               rt_size_t send_length2);//发送数据2字节数

此函数可以连续发送2个缓冲区的数据,忽略接收到的数据,发送send_buf1时片选选中,发送完send_buf2后释放片选

用处(摘自RTT官方教程)

本函数适合向 SPI 设备中写入一块数据,第一次先发送命令和地址等数据,第二次再发送指定长度的数据。之所以分两次发送而不是合并成一个数据块发送,或调用两次 rt_spi_send(),是因为在大部分的数据写操作中,都需要先发命令和地址,长度一般只有几个字节。如果与后面的数据合并在一起发送,将需要进行内存空间申请和大量的数据搬运。而如果调用两次 rt_spi_send(),那么在发送完命令和地址后,片选会被释放,大部分 SPI 设备都依靠设置片选一次有效为命令的起始,所以片选在发送完命令或地址数据后被释放,则此次操作被丢弃

和上面rt_spi_transfer_message()的使用例不能说是比较像,只能说是完全一致(但是更方便)

先发送后接收数据

使用以下API向从设备先发送数据,然后接收从设备发送的数据,中间片选不释放

rt_err_t rt_spi_send_then_recv(struct rt_spi_device *device,//SPI设备句柄
                               const void *send_buf,//发送数据缓冲区指针
                               rt_size_t send_length,//发送数据字节数
                               void *recv_buf,//接收数据缓冲区指针
                               rt_size_t recv_length);//接收数据字节数

此函数发送第一条数据send_buf时开始片选,此时忽略接收到的数据;然后发送第二条数据,此时主设备会发送数据0XFF,接收到的数据保存在recv_buf里,函数返回时释放片选

本函数适合从 SPI 从设备中读取一块数据,第一次会先发送一些命令和地址数据,然后再接收指定长度的数据

等同于调用两次rt_spi_transfer_message(),一次发送一次接收,中间不释放片选

访问QSPI设备

  1. 传输数据API
rt_size_t rt_qspi_transfer_message(struct rt_qspi_device *device,//QSPI设备句柄
                                   struct rt_qspi_message *message);//消息指针

rt_qspi_message结构体设置消息内容,原型如下

struct rt_qspi_message
{
    struct rt_spi_message parent;   /* 继承自struct rt_spi_message */
    struct
    {
        rt_uint8_t content;         /* 指令内容 */
        rt_uint8_t qspi_lines;      /* 指令模式,单线模式 1 位、双线模式 2 位,4 线模式 4 位 */
    } instruction;                  /* 指令阶段 */

     struct
    {
        rt_uint32_t content;        /* 地址/交替字节 内容 */
        rt_uint8_t size;            /* 地址/交替字节 长度 */
        rt_uint8_t qspi_lines;      /* 地址/交替字节 模式,单线模式 1 位、双线模式 2 位,4 线模式 4 位 */
    } address, alternate_bytes;     /* 地址/交替字节 阶段 */

    rt_uint32_t dummy_cycles;       /* 空指令周期阶段 */
    rt_uint8_t qspi_data_lines;     /*  QSPI 总线位宽 */
};
  1. 接收数据API
rt_err_t rt_qspi_send_then_recv(struct rt_qspi_device *device,//QSPI设备句柄
                                const void *send_buf,//发送数据缓存区指针
                                rt_size_t send_length,//发送数据字节数
                                void *recv_buf,//接收数据缓存区指针
                                rt_size_t recv_length);//接收数据字节数
  1. 发送数据API
rt_err_t rt_qspi_send(struct rt_qspi_device *device,//QSPI设备句柄
                      const void *send_buf,//发送数据缓存区指针
                      rt_size_t length)//发送数据字节数

QSPI一般使用接收数据和发送数据API来控制片外FLASH/SRAM

特殊使用场景

在特殊情况下,某设备需要独占总线,但独占期间数据传输间断,此时必须使用rt_spi_transfer_message()函数接口此函数每个待传输消息的片选控制域 cs_take 和 cs_release 都要设置为 0 值,因为片选信号已经使用其他API控制,无需在数据传输时控制

使用以下API实现CS长时间获取和释放

获取与释放总线

rt_err_t rt_spi_take_bus(struct rt_spi_device *device);//获取总线
rt_err_t rt_spi_release_bus(struct rt_spi_device *device);//释放总线

多线程情况下,SPI总线资源可能成为临界区资源,悲不同线程使用,为了防止SPI总线上数据丢失,必须在传输前获取总线使用权,使用成功才能够开启片选、传输数据;总线使用完毕后必须释放,否则其他从设备无法使用SPI总线传输数据

片选信号控制

rt_err_t rt_spi_take(struct rt_spi_device *device);//选中片选
rt_err_t rt_spi_release(struct rt_spi_device *device);//释放片选

从设备获取总线的使用权后,需要设置自己对应的片选信号为有效;从设备数据传输完成后,必须释放片选

发送消息

使用rt_spi_transfer_message()函数时,所有消息以单向链表的形式连接起来,挂载等待队列上准备发送

使用以下API在消息链表中增加一条新的待传输消息

void rt_spi_message_append(struct rt_spi_message *list,//待传输的消息链表节点
                           struct rt_spi_message *message);//新增消息指针

SPI设备使用示例

#include <rtthread.h>
#include <rtdevice.h>//需要引入该头文件才能使用SPI设备

//定义设备名
#define W25Q_SPI_DEVICE_NAME "qspi10"

static void spi_w25q_sample(int argc, char *argv[])//示例线程-导出为shell指令
{
    struct rt_spi_device *spi_dev_w25q;
    char name[RT_NAME_MAX];
    rt_uint8_t w25x_read_id = 0x90;
    rt_uint8_t id[5] = {0};

    if (argc == 2)
        rt_strncpy(name, argv[1], RT_NAME_MAX);
    else
        rt_strncpy(name, W25Q_SPI_DEVICE_NAME, RT_NAME_MAX);

    /* 查找 spi 设备获取设备句柄 */
    spi_dev_w25q = (struct rt_spi_device *)rt_device_find(name);
    if (!spi_dev_w25q)
        rt_kprintf("spi sample run failed! can't find %s device!\n", name);//报错
    else
    {
        /* 方式1:使用 rt_spi_send_then_recv()发送命令读取ID */
        rt_spi_send_then_recv(spi_dev_w25q, &w25x_read_id, 1, id, 5);
        rt_kprintf("use rt_spi_send_then_recv() read w25q ID is:%x%x\n", id[3], id[4]);

        /* 方式2:使用 rt_spi_transfer_message()发送命令读取ID */
        struct rt_spi_message msg1, msg2;

        msg1.send_buf   = &w25x_read_id;
        msg1.recv_buf   = RT_NULL;
        msg1.length     = 1;
        msg1.cs_take    = 1;
        msg1.cs_release = 0;
        msg1.next       = &msg2;

        msg2.send_buf   = RT_NULL;
        msg2.recv_buf   = id;
        msg2.length     = 5;
        msg2.cs_take    = 0;
        msg2.cs_release = 1;
        msg2.next       = RT_NULL;

        rt_spi_transfer_message(spi_dev_w25q, &msg1);
        rt_kprintf("use rt_spi_transfer_message() read w25q ID is:%x%x\n", id[3], id[4]);
    }
}

IIC设备

内部集成电路总线IIC是半双工双向二线制同步串行总线

物理层需要两条数据线:双向数据线SDA双向时钟线SCL,IIC使用单线(SDA)进行数据收发

IIC允许多个主设备存在,但同一时刻只允许有一个主设备,每个连接到总线上的器件都被分配唯一的地址,主设备启动数据传输并产生时钟信号,从设备被主设备寻址

一般IIC的两条信号线需要上拉到VDD,当总线空闲时,保证SDA和SCL都处于高电平状态

IIC时序

IIC总线的数据传输格式如下:

在这里插入图片描述

  1. 开始条件:由主机发出的低电平信号,表示传输即将开始

  2. 从设备地址与读写位:主机发送的第一个字节,其中高7位表示从机地址,最低位标识R/W读写位,读写位中1表示读取,0表示写入;特别地,可以选择使用10位地址模式,此模式下第一个字节的前7位是11110xxF的组合,最后xx表示10位地址的两个最高位,F表示R/W读写位,表示原则与7位地址模式相同,第二个字节为10位从机地址的剩下8位。图示如下

在这里插入图片描述

  1. 应答信号ACK:每传输完成一个字节的数据,接收方(从设备或主设备)需要回复一个ACK应答信号:写数据时由从机发送,读数据时由主机发送。当主机读到最后一个字节的数据时,可以选择发送NACK,然后再发送停止条件

  2. 停止条件:SDA=0,SCL上升沿且保持高电平,再将SDA拉高,表示传输结束

  3. 数据:每个数据规定为8位(1字节),数据字节数无限制

  4. 重复开始条件:在一次通信中,主机如果需要和不同的从机传输数据或需要切换读写操作时,可以再发送1个开始条件

在RTT中使用IIC设备

RTT将IIC主机虚拟为IIC总线设备,IIC从机通过IIC设备接口和IIC总线设备(主机)通讯

以下为API

查找IIC设备

rt_device_t rt_device_find(const char* name);

使用这个API根据总线设备名称获取设备句柄来操作IIC总线设备

使用例如下

#define AHT10_I2C_BUS_NAME "i2c1" /* I2C总线设备名称 */
struct rt_i2c_bus_device *i2c_bus; /* I2C总线设备句柄 */

/* 查找I2C总线设备,获取I2C总线设备句柄 */
i2c_bus = (struct rt_i2c_bus_device *)rt_device_find(name);

一般IIC设备直接命名为iicx或i2cx,x代表数字

传输数据

注意:该API会调用rt_mutex_take(),不能在中断服务函数中调用,否则会导致assertion报错

rt_size_t rt_i2c_transfer(struct rt_i2c_bus_device *bus,//总线设备句柄
                          struct rt_i2c_msg msgs[],//待传输消息数组指针
                          rt_uint32_t num);//消息数组的元素个数

//消息数据结构原型
struct rt_i2c_msg
{
    rt_uint16_t addr;    /* 从机地址,支持7位和10位二进制地址 */
    rt_uint16_t flags;   /* 读、写标志等 */
    rt_uint16_t len;     /* 读写数据字节数 */
    rt_uint8_t  *buf;    /* 读写数据缓冲区指针 */
}

//flag可用取值,实际使用中可以使用位与|进行组合
#define RT_I2C_WR              0x0000        /* 写标志 */
#define RT_I2C_RD              (1u << 0)     /* 读标志 */
#define RT_I2C_ADDR_10BIT      (1u << 2)     /* 10 位地址模式 */
#define RT_I2C_NO_START        (1u << 4)     /* 无开始条件 */
#define RT_I2C_IGNORE_NACK     (1u << 5)     /* 忽视 NACK */
#define RT_I2C_NO_READ_ACK     (1u << 6)     /* 读的时候不发送 ACK */

使用以上API进行IIC数据传输

IIC数据以消息为单位,参数msgs应为指向待传输数据的消息数组(指针)struct rt_i2c_msg *msg

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值