GD32入门到实战35--485实现OTA

发布于:2025-09-08 ⋅ 阅读:(18) ⋅ 点赞:(0)

 单片机常见烧录方法:

IAP远程更新程序

我们内存这样分配,简单来说就是 把单片机 flash 分成两份,bootloader 引导程序 12k, appflash 主程序 500k,上电默认进入引导程序,可以用 485  通过 ymodem 烧写 bin 文件,修改 appflash,实现更新固件(程序)

bootloader启动 boot的复位函数 ----> app的复位函数

我们用的是Ymodem协议

update.c

/**
 *******************************************************************************
 * @file    update.c
 * @brief   YMODEM 协议在线升级(IAP)接收与 Flash 烧写
 *          支持 128/1024 字节包,CRC16-ymodem 校验,自动擦写 Flash
 *******************************************************************************
 */

#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "rs485_drv.h"
#include "delay.h"
#include "flash_drv.h"
#include "update.h"

/* -------------------- 字符工具宏 -------------------- */
#define IS_AF(c)   ((c) >= 'A' && (c) <= 'F')
#define IS_af(c)   ((c) >= 'a' && (c) <= 'f')
#define IS_09(c)   ((c) >= '0' && (c) <= '9')
#define ISVALIDHEX(c) (IS_AF(c) || IS_af(c) || IS_09(c))
#define ISVALIDDEC(c) IS_09(c)
#define CONVERTDEC(c) ((c) - '0')
#define CONVERTHEX_alpha(c) (IS_AF(c) ? (c) - 'A' + 10 : (c) - 'a' + 10)
#define CONVERTHEX(c) (IS_09(c) ? CONVERTDEC(c) : CONVERTHEX_alpha(c))

/* -------------------- 整数转字符串 -------------------- */
/**
 * @brief  将 int32 转成 ASCII 字符串(无符号)
 * @param  str     输出缓冲区
 * @param  intnum  待转换整数
 */
void Int2Str(uint8_t *str, int32_t intnum)
{
    uint32_t i, div = 1000000000, j = 0, Status = 0;

    for (i = 0; i < 10; i++)
    {
        str[j++] = (intnum / div) + '0';
        intnum %= div;
        div /= 10;
        /* 去掉前导 0 */
        if (str[j - 1] == '0' && Status == 0)
            j = 0;
        else
            Status = 1;
    }
}

/* -------------------- 字符串转整数 -------------------- */
/**
 * @brief  解析字符串为整数(10/16 进制,支持 K/M 后缀)
 * @param  inputstr 输入字符串
 * @param  intnum   输出整数
 * @return 1 成功;0 格式错误
 */
uint32_t Str2Int(uint8_t *inputstr, int32_t *intnum)
{
    uint32_t i = 0, res = 0, val = 0;

    /* 16 进制 0x/0X 前缀 */
    if (inputstr[0] == '0' && (inputstr[1] == 'x' || inputstr[1] == 'X'))
    {
        if (inputstr[2] == '\0') return 0;
        for (i = 2; i < 11; i++)
        {
            if (inputstr[i] == '\0')
            {
                *intnum = val;
                res = 1;
                break;
            }
            if (ISVALIDHEX(inputstr[i]))
                val = (val << 4) + CONVERTHEX(inputstr[i]);
            else
            {
                res = 0;
                break;
            }
        }
        if (i >= 11) res = 0;
    }
    else /* 10 进制,支持 K/M 后缀 */
    {
        for (i = 0; i < 11; i++)
        {
            if (inputstr[i] == '\0')
            {
                *intnum = val;
                res = 1;
                break;
            }
            else if ((inputstr[i] == 'k' || inputstr[i] == 'K') && i > 0)
            {
                val <<= 10;
                *intnum = val;
                res = 1;
                break;
            }
            else if ((inputstr[i] == 'm' || inputstr[i] == 'M') && i > 0)
            {
                val <<= 20;
                *intnum = val;
                res = 1;
                break;
            }
            else if (ISVALIDDEC(inputstr[i]))
                val = val * 10 + CONVERTDEC(inputstr[i]);
            else
            {
                res = 0;
                break;
            }
        }
        if (i >= 11) res = 0;
    }
    return res;
}

