STM32F1之SPI通信·软件SPI代码编写

2024-05-25 07:52

本文主要是介绍STM32F1之SPI通信·软件SPI代码编写,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

1.  简介

2.  硬件电路

移位示意图

3.  SPI时序基本单元

3.1  起始条件

3.2  终止条件

3.3  交换一个字节(模式0)

3.4  交换一个字节(模式1)

3.5  交换一个字节(模式2)

3.6  交换一个字节(模式3)

4.  代码编写

4.1  引脚初始化

4.2  引脚置高低电平封装

4.2.1  SPI写SS引脚电平

4.2.2  SPI写SCK引脚电平

4.2.3  SPI写MOSI引脚电平

4.2.4  I2C读MISO引脚电平

4.3  SPI起始

4.4  SPI终止

4.5  SPI交换传输一个字节

4.5.1  模式0

4.5.2  模式1

4.5.2  模式2

4.5.2  模式3


1.  简介

        SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线。同步,全双工。支持总线挂载多设备(一主多从)。

四根通信线:SCK(Serial Clock)串行时钟线;

                      MOSI(Master Output Slave Input)主机输出从机输入;

                      MISO(Master Input Slave Output)主机输入从机输出;

                      SS(Slave Select)从机选择(若是有多个从机,有几个从机就有几条SS线,可见硬件电路中的连接图)。

2.  硬件电路

        所有SPI设备的SCK、MOSI、MISO分别连在一起;

        主机另外引出多条SS控制线,分别接到各从机的SS引脚;

        输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入。

移位示意图

        工作原理,假如主机想要发送一个字节给从机,从机也想发送一个字节给主机,开始,当SCK处于上升沿移位寄存器最左边数据移出,例如SPI主机中移位寄存器最左边“1”,移出到MOSI引脚上,而SPI从机的移位寄存器的最左边的数据“0”,移出到MISO引脚上,当SCK处于下降沿,MOSI上的数据进入到SPI从机的移位寄存器最右边,MISO上的数据进入到SPI主机的移位寄存器最右边。往复八次经过时钟的上升沿和下降沿,即可完成相互发送一个字节数据。

         当多个从机输出连在一起,如果同时开启输出,会造成冲突,解决方法是,当SS未被选中的状态,从机的MISO引脚必须关断输出,即配置为高阻态。

3.  SPI时序基本单元

3.1  起始条件

        SS从高电平切换到低电平

3.2  终止条件

        SS从低电平切换到高电平

3.3  交换一个字节(模式0)

        CPOL=0:空闲状态时,SCK为低电平

        CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据

3.4  交换一个字节(模式1)

        CPOL=0:空闲状态时,SCK为低电平

        CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据

3.5  交换一个字节(模式2)

        CPOL=1:空闲状态时,SCK为高电平

        CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据

3.6  交换一个字节(模式3)

        CPOL=1:空闲状态时,SCK为高电平

        CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据

4.  代码编写

4.1  引脚初始化

void MySPI_Init(void)
{/*开启时钟*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA4、PA5和PA7引脚初始化为推挽输出GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA6引脚初始化为上拉输入/*设置默认电平*/MySPI_W_SS(1);											//SS默认高电平MySPI_W_SCK(0);											//SCK默认低电平
}

其中,MySPI_W_SS(1); 和 MySPI_W_SCK(0);为封装函数,可以参照下一条。                           

4.2  引脚置高低电平封装

        为了后续代码的编写方便,我们可以将,初始化的引脚进行封装。

4.2.1  SPI写SS引脚电平

        此函数需要用户实现内容,当BitValue为0时,需要置SS为低电平,当BitValue为1时,需要置SS为高电平。

void MySPI_W_SS(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);		//根据BitValue,设置SS引脚的电平
}
4.2.2  SPI写SCK引脚电平

        此函数需要用户实现内容,当BitValue为0时,需要置SCK为低电平,当BitValue为1时,需要置SCK为高电平。

void MySPI_W_SCK(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);		//根据BitValue,设置SCK引脚的电平
}
4.2.3  SPI写MOSI引脚电平

        此函数需要用户实现内容,当BitValue为0时,需要置MOSI为低电平,当BitValue非0时,需要置MOSI为高电平。

void MySPI_W_MOSI(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);		//根据BitValue,设置MOSI引脚的电平,BitValue要实现非0即1的特性
}
4.2.4  I2C读MISO引脚电平

        此函数需要用户实现内容,当前MISO为低电平时,返回0,当前MISO为高电平时,返回1。

uint8_t MySPI_R_MISO(void)
{return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);			//读取MISO电平并返回
}

也可以参考:

STM32F1之I2C通信·软件I2C代码编写-CSDN博客

进行其他方法也能实现同样功能。

4.3  SPI起始

        根据3.1我们可以看出SPI起始只需要将SS拉低就可以开始时序。

