Параллельный вывод бит с Arduino

Недавно мне пришлось решать вопрос, как подать цифровой сигнал с Arduino Pro Mini на вход параллельного цифро-аналогового преобразователя (ЦАП). С одной стороны, кажется все просто, но самом деле существуют нюансы, знание которых позволит избежать ошибок и потерю времени.

Параллельный вывод бит с Arduino

Начиная вникать в вопрос, я рассуждал следующим образом:

“Аналоговые” пины на Arduino могут принимать напряжение от 0 до опорного напряжения и преобразовывать его в цифровое значение с разрядностью в 10 бит (от 0 до 1023). Далее сигнал можно, например, отфильтровать и выводить на ЦАП. Для эксперимента можно обойтись без фильтра. ЦАП должен быть тоже 10-ти разрядным, чтобы не было потерь в качестве.

Исходя из рассуждений для эксперимента использовал 10-ти разрядный ЦАП на микросхеме КР572ПА1(pdf) (зарубежный аналог – AD7520). В качестве источника сигнала взял старенький зашумленный потенциометр на 10 кОм. Ну, и в центре внимания – Arduino Pro Mini Mega 328P с 5-ти вольтовым питанием.

Даже самая маленькая Arduino Pro Mini имеет достаточное количество цифровых выводов чтобы вывести на них 10-ти разрядное число (максимум – 1023 или 1111111111). Т.е. на каждый цифровой вывод можно вывести «1» или «0» начиная от младшего разряда (крайняя цифра справа) до старшего разряда (первая цифра слева). Для этого буду использовать цифровые выводы со второго по тринадцатый (по очереди, за исключением 9 и 10 (были свои причины 😊)).

На аналоговый вход А0 буду подавать входное напряжения с потенциометра, а на входе А1 буду отслеживать качество сигнала на выходе ЦАП. На микросхеме КР572ПА1 цифровыми входами являются выводы с 4 по 13, где 4 – старший разряд, а 13 – младший.

pinMode(2, OUTPUT); // 4вывод ЦАП - старший разряд 
pinMode(3, OUTPUT); // 5вывод ЦАП -
pinMode(4, OUTPUT); // 6вывод ЦАП -
pinMode(5, OUTPUT); // 7вывод ЦАП -
pinMode(6, OUTPUT); // 8вывод ЦАП -
pinMode(7, OUTPUT); // 9вывод ЦАП -
pinMode(8, OUTPUT); // 10вывод ЦАП -
pinMode(11, OUTPUT); // 11вывод ЦАП -
pinMode(12, OUTPUT); // 12вывод ЦАП -
pinMode(13, OUTPUT); // 13вывод ЦАП – младший (нулевой) разряд
pinMode(A0, INPUT); // сигнал с потенциометра
pinMode(A1, INPUT); // сигнал на выходе ЦАП

Также назначу две переменные:

unsigned int VHOD, VIHOD; // переменные запоминают значения входного сигнала с потенциометра и с ЦАП соответственно

Для чтения значения определенного бита можно воспользоваться функцией bitRead(). Функция bitRead() позволяет прочитать значение определенного бита в указанной переменной. В нашем случае эта переменная VHOD.
Если назначить еще 10 переменных для сохранения битов, например r0-r9, то можно записать как-то так:

VHOD = analogRead(A0);
r0 = bitRead(VHOD, 0); //младший бит
r1 = bitRead(VHOD, 1);
r2 = bitRead(VHOD, 2);
r3 = bitRead(VHOD, 3);
r4 = bitRead(VHOD, 4);
r5 = bitRead(VHOD, 5);
r6 = bitRead(VHOD, 6);
r7 = bitRead(VHOD, 7);
r8 = bitRead(VHOD, 8);
r9 = bitRead(VHOD, 9); // старший бит

