最近在准备2026年全国大学生电子设计竞赛(电赛)的同学,是不是又在为小车底盘的选择和二次开发而头疼?市面上的底盘要么尺寸太小、拓展性差,要么控制复杂、不符合电赛清单的严格要求。本文将围绕一款专为电赛设计的“轮趣2026电赛专用小车底盘”,从硬件拆解、环境搭建、核心控制到二次开发实战,为你提供一份从零到一的完整技术指南。无论你是初次接触电赛的新手,还是希望优化现有方案的进阶开发者,都能在这里找到清晰的步骤、可复用的代码和关键的避坑经验。
1. 背景与核心概念:为什么需要专用电赛底盘?
全国大学生电子设计竞赛(电赛)的控制类题目,如循迹、避障、搬运等,其核心执行机构往往是一个移动平台,也就是我们常说的小车底盘。一个优秀的底盘不仅是完成比赛的基础,更是算法和策略得以稳定发挥的载体。
1.1 电赛对底盘的核心要求
电赛清单对参赛作品有明确的技术指标要求,这直接决定了底盘的设计方向:
- 尺寸与结构:底盘尺寸需适配比赛场地(如迷宫、赛道),结构需稳固以承载传感器(摄像头、激光雷达)、执行器(机械臂)和主控板。
- 驱动与转向:需要精确的速度控制和灵活的转向能力,常见方案包括差速驱动(两轮)、阿克曼转向(前轮转向)以及更灵活的四轮麦克纳姆轮或全向轮。
- 控制接口:必须提供标准、易用的控制接口(如PWM、串口、CAN总线),方便参赛者使用STM32、树莓派、Jetson Nano等主流控制器进行二次开发。
- 供电与续航:需匹配电赛常见的电池规格(如12V锂电池),并提供稳定的电源管理,确保整个系统在比赛期间稳定运行。
- 拓展性:预留丰富的安装孔位和标准接口(如舵机接口、传感器I2C/SPI接口、扩展GPIO),方便快速集成各种外设。
1.2 通用底盘 vs. 电赛专用底盘
许多同学会使用玩具车改装或通用机器人底盘,但这往往面临以下问题:
- 精度不足:玩具电机编码器分辨率低,导致速度环、位置环控制精度差。
- 刚性不够:车体晃动会影响传感器数据(如IMU)的准确性。
- 接口混乱:需要自行焊接、转接,增加了不稳定性和调试难度。
- 不符合清单:可能使用了清单禁止的集成模块(如成品驱动板),导致评审扣分。
因此,一款像“轮趣2026电赛专用小车底盘”这样,针对电赛需求进行针对性设计(大尺寸、高刚性、标准接口、开源控制库)的产品,能极大节省底层机械和驱动调试时间,让团队更专注于上层算法和策略的实现。
2. 环境准备与版本说明
在开始二次开发前,我们需要搭建好软硬件环境。以下配置基于常见的电赛开发流程。
2.1 硬件清单与连接
假设“轮趣2026底盘”包含以下核心部件(具体以实物为准):
- 底盘本体:大尺寸铝合金结构,四轮差速或麦克纳姆轮驱动。
- 驱动电机:带增量式编码器的直流减速电机。
- 电机驱动器:可能集成在底板上,支持PWM控制和编码器反馈,接口为端子或插头。
- 主控板:STM32F4系列或树莓派4B(根据你的选择)。本文示例将以STM32为主控。
- 电源模块:12V锂电池,通过底板转换为5V、3.3V等供主控和传感器使用。
- 调试工具:ST-Link调试器、USB转TTL串口模块、万用表。
硬件连接示意图(逻辑连接):
12V锂电池 --> 底盘电源输入端子 底盘电机驱动接口(PWM1, PWM2, ENCODER_A, ENCODER_B) --> STM32对应GPIO引脚 STM32 USART1 (TX/RX) --> USB转TTL --> PC (用于打印调试信息) ST-Link (SWDIO, SWCLK, GND, 3.3V) --> STM32 SWD接口2.2 软件环境准备
- 操作系统:Windows 10/11 或 Ubuntu 20.04 LTS。
- 集成开发环境 (IDE):
- STM32开发:Keil uVision 5 (MDK-ARM) 或 STM32CubeIDE。本文示例使用STM32CubeIDE,因其集成了HAL库和图形化配置。
- 上位机/算法开发:Python 3.8+ (用于视觉处理、路径规划) 或 C++ (ROS)。
- 关键软件/库:
- STM32CubeMX (用于初始化代码生成)。
- Serial调试助手 (如Putty、SecureCRT)。
- Python库:
pyserial,opencv-python,numpy(可选)。
3. 核心控制原理与通信协议拆解
要对底盘进行精准控制,必须理解其底层驱动原理和通信方式。
3.1 电机控制原理(PWM与编码器)
底盘通常通过PWM(脉冲宽度调制)信号控制电机速度,通过编码器读取电机实际转速,形成闭环控制。
- PWM控制:通过调节占空比来控制电机平均电压,从而控制转速。例如,20kHz频率下,50%占空比对应一半的电源电压。
- 编码器反馈:增量式编码器输出A、B两相正交脉冲。通过STM32的定时器编码器接口模式,可以自动计数脉冲数和判断方向,从而计算速度和位移。
3.2 常见底盘通信协议
底盘与主控之间通常通过以下几种方式通信:
- 直接PWM/GPIO控制:最基础的方式,主控直接产生PWM波控制电机驱动器,通过GPIO读取编码器信号。简单直接,但需要主控资源较多。
- 串口指令控制:底盘内置一个单片机(下位机),主控(上位机)通过串口发送特定格式的指令(如速度指令
V 100 100\n),下位机解析后执行电机控制,并可能返回状态数据。这种方式解放了主控,是电赛中非常流行的方案。 - CAN总线控制:在更复杂或多节点系统中使用,抗干扰能力强,可扩展性好。例如,每个电机驱动器作为一个CAN节点。
假设“轮趣底盘”采用串口指令控制,其协议可能如下(需根据实际手册调整):
// 指令格式:指令头 数据1 数据2 ... 校验和\n // 示例:设置左轮速度100,右轮速度100 // 发送:`V 100 100\n` // 回复:`OK\n` 或 `V 100 95\n` (返回实际速度) // 速度指令 #define CMD_SET_VELOCITY ‘V‘ // 格式: “V left_velocity right_velocity\n” // 查询速度指令 #define CMD_GET_VELOCITY ‘Q‘ // 格式: “Q\n”, 回复: “V lv rv\n” // 停止指令 #define CMD_STOP ‘S‘ // 格式: “S\n”4. 完整实战案例:基于STM32的底盘控制与二次开发
我们将完成一个完整的项目:通过STM32控制底盘运动,并实现简单的串口指令调速功能。
4.1 创建STM32工程并配置外设
- 打开STM32CubeMX,选择你的芯片型号(如STM32F407ZGTx)。
- 配置时钟树:将HCLK设置为最大频率(如168MHz)。
- 配置外设:
- USART1:异步模式,波特率115200,用于与PC调试和可能的下位机通信。
- TIM1/TIM8:选择两个高级定时器用于生成PWM控制电机。例如,TIM1_CH1, CH2用于左轮,TIM1_CH3, CH4用于右轮(假设是双路H桥驱动)。模式设为“PWM Generation CHx”。
- TIM2/TIM3:选择两个通用定时器用于编码器接口模式。配置为“Encoder Mode”,对应编码器的A、B相引脚。
- GPIO:配置可能的电机使能引脚、方向引脚为输出模式。
- SYS:Debug设为Serial Wire。
- 生成代码:设置项目名称、路径,选择IDE为STM32CubeIDE,然后生成代码。
4.2 编写电机驱动层代码
在生成的工程中,我们创建独立的电机驱动文件。
motor_driver.h
#ifndef __MOTOR_DRIVER_H #define __MOTOR_DRIVER_H #include “main.h” #include “tim.h” typedef struct { TIM_HandleTypeDef *pwm_tim; // PWM定时器句柄 uint32_t pwm_channel; // PWM通道 TIM_HandleTypeDef *encoder_tim; // 编码器定时器句柄 int16_t last_encoder_count; // 上次编码器计数值 int32_t total_pulse; // 累计脉冲数(用于里程计) float speed_rpm; // 计算出的转速(RPM) } Motor_HandleTypeDef; void Motor_Init(Motor_HandleTypeDef *motor, TIM_HandleTypeDef *pwm_tim, uint32_t pwm_ch, TIM_HandleTypeDef *enc_tim); void Motor_SetSpeed(Motor_HandleTypeDef *motor, int16_t pwm_value); void Motor_UpdateSpeed(Motor_HandleTypeDef *motor, uint32_t sample_time_ms); int32_t Motor_GetTotalPulse(Motor_HandleTypeDef *motor); #endifmotor_driver.c
#include “motor_driver.h” #define ENCODER_RESOLUTION 11 // 电机编码器线数,假设为11线 #define GEAR_RATIO 30 // 减速比,假设为30:1 #define PULSES_PER_REV (ENCODER_RESOLUTION * 4 * GEAR_RATIO) // 每转总脉冲数(4倍频) void Motor_Init(Motor_HandleTypeDef *motor, TIM_HandleTypeDef *pwm_tim, uint32_t pwm_ch, TIM_HandleTypeDef *enc_tim) { motor->pwm_tim = pwm_tim; motor->pwm_channel = pwm_ch; motor->encoder_tim = enc_tim; motor->last_encoder_count = __HAL_TIM_GET_COUNTER(enc_tim); motor->total_pulse = 0; motor->speed_rpm = 0.0f; HAL_TIM_PWM_Start(pwm_tim, pwm_ch); // 启动PWM输出 HAL_TIM_Encoder_Start(enc_tim, TIM_CHANNEL_ALL); // 启动编码器接口 } void Motor_SetSpeed(Motor_HandleTypeDef *motor, int16_t pwm_value) { // 限制PWM值在合理范围,例如定时器ARR为999,则PWM值范围-999~999 if (pwm_value > 999) pwm_value = 999; if (pwm_value < -999) pwm_value = -999; // 根据正负值设置方向(假设需要控制一个方向引脚,这里简化处理) // HAL_GPIO_WritePin(DIR_GPIO_Port, DIR_Pin, (pwm_value >= 0) ? GPIO_PIN_SET : GPIO_PIN_RESET); // 设置PWM占空比,取绝对值 __HAL_TIM_SET_COMPARE(motor->pwm_tim, motor->pwm_channel, abs(pwm_value)); } void Motor_UpdateSpeed(Motor_HandleTypeDef *motor, uint32_t sample_time_ms) { int16_t current_count = __HAL_TIM_GET_COUNTER(motor->encoder_tim); int16_t delta_pulse = current_count - motor->last_encoder_count; // 处理定时器溢出(16位定时器) if(delta_pulse > 32767) delta_pulse -= 65536; else if(delta_pulse < -32767) delta_pulse += 65536; motor->total_pulse += delta_pulse; motor->last_encoder_count = current_count; // 计算转速 (RPM) = (delta_pulse / 采样时间(ms) * 1000 * 60) / PULSES_PER_REV float delta_time_s = sample_time_ms / 1000.0f; motor->speed_rpm = (delta_pulse / delta_time_s * 60.0f) / PULSES_PER_REV; } int32_t Motor_GetTotalPulse(Motor_HandleTypeDef *motor) { return motor->total_pulse; }4.3 编写串口指令解析与底盘控制任务
在main.c中,我们整合电机驱动,并实现一个简单的串口指令解析器。
main.c关键部分
/* Private includes ----------------------------------------------------------*/ #include “motor_driver.h” #include “string.h” #include “stdio.h” /* Private variables ---------------------------------------------------------*/ Motor_HandleTypeDef left_motor, right_motor; UART_HandleTypeDef huart1; uint8_t uart_rx_buffer[64]; uint8_t uart_rx_len = 0; char cmd_buffer[64]; /* Private function prototypes -----------------------------------------------*/ void SystemClock_Config(void); void Parse_UART_Command(char* cmd); int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM1_Init(); // PWM定时器 MX_TIM2_Init(); // 左编码器 MX_TIM3_Init(); // 右编码器 MX_USART1_UART_Init(); // 初始化左右电机 Motor_Init(&left_motor, &htim1, TIM_CHANNEL_1, &htim2); // 左轮 PWM CH1, 编码器 TIM2 Motor_Init(&right_motor, &htim1, TIM_CHANNEL_2, &htim3); // 右轮 PWM CH2, 编码器 TIM3 // 启动串口接收中断 HAL_UART_Receive_IT(&huart1, uart_rx_buffer, 1); uint32_t last_speed_update = HAL_GetTick(); while (1) { uint32_t now = HAL_GetTick(); // 每50ms更新一次电机速度计算 if(now - last_speed_update >= 50) { Motor_UpdateSpeed(&left_motor, 50); Motor_UpdateSpeed(&right_motor, 50); last_speed_update = now; // 可选:通过串口打印速度信息 // printf(“L:%.2f R:%.2f\r\n“, left_motor.speed_rpm, right_motor.speed_rpm); } // 其他任务,如PID计算、状态机等 // ... } } // 串口接收中断回调函数 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(huart->Instance == USART1) { uint8_t rx_char = uart_rx_buffer[0]; if(rx_char == ‘\n‘ || rx_char == ‘\r‘) { // 指令结束符 if(uart_rx_len > 0) { cmd_buffer[uart_rx_len] = ‘\0‘; // 字符串结束符 Parse_UART_Command(cmd_buffer); uart_rx_len = 0; } } else if(uart_rx_len < sizeof(cmd_buffer)-1) { cmd_buffer[uart_rx_len++] = rx_char; } // 重新启动接收中断,等待下一个字符 HAL_UART_Receive_IT(&huart1, uart_rx_buffer, 1); } } // 简单的指令解析函数 void Parse_UART_Command(char* cmd) { char cmd_char = cmd[0]; int left_speed = 0, right_speed = 0; switch(cmd_char) { case ‘V‘: // 速度指令 V left right if(sscanf(cmd, “V %d %d“, &left_speed, &right_speed) == 2) { Motor_SetSpeed(&left_motor, left_speed); Motor_SetSpeed(&right_motor, right_speed); printf(“OK: V set to %d %d\r\n“, left_speed, right_speed); } else { printf(“ERR: Invalid V command format\r\n“); } break; case ‘S‘: // 停止 Motor_SetSpeed(&left_motor, 0); Motor_SetSpeed(&right_motor, 0); printf(“OK: Stopped\r\n“); break; case ‘Q‘: // 查询速度 printf(“V %.1f %.1f\r\n“, left_motor.speed_rpm, right_motor.speed_rpm); break; default: printf(“ERR: Unknown command ‘%c‘\r\n“, cmd_char); break; } } // 重定向printf到串口 #ifdef __GNUC__ #define PUTCHAR_PROTOTYPE int __io_putchar(int ch) #else #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f) #endif PUTCHAR_PROTOTYPE { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY); return ch; }4.4 运行与验证
- 编译与下载:在STM32CubeIDE中编译工程,通过ST-Link下载到开发板。
- 硬件连接:确保STM32与底盘驱动板、编码器正确连接,串口与PC连接。
- 打开串口助手:设置波特率115200,打开对应COM口。
- 发送指令测试:
- 发送
V 200 200\n,观察两个轮子是否同时正转。 - 发送
V -150 150\n,观察小车是否原地左转(差速模式)。 - 发送
Q\n,查看返回的速度值。 - 发送
S\n,小车应停止。
- 发送
- 结果说明:如果小车能按指令运动,并且返回的速度值随负载变化,说明底层驱动和串口通信已基本打通。你可以进一步封装更复杂的指令,如
GOTO x y,或接入遥控器、传感器。
5. 常见问题与排查思路
在底盘二次开发过程中,你可能会遇到以下典型问题。
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 电机不转,但PWM有输出 | 1. 电源未接通或电压不足。 2. 电机使能引脚未拉高。 3. 驱动器故障或模式设置错误。 4. 电机线接触不良。 | 1. 用万用表测量驱动板电源输入端电压。 2. 检查STM32代码中使能引脚的初始化状态。 3. 查阅驱动器手册,确认控制逻辑(如IN1/IN2、PWM/DIR模式)。 4. 重新插拔电机线。 |
| 电机只朝一个方向转 | 1. 方向控制引脚逻辑错误。 2. 驱动器一路输入损坏。 3. PWM占空比计算有误,未处理负值。 | 1. 用逻辑分析仪或示波器查看方向引脚信号。 2. 交换左右电机的PWM线,判断是电机问题还是驱动问题。 3. 检查 Motor_SetSpeed函数中对负速度(反转)的处理逻辑。 |
| 编码器读数始终为0或不变 | 1. 编码器供电不正常。 2. 编码器A/B相接反。 3. STM32定时器未配置为编码器模式。 4. 脉冲频率过高,超过定时器计数频率。 | 1. 测量编码器VCC和GND电压。 2. 交换A、B相接线试试。 3. 在CubeMX中确认TIMx配置为“Encoder Mode”。 4. 降低电机转速测试,或使用具有更高时钟的定时器。 |
| 串口无法接收或发送数据 | 1. 波特率不匹配。 2. TX/RX线接反。 3. 串口助手中未正确设置停止位、校验位。 4. 代码中未开启串口接收中断或DMA。 | 1. 确认代码和串口助手波特率一致(如115200)。 2. 交换TX和RX线连接。 3. 检查CubeMX中USART的配置(数据位8,停止位1,无校验)。 4. 检查 HAL_UART_Receive_IT是否在main中调用。 |
| 小车运动不直或打滑 | 1. 左右轮电机参数不一致(空载转速差)。 2. 轮胎磨损或地面摩擦系数不均。 3. 编码器分辨率或减速比参数设置错误。 4. 未进行速度闭环PID控制。 | 1. 分别测试单个电机的PWM-速度曲线,进行标定。 2. 使用相同的轮胎,在平整地面测试。 3. 核对电机铭牌上的编码器线数和减速比。 4. 为每个电机增加PID控制器,根据编码器反馈实时调整PWM输出。 |
| 控制响应延迟大 | 1. 主循环任务过重,阻塞了控制周期。 2. 串口指令解析效率低。 3. 未使用定时器中断进行精确周期控制。 | 1. 优化代码,将非实时任务(如打印)放到低优先级。 2. 使用DMA进行串口收发,或简化指令格式。 3. 使用一个硬件定时器中断,固定周期(如10ms)执行核心控制任务(PID计算、速度更新)。 |
6. 最佳实践与工程建议
掌握了基础控制后,要打造一个稳定、可靠、易于调试的电赛小车,还需要遵循以下工程实践。
6.1 软件架构分层
将代码模块化,提高可读性和可维护性。
/applications (应用层,如 main.c, 任务调度) /tasks - control_task.c // 控制任务 - sensor_task.c // 传感器数据采集 /modules (模块层) - motor_driver.c // 电机驱动 - pid_controller.c // PID算法 - encoder.c // 编码器处理 - uart_protocol.c // 串口协议解析 /bsp (板级支持包) - bsp_motor.c // 硬件相关的电机初始化 - bsp_uart.c /utils (工具库) - filter.c // 滤波器(如卡尔曼) - debug.c // 调试信息输出使用头文件清晰定义模块接口,避免全局变量滥用。
6.2 实现速度闭环PID控制
开环PWM控制无法克服负载变化,必须引入编码器反馈进行闭环控制。
// pid_controller.h typedef struct { float kp, ki, kd; float integral; float prev_error; float output_limit; } PID_HandleTypeDef; void PID_Init(PID_HandleTypeDef *pid, float kp, float ki, float kd, float limit); float PID_Calculate(PID_HandleTypeDef *pid, float setpoint, float measurement); // 在控制任务中 float target_speed_left = 100.0f; // 目标转速 RPM float current_speed_left = left_motor.speed_rpm; float pwm_out_left = PID_Calculate(&pid_left, target_speed_left, current_speed_left); Motor_SetSpeed(&left_motor, (int16_t)pwm_out_left);调参建议:先调P(比例),让系统有基本响应;再调D(微分),抑制超调和振荡;最后调I(积分),消除静差。务必设定积分限幅和输出限幅。
6.3 设计健壮的通信协议
对于串口指令,建议增加校验和,提高可靠性。
// 发送端:V,100,100 -> 计算校验和(如简单求和取低8位) // 指令格式:$指令,参数1,参数2,*校验和\r\n // 例如:$V,100,100,*44\r\n // 接收端解析步骤: // 1. 判断起始符‘$‘ // 2. 找到结束符‘\n‘ // 3. 定位‘*‘,分离出数据部分和校验和部分 // 4. 重新计算数据部分的校验和,与接收的校验和比对 // 5. 校验通过才执行指令,并回复“ACK”;失败则回复“NAK”。6.4 传感器融合与数据同步
电赛小车通常需要融合多传感器数据(编码器、IMU、摄像头)。
- 定时采样:使用定时器中断确保传感器数据采集周期固定。
- 数据滤波:对编码器速度、IMU角速度进行低通滤波或卡尔曼滤波,减少噪声。
- 时间戳:为每个传感器数据打上时间戳,便于后续融合算法(如里程计)使用。
- 线程/任务安全:如果使用RTOS,确保共享数据(如全局位姿)通过信号量或队列安全访问。
6.5 调试与日志系统
一个高效的调试系统能极大提升开发效率。
- 分级日志:定义不同的日志级别(INFO, WARN, ERROR),通过宏控制输出。
#define DEBUG_LEVEL 1 #if DEBUG_LEVEL >= 1 #define LOG_INFO(fmt, ...) printf(“[INFO] “ fmt “\r\n“, ##__VA_ARGS__) #else #define LOG_INFO(fmt, ...) #endif - 上位机调试工具:使用Python的
matplotlib或PyQtGraph制作简单的上位机,实时绘制小车速度、PID输出、传感器数据曲线。 - 离线数据分析:将关键数据通过串口发送到PC并保存为文件,赛后用MATLAB或Python进行深入分析,优化参数。
6.6 备赛与项目管理
- 版本控制:务必使用Git管理代码,每次重大更改或调试通过后及时提交。
- 文档记录:维护一个开发日志,记录硬件连接图、引脚定义、协议文档、调参记录、遇到的问题和解决方案。
- 模块化测试:每完成一个模块(如电机驱动、PID、视觉识别),都进行独立的单元测试,确保其功能正确。
- 系统联调:留出充足时间进行整机联调,模拟比赛场景进行压力测试(如连续运行1小时)。
从选型一款合适的电赛专用底盘,到完成底层驱动、通信协议、闭环控制,再到集成传感器和上层算法,每一步都需要清晰的思路和扎实的调试。本文提供的STM32示例代码和开发框架,可以直接作为你项目的起点。记住,电赛不仅是技术的比拼,更是工程实践能力和团队协作的考验。在开发过程中,多思考、多测试、多记录,将复杂问题分解为一个个可解决的小模块,你的小车一定会越来越“智能”。