我们的终极目标就是让视觉模块与STM32进行串口通讯,那么我们就要分别先对单片机端和视觉模块端进行调试,一般他们通讯是由视觉模块发送数据帧给单片机,然后单片机对接收的数据帧进行解析,如果解析成功,然后再根据解析出来的数据帧里面的数据来进行其他操作。
通过分析,我们可以++拆分任务++
-
1、让单片机能够对数据帧进行解析(我们采用串口中断接收+状态机)
-
2、调试视觉模块
-
3、让我们的stm32与视觉模块通讯
我使用的单片机是STM32G431RBT6

视觉模块则是 maixcam

如果你和我不一样也只是其中的一些地方,但是本质思想是一样的,换汤不换药,再加上现在又AI,有不知道的直接问ai,代码也可以让ai改成你对应的单片机或者视觉模块适用的
1、MCU
基础配置
创建Cubemax工程



具体配置不再赘述,主要就配置一下LED,和串口(串口记得打开NVIC中断 ),然后我还配置了一下PD2(因为我这个板子上的LED有锁存器是PD2来控制),串口的波特率我这里选的是115200,这里大家记住自己选的是多少,到时候用串口助手时不要用错了就行
串口我这里还要选USART1,因为我单片机的的USART0是和烧录器在一起的,所以后续没法接线和视觉模相连,所以我需要使用USART1与视觉模块相连


这里我选择外部晶振,外部晶振的大小大家可以查看原理图



点击Generate Code生成即可
如果想和我一样用CLion开发可以看看我之前的这篇博客 Clion安装以及配置_clion 安装编译器-CSDN博客
打开Clion,打开我们的工程




这里圈起来的地方如果不知道,还是请看一下我之前的博客Clion安装以及配置_clion 安装编译器-CSDN博客
编写代码
我们想要实现的功能就是让我们的MCU(就是单片机)实现能够接收数据帧,并且能够解析数据
一些常见的数据帧格式
【包头】 + 【数据长度】 + 【数据】 + 【校验】 + 【包尾】
然后校验的话一般就是校验和或者CRC校验二选一
例如:

发送数据长度的话一般采用小端序 ,不了解是什么的现在去看这个文章(18 封私信) 字节序:大端序和小端序 - 知乎
或者图简单就
【包头1】 + 【包头2】 + 【数据】 + 【包尾】
假设为 0xAA(包头1) 0xBB(包头2) 数据位(假设为2为) 0XCC(包尾) 一共5位

代码如下
cpp
/* USER CODE BEGIN 1 */
uint8_t rxData = 0;
uint8_t command[32];
uint8_t msg[]= "hello";
void start_usart_recv(void)
{
HAL_UART_Receive_IT(&huart2,&rxData,1);
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_8, RESET);
GPIOD->BSRR |= 0x01 << 2; // 设置 GPIOD 第2位
GPIOD->BRR |= 0x01 << 2; // 重置 GPIOD 第2位
if (huart->Instance == USART2)
{
static int i = 0;
command[i++] = rxData;
if(command[0] != 0XAA)
i =0;
if((i == 2) && (command[1] != 0XBB))
i = 0;
if(i == 5)
{
if(command[4] == 0XCC) //开始校验
{
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13, RESET);
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_14, SET);
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_15, RESET);
GPIOD->BSRR |= 0x01 << 2; // 设置 GPIOD 第2位
GPIOD->BRR |= 0x01 << 2; // 重置 GPIOD 第2位
HAL_UART_Transmit(&huart1, (uint8_t *)command, 5, HAL_MAX_DELAY); //将发送出去的数据通过串口0发送,方便我们调试
HAL_UART_Transmit(&huart2, (uint8_t *)command, 5, HAL_MAX_DELAY);
}
i = 0;
}
start_usart_recv();
}
}
/* USER CODE END 1 */
这个代码就是解析数据帧,我通过电脑的串口调试助手1发送数据给单片机,如果数据为正确的数据帧则,再通过串口0发送出去
如果不太能理解这个代码的话,推荐大家现在去看看这个视频【STM32串口通信 - 怎么一次性传输多个数据?】https://www.bilibili.com/video/BV1Uu4y1i7ch?vd_source=04ec85bac321be52c18d0e8083e4c6bc
然后我们将**start_usart_recv()**这个函数加到uart.h里

在main.c中,一开始要调用一下**start_usart_recv()** 这个函数,本质是调用**HAL_UART_Receive_IT()**(为什么一开始要调用呢,因为你要告诉他接收到数据了后数据应该往哪里存)


然后我们将程序烧录进板子然后 上电脑的串口助手调试了
调试
我们通过上位机发送 AA BB 11 22 CC

可以发现成功实现我们的要求
这里可以留个小作业,在cuebmax中勾选FreeRTOS,用加上操作系统,看看如何实现这个效果
提示,队列+状态机 (不会的话建议看看我之前的博客RTOS:keysking队列实战-CSDN博客,相信看完你会有所启发)
2、视觉模块
然后我们现在到视觉模块,然后我这里用到的视觉模块是maixcam,他的开发的IDE是下面这个

没有的安装的记得先安装一下,我们选择一份示例代码作为我们的起点

打开文件,然后我们按下ctrl + s 保存一下这个代码

have some problem
这里我出现了一些问题,我不知道为什么我的视觉模块好像发送数据好像有问题,但是我不知道他具体是的发送还是接收有问题,所以我问了一下ai他让我进行回环测试即把maixcam的视觉模块的Tx和Rx接在一起,然后烧录下面的代码:
python
from maix import uart, time
serial0 = uart.UART("/dev/ttyS0", 115200)
while True:
# 直接写十六进制字符串
data = b'\xAA\xBB\x01\x22\xCC' # ✅ 最简洁
serial0.write(data)
print(f"✅ 发送: {' '.join(f'{b:02X}' for b in data)}")
reply = serial0.read(timeout=1000)
if reply:
print(f"✅ 收到回复: {' '.join(f'{b:02X}' for b in reply)}")
else:
print("❌ 未收到回复")
time.sleep_ms(2000)
但是注意,一开始不要让RX和TX相接,等到maixcam开机后再用一根杜邦线连接他的Tx和Rx

如果现象如下,那么说明maixcam的接收和发送没有问题

那么我们再来验证一下STM32的收发,目标是让他将接受到的数据再发送出去
我的相关代码如下,这是我找的之前的一个工程然后简单改了一下代码


都勾选了开启NVIC中断和DMA


main.c中记得添加

usart_app.c
cpp
#include "uart_app.h"
#define BUUFER_SIZE 64
// 定义环形缓冲区和接收缓冲区
//串口1
ringbuffer_t usart1_rb;
uint8_t usart1_read_buffer[BUUFER_SIZE];
uint8_t uart1_rx_dma_buffer[128] = {0};
//串口2
ringbuffer_t usart2_rb;
uint8_t usart2_read_buffer[BUUFER_SIZE];
uint8_t uart2_rx_dma_buffer[128] = {0};
int my_printf(UART_HandleTypeDef *huart, const char *format, ...)
{
char buffer[512];
va_list arg;
int len;
// 初始化可变参数列表
va_start(arg, format);
len = vsnprintf(buffer, sizeof(buffer), format, arg);
va_end(arg);
HAL_UART_Transmit(huart, (uint8_t *)buffer, (uint16_t)len, 0xFF);
return len;
}
/**
* @brief UART DMA接收完成回调函数
* 将接收到的数据写入环形缓冲区,并清空DMA缓冲区
* @param huart UART句柄
* @param Size 接收到的数据大小
* @retval None
*/
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
// 1. 确认是目标串口 (USART1)
if (huart->Instance == USART1)
{
// 2. 紧急停止当前的 DMA 传输 (如果还在进行中)
// 因为空闲中断意味着发送方已经停止,防止 DMA 继续等待或出错
HAL_UART_DMAStop(huart);
// 如果环形缓冲区未满
if (!ringbuffer_is_full(&usart1_rb))
{
// 将DMA缓冲区中的数据写入环形缓冲区
ringbuffer_write(&usart1_rb, uart1_rx_dma_buffer, Size);
}
// 清空DMA缓冲区
memset(uart1_rx_dma_buffer, 0, sizeof(uart1_rx_dma_buffer));
// 6. **关键:重新启动下一次 DMA 空闲接收**
// 必须再次调用,否则只会接收这一次
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, uart1_rx_dma_buffer, sizeof(uart1_rx_dma_buffer));
// 7. 如果之前关闭了半满中断,可能需要在这里再次关闭 (根据需要)
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT);
ucLed[4] = 1;
} else if (huart->Instance == USART2)
{
ucLed[6] = 1 - ucLed[6];
// 2. 紧急停止当前的 DMA 传输 (如果还在进行中)
// 因为空闲中断意味着发送方已经停止,防止 DMA 继续等待或出错
HAL_UART_DMAStop(huart);
// 如果环形缓冲区未满
if (!ringbuffer_is_full(&usart2_rb))
{
// 将DMA缓冲区中的数据写入环形缓冲区
ringbuffer_write(&usart2_rb, uart2_rx_dma_buffer, Size);
}
// 清空DMA缓冲区
memset(uart2_rx_dma_buffer, 0, sizeof(uart2_rx_dma_buffer));
// 6. **关键:重新启动下一次 DMA 空闲接收**
// 必须再次调用,否则只会接收这一次
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, uart2_rx_dma_buffer, sizeof(uart2_rx_dma_buffer));
// 7. 如果之前关闭了半满中断,可能需要在这里再次关闭 (根据需要)
__HAL_DMA_DISABLE_IT(&hdma_usart2_rx, DMA_IT_HT);
ucLed[5] = 1;
}
}
/**
* @brief 处理UART接收缓冲区中的数据。
* 如果在100ms内没有接收到新的数据,将清空缓冲区。
* @param None
* @retval None
*/
void uart_proc(void)
{
// 如果环形缓冲区为空,直接返回
if (ringbuffer_is_empty(&usart1_rb) == 0)
{
// 从环形缓冲区读取数据到读取缓冲区
ringbuffer_read(&usart1_rb, usart1_read_buffer, usart1_rb.itemCount);
// // 打印读取缓冲区中的数据
my_printf(&huart1,"usart1 recv data: %s\n", usart1_read_buffer);
// 清空读取缓冲区
memset(usart1_read_buffer, 0, sizeof(uint8_t) * BUUFER_SIZE);
}else if( ringbuffer_is_empty(&usart2_rb) == 0)
{
ringbuffer_read(&usart2_rb, usart2_read_buffer, usart2_rb.itemCount);
my_printf(&huart1,"usart2 recv data: %s\n", usart2_read_buffer); //串口2我们用于与esp15f连接,然后我们将返回的数据通过串口1打印到串口助手上方便我们调试
memset(usart2_read_buffer, 0, sizeof(uint8_t) * BUUFER_SIZE);
}
}
usart_app.h
cpp
#ifndef __UART_APP_H__
#define __UART_APP_H__
#include "bsp_system.h"
int my_printf(UART_HandleTypeDef *huart, const char *format, ...);
void uart_proc(void);
#endif
因为我这个工程的串口部分是采用的空闲中断+DMA+环形缓存区,
环形缓存区代码如下
ringbuffer.c
#include "ringbuffer.h"
// 初始化环形缓冲区
void ringbuffer_init(ringbuffer_t *rb)
{
// 设置读指针和写指针初始值为0
rb->r = 0;
rb->w = 0;
// 将缓冲区内存清零
memset(rb->buffer, 0, sizeof(uint8_t) * RINGBUFFER_SIZE);
// 初始化项目计数为0
rb->itemCount = 0;
}
// 检查环形缓冲区是否已满
uint8_t ringbuffer_is_full(ringbuffer_t *rb)
{
// 如果项目计数等于缓冲区大小,返回1(已满),否则返回0(未满)
return (rb->itemCount == RINGBUFFER_SIZE);
}
// 检查环形缓冲区是否为空
uint8_t ringbuffer_is_empty(ringbuffer_t *rb)
{
// 如果项目计数为0,返回1(为空),否则返回0(非空)
return (rb->itemCount == 0);
}
// 向环形缓冲区写入数据
int8_t ringbuffer_write(ringbuffer_t *rb, uint8_t *data, uint32_t num)
{
// 如果缓冲区已满,返回-1
if(ringbuffer_is_full(rb))
return -1;
// 将数据写入缓冲区
while(num--)
{
rb->buffer[rb->w] = *data++; // 写入数据并移动写指针
rb->w = (rb->w + 1) % RINGBUFFER_SIZE; // 写指针循环递增
rb->itemCount++; // 增加项目计数
}
return 0; // 写入成功返回0
}
// 从环形缓冲区读取数据
int8_t ringbuffer_read(ringbuffer_t *rb, uint8_t *data, uint32_t num)
{
// 如果缓冲区为空,返回-1
if(ringbuffer_is_empty(rb))
return -1;
// 从缓冲区读取数据
while(num--)
{
*data++ = rb->buffer[rb->r]; // 读取数据并移动读指针
rb->r = (rb->r + 1) % RINGBUFFER_SIZE; // 读指针循环递增
rb->itemCount--; // 减少项目计数
}
return 0; // 读取成功返回0
}
ringbuffer.h
cpp
#ifndef RINGBUFFER_H
#define RINGBUFFER_H
#include "bsp_system.h"
#define RINGBUFFER_SIZE (30)
typedef struct {
uint32_t w;
uint32_t r;
uint8_t buffer[RINGBUFFER_SIZE];
uint32_t itemCount;
}ringbuffer_t;
void ringbuffer_init(ringbuffer_t *rb);
uint8_t ringbuffer_is_full(ringbuffer_t *rb);
uint8_t ringbuffer_is_empty(ringbuffer_t *rb);
int8_t ringbuffer_write(ringbuffer_t *rb, uint8_t *data, uint32_t num);
int8_t ringbuffer_read(ringbuffer_t *rb, uint8_t *data, uint32_t num);
extern ringbuffer_t usart_rb;
#endif

发现发送和接收也没有问题,那么问题来了,问题现在出在哪里呢?
几经周折,给单片机用了这个程序
cpp
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
MX_USART2_UART_Init();
/* USER CODE BEGIN 2 */
start_usart_recv();
/* USER CODE END 2 */
uint8_t test[] = {0xAA, 0xBB, 0x01, 0x22, 0xCC};
uint8_t recv[5];
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
// 发送
HAL_UART_Transmit(&huart2, test, 5, 100);
// 接收(阻塞1秒)
if (HAL_UART_Receive(&huart2, recv, 5, 1000) == HAL_OK)
{
// 收到数据,LED闪烁
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
GPIOD->BSRR |= 0x01 << 2; // 设置 GPIOD 第2位
GPIOD->BRR |= 0x01 << 2; // 重置 GPIOD 第2位
}
HAL_Delay(500);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
神奇解决
还是不行,就在快要放弃之时,我在思考,我的maixcam的一个转接口有四个引脚Tx,Rx,5V,GND,
然后我用的是我板子上的串口2,PA2(Tx),PA3(Rx)
正常来说,我们想要串口通讯有个让他们先共地,然后PA2接maixcam的Rx,PA3接maixcam的Tx,但是我发送maixcam家的这个转接模块好像有点独特,我需要PA2接Tx,PA3接Rx才行,也是神奇了,我真没招了😂😂😂
接线如下

