SPI总线

本文介绍了STM32F407中SPI总线的工作原理,包括USART和SPI的区别,以及SPI的时钟极性(CPOL)和时钟相位(CPHA)的设置。同时详细讲解了只读存储器(ROM)、随机存储器(RAM)和Flash的不同类型及其特点。最后展示了如何使用软件模拟SPI进行数据通信,如W25Q128的初始化和数据操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、原理

设备和外界传输时,是通过USART串口传输的,它是异步全双工串行总线,速度主要是9600, 38400,115200bsp等等,速率不高。

SPI总线就是同步全双通串行总线,最高速率可以达到10M/S

SCLK:时钟线。决定SPI总线上的传输速率

MOSI:主机将数据发送到从机的线路,主机输出,从机输入

MISO:从机将数据发送到主机的线路,主机输入,从机输出

SS/CS:片选信号,当设置为低电平时,主机和从机进行通信

CPOL:时钟极性

  • 当CPOL为0时,时钟空闲时电平为低;
  • 当CPOL为1时,时钟空闲时电平为高;

CPHA:时钟相位

  • 当CPHA为0时,时钟周期的前边缘采集数据(读数据),后边缘输出数据(发送数据)
  • 当CPHA为1时,时钟周期的后边缘采集数据(读数据),前边缘输出数据(发送数据)
SPI 模式CPOLCPHA空闲状态下的时钟极性用于采样和/或移位数据的时钟相位
000逻辑低电平数据在上升沿采样(读取数),在下降沿(发送数据)切换
101逻辑低电平数据在下降沿采样,在上升沿切换
210逻辑高电平数据在下降沿采样,在上升沿切换
311逻辑高电平数据在上升沿(后边沿)采样,在下降(前边沿)沿切换

二、 存储器分类

  • 只读存储器 Read Only Memory

  • 随机存储器 Random Access Memory

  • FLash

2.1ROM

MASK ROM:厂商在制造过程中设置的数据,不可修改。
PROM: 可编程只读存储器,可以写入一次。也被称为一次性可编程只读存储器。
EPROM: 断电后仍然能够保存数据的只读存储器,但是它可以通过强紫外线擦除。无法电擦除。
EEPROM:可读可写的的ROM,可以按照字节单位进行读写操作。速度特别慢。一般的EEPROM可擦除次数不会超过100W次,体积一般在几十KB~512KB之间。保存事件特别长,至少100年以上。

2.2 RAM

随机存储器,也被称为易失型存储器 ​

RAM与CPU进行数据交换,速度特别快,但是他会掉电丢失。电脑上的内存条,就是RAM的一种,保存系统正在运行过程中的临时数据,当电源关闭失,数据丢失,RAM不保存数据。

​ 静态RAM: static RAM ----SRAM 速度最快,不需要电刷新即可保存数据,但非常昂贵。

​ 动态RAM:dynamic RAM ----DRAM 需要电刷新来保存数据,并且长时间不通电,里面的数据也会丢失。平时电脑里面的内存条,就是DRAM。

​ RAM存储0还是1由电荷决定,当电容中的电荷大于1/2时,存储器表示的是1,当小于1/2时,存储器表示的是0。

2.3 FLsh

被称为非易失型存储器

结合了RAM和ROM的优点,可以长时间保存。可编程,可读写,断电不会丢失,可以快读的存取数据。平时的U盘,SD卡,机械硬盘都是flash。

特点: 只能够写0,不能够写1。

Flash在擦除的时候,一般只能够做块擦除,擦除动作上把所有的电容充满电荷,此处存储器表示的是1。当写入数据时,可以对1bit位上的电容进行放电操作,放电完毕后,表示0。 flash无法对1bit的电容做充电操作。

写1清零:把flash清零,表示把它内存上的所有bit全部写1。

#include "stm32f4xx.h"

#include "sys.h"

#include "led.h"
#include "key.h"
#include "delay.h"
#include "beep.h"
#include "sg90.h"
#include "usart.h"
#include "dht11.h"
#include "string.h" //微型标准库
#include "stdio.h"

