L298N电机驱动+PWM调速:STM32智能风扇速度控制实战教程

最近在工作室捣鼓一个温控小项目,需要精确控制风扇转速,正好把之前用过的L298N模块和STM32的PWM功能重新梳理了一遍。我发现很多朋友在初次接触电机驱动时,往往会被L298N上那几个跳线帽、电源接口搞得一头雾水,而PWM调速的参数配置又直接影响到电机的运行平稳性和噪音水平。这篇文章,我就结合一个实际的智能风扇控制案例,把从硬件接线到软件调试的完整流程拆解清楚,特别是那些容易踩坑的细节。无论你是想做个智能散热装置、小型机器人底盘,还是其他需要精确控制直流电机的项目,这里面的思路和代码都能直接拿来用。

1. 项目核心:为什么是L298N与PWM?

在做嵌入式开发时,直接拿单片机的IO口去驱动一个小电机,可能勉强转得起来,但一旦遇到功率稍大、需要正反转或者精确调速的场景,就力不从心了。STM32的GPIO引脚驱动能力通常在几十毫安,而一个普通的直流电机启动电流轻松达到几百毫安,这就是我们需要电机驱动模块的原因。

L298N可以看作一个“电流放大器”,它接收单片机发出的微弱控制信号,然后从外部电源(比如12V适配器或电池组)取电,输出足够大的电流来驱动电机。它的核心是一个双H桥电路,这个结构允许它控制两个直流电机,并且能让每个电机实现正转、反转和刹车(快速停止)功能。

而PWM(脉冲宽度调制)则是调速的关键。简单理解,它不是通过改变电压来调速(那样效率很低),而是通过高速开关来控制电机通电时间的比例。比如,50%的占空比意味着在一个周期内,一半时间通电,一半时间断电。由于电机线圈的惯性,这种高速通断的结果就是电机以全速的一半左右平稳旋转。PWM的频率选择很有讲究,太低电机会抖动并发出刺耳的噪音,太高则可能超出驱动芯片的响应能力。对于直流有刷电机,1kHz到20kHz是一个比较常用的范围。

提示:L298N模块上通常有多个供电接口。驱动逻辑部分(给芯片本身供电)需要5V,可以从单片机取电;而电机动力部分则需要接入更高的电压(如12V)以获得足够的扭矩和转速。务必分清,接错可能烧毁模块或单片机。

所以,这个组合的经典之处在于:L298N解决了“能不能驱动”的动力问题,而PWM则解决了“驱动得多好”的质量问题。下面我们就从硬件连接开始,一步步搭建系统。

2. 硬件连接:厘清L298N的两种工作模式

拿到一个L298N模块,你会看到一排排的接线端子和跳线帽。别慌,我们化繁为简,只关注控制一个电机所需的必要连接。这里必须搞清楚它的两种使能模式,这直接决定了你能否进行PWM调速。

模式一:使能端跳线帽在位(默认状态) 这是最简单的方式。模块上的ENAENB(分别对应电机A和B的使能端)插着跳线帽,这意味着使能端被直接拉高,电机始终处于“使能”状态。此时,你只需要通过IN1IN2(对应电机A)或IN3IN4(对应电机B)输入高低电平来控制转向,电机将以全速运行。这种方式下,PWM引脚是无效的。

模式二:拔掉使能端跳线帽,接入PWM信号 这才是我们实现调速需要的模式。以控制电机A为例:

  1. 拔掉ENA上的跳线帽。
  2. 将STM32的一个具有PWM输出功能的引脚(如PA8, PB5等)连接到ENA端子。
  3. IN1IN2连接STM32的两个普通GPIO,用于控制方向。
  4. 电机的两条线接到OUT1OUT2
  5. 电源部分:12VGND接外部动力电源(如12V电池),模块上的5V输出口可以反过来给STM32板子供电(如果板子支持5V输入),或者单独给STM32供电并共地。

为了更直观,我将一个典型连接配置整理成下表:

