Введение в библиотеку SPI для Arduino с АЦП LTC1286 и ЦАП DAC714
Введение в библиотеку SPI для Arduino с примером скетча для 12-битного аналого-цифрового преобразователя LTC1286 и 16-битного цифро-аналогового преобразователя DAC714.
Об SPI
Последовательный периферийный интерфейс, более известный как SPI, был создан компанией Motorola для передачи данных между микроконтроллерами и периферийными устройствами, используя меньшее количество выводов по сравнению с параллельной шиной. SPI может использоваться для сопряжения любых возможных периферийных устройств, таких как датчики, сенсорные экраны и IMU датчики. SPI может даже использоваться для связи одного микроконтроллера с другим или для связи с физическими интерфейсами Ethernet, USB, USART, CAN и WiFi модулей. SPI чаще всего реализуется, как четырехпроводная шина с линиями для тактового сигнала, входа данных, выхода данных и выбора периферийного устройства. Линии тактового сигнала и данных используются всеми периферийными (или ведомыми) устройствами на шине, а вывод выбора ведомого служит для идентификации каждого подключенного периферийного устройства.
Все шины SPI должны содержать одно ведущее устройство (master) и одно или несколько ведомых устройств (slave). Некоторые устройства, такие как DAC714, используют дополнительные линии управления. В случае с DAC714 эта линия используется для очистки двойного буфера, позволяя использовать до трех микросхем DAC714 на одной пятипроводной шине, устраняя необходимость в дополнительной линии управления выборов ведомого устройства. Основными ограничивающими характеристиками шины SPI являются полоса пропускания и количество доступных выводов выбора ведомого устройства. Для микроконтроллеров в больших корпусах с тактовой частотой 20 МГц и выше и с сотнями выводов GPIO это вряд ли является ограничением.
Также об основных особенностях SPI можно прочитать в статье «Назад к основам: SPI (последовательный периферийный интерфейс)».
Реализация SPI
Существует два основных способа реализации связи через SPI на Arduino или любом микроконтроллере. Первый и более распространенный метод – это аппаратный контроллер SPI. Arduino поставляется с библиотекой SPI для взаимодействия с аппаратным контроллером SPI, поэтому мы будем использовать эту библиотеку в наших примерах. Другой метод – программный SPI или "побитовое управление". Побитовое управление включает в себя ручное определение всех аспектов связи через SPI программно и может быть реализовано на любом выводе, в то время как аппаратный SPI должен быть подключен к конкретным SPI выводам микроконтроллера. Программный SPI намного медленнее, чем аппаратный SPI, и может использовать на себя ценную память программ и ресурсы процессора. Однако, в некоторых случаях, когда требуется несколько шин SPI на одном микроконтроллере, или при отладке нового SPI интерфейса побитовое управление может быть очень полезным.
Когда у ведомого устройства вывод выбора ведомого устройства принимает состояние низкого логического уровня, значит, это ведомое устройство пытается отправить данные по шине SPI ведущему устройству или принять данные. Когда у ведомого устройства вывод выбора ведомого устройства находится в состоянии логической единицы, это ведомое устройство игнорирует ведущее устройство, что позволяет нескольким ведомым устройствам совместно использовать одни и те же линии данных и тактового сигнала. Для передачи данных от ведомого устройства к ведущему служит линия MISO (Master In Slave Out, вход ведущего – выход ведомого), иногда называемая SDI (Serial Data In, вход последовательных данных). Для передачи данных от ведущего устройства к периферии используется линия MOSI (Master Out Slave In, выход ведущего – вход ведомого), также известная как SDO (Serial Data Out, выход последовательных данных). И наконец, тактовые импульсы от ведущего устройства SPI идут по линии, обычно называемой SCK (Serial Clock, последовательный тактовый сигнал) или SDC (Serial Data Clock, тактовый сигнал последовательных данных). В документации на Arduino предпочтение отдается названиям MISO, MOSI и SCK, поэтому мы будем использовать их.
Начало работы
Прежде чем приступить к написанию нового кода для периферийных устройств SPI, крайне важно обратить внимание на пару элементов в техническом описании новых компонентов. Во-первых, мы должны учитывать полярность и фазу тактового сигнала по отношению к данным. Они различаются у разных устройств и разных производителей. Полярность тактового сигнала может быть высокой или низкой и, как правило, называется CPOL (Clock POLarity). Когда CPOL = 0, логическая единица указывает на тактовый цикл, и когда CPOL = 1, логический ноль указывает на тактовый цикл. Фаза тактового сигнала, обычно называемая CPHA, указывает, когда данные захватываются и продвигаются в сдвиговом регистре относительно тактового сигнала. Для CPHA = 0 данные захватываются при нарастающем фронте тактового импульса, а продвигаются при спадающем фронте; а для CPHA = 1 – наоборот. Комбинация полярности и фазы тактового сигнала дает четыре отдельных режима данных SPI. SPI режим 0: CPOL и CPHA равны 0. SPI режим 1: CPOL = 0, а CPHA = 1. SPI режим 2: CPOL = 1, а CPHA = 0. И наконец, SPI режим 3: я уверен, вы сможете догадаться, в каких состояниях будут CPOL и CPHA.
Некоторые технические описания не испльзуют названия CPOL и CPHA, разработанные Freescale. Чтобы помочь понять режимы SPI, LTC1286 использует SPI режим 2. Взгляд на временную диаграмму в техническом описании поможет вам ознакомиться с SPI режимами данных. Для справки, DAC714 использует SPI режим 0 (спецификации DAC714 и LTC1286 приведены ниже). Далее нам нужно определить, как периферийное устройство сдвигает биты. Существует два возможных варианта: MSB или LSB – идет первым старший или младший значащий бит, и установить порядок функцией setBitOrder()
. Наконец, нам нужно определить, какую тактовую частоту наше устройство может принять, и на какой тактовой частоте работает аппаратный SPI на нашей плате Arduino. В случае с Arduino Mega и платами с тактовой частотой 16 МГц по умолчанию значение тактового сигнала шины SPI составляет 4 МГц. Библиотека SPI для Arduino позволяет разделить тактовую частоту на 2, 4, 8, 16, 32, 64 и 128.
Библиотека SPI для Arduino
Библиотека SPI для Arduino за раз передает и принимает один байт (8 бит). Как мы увидим в примерах два и три, для этого требуется выполнить некоторые манипуляции с переданными и полученными байтами. Выводы аппаратного SPI на платах Arduino используются для разъема ICSP, на всех платах Arduino MOSI находится на выводе 4 ICSP разъема, MISO – на выводе 1, а SCK – на выводе 3. Если Arduino является ведущим устройством на шине SPI, любой ее вывод может использоваться в качестве вывода выбора ведомого устройства. Если Arduino является ведомым устройством на шине SPI, то для выбора ведомого должен использоваться вывод 10 на платах Uno и Duemilanove и вывод 53 на платах Mega 1280 и 2560.
Мы сфокусируемся на следующих функциях библиотеки SPI для Arduino:
SPISettings()
begin()
end()
beginTransaction()
endTransaction()
setBitOrder()
setClockDivider()
setDataMode()
transfer()
Пример 1
В примере датчика давления BarometricPressureSensor SCP1000 требует записи конкретных значений в определенные регистры для настройки SCP1000 для работы с низким уровнем шума. Этот скетч также содержит определенную команду для чтения и записи в SCP1000. Это самый важный шак во взаимодействии с периферийными SPI устройствами и требует внимательно изучения технического описания и временной диаграммы.
const byte READ = 0b11111100; // команда чтения SCP1000
const byte WRITE = 0b00000010; // команда записи SCP1000
// Настройка SCP1000 для работы с низким уровнем шума:
writeRegister(0x02, 0x2D);
writeRegister(0x01, 0x03);
writeRegister(0x03, 0x02);
// дать датчику время для настройки:
delay(100);
Пример 2
В примере 2 демонстрируется прием данных от 12-разрядного АЦП с помощью библиотеки SPI для Arduino. В качестве подопытного АЦП используется LTC1286. 1286 – это хорошо известный АЦП, который существует на рынке очень долгое время и имеет несколько аналогов. 1286 – это дифференциальный АЦП последовательного приближения, доступный в 8-выводном DIP корпусе, что делает его удобным для макетирования и прототипирования. Способ, которым мы получаем данные от LTC1286, также приведет к редкому сценарию, в котором побитовое управление менее сложно, чем использование библиотеки SPI для Arduino. Прикрепленное описание LTC1286 содержит временную диаграмму передачи данных, которая очень полезна для понимания кода. 1286 не требует настройки, а только передает данные. Это делает реализацию связи с 1286 на Arduino очень простой.
Однако, сложная часть заключается в том, как библиотека SPI будет интерпретировать то, что получила. Вызов SPI.transfer()
обычно передает команду по каналу SPI и прослушивает его на предмет получения данных. В этом случае мы ничего не передаем: SPI.transfer(0)
. Функция transfer
принимает первый байт данных и присвает его byte_0
. Первый байт данных включает все принятые данные в то время, когда на CS (выбор ведомого) был низкий логический уровень. Он включает в себя два бита данных HI-Z
, когда АЦП производит выборку аналогового напряжения для преобразования, и нулевой бит, указывающий начало пакета. Это означает, что наш первый байт будет содержать только пять полезных битов. Сразу после нашего первого вызова SPI.transfer(0)
, мы вызываем эту функцию снова и на этот раз присваиваем ее результат переменной byte_1
. byte_1
будет содержать 8 бит данных, но нам интересны только семь из них. седьмой бид будет обычно совпадать с шестому, и его можно не учитывать, так как эффективное количество бит составляет только одиннадцать из двенадцати. По этой причине справедливо рассматривать LTC1286 как 11-разрядный АЦП. После отброса ненужных битов восстанавливается аналоговое значение.
// SPI выводы
// SS вывод 48
// MISO вывод 50
// SCK вывод 52
#include <SPI.h>
const int spi_ss = 48; // вывод выбора ведомого SPI
uint8_t byte_0, byte_1; // Первый и второй байты для чтения
uint16_t spi_bytes; // Окончательное 12-разрядное сдвинутое значение
float v_out; // Напряжение с десятичной запятой
float vref = 5.0; // Опорное напряжение на выводе Vref
void setup()
{
Serial.begin(9600); // Инициализировать последовательный порт и установить скорость
pinMode(spi_ss, OUTPUT); // Установить SPI вывод выбора ведомого на выход
digitalWrite(spi_ss, HIGH); // Убедиться, что на spi_ss установлена логическая единица
SPI.begin(); // begin SPI
}
void loop()
{
// установить скорость, формат и полярность тактового сигнала/данных во время инициации SPI
SPI.beginTransaction(SPISettings(1000, MSBFIRST, SPI_MODE2));
// установить CS вывод LTC в низкий уровень для инициации выборки АЦП и передачи данных
digitalWrite(spi_ss, LOW);
byte_0 = SPI.transfer(0); // read firt 8 bits
byte_1 = SPI.transfer(0); // read second 8 bits
// установить CS вывод LTC в высокий уровень, чтобы остановить LTC от передачи нулей
digitalWrite(spi_ss, HIGH);
// закрыть SPI соединение
SPI.endTransaction();
// & B000 сбрасывает первые 3 бита (два HI-Z бита и один нулевой бит) и сдвинуть в spi_bytes
// затем мы добавляем оставшийся байт, сдвинутый вправо для удаления бита 12
spi_bytes = ( ( (byte_0 & B00011111) <<7) + (byte_1 >>1) );
// и наконец, мы преобразуем значение в вольты. 1LSB = vref/2048
v_out = vref * (float(spi_bytes) / 2048.0);
Serial.println(v_out, 3);
delay(250);
}
Пример 3
Мы увидели, как получать данные по SPI, теперь пришло время рассмотреть, как отправлять данные. В примере 3 рассматривается, как общаться с микросхемой с наследием, аналогичным LTC1286, но с совершенно противоположной функциональностью. DAC714 – это 16-разрядный цифро-аналоговый преобразователь. У DAC714 имеется дополнительный вывод связи, который включает дополнительную защелку данных. Это позволяет включать DAC714 последовательно с другими DAC714 (до двух штук) без дополнительной линии выбора ведомого. Двойной буфер DAC714 позволяет загружать два значения в DAC714 за каждый цикл. Временная диаграмма DAC714 приведена в техническом описании.
// Выводы SPI
// SS вывод 48
// MOSI вывод 51
// MISO вывод 50
// SCK вывод 52
// latch вывод 46
#include <SPI.h>
const int spi_ss = 48; // регистр сдвига A0 DAC714P
const int dac_lch = 46; // защелка ЦАП A1 DAC714
uint16_t input_0, input_1; // входные 16-битные значения
uint8_t byte_0, byte_1, byte_2, byte_3; // байты для передачи SPI
void setup()
{
Serial.begin(9600);
pinMode(spi_ss, OUTPUT);
pinMode(dac_lch, OUTPUT);
digitalWrite(spi_ss, HIGH);
digitalWrite(dac_lch, HIGH);
SPI.setDataMode(SPI_MODE0);
SPI.setBitOrder(MSBFIRST);
SPI.setClockDivider(SPI_CLOCK_DIV16);
SPI.begin();
}
void loop()
{
static uint16_t count = 0;
input_0 = count;
input_1 = -count;
count += 1;
Serial.println(input_0);
Serial.println(input_1);
//
digitalWrite(spi_ss, LOW); // A0
byte_0 = (input_1 >> 8);
byte_1 = (input_1 & 0xFF);
byte_2 = (input_0 >> 8);
byte_3 = (input_0 & 0xFF);
SPI.transfer(byte_0);
SPI.transfer(byte_1);
SPI.transfer(byte_2);
SPI.transfer(byte_3);
digitalWrite(spi_ss, HIGH);
digitalWrite(dac_lch, LOW);
digitalWrite(dac_lch, HIGH);
delay(3);
}
Мы указали параметры SPI с помщью setDataMode()
, setBitOrder()
и setClockDivider()
в void setup()
, вместо использования SPI.beginTransaction()
, просто, чтобы продемонстрировать еще один способо настройки. Снова испльзуется функция SPI.transfer()
, но на этот раз нам неинтересен прием данных. Два 16-разрядных целых числа преобразуются в четыре байта для передачи через функцию SPI.transfer()
. Сначала мы закружаем второе входное целое число, input_1
, первым потому, что оно будет зафиксировано и загружено после преобразования input_0
. Также обратите внимание, что делитель дает тактовую частоту, вероятно, намного медленную, чем максимальный тактовый сигнал, который DAC714 может принять.
Вот и всё! Надеюсь, статья оказалась полезной. Оставляйте комментарии!