#define W25Q128_CS 		PBout(14)
void w25q128_init()
{
	GPIO_InitTypeDef a;
	SPI_InitTypeDef  b;
	//初始化GPIO为SPI的复用模式
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB , ENABLE);
	//SCLK--PB3,MISO--PB4,MOSI--PB5S
	a.GPIO_Mode = GPIO_Mode_AF;//复用模式
	a.GPIO_OType= GPIO_OType_PP;//推挽模式
	a.GPIO_Pin = GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5;
	a.GPIO_PuPd = GPIO_PuPd_UP;//上拉电阻
	a.GPIO_Speed = GPIO_High_Speed;//高速模式
	GPIO_Init(GPIOB,&a);
	//指定PB3,PB4,PB5是复用成了SPI1的外设
	GPIO_PinAFConfig(GPIOB,GPIO_PinSource3,GPIO_AF_SPI1);
	GPIO_PinAFConfig(GPIOB,GPIO_PinSource4,GPIO_AF_SPI1);
	GPIO_PinAFConfig(GPIOB,GPIO_PinSource5,GPIO_AF_SPI1);
	a.GPIO_Mode = GPIO_Mode_OUT;//输出模式
	a.GPIO_Pin = GPIO_Pin_14;
	GPIO_Init(GPIOB,&a);
	//空闲时,片选信号为高电平
	W25Q128_CS = 1;
	//使能SPI的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
	//初始化SPI的结构体  最大10M/s SPI1在APB2总线上,输入频率84Mhz
	//84M至少除以9才能降到10M以下,所以设置为16分频
	//根据自己的需求设置的
	b.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
	b.SPI_Mode = SPI_Mode_Master;//主机
	b.SPI_DataSize = SPI_DataSize_8b;//8bit数据
	b.SPI_NSS = SPI_NSS_Soft;//软件触发
	b.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16;//分频
	//必须根据通信的模块来设置---W25Q128设置为模式0
	b.SPI_CPOL = SPI_CPOL_High;//SCLK空闲为高电平
	b.SPI_CPHA = SPI_CPHA_2Edge;//时钟相位是先发送,再接收
	b.SPI_FirstBit = SPI_FirstBit_MSB;//高位先出
	b.SPI_CRCPolynomial = 0;//没有校验位
	SPI_Init(SPI1,&b);
	//开启SPI模块
	SPI_Cmd(SPI1,ENABLE);
}

//全双工的数据收发--- 8次脉冲
uint8_t w25q128_sendByte(uint8_t val)
{
	uint8_t data=0;//接收到的数据
	//等待发送寄存器为空
	while( SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) != SET);
	//发送数据---逐个bit发送,同时接收寄存器开始逐个bit接收数据
	SPI_I2S_SendData(SPI1,val);
	//等待接收完成-----模式3的情况下,前边沿发送,后边沿接收
	//当8个bit全部接收完成时,发送的8bit已经全部发送完毕了
	while( SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) != SET);
	//从接收数据寄存器中读取收到的数据
	data = SPI_I2S_ReceiveData(SPI1);
	return data;
}

//读取厂家和设备ID
void w25q128_read_ID()
{
	uint32_t MID,DID;
	//片选信号拉低
	W25Q128_CS = 0;
	//主机发送0x90指令
	w25q128_sendByte(0x90);
	//主机发送 0x00 0000 指令,用时24次脉冲
	w25q128_sendByte(0x00);
	w25q128_sendByte(0x00);
	w25q128_sendByte(0x00);
	//主机接收到厂家ID
	MID = w25q128_sendByte(0xFF);
	//主机接收到设备ID
	DID = w25q128_sendByte(0xFF);
	//片选信号拉高
	W25Q128_CS = 1;
	printf("MID=%#x DID=%#x\n",MID,DID);	
}

//写使能
void w25q128_write_enable()
{
	//拉低片选信号
	W25Q128_CS = 0;
	//发送指令 0x06
	w25q128_sendByte(0x06);
	//拉高片选信号
	W25Q128_CS = 1;
}

//等待状态寄存器
void w25q128_wait_busy()
{
	uint8_t temp;
	//拉低片选信号
	W25Q128_CS = 0;
	//发送指令 0x05
	w25q128_sendByte(0x05);	
	while(1)
	{
		//发送任意数据,接收到状态寄存器1的值
		temp = w25q128_sendByte(0xFF);		
		//当第0bit---busy位被置0以后,跳出循环
		if( (temp & 0x01) == 0)
			break;
	}	
	//拉高片选信号
	W25Q128_CS = 1;		
}

