1
0
mirror of https://github.com/RIOT-OS/RIOT.git synced 2024-12-29 04:50:03 +01:00

Merge pull request #13908 from benpicco/periph/dac_play

drivers/periph_common: dac: add dac_play()
This commit is contained in:
chrysn 2020-12-11 07:31:44 +01:00 committed by GitHub
commit 272c8d29cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 721 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 */
/** @} */

View File

@ -0,0 +1,15 @@
BOARD ?= mcb2388
include ../Makefile.tests_common
BLOBS += hello.raw
USEMODULE += dac_dds
USEMODULE += shell
# Disable the greeting sample if the board has too little memory
# or flashing takes too long
ENABLE_GREETING ?= 1
CFLAGS += -DENABLE_GREETING=$(ENABLE_GREETING)
include $(RIOTBASE)/Makefile.include

File diff suppressed because one or more lines are too long

323
tests/driver_dac_dds/main.c Normal file
View File

@ -0,0 +1,323 @@
/*
* 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 tests
* @{
*
* @file
* @brief DAC (audio) test application
*
* Generates Sine, Square and Sawtooth waves using
* a DAC.
* Connect a speaker or headphones to the DAC output
* pins of your board, you should be able to hear the
* generated sounds.
*
* @author Benjamin Valentin <benpicco@beuth-hochschule.de>
*
* @}
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "mutex.h"
#include "dac_dds.h"
#include "dac_dds_params.h"
#include "shell.h"
#include "blob/hello.raw.h"
#ifndef DAC_DDS_CHAN
#define DAC_DDS_CHAN 0
#endif
#ifndef ENABLE_GREETING
#define ENABLE_GREETING 1
#endif
#ifndef min
#define min(a, b) ((a) > (b) ? (b) : (a))
#endif
#define DAC_BUF_SIZE (2048)
static bool res_16b = 0;
static unsigned sample_rate = 8000;
#define ISIN_PERIOD (0x7FFF)
#define ISIN_MAX (0x1000)
/**
* @brief A sine approximation via a fourth-order cosine approx.
* source: https://www.coranac.com/2009/07/sines/
*
* @param x angle (with 2^15 units/circle)
* @return sine value (Q12)
*/
static int32_t isin(int32_t x)
{
int32_t c, y;
static const int32_t qN = 13,
qA = 12,
B = 19900,
C = 3516;
c = x << (30 - qN); /* Semi-circle info into carry. */
x -= 1 << qN; /* sine -> cosine calc */
x = x << (31 - qN); /* Mask with PI */
x = x >> (31 - qN); /* Note: SIGNED shift! (to qN) */
x = x * x >> (2 * qN - 14); /* x=x^2 To Q14 */
y = B - (x * C >> 14); /* B - x^2*C */
y = (1 << qA) /* A - x^2*(B-x^2*C) */
- (x * y >> 16);
return c >= 0 ? y : -y;
}
/* simple function to fill buffer with samples */
typedef void (*sample_gen_t)(uint8_t *dst, size_t len, uint16_t period);
static void _fill_saw_samples_8(uint8_t *buf, size_t len, uint16_t period)
{
uint8_t x = 0;
unsigned step = 0xFF / period;
for (uint16_t i = 0; i < len; ++i) {
x += step;
buf[i] = x;
}
}
static void _fill_saw_samples_16(uint8_t *buf, size_t len, uint16_t period)
{
uint16_t x = 0;
unsigned step = 0xFFFF / period;
for (uint16_t i = 0; i < len; ++i) {
x += step;
buf[i] = x;
buf[++i] = x >> 8;
}
}
static void _fill_sine_samples_8(uint8_t *buf, size_t len, uint16_t period)
{
uint16_t x = 0;
unsigned step = ISIN_PERIOD / period;
for (uint16_t i = 0; i < period; ++i) {
x += step;
buf[i] = isin(x) >> 5;
}
for (uint16_t i = period; i < len; i += period) {
memcpy(&buf[i], &buf[0], period);
}
}
static void _fill_sine_samples_16(uint8_t *buf, size_t len, uint16_t period)
{
uint16_t x = 0;
unsigned step = ISIN_PERIOD / period;
period *= 2;
for (uint16_t i = 0; i < period; ++i) {
x += step;
uint16_t y = isin(x);
buf[i] = y;
buf[++i] = y >> 8;
}
for (uint16_t i = period; i < len; i += period) {
memcpy(&buf[i], &buf[0], period);
}
}
static void _fill_square_samples(uint8_t *buf, size_t len, uint16_t period)
{
period /= 2;
if (res_16b) {
period *= 2;
}
for (uint8_t *end = buf + len; buf < end; ) {
memset(buf, 0xFF, period);
buf += period;
memset(buf, 0x0, period);
buf += period;
}
}
static void _unlock(void *arg)
{
mutex_unlock(arg);
}
static void play_function(uint16_t period, uint32_t samples, sample_gen_t fun)
{
static uint8_t buf[DAC_BUF_SIZE];
mutex_t lock = MUTEX_INIT_LOCKED;
/* only work with whole wave periods */
uint16_t len_aligned = DAC_BUF_SIZE - DAC_BUF_SIZE % period;
/* 16 bit samples doubles data rate */
if (res_16b) {
samples *= 2;
}
/* pre-calculate buffer */
fun(buf, len_aligned, period);
/* we want to block until the next buffer can be queued */
dac_dds_set_cb(DAC_DDS_CHAN, _unlock, &lock);
while (samples) {
size_t len = min(samples, len_aligned);
samples -= len;
dac_dds_play(DAC_DDS_CHAN, buf, len);
/* wait for buffer flip */
mutex_lock(&lock);
}
}
#if IS_USED(ENABLE_GREETING)
static int cmd_greeting(int argc, char **argv)
{
(void) argc;
(void) argv;
if (sample_rate != 8000 || res_16b) {
puts("Warning: audio clip was recoded with 8bit/8000 Hz");
}
puts("Play Greeting…");
/* we only want to play a single sample */
dac_dds_set_cb(DAC_DDS_CHAN, NULL, NULL);
dac_dds_play(DAC_DDS_CHAN, hello_raw, hello_raw_len);
return 0;
}
#endif
static void _dac_init(void)
{
printf("init DAC with %d bit, %u Hz\n", res_16b ? 16 : 8, sample_rate);
dac_dds_init(DAC_DDS_CHAN, sample_rate,
res_16b ? DAC_FLAG_16BIT : DAC_FLAG_8BIT, NULL, NULL);
}
static int cmd_init(int argc, char **argv)
{
printf("argc: %d\n", argc);
if (argc < 2) {
printf("usage: %s <freq> <bit>\n", argv[0]);
return 1;
}
if (argc > 2) {
unsigned bit = atoi(argv[2]);
if (bit != 8 && bit != 16) {
printf("Only 8 and 16 bit samples supported.\n");
return 1;
}
res_16b = bit == 16;
}
sample_rate = atoi(argv[1]);
_dac_init();
return 0;
}
static int cmd_saw(int argc, char **argv)
{
if (argc < 3) {
printf("usage: %s <freq> <secs>\n", argv[0]);
return 1;
}
unsigned freq = atoi(argv[1]);
unsigned secs = atoi(argv[2]);
play_function((sample_rate + freq/2) / freq, secs * sample_rate,
res_16b ? _fill_saw_samples_16 : _fill_saw_samples_8);
return 0;
}
static int cmd_sine(int argc, char **argv)
{
if (argc < 3) {
printf("usage: %s <freq> <secs>\n", argv[0]);
return 1;
}
unsigned freq = atoi(argv[1]);
unsigned secs = atoi(argv[2]);
play_function((sample_rate + freq/2) / freq, secs * sample_rate,
res_16b ? _fill_sine_samples_16 : _fill_sine_samples_8);
return 0;
}
static int cmd_square(int argc, char **argv)
{
if (argc < 3) {
printf("usage: %s <freq> <secs>\n", argv[0]);
return 1;
}
unsigned freq = atoi(argv[1]);
unsigned secs = atoi(argv[2]);
play_function((sample_rate + freq/2) / freq, secs * sample_rate,
_fill_square_samples);
return 0;
}
static const shell_command_t shell_commands[] = {
#if IS_USED(ENABLE_GREETING)
{ "hello", "Play Greeting", cmd_greeting },
#endif
{ "init", "Initialize DAC", cmd_init },
{ "saw", "Play sawtooth wave", cmd_saw },
{ "sine", "Play Sine wave", cmd_sine },
{ "square", "Play Square wave", cmd_square },
{ NULL, NULL, NULL }
};
int main(void)
{
dac_init(DAC_DDS_PARAM_DAC);
_dac_init();
/* start the shell */
char line_buf[SHELL_DEFAULT_BUFSIZE];
shell_run(shell_commands, line_buf, SHELL_DEFAULT_BUFSIZE);
return 0;
}