[開源&分享]一個用於微控制器IAP自動傳送的串列埠助手,上位機,使用Python+tkinter製作

quanshimutou發表於2024-08-19

使用Python + tkinter製作。

功能:

這是個給微控制器透過串列埠進行IAP的上位機,與微控制器中的BOOT程式配合使用,完成對微控制器APP程式的升級。可以完成bin檔案的切片,CRC校驗(使用Crc32Mpeg2),打包自動傳送。

介面如下圖所示:

image

  1. 接收區是顯示資訊的區域,接收和傳送的資訊都在這顯示
  2. 串列埠配置區域用來配置和開啟串列埠
  3. 命令設定區域,設定上位機傳送到微控制器開始升級的命令,可以手動點選按鈕傳送(點一次發1次),可以勾選自動傳送(約60ms發一次直到接收到begin),設定CRC一致,設定每次傳送bin檔案的位元組數。
  4. bin檔案路徑區域,就是選擇你要傳送到微控制器的bin檔案

工作流程

  1. 上位機傳送(手動或自動)開始命令(圖中1.命令)
  2. 微控制器接收到開始命令,準備好升級,回覆上位機開始(圖中2.命令)
  3. 選擇bin檔案,開始傳送
  4. 程式自動對bin檔案分成使用者設定的大小,然後CRC校驗(使用Crc32Mpeg2),並把結果加到傳送資料的最後四位
  5. 微控制器接收資料取出前邊的資料進行CRC校驗,並於接收資料的後四位比較,如果一致,返回CRC一致命令(圖中3.命令)
  6. 上位機接收到CRC一致命令(圖中3.命令),開始下一輪傳送,直到傳送完畢

注意事項

  1. 成功開始升級命令(圖中2.命令),是字串格式
  2. CRC一致命令(圖中3.命令)是16進位制格式,兩個數表示8位,程式只判斷第一個8位,比如在輸入框中填303132,但只判斷接收到的第一個8位在不在裡面
  3. 傳送位元組數單位是KB
  4. CRC校驗使用的是Crc32Mpeg2,如下圖所示,這個和STM32H7系列的CRC預設設定一樣的,其他系列不知道
    image

原始碼,兩個檔案

點選檢視程式碼,GUI.py
from tkinter import *
import tkinter.filedialog
import tkinter.messagebox
import tkinter.ttk as ttk
import serial
import serial.tools.list_ports
import time
import IAP_Send
import threading


global ser
lock = threading.Lock()
rx_data = ""

#建立視窗
root = Tk()
root.title("IAP傳送助手")
root.geometry("750x430")

#建立窗格管理
pw = PanedWindow(root, orient="horizontal",showhandle=True)
pw.pack(fill="both", expand=True)

#建立框架
fr_receive = LabelFrame(master=root,text="接收區",width=450,height=390)
fr_receive.pack(side="left",anchor="nw",fill="both",padx=5,pady=5)

fr_right = Frame(master=root,width=230,height=390)
fr_right.pack(side="right",anchor="ne",fill="both",padx=5,pady=5)

fr_port_set = LabelFrame(master=fr_right,text="串列埠配置",width=250, height=230)
fr_port_set.pack(side="top",anchor="nw",padx=5,pady=5,fill="x")

fr_cmd_set = LabelFrame(master=fr_right,text="命令設定",width=220, height=50)
fr_cmd_set.pack(side="top",anchor="se",padx=5,pady=5,fill="x")

fr_send = LabelFrame(master=fr_right,text="bin檔案路徑",width=220, height=100)
fr_send.pack(side="top",anchor="se",padx=5,pady=5,fill="both")

#拖動柄
sg = ttk.Sizegrip(master=fr_right)
sg.pack(side="bottom", anchor="se", padx=2, pady=2)

#新增框架到窗格管理
pw.add(fr_receive)
pw.add(fr_right)

#建立文字區和捲軸
text1 = Text(master=fr_receive,width=45,height=30,font=("宋體",10))
sb = Scrollbar(master=fr_receive,width=20,command=text1.yview)
# 注意順序,先放scroll,再放text
sb.pack(side="right",fill="y")
text1.pack(side="top",padx=5,pady=5)
text1.config(yscrollcommand=sb.set)

#-----------------------------------------右側 上邊 串列埠配置
lb1=Label(master=fr_port_set,text="串列埠號")
lb2=Label(master=fr_port_set,text="波特率")
lb3=Label(master=fr_port_set,text="資料位:")
lb4=Label(master=fr_port_set,text="停止位:")
lb5=Label(master=fr_port_set,text="校驗位:")
lb1.grid(row=0,column=0,padx=5,pady=5,sticky="w")
lb2.grid(row=1,column=0,padx=5,pady=5,sticky="w")
lb3.grid(row=2,column=0,padx=5,pady=5,sticky="w")
lb4.grid(row=3,column=0,padx=5,pady=5,sticky="w")
lb5.grid(row=4,column=0,padx=5,pady=5,sticky="w")

#全域性變數
var_btn   = StringVar(value="開啟串列埠")
data_len  = IntVar(value=8)
stop_len  = DoubleVar(value=1)

datalen = Entry(master=fr_port_set,textvariable=data_len,width=5)
datalen.grid(row=2,column=1,padx=5,pady=5,sticky="w")
stoplen = Entry(master=fr_port_set,textvariable=stop_len,width=5)
stoplen.grid(row=3,column=1,padx=5,pady=5,sticky="w")
#建立下拉選單
var_cb1 = StringVar()
cb1 = ttk.Combobox(fr_port_set,textvariable=var_cb1,state="readonly", 
                   width=35)
cb1['values'] = serial.tools.list_ports.comports() #列出可用串列埠
cb1.current(1)  # 設定預設選項
cb1.grid(row=0,column=1,padx=5,pady=5,sticky="w",columnspan=2)

var_cb2 = IntVar()
cb2 = ttk.Combobox(fr_port_set,textvariable=var_cb2,state="readonly",width=18)
cb2['values'] = [9600,115200]
cb2.current(1)  # 設定預設選項
cb2.grid(row=1,column=1,padx=5,pady=5,sticky="w")

parity_bit = StringVar()
parity_cb = ttk.Combobox(fr_port_set,textvariable=parity_bit,state="readonly",width=9)
parity_cb['values'] = ["無","奇校驗","偶校驗"]
parity_cb.current(0)  # 設定預設選項
parity_cb.grid(row=4,column=1,padx=5,pady=5,sticky="w")
#-定義函式
def open_port():
    global ser
    global rx_data
    if(var_btn.get()=="開啟串列埠"):
        try:
            ser=serial.Serial(port=str(cb1.get())[0:5],baudrate=cb2.get(),
                            bytesize=data_len.get(),
                            stopbits=stop_len.get(),timeout=0.1)
            #傳遞下拉框選擇的引數 COM號+波特率  [0:5]表示只提取COM號字元
        except:
            tkinter.messagebox.showinfo('錯誤','串列埠開啟失敗')
            return
        #ser.parity   #校驗位N-無校驗,E-偶校驗,O-奇校驗
        if(parity_cb.get()=="無"):
            ser.parity=serial.PARITY_NONE#無校驗
        elif parity_cb.get()=="奇校驗":
            ser.parity=serial.PARITY_ODD#奇校驗
        elif parity_cb.get()=="偶校驗":
            ser.parity=serial.PARITY_EVEN#偶校驗

        if(ser.is_open):
            var_btn.set('關閉串列埠')            #改變按鍵內容
            btn1.config(background='red')
            cb1.config(state="disabled")
            cb2.config(state="disabled")
            parity_cb.config(state="disabled")
            datalen.config(state="disabled")
            stoplen.config(state="disabled")
            rx_th=threading.Thread(target=usart_receive,name="serial_receive",daemon=True)
            rx_th.start()
        else:
            tkinter.messagebox.showinfo('錯誤','串列埠開啟失敗')
    elif(var_btn.get()=="關閉串列埠"):
        if(ser.is_open):
            ser.close()
            var_btn.set("開啟串列埠")
            cb1.config(state="normal")
            cb2.config(state="normal")
            parity_cb.config(state="normal")
            datalen.config(state="normal")
            stoplen.config(state="normal")
            btn1.config(background=default_color)
            text1.delete(1.0,END)
            rx_data=""

def usart_receive():
    global rx_data
    rx_data=""
    while True:
        lock.acquire()
        if(ser.is_open):
            rx_buf = ser.read()
            if len(rx_buf) >0:
                time.sleep(0.01)
                rx_buf += ser.readall()  #有延遲但不易出錯
                hex_data=rx_buf.hex().upper()
                if(len(hex_data)==8):
                    text1.insert(END, hex_data+'\n')
                    rx_data = "no CRC"
                elif(len(hex_data)>8):
                    str_data = str(rx_buf, encoding='utf-8')
                    text1.insert(END, str_data)
                    text1.insert(END,"\n")
                    if("egin" in str_data):
                        rx_data = "begin ok"
                    else:
                        rx_data = "no begin"
                elif(len(hex_data)<8):
                    if(hex_data[0:2] in entry_CRC.get().upper()):
                        text1.insert(END, hex_data+'\n')
                        rx_data = "CRC ok"
                    else:
                        rx_data = "no CRC"
                text1.yview_moveto(1)
                text1.update()
        else:
            rx_data = "no ser"
            break
        lock.release()
        time.sleep(0.01)
    lock.release()

#建立按鈕
btn1 = Button(fr_port_set, textvariable=var_btn,width=10,state="normal",command=open_port)
btn1.grid(row=4,column=2,padx=5,pady=5)
default_color = btn1.cget('background')  # 獲取預設背景顏色

#----------------------------------------右側 中間 命令設定
CRC_lb=Label(master=fr_cmd_set,text="CRC一致接收到(HEX)")
CRC_lb.grid(row=1,column=0,padx=5,pady=5,sticky="w")
lb8=Label(master=fr_cmd_set,text="傳送位元組數(KB)")
lb8.grid(row=1,column=2,padx=5,pady=5,sticky="w")

#全域性變數
begin_cmd = StringVar(value=":UD")
cmd_CRC_right = StringVar(value="30")
send_size = IntVar(value=1)
auto_send_begincmd = BooleanVar()

#建立輸入框
entry_begin = Entry(master=fr_cmd_set,textvariable=begin_cmd,width=8)
entry_begin.grid(row=0,column=1,padx=5,pady=5,sticky="w")
entry_CRC = Entry(master=fr_cmd_set,textvariable=cmd_CRC_right,width=8)
entry_CRC.grid(row=1,column=1,padx=5,pady=5,sticky="w")
entry_size = Entry(master=fr_cmd_set,textvariable=send_size,width=5)
entry_size.grid(row=1,column=3,padx=5,pady=5,sticky="w")

def send_begin_command():#傳送:UD 開始升級命令
    send_data = entry_begin.get().strip()
    try:#字元傳送
        if(ser.is_open):  #傳送前判斷串列埠狀態 避免錯誤
            ser.write(send_data.encode('utf-8'))
            text1.insert(index=END,chars=send_data+"      ")
    except:#錯誤返回
        tkinter.messagebox.showinfo('錯誤', '傳送開始失敗,串列埠沒開')

def create_auto_send_cmd():
    global rx_data
    if(auto_send_begincmd.get()):
        try:
            if(not (ser.is_open)):
                tkinter.messagebox.showinfo('錯誤', '串列埠沒開啟')
                return
            else:
                entry_begin.config(state="readonly")
                if(rx_data!="begin ok"):
                    text1.delete(1.0,END)
                th_auto_send = threading.Thread(target=auto_send_cmd,daemon=True)
                th_auto_send.start()
        except:
            tkinter.messagebox.showinfo('錯誤', '串列埠沒開啟')
            cbtn1.deselect()
    else:
        entry_begin.config(state="normal")

def auto_send_cmd():
    global rx_data
    while(rx_data!="begin ok"):
        if(auto_send_begincmd.get()==False):
            break
        else:
            lock.acquire()
            if(rx_data=="begin ok"):
                lock.release()
                break
            else:
                send_begin_command()
                rx_data=""
            lock.release()
            time.sleep(0.05)

# 建立按鈕
btn6 = Button(fr_cmd_set,text="開始命令",width=10,command=send_begin_command)
btn6.grid(row=0,column=0,padx=5,pady=5,sticky="w")

#建立選擇框
cbtn1 = Checkbutton(master=fr_cmd_set,text="自動傳送開始命令",variable=auto_send_begincmd,
                    command=create_auto_send_cmd)
cbtn1.grid(row=0,column=2,padx=5,pady=5,columnspan=2,sticky="w")
#---------------------------------------右側 下邊 bin檔案路徑
#全域性變數
path = StringVar(value="")
#建立輸入框,選擇檔案
entry_path = Entry(master=fr_send,textvariable=path,width=40)
entry_path.pack(side="top",padx=5,pady=5,fill='both')

def send_data(): #傳送資料
    global rx_data
    lock.acquire()
    if(entry_path.get()==""):
        tkinter.messagebox.showinfo('錯誤', '檔案錯誤')
        lock.release()
        return
    lock.release()

    while(rx_data==""):
        pass

    lock.acquire()
    if(rx_data == "begin ok"):#已經開始,傳送bin pack
        bin_list = IAP_Send.IAP_CRC(entry_path.get(),send_size.get()*1024)
        text1.insert(index=END,chars=f"分包、CRC校驗完成,傳送次數:{len(bin_list)}\n")
    else:
        tkinter.messagebox.showinfo('錯誤', '接收到的begin不對')
        lock.release()
        return
    
    #傳送bin pack
    bin_i = 0
    retry_num = 0
    while(True):
        if(send_bin_pack(bin_list[bin_i])):
            text1.insert(index=END, chars=f"{bin_i}   ,   ")
            rx_data=""
            lock.release()
            while(rx_data==""):
                pass
            lock.acquire()
            if(rx_data=="CRC ok"):
                bin_i+=1
                retry_num=0
            elif(rx_data=="no CRC"):
                retry_num+=1
                if(retry_num>5):
                    tkinter.messagebox.showinfo('錯誤', '傳送失敗,no CRC * 5')
                    break
            elif(rx_data=="no ser"):
                tkinter.messagebox.showinfo('錯誤', '接收失敗,串列埠沒開啟,no ser')
                break
        else:
            if(not ser.is_open):
                tkinter.messagebox.showinfo('錯誤', '傳送失敗,串列埠沒開啟,no ser')
                break
            retry_num+=1
            if(retry_num>5):
                tkinter.messagebox.showinfo('錯誤', '傳送失敗*5,重試')
                break
        if(bin_i==len(bin_list)):
            text1.insert(index=END, chars="傳送完成")
            tkinter.messagebox.showinfo('成功', '傳送完成')
            break
        time.sleep(0.01)
    lock.release()    

def create_thread():
    global rx_data
    try:
        if(not (ser.is_open)):
            tkinter.messagebox.showinfo('錯誤', '串列埠沒開啟')
            return
        else:
            if(rx_data!="begin ok"):
                text1.delete(1.0,END)
            th_send = threading.Thread(target=send_data,name="send_bin_file",daemon=True)
            th_send.start()
    except:
        tkinter.messagebox.showinfo('錯誤', '串列埠沒開啟')

def send_bin_pack(bin_pack):
    try:
        ser.write(bin_pack)
        return True
    except:#錯誤返回
        tkinter.messagebox.showinfo('錯誤', '傳送bin pack失敗')
        return False

def selectPath():
    path1 = tkinter.filedialog.askopenfilename(filetypes=[("bin檔案", "*.bin")])
    if path1:
        path1 = path1.replace("/", "\\")  # 實際在程式碼中執行的路徑為“\“ 所以替換一下
        path.set(path1)

#建立按鈕
btn2 = Button(fr_send, text="選擇檔案",width=20,command=selectPath)
btn2.pack(side='left',anchor="center",padx=5,pady=5)
btn3 = Button(fr_send, text="開始傳送",width=20,command=create_thread)
btn3.pack(side="right",anchor="center",padx=5,pady=5)

mainloop()

點選檢視程式碼,IAP_Send.py
import crccheck
import os

def IAP_CRC(filepath, send_size=1024):
    # send_size : 每次傳送的位元組數
    # filepath : bin檔案路徑 
    # 'D:\\STM32 Projects\\Power_Control\\Debug Internal\\Power_Control.bin'

    # 開啟bin檔案
    binfile = open(filepath, 'rb') #開啟二進位制檔案
    file_size = os.path.getsize(filepath) #獲得檔案大小
    file_data = binfile.read()
    binfile.close()
    
    # 傳送資料的列表,一次一個
    send_list = []
    # 傳送次數
    send_num = int(file_size/send_size)+1

    for i in range(send_num-1):
        data = file_data[i*send_size:(i+1)*send_size]
        crc_value = crccheck.crc.Crc32Mpeg2.calcbytes(data)
        send_list.append(data+crc_value)
    data = file_data[(send_num-1)*send_size:]
    crc_value = crccheck.crc.Crc32Mpeg2.calcbytes(data)
    send_list.append(data+crc_value)

    return send_list

相關文章