WIFI通信物联网 OTA远程升级 BootLoader程序设计(STM32 HAL库)(更新中)

简介:学习BootLoader,教程来自:超子哥

一、OTA和BootLoader概念

1.OTA简介:

OTA(Over-The-Air) 指通过无线通信技术(如4G、WiFi、LoRa等)远程更新设备固件的技术。在物联网场景中,OTA是设备维护和功能升级的核心手段,可避免人工现场操作,显著降低运维成本。

物联网OTA核心流程:

2.BootLoader简介:

二、Flash划分区域 

(1)为什么要在Flash中划分区域?

(2)为什么BootLoader区要在Flash前段,程序代码区在后端?

需要注意的是:

1.OTA_Flag是需要存储在掉电不丢失的区域,如24C02芯片。

2.更新的2.0版本需要分块下载,下载一点存一点,存储在外部Flash芯片W25QX中。

当新版本下载完成后,再将24C02芯片中的标志位OTA_Flag置位。

BootLoader程序目前所需要的功能:

1.当没有OTA实践时,通过函数指针调转到A区运行程序。

2.当有OTA事件时,擦除A区并写入新版本2.0.

三、硬件介绍及所设计到的外设代码 

 我使用的是正点原子的STM32F407 V3开发板 其他开发板也可以

需要用到的芯片有IIC的24C02 SPI的W25QX

(24C02用于存储OTA_Flag W25QX存放更新的程序,就可以通过BootLoader跳转程序)

外设涉及到的知识有:串口 Flash擦除 写入

1.串口代码

串口IAP 我们采用的是 串口+DMA+空闲中断的方式接收数据

具体思路如下:

CUBEMX配置如下:

代码如下:

U1_BootLoader.c:

#include "U1_BootLoader.h"

//缓冲区数组 最大2048
uint8_t U1_RX_Buffer[U1RX_Bufffer_Size];
uint8_t U1_TX_Buffer[U1TX_Bufffer_Size];
//数据控制结构体
UCB_ControlBlock UCB_CB;
//记录单次接收数据的长度
volatile uint8_t Rx_Len = 0;


/**
 * @brief   初始化指针
 * @param   None
 * @retval  None
 */
void UCBRx_PtrInit(void)
{
    UCB_CB.URxDataIn    = &UCB_CB.URxDataBuffer[0];
    UCB_CB.URxDataOut   = &UCB_CB.URxDataBuffer[0];
    UCB_CB.URxDataEnd   = &UCB_CB.URxDataBuffer[URXData_Buffer_Size - 1];
    UCB_CB.URxDataIn->start = U1_RX_Buffer;
    UCB_CB.U1RX_Counter = 0;
}
/**
 * @brief   初始化串口1 开启DMA接收和空闲中断并初始化UCB指针
 * @param   None
 * @retval  None
 */
void U1BootLoader_Init(void)
{
    UCBRx_PtrInit();
    __HAL_UART_ENABLE_IT(&BootLoader_Handle, UART_IT_IDLE);
    HAL_UART_Receive_DMA(&BootLoader_Handle, UCB_CB.URxDataIn->start, U1RX_Buffer_Max);
}

/**
 * @brief   U1串口空闲中断回调函数 
 * @param   None
 * @retval  None
 */
void U1RXBootLoader_IDLE_CallBack(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART1 && __HAL_UART_GET_FLAG(&BootLoader_Handle, UART_IT_IDLE)) {
      __HAL_UART_CLEAR_IDLEFLAG(huart);
    }
    //计算接收数据长度
    __disable_irq();
    Rx_Len = U1RX_Buffer_Max - __HAL_DMA_GET_COUNTER(huart -> hdmarx);
    __enable_irq();
    UCB_CB.U1RX_Counter += Rx_Len;                          //记录缓冲区数组已用长度
    UCB_CB.URxDataIn->end = &U1_RX_Buffer[UCB_CB.U1RX_Counter - 1]; //记录本次接收数据的长度
    UCB_CB.URxDataIn++;                                             //本次In指针记录完成后向后移
    if (UCB_CB.URxDataIn == UCB_CB.URxDataEnd){                     //判断是否SE数组是否满了
        UCB_CB.URxDataIn    = &UCB_CB.URxDataBuffer[0];
    }
    //检查数据缓冲区是否满
    if(U1RX_Bufffer_Size - UCB_CB.U1RX_Counter >= 256){             //判断缓冲区数组是否满了 若没满则更新start位置
        UCB_CB.URxDataIn->start = &U1_RX_Buffer[UCB_CB.U1RX_Counter];
    }else{
        UCB_CB.URxDataIn->start = U1_RX_Buffer;
        UCB_CB.U1RX_Counter = 0;
    }
		
    HAL_UART_AbortReceive(&BootLoader_Handle);               // 终止当前DMA接收
    HAL_UART_Receive_DMA(&BootLoader_Handle, UCB_CB.URxDataIn->start, U1RX_Buffer_Max); // 重启DMA
    Rx_Len = 0;
  }
/**
 * @brief   U1 Printf重定向
 * @param   format 格式字符串
 * @retval  None
 */
void U1_Printf(char *format, ...)
{
    va_list listdata;
    va_start(listdata, format);
    int len = vsnprintf((char*)U1_TX_Buffer, sizeof(U1_TX_Buffer), format, listdata);
    va_end(listdata);

    if (len > 0) {
        // 一次性发送有效数据(非整个数组)
        HAL_UART_Transmit(&BootLoader_Handle, U1_TX_Buffer, len, 100);
    }
    // 可选:等待最后一个字节发送完成(确保硬件完成)
    while (__HAL_UART_GET_FLAG(&BootLoader_Handle, UART_FLAG_TC) == RESET);
}

U1_BootLoader.h:

#ifndef __U1_BOOTLOADER_H
#define __U1_BOOTLOADER_H

#include "main.h"
#include "usart.h"
#include <string.h>
#include <stdarg.h>
#include <stdio.h>


#define BootLoader_Handle   huart1
#define U1TX_Bufffer_Size   2048
#define U1RX_Bufffer_Size   2048
#define U1RX_Buffer_Max     256
#define URXData_Buffer_Size 10
//定义记录接收和结束数据的指针结构体
typedef struct{
    uint8_t *start;
    uint8_t *end;
}UCB_RxBufferPtr;

typedef struct 
{   
    //记录累加值和存放SE指针的数组
    uint16_t U1RX_Counter;
    UCB_RxBufferPtr URxDataBuffer[URXData_Buffer_Size];
    //记录接收数据和处理数据速度
    UCB_RxBufferPtr *URxDataIn;
    UCB_RxBufferPtr *URxDataOut;
    UCB_RxBufferPtr *URxDataEnd;
}UCB_ControlBlock;
//函数声明
void UCBRx_PtrInit(void);
void U1BootLoader_Init(void);
void U1RXBootLoader_IDLE_CallBack(UART_HandleTypeDef *huart);
void U1_Printf(char *format, ...);
//外部声明
extern UCB_ControlBlock UCB_CB;
extern uint8_t U1_RX_Buffer[U1RX_Bufffer_Size];

#endif

注意:BootLoader_IDLE空闲函数要放在串口1的中断里面。 

注意:代码中我使用的是:

HAL_UART_Receive_DMA(&BootLoader_Handle, UCB_CB.URxDataIn->start, U1RX_Buffer_Max); // 重启DMA

而非: 

HAL_UART_Receive_DMA(&huart1, U1_RxBuff, U1_RxMAX);

因为AL说每次调用这个函数都会将数据缓冲区清零,所以我换为了接收缓冲区的起始地址。 

dmain.c代码如下:(测试代码)

	U1_Printf("Test\r\n");
	uint16_t i;
	U1BootLoader_Init();
	U1_Printf("Test1\r\n");
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
		if(UCB_CB.URxDataOut != UCB_CB.URxDataIn){
			U1_Printf("本次接收数据长度为:%d\r\n", UCB_CB.URxDataOut->end - UCB_CB.URxDataOut -> start + 1);
			U1_Printf("字符串为:\r\n");
			for (i = 0; i < UCB_CB.URxDataOut -> end - UCB_CB.URxDataOut ->start +1; i++){
				U1_Printf("%c", UCB_CB.URxDataOut->start[i]);
			}
			UCB_CB.URxDataOut++;
			if (UCB_CB.URxDataOut == UCB_CB.URxDataEnd)
			{
				UCB_CB.URxDataOut = &UCB_CB.URxDataBuffer[0];
			}
		}

测试结果如下:

 2.IIC-24C02 EEPROM代码

我使用的是硬件IIC,未使用软件IIC。

24C02主要存储一些关键信息如OTA_Flag,24C02关键在于掉电不丢失。

CUBEMX设置如下:

自动配置的引脚有误,我们手动根据原理图改为PB8和PB9 。

代码如下.c:

#include "24cxx.h"

//超时时间
#define EP24C_TIMEOUT		200
#define EP24C_MEMADD_SIZE 	I2C_MEMADD_SIZE_8BIT
/**
 * @brief   检查设备是否准备好
 * @param   None
 * @retval  HAL_OK or ERROR
 */
HAL_StatusTypeDef EP24C_IsDeviceReady(void) {
	uint32_t Trials = 10;
	return HAL_I2C_IsDeviceReady(&I2C_HANDLE, DEV_ADDR_24CXX, Trials, EP24C_TIMEOUT);
}
/**
 * @brief   指定地址写入一个字节
 * @param   memAddress指定的地址 byteData一字节数据
 * @retval  HAL_OK or ERROR
 */
HAL_StatusTypeDef EP24C_WriteOneByte(uint16_t memAddress, uint8_t byteData) {
	return HAL_I2C_Mem_Write(&I2C_HANDLE, DEV_ADDR_24CXX, memAddress,
			EP24C_MEMADD_SIZE, &byteData, 1, EP24C_TIMEOUT);
}
/**
 * @brief   指定地址读取一个字节
 * @param   memAddress指定的地址 byteData读取的数据
 * @retval  HAL_OK or ERROR
 */
HAL_StatusTypeDef EP24C_ReadOneByte(uint16_t memAddress, uint8_t *byteData) {
	return HAL_I2C_Mem_Read(&I2C_HANDLE, DEV_ADDR_24CXX, memAddress,
				EP24C_MEMADD_SIZE, byteData, 1, EP24C_TIMEOUT);
}
/**
 * @brief   指定地址读取多个字节
 * @param   memAddress指定的地址  *pBuffer读取数据存储的指针 bufferLen读取的长度
 * @retval  HAL_OK or ERROR
 */
HAL_StatusTypeDef EP24C_ReadBytes(uint16_t memAddress, uint8_t *pBuffer, uint16_t bufferLen) {
	if (bufferLen > MEM_SIZE_24CXX) {
		return HAL_ERROR;
	}
	return HAL_I2C_Mem_Read(&I2C_HANDLE, DEV_ADDR_24CXX, memAddress,
				EP24C_MEMADD_SIZE, pBuffer, bufferLen, EP24C_TIMEOUT);
}
/**
 * @brief   指定地址写入一页 不会超过一页
 * @param   memAddress指定的地址 *pBuffer写入的数据 bufferLen写入的长度
 * @retval  HAL_OK or ERROR
 */
HAL_StatusTypeDef EP24C_WriteInOnePage(uint16_t memAddress, uint8_t *pBuffer, uint16_t bufferLen) {
	if (bufferLen > PAGE_SIZE_24CXX) {
		return HAL_ERROR;
	}
	return HAL_I2C_Mem_Write(&I2C_HANDLE, DEV_ADDR_24CXX, memAddress,
				EP24C_MEMADD_SIZE, pBuffer, bufferLen, EP24C_TIMEOUT);
}
/**
 * @brief   指定地址写入超一页地址
 * @param   memAddress指定的地址 *pBuffer写入的数据 bufferLen写入的长度
 * @retval  HAL_OK or ERROR
 */
HAL_StatusTypeDef EP24C_WriteLongData(uint16_t memAddress, uint8_t *pBuffer, uint16_t bufferLen) {
	if (bufferLen > MEM_SIZE_24CXX) {
		return HAL_ERROR;
	}
	HAL_StatusTypeDef result = HAL_ERROR;
	if (bufferLen <= PAGE_SIZE_24CXX) {
		return HAL_I2C_Mem_Write(&I2C_HANDLE, DEV_ADDR_24CXX, memAddress,
						EP24C_MEMADD_SIZE, pBuffer, bufferLen, EP24C_TIMEOUT);
	}

	uint8_t *pt = pBuffer;
	uint16_t pageCount = bufferLen / PAGE_SIZE_24CXX;
	for (uint16_t i = 0; i < pageCount; ++i) {
		result = HAL_I2C_Mem_Write(&I2C_HANDLE, DEV_ADDR_24CXX, memAddress,
								EP24C_MEMADD_SIZE, pt, PAGE_SIZE_24CXX, EP24C_TIMEOUT);
		pt += PAGE_SIZE_24CXX;
		memAddress += PAGE_SIZE_24CXX;
		HAL_Delay(5);

		if (result != HAL_OK) {
			return result;
		}
	}

	uint8_t leftBytes = bufferLen % PAGE_SIZE_24CXX;
	if (leftBytes > 0) {
		result = HAL_I2C_Mem_Write(&I2C_HANDLE, DEV_ADDR_24CXX, memAddress,
										EP24C_MEMADD_SIZE, pt, leftBytes, EP24C_TIMEOUT);
	}
	return result;
}

24C02.h代码如下:

#ifndef __24CXX_H
#define __24CXX_H

#include "main.h"
#include "i2c.h"

#define I2C_HANDLE		hi2c1
//设备ID地址
#define DEV_ADDR_24CXX	0x00A0

#define PAGE_SIZE_24CXX	0x0008
#define MEM_SIZE_24CXX	(uint16_t)256

HAL_StatusTypeDef EP24C_IsDeviceReady(void);
HAL_StatusTypeDef EP24C_WriteOneByte(uint16_t memAddress, uint8_t byteData);
HAL_StatusTypeDef EP24C_ReadOneByte(uint16_t memAddress, uint8_t *byteData);
HAL_StatusTypeDef EP24C_ReadBytes(uint16_t memAddress, uint8_t *pBuffer, uint16_t bufferLen);
HAL_StatusTypeDef EP24C_WriteInOnePage(uint16_t memAddress, uint8_t *pBuffer, uint16_t bufferLen);
HAL_StatusTypeDef EP24C_WriteLongData(uint16_t memAddress, uint8_t *pBuffer, uint16_t bufferLen);

#endif //__24CXX_H

测试代码和结果如下:

	uint8_t i;
	uint8_t strOut[100];
	if (EP24C_IsDeviceReady() == HAL_OK){
		U1_Printf("Device is ready\r\n");
	}else{
		U1_Printf("Device is error\r\n");
	}
	for (i = 0; i < 100; i++){
		EP24C_WriteOneByte(i, i);
		HAL_Delay(5);
	}
	EP24C_ReadBytes(0, strOut, 256);
	for (i = 0;i < 100; i++){
		U1_Printf("MemAddress%d = %d\r\n",i, strOut[i]);
	}

3.SPI W25Q128代码 

W25Q128主要用于存储代码。

CUBEMX配置如下(主要配置SPI和使能引脚):

因为代码是移植的,忘记配置使能引脚了,又卡了我半天,干。

W25QX驱动代码如下:

W25.C:

/* 文件: w25flash.c
 * 功能描述: Flash 存储器W25Q128的驱动程序
 * 作者:王维波
 * 修改日期:2019-06-05
 */

#include "w25flash.h"

#define MAX_TIMEOUT   200		//SPI轮询操作时的最大等待时间,ms

//SPI接口发送一个字节,byteData是需要发送的数据
HAL_StatusTypeDef SPI_TransmitOneByte(uint8_t byteData) {
	return HAL_SPI_Transmit(&SPI_HANDLE, &byteData, 1, MAX_TIMEOUT);
}

//SPI接口发送多个字节, pBuffer是发送数据缓存区指针,byteCount是发送数据字节数,byteCount最大256
HAL_StatusTypeDef SPI_TransmitBytes(uint8_t *pBuffer, uint16_t byteCount) {
	return HAL_SPI_Transmit(&SPI_HANDLE, pBuffer, byteCount, MAX_TIMEOUT);
}

//SPI接口接收一个字节, 返回接收的一个字节数据
uint8_t SPI_ReceiveOneByte() {
	uint8_t byteData = 0;
	HAL_SPI_Receive(&SPI_HANDLE, &byteData, 1, MAX_TIMEOUT);
	return byteData;
}

//SPI接口接收多个字节, pBuffer是接收数据缓存区指针,byteCount是需要接收数据的字节数
HAL_StatusTypeDef SPI_ReceiveBytes(uint8_t *pBuffer, uint16_t byteCount) {
	return HAL_SPI_Receive(&SPI_HANDLE, pBuffer, byteCount, MAX_TIMEOUT);
}

