参考教程:https://www.bilibili.com/video/BV1SatHeBEVG/?spm_id_from=333.1387.favlist.content.click

一、准备工作

1、软件层的准备(Keil工程)

(1)拷贝一份STM32教程中使用OLED屏进行显示的工程文件夹,并更名为“OTA&BootLoader实验工程”。

(2)打开工程,将模拟I2C、模拟SPI、串口模块、DMA模块、W25Q64模块和FLASH模块的分文件添加到工程中(这几个分文件在STM32教程中已经完成开发),并编译。24C02模块(使用I2C通信的外设EEPROM)只在51单片机教程中开发过,因此需要做代码移植。

2、硬件层的准备(接线)

(1)USB转串口模块:

STM32引脚

模块引脚

PA10

TXD

PA9

RXD

GND

GND

/

VCC与3.3V短接

(2)W25Q64模块:

STM32引脚

模块引脚

PA4

CS

PA5

CLK

PA6

DO

PA7

DI

3.3V

VCC

GND

GND

(3)24C02模块:

STM32引脚

模块引脚

PB11

SDA

PB10

SCL

3.3V

VCC

GND

GND

二、串口&DMA功能开发

1、处理接收数据方案设计

(1)如果使用CPU进行接收数据的处理,对于简单的项目而言问题不大,但对于复杂的项目,CPU往往有“更重要的任务”需要处理,如果接收数据全程由CPU进行算力支持,是非常奢侈的,而DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,能够节省CPU的资源,因此数据传输可采用DMA。

(2)涉及数据传输,还需要判断单次数据传输是否完成,最简单的就是判断传输信号线是否空闲,如果空闲,说明一次数据传输结束,可以根据此逻辑配置中断,用于请求CPU发出数据处理指令。

(3)单片机需定义缓冲区(一般为一维数组)用于存放接收但未处理的数据,如果单片机的数据处理效率高于数据接收速率,那么缓冲区的大小可相应地较小,为方便介绍,以下以一个2048 bytes的缓冲区为例进行介绍。

①当有源源不断的数据到来时,缓冲区数组从0号下标开始存放数据,先使用标号较小的空间,按顺序往缓冲区中填入数据。

②当缓冲区被使用的标号超过2047后,继续增大标号会引发数组越界,因此需要约定好单次传递数据的最大字节数,比如412 bytes,当新的一组数据到来时,如果缓冲区剩余空间小于412 bytes,那么新的数据应该从缓冲区0号下标位置写入,以规避数据越界风险。当然,也可以采用传统的循环队列,不管高地址的剩余空间有多少,都按标号递增的策略写入数据,直到标号到达2047,再转到0标号继续写入数据,这种策略的开发逻辑会变得稍微复杂一点,且意义不大,并没有起到节约空间的作用。

③缓冲区中只是存放了若干组连续的数据,但并没法直接区分不同组数据的头尾,因此在将数据写入缓冲区时,还应该记录下数据组的起始地址Start和结束地址End,为了方便管理,可以将这些信息定义为一个结构体,再创建结构体数组,当结构体数组溢出时,可按照循环队列的思想从首元素重新写入。

④由于缓冲区是个写入与读出同时不断进行的结构,因此需要再设置IN指针和OUT指针指示缓冲区写入和读出的位置,以便DMA进行数据处理。IN指针和OUT指针可直接指向结构体数组,通过结构体成员间接索引缓冲区的数据,IN指针指向下一个可写入数据的位置,OUT指针指向下一个DMA需要读取的数据的位置,整体思想与循环队列是极其相似的。(可再定义END指针指示结构体数组末端,下图未示出)

2、Serial.h文件修改

        在Serial.h文件中添加如下代码,主要是缓冲区相关的定义

#define U0_RX_SIZE 	2048        //接收数据缓冲区大小(单位为字节)
#define U0_RX_MAX  	256         //单次接收最大数据大小(单位为字节)
#define NUM        	10          //数据组指针结构体数组大小

