| /* |
| * Copyright (c) 2023-2024 Nordic Semiconductor ASA |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #include <stdint.h> |
| #include <stdlib.h> |
| |
| #include <sample_usbd.h> |
| #include "feedback.h" |
| |
| #include <zephyr/cache.h> |
| #include <zephyr/device.h> |
| #include <zephyr/usb/usbd.h> |
| #include <zephyr/usb/class/usbd_uac2.h> |
| #include <zephyr/drivers/i2s.h> |
| #include <zephyr/logging/log.h> |
| |
| 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(NULL); |
| if (sample_usbd == NULL) { |
| return -ENODEV; |
| } |
| |
| ret = usbd_enable(sample_usbd); |
| if (ret) { |
| return ret; |
| } |
| |
| return 0; |
| } |