基于STM32实现OTA&BootLoader 第二章——外设功能开发
参考教程: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中的数据正是写入的数据,轮流注释擦除和写入函数即可测试函数是否编写正确。
更多推荐
所有评论(0)