简介:本资源是南京航空航天大学电子设计竞赛校赛‘自动泊车’题目的完整实现方案,面向嵌入式初学者与课程设计、毕设、工程实训学习者,提供从机器视觉识别到运动控制的端到端技术闭环。资源包含基于STM32F103主控的Keil工程(含34个C源文件、36个头文件及编译生成文件)与OpenMV端Python代码(3个.py文件),集成光电寻迹、码盘角度反馈、异步通信协议、PWM电机驱动及自训练tflite数字识别模型,配套PDF设计报告详述算法逻辑与硬件架构。压缩包共202个文件,大小6.55MB,涵盖固件(.hex/.axf)、配置(.ini/.dbgconf)、文档(.pdf/.md)及底层驱动(如stm32f10x_usart.c等),目录结构模块清晰,便于分层理解与调试。已有349人学习下载,可直接部署运行,快速掌握多传感器协同控制与轻量级AI视觉落地的关键实践路径。
1. 这不是“识别数字就停车”的玩具小车,而是把OpenMV当边缘AI协处理器、STM32做实时运动控制器的闭环系统
南航电赛校赛自动泊车题目的真实难点,从来不在“拍到数字”——OpenMV IDE里跑个find_numbers()函数三分钟就能出结果。真正卡住90%参赛队的是:识别结果如何在毫秒级内变成转向角度?光电码盘反馈的脉冲怎么和OpenMV发来的目标坐标对齐时间戳?为什么烧写后小车在识别区原地打转、但串口打印全是“OK”?这个资源包的价值,恰恰在于它用一套可复现的硬件协同逻辑,把视觉识别、运动控制、状态机调度三个层面拧成一股绳。它不教你怎么调OpenMV阈值,而是告诉你:当OpenMV识别到“3”时,STM32必须在≤12ms内完成PID参数切换+PWM占空比重载+编码器清零;当小车压过左侧线触发中断,要立刻冻结OpenMV帧率并启动循迹PID。适合正在啃《STM32标准库开发实战》却卡在“多模块时序协同”的人,也适合毕设选题定在智能车但被导师问“你这识别和控制是串行还是并行”的同学——所有代码都带// [TIM2] 编码器计数同步点这类注释,设计报告里甚至画出了OpenMV与STM32的异步通信状态机图。
2. OpenMV端:云训练模型+自定义通信协议实现低延迟识别输出
2.1 为什么不用OpenMV内置数字识别,而要自己云训练?
OpenMV官方固件的find_numbers()对印刷体数字鲁棒性尚可,但在电赛现场强光反射、斜角拍摄、纸张褶皱场景下误检率超35%。本方案采用OpenMV IDE 4.3.0 + Tensorflow Lite Micro框架,在Edge Impulse平台完成端侧模型训练:上传200张实测场景下的数字图像(含光照不均、透视畸变、阴影遮挡),标注为0-9共10类,选用MobileNetV1-0.25/96×96输入尺寸,量化为int8模型。生成的.tflite文件通过IDE的Tools → Load Model导入,关键代码如下:
import sensor, image, time, tf from pyb import UART # 初始化传感器与模型 sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) # 320x240,平衡速度与精度 sensor.skip_frames(time=2000) net = tf.load('digits_model.tflite') # 模型文件需提前存入OpenMV SD卡根目录 # 串口初始化:波特率115200,无校验,1停止位 uart = UART(3, 115200, timeout_char=1000) uart.init(115200, bits=8, parity=None, stop=1) while True: img = sensor.snapshot() # ROI裁剪:仅处理画面中央120x120区域(减少计算量) roi = (100, 60, 120, 120) res = net.predict(img, roi=roi, threshold=0.7, min_scale=1.0, scale_mul=0.8) if res and res[0].confidence > 0.85: # 置信度阈值提高至0.85 digit = res[0].class_id() # 返回0-9整数 # 发送自定义协议帧:0xAA + 数字 + 校验和 + 0x55 checksum = (0xAA + digit) & 0xFF uart.write(bytes([0xAA, digit, checksum, 0x55])) time.sleep_ms(50) # 强制50ms间隔,避免STM32端缓冲区溢出注意:
threshold=0.7指NMS(非极大值抑制)阈值,min_scale=1.0禁用多尺度检测,scale_mul=0.8控制缩放步长——这些参数直接决定识别速度。实测QVGA分辨率下单帧推理耗时42ms,若改用QQVGA(160x120)可降至28ms,但数字边缘模糊导致置信度下降,需同步调整confidence阈值。
2.2 自定义异步通信协议的设计逻辑与STM32解析实现
OpenMV与STM32间不使用标准UART协议(如Modbus),而是设计4字节固定帧结构:[HEAD:0xAA][DATA:0-9][CHKSUM][TAIL:0x55]。这种设计规避了帧头搜索开销,STM32端可用状态机硬解码。Keil工程中stm32f10x_usart.c的关键修改如下:
// USART3接收中断服务函数(对应OpenMV UART3) void USART3_IRQHandler(void) { static uint8_t rx_buf[4] = {0}; static uint8_t rx_index = 0; uint8_t byte; if(USART_GetITStatus(USART3, USART_IT_RXNE) != RESET) { byte = USART_ReceiveData(USART3); switch(rx_index) { case 0: // 检查帧头 if(byte == 0xAA) { rx_buf[0] = byte; rx_index = 1; } else { rx_index = 0; // 丢弃非法字节 } break; case 1: // 存储数字 rx_buf[1] = byte; rx_index = 2; break; case 2: // 校验和验证 rx_buf[2] = byte; if((rx_buf[0] + rx_buf[1]) == rx_buf[2]) { rx_index = 3; } else { rx_index = 0; // 校验失败,重置 } break; case 3: // 帧尾验证 if(byte == 0x55) { g_digit_result = rx_buf[1]; // 全局变量存储识别结果 g_new_digit_flag = 1; // 置新数据标志 } rx_index = 0; // 无论成功与否,重置索引 break; } } }提示:
g_new_digit_flag是核心同步信号。STM32主循环中通过if(g_new_digit_flag)判断是否更新目标数字,而非轮询读取——这避免了因OpenMV发送间隔抖动导致的状态机错位。设计报告第3.2节明确指出:“该协议将平均通信延迟从标准UART的18ms压缩至3.2ms(实测值),为后续PID响应争取关键时间窗口”。
3. STM32端:基于定时器编码器接口与双闭环PID的运动控制实现
3.1 光电码盘信号接入与TIM2编码器模式配置
本系统未使用霍尔传感器,而是将光电码盘A/B相脉冲直接接入STM32F103C8T6的PA0/PA1引脚,配置TIM2为编码器接口模式。stm32f10x_tim.c中关键初始化代码:
void TIM2_Encoder_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // PA0/PA1配置为浮空输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); // TIM2编码器模式:TI1FP1和TI2FP2作为编码器输入 TIM_TimeBaseStructure.TIM_Period = 0xFFFF; // 计数器自动重装载值 TIM_TimeBaseStructure.TIM_Prescaler = 0; // 不分频 TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); // 配置通道1和2为编码器输入 TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_BothEdge; // 双边沿触发 TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter = 0x0F; // 采样滤波,抗干扰 TIM_ICInit(TIM2, &TIM_ICInitStructure); TIM_ICInitStructure.TIM_Channel = TIM_Channel_2; TIM_ICInit(TIM2, &TIM_ICInitStructure); TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_Cmd(TIM2, ENABLE); }关键参数说明:
TIM_ICPolarity_BothEdge使A/B相每个边沿都计数,4倍频提升分辨率;TIM_ICFilter=0x0F启用8个采样周期滤波,有效抑制电机换向火花干扰;TIM_Period=0xFFFF设置计数器为16位,避免高速旋转时溢出。实测1000线码盘在小车全速运行时,TIM2计数值每10ms变化约±1200,完全满足停车区精确定位需求。
3.2 双闭环PID控制器:外环位置PID + 内环速度PID
停车过程分为两个阶段:第一阶段(识别区→左侧线)采用纯光电循迹,第二阶段(左侧线→停车位)启用编码器位置闭环。stm32f10x_fsmc.c中实现的位置PID核心逻辑:
// 全局变量声明 float pos_setpoint = 0.0f; // 目标位置(单位:mm) float pos_current = 0.0f; // 当前位置(由编码器脉冲换算) float pos_error = 0.0f; float pos_integral = 0.0f; float pos_output = 0.0f; // 位置PID计算(执行周期10ms) void Position_PID_Calc(void) { pos_error = pos_setpoint - pos_current; // 积分分离:误差过大时禁用积分,防饱和 if(fabsf(pos_error) < 5.0f) { pos_integral += pos_error * 0.01f; // 采样周期0.01s } else { pos_integral = 0.0f; } // 比例+积分+微分 pos_output = 0.8f * pos_error + 0.05f * pos_integral + 0.15f * (pos_error - pos_last_error); pos_last_error = pos_error; // 输出限幅:-100 ~ +100(对应PWM占空比0%-100%) if(pos_output > 100.0f) pos_output = 100.0f; if(pos_output < -100.0f) pos_output = -100.0f; } // 编码器脉冲转位置(已知车轮周长180mm,码盘1000线,齿轮比1:5) void Encoder_To_Position(void) { int16_t cnt = TIM_GetCounter(TIM2); // 获取当前计数值 static int16_t last_cnt = 0; int16_t delta = cnt - last_cnt; last_cnt = cnt; // 四倍频后每转脉冲数 = 1000 * 4 * 5 = 20000 // 每脉冲对应距离 = 180mm / 20000 = 0.009mm pos_current += delta * 0.009f; }参数整定依据:
Kp=0.8保证快速响应,Ki=0.05抑制稳态误差,Kd=0.15抑制超调。设计报告附录B给出整定过程:先关闭Ki/Kd,调Kp至临界振荡(Kp=1.2),再加入Ki消除静差,最后加Kd抑制振荡。实测停车定位精度±1.2mm,远超电赛要求的±5mm。
4. 硬件协同调试:解决OpenMV与STM32通信丢帧及电机抖动问题
4.1 通信丢帧的三大根源与逐层排查法
当OpenMV发送频率为20Hz时,STM32端偶尔丢失整帧数据,现象为g_new_digit_flag长时间为0。按优先级排查:
| 排查层级 | 检查项 | 验证方法 | 修复措施 |
|---|---|---|---|
| 电源层 | OpenMV与STM32共地不良 | 用万用表测GND间电阻>1Ω | 增加粗铜线直连两地,避免通过PCB走线 |
| 信号层 | UART3 RX引脚存在高频干扰 | 示波器观察RX波形过冲>1V | 在PA3(RX引脚)串联33Ω电阻,靠近MCU端并联0.1μF电容 |
| 软件层 | USART3中断优先级低于TIM2 | NVIC_GetPriority(USART3_IRQn)返回值>NVIC_GetPriority(TIM2_IRQn) | 将USART3中断优先级设为2(TIM2为1),确保编码器计数不被阻塞 |
实测数据:增加硬件滤波后,RX波形过冲从1.8V降至0.3V;调整中断优先级后,
g_new_digit_flag置位延迟从最大12ms降至稳定3.2ms。最终丢帧率从12.7%降至0.3%(连续1000帧测试)。
4.2 电机驱动电路引起的抖动问题与PWM优化
使用L298N驱动直流电机时,小车在低速(PWM<20%)出现明显抖动。示波器捕获发现:H桥上下管存在短暂直通,导致电流尖峰。解决方案分三步:
- 硬件级:在L298N的ENA/ENB引脚串联10kΩ上拉电阻,确保使能端默认高电平;
- 驱动级:修改
stm32f10x_tim.c中TIM3 PWM输出,启用死区插入(Dead Time Insertion):TIM_BDTRInitTypeDef TIM_BDTRInitStructure; TIM_BDTRInitStructure.TIM_OSSRState = TIM_OSSRState_Enable; TIM_BDTRInitStructure.TIM_OSSIState = TIM_OSSIState_Enable; TIM_BDTRInitStructure.TIM_LOCKLevel = TIM_LOCKLevel_1; TIM_BDTRInitStructure.TIM_DeadTime = 100; // 死区时间100ns TIM_BDTRInit(TIM3, &TIM_BDTRInitStructure); - 算法级:在PID输出后增加S型加减速曲线:
float S_Curve_Ramp(float target, float current, float max_step) { float diff = target - current; if(fabsf(diff) < max_step) return target; return current + (diff > 0 ? max_step : -max_step); } // 调用:pwm_duty = S_Curve_Ramp(pos_output, pwm_duty, 2.0f);
效果对比:未优化时PWM=15%对应电机转速波动±80rpm;启用死区+加减速后,同一占空比下转速稳定在±5rpm内。设计报告图5.7显示:抖动消除后,小车在停车位前30cm处能以0.8m/s匀速滑行,而非断续启停。
5. 工程落地技巧:Keil5编译报错error: no stm32 target found!的精准定位与解决
5.1 ST-Link驱动与Target页面配置的隐性冲突
当Keil5编译通过但下载时报no stm32 target found,90%情况并非硬件连接问题,而是ST-Link Utility与Keil的Target配置存在隐性冲突。具体表现为:ST-Link Utility能正常识别芯片,但Keil中Project → Options → Debug → Settings → SW Device显示No Target Connected。
根本原因:ST-Link Utility在后台独占SWD接口,导致Keil无法获取设备描述符。验证方法:任务管理器中结束ST-LINK Utility.exe进程,立即在Keil中点击Load——若此时能成功下载,则确认为此问题。
永久解决步骤:
- 打开Keil
Options for Target → Debug → Settings - 在
SW Device列表中,手动选择STM32F103C8(而非自动识别) - 勾选
Connect under reset(强制复位连接) - 取消勾选
Run to main()(避免首次下载后立即运行导致调试中断)
5.2 STM32芯片包安装后的关键校验点
Keil5安装ARM::CMSIS和STMicroelectronics::STM32F1xx_DFP后,必须验证三项:
| 校验项 | 正确路径 | 错误表现 | 修复操作 |
|---|---|---|---|
| 启动文件 | .\ARM\Startup\startup_stm32f10x_md.s | 编译报undefined symbol Reset_Handler | 检查Options → C/C++ → Define中是否包含STM32F10X_MD |
| 外设寄存器定义 | .\ARM\CMSIS\Device\ST\STM32F1xx\Include\stm32f10x.h | TIM2等外设名标红 | 在Options → C/C++ → Include Paths中添加.\ARM\CMSIS\Device\ST\STM32F1xx\Include |
| Flash算法 | .\ARM\Flash\STM32F10x_128.FLM | 下载时报Flash Download failed | 复制STM32F10x_128.FLM到.\ARM\Flash\目录,并在Options → Utilities → Settings → Flash中选择该算法 |
特别注意:
stm32f10x_tim.c等标准库文件中的#include "stm32f10x.h"必须指向CMSIS路径,而非项目本地路径。若工程中存在同名头文件,Keil会优先加载本地版本,导致寄存器地址错误——这是TIM2无法启动的最隐蔽原因。设计报告附录C提供完整的Keil5工程配置截图(含所有Include Paths和Define宏)。
本文还有配套的精品资源,点击获取