1、器件选择 小车构成名称上位机Jetson nano下位机STM32F103 STM32F407舵机DS3120数字舵机底盘四麦克纳姆轮移动底盘电机1:30减速比 带编码器减速电机电机驱动双路直流电机驱动摄像头USB工业摄像头陀螺仪JY901S九轴陀螺仪 HWT101CT航向角陀螺仪语音模块SYN-6288语音模块扫码模块Scanner V3.0嵌入式扫码模块 2、总体创作历程

既然是小车 那自然是从底盘开始干起 一开始我们采用的是STM32F103最小系统板作为底盘控制板 然后在实验室找了一块电源板 准备开始测试电机驱动。我们使用的电机是1:30减速比减速电机 电机驱动是韦恩斯塔克科技的双路直流电机驱动。
于是有了下面这张我们小车的雏形
在这里插入图片描述
浅浅地落地试了一下 效果还不错。测试完毕后 我开始着手设计我的上位机UI界面 并编写我的上位机程序。我采用Jetson nano作为上位机 与单片机进行通信 控制单片机完成各种动作。测试上下位机通信正常后 我再加入了颜色识别和扫码模块的程序 而后搭载于车上 进行了测试。可能由于配重问题或场地不平稳问题 导致小车无法正常直行和平移 东倒西歪 于是我加上了PID算法 使得小车能够稳定行驶 这个时候整块STM32F103系统板引脚基本都被插满了 看着也很乱 于是我换成了STM32F407作为底盘控制板使用。

测试后发现小车虽然能够正常直行 但是平移的时候还是会有些许问题。经过反复测试后 发现是因场地不平稳导致麦轮悬空 从而走歪 于是我加了陀螺仪对小车进行矫正 一开始使用的是九轴陀螺仪JY901S 但是实际上路时发现效果不是很好 会有-3°至3°的误差。而且最难受的是 当你开始怀疑陀螺仪不精准是不是受到了磁场影响 于是你去掉了可能存在的磁场干扰。但是当你做完这些后准备重新校准时 你就麻烦大了 你需要将整辆车抬起来 绕两个方向旋转360°。我的这辆小车还是挺重的 转个一两次直接裂开。于是后面换了HWT101CT单轴陀螺仪解决了这个问题。后面又继续调试了一些视觉方面的程序 修改了一些bug后 也算是基本完成了。

到这里你可能会觉得总体思路挺顺畅的 其实在实现的过程中还有很多奇奇怪怪的问题和一些值得一提的坑 我会在下文一一道来。

二、SMT32下位机控制程序和逻辑分析

我的STM32下位机控制采用的是STM32F407ZGT6最小系统板 基于HAL库编写了减速电机、编码器、陀螺仪、语音模块灯模块的驱动程序 以及PID速度环的闭环系统。

1、电机驱动

首先放上一些简单的电机驱动程序 包括电机PWM设置和电机的使能设置。

/*---------------------------- 设置电机PWM ----------------------------*/
void Motor1_pwm_Set(int motor1_n) // 电机1设置PWM
 HAL_TIM_PWM_Stop( htim1, TIM_CHANNEL_1);
 Motor_PWM_sConfigOC.Pulse motor1_n;
 HAL_TIM_PWM_ConfigChannel( htim1, Motor_PWM_sConfigOC, TIM_CHANNEL_1);
 HAL_TIM_PWM_Start( htim1, TIM_CHANNEL_1);
 //Motor1_pwm_num (motor1_n / (float)1000)*100;
/*---------------------------- 电机使能 ----------------------------*/
void Motor1_up_enable(void) // 电机1正转使能
 HAL_GPIO_WritePin(Motor_INA_GPIOx, Motor1_IN_GPIO, GPIO_PIN_RESET);
 HAL_GPIO_WritePin(Motor_INB_GPIOx, Motor1_IN_GPIO, GPIO_PIN_SET);
void Motor1_lost_enable(void) // 电机1反转使能
 HAL_GPIO_WritePin(Motor_INA_GPIOx, Motor1_IN_GPIO, GPIO_PIN_SET);
 HAL_GPIO_WritePin(Motor_INB_GPIOx, Motor1_IN_GPIO, GPIO_PIN_RESET);
void Motor1_disable(void) // 电机1失能
 HAL_GPIO_WritePin(Motor_INA_GPIOx, Motor1_IN_GPIO, GPIO_PIN_RESET);
 HAL_GPIO_WritePin(Motor_INB_GPIOx, Motor1_IN_GPIO, GPIO_PIN_RESET);
2、速度环PID

如上总体创作历程所述 为了解决配送小车行走偏移的问题 我先采用的是增量式PID算法 利用编码器实现配送小车4轮的速度闭环。

1 、编码器

我们选购的编码器为AB双向增量式磁性霍尔编码器 其会将电机运转时的位移信息转变成一段连续的脉冲信号 利用脉冲个数可算得位移量。
在开始上手之前需要注意的事项

1、首先第一步还是要把传感器搞定 确保单位时间内获取的编码器脉冲值是正确的。
2、我们购买的编码器是霍尔编码器 即为磁电转换的编码器 而且我在装车前就已经将编码器外壳拆卸掉了 因此需要确保编码器不会受到外界磁场过强的干扰。
3、给编码器供电的电源板切记要和单片机共地 我就是因为没有和单片机共地导致信号不稳定 耗费了很长时间。
4、注意好CubeMx配置的几倍频。

下面是我做的一些编码器的笔记 记录下以后方便查阅 就是字有点小丑
在这里插入图片描述
如上所示 假如我50ms获取一次编码器的当前总值 那么用当前编码器的值除以编码器单圈脉冲数 就能得到50ms内电机转了几圈。
编码器单圈脉冲数的值 倍频数 × 电机线数 × 减速比
编码器值分析程序如下

float girth 77*3.1415f; // 轮周长(mm)
float encoder_mc 4*11*30; // 编码器单圈脉冲数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) // 定时器中断回调函数
 if (htim htim7) //50ms调用一次
 enc_1 -(int32_t)__HAL_TIM_GET_COUNTER( htim3); // 获取编码器1值(enc_1/50ms)
 __HAL_TIM_SetCounter( htim3, 0); // mcu编码器接口置0
 Motor1_now_Speed (((float)enc_1 / encoder_mc)*girth/50)*1000; //计算轮1当前速度(mm/s)

(enc_1 / encoder_mc)求出了50ms内的圈数 乘以轮周长girth则得到50ms内走的位移 再除以50乘以1000则得到一秒单位时间内的位移。

2 、增量式PID算法

增量式PID公式如下图所示
在这里插入图片描述

关于PID算法我是在b站up主421施公队的从不懂到会用 PID从理论到实践视频学的 视频里讲的PID算法个人认为通俗易懂 是个相当不错的视频。
关于增量式PID算法程序如下所示

int threshold 800; // 我电机定时器的ARR设置为1000 所以我这里限幅在-800至800的范围内
Error target_Speed - now_Speed; // 用目标速度减去当前速度得到当前的误差值
P_Error Error - Last_Error;
I_Error Error;
D_Error Error - 2*Last_Error Last_Last_Error;
add KP * P_Error KI * I_Error KD * D_Error; // 对应上图公式
PWM add; // 增加正数或负数的PID输出
Last_Last_Error Last_Error; // 上一个误差在下一次变为上上个误差
Last_Error Error; // 当前误差在下一次变为上一个误差
if(PWM 800) PWM 800; // 限幅
else if(PWM -800) PWM -800; // 限幅

如上程序所示 限幅为800 那么为什么会有一个-800呢 其实这也是PID的特性之一 后面程序当判断为负值时反向旋转轮子 并把值置为正数。如下程序所示(以电机1为例)

if(motor1_pwm 0){Motor1_up_enable();} // 判断PWM为正数 正转且可直接赋值
else if(motor1_pwm 0){Motor1_lost_enable();motor1_pwm -motor1_pwm;} // 判断PWM为负数 反转且将PWM正负翻转
Motor1_pwm_Set(motor1_pwm); // 电机1赋pwm
3 、野火上位机PID参数整定

