基于天空星GD32F407的L298N电机驱动模块PWM调速实战最近在做一个智能小车项目需要控制两个直流电机的速度和方向L298N这个经典的驱动模块自然就成了首选。但很多刚开始接触嵌入式开发的朋友特别是用国产MCU比如我手头这块天空星GD32F407的朋友可能会觉得从硬件连接到软件驱动有点无从下手。别担心今天我就以“过来人”的身份带大家走一遍完整的流程。咱们的目标很明确用GD32F407的定时器产生PWM波通过L298N模块实现对直流电机的正反转和无级调速。我会把硬件怎么接、代码怎么写、参数怎么算都讲清楚保证你跟着做就能让电机转起来。1. 认识我们的“搭档”L298N模块在动手写代码之前咱们得先了解手里的“兵器”。L298N模块可以说是电机驱动里的“老黄牛”了皮实耐用很多教学和DIY项目里都能看到它。简单来说L298N芯片内部集成了两个H桥电路。你可以把H桥想象成一个聪明的“开关网络”通过控制四个开关晶体管的导通和关断就能轻松改变电机两端的电压方向从而实现电机的正转、反转和刹车。我用的这个模块采购自原文提供的链接主要参数如下驱动电压5V ~ 24V给电机供电驱动电流最大持续2A逻辑电压5V给芯片内部控制逻辑供电控制信号支持3.3V或5V逻辑电平兼容我们的GD32F4073.3V IO模块上有几个关键接口需要特别注意12V / GND这是给电机供电的电源输入端。如果你的电机额定电压是12V就接12V。5V / GND这是模块的逻辑电源。这里有个坑我踩过当电机驱动电压接12V那边在7V到12V之间时模块板载的5V稳压芯片78M05会自动工作这个5V引脚可以输出5V给你的单片机供电。但如果电机电压高于12V比如24V就必须断开板载5V使能跳线帽然后从这个5V引脚外部输入一个5V电源单独给L298N的逻辑部分供电。ENA, ENB这是两个电机的使能端。接高电平电机才能转。如果想用PWM调速就需要把跳线帽拔掉然后接到单片机的PWM输出引脚上。IN1, IN2, IN3, IN4这是控制电机转向的逻辑输入引脚。它们和ENA/ENB配合工作。重要提示无论你怎么供电单片机的地GND必须和L298N模块的GND连接在一起这是保证信号电平有共同参考点的关键否则控制信号会乱套。2. 硬件连接与思路梳理咱们用的是天空星GD32F407开发板目标是控制两个直流电机。连接思路是这样的电源连接我用一个12V的电池或电源适配器正极接模块的“12V”负极接模块的“GND”。因为电压在7-12V范围内所以板载5V使能有效模块的“5V”引脚可以输出5V。我用一根杜邦线将这个“5V”接到开发板的“5V”引脚给开发板供电。这样只需要一个电源就行了。信号连接为了实现PWM调速我们把两个使能端ENA, ENB和四个逻辑输入端IN1-IN4全部连接到单片机的具有PWM输出功能的GPIO引脚上。这样我们就可以通过程序灵活地控制方向和速度。电机连接电机A接在模块的“OUT1”和“OUT2”上电机B接在“OUT3”和“OUT4”上。根据原文提供的代码具体的引脚连接关系已经定义好了我们直接引用GD32F407引脚对应功能连接至L298N模块引脚PB14 (TIMER11_CH0)电机A PWM控制1IN1PB15 (TIMER11_CH1)电机A PWM控制2IN2PB1 (TIMER2_CH3)电机B PWM控制1IN3PB0 (TIMER2_CH2)电机B PWM控制2IN45V逻辑电源来自模块5VGND公共地GND注意ENA和ENB的跳线帽需要拔掉否则PWM信号无法输入。拔掉后ENA和IN1/IN2一组控制电机AENB和IN3/IN4一组控制电机B。在我们的配置里IN1/2/3/4本身已经输出PWM所以ENA和ENB在模块内部应该是被上拉到有效电平了。3. 软件驱动代码解析硬件接好了接下来就是重头戏——写代码。咱们的目标是封装好bsp_L298N.c和bsp_L298N.h两个文件方便在任何工程里调用。我会把关键部分拆开揉碎了讲。3.1 引脚与定时器宏定义 (bsp_L298N.h)头文件里主要做了两件事一是把用到的引脚和定时器通道用宏定义好二是声明三个函数。#ifndef _BSP_L298N_H #define _BSP_L298N_H #include gd32f4xx.h #include board.h // 电机A控制引脚 IN1, IN2 定义 #define RCU_IN1 RCU_GPIOB #define PORT_IN1 GPIOB #define GPIO_IN1 GPIO_PIN_14 #define AF_IN1 GPIO_AF_9 // 复用功能AF9对应TIMER11 #define RCU_IN2 RCU_GPIOB #define PORT_IN2 GPIOB #define GPIO_IN2 GPIO_PIN_15 #define AF_IN2 GPIO_AF_9 // 电机B控制引脚 IN3, IN4 定义 #define RCU_IN3 RCU_GPIOB #define PORT_IN3 GPIOB #define GPIO_IN3 GPIO_PIN_1 #define AF_IN3 GPIO_AF_2 // 复用功能AF2对应TIMER2 #define RCU_IN4 RCU_GPIOB #define PORT_IN4 GPIOB #define GPIO_IN4 GPIO_PIN_0 #define AF_IN4 GPIO_AF_2 // 定时器定义 // 电机A使用 TIMER11 的两个通道 #define RCU_IN1_TIMER RCU_TIMER11 #define BSP_IN1_TIMER TIMER11 #define BSP_IN1_CHANNEL TIMER_CH_0 #define RCU_IN2_TIMER RCU_TIMER11 #define BSP_IN2_TIMER TIMER11 #define BSP_IN2_CHANNEL TIMER_CH_1 // 电机B使用 TIMER2 的两个通道 #define RCU_IN3_TIMER RCU_TIMER2 #define BSP_IN3_TIMER TIMER2 #define BSP_IN3_CHANNEL TIMER_CH_3 #define RCU_IN4_TIMER RCU_TIMER2 #define BSP_IN4_TIMER TIMER2 #define BSP_IN4_CHANNEL TIMER_CH_2 // 函数声明 void L298N_Init(uint16_t pre, uint16_t per); void AO_Control(uint8_t dir, uint32_t speed); void BO_Control(uint8_t dir, uint32_t speed); #endif /* BSP_L298N_H */这里可以看到我们为两个电机分配了两个定时器TIMER11控制电机AIN1, IN2TIMER2控制电机BIN3, IN4。每个定时器的两个通道分别控制一个电机的两个输入引脚。3.2 PWM初始化函数 (L298N_Init)这个函数是核心它负责配置GPIO和定时器让它们输出PWM信号。函数需要两个参数pre预分频值和per周期值它们共同决定了PWM的频率。PWM频率计算公式PWM频率 定时器时钟源 / ((pre 1) * (per 1))天空星GD32F407的APB总线定时器时钟是168MHz。代码里有一行rcu_timer_clock_prescaler_config(RCU_TIMER_PSC_MUL4)这是配置定时器时钟预分频器为4分频所以实际进入定时器的时钟是168MHz / 4 42MHz。因此更准确的公式是PWM频率 42,000,000 / ((pre 1) * (per 1))举个例子如果调用L298N_Init(10, 8000)那么PWM频率 42,000,000 / ((101) * (80001)) ≈ 42,000,000 / (11 * 8001) ≈ 476.8 Hz这个频率几百Hz对于驱动直流电机调速来说是合适的。现在我们一步步看初始化代码void L298N_Init(uint16_t pre, uint16_t per) { timer_parameter_struct timere_initpara {0}; // 定时器基本参数结构体 timer_oc_parameter_struct timer_ocintpara {0}; // 定时器输出比较参数结构体 // 1. 开启时钟 rcu_periph_clock_enable(RCU_IN1_TIMER); // 开启TIMER11时钟 rcu_periph_clock_enable(RCU_IN2_TIMER); // 重复开启也没关系但规范起见应该分开 rcu_periph_clock_enable(RCU_IN3_TIMER); // 开启TIMER2时钟 rcu_periph_clock_enable(RCU_IN4_TIMER); // 开启GPIO时钟 rcu_periph_clock_enable(RCU_IN1); rcu_periph_clock_enable(RCU_IN2); rcu_periph_clock_enable(RCU_IN3); rcu_periph_clock_enable(RCU_IN4); rcu_timer_clock_prescaler_config(RCU_TIMER_PSC_MUL4); // 定时器时钟4分频得到42MHz // 2. 配置GPIO为复用功能(AF) /* 配置AIN1 (PB14) 为复用推挽输出高速 */ gpio_mode_set(PORT_IN1, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_IN1); gpio_output_options_set(PORT_IN1, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_IN1); gpio_af_set(PORT_IN1, AF_IN1, GPIO_IN1); // 复用功能选择AF9 (TIMER11) // ... 类似地配置 IN2, IN3, IN4 // 3. 配置定时器基本参数 timer_deinit(BSP_IN1_TIMER); // 复位定时器到默认状态 // ... 复位其他定时器 timere_initpara.prescaler pre - 1; // 预分频值 timere_initpara.alignedmode TIMER_COUNTER_EDGE; // 边沿对齐模式最常用 timere_initpara.counterdirection TIMER_COUNTER_UP; // 向上计数模式 timere_initpara.period per - 1; // 自动重装载值即PWM周期 timere_initpara.clockdivision TIMER_CKDIV_DIV1; // 时钟分频这里不分频 timere_initpara.repetitioncounter 0; // 重复计数器高级定时器用这里为0 timer_init(BSP_IN1_TIMER, timere_initpara); // ... 初始化其他定时器注意IN1和IN2共用TIMER11所以配置一次即可但代码中为清晰重复初始化 // 4. 配置定时器输出比较参数即PWM输出特性 timer_ocintpara.ocpolarity TIMER_OC_POLARITY_HIGH; // 输出极性高电平有效 timer_ocintpara.outputstate TIMER_CCX_ENABLE; // 使能通道输出 // 以下参数主要针对高级定时器的互补输出我们用不到保持默认 timer_ocintpara.ocnpolarity TIMER_OCN_POLARITY_HIGH; timer_ocintpara.outputnstate TIMER_CCXN_DISABLE; timer_ocintpara.ocidlestate TIMER_OC_IDLE_STATE_LOW; timer_ocintpara.ocnidlestate TIMER_OCN_IDLE_STATE_LOW; // 将输出配置应用到各个通道 timer_channel_output_config(BSP_IN1_TIMER, BSP_IN1_CHANNEL, timer_ocintpara); // ... 配置其他通道 // 5. 设置初始占空比为0电机不转 timer_channel_output_pulse_value_config(BSP_IN1_TIMER, BSP_IN1_CHANNEL, 0); // ... 设置其他通道 // 6. 配置通道为PWM模式1 timer_channel_output_mode_config(BSP_IN1_TIMER, BSP_IN1_CHANNEL, TIMER_OC_MODE_PWM0); // ... 配置其他通道 // PWM模式1当计数器小于比较值时输出有效电平我们设的是高电平否则输出无效电平。 // 7. 使能定时器的自动重装载影子寄存器保证参数更新同步 timer_auto_reload_shadow_enable(BSP_IN1_TIMER); // ... 使能其他定时器 // 8. 对于高级定时器需要主输出使能通用定时器此函数无影响但写上兼容性好 timer_primary_output_config(BSP_IN1_TIMER, ENABLE); // ... 配置其他定时器 // 9. 最后使能定时器开始计数并输出PWM timer_enable(BSP_IN1_TIMER); // ... 使能其他定时器 }这个过程虽然步骤多但逻辑很清晰开时钟 - 配引脚 - 设定时器基础频率/周期 - 配PWM输出特性 - 设初始占空比 - 选择PWM模式 - 使能。按照这个顺序来一般不会出错。3.3 电机控制函数 (AO_Control和BO_Control)初始化完成后我们只需要改变PWM的占空比就能调速改变两个引脚的电平组合就能控制方向。这两个函数封装了这些操作。void AO_Control(uint8_t dir, uint32_t speed) { if(dir 1) // 正转 { // IN1 输出低电平 (占空比0) timer_channel_output_pulse_value_config(BSP_IN1_TIMER, BSP_IN1_CHANNEL, 0); // IN2 输出PWM波占空比由speed决定 timer_channel_output_pulse_value_config(BSP_IN2_TIMER, BSP_IN2_CHANNEL, speed); } else // 反转 { // IN1 输出PWM波 timer_channel_output_pulse_value_config(BSP_IN1_TIMER, BSP_IN1_CHANNEL, speed); // IN2 输出低电平 timer_channel_output_pulse_value_config(BSP_IN2_TIMER, BSP_IN2_CHANNEL, 0); } }BO_Control函数的结构完全类似只是操作的是IN3和IN4。这里就是控制逻辑的核心正转IN10, IN2PWM。电流从OUT2流向OUT1。反转IN1PWM, IN20。电流从OUT1流向OUT2。speed参数范围是0到(per-1)。speed值越大高电平时间越长占空比越大电机转速越快。当speed0时占空比为0%电机停止当speed per-1时占空比接近100%电机全速运行。4. 实战在主函数中调用并验证代码都封装好了用起来就非常简单。在你的main.c文件中可以这样测试#include board.h #include bsp_L298N.h int main(void) { uint32_t speed 0; nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2); // 设置中断优先级分组 board_init(); // 开发板基础初始化系统时钟、延时函数等 bsp_uart_init(); // 初始化串口用于调试打印可选 // 初始化L298N驱动设置PWM频率约为 42M / ((101)*(80001)) ≈ 477Hz L298N_Init(10, 8000); while(1) { // 速度值递增 speed 100; if(speed 8000) speed 0; // 速度超过最大值后归零 // 电机A反转速度递增 AO_Control(0, speed); // 电机B正转速度递增 BO_Control(1, speed); delay_ms(50); // 延时50ms让速度变化可见 } }下载程序到天空星开发板并正确连接L298N模块和电机。你应该能看到两个电机开始旋转并且速度由慢逐渐变快到达最快后又重新从慢开始循环。一个电机正转另一个反转。5. 几个你可能遇到的问题与进阶思考电机不转只有震动或嗡嗡声检查PWM频率频率太低几十Hz电机会有噪音频率太高几十kHz可能超出模块或电机响应范围。建议设置在几百Hz到几kHz之间尝试。调整L298N_Init函数的pre和per参数。检查电源功率确保你的电源12V能提供足够的电流至少大于电机堵转电流。电源功率不足会导致模块重启或电机无力。检查使能端确认ENA和ENB的跳线帽已经拔掉。想控制启停和刹车怎么办目前的代码通过设置speed0可以实现停止。但更可靠的停止是“刹车”即让电机两个输入端短接。你可以扩展控制函数比如void AO_Brake(void) { // IN1 和 IN2 都输出高电平或都输出低电平取决于模块逻辑 timer_channel_output_pulse_value_config(BSP_IN1_TIMER, BSP_IN1_CHANNEL, 0); timer_channel_output_pulse_value_config(BSP_IN2_TIMER, BSP_IN2_CHANNEL, 0); // 或者输出占空比100%的高电平需要根据模块实际效果测试 }如何更平滑地调速在主循环里直接跳跃地改变speed值电机转速变化会显得突兀。在实际项目中我通常会做一个“速度斜坡函数”让speed值缓慢地递增或递减实现软启动和软停止对电机和机械结构都更友好。好了关于用天空星GD32F407驱动L298N进行PWM调速的核心内容就是这些。整个过程从硬件认识到软件封装最后到实际验证希望对你有所帮助。把上面的代码移植到你的工程里理解每一步的作用你就能举一反三用类似的思路去驱动其他需要PWM控制的设备了。