From 73a3f2a2c1a127646b0696fe62f4c7345b8a9240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Mo=C5=84?= Date: Wed, 20 Dec 2023 09:40:56 +0100 Subject: [PATCH] samples: usb: add UAC2 explicit feedback sample MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sample illustrates how to calculate feedback value based on I2S sample clock measurement or indirect I2S FRAMESTART and USBD SOF. The sample is currently only supported on nrf5340dk_nrf5340_cpuapp target because the feedback measurement uses target specific peripherals. While it should be possible to perform feedback value calculation entirely in software (possibly with some additional filtering due to software scheduling jitter), the I2S API does not provide necessary timestamps. Signed-off-by: Tomasz Moń --- doc/connectivity/usb/device/usb_device.rst | 2 + .../usb/uac2_explicit_feedback/CMakeLists.txt | 14 + .../subsys/usb/uac2_explicit_feedback/Kconfig | 19 + .../usb/uac2_explicit_feedback/README.rst | 64 +++ .../usb/uac2_explicit_feedback/app.overlay | 44 ++ .../boards/nrf5340dk_nrf5340_cpuapp.conf | 2 + .../boards/nrf5340dk_nrf5340_cpuapp.overlay | 28 ++ .../usb/uac2_explicit_feedback/prj.conf | 12 + .../usb/uac2_explicit_feedback/sample.yaml | 6 + .../usb/uac2_explicit_feedback/src/feedback.h | 23 + .../src/feedback_dummy.c | 39 ++ .../src/feedback_nrf53.c | 398 ++++++++++++++++++ .../usb/uac2_explicit_feedback/src/main.c | 299 +++++++++++++ 13 files changed, 950 insertions(+) create mode 100644 samples/subsys/usb/uac2_explicit_feedback/CMakeLists.txt create mode 100644 samples/subsys/usb/uac2_explicit_feedback/Kconfig create mode 100644 samples/subsys/usb/uac2_explicit_feedback/README.rst create mode 100644 samples/subsys/usb/uac2_explicit_feedback/app.overlay create mode 100644 samples/subsys/usb/uac2_explicit_feedback/boards/nrf5340dk_nrf5340_cpuapp.conf create mode 100644 samples/subsys/usb/uac2_explicit_feedback/boards/nrf5340dk_nrf5340_cpuapp.overlay create mode 100644 samples/subsys/usb/uac2_explicit_feedback/prj.conf create mode 100644 samples/subsys/usb/uac2_explicit_feedback/sample.yaml create mode 100644 samples/subsys/usb/uac2_explicit_feedback/src/feedback.h create mode 100644 samples/subsys/usb/uac2_explicit_feedback/src/feedback_dummy.c create mode 100644 samples/subsys/usb/uac2_explicit_feedback/src/feedback_nrf53.c create mode 100644 samples/subsys/usb/uac2_explicit_feedback/src/main.c diff --git a/doc/connectivity/usb/device/usb_device.rst b/doc/connectivity/usb/device/usb_device.rst index 134ec5972cdd..bf446ae7524c 100644 --- a/doc/connectivity/usb/device/usb_device.rst +++ b/doc/connectivity/usb/device/usb_device.rst @@ -537,6 +537,8 @@ The following Product IDs are currently used: +----------------------------------------------------+--------+ | :zephyr:code-sample:`wpan-usb` | 0x000D | +----------------------------------------------------+--------+ +| :zephyr:code-sample:`uac2-explicit-feedback` | 0x000E | ++----------------------------------------------------+--------+ The USB device descriptor field ``bcdDevice`` (Device Release Number) represents the Zephyr kernel major and minor versions as a binary coded decimal value. diff --git a/samples/subsys/usb/uac2_explicit_feedback/CMakeLists.txt b/samples/subsys/usb/uac2_explicit_feedback/CMakeLists.txt new file mode 100644 index 000000000000..26ce264c4217 --- /dev/null +++ b/samples/subsys/usb/uac2_explicit_feedback/CMakeLists.txt @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(usb_audio_async_i2s) + +include(${ZEPHYR_BASE}/samples/subsys/usb/common/common.cmake) +target_sources(app PRIVATE src/main.c) + +if (CONFIG_SOC_COMPATIBLE_NRF5340_CPUAPP) + target_sources(app PRIVATE src/feedback_nrf53.c) +else() + target_sources(app PRIVATE src/feedback_dummy.c) +endif() diff --git a/samples/subsys/usb/uac2_explicit_feedback/Kconfig b/samples/subsys/usb/uac2_explicit_feedback/Kconfig new file mode 100644 index 000000000000..bee6118a00d6 --- /dev/null +++ b/samples/subsys/usb/uac2_explicit_feedback/Kconfig @@ -0,0 +1,19 @@ +# Copyright (c) 2023-2024 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +menu "UAC2 external feedback sample options" + +config APP_USE_I2S_LRCLK_EDGES_COUNTER + bool "Measure I2S LRCLK edges directly" + help + Use this to use I2S LRCLK edge counting for calculating feedback. + On nRF53 this option requires externally connecting I2S LRCLK back to + separate GPIOTE input pin (P1.09). +endmenu + +# Source common USB sample options used to initialize new experimental USB +# device stack. The scope of these options is limited to USB samples in project +# tree, you cannot use them in your own application. +source "samples/subsys/usb/common/Kconfig.sample_usbd" + +source "Kconfig.zephyr" diff --git a/samples/subsys/usb/uac2_explicit_feedback/README.rst b/samples/subsys/usb/uac2_explicit_feedback/README.rst new file mode 100644 index 000000000000..8d9ff40dc7a2 --- /dev/null +++ b/samples/subsys/usb/uac2_explicit_feedback/README.rst @@ -0,0 +1,64 @@ +.. zephyr:code-sample:: uac2-explicit-feedback + :name: USB Audio asynchronous explicit feedback sample + :relevant-api: _usb_device_core_api i2s_interface + + USB Audio 2 explicit feedback sample playing audio on I2S. + +Overview +******** + +This sample demonstrates how to implement USB asynchronous audio playback with +explicit feedback. It can run on any board with USB and I2S support, but the +feedback calculation is currently only implemented for the Nordic nRF5340 IC. + +The device running this sample presents itself to the host as a Full-Speed +Asynchronous USB Audio 2 class device supporting 48 kHz 16-bit 2-channel +(stereo) playback. + +Explicit Feedback +***************** + +Asynchronous USB Audio is used when the actual sample clock is not controlled by +USB host. Because the sample clock is independent from USB SOF it is inevitable +that 1 ms according to audio sink (I2S) will be either slightly longer or +shorter than 1 ms according to audio source (USB host). In the long run, this +discrepancy leads to overruns or underruns. By providing explicit feedback to +host, the device can tell host how many samples on average it needs every frame. +The host achieves the average by sending either nominal or nominal ±1 sample +packets every frame. + +The dummy feedback implementation, used when there is no target-specific +feedback code available, returns a feedback value that results in host sending +nominal number of samples every frame. Theoretically it should be possible to +obtain the timing information based on I2S and USB interrupts, but currently +neither subsystem provides the necessary timestamp information. + +Explcit Feedback on nRF5340 +*************************** + +The nRF5340 is capable of counting both edges of I2S LRCLK relative to USB SOF +with the use of DPPI, TIMER and GPIOTE input. Alternatively, if the GPIOTE input +is not available, the DPPI and TIMER peripherals on nRF5340 can be configured to +provide relative timing information between I2S FRAMESTART and USB SOF. + +This sample in both modes (direct sample counting and indirect I2S buffer output +to USB SOF offset) has been tested on :ref:`nrf5340dk_nrf5340`. + +The sample defaults to indirect feedback calculation because direct sample +counting requires external connection between I2S LRCLK output pin to GPIOTE +input pin (hardcoded to P1.09) on :ref:`nrf5340dk_nrf5340`. In the indirect mode +no extra connections are necessary and the sample can even be used without any +I2S device connected where I2S signals can be checked e.g. on logic analyzer. + +Building and Running +******************** + +The code can be found in :zephyr_file:`samples/subsys/usb/uac2_explicit_feedback`. + +To build and flash the application: + +.. zephyr-app-commands:: + :zephyr-app: samples/subsys/usb/uac2_explicit_feedback + :board: nrf5340dk_nrf5340_cpuapp + :goals: build flash + :compact: diff --git a/samples/subsys/usb/uac2_explicit_feedback/app.overlay b/samples/subsys/usb/uac2_explicit_feedback/app.overlay new file mode 100644 index 000000000000..08044d529f52 --- /dev/null +++ b/samples/subsys/usb/uac2_explicit_feedback/app.overlay @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +/ { + uac2_headphones: usb_audio2 { + compatible = "zephyr,uac2"; + status = "okay"; + audio-function = ; + + uac_aclk: aclk { + compatible = "zephyr,uac2-clock-source"; + clock-type = "internal-programmable"; + frequency-control = "host-programmable"; + sampling-frequencies = <48000>; + }; + + out_terminal: out_terminal { + compatible = "zephyr,uac2-input-terminal"; + clock-source = <&uac_aclk>; + terminal-type = ; + front-left; + front-right; + }; + + headphones_output: headphones { + compatible = "zephyr,uac2-output-terminal"; + data-source = <&out_terminal>; + clock-source = <&uac_aclk>; + terminal-type = ; + }; + + as_iso_out: out_interface { + compatible = "zephyr,uac2-audio-streaming"; + linked-terminal = <&out_terminal>; + subslot-size = <2>; + bit-resolution = <16>; + }; + }; +}; diff --git a/samples/subsys/usb/uac2_explicit_feedback/boards/nrf5340dk_nrf5340_cpuapp.conf b/samples/subsys/usb/uac2_explicit_feedback/boards/nrf5340dk_nrf5340_cpuapp.conf new file mode 100644 index 000000000000..ce67033381a6 --- /dev/null +++ b/samples/subsys/usb/uac2_explicit_feedback/boards/nrf5340dk_nrf5340_cpuapp.conf @@ -0,0 +1,2 @@ +#Enable timer for asynchronous feedback +CONFIG_NRFX_TIMER2=y diff --git a/samples/subsys/usb/uac2_explicit_feedback/boards/nrf5340dk_nrf5340_cpuapp.overlay b/samples/subsys/usb/uac2_explicit_feedback/boards/nrf5340dk_nrf5340_cpuapp.overlay new file mode 100644 index 000000000000..47205e2ed61e --- /dev/null +++ b/samples/subsys/usb/uac2_explicit_feedback/boards/nrf5340dk_nrf5340_cpuapp.overlay @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "../app.overlay" + +&pinctrl { + i2s0_default_alt: i2s0_default_alt { + group1 { + psels = , + , + ; + }; + }; +}; + +&clock { + hfclkaudio-frequency = <12288000>; +}; + +i2s_tx: &i2s0 { + status = "okay"; + pinctrl-0 = <&i2s0_default_alt>; + pinctrl-names = "default"; + clock-source = "ACLK"; +}; diff --git a/samples/subsys/usb/uac2_explicit_feedback/prj.conf b/samples/subsys/usb/uac2_explicit_feedback/prj.conf new file mode 100644 index 000000000000..3979a5190e6c --- /dev/null +++ b/samples/subsys/usb/uac2_explicit_feedback/prj.conf @@ -0,0 +1,12 @@ +CONFIG_I2S=y + +#USB related configs +CONFIG_USB_DEVICE_STACK_NEXT=y +CONFIG_USBD_AUDIO2_CLASS=y +CONFIG_SAMPLE_USBD_PID=0x000E +CONFIG_SAMPLE_USBD_PRODUCT="UAC2 explicit feedback sample" + +#LOG subsystem related configs +CONFIG_LOG=y +CONFIG_USBD_LOG_LEVEL_WRN=y +CONFIG_UDC_DRIVER_LOG_LEVEL_WRN=y diff --git a/samples/subsys/usb/uac2_explicit_feedback/sample.yaml b/samples/subsys/usb/uac2_explicit_feedback/sample.yaml new file mode 100644 index 000000000000..7fa75c761278 --- /dev/null +++ b/samples/subsys/usb/uac2_explicit_feedback/sample.yaml @@ -0,0 +1,6 @@ +sample: + name: USB Audio 2 asynchronous explicit feedback sample +tests: + sample.subsys.usb.uac2_explicit_feedback: + tags: usb i2s + platform_allow: nrf5340dk_nrf5340_cpuapp diff --git a/samples/subsys/usb/uac2_explicit_feedback/src/feedback.h b/samples/subsys/usb/uac2_explicit_feedback/src/feedback.h new file mode 100644 index 000000000000..7f5bf2027d8f --- /dev/null +++ b/samples/subsys/usb/uac2_explicit_feedback/src/feedback.h @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef FEEDBACK_H_ +#define FEEDBACK_H_ + +#include + +/* Nominal number of samples received on each SOF. This sample is currently + * supporting only 48 kHz sample rate. + */ +#define SAMPLES_PER_SOF 48 + +struct feedback_ctx *feedback_init(void); +void feedback_reset_ctx(struct feedback_ctx *ctx); +void feedback_process(struct feedback_ctx *ctx); +void feedback_start(struct feedback_ctx *ctx, int i2s_blocks_queued); +uint32_t feedback_value(struct feedback_ctx *ctx); + +#endif /* FEEDBACK_H_ */ diff --git a/samples/subsys/usb/uac2_explicit_feedback/src/feedback_dummy.c b/samples/subsys/usb/uac2_explicit_feedback/src/feedback_dummy.c new file mode 100644 index 000000000000..7e274ca2ed05 --- /dev/null +++ b/samples/subsys/usb/uac2_explicit_feedback/src/feedback_dummy.c @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "feedback.h" + +#warning "No target specific feedback code, overruns/underruns will occur" + +#define FEEDBACK_K 10 + +struct feedback_ctx *feedback_init(void) +{ + return NULL; +} + +void feedback_process(struct feedback_ctx *ctx) +{ + ARG_UNUSED(ctx); +} + +void feedback_reset_ctx(struct feedback_ctx *ctx) +{ + ARG_UNUSED(ctx); +} + +void feedback_start(struct feedback_ctx *ctx, int i2s_blocks_queued) +{ + ARG_UNUSED(ctx); + ARG_UNUSED(i2s_blocks_queued); +} + +uint32_t feedback_value(struct feedback_ctx *ctx) +{ + /* Always request nominal number of samples */ + return SAMPLES_PER_SOF << FEEDBACK_K; +} diff --git a/samples/subsys/usb/uac2_explicit_feedback/src/feedback_nrf53.c b/samples/subsys/usb/uac2_explicit_feedback/src/feedback_nrf53.c new file mode 100644 index 000000000000..e5f93a846fdf --- /dev/null +++ b/samples/subsys/usb/uac2_explicit_feedback/src/feedback_nrf53.c @@ -0,0 +1,398 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "feedback.h" + +#include +#include +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(feedback, LOG_LEVEL_INF); + +static const nrfx_gpiote_t gpiote = NRFX_GPIOTE_INSTANCE(0); + +#define FEEDBACK_PIN NRF_GPIO_PIN_MAP(1, 9) +#define FEEDBACK_TIMER_INSTANCE_NUMBER 2 +#define FEEDBACK_TIMER_USBD_SOF_CAPTURE 0 +#define FEEDBACK_TIMER_I2S_FRAMESTART_CAPTURE 1 + +static const nrfx_timer_t feedback_timer_instance = + NRFX_TIMER_INSTANCE(FEEDBACK_TIMER_INSTANCE_NUMBER); + +/* See 5.12.4.2 Feedback in Universal Serial Bus Specification Revision 2.0 for + * more information about the feedback. There is a direct implementation of the + * specification where P=1 when @kconfig{CONFIG_APP_USE_I2S_LRCLK_EDGES_COUNTER} + * is enabled, because I2S LRCLK edges (and not the clock) are being counted by + * a timer. Otherwise, when @kconfig{CONFIG_APP_USE_I2S_LRCLK_EDGES_COUNTER} is + * disabled, we are faking P=5 value using indirect offset measurements and + * we use such estimate on PI controller updated on every SOF. + * + * While it might be possible to determine I2S FRAMESTART to USB SOF offset + * entirely in software, the I2S API lacks appropriate timestamping. Therefore + * this sample uses target-specific code to perform the measurements. Note that + * the use of dedicated target-specific peripheral essentially eliminates + * software scheduling jitter and it is likely that a pure software only + * solution would require additional filtering in indirect offset measurements. + * + * Full-Speed isochronous feedback is Q10.10 unsigned integer left-justified in + * the 24-bits so it has Q10.14 format. This sample application puts zeroes to + * the 4 least significant bits (does not use the bits for extra precision). + */ +#define FEEDBACK_K 10 +#if IS_ENABLED(CONFIG_APP_USE_I2S_LRCLK_EDGES_COUNTER) +#define FEEDBACK_P 1 +#else +#define FEEDBACK_P 5 +#endif + +#define FEEDBACK_FS_SHIFT 4 + +static struct feedback_ctx { + uint32_t fb_value; + int32_t rel_sof_offset; + int32_t base_sof_offset; + union { + /* For edge counting */ + struct { + uint32_t fb_counter; + uint16_t fb_periods; + }; + /* For PI controller */ + int32_t integrator; + }; +} fb_ctx; + +static nrfx_err_t feedback_edge_counter_setup(void) +{ + nrfx_err_t err; + uint8_t feedback_gpiote_channel; + uint8_t feedback_gppi_channel; + nrfx_gpiote_trigger_config_t trigger_config = { + .trigger = NRFX_GPIOTE_TRIGGER_TOGGLE, + .p_in_channel = &feedback_gpiote_channel, + }; + nrfx_gpiote_input_pin_config_t input_pin_config = { + .p_trigger_config = &trigger_config, + }; + + /* App core is using feedback pin */ + nrf_gpio_pin_control_select(FEEDBACK_PIN, NRF_GPIO_PIN_SEL_APP); + + err = nrfx_gpiote_channel_alloc(&gpiote, &feedback_gpiote_channel); + if (err != NRFX_SUCCESS) { + return err; + } + + nrfx_gpiote_input_configure(&gpiote, FEEDBACK_PIN, &input_pin_config); + nrfx_gpiote_trigger_enable(&gpiote, FEEDBACK_PIN, false); + + /* Configure TIMER in COUNTER mode */ + const nrfx_timer_config_t cfg = { + .frequency = NRFX_MHZ_TO_HZ(1UL), + .mode = NRF_TIMER_MODE_COUNTER, + .bit_width = NRF_TIMER_BIT_WIDTH_32, + .interrupt_priority = NRFX_TIMER_DEFAULT_CONFIG_IRQ_PRIORITY, + .p_context = NULL, + }; + + err = nrfx_timer_init(&feedback_timer_instance, &cfg, NULL); + if (err != NRFX_SUCCESS) { + LOG_ERR("nrfx timer init error (sample clk feedback) - Return value: %d", err); + return err; + } + + /* Subscribe TIMER COUNT task to GPIOTE IN event */ + err = nrfx_gppi_channel_alloc(&feedback_gppi_channel); + if (err != NRFX_SUCCESS) { + LOG_ERR("gppi_channel_alloc failed with: %d\n", err); + return err; + } + + nrfx_gppi_channel_endpoints_setup(feedback_gppi_channel, + nrfx_gpiote_in_event_address_get(&gpiote, FEEDBACK_PIN), + nrfx_timer_task_address_get(&feedback_timer_instance, NRF_TIMER_TASK_COUNT)); + + nrfx_gppi_channels_enable(BIT(feedback_gppi_channel)); + + return NRFX_SUCCESS; +} + +static nrfx_err_t feedback_relative_timer_setup(void) +{ + nrfx_err_t err; + const nrfx_timer_config_t cfg = { + .frequency = NRFX_MHZ_TO_HZ(16UL), + .mode = NRF_TIMER_MODE_TIMER, + .bit_width = NRF_TIMER_BIT_WIDTH_32, + .interrupt_priority = NRFX_TIMER_DEFAULT_CONFIG_IRQ_PRIORITY, + .p_context = NULL, + }; + + err = nrfx_timer_init(&feedback_timer_instance, &cfg, NULL); + if (err != NRFX_SUCCESS) { + LOG_ERR("nrfx timer init error (relative timer) - Return value: %d", err); + } + + return err; +} + +struct feedback_ctx *feedback_init(void) +{ + nrfx_err_t err; + uint8_t usbd_sof_gppi_channel; + uint8_t i2s_framestart_gppi_channel; + + feedback_reset_ctx(&fb_ctx); + + if (IS_ENABLED(CONFIG_APP_USE_I2S_LRCLK_EDGES_COUNTER)) { + err = feedback_edge_counter_setup(); + } else { + err = feedback_relative_timer_setup(); + } + + if (err != NRFX_SUCCESS) { + return &fb_ctx; + } + + /* Subscribe TIMER CAPTURE task to USBD SOF event */ + err = nrfx_gppi_channel_alloc(&usbd_sof_gppi_channel); + if (err != NRFX_SUCCESS) { + LOG_ERR("gppi_channel_alloc failed with: %d\n", err); + return &fb_ctx; + } + + nrfx_gppi_channel_endpoints_setup(usbd_sof_gppi_channel, + nrf_usbd_event_address_get(NRF_USBD, NRF_USBD_EVENT_SOF), + nrfx_timer_capture_task_address_get(&feedback_timer_instance, + FEEDBACK_TIMER_USBD_SOF_CAPTURE)); + nrfx_gppi_fork_endpoint_setup(usbd_sof_gppi_channel, + nrfx_timer_task_address_get(&feedback_timer_instance, + NRF_TIMER_TASK_CLEAR)); + + nrfx_gppi_channels_enable(BIT(usbd_sof_gppi_channel)); + + /* Subscribe TIMER CAPTURE task to I2S FRAMESTART event */ + err = nrfx_gppi_channel_alloc(&i2s_framestart_gppi_channel); + if (err != NRFX_SUCCESS) { + LOG_ERR("gppi_channel_alloc failed with: %d\n", err); + return &fb_ctx; + } + + nrfx_gppi_channel_endpoints_setup(i2s_framestart_gppi_channel, + nrf_i2s_event_address_get(NRF_I2S0, NRF_I2S_EVENT_FRAMESTART), + nrfx_timer_capture_task_address_get(&feedback_timer_instance, + FEEDBACK_TIMER_I2S_FRAMESTART_CAPTURE)); + + nrfx_gppi_channels_enable(BIT(i2s_framestart_gppi_channel)); + + /* Enable feedback timer */ + nrfx_timer_enable(&feedback_timer_instance); + + return &fb_ctx; +} + +static void update_sof_offset(struct feedback_ctx *ctx, uint32_t sof_cc, + uint32_t framestart_cc) +{ + int sof_offset; + + if (!IS_ENABLED(CONFIG_APP_USE_I2S_LRCLK_EDGES_COUNTER)) { + uint32_t clks_per_edge; + + /* Convert timer clock (independent from both Audio clock and + * USB host SOF clock) to fake sample clock shifted by P values. + * This works fine because the regulator cares only about error + * (SOF offset is both error and regulator input) and achieves + * its goal by adjusting feedback value. SOF offset is around 0 + * when regulated and therefore the relative clock frequency + * discrepancies are essentially negligible. + */ + clks_per_edge = sof_cc / (SAMPLES_PER_SOF << FEEDBACK_P); + sof_cc /= MAX(clks_per_edge, 1); + framestart_cc /= MAX(clks_per_edge, 1); + } + + /* /2 because we treat the middle as a turning point from being + * "too late" to "too early". + */ + if (framestart_cc > (SAMPLES_PER_SOF << FEEDBACK_P)/2) { + sof_offset = framestart_cc - (SAMPLES_PER_SOF << FEEDBACK_P); + } else { + sof_offset = framestart_cc; + } + + /* The heuristic above is not enough when the offset gets too large. + * If the sign of the simple heuristic changes, check whether the offset + * crossed through the zero or the outer bound. + */ + if ((ctx->rel_sof_offset >= 0) != (sof_offset >= 0)) { + uint32_t abs_diff; + int32_t base_change; + + if (sof_offset >= 0) { + abs_diff = sof_offset - ctx->rel_sof_offset; + base_change = -(SAMPLES_PER_SOF << FEEDBACK_P); + } else { + abs_diff = ctx->rel_sof_offset - sof_offset; + base_change = SAMPLES_PER_SOF << FEEDBACK_P; + } + + /* Adjust base offset only if the change happened through the + * outer bound. The actual changes should be significantly lower + * than the threshold here. + */ + if (abs_diff > (SAMPLES_PER_SOF << FEEDBACK_P)/2) { + ctx->base_sof_offset += base_change; + } + } + + ctx->rel_sof_offset = sof_offset; +} + +static inline int32_t offset_to_correction(int32_t offset) +{ + return -(offset / BIT(FEEDBACK_P)) * BIT(FEEDBACK_FS_SHIFT); +} + +static int32_t pi_update(struct feedback_ctx *ctx) +{ + int32_t sof_offset = ctx->rel_sof_offset + ctx->base_sof_offset; + /* SOF offset is measured in pow(2, -FEEDBACK_P) samples, i.e. when + * FEEDBACK_P is 0, offset is in samples, and for 1 -> half-samples, + * 2 -> quarter-samples, 3 -> eightth-samples and so on. + * In order to simplify the PI controller description here, normalize + * the offset to 1/1024 samples (alternatively it can be treated as + * samples in Q10 fixed point format) and use it as Process Variable. + */ + int32_t PV = BIT(10 - FEEDBACK_P) * sof_offset; + /* The control goal is to keep I2S FRAMESTART as close as possible to + * USB SOF and therefore Set Point is 0. + */ + int32_t SP = 0; + int32_t error = SP - PV; + + /* + * With above normalization at Full-Speed, when data received during + * SOF n appears on I2S during SOF n+3, the Ziegler Nichols Ultimate + * Gain is around 1.15 and the oscillation period is around 90 SOF. + * (much nicer oscillations with 204.8 SOF period can be observed with + * gain 0.5 when the delay is not n+3, but n+33 - surprisingly the + * resulting PI coefficients after power of two rounding are the same). + * + * Ziegler-Nichols rule with applied stability margin of 2 results in: + * Kc = 0.22 * Ku = 0.22 * 1.15 = 0.253 + * Ti = 0.83 * tu = 0.83 * 80 = 66.4 + * + * Converting the rules above to parallel PI gives: + * Kp = Kc = 0.253 + * Ki = Kc/Ti = 0.254/66.4 ~= 0.0038253 + * + * Because we want fixed-point optimized non-tunable implementation, + * the parameters can be conveniently expressed with power of two: + * Kp ~= pow(2, -2) = 0.25 (divide by 4) + * Ki ~= pow(2, -8) = 0.0039 (divide by 256) + * + * This can be implemented as: + * ctx->integrator += error; + * return (error + (ctx->integrator / 64)) / 4; + * but unfortunately such regulator is pretty aggressive and keeps + * oscillating rather quickly around the setpoint (within +-1 sample). + * + * Manually tweaking the constants so the regulator output is shifted + * down by 4 bits (i.e. change /64 to /2048 and /4 to /128) yields + * really good results (the outcome is similar, even slightly better, + * than using I2S LRCLK edge counting directly). + */ + ctx->integrator += error; + return (error + (ctx->integrator / 2048)) / 128; +} + +void feedback_process(struct feedback_ctx *ctx) +{ + uint32_t sof_cc; + uint32_t framestart_cc; + uint32_t fb; + + sof_cc = nrfx_timer_capture_get(&feedback_timer_instance, + FEEDBACK_TIMER_USBD_SOF_CAPTURE); + framestart_cc = nrfx_timer_capture_get(&feedback_timer_instance, + FEEDBACK_TIMER_I2S_FRAMESTART_CAPTURE); + + update_sof_offset(ctx, sof_cc, framestart_cc); + + if (IS_ENABLED(CONFIG_APP_USE_I2S_LRCLK_EDGES_COUNTER)) { + int32_t offset = ctx->rel_sof_offset + ctx->base_sof_offset; + + ctx->fb_counter += sof_cc; + ctx->fb_periods++; + + if (ctx->fb_periods == BIT(FEEDBACK_K - FEEDBACK_P)) { + + /* fb_counter holds Q10.10 value, left-justify it */ + fb = ctx->fb_counter << FEEDBACK_FS_SHIFT; + + /* Align I2S FRAMESTART to USB SOF by adjusting reported + * feedback value. This is endpoint specific correction + * mentioned but not specified in USB 2.0 Specification. + */ + if (abs(offset) > BIT(FEEDBACK_P)) { + fb += offset_to_correction(offset); + } + + ctx->fb_value = fb; + ctx->fb_counter = 0; + ctx->fb_periods = 0; + } + } else { + /* Use PI controller to generate required feedback deviation + * from nominal feedback value. + */ + fb = SAMPLES_PER_SOF << (FEEDBACK_K + FEEDBACK_FS_SHIFT); + /* Clear the additional LSB bits in feedback value, i.e. do not + * use the optional extra resolution. + */ + fb += pi_update(ctx) & ~0xF; + ctx->fb_value = fb; + } +} + +void feedback_reset_ctx(struct feedback_ctx *ctx) +{ + /* Reset feedback to nominal value */ + ctx->fb_value = SAMPLES_PER_SOF << (FEEDBACK_K + FEEDBACK_FS_SHIFT); + if (IS_ENABLED(CONFIG_APP_USE_I2S_LRCLK_EDGES_COUNTER)) { + ctx->fb_counter = 0; + ctx->fb_periods = 0; + } else { + ctx->integrator = 0; + } +} + +void feedback_start(struct feedback_ctx *ctx, int i2s_blocks_queued) +{ + /* I2S data was supposed to go out at SOF, but it is inevitably + * delayed due to triggering I2S start by software. Set relative + * SOF offset value in a way that ensures that values past "half + * frame" are treated as "too late" instead of "too early" + */ + ctx->rel_sof_offset = (SAMPLES_PER_SOF << FEEDBACK_P) / 2; + /* If there are more than 2 I2S blocks queued, use feedback regulator + * to correct the situation. + */ + ctx->base_sof_offset = (i2s_blocks_queued - 2) * + (SAMPLES_PER_SOF << FEEDBACK_P); +} + +uint32_t feedback_value(struct feedback_ctx *ctx) +{ + return ctx->fb_value; +} diff --git a/samples/subsys/usb/uac2_explicit_feedback/src/main.c b/samples/subsys/usb/uac2_explicit_feedback/src/main.c new file mode 100644 index 000000000000..69dc731833d2 --- /dev/null +++ b/samples/subsys/usb/uac2_explicit_feedback/src/main.c @@ -0,0 +1,299 @@ +/* + * Copyright (c) 2023-2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include +#include "feedback.h" + +#include +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(uac2_sample, LOG_LEVEL_INF); + +#define HEADPHONES_OUT_TERMINAL_ID UAC2_ENTITY_ID(DT_NODELABEL(out_terminal)) + +#define SAMPLE_FREQUENCY (SAMPLES_PER_SOF * 1000) +#define SAMPLE_BIT_WIDTH 16 +#define NUMBER_OF_CHANNELS 2 +#define BYTES_PER_SAMPLE DIV_ROUND_UP(SAMPLE_BIT_WIDTH, 8) +#define BYTES_PER_SLOT (BYTES_PER_SAMPLE * NUMBER_OF_CHANNELS) +#define MIN_BLOCK_SIZE ((SAMPLES_PER_SOF - 1) * BYTES_PER_SLOT) +#define BLOCK_SIZE (SAMPLES_PER_SOF * BYTES_PER_SLOT) +#define MAX_BLOCK_SIZE ((SAMPLES_PER_SOF + 1) * BYTES_PER_SLOT) + +/* Absolute minimum is 5 buffers (1 actively consumed by I2S, 2nd queued as next + * buffer, 3rd acquired by USB stack to receive data to, and 2 to handle SOF/I2S + * offset errors), but add 2 additional buffers to prevent out of memory errors + * when USB host decides to perform rapid terminal enable/disable cycles. + */ +#define I2S_BUFFERS_COUNT 7 +K_MEM_SLAB_DEFINE_STATIC(i2s_tx_slab, MAX_BLOCK_SIZE, I2S_BUFFERS_COUNT, 4); + +struct usb_i2s_ctx { + const struct device *i2s_dev; + bool terminal_enabled; + bool i2s_started; + /* Number of blocks written, used to determine when to start I2S. + * Overflows are not a problem becuse this variable is not necessary + * after I2S is started. + */ + uint8_t i2s_blocks_written; + struct feedback_ctx *fb; +}; + +static void uac2_terminal_update_cb(const struct device *dev, uint8_t terminal, + bool enabled, bool microframes, + void *user_data) +{ + struct usb_i2s_ctx *ctx = user_data; + + /* This sample has only one terminal therefore the callback can simply + * ignore the terminal variable. + */ + __ASSERT_NO_MSG(terminal == HEADPHONES_OUT_TERMINAL_ID); + /* This sample is for Full-Speed only devices. */ + __ASSERT_NO_MSG(microframes == false); + + ctx->terminal_enabled = enabled; + if (ctx->i2s_started && !enabled) { + i2s_trigger(ctx->i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP); + ctx->i2s_started = false; + ctx->i2s_blocks_written = 0; + feedback_reset_ctx(ctx->fb); + } +} + +static void *uac2_get_recv_buf(const struct device *dev, uint8_t terminal, + uint16_t size, void *user_data) +{ + ARG_UNUSED(dev); + struct usb_i2s_ctx *ctx = user_data; + void *buf = NULL; + int ret; + + if (terminal == HEADPHONES_OUT_TERMINAL_ID) { + __ASSERT_NO_MSG(size <= MAX_BLOCK_SIZE); + + if (!ctx->terminal_enabled) { + LOG_ERR("Buffer request on disabled terminal"); + return NULL; + } + + ret = k_mem_slab_alloc(&i2s_tx_slab, &buf, K_NO_WAIT); + if (ret != 0) { + buf = NULL; + } + } + + return buf; +} + +static void uac2_data_recv_cb(const struct device *dev, uint8_t terminal, + void *buf, uint16_t size, void *user_data) +{ + struct usb_i2s_ctx *ctx = user_data; + int ret; + + if (!ctx->terminal_enabled) { + k_mem_slab_free(&i2s_tx_slab, buf); + return; + } + + if (!size) { + /* Zero fill to keep I2S going. If this is transient error, then + * this is probably best we can do. Otherwise, host will likely + * either disable terminal (or the cable will be disconnected) + * which will stop I2S. + */ + size = BLOCK_SIZE; + memset(buf, 0, size); + sys_cache_data_flush_range(buf, size); + } + + LOG_DBG("Received %d data to input terminal %d", size, terminal); + + ret = i2s_write(ctx->i2s_dev, buf, size); + if (ret < 0) { + ctx->i2s_started = false; + ctx->i2s_blocks_written = 0; + feedback_reset_ctx(ctx->fb); + + /* Most likely underrun occurred, prepare I2S restart */ + i2s_trigger(ctx->i2s_dev, I2S_DIR_TX, I2S_TRIGGER_PREPARE); + + ret = i2s_write(ctx->i2s_dev, buf, size); + if (ret < 0) { + /* Drop data block, will try again on next frame */ + k_mem_slab_free(&i2s_tx_slab, buf); + } + } + + if (ret == 0) { + ctx->i2s_blocks_written++; + } +} + +static void uac2_buf_release_cb(const struct device *dev, uint8_t terminal, + void *buf, void *user_data) +{ + /* This sample does not send audio data so this won't be called */ +} + +/* Variables for debug use to facilitate simple how feedback value affects + * audio data rate experiments. These debug variables can also be used to + * determine how well the feedback regulator deals with errors. The values + * are supposed to be modified by debugger. + * + * Setting use_hardcoded_feedback to true, essentially bypasses the feedback + * regulator and makes host send hardcoded_feedback samples every 16384 SOFs + * (when operating at Full-Speed). + * + * The feedback at Full-Speed is Q10.14 value. For 48 kHz audio sample rate, + * there are nominally 48 samples every SOF. The corresponding value is thus + * 48 << 14. Such feedback value would result in host sending always 48 samples. + * Now, if we want to receive more samples (because 1 ms according to audio + * sink is shorter than 1 ms according to USB Host 500 ppm SOF timer), then + * the feedback value has to be increased. The fractional part is 14-bit wide + * and therefore increment by 1 means 1 additional sample every 2**14 SOFs. + * (48 << 14) + 1 therefore results in host sending 48 samples 16383 times and + * 49 samples 1 time during every 16384 SOFs. + * + * Similarly, if we want to receive less samples (because 1 ms according to + * audio signk is longer than 1 ms according to USB Host), then the feedback + * value has to be decreased. (48 << 14) - 1 therefore results in host sending + * 48 samples 16383 times and 47 samples 1 time during every 16384 SOFs. + * + * If the feedback value differs by more than 1 (i.e. LSB), then the +1/-1 + * samples packets are generally evenly distributed. For example feedback value + * (48 << 14) + (1 << 5) results in 48 samples 511 times and 49 samples 1 time + * during every 512 SOFs. + * + * For High-Speed above changes slightly, because the feedback format is Q16.16 + * and microframes are used. The 48 kHz audio sample rate is achieved by sending + * 6 samples every SOF (microframe). The nominal value is the average number of + * samples to send every microframe and therefore for 48 kHz the nominal value + * is (6 << 16). + */ +static volatile bool use_hardcoded_feedback; +static volatile uint32_t hardcoded_feedback = (48 << 14) + 1; + +static uint32_t uac2_feedback_cb(const struct device *dev, uint8_t terminal, + void *user_data) +{ + /* Sample has only one UAC2 instance with one terminal so both can be + * ignored here. + */ + ARG_UNUSED(dev); + ARG_UNUSED(terminal); + struct usb_i2s_ctx *ctx = user_data; + + if (use_hardcoded_feedback) { + return hardcoded_feedback; + } else { + return feedback_value(ctx->fb); + } +} + +static void uac2_sof(const struct device *dev, void *user_data) +{ + ARG_UNUSED(dev); + struct usb_i2s_ctx *ctx = user_data; + + if (ctx->i2s_started) { + feedback_process(ctx->fb); + } + + /* We want to maintain 3 SOFs delay, i.e. samples received during SOF n + * should be on I2S during SOF n+3. This provides enough wiggle room + * for software scheduling that effectively eliminates "buffers not + * provided in time" problem. + * + * ">= 2" translates into 3 SOFs delay because the timeline is: + * USB SOF n + * OUT DATA0 n received from host + * USB SOF n+1 + * DATA0 n is available to UDC driver (See Universal Serial Bus + * Specification Revision 2.0 5.12.5 Data Prebuffering) and copied + * to I2S buffer before SOF n+2; i2s_blocks_written = 1 + * OUT DATA0 n+1 received from host + * USB SOF n+2 + * DATA0 n+1 is copied; i2s_block_written = 2 + * OUT DATA0 n+2 received from host + * USB SOF n+3 + * This function triggers I2S start + * DATA0 n+2 is copied; i2s_block_written is no longer relevant + * OUT DATA0 n+3 received from host + */ + if (!ctx->i2s_started && ctx->terminal_enabled && + ctx->i2s_blocks_written >= 2) { + i2s_trigger(ctx->i2s_dev, I2S_DIR_TX, I2S_TRIGGER_START); + ctx->i2s_started = true; + feedback_start(ctx->fb, ctx->i2s_blocks_written); + } +} + +static struct uac2_ops usb_audio_ops = { + .sof_cb = uac2_sof, + .terminal_update_cb = uac2_terminal_update_cb, + .get_recv_buf = uac2_get_recv_buf, + .data_recv_cb = uac2_data_recv_cb, + .buf_release_cb = uac2_buf_release_cb, + .feedback_cb = uac2_feedback_cb, +}; + +static struct usb_i2s_ctx main_ctx; + +int main(void) +{ + const struct device *dev = DEVICE_DT_GET(DT_NODELABEL(uac2_headphones)); + struct usbd_contex *sample_usbd; + struct i2s_config config; + int ret; + + main_ctx.i2s_dev = DEVICE_DT_GET(DT_NODELABEL(i2s_tx)); + + if (!device_is_ready(main_ctx.i2s_dev)) { + printk("%s is not ready\n", main_ctx.i2s_dev->name); + return 0; + } + + config.word_size = SAMPLE_BIT_WIDTH; + config.channels = NUMBER_OF_CHANNELS; + config.format = I2S_FMT_DATA_FORMAT_I2S; + config.options = I2S_OPT_BIT_CLK_MASTER | I2S_OPT_FRAME_CLK_MASTER; + config.frame_clk_freq = SAMPLE_FREQUENCY; + config.mem_slab = &i2s_tx_slab; + config.block_size = MAX_BLOCK_SIZE; + config.timeout = 0; + + ret = i2s_configure(main_ctx.i2s_dev, I2S_DIR_TX, &config); + if (ret < 0) { + printk("Failed to configure TX stream: %d\n", ret); + return 0; + } + + main_ctx.fb = feedback_init(); + + usbd_uac2_set_ops(dev, &usb_audio_ops, &main_ctx); + + sample_usbd = sample_usbd_init_device(); + if (sample_usbd == NULL) { + return -ENODEV; + } + + ret = usbd_enable(sample_usbd); + if (ret) { + return ret; + } + + return 0; +}