关于最重要的PID参数的整定 我请教了师兄后了解到了野火的上位机和正点原子的无线调试器 于是我参考野火手册上手写了一套野火上位机的远程调试程序 但是不知道什么原因 无线调试器一直无法实现电脑端向单片机端发送数据 。因此无线调试时只能看到波形 然后改代码远程烧录。
具体的上位机介绍和部分例程在野火的上位机文档里也有 有需要的也可以去参考野火上位机的文档。如下为野火的上位机程序

/* ↓上下位机通信↓ */
 * brief 设置pid
void set_p_i_d(float p, float i, float d)
 KP p; // P
 KI i; // I
 KD d; // D
 * brief 计算校验和
 * param ptr 需要计算的数据
 * param len 需要计算的长度
 * retval 校验和
uint8_t check_sum(uint8_t init, uint8_t *ptr, uint8_t len)
 uint8_t sum init;
 while(len--)
 sum *ptr;
 ptr ;
 return sum;
 * brief 设置上位机的值
 * param cmd 命令
 * param ch: 曲线通道
 * param data 参数指针
 * param num 参数个数
 * retval 无
void set_computer_value(uint8_t cmd, uint8_t ch, void *data, uint8_t num)
 uint8_t sum 0; // 校验和
 num * 4; // 一个参数4个字节
 static packet_head_t set_packet;
 set_packet.head FRAME_HEADER; // 包头 0x59485A53
 set_packet.len 0x0B num; // 包头
 set_packet.ch ch; // 设置通道
 set_packet.cmd cmd; // 设置命令
 sum check_sum(0, (uint8_t *) set_packet, sizeof(set_packet)); // 计算包头校验和
 sum check_sum(sum, (uint8_t *)data, num); // 计算参数校验和
 HAL_UART_Transmit( huart1, (uint8_t *) set_packet, sizeof(set_packet), 0xFFFFF); // 发送数据头
 HAL_UART_Transmit( huart1, (uint8_t *)data, num, 0xFFFFF); // 发送参数
 HAL_UART_Transmit( huart1, (uint8_t *) sum, sizeof(sum), 0xFFFFF); // 发送校验和
* brief 读取上位机的值
 * param cmd 命令
 * param ch: 曲线通道
 * param data 参数指针
 * retval 无
void Read_Computer(UART_HandleTypeDef *husart)
 packet_head_t packet;
 packet.cmd rx_buffer[CMD_INDEX_VAL];
 packet.len COMPOUND_32BIT( rx_buffer[LEN_INDEX_VAL]); 
 packet.head COMPOUND_32BIT( rx_buffer[HEAD_INDEX_VAL]); 
 if (packet.head FRAME_HEADER) 
 switch(packet.cmd)
 case SET_P_I_D_CMD: // 设置PID值
 uint32_t temp0 COMPOUND_32BIT( rx_buffer[13]);
 uint32_t temp1 COMPOUND_32BIT( rx_buffer[17]);
 uint32_t temp2 COMPOUND_32BIT( rx_buffer[21]);
 float p_temp, i_temp, d_temp;
 p_temp *(float *) temp0;
 i_temp *(float *) temp1;
 d_temp *(float *) temp2;
 set_p_i_d(p_temp, i_temp, d_temp); // 设置P I D
 break;
 case SET_TARGET_CMD:
 // 设置各电机期待值
 break;
 case START_CMD: // 开始按键
 car_forward();
 //car_left();
 //car_right();
 run_ornot 1;
 break;
 case STOP_CMD: // 停止按键
 run_ornot 0;
 Motor1_disable();
 Motor2_disable();
 Motor3_disable();
 Motor4_disable();
 Motor1.ESC_Output_PWM 0;
 Motor1.Last_Error 0;
 Motor1.Last_Last_Error 0;
 Motor2.ESC_Output_PWM 0;
 Motor2.Last_Error 0;
 Motor2.Last_Last_Error 0;
 Motor3.ESC_Output_PWM 0;
 Motor3.Last_Error 0;
 Motor3.Last_Last_Error 0;
 Motor4.ESC_Output_PWM 0;
 Motor4.Last_Error 0;
 Motor4.Last_Last_Error 0; 
 break;
 case RESET_CMD: // 复位单片机
 // HAL_NVIC_SystemReset(); 
 break;

然后只需在获取编码器后调用set_computer_value发送当前速度值 和在串口接收的回调函数中添加Read_Computer函数获取上位机指令就ok了。

3、HWT101CT单轴陀螺仪

为了解决麦轮悬空而引起的偏移问题 我决定加上一个陀螺仪来矫正。
值得注意的点是 一开始我们选用了九轴陀螺仪JY901S 但是其实这类型的陀螺仪并不适用于我的这辆配送小车。首先我的需求只有单纯的Z轴上的角度 其次这款陀螺仪要矫正起来非常麻烦 而且容易受磁场干扰 车上的空间非常有限 磁场强的东西又非常多 比如Jetson nano和电机 还有个磁场最猛的扬声器在车上。后面选用了HWT101CT单轴陀螺仪解决了这一问题 首先这块陀螺仪可以满足我单Z轴上的需求 其次它有一个较好的外壳 包括线也是有比较好的保护 而且它受磁场干扰后 只要离开磁场就能自动回到正常状态 不用我搬着整台车翻来覆去地矫正。

链接为陀螺仪说明书 HWT101CT通信协议
在这里插入图片描述
在这里插入图片描述从0x52开始 以0为初始值算起的16位和17位为角度输出的低8位和高8位。如下图所示
在这里插入图片描述

陀螺仪角度获取程序如下所示

// 陀螺仪串口接收
uint8_t value_IMU; // 串口输入值
uint8_t getBuffer[22]; // 存放数据的数组
uint8_t Enter[] \r\n ;
float IMU_DATA; // 角度值
uint8_t IMU_DATAL; // 数据位低8位
uint8_t IMU_DATAH; // 数据位高8位
volatile float IMU_Z 0.00f;
volatile float IMU_init_data 0.00f; // 陀螺仪初始角度
volatile char first_flag 0; // 判断第一次录入陀螺仪角度
uint8_t countOfGetBuffer 0; // 位数累计
char flag 0; // 判断接收开始
char now_uart_flag 0; // 当前输入值
char last_uart_flag 0; // 上一个输入值
void USART3_IRQHandler(void)
 /* USER CODE BEGIN USART3_IRQn 0 */
 /* USER CODE END USART3_IRQn 0 */
 HAL_UART_IRQHandler( huart3);
 /* USER CODE BEGIN USART3_IRQn 1 */
 last_uart_flag now_uart_flag; // 上一次的串口输入值
 now_uart_flag value_IMU; // 当前的串口输入值
 if(last_uart_flag 0x55 now_uart_flag 0x52) // 协议判断输入角度值
 //printf( IMU\r\n 
 flag 1; // 开启接收程序
 //printf( %c , value_IMU);
 if(flag 1)
 getBuffer[countOfGetBuffer ] value_IMU; 
 if(countOfGetBuffer 18) // 位长
 flag 0;
 //while(HAL_UART_Transmit( huart3, (uint8_t*)getBuffer, countOfGetBuffer, 5000)! HAL_OK);
 //while(HAL_UART_Transmit( huart3, (uint8_t*)Enter, 4, 5000)! HAL_OK);
 IMU_DATAL getBuffer[16];
 IMU_DATAH getBuffer[17];
 IMU_DATA (((short)IMU_DATAH 8) | IMU_DATAL); // 高8位低8位
 IMU_Z ((float)IMU_DATA/32768)*180-180; // 把0°至360°范围改为-180°至180°
 if(first_flag 0) // 单片机复位第一次获取陀螺仪值设置为初始正向
 IMU_init_data IMU_Z;
 first_flag 1;
 printf( Start:IMU_init_data:%.2f\r\n , IMU_init_data);
 //printf( IMU_init_data:%.2f\r\n , IMU_init_data);
 memset(getBuffer, 0, countOfGetBuffer); // 清空数组
 countOfGetBuffer 0; // 位数清零
 HAL_UART_Receive_IT( huart3, (uint8_t *) value_IMU,1);
 /* USER CODE END USART3_IRQn 1 */
