【电赛学习笔记】MaxiCAM 项目实践——与单片机的串口通信

前言

本文是对视觉模块和STM32如何进行串口通信_哔哩哔哩_bilibili大佬的项目实践与拓展实现与mspm0g3507的串口通信,侵权即删。

MaxiCAM与STM32串口通信实践

串口协议数据传输

python 复制代码
import struct

'''
协议数据格式:
帧头(0xAA) + 数据域长度 + 数据域 + 长度及数据域数据和校验 + 帧尾(0x55)
'''

class SerialProtocol():
    HEAD = 0xAA
    TAIL = 0x55

    def __init__(self) -> None:
        pass

    def _checksum(self, data:bytes)-> int:
        '''
        计算和校验
        '''
        check_sum = 0
        for a in data:
            check_sum = (check_sum + a) & 0xFF
        return check_sum

    def is_valid(self, raw_data:bytes) -> tuple:
        '''
        判断数据是否有效
        返回值: -1 -- 参数错误 -2 -- 数据长度不够 -3 -- 数据格式错误
        '''
        if len(raw_data) == 0:
            return (-1, 0)

        bytes_redundant = 0
        index = 0
        for a in raw_data:
            if a != SerialProtocol.HEAD:
                index += 1
            else:
                break
        bytes_redundant = index

        if len(raw_data[index:]) < 3:
            return (-2, bytes_redundant)

        payload_len = struct.unpack('<H', raw_data[index+1:index+3])[0]
        if len(raw_data)-bytes_redundant < payload_len+5:
            return (-2, bytes_redundant)
        
        if raw_data[index+3+payload_len+1] != SerialProtocol.TAIL or self._checksum(raw_data[index+1:index+3+payload_len]) != raw_data[index+3+payload_len]:
            return (-3, bytes_redundant)
        else:
            return (0, bytes_redundant)

    def length(self, raw_data:bytes) -> int:
        '''
        取得有效数据包的整体长度
        '''
        if len(raw_data) < 5 or raw_data[0] != SerialProtocol.HEAD:
            return -1

        payload_len = struct.unpack('<H', raw_data[1:3])[0]
        return (3+payload_len+2)      


    def encode(self, payload:bytes) -> bytes:
        '''
        编码数据负载部分,添加帧头帧尾校验等部分
        '''
        frame = bytearray()
        frame.append(SerialProtocol.HEAD)
        frame.extend(struct.pack('<H',len(payload)))
        frame.extend(payload)
        frame.append(self._checksum(frame[1:]))
        frame.append(SerialProtocol.TAIL)

        return bytes(frame)

    def decode(self, raw_data:bytes) -> bytes:
        '''
        解码出数据负载部分
        '''
        if len(raw_data) < 5 or raw_data[0] != SerialProtocol.HEAD:
            return bytes()
        
        payload_len = struct.unpack('<H', raw_data[1:3])[0]
        return raw_data[3:3+payload_len]

if __name__ == '__main__':
    payload = 'hello'
    proto = SerialProtocol()

    encoded = proto.encode(payload.encode())
    print(encoded.hex())

    encoded = bytes([0x01,  0x02]) + encoded
    valid = proto.is_valid(encoded)
    print(valid)

    decoded = encoded[valid[1]:]
    decoded = proto.decode(decoded)
    print(decoded.decode())
   

串口协议通俗解释

把这段代码想象成"寄快递"就懂了------

只不过寄的不是衣服鞋子,而是 "一串字节数据"


1️⃣ 为什么要"打包"?

串口(就像一条电话线)只能 一个字节一个字节 地发。

如果直接发 "hello",对方可能不知道:

  • 从哪开始读?

  • 读多长?

  • 有没有读错?

于是我们要给数据 穿上一层"快递包装" ------ 这就是 串口协议


2️⃣ 快递包装长什么样?

代码里写得很清楚:

包装部分 相当于快递的什么? 代码里的字节 作用
帧头 快递上的"收件人"标签 0xAA 告诉对方:"我要开始发数据啦!"
长度 包裹里有多少件衣服 2字节小端长度 告诉对方:"后面还有这么多个字节要读。"
数据 衣服本身 hello 的字节 真正有用的内容
校验和 包裹里的"验货清单" 1字节累加和 检查路上有没有丢/错字节
帧尾 快递胶带封口 0x55 告诉对方:"我读完了!"

3️⃣ 发快递(编码)

