drivers/dac_dds: add module to play sample buffer over a DAC

This adds an API function to play a buffer of Audio samples using a DAC.

Double buffered operation is supported by specifying a callback that will
be called when the next buffer can be queued with dac_dds_play().
This commit is contained in:
Benjamin Valentin 2020-04-19 16:48:04 +02:00
parent 7946eb766e
commit ceb3f8443a
6 changed files with 382 additions and 0 deletions

1
drivers/dac_dds/Makefile Normal file
View File

@ -0,0 +1 @@
include $(RIOTBASE)/Makefile.base

View File

@ -0,0 +1,2 @@
FEATURES_REQUIRED += periph_dac
FEATURES_REQUIRED += periph_timer_periodic

View File

@ -0,0 +1,2 @@
USEMODULE_INCLUDES_dac_dds := $(LAST_MAKEFILEDIR)/include
USEMODULE_INCLUDES += $(USEMODULE_INCLUDES_dac_dds)

143
drivers/dac_dds/dac_dds.c Normal file
View File

@ -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 <benpicco@beuth-hochschule.de>
*
* @}
*/
#include <assert.h>
#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;
}

View File

@ -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 <benpicco@beuth-hochschule.de>
* @}
*/
#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 */
/** @} */

164
drivers/include/dac_dds.h Normal file
View File

@ -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 <benpicco@beuth-hochschule.de>
*/
#ifndef DAC_DDS_H
#define DAC_DDS_H
#include <stddef.h>
#include <stdint.h>
#include <limits.h>
#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 */
/** @} */