/* -------------------- CRC16-YMODEM 计算 -------------------- */
/**
 * @brief  CRC16-ymodem(多项式 0x1021)
 * @param  data   数据指针
 * @param  length 数据长度
 * @return CRC16 值
 */
uint16_t Crc16Ymodem(uint8_t *data, uint16_t length)
{
    uint16_t crc = 0;
    while (length--)
    {
        crc ^= (uint16_t)(*data++) << 8;
        for (uint8_t i = 0; i < 8; i++)
        {
            if (crc & 0x8000)
                crc = (crc << 1) ^ 0x1021;
            else
                crc <<= 1;
        }
    }
    return crc;
}

/* -------------------- YMODEM 协议常量 -------------------- */
#define PACKET_SEQNO_INDEX      1
#define PACKET_SEQNO_COMP_INDEX 2
#define PACKET_HEADER           3
#define PACKET_TRAILER          2
#define PACKET_OVERHEAD         (PACKET_HEADER + PACKET_TRAILER)
#define PACKET_SIZE             128
#define PACKET_1K_SIZE          1024

#define SOH                     0x01  /* 128 字节数据包 */
#define STX                     0x02  /* 1024 字节数据包 */
#define EOT                     0x04  /* 结束传输 */
#define ACK                     0x06  /* 回应正确 */
#define NAK                     0x15  /* 回应错误 */
#define CA                      0x18  /* 连续两个 CA 表示中止 */
#define CREQ                    0x43  /* 'C' 请求数据 */

#define ABORT1                  0x41  /* 'A' 用户中止 */
#define ABORT2                  0x61  /* 'a' 用户中止 */

#define NAK_TIMEOUT             0x100000
#define MAX_ERRORS              5

/* -------------------- 静态缓冲区 -------------------- */
#define YMODEM_PACKET_LENGTH    1024
static uint8_t g_packetBuffer[YMODEM_PACKET_LENGTH];

#define FILE_NAME_LENGTH        256
#define FILE_SIZE_LENGTH        16
static char  g_imageName[FILE_NAME_LENGTH];  /* 接收到的文件名 */

/* -------------------- 接收一个 YMODEM 包 -------------------- */
/**
 * @brief  接收单个 YMODEM 数据包
 * @param  data    输出包缓冲区(含头、数据、CRC)
 * @param  length  输出数据区长度(128/1024/0/-1)
 * @param  timeout 接收超时(ms)
 * @return 0 正常;1 用户中止;-1 出错/超时
 */
static int32_t ReceivePacket(uint8_t *data, int32_t *length, uint32_t timeout)
{
    uint16_t i, packetSize;
    uint8_t  c;
    *length = 0;

    /* 等待首字节 */
    if (ReceiveByteTimeout(&c, timeout) != 0)//没有接收到数据还超时了
        return -1;

    switch (c)
    {
    case SOH: packetSize = PACKET_SIZE; break;   //如果是SOH就是128个数据
    case STX: packetSize = PACKET_1K_SIZE; break;//如果是STX就是1024个数据
    case EOT: return 0;                          /* 正常结束 */
    case CA:                                    /* 双 CA 中止 */
        if ((ReceiveByteTimeout(&c, timeout) == 0) && (c == CA))
        {
            *length = -1;
            return 0;
        }
        else return -1;
    case ABORT1:
    case ABORT2: return 1;                      /* 用户中止 */
    default: return -1;
    }

    *data = c;  /* 保存首字节 */

    /* 接收剩余字节(头+数据+CRC) */
    for (i = 1; i < (packetSize + PACKET_OVERHEAD); i++)
    {
        if (ReceiveByteTimeout(data + i, timeout) != 0)
            return -1;
    }

    /* 序号校验 */
    if ((data[PACKET_SEQNO_INDEX] | data[PACKET_SEQNO_COMP_INDEX]) != 0xFF)
        return -1;

    /* CRC16 校验 */
    uint16_t crc16     = Crc16Ymodem(&data[PACKET_HEADER], packetSize);
    uint16_t raw_crc16 = (uint16_t)(data[packetSize + PACKET_OVERHEAD - 2] << 8) |
                          data[packetSize + PACKET_OVERHEAD - 1];
    if (crc16 != raw_crc16)
        return -1;

    *length = packetSize;
    return 0;
}

/* -------------------- YMODEM 文件接收主流程 -------------------- */
/**
 * @brief  YMODEM 协议接收文件并写入 Flash
 * @param  buf 临时缓存(≥1 KB)
 * @return 文件大小(>0 成功);≤0 错误码
 */
int32_t YmodemReceive(uint8_t *buf)
{
    uint8_t  packetData[PACKET_1K_SIZE + PACKET_OVERHEAD];
    uint8_t  fileSize[FILE_SIZE_LENGTH], *filePtr, *bufPtr;
    int32_t  i, packetLength, sessionDone, fileDone, packetsReceived, errors, sessionBegin, size = 0;
    uint32_t flashDestination = APP_ADDR_IN_FLASH; /* APP 起始地址 */

    /* 大循环:处理整个会话(可能含多个文件) */
    for (sessionDone = 0, errors = 0, sessionBegin = 0; ; )
    {
        /* 单文件循环 */
        for (packetsReceived = 0, fileDone = 0, bufPtr = buf; ; )
        {
            switch (ReceivePacket(packetData, &packetLength, NAK_TIMEOUT))
            {
            case 0:  /* 收到正常包 */
                errors = 0;
                switch (packetLength)
                {
                case -1: /* 发送方中止 */
                    SendByte(ACK);
                    return 0;
                case 0:  /* EOT 结束当前文件 */
                    SendByte(ACK);
                    fileDone = 1;
                    break;
                default: /* 数据包 */
                    if (packetsReceived == 0) /* 首包 = 文件名包 */
                    {
                        if (packetData[PACKET_HEADER] != 0) /* 有文件名 */
                        {
                            /* 提取文件名 */
                            for (i = 0, filePtr = packetData + PACKET_HEADER;
                                 *filePtr != 0 && i < FILE_NAME_LENGTH - 1; )
                                g_imageName[i++] = *filePtr++;
                            g_imageName[i] = '\0';

                            /* 提取文件大小 */
                            for (i = 0, filePtr++;
                                 *filePtr != ' ' && i < FILE_SIZE_LENGTH - 1; )
                                fileSize[i++] = *filePtr++;
                            fileSize[i] = '\0';
                            Str2Int((uint8_t *)fileSize, &size);

                            /* 大小检查 */
                            if (size > FLASH_APP_SIZE) /* APP 区装不下 */
                            {
                                SendByte(CA);
                                SendByte(CA);
                                return -1;
                            }
                            /* 擦除 App 区 */
                            FlashErase(flashDestination, size);
                            SendByte(ACK);
                            SendByte(CREQ); /* 请求下一块 */
                        }
                        else /* 空文件名 → 会话结束 */
                        {
                            SendByte(ACK);
                            fileDone = 1;
                            sessionDone = 1;
                            break;
                        }
                    }
                    else /* 普通数据包 */
                    {
                        memcpy(bufPtr, packetData + PACKET_HEADER, packetLength);
                        FlashWrite(flashDestination, bufPtr, packetLength);
                        flashDestination += packetLength;
                        SendByte(ACK);
                    }
                    packetsReceived++;
                    sessionBegin = 1;
                    break;
                }
                break;
            case 1: /* 用户中止 */
                SendByte(CA);
                SendByte(CA);
                return -3;
            default: /* 超时或错包 */
                if (sessionBegin > 0) errors++;
                if (errors > MAX_ERRORS)
                {
                    SendByte(CA);
                    SendByte(CA);
                    return 0;
                }
                SendByte(CREQ); /* 继续请求 */
                break;
            }
            if (fileDone) break;
        }
        if (sessionDone) break;
    }
    return size; /* 返回文件大小 */
}

/* -------------------- 对外升级入口 -------------------- */
/**
 * @brief  等待 PC 发送 YMODEM 文件并升级 APP
 */
