852 lines
32 KiB
C
852 lines
32 KiB
C
/*
|
|
* SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
#if CONFIG_DAC_ENABLE_DEBUG_LOG
|
|
// The local log level must be defined before including esp_log.h
|
|
// Set the maximum log level for this source file
|
|
#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG
|
|
#endif
|
|
|
|
#include <assert.h>
|
|
#include <stdatomic.h>
|
|
#include <string.h>
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/queue.h"
|
|
#include "freertos/semphr.h"
|
|
#include "freertos/idf_additions.h"
|
|
#include "sdkconfig.h"
|
|
|
|
#include "soc/soc_caps.h"
|
|
#include "driver/dac_continuous.h"
|
|
#include "esp_private/gdma_link.h"
|
|
#include "esp_check.h"
|
|
|
|
#include "dac_priv_common.h"
|
|
#include "dac_priv_dma.h"
|
|
|
|
#if CONFIG_PM_ENABLE
|
|
#include "esp_pm.h"
|
|
#endif
|
|
|
|
#define DAC_DMA_MAX_BUF_SIZE 4092 // Max DMA buffer size is 4095 but better to align with 4 bytes, so set 4092 here
|
|
|
|
#if CONFIG_DAC_ISR_IRAM_SAFE || CONFIG_DAC_CTRL_FUNC_IN_IRAM
|
|
#define DAC_MEM_ALLOC_CAPS (MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)
|
|
#else
|
|
#define DAC_MEM_ALLOC_CAPS MALLOC_CAP_DEFAULT
|
|
#endif
|
|
|
|
#if CONFIG_DAC_ISR_IRAM_SAFE
|
|
#define DAC_INTR_ALLOC_FLAGS (ESP_INTR_FLAG_LOWMED | ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_INTRDISABLED | ESP_INTR_FLAG_SHARED)
|
|
#else
|
|
#define DAC_INTR_ALLOC_FLAGS (ESP_INTR_FLAG_LOWMED | ESP_INTR_FLAG_INTRDISABLED | ESP_INTR_FLAG_SHARED)
|
|
#endif
|
|
|
|
#define DAC_DMA_ALLOC_CAPS (MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA)
|
|
|
|
struct dac_continuous_s {
|
|
dac_continuous_config_t cfg;
|
|
intr_handle_t intr_handle; /* Interrupt handle */
|
|
#if CONFIG_PM_ENABLE
|
|
esp_pm_lock_handle_t pm_lock;
|
|
#endif
|
|
|
|
dac_event_callbacks_t cbs; /* Interrupt callbacks */
|
|
void *user_data;
|
|
|
|
uint32_t cur_index; /* Index of the DMA descriptor that is currently being used by DMA. */
|
|
uint32_t used_desc_num; /* Number of used DMA descriptors. Determines cur_index wrap-around. */
|
|
|
|
QueueHandle_t free_desc_queue; /* Queue of free DMA descriptors indices. Only used in sync writing mode. */
|
|
SemaphoreHandle_t mutex; /* Serializes the public writing APIs (sync / cyclic) */
|
|
#if SOC_IS(ESP32)
|
|
portMUX_TYPE dma_lock; /* Serializes the link/restart decision between the sync writing task and the ISR */
|
|
bool dma_running; /* Whether the DMA is running (guarded by 'dma_lock'). Only used in sync writing mode. */
|
|
#endif
|
|
|
|
gdma_link_list_handle_t link; /* DMA descriptor link list */
|
|
uint8_t *bufs[]; /* Array of DMA buffers pointers */
|
|
};
|
|
|
|
typedef enum {
|
|
DAC_CONT_FSM_IDLE,
|
|
DAC_CONT_FSM_REGISTERED,
|
|
DAC_CONT_FSM_ENABLED, // Ready and DMA is NOT running
|
|
DAC_CONT_FSM_ASYNC,
|
|
DAC_CONT_FSM_CYCLIC,
|
|
DAC_CONT_FSM_SYNC, // Sync writing mode. DMA may or may not be running.
|
|
DAC_CONT_FSM_SYNC_WAIT, // Transition state for sync writing mode
|
|
DAC_CONT_FSM_WAIT, // Transition state
|
|
} dac_continuous_fsm_t;
|
|
|
|
static _Atomic dac_continuous_fsm_t s_dac_cont_fsm = DAC_CONT_FSM_IDLE;
|
|
|
|
static const char *TAG = "dac_continuous";
|
|
|
|
static esp_err_t s_dac_continuous_stop_sync(dac_continuous_handle_t handle);
|
|
|
|
static void s_dac_free_dma_desc(dac_continuous_handle_t handle)
|
|
{
|
|
if (handle->link != NULL) {
|
|
gdma_del_link_list(handle->link);
|
|
handle->link = NULL;
|
|
}
|
|
|
|
for (uint32_t i = 0; i < handle->cfg.desc_num; i++) {
|
|
free(handle->bufs[i]);
|
|
handle->bufs[i] = NULL;
|
|
}
|
|
}
|
|
|
|
static esp_err_t s_dac_alloc_dma_desc(dac_continuous_handle_t handle)
|
|
{
|
|
esp_err_t ret = ESP_OK;
|
|
|
|
const uint32_t desc_num = handle->cfg.desc_num;
|
|
const size_t buf_size = handle->cfg.buf_size;
|
|
|
|
/* Allocate DMA buffers */
|
|
for (uint32_t i = 0; i < desc_num; i++) {
|
|
handle->bufs[i] = heap_caps_calloc(1, buf_size, DAC_DMA_ALLOC_CAPS);
|
|
ESP_GOTO_ON_FALSE(handle->bufs[i], ESP_ERR_NO_MEM, err, TAG, "failed to allocate dma buffer");
|
|
}
|
|
|
|
/**
|
|
* Create the DMA descriptor link list.
|
|
* The descriptor format of the link list item is binary-compatible with 'lldesc_t',
|
|
* so the link list head address can be fed to the old DMA backend directly.
|
|
*/
|
|
gdma_link_list_config_t link_cfg = {
|
|
.num_items = desc_num,
|
|
.item_alignment = 4,
|
|
.flags = {
|
|
.items_in_ext_mem = false,
|
|
.check_owner = false,
|
|
},
|
|
};
|
|
ESP_GOTO_ON_ERROR(gdma_new_link_list(&link_cfg, &handle->link), err, TAG, "failed to create dma link list");
|
|
|
|
/**
|
|
* Mount each DMA buffer to its own link list item once.
|
|
* The buffer<->item binding stays fixed afterwards.
|
|
*/
|
|
for (uint32_t i = 0; i < desc_num; i++) {
|
|
gdma_buffer_mount_config_t mount_cfg = {
|
|
.buffer = handle->bufs[i],
|
|
.length = buf_size,
|
|
.buffer_alignment = 4,
|
|
.flags = {
|
|
.mark_final = GDMA_FINAL_LINK_TO_DEFAULT,
|
|
},
|
|
};
|
|
ESP_GOTO_ON_ERROR(gdma_link_mount_buffers(handle->link, i, &mount_cfg, 1, NULL),
|
|
err, TAG, "failed to mount dma buffer");
|
|
}
|
|
|
|
return ESP_OK;
|
|
err:
|
|
s_dac_free_dma_desc(handle);
|
|
return ret;
|
|
}
|
|
|
|
static void IRAM_ATTR s_dac_default_intr_handler(void *arg)
|
|
{
|
|
dac_continuous_handle_t handle = arg;
|
|
BaseType_t need_awoke = pdFALSE;
|
|
BaseType_t tmp = pdFALSE;
|
|
|
|
uint32_t intr_mask = dac_dma_periph_intr_get_mask();
|
|
dac_continuous_fsm_t fsm = atomic_load(&s_dac_cont_fsm);
|
|
|
|
if (intr_mask & DAC_DMA_DONE_INTR) {
|
|
if (fsm == DAC_CONT_FSM_SYNC || fsm == DAC_CONT_FSM_SYNC_WAIT) {
|
|
/* Sync writing mode: Recycle the descriptor */
|
|
xQueueSendFromISR(handle->free_desc_queue, &handle->cur_index, &tmp);
|
|
need_awoke |= tmp;
|
|
}
|
|
|
|
if (handle->cbs.on_convert_done) {
|
|
dac_event_data_t evt_data = {
|
|
.buf = handle->bufs[handle->cur_index],
|
|
.buf_size = handle->cfg.buf_size,
|
|
.write_bytes = gdma_link_get_length(handle->link, handle->cur_index),
|
|
};
|
|
need_awoke |= handle->cbs.on_convert_done(handle, &evt_data, handle->user_data);
|
|
}
|
|
|
|
handle->cur_index = (handle->cur_index + 1) % handle->used_desc_num;
|
|
}
|
|
if (intr_mask & DAC_DMA_TEOF_INTR) {
|
|
/**
|
|
* Total EOF interrupt: DMA has reached the end of a descriptor chain (NULL next pointer).
|
|
* This only occurs naturally in sync writing mode when all queued data has been transmitted.
|
|
*/
|
|
bool dma_restart = false;
|
|
#if SOC_IS(ESP32)
|
|
if (fsm == DAC_CONT_FSM_SYNC || fsm == DAC_CONT_FSM_SYNC_WAIT) {
|
|
/* Check for any remaining descriptors (ignored due to prefetching), and restart the DMA */
|
|
portENTER_CRITICAL_ISR(&handle->dma_lock);
|
|
if (!handle->dma_running) {
|
|
/* Stop already in progress, do not restart */
|
|
} else if (gdma_link_check_end(handle->link, (int)handle->cur_index - 1) == false) {
|
|
dac_dma_periph_trans_start(gdma_link_get_item_addr(handle->link, handle->cur_index));
|
|
dma_restart = true;
|
|
} else {
|
|
handle->dma_running = false;
|
|
}
|
|
portEXIT_CRITICAL_ISR(&handle->dma_lock);
|
|
}
|
|
#endif
|
|
|
|
if (!dma_restart && handle->cbs.on_stop) {
|
|
need_awoke |= handle->cbs.on_stop(handle, NULL, handle->user_data);
|
|
}
|
|
}
|
|
if (need_awoke == pdTRUE) {
|
|
portYIELD_FROM_ISR();
|
|
}
|
|
}
|
|
|
|
esp_err_t dac_continuous_new_channels(const dac_continuous_config_t *cont_cfg, dac_continuous_handle_t *ret_handle)
|
|
{
|
|
#if CONFIG_DAC_ENABLE_DEBUG_LOG
|
|
esp_log_level_set(TAG, ESP_LOG_DEBUG);
|
|
#endif
|
|
/* Parameters validation */
|
|
DAC_NULL_POINTER_CHECK(cont_cfg);
|
|
DAC_NULL_POINTER_CHECK(ret_handle);
|
|
ESP_RETURN_ON_FALSE(IS_VALID_DAC_CHANNEL_MASK(cont_cfg->chan_mask) && cont_cfg->chan_mask, ESP_ERR_INVALID_ARG, TAG, "invalid dac channel mask");
|
|
ESP_RETURN_ON_FALSE(cont_cfg->desc_num > 1, ESP_ERR_INVALID_ARG, TAG, "at least two DMA descriptor needed");
|
|
ESP_RETURN_ON_FALSE(cont_cfg->buf_size > 0 && cont_cfg->buf_size % 2 == 0, ESP_ERR_INVALID_ARG, TAG, "buf_size must be a positive even number");
|
|
ESP_RETURN_ON_FALSE(cont_cfg->buf_size <= DAC_DMA_MAX_BUF_SIZE, ESP_ERR_INVALID_ARG, TAG, "buf_size exceeds the maximum limit");
|
|
|
|
esp_err_t ret = ESP_OK;
|
|
|
|
/* FSM: IDLE -> WAIT */
|
|
dac_continuous_fsm_t expected_fsm = DAC_CONT_FSM_IDLE;
|
|
if (!atomic_compare_exchange_strong(&s_dac_cont_fsm, &expected_fsm, DAC_CONT_FSM_WAIT)) {
|
|
ESP_LOGE(TAG, "DAC continuous is already in use");
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
|
|
/* Register the channels */
|
|
dac_channel_mask_t registered_chan_mask = 0;
|
|
DAC_CHANNEL_MASK_FOREACH(chan, cont_cfg->chan_mask) {
|
|
ESP_GOTO_ON_ERROR(dac_priv_register_channel(chan),
|
|
err4, TAG, "register dac channel %"PRIu32" failed", chan);
|
|
registered_chan_mask |= BIT(chan);
|
|
}
|
|
|
|
/* Allocate continuous mode struct */
|
|
dac_continuous_handle_t handle = heap_caps_calloc(1, sizeof(struct dac_continuous_s) + cont_cfg->desc_num * sizeof(uint8_t *), DAC_MEM_ALLOC_CAPS);
|
|
ESP_GOTO_ON_FALSE(handle, ESP_ERR_NO_MEM, err4, TAG, "no memory for the dac continuous mode structure");
|
|
|
|
handle->cfg = *cont_cfg;
|
|
|
|
#if SOC_IS(ESP32)
|
|
handle->dma_lock = (portMUX_TYPE)portMUX_INITIALIZER_UNLOCKED;
|
|
#endif
|
|
|
|
handle->free_desc_queue = xQueueCreateWithCaps(cont_cfg->desc_num, sizeof(int), DAC_MEM_ALLOC_CAPS);
|
|
ESP_GOTO_ON_FALSE(handle->free_desc_queue, ESP_ERR_NO_MEM, err3, TAG, "Failed to create free descriptor queue");
|
|
handle->mutex = xSemaphoreCreateMutexWithCaps(DAC_MEM_ALLOC_CAPS);
|
|
ESP_GOTO_ON_FALSE(handle->mutex, ESP_ERR_NO_MEM, err3, TAG, "Failed to create mutex");
|
|
|
|
/* Create PM lock */
|
|
#if CONFIG_PM_ENABLE
|
|
esp_pm_lock_type_t pm_lock_type = cont_cfg->clk_src == DAC_DIGI_CLK_SRC_APLL ? ESP_PM_NO_LIGHT_SLEEP : ESP_PM_APB_FREQ_MAX;
|
|
ESP_GOTO_ON_ERROR(esp_pm_lock_create(pm_lock_type, 0, "dac_driver", &handle->pm_lock), err3, TAG, "Failed to create DAC pm lock");
|
|
#endif
|
|
|
|
/* Create DMA descriptors and buffers */
|
|
ESP_GOTO_ON_ERROR(s_dac_alloc_dma_desc(handle), err3, TAG, "Failed to create DMA descriptors and buffers");
|
|
|
|
/* Initialize DAC DMA peripheral */
|
|
ESP_GOTO_ON_ERROR(dac_dma_periph_init(cont_cfg->freq_hz,
|
|
cont_cfg->chan_mode == DAC_CHANNEL_MODE_ALTER,
|
|
cont_cfg->clk_src == DAC_DIGI_CLK_SRC_APLL),
|
|
err2, TAG, "Failed to initialize DAC DMA peripheral");
|
|
|
|
/* Register DMA interrupt */
|
|
ESP_GOTO_ON_ERROR(esp_intr_alloc(dac_dma_periph_get_intr_signal(), DAC_INTR_ALLOC_FLAGS,
|
|
s_dac_default_intr_handler, handle, &(handle->intr_handle)),
|
|
err1, TAG, "Failed to register DAC DMA interrupt");
|
|
|
|
/* Connect DAC module to the DMA peripheral */
|
|
DAC_RTC_ENTER_CRITICAL();
|
|
dac_ll_digi_enable_dma(true);
|
|
DAC_RTC_EXIT_CRITICAL();
|
|
|
|
/* FSM: WAIT -> REGISTERED */
|
|
atomic_store(&s_dac_cont_fsm, DAC_CONT_FSM_REGISTERED);
|
|
|
|
*ret_handle = handle;
|
|
return ret;
|
|
|
|
err1:
|
|
dac_dma_periph_deinit();
|
|
err2:
|
|
s_dac_free_dma_desc(handle);
|
|
err3:
|
|
if (handle->free_desc_queue) {
|
|
vQueueDeleteWithCaps(handle->free_desc_queue);
|
|
}
|
|
if (handle->mutex) {
|
|
vSemaphoreDeleteWithCaps(handle->mutex);
|
|
}
|
|
#if CONFIG_PM_ENABLE
|
|
if (handle->pm_lock) {
|
|
esp_pm_lock_delete(handle->pm_lock);
|
|
}
|
|
#endif
|
|
free(handle);
|
|
err4:
|
|
/* Deregister registered channels */
|
|
DAC_CHANNEL_MASK_FOREACH(chan, registered_chan_mask) {
|
|
dac_priv_deregister_channel(chan);
|
|
}
|
|
/* FSM: WAIT -> IDLE */
|
|
atomic_store(&s_dac_cont_fsm, DAC_CONT_FSM_IDLE);
|
|
return ret;
|
|
}
|
|
|
|
esp_err_t dac_continuous_del_channels(dac_continuous_handle_t handle)
|
|
{
|
|
DAC_NULL_POINTER_CHECK(handle);
|
|
|
|
/* FSM: REGISTERED -> WAIT */
|
|
dac_continuous_fsm_t expected_fsm = DAC_CONT_FSM_REGISTERED;
|
|
if (!atomic_compare_exchange_strong(&s_dac_cont_fsm, &expected_fsm, DAC_CONT_FSM_WAIT)) {
|
|
ESP_LOGE(TAG, "DAC continuous is not registered / disabled");
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
|
|
/* Deregister DMA interrupt */
|
|
if (handle->intr_handle) {
|
|
ESP_RETURN_ON_ERROR(esp_intr_free(handle->intr_handle), TAG, "Failed to deregister DMA interrupt");
|
|
handle->intr_handle = NULL;
|
|
}
|
|
|
|
/* Deinitialize DMA peripheral */
|
|
ESP_RETURN_ON_ERROR(dac_dma_periph_deinit(), TAG, "Failed to deinitialize DAC DMA peripheral");
|
|
|
|
/* Disconnect DAC module from the DMA peripheral */
|
|
DAC_RTC_ENTER_CRITICAL();
|
|
dac_ll_digi_enable_dma(false);
|
|
DAC_RTC_EXIT_CRITICAL();
|
|
|
|
/* Free allocated resources */
|
|
s_dac_free_dma_desc(handle);
|
|
|
|
if (handle->free_desc_queue) {
|
|
vQueueDeleteWithCaps(handle->free_desc_queue);
|
|
handle->free_desc_queue = NULL;
|
|
}
|
|
if (handle->mutex) {
|
|
vSemaphoreDeleteWithCaps(handle->mutex);
|
|
handle->mutex = NULL;
|
|
}
|
|
|
|
#if CONFIG_PM_ENABLE
|
|
if (handle->pm_lock) {
|
|
esp_pm_lock_delete(handle->pm_lock);
|
|
handle->pm_lock = NULL;
|
|
}
|
|
#endif
|
|
|
|
/* Deregister the channels */
|
|
DAC_CHANNEL_MASK_FOREACH(chan, handle->cfg.chan_mask) {
|
|
dac_priv_deregister_channel(chan);
|
|
}
|
|
|
|
free(handle);
|
|
|
|
/* FSM: WAIT -> IDLE */
|
|
atomic_store(&s_dac_cont_fsm, DAC_CONT_FSM_IDLE);
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t dac_continuous_register_event_callback(dac_continuous_handle_t handle, const dac_event_callbacks_t *callbacks, void *user_data)
|
|
{
|
|
DAC_NULL_POINTER_CHECK(handle);
|
|
|
|
if (callbacks == NULL) {
|
|
memset(&handle->cbs, 0, sizeof(dac_event_callbacks_t));
|
|
handle->user_data = NULL;
|
|
return ESP_OK;
|
|
}
|
|
#if CONFIG_DAC_ISR_IRAM_SAFE
|
|
if (callbacks->on_convert_done) {
|
|
ESP_RETURN_ON_FALSE(esp_ptr_in_iram(callbacks->on_convert_done), ESP_ERR_INVALID_ARG, TAG, "on_convert_done callback not in IRAM");
|
|
}
|
|
if (callbacks->on_stop) {
|
|
ESP_RETURN_ON_FALSE(esp_ptr_in_iram(callbacks->on_stop), ESP_ERR_INVALID_ARG, TAG, "on_stop callback not in IRAM");
|
|
}
|
|
if (user_data) {
|
|
ESP_RETURN_ON_FALSE(esp_ptr_internal(user_data), ESP_ERR_INVALID_ARG, TAG, "user context not in internal RAM");
|
|
}
|
|
#endif
|
|
handle->cbs = *callbacks;
|
|
handle->user_data = user_data;
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t dac_continuous_enable(dac_continuous_handle_t handle)
|
|
{
|
|
DAC_NULL_POINTER_CHECK(handle);
|
|
|
|
/* FSM: REGISTERED -> WAIT */
|
|
dac_continuous_fsm_t expected_fsm = DAC_CONT_FSM_REGISTERED;
|
|
if (!atomic_compare_exchange_strong(&s_dac_cont_fsm, &expected_fsm, DAC_CONT_FSM_WAIT)) {
|
|
ESP_LOGE(TAG, "DAC continuous is not registered / disabled");
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
|
|
#ifdef CONFIG_PM_ENABLE
|
|
esp_pm_lock_acquire(handle->pm_lock);
|
|
#endif
|
|
|
|
DAC_CHANNEL_MASK_FOREACH(chan, handle->cfg.chan_mask) {
|
|
dac_priv_enable_channel(chan);
|
|
}
|
|
dac_dma_periph_enable();
|
|
esp_intr_enable(handle->intr_handle);
|
|
|
|
DAC_RTC_ENTER_CRITICAL();
|
|
dac_ll_digi_enable_dma(true);
|
|
DAC_RTC_EXIT_CRITICAL();
|
|
|
|
/* FSM: WAIT -> ENABLED */
|
|
atomic_store(&s_dac_cont_fsm, DAC_CONT_FSM_ENABLED);
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t dac_continuous_disable(dac_continuous_handle_t handle)
|
|
{
|
|
DAC_NULL_POINTER_CHECK(handle);
|
|
|
|
/* For backward compatibility, check if there is any ongoing cyclic conversion and stop it */
|
|
if (atomic_load(&s_dac_cont_fsm) == DAC_CONT_FSM_CYCLIC) {
|
|
ESP_LOGW(TAG, "It is recommended to explicitly stop the cyclic conversion by calling dac_continuous_stop_cyclically() before performing other operations.");
|
|
ESP_RETURN_ON_ERROR(dac_continuous_stop_cyclically(handle), TAG, "Failed to stop cyclic conversion");
|
|
}
|
|
|
|
/* Check if there is any ongoing SYNC writing and wait for it to stop */
|
|
if (atomic_load(&s_dac_cont_fsm) == DAC_CONT_FSM_SYNC) {
|
|
ESP_RETURN_ON_ERROR(s_dac_continuous_stop_sync(handle), TAG, "Failed to stop sync writing");
|
|
}
|
|
|
|
/* FSM: ENABLED -> WAIT */
|
|
dac_continuous_fsm_t expected_fsm = DAC_CONT_FSM_ENABLED;
|
|
ESP_RETURN_ON_FALSE(atomic_compare_exchange_strong(&s_dac_cont_fsm, &expected_fsm, DAC_CONT_FSM_WAIT),
|
|
ESP_ERR_INVALID_STATE, TAG, "DAC continuous is running/not enabled");
|
|
|
|
dac_dma_periph_disable();
|
|
esp_intr_disable(handle->intr_handle);
|
|
|
|
DAC_RTC_ENTER_CRITICAL();
|
|
dac_ll_digi_enable_dma(false);
|
|
DAC_RTC_EXIT_CRITICAL();
|
|
|
|
DAC_CHANNEL_MASK_FOREACH(chan, handle->cfg.chan_mask) {
|
|
dac_priv_disable_channel(chan);
|
|
}
|
|
#ifdef CONFIG_PM_ENABLE
|
|
esp_pm_lock_release(handle->pm_lock);
|
|
#endif
|
|
|
|
/* FSM: WAIT -> REGISTERED */
|
|
atomic_store(&s_dac_cont_fsm, DAC_CONT_FSM_REGISTERED);
|
|
return ESP_OK;
|
|
}
|
|
|
|
//////////////////////////////////// Async writing ////////////////////////////////////
|
|
|
|
esp_err_t dac_continuous_start_async_writing(dac_continuous_handle_t handle)
|
|
{
|
|
DAC_NULL_POINTER_CHECK(handle);
|
|
ESP_RETURN_ON_FALSE(handle->cbs.on_convert_done, ESP_ERR_INVALID_STATE, TAG,
|
|
"please register 'on_convert_done' callback before starting asynchronous writing");
|
|
|
|
/* For backward compatibility, check if there is any ongoing cyclic conversion and stop it */
|
|
if (atomic_load(&s_dac_cont_fsm) == DAC_CONT_FSM_CYCLIC) {
|
|
ESP_LOGW(TAG, "It is recommended to explicitly stop the cyclic conversion by calling dac_continuous_stop_cyclically() before performing other operations.");
|
|
ESP_RETURN_ON_ERROR(dac_continuous_stop_cyclically(handle), TAG, "Failed to stop cyclic conversion");
|
|
}
|
|
|
|
/* Check if there is any ongoing SYNC writing and wait for it to stop */
|
|
if (atomic_load(&s_dac_cont_fsm) == DAC_CONT_FSM_SYNC) {
|
|
ESP_RETURN_ON_ERROR(s_dac_continuous_stop_sync(handle), TAG, "Failed to stop sync writing");
|
|
}
|
|
|
|
/* FSM: ENABLED -> WAIT */
|
|
dac_continuous_fsm_t expected_fsm = DAC_CONT_FSM_ENABLED;
|
|
ESP_RETURN_ON_FALSE(atomic_compare_exchange_strong(&s_dac_cont_fsm, &expected_fsm, DAC_CONT_FSM_WAIT),
|
|
ESP_ERR_INVALID_STATE, TAG, "DAC continuous is running/not enabled");
|
|
|
|
/* Link all descriptors as a ring */
|
|
for (int i = 0; i < handle->cfg.desc_num; i++) {
|
|
memset(handle->bufs[i], 0, handle->cfg.buf_size);
|
|
gdma_link_set_length(handle->link, i, handle->cfg.buf_size);
|
|
gdma_link_set_owner(handle->link, i, GDMA_LLI_OWNER_DMA);
|
|
gdma_link_concat(handle->link, i, handle->link, (i < handle->cfg.desc_num - 1) ? i + 1 : 0);
|
|
}
|
|
|
|
handle->cur_index = 0;
|
|
handle->used_desc_num = handle->cfg.desc_num;
|
|
/* Start with an all-zero buffer. User will be notified by the 'on_convert_done' callback, then load the data into the buffer. */
|
|
dac_dma_periph_trans_start(gdma_link_get_head_addr(handle->link));
|
|
|
|
/* FSM: WAIT -> ASYNC */
|
|
atomic_store(&s_dac_cont_fsm, DAC_CONT_FSM_ASYNC);
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t dac_continuous_stop_async_writing(dac_continuous_handle_t handle)
|
|
{
|
|
DAC_NULL_POINTER_CHECK(handle);
|
|
|
|
/* FSM: ASYNC -> WAIT */
|
|
dac_continuous_fsm_t expected_fsm = DAC_CONT_FSM_ASYNC;
|
|
if (!atomic_compare_exchange_strong(&s_dac_cont_fsm, &expected_fsm, DAC_CONT_FSM_WAIT)) {
|
|
ESP_LOGE(TAG, "DAC continuous is not in asynchronous writing mode");
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
|
|
dac_dma_periph_trans_stop();
|
|
|
|
/* FSM: WAIT -> ENABLED */
|
|
atomic_store(&s_dac_cont_fsm, DAC_CONT_FSM_ENABLED);
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
/* Buffer expanding coefficient, the input buffer will expand to twice length while enabled AUTO_16_BIT */
|
|
#if CONFIG_DAC_DMA_AUTO_16BIT_ALIGN
|
|
#define DAC_16BIT_ALIGN_COEFF 2
|
|
#else
|
|
#define DAC_16BIT_ALIGN_COEFF 1
|
|
#endif
|
|
|
|
/**
|
|
* @brief Load data into the DMA descriptor
|
|
*
|
|
* @param auto_balance Whether to balance the data between the last two descriptors. If disabled, we will load as much data as possible.
|
|
* @return Loaded data length. The remaining data length is (data_len - return_value)
|
|
*
|
|
* @note if CONFIG_DAC_DMA_AUTO_16BIT_ALIGN is enabled, data_len can be odd, otherwise it must be even
|
|
*/
|
|
static size_t s_dac_load_data_into_desc(dac_continuous_handle_t handle, int index, const uint8_t *data, size_t data_len, bool auto_balance)
|
|
{
|
|
/* Calculate the length of the data to be loaded */
|
|
size_t buf_size = handle->cfg.buf_size; // must be even
|
|
size_t need_len = data_len * DAC_16BIT_ALIGN_COEFF; // must be even
|
|
size_t load_len; // must be even
|
|
if (need_len <= buf_size) {
|
|
load_len = need_len;
|
|
} else if (auto_balance && need_len < buf_size * 2) {
|
|
/**
|
|
* The remaining data can fit into two descriptors, so we load half in this round,
|
|
* and the next round will naturally fall into the branch above.
|
|
*/
|
|
load_len = need_len / 2;
|
|
load_len += load_len & 1U; // make it even
|
|
} else {
|
|
load_len = buf_size;
|
|
}
|
|
|
|
uint8_t *buf = handle->bufs[index];
|
|
#if CONFIG_DAC_DMA_AUTO_16BIT_ALIGN
|
|
/* Load the data to the high 8 bit in the 16-bit width slot */
|
|
for (size_t i = 0; i < load_len; i += 2) {
|
|
buf[i + 1] = data[i / 2] + handle->cfg.offset;
|
|
}
|
|
#else
|
|
/* Load the data into the DMA buffer */
|
|
for (size_t i = 0; i < load_len; i++) {
|
|
buf[i] = data[i] + handle->cfg.offset;
|
|
}
|
|
#endif
|
|
|
|
gdma_link_set_length(handle->link, index, load_len);
|
|
gdma_link_set_owner(handle->link, index, GDMA_LLI_OWNER_DMA);
|
|
|
|
return load_len / DAC_16BIT_ALIGN_COEFF;
|
|
}
|
|
|
|
esp_err_t dac_continuous_write_asynchronously(dac_continuous_handle_t handle, uint8_t *dma_buf, size_t dma_buf_len,
|
|
const uint8_t *data, size_t data_len, size_t *bytes_loaded)
|
|
{
|
|
DAC_NULL_POINTER_CHECK_ISR(handle);
|
|
DAC_NULL_POINTER_CHECK_ISR(dma_buf);
|
|
DAC_NULL_POINTER_CHECK_ISR(data);
|
|
ESP_RETURN_ON_FALSE_ISR(data_len > 0, ESP_ERR_INVALID_ARG, TAG, "data_len must be > 0");
|
|
#if !CONFIG_DAC_DMA_AUTO_16BIT_ALIGN
|
|
ESP_RETURN_ON_FALSE_ISR(data_len % 2 == 0, ESP_ERR_INVALID_ARG, TAG, "data_len must be even when AUTO_16BIT_ALIGN is disabled");
|
|
#endif
|
|
|
|
/* FSM: ASYNC -> WAIT */
|
|
dac_continuous_fsm_t expected_fsm = DAC_CONT_FSM_ASYNC;
|
|
if (!atomic_compare_exchange_strong(&s_dac_cont_fsm, &expected_fsm, DAC_CONT_FSM_WAIT)) {
|
|
ESP_EARLY_LOGE(TAG, "DAC continuous is not in asynchronous writing mode");
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
|
|
esp_err_t ret = ESP_OK;
|
|
|
|
/**
|
|
* Normally, dma_buf_len should always be equal to the buffer size of descriptors
|
|
*/
|
|
if (dma_buf_len != handle->cfg.buf_size) {
|
|
ESP_EARLY_LOGW(TAG, "dma_buf_len != DMA buffer size. This parameter is ignored.");
|
|
}
|
|
|
|
/* Find the corresponding DMA descriptor index */
|
|
int index = 0;
|
|
for (; index < handle->cfg.desc_num; index++) {
|
|
if (dma_buf == handle->bufs[index]) {
|
|
break;
|
|
}
|
|
}
|
|
ESP_GOTO_ON_FALSE_ISR(index < handle->cfg.desc_num, ESP_ERR_NOT_FOUND, clean_up, TAG, "Corresponding DMA descriptor not found");
|
|
|
|
/* Load data into DMA buffer. We disable the auto balance here because the total length is actually uncertain. */
|
|
size_t loaded_len = s_dac_load_data_into_desc(handle, index, data, data_len, false);
|
|
if (bytes_loaded) {
|
|
*bytes_loaded = loaded_len;
|
|
}
|
|
|
|
clean_up:
|
|
/* FSM: WAIT -> ASYNC */
|
|
atomic_store(&s_dac_cont_fsm, DAC_CONT_FSM_ASYNC);
|
|
return ret;
|
|
}
|
|
|
|
//////////////////////////////////// Cyclic writing ////////////////////////////////////
|
|
|
|
esp_err_t dac_continuous_write_cyclically(dac_continuous_handle_t handle, uint8_t *buf, size_t buf_size, size_t *bytes_loaded)
|
|
{
|
|
DAC_NULL_POINTER_CHECK(handle);
|
|
DAC_NULL_POINTER_CHECK(buf);
|
|
ESP_RETURN_ON_FALSE(buf_size > 0, ESP_ERR_INVALID_ARG, TAG, "buf_size must be > 0");
|
|
#if !CONFIG_DAC_DMA_AUTO_16BIT_ALIGN
|
|
ESP_RETURN_ON_FALSE(buf_size % 2 == 0, ESP_ERR_INVALID_ARG, TAG, "buf_size must be even when AUTO_16BIT_ALIGN is disabled");
|
|
#endif
|
|
ESP_RETURN_ON_FALSE(buf_size * DAC_16BIT_ALIGN_COEFF <= handle->cfg.buf_size * handle->cfg.desc_num,
|
|
ESP_ERR_INVALID_ARG, TAG, "Data size exceeds the total DMA buffer size");
|
|
|
|
esp_err_t ret = ESP_OK;
|
|
|
|
/* Serialize with the other writing APIs */
|
|
ESP_RETURN_ON_FALSE(xSemaphoreTake(handle->mutex, portMAX_DELAY) == pdTRUE,
|
|
ESP_ERR_TIMEOUT, TAG, "Take mutex timeout");
|
|
|
|
/* For backward compatibility, check if there is any ongoing cyclic conversion and stop it */
|
|
if (atomic_load(&s_dac_cont_fsm) == DAC_CONT_FSM_CYCLIC) {
|
|
ESP_GOTO_ON_ERROR(dac_continuous_stop_cyclically(handle), err, TAG, "Failed to stop cyclic conversion");
|
|
}
|
|
|
|
/* Check if there is any ongoing SYNC writing and wait for it to stop */
|
|
if (atomic_load(&s_dac_cont_fsm) == DAC_CONT_FSM_SYNC) {
|
|
ESP_GOTO_ON_ERROR(s_dac_continuous_stop_sync(handle), err, TAG, "Failed to stop sync writing");
|
|
}
|
|
|
|
/* FSM: ENABLED -> WAIT */
|
|
dac_continuous_fsm_t expected_fsm = DAC_CONT_FSM_ENABLED;
|
|
ESP_GOTO_ON_FALSE(atomic_compare_exchange_strong(&s_dac_cont_fsm, &expected_fsm, DAC_CONT_FSM_WAIT),
|
|
ESP_ERR_INVALID_STATE, err, TAG, "DAC continuous is running/not enabled");
|
|
|
|
size_t remain_size = buf_size;
|
|
uint32_t index = 0;
|
|
for (; index < handle->cfg.desc_num && remain_size > 0; index++) {
|
|
size_t loaded_len = s_dac_load_data_into_desc(handle, index, buf, remain_size, true);
|
|
remain_size -= loaded_len;
|
|
buf += loaded_len;
|
|
}
|
|
/* All data should be loaded */
|
|
assert(remain_size == 0);
|
|
|
|
/* Link the used descriptors as a ring: 0 -> 1 -> ... -> (index-1) -> 0 */
|
|
for (int k = 0; k < index - 1; k++) {
|
|
gdma_link_concat(handle->link, k, handle->link, k + 1);
|
|
}
|
|
gdma_link_concat(handle->link, index - 1, handle->link, 0);
|
|
|
|
handle->cur_index = 0;
|
|
handle->used_desc_num = index;
|
|
dac_dma_periph_trans_start(gdma_link_get_head_addr(handle->link));
|
|
|
|
/* FSM: WAIT -> CYCLIC */
|
|
atomic_store(&s_dac_cont_fsm, DAC_CONT_FSM_CYCLIC);
|
|
|
|
if (bytes_loaded) {
|
|
*bytes_loaded = buf_size;
|
|
}
|
|
|
|
err:
|
|
xSemaphoreGive(handle->mutex);
|
|
return ret;
|
|
}
|
|
|
|
esp_err_t dac_continuous_stop_cyclically(dac_continuous_handle_t handle)
|
|
{
|
|
DAC_NULL_POINTER_CHECK(handle);
|
|
|
|
/* FSM: CYCLIC -> WAIT */
|
|
dac_continuous_fsm_t expected_fsm = DAC_CONT_FSM_CYCLIC;
|
|
if (!atomic_compare_exchange_strong(&s_dac_cont_fsm, &expected_fsm, DAC_CONT_FSM_WAIT)) {
|
|
ESP_LOGE(TAG, "DAC continuous is not in cyclic writing mode");
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
|
|
dac_dma_periph_trans_stop();
|
|
|
|
/* FSM: WAIT -> ENABLED */
|
|
atomic_store(&s_dac_cont_fsm, DAC_CONT_FSM_ENABLED);
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
//////////////////////////////////// Synchronous writing ////////////////////////////////////
|
|
|
|
esp_err_t dac_continuous_write(dac_continuous_handle_t handle, uint8_t *buf, size_t buf_size, size_t *bytes_loaded, int timeout_ms)
|
|
{
|
|
DAC_NULL_POINTER_CHECK(handle);
|
|
DAC_NULL_POINTER_CHECK(buf);
|
|
ESP_RETURN_ON_FALSE(buf_size > 0, ESP_ERR_INVALID_ARG, TAG, "buf_size must be > 0");
|
|
#if !CONFIG_DAC_DMA_AUTO_16BIT_ALIGN
|
|
ESP_RETURN_ON_FALSE(buf_size % 2 == 0, ESP_ERR_INVALID_ARG, TAG, "buf_size must be even when AUTO_16BIT_ALIGN is disabled");
|
|
#endif
|
|
|
|
esp_err_t ret = ESP_OK;
|
|
|
|
TickType_t timeout_tick = timeout_ms < 0 ? portMAX_DELAY : pdMS_TO_TICKS(timeout_ms);
|
|
size_t remain_size = buf_size;
|
|
|
|
/* Serialize with the other writing APIs */
|
|
ESP_RETURN_ON_FALSE(xSemaphoreTake(handle->mutex, timeout_tick) == pdTRUE,
|
|
ESP_ERR_TIMEOUT, TAG, "Take mutex timeout");
|
|
|
|
dac_continuous_fsm_t fsm = atomic_load(&s_dac_cont_fsm);
|
|
switch (fsm) {
|
|
case DAC_CONT_FSM_CYCLIC:
|
|
/* For backward compatibility, check if there is any ongoing cyclic conversion and stop it */
|
|
ESP_LOGW(TAG, "It is recommended to explicitly stop the cyclic conversion by calling dac_continuous_stop_cyclically() before performing other operations.");
|
|
ESP_GOTO_ON_ERROR(dac_continuous_stop_cyclically(handle), err, TAG, "Failed to stop cyclic conversion");
|
|
[[fallthrough]];
|
|
|
|
case DAC_CONT_FSM_ENABLED:
|
|
/* FSM: ENABLED -> SYNC_WAIT */
|
|
dac_continuous_fsm_t expected_fsm = DAC_CONT_FSM_ENABLED;
|
|
ESP_GOTO_ON_FALSE(atomic_compare_exchange_strong(&s_dac_cont_fsm, &expected_fsm, DAC_CONT_FSM_SYNC_WAIT),
|
|
ESP_ERR_INVALID_STATE, err, TAG, "DAC continuous is running/not enabled");
|
|
|
|
/* Reset the free_desc_queue and the cur_index */
|
|
xQueueReset(handle->free_desc_queue);
|
|
for (int i = 1; i < handle->cfg.desc_num; i++) { // skip 0 because we will use it right now
|
|
xQueueSend(handle->free_desc_queue, &i, 0);
|
|
}
|
|
handle->cur_index = 0;
|
|
handle->used_desc_num = handle->cfg.desc_num;
|
|
|
|
/* Load one descriptor and start the DMA */
|
|
size_t loaded_len = s_dac_load_data_into_desc(handle, 0, buf, remain_size, true);
|
|
remain_size -= loaded_len;
|
|
buf += loaded_len;
|
|
gdma_link_concat(handle->link, 0, NULL, 0);
|
|
#if SOC_IS(ESP32)
|
|
/* It is safe to operate without the lock here because the DMA is not running yet. */
|
|
handle->dma_running = true;
|
|
#endif
|
|
dac_dma_periph_trans_start(gdma_link_get_head_addr(handle->link));
|
|
|
|
goto skip_cas;
|
|
|
|
case DAC_CONT_FSM_SYNC:
|
|
/* FSM: SYNC -> SYNC_WAIT */
|
|
expected_fsm = DAC_CONT_FSM_SYNC;
|
|
ESP_GOTO_ON_FALSE(atomic_compare_exchange_strong(&s_dac_cont_fsm, &expected_fsm, DAC_CONT_FSM_SYNC_WAIT),
|
|
ESP_ERR_INVALID_STATE, err, TAG, "CAS failed: SYNC -> SYNC_WAIT");
|
|
|
|
skip_cas:
|
|
while (remain_size > 0) {
|
|
int index;
|
|
if (xQueueReceive(handle->free_desc_queue, &index, timeout_tick) != pdTRUE) {
|
|
ret = ESP_ERR_TIMEOUT;
|
|
break;
|
|
}
|
|
size_t loaded_len = s_dac_load_data_into_desc(handle, index, buf, remain_size, true);
|
|
remain_size -= loaded_len;
|
|
buf += loaded_len;
|
|
/**
|
|
* link: (index-1) -> index -> NULL
|
|
* NOTE: gdma_link_concat() can normalize the index to be between 0 and desc_num - 1.
|
|
*/
|
|
gdma_link_concat(handle->link, index, NULL, 0);
|
|
|
|
#if SOC_IS(ESP32)
|
|
/**
|
|
* The ESP32 I2S DMA append() (restart) is buggy, so we re-issue start() when the DMA has stopped. See IDF-15791.
|
|
* Synchronize with the TEOF handler via dma_lock to prevent duplicate or missed starts.
|
|
*/
|
|
portENTER_CRITICAL(&handle->dma_lock);
|
|
gdma_link_concat(handle->link, index - 1, handle->link, index);
|
|
if (!handle->dma_running) {
|
|
handle->dma_running = true;
|
|
dac_dma_periph_trans_start(gdma_link_get_item_addr(handle->link, index));
|
|
}
|
|
portEXIT_CRITICAL(&handle->dma_lock);
|
|
#else
|
|
gdma_link_concat(handle->link, index - 1, handle->link, index);
|
|
dac_dma_periph_trans_append();
|
|
#endif
|
|
}
|
|
break;
|
|
|
|
default:
|
|
ESP_LOGE(TAG, "Unexpected FSM state: %u", fsm);
|
|
ret = ESP_ERR_INVALID_STATE;
|
|
goto err;
|
|
}
|
|
|
|
/* FSM: SYNC_WAIT -> SYNC */
|
|
atomic_store(&s_dac_cont_fsm, DAC_CONT_FSM_SYNC);
|
|
err:
|
|
xSemaphoreGive(handle->mutex);
|
|
if (bytes_loaded) {
|
|
*bytes_loaded = buf_size - remain_size;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static esp_err_t s_dac_continuous_stop_sync(dac_continuous_handle_t handle)
|
|
{
|
|
/* FSM: SYNC -> WAIT */
|
|
dac_continuous_fsm_t expected_fsm = DAC_CONT_FSM_SYNC;
|
|
ESP_RETURN_ON_FALSE(atomic_compare_exchange_strong(&s_dac_cont_fsm, &expected_fsm, DAC_CONT_FSM_WAIT),
|
|
ESP_ERR_INVALID_STATE, TAG, "DAC continuous is not in sync writing mode");
|
|
|
|
#if SOC_IS(ESP32)
|
|
/**
|
|
* Serialize with the TEOF ISR which may also call trans_start() on another core.
|
|
* Both must be guarded by dma_lock to prevent concurrent hardware register access.
|
|
*/
|
|
portENTER_CRITICAL(&handle->dma_lock);
|
|
dac_dma_periph_trans_stop();
|
|
handle->dma_running = false;
|
|
portEXIT_CRITICAL(&handle->dma_lock);
|
|
#else
|
|
dac_dma_periph_trans_stop();
|
|
#endif
|
|
|
|
/* FSM: WAIT -> ENABLED */
|
|
atomic_store(&s_dac_cont_fsm, DAC_CONT_FSM_ENABLED);
|
|
|
|
return ESP_OK;
|
|
}
|