模块接口连接目标说明与注意事项
12V外部电源正极 (7V-35V)电机动力电源,电压决定电机最高转速。
GND外部电源负极 & STM32 GND必须共地! 这是信号参考的基础。
5V(可选) STM32 VCC可为单片机供电,仅当输入电压<12V时使用。若输入电压高,建议单独给MCU供电。
ENASTM32 PWM引脚 (如PB5)调速关键,拔掉跳线帽后才有效。
IN1STM32 GPIO (如PB13)方向控制信号线1。
IN2STM32 GPIO (如PB14)方向控制信号线2。
OUT1 & OUT2直流电机两极无正负之分,转向由IN1/IN2逻辑决定。

方向控制逻辑很简单,可以这样记忆:

  • IN1=1, IN2=0 -> 电机正转
  • IN1=0, IN2=1 -> 电机反转
  • IN1=0, IN2=0IN1=1, IN2=1 -> 电机刹车(快速停止)

硬件连好后先别急着写代码,用杜邦线手动给IN1/IN2高低电平,看看电机是否按预期正反转,这能排除大部分接线错误。

3. STM32的PWM配置与波形生成

硬件通路打通后,我们来解决“信号源”的问题——让STM32产生一路频率和占空比都可调的PWM波。STM32的定时器功能非常强大,PWM只是其众多功能之一。这里我以最常用的通用定时器TIM3的通道2(对应引脚PB5)为例,展示如何从零配置。

首先,你需要明确两个核心参数:ARR(自动重装载值)PSC(预分频器)。它们共同决定了PWM的频率。

  • 频率公式PWM_Freq = System_CLK / ((PSC + 1) * (ARR + 1)) 假设系统时钟是72MHz,我们想要一个10kHz的PWM,可以设PSC=71ARR=99,计算得:72,000,000 / (72 * 100) = 10,000 Hz。

  • 占空比:由CCR(捕获/比较寄存器)的值决定。占空比 = CCR / (ARR + 1)。 当ARR=99时,设置CCR=30,占空比就是30%。

理解了原理,看代码就清晰了。下面是一个完整的初始化函数:

/**
  * @brief  初始化TIM3的通道2(PB5)为PWM输出模式
  * @param  arr: 自动重装载值,决定PWM周期
  * @param  psc: 时钟预分频系数
  * @retval 无
  */
void TIM3_PWM_Init(uint16_t arr, uint16_t psc)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct = {0};
    TIM_OCInitTypeDef TIM_OCInitStruct = {0};

    // 1. 开启时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // TIM3挂在APB1总线
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); // 开启GPIOB和复用功能时钟

    // 2. 配置PB5为复用推挽输出(用于PWM)
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;  // 复用推挽输出
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStruct);

    // 3. 部分重映射,将TIM3_CH2映射到PB5(对于F103,有些通道需要重映射)
    GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE);

    // 4. 初始化定时器时基单元
    TIM_TimeBaseInitStruct.TIM_Period = arr;         // 自动重装载值ARR
    TIM_TimeBaseInitStruct.TIM_Prescaler = psc;      // 预分频器PSC
    TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟分频
    TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStruct);

    // 5. 初始化PWM输出通道
    TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM2;       // 模式2:当计数器小于CCR时输出有效电平
    TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; // 使能输出
    TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; // 输出极性高:有效电平为高
    TIM_OC2Init(TIM3, &TIM_OCInitStruct); // 初始化通道2

    // 6. 使能预装载寄存器(保证CCR值在更新事件时才生效,避免中间出现毛刺)
    TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable);

    // 7. 使能定时器
    TIM_Cmd(TIM3, ENABLE);
}

初始化之后,在main函数里调用TIM3_PWM_Init(999, 71),就能在PB5引脚上得到一个频率约为1kHz(72M/(72*1000))的PWM波。此时占空比默认为0,电机不转。我们需要动态改变占空比来调速,使用库函数TIM_SetCompare2(TIM3, compareValue)即可,其中compareValue就是CCR的值,范围在0到ARR(这里是999)之间。

注意:PWM频率的选择需要权衡。频率太低(如几十Hz),电机会明显抖动和啸叫;频率太高(如超过20kHz),虽然人耳听不到噪音,但L298N内部的开关损耗会增加,可能发热更严重。对于普通直流风扇电机,5kHz到10kHz是个不错的起点。

4. 软件架构:将控制逻辑模块化

有了硬件和PWM基础,我们就可以构建一个稳定、易扩展的智能风扇控制程序了。好的软件架构能让后续添加新功能(如红外遥控、温度反馈)变得轻松。我习惯将代码分为驱动层、应用层和主循环。

驱动层 封装最底层的操作,比如电机驱动和PWM设置。我们创建一个motor.c文件:

// motor.h 中定义
typedef enum {
    MOTOR_STOP = 0,
    MOTOR_CW,   // 顺时针(正转)
    MOTOR_CCW   // 逆时针(反转)
} Motor_Dir_t;

typedef struct {
    uint16_t speed;     // 速度值,对应CCR
    Motor_Dir_t dir;    // 方向
} Motor_State_t;

// motor.c 中的关键函数
void Motor_Init(void) {
    // 初始化控制方向的GPIO (PB13, PB14为输出)
    GPIO_Init(...);
    // 初始化PWM (PB5)
    TIM3_PWM_Init(999, 71);
}

void Motor_Set(Motor_Dir_t direction, uint16_t speed) {
    // 设置方向
    switch(direction) {
        case MOTOR_CW:
            GPIO_SetBits(GPIOB, GPIO_Pin_13);
            GPIO_ResetBits(GPIOB, GPIO_Pin_14);
            break;
        case MOTOR_CCW:
            GPIO_ResetBits(GPIOB, GPIO_Pin_13);
            GPIO_SetBits(GPIOB, GPIO_Pin_14);
            break;
        default: // STOP
            GPIO_ResetBits(GPIOB, GPIO_Pin_13);
            GPIO_ResetBits(GPIOB, GPIO_Pin_14);
            break;
    }
    // 设置速度(限制速度在安全范围内)
    if(speed > MAX_SPEED) speed = MAX_SPEED;
    TIM_SetCompare2(TIM3, speed);
    current_state.speed = speed;
    current_state.dir = direction;
}

应用层 则根据具体的业务逻辑调用驱动层。例如,响应红外遥控器的按键:

// 在红外解码中断或查询函数中
void IR_Handler(uint8_t key_code) {
    switch(key_code) {
        case KEY_POWER:
            Motor_Set(MOTOR_STOP, 0);
            break;
        case KEY_SPEED_UP:
            current_speed += SPEED_STEP;
            Motor_Set(current_state.dir, current_speed);
            break;
        case KEY_SPEED_DOWN:
            current_speed -= SPEED_STEP;
            Motor_Set(current_state.dir, current_speed);
            break;
        case KEY_REVERSE:
            // 反转方向,保持当前速度
            Motor_Dir_t new_dir = (current_state.dir == MOTOR_CW) ? MOTOR_CCW : MOTOR_CW;
            Motor_Set(new_dir, current_state.speed);
            break;
    }
}

主循环 保持简洁,主要负责状态监控和非实时任务。例如,可以加入一个简单的温度控制逻辑:

float read_temperature(void); // 假设的温感读取函数

int main(void) {
    System_Init(); // 系统时钟等初始化
    Motor_Init();
    IR_Init();     // 红外初始化
    // ... 其他外设初始化

    Motor_State_t auto_state = {0};
    uint32_t last_check_time = 0;

    while(1) {
        // 1. 处理红外遥控(中断方式更好,这里仅为示例)
        IR_Process();

        // 2. 每1秒检查一次温度,实现简易温控
        if(HAL_GetTick() - last_check_time > 1000) {
            last_check_time = HAL_GetTick();
            float temp = read_temperature();
            if(temp > 30.0f) {
                auto_state.speed = 800; // 高温全速
            } else if(temp > 26.0f) {
                auto_state.speed = 500; // 中速
            } else if(temp > 22.0f) {
                auto_state.speed = 200; // 低速
            } else {
                auto_state.speed = 0;   // 停止
            }
            // 只有在自动模式下才应用温控速度
            if(current_mode == MODE_AUTO) {
                Motor_Set(auto_state.dir, auto_state.speed);
            }
        }

        // 3. 其他任务,如刷新显示等
        Display_Update();
    }
}

