四、整体分层架构概览:四层架构
好,咱们进入正题。分拣机的嵌入式软件,说白了就是一套“指挥系统”。它要管电机、读传感器、算路径、还要跟PLC唠嗑。这么多活,如果全堆在一个文件里,那维护起来就是噩梦。
我个人习惯,把软件拆成四层。每一层各司其职,互不干扰。这就像工厂里的流水线——有人专门拧螺丝,有人专门喷漆,分工明确,效率才高。
4.1 四层架构长什么样?
从上到下,分别是:
- 应用层:负责业务逻辑。比如“这个包裹该去哪个滑槽”、“当前分拣速度是否过快”。
- 中间件层:负责“翻译”和“调度”。比如任务队列、状态机、通信协议解析。
- 驱动层:直接跟硬件寄存器打交道。比如初始化SPI、配置PWM频率。
- 硬件抽象层(HAL):把硬件差异“藏起来”。比如“读编码器”这个操作,不管你是用STM32还是GD32,接口都一样。
核心原则:上层依赖下层,但下层绝不能依赖上层。应用层可以调用中间件,但驱动层不能直接调用应用层的函数。否则,改一个传感器型号,整个系统都得重写。
我在项目中遇到过最典型的反面教材:有人把业务逻辑写在了中断服务函数里。结果换了个主频更高的芯片,中断响应时间变了,整个分拣逻辑全乱套。嗯,这就是没分层的代价。
4.2 每一层具体干什么?
4.2.1 硬件抽象层(HAL)
这一层最底层。它直接面对芯片的寄存器、GPIO、定时器。说白了,就是给硬件“穿件衣服”。
举个例子,你要读一个编码器的脉冲数。不同芯片的定时器模式可能不一样。但HAL层会提供一个统一的接口:
/* HAL层接口 */
int32_t HAL_Encoder_GetCount(Encoder_Handle_t *handle);
这样,驱动层就不用关心底层是STM32的TIM还是GD32的TIM。换芯片?只需要重写HAL层,上层代码一行都不用改。
我的经验:HAL层不要做任何逻辑判断。它只负责“读”和“写”。比如“读温度值”,HAL层只返回原始ADC值,至于这个值对应多少摄氏度,那是中间件层的事。
4.2.2 驱动层
驱动层在HAL层之上。它负责管理具体的硬件外设。比如步进电机驱动、电磁阀驱动、光电传感器驱动。
驱动层会封装一些“动作”。比如:
/* 驱动层接口 */
void DRV_Stepper_MoveTo(Stepper_t *motor, int32_t target_position);
void DRV_Stepper_Stop(Stepper_t *motor);
驱动层会调用HAL层的接口,去设置PWM占空比、控制方向引脚、使能电机。但它不会去判断“为什么要移动到这个位置”。那是应用层的事。
注意:驱动层要处理超时和错误。比如电机堵转了,驱动层应该能检测到,并向上层报告错误码。我曾经见过一个项目,电机堵转了,驱动层还在傻傻地发脉冲,结果把驱动器烧了。后来我加了个“堵转检测”机制,才算彻底解决。
4.2.3 中间件层
中间件层是“承上启下”的关键。它不直接控制硬件,但负责协调多个驱动模块。
常见的中间件包括:
- 任务调度器:决定哪个任务先执行。比如“先读传感器,再计算路径,最后控制电机”。
- 状态机:管理分拣机的运行状态。比如“空闲”、“运行中”、“故障停机”。
- 通信协议栈:解析Modbus、CANopen等协议。把收到的报文转换成应用层能理解的数据。
- 数据缓冲区:比如环形队列,用于缓存传感器数据,防止数据丢失。
你想想看,如果没有中间件层,应用层就得直接去解析Modbus报文,还得自己管理任务优先级。那代码得多乱?
4.2.4 应用层
应用层是“大脑”。它只关心业务逻辑,不关心硬件细节。
比如一个典型的分拣逻辑:
/* 应用层伪代码 */
void APP_SortProcess(void)
{
ParcelInfo_t parcel = APP_GetParcelInfo(); // 获取包裹信息
uint8_t chute_id = APP_CalcChute(parcel); // 计算目标滑槽
MID_Conveyor_SetSpeed(SPEED_NORMAL); // 设置传送带速度
MID_Sorter_Actuate(chute_id); // 触发分拣动作
}
你看,应用层根本不知道电机是步进电机还是伺服电机,也不知道传感器是光电式还是超声波式。它只调用中间件层的接口,中间件层再去调用驱动层,驱动层再去调用HAL层。层层传递,职责清晰。
4.3 层间通信机制
层与层之间怎么通信?说白了,就是“函数调用”和“回调函数”。
| 通信方式 | 方向 | 典型场景 |
|---|---|---|
| 函数调用 | 上层调用下层 | 应用层调用中间件层的“设置速度”函数 |
| 回调函数 | 下层通知上层 | 驱动层检测到电机堵转,回调应用层的错误处理函数 |
| 消息队列 | 异步通信 | 传感器数据通过队列传递给应用层,避免中断中处理复杂逻辑 |
| 共享变量 | 同层或跨层 | 状态机变量,多个模块需要读取当前系统状态 |
我建议:尽量用函数调用和回调函数。消息队列虽然灵活,但会增加系统复杂度。共享变量要加锁,否则容易出竞态条件。我在一个老项目里见过,两个任务同时修改一个全局变量,结果分拣计数全乱了。后来改成消息队列,问题才解决。
4.4 数据流方向
数据流有两个方向:
- 下行数据流(控制流):从应用层到硬件。比如“应用层决定让电机转10圈”,这个指令会经过中间件层、驱动层、HAL层,最终变成PWM波形。
- 上行数据流(采集流):从硬件到应用层。比如“传感器检测到包裹”,信号经过HAL层、驱动层、中间件层,最终变成应用层能处理的“包裹到达事件”。
你想想看,如果数据流方向乱了,比如驱动层直接修改了应用层的变量,那调试起来得多痛苦?
我曾经接手过一个项目,驱动层里居然有“如果包裹重量大于5kg,就停止传送带”的逻辑。结果换了个称重传感器,驱动层代码得大改。这就是数据流方向搞反了的典型例子。
4.5 避坑指南
我曾经踩过的坑:
- 不要跨层调用:应用层不要直接调用HAL层。哪怕你觉得“就这一次,图省事”。相信我,以后会有无数次。
- 不要在下层做业务判断:驱动层只负责“执行”,不负责“决策”。比如“是否要急停”,那是应用层的事。
- 接口要稳定:一旦确定了层间接口,尽量不要改。改一个接口,可能影响上面所有层。我建议在项目初期,先花时间把接口定义好,后面会省很多事。
好了,四层架构就讲到这里。下一章咱们深入讲讲硬件抽象层的具体实现。到时候我会拿一个实际的编码器驱动来举例,看看HAL层到底怎么写才够“抽象”。