spi
1.应用层的spi操作
spi 消息结构
struct rt_spi_message
{
const void *send_buf;//发送数据的缓存
void *recv_buf;//接收数据的缓存
rt_size_t length;//数据长度
struct rt_spi_message *next;//指向下一个消息结构
unsigned cs_take : 1;//是否执行获取cs
unsigned cs_release : 1;//是否执行释放cs
};
通用的数据收发函数
struct rt_spi_message *rt_spi_transfer_message(struct rt_spi_device *device,
struct rt_spi_message *message)
2.摘录自RTT官方-应用层文档
挂载 SPI 设备
SPI 驱动会注册 SPI 总线,SPI 设备需要挂载到已经注册好的 SPI 总线上。
rt_err_t rt_spi_bus_attach_device_cspin(struct rt_spi_device *device,
const char *name,
const char *bus_name,
rt_base_t cs_pin,
void *user_data)
参数 | 描述 |
---|---|
device | SPI 设备句柄 |
name | SPI 设备名称 |
bus_name | SPI 总线名称 |
cs_pin | SPI 片选引脚号(基于PIN框架) |
user_data | 用户数据指针 |
返回 | —— |
RT_EOK | 成功 |
其他错误码 | 失败 |
此函数用于挂载一个 SPI 设备到指定的 SPI 总线,并向内核注册 SPI 设备。并且可以依赖RT-Thread的PIN框架来绑定SPI的片选引脚(cs_pin),避免了不同bsp的上层应用对片选引脚操作不统一的问题。
一般 SPI 总线命名原则为 spix, SPI 设备命名原则为 spixy ,如 spi10 表示挂载在 spi1 总线上的 0 号设备。cs_pin可以通过PIN框架rt_pin_get
函数来获取,也可以使用BSP级提供的GET_PIN
宏定义来获取。user_data 在用户使用不到的情况下可以设置为RT_NULL
。
使用示例:
struct rt_spi_device *spi_device;
spi_device = (struct rt_spi_device *)rt_malloc(sizeof(struct rt_spi_device));
/* BSP级 GET_PIN 宏定义方式 */
rt_hw_spi_device_attach(spi_device, "spi1", "spi10", GET_PIN(B, 14), RT_NULL);
/* PIN框架级 rt_pin_get api方式 */
rt_hw_spi_device_attach(spi_device, "spi1", "spi10", rt_pin_get("PB.14"), RT_NULL);
[!NOTE]
此函数是RT-Thread 5.0.0 添加的新函数,如果低于5.0.0版本不支持这个函数。
为了兼容RT-Thread 5.0.0 版本前的SPI设备片选引脚通过user_data挂载的方式,我们保留了rt_spi_bus_attach_device
这个api,但是希望大家在今后使用的时候,尽量使用rt_spi_bus_attach_device_cspin
这个新特性api。
rt_err_t rt_spi_bus_attach_device(struct rt_spi_device *device,
const char *name,
const char *bus_name,
void *user_data)
参数 | 描述 |
---|---|
device | SPI 设备句柄 |
name | SPI 设备名称 |
bus_name | SPI 总线名称 |
user_data | 用户数据指针 |
返回 | —— |
RT_EOK | 成功 |
其他错误码 | 失败 |
此函数用于挂载一个 SPI 设备到指定的 SPI 总线,并向内核注册 SPI 设备,并将 user_data 保存到 SPI 设备的控制块里。
一般 SPI 总线命名原则为 spix, SPI 设备命名原则为 spixy ,如 spi10 表示挂载在 spi1 总线上的 0 号设备。user_data 一般为 SPI 设备的 CS 引脚指针,进行数据传输时 SPI 控制器会操作此引脚进行片选。
下面的示例代码挂载 SPI FLASH W25Q128 到 SPI 总线:
static int rt_hw_spi_flash_init(void)
{
struct rt_spi_device *spi_device = RT_NULL;
spi_device = (struct rt_spi_device *)rt_malloc(sizeof(struct rt_spi_device));
if(RT_NULL == spi_device)
{
LOG_E("Failed to malloc the spi device.");
return -RT_ENOMEM;
}
if (RT_EOK != rt_spi_bus_attach_device_cspin(spi_device, "spi10", "spi1",GET_PIN(B, 14), RT_NULL))
{
LOG_E("Failed to attach the spi device.");
return -RT_ERROR;
}
if (RT_NULL == rt_sfud_flash_probe("W25Q128", "spi10"))
{
LOG_E("Failed to probe the W25Q128.");
return -RT_ERROR;
};
return RT_EOK;
}
/* 导出到自动初始化 */
INIT_COMPONENT_EXPORT(rt_hw_spi_flash_init);
配置 SPI 设备
挂载 SPI 设备到 SPI 总线后需要配置 SPI 设备的传输参数。
主要是工作模式,数据宽度,spi工作频率
rt_err_t rt_spi_configure(struct rt_spi_device *device,
struct rt_spi_configuration *cfg)
参数 | 描述 |
---|---|
device | SPI 设备句柄 |
cfg | SPI 配置参数指针 |
返回 | —— |
RT_EOK | 成功 |
此函数会保存 cfg 指向的配置参数到 SPI 设备 device 的控制块里,当传输数据时会使用此配置参数。
struct rt_spi_configuration 原型如下:
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; /* 最大频率 */
};
模式: 包含 MSB/LSB、主从模式、 时序模式等,可取宏组合如下:
/* 设置数据传输顺序是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 master device */
#define RT_SPI_SLAVE (1<<3) /* SPI slave device */
/* 设置时钟极性和时钟相位 */
#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) /* Chipselect active high */
#define RT_SPI_NO_CS (1<<5) /* No chipselect */
#define RT_SPI_3WIRE (1<<6) /* SI/SO pin shared */
#define RT_SPI_READY (1<<7) /* Slave pulls low to pause */
数据宽度: 根据 SPI 主设备及 SPI 从设备可发送及接收的数据宽度格式设置为8位、16位或者32位。
最大频率: 设置数据传输的波特率,同样根据 SPI 主设备及 SPI 从设备工作的波特率范围设置。
配置示例如下所示:
struct rt_spi_configuration cfg;
cfg.data_width = 8;
cfg.mode = RT_SPI_MASTER | RT_SPI_MODE_0 | RT_SPI_MSB;
cfg.max_hz = 20 * 1000 *1000; /* 20M */
rt_spi_configure(spi_dev, &cfg);
访问 SPI 设备
一般情况下 MCU 的 SPI 器件都是作为主机和从机通讯,在 RT-Thread 中将 SPI 主机虚拟为 SPI 总线设备,应用程序使用 SPI 设备管理接口来访问 SPI 从机器件,主要接口如下所示:
函数 | 描述 |
---|---|
rt_device_find() | 根据 SPI 设备名称查找设备获取设备句柄 |
rt_spi_transfer_message() | 自定义传输数据 |
rt_spi_transfer() | 传输一次数据 |
rt_spi_send() | 发送一次数据 |
rt_spi_recv() | 接受一次数据 |
rt_spi_send_then_send() | 连续两次发送 |
rt_spi_send_then_recv() | 先发送后接收 |
[!NOTE] 注:SPI 数据传输相关接口会调用 rt_mutex_take(), 此函数不能在中断服务程序里面调用,会导致 assertion 报错。
查找 SPI 设备
在使用 SPI 设备前需要根据 SPI 设备名称获取设备句柄,进而才可以操作 SPI 设备,查找设备函数如下所示,
rt_device_t rt_device_find(const char* name);
参数 | 描述 |
---|---|
name | 设备名称 |
返回 | —— |
设备句柄 | 查找到对应设备将返回相应的设备句柄 |
RT_NULL | 没有找到相应的设备对象 |
一般情况下,注册到系统的 SPI 设备名称为 spi10, qspi10等,使用示例如下所示:
#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);
自定义传输数据
获取到 SPI 设备句柄就可以使用 SPI 设备管理接口访问 SPI 设备器件,进行数据收发。可以通过如下函数传输消息:
struct rt_spi_message *rt_spi_transfer_message(struct rt_spi_device *device,struct rt_spi_message *message);
参数 | 描述 |
---|---|
device | SPI 设备句柄 |
message | 消息指针 |
返回 | —— |
RT_NULL | 成功发送 |
非空指针 | 发送失败,返回指向剩余未发送的 message 的指针 |
此函数可以传输一连串消息,用户可以自定义每个待传输的 message 结构体各参数的数值,从而可以很方便的控制数据传输方式。struct rt_spi_message 原型如下:
struct rt_spi_message
{
const void *send_buf; /* 发送缓冲区指针 */
void *recv_buf; /* 接收缓冲区指针 */
rt_size_t length; /* 发送 / 接收 数据字节数 */
struct rt_spi_message *next; /* 指向继续发送的下一条消息的指针 */
unsigned cs_take : 1; /* 片选选中 */
unsigned cs_release : 1; /* 释放片选 */
};
- sendbuf 为发送缓冲区指针,其值为 RT_NULL 时,表示本次传输为只接收状态,不需要发送数据。
- recvbuf 为接收缓冲区指针,其值为 RT_NULL 时,表示本次传输为只发送状态,不需要保存接收到的数据,所以收到的数据直接丢弃。
- length 的单位为 word,即数据长度为 8 位时,每个 length 占用 1 个字节;当数据长度为 16 位时,每个 length 占用 2 个字节。
- 参数 next 是指向继续发送的下一条消息的指针,若只发送一条消息,则此指针值为 RT_NULL。多个待传输的消息通过 next 指针以单向链表的形式连接在一起。
- cs_take 值为 1 时,表示在传输数据前,设置对应的 CS 为有效状态。
- cs_release 值为 1 时,表示在数据传输结束后,释放对应的 CS。
[!NOTE] 注:* 当 send_buf 或 recv_buf 不为空时,两者的可用空间都不得小于 length。
- 若使用此函数传输消息,传输的第一条消息 cs_take 需置为 1,设置片选为有效,最后一条消息的 cs_release 需置 1,释放片选。
使用示例如下所示:
#define W25Q_SPI_DEVICE_NAME "qspi10" /* SPI 设备名称 */
struct rt_spi_device *spi_dev_w25q; /* SPI 设备句柄 */
struct rt_spi_message msg1, msg2;
rt_uint8_t w25x_read_id = 0x90; /* 命令 */
rt_uint8_t id[5] = {0};
/* 查找 spi 设备获取设备句柄 */
spi_dev_w25q = (struct rt_spi_device *)rt_device_find(W25Q_SPI_DEVICE_NAME);
/* 发送命令读取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]);
传输一次数据
如果只传输一次数据可以通过如下函数:
rt_size_t rt_spi_transfer(struct rt_spi_device *device,
const void *send_buf,
void *recv_buf,
rt_size_t length);
参数 | 描述 |
---|---|
device | SPI 设备句柄 |
send_buf | 发送数据缓冲区指针 |
recv_buf | 接收数据缓冲区指针 |
length | 发送/接收 数据字节数 |
返回 | —— |
0 | 传输失败 |
非 0 值 | 成功传输的字节数 |
此函数等同于调用rt_spi_transfer_message()
传输一条消息,开始发送数据时片选选中,函数返回时释放片选,message 参数配置如下:
struct rt_spi_message msg;
msg.send_buf = send_buf;
msg.recv_buf = recv_buf;
msg.length = length;
msg.cs_take = 1;
msg.cs_release = 1;
msg.next = RT_NULL;
发送一次数据
如果只发送一次数据,而忽略接收到的数据可以通过如下函数:
rt_size_t rt_spi_send(struct rt_spi_device *device,
const void *send_buf,
rt_size_t length)
参数 | 描述 |
---|---|
device | SPI 设备句柄 |
send_buf | 发送数据缓冲区指针 |
length | 发送数据字节数 |
返回 | —— |
0 | 发送失败 |
非 0 值 | 成功发送的字节数 |
调用此函数发送 send_buf 指向的缓冲区的数据,忽略接收到的数据,此函数是对 rt_spi_transfer()
函数的封装。
此函数等同于调用 rt_spi_transfer_message()
传输一条消息,开始发送数据时片选选中,函数返回时释放片选,message 参数配置如下:
struct rt_spi_message msg;
msg.send_buf = send_buf;
msg.recv_buf = RT_NULL;
msg.length = length;
msg.cs_take = 1;
msg.cs_release = 1;
msg.next = RT_NULL;
接收一次数据
如果只接收一次数据可以通过如下函数:
rt_size_t rt_spi_recv(struct rt_spi_device *device,
void *recv_buf,
rt_size_t length);
参数 | 描述 |
---|---|
device | SPI 设备句柄 |
recv_buf | 接收数据缓冲区指针 |
length | 接收数据字节数 |
返回 | —— |
0 | 接收失败 |
非 0 值 | 成功接收的字节数 |
调用此函数接收数据并保存到 recv_buf 指向的缓冲区。此函数是对 rt_spi_transfer()
函数的封装。SPI 总线协议规定只能由主设备产生时钟,因此在接收数据时,主设备会发送数据 0XFF。
此函数等同于调用 rt_spi_transfer_message()
传输一条消息,开始接收数据时片选选中,函数返回时释放片选,message 参数配置如下:
struct rt_spi_message msg;
msg.send_buf = RT_NULL;
msg.recv_buf = recv_buf;
msg.length = length;
msg.cs_take = 1;
msg.cs_release = 1;
msg.next = RT_NULL;
连续两次发送数据
如果需要先后连续发送 2 个缓冲区的数据,并且中间片选不释放,可以调用如下函数:
rt_err_t rt_spi_send_then_send(struct rt_spi_device *device,
const void *send_buf1,
rt_size_t send_length1,
const void *send_buf2,
rt_size_t send_length2);
参数 | 描述 |
---|---|
device | SPI 设备句柄 |
send_buf1 | 发送数据缓冲区 1 指针 |
send_length1 | 发送数据缓冲区 1 数据字节数 |
send_buf2 | 发送数据缓冲区 2 指针 |
send_length2 | 发送数据缓冲区 2 数据字节数 |
返回 | —— |
RT_EOK | 发送成功 |
-RT_EIO | 发送失败 |
此函数可以连续发送 2 个缓冲区的数据,忽略接收到的数据,发送 send_buf1 时片选选中,发送完 send_buf2 后释放片选。
本函数适合向 SPI 设备中写入一块数据,第一次先发送命令和地址等数据,第二次再发送指定长度的数据。之所以分两次发送而不是合并成一个数据块发送,或调用两次 rt_spi_send()
,是因为在大部分的数据写操作中,都需要先发命令和地址,长度一般只有几个字节。如果与后面的数据合并在一起发送,将需要进行内存空间申请和大量的数据搬运。而如果调用两次 rt_spi_send()
,那么在发送完命令和地址后,片选会被释放,大部分 SPI 设备都依靠设置片选一次有效为命令的起始,所以片选在发送完命令或地址数据后被释放,则此次操作被丢弃。
此函数等同于调用 rt_spi_transfer_message()
传输 2 条消息,message 参数配置如下:
struct rt_spi_message msg1,msg2;
msg1.send_buf = send_buf1;
msg1.recv_buf = RT_NULL;
msg1.length = send_length1;
msg1.cs_take = 1;
msg1.cs_release = 0;
msg1.next = &msg2;
msg2.send_buf = send_buf2;
msg2.recv_buf = RT_NULL;
msg2.length = send_length2;
msg2.cs_take = 0;
msg2.cs_release = 1;
msg2.next = RT_NULL;
先发送后接收数据
如果需要向从设备先发送数据,然后接收从设备发送的数据,并且中间片选不释放,可以调用如下函数:
rt_err_t rt_spi_send_then_recv(struct rt_spi_device *device,
const void *send_buf,
rt_size_t send_length,
void *recv_buf,
rt_size_t recv_length);
参数 | 描述 |
---|---|
device | SPI 从设备句柄 |
send_buf | 发送数据缓冲区指针 |
send_length | 发送数据缓冲区数据字节数 |
recv_buf | 接收数据缓冲区指针 |
recv_length | 接收数据字节数 |
返回 | —— |
RT_EOK | 成功 |
-RT_EIO | 失败 |
此函数发送第一条数据 send_buf 时开始片选,此时忽略接收到的数据,然后发送第二条数据,此时主设备会发送数据 0XFF,接收到的数据保存在 recv_buf 里,函数返回时释放片选。
本函数适合从 SPI 从设备中读取一块数据,第一次会先发送一些命令和地址数据,然后再接收指定长度的数据。
此函数等同于调用 rt_spi_transfer_message()
传输 2 条消息,message 参数配置如下:
struct rt_spi_message msg1,msg2;
msg1.send_buf = send_buf;
msg1.recv_buf = RT_NULL;
msg1.length = send_length;
msg1.cs_take = 1;
msg1.cs_release = 0;
msg1.next = &msg2;
msg2.send_buf = RT_NULL;
msg2.recv_buf = recv_buf;
msg2.length = recv_length;
msg2.cs_take = 0;
msg2.cs_release = 1;
msg2.next = RT_NULL;
SPI 设备管理模块还提供 rt_spi_sendrecv8()
和 rt_spi_sendrecv16()
函数,这两个函数都是对此函数的封装,rt_spi_sendrecv8()
发送一个字节数据同时收到一个字节数据,rt_spi_sendrecv16()
发送 2 个字节数据同时收到 2 个字节数据。
3.底层的spi操作
spi 分为spi总线和spi设备,分为3层框架,spi设备继承自spi总线,spi总线继承自io设备驱动框架,例如w25q128就是一个spi设备
spi总线抽象接口
struct rt_spi_ops
{
rt_err_t (*configure)(struct rt_spi_device *device, struct rt_spi_configuration *configuration);
rt_uint32_t (*xfer)(struct rt_spi_device *device, struct rt_spi_message *message);
};
spi设备抽象
继承自rt_device 设备驱动模型
struct rt_spi_bus
{
struct rt_device parent;//父类
rt_uint8_t mode;
const struct rt_spi_ops *ops;//spi操作函数
struct rt_mutex lock;//一个总线可能会挂载多个设备,所以需要使用锁操作
struct rt_spi_device *owner; //总线目前归属那个spi设备
};
spi设备
继承自rt_device 和spi总线
//老结构
struct rt_spi_device
{
struct rt_device parent;
struct rt_spi_bus *bus;
struct rt_spi_configuration config;
void *user_data;
};
//新结构-增加了CS片选
struct rt_spi_device
{
struct rt_device parent;
struct rt_spi_bus *bus;
struct rt_spi_configuration config;
rt_base_t cs_pin;//直接提供一个RTT框架下的pin使用 GET_PIN(C,1);
void *user_data;
};
如何注册一个spi总线
- 实现一个结构体对象rt_spi_bus ,实现内部的操作函数configure和xfer
- 注册spi总线 rt_spi_bus_register
spi总线注册如下图
spi设备注册
spi 设备注册依赖一个spi总线,在rt_spi_bus_attach_device 函数注册时,需要提供spi总线,cs片选
rt_err_t rt_spi_bus_attach_device_cspin(struct rt_spi_device *device,
const char *name,//设备名字
const char *bus_name,//总线名字
rt_base_t cs_pin,
void *user_data)
下图就是spi设备如何注册,如何读写数据到spi设备
5.0框架下的spi设备注册
4.如何修改spi总线的引脚
在drv_spi.h文件中定义了spi外设的引脚
#ifdef RT_USING_SPI1
#define SPI1_SCK_PIN GPIO_PIN_5 /* PA.05 */
#define SPI1_SCK_GPIO_PORT GPIOA /* GPIOA */
#define SPI1_SCK_GPIO_CLK RCC_APB2_PERIPH_GPIOA
#define SPI1_MISO_PIN GPIO_PIN_6 /* PA.06 */
#define SPI1_MISO_GPIO_PORT GPIOA /* GPIOA */
#define SPI1_MISO_GPIO_CLK RCC_APB2_PERIPH_GPIOA
#define SPI1_MOSI_PIN GPIO_PIN_7 /* PA.07 */
#define SPI1_MOSI_GPIO_PORT GPIOA /* GPIOA */
#define SPI1_MOSI_GPIO_CLK RCC_APB2_PERIPH_GPIOA
#endif /* RT_USING_SPI1 */
#ifdef RT_USING_SPI2
#define SPI2_SCK_PIN GPIO_PIN_13 /* PB.13 */
#define SPI2_SCK_GPIO_PORT GPIOB /* GPIOB */
#define SPI2_SCK_GPIO_CLK RCC_APB2_PERIPH_GPIOB
#define SPI2_MISO_PIN GPIO_PIN_14 /* PB.14 */
#define SPI2_MISO_GPIO_PORT GPIOB /* GPIOB */
#define SPI2_MISO_GPIO_CLK RCC_APB2_PERIPH_GPIOB
#define SPI2_MOSI_PIN GPIO_PIN_15 /* PB.15 */
#define SPI2_MOSI_GPIO_PORT GPIOB /* GPIOB */
#define SPI2_MOSI_GPIO_CLK RCC_APB2_PERIPH_GPIOB
#endif /* RT_USING_SPI2 */
5.W25Q128 nor flash
状态寄存器
芯片id
指令
时序
每个指令都是在cs拉高后开始执行
写使能时序
读取状态寄存器
读数据时序
写时序