STM32F103智能小车开发实战:硬件搭建与核心算法
1. STM32F103开发板与智能小车项目概述
在嵌入式开发领域,STM32系列MCU因其出色的性价比和丰富的生态资源,成为工程师和爱好者的首选平台。我最近基于STM32F103C8T6开发板完成了一个智能小车的二次开发项目,这个蓝色的小板子虽然只有拇指大小,但通过合理的外设扩展和编程,可以实现相当复杂的移动机器人功能。
STM32F103系列属于ARM Cortex-M3内核,主频可达72MHz,具有128KB Flash和20KB SRAM,对于智能小车这类需要实时控制的项目完全够用。开发板通常自带USB转串口、复位电路、用户按键和LED等基础外设,而智能小车项目需要额外扩展电机驱动、传感器等模块。我选择这款开发板主要考虑三点:首先是社区支持完善,遇到问题容易找到解决方案;其次是外设接口丰富,GPIO、PWM、ADC等资源充足;最后是开发工具链成熟,Keil、IAR、STM32CubeIDE等多种选择。
这个项目的核心目标是通过二次开发,让基础开发板具备智能小车的完整功能。所谓"二次开发",就是在现有硬件基础上进行功能扩展和软件优化,区别于从零设计PCB的"一次开发"。对于大多数爱好者来说,二次开发既能降低硬件门槛,又能快速实现创意,是性价比极高的选择。
2. 硬件搭建与关键模块选型
2.1 基础车体结构与电机驱动
智能小车的机械结构看似简单,实则每个细节都影响最终性能。我选用的是常见的四轮小车底盘,两个主动轮配合两个万向轮的方案。主动轮采用N20减速电机,工作电压3-6V,减速比1:120,这种电机体积小、扭矩适中,非常适合实验室环境下的智能小车。
电机驱动模块的选择至关重要,常见方案有L298N、TB6612FNG和DRV8833等。经过实测对比,我最终选择了TB6612FNG,原因有三:首先它的效率高达90%以上,远高于L298N的60%;其次支持1.2A持续电流(峰值3.2A),足够驱动N20电机;最后是体积小巧,便于安装在紧凑的车体上。接线时需要注意,电机的PWM输入信号需要连接到STM32的定时器通道,我使用的是TIM3的CH1和CH2。
重要提示:电机与开发板必须共地,否则会出现控制异常。同时建议在电源输入端加装470μF以上的电解电容,防止电机启动时的电压波动导致MCU复位。
2.2 核心传感器配置方案
智能小车的"智能"很大程度上取决于传感器系统的设计。基于功能需求和成本考虑,我的配置方案如下:
超声波模块HC-SR04:用于避障和距离测量,精度±3mm,测量范围2-400cm。通过STM32的GPIO触发和输入捕获功能实现测距。
红外循迹模块TCRT5000:五路数字输出,用于黑线识别。灵敏度可调,通过STM32的普通GPIO读取状态。
六轴姿态传感器MPU6050:通过I2C接口连接,提供加速度和角速度数据,用于车身姿态校正。
红外接收模块VS1838B:配合普通遥控器实现无线控制,使用STM32的外部中断捕获信号。
传感器布局需要特别注意:
- 超声波传感器应安装在小车正前方,高度约10-15cm
- 红外循迹模块安装在小车底部,距地面0.5-1cm
- MPU6050应尽量靠近车体中心位置,减少振动干扰
2.3 电源系统设计
稳定的电源是系统可靠运行的基础。我的方案采用两路独立供电:
- 主控部分:通过开发板的Micro USB接口供电(5V)
- 动力部分:18650锂电池(3.7V)经升压模块输出至6V
这种设计有三个优点:一是避免了电机噪声对MCU的干扰;二是可以单独调试控制系统;三是延长了电池使用时间。实测表明,2000mAh的18650电池可以支持小车连续运行约2小时。
3. 开发环境搭建与基础配置
3.1 工具链安装与配置
对于STM32开发,Keil MDK-ARM是最常用的IDE之一。安装时需要注意:
- 先安装Keil软件主体(建议v5.25以上版本)
- 安装STM32F1xx_DFP芯片支持包(当前最新为2.3.0)
- 安装ST-Link驱动(用于程序下载调试)
配置工程时关键点:
- 选择正确的设备型号:STM32F103C8Tx
- 设置正确的时钟源:HSE 8MHz
- 配置调试接口为SWD模式
- 勾选"Use MicroLIB"以减小代码体积
3.2 HAL库与硬件抽象层初始化
STM32CubeMX是ST官方提供的图形化配置工具,可以大幅提升开发效率。我的配置流程如下:
- 新建工程选择STM32F103C8Tx
- 配置时钟树:HSE→PLL→系统时钟72MHz
- 启用必要的外设:
- GPIO:超声波Trig/Echo、红外接收等
- TIM3:通道1/2用于电机PWM,通道4用于超声波测距
- I2C1:MPU6050通信
- USART1:调试信息输出
- 生成代码时选择"生成独立的.c/.h文件"
经验分享:在CubeMX中配置外设时,建议为每个功能模块创建单独的GPIO组,并添加有意义的标签,这样生成的代码可读性更好。例如将超声波相关引脚标记为"USONIC_TRIG"和"USONIC_ECHO"。
3.3 基础驱动实现
3.3.1 电机PWM驱动
// 电机初始化 void Motor_Init(void) { HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); // 左电机PWM HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2); // 右电机PWM // 设置初始占空比为0 __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 0); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, 0); } // 设置电机速度 void Motor_SetSpeed(int16_t left, int16_t right) { // 限幅处理 left = (left > 1000) ? 1000 : ((left < -1000) ? -1000 : left); right = (right > 1000) ? 1000 : ((right < -1000) ? -1000 : right); // 设置PWM占空比 if(left >= 0) { HAL_GPIO_WritePin(MOTOR_L_DIR_GPIO_Port, MOTOR_L_DIR_Pin, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, left); } else { HAL_GPIO_WritePin(MOTOR_L_DIR_GPIO_Port, MOTOR_L_DIR_Pin, GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, -left); } // 右电机同理 ... }3.3.2 超声波测距实现
超声波测距的关键是精确测量高电平持续时间。我采用输入捕获方式实现:
// 超声波测距函数 float USonic_GetDistance(void) { static uint32_t start_time = 0, end_time = 0; float distance = 0; // 发送10us的触发信号 HAL_GPIO_WritePin(USONIC_TRIG_GPIO_Port, USONIC_TRIG_Pin, GPIO_PIN_SET); delay_us(10); HAL_GPIO_WritePin(USONIC_TRIG_GPIO_Port, USONIC_TRIG_Pin, GPIO_PIN_RESET); // 等待回波信号 while(HAL_GPIO_ReadPin(USONIC_ECHO_GPIO_Port, USONIC_ECHO_Pin) == GPIO_PIN_RESET); start_time = HAL_GetTick(); while(HAL_GPIO_ReadPin(USONIC_ECHO_GPIO_Port, USONIC_ECHO_Pin) == GPIO_PIN_SET); end_time = HAL_GetTick(); // 计算距离(声速340m/s) distance = (end_time - start_time) * 340.0 / 2.0 / 1000.0; return distance; }4. 智能小车核心功能实现
4.1 多模式控制框架设计
为了让小车支持多种控制方式,我设计了一个状态机框架:
typedef enum { MODE_MANUAL, // 手动遥控模式 MODE_LINE_TRACE,// 循迹模式 MODE_AVOID, // 避障模式 MODE_COUNT } RunMode_t; RunMode_t current_mode = MODE_MANUAL; void MainControlLoop(void) { static uint32_t last_ticks = 0; if(HAL_GetTick() - last_ticks < 50) return; // 20Hz控制频率 last_ticks = HAL_GetTick(); switch(current_mode) { case MODE_MANUAL: ManualMode_Handler(); break; case MODE_LINE_TRACE: LineTrace_Handler(); break; case MODE_AVOID: Avoid_Handler(); break; default: break; } }4.2 红外循迹算法实现
五路红外循迹模块的布局为"左2、中1、右2",算法设计如下:
#define LINE_SENSOR_COUNT 5 #define BASE_SPEED 600 void LineTrace_Handler(void) { uint8_t sensor_state[LINE_SENSOR_COUNT]; int16_t left_speed = BASE_SPEED, right_speed = BASE_SPEED; // 读取传感器状态 sensor_state[0] = HAL_GPIO_ReadPin(LINE_L2_GPIO_Port, LINE_L2_Pin); sensor_state[1] = HAL_GPIO_ReadPin(LINE_L1_GPIO_Port, LINE_L1_Pin); sensor_state[2] = HAL_GPIO_ReadPin(LINE_M_GPIO_Port, LINE_M_Pin); sensor_state[3] = HAL_GPIO_ReadPin(LINE_R1_GPIO_Port, LINE_R1_Pin); sensor_state[4] = HAL_GPIO_ReadPin(LINE_R2_GPIO_Port, LINE_R2_Pin); // PID控制算法 static float last_error = 0, integral = 0; float error = 0; // 计算偏差 if(!sensor_state[2]) { // 中间传感器在黑线上 error = 0; } else if(!sensor_state[1]) { error = -1; } else if(!sensor_state[3]) { error = 1; } else if(!sensor_state[0]) { error = -2; } else if(!sensor_state[4]) { error = 2; } // PID计算 float Kp = 150.0, Ki = 0.5, Kd = 80.0; integral += error; float output = Kp * error + Ki * integral + Kd * (error - last_error); last_error = error; // 应用控制量 left_speed += output; right_speed -= output; Motor_SetSpeed(left_speed, right_speed); }4.3 超声波避障策略
避障模式采用有限状态机设计,包含"前进"、"避障"、"旋转"三个状态:
typedef enum { AVOID_FORWARD, AVOID_BACK_OFF, AVOID_TURN } AvoidState_t; AvoidState_t avoid_state = AVOID_FORWARD; uint32_t avoid_timer = 0; void Avoid_Handler(void) { float distance = USonic_GetDistance(); switch(avoid_state) { case AVOID_FORWARD: if(distance < 20.0) { // 检测到障碍物 Motor_SetSpeed(-400, -400); // 后退 avoid_state = AVOID_BACK_OFF; avoid_timer = HAL_GetTick(); } else { Motor_SetSpeed(600, 600); // 前进 } break; case AVOID_BACK_OFF: if(HAL_GetTick() - avoid_timer > 500) { // 后退500ms Motor_SetSpeed(400, -400); // 左转 avoid_state = AVOID_TURN; avoid_timer = HAL_GetTick(); } break; case AVOID_TURN: if(HAL_GetTick() - avoid_timer > 800) { // 旋转800ms avoid_state = AVOID_FORWARD; } break; } }5. 系统优化与调试技巧
5.1 电源噪声抑制实践
在调试过程中,电机启停经常导致系统复位,经过排查发现是电源问题。采取的解决方案:
- 在电机电源输入端并联470μF电解电容和0.1μF陶瓷电容组合
- 为STM32的VDDA引脚增加LC滤波电路(10μH电感+1μF电容)
- 在软件上实现电机软启动,避免电流突变
// 电机软启动实现 void Motor_SoftStart(int16_t target_left, int16_t target_right, uint16_t duration_ms) { int16_t step_left = target_left / (duration_ms / 10); int16_t step_right = target_right / (duration_ms / 10); int16_t current_left = 0, current_right = 0; while(abs(current_left) < abs(target_left) || abs(current_right) < abs(target_right)) { current_left += step_left; current_right += step_right; Motor_SetSpeed(current_left, current_right); HAL_Delay(10); } }5.2 传感器数据滤波处理
原始传感器数据往往包含噪声,需要进行滤波处理。对于不同类型的传感器,我采用不同的滤波策略:
- 超声波测距:中值滤波+滑动平均
#define USONIC_FILTER_SIZE 5 float USonic_FilteredDistance(void) { static float buffer[USONIC_FILTER_SIZE] = {0}; static uint8_t index = 0; float temp; // 获取新数据 buffer[index] = USonic_GetDistance(); index = (index + 1) % USONIC_FILTER_SIZE; // 排序找中值 for(uint8_t i=0; i<USONIC_FILTER_SIZE-1; i++) { for(uint8_t j=i+1; j<USONIC_FILTER_SIZE; j++) { if(buffer[i] > buffer[j]) { temp = buffer[i]; buffer[i] = buffer[j]; buffer[j] = temp; } } } return buffer[USONIC_FILTER_SIZE/2]; }- MPU6050姿态数据:互补滤波
void MPU6050_GetAngle(float *pitch, float *roll) { static float angle_pitch = 0, angle_roll = 0; float accel[3], gyro[3]; float dt = 0.02; // 50Hz采样率 // 读取原始数据 MPU6050_ReadAccel(accel); MPU6050_ReadGyro(gyro); // 加速度计计算角度 float acc_pitch = atan2(accel[1], accel[2]) * 180/PI; float acc_roll = atan2(-accel[0], sqrt(accel[1]*accel[1] + accel[2]*accel[2])) * 180/PI; // 互补滤波 angle_pitch = 0.98 * (angle_pitch + gyro[0]*dt) + 0.02 * acc_pitch; angle_roll = 0.98 * (angle_roll + gyro[1]*dt) + 0.02 * acc_roll; *pitch = angle_pitch; *roll = angle_roll; }5.3 无线调试技巧
为了便于现场调试,我实现了基于HC-05蓝牙模块的无线调试功能:
- 硬件连接:HC-05的TXD→STM32的USART2_RX,HC-05的RXD→STM32的USART2_TX
- 软件配置:
// 初始化USART2 void DebugUART_Init(void) { huart2.Instance = USART2; huart2.Init.BaudRate = 9600; huart2.Init.WordLength = UART_WORDLENGTH_8B; huart2.Init.StopBits = UART_STOPBITS_1; huart2.Init.Parity = UART_PARITY_NONE; huart2.Init.Mode = UART_MODE_TX_RX; huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart2.Init.OverSampling = UART_OVERSAMPLING_16; HAL_UART_Init(&huart2); } // 发送调试信息 void Debug_Printf(const char *fmt, ...) { char buffer[128]; va_list args; va_start(args, fmt); vsnprintf(buffer, sizeof(buffer), fmt, args); va_end(args); HAL_UART_Transmit(&huart2, (uint8_t *)buffer, strlen(buffer), HAL_MAX_DELAY); }使用时可以通过手机蓝牙串口APP实时查看传感器数据和控制参数,极大提高了调试效率。