Khi làm 1 project có tính thương mại, việc update và bảo trì chương trình cho khách hàng nhất thiết phải có, bạn không thể đưa mạch nạp cho khách rồi bảo họ làm theo hướng dẫn để nạp file hex được. Thay vào đó chúng ta sẽ update firmwave khách qua chương trình bootloader
Một vài ứng dụng của chương trình bootloader
- Nạp code không cần mạch nạp gốc, chỉ cần 1 module UART-USB bất kì
- Làm mạch nạp offline (máy nạp code hàng loạt)
- Tạo chức năng FOTA (Firmware Over The Air) tức là cập nhật từ xa qua internet, wifi, bluetooth
Về phần lí thuyết các bạn có thể tìm hiểu về OTA tại đây:
- https://tapit.vn/cap-nhat-chuong-trinh-tu-xa-tren-vi-dieu-khien-stm32-p1/
- https://tapit.vn/cap-nhat-chuong-trinh-tu-xa-tren-vi-dieu-khien-stm32-firmware-air-fota-p2/
- https://tapit.vn/cap-nhat-chuong-trinh-tu-xa-tren-vi-dieu-khien-stm32-firmware-air-fota-p3/
- https://tapit.vn/cap-nhat-chuong-trinh-tu-xa-tren-vi-dieu-khien-stm32-firmware-air-fota-p4/
- https://tapit.vn/cap-nhat-chuong-trinh-tu-xa-tren-vi-dieu-khien-stm32-firmware-air-fota-p5/
DEMO chương trình bootloader
Do stm32f103c8t6 có 128 Page bắt đầu từ 0 và mỗi page là 128KB nên mình sẽ tạo 1 buffer đúng bằng kích cỡ của 1page, bởi mỗi lần ghi vào flash chúng ta phải ghi cả page chứ không thể ghi lẻ tẻ được.
Khơi tạo ram cho 1 page
1 |
unsigned char code_save[1024]; |
Chương trình xóa page, do mỗi lần ghi vào xóa page đó đi
1 2 3 4 5 6 7 8 9 10 11 12 |
void Erase_PAGE(uint32_t addr) { Flash_Unlock(); while((FLASH->SR&FLASH_SR_BSY)); FLASH->CR |= FLASH_CR_PER; //Page Erase Set FLASH->AR = addr; //Page Address FLASH->CR |= FLASH_CR_STRT; //Start Page Erase while((FLASH->SR&FLASH_SR_BSY)); FLASH->CR &= ~FLASH_SR_BSY; FLASH->CR &= ~FLASH_CR_PER; //Page Erase Clear Flash_Lock(); } |
Chương trình đọc 1byte kiểu 16 ra từ 1 địa chỉ
1 2 3 4 5 |
uint16_t F_read(uint32_t addr) { uint16_t* val = (uint16_t *)addr; return *val; } |
Chương trình ghi data vào 1 page + check lại, chúng ta sẽ cần nhập vào tham số là page vần ghi và size (thực ra cái này size mặc định là 1024 rồi)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
char Write_PAGE(uint32_t size,uint32_t page) { uint32_t ADDRESS_PAGE = ((uint32_t)0x08000000 + (page*1024)); //tinh dia chi cua page uint32_t addr; uint16_t data; Flash_Erase(ADDRESS_PAGE); Flash_Unlock(); for(int i=0;i<size/2;i++) { addr = ADDRESS_PAGE + (i*2); FLASH->CR |= FLASH_CR_PG; /*!< Programming */ while((FLASH->SR&FLASH_SR_BSY)); data=((uint16_t)code_save[i*2+1]<<8) | ((uint16_t)code_save[i*2]); *(__IO uint16_t*)addr = data; while((FLASH->SR&FLASH_SR_BSY)); FLASH->CR &= ~FLASH_CR_PG; } //check lại for(int i=0;i<size/2;i++) { addr = ADDRESS_PAGE + (i*2); data=((uint16_t)code_save[i*2+1]<<8) | ((uint16_t)code_save[i*2]); uint16_t test2=F_read(addr); if(data != test2) { Flash_Lock(); return 1; } } Flash_Lock(); return 0 ; } |
Phương pháp giao tiếp với máy tính
Đầu tiên phần mềm sẽ gửi dữ liệu gồm số lượng byte của code, ở dưới đọc xong sẽ phẩn hồi OK cho app biết nó sẵn sàng nhận data. Sau đó app sẽ gửi liên tục gửi 1page (1024byte) xuống cho ở dưới nạp vào flash, sau khi nạp xong sẽ nhảy tới phần vùng thực thi code
Chương trình ngắt UART nhận dữ liệu
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(flag==0) { rx_buff[count++]=rx_data; if(rx_data == '\n') { flag=1; int ts=1; size_program=0;size_count=0;end=0; page_write=PROGRAM_START_PAGE; for(count=count-2;count>0;count--) { if(rx_buff[count] == '=')break; size_program+= (rx_buff[count]-48)*ts; ts*=10; } if(size_program>1024)HAL_UART_Receive_IT(&huart2,code_save,1024); else HAL_UART_Receive_IT(&huart2,code_save,size_program); HAL_UART_Transmit(&huart2,(uint8_t *)"OK MEN !\r\n",10,100); } else { HAL_UART_Receive_IT(&huart2,(uint8_t *)&rx_data,1); } } else if(flag==1) { if(end==1) { HAL_UART_Receive_IT(&huart2,(uint8_t *)&rx_data,1); if(Write_PAGE(1024,page_write) == 0)HAL_UART_Transmit(&huart2,(uint8_t *)"Read OK !\r\n",11,100); else { flag=0; HAL_UART_Transmit(&huart2,(uint8_t *)"Write Fall !\r\n",14,100); HAL_UART_Receive_IT(&huart2,(uint8_t *)&rx_data,1); } clear_buffer(); flag=0; goto_aplication=1; //chuyen toi ct BOOT return; } size_count+=1024; if((size_count+1024) < size_program) HAL_UART_Receive_IT(&huart2,code_save,1024); else { HAL_UART_Receive_IT(&huart2,code_save,size_program-size_count); end=1; } if(Write_PAGE(1024,page_write) == 0)HAL_UART_Transmit(&huart2,(uint8_t *)"Read OK !\r\n",11,100); else { flag=0; HAL_UART_Transmit(&huart2,(uint8_t *)"Write Fall !\r\n",14,100); HAL_UART_Receive_IT(&huart2,(uint8_t *)&rx_data,1); } page_write++; clear_buffer(); } } |
Sau khi nhận dầy đủ mình sẽ set cờ goto_aplication=1
Ở hàm main check cờ và nhảy tới vùng code thực thi
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
if(goto_aplication==1) { void (*SysMemBootJump)(void); volatile uint32_t addr = PROGRAM_START_ADDRESS; #if defined(USE_HAL_DRIVER) HAL_RCC_DeInit(); #endif /* defined(USE_HAL_DRIVER) */ #if defined(USE_STDPERIPH_DRIVER) RCC_DeInit(); #endif /* defined(USE_STDPERIPH_DRIVER) */ SysTick->CTRL = 0; SysTick->LOAD = 0; SysTick->VAL = 0; __disable_irq(); #if defined(STM32F4) SYSCFG->MEMRMP = 0x01; #endif #if defined(STM32F0) SYSCFG->CFGR1 = 0x01; #endif SysMemBootJump = (void (*)(void)) (*((uint32_t *)(addr + 4))); __set_MSP(*(uint32_t *)addr); SysMemBootJump(); } |
Demo chương trình c# gửi dữ liệu
Hàm mở code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
private void button3_Click(object sender, EventArgs e) { OpenFileDialog diaglog = new OpenFileDialog(); diaglog.Filter = "HEX files|*.hex"; if (diaglog.ShowDialog() == DialogResult.OK) { for (int i = 0; i < max_size; i++) data_send[i] = 0xFF; //clear bộ đệm gửi program_size = 0; hex_data = File.ReadAllText(diaglog.FileName); textBox1.Text = hex_data; int _2c_line1 = hex_data.IndexOf(":"); int _2c_line_end = hex_data.LastIndexOf(":"); hex_data = hex_data.Substring(_2c_line1+1, _2c_line_end-1); // bỏ dòng 1 và dòng cuối _2c_line1 = hex_data.IndexOf(":"); _2c_line_end = hex_data.LastIndexOf(":"); hex_data = hex_data.Substring(_2c_line1, _2c_line_end - _2c_line1 - 1 ); // bỏ dòng 1 và dòng cuối //tiến hành lọc lấy phần mã hex while (true) { int count = 0; count = hex_data.IndexOf(":"); //lấy vị tri bắt đầu if (count != -1) //nếu còn dữ liệu { byte[] data_num = StringToByteArrayFastest(hex_data.Substring(count + 1, 2)); //kiểm tra số lượng byte trong hàng int byte_num = (int)data_num[0]; byte[] data_line = new byte[byte_num]; //số lượng byte data trong line đó data_line = StringToByteArrayFastest(hex_data.Substring(count + 9, byte_num * 2)); //đưa data vào mảng for(int i=0;i< byte_num;i++) data_send[program_size++] = data_line[i]; //lấy xong data thì cắt bỏ luôn hex_data =hex_data.Substring(count+1); } else { label1.Text = "0/" + program_size.ToString() + " byte"; return; } } } } |
Hàm nạp code xuống
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
private async void button4_Click(object sender, EventArgs e) { // nạp xuống từng byte data một int dem = 0; //gửi thông điệp upload code serialPort.Write("Upload=" + program_size.ToString() + "\n"); flag_check = 0; await Task.Delay(50); //chờ phản hồi từ mạch if (flag_check == 1) { while (true) { if(program_size/1024 != dem)serialPort.Write(data_send, dem * 1024, 1024); //tryền từng mảng 1024 byte 1 lần else serialPort.Write(data_send, dem * 1024, program_size-(1024*dem)); //truyền nốt phần data cuối flag_check = 0; timer1.Enabled = true; await Task.Delay(500); //chờ phản hồi if (flag_check == 0) { MessageBox.Show("Lỗi"); return; } if (flag_check == 2) { MessageBox.Show("Lỗi khi ghi"); return; } dem++; String vl = (dem * 1024).ToString() + "/" + program_size.ToString() + " byte"; if (label1.InvokeRequired) label1.Invoke(new Action(() => label1.Text = vl)); else { label1.Text = vl; Application.DoEvents(); } //cập nhật quá trình nạp if (dem*1024 > program_size) { label1.Text = "Nạp thành công !"; MessageBox.Show("Xong"); return; } } } else { MessageBox.Show("Kiểm tra kết nối !"); } } |
Chương trình demo
Chương trình code demo mình sẽ cho gửi 1 đoạn văn bản lên mỗi 500 mili giây. Do phần vùng của chương trình này bắt đầu ở địa chỉ 0x08002000 nên ngay khi vào hàm main, chúng ta dời bảng vector ngắt đến địa chỉ này, đồng thười cũng bật isr lên luôn
1 2 3 4 5 6 |
int main(void) { /* USER CODE BEGIN 1 */ SCB->VTOR = (uint32_t)0x08002000; __enable_irq(); /* USER CODE END 1 */ |
Khi biên dịch các bạn tích chọn creat file hex trong build option
Trong thẻ tagret sửa địa chỉ bắt đầu thành 0x8002000
Sau đó ấn build để lấy file hex
Khi khởi động mạch, chip sẽ chạy chương trình boot trước, sau khi nạp code xuống thành công chíp sẽ nhảy sang chương trình chính ! Các bạn có thể thêm 1 nút nhấn để khi khởi động chương trình boot sẽ kiểm tra nút nhấn để quyết định xem nó chạy ở bootloader hay nhảy tới phân vùng code chính
DEMO
Download
Tải full project tại đây:
https://drive.google.com/open?id=17tEAYjTLKRL0ZrXpkZRS82JDyFM1v0Ek
128 page và mỗi page là 128KB là sao vậy anh. Em vẫn chưa hiểu chỗ này lắm
Lộn đó bạn. 1KB thui
cho mình hỏi thêm là tại sao trong code c# lại phải bỏ dòng đầu với dòng cuối của hex file vậy bạn
Con Stm32F0 không có bảng VTOR ngắt thì rời vector ngắt sang Application thế nào ạ
mình tải về và làm giống bạn mà khi nạp chương trình thì có hộp thoại thông báo lỗi