参考教程https://www.bilibili.com/video/BV1SatHeBEVG/?spm_id_from333.1387.favlist.content.click一、准备工作1、软件层的准备Keil工程1拷贝一份STM32教程中使用OLED屏进行显示的工程文件夹并更名为“OTABootLoader实验工程”。2打开工程将模拟I2C、模拟SPI、串口模块、DMA模块、W25Q64模块和FLASH模块的分文件添加到工程中这几个分文件在STM32教程中已经完成开发并编译。24C02模块使用I2C通信的外设EEPROM只在51单片机教程中开发过因此需要做代码移植。2、硬件层的准备接线1USB转串口模块STM32引脚模块引脚PA10TXDPA9RXDGNDGND/VCC与3.3V短接2W25Q64模块STM32引脚模块引脚PA4CSPA5CLKPA6DOPA7DI3.3VVCCGNDGND324C02模块STM32引脚模块引脚PB11SDAPB10SCL3.3VVCCGNDGND二、串口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_TXPA9配置为复用输出模式USART1_RXPA10配置为输入模式 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初始化函数修改1DMA的数据转运方向配置为串口外设数据寄存器至接收数据缓存区同时配置使用硬件触发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);2IN指针和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使用串口助手向单片机发送数据能看到单片机返回相应的内容。三、I2C24C02功能开发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中的数据正是其写入的数据。四、SPIW25Q64功能开发1、W25Q64模块开发1在STM32入门教程中曾编写过软件SPI模块文件MySPI.c和MySPI.h其中已经将SPI的时序封装在本工程可以直接使用。2W25Q64模块在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*256j,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*10240x08000000)(i*4))); } while (1) { } }2运行程序能看到单片机通过串口打印出Flash中的数据正是写入的数据轮流注释擦除和写入函数即可测试函数是否编写正确。