大一新生手搓STM32寻迹小车:从零到跑通,我的HAL库踩坑与填坑实录
第一次点亮STM32开发板时,LED微弱的光芒让我兴奋得像个发现新大陆的孩子。作为刚接触嵌入式开发的菜鸟,我从未想过三个月后自己能独立完成一辆寻迹小车的制作。这段从零开始的旅程,充满了HAL库的迷惑、硬件接线的混乱和深夜调试的崩溃,但最终小车稳稳跑起来的那一刻,所有挫折都化作了成长的养分。
1. 硬件准备:从散件到系统搭建
在淘宝下单的那一刻,我根本分不清L298N和TB6612的区别。店家推荐的"STM32小车全家桶"包含:
- 核心控制:STM32F103C8T6最小系统板(后来发现正点原子的教程更友好)
- 动力系统:L298N驱动模块 + 12V减速电机 ×2 + 万向轮
- 感知部件:TCRT5000红外循迹模块 ×4
- 能源方案:18650电池盒 + AMS1117降压模块
第一个坑出现在电源系统。当我直接把12V电池接入L298N的电源口时,电机疯狂转动但STM32不断重启。后来用万用表测量才发现:
| 测量点 | 理论电压 | 实际电压 |
|---|---|---|
| 电池输出端 | 12V | 11.7V |
| L298N 5V输出 | 5V | 4.3V |
| AMS1117输出 | 3.3V | 2.9V |
提示:L298N的5V输出只能给模块自身供电,不能作为MCU电源。正确做法是电池正极同时接L298N和独立降压模块,分别给电机和STM32供电。
接线时还闹过笑话:把红外模块的AO输出当数字信号接GPIO,结果小车像醉汉一样左右摇摆。直到示波器显示AO输出的是模拟波形,才明白需要接ADC引脚或者改用DO模式。
2. HAL库初体验:从迷茫到顿悟
正点原子的标准库教程看了三遍,却发现自己的开发板只能用HAL库。CubeMX生成的代码像天书一样,尤其是这个神秘结构体:
TIM_HandleTypeDef htim4; htim4.Instance = TIM4; htim4.Init.Prescaler = 72-1; htim4.Init.CounterMode = TIM_COUNTERMODE_UP; htim4.Init.Period = 1000-1;三个关键认知突破:
- HAL库通过句柄(Handle)管理外设,比直接操作寄存器友好
- CubeMX生成的代码只是骨架,需要自己填充肌肉
- HAL_Delay()会阻塞整个系统,后来改用HAL_TIM_PeriodElapsedCallback实现非阻塞延时
最痛苦的PWM配置过程让我理解了时钟树:
- 系统时钟72MHz通过APB1预分频(默认2分频)得到36MHz
- TIM4挂在APB1上,因此需要设置Prescaler=36-1得到1MHz计数频率
- 设置Period=100-1实现10kHz PWM波
// 电机速度控制实战代码 void Set_Motor_Speed(uint8_t speed) { __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, speed); __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_2, speed); }3. 循迹算法:从理论到实践
四路红外模块的安装位置决定了算法效果。最初将模块间距设为2cm,结果小车在弯道频繁出轨。通过实验数据对比:
| 模块间距 | 直道稳定性 | 弯道通过率 | 反应速度 |
|---|---|---|---|
| 1.5cm | ★★★★☆ | ★★☆☆☆ | 快速 |
| 2.0cm | ★★★☆☆ | ★★★☆☆ | 中等 |
| 3.0cm | ★★☆☆☆ | ★★★★☆ | 较慢 |
最终采用非对称安装:中间两模块间隔1.8cm,外侧模块间隔3cm。控制逻辑优化为:
void Track_Handler(void) { uint8_t sensor = (HAL_GPIO_ReadPin(GPIOA, L1_Pin) << 3) | (HAL_GPIO_ReadPin(GPIOA, L2_Pin) << 2) | (HAL_GPIO_ReadPin(GPIOA, R2_Pin) << 1) | HAL_GPIO_ReadPin(GPIOA, R1_Pin); switch(sensor) { case 0b1000: Hard_Left(); break; case 0b1100: Soft_Left(); break; case 0b0110: Forward(); break; case 0b0011: Soft_Right(); break; case 0b0001: Hard_Right(); break; default: Stop(); } }4. 调试血泪史:那些教科书不会教的事
最诡异的bug:小车偶尔会突然加速撞墙。用逻辑分析仪抓取PWM波形后发现,当电池电压低于10V时,L298N的输出会出现异常脉冲。解决方案是增加电压检测:
// 在main.c的while循环中添加 if(HAL_ADC_GetValue(&hadc1) < 2400) { // 对应10V Stop(); HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, GPIO_PIN_SET); }GPIO配置的坑:
- 忘记启用GPIO时钟(__HAL_RCC_GPIOA_CLK_ENABLE())
- 输出模式误设为开漏(GPIO_MODE_OUTPUT_OD)
- 输入模式未启用上拉电阻(GPIO_PULLUP)
最耗时的调试竟然是电机线序问题。当发现左右轮转向相反时,我花了两个小时检查代码,最后发现是L298N的输出线接反了。现在养成了新习惯:
// 电机测试函数 void Motor_Test(void) { Forward(); HAL_Delay(1000); Turn_Left(); HAL_Delay(1000); Turn_Right(); HAL_Delay(1000); Stop(); }5. 进阶优化:从能跑到跑得好
加入PID控制后,小车的循迹效果明显提升。简易位置式PID实现:
typedef struct { float Kp, Ki, Kd; float error, last_error, integral; } PID_TypeDef; void PID_Update(PID_TypeDef *pid, float setpoint, float actual) { pid->error = setpoint - actual; pid->integral += pid->error; float derivative = pid->error - pid->last_error; float output = pid->Kp * pid->error + pid->Ki * pid->integral + pid->Kd * derivative; pid->last_error = pid->error; return output; }参数调试经验值:
- Kp:从0.5开始,观察小车摆动幅度
- Kd:通常取Kp的1/10,抑制超调
- Ki:最后微调,避免积分饱和
电池续航问题通过引入休眠模式解决:当所有传感器10秒未检测到黑线时,自动进入STOP模式,电流从120mA降至2mA。唤醒方式很简单:
// 在中断回调函数中 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if(GPIO_Pin == SENSOR_Pin) { HAL_NVIC_SystemWakeUp(); } }这段开发经历让我明白,嵌入式开发就像在黑暗森林中探险——每解决一个问题就会点亮一块区域,而更多未知的领域仍在等待探索。当看到自己亲手打造的小车稳稳跑完全程时,那种成就感比通关任何游戏都来得真实。