1. 智能小车进阶之路:从基础PWM到多传感器融合

做智能小车最让人上头的时刻,就是看着自己亲手焊接的小车在地上稳稳跑起来,还能聪明地避开障碍物。我记得第一次成功让小车用PWM平滑调速时,那种成就感比喝了十杯咖啡还提神。今天我就把自己在STM32智能小车开发中踩过的坑和积累的经验,特别是从基础PWM调速到多传感器融合控制的进阶过程,毫无保留地分享给大家。

很多初学者刚开始接触智能小车时,往往只满足于让小车简单动起来,但很快就会遇到瓶颈——为什么小车总是跑不直?为什么避障反应那么迟钝?为什么速度控制不精准?这些问题其实都需要通过PWM精细控制和多传感器融合来解决。智能小车的真正魅力在于让多个传感器协同工作,像给小车装上了"眼睛"和"耳朵",让它能感知环境并做出智能决策。

接下来,我会带你一步步深入STM32的PWM编程,然后融合超声波、红外等传感器,最终实现一个真正智能的小车控制系统。无论你是刚入门的新手,还是已经有一定经验的开发者,相信这些实战经验都能帮你少走弯路。

2. PWM调速原理与STM32实现详解

2.1 PWM到底是什么?其实比想象中简单

PWM(脉冲宽度调制)听起来很高大上,但其实原理特别简单。想象一下你在用开关控制一盏灯:如果快速开关开关,灯就会闪烁;如果开的时间长、关的时间短,灯就显得亮;如果开的时间短、关的时间长,灯就显得暗。PWM就是这个原理,通过调整高电平和低电平的时间比例(占空比)来模拟不同的电压水平。

对于电机控制来说,PWM的占空比直接决定了电机的转速。占空比越大,电机转速越快;占空比越小,转速越慢。STM32的定时器外设可以产生非常精确的PWM信号,这是我们能够精细控制电机的基础。

在实际项目中,我通常使用72MHz的系统时钟,通过预分频器和自动重装载寄存器来设置PWM频率。对于直流电机控制,一般选择1-10kHz的PWM频率比较合适。频率太低电机会有噪音,频率太高则驱动模块可能响应不过来。

2.2 STM32定时器配置实战

下面是一个完整的PWM初始化代码,我用的是TIM1定时器,它可以产生4路PWM信号,正好控制两个电机的正反转:

void PWM_Init(uint32_t freq)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct = {0};
    TIM_OCInitTypeDef TIM_OCInitStruct = {0};
    
    // 使能时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_TIM1, ENABLE);
    
    // 配置GPIOA的8、9、10、11引脚为复用推挽输出
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10 | GPIO_Pin_11;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStruct);
    
    // 计算预分频器和自动重装载值
    uint32_t prescaler = 72 - 1;  // 72分频,计数频率1MHz
    uint32_t period = (1000000 / freq) - 1;  // 根据频率计算ARR值
    
    // 配置定时器时基
    TIM_TimeBaseStruct.TIM_Period = period;
    TIM_TimeBaseStruct.TIM_Prescaler = prescaler;
    TIM_TimeBaseStruct.TIM_ClockDivision = 0;
    TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStruct);
    
    // 配置PWM模式
    TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1;
    TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High;
    TIM_OCInitStruct.TIM_Pulse = 0;  // 初始占空比为0
    
    // 初始化四个通道
    TIM_OC1Init(TIM1, &TIM_OCInitStruct);
    TIM_OC2Init(TIM1, &TIM_OCInitStruct);
    TIM_OC3Init(TIM1, &TIM_OCInitStruct);
    TIM_OC4Init(TIM1, &TIM_OCInitStruct);
    
    // 使能预装载寄存器
    TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable);
    TIM_OC2PreloadConfig(TIM1, TIM_OCPreload_Enable);
    TIM_OC3PreloadConfig(TIM1, TIM_OCPreload_Enable);
    TIM_OC4PreloadConfig(TIM1, TIM_OCPreload_Enable);
    
    // 使能定时器和PWM输出
    TIM_Cmd(TIM1, ENABLE);
    TIM_CtrlPWMOutputs(TIM1, ENABLE);
}

这段代码中,我特意把PWM频率做成了参数,这样你可以根据实际需要调整。在实际测试中,我发现5kHz的频率效果很好,既能保证电机运行平稳,又不会对驱动模块造成负担。

2.3 电机控制函数封装

有了PWM输出,接下来需要封装电机控制函数。我设计了一个灵活的控制函数,可以独立控制左右电机的速度和方向:

// 电机控制结构体
typedef struct {
    int16_t left_speed;
    int16_t right_speed;
    uint8_t left_direction;
    uint8_t right_direction;
} Motor_State;

Motor_State car_state;

void Motor_Control(int16_t left_speed, int16_t right_speed)
{
    // 限制速度范围
    left_speed = constrain(left_speed, -1000, 1000);
    right_speed = constrain(right_speed, -1000, 1000);
    
    // 更新状态
    car_state.left_speed = abs(left_speed);
    car_state.right_speed = abs(right_speed);
    car_state.left_direction = (left_speed >= 0) ? FORWARD : BACKWARD;
    car_state.right_direction = (right_speed >= 0) ? FORWARD : BACKWARD;
    
    // 左电机控制
    if (left_speed > 0) {
        TIM_SetCompare1(TIM1, left_speed);  // IN1
        TIM_SetCompare2(TIM1, 0);           // IN2
    } else if (left_speed < 0) {
        TIM_SetCompare1(TIM1, 0);
        TIM_SetCompare2(TIM1, -left_speed);
    } else {
        TIM_SetCompare1(TIM1, 0);
        TIM_SetCompare2(TIM1, 0);
    }
    
    // 右电机控制
    if (right_speed > 0) {
        TIM_SetCompare3(TIM1, right_speed); // IN3
        TIM_SetCompare4(TIM1, 0);           // IN4
    } else if (right_speed < 0) {
        TIM_SetCompare3(TIM1, 0);
        TIM_SetCompare4(TIM1, -right_speed);
    } else {
        TIM_SetCompare3(TIM1, 0);
        TIM_SetCompare4(TIM1, 0);
    }
}

这个函数支持正反转和调速,通过传入正负值来控制方向。我特意加了范围限制,防止PWM值超出合理范围损坏电机。

3. 多传感器数据采集与处理

3.1 超声波传感器精准测距

超声波模块是智能小车的"眼睛",我用的是常见的HC-SR04模块。它的原理很简单:发送一个10μs的触发信号,然后测量回波高电平的持续时间,根据声速计算距离。

但在实际使用中,我发现直接读取距离值会有很多抖动,需要进行滤波处理。下面是我优化后的超声波测距代码:

#define ULTRASONIC_TRIG_PIN    GPIO_Pin_1
#define ULTRASONIC_TRIG_PORT   GPIOB
#define ULTRASONIC_ECHO_PIN    GPIO_Pin_0
#define ULTRASONIC_ECHO_PORT   GPIOB

float Ultrasonic_Measure(void)
{
    static float history[5] = {0};
    static uint8_t index = 0;
    
    // 发送10us触发脉冲
    GPIO_SetBits(ULTRASONIC_TRIG_PORT, ULTRASONIC_TRIG_PIN);
    delay_us(10);
    GPIO_ResetBits(ULTRASONIC_TRIG_PORT, ULTRASONIC_TRIG_PIN);
    
    // 等待回波信号变高
    while (GPIO_ReadInputDataBit(ULTRASONIC_ECHO_PORT, ULTRASONIC_ECHO_PIN) == RESET);
    
    // 开始计时
    uint32_t start_time = TIM5->CNT;
    
    // 等待回波信号变低
    while (GPIO_ReadInputDataBit(ULTRASONIC_ECHO_PORT, ULTRASONIC_ECHO_PIN) != RESET);
    
    uint32_t end_time = TIM5->CNT;
    
    // 计算时间差(单位us)
    uint32_t time_diff = (end_time > start_time) ? (end_time - start_time) : (0xFFFFFFFF - start_time + end_time);
    
    // 计算距离(声速340m/s,除以2因为是往返距离)
    float distance = time_diff * 0.034 / 2.0;
    
    // 中值滤波
    history[index] = distance;
    index = (index + 1) % 5;
    
    // 排序找中值
    float temp[5];
    memcpy(temp, history, sizeof(history));
    
    for (int i = 0; i < 4; i++) {
        for (int j = i + 1; j < 5; j++) {
            if (temp[i] > temp[j]) {
                float swap = temp[i];
                temp[i] = temp[j];
                temp[j] = swap;
            }
        }
    }
    
    return temp[2];  // 返回中值
}

