串口通信

前言

本人大四学生,即将毕业,开始与生活对线了。随着今年TI杯省赛的结束,我的电赛生涯也已经走到尽头,一路上踩过不少坑,但运气也不错,今年和去年都拿到了省一等奖。思来想去,我决定开始写博客,将电赛经验分享一二,能力有限,高手轻喷。

为何需要串口通信

近年来,电赛的题目越来复杂,仅单靠STM32或其他主控已经很难解决问题。比如2021年无人机题,小车题,以及今年的无人机题,必须要结合摄像头模块,才能准确完成任务。但加入了摄像头模块,首先要解决的问题就是如何让摄像头模块和主控之间进行通信,如果无法通信,就算在摄像头模块上识别做的再好也没有用。摄像头模块我只用过K210和OpenMV,另外STM32相关的开发做的比较多,这里就介绍一下我是怎么让K210和STM32通信、K210和OpenMV通信。K210和OpenMV是用python编程,STM32是用C编程,其实学清楚之后,要改到其他平台上也就是要懂得其他平台的串口怎么配置,然后串口发送函数和接收函数是哪两个,调用即可。其他的像是发送数组打包封装,接收数组保存解析等,编程语言对的上,自然也是通用的。

K210如何进行串口通信

K210串口配置

一口吃不成胖子,串口通信自然要一步一步来,第一步要解决的是串口配置,但在串口配置前,需要导入串口相关模块,以提供硬件支持。UART、fm、GPIO、math这四个是必不可少的。当然,若要进行图像处理,sensor、image、time也是必不可少的。K210还有很多其他模块,按个人需求添加即可。

#__________________________________________________________________
# 导入模块
import sensor, image, time, math                            # 导入感光元件模块 sensor 机器视觉模块 image 跟踪运行时间模块 time 数学函数模块 math
import machine                                              # 导入模块 machine
from machine import UART                                    # 从 machine 模块中导入 双向串行通信模块 UART
from fpioa_manager import fm                                # 从 fpioa_manager 模块中导入 引脚注册模块 fm
from Maix import GPIO                                       # 从 Maix 模块中导入 引脚模块 GPIO

这里不妨提一下我对感光元件sensor的设置。去年和今年的电赛我做的都是无人机题,在队里主要负责摄像头模块的识别。去年的题目是植保无人机,今年的题目是送货无人机,我采用的方法是用摄像头模块进行颜色追踪,传回坐标给飞控,飞控飞到指定地点执行任务。植保无人机就是通过摄像头获得绿白交界线的坐标,传回飞控,飞控保持这个坐标值不变,前进,摄像头模块会识别中心点颜色是否是绿色,若为绿色,则进行模拟打药操作。而今年的送货无人机,是通过识别颜色,经过一定处理,只传回目标点的坐标,让飞控控制无人机在目标点正上方,然后执行送货作业。
由于都要用到颜色追踪,所以需要固定感光元件的参数,这样才能准确的追踪颜色。如果这些参数没有固定,在不同环境下重新上电摄像头,画面呈现的色彩也不一样。
举个例子,小伙伴们可以试一试,在K210或OpenMV上电前,将摄像头对准红色纸片,然后再进行上电,等IDE中出现画面了,会发现画面会偏绿(好像是偏绿,总之红色的表现效果会变差,因为对着红色曝光,会导致画面的红色增益会被设置比较低,这样会直接影响到追踪颜色的效果),这也是我比赛以来遇到的一个大坑,有的时候上电的环境不同,影响到了增益,导致追踪颜色的时候,效果不理想(我是用摄像头模块上的LED来判断有没有成功识别,比如识别红色,如果成功识别,则红灯常亮,识别失败,则红灯熄灭,有的时候就会出现灯一直闪的情况,这就说明阈值出现问题,但我没改过阈值),最后发现,其实是上电时候镜头对着的物体会影响感光元件的增益,因此在画面中呈现的颜色会发生变化,之前的阈值在这种情况下就概率识别不了,因此如果你要用到颜色追踪,一定要记得固定这些参数,然后再取阈值,这样才能保证这个阈值一直都可以用。

#__________________________________________________________________
# 感光元件设置
sensor.reset()                                              # 重置和初始化相机传感器 默认设置为 摄像头频率24M 不开启双缓冲模式

sensor.set_pixformat(sensor.RGB565)                         # 设置图像格式为 RGB565 (彩色)
sensor.set_framesize(sensor.QVGA)                           # 设置图像大小为 QVGA (320 x 240) 像素个数 76800

sensor.set_auto_exposure(1)                                 # 设置自动曝光
sensor.set_auto_gain(0, gain_db = 17)                       # 设置增益 17
sensor.set_auto_whitebal(0, rgb_gain_db = (0,0,0))          # 设置RGB增益 0 0 0

sensor.set_contrast(0)                                      # 设置对比度 0
sensor.set_brightness(0)                                    # 设置亮度 0
sensor.set_saturation(0)                                    # 设置饱和度 0

sensor.set_vflip(1)                                         # 打开垂直翻转
sensor.set_hmirror(1)                                       # 打开水平镜像

sensor.skip_frames(time = 2000)                             # 延时跳过2s 等待感光元件稳定

#__________________________________________________________________
# 创建时钟对象
clock = time.clock()                                        # 创建一个时钟对象 clock

然后就是对K210的串口进行设置,这里我设置了两个串口。K210的串口引脚是可以自行映射的,我用的K210是01Studio的,这一款K210的P9和P6引脚是相邻的,P7和P8引脚是相邻的,因此我就把P9映射为串口1的RX,P6映射为串口1的TX,P7映射为串口2的RX,P8映射为串口2的TX。波特率是设置为921600,这个是可以更改的,你要使用115200也是可以的,只要和其他硬件的串口波特率对的上都可以。至于其他设置,数据位8,校验位0,停止位1,一般都是这样设置,这个不建议更改。
需要注意的是,不建议使用K210引脚号数偏大的引脚,比如26以后的,因为这些引脚似乎在接入SD卡、LCD、麦克风阵列等外设的时候会被占用。

#__________________________________________________________________
# 串口设置
# 串口1 设置 P9 RX P6 TX
fm.register(9, fm.fpioa.UART1_RX, force = True)      # 配置 9 脚为 UART1_RX 强制注册
fm.register(6, fm.fpioa.UART1_TX, force = True)      # 配置 6 脚为 UART1_TX 强制注册

uart1 = UART(UART.UART1, 921600, 8, 0, 1)            # 设置 uart1 为 串口1 波特率 921600 数据位 8位 校验位 0位 停止位 1位

# 串口2 设置 P7 RX P8 TX
fm.register(7, fm.fpioa.UART2_RX, force = True)      # 配置 7 脚为 UART2_RX 强制注册
fm.register(8, fm.fpioa.UART2_TX, force = True)      # 配置 8 脚为 UART2_TX 强制注册

uart2 = UART(UART.UART2, 921600, 8, 0, 1)            # 设置 uart2 为 串口2 波特率 921600 数据位 8位 校验位 0位 停止位 1位

K210引脚

K210串口发送相关定义

这里我定义了两个,一个是发往STM32,另一个是发往OpenMV,按需使用。当然,其实两段代码就是名字有区别,其他的基本一样。如果你还需要跟第三个模块进行串口通信,复制一遍代码然后改个名字即可。
串口发送数据的核心思想就是将要发送的数据打包成一个数组,然后将这个数组发送出去。如果不打包,接收端很难将数据正常解析出来,帧头的目的就是方便接收端找到数组的“头”,然后从这个“头”开始,逐个接收数据,有效数据长度位控制的是接收多少个有效数据(就是你要发送的数据),加有效数据长度位是方便后续添加新的需要发送的数据,另外接收端可以灵活接收不同长度的数据,最后接收端对接收数据进行校验,如果校验通过,则保存这些数据,这里用的校验方法是校验和。
帧头我都是用0xAA,因为0xAA转换成8位是 1010 1010 电平是有持续跳变的,可以提高点稳定性。
然后我只定义了5个发送的参数,当然,这也是可以加的,比如需要添加一个标志位2,那只需要在类STM32_transmit的最后一行添加上flag2 = 0,然后在打包函数中的0x00前添加上TSTM32.flag2, 即可,如代码中注释的部分。
需要注意的是这个最好不要加太多,我在接收端代码的定义中是定义有效数据长度不超过40个8位,代码中的参数有x,y,color,shape,flag共五个,但x和y是需要使用两个8位来发送的,因此实际有效数据长度是7个8位。关于为什么要用两个8位来发,原因就是我感光元件设置的图像大小是QVGA,x轴坐标变化范围为0到319,y轴坐标变化范围为0到239,x超过了255因此需要用两个8位来发送,一个8位能保存的数据是从0到255,两个8位是可以保存0到65535的。至于像颜色标志位、形状标志位、目标标志位这些一个8位就足够了,比如用1、2、3分别表示红、绿、蓝,也就是在识别到红色的时候,加一句TSTM32.color = 1,以此类推。当然,这些参数都只是一个名字而已,想来保存什么标志位或者什么数据发送到主控,都可以根据个人需求定义,理解了就不难。
还有一点就是,如果接收端经常出现乱序,不妨可以试一下更改帧头。当然,发送端发送的帧头和接收端识别接收的帧头要一并修改,这一点需要牢记。

#__________________________________________________________________
# 串口发送 STM32
# 定义 STM32 发送类
class STM32_transmit():                              # 定义 STM32 发送类
    head1  = 0xAA                                    # uint8_t   帧头1
    head2  = 0xAA                                    # uint8_t   帧头2
    x      = 0                                       # uint16_t  目标x轴坐标
    y      = 0                                       # uint16_t  目标y轴坐标
    color  = 0                                       # uint8_t   目标颜色标志位
    shape  = 0                                       # uint8_t   目标形状标志位
    flag   = 0                                       # uint8_t   目标标志位
    #flag2 = 0

# 实例化类
TSTM32 = STM32_transmit()                            # 实例化 STM32_transmit() 为 TSTM32

# 定义打包函数
def TSTM32_data():                                   # 数据打包函数
    data=bytearray([TSTM32.head1,                    # 帧头1
                    TSTM32.head2,                    # 帧头2
                    0x00,                            # 有效数据长度 0x00 + data_len - 4
                    TSTM32.x>>8,TSTM32.x,            # 保存目标坐标x 将整形数据拆分成两个8位
                    TSTM32.y>>8,TSTM32.y,            # 保存目标坐标y 将整形数据拆分成两个8位
                    TSTM32.color,                    # 保存目标颜色标志位
                    TSTM32.shape,                    # 保存目标形状标志位
                    TSTM32.flag,                     # 保存目标标志位
                    #TSTM32.flag2,
                    0x00])                           # 数据和校验位

    # 数据包的长度
    data_len = len(data)                             # 获得数据包总长度
    data[2]  = data_len - 4                          # 有效数据的长度 扣去 帧头1 帧头2 有效数据长度位 校验位

    # 校验和
    sum = 0                                          # 和置零
    for i in range(0,data_len-1):
        sum = sum + data[i]                          # 和累加
    data[data_len-1] = sum                           # 和赋值 给数组最后一位发送 只保存低8位 溢出部分无效

    # 返回打包好的数据
    return data

串口发送到OpenMV实际上就是复制上面的发送到STM32部分的代码,然后改个名字,发送类中的变量名可修改,可增减,记得在打包函数中修改增减即可。

#__________________________________________________________________
# 串口发送 OpenMV
# 定义 OpenMV 发送类
class OpenMV_transmit():                             # 定义 OpenMV 发送类
    head1  = 0xAA                                    # uint8_t   帧头1
    head2  = 0xAA                                    # uint8_t   帧头2
    x      = 0                                       # uint16_t  目标x轴坐标
    y      = 0                                       # uint16_t  目标y轴坐标
    color  = 0                                       # uint8_t   目标颜色标志位
    shape  = 0                                       # uint8_t   目标形状标志位
    flag   = 0                                       # uint8_t   目标标志位

# 实例化类
TOpenMV = OpenMV_transmit()                          # 实例化 OpenMV_transmit() 为 TOpenMV

# 定义打包函数
def TOpenMV_data():                                  # 数据打包函数
    data=bytearray([TOpenMV.head1,                   # 帧头1
                    TOpenMV.head2,                   # 帧头2
                    0x00,                            # 有效数据长度 0x00 + data_len - 4
                    TOpenMV.x>>8,TOpenMV.x,          # 保存目标坐标x 将整形数据拆分成两个8位
                    TOpenMV.y>>8,TOpenMV.y,          # 保存目标坐标y 将整形数据拆分成两个8位
                    TOpenMV.color,                   # 保存目标颜色标志位
                    TOpenMV.shape,                   # 保存目标形状标志位
                    TOpenMV.flag,                    # 保存目标标志位
                    0x00])                           # 数据和校验位

    # 数据包的长度
    data_len = len(data)                             # 获得数据包总长度
    data[2]  = data_len - 4                          # 有效数据的长度 扣去 帧头1 帧头2 有效数据长度位 校验位

    # 校验和
    sum = 0                                          # 和置零
    for i in range(0,data_len-1):
        sum = sum + data[i]                          # 和累加
    data[data_len-1] = sum                           # 和赋值 给数组最后一位发送 只保存低8位 溢出部分无效

    # 返回打包好的数据
    return data

最后在while中将打包的数据发送出去就好啦!如果觉得发送的太快,也可以加一个定时器,间隔一定时间发送一次,定时器后续文章再写咯!

    uart1.write(TSTM32_data())                       # 串口1 数据发送
    uart2.write(TOpenMV_data())                      # 串口2 数据发送
    

K210串口发送测试

配置完串口,以及串口发送相关函数,不妨可以先来测试一下串口能不能成功发送,需要用到CH340模块,以及串口软件XCOM。
肯定有人会问,不是让K210和STM32、K210和OpenMV通信吗?怎么是用XCOM?其实这里我就是要测试一下,K210配置的串口能不能成功将数据发送出去,能成功发送,才能证明K210的配置没问题。如果不做这一步,直接将K210和STM32连起来,要是无法通信成功,那到底是K210配置出问题呢?还是STM32配置出问题?无论做什么事情都要一步一步来,这样在思考为什么不能实现功能的时间才会少,学会控制变量法,才是制胜的根本。
但我的CH340模块忘记带回来了,只能用手头上的STM32开发板上带的CH340进行测试了,串口通信最少三根线即可,两个模块之间需要共地,RX接TX,TX接RX,这里我先测试K210的串口1,因此图中的黄线接K210的P6,橙线接K210的P9。
在这里插入图片描述
在这里插入图片描述
然后打开MaixPy IDE,连接K210,在While前添加以下代码,给发送的变量赋值,然后在XCOM上观察发送情况。

TSTM32.x     = 1
TSTM32.y     = 2
TSTM32.color = 3
TSTM32.shape = 4
TSTM32.flag  = 5

TOpenMV.x     = 257
TOpenMV.y     = 258
TOpenMV.color = 3
TOpenMV.shape = 4
TOpenMV.flag  = 5

在XCOM,选择串口(标识有CH340),波特率921600,停止位1,数据位8,校验位0,十六进制显示,然后打开串口。
在这里插入图片描述
在MaixPy IDE中连接上K210,然后运行。
在这里插入图片描述
可以在XCOM看到串口能成功发送过来数据,数据包为
AA AA 07 00 01 00 02 03 04 05 6A
两个AA是帧头,07表示有效数据有7位,即
00 01 00 02 03 04 05
6A是校验位
将有效数据还原成参数x、y、color、shape、flag就是
x = 0 * 256 + 1 = 1
y = 0 * 256 + 2 = 2
color = 3
shape = 4
flag = 5
与之前的赋值结果一致,说明串口1能成功发送数据。
在这里插入图片描述
同理,我们将橙线接P7,黄线接P8,即可测试串口2能否成功发送数据。从图中可得,数据包为
AA AA 07 01 01 01 02 03 04 05 6C
两个AA是帧头,07表示有效数据有7位,即
01 01 01 02 03 04 05
6C是校验位
将有效数据还原成参数x、y、color、shape、flag就是
x = 1 * 256 + 1 = 257
y = 1 * 256 + 2 = 258
color = 3
shape = 4
flag = 5
与之前的赋值结果一致,说明串口2能成功发送数据,那么K210的串口发送就算配置成功了。
在这里插入图片描述

K210串口接收相关定义

串口接收需要定义一个类来保存接收到的数据,我这里定义的参数比较少,可根据个人需要进行修改。类中前五个参数我是用来保存OpenMV发送到K210的数据的,第六个参数mode是用来保存STM32发送到K210的数据的,我是想用mode这个参数来控制K210的模式,从而实现不同模式之间的切换。然后OpenMV发送过来的五个数据,这里只是作为测试使用,并没有其他意义,这个根据个人需要进行修改。

#__________________________________________________________________
# 串口接收 K210
# 定义 K210 接收类
class K210_receive(object):                          # 定义 K210 接收类
    x      = 0                                       # uint16_t  目标x轴坐标
    y      = 0                                       # uint16_t  目标y轴坐标
    color  = 0                                       # uint8_t   目标颜色标志位
    shape  = 0                                       # uint8_t   目标形状标志位
    flag   = 0                                       # uint8_t   目标标志位
    mode   = 0                                       # uint8_t   工作模式位

# 实例化类
K210 = K210_receive()                                # 实例化 K210_receive() 为 K210

串口接收需要用数组来缓冲,因此还需要定义一个类来缓冲串口数据。

#__________________________________________________________________
# 定义串口接收数据保存类
class uart_buf_save(object):                         # 定义 uart 接收数据保存类
    uart_buf  = []                                   # 串口缓冲区数组
    data_len  = 0                                    # 有效数据长度
    data_cnt  = 0                                    # 总数据长度
    state     = 0                                    # 接收状态
    

然后就是完成STM32发送到K210的数据接收与解析,这段代码比较长,先上完整代码,再逐个进行解读。

#__________________________________________________________________
# 接收 STM32 数据
# 实例化类
RSTM32  = uart_buf_save()                            # 实例化 uart_buf_prase() 为 RSTM32

# 定义接收帧头
STM32_HEADER  = [0xAA,0xAA]                          # 接收帧头 STM32

# 串口数据解析 STM32
def Receive_STM32(data_buf,num):
    # 和累加
    sum = 0
    i = 0
    while i<(num-1):
        sum = sum + data_buf[i]
        i = i + 1
    # 求余 因为 校验和 为 8 位 超出部分无效 因此只校验 低8位 即可
    sum = sum%256

    # 和校验失败则退出
    if sum != data_buf[num-1]:
        return

    # 和校验成功则接收数据
    # 接收 STM32 数据
    K210.mode = 0x00 + data_buf[3]

# 串口数据接收 STM32
def uart_receive_stm32(buf):
    if RSTM32.state==0 and buf==STM32_HEADER[0]:     # 判断帧头1是否符合要求 符合则进入下一个状态
        RSTM32.state=1
        RSTM32.uart_buf.append(buf)                  # 将这个数据添加到数组末尾

    elif RSTM32.state==1 and buf==STM32_HEADER[1]:   # 判断帧头2是否符合要求 符合则进入下一个状态
        RSTM32.state=2
        RSTM32.uart_buf.append(buf)                  # 将这个数据添加到数组末尾

    elif RSTM32.state==2 and buf<40:                 # 有效数据长度位 规定有效数据长度小于40 符合则进入下一个状态
        RSTM32.state=3
        RSTM32.data_len=buf                          # 获得有效数据长度
        RSTM32.data_cnt=buf+4                        # 获得总数据长度 总数据长度 = 帧头1 + 帧头2 + 有效数据长度位 + 有效数据 + 校验位
        RSTM32.uart_buf.append(buf)                  # 将这个数据添加到数组末尾

    elif RSTM32.state==3 and RSTM32.data_len>0:      # 存储有效数据长度个数据
        RSTM32.data_len=RSTM32.data_len-1            # 每存储一次 还需要存储的数据个数减1
        RSTM32.uart_buf.append(buf)                  # 将这个数据添加到数组末尾
        if RSTM32.data_len==0:                       # 直到存储完毕
            RSTM32.state=4                           # 进入下一个状态

    elif RSTM32.state==4:                            # 当接收到存储完毕的信息
        RSTM32.uart_buf.append(buf)                  # 保存最后一位校验位 将这个数据添加到数组末尾
        RSTM32.state=0                               # 状态重置为0 调用串口数据解析函数进行数据解析
        Receive_STM32(RSTM32.uart_buf,RSTM32.uart_buf[2]+4)
        #print(RSTM32.uart_buf)
        RSTM32.uart_buf=[]                           # 清空缓冲区 准备下次接收数据

    else:                                            # 不满足以上条件 视为接收出错 重置状态为0 丢弃所有数据 准备下一次接收数据
        RSTM32.state=0                               # 重置状态为0
        RSTM32.uart_buf=[]                           # 清空缓冲区 准备下一次接收数据

# 串口数据读取 最后调用这个函数即可
def uart1_stm32_read():
    buf_len = uart1.any()                            # 检查 串口1 是否有内容需要读取 返回等待的字节数量(可能为0)
    for i in range(0,buf_len):                       # 读取 buf_len 个数据
        uart_receive_stm32(uart1.readchar())         # 接收单个数据 uart1.readchar() 然后将这个数据传递到函数 uart_receive_stm32() 进行 STM32 数据接收

最后在while中调用即可接收来自STM32的数据。

	uart1_stm32_read()                               # 串口1 数据接收

接下来逐步分析,在while中,执行函数uart1_stm32_read时,检查串口1是否有数据,如果有数据,则进行读取,并将数据传递给串口数据接收函数uart_receive_stm32

# 串口数据读取 最后调用这个函数即可
def uart1_stm32_read():
    buf_len = uart1.any()                            # 检查 串口1 是否有内容需要读取 返回等待的字节数量(可能为0)
    for i in range(0,buf_len):                       # 读取 buf_len 个数据
        uart_receive_stm32(uart1.readchar())         # 接收单个数据 uart1.readchar() 然后将这个数据传递到函数 uart_receive_stm32() 进行 STM32 数据接收

当有数据传入接收函数uart_receive_stm32时,接收函数会判断当前接收状态state
若为0且传入的数据等于帧头1,则保存该数据,然后进入状态1。
进入状态1后,若传入的数据等于帧头2,则保存该数据,然后进入状态2。
进入状态2后,判断需要接收的有效数据长度是否小于40,若小于,则保存该数据以及总数据长度,然后进入状态3。
进入状态3后,开始接收有效数据长度个数据,接收完毕会进入状态4。
进入状态4后,保存最后一位校验位,然后将数组以及数据总长传入解析函数Receive_STM32进行解析。
解析完会将状态置0,数组清空,准备下一次接收。

# 串口数据接收 STM32
def uart_receive_stm32(buf):
    if RSTM32.state==0 and buf==STM32_HEADER[0]:     # 判断帧头1是否符合要求 符合则进入下一个状态
        RSTM32.state=1
        RSTM32.uart_buf.append(buf)                  # 将这个数据添加到数组末尾

    elif RSTM32.state==1 and buf==STM32_HEADER[1]:   # 判断帧头2是否符合要求 符合则进入下一个状态
        RSTM32.state=2
        RSTM32.uart_buf.append(buf)                  # 将这个数据添加到数组末尾

    elif RSTM32.state==2 and buf<40:                 # 有效数据长度位 规定有效数据长度小于40 符合则进入下一个状态
        RSTM32.state=3
        RSTM32.data_len=buf                          # 获得有效数据长度
        RSTM32.data_cnt=buf+4                        # 获得总数据长度 总数据长度 = 帧头1 + 帧头2 + 有效数据长度位 + 有效数据 + 校验位
        RSTM32.uart_buf.append(buf)                  # 将这个数据添加到数组末尾

    elif RSTM32.state==3 and RSTM32.data_len>0:      # 存储有效数据长度个数据
        RSTM32.data_len=RSTM32.data_len-1            # 每存储一次 还需要存储的数据个数减1
        RSTM32.uart_buf.append(buf)                  # 将这个数据添加到数组末尾
        if RSTM32.data_len==0:                       # 直到存储完毕
            RSTM32.state=4                           # 进入下一个状态

    elif RSTM32.state==4:                            # 当接收到存储完毕的信息
        RSTM32.uart_buf.append(buf)                  # 保存最后一位校验位 将这个数据添加到数组末尾
        RSTM32.state=0                               # 状态重置为0 调用串口数据解析函数进行数据解析
        Receive_STM32(RSTM32.uart_buf,RSTM32.uart_buf[2]+4)
        #print(RSTM32.uart_buf)
        RSTM32.uart_buf=[]                           # 清空缓冲区 准备下次接收数据

    else:                                            # 不满足以上条件 视为接收出错 重置状态为0 丢弃所有数据 准备下一次接收数据
        RSTM32.state=0                               # 重置状态为0
        RSTM32.uart_buf=[]                           # 清空缓冲区 准备下一次接收数据

