Cube
配置SPI引脚,仅使能引脚即可
注意这里的NSS引脚不使能SPI的线选功能,将在程序内部自定义线选引脚
Kconfig
在 menu “On-chip Peripheral Drivers”`` 选项卡下添加SPI驱动选项
config BSP_USING_SPI1bool "Enable SPI1 BUS"select RT_USING_SPIdefault 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
