XU316 MCU Development Example¶
This example is based on the GD32 platform. For other platforms, please adapt accordingly.
Minimal Usage Example¶
This code demonstrates how to initialize the protocol layer and process data in the main loop. Users need to implement their own UART send function and call ring_buffer_write in the UART receive interrupt to write data into the ring buffer.
#include "xu316_protocol.h"
// Send callback: transmit data to XU316 via UART
// Users need to implement uart_send based on their hardware
void my_send(const uint8_t *data, uint16_t len) {
uart_send(data, len);
}
// Log callback: output debug information (optional)
void my_log(const char *tag, const char *msg) {
printf("[%s] %s\n", tag, msg);
}
// Initialization
void init(void) {
xu316_port_t port = {
.send = my_send,
.log = my_log
};
xu316_register_port(&port);
xu316_init();
}
// Called in UART receive interrupt, writes received data to ring buffer
void uart_rx_isr(uint8_t *data, uint16_t len) {
ring_buffer_write(data, len);
}
// Called periodically in main loop to process received commands
void main_loop(void) {
uart_data_process();
}
Protocol Layer Functions¶
Checksum Calculation¶
Calculates the cumulative sum of all bytes from the frame header to the data area, then takes the result modulo 256. This checksum is used to verify data frame integrity.
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;
}
Frame Packing¶
Encapsulates the command and data into a protocol-compliant frame and sends it through the registered send callback.
Frame format: Header(0x55AA) + Version(0x03) + Command + Data Length + Data + Checksum
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 version)
g_tx_buffer[3] = cmd; // Command
g_tx_buffer[4] = len; // Data length
if (data && len > 0) {
memcpy(g_tx_buffer + 5, data, len);
}
// Calculate checksum (header + version + command + length + data)
g_tx_buffer[len + 5] = xu316_calc_checksum(g_tx_buffer, len + 5);
// Send via registered callback
if (g_xu316_port.send) {
g_xu316_port.send(g_tx_buffer, len + 6);
}
return len + 6;
}
Frame Extraction¶
Pre-reads data from the ring buffer and checks if the frame header is valid. Returns the complete frame length if valid, or -1 if bytes need to be discarded.
static int check_frame_length(uint8_t *buf, uint16_t len) {
// Check if header is 0x55AA
if (buf[0] != FRAME_HEADER_H || buf[1] != FRAME_HEADER_L) {
return -1; // Invalid header
}
// Return complete frame length = data length + 6 (header2 + version1 + cmd1 + len1 + checksum1)
return buf[4] + 6;
}
Data Processing Main Function¶
Called periodically in the main loop. Extracts complete data frames from the ring buffer, verifies frame header validity, confirms sufficient data length, copies the complete frame to the receive buffer, and then calls uart_data_parse for command parsing. If the frame header is invalid, bytes are discarded one by one until a valid header is found.
void uart_data_process(void) {
uint8_t peek_buffer[8]; // Peek buffer
uint8_t process_buffer[256]; // Frame data buffer
int frame_length;
// Need at least 6 bytes to start checking header
while (uart_ring_buffer.count >= 6) {
// Peek 6 bytes to check header
ring_buffer_peek(peek_buffer, 6);
// Check frame length
frame_length = check_frame_length(peek_buffer, 6);
if (frame_length < 0) {
// Invalid header, discard 1 byte and continue searching
uint8_t dummy;
ring_buffer_read(&dummy, 1);
continue;
}
// Check if ring buffer has enough data for complete frame
if (uart_ring_buffer.count < (uint16_t)frame_length) {
break; // Insufficient data, wait for more
}
// Read complete frame to processing buffer
if (ring_buffer_read(process_buffer, (uint16_t)frame_length)) {
// Copy complete frame to global receive buffer
memcpy(g_rx_buffer, process_buffer, frame_length);
g_rx_count = frame_length;
g_rx_data = g_rx_buffer;
// Call parse function to process command
uart_data_parse();
}
}
}
Command Parsing¶
Parse Entry¶
The core function of protocol processing. Verifies the received data frame, extracts the command and data area, then dispatches to the corresponding handler based on the command.
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};
// Frame verification: check frame format and checksum
ret = uart_frame_check((uint8_t *)g_rx_data, (uint8_t)rx_len);
if (ret == 0) {
return -1; // Frame verification failed
}
// Calculate data area length: total length - 6 (header2 + version1 + cmd1 + len1 + checksum1)
data_len = rx_len - 6;
// Extract command (4th byte, index 3)
cmd = g_rx_data[3];
// Copy data area to buffer (from 6th byte, index 5)
memcpy(buffer, g_rx_data + 5, data_len);
// Dispatch based on command
switch (cmd) {
case 0x00: /* Boot command */ break;
case 0x01: /* Read product info */ break;
case 0x02: /* Read power-on config */ break;
case 0x03: /* Get audio mode */ break;
case 0x04: /* Get user config */ break;
case 0x05: /* Startup complete */ break;
case 0x20: /* Status report */ break;
case 0x22: /* Audio format set */ break;
case 0x24: /* Playback volume */ break;
case 0x25: /* Recording volume */ break;
case 0x27: /* Unmute response */ break;
case 0x28: /* Audio format delay */ break;
case 0xEE: /* HID passthrough */ break;
default: /* Unknown command */ break;
}
return ret;
}
Command Reference¶
Boot Command (0x00)¶
XU316 sends this command first after power-on or reboot. XU316 sends 17 bytes of data including VID/PID and CRC values. MCU compares CRC to determine if configuration needs updating, then replies with 1 byte of boot options.
case 0x00: {
// XU316 data format:
// Reboot reason(1B) + VID1(2B) + PID1(2B) + VID2(2B) + PID2(2B) + CRC1(4B) + CRC2(4B) = 17 bytes
// MCU reply: Boot option(1B)
// Parse XU316 data
uint8_t reboot_reason = buffer[0]; // 0x00=power-on, 0x01=mode switch, 0xFF=other
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; // Basic info CRC
uint8_t *power_crc = buffer + 13; // Power config CRC
// Check if config update is needed: compare XU316 CRC with local stored 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; // Need to update basic info
}
if (memcmp(power_crc, mcu_data.power_cfg_crc, 4) != 0) {
mcu_data.boot_option |= BOOT_OPTION_UPDATE_POWER_CFG; // Need to update power config
}
// Send reply: boot option
ret = xu316_pack_frame(cmd, &mcu_data.boot_option, CMD00_MCU_DATA_LEN);
break;
}
Read Product Info (0x01)¶
XU316 requests product basic information. MCU replies with 60 bytes of data including VID, PID, manufacturer name, product name, serial number, and CRC.
case 0x01: {
// XU316 sends: no data
// MCU reply: 60 bytes product info
// VID1(2B) + PID1(2B) + VID2(2B) + PID2(2B) +
// Manufacturer(16B) + Product(16B) + Serial(16B) + CRC(4B)
// Recalculate CRC32 for basic info (first 56 bytes)
crc = calculate_crc32(mcu_data.vid_uac1, 56);
// Store CRC in big-endian
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);
// Send product info (60 bytes starting from vid_uac1)
ret = xu316_pack_frame(cmd, mcu_data.vid_uac1, CMD01_MCU_DATA_LEN);
break;
}
Read Power-On Config (0x02)¶
XU316 requests power-on configuration parameters. MCU replies with 14 bytes including boot status, audio mode, mute time, volume settings, and CRC.
case 0x02: {
// XU316 sends: no data
// MCU reply: 14 bytes config info
// Boot status(1B) + Audio mode(5B) + Mute time(2B) +
// Mic volume(1B) + Left vol(1B) + Right vol(1B) + CRC(4B)
// Recalculate CRC32 for power config (10 bytes starting from audio_mode)
crc = calculate_crc32((uint8_t *)&mcu_data.audio_mode, 0x0a);
// Store in big-endian
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);
// Send power config (14 bytes starting from audio_mode)
ret = xu316_pack_frame(cmd, (uint8_t *)&mcu_data.audio_mode, CMD02_MCU_DATA_LEN);
break;
}
Get Audio Mode (0x03)¶
XU316 requests current input/output mode. MCU replies with 5 bytes of audio mode configuration.
case 0x03: {
// XU316 sends: no data
// MCU reply: 5 bytes audio mode
ret = xu316_pack_frame(cmd, (uint8_t *)&mcu_data.audio_mode, CMD03_MCU_DATA_LEN);
break;
}
Get User Config (0x04)¶
XU316 requests user configuration parameters. MCU replies with 14 bytes, same format as 0x02 command. Sets the currently selected audio mode and recalculates CRC.
case 0x04: {
// XU316 sends: no data
// MCU reply: 14 bytes user config (same format as 0x02)
// Set currently selected audio mode
memcpy((uint8_t *)&mcu_data.audio_mode, audio_modes[g_current_mode], 5);
// Calculate 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;
}
Startup Complete (0x05)¶
XU316 notifies MCU that startup is complete and sends current status data. MCU only needs to reply with acknowledgment (no data).
case 0x05: {
// XU316 sends: 21 bytes status data
// MCU reply: no data
// Save XU316 status to mcu_data structure
memcpy(&mcu_data.startup_status, buffer, 15);
// Reply acknowledgment
ret = xu316_pack_frame(cmd, NULL, CMD05_MCU_DATA_LEN);
break;
}
Status Report (0x20)¶
XU316 proactively reports status changes. MCU saves status data and replies with acknowledgment.
case 0x20: {
// XU316 sends: 20 bytes status data
// Audio mode(5B) + Mute time(2B) + Mic vol(1B) + Left vol(1B) + Right vol(1B) + CRC(4B)
// MCU reply: no data
// Save status data
memcpy(&mcu_data.audio_mode, buffer, 14);
// Reply acknowledgment
ret = xu316_pack_frame(cmd, NULL, CMD20_MCU_DATA_LEN);
break;
}
Audio Format Setting (0x22)¶
XU316 notifies current playback audio format and type. MCU saves this information and replies with acknowledgment.
case 0x22: {
// XU316 sends: 2 bytes (audio format + audio type)
// MCU reply: no data
// Save audio format and type
memcpy(&mcu_data.audio_format, buffer, 2);
// Reply acknowledgment
ret = xu316_pack_frame(cmd, NULL, CMD22_MCU_DATA_LEN);
break;
}
Playback Volume (0x24)¶
XU316 sends current playback volume. MCU saves left/right channel volume values and replies with acknowledgment.
case 0x24: {
// XU316 sends: 2 bytes (left volume + right volume)
// MCU reply: no data
// Save volume values
memcpy(&mcu_data.dac_l_volume, buffer, 2);
// Reply acknowledgment
ret = xu316_pack_frame(cmd, NULL, CMD24_MCU_DATA_LEN);
break;
}
HID Passthrough (0xEE)¶
HID data passthrough or OTA upgrade data. XU316 sends 57 bytes, MCU replies with 57 bytes (echo mode).
case 0xEE: {
// XU316 sends: 57 bytes HID/OTA data
// MCU reply: 57 bytes (echo mode)
// Check version and data length
if (g_rx_data[2] == 0x00 && g_rx_data[4] == CMD_HID_TRANSPARENT_DATA_LEN) {
// Simple echo reply: return received data as-is
ret = xu316_pack_frame(0xEE, buffer, CMD_HID_TRANSPARENT_MCU_DATA_LEN);
}
break;
}
MCU-Initiated Commands¶
The following commands are sent proactively by MCU to XU316.
Unmute Command¶
Command 0x27, data is 2 bytes of 0x00.
Audio Format Delay Command¶
Command 0x28, data is 2 bytes of 0x00.
void send_audio_format_delay_cmd(void) {
uint8_t data[2] = {0x00, 0x00};
xu316_pack_frame(0x28, data, 2);
}
Media Control Command¶
Command 0x21, data is 1 byte media control code.
void send_media_control(media_control_t cmd) {
uint8_t data = (uint8_t)cmd;
xu316_pack_frame(0x21, &data, 1);
}
// Usage examples
send_media_control(MEDIA_KEY_PLAY_PAUSE); // Play/Pause
send_media_control(MEDIA_KEY_VOLUME_UP); // Volume up
send_media_control(MEDIA_KEY_VOLUME_DOWN); // Volume down
Set Audio Mode¶
Command 0x23, data is 5 bytes of audio mode configuration.
void set_audio_mode(uint8_t mode_index) {
// audio_modes is a predefined array of 8 modes
xu316_pack_frame(0x23, (uint8_t *)audio_modes[mode_index], 5);
}
// 8 predefined audio modes
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
};

