Modifying the Software#

Adding a Control Command#

The XVF3800 software allows for easily extensible control. Each time the firmware is built, the command definition YAML files are parsed and the firmware hooks and enums are updated automatically. See Building the Software for how to build the XVF3800 firmware.

The command definition files can be found in sources/app_xvf3800/autogeneration/yaml_files. There is a file for each control servicer within the firmware. The control servicers are:

  • aec_cmds.yaml - This is where high level voice-DSP parameters are accessed as well as AEC information. The voice-DSP is not modifiable other than the published API. It is not expected that this file will need to be modified.

  • application_cmds.yaml - This is where build information is accessed and some test features. The application servicer does not directly connect to any peripherals, however commands requiring internal storage or calling a user API may be added here.

  • audio_cmds.yaml - This is where high-level aspects of the audio framework including SRC, packing, I2S and user-DSP are accessed. If extending the DSP capabilities of the design it is likely that commands may be added here, for example to control user-DSP. See Adding Custom Digital Signal Processing. Note that two tasks are controlled by this servicer (Audio Manager and I2S) with the I2S task being accessed via a shared-memory structure.

  • dfu_cmds.yaml - This is where DFU messages are handled and processed. It is not expected that this file will need to be modified. This servicer is only used in the INT device.

  • hid_task_cmds.yaml - This is where internal messages to send HID IN events are handled. It is not expected that this file will need to be modified. This servicer is only used in the UA device.

  • io_config_cmds.yaml - This is where GPIO parameters are accessed. Commands are already provided for manipulating many aspects of these pins, although any custom requirements involving GPIO access may be added here.

  • io_expander_cmds.yaml - This is where internal messages to control the IO expander GPO’s are handled. It is not expected that this file will need to be modified. This servicer is only used in the builds with the IO expander enabled.

  • pll_cmds.yaml - This is where PLL information is accessed. Generation of the MCLK signal uses the PLL.

  • pp_cmds.yaml - This is where high level voice-DSP parameters are accessed as well as post processing information. The voice-DSP is not modifiable other than the published API. It is not expected that this file will need to be modified.

  • shf_aec_cmds.yaml - This is where low-level voice-DSP parameters are accessed. The voice-DSP is not modifiable other than the published API. This file is auto-generated and should not be modified.

  • shf_pp_cmds.yaml - This is where low-level voice-DSP parameters are accessed. The voice-DSP is not modifiable other than the published API. This file is auto-generated and should not be modified.

  • usb_buffer_cmds.yaml - This is where USB information and parameters are accessed. It only applies to UA configurations.

Adding a new control command#

This process is illustrated by adding a simple read/write parameter via application_cmds.yaml. As an example of how to extend this to controlling IO, see the FAR_END_DSP_ENABLE parameter contained in audio_cmds.yaml.

First, add a command to the YAML file. The valid types that can be used for command parameters are as follows:

TYPE_INT32
TYPE_UINT32
TYPE_INT16
TYPE_UINT16
TYPE_INT8
TYPE_UINT8
TYPE_CHAR
TYPE_FLOAT
TYPE_RADIANS

Any number of these parameters may be defined in a control command, up to the total maximum command size of 64 bytes. Commands attributable to the pp_cmds servicer are exceptions; these are limited to 20 bytes.

The following access permissions may be assigned to parameters:

CMD_READ_ONLY
CMD_WRITE_ONLY
CMD_READ_WRITE

For write commands, a range must be provided for each value. If no value range is specified for such commands, the firmware code will fail to compile. The ranges must be listed in the value_ranges array and must follow one of the two formats:

  1. list of intervals - each interval is listed using the syntax [A .. B]

    • the syntax is the same for both integers and float values

    • multiple intervals can be specified, for example [0 .. 5, 10 .. 15]

    • all the intervals must be closed, meaning that they include all the limit points

    • if only one value is valid, the range can be specified as [E .. E]

  2. any value is valid - this is declared using the word any and the range depends on the maximum and minimum values of the specific type. For example, TYPE_UINT8 can have values from 0 to 255.

An example of a command with two arguments, where the first requires a list of intervals and the second accepts any value, is shown below:

value_ranges:
    - value0: [0 .. 5, 10 .. 15]
    - value1: any

Note

The host control application performs range checking before sending the control command to the device and it returns an error if any argument value is out of range.

An example of adding a command to application_cmds.yaml is shown below. The position in the list at which the command is added is not important so long as it is in the appropriate section:

- cmd: MY_INTERNAL_REGISTER
  number_of_values: 1
  type: CMD_READ_WRITE
  help: A simple example of setting / getting a variable in the firmware
  value_type: TYPE_UINT32

Next, in the appropriate servicer C file, add the handlers for the command. In this case we are adding the following code to sources/modules/fwk_xvf/modules/xvf/src/control_plane/application_servicer.c:

// Global variable to get or set
uint32_t my_var = 0;

In the function control_ret_t application_servicer_read_cmd() add the following case. Note the pre-pending of the resource ID to the command name:

case APPLICATION_SERVICER_RESID_MY_INTERNAL_REGISTER:
    memcpy(payload, &my_var, sizeof(my_var));
    break;

In the function control_ret_t application_servicer_write_cmd() add the following case:

case APPLICATION_SERVICER_RESID_MY_INTERNAL_REGISTER:
    memcpy(&my_var, payload, sizeof(my_var));
    break;

Next, build the firmware and host app; see Building the Software for instructions on this. Test the new command:

(sudo) xvf_host(.exe) MY_INTERNAL_REGISTER
0
(sudo) xvf_host(.exe) MY_INTERNAL_REGISTER 1066
(sudo) xvf_host(.exe) MY_INTERNAL_REGISTER
1066

Adding Custom Digital Signal Processing#

