DMA-based ADC input (microphone, analog sensor) (#1101)

Mimics the I2S/PWMAudio/Stream interface for ease of use.

* Fix non-32b DMA size transfer calculation in ABM
* Rename wasHolding to isHolding in the I2S/PWM
  It is the **current** number of bits left, not the past number.
* Add commented microphone example
* Add docs
This commit was merged in pull request #1101.
This commit is contained in:
Earle F. Philhower, III
2023-01-05 16:00:34 -08:00
committed by GitHub
parent 6bef238772
commit da26016edf
14 changed files with 15526 additions and 15114 deletions
+87
View File
@@ -0,0 +1,87 @@
ADC Input Library
=================
The ADC pins can be sampled and recorded by an application using the same
interface as the I2S or PWM Audio libraries. This allows analog devices which
need to be periodically sampled to be read by applications, easily, such as:
* Analog electret microphones
* Potentiometers
* Light dependent resistors (LDR), etc.
Up to 4 analog samples can be recorded by the hardware (``A0`` ... ``A4``), and all
recording is done at 16-bit levels (but be aware that the ADC in the Pico will only
ever return values between 0...4095).
The interface for the ``ADCInput`` device is very similar to the ``I2S`` input
device, and most code can be ported simply by instantiating a ``ADCInput``
object in lieu of an ``I2S`` input object and choosing the pins to record.
Since this uses the ADC hardware, no ``analogRead`` or ``analogReadTemp`` calls are
allowed while in use.
ADC Input API
-------------
ADCInput(pin0 [, pin1, pin2, pin3])
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Creates an ADC input object which will record the pins specified in the code.
Only pins ``A0`` ... ``A4`` can be used, and they must be specified in increasing
order (i.e. ``ADCInput(A0, A1);`` is valid, but ``ADCInput(A1, A0)`` is not.
bool setBuffers(size_t buffers, size_t bufferWords)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Set the number of DMA buffers and their size in 32-bit words. Call before
``ADCInput::begin()``.
When running at high sample rates, it is recommended to increase the
``bufferWords`` to 32 or higher (i.e. ``adcinput.setBuffers(4, 32);`` ).
bool setPins(pin_size_t pin [, pin1, pin2, pin3])
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Adjusts the pin to record. Only legal before ``ADCInput::begin()``.
bool setFrequency(long sampleRate)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Sets the ADC sampling frequency, but does not start recording (however if the
device was already running, it will wontinue to run at the new frequency). Note
that every pin requested will be sampled at this frequency, one after the other.
That is, if you have code like this:
.. code:: cpp
ADCInput adc(A0, A1);
adc.setFrequency(1000);
``A0`` will be sampled at 0ms, 1ms, 2ms, etc. and ``A1`` will be sampled at 0.5ms
1.5ms, 2.5ms, etc. Each input is sampled at the proper frequency but offset in time
since there is only one active ADC at a time.
bool begin()/begin(long sampleRate)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Start the ADC input up with the given sample rate, or with the value set
using the prior ``setFrequency`` call.
void end()
~~~~~~~~~~
Stops the ADC Input device.
int read()
~~~~~~~~~~
Reads a single sample of recorded ADC data, as a 16-bit value. When multiple pins are
recorded the first read will be pin 0, the second will be pin 1, etc. Applications need
to keep track of which pin is being returned (normally by always reading out all pins
at once). Will not return until data is available.
int available()
~~~~~~~~~~~~~~~
Returns the number of samples that can be read without potentially blocking.
void onReceive(void (\*fn)(void))
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Sets a callback to be called when a ADC input DMA buffer is fully filled.
Will be in an interrupt context so the specified function must operate
quickly and not use blocking calls like delay().
+1
View File
@@ -31,6 +31,7 @@ For the latest version, always check https://github.com/earlephilhower/arduino-p
EEPROM <eeprom>
I2S Audio <i2s>
PWM Audio <pwm>
Microphone (and Analog Sensor) Input <adc>
Serial USB and UARTs <serial>
"Software Serial" PIO UART <piouart>
Servo <servo>
+1 -2
View File
@@ -36,8 +36,7 @@ in mono mode applies.
bool setBuffers(size_t buffers, size_t bufferWords)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Set the number of DMA buffers and their size in 32-bit words as well as
the word to fill when no data is available to send to the PWM hardware.
Set the number of DMA buffers and their size in 32-bit words.
Call before ``PWMAudio::begin()``.
When running at high sample rates, it is recommended to increase the
@@ -0,0 +1,30 @@
/*
Mono analog microphone example using electret mike on A0
Run using the Arduino Serial Plotter to see waveform.
Released to the Public Domain by Earle F. Philhower, III
Wire the mike's VCC to 3.3V on the Pico, connect the mike's
GND to a convenient Pico GND, and then connect mike OUT to A0
*/
#include <ADCInput.h>
ADCInput mike(A0);
// For stereo/dual mikes, could use this line instead
// ADCInput(A0, A1);
void setup() {
Serial.begin(115200);
mike.begin(8000);
while (1) {
Serial.printf("%d\n", mike.read());
// For stereo/dual mikes, use this line instead
// Serial.printf("%d %d\n", mike.read(), mike.read());
}
}
void loop() {
/* Nothing here */
}
+25
View File
@@ -0,0 +1,25 @@
#######################################
# Syntax Coloring Map
#######################################
#######################################
# Datatypes (KEYWORD1)
#######################################
ADCInput KEYWORD1
#######################################
# Methods and Functions (KEYWORD2)
#######################################
begin KEYWORD2
end KEYWORD2
setPins KEYWORD2
setFrequency KEYWORD2
setBuffers KEYWORD2
onReceive KEYWORD2
#######################################
# Constants (LITERAL1)
#######################################
+10
View File
@@ -0,0 +1,10 @@
name=ADCInput
version=1.0
author=Earle F. Philhower, III <earlephilhower@yahoo.com>
maintainer=Earle F. Philhower, III <earlephilhower@yahoo.com>
sentence=Records ADC values (i.e. microphone, sensors) and presents an I2S-like callback/immediate read interface
paragraph=Records ADC values (i.e. microphone, sensors) and presents an I2S-like callback/immediate read interface
category=Communication
url=http://github.com/earlephilhower/arduino-pico
architectures=rp2040
dot_a_linkage=true
+172
View File
@@ -0,0 +1,172 @@
/*
ADCInput
Records ADC values (i.e. microphone, sensors) and presents an I2S-like
callback/immediate read interface
Copyright (c) 2023 Earle F. Philhower, III <earlephilhower@yahoo.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <Arduino.h>
#include "ADCInput.h"
#include <hardware/adc.h>
ADCInput::ADCInput(pin_size_t p0, pin_size_t p1, pin_size_t p2, pin_size_t p3) {
_running = false;
setPins(p0, p1, p2, p3);
_freq = 48000;
_arb = nullptr;
_cb = nullptr;
_buffers = 8;
_bufferWords = 0;
}
ADCInput::~ADCInput() {
end();
}
bool ADCInput::setBuffers(size_t buffers, size_t bufferWords) {
if (_running || (buffers < 3) || (bufferWords < 8)) {
return false;
}
_buffers = buffers;
_bufferWords = bufferWords;
return true;
}
int ADCInput::_mask(pin_size_t p) {
switch (p) {
case 26: return 1;
case 27: return 2;
case 28: return 4;
case 29: return 8;
default: return 0;
}
}
bool ADCInput::setPins(pin_size_t pin0, pin_size_t pin1, pin_size_t pin2, pin_size_t pin3) {
if (_running) {
return false;
}
_pinMask = _mask(pin0) | _mask(pin1) | _mask(pin2) | _mask(pin3);
return true;
}
bool ADCInput::setFrequency(int newFreq) {
_freq = newFreq * __builtin_popcount(_pinMask); // Want to sample all channels at given frequency
adc_set_clkdiv(48000000.0f / _freq - 1.0f);
return true;
}
void ADCInput::onReceive(void(*fn)(void)) {
_cb = fn;
if (_running) {
_arb->setCallback(_cb);
}
}
bool ADCInput::begin() {
_running = true;
_isHolding = 0;
if (!_bufferWords) {
_bufferWords = 16;
}
// Set up the GPIOs to go to ADC
adc_init();
int cnt = 0;
for (int mask = 1, pin = 26; pin <= 29; mask <<= 1, pin++) {
if (_pinMask & mask) {
if (!cnt) {
adc_select_input(pin - 26);
}
cnt++;
adc_gpio_init(pin);
}
}
adc_set_round_robin(_pinMask);
adc_fifo_setup(true, true, 1, false, false);
setFrequency(_freq);
_arb = new AudioBufferManager(_buffers, _bufferWords, 0, INPUT, DMA_SIZE_16);
_arb->begin(DREQ_ADC, (volatile void*)&adc_hw->fifo);
_arb->setCallback(_cb);
adc_fifo_drain();
adc_run(true);
return true;
}
void ADCInput::end() {
if (_running) {
_running = false;
delete _arb;
_arb = nullptr;
}
adc_run(false);
adc_fifo_drain();
}
int ADCInput::available() {
if (!_running) {
return 0;
} else {
return _arb->available();
}
}
int ADCInput::read() {
if (!_running) {
return -1;
}
if (_hasPeeked) {
_hasPeeked = false;
return _peekSaved;
}
if (_isHolding <= 0) {
_arb->read(&_holdWord, true);
_isHolding = 32;
}
int ret = _holdWord & 0x0fff;
_holdWord >>= 16;
_isHolding -= 16;
return ret;
}
int ADCInput::peek() {
if (!_running) {
return -1;
}
if (!_hasPeeked) {
_peekSaved = read();
_hasPeeked = true;
}
return _peekSaved;
}
void ADCInput::flush() {
if (_running) {
_arb->flush();
}
}
+87
View File
@@ -0,0 +1,87 @@
/*
ADCInput
Records ADC values (i.e. microphone, sensors) and presents an I2S-like
callback/immediate read interface
Copyright (c) 2023 Earle F. Philhower, III <earlephilhower@yahoo.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
#include <Arduino.h>
#include "AudioBufferManager.h"
class ADCInput : public Stream {
public:
ADCInput(pin_size_t pin0, pin_size_t pin1 = 255, pin_size_t pin2 = 255, pin_size_t pin3 = 255);
virtual ~ADCInput();
bool setBuffers(size_t buffers, size_t bufferWords);
bool setFrequency(int newFreq);
bool setPins(pin_size_t pin0, pin_size_t pin1 = 255, pin_size_t pin2 = 255, pin_size_t pin3 = 255);
bool begin(long sampleRate) {
setFrequency(sampleRate);
return begin();
}
bool begin();
void end();
// from Stream
virtual int available() override;
virtual int read() override;
virtual int peek() override;
virtual void flush() override;
// from Print, not supported
virtual size_t write(const uint8_t *buffer, size_t size) override {
(void) buffer;
(void) size;
return -1;
}
virtual size_t write(uint8_t x) override {
(void) x;
return -1;
}
virtual int availableForWrite() override {
return 0;
}
// Note that these callback are called from **INTERRUPT CONTEXT** and hence
// should be in RAM, not FLASH, and should be quick to execute.
void onReceive(void(*)(void));
private:
uint32_t _pinMask;
int _freq;
size_t _buffers;
size_t _bufferWords;
bool _running;
void (*_cb)();
bool _hasPeeked;
uint32_t _peekSaved;
uint32_t _holdWord = 0;
int _isHolding = 0;
int _mask(pin_size_t pin);
AudioBufferManager *_arb;
};
@@ -59,6 +59,7 @@ AudioBufferManager::AudioBufferManager(size_t bufferCount, size_t bufferWords, i
for (size_t i = 0; i < bufferCount; i++) {
auto ab = new AudioBuffer;
ab->buff = new uint32_t[_wordsPerBuffer];
bzero(ab->buff, _wordsPerBuffer * 4);
ab->next = nullptr;
_addToList(&_empty, ab);
}
@@ -135,10 +136,10 @@ bool AudioBufferManager::begin(int dreq, volatile void *pioFIFOAddr) {
channel_config_set_irq_quiet(&c, false); // Need IRQs
if (_isOutput) {
dma_channel_configure(_channelDMA[i], &c, pioFIFOAddr, _silence->buff, _wordsPerBuffer, false);
dma_channel_configure(_channelDMA[i], &c, pioFIFOAddr, _silence->buff, _wordsPerBuffer * (_dmaSize == DMA_SIZE_16 ? 2 : 1), false);
} else {
_active[i] = _takeFromList(&_empty);
dma_channel_configure(_channelDMA[i], &c, _active[i]->buff, pioFIFOAddr, _wordsPerBuffer, false);
dma_channel_configure(_channelDMA[i], &c, _active[i]->buff, pioFIFOAddr, _wordsPerBuffer * (_dmaSize == DMA_SIZE_16 ? 2 : 1), false);
}
dma_channel_set_irq0_enabled(_channelDMA[i], true);
__channelMap[_channelDMA[i]] = this;
@@ -264,7 +265,7 @@ void __not_in_flash_func(AudioBufferManager::_dmaIRQ)(int channel) {
}
dma_channel_set_write_addr(channel, _active[0]->buff, false);
}
dma_channel_set_trans_count(channel, _wordsPerBuffer, false);
dma_channel_set_trans_count(channel, _wordsPerBuffer * (_dmaSize == DMA_SIZE_16 ? 2 : 1), false);
dma_channel_acknowledge_irq0(channel);
if (_callback) {
_callback();
+14 -14
View File
@@ -188,9 +188,9 @@ int I2S::read() {
return _peekSaved;
}
if (_wasHolding <= 0) {
if (_isHolding <= 0) {
read(&_holdWord, true);
_wasHolding = 32;
_isHolding = 32;
}
int ret;
@@ -198,18 +198,18 @@ int I2S::read() {
case 8:
ret = _holdWord >> 24;
_holdWord <<= 8;
_wasHolding -= 8;
_isHolding -= 8;
return ret;
case 16:
ret = _holdWord >> 16;
_holdWord <<= 16;
_wasHolding -= 32;
_isHolding -= 32;
return ret;
case 24:
case 32:
default:
ret = _holdWord;
_wasHolding = 0;
_isHolding = 0;
return ret;
}
}
@@ -238,26 +238,26 @@ size_t I2S::_writeNatural(int32_t s) {
switch (_bps) {
case 8:
_holdWord |= s & 0xff;
if (_wasHolding >= 24) {
if (_isHolding >= 24) {
auto ret = write(_holdWord, true);
_holdWord = 0;
_wasHolding = 0;
_isHolding = 0;
return ret;
} else {
_holdWord <<= 8;
_wasHolding += 8;
_isHolding += 8;
return 1;
}
case 16:
_holdWord |= s & 0xffff;
if (_wasHolding) {
if (_isHolding) {
auto ret = write(_holdWord, true);
_holdWord = 0;
_wasHolding = 0;
_isHolding = 0;
return ret;
} else {
_holdWord <<= 16;
_wasHolding = 16;
_isHolding = 16;
return 1;
}
case 24:
@@ -314,13 +314,13 @@ bool I2S::read8(int8_t *l, int8_t *r) {
if (!_running || _isOutput) {
return false;
}
if (_wasHolding) {
if (_isHolding) {
*l = (_holdWord >> 8) & 0xff;
*r = (_holdWord >> 0) & 0xff;
_wasHolding = 0;
_isHolding = 0;
} else {
read(&_holdWord, true);
_wasHolding = 16;
_isHolding = 16;
*l = (_holdWord >> 24) & 0xff;
*r = (_holdWord >> 16) & 0xff;
}
+1 -1
View File
@@ -118,7 +118,7 @@ private:
bool _writtenHalf;
int32_t _holdWord = 0;
int _wasHolding = 0;
int _isHolding = 0;
void (*_cb)();
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -71,7 +71,7 @@ bool PWMAudio::setFrequency(int newFreq) {
if (fPWM > clock_get_hz(clk_sys)) {
// Need to downscale the range to hit the frequency target
float pwmMax = (float) clock_get_hz(clk_sys) /(float) _freq;
float pwmMax = (float) clock_get_hz(clk_sys) / (float) _freq;
_pwmScale = pwmMax;
fPWM = clock_get_hz(clk_sys);
} else {
@@ -166,7 +166,7 @@ size_t PWMAudio::write(int16_t val, bool sync) {
return 0;
}
// Go from signed -32K...32K to unsigned 0...64K
uint32_t sample = (uint32_t) (val + 0x8000);
uint32_t sample = (uint32_t)(val + 0x8000);
// Adjust to the real range
sample *= _pwmScale;
sample >>= 16;
+2 -2
View File
@@ -1,8 +1,8 @@
#!/bin/bash
for dir in ./cores/rp2040 ./libraries/EEPROM ./libraries/I2S ./libraries/SingleFileDrive \
./libraries/LittleFS/src ./libraries/LittleFS/examples \
./libraries/rp2040 ./libraries/SD ./libraries/ESP8266SdFat \
./libraries/LittleFS/src ./libraries/LittleFS/examples ./libraries/PWMAudio \
./libraries/rp2040 ./libraries/SD ./libraries/ESP8266SdFat ./libraries/ADCInput \
./libraries/Servo ./libraries/SPI ./libraries/Wire ./libraries/PDM \
./libraries/WiFi ./libraries/lwIP_Ethernet ./libraries/lwIP_CYW43 \
./libraries/FreeRTOS/src ./libraries/LEAmDNS ./libraries/MD5Builder \