小马哥四轴代码解读-SPI-flash篇
Posted dengqiangjiayou
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了小马哥四轴代码解读-SPI-flash篇相关的知识,希望对你有一定的参考价值。
小马哥四轴代码解读-SPI-flash篇
SPI通信
SPI是一种高速的、全双工、同步的通信总线,主要应用于EEPROM、FLASH、实时时钟、AD转换器和etc
SPI内部结构图:
SPI接口总共有4条线:
MISO、MOSI、SCL、CS要根据stm32的芯片引脚进行定义,如下图:小马哥实验中用的是SPI2,要用到PB13、PB14、PB15
SPI 主要特点有: 可以同时发出和接收串行数据; 可以当作主机或从机工作; 提供频率可
编程时钟; 发送结束中断标志; 写冲突保护; 总线竞争保护等。
下面来看代码:
/*****************************************************************************
* 函 数:void SPI_GPIO_Init(void)
* 功 能:配置SI24R1的 SCK、MISO、MOSI引脚,以及SPI2初始化
* 参 数:无
* 返回值:无
* 备 注:调试SPI通信时一定要分清主机从机模式
* 主机从机模式的 空闲状态 电平
* 2.4G模块通信时,SPI速率一般不大于10Mbps
*****************************************************************************/
void SPI_GPIO_Init(void)
SPI_InitTypeDef SPI_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2,ENABLE);
//配置SPI的SCK,MISO和MOSI引脚为复用推挽模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13|GPIO_Pin_14|GPIO_Pin_15;//根据原理图来设置引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //
GPIO_Init(GPIOB,&GPIO_InitStructure);
SPI_InitStructure.SPI_Mode=SPI_Mode_Master; //配置为主机模式,当然也可以设置为从机模式
SPI_InitStructure.SPI_NSS=SPI_NSS_Soft; //NSS软件管理
SPI_InitStructure.SPI_CPHA=SPI_CPHA_1Edge; //第一个时钟沿捕获如果 CPHA=0,在串行同步时钟的第一个跳变沿(上升或下降)数据被采样;如果 CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样。
SPI_InitStructure.SPI_CPOL=SPI_CPOL_Low; //空闲状态为低电平,SPI 总线四种工作方式 SPI 模块为了和外设进行数据交换,根据外设工作要求,其输出串行同步时钟极性和相位可以进行配置,时钟极性(CPOL)对传输协议没有重大的影响。如果CPOL=0,串行同步时钟的空闲状态为低电平;如果 CPOL=1,串行同步时钟的空闲状态为高电平
SPI_InitStructure.SPI_DataSize=SPI_DataSize_8b; //8位数据帧
SPI_InitStructure.SPI_BaudRatePrescaler=SPI_BaudRatePrescaler_8; //SPI波特率8分频 36/8=4.5M
SPI_InitStructure.SPI_Direction=SPI_Direction_2Lines_FullDuplex; //全双工模式
SPI_InitStructure.SPI_FirstBit=SPI_FirstBit_MSB; //数据高位先行,大端系统
SPI_InitStructure.SPI_CRCPolynomial=7; //CRC计算多项式
SPI_Init(SPI2,&SPI_InitStructure);
SPI_Cmd(SPI2,ENABLE); //SPI2使能
/*****************************************************************************
* 函 数:uint8_t SPI2_WriteReadByte(uint8_t data)
* 功 能:SPI2读写一个字节
* 参 数:无
* 返回值:无
* 备 注:无
*****************************************************************************/
uint8_t SPI2_WriteReadByte(uint8_t data)
//硬件SPI有一个好处是就是你可以直接读取寄存器里面的值,还可以判断它的状态,也可以产生中断
while(!SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE));
SPI_I2S_SendData(SPI2, data);
while(!SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE));
return SPI_I2S_ReceiveData(SPI2);
FLASH存储
从flash读取数据到PIDflash结构体中
用到的flash是128KB,也就是1024 * 128B,写入的时候最多只能写入这么多字节的数据,而且不能写入浮点型数据,所以调参的浮点型数据要先扩大1000倍再存储。需要读数据和写数据的函数
从指定地址开始读出指定长度的数据:一次读取32位也就是U32类型的,就需要用到循环,首先得知道要读取的数据的地址,同样读取之后需存储在一个指针中u32 *pBuffer,或者数组,但是在函数的参数中,指针参数退化为数组,也就是 *( pBuffer + i ) 等价于 pBuffer[ i ],这里的返回值是void类型,如果返回值是u32 *pBuffer这种类型呢?经笔者测试,函数在执行完之后返回值并不会立即被销毁也就是说这个得到的数据可以赋值给函数之外的指针,比如下面的程序中,第一个是返回一个地址,但是这样程序会崩溃。第二个,指针作为函数的形参,**在调用时一定要划出一块地址来作为函数的参数,这个地址可以使堆(即使用malloc函数),也可以是栈,**一定要指定大小,不能用一个指针来接住这个返回值!!!
#include <stdio.h>
#include <malloc.h>
int* f()
int i=0;
int* nums;
for(i=0;i<5;++i)
*( nums + i ) = i*2;
return nums;
int main()
int* nums = f();
printf("%d , %d , %d , %d , %d\\n",*(nums+0),*(nums+1),*(nums+2),*(nums+3),*(nums+4));
程序编译没有问题但是程序已经崩溃了,最好的办法如下(就和小马哥的程序一样):
#include <stdio.h>
#include <malloc.h>
void f(int* nums)
int i=0;
for(i=0;i<5;++i)
nums[i] = i*2;
int main()
int nums[5];
int i=6;
f(nums);
printf("%d , %d , %d , %d , %d\\n",*(nums+0),*(nums+1),*(nums+2),*(nums+3),*(nums+4) );
下面回到正题
/******************************************************************************
* 函 数:void STMFLASH_Read(u32 ReadAddr,u32 *pBuffer,u32 NumToRead)
* 功 能:从指定地址开始读出指定长度的数据
* 参 数:ReadAddr:起始地址
* pBuffer:数据指针
* NumToRead:字(4位)数
* 返回值:无
* 备 注:无
*******************************************************************************/
void STMFLASH_Read(u32 ReadAddr,u32 *pBuffer,u32 NumToRead)
u32 i;
for(i=0;i<NumToRead;i++)
pBuffer[i]=STMFLASH_ReadWord(ReadAddr);//读取4个字节.
ReadAddr+=4;//偏移4个字节.
//保存的参数数据结构
typedef struct PIDSave
//陀螺仪校准数据
u16 ACC_OFFSET_X;
u16 ACC_OFFSET_Y;
u16 ACC_OFFSET_Z;
u16 GYRO_OFFSET_X;
u16 GYRO_OFFSET_Y;
u16 GYRO_OFFSET_Z;
//角度环
u16 ROL_Angle_P;
u16 ROL_Angle_I;
u16 ROL_Angle_D;
u16 PIT_Angle_P;
u16 PIT_Angle_I;
u16 PIT_Angle_D;
u16 YAW_Angle_P;
u16 YAW_Angle_I;
u16 YAW_Angle_D;
//角速度环
u16 ROL_Rate_P;
u16 ROL_Rate_I;
u16 ROL_Rate_D;
u16 PIT_Rate_P;
u16 PIT_Rate_I;
u16 PIT_Rate_D;
u16 YAW_Rate_P;
u16 YAW_Rate_I;
u16 YAW_Rate_D;
//高度环
u16 ALT_Rate_P;
u16 ALT_Rate_I;
u16 ALT_Rate_D;
u16 ALT_P;
u16 ALT_I;
u16 ALT_D;
u16 SI24R1addr;
PID_SAVE;
可以在函数体外定义一个u32 pBuffer的指针来给保存从flash拿出的地址里面的值,下面注意*(uint32_t*)(&PIDflash)** 和 ** (uint32_t*)(PIDflash) ** 虽然他们的值相同但是他们代表的含义却不同
//下面是在其他文件中使用
len = sizeof(PIDflash);
size = len/(4+(len%4)?1:0); //保存的数据长度
STMFLASH_Write(FLASH_SAVE_ADDR,(uint32_t*)(&PIDflash),size);
//!!!定义了结构体,PID_SAVE PIDflash;下面是结构体的成员,结构体名字
写数据到flash中
首先得知道起始地址和已经初始化好的PIDflash结构体,然后就是要写入的数据的个数
起始地址:
//FLASH起始地址
#define STM32_FLASH_BASE 0x08000000 //STM32 FLASH的起始地址
#define FLASH_SAVE_ADDR 0x0801F800 //设置FLASH 保存地址(必须为偶数,且所在扇区,要大于本代码所占用到的扇区.否则,写操作的时候,可能会导致擦除整个扇区,从而引起部分程序丢失.引起死机.
看代码:
/******************************************************************************
* 函 数:void STMFLASH_Write(u32 WriteAddr,u32 *pBuffer,u32 NumToWrite)
* 功 能:从指定地址开始写入指定长度的数据
* 参 数:WriteAddr:起始地址(此地址必须为4的倍数!!)
* pBuffer:数据指针
* NumToWrite:字(32位)数(就是要写入的32位数据的个数.)
* 返回值:无
* 备 注:STM32F1的Flash未写扇区默认是0xFFF...F
*******************************************************************************/
void STMFLASH_Write(u32 WriteAddr,u32 *pBuffer,u32 NumToWrite)
FLASH_Status status = FLASH_COMPLETE;
u32 addrx=0;
u32 endaddr=0;
if(WriteAddr<STM32_FLASH_BASE||(WriteAddr>=(STM32_FLASH_BASE+1024*128)))return; //非法地址,判断要写入的地址是否合法
FLASH_Unlock(); //解锁
addrx=WriteAddr; //写入的起始地址
endaddr=WriteAddr+NumToWrite*4; //写入的结束地址,写入的u32类型的32/8 = 4字节
if(addrx<0X08020000) //只有主存储区,才需要执行擦除操作!!
while(addrx<endaddr) //扫清一切障碍.(对非FFFFFFFF的地方,先擦除)
if(STMFLASH_ReadWord(addrx)!=0XFFFFFFFF)//有非0XFFFFFFFF的地方,要擦除这个页
status=FLASH_ErasePage(FLASH_SAVE_ADDR);
if(status!=FLASH_COMPLETE)break; //发生错误了
else
addrx+=4;//每写完一个u32类型后都要移动要写入的地址,直到这个地址超出最大地址
//!!这里不要复制,程序中有2个函数
/* @brief删除指定的FLASH页面。
此函数可用于所有STM32F10x设备。
* @param Page_Address:要删除的页面地址。
* @retval FLASH状态:返回值为:FLASH_BUSY, FLASH_ERROR_PG,
* FLASH_ERROR_WRP, FLASH_COMPLETE或FLASH_TIMEOUT。
*/
//FLASH_Status FLASH_ErasePage(uint32_t Page_Address)//注意这个函数时在stm32F1的库函数里面的
if(status==FLASH_COMPLETE) //当flash完成后
while(WriteAddr<endaddr) //写数据
/**
* 在指定的地址上Programs一个字。
此函数可用于所有STM32F10x设备。
* @param Address:要编程的地址。
* @param Data:要编程的数据。
* @retval FLASH Status:返回值为:FLASH_ERROR_PG,
* FLASH_ERROR_WRP, FLASH_COMPLETE或FLASH_TIMEOUT。
*/
//FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data)
if(FLASH_ProgramWord(WriteAddr,*pBuffer)!=FLASH_COMPLETE) //写入数据
break; //写入异常
WriteAddr+=4;
pBuffer++;
FLASH_Lock(); //上锁
整理一下思路:
我们要读写flash,就必须去阅读flash的数据手册,查看其读写相关寄存器
这里讲一个题外话:就是要想程序正常启动,B0/B1引脚一定要接对,不然,在你调试的时候程序进不去main函数一直停留在main函数之外
flash的读取就跟访问指针里面的值一样,
/******************************************************************************
* 函 数:u32 STMFLASH_ReadWord(u32 faddr)
* 功 能:读取指定地址的字(32位数据)
* 参 数:faddr:读地址
* 返回值:对应数据
* 备 注:无
*******************************************************************************/
uint32_t STMFLASH_ReadWord(uint32_t faddr)
return *(vu32*)faddr;
整个过程就是先擦除非0xFFFFFFFF区域然后写入,写完数据就上锁。
void STMFLASH_Write(u32 WriteAddr,u32 *pBuffer,u32 NumToWrite)
其中指针移动:
WriteAddr+=4;
pBuffer++;
至此,我们仍然没有看到flash和SPI总线的关系。
这大概就是使用别人写的库函数的弊端之一吧
有的使用SPI有的使用I2C:https://blog.csdn.net/qq_35120460/article/details/111576846
软件模拟SPI和硬件SPI的区别:IO模拟SPI更容易理解SPI的时序及通信过程
软件SPI: 用IO模拟SPI时序,这个模拟过程全部是CPU在负责执行,为了知稳定得存取数据,你可能会插入软件延时,这个时间在读取数据量不大的情况下并不明显,但是基本上你在读取过程中,其他非中断非异常程序是无法得到执行。
硬件SPI:首先这个数据存道储的过程是不需要CPU参与得,程序中配置好SPI的访问时序,开启中断,CPU就可以在中断函数中搬移数据,省下了软件模拟IO得存取时间。
仔细研究就会发现,CPU在进行SPI中断服务程序还版是需要耽误时间得,这个过程在大数据量传输中还是很耗时,ARM中Cortex-M3内核得处理器在硬件SPI上加入了DMA,这个DMA直接从SPI的数据寄存器,软件配置好DMA之后,基本上整个传输都不要权CPU参与,软件设计得好的话,整个数据传输都不要CPU参与,此时CPU就可以做更多其他有意义的事了。
以上是关于小马哥四轴代码解读-SPI-flash篇的主要内容,如果未能解决你的问题,请参考以下文章