STM32串口IAP(OTA)升级过程解析

STM32串口IAP(OTA)升级过程解析

总结

我们可以通过串口使用Xmodem协议将.bin文件发送到外部norlash,再读取norlash数据到A区实现IAP固件更新。
也可通过基于串口的如蓝牙 WIFI模块 4G模块 CAN设备实现空中升级OTA。
基于此思想,可以扩展阿里云带版本推送的双AB区的OTA无感升级。

目录

1.STM32内存分布和启动流程
2.设计逻辑–AB分区理念
3.串口空闲中断+DMA传输
4.读写外部flash 写内部flahs
5.Xmodem协议
6.Bootloader命令行
7.命令对应的API
8.跳转A区,执行应用程序
9.MDK生成.bin文件

1.STM32启动方式和内存分布

STM32内存映射

1.1重映射与从FLASH启动

我们可以发现每次我们不论在那学习关于STM32相关的知识都避免不了要接触到0X08000000这个关键的地址。
这个地址使我们把BOOT = 0 BOOT1 = X代码下载到的位置,但是我们的STM32不是上电之后从0开始运行的吗?这是怎么回事呢

接下来我们先看一下STM32的自举方式
在这里插入图片描述
无论是从内部flash启动,还是系统存储器,亦或者是SRAM都无一例外提到了映射,那什么是映射,又为什么要映射呢?


在这里插入图片描述


我们来看这句话,会发现改变BOOT引脚会将0X00000000 - 0X001FFFFF的映射也给一起改变
我们可以通俗的理解 此时的0X00000000-0x001 F FFFF 被 0X08000000-0X08FFFFFF替代了(前提是*BOOT = 0 BOOT1 = X)。


在这里插入图片描述


1.2SRAM启动使用场景

映射到SRAM 也就是0X20000000 (SRAM的起始地址)
那就很清晰的认识到,的确是从0开始运行,只不过通过一些手段(y映射),把FLASH或者SRAM的代码给“搬运”倒了0X00000000。
在这里插入图片描述
而下载到SRAM如上面图片所说,RAM有易失性,仅在调试时代码改动较小,代码量大时相对有很大的效果提升。

1.3系统存储器启动

那不是还有一个系统存储器(BOOT = 1 BOOT1 = 0)启动吗,又是怎么回事?
其实soc厂商在生产时给内部弄了一个Bootloader,我们下载时使用下载器如STLINK JLINK ,芯片内部Bootloader ,会将代码加载到目标芯片的FLAH。
所以我们在下载到
系统存储器
之后,还要将BOOT引脚恢复到BOOT = 0 BOOT1 = X,即还是从FLASH启动

1.4main函数开始前STM32干了什么

1.4.1开辟栈和堆

打开startup启动文件

Stack_Size      EQU     0x00000400

                AREA    STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem       SPACE   Stack_Size
__initial_sp
                                                  
; <h> Heap Configuration
;   <o>  Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>

栈的作用是用于局部变量,函数调用,函数形参等的开销,栈的大小不能超过内部SRAM 的大小。不然可能会出现的HardFault的错误。代码量过大时需手动更改栈大小。

Stack_Size      EQU     0x00000400
				||
Stack_Size      EQU     0x00000800

Stack_Size EQU 0x00000200表示开辟栈的大小为 0X00002000,EQU==C 中的 define。

ARER 后面的接段属性。段名为STACK;NOINIT 表示不初始化在(后续__main启动);READWRITE 表示可读可写,ALIGN=3,表示按照 8 字节对齐。
SPACE 用于分配大小等于 Stack_Size连续内存空间,单位为字节。



堆主要用来动态内存的分配,像 malloc()函数申请的内存就在堆中(我们常常使用自定义的malloc,因为c库的malloc存在线程安全问题,此处不讨论,否则文章太长了)。

Heap_Size       EQU     0x00000200

                AREA    HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem        SPACE   Heap_Size
__heap_limit

                PRESERVE8
                THUMB

开辟堆的大小为 0X00000200(512 字节),名字为 HEAP,NOINIT 即不初始化(后续__main启动),可读可写,8字节对齐。__heap_base 表示堆的起始地址,__heap_limit 表示堆的结束地址。

1.4.2设置中断向量表
__Vectors       DCD     __initial_sp               ; Top of Stack
                DCD     Reset_Handler              ; Reset Handler
                DCD     NMI_Handler                ; NMI Handler
                DCD     HardFault_Handler          ; Hard Fault Handler
                DCD     MemManage_Handler          ; MPU Fault Handler
                DCD     BusFault_Handler           ; Bus Fault Handler
                DCD     UsageFault_Handler         ; Usage Fault Handler
                DCD     0                          ; Reserved
                ……

我们发现0开始首先居然是 __initial_sp栈顶指针 这是我们后续IAP一个判断依据。前4B为栈顶指针,0X00000004才是中断向量表起始

1.4.3中断服务函数注册
; Reset handler
Reset_Handler   PROC
                EXPORT  Reset_Handler             [WEAK]
                IMPORT  __main
                IMPORT  SystemInit
                LDR     R0, =SystemInit
                BLX     R0               
                LDR     R0, =__main
                BX      R0
                ENDP
                
; Dummy Exception Handlers (infinite loops which can be modified)

NMI_Handler     PROC
                EXPORT  NMI_Handler                [WEAK]
                B       .
                ENDP
HardFault_Handler\
                PROC
                EXPORT  HardFault_Handler          [WEAK]
                B       .
                ENDP

除了复位中断服务函数Reset_Handler被实现
其他的都未被实现函数体 (weak若定义),仅仅实现了注册(牵扯到内部函数指针与回调函数的定义,此处不涉及)。

1.4.4Reset_Handler复位中断

前面其实还有特权级(M3内核)NVIC清零设置,主堆栈指针设置,内核堆栈指针设置,PC (PC = Reset_Handlerr,SP = *(0X80000000),其中、 *(0X80000000)指向的是SRAM 0x2000 0000即栈在0X2000 0000 (左右))设置我们不过多赘述,直接查看Reset_Handler复位中断。

; Reset handler
Reset_Handler   PROC
                EXPORT  Reset_Handler             [WEAK]
                IMPORT  __main
                IMPORT  SystemInit
                LDR     R0, =SystemInit
                BLX     R0               
                LDR     R0, =__main
                BX      R0
                ENDP

定义了一个服务程序,PROC表示程序的开始。
使用EXPORT将Reset_Handler申明为可被外部引用。
使用IMPORT表示该标号来自外部文件,**SystemInit()**是一个库函数,在system_stm32f1xx.c中定义的。
SystemInit ()初始化系统时钟

__main 是一个标准的 C 库函数,主要作用是初始化用户堆栈,这个是由编译器完成的
__mian 会对开辟的堆和栈进行初始化 并且调用main函数
在这里插入图片描述
当我们在Options for Target 使用微库 Use MocroLIB设置
该函数最终会调用我们自己写的main函数,执行我们自己的代码。

LDR     R0, =SystemInit

这是一条汇编指令,表示从存储器中加载SystemInit到一个寄存器R0的地址中。