void UpdateApp(void)
{
    uint8_t strBuffer[10];
    int32_t imageSize = 0;

    printf("等待文件传输... (按 'a' 中止)\n\r");
    imageSize = YmodemReceive(g_packetBuffer);//接收文件烧写文件
    DelayNms(50); /* 留时间给串口工具显示 */

    if (imageSize > 0)
    {
        printf("\n\n\r 编程完成!\n\r");
        printf("[ 文件名: %s", g_imageName);
        Int2Str(strBuffer, imageSize);
        printf(", 大小: %s 字节 ]\r\n", strBuffer);
    }
    else if (imageSize == -1)
        printf("\n\n\r 文件超出 Flash 容量!\n\r");
    else if (imageSize == -2)
        printf("\n\n\r 校验失败!\n\r");
    else if (imageSize == -3)
        printf("\r\n\n 用户中止。\n\r");
    else
        printf("\n\r 接收失败!\n\r");
}

.h

#ifndef _UPDATE_H_
#define _UPDATE_H_

#define FLASH_SIZE                        0x80000       //512k
#define APP_ADDR_IN_FLASH                 0x8003000     //APP烧写地址
#define FLASH_APP_SIZE                    (FLASH_SIZE - (APP_ADDR_IN_FLASH - 0x08000000))  //计算app空间可用大小

void UpdateApp(void);

#endif

485.c

/**
 *******************************************************************************
 * @file    rs485_drv.c
 * @brief   GD32F30x 硬件 USART1 + RS485 半双工驱动
 *          使用 GPIOA2/3 做 TX/RX,PC5 做 DE/RE 方向控制
 *          支持:字符收发、超时接收、printf 重定向
 *******************************************************************************
 */

#include <stdint.h>
#include <stdio.h>
#include <stdbool.h>
#include "gd32f30x.h"

/* -------------------- 硬件引脚配置 -------------------- */
typedef struct
{
    uint32_t uartNo;      /* USART 外设编号 */
    rcu_periph_enum rcuUart;  /* USART 时钟 */
    rcu_periph_enum rcuGpio;  /* GPIO 时钟 */
    uint32_t gpio;        /* GPIO 端口 */
    uint32_t txPin;       /* TX 引脚 */
    uint32_t rxPin;       /* RX 引脚 */
    uint8_t  irq;         /* 中断号(暂未用) */
} UartHwInfo_t;

/* 默认使用 USART1 + PA2/PA3 + PC5 方向控制 */
static UartHwInfo_t g_uartHwInfo =
{
    USART1, RCU_USART1, RCU_GPIOA, GPIOA, GPIO_PIN_2, GPIO_PIN_3, USART1_IRQn
};

/* -------------------- GPIO 初始化 -------------------- */
/**
 * @brief  初始化 TX/RX 引脚复用
 */
static void GpioInit(void)
{
    /* 使能 GPIO 时钟 */
    rcu_periph_clock_enable(g_uartHwInfo.rcuGpio);

    /* TX:复用推挽输出 */
    gpio_init(g_uartHwInfo.gpio, GPIO_MODE_AF_PP, GPIO_OSPEED_10MHZ, g_uartHwInfo.txPin);

    /* RX:上拉输入 */
    gpio_init(g_uartHwInfo.gpio, GPIO_MODE_IPU, GPIO_OSPEED_10MHZ, g_uartHwInfo.rxPin);
}

/* -------------------- USART 初始化 -------------------- */
/**
 * @brief  配置 USART 波特率及基本参数
 * @param  baudRate 目标波特率
 */
static void UartInit(uint32_t baudRate)
{
    /* ① 使能 USART 时钟 */
    rcu_periph_clock_enable(g_uartHwInfo.rcuUart);

    /* ② 复位 USART 外设 */
    usart_deinit(g_uartHwInfo.uartNo);

    /* ③ 波特率 */
    usart_baudrate_set(g_uartHwInfo.uartNo, baudRate);

    /* ④ 发送使能 */
    usart_transmit_config(g_uartHwInfo.uartNo, USART_TRANSMIT_ENABLE);

    /* ⑤ 接收使能 */
    usart_receive_config(g_uartHwInfo.uartNo, USART_RECEIVE_ENABLE);

    /* ⑥ 启动 USART */
    usart_enable(g_uartHwInfo.uartNo);
}

/* -------------------- RS485 方向控制 -------------------- */
/* PC5 输出高 = 发送;低 = 接收 */
#define SWITCH_RS485_TO_RX()   gpio_bit_reset(GPIOC, GPIO_PIN_5)
#define SWITCH_RS485_TO_TX()   gpio_bit_set(GPIOC, GPIO_PIN_5)