//Command=0x05:  Read Status Register-1,返回寄存器SR1的值
uint8_t Flash_ReadSR1(void) {
	uint8_t byte = 0;
	__Select_Flash();	//CS=0
	SPI_TransmitOneByte(0x05); //Command=0x05:  Read Status Register-1
	byte = SPI_ReceiveOneByte();
	__Deselect_Flash();	//CS=1
	return byte;
}

//Command=0x35:  Read Status Register-2,返回寄存器SR2的值
uint8_t Flash_ReadSR2(void) {
	uint8_t byte = 0;
	__Select_Flash();	//CS=0
	SPI_TransmitOneByte(0x35); //Command=0x35:  Read Status Register-2
	byte = SPI_ReceiveOneByte();	//读取一个字节
	__Deselect_Flash();	//CS=1
	return byte;
}

//Command=0x01:  Write Status Register,	只写SR1的值
//耗时大约10-15ms
void Flash_WriteSR1(uint8_t SR1) {
	Flash_Write_Enable();       //必须使 WEL=1

	__Select_Flash();	//CS=0
	SPI_TransmitOneByte(0x01);  //Command=0x01:  Write Status Register,	只写SR1的值
	SPI_TransmitOneByte(0x00);    //SR1的值
//	SPI_WriteOneByte(0x00);    //SR2的值, 只发送SR1的值,而不发送SR2的值, QE和CMP将自动被清零
	__Deselect_Flash();	//CS=1

	Flash_Wait_Busy(); 	   //耗时大约10-15ms
}

HAL_StatusTypeDef Flash_WriteVolatile_Enable(void) //Command=0x50: Write Volatile Enable
{
	__Select_Flash();	//CS=0
	HAL_StatusTypeDef result = SPI_TransmitOneByte(0x50);
	__Deselect_Flash();	//CS=1
	return result;
}

//Command=0x06: Write Enable,    使WEL=1
HAL_StatusTypeDef Flash_Write_Enable(void) {
	__Select_Flash();	//CS=0
	HAL_StatusTypeDef result = SPI_TransmitOneByte(0x06); //Command=0x06: Write Enable,    使WEL=1
	__Deselect_Flash();	//CS=1
	Flash_Wait_Busy(); 	//等待操作完成
	return result;
}

//Command=0x04, Write Disable,	  使WEL=0
HAL_StatusTypeDef Flash_Write_Disable(void) {
	__Select_Flash();	//CS=0
	HAL_StatusTypeDef result = SPI_TransmitOneByte(0x04); //Command=0x04, Write Disable,	  使WEL=0
	__Deselect_Flash();	//CS=1
	Flash_Wait_Busy(); 	   //
	return result;
}

//根据Block绝对编号获取地址, 共256个Block, BlockNo 取值范围0-255
//每个块64K字节,16位地址,块内地址范围0x0000-0xFFFF。
uint32_t Flash_Addr_byBlock(uint8_t BlockNo) {
//	uint32_t addr=BlockNo*0x10000;

	uint32_t addr = BlockNo;
	addr = addr << 16; //左移16位,等于乘以0x10000
	return addr;
}

//根据Sector绝对编号获取地址, 共4096个Sector, SectorNo取值范围0-4095
//每个扇区4K字节,12位地址,扇区内地址范围0x000-0xFFF
uint32_t Flash_Addr_bySector(uint16_t SectorNo) {
	if (SectorNo > 4095)	//不能超过4095
		SectorNo = 0;
//	uint32_t addr=SectorNo*0x1000;

	uint32_t addr = SectorNo;
	addr = addr << 12;		//左移12位,等于乘以0x1000
	return addr;
}

//根据Page绝对编号获取地址,共65536个Page,  PageNo取值范围0-65535
//每个页256字节,8位地址,页内地址范围0x00—0xFF
uint32_t Flash_Addr_byPage(uint16_t PageNo) {
//	uint32_t addr=PageNo*0x100;

	uint32_t addr = PageNo;
	addr = addr << 8;		//左移8位,等于乘以0x100
	return addr;
}

//根据Block编号和内部Sector编号计算地址,一个Block有16个Sector
//BlockNo取值范围0-255,  内部SubSectorNo取值范围0-15
uint32_t Flash_Addr_byBlockSector(uint8_t BlockNo, uint8_t SubSectorNo) {
	if (SubSectorNo > 15)	 //不能超过15
		SubSectorNo = 0;

//	uint32_t addr=BlockNo*0x10000;	//先计算Block的起始地址
	uint32_t addr = BlockNo;
	addr = addr << 16;	//先计算Block的起始地址

//	uint32_t offset=SubSectorNo*0x1000;	//计算Sector的偏移地址
	uint32_t offset = SubSectorNo;	//计算Sector的偏移地址
	offset = offset << 12;	//计算Sector的偏移地址

	addr += offset;

	return addr;
}

//根据Block编号,内部Sector编号,内部Page编号获取地址
//BlockNo取值范围0-255
//一个Block有16个Sector, 内部SubSectorNo取值范围0-15
//一个Sector有16个Page , 内部SubPageNo取值范围0-15
uint32_t Flash_Addr_byBlockSectorPage(uint8_t BlockNo, uint8_t SubSectorNo,
		uint8_t SubPageNo) {
	if (SubSectorNo > 15)	//不能超过15
		SubSectorNo = 0;

	if (SubPageNo > 15)	//不能超过15
		SubPageNo = 0;

//	uint32_t addr=BlockNo*0x10000;		//先计算Block的起始地址
	uint32_t addr = BlockNo;
	addr = addr << 16;		//先计算Block的起始地址

//	uint32_t offset=SubSectorNo*0x1000;	//计算Sector的偏移地址
	uint32_t offset = SubSectorNo;	//计算Sector的偏移地址
	offset = offset << 12;	//计算Sector的偏移地址
	addr += offset;

//	offset=SubPageNo*0x100;	//计算Page的偏移地址
	offset = SubPageNo;
	offset = offset << 8;	//计算Page的偏移地址

	addr += offset;		//Page的起始地址
	return addr;
}

//将24位地址分解为3个字节
//globalAddr是全局24位地址, 返回 addrHigh高字节,addrMid中间字节,addrLow低字节
void Flash_SpliteAddr(uint32_t globalAddr, uint8_t *addrHigh, uint8_t *addrMid,
		uint8_t *addrLow) {
	*addrHigh = (globalAddr >> 16);	//addrHigh=高字节

	globalAddr = globalAddr & 0x0000FFFF;	//屏蔽高字节
	*addrMid = (globalAddr >> 8);	//addrMid=中间字节

	*addrLow = globalAddr & 0x000000FF;	//屏蔽中间字节, 只剩低字节,addrLow=低字节
}

//读取芯片ID
//返回值如下:
// 0xEF17,表示芯片型号为W25Q128, Winbond,用过
// 0xC817,表示芯片型号为GD25Q128,ELM,用过
// 0x1C17,表示芯片型号为EN25Q128,台湾EON
// 0xA117,表示芯片型号为FM25Q128,复旦微电子
// 0x2018,表示芯片型号为N25Q128,美光
// 0x2017,表示芯片型号为XM25QH128,武汉新芯,用过

//读取芯片的制造商和器件ID,高字节是Manufacturer ID,低字节是Device ID
uint16_t Flash_ReadID(void) {
	uint16_t Temp = 0;
	__Select_Flash();	//CS=0

	SPI_TransmitOneByte(0x90);		//指令码,0x90=Manufacturer/Device ID
	SPI_TransmitOneByte(0x00);		//dummy
	SPI_TransmitOneByte(0x00);		//dummy
	SPI_TransmitOneByte(0x00);		//0x00
	Temp = SPI_ReceiveOneByte() << 8;	//Manufacturer ID
	Temp |= SPI_ReceiveOneByte();	 	//Device ID, 与具体器件相关

	__Deselect_Flash();	//CS=1
	return Temp;
}

