2. 系统架构设计:硬件选型与软件分层

好,咱们接着聊。上一章我们把需求理清楚了,现在要开始动真格的了——选芯片、定方案、搭框架。这一步走歪了,后面全得推倒重来。我见过太多项目,硬件选型拍脑袋,结果软件写不下去,最后换主控,那叫一个酸爽。

说白了,系统架构设计就是回答三个问题:用什么脑子(主控)、用什么力气(电机驱动)、怎么跟人聊天(通信)。然后,软件怎么把这些东西组织起来,不出乱子。

2.1 硬件选型:别光看参数,要看坑

硬件选型这事儿,我个人的习惯是:先定通信,再定主控,最后看驱动。为什么?因为通信决定了你的产品形态,是走Wi-Fi还是蓝牙,是Zigbee还是私有协议,这直接影响了主控的资源需求。

2.1.1 主控芯片:算力与成本的平衡

窗帘电机的主控,不需要跑Linux,那太浪费了。一颗Cortex-M系列的内核就足够了。我推荐几个常用的:

芯片型号 内核 Flash/RAM 适用场景
STM32F103C8T6 Cortex-M3 64KB / 20KB 基础款,Wi-Fi+电机控制
ESP32-S3 Xtensa LX7 512KB / 320KB 自带Wi-Fi/BLE,性价比高
GD32F303 Cortex-M4 256KB / 96KB 国产替代,带硬件浮点

嗯,这里要注意。ESP32虽然香,但它的ADC精度是个坑。我在项目中遇到过,用ESP32采集霍尔传感器的电流信号,结果噪声大得离谱,最后不得不外挂一个ADC芯片。所以,如果你对模拟信号采集有要求,老老实实用STM32或者GD32。

避坑指南: 我曾经在一个项目里选了某款国产芯片,手册上写着支持CAN总线,结果买回来发现是“兼容模式”,跟标准CAN收发器死活对不上。所以,选型时一定要看勘误表,最好能拿到开发板先跑一遍。

2.1.2 电机驱动:有刷还是无刷?

窗帘电机,说白了就是让电机正转、反转、停。有刷电机便宜,控制简单,但寿命短、噪音大。无刷电机(BLDC)安静、寿命长,但控制复杂,需要FOC算法。

我个人建议:家用窗帘,用有刷电机+减速箱就够了。成本低,驱动芯片用一颗L298N或者DRV8833就能搞定。如果是高端项目,或者对静音有要求,那就上BLDC,驱动芯片可以考虑TI的DRV8301或者国产的AT8310。

驱动电路的设计,核心就是H桥。这里给个简单的参考电路:

// 伪代码:H桥控制逻辑
void motor_control(uint8_t direction, uint8_t speed) {
    if (direction == FORWARD) {
        IN1 = PWM(speed);  // 高边PWM
        IN2 = 0;           // 低边拉低
    } else if (direction == BACKWARD) {
        IN1 = 0;
        IN2 = PWM(speed);
    } else { // STOP
        IN1 = 0;
        IN2 = 0;
    }
}

你想想看,如果IN1和IN2同时为高,会发生什么?对,短路!所以软件上一定要做互锁保护。我刚开始做的时候,就因为这个烧过驱动板,那味道,记忆犹新。

2.1.3 通信模块:Wi-Fi还是蓝牙?

这个选择其实挺简单的。如果你需要远程控制(人在外面关窗帘),那就必须用Wi-Fi。如果只是本地控制(手机在屋里操作),蓝牙就够用了。

  • Wi-Fi方案: ESP8266/ESP32,便宜,生态好。但要注意,Wi-Fi模块的功耗比较高,待机时一定要进入深度睡眠。
  • 蓝牙方案: Nordic nRF52832或者TI CC2540。功耗低,但穿墙能力弱。
  • Zigbee方案: 适合做智能家居网关,但需要搭配协调器,开发成本高。
我的经验: 如果产品只卖国内,用Wi-Fi方案最省心。因为国内家庭基本都有路由器,用户不需要额外买网关。但如果是出口到欧美,他们喜欢用Zigbee或者Thread,因为隐私意识更强。

2.2 软件分层架构:别让代码变成一锅粥

硬件选完了,接下来就是软件怎么组织。我见过很多新手,把所有代码都写在main.c里,一个函数上千行。那调试起来,简直要命。

