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。
音频格式延迟命令¶
命令字 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
};

