4、Wi-Fi/BT通信HAL:Wi-Fi模组AT指令封装、蓝牙A2DP/HFP抽象层、网络状态回调机制
好,咱们进入通信模块的HAL设计。这一章我打算聊聊智能音箱里最核心的两个无线通信方式——Wi-Fi和蓝牙。说实话,这两个东西在项目里踩过的坑,比我吃过的盐还多(笑)。
Wi-Fi负责联网,蓝牙负责音频传输。你想想看,如果HAL层没设计好,上层应用调起来会非常痛苦。我个人习惯是把通信HAL分成三层:物理层封装、协议抽象层、状态回调层。这样分工明确,后期维护也轻松。
4.1 Wi-Fi模组AT指令封装
很多Wi-Fi模组(比如ESP8266、RTL8710)都支持AT指令。但直接裸调AT指令?那代码会乱成一锅粥。我建议做一层指令封装器。
核心思路:把AT指令变成函数调用,把串口数据流变成事件驱动。
举个例子,连接Wi-Fi的AT指令是 AT+CWJAP="SSID","PASSWORD"。我们封装成:
// wifi_hal.h
typedef void (*wifi_connect_cb_t)(bool success, const char* ip);
int wifi_hal_connect(const char* ssid, const char* pwd, wifi_connect_cb_t cb);
int wifi_hal_disconnect(void);
int wifi_hal_scan(wifi_scan_cb_t cb);
你看,上层根本不需要知道AT指令长什么样。我在项目中遇到过一个问题:AT指令的响应时间不稳定,有时候200ms,有时候2秒。如果同步等待,整个系统就卡死了。所以必须用异步回调。
我的经验:AT指令封装里一定要加超时机制。我曾经遇到过模组死机,AT指令发出去没响应,整个Wi-Fi线程就挂在那了。后来我加了个看门狗定时器,5秒没响应就复位模组。
封装层的内部实现,我一般用状态机:
// 伪代码
typedef enum {
WIFI_IDLE,
WIFI_SENDING,
WIFI_WAITING_RESP,
WIFI_TIMEOUT,
WIFI_ERROR
} wifi_state_t;
static wifi_state_t g_state = WIFI_IDLE;
static char g_rx_buffer[256];
static uint32_t g_timeout_ms;
void wifi_hal_process(void) {
// 从UART读取数据,解析响应
// 匹配 "OK" 或 "FAIL" 或 "ERROR"
// 触发对应的回调
}
嗯,这里要注意:AT指令的响应可能是多行的。比如查询Wi-Fi列表,会返回一堆 +CWLAP:...。所以解析器要能处理多行数据,直到收到 OK 或 ERROR 为止。
4.2 蓝牙A2DP/HFP抽象层
蓝牙这块更复杂。A2DP负责播放音乐,HFP负责通话。两个协议栈完全不同,但上层应用希望统一接口。
我设计的抽象层长这样:
// bt_audio_hal.h
typedef enum {
BT_AUDIO_SOURCE_A2DP, // 音乐播放
BT_AUDIO_SOURCE_HFP, // 通话
BT_AUDIO_SOURCE_NONE
} bt_audio_source_t;
typedef struct {
bt_audio_source_t source;
int volume; // 0-100
bool is_streaming;
} bt_audio_status_t;
// 回调
typedef void (*bt_audio_connected_cb_t)(bt_audio_source_t source);
typedef void (*bt_audio_disconnected_cb_t)(bt_audio_source_t source);
typedef void (*bt_audio_volume_cb_t)(int volume);
int bt_audio_hal_init(void);
int bt_audio_hal_start_stream(bt_audio_source_t source);
int bt_audio_hal_stop_stream(void);
int bt_audio_hal_set_volume(int volume);
说白了,就是把A2DP和HFP的差异藏起来。上层只需要知道「现在在播放音乐」还是「在通话」,音量怎么调,连接断开了怎么办。
避坑指南:我曾经在A2DP和HFP切换时遇到音频卡顿。原因是两个协议栈共用了同一个I2S接口,切换时没有做静音处理。后来我在切换前先 mute 音频输出,切换完成后再 unmute。这个坑踩得我印象深刻。
蓝牙的HAL层还有一个关键点:蓝牙状态机。蓝牙的状态变化比Wi-Fi复杂得多:
// 蓝牙状态机简化版
typedef enum {
BT_IDLE,
BT_DISCOVERING,
BT_PAIRING,
BT_CONNECTING_A2DP,
BT_CONNECTING_HFP,
BT_CONNECTED_A2DP,
BT_CONNECTED_HFP,
BT_CONNECTED_DUAL, // A2DP + HFP 同时连接
BT_DISCONNECTING
} bt_state_t;
你想想看,如果状态机没设计好,用户一边听歌一边来电话,系统就乱了。我建议把状态变化通过回调通知上层,上层再决定是暂停音乐还是混音。
4.3 网络状态回调机制
网络状态变化是智能音箱最频繁的事件。Wi-Fi断开、重连、IP变化、蓝牙断开……如果每个模块都自己去轮询,代码会非常碎片化。
我设计了一个统一网络状态管理器:
// net_status_hal.h
typedef enum {
NET_STATUS_WIFI_CONNECTED,
NET_STATUS_WIFI_DISCONNECTED,
NET_STATUS_WIFI_GOT_IP,
NET_STATUS_WIFI_LOST_IP,
NET_STATUS_BT_CONNECTED,
NET_STATUS_BT_DISCONNECTED,
NET_STATUS_BT_A2DP_STARTED,
NET_STATUS_BT_HFP_STARTED
} net_event_t;
typedef void (*net_status_cb_t)(net_event_t event, void* user_data);
int net_status_hal_register_cb(net_status_cb_t cb, void* user_data);
int net_status_hal_unregister_cb(net_status_cb_t cb);
void net_status_hal_notify(net_event_t event, void* data);
这个机制的好处是:任何模块都可以订阅网络事件。比如UI模块想显示Wi-Fi图标,只需要注册一个回调,收到 NET_STATUS_WIFI_CONNECTED 就显示已连接图标。
核心设计原则:网络状态变化由HAL层主动推送,上层被动接收。不要上层去轮询「Wi-Fi连上了吗?」这种问题。
我在项目中遇到过一个问题:Wi-Fi断开后,蓝牙还在播放音乐。用户以为音箱坏了。后来我在网络状态回调里加了联动逻辑——Wi-Fi断开时,自动暂停蓝牙音乐,并播报语音提示。这个逻辑就写在HAL层的回调分发器里。
回调机制的实现,我建议用链表+互斥锁:
static struct list_head cb_list;
static mutex_t cb_mutex;
void net_status_hal_notify(net_event_t event, void* data) {
mutex_lock(&cb_mutex);
struct cb_node *node;
list_for_each_entry(node, &cb_list, list) {
node->cb(event, data);
}
mutex_unlock(&cb_mutex);
}
嗯,这里要注意:回调函数里不能做耗时操作。如果某个回调要写Flash或者发网络请求,一定要放到任务队列里异步执行。否则一个回调卡住了,所有网络事件都发不出去。
4.4 小结
这一章我们聊了三个核心点:
- Wi-Fi AT指令封装:用状态机+异步回调,把串口数据流变成函数调用
- 蓝牙A2DP/HFP抽象层:统一接口,隐藏协议差异,注意状态机设计
- 网络状态回调机制:统一事件分发,避免轮询,注意回调执行效率
说实话,通信HAL设计得好不好,直接决定了上层应用的开发效率。我见过太多项目,Wi-Fi和蓝牙的代码混在一起,改一个bug引出三个新bug。所以,花时间把HAL层设计好,绝对是值得的。
下一章我们聊聊音频编解码HAL,那个更刺激——音频延迟、回声消除、音量控制……嗯,到时候再细说。