The XVF3800 supports the addition of user DSP at two parts of the signal path. These points are:

  • Far-end DSP between the far-end (reference) input and the start of the far-field voice pipeline. This allows the far-end signal to be processed to allow for speaker/amplifier imperfections and a copy of the pre-processed far-end be sent to the voice pipeline (and optionally over I2S to the DAC) to ensure optimum AEC performance.

  • Voice post-processing. While the voice processing offers a wide range of typically needed functions such as AGC, high-pass filtering and automatic beam selection, some users may wish to augment these functions.

Fig. 72 shows the audio paths for the far-end DSP as well as where up/down-sampling may occur. Voice post-processing occurs immediately after the voice pipeline and before the processed microphone signals are sent to the host.

../../../../_images/far-end-audio-paths.png

Fig. 72 Audio paths for far-end DSP and where up/down-sampling may occur in the XVF3800.#

Both DSP hooks provide a sample-based processing API. This means a single sample is processed at each time. The reason for this is to reduce latency (block based algorithms introduce a minimum latency of the block size) and to simplify the integration into the main firmware framework. Various DSP functions are available in lib_xcore_math including FIR filters and Biquad IIR filters. An example of the latter is given further on.

Note

Integration of user DSP consumes processing cycles from the processor. These are limited according to the build and host sample rates used. Please see Meeting timing for details.

The API for the user-DSP functions is transcluded below from sources/modules/fwk_xvf/modules/xvf/src/user_interfaces/user_dsp.h:

// Copyright 2022-2023 XMOS LIMITED.
// This Software is subject to the terms of the XCORE VocalFusion Licence.

#ifndef __USER_DSP_H_
#define __USER_DSP_H_

#include "aec_cmds.h"
#include "shf_wrapper.h"
#include <stdint.h>

/// There is a timing limit on the time spent in these functions. Please use the 
/// minimum idle time control commands in conjunction with the TEST_CORE_BURN command to
/// characterise the amount of cycles available.
/// The far_end_dsp function is called from I2S and so check min_idle time for that task
/// The far_end_dsp function is called from Audio so check min_idle time for that task


/// @brief callback to pre-process one sample of the far end before outputting to DAC/SHF DSP input
/// Note that this callback runs at the I2S sample rate.
/// @param far_end_sample      input and output (sample is processed in place)
/// @param far_end_dsp_enable  Set to 1 to enable, 0 to disable. This is handled by the user. 
void far_end_dsp(int32_t far_end_samples[BECLEAR_NUMBER_OF_FAR], bool far_end_dsp_enable);

/// Struct passed to post_shf_dsp which contains all the information that is available 
/// for additional post-processing.
typedef struct {
    /// Pointer to array containing the BECLEAR_NUMBER_OF_OUTPUTS processed microphone channels.
    int32_t* post_shf_processed_mic_samples;

    /// BECLEAR_NUMBER_OF_MICS channels containing the microphones after AEC before post-processing.
    int32_t* aec_residuals;

    /// Pointer to array of BECLEAR_NUMBER_OF_OUTPUTS azimuths, each element is the azimuth for the
    /// post_shf_processed_mic_samples of the same index. It can be NULL if azimuths have not been 
    /// calculated.
    float* azimuths;

    /// The spenergy (speech energy) for each beam in post_shf_processed_mic_samples. If the spenergy 
    /// is non-zero then it contains energy that is likely speech. The value will be higher for louder
    /// or closer voices, noise and distortion will cause the speech energy to decrease. This points
    /// to an array of size BECLEAR_NUMBER_OF_OUTPUTS where each value corresponds to the beam of the
    /// same index. It can be NULL if spenergy has not been calculated.
    float* spenergy;

    /// Output of xmos algorithm to determine the direction of voice, NAN if no voice detected
    float direction_of_voice;
} user_dsp_post_shf_input_t;

/// @brief callback to post-process one sample of audio after the SHF voice DSP stage
/// Note that this callback runs at the SHF sample rate.
/// @param out Array to fill with the output of this function.
/// @param input See user_dsp_post_shf_input_t comments for details.
void post_shf_dsp(int32_t out[BECLEAR_NUMBER_OF_OUTPUTS],
                  user_dsp_post_shf_input_t* input);

#define USER_DSP_NUM_OUTPUT_CHANNELS 2

/// @brief called immediately after post_shf_dsp and will be used to determine the
/// channels that MUX_USER_CHOSEN_CHANNELS will consist of. It also sets the azimuth
/// of each chosen channel so that it can be requested via control command.
///
/// @param[in,out] out_idx 2 chosen channels from `out` which were written by
///    the function `post_shf_dsp`. Before being passed to beam_selection, 
///    out_idx will be populated by the suggested indices.
/// @param[out] out_azimuths azimuths of the two channels that have been
///    selected. Most likely a copy of the correct input from above.
void beam_selection(uint8_t out_idx[USER_DSP_NUM_OUTPUT_CHANNELS],
                    float out_azimuths[USER_DSP_NUM_OUTPUT_CHANNELS]);

#endif

Meeting Timing#

When adding DSP, it is important to check timing. It is not sufficient just to check audio is playing cleanly because the available cycles within the XVF3800 varies significantly depending on operation and will increase under certain conditions. A comprehensive method for checking worst case timing is included and can be found in the Testing the Software section of the Programming Guide.

Adding control to user DSP#

In some cases it may be desirable to add a custom control command to allow the host application to enable/disable or adjust the user DSP. The steps in Adding a Control Command show how to add controls to the firmware and the below examples include adding a control for each case.

The far-end reference DSP already has a built-in control which is AUDIO_MGR_FAR_END_DSP_ENABLE. It is possible to add further controls if needed.

Far-end Reference#