/**
 * @brief  初始化 RS485 方向控制引脚
 */
static void SwitchInit(void)
{
    rcu_periph_clock_enable(RCU_GPIOC);
    gpio_init(GPIOC, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_5);
    SWITCH_RS485_TO_RX();   /* 默认接收 */
}

/* -------------------- 驱动统一入口 -------------------- */
/**
 * @brief  RS485 驱动初始化(GPIO + USART + 方向)
 */
void RS485DrvInit(void)
{
    GpioInit();
    UartInit(9600);   /* 默认 9600 bps */
    SwitchInit();
}

/* -------------------- 接收一个字节 -------------------- */
/**
 * @brief  非阻塞接收 1 字节
 * @param  key 输出字节
 * @return true 收到;false 空
 */
static bool ReceiveByte(uint8_t *key)
{
    if (usart_flag_get(g_uartHwInfo.uartNo, USART_FLAG_RBNE) != RESET)
    {
        *key = (uint8_t)usart_data_receive(g_uartHwInfo.uartNo);
        return true;
    }
    return false;
}

/**
 * @brief  超时接收 1 字节
 * @param  c       输出字节
 * @param  timeout 超时时间(循环次数)
 * @return 0 成功;-1 超时
 */
int32_t ReceiveByteTimeout(uint8_t *c, uint32_t timeout)
{
    while (timeout-- > 0)
    {
        if (ReceiveByte(c))
            return 0;
    }
    return -1;
}

/**
 * @brief  检测是否有按键按下(非阻塞)
 * @param  key 输出字节
 * @return true 收到;false 空
 */
bool GetKeyPressed(uint8_t *key)
{
    return ReceiveByte(key);
}

/* -------------------- 发送一个字节 -------------------- */
/**
 * @brief  发送单个字符(自动切换方向)
 * @param  c 待发送字符
 */
static void SerialPutChar(uint8_t c)
{
    SWITCH_RS485_TO_TX();                                    /* 方向 = 发送 */
    usart_data_transmit(g_uartHwInfo.uartNo, (uint8_t)c);    /* 发送数据 */
    while (RESET == usart_flag_get(g_uartHwInfo.uartNo, USART_FLAG_TC)); /* 等待完成 */
    SWITCH_RS485_TO_RX();                                    /* 方向 = 接收 */
}

/**
 * @brief  对外发送接口
 */
void SendByte(uint8_t c)
{
    SerialPutChar(c);
}

/* -------------------- printf 重定向 -------------------- */
/**
 * @brief  printf 重定向到 RS485
 */
int fputc(int ch, FILE *f)
{
    SWITCH_RS485_TO_TX();
    usart_data_transmit(g_uartHwInfo.uartNo, (uint8_t)ch);
    while (RESET == usart_flag_get(g_uartHwInfo.uartNo, USART_FLAG_TC));
    SWITCH_RS485_TO_RX();
    return ch;
}

main.c

/**
 *******************************************************************************
 * @file    boot.c
 * @brief   简易 BootLoader 入口
 *          1. 上电倒计时,超时自动跳 APP
 *          2. 串口菜单:下载 / 执行 APP
 *          3. 使用 YMODEM 协议接收新固件并烧写内部 Flash
 *******************************************************************************
 */

#include <stdint.h>
#include <stdio.h>
#include "systick.h"
#include "rs485_drv.h"
#include "delay.h"
#include "update.h"
#include "gd32f30x.h"

/* -------------------- 宏定义 -------------------- */
#define BOOT_DELAY_COUNT   20000U          /* 倒计时 20 s */
#define RAM_START_ADDRESS  0x20000000U     /* RAM 起始地址 */
#define RAM_SIZE           0x10000U         /* RAM 大小 64 KB */

#define DOWNLOAD_KEY_VALUE 0x31            /* 字符 '1' */
#define EXECUTE_KEY_VALUE  0x32            /* 字符 '2' */

/* -------------------- 函数指针类型 -------------------- */
typedef void (*pFunction)(void);

/* -------------------- 外设初始化 -------------------- */
/**
 * @brief  初始化串口、延时、滴答定时器
 */
