当前位置: 首页 > ds >正文

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

 单片机常见烧录方法:

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;elseStatus = 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;elsecrc <<= 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");elseprintf("\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,会开始烧录程序

烧录完成:

http://www.xdnf.cn/news/20248.html

相关文章:

  • 警惕!你和ChatGPT的对话,可能正在制造分布式妄想
  • 计算机网络2 第二章 物理层——用什么方式传输邮件
  • 狗都能看懂的HunYuan3D 1.0详解
  • 一种基于注解与AOP的Spring Boot接口限流防刷方案
  • C#海康车牌识别实战指南带源码
  • VAE(变分自动编码器)技术解析
  • iOS混淆工具实战 在线教育直播类 App 的课程与互动安全防护
  • FairGuard游戏加固产品常见问题解答
  • 云市场周报 (2025.09.05):解读腾讯云AI安全、阿里数据湖与KubeVela
  • C语言中常见的数据结构及其代码实现
  • 数据传输优化-异步不阻塞处理增强首屏体验
  • 自演化大语言模型的技术背景
  • 心理学家称AI大模型交流正在引发前所未见的精神障碍
  • 手把手教你用CUDA Graph:将你的LLM推理延迟降低一个数量级
  • 51单片机------中断系统
  • 51单片机基础day3
  • 开源混合专家大语言模型(DBRX)
  • Spring WebFlux 流式数据拉取与推送的实现
  • UIViewController生命周期
  • Word封面对齐技巧(自制)
  • UE4 UAT 的六大流程 build cook stage pacakge archive deploy 与UAT的参数
  • 硬件(二) 中断、定时器、PWM
  • 当电力设计遇上AI:良策金宝AI如何重构行业效率边界?
  • Linux2.6内核进程O(1)调度队列
  • 电机控制(三)-电机控制方法基础
  • Java集合---Collection接口和Map接口
  • C++:类和对象(中)
  • 在线测评系统---第n天
  • 执行select * from a where rownum<1;,数据库子进程崩溃,业务中断。
  • LabVIEW--二维数组、三维数组、四维数组