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:...。所以解析器要能处理多行数据,直到收到 OKERROR 为止。

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,那个更刺激——音频延迟、回声消除、音量控制……嗯,到时候再细说。