diff --git a/drivers/dac_dds/Makefile b/drivers/dac_dds/Makefile new file mode 100644 index 0000000000..48422e909a --- /dev/null +++ b/drivers/dac_dds/Makefile @@ -0,0 +1 @@ +include $(RIOTBASE)/Makefile.base diff --git a/drivers/dac_dds/Makefile.dep b/drivers/dac_dds/Makefile.dep new file mode 100644 index 0000000000..6b811b71f3 --- /dev/null +++ b/drivers/dac_dds/Makefile.dep @@ -0,0 +1,2 @@ +FEATURES_REQUIRED += periph_dac +FEATURES_REQUIRED += periph_timer_periodic diff --git a/drivers/dac_dds/Makefile.include b/drivers/dac_dds/Makefile.include new file mode 100644 index 0000000000..50b100afd7 --- /dev/null +++ b/drivers/dac_dds/Makefile.include @@ -0,0 +1,2 @@ +USEMODULE_INCLUDES_dac_dds := $(LAST_MAKEFILEDIR)/include +USEMODULE_INCLUDES += $(USEMODULE_INCLUDES_dac_dds) diff --git a/drivers/dac_dds/dac_dds.c b/drivers/dac_dds/dac_dds.c new file mode 100644 index 0000000000..0a48143187 --- /dev/null +++ b/drivers/dac_dds/dac_dds.c @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2020 Beuth Hochschule für Technik Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup drivers_periph_dac + * @{ + * + * @file + * @brief Common DAC function fallback implementations + * + * @author Benjamin Valentin + * + * @} + */ + +#include +#include "board.h" +#include "dac_dds.h" +#include "dac_dds_params.h" +#include "irq.h" +#include "kernel_defines.h" +#include "macros/units.h" +#include "periph/timer.h" + +static struct dac_ctx { + const uint8_t *buffers[2]; /* The two sample buffers */ + size_t buffer_len[2]; /* Size of the sample buffers */ + size_t idx; /* Current position in the current buffer */ + dac_dds_cb_t cb; /* Called when the current buffer is done */ + void *cb_arg; /* Callback argument */ + uint16_t sample_ticks; /* Timer ticks per sample */ + uint8_t cur; /* Active sample buffer */ + uint8_t playing; /* DAC is playing */ + uint8_t is_16bit; /* Sample size is 16 instead of 8 bit */ +} _ctx[DAC_DDS_NUMOF]; + +static void _timer_cb(void *arg, int chan) +{ + (void)chan; + + struct dac_ctx *ctx = arg; + + dac_dds_t dac_dds = ctx - _ctx; + dac_t dac = dac_dds_params[dac_dds].dac; + const uint8_t cur = ctx->cur; + const uint8_t *buf = ctx->buffers[cur]; + const size_t len = ctx->buffer_len[cur]; + + if (ctx->is_16bit) { + uint8_t l = buf[ctx->idx++]; + uint8_t h = buf[ctx->idx++]; + + dac_set(dac, (h << 8) | l); + } else { + dac_set(dac, buf[ctx->idx++] << 8); + } + + if (ctx->idx >= len) { + + /* invalidate old buffer */ + ctx->buffer_len[cur] = 0; + + ctx->idx = 0; + ctx->cur = !cur; + + /* stop playing if no more samples are queued */ + if (ctx->buffer_len[!cur] == 0) { + ctx->playing = 0; + timer_stop(dac_dds_params[dac_dds].timer); + /* notify user that next sample buffer can be queued */ + } else if (ctx->cb) { + ctx->cb(ctx->cb_arg); + } + } +} + +void dac_dds_init(dac_dds_t dac, uint16_t sample_rate, uint8_t flags, + dac_dds_cb_t cb, void *cb_arg) +{ + assert(dac < DAC_DDS_NUMOF); + + _ctx[dac].cb = cb; + _ctx[dac].cb_arg = cb_arg; + _ctx[dac].sample_ticks = dac_dds_params[dac].timer_hz / sample_rate; + _ctx[dac].is_16bit = flags & DAC_FLAG_16BIT; + + timer_init(dac_dds_params[dac].timer, dac_dds_params[dac].timer_hz, _timer_cb, &_ctx[dac]); +} + +void dac_dds_set_cb(dac_dds_t dac, dac_dds_cb_t cb, void *cb_arg) +{ + unsigned state = irq_disable(); + + /* allow to update cb_arg independent of cb */ + if (cb || cb_arg == NULL) { + _ctx[dac].cb = cb; + } + _ctx[dac].cb_arg = cb_arg; + + irq_restore(state); +} + +bool dac_dds_play(dac_dds_t dac, const void *buf, size_t len) +{ + struct dac_ctx *ctx = &_ctx[dac]; + + unsigned state = irq_disable(); + + uint8_t next = !ctx->cur; + ctx->buffers[next] = buf; + ctx->buffer_len[next] = len; + + bool is_playing = ctx->playing; + irq_restore(state); + + if (is_playing) { + return true; + } + + ctx->playing = 1; + ctx->cur = next; + + timer_set_periodic(dac_dds_params[dac].timer, 0, ctx->sample_ticks, + TIM_FLAG_RESET_ON_MATCH | TIM_FLAG_RESET_ON_SET); + + /* We can already queue the next buffer */ + if (ctx->cb) { + ctx->cb(ctx->cb_arg); + } + + return false; +} + +void dac_dds_stop(dac_dds_t dac) +{ + timer_stop(dac_dds_params[dac].timer); + _ctx[dac].playing = 0; +} diff --git a/drivers/dac_dds/include/dac_dds_params.h b/drivers/dac_dds/include/dac_dds_params.h new file mode 100644 index 0000000000..dc0db57fc0 --- /dev/null +++ b/drivers/dac_dds/include/dac_dds_params.h @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2020 Beuth Hochschule für Technik Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup drivers_periph_dac + * @{ + * + * @file + * @brief Default configuration for the DAC DDS driver + * + * @author Benjamin Valentin + * @} + */ + +#ifndef DAC_DDS_PARAMS_H +#define DAC_DDS_PARAMS_H + +#include "board.h" +#include "macros/units.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @name Set default configuration parameters for the DAC DDS driver + * @{ + */ +#ifndef DAC_DDS_PARAM_DAC +#define DAC_DDS_PARAM_DAC DAC_LINE(0) +#endif +#ifndef DAC_DDS_PARAM_TIMER +#define DAC_DDS_PARAM_TIMER (TIMER_NUMOF - 1) +#endif +#ifndef DAC_DDS_PARAM_TIMER_HZ +#define DAC_DDS_PARAM_TIMER_HZ MHZ(1) +#endif + +#ifndef DAC_DDS_PARAMS +#define DAC_DDS_PARAMS { .dac = DAC_DDS_PARAM_DAC, \ + .timer = DAC_DDS_PARAM_TIMER, \ + .timer_hz = DAC_DDS_PARAM_TIMER_HZ, \ + } +#endif +/**@}*/ + +/** + * @brief DAC DDS configuration + */ +static const dac_dds_params_t dac_dds_params[] = +{ + DAC_DDS_PARAMS +}; + +/** + * @brief DAC DDS instances + */ +#define DAC_DDS_NUMOF ARRAY_SIZE(dac_dds_params) + +#ifdef __cplusplus +} +#endif + +#endif /* DAC_DDS_PARAMS_H */ +/** @} */ diff --git a/drivers/include/dac_dds.h b/drivers/include/dac_dds.h new file mode 100644 index 0000000000..688e51c4a4 --- /dev/null +++ b/drivers/include/dac_dds.h @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2020 Beuth Hochschule für Technik Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @defgroup drivers_dac_dds DAC Direct Digital Synthesis + * @ingroup drivers_periph_dac + * + * @{ + * @file + * @brief Use a DAC to play a buffer of samples + * + * A buffer of (audio) samples can be played on a DAC with + * a given sample rate. + * To supply a steady stream of samples, a double buffer is + * used. + * While buffer A is played on the DAC the user can fill buffer + * B with the next batch of samples by calling @ref dac_dds_play + * again. Once buffer A has finished playing, buffer B will + * automatically be used instead. + * + * A callback can be registered that signals when the next buffer + * is ready to be filled. + * Do not do the actual buffer filling inside the callback as this + * is called from the timer context that is used to play the samples. + * Instead, use it to wake a thread that then provides the samples + * and calls @ref dac_dds_play. + * If the next sample buffer is already prepared, you can also call + * `dac_dds_play` within the callback, just don't do any I/O or + * heavy processing. + * + * @author Benjamin Valentin + */ + +#ifndef DAC_DDS_H +#define DAC_DDS_H + +#include +#include +#include + +#include "periph/dac.h" +#include "periph/timer.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief The callback that will be called when the end of the current sample buffer + * has been reached. + * Should be used to start filling the next sample buffer with @ref dac_dds_play. + * + * @note Will be called in interrupt context. Only use the callback to signal a + * thread. Don't directly fill the sample buffer in the callback. + */ +typedef void (*dac_dds_cb_t)(void *arg); + +/** + * @brief Configuration struct for a DAC DDS channel + * @{ + */ +typedef struct { + dac_t dac; /**< The DAC used for playing the sample */ + tim_t timer; /**< Timer used for periodic DAC events */ + uint32_t timer_hz; /**< Timer frequency, must be at least 2x sample rate */ +} dac_dds_params_t; +/** @} */ + +/** + * @brief Index of the DAC DDS channel + */ +typedef uint8_t dac_dds_t; + +/** + * @brief A sample has a resolution of 8 bit + */ +#ifndef DAC_FLAG_8BIT +#define DAC_FLAG_8BIT (0x0) +#endif + +/** + * @brief A sample has a resolution of 16 bit + */ +#ifndef DAC_FLAG_16BIT +#define DAC_FLAG_16BIT (0x1) +#endif + +/** + * @brief Initialize a DAC for playing audio samples + * A user defined callback can be provided that will be called when + * the next buffer can be queued. + * @experimental + * + * @param[in] dac The DAC to initialize + * @param[in] sample_rate The sample rate in Hz + * @param[in] flags Optional flags (@ref DAC_FLAG_16BIT) + * @param[in] cb Will be called when the next buffer can be queued + * @param[in] cb_arg Callback argument + */ +void dac_dds_init(dac_dds_t dac, uint16_t sample_rate, uint8_t flags, + dac_dds_cb_t cb, void *cb_arg); + +/** + * @brief Change the 'buffer done' callback. + * A user defined callback can be provided that will be called when + * the next buffer can be queued. + * This function can be used to change the callback on the fly. + * + * Passing in a @p cb of `NULL` can be used to only update the arg + * without updating the @p cb. Conversely, to clear the callback, both + * @p cb and @p cb_arg need to be passed in as NULL. + * @experimental + * + * @param[in] dac The DAC to configure + * @param[in] cb Called when the played buffer is done + * @param[in] cb_arg Callback argument + */ +void dac_dds_set_cb(dac_dds_t dac, dac_dds_cb_t cb, void *cb_arg); + +/** + * @brief Play a buffer of (audio) samples on a DAC + * + * If this function is called while another buffer is already + * being played, the new `buf` will be played when the current + * buffer has finished playing. + * + * The DAC implementations allows one buffer to be queued + * (double buffering). + * + * Whenever a new buffer can be queued, the @ref dac_dds_cb_t + * callback function will be executed. + * + * @experimental + * + * @param[in] dac The DAC to play the sample on + * @param[in] buf A buffer with (audio) samples + * @param[in] len Number of bytes to be played + * + * @return `true` if the buffer was queued while another + * buffer is currently playing. + * `false` if the new buffer is played immediately. + * That means playing was just started or an underrun + * occurred. + */ +bool dac_dds_play(dac_dds_t dac, const void *buf, size_t len); + +/** + * @brief Stop playback of the current sample buffer + * + * @param[in] dac The DAC to stop + */ +void dac_dds_stop(dac_dds_t dac); + +#ifdef __cplusplus +} +#endif + +#endif /* DAC_DDS_H */ +/** @} */