static void DrvInit(void)
{
    RS485DrvInit();   /* 串口 485/232 驱动 */
    DelayInit();      /* 毫秒延时 */
    SystickInit();    /* 系统滴答 */
}

/* -------------------- 跳转到 APP -------------------- */
/**
 * @brief  检查 APP 栈顶合法性后跳转
 * @note   栈顶地址位于 APP 中断向量表第 1 个字(偏移 0)
 *         复位向量位于第 2 个字(偏移 4)
 */
static void BootToApp(void)
{
    /* 读取 APP 栈顶地址 */
    uint32_t stackTopAddr = *(volatile uint32_t *)APP_ADDR_IN_FLASH;//读取APP的烧写地址

    /* 判断栈顶是否在 RAM 合法范围 */
    if (stackTopAddr > RAM_START_ADDRESS &&
        stackTopAddr < (RAM_START_ADDRESS + RAM_SIZE))
    {
        __disable_irq();                 /* 关全局中断 */
        __set_MSP(stackTopAddr);         /* 设置主栈指针 */

        /* 获取 APP 复位向量 */
        uint32_t resetHandlerAddr = *(volatile uint32_t *)(APP_ADDR_IN_FLASH + 4);//获取复位函数地址
        pFunction JumpToApplication = (pFunction)resetHandlerAddr;

        /* 跳转到复位函数 */
        JumpToApplication();
    }

    /* 非法则重启 */
    NVIC_SystemReset();
}

/* -------------------- 串口菜单 -------------------- */
/**
 * @brief  倒计时 + 交互菜单
 *         超时自动跳 APP;按键进入下载/执行选择
 */
static void MainMenuCmd(void)
{
    uint8_t serialKey;
    uint32_t timCount = GetSysRunTime();   /* 当前已运行时间(ms) */
    uint8_t bootDelayNow = 0, bootDelayLast = 0;

    printf("\rHit any key to stop autoboot:  ");

    /* 20 秒倒计时 */
    while ((timCount < BOOT_DELAY_COUNT) && !GetKeyPressed(&serialKey))
    {//如果倒计时没结束没接收到上位机的任意按键
        timCount = GetSysRunTime();  
        bootDelayNow = (BOOT_DELAY_COUNT - timCount) / 1000; /* 剩余秒数 */
        if (bootDelayNow != bootDelayLast)                   /* 每秒刷新一次 */
        {//一秒变化时再打印
            printf("\b\b%2d", bootDelayNow);//\b\b可以让数据在原本位置打印数据
            bootDelayLast = bootDelayNow;
        }
    }

    /* 倒计时结束 → 直接启动 APP */
    if (timCount >= BOOT_DELAY_COUNT)
    {
        BootToApp();
    }

    /* 用户按了任意键 → 进入菜单 */
    while (1)
    {
        printf("\r\n\n======================= Main Menu ============================\r\n\n");
        printf("************[1].Download Image To Internal Flash*************\r\n\n");
        printf("************[2].Execute The APP******************************\r\n\n");
        printf("\r\n==============================================================\r\n\n");

        /* 等待用户选择 */
        while (!GetKeyPressed(&serialKey));

        if (serialKey == DOWNLOAD_KEY_VALUE)   /* '1' */
        {
            UpdateApp();                       /* YMODEM 接收并烧写 */
        }
        else if (serialKey == EXECUTE_KEY_VALUE) /* '2' */
        {
            BootToApp();                       /* 立即跳转 APP */
        }
    }
}

/* -------------------- 主函数 -------------------- */
int main(void)
{
    DrvInit();          /* 初始化外设 */

    while (1)
    {
        MainMenuCmd();  /* 进入菜单循环 */
    }
}

我们打开CRT点击闪电图标新建连接

选择连接485的端口

我们给单片机进行复位:

可以看到倒计时;

随机按下键盘任意键可以看到菜单

按下键盘1可以进行固件(程序烧写)bin

当出现C,就可以进行烧录

点击Transfer 选择Send Ymodem选择你要烧录的bin文件

点击ok

注意我们烧写的bin文件前 ,烧写程序的工程要修改一下

设置app程序区域大小为500kb起始地址为0x8003000,前面的12kb为引导启动

言归正传,点击ok,会开始烧录程序

烧录完成:


网站公告

今日签到

点亮在社区的每一天
去签到