Skip to content

XU316 MCU开发示例

本示例基于 GD32 平台,其他平台请自行适配。

最小使用示例

这段代码展示了如何初始化协议层并在主循环中处理数据。用户需要实现自己的 UART 发送函数,并在串口接收中断中调用 ring_buffer_write 将数据写入环形缓冲区。

#include "xu316_protocol.h"

// 发送回调:将数据通过UART发送给XU316
// 用户需要根据自己的硬件实现uart_send函数
void my_send(const uint8_t *data, uint16_t len) {
    uart_send(data, len);
}

// 日志回调:输出调试信息(可选)
void my_log(const char *tag, const char *msg) {
    printf("[%s] %s\n", tag, msg);
}

// 初始化
void init(void) {
    xu316_port_t port = {
        .send = my_send,
        .log = my_log
    };
    xu316_register_port(&port);
    xu316_init();
}

// 串口接收中断中调用,将接收到的数据写入环形缓冲区
void uart_rx_isr(uint8_t *data, uint16_t len) {
    ring_buffer_write(data, len);
}

// 主循环中周期性调用,处理接收到的命令
void main_loop(void) {
    uart_data_process();
}

协议层函数

校验和计算

计算从帧头到数据区所有字节的累加和,结果对 256 取余。该校验和用于验证数据帧的完整性。

uint8_t xu316_calc_checksum(uint8_t *data, uint8_t len) {
    uint8_t sum = 0;
    for (uint8_t i = 0; i < len; i++) {
        sum += data[i];
    }
    return sum;
}

帧封装函数

将命令字和数据封装成符合协议格式的数据帧,并通过注册的 send 回调发送出去。

帧格式:帧头(0x55AA) + 版本(0x03) + 命令字 + 数据长度 + 数据区 + 校验和

int xu316_pack_frame(uint8_t cmd, uint8_t *data, uint8_t len) {
    memset(g_tx_buffer, 0, sizeof(g_tx_buffer));
    g_tx_buffer[0] = FRAME_HEADER_H;      // 0x55
    g_tx_buffer[1] = FRAME_HEADER_L;      // 0xAA
    g_tx_buffer[2] = PROTOCOL_VERSION_RX; // 0x03 (MCU版本)
    g_tx_buffer[3] = cmd;                 // 命令字
    g_tx_buffer[4] = len;                 // 数据长度

    if (data && len > 0) {
        memcpy(g_tx_buffer + 5, data, len);
    }

    // 计算校验和(帧头+版本+命令字+数据长度+数据区)
    g_tx_buffer[len + 5] = xu316_calc_checksum(g_tx_buffer, len + 5);

    // 通过注册的回调发送
    if (g_xu316_port.send) {
        g_xu316_port.send(g_tx_buffer, len + 6);
    }
    return len + 6;
}

数据帧提取

从环形缓冲区中预读数据,检查帧头是否有效。如果帧头有效,返回完整帧的长度;如果无效,返回 -1 表示需要丢弃字节。

static int check_frame_length(uint8_t *buf, uint16_t len) {
    // 检查帧头是否为0x55AA
    if (buf[0] != FRAME_HEADER_H || buf[1] != FRAME_HEADER_L) {
        return -1;  // 帧头错误
    }
    // 返回完整帧长度 = 数据长度 + 6(帧头2+版本1+命令1+长度1+校验1)
    return buf[4] + 6;
}

数据处理主函数

在主循环中周期性调用。从环形缓冲区中提取完整的数据帧,检查帧头有效性,确认数据长度足够后,将完整帧复制到接收缓冲区,然后调用 uart_data_parse 进行命令解析。如果帧头无效,会逐字节丢弃直到找到有效帧头。

void uart_data_process(void) {
    uint8_t peek_buffer[8];      // 预读缓冲区
    uint8_t process_buffer[256]; // 帧数据缓冲区
    int frame_length;

    // 至少需要6字节才能开始检查帧头
    while (uart_ring_buffer.count >= 6) {
        // 预读6字节检查帧头
        ring_buffer_peek(peek_buffer, 6);

        // 检查帧长度
        frame_length = check_frame_length(peek_buffer, 6);
        if (frame_length < 0) {
            // 帧头无效,丢弃1字节继续查找
            uint8_t dummy;
            ring_buffer_read(&dummy, 1);
            continue;
        }

        // 检查环形缓冲区中是否有足够的数据组成完整帧
        if (uart_ring_buffer.count < (uint16_t)frame_length) {
            break;  // 数据不足,等待更多数据
        }

        // 读取完整帧到处理缓冲区
        if (ring_buffer_read(process_buffer, (uint16_t)frame_length)) {
            // 将完整帧复制到全局接收缓冲区
            memcpy(g_rx_buffer, process_buffer, frame_length);
            g_rx_count = frame_length;
            g_rx_data = g_rx_buffer;
            // 调用解析函数处理命令
            uart_data_parse();
        }
    }
}

命令解析

解析入口

协议处理的核心函数。对接收到的数据帧进行校验,提取命令字和数据区,然后根据命令字分发到对应的处理逻辑。

int uart_data_parse(void) {
    int ret = 0;
    uint8_t cmd = 0;
    uint16_t data_len = 0;
    uint16_t rx_len = (uint16_t)g_rx_count;
    static uint8_t buffer[256] = {0};

    // 帧校验:检查帧格式和校验和
    ret = uart_frame_check((uint8_t *)g_rx_data, (uint8_t)rx_len);
    if (ret == 0) {
        return -1;  // 帧校验失败
    }

    // 计算数据区长度:总长度 - 6(帧头2+版本1+命令1+长度1+校验1)
    data_len = rx_len - 6;
    // 提取命令字(第4个字节,索引3)
    cmd = g_rx_data[3];
    // 复制数据区到缓冲区(从第6个字节开始,索引5)
    memcpy(buffer, g_rx_data + 5, data_len);

    // 根据命令字分发处理
    switch (cmd) {
        case 0x00: /* 启动命令处理 */   break;
        case 0x01: /* 读取产品信息 */   break;
        case 0x02: /* 读取上电配置 */   break;
        case 0x03: /* 获取音频模式 */   break;
        case 0x04: /* 获取用户配置 */   break;
        case 0x05: /* 启动完成 */       break;
        case 0x20: /* 状态报告 */       break;
        case 0x22: /* 音频格式设置 */   break;
        case 0x24: /* 播放音量 */       break;
        case 0x25: /* 录音音量 */       break;
        case 0x27: /* 静音解除响应 */   break;
        case 0x28: /* 音频格式延迟响应 */ break;
        case 0xEE: /* HID透传 */        break;
        default:   /* 未知命令处理 */   break;
    }
    return ret;
}

命令参考

启动命令 (0x00)

XU316 上电或重启后首先发送此命令。XU316 发送 17 字节数据,包含 VID/PID 和 CRC 校验值。MCU 需要比较 CRC 判断是否需要更新配置,然后回复 1 字节的启动选项。

case 0x00: {
    // XU316发送数据格式:
    //   重启原因(1B) + VID1(2B) + PID1(2B) + VID2(2B) + PID2(2B) + CRC1(4B) + CRC2(4B) = 17字节
    // MCU回复:启动选项(1B)

    // 解析XU316发送的数据
    uint8_t reboot_reason = buffer[0];           // 0x00=上电重启, 0x01=切换模式, 0xFF=其他
    uint8_t *vid_uac1 = buffer + 1;              // UAC1.0 VID
    uint8_t *pid_uac1 = buffer + 3;              // UAC1.0 PID
    uint8_t *vid_uac2 = buffer + 5;              // UAC2.0 VID
    uint8_t *pid_uac2 = buffer + 7;              // UAC2.0 PID
    uint8_t *basic_crc = buffer + 9;             // 基础信息CRC
    uint8_t *power_crc = buffer + 13;            // 上电配置CRC

    // 判断是否需要更新配置:比较XU316发送的CRC与本地存储的CRC
    mcu_data.boot_option = 0;
    if (memcmp(basic_crc, mcu_data.basic_info_crc, 4) != 0) {
        mcu_data.boot_option |= BOOT_OPTION_UPDATE_BASIC_INFO;  // 需要更新基础信息
    }
    if (memcmp(power_crc, mcu_data.power_cfg_crc, 4) != 0) {
        mcu_data.boot_option |= BOOT_OPTION_UPDATE_POWER_CFG;   // 需要更新上电配置
    }

    // 发送回复:启动选项
    ret = xu316_pack_frame(cmd, &mcu_data.boot_option, CMD00_MCU_DATA_LEN);
    break;
}

读取产品信息 (0x01)

XU316 请求获取产品基础信息。MCU 需要回复 60 字节数据,包含 VID、PID、制造商名称、产品名称、序列号和 CRC 校验值。

