Skip to content

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.

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

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
};

Consultation and Feedback

Click to expand consultation and feedback form
×

Notice

Company Name:

Email Address:

Subject:

Message: