首页 > 编程语言 >[开源&分享]一个用于单片机IAP自动发送的串口助手,上位机,使用Python+tkinter制作

[开源&分享]一个用于单片机IAP自动发送的串口助手,上位机,使用Python+tkinter制作

时间:2024-08-19 18:04:13浏览次数:9  
标签:fr tkinter ser Python send 串口 pady data

使用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

标签:fr,tkinter,ser,Python,send,串口,pady,data
From: https://www.cnblogs.com/lizesen/p/18361530

相关文章

  • [Python学习日记-9] Python中的运算符
    简介        计算机可以进行的运算有很多种,但可不只加减乘除这么简单,运算按种类可分为算数运算、比较运算、逻辑运算、赋值运算、成员运算、身份运算、位运算,而本篇我们暂只介绍算数运算、比较运算、逻辑运算、赋值运算算数运算一、运算符描述        以下......
  • python基础语法 010 类和对象-6-1 继承定义
    前提:    在真实世界中,类型之间可能存在范围包含关系,比如:人这个类型和亚洲人这个类型。        人是包括了亚洲人的,如果某人是员工亚洲人,那么它必定是一个人        这种关系,在编程语言中称为继承关系        比如上面例子:亚洲人这个类就继......
  • Python 实现Word和TXT文本相互转换
    Word文档(.doc或.docx)和纯文本文件(.txt)是两种常用的文件格式。Word文档通常用于复杂的文档处理和排版,而纯文本文件则用于存储和传输纯文本信息。了解如何在这两种格式之间进行转换能提高工作效率,并便于文件管理。本文将详细介绍如何使用Python实现Word和TXT文件格式之间的相互转换......
  • Python一些简单基础的模板化语法
    字符串的格式化%s:可以接收任何类型的传值(%d只可以接收整型int)单个值"myageis%s"%"18.56"按照值对应res="mynameis%s,myageis%s"%("xj",18)按照字典传值res="mynameis%(name)s,mynameis%(age)s"%{"age":"......
  • 【Python实现禁用任务管理器】
    效果展示:代码及解析:提示:使用pyinstaller打包可以避免弹窗,食用更佳哦提示(2):运行后要等待一段时间再关机哦禁用代码:要禁止任务管理器关闭Python程序,可以通过修改注册表来实现。以下是示例代码:importwinregimportctypesimportsys#以管理员身份运行此程序ct......
  • 【python实现修改所有可执行程序的图标】
    实现效果:图标在此替换前:吐槽:这原版看着也不像原版......
  • 基于python个性化旅游线路推荐系统(源码+文档+调试+讲解)
    收藏关注不迷路!!......
  • 2024年新版Python零基础从入门到进阶学习路线!
    Python基础初始Python基础语法流程控制-选择结构流程控制-循环结构字符串和正则函数入门函数高级数据结构-列表和元组数据结构-字典和集合IO和文件操作文件操作进阶面向对象入门面向对象三大特性面向对象应用异常处理常用内置模块序列化模块网络请求模块MySQL入门MySQL命......
  • [GUET-CTF2019]虚假的压缩包1附送RSA解密&CRC破解png宽高&异或python脚本
    解压得到下图两个zip文件,虚假的压缩包、真实的压缩包使用010editor打开,发现是伪加密,把09改为00即可打开打开以后,发现是是一个rsa解密题上python脚本importgmpy2deffind_pq(n):forpinrange(2,int(n**0.5)+1):ifn%p==0:q=......
  • python入门教程(非常详细!3w+ 文字)
    先序:学习编程语言要先学个轮廓,刚开始只用学核心的部分,一些细节、不常用的内容先放着,现用现查即可;把常用的东西弄熟练了在慢慢补充。1、安装Python解释器为什么需要安装PythonPython语言本身是由解释器执行的,因此你需要在你的计算机上安装Python解释器。这个解释器会将......