使用Python + tkinter制作。
功能:
这是个给单片机通过串口进行IAP的上位机,与单片机中的BOOT程序配合使用,完成对单片机APP程序的升级。可以完成bin文件的切片,CRC校验(使用Crc32Mpeg2),打包自动发送。
界面如下图所示:
- 接收区是显示信息的区域,接收和发送的信息都在这显示
- 串口配置区域用来配置和打开串口
- 命令设置区域,设置上位机发送到单片机开始升级的命令,可以手动点击按钮发送(点一次发1次),可以勾选自动发送(约60ms发一次直到接收到begin),设置CRC一致,设置每次发送bin文件的字节数。
- bin文件路径区域,就是选择你要发送到单片机的bin文件
工作流程
- 上位机发送(手动或自动)开始命令(图中1.命令)
- 单片机接收到开始命令,准备好升级,回复上位机开始(图中2.命令)
- 选择bin文件,开始发送
- 程序自动对bin文件分成用户设置的大小,然后CRC校验(使用Crc32Mpeg2),并把结果加到发送数据的最后四位
- 单片机接收数据取出前边的数据进行CRC校验,并于接收数据的后四位比较,如果一致,返回CRC一致命令(图中3.命令)
- 上位机接收到CRC一致命令(图中3.命令),开始下一轮发送,直到发送完毕
注意事项
- 成功开始升级命令(图中2.命令),是字符串格式
- CRC一致命令(图中3.命令)是16进制格式,两个数表示8位,程序只判断第一个8位,比如在输入框中填303132,但只判断接收到的第一个8位在不在里面
- 发送字节数单位是KB
- CRC校验使用的是Crc32Mpeg2,如下图所示,这个和STM32H7系列的CRC默认设置一样的,其他系列不知道
源码,两个文件
点击查看代码,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 threadingglobal 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 serglobal rx_dataif(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_datarx_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"breaklock.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_dataif(auto_send_begincmd.get()):try:if(not (ser.is_open)):tkinter.messagebox.showinfo('错误', '串口没打开')returnelse: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_datawhile(rx_data!="begin ok"):if(auto_send_begincmd.get()==False):breakelse:lock.acquire()if(rx_data=="begin ok"):lock.release()breakelse: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_datalock.acquire()if(entry_path.get()==""):tkinter.messagebox.showinfo('错误', '文件错误')lock.release()returnlock.release()while(rx_data==""):passlock.acquire()if(rx_data == "begin ok"):#已经开始,发送bin packbin_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 packbin_i = 0retry_num = 0while(True):if(send_bin_pack(bin_list[bin_i])):text1.insert(index=END, chars=f"{bin_i} , ")rx_data=""lock.release()while(rx_data==""):passlock.acquire()if(rx_data=="CRC ok"):bin_i+=1retry_num=0elif(rx_data=="no CRC"):retry_num+=1if(retry_num>5):tkinter.messagebox.showinfo('错误', '发送失败,no CRC * 5')breakelif(rx_data=="no ser"):tkinter.messagebox.showinfo('错误', '接收失败,串口没打开,no ser')breakelse:if(not ser.is_open):tkinter.messagebox.showinfo('错误', '发送失败,串口没打开,no ser')breakretry_num+=1if(retry_num>5):tkinter.messagebox.showinfo('错误', '发送失败*5,重试')breakif(bin_i==len(bin_list)):text1.insert(index=END, chars="发送完成")tkinter.messagebox.showinfo('成功', '发送完成')breaktime.sleep(0.01)lock.release() def create_thread():global rx_datatry:if(not (ser.is_open)):tkinter.messagebox.showinfo('错误', '串口没打开')returnelse: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 Trueexcept:#错误返回tkinter.messagebox.showinfo('错误', '发送bin pack失败')return Falsedef 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 osdef 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)+1for 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