case 0x01: {
    // XU316发送:无数据
    // MCU回复:60字节产品信息
    //   VID1(2B) + PID1(2B) + VID2(2B) + PID2(2B) +
    //   制造商(16B) + 产品名(16B) + 序列号(16B) + CRC(4B)

    // 重新计算基础信息的CRC32校验值(前56字节)
    crc = calculate_crc32(mcu_data.vid_uac1, 56);
    // 将CRC存储为大端序
    mcu_data.basic_info_crc[0] = (uint8_t)((crc >> 24) & 0xFF);
    mcu_data.basic_info_crc[1] = (uint8_t)((crc >> 16) & 0xFF);
    mcu_data.basic_info_crc[2] = (uint8_t)((crc >> 8) & 0xFF);
    mcu_data.basic_info_crc[3] = (uint8_t)(crc & 0xFF);

    // 发送产品信息(从vid_uac1开始的60字节)
    ret = xu316_pack_frame(cmd, mcu_data.vid_uac1, CMD01_MCU_DATA_LEN);
    break;
}

读取上电配置 (0x02)

XU316 请求获取上电配置参数。MCU 回复 14 字节数据,包含启动状态、音频模式、静音时间、音量设置和 CRC 校验值。

case 0x02: {
    // XU316发送:无数据
    // MCU回复:14字节配置信息
    //   启动状态(1B) + 音频模式(5B) + 静音时间(2B) +
    //   麦克风音量(1B) + 左音量(1B) + 右音量(1B) + CRC(4B)

    // 重新计算上电配置的CRC32校验值(audio_mode开始的10字节)
    crc = calculate_crc32((uint8_t *)&mcu_data.audio_mode, 0x0a);
    // 存储为大端序
    mcu_data.power_cfg_crc[0] = (uint8_t)((crc >> 24) & 0xFF);
    mcu_data.power_cfg_crc[1] = (uint8_t)((crc >> 16) & 0xFF);
    mcu_data.power_cfg_crc[2] = (uint8_t)((crc >> 8) & 0xFF);
    mcu_data.power_cfg_crc[3] = (uint8_t)(crc & 0xFF);

    // 发送上电配置(从audio_mode开始的14字节)
    ret = xu316_pack_frame(cmd, (uint8_t *)&mcu_data.audio_mode, CMD02_MCU_DATA_LEN);
    break;
}

获取音频模式 (0x03)

XU316 请求获取当前输入输出模式。MCU 回复 5 字节的音频模式配置。

case 0x03: {
    // XU316发送:无数据
    // MCU回复:5字节音频模式

    ret = xu316_pack_frame(cmd, (uint8_t *)&mcu_data.audio_mode, CMD03_MCU_DATA_LEN);
    break;
}

获取用户配置 (0x04)

XU316 请求获取用户配置参数。MCU 回复 14 字节,格式与 0x02 命令相同。会设置当前选中的音频模式并重新计算 CRC。

case 0x04: {
    // XU316发送:无数据
    // MCU回复:14字节用户配置(格式同0x02)

    // 设置当前选中的音频模式
    memcpy((uint8_t *)&mcu_data.audio_mode, audio_modes[g_current_mode], 5);

    // 计算CRC
    crc = calculate_crc32((uint8_t *)&mcu_data.audio_mode, 10);
    mcu_data.power_cfg_crc[0] = (uint8_t)((crc >> 24) & 0xFF);
    mcu_data.power_cfg_crc[1] = (uint8_t)((crc >> 16) & 0xFF);
    mcu_data.power_cfg_crc[2] = (uint8_t)((crc >> 8) & 0xFF);
    mcu_data.power_cfg_crc[3] = (uint8_t)(crc & 0xFF);

    ret = xu316_pack_frame(cmd, (uint8_t *)&mcu_data.audio_mode, CMD04_MCU_DATA_LEN);
    break;
}

启动完成 (0x05)

XU316 通知 MCU 启动完成,并发送当前状态数据。MCU 只需回复确认(无数据)。

case 0x05: {
    // XU316发送:21字节状态数据
    // MCU回复:无数据

    // 保存XU316发送的状态到mcu_data结构体
    memcpy(&mcu_data.startup_status, buffer, 15);

    // 回复确认
    ret = xu316_pack_frame(cmd, NULL, CMD05_MCU_DATA_LEN);
    break;
}

状态报告 (0x20)

XU316 在状态变化时主动上报。MCU 保存状态数据并回复确认。

