锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

基于Jetson Nano与STM32通信的颜色识别与伺服驱动器控制

时间:2022-09-10 20:00:00 100pin矩形连接器伺服电机专用圆形连接器8pin电子终端连接器d系列圆形连接器

基于Jetson Nano与STM32通信颜色识别和伺服驱动器控制

    • jetrson nano部分
      • 颜色识别
      • 串口通信
      • 数据传输
      • 完整代码
    • stm32 部分
      • 数据解读
      • 电机控制
      • 主函数
      • 电机加减速
      • 硬件图:
    • 总结

本文主要用于使用Jetson Nano通过颜色识别识别物体后,将目标中心点坐标与摄像头中心点坐标之间的误差传输到stm32开发板。由stm32判断数据进行一系列操作,并使用定时器产生PWM驱动伺服驱动器使摄像头中心面向目标中心。

jetrson nano部分

jetson nano主要负责图像识别和误差坐标(x轴和y轴的传输,我在这里方便实验opencv的HSV神经网络也可用于色域颜色识别。
所需的库包括

import cv2 import numpy as np import serial import struct,time import sys 

有些可能没用

颜色识别

cam= cv2.VideoCapture(0) #因为选定的颜色是红色,正好处于0与180连接处,所以需要2个HSV融合色域范围 l_b=np.array([0,130,105]) u_b=np.array([4,255,217])  l_b2=np.array([166,130,105]) u_b2=np.array([179,255,217])   ret, frame = cam.read()     frame = cv2.resize(frame, (width, height))               #resize     frame_=cv2.GaussianBlur(frame,(5,5),0)              #高斯滤波,适用于消除高斯噪声,广泛应用于图像处理的降噪过程。      hsv=cv2.cvtColor(frame,cv2.COLOR_BGR2HSV)           #转换色域
    FGmask=cv2.inRange(hsv,l_b,u_b)
    FGmask2=cv2.inRange(hsv,l_b2,u_b2)
    mask=cv2.add(FGmask,FGmask2)
    mask=cv2.erode(mask,None,iterations=2)              #cv2.erode()腐蚀:将前景物体变小,理解成将图像断开裂缝变大(在图片上画上黑色印记,印记越来越大)扩大黑色
    mask=cv2.dilate(mask,None,iterations=2)             #cv2.dilate()膨胀:将前景物体变大,理解成将图像断开裂缝变小(在图片上画上黑色印记,印记越来越小)缩小黑色
    mask=cv2.GaussianBlur(mask,(3,3),0)     
    # contours=sorted(contours,key=lambda x:cv2.contourArea(x),reverse=True)

    cnts=cv2.findContours(mask.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[-2]    #查找检测物体的轮廓,不能在源图像上直接修改
                                                                                        #contours, hierarchy = cv2.findContours(image,mode,method)
                                                                                        #image:输入图像
                                                                                        #mode:轮廓的模式。cv2.RETR_EXTERNAL只检测外轮廓;cv2.RETR_LIST检测的轮廓不建立等级关系;cv2.RETR_CCOMP建立两个等级的轮廓,上一层为外边界,内层为内孔的边界。如果内孔内还有连通物体,则这个物体的边界也在顶层;cv2.RETR_TREE建立一个等级树结构的轮廓。
                                                                                        #method:轮廓的近似方法。cv2.CHAIN_APPROX_NOME存储所有的轮廓点,相邻的两个点的像素位置差不超过1;cv2.CHAIN_APPROX_SIMPLE压缩水平方向、垂直方向、对角线方向的元素,只保留该方向的终点坐标,例如一个矩形轮廓只需要4个点来保存轮廓信息;cv2.CHAIN_APPROX_TC89_L1,cv2.CV_CHAIN_APPROX_TC89_KCOS
                                                                                        #contours:返回的轮廓
                                                                                        #hierarchy:每条轮廓对应的属性
                                                                                        #[-2]的作用是只返回轮廓,不返回其他的

    if len(cnts)>0:
        cnt = max (cnts,key=cv2.contourArea)                                            #按像素面积计算轮廓,进行排序,取最大的
        (color_x,color_y),color_radius=cv2.minEnclosingCircle(cnt)                      #寻找包裹轮廓的最小圆:1.轮廓上的点均在圆形空间内。2.没有面积更小的满足条件的圆。
                                                                                        #返回值:圆心,圆半径 

        if color_radius > 10:                                                           #如果半径大于10个像素
            # 将检测到的颜色标记出来
            cv2.circle(frame,(int(color_x),int(color_y)),int(color_radius),(255,0,255),2)   #在图像上画圆

串口通信

串口通信函数,主要参考:基于JETSON NANO的激光测距和色块识别综合代码(包括和STM32通信)连接的是stm32UART1
注意!!!使用前需要开启 ttyTHS1打开串口权限(jetson 系列好像每次开机都需要这样做),在终端使用sudo chmod 777 ‘/dev/tthTHS1’.如果想开机自启动打开权限请参考:linux systemctl命令添加开机启动脚本

class Comcontrol(serial.Serial):
    def __init__(self, port, baudrate, bytesize, stopbits, timeout, parity):
        super(Comcontrol, self).__init__()
        self.port = port
        self.baudrate = baudrate
        self.bytesize = bytesize
        self.stopbits = stopbits
        self.timeout = timeout
        self.parity = parity
        self.com = serial.Serial(port = self.port,
                                baudrate = self.baudrate,
                                bytesize = self.bytesize,
                                stopbits = self.stopbits,
                                timeout = self.timeout,
                                parity = self.parity)

def mpu_com_connect():
    mpucom = Comcontrol(port = '/dev/ttyTHS1',     
                        baudrate = 115200,
                        bytesize = 8,
                        stopbits = 1,
                        timeout = 0.8,
                        parity = 'N')
    if(mpucom.com.is_open):
        print("mpu connection success\r\n")
    return mpucom

数据传输

 			x_bias = int(color_x - width/2)
            y_bias = int(color_y - height/2)
            
            # print(int(x_bias),int(y_bias))
            # print('Y')
            mpucom.com.write('#'.encode()+str(int(x_bias)).encode()+'e'.encode())
            mpucom.com.write('$'.encode()+str(int(y_bias)).encode()+'e'.encode())
            print('Y'.encode()+str(int(y_bias)).encode()+'e'.encode())
            print('X'.encode()+str(int(x_bias)).encode()+'e'.encode())
            # print(len('Y'))
            # print(len(str(int(x_bias))))
            # print(len(str(int(y_bias))))

添加报头报尾,方便判断x轴y轴,也方便判断数据位数。
uart通信每次发送到格式是 起始位 + 8位数据位 +1位停止位
这其中的8位数据就是我们要发送的,发送的规格是 # 123 e其实是发送了三次 。
串口只能发送为str()格式的数据,同时,如果有汉字可以使用‘汉字’.encode('utf-8')

完整代码

import cv2
import numpy as np
import serial
import struct,time
import sys
print(cv2.__version__)

def nothing(x):
    pass

class Comcontrol(serial.Serial):
    def __init__(self, port, baudrate, bytesize, stopbits, timeout, parity):
        super(Comcontrol, self).__init__()
        self.port = port
        self.baudrate = baudrate
        self.bytesize = bytesize
        self.stopbits = stopbits
        self.timeout = timeout
        self.parity = parity
        self.com = serial.Serial(port = self.port,
                                baudrate = self.baudrate,
                                bytesize = self.bytesize,
                                stopbits = self.stopbits,
                                timeout = self.timeout,
                                parity = self.parity)

def mpu_com_connect():
    mpucom = Comcontrol(port = '/dev/ttyTHS1',                 # 串口
                        baudrate = 115200,                     #波特率 
                        bytesize = 8,                           #数据位
                        stopbits = 1,                           #停止位
                        timeout = 0.8,                          #间隔
                        parity = 'N')                           #校验位
    if(mpucom.com.is_open):
        print("mpu connection success\r\n")
    return mpucom


cam= cv2.VideoCapture(0)   #使用的是USB摄像头,如果使用SCI摄像头,请使用以下接口
#camSet='nvarguscamerasrc ! video/x-raw(memory:NVMM), width=3264, height=2464, format=NV12, framerate=21/1 ! nvvidconv flip-method='+str(flip)+' ! video/x-raw, width='+str(dispW)+', height='+str(dispH)+', format=BGRx ! videoconvert ! video/x-raw, format=BGR ! appsink'
#cam= cv2.VideoCapture(camSet)

width = 400
height = 400

l_b=np.array([0,130,105])
u_b=np.array([4,255,217])

l_b2=np.array([166,130,105])
u_b2=np.array([179,255,217])

mpucom = mpu_com_connect()

while 1:
    ret, frame = cam.read()
    frame = cv2.resize(frame, (width, height))               #resize
    frame_=cv2.GaussianBlur(frame,(5,5),0)              #高斯滤波,适用于消除高斯噪声,广泛应用于图像处理的减噪过程。 
    hsv=cv2.cvtColor(frame,cv2.COLOR_BGR2HSV)           #转换色域
    FGmask=cv2.inRange(hsv,l_b,u_b)
    FGmask2=cv2.inRange(hsv,l_b2,u_b2)
    mask=cv2.add(FGmask,FGmask2)
    mask=cv2.erode(mask,None,iterations=2)              #cv2.erode()腐蚀:将前景物体变小,理解成将图像断开裂缝变大(在图片上画上黑色印记,印记越来越大)扩大黑色
    mask=cv2.dilate(mask,None,iterations=2)             #cv2.dilate()膨胀:将前景物体变大,理解成将图像断开裂缝变小(在图片上画上黑色印记,印记越来越小)缩小黑色
    mask=cv2.GaussianBlur(mask,(3,3),0)     
    # contours=sorted(contours,key=lambda x:cv2.contourArea(x),reverse=True)

    cnts=cv2.findContours(mask.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[-2]    #查找检测物体的轮廓,不能在源图像上直接修改
                                                                                        #contours, hierarchy = cv2.findContours(image,mode,method)
                                                                                        #image:输入图像
                                                                                        #mode:轮廓的模式。cv2.RETR_EXTERNAL只检测外轮廓;cv2.RETR_LIST检测的轮廓不建立等级关系;cv2.RETR_CCOMP建立两个等级的轮廓,上一层为外边界,内层为内孔的边界。如果内孔内还有连通物体,则这个物体的边界也在顶层;cv2.RETR_TREE建立一个等级树结构的轮廓。
                                                                                        #method:轮廓的近似方法。cv2.CHAIN_APPROX_NOME存储所有的轮廓点,相邻的两个点的像素位置差不超过1;cv2.CHAIN_APPROX_SIMPLE压缩水平方向、垂直方向、对角线方向的元素,只保留该方向的终点坐标,例如一个矩形轮廓只需要4个点来保存轮廓信息;cv2.CHAIN_APPROX_TC89_L1,cv2.CV_CHAIN_APPROX_TC89_KCOS
                                                                                        #contours:返回的轮廓
                                                                                        #hierarchy:每条轮廓对应的属性
                                                                                        #[-2]的作用是只返回轮廓,不返回其他的

    if len(cnts)>0:
        cnt = max (cnts,key=cv2.contourArea)                                            #按像素面积计算轮廓,进行排序,取最大的
        (color_x,color_y),color_radius=cv2.minEnclosingCircle(cnt)                      #寻找包裹轮廓的最小圆:1.轮廓上的点均在圆形空间内。2.没有面积更小的满足条件的圆。
                                                                                        #返回值:圆心,圆半径 

        if color_radius > 10:                                                           #如果半径大于10个像素
            # 将检测到的颜色标记出来
            cv2.circle(frame,(int(color_x),int(color_y)),int(color_radius),(255,0,255),2)   #在图像上画圆
            x_bias = int(color_x - width/2)
            y_bias = int(color_y - height/2)
            
            # print(int(x_bias),int(y_bias))
            # print('Y')
            mpucom.com.write('#'.encode()+str(int(x_bias)).encode()+'e'.encode())
            mpucom.com.write('$'.encode()+str(int(y_bias)).encode()+'e'.encode())
            print('Y'.encode()+str(int(y_bias)).encode()+'e'.encode())
            print('X'.encode()+str(int(x_bias)).encode()+'e'.encode())
            # print(len('Y'))
            # print(len(str(int(x_bias))))
            # print(len(str(int(y_bias))))
    else:   
        print('N')
        mpucom.com.write('N'.encode())
             
    if cv2.waitKey(1) == ord('q'):
        break
cam.release()
cv2.destroyAllWindows()


stm32 部分

stm32部分主要分为数据解读和伺服驱动器控制部分,使用到的常规led beep代码就不放了,可以自行设计。
stm32不仅要产生PWM波控制伺服驱动器,更要考虑实际情况进行软件限位,即精确获得产出的PWM数。
这里使用的伺服驱动器是
台达的A2,使用差分信号驱动模式,实测stm32的3.3V电压可以驱动。
在这里插入图片描述

数据解读

stm32f10x_it.c

//中断函数变量
static u8 i=0;									//i为数组接收计数
static u8 j=0;									//j为取数据计数
char uctemp[8] = { 
        0};						//uctemp为接收数组,因为uart接收只能一个字节一个字节的接收
char x_temp[8] = { 
        0};						//x_temp为x轴偏移量存储数组
char y_temp[8] = { 
        0};						//y_temp为y轴偏移量存储数组
extern volatile int x_bais;					//x为x轴偏移量
extern volatile int y_bais;					//y为y轴偏移量
extern volatile int target;					//y为y轴偏移量
extern volatile int receive;				//数据接收flag

extern volatile int Rotation_angle;
extern volatile int Limit_angle;

首先判断是否存在目标,如果不存在则不启动电机,同时亮红灯表示;
检测到目标则亮绿灯,同时可以启动电机,并判断出x轴y轴偏移误差,将char转化为int。

atoi(‘124e’)= 124;最后的‘e’会被忽略掉。
void DEBUG_USART_IRQHandler(void)										//每次中断都会调用中断函数
{ 
        
	u8 k=0;																//k为循环计数
	if(USART_GetITStatus(DEBUG_USARTx,USART_IT_RXNE)!=RESET)			//USART_IT_RXNE为接收中断标志位
	{ 
        	
		receive = 3;												//是否接收到传输的数据
		uctemp[i] = USART_ReceiveData(DEBUG_USARTx);								//一位一位的接收
		j=i;
		if((uctemp[0] != '#'&&uctemp[0] != '$')||uctemp[i]=='e') i=0;		//判断数据报头报尾,当数据接收完毕的时i归0
		
		
		if(uctemp[0] == 'N')											//判断是否检测到目标,用target和LED作为检测结果展示 
		{ 
        
			target=0;
			LED2_OFF;
			LED1_ON;
			x_bais = 0;
			y_bais = 0;
		}
		else if((uctemp[0] == '#') &&(uctemp[j] == 'e'))				//判断X偏移量
		{ 
        
			target=1;
			LED1_OFF;
			LED2_ON;		
			for(k=0;k<j;k++){ 
        
			x_temp[k] = uctemp[k+1];
			}
			x_bais = atoi(x_temp);										//char转int

		}
		else if((uctemp[0] == '$') &&(uctemp[j] == 'e'))				//判断y偏移量
		{ 
        
			target=1;
			LED1_OFF;
			LED2_ON;
			for(k=0;k<j;k++){ 
        
			y_temp[k] = uctemp[k+1];
			}
			y_bais = atoi(y_temp);

		}else{ 
        i++;} 

	}
}

电机控制

驱动伺服电机需要使用pwm波,我采用通用定时器产生PWM波,同时为了精确限位,使用另一个从定时器统计产生的脉冲数量。
因为伺服驱动器是通过脉冲数量来驱动电机运行的,本文设置的3600脉冲转一转,则转一度需要10脉冲

timer.c

#include "stm32f10x.h"
#include "timer.h"
 
/*************** 主定时器配置函数 period:PWM周期 prescaler:预分频系数 pulse:占空比控制变量 也就是PWM有效电平的宽度 PWM输出IO为GPIOC_7 完全重映射至 TIM3_CH2 ***************/
 
void Master_TIM(u16 period,u16 prescaler,u16 pulse)
{ 
               
	GPIO_InitTypeDef GPIO_InitStructure;
	
	// 输出比较通道2 GPIO 初始化
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
  GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_7;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);

/*--------------------时基结构体初始化-------------------------*/
	// 配置周期,这里配置为100K
	
  TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
	// 自动重装载寄存器的值,累计TIM_Period+1个频率后产生一个更新或者中断
	TIM_TimeBaseStructure.TIM_Period=period;	
	// 驱动CNT计数器的时钟 = Fck_int/(psc+1)
	TIM_TimeBaseStructure.TIM_Prescaler= prescaler;	
	// 时钟分频因子 ,配置死区时间时需要用到
	TIM_TimeBaseStructure元器件数据手册IC替代型号,打造电子元器件IC百科大全!
          

相关文章