The far-end DSP offers the opportunity to add processing between the host audio signal and the DAC output. This is particularly important if adding non-linear processing (eg. bass-enhancement, dynamic-range compression) which will cause a degradation in AEC performance if the far-end reference to the voice pipeline differs from what is being played through the speaker.

The rate of the DSP is the host interface rate. For example, if I2S runs at 48 kHz, then the far-end DSP also runs at 48 kHz. The samples will not be sent to the voice pipeline until after the far-end DSP has occurred and will be down-sampled if required.

Because the far-end DSP block will add delay to the voice pipeline reference signal, it is essential that any delay added is not so large that the direct path (loudspeaker to microphone) of the far-end signal arriving at the microphones gets to the voice pipeline before the far-end processed signal does. This non-causal relationship will cause a rapid degradation in AEC performance. For more information on this, including how to measure this effect, see the Tuning the Application section of the User Guide.

Depending on the external audio path and the type of processing applied (linear vs non-linear) it may be necessary to add an additional audio line. For example, an I2S connected system may need one audio line for the reference input, one audio line for the processed microphone output and an additional line for the processed far-end output to the DAC. Where the far-end audio source is USB, this is not necessary since the far-end processed output will always be sent to the DAC pin as well as the voice pipeline input.

The XVF3800 allows provision of a third I2S line for far-end processed output using the following steps:

  • Increase appconfNUM_I2S_PINS_OUT in app_conf.h from 1 to 2. This will enable PORT_I2S_DATA2 as an I2S output pin. The default is to set the second pin to output the post-processed far-end input with sample rate conversion disabled.

  • Set USE_FAR_END_DSP in far_end_dsp.c to 1. Far-end DSP is kept out of the standard build to avoid using extra memory and processing cycles, and to avoid modifying the far-end when not needed.

  • Make sure the DAC input is connected to the far-end DSP processed signal.

If using USB, it is possible that the hardware will not easily support routing the second I2S line to the DAC. Therefore, in the USB build, the pin formerly used as the I2S input to the device may be repurposed as an I2S output. To take advantage of this, keep appconfNUM_I2S_PINS_OUT set to 1 and instead use the audio MUX command to route the post-processed far-end signal to the output. The following commands ensure up-sampling is disabled and route far-end DSP signal to the output:

(sudo) xvf_host(.exe) AUDIO_MGR_OP_UPSAMPLE 0 0
(sudo) xvf_host(.exe) AUDIO_MGR_OP_ALL 10 0 10 2 10 4 10 1 10 3 10 5

A control command has already been provided which passes a boolean to the far_end_dsp() function, which allows the user to enable and disable the far-end DSP and verify its functionality. This may be controlled using:

(sudo) xvf_host(.exe) AUDIO_MGR_FAR_END_DSP_ENABLE 1
(sudo) xvf_host(.exe) AUDIO_MGR_FAR_END_DSP_ENABLE 0

For example purposes, a three stage biquad filter has been implemented which boosts bass and treble by 6 dB and cuts mid-range by 6 dB. This will produce a noticeable effect suitable for demonstration purposes and to verify that the DSP is active. This example has coefficients that have generated assuming a 48 kHz sample rate. They will not work properly at 16 kHz and will need to be re-calculated.

The code is shown below, transcluded from sources/app_xvf3800/src/user_dsp/far_end_dsp.c:

// Copyright 2022-2023 XMOS LIMITED.
// This Software is subject to the terms of the XCORE VocalFusion Licence.

#include "user_dsp.h"

#ifndef USE_FAR_END_DSP
    #define USE_FAR_END_DSP 0
#endif

#if USE_FAR_END_DSP
#include "xmath/xmath.h"

// Simple example DSP that processes far end using an EQ. Note the coeffs are correct for 48kHz only
// Each filter_biquad_s32_t can store (up to) 8 biquad filter sections
#define SECTION_COUNT   3

filter_biquad_s32_t filter[BECLEAR_NUMBER_OF_FAR] = {{
    // Number of biquad sections in this filter block
    .biquad_count = SECTION_COUNT,

    // Filter state, initialized to 0
    .state = {{0}},

    // Filter coefficients
    // Section 0: Frequency = 100 Hz, Q Factor = +1.2,  Gain = +6.0dB
    // Section 1: Frequency = 1000 Hz, Q Factor = +0.2, Gain = -6.0dB
    // Section 2: Frequency = 8000 Hz, Q Factor = +1.3, Gain = +6.0dB
    .coef = {
        { Q30(+1.00286226693868),  Q30(+0.86561763867029),  Q30(+1.58124059575259)},
        { Q30(-1.98608850422494),  Q30(-1.44869047773446),  Q30(-1.32398889950623)},
        { Q30(+0.98346660965693),  Q30(+0.59557353208939),  Q30(+0.56062804987035)},
        { Q30(+1.98614845462853),  Q30(+1.44869047773446),  Q30(+0.45958190453639)},
        { Q30(-0.98626892619203),  Q30(-0.46119117075968),  Q30(-0.27746165065311)}
        }
}};


void far_end_dsp(int32_t far_end_samples[BECLEAR_NUMBER_OF_FAR], bool far_end_dsp_enable)
{
    // See note in user_dsp.h and user documentation about timing constraints
    if(far_end_dsp_enable)
    {
        for(int channel = 0; channel < BECLEAR_NUMBER_OF_FAR; channel++)
        {
            far_end_samples[channel] >>= 1; // Simple 6db pre-attenuate to account for gain in filter
            far_end_samples[channel] = filter_biquad_s32(&filter[channel], far_end_samples[channel]);
        }
    } else {
        for(int channel = 0; channel < BECLEAR_NUMBER_OF_FAR; channel++)
        {
            far_end_samples[channel] >>= 1; // Simple 6db attenuate to account for filter gains to give similar apparent volume
        }
    }
}

#else

void far_end_dsp(int32_t far_end_samples[BECLEAR_NUMBER_OF_FAR], bool far_end_dsp_enable)
{
    // Do nothing - samples unmodified
}

#endif

Testing the effect on available cycles, we can see a modest drop from the three stage biquad of just 85 x 10 ns = 0.85 us; this is due in part to the use of the Vector Processing Unit (VPU), which is highly efficient for signal processing purposes. Note that the idle time is not calculated until I2S runs:

(sudo) xvf_host(.exe) TEST_CORE_BURN 1
(sudo) xvf_host(.exe) I2S_MIN_IDLE_TIME
2083
aplay <short wav>
aplay <short wav>
aplay <short wav>
(sudo) xvf_host(.exe) I2S_MIN_IDLE_TIME
1245
(sudo) xvf_host(.exe) AUDIO_MGR_FAR_END_DSP_ENABLE 1
aplay <short wav>
aplay <short wav>
aplay <short wav>
(sudo) xvf_host(.exe) I2S_MIN_IDLE_TIME
1160

Voice post-processing#

As the name suggests, this DSP hook allows the user to add any required DSP after the microphone signals have passed through the voice pipeline. The rate of the processing is always the rate of the voice pipeline (nominally 16 kHz). A number of audio signals are available including multiple output beams and AEC residuals which are the echo-cancelled only signals for each of the four microphones.

To allow more informed selection of the output beam, the Direction Of Arrival (DOA) azimuths and the speech energy are also provided to allow custom logic to choose the desired signal.

Adding processing to this part of the chain will not affect the performance of the core voice pipeline; however, it will add to the total delay through the device from microphones to output interface.

Follow the same steps as per the Far-end Reference except when checking timing, please use AUDIO_MGR_MIN_IDLE_TIME instead of I2S_MIN_IDLE_TIME since the processing cycles are consumed from a different task. The Testing the Software section also provides detailed information on how to check timing.

Spatial output example#

An example is provided in post_shf_dsp.c which uses the DOA information to pan the auto select beam onto the left and right output channel. This allows for a stereo output from the device giving audible indication of the location of the speaker. This feature can be enabled using a macro named appconfSPATIAL and is enabled in build configs with the -spatial suffix. These configs also configure the output mux to play the left and right outputs back to the host correctly.

To get the best output from this example the macro LEFT_ANGLE_RADIANS may need to be updated so that the correct output is heard. For linear microphone arrays this will either be 0 or M_PI depending on the microphone geometry. After changing the software, it will need to be recompiled and flashed to the device.

Modifying Existing Functionality#

The XVF3800 provides the ability to customise the initialisation code for any connected hardware at firmware boot time. During run-time, the GPIO pins may be modified via control commands from the host control application. Initialisation typically involves setting GPO pins to control board level features such as LEDs and configuring I2C connected devices such as an IO expander, a DAC, or a digital amplifier. The code for user hardware initialisation can be found in sources/app_xvf3800/src/user_config. The main file to be modified is user_config.c which is shown below.

// Copyright 2022-2023 XMOS LIMITED.
// This Software is subject to the terms of the XCORE VocalFusion Licence.

#include "FreeRTOS.h"
#include "app_conf.h"
#include "user_config.h"
#include "io_config_servicer.h"
#include "dac3101.h"

// This file contains the user hardware configuration code. It uses the implementations in dac3101.h (EVK3800)
// and the lower level hardware implementations in dac_port.c

int user_init_hardware(device_control_t *device_control_gpio_ctx)
{
    int errors_encountered = 0;

    rtos_printf("user_init_hardware\n");

#if !defined(MIC_ARRAY_TYPE)
#error
#endif
#if MIC_ARRAY_TYPE == BECLEAR_LINEAR_ARRAY
    write_gpo_pin(device_control_gpio_ctx, GPO_SQ_nLIN_PIN, 0);
#elif MIC_ARRAY_TYPE == BECLEAR_CIRCULAR_ARRAY
    write_gpo_pin(device_control_gpio_ctx, GPO_SQ_nLIN_PIN, 1);
#else
    #error MIC_ARRAY_TYPE invalid
#endif

    // De-assert HOST INTERRUPT line
    write_gpo_pin(device_control_gpio_ctx, GPO_INT_N_PIN, 1); // No interrupt to host asserted when high

#if (appconfUSER_CONFIG_ENABLED == 1)
    // Reset the DAC
    dac3101_codec_reset(device_control_gpio_ctx);

    errors_encountered |= dac3101_init(appconfLRCLK_NOMINAL_HZ);
#endif

    // Test that we can turn on a LED by sending a command from this task to the GPO task
    // Note even though LEDs are active low, we have setup the LED pins in gpo_servicer to drive negaive logic
    for(int i = 0; i < 5; i++){
        write_gpo_pin(device_control_gpio_ctx, GPO_LED_GREEN_PIN, 1); // Turn the green LED on
        vTaskDelay(pdMS_TO_TICKS(100));
        write_gpo_pin(device_control_gpio_ctx, GPO_LED_GREEN_PIN, 0); // Turn the green LED off
        vTaskDelay(pdMS_TO_TICKS(100));
    }

    return errors_encountered;
}

This file is currently configured for the XK-VOICE-SQ66 development kit and its associated hardware set.

In this file we can see the following actions taken:

  • Setting of GPO output line on the XK-VOICE-SQ66 development kit to control the microphone array topology.

  • Setting of GPO output line to de-assert the host interrupt line.

  • Resetting of the DAC. See the next section Digital to Analogue Converter Configuration for details.

  • Configuring the DAC. See the next section Digital to Analogue Converter Configuration for details.

  • Flashing the green LED five times to show that booting of the XVF3800 is occurring.

Note