现在大功告成,可以实现maixcam给单片机发送数据帧,然后单片机解析完数据帧后再讲数据帧原样发送

maixVision IDE 的代码如下
python
from maix import uart, time
serial0 = uart.UART("/dev/ttyS0", 115200)
while True:
# 直接写十六进制字符串
data = b'\xAA\xBB\x01\x22\xCC' # ✅ 最简洁
serial0.write(data)
print(f"✅ 发送: {' '.join(f'{b:02X}' for b in data)}")
reply = serial0.read(timeout=1000)
if reply:
print(f"✅ 收到回复: {' '.join(f'{b:02X}' for b in reply)}")
else:
print("❌ 未收到回复")
time.sleep_ms(2000)
STM32的代码见MCU那里
New Things
现在我们我们已经实现了stm32和maixcam视觉模块的简单通讯,那么让我们慢慢深入-------其实我在了解视觉模块与STM32进行串口通讯之前我有一些疑惑,然后我问了一下ai
my question
大家平时向AI提问时也可以参考我这样提问,当然如果有什么更好的提问方式欢迎分享到评论区
如果你能看到这里来,说明你有一些耐心,很厉害,那么我将掏出焚诀,看完剩下的内容你一定会对++视觉模块与STM32进行串口通讯++有一些自己的了解
为什么突然想弄这个呢?因为之前我的一个学长突然问我视觉模块与STM32进行串口通讯应该怎么弄,我当时心想好像自己一直没有去接触过这个,之前知识简单(非常粗略)的了解了一下,只知道用串口应该可以通讯,但是没有自己尝试过,也不知道具体应该是什么样的,也没有自己跑过,然后就像尝试一下,再一个是可以让实验室的后代以后学这一部分内容的时候可以轻松一点,当然我也只是了个门,还是小白吗,因为之前都没有接触过视觉,只是觉得视觉好像很厉害,所以害怕它很难,看来人还是要祛魅的,天下所以的事情最难得只有一件事,那就是开始。
好了言归正传,我的提问:
首先我是想知道,maixcam视觉模块如何和stm32如何进行串口通信,然后如果他们已经能够串口通讯了,那是不是就可以让maixcam跑一些模型,然后讲结果通过串口发送给mcu,mcu在根据接收到的数据来控制其他的东西对吗, 但是我不知道maixcam的模型是怎么跑的,以及怎么样让他技能跑特定的模型还能够通过串口发送数据给mcu, 你能懂我疑惑点吗?如果你还有哪里不懂你可以先问我然后再回答,如果你懂我的意思就请你给我解答一下
AI答疑--gemini
我完全懂你的意思,也明白你的疑惑点。你的逻辑闭环是非常清晰且正确的。
简单来说,你的困惑在于:"我知道MaixCam能跑AI,也知道它能发串口,但这两个动作怎么在一个程序里'串'起来?也就是模型识别出的'结果'(比如坐标、物体名称)是怎么变成串口能发送的'字节'的?"
下面我为你拆解这个过程,把你脑海中的"黑盒"打开。
第一步:确认你的逻辑闭环 (完全正确)
你的设想完全可行,这也是嵌入式视觉最主流的玩法:
MaixCam (大脑 1):负责"看"和"想"。运行AI模型,从画面中提取有用信息(比如:前面有一个苹果,坐标是 X=50, Y=100)。
UART (传话筒):MaixCam 把"苹果, 50, 100"这几个数据打包,通过串口线发出去。
STM32 (大脑 2 / 执行者):接收数据,解析出"哦,原来是苹果在(50,100)",然后控制电机或舵机去抓取。
第二步:解答疑惑------MaixCam 如何"一边跑模型,一边发数据"
MaixCam 内部通常运行的是 MaixPy (基于 Python) 或者是 C++ 程序。以最常用的 Python 为例,这其实就是一个死循环(While True),在循环里按顺序做两件事:识别 + 发送。
代码的逻辑结构是这样的(伪代码):-------------------注意是伪代码,不能直接用,主要看思想
cpp
# 1. 初始化部分
import sensor, image, time
from machine import UART # 导入串口库
import maix.kpu as kpu # 导入AI模型库
# 初始化串口,波特率要和STM32一样,比如115200
uart = UART(1, baudrate=115200)
# 加载你的模型(比如人脸识别或物体检测)
task = kpu.load("你的模型文件.kmodel")
# 2. 循环部分 (这部分一直在重复跑)
while(True):
# A. 拍照/获取图像
img = sensor.snapshot()
# B. 跑模型 (这就是你疑惑的起点)
# 这一步会返回一个列表,里面包含了所有识别到的物体信息
# 比如 objects 可能等于 [{x:10, y:20, class:1}, {x:80, y:90, class:2}]
objects = kpu.run_yolo2(task, img)
# C. 处理结果并发送 (这就是你疑惑的终点)
if objects: # 如果识别到了东西
for obj in objects:
# 获取具体的数值
x_coord = obj.x()
y_coord = obj.y()
# D. 发送数据给STM32
# 这里的关键是:你需要把整数变成字节(byte)发出去
# 假设我们定一个协议:0xAA 是帧头,然后是X坐标,Y坐标,0x55是帧尾
data_to_send = bytearray([0xAA, x_coord, y_coord, 0x55])
uart.write(data_to_send)
# 在画面上画框框给自己看(可选)
img.draw_rectangle(obj.rect())
解答你的核心疑惑:
-
模型怎么跑的? 通过
kpu.run(或类似函数) 跑,它会返回一个对象(变量),这个变量里直接就存着结果数据(坐标、类别 ID、置信度)。 -
怎么发给MCU? 你直接读取这个变量的值,用
uart.write()函数写出去,就像你在 C 语言里用printf或HAL_UART_Transmit一样简单。
第三步:硬件连接与通信协议 (关键细节)
如果这一步没做好,代码写对了也通不了。
1. 硬件接线
MaixCam TX 接 STM32 RX
MaixCam RX 接 STM32 TX
GND 接 GND (共地,非常重要,否则无法通信)
2. 通信协议 (制定"暗号")
你不能只发一个数字过去,因为串口容易丢包或错位。你需要制定一个简单的协议。 比如你想发坐标 X 和 Y,你可以这样打包:
帧头 (Header):
0x2C(用来告诉STM32,数据开始了)数据 (Data):
X坐标(高8位),X坐标(低8位),Y坐标(高8位),Y坐标(低8位)帧尾 (Ender):
0x5B(结束标志)
MaixCam端 (Python):
x = 120
y = 200
# 比如 x是120 (0x0078), 分成 0x00 和 0x78 发送
uart.write(bytearray([0x2C, (x>>8), (x&0xFF), (y>>8), (y&0xFF), 0x5B]))
STM32端 (C语言): 你需要开启串口中断(RXNE),在中断里检测:
-
收到
0x2C-> 准备接收数据 -
接收接下来的4个字节 -> 拼合成 X 和 Y
-
收到
0x5B-> 确认数据有效 -> 执行控制逻辑
gemini重磅来袭
为了让你能够直接上手,我制定了一个简单且通用的通信协议。只要 MaixCam 按这个格式发,STM32 按这个格式收,就绝对不会乱。
📅 通信协议设计 (Packet Structure)
我们要发送的数据假设是:X坐标 (int), Y坐标 (int), 物体类别 (int)。
由于坐标可能超过 255,所以需要分成高低两个字节。
| 字节顺序 | 内容 | 说明 | 示例 (Hex) |
|---|---|---|---|
| 0 | 0xAA | 帧头1 (固定) | 0xAA |
| 1 | 0x55 | 帧头2 (固定) | 0x55 |
| 2 | X 高8位 | X坐标的高位 | 0x01 (假设X=300) |
| 3 | X 低8位 | X坐标的低位 | 0x2C |
| 4 | Y 高8位 | Y坐标的高位 | 0x00 (假设Y=100) |
| 5 | Y 低8位 | Y坐标的低位 | 0x64 |
| 6 | ID | 识别到的物体ID | 0x01 |
| 7 | 校验和 | 简单的校验 (防止数据出错) | Sum(前7个字节) & 0xFF |
1. MaixCam 发送端代码 (Python)
这段代码可以直接放在你的 MaixCam (MaixPy IDE) 里运行。
这里gemini生成的代码有点问题,我修改了一下,可以直接用
cpp
############################################################################
import time
from maix import uart
import struct
# --- 配置串口 ---
# MaixCam 的串口引脚请参考官方文档,通常是 UART1 或 UART2
# 波特率设为 115200
uart = uart.UART("/dev/ttyS0", 115200)
def send_data_packet(x, y, class_id):
# 1. 准备帧头
head1 = 0xAA
head2 = 0x55
# 2. 拆分数据 (将整数拆成高低字节)
# int() 确保数据是整数
x_high = (int(x) >> 8) & 0xFF
x_low = int(x) & 0xFF
y_high = (int(y) >> 8) & 0xFF
y_low = int(y) & 0xFF
class_id = int(class_id) & 0xFF
# 3. 计算校验和 (把前面所有数据加起来,取最后8位)
# 简单的校验:让STM32算一遍,如果不相等说明传输错了
check_sum = (head1 + head2 + x_high + x_low + y_high + y_low + class_id) & 0xFF
# 4. 打包成 bytearray (方便修改和拼接)
# bytearray 也是一种列表,但里面只能存 0-255 的数字
data_bytearray = bytearray([head1, head2, x_high, x_low, y_high, y_low, class_id, check_sum])
# 5. 发送 【这里是关键修改点!】
# 必须用 bytes() 函数把 bytearray 包裹一下
uart.write(bytes(data_bytearray))
# (可选) 打印在终端方便调试看
print("Sent:", [hex(b) for b in data_bytearray])
# --- 模拟主循环 ---
while True:
# 假设这里是你的模型 kpu.run() 得到的结果
# 模拟:识别到一个坐标 (300, 100) 的物体,ID是 1
current_x = 300
current_y = 100
current_id = 1
# 发送数据
send_data_packet(current_x, current_y, current_id)
time.sleep(0.1) # 模拟每100ms识别一次
2. STM32 接收解析端代码 (C语言 / HAL库)
这段代码基于你之前提供的代码风格进行了适配。我们使用 状态机 来解析数据,这是最稳健的方法。
在 main.c 或 相关的 .c 文件中:
cpp
/* 全局变量定义 */
uint8_t rx_byte; // 串口接收缓冲 (1个字节)
uint8_t rx_state = 0; // 状态机状态:0=找包头1, 1=找包头2, 2=接收数据
uint8_t rx_buffer[10]; // 暂存接收到的有效数据
uint8_t rx_count = 0; // 接收计数索引
/* 定义一个结构体方便使用数据 */
typedef struct {
uint16_t x;
uint16_t y;
uint8_t id;
} VisionData_t;
VisionData_t vision_data; // 实例化一个对象
// 开启接收中断 (放在 main 函数的初始化代码里)
// HAL_UART_Receive_IT(&huart1, &rx_byte, 1);
/* 串口中断回调函数 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) // 确保是连接MaixCam的那个串口
{
switch (rx_state)
{
case 0: // 状态0:寻找帧头 0xAA
if (rx_byte == 0xAA)
{
rx_state = 1; // 找到了,进入下一状态
rx_buffer[0] = rx_byte; // 存入校验用的数组(可选)
}
break;
case 1: // 状态1:寻找帧头 0x55
if (rx_byte == 0x55)
{
rx_state = 2; // 找到了,准备接收数据
rx_count = 0; // 重置数据计数器
rx_buffer[1] = rx_byte;
}
else
{
rx_state = 0; // 错了,回到起点重新找
}
break;
case 2: // 状态2:接收实际数据 (X高, X低, Y高, Y低, ID, 校验)
// 从 buffer[2] 开始存,因为前两个是包头
rx_buffer[2 + rx_count] = rx_byte;
rx_count++;
// 我们还需要接收 6 个字节 (X_H, X_L, Y_H, Y_L, ID, Sum)
if (rx_count >= 6)
{
// 接收完毕,开始处理数据
Process_Vision_Data();
rx_state = 0; // 回到起点等待下一包
}
break;
}
// 重新开启中断接收下一个字节
HAL_UART_Receive_IT(&huart1, &rx_byte, 1);
}
}
/* 数据解析与校验函数 */
void Process_Vision_Data(void)
{
// 1. 计算校验和
// 把接收到的前7个字节加起来 (包头AA+55 + 数据5个字节)
uint8_t cal_sum = 0;
for(int i = 0; i < 7; i++)
{
cal_sum += rx_buffer[i];
}
// 2. 比较 接收到的校验位 (Buffer第7个位置,即rx_buffer[7])
if (cal_sum == rx_buffer[7])
{
// --- 校验成功,提取数据 ---
// 拼接 X 坐标 (高8位左移8 + 低8位)
vision_data.x = (rx_buffer[2] << 8) | rx_buffer[3];
// 拼接 Y 坐标
vision_data.y = (rx_buffer[4] << 8) | rx_buffer[5];
// 提取 ID
vision_data.id = rx_buffer[6];
// --- 在这里写你的控制逻辑 ---
// 例如:
// if (vision_data.x > 160) { Turn_Right(); }
// else { Turn_Left(); }
}
else
{
// 校验失败,可能是干扰
}
}
💡 关键点提示
-
共地 (GND):再次强调,MaixCam 的 GND 必须和 STM32 的 GND 接在一起,否则收到的全是乱码。
-
波特率:代码里设的是 115200,请确保你的 STM32 CubeMX 配置里也是 115200。
-
调试技巧:
-
如果 STM32 没反应,可以在 STM32 的
Process_Vision_Data校验成功那里翻转一个 LED 灯。 -
如果 LED 狂闪,说明数据传输成功且校验通过了。
-
这套模板你可以直接拿去用,如果之后你需要传更多数据(比如宽、高、置信度),只需要把数组加大,修改一下发送和接收的字节数即可。
如果你上面一直跟的我的话可以用我用的STm32的代码
我们还是在uart.c的最下面写

cpp
/* USER CODE BEGIN 1 */
/*法一
*uint8_t rxData = 0;
uint8_t command[32];
uint8_t msg[]= "hello";
*/
//法二
/* 全局变量定义 */
uint8_t rxData; // 串口接收缓冲 (1个字节)
uint8_t rx_state = 0; // 状态机状态:0=找包头1, 1=找包头2, 2=接收数据
uint8_t rx_buffer[30]; // 暂存接收到的有效数据
uint8_t rx_count = 0; // 接收计数索引
#define Recv_DATA_LENGTH 8
/* 定义一个结构体方便使用数据 */
typedef struct {
uint16_t x;
uint16_t y;
uint8_t id;
} VisionData_t;
VisionData_t vision_data; // 实例化一个对象
void Process_Vision_Data(void);
void start_usart_recv(void)
{
HAL_UART_Receive_IT(&huart2,&rxData,1);
}
/* 串口中断回调函数 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART2) // 确保是连接MaixCam的那个串口
{
switch (rx_state)
{
case 0: // 状态0:寻找帧头 0xAA
if (rxData == 0xAA)
{
rx_state = 1; // 找到了,进入下一状态
rx_buffer[0] = rxData; // 存入校验用的数组(可选)
}
break;
case 1: // 状态1:寻找帧头 0x55
if (rxData == 0x55)
{
rx_state = 2; // 找到了,准备接收数据
rx_count = 0; // 重置数据计数器
rx_buffer[1] = rxData;
}
else
{
rx_state = 0; // 错了,回到起点重新找
}
break;
case 2: // 状态2:接收实际数据 (X高, X低, Y高, Y低, ID, 校验)
// 从 buffer[2] 开始存,因为前两个是包头
rx_buffer[2 + rx_count] = rxData;
rx_count++;
// 我们还需要接收 6 个字节 (X_H, X_L, Y_H, Y_L, ID, Sum)
if (rx_count >= 6)
{
// 接收完毕,开始处理数据
Process_Vision_Data();
rx_state = 0; // 回到起点等待下一包
}
break;
}
// 重新开启中断接收下一个字节
HAL_UART_Receive_IT(&huart2, &rxData, 1);
}
}
/* 数据解析与校验函数 */
void Process_Vision_Data(void)
{
// 1. 计算校验和
// 把接收到的前7个字节加起来 (包头AA+55 + 数据5个字节)
uint8_t cal_sum = 0;
for(int i = 0; i < 7; i++)
{
cal_sum += rx_buffer[i];
}
// 2. 比较 接收到的校验位 (Buffer第7个位置,即rx_buffer[7])
if (cal_sum == rx_buffer[7])
{
// --- 校验成功,提取数据 ---
// 拼接 X 坐标 (高8位左移8 + 低8位)
vision_data.x = (rx_buffer[2] << 8) | rx_buffer[3];
// 拼接 Y 坐标
vision_data.y = (rx_buffer[4] << 8) | rx_buffer[5];
// 提取 ID
vision_data.id = rx_buffer[6];
// --- 在这里写你的控制逻辑 ---
// 例如:
// if (vision_data.x > 160) { Turn_Right(); }
// else { Turn_Left(); }
//调试的,如果单片机能够成功接受到数据并解析那么让一个小灯亮起,便于我们观查
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_11, RESET);
GPIOD->BSRR |= 0x01 << 2; // 设置 GPIOD 第2位
GPIOD->BRR |= 0x01 << 2; // 重置 GPIOD 第2位
HAL_UART_Transmit(&huart1, rx_buffer,Recv_DATA_LENGTH , 1000);
}
}
/*
*法一
*void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_8, RESET);
GPIOD->BSRR |= 0x01 << 2; // 设置 GPIOD 第2位
GPIOD->BRR |= 0x01 << 2; // 重置 GPIOD 第2位
if (huart->Instance == USART2)
{
static int i = 0;
command[i++] = rxData;
if(command[0] != 0XAA)
i =0;
if((i == 2) && (command[1] != 0XBB))
i = 0;
if(i == 5)
{
if(command[4] == 0XCC) //开始校验
{
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13, RESET);
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_14, SET);
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_15, RESET);
GPIOD->BSRR |= 0x01 << 2; // 设置 GPIOD 第2位
GPIOD->BRR |= 0x01 << 2; // 重置 GPIOD 第2位
HAL_UART_Transmit(&huart1, (uint8_t *)command, 5, HAL_MAX_DELAY); //将发送出去的数据通过串口0发送,方便我们调试
HAL_UART_Transmit(&huart2, (uint8_t *)command, 5, HAL_MAX_DELAY);
}
i = 0;
}
start_usart_recv();
}
}*/
/* USER CODE END 1 */
主播这里亲测有效,没有问题


大家可以用给出来的代码试试跑一跑
然后这里看到maixVision打印的数据细心的小伙伴可能就会产生一个疑惑
显示的Sent: ['0xaa', '0x55', '0x1', '0x2c', '0x0', '0x64', '0x1', '0x91'] 这里的0x1是指0x01还是0x10?
结论是:这里的 0x1 代表的是 0x01 (即十进制的 1)。
为什么会显示成这样?
这是因为 Python 自带的 hex() 函数比较"偷懒",对于小于 16 的数字,它省略了前面的 0。
如果是 1,它显示
0x1。如果是 16,它才会显示
0x10。如果是 0,它显示
0x0(代表0x00)。
帮你拆解一下你发出来的这串数据
为了让你更放心,我把你的这串数据 ['0xaa', '0x55', '0x1', '0x2c', '0x0', '0x64', '0x1', '0x91'] 和我们代码里的逻辑一一对应起来:
0xaa: 帧头1
0x55: 帧头2
0x1: X坐标的高8位。
你的 X=300,转成十六进制是
0x012C。高8位就是
0x01。这里 Python 省略了0,所以显示0x1。(正确)
0x2c: X坐标的低8位。
- 即
0x012C的后半部分2C。(正确)
0x0: Y坐标的高8位。
你的 Y=100,转成十六进制是
0x0064。高8位是
0x00。Python 显示为0x0。(正确)
0x64: Y坐标的低8位。
- 即
0x0064的后半部分64。(正确)
0x1: Class ID。
- 你的 ID=1,所以是
0x01。(正确)
0x91: 校验和。
计算过程:
AA + 55 + 01 + 2C + 00 + 64 + 01(十六进制相加)结果是
0x191,取最后两位就是91。(正确)
放心使用
STM32 接收到的是二进制的字节 。 无论 Python 打印出来是 0x1 还是 0x01,在串口线上跑的电信号都是 0000 0001。STM32 能够完美识别它是 1,而不是 16。
AI答疑Cluade Code
一、你的理解(完全正确✅)
MaixCAM STM32 ↓ ↓ 跑AI模型 等待串口数据 ↓ ↓ 识别出结果 收到结果数据 ↓ ↓ 通过串口发送 ──────────► 解析数据 ↓ 控制电机/舵机/灯
你说的完全对!现在你的疑惑点是:
-
❓ MaixCAM的模型怎么跑?
-
❓ 怎么让它既跑模型,又发送数据?
二、MaixCAM跑模型的完整流程(超详细)
流程图:
┌─────────────────────────────────────────────────┐ │ 步骤1:选择/训练模型 │ │ ├─ 用MaixHub在线训练(推荐新手) │ │ └─ 用自己的数据集训练(进阶) │ └─────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────┐ │ 步骤2:下载模型文件到MaixCAM │ │ - 模型格式:.mud(MaixCAM专用格式) │ │ - 存储位置:TF卡或内部存储 │ └─────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────┐ │ 步骤3:Python代码加载模型 │ │ - 用 maix.nn 库加载模型 │ │ - 调用模型推理(识别) │ └─────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────┐ │ 步骤4:处理识别结果 │ │ - 提取坐标、类别、置信度 │ │ - 构建数据包 │ └─────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────┐ │ 步骤5:通过串口发送给STM32 │ │ - 用 uart.write() 发送 │ └─────────────────────────────────────────────────┘
跑个小Demo吧
python
#小Demo(测试MaixCAM能否识别颜色)--太慢了
from maix import camera, display, image
cam = camera.Camera(640, 480)
disp = display.Display()
# LAB 阈值 (L Min, L Max, A Min, A Max, B Min, B Max)
red_threshold = [(30, 100, 15, 127, 15, 127)]
while True:
img = cam.read()
# 查找色块
blobs = img.find_blobs(red_threshold, pixels_threshold=200)
for blob in blobs:
# 1. 画矩形框
# 注意:必须使用 image.Color() 来定义颜色
img.draw_rect(blob.x(), blob.y(), blob.w(), blob.h(), color=image.Color(255, 0, 0))
# 2. 画中心十字
# 同样,颜色也要用 image.Color()
img.draw_cross(blob.cx(), blob.cy(), color=image.Color(0, 255, 0))
# 打印坐标
print(f"找到红色: ({blob.cx()}, {blob.cy()})")
disp.show(img)
最后附上maixcam的引脚图(maixcam转接器外接出来的Tx和Rx就是UART0的tx和rx)

看到这里相信你已经对视觉模块和stm32通讯有了些了解了,那么接下来就是去了解视觉模块的一些玩法了,因为本质还是视觉模块通过视觉处理然后得到数据再将数据通过串口发送给单片机
这些文章写得也很好,小伙伴有时间可以看看
【电赛学习笔记】MaxiCAM 项目实践------与单片机的串口通信-EW帮帮网
OpenMV与STM32串口通信 (OpenMV、K210、视觉模块 与 STM32单片机、51单片机串口通信,Python与C语言串口通信)_openmv uart数组-CSDN博客
技术指导:
【视觉模块和STM32如何进行串口通信】https://www.bilibili.com/video/BV175vWe5EfV?vd_source=04ec85bac321be52c18d0e8083e4c6bc
感谢你的观看,如果我有什么问题还请评论区指正一下🤞