遠端韌體更新是嵌入式系統開發者不可或缺的方便工具之一,筆者使用TI 的TMS320F280025C 並搭配 Raspberry Pi 作為傳送更新的系統,透過 I2C 介面將編譯好的 .hex 韌體檔案由 Raspberry Pi傳送到 MCU ,並且正確執行更新後的應用程式。前面四篇文章分別帶大家從流程設計、Bootloader撰寫、Hex檔的製作與通訊協議設計以及 Flash 的寫入操作與實驗。這篇文章會帶大家看最後韌體更新的實驗結果,以及除錯經驗的分享。{alertInfo}
目錄
前言
我們一路從韌體更新的原理與流程設計需要將記憶體規劃成兩個區塊,並且設計更新步驟需要有版本與型號確認的機制避免韌體亂更新之後,接著想辦法讓一顆 MCU可以裝兩種韌體。設計好兩種任體,他們之間的切換可以透過 second bootloader的方式來實現,並且透過修改 linker command file來規劃記憶體使用。
再來就要搞清楚要被傳送的韌體格式到底為何,且包含哪些資料是可以被拿來利用當作通訊設計。使用 Intel hex格式可以很好的發揮他的功能,糗資料可以直接寫入 Flash而不需要進行轉碼。
最後,當資料封包都接收好後,我們就可以把資料寫入到第一步驟規劃的 Flash記憶體中。
前情提要:
韌體更新設計實例 (一) 流程設計
韌體更新設計實例 (二) Bootloader 與 Code branch
韌體更新設計實例 (三) Image檔製作與通訊設計
韌體更新設計實例 (四) Flash API 寫入實驗
今天這篇文章會整合過去四篇文章的功能,並且在 Raspberry pi上撰寫一個 bash腳本,將新的韌體檔案透過 I2C傳送給 MCU完成韌體更新。
Host 端腳本
Raspberry pi 是很常見的單版電腦,他的學習資源很多,我們這邊利用他內建 I2C的優勢,不需要額外安裝驅動程式就能開發的優勢,作為我們遠端更新 MCU韌體的主機,也就是第一篇文章中所說明的 Host端。
有用過 PICkit Serial Analyser的朋友可能會納悶,這東西這麼好用為何不用他來測試韌體更新呢?
因為筆者在做實驗的時候發現,他為了降低錯誤發生的狀況,每個位元傳輸的間隔大約會隔1mS左右,避免太頻繁的讀取 MCU造成使用者無法除錯的狀況。白話一點來說就是我認為他不夠嚴苛且不夠貼近實際情況,所以使用 Raspberry Pi 會是比較合理一點的選擇。
當然,缺點也顯而易見就是需要自己撰寫檔案解碼的程式或腳本,再由樹梅派執行。我比較沒有這方面的經驗,所以使用最常用的 i2ctool 系列工具來達成我的目的。
i2ctool
i2ctool 是一套可以操作 I2C總線的軟體,常用的有 i2cdetect,i2cget,i2cset,i2ctransfer等等。i2cdetect 用來偵測 I2C總線上哪幾個位址有掛載設備;i2cset與 i2cget一次只能傳送或接收一個byte的資料,一開始有想過用這個指令來完成韌體更新,但仔細想想太浪費時間了。最後選擇使用 i2ctransfer來完成這項任務。
他的使用方法很簡單,再指令後面加上要使用的 i2c bus編號,w/r 表示寫入或讀取,後面直接加上傳送的位元組數量。最後加上要傳送的資料即可。範例如下:
i2ctransfer –y 1 w8@0x60 0x3a 0x02 0x80 0x00 0x48 0xed 0x49
這行指令透過 i2c bus1 寫入 8個 byte,內容是0x60 0x3a 0x02 0x80 0x00 0x48 0xed 0x49。我們的腳本最主要就是要把 HEX檔的內容轉換成 i2ctransfer的命令,當樹梅派要傳送韌體更新封包的時候,就可以整包整包的傳送給 MCU。
此時 MCU收到封包就會進行對應資料種類的判斷,再將其寫入到指定的記憶體位置。
詳細的 bash腳本原始碼如下:
#!/bin/bash
file=$1
delay=$2
counter=0
# remove log.txt if exist
rm log.txt
echo "Initial firmware upgrade process"
sleep .5
echo "Change to FW upgrade mode..."
i2c_result=$(i2cget -y 1 0x60 0x5a w)
if [ "$i2c_result" == "0x0cc0" ]; then
echo -e "\033[32mDone\033[0m"
else
echo -e "\033[31mFailed\033[0m"
exit 0
fi
sleep 1
echo "Sending firmware information..."
i2cset -y 1 0x60 0x59 0x50 0x57 0x52 0x2d 0x32 0x30 0x30 0x2d 0x57 0x56 0x44 0x2d 0x35 0x34 \
0x31 0x32 0x2d 0x4f 0x46 0x2d 0x50 0x4d 0x01 0x02 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xcf i
echo -e "\033[32mDone\033[0m"
sleep 1
echo "Erasing Flash sectors..."
i2c_result=$(i2cget -y 1 0x60 0x57 w)
if [ "$i2c_result" == "0x2cc2" ]; then
echo -e "\033[32mDone\033[0m"
else
echo -e "\033[31mFailed\033[0m"
exit 0
fi
sleep 1
echo "Sending package..."
sleep 1
while read line; do
counter=$((counter+1))
echo "===================="
echo -e "\e[36m""|Sending Line""$counter""\t|""$line""\e[0m"
# firmware record sector1, length, flash address and record type
# remove : symbol
line=$(echo "$line" | sed 's/:/3A/g')
length=${#line}
length=$(("$length"/2))
# get line lengh for i2c-transfer
echo "length:""$length""byte"
# add 0x before data
line=$(echo "$line" | awk '{gsub(/../," 0x&", $0)}1')
# add i2c-transfer parameter
line=$(echo "i2ctransfer -y 1 wNN@0x60""$line")
if [[ "$length" -gt 37 ]]; then
length=$(("$length"+1))
line=$(echo "$line" | sed "s/NN/"$length"/" | sed 's/.$/ 0x66/')
elif [[ "$length" -eq 22 ]]; then
length=$(("$length"+1))
line=$(echo "$line" | sed "s/NN/"$length"/" | sed 's/.$/ 0x66/')
else
# replace "NN' with data length
line=$(echo "$line" | sed "s/NN/"$length"/" | sed 's/.$//')
fi
echo "$line"
echo "$line" >> log.txt
$line
sleep "$delay"
done < "$1"
echo "All data transferred..."
echo "System reboot in"
echo "3"
sleep 1
echo "2"
sleep 1
echo "1"
sleep 1
i2cget -y 1 0x60 0xA5 w
我插入好多個 echo來顯示當前腳本運行的進度,並將腳本運行的結果紀錄在 log.txt內。第一個 if接收 MCU回傳的資料,判斷當前可不可以進行韌體更新,若回傳值不等於 0x0CC0,表示 MCU狀態切換失敗,不能更新。
下面則是傳送韌體資訊,包含韌體型號與版本編號給 MCU,照理來說這邊也要有一個回傳機制,但當時礙於開發時間沒有完善。再來就是清除 Flash,並確認清除成功與否。
再來就是激動人心的時刻,while 迴圈會把新的韌體檔案一行一行的讀取出來,接著將最前面的 ":"字元移除,並將後面的資料以兩兩成對的方式再前面加上 "0x" 作為前綴。如此一來,我們就將原本 hex檔的資料變成 i2ctransfer需要的 "0x"開頭的資料。
但這裡有個小 bug後面會提到,當他傳送到第 16行,以及 32行之後,會莫名其妙的需要增加 "dummy write",原因未知。也因此在後半段有一些意義不明的 0x66會被加入到我們的指令中。(腳本內是22, 37 這是 16+5與 32+5的結果)
最後,當所有資料都傳送完畢,並且等待三秒倒數計時,時間到樹梅派會傳送重新啟動的命令,此時 MCU會重啟,就可以觀察到他在執行新的韌體了!
測試與執行結果
我們觀察示波器的波形, SDA 線與 RX FIFO 的 GPIO指示訊號,可以看到封包傳送完畢後, data processing會點亮一下,接著進行 Flash的寫入,如此重複一直到新韌體傳送完畢。
看似很美好的過程,其實裡頭有兩個小 bug我想跟大家分享與討論。
接收錯誤
第一個是在資料傳送的過程中如果資料量太多,會造成 MCU處理不過來,導致傳送速度比處理速度慢,這時候資料就會塞車。即使使用 FIFO作為緩衝也會有同樣的狀況。這時候有支援 clock stretching的 I2C Master會把 SCK訊號延展,等待 Slave端的 SDA動作才會接著動作。但樹梅派的這個功能是靠軟體模擬出來的,所以有時候有會出狀況:
下圖的資料本來應該是0x07 0x00 0x69 0x06,但對應到錯誤的 SCK訊號讓接收端誤以為資料是 0x07 0x00 0x8D 0xDE。不要小看這一個 bit的差異,他對應到的組合語言指令是完全大相逕庭的!
這個問題的解決方法很簡單,讓傳送端傳送的速度不要這麼快就可以了。也就是我們腳本中,while 迴圈內 slee $delay 的用處。
加了一點適當的延遲,我們確保MCU的寫入速度不會因為傳輸太快而塞車,就能確保整個更新檔的傳輸順暢。
傳送錯誤
實驗過程中發現的另外一個問題,是 Raspberry Pi 再傳輸的時候,當資料長度等於特定長度時,會莫名的需要加入 Dummy byte 才能正確的傳送封包。原本的實驗中本來想一次傳輸一筆 125 byte的資料,並觀察寫入 Flash的 GIPO狀態,但卻發現他分段寫入的時候,有特定長度的資料會被吃掉!也就是完全不見,沒有被寫到 Flash之中。
仔細展開看發現,第一筆資料傳輸的時候很正常,寫入的 GPIO顯示 16 byte的 FIFO滿了之後就會對 Flash進行擦寫。但這時候 MCU就有點力不從心,發現 SCL的訊號有被延展,但資料傳輸無誤。 接著送第二筆資料的時候,接收與寫入也沒問題。但隨著時間越來越長,GPIO的狀態逐漸不能對齊資料封包的傳送。
當第二包封包傳到一半,16 byte容量的 FIFO被填滿時候,其實此時的 MCU還在寫入 Flash的第二部分,也就是第17-32 byte。從這裡可以感受出 Flash再清除與寫入速度上的劣勢。
當第三包封包傳輸過來的時候,MCU偷懶了!他做事只做一半。
他只把第二包的第一部分,也就是第1-16 byte寫到 Flash內,後面的竟然直接放棄,導致 Flash對應位址完全沒有資料。
怎麼可以這樣(怒),給我乖乖工作啊!!
筆者猜測應該是 clock stretching的功能還不夠成熟,導致在使用這個功能的時候在 MCU端產生誤判,讓他以為資料已經傳輸完畢可以寫入給 Flash。
我們只能在腳本內加入一些條件式,當資料長度等於特定長度時,自動在資料尾端加入一個 Dummy byte來繞開這個錯誤。如此一來可在不影響開發時成以及成本的情況下,將這個問題解決。
下圖是一次傳送 345 byte的實驗結果,可以觀察每次的寫入都有成功。
結論
這系列的文裝從頭到尾設計、說明、實作、測試整個微處理機的韌體更新流程,從無到有開發出一套完整的韌體更新架構。只不過目前這套系統沒有完整的交互握手功能,理論上收到程式碼後,若 check sum錯誤,或者寫入失敗時應該要有一個機制讓 Host端重新傳輸一次資料。
但目前的腳本礙於開發時間不夠以及人力資源不足,沒辦法完成這個部分。儘管如此,還是希望這幾篇文章可以給那些需要幫助的夥伴們一點靈感或提示,如果有任何想法都歡迎在底下留言,我們一起互相討論!
張貼留言
留個言吧