extern uint8_t U0_RxBuff[U0_RX_SIZE];   //接收数据缓冲区

//数据组指针结构体
typedef struct{
	uint8_t *start;      	//单组数据起始地址
	uint8_t *end;       	//单组数据结束地址
}UCB_URxBuffptr;

//数据组指针结构体数组结构体
typedef struct{
	uint16_t URxCounter;             	//接收数据个数
	UCB_URxBuffptr URxDataPtr[NUM];  	//数据组指针结构体数组
	UCB_URxBuffptr *URxDataIN;       	//IN指针
	UCB_URxBuffptr *URxDataOUT;      	//OUT指针
	UCB_URxBuffptr *URxDataEND;      //END指针
}UCB_CB;

extern UCB_CB U0CB;

3、串口初始化函数修改

(1)首先在Serial.c文件中包含Serial.h文件,否则无法使用其中的定义。

(2)核对串口初始化函数Serial_Init,串口波特率需改为921600(数据传输速率要求高),并配置开启串口空闲中断。

void Serial_Init(void){
	//开启GPIO和USART的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	
	//USART1_TX(PA9)配置为复用输出模式,USART1_RX(PA10)配置为输入模式
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;         //复用推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;           //上拉输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	//配置USART1
	USART_InitTypeDef USART_InitStructure;
	USART_InitStructure.USART_BaudRate = 921600;              //波特率
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;  //不使用硬件流控
	USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;  //支持发送和接收功能
	USART_InitStructure.USART_Parity = USART_Parity_No;     //无校验
	USART_InitStructure.USART_StopBits = USART_StopBits_1;  //停止位为1位
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;  //无校验,一共8位数据
	USART_Init(USART1, &USART_InitStructure);
	
	//开启中断用于接收数据,配置NVIC
	USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);			//开启串口空闲的中断
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);           //分组方式2
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;       //USART1到NVIC的通道
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;        //开启中断
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //抢占优先级
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;        //响应优先级
	NVIC_Init(&NVIC_InitStructure);
	
	//开启USART1
	USART_Cmd(USART1, ENABLE);
}

4、DMA初始化函数修改

(1)DMA的数据转运方向配置为串口外设数据寄存器至接收数据缓存区,同时配置使用硬件触发(USART_DR有数据到来后,USART_RX自己向DMA请求数据转运,这在STM32F103C8T6中是通过DMA1通道5进行的,具体可见官方文档)。

(2)单次数据传输是否结束是由串口空闲中断是否触发判断,不依赖DMA的传输计数器,因此将传输计数器设置为大于单次数据传输允许的数据大小最大值。

(3)针对以上两点,修改DMA初始化函数MyDMA_Init及MyDMA.h中的函数声明。

void MyDMA_Init(void)
{
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);	//开启DMA的时钟
	
	/*DMA初始化*/
	DMA_InitTypeDef DMA_InitStructure;					//定义结构体变量
	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;	//外设基地址,设置为USART1的数据寄存器地址
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //每次转运1个字节
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //起点地址不自增
	DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)U0_RxBuff;  //终点的基地址,设置为接收数据缓冲区数组首地址
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;  //每次转运1个字节
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;  //终点地址自增
	DMA_InitStructure.DMA_BufferSize = U0_RX_MAX + 1;    //传输计数器初值,设置为大于单次传输最大值
	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;   //正常模式
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //外设站点(DMA_PeripheralBaseAddr)作为数据源(方向参数)
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;         //非内存到内存
	DMA_InitStructure.DMA_Priority = DMA_Priority_High;  //指定优先级
	DMA_Init(DMA1_Channel5, &DMA_InitStructure);

	USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE); //使能USART1_RX对DMA的请求
    DMA_Cmd(DMA1_Channel5, ENABLE);      //使能DMA1通道5
}

5、接收数据缓冲区初始化函数编写