解析函数其实就是判断接收到的数据累加和是否等于接收到的和,等于则校验成功,保存数据,不等于则退出,不保存数据。
这里校验成功我只将mode保存到K210.mode
假设STM32端发送过来的数据包是
AA AA 01 01 56
数组的下标从0开始,AA是帧头,第一个01是有效数据长度位,第二个01是要被保存的模式,56是校验和,因此数组下标范围0到4。
第二个01在数组中的下标是3,因此数组下标3的数据被保存到K210.mode

# 串口数据解析 STM32
def Receive_STM32(data_buf,num):
    # 和累加
    sum = 0
    i = 0
    while i<(num-1):
        sum = sum + data_buf[i]
        i = i + 1
    # 求余 因为 校验和 为 8 位 超出部分无效 因此只校验 低8位 即可
    sum = sum%256

    # 和校验失败则退出
    if sum != data_buf[num-1]:
        return

    # 和校验成功则接收数据
    # 接收 STM32 数据
    K210.mode = 0x00 + data_buf[3]
    

然后就是OpenMV发送到K210的数据解析了,这里的代码其实是和STM32发送到K210的是基本一致的,只是改了名字而已,因此就不逐个介绍了,代码如下所示。

#__________________________________________________________________
# 接收 OpenMV 数据
# 实例化类
ROpenMV = uart_buf_save()                            # 实例化 uart_buf_prase() 为 ROpenMV

# 定义接收帧头
OpenMV_HEADER = [0xAA,0xAA]                          # 接收帧头 OpenMV

# 串口数据解析 OpenMV
def Receive_OpenMV(data_buf,num):
    # 和累加
    sum = 0
    i = 0
    while i<(num-1):
        sum = sum + data_buf[i]
        i = i + 1
    # 求余 因为 校验和 为 8 位 超出部分无效 因此只校验 低8位 即可
    sum = sum%256

    # 和校验失败则退出
    if sum != data_buf[num-1]:
        return

    # 和校验成功则接收数据
    # 接收 OpenMV 数据
    K210.x      = data_buf[3]*256 + data_buf[4]
    K210.y      = data_buf[5]*256 + data_buf[6]
    K210.color  = data_buf[7]
    K210.shape  = data_buf[8]
    K210.flag   = data_buf[9]

# 串口数据接收 OpenMV
def uart_receive_openmv(buf):
    if ROpenMV.state==0 and buf==OpenMV_HEADER[0]:   # 判断帧头1是否符合要求 符合则进入下一个状态
        ROpenMV.state=1
        ROpenMV.uart_buf.append(buf)                 # 将这个数据添加到数组末尾

    elif ROpenMV.state==1 and buf==OpenMV_HEADER[1]: # 判断帧头2是否符合要求 符合则进入下一个状态
        ROpenMV.state=2
        ROpenMV.uart_buf.append(buf)                 # 将这个数据添加到数组末尾

    elif ROpenMV.state==2 and buf<40:                # 有效数据长度位 规定有效数据长度小于40 符合则进入下一个状态
        ROpenMV.state=3
        ROpenMV.data_len=buf                         # 获得有效数据长度
        ROpenMV.data_cnt=buf+4                       # 获得总数据长度 总数据长度 = 帧头1 + 帧头2 + 有效数据长度位 + 有效数据 + 校验位
        ROpenMV.uart_buf.append(buf)                 # 将这个数据添加到数组末尾

    elif ROpenMV.state==3 and ROpenMV.data_len>0:    # 存储有效数据长度个数据
        ROpenMV.data_len=ROpenMV.data_len-1          # 每存储一次 还需要存储的数据个数减1
        ROpenMV.uart_buf.append(buf)                 # 将这个数据添加到数组末尾
        if ROpenMV.data_len==0:                      # 直到存储完毕
            ROpenMV.state=4                          # 进入下一个状态

    elif ROpenMV.state==4:                           # 当接收到存储完毕的信息
        ROpenMV.uart_buf.append(buf)                 # 保存最后一位校验位 将这个数据添加到数组末尾
        ROpenMV.state=0                              # 状态重置为0 调用串口数据解析函数进行数据解析
        Receive_OpenMV(ROpenMV.uart_buf,ROpenMV.uart_buf[2]+4)
        #print(ROpenMV.uart_buf)
        ROpenMV.uart_buf=[]                          # 清空缓冲区 准备下次接收数据

    else:                                            # 不满足以上条件 视为接收出错 重置状态为0 丢弃所有数据 准备下一次接收数据
        ROpenMV.state=0                              # 重置状态为0
        ROpenMV.uart_buf=[]                          # 清空缓冲区 准备下一次接收数据

# 串口数据读取 最后调用这个函数即可
def uart2_openmv_read():
    buf_len = uart2.any()                            # 检查 串口2 是否有内容需要读取 返回等待的字节数量(可能为0)
    for i in range(0,buf_len):                       # 读取 buf_len 个数据
        uart_receive_openmv(uart2.readchar())        # 接收单个数据 uart2.readchar() 然后将这个数据传递到函数 uart_receive_openmv() 进行 openmv 数据接收

最后在while中调用即可接收来自OpenMV的数据。

	uart2_openmv_read()                              # 串口2 数据接收

但还要提一点,接收STM32数据和接收OpenMV的数据两段代码的主要不同就是在解析函数中,K210将OpenMV发送过来的数据保存给x,y,color,shape,flag,因为x和y是16位,因此高8位需要乘以256,举个例子。
比如需要发送数据为 257 258 3 4 5 而一个8位只能保存0到255,那么就需要将257分成两个8位发送,第一个8位是高八位,需要乘以256还原,第二个8位是低8位,因此257 = 256 + 1 也就是 01 01,所以258就是 01 02
因此整个数组为
AA AA 07 01 01 01 02 03 04 05 6C
K210.x 所对应的高8位在数组中下标是3,低8位在数组中下标是4,数组下标3的数据是01,下标4的数据是01,则有
K210.x = 1 * 256 + 1 = 257
以此类推

# 串口数据解析 OpenMV
def Receive_OpenMV(data_buf,num):
    # 和累加
    sum = 0
    i = 0
    while i<(num-1):
        sum = sum + data_buf[i]
        i = i + 1
    # 求余 因为 校验和 为 8 位 超出部分无效 因此只校验 低8位 即可
    sum = sum%256

    # 和校验失败则退出
    if sum != data_buf[num-1]:
        return

    # 和校验成功则接收数据
    # 接收 OpenMV 数据
    K210.x      = data_buf[3]*256 + data_buf[4]
    K210.y      = data_buf[5]*256 + data_buf[6]
    K210.color  = data_buf[7]
    K210.shape  = data_buf[8]
    K210.flag   = data_buf[9]
    

K210串口接收测试

终于到最后一步了,这一步完成,那么就可以成功实现K210串口收发了,还是先用CH340模块和串口软件XCOM模拟,若可行,再与STM32或OpenMV进行通信测试。先来模拟测试一下是否可以通过串口成功更改K210的模式。需要在主函数中加入这段代码,实时打印K210当前的接收数据,观察串口接收情况。

	print("Mode: "+str(K210.mode)+" OpenMV: "+str(K210.x)+" "+str(K210.y)+" "+str(K210.color)+" "+str(K210.shape)+" "+str(K210.flag))

重新上电K210,可以看到当前的数据如下。
在这里插入图片描述
橙线接P9,黄线接P6,也就是将CH340模块接到K210的串口1上。
模拟STM32发送数据,更改mode
这里发送的数组是
AA AA 01 01 56
两个AA是帧头,01表示有效数据有1位,即
01
56是校验位
将有效数据还原成mode就是
mode = 1
在这里插入图片描述
可以看到模式已经被成功更改了。
在这里插入图片描述
如果要改回模式0,需要将第二个01改成00,56改成55(不改这个无法通过和校验,无法更改模式)。
在这里插入图片描述
可以看到模式成功更改回去了。
在这里插入图片描述

橙线接P7,黄线接P8,也就是将CH340模块接到K210的串口2上。
模拟OpenMV发送数据,更改x、y、color、shape、flag
这里发送的数组是
AA AA 07 01 01 01 02 03 04 05 6C
两个AA是帧头,07表示有效数据有7位,即
01 01 01 02 03 04 05
6C是校验位
将有效数据还原成x、y、color、shape、flag就是
x = 1 * 256 + 1 = 257
y = 1 * 256 + 2 = 258
color = 3
shape = 4
flag = 5
在这里插入图片描述
在这里插入图片描述
可以看到模拟OpenMV发送的数据已经被K210成功保存了,如此一来,K210端的串口发送与接收就算完成了,K210的完整代码贴在文章末尾,需要自取,接下来就是完成STM32的串口发送与接收函数的定义。
在这里插入图片描述

STM32如何进行串口通信

STM32串口配置

这里我使用STM32CubeMX来配置串口,我的STM32的型号为STM32F103ZET6,打开STM32CubeMX,点击ACCESS TO MCU SELECTOR。
在这里插入图片描述
搜索并选择STM32F103ZET6
在这里插入图片描述
选择时钟源为外部晶振。
在这里插入图片描述
在时钟树中,配置时钟为72M。
在这里插入图片描述在SYS中,设置Debug方式为SW,方便后面用ST-Link调试,如果你没有ST-Link,后面可以多配置一个串口来发送接收到的数据,通过CH340模块在电脑上显示接收信息。
在这里插入图片描述
串口2配置为异步,波特率921600,其余配置默认(默认数据位8、停止位1、无校验位)。

在这里插入图片描述
在NVIC中使能串口2中断。
在这里插入图片描述
然后配置工程名字和保存路径,我用的开发工具是Keil 5,因此IDE这一栏需要选择MDK-ARM,版本选择V5。
在这里插入图片描述
然后点击进入Code Generator进行设置,一般我只复制必要的库,然后会另外生成c和h文件。
在这里插入图片描述
OK,点击GENERATE CODE即可开始生成代码!
在这里插入图片描述
在生成的工程目录下,创建一个文件夹,名字自定义,这里取的是MyLibrary。
在这里插入图片描述
在MDK-ARM文件夹下点击打开工程。
在这里插入图片描述
在Manage Project Items中的Groups一栏里,点击第一个按钮,新建名称为MyLibrary的文件夹,然后OK保存。
在这里插入图片描述
在魔术棒的C/C++一栏的Floder Setup中添加环境,定位到之前创建的文件夹MyLibrary。
在这里插入图片描述
然后点击左上角按钮新建一个text,Ctrl + s 保存到之前创建的目录 MyLibrary 下,命名为 uartdata.c
在这里插入图片描述
然后再创建好uartdata.h 和 system.h 都是保存到 Mylibrary 下。
在这里插入图片描述
这里先贴上这三个文件要保存的代码。
uartdata.c

/********************************************************************/
//	作用:				STM32串口数据发送与接收
//	作者:				FITQY
//	时间:				2022.08.18
/********************************************************************/
#include "uartdata.h"
#include "system.h"

/********************************************************************

串口发送数据结构体初始化函数 Data_Init

	 各参数作用
	 DataTransmit *data: 							    选择要初始化的结构体 如 &data_uart2
	 head1:			        						帧头1 如 0xAA
	 head2:			        						帧头2 如 0xAA
	 length:			        						有效数据长度 
	 
********************************************************************/
void Data_Init_Transmit(DataTransmit *data, uint8_t head1, uint8_t head2, uint8_t length)
{
	
	data -> head1  = head1;
	data -> head2  = head2;
	data -> length = length;
	
	for(uint8_t i = 0; i < length; i++)
	{
		data -> data[i] = 0;
	}
	
	for(uint8_t j = 0; j < length + 4; j++)
	{
		data -> transmit_data[j] = 0;
	}
	
}

/********************************************************************

串口发送数据打包函数 Data_Pack

	 各参数作用
	 DataTransmit *data: 			带入结构体 如 &data_uart2
	 
********************************************************************/
void Data_Pack(DataTransmit *data)
{
	
	data -> transmit_data[0] = data -> head1;
	data -> transmit_data[1] = data -> head2;
	data -> transmit_data[2] = data -> length;
	
	for(uint8_t i = 0; i < data -> length; i++)
	{
		data -> transmit_data[3+i] = data -> data[i];
	}
	
	uint8_t sum = 0;
	
  for(uint8_t j = 0; j < data -> length + 3; j++)
	{
		sum += data -> transmit_data[j];
	}
	
  data -> transmit_data[data -> length + 3] = sum;
	
}

/********************************************************************

串口数据发送函数 Data_Transmit

	 各参数作用
	 UART_HandleTypeDef *huart:				选择通过哪一个串口发送 如 &huart2
	 DataTransmit *data: 	        		带入结构体 如 &data_uart2
	
	注意:此处用到了 STM32 HAL库 的函数 HAL_UART_Transmit 若非 HAL库 版本的代码 无法使用 
	 
********************************************************************/
void Data_Transmit(UART_HandleTypeDef *huart, DataTransmit *data)
{
	
	HAL_UART_Transmit(huart, data -> transmit_data, data -> transmit_data[2] + 4 , 0xFFFF);
	
}

/********************************************************************

串口数据打包发送函数 Data_Pack_Transmit

	 各参数作用
	 UART_HandleTypeDef *huart:				选择通过哪一个串口发送 如 &huart2
	 DataTransmit *data: 	        		带入结构体 如 &data_uart2
	 
********************************************************************/
void Data_Pack_Transmit(UART_HandleTypeDef *huart, DataTransmit *data)
{
	
	Data_Pack(data);				
  Data_Transmit(huart,data);			
	
}

// 至此串口数据发送函数定义结束 使用例如下
/*

DataTransmit data_uart2;						// 声明全局结构体 data_uart2 这个要放在 main 函数外面

Data_Init_Transmit(&data_uart2,0xAA,0xAA,1);	// main函数中在while(1)前 对结构体 data_uart2 进行初始化 这里的长度是有效数据长度 比如发送1个模式控制位 就带入 1

data_uart2.data[0] = 1;							// 模式控制位 为1 这段放的位置依据个人需求 需要更改 K210 或者 OpenMV 工作模式 则将 data_uart2.data[0] 改成需要的数即可 这里的 1 代表 0x01

while(1)										// 在while(1)中 或者依据个人需求 放在其他函数中
{
	Data_Pack_Transmit(&huart2,&data_uart2);	// 对数据进行打包发送
}

*/
// 至此串口数据发送函数使用例结束 接下来完成对数据接收函数定义

/********************************************************************

串口接收数据结构体初始化函数 Data_Init_Receive

	 各参数作用
	 DataReceive *data: 							    选择要初始化的结构体 如 &data_uart2_receive
	 head1:			        						帧头1 如 0xAA
	 head2:			        						帧头2 如 0xAA
	 
********************************************************************/
void Data_Init_Receive(DataReceive *data, uint8_t head1, uint8_t head2)
{
	
	data -> head1  = head1;
	data -> head2  = head2;
	data -> length = 0;
	data -> i      = 0;
	data -> cnt    = 0;
	data -> state  = 0;
	
	for(uint8_t j = 0; j < 50; j++)
	{
		data -> receive_data[j] = 0;
	}
	
}

/********************************************************************

串口接收1个数据函数 Buf_Receive

	 各参数作用
	 UART_HandleTypeDef *huart:			选择通过哪一个串口接收 如 &huart2
	 DataReceive *data: 				选择要带入的结构体 如 &data_uart2_receive
	 
********************************************************************/
uint8_t Buffer_Receive(UART_HandleTypeDef *huart, DataReceive *data)
{
	HAL_UART_Receive_IT(huart, &data -> data, 1);
	return data -> data;
}

/********************************************************************

串口接收数据函数 Data_Receive

	 各参数作用
	 DataReceive *data: 						选择要带入的结构体 如 &data_uart2_receive
	 buf:										串口接收到的数据
	 
********************************************************************/
void Data_Receive(DataReceive *data, uint8_t buf)
{
	
	if(data -> state == 0 && buf == data -> head1)
	{
		
		data -> state = 1;
		data -> receive_data[0] = buf;
		
	}
	else if(data -> state == 1 && buf == data -> head2)
	{
		
		data -> state = 2;
		data -> receive_data[1] = buf;
		
	}
	else if(data -> state == 2 && buf < 40)
	{
		
		data -> state = 3;
		data -> length = buf;
		data -> cnt = buf+4;
		data -> receive_data[2] = buf;
		
	}
	else if(data -> state == 3 && data -> length > 0)
	{
		
		data -> length = data -> length - 1;
		data -> receive_data[3 + data -> i] = buf;
		data -> i = data -> i + 1;
		if(data -> length == 0)
		{
			
			data -> state = 4;
			
		}
		
	}
	else if(data -> state == 4)
	{
		
		data -> receive_data[3 + data -> i] = buf;
		data -> state = 0;
		data -> i = 0;
		
	}
	else
	{
		
		data -> state = 0;
		data -> i = 0;
		
	}
	
}

// 至此串口数据接收函数定义结束 使用例如下
/*

DataReceive data_uart2_receive;										// 声明全局结构体 data_uart2_receive 这个要放在 main 函数外面

Data_Init_Receive(&data_uart2_receive,0xAA,0xAA);					// main的函数在while(1)前 对接收结构体进行初始化

Buffer_Receive(&huart2,&data_uart2_receive);						// main的函数在while(1)前 HAL库需要接收一次 使之可以进入中断回调

// 中断回调函数中调用
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{

	if( huart == &huart2 )											// 检测到串口中断2
	{

		Data_Receive(&data_uart2_receive,Buffer_Receive(&huart2,&data_uart2_receive));

	}

}

// 需注意 函数 HAL_UART_RxCpltCallback 是一个弱定义 可以在 main.c 的 USER CODE BEGIN 4 中进行重写

*/
// 至此串口数据接收函数使用例结束 接下来举例说明数据解析

/********************************************************************

串口接收数据结构体初始化函数 Target_Init

	 各参数作用
	 TargetAttribute *target: 	选择要初始化的结构体 如 &target_1
	 
********************************************************************/
void Target_Init(TargetAttribute *target)
{
	
	target -> x     = 0;
	target -> y     = 0;
	target -> color = 0;
	target -> shape = 0;
	target -> flag  = 0;
	
}

/********************************************************************

串口接收数据结构体初始化函数 Target_Init

   各参数作用
   DataReceive *data: 				选择要带入的结构体 如 &data_uart2_receive
   TargetAttribute *target: 		选择要带入的结构体 如 &target_1
	 
********************************************************************/
void Target_Parse(DataReceive *data,TargetAttribute *target)
{
	
	uint8_t sum = 0;
	uint8_t i   = 0;
	
	while(i < data -> cnt - 1)
	{
		
		sum = sum + data -> receive_data[i];
		i = i + 1;
		
	}
	
	if(sum == data -> receive_data[data -> cnt - 1])
	{
		
		target -> x     = data -> receive_data[3]*256 + data -> receive_data[4];
		target -> y     = data -> receive_data[5]*256 + data -> receive_data[6];
		target -> color = data -> receive_data[7];
		target -> shape = data -> receive_data[8];
		target -> flag  = data -> receive_data[9];
		
	}
	
}

// 至此数据解析函数定义结束 使用例如下
/*

TargetAttribute target_1;											// 声明全局结构体 target_1 这个要放在 main 函数外面

Target_Init(&target_1);												// main的函数在while(1)前 对解析结构体进行初始化

Target_Parse(&data_uart2_receive,&target_1);						// 可以放在while(1)中 也可自定义放在其他地方

*/
// 解析函数可根据自身需求自由定义 方法不唯一

uartdata.h

#ifndef __UARTDATA_H
#define __UARTDATA_H

#include "system.h"

typedef struct
{
	
	uint8_t	head1;							// 帧头1
	uint8_t	head2;							// 帧头2
	uint8_t length;							// 有效数据长度
	uint8_t data[40];						// 有效数据组成的数组
	uint8_t transmit_data[50];				// 实际发送的数组 附带上帧头1 帧头2 有效数据长度位 校验位
	
}DataTransmit;

void Data_Init_Transmit(DataTransmit *data, uint8_t head1, uint8_t head2, uint8_t length);
void Data_Pack(DataTransmit *data);
void Data_Transmit(UART_HandleTypeDef *huart, DataTransmit *data);
void Data_Pack_Transmit(UART_HandleTypeDef *huart, DataTransmit *data);

typedef struct
{
	
	uint8_t	head1;							// 帧头1
	uint8_t	head2;							// 帧头2
	uint8_t length;							// 有效数据长度
	uint8_t i;								// 有效数据下标
	uint8_t cnt;							// 总数据长度
	uint8_t state;							// 接收状态
	uint8_t receive_data[50];				// 实际接收的数组 附带上帧头1 帧头2 有效数据长度位 校验位
	uint8_t data;							// 接收数据
	
}DataReceive;

void Data_Init_Receive(DataReceive *data, uint8_t head1, uint8_t head2);
uint8_t Buffer_Receive(UART_HandleTypeDef *huart, DataReceive *data);
void Data_Receive(DataReceive *data, uint8_t buf);

typedef struct
{
	
	uint16_t	x;							// 目标x轴坐标
	uint16_t	y;							// 目标y轴坐标
	uint8_t color;							// 目标颜色标志位
	uint8_t shape;							// 目标形状标志位
	uint8_t flag;							// 目标标志位
	
}TargetAttribute;

void Target_Init(TargetAttribute *target);
void Target_Parse(DataReceive *data,TargetAttribute *target);

#endif

system.h

#ifndef __SYSTEM_H
#define __SYSTEM_H

#include "main.h"
#include "usart.h"
#include "gpio.h"

#include "uartdata.h"

typedef signed char             	int8_t;   
typedef short int               	int16_t;  
typedef int                     	int32_t; 

typedef unsigned char 				uint8_t;
typedef unsigned short int 			uint16_t;
typedef unsigned int            	uint32_t;

#endif

然后要把 uartdata.c 文件添加到工程里。
在这里插入图片描述

STM32串口发送相关定义

需要事先说明的是,system.h中包含了一些头文件和一些定义,因此在uartdata.c和uartdata.h的开头需要将system.h导入进来。如果不导入,将无法调用 HAL库 的函数,以及无法识别 uint8_t 等。

#include "system.h"

在 uartdata.h 中我定义了如下结构体来保存发送数组,定义如下函数名来执行串口发送任务。其实就跟在K210上是一样的,只不过python里面定义的是类,这里改成了定义结构体而已。
有效数据数组data是方便传输数据用的,比如需要发送1个数据,那你只需要
结构体名.data[0] = 你要发送的数据;

typedef struct
{
	
	uint8_t	head1;							// 帧头1
	uint8_t	head2;							// 帧头2
	uint8_t length;							// 有效数据长度
	uint8_t data[40];						// 有效数据组成的数组
	uint8_t transmit_data[50];	            // 实际发送的数组 附带上帧头1 帧头2 有效数据长度位 校验位
	
}DataTransmit;

void Data_Init_Transmit(DataTransmit *data, uint8_t head1, uint8_t head2, uint8_t length);
void Data_Pack(DataTransmit *data);
void Data_Transmit(UART_HandleTypeDef *huart, DataTransmit *data);
void Data_Pack_Transmit(UART_HandleTypeDef *huart, DataTransmit *data);

然后就是初始化发送数据结构体函数的定义,传入参数为结构体,帧头1,帧头2,有效数据长度。该函数会将帧头及有效数据长度保存,然后初始化有效数据数组和总数组。

void Data_Init_Transmit(DataTransmit *data, uint8_t head1, uint8_t head2, uint8_t length)
{
	
	data -> head1  = head1;
	data -> head2  = head2;
	data -> length = length;
	
	for(uint8_t i = 0; i < length; i++)
	{
		data -> data[i] = 0;
	}
	
	for(uint8_t j = 0; j < length + 4; j++)
	{
		data -> transmit_data[j] = 0;
	}
	
}

打包函数就是将帧头、有效数据长度、有效数据数组、校验位打包成一个数组。该数组中,下标0是帧头1、下标1是帧头2、下标2是有效数据长度、然后下标从3开始,根据有效数据长度是多少就打包多少个有效数据到总数组,最后再打包一下校验和。

void Data_Pack(DataTransmit *data)
{
	
	data -> transmit_data[0] = data -> head1;
	data -> transmit_data[1] = data -> head2;
	data -> transmit_data[2] = data -> length;
	
	for(uint8_t i = 0; i < data -> length; i++)
	{
		data -> transmit_data[3+i] = data -> data[i];
	}
	
	uint8_t sum = 0;
	
  for(uint8_t j = 0; j < data -> length + 3; j++)
	{
		sum += data -> transmit_data[j];
	}
	
  data -> transmit_data[data -> length + 3] = sum;
	
}

发送函数实际上是调用了HAL库的串口发送函数HAL_UART_Transmit,传入结构体为HAL库的结构体UART_HandleTypeDef以及自定义的结构体DataTransmit。
在HAL库中,若串口被配置,默认名字为huartx,比如配置了串口2,则串口2的名字为huart2。
很明显,传入的结构体UART_HandleTypeDef决定通过哪一个串口发送数据,比如串口2,则需要传入&huart2
而传入的结构体DataTransmit决定发送哪一个结构体中的数组transmit_data
比如定义了两个,分别是
DataTransmit k210;
DataTransmit openmv;
而串口2是连接到k210的,那么就需要传入&k210,而不是&openmv

void Data_Transmit(UART_HandleTypeDef *huart, DataTransmit *data)
{
	
	HAL_UART_Transmit(huart, data -> transmit_data, data -> transmit_data[2] + 4 , 0xFFFF);
	
}

打包发送函数实际上就是将上面的打包函数和发送函数封装成一个函数,在使用的时候只需要调用这一个函数即可完成打包和发送操作,在后面的注释中也给出了以上结构体和函数详细的使用方法。

void Data_Pack_Transmit(UART_HandleTypeDef *huart, DataTransmit *data)
{
	
	Data_Pack(data);				
 	Data_Transmit(huart,data);			
	
}

// 至此串口数据发送函数定义结束 使用例如下
/*

DataTransmit data_uart2;						// 声明全局结构体 data_uart2 这个要放在 main 函数外面

Data_Init_Transmit(&data_uart2,0xAA,0xAA,1);	// main函数中在while(1)前 对结构体 data_uart2 进行初始化 这里的长度是有效数据长度 比如发送1个模式控制位 就带入 1

data_uart2.data[0] = 1;							// 模式控制位 为1 这段放的位置依据个人需求 需要更改 K210 或者 OpenMV 工作模式 则将 data_uart2.data[0] 改成需要的数即可 这里的 1 代表 0x01

while(1)										// 在while(1)中 或者依据个人需求 放在其他函数中 这里的while(1)别复制哦
{
	Data_Pack_Transmit(&huart2,&data_uart2);	// 对数据进行打包发送
}

*/
// 至此串口数据发送函数使用例结束 接下来完成对数据接收函数定义

别忘记了在main.c中 需要先引入

#include "system.h"

如果你要通过串口2来发送数据,可以这样声明结构体,这个是要放在main.c文件中的 main 函数外面的。

DataTransmit data_uart2;						// 声明全局结构体 data_uart2 这个要放在 main 函数外面

然后需要在 main 函数中的 while 循环前,对该结构体进行初始化。

Data_Init_Transmit(&data_uart2,0xAA,0xAA,1);	// main函数中在while(1)前 对结构体 data_uart2 进行初始化 这里的长度是有效数据长度 比如发送1个模式控制位 就带入 1

然后你就可以写入你要发送的数据了,这一段放的位置由你来定,执行到哪一步需要哪一个模式,你就在哪一步加上这句话,然后把模式改过去。

data_uart2.data[0] = 1;							// 模式控制位 为1 这段放的位置依据个人需求 需要更改 K210 或者 OpenMV 工作模式 则将 data_uart2.data[0] 改成需要的数即可 这里的 1 代表 0x01

最后是串口数据发送,我这里是写在 while 循环中,这个你也可以写在其他地方,可以一直发送也可以用其他标志位控制发送。
注释中的 while(1) 是告诉你我将这个函数放在main函数的 while 循环中的意思,可别复制哦!复制了就一直卡死在发送这个循环里面了。

Data_Pack_Transmit(&huart2,&data_uart2);	// 对数据进行打包发送

STM32串口发送测试

接线方式如下所示。
在这里插入图片描述

如果你按照使用说明添加好相关代码到main.c中,就可以编译烧入,然后在XCOM上查看结果了。我使用了ST-Link,所以直接在Keil中就可以烧入了。
在这里插入图片描述
然后打开串口工具软件,设置和之前是一样的,波特率921600,数据位8,停止位1,无校验位。
在这里插入图片描述
因为初始化的时候是给的1位有效数据,然后将这个有效数据赋值为1,因此发送的数组就是
AA AA 01 01 56
在这里插入图片描述
当然如果你要发送两位,可以这样改。

	Data_Init_Transmit(&data_uart2,0xAA,0xAA,2);	// 将2传递给有效数据长度

	data_uart2.data[0] = 1;							// 发送第一个数据为1					
	data_uart2.data[1] = 2;							// 发送第二个数据为2
		

编译烧入后你就可以得到这种结果
AA AA 02 01 02 59
可以看到发送的数据多了1位了,2也被成功发送了出去。
在这里插入图片描述
需要注意的是,这里都是用1个8位来发送,如果你要发送超过255的数据,需要拆分成两个8位哦,不然就会数据溢出,用左移8位来获得高八位就好了,和K210那边是一样的。

STM32串口接收相关定义

这里我是定义了两个结构体,一个结构体用于接收数据,一个结构体用于解析并保存数据。DataReceive是用于接收数据的,head1和head2是帧头,length是有效数据长度,i是有效数据下标(用来逐个保存有效数据),cnt是总数据长度,state是接收状态,receive_data是接收数据缓冲数组,data是接收数据缓冲变量(用于保存 HAL库 的串口接收函数中得到的数据)。

typedef struct
{
	
	uint8_t	head1;							// 帧头1
	uint8_t	head2;							// 帧头2
	uint8_t length;							// 有效数据长度
	uint8_t i;								// 有效数据下标
	uint8_t cnt;							// 总数据长度
	uint8_t state;							// 接收状态
	uint8_t receive_data[50];		        // 实际接收的数组 附带上帧头1 帧头2 有效数据长度位 校验位
	uint8_t data;							// 接收数据
	
}DataReceive;

void Data_Init_Receive(DataReceive *data, uint8_t head1, uint8_t head2);
uint8_t Buffer_Receive(UART_HandleTypeDef *huart, DataReceive *data);
void Data_Receive(DataReceive *data, uint8_t buf);

typedef struct
{
	
	uint16_t	x;							// 目标x轴坐标
	uint16_t	y;							// 目标y轴坐标
	uint8_t color;							// 目标颜色标志位
	uint8_t shape;							// 目标形状标志位
	uint8_t flag;							// 目标标志位
	
}TargetAttribute;

void Target_Init(TargetAttribute *target);
void Target_Parse(DataReceive *data,TargetAttribute *target);

与发送初始化函数不同的是,接收初始化函数只需要传入结构体,帧头,无需传入有效数据长度(有效数据长度通过发送端得到),这样目的是实现不同长度的接收。

void Data_Init_Receive(DataReceive *data, uint8_t head1, uint8_t head2)
{
	
	data -> head1  = head1;
	data -> head2  = head2;
	data -> length = 0;
	data -> i      = 0;
	data -> cnt    = 0;
	data -> state  = 0;
	
	for(uint8_t j = 0; j < 50; j++)
	{
		data -> receive_data[j] = 0;
	}
	
}

数据接收函数是调用了 HAL库 的串口接收函数(中断接收),传入的结构体是UART_HandleTypeDef和DataReceive
比如要从串口2接收数据保存到结构体uart2_receive则需要传入
&huart2
&uart2_receive
需要注意的是 HAL库 中串口接收中断需要在 while 循环前调用一次接收函数,才能进入该中断,因此这个函数需要在 while 循环之前写一句。

uint8_t Buffer_Receive(UART_HandleTypeDef *huart, DataReceive *data)
{
	HAL_UART_Receive_IT(huart, &data -> data, 1);
	return data -> data;
}

然后就是串口数据接收函数,这个原理和K210上的原理是一样的,只是变成了用c语言来写,不妨回顾下接收经历的这些状态。
若状态为0且传入的数据等于帧头1,则保存该数据,然后进入状态1。
进入状态1后,若传入的数据等于帧头2,则保存该数据,然后进入状态2。
进入状态2后,判断需要接收的有效数据长度是否小于40,若小于,则保存该数据以及总数据长度,然后进入状态3。
进入状态3后,开始接收有效数据长度个数据,接收完毕会进入状态4。
进入状态4后,保存最后一位校验位,然后将数组以及数据总长传入解析函数Receive_STM32进行解析。
解析完会将状态置0,数组清空,准备下一次接收。
以上是K210的接收过程,STM32上有一点区别
若状态为0且传入的数据等于帧头1,则保存该数据,然后进入状态1。
进入状态1后,若传入的数据等于帧头2,则保存该数据,然后进入状态2。
进入状态2后,判断需要接收的有效数据长度是否小于40,若小于,则保存该数据以及总数据长度,然后进入状态3。
进入状态3后,开始接收有效数据长度个数据,接收完毕会进入状态4。
进入状态4后,保存最后一位校验位,不进行解析(我将解析函数独立了出来,并且数组不清空,方便灵活运用,以及提高效率),将有效数据下标 i 和状态 state 置零。

void Data_Receive(DataReceive *data, uint8_t buf)
{
	
	if(data -> state == 0 && buf == data -> head1)
	{
		
		data -> state = 1;
		data -> receive_data[0] = buf;
		
	}
	else if(data -> state == 1 && buf == data -> head2)
	{
		
		data -> state = 2;
		data -> receive_data[1] = buf;
		
	}
	else if(data -> state == 2 && buf < 40)
	{
		
		data -> state = 3;
		data -> length = buf;
		data -> cnt = buf+4;
		data -> receive_data[2] = buf;
		
	}
	else if(data -> state == 3 && data -> length > 0)
	{
		
		data -> length = data -> length - 1;
		data -> receive_data[3 + data -> i] = buf;
		data -> i = data -> i + 1;
		if(data -> length == 0)
		{
			
			data -> state = 4;
			
		}
		
	}
	else if(data -> state == 4)
	{
		
		data -> receive_data[3 + data -> i] = buf;
		data -> state = 0;
		data -> i = 0;
		
	}
	else
	{
		
		data -> state = 0;
		data -> i = 0;
		
	}
	
}
// 至此串口数据接收函数定义结束 使用例如下
/*

DataReceive data_uart2_receive;										// 声明全局结构体 data_uart2_receive 这个要放在 main 函数外面

Data_Init_Receive(&data_uart2_receive,0xAA,0xAA);					// main的函数在while(1)前 对接收结构体进行初始化

Buffer_Receive(&huart2,&data_uart2_receive);						// main的函数在while(1)前 HAL库需要接收一次 使之可以进入中断回调

// 中断回调函数中调用
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{

	if( huart == &huart2 )											// 检测到串口中断2
	{

		Data_Receive(&data_uart2_receive,Buffer_Receive(&huart2,&data_uart2_receive));

	}

}

接收函数的使用方法如下,与发送一样的,结构体要在 main.c 文件中 main 函数之前进行声明。

DataReceive data_uart2_receive;										// 声明全局结构体 data_uart2_receive 这个要放在 main 函数外面

然后要对接收结构体进行初始化,并接收1个数据,才能进入中断。

Data_Init_Receive(&data_uart2_receive,0xAA,0xAA);					// main的函数在while(1)前 对接收结构体进行初始化

Buffer_Receive(&huart2,&data_uart2_receive);						// main的函数在while(1)前 HAL库需要接收一次 使之可以进入中断回调

然后重写中断回调函数,接收函数放在中断回调函数里面。中断回调函数是一个弱定义,可以直接在 main.c 文件中重写,我写入的位置是
/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

// 中断回调函数中调用
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{

	if( huart == &huart2 )											// 检测到串口中断2
	{

		Data_Receive(&data_uart2_receive,Buffer_Receive(&huart2,&data_uart2_receive));

	}

}

然后就是完成数据解析,首先便是结构体初始化函数,实际上就是把结构体中所有变量置0,我这里只写了5个,如果你要加新的变量,除了在结构体加新变量以外,记得在初始化函数中也加入新变量。

void Target_Init(TargetAttribute *target)
{
	
	target -> x     = 0;
	target -> y     = 0;
	target -> color = 0;
	target -> shape = 0;
	target -> flag  = 0;
	
}

然后就是数据解析了,这个和K210那边也是一样的,先校验和,通过则保存数据。

void Target_Parse(DataReceive *data,TargetAttribute *target)
{
	
	uint8_t sum = 0;
	uint8_t i   = 0;
	
	while(i < data -> cnt - 1)
	{
		
		sum = sum + data -> receive_data[i];
		i = i + 1;
		
	}
	
	if(sum == data -> receive_data[data -> cnt - 1])
	{
		
		target -> x     = data -> receive_data[3]*256 + data -> receive_data[4];
		target -> y     = data -> receive_data[5]*256 + data -> receive_data[6];
		target -> color = data -> receive_data[7];
		target -> shape = data -> receive_data[8];
		target -> flag  = data -> receive_data[9];
		
	}
	
}

// 至此数据解析函数定义结束 使用例如下
/*

TargetAttribute target_1;						// 声明全局结构体 target_1 这个要放在 main 函数外面

Target_Init(&target_1);							// main的函数在while(1)前 对解析结构体进行初始化

Target_Parse(&data_uart2_receive,&target_1);	// 可以放在while(1)中 也可自定义放在其他地方

*/
// 解析函数可根据自身需求自由定义 方法不唯一

使用方法也是和之前的大同小异,声明在 main.c 文件中,在 main 函数前。

TargetAttribute target_1;						// 声明全局结构体 target_1 这个要放在 main 函数外面

在 main 函数的 while 循环前,初始化。

Target_Init(&target_1);							// main的函数在while(1)前 对解析结构体进行初始化

解析函数放的位置依据个人需求来定,你也可以写一个定时器,在定时器的回调函数中调用,让解析有一个固定的周期,比如一秒解析十次。我这里为了测试方便就直接放在了 while 循环中。

Target_Parse(&data_uart2_receive,&target_1);	// 可以放在while(1)中 也可自定义放在其他地方

STM32串口接收测试

接线方式和测试发送的时候是一模一样的。
在这里插入图片描述
如果你按照使用说明写好代码了,就可以编译烧入了。为了方便看到接收数据,我们使用ST-Link进行Debug测试。
在这里插入图片描述
右键结构体添加到Watch 1,data_uart2_receive是接收数据,target_1是解析数据,两个都需要添加进来。
在这里插入图片描述
然后发送如下测试数据。
在这里插入图片描述
在这里插入图片描述
可以看到receive_data可以接收到数据了。
在这里插入图片描述
解析函数也能正确将数据解析出来。
在这里插入图片描述

K210与STM32进行通信

之前都是发送固定的数据,这次不妨来发送动态的数据进行测试,在K210上简单写一个寻找黑色的函数,然后将找到的黑色坐标保存到数组中发回给32,测试能否成功通信并且接收正确。
在K210中需要添加如下代码,这里我让color值为1来表示颜色黑色,flag值为1来表示找到目标。

#__________________________________________________________________
# 寻找黑色
black=[(0, 40, -10, 10, -10, 10)]                                   # 黑色阈值

def opv_find_black():                                               # 定义寻找黑色函数
    TSTM32.flag=0                                                   # 目标标志位置0

    if (K210.mode&0x02)!= 0:                                        # 如果工作在模式2下
        img=sensor.snapshot()                                       # 拍摄一张照片
        pixels_max=0                                                # 像素最大值初始化为0

        # 遍历寻找颜色
        for color in img.find_blobs([black[0]],                     # 颜色阈值
        roi=(0,0,320,240),                                          # 感兴趣区域(x,y,w,h)
        x_stride=2,y_stride=2,                                      # x轴最小宽度像素 y轴最小像素宽度 如果色块比较大可以调大此参数以获取更高速度
        pixels_threshold=100,area_threshold=100,                    # 像素个数阈值 色块被框面积阈值 小于滤除
        merge=True,margin=20):                                      # True则合并像素 False则不合并像素 margin是合并像素最大间距
            img.draw_rectangle(color[0:4])                          # 圈出搜索到的色块b b[0:4] 表示b[0]~b[3] 分别对应(x,y,w,h)
            if pixels_max<color.pixels():                           # 如果最大像素值小于色块b的像素值
                pixels_max=color.pixels()                           # 则将色块b当作最大像素色块
                TSTM32.x = color.cx()                               # 目标x轴中心坐标等于最大像素色块的x轴中心坐标
                TSTM32.y = color.cy()                               # 目标y轴中心坐标等于最大像素色块的y轴中心坐标
                TSTM32.color = 1
                TSTM32.flag = 1                                     # 将目标标志位置1

        if TSTM32.flag==1:                                          # 如果目标标志位被置1
            img.draw_cross(TSTM32.x,TSTM32.y, color=127, size = 15) # 在目标中心画一个十字 坐标为目标中心坐标 颜色127 长度15
            img.draw_circle(TSTM32.x,TSTM32.y, 15, color = 127)     # 在目标中心画一个圆 坐标为目标中心坐标 半径15 颜色127
            led_land_flag=1                                         # led标志位

然后需要删除或者注释之前的测试代码

TSTM32.x     = 1
TSTM32.y     = 2
TSTM32.color = 3
TSTM32.shape = 4
TSTM32.flag  = 5

TOpenMV.x     = 257
TOpenMV.y     = 258
TOpenMV.color = 3
TOpenMV.shape = 4
TOpenMV.flag  = 5

主函数中删除或注释如下代码

	print("Mode: "+str(K210.mode)+" OpenMV: "+str(K210.x)+" "+str(K210.y)+" "+str(K210.color)+" "+str(K210.shape)+" "+str(K210.flag))

    for item in fm.get_gpio_used():
        print(item)

主函数中添加如下代码,方便观察K210当前处于什么模式。

	print("Mode: "+str(K210.mode))

主函数中将模式2改成如下代码,这样在进入模式2的时候就会寻找黑色了。

	elif K210.mode==0x02:
	        opv_find_black()
	        print(TSTM32.x,TSTM32.y,TSTM32.color,TSTM32.flag)
	        #img=sensor.snapshot()                       # 拍摄一张照片

接线方面是K210的P9接STM32的PA2,K210的P6接STM32的PA3,不要忘记共地。
在这里插入图片描述
上电,STM32进入Debug。
在这里插入图片描述
K210连接后运行。
在这里插入图片描述
将STM32的发送结构体添加到Watch 1
在这里插入图片描述
然后运行,将data[0]改为2发送(输入2然后回车键即可)。
在这里插入图片描述
但测试发现,K210端并不能正确接收,于是我将接收到的数组打印了出来,发现出现了乱序的情况。波特率越高,对导线的要求也就越高,我在比赛的时候用的921600的波特率是没问题的,但今天出现这种情况,我怀疑是我现在用的这两根杜邦线用太久,老化严重了,因此我降低了波特率到115200
在这里插入图片描述
更改K210的波特率为115200。
在这里插入图片描述
更改STM32的波特率为115200。
在这里插入图片描述
然后重新烧入测试,输入0,发现可以成功通信并且更改模式为0了!所以如果你们也出现这种情况,可以考虑下降低波特率这个方法。
在这里插入图片描述
在这里插入图片描述
然后我们把模式更改为2,看看是否能找到黑色并且成功将黑色坐标发送回来。但是结果是K210端可以把模式切换为模式2了,但STM32端却无法实时显示坐标,这个原因是因为K210发送的太快了,让STM32一直进入串口中断,导致无法解析,因为我这里为了方便测试,解析函数是写在while循环里面的,一直进入中断,导致无法成功解析
在这里插入图片描述
在这里插入图片描述
解决这种问题的方法有两种,一种是写个定时器,一种是写一个计数器,为了测试方便,这里先用计数器的方法来控制发送频率,需要注意的是不能用延时函数,使用延时函数会导致识别颜色整体速度下降,这里大概控制一下发送频率,观察32端能否正常解析。
在这里插入图片描述
在这里插入图片描述
发现STM32已经可以正常更新数据了,坐标也与在K210的IDE上看到的所差无几,这是正常现象,因为每一次识别的颜色坐标难免会产生偏差。
在这里插入图片描述
在这里插入图片描述

OpenMV如何进行串口通信

这部分下次再写吧!从8.18开始写到现在,54700多个字,休息一下嘿嘿嘿!

完整源码

K210完整源码

# UART_V1.0 - By: FITQY - 周四 8 月 18 日 2022
#__________________________________________________________________
# 导入模块
import sensor, image, time, math                            # 导入感光元件模块 sensor 机器视觉模块 image 跟踪运行时间模块 time 数学函数模块 math
import machine                                              # 导入模块 machine
from machine import UART                                    # 从 machine 模块中导入 双向串行通信模块 UART
from fpioa_manager import fm                                # 从 fpioa_manager 模块中导入 引脚注册模块 fm
from Maix import GPIO                                       # 从 Maix 模块中导入 引脚模块 GPIO

#__________________________________________________________________
# 感光元件设置
sensor.reset()                                              # 重置和初始化相机传感器 默认设置为 摄像头频率24M 不开启双缓冲模式

sensor.set_pixformat(sensor.RGB565)                         # 设置图像格式为 RGB565 (彩色)
sensor.set_framesize(sensor.QVGA)                           # 设置图像大小为 QVGA (320 x 240) 像素个数 76800

sensor.set_auto_exposure(1)                                 # 设置自动曝光
sensor.set_auto_gain(0, gain_db = 17)                       # 设置增益 17
sensor.set_auto_whitebal(0, rgb_gain_db = (0,0,0))          # 设置RGB增益 0 0 0

sensor.set_contrast(0)                                      # 设置对比度 0
sensor.set_brightness(0)                                    # 设置亮度 0
sensor.set_saturation(0)                                    # 设置饱和度 0

sensor.set_vflip(1)                                         # 打开垂直翻转
sensor.set_hmirror(1)                                       # 打开水平镜像

sensor.skip_frames(time = 2000)                             # 延时跳过2s 等待感光元件稳定

#__________________________________________________________________
# 创建时钟对象
clock = time.clock()                                        # 创建一个时钟对象 clock

#__________________________________________________________________
# 串口设置
# 串口1 设置 P9 RX P6 TX
fm.register(9, fm.fpioa.UART1_RX, force = True)      # 配置 9 脚为 UART1_RX 强制注册
fm.register(6, fm.fpioa.UART1_TX, force = True)      # 配置 6 脚为 UART1_TX 强制注册

uart1 = UART(UART.UART1, 921600, 8, 0, 1)            # 设置 uart1 为 串口1 波特率 921600 数据位 8位 校验位 0位 停止位 1位

# 串口2 设置 P7 RX P8 TX
fm.register(7, fm.fpioa.UART2_RX, force = True)      # 配置 7 脚为 UART2_RX 强制注册
fm.register(8, fm.fpioa.UART2_TX, force = True)      # 配置 8 脚为 UART2_TX 强制注册

uart2 = UART(UART.UART2, 921600, 8, 0, 1)            # 设置 uart2 为 串口2 波特率 921600 数据位 8位 校验位 0位 停止位 1位

#__________________________________________________________________
# 串口发送 STM32
# 定义 STM32 发送类
class STM32_transmit():                              # 定义 STM32 发送类
    head1  = 0xAA                                    # uint8_t   帧头1
    head2  = 0xAA                                    # uint8_t   帧头2
    x      = 0                                       # uint16_t  目标x轴坐标
    y      = 0                                       # uint16_t  目标y轴坐标
    color  = 0                                       # uint8_t   目标颜色标志位
    shape  = 0                                       # uint8_t   目标形状标志位
    flag   = 0                                       # uint8_t   目标标志位

