Обаяние замечательной схемы R2R

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Заметка посвящена тому, как повесить до семи кнопок (переключателей) на один аналоговый пин используя простую и элегантную схему R2R. Рассматривается в основном теория и как оно должно работать. Есть продолжение этой заметки - о граблях, которые разбросаны на пути практической реализации этой идеи. Те читатели, которые и без меня знают что такое R2R и как она работает, могут сразу переходить ко второй заметке.

Задача

Подключить несколько (от двух до семи) кнопок (переключателей) на один единственный аналоговый пин.

R2R схема

R2R схема – это необычайно красивое и элегантное решение для построения ЦАП, описанное в огромном количестве литературы, включая и собственно «библию» (стр. 642). Идея состоит том, чтобы комбинацию кнопок  преобразовать в аналоговый сигнал, а затем оцифровать этот сигнал, прочитав с аналогового входа контроллера. Получаем комбинацию кнопок в цифровом виде.

Отметим сразу, R2R - это не конкретная схема, а скорее идея. Чтобы понять её, рассмотрим сначала простейшую схему с одной кнопкой:

Если переключатель SA1 замкнут на землю, то на пине имеем, очевидно, потенциал земли. Если же переключатель SA1 замкнут на питание, то на пин попадает напряжения с делителя, у которого верхнее плечо – 20К (резистор R1), и нижнее – тоже 20К (последовательно резисторы R2 и R3). Т.е. не пине имеем половину напряжения питания.

Добавим в схему ещё один блок полностью идентичный тому, заключён в синий прямоугольник.

Посмотрим, что у на здесь с напряжениями:

SA1

SA2

Напряжение на пине

 Земля   Земля  Очевидно – потенциал земли, т.к.  Питание  от схемы отключено полностью
 Земля   Питание  Перерисуем схему, убрав переключатели и немного переставив детали, чтобы было легче считать (справа).  Напряжение в точке А определяется делителем в верхнем плече которого 20К (R4), а в нижнем – 12К (параллельно стоят R5+R3 и R1+R2), т.е. напряжение – 3/8 от питания. Это напряжение в свою очередь делится делителем R2 (верхнее плечо) и R1 (нижнее), т.е. на пин попадает 2/3 от 3/8 или 1/4 напряжения питания.
 Питание   Земля  На пине результат делителя с верхним плечом 20К (R1) и нижним плечом тоже 20К (R2 последовательно с параллельными R4||(R5+R3)), т.е. половина напряжения питания
 Питание   Питание  Перерисуем схему, убрав переключатели и немного переставив детали, чтобы было легче считать (справа). Напряжение в точке А определяется делителем с верхним плечом 12К (параллельно стоят R4 и R2+R1) и нижним плечом – 20К (R5+R3) и равно 5/8 напряжения питания. Это напряжение подаётся на нижнее плечо делителя  R1-R2 к верхнему плечу которого подано Vcc. Таким образом напряжение на пине будет равно 5/8 напряжения питания плюс 1/3 от (Vcc – 5*Vcc/8) = 6/8 или 3/4 напряжения питания.

Итак, что мы получили? Самая левая кнопка даёт нам 0, если замкнута на землю и половину питания, если замкнута на питание.

Вторая слева кнопка (независимо от первой!!!) даёт 0, если замкнута на землю и четверть питания, если замкнута на питание. Причём эта четверть просто прибавляется напряжению, которое получается от первой кнопки! Т.е. по напряжению на пине мы всегда можем однозначно определить, какая кнопка нажата, нажаты обе, или не нажата ни одна!

Более того, если добавить третью (четвёртую и т.д.) кнопки, то получим всё ту же картину:

Первая (самая левая) кнопка по-прежнему даёт 1/2 напряжения питания, вторая – 1/4, третья 1/8, четвёртая 1/16 и т.д.

Вообще, для этой цепочки, сколько бы кнопок мы не нарисовали, всё равно будет верно следующее:

1.  Если замкнуть на землю все кнопки, кроме n-ой слева, а n-ую слева замкнуть на питание, то напряжение на пине будет равно напряжению питания, поделённому на 2n.

2.  Если включено одновременно несколько кнопок, напряжения указанные в п.1 для каждой в отдельности, будут просто суммироваться.

Например, если на питание замкнуты первая (слева), третья и четвёртая кнопки то, то напряжение на пине будет равно 1/2 + 1/8 + 1/16  = 11/16 от напряжения питания.

Эти два утверждения несложно доказать, если, вооружившись законом Ома и правилами Кирхгофа, выписать формулу для одной кнопки, а затем, воспользовавшись математической индукцией показать, что если эта формула верна для n кнопок, то верна и для n+1 кнопки. Но это я предпочитаю оставить склонному к математике читателю для самостоятельного упражнения. Уж если авторы «библии» поступили так, то мне – грешному …

Используются резисторы двух номиналов. Сами по себе номиналы не особо важны, важно лишь, чтобы они были как можно более точными (не хуже, чем 1%) и одни были ровно вдвое больше других. А так, можно использовать от 1К / 2К, до 100К / 200К – ничего не изменится.

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

Левая кнопка  1000000000
2-ая кнопка слева  0100000000
3-я кнопка слева  0010000000
4-ая кнопка слева  0001000000
и т.д.  

(здесь и далее биты расположены по правилу MSBF, т.е. самый младший бит - самый правый)

Т.е. каждой кнопке соответствует свой бит! Таким образом, получив результат функции analogRead, нам достаточно просто проверить бит, соответствующий кнопке, что узнать нажата она или отпущена! Во как здорово!

Вот собственно и всё про замечательную схему R2R. Согласитесь, ведь красиво! Когда я впервые её увидел – просто влюбился и очень жалею, что придумал её не я.

Максимальное количество кнопок

Если мы откроем даташит, то в описании АЦП увидим, что его абсолютная погрешность ±2 LSB. На практике это означает, что полученное от analogRead число имеет право на ±2 отличаться от точного. Значит двум младшим битам доверять нельзя! Поэтому, первая идея – ставить восемь кнопок, а по двум младшим битам определять наличие и знак ошибки. В случае, если ошибка была «в плюс», то ничего не делать, а если «в минус», то прибавить 4. Но это не работает. Смотрим пример. Допустим, у нас восемь кнопок и нажаты все нечётные (а все чётные замкнуты на землю). В идеале analogRead должен вернуть нам число 680 (дв. 1010101000). Посмотрим, как будет выглядеть результат в случае ошибки на +2 LSB и на -2 LSB:

 Правильный результат - 680   1010101000 
 Ошибка: +2 LSB - 682  1010101010
 Ошибка: -2 LSB - 678  1010100110

Действительно, при положительной ошибке можно ничего не делать, а при отрицательной, прибавление к результату числа 4 спасло бы ситуацию и восстановило бы правильное значение (красные битики восстановились бы). Но!!! Внезапно!!! А как мы узнаем какая была ошибка? Положительная или отрицательная? Ээээ … А никак! Младшие то биты в которых сидит ошибка абсолютно идентичны! Т.е. в этой ситуации мы просто не можем узнать в какую сторону была ошибка и, стало быть, не можем узнать нужно ли нам прибавлять 4 к результату или не нужно!

Так что, восемь  кнопок – многовато!

А вот семь или меньше кнопок – вполне возможно! Ведь при таком количестве кнопок, у нас справа от значащих битов образуются неиспользуемые биты, которым мы можем доверять! Для семи кнопок такой бит будет один (третий справа), для шести – два (3-й и 4-й справа), для пяти – три и т.д. Более того, эти самые неиспользуемые. Но доверительные биты дадут на индикатор в какую сторону была ошибка!

Если ошибка была в «плюс» (или не было вовсе), то они все будут нулями. Если же ошибка была «в минус», они все будут единицами! В последнем случае, нам просто нужно взять число, образованное используемыми битами (т.е. взять слева количество битов, равное количеству кнопок), прибавить к этому числу единицу и получи правильный результат.

Давайте проверим, допустим, у нас шесть кнопок и, как и раньше, на питание замкнуты все нечётные, а на землю все чётные. В идеале мы должны получить число 672 (дв. 1010100000). Неиспользуемых, но надёжных битов будет два – они обозначены зелёным цветом:

 Правильный результат - 672   1010100000 
 Ошибка: +2 LSB - 674  1010100010
 Ошибка: -2 LSB - 670  1010011110

Как видите, всё получилось. В случае положительной ошибки, когда зелёные биты равны 0 – не делаем ничего. А в случае отрицательной ошибки, когда зелёные биты равны 1 – просто берём значащую часть результат (левые 6 бит) и прибавляем к ней 1. В результате, красные ошибочные биты исправятся и всё станет на свои места – результату можно доверять.

Вывод: данную технику можно использовать не более, чем для семи кнопок.

Реализация

Но, к делу. Мне нужен был четырёхканальный выключатель для дистанционного включения праздничной иллюминации во дворе моего дома. Схема R2R на четыре кнопки выглядит, очевидно, вот так:

Из неё получилась вот такая плата (кнопки расположены зеркально, т.е. самая старшая - справа, а не слева, как на схеме - мне так было удобнее):

Плата для 4-кнопочного R2R модуля

А из платы - вот такой модуль:

 

Архитектура приложения была выбрана такая:

1. Имеем переменную, в которой хранится текущее состояние всех кнопок в виде числа от 0 до 15 (для четырёх это покрывает все возможные сочетания);

2. Имеем переменную candidate – в ней хранится новое состояние кнопок, которое мы уже прочитали, но оно ещё не стало «текущим состоянием» (или равно ему);

3. С периодичностью 10-15 миллисекунд

3.1. читаем напряжение на пине функцией analogRead;

3.2. округляем ошибки АЦП, сдвигаем на 6 бит вправо и получаем число от 0 до 15 (переменная newState);

3.3. Если newState равна переменной candidate, но при этом не равна текущему состоянию кнопок – значит мы получили новое состояние! Трубим во все колокола (флаг изменения, call-back), и запоминаем новое текущее состояние.

3.4. Независимо от верности условия в п.3.3, присваиваем candidate = newState

Собственно всё. При таком подходе вновь прочитанное состояние кнопок становится текущим только в том случае, если оно дважды одинаково прочиталось с интервалов в наши 10-15 мс. Это даёт защиту от считывания переходных состояний (когда напряжение начало повышаться/понижаться, но еще не успело дойти до нужного уровня), а также, в известной мере, от дребезга кнопок.

Осталось только пояснить, как была устроена фильтрация ошибок из п. 3.2.

Поскольку мы всегда имеем 6 младших бит, которые по идее должны быть нулями, и именно в них и сидит погрешность измерения, было решено поступать просто. Всего в шести битах может поместиться число от 0 до 63, значит, если там число меньше 32, просто отбрасываем эти биты (округляем вниз), а если больше 31, то к результату, полученному после сдвига вправо, прибавляем единицу (округляем вверх). Это примерно то, что мы описывали выше, только чуть проще. Идея настолько проста, что программа оказалась в разы короче этого описания.

#define	TOTAL_BUTTONS	4

#define	RIGHT_BITS	(10 - TOTAL_BUTTONS)
#define	RIGHT_MIDDL	(1 << (RIGHT_BITS-1))

...............................

static uint8_t getButtonsMask(register const uint16_t adcValue) {
	register const uint8_t res = adcValue >> RIGHT_BITS;
	return (adcValue & RIGHT_MIDDL) ? (res + 1) : res;
}

И на самом деле всё это отлично заработало! 

Но, потоптавшись немного вокруг работающего модуля, я вдруг ощутимо получил по лбу, от валявшихся вокруг и доселе незамеченных граблей. Работать-то схема работала, хорошо работала, как и задумывалось, но ... впрочем. это уже совсем другая история.

inspiritus
Offline
Зарегистрирован: 17.12.2012

Единожды вступив в ЭТО , берегусь отныне.

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

Один магазин на 12 кнопок аналоговую клавиатуру продаеть. 

https://ru.aliexpress.com/item/Button-Keypad-3x4-module/32530917534.html

Правда, руки еще не дошли протестировать, надежно ли кнопки ловятся. Там интервал между кнопками маленький, может и с ошибками распознаваться.  На выходных подключу, проверю

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

По идее, должна нормально работать, достаточные там интервалы, Другое дело, что там невозможно распозназнать комбинации одновременно нажатых клавиш, а мне это было важно (я ж делал выключатель освещения). В R2R-то как раз главная фишка в том, что это можно.

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

между последними кнопками интервал всего 20 попугаев, а погрешность +-5 написано.  В выходные проверю. :)

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Ну и хватит. Окруллять до ближайшего кратного 20. Вообще 20 попугаев - это очень много, должно быть всё нормально.

Другое дело, что комбинации одновременно нажатых кнопок она не показывает (и не может показать в принципе), но что-то же она покажет, если одновременно нажать. Думаю, там кошмар начнётся - несколько одновременно нажатых кнопок будут выглядеть как одна, причём совершенно другая :)  Вот это внимательно посмотрите.

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

Спасибо Вам, за Ваши замечательные статьи и уроки. 

Voodoo Doll
Voodoo Doll аватар
Offline
Зарегистрирован: 18.09.2016

Тут кем-то обсуждалась проблема когда кнопка разомкнула средний от плюса но ещё не замкнула на минус, так вот, даю бесплатную подсказку: вас избавит от этой проблемы любое нечто, имеющее комплементарный выход (555 например, или половина LM393).

Logik
Offline
Зарегистрирован: 05.08.2014

От этой проблемы изибавляет умение составлять алгоритмы и  реализовывать их. Момент "когда кнопка разомкнула средний от плюса но ещё не замкнула на минус" кратковременный и это его отличает от стационарных. Малтого идея оцифровывать напряжение от высокоомного источника черевата ловлей помех, что тоже требует програмного умения или за его отсутствием - паяния конденсаторов.

ПС. Солидарен с inspiritus.

Ворота
Ворота аватар
Offline
Зарегистрирован: 10.01.2016

inspiritus пишет:

Единожды вступив в ЭТО , берегусь отныне.

Logik пишет:

ПС. Солидарен с inspiritus.

Не умеете их готовить?

Logik
Offline
Зарегистрирован: 05.08.2014

Хотим без гемороя обойтись

qwone
qwone аватар
Offline
Зарегистрирован: 03.07.2016

Logik пишет:

Хотим без гемороя обойтись

Правильное питание залог хорошего стула.  А по теме.  Или у вас несколько независимых кнопок, то обрабатывать их надо независимо. Или консоль ввода , тогда надо писать аналог serial. 

Logik
Offline
Зарегистрирован: 05.08.2014

qwone пишет:

Logik пишет:

Хотим без гемороя обойтись

Правильное питание залог хорошего стула.

Стул не комфортно. Попробуйте кресло, от питания не зависит. 

qwone пишет:

  А по теме.  Или у вас несколько независимых кнопок, то обрабатывать их надо независимо. Или консоль ввода , тогда надо писать аналог serial. 

Уверены что это было по теме? ;)