(1)首先在Serial.c文件中定义接收数据缓冲区以及数据组指针结构体数组结构体,并声明接收数据缓冲区初始化函数。

uint8_t U0_RxBuff[U0_RX_SIZE];       //接收数据缓冲区
UCB_CB U0CB;                     //数据组指针结构体数组结构体

void U0Rx_PtrInit(void);

(2)IN指针和OUT指针初始化为指向结构体数组的首个成员的位置,END指针初始化为指向结构体数组最后一个成员的位置,该初始化函数也定义在Serial.c文件中。

void U0Rx_PtrInit(void)
{
	U0CB.URxDataIN = &U0CB.URxDataPtr[0];  //IN指针指向结构体数组首个成员的位置
	U0CB.URxDataOUT = &U0CB.URxDataPtr[0];//OUT指针指向结构体数组首个成员的位置
	U0CB.URxDataEND = &U0CB.URxDataPtr[NUM-1];  //END指针指向结构体数组最后一个成员的位置
	
	U0CB.URxDataIN->start = U0_RxBuff;
	
	U0CB.URxCounter = 0;       //结构体数组中的“有效成员”数量为0
}

6、串口空闲中断函数编写

(1)当检测到总线空闲时,USART_SR寄存器的IDLE位被硬件置位,由此能够触发串口空闲中断。进入中断函数后,软件需要先读USART_SR寄存器,再读USART_DR寄存器,才可将该位清除(具体参考STM32官方手册)。

(2)数据组指针结构体数组结构体U0CB中维护了缓冲区数组单次使用循环已使用的字节数,该变量的意义是,在下一次IN指针在缓冲区中的索引位置发生回滚前,缓冲区中填入的数据字节数(不管前面的数据有没有被取走)。

(3)完成单次数据接收后,应更新缓冲区相关的信息变量:

①更新IN指针当前指向成员的End指针,也就是该次数据在缓冲区中的结束地址。

②再将IN指针指向下一个成员的位置,根据END指针判断是否越界,越界则需要回滚至第一个成员的位置。

③更新IN指针指向的成员的Start指针,也就是下次接收数据在缓冲区中的起始地址,需要判断缓冲区尾部的空间是否足够,如不足则循环返回缓冲区数组首位,即开启下一次缓冲区数组使用循环,已使用的字节数需要更新为0。

(4)完成单组数据传输,需重新配置DMA的传输计数器和数据终点基地址,为下一次数据传输做准备。

(5)基于以上几点,编写USART1_IRQHandler函数(中断函数,函数名不得随意命名)。

void USART1_IRQHandler(void){
	if (USART_GetITStatus(USART1, USART_IT_IDLE) == SET){  //判断是否是USART1的空闲事件触发的中断
		USART1->SR;						//读取SR寄存器
		USART_ReceiveData(USART1);		//读取DR寄存器
		U0CB.URxCounter += (U0_RX_MAX + 1) - DMA_GetCurrDataCounter(DMA1_Channel5);   //缓冲区数组单次使用循环已使用的字节数更新
		U0CB.URxDataIN->end = &U0_RxBuff[U0CB.URxCounter - 1];  //更新结构体数组IN指针指向成员的end指针,完成一组数据维护
		U0CB.URxDataIN++;               //IN指针指向结构体数组下一个位置
		//防止数组越界,IN指针不能大于等于END指针
		if(U0CB.URxDataIN == U0CB.URxDataEND){
			U0CB.URxDataIN = &U0CB.URxDataPtr[0];  //循环返回结构体数组首位
		}
		/*判断缓冲区尾部剩余空间是否允许存放下一组数据*/
		if(U0_RX_SIZE - U0CB.URxCounter >= U0_RX_MAX){
			U0CB.URxDataIN->start = &U0_RxBuff[U0CB.URxCounter];  //更新结构体数组IN指针指向成员的start指针,指向下一组数据的起始位置
		}
		else{
			U0CB.URxDataIN->start = U0_RxBuff;  //缓冲区尾部剩余空间不足,循环返回缓冲区数组首位
			U0CB.URxCounter = 0;      //开启下一次缓冲区数组使用循环,已使用的字节数更新为0
		}
		/*单组数据传输完成,重新配置DMA*/
		DMA_Cmd(DMA1_Channel5, DISABLE);
		DMA_SetCurrDataCounter(DMA1_Channel5, U0_RX_MAX + 1);  //传输计数器重置
		DMA1_Channel5->CMAR = (uint32_t)U0CB.URxDataIN->start;  //数据终点的基地址重置
		DMA_Cmd(DMA1_Channel5, ENABLE);
	}
}

7、重写Print函数

(1)在STM32教程中已重写过Print函数(将Print函数的打印输出映射到串口发送上),这里需要对其做修改。

(2)在Serial.h文件中添加如下代码,主要是发送数据缓冲区相关的定义。

#define U0_TX_SIZE 2048        //发送数据缓冲区大小(单位为字节)

extern uint8_t U0_TxBuff[U0_TX_SIZE];   //发送数据缓冲区

(3)在Serial.c文件中定义发送数据缓冲区,更改Serial_Printf函数(使用strlen函数需要包含string.h)。

#include "string.h"

uint8_t U0_TxBuff[U0_TX_SIZE];   //发送数据缓冲区

void Serial_Printf(char *format, ...)
{
	uint8_t i;
	va_list arg;					//定义可变参数列表数据类型的变量arg
	va_start(arg, format);			//从format开始,接收参数列表到arg变量
	vsprintf((char *)U0_TxBuff, format, arg);	//使用vsprintf打印格式化字符串和参数列表到字符数组中
	va_end(arg);					//结束变量arg
		
	for (i = 0; i < strlen((const char *)U0_TxBuff); i++)
	{
		Serial_SendByte(U0_TxBuff[i]);	//依次调用Serial_SendByte发送每个字节数据
	}
}

8、功能开发阶段性调试

(1)在main.c文件中添加如下调试代码。

#include "stm32f10x.h"                  // Device header
#include "Serial.h"
#include "MyDMA.h"

int main(void)
{
	/*模块初始化*/
	Serial_Init();
	U0Rx_PtrInit();
	MyDMA_Init();
	
	while (1)
	{
		if(U0CB.URxDataOUT != U0CB.URxDataIN)
		{
			Serial_Printf("本次接收了%d字节数据\r\n",U0CB.URxDataOUT->end - U0CB.URxDataOUT->start + 1);
			for(uint16_t i = 0;i < U0CB.URxDataOUT->end - U0CB.URxDataOUT->start + 1;i++)
				Serial_Printf("%c",U0CB.URxDataOUT->start[i]);
			Serial_Printf("\r\n\r\n");
			
			U0CB.URxDataOUT++;    //读出一组数据,OUT指针右移
			if(U0CB.URxDataOUT == U0CB.URxDataEND)
			{
				U0CB.URxDataOUT = &U0CB.URxDataPtr[0];   //OUT指针越界回滚
			}
		}
	}
}

(2)使用串口助手向单片机发送数据,能看到单片机返回相应的内容。

三、I2C&24C02功能开发

1、AT24C02模块开发

(1)在STM32入门教程中曾编写过软件I2C模块文件(MyI2C.c和MyI2C.h),其中已经将I2C的时序封装,在本工程可以直接使用。

(2)本实验采用型号为AT24C02的EEPROM,由于只在51单片机教程中开发过此模块,因此需要做代码移植,修改部分不匹配的地方。

①在Hardware文件夹中添加AT24C02.h文件和AT24C02.c文件。

②编写AT24C02.h文件:

#ifndef __AT24C02_H
#define __AT24C02_H

void AT24C02_WriteByte(unsigned char WordAddress, uint8_t Data);
void AT24C02_WritePage(uint8_t WordAddress,uint8_t* Data_Array);
unsigned char AT24C02_ReadByte(unsigned char WordAddress);
void AT24C02_ReadData(uint8_t WordAddress, uint8_t *rData, uint16_t datalen);

#endif

③编写AT24C02.c文件:

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "MyI2C.h"

#define AT24C02_ADDRESS    0xA0
 
/**
  * @brief  AT24C02写入一个字节数据
  * @param  WordAddress:要写入数据的地址
  * @param  Data:要写入的字节数据
  * @retval 无
  */
void AT24C02_WriteByte(uint8_t WordAddress,uint8_t Data)
{
	MyI2C_Start();  //开始标志
	MyI2C_SendByte(AT24C02_ADDRESS);
	//需要接收数据的从机的地址(后7位)以及读/写选择位(第0位,主机发送数据则需要置为0)
	MyI2C_ReceiveAck();    //主机接收从机的应答
	MyI2C_SendByte(WordAddress);  //存储器中需要接收数据的地址
	MyI2C_ReceiveAck();    //主机接收从机的应答
	MyI2C_SendByte(Data);  //发送一个字节的数据
	MyI2C_ReceiveAck();    //主机接收从机的应答
	MyI2C_Stop();   //结束标志
}
 
/**
  * @brief  AT24C02写入一页数据
  * @param  WordAddress:要写入数据的起始地址
  * @param  Data_Array:要写入的数据数组
  * @retval 无
  */
void AT24C02_WritePage(uint8_t WordAddress,uint8_t* Data_Array)
{
	MyI2C_Start();  //开始标志
	MyI2C_SendByte(AT24C02_ADDRESS);
	//需要接收数据的从机的地址(后7位)以及读/写选择位(第0位,主机发送数据则需要置为0)
	MyI2C_ReceiveAck();    //主机接收从机的应答
	MyI2C_SendByte(WordAddress);  //存储器中需要接收数据的地址
	MyI2C_ReceiveAck();    //主机接收从机的应答
	for(uint8_t i = 0;i < 8;i++)   //不同型号每页的字节数可能不一样
	{
		MyI2C_SendByte(Data_Array[i]);  //发送一个字节的数据
		MyI2C_ReceiveAck();    //主机接收从机的应答
	}
	MyI2C_Stop();   //结束标志
}

/**
  * @brief  AT24C02读取一个字节数据
  * @param  WordAddress:要读出数据的地址
  * @retval 读出的数据
  */
unsigned char AT24C02_ReadByte(uint8_t WordAddress)
{
	uint8_t Data;
	MyI2C_Start();  //开始标志
	MyI2C_SendByte(AT24C02_ADDRESS);
	MyI2C_ReceiveAck();  //主机接收从机的应答
	MyI2C_SendByte(WordAddress);  //存储器中需要被读出数据的地址
	MyI2C_ReceiveAck();    //主机接收从机的应答
	MyI2C_Start();  //开始标志
	MyI2C_SendByte(AT24C02_ADDRESS | 0x01);
	//需要发送数据的从机的地址(后7位)以及读/写选择位(第0位,主机接收数据则需要置为1)
	MyI2C_ReceiveAck();    //主机接收从机的应答
	Data = MyI2C_ReceiveByte();  //从机发来的数据记录在Data中
	MyI2C_SendAck(1);       //主机发送非应答
	MyI2C_Stop();   //结束标志
	return Data;
}

/**
  * @brief  AT24C02读取若干个字节数据
  * @param  WordAddress:要读出数据的地址
  * @param  rData:读出数据存放的数组的首元素地址
  * @param  datalen:需求读出字节个数
  * @retval 无
  */
void AT24C02_ReadData(uint8_t WordAddress, uint8_t *rData, uint16_t datalen)
{
	MyI2C_Start();  //发送开始标志
	MyI2C_SendByte(AT24C02_ADDRESS);
	MyI2C_ReceiveAck();  //主机接收从机的应答
	MyI2C_SendByte(WordAddress);  //发送存储器中需要被读出数据的地址
	MyI2C_ReceiveAck();  //主机接收从机的应答
	MyI2C_Start();  //发送开始标志
	MyI2C_SendByte(AT24C02_ADDRESS | 0x01);
	//需要发送数据的从机的地址(后7位)以及读/写选择位(第0位,主机接收数据则需要置为1)
	MyI2C_ReceiveAck();    //主机接收从机的应答
	for(uint16_t i = 0;i < datalen;i++)
	{
		rData[i] = MyI2C_ReceiveByte();  //从机发来的数据记录在Data中
		if(i == datalen - 1)
            MyI2C_SendAck(1);            //最后一个字节发NACK
        else
            MyI2C_SendAck(0);            //其它字节发ACK
	}
	MyI2C_Stop();   //发送结束标志
}

2、功能开发阶段性调试

(1)在main.c文件中添加如下调试代码。

#include "stm32f10x.h"                  // Device header
#include "Serial.h"
#include "MyDMA.h"
#include "Delay.h"
#include "MyI2C.h"
#include "AT24C02.h"

uint8_t wbuff[8] ={0,1,2,3,4,5,6,7};
uint8_t rbuff[256];

int main(void)
{
	/*模块初始化*/
	Serial_Init();
	U0Rx_PtrInit();
	MyDMA_Init();
	MyI2C_Init();
	
	for(uint16_t i = 0;i < 32;i++)    //按页将EEPROM写满
	{
		AT24C02_WritePage(i * 8, wbuff);
		Delay_ms(5);
	}
	AT24C02_ReadData(0, rbuff, 256);  //读出EEPROM中的全部数据
	
	for(uint16_t i = 0;i < 256;i++)
	{
		Serial_Printf("地址%d=%x\r\n",i,rbuff[i]);
	}
	
	while (1)
	{
		
	}
}

(2)运行程序,能看到单片机通过串口打印出EEPROM中的数据正是其写入的数据。

四、SPI&W25Q64功能开发

1、W25Q64模块开发

(1)在STM32入门教程中曾编写过软件SPI模块文件(MySPI.c和MySPI.h),其中已经将SPI的时序封装,在本工程可以直接使用。

(2)W25Q64模块在STM32教程中开发过,为了适配本实验的需求,需做如下改动。

①在W25Q64.c文件增加W25Q64块擦除(64KB)函数,本实验预期一个程序的大小不会大于64KB,可以每一块64KB的空间存放一个程序,因此擦除也以64KB为单位。

/**
  * 函    数:W25Q64块擦除(64KB)
  * 参    数:BlockNumber 指定块的标号,范围:0~127
  * 返 回 值:无
  */
void W25Q64_Erase64K(uint8_t BlockNumber)
{
	W25Q64_WriteEnable();						//写使能
	
	MySPI_Start();								//SPI起始
	MySPI_SwapByte(W25Q64_BLOCK_ERASE_64KB);	//交换发送块(64KB)擦除的指令
	MySPI_SwapByte((BlockNumber*64*1024)>> 16);	//交换发送地址23~16位
	MySPI_SwapByte((BlockNumber*64*1024)>> 8);	//交换发送地址15~8位
	MySPI_SwapByte(BlockNumber*64*1024);		//交换发送地址7~0位
	MySPI_Stop();								//SPI终止
	
	W25Q64_WaitBusy();							//等待忙
}

②在W25Q64.h文件中声明W25Q64块擦除(64KB)函数。

void W25Q64_Erase64K(uint8_t BlockNumber);

2、功能开发阶段性调试

(1)在main.c文件中添加如下调试代码。

#include "stm32f10x.h"                  // Device header
#include "Serial.h"
#include "MyDMA.h"
#include "Delay.h"
#include "W25Q64.h"
#include "AT24C02.h"

uint8_t wdata[256];
uint8_t rdata[256];

