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,会开始烧录程序
烧录完成: