1. 1. Bootloader+IAP
    1. 1.1. 简要说明
      1. 1.1.1. 说明
    2. 1.2. 整体流程图
    3. 1.3. 分区
    4. 1.4. 方案
    5. 1.5. 编程
      1. 1.5.1. 串口与DMA
        1. 1.5.1.0.1. .h程序
      2. 1.5.1.1. 配置串口和DMA
      3. 1.5.1.2. 串口空闲中断函数
      4. 1.5.1.3. 编写新的Printf函数
    6. 1.5.2. I2C
      1. 1.5.2.1. .h程序
      2. 1.5.2.2. Delay
      3. 1.5.2.3. 初始化、START和STOP信号
      4. 1.5.2.4. 发送一个字节
      5. 1.5.2.5. 读取一个字节
      6. 1.5.2.6. 等待从机ACK
    7. 1.5.3. AT24C02
      1. 1.5.3.1. 写入
        1. 1.5.3.1.1. Byte Write
        2. 1.5.3.1.2. Page Write
      2. 1.5.3.2. 读取
    8. 1.5.4. SPI
      1. 1.5.4.1. 数据收发
    9. 1.5.5. W25Q64
      1. 1.5.5.1. 状态寄存器
      2. 1.5.5.2. 指令表
      3. 1.5.5.3. 相关程序
      4. 1.5.5.4. 擦除64KB
      5. 1.5.5.5. 页写入256Byte
      6. 1.5.5.6. 读取数据
    10. 1.5.6. STM32的FLASH
      1. 1.5.6.1. 擦除Flash
      2. 1.5.6.2. 写入Flash
  2. 1.6. Bootloader功能实现
    1. 1.6.1. AB分区规划
    2. 1.6.2. 注意点
    3. 1.6.3. OTA宏定义
    4. 1.6.4. OTA读取标志位
    5. 1.6.5. OTA保存关键变量到AT24C02
    6. 1.6.6. 引导更新OTA
    7. 1.6.7. BootLoader事件
  3. 1.7. 主程序
  • 2. Xmodem协议
    1. 2.1. 格式
    2. 2.2. 指令
    3. 2.3. 例子
    4. 2.4. CRC16程序
  • 3. 参考
  • 4. 补充(7.24)
    1. 4.1. Bootloader执行流程
    2. 4.2. SRAM在此的意义
    3. 4.3. APP程序配置
    4. 4.4. DMA如何配置RX_BUFFER + 1配合IDLE实现接收不定长数据
  • Bootloader+IAP

    简要说明

    本学习笔记主要包括STM32F103C8T6下的Bootloader+IAP

    Bootloader:用于更新APP的程序

    IAP:在设备运行时,由Bootloader引导对自身程序擦写

    OTA:无线通信将新的固件下载到设备中(Over The Air)

    说明

    使用的APP数据,OTA标志位,版本号存储在AT24C02

    多个备份APP数据存储在W25Q64

    Q:已经操作了AT24C02和W25Q64,为什么还要操作单片机上的SRAM?

    AT24C02和Flash读写太慢,跟不上CPU的速度,SRAM是临时、高速的数据交换区

    整体流程图

    这里说明下没有Running APP那一环,Visio源文件找不到了

    分区

    A区存放APP,B区存放Bootloader程序。OTA_Flag表示是否更新APP,存放在AT24C02中

    1. ❌️ | A | B | ------>程序运行后,先进入A区;若OTA_Flag=1开始更新数据,当正在更新的A区出现异常退出,下一次上电后运行A发现程序不全,无法再进入B区进行后续更新A区的操作,并且之前的A区程序有出现问题,悲报,成砖头了

    2. ✔️ | B | A | ------>程序运行后,先进入B区,观测到OTA_Flag=1后对A区进行数据更新,即使异常退出,后续仍然可以进入B区对A区进行更新,更新完成后置OTA_Flag=0。

    方案

    🦝DMA + 空闲中断

    在无需CPU帮助下,DMA负责外设与寄存器之间数据传输

    空闲中断(IDLE)是串口通信中判断“接收完成”的经典方式

    Q:如何消除空闲中断?

    读一次标志位寄存器,再读一次数据寄存器,即可消除空闲中断

    环形缓冲区数据传输过程中,接收速度和处理速度可能不一致,因此需要缓冲区先保存数据,防止数据丢失;一维数组,确认单次接收最大量,防止出现越界问题

    Q:如何判断Read和Write的状态问题?

    1. 预留一个空位。Read == Write,即空;(Write + 1)% N == R,即满;

    2. 通过一个count计数,W是生产者,R是消费者;每次W就count+1,每次R就count-1;0<=count<=Max_Count。

    SE指针对(环形缓冲):缓冲区是一维数组,总长度为 2048 Byte,划分为10个数据块,每个数据块200 Byte。通过两个指针 IN(生产者指针)和 OUT(消费者指针)管理缓冲区的读写操作。当 IN ≠ OUT 时,表示缓冲区中存在待处理的数据包。指针每次移动一个数据块(200字节),当指针到达缓冲区末尾时自动回卷到起始位置,实现环形缓冲机制。

    ❗TIP:每次接收后,都需要判断剩余空间,以防内存不足,及时回卷;需要记录已经存放的累加值

    编程

    STM32F103C8T6使用外部高速时钟HSE,8MHz;通过PLL(倍频锁相环)可以达到72MHz

    串口与DMA

    .h程序
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    // 缓冲区大小定义
    #define URx_SIZE 2048 // 接收缓冲区总大小
    #define UTx_SIZE 2048 // 发送缓冲区大小
    #define URx_MAX 200 // 单次DMA接收最大字节数

    #define NUM 10 // 接收数据包队列深度

    uint8_t URx_Buff[URx_SIZE];
    uint8_t UTx_Buff[UTx_SIZE];

    // 某一组数据接收开始与结束, start:开始, end:结尾
    typedef struct
    {
    uint8_t *start; // 数据包起始地址
    uint8_t *end; // 数据包结束地址
    }UCB_URxBuff;

    /* 管理串口接收数据
    URxCounter 当前缓冲区中未处理的数据量
    URxDataPtr 缓冲区存储数组序号
    URxDataIN 指向下一组可写入数据的位置
    URxDataOUT 指向下一个待读取数据的位置
    URxDataEND 指向数组末尾 */
    typedef struct
    {
    uint16_t URxCounter; // 接收计数器,记录当前接收位置
    UCB_URxBuff URxDataPtr[NUM]; // 数据包指针数组(环形队列)
    UCB_URxBuff *URxDataIN; // 输入指针(生产者)
    UCB_URxBuff *URxDataOUT; // 输出指针(消费者)
    UCB_URxBuff *URxDataEND; // 队列末尾指针
    }UCB_CB;

    extern UCB_CB UCB;
    extern uint8_t URx_Buff[URx_SIZE];

    配置串口和DMA

    1. 设置相关外设时钟
    2. 初始化GPIO(IO分区,MODE,频率,IO口)
    3. 配置串口(初始化,波特率,校验,数据位长度,停止位个数,接收发情况)
    4. 配置串口DMA
    5. 打开串口中断(IDLE为空闲中断)
    6. 调用其他功能函数
    7. 使能串口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    void RCC_USART_Init(void)
    {
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 配置DMA
    }

    void GPIO_USART_Init(void)
    {
    GPIO_InitTypeDef GPIO_InitStructure;// 配置PA9为TX(复用推挽输出)
    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_IN_FLOATING;// 配置PA10为RX(浮空输入)
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    }

    void NVIC_USART_Init(void)
    {
    // 配置中断
    NVIC_SetPriorityGrouping(NVIC_PriorityGroup_2);

    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_Init(&NVIC_InitStructure);
    }

    void DMA_USART_Init(void)
    {
    DMA_InitTypeDef DMA_InitStructure;
    //URx_Buff长度为URx_MAX,此处+1是为了配合IDLE实现中断
    DMA_InitStructure.DMA_BufferSize = URx_MAX + 1;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)URx_Buff;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;
    DMA_Init(DMA1_Channel5, &DMA_InitStructure);

    DMA_Cmd(DMA1_Channel5, ENABLE);
    }
    // USART1初始化
    void USART1_Init(uint32_t band)
    {
    // 使能时钟
    RCC_USART_Init();
    GPIO_USART_Init();

    // 配置USART1参数
    USART_InitTypeDef USART_InitStructure;
    USART_InitStructure.USART_BaudRate = band;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
    USART_InitStructure.USART_Parity = USART_Parity_No;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_Init(USART1, &USART_InitStructure);

    USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
    USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);

    NVIC_USART_Init();
    DMA_USART_Init();

    // 初始化串口控制块
    UCB.URxCounter = 0;
    UCB.URxDataIN = &UCB.URxDataPtr[0];
    UCB.URxDataOUT = &UCB.URxDataPtr[0];
    UCB.URxDataEND = &UCB.URxDataPtr[NUM - 1];
    UCB.URxDataIN->start = URx_Buff;

    USART_Cmd(USART1, ENABLE);
    }

    串口空闲中断函数

    1. 检测空闲中断
    2. 消除空闲中断标志位(只有读标志位寄存器和数据位寄存器才能将空闲标志位清除)
    3. 在空闲中断中读取串口DMA增添数据的长度(DMA,通道) — counter += (总量 - 剩余空闲值)
    4. 设置缓冲区的IN指针
    5. Disable DMA,重新配置DMA(DMA,通道,大小,地址),保证不出现完成状态,再使能DMA

    要一直让DMA读取数据,直到出现空闲中断才会停止,因此不会出现完成状态

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    /**
    * @brief USART1中断处理函数
    * @param 无
    * @return 无
    *
    * 处理USART1的IDLE(空闲)中断:
    * 1. 检测到串口空闲时,表示一个数据包接收完成
    * 2. 计算实际接收的数据长度
    * 3. 更新环形缓冲区指针
    * 4. 重新配置DMA为下一次接收做准备
    *
    * 工作流程:
    * - IDLE中断触发 -> 数据包接收完成
    * - 计算接收长度 -> 更新数据包结束指针
    * - 移动输入指针 -> 处理环形缓冲区绕回
    * - 重启DMA接收 -> 准备接收下一个数据包
    */
    void USART1_IRQHandler(void)
    {
    // 检查IDLE中断标志位
    if(USART_GetFlagStatus(USART1, USART_FLAG_IDLE) != 0)
    {
    // 清除IDLE中断标志位
    USART_ClearFlag(USART1, USART_FLAG_IDLE);
    // 读取数据寄存器以清除IDLE标志(必须操作)
    USART_ReceiveData(USART1);

    // 计算本次接收的数据长度并更新接收计数器
    UCB.URxCounter += (URx_MAX + 1) - DMA_GetCurrDataCounter(DMA1_Channel5);
    // 设置当前数据包的结束地址
    UCB.URxDataIN->end = &URx_Buff[UCB.URxCounter - 1];

    // 移动输入指针到下一个数据包位置
    UCB.URxDataIN++;
    // 检查是否到达队列末尾,进行环形处理
    if(UCB.URxDataIN == UCB.URxDataEND)
    {
    UCB.URxDataIN = &UCB.URxDataPtr[0]; // 回到队列开头
    }

    // 检查剩余缓冲区空间,决定下一次DMA接收的起始地址
    if(URx_SIZE - UCB.URxCounter >= URx_MAX)
    {
    // 剩余空间足够,继续在当前位置接收
    UCB.URxDataIN->start = &URx_Buff[UCB.URxCounter];
    }
    else
    {
    // 剩余空间不足,回到缓冲区开头
    UCB.URxDataIN->start = URx_Buff;
    UCB.URxCounter = 0; // 重置接收计数器
    }

    // 重新配置DMA为下一次接收
    DMA_Cmd(DMA1_Channel5, DISABLE); // 禁用DMA
    DMA_SetCurrDataCounter(DMA1_Channel5, URx_MAX + 1); // 设置传输计数
    DMA1_Channel5->CMAR = (uint32_t)UCB.URxDataIN->start; // 设置内存地址
    DMA_Cmd(DMA1_Channel5, ENABLE); // 重新启用DMA
    }
    }

    编写新的Printf函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 格式化打印函数
    void uprintf(char *format, ...)
    {
    va_list list_data;
    va_start(list_data, format);

    vsnprintf((char *)UTx_Buff, sizeof(UTx_Buff), format, list_data);
    va_end(list_data);

    uint16_t len = strlen((const char *)UTx_Buff);

    for (uint16_t i = 0; i < len; i++)
    {
    while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) != 1);
    USART_SendData(USART1, UTx_Buff[i]);
    }

    while (USART_GetFlagStatus(USART1, USART_FLAG_TC) != 1);
    }

    I2C

    软件I2C,延时部分需要自己重新设计

    .h程序

    1
    2
    3
    4
    5
    6
    7
    #define SCL_H GPIO_SetBits(GPIOB, GPIO_Pin_3)
    #define SCL_L GPIO_ResetBits(GPIOB, GPIO_Pin_3)

    #define SDA_H GPIO_SetBits(GPIOB, GPIO_Pin_4)
    #define SDA_L GPIO_ResetBits(GPIOB, GPIO_Pin_4)

    #define Read_SDA GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_4)

    Delay

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    // 延时功能初始化
    void delay_init(void)
    {
    SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK);
    }

    // 微秒级延时
    void delay_us(uint16_t us)
    {
    SysTick_Config(SystemCoreClock / 1000000);

    while(us--)
    {
    while(!((SysTick->CTRL) & (SysTick_CTRL_COUNTFLAG)));
    }

    SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
    }

    // 毫秒级延时
    void delay_ms(uint16_t ms)
    {
    SysTick_Config(SystemCoreClock / 1000);

    while(ms--)
    {
    while(!((SysTick->CTRL) & (SysTick_CTRL_COUNTFLAG)));
    }

    SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
    }

    初始化、START和STOP信号

    1. 硬件I2C,开启时钟

    2. 配置GPIO(PB6,PB7,开漏模式)

    3. 配置SCL和SDA两条线

    起始信号:SCL高电平,SDA从高电平变为低电平

    停止信号:SCL高电平,SDA从低电平变为高电平

    数据信号:SCL高电平,SDA保持不变;SCL低电平,SDA电平可以修改

    应答信号:SCL高电平,SDA低电平应答,否则,非应答

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    void I2C1_Init(void)
    {
    // 使能GPIOB时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

    GPIO_InitTypeDef GPIO_InitStructure;

    // 配置SCL引脚(PB3)为开漏输出
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏输出模式
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    // 配置SDA引脚(PB4)为开漏输出
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏输出模式
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    // 设置总线初始状态为高电平(空闲状态)
    SCL_H;
    SDA_H;
    }
    /* Start信号 */
    // 一旦 Start 产生后,数据传输必须在 SCL 为低时开始。
    void I2C_Start(void)
    {
    SCL_H; // 确保SCL为高电平
    SDA_H; // 确保SDA为高电平
    delay_us(2); // 延时等待信号稳定
    SDA_L; // SDA变为低电平(起始条件)
    SCL_L; // SCL变为低电平,准备发送数据
    }
    /* End信号 */
    void I2C_Stop(void)
    {
    SCL_L; // 确保SCL为低电平
    SDA_L; // 确保SDA为低电平
    delay_us(2); // 延时等待信号稳定
    SCL_H; // SCL变为高电平
    SDA_H; // SDA变为高电平(停止条件)
    }

    发送一个字节

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    void I2C_SendByte(uint8_t tx)
    {
    // 从最高位开始发送(MSB first)
    for(int8_t i = 7; i >= 0; i--)
    {
    SCL_L; // SCL置低,准备设置数据
    if(tx & (1 << i)) // 检查当前位是否为1
    {
    SDA_H; // 发送逻辑1
    }
    else
    {
    SDA_L; // 发送逻辑0
    }
    delay_us(2); // 延时等待数据稳定
    SCL_H; // SCL置高,让从机读取数据
    delay_us(2); // 延时保持时钟高电平
    }
    SCL_L; // 发送完成,SCL置低
    SDA_H; // 释放SDA线,准备接收ACK
    }

    读取一个字节

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    uint8_t I2C_ReadByte(uint8_t ack)
    {
    uint8_t rx = 0;
    // 从最高位开始接收(MSB first)
    for(int8_t i = 7; i >= 0; i--)
    {
    SCL_L; // SCL置低,准备读取数据
    delay_us(2);
    SCL_H; // SCL置高,读取数据
    if(Read_SDA) // 读取SDA上的数据
    {
    rx |= (1 << i); // 如果SDA为高,设置对应位
    }
    delay_us(2);
    }
    SCL_L; // 数据接收完成,SCL置低
    // 发送应答或非应答
    if(ack)
    {
    SDA_L; // 发送ACK(应答)
    SCL_H;
    delay_us(2);
    SCL_L;
    SDA_H; // 释放SDA线
    }
    else
    {
    SDA_H;
    SCL_H;
    delay_us(2);
    SCL_L;
    }
    return rx;
    }

    等待从机ACK

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    uint8_t I2C_Wait_ACK(int16_t timeout)
    {
    // 等待SDA变为低电平(从机应答)
    do
    {
    timeout--;
    delay_us(2);
    }while(Read_SDA && timeout >= 0);

    if(timeout < 0)
    {
    return 1; // 等待超时
    }

    SCL_H; // SCL置高,读取应答信号
    delay_us(2);
    if(Read_SDA != 0)
    {
    return 2; // 应答信号无效
    }
    SCL_L; // SCL置低,应答读取完成
    delay_us(2);
    return 0; // 成功接收应答
    }

    AT24C02

    I2C通信,设备地址:1 0 1 0 E2 E1 E0 R/非W;

    读R=1,0XA1;

    写W=0,0XA0;

    按字节写入

    1
    2
    #define AT24C02_WADDR 0xA0
    #define AT24C02_RADDR 0xA1

    AT24C02共32页一页8字节,共256字节;存在回卷问题

    写入

    Byte Write
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // addr:写入的EEPROM内存地址
    // wdata:写入的数据
    uint8_t AT24C02_WriteByte(uint8_t addr, uint8_t wdata)
    {
    I2C_Start();
    I2C_SendByte(AT24C02_WADDR); // 发送写入信号
    if (I2C_Wait_ACK(100) != 0) // 100为等待应答的信号
    return 1; // 芯片无应答

    I2C_SendByte(addr);
    if (I2C_Wait_ACK(100) != 0)
    return 2; // 写入地址失败

    I2C_SendByte(wdata);
    if (I2C_Wait_ACK(100) != 0)
    return 2; // 写入数据失败
    I2C_Stop();
    return 0; // 成功
    }
    Page Write
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // addr:写入地址
    // wdata:写入数据
    uint8_t AT24C02_WritePage(uint8_t addr, uint8_t *wdata)
    {
    I2C_Start();
    I2C_SendByte(AT24C02_WADDR); // 发送写入信号
    if (I2C_Wait_ACK(100) != 0)
    return 1; // 芯片无应答

    I2C_SendByte(addr);
    if (I2C_Wait_ACK(100) != 0)
    return 2; // 写入地址失败

    for (uint8_t i = 0; i < 8; i++)
    {
    I2C_SendByte(wdata[i]);
    if (I2C_Wait_ACK(100) != 0)
    return 3 + i; // 写入数据失败
    }
    I2C_Stop();
    return 0; // 成功
    }

    读取

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // addr:读取的起始地址
    // rdata:指向存储读取数据的缓冲区的指针
    // datalen:读取数据长度
    uint8_t AT24C02_ReadData(uint8_t addr, uint8_t *rdata, uint16_t datalen)
    {
    I2C_Start();
    I2C_SendByte(AT24C02_WADDR); // 发送写入信号
    if (I2C_Wait_ACK(100) != 0)
    return 1; // 芯片无应答

    I2C_SendByte(addr); // 写入读取地址
    if (I2C_Wait_ACK(100) != 0)
    return 2; // 写入读取地址失败

    I2C_Start();
    I2C_SendByte(AT24C02_RADDR); // 发送读取信号
    if (I2C_Wait_ACK(100) != 0)
    return 3; // 切换读模式时芯片无应答
    for (uint8_t i = 0; i < datalen - 1; i++)
    {
    rdata[i] = I2C_ReadByte(1);
    }
    rdata[datalen - 1] = I2C_ReadByte(0); // 读取最后一个字节时不发送ACK信号
    I2C_Stop();
    return 0; // 成功
    }

    SPI

    外部FLASH型号为W25Q64,使用硬件SPI

    1. 配置SPI0时钟,相关GPIO开启

    2. 复位SPI外设

    3. 配置SPI结构体成员(主从模式,发送类型(全双工),一帧大小,硬件/软件,大端/小端(大端),工作方式(极性(上下)/相位(一二),从机决定),传输速度)

    4. 初始化SPI

    5. 使能SPI

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    // 初始化SPI1
    void SPI1_Init(void)
    {
    // 使能GPIOA和SPI1时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE);

    GPIO_InitTypeDef GPIO_StructInit;
    GPIO_StructInit.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; // 配置SCK和MOSI引脚(PA5,PA7)
    GPIO_StructInit.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出
    GPIO_StructInit.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_StructInit);

    GPIO_StructInit.GPIO_Pin = GPIO_Pin_6; // 配置MISO引脚(PA6)
    GPIO_StructInit.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入
    GPIO_StructInit.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_StructInit);

    SPI_I2S_DeInit(SPI1); // 复位SPI1外设

    SPI_InitTypeDef SPI_InitStructure; // 配置SPI参数
    /* SPI初始化结构体配置 */
    // SPI工作模式:主模式(控制时钟信号)
    SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
    // SPI通信方向:双线全双工(同时发送和接收)
    SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
    // 数据帧格式:8位数据
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
    // 时钟极性:高电平空闲(CPOL=1)
    SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
    // 时钟相位:第二个边沿采样(CPHA=1)
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;
    // 片选控制:软件NSS管理(手动控制片选)
    SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
    // 波特率预分频:系统时钟2分频(fPCLK/2)
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;
    // 数据传输顺序:高位先发送(MSB first)
    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
    // CRC多项式:7,大部分SPI设备用不到,随便给个值
    SPI_InitStructure.SPI_CRCPolynomial = 7;
    SPI_Init(SPI1, &SPI_InitStructure);

    // 使能SPI1
    SPI_Cmd(SPI1, ENABLE);
    }

    数据收发

    SPI_I2S_FLAG_TXE表示发送区空了SPI_SPI_FLAG_RXNE表示接收区不为空。切记全双工,有发就有收!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // SPI单字节读写,tx是要发送的数据字节
    uint16_t SPI1_ReadWrite_Byte(uint16_t tx)
    {
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != 1); // 等待发送缓冲区空
    SPI_I2S_SendData(SPI1, tx); // 发送数据
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != 1);// 等待接收缓冲区非空
    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_ReadWrite_Byte(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_ReadWrite_Byte(0xff); // 发送dummy数据(0xFF)来读取,想想Flash清零
    }
    }

    W25Q64

    状态寄存器

    主要看S0 — BUSY

    指令表

    相关程序

    SPI+Flash需等待Busy状态,先读取状态寄存器地址,后随便写入其它(此时写入什么都不重要,主要获取寄存器中的数值),打开片选,使能Write再关闭

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    #define CS_ENABLE GPIO_ResetBits(GPIOA, GPIO_Pin_4)
    #define CS_DISABLE GPIO_SetBits(GPIOA, GPIO_Pin_4)

    // W25Q64初始化
    void W25Q64_Init(void)
    {
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    GPIO_InitTypeDef GPIO_StructInit;
    GPIO_StructInit.GPIO_Pin = GPIO_Pin_4; // CS片选引脚(低电平有效)
    GPIO_StructInit.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出模式
    GPIO_StructInit.GPIO_Speed = GPIO_Speed_50MHz; // 50MHz速度
    GPIO_Init(GPIOA, &GPIO_StructInit);
    CS_DISABLE; // 默认禁用片选
    SPI1_Init(); // 初始化SPI1
    }
    // 等待芯片空闲
    void W25Q64_WaitBusy(void)
    {
    uint8_t res;
    do
    {
    CS_ENABLE; // 使能片选
    SPI1_ReadWrite_Byte(0x05); // 发送读取状态寄存器命令
    res = SPI1_ReadWrite_Byte(0xff); // 读取状态寄存器值
    CS_DISABLE; // 禁用片选
    } while ((res & 0x01) == 0x01); // 检查BUSY位(bit0)
    }
    // 使能写操作
    void W25Q64_Enable(void)
    {
    W25Q64_WaitBusy(); // 等待空闲
    CS_ENABLE;
    SPI1_ReadWrite_Byte(0x06); // 发送写使能指令
    CS_DISABLE;
    }

    擦除64KB

    W25Q64总共8MB = 8 * 1024KB,一次擦除64KB,可得共计8 * 1024 / 64 = 128个Block

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 擦除64KB的块,block: 要擦除的块编号
    void W25Q64_Erase_64k(uint8_t block)
    {
    uint8_t wdata[4];
    wdata[0] = 0xD8; // 块擦除指令
    wdata[1] = (block * 64 * 1024) >> 16; // 计算块起始地址(高字节)
    wdata[2] = (block * 64 * 1024) >> 8; // 中字节
    wdata[3] = (block * 64 * 1024) >> 0; // 低字节
    W25Q64_WaitBusy(); // 等待BUSY
    W25Q64_Enable(); // 使能写操作
    CS_ENABLE;
    SPI1_Write(wdata, 4); // 发送擦除指令和地址
    CS_DISABLE;
    W25Q64_WaitBusy(); // 等待擦除完成
    }

    页写入256Byte

    页地址 = page * 256 Byte,每次从页地址开始写入256 Byte,页地址总长24位

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 按页写入数据
    // wbuff: 待写入数据的缓冲区指针
    // page: 目标页号
    void W25Q64_PageWrite(uint8_t *wbuff, uint16_t page)
    {
    uint8_t wdata[4];
    wdata[0] = 0x02; // 页写指令
    wdata[1] = (page * 256) >> 16; // 计算页起始地址(高字节)
    wdata[2] = (page * 256) >> 8; // 中字节
    wdata[3] = (page * 256) >> 0; // 低字节
    W25Q64_WaitBusy();
    W25Q64_Enable(); // 使能写操作
    CS_ENABLE;
    SPI1_Write(wdata, 4); // 发送写指令和地址
    SPI1_Write(wbuff, 256); // 写入256字节数据
    CS_DISABLE;
    }

    读取数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 读取数据
    // rbuff: 数据读取缓冲区指针
    // addr: 起始地址
    // datalen: 要读取的字节数
    void W25Q64_Read(uint8_t *rbuff, uint32_t addr, uint32_t datalen) // 地址只用24位
    {
    W25Q64_WaitBusy();
    CS_ENABLE;
    SPI1_ReadWrite_Byte(0x03);
    SPI1_ReadWrite_Byte((uint16_t)(addr >> 16));
    SPI1_ReadWrite_Byte((uint16_t)(addr >> 8));
    SPI1_ReadWrite_Byte((uint16_t)(addr >> 0));
    SPI1_Read(rbuff, datalen);
    CS_DISABLE;
    }

    STM32的FLASH

    Flash擦除后为0xFF

    擦除Flash

    Flash一页1KB,下面函数实现一次擦除num页数据

    流程

    1. Flash开锁

    2. 确认地址,擦除地址数据

    3. Flash锁定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // start	起始页号
    // num 需要擦除的页数
    void FLASH_Erase(uint16_t start, uint16_t num)
    {
    uint16_t i;
    FLASH_Unlock(); // 解锁FLASH操作
    for (i = 0; i < num; i++)
    {
    // 计算目标页地址:FLASH起始地址 + 起始页号 * 页大小 + 当前页偏移
    FLASH_ErasePage((FLASH_SADDR + start * 1024) + (1024 * i));
    }
    FLASH_Lock(); // 锁定FLASH
    }

    写入Flash

    一次写入num个4字节数据,地址自动递增

    流程

    1. Flash开锁

    2. 将数据写入对应地址

    3. Flash锁定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //	saddr	目标起始地址(必须为4的倍数)
    // wdata 待写入数据的指针
    // wnum 需要写入的字节数
    void FLASH_Write(uint32_t saddr, uint32_t *wdata, uint32_t wnum)
    {
    FLASH_Unlock(); // 解锁FLASH操作
    while (wnum)
    {
    FLASH_ProgramWord(saddr, *wdata); // 按uint32_t写入数据
    wnum -= 4; // 剩余字节数减4
    saddr += 4; // 地址指针递增4字节
    wdata++; // 数据指针指向下一个32位数据
    }
    FLASH_Lock(); // 锁定FLASH
    }

    Bootloader功能实现

    AB分区规划

    1个扇页1KB,A区起始位置:0x 0800 5000, 单片机RAM位置:0x20000000 ~ 0x20004FFF

    STM32F103C8T6 64KB 扇页
    B区 20KB 0 ~ 19
    A区 44KB 20 ~ 63

    注意点

    问题 解答
    谁将OTA_Flag打勾? A区负责控制,标志位存放在AT24C02
    什么时候OTA_flag打勾 A区下载完毕之后
    OTA时,最新版本的程序文件下载到哪? 分片下载,共计256片塞入W25Q64(一页256Byte,共256页)
    OTA时,最新版本的程序文件如何下载?下载多少? 服务器下发程序大小,分片下载到W25Q64
    下载多少这个变量用不用保存? 需要,保存到AT24C02之中
    发生OTA事件时,B区如何更新A区 从W25Q64读取数据,写入A区Flash

    OTA宏定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    #define FLASH_SADDR 0x08000000                                     // FLASH起始地址
    #define FLASH_PAGE_SIZE 1024 // Flash页大小
    #define FLASH_NUM 64 // FLASH总页数
    #define FLASH_B_NUM 20 // B区页数
    #define FLASH_A_NUM FLASH_NUM - FLASH_B_NUM // A区页数
    #define FLASH_A_SPAGE FLASH_B_NUM // A区起始页数
    #define FLASH_A_SADDR FLASH_SADDR + FLASH_A_SPAGE *FLASH_PAGE_SIZE // A区起始地址

    #define UPDATA_A_FLAG 0x00000001 // A区更新标志
    #define IAP_XMODEM_FLAG 0x00000002 // 使用Xmodem协议的标志
    #define IAP_XMODEMD_FLAG 0x00000004 // Xmodem协议传输数据的标志
    #define SET_VERSION_FLAG 0x00000008 // 设置版本号的命令标志
    #define CMD_5_FLAG 0x000000010 // 向外部FLASH下载程序的命令标志
    #define CMD5_XMODEM_FLAG 0x000000020 // 标记使用命令5后,Xmodem协议传输
    #define CMD_6_FLAG 0x000000040 // 使用外部FLASH内程序

    #define OTA_SET_FLAG 0xAABBCCDD

    // 关于数组大小是11的原因,一个uint32_t为4位,(1+11)*4=48 Byte,对应24C02存储中每页16Byte,只需要3页就能存储
    typedef struct
    {
    uint32_t OTA_flag; // OTA标志位
    uint32_t FireLen[11]; // 用于存储固件各部分的大小,0号成员固定W25Q64
    uint8_t OTA_Ver[32]; // 版本号
    } OTA_CB;

    typedef struct
    {
    uint8_t UpDataBuff[FLASH_PAGE_SIZE]; // 临时存储外部接收的固件数据块
    uint32_t W25Q64_BlockNM; // 记录当前固件写入哪个块
    uint32_t XmodemTimer; // 记录延时
    uint32_t XmodemNB; // 数据包接收数量
    uint32_t XmodemCRC; // 存放CRC校验
    } UpData;

    extern OTA_CB OTA;
    extern UpData Updata_A;

    #define OTA_INFO_SIZE sizeof(OTA_CB)

    OTA读取标志位

    1
    2
    3
    4
    5
    6
    7
    // 读取OTA标志位,判断是否有数据需要更新
    void AT24C02_ReadOTA(void)
    {
    memset(&OTA, 0, OTA_INFO_SIZE); // 开辟一个OTA_INFO_SIZE大小的地址,初始化为0
    // 从AT24C02读取OTA_INFO_SIZE的数据到结构体中
    AT24C02_ReadData(0, (uint8_t *)&OTA, OTA_INFO_SIZE);
    }

    OTA保存关键变量到AT24C02

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 写入OTA配置到EEPROM
    void AT24C02_WriteOTA(void)
    {
    uint8_t *pdata = (uint8_t *)&OTA;

    for(uint8_t i = 0; i < (OTA_INFO_SIZE + 7) / 8; i++)
    {
    AT24C02_WritePage(i * 8, pdata + i * 8);
    delay_ms(10);
    }
    }

    引导更新OTA

    (从下面向上看)

    分区跳转两大关键SP、PC设定

    Cortex-M3有R0-R12通用寄存器,R13有MSP(主堆栈指针)和PSP(进程堆栈指针)(保存现场和恢复现场的指针),R14是LR(连接寄存器,保存子函数之间跳转的返回值),R15是PC(程序计数器)

    Q:MSP指针和PSP指针,分别在什么情况下使用?

    MSP (Main Stack Pointer)

    • 系统默认的栈指针。
    • 上电复位后,CPU 自动把 MSP 当栈指针
    • 通常用来处理 异常 / 中断 / 内核级任务

    PSP (Process Stack Pointer)

    • 需要软件设置(CONTROL 寄存器里切换)。
    • 通常用来跑 用户线程 / 普通任务。(FreeRTOS里的每个任务栈)
    • 20(A区起始页) * 1024 = 20480 —> 0x00005000 + 0x08000000 = 0x08005000,此为A区开始时SP地址

    • A区起始位置0x08005000 + 4(32位的指针是4Byte),此为A区开始时PC地址

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    // RAM : 0x20000000 ~ 0x20004FFF

    #define FLASH_A_SADDR FLASH_SADDR + FLASH_A_SPAGE * FLASH_PAGE_SIZE // A区起始地址

    typedef void (*load_a)(void); // 回调函数,函数指针,指向一个无参数、无返回值的函数
    load_a load_A;

    // 用于修改主堆栈指针(MSP)
    __ASM void MSR_SP(uint32_t addr)
    {
    MSR MSP, r0;// MSR指令用于将程序状态寄存器的内容传送到通用寄存器中,R0=addr
    BX r14;// 通过链接寄存器LR(r14)返回调用者,等同于return
    }

    // 跳转到A区应用程序
    void LOAD_A(uint32_t addr)
    {
    if ((*(uint32_t *)addr >= 0x20000000) && (*(uint32_t *)addr <= 0x20004FFF))
    {
    // 设置A区起始地址,因为MSR_SP函数接收uint32_t的数据,所以先强制转换,再指针取地址
    MSR_SP(*(uint32_t *)addr);
    load_A = (load_a) * (uint32_t *)(addr + 4); // 获取复位中断处理程序地址
    load_A(); // 实际执行的是Reset_Handler
    }
    else
    {
    uprintf("跳转A分区失败\r\n");
    }
    }

    // 引导OTA函数
    void BootLoader_Jump(void)
    {
    // 等待20*100ms检测是否进入命令行模式
    if (BootLoader_Enter(20) == 0)
    {
    // 检查OTA更新标志
    if (OTA.OTA_flag == OTA_SET_FLAG)
    {
    uprintf("OTA更新 \r\n");
    BootState |= UPDATA_A_FLAG; // 设置A区更新标志
    UpDATA_A.W25Q64_BlockNM = 0; // 默认使用W25Q64的块0
    }
    else
    {
    uprintf("跳转A分区 \r\n");
    LOAD_A(FLASH_A_SADDR); // 跳转到A区应用程序
    }
    }
    uprintf("进入BootLoader命令行 \r\n");
    BootLoader_Info();
    }
    // 复位外设,没用到
    void BootLoader_Clear(void)
    {
    USART_DeInit(USART1);
    GPIO_DeInit(GPIOA);
    GPIO_DeInit(GPIOB);
    }

    BootLoader事件

    (从上面向下看)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    uint32_t BootState; 			// 系统启动状态标志位
    uint8_t wdata[2048];
    uint8_t rdata[2048];

    /* 显示BootLoader命令行帮助信息 */
    void BootLoader_Info(void)
    {
    uprintf(" \r\n");
    uprintf("[1]擦除A区 \r\n");
    uprintf("[2]串口IAP下载A区 \r\n");
    uprintf("[3]设置OTA版本号 \r\n");
    uprintf("[4]查询OTA版本号 \r\n");
    uprintf("[5]向外部FLASH下载程序 \r\n");
    uprintf("[6]使用外部FLASH内程序 \r\n");
    uprintf("[7]重启 \r\n");
    }
    /* 检测是否进入命令行模式,timeout 超时时间,1-进入命令行,0-不进入 */
    uint8_t BootLoader_Enter(uint8_t timeout)
    {
    uprintf("输入小写字母w,进入BootLoader命令行 \r\n");
    while (timeout--)
    {
    Delay_ms(200);
    if (U1_RX_Buff[0] == 'w')
    {
    return 1; // 进入命令行
    }
    }
    return 0; // 不进入
    }
    // Bootloader事件组
    void BootLoader_Event(uint8_t *data, uint16_t datalen)
    {
    int temp, i;
    if (BootState == 0)// BootState=0:不进行其他操作,显示界面
    {
    if ((datalen == 1) && (data[0] == '1'))// 擦除A区
    {
    uprintf("[1]擦除A区 \r\n");
    FLASH_Erase(FLASH_A_SPAGE, FLASH_A_NUM);
    Delay_ms(100);
    BootLoader_Info();
    }
    else if ((datalen == 1) && (data[0] == '2'))// 通过Xmodem下载固件到A区
    {
    uprintf("通过Xmodem协议,串口IAP下载A区程序,使用bin文件 \r\n");
    FLASH_Erase(FLASH_A_SPAGE, FLASH_A_NUM); // 擦除目标区域
    BootState |= (IAP_XMODEM_FLAG | IAP_XMODEMD_FLAG); // 设置Xmodem标志
    UpDATA_A.XmodemTimer = 0; // 重置计时器
    UpDATA_A.XmodemNB = 0; // 重置数据包计数器
    }
    else if ((datalen == 1) && (data[0] == '3')) // 设置版本号
    {
    uprintf("设置版本号 \r\n");
    BootState |= SET_VERSION_FLAG;
    }
    else if ((datalen == 1) && (data[0] == '4')) // 查询版本号
    {
    uprintf("查询版本号 \r\n");
    AT24C02_ReadOTA();
    uprintf("版本号:%s \r\n", OTA.OTA_Ver);
    }
    else if ((datalen == 1) && (data[0] == '5')) //向外部FLASH传输程序
    {
    uprintf("向外部FLASH传输程序,输入需要使用的块编号(1~9) \r\n");
    BootState |= CMD_5_FLAG;
    }
    else if ((datalen == 1) && (data[0] == '6')) //使用外部FLASH下载程序
    {
    uprintf("使用外部FLASH下载程序,输入需要使用的块编号(1~9) \r\n");
    BootState |= CMD_6_FLAG;
    }
    else if ((datalen == 1) && (data[0] == '7')) // Reset
    {
    uprintf("重启中 \r\n");
    __set_FAULTMASK(1);
    Delay_ms(100);
    NVIC_SystemReset(); //NVIC重启
    }
    }
    else if (BootState & IAP_XMODEMD_FLAG) // Xmodem协议数据处理
    {
    // 接收数据包(133字节:1个字节+128字节+2CRC+2Byte序号)
    if ((datalen == 133) && (data[0] == 0x01))
    {
    BootState &= ~IAP_XMODEM_FLAG;
    UpDATA_A.XmodemCRC = Xmodem_CRC16(&data[3], 128);// 计算接收数据的CRC校验值
    if (UpDATA_A.XmodemCRC == data[131] * 256 + data[132]) // 校验通过处理
    {
    UpDATA_A.XmodemNB++;// Xmodem接收数据包增加
    // 将数据拷贝到缓冲区
    memcpy(&UpDATA_A.UpDataBuff[((UpDATA_A.XmodemNB - 1) % (FLASH_PAGE_SIZE / 128)) * 128], &data[3], 128);
    //&data[3]:Xmodem包的数据部分从第4字节开始(跳过1字节头+2字节序号)
    //(FLASH_PAGE_SIZE(1024) / 128)计算一页可以存下的包数,对该取模,当超过一页重新计算
    if ((UpDATA_A.XmodemNB % (FLASH_PAGE_SIZE / 128)) == 0) // 1024/128=8,每8个数据包发送一次
    {
    if (BootState & CMD5_XMODEM_FLAG)// 选择写入W25Q64或FLASH
    {
    for (i = 0; i < 4; i++)// 写入外部W25Q64,每次写入256字节,写4次为一页
    {
    W25Q64_PageWrite(&UpDATA_A.UpDataBuff[i * 256],
    (UpDATA_A.XmodemNB / 8 - 1) * 4 + i + UpDATA_A.W25Q64_BlockNM * 64 * 4);
    //(x/8-1)*4+i,x表示接收到的数据包个数,4表示写入W25Q64的页数,i用来定位页数
    }
    }
    else
    {
    // FLASH_A_SADDR + ((UpDATA_A.XmodemNB / (FLASH_PAGE_SIZE / 128)) - 1) * FLASH_PAGE_SIZE
    // A区的起始地址+((接收的8个Xmodem协议包 / (一页占据的协议包数量)) -1(索引从0开始) * 当前页偏移地址
    // 通过Xmodem协议传输的写入到缓冲区的数据
    FLASH_Write(FLASH_A_SADDR + ((UpDATA_A.XmodemNB / (FLASH_PAGE_SIZE / 128)) - 1) * FLASH_PAGE_SIZE,
    (uint32_t *)UpDATA_A.UpDataBuff, FLASH_PAGE_SIZE);
    }
    }
    uprintf("\x06"); // 表示Xmodem协议包读取成功
    }
    else
    {
    uprintf("\x15"); // 表示Xmodem协议包读取失败
    }
    }
    if ((datalen == 1) && (data[0] == 0x04)) // 读取Xmodem协议包剩余的数据
    {
    uprintf("\x06");
    if ((UpDATA_A.XmodemNB % (FLASH_PAGE_SIZE / 128)) != 0) // 如果有剩余的数据
    {
    if (BootState & CMD5_XMODEM_FLAG)
    {
    for (i = 0; i < 4; i++)
    {
    W25Q64_PageWrite(&UpDATA_A.UpDataBuff[i * 256],
    (UpDATA_A.XmodemNB / 8) * 4 + i + UpDATA_A.W25Q64_BlockNM * 64 * 4);
    }
    }
    else
    {
    FLASH_Write(FLASH_A_SADDR + ((UpDATA_A.XmodemNB / (FLASH_PAGE_SIZE / 128))) * FLASH_PAGE_SIZE,
    (uint32_t *)UpDATA_A.UpDataBuff,
    (UpDATA_A.XmodemNB % (FLASH_PAGE_SIZE / 128)) * 128);
    }
    }
    BootState &= ~IAP_XMODEMD_FLAG;
    if (BootState & CMD5_XMODEM_FLAG) // 将数据下载到W25Q64
    {
    BootState &= ~CMD5_XMODEM_FLAG; // 将标志位复位
    OTA.FireLen[UpDATA_A.W25Q64_BlockNM] = UpDATA_A.XmodemNB * 128; // 将Xmodem数据存入W25Q64中
    AT24C02_WriteOTA(); // 存储数据,掉电不丢失
    Delay_ms(100);
    BootLoader_Info(); //重新显示命令行
    }
    else
    {
    __set_FAULTMASK(1); // 关闭所有中断
    Delay_ms(100);
    NVIC_SystemReset(); // 程序重启
    }
    }
    }
    else if (BootState & SET_VERSION_FLAG) // 设置版本号
    {
    if (datalen <= 32)
    {
    if (sscanf((const char *)data, "VER-%d.%d.%d-%d/%d/%d-%d:%d", &temp, &temp, &temp, &temp, &temp, &temp, &temp, &temp) == 8)
    { // VER-1.0.0-2025/5/1-10:28
    memset(OTA.OTA_Ver, 0, 32); // 将之前的版本号清零
    memcpy(OTA.OTA_Ver, data, 32); // 填入新的版本号
    AT24C02_WriteOTA(); // 写入掉电不丢失芯片
    uprintf("版本号正确 \r\n");
    BootLoader_Info();
    BootState &= ~SET_VERSION_FLAG; // 使用完复位标志位
    }
    else
    {
    uprintf("版本号格式错误 \r\n");
    }
    }
    else
    {
    uprintf("版本号长度错误 \r\n");
    }
    }
    else if (BootState & CMD_5_FLAG) // 将bin程序传入W25Q64的第%d个块
    {
    if (datalen == 1)
    {
    if ((data[0] >= 0x31) && (data[0] <= 0x39))
    {
    UpDATA_A.W25Q64_BlockNM = data[0] - 0x30; // 字符转换数字
    BootState |= (IAP_XMODEM_FLAG | IAP_XMODEMD_FLAG | CMD5_XMODEM_FLAG);//加上XMODEM的标志位
    UpDATA_A.XmodemTimer = 0; // 清空定时器
    UpDATA_A.XmodemNB = 0; // 清空发送的包数
    OTA.FireLen[UpDATA_A.W25Q64_BlockNM] = 0; // 置零表示清空数据
    W25Q64_Erase_64k(UpDATA_A.W25Q64_BlockNM); // 擦除整块
    uprintf("通过Xmodem协议,向外部FLASH第%d个块传输程序,使用bin文件 \r\n", UpDATA_A.W25Q64_BlockNM);
    BootState &= ~CMD_5_FLAG;
    }
    else
    {
    uprintf("编号错误 \r\n");
    }
    }
    else
    {
    uprintf("数据长度错误 \r\n");
    }
    }
    else if (BootState & CMD_6_FLAG) // 使用W25Q64保存的APP程序
    {
    if (datalen == 1)
    {
    if ((data[0] >= 0x31) && (data[0] <= 0x39))
    {
    UpDATA_A.W25Q64_BlockNM = data[0] - 0x30; // 字符转换数字
    BootState |= UPDATA_A_FLAG; // A区更新标志
    BootState &= ~CMD_6_FLAG; // 将命令6标志置位
    }
    else
    {
    uprintf("编号错误 \r\n");
    }
    }
    else
    {
    uprintf("长度错误 \r\n");
    }
    }
    }

    主程序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    // 需要用到的功能:串口,SPI,FLASH
    uint32_t BootState; // 系统启动状态标志位
    uint8_t wdata[2048];
    uint8_t rdata[2048];
    extern OTA_CB OTA;
    extern UpData UpDATA_A;
    extern uint32_t BootState;

    int main(void)
    {
    uint8_t i;
    USART1_Init(921600); // 初始化串口1,波特率921600
    Delay_Init(); // 初始化延时函数
    I2C1_Init(); // 初始化I2C1(用于AT24C02通信)
    AT24C02_ReadOTA(); // 从AT24C02读取OTA标志位
    W25Q64_Init(); // 初始化W25Q64(SPI Flash)
    BootLoader_Jump(); // 检查是否需要跳转到BootLoader
    while (1)
    {
    Delay_ms(10); // 主循环延时10ms

    // 串口1处理缓冲区数据
    if (U1CB.URxDataOUT != U1CB.URxDataIN)
    {
    // 处理接收到的数据(Xmodem协议或其他指令)
    BootLoader_Event(U1CB.URxDataOUT->start, U1CB.URxDataOUT->end - U1CB.URxDataOUT->start + 1);
    // 移动接收缓冲区指针
    U1CB.URxDataOUT++;
    if (U1CB.URxDataOUT == U1CB.URxDataEND)
    {
    U1CB.URxDataOUT = &U1CB.URxDataPtr[0]; // 环形缓冲区回卷
    }
    }

    // 检查是否需要发送Xmodem协议的控制字符'C'
    if (BootState & IAP_XMODEM_FLAG)
    {
    if (UpDATA_A.XmodemTimer >= 100) // 每100次循环发送一次'C'
    {
    uprintf("C"); // 发送Xmodem起始信号
    UpDATA_A.XmodemTimer = 0;
    }
    UpDATA_A.XmodemTimer++;
    }

    // 检查是否需要更新A区固件
    if (BootState & UPDATA_A_FLAG)
    {
    uprintf("长度%d字节\r\n", OTA.FireLen[UpDATA_A.W25Q64_BlockNM]);
    // 擦除目标FLASH区域(A区)
    FLASH_Erase(FLASH_A_SPAGE, FLASH_A_NUM);
    uprintf("A区已擦除 \r\n");
    // 检查固件长度是否为4的倍数
    if (OTA.FireLen[UpDATA_A.W25Q64_BlockNM] % 4 == 0)
    {
    // 按1KB页循环写入完整数据块
    for (i = 0; i < (OTA.FireLen[UpDATA_A.W25Q64_BlockNM] / 1024); i++)
    {
    // 从W25Q64读取1KB数据到缓冲区
    // 源地址:块号×64KB + 偏移量, 读取长度=1KB
    W25Q64_Read(UpDATA_A.UpDataBuff, i * FLASH_PAGE_SIZE + 64 * 1024 * UpDATA_A.W25Q64_BlockNM, FLASH_PAGE_SIZE);
    // 将1KB数据写入FLASH(目标地址递增)
    // 目标地址
    // 数据指针(强制转换为uint32_t*)
    // 写入长度=1KB
    FLASH_Write(FLASH_A_SADDR + i * FLASH_PAGE_SIZE, (uint32_t *)UpDATA_A.UpDataBuff, FLASH_PAGE_SIZE);
    }

    // 处理剩余不足1KB的数据
    if (OTA.FireLen[UpDATA_A.W25Q64_BlockNM] % 1024 != 0)
    {
    memset(UpDATA_A.UpDataBuff, 0, FLASH_PAGE_SIZE);
    // 读取剩余数据
    // 剩余数据的起始地址,剩余数据长度
    W25Q64_Read(UpDATA_A.UpDataBuff, i * FLASH_PAGE_SIZE + 64 * 1024 * UpDATA_A.W25Q64_BlockNM,
    OTA.FireLen[UpDATA_A.W25Q64_BlockNM] % 1024);

    // 写入剩余数据到FLASH
    FLASH_Write(FLASH_A_SADDR + i * FLASH_PAGE_SIZE, (uint32_t *)UpDATA_A.UpDataBuff, OTA.FireLen[UpDATA_A.W25Q64_BlockNM] % 1024);
    }

    // OTA更新后,UpDATA_A.W25Q64_BlockNM == 0
    if (UpDATA_A.W25Q64_BlockNM == 0)
    {
    OTA.OTA_flag = 0; // 清除OTA标志
    AT24C02_WriteOTA(); // 更新后写入AT24C02(持久化存储)
    }

    // 重启
    uprintf(" \r\nA区更新完毕 \r\n");
    __set_FAULTMASK(1); // 屏蔽所有异常
    Delay_ms(100);
    NVIC_SystemReset(); // 系统复位
    }
    else
    {
    uprintf("长度错误\r\n"); // 数据长度未对齐
    BootState &= ~UPDATA_A_FLAG; // 清除更新标志
    BootLoader_Info();
    }
    }
    }
    }

    Xmodem协议

    Xmodem使用SecureCRTP软件配置连接串口通信,它相较于Ymodem的区别是具有更小的Package长度

    格式

    Byte1 Byte2 Byte3 Byte4 ~ Byte131 Byte132 ~ Byte133
    Start of Header(SOH) Packet Number ~(Packet Number) Pcacket Data CRC16 Check

    指令

    指令 说明
    SOH 0x01 128字节数据包帧头
    STX 0x02 1024字节数据包帧头
    EOT 0x04 结束传输
    ACK 0x06 正确应答
    NAK 0x15 错误应答,重传数据
    CAN 0x18 取消传输
    CTRLZ 0x1A 数据填充
    HSC 0x43 握手

    例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    ‘C’											// 发送一个C等待数据包
    // (3s一次,等待应答)
    SOH | 0x01 | 0xFE | Data[0~127] | CRC16 // 第一条指令
    ACK(正确应答)
    SOH | 0x02 | 0xFD | Data[0~127] | CRC16 // 第二条指令
    NAK(错误应答)
    SOH | 0x02 | 0xFD | Data[0~127] | CRC16 // 第二条指令
    ACK
    SOH | 0x03 | 0xFC | Data[0~127] | CRC16 // 第三条指令
    ......

    CRC16程序

    STM32支持CRC32,不支持CRC16,需要自己写

    多项式p(x) = x^16 + x^12 + x^5 + 1,借助多项式将输入的数值进行模2除法,在C语言中是进行异或运算^。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    // 文字说明
    寄存器清零
    数据最右边补齐W位0 // W是CRC校验值的位数
    when(还有数据){
    左移寄存器1位,读取数据的下一位到寄存器的bit 0
    if (左移寄存器时出现溢出){
    寄存器 ^= poly; // 这里的poly=0011,按照上面的例子
    }
    }
    寄存器的值就是校验值

    uint16_t Xmodem_CRC16(uint8_t *data, uint16_t datalen)
    {
    uint8_t i;
    uint16_t Crcinit = 0x0000; // 初始化为0
    uint16_t Poly = 0x1021; // XMODEM 使用的多项式:x^16 + x^12 + x^5 + 1

    while (datalen--)
    {
    Crcinit = (*data << 8) ^ Crcinit;
    // 左移八位是因为CRC是高位优先计算,异或是为了改变当前CRC的值
    // 异或将新数据“混合”进当前的CRC值,使CRC计算能覆盖所有输入数据
    // 若直接赋值会丢失之前计算的CRC值
    for (i = 0; i < 8; i++) // 每个bit都要影响CRC计算,所以必须循环 8 次
    {
    if (Crcinit & 0x8000) // 检查最高位是否为1
    Crcinit = (Crcinit << 1) ^ Poly;
    // 如果最高位是1,说明当前的CRC值已经达到或超过多项式的最高位(Poly最高位为0x1000)
    // 必须减去多项(即^Poly)否则CRC值会越来越大
    else
    Crcinit = (Crcinit << 1);
    }
    data++;
    }
    return Crcinit;
    }

    参考

    havenxie/stm32-iap-uart-boot: STM32 IAP(UART模式)的BOOT部分

    【手把手教程 4G通信物联网 OTA远程升级 BootLoader程序设计】GD32F103C8T6单片机【上篇章】_哔哩哔哩_bilibili

    补充(7.24)

    Bootloader执行流程

    1. 上电或复位

      • 当系统上电或复位时,处理器从一个固定的地址开始执行,这个地址称为 向量表(Vector Table) 的起始地址。
    2. 读取向量表地址(比如 Flash 起始地址)

      • 默认情况下,ARM Cortex-M 处理器会从地址 0x08000000(即 Flash 起始地址)读取:
        • 0x08000000:初始 MSP(Main Stack Pointer)
        • 0x08000004Reset Handler 的地址,也就是主程序的入口点
    3. 设置 MSP

    • 处理器将 0x08000000 处的值加载到 MSP(Main Stack Pointer),为堆栈初始化。
    1. 跳转到 Reset Handler
      • 处理器将 0x08000004 处的值作为程序计数器 PC,开始执行实际程序。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // 设置主栈指针
    __ASM void MSR_SP(uint32_t addr)
    {
    MSR MSP, r0;
    BX r14;
    }

    // 跳转到A区应用程序
    void LOAD_A(uint32_t addr)
    {
    if((*(uint32_t *)addr >= 0x20000000) && (*(uint32_t *)addr <= 0x20004FFF))
    {
    MSR_SP(*(uint32_t *)addr);
    load_A = (load_a)*(uint32_t *)(addr + 4);
    load_A();
    }
    else
    {
    uprintf("Failed to jump to Area A \r\n");
    }
    }

    SRAM在此的意义

    • SRAM(Static RAM)是 MCU 的运行内存(RAM),栈、全局变量、局部变量都存放在这里。

    • MSP 指针一般会指向 SRAM 的顶端(例如 0x20000000),向下增长。

    • 应用程序运行期间所有动态数据、堆栈帧等都存在于 SRAM 中。

    APP程序配置

    1. system_stm32f10x.c文件中的VECT_TAB_OFFSET,设置0x5000

    2. 配置Target

    DMA如何配置RX_BUFFER + 1配合IDLE实现接收不定长数据

    1. DMA设置:传输计数为RX_BUFFER + 1

    2. IDLE触发:当接收数据长度小于设置计数时触发

    3. 长度计算:通过剩余计数计算实际接收长度

    4. 重新配置:每次IDLE中断后重新配置DMA