4、SYN-6288语音模块

语音模块一样也是用串口通信 程序如下 商家提供的库函数的协议程序 不过是寄存器操作 所以影响不大

/*---------------------- 语音模块串口播放 ----------------------*/
void USART2_SendData(u8 data)
 while((USART2- SR 0X40) 0);
 USART2- DR data;
void USART2_SendString(u8 *DAT, u8 len)
 u8 i;
 for(i 0; i len; i )
 USART2_SendData(*DAT );
/*---------------------- 语音播报API定义 ----------------------*/
//Music:选择背景音乐 0:无背景音乐 1~15:选择背景音乐
//v[0~16]:朗读音量
//m[0~16]:背景音乐音量
//t[0~5]:语速
void SYN_FrameInfo(u8 Music, u8 *HZdata)
 /****************需要发送的文本**********************************/
 unsigned char Frame_Info[50];
 unsigned char HZ_Length;
 unsigned char ecc 0; //定义校验字节
 unsigned int i 0;
 HZ_Length strlen((char*)HZdata); //需要发送的文本长度
 /*****************帧固定配置信息**************************************/
 Frame_Info[0] 0xFD ; //构造帧头FD
 Frame_Info[1] 0x00 ; //构造数据区长度的高字节
 Frame_Info[2] HZ_Length 3; //构造数据区长度的低字节
 Frame_Info[3] 0x01 ; //构造命令字:合成播放命令
 Frame_Info[4] 0x01 | Music 4 ; //构造命令参数 背景音乐设定
 /*******************校验码计算***************************************/
 for(i 0; i 5; i )
 ecc ecc ^ (Frame_Info[i]);
 for(i 0; i HZ_Length; i )
 ecc ecc ^ (HZdata[i]);
 memcpy( Frame_Info[5], HZdata, HZ_Length);
 Frame_Info[5 HZ_Length] ecc;
 USART2_SendString(Frame_Info, 5 HZ_Length 1);
void YS_SYN_Set(u8 *Info_data)
 u8 Com_Len;
 Com_Len strlen((char*)Info_data);
 USART2_SendString(Info_data, Com_Len);

只需要简单的调用即可 如下所示

void speaker0(void)
 SYN_FrameInfo(1,(u8 *) [v6][m6][t5]¸高德地图持续为您导航 );
 HAL_Delay(3000);
5、舵机

本俩配送小车共搭载了3个舵机 一个用于摄像头的抬头低头 两个用于打开和关闭仓门。
首先要将定时器周期配置为20ms 然后给出的PWM高电平应为0.5ms至2.5ms 即PWM占空比为2.5%至12.5%。我的ARR值设置为1000 因此修改范围为25~125。
舵机PWM占空比设置程序如下所示

/*---------------------- 舵机PWM设置 ----------------------*/
// PWM占空比设置范围 25 ~ 125
void Servo1_pwm_Set(int Servo1_n)
 HAL_TIM_PWM_Stop( htim2, TIM_CHANNEL_1);
 Servo_PWM_sConfigOC.Pulse Servo1_n;
 HAL_TIM_PWM_ConfigChannel( htim2, Servo_PWM_sConfigOC, TIM_CHANNEL_1);
 HAL_TIM_PWM_Start( htim2, TIM_CHANNEL_1);
三、Jetson nano上位机程序和可视化界面

对于Jetson nano上位机程序的编写 我使用的是Python语言 主要功能视觉用的是OpenCV 界面设计用的是PyQt5。先从我使用Jetson nano编写程序时遇到的问题介绍起吧。

0、遇到的问题和解决方案 1 、Serial

在安装python的serial库后遇过一个问题 在引用serial 即import serial时出现了报错

AttributeError: module serial has no attribute Serial 

解决方案
一开始只使用了pip3 install serial安装serial库 少安装了一个pyserial。
正确的安装方法

pip3 install serial
pip3 install pyserial
2 、端口号问题

如果按照nano系统默认的端口配对 当接入多个端口时 拔插一个端口后名字就会改变 非常麻烦 需要一直改程序 或者不拔插端口 很不现实。
于是我决定定死端口 给要用的端口配对好名称 对应摄像头ID配对好摄像头接口名称 只要除摄像头外的接口插在对应的端口上就可以防止以上问题发生。
因此 解决方案
1 、打开终端

udevadm info /dev/videoxx(/dev/ttyUSBxx) # 查看当前端口所在接口的信息
cd /etc/udev/rules.d
sudo gedit serial.rules

进入文档 以下为我的配置案例 可参考使用

ACTION add ,KERNEL video* ,ATTRS{idVendor} 0bc8 , ATTRS{idProduct} 5880 , MODE: 0777 , SYMLINK car_video 
ACTION add ,KERNELS 1-2.1.1:1.0 ,SUBSYSTEMS usb ,MODE: 0777 ,SYMLINK car_scanner 
ACTION add ,KERNELS 1-2.2:1.0 ,SUBSYSTEMS usb ,MODE: 0777 ,SYMLINK car_mcu 
ACTION add ,KERNELS 1-2.4:1.0 ,SUBSYSTEMS usb ,MODE: 0777 ,SYMLINK car_gyro 

退出后输入以下命令完成配置。

sudo /etc/init.d/udev restart

重新拔插一次端口就ok了。

3 、调节摄像头曝光度问题

一开始我在windows使用opencv能够正常切换摄像头自动和手动曝光 但是来到nano的ubuntu系统上就不能用了 也可能是opencv的版本问题 后面我解决的方法是在终端上对摄像头模式进行调节。
首先需要安装pexpect库

pip3 install pexpect

在程序里调用如下

# 自动调节
child pexpect.spawn( v4l2-ctl -d /dev/car_video -c exposure_auto 3 )
time.sleep(1)
# 手动调节
child pexpect.spawn( v4l2-ctl -d /dev/car_video -c exposure_auto 1 )
time.sleep(1)
child pexpect.spawn( v4l2-ctl -d /dev/car_video -c exposure_absolute 50 )
time.sleep(1)

v4l2-ctl -d /dev/car_video -c exposure_auto xx为终端命令。
exposure_auto 3为自动调节 1为手动调节 exposure_absolute 50指曝光值为50。
以下为摄像头改变为手动曝光模式

def video_modeltoled(self):
 self.video.release()
 child pexpect.spawn( v4l2-ctl -d /dev/car_video -c exposure_auto 1 )
 time.sleep(1)
 child pexpect.spawn( v4l2-ctl -d /dev/car_video -c exposure_absolute 1 )
 time.sleep(1)
 while True:
 self.video cv2.VideoCapture( /dev/car_video ) # 定义识别红绿灯摄像头对象
 if self.video.isOpened() is True:
 print( 摄像头已打开。 )
 break
 elif self.video.isOpened() is False:
 print( 未接入摄像头。 )
1、扫码模块

首先需要配置好扫码模块的功能 配置完成后 则开始编写扫码模块调用程序。
如下所示 我的扫码模块是串口通信的 因此在运行主程序前先要判断扫码模块是否连接成功。

while True: # 判断串口是否正常连接 且防止报错
 try:
 self.scanner_ser serial.Serial( /dev/car_scanner , 115200, timeout 10) # 定义串口对象
 if self.scanner_ser.isOpen(): # 判断串口是否成功打开
 print( 扫码模块连接成功。 )
 break
 else:
 print( 扫码模块连接失败。 )
 except:
 print( 扫码模块未接入 或 扫码模块串口端口号错误。 )
 pass

进入while后将判断扫码模块是否连接成功 如果连接成功则退出while 否则将不停地循环while 直至确认连接成功为止。当终端打印出 扫码模块连接失败。扫码模块未接入 或 扫码模块串口端口号错误。 时 试着拔插扫码模块 即可成功连接 程序退出while 正常运行。
而后开始扫码工作

print( 进入扫码模式。 )
self.textEdit.setPlainText( 进入扫码模式。 )
while True: # 等待扫描二维码
 ret, img self.video.read() # 使用摄像头获取图像
 scanner_data self.scanner_run() # 运行扫码模块信息获取函数
 if scanner_data b 11 :
 print( 二维码信息为 11 。 )
 self.textEdit.append( 二维码信息为 11 。 )
 self.QR_label.setText( 11 )
 self.mcu_ser.write( $a .encode( utf-8 ))
 self.end_num 1
 break
 elif scanner_data b 12 :
 print( 二维码信息为 12 。 )
 self.textEdit.append( 二维码信息为 12 。 )
 self.QR_label.setText( 12 )
 self.mcu_ser.write( $a .encode( utf-8 ))
 self.end_num 2
 break
 elif scanner_data b 21 :
 print( 二维码信息为 21 。 )
 self.textEdit.append( 二维码信息为 21 。 )
 self.QR_label.setText( 21 )
 self.mcu_ser.write( $b .encode( utf-8 ))
 self.end_num 1
 break
 elif scanner_data b 22 :
 print( 二维码信息为 22 。 )
 self.textEdit.append( 二维码信息为 22 。 )
 self.QR_label.setText( 22 )
 self.mcu_ser.write( $b .encode( utf-8 ))
 self.end_num 2
 break
self.textEdit.append( 打开Qt界面。 )

该程序为主程序中的一部分 当中有些是和扫码模块无关的 在这里稍微解释一下

关于self.scanner_run()函数下文会挂上程序。self.textEdit和self.QR_label都为配送小车主界面的控件 分别用于显示终端打印信息和二维码信息。self.mcu_ser则为单片机串口对象 此处发送$a为打开1号仓门 $b为打开2号仓门。self.end_num定义目的地变量。使用ret, img self.video.read()目的是确保摄像头图像获取通道不会一直堵塞着 导致后续获取的图像有所延时。当然这里也可以在前面使用self.video.release()直接关闭摄像头 也可防止出现这种问题 后面完成扫码后再次打开摄像头即可。因为此处资源占用也不多 不会导致程序运行过慢 所以我也是直接调取摄像头来解决该问题。

scanner_run程序如下所示

def scanner_run(self): # 扫码模式
 scanner_data self.scanner_ser.read(4)
 scanner_data scanner_data.splitlines()
 if scanner_data:
 scanner_data scanner_data[0]
 return scanner_data
2、可视化界面 1 、视觉调试界面

赛场调试时间紧迫 如果视觉方面出现什么错误 每次都要重新跑一遍才能发现错误出在哪里那将浪费很多的时间 因此我编写了一个上位机视觉调试qt界面 如下所示。
在这里插入图片描述
视觉调试界面功能

右侧第二行3个按键分别为3种识别或者说摄像头的模式 自动曝光则是摄像头切换自动曝光模式 手动曝光则是人为调节曝光度 颜色识别模式则是识别色块或是红绿灯的模式。右侧第三行可实现在手动曝光模式下切换曝光值 输入曝光值后按确认即可。右侧颜色区块选择是我在写程序的时候默认保存了一些阈值范围 方便快速切换到某个区域 再进行微调。下面6行拖动条则是调节HSV阈值的 H范围为0-180 S范围为0-255 V范围为0-255。

在这里插入图片描述
左边为摄像头拍摄的图像 右边为颜色识别后的图像。

2 、配送小车主界面

如下图所示 当扫描完二维码 并获得下位机反馈回来的出发信号时 上位机打开此配送小车主界面。
在这里插入图片描述
配送小车主界面功能

界面左上角图像为小车的颜色识别图像 在其右侧为摄像头拍摄后进行一定比例缩小的原图像。这两张图像伴随小车行进实时变换 方便在运行中和编写逻辑程序后确认程序是否存在bug 也方便修改bug时的调试。界面左下角为配送小车扫描到的二维码信息 进行可视化便于调试。界面右下角为配送小车主程序运行时的信息打印 同样也是便于调试和检测程序是否存在bug。 3、颜色识别