The control plane part of the XVF3800 firmware, responsible for servicing control commands from the host, will not start until the call to user_init_hardware() is complete. Any control commands issued by the host before initialisation is complete will not be serviced.

Digital to Analogue Converter Configuration#

Each DAC or digital amplifier selected will normally have its own set of registers that need to be configured. There are two parts to the DAC configuration code. The first is the abstraction layer responsible for providing the I2C, GPO, and wait functions. These may need to be modified if the GPO responsible for resetting the DAC or the I2C register access methods needs to be altered. This file can be seen below, transcluded from sources/app_xvf3800src/user_config/dac_port.c:

// Copyright 2022-2023 XMOS LIMITED.
// This Software is subject to the terms of the XCORE VocalFusion Licence.

// This file contains the implementations of the DAC configuration steps such as which registers to write with which values
// Note there are currently two implementations because in I2C slave we init the DAC pre-RTOS

/* FreeRTOS headers */
#include "FreeRTOS.h"

/* App headers */
#include "xcore/port.h"
#include "rtos_i2c_master.h" // Includes "i2c.h" too
#include "platform/driver_instances.h"
#include "io_config_servicer.h"
#include "user_config.h"
#include "dac3101.h"


void dac3101_wait(uint32_t wait_ms)
{
    vTaskDelay(pdMS_TO_TICKS(wait_ms));
}

#if (appconfUSER_CONFIG_ENABLED == 1) // Some builds use a specific control transport but DO NOT require setting up of DAC HW

int dac3101_reg_write(uint8_t reg, uint8_t val)
{
    rtos_i2c_master_t * i2c_master_ctx = get_i2c_master_ctx();
    i2c_regop_res_t ret = rtos_i2c_master_reg_write(i2c_master_ctx, DAC3101_I2C_DEVICE_ADDR, reg, val);

    if (ret == I2C_REGOP_SUCCESS) {
        return 0;
    } else {
        return -1;
    }
}


void dac3101_codec_reset(void * args)
{
    device_control_t *device_control_gpio_ctx = args;

    write_gpo_pin(device_control_gpio_ctx, GPO_DAC_RST_N_PIN, 0);
    dac3101_wait(1);  /* From DS - The hardware reset pin (RESET) must be pulled low for at least 10ns */
    write_gpo_pin(device_control_gpio_ctx, GPO_DAC_RST_N_PIN, 1);
    dac3101_wait(1); /* From DS - This initialization takes place within 1 ms after pulling the RESET signal high */
}
#endif

The second file, which specifies the sequence of GPO accesses and I2C register writes specific to the chosen DAC, can be found in sources/modules/fwk_xvf/modules/bsp/dac. There are two files which may need to be modified: dac3101.h, which contains the defines and function prototypes, and dac3101.c, which contains the dac3101_init() function that is called from user_config.c and performs the sequence of operations required to configure the DAC. These sources are not printed here for documentation brevity.

Note

Error detection is included and errors (non-zero return) will be reported back to the application if encountered.

General Purpose Input and Output Operation#

Several GPIO ports are provided by the XVF3800 to allow input and output capability. These may be accessed from the firmware at startup via user_config.c or by the host application using GPO and GPI commands. The XVF3800 ports contain a single direction register and therefore groups of pins on a single port are all either input or output. The firmware provides functionality to address individual pins within a port.

GPI ports provide the capability to read the current state of pins, invert their logic, and capture an edge (event). GPO ports provide the ability to output a logic level, autonomously flash a 32b serial pattern, or provide a PWM signal suitable for dimming LEDs.

The initial configuration of the roles of each GPO pin can be found in the function init_gpo() in sources/modules/fwk_xvf/modules/xvf/src/control_plane/gpo_servicer.c. This contains the GPO setup for the XVF3800 demonstration board including initial level and drive invert. Drive invert can be useful for negative logic hardware such as LEDs connected between the 3v3 rail and the GPO pin.

Note

Because GPO pins support PWM, setting the duty to 100% or 0% is the same as setting a 1 or 0. The write_gpo_pin() function hides this functionality by providing a simple logic write; however, the initialisation section in gpo_servicer.c initialises a PWM value of 0 or 100. Additionally, the flash mask is set to 0xffffffff so that there is no flash sequence enabled.

Individual bit defines for the individual pins in the default firmware can be found in sources/app_xvf3800/src/app_conf.h.

Once the pin roles and initial values have been configured, they may be accessed using a simple API providing logic level access. An example is shown in the code listing in Modifying Existing Functionality which asserts GPO pins during the DAC setup.

USB configuration#

In the XVF3800-UA device, several settings related to the USB interfaces can be configured. The USB audio sample rate can be configured using the appropriate build configuration as described in the Building the Application section of the User Guide. The remaining USB settings can be updated using the usb_param_values.yaml in sources/app_xvf3800/autogeneration/yaml_files/settings_and_defaults/. This file contains additional configurable parameters used in the USB descriptors, such as vendor ID and product ID, and the default data bit depths of the input and output audio. The full list of parameters and their default values are below:

VENDOR_ID: 0x20B1
PRODUCT_ID: 0x4F00
MANUFACTURER_STR: "XMOS"
PRODUCT_STR: "XVF3800 Voice Processor"
SERIAL_NUMBER_STR: "000000"
CONTROL_INTERFACE_STR: "XMOS Control"
HID_INTERFACE_STR: "XMOS HID"
DFU_FACTORY_INTERFACE_STR: "XMOS DFU Factory"
DFU_UPGRADE_INTERFACE_STR: "XMOS DFU Upgrade"
DEFAULT_BIT_DEPTH_IN: "16"
DEFAULT_BIT_DEPTH_OUT: "16"

Modifying the HID to GPIO mapping#