encode() 就像 打包

  • "hello" 变成 68 65 6C 6C 6F(ASCII)。

  • 再在前面加 AA 05 00,后面加校验和 78 和帧尾 55

    最终发出去的是一条完整的字节流:

AA 05 00 68 65 6C 6C 6F 78 55


4️⃣ 收快递(解码)

decode() 就像 拆包

  • 先找 0xAA(确认是"我的快递")。

  • 读接下来的 2 字节长度,知道后面有多少数据。

  • 读完数据后,用 校验和 验货。

  • 最后看到 0x55 封口,确认包裹完整。


5️⃣ 快递丢了怎么办?

is_valid() 会告诉你:

  • -1:包裹是空的(参数错误)。

  • -2:包裹没发完(长度不够)。

  • -3:包裹破了(校验和不对)。

如果一切正常,返回 0,并告诉你前面有多少 废字节(比如快递单贴歪了)。


6️⃣ 总结一句话

串口协议就像 给数据穿"快递包装"

  • 帧头/帧尾 告诉对方"开始/结束"。

  • 长度 告诉对方"读多少"。

  • 校验和 检查"有没有读错"。

这样,就算线路有干扰,对方也能 准确无误地拿到你的"hello"

结合代码详细解释

下面用 "寄快递"类比 + 逐行源码拆解 的方式,为完全零基础的小白把这段串口协议代码讲透。

读完你会知道:

  1. 每一行代码到底在干什么;

  2. 为什么必须这样做;

  3. 如果少一步会出现什么"快递事故"。


🔍 1. 协议格式总览(先背下来,后面逐字节拆解)

python 复制代码
[帧头1B] [长度2B] [数据N B] [校验1B] [帧尾1B]
  0xAA      L       payload   Check     0x55
  • 长度 L 采用 小端(低位在前,高位在后)。

  • 校验 = 从"长度"到"数据"所有字节 累加和 & 0xFF(只保留低8位)。


📦 2. 打包函数 encode() ------ 把普通数据变成"快递包裹"

python 复制代码
def encode(self, payload: bytes) -> bytes:
    frame = bytearray()
    frame.append(SerialProtocol.HEAD)          # ① 贴快递单:0xAA
    frame.extend(struct.pack('<H', len(payload)))  # ② 写包裹长度(2字节小端)
    frame.extend(payload)                      # ③ 放衣服(真实数据)
    frame.append(self._checksum(frame[1:]))    # ④ 放验货清单(校验和)
    frame.append(SerialProtocol.TAIL)          # ⑤ 胶带封口:0x55
    return bytes(frame)

逐行生活化解释:

代码 生活动作 如果漏掉会怎样
append(0xAA) 在包裹最前面写"这是给你的快递" 收件人不知道包裹从哪开始,直接拒收
struct.pack('<H', len) 写"里面有5件衣服" 收件人不知道要读多少字节,可能多读/少读
extend(payload) 把5件衣服放进去 没衣服,空包裹
_checksum(frame[1:]) 放一张"清单",数字=衣服件数+尺寸累加 路上掉了一件,对方发现清单对不上,直接退件
append(0x55) 最后贴封条"读完了" 收件人永远等不到"结束",以为后面还有

🔍 3. 校验函数 _checksum() ------ 验货清单怎么算

python 复制代码
def _checksum(self, data: bytes) -> int:
    check_sum = 0
    for a in data:
        check_sum = (check_sum + a) & 0xFF  # 只保留低8位
    return check_sum
  • 把"长度+数据"每个字节相加,超过255就截断(像只保留发票后两位)。

  • 收件人收到后也用同样算法算一遍,结果必须 完全一致


📬 4. 拆包函数 decode() ------ 把包裹还原成衣服

python 复制代码
def decode(self, raw_data: bytes) -> bytes:
    if 长度不足 or 帧头不是0xAA:          # 包裹太短/单号不对
        return bytes()                    # 直接拒收
    payload_len = struct.unpack('<H', raw_data[1:3])[0]  # 读清单
    return raw_data[3:3+payload_len]      # 剪开胶带,取衣服

