(一)SPI协议
SPI和I2C同样是一种通信协议,SPI相对I2C的优势是更快的传输速度,其和I2C一样为同步传输,即拥有一根时钟线,但是SPI拥有两根数据线,一根用于主机发送,一根用于主机接收,每个使用SPI通信的从机还拥有一根寻址线连接主机,主机直接在此线拉低某个从机低电平以实现呼唤从机
通信协议 | 时钟线 | 数据线 | 全/半双工 |
I2C | SCL | SDA | 半双工 |
SPI | SCK | MISO、MOSI | 全双工 |
SPI有四种工作模式,我们可以配置默认电平和在第一个还是第二个触发沿进行采样,我们常用的是模式0,即时钟空闲时为低电平,在上升沿时进行数据采样,和I2C的时钟逻辑一样
- 模式0:CPOL=0,CPHA=0,SCK空闲为低电平,数据在SCK的上升沿被采样。
- 模式1:CPOL=0,CPHA=1,SCK空闲为低电平,数据在SCK的下降沿被采样。
- 模式2:CPOL=1,CPHA=0,SCK空闲为高电平,数据在SCK的下降沿被采样。
- 模式3:CPOL=1,CPHA=1,SCK空闲为高电平,数据在SCK的上升沿被采样。
SPI的发送数据和读取数据是同时进行的,在对指定位置进行写操作时,该位置原本的数据会被读出,即使我们不需要这个数据,和I2C一样,SPI也是高位先行,主机数据的高位通过MOSI进入到从机低位,同时从机高位通过MISO进入主机低位,如图
(二)软件SPI
我们可以通过软件模拟SPI通信,和之前所说,我们需要呼叫某个从设备时,就将其寻址先(SS)拉低为低电平,同时我们选择SPI的模式0,即SCK默认为低电平,上升沿开始采样,这里我们把寻址线(SS)接在PA4口,将时钟线接在PA5口,主机输入(MISO)和主机输出(MOSI)线分别接在PA6和PA7口
#define ss GPIO_Pin_4
#define sck GPIO_Pin_5
#define miso GPIO_Pin_6
#define mosi GPIO_Pin_7
由于我们在软件模拟时需要频繁跳转和读取端口的电平,因此我们把跳转电平函数和读取端口函数进行封装,分别为设置端口高电平,设置端口低电平和读取MISO端口的电平
void spi_set(uint16_t io)
{
GPIO_SetBits(GPIOA, io);
}
void spi_reset(uint16_t io)
{
GPIO_ResetBits(GPIOA, io);
}
char spi_read()
{
return GPIO_ReadInputDataBit(GPIOA, miso);
}
(1)SPI初始化
我们要把对应的连接端口进行初始化,其中时钟线sck和主机发送线MOSI我们设置为推挽输出,这样可以更快地翻转电平,寻址线也可以设置为推挽输出,主机接收线MISO可以设置为上拉输入
void spi_init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef gpio_init;
gpio_init.GPIO_Mode = GPIO_Mode_Out_PP;
gpio_init.GPIO_Pin = ss | sck | mosi;
gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init);
gpio_init.GPIO_Mode = GPIO_Mode_IPU;
gpio_init.GPIO_Pin = miso;
gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init);
spi_set(ss);
spi_reset(sck);
}
最后我们把ss置高电平,即不呼唤从机,sck置低电平,即空闲状态
(2)开始和结束传输
我们要对指定的从机进行呼叫,只需要把其SS线置低电平,如果要停止对从机的传输,只要把SS线置高电平,开始和结束都只需要一句代码
开始传输
void spi_start()
{
spi_reset(ss);
}
结束传输
void spi_end()
{
spi_set(ss);
}
(3)交换数据
前面说过,我们选择的是模式0,即在上升沿采样,同时从机会自动地把其高位放置到主机的最低位中,我们循环八次,就可以完成主机和从机的数据交换了
这里我们在时钟sck低电平期间把发送的数据放置在MOSI上,拉高时钟电平发送,与此同时从机会自动把高位数据放置MISO上,我们只需要读取MISO引脚上的电平即可,随后恢复时钟线为默认值,为下一次交换数据做准备,循环八次
unsigned char spi_swap_byte(unsigned char inf)
{
unsigned char result = 0x00;
unsigned char i;
for (i = 0; i < 8; i++)
{
if (inf & (0x80>>i))
{
spi_set(mosi);
}
else
{
spi_reset(mosi);
}
spi_set(sck);
if (spi_read())
{
result |= (0x80>>i);
}
spi_reset(sck);
}
return result;
}
如果我们只要发送数据,不必理会返回值即可,如果我们要接收数据,我们要先发送一些无用的数据,例如0xFF等;
(4)封装及声明
我们只要声明初始化函数、开始结束传输函数和交换数据函数即可,最后.c 和 .h如下
#include "stm32f10x.h" // Device header
#define ss GPIO_Pin_4
#define sck GPIO_Pin_5
#define miso GPIO_Pin_6
#define mosi GPIO_Pin_7
void spi_set(uint16_t io)
{
GPIO_SetBits(GPIOA, io);
}
void spi_reset(uint16_t io)
{
GPIO_ResetBits(GPIOA, io);
}
char spi_read()
{
return GPIO_ReadInputDataBit(GPIOA, miso);
}
void spi_init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef gpio_init;
gpio_init.GPIO_Mode = GPIO_Mode_Out_PP;
gpio_init.GPIO_Pin = ss | sck | mosi;
gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init);
gpio_init.GPIO_Mode = GPIO_Mode_IPU;
gpio_init.GPIO_Pin = miso;
gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init);
spi_set(ss);
spi_reset(sck);
}
void spi_start()
{
spi_reset(ss);
}
void spi_end()
{
spi_set(ss);
}
unsigned char spi_swap_byte(unsigned char inf)
{
unsigned char result = 0x00;
unsigned char i;
for (i = 0; i < 8; i++)
{
if (inf & (0x80>>i))
{
spi_set(mosi);
}
else
{
spi_reset(mosi);
}
spi_set(sck);
if (spi_read())
{
result |= (0x80>>i);
}
spi_reset(sck);
}
return result;
}
#ifndef __SPI_H__
#define __SPI_H__
void spi_init(void);
void spi_start(void);
void spi_end(void);
unsigned char spi_swap_byte(unsigned char inf);
#endif
(三)硬件SPI
stm32中集成有SPI硬件电路,如果我们要用硬件SPI,则要把对应的端口接入对应的引脚,这里使用的是SPI1,对应引脚和之前软件引脚一样
打开SPI的标准函数库,可以了解一些硬件SPI常用的函数
void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);
//通过初始化结构体初始化SPI
void SPI_StructInit(SPI_InitTypeDef* SPI_InitStruct);
//给SPI结构体配置默认值
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);
//使能SPI,初始化完成后必须打开SPI才开始工作
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
//发送数据
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);
//接收数据
void SPI_DataSizeConfig(SPI_TypeDef* SPIx, uint16_t SPI_DataSize);
//选择数据大小,8字节或16字节,初始化中有配置,这里没用到
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
//获取标志位信息,我们要获取发送寄存器空以写入数据,获取接收寄存器非空以读取数据
void SPI_I2S_ClearFlag(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
//清除标志位,这里发送寄存器和接收寄存器的标志位都是硬件清除,其他标志位可能要使用软件手动清除
ITStatus SPI_I2S_GetITStatus(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);
//获取中断标志位
void SPI_I2S_ClearITPendingBit(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);
//清除中断标志位
有了这些函数,我们就可以使用硬件SPI了
(1)SPI初始化
(1)打开时钟
我们要打开SPI时钟和对应GPIO时钟,这里SPI1是APB2的外设,应该调用APB2的时钟函数
void spi_rcc_init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
}
(2)初始化GPIO
我们的寻址先SS仍然采用软件操控,而时钟线和主机发送、接收数据线则交给硬件控制,与软件模拟SPI不同的是时钟线和主机发送线采用复用推挽模式,其余不变,主机接收线采用上拉输入模式,SS仍采用推挽输出模式
void spi_gpio_init()
{
GPIO_InitTypeDef gpio_init;
gpio_init.GPIO_Pin = GPIO_Pin_4;
gpio_init.GPIO_Mode = GPIO_Mode_Out_PP;
gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init);
gpio_init.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
gpio_init.GPIO_Mode = GPIO_Mode_AF_PP;
gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init);
gpio_init.GPIO_Pin = GPIO_Pin_6;
gpio_init.GPIO_Mode = GPIO_Mode_IPU;
gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init);
GPIO_SetBits(GPIOA, GPIO_Pin_4); //ss->1
}
(3)初始化SPI
先看一下SPI的初始化结构体中的变量
第一个参数是选择数据传输模式,可以是双线双向全双工,也可以是双线单向接收/发送等,这里选择双线双向全双工;
第二个参数是选择stm32作为spi主从模式,这里选择主模式;
第三个参数选择的是数据的大小,有8位和16位,这里选择8位;
第四个参数是选择时钟的默认电平,我们使用的是模式0,默认电平为低电平;
第五个参数选择的是第一个触发沿采样数据还是第二个触发沿采样数据,这里选择第一个触发沿采样,也就是上升沿采样;
第六个参数是选择SS使用硬件控制还是软件控制,这里使用软件控制;
第七个参数是预分频,时钟是72MHz(SPI1),我们要进行分频,最小2分频,最大256分频,分频系数越小,传输速度越快;
第八个参数是选择发送模式,可以选择高位先行或低位先行,我们选择高位先行;
第九个参数用于CRC值的计算,给默认值7;
这样我们就可以初始化SPI了
void spi_spi_init()
{
SPI_InitTypeDef spi_init;
spi_init.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
spi_init.SPI_Mode = SPI_Mode_Master;
spi_init.SPI_DataSize = SPI_DataSize_8b;
spi_init.SPI_CPOL = SPI_CPOL_Low;
spi_init.SPI_CPHA = SPI_CPHA_1Edge;
spi_init.SPI_NSS = SPI_NSS_Soft;
spi_init.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;
spi_init.SPI_FirstBit = SPI_FirstBit_MSB;
spi_init.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &spi_init);
}
(4)初始化封装
我们把所有的初始化函数封装为一个函数供外部调用,最后再使能一下SPI
void spi_init()
{
spi_rcc_init();
spi_gpio_init();
spi_spi_init();
SPI_Cmd(SPI1, ENABLE);
}
(2)开始和结束传输
和之前的相同,开始和结束传输就是把SS置0和置1
void spi_start()
{
GPIO_ResetBits(GPIOA, GPIO_Pin_4); //ss->0
}
void spi_end()
{
GPIO_SetBits(GPIOA, GPIO_Pin_4); //ss->1
}
(3)交换数据
在每次发送数据时,我们要判断发送寄存器是否为空,接着我们发送数据,发送时从机的数据同步发送到接收寄存器,因此我们判断接收寄存器是否为非空,接收数据
unsigned char spi_swap_inf(unsigned char inf)
{
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);
SPI_I2S_SendData(SPI1, inf);
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);
return SPI_I2S_ReceiveData(SPI1);
}
这里发送函数已经自动帮助我们在软件模拟时候执行的操作,且发送寄存器标志位在写入时会自动清零,接收寄存器标志位在读取数据时会自动清零,因此我们只要四句代码即可完成数据交换;
(5)封装与声明
和软件模拟的一样,把初始化、开始和结束、交换数据函数声明为外部可调用,最终.c 和 .h文件如下
#include "stm32f10x.h" // Device header
void spi_rcc_init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
}
void spi_gpio_init()
{
GPIO_InitTypeDef gpio_init;
gpio_init.GPIO_Pin = GPIO_Pin_4;
gpio_init.GPIO_Mode = GPIO_Mode_Out_PP;
gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init);
gpio_init.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
gpio_init.GPIO_Mode = GPIO_Mode_AF_PP;
gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init);
gpio_init.GPIO_Pin = GPIO_Pin_6;
gpio_init.GPIO_Mode = GPIO_Mode_IPU;
gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init);
GPIO_SetBits(GPIOA, GPIO_Pin_4);
}
void spi_spi_init()
{
SPI_InitTypeDef spi_init;
spi_init.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
spi_init.SPI_Mode = SPI_Mode_Master;
spi_init.SPI_DataSize = SPI_DataSize_8b;
spi_init.SPI_CPOL = SPI_CPOL_Low;
spi_init.SPI_CPHA = SPI_CPHA_1Edge;
spi_init.SPI_NSS = SPI_NSS_Soft;
spi_init.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;
spi_init.SPI_FirstBit = SPI_FirstBit_MSB;
spi_init.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &spi_init);
}
void spi_init()
{
spi_rcc_init();
spi_gpio_init();
spi_spi_init();
SPI_Cmd(SPI1, ENABLE);
}
void spi_start()
{
GPIO_ResetBits(GPIOA, GPIO_Pin_4); //ss->0
}
void spi_end()
{
GPIO_SetBits(GPIOA, GPIO_Pin_4); //ss->1
}
unsigned char spi_swap_inf(unsigned char inf)
{
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);
SPI_I2S_SendData(SPI1, inf);
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);
return SPI_I2S_ReceiveData(SPI1);
}
#ifndef __SPI_H__
#define __SPI_H__
void spi_init(void);
void spi_start(void);
void spi_end(void);
unsigned char spi_swap_inf(unsigned char inf);
#endif
(三)SPI操作W25Q64
W25Q64是一个64Mbits的掉电不丢失的非易失存储器,其写入的数据可以实现掉电不丢失,其使用SPI进行通信,支持SPI的模式0和模式3;
和陀螺仪一样,W25Q64也有自己的通信协议,由于其内部存储空间大,所以其地址是24位的,其通信协议包括这些
(1)开始时,其要先发送特定的指令码以明确下面执行的操作,再发送地址码操作指定区域;
(2)写入操作(包括擦除)前必须写使能;
(3)写入数据之前必须擦除,擦除后所有数据位变为1;
(4)写入和擦除数据需要一定时间,芯片进入忙状态,此时不能写入或读出数据;
W25Q64的指令码如下:
第一列是指令内容,第二列是16进制的指令码,后面为发送接收内容,如果后面没有数据则表示可以停止,没有括号表示应该输入数据,右括号表示该接收数据,通过这些指令码,我们就可以编写操作W25Q64的函数了;
(1)读取存储器状态
我们在操作存储器之前都要读取存储器状态,判断其是否在忙,通过指令码可知,我们只要发送读取存储器状态指令0x05,后面即可连续接受存储器状态
void wq_can_use()
{
spi_start();
spi_swap_inf(0x05);
while ((spi_swap_inf(0xFF) & 0x01) != 0); //if busy?
spi_end();
}
(2)写入一个数组
由于W25Q64写入操作可以自动地址增加,我们可以写入一个数组的数据,根据之前所说,执行操作前我们要判断存储器状态,即调用上一个函数,下一个时序发送写使能指令,这样才可以修改存储器,然后我们就可以发送写指令和对应要写的地址,最后可以不断写入数据,这里的写入并没有翻页功能,超过了一页的数据会覆盖页开始的数据(W25Q64的内存结构被细致地划分为块、扇区和页。整个8M字节的存储空间被切割成128个块,每个块的大小为64K字节。每个块进一步被分为16个扇区,每个扇区包含4K字节。每个扇区又被细分为16个页,每页便是256字节)
void wq_write(int address, unsigned char* inf, unsigned char length)
{
unsigned char i;
wq_can_use();
spi_start();
spi_swap_inf(0x06); // write enable
spi_end();
spi_start();
spi_swap_inf(0x02); // write
spi_swap_inf(address>>16);
spi_swap_inf(address>>8);
spi_swap_inf(address);
for (i = 0; i < length; i++)
{
spi_swap_inf(inf[i]);
}
spi_end();
}
这里传递地址需要3此交换数据,我们把一个32位的数据右移16位,低八位为16~25,转化为8位数据会自动舍弃高位,同理右移8位后第八位为8~15,没有移动时为0~7,这里注意传输模式是高位先行,即先传输高位;
(3)读取操作
和写入操作相似,读取数据之前也要判断芯片是否在忙,但是不用发送写使能指令,我们直接发送读指令,即可连续不断往后读取数据,其地址也会自动增加,其会自动翻页
void wq_read(int address, unsigned char* inf, unsigned int length)
{
unsigned int i;
wq_can_use();
spi_start();
spi_swap_inf(0x03); // read
spi_swap_inf(address>>16);
spi_swap_inf(address>>8);
spi_swap_inf(address);
for (i = 0; i < length; i++)
{
inf[i] = spi_swap_inf(0xFF);
}
spi_end();
}
我们把读取的数据存在一个数组中,由于读取会自动翻页,因此读取的数量可能比写入数量大得多,可以用一个int类型的长度来接收长度变量;
(4)擦除数据
擦除数据没有指定地址擦除,其只有按块擦除、按扇区擦除和按页擦除,擦除范围从大到小,这里写的是按页擦除,我们可以给写入数据的地址,写入数据地址所属的整页数据都会被清除
和写入相似,擦除前也要判断芯片是否在忙,然后下一个时序发送写使能,然后发送擦除指令和指定地址即可
void wq_erase_4kb(int address)
{
wq_can_use();
spi_start();
spi_swap_inf(0x06); // write enable
spi_end();
spi_start();
spi_swap_inf(0x20); // erase 4kb
spi_swap_inf(address>>16);
spi_swap_inf(address>>8);
spi_swap_inf(address);
spi_end();
}
(5)封装和声明
最后我们把SPI的初始化也放在这个文件中,这样头文件只要引用一个文件即可,最后的.c 和 .h文件如下
#include "stm32f10x.h" // Device header
#include "spi.h"
void wq_init()
{
spi_init();
}
void wq_can_use()
{
spi_start();
spi_swap_inf(0x05);
while ((spi_swap_inf(0xFF) & 0x01) != 0); //if busy?
spi_end();
}
void wq_write(int address, unsigned char* inf, unsigned char length)
{
unsigned char i;
wq_can_use();
spi_start();
spi_swap_inf(0x06); // write enable
spi_end();
spi_start();
spi_swap_inf(0x02); // write
spi_swap_inf(address>>16);
spi_swap_inf(address>>8);
spi_swap_inf(address);
for (i = 0; i < length; i++)
{
spi_swap_inf(inf[i]);
}
spi_end();
}
void wq_read(int address, unsigned char* inf, unsigned int length)
{
unsigned int i;
wq_can_use();
spi_start();
spi_swap_inf(0x03); // read
spi_swap_inf(address>>16);
spi_swap_inf(address>>8);
spi_swap_inf(address);
for (i = 0; i < length; i++)
{
inf[i] = spi_swap_inf(0xFF);
}
spi_end();
}
void wq_erase_4kb(int address)
{
wq_can_use();
spi_start();
spi_swap_inf(0x06); // write enable
spi_end();
spi_start();
spi_swap_inf(0x20); // erase 4kb
spi_swap_inf(address>>16);
spi_swap_inf(address>>8);
spi_swap_inf(address);
spi_end();
}
#ifndef __W25Q64_H__
#define __W25Q64_H__
void wq_init(void);
void wq_write(int address, unsigned char* inf, unsigned char length);
void wq_read(int address, unsigned char* inf, unsigned char length);
void wq_erase_4kb(int address);
#endif
(四)主函数调用
我们可以先在存储器上写入一些数据再读出来认证我们的函数没有问题,注意写入之前最好先擦除
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "w25q64.h"
int main()
{
unsigned char write_inf[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
unsigned char read_inf[5];
unsigned char i;
OLED_Init();
wq_init();
wq_erase_4kb(0x00000000);
wq_write(0x00000000, write_inf, 10);
wq_read(0x00000000, read_inf, 5);
OLED_ShowString(1, 1, "write:");
for (i = 0;i < 10; i++)
{
OLED_ShowNum(2, i+1, write_inf[i], 1);
}
OLED_ShowString(3, 1, "read:");
for (i = 0; i < 5; i++)
{
OLED_ShowNum(4, i+1, read_inf[i], 1);
}
while(1);
return 0;
}
(五)总结
通过SPI来操作一个存储器,我们认识了SPI通信的基本原理和通信方式,学习了存储器W25Q64的通信协议