这个函数采用了中值滤波算法,能有效消除异常值的干扰。在实际测试中,滤波后的距离数据稳定性提高了70%以上。

3.2 红外传感器循迹原理

红外循迹模块是小车"看"地线的关键。它通过发射红外光并接收反射光来判断地面颜色。黑色吸收红外光,反射弱;白色反射红外光,反射强。

我通常使用5路红外传感器阵列,这样能更精确地检测路径。每个传感器的数据处理都很重要:

#define IR_SENSOR_COUNT 5

typedef struct {
    GPIO_TypeDef* port;
    uint16_t pin;
    uint8_t value;
    uint8_t threshold;
} IR_Sensor;

IR_Sensor ir_sensors[IR_SENSOR_COUNT];

void IR_Sensors_Init(void)
{
    // 初始化5个红外传感器
    ir_sensors[0] = (IR_Sensor){GPIOA, GPIO_Pin_0, 0, 500};
    ir_sensors[1] = (IR_Sensor){GPIOA, GPIO_Pin_1, 0, 500};
    ir_sensors[2] = (IR_Sensor){GPIOA, GPIO_Pin_2, 0, 500};
    ir_sensors[3] = (IR_Sensor){GPIOA, GPIO_Pin_3, 0, 500};
    ir_sensors[4] = (IR_Sensor){GPIOA, GPIO_Pin_4, 0, 500};
}

uint8_t IR_Read_Pattern(void)
{
    uint8 pattern = 0;
    
    for (int i = 0; i < IR_SENSOR_COUNT; i++) {
        // 读取ADC值并比较阈值
        uint16_t adc_value = ADC_Read(ir_sensors[i].pin);
        ir_sensors[i].value = (adc_value > ir_sensors[i].threshold) ? 1 : 0;
        pattern |= (ir_sensors[i].value << i);
    }
    
    return pattern;
}

每个传感器都需要单独校准阈值,因为即使同一批次的传感器也会有差异。我通常的做法是在黑色和白色表面上分别读取ADC值,然后取中间值作为阈值。

4. 传感器数据融合与智能决策

4.1 多传感器数据融合算法

单一的传感器往往有局限性,比如超声波只能测距但不能分辨颜色,红外能循迹但不能避障。把多个传感器的数据融合起来,就能让小车真正"智能"起来。

我常用的融合方法是加权决策算法,根据不同传感器的重要性分配不同的权重:

typedef struct {
    float ultrasonic_weight;    // 超声波权重
    float ir_weight;            // 红外权重
    float safe_distance;        // 安全距离
} Fusion_Params;

Fusion_Params fusion_params = {0.6, 0.4, 20.0};  // 厘米

uint8_t Sensor_Fusion_Decision(float ultrasonic_dist, uint8_t ir_pattern)
{
    float ultrasonic_score = 0.0;
    float ir_score = 0.0;
    
    // 超声波评分:距离越近,评分越高(危险程度越高)
    if (ultrasonic_dist < fusion_params.safe_distance) {
        ultrasonic_score = 1.0 - (ultrasonic_dist / fusion_params.safe_distance);
    }
    
    // 红外评分:根据偏离路径的程度评分
    switch (ir_pattern) {
        case 0b00100:  // 正好在路径上
            ir_score = 0.0;
            break;
        case 0b00010:
        case 0b01000:
            ir_score = 0.3;
            break;
        case 0b00001:
        case 0b10000:
            ir_score = 0.6;
            break;
        case 0b00000:  // 完全偏离
            ir_score = 1.0;
            break;
        default:
            ir_score = 0.5;
    }
    
    // 加权综合评分
    float total_score = ultrasonic_score * fusion_params.ultrasonic_weight 
                      + ir_score * fusion_params.ir_weight;
    
    // 根据评分做出决策
    if (total_score > 0.7) {
        return EMERGENCY_STOP;
    } else if (total_score > 0.4) {
        return AVOID_OBSTACLE;
    } else {
        return FOLLOW_LINE;
    }
}

这个算法让小车能够根据环境情况做出智能决策。比如当前方有障碍物同时又偏离路径时,它会优先考虑避障。

4.2 状态机实现智能行为控制

为了让小车的行为更加有条理,我使用了状态机模型。状态机让程序结构清晰,容易维护和扩展:

typedef enum {
    STATE_IDLE,          // 空闲状态
    STATE_FOLLOW_LINE,   // 循迹状态
    STATE_AVOID_OBSTACLE, // 避障状态
    STATE_EMERGENCY,     // 紧急状态
    STATE_MANUAL         // 手动控制状态
} Car_State;

Car_State current_state = STATE_IDLE;

void State_Machine_Update(void)
{
    float distance = Ultrasonic_Measure();
    uint8_t ir_pattern = IR_Read_Pattern();
    uint8_t decision = Sensor_Fusion_Decision(distance, ir_pattern);
    
    switch (current_state) {
        case STATE_IDLE:
            if (decision != EMERGENCY_STOP) {
                current_state = STATE_FOLLOW_LINE;
            }
            break;
            
        case STATE_FOLLOW_LINE:
            Follow_Line_Behavior(ir_pattern);
            
            if (decision == EMERGENCY_STOP) {
                current_state = STATE_EMERGENCY;
            } else if (decision == AVOID_OBSTACLE) {
                current_state = STATE_AVOID_OBSTACLE;
            }
            break;
            
        case STATE_AVOID_OBSTACLE:
            Avoid_Obstacle_Behavior(distance);
            
            if (decision == EMERGENCY_STOP) {
                current_state = STATE_EMERGENCY;
            } else if (decision == FOLLOW_LINE) {
                current_state = STATE_FOLLOW_LINE;
            }
            break;
            
        case STATE_EMERGENCY:
            Emergency_Stop();
            
            // 紧急状态需要手动恢复或者自动检测危险解除
            if (distance > fusion_params.safe_distance * 1.5) {
                current_state = STATE_IDLE;
            }
            break;
            
        case STATE_MANUAL:
            // 手动控制模式,忽略自动决策
            break;
    }
}

状态机让每个行为都模块化,比如循迹行为、避障行为都可以单独编写和测试:

void Follow_Line_Behavior(uint8_t ir_pattern)
{
    switch (ir_pattern) {
        case 0b00100:  // 居中
            Motor_Control(800, 800);
            break;
        case 0b00010:  // 稍微偏右
            Motor_Control(600, 800);
            break;
        case 0b01000:  // 稍微偏左
            Motor_Control(800, 600);
            break;
        case 0b00001:  // 偏右较多
            Motor_Control(400, 800);
            break;
        case 0b10000:  // 偏左较多
            Motor_Control(800, 400);
            break;
        case 0b00000:  // 丢失路径
            // 原地旋转寻找路径
            Motor_Control(-600, 600);
            break;
        default:
            Motor_Control(700, 700);
    }
}

void Avoid_Obstacle_Behavior(float distance)
{
    if (distance < 15.0) {
        // 紧急后退
        Motor_Control(-600, -600);
        delay_ms(300);
        
        // 随机选择转向方向
        if (rand() % 2 == 0) {
            Motor_Control(-500, 500);  // 左转
        } else {
            Motor_Control(500, -500);  // 右转
        }
        delay_ms(400);
    } else {
        // 继续前进但减速
        Motor_Control(500, 500);
    }
}

5. 无线控制与实时监控实现

5.1 蓝牙模块配置与通信协议

无线控制让小车摆脱线缆的束缚,我常用HC-05蓝牙模块,性价比高而且稳定。配置蓝牙模块时,波特率一定要和STM32的串口波特率一致,通常用9600或115200。

#define BLUETOOTH_USART USART2