// 参数High32和Low32分别返回64位序列号的高32位和低32位的值
// 函数返回值为64位序列号的值
uint64_t Flash_ReadSerialNum(uint32_t *High32, uint32_t *Low32)	//读取64位序列号,
{
	uint8_t Temp = 0;
	uint64_t SerialNum = 0;
	uint32_t High = 0, Low = 0;

	__Select_Flash();	//CS=0
	SPI_TransmitOneByte(0x4B);		//发送指令码, 4B=read Unique ID
	SPI_TransmitOneByte(0x00);		//发送4个Dummy字节数据
	SPI_TransmitOneByte(0x00);
	SPI_TransmitOneByte(0x00);
	SPI_TransmitOneByte(0x00);

	for (uint8_t i = 0; i < 4; i++)  //高32位
			{
		Temp = SPI_ReceiveOneByte();
		High = (High << 8);
		High = High | Temp;  //按位或
	}

	for (uint8_t i = 0; i < 4; i++)	//低32位
			{
		Temp = SPI_ReceiveOneByte();
		Low = (Low << 8);
		Low = Low | Temp;  //按位或
	}
	__Deselect_Flash();	//CS=1

	*High32 = High;
	*Low32 = Low;

	SerialNum = High;
	SerialNum = SerialNum << 32;  //高32位
	SerialNum = SerialNum | Low;

	return SerialNum;
}

//在任意地址读取一个字节的数据,返回读取的字节数据
// globalAddr是24位全局地址
uint8_t Flash_ReadOneByte(uint32_t globalAddr) {
	uint8_t byte2, byte3, byte4;
	Flash_SpliteAddr(globalAddr, &byte2, &byte3, &byte4);	//24位地址分解为3个字节

	__Select_Flash();	//CS=0
	SPI_TransmitOneByte(0x03);      //Command=0x03, read data
	SPI_TransmitOneByte(byte2);		//发送24位地址
	SPI_TransmitOneByte(byte3);
	SPI_TransmitOneByte(byte4);
	byte2 = SPI_ReceiveOneByte();	//接收1个字节
	__Deselect_Flash();	//CS=1

	return byte2;
}

//从任何地址开始读取指定长度的数据
//globalAddr:开始读取的地址(24bit), pBuffer:数据存储区指针,byteCount:要读取的字节数
void Flash_ReadBytes(uint32_t globalAddr, uint8_t *pBuffer, uint16_t byteCount) {
	uint8_t byte2, byte3, byte4;
	Flash_SpliteAddr(globalAddr, &byte2, &byte3, &byte4);	//24位地址分解为3个字节

	__Select_Flash();	//CS=0
	SPI_TransmitOneByte(0x03);      //Command=0x03, read data
	SPI_TransmitOneByte(byte2);		//发送24位地址
	SPI_TransmitOneByte(byte3);
	SPI_TransmitOneByte(byte4);
	SPI_ReceiveBytes(pBuffer, byteCount);	//接收byteCount个字节数据
	__Deselect_Flash();	//CS=1
}

//Command=0x0B,  高速连续读取flash多个字节,任意全局地址, 速度大约是常规读取的2倍
void Flash_FastReadBytes(uint32_t globalAddr, uint8_t *pBuffer,
		uint16_t byteCount) {
// 	uint16_t i;
	uint8_t byte2, byte3, byte4;
	Flash_SpliteAddr(globalAddr, &byte2, &byte3, &byte4);	//24位地址分解为3个字节

	__Select_Flash();	//CS=0

	SPI_TransmitOneByte(0x0B);      //Command=0x0B, fast read data
	SPI_TransmitOneByte(byte2);		//发送24位地址
	SPI_TransmitOneByte(byte3);
	SPI_TransmitOneByte(byte4);
	SPI_TransmitOneByte(0x00);		//Dummy字节

	SPI_ReceiveBytes(pBuffer, byteCount);	//接收byteCount个字节数据
	__Deselect_Flash();	//CS=1

}

//Command=0xC7: Chip Erase, 擦除整个器件
// 擦除后,所有存储区内容为0xFF,耗时大约25秒
void Flash_EraseChip(void) {
	Flash_Write_Enable();   //使 WEL=1
	Flash_Wait_Busy();   	//等待空闲

	__Select_Flash();		//CS=0
	SPI_TransmitOneByte(0xC7);  // Command=0xC7: Chip Erase, 擦除整个器件
	__Deselect_Flash();		//CS=1

	Flash_Wait_Busy();   //等待芯片擦除结束,大约25秒
}

// Command=0x02: Page program, 对一个页(256字节)编程, 耗时大约3ms,
// globalAddr是写入初始地址,全局地址
// pBuffer是要写入数据缓冲区指针,byteCount是需要写入的数据字节数
// 写入的Page必须是前面已经擦除过的,如果写入地址超出了page的边界,就从Page的开头重新写
void Flash_WriteInPage(uint32_t globalAddr, uint8_t *pBuffer,
		uint16_t byteCount) {
	uint8_t byte2, byte3, byte4;
	Flash_SpliteAddr(globalAddr, &byte2, &byte3, &byte4);	//24位地址分解为3个字节

	Flash_Write_Enable();   //SET WEL
	Flash_Wait_Busy();

	__Select_Flash();	//CS=0
	SPI_TransmitOneByte(0x02);      //Command=0x02: Page program 对一个扇区编程
	SPI_TransmitOneByte(byte2);	//发送24位地址
	SPI_TransmitOneByte(byte3);
	SPI_TransmitOneByte(byte4);
	SPI_TransmitBytes(pBuffer, byteCount); 		//发送byteCount个字节的数据
//	for(uint16_t i=0; i<byteCount; i++)
//	{
//		byte2=pBuffer[i];
//		SPI_WriteOneByte(byte2);	//要写入的数据
//	}
	__Deselect_Flash();	//CS=1

	Flash_Wait_Busy(); 	   //耗时大约3ms
}

//从某个Sector的起始位置开始写数据,数据可能跨越多个Page,甚至跨越Sector,不必提前擦除
// globalAddr是写入初始地址,全局地址,是扇区的起始地址,
// pBuffer是要写入数据缓冲区指针
// byteCount是需要写入的数据字节数,byteCount不能超过64K,也就是一个Block(16个扇区)的大小,但是可以超过一个Sector(4K字节)
// 如果数据超过一个Page,自动分成多个Page,调用EN25Q_WriteInPage分别写入
void Flash_WriteSector(uint32_t globalAddr, const uint8_t *pBuffer,
		uint16_t byteCount) {
//需要先擦除扇区,可能是重复写文件
	uint8_t secCount = (byteCount / FLASH_SECTOR_SIZE);	//数据覆盖的扇区个数
	if ((byteCount % FLASH_SECTOR_SIZE) > 0)
		secCount++;

	uint32_t startAddr = globalAddr;
	for (uint8_t k = 0; k < secCount; k++) {
		Flash_EraseSector(startAddr);	//擦除扇区
		startAddr += FLASH_SECTOR_SIZE;	//移到下一个扇区
	}

//分成Page写入数据,写入数据的最小单位是Page
	uint16_t leftBytes = byteCount % FLASH_PAGE_SIZE; //非整数个Page剩余的字节数,即最后一个Page写入的数据
	uint16_t pgCount = byteCount / FLASH_PAGE_SIZE;  //前面整数个Page
	uint8_t *buff = pBuffer;
	for (uint16_t i = 0; i < pgCount; i++)	//写入前面pgCount个Page的数据,
			{
		Flash_WriteInPage(globalAddr, buff, FLASH_PAGE_SIZE);  //写一整个Page的数据
		globalAddr += FLASH_PAGE_SIZE;	//地址移动一个Page
		buff += FLASH_PAGE_SIZE;		//数据指针移动一个Page大小
	}

	if (leftBytes > 0)
		Flash_WriteInPage(globalAddr, buff, leftBytes);  //最后一个Page,不是一整个Page的数据
}

//Command=0xD8: Block Erase(64KB) 擦除整个Block, globalAddr是全局地址
//清除后存储区内容全部为0xFF,  耗时大概150ms
void Flash_EraseBlock64K(uint32_t globalAddr) {
	Flash_Write_Enable();   //SET WEL
	Flash_Wait_Busy();

	uint8_t byte2, byte3, byte4;
	Flash_SpliteAddr(globalAddr, &byte2, &byte3, &byte4);	//24位地址分解为3个字节

	__Select_Flash();	//CS=0

	SPI_TransmitOneByte(0xD8);      //Command=0xD8, Block Erase(64KB)
	SPI_TransmitOneByte(byte2);	//发送24位地址
	SPI_TransmitOneByte(byte3);
	SPI_TransmitOneByte(byte4);

	__Deselect_Flash();	//CS=1
	Flash_Wait_Busy(); 	   //耗时大概150ms
}

