Python / 单片机 · 2025年3月18日

基于STM32制作PC端可视化输入输出工具(SPI部分)

只谈思路,莫谈效益。

单片机开发中,会经常遇到用SPI或UART驱动某MCU的场景,通常会购买对应的通信工具,虽工具大多开源,但终究显些繁琐,并且功能比较单一,可操作性不强。因此用较廉价的STM32F103系列实现诸多日常开发需要的功能。最终实现SPI、UART、IIC、自定义通信协议、PWM输出、逻辑分析仪(因该系列性能较低,故采样率不高)。

STM32F103(下文称Tools)系列具备USB虚拟串口功能,故PC与Tools以USB通信。PC端可视化工具功能并不复杂,故用Python+Tkinter实现。

此篇中,仅实现SPI相关部分,下图为SPI部分功能截图。源码链接放在文末,文中仅对关键部分详细说明。
file

# app.py
"""
PC向Tools发送数据时,需要区分编码,即Hex或Utf8编码,其他编码亦可
同样,Tools也需要设定好编码格式,精确点是指用Keil编辑Tools代码时,Keil需要设定的编码,只有设定好正确的编码格式,编译时字符串才会以正确的编码被编译。
"""
def func_com_write(self, content: str | list, send_type: int = 0):
    if (not self.seri) or (not self.seri.is_open):
    self.log_info("Com is not connected")
    return

    try:
        # Tools以接收到 \r\n(0x0D, 0x0A) 作为一次传输的结束
        # utf8
        if send_type == ToolsFlag.SPI_TYPE_UTF8:
            content += "\r\n"
            self.seri.write(content.encode("utf-8"))
            pass 
        # hex 
        elif send_type == ToolsFlag.SPI_TYPE_HEX:
            content.extend([0x0D, 0x0A])
            self.seri.write(content)
    except serial.serialutil.SerialTimeoutException:
        self.btn_open_com.config(text="Open")
        self.log_info("Com is not connected")
        self.func_com_disconnect()
    except serial.serialutil.SerialException:
        self.btn_open_com.config(text="Open")
        self.log_info("Com connect failed")
        self.func_com_disconnect()
        return

# 接受数据时以bytes流接受,之后以何种编码显示再单独处理。
def func_com_read(self, recv_type: int = 0):
    if (not self.seri) or (not self.seri.is_open):
        self.log_info("Com is not connected")
        return bytearray()

    com_print = bytearray()
    try:
        while True:
            t = self.seri.read(1)
            if t:
                com_print += t
            else:
                break
    except serial.serialutil.SerialTimeoutException:
        self.btn_open_com.config(text="Open")
        self.log_info("Com is not connected")
        self.func_com_disconnect()
    except serial.serialutil.SerialException:
        self.btn_open_com.config(text="Open")
        self.log_info("Com connect failed")
        self.func_com_disconnect()
        return com_print

# 此函数会从Tools通过SPI接受若干字节数据,在此处理SPI的编码转换
def func_btn_spi_recv(self, btn: Button, ipt: Entry, idx: int = 0, type_: int = 0):
    if not self.seri or not self.seri.is_open:

    size = ipt.get().strip().split(" ")[0]
    if re.search(r"[^0-9]", size) or len(size) == 0:
        self.log_info("Wrong input")
        return 

    size = int(size)
    cntt_ls = [0x00, 0x00, (ToolsEvent.SPI_CMD_RECVDATA >> 8) & 0xFF, ToolsEvent.SPI_CMD_RECVDATA & 0xFF, size]
    self.func_spi_send(cntt_ls, 1)
    res = self.func_spi_recv(size)

    if (self.spi_recv_type.get() == "utf8"):
        # utf8 兼容 ASCII编码,但ASCII编码采用有符号形式,即仅有0x00~0x7F为有效字符,所以为防止数据显示丢失,故解析异常时,提示以hex显示
        try:
            self.log_info(res.decode("utf-8").replace('\x00', ''))
        except:
            self.log_info("Contains invalid characters, please use hex mode")
    elif self.spi_recv_type.get() == "hex":
        # res[:10] 为Tools的发送头,Received: 共计10个字节
        self.log_info(res[:10].decode("utf-8") + "".join("0x{:0>2X} ".format(he) for he in res[10:]))
    pass