本人是大一的学生,学习了一段时间的stm32,此系列博客为个人的学习笔记,方便个人复习,如有错误或问题,非常非常欢迎大家来大力指正。
HAL库的知识参考keysking的教程,标准库参考江科大的教材。本视频的部分图也来自keysking和江科大视频中的图片,代码也是参考他们的大家如果要学习强烈推荐他们的视频
SPI有4条通讯线。SCK(SCLK,CLK,CK)串行时钟线。MOSI(DO)主出从入,MISO(DI)主入从出。SS(NSS,CS)从机选择。ss有几个从机接几条线.
起始条件:ss从高电平切换到低电平,低电平有效
终止条件:ss从低电平切换到高电平
当ss为高电平(未启动)输出口都为高阻态。
spi的本质就是在一个时序内互相同时交换数据,这次传输的指令下一次就会响应,对方也要用数据进行交换。
TXE是发送缓冲器空闲标志,1为空,0为非空
RXNE是接收缓冲器非空中断标志位,1为非空,0为空
而有两种模式
模式0在ss下拉时,已经将数据移出,数据通过发送指令+读写数据的模式来驱动从机
关于W25Q64的知识:
FLASH中由块组成,块又可以分为很多个扇区,扇区可以分为很多的页(256字节)
软件SPI的实现:
输出引脚为推挽输出(sck,MOSI,ss),输出引脚为推挽输出(sck,MOSI,ss),输入引脚为浮空或者上拉输入(MISO)
void spi_ss(uint8_t spiBit)
{
GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)spiBit);
}
void spi_sck(uint8_t spiBit)
{
GPIO_WriteBit(GPIOA,GPIO_Pin_5,(BitAction)spiBit);
}
void spi_mosi(uint8_t spiBit)
{
GPIO_WriteBit(GPIOA,GPIO_Pin_7,(BitAction)spiBit);
}
uint8_t spi_miso(void)
{
return GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6);
}
void SOFTSPI_init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//输出引脚为推挽输出(sck,MOSI,ss),输入引脚为浮空或者上拉输入(MISO)
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);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//输出引脚为推挽输出(sck,MOSI,ss),输入引脚为浮空或者上拉输入(MISO)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
spi_ss(1);//保持结束状态ss置高位
spi_sck(0);//sck不输出
}
定义开始和结束
void spi_start()
{
spi_ss(0);//ss下拉开始
}
void spi_stop()
{
spi_ss(1);//上拉结束
}
因为spi实质上是数据的交换,所以就定义个交换函数
uint8_t spi_swap(uint8_t ByteSend)
{
uint8_t i,ByteReceive=0x00;
for ( i = 0; i < 8; i++)
{
spi_mosi(ByteSend & (0x80>>i));//传输自己寄存器的各个数值
spi_sck(1);//上升沿读走
if(spi_miso()==1)//如果接收到高电平
{
ByteReceive=ByteReceive|(0x80>>i); //一个一个读取
}
spi_sck(0);
}
return ByteReceive;
}//各个模式改变一下相位和顺序就行
spi就定义好了
接下来写w25q64
读取ID,写使能,busy位判断
void w25q64READID(uint8_t *MID,uint16_t *DID)
{
spi_start();
spi_swap(W25Q64_JEDEC_ID);//首先传指令,告诉从机接下来(下一次)要传给我特定信息
*MID=spi_swap(W25Q64_DUMMY_BYTE);
//这一次从机就发送特定信息,但是主机要拿信息给他换,所以可以给个FF就行
*DID=spi_swap(W25Q64_DUMMY_BYTE);
*DID=*DID<<8;//将did移动到高8位,然后将后面的数据读入
*DID=*DID|spi_swap(W25Q64_DUMMY_BYTE);
spi_stop();
}
void w25q64_writeenable()
{
spi_start();
spi_swap(W25Q64_WRITE_ENABLE);//开启写入使能,写入,删除都要开启一遍
spi_stop();
}
void w25q64_waitBusy()
{
spi_start();
spi_swap(W25Q64_READ_STATUS_REGISTER_1);//读状态寄存器1
while((spi_swap(W25Q64_DUMMY_BYTE) & 0x01)==0x01); //最后一位是busy位,判断是否busy,是就将进程卡住
spi_stop();
}
以下是功能函数
void w25q64_delect(uint32_t Address)//删除页
{
spi_start();
w25q64_writeenable();//涉及写入操作的开头要开写使能
spi_swap(W25Q64_SECTOR_ERASE_4KB);//删除页4kb
spi_swap(Address>>16);//第一次高24位
spi_swap(Address>>8);//第二次高16位
spi_swap(Address);//第三次低8位
spi_stop();
w25q64_waitBusy();
}
void w25q64_write(uint32_t Address,uint8_t *DataArray,uint16_t count)
{
uint16_t i;
//事前等待,不忙的时候执行以后的事,读和写都要放
w25q64_writeenable();
spi_start();
spi_swap(W25Q64_PAGE_PROGRAM);//写页数据
spi_swap(Address>>16);
spi_swap(Address>>8);
spi_swap(Address);
for ( i = 0; i < count; i++)
{
spi_swap(DataArray[i]);
}
spi_stop();
w25q64_waitBusy();//事后等待busy,不忙就退出,写入操作后写入
}
void w25q64_Read(uint32_t Address,uint8_t *DataArray,uint32_t count)//没有页的限制
{
uint32_t i;
spi_start();
spi_swap(W25Q64_READ_DATA);//写页数据
spi_swap(Address>>16);
spi_swap(Address>>8);
spi_swap(Address);
for ( i = 0; i < count; i++)
{
DataArray[i]=spi_swap(W25Q64_DUMMY_BYTE);
}
spi_stop();
}
硬件的实现:
原理图
左上角是TDR和RDR寄存器(数据的传输),左下是分频设置。
右边是标志位寄存器和配置的寄存器,NSS脚是设计多主机的,我们一般一主机就继续用软件输出ss就行。
简化版:
有两种模式(非连续传输模式和连续传输,这里是非连续)
根据时序写交换代码,,根据基本结构配置寄存器和引脚
和软件的区别主要是定义和交换函数:
void HARDSPI_init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//输出引脚为通用推挽输出ss
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA1和PA2引脚初始化为推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//输出引脚为复用推挽输出sck,MOSI
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5|GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//输入引脚为浮空或者上拉输入(MISO)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
SPI_InitTypeDef SPI_InitStructure;
SPI_InitStructure.SPI_Mode=SPI_Mode_Master;//主从选择
SPI_InitStructure.SPI_BaudRatePrescaler=SPI_BaudRatePrescaler_128;//分频系数
SPI_InitStructure.SPI_CPHA=SPI_CPHA_1Edge;//时钟极性,1边是0,2边是1
SPI_InitStructure.SPI_CPOL=SPI_CPOL_Low;//着两个是配置模式的,采样输入边缘,low 0,hight 1
SPI_InitStructure.SPI_DataSize=SPI_DataSize_8b;//数据帧
SPI_InitStructure.SPI_FirstBit=SPI_FirstBit_MSB;//高位or低位先行(高)
SPI_InitStructure.SPI_Direction=SPI_Direction_2Lines_FullDuplex;//双线全双工
SPI_InitStructure.SPI_NSS=SPI_NSS_Soft;//我们使用软件模拟ss所以用软件模式
SPI_InitStructure.SPI_CRCPolynomial=7;
SPI_Init(SPI1,&SPI_InitStructure);
SPI_Cmd(SPI1,ENABLE);
spi_ss(1);
// spi_sck(0);
}
交换函数就只要在读取标志位,然后传输完就接收
uint8_t spi_swap(uint8_t ByteSend)
{
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE)==RESET);//等待TXE标志位置1(空) ,自动清除
SPI_I2S_SendData(SPI1,ByteSend);
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE)==RESET);//等待接收完成,RXNE有数据就是非空,就代表传输结束
return SPI_I2S_ReceiveData(SPI1);
}
一开始,要发送寄存器空闲时才能发送,所以我们要在TXE为0(非空)时等待到TXE=1,然后就发送。
之后是要判断RXNE是否接收到数据,所以我们要在RXNE为0(空)时等待到RXNE=1,然后就读取