BLX     R0

表示跳转到寄存器R0的地址,并根据寄存器的 LSE 确定处理器的状态,还要把跳转前的下条指令地址保存到 LR。

LDR     R0, =__main

表示从存储器中加载__main到一个寄存器R0的地址中。

BX      R0

跳转到至指定寄存器的地址后,不会返回。
与PROC是对应的,表示程序的结束。

2.设计逻辑–AB分区理念

在这里插入图片描述
以F103C8T6 (64KB的FLASH) 为例:将FLASH分为两个部分 B区:Bototloader区 A区:APP区
相关宏定义设置

//以下单位均为B字节
//flash起始地址
#define C8T6_FLASH_START_ADDR           0X08000000     
//flash页page个数
#define C8T6_FLASH_NUM                  64
//flash页page大小 1k
#define C8T6_FLASH_PAGE_SIZE            1024

//b区页数
#define C8T6_B_PAGE_NUM                 20
//a区页数
#define C8T6_A_PAGE_NUM                 C8T6_FLASH_NUM - C8T6_B_PAGE_NUM
//a区起始页号
#define C8T6_A_START_PAGE               C8T6_B_PAGE_NUM
//a区起始地址
#define C8T6_A_START_ADDR               C8T6_FLASH_START_ADDR + C8T6_A_START_PAGE * C8T6_FLASH_PAGE_SIZE

前20KB为Bootloader引导程序区此区域代码功能:
①擦除A区代码数据;
②串口IAP下载A区;
③向外部FLASH下载程序;
④读取外部FLSAH到内部FLASH使用;
⑤重启

3.串口空闲中断+DMA传输

3.1环形接收区建立

#define NUM                 10


typedef struct
{
    uint8_t* start;
    uint8_t* end;
}UCB_URXBufferptr;

typedef struct
{
    uint16_t URxcounter;
    UCB_URXBufferptr URxDataptr[NUM];
    UCB_URXBufferptr *URxDataIN;
    UCB_URXBufferptr *URxDataOUT;
    UCB_URXBufferptr *URxDateEND;
}UCB_CB;

缓冲区指针初始化

void URx1_ptrInit(void)
{
    U1CB.URxcounter = 0;
    U1CB.URxDataIN = &U1CB.URxDataptr[0];
    U1CB.URxDataOUT = &U1CB.URxDataptr[0];
    U1CB.URxDateEND = &U1CB.URxDataptr[NUM-1];
    U1CB.URxDataIN->start = rx1_buffer;
}

在这里插入图片描述
其实我们还是定义了一整个大数组 但是被我们分成了NUM个部分 后续称为块 使用IN OUT END来界定。

IN与OUT一旦不相等说明接受了多少个数据包,也就是进入了多少次空闲中断

块指针有start 和 end指针
start表示下次接受起始位置
end一直指向当前块的起始
原理:如果end != start表示当前这个块有数据接收 可以进行处理

每接收数据一次IN后移 IN与END重合表明当前接收已满,IN再次指向开头,实现循环

OUT指针就是来指示上次接收完毕的块地址,每次接收IN会后移,那我怎么知道这次接收从缓冲区哪开始的?
那就通过OUT指向进入中断管服务函数前的IN,即上次的接收完的末尾,然后对OUT的start与end进行判断

具体详情可参考中断服务函数

3.2串口与DMA配置

串口初始化配置

注意开启串口空闲中断以及调用URx1_ptrInit函数。

void Usart1_Init(uint32_t baurd)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
    
    GPIO_InitTypeDef GPIO_initstruct;
    GPIO_initstruct.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_initstruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_initstruct.GPIO_Pin = GPIO_Pin_10;
    GPIO_Init(GPIOA,&GPIO_initstruct);
    
    GPIO_initstruct.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_initstruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_initstruct.GPIO_Pin = GPIO_Pin_9;
    GPIO_Init(GPIOA,&GPIO_initstruct);
    
    
    USART_InitTypeDef usart_initstruct;
    usart_initstruct.USART_BaudRate = baurd;
    usart_initstruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    usart_initstruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
    usart_initstruct.USART_Parity = USART_Parity_No;
    usart_initstruct.USART_StopBits = USART_StopBits_1;
    usart_initstruct.USART_WordLength = USART_WordLength_8b;
    USART_Init(USART1,&usart_initstruct);
    
    USART_ITConfig(USART1,USART_IT_IDLE,ENABLE);//!!!!!!!!!!!!!!!!!!!!!!!!!!USART_IT_IDLE
    USART_DMACmd(USART1,USART_DMAReq_Rx,ENABLE);
    
    NVIC_InitTypeDef nvic_initstruct;
    nvic_initstruct.NVIC_IRQChannel = USART1_IRQn;
    nvic_initstruct.NVIC_IRQChannelCmd = ENABLE;
    nvic_initstruct.NVIC_IRQChannelPreemptionPriority = 5;
    NVIC_Init(&nvic_initstruct);
    
    URx1_ptrInit();
    
    USART_Cmd(USART1,ENABLE);
    
}

DMA配置
串口1接收通道对应DMA1_Channel5

void Dma_init(uint32_t dma_PeripheralBaseAddr1,uint32_t dma_MemoryBaseAddr1,
              uint32_t dma_PeripheralBaseAddr2,uint32_t dma_MemoryBaseAddr2)
{
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
    
    //串口dma to rx1_buffer
    dam_initstruct.DMA_BufferSize = USART1_RX_MAX + 1;  //不让其产生完成中断
    dam_initstruct.DMA_DIR = DMA_DIR_PeripheralSRC;
    dam_initstruct.DMA_M2M = DMA_M2M_Disable;
    dam_initstruct.DMA_MemoryBaseAddr = dma_MemoryBaseAddr1;     //usart1_rx_buf
    dam_initstruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
    dam_initstruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
    dam_initstruct.DMA_Mode = DMA_Mode_Normal;
    dam_initstruct.DMA_PeripheralBaseAddr = dma_PeripheralBaseAddr1;     //&(USART1->DR)
    dam_initstruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    dam_initstruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    dam_initstruct.DMA_Priority = DMA_Priority_High;
    DMA_Init(DMA1_Channel5,&dam_initstruct);
    
    DMA_Cmd(DMA1_Channel5,ENABLE);//usart1 rx channel5
    }

注意以下代码

dam_initstruct.DMA_BufferSize = USART1_RX_MAX + 1;  //不让其产生完成中断

我们这样子设置的目的是不让DMA产生完成中断(虽然我们也没有使能),这样子就算DMA模式是DMA_Mode_Normal,我们也可以将传输次数恢复在串口空闲中断实现。

串口中断服务函数

