Cube
配置SPI引脚,仅使能引脚即可
注意这里的NSS引脚不使能SPI的线选功能,将在程序内部自定义线选引脚
Kconfig
在
menu “On-chip Peripheral Drivers”`` 选项卡下添加SPI驱动选项
config BSP_USING_SPI1
bool "Enable SPI1 BUS"
select RT_USING_SPI
default y
即驱动SPI1总线
Env
使能SPI1总线
scons —target=mdk5`` 生成MDK工程
MDK
接口函数
挂载 SPI 设备
SPI 驱动会注册 SPI 总线,SPI 设备需要挂载到已经注册好的 SPI 总线上。
rt_err_t rt_hw_spi_device_attach(const char *bus_name,
const char *device_name,
GPIO_TypeDef* cs_gpiox,
uint16_t cs_gpio_pin);
参数 | 描述 |
---|---|
bus_name | SPI 总线名称 |
device_name | SPI 设备名称 |
cs_gpiox | 片选信号引脚号 |
cs_gpiox_pin | 片选信号引脚 |
返回 | —— |
RT_EOK | 成功 |
其他错误码 | 失败 |
这个函数在`drv_spi.h`` 中被定义,因此需要添加头文件<br />总线名称已被定义,跟从驱动,若已开启SPI1,则名称为
spi1``<br />设备名称为自定义,习惯为
`spi10`` 即为总线1上的0号设备
配置 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 从设备工作的波特率范围设置。
其中的参数 设备句柄
需要用到查找设备函数填写,见后续接口函数
查找 SPI 设备
在使用 SPI 设备前需要根据 SPI 设备名称获取设备句柄,进而才可以操作 SPI 设备,查找设备函数如下所示,
rt_device_t rt_device_find(const char* name);
参数 | 描述 |
---|---|
name | 设备名称 |
返回 | —— |
设备句柄 | 查找到对应设备将返回相应的设备句柄 |
RT_NULL | 没有找到相应的设备对象 |
自定义传输数据
获取到 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。
连续两次发送数据
如果需要先后连续发送 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_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 从设备中读取一块数据,第一次会先发送一些命令和地址数据,然后再接收指定长度的数据。
这里只介绍了在驱动Flash时最常用的接口函数,如有其它特殊需求,参考RT-Thread的说明文档
https://www.rt-thread.org/document/site/programming-manual/device/spi/spi/
上机实验
#include <rtthread.h>
#include <rtdevice.h>
#include <board.h>
#include <drv_spi.h>
#define W25Q_SPI_DEVICE_NAME "spi10"
static int rt_hw_spi_flash_init(void)
{
rt_err_t err_message = rt_hw_spi_device_attach("spi1", "spi10", GPIOA, GPIO_PIN_4);
if(err_message != RT_EOK)
{
rt_kprintf("spi Init failed!");
return RT_NULL;
}
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((struct rt_spi_device *)rt_device_find(W25Q_SPI_DEVICE_NAME), &cfg);
return RT_EOK;
}
/* 导出到自动初始化 */
INIT_COMPONENT_EXPORT(rt_hw_spi_flash_init);
static void flash_app(int argc, char *argv[])
{
struct rt_spi_device *spi_dev_w25q;
rt_uint8_t w25x_read_id = 0x9F;
rt_uint8_t id[3] = {0};
/* 查找 spi 设备获取设备句柄 */
spi_dev_w25q = (struct rt_spi_device *)rt_device_find(W25Q_SPI_DEVICE_NAME);
if (!spi_dev_w25q)
{
rt_kprintf("spi sample run failed! can't find %s device!\n", W25Q_SPI_DEVICE_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:\n%x%x%x\n",id[0],id[1],id[2]);
}
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(flash_app, flash ID test);
通信成功后可以读取到Flash芯片设备的ID