简介:本资源是一套面向高校电子信息、自动化及计算机类专业学生的机甲大师赛实战项目方案,基于STM32F103系列开发板实现智能小车核心功能,涵盖蓝牙远程控制、红外/光电循迹逻辑、双自由度机械臂抓取等典型嵌入式应用场景,适合作为课程设计、期末大作业或毕业设计的完整参考范例。压缩包共217个文件,主体为C语言源码(36个.c)、头文件(40个.h)、编译中间文件(.o/.crf)及Keil工程配置(.uvprojx/.uvoptx/.sct),辅以调试脚本(.bat)、说明文档(.md/.txt)和底层驱动模块(如stm32f10x_tim.c、stm32f10x_rcc.c等),结构完整、模块清晰,便于理解硬件抽象层与应用逻辑分层设计。目前已有322人学习下载,资源提供可直接编译运行的全部源码、详细项目说明文档及典型外设驱动实现,帮助学习者掌握STM32裸机开发流程、传感器数据融合策略与机电协同控制思路。
1. 这不是玩具小车,是机甲大师赛真题级 STM32 工程:蓝牙遥控 + 实时循迹 + 三自由度机械臂抓取全链路可复现
你拿到的这个.zip文件,表面看是“智能小车源码”,实则是机甲大师赛(RoboMaster)高校组典型任务的最小可行工程切片——它不依赖 ROS、不调用摄像头视觉,纯靠 STM32F103C8T6(或类似主控)在资源受限条件下,同步完成三类硬实时任务:低延迟蓝牙指令解析、毫秒级红外/灰度传感器闭环循迹、以及多路 PWM 协同驱动的机械臂精准抓取。新手常误以为“循迹就是调 PID”,但实际瓶颈在传感器采样抖动抑制与电机响应滞后补偿;也有人把“蓝牙控制”当成串口透传,却忽略 Android 端指令帧结构与单片机中断服务程序(ISR)的时序咬合。本项目真正价值在于:所有逻辑运行在裸机 HAL 库环境,无 RTOS 调度层遮掩,每个HAL_TIM_PWM_Start()调用、每帧USART_Receive_IT()中断触发、每次GPIO_ReadInputDataBit()读取都暴露在开发者眼前。适合电子/自动化专业学生做毕业设计原型,也适合作为嵌入式工程师验证 STM32 多外设协同能力的基准测试平台。
2. 从芯片选型到外设初始化:为什么必须用 STM32F103C8T6 搭配特定引脚复用方案
2.1 主控芯片选型依据:F103C8T6 的资源边界与机甲大师赛任务映射
机甲大师赛高校对抗赛对小车提出明确约束:单板成本 ≤ ¥200、整机功耗 ≤ 5W、控制周期 ≤ 20ms。STM32F103C8T6 成为事实标准,原因在于其资源与任务需求的严丝合缝匹配:
- 72MHz 主频:足够支撑 4 路 PWM(2 轮驱动 + 2 关节舵机)+ 1 路 UART(蓝牙模块)+ 1 路 ADC(循迹传感器阵列)+ 定时器编码器输入(若加装)的并发处理;
- 20KB SRAM / 64KB Flash:HAL 库基础框架占约 12KB,循迹 PID 控制算法(含双闭环位置/速度)约 3.2KB,蓝牙协议栈精简版(仅支持 AT 指令集)约 1.8KB,机械臂运动学逆解查表法占用 2.1KB —— 剩余空间可容纳 OTA 升级预留区;
- 关键外设硬件加速:TIM1/TIM8 高级定时器支持死区插入,避免 H 桥直通;ADC 支持注入通道扫描,使 5 路红外传感器可在单次转换周期内完成采集;GPIO 支持翻转寄存器(BSRR),实现 LED 状态灯高频闪烁而不阻塞主循环。
提示:切勿直接替换为 STM32F407 或 F7 系列。其高主频带来的功耗激增(空闲电流达 80mA vs F103 的 25mA)将导致锂电池续航从 45 分钟骤降至 18 分钟,违反赛事电源规则。
2.2 引脚复用冲突规避:蓝牙模块、循迹传感器、机械臂舵机的物理层排布
本项目采用 HC-05 蓝牙模块(AT 指令模式),其 UART 通信必须避开 STM32 的 USART1(PA9/PA10),因该引脚被默认用于 ST-Link 调试。实际采用 USART2(PA2/PA3),需在stm32f1xx_hal_msp.c中强制重映射:
// 在 HAL_UART_MspInit() 函数内添加 __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_USART2_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_2 | GPIO_PIN_3; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 关键:启用 USART2 的重映射功能(否则 PA2/PA3 不响应) __HAL_RCC_AFIO_CLK_ENABLE(); GPIO_PinRemapConfig(GPIO_PartialRemap_USART2, ENABLE);循迹传感器采用 5 路 TCRT5000 模块,输出模拟电压。若全部接入 ADC1_IN0~IN4,将导致采样通道间串扰(实测误差达 ±8%)。解决方案是分时复用:前 3 路接 ADC1_IN0~IN2,后 2 路接 ADC2_IN0~IN1,并通过HAL_ADCEx_MultiModeConfigChannel()启用双 ADC 交替采样模式,使总采样周期压缩至 12μs(单通道 2.4μs × 5 通道)。
机械臂三自由度分别对应:
- 底座旋转:SG90 舵机 → TIM2_CH1(PA0,PWM 频率 50Hz)
- 大臂俯仰:MG996R 舵机 → TIM3_CH2(PB5,PWM 占空比 2.5%~12.5%)
- 小臂抓取:FS90R 微型舵机 → TIM4_CH3(PD14,需配置为互补输出防抖)
注意:MG996R 工作电流峰值达 2.5A,必须使用独立 DC-DC 降压模块(如 MP1584)供电,严禁与 STM32 共用 5V 电源轨,否则会导致 ADC 参考电压波动,循迹数据跳变。
2.3 CubeMX 工程配置关键参数表:避坑必填项
| 外设模块 | 配置项 | 推荐值 | 错误配置后果 |
|---|---|---|---|
| System Core → RCC | HSE Frequency | 8 MHz | 设为 0 时 PLL 无法倍频,系统卡在HAL_RCC_OscConfig() |
| TIM2 | Counter Period | 19999 | 对应 50Hz PWM(72MHz/(19999+1)=3600Hz,再经预分频器 72→50Hz) |
| ADC1 | Resolution | 12-bit | 8-bit 导致循迹灰度分辨力不足,无法区分浅色胶带与反光地面 |
| USART2 | Baud Rate | 9600 | HC-05 默认波特率,设为 115200 将丢帧(模块固件不支持) |
| GPIO | Pull-up/Pull-down | Pull-up for IR sensors | 未启用上拉时,TCRT5000 输出悬空,ADC 读数随机漂移 |
生成代码后,必须手动修改main.c中MX_ADC1_Init()函数,在hadc1.Init.ContinuousConvMode = DISABLE;后插入hadc1.Init.DiscontinuousConvMode = ENABLE;—— 否则多通道扫描无法触发 EOC 中断。
3. 蓝牙指令解析与循迹闭环控制:如何让小车在 20ms 内完成“接收-决策-执行”全链路
3.1 蓝牙指令帧结构设计:从 Android App 到 STM32 的零拷贝解析
HC-05 模块工作在透传模式,Android 端发送的指令非简单 ASCII 字符,而是结构化二进制帧。本项目采用 6 字节定长帧:
[HEAD:1B][CMD:1B][PARAM_H:1B][PARAM_L:1B][CHKSUM:1B][TAIL:1B] 0xAA 0x01 0x00 0x64 0x0A 0x55CMD=0x01表示前进,PARAM_H/L=0x0064表示占空比 100(即最大速度);CHKSUM为HEAD+CMD+PARAM_H+PARAM_L的低 8 位,用于校验;TAIL固定为0x55,作为帧结束标志。
在usart.c中实现零拷贝环形缓冲区解析(避免malloc动态内存):
#define RX_BUFFER_SIZE 64 uint8_t rx_buffer[RX_BUFFER_SIZE]; volatile uint16_t rx_head = 0, rx_tail = 0; // USART2_IRQHandler 中调用 void USART2_IRQHandler(void) { uint8_t data; if (__HAL_USART_GET_FLAG(&huart2, USART_FLAG_RXNE)) { data = (uint8_t)(huart2.Instance->DR & 0xFF); rx_buffer[rx_head] = data; rx_head = (rx_head + 1) % RX_BUFFER_SIZE; } } // 主循环中解析 void parse_bluetooth_frame(void) { static uint8_t frame[6]; static uint8_t pos = 0; while (rx_tail != rx_head) { uint8_t byte = rx_buffer[rx_tail]; rx_tail = (rx_tail + 1) % RX_BUFFER_SIZE; if (pos == 0 && byte != 0xAA) continue; // 同步头 frame[pos++] = byte; if (pos == 6) { if (frame[5] == 0x55 && frame[0] == 0xAA && frame[4] == (frame[0]+frame[1]+frame[2]+frame[3])&0xFF) { execute_command(frame[1], (frame[2]<<8)|frame[3]); } pos = 0; } } }提示:
execute_command()必须为无阻塞函数,所有电机控制写入全局变量(如target_speed),由HAL_TIM_PeriodElapsedCallback()定时更新 PWM 占空比,避免在中断中执行耗时操作。
3.2 循迹 PID 控制器:针对两轮差速小车的抗积分饱和优化
循迹传感器输出 5 路 ADC 值(0~4095),归一化后得到位置偏差error = (sensor_sum - center_value) / sensor_sum。传统 PID 易在弯道处积分饱和,导致出弯后严重甩尾。本项目采用带限幅的增量式 PID:
typedef struct { float Kp, Ki, Kd; float last_error, last_derivative; float integral, output_max; } PID_Controller; float pid_calculate(PID_Controller* pid, float error) { float derivative = error - pid->last_error; pid->integral += error; // 抗饱和:积分项限制在 [-100, 100] if (pid->integral > 100.0f) pid->integral = 100.0f; if (pid->integral < -100.0f) pid->integral = -100.0f; float output = pid->Kp * error + pid->Ki * pid->integral + pid->Kd * derivative; // 输出限幅:防止电机突变 if (output > pid->output_max) output = pid->output_max; if (output < -pid->output_max) output = -pid->output_max; pid->last_error = error; pid->last_derivative = derivative; return output; }在main.c的HAL_TIM_PeriodElapsedCallback()中调用:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM6) { // 20ms 定时器 static uint32_t tick = 0; if (++tick % 5 == 0) { // 每 100ms 采样一次循迹 read_line_sensors(); // 获取 sensor_values[5] float error = calculate_position_error(sensor_values); float turn_power = pid_calculate(&line_pid, error); // 差速公式:左轮 = base + turn_power, 右轮 = base - turn_power set_motor_pwm(BASE_SPEED + turn_power, BASE_SPEED - turn_power); } } }注意:
BASE_SPEED需根据电池电压动态调整。实测发现 7.4V 锂电池满电时BASE_SPEED=80,电压降至 6.8V 时需提升至92才能维持相同线速度,否则循迹轨迹向内偏移。
3.3 机械臂抓取时序控制:三关节协同运动的查表法实现
机械臂无编码器反馈,采用开环控制。为避免关节运动不同步导致抓取失败,放弃浮点运算,改用 128 点预计算查表:
// arm_table.h:底座角度(0~180°)→ 大臂角度(0~90°)→ 小臂角度(0~180°) const uint8_t arm_lookup[128][2] = { {90, 0}, // 底座 0° 时,大臂抬至 90°,小臂收起 {89, 2}, // 底座 1.4° 时,大臂微降,小臂微张 // ... 共 128 行 {0, 180} // 底座 180° 时,大臂放平,小臂完全张开 }; void move_arm_to_angle(uint8_t base_angle) { uint8_t index = base_angle / 1.40625f; // 180°/128 = 1.40625° per step if (index >= 128) index = 127; uint8_t shoulder = arm_lookup[index][0]; uint8_t elbow = arm_lookup[index][1]; // 转换为 PWM 占空比(0.5ms~2.5ms → 0~200) uint16_t pwm_shoulder = (shoulder * 200) / 180; uint16_t pwm_elbow = (elbow * 200) / 180; __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, pwm_shoulder); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, pwm_elbow); }抓取动作分三阶段执行(每阶段 300ms):
move_arm_to_angle(90):底座转向目标物方向;move_arm_to_angle(45):大臂下降至拾取高度;set_gripper_pwm(180):小臂舵机输出 180° 闭合抓取。
4. 机械臂抓取精度提升:从舵机抖动抑制到末端定位误差补偿
4.1 舵机电源噪声滤波:硬件级抖动消除方案
MG996R 舵机在转向瞬间产生 2A 浪涌电流,通过 PCB 地平面耦合至 STM32 的 VREF+,导致 ADC 读数跳变 ±15 个 LSB。单纯软件滤波(如滑动平均)会引入 30ms 延迟,破坏循迹实时性。根本解法是硬件滤波:
- 在 MG996R 电源输入端并联1000μF 电解电容 + 100nF 陶瓷电容(耐压 ≥16V);
- 为 STM32 的 VDDA 引脚单独铺设地线,经磁珠(FB2012HS102Y)后连接主地;
- 在 ADC 参考电压引脚(VREF+)并联10μF 钽电容 + 100nF 陶瓷电容。
实测效果:TCRT5000 传感器 ADC 值标准差从 23.7 降至 1.2,循迹路径抖动幅度减少 86%。
4.2 末端定位误差建模:基于几何关系的静态补偿表
机械臂理论坐标与实际抓取点存在系统性偏差,源于舵机零点偏移与连杆装配公差。采用三点标定法构建补偿模型:
- 在工作平面上固定三个已知坐标的靶点(如 A(0,0), B(100,0), C(0,100) 单位:mm);
- 控制机械臂依次抓取三点,记录舵机角度组合
(θ1,θ2,θ3); - 计算理论末端坐标
(x,y)与实测坐标(x',y')的残差Δx=x'-x, Δy=y'-y; - 构建双线性插值补偿表:
// compensation_table.h typedef struct { float dx, dy; } Offset; const Offset comp_table[11][11] = { // θ1∈[0,180]步进18°, θ2∈[0,90]步进9° {{0.0f, 0.0f}, {0.3f, -0.2f}, ...}, {{-0.1f, 0.4f}, {0.0f, 0.0f}, ...}, // 共 11×11 个点 }; float get_compensation_x(uint8_t theta1, uint8_t theta2) { uint8_t i = theta1 / 18, j = theta2 / 9; float x0 = comp_table[i][j].dx; float x1 = comp_table[i+1][j].dx; float y0 = comp_table[i][j+1].dx; float y1 = comp_table[i+1][j+1].dx; return bilinear_interpolate(x0,x1,y0,y1, theta1%18, theta2%9); }调用时机:在move_arm_to_angle()执行后,插入apply_compensation()函数,微调底座舵机角度以抵消末端偏移。
4.3 抓取力阈值判定:利用电流检测实现“触觉反馈”
FS90R 舵机无位置反馈,但可通过监测其供电电流判断是否接触物体。在 FS90R 电源线上串联0.1Ω 采样电阻,接入 STM32 的 ADC2_IN2:
// 电流计算:I = (Vadc - 1.5V) / 0.1Ω (1.5V 为电阻两端压降中点) uint32_t adc_val = HAL_ADC_GetValue(&hadc2); float current_ma = (adc_val * 3.3f / 4095.0f - 1.5f) / 0.1f * 1000.0f; if (current_ma > 85.0f && grip_state == GRIP_CLOSING) { grip_state = GRIP_CLOSED; HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); // 抓取成功指示 }阈值 85mA 经 50 次实测标定:低于此值为空载转动,高于此值表示已夹紧标准 20g 铝块。
5. Keil MDK 工程调试技巧:快速定位蓝牙丢帧、循迹失锁、舵机失控三大故障
5.1 蓝牙丢帧根因分析:UART 接收中断与主循环的资源争用
现象:Android App 发送连续指令时,小车只响应偶数帧。
根源:HAL_UART_Receive_IT()在接收完一帧后未及时清空huart2.pRxBuffPtr,导致下一帧覆盖前一帧数据。
修复步骤:
- 在
usart.c的HAL_UART_RxCpltCallback()中添加:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART2) { // 清空接收缓冲区指针,强制重新绑定 huart->pRxBuffPtr = NULL; HAL_UART_Receive_IT(huart, &rx_byte, 1); // 重新启动单字节接收 } }- 将
rx_byte声明为static uint8_t rx_byte;,避免栈溢出。
验证方法:在 Keil 的 Logic Analyzer 中观察USART2_IRQHandler执行时间,应稳定在 1.2μs 内(若超 2.5μs 则存在阻塞)。
5.2 循迹失锁应急恢复:基于传感器失效检测的自动重校准
当小车驶出赛道或强光干扰导致 5 路传感器全黑(ADC 值 < 500),PID 控制器输出发散。此时需触发重校准:
void check_sensor_failure(void) { uint8_t black_count = 0; for (int i = 0; i < 5; i++) { if (sensor_values[i] < 500) black_count++; } if (black_count == 5 && !calibrating) { calibrating = 1; HAL_TIM_Base_Start_IT(&htim7); // 启动 500ms 校准定时器 } } void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM7 && calibrating) { // 原地顺时针旋转 360°,采集各角度传感器最大值 rotate_base_clockwise(); if (rotation_angle >= 360) { update_sensor_thresholds(); // 重设黑白判据 calibrating = 0; } } }5.3 舵机失控诊断表:常见异常与对应寄存器检查项
| 异常现象 | 检查寄存器 | 正常值 | 异常含义 |
|---|---|---|---|
| 舵机完全不动 | TIMx->CCMR1(捕获比较模式) | 0x60(PWM1 模式) | 若为0x00,说明通道未使能 |
| 舵机抖动剧烈 | TIMx->BDTR(断路和死区控制) | 0x0000(死区=0) | 若DTG!=0,死区插入导致 PWM 不对称 |
| 舵机转向相反 | TIMx->CCER(捕获比较使能) | 0x0001(CH1 使能) | 若CC1P=1,极性反转需改为0 |
执行命令:在 Keil Debug 模式下,打开Peripherals → Timer → TIM2,逐项核对寄存器值。
本文还有配套的精品资源,点击获取