# 实例化类
TSTM32 = STM32_transmit()                            # 实例化 STM32_transmit() 为 TSTM32

# 定义打包函数
def TSTM32_data():                                   # 数据打包函数
    data=bytearray([TSTM32.head1,                    # 帧头1
                    TSTM32.head2,                    # 帧头2
                    0x00,                            # 有效数据长度 0x00 + data_len - 4
                    TSTM32.x>>8,TSTM32.x,            # 保存目标坐标x 将整形数据拆分成两个8位
                    TSTM32.y>>8,TSTM32.y,            # 保存目标坐标y 将整形数据拆分成两个8位
                    TSTM32.color,                    # 保存目标颜色标志位
                    TSTM32.shape,                    # 保存目标形状标志位
                    TSTM32.flag,                     # 保存目标标志位
                    0x00])                           # 数据和校验位

    # 数据包的长度
    data_len = len(data)                             # 获得数据包总长度
    data[2]  = data_len - 4                          # 有效数据的长度 扣去 帧头1 帧头2 有效数据长度位 校验位

    # 校验和
    sum = 0                                          # 和置零
    for i in range(0,data_len-1):
        sum = sum + data[i]                          # 和累加
    data[data_len-1] = sum                           # 和赋值 给数组最后一位发送 只保存低8位 溢出部分无效

    # 返回打包好的数据
    return data

#__________________________________________________________________
# 串口发送 OpenMV
# 定义 OpenMV 发送类
class OpenMV_transmit():                             # 定义 OpenMV 发送类
    head1  = 0xAA                                    # uint8_t   帧头1
    head2  = 0xAA                                    # uint8_t   帧头2
    x      = 0                                       # uint16_t  目标x轴坐标
    y      = 0                                       # uint16_t  目标y轴坐标
    color  = 0                                       # uint8_t   目标颜色标志位
    shape  = 0                                       # uint8_t   目标形状标志位
    flag   = 0                                       # uint8_t   目标标志位

# 实例化类
TOpenMV = OpenMV_transmit()                          # 实例化 OpenMV_transmit() 为 TOpenMV

# 定义打包函数
def TOpenMV_data():                                  # 数据打包函数
    data=bytearray([TOpenMV.head1,                   # 帧头1
                    TOpenMV.head2,                   # 帧头2
                    0x00,                            # 有效数据长度 0x00 + data_len - 4
                    TOpenMV.x>>8,TOpenMV.x,          # 保存目标坐标x 将整形数据拆分成两个8位
                    TOpenMV.y>>8,TOpenMV.y,          # 保存目标坐标y 将整形数据拆分成两个8位
                    TOpenMV.color,                   # 保存目标颜色标志位
                    TOpenMV.shape,                   # 保存目标形状标志位
                    TOpenMV.flag,                    # 保存目标标志位
                    0x00])                           # 数据和校验位

    # 数据包的长度
    data_len = len(data)                             # 获得数据包总长度
    data[2]  = data_len - 4                          # 有效数据的长度 扣去 帧头1 帧头2 有效数据长度位 校验位

    # 校验和
    sum = 0                                          # 和置零
    for i in range(0,data_len-1):
        sum = sum + data[i]                          # 和累加
    data[data_len-1] = sum                           # 和赋值 给数组最后一位发送 只保存低8位 溢出部分无效

    # 返回打包好的数据
    return data

#__________________________________________________________________
# 串口接收 K210
# 定义 K210 接收类
class K210_receive(object):                          # 定义 K210 接收类
    x      = 0                                       # uint16_t  目标x轴坐标
    y      = 0                                       # uint16_t  目标y轴坐标
    color  = 0                                       # uint8_t   目标颜色标志位
    shape  = 0                                       # uint8_t   目标形状标志位
    flag   = 0                                       # uint8_t   目标标志位
    mode   = 0                                       # uint8_t   工作模式位

# 实例化类
K210 = K210_receive()                                # 实例化 K210_receive() 为 K210

#__________________________________________________________________
# 定义串口接收数据保存类
class uart_buf_save(object):                         # 定义 uart 接收数据保存类
    uart_buf  = []                                   # 串口缓冲区数组
    data_len  = 0                                    # 有效数据长度
    data_cnt  = 0                                    # 总数据长度
    state     = 0                                    # 接收状态

#__________________________________________________________________
# 接收 STM32 数据
# 实例化类
RSTM32  = uart_buf_save()                            # 实例化 uart_buf_prase() 为 RSTM32

# 定义接收帧头
STM32_HEADER  = [0xAA,0xAA]                          # 接收帧头 STM32

# 串口数据解析 STM32
def Receive_STM32(data_buf,num):
    # 和累加
    sum = 0
    i = 0
    while i<(num-1):
        sum = sum + data_buf[i]
        i = i + 1
    # 求余 因为 校验和 为 8 位 超出部分无效 因此只校验 低8位 即可
    sum = sum%256

    # 和校验失败则退出
    if sum != data_buf[num-1]:
        return

    # 和校验成功则接收数据
    # 接收 STM32 数据
    K210.mode = 0x00 + data_buf[3]

# 串口数据接收 STM32
def uart_receive_stm32(buf):
    if RSTM32.state==0 and buf==STM32_HEADER[0]:     # 判断帧头1是否符合要求 符合则进入下一个状态
        RSTM32.state=1
        RSTM32.uart_buf.append(buf)                  # 将这个数据添加到数组末尾

    elif RSTM32.state==1 and buf==STM32_HEADER[1]:   # 判断帧头2是否符合要求 符合则进入下一个状态
        RSTM32.state=2
        RSTM32.uart_buf.append(buf)                  # 将这个数据添加到数组末尾

    elif RSTM32.state==2 and buf<40:                 # 有效数据长度位 规定有效数据长度小于40 符合则进入下一个状态
        RSTM32.state=3
        RSTM32.data_len=buf                          # 获得有效数据长度
        RSTM32.data_cnt=buf+4                        # 获得总数据长度 总数据长度 = 帧头1 + 帧头2 + 有效数据长度位 + 有效数据 + 校验位
        RSTM32.uart_buf.append(buf)                  # 将这个数据添加到数组末尾

    elif RSTM32.state==3 and RSTM32.data_len>0:      # 存储有效数据长度个数据
        RSTM32.data_len=RSTM32.data_len-1            # 每存储一次 还需要存储的数据个数减1
        RSTM32.uart_buf.append(buf)                  # 将这个数据添加到数组末尾
        if RSTM32.data_len==0:                       # 直到存储完毕
            RSTM32.state=4                           # 进入下一个状态

    elif RSTM32.state==4:                            # 当接收到存储完毕的信息
        RSTM32.uart_buf.append(buf)                  # 保存最后一位校验位 将这个数据添加到数组末尾
        RSTM32.state=0                               # 状态重置为0 调用串口数据解析函数进行数据解析
        Receive_STM32(RSTM32.uart_buf,RSTM32.uart_buf[2]+4)
        #print(RSTM32.uart_buf)
        RSTM32.uart_buf=[]                           # 清空缓冲区 准备下次接收数据

    else:                                            # 不满足以上条件 视为接收出错 重置状态为0 丢弃所有数据 准备下一次接收数据
        RSTM32.state=0                               # 重置状态为0
        RSTM32.uart_buf=[]                           # 清空缓冲区 准备下一次接收数据

# 串口数据读取 最后调用这个函数即可
def uart1_stm32_read():
    buf_len = uart1.any()                            # 检查 串口1 是否有内容需要读取 返回等待的字节数量(可能为0)
    for i in range(0,buf_len):                       # 读取 buf_len 个数据
        uart_receive_stm32(uart1.readchar())         # 接收单个数据 uart1.readchar() 然后将这个数据传递到函数 uart_receive_stm32() 进行 STM32 数据接收

#__________________________________________________________________
# 接收 OpenMV 数据
# 实例化类
ROpenMV = uart_buf_save()                            # 实例化 uart_buf_prase() 为 ROpenMV

# 定义接收帧头
OpenMV_HEADER = [0xAA,0xAA]                          # 接收帧头 OpenMV

# 串口数据解析 OpenMV
def Receive_OpenMV(data_buf,num):
    # 和累加
    sum = 0
    i = 0
    while i<(num-1):
        sum = sum + data_buf[i]
        i = i + 1
    # 求余 因为 校验和 为 8 位 超出部分无效 因此只校验 低8位 即可
    sum = sum%256

    # 和校验失败则退出
    if sum != data_buf[num-1]:
        return

    # 和校验成功则接收数据
    # 接收 OpenMV 数据
    K210.x      = data_buf[3]*256 + data_buf[4]
    K210.y      = data_buf[5]*256 + data_buf[6]
    K210.color  = data_buf[7]
    K210.shape  = data_buf[8]
    K210.flag   = data_buf[9]

# 串口数据接收 OpenMV
def uart_receive_openmv(buf):
    if ROpenMV.state==0 and buf==OpenMV_HEADER[0]:   # 判断帧头1是否符合要求 符合则进入下一个状态
        ROpenMV.state=1
        ROpenMV.uart_buf.append(buf)                 # 将这个数据添加到数组末尾

    elif ROpenMV.state==1 and buf==OpenMV_HEADER[1]: # 判断帧头2是否符合要求 符合则进入下一个状态
        ROpenMV.state=2
        ROpenMV.uart_buf.append(buf)                 # 将这个数据添加到数组末尾

    elif ROpenMV.state==2 and buf<40:                # 有效数据长度位 规定有效数据长度小于40 符合则进入下一个状态
        ROpenMV.state=3
        ROpenMV.data_len=buf                         # 获得有效数据长度
        ROpenMV.data_cnt=buf+4                       # 获得总数据长度 总数据长度 = 帧头1 + 帧头2 + 有效数据长度位 + 有效数据 + 校验位
        ROpenMV.uart_buf.append(buf)                 # 将这个数据添加到数组末尾

    elif ROpenMV.state==3 and ROpenMV.data_len>0:    # 存储有效数据长度个数据
        ROpenMV.data_len=ROpenMV.data_len-1          # 每存储一次 还需要存储的数据个数减1
        ROpenMV.uart_buf.append(buf)                 # 将这个数据添加到数组末尾
        if ROpenMV.data_len==0:                      # 直到存储完毕
            ROpenMV.state=4                          # 进入下一个状态

    elif ROpenMV.state==4:                           # 当接收到存储完毕的信息
        ROpenMV.uart_buf.append(buf)                 # 保存最后一位校验位 将这个数据添加到数组末尾
        ROpenMV.state=0                              # 状态重置为0 调用串口数据解析函数进行数据解析
        Receive_OpenMV(ROpenMV.uart_buf,ROpenMV.uart_buf[2]+4)
        #print(ROpenMV.uart_buf)
        ROpenMV.uart_buf=[]                          # 清空缓冲区 准备下次接收数据

    else:                                            # 不满足以上条件 视为接收出错 重置状态为0 丢弃所有数据 准备下一次接收数据
        ROpenMV.state=0                              # 重置状态为0
        ROpenMV.uart_buf=[]                          # 清空缓冲区 准备下一次接收数据

# 串口数据读取 最后调用这个函数即可
def uart2_openmv_read():
    buf_len = uart2.any()                            # 检查 串口2 是否有内容需要读取 返回等待的字节数量(可能为0)
    for i in range(0,buf_len):                       # 读取 buf_len 个数据
        uart_receive_openmv(uart2.readchar())        # 接收单个数据 uart2.readchar() 然后将这个数据传递到函数 uart_receive_openmv() 进行 openmv 数据接收

#__________________________________________________________________
# 调试区
K210.mode = 0x00                                     # 工作模式

TSTM32.x     = 1
TSTM32.y     = 2
TSTM32.color = 3
TSTM32.shape = 4
TSTM32.flag  = 5

TOpenMV.x     = 257
TOpenMV.y     = 258
TOpenMV.color = 3
TOpenMV.shape = 4
TOpenMV.flag  = 5