uint8_t length = 0;
void USART1_IRQHandler(void)
{
    uint16_t clear_idle = 0;
    if(USART_GetITStatus(USART1,USART_IT_IDLE) == SET)
    {
        clear_idle = USART1->SR;
        clear_idle = USART1->DR;
        
        //清楚USART_IT_IDLE的参数
        //因此清楚这个标志位 先读sr 再读dr
        
        U1CB.URxcounter += (USART1_RX_MAX + 1) - DMA_GetCurrDataCounter(DMA1_Channel5);
        U1CB.URxDataIN->end = &rx1_buffer[U1CB.URxcounter - 1];
        U1CB.URxDataIN++;
        if(U1CB.URxDataIN == U1CB.URxDateEND)
        {
            U1CB.URxDataIN = &U1CB.URxDataptr[0];
        }
        
        if(USART1_RX_BUFSIZE - U1CB.URxcounter >= USART1_RX_MAX)
        {
            U1CB.URxDataIN->start = &rx1_buffer[U1CB.URxcounter - 1 + 1];
        }
        else
        {
             U1CB.URxDataIN->start = rx1_buffer;
             U1CB.URxcounter = 0;
        }
        
        DMA_Cmd(DMA1_Channel5,DISABLE);
        DMA_SetCurrDataCounter(DMA1_Channel5,NUM + 1);
        dam_initstruct.DMA_MemoryBaseAddr = (uint32_t)U1CB.URxDataIN->start;
        DMA_Init(DMA1_Channel5,&dam_initstruct);  //次数会再次在这配置
        DMA_Cmd(DMA1_Channel5,ENABLE);
    }
}

注意IDLE空闲中断标志位的清除方式,以及

DMA_Init(DMA1_Channel5,&dam_initstruct);

实现再次复制DMA的传输次数。

HAL库版本

void URx1_ptrInit(void)
{
    U1CB.URxcounter = 0;
    U1CB.URxDataIN = &U1CB.URxDataptr[0];
    U1CB.URxDataOUT = &U1CB.URxDataptr[0];
    U1CB.URxDateEND = &U1CB.URxDataptr[NUM-1];
    U1CB.URxDataIN->start = rx1_buffer;
}



void usart_init(uint32_t baudrate)
{                         

    __HAL_RCC_USART1_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();
    
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = GPIO_PIN_9;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    GPIO_InitStruct.Pin = GPIO_PIN_10;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
        
    huart1.Instance = USART1;
    huart1.Init.BaudRate = baudrate;
    huart1.Init.WordLength = UART_WORDLENGTH_8B;
    huart1.Init.StopBits = UART_STOPBITS_1;
    huart1.Init.Parity = UART_PARITY_NONE;
    huart1.Init.Mode = UART_MODE_TX_RX;
    huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
    huart1.Init.OverSampling = UART_OVERSAMPLING_16;
    HAL_UART_Init(&huart1); 
    
    
    
    // 启用空闲中断
    __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
    
    // 初始化缓冲区指针
    URx1_ptrInit();
    
    // NVIC配置
    HAL_NVIC_SetPriority(USART1_IRQn, 5, 0);
    HAL_NVIC_EnableIRQ(USART1_IRQn);
   
}


void USART1_IRQHandler(void)
{
    uint16_t clear_idle = 0;
    clear_idle = USART1->SR;
    clear_idle = USART1->DR;
    
    __HAL_UART_CLEAR_IDLEFLAG(&huart1);
    
    HAL_UART_IdleCallback(&huart1);
}

void HAL_UART_IdleCallback(UART_HandleTypeDef *huart)
{
    if(huart->Instance == USART1)
    {        
        HAL_UART_AbortReceive(huart);
        
        uint32_t temp;
        temp = huart->Instance->SR;  // 清除空闲中断标志
        temp = huart->Instance->DR;
        
        // 计算接收数据长度
        U1CB.URxcounter += USART1_RX_MAX  - __HAL_DMA_GET_COUNTER(huart->hdmarx);
        
        // 更新缓冲区指针
        U1CB.URxDataIN->end = &rx1_buffer[U1CB.URxcounter - 1];
        U1CB.URxDataIN++;
        
        if(U1CB.URxDataIN > U1CB.URxDateEND)
        {
            U1CB.URxDataIN = &U1CB.URxDataptr[0];
        }
        
        
        // 处理缓冲区回绕
        if(USART1_RX_BUFSIZE - U1CB.URxcounter >= USART1_RX_MAX)
        {
            U1CB.URxDataIN->start = &rx1_buffer[U1CB.URxcounter];
        }
        else 
        {
            U1CB.URxDataIN->start = rx1_buffer;
            U1CB.URxcounter = 0;
        }
        
         __HAL_DMA_ENABLE(huart->hdmarx);
        HAL_UART_Receive_DMA(&huart1, (uint8_t*)U1CB.URxDataIN->start, USART1_RX_MAX);
    }
}

数据回显测试
main.c

while(1)
	{
        if(U1CB.URxDataIN != U1CB.URxDataOUT)
        {
            BootLoader_Event(U1CB.URxDataOUT->start,U1CB.URxDataOUT->end - U1CB.URxDataOUT->start + 1);
            
            U1CB.URxDataOUT++;
            if(U1CB.URxDataOUT == U1CB.URxDateEND)
            {
                U1CB.URxDataOUT = &U1CB.URxDataptr[0];
            }
        }
        …………

判断OUT与IN是否重合来判断数据包接收个数 判断OUT的start与end来判断当前包接收数据个数

4.读写外部flash 写内部flahs

首先完成norflash(W25Q64)操作API,包括底层SPI驱动。
SPI驱动

void My_SPI_Init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
    
    GPIO_InitTypeDef GPIO_Initstructure;
    GPIO_Initstructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;                       //MISO配置为浮空输入
    GPIO_Initstructure.GPIO_Pin = GPIO_Pin_6 ;
    GPIO_Initstructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA,&GPIO_Initstructure);
    
    GPIO_Initstructure.GPIO_Mode = GPIO_Mode_AF_PP;                             //MOSI配置为推完服用输出
    GPIO_Initstructure.GPIO_Pin = GPIO_Pin_7 | GPIO_Pin_5;
    GPIO_Init(GPIOA,&GPIO_Initstructure);
    
    
    SPI_InitTypeDef SPI_Initstructure;
    SPI_Initstructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;        //波特率分频128
    SPI_Initstructure.SPI_CPHA = SPI_CPHA_1Edge;                                  //空闲电平为低
    SPI_Initstructure.SPI_CPOL = SPI_CPOL_Low;                                //第二个边沿采样
    SPI_Initstructure.SPI_CRCPolynomial = 7;                                    //crc默认配置为7
    SPI_Initstructure.SPI_DataSize = SPI_DataSize_8b;                           //数据长度8为
    SPI_Initstructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;          //全双工
    SPI_Initstructure.SPI_FirstBit = SPI_FirstBit_MSB;                          //高位先行
    SPI_Initstructure.SPI_Mode = SPI_Mode_Master;                               //主机模式
    SPI_Initstructure.SPI_NSS = SPI_NSS_Soft;                                   //软件片选
    SPI_Init(SPI1,&SPI_Initstructure);
    
    SPI_Cmd(SPI1,ENABLE);
}