以下程序可调转至该链接有详细的解析 OpenCV python 三 【图像预处理 颜色空间转换】 颜色识别

(1)、区块识别

挂几个函数

def transform(self, image): # 颜色空间转换函数
 # 颜色空间转换 image为导入的RGB图像
 image_g cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # GRAY图像
 image_hsv cv2.cvtColor(image, cv2.COLOR_BGR2HSV) # HSV图像
 return image_g, image_hsv
def color(self, image_hsv, color_mod): # 颜色识别函数
 if color_mod is 1:
 image_hsv image_hsv[142:192 , 0:255]
 image_color cv2.inRange(image_hsv, (109, 78, 77), (157, 180, 180)) # HSV图像二值化(紫色)
 elif color_mod is 2:
 image_hsv image_hsv[72:182 , 0:255]
 image_color cv2.inRange(image_hsv, (60, 70, 95), (96, 255, 255)) # HSV图像二值化(绿色)
 elif color_mod is 3:
 if self.brown_model is 1:
 if self.end_num is 1:
 image_hsv image_hsv[122:192 , 155:255]
 elif self.end_num is 2:
 image_hsv image_hsv[122:192 , 0:100]
 else:
 pass
 # 。。。。。。等等
 image_color cv2.inRange(image_hsv, (0, 95, 36), (13, 255, 255)) # HSV图像二值化(棕色1)
 image_color cv2.erode(image_color, (3, 3), iterations 5) # 腐蚀
 image_color cv2.dilate(image_color, (3, 3), iterations 5) # 膨胀
 return image_color
(2)、红绿灯识别

上述的调节曝光度则是为现在红绿灯识别做准备 一般环境下看红绿灯效果如下
在这里插入图片描述
上图亮的是红灯 但是完全看不出来红色 后面和师兄聊过后 师兄建议我试试看调节曝光度看看 于是我尝试了一下 如下图所示
在这里插入图片描述
识别效果很不错 容错率很高 效果如下
在这里插入图片描述

四、电路设计

以下为我们设计的单片机扩展板和电源板。
在这里插入图片描述
在这里插入图片描述

以上为智能终端配送机器人的创作思路和部分代码 可能或多或少也会存在一些问题 如有错误的话还请指正。过段时间整理好后再开源。


本次参加了广东省工科赛智能终端配送组,记录了自己的创作历程和一些踩过的坑,包括解决方案,包括上位机视觉和界面、扫码模块、语音模块、下位机PID速度环、陀螺仪等。 复制链接
STM32控制步进电机:基于定时器中断的ULN2003驱动器/步进电机驱动程序 16183
STM32控制步进电机:基于HAL库定时器中断的闭环步进电机驱动+精准控制脉冲数 11592
STM32控制步进电机:工作原理及库函数(标准库) / HAL库控制程序(不定期更新) 6673
OpenCV python(四)图像预处理:二值化 滤波操作 Open-AI: import cv2 import os # 图像文件夹路径 img_folder = ./imgs/ output_folder = ./output/ # 获取文件夹中所有图像文件的路径 # 如果输出文件夹不存在,则创建它 if not os.path.exists(output_folder): os.makedirs(output_folder) img_paths = [os.path.join(img_folder, img) for img in os.listdir(img_folder) if img.endswith(( .jpg , .png , .jpeg ))] # 循环处理每个图像 for img_path in img_paths: # 读取图像 img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) # 二值化处理 t, img_bin = cv2.threshold(img, 180, 255, cv2.THRESH_BINARY)
base_name = os.path.basename(img_path) output_path = os.path.join( ./output/ , f binary_{base_name} ) cv2.imwrite(output_path, img_bin) print( Binary conversion completed. ) # 循环处理每个图像 for img_path in img_paths: # 读取图像 img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) # 均值滤波 img_blur = cv2.blur(img, (1, 5)) # # 均值滤波 cv2.imshow( Binary Ima


抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

更多文章请关注《万象专栏》