1. L298N驱动板硬件解析与选型指南

L298N电机驱动板可以说是智能小车和机器人项目中最常见的驱动模块之一了,我这些年做过的小车项目几乎都用过它。这块板子的核心是L298N芯片,它是一个双H桥驱动器,可以同时控制两个直流电机或者一个步进电机。

先说说它的核心参数。L298N的工作电压范围是5V到35V,这意味着你可以用各种常见的电池供电,从7.4V的锂电池到12V的铅酸电池都没问题。最大持续电流是2A,峰值可以达到3A,对于大多数小型直流电机来说完全够用。总功耗不超过35W,这个参数在选择电源的时候要特别注意。

引脚功能方面,以控制一个电机为例,需要用到IN1、IN2和ENA三个引脚。IN1和IN2控制电机的方向,ENA则负责调速。OUT1和OUT2接电机两端,电源部分有VM(电机电源)、VCC(逻辑电源)和GND。

在实际选型时,我建议注意这几个要点。首先是散热问题,L298N的发热量不小,一定要选带散热片的版本。其次是保护电路,好的驱动板会内置续流二极管和稳压电路,这对保护你的STM32控制器很重要。最后是接口设计,有些板子直接排针引出,有些用的是接线端子,根据你的项目需求选择。

四驱方案需要两块L298N驱动板,这里有个省钱小技巧:你可以买现成的四驱驱动板,但往往比买两块双驱板更贵。我自己都是直接买两块双驱板组合使用,性价比更高。

2. 四驱系统硬件连接详解

搭建四驱系统时,硬件连接是关键的第一步。我建议采用左右分控的方案:一块L298N控制左侧两个电机,另一块控制右侧两个电机。这样的布局在后续编程控制转向时会方便很多。

电源连接要特别注意。电机电源最好独立供电,不要从STM32取电。我一般会用一块7.4V的锂电池直接接到驱动板的VM引脚,然后用一个降压模块给STM32提供5V电源。记得一定要共地!就是把驱动板的GND和STM32的GND连在一起,这是很多新手容易忽略的点。

信号线连接方面,每块L298N需要6个IO口:4个方向控制(IN1-IN4)和2个PWM使能(ENA、ENB)。四驱总共需要12个IO口,看起来很多,但STM32完全能够胜任。我常用的接法是:左侧驱动板的IN1-IN4接PA0-PA3,ENA、ENB接TIM2的PWM输出;右侧驱动板的IN1-IN4接PB0-PB3,ENA、ENB接TIM3的PWM输出。

在实际布线时,我有几个实用建议。首先用不同颜色的导线区分功能,比如红色接电源,黑色接地,黄色接信号线。其次尽量使用杜邦线连接,方便调试时插拔。最后一定要先断电再接线,我当年就因为带电接线烧过一块驱动板,这都是血的教训。

3. STM32CubeMX配置实战

现在我们来用STM32CubeMX进行配置,这是我最喜欢的部分,因为图形化配置真的能省很多时间。首先新建一个工程,选择你使用的STM32型号,我常用的是STM32F103C8T6,性价比高而且资源足够。

时钟配置是关键第一步。我一般选择外部晶振作为时钟源,然后配置系统时钟为72MHz。这个频率对电机控制来说绰绰有余,而且能提供准确的PWM频率。

GPIO配置方面,我们需要配置12个GPIO口作为方向控制。以左侧驱动板为例,将PA0-PA3配置为GPIO Output,模式推挽输出,上拉,低速。这样配置可以确保信号稳定,而且功耗较低。

定时器配置是PWM输出的核心。我们需要配置两个定时器,比如TIM2和TIM3。以TIM2为例,选择Clock Source为Internal Clock,Channel1和Channel3选择PWM Generation CH1和CH3。

PWM频率的设置很重要。我建议设置在10kHz左右,这个频率既能保证调速平滑,又不会让驱动板发热太严重。计算公式是:PWM频率 = 定时器时钟 / (PSC+1) / (ARR+1)。以72MHz时钟为例,设置PSC=71,ARR=999,得到的频率就是72MHz/72/1000=1kHz。如果需要10kHz,就把ARR设为99。

最后生成代码时,记得选择HAL库,Toolchain/IDE选择你用的开发环境,比如MDK-ARM或TrueStudio。我习惯勾选"Generate peripheral initialization as a pair of .c/.h files per peripheral",这样代码结构更清晰。

4. 电机控制原理深入理解

要写好控制代码,首先得理解电机的工作原理。直流电机的控制其实就两个维度:方向和速度。方向控制通过H桥电路实现,简单说就是通过不同的电平组合来控制电流方向。

具体到L298N,当IN1=高电平、IN2=低电平时,电机正转;IN1=低电平、IN2=高电平时,电机反转;两个都是低电平时,电机自由停止;两个都是高电平时,电机刹车停止。在实际使用中,我建议用正转/反转/停止三种状态就够了,刹车停止用的比较少。

速度控制是通过PWM实现的。PWM的原理就是通过快速开关来控制平均电压。占空比0%相当于0V,电机不转;50%相当于一半电压,半速转动;100%就是全电压,全速转动。这里有个重要概念:PWM频率不能太低,否则电机会有抖动感;也不能太高,否则驱动板会发热。10-20kHz是个甜点区间。

在四驱控制中,差速转向是关键。通过让左右轮以不同速度甚至相反方向转动,可以实现原地转向。比如左轮反转、右轮正转,小车就会原地左转。这个原理在实际编程中会用到。

我还想强调一个容易忽略的点:电机的启动电流很大,往往是正常工作电流的2-3倍。所以在设置PWM占空比时,不要一下子从0跳到很大值,应该逐渐增加,这就是软启动的概念。

5. 方向控制函数实现细节

方向控制函数是电机驱动的基础,写得好能让你后续开发事半功倍。我先定义了一个枚举类型来表示电机方向:

typedef enum {
    MOTOR_STOP = 0,
    MOTOR_FORWARD,
    MOTOR_BACKWARD
} Motor_Direction;

然后为每个电机定义控制引脚。我习惯用结构体来组织这些定义,这样代码更清晰:

typedef struct {
    GPIO_TypeDef* IN1_Port;
    uint16_t IN1_Pin;
    GPIO_TypeDef* IN2_Port;
    uint16_t IN2_Pin;
} Motor_GPIO;

Motor_SetDirection函数是这个部分的核心。我采用switch-case结构来处理不同电机,虽然看起来有点冗长,但是可读性最好:

void Motor_SetDirection(uint8_t motor_id, Motor_Direction dir)
{
    switch(motor_id) {
        case MOTOR_LF:  // 左前电机
            HAL_GPIO_WritePin(IN1_LF_GPIO_Port, IN1_LF_Pin, 
                            (dir == MOTOR_FORWARD) ? GPIO_PIN_SET : GPIO_PIN_RESET);
            HAL_GPIO_WritePin(IN2_LF_GPIO_Port, IN2_LF_Pin,
                            (dir == MOTOR_BACKWARD) ? GPIO_PIN_SET : GPIO_PIN_RESET);
            break;
        // 其他三个电机类似
    }
}

这里有个优化技巧:为了避免电机在切换方向时产生短路,最好先停止电机,稍作延时再切换方向。我通常加一个1ms的延时:

void Motor_SafeSetDirection(uint8_t motor_id, Motor_Direction dir)
{
    Motor_SetDirection(motor_id, MOTOR_STOP);
    HAL_Delay(1);
    Motor_SetDirection(motor_id, dir);
}

在实际项目中,我还会添加一些保护机制,比如防止电机同时正反转,这种状态会短路烧坏驱动板。可以添加状态检查:

if (current_direction != MOTOR_STOP && dir != MOTOR_STOP) {
    // 先停止再切换方向
    Motor_SetDirection(motor_id, MOTOR_STOP);
    HAL_Delay(1);
}

6. PWM速度控制编程技巧

PWM速度控制是让电机平稳运行的关键。首先要在CubeMX中正确配置定时器,我以TIM2为例,配置Channel1和Channel3为PWM模式。

速度控制函数相对简单,但有些细节需要注意。首先是要限制占空比范围,避免设置超出允许的值:

void Motor_SetSpeed(uint8_t motor_id, uint16_t speed)
{
    // 限制速度范围
    if (speed > 999) speed = 999;
    
    switch(motor_id) {
        case MOTOR_LF:
            __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_3, speed);
            break;
        // 其他电机类似
    }
}

这里用的999是因为我设置定时器的ARR值为999,这样speed值就直接对应占空比(speed/1000)。如果你想用百分比表示速度,可以写一个转换函数:

void Motor_SetSpeedPercent(uint8_t motor_id, uint8_t percent)
{
    uint16_t pulse = (percent * 999) / 100;
    Motor_SetSpeed(motor_id, pulse);
}

在实际使用中,我建议添加一个加速度控制。突然的速度变化会让电机产生冲击,也可能导致驱动板过流保护:

void Motor_RampSpeed(uint8_t motor_id, uint16_t target_speed, uint16_t ramp_time)
{
    uint16_t current_speed = Motor_GetCurrentSpeed(motor_id);
    uint16_t step = (target_speed > current_speed) ? 1 : -1;
    
    for (uint16_t s = current_speed; s != target_speed; s += step) {
        Motor_SetSpeed(motor_id, s);
        HAL_Delay(ramp_time / abs(target_speed - current_speed));
    }
}

还有一个重要技巧是死区补偿。有些电机在低占空比时无法启动,需要设置一个启动阈值:

uint16_t Motor_ApplyDeadband(uint16_t speed, uint16_t deadband)
{
    if (speed < deadband && speed > 0) {
        return deadband;
    }
    return speed;
}

7. 四驱动作函数封装

有了基础的方向和速度控制函数,我们就可以封装更高级的四驱动作函数了。这些函数会让你的小车控制变得非常简单。

首先是基本的前进后退函数:

void Car_Forward(uint16_t speed)
{
    // 设置所有电机正转
    for (uint8_t i = 0; i < 4; i++) {
        Motor_SetDirection(i, MOTOR_FORWARD);
    }
    
    // 设置速度
    Car_SetAllSpeed(speed);
}

转向函数有两种实现方式:差速转向和原地转向。差速转向更平滑,适合高速行驶;原地转向更灵活,适合狭窄空间:

// 差速左转
void Car_DifferentialLeft(uint16_t speed, float ratio)
{
    uint16_t left_speed = speed * (1 - ratio);
    uint16_t right_speed = speed;
    
    // 左侧电机正转但速度较慢
    Motor_SetDirection(MOTOR_LF, MOTOR_FORWARD);
    Motor_SetDirection(MOTOR_LB, MOTOR_FORWARD);
    // 右侧电机正常速度
    Motor_SetDirection(MOTOR_RF, MOTOR_FORWARD);
    Motor_SetDirection(MOTOR_RB, MOTOR_FORWARD);
    
    Motor_SetSpeed(MOTOR_LF, left_speed);
    Motor_SetSpeed(MOTOR_LB, left_speed);
    Motor_SetSpeed(MOTOR_RF, right_speed);
    Motor_SetSpeed(MOTOR_RB, right_speed);
}

// 原地左转
void Car_SpinLeft(uint16_t speed)
{
    // 左侧电机反转
    Motor_SetDirection(MOTOR_LF, MOTOR_BACKWARD);
    Motor_SetDirection(MOTOR_LB, MOTOR_BACKWARD);
    // 右侧电机正转
    Motor_SetDirection(MOTOR_RF, MOTOR_FORWARD);
    Motor_SetDirection(MOTOR_RB, MOTOR_FORWARD);
    
    Car_SetAllSpeed(speed);
}

我建议再封装一些实用函数,比如缓启动停止:

void Car_GentleStop(uint16_t ramp_time)
{
    uint16_t current_speed = Motor_GetCurrentSpeed(0); // 获取第一个电机的速度
    for (uint16_t s = current_speed; s > 0; s--) {
        Car_SetAllSpeed(s);
        HAL_Delay(ramp_time / current_speed);
    }
    Car_Stop(); // 最后完全停止
}

还有定距移动函数,这个需要编码器配合,但先预留接口:

void Car_MoveDistance(uint16_t speed, uint32_t distance_mm)
{
    Car_Forward(speed);
    // 这里将来加入编码器计数逻辑
    // while (encoder_count < distance_count) {}
    Car_GentleStop(100);
}

8. 硬件调试与故障排除

调试是项目中最花时间的部分,但掌握一些技巧可以大大缩短调试时间。首先推荐分阶段调试法:先调通一个电机,再调四个电机;先调方向控制,再调速度控制。

第一个常见问题是电机不转。这时候要按照以下步骤排查:检查电源指示灯是否亮起;用万用表测量电机端电压;检查ENA/ENB是否使能(PWM输出);检查INx引脚电平是否正确;最后检查共地是否连接。

第二个常见问题是电机转动方向不对。这个问题很好解决,要么在程序里修改方向控制逻辑,要么直接调换电机两根线。我建议修改程序,因为这样更规范。

第三个问题是电机抖动或噪音大。这通常是PWM频率不合适导致的。可以用示波器查看PWM波形,调整频率到10-20kHz范围内。另外也要检查电源电压是否稳定,电压不足会导致电机无力且抖动。

驱动板发热是第四个常见问题。轻微的发热是正常的,但如果烫手就不对了。检查电机是否堵转,负载是否过大,PWM频率是否过高。必要时加装散热片甚至风扇。

我强烈建议使用调试助手类工具。比如用串口输出电机状态信息,或者用LED指示电机工作状态。还可以添加一个调试函数,通过串口命令控制单个电机:

void Debug_MotorControl(void)
{
    if (收到串口命令) {
        uint8_t motor_id = 获取电机编号;
        uint8_t dir = 获取方向;
        uint16_t speed = 获取速度;
        
        Motor_SetDirection(motor_id, dir);
        Motor_SetSpeed(motor_id, speed);
        
        串口打印当前状态;
    }
}

最后记得做好保护措施。我在电源输入端加了保险丝,在电机端口加了续流二极管,这些都能有效防止硬件损坏。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