这种分层结构的好处是显而易见的:底层硬件更换(比如换一个电机驱动芯片)只需修改motor.c;业务逻辑变更(比如增加手机蓝牙控制)只需在应用层添加;它们之间通过清晰的接口耦合,大大提高了代码的可维护性。

5. 调试与优化:让风扇转得更“聪明”

代码烧录进去,电机能转起来只是第一步。要让整个系统运行得稳定、安静、高效,离不开调试和优化。这里分享几个我实践中总结的关键点。

首先,用示波器观察PWM波形。 这是最直接的调试手段。将探头接到ENA引脚(或STM32的PWM输出脚),你应该能看到一个干净、稳定的方波。检查以下几点:

  • 频率:是否与设定值一致?
  • 幅值:高电平是否接近3.3V(STM32的IO电平)?如果被拉低,可能是负载太重或接线问题。
  • 占空比变化:当你调用TIM_SetCompare2改变速度值时,波形的脉宽是否平滑变化?

其次,关注电机启动和停止的瞬态。 直流电机在启动瞬间的电流(堵转电流)可能是额定电流的5-10倍。直接给一个高占空比的PWM可能导致L298N过流保护甚至损坏。一个实用的技巧是软启动

void Motor_Soft_Start(uint16_t target_speed, uint16_t duration_ms) {
    uint16_t step = target_speed / (duration_ms / 10); // 假设每10ms增加一次
    for(uint16_t s = 0; s < target_speed; s += step) {
        TIM_SetCompare2(TIM3, s);
        delay_ms(10); // 使用系统滴答延时,避免阻塞
    }
    TIM_SetCompare2(TIM3, target_speed); // 确保达到目标值
}

同样,急停(突然将占空比设为0)可能产生反电动势。更好的做法是先短接电机两端(让IN1IN2同时为高或低,实现刹车),再逐步降低PWM。

第三,处理噪音和振动。 如果电机在低速时抖动严重或发出“滋滋”声,可以尝试:

  1. 提高PWM频率:将频率提升到人耳听不到的20kHz以上。
  2. 使用互补PWM和死区插入(高级技巧):对于更高级的驱动,这可以改善波形,但对L298N这种简单驱动模块意义不大。
  3. 检查电源:电源功率不足或纹波过大,是噪音和转速不稳的常见原因。确保你的电源适配器能提供足够的电流,并在电机电源端并联一个大容量电解电容(如1000uF)和一个小容量瓷片电容(如0.1uF)来滤除高低频噪声。

最后,引入反馈机制。 基础的开环控制(我们设定PWM,假设电机就会按比例转)受负载、电压变化影响大。可以尝试:

  • 加装编码器:测量电机实际转速,构成闭环PID控制,转速将极其稳定。
  • 电流检测:在电机回路串联一个小阻值采样电阻,通过运放放大后由STM32的ADC读取,实现过流保护或力矩控制。

调试过程中,可能会遇到电机只震动不转、一个方向正常另一个方向不动、调速范围窄等问题。大部分问题都可以通过以下清单排查:

  • [ ] 电源检查:电机电源电压是否足够?STM32与L298N共地了吗?
  • [ ] 使能端ENA跳线帽拔了吗?PWM信号线接对了吗?
  • [ ] 逻辑电平:用万用表量一下IN1/IN2,在程序控制下电平是否正确翻转?
  • [ ] PWM信号:用示波器或LED(接在PWM引脚上,看亮度变化)确认PWM是否有输出且占空比可变?
  • [ ] 负载:电机是否被卡住?空载和带载表现是否不同?

把这些问题都过一遍,基本上都能找到症结所在。嵌入式开发就是这样,理论搭建框架,调试填充细节。当你听到风扇随着PWM占空比平滑地改变风量,那种成就感就是对我们折腾硬件、调试代码的最好回报。

Logo

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

更多推荐