Пошаговое руководство (с кодом)

Что приходит вам на ум, когда вы видите эту строку кода?

result |= (result & 1UL << 23 ? 0xFFUL : 0x00UL) << 24;

Если вашей первой мыслью было не «расширение знака дополнения до двух в C» и вам нравятся биты и байты, то поверьте мне, это будет интересно!

Зачем говорить с чипом?

Недавно я наткнулся на АЦП HX711, когда искал простой способ взаимодействия с аналоговыми датчиками силы с помощью Arduino. Было довольно весело реализовывать его причудливый цифровой интерфейс, и я думаю, что это идеальная тема для знакомства с цифровыми интерфейсами. Пользовательский интерфейс этого чипа прост, но при его реализации можно узнать много интересного.

HX711 преобразует маленькое дифференциальное аналоговое напряжение (обычно создаваемое аналоговыми датчиками силы) в цифровое 24-битное значение, что намного удобнее для микроконтроллеров. Как это происходит — действительно интересная тема для другого раза. На этот раз мы просто сосредоточимся на его цифровом интерфейсе и повеселимся, реализовав его с помощью Arduino! Мы будем использовать Arduino Mega2560 и HX711 на переходной плате, подключенные к тензодатчику.

От таблицы к данным

Цифровой интерфейс HX711 состоит всего из двух контактов: тактового входа (PD_SCK) и выхода данных (DOUT). Техническому описанию нужно всего несколько предложений, чтобы рассказать нам, как его использовать:

Когда выходные данные не готовы для извлечения, цифровой выходной контакт DOUT имеет высокий уровень. […] При подаче 25–27 положительных тактовых импульсов на вывод PD_SCK данные смещаются с выходного вывода DOUT. Каждый импульс PD_SCK смещает один бит, начиная со старшего бита, пока не будут смещены все 24 бита. 25-й импульс на входе PD_SCK вернет вывод DOUT обратно в высокий уровень. […] Выходные 24 бита данных имеют формат дополнения до 2.

Это впечатляюще лаконично для таблицы данных! В даташите даже есть полезная для нас небольшая диаграмма, показывающая, как выглядит связь на уровне сигнала.

Подводя итог: получение одного бита данных из HX711 работает путем установки высокого уровня на линии PD_SCK, считывания цифрового значения (высокого или низкого уровня) на линии DOUT и повторного получения низкого уровня на PD_SCK. Делая это 24 раза, мы получаем 24 бита данных целочисленного значения со знаком, начиная со старшего бита (MSB). 25-й такт завершает транзакцию. Немного похоже на SPI, но не совсем. Это наш редкий повод пойти дальше и реализовать сторону Arduino этого интерфейса с нуля, что профессионалы называют битовым ударом (я это не придумал!).

Время написать код

Как и в любой хорошей программе для Arduino, мы начинаем с присвоения значащих имен выводам, которые будем использовать:

constexpr uint8_t kClockPin = 4; // Pin 4 connected to HX711 PD_SCK.
constexpr uint8_t kDataPin = 5;  // Pin 5 connected to HX7111 DOUT.

Затем мы настраиваем эти выводы и запускаем последовательный интерфейс Arduino в функции setup(). Последовательный интерфейс вовсе не нужен для связи с HX711, но мы будем использовать его позже для построения графика считанных данных.

void setup() {
  pinMode(kDataPin, INPUT);   // kDataPin is input to the Arduino. 
  pinMode(kClockPin, OUTPUT); // kClockPin is output from the Arduino.
  Serial.begin(115200);       // Choose your favorite baudrate here.
}

Закончив эти приготовления, можно приступить к самой интересной части: написать функцию, которая побитно считывает один аналоговый сэмпл из HX711. Поскольку в C/C++ нет 24-битных типов данных, эта функция вернет следующий лучший вариант — 32-битное значение со знаком (int32_t). Он будет принимать два параметра, а именно контакты, подключенные к часам HX711 и данным. Это означает, что сигнатура его функции выглядит так:

int32_t getData(uint8_t dat_pin, uint8_t clk_pin);

Теперь давайте пройдемся по всем шагам, которые нам нужно выполнить внутри этой функции, чтобы получить искомые данные. Во-первых, нам нужно дождаться, пока данные станут доступными, на что указывает низкий уровень на выводе DATA. Мы можем сделать это с помощью цикла while, который просто ничего не делает, пока значение DATA высокое:

while (digitalRead(dat_pin) == HIGH) {}; // Wait for data.

Шаг первый, проверьте! Как только мы преодолеем этот цикл while и данные будут доступны, нам нужно вывести такт и сохранить 24 бита. Сначала мы объявляем 32-битное целое число с именем result (здесь нет места для творчества), чтобы зарезервировать место в памяти для результата. Затем мы просто тянем тактовую линию вверх и вниз 24 раза, сэмплируя линию данных между ними и сдвигая выбранный бит в правильное положение. Поскольку данные выводятся первыми со старшим битом, чем раньше бит, тем дальше влево он сдвигается. Первый бит, выходящий из DOUT, сдвигается на 23 позиции влево, второй — на 22 и т. д. вплоть до 0.

int32_t result = 0;
// Read 24 bits into result.
for (int i = 23; i >= 0; --i) {
  digitalWrite(clk_pin, HIGH);  // Clock line high.
  // Sample data line and left-shift the result to its correct location.
  result |= static_cast<int32_t>(digitalRead(dat_pin)) << i;
  digitalWrite(clk_pin, LOW);  // Clock line low.
}

Одна загвоздка здесь в том, что нам нужно преобразовать результат digitalRead() из его собственного 16-битного целочисленного представления в 32-битный тип, прежде чем сдвигать его влево. В противном случае мы попытались бы сдвинуть что-то на 23 бита влево, когда в памяти для работы всего 16 бит — рецепт несчастья.

Приведенная ниже диаграмма — это моя попытка более удобного для человека представления того, где находятся все биты в наших 32 битах или 4 байтах памяти.

Вы могли заметить вопросительные знаки в старшем байте. Мы доберемся до них чуть позже (каламбур), и здесь пригодится расширение знака.

После тактирования 24 битов данных таблица данных требует, чтобы мы еще раз прокрутили тактовую линию, так что поехали. На этот раз нет данных для чтения.

// One additional clock cycle as per the datasheet.
digitalWrite(clk_pin, HIGH);
digitalWrite(clk_pin, LOW);

Теперь поговорим о вопросительных знаках в старшем байте. Наши 24 бита данных представляют собой целое число со знаком в дополнительной нотации до двух. Нотация дополнения до двух не совсем интуитивна (может быть, это только мне кажется), но изящным следствием этого является то, что мы можем заполнить эти неизвестные биты, просто расширив в них старший известный бит, также известный как бит знака. Другими словами: Если 23-й бит справа равен 1, заполните старший байт единицами. Если он равен 0, заполните старший байт нулями.

Оказывается, мы можем сделать это всего одной замечательной строкой кода, используя битовый сдвиг и тернарный оператор (я не говорю, что вы должны делать это так, но это довольно изящно):

result |= (result & 1UL << 23 ? 0xFFUL : 0x00UL) << 24;

WTH — вполне разумная реакция на подобный код, поэтому давайте немного разберем эту строку. Левая часть оператора (между = и ?) спрашивает: установлен ли 23-й бит слева от переменной result . Это достигается путем выполнения двоичного И между результатом и 32-битным значением, в котором установлен только 23-й бит, созданным на лету путем сдвига битов. Обратите внимание, что нам нужно сообщить компилятору, что 1, которую мы сдвигаем на 23 бита, является 32-битным значением, поэтому за ним стоит UL (unsigned long). Если бит установлен, тернарный оператор оценивает байт, полный единиц (0xFF в шестнадцатеричном представлении). Если он не установлен, он оценивается как байт, полный нулей (0x00). Поскольку впереди еще один большой битовый сдвиг, эти результаты также нуждаются в суффиксе UL . Наконец, мы делаем результирующий байт тернарного оператора старшим из четырех байтов в result , сдвигая его влево на 24 бита и закрашивая старший байт в result с помощью побитовое ИЛИ.

Наконец, все, что осталось сделать для функции getData(), — это вернуть значение result. Ниже представлена ​​вся функция во всей красе (без комментариев, чтобы подчеркнуть ее лаконичность).

int32_t getData(uint8_t dat_pin, uint8_t clk_pin) {
  while (digitalRead(dat_pin)) {};
  int32_t result = 0;
  for (int i = 23; i >= 0; --i) {
    digitalWrite(clk_pin, HIGH);
    result |= static_cast<int32_t>(digitalRead(dat_pin)) << i;
    digitalWrite(clk_pin, LOW);
  }
  digitalWrite(clk_pin, HIGH);
  digitalWrite(clk_pin, LOW);
  result |= (result & 1UL << 23 ? 0xFFUL : 0x00UL) << 24;
  return result;
}

Красиво, не так ли? Если вас интересует кажущийся случайным 25-й тактовый цикл, указанный в техническом описании, есть еще кое-что: количество тактовых циклов сверх 24 (1, 2 или 3) определяет усиление HX711 и канал настройки последующего чтения. Один дополнительный цикл просто сохраняет настройку по умолчанию (канал A, усиление 128). Мы полностью упустили эту деталь здесь, но представление этого параметра в качестве параметра в подписи getData() — отличное упражнение, если вы хотите попрактиковаться!

От силы к битам к данным

Давайте теперь насладимся наградами за всю эту тяжелую работу! Все, что нам нужно сделать, это добавить еще две строки в функцию loop() нашей программы, которая будет передавать полученные данные через последовательный интерфейс:

void loop() {
  Serial.print("Force_raw:");
  Serial.println(getData(kDataPin, kClockPin));
}

Как только мы запустим Последовательный плоттер Arduino IDE (со скоростью передачи, которая соответствует той, которую мы установили ранее), мы можем поразиться силовым данным, отображаемым перед нашими глазами, представляя связь между физическим и вычислительным миром через цифровое изображение. интерфейс, который мы реализовали полностью с нуля.

Если вы проделали весь этот путь, я надеюсь, вы узнали что-то интересное по пути! Если вы хотите продолжать узнавать, как делать интересные вещи с помощью Arduino (например, создавать роботов!), посмотрите мою книгу Практическая робототехника Arduino!

PS: Если вы дочитали до этого места, но на самом деле только что пришли сюда в поисках библиотеки HX711 Arduino, пользователь GitHub Bogde предлагает вам эту действительно хорошо документированную библиотеку.