//擦除一个扇区(4KB字节),Command=0x20, Sector Erase(4KB)
//globalAddr: 扇区的绝对地址,24位地址0x00XXXXXX
//擦除后,扇区内全部内容为0xFF, 耗时大约30ms,
void Flash_EraseSector(uint32_t globalAddr) {
	Flash_Write_Enable();   //SET WEL
	Flash_Wait_Busy();
	uint8_t byte2, byte3, byte4;
	Flash_SpliteAddr(globalAddr, &byte2, &byte3, &byte4);	//24位地址分解为3个字节

	__Select_Flash();	//CS=0

	SPI_TransmitOneByte(0x20);      //Command=0x20, Sector Erase(4KB)
	SPI_TransmitOneByte(byte2);		//发送24位地址
	SPI_TransmitOneByte(byte3);
	SPI_TransmitOneByte(byte4);

	__Deselect_Flash();		//CS=1
	Flash_Wait_Busy(); 	   //大约30ms
}

//检查寄存器SR1的BUSY位,直到BUSY位为0
uint32_t Flash_Wait_Busy(void) {
	uint8_t SR1 = 0;
	uint32_t delay = 0;
	SR1 = Flash_ReadSR1();	//读取状态寄存器SR1
	while ((SR1 & 0x01) == 0x01) {
		HAL_Delay(1);	//延时1ms
		delay++;
		SR1 = Flash_ReadSR1();	//读取状态寄存器SR1
	}
	return delay;
}

//进入掉电模式
//Command=0xB9: Power Down
void Flash_PowerDown(void) {
	__Select_Flash();	//CS=0
	SPI_TransmitOneByte(0xB9);  //Command=0xB9: Power Down
	__Deselect_Flash();	//CS=1
	HAL_Delay(1); //等待TPD
}

//唤醒
//Command=0xAB: Release Power Down
void Flash_WakeUp(void) {
	__Select_Flash();	//CS=0
	SPI_TransmitOneByte(0xAB);		//Command=0xAB: Release Power Down
	__Deselect_Flash();	//CS=1
	HAL_Delay(1);     //等待TRES1
}

w25.h:

/* 文件: w25flash.h
 * 功能描述: Flash 存储器W25Q128的驱动程序
 * 作者:王维波
 * 修改日期:2019-06-05
 * W25Q128 芯片参数: 16M字节,24位地址线
 * 分为256个Block,每个Block 64K字节
 * 一个Block又分为16个Sector,共4096个Sector,每个Sector 4K字节
 * 一个Sector又分为16个Page,共65536个Page,每个Page 256字节
 * 写数据操作的基本单元是Page,一次连续写入操作不能超过一个Page的范围。写的Page必须是擦除过的。
 */

#ifndef _W25FLASH_H
#define _W25FLASH_H

#include 	"stm32f4xx_hal.h"
#include	"spi.h"		//使用其中的变量 hspi1,表示SPI1接口
#include  "main.h"
/*  W25Q128硬件接口相关的部分:CS引脚和SPI接口 ,若电路不同,更改这部分配置即可   */
// Flash_CS -->PB14, 片选信号CS操作的宏定义函数
#define CS_PORT		GPIOB
#define CS_PIN		GPIO_PIN_14
#define SPI_HANDLE	hspi1		//SPI接口对象,使用spi.h中的变量 hspi1

#define	__Select_Flash()		HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET)	//CS=0
#define	__Deselect_Flash()		HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET)	//CS=1

//===========Flash存储芯片W25Q128的存储容量参数================
#define		FLASH_PAGE_SIZE			256		//一个Page是256字节

#define		FLASH_SECTOR_SIZE		4096	//一个Sector是4096字节

#define		FLASH_SECTOR_COUNT		4096	//总共4096个 Sector

//=======1. SPI 基本发送和接收函数,阻塞式传输============
HAL_StatusTypeDef SPI_TransmitOneByte(uint8_t byteData);	//SPI接口发送一个字节
HAL_StatusTypeDef SPI_TransmitBytes(uint8_t *pBuffer, uint16_t byteCount);//SPI接口发送多个字节

uint8_t SPI_ReceiveOneByte();	//SPI接口接收一个字节
HAL_StatusTypeDef SPI_ReceiveBytes(uint8_t *pBuffer, uint16_t byteCount);//SPI接口接收多个字节

//=========2. W25Qxx 基本控制指令==========
// 0xEF17,表示芯片型号为W25Q128, Winbond,用过
// 0xC817,表示芯片型号为GD25Q128,ELM,用过
// 0x1C17,表示芯片型号为EN25Q128,台湾EON
// 0xA117,表示芯片型号为FM25Q128,复旦微电子
// 0x2018,表示芯片型号为N25Q128,美光
// 0x2017,表示芯片型号为XM25QH128,武汉新芯,用过

uint16_t Flash_ReadID(void); 	// Command=0x90, Manufacturer/Device ID

uint64_t Flash_ReadSerialNum(uint32_t *High32, uint32_t *Low32); //Command=0x4B, Read Unique ID, 64-bit

HAL_StatusTypeDef Flash_WriteVolatile_Enable(void); //Command=0x50: Write Volatile Enable

HAL_StatusTypeDef Flash_Write_Enable(void); //Command=0x06: Write Enable,    使WEL=1
HAL_StatusTypeDef Flash_Write_Disable(void);//Command=0x04, Write Disable,	  使WEL=0

uint8_t Flash_ReadSR1(void); //Command=0x05:  Read Status Register-1,	返回寄存器SR1的值
uint8_t Flash_ReadSR2(void); //Command=0x35:  Read Status Register-2,	返回寄存器SR2的值

void Flash_WriteSR1(uint8_t SR1); //Command=0x01:  Write Status Register,	只写SR1的值,禁止写状态寄存器

uint32_t Flash_Wait_Busy(void);  	//读状态寄存器SR1,等待BUSY变为0,返回值是等待时间
void Flash_PowerDown(void);   		//Command=0xB9: Power Down
void Flash_WakeUp(void);  			//Command=0xAB: Release Power Down

//========3. 计算地址的辅助功能函数========
//根据Block  绝对编号获取地址,共256个Block
uint32_t Flash_Addr_byBlock(uint8_t BlockNo);
//根据Sector 绝对编号获取地址,共4096个Sector
uint32_t Flash_Addr_bySector(uint16_t SectorNo);
//根据Page  绝对编号获取地址,共65536个Page
uint32_t Flash_Addr_byPage(uint16_t PageNo);

//根据Block编号,和内部Sector编号计算地址,一个Block有16个Sector,
uint32_t Flash_Addr_byBlockSector(uint8_t BlockNo, uint8_t SubSectorNo);
//根据Block编号,内部Sector编号,内部Page编号计算地址
uint32_t Flash_Addr_byBlockSectorPage(uint8_t BlockNo, uint8_t SubSectorNo,
		uint8_t SubPageNo);
//将24位地址分解为3个字节
void Flash_SpliteAddr(uint32_t globalAddr, uint8_t *addrHigh, uint8_t *addrMid,
		uint8_t *addrLow);

//=======4. chip、Block,Sector擦除函数============
//Command=0xC7: Chip Erase, 擦除整个器件,大约25秒
void Flash_EraseChip(void);

//Command=0xD8: Block Erase(64KB) 擦除整个Block, globalAddr是全局地址,耗时大约150ms
void Flash_EraseBlock64K(uint32_t globalAddr);

//Command=0x20: Sector Erase(4KB) 扇区擦除, globalAddr是扇区的全局地址,耗时大约30ms
void Flash_EraseSector(uint32_t globalAddr);

//=========5. 数据读写函数=============
//Command=0x03,  读取一个字节,任意全局地址
uint8_t Flash_ReadOneByte(uint32_t globalAddr);

//Command=0x03,  连续读取多个字节,任意全局地址
void Flash_ReadBytes(uint32_t globalAddr, uint8_t *pBuffer, uint16_t byteCount);

//Command=0x0B,  高速连续读取多个字节,任意全局地址, 速度大约是常规读取的2倍
void Flash_FastReadBytes(uint32_t globalAddr, uint8_t *pBuffer,
		uint16_t byteCount);

//Command=0x02: Page program 对一个Page写入数据(最多256字节), globalAddr是初始位置的全局地址,耗时大约3ms
void Flash_WriteInPage(uint32_t globalAddr, uint8_t *pBuffer,
		uint16_t byteCount);

//从某个Sector的起始地址开始写数据,数据可能跨越多个Page,甚至跨越Sector,总字节数byteCount不能超过64K,也就是一个Block的大小
void Flash_WriteSector(uint32_t globalAddr, const uint8_t *pBuffer,
		uint16_t byteCount);

