Как корректно работать с АЦП ADS1220 на Ардуино

Иногда нужны точные аналого-цифровые преобразования (АЦП) на Ардуино. Например, для регистрации ЭЭГ, ЭКГ и других малых сигналов. Возможным решением может послужить модуль АЦП ADS1220.
Во-первых, ADS1220 имеет точность в 24 бит в отличии от 10 бит в Ардуино. Во-вторых, этот АЦП модуль довольно доступен и не дорог. Модули с ADS1220 выпускаются множеством производителей, но в данной статье рассматривается CJMCU, он является самой дешёвый версией (работа с другими не отличается).

Поставщика данного модуля можно узнать здесь

Содержание:

Микросхема АЦП ADS1220
Модуль CJMCU ADS1220
Подключение модуля ADS1220 к Ардуино
Библиотека Ардуино ADS1220 модуля
Использование библиотеки
Проблемы в библиотеке
Решение проблем

Микросхема АЦП ADS1220

ADS1220
Внешний вид чипа ADS1220

Микросхема ADS1220 является прецизионным 24-битным аналого-цифровым преобразователем, который имеет 4 однополюсных канала или 2 дифференциальных входа. Также он имеет малошумящий программируемый усилитель, благодаря которому можно осуществить контролируемое оконечное усиление.
Кроме того, он дает два режима преобразования: непрерывное и одиночное.
Основные особенности ADS1220:

  • 4 однополярных (или 2 дифференциальных) аналоговых входа
  • 24 битное разрешение (20 битное эффективное разрешение)
  • Низкое энергопотребление и широкий диапазон напряжений питания
  • Программируемый коэффициент усиления и частоты выборки
  • Встроенный цифровой фильтр подавления помех 50 и 60 Гц
  • Стабильные источник опорного напряжения и тактовый генератор
  • Программируемые источники тока
  • Встроенный датчик температуры
  • SPI интерфейс
  • Малогабаритный VQFN корпус для поверхностного монтажа.

Структура показана на рисунке

Как корректно работать с АЦП ADS1220 на Ардуино
Структура ADS1220

Модуль CJMCU ADS1220

Внешний вид модуля

модуль ADS1220

Схема модуля ADS1220 от CJMCU и Protocentral

Как корректно работать с АЦП ADS1220 на Ардуино

На рисунке показана схема от Protocentral, а модуль от CJMCU отличается только номиналом резисторов SPI интерфейса 24 Ом.

Подключение модуля ADS1220 к Ардуино

Для подключения к Ардуино нужно определить выводы, задействованные под SPI интерфейс. У Arduino UNO это D11, D12, D13, а в Arduino ProMicro (в моем случае) — D15, D14, D16.
Главное нужно найти выводы, связанные с контактами MOSI, MISO, SCK.
Также нужно выделить два свободных контакта ввода/вывода для управлением разрешения работы (CS) и приема сигнала о готовности данных (DRDY). Причем, DRDY лучше по возможности подключать к выводу с поддержкой прерывания. Обычно это D1, D2.
Вариант подключения выводов модуля ADS1220 к Ардуино (двух моделей) показано в таблице.

Модуль ADS1220 Arduino UNOArduino ProMicros
DRDYD2D2
MISOD12D14
MOSID11D16
SCLKD13D15
CSD7D7
CLK
DVDD5V5V
DGNDGNDGND
Соединение модуля к разным моделям Ардуино

Как установить библиотеку Ардуино ADS1220 модуля

Для ADS1220 не так уж много вариантов и самый доступный — библиотека от Protocentral. Она доступна из меню Скетч -> Подключить библиотеку -> Управление библиотеками, ввести в поиск ADS1220 и выбрать ProtoСentral ADS1220 24-bit ADS Library. Последнюю на данный момент версию 1.1.2 можно получить на GitHub
Забегая вперед, надо отметить, что она (версия 1.1.2) видимо написана в спешке и не лишена ошибок. Однако, это все же лучше, чем ничего и надеюсь данная информация дойдет до сотрудников ProtoСentral и библиотеку исправят.
Кстати, известна еще одна Ардуино библиотека для ADS1220. Работа с ней несколько сложнее, чем с версией от Protocentral и взглянув на её исходный код, заметил наличие тех же недочетов, хоть и в меньшей мере.
Но вначале нужно определить последовательность использования библиотеки.

Использование библиотеки ProtoСentral ADS1220 24-bit ADS Library

Для использования нужно осуществить следующие действия в коде:

1. Подключение библиотеки

#include <SPI.h>
#include "Protocentral_ADS1220.h"

2. Выделить представителя класса Protocentral_ADS1220

Protocentral_ADS1220 ads1220;

3. В функции setup() произвести инициализацию АЦП

ads1220.begin(ADC_CS_pin,ADC_DRDY_pin);

Здесь ADC_CS_pin — номер вывода Ардуино к которому подключен CS вход АЦП, ADC_DRDY_pin — номер вывода Ардуино к которому подключен выход DRDY АЦП.

В результате будут устанослены следующие настройки по умолчанию:

  • Входной мультиплексор работает в дифференциальном режиме, подавая AIN0 на положительный вход, а AIN1 на отрицательный (инвертирующий) вход усилителя.
  • Усилитель PGA включен
  • Коэффициент усиления PGA равно 1
  • Частота преобразования АЦП 20 Гц
  • Режим АЦП нормальный
  • Режим преобразования непрерывный
  • Температурный датчик выключен
  • Источники тока смещения (burnout) выключены
  • Генератор опорного напряжения встроенный 2.048 В
  • Цифровой фильтр на подавление 50 и 60 Гц
  • Выключатель питания (Low-Side Power Switch) открыт
  • Источник тока возбуждения выключен
  • Переключатели тока возбуждения отключены
  • Режим режима готовности данных — использование только выхода DRDY

4. Затем можно изменить настройки по своему желанию:

ads1220.set_data_rate(DR_1000SPS);//Частота выборки 20,45,90,175,330,600,1000
ads1220.set_pga_gain(PGA_GAIN_1); //Усиление 1,2,4,8,16,32,64,128
ads1220.set_FIR_Filter(FIR_OFF);//Цифровой фильтр OFF, 5060, 50Hz, 60Hz    
ads1220.set_conv_mode_continuous(); //Режим непрерывного преобразования
ads1220.set_conv_mode_single_shot(); //режим одиничного преобразования и перехода в спящий режим
ads1220.set_OperationMode(MODE_TURBO); //Режимы работы NORMAL, DUTYCYCLE, TURBO
ads1220.DRDYmode_default(); // режим DRDY по умолчанию
ads1220.DRDYmode_DOUT(); // Режим DRDY и DOUT вместе
ads1220.PGA_ON(); // Усилитель включить
ads1220.PGA_OFF(); // Усилитель выключить
ads1220.TemperatureSensorMode_disable(); // Датчик температуры выклюить
ads1220.TemperatureSensorMode_enable(); // Датчик температуры вклюить
ads1220.CurrentSources_OFF(); // Источник тока выключить
ads1220.CurrentSources_ON(); // Источник тока включить
ads1220.set_VREF(VREF_2048); // Выбор опорного напряжения 2048, REFP0, AIN0, ANALOG
ads1220.LowSideSwitch_OPEN(); // Выключатель питания открыть
ads1220.LowSideSwitch_CLOSED(); // Выключатель питания закрыть
ads1220.set_IDAC_Current(IDAC_OFF); // Выбор тока возбуждения OFF, 10, 50, 100, 250, 500, 1000, 1500
ads1220.set_IDAC1_Route(IDAC1_disable); // Переключатель тока возбуждения 1 disable, AIN0, AIN2, AIN3, REFP0, REFN0
ads1220.set_IDAC2_Route(IDAC2_disable); // Переключатель тока возбуждения 2 disable, AIN0, AIN2, AIN3, REFP0, REFN0

5. Дальнейшие команды зависят от выбранных режимов

Это режимы преобразования (непрерывного и одиночного преобразования) и настройки мультиплексора (однополярный и дифференциальный).
В итоге получаются четыре возможных конфигураций:

  • Постоянное преобразование однополярного сигнала
  • Постоянное преобразование дифференциального сигнала
  • Одиночное преобразование однополярного сигнала
  • Одиночное преобразование дифференциального сигнала

Библиотека от Protocentral дает 4 метода для чтения данных:
Read_WaitForData() — проверяет (не ждет!) установку сигнала готовности на выводе DRDY до чтения регистров данных. Если данные не готовы возвратит 0. Его можно применять со всеми конфигурациями.
Read_SingleShot_WaitForData() — перед чтением посылает команду на начало преобразования. После этого сразу проверяет готовность данных и если не готово — результат будет 0. Применяется для одиночного преобразования, т.к. после преобразования АЦП засыпает.
Read_SingleShot_SingleEnded_WaitForData(channel) — в этом режиме мультиплексор переключается на выбранный вход (channel), ждет 100 мс затем производит преобразование, ждет 100 мс и если данные готовы считывает регистр данных иначе возвратит 0. Применяется для одиночного преобразования однополярного сигнала.
Read_Data_Samples() — сразу читает данные из регистра без проверки готовности. Применяется в режиме аппаратного прерывания по сигналу DRDY.

Также можно использовать второстепенные функции:

ads1220_Reset() — сброс АЦП
Start_Conv() — запуск преобразования
select_mux_channels(channel) — выбор требуемого канала мультиплексора. Есть следующие варианты:
дифференциальные комбинации
MUX_AIN0_AIN1
MUX_AIN0_AIN2
MUX_AIN0_AIN3
MUX_AIN1_AIN2
MUX_AIN1_AIN3
MUX_AIN2_AIN3
MUX_AIN1_AIN0
MUX_AIN3_AIN2

однополярные каналы
MUX_SE_CH0
MUX_SE_CH1
MUX_SE_CH2
MUX_SE_CH3
MUX_AIN0_AVSS
MUX_AIN1_AVSS
MUX_AIN2_AVSS
MUX_AIN3_AVSS
Start_Conv() — Для запуска АЦП вручную. Данная функция должна быть выполнена для запуска непрерывного преобразования или каждый раз запускаться до чтения данных в случае одиночного преобразования. Эта команда автоматически исполняется в функциях Read_SingleShot_WaitForData() и Read_SingleShot_SingleEnded_WaitForData(channel).

Проблемы в библиотеке Protocentral ADS1220

К сожалению, данной библиотекой в исходном виде (актуально для версии 1.1.2 и ниже) нельзя полноценно воспользоваться для регистрации сигналов!
Из публикаций в сети Интернет можно встретить лишь упоминания об ошибках в примерах поздней версии 1.1.1 библиотеки. Были неточности в вычислении напряжения и дублирование кода. В частности: во втором примере из библиотеки версии 1.1.1 производился некорректный расчет напряжения, т.к. в расчетах использовался коэффициент усиления 1, а в коде задавался 32. Нужно просто корректно задать коэффициент усиления #define PGA 32 и все.
Этот пример потом вовсе убрали из библиотеки, заменив на пример использования прерывания.

Но главная проблема находится в таймингах организации управления АЦП.

Общий алгоритм работы АЦП состоит в следующих действиях:

  1. Выбор канала мультиплексором
  2. Отдача команды начала преобразования
  3. Ожидание окончания АЦП
  4. Чтение данных

Произведя профилирование кода на каждой операции для частоты АЦП 20 Гц в однополярном режиме, были получены следующие результаты: выбор канала 10 мс, старт АЦП 8 мс, АЦП 58 мс, чтение 1 мс. Итого — 67 мс на измерение одного канала, что дает частоту дискретизации не более 15 Гц! Т.е. с такой задержкой можно зарегистрировать сигнал с частотой не более 7.5 Гц!
Увеличив частоту АЦП до 1000 Гц, задержка снизилась до 18 мс, однако это все равно не решает проблему — максимальная частота получалась всего 56 Гц, что далеко от заданного 1 кГц.
Поэтому слепо использовав функции библиотеки будет получен некорректно искаженный сигнал.
Если заглянуть в исходный код библиотеки станут ясны причины проблем в основных функциях:

1. В функции смены канала используется базовая команда записи в регистр writeRegister()

void Protocentral_ADS1220::writeRegister(uint8_t address, uint8_t value)
{
    digitalWrite(m_cs_pin,LOW);
    delay(5);
    SPI.transfer(WREG|(address<<2));
    SPI.transfer(value);
    delay(5);
    digitalWrite(m_cs_pin,HIGH);
}

Здесь и находится первая причина задержки — два delay() по 5 мс. Они в сумме дают 10 мс жесткой задержки, что уже снижает максимальную частоту дискретизации до 100 Гц. Однако, требуемые тайминги в спецификации к ADS1220 не превышают 150 нс.

2. Вторая серия проблем в функции Read_WaitForData()

int32_t Protocentral_ADS1220::Read_WaitForData()
{
    static byte SPI_Buff[3];
    int32_t mResult32=0;
    long int bit24;

    if((digitalRead(m_drdy_pin)) == LOW)             //        Wait for DRDY to transition low
    {
        digitalWrite(m_cs_pin,LOW);                         //Take CS low
        delayMicroseconds(100);
        for (int i = 0; i < 3; i++)
        {
          SPI_Buff[i] = SPI.transfer(SPI_MASTER_DUMMY);
        }
        delayMicroseconds(100);
        digitalWrite(m_cs_pin,HIGH);                  //  Clear CS to high

        bit24 = SPI_Buff[0];
        bit24 = (bit24 << 8) | SPI_Buff[1];
        bit24 = (bit24 << 8) | SPI_Buff[2];                                 // Converting 3 bytes to a 24 bit int

        bit24= ( bit24 << 8 );
        mResult32 = ( bit24 >> 8 );                      // Converting 24 bit two's complement to 32 bit two's complement
    }
    return mResult32;
}

Во-первых, здесь нет ожидания окончания окончания АЦП, а только проверка через if((digitalRead(m_drdy_pin)) == LOW).
То есть если АЦП не успеет к этому времени закончиться, то результата просто не будет. С теми задержками проблема была не видна, но если их снизить — будут вылезать 0 в результатах. Поэтому нужно эту функцию постоянно вызывать для ожидания и при 0 ничего не делать или ждать сигнал DRDY до вызова.
Затем стоят два delayMicroseconds(100) — дающие добавку в 0.2 мс к общей задержке.

3. Такие же завышенные тайминги и отсутствие реального ожидания DRDY можно встретить в функциях Read_Data_Samples(), Read_SingleShot_WaitForData(), Read_SingleShot_SingleEnded_WaitForData().

4. В функции Read_SingleShot_WaitForData() присутствует потенциальная проблема.

int32_t Protocentral_ADS1220::Read_SingleShot_WaitForData(void)
{
static byte SPI_Buff[3];
int32_t mResult32=0;
long int bit24;
Start_Conv();

if((digitalRead(m_drdy_pin)) == LOW)             //        Wait for DRDY to transition low
{
    digitalWrite(m_cs_pin,LOW);                         //Take CS low
    delayMicroseconds(100);
    for (int i = 0; i < 3; i++)
    {
      SPI_Buff[i] = SPI.transfer(SPI_MASTER_DUMMY);
    }
    delayMicroseconds(100);
    digitalWrite(m_cs_pin,HIGH);                  //  Clear CS to high

    bit24 = SPI_Buff[0];
    bit24 = (bit24 << 8) | SPI_Buff[1];
    bit24 = (bit24 << 8) | SPI_Buff[2];                                 // Converting 3 bytes to a 24 bit int

    bit24= ( bit24 << 8 );
    mResult32 = ( bit24 >> 8 );                      // Converting 24 bit two's complement to 32 bit two's complement
}
return mResult32;
}

Происходит запуск АЦП Start_Conv() и затем без всякого ожидания проверка на завершение операции. Поэтому тут остается только надеяться, что АЦП успеет произвестись, иначе сбой с выдачей 0.

5. В функции Read_SingleShot_SingleEnded_WaitForData() ситуация обратная.

int32_t Protocentral_ADS1220::Read_SingleShot_SingleEnded_WaitForData(uint8_t channel_no)
{
    static byte SPI_Buff[3];
    int32_t mResult32=0;
    long int bit24;

    select_mux_channels(channel_no);
    delay(100);

    Start_Conv();
    delay(100);

    if((digitalRead(m_drdy_pin)) == LOW)             //        Wait for DRDY to transition low
    {
        digitalWrite(m_cs_pin,LOW);                         //Take CS low
        delayMicroseconds(100);
        for (int i = 0; i < 3; i++)
        {
          SPI_Buff[i] = SPI.transfer(SPI_MASTER_DUMMY);
        }
        delayMicroseconds(100);
        digitalWrite(m_cs_pin,HIGH);                  //  Clear CS to high

        bit24 = SPI_Buff[0];
        bit24 = (bit24 << 8) | SPI_Buff[1];
        bit24 = (bit24 << 8) | SPI_Buff[2];                                 // Converting 3 bytes to a 24 bit int

        bit24= ( bit24 << 8 );
        mResult32 = ( bit24 >> 8 );                      // Converting 24 bit two's complement to 32 bit two's complement
    }
    return mResult32;
}

После вызова переключения канала select_mux_channels() и после запуска АЦП Start_Conv() стоят 100 мс задержки. Это конечно даст уверенное получение сигнала готовности к моменту его проверки, но задержка получается самая большая 10 см (выбор канала) + 100 мс (задержка после выбора канала) + 100 мс (задержка после запуска АЦП) + 100 мкс (до получения данных) + 100 мкс (после получения данных) = 210.2 мс на один канал(!!!) Что дает частоту дискретизацию максимально 4.8 Гц, а в случае 4 канального преобразования частота дискретизации падает до примерно 1 Гц и о регистрации большинства сигналов можно забыть.

Решение проблемы библиотеки Ардуино от ProtoСentral

Все описанные проблемы были исправлены в исходном коде библиотеки Protocentral_ADS1220.

  • снижены тайминги записи регистра АЦП
  • снижены тайминги функций чтения данных
  • заменен алгоритм проверки сигнала готовности на полноценное ожидание в тех функциях, где это необходимо.
  • добавлены константы для работы в турбо режиме и режиме рабочего цикла

Теперь задержка после полного цикла коммутации — АЦП — чтения находится в районе 1 мс, что дает возможность получения максимальной частоты дискретизации около 1000 Гц в нормальном режиме. Один канал 1080 мкс, два канала 2336 мс, три канала 3596 мс, четыре канала 4864 мкс.

В турбо режиме на частоте 2000 Гц один канал измеряется за 568 мкс, два канала 1276 мкс, три канала 2020 мкс, четыре канала 2772 мкс.

При одиночных измерениях результаты получаются несколько хуже: нормальный режим 1 канал за 8104 мкс, турбо режим — 604 мкс.

Поэтому для регистрации сигналов предпочтителен непрерывный режим.

Т.е. на одно измерение приходится минимально 0.6 мс, а 4 канала можно оцифровать с частотой до 333 Гц.

Библиотеку можно скачать по ссылке. Установка производится копированием содержимого архива в папку Документы -> Arduino -> library

Рабочий пример использования библиотеки для регистрации 4 каналов с частотой дискретизации 200 Гц.

// Пример регистрации 4 каналов с ADS1220
// с частотой дискретизации 200 Гц
// Использование модифицированной библиотеки LD_Protocentral_ADS1220
// (с) Исаков Роман, 2021
// (с) LabData.ru
//---------------------------------------
#define SPI_CLK  15
#define SPI_MISO 14
#define SPI_MOSI 16
#define ADC_CS   7
#define ADC_DRDY 2
#define AI0 A0
#define AI1 A1
#define AI2 A2
#define AI3 A3

#include <SPI.h>
#include "LD_Protocentral_ADS1220.h"
Protocentral_ADS1220 ads1220;

int32_t adc_data;
uint8_t  Td = 5;
uint32_t ADC_t = 0;

void setup() {
  Serial.begin(9600);
  while (!Serial) {  ; } // Ожидание подключения для настоящих USB
  ads1220.begin(ADC_CS,ADC_DRDY);
  ads1220.set_data_rate(DRT_2000SPS);// Частота АЦП для турбо режима
  ads1220.set_pga_gain(PGA_GAIN_1); // Усиление 1,2,4,8,16,32,64,128
  ads1220.set_FIR_Filter(FIR_OFF);//FIR_OFF, FIR_5060, FIR_50Hz, FIR_60Hz    
  ads1220.set_conv_mode_continuous(); // Непрерывное преобразование
  //ads1220.set_conv_mode_single_shot(); // Одиночное преобразование
  ads1220.set_OperationMode(MODE_TURBO); // Турбо режим
  ads1220.Start_Conv(); // запуск преобразований
}

void loop() {
  if (millis()-ADC_t >=Td) {
    ADC_t = millis();
    ads1220.select_mux_channels(MUX_SE_CH0); // Выюор 1 канала
    int32_t adc_data=ads1220.Read_WaitForData(); // Чтение измерения
    //int32_t adc_data=ads1220.Read_SingleShot_SingleEnded_WaitForData(MUX_SE_CH0);  // Чтение 1 канала в одиночном режиме
    Serial.print(adc_data);
    Serial.print(",");
    ads1220.select_mux_channels(MUX_SE_CH1); // Выюор 2 канала
    adc_data=ads1220.Read_WaitForData(); // Чтение измерения
    //adc_data=ads1220.Read_SingleShot_SingleEnded_WaitForData(MUX_SE_CH1); // Чтение 2 канала в одиночном режиме
    Serial.print(adc_data);
    Serial.print(",");
    ads1220.select_mux_channels(MUX_SE_CH2); // Выюор 3 канала
    adc_data=ads1220.Read_WaitForData(); // Чтение измерения
    //adc_data=ads1220.Read_SingleShot_SingleEnded_WaitForData(MUX_SE_CH2); // Чтение 3 канала в одиночном режиме
    Serial.print(adc_data);
    Serial.print(",");
    ads1220.select_mux_channels(MUX_SE_CH3); // Выюор 4 канала
    adc_data=ads1220.Read_WaitForData(); // Чтение измерения
    //adc_data=ads1220.Read_SingleShot_SingleEnded_WaitForData(MUX_SE_CH3); // Чтение 4 канала в одиночном режиме
    Serial.print(adc_data);
    Serial.println();
  }
    
}

В плоттере по последовательному соединению можно увидеть результат.

Как корректно работать с АЦП ADS1220 на Ардуино

При изменении потенциометров, пропорционально меняются уровни сигналов. Сбоев и других дефектов не обнаружено.

Не забывайте подписаться на группу ВК и канал YouTub, чтобы оперативно узнать об этой разработке!

(c) Роман В. Исаков, 2021