-
基於STM32F030R8Tx爲例來剖析串口升級,本例程分爲三個部分
- STM32應用程序部分
- STM32 bootloader部分
- 上位機串口通信分發升級包部分
-
上位機與STM32之間的串口數據通信協議約定
包頭 | 包長度 | 命令 | 0~n字節數據 | 校驗和 |
0xff | 0x** | 0x** | 0x**....0x** | 0x** |
-
校驗和計算方法
從包頭開始到校驗和之前的所有數據累加和,取低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; i<4; i++)
{
buf[i] = version_buf[i];
}
buf[4] = enter_stm32_dfu;
USART_Send_Data(UART_GET_VERSION,buf,5);
}
9. 上位機發送STM32_ENTER_DFU命令到stm32,收到此命令後會在flash最後一個扇區將升級包的版本和升級標誌寫入到
flash中,然後啓動軟件復位進入bootloader引導程序,寫入flash實現如下
/**
* @brief 寫入版本
*/
void write_version_to_flash(uint8_t *dfu_version)
{
HAL_FLASH_Unlock();
EraseInitStruct.TypeErase = FLASH_TYPEERASE_PAGES;
EraseInitStruct.PageAddress = FLASH_USER_START_ADDR;
EraseInitStruct.NbPages = (FLASH_USER_END_ADDR - FLASH_USER_START_ADDR) / FLASH_PAGE_SIZE;
uint32_t PageError;
HAL_StatusTypeDef err_code = HAL_FLASHEx_Erase(&EraseInitStruct, &PageError);
APP_ERROR_CHECK(err_code);
Address = FLASH_USER_START_ADDR;
uint8_t version_buf[] = FIRMWARE_VERSION_DEF;
uint32_t content = *(uint32_t*)version_buf;
err_code = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, Address, content);
APP_ERROR_CHECK(err_code);
Address += 4;
if(dfu_version)
content = *(uint32_t*)dfu_version;
else
content = U32_MAX_VALUE;
err_code = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, Address, content);
APP_ERROR_CHECK(err_code);
/* Lock the Flash to disable the flash control register access (recommended
to protect the FLASH memory against possible unwanted operation) *********/
HAL_FLASH_Lock();
}
/**
* @brief 進入DFU
*/
static void enter_dfu(void)
{
if(enter_stm32_dfu)
{
enter_stm32_dfu = 0;
write_version_to_flash(from_app_stm32_version);
NRF_LOG_INFO("STM32_ENTER_DFU");
delay_ms(100);
NVIC_SystemReset();
}
}
-
STM32 bootloader部分
1. 由於bootloader打算分配12KB空間,則flash和RAM配置如下
2. bootloader程序會先從最後一個扇區讀取OTA升級標誌
#define FLASH_USER_START_ADDR ADDR_FLASH_PAGE_63 /* Start @ of user Flash area */
#define FLASH_USER_END_ADDR ADDR_FLASH_PAGE_63 + FLASH_PAGE_SIZE /* End @ of user Flash area */
uint32_t JumpAddress;
pFunction Jump_To_Application;
uint32_t Address = 0;
__IO uint32_t read_flash_value = 0;
/*Variable used for Erase procedure*/
static FLASH_EraseInitTypeDef EraseInitStruct;
uint8_t version_buffer[4];
uint8_t dfu_flag;
3.如果dfu_flag爲0表示無需升級,由以下代碼進行跳轉到0x08003000地址
if (((*(__IO uint32_t*)ApplicationAddress) & 0x2FFE0000 ) == 0x20000000)
{
NRF_LOG_INFO("enter app");
//跳轉至用戶代碼
EXTI->PR = 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 | 校驗和 | |||
0xff | 0x10 | 0xd9 | 0x73 | 0x70 | 0x69 | 0x2e | 0x62 | 0x69 | 0x6e | 0x00 | 0x00 | 0x00 | 0x72 | 0x94 | 0xa1 |
8.stm收到UART_DFU_FILE命令會對文件名和長度的有效性等進行判斷
9.stm32回覆UART_DFU_DOWNLOAD命令給上位機,上位機收到此命令後開始傳輸升級包內容,升級包內容格式內如下
數據長度超過128,則按(128+8)的包長髮送,不足128的數據,則發送(餘下的數據+8)的包長髮送
包頭 | 包長 | 命令 | 包序號從0開始遞增 | 升級包數據(1~128Byte) | 校驗和 | ||||||||||
0xff | 0x10 | 0xda | 0x00 | 0x00 | 0x00 | 0x00 | 0x** | 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<SerialPortInfo> 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