/*函数功能:spi读取一个字节------------------------*/
/*参数:要发送的空白无用字节0xff-------------------*/
/*返回值:读取到的字节----------------------------*/
uint8_t SPI1_ReadBytedata(uint8_t txd)
{
    while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) != SET);       //判断TXE是否成立 成立就跳转 
    SPI_I2S_SendData(SPI1,txd);                                         //0XFF发送成功
    while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) != SET);      //等待RXNE成立 即接收完成
    return SPI_I2S_ReceiveData(SPI1);                                   //返回读取
}


/*函数功能:spi写------------------------*/
/*参数:wdata要写的数据地址-------------------*/
/*参数:datalen要写的数据个数-------------------*/
/*返回值:无----------------------------*/
void SPI1_Write(uint8_t *wdata,uint16_t datalen)
{
    uint16_t i ;
    for(i = 0;i < datalen; i++)
    {
        SPI1_ReadBytedata(wdata[i]);
    }
}



/*函数功能:spi读取------------------------*/
/*参数:rdata读取到的数据放的位置-------------------*/
/*参数:datalen要读的数据个数-------------------*/
/*返回值:无----------------------------*/
void SPI1_Read(uint8_t *rdata,uint16_t datalen)
{
    uint16_t i ;
    for(i = 0;i < datalen; i++)
    {
        rdata[i] = SPI1_ReadBytedata(0xff);
    }
}

norflash操作API

#define CS_ENABLE    GPIO_ResetBits(GPIOA,GPIO_Pin_4)
#define CS_DISABLE   GPIO_SetBits(GPIOA,GPIO_Pin_4)

#define RD_SR_CMD                  0X05
#define NOUSE_MES               0XFF
#define WR_ENABLE               0X06
#define BLOCK_ERASE             0XD8
#define PAGE_PROGRAM            0X02
#define RRAD_DATA               0X03
…………

void W25Q64_Init(void)
{ 
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
    
    GPIO_InitTypeDef gpio_initstructure;
    gpio_initstructure.GPIO_Mode = GPIO_Mode_Out_PP;
    gpio_initstructure.GPIO_Pin = GPIO_Pin_4;
    gpio_initstructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA,&gpio_initstructure);
    
    
    CS_DISABLE;                                                 //初始化先不选中
    My_SPI_Init();
}




/*函数功能:w25q64等待忙------------------------*/
/*参数:无-------------------*/
/*返回值:无----------------------------*/
void W25Q64_WaitBusy(void)
{
    uint8_t res;
    do
    {
        CS_ENABLE;                                              //片选选中
        
        SPI1_ReadBytedata(RD_SR_CMD);                           //发送读状态寄存器的命令
        res = SPI1_ReadBytedata(NOUSE_MES);                     //读取状态寄存器 参数为OXFF
        
        CS_DISABLE;                                             //片选取消
    }while((res & 0x01) == 1);//sr.0 busy                              //判断忙
}


/*函数功能:w25q64写使能------------------------*/
/*参数:无-------------------*/
/*返回值:无----------------------------*/
void W25Q64_WR_ENABLE(void)
{
    W25Q64_WaitBusy();                                          //等待忙
    CS_ENABLE;                                                  //片选
    
    SPI1_ReadBytedata(WR_ENABLE);                               //发送写使能
    
    CS_DISABLE;                                                 //取消片选
}



/*函数功能:w25q64块擦除 64k------------------------*/
/*参数:块的序号  num*1024*64 具体地址-------------------*/
/*返回值:无----------------------------*/
void W25Q64_64K_ERASE(uint8_t blocknum)//8m = 8*1024 = 8192k 8192k/64k = 128块 1块64kB
{
    uint8_t wdata[4] = {0};
    wdata[0] = BLOCK_ERASE;                                     //块擦除命令
    wdata[1] = (blocknum * 64 * 1024) >> 16;                    //擦除地址高8位
    wdata[2] = (blocknum * 64 * 1024) >> 8;                     //擦除地址中八位
    wdata[3] = (blocknum * 64 * 1024);                          //擦除地址第八位
    
    W25Q64_WaitBusy();                                          //等待忙
    
    W25Q64_WR_ENABLE();                                         //写使能
    
    CS_ENABLE;                                                  //片选
    
    
    SPI1_Write(wdata,4);                                        //发送擦除
    
    
    CS_DISABLE;                                                 //取消片选
    W25Q64_WaitBusy();                                          //等待忙
}   



/*函数功能:w25q64页写256B------------------------*/
/*参数:写的数据的地址 wbuf-------------------*/
/*参数:页的序号  -------------------*/
/*返回值:无----------------------------*/
void W25Q64_Page_write(uint8_t* wbuf,uint16_t pagenum) //256B一页
{
    uint8_t wdata[4] = {0};
    wdata[0] = PAGE_PROGRAM;                                    //页编程命令
    wdata[1] = (pagenum * 256) >> 16;                           //页高八位地址
    wdata[2] = (pagenum * 256) >> 8;                            //页中八位地址
    wdata[3] =  pagenum * 256;                                  //页第八位地址
    
    W25Q64_WaitBusy();                                          //等待忙
    
    W25Q64_WR_ENABLE();                                         //写使能
    
    CS_ENABLE;                                                  //片选
    
    
    SPI1_Write(wdata,4);                                        //发命令
    SPI1_Write(wbuf,256);                                       //写字节
    
    
    CS_DISABLE;                                                 //取消片选
}



/*函数功能:w25q64读------------------------*/
/*参数:读的数据的地址 rbuf-------------------*/
/*参数:读取的地址addr  -------------------*/
/*参数:读取的数据长度址datalen  -------------------*/
/*返回值:无----------------------------*/
void W25Q64_Readdata(uint8_t* rbuf,uint32_t addr,uint32_t datalen) 
{
    uint8_t wdata[4] = {0};
    wdata[0] = RRAD_DATA;
    wdata[1] = addr  >> 16;
    wdata[2] = addr  >> 8;
    wdata[3] = addr ;
    
    W25Q64_WaitBusy();
    CS_ENABLE;
    
    
    SPI1_Write(wdata,4);
    SPI1_Read(rbuf,datalen);
    
    
    CS_DISABLE;
}

因为是要进行IAP(OTA)固件升级,整个新版固件.bin文件很大,所以提供页写W25Q64_Page_write与块擦除W25Q64_WR_ENABLE

为什么弄一个页写却弄一个块擦除呢
是因为我们把norlash的每一个块都用来保存一个版本的固件(固件大小 <= 块大小)\

注意我们设置第0个块存放最新OTA的固件,其他存放IAP或者低版本的OTA的固件

每次版本覆盖,使用最小擦除单位扇区擦除,会比较耗时
而写入采用页(1KB)写是为了和后续接收协议Xmodem协议适配,而且F1内部的flash有写入要求,即一次操作必须为4个字节,即字写入

内部FLASH操作API

/*函数功能:flash擦除------------------------*/
/*参数:start_num 起始块的序号  一块1KB-------------------*/
/*参数:num 擦除个数-------------------*/
/*返回值:无----------------------------*/
void Erase_Flash(uint16_t start_num,uint16_t num)
{
    uint16_t i;
    
    FLASH_Unlock();                                                             //整片flash解锁
    
    
    for(i = 0;i < num;i++)
    {
        FLASH_ErasePage( (start_num * 1024 + 0x08000000) + (1024 * i) );        //擦除
    }
    
    FLASH_Lock();                                                               //上锁
}



/*函数功能:flash写   一次写一个字------------------------*/
/*参数:addr写的地址  -------------------*/
/*参数:wdata 写的数据存放的地址-------------------*/
/*参数:datalen 写的数据个数-------------------*/
/*返回值:无----------------------------*/
void Write_Flash(uint32_t addr,uint32_t* wdata,uint32_t datalen)
{
    FLASH_Unlock();
    
    while(datalen)
    {
        FLASH_ProgramWord(addr,*wdata);                                         //一次写入一个字
        datalen -= 4;                                                           //长度1表示一个B 减4
        addr += 4;                                                              //地址一位一字节
        wdata++;                                                                //wdata形式位uint32_t
    }
    
    FLASH_Lock();
}

5.Xmodem协议

前面提到了页写为了匹配Xmodem协议,接下来看看Xmodem协议到底几斤几两。
在这里插入图片描述

Xmodem传输数据单位为信息包,信息包格式如下:

---------------------------------------------------------------------------
|     Byte1     |    Byte2    |     Byte3      |Byte4~Byte131|  Byte132   |
|-------------------------------------------------------------------------|
|Start Of Header|Packet Number|~(Packet Number)| Packet Data |  Check Sum |
---------------------------------------------------------------------------

数据传输流程

#<SOH> 01H
#<EOT> 04H
#<ACK> 06H
#<NAK> 15H
#<CAN> 18H
------------------------------------------------------------------------------
|               SENDER                |          |          RECIEVER         |
|                                     |  <---    |  NAK                      |
|                                     |          |  Time out after 3 second  |
|                                     |  <---    |  NAK                      |
| SOH|0x01|0xFE|Data[0~127]|CheckSum| |  --->    |                           |
|                                     |  <---    |  ACK                      |
| SOH|0x02|0xFD|Data[0~127]|CheckSum| |  --->    |                           |
|                                     |  <---    |  NAK                      |
| SOH|0x02|0xFD|Data[0~127]|CheckSum| |  --->    |                           |
|                                     |  <---    |  ACK                      |
| SOH|0x03|0xFC|Data[0~127]|CheckSum| |  --->    |                           |
|                                     |  <---    |  ACK                      |
| .                                   |          |  .                        |
| .                                   |          |  .                        |
| .                                   |          |  .                        |
|                                     |  <---    |  ACK                      |
| EOT                                 |  --->    |                           |
|                                     |  <---    |  ACK                      |
------------------------------------------------------------------------------

持续发’C’ 然后以SOH开头 CRC16结尾发送数据包 开始发数据停止发送’C’
目标每收到一个数据包 且校验CRC16正确发送ACK应答
发送EOT表示结束发送 再次发送最后的ACK应答

我们仅使用Xmodem协议 也就是完成持续发’C’以及CRC校验

while(1)
{
……
Delay_ms(10);                       //实现间隔1s发送C
        if(BootStateFlag & IAP_XMODE_C_FLAG)
        {
            if(updata_a.XmodemTime >= 100)
            {
                printf("C");
                updata_a.XmodemTime = 0;
            }
            updata_a.XmodemTime ++;
        }
……
}

CRC校验

/*函数功能:生成crc16 ------------------------*/
/*参数:帧 帧长度------------------*/
/*返回值:无----------------------------*/
uint16_t Xmodem_CRC16(uint8_t* data,uint16_t datalen)
{
    uint8_t i;
    
    uint16_t CRC_init = 0x0000;
    uint16_t CRC_poly = 0x1021;//多项式
    
    while(datalen--)
    {
        CRC_init = ( *data << 8 ) ^ CRC_init;
        for(i = 0;i < 8;i++)
        {
            if(CRC_init & 0x8000)//1000 0000 0000 0000
            {
                CRC_init = ( CRC_init << 1 ) ^ CRC_poly;
            }
            else
            {
                CRC_init = ( CRC_init << 1 );
            }
        }
        data++;
    }
    return CRC_init;
}

为了方便管理我们采用结构体来表示。

typedef struct
{
    uint8_t UpdataBuf[C8T6_FLASH_PAGE_SIZE];    //读取buffer大小
    uint32_t W25Q64_BlockNum;                   //外部flash的块号
    uint32_t XmodemTime;                        //xmode协议数据发送周期
    uint32_t XmodemDa_num;                      //数据包接收数量
    uint32_t XmodemCRC;                         //程序自己计算的CRC校验 和发来的比较
    
}UpDataA_CB;
//Xmodem只能一次接收128B                        

单独使用上述就可以满足要求了。
我们可以直接使用现成的软件SecureCRTPortable

在这里插入图片描述
在这里插入图片描述
直接发送我们的.bin文件

拓展:
1.将.bin文件直接生成.xmodem文件

#include <stdio.h>

#include <stdlib.h>

#include <stdint.h>



#define XMODEM_BLOCK_SIZE  128

#define PAD_CHAR           0x1A  // 填充字符



// XMODEM控制字符定义

#define SOH 0x01

#define EOT 0x04



// CRC-16-CCITT计算(XMODEM标准)

uint16_t calculate_crc(const uint8_t *data, size_t length) {

    uint16_t crc = 0x0000;

    for (size_t i = 0; i < length; ++i) {

        crc ^= (uint16_t)data[i] << 8;

        for (uint8_t j = 0; j < 8; ++j) {

            if (crc & 0x8000) {

                crc = (crc << 1) ^ 0x1021;

            } else {

                crc <<= 1;

            }

        }

    }

    return crc;

}



// 写入单个XMODEM数据包

int write_xmodem_block(FILE *out, uint8_t block_num, const uint8_t *data) {

    uint8_t header[3] = {

        SOH,                        // 起始字符

        block_num,                   // 块编号

        0xFF - block_num            // 块编号补码

    };

    

    // 写入包头

    if (fwrite(header, 1, sizeof(header), out) != sizeof(header)) {

        return -1;

    }

    

    // 写入数据块

    if (fwrite(data, 1, XMODEM_BLOCK_SIZE, out) != XMODEM_BLOCK_SIZE) {

        return -1;

    }

    

    // 计算并写入CRC

    uint16_t crc = calculate_crc(data, XMODEM_BLOCK_SIZE);

    uint8_t crc_bytes[2] = { (uint8_t)(crc >> 8), (uint8_t)crc };

    if (fwrite(crc_bytes, 1, sizeof(crc_bytes), out) != sizeof(crc_bytes)) {

        return -1;

    }

    

    return 0;

}