The init_hid_button_config and init_hid_led_config functions in sources/modules/fwk_xvf/modules/xvf/src/usb/control_plane/hid_init.c can be modified to change the mapping between the HID buttons/LEDs and the GPIO buttons/LEDs. For more details about the HID design, refer to HID Interface design

Changing button mapping#

By default, the code in init_hid_button_config maps the HID Mute button to the button on the XK-VOICE-SQ66 development kit.

    hid_button_config_t config = {.report_id = REPORT_ID_MISC_BUTTONS, .offset = BUTTON_MUTE_OFFSET, .size = 1, .gpi_source = GPI_SOURCE_EVK, .gpi_pin_index = EVK_BUTTON_INDEX, .button_type = BUTTON_TYPE_OSC, .button_press_precondition=HOOKSWITCH_BUTTON};
    hid_button_config[MUTE_BUTTON] = config;

This mapping can be changed subject to some contraints:

  • the Dialpad buttons are not supported.

  • the Teams button is not supported.

Any of the other remaining buttons, HookSwitch, Flash, Redial, Volume Increment and Volume Decrement, can be mapped to the EVK button. To do that, change the gpi_source and gpi_pin_index to BUTTON_GPI_UNMAPPED for the Mute button.

{
   hid_button_config_t config = {.report_id = REPORT_ID_MISC_BUTTONS, .offset = BUTTON_MUTE_OFFSET, .size = 1, .gpi_source = BUTTON_GPI_UNMAPPED, .gpi_pin_index = BUTTON_GPI_UNMAPPED, .button_type = BUTTON_TYPE_OSC, .button_press_precondition=HOOKSWITCH_BUTTON};
   hid_button_config[MUTE_BUTTON] = config;
}

Then, for the HID button that needs to be mapped to the EVK button, change the gpi_source to GPI_SOURCE_EVK and change the gpi_pin_index to EVK_BUTTON_INDEX. Below is an example of mapping the Volume Increment button to the EVK button.

{
   hid_button_config_t config = {.report_id = REPORT_ID_VOLUME_BUTTONS, .offset = BUTTON_VOL_UP_OFFSET, .size = 1, .gpi_source = GPI_SOURCE_EVK, .gpi_pin_index = EVK_BUTTON_INDEX, .button_type = BUTTON_TYPE_RTC, .button_press_precondition = NO_BUTTON_PRESS_PRECONDITION};
   hid_button_config[VOLUME_UP_BUTTON] = config;
}

Note

This code change needs to be made for the code that is not in the #if (IO_EXPANDER_ENABLED) define block.

Changing LED mapping#

By default, the code in init_hid_led_config maps the HID Off-Hook LED to the Green LED on the XK-VOICE-SQ66 development kit and the HID Mute LED to the Red LED on the XK-VOICE-SQ66 development kit.

    hid_led_config_t config = {.report_id = REPORT_ID_MISC_BUTTONS, .offset = LED_OFFHOOK_OFFSET, .gpo_source = GPO_SOURCE_EVK, .gpo_pin_index = EVK_LED_GREEN, .notify_hid_task = true, .trigger_hid_input_index = HOOKSWITCH_BUTTON, .led_mode=LED_MODE_STEADY};
    hid_led_config[OFFHOOK_LED] = config;
    hid_led_config_t config = {.report_id = REPORT_ID_MISC_BUTTONS, .offset = LED_MUTE_OFFSET, .gpo_source = GPO_SOURCE_EVK, .gpo_pin_index = EVK_LED_RED, .notify_hid_task = false, .trigger_hid_input_index = NO_HID_IN_TRIGGER, .led_mode=LED_MODE_STEADY};
    hid_led_config[MUTE_LED] = config;

This can be changed and any of the remaining HID LEDs, the Ring and Hold LED, can be mapped to the XK-VOICE-SQ66 development kit LEDs. To make the change, first make sure that the existing mapping is removed. For example, when mapping the Green LED to something else, make sure that the existing Off-Hook to Green LED mapping is removed. This is done by changing the gpo_source and gpo_pin_index to LED_GPO_UNMAPPED for the OffHook LED.

{
   hid_led_config_t config = {.report_id = REPORT_ID_MISC_BUTTONS, .offset = LED_OFFHOOK_OFFSET, .gpo_source = LED_GPO_UNMAPPED, .gpo_pin_index = LED_GPO_UNMAPPED, .notify_hid_task = true, .trigger_hid_input_index = HOOKSWITCH_BUTTON, .led_mode=LED_MODE_STEADY};
   hid_led_config[OFFHOOK_LED] = config;
}

Then, for the HID LED that needs to be mapped to the Green XK-VOICE-SQ66 development kit LED, set the .gpo_source as GPO_SOURCE_EVK and gpo_pin_index as EVK_LED_GREEN. For example, if mapping the Ring LED to the Green LED, change

{
   hid_led_config_t config = {.report_id = REPORT_ID_MISC_BUTTONS, .offset = LED_RING_OFFSET, .gpo_source = GPO_SOURCE_EVK, .gpo_pin_index = EVK_LED_GREEN, .notify_hid_task = true, .trigger_hid_input_index = NO_HID_IN_TRIGGER, .led_mode=LED_MODE_FAST_FLASH};
   hid_led_config[RING_LED] = config;
}

Note

This code change needs to be made for the code that is not in the #if (IO_EXPANDER_ENABLED) define block.

Modifying the HID to GPIO mapping for the IO expander build#

Similar to Modifying the HID to GPIO mapping the GPIO to HID events mapping can be changed by modifying the code in the init_hid_button_config and init_hid_led_config functions in sources/modules/fwk_xvf/modules/xvf/src/usb/control_plane/hid_init.c. For the IO expander build (application_xvf3800_ua-io48-lin-io-exp), the code within #if (IO_EXPANDER_ENABLED) needs to be modified.

