🌱 Hướng dẫn xử lý file HEX trong Bootloader và FOTA
Trong quá trình thực hiện Update Firmware của Bootloader hay FOTA, việc xử lý các file có định dạng hex (.HEX, .SREC) là rất quan trọng. Chương trình Boot sẽ cần phân tích định dạng file hex đó, lọc ra phần data, kiểm tra tính đúng đắn của dữ liệu và ghi vào bộ nhớ Flash.
Trong khi file hex có dung lượng data khá lớn, thì dữ liệu được truyền qua các giao thức truyền thông (ví dụ UART), chỉ truyền được theo từng Byte. Vì vậy việc cần có một phương pháp xử lý dữ liệu một cách hiệu quả là rất quan trọng, để tránh mất mát và sai lệch dữ liệu.
Bài viết này sẽ hướng dẫn một số phương pháp để nhận và xử lý file HEX, giả sử được nhận qua giao thức UART.
Table of Contents
- Truyền nhận Data trong bài toán FOTA thực tế
- 1. Phương án sử dụng Polling
- 2. Phương án sử dụng UART Interrupt
- 3. Phương án sử dụng UART + DMA
Truyền nhận Data trong bài toán FOTA thực tế
Trên thực tế, có rất nhiều phương án truyền dữ liệu trong File HEX thông qua FOTA, tùy thuộc vào cách quy định frame truyền giữa Server / Wifi chip và/hoặc Vi điều khiển. Cách đơn giản nhất vẫn là truyền trực tiếp file HEX với định dạng đã được build ra sẵn.
Trong bài viết này, mình sẽ đề cập đến phương án xử lý file Intel HEX với phương án là chip Wifi/Bluetooth truyền trực tiếp file HEX sang phía vi điều khiển (hoặc mô phỏng bằng cách truyền trực tiếp file HEX từ PC sang vi điều khiển).
Đối với vi điều khiển, có 3 cách thiết kế chương trình chính khi làm việc với giao thức UART (Polling, Interrupt, DMA).
1. Phương án sử dụng Polling
Phương án đơn giản nhất là nhận UART bằng Polling, tức là nhận được và xử lý từng Byte dữ liệu. Thông thường phương án này là không quá tốt, vì CPU liên tục kiểm tra cờ UART để xem dữ liệu đã sẵn sàng hay chưa, và không thể làm việc khác trong quá trình này.
Vì vậy, khi muốn xử lý hoặc kiểm tra dữ liệu thì CPU sẽ không có thời gian, phương án Polling sẽ thích hợp trong trường hợp:
- Hệ thống nhỏ hoặc khi cập nhật firmware qua UART có tốc độ truyền thấp.
- Vi điều khiển và chip Wifi đã thống nhất với nhau cách truyền, ví dụ Wifi sẽ truyền từng Line Hex một lần, sau đó Vi điều khiển nhận toàn bộ Line đó, phân tích, sau đó phản hồi về để Wifi quyết định gửi Line tiếp theo!
⇒ Phương án này cũng làm giảm tốc độ Update Firmware.
2. Phương án sử dụng UART Interrupt
Phương án này sẽ giúp tránh việc CPU phải chờ đợi UART RX Buffer đầy, thời gian đó CPU có thể xử lý và kiểm tra dữ liệu. Đến đây thì cũng có một số cách làm:
- Xử lý dữ liệu ngay lập tức khi nhận được 1 byte dữ liệu
- Nhận một Line dữ liệu sau đó mới xử lý
- Nhận một số byte dữ liệu sau đó xử lý (ví dụ: nhận 2 byte kí tự Byte Count rồi mới xử lý).
Xử lý dữ liệu ngay lập tức khi nhận được 1 byte dữ liệu
Khi nhận được một byte dữ liệu từ UART, CPU sẽ nhảy vào ngắt, và chúng ta cần xác định xem data mình vừa nhận được thuộc trường nào trong một line HEX.
Note: ví dụ nhận được UART Data = '1' thì cần check xem '1' này là Byte Count hay Address hay Data, ... Vì CPU không hề nhìn thấy toàn bộ File HEX như chúng ta!
Để CPU xác định được ký tự đang nhận thuộc trường nào, chúng ta cần có các cờ để đánh dấu xem chương trình đang nhận đến đâu, ví dụ, nhận được dấu bắt đầu line hex là dấu hai chấm ':', thì đánh dấu lại, hai ký tự tiếp theo sẽ là Bytecount, ....
Để đánh dấu trạng thái của chương trình tại từng thời điểm như trên, dễ thấy ta có thể tổ chức chương trình theo kiểu: State Machine.
Dưới đây là ví dụ xử lý, chú ý thứ tự Endianness trong các file HEX nên phần xử lý data sẽ cần sắp xếp lại cho phù hợp với bộ nhớ vi điều khiển.
Tạo một enum quản lý State Machine
- enum {
- HEX_STARTCODE = 0x0U, // ':'
- HEX_BYTECOUNT,
- HEX_ADDRESS,
- HEX_RECORDTYPE,
- HEX_DATA,
- HEX_CHECKSUM,
- HEX_END_OF_LINE, // "\r\n"
- HEX_DONE, // EOF
- HEX_ERROR, // Invalid Format or checksum
- } eHexState; // Default HEX_STARTCODE
Về cơ bản các State chính sẽ điển hình cho việc UART đang nhận dữ liệu gì trong file HEX.
Hex File Handling State Machine |
- switch(eHexState)
- {
- case HEX_STARTCODE:
- eHexState = (':' == UART_Data) ? HEX_BYTECOUNT : HEX_ERROR;
- break;
- case HEX_BYTECOUNT:
- count++;
- UART_Data = GetHexData(UART_Data);
- if( HEX_INVALID == UART_Data )
- {
- eHexState = HEX_ERROR;
- break;
- }
- stLineData.ByteCount += UART_Data * pow(16, 2-count);
- if( 2 == count )
- {
- eHexState = HEX_ADDRESS;
- count = 0;
- }
- break;
- case HEX_ADDRESS:
- count++;
- UART_Data = GetHexData(UART_Data);
- if( HEX_INVALID == UART_Data )
- {
- eHexState = HEX_ERROR;
- break;
- }
- stLineData.Address += UART_Data * pow(16, 4-count);
- if( 4 == count )
- {
- eHexState = HEX_RECORDTYPE;
- count = 0;
- }
- break;
- // Other States .......
- }
Nhận một Line dữ liệu sau đó mới xử lý
Đối với cách này thì cần lưu trữ dữ liệu của cả Line vào một mảng rồi sau đó mới xử lý. Cách làm này có thể giúp chương trình hạn chế tác vụ trong ngắt hơn, lúc này chương trình ngắt UART sẽ đảm nhận việc nhận data, còn hàm main() sẽ xử lý data của cả Line nhận được. Tất nhiên, để tránh dữ liệu bị conflict khi main() đang xử lý mảng dữ liệu, mà ngắt UART lại ghi đè dữ liệu vào mảng đó, chúng ta có thể tạo một Queue các mảng dữ liệu.
Lúc này trong Ngắt sẽ chỉ việc check và nhận Data vào mảng global, khi nhận được '\r', '\n' báo hiệu việc hết dòng thì sẽ dựng một Flag để báo cho main xử lý, và đẩy mảng Data vào Queue.
- volatile uint8_t Hex_LineData[256];
- volatile uint8_t count;
- volatile uint8_t StartNewLineFlag = 0;
- volatile uint8_t HexLineHandleFlag = 0;
- void UART_IRQHandler(void)
- {
- uint8_t Data = UART_D;
- if( StartNewLineFlag && '\r' != Data && '\n' != Data )
- {
- Hex_LineData[count++] = Data;
- }
- else if( ':' == Data )
- {
- count = 0;
- StartNewLineFlag = 1
- }
- else if( '\r' != Data || '\n' != Data )
- {
- StartNewLineFlag = 0;
- HexLineHandleFlag = 1;
- Queue_Push(Hex_LineData);
- }
- else
- {;}
- }
Trong main() khi thấy cờ dựng lên thì sẽ Pop Data khỏi Queue để xử lý từng trường và ghi vào bộ nhớ Flash.
- void main()
- {
- // ... Init
- while(1)
- {
- // ...
- if( HexLineHandleFlag )
- {
- LineDataArr = Queue_Pop();
- ByteCountH = GetHexData(LineDataArr[0]);
- ByteCountL = GetHexData(LineDataArr[1]);
- if( HEX_INVALID != ByteCountH && HEX_INVALID != ByteCountL )
- {
- stLineData.ByteCount = ByteCountH * 16 + ByteCountL;
- }
- else
- {
- // Error Format
- }
- // ....
- }
- }
- }
3. Phương án sử dụng UART + DMA
Phương án này có lẽ là phương án hiệu quả nhất, khi tận dụng được khả năng tối ưu tốc độ nhận dữ liệu của bộ DMA (Direct Memory Access). Có thể kết hợp cách nhận DMA với phương án lưu dữ liệu một Line Hex vào một mảng, để DMA nhận dữ liệu liên tục và ghi vào địa chỉ mảng thay vì phải nhảy vào ngắt liên tục.
Phương án này mình sẽ hướng dẫn chi tiết trong một bài viết / video sau!
Trên đây là tổng quan một số phương án để phân tích và xử lý file Intel Hex trong việc nhận data và update Firmware trong chương trình Bootloader. Cách tốt nhất là bạn cần thực hành và đưa nó vào test chương trình Bootloader của riêng mình!
>>>>>> Follow ngay <<<<<<<
Để nhận được những bài học miễn phí mới nhất nhé 😊
Chúc các bạn học tập tốt 😊