我的习惯是,把软件分成三层:硬件抽象层(HAL)、操作系统层(OS)、应用层(APP)。每一层各司其职,层与层之间通过接口通信。

2.2.1 HAL层:把硬件藏起来

HAL层,说白了就是给硬件穿个马甲。比如,你要控制电机,直接操作GPIO和定时器,那代码就绑死在STM32上了。以后换了GD32,所有代码都得重写。

所以,我定义一组接口:

// hal_motor.h
void hal_motor_init(void);
void hal_motor_set_speed(uint8_t speed);
void hal_motor_set_direction(uint8_t dir);
void hal_motor_stop(void);

底层实现用STM32的HAL库或者LL库,上层APP根本不知道底层是啥芯片。这就是抽象的魅力。

核心思想: HAL层只做一件事——把硬件差异封装起来。上层代码永远只调用HAL接口,不直接操作寄存器。

2.2.2 OS层:让任务跑起来

窗帘电机需要同时处理好几件事:接收遥控指令、控制电机转动、检测堵转、上报状态。如果不用操作系统,就得在while循环里轮询,那实时性很难保证。

我建议用轻量级RTOS,比如FreeRTOS或者RT-Thread。任务划分如下:

  • 通信任务: 处理Wi-Fi/蓝牙数据,解析协议。
  • 电机控制任务: 根据指令控制电机启停、速度。
  • 状态监测任务: 检测电流、位置、堵转。
  • 看门狗任务: 喂狗,防止死机。

任务之间的通信,用消息队列或者信号量。我个人的习惯是,所有任务之间不直接调用函数,而是通过队列发送消息。这样耦合度低,出了问题也好定位。

// 伪代码:任务间通信
void communication_task(void *param) {
    while(1) {
        uint8_t cmd = uart_receive();
        xQueueSend(motor_cmd_queue, &cmd, portMAX_DELAY);
    }
}

void motor_control_task(void *param) {
    uint8_t cmd;
    while(1) {
        xQueueReceive(motor_cmd_queue, &cmd, portMAX_DELAY);
        switch(cmd) {
            case CMD_OPEN: hal_motor_set_direction(FORWARD); break;
            case CMD_CLOSE: hal_motor_set_direction(BACKWARD); break;
        }
    }
}

2.2.3 APP层:业务逻辑的归宿

APP层,就是真正干活的。比如,用户按了“打开窗帘”按钮,APP层要做什么?

  1. 检查当前状态(是否已经打开?是否在运动中?)
  2. 发送指令给电机控制任务。
  3. 启动超时计时器(防止电机卡住)。
  4. 更新状态到显示屏或者APP。

这些逻辑,全部放在APP层。HAL层和OS层不关心业务,它们只提供能力。

一个小技巧: APP层的代码,尽量写成“状态机”。窗帘电机无非就是几个状态:空闲、打开中、关闭中、堵转、错误。用状态机来管理,逻辑清晰,不容易出bug。

2.3 模块接口定义:把话说清楚

层与层之间、模块与模块之间,一定要有明确的接口定义。我见过最痛苦的事,就是两个工程师各自写代码,最后联调时发现接口对不上。

接口定义,我建议用结构体+函数指针的方式。比如,电机驱动模块的接口:

typedef struct {
    void (*init)(void);
    void (*set_speed)(uint8_t speed);
    void (*set_direction)(uint8_t dir);
    void (*stop)(void);
    uint8_t (*get_status)(void);
} motor_driver_t;

// 使用示例
motor_driver_t motor = {
    .init = hal_motor_init,
    .set_speed = hal_motor_set_speed,
    .set_direction = hal_motor_set_direction,
    .stop = hal_motor_stop,
    .get_status = hal_motor_get_status
};

这样,APP层只需要调用 motor.set_speed(50),根本不需要知道底层是怎么实现的。以后换了电机驱动芯片,只需要重新实现这个结构体里的函数,APP层一行代码都不用改。

总结一下: 系统架构设计,说白了就是“分而治之”。硬件选型要务实,软件分层要清晰,接口定义要严谨。这三件事做好了,后面的开发就是水到渠成的事。

嗯,这一章的内容就到这里。下一章,我们开始搭建开发环境,把代码跑起来。到时候我会手把手教你怎么配置FreeRTOS,怎么把电机转起来。