#__________________________________________________________________
# 主函数
while(True):

    clock.tick()

    print("Mode: "+str(K210.mode)+" OpenMV: "+str(K210.x)+" "+str(K210.y)+" "+str(K210.color)+" "+str(K210.shape)+" "+str(K210.flag))

    for item in fm.get_gpio_used():
        print(item)

    # 模式选择______________________________________________________

    if K210.mode==0x00:
        img=sensor.snapshot()                        # 拍摄一张照片

    elif K210.mode==0x01:
        img=sensor.snapshot()                        # 拍摄一张照片

    elif K210.mode==0x02:
        img=sensor.snapshot()                        # 拍摄一张照片

    elif K210.mode==0x03:
        img=sensor.snapshot()                        # 拍摄一张照片

    else:
        img=sensor.snapshot()                        # 拍摄一张照片

    # 串口通信______________________________________________________

    uart1.write(TSTM32_data())                       # 串口1 数据发送
    uart2.write(TOpenMV_data())                      # 串口2 数据发送

    uart1_stm32_read()                               # 串口1 数据接收
    uart2_openmv_read()                              # 串口2 数据接收

#__________________________________________________________________

STM32完整源码

uartdata.c

/********************************************************************/
//	作用:				STM32串口数据发送与接收
//	作者:				FITQY
//	时间:				2022.08.18
/********************************************************************/
#include "uartdata.h"
#include "system.h"

/********************************************************************

串口发送数据结构体初始化函数 Data_Init

	 各参数作用
	 DataTransmit *data: 							    选择要初始化的结构体 如 &data_uart2
	 head1:			        						帧头1 如 0xAA
	 head2:			        						帧头2 如 0xAA
	 length:			        						有效数据长度 
	 
********************************************************************/
void Data_Init_Transmit(DataTransmit *data, uint8_t head1, uint8_t head2, uint8_t length)
{
	
	data -> head1  = head1;
	data -> head2  = head2;
	data -> length = length;
	
	for(uint8_t i = 0; i < length; i++)
	{
		data -> data[i] = 0;
	}
	
	for(uint8_t j = 0; j < length + 4; j++)
	{
		data -> transmit_data[j] = 0;
	}
	
}

/********************************************************************

串口发送数据打包函数 Data_Pack

	 各参数作用
	 DataTransmit *data: 			带入结构体 如 &data_uart2
	 
********************************************************************/
void Data_Pack(DataTransmit *data)
{
	
	data -> transmit_data[0] = data -> head1;
	data -> transmit_data[1] = data -> head2;
	data -> transmit_data[2] = data -> length;
	
	for(uint8_t i = 0; i < data -> length; i++)
	{
		data -> transmit_data[3+i] = data -> data[i];
	}
	
	uint8_t sum = 0;
	
  for(uint8_t j = 0; j < data -> length + 3; j++)
	{
		sum += data -> transmit_data[j];
	}
	
  data -> transmit_data[data -> length + 3] = sum;
	
}

/********************************************************************

串口数据发送函数 Data_Transmit

	 各参数作用
	 UART_HandleTypeDef *huart:				选择通过哪一个串口发送 如 &huart2
	 DataTransmit *data: 	        		带入结构体 如 &data_uart2
	
	注意:此处用到了 STM32 HAL库 的函数 HAL_UART_Transmit 若非 HAL库 版本的代码 无法使用 
	 
********************************************************************/
void Data_Transmit(UART_HandleTypeDef *huart, DataTransmit *data)
{
	
	HAL_UART_Transmit(huart, data -> transmit_data, data -> transmit_data[2] + 4 , 0xFFFF);
	
}

/********************************************************************

串口数据打包发送函数 Data_Pack_Transmit

	 各参数作用
	 UART_HandleTypeDef *huart:				选择通过哪一个串口发送 如 &huart2
	 DataTransmit *data: 	        		带入结构体 如 &data_uart2
	 
********************************************************************/
void Data_Pack_Transmit(UART_HandleTypeDef *huart, DataTransmit *data)
{
	
	Data_Pack(data);				
  Data_Transmit(huart,data);			
	
}

// 至此串口数据发送函数定义结束 使用例如下
/*

DataTransmit data_uart2;						// 声明全局结构体 data_uart2 这个要放在 main 函数外面

Data_Init_Transmit(&data_uart2,0xAA,0xAA,1);	// main函数中在while(1)前 对结构体 data_uart2 进行初始化 这里的长度是有效数据长度 比如发送1个模式控制位 就带入 1

data_uart2.data[0] = 1;							// 模式控制位 为1 这段放的位置依据个人需求 需要更改 K210 或者 OpenMV 工作模式 则将 data_uart2.data[0] 改成需要的数即可 这里的 1 代表 0x01

while(1)										// 在while(1)中 或者依据个人需求 放在其他函数中
{
	Data_Pack_Transmit(&huart2,&data_uart2);	// 对数据进行打包发送
}

*/
// 至此串口数据发送函数使用例结束 接下来完成对数据接收函数定义

/********************************************************************

串口接收数据结构体初始化函数 Data_Init_Receive

	 各参数作用
	 DataReceive *data: 							    选择要初始化的结构体 如 &data_uart2_receive
	 head1:			        						帧头1 如 0xAA
	 head2:			        						帧头2 如 0xAA
	 
********************************************************************/
void Data_Init_Receive(DataReceive *data, uint8_t head1, uint8_t head2)
{
	
	data -> head1  = head1;
	data -> head2  = head2;
	data -> length = 0;
	data -> i      = 0;
	data -> cnt    = 0;
	data -> state  = 0;
	
	for(uint8_t j = 0; j < 50; j++)
	{
		data -> receive_data[j] = 0;
	}
	
}

/********************************************************************

串口接收1个数据函数 Buf_Receive

	 各参数作用
	 UART_HandleTypeDef *huart:			选择通过哪一个串口接收 如 &huart2
	 DataReceive *data: 				选择要带入的结构体 如 &data_uart2_receive
	 
********************************************************************/
uint8_t Buffer_Receive(UART_HandleTypeDef *huart, DataReceive *data)
{
	HAL_UART_Receive_IT(huart, &data -> data, 1);
	return data -> data;
}

/********************************************************************

串口接收数据函数 Data_Receive

	 各参数作用
	 DataReceive *data: 						选择要带入的结构体 如 &data_uart2_receive
	 buf:										串口接收到的数据
	 
********************************************************************/
void Data_Receive(DataReceive *data, uint8_t buf)
{
	
	if(data -> state == 0 && buf == data -> head1)
	{
		
		data -> state = 1;
		data -> receive_data[0] = buf;
		
	}
	else if(data -> state == 1 && buf == data -> head2)
	{
		
		data -> state = 2;
		data -> receive_data[1] = buf;
		
	}
	else if(data -> state == 2 && buf < 40)
	{
		
		data -> state = 3;
		data -> length = buf;
		data -> cnt = buf+4;
		data -> receive_data[2] = buf;
		
	}
	else if(data -> state == 3 && data -> length > 0)
	{
		
		data -> length = data -> length - 1;
		data -> receive_data[3 + data -> i] = buf;
		data -> i = data -> i + 1;
		if(data -> length == 0)
		{
			
			data -> state = 4;
			
		}
		
	}
	else if(data -> state == 4)
	{
		
		data -> receive_data[3 + data -> i] = buf;
		data -> state = 0;
		data -> i = 0;
		
	}
	else
	{
		
		data -> state = 0;
		data -> i = 0;
		
	}
	
}

// 至此串口数据接收函数定义结束 使用例如下
/*

DataReceive data_uart2_receive;										// 声明全局结构体 data_uart2_receive 这个要放在 main 函数外面

Data_Init_Receive(&data_uart2_receive,0xAA,0xAA);					// main的函数在while(1)前 对接收结构体进行初始化

Buffer_Receive(&huart2,&data_uart2_receive);						// main的函数在while(1)前 HAL库需要接收一次 使之可以进入中断回调

// 中断回调函数中调用
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{

	if( huart == &huart2 )											// 检测到串口中断2
	{

		Data_Receive(&data_uart2_receive,Buffer_Receive(&huart2,&data_uart2_receive));

	}

}

// 需注意 函数 HAL_UART_RxCpltCallback 是一个弱定义 可以在 main.c 的 USER CODE BEGIN 4 中进行重写

*/
// 至此串口数据接收函数使用例结束 接下来举例说明数据解析

/********************************************************************

串口接收数据结构体初始化函数 Target_Init

	 各参数作用
	 TargetAttribute *target: 	选择要初始化的结构体 如 &target_1
	 
********************************************************************/
void Target_Init(TargetAttribute *target)
{
	
	target -> x     = 0;
	target -> y     = 0;
	target -> color = 0;
	target -> shape = 0;
	target -> flag  = 0;
	
}

/********************************************************************

串口接收数据结构体初始化函数 Target_Init

   各参数作用
   DataReceive *data: 				选择要带入的结构体 如 &data_uart2_receive
   TargetAttribute *target: 		选择要带入的结构体 如 &target_1
	 
********************************************************************/
void Target_Parse(DataReceive *data,TargetAttribute *target)
{
	
	uint8_t sum = 0;
	uint8_t i   = 0;
	
	while(i < data -> cnt - 1)
	{
		
		sum = sum + data -> receive_data[i];
		i = i + 1;
		
	}
	
	if(sum == data -> receive_data[data -> cnt - 1])
	{
		
		target -> x     = data -> receive_data[3]*256 + data -> receive_data[4];
		target -> y     = data -> receive_data[5]*256 + data -> receive_data[6];
		target -> color = data -> receive_data[7];
		target -> shape = data -> receive_data[8];
		target -> flag  = data -> receive_data[9];
		
	}
	
}

// 至此数据解析函数定义结束 使用例如下
/*

TargetAttribute target_1;											// 声明全局结构体 target_1 这个要放在 main 函数外面

Target_Init(&target_1);												// main的函数在while(1)前 对解析结构体进行初始化

Target_Parse(&data_uart2_receive,&target_1);						// 可以放在while(1)中 也可自定义放在其他地方

*/
// 解析函数可根据自身需求自由定义 方法不唯一

uartdata.h

#ifndef __UARTDATA_H
#define __UARTDATA_H

#include "system.h"

typedef struct
{
	
	uint8_t	head1;							// 帧头1
	uint8_t	head2;							// 帧头2
	uint8_t length;							// 有效数据长度
	uint8_t data[40];						// 有效数据组成的数组
	uint8_t transmit_data[50];				// 实际发送的数组 附带上帧头1 帧头2 有效数据长度位 校验位
	
}DataTransmit;

void Data_Init_Transmit(DataTransmit *data, uint8_t head1, uint8_t head2, uint8_t length);
void Data_Pack(DataTransmit *data);
void Data_Transmit(UART_HandleTypeDef *huart, DataTransmit *data);
void Data_Pack_Transmit(UART_HandleTypeDef *huart, DataTransmit *data);

typedef struct
{
	
	uint8_t	head1;							// 帧头1
	uint8_t	head2;							// 帧头2
	uint8_t length;							// 有效数据长度
	uint8_t i;								// 有效数据下标
	uint8_t cnt;							// 总数据长度
	uint8_t state;							// 接收状态
	uint8_t receive_data[50];				// 实际接收的数组 附带上帧头1 帧头2 有效数据长度位 校验位
	uint8_t data;							// 接收数据
	
}DataReceive;

void Data_Init_Receive(DataReceive *data, uint8_t head1, uint8_t head2);
uint8_t Buffer_Receive(UART_HandleTypeDef *huart, DataReceive *data);
void Data_Receive(DataReceive *data, uint8_t buf);

typedef struct
{
	
	uint16_t	x;							// 目标x轴坐标
	uint16_t	y;							// 目标y轴坐标
	uint8_t color;							// 目标颜色标志位
	uint8_t shape;							// 目标形状标志位
	uint8_t flag;							// 目标标志位
	
}TargetAttribute;

void Target_Init(TargetAttribute *target);
void Target_Parse(DataReceive *data,TargetAttribute *target);

#endif

system.h

#ifndef __SYSTEM_H
#define __SYSTEM_H

#include "main.h"
#include "usart.h"
#include "gpio.h"

#include "uartdata.h"

typedef signed char             	int8_t;   
typedef short int               	int16_t;  
typedef int                     	int32_t; 

typedef unsigned char 				uint8_t;
typedef unsigned short int 			uint16_t;
typedef unsigned int            	uint32_t;

#endif

OpenMV完整源码

# 下次再写哦

转自:
https://blog.csdn.net/adas323/article/details/126257819