STM32F407ZGT6作为智能小车的大脑,我推荐选择带ST-Link调试器的开发板套装。这块芯片的168MHz主频和1MB Flash完全够用,关键是自带硬件浮点运算单元,这在处理多传感器数据时特别有用。第一次拿到板子时,建议先用STM32CubeMX生成个LED闪烁程序测试基础环境。
配置时钟树时有个小技巧:在RCC设置里把HSE(外部高速时钟)选为Crystal/Ceramic Resonator,然后在Clock Configuration标签页把HCLK调到168MHz。记得勾选USB OTG FS的时钟使能,后期做蓝牙通信可能会用到。我遇到过因为时钟配置错误导致串口波特率偏差的问题,实测用示波器抓波形才定位到问题。
GPIO配置要注意复用功能映射。比如PA9/PA10默认是串口1,但也可以重映射到其他引脚。建议在原理图上把所有要用到的外设引脚先标注出来,避免后期硬件冲突。有个容易踩的坑是JTAG调试口默认占用PB3/PB4,如果要用这些引脚做普通IO,得先禁用JTAG功能。
常用的TCRT5000红外对管建议买集成好的模块,自带比较器输出数字信号。接法上,VCC接3.3V,GND共地,OUT引脚接STM32的普通IO口。我在面包板上测试时发现环境光干扰严重,后来给每个传感器加了遮光罩就好多了。
寻迹算法可以采用简单的阈值判断:
c复制#define TRACK_THRESHOLD 1500
if(ADC_GetValue(IR_LEFT) > TRACK_THRESHOLD){
Motor_TurnLeft(30); // 30%功率转向
}
实际调试中发现不同地面反射率差异很大,最好做成动态阈值。我在EEPROM里存储了5组校准值,上电时自动加载。
HC-SR04模块的Trig引脚建议用PWM触发,Echo回波检测可以用输入捕获。这里有个硬件技巧:在Echo信号线上加个100Ω电阻和104电容滤波,能有效消除误触发。测距算法我推荐中值滤波:
c复制float Get_Distance(void){
uint16_t buf[5];
for(int i=0; i<5; i++){
buf[i] = HC_SR04_Measure();
delay_ms(10);
}
bubble_sort(buf, 5); // 冒泡排序取中间值
return buf[2]*0.034/2; // 换算成厘米
}
L298N模块虽然经典但效率较低,我改用TB6612FNG后功耗降低了40%。PWM频率建议设置在10-20kHz之间,太低会有啸叫声,太高会导致MOS管发热。注意死区时间配置,STM32的硬件死区发生器可以这样设置:
c复制TIM_BDTRInitStruct.TIM_DeadTime = 0x4F; // 约5us死区
TIM_BDTRConfig(TIM1, &TIM_BDTRInitStruct);
车轮编码器建议用AB相正交解码模式,STM32的TIMx编码器接口正好支持:
c复制TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12,
TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
位置式PID的代码框架如下,注意积分限幅和抗饱和处理:
c复制typedef struct{
float Kp,Ki,Kd;
float integral;
float max_output;
}PID_TypeDef;
float PID_Calculate(PID_TypeDef *pid, float target, float feedback){
static float last_error = 0;
float error = target - feedback;
pid->integral += error;
if(pid->integral > 1000) pid->integral = 1000;
if(pid->integral < -1000) pid->integral = -1000;
float output = pid->Kp*error + pid->Ki*pid->integral + pid->Kd*(error-last_error);
last_error = error;
if(output > pid->max_output) output = pid->max_output;
if(output < -pid->max_output) output = -pid->max_output;
return output;
}
HC-05模块的AT指令配置有个坑:波特率必须先用38400配置好参数,再改为115200工作。建议用手机APP测试时,先发送"AT+NAME=MyCar"这样的指令修改设备名,避免多车干扰。数据解析可以采用状态机方式:
c复制enum {CMD_START, CMD_TYPE, CMD_DATA, CMD_END} state;
void BT_Parse(uint8_t ch){
static uint8_t buffer[10], index=0;
switch(state){
case CMD_START:
if(ch == '$') state = CMD_TYPE;
break;
case CMD_TYPE:
if(ch == 'M'){ // 运动指令
state = CMD_DATA;
index = 0;
}
break;
// ...其他状态处理
}
}
0.96寸OLED建议用硬件I2C驱动,实测比软件模拟快3倍。显示刷新时注意局部刷新策略,比如只更新变化的数据区域。字体取模可以用PCtoLCD2002工具,我常用的12x12点阵汉字库占用约5KB Flash。多层菜单实现可以参考下面结构体:
c复制typedef struct{
uint8_t current;
uint8_t max_item;
char *items[5];
void (*action[5])();
}Menu_TypeDef;
void Menu_Next(Menu_TypeDef *menu){
if(++menu->current >= menu->max_item)
menu->current = 0;
OLED_ShowMenu(menu);
}
我用的是时间片轮询+中断的混合架构。关键任务如电机控制放在1ms定时器中断,非实时任务放在主循环:
c复制void TIM2_IRQHandler(void){
static uint16_t tick = 0;
if(TIM_GetITStatus(TIM2, TIM_IT_Update)){
Motor_Control(); // 1ms执行一次
if(++tick >= 100){
tick = 0;
g_100ms_flag = 1; // 100ms标志位
}
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
void main(void){
while(1){
if(g_100ms_flag){
g_100ms_flag = 0;
Sensor_Update();
OLED_Refresh();
}
// ...其他非实时任务
}
}
建议用TPS5430做12V转5V,再用AMS1117-3.3给单片机供电。电流检测可以在电机回路加0.1Ω采样电阻,通过运放放大后送ADC。低电量提醒代码示例:
c复制#define BAT_LOW 3.3
void Check_Battery(void){
float voltage = ADC_GetValue(BAT_PIN)*3.3/4096*2; // 分压比1:1
if(voltage < BAT_LOW){
OLED_ShowWarning("LOW POWER!");
Buzzer_Beep(3);
}
}
逻辑分析仪是调试时序问题的神器,我用的Saleae能同时抓8路信号。有个SPI通信的坑:当CS片选信号下降沿太快时,从设备可能无法正确识别,这时需要在代码里加个微小延时:
c复制void SPI_CS_Low(void){
GPIO_ResetBits(CS_PORT, CS_PIN);
delay_us(1); // 关键延时
}
遇到程序跑飞时,先检查堆栈大小是否足够。在启动文件startup_stm32f40xx.s里,把Stack_Size改为0x1000,Heap_Size改为0x800能解决大部分问题。