digitalWrite(13, r0); //младший бит
digitalWrite(12, r1);
digitalWrite(11, r2);
digitalWrite(8, r3);
digitalWrite(7, r4);
digitalWrite(6, r5);
digitalWrite(5, r6);
digitalWrite(4, r7);
digitalWrite(3, r8);
digitalWrite(2, r9); // старший бит

Или не создавать переменных и записать сразу:

VHOD = analogRead(A0);
digitalWrite(13, bitRead(VHOD, 0));
digitalWrite(12, bitRead(VHOD, 1));
digitalWrite(11, bitRead(VHOD, 2));
digitalWrite(8, bitRead(VHOD, 3));
digitalWrite(7, bitRead(VHOD, 4));
digitalWrite(6, bitRead(VHOD, 5));
digitalWrite(5, bitRead(VHOD, 6));
digitalWrite(4, bitRead(VHOD, 7));
digitalWrite(3, bitRead(VHOD, 8));
digitalWrite(2, bitRead(VHOD, 9));

Чтобы увидеть результаты преобразований и нарисовать их плоттером, в void setup() допишем открытие последовательного порта:

Serial.begin(9600);

А в void loop() добавим:

VIHOD = analogRead(1); // сигнал, подаваемый с выхода ЦАП
Serial.println(VHOD); // выводим первичный сигнал
Serial.print(',');
Serial.println(VIHOD); // выводим преобразованный сигнал
Serial.println();

В целом получается небольшой скетч:

unsigned int VHOD, VHOD;

void setup() {
Serial.begin(9600);

pinMode(2, OUTPUT); // 4вывод ЦАП - старший разряд
pinMode(3, OUTPUT); // 5вывод ЦАП -
pinMode(4, OUTPUT); // 6вывод ЦАП -
pinMode(5, OUTPUT); // 7вывод ЦАП -
pinMode(6, OUTPUT); // 8вывод ЦАП -
pinMode(7, OUTPUT); // 9вывод ЦАП -
pinMode(8, OUTPUT); // 10вывод ЦАП -
pinMode(11, OUTPUT); // 11вывод ЦАП -
pinMode(12, OUTPUT); // 12вывод ЦАП -
pinMode(13, OUTPUT); // 13вывод ЦАП – младший (нулевой) разряд
pinMode(A0, INPUT); // сигнал с потенциометра
pinMode(A1, INPUT); // сигнал на выходе ЦАП

}
void loop() {

VHOD = analogRead(A0);
digitalWrite(13, bitRead(VHOD, 0));
digitalWrite(12, bitRead(VHOD, 1));
digitalWrite(11, bitRead(VHOD, 2));
digitalWrite(8, bitRead(VHOD, 3));
digitalWrite(7, bitRead(VHOD, 4));
digitalWrite(6, bitRead(VHOD, 5));
digitalWrite(5, bitRead(VHOD, 6));
digitalWrite(4, bitRead(VHOD, 7));
digitalWrite(3, bitRead(VHOD, 8));
digitalWrite(2, bitRead(VHOD, 9));

VIHOD = analogRead(1);
Serial.println(VHOD);
Serial.print(',');
Serial.println(VIHOD);
Serial.println();
}

Посмотрите на скриншотах графиков, что в результате получится. Синим цветом помечен сигнал от потенциометра, а красным — после ЦАП.

Параллельный вывод бит с Arduino. Вывод на цифровые пины без изменения сигнала.
Параллельный вывод бит с Arduino. Вывод на цифровые пины с изменением сигнала.

Как видно малейшее дрожание сигнала или его изменение приводит к скачкообразным изменениям на выходе ЦАП. И дело не в качестве ЦАП и не в помехах. Дело в том, что цифро-аналоговый преобразователь получает на свои входы сигнал не целиком, а последовательно, хоть и с минимальной задержкой.  В моем примере это входы КР572ПА1: 13-12-11-10-9-8-7-6-5-4, начиная с младшего разряда.

Функция digitalWrite обрабатываются процессором далеко не в один такт. DigitalWrite состоит из нескольких строк кода, которые компилируются в еще большее количество машинных команд. Каждая обрабатывается в один такт. А в скетче таких «digitalWrite» десяток. Это сбивает с толку работу ЦАП. Задержки воспринимаются как скачки значений величин вплоть от минимума до максимума. Причем не важно начинаем выводить число с старшего разряда или с младшего.
На рисунке ниже изображен скриншот сигналов при измененном порядке вывода числа с 4 по 13 вывод ЦАП:

Параллельный вывод бит с Arduino. Вывод на цифровые пины в обратной последовательности с изменением сигнала.

В результате все те же скачки.

Ситуацию можно улучшить, используя прямой вывод в порт. Согласно описанию регистров, для Arduino Pro Mini доступно три порта.
PORTD отображается на цифровые выводы Arduino от 0 до 7.
PORTB отображается на цифровые выводы Arduino от 8 до 13.
PORTC отображается на аналоговые выводы Arduino от 0 до 5 (и как я понимаю RESET).

Отсюда видно, что единственным полноценным 8-битным портом является порт D.
Программная настройка несложная. Для начала в «void setup ()» необходимо прописать какие выводы порта будут входными, какие выходными. Для этого существует регистр направления передачи данных порта D — DDRD. Например:

DDRD = B11111110; // назначает выводы Arduino 1-7 выходными, вывод 0- входным

В моем случае все выводы выходные:

DDRD = B11111111; 

За непосредственный вывод в порт значения измеряемой величины, отвечает команда

PORTD = x; // выводит значение переменной x в порт

Не стоит забывать также что измеряемая величина на аналоговом входе Arduino имеет 10-ти разрядный формат, а нам теперь придется иметь дело с 8-ми разрядным. В цикле нужно задать формулу пересчета.
Скетч с прямым выводом в порт примет следующий вид:

unsigned int VHOD_r, VHOD; // 
float VHOD8bit; // для пересчета в 8-ми разрядное число

void setup () {
Serial.begin(9600); // для построения графиков
pinMode(A1, INPUT); // здесь сигнал с выхода ЦАП
pinMode(A0, INPUT); // здесь сигнал от потенциометра
DDRD = B11111111; // назначаем все выводы порта выходными (0 – отключен)
}
void loop() {
VHOD = analogRead(0);
VHOD8bit = VHOD/4.0; // пересчитываем в восемь бит
VHOD_r = VHOD8bit; // округление до целого вниз
PORTD = VHOD_r; // выводим значение в порт
// выводим результаты на плоттер
Serial.print(VHOD);
Serial.print(',');
Serial.print(analogRead(1));
Serial.println();
}

При таком подходе у 10-ти разрядного ЦАП два младших разряда останутся не задействованными, их нужно просто заземлить. При этом опять же будут потери в точности. Но даже с учетом потерь разрядов, результат выглядит гораздо лучше:

Прямой вывод в порт Ардуино. Параллельный вывод бит с Arduino
Синий - сигнал от потенциометра, красный – он же, после ЦАП

Кстати, к моему удивлению при использовании порта D проблем с загрузкой через последовательный порт не было и еще к большему удивлению сигнал легко удалось вывести на плоттер. Дело в том, что на Arduino Pro Mini цифровые выводы 0 и 1 используются как последовательный порт передачи данных. Они же использованы при выводе информации в порт D (два младших разряда). Теоретически последовательный порт должен быть занят, и я не должен был получить картинку на плоттере. В общем конфликтов при использовании последовательного порта я не заметил.

Итак, вывод: прямой доступ к порту делает вычисление за гораздо меньшее число тактов. В результате пропадают большие скачки, результаты становятся более точными. Главное подобрать приемное устройство (в моем случае ЦАП) с одинаковым количеством разрядов на входе. Т.е. восемь разрядов на выходе восемь на входе (похоже это максимум).
Прямой доступ к порту можно и нужно использовать при параллельном выводе числа, но не более чем 8-ми разрядного.