void MySPI_Start(void)
{MySPI_W_SS(0);				//拉低SS,开始时序
}

4.4  SPI终止

        同理,根据3.2我们可以看出SPI起始只需要将SS拉高就可以结束时序。

void MySPI_Stop(void)
{MySPI_W_SS(1);				//拉高SS,终止时序
}

4.5  SPI交换传输一个字节

4.5.1  模式0

        这里需要注意一下,在SPI中对于硬件SPI来说,由于使用了硬件的移位寄存器电路,所以下图中黄色部分几乎是同时发生的,但是对于软件SPI来说程序执行需要一条一条执行,有一个先后顺序,因此我们可以将这里看成一个先后执行的逻辑:

         因此我们可以将其传送一位数据的流程如下,先SS下降,再移出数据,在SCK上升沿,在移入数据,在SCK下降沿,再移出数据。

uint8_t MySPI_SwapByte(uint8_t ByteSend)
{uint8_t ByteReceive = 0x00;					//定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到MySPI_W_MOSI(ByteSend & 0x80);		//使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线MySPI_W_SCK(1);								//拉高SCK,上升沿移出数据if (MySPI_R_MISO() == 1){ByteReceive |= 0x80;}	//读取MISO数据,并存储到Byte变量//当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0MySPI_W_SCK(0);								//拉低SCK,下降沿移入数据MySPI_W_MOSI(ByteSend & 0x40);		//使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线MySPI_W_SCK(1);								//拉高SCK,上升沿移出数据if (MySPI_R_MISO() == 1){ByteReceive |= 0x40;}	//读取MISO数据,并存储到Byte变量//当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0MySPI_W_SCK(0);	MySPI_W_MOSI(ByteSend & 0x20);		//使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线MySPI_W_SCK(1);								//拉高SCK,上升沿移出数据if (MySPI_R_MISO() == 1){ByteReceive |= 0x20;}	//读取MISO数据,并存储到Byte变量//当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0MySPI_W_SCK(0);	MySPI_W_MOSI(ByteSend & 0x10);		//使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线MySPI_W_SCK(1);								//拉高SCK,上升沿移出数据if (MySPI_R_MISO() == 1){ByteReceive |= 0x10;}	//读取MISO数据,并存储到Byte变量//当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0MySPI_W_SCK(0);	MySPI_W_MOSI(ByteSend & 0x08);		//使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线MySPI_W_SCK(1);								//拉高SCK,上升沿移出数据if (MySPI_R_MISO() == 1){ByteReceive |= 0x08;}	//读取MISO数据,并存储到Byte变量//当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0MySPI_W_SCK(0);	//......一直移出到第八位return ByteReceive;								//返回接收到的一个字节数据
}

         以上代码太过冗余,我们可以使用for循环来进行实现:

uint8_t MySPI_SwapByte(uint8_t ByteSend)
{uint8_t i, ByteReceive = 0x00;					//定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到for (i = 0; i < 8; i ++)						//循环8次,依次交换每一位数据{MySPI_W_MOSI(ByteSend & (0x80 >> i));		//使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线MySPI_W_SCK(1);								//拉高SCK,上升沿移出数据if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}	//读取MISO数据,并存储到Byte变量//当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0MySPI_W_SCK(0);								//拉低SCK,下降沿移入数据}return ByteReceive;								//返回接收到的一个字节数据
}

        我们还可以根据“2.硬件电路”中的移位示意图中的数据进行操作,编写代码:

uint8_t MySPI_SwapByte(uint8_t ByteSend)
{uint8_t i;					for (i = 0; i < 8; i ++)						//循环8次,依次交换每一位数据{MySPI_W_MOSI(ByteSend & 0x80);ByteSend <<=1;MySPI_W_SCK(1);								//拉高SCK,上升沿移出数据if (MySPI_R_MISO() == 1){ByteReceive |= 0x01;}MySPI_W_SCK(0);								//拉低SCK,下降沿移入数据}return ByteReceive;								//返回接收到的一个字节数据
}

        这种方式相较于上一种代码效率更高,但是原始数据ByteSend会发生改变,因为这种方法是用移位数据本身进行操作的,效率跟高,但是原始数据ByteSend会在移位过程中发生改变,对于上一种方式编写的代码是还有掩码一次提取数据每一位,不会改变参数本身,两种方法皆可使用。

4.5.2  模式1

         我们也可以将其传送一位数据的流程描述如下,先SS下降之后,在SCK上升沿,再移出数据,在SCK下降沿,在移入数据。

uint8_t MySPI_SwapByte(uint8_t ByteSend)
{uint8_t i, ByteReceive = 0x00;					//定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到for (i = 0; i < 8; i ++)						//循环8次,依次交换每一位数据{MySPI_W_SCK(1);								MySPI_W_MOSI(ByteSend & (0x80 >> i));		//使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线MySPI_W_SCK(0);								if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}	//读取MISO数据,并存储到Byte变量//当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0}return ByteReceive;								//返回接收到的一个字节数据
}
4.5.2  模式2

        可以对比模式0,可以发现只是SCK极性相反,只需要将模式0的代码中出现SCK的地方“1”改为“0”,“0”改为“1”即可,将极性翻转一下。(注意初始化中的极性也要进行修改)

4.5.2  模式3

        同理,可以对比模式1,可以发现只是SCK极性相反,只需要将模式0的代码中出现SCK的地方“1”改为“0”,“0”改为“1”即可,将极性翻转一下。(注意初始化中的极性也要进行修改)

这篇关于STM32F1之SPI通信·软件SPI代码编写的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1000914

相关文章

利用Python调试串口的示例代码

《利用Python调试串口的示例代码》在嵌入式开发、物联网设备调试过程中,串口通信是最基础的调试手段本文将带你用Python+ttkbootstrap打造一款高颜值、多功能的串口调试助手,需要的可以了... 目录概述:为什么需要专业的串口调试工具项目架构设计1.1 技术栈选型1.2 关键类说明1.3 线程模

Python Transformers库(NLP处理库)案例代码讲解

《PythonTransformers库(NLP处理库)案例代码讲解》本文介绍transformers库的全面讲解,包含基础知识、高级用法、案例代码及学习路径,内容经过组织,适合不同阶段的学习者,对... 目录一、基础知识1. Transformers 库简介2. 安装与环境配置3. 快速上手示例二、核心模

Java的栈与队列实现代码解析

《Java的栈与队列实现代码解析》栈是常见的线性数据结构,栈的特点是以先进后出的形式,后进先出,先进后出,分为栈底和栈顶,栈应用于内存的分配,表达式求值,存储临时的数据和方法的调用等,本文给大家介绍J... 目录栈的概念(Stack)栈的实现代码队列(Queue)模拟实现队列(双链表实现)循环队列(循环数组

使用Java将DOCX文档解析为Markdown文档的代码实现

《使用Java将DOCX文档解析为Markdown文档的代码实现》在现代文档处理中,Markdown(MD)因其简洁的语法和良好的可读性,逐渐成为开发者、技术写作者和内容创作者的首选格式,然而,许多文... 目录引言1. 工具和库介绍2. 安装依赖库3. 使用Apache POI解析DOCX文档4. 将解析

C++使用printf语句实现进制转换的示例代码

《C++使用printf语句实现进制转换的示例代码》在C语言中,printf函数可以直接实现部分进制转换功能,通过格式说明符(formatspecifier)快速输出不同进制的数值,下面给大家分享C+... 目录一、printf 原生支持的进制转换1. 十进制、八进制、十六进制转换2. 显示进制前缀3. 指

使用Python实现全能手机虚拟键盘的示例代码

《使用Python实现全能手机虚拟键盘的示例代码》在数字化办公时代,你是否遇到过这样的场景:会议室投影电脑突然键盘失灵、躺在沙发上想远程控制书房电脑、或者需要给长辈远程协助操作?今天我要分享的Pyth... 目录一、项目概述:不止于键盘的远程控制方案1.1 创新价值1.2 技术栈全景二、需求实现步骤一、需求

Java中Date、LocalDate、LocalDateTime、LocalTime、时间戳之间的相互转换代码

《Java中Date、LocalDate、LocalDateTime、LocalTime、时间戳之间的相互转换代码》:本文主要介绍Java中日期时间转换的多种方法,包括将Date转换为LocalD... 目录一、Date转LocalDateTime二、Date转LocalDate三、LocalDateTim

jupyter代码块没有运行图标的解决方案

《jupyter代码块没有运行图标的解决方案》:本文主要介绍jupyter代码块没有运行图标的解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录jupyter代码块没有运行图标的解决1.找到Jupyter notebook的系统配置文件2.这时候一般会搜索到

Python通过模块化开发优化代码的技巧分享

《Python通过模块化开发优化代码的技巧分享》模块化开发就是把代码拆成一个个“零件”,该封装封装,该拆分拆分,下面小编就来和大家简单聊聊python如何用模块化开发进行代码优化吧... 目录什么是模块化开发如何拆分代码改进版:拆分成模块让模块更强大:使用 __init__.py你一定会遇到的问题模www.

springboot循环依赖问题案例代码及解决办法

《springboot循环依赖问题案例代码及解决办法》在SpringBoot中,如果两个或多个Bean之间存在循环依赖(即BeanA依赖BeanB,而BeanB又依赖BeanA),会导致Spring的... 目录1. 什么是循环依赖?2. 循环依赖的场景案例3. 解决循环依赖的常见方法方法 1:使用 @La