//擦除块
void w25q128_erase_sertor(uint32_t addr)
{
	//开启写使能
	w25q128_write_enable();
	//片选信号拉低
	W25Q128_CS = 0;	
	//传输指令0x20
	w25q128_sendByte(0x20);	
	//传输24bit的地址 高位先出
	w25q128_sendByte((addr>>16) &0xFF );
	w25q128_sendByte((addr>>8) &0xFF );
	w25q128_sendByte(addr &0xFF );
	//片选信号拉高
	W25Q128_CS = 1;	
	//等待内建时间结束
	w25q128_wait_busy();
	printf("sector erase success\n");
}

//从addr地址上读取len长度的字符到buf缓冲区中
void w25q128_read_buf(uint32_t addr,uint8_t *buf,int len)
{
	//片选信号拉低
	W25Q128_CS = 0;
	//传输指令0x03
	w25q128_sendByte(0x03);
	//传输24bit的地址 高位先出
	w25q128_sendByte((addr>>16) &0xFF );
	w25q128_sendByte((addr>>8) &0xFF );
	w25q128_sendByte(addr &0xFF );	
	while(len--)
	{
		*buf = w25q128_sendByte(0xFF);
		buf++;//缓冲区的地址累加
	}
	//片选信号拉高
	W25Q128_CS = 1;
	printf("w25q128_read_buf success\n");
}

//从addr地址开始写入len长度的buf字符串
void w25q128_write_buf(uint32_t addr,uint8_t *buf,int len)
{
	//开启写使能
	w25q128_write_enable();
	//片选信号拉低
	W25Q128_CS = 0;
	//发送0x02指令
	w25q128_sendByte(0x02);
	//发送24bit的地址,高位先出
	w25q128_sendByte( (addr>>16) & 0xFF );
	w25q128_sendByte( (addr>> 8) & 0xFF );
	w25q128_sendByte( addr & 0xFF );
	while(len--)
	{
		w25q128_sendByte(*buf);
		buf++;//缓冲区地址偏移
	}
	//拉高片选信号
	W25Q128_CS = 1;
	//等待状态寄存器BUSY位被置0
	w25q128_wait_busy();
}

int main()
{
	int i;
	uint8_t Rbuf[32];
	//要设置中断分组,否则默认是分组0,---不存在抢占优先级,只有响应优先级
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	//设置滴答定时器8分频
	SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);
	led_init();
	usart1_dma_init();
	w25q128_init();
	w25q128_read_ID();
	
//	//擦除第0个块的第1个扇区
//	w25q128_erase_sertor(0x001000);
//	
//	//从0x0地址读取32个字节放在Rbuf中
//	w25q128_read_buf(0x001000,Rbuf,32);
//	
//	//打印
//	for(i = 0; i<32;i++)
//		printf("Rbuf[%d] = %#x\n",i,Rbuf[i]);
//	
//	//写入数据
//	w25q128_write_buf(0x001000,(uint8_t*)"gec-stm32f407",14);
//	//读取数据
	w25q128_read_buf(0x001000,Rbuf,14);
	
	printf("%s\n",Rbuf);
	while(1)
	{
		//当串口1接收完成以后
		if( usart1_flag == 1)
		{
			//打印串口1接收到的数据
			printf("usart1 recv: %s",(char*)usart1_buf);	
			//还原参数,必须先关闭DMA,才可以清零缓冲区
			//关闭DMA,还可以把下一次移动位置的下标放在 buf[0]上
			DMA_Cmd(DMA2_Stream2,DISABLE);
			memset((char*)usart1_buf,0,128);
			usart1_flag = 0;	
			//清空缓冲区以后,重新打开DMA
			DMA_Cmd(DMA2_Stream2,ENABLE);
		}
			
		//当串口3接收完成以后----wifi接收到内容
		if( usart3_flag == 1)
		{
			printf("usart3 recv: %s\n",(char*)usart3_buf);	
			//解析json数据
			recvjson();
			DMA_Cmd(DMA1_Stream1,DISABLE);
			memset((char*)usart3_buf,0,128);
			usart3_flag = 0;
			DMA_Cmd(DMA1_Stream1,ENABLE);
		}
	}
}

三、软件SPI

软件SPI也被称为模拟SPI。它指的是利用SPI总线的通信特点,和IO引脚的变化,来模拟SPI通讯。

