本文学习一下I/O 设备模型之SPI设备使用,I/O 设备模型篇的最后一篇文章。
前言
本文应该是 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 中断处理与阶段小结)
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 通讯有4中模式,由 CPOL (时钟的极性)和 CPHA (时钟的相位)决定:
CPOL=0,表示当SCLK=0时处于空闲态,空闲低电平,所以有效状态就是SCLK处于高电平时
CPOL=1,表示当SCLK=1时处于空闲态,空闲高电平,所以有效状态就是SCLK处于低电平时
CPHA=0,表示数据采样是在第1个边沿
CPHA=1,表示数据采样是在第2个边沿
如下表格:
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 驱动注册完 SPI 总线,需要用 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,释放片选。
示例 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);
示例 2 ,先发后收(主要关注最后两个参数的设置):
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);
示例 3 ,假如有3个 message:
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的 使用步骤的注释:
.
1、首先,在 RT-Thread Studio 工程中,打开 RT-Thread Settings,使能 SPI 驱动,如下图所示:
.
.
2、 宏定义 #define BSP_USING_SPI1
(根据自己使用的设备硬件连接定义):
.
比如我使用的开发板原理图(忽略当时的引脚标号,这里应该是 SPI1,当时写标号居然写的是 SPI2 ):
查看对应的手册资料:
所以我们需要使能的是 SPI1:
.
3、通过STM32CubeMX 配置 SPI :
.
和上一篇文章的 ADC 设备一样进行操作,如下图:
到这一步,我们已经能够找到我们需要的 HAL_SPI_MspInit
文件了,通过 spi.h
头文件找到 spi.c
文件中的这个函数:
.
4、 把HAL_SPI_MspInit
函数复制到 board.c
文件最后面,如下图:
.
.
5. 查看 stm32xxxx_hal_config.h
文件SPI 模块是否使能:
.
在上一篇文章 ADC 步骤中已经讲解过,使用 STM32CubeMX 设置以后,文件会自动使能:
到这里 SPI 的配置就算全部完成了,我们可以直接在应用程序中,使用 SPI 设备操作函数实现 SPI 的读取。
3.2 测试
我们板载的是SPI设备是 W25Q128 ,我们测试一下 RT-Thread 的 SPI 设备模型是否能够正常通行,这里只做简单的读取 ID 的测试,官方的示例也是针对 W25Qxx 系列的,但是我还是按照自己的理解来进行。
第一步:检查 spi 总线
我们根据上面的使用步骤,配置好 SPI ,我们应用程序什么都不操作,看看初始化以后是否有 spi1 总线设备,如下图:
.
第二步:挂载 spi 设备至 spi 总线
确认了上电初始化以后 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 0xC7
#define W25X_PowerDown 0xB9
#define W25X_ReleasePowerDown 0xAB
#define W25X_DeviceID 0xAB
#define W25X_ManufactDeviceID 0x90
#define W25X_JedecDeviceID 0x9F
上电的时候读取一次设备的 ID,如果 读取的 ID 正常,说明设备正常,可以进行接下来的通讯。
通过上面的操作我们可以看到这个操作流程,先发送一个字节消息(读取指令), 然后再读取 5个字节的消息,第 4个字节和第5个字节就是 SPI Flash 的设备ID (数据宽度 8 位),通过手册我们可以可以看到说明:
搞清楚了流程,下面的读取代码,其实和官方基本一样:
测试结果:
测试出来居然是反了,这个倒是无所谓,因为简单,反的原因这里不深究了。
当然上面是用的自定义数据传输函数rt_spi_transfer_message
实现,我们也可以通过上面讲到的先发送后接收数据函数rt_spi_send_then_recv
实现:
可以看到使用这种专有函数,程序会更加简单,但是我更加建议使用自定义,因为可以满足不同需求。
结语
本文我们学习了 RT-Thread 中 SPI 设备的使用方法,最终也通过简单的测试成功操作了 SPI 设备。
但是我们并没有进行正真的数据读写,在实际应用中,我们需要用到不同的 SPI 设备,就算是 SPI Flash 这一种设备,都有不同厂家不同型号的,难免有不同之处。
RT-Thread 有一个很大的特点在于他的生态比一般的 RTOS 完善,我们在实际应用中,有许许多多现成的官方或者很多开发者提供的组件或者软件包,我们可以直接导入工程进行使用。
比如就本文我们学习的 SPI 设备,我们就可以使用官方标准的组件 — SFUD组件。
对于RT-Thread 设备模型篇的内容,我也就更新到这篇文章,接下来就要开始学习使用 RT-Thread 的组件和软件包。
希望大家多多支持!本文就到这里,谢谢!