本文学习一下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 (四、无线温湿度传感器 之 串口通讯)
❤️
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是串行外设接口(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个边沿
如下表格:
对于我们的从机设备,比如传感器,支持的模式会使用手册中说明:比如我们今天要测试的 SPI Flash:
来了解一下 RT-Thread 提供的 SPI 设备操作函数:
与前面的设备不同的地方在于,SPI 因为可以一主多从,所以 SPI 设备多了一个挂载操作,就是 RT-Thread 系统驱动会注册好 SPI 总线,然后我们需要把自己所用的 SPI 设备挂载到总线上,使得可以对该设备进行操作 。
☆ 自定义传输数据函数 rt_spi_transfer_message
为核心,其实在其之后的那些都可以使用这个函数来表达,这个下文会说明。☆
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)
上面介绍 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);
前面的两个函数类似于 SPI 的初始化工作,接下来就是我们熟悉的设备操作函数:
I/O 设备模型通用的查找函数:
/*
参数 描述
name SPI 设备名称
返回 ——
设备句柄 查找到对应设备将返回相应的设备句柄
RT_NULL 没有找到设备
*/
rt_device_t rt_device_find(const char* name);
注意事项和 ADC 设备一样,用来接收的设备句柄不是使用rt_device_t
,但是与 ADC 也有不一样的地方,具体如下图:
因为 SPI 设备的接口体并没有 typedef 重定义,所以使用起来还得直接使用结构体指针表示。
自定义传输函数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)
除了上面通用的自定义数据传输函数, 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);
特殊场景部分暂时并不能体会其中的意义,所以这里直接套用官方的说明,等以后再使用过程中如果确实遇到问题,再来更新自己的心得体会。
在一些特殊的使用场景,某个设备希望独占总线一段时间,且期间要保持片选一直有效,期间数据传输可能是间断的,则可以按照如所示步骤使用相关接口。传输数据函数必须使用 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);
与上一篇文章说的 ADC 设备类似,我们可以通过,但是也需要注意他的使用步骤:
在 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 的读取。
我们板载的是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 的组件和软件包。
希望大家多多支持!本文就到这里,谢谢!
审核编辑:符乾江
全部0条评论
快来发表一下你的评论吧 !