STM32F407只有3个SPI模块,虽然一个SPI总线上可以挂载多个设备,但实际应用中,可能由于硬件设计的原因,或者工程需要的原因,我们要用3个以上的SPI通讯,就要用到软件模拟。

模拟总线时,对应模块的通讯协议不需要做修改,只需要对开发板上属于GPIO和外设的部分进行修改。

软件模拟时必须考虑该总线的通讯效率,模拟时的脉冲频率,不可以高于总线通讯效率。

#define W25Q128_CS 		PBout(14)
#define W25Q128_SCLK	PBout(3)
#define W25Q128_MOSI	PBout(5)
#define W25Q128_MISO	PBin(4)

//=======================根据STM32外设SPI写出来的,它需要修改=============
//SCLK设置输出模式,因为stm32是主机,它来控制时钟线
//MOSI设置输出模式,因为它需要主动发送0或者1,主动拉高或者拉低电平
//MISO设置输入模式,因为它需要读取0或者1,接收到高电平或者低电平
void w25q128_init()
{
	GPIO_InitTypeDef a;
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB , ENABLE);
	//SCLK--PB3,,MOSI--PB5
	a.GPIO_Mode = GPIO_Mode_OUT;//输出模式
	a.GPIO_OType= GPIO_OType_PP;//推挽模式
	a.GPIO_Pin = GPIO_Pin_3 | GPIO_Pin_5;
	a.GPIO_PuPd = GPIO_PuPd_UP;//上拉电阻
	a.GPIO_Speed = GPIO_High_Speed;//高速模式
	GPIO_Init(GPIOB,&a);
	//MISO--PB4
	a.GPIO_Mode = GPIO_Mode_IN;//输入模式
	a.GPIO_Pin = GPIO_Pin_4;//
	GPIO_Init(GPIOB,&a);
	//CS引脚:若由代码控制收发条件,则设置为输出模式,手动拉高或拉低
	//				若由外界设备控制,则设置为输入或者复用模式。
	a.GPIO_Mode = GPIO_Mode_OUT;//输出模式
	a.GPIO_Pin = GPIO_Pin_14;
	GPIO_Init(GPIOB,&a);
	//通讯模式为SPI mode3,时钟极性为高
	W25Q128_SCLK = 1;
	//空闲时,片选信号为高电平
	W25Q128_CS = 1;
}

//全双工的数据收发--- 8次脉冲
uint8_t w25q128_sendByte(uint8_t val)
{
    uint8_t data=0;//接收到的数据
    int i;
    //SPI mode3 在前边沿发送数据,后边沿接收数据
    //时钟极性为高,那么前边沿是下降沿,后边沿是上升沿
    //根据模块的资料,高位先出,也就是 val 的高位bit先发送
    for(i=0 ;i<8 ; i++)
    {
        //判断val需要发送0还是发送1
        //判断val的7-i位是0还是1,根据结果修改 MOSI的电平
        if( (val>>(7-i)) & 0x01 )
        {
            W25Q128_MOSI = 1;
        }
        else{
            W25Q128_MOSI = 0;
        }

        //前边沿---发送数据
        W25Q128_SCLK = 0;
        //延时--保证前边沿发送数据完成
        delay_us(5);
        //后边沿---读取数据
        W25Q128_SCLK = 1;
        //延时--保证后边沿读取成功
        delay_us(5);
        //判断读取到的是0还是1,高位先读取
        if( W25Q128_MISO == 1)
        {
            data |= 1<<(7-i);
        }
        //如果读到0,应该写1bit的0,但是data初值为0,所以省略
    }
    return data;
}

int main()
{
	int i;
	uint8_t Rbuf[32];
	//要设置中断分组,否则默认是分组0,---不存在抢占优先级,只有响应优先级
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	//设置滴答定时器8分频
	SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);
	usart1_dma_init();
	w25q128_init();
	w25q128_read_ID();
	//擦除第0个块的第1个扇区
	w25q128_erase_sertor(0x001000);
	//从0x0地址读取32个字节放在Rbuf中
	w25q128_read_buf(0x001000,Rbuf,32);
	//打印
	for(i = 0; i<32;i++)
		printf("Rbuf[%d] = %#x\n",i,Rbuf[i]);
	//写入数据
	w25q128_write_buf(0x001000,(uint8_t*)"gec-6818wuhan",14);
	//读取数据
	w25q128_read_buf(0x001000,Rbuf,14);
	printf("%s\n",Rbuf);
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值