int main(int argc, char *argv[]) {

    if (argc != 3) {

        fprintf(stderr, "Usage: %s <input.bin> <output.xmodem>\n", argv[0]);

        return EXIT_FAILURE;

    }



    FILE *in_file = fopen(argv[1], "rb");

    if (!in_file) {

        perror("Error opening input file");

        return EXIT_FAILURE;

    }



    FILE *out_file = fopen(argv[2], "wb");

    if (!out_file) {

        perror("Error opening output file");

        fclose(in_file);

        return EXIT_FAILURE;

    }



    uint8_t block[XMODEM_BLOCK_SIZE] = {0};

    uint8_t block_num = 1;

    size_t bytes_read;



    while ((bytes_read = fread(block, 1, XMODEM_BLOCK_SIZE, in_file)) > 0) {

        // 填充不足的块

        if (bytes_read < XMODEM_BLOCK_SIZE) {

            memset(block + bytes_read, PAD_CHAR, XMODEM_BLOCK_SIZE - bytes_read);

        }

        

        if (write_xmodem_block(out_file, block_num, block) != 0) {

            fprintf(stderr, "Failed to write block %d\n", block_num);

            break;

        }

        

        // 处理块编号回绕(XMODEM规范允许)

        if (++block_num == 0) block_num = 1;

    }



    // 写入传输结束标记

    fputc(EOT, out_file);



    fclose(in_file);

    fclose(out_file);

    printf("Successfully created XMODEM package: %s\n", argv[2]);

    return EXIT_SUCCESS;

}

使用gcc编译出可执行文件

gcc xmodem.c -o xmodem_pack

给予文件可执行权限

chmod 777 xmodem_pack

编译.bin文件直接生成.xmodem文件

./xmodem_pack Project.bin project.xmodem

在这里插入图片描述
然后我们就可以使用串口助手直接发送文件了。

6.Bootloader命令行

首先我们定义一些标志位(bit标志,个数多,空间小)

//为a区更新flag  
#define  UPDATA_A_FLAG                      0X00000001//0000 0001
//xmode发送C的flag
#define IAP_XMODE_C_FLAG                    0X00000002//0000 0010
//xmode发送data的flag
#define IAP_XMODE_DATA_FLAG                 0X00000004//0000 0100
//CMD外部flash下载程序
#define BOOT_CMD_WB_FLASH_DOWN              0X00000010//0000 1000
//外部flash通过Xmodem下载程序flag
#define WRITE_WB_FLASH_XMODEM_FLAG          0X00000020//0001 0000
//CMD使用外部flash程序
#define BOOT_CMD_RED_WB_FLASH               0X00000040//0010 0000

倒计时等待

/*函数功能:输入M进入Bootloder命令行 ------------------------*/
/*参数:超时等待时间s------------------*/
/*返回值:1进入 0未进入----------------------------*/
uint8_t BootLoader_Enter(uint8_t Timeout)
{
    printf("%ds内,输入大写字母M,进入 BootLoader命令行\r\n",Timeout);
    while(Timeout--)
    {
        Delay_ms(1000);
        if(rx1_buffer[0] == 'M')
        {
            return 1;
        }
    }
    return 0;
}

倒计时超时执行

Load_A(C8T6_A_START_ADDR);

跳转A区,否则进入bootloader命令行

BootLoader_Info();
typedef struct
{
    uint32_t OTA_Flag;
    //25q64每块真正含多少B 一一对应  其他快有其他用途 每一个值对应对应w25q64的块的大小
    uint32_t FileLen[11];//但成员0为表示OTA的大小 
    
    uint8_t OTA_ver[32];                        //OTA版本号    
    
}OTA_InfoCB;
OTA_InfoCB OTA_Info;
UpDataA_CB updata_a;




//标志位
uint32_t BootStateFlag;
/*函数功能:判断OTA RUN_a ------------------------*/
/*参数:无------------------*/
/*返回值:无----------------------------*/
void BootLoader_Brance(void)
{
    if( BootLoader_Enter(5) == 0 )
    {
        if(OTA_Info.OTA_Flag == OTA_FLAG_SET)
        {
            printf("OTA更新\r\n");
            
            BootStateFlag |= UPDATA_A_FLAG;                 //OTA更新 置BootStateFlag第0位为1
            updata_a.W25Q64_BlockNum = 0;                   //OTA事件 使用W25Q64的块0 其他块有其他作用
        }
        else
        {
            printf("跳转A分区\r\n");                        //不更新  执行a区
            Load_A(C8T6_A_START_ADDR);                     //跳转  a区
        }
    }
       printf("进入 BootLoader命令行\r\n");                 
       BootLoader_Info();
}

命令行

/*函数功能Bootloder命令行 ------------------------*/
/*参数:无------------------*/
/*返回值:无----------------------------*/
void BootLoader_Info(void)
{
    printf("(1)擦除A区\r\n");
    printf("(2)串口IAP下载A区\r\n");
    printf("(3)向外部flash下载程序\r\n");
    printf("(4)使用外部flash程序\r\n");
    printf("(5)重启\r\n");
}

当我们串口输入对应编号BootStateFlag 就会被&上对应标志,如

#define  UPDATA_A_FLAG                      0X00000001//0000 0001
…………
BootStateFlag  &= UPDATA_A_FLAG;

7.命令对应的API

/*函数功能:Bootloder命令动作  置标志位 while循环才具体实现    一切命令来自串口信息 ------------------------*/
/*参数:串口接收命令数据和长度------------------*/
/*返回值:无----------------------------*/
void BootLoader_Event(uint8_t* data,uint16_t datalen)
{
    
    //顶层命令   选择性调用后续4个函数
    BOOT_CMD_process(data,&datalen);
    
     
    //处理串口IAP事件
    IAP_process(data,datalen);
     
    
    //向外部flash块写程序
    WRITE_WB_FLAH_process(data,datalen);
    
    
    //使用外部flash程序
    READ_WB_FLAH_process(data,datalen);
}

API具体实现

void BOOT_CMD_process(uint8_t* data,uint16_t *datalen)
{
    if(BootStateFlag == 0)                                  //顶层命令 即初始化时串口最先执行的
    {
        if(*datalen == 1 && data[0] == '1')                  //命令1擦除a区
        {
            printf("擦除A区ing\r\n");
            Delay_ms(100);
            Erase_Flash(C8T6_A_START_PAGE,C8T6_A_PAGE_NUM); //擦除A区
            printf("擦除A区成功\r\n");
            BootLoader_Info();
        }
        else if(*datalen == 1 && data[0] == '2')             //命令2IAP下载
        {
            printf("通过Xmodem协议,串口IAP下载A区程序,请使用.bin文件\r\n");
            Delay_ms(100);
            Erase_Flash(C8T6_A_START_PAGE,C8T6_A_PAGE_NUM); //擦除A区
            BootStateFlag |= ( IAP_XMODE_C_FLAG | IAP_XMODE_DATA_FLAG );              //置位BootStateFlag
            updata_a.XmodemTime = 0;                        //清除发送间隔
            updata_a.XmodemDa_num = 0;                      //接收数据包数量清零
        }
        else if(*datalen == 1 && data[0] == '3')             //命令3向外部flash下载程序
        {
            printf("向外部flash下载程序,输入需要使用的块编号(1-9)\r\n");
            BootStateFlag |= BOOT_CMD_WB_FLASH_DOWN;            //置位BootStateFlag
            
        }
        else if(*datalen == 1 && data[0] == '4')             //命令4使用外部flash程序
        {
            printf("使用外部flash程序,输入需要读取的块编号(1-9)\r\n");
            BootStateFlag |= BOOT_CMD_RED_WB_FLASH;            //置位BootStateFlag
            
        }
        else if(*datalen == 1 && data[0] == '5')             //命令5重启
        {
            printf("重启ing\r\n");
            Delay_ms(100);
            printf("重启成功\r\n");
            NVIC_SystemReset();                             //重启
        }
        
        *datalen = 0;
    }
}


//串口IAP事件  写A区或者写外部FLASH
void IAP_process(uint8_t* data,uint16_t datalen)
{
    uint8_t i;
    
    if(BootStateFlag & IAP_XMODE_DATA_FLAG)
    {
        //SOH(0X01) 数据序号(0x0?,1开始)  0XFE-0XF0  数据0-127 crc16  1+1+1+128+2=133
        if( (datalen == 133) &&(data[0] ==0X01) )
        {
            BootStateFlag &= ~IAP_XMODE_C_FLAG;             //开始接收数据 关闭发送C的功能
            updata_a.XmodemCRC = Xmodem_CRC16(&data[3],128);//程序自己求crc 和发来的比对
            if(updata_a.XmodemCRC == (( data[131]<<8) | data[132]) )
            {
                //此处的减一 是因为第一个的偏移量为0 所以减1
                updata_a.XmodemDa_num++;
                memcpy(&updata_a.UpdataBuf[((updata_a.XmodemDa_num-1) % 8) *128],&data[3],128);
                
                if(updata_a.XmodemDa_num % 8 == 0) //凑满了8个包 即1KB可以写a区flash
                {
                    if(BootStateFlag & WRITE_WB_FLASH_XMODEM_FLAG)
                    {
                        //一页256B 分四次写
                        for(i = 0;i < 4;i++)
                        {
                            //updata_a.W25Q64_BlockNum * 64 * 4  块的偏移量 一块64KB
                            // 64 * 1024 /256 一页是256B
                            
                            //(updata_a.XmodemDa_num / 8 + 1) * 4 + i  xmodem收到的数据包对于页的偏移量
                            /*-------------------------------------------------------
                                 i      |    0     |    1      |    2      |    3      |
                            8包         |    0页   |    1页    |    2页    |    3 页   |
                            16包        |    4页   |    5页    |    6 页   |    7页    |
                            24包        |    8页   |    9页    |    10页   |    11页   |
                            
                            页数 = (包数 / 8 - 1) * 4 + i
                            -------------------------------------------------------------*/
                            W25Q64_Page_write(&updata_a.UpdataBuf[i*256],
                                               updata_a.W25Q64_BlockNum * (64 * 4)  +  (updata_a.XmodemDa_num / 8 - 1) * 4 + i);
                        }
                    }
                    else
                    {
                     Write_Flash( (updata_a.XmodemDa_num/(C8T6_FLASH_PAGE_SIZE / 128) - 1) * C8T6_FLASH_PAGE_SIZE + C8T6_A_START_ADDR,
                                  (uint32_t *)updata_a.UpdataBuf,
                                   C8T6_FLASH_PAGE_SIZE);
                    }
                }
                
                printf("\x06");                             //发送ACK
            }
            else
            {
                printf("\x15");                             //发送NACK
            }
        }
    }
        
        //EOT 结束信号 0x04
    if( (datalen == 1) &&(data[0] ==0X04) )
    {
        printf("\x06");                             //发送ACK
        if(updata_a.XmodemDa_num % (C8T6_FLASH_PAGE_SIZE / 128) != 0) //有剩余未满八个包的
        {
            if(BootStateFlag & WRITE_WB_FLASH_XMODEM_FLAG)
            {
                for(i = 0;i < 4;i++)
                {
                    //此处不减一是因为 这是对于已经写好了的下一篇了
                    W25Q64_Page_write(&updata_a.UpdataBuf[i*256],
                                       updata_a.W25Q64_BlockNum * 64 * 4  +  (updata_a.XmodemDa_num / 8) * 4 + i);
                }
            }
            else
            {
                //此处不减一是因为 这是对于已经写好了的下一篇了
                 Write_Flash( (updata_a.XmodemDa_num/(C8T6_FLASH_PAGE_SIZE / 128)) * C8T6_FLASH_PAGE_SIZE + C8T6_A_START_ADDR,
                              (uint32_t *)updata_a.UpdataBuf,
                              (updata_a.XmodemDa_num % (C8T6_FLASH_PAGE_SIZE / 128) *128));
            }
            BootStateFlag &= ~IAP_XMODE_DATA_FLAG;             //关闭接收数据功能
            
            if(BootStateFlag & WRITE_WB_FLASH_XMODEM_FLAG)
            {
                BootStateFlag &= ~ WRITE_WB_FLASH_XMODEM_FLAG;
                OTA_Info.FileLen[updata_a.W25Q64_BlockNum] = updata_a.XmodemDa_num * 128;
                AT24C02_WriteOTAInfo();
                Delay_ms(100);
                BootLoader_Info();
            }
            else
            {
                Delay_ms(100);
                NVIC_SystemReset();                                     //重启
            }
        }
    }
}


//使能向外部FLASH写 
void WRITE_WB_FLAH_process(uint8_t* data,uint16_t datalen)
{
    if(BootStateFlag & BOOT_CMD_WB_FLASH_DOWN)
    {
        if(datalen != 1) return;
        if(datalen == 1)
        {
            //0X31=='1'  0X39=='9'
            if(data[0] >= 0X31 && data[0] <= 0X39)
            {
                updata_a.W25Q64_BlockNum = data[0] - 0X30;
                
                BootStateFlag |= WRITE_WB_FLASH_XMODEM_FLAG | IAP_XMODE_C_FLAG | IAP_XMODE_DATA_FLAG;
                updata_a.XmodemTime = 0;                        //清除发送间隔
                updata_a.XmodemDa_num = 0;                      //接收数据包数量清零
                OTA_Info.FileLen[updata_a.W25Q64_BlockNum] = 0;
                W25Q64_64K_ERASE(updata_a.W25Q64_BlockNum);
                printf("通过Xmodem协议,向外部flash第%d个块下载a区程序,请使用.bin文件\r\n",updata_a.W25Q64_BlockNum);
                BootStateFlag &= ~BOOT_CMD_WB_FLASH_DOWN;
            }
            else
            {
                printf("块编号数据输入错误\r\n");
            }
        }
        else
        {
            printf("块编号长度输入错误\r\n");
        }
    }
}



void READ_WB_FLAH_process(uint8_t* data,uint16_t datalen)
{
    if(BootStateFlag & BOOT_CMD_RED_WB_FLASH)
    {
        if(datalen != 1) return;
        if(datalen == 1)
        {
            //0X31=='1'  0X39=='9'
            if(data[0] >= 0X31 && data[0] <= 0X39)
            {
                updata_a.W25Q64_BlockNum = data[0] - 0X30;
                
                BootStateFlag |= UPDATA_A_FLAG;
                
                BootStateFlag &= ~BOOT_CMD_RED_WB_FLASH;
            }
            else
            {
                printf("块编号数据输入错误\r\n");
            }
        }
        else
        {
            printf("块编号长度输入错误\r\n");
        }
    }
}



void UPDATA_A_process(void)
{
    uint8_t i;
    
    if(BootStateFlag & UPDATA_A_FLAG)               //判断BootStateFlag 第一位置1否? 即判断OTA
    {
        //更新a区
        printf("长度%d字节\r\n",OTA_Info.FileLen[updata_a.W25Q64_BlockNum]);
        if(OTA_Info.FileLen[updata_a.W25Q64_BlockNum] % 4 == 0)             //Flash设置好了只能写4个字节 强制写会进hardfalut
        {
            Erase_Flash(C8T6_A_START_PAGE,C8T6_A_PAGE_NUM);                 //先擦除 擦除a区
            
            for(i = 0;i<OTA_Info.FileLen[updata_a.W25Q64_BlockNum] / C8T6_FLASH_PAGE_SIZE;i++)
            {
                //一个block写 i*1024 写64次满一个block 64k
                //updata_a.W25Q64_BlockNum*64*1024 是多块bolck偏移
                W25Q64_Readdata(updata_a.UpdataBuf,i*1024 + updata_a.W25Q64_BlockNum*64*1024,C8T6_FLASH_PAGE_SIZE);     //读取整页 1k
                
                
                //i * C8T6_FLASH_PAGE_SIZE + C8T6_A_START_ADDR      起始地址加偏移
                Write_Flash(i * C8T6_FLASH_PAGE_SIZE + C8T6_A_START_ADDR,(uint32_t *)updata_a.UpdataBuf,C8T6_FLASH_PAGE_SIZE);          //写入内部flash
            }
            
            
            //此处i是上面退出的 是加一了的 直接使用
            if(OTA_Info.FileLen[updata_a.W25Q64_BlockNum] % 1024 != 0)      //mod1k之后剩余不满1k的读取
            {
                W25Q64_Readdata(updata_a.UpdataBuf,i*1024 + updata_a.W25Q64_BlockNum*64*1024,OTA_Info.FileLen[updata_a.W25Q64_BlockNum] % 1024);
                
                //***************************************************************************
                Write_Flash(i * C8T6_FLASH_PAGE_SIZE + C8T6_A_START_ADDR,(uint32_t *)updata_a.UpdataBuf,OTA_Info.FileLen[updata_a.W25Q64_BlockNum] % 1024);//写入内部flash
                //***************************************************************************
            }
            
            if(updata_a.W25Q64_BlockNum == 0)                       //是块0 说明是OTA事件
            {
                OTA_Info.OTA_Flag = 0;                              //OTA完毕 更改标志
                
                AT24C02_WriteOTAInfo();                             //更改24c02OTA相关信息
            }
            printf("A区更新完毕\r\n");
            NVIC_SystemReset();
        }
        else
        {
            printf("长度错误\r\n");
            BootStateFlag &= ~UPDATA_A_FLAG;                        //退出 清楚标志位
        }
    }
}

上述API就是实现写norlash,读norflash,擦除内部flash,写内部flash的APP区全过程。
代码逻辑并不难,不再讲解代码。

但要注意每次进入其中一个API,要对BootStateFlag清除标志位,如

BootStateFlag &= ~IAP_XMODE_C_FLAG;             //开始接收数据 关闭发送C的功能

以及对于写入对于多余不满足一页的数据,为了保障norflash耐用性与内部flash写入规则,应该单独再一次写入

8.跳转A区,执行应用程序

我们已经将我们的新固件给下载到了A区,那该如何启动呢?

我们定义一个函数指针(其实可以定义在结构体的内部,来实现面对对象的思想,类似于回调函数)

typedef void (*Load_A_Point)(void);                     //函数指针

让这个函数指针指向我们要执行的地址,或者说代码 然后调用此函数指针,跳转执行

//Load_a 指向了复位向量指针
        load_ap = (Load_A_Point)*(uint32_t*)(addr + 4);                         //设置pc addr要为0X00000004
        load_ap();                                                              //取地址指向的函数

跳转A区API

/*函数功能:设置pc  即跳转 ------------------------*/
/*参数:pc指向的目标------------------*/
/*返回值:无----------------------------*/
void Load_A(uint32_t addr)
{
    if( (*(uint32_t*)addr >= 0x20000000) && (*(uint32_t*)addr <= 0x20004FFF) )//判断设置sp是否合理 即是否在堆栈
    {
        MSR_SP(*(uint32_t*)addr);
        
        
        //Load_a 指向了复位向量指针
        load_ap = (Load_A_Point)*(uint32_t*)(addr + 4);                         //设置pc addr要为0X00000004
        load_ap();                                                              //取地址指向的函数
    }
    else
    {
        printf("跳转A分区失败\r\n");
    }
}

上述代码为什么会有这个判断呢?

if( (*(uint32_t*)addr >= 0x20000000) && (*(uint32_t*)addr <= 0x20004FFF) )//判断设置sp是否合理 即是否在堆栈

9.MDK生成.bin文件

使用指令

D:\keli5\ARM\ARMCC\bin\fromelf.exe --bin --output  .\Objects\project.bin  .\Objects\project.axf

根据.axf文件编译出.bin文件,注意换成自己的文件路径。

A区不也是一个完整的工程吗,那为什么我没看见启动过程中的中断向量表和堆栈设置
其实,我们是对他进行了一个偏移
在这里插入图片描述

在这里插入图片描述
首先是ROM程序存储区也就是内部flash 偏移了0X00005000
对应A区起始位置(0X5000 = 20480 = 20k)

然后就是中断向量表的设置,当然也包括咱们的栈顶指针设置。
system_stm32f0x.c找到如下语句

/*!< Uncomment the following line if you need to relocate your vector Table in
     Internal SRAM. */ 
/* #define VECT_TAB_SRAM */
#define VECT_TAB_OFFSET  0x0 /*!< Vector Table base offset field. 
                                  This value must be a multiple of 0x200. */

此处就是设置中断向量表起始位置,只不过是偏移量,将其改为如下数值:

#define VECT_TAB_OFFSET  (uint32_t)0X5000 /*!< Vector Table base offset field. 
                                  This value must be a multiple of 0x200. */

也就正对应与20k
设置了中断向量表偏移,在前面知道中断向量表前4B为栈顶指针,
而栈顶在Bootloader里面是SRAM0X2000 0000也就对应着跳转A区时的验证了,即验证栈的位置。

if( (*(uint32_t*)addr >= 0x20000000) && (*(uint32_t*)addr <= 0x20004FFF) )//判断设置sp是否合理 即是否在堆栈

点击编译就会看到生成了bin文件了:
在这里插入图片描述

至此 IAP升级也算是完成了,至于OTA,我们可以构建单纯的MQTT报文,也可以使用WIFI模块如ESP8266的透传模式,至于双AB区无感OTA升级,有兴趣的可在此基础上拓展。

评论 25
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BaiZer.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值