int main(void)
{
	/*模块初始化*/
	Serial_Init();
	U0Rx_PtrInit();
	MyDMA_Init();
	W25Q64_Init();
	
	W25Q64_Erase64K(0);   //擦除第0块
	
	for(uint16_t i = 0;i < 256;i++)   //初始化写入的数据并按页写入W25Q64
	{
		for(uint16_t j = 0;j < 256;j++)
			wdata[j] = i;
		W25Q64_PageProgram(i*256, wdata, 256);
	}
	
	Delay_ms(50);
	
	for(uint16_t i = 0;i < 256;i++)   //读出W25Q64的所有数据并打印
	{
		W25Q64_ReadData(i*256, rdata, 256);
		for(uint16_t j = 0;j < 256;j++)	
			Serial_Printf("Address%d=%x\r\n",i*256+j,rdata[j]);
	}
	
	while (1)
	{
		
	}
}

(2)运行程序,能看到单片机通过串口打印出外部Flash中的数据正是其写入的数据,改变W25Q64_Erase64K函数的位置即可测试函数是否编写正确。

五、内部Flash功能开发

1、内部Flash模块开发

(1)内部Flash模块在STM32教程中开发过,为了适配本实验的需求,需做如下改动。

①在MyFLASH.c文件增加FLASH多页擦除函数和FLASH编程多字函数。

/**
  * 函    数:FLASH多页擦除
  * 参    数:StartPage 要擦除的第一页页号
  * 参    数:num       要擦除的页数
  * 返 回 值:无
  */
void MyFLASH_EraseFlash(uint16_t StartPage, uint16_t num)
{
	FLASH_Unlock();					//解锁
	for(uint16_t i = 0;i < num;i++)
	{
		FLASH_ErasePage((0x08000000 + StartPage * 1024) + (1024 * i));	//页擦除
	}
	FLASH_Lock();					   //加锁
}

/**
  * 函    数:FLASH编程多字
  * 参    数:StartAddress 要写入数据的起始地址
  * 参    数:wData        要写入的数据数组首元素地址
  * 参    数:wnum         要写入的数据字节数
  * 返 回 值:无
  */
void MyFLASH_WriteFlash(uint32_t StartAddress, uint32_t *wData, uint32_t wnum)
{
	FLASH_Unlock();							//解锁
	while(wnum)
	{
		FLASH_ProgramWord(StartAddress, *wData);		//编程字
		wnum -= 4;           //完成4个字节写入
		StartAddress += 4;   //下一次写入数据的地址后移4个字节
		wData++;             //数据数组指针右移
	}
	FLASH_Lock();							   //加锁
}

②在MyFLASH.h文件中声明以上两个函数。

void MyFLASH_EraseFlash(uint16_t StartPage, uint16_t num);
void MyFLASH_WriteFlash(uint32_t StartAddress, uint32_t *wData, uint32_t wnum);

2、功能开发阶段性调试

(1)在main.c文件中添加如下调试代码。

#include "stm32f10x.h"                  // Device header
#include "Serial.h"
#include "MyDMA.h"
#include "Delay.h"
#include "MyFLASH.h"

uint32_t wbuff[1024];

int main(void)
{
	/*模块初始化*/
	Serial_Init();
	U0Rx_PtrInit();
	MyDMA_Init();
	
	for(uint32_t i = 0;i < 1024;i++)
	{
		wbuff[i] = 0x87654321;
	}
	
	MyFLASH_EraseFlash(60, 4);
	
	MyFLASH_WriteFlash(60 * 1024 + 0x08000000, wbuff, 1024*4);
	
	for(uint32_t i = 0;i < 1024;i++)
	{
		Serial_Printf("%x\r\n",*(uint32_t *)((60*1024+0x08000000)+(i*4)));
	}
	
	while (1)
	{
		
	}
}

(2)运行程序,能看到单片机通过串口打印出Flash中的数据正是写入的数据,轮流注释擦除和写入函数即可测试函数是否编写正确。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