✅ 5. 完整"快递流程"演示(运行 __main__

python 复制代码
payload = 'hello'
encoded = proto.encode(payload.encode())
# 得到十六进制字节流:
# AA 05 00 68 65 6C 6C 6F 78 55
# 解释:
# AA(帧头) 0500(长度5) 68656C6C6F(hello) 78(校验) 55(帧尾)

再模拟 "路上混进垃圾字节"

python 复制代码
encoded = bytes([0x01, 0x02]) + encoded
# 现在前面多了 01 02 两个垃圾
valid = proto.is_valid(encoded)
# 返回 (0, 2)
# 0   → 包裹有效
# 2   → 前面有2字节垃圾,跳过它们再拆包
decoded = encoded[2:]       # 去掉垃圾
decoded = proto.decode(decoded)
print(decoded.decode())     # 得到 'hello'

🚧 6. 常见"快递事故"与代码对应

事故 代码表现 is_valid()返回值
快递单贴错(帧头不是0xAA) raw_data[0] != 0xAA (-3, ...)
包裹被撕开少字节 len(raw_data) < 长度字段要求 (-2, ...)
清单与衣服不符 校验和不匹配 (-3, ...)
空包裹 len(payload)=0 (-1, ...)

🎓 小白一句话总结

这段代码就是 "自动打包 + 自动验货 + 自动拆包" 的三件套:

  • encode() 把普通数据套上一层"快递包装";

  • is_valid() 检查包裹有没有破/少/错;

  • decode() 把包装撕掉,只把真正的"衣服"(payload)交给你。

只要收发双方都按这个格式走,哪怕串口线嘈杂、偶尔丢字节,也能 100% 准确 拿到原始数据。

与STM32的串口协议数据传输

python 复制代码
from maix import camera, display, image, nn, app, uart, time
import struct
import serial_protocol

detector = nn.Retinaface(model="/root/models/retinaface.mud")

cam = camera.Camera(detector.input_width(), detector.input_height(), detector.input_format())
dis = display.Display()

comm_proto = serial_protocol.SerialProtocol()
device = "/dev/ttyS0"
serial = uart.UART(device, 115200)

while not app.need_exit():
    img = cam.read()
    objs = detector.detect(img, conf_th = 0.4, iou_th = 0.45)
    for obj in objs:
        img.draw_rect(obj.x, obj.y, obj.w, obj.h, color = image.COLOR_RED)
        #print(obj.x, obj.y, obj.w, obj.h)
        payload = struct.pack('<iiii',obj.x,obj.y,obj.w,obj.h)
        
        encoded = comm_proto.encode(payload)
        serial.write(encoded)
        print(encoded.hex(' '))
    #time.sleep_ms(50)

    dis.show(img)

把这段代码想象成 "一台带摄像头的小电脑,看到人脸后立刻把人脸坐标发快递给朋友"

下面把它拆成 小学生都能听懂的 4 步流程,再逐行、逐参数讲透。


🧩 整体 4 步流程(先背下来,再往下看细节)

  1. 开机准备:打开摄像头 + 打开串口(快递通道)。

  2. 一直拍照:每拍一张,立刻用 AI 模型找人脸。

  3. 发现人脸 :把人脸的 x, y, 宽, 高 4 个数,打包成 16 个字节的小包裹。

  4. 发快递 :用"串口协议"给它贴上 帧头、长度、校验、帧尾,然后丢进串口发走。


📷 第 1 段:开机准备(3 行)

python 复制代码
detector = nn.Retinaface(model="/root/models/retinaface.mud")

装一个"人脸检测大脑",名字叫 RetinaFace,文件在 /root/models/retinaface.mud

python 复制代码
cam = camera.Camera(detector.input_width(), detector.input_height(), detector.input_format())

打开摄像头,让它的分辨率、格式跟 AI 大脑匹配(否则会报错)。

python 复制代码
dis = display.Display()

打开屏幕,让我们能看到实时画面。


🔌 第 2 段:打开串口(快递通道)

python 复制代码
comm_proto = serial_protocol.SerialProtocol()

准备一个"打包工具箱"(上一课学的串口协议类),后面用来给数据穿"快递包装"。

python 复制代码
device = "/dev/ttyS0"
serial = uart.UART(device, 115200)

打开"快递卡车"------串口 /dev/ttyS0,车速 115200 波特(比特/秒)。

115200 越大,车越快,但太远/线太长容易出错。


🔁 第 3 段:主循环------一直拍照、找人、发包

python 复制代码
while not app.need_exit():

一直重复,直到你按停止键。

python 复制代码
    img = cam.read()

拍一张照片,存在变量 img 里。

python 复制代码
    objs = detector.detect(img, conf_th=0.4, iou_th=0.45)

把照片给 AI 大脑检测人脸。

  • conf_th=0.4:只保留 信心值 ≥ 40% 的框(太低可能是误报)。

  • iou_th=0.45:如果两个框重叠 45% 以上,只保留最自信的那个。


🎯 第 4 段:发现人脸后做什么?

python 复制代码
    for obj in objs:

每找到一张人脸(可能有多个),就执行下面动作。

① 画红框

python 复制代码
        img.draw_rect(obj.x, obj.y, obj.w, obj.h, color=image.COLOR_RED)

在屏幕上画一个红色矩形,左上角是 (obj.x, obj.y),宽 obj.w,高 obj.h

② 把坐标打包成 16 字节

python 复制代码
        payload = struct.pack('<iiii', obj.x, obj.y, obj.w, obj.h)

把 4 个整数 x, y, w, h 变成 16 个连续字节。

  • <:小端字节序(低位在前)。

  • i:一个 4 字节带符号整数。

    结果:payload 长度固定 16 字节。

③ 给 16 字节穿"快递包装"

python 复制代码
        encoded = comm_proto.encode(payload)

调用上一课学的 SerialProtocol,在 16 字节前后加上:

  • 帧头 0xAA

  • 长度 0x10 0x00(16 的小端表示)

  • 校验和

  • 帧尾 0x55

    最终 encoded 长度 = 1 + 2 + 16 + 1 + 1 = 21 字节

④ 把包裹塞进串口卡车

python 复制代码
        serial.write(encoded)

21 字节一口气从 TX 引脚飞出去,另一端的电脑/单片机就能收到。

⑤ 调试用:打印 21 字节

python 复制代码
        print(encoded.hex(' '))

串口监视器里能看到类似
aa 10 00 64 00 00 00 46 00 00 00 32 00 00 00 24 00 00 00 7c 55

方便肉眼检查格式是否正确。


📺 第 5 段:把画面显示出来

python 复制代码
    dis.show(img)

把带红框的实时画面送到屏幕,你就能看到摄像头在追踪人脸。


🚫 被注释掉的延时

python 复制代码
    #time.sleep_ms(50)

如果取消注释,每帧停 50 ms,帧率会降到约 20 FPS;

不延时则全速运行,串口可能发得更快。


🎒 小白一句话总结

这段程序就是 "摄像头 + AI 人脸识别 + 串口快递" 三合一:

  1. 每拍一张照片,AI 找人脸。

  2. 把人脸的 x, y, w, h 4 个数字变成 16 字节。

  3. SerialProtocol 给它穿 21 字节的"快递包装"。

  4. 通过 /dev/ttyS0 115200 波特发出去,另一头就能实时收到"有人脸在画面哪个位置"。

TI端串口通信协议模块代码编写

.c文件

cpp 复制代码
#include "serial_protocol.h"
#include <string.h>

#define HEAD 0xAA
#define TAIL 0x55

/* 计算校验和 */
static uint8_t check_sum(uint8_t *data, uint32_t len)
{
    uint8_t sum = 0;
    for (uint32_t i = 0; i < len; i++) {
        sum += data[i];
    }
    return sum;
}

/* 检查数据包有效性 */
int32_t packet_is_valid(uint8_t *data, uint32_t len, uint32_t *redundant)
{
    if (!data || !len || !redundant) return -1;

    uint32_t idx = 0;
    while (idx < len && data[idx] != HEAD) idx++;
    *redundant = idx;

    if (len - idx < 3) return -2;

    uint16_t payload_len;
    memcpy(&payload_len, &data[idx + 1], 2);

    if (len - idx < payload_len + 5) return -2;

    if (data[idx + 3 + payload_len + 1] != TAIL ||
        check_sum(&data[idx + 1], 2 + payload_len) != data[idx + 3 + payload_len])
        return -3;

    return 0;
}

/* 获取完整包长度 */
uint32_t packet_length(uint8_t *data, uint32_t len)
{
    if (!data || len < 5 || data[0] != HEAD) return 0;

    uint16_t payload_len;
    memcpy(&payload_len, &data[1], 2);
    return 3 + payload_len + 2;
}

/* 编码:给 payload 穿协议外套 */
int32_t packet_encode(uint8_t *payload, uint32_t len,
                      uint8_t *packet_buff, uint32_t buff_len)
{
    if (!payload || !packet_buff || (len + 5) > buff_len) return -1;

    uint32_t idx = 0;
    packet_buff[idx++] = HEAD;

    memcpy(&packet_buff[idx], &len, 2);   // 小端长度
    idx += 2;

    memcpy(&packet_buff[idx], payload, len);
    idx += len;

    packet_buff[idx++] = check_sum(&packet_buff[1], 2 + len);
    packet_buff[idx++] = TAIL;

    return idx;   // 返回实际包长度
}

/* 解码:脱掉协议外套,得到 payload */
int32_t packet_decode(uint8_t *data, uint32_t len,
                      uint8_t *payload_buff, uint32_t buff_len)
{
    if (!data || len < 5 || data[0] != HEAD || !payload_buff) return -1;

    uint16_t payload_len;
    memcpy(&payload_len, &data[1], 2);

    if (len < payload_len + 5 || buff_len < payload_len) return -1;

    memcpy(payload_buff, &data[3], payload_len);
    return payload_len;
}

.h文件

cpp 复制代码
#ifndef __SERIAL_PROTOCOL_H
#define __SERIAL_PROTOCOL_H

#include <stdint.h>

int32_t  packet_is_valid(uint8_t *data, uint32_t len, uint32_t *redundant);
uint32_t packet_length (uint8_t *data, uint32_t len);
int32_t  packet_encode (uint8_t *payload, uint32_t len,
                        uint8_t *packet_buff, uint32_t buff_len);
int32_t  packet_decode (uint8_t *data, uint32_t len,
                        uint8_t *payload_buff, uint32_t buff_len);

#endif

main.c中应用示例

cpp 复制代码
#include "ti_msp_dl_config.h"   // SysConfig 生成的头文件
#include "serial_protocol.h"

#define BUF_LEN  128

static uint8_t  txBuf[BUF_LEN];
static uint8_t  rxBuf[BUF_LEN];
static uint8_t  payload[BUF_LEN];

/* 阻塞发送 len 字节 */
static void uart_send(uint8_t *data, uint32_t len)
{
    for (uint32_t i = 0; i < len; i++)
        DL_UART_Main_transmitDataBlocking(UART0_INST, data[i]);
}

/* 非阻塞接收,返回已收到字节数 */
static uint32_t uart_recv(uint8_t *data, uint32_t maxLen)
{
    uint32_t cnt = 0;
    while ((cnt < maxLen) && DL_UART_isRXFIFOEmpty(UART0_INST) == false)
        data[cnt++] = DL_UART_Main_receiveData(UART0_INST);
    return cnt;
}

int main(void)
{
    SYSCFG_DL_init();          // SysConfig 生成的初始化

    /* 测试:发送一包 demo */
    uint8_t demo[] = "HelloMSPM0";
    int32_t pktLen = packet_encode(demo, sizeof(demo), txBuf, BUF_LEN);
    if (pktLen > 0) uart_send(txBuf, pktLen);

    while (1)
    {
        uint32_t rxCnt = uart_recv(rxBuf, BUF_LEN);
        if (rxCnt)
        {
            uint32_t skip = 0;
            int32_t ret = packet_is_valid(rxBuf, rxCnt, &skip);
            if (ret == 0)
            {
                int32_t payloadLen = packet_decode(&rxBuf[skip], rxCnt - skip,
                                                   payload, BUF_LEN);
                if (payloadLen > 0)
                {
                    /* 在这里处理收到的 payload */
                    /* 例如:DL_GPIO_togglePins(GPIOA, DL_GPIO_PIN_18_PIN); */
                }
            }
        }
    }
}
相关推荐
mortimer1 小时前
安装NVIDIA Parakeet时,我遇到的两个Pip“小插曲”
python·github
@昵称不存在1 小时前
Flask input 和datalist结合
后端·python·flask
赵英英俊2 小时前
Python day25
python
东林牧之2 小时前
Django+celery异步:拿来即用,可移植性高
后端·python·django
_Kayo_2 小时前
VUE2 学习笔记6 vue数据监测原理
vue.js·笔记·学习
何双新2 小时前
基于Tornado的WebSocket实时聊天系统:从零到一构建与解析
python·websocket·tornado
chenchihwen3 小时前
大模型应用班-第2课 DeepSeek使用与提示词工程课程重点 学习ollama 安装 用deepseek-r1:1.5b 分析PDF 内容
人工智能·学习
Ronin-Lotus3 小时前
嵌入式硬件篇---有线串口通信问题解决
单片机·嵌入式硬件·ttl·rs232·rs485·有线串口
超浪的晨3 小时前
Java UDP 通信详解:从基础到实战,彻底掌握无连接网络编程
java·开发语言·后端·学习·个人开发
AntBlack3 小时前
从小不学好 ,影刀 + ddddocr 实现图片验证码认证自动化
后端·python·计算机视觉