单片机常见烧录方法:
、
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,会开始烧录程序
烧录完成: