Найти тему
Мастерю понемногу.

Raspberi Pi Pico (RP2040). Непрерывная работа АЦП, библиотека ADCInput с использованием интерфейса DMA.

Распиновка платы.
Распиновка платы.

Микроконтроллер RP2040 оснащен встроенным аналого-цифровым преобразователем (АЦП) со следующими функциями:

• SAR-АЦП

• 500 кС/с (с использованием независимого тактового сигнала частотой 48 МГц)

• 12-разрядный (9 ENOB)

• Пять входных мультиплексоров:

° Для входов, которые доступны на выводах пакета, совместно используемых с GPIO[29:26]

◦ Один вход предназначен для внутреннего датчика температуры

• Четырехэлементный приемный модуль FIFO.

• Генерация прерываний.

• Интерфейс DMA

Цифровой контроллер управляет работой АЦП RP2040 и предоставляет дополнительные функциональные возможности:
• Режим однократного или непрерывного преобразования;
• Дискретизация FIFO с интерфейсом DMA;
• Синхронизирующий таймер (16 целых бит, 8 дробных бит) для настройки частоты дискретизации в режиме свободного хода;
• Циклическая выборка нескольких каналов в режиме свободного хода;
• Дополнительный сдвиг вправо на 8 бит в режиме непрерывного захвата, чтобы сэмплы можно было записывать в байтовый буфер в системной памяти.

АЦП SAR (аналого-цифровой преобразователь регистра последовательного приближения) представляет собой комбинацию цифрового контроллера
и аналоговой схемы, как показано на рисунке 115.

Описание работы АЦП из даташита (переведено через Яндекс переводчик).
Для АЦП требуется тактовая частота 48 МГц ( clk_adc ), которая может поступать от USB PLL. Для получения выборки требуется 96 тактовых
циклов (96 x 1/48 МГц) = 2 мкс на выборку (500 Кбит/с). Перед включением АЦП необходимо правильно настроить тактовую частоту.
Как только блок АЦП будет оснащен тактовым сигналом и его сброс будет отменен, запись 1 в CS.EN запустит короткую
внутреннюю последовательность включения аналогового оборудования АЦП. Через несколько тактов значение CS.READY повысится,
указывая на то, что АЦП готов к началу первого преобразования.
АЦП можно снова отключить в любое время, отключив CS.EN для экономии электроэнергии. CS.EN не включает
источник смещения датчика температуры (см. раздел 4.9.4). Это регулируется отдельно.
Вход АЦП является емкостным, и при выборке на вход подается около 1 ПФ (к этому добавляется дополнительная емкость извне АЦП, такая как упаковка и прокладка печатной платы). Эффективное сопротивление, даже при дискретизации со скоростью 500 Кбит/с, превышает 100 Ком, и для измерений на постоянном токе не требуется буферизация.
4.9.3.3. Выборка нескольких входов
CS.RROBIN позволяет АЦП производить выборку нескольких входов с чередованием, выполняя непрерывную выборку.
Каждый бит в RROBIN соответствует одному из пяти возможных значений CS.AINSEL. Когда АЦП завершит преобразование, CS.ANSELL автоматически перейдет к следующему входному сигналу, соответствующий бит которого установлен в ROBIN.
Функция циклической выборки отключается путем записи всех нулей в CS.ROBIN.
Например, если AINSEL изначально равен 0, а ROBYN установлен в 0x06 (установлены биты 1 и 2), АЦП будет производить выборку каналов в
следующем порядке:
1. Канал 0
2. Канал 1
3. Канал 2
4. Канал 1
5. Канал 2
6. Канал 1...
4.9.3.4. Выборочный FIFO
Выборки АЦП могут считываться непосредственно из регистра результатов или сохраняться в локальном FIFO с 4 записями и считываться из FIFO. Операцией FIFO управляет регистр FCS.
Если FCS.Если значение EN задано, результат каждого преобразования АЦП записывается в FIFO. Программный обработчик прерываний или RP2040
DMA может считывать эту выборку из FIFO при получении уведомления от сигналов IRQ или DREQ от АЦП. В качестве альтернативы программное обеспечение может опрашивать биты состояния в FCS, чтобы дождаться появления каждого образца.
Если в момент завершения преобразования FIFO заполнен, устанавливается флажок фиксированной ошибки FCS.OVER. Текущее содержимое FIFO не изменяется в результате этого события, но любое преобразование, завершенное при заполнении FIFO, будет потеряно.
Существуют два флага, которые управляют данными, записываемыми в FIFO с помощью АЦП:
• Функция FCS.SHIFT приводит к сдвигу данных FIFO вправо на восемь бит (т.е. биты FIFO 7:0 являются битами результата преобразования 11:4). Это подходит для передачи 8-битного DMA в байтовый буфер в памяти, обеспечивая более глубокий захват буферов за счет некоторой точности.
• FCS.ERR установит флаг в 12-м бите каждой записи FIFO, показывая, что произошла ошибка преобразования, т.е. не удалось сопоставить SAR.
4.9.3.5. DMA
RP2040 DMA (раздел 2.5) может извлекать дискреты АЦП из выборочного FIFO, выполняя обычное считывание из регистра FIFO, отображаемого в памяти, в соответствии с сигналом запроса системных данных ADC_DREQ. Необходимо учитывать следующее:
• Функция выборочного преобразования FIFO должна быть включена (FCS.RU), чтобы в нее записывались выборки; функция FIFO по умолчанию отключена, чтобы она случайно не заполнялась при использовании АЦП для однократных преобразований.
• Через FCS.DREQ_EN должна быть включена функция подтверждения запроса данных АЦП (DREQ).
• Канал DMA, используемый для передачи, должен выбрать сигнал запроса данных DREQ_ADC (раздел 2.5.3.1).
• Пороговое значение для подтверждения DREQ (FCS.THRESH) должно быть установлено равным 1, чтобы DMA передавался, как только в FIFO появится один образец. Обратите внимание, что это также пороговое значение, используемое для подтверждения IRQ, поэтому в случаях использования, отличных от DMA, может быть предпочтительнее более высокое значение для менее частых прерываний.
• Если размер передаваемого DMA установлен равным 8 битам, чтобы DMA передавался в массив байтов в памяти, также необходимо установить FCS.SHIFT, чтобы предварительно сместить выборки FIFO на 8 значимых бит.
• Если требуется выполнить выборку нескольких входных каналов, CS.RROBIN содержит 5-битную маску для этих каналов (4 внешних входа
плюс датчик температуры). Кроме того, CS.AINSEL должен выбрать канал для первой выборки.
• Частота дискретизации АЦП (раздел 4.9.3.2) должна быть настроена перед запуском АЦП.
Как только АЦП настроен соответствующим образом, сначала следует запустить канал DMA, а затем запустить преобразование АЦП
через CS.START_MANY. После завершения DMA АЦП может быть остановлен или немедленно передан новый DMA начатый. После очистки CS.START_MANY для остановки АЦП программное обеспечение должно также опросить CS.READY, чтобы убедиться, что последнее
преобразование завершено, а затем удалить все случайные выборки из FIFO.
4.9.3.6. Прерывание может быть сгенерировано, когда уровень FIFO достигает настраиваемого порогового значения FCS.THRESH. Вывод прерывания должен быть включен через INTE. Статус можно прочитать из INTS. Прерывание устраняется путем слива FIFO до уровня ниже FCS.THRESH.
4.9.3.7. Источник питания АЦП вынесен на отдельный вывод для обеспечения фильтрации помех.

RP2040 имеет 4 аналоговых входа на пинах 26(A0), 27(A1), 28(A2), 29(A3).

Библиотека ADCInput позволяет настроить АЦП на непрерывный режим работы, считывать аналоговый сигнал последовательно с указанных пинов, сохранять данные в буфер для каждого пина через интерфейс DMA. Дополнительно используются следующие библиотеки (встроены в ядро для RP2040):

  • AudioBufferManager.h
  • hardware/adc.h
  • hardware/dma.h
  • hardware/irq.h

Минимальный код:

#include <ADCInput.h>
ADCInput adc(A0, A1); // создать обьект adc и указать какие пины опрашивать
void setup() {
Serial.begin(115200);
adc.setBuffers(4, 8); // создать 4 буфера по 8 значений
// adc.setBuffers(3, 8); // минимальные значения для создания буфера
// adc.setBuffers(8, 16); // значения по умолчанию для создания буфера
adc.setFrequency(1000) ; // установить частоту опроса пинов, Гц.
adc.begin(); // запустить АЦП.
// АЦП будет работать постоянно. Каждые 1 мс (1 / 1000 = 1 мс) считываем данные сразу с одного-четырех пинов поочередно
}
void loop() {
if (adc.available()) { // если данные в буфере доступны для чтения
// данные из буфера считываются последовательно
int A0_digital = adc.read(); // прочитать данные пина A0 из буфера
int A1_digital = adc.read(); // прочитать данные пина A1 из буфера
}
}

Пример кода с прерыванием:

/*
Задача.
Мне нужно периодически оцифровать сигнал на пинах 26 и 27 протяженностью 10 мс.
За этот период нужно произвести 100 замеров и записать их в отдельные архивы.
*/
/*
В данном примере считываются аналоговые данные с пинов 26, 27(, 28, 29), оцифровка АЦП, передача полученных данных через DMA в буфер.
Работа АЦП настроена таким образом:
АЦП работает постоянно. Опрос пинов происходит с периодом 100 микросекунд (50 мкс на два пина (25 мкс на 4 пина)). Частота опроса 10000 Гц
Считанное значение каждого пина сохраняется в буфере.
После заполнения буфера генерируется прерывание.
В прерывании устанавливается флаг готовности. В главном цикле данные из буфера считываются в массивы для каждого пина.
Дополнительно рассчитывается среднее значение напряжения по каждому пину.
*/
// напряжение на пины подается через резистивный делитель. Максимальное напряжение 36 Вольт.
// R1 - верний резистор 10000 Ом
// R2 - нижний резистор 1000 Oм
// 4095 - разрешение АЦП 12 бит
// 3300 - опорное напряжение АЦП в мВ
#define DIVIDER ((float)10000 + 1000) / 1000 / 4095 * 3300 / 1000 // рассчет напряжения в вольтах
#include <ADCInput.h> // Библиотека работы с АЦП
ADCInput adc(A0, A1 /*, A2, A3*/); // создать обьект adc и указать какие пины опрашивать
bool flag_irc = false; // флаг заполнения буфера АЦП
// функция выполняется в прерывании от АЦП
void adc_irc(void) {
flag_irc = true; // устанавливается флаг готовности
}
void setup() {
Serial.begin(115200); // активировать серийный порт
adc.setBuffers(4, 200); // для двух пинов создаем буфер по 200 значений (по 100 на каждый пин)
adc.setFrequency(10000); // установить частоту опроса пинов, Гц. (1 / 10000 = 100 мкс)
adc.onReceive(adc_irc); // определение функции которая будет работать в прерывании АЦП.
adc.begin(); // запустить АЦП. АЦП будет работать постоянно. Каждые 100 мкс срабатывает прерывание от АЦП.
}
void loop() {
unsigned long out_serial = millis(); // переменная времени вывода в серийный порт
const int out_period = 1000; // период вывода в серийный порт, мс
int kol = 0; // подсчитывается количество считываний из буфера за 1 секунду
int kol_irc = 0; // подсчитывается количество прерываний в секунду
int aliv_0 = 0; // количество значений которые можно считать из буфера. Для вывода в серийный порт
int aliv_1 = 0; // количество оставшихся значений в буфере. Для вывода в серийный порт
int arxiv_0[100]{ 0 }; // в данный архив сохраняются данные из буфера для пин 26
int arxiv_1[100]{ 0 }; // в данный архив сохраняются данные из буфера для пин 27
// int arxiv_2[100]{ 0 }; // в данный архив сохраняются данные из буфера для пин 28
// int arxiv_3[100]{ 0 }; // в данный архив сохраняются данные из буфера для пин 29
float A_float[4]{ 0.0 }; // массив с усредненными данными с АЦП с пинов 26, 27, 28, 29.
int out_all = 0; // общее количество циклов в секунду
while (1) {
out_all++;
// когда буфер на 200 значений заполнится сработает прерывание, запустится функция adc_irc(void), установится флаг flag_irc = true.
// здесь считываем данные из буфера в архивы.
if (flag_irc) {
flag_irc = false; // сбросить флаг заполнения буфера АЦП
kol_irc++; // подсчитывается количество прерываний за 1 секунду
int index = 0; // индекс массива для 100 считанных значений
aliv_0 = adc.available(); // считываем количество элементов в буфере для вывода в серийный порт
// в данном цикле считываются данные из буфера пока в нем есть значения
while (adc.available()) {
kol++; // подсчитывается количество считываний из буфера
// данные каждого пина считываются из буфера последовательным чтением через adc.read()
arxiv_0[index] = adc.read(); // считываем значение из буфера - пин 26
arxiv_1[index] = adc.read(); // считываем значение из буфера - пин 27
// arxiv_2[index] = adc.read(); // считываем значение из буфера - пин 28
// arxiv_3[index] = adc.read(); // считываем значение из буфера - пин 29
A_float[0] += (((float)arxiv_0[index] - A_float[0]) * 0.01); // рассчитываем скользящее среднее для пина // пин 26
A_float[1] += (((float)arxiv_1[index] - A_float[1]) * 0.01); // рассчитываем скользящее среднее для пина // пин 27
// A_float[2] += (((float)arxiv_2[index] - A_float[2]) * 0.01); // рассчитываем скользящее среднее для пина // пин 28
// A_float[3] += (((float)arxiv_3[index] - A_float[3]) * 0.01); // рассчитываем скользящее среднее для пина // пин 29
if (index < 99) index++;
}
aliv_1 = adc.available(); // считываем количество элементов в буфере для вывода в серийный порт
}
// здесь выводим данные из архива в серийный порт каждую секунду
if (millis() - out_serial >= out_period) {
out_serial = millis();
for (int i = 0; i < 4; i++) {
Serial.printf("%.2fV ", A_float[i] * DIVIDER); // расчитать и вывести реальное напряжение в Вольтах. Два знака после запятой
}
Serial.println();
for (int i = 0; i < 100; i++) {
Serial.printf("%d ", arxiv_0[i]); // вывести массив значений пина 26
}
Serial.println();
for (int i = 0; i < 100; i++) {
Serial.printf("%d ", arxiv_1[i]); // вывести массив значений пина 27
}
Serial.println();
/*
for (int i = 0; i < 100; i++) {
Serial.printf("%d ", arxiv_2[i]); // вывести массив значений пина 28
}
Serial.println();
for (int i = 0; i < 100; i++) {
Serial.printf("%d ", arxiv_3[i]); // вывести массив значений пина 29
}
Serial.println();
*/
Serial.printf("kol - %d\r\n", kol); // вывести количество считываний из буфера в секунду
kol = 0;
Serial.printf("aliv_0 - %d\r\n", aliv_0); // вывести количество элементов в буфере до чтения из буфера
Serial.printf("aliv_1 - %d\r\n", aliv_1); // вывести количество элементов в буфере после чтения из буфера
Serial.printf("kol_irc - %d\r\n", kol_irc); // вывести количество прерываний в секунду
kol_irc = 0;
Serial.printf("cycles - %d\r\n", out_all); // вывести общее количество циклов в секунду
out_all = 0;
Serial.println();
}
}
}

Работа программы
Работа программы

Библиотека ADCInput позволяет очень просто настроить АЦП для работы в беспрерывном режиме.

Ссылки:

ADC Input Library — Arduino-Pico 3.6.0 documentation
ADC Input
Quadrupling the sample rate of an RP2040 oscilloscope
Scoppy Oscilloscope - Part 1. Getting Started
Scoppy Oscilloscope Math Channels Tutorial
RPI Pico ADC and DMA Buffer - Raspberry Pi Forums