- 基于STM32F030R8Tx为例来剖析串口升级,本例程分为三个部分
- STM32应用程序部分
- STM32 bootloader部分
- 上位机串口通信分发升级包部分
- 上位机与STM32之间的串口数据通信协议约定
- 校验和计算方法
从包头开始到校验和之前的所有数据累加和,取低8位数据作为整个包的检验和
例如 0xff 0x4 0x01,无数据部分,则检验和为0xff + 0x04 + 0x01 = 0x104
取低8位数据作为整包的检验和,则校验和是0x04,那么完整的数据包是 0xff 0x04 0x01 0x04
- STM32用户应用程序部分
1. 整个程序由bootloader引导程序和用户应用程序两部分组成,这里分配12KB的flash空间给bootloader使用,那么用户应用
程序flash地址则要从0x8000000的基础上再偏移0x3000,则flash的起始地址为0x8003000,由于STM32F030R8Tx的flash
的大小为 64KB,那么应用程序可用的flash空间为0x10000减去bootloader的大小0x3000等于0xd000
2.应用程序的RAM起始地址计算,由于bootloader跳转后要复制启动文件中的向量表到RAM中,所以至少要分配的RAM大小
根据启动文件中的向量表决定,stm32f030R8Tx启动文件startup_stm32f030x8.s中的向量部分内部如下,共有45个DCD
定义,每个占4个字节,则复制向量表总共所需占用的RAM为45*4=180个字节(0xb4),那么应用程序的起始RAM要从
0x20000000加上0xb4,即0x200000B4,如果stm32f030R8Tx的RAM大小为8KB,那么应用程序可用RAM大小
为0x2000-0xb4=0x1F4C
__Vectors DCD __initial_sp ; Top of Stack 1
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler ; Hard Fault Handler
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD SVC_Handler ; SVCall Handler
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD PendSV_Handler ; PendSV Handler
DCD SysTick_Handler ; SysTick Handler 16
; External Interrupts
DCD WWDG_IRQHandler ; Window Watchdog
DCD 0 ; Reserved
DCD RTC_IRQHandler ; RTC through EXTI Line
DCD FLASH_IRQHandler ; FLASH
DCD RCC_IRQHandler ; RCC
DCD EXTI0_1_IRQHandler ; EXTI Line 0 and 1
DCD EXTI2_3_IRQHandler ; EXTI Line 2 and 3
DCD EXTI4_15_IRQHandler ; EXTI Line 4 to 15
DCD 0 ; Reserved
DCD DMA1_Channel1_IRQHandler ; DMA1 Channel 1
DCD DMA1_Channel2_3_IRQHandler ; DMA1 Channel 2 and Channel 3
DCD DMA1_Channel4_5_IRQHandler ; DMA1 Channel 4 and Channel 5
DCD ADC1_IRQHandler ; ADC1
DCD TIM1_BRK_UP_TRG_COM_IRQHandler ; TIM1 Break, Update, Trigger and Commutation 30
DCD TIM1_CC_IRQHandler ; TIM1 Capture Compare
DCD 0 ; Reserved
DCD TIM3_IRQHandler ; TIM3
DCD TIM6_IRQHandler ; TIM6
DCD 0 ; Reserved
DCD TIM14_IRQHandler ; TIM14 36
DCD TIM15_IRQHandler ; TIM15
DCD TIM16_IRQHandler ; TIM16
DCD TIM17_IRQHandler ; TIM17
DCD I2C1_IRQHandler ; I2C1
DCD I2C2_IRQHandler ; I2C2 41
DCD SPI1_IRQHandler ; SPI1
DCD SPI2_IRQHandler ; SPI2
DCD USART1_IRQHandler ; USART1
DCD USART2_IRQHandler ; USART2 45
__Vectors_End
3.根据上述计算,在keil中配置flash和RAM如下
4. STM32F030R8Tx从bootloader跳转到应用程序,需要在进入main函数第一时间将0x8003000起始地址开始复制向量表
到0x20000000起始的RAM中,复制内容的大小是上面计算出的0xb4,那么添加以下代码实现
5.串口通信校验和计算
/**
* @brief 计算checksum
* @param [in] uint8_t const*
* - 待计算的数据
* @param [in] uint8_t
* - len 计算数据的长度
* @return uint8_t
* - checksum计算结果
*
*/
static uint8_t get_checksum(uint8_t const*dat, uint8_t len)
{
uint8_t chksum = 0;
for (uint8_t i = 0; i < len; i++)
chksum += dat[i];
return chksum;
}
6. 通信实现2个命令,一个是获取硬件版本命令,另一个是进入OTA升级命令,定义如下
typedef enum
{
UART_GET_VERSION = 0x01,
STM32_ENTER_DFU = 0X02,
UART_HEAD = 0xff,
}E_UART_COMMAND;
7.本例程版本使用4个字节的字符串格式,格式为Vx.x,其中V和.为固定格式,x为任意字符,版本在sdk_config.h中定义,
定义如下
8.上位机发送UART_GET_VERSION到stm32,收到此命令后会回复硬件版本给上位机
/**
* @brief 回复版本
*/
void reply_stm32_version(void)
{
uint8_t buf[5];
uint8_t version_buf[] = FIRMWARE_VERSION_DEF;
for(uint8_t i=0; iPR = 0x1fffff;
JumpAddress = *(__IO uint32_t*) (ApplicationAddress + 4);
Jump_To_Application = (pFunction) JumpAddress;
//初始化用户程序的堆栈指针
__set_MSP(*(__IO uint32_t*) ApplicationAddress);
Jump_To_Application();
}
else
{
NRF_LOG_INFO("no user Program");
}
while(1);
4.如果dfu_flag为1表示需要升级,会先初时化LCD并显示0%,然后擦除从0x08003000地址到最后一个扇区前的所有空间
5. bootloader串口命令定义
typedef enum
{
UART_GET_VERSION = 0x01,
UART_DFU_FILE = 0XD9,
UART_DFU_DOWNLOAD = 0XDA,
UART_HEAD = 0xff,
}E_UART_COMMAND;
6.初时化串口,并发送硬件版本和OTA标志到上位机,请求上位机发送升级包内容
7.上位机收到OTA标志,将升级包的文件名称和文件长度发给stm32,例如 spi.bin升级文件,假如文件长度为0x7294,则发送的数据如下
包头包长命令 s p i .b i n \0 升级包文件长度29332校验和0xff0x100xd90x730x700x690x2e0x620x690x6e0x000x000x000x720x940xa18.stm收到UART_DFU_FILE命令会对文件名和长度的有效性等进行判断
9.stm32回复UART_DFU_DOWNLOAD命令给上位机,上位机收到此命令后开始传输升级包内容,升级包内容格式内如下
数据长度超过128,则按(128+8)的包长发送,不足128的数据,则发送(余下的数据+8)的包长发送
包头包长命令 包序号从0开始递增 升级包数据(1~128Byte)校验和0xff0x100xda0x000x000x000x000x**0x**0x**0x**0x**0x**0x**0x**0x**10.stm32收到升级包后会对包序号是否连续进行判断,如果不连续则说明丢包,升级会超时重启
/**
* @brief 升级超时
*/
void dfu_timeout_handler(void)
{
if(dfu_timeout)
{
if(--dfu_timeout == 0)
{
NVIC_SystemReset();
}
}
}
11.如果接收的包连续,则会一直写入到flash中
12.升级包百分比计算
当前所写入的总长度与升级包文件长度的比,得出升级进度百分比
calcPercent = readTotalLen;
calcPercent *= 100;
dfu_percent = calcPercent / fileLength;
13.升级完成后,dfu_flag清0,result设成0表示升级完成
if(readTotalLen == fileLength)
{
dfu_flag = 0;
result = 0;
}
14.dfu_flag为0,退出循环
15.擦除OTA标志,发送OTA完成标志给上位机,最后stm32复位重启
- 上位机串口通信分发升级包部分
1.基于github上的例程进行修改,使用VS2019打开解决方案
2.定义串口类对象
CSerialPort m_SerialPort;//About CSerialPort
3.获取已插入的有效串口号
//获取串口号
vector m_portsList = CSerialPortInfo::availablePortInfos();
TCHAR m_regKeyValue[255];
for (unsigned int i = 0; i < m_portsList.size(); i++)
{
#ifdef UNICODE
int iLength;
const char * _char = m_portsList[i].portName.c_str();
iLength = MultiByteToWideChar(CP_ACP, 0, _char, strlen(_char) + 1, NULL, 0);
MultiByteToWideChar(CP_ACP, 0, _char, strlen(_char) + 1, m_regKeyValue, iLength);
#else
strcpy_s(m_regKeyValue, 255, m_portsList[i].portName.c_str());
#endif
m_PortNr.AddString(m_regKeyValue);
m_PortNr2.AddString(m_regKeyValue);
}
m_PortNr.SetCurSel(0);
m_PortNr2.SetCurSel(0);
3. 准备串口连接
m_SerialPort.readReady.connect(this, &CCommDlg::OnReceive);
4.打开和关闭串口,波特率固定为115200,无奇偶校验,8位数据长度,1位停止位
void CCommDlg::OnBnClickedButtonOpenClose()
{
// TODO: 在此添加控件通知处理程序代码
//GetDlgItem(IDC_SendEdit)->SetFocus();
CString temp;
m_OpenCloseCtrl.GetWindowText(temp);///获取按钮的文本
UpdateData(true);
if (temp == _T("关闭串口"))///表示点击后是"关闭串口",也就是已经关闭了串口
{
m_SerialPort.close();
m_OpenCloseCtrl.SetWindowText(_T("打开串口"));///设置按钮文字为"打开串口"
}
///打开串口操作
else if (m_PortNr.GetCount() > 0)///当前列表的内容个数
{
string portName;
int SelBaudRate;
int SelParity;
int SelDataBits;
int SelStop;
UpdateData(true);
m_PortNr.GetWindowText(temp);///CString temp
#ifdef UNICODE
portName = CW2A(temp.GetString());
#else
portName = temp.GetBuffer();
#endif
SelBaudRate = 115200;
SelParity = 0;
SelDataBits = 8;
SelStop = 0;
m_SerialPort.init(portName, SelBaudRate, itas109::Parity(SelParity), itas109::DataBits(SelDataBits), itas109::StopBits(SelStop));
m_SerialPort.open();
if (m_SerialPort.isOpened())
{
m_OpenCloseCtrl.SetWindowText(_T("关闭串口"));
}
else
{
AfxMessageBox(_T("串口已被占用!"));
}
}
else
{
AfxMessageBox(_T("没有发现串口!"));
}
}
5.关闭窗口释放串口资源
void CCommDlg::OnClose()
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
m_SerialPort.close();
m_SerialPort2.close();
if (pOTA_file_buf != NULL) {
delete pOTA_file_buf;
pOTA_file_buf = NULL;
}
CDialogEx::OnClose();
}
6. 获取升级包文件名和长度
void CCommDlg::OnBnClickedButtonOpenClose4()
{
// TODO: 在此添加控件通知处理程序代码
// 设置过滤器
TCHAR szFilter[] = _T("文本文件(*.bin)|*.bin|所有文件(*.*)|*.*||");
// 构造打开文件对话框
CFileDialog fileDlg(TRUE, _T("txt"), NULL, 0, szFilter, this);
CString strFilePath;
if (IDOK == fileDlg.DoModal())
{
// 如果点击了文件对话框上的“打开”按钮,则将选择的文件路径显示到编辑框里
strFilePath = fileDlg.GetPathName();
SetDlgItemText(IDC_EDIT2, strFilePath);
CFile file;
file.Open(strFilePath, CFile::typeBinary, NULL);
file_len = (DWORD)file.GetLength();
char buf[200];
sprintf(buf, "%d", file_len);
CString str_temp((char*)buf);
m_file_size.SetWindowTextW(str_temp);
m_ota_file_name = file.GetFileName(); //获取升级包文件名
if (pOTA_file_buf) //如果指针为非空,先释放内存
{
delete pOTA_file_buf;
pOTA_file_buf = NULL;
}
if (pOTA_file_buf == NULL) // 分配内存
{
pOTA_file_buf = new char[file_len];
file.Read(pOTA_file_buf, file_len); // 读取升级包所有内容
}
file.Close();
}
}
7.串口接收监听OnReceive,
void CCommDlg::OnReceive()
{
int iRet = m_SerialPort.readAllData(uart_receive_buf);
if (iRet > 0)
{
if (uart_receive_buf[0] != (char)0xff) //包头检查
return;
BYTE len = uart_receive_buf[1];
if (len < 4 || len > 150) // 长度限制检查
return;
len -= 1;
char checksum = calc_checksum((BYTE*)uart_receive_buf, len);
if (checksum != uart_receive_buf[len]) // 校验和检查
return;
switch ((BYTE)uart_receive_buf[2])
{
case 0x01: // 获取版本及OTA标志
{
if (uart_receive_buf[3 + 4]) // 升级标志
{
m_dfu_status.SetWindowTextW(_T("升级中..."));
if ((pOTA_file_buf != NULL) && (file_len > 0)) {
m_dfu_index = 0;
package_index = 0;
BYTE len = m_ota_file_name.GetLength();
char send_buf[128];
BYTE index = 0;
send_buf[index++] = (char)0xff;
send_buf[index++] = len + 9;
send_buf[index++] = (char)0XD9; //文件名和长度发送命令
for (DWORD i = 0; i < len; i++)
{
send_buf[index++] = (char)m_ota_file_name.GetAt(i);
}
send_buf[index++] = '\0';
send_buf[index++] = (file_len >> 24) & 0xff;
send_buf[index++] = (file_len >> 16) & 0xff;
send_buf[index++] = (file_len >> 8) & 0xff;
send_buf[index++] = file_len & 0xff;
send_buf[index] = calc_checksum((BYTE*)send_buf, index);
m_SerialPort.writeData(send_buf, index+1);
}
else
{
m_dfu_status.SetWindowTextW(_T("请选择升级bin文件"));
}
}
else
{
m_dfu_status.SetWindowTextW(_T("应用程序"));
}
uart_receive_buf[3 + 4] = '\0';
CString str((char*)&uart_receive_buf[3]);
m_version_name.SetWindowTextW(str);
}
break;
case 0XDA: // 发送升级包内容
if ((pOTA_file_buf != NULL) && (file_len > 0)) {
switch (uart_receive_buf[4])
{
case 0:
case 1:
{
char buf[200];
sprintf(buf, "升级完成 %d%%", uart_receive_buf[3]);
CString temp((char*)buf);
m_dfu_status.SetWindowTextW(temp); //升级进度
if (m_dfu_index >= file_len)
break;
DWORD len = 128;
if ((m_dfu_index + 128) > file_len) // 不够128
{
len = file_len - m_dfu_index;
}
buf[0] = (char)0xff;
buf[1] = (char)(len+8);
buf[2] = (char)0xda;
buf[3] = (package_index >> 24) & 0xff;
buf[4] = (package_index >> 16) & 0xff;
buf[5] = (package_index >> 8) & 0xff;
buf[6] = package_index & 0xff;
for (DWORD i = 0; i < len; i++) {
buf[7 + i] = pOTA_file_buf[m_dfu_index + i];
}
m_dfu_index += len;
package_index++; //包序号递增
len += 7;
buf[len] = (char)calc_checksum((BYTE*)buf, (BYTE)len);
m_SerialPort.writeData(buf, len+1); //发送升级包
}
break;
case 2:
m_dfu_status.SetWindowTextW(_T("升级失败!"));
break;
default:
break;
}
}
else
{
m_dfu_status.SetWindowTextW(_T("请选择升级bin文件"));
}
break;
default:
break;
}
CString str1;
if (m_nHexCheck)
{
char buf[2048];
hex_disp_buffer(uart_receive_buf, buf, iRet);
str1 = ((char*)buf);
str1 += "\n";
}
else
{
str1 = (char*)uart_receive_buf;
}
m_ReceiveCtrl.SetSel(-1, -1);
m_ReceiveCtrl.ReplaceSel(str1);
}
}
- 运行结果:

- DEMO下载地址:
https://download.csdn.net/download/mygod2008ok/12579383