4. 软件架构设计:主循环架构、状态机设计、定时器中断设计、ADC采样设计、保护逻辑设计

好,咱们进入正题。软件架构,说白了就是程序的骨架。骨架搭歪了,后面填多少肉都别扭。我在做小家电堵转保护时,最深的体会就是——架构设计决定了你调试时是喝茶还是流汗

这一章,我会把主循环、状态机、定时器中断、ADC采样和保护逻辑这五个核心模块,一个一个拆开来讲。嗯,都是实战中摸爬滚打出来的经验。

4.1 主循环架构:别让CPU闲着,也别让它累死

主循环,就是程序的大本营。我见过不少新手,把所有逻辑都塞进主循环里,结果一个函数跑几百毫秒,电机都烧了还没反应过来。

我个人习惯用前后台系统:前台是中断,处理紧急事件;后台是主循环,处理非实时任务。主循环的典型结构是这样的:

void main(void)
{
    // 初始化
    System_Init();
    
    while(1)
    {
        // 1. 喂狗(如果有看门狗)
        Feed_Watchdog();
        
        // 2. 处理状态机
        StateMachine_Run();
        
        // 3. 处理通信(如I2C、UART)
        Comm_Process();
        
        // 4. 处理按键/显示等慢速任务
        UI_Update();
        
        // 5. 进入低功耗(可选)
        // Sleep_Mode();
    }
}

这里有个关键点:主循环不能阻塞。每个函数执行时间要控制在1ms以内。你想想看,如果按键扫描用了10ms,那电机堵转检测就晚了10ms——这10ms可能就让MOS管冒烟了。

我的经验:主循环里尽量别用延时函数(delay_ms)。实在要用,用状态机拆成非阻塞形式。我曾经在一个风扇项目里,就因为主循环里加了个50ms的按键消抖延时,导致堵转保护响应慢了半拍,烧了三个驱动板才找到原因。

4.2 状态机设计:让程序知道自己该干嘛

状态机,说白了就是给程序画个「行为地图」。电机控制里,状态机是灵魂。我常用的电机状态机有这几个状态:

状态 含义 典型动作
IDLE 空闲/待机 关闭PWM,等待启动指令
START 启动中 软启动,逐步增加占空比
RUN 正常运行 闭环控制,监测电流
STALL 堵转检测到 立即关断,尝试重启
FAULT 故障锁定 停止输出,等待复位

状态切换的逻辑,我建议用switch-case实现,清晰又好维护:

void StateMachine_Run(void)
{
    switch(motor_state)
    {
        case IDLE:
            if(Start_Flag)
                motor_state = START;
            break;
            
        case START:
            Soft_Start();
            if(Start_Complete)
                motor_state = RUN;
            if(Overcurrent_Detected)
                motor_state = STALL;
            break;
            
        case RUN:
            Motor_Control();
            if(Overcurrent_Detected)
                motor_state = STALL;
            if(Stop_Flag)
                motor_state = IDLE;
            break;
            
        case STALL:
            Motor_Stop();
            Stall_Count++;
            if(Stall_Count > MAX_RETRY)
                motor_state = FAULT;
            else
                motor_state = START;  // 自动重启
            break;
            
        case FAULT:
            // 等待外部复位或断电
            break;
    }
}
注意:状态切换一定要加防抖处理。比如从RUN切换到STALL,不能因为一次电流尖峰就误判。我一般会连续采样3次,都超标才切换状态。

4.3 定时器中断设计:精准的时间节拍

定时器中断,是嵌入式系统的「心跳」。没有它,ADC采样、PWM控制、保护检测全都乱套。

我通常设置三个定时器

  • 定时器0(1ms):系统心跳,用于状态机调度、按键扫描
  • 定时器1(100μs):ADC采样触发,电流检测
  • 定时器2(PWM频率):电机驱动,占空比控制

这里有个设计原则:中断服务函数要短,短到不能再短。我见过有人把整个状态机都放在中断里跑,结果中断嵌套把自己搞死了。

// 1ms定时器中断服务函数
void TIM0_IRQHandler(void)
{
    // 清除中断标志
    TIM0_ClearFlag();
    
    // 只做两件事:
    // 1. 设置一个标志位
    SysTick_1ms = 1;
    
    // 2. 更新软件定时器
    Software_Timer_Update();
    
    // 绝对不要在这里调用复杂函数!
}
避坑指南:我曾经在一个吸尘器项目里,把电流保护逻辑写在了100μs的中断里。结果中断执行时间太长,导致PWM波形失真,电机噪音大得像拖拉机。后来我把保护逻辑移到主循环里,中断只负责采样和置标志,问题就解决了。

