5.1. 前言
这一节非常重要,因为它决定了你的程序是否稳定、可维护、易扩展。初学嵌入式时最常见的大问题,就是把所有东西都堆在 main.c 里:初始化写在一起、控制逻辑写在一起、通信解析写在一起、调试代码也写在一起。这样的代码在最开始也许能跑,但一旦系统功能增加,维护难度会急剧上升
所以首先必须建立一个基本意识:代码不是“写出来能跑”就结束了,而是要能看、能改、能查、能扩展,这就引出了“架构思维”
而架构思维是指你对你的系统的整体结构设计需要有一个清晰的规划,简单来说就是你设计的东西的“蓝图”,常见的设计方法有以下几种:
然后就是调试能力,因为码农就不可能遇不到 BUG,永远都是三分写七分 DEBUG,提高调试能力就是提高开发效率,主要能力就是以下几个:
最后就是编程环境,主要是我觉得 Keil 太丑了(,所以就会转移到 VS Code 上开发,你们可以了解一下,像 EIDE、PlatformIO、Clion 这些都可以试试
5.2. 模块化编程
模块化编程是程序设计的基本要求,最基础的一步,是不要把所有功能都写在一个文件里,例如一个稍微复杂一点的系统,往往会同时涉及:
如果这些内容全部堆在 main.c 中,那么后续只要改一处,其他地方就可能被连带影响
5.2.1. 模块化的最基本要求
最简单也最实用的做法,就是按职责拆分 .c/.h 文件:
这样做的好处是:
5.2.2. 模块化不等于“文件拆开”就结束了
很多人以为模块化只是“多建几个文件夹”,其实不是,真正的模块化有两个核心要求:
5.2.3. 从模块化走向工程化
当你养成基本的模块化习惯后,就应该继续往前走,学习代码架构的设计方法,因为系统一旦变复杂,仅靠“拆文件”已经不够了,还需要思考:
5.3. 代码架构设计方法
从前言中可以了解到代码架构常用的几种设计方法,通常都是混合使用来实现最清晰好用最适合自己的架构,代码架构设计通常是在裸机上完成,其中最后一项时间片伪多线程是 RTOS 的基础,与裸机相对的便是 RTOS 实时操作系统,使用操作系统时就只需要考虑代码文件分层,下面就是裸机开发的代码架构设计方法:
5.3.1. 分层结构
分层结构是最值得优先掌握的能力之一,它的核心思想是:不同抽象层做不同层的事,一个比较通用的分层思路可以是:
分层结构最大的意义不是“看起来高级”,而是降低耦合,当你换一个传感器、换一个通信模块、换一个控制算法时,如果边界划分清楚,就不需要把整个系统推倒重来,例如你写了一个控制系统,在当前项目使用的是 STM32,而另一个项目中要求用更低成本的芯片完成通用的控制系统,那实际上你只需要替换平台层 + 修改驱动层的代码,甚至如果层级之间解耦与抽象做得好,只需要替换平台层中的具体实现即可,案例见下文的单例模式
5.3.2. 前后台系统
前后台系统是裸机最经典的组织方法之一:
5.3.3. 事件驱动
事件驱动的核心不是“有个事件枚举”这么简单,而是:系统行为由事件推动,而不是由调用链硬连接,例如:
事件驱动适合把复杂业务流程拆开,这样每个模块不需要知道全系统发生了什么,只需要知道“我收到了什么事件,该做什么响应”
5.3.4. 周期轮询
周期轮询就是在主循环中按固定节奏处理任务,它很适合这类内容:
关键不是“有没有 while(1)”——因为裸机一定有 while(1)——而是你是否把 while(1) 组织成了一个有节奏、有边界的调度中心
5.3.5. 状态机
状态机适合管理“有明显阶段变化的系统行为”,例如:
状态机的优点是:
当系统流程再复杂一些时,就可以从普通有限状态机继续升级到层级有限状态机(HFSM),把公共逻辑提到父状态,把子状态差异保留在局部
5.3.6. 时间片伪多线程
时间片伪多线程本质上是 RTOS 的基础思想,在裸机里,可以通过系统心跳和任务切片实现“多个任务交替运行”的效果,例如每 1 ms 更新节拍,然后在主循环中根据节拍执行不同任务:
- 1 ms:采样
- 5 ms:控制
- 10 ms:通信
- 100 ms:日志输出
这种方法适合任务很多、但每个任务都比较短小的系统。
5.4. 调试方法
基本思路
- 先复现,后定位,再解决,稳定复现 bug 否则调试就是个玄学(好吧就是);
- 先软件后硬件,先上层再底层(当然新建的工程肯定是先排查底层),一般硬件是不会出问题的,大概率是人的问题,然后就是底层一般之前能用现在不能用那就不应该是底层问题;
- 信息!必须尽可能地获取信息来更精确地定位,这就是下面代码层调试的方法;
- 永远先怀疑自己写的最新的那部分代码,老的能跑说明大概率没问题,所以要 git 版本管理是必须要学会的,同时上传 github 也能作为自己的简历。
代码层调试
- 串口打印:涉及到串口重定向与可变参数实现日志,总的来说是利用串口获取关键信息(关键位置的任何可疑的变量),这里强烈推荐用蓝牙结合 VOFA+ 上位机的形式(VOFA-Plus 上位机,同时也适用于远程波形调参)
- 串口重定向:因为 printf 内部实现输出的函数是弱定义,可以在外部自行重新定向到串口上,快速实现格式化输出(同理 scanf 也可以实现重定向)串口重定向原理与方法
- 当需要多个串口实现格式化输出时,可以基于 VA_LIST 实现可变参数格式化输出可变参数函数的介绍(串口)
- 调试器 + 断点 + 变量监视:
- 工具:STM32CubeIDE、Keil、IAR + STLinkV3 或 JLink
- 常用技巧:
- 在 HardFault_Handler 里直接死循环,让你能连上调试器看寄存器
- 看栈指针(SP)、链接寄存器(LR)、程序计数器(PC),定位到底是野指针还是除零
- 打开 “Expressions” 窗口,把整个电机结构体、PID 结构体直接拖进去实时看
- 用条件断点:比如 error > 1000 的时候才停,专门抓异常瞬间
- 用 “Memory” 窗口直接看 Flash 是否烧对了、RAM 区有没有被覆盖
- 串口打印:涉及到串口重定向与可变参数实现日志,总的来说是利用串口获取关键信息(关键位置的任何可疑的变量),这里强烈推荐用蓝牙结合 VOFA+ 上位机的形式(VOFA-Plus 上位机,同时也适用于远程波形调参)
硬件层调试
项目 工具 检查点 典型问题 电源 万用表、示波器 3.3V、5V、VBUS纹波<50mV 降压模块坏、电源线太细、峰值电流不足 信号电平 万用表、逻辑分析仪 TTL/CAN/RS485电平是否正常 3.3V和5V混接烧IO 波形质量 示波器、逻辑分析仪 PWM方波、CAN差分波形、I2C时序 上升沿太慢、毛刺、总线竞争 通信总线 CAN分析仪、逻辑分析仪 CAN:错误帧、ACK错误 串口:波特率错、帧格式 终端电阻漏接、收发接反 传感器 示波器+已知好板对比 IMU是否有周期性毛刺 编码器A/B相是否90°相位差 电磁干扰、接线虚焊 电机/电调 示波器、电流探头 PWM占空比、电流纹波、反电动势 相线断、MOS管击穿
5.5. git 版本管理与 github
Git 不是“以后再学”的东西,而是越早学越有价值的基本功。
5.5.1. 为什么必须学 Git
对个人来说:
对团队来说:
5.5.2. 推荐习惯
- 小功能单独提交
- 每次提交写清楚目的
- 不要把“能跑的一版”和“实验性修改”混在一起
- 会用分支,不要所有改动都堆在主分支
- 工程文件、配置文件、脚本、文档尽量一起管理
5.5.3. 构建环境也属于工程能力的一部分
现代嵌入式工程不应该只依赖某个 IDE 的按钮,你至少应该逐步理解这些东西:
因为当工程规模变大后,命令行构建、跨平台构建、自动化构建和配置复现都会变得越来越重要,一个完整嵌入式工程如果同时包含底层生成代码、应用代码、数学验证脚本和联调脚本,那么能否用标准化方式构建和调试,会直接影响维护成本
5.5.4. 教程与插件
git 较为复杂,学习周期也较长,推荐教程:给傻子的 Git 教程;傻子也能做开源;和傻子一起写代码;这套教程通俗易懂非常好上手(认真),最好的教程!(;第一个是 Git 教程,第二个第三个是 Github 教程,如果使用 Github 则需要注意仓库私密性问题,自用的设置为私有仓库。
然后是 VS Code 自带 Git,同时推荐个插件:Git Graph,特别是 ubuntu 的 VS Code 必备。

5.6. C 语言模拟 OOP(面向对象)与设计模式
5.6.1. C 语言模拟 OOP
这一节很重要,因为很多嵌入式系统一旦变复杂,单纯“面向过程堆函数”会越来越难维护,但这并不意味着必须上 C++ 才能做抽象。C 语言同样可以通过良好的结构设计,模拟出很实用的“对象化”效果
- 核心思想
在 C 里,模拟 OOP 的常见方法就是:
- 适合用这种方式管理的对象
在嵌入式里,下面这些内容特别适合做成“对象”:
- 示例:PID 控制器对象化
typedef struct PID_t {
float p, i, d;
float output;
void (*set_pid)(struct PID_t* this, float p, float i, float d);
void (*reset)(struct PID_t* this);
void (*calc)(struct PID_t* this, float target, float actual, float dt_s);
float _integral;
float _last_actual;
} PID_t;这种写法的重点不是“像不像类”,而是它解决了两个问题:
C 模拟 OOP 的优点
- 保持 C 的可控性和轻量性
- 对裸机和资源受限环境友好
- 便于统一接口
- 便于做模块复用和替换
C 模拟 OOP 的边界
但也要明白,C 模拟 OOP 不是万能的,如果结构体里塞太多函数指针,或者继承关系过深,就会开始变得别扭,这时候更重要的不是“继续硬模拟”,而是及时判断:这个模块到底适不适合继续留在 C 的抽象方式里
5.6.2. 设计模式
基于之前所述代码架构思想和 C 模拟 OOP ,在实际应用时主要使用七种设计模式:
这里需要先明确一点:设计模式不是为了“显得高级”,而是为了解决反复出现的结构性问题,如果一个模式不能降低耦合、提升可维护性、减少重复代码,那它就不值得引入,从适用场景来看,可分类为:
5.6.2.1. 观察者模式
- 对比原来的分层结构实现了彻底解耦,解决原来同层(主要是驱动层)之间相互依赖问题
- 核心逻辑:类似于 ROS 中的话题 "发布 - 订阅" 模式
- 被观察者(发布者):如驱动层,只负责发布数据,提供读取数据接口
- 观察者(订阅者):如驱动层,暴露回调函数注册接口让上层完善订阅
- 胶水层:如应用层,负责将被观察者的读取数据接口注册到观察者的回调函数上
- 注意事项:观察者的回调函数注册接口要注释好需要什么格式的函数
- 代码示例
// --- sensor.h (被观察者) ---
void SensorUpdate(void); // 传感器数据更新
float SensorRead(void); // 传感器数据读取接口函数
// --- display.h (观察者) ---
typedef float(* ReadDataHandler)(void);
void DisplayRegisterCallback(ReadDataHandler handler);
void Display(void);
// --- display.c (观察者) ---
./5 float(* ReadData)(void);
/**
* @brief ...
* @param handler -> float SensorRead(void);
*/
void DisplayRegisterCallback(ReadDataHandler handler)
{
ReadData = handler;
}
void Display(void)
{
float data = ReadData();
DisplayData(data);
}
// --- app.c (胶水层) ---
#include "sensor.h"
#include "display.h"
void DockSensorAndDisplay(void)
{
DisplayRegisterCallback(SensorRead);
}- 上述为 Pull 观察者模式,为订阅者驱动即订阅者需要的时候才读,实时性一般;同时代码示例为一对一情况,如果需要发布者一对多则每个观察者都写一次回调函数注册接口,如果需要订阅者一对多则写为数据组结构体;当情况为多对多的大型数据流通时,则需要专门写一个消息中心应用代码(ROS 中的话题就是这一种)用于统一管理(又称为中介者模式)
5.6.2.2. 策略模式
- 当系统中有多类同种硬件或多种算法,需要选择性使用时,策略模式能够解决传统写法中充斥大量
if-else或switch-case的问题 - 核心逻辑:
- 定义一个包含函数指针的结构体用于定义接口规范
- 在初始化时完成函数指针的所有指向,从而统一后续的所有接口
- 代码示例
/**
* @brief 初始化PID控制器对象
* @param obj 指向PID_t对象的指针
* @return 初始化成功返回true,失败返回false
*/
bool PID_Init(PID_t* obj, PID_Mode_te mode)
{
if(obj == 0) return false;
// 初始化成员
obj->p = 0.0f;
obj->i = 0.0f;
obj->d = 0.0f;
obj->output = 0.0f;
obj->_integral = 0.0f;
obj->_last_actual = 0.0f;
// 绑定方法
obj->deInit = PID_DeInit;
obj->setPID = PID_SetPID;
if(mode == PID_MODE_PI) obj->controller = PI_Controller; // 根据模式
else if(mode == PID_MODE_PD) obj->controller = PD_Controller; // 绑定不同方法
else if(mode == PID_MODE_PID) obj->controller = PID_Controller; // 在初始化时完成
obj->infoParam = PID_InfoParam;
return true;
}5.6.2.3. 单例模式
单例模式的核心作用,是管理全局唯一资源,所谓全局唯一资源,是指在系统中逻辑上只应该存在一份的对象,例如:
核心逻辑
- 分为数据单例和接口单例或二者结合
- 用
.c隐藏数据实例,对外提供唯一访问入口 - 必要时加入初始化保护、重复初始化保护、互斥保护
- 用
extern const struct ...对外提供接口,形成一个完整的模块
优点
- 明确资源唯一性
- 避免对象被随意复制
- 便于集中管理全局能力
- 调用时 Intellisense 更加清晰,更加模块化
- 无需修改上层,由于接口单例中“接口与实现分离”,只需修改接口指向即可实现切换底层(如定义 motor 控制接口单例,切换不同厂家的电机只需切换单例的指向)
风险
单例模式最容易被滥用,很多人一看到“全局都能用”就很开心,最后把一堆本不该全局唯一的对象也做成单例,结果整个系统到处都在偷偷依赖全局变量,所以要记住:“访问方便”不是使用单例的理由,“逻辑上必须唯一”才是使用单例的理由
什么时候适合用
- 系统里真的只允许存在一个实例
- 这个实例承担的是全局基础服务
- 它的生命周期与整个系统一致
什么时候不适合用
- 一个模块未来可能需要多个实例
- 对象具有明显的设备实例属性
- 只是为了偷懒不想传
示例
#_ifndef_ __wifi_bt_h__
#_define_ __wifi_bt_h__
#_include_ "_infra/delay.h_"
#_include_ "_platform/uart.h_"
#_include_ <_stdint.h_>
//_ ! ========================= 接 口 变 量 / Typedef 声 明 ========================= ! //_
/**
_ * @brief WiFi/BT 模块实例 - 用户自定义名称,包含状态码和函数指针_
_ _*/
#_define_ _wifi_ wifi_bt_instance
/**
_ * @brief WiFi/BT 模块状态码表,使用 X-Macro 定义,方便维护和扩展_
_ * @param OK 成功_
_ * @param ERROR 错误_
_ * @param WAIT_AP 等待连接 AP_
_ * @param TIMEOUT 超时_
_ * @param PROCESSING 处理中_
_ * @param FRAME_READY 有帧可处理_
_ * @param NO_FRAME 没有帧可处理_
_ _*/
#_define_ _WIFI_BT_STATUS_TABLE_
_X_(OK, _0_)
_X_(ERROR, _1_)
_X_(WAIT_AP, _2_)
_X_(TIMEOUT, _3_)
_X_(PROCESSING, _4_)
_X_(FRAME_READY, _5_)
_X_(NO_FRAME, _6_)
/**
_ * @brief WiFi/BT 模块状态码,由 X-Macro 自动生成枚举类型_
_ _*/
#_define_ _X_(_name_, _value_) WIFI_BT__##name_ = value,
_typedef_ _enum_ {
_WIFI_BT_STATUS_TABLE_
} _WifiBtStatus_;
#_undef_ _X_
/**
_ * @brief WiFi/BT 模块工作模式_
_ * @param STA 站点模式_
_ * @param SOFT_AP 软 AP 模式_
_ * @param AP_STA AP + 站点模式_
_ _*/
_typedef_ _enum_ {
_STA_ = _0_,
_SOFT_AP_ = _2_,
_AP_STA_ = _3_,
} _WifiBtWorkMode_;
/**
_ * @brief 网络协议类型_
_ * @param TCP TCP 协议_
_ * @param UDP UDP 协议_
_ _*/
_typedef_ _enum_ {
_TCP_ = _0_,
_UDP_ = _1_,
} _NetworkProtocol_;
/**
_ * @brief 本地角色_
_ * @param Client 客户端_
_ * @param Server 服务器_
_ _*/
_typedef_ _enum_ {
_Client_ = _0_,
_Server_ = _1_,
} _LocalRole_;
/**
_ * @brief WiFi/BT 连接信息结构体_
_ * @param ssid WiFi SSID_
_ * @param password WiFi 密码_
_ * @param protocol 网络协议类型_
_ * @param role 本地角色_
_ * @param ip 远程 IP 地址_
_ * @param remote_port 远程端口_
_ * @param local_port 本地端口_
_ * @param socket_port 套接字端口_
_ _*/
_typedef_ _struct_ {
_const_ _char_* _ssid_;
_const_ _char_* _password_;
_NetworkProtocol_ _protocol_;
_LocalRole_ _role_;
_char_ _ip_[_16_];
_uint16_t_ _remote_port_;
_uint16_t_ _local_port_;
_uint16_t_ _socket_port_;
} _WifiBtConnectInfo_;
///_ @brief WiFi/BT 模块帧缓冲区大小_
#_define_ _WIFI_BT_FRAME_RX_BUF_SIZE_ _512_
///_ @brief WiFi/BT 模块帧发送缓冲区大小_
#_define_ _WIFI_BT_FRAME_BUF_SIZE_ _256_
/**
_ * @brief WiFi/BT 模块单例结构体,包含状态码和函数指针_
_ _*/
#_define_ _X_(_name_, _value_) _const_ WifiBtStatus name;
_extern_ _const_ _struct_ _WifiBtInstance_ {
/**
_ * @brief WiFi/BT 模块状态码_
_ * @param OK 成功_
_ * @param ERROR 错误_
_ * @param WAIT_AP 等待连接 AP_
_ * @param TIMEOUT 超时_
_ * @param PROCESSING 处理中_
_ * @param FRAME_READY 有帧可处理_
_ * @param NO_FRAME 没有帧可处理_
_ _*/
_struct_ {
_WIFI_BT_STATUS_TABLE_
};
/**
_ * @brief WiFi/BT 模块初始化函数,配置 UART 端口、工作模式、帧解析器等_
_ * @param uart_instance UART 端口枚举值,表示使用哪个 UART 进行通信_
_ * @param mode WiFi/BT 工作模式枚举值,表示模块的工作模式(如 STA、SOFT_AP 等)_
_ * @param header 帧头标识指针,用于帧头匹配_
_ * @param header_len 帧头标识的长度,最小为 2 字节_
_ * @return WifiBtStatus 枚举类型,表示操作结果_
_ _*/
_WifiBtStatus_(*_init_)(_Uart_te_ _uart_, _WifiBtWorkMode_ _mode_, _const_ _uint8_t_* _const_ _header_, _const_ _uint8_t_ _header_len_);
/**
_ * @brief 发送 AT 命令到 WiFi/BT 模块_
_ * @param cmd 要发送的 AT 命令字符串_
_ * @return WifiBtStatus 枚举类型,表示操作结果_
_ _*/
_WifiBtStatus_(*_send_cmd_)(_const_ _char_* _cmd_);
/**
_ * @brief 加入 WiFi AP,连接到指定的 WiFi 网络_
_ * @param ssid WiFi 网络的 SSID_
_ * @param pwd WiFi 网络的密码_
_ * @return WifiBtStatus 枚举类型,表示操作结果_
_ _*/
_WifiBtStatus_(*_join_ap_)(_const_ _char_* _ssid_, _const_ _char_* _password_);
/**
_ * @brief 重新加入 WiFi AP_
_ * @param ssid WiFi 网络的 SSID_
_ * @param pwd WiFi 网络的密码_
_ * @return WifiBtStatus 枚举类型,表示操作结果_
_ _*/
_WifiBtStatus_(*_rejoin_ap_)(_const_ _char_* _ssid_, _const_ _char_* _password_);
/**
_ * @brief 检查 WiFi AP 连接状态_
_ * @return WifiBtStatus 枚举类型,表示操作结果_
_ _*/
_WifiBtStatus_(*_check_ap_)(_void_);
/**
_ * @brief WiFi/BT 模块连接函数,建立 socket 连接并进入透传模式_
_ * @param info 包含连接信息的 WifiBtConnectInfo 结构体,至少需要包含 ssid、password、protocol、role、ip、remote_port 和 local_port 字段_
_ * @return WifiBtStatus 枚举类型,表示操作结果_
_ _*/
_WifiBtStatus_(*_connect_)(_WifiBtConnectInfo_* _info_);
/**
_ * @brief 断开 WiFi/BT 模块的连接,关闭指定的 socket 连接_
_ * @param info 包含连接信息的 WifiBtConnectInfo 结构体,至少需要包含 socket_port 字段_
_ * @return WifiBtStatus 枚举类型,表示操作结果_
_ _*/
_WifiBtStatus_(*_disconnect_)(_WifiBtConnectInfo_* _info_);
/**
_ * @brief 复位 W800 模块并重新初始化(AT+Z)_
_ * @param mode 工作模式_
_ _*/
_WifiBtStatus_(*_reset_)(_WifiBtWorkMode_ _mode_);
/**
_ * @brief 进入透传模式_
_ * @param socket_id 要进入透传模式的 socket 连接 ID,通常由 wifi_bt_connect 函数返回_
_ * @return WifiBtStatus 枚举类型,表示操作结果_
_ _*/
_WifiBtStatus_(*_enter_transparent_)(_uint16_t_ _socket_id_);
/**
_ * @brief 退出透传模式_
_ _*/
_void_(*_exit_transparent_)(_void_);
/**
_ * @brief 心跳检测函数,定期发送心跳帧并检查连接状态,必要时尝试重连_
_ * @param info 包含连接信息的 WifiBtConnectInfo 结构体,至少需要包含 socket_port 字段_
_ * @param timeout_ms 心跳超时时间,单位为毫秒_
_ * @return WifiBtStatus 枚举类型,表示操作结果_
_ _*/
_WifiBtStatus_(*_heartbeat_)(_WifiBtConnectInfo_* _info_, _ms_t_ _timeout_ms_);
/**
_ * @brief 处理 WiFi/BT 模块接收的数据,解析帧数据并检查是否有新的帧就绪_
_ * @param frame_buf 输出参数,指向存储帧数据的缓冲区指针_
_ * @param frame_len 输出参数,指向存储帧数据长度的变量指针_
_ * @return WifiBtStatus 枚举类型,表示操作结果_
_ _*/
_WifiBtStatus_(*_process_)(_uint8_t_** _const_ _frame_buf_, _uint16_t_* _const_ _frame_len_);
/**
_ * @brief 发送数据帧到 WiFi/BT 模块,数据将通过指定的 socket 连接发送_
_ * @param info 包含连接信息的 WifiBtConnectInfo 结构体,至少需要包含 socket_port 字段_
_ * @param frame 要发送的数据帧缓冲区指针_
_ * @param length 要发送的数据帧长度_
_ * @return WifiBtStatus 枚举类型,表示操作结果_
_ _*/
_WifiBtStatus_(*_send_frame_)(_WifiBtConnectInfo_ _info_, _const_ _uint8_t_* _frame_, _uint16_t_ _length_);
/**
_ * @brief 发送字符串数据,自动包装帧头和长度信息_
_ * @param info 包含连接信息的 WifiBtConnectInfo 结构体,至少需要包含 socket_port 字段_
_ * @param data 要发送的字符串数据指针_
_ * @param length 要发送的字符串数据长度_
_ * @return WifiBtStatus 枚举类型,表示操作结果_
_ _*/
_WifiBtStatus_(*_send_)(_WifiBtConnectInfo_ _info_, _const_ _char_* _data_, _uint16_t_ _length_);
} _wifi_bt_instance_;
#_undef_ _X_
//_ ! ========================= 接 口 函 数 声 明 ========================= ! //_
_WifiBtStatus_ _wifi_bt_init_(_Uart_te_ _uart_, _WifiBtWorkMode_ _mode_, _const_ _uint8_t_* _const_ _header_, _const_ _uint8_t_ _header_len_);
_WifiBtStatus_ _wifi_bt_send_cmd_(_const_ _char_* _cmd_);
_WifiBtStatus_ _wifi_bt_join_ap_(_const_ _char_* _ssid_, _const_ _char_* _password_);
_WifiBtStatus_ _wifi_bt_rejoin_ap_(_const_ _char_* _ssid_, _const_ _char_* _password_);
_WifiBtStatus_ _wifi_bt_check_ap_(_void_);
_WifiBtStatus_ _wifi_bt_connect_(_WifiBtConnectInfo_* _info_);
_WifiBtStatus_ _wifi_bt_disconnect_(_WifiBtConnectInfo_* _info_);
_WifiBtStatus_ _wifi_bt_reset_(_WifiBtWorkMode_ _mode_);
_WifiBtStatus_ _wifi_bt_enter_transparent_(_uint16_t_ _socket_id_);
_void_ _wifi_bt_exit_transparent_(_void_);
_WifiBtStatus_ _wifi_bt_heartbeat_(_WifiBtConnectInfo_* _info_, _ms_t_ _timeout_ms_);
_WifiBtStatus_ _wifi_bt_process_(_uint8_t_** _const_ _frame_buf_, _uint16_t_* _const_ _frame_len_);
_WifiBtStatus_ _wifi_bt_send_frame_(_WifiBtConnectInfo_ _info_, _const_ _uint8_t_* _frame_, _uint16_t_ _length_);
_WifiBtStatus_ _wifi_bt_send_(_WifiBtConnectInfo_ _info_, _const_ _char_* _data_, _uint16_t_ _length_);
#_endif_调用示例:
#include "device/wifi_bt.h"
void sys_init(WifiBtConnectInfo* info)
{
wifi.init(UART6, STA, (const uint8_t*)"RENE:", 5);
if (wifi.join_ap(info->ssid, info->password) != wifi.OK)
printf("WiFi 连接失败!\r\n");
else
printf("WiFi 连接成功!\r\n");
}
int main(void)
{
sys_init(&info);
if (wifi.connect(&info) == wifi.OK) {
if (wifi.enter_transparent(info.socket_port) == wifi.OK)
printf("进入透传模式成功\r\n");
else
printf("进入透传模式失败\r\n");
}
while (1) {
WifiBtStatus net_status = wifi.heartbeat(&info, 2000);
if (net_status == wifi.OK) {
if (wifi.process(&frame_buf, &frame_len) == wifi.FRAME_READY) {
// ...
}
else {
// ...
}
}
else {
// ...
}
}
}5.6.2.4. 状态模式
状态模式可以看作是“面向对象化的状态机实现方式”,它最适合解决的问题是:业务流程有复杂状态切换,而且每个状态本身的逻辑都不短,传统写法中,很多人会把所有状态、所有事件、所有行为全塞进一个大 switch-case,一开始状态少时还好,一旦状态和事件都变多,整个处理函数会迅速膨胀,状态模式的核心思想是:
- 和普通状态机的关系
状态模式并不是否定状态机,而是把状态机进一步结构化,尤其是当你开始需要:
这时候就很适合继续往层级状态机(HFSM)方向走
适用场景
- 初始化 / 待机 / 工作 / 完成 / 故障 这类阶段式流程
- 机械臂、底盘、夹爪、升降机构等设备任务流程
- 通信流程控制
- 多阶段控制任务
优点
- 比大
switch-case更容易维护 - 每个状态职责更清晰
- 入口/退出/动作逻辑天然归位
- 更容易扩展成层级状态机
- 比大
注意事项
- 状态太少时,不必强行上状态模式
- 状态边界一定要明确,否则只是把混乱拆成多个文件
- 如果状态之间共享逻辑很多,优先考虑父状态抽象,而不是复制代码
5.6.2.5. 适配器模式
- 其实就是将第三方库通过适配器对接自己的系统接口
- 核心逻辑:当系统接口是一个类中的方法时,新写一个中间函数,内部调用第三方库,外部系统接口函数指针指向该中间函数
5.6.2.6. 命令模式
命令模式的核心作用,是解耦“谁发起动作”和“谁真正执行动作”,传统写法中,业务层往往会直接调用底层驱动函数,例如“收到某个条件后立刻让电机转动、让夹爪闭合、让激光开启”,短期看这样最直接,但问题在于:
命令模式的思路就是:不要直接执行动作,而是先把动作封装成一个“命令”对象,再交给专门的执行者去执行,一个命令通常至少包含:
适用场景
优点
注意事项
代码示例
上面的例子中,应用层并没有直接调用 Motor_SetPosition() 或 Gripper_Close(),而是把动作封装成命令后放入队列,由执行器统一调度,这样后续要加:
5.6.2.7. 行为树模式
行为树模式(Behavior Tree, BT)更适合解决复杂任务决策问题,尤其是在任务流程中同时存在:
- 多条件判断
- 多种动作选择
- 回退与恢复
- 优先级切换
- 顺序执行与失败重试 和状态机相比:
- 状态机更擅长“阶段切换”
- 行为树更擅长“决策组织”
所以行为树并不是状态机的“上位替代”,而是另一种更适合复杂任务决策的组织方式。行为树在机器人和游戏 AI 中都很常见,官方文档也明确把它描述为一种决策引擎;其中叶子节点通常是动作或条件节点,而组合节点常见为 Sequence、Selector、Parallel,另外还有 Decorator 和 Blackboard 等机制
- 本文链接:https://kaede-rei.github.io/learning-path/electrical-control/5
- 版权声明:本博客所有文章除特别声明外,均默认采用 CC BY-NC-SA 许可协议。