The GPI button is defined by the gpi_source and gpi_pin_index fields in the hid_button_config_t structure. To change the GPI button mapped to a given HID button, change the gpi_source and gpi_pin_index fields in the initialisation code for that button in the init_hid_button_config function. The available GPI sources and pin indexes for every source are defined in sources/modules/fwk_xvf/modules/xvf/src/usb/control_plane/usb_hid.h

/// @brief Buttons sources. The EVK and the IO expander board
typedef enum
{
    GPI_SOURCE_EVK = 0,
    GPI_SOURCE_IO_EXP
}all_gpi_sources_t;

/// @brief Button indexes for the buttons on the EVK.
typedef enum
{
    EVK_BUTTON_INDEX = 0,
    TOTAL_EVK_BUTTONS
}all_evk_buttons_t;

/// @brief Button indexes for the buttons on the IO expander
typedef enum
{
    IO_EXP_MUTE_BUTTON_INDEX = 0,
    IO_EXP_VOL_UP_BUTTON_INDEX,
    IO_EXP_VOL_DN_BUTTON_INDEX,
    IO_EXP_ACTION_BUTTON_INDEX,
    TOTAL_IO_EXP_BUTTONS
}all_io_exp_buttons_t;

The GPO LED is defined by the gpo_source and gpo_pin_index fields in the hid_led_config_t structure. To change the GPO LED mapped to a given HID LED, change the gpo_source and gpo_pin_index fields in the initialisation code for that LED in init_hid_led_config function. The available GPO sources and pin indexes for every source are defined in sources/modules/fwk_xvf/modules/xvf/src/usb/control_plane/usb_hid.h

/// @brief LED sources. The EVK and the IO expander board
typedef enum
{
    GPO_SOURCE_EVK = 0,
    GPO_SOURCE_IO_EXP
}all_gpo_sources_t;

/// @brief LEDs on the EVK board
typedef enum
{
    EVK_LED_GREEN,
    EVK_LED_RED,
    TOTAL_EVK_LEDS
}all_evk_leds_t;

/// @brief LEDs on the IO Expander board
typedef enum
{
    IO_EXP_PCAL6416A_LED, // Red LED on the IO expander
    IO_EXP_IS31FL3193_RGB_LED,  // IS31FL3193 RGB LED on the IO expander
    TOTAL_IO_EXP_LEDS
}all_io_exp_leds_t;

Adding a different I2C Expander#

The HID + I2C expander design described in System Design mentions the io_expander_task that is responsible for responding to buttons and driving LEDs on the I2C expander. The existing io_expander_task is written for supporting the PCAL6416A I2C expander and requires modifications when supporting a different I2C expander.

In addition, the definitions for the buttons and LEDs supported on the I2C expander that get exposed to the HID tasks will need to be modified. Section Defining available GPIO on the IO expander describes this, followed by Modifying the IO expander task which describes the changes required to the io_expander_task for supporting a different I2C exapnder.

Defining available GPIO on the IO expander#

The (gpi_source, gpi_pin_index) fields in the hid_button_config_t structure and the (gpo_source, gpo_pin_index) fields in the hid_led_config_t structure define the GPIO buttons/LEDs that are mapped to HID buttons/LEDs. The available GPIO sources and the buttons/LEDs supported per source are defined as enums in sources/modules/fwk_xvf/modules/xvf/src/usb/control_plane/usb_hid.h.

/// @brief Buttons sources. The EVK and the IO expander board
typedef enum
{
    GPI_SOURCE_EVK = 0,
    GPI_SOURCE_IO_EXP
}all_gpi_sources_t;

/// @brief Button indexes for the buttons on the EVK.
typedef enum
{
    EVK_BUTTON_INDEX = 0,
    TOTAL_EVK_BUTTONS
}all_evk_buttons_t;

/// @brief Button indexes for the buttons on the IO expander
typedef enum
{
    IO_EXP_MUTE_BUTTON_INDEX = 0,
    IO_EXP_VOL_UP_BUTTON_INDEX,
    IO_EXP_VOL_DN_BUTTON_INDEX,
    IO_EXP_ACTION_BUTTON_INDEX,
    TOTAL_IO_EXP_BUTTONS
}all_io_exp_buttons_t;

/// @brief LED sources. The EVK and the IO expander board
typedef enum
{
    GPO_SOURCE_EVK = 0,
    GPO_SOURCE_IO_EXP
}all_gpo_sources_t;

/// @brief LEDs on the EVK board
typedef enum
{
    EVK_LED_GREEN,
    EVK_LED_RED,
    TOTAL_EVK_LEDS
}all_evk_leds_t;

/// @brief LEDs on the IO Expander board
typedef enum
{
    IO_EXP_PCAL6416A_LED, // Red LED on the IO expander
    IO_EXP_IS31FL3193_RGB_LED,  // IS31FL3193 RGB LED on the IO expander
    TOTAL_IO_EXP_LEDS
}all_io_exp_leds_t;

These enums are used in the init_hid_button_config and init_hid_led_config functions when initialising the hid_button_config_t and hid_led_config_t structures for all HID buttons and LEDs.

When replacing the existing I2C expander with a different one, change the all_io_exp_buttons_t and all_io_exp_leds_t enums to define the available GPIO buttons and LEDs, and change the init_hid_button_config and init_hid_led_config functions to map the HID buttons/LEDs to these new ones.

Modifying the IO expander task#

This section describes the modifications required in the io_expander_task to support a different I2C expander. It walks through the io_expander_task code describing the purpose of each bit and the changes required in it when supporting a different I2C expander.

  1. Initialise the IO expander registers. At the start of the io_expander_task there is code for initialising the I2C expander registers.

