【电赛学习笔记】MaxiCAM 项目实践——与单片机的串口通信
前言
本文是对视觉模块和STM32如何进行串口通信_哔哩哔哩_bilibili大佬的项目实践与拓展实现与mspm0g3507的串口通信,侵权即删。
MaxiCAM与STM32串口通信实践
串口协议数据传输
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 int: \'\'\' 取得有效数据包的整体长度 \'\'\' if len(raw_data) < 5 or raw_data[0] != SerialProtocol.HEAD: return -1 payload_len = struct.unpack(\' bytes: \'\'\' 编码数据负载部分,添加帧头帧尾校验等部分 \'\'\' frame = bytearray() frame.append(SerialProtocol.HEAD) frame.extend(struct.pack(\' 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. 协议格式总览(先背下来,后面逐字节拆解)
[帧头1B] [长度2B] [数据N B] [校验1B] [帧尾1B] 0xAA L payload Check 0x55
-
长度 L 采用 小端(低位在前,高位在后)。
-
校验 = 从“长度”到“数据”所有字节 累加和 & 0xFF(只保留低8位)。
📦 2. 打包函数 encode()
—— 把普通数据变成“快递包裹”
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)
extend(payload)
_checksum(frame[1:])
append(0x55)
🔍 3. 校验函数 _checksum()
—— 验货清单怎么算
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()
—— 把包裹还原成衣服
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__
)
payload = \'hello\'encoded = proto.encode(payload.encode())# 得到十六进制字节流:# AA 05 00 68 65 6C 6C 6F 78 55# 解释:# AA(帧头) 0500(长度5) 68656C6C6F(hello) 78(校验) 55(帧尾)
再模拟 “路上混进垃圾字节”:
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. 常见“快递事故”与代码对应
🎓 小白一句话总结
这段代码就是 “自动打包 + 自动验货 + 自动拆包” 的三件套:
-
encode() 把普通数据套上一层“快递包装”;
-
is_valid() 检查包裹有没有破/少/错;
-
decode() 把包装撕掉,只把真正的“衣服”(payload)交给你。
只要收发双方都按这个格式走,哪怕串口线嘈杂、偶尔丢字节,也能 100% 准确 拿到原始数据。
与STM32的串口协议数据传输
from maix import camera, display, image, nn, app, uart, timeimport structimport serial_protocoldetector = 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 步流程(先背下来,再往下看细节)
-
开机准备:打开摄像头 + 打开串口(快递通道)。
-
一直拍照:每拍一张,立刻用 AI 模型找人脸。
-
发现人脸:把人脸的 x, y, 宽, 高 4 个数,打包成 16 个字节的小包裹。
-
发快递:用“串口协议”给它贴上 帧头、长度、校验、帧尾,然后丢进串口发走。
📷 第 1 段:开机准备(3 行)
detector = nn.Retinaface(model=\"/root/models/retinaface.mud\")
装一个“人脸检测大脑”,名字叫 RetinaFace,文件在
/root/models/retinaface.mud
。
cam = camera.Camera(detector.input_width(), detector.input_height(), detector.input_format())
打开摄像头,让它的分辨率、格式跟 AI 大脑匹配(否则会报错)。
dis = display.Display()
打开屏幕,让我们能看到实时画面。
🔌 第 2 段:打开串口(快递通道)
comm_proto = serial_protocol.SerialProtocol()
准备一个“打包工具箱”(上一课学的串口协议类),后面用来给数据穿“快递包装”。
device = \"/dev/ttyS0\"serial = uart.UART(device, 115200)
打开“快递卡车”——串口
/dev/ttyS0
,车速 115200 波特(比特/秒)。
115200 越大,车越快,但太远/线太长容易出错。
🔁 第 3 段:主循环——一直拍照、找人、发包
while not app.need_exit():
一直重复,直到你按停止键。
img = cam.read()
拍一张照片,存在变量
img
里。
objs = detector.detect(img, conf_th=0.4, iou_th=0.45)
把照片给 AI 大脑检测人脸。
conf_th=0.4
:只保留 信心值 ≥ 40% 的框(太低可能是误报)。
iou_th=0.45
:如果两个框重叠 45% 以上,只保留最自信的那个。
🎯 第 4 段:发现人脸后做什么?
for obj in objs:
每找到一张人脸(可能有多个),就执行下面动作。
① 画红框
img.draw_rect(obj.x, obj.y, obj.w, obj.h, color=image.COLOR_RED)
在屏幕上画一个红色矩形,左上角是
(obj.x, obj.y)
,宽obj.w
,高obj.h
。
② 把坐标打包成 16 字节
payload = struct.pack(\'<iiii\', obj.x, obj.y, obj.w, obj.h)
把 4 个整数
x, y, w, h
变成 16 个连续字节。
<
:小端字节序(低位在前)。
i
:一个 4 字节带符号整数。
结果:payload
长度固定 16 字节。
③ 给 16 字节穿“快递包装”
encoded = comm_proto.encode(payload)
调用上一课学的
SerialProtocol
,在 16 字节前后加上:
帧头
0xAA
长度
0x10 0x00
(16 的小端表示)校验和
帧尾
0x55
最终encoded
长度 = 1 + 2 + 16 + 1 + 1 = 21 字节。
④ 把包裹塞进串口卡车
serial.write(encoded)
21 字节一口气从 TX 引脚飞出去,另一端的电脑/单片机就能收到。
⑤ 调试用:打印 21 字节
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 段:把画面显示出来
dis.show(img)
把带红框的实时画面送到屏幕,你就能看到摄像头在追踪人脸。
🚫 被注释掉的延时
#time.sleep_ms(50)
如果取消注释,每帧停 50 ms,帧率会降到约 20 FPS;
不延时则全速运行,串口可能发得更快。
🎒 小白一句话总结
这段程序就是 “摄像头 + AI 人脸识别 + 串口快递” 三合一:
-
每拍一张照片,AI 找人脸。
-
把人脸的 x, y, w, h 4 个数字变成 16 字节。
-
用
SerialProtocol
给它穿 21 字节的“快递包装”。 -
通过
/dev/ttyS0
115200 波特发出去,另一头就能实时收到“有人脸在画面哪个位置”。
TI端串口通信协议模块代码编写
.c文件
#include \"serial_protocol.h\"#include #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 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文件
#ifndef __SERIAL_PROTOCOL_H#define __SERIAL_PROTOCOL_H#include 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中应用示例
#include \"ti_msp_dl_config.h\" // SysConfig 生成的头文件#include \"serial_protocol.h\"#define BUF_LEN 128static 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 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); */ } } } }}