4.4 ADC采样设计:电流检测的「眼睛」

ADC采样,是堵转保护的数据来源。采样不准,保护逻辑就是瞎指挥。

我常用的采样方案是同步采样:在PWM的固定相位点触发ADC。为什么?因为电机电流是脉动的,在PWM导通中间采样最稳定。

// ADC初始化配置
void ADC_Init(void)
{
    // 设置采样通道(电流检测引脚)
    ADC_Channel_Select(ADC_CH_CURRENT);
    
    // 设置采样时间(建议 > 1μs)
    ADC_SampleTime_Set(ADC_SAMPLETIME_1US);
    
    // 开启连续转换模式
    ADC_ContinuousConvMode_Enable();
    
    // 启动ADC
    ADC_Start();
}

// ADC中断服务函数
void ADC_IRQHandler(void)
{
    uint16_t adc_value;
    
    // 读取转换结果
    adc_value = ADC_GetValue();
    
    // 数字滤波:滑动平均
    current_filtered = Moving_Average(adc_value);
    
    // 设置数据就绪标志
    ADC_Ready = 1;
}

采样值出来后,要做数字滤波。我推荐滑动平均滤波,简单有效:

#define FILTER_LEN 8

uint16_t Moving_Average(uint16_t new_value)
{
    static uint16_t buffer[FILTER_LEN] = {0};
    static uint8_t index = 0;
    static uint32_t sum = 0;
    
    // 减去旧值,加上新值
    sum -= buffer[index];
    sum += new_value;
    
    // 更新缓冲区
    buffer[index] = new_value;
    index = (index + 1) % FILTER_LEN;
    
    // 返回平均值
    return (uint16_t)(sum / FILTER_LEN);
}
注意:ADC的参考电压一定要稳定。我遇到过因为参考电压纹波大,导致采样值跳变,保护逻辑频繁误触发。后来在参考电压引脚上加了个10μF的电容,问题就解决了。

4.5 保护逻辑设计:最后的防线

保护逻辑,是堵转保护的「大脑」。它要判断:什么时候是堵转?什么时候是正常过载?什么时候该重启?

我设计的保护逻辑包含三个层级

  1. 瞬时过流保护(硬件级):比较器直接触发,关断PWM,响应时间<1μs
  2. 堵转检测(软件级):连续N次采样电流超过阈值,判定为堵转
  3. 过温保护(软件级):监测MOS管温度,超过阈值降功率或停机

堵转检测的核心算法是这样的:

#define CURRENT_THRESHOLD  3000  // 电流阈值(ADC值)
#define STALL_COUNT_THRESHOLD  5  // 连续超标次数

uint8_t Stall_Detect(uint16_t current)
{
    static uint8_t stall_count = 0;
    
    if(current > CURRENT_THRESHOLD)
    {
        stall_count++;
        if(stall_count >= STALL_COUNT_THRESHOLD)
        {
            // 堵转确认
            stall_count = 0;
            return 1;  // 返回堵转标志
        }
    }
    else
    {
        // 电流恢复正常,清零计数器
        stall_count = 0;
    }
    
    return 0;
}

这里有个细节:堵转后的重启策略。我一般用「指数退避」——第一次堵转立即重启,第二次等1秒,第三次等2秒,第四次等4秒...最多重试5次,然后锁定故障。

我的经验:堵转阈值不能设得太死。比如一个搅拌机,刚启动时电流本来就大,如果阈值设低了,每次启动都误报堵转。我一般会加一个「启动豁免期」——启动后前500ms不检测堵转,只检测过流。

知识体系总览

下面这张图,是我画的本章节知识结构。你看一眼,就能明白各个模块之间的关系:

小家电堵转保护软件架构 主循环架构 状态机设计 定时器中断 ADC采样设计 保护逻辑设计 IDLE/START/RUN/STALL/FAULT 1ms/100μs/PWM 同步采样/滑动平均滤波 过流/堵转/过温/重启策略 核心设计原则 中断要短 · 主循环不阻塞 · 状态切换加防抖 · 采样要滤波 · 保护分层级

嗯,以上就是软件架构设计的核心内容。说白了,就是让程序有条理、不慌乱、反应快。你把这些模块搭好了,后面调试堵转保护就是水到渠成的事。

本章要点回顾:

  • 主循环采用前后台架构,中断处理紧急事件,主循环处理非实时任务
  • 状态机用switch-case实现,状态切换要加防抖
  • 定时器中断服务函数要短,只做置标志和简单更新
  • ADC采样用同步采样+滑动平均滤波,保证数据稳定
  • 保护逻辑分三级:硬件过流、软件堵转、过温保护

专注资料整理