case 0x20: {
    // XU316发送:20字节状态数据
    //   音频模式(5B) + 静音时间(2B) + 麦克风音量(1B) + 左音量(1B) + 右音量(1B) + CRC(4B)
    // MCU回复:无数据

    // 保存状态数据
    memcpy(&mcu_data.audio_mode, buffer, 14);

    // 回复确认
    ret = xu316_pack_frame(cmd, NULL, CMD20_MCU_DATA_LEN);
    break;
}

音频格式设置 (0x22)

XU316 通知当前播放的音频格式和类型。MCU 保存这些信息并回复确认。

case 0x22: {
    // XU316发送:2字节(音频格式 + 音频类型)
    // MCU回复:无数据

    // 保存音频格式和类型
    memcpy(&mcu_data.audio_format, buffer, 2);

    // 回复确认
    ret = xu316_pack_frame(cmd, NULL, CMD22_MCU_DATA_LEN);
    break;
}

播放音量 (0x24)

XU316 发送当前播放音量。MCU 保存左右声道音量值并回复确认。

case 0x24: {
    // XU316发送:2字节(左音量 + 右音量)
    // MCU回复:无数据

    // 保存音量值
    memcpy(&mcu_data.dac_l_volume, buffer, 2);

    // 回复确认
    ret = xu316_pack_frame(cmd, NULL, CMD24_MCU_DATA_LEN);
    break;
}

HID 透传 (0xEE)

HID 数据透传或 OTA 升级数据。XU316 发送 57 字节数据,MCU 需要回复 57 字节(Echo 模式)。

case 0xEE: {
    // XU316发送:57字节HID/OTA数据
    // MCU回复:57字节(Echo模式)

    // 检查版本号和数据长度
    if (g_rx_data[2] == 0x00 && g_rx_data[4] == CMD_HID_TRANSPARENT_DATA_LEN) {
        // 简单Echo回复:将接收到的数据原样返回
        ret = xu316_pack_frame(0xEE, buffer, CMD_HID_TRANSPARENT_MCU_DATA_LEN);
    }
    break;
}

MCU 主动发送命令

以下命令由 MCU 主动发送给 XU316。

静音解除命令

命令字 0x27,数据为 2 字节 0x00

void send_unmute_cmd(void) {
    uint8_t data[2] = {0x00, 0x00};
    xu316_pack_frame(0x27, data, 2);
}

音频格式延迟命令

命令字 0x28,数据为 2 字节 0x00

void send_audio_format_delay_cmd(void) {
    uint8_t data[2] = {0x00, 0x00};
    xu316_pack_frame(0x28, data, 2);
}

媒体控制命令

命令字 0x21,数据为 1 字节的媒体控制码。

void send_media_control(media_control_t cmd) {
    uint8_t data = (uint8_t)cmd;
    xu316_pack_frame(0x21, &data, 1);
}

// 使用示例
send_media_control(MEDIA_KEY_PLAY_PAUSE);  // 播放/暂停
send_media_control(MEDIA_KEY_VOLUME_UP);   // 音量增加
send_media_control(MEDIA_KEY_VOLUME_DOWN); // 音量减小

设置音频模式

命令字 0x23,数据为 5 字节音频模式配置。

void set_audio_mode(uint8_t mode_index) {
    // audio_modes是预定义的8种模式数组
    xu316_pack_frame(0x23, (uint8_t *)audio_modes[mode_index], 5);
}

// 8种预定义音频模式
static const uint8_t audio_modes[][5] = {
    {0x00, 0x80, 0xa9, 0x00, 0x01},  // UAC2.0->I2S
    {0x00, 0x80, 0x01, 0x00, 0x02},  // UAC1.0-I2S
    {0x10, 0x80, 0x65, 0x10, 0x03},  // S/PDIF1 IN-I2S OUT (COAX)
    {0x00, 0x80, 0x65, 0x10, 0x04},  // S/PDIF2 IN-I2S OUT (OPT)
    {0x00, 0x80, 0xc5, 0x08, 0x05},  // UAC2.0-SPDIF OUT
    {0x00, 0x82, 0xd5, 0x81, 0x06},  // I2S IN-I2S OUT
    {0x20, 0x80, 0x65, 0x10, 0x07},  // S/PDIF3 IN-I2S OUT (HDMI)
    {0x30, 0x80, 0x65, 0x10, 0x08}   // S/PDIF4 IN-I2S OUT
};

咨询反馈

点击展开咨询反馈表单
×

提示

公司名:

邮箱地址:

主旨:

正文: