2024-11-27 16:19:17 +01:00
|
|
|
/*
|
|
|
|
* Copyright (c) 2024 Nordic Semiconductor ASA
|
|
|
|
*
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
*/
|
|
|
|
|
|
|
|
#if CONFIG_CLOCK_CONTROL_NRF
|
|
|
|
#include <hal/nrf_clock.h>
|
|
|
|
#endif
|
|
|
|
#include <hal/nrf_tdm.h>
|
|
|
|
#include <haly/nrfy_gpio.h>
|
|
|
|
#include <zephyr/drivers/clock_control/nrf_clock_control.h>
|
|
|
|
#include <zephyr/drivers/i2s.h>
|
|
|
|
#include <zephyr/drivers/pinctrl.h>
|
|
|
|
#include <zephyr/irq.h>
|
|
|
|
#include <zephyr/kernel.h>
|
|
|
|
#include <zephyr/logging/log.h>
|
|
|
|
#include <dmm.h>
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
|
|
|
LOG_MODULE_REGISTER(tdm_nrf, CONFIG_I2S_LOG_LEVEL);
|
|
|
|
|
|
|
|
/* The application must provide buffers that are to be used in the next
|
|
|
|
* part of the transfer.
|
|
|
|
*/
|
|
|
|
#define NRFX_TDM_STATUS_NEXT_BUFFERS_NEEDED BIT(0)
|
|
|
|
|
|
|
|
/* The TDM peripheral has been stopped and all buffers that were passed
|
|
|
|
* to the driver have been released.
|
|
|
|
*/
|
|
|
|
#define NRFX_TDM_STATUS_TRANSFER_STOPPED BIT(1)
|
|
|
|
|
|
|
|
#define NRFX_TDM_NUM_OF_CHANNELS (TDM_CONFIG_CHANNEL_NUM_NUM_Max + 1)
|
|
|
|
|
|
|
|
#define NRFX_TDM_TX_CHANNELS_MASK \
|
|
|
|
GENMASK(TDM_CONFIG_CHANNEL_MASK_Tx0Enable_Pos + TDM_CONFIG_CHANNEL_NUM_NUM_Max, \
|
|
|
|
TDM_CONFIG_CHANNEL_MASK_Tx0Enable_Pos)
|
|
|
|
#define NRFX_TDM_RX_CHANNELS_MASK \
|
|
|
|
GENMASK(TDM_CONFIG_CHANNEL_MASK_Rx0Enable_Pos + TDM_CONFIG_CHANNEL_NUM_NUM_Max, \
|
|
|
|
TDM_CONFIG_CHANNEL_MASK_Rx0Enable_Pos)
|
|
|
|
|
|
|
|
#if DT_NODE_HAS_STATUS_OKAY(DT_NODELABEL(audiopll))
|
|
|
|
#define NODE_ACLK DT_NODELABEL(audiopll)
|
|
|
|
#define ACLK_FREQUENCY DT_PROP_OR(NODE_ACLK, frequency, 0)
|
|
|
|
|
|
|
|
static const struct device *audiopll = DEVICE_DT_GET(NODE_ACLK);
|
|
|
|
static const struct nrf_clock_spec aclk_spec = {
|
|
|
|
.frequency = ACLK_FREQUENCY,
|
|
|
|
};
|
|
|
|
#elif DT_NODE_HAS_STATUS_OKAY(DT_NODELABEL(aclk))
|
|
|
|
#define NODE_ACLK DT_NODELABEL(aclk)
|
|
|
|
#define ACLK_FREQUENCY DT_PROP_OR(NODE_ACLK, clock_frequency, 0)
|
|
|
|
#else
|
|
|
|
#define ACLK_FREQUENCY 0
|
|
|
|
#endif
|
|
|
|
|
|
|
|
typedef struct {
|
|
|
|
uint32_t *p_rx_buffer;
|
|
|
|
uint32_t const *p_tx_buffer;
|
|
|
|
void *p_tx_mem_slab;
|
|
|
|
void *p_rx_mem_slab;
|
|
|
|
uint16_t buffer_size;
|
|
|
|
} tdm_buffers_t;
|
|
|
|
|
|
|
|
typedef void (*tdm_data_handler_t)(tdm_buffers_t const *p_released, uint32_t status);
|
|
|
|
|
|
|
|
typedef struct {
|
|
|
|
tdm_data_handler_t handler;
|
|
|
|
bool use_rx: 1;
|
|
|
|
bool use_tx: 1;
|
|
|
|
bool rx_ready: 1;
|
|
|
|
bool tx_ready: 1;
|
|
|
|
bool buffers_needed: 1;
|
|
|
|
bool buffers_reused: 1;
|
|
|
|
tdm_buffers_t next_buffers;
|
|
|
|
tdm_buffers_t current_buffers;
|
|
|
|
} tdm_ctrl_t;
|
|
|
|
|
|
|
|
struct stream_cfg {
|
|
|
|
struct i2s_config cfg;
|
|
|
|
nrf_tdm_config_t nrfx_cfg;
|
|
|
|
};
|
|
|
|
|
|
|
|
struct tdm_buf {
|
|
|
|
void *mem_block;
|
|
|
|
size_t size;
|
|
|
|
void *dmm_buf;
|
|
|
|
};
|
|
|
|
|
|
|
|
struct tdm_drv_cfg {
|
|
|
|
tdm_data_handler_t data_handler;
|
|
|
|
const struct pinctrl_dev_config *pcfg;
|
|
|
|
NRF_TDM_Type *p_reg;
|
|
|
|
void *mem_reg;
|
|
|
|
tdm_ctrl_t *control_data;
|
|
|
|
uint32_t mck_frequency;
|
|
|
|
uint32_t pclk_frequency;
|
|
|
|
enum clock_source {
|
|
|
|
PCLK,
|
|
|
|
ACLK
|
|
|
|
} sck_src, mck_src;
|
|
|
|
};
|
|
|
|
|
|
|
|
struct tdm_drv_data {
|
|
|
|
#if CONFIG_CLOCK_CONTROL_NRF
|
|
|
|
struct onoff_manager *clk_mgr;
|
|
|
|
#endif
|
|
|
|
struct onoff_client clk_cli;
|
|
|
|
struct stream_cfg tx;
|
|
|
|
struct k_msgq tx_queue;
|
|
|
|
struct stream_cfg rx;
|
|
|
|
struct k_msgq rx_queue;
|
|
|
|
const struct tdm_drv_cfg *drv_cfg;
|
|
|
|
const uint32_t *last_tx_buffer;
|
|
|
|
void *last_tx_mem_slab;
|
|
|
|
enum i2s_state state;
|
|
|
|
enum i2s_dir active_dir;
|
|
|
|
bool stop; /* stop after the current (TX or RX) block */
|
|
|
|
bool discard_rx; /* discard further RX blocks */
|
|
|
|
volatile bool next_tx_buffer_needed;
|
|
|
|
bool tx_configured: 1;
|
|
|
|
bool rx_configured: 1;
|
|
|
|
bool request_clock: 1;
|
|
|
|
};
|
|
|
|
|
|
|
|
static int audio_clock_request(struct tdm_drv_data *drv_data)
|
|
|
|
{
|
|
|
|
#if DT_NODE_HAS_STATUS_OKAY(NODE_ACLK) && CONFIG_CLOCK_CONTROL_NRF
|
|
|
|
return onoff_request(drv_data->clk_mgr, &drv_data->clk_cli);
|
2025-06-06 12:17:06 +02:00
|
|
|
#elif DT_NODE_HAS_STATUS_OKAY(NODE_ACLK) && CONFIG_CLOCK_CONTROL_NRFS_AUDIOPLL
|
2024-11-27 16:19:17 +01:00
|
|
|
return nrf_clock_control_request(audiopll, &aclk_spec, &drv_data->clk_cli);
|
|
|
|
#else
|
|
|
|
(void)drv_data;
|
|
|
|
|
|
|
|
return -ENOTSUP;
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
|
|
|
static int audio_clock_release(struct tdm_drv_data *drv_data)
|
|
|
|
{
|
|
|
|
#if DT_NODE_HAS_STATUS_OKAY(NODE_ACLK) && CONFIG_CLOCK_CONTROL_NRF
|
|
|
|
return onoff_release(drv_data->clk_mgr);
|
2025-06-06 12:17:06 +02:00
|
|
|
#elif DT_NODE_HAS_STATUS_OKAY(NODE_ACLK) && CONFIG_CLOCK_CONTROL_NRFS_AUDIOPLL
|
2024-11-27 16:19:17 +01:00
|
|
|
(void)drv_data;
|
|
|
|
|
|
|
|
return nrf_clock_control_release(audiopll, &aclk_spec);
|
|
|
|
#else
|
|
|
|
(void)drv_data;
|
|
|
|
|
|
|
|
return -ENOTSUP;
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
|
|
|
static nrf_tdm_channels_count_t nrf_tdm_chan_num_get(uint8_t nb_of_channels)
|
|
|
|
{
|
|
|
|
return (nrf_tdm_channels_count_t)(NRF_TDM_CHANNELS_COUNT_1 + nb_of_channels - 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void tdm_irq_handler(const struct device *dev)
|
|
|
|
{
|
|
|
|
const struct tdm_drv_cfg *drv_cfg = dev->config;
|
|
|
|
NRF_TDM_Type *p_reg = drv_cfg->p_reg;
|
|
|
|
tdm_ctrl_t *ctrl_data = drv_cfg->control_data;
|
|
|
|
uint32_t event_mask = 0;
|
|
|
|
|
|
|
|
if (nrf_tdm_event_check(p_reg, NRF_TDM_EVENT_MAXCNT)) {
|
|
|
|
nrf_tdm_event_clear(p_reg, NRF_TDM_EVENT_MAXCNT);
|
|
|
|
}
|
|
|
|
if (nrf_tdm_event_check(p_reg, NRF_TDM_EVENT_TXPTRUPD)) {
|
|
|
|
nrf_tdm_event_clear(p_reg, NRF_TDM_EVENT_TXPTRUPD);
|
|
|
|
event_mask |= NRFY_EVENT_TO_INT_BITMASK(NRF_TDM_EVENT_TXPTRUPD);
|
|
|
|
ctrl_data->tx_ready = true;
|
|
|
|
if (ctrl_data->use_tx && ctrl_data->buffers_needed) {
|
|
|
|
ctrl_data->buffers_reused = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (nrf_tdm_event_check(p_reg, NRF_TDM_EVENT_RXPTRUPD)) {
|
|
|
|
nrf_tdm_event_clear(p_reg, NRF_TDM_EVENT_RXPTRUPD);
|
|
|
|
event_mask |= NRFY_EVENT_TO_INT_BITMASK(NRF_TDM_EVENT_RXPTRUPD);
|
|
|
|
ctrl_data->rx_ready = true;
|
|
|
|
if (ctrl_data->use_rx && ctrl_data->buffers_needed) {
|
|
|
|
ctrl_data->buffers_reused = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (nrf_tdm_event_check(p_reg, NRF_TDM_EVENT_STOPPED)) {
|
|
|
|
nrf_tdm_event_clear(p_reg, NRF_TDM_EVENT_STOPPED);
|
|
|
|
event_mask |= NRFY_EVENT_TO_INT_BITMASK(NRF_TDM_EVENT_STOPPED);
|
|
|
|
nrf_tdm_int_disable(p_reg, NRF_TDM_INT_STOPPED_MASK_MASK);
|
|
|
|
nrf_tdm_disable(p_reg);
|
|
|
|
/* When stopped, release all buffers, including these scheduled for
|
|
|
|
* the next part of the transfer, and signal that the transfer has
|
|
|
|
* finished.
|
|
|
|
*/
|
|
|
|
ctrl_data->handler(&ctrl_data->current_buffers, 0);
|
|
|
|
ctrl_data->handler(&ctrl_data->next_buffers, NRFX_TDM_STATUS_TRANSFER_STOPPED);
|
|
|
|
} else {
|
|
|
|
/* Check if the requested transfer has been completed:
|
|
|
|
* - full-duplex mode
|
|
|
|
*/
|
|
|
|
if ((ctrl_data->use_tx && ctrl_data->use_rx && ctrl_data->tx_ready &&
|
|
|
|
ctrl_data->rx_ready) ||
|
|
|
|
/* - TX only mode */
|
|
|
|
(!ctrl_data->use_rx && ctrl_data->tx_ready) ||
|
|
|
|
/* - RX only mode */
|
|
|
|
(!ctrl_data->use_tx && ctrl_data->rx_ready)) {
|
|
|
|
ctrl_data->tx_ready = false;
|
|
|
|
ctrl_data->rx_ready = false;
|
|
|
|
|
|
|
|
/* If the application did not supply the buffers for the next
|
|
|
|
* part of the transfer until this moment, the current buffers
|
|
|
|
* cannot be released, since the TDM peripheral already started
|
|
|
|
* using them. Signal this situation to the application by
|
|
|
|
* passing NULL instead of the structure with released buffers.
|
|
|
|
*/
|
|
|
|
if (ctrl_data->buffers_reused) {
|
|
|
|
ctrl_data->buffers_reused = false;
|
|
|
|
/* This will most likely be set at this point. However, there is
|
|
|
|
* a small time window between TXPTRUPD and RXPTRUPD events,
|
|
|
|
* and it is theoretically possible that next buffers will be
|
|
|
|
* set in this window, so to be sure this flag is set to true,
|
|
|
|
* set it explicitly.
|
|
|
|
*/
|
|
|
|
ctrl_data->buffers_needed = true;
|
|
|
|
ctrl_data->handler(NULL, NRFX_TDM_STATUS_NEXT_BUFFERS_NEEDED);
|
|
|
|
} else {
|
|
|
|
/* Buffers that have been used by the TDM peripheral (current)
|
|
|
|
* are now released and will be returned to the application,
|
|
|
|
* and the ones scheduled to be used as next become the current
|
|
|
|
* ones.
|
|
|
|
*/
|
|
|
|
tdm_buffers_t released_buffers = ctrl_data->current_buffers;
|
|
|
|
|
|
|
|
ctrl_data->current_buffers = ctrl_data->next_buffers;
|
|
|
|
ctrl_data->next_buffers.p_rx_buffer = NULL;
|
|
|
|
ctrl_data->next_buffers.p_tx_buffer = NULL;
|
|
|
|
ctrl_data->buffers_needed = true;
|
|
|
|
ctrl_data->handler(&released_buffers,
|
|
|
|
NRFX_TDM_STATUS_NEXT_BUFFERS_NEEDED);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static uint32_t div_calculate(uint32_t src_freq, uint32_t requested_clk_freq)
|
|
|
|
{
|
|
|
|
enum {
|
|
|
|
MCKCONST = 1048576
|
|
|
|
};
|
|
|
|
/* As specified in the PS:
|
|
|
|
*
|
|
|
|
* DIV = 4096 * floor(f_MCK * 1048576 /
|
|
|
|
* (f_source + f_MCK / 2))
|
|
|
|
* f_actual = f_source /
|
|
|
|
* floor(1048576 * 4096 / DIV)
|
|
|
|
*/
|
|
|
|
|
|
|
|
uint32_t ck_div = (uint32_t)(((uint64_t)requested_clk_freq * MCKCONST) /
|
|
|
|
(src_freq + requested_clk_freq / 2));
|
|
|
|
return (ck_div * 4096);
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool get_next_tx_buffer(struct tdm_drv_data *drv_data, tdm_buffers_t *buffers)
|
|
|
|
{
|
|
|
|
struct tdm_buf buf;
|
|
|
|
int ret = k_msgq_get(&drv_data->tx_queue, &buf, K_NO_WAIT);
|
|
|
|
|
|
|
|
if (ret != 0) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
buffers->p_tx_buffer = buf.dmm_buf;
|
|
|
|
buffers->p_tx_mem_slab = buf.mem_block;
|
|
|
|
buffers->buffer_size = buf.size / sizeof(uint32_t);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool get_next_rx_buffer(struct tdm_drv_data *drv_data, tdm_buffers_t *buffers)
|
|
|
|
{
|
|
|
|
const struct tdm_drv_cfg *drv_cfg = drv_data->drv_cfg;
|
|
|
|
int ret = k_mem_slab_alloc(drv_data->rx.cfg.mem_slab, &buffers->p_rx_mem_slab, K_NO_WAIT);
|
|
|
|
|
|
|
|
if (ret < 0) {
|
|
|
|
LOG_ERR("Failed to allocate next RX buffer: %d", ret);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
ret = dmm_buffer_in_prepare(drv_cfg->mem_reg, buffers->p_rx_mem_slab,
|
|
|
|
buffers->buffer_size * sizeof(uint32_t),
|
|
|
|
(void **)&buffers->p_rx_buffer);
|
|
|
|
if (ret < 0) {
|
|
|
|
LOG_ERR("Failed to prepare buffer: %d", ret);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void free_tx_buffer(struct tdm_drv_data *drv_data, struct tdm_buf *buf)
|
|
|
|
{
|
|
|
|
const struct tdm_drv_cfg *drv_cfg = drv_data->drv_cfg;
|
|
|
|
|
|
|
|
(void)dmm_buffer_out_release(drv_cfg->mem_reg, buf->dmm_buf);
|
|
|
|
k_mem_slab_free(drv_data->tx.cfg.mem_slab, buf->mem_block);
|
|
|
|
LOG_DBG("Freed TX %p", buf->mem_block);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void free_rx_buffer(struct tdm_drv_data *drv_data, struct tdm_buf *buf)
|
|
|
|
{
|
|
|
|
const struct tdm_drv_cfg *drv_cfg = drv_data->drv_cfg;
|
|
|
|
|
|
|
|
(void)dmm_buffer_in_release(drv_cfg->mem_reg, buf->mem_block, buf->size, buf->dmm_buf);
|
|
|
|
k_mem_slab_free(drv_data->rx.cfg.mem_slab, buf->mem_block);
|
|
|
|
LOG_DBG("Freed RX %p", buf->mem_block);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void tdm_start(struct tdm_drv_data *drv_data, tdm_buffers_t const *p_initial_buffers)
|
|
|
|
{
|
|
|
|
NRF_TDM_Type *p_reg = drv_data->drv_cfg->p_reg;
|
|
|
|
tdm_ctrl_t *ctrl_data = drv_data->drv_cfg->control_data;
|
|
|
|
nrf_tdm_rxtxen_t dir = NRF_TDM_RXTXEN_DUPLEX;
|
|
|
|
uint32_t rxtx_mask = NRF_TDM_INT_TXPTRUPD_MASK_MASK | NRF_TDM_INT_RXPTRUPD_MASK_MASK;
|
|
|
|
|
|
|
|
__ASSERT_NO_MSG(p_initial_buffers->p_rx_buffer != NULL ||
|
|
|
|
p_initial_buffers->p_tx_buffer != NULL);
|
|
|
|
ctrl_data->use_rx = (p_initial_buffers->p_rx_buffer != NULL);
|
|
|
|
ctrl_data->use_tx = (p_initial_buffers->p_tx_buffer != NULL);
|
|
|
|
ctrl_data->rx_ready = false;
|
|
|
|
ctrl_data->tx_ready = false;
|
|
|
|
ctrl_data->buffers_needed = false;
|
|
|
|
ctrl_data->buffers_reused = false;
|
|
|
|
|
|
|
|
ctrl_data->next_buffers = *p_initial_buffers;
|
|
|
|
ctrl_data->current_buffers.p_rx_buffer = NULL;
|
|
|
|
ctrl_data->current_buffers.p_tx_buffer = NULL;
|
|
|
|
nrf_tdm_enable(p_reg);
|
|
|
|
|
|
|
|
nrf_tdm_event_clear(p_reg, NRF_TDM_EVENT_RXPTRUPD);
|
|
|
|
nrf_tdm_event_clear(p_reg, NRF_TDM_EVENT_TXPTRUPD);
|
|
|
|
|
|
|
|
if (p_initial_buffers->p_tx_buffer == NULL) {
|
|
|
|
dir = NRF_TDM_RXTXEN_RX;
|
|
|
|
rxtx_mask = NRF_TDM_INT_RXPTRUPD_MASK_MASK;
|
|
|
|
}
|
|
|
|
if (p_initial_buffers->p_rx_buffer == NULL) {
|
|
|
|
dir = NRF_TDM_RXTXEN_TX;
|
|
|
|
rxtx_mask = NRF_TDM_INT_TXPTRUPD_MASK_MASK;
|
|
|
|
}
|
|
|
|
|
|
|
|
nrf_tdm_int_enable(p_reg, rxtx_mask | NRF_TDM_INT_STOPPED_MASK_MASK);
|
|
|
|
nrf_tdm_tx_count_set(p_reg, p_initial_buffers->buffer_size);
|
|
|
|
nrf_tdm_tx_buffer_set(p_reg, p_initial_buffers->p_tx_buffer);
|
|
|
|
nrf_tdm_rx_count_set(p_reg, p_initial_buffers->buffer_size);
|
|
|
|
nrf_tdm_rx_buffer_set(p_reg, p_initial_buffers->p_rx_buffer);
|
|
|
|
nrf_tdm_transfer_direction_set(p_reg, dir);
|
|
|
|
nrf_tdm_task_trigger(p_reg, NRF_TDM_TASK_START);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void tdm_stop(NRF_TDM_Type *p_reg)
|
|
|
|
{
|
|
|
|
nrf_tdm_int_disable(p_reg, NRF_TDM_INT_RXPTRUPD_MASK_MASK | NRF_TDM_INT_TXPTRUPD_MASK_MASK);
|
|
|
|
|
|
|
|
nrf_tdm_task_trigger(p_reg, NRF_TDM_TASK_STOP);
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool next_buffers_set(struct tdm_drv_data *drv_data, tdm_buffers_t const *p_buffers)
|
|
|
|
{
|
|
|
|
NRF_TDM_Type *p_reg = drv_data->drv_cfg->p_reg;
|
|
|
|
tdm_ctrl_t *ctrl_data = drv_data->drv_cfg->control_data;
|
|
|
|
|
|
|
|
__ASSERT_NO_MSG(p_buffers->p_rx_buffer != NULL || p_buffers->p_tx_buffer != NULL);
|
|
|
|
|
|
|
|
if (!ctrl_data->buffers_needed) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
nrf_tdm_tx_count_set(p_reg, p_buffers->buffer_size);
|
|
|
|
nrf_tdm_rx_count_set(p_reg, p_buffers->buffer_size);
|
|
|
|
nrf_tdm_rx_buffer_set(p_reg, p_buffers->p_rx_buffer);
|
|
|
|
nrf_tdm_tx_buffer_set(p_reg, p_buffers->p_tx_buffer);
|
|
|
|
|
|
|
|
ctrl_data->next_buffers = *p_buffers;
|
|
|
|
ctrl_data->buffers_needed = false;
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool supply_next_buffers(struct tdm_drv_data *drv_data, tdm_buffers_t *next)
|
|
|
|
{
|
|
|
|
const struct tdm_drv_cfg *drv_cfg = drv_data->drv_cfg;
|
|
|
|
|
|
|
|
if (drv_data->active_dir != I2S_DIR_TX) { /* -> RX active */
|
|
|
|
if (!get_next_rx_buffer(drv_data, next)) {
|
|
|
|
drv_data->state = I2S_STATE_ERROR;
|
|
|
|
tdm_stop(drv_cfg->p_reg);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
/* Set buffer size if there is no TX buffer (which effectively
|
|
|
|
* controls how many bytes will be received).
|
|
|
|
*/
|
|
|
|
if (drv_data->active_dir == I2S_DIR_RX) {
|
|
|
|
next->buffer_size = drv_data->rx.cfg.block_size / sizeof(uint32_t);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
drv_data->last_tx_buffer = next->p_tx_buffer;
|
|
|
|
drv_data->last_tx_mem_slab = next->p_tx_mem_slab;
|
|
|
|
|
|
|
|
LOG_DBG("Next buffers: %p/%p", next->p_tx_buffer, next->p_rx_buffer);
|
|
|
|
return next_buffers_set(drv_data, next);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void purge_queue(const struct device *dev, enum i2s_dir dir)
|
|
|
|
{
|
|
|
|
struct tdm_drv_data *drv_data = dev->data;
|
|
|
|
struct tdm_buf buf;
|
|
|
|
|
|
|
|
if (dir == I2S_DIR_TX || dir == I2S_DIR_BOTH) {
|
|
|
|
while (k_msgq_get(&drv_data->tx_queue, &buf, K_NO_WAIT) == 0) {
|
|
|
|
free_tx_buffer(drv_data, &buf);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (dir == I2S_DIR_RX || dir == I2S_DIR_BOTH) {
|
|
|
|
while (k_msgq_get(&drv_data->rx_queue, &buf, K_NO_WAIT) == 0) {
|
|
|
|
free_rx_buffer(drv_data, &buf);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void tdm_uninit(struct tdm_drv_data *drv_data)
|
|
|
|
{
|
|
|
|
NRF_TDM_Type *p_reg = drv_data->drv_cfg->p_reg;
|
|
|
|
|
|
|
|
tdm_stop(p_reg);
|
|
|
|
NRFX_IRQ_DISABLE(nrfx_get_irq_number(p_reg));
|
|
|
|
}
|
|
|
|
|
|
|
|
static int tdm_nrf_configure(const struct device *dev, enum i2s_dir dir,
|
|
|
|
const struct i2s_config *tdm_cfg)
|
|
|
|
{
|
|
|
|
nrf_tdm_config_t nrfx_cfg;
|
|
|
|
struct tdm_drv_data *drv_data = dev->data;
|
|
|
|
const struct tdm_drv_cfg *drv_cfg = dev->config;
|
|
|
|
uint32_t chan_mask = 0;
|
|
|
|
uint8_t extra_channels = 0;
|
|
|
|
uint8_t max_num_of_channels = NRFX_TDM_NUM_OF_CHANNELS;
|
|
|
|
|
|
|
|
if (drv_data->state != I2S_STATE_READY) {
|
|
|
|
LOG_ERR("Cannot configure in state: %d", drv_data->state);
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tdm_cfg->frame_clk_freq == 0) { /* -> reset state */
|
|
|
|
purge_queue(dev, dir);
|
|
|
|
if (dir == I2S_DIR_TX || dir == I2S_DIR_BOTH) {
|
|
|
|
drv_data->tx_configured = false;
|
|
|
|
memset(&drv_data->tx, 0, sizeof(drv_data->tx));
|
|
|
|
}
|
|
|
|
if (dir == I2S_DIR_RX || dir == I2S_DIR_BOTH) {
|
|
|
|
drv_data->rx_configured = false;
|
|
|
|
memset(&drv_data->rx, 0, sizeof(drv_data->rx));
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
__ASSERT_NO_MSG(tdm_cfg->mem_slab != NULL && tdm_cfg->block_size != 0);
|
|
|
|
|
|
|
|
if ((tdm_cfg->block_size % sizeof(uint32_t)) != 0) {
|
|
|
|
LOG_ERR("This device can transfer only full 32-bit words");
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (tdm_cfg->word_size) {
|
|
|
|
case 8:
|
|
|
|
nrfx_cfg.sample_width = NRF_TDM_SWIDTH_8BIT;
|
|
|
|
break;
|
|
|
|
case 16:
|
|
|
|
nrfx_cfg.sample_width = NRF_TDM_SWIDTH_16BIT;
|
|
|
|
break;
|
|
|
|
case 24:
|
|
|
|
nrfx_cfg.sample_width = NRF_TDM_SWIDTH_24BIT;
|
|
|
|
break;
|
|
|
|
case 32:
|
|
|
|
nrfx_cfg.sample_width = NRF_TDM_SWIDTH_32BIT;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
LOG_ERR("Unsupported word size: %u", tdm_cfg->word_size);
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (tdm_cfg->format & I2S_FMT_DATA_FORMAT_MASK) {
|
|
|
|
case I2S_FMT_DATA_FORMAT_I2S:
|
|
|
|
nrfx_cfg.alignment = NRF_TDM_ALIGN_LEFT;
|
|
|
|
nrfx_cfg.fsync_polarity = NRF_TDM_POLARITY_NEGEDGE;
|
|
|
|
nrfx_cfg.sck_polarity = NRF_TDM_POLARITY_POSEDGE;
|
|
|
|
nrfx_cfg.fsync_duration = NRF_TDM_FSYNC_DURATION_CHANNEL;
|
|
|
|
nrfx_cfg.channel_delay = NRF_TDM_CHANNEL_DELAY_1CK;
|
|
|
|
max_num_of_channels = 2;
|
|
|
|
break;
|
|
|
|
case I2S_FMT_DATA_FORMAT_LEFT_JUSTIFIED:
|
|
|
|
nrfx_cfg.alignment = NRF_TDM_ALIGN_LEFT;
|
|
|
|
nrfx_cfg.fsync_polarity = NRF_TDM_POLARITY_POSEDGE;
|
|
|
|
nrfx_cfg.sck_polarity = NRF_TDM_POLARITY_POSEDGE;
|
|
|
|
nrfx_cfg.fsync_duration = NRF_TDM_FSYNC_DURATION_CHANNEL;
|
|
|
|
nrfx_cfg.channel_delay = NRF_TDM_CHANNEL_DELAY_NONE;
|
|
|
|
max_num_of_channels = 2;
|
|
|
|
break;
|
|
|
|
case I2S_FMT_DATA_FORMAT_RIGHT_JUSTIFIED:
|
|
|
|
nrfx_cfg.alignment = NRF_TDM_ALIGN_RIGHT;
|
|
|
|
nrfx_cfg.fsync_polarity = NRF_TDM_POLARITY_POSEDGE;
|
|
|
|
nrfx_cfg.sck_polarity = NRF_TDM_POLARITY_POSEDGE;
|
|
|
|
nrfx_cfg.fsync_duration = NRF_TDM_FSYNC_DURATION_CHANNEL;
|
|
|
|
nrfx_cfg.channel_delay = NRF_TDM_CHANNEL_DELAY_NONE;
|
|
|
|
max_num_of_channels = 2;
|
|
|
|
break;
|
|
|
|
case I2S_FMT_DATA_FORMAT_PCM_SHORT:
|
|
|
|
nrfx_cfg.alignment = NRF_TDM_ALIGN_LEFT;
|
|
|
|
nrfx_cfg.fsync_polarity = NRF_TDM_POLARITY_NEGEDGE;
|
|
|
|
nrfx_cfg.sck_polarity = NRF_TDM_POLARITY_NEGEDGE;
|
|
|
|
nrfx_cfg.fsync_duration = NRF_TDM_FSYNC_DURATION_SCK;
|
|
|
|
nrfx_cfg.channel_delay = NRF_TDM_CHANNEL_DELAY_NONE;
|
|
|
|
break;
|
|
|
|
case I2S_FMT_DATA_FORMAT_PCM_LONG:
|
|
|
|
nrfx_cfg.alignment = NRF_TDM_ALIGN_LEFT;
|
|
|
|
nrfx_cfg.fsync_polarity = NRF_TDM_POLARITY_POSEDGE;
|
|
|
|
nrfx_cfg.sck_polarity = NRF_TDM_POLARITY_NEGEDGE;
|
|
|
|
nrfx_cfg.fsync_duration = NRF_TDM_FSYNC_DURATION_SCK;
|
|
|
|
nrfx_cfg.channel_delay = NRF_TDM_CHANNEL_DELAY_NONE;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
LOG_ERR("Unsupported data format: 0x%02x", tdm_cfg->format);
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((tdm_cfg->format & I2S_FMT_DATA_ORDER_LSB) || (tdm_cfg->format & I2S_FMT_BIT_CLK_INV) ||
|
|
|
|
(tdm_cfg->format & I2S_FMT_FRAME_CLK_INV)) {
|
|
|
|
LOG_ERR("Unsupported stream format: 0x%02x", tdm_cfg->format);
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tdm_cfg->channels == 1 && nrfx_cfg.fsync_duration == NRF_TDM_FSYNC_DURATION_CHANNEL) {
|
|
|
|
/* For I2S mono standard, two channels are to be sent.
|
|
|
|
* The unused half period of LRCK will contain zeros.
|
|
|
|
*/
|
|
|
|
extra_channels = 1;
|
|
|
|
} else if (tdm_cfg->channels > max_num_of_channels) {
|
|
|
|
LOG_ERR("Unsupported number of channels: %u", tdm_cfg->channels);
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
nrfx_cfg.num_of_channels = nrf_tdm_chan_num_get(tdm_cfg->channels + extra_channels);
|
|
|
|
chan_mask = BIT_MASK(tdm_cfg->channels);
|
|
|
|
|
|
|
|
if ((tdm_cfg->options & I2S_OPT_BIT_CLK_SLAVE) &&
|
|
|
|
(tdm_cfg->options & I2S_OPT_FRAME_CLK_SLAVE)) {
|
|
|
|
nrfx_cfg.mode = NRF_TDM_MODE_SLAVE;
|
|
|
|
} else if (!(tdm_cfg->options & I2S_OPT_BIT_CLK_SLAVE) &&
|
|
|
|
!(tdm_cfg->options & I2S_OPT_FRAME_CLK_SLAVE)) {
|
|
|
|
nrfx_cfg.mode = NRF_TDM_MODE_MASTER;
|
|
|
|
} else {
|
|
|
|
LOG_ERR("Unsupported operation mode: 0x%02x", tdm_cfg->options);
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
nrfx_cfg.mck_setup = 0;
|
|
|
|
uint32_t src_freq = (drv_cfg->mck_src == ACLK) ? ACLK_FREQUENCY : drv_cfg->pclk_frequency;
|
|
|
|
|
|
|
|
if ((FIELD_GET(TDM_PSEL_MCK_CONNECT_Msk, nrf_tdm_mck_pin_get(drv_cfg->p_reg)) ==
|
|
|
|
TDM_PSEL_MCK_CONNECT_Connected) &&
|
|
|
|
drv_cfg->mck_frequency != 0) {
|
|
|
|
nrfx_cfg.mck_setup = div_calculate(src_freq, drv_cfg->mck_frequency);
|
|
|
|
}
|
|
|
|
if (nrfx_cfg.mode == NRF_TDM_MODE_MASTER) {
|
|
|
|
uint32_t sck_freq = tdm_cfg->word_size * tdm_cfg->frame_clk_freq *
|
|
|
|
(tdm_cfg->channels + extra_channels);
|
|
|
|
|
|
|
|
src_freq = (drv_cfg->sck_src == ACLK) ? ACLK_FREQUENCY : drv_cfg->pclk_frequency;
|
|
|
|
nrfx_cfg.sck_setup = div_calculate(src_freq, sck_freq);
|
|
|
|
}
|
|
|
|
/* Unless the PCLK source is used,
|
|
|
|
* it is required to request the proper clock to be running
|
|
|
|
* before starting the transfer itself.
|
|
|
|
*/
|
|
|
|
drv_data->request_clock = (drv_cfg->sck_src != PCLK) || (drv_cfg->mck_src != PCLK);
|
|
|
|
|
|
|
|
if ((tdm_cfg->options & I2S_OPT_LOOPBACK) || (tdm_cfg->options & I2S_OPT_PINGPONG)) {
|
|
|
|
LOG_ERR("Unsupported options: 0x%02x", tdm_cfg->options);
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
if (dir == I2S_DIR_TX || dir == I2S_DIR_BOTH) {
|
|
|
|
nrfx_cfg.channels = FIELD_PREP(NRFX_TDM_TX_CHANNELS_MASK, chan_mask);
|
|
|
|
drv_data->tx.cfg = *tdm_cfg;
|
|
|
|
drv_data->tx.nrfx_cfg = nrfx_cfg;
|
|
|
|
drv_data->tx_configured = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (dir == I2S_DIR_RX || dir == I2S_DIR_BOTH) {
|
|
|
|
nrfx_cfg.channels = FIELD_PREP(NRFX_TDM_RX_CHANNELS_MASK, chan_mask);
|
|
|
|
drv_data->rx.cfg = *tdm_cfg;
|
|
|
|
drv_data->rx.nrfx_cfg = nrfx_cfg;
|
|
|
|
drv_data->rx_configured = true;
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static const struct i2s_config *tdm_nrf_config_get(const struct device *dev, enum i2s_dir dir)
|
|
|
|
{
|
|
|
|
struct tdm_drv_data *drv_data = dev->data;
|
|
|
|
|
|
|
|
if (dir == I2S_DIR_TX && drv_data->tx_configured) {
|
|
|
|
return &drv_data->tx.cfg;
|
|
|
|
}
|
|
|
|
if (dir == I2S_DIR_RX && drv_data->rx_configured) {
|
|
|
|
return &drv_data->rx.cfg;
|
|
|
|
}
|
|
|
|
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int tdm_nrf_read(const struct device *dev, void **mem_block, size_t *size)
|
|
|
|
{
|
|
|
|
struct tdm_drv_data *drv_data = dev->data;
|
|
|
|
const struct tdm_drv_cfg *drv_cfg = drv_data->drv_cfg;
|
|
|
|
struct tdm_buf buf;
|
|
|
|
int ret;
|
|
|
|
|
|
|
|
if (!drv_data->rx_configured) {
|
|
|
|
LOG_ERR("Device is not configured");
|
|
|
|
return -EIO;
|
|
|
|
}
|
|
|
|
ret = k_msgq_get(&drv_data->rx_queue, &buf,
|
|
|
|
(drv_data->state == I2S_STATE_ERROR)
|
|
|
|
? K_NO_WAIT
|
|
|
|
: SYS_TIMEOUT_MS(drv_data->rx.cfg.timeout));
|
|
|
|
if (ret == -ENOMSG) {
|
|
|
|
return -EIO;
|
|
|
|
}
|
|
|
|
|
|
|
|
LOG_DBG("Released RX %p", buf.mem_block);
|
|
|
|
|
|
|
|
if (ret == 0) {
|
|
|
|
(void)dmm_buffer_in_release(drv_cfg->mem_reg, buf.mem_block, buf.size, buf.dmm_buf);
|
|
|
|
*mem_block = buf.mem_block;
|
|
|
|
*size = buf.size;
|
|
|
|
}
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int tdm_nrf_write(const struct device *dev, void *mem_block, size_t size)
|
|
|
|
{
|
|
|
|
struct tdm_drv_data *drv_data = dev->data;
|
|
|
|
const struct tdm_drv_cfg *drv_cfg = dev->config;
|
|
|
|
struct tdm_buf buf = {.mem_block = mem_block, .size = size};
|
|
|
|
int ret;
|
|
|
|
|
|
|
|
if (!drv_data->tx_configured) {
|
|
|
|
LOG_ERR("Device is not configured");
|
|
|
|
return -EIO;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (drv_data->state != I2S_STATE_RUNNING && drv_data->state != I2S_STATE_READY) {
|
|
|
|
LOG_ERR("Cannot write in state: %d", drv_data->state);
|
|
|
|
return -EIO;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (size > drv_data->tx.cfg.block_size || size < sizeof(uint32_t)) {
|
|
|
|
LOG_ERR("This device can only write blocks up to %u bytes",
|
|
|
|
drv_data->tx.cfg.block_size);
|
|
|
|
return -EIO;
|
|
|
|
}
|
|
|
|
ret = dmm_buffer_out_prepare(drv_cfg->mem_reg, buf.mem_block, buf.size,
|
|
|
|
(void **)&buf.dmm_buf);
|
|
|
|
ret = k_msgq_put(&drv_data->tx_queue, &buf, SYS_TIMEOUT_MS(drv_data->tx.cfg.timeout));
|
|
|
|
if (ret < 0) {
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Check if interrupt wanted to get next TX buffer before current buffer
|
|
|
|
* was queued. Do not move this check before queuing because doing so
|
|
|
|
* opens the possibility for a race condition between this function and
|
|
|
|
* data_handler() that is called in interrupt context.
|
|
|
|
*/
|
|
|
|
if (drv_data->state == I2S_STATE_RUNNING && drv_data->next_tx_buffer_needed) {
|
|
|
|
tdm_buffers_t next = {0};
|
|
|
|
|
|
|
|
if (!get_next_tx_buffer(drv_data, &next)) {
|
|
|
|
/* Log error because this is definitely unexpected.
|
|
|
|
* Do not return error because the caller is no longer
|
|
|
|
* responsible for releasing the buffer.
|
|
|
|
*/
|
|
|
|
LOG_ERR("Cannot reacquire queued buffer");
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
drv_data->next_tx_buffer_needed = false;
|
|
|
|
|
|
|
|
LOG_DBG("Next TX %p", next.p_tx_buffer);
|
|
|
|
|
|
|
|
if (!supply_next_buffers(drv_data, &next)) {
|
|
|
|
LOG_ERR("Cannot supply buffer");
|
|
|
|
return -EIO;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int start_transfer(struct tdm_drv_data *drv_data)
|
|
|
|
{
|
|
|
|
tdm_buffers_t initial_buffers = {0};
|
|
|
|
int ret = 0;
|
|
|
|
|
|
|
|
if (drv_data->active_dir != I2S_DIR_RX && /* -> TX to be started */
|
|
|
|
!get_next_tx_buffer(drv_data, &initial_buffers)) {
|
|
|
|
LOG_ERR("No TX buffer available");
|
|
|
|
ret = -ENOMEM;
|
|
|
|
} else if (drv_data->active_dir != I2S_DIR_TX && /* -> RX to be started */
|
|
|
|
!get_next_rx_buffer(drv_data, &initial_buffers)) {
|
|
|
|
/* Failed to allocate next RX buffer */
|
|
|
|
ret = -ENOMEM;
|
|
|
|
} else {
|
|
|
|
/* It is necessary to set buffer size here only for I2S_DIR_RX,
|
|
|
|
* because only then the get_next_tx_buffer() call in the if
|
|
|
|
* condition above gets short-circuited.
|
|
|
|
*/
|
|
|
|
if (drv_data->active_dir == I2S_DIR_RX) {
|
|
|
|
initial_buffers.buffer_size =
|
|
|
|
drv_data->rx.cfg.block_size / sizeof(uint32_t);
|
|
|
|
}
|
|
|
|
|
|
|
|
drv_data->last_tx_buffer = initial_buffers.p_tx_buffer;
|
|
|
|
drv_data->last_tx_mem_slab = initial_buffers.p_tx_mem_slab;
|
|
|
|
|
|
|
|
tdm_start(drv_data, &initial_buffers);
|
|
|
|
}
|
|
|
|
if (ret < 0) {
|
|
|
|
tdm_uninit(drv_data);
|
|
|
|
if (drv_data->request_clock) {
|
|
|
|
(void)audio_clock_release(drv_data);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (initial_buffers.p_tx_buffer) {
|
|
|
|
struct tdm_buf buf = {.mem_block = (void *)initial_buffers.p_tx_mem_slab,
|
|
|
|
.dmm_buf = (void *)initial_buffers.p_tx_buffer,
|
|
|
|
.size = initial_buffers.buffer_size *
|
|
|
|
sizeof(uint32_t)};
|
|
|
|
free_tx_buffer(drv_data, &buf);
|
|
|
|
}
|
|
|
|
if (initial_buffers.p_rx_buffer) {
|
|
|
|
struct tdm_buf buf = {.mem_block = initial_buffers.p_rx_mem_slab,
|
|
|
|
.dmm_buf = (void *)initial_buffers.p_rx_buffer,
|
|
|
|
.size = initial_buffers.buffer_size *
|
|
|
|
sizeof(uint32_t)};
|
|
|
|
free_rx_buffer(drv_data, &buf);
|
|
|
|
}
|
|
|
|
|
|
|
|
drv_data->state = I2S_STATE_ERROR;
|
|
|
|
}
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool channels_configuration_check(uint32_t tx, uint32_t rx)
|
|
|
|
{
|
|
|
|
|
|
|
|
tx = FIELD_GET(NRFX_TDM_TX_CHANNELS_MASK, tx);
|
|
|
|
rx = FIELD_GET(NRFX_TDM_RX_CHANNELS_MASK, rx);
|
|
|
|
|
|
|
|
return (tx == rx);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void tdm_init(struct tdm_drv_data *drv_data, nrf_tdm_config_t const *p_config,
|
|
|
|
tdm_data_handler_t handler)
|
|
|
|
{
|
|
|
|
tdm_ctrl_t *ctrl_data = drv_data->drv_cfg->control_data;
|
|
|
|
NRF_TDM_Type *p_reg = drv_data->drv_cfg->p_reg;
|
|
|
|
|
|
|
|
nrf_tdm_configure(p_reg, p_config);
|
|
|
|
nrf_tdm_mck_set(p_reg, p_config->mck_setup != 0);
|
|
|
|
|
|
|
|
ctrl_data->handler = handler;
|
|
|
|
|
|
|
|
nrf_tdm_event_clear(p_reg, NRF_TDM_EVENT_RXPTRUPD);
|
|
|
|
nrf_tdm_event_clear(p_reg, NRF_TDM_EVENT_TXPTRUPD);
|
|
|
|
nrf_tdm_event_clear(p_reg, NRF_TDM_EVENT_STOPPED);
|
|
|
|
NRFX_IRQ_ENABLE(nrfx_get_irq_number(p_reg));
|
|
|
|
}
|
|
|
|
|
|
|
|
static void clock_started_callback(struct onoff_manager *mgr, struct onoff_client *cli,
|
|
|
|
uint32_t state, int res)
|
|
|
|
{
|
|
|
|
struct tdm_drv_data *drv_data = CONTAINER_OF(cli, struct tdm_drv_data, clk_cli);
|
|
|
|
|
|
|
|
/* The driver state can be set back to READY at this point if the DROP
|
|
|
|
* command was triggered before the clock has started. Do not start
|
|
|
|
* the actual transfer in such case.
|
|
|
|
*/
|
|
|
|
if (drv_data->state == I2S_STATE_READY) {
|
|
|
|
tdm_uninit(drv_data);
|
|
|
|
(void)audio_clock_release(drv_data);
|
|
|
|
} else {
|
|
|
|
(void)start_transfer(drv_data);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static int trigger_start(const struct device *dev)
|
|
|
|
{
|
|
|
|
struct tdm_drv_data *drv_data = dev->data;
|
|
|
|
const struct tdm_drv_cfg *drv_cfg = dev->config;
|
|
|
|
int ret;
|
|
|
|
const nrf_tdm_config_t *nrfx_cfg = (drv_data->active_dir == I2S_DIR_TX)
|
|
|
|
? &drv_data->tx.nrfx_cfg
|
|
|
|
: &drv_data->rx.nrfx_cfg;
|
|
|
|
|
|
|
|
tdm_init(drv_data, nrfx_cfg, drv_cfg->data_handler);
|
|
|
|
|
|
|
|
drv_data->state = I2S_STATE_RUNNING;
|
|
|
|
|
|
|
|
nrf_tdm_sck_configure(drv_cfg->p_reg,
|
|
|
|
drv_cfg->sck_src == ACLK ? NRF_TDM_SRC_ACLK : NRF_TDM_SRC_PCLK32M,
|
|
|
|
false);
|
|
|
|
|
|
|
|
nrf_tdm_mck_configure(drv_cfg->p_reg,
|
|
|
|
drv_cfg->mck_src == ACLK ? NRF_TDM_SRC_ACLK : NRF_TDM_SRC_PCLK32M,
|
|
|
|
false);
|
|
|
|
/* If it is required to use certain HF clock, request it to be running
|
|
|
|
* first. If not, start the transfer directly.
|
|
|
|
*/
|
|
|
|
if (drv_data->request_clock) {
|
|
|
|
sys_notify_init_callback(&drv_data->clk_cli.notify, clock_started_callback);
|
|
|
|
ret = audio_clock_request(drv_data);
|
|
|
|
if (ret < 0) {
|
|
|
|
tdm_uninit(drv_data);
|
|
|
|
drv_data->state = I2S_STATE_READY;
|
|
|
|
|
|
|
|
LOG_ERR("Failed to request clock: %d", ret);
|
|
|
|
return -EIO;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
ret = start_transfer(drv_data);
|
|
|
|
if (ret < 0) {
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int tdm_nrf_trigger(const struct device *dev, enum i2s_dir dir, enum i2s_trigger_cmd cmd)
|
|
|
|
{
|
|
|
|
struct tdm_drv_data *drv_data = dev->data;
|
|
|
|
const struct tdm_drv_cfg *drv_cfg = dev->config;
|
|
|
|
bool configured = false;
|
|
|
|
bool cmd_allowed;
|
|
|
|
|
|
|
|
/* This driver does not use the I2S_STATE_NOT_READY value.
|
|
|
|
* Instead, if a given stream is not configured, the respective
|
|
|
|
* flag (tx_configured or rx_configured) is cleared.
|
|
|
|
*/
|
|
|
|
|
|
|
|
if (dir == I2S_DIR_BOTH) {
|
|
|
|
configured = drv_data->tx_configured && drv_data->rx_configured;
|
|
|
|
} else if (dir == I2S_DIR_TX) {
|
|
|
|
configured = drv_data->tx_configured;
|
|
|
|
} else if (dir == I2S_DIR_RX) {
|
|
|
|
configured = drv_data->rx_configured;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!configured) {
|
|
|
|
LOG_ERR("Device is not configured");
|
|
|
|
return -EIO;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (dir == I2S_DIR_BOTH) {
|
|
|
|
if (!channels_configuration_check(drv_data->tx.nrfx_cfg.channels,
|
|
|
|
drv_data->rx.nrfx_cfg.channels)) {
|
|
|
|
LOG_ERR("TX and RX channels configurations are different");
|
|
|
|
return -EIO;
|
|
|
|
}
|
|
|
|
/* The TX and RX channel masks are to be stored in a single TDM register.
|
|
|
|
* In case of I2S_DIR_BOTH, only the rx.nrfx_cfg structure is used, so
|
|
|
|
* it must also contain the TX channel mask.
|
|
|
|
*/
|
|
|
|
uint32_t tx_rx_merged =
|
|
|
|
drv_data->tx.nrfx_cfg.channels | drv_data->rx.nrfx_cfg.channels;
|
|
|
|
drv_data->tx.nrfx_cfg.channels = tx_rx_merged;
|
|
|
|
drv_data->rx.nrfx_cfg.channels = tx_rx_merged;
|
|
|
|
if (memcmp(&drv_data->tx.nrfx_cfg, &drv_data->rx.nrfx_cfg,
|
|
|
|
sizeof(drv_data->rx.nrfx_cfg)) != 0 ||
|
|
|
|
(drv_data->tx.cfg.block_size != drv_data->rx.cfg.block_size)) {
|
|
|
|
LOG_ERR("TX and RX configurations are different");
|
|
|
|
return -EIO;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (cmd) {
|
|
|
|
case I2S_TRIGGER_START:
|
|
|
|
cmd_allowed = (drv_data->state == I2S_STATE_READY);
|
|
|
|
break;
|
|
|
|
case I2S_TRIGGER_STOP:
|
|
|
|
case I2S_TRIGGER_DRAIN:
|
|
|
|
cmd_allowed = (drv_data->state == I2S_STATE_RUNNING);
|
|
|
|
break;
|
|
|
|
case I2S_TRIGGER_DROP:
|
|
|
|
cmd_allowed = configured;
|
|
|
|
break;
|
|
|
|
case I2S_TRIGGER_PREPARE:
|
|
|
|
cmd_allowed = (drv_data->state == I2S_STATE_ERROR);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
LOG_ERR("Invalid trigger: %d", cmd);
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!cmd_allowed) {
|
|
|
|
LOG_ERR("Not allowed");
|
|
|
|
return -EIO;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* For triggers applicable to the RUNNING state (i.e. STOP, DRAIN,
|
|
|
|
* and DROP), ensure that the command is applied to the streams
|
|
|
|
* that are currently active (this device cannot e.g. stop only TX
|
|
|
|
* without stopping RX).
|
|
|
|
*/
|
|
|
|
if (drv_data->state == I2S_STATE_RUNNING && drv_data->active_dir != dir) {
|
|
|
|
LOG_ERR("Inappropriate trigger (%d/%d), active stream(s): %d", cmd, dir,
|
|
|
|
drv_data->active_dir);
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (cmd) {
|
|
|
|
case I2S_TRIGGER_START:
|
|
|
|
drv_data->stop = false;
|
|
|
|
drv_data->discard_rx = false;
|
|
|
|
drv_data->active_dir = dir;
|
|
|
|
drv_data->next_tx_buffer_needed = false;
|
|
|
|
return trigger_start(dev);
|
|
|
|
|
|
|
|
case I2S_TRIGGER_STOP:
|
|
|
|
drv_data->state = I2S_STATE_STOPPING;
|
|
|
|
drv_data->stop = true;
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
case I2S_TRIGGER_DRAIN:
|
|
|
|
drv_data->state = I2S_STATE_STOPPING;
|
|
|
|
/* If only RX is active, DRAIN is equivalent to STOP. */
|
|
|
|
drv_data->stop = (drv_data->active_dir == I2S_DIR_RX);
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
case I2S_TRIGGER_DROP:
|
|
|
|
if (drv_data->state != I2S_STATE_READY) {
|
|
|
|
drv_data->discard_rx = true;
|
|
|
|
tdm_stop(drv_cfg->p_reg);
|
|
|
|
}
|
|
|
|
purge_queue(dev, dir);
|
|
|
|
drv_data->state = I2S_STATE_READY;
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
case I2S_TRIGGER_PREPARE:
|
|
|
|
purge_queue(dev, dir);
|
|
|
|
drv_data->state = I2S_STATE_READY;
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
default:
|
|
|
|
LOG_ERR("Invalid trigger: %d", cmd);
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void data_handler(const struct device *dev, const tdm_buffers_t *released, uint32_t status)
|
|
|
|
{
|
|
|
|
struct tdm_drv_data *drv_data = dev->data;
|
|
|
|
const struct tdm_drv_cfg *drv_cfg = dev->config;
|
|
|
|
bool stop_transfer = false;
|
|
|
|
struct tdm_buf buf = {.mem_block = NULL, .dmm_buf = NULL, .size = 0};
|
|
|
|
|
|
|
|
if (released != NULL) {
|
|
|
|
buf.size = released->buffer_size * sizeof(uint32_t);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (status & NRFX_TDM_STATUS_TRANSFER_STOPPED) {
|
|
|
|
if (drv_data->state == I2S_STATE_STOPPING) {
|
|
|
|
drv_data->state = I2S_STATE_READY;
|
|
|
|
}
|
|
|
|
if (drv_data->last_tx_buffer) {
|
|
|
|
/* Usually, these pointers are equal, i.e. the last TX
|
|
|
|
* buffer that were to be transferred is released by the
|
|
|
|
* driver after it stops. The last TX buffer pointer is
|
|
|
|
* then set to NULL here so that the buffer can be freed
|
|
|
|
* below, just as any other TX buffer released by the
|
|
|
|
* driver. However, it may happen that the buffer is not
|
|
|
|
* released this way, for example, when the transfer
|
|
|
|
* ends with an error because an RX buffer allocation
|
|
|
|
* fails. In such case, the last TX buffer needs to be
|
|
|
|
* freed here.
|
|
|
|
*/
|
|
|
|
|
|
|
|
if ((released != NULL) != 0 &&
|
|
|
|
(drv_data->last_tx_buffer != released->p_tx_buffer)) {
|
|
|
|
buf.dmm_buf = (void *)drv_data->last_tx_buffer;
|
|
|
|
buf.mem_block = (void *)drv_data->last_tx_mem_slab;
|
|
|
|
free_tx_buffer(drv_data, &buf);
|
|
|
|
}
|
|
|
|
drv_data->last_tx_buffer = NULL;
|
|
|
|
}
|
|
|
|
tdm_uninit(drv_data);
|
|
|
|
if (drv_data->request_clock) {
|
|
|
|
(void)audio_clock_release(drv_data);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (released == NULL) {
|
|
|
|
/* This means that buffers for the next part of the transfer
|
|
|
|
* were not supplied and the previous ones cannot be released
|
|
|
|
* yet, as pointers to them were latched in the TDM registers.
|
|
|
|
* It is not an error when the transfer is to be stopped (those
|
|
|
|
* buffers will be released after the transfer actually stops).
|
|
|
|
*/
|
|
|
|
if (drv_data->state != I2S_STATE_STOPPING) {
|
|
|
|
drv_data->state = I2S_STATE_ERROR;
|
|
|
|
}
|
|
|
|
tdm_stop(drv_cfg->p_reg);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (released->p_rx_buffer) {
|
|
|
|
buf.mem_block = (void *)released->p_rx_mem_slab;
|
|
|
|
buf.dmm_buf = (void *)released->p_rx_buffer;
|
|
|
|
if (drv_data->discard_rx) {
|
|
|
|
free_rx_buffer(drv_data, &buf);
|
|
|
|
} else {
|
|
|
|
int ret = k_msgq_put(&drv_data->rx_queue, &buf, K_NO_WAIT);
|
|
|
|
|
|
|
|
if (ret < 0) {
|
|
|
|
LOG_ERR("No room in RX queue");
|
|
|
|
drv_data->state = I2S_STATE_ERROR;
|
|
|
|
stop_transfer = true;
|
|
|
|
|
|
|
|
free_rx_buffer(drv_data, &buf);
|
|
|
|
} else {
|
|
|
|
|
|
|
|
/* If the TX direction is not active and
|
|
|
|
* the transfer should be stopped after
|
|
|
|
* the current block, stop the reception.
|
|
|
|
*/
|
|
|
|
if (drv_data->active_dir == I2S_DIR_RX && drv_data->stop) {
|
|
|
|
drv_data->discard_rx = true;
|
|
|
|
stop_transfer = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (released->p_tx_buffer) {
|
|
|
|
buf.mem_block = (void *)released->p_tx_mem_slab;
|
|
|
|
buf.dmm_buf = (void *)released->p_tx_buffer;
|
|
|
|
/* If the last buffer that was to be transferred has just been
|
|
|
|
* released, it is time to stop the transfer.
|
|
|
|
*/
|
|
|
|
if (released->p_tx_buffer == drv_data->last_tx_buffer) {
|
|
|
|
drv_data->discard_rx = true;
|
|
|
|
stop_transfer = true;
|
|
|
|
} else {
|
|
|
|
free_tx_buffer(drv_data, &buf);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (stop_transfer) {
|
|
|
|
tdm_stop(drv_cfg->p_reg);
|
|
|
|
} else if (status & NRFX_TDM_STATUS_NEXT_BUFFERS_NEEDED) {
|
|
|
|
tdm_buffers_t next = {0};
|
|
|
|
|
|
|
|
if (drv_data->active_dir != I2S_DIR_RX) { /* -> TX active */
|
|
|
|
if (drv_data->stop) {
|
|
|
|
/* If the stream is to be stopped, don't get
|
|
|
|
* the next TX buffer from the queue, instead
|
|
|
|
* supply the one used last time (it won't be
|
|
|
|
* transferred, the stream will stop right
|
|
|
|
* before this buffer would be started again).
|
|
|
|
*/
|
|
|
|
next.p_tx_buffer = drv_data->last_tx_buffer;
|
|
|
|
next.p_tx_mem_slab = drv_data->last_tx_mem_slab;
|
|
|
|
next.buffer_size = 1;
|
|
|
|
} else if (get_next_tx_buffer(drv_data, &next)) {
|
|
|
|
/* Next TX buffer successfully retrieved from
|
|
|
|
* the queue, nothing more to do here.
|
|
|
|
*/
|
|
|
|
} else if (drv_data->state == I2S_STATE_STOPPING) {
|
|
|
|
/* If there are no more TX blocks queued and
|
|
|
|
* the current state is STOPPING (so the DRAIN
|
|
|
|
* command was triggered) it is time to finish
|
|
|
|
* the transfer.
|
|
|
|
*/
|
|
|
|
drv_data->stop = true;
|
|
|
|
/* Supply the same buffer as last time; it will
|
|
|
|
* not be transferred anyway, as the transfer
|
|
|
|
* will be stopped earlier.
|
|
|
|
*/
|
|
|
|
next.p_tx_buffer = drv_data->last_tx_buffer;
|
|
|
|
next.p_tx_mem_slab = drv_data->last_tx_mem_slab;
|
|
|
|
next.buffer_size = 1;
|
|
|
|
} else {
|
|
|
|
/* Next TX buffer cannot be supplied now.
|
|
|
|
* Defer it to when the user writes more data.
|
|
|
|
*/
|
|
|
|
drv_data->next_tx_buffer_needed = true;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
(void)supply_next_buffers(drv_data, &next);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void clock_manager_init(const struct device *dev)
|
|
|
|
{
|
|
|
|
#if CONFIG_CLOCK_CONTROL_NRF && NRF_CLOCK_HAS_HFCLKAUDIO
|
|
|
|
clock_control_subsys_t subsys;
|
|
|
|
struct tdm_drv_data *drv_data = dev->data;
|
|
|
|
|
|
|
|
subsys = CLOCK_CONTROL_NRF_SUBSYS_HFAUDIO;
|
|
|
|
drv_data->clk_mgr = z_nrf_clock_control_get_onoff(subsys);
|
|
|
|
__ASSERT_NO_MSG(drv_data->clk_mgr != NULL);
|
|
|
|
#else
|
|
|
|
(void)dev;
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
|
|
|
static int data_init(const struct device *dev)
|
|
|
|
{
|
|
|
|
struct tdm_drv_data *drv_data = dev->data;
|
|
|
|
const struct tdm_drv_cfg *drv_cfg = dev->config;
|
|
|
|
|
|
|
|
drv_data->state = I2S_STATE_READY;
|
|
|
|
int err = pinctrl_apply_state(drv_cfg->pcfg, PINCTRL_STATE_DEFAULT);
|
|
|
|
|
|
|
|
if (err < 0) {
|
|
|
|
return err;
|
|
|
|
}
|
|
|
|
drv_data->drv_cfg = drv_cfg;
|
|
|
|
return err;
|
|
|
|
}
|
|
|
|
|
|
|
|
static DEVICE_API(i2s, tdm_nrf_drv_api) = {
|
|
|
|
.configure = tdm_nrf_configure,
|
|
|
|
.config_get = tdm_nrf_config_get,
|
|
|
|
.read = tdm_nrf_read,
|
|
|
|
.write = tdm_nrf_write,
|
|
|
|
.trigger = tdm_nrf_trigger,
|
|
|
|
};
|
|
|
|
|
|
|
|
#define TDM(idx) DT_NODELABEL(tdm##idx)
|
|
|
|
#define TDM_SCK_CLK_SRC(idx) DT_STRING_TOKEN(TDM(idx), sck_clock_source)
|
|
|
|
#define TDM_MCK_CLK_SRC(idx) DT_STRING_TOKEN(TDM(idx), mck_clock_source)
|
|
|
|
#define PCLK_NODE(idx) DT_CLOCKS_CTLR(TDM(idx))
|
|
|
|
|
|
|
|
#define TDM_NRF_DEVICE(idx) \
|
|
|
|
static tdm_ctrl_t tdm##idx##data; \
|
|
|
|
static struct tdm_buf tx_msgs##idx[CONFIG_I2S_NRF_TDM_TX_BLOCK_COUNT]; \
|
|
|
|
static struct tdm_buf rx_msgs##idx[CONFIG_I2S_NRF_TDM_RX_BLOCK_COUNT]; \
|
|
|
|
static void tdm_##idx##_irq_handler(const struct device *dev) \
|
|
|
|
{ \
|
|
|
|
tdm_irq_handler(dev); \
|
|
|
|
} \
|
|
|
|
static void tdm_##idx##data_handler(tdm_buffers_t const *p_released, uint32_t status) \
|
|
|
|
{ \
|
|
|
|
data_handler(DEVICE_DT_GET(TDM(idx)), p_released, status); \
|
|
|
|
} \
|
|
|
|
PINCTRL_DT_DEFINE(TDM(idx)); \
|
|
|
|
static const struct tdm_drv_cfg tdm_nrf_cfg##idx = { \
|
|
|
|
.data_handler = tdm_##idx##data_handler, \
|
|
|
|
.pcfg = PINCTRL_DT_DEV_CONFIG_GET(TDM(idx)), \
|
|
|
|
.sck_src = TDM_SCK_CLK_SRC(idx), \
|
|
|
|
.mck_src = TDM_MCK_CLK_SRC(idx), \
|
|
|
|
.mck_frequency = DT_PROP_OR(TDM(idx), mck_frequency, 0), \
|
|
|
|
.pclk_frequency = DT_PROP(PCLK_NODE(idx), clock_frequency), \
|
|
|
|
.p_reg = NRF_TDM##idx, \
|
|
|
|
.control_data = &tdm##idx##data, \
|
|
|
|
.mem_reg = DMM_DEV_TO_REG(TDM(idx)), \
|
|
|
|
}; \
|
|
|
|
static struct tdm_drv_data tdm_nrf_data##idx; \
|
|
|
|
static int tdm_nrf_init##idx(const struct device *dev) \
|
|
|
|
{ \
|
|
|
|
IRQ_CONNECT(DT_IRQN(TDM(idx)), DT_IRQ(TDM(idx), priority), \
|
|
|
|
tdm_##idx##_irq_handler, DEVICE_DT_GET(TDM(idx)), 0); \
|
|
|
|
\
|
|
|
|
int err = data_init(dev); \
|
|
|
|
if (err < 0) { \
|
|
|
|
return err; \
|
|
|
|
} \
|
|
|
|
k_msgq_init(&tdm_nrf_data##idx.tx_queue, (char *)tx_msgs##idx, \
|
|
|
|
sizeof(struct tdm_buf), ARRAY_SIZE(tx_msgs##idx)); \
|
|
|
|
k_msgq_init(&tdm_nrf_data##idx.rx_queue, (char *)rx_msgs##idx, \
|
|
|
|
sizeof(struct tdm_buf), ARRAY_SIZE(rx_msgs##idx)); \
|
|
|
|
clock_manager_init(dev); \
|
|
|
|
return 0; \
|
|
|
|
} \
|
|
|
|
BUILD_ASSERT((TDM_SCK_CLK_SRC(idx) != ACLK && TDM_MCK_CLK_SRC(idx) != ACLK) || \
|
|
|
|
DT_NODE_HAS_STATUS_OKAY(NODE_ACLK), \
|
|
|
|
"Clock source ACLK requires the audiopll node."); \
|
|
|
|
DEVICE_DT_DEFINE(TDM(idx), tdm_nrf_init##idx, NULL, &tdm_nrf_data##idx, &tdm_nrf_cfg##idx, \
|
|
|
|
POST_KERNEL, CONFIG_I2S_INIT_PRIORITY, &tdm_nrf_drv_api);
|
|
|
|
|
|
|
|
/* Execute macro f(x) for all instances. */
|
|
|
|
#define TDM_FOR_EACH_INSTANCE(f, sep, off_code, ...) \
|
|
|
|
NRFX_FOREACH_PRESENT(TDM, f, sep, off_code, __VA_ARGS__)
|
|
|
|
|
|
|
|
#define COND_TDM_NRF_DEVICE(unused, prefix, i, _) \
|
|
|
|
IF_ENABLED(CONFIG_HAS_HW_NRF_TDM##prefix##i, (TDM_NRF_DEVICE(prefix##i);))
|
|
|
|
|
|
|
|
TDM_FOR_EACH_INSTANCE(COND_TDM_NRF_DEVICE, (), ())
|