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管冒烟了。
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;
}
}
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();
// 绝对不要在这里调用复杂函数!
}
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);
}
4.5 保护逻辑设计:最后的防线
保护逻辑,是堵转保护的「大脑」。它要判断:什么时候是堵转?什么时候是正常过载?什么时候该重启?
我设计的保护逻辑包含三个层级:
- 瞬时过流保护(硬件级):比较器直接触发,关断PWM,响应时间<1μs
- 堵转检测(软件级):连续N次采样电流超过阈值,判定为堵转
- 过温保护(软件级):监测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次,然后锁定故障。
知识体系总览
下面这张图,是我画的本章节知识结构。你看一眼,就能明白各个模块之间的关系:
嗯,以上就是软件架构设计的核心内容。说白了,就是让程序有条理、不慌乱、反应快。你把这些模块搭好了,后面调试堵转保护就是水到渠成的事。
本章要点回顾:
- 主循环采用前后台架构,中断处理紧急事件,主循环处理非实时任务
- 状态机用switch-case实现,状态切换要加防抖
- 定时器中断服务函数要短,只做置标志和简单更新
- ADC采样用同步采样+滑动平均滤波,保证数据稳定
- 保护逻辑分三级:硬件过流、软件堵转、过温保护