void Bluetooth_Init(uint32_t baudrate)
{
    USART_InitTypeDef USART_InitStruct;
    GPIO_InitTypeDef GPIO_InitStruct;
    
    // 使能时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
    
    // 配置TX引脚(PA2)
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_2;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStruct);
    
    // 配置RX引脚(PA3)
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_3;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStruct);
    
    // 配置串口参数
    USART_InitStruct.USART_BaudRate = baudrate;
    USART_InitStruct.USART_WordLength = USART_WordLength_8b;
    USART_InitStruct.USART_StopBits = USART_StopBits_1;
    USART_InitStruct.USART_Parity = USART_Parity_No;
    USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
    USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_Init(BLUETOOTH_USART, &USART_InitStruct);
    
    // 使能串口和接收中断
    USART_Cmd(BLUETOOTH_USART, ENABLE);
    USART_ITConfig(BLUETOOTH_USART, USART_IT_RXNE, ENABLE);
    
    // 配置NVIC
    NVIC_InitTypeDef NVIC_InitStruct;
    NVIC_InitStruct.NVIC_IRQChannel = USART2_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStruct);
}

设计一个简单的通信协议很重要,我用的协议格式是:命令字+参数。比如"F500"表示前进,速度500;"B300"表示后退,速度300。

5.2 实时数据监控与调试技巧

调试智能小车时,实时监控传感器数据非常重要。我通过蓝牙把关键数据发送到电脑上,用串口助手查看:

void Send_Sensor_Data(void)
{
    char buffer[64];
    float distance = Ultrasonic_Measure();
    uint8_t ir_pattern = IR_Read_Pattern();
    
    sprintf(buffer, "Dist:%.1fcm IR:%02X L:%d R:%d\r\n", 
            distance, ir_pattern, 
            car_state.left_speed, car_state.right_speed);
    
    for (int i = 0; buffer[i] != '\0'; i++) {
        USART_SendData(BLUETOOTH_USART, buffer[i]);
        while (USART_GetFlagStatus(BLUETOOTH_USART, USART_FLAG_TC) == RESET);
    }
}

在调试过程中,我发现几个特别有用的技巧:一是用LED指示灯显示当前状态,不同颜色代表不同状态;二是添加软串口功能,当硬件串口被占用时可以用其他引脚模拟串口;三是使用看门狗定时器,防止程序跑飞时小车失控。

6. 系统优化与性能提升

6.1 PID算法实现精准控制

基础的速度控制往往不够精准,特别是当路面不平或者电池电压变化时。PID控制器能显著改善控制效果,让小车运行更加平稳。

我先实现了简单的P控制器(比例控制),然后逐步加入I(积分)和D(微分)环节:

typedef struct {
    float Kp;           // 比例系数
    float Ki;           // 积分系数
    float Kd;           // 微分系数
    float integral;     // 积分项
    float prev_error;   // 上一次误差
    float output_limit; // 输出限制
} PID_Controller;

PID_Controller speed_pid_left = {0.8, 0.2, 0.1, 0, 0, 1000};
PID_Controller speed_pid_right = {0.8, 0.2, 0.1, 0, 0, 1000};

float PID_Update(PID_Controller* pid, float setpoint, float actual)
{
    float error = setpoint - actual;
    
    // 比例项
    float proportional = pid->Kp * error;
    
    // 积分项(抗积分饱和)
    pid->integral += error;
    if (pid->integral > pid->output_limit) pid->integral = pid->output_limit;
    if (pid->integral < -pid->output_limit) pid->integral = -pid->output_limit;
    float integral = pid->Ki * pid->integral;
    
    // 微分项
    float derivative = pid->Kd * (error - pid->prev_error);
    pid->prev_error = error;
    
    // 计算总输出并限制范围
    float output = proportional + integral + derivative;
    if (output > pid->output_limit) output = pid->output_limit;
    if (output < -pid->output_limit) output = -pid->output_limit;
    
    return output;
}

在实际调参时,我建议先调P系数,让系统有基本响应;然后加D系数抑制超调;最后加I系数消除静差。每个电机最好单独调参,因为两个电机特性可能有细微差别。

6.2 电源管理与低功耗优化

智能小车通常用电池供电,功耗优化很重要。我采取了多种省电措施:一是动态调整系统时钟,在不需要高性能时降低主频;二是使用STM32的睡眠模式,当小车静止时进入低功耗状态;三是优化程序结构,减少不必要的循环和延迟。

void Enter_Low_Power_Mode(void)
{
    // 降低系统时钟
    SystemCoreClockUpdate();
    
    // 关闭不必要的外设时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC, DISABLE);
    
    // 配置睡眠模式
    PWR_EnterSleepMode(PWR_Regulator_LowPower, PWR_SLEEPEntry_WFI);
}

void Wake_From_Low_Power(void)
{
    // 恢复系统时钟
    SystemCoreClockUpdate();
    
    // 重新启用外设时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC, ENABLE);
}

电池电量监测也很重要,我通过ADC读取电池电压,当电压过低时自动减速并报警:

#define BATTERY_ADC_CHANNEL ADC_Channel_4

float Read_Battery_Voltage(void)
{
    // 读取ADC值(12位分辨率)
    uint16_t adc_value = ADC_Read(BATTERY_ADC_CHANNEL);
    
    // 转换为电压(假设使用电阻分压,比例2:1)
    float voltage = (adc_value * 3.3 / 4095.0) * 3.0;
    
    return voltage;
}

void Check_Battery_Level(void)
{
    float voltage = Read_Battery_Voltage();
    
    if (voltage < 6.0) {  // 假设2节锂电池,截止电压3.0V×2
        // 低电量报警
        Beep_Alert(3);
        
        // 限制最大速度
        max_speed = 500;
    } else {
        max_speed = 1000;
    }
}

7. 常见问题排查与实战经验

7.1 硬件问题排查指南

在智能小车开发中,硬件问题往往比软件问题更让人头疼。我总结了一些常见问题的排查方法:

电机不转是最常见的问题。首先检查电源,用万用表测量电池电压,确保电量充足。然后检查L298N的使能引脚是否接高电平,这个坑我踩过好几次。接着检查STM32的PWM输出,可以用示波器或者LED灯测试是否有信号输出。

小车跑偏也是常见问题。先分别测试左右电机,看是否是电机本身的问题。然后检查机械结构,确保轮子安装对称,底盘没有变形。最后可以通过软件补偿,给跑得快的电机减少PWM值。

电源干扰问题很隐蔽但很常见。电机启动时会产生很大的电流冲击,可能导致STM32复位。解决方法是在电源处加一个大电容(1000μF以上)滤波,电机驱动模块的电源最好和STM32的电源分开。

7.2 软件调试技巧

软件调试我习惯用"分而治之"的方法,每个模块单独测试通过后再整合。比如先测试PWM输出,用示波器看波形是否正确;再测试超声波模块,用手在前面移动看距离变化是否合理;最后测试红外传感器,在不同颜色的地面上看读数变化。

串口调试是必备技能。我除了用串口输出调试信息,还实现了通过串口实时调整参数的功能,比如在不重新烧录程序的情况下调整PID参数:

void Process_Command(char* command)
{
    if (strncmp(command, "KP ", 3) == 0) {
        float value = atof(command + 3);
        speed_pid_left.Kp = value;
        speed_pid_right.Kp = value;
        USART_SendString("KP updated\r\n");
    }
    // 其他命令处理...
}

断言机制也很实用,在可能出现问题的地方添加断言检查,一旦条件不满足就立即报警:

#define ASSERT(condition) do { \
    if (!(condition)) { \
        Error_Handler(); \
    } \
} while (0)

void Motor_Control(int16_t left_speed, int16_t right_speed)
{
    ASSERT(left_speed >= -1000 && left_speed <= 1000);
    ASSERT(right_speed >= -1000 && right_speed <= 1000);
    
    // 正常控制代码...
}

7.3 实战经验分享

在多次项目实践中,我积累了一些宝贵经验。首先是代码结构要模块化,把电机控制、传感器读取、决策逻辑等分开,这样调试和维护都方便。其次是参数要可配置,不要把阈值、系数等硬编码在程序里,而是做成可调整的变量。

版本控制很重要,我用Git管理代码,每次重大改动前都提交一次,这样出现问题可以快速回退。文档也要及时更新,记录每个参数的意义和调整方法。

最后是要有耐心,智能小车开发会遇到各种奇怪的问题,比如电池电量不足导致的行为异常、传感器受到环境光干扰、电机线接触不良等等。这些问题都需要耐心排查,有时候放下问题休息一下,回来再看可能就有新思路。

我最成功的一次是在一个小车上实现了完整的自主导航,它能够循迹行驶、自动避障、并通过无线传输实时数据。当看到小车流畅地完成整个任务时,觉得所有的调试和改代码都是值得的。智能小车开发就是这样,既有挑战性又有成就感,每次解决一个问题,小车的"智商"就提高一点

更多推荐