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。
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方案: 适合做智能家居网关,但需要搭配协调器,开发成本高。
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根本不知道底层是啥芯片。这就是抽象的魅力。
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层要做什么?
- 检查当前状态(是否已经打开?是否在运动中?)
- 发送指令给电机控制任务。
- 启动超时计时器(防止电机卡住)。
- 更新状态到显示屏或者APP。
这些逻辑,全部放在APP层。HAL层和OS层不关心业务,它们只提供能力。
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,怎么把电机转起来。