本章目标,
1、学习SPI的通讯原理以及stm32spi外设
2、通过软件SPI和硬件SPI分别读写W25Q64芯片
暂时没有完结(8.30号只写了软件模拟spi测试,硬件还没写,测试完成之后会拿几款spi的芯片做测试,这周先把can写完,之后回更新硬件spi的)
一、原理部分
1、什么是SPI通讯
SPI通讯相对于IIC来说要更加的简单、因为IIC想用一根线解决的事情直接被SPI分出了三根线去解决,IIC只有一根数据线,要完成开始、停止、输入、输出、输出应答、输入应答这么多信号有些麻烦。
但是在SPI中变得相对简单,SPI主机发送数据有专门的MOSI引脚直接发送、接收有MISO接收、甚至设备号都不需要,大家学了IIC就知道通过IIC写数据我们得先发送其实信号,之后发送设备号,然后是指定地址、指定数据,这个发送设备号是需要我们发送完起始信号接着发一个字节数据来确定的(废话有点多,就当复习下IIC好吧),但是SPI不需要,他是每个从机都有一根信号线,我要用你我直接操作你的信号线(土豪哥玩法),然后发送接收数据都有单独的线,这个发送接收的时序和方式后面写到了再说。
然后这个协议是全双工,可以同时收发,但是只能说一主动从,从机的片选线必须连接到主机。
2 SPI硬件连接图
和IIC一样,这里还是以总线的方式连接的,除了SS这种每个从机一根的片选信号,然后输出的MOSI位推挽输出,输入的MISO为了防止两个设备同时相应造成短路,所以这里主机没有呼叫到的时候他们片选线都为高组态的状态(这时候引脚就相当于没有输出,防止有两个从机同时输出造成短路,因为SPI是可以推挽输出的这也就让传输的速率提高了很多,相比于IIC的几百k,SPI能到达几十兆甚至百兆,IIC由于是若上拉,所以信号从低跳到高还是有很大的延迟的)
3 数据交换示意图
SPI主机和从机的数据都来自与各自的移位寄存器,这里在时钟的上升沿或者下降会触发一次数据移位(这个上升或者下降触发是看你自己配置的模式后面会写模式)
在第一个时钟沿,会把最左边(高位)的数据放到数据发送寄存器上,然后左移,这时候会把MOSI和MISO置为为相同的电平,这是发送的过程。
在下一个时钟沿到来时,主机从机会读取数据,这个时候就把数据存放到刚刚的最低位就会形成这样,
然后进过八个时钟信号(8个完整的上升下降)最后两个移位寄存器的数据就交换了,这里发送的数据也到了从机,从机返回的数据也到了主机,虽然是实现了全双工是吧(但是我们大多数场景就是单读和单写,所以另外一边的寄存器大概率是随便填写一些数据能把对方的数据换过来就行的,直接造成了资源浪费,但是没关系- -快和方便嘛)
4 协议时序图
1、起始和停止
这个起始和停止对比IIC什么SCK和SDA相互配合来完成爽多了好吧,很简单的图,右边是SPI的,左边的IIC的,SPI我们只需要将相应从机的SS,拉低为起始,拉高为结束
2、 交换字节的时序
这个之前说过了,上升沿采集还是发送这个是可以配置的,记得从机不被呼叫他的发送线就得为高阻态哈,虽然这个是从机的事情。
比如这的模式0(也是常用的模式):CPOL:代表空闲时SCK的状态:为0就是空闲时SCK为0
CPHA:为0时第一个边沿移入,第二个溢出
这个模式0和刚刚写的数据移位逻辑有点冲突(但是没办法,比较这个我们得看从机支持哪些,实在不行我们以后自己写从机好吧= =
这个模式下在ss为低时,数据就好立即被放到数据发送寄存器去,然后在等到第一个时钟沿的时候就会对发送寄存器采样,读到数据,然后把数据放在移位寄存器的最低位,然后后面就是重复了。
模式1:
这个就是刚刚那个移位图的逻辑,不管ss输入,我们等到第一个时钟的上升沿移除数据到发送寄存器,下降沿采样发送寄存器发来的数据就好。
模式2
空闲时为高,这样第一个时钟沿就是低了,就变了ss下拉,移位寄存器移出数据,第一个时钟线时采样数据,第二个线(和ss的下拉信号这两个信号)移除
模式3
CPOL=1:空闲时为高,所以第一个时钟沿为下降嘛,然后CPHA相位这里为1我的理解就是不反向的意思把(没去具体看手册)这里就是第一个边沿移出,第二个边沿采样。
5 然后我们来看一下数据发送和接收过程中的采样图
1、发送一个指令
在片选线拉低后的第一个数据一般为指令比如这个0x0(这个功能码是由厂商自己去规定的,倒到时候要用了去查一下手册就好) 这个时候就是主机的移位寄存器去换取从机的一个全FF的数据(等于是没有数据),这里用的是模式0,前面说了这个模式用的比较多,在cs下拉的时候就移出数据了,在第一个时钟沿的时候采样,此时为0,直到后面有上升沿为1的部分,可以看出是上升沿采样,这个每一条都是画线对应了的。
2 发送一个写数据
首先我们发送写指令(这里为0x02)然后3字节的地址为,为什么是3字节后面写w25q64的时候详细写,最后跟一个字节自己想要写入的数据即可(图片看不清没关系,这里只了解一个时序组合
3 接收数据
一样的,指令码+地址号然后代码读数据,这个返回的数据是从机返回的你只管读就对了,我们写的是主机不是从机
6 w25q64的介绍
w25q64的介绍,首先他是掉电不丢失的,可以用来存放单片机需要掉电保存的数据,其次频率为80m这是SPI的上线,但不是w25q64的因为他还能把其他线配置为输入或者输出,达到3通道输入这种操作。
然后是容量我们这次用的q64就是64mbit换算为兆字节就除以8为8兆字节。然后24位模式最大的存储量就是16m,这个w25q256芯片会有4字节模式,如果下读取后面16字节就得进入4字节模式了
7 w25q64硬件电路
8个引脚,除了我前面说的4跟线MISO MOSC CS CLK外除去VCC和GND还有2跟
这2跟一个位写保护一个位数据保持,前面得什么3倍速两倍速就是可以把这写线改为输入输出
wp:低电平有效 :硬件保护 一般我们都不用
HOLD:数据保持:在读写数据得时候有其他中断运行得时候,如果有数据保持,拿cs就不会跳为0让数据中断,会保持住当前状态,然后中断执行完之后继续刚刚读数据。
8 内存得划分
整体图
设备分区一般是将一个存储器先分为若干快,再将若干块分为扇区,再把扇区分页
将整个存储器分页,将8m字节分为每个页64k,这样就能分出128块得数据,然后从000000一直到7ffff 前面说了哈24位最大寻址是16m 我们这里是q64只有8m所以只能用到7ff哈
分扇区
这里将分出来得128个块分为扇区 每个64k得块可以分位16个4kb的扇区(这个4k真不是瞎分的好吧,这个可以看的出来只有分就只变动后面2个字节了,从xx0000一直到xxFFFF因为这个前面分为了64k,2的16次方就是64k了,刚好占了两个字节嘛。
最后是页地址了
每256字节为一个页,所以刚刚的4k我们就能分为16个页,然后页地址变化就是最后一个字节,从000000到0000ff 然后看箭头指向的小标那里写了页地址 上一个扇区图也有哈会从xx0000到xx00ff这样,每一行是一个页号。就是刚好只变最后一个字节的存储计数为255字节
1 高电压发生器 (用来实现掉电不丢失的设备 高电压让存储器留下掉电不会丢失的痕迹
2 页地址锁存器 站高16位就是xxxx00 的前四位来控制
3 字节锁存器 站低8位 就是0000xx 发送的低8位就会存储在这里,从指定地址连续读取多字节
4 256kb的缓存 这个是用来转存写入数据,一次写入为1字节 ,所以把我们存入的字节存入,然后一次写入到芯片里面去。
9 写入读取限制
写入 限制比较多一天天看。
1 写入操作前写使能:防止误操作,我们直接发送一个写使能指令给芯片就行
2 只能1改0 不能0改1 所以数据不能直接覆盖 比如0xaa改为0x55
这个0xaa就是0x1010
0x55就是0x0101 这样按照特性特性来说你直接覆盖芯片数据就好变为0000因为0是不能改为1的,这个就可以和第三个性质连起来看了。
3 每次必须擦除 可以和4连着看
4 每次必须按一个块或者一个扇区 就是最小4096字节= = 很离谱,你只能先把一个扇区的东西全擦掉,连按页擦除都不能支持。
5 最多只能写入256字节 写入多了会返回到页首从新写,并且和页地址是对应的,你从页中心开始写,这个页缓存也会从中心开始写,你就写不到256字节了。在写入后芯片会进入忙状态,这个时候可以查询状态寄存器,在芯片忙的时候芯片不会相应任何操作。
读操作
基本没什么限制:1 就是前面说到的在写入数据芯片进入一段时间的忙状态时不能读
2 直接读取,没有页的限制,不需要使能
状态寄存器
这里有两个参数:一个是刚刚说的写入后有一个忙状态,就是可以读取这个BUSY 忙的时候写1不忙的时候清零
使能:写入时写使能,执行了写失能、执行写入数据后会自动使能,所以一个写使能只能保证后面的一次写入。
指令集
1 很重要的设备ID号
确定设备是否工作正常的号,不是IIC那种设备号哦,发送指令 ,厂商ID是EF 设备ID根据两条发送的指令,会有不同的回复(这个我们可以后面写代码的时候看
2 部分常用指令集
先看划线的这几个,其他的可以自己做一下了解
1 使能指令 0x06
2 读状态寄存器1 0x05 这个状态寄存器的图我放在下面了,主要就是要他的BUSY位嘛
3 写入指令 0x20 先拉低片选,然后使能 发送写入指令 跟上3字节地址 之后跟上数据(d7-d0)
4 擦除指令 按4k(扇区擦除)直接3字节地址
5 ID号发送0x9f 然后读取返回3个字节,1个字节的厂商id 后两个字节是设备id
其他的自己看看基本都差不多,全部擦除的指令也是有的
最后还差个read data
第一行 先发送0x03 然后地址号 最后是接收数据
之后就可以开始写SPI和读取w25q64的代码了
二、代码部分
1 SPI协议的代码(模拟SPI
这模拟四根线的时序(MISO MOSI SS CLK先确定一下这四根线怎么连,我这里直接连接了硬件SPI的线,这个大家可以打开数据手册根据自己的想法选几条线啊,你随便配几个GPIO出来模拟也可以,但是可以后面还要写硬件的SPI这里就不是很想改引脚了
因为我这直接使用的板载芯片,所以不能按照自己想的配置这里先查查原理图,处理cs引脚,其他的和我刚刚选的一样。
然后我们就可以初始化引脚了
前面也说了MOSI可以配置为推挽,因为是和MISO分开的,不会有什么正负电压的冲突,然后MISO就不能是推挽了只能是上拉或者浮空,sck也为推挽输出这里实在不知道的可以去参考GPIO章节的外设引脚配置
引脚配置代码(和刚刚描述的是一样的,记得开APB2的时钟,时钟没开等于人心脏没跳好吧
void Myspi_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP;//GPIO_Mode_Out_PP
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP; //ss
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_0;
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOC,&GPIO_InitStruct);
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_IPU;//GPIO_Mode_IPU MISO
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_6;
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
Myspi_W_SS(1);
Myspi_W_SCK(0);
}
然后是写出信号线操作
1 SS 2 CLK 3 MOSI 4 MISO前面三个只需要发送即可,最后的读我们只需要读引脚电平就好
void Myspi_W_SS(uint8_t Bitvalue)
{
GPIO_WriteBit(GPIOC,GPIO_Pin_0,(BitAction)Bitvalue);
}
void Myspi_W_SCK(uint8_t Bitvalue)
{
GPIO_WriteBit(GPIOA,GPIO_Pin_5,(BitAction)Bitvalue);
}
void Myspi_W_MOSI(uint8_t Bitvalue)
{
GPIO_WriteBit(GPIOA,GPIO_Pin_7,(BitAction)Bitvalue);
}
uint8_t Myspi_R_MISO(void)
{
return GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6);
}
写好了信号线操作,接着就是模拟时序了在这之前去在刚刚的初始化最后加上把电平初始化
讲ss拉高初始化,然后sck拉低(这个可以照着模式0来配置,因为存储芯片配的是模式0.
Muspi_W_SS(1);
Myspi_W_SCK(0);
1 起始 结束
回忆一下时序,就是ss拉低拉高就行很简单找到ss的引脚号模拟ss的
void Myspi_Start(void)
{
Myspi_W_SS(0);
}
void Myspi_End(void)
{
Myspi_W_SS(1);
}
2 交换数据
主机主动控制MOSI讲数据通过MOSI发送出去,这时候从机也会把数据移到MISO我们读取数据就好,但是这里软件模拟有个细节要注意:就是是信号线先跳我们再放数据,不是把数据放上去再改变信号线。(硬件上可以算是同时发生,但是我们软件上肯定是不行
uint8_t Myspi_WritrRead(uint8_t ByteSend)
{
uint8_t ByteReceive=0x00;
uint8_t i=0;
for(i=0;i<8;i++)
{
Myspi_W_MOSI(ByteSend & (0x80 >>i));
Myspi_W_SCK(1);
if(Myspi_R_MISO()==1)
{
ByteReceive |= (0x80 >> i);
}
Myspi_W_SCK(0);
}
return ByteReceive;
}
交换数据
接着开始的信号,开始信号拉低了ss,这个时候就能把数据发出去了(只管主机,从机代码不是我们写的)发出去之后拉高时钟线(这里我们用的模式0,时钟线空闲时为0,所以第一个操作只能是拉高)拉高之后就能执行读取的操作了,就是讲函数写入的字节,一位一位的发出去,我们将发送的数据与0x80相与就能发出最高位,之后在与0x40相与就能发出第二位,这样我们直接写一个循环,让0x80依次右移一位,0x1000 0000 这样有数据的位就能发送出去了。
发送一位接收一位,这个时候先将sck拉下来,然后这个时候接收数据,用一个中间变量先存储数据,判断信号线是否为1,如果为1就拿中间量与0x80相或,此时中间量为0x00 与0x80相或就行,然后也右移,最后拉低sck(在第二个时钟沿和cs拉低的线为低)
最后就存储的数据返回回去,这样SPI协议(模式0)就写完了,如果有设备支持的其他模式自己跟着时序图改改就好。
然后我们测试读取设备号
首先发送起始信号,然后交换一个0x9f过去,这个时候从机就会返回数据了,我们随便丢3个字节接收返回的数据,前两个字节为
void W25Q64Init(void)
{
Myspi_GPIO_Init();
}
void W25Q64_ReadID(uint8_t *Mid,uint16_t *Did)
{
Myspi_Start();
Myspi_WritrRead(0x9f);
*Mid=Myspi_WritrRead(0xFF);
*Did=Myspi_WritrRead(0xFF);
*Did <<= 8;
*Did |= Myspi_WritrRead(0xFF);
Myspi_End();
}
这里我们发送9f,前一个字节会接收到manufacturer后面两个会接收到deviceid,我们拿两个变量存一下,取mid和did的地址传进来,直接解引用操作原数据(不然就创4个中间变量,然后分别存到3个中间变量,最后将这个3个中间量,存入想要返回的变量- -不然你不可能保存出一个形参的变量,也不能同时返回3个形参变量)
测试返回
int main(void)
{
u8 Mid;
u16 Did;
/*初始化USART 配置模式为 115200 8-N-1,中断接收*/
USART_Config();
delay_init();
Myiic_GPIO_Init();
W25Q64Init();
//发送设备号测试是否有应答信号
W25Q64_ReadID(&Mid,&Did);
printf("%x %x\r\n",Mid,Did);
while(1)
{
}
}
看看SPI协议是不是写对了(最后返回确实是ef 4017 证明SPI协议是没有问题的
2 硬件SPI