
1. 硬件连接与原理分析直流电机控制是嵌入式开发中的经典项目而STM32F103C8T6搭配L298N驱动模块的组合可以说是性价比最高的入门方案之一。我第一次用这套系统做小车底盘控制时发现它比想象中要稳定可靠得多。1.1 L298N驱动模块解析L298N内部集成了两个完整的H桥电路这意味着它可以同时驱动两个直流电机。每个H桥都有三个关键引脚IN1/IN2控制电机转向的逻辑输入ENAPWM调速使能端注意要拔掉跳线帽才能启用PWM控制实际接线时有个容易踩的坑如果电机不转先检查L298N的5V供电选择跳线。当使用12V驱动电压时务必短接板载5V稳压芯片的使能跳线否则逻辑电路无法工作。1.2 STM32引脚分配以STM32F103C8T6为例推荐这样连接/* 电机1控制引脚 */ #define MOTOR1_IN1_PIN GPIO_Pin_0 // PA0 #define MOTOR1_IN2_PIN GPIO_Pin_1 // PA1 #define MOTOR1_PWM_PIN GPIO_Pin_2 // PA2 (TIM2_CH3) /* 电机2控制引脚 */ #define MOTOR2_IN1_PIN GPIO_Pin_3 // PA3 #define MOTOR2_IN2_PIN GPIO_Pin_4 // PA4 #define MOTOR2_PWM_PIN GPIO_Pin_5 // PA5 (TIM2_CH1)这种分配方案充分利用了TIM2的PWM通道而且所有GPIO都在同一个GPIOA端口初始化代码会更简洁。1.3 电源设计要点我遇到过最头疼的问题是电机干扰导致单片机复位。后来发现必须遵守这三个原则物理隔离电机电源与MCU电源完全独立滤波电容在L298N的电源输入端并联100μF电解电容和0.1μF陶瓷电容地线处理所有地线最终单点连接避免形成环路2. PWM调速实现2.1 定时器配置TIM2的初始化代码需要特别注意预分频器和自动重装载值的计算。假设系统时钟是72MHz想要20kHz的PWM频率超出人耳听觉范围避免噪音void PWM_Init(void) { TIM_TimeBaseInitTypeDef TIM_BaseStruct; // 时钟配置 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 时基设置 TIM_BaseStruct.TIM_Prescaler 72 - 1; // 72MHz/72 1MHz TIM_BaseStruct.TIM_Period 50 - 1; // 1MHz/50 20kHz TIM_TimeBaseInit(TIM2, TIM_BaseStruct); // PWM通道配置 TIM_OCInitTypeDef TIM_OCStruct; TIM_OCStruct.TIM_OCMode TIM_OCMode_PWM1; TIM_OCStruct.TIM_OutputState TIM_OutputState_Enable; TIM_OCStruct.TIM_Pulse 0; // 初始占空比0% TIM_OCStruct.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC3Init(TIM2, TIM_OCStruct); // 通道3 TIM_Cmd(TIM2, ENABLE); }2.2 动态调整占空比通过修改CCR寄存器值来改变占空比这里封装一个安全范围检查函数void Set_PWM_Duty(TIM_TypeDef* TIMx, uint32_t Channel, uint8_t duty) { duty (duty 100) ? 100 : duty; // 限制在0-100% uint16_t pulse (TIMx-ARR * duty) / 100; switch(Channel) { case TIM_Channel_1: TIMx-CCR1 pulse; break; case TIM_Channel_2: TIMx-CCR2 pulse; break; case TIM_Channel_3: TIMx-CCR3 pulse; break; case TIM_Channel_4: TIMx-CCR4 pulse; break; } }3. 多档位速度控制3.1 档位逻辑设计8档速度控制可以通过查表法实现定义不同档位对应的占空比const uint8_t speed_table[8] {10, 20, 30, 45, 60, 75, 90, 100}; uint8_t current_speed 0; // 当前档位 void Speed_Up(void) { if(current_speed 7) { current_speed; Set_PWM_Duty(TIM2, TIM_Channel_3, speed_table[current_speed]); } } void Speed_Down(void) { if(current_speed 0) { current_speed--; Set_PWM_Duty(TIM2, TIM_Channel_3, speed_table[current_speed]); } }3.2 按键扫描实现推荐使用状态机方式处理按键避免阻塞和抖动问题typedef enum { KEY_IDLE, KEY_DEBOUNCE, KEY_PRESSED } KeyState; KeyState key_scan(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) { static KeyState state KEY_IDLE; static uint32_t tick 0; switch(state) { case KEY_IDLE: if(GPIO_ReadInputDataBit(GPIOx, GPIO_Pin) 0) { state KEY_DEBOUNCE; tick HAL_GetTick(); } break; case KEY_DEBOUNCE: if(HAL_GetTick() - tick 20) { // 20ms消抖 if(GPIO_ReadInputDataBit(GPIOx, GPIO_Pin) 0) { state KEY_PRESSED; return KEY_PRESSED; } state KEY_IDLE; } break; case KEY_PRESSED: if(GPIO_ReadInputDataBit(GPIOx, GPIO_Pin) 1) { state KEY_IDLE; } break; } return KEY_IDLE; }4. 正反转控制4.1 方向控制逻辑L298N控制电机转向的真值表如下IN1IN2电机状态00刹车01反转10正转11刹车对应的控制函数void Motor_Direction(uint8_t dir) { switch(dir) { case FORWARD: GPIO_SetBits(GPIOA, MOTOR1_IN1_PIN); GPIO_ResetBits(GPIOA, MOTOR1_IN2_PIN); break; case BACKWARD: GPIO_ResetBits(GPIOA, MOTOR1_IN1_PIN); GPIO_SetBits(GPIOA, MOTOR1_IN2_PIN); break; case BRAKE: GPIO_SetBits(GPIOA, MOTOR1_IN1_PIN | MOTOR1_IN2_PIN); break; default: // STOP GPIO_ResetBits(GPIOA, MOTOR1_IN1_PIN | MOTOR1_IN2_PIN); } }4.2 转向保护机制在实际项目中我发现电机高速运行时突然反转会导致电流冲击。解决方法是在方向切换时加入延时void Safe_Direction_Change(uint8_t new_dir) { uint8_t current_duty TIM2-CCR3; // 先减速到30%以下 while(current_duty 30) { current_duty - 5; Set_PWM_Duty(TIM2, TIM_Channel_3, current_duty); HAL_Delay(50); } // 短暂刹车 Motor_Direction(BRAKE); HAL_Delay(100); // 切换方向 Motor_Direction(new_dir); // 恢复原速度 Set_PWM_Duty(TIM2, TIM_Channel_3, current_duty); }5. 系统集成与调试5.1 典型问题排查电机抖动不转检查ENA/ENB跳线帽是否已移除用万用表测量PWM引脚是否有信号输出尝试提高PWM频率某些电机对低频PWM响应差L298N发热严重确保电机电流不超过2A短时峰值加装散热片我用旧CPU散热片改造效果很好按键响应异常检查GPIO是否配置为上拉输入模式在按键两端并联104电容可增强抗干扰5.2 进阶优化建议加入电流检测在电机回路串联0.1Ω采样电阻通过运放放大后接入ADC实现软启动上电时PWM占空比从0%渐变到目标值减少冲击电流增加温度保护当L298N温度超过60℃时自动降速这套系统虽然简单但涵盖了嵌入式开发的多个关键技术点GPIO控制、定时器PWM、中断处理、状态机编程等。我建议初学者可以先用它练手等熟练掌握后再过渡到更先进的DRV8833等驱动芯片。