RT-Thread记录(十五、I/O 设备模型之SPI设备)

本文学习一下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 中断处理与阶段小结)
❤️
在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 通讯基本知识不过多介绍,原理与基础可自行网上查询,本文这里只做应用所需的简单概述:

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_takecs_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 的组件和软件包。

希望大家多多支持!本文就到这里,谢谢!

猜你喜欢

转载自blog.csdn.net/weixin_42328389/article/details/125105796