【电赛学习笔记】MaxiCAM 项目实践——与单片机的串口通信
前言
本文是对视觉模块和STM32如何进行串口通信_哔哩哔哩_bilibili大佬的项目实践与拓展实现与mspm0g3507的串口通信,侵权即删。
MaxiCAM与STM32串口通信实践
串口协议数据传输
import struct'''
协议数据格式:
帧头(0xAA) + 数据域长度 + 数据域 + 长度及数据域数据和校验 + 帧尾(0x55)
'''class SerialProtocol():HEAD = 0xAATAIL = 0x55def __init__(self) -> None:passdef _checksum(self, data:bytes)-> int:'''计算和校验'''check_sum = 0for a in data:check_sum = (check_sum + a) & 0xFFreturn check_sumdef is_valid(self, raw_data:bytes) -> tuple:'''判断数据是否有效返回值: -1 -- 参数错误 -2 -- 数据长度不够 -3 -- 数据格式错误'''if len(raw_data) == 0:return (-1, 0)bytes_redundant = 0index = 0for a in raw_data:if a != SerialProtocol.HEAD:index += 1else:breakbytes_redundant = indexif 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 -1payload_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]) + encodedvalid = 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) # ① 贴快递单:0xAAframe.extend(struct.pack('<H', len(payload))) # ② 写包裹长度(2字节小端)frame.extend(payload) # ③ 放衣服(真实数据)frame.append(self._checksum(frame[1:])) # ④ 放验货清单(校验和)frame.append(SerialProtocol.TAIL) # ⑤ 胶带封口:0x55return bytes(frame)
逐行生活化解释:
代码 | 生活动作 | 如果漏掉会怎样 |
---|---|---|
append(0xAA) | 在包裹最前面写“这是给你的快递” | 收件人不知道包裹从哪开始,直接拒收 |
struct.pack('<H', len) | 写“里面有5件衣服” | 收件人不知道要读多少字节,可能多读/少读 |
extend(payload) | 把5件衣服放进去 | 没衣服,空包裹 |
_checksum(frame[1:]) | 放一张“清单”,数字=衣服件数+尺寸累加 | 路上掉了一件,对方发现清单对不上,直接退件 |
append(0x55) | 最后贴封条“读完了” | 收件人永远等不到“结束”,以为后面还有 |
🔍 3. 校验函数 _checksum()
—— 验货清单怎么算
def _checksum(self, data: bytes) -> int:check_sum = 0for 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. 常见“快递事故”与代码对应
事故 | 代码表现 | is_valid()返回值 |
---|---|---|
快递单贴错(帧头不是0xAA) | raw_data[0] != 0xAA | (-3, …) |
包裹被撕开少字节 | len(raw_data) < 长度字段要求 | (-2, …) |
清单与衣服不符 | 校验和不匹配 | (-3, …) |
空包裹 | len(payload)=0 | (-1, …) |
🎓 小白一句话总结
这段代码就是 “自动打包 + 自动验货 + 自动拆包” 的三件套:
-
encode() 把普通数据套上一层“快递包装”;
-
is_valid() 检查包裹有没有破/少/错;
-
decode() 把包装撕掉,只把真正的“衣服”(payload)交给你。
只要收发双方都按这个格式走,哪怕串口线嘈杂、偶尔丢字节,也能 100% 准确 拿到原始数据。
与STM32的串口协议数据传输
from maix import camera, display, image, nn, app, uart, time
import struct
import 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 <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文件
#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中应用示例
#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 < 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); */}}}}
}