Add the missing "t" to struct usbd_contex. No functional changes. Signed-off-by: Tomasz Moń <tomasz.mon@nordicsemi.no>
299 lines
9.2 KiB
C
299 lines
9.2 KiB
C
/*
|
|
* 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_context *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;
|
|
}
|