Currently there is a mismatch between the naming of the hardware and the drivers targetting the hardware. nrf2_ is used instead of the actual bindings names, like nrf2_audiopll instead of nrfs_audiopll. This makes it hard to map drivers to the hardware they are targetting. There is historical reason for some of this, namely the same binding name was used for different hardware, which is why nrf2_ was used on newer platforms. This is no longer the case though, so drivers and configs can be named according to the hardware without conflict. Signed-off-by: Bjarki Arge Andreasen <bjarki.andreasen@nordicsemi.no>
1205 lines
39 KiB
C
1205 lines
39 KiB
C
/*
|
|
* 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);
|
|
#elif DT_NODE_HAS_STATUS_OKAY(NODE_ACLK) && CONFIG_CLOCK_CONTROL_NRFS_AUDIOPLL
|
|
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);
|
|
#elif DT_NODE_HAS_STATUS_OKAY(NODE_ACLK) && CONFIG_CLOCK_CONTROL_NRFS_AUDIOPLL
|
|
(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, (), ())
|