#endif

测试代码和结果如下:

	U1_Printf("Test\r\n");
	uint16_t devID = Flash_ReadID();
	U1_Printf("Device ID = 0x%04X\r\n", devID);
  Flash_EraseBlock64K(0);
	U1_Printf("Erase OK\r\n");
	
	uint8_t BlockNo = 0;
	uint8_t SubSectorNo = 0;
	uint8_t SubPageNo = 0;
	uint32_t memAddress = 0;
	memAddress = Flash_Addr_byBlockSectorPage(BlockNo, SubSectorNo, SubPageNo);
	uint8_t bufStr1[30] = "1,2,3";
	Flash_WriteInPage(memAddress, bufStr1, strlen(bufStr1) + 1);
	U1_Printf("Write In Page 0 = %s\r\n", bufStr1);
	uint8_t bufStr[50];
	Flash_ReadBytes(memAddress, bufStr, 50);
	U1_Printf("Read In Page 0 = %s\r\n", bufStr);

4.内部Flash擦除和数据写入 

从参考手册中我们可以看到F407一共有12个扇区,写入之前必须擦除;

代码有部分使用的江科大的代码:

MyFlash.c如下:

#include "My_Flash.h"


/**
  * 函    数:FLASH读取一个32位的字
  * 参    数:Address 要读取数据的字地址
  * 返 回 值:指定地址下的数据
  */
uint32_t MyFLASH_ReadWord(uint32_t Address)
{
	return *((__IO uint32_t *)(Address));	//使用指针访问指定地址下的数据并返回
}

/**
  * 函    数:FLASH读取一个16位的半字
  * 参    数:Address 要读取数据的半字地址
  * 返 回 值:指定地址下的数据
  */
uint16_t MyFLASH_ReadHalfWord(uint32_t Address)
{
	return *((__IO uint16_t *)(Address));	//使用指针访问指定地址下的数据并返回
}

/**
  * 函    数:FLASH读取一个8位的字节
  * 参    数:Address 要读取数据的字节地址
  * 返 回 值:指定地址下的数据
  */
uint8_t MyFLASH_ReadByte(uint32_t Address)
{
	return *((__IO uint8_t *)(Address));	//使用指针访问指定地址下的数据并返回
}
/**
  * @brief  擦除指定 Flash 扇区
  * @param  start_sector   起始扇区号(STM32F407: 0~11)
  * @param  num_sectors    擦除扇区数量(最多 12-start_sector)
  * @retval HAL_StatusTypeDef 操作状态(HAL_OK 表示成功)
  */
HAL_StatusTypeDef MyFlash_EraseSectors(uint8_t start_sector, uint8_t num_sectors)
{
    HAL_StatusTypeDef status;
    FLASH_EraseInitTypeDef erase_init;
    uint32_t sector_error = 0;

    // 1. 参数合法性校验
    if (start_sector > 11 || num_sectors == 0 || (start_sector + num_sectors) > 12) {
        return HAL_ERROR; // 参数非法
    }

    // 2. 解锁 Flash 操作权限
    status = HAL_FLASH_Unlock();
    if (status != HAL_OK) {
        return status;
    }

    // 3. 配置擦除参数
    erase_init.TypeErase    = FLASH_TYPEERASE_SECTORS;  // 按扇区擦除
    erase_init.Banks        = FLASH_BANK_1;            // F407 单 Bank
    erase_init.Sector       = start_sector;            // 起始扇区
    erase_init.NbSectors    = num_sectors;             // 擦除扇区数量
    erase_init.VoltageRange = FLASH_VOLTAGE_RANGE_3;   // 电压范围 2.7V~3.6V

    // 4. 执行擦除
    status = HAL_FLASHEx_Erase(&erase_init, &sector_error);
    if (status != HAL_OK) {
        // 可在此记录出错扇区(sector_error)
        // U1_Printf("擦除失败,错误扇区:%lu\r\n", sector_error);
    }

    // 5. 重新锁定 Flash
    HAL_FLASH_Lock();

    return status;
}
/**
  * @brief  Flash 字(32位)写入
  * @param  address  目标地址(需4字节对齐,如0x0800xxxx)
  * @param  data     待写入的32位数据
  * @retval HAL_StatusTypeDef 操作状态
  */
HAL_StatusTypeDef MyFlash_WriteWord(uint32_t address, uint32_t data)
{
    HAL_StatusTypeDef status;

    // 检查地址是否4字节对齐
    if (address % 4 != 0) {
        return HAL_ERROR;
    }

    // 解锁Flash
    status = HAL_FLASH_Unlock();
    if (status != HAL_OK) {
        return status;
    }

    __disable_irq(); // 禁用中断

    // 执行字编程
    status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, address, data);

    __enable_irq();  // 启用中断
    HAL_FLASH_Lock(); // 重新锁定

    return status;
}
/**
  * @brief  Flash 半字(16位)写入
  * @param  address  目标地址(需2字节对齐,如0x0800xxxx)
  * @param  data     待写入的16位数据
  * @retval HAL_StatusTypeDef 操作状态
  */
HAL_StatusTypeDef MyFlash_WriteHalfWord(uint32_t address, uint16_t data)
{
    uint32_t target_word_addr = address & ~0x03; // 对齐到4字节边界
    uint32_t existing_data = *(__IO uint32_t*)target_word_addr;
    uint32_t new_data;

    // 检查地址是否2字节对齐
    if (address % 2 != 0) {
        return HAL_ERROR;
    }

    // 合并数据:替换目标半字,保留其他部分
    if ((address % 4) < 2) {
        new_data = (existing_data & 0xFFFF0000) | data; // 低16位
    } else {
        new_data = (existing_data & 0x0000FFFF) | (data << 16); // 高16位
    }

    // 调用字写入函数
    return MyFlash_WriteWord(target_word_addr, new_data);
}
/**
  * @brief  Flash 字节(8位)写入
  * @param  address  目标地址(任意地址,如0x0800xxxx)
  * @param  data     待写入的8位数据
  * @retval HAL_StatusTypeDef 操作状态
  */
HAL_StatusTypeDef MyFlash_WriteByte(uint32_t address, uint8_t data)
{
    uint32_t target_word_addr = address & ~0x03; // 对齐到4字节边界
    uint32_t existing_data = *(__IO uint32_t*)target_word_addr;
    uint32_t shift = (address % 4) * 8; // 计算字节偏移
    uint32_t mask = 0xFF << shift;
    uint32_t new_data;

    // 合并数据:替换目标字节,保留其他部分
    new_data = (existing_data & ~mask) | (data << shift);

    // 调用字写入函数
    return MyFlash_WriteWord(target_word_addr, new_data);
}

MyFlash.h如下:

#ifndef __MY_FLASH_H
#define __MY_FLASH_H

#include "main.h"
#include "stm32f4xx_hal.h"



//函数声明
uint32_t MyFLASH_ReadWord(uint32_t Address);
uint16_t MyFLASH_ReadHalfWord(uint32_t Address);
uint8_t MyFLASH_ReadByte(uint32_t Address);
HAL_StatusTypeDef MyFlash_EraseSectors(uint8_t start_sector, uint8_t num_sectors);
HAL_StatusTypeDef MyFlash_WriteWord(uint32_t address, uint32_t data);
HAL_StatusTypeDef MyFlash_WriteHalfWord(uint32_t address, uint16_t data);
HAL_StatusTypeDef MyFlash_WriteByte(uint32_t address, uint8_t data);
#endif

测试代码和结果如下,我擦除的是扇区3,写入的是20000806;

	U1_Printf("Test\r\n");
	uint32_t Num = MyFLASH_ReadWord(0x08004000);
  U1_Printf("Num = %" PRIu32 "\r\n", Num);  // 十进制输出
  U1_Printf("Num (HEX) = 0x%08" PRIX32 "\r\n", Num); // 十六进制,固定8位宽度补零
	if (MyFlash_EraseSectors(2,1) == HAL_OK) U1_Printf("Errase Sectors3 OK");
	MyFlash_WriteWord(0x08008000, 0x20000806);

我们可以看到串口助手上显示了擦除成功,并且Utility上显示了写入的数据; 

 四、划分BootLoader区和程序代码区

1.划分B A区

这部分代码我写在创建的主任务代码中的(简单测试)

TASK_Main.h:

#ifndef __TASK_MAIN_H
#define __TASK_MAIN_H
//头文件包含
#include "main.h"
#include "stm32f4xx_hal.h"
#include "U1_BootLoader.h"
#include "My_Flash.h"
#include "24cxx.h"
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
//Flash私有定义
//F407FLASH一共12个扇区 前四个扇区为16K 第五个为64K 后面七个为128K
#define F407_FLASH_STARTADDR				0x08000000
#define F407_FLASH_16SIZE						16  * 1024
#define F407_FLASH_64SIZE						64  * 1024
#define F407_FLASH_128SIZE					128 * 1024
#define FLASH_SECTOR_NUM						12
#define FLASH_BOOTLOADER_NUM				4
#define FLASH_ACode_NUM							FLASH_SECTOR_NUM - FLASH_BOOTLOADER_NUM
#define FLASH_ACode_STARTNUM				FLASH_BOOTLOADER_NUM											//B区扇区为0 1 2 3则B区起始就为4
#define FLASH_ACode_STARTADDR				F407_FLASH_STARTADDR + F407_FLASH_16SIZE + (FLASH_ACode_STARTNUM - 1) * F407_FLASH_128SIZE
#define OTA_SET_FLAG								0xAABB1122

typedef struct{
	uint32_t OTA_FLAG;
}OTA_INFOCB;
//函数声明
void Task_Main(void);
//外部声明
#define OTA_INFOCB_SIZE							sizeof(OTA_INFOCB)

#endif

Task_Main.c:

#include "Task_Main.h"

//初始化OTA结构体
OTA_INFOCB OTA_INFO;
/**
  * 函    数:读取24C02地址0中的OTA_FLAG信息
  * 参    数:None
  * 返 回 值:None
  */
void EP24C_ReadOTAInFo(void)
{
	memset(&OTA_INFO, 0, OTA_INFOCB_SIZE);
	EP24C_ReadBytes(0, (uint8_t *)&OTA_INFO, OTA_INFOCB_SIZE);
}
/**
  * 函    数:BootLoader功能选择函数
  * 参    数:None
  * 返 回 值:None
  * 注    意: 通过判断OTA_FLAG选择更新程序还是跳转程序
  */
void BootLoader_Brance(void)
{
	if(OTA_INFO.OTA_FLAG == OTA_SET_FLAG){
		U1_Printf("OTA SET\r\n");
	}else{
		U1_Printf("OTA NOT SET,TO Sector A");
	}
}
/**
  * 函    数:函数初始化,所有初始化代码都放在这
  * 参    数:None
  * 返 回 值:None
  */
void Task_Main_Init(void)
{
	
}
/**
  * 函    数:主函数
  * 参    数:None
  * 返 回 值:None
  */
void Task_Main(void)
{
	// 在代码中添加临时写入操作(测试后删除)
  uint32_t flag = OTA_SET_FLAG;
	EP24C_WriteInOnePage(0, (uint8_t*)&flag, sizeof(flag));
  HAL_Delay(10); // 等待写入完成
	
	EP24C_ReadOTAInFo();
	BootLoader_Brance();
}

通过直接置标志位OTA_FLAG,我们就会再串口上输出OTA_SET,表示将更新代码。

2.设置栈SP 和 跳转PC指针 

 

3.实现无OTA事件跳转 

Task_Main.c代码:

#include "Task_Main.h"

//初始化OTA结构体
OTA_INFOCB OTA_INFO;
//初始化函数指针结构体
load_a LOAD_A;
/**
  * 函    数:读取24C02地址0中的OTA_FLAG信息
  * 参    数:None
  * 返 回 值:None
  */
void EP24C_ReadOTAInFo(void)
{
	memset(&OTA_INFO, 0, OTA_INFOCB_SIZE);
	EP24C_ReadBytes(0, (uint8_t *)&OTA_INFO, OTA_INFOCB_SIZE);
}
/**
  * 函    数:BootLoader功能选择函数
  * 参    数:None
  * 返 回 值:None
  * 注    意: 通过判断OTA_FLAG选择更新程序还是跳转程序
  */
void BootLoader_Brance(void)
{
	if(OTA_INFO.OTA_FLAG == OTA_SET_FLAG){
		U1_Printf("OTA SET\r\n");
		Load_A(FLASH_ACode_STARTADDR);
	}else{
		U1_Printf("OTA NOT SET,TO Sector A");
	}
}
/**
  * 函    数:SP指针赋值函数
  * 参    数:Addr地址
  * 返 回 值:None
  */
__asm void MSR_SP(uint32_t addr)
{
	MSR MSP, r0
	BX r14
}
/**
  * 函    数:无OTA跳转函数
  * 参    数:Addr地址
  * 返 回 值:None
  */
void Load_A(uint32_t addr)
{
	if (*(uint32_t *)addr >= 0x20000000 && *(uint32_t *)addr <= 0x2002FFFF){
		MSR_SP(*(uint32_t *)addr);
		LOAD_A = (load_a)*(uint32_t *)(addr + 4);
		BootLoader_Clear();
		LOAD_A();
	}
}
void BootLoader_Clear(void)
{
	HAL_UART_DeInit(&huart1);
	HAL_I2C_DeInit(&hi2c1);
}
/**
  * 函    数:函数初始化,所有初始化代码都放在这
  * 参    数:None
  * 返 回 值:None
  */
void Task_Main_Init(void)
{
	
}
/**
  * 函    数:主函数
  * 参    数:None
  * 返 回 值:None
  */
void Task_Main(void)
{
	// 在代码中添加临时写入操作(测试后删除)
  uint32_t flag = OTA_SET_FLAG;
	EP24C_WriteInOnePage(0, (uint8_t*)&flag, sizeof(flag));
  HAL_Delay(10); // 等待写入完成
	
	EP24C_ReadOTAInFo();
	BootLoader_Brance();
}

主要新增了这三个函数 

然后,我们需要将A区的程序烧录进去,A区的起始地址为0x0801 0000

我们需要新建一个BOOTLOARDER_A的测试文件,修改程序起始地址和向量表。

修改起始地址:

修改向量表:

HAL库需要先解除94行的注释,然后修改飘逸:

测试程序的代码就为:

/**
  * 函    数:主函数
  * 参    数:None
  * 返 回 值:None
  */
void Task_Main(void)
{
	U1_Printf("A Sector Test\r\n");
}

然后我们烧写测试代码程序, 测试代码程序从A区 0x0801 0000开始

B区为跳转代码从0x0800 0000

测试结果如下:

表示无OTA时间发生,跳转到了A区。

4.实现OTA事件跳转 

代码部分,主要在OTA结构中增加了一个Firelen,长度为11,Firelen数组用于存储 OTA 更新过程中所需的 长度、校验和、地址或其他元数据,具体用途需结合系统设计文档或实际应用场景确定。;

并增加了OTA更新结构体,定义了一个更新缓冲区,长度为1024,表示每次从W25Q128拿256*4的字节数用于更新到A区,还定义了一个记录用于更新指定程序的变量,用于实现当外部FLASH存储有多个程序 时,需要那个程序就更新到A区 ;

还增加了一个Write_OTAINFO函数,用于写OTA_INFO结构体;

整体代码和测试结果如下:

#include "Task_Main.h"

//初始化OTA结构体
OTA_INFOCB OTA_INFO;
//初始化函数指针结构体
load_a LOAD_A;
//初始化更新结构体
UpDataA_CB UpDataA;
/**
  * 函    数:读取24C02地址0中的OTA_FLAG信息
  * 参    数:None
  * 返 回 值:None
  */
void EP24C_ReadOTAInFo(void)
{
	memset(&OTA_INFO, 0, OTA_INFOCB_SIZE);
	EP24C_ReadBytes(0, (uint8_t *)&OTA_INFO, OTA_INFOCB_SIZE);
}
/**
  * 函    数:写入24C02地址0中的OTA_FLAG信息
  * 参    数:None
  * 返 回 值:None
  */
void EP24C_WriteOTAInfo(void) {
    uint8_t i;
    uint8_t *wptr = (uint8_t *)&OTA_INFO;
    uint16_t write_size = 16;  // 24C02页大小
    
    for (i = 0; i < OTA_INFOCB_SIZE / write_size; i++) {
        // 每次写入16字节,避免指针大小错误
        EP24C_WriteLongData(i * write_size, wptr + i * write_size, write_size);
        HAL_Delay(5);  // 确保写入周期完成
    }
}
/**
  * 函    数:BootLoader功能选择函数
  * 参    数:None
  * 返 回 值:None
  * 注    意: 通过判断OTA_FLAG选择更新程序还是跳转程序
  */
void BootLoader_Brance(void)
{
	if(OTA_INFO.OTA_FLAG == OTA_SET_FLAG){
		U1_Printf("OTA SET\r\n");

	}else{
		U1_Printf("OTA NOT SET,TO Sector A\r\n");
		Load_A(FLASH_ACode_STARTADDR);
	}
}
/**
  * 函    数:SP指针赋值函数
  * 参    数:Addr地址
  * 返 回 值:None
  */
__asm void MSR_SP(uint32_t addr)
{
	MSR MSP, r0
	BX r14
}
/**
  * 函    数:无OTA跳转函数
  * 参    数:Addr地址
  * 返 回 值:None
  */
void Load_A(uint32_t addr)
{
	if (*(uint32_t *)addr >= 0x20000000 && *(uint32_t *)addr <= 0x2002FFFF){
		MSR_SP(*(uint32_t *)addr);
		LOAD_A = (load_a)*(uint32_t *)(addr + 4);
		BootLoader_Clear();
		LOAD_A();
	}
}
void BootLoader_Clear(void)
{
	HAL_UART_DeInit(&huart1);
	HAL_I2C_DeInit(&hi2c1);
}
/**
  * 函    数:函数初始化,所有初始化代码都放在这
  * 参    数:None
  * 返 回 值:None
  */
void Task_Main_Init(void)
{
	
}
/**
  * 函    数:主函数
  * 参    数:None
  * 返 回 值:None
  */
void Task_Main(void) {
    // 初始化数据
    OTA_INFO.OTA_FLAG = 0xAABB1122;
    for (uint8_t i = 0; i < 11; i++) {
        OTA_INFO.Firelen[i] = i;
    }
    
    // 写入到24C02
    EP24C_WriteOTAInfo();
    
    // 从24C02读取
    EP24C_ReadOTAInFo();
    
    // 打印结果
    U1_Printf("OTA FLAG = 0x%08X\r\n", OTA_INFO.OTA_FLAG);
    for (uint8_t i = 0; i < 11; i++) {
        U1_Printf("FireLen[%d] = %u\r\n", i, OTA_INFO.Firelen[i]);
    }
    
    BootLoader_Brance();
}

注意我在void EP24C_WriteOTAInfo(void)函数中使用的是24C02写入长数据,可以跨页; 

5.完成OTA更新过程 

 新增了状态机;测试代码是OTA更新判断中将对应状态置位,并记录OTA事件;

然后通过再while循环中判断状态机的状态 执行相应程序

while中有很多if来判断状态 这里只有一个if,因为我此时测试的只有OTA事件状态

Firelen的0号成员记录了更新的长度,此时我已经将FIRELEN写为了0x55;

0x55的十进制为85

/**
  * 函    数:主函数中的循环,所有需要循环的代码放在这
  * 参    数:None
  * 返 回 值:None
  * 注    意:我们在循环中判断状态机的标志 不同的标志对应不同的功能 我们这里测试的是OTA更新事件功能  
  */
void Task_Main_While(void)
{
	uint8_t i;
	//表示测试给OTA更新事件
	if (BootStatusFlag & UPDATA_UPDTA_A){
		//更新A区 OTA事件更新的长度存储在Firelen的0号中
		U1_Printf("本次更新的长度为:%d\r\n",OTA_INFO.Firelen[UpDataA.W25Q128_BlockNum]);
		//STM32的Flash控制器对写入操作有严格的 对齐限制 最小写入4个字节
		if (OTA_INFO.Firelen[UpDataA.W25Q128_BlockNum] % 4 == 0){
			//先擦除A区 A区起始为第四个扇区 4-11一共八个扇区
			MyFlash_EraseSectors(FLASH_ACode_STARTNUM, FLASH_ACode_NUM);
			//判断1024的倍数  拿数据 写入FLASH
			for(i = 0;i < OTA_INFO.Firelen[UpDataA.W25Q128_BlockNum] / UPDATA_SINGLE_SIZE; i++){
				//从W25Q128的指定BLOCK读取数据 
				Flash_ReadBytes(1024 * i + UpDataA.W25Q128_BlockNum * 64 * 1024, UpDataA.UpDataBuffer, UPDATA_SINGLE_SIZE);
				//拿完数据后 更新到A区 写整数
				MyFlash_WriteBuffer(i * 1024 + F407_FLASH_STARTADDR, UpDataA.UpDataBuffer, UPDATA_SINGLE_SIZE);
			}
			if (OTA_INFO.Firelen[UpDataA.W25Q128_BlockNum] % 1024 != 0){	//写余数
				Flash_ReadBytes(1024 * i + UpDataA.W25Q128_BlockNum * 64 * 1024, UpDataA.UpDataBuffer, OTA_INFO.Firelen[UpDataA.W25Q128_BlockNum] % 1024);
				//拿完数据后 更新到A区
				MyFlash_WriteBuffer(i * 1024 + F407_FLASH_STARTADDR, UpDataA.UpDataBuffer, OTA_INFO.Firelen[UpDataA.W25Q128_BlockNum] % 1024);
			}
			if (UpDataA.W25Q128_BlockNum == 0){
				OTA_INFO.OTA_FLAG = 0;
				EP24C_WriteOTAInfo();
			}
			HAL_NVIC_SystemReset();
		}else{
			U1_Printf("本次写入长度有误ERROR");
			//清除标志位
			BootStatusFlag &=~ UPDATA_UPDTA_A;
		}
	}	
}

测试结果如下:

6.实现串口命令行跳转函数 

主要编写了两个进入串口 CMD的函数

通过再BootLoder跳转函数中判断,进入CMD模式还是选择OTA事件;

如果我们输入w则进入CMD模式;测试结果如下:

注意我没将接收到w打印出来,直接跳转到CMD;代码中我写的W,我用w调试了好久,干;

(1)实现串口CMD功能1/7

功能1和功能7分别为擦除A扇区和重启;

我们只用编写一个函数用于判断接收到的第一个字符是对应数字就执行对应功能;

/**
  * 函    数:实现串口命令行指令
  * 参    数:Data* 接收的数据 DataLength接收数据长度
  * 返 回 值:None
  */
void BootLoader_CMDFunciton(uint8_t *Data, uint16_t DataLength)
{
	if (DataLength == 1 && Data[0] == '1')
	{
		U1_Printf("擦除A区成功\r\n");
		MyFlash_EraseSectors(FLASH_ACode_STARTNUM, FLASH_ACode_NUM);
	}
	else if(DataLength == 1 && Data[0] == '7')
	{
		HAL_Delay(50);
		U1_Printf("重启成功\r\n");
		HAL_NVIC_SystemReset();
	}
}

注意:当通过串口发送数字1时,实际传输的是其ASCII字符'1',需要加上单引号;

然后我们在循环中判断接收到的字符就行;

	uint8_t i;
	if(UCB_CB.URxDataOut != UCB_CB.URxDataIn){
		for (i = 0; i < UCB_CB.URxDataOut -> end - UCB_CB.URxDataOut ->start +1; i++){
			U1_Printf("%c\r\n", UCB_CB.URxDataOut->start[i]);
		}
		BootLoader_CMDFunciton(UCB_CB.URxDataOut ->start, UCB_CB.URxDataOut -> end - UCB_CB.URxDataOut ->start +1);
		UCB_CB.URxDataOut++;
		if (UCB_CB.URxDataOut == UCB_CB.URxDataEnd)
		{
			UCB_CB.URxDataOut = &UCB_CB.URxDataBuffer[0];
		}
	}

然后,我们在串口中进行测试;测试结果如下:

 

注意:我发现当我在main.c中的主循环中放入代码后,当跳转到A区时,不能正常打印A区测试代码“Test”;通过询问AI 发现需要在跳转前清空掉所使用的外设和禁止全部中断;我已经实现了上一步,还需禁止全部中断,在Load_A函数中加入__disable_irq()后能够正常打印;

(2)实现功能2 

Xmdoem协议的传输图如下:

 

首先,由单片机发送大写字母C,3s后再发C,表示能够接收数据了;

然后,服务器开始分片发送,先发送包头SOH,然后发送数据编码0x01,接着发0x01的反码0xFE; 发送数据128个字节和校验位CRC 2个字节; 

接着,单片机收到数据 校验无误后 发送应答位 反之 非应答;

最后,由服务器发送两次EOT 结束传输;

一片数据有 包头 编号 编号取反 128字节数据 2字节CRC 一共133个字节;

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值