1. 项目概述:电机控制系统的多设备协同
这个项目本质上是一个典型的嵌入式电机控制系统开发案例,核心在于通过微控制器实现对直流有刷电机的精准控制,同时整合了人机交互(HMI)所需的按键输入、状态显示(数码管/LED)和声音反馈(蜂鸣器)功能模块。在实际工业控制领域,类似架构广泛应用于电动工具、家用电器、自动化设备等场景。
我去年为一家电动窗帘厂商开发的控制器就采用了几乎相同的技术路线。通过STM32F103芯片,用PWM信号驱动MOSFET桥电路控制电机正反转,配合旋转编码器实现位置反馈,而运行状态则通过4位数码管显示,配合蜂鸣器提示到位信号。这种多模块协同工作的设计,既要考虑实时性要求,又要处理各外设间的资源竞争问题。
2. 硬件架构设计要点
2.1 电机驱动电路设计
直流有刷电机控制的核心是H桥驱动电路。以常用的DRV8871驱动芯片为例,其典型应用电路需要注意:
- 电机电源与逻辑电源隔离(使用0.1μF去耦电容)
- 在VM和GND之间放置47μF以上的电解电容
- 每个MOSFET并联续流二极管(或选用集成保护功能的驱动芯片)
c复制// 典型H桥控制真值表
// | IN1 | IN2 | 电机状态 |
// |-----|-----|----------|
// | 0 | 0 | 刹车 |
// | 0 | 1 | 反转 |
// | 1 | 0 | 正转 |
// | 1 | 1 | 滑行 |
2.2 人机交互设备选型
- 按键输入:推荐使用矩阵键盘扫描方式(如4x4矩阵可节省IO口)
- 数码管显示:共阳/共阴类型选择需匹配驱动电路,动态扫描频率建议>100Hz
- 蜂鸣器:分有源(固定频率)和无源(需PWM驱动)两种类型
- 状态LED:可增加RGB LED实现多状态颜色指示
关键提示:所有数字输入口必须配置上拉/下拉电阻,防止浮空状态导致误触发。我曾遇到因按键电路未加上拉电阻,导致电机莫名启动的故障案例。
3. 软件架构设计与实现
3.1 主程序流程图设计
采用时间片轮询架构是这类多外设系统的合理选择:
- 系统时钟初始化(配置SysTick定时器)
- 外设初始化(GPIO、PWM、定时器等)
- 创建任务队列:
- 10ms任务:按键扫描
- 20ms任务:数码管刷新
- 50ms任务:电机状态检测
- 100ms任务:系统状态更新
c复制void SystemTask_Schedule(void)
{
static uint32_t tick = 0;
tick++;
if(tick % 10 == 0) Key_Scan();
if(tick % 20 == 0) DigitalTube_Refresh();
if(tick % 50 == 0) Motor_StatusCheck();
if(tick % 100 == 0) System_Update();
}
3.2 PWM电机控制实现
直流有刷电机速度控制本质是调节PWM占空比。以STM32的TIM1为例:
c复制void PWM_Init(uint16_t freq)
{
TIM_TimeBaseInitTypeDef TIM_Base;
TIM_OCInitTypeDef TIM_OC;
// 时基配置(假设72MHz主频)
uint16_t prescaler = SystemCoreClock / (freq * 1000) - 1;
TIM_Base.TIM_Prescaler = prescaler;
TIM_Base.TIM_Period = 999; // 1kHz PWM频率
TIM_TimeBaseInit(TIM1, &TIM_Base);
// PWM通道配置
TIM_OC.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OC.TIM_OutputState = TIM_OutputState_Enable;
TIM_OC.TIM_Pulse = 0; // 初始占空比0%
TIM_OC.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInit(TIM1, &TIM_OC);
TIM_Cmd(TIM1, ENABLE);
}
实测经验:PWM频率选择需权衡:
- 低频(1-5kHz):可能产生可闻噪声但驱动损耗小
- 高频(>20kHz):无噪声但MOSFET开关损耗增加
建议根据电机功率选择,小功率电机可用15kHz左右
3.3 多外设协同处理技巧
按键消抖处理:
c复制#define DEBOUNCE_TIME 20 // 20ms消抖时间
uint8_t Key_GetValid(void)
{
static uint8_t last_state = 0;
static uint32_t last_time = 0;
if(KEY_PORT != last_state) {
last_time = systick;
last_state = KEY_PORT;
return 0;
}
if((systick - last_time) > DEBOUNCE_TIME) {
return last_state;
}
return 0;
}
数码管动态显示:
采用定时器中断实现扫描刷新,避免主程序阻塞:
c复制void TIM2_IRQHandler(void)
{
static uint8_t pos = 0;
if(TIM_GetITStatus(TIM2, TIM_IT_Update)) {
DIGI_PORT = 0xFF; // 关闭所有段
DIGI_SEL = (1 << pos); // 选择位
DIGI_PORT = digit_buf[pos]; // 输出段码
pos = (pos + 1) % 4;
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
4. 典型问题排查指南
4.1 电机异常抖动问题
现象:电机运行时出现不规则抖动或噪音
排查步骤:
- 用示波器检查PWM波形是否纯净(无毛刺)
- 测量电源电压在负载下的波动(应<5%)
- 检查H桥死区时间设置(建议1-2μs)
- 确认电机参数与驱动匹配(特别是启动电流)
4.2 显示设备干扰问题
现象:数码管显示时电机响应变慢或LED异常闪烁
解决方案:
- 增加显示刷新与电机控制的优先级差异
- 为数码管段选线添加74HC245等总线驱动器
- 在GPIO线上串联100Ω电阻抑制振铃
- 优化地线布局(星型接地)
4.3 系统资源冲突案例
曾遇到一个典型问题:当蜂鸣器鸣叫时,电机PWM出现波动。最终发现是共用定时器资源导致。解决方案:
- 为蜂鸣器分配独立定时器
- 或采用PWM分时复用技术:
c复制void TIM_Config(void)
{
// 主PWM用于电机控制
TIM1->CCR1 = motor_speed;
// 仅在需要时启用蜂鸣器PWM
if(buzzer_enable) {
TIM1->CCR2 = buzzer_duty;
TIM1->CCER |= TIM_CCER_CC2E;
} else {
TIM1->CCER &= ~TIM_CCER_CC2E;
}
}
5. 系统优化进阶技巧
5.1 速度闭环控制实现
在基础开环控制上增加编码器反馈:
c复制void Motor_PID_Update(void)
{
static float err_sum = 0;
float error = target_speed - actual_speed;
err_sum += error;
// 简易PID实现
float output = KP * error + KI * err_sum + KD * (error - last_error);
last_error = error;
// 输出限幅
output = constrain(output, 0, 1000);
TIM1->CCR1 = (uint16_t)output;
}
5.2 低功耗设计要点
- 空闲时关闭数码管显示(保留1%占空比维持亮度)
- 采用WFI指令进入睡眠模式
- 按键中断唤醒设计:
c复制void EXTI0_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line0)) {
EXTI_ClearITPendingBit(EXTI_Line0);
System_WakeUp();
}
}
5.3 状态机编程实践
将电机控制逻辑抽象为状态机:
c复制typedef enum {
MOTOR_STOP,
MOTOR_ACCEL,
MOTOR_RUN,
MOTOR_BRAKE
} MotorState;
void Motor_StateMachine(void)
{
static MotorState state = MOTOR_STOP;
switch(state) {
case MOTOR_STOP:
if(start_cmd) {
state = MOTOR_ACCEL;
accel_timer = 0;
}
break;
case MOTOR_ACCEL:
PWM_Set(accel_curve[accel_timer++]);
if(accel_timer >= CURVE_SIZE) {
state = MOTOR_RUN;
}
break;
// 其他状态处理...
}
}
在完成这个项目时,最深的体会是"细节决定成败"。曾因一个简单的按键消抖没处理好,导致生产线上的设备偶发误动作。后来建立了严格的测试流程:所有输入信号必须通过示波器验证,所有输出负载必须进行边界测试。建议开发类似系统时,务必预留30%时间用于异常情况测试。