res = rtos_i2c_master_reg_write(i2c_master_ctx, addr_ioexp, 0x2, 0x00); // drive mute led and dac_rst low
res |= rtos_i2c_master_reg_write(i2c_master_ctx, addr_ioexp, 0x6, ~(int8_t)0b10010000); // all input except mic_off and dac_rst
res |= rtos_i2c_master_reg_write(i2c_master_ctx, addr_ioexp, 0x7, ~0x00); // all input
res |= rtos_i2c_master_reg_write(i2c_master_ctx, addr_ioexp, 0x44, 0x0f); // Latching on bits 0..3
res |= rtos_i2c_master_reg_write(i2c_master_ctx, addr_ioexp, 0x45, 0x00); // No latching

When using a different I2C expander the register initialisation code will need to change accordingly.

  1. Call the init_io_exp_gpo function to initialise the GPO LED states and initialise the io_exp_gpo_config structure. The io_exp_gpo_config structure is of type io_exp_gpo_config_t and is defined to contain the information for mapping from the LED index exposed to the HID task (all_io_exp_leds_t) to the actual LED on the I2C expander. Since the two LEDs added via the I2C expander in the current design are completely different and reside on different I2C addresses, the io_exp_gpo_config_t doesn’t contain any information other than the location of the LED. Instead, the code in io_exp_drive_leds which is the function responsible to drive the LEDs does so by executing different pieces of code selected based on the LED port.

if(led_port == IO_EXP_GPO_PORT_PCAL6416A)   // Red LED on the PCAL6416A
{
    // Program the LED on PCAL6416A
}
else if(led_port == IO_EXP_GPO_PORT_IS31FL3193) // RGB LED on the IS31FL3193
{
    // Program the LED on the IS31FL3193
}

The GPO LED states initialisation and the structure and initialisation of the io_exp_gpo_config_t structure will change when using a different I2C expander.

  1. Initialise the io_exp_gpi_info structure. This structure is of type io_exp_gpi_info_t which contains the information required to map from the I2C expander buttons exposed to the HID tasks (all_io_exp_buttons_t) to the actual GPI buttons on the IO expander. It currently contains the GPI pin index in the IO expander Input Port register 00h for the all_io_exp_buttons_t buttons and is initialised as follows:

io_exp_gpi_info_t io_exp_gpi_info[TOTAL_IO_EXP_BUTTONS] = {
    {.pin =  IO_EXP_MUTE_BUTTON_PIN, ._previous_event_time = 0},    // IO_EXP_MUTE_BUTTON_INDEX
    {.pin =  IO_EXP_VOL_UP_BUTTON_PIN, ._previous_event_time = 0},  // IO_EXP_VOL_UP_BUTTON_INDEX
    {.pin =  IO_EXP_VOL_DN_BUTTON_PIN, ._previous_event_time = 0},  // IO_EXP_VOL_DN_BUTTON_INDEX
    {.pin =  IO_EXP_ACTION_BUTTON_PIN, ._previous_event_time = 0},  // IO_EXP_ACTION_BUTTON_INDEX
};

The io_exp_gpi_info_t structure and its initialisation will change when using a different I2C expander.

  1. Create the io_expander_gpo_servicer task:

// Create task for receiving internal GPO commands from tud_hid_set_report_cb()
xTaskCreate((TaskFunction_t) io_expander_gpo_servicer,
                    "io_expander_gpo_task",
                    portTASK_STACK_DEPTH(io_expander_gpo_servicer),
                    &io_exp_gpo_state,
                    appconfTEST_TASK_PRIORITY,
                    NULL);

The io_expander_gpo_servicer responds to the LED control commands from the HID task. The io_exp_gpo_state is shared between the io_expander_gpo_servicer and the io_expander_task. io_expander_gpo_servicer receives the IO_EXPANDER_SERVICER_RESID_INTERNAL_GPO_LED_STATE control command from tud_hid_set_report_cb -> handle_hid_output_report_bit_change -> send_led_command path. The IO_EXPANDER_SERVICER_RESID_INTERNAL_GPO_LED_STATE contains the LED index (all_io_exp_leds_t) and LED state (e_led_mode_t) that a given LED needs to be set to. The io_expander_gpo_servicer updates the gpo_state->led_state[led_index].led_mode and gpo_state->led_state[led_index].counter. Since the io_expander_gpo_servicer doesn’t actually program the LEDs on the I2C expander, it shouldn’t need to change when modifying the code for a different I2C expander.

  1. In a timer driven loop, for every button, read its state and if changed from the previous read, send a HID_TASK_RESID_INTERNAL_BUTTON_PRESS command to the hid_in_servicer notifying button state change.

for(;;){

    // Read the GPI pins logic levels from the input port register 00h over I2C master

    // For every pin with a state change, populate the button_info structure

    //Call send_write_cmd_to_servicer() and send a HID_TASK_RESID_INTERNAL_BUTTON_PRESS command to hid_in_servicer over the device_control context.

    vTaskDelay(pdMS_TO_TICKS(IO_EXP_POLL_TIME_MS));
}

The code for reading the GPI pin logic level and deducing the button state is I2C expander specific and will change when using a different one. Once the button_info structure is populated, the send_write_cmd_to_servicer call to send it via a control command to the HID task will remain the same.

  1. Configure any LED states that need changing. This in done in the io_exp_drive_leds function that is called at the end of the timer driven loop described above.

for(;;){

    io_exp_drive_leds(&io_exp_gpo_state, io_exp_gpo_config, i2c_master_ctx);

    vTaskDelay(pdMS_TO_TICKS(IO_EXP_POLL_TIME_MS));
}

The io_exp_drive_leds function reads the LED modes from the shared io_exp_gpo_state structure that the io_expander_gpo_servicer updates and configures the LED registers over the I2C interface accordingly. The io_exp_drive_leds function will need to be modified when using a different I2C expander.