学习这篇文章I/O 设备模型之SPI设备使用,I/O 最后一篇关于设备模型的文章。
目录
- 前言
- 一、SPI 通讯基础
- 二、SPI 设备操作函数
-
- 2.1 挂载 SPI 设备
- 2.2 配置 SPI 设备
- 2.3 访问 SPI设备
-
- 2.3.1 查找 SPI 设备
- 2.3.2 自定义数据传输
- 2.3.3 数据收发函数
- 2.3.4 特殊场景
- 三、SPI 设备测试
-
- 3.1 SPI 设备使用步骤
- 3.2 测试
- 结语
前言
本文应该是 RT-Thread I/O 最后一篇设备模型,SPI 设备的学习测试。
我之前说过,我的记录是为了应用,但实际上我们在使用它 RT-Thread 有很多常用的设备,官方或许多开发人员已经为我们写了驱动程序和软件包,我们不需要再写一篇文章,大多数时候直接导入软件包,直接呼叫现成的 API 函数就可以了。
RT-Thread 接下来的文章系列应该更新几篇文章 本文将使用软件包和组件 SPI 学习测试设备。
?? 本 RT-Thread 专栏记录的开发环境: RT-Thread记录(一、RT-Thread 版本、RT-Thread Studio开发环境 及 配合CubeMX快速开发) RT-Thread记录(二、RT-Thread内核启动过程 — 启动文件及源码分析) ?? RT-Thread 内核篇系列博文链接: RT-Thread记录(三、RT-Thread 线程操作函数和线程管理FreeRTOS的比较) RT-Thread记录(四、RT-Thread 时钟节拍和软件定时器) RT-Thread记录(五、RT-Thread 临界保护) RT-Thread记录(六、IPC信号量、相互排斥和事件集的机制) RT-Thread记录(七、IPC机制之邮箱、消息队列) RT-Thread记录(八、理解 RT-Thread 内存管理) RT-Thread记录(九、RT-Thread 中断处理和阶段总结) ?? 在STM32L051C8 上使用 RT-Thread 应用文系列博文连接: RT-Thread 应用篇 — 在STM32L051上使用 RT-Thread (一)无线温湿度传感器 之 新建项目) RT-Thread 应用篇 — 在STM32L051上使用 RT-Thread (二)无线温湿度传感器 之 CubeMX配置) RT-Thread 应用篇 — 在STM32L051上使用 RT-Thread (三)无线温湿度传感器 之 I2C通讯) RT-Thread 应用篇 — 在STM32L051上使用 RT-Thread (4)无线温湿度传感器 之 串口通讯) ?? RT-Thread 设备系列博文链接: RT-Thread记录(十、全面了解 RT-Thread I/O 设备模型) RT-Thread记录(十一)I/O 设备模型之UART设备 — 源码解析) RT-Thread记录(十二)I/O 设备模型之UART设备 — 使用测试) RT-Thread记录(十三、I/O 设备模型之PIN设备) RT-Thread记录(十四)I/O 设备模型之ADC设备)
一、SPI 通讯基础
SPI 通信基础知识介绍不多,原则和基础可以在网上查询。本文作为应用所需的简要概述:
SPI是串行外设界面(Serial Peripheral Interface)缩写是一种高速、全双工、同步通信总线,SPI 通信速度可以达到几十个M,芯片管脚上只有四条线:
(1)MISO– Master Input Slave Output,从设备数据输出输入主设备数据; (2)MOSI– Master Output Slave Input,从设备数据输入主设备数据输出; (3)SCLK – Serial Clock,由主设备产生的时钟信号; (4)CS – Chip Select,主设备控制从设备使能信号。
SPI 以主从方式工作,通常有一个主设备和一个或多个从设备。
SPI 通信有四种模式,由 CPOL 和 CPHA 决定:
CPOL=0,表示当SCLK=0点处于空闲状态,空闲低电平,所以有效状态是SCLK在高电平时 CPOL=1,表示当SCLK=1:00处于空闲状态,空闲高电平,所以有效状态是SCLK低电平时 CPHA=0,表示数据采样在第一个边缘 CPHA=1.表示数据采样在第二个边缘
如下表格:
CPOL | CPHA | 说明 |
---|---|---|
1 | 1 | 时钟空闲为高电平,第二时钟边缘开始采样 |
0 | 0 | 时钟空闲为低电平,采样始于第一时钟边缘 |
1 | 0 | 时钟空闲为高电平,采样始于第一时钟边缘 |
0 | 1 | 时钟空闲为低电平,在第二个时钟边沿开始采样 |
对于我们的从机设备,如传感器,支持模式将在手册中说明:例如,我们今天要测试的 SPI Flash:
二、SPI 设备操作函数
来了解一下 RT-Thread 提供的 SPI 设备操作函数:
函数 | 描述 |
---|---|
rt_spi_bus_attach_device() | SPI 设备需要挂载至注册设备 SPI 总线,挂载SPI 设备 |
rt_spi_configure() | 配置 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() | 接收前先发送 |
与前面的设备不同的是,SPI 因为一主多从可以, SPI 设备多了一个挂载操作,就是 RT-Thread 注册系统驱动 SPI 然后我们需要使用自己的总线 SPI 设备挂载到总线,使设备能够操作 。
☆ 数据函数的自定义传输 rt_spi_transfer_message
作为核心,其实这个函数可以用来表达,下面就解释一下。☆
.
2.1 挂载 SPI 设备
SPI 驱动注册完 SP 总线,需要用 SPI 挂载函数将要使用的 SPI 设备需要挂载到已经注册好的 SPI 总线上:
/* 参数 描述 device SPI 设备句柄 name SPI 设备名称 bus_name SPI 总线名称 user_data 用户数据指针 返回 —— RT_EOK 成功 其他错误码 失败 */
rt_err_t rt_spi_bus_attach_device(struct rt_spi_device *device,
const char *name,
const char *bus_name,
void *user_data)
此函数用于挂载一个 SPI 设备到指定的 SPI 总线,并向内核注册 SPI 设备,并将 user_data 保存到 SPI 设备的控制块里。
一般 SPI 总线命名原则为 spix, SPI 设备命名原则为 spixy ,如 spi10 表示挂载在 spi1 总线上的 0 号设备。 user_data 一般为 SPI 设备的 CS 引脚指针,进行数据传输时 SPI 控制器会操作此引脚进行片选。
对于我们测试使用的 STM32 而言,有专门的挂载函数 rt_hw_spi_device_attach
:
/* 参数 描述 bus_name SPI 总线名称 device_name SPI 设备名称 后面2个参数是设置片选引脚: cs_gpiox GPIOA、GPIOB之类... cs_gpio_pin GPIO口名称 返回 —— RT_EOK 成功 其他错误码 失败 */
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)
2.2 配置 SPI 设备
上面介绍 SPI 通讯基础的时候讲到过 SPI 的工作模式等细节,RT-Thread 里使用 SPI 配置函数进行配置:
/* 参数 描述 device SPI 设备句柄 cfg SPI 配置参数指针 返回 —— RT_EOK 成功 */
rt_err_t rt_spi_configure(struct rt_spi_device *device,
struct rt_spi_configuration *cfg)
...
/** * SPI configuration structure */
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; /* 最大频率 */
};
/** * 上面结构体第一个参数: mode * SPI configuration structure * 其中与 SPI mode 相关的宏定义有 */
#define RT_SPI_CPHA (1<<0) /* bit[0]:CPHA, clock phase */
#define RT_SPI_CPOL (1<<1) /* bit[1]:CPOL, clock polarity */
/* 设置数据传输顺序是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_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 */
#define RT_SPI_MODE_MASK (RT_SPI_CPHA | RT_SPI_CPOL | RT_SPI_MSB | RT_SPI_SLAVE | RT_SPI_CS_HIGH | RT_SPI_NO_CS | RT_SPI_3WIRE | RT_SPI_READY)
/* 设置时钟极性和时钟相位 */
#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_BUS_MODE_SPI (1<<0)
#define RT_SPI_BUS_MODE_QSPI (1<<1)
/** * 上面结构体第二个和第四个参数: data_width 和 max_hz */
//根据 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);
2.3 访问 SPI设备
前面的两个函数类似于 SPI 的初始化工作,接下来就是我们熟悉的设备操作函数:
2.3.1 查找 SPI 设备
I/O 设备模型通用的查找函数:
/* 参数 描述 name SPI 设备名称 返回 —— 设备句柄 查找到对应设备将返回相应的设备句柄 RT_NULL 没有找到设备 */
rt_device_t rt_device_find(const char* name);
注意事项和 ADC 设备一样,用来接收的设备句柄不是使用rt_device_t
,但是与 ADC 也有不一样的地方,具体如下图:
因为 SPI 设备的接口体并没有 typedef 重定义,所以使用起来还得直接使用结构体指针表示。
2.3.2 自定义数据传输
自定义传输函数rt_spi_transfer_message
,是访问 SPI 设备的关键函数!
获取到 SPI 设备句柄就可以使用 SPI 设备管理接口访问 SPI 设备器件,进行数据收发:
/* 参数 描述 device SPI 设备句柄 message 消息指针 返回 —— RT_NULL 成功发送 非空指针 发送失败,返回指向剩余未发送的 message 的指针 */
struct rt_spi_message *rt_spi_transfer_message(struct rt_spi_device *device,
struct rt_spi_message *message)
其中第二个参数,消息的结构体,这也是发送消息的关键:
/** * SPI message structure */
struct rt_spi_message
{
const void *send_buf; /* 发送缓冲区指针,其值为 RT_NULL 时, 表示本次传输为只接收状态,不需要发送数据。*/
void *recv_buf; /* 接收缓冲区指针,其值为 RT_NULL 时, 表示本次传输为只发送状态,不需要保存接收到的数据 */
rt_size_t length; /* 发送 / 接收 数据字节数,单位为 word , 长度为 8 位时,每个 length 占用 1 个字节; 当数据长度为 16 位时,每个 length 占用 2 个字节*/
struct rt_spi_message *next; /* 指向继续发送的下一条消息的指针 , 若只发送一条消息,则此指针值为 RT_NULL。 多个待传输的消息通过 next 指针以单向链表的形式连接在一起。*/
unsigned cs_take : 1; /* 片选选中 cs_take 值为 1 时,表示在传输数据前,设置对应的 CS 为有效状态。*/
unsigned cs_release : 1; /* 释放片选 cs_release 值为 1 时,表示在数据传输结束后,释放对应的 CS。*/
};
关于最后两个参数:
传输的第一条消息 cs_take 需置为 1,设置片选为有效,
传输的最后一条消息的 cs_release 需置 1,释放片选。
struct rt_spi_message msg1;
msg1.send_buf = send_buf;
msg1.recv_buf = receive_buf;
msg1.length = send_length;
msg1.cs_take = 1; // 传输之前要先把总线拉低
msg1.cs_release = 1; // 传输之后要把总线释放
msg1.next = RT_NULL;
rt_spi_transfer_message(struct rt_spi_device *device, &msg1);
struct rt_spi_message msg1,msg2;
uint8 id[5] = {
0};
msg1.send_buf = send_buf;
msg1.recv_buf = RT_NULL;
msg1.length = send_length;
msg1.cs_take = 1; // 传输之前要先把总线拉低
msg1.cs_release = 0; // 本次结束之后并不释放总线,因为还要发送,所以为0
msg1.next = &msg2;
msg2.send_buf = RT_NULL;
msg2.recv_buf = id;
msg2.length = 5; //接收5个字节
msg2.cs_take = 0; //前面已经拉低了,没有释放,所以这里是不需要拉低的
msg2.cs_release = 1; //但是这个完成以后,需要释放总线,这是结尾
msg2.next = RT_NULL;
rt_spi_transfer_message(struct rt_spi_device *device, &msg1);
struct rt_spi_message msg1,msg2,msg3;
msg1.send_buf = send_buf;
msg1.recv_buf = RT_NULL;
msg1.length = length1;
msg1.cs_take = 1; // 传输之前要先把总线拉低
msg1.cs_release = 0; // 本次结束之后并不释放总线,因为还要发送,所以为0
msg1.next = &msg2;
msg2.send_buf = RT_NULL;
msg2.recv_buf = receive_buff;
msg2.length = length2;
msg2.cs_take = 0; //前面已经拉低了,没有释放,所以这里是不需要拉低的
msg2.cs_release = 0; //这里也不需要释放,前面会拉,后面会放
msg2.next = &msg3;
msg3.send_buf = RT_NULL;
msg3.recv_buf = receive_buff;
msg3.length = len3; //
msg3.cs_take = 0; //前面已经拉低了,没有释放,所以这里是不需要拉低的
msg3.cs_release = 1; //但是这个完成以后,需要释放总线,这是结尾
msg3.next = RT_NULL;
rt_spi_transfer_message(struct rt_spi_device *device, &msg1);
2.3.3 数据收发函数
除了上面通用的自定义数据传输函数, RT-Thread 还提供了一系列简单的数据收发函数,其实都是通过上面的函数演变而来,我们也简单的过一遍:
/* 参数 描述 device SPI 设备句柄 send_buf 发送数据缓冲区指针 recv_buf 接收数据缓冲区指针 length 发送/接收 数据字节数 返回 —— 0 传输失败 非 0 值 成功传输的字节数 */
rt_size_t rt_spi_transfer(struct rt_spi_device *device,
const void *send_buf,
void *recv_buf,
rt_size_t length)
使用此函数等同于:
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_spi_transfer_message(struct rt_spi_device *device, &msg);
/* 参数 描述 device SPI 设备句柄 send_buf 发送数据缓冲区指针 length 发送数据字节数 返回 —— 0 发送失败 非 0 值 成功发送的字节数 */
rt_inline rt_size_t rt_spi_send(struct rt_spi_device *device,
const void *send_buf,
rt_size_t length)
{
return rt_spi_transfer(device, send_buf, RT_NULL, length);
}
此函数直接是上面函数忽略接收数据的效果,可以直接看上面的函数内容。
/* 参数 描述 device SPI 设备句柄 recv_buf 接收数据缓冲区指针 length 接收数据字节数 返回 —— 0 接收失败 非 0 值 成功接收的字节数 */
rt_inline rt_size_t rt_spi_recv(struct rt_spi_device *device,
void *recv_buf,
rt_size_t length)
{
return rt_spi_transfer(device, RT_NULL, recv_buf, length);
}
与上面发送一次数据相反,传输一次数据函数忽略接收的数据。
/* 参数 描述 device SPI 设备句柄 send_buf1 发送数据缓冲区 1 指针 send_length1 发送数据缓冲区 1 数据字节数 send_buf2 发送数据缓冲区 2 指针 send_length2 发送数据缓冲区 2 数据字节数 返回 —— RT_EOK 发送成功 -RT_EIO 发送失败 */
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)
本函数适合向 SPI 设备中写入一块数据,第一次先发送命令和地址等数据,第二次再发送指定长度的数据。
之所以分两次发送而不是合并成一个数据块发送,或调用两次 rt_spi_send(),是因为在大部分的数据写操作中,都需要先发命令和地址,长度一般只有几个字节。如果与后面的数据合并在一起发送,将需要进行内存空间申请和大量的数据搬运。
而如果调用两次 rt_spi_send(),那么在发送完命令和地址后,片选会被释放,大部分 SPI 设备都依靠设置片选一次有效为命令的起始,所以片选在发送完命令或地址数据后被释放,则此次操作被丢弃。
使用此函数等同于:
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_spi_transfer_message(struct rt_spi_device *device, &msg1);
/* 参数 描述 device SPI 从设备句柄 send_buf 发送数据缓冲区指针 send_length 发送数据缓冲区数据字节数 recv_buf 接收数据缓冲区指针 recv_length 接收数据字节数 返回 —— RT_EOK 成功 -RT_EIO 失败 */
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)
本函数适合从 SPI 从设备中读取一块数据,第一次会先发送一些命令和地址数据,然后再接收指定长度的数据。
使用此函数等同于:
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;
rt_spi_transfer_message(struct rt_spi_device *device, &msg1);
2.3.4 特殊场景
特殊场景部分暂时并不能体会其中的意义,所以这里直接套用官方的说明,等以后再使用过程中如果确实遇到问题,再来更新自己的心得体会。
在一些特殊的使用场景,某个设备希望独占总线一段时间,且期间要保持片选一直有效,期间数据传输可能是间断的,则可以按照如所示步骤使用相关接口。传输数据函数必须使用 rt_spi_transfer_message()
,并且此函数每个待传输消息的片选控制域 cs_take
和 cs_release
都要设置为 0 值,因为片选已经使用了其他接口控制,不需要在数据传输的时候控制。
在多线程的情况下,同一个 SPI 总线可能会在不同的线程中使用,为了防止 SPI 总线正在传输的数据丢失,从设备在开始传输数据前需要先获取 SPI 总线的使用权,获取成功才能够使用总线传输数据:
/* 参数 描述 device SPI 设备句柄 返回 —— RT_EOK 成功 错误码 失败 */
rt_err_t rt_spi_take_bus(struct rt_spi_device *device)
从设备获取总线的使用权后,需要设置自己对应的片选信号为有效:
/* 参数 描述 device SPI 设备句柄 返回 —— 0 成功 错误码 失败 */
rt_err_t rt_spi_take(struct rt_spi_device *device)
使用 rt_spi_transfer_message() 传输消息时,所有待传输的消息都是以单向链表的形式连接起来的:
/* 参数 描述 list 待传输的消息链表节点 message 新增消息指针 */
rt_inline void rt_spi_message_append(struct rt_spi_message *list,
struct rt_spi_message *message)
传输完成释放片选:
/* device SPI 设备句柄 返回 —— 0 成功 错误码 失败 */
rt_err_t rt_spi_release(struct rt_spi_device *device)
从设备不在使用 SPI 总线传输数据,必须尽快释放总线,这样其他从设备才能使用 SPI 总线传输数据:
/* 参数 描述 device SPI 设备句柄 返回 —— RT_EOK 成功 */
rt_err_t rt_spi_release_bus(struct rt_spi_device *device);
三、SPI 设备测试
与上一篇文章说的 ADC 设备类似,我们可以通过,但是也需要注意他的使用步骤:
3.1 SPI 设备使用步骤
在 board.h
文件中,我们可以查看其中关于 SPI的 使用步骤的注释:
.
.
.
.
比如我使用的开发板原理图(忽略当时的引脚标号,这里应该是 SPI1,当时写标号居然写的是 SPI2 ):
查看对应的手册资料:
所以我们需要使能的是 SPI1:
.
.
和上一篇文章的 ADC 设备一样进行操作,如下图:
到这一步,我们已经能够找到我们需要的 HAL_SPI_MspInit
文件了,通过 spi.h
头文件找到 spi.c
文件中的这个函数:
.
.
.
.
在上一篇文章 ADC 步骤中已经讲解过,使用 STM32CubeMX 设置以后,文件会自动使能:
到这里 SPI 的配置就算全部完成了,我们可以直接在应用程序中,使用 SPI 设备操作函数实现 SPI 的读取。
3.2 测试
我们板载的是SPI设备是 W25Q128 ,我们测试一下 RT-Thread 的 SPI 设备模型是否能够正常通行,这里只做简单的读取 ID 的测试,官方的示例也是针对 W25Qxx 系列的,但是我还是按照自己的理解来进行。
我们根据上面的使用步骤,配置好 SPI ,我们应用程序什么都不操作,看看初始化以后是否有 spi1 总线设备,如下图:
.
确认了上电初始化以后 spi1 总线就已经存在,我们就可以使用 SPI 的操作函数进行,我们先把 spi 设备挂载上 spi 总线,然后进行必要的配置,操作代码如图:
到这一步,看可以看设备是否正常注册:
.
好了,接下来就可以经常正常的操作了,官方的示例是读取 W25Qxx 的 ID,至于读取 ID 操作流程,是需要查看 芯片手册的,但是我还想想到曾经在裸机使用过这个 SPI Flash ,那么我可以直接参考以前的驱动代码,这样就省去了再一次的手册查看资料 = = !
上一下裸机的有关操作代码:
//读取芯片ID W25Q128的ID:0XEF17
u16 SPI_Flash_ReadID()
{
u16 Temp = 0;
W25Qxx_CS_ON;
SPI1_ReadWriteByte(W25X_ManufactDeviceID);//
SPI1_ReadWriteByte(0x00);
SPI1_ReadWriteByte(0x00);
SPI1_ReadWriteByte(0x00); //
Temp|=SPI1_ReadWriteByte(0xFF)<<8;
Temp|=SPI1_ReadWriteByte(0xFF);
W25Qxx_CS_OFF;
return Temp;
}
//指令表
#define W25X_WriteEnable 0x06
#define W25X_WriteDisable 0x04
#define W25X_ReadStatusReg 0x05
#define W25X_WriteStatusReg 0x01
#define W25X_ReadData 0x03
#define W25X_FastReadData 0x0B
#define W25X_FastReadDual 0x3B
#define W25X_PageProgram 0x02
#define W25X_BlockErase 0xD8
#define W25X_SectorErase 0x20
#define W25X_ChipErase 0