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

в 16:41, , рубрики: c++, ардуино головного мозга, ацп, Занимательные задачки, математика, ненормальное программирование, программирование микроконтроллеров, расчёт цепей, троичная логика, цап

Расчёт цепей постоянного тока на пальцах, или давайте считать ЦАП для троичной логики

Но для начала неонки, какой же русский их не любит?

Итак, снова я со своими троичными железками, но в этой статье они выступают фоном, сегодня статья про резисторы. Запаял я было несколько платок, в которые можно воткнуть газоразрядные лампы типа ИН-12 или ИН-15, но часы делать не захотел :)

Сказ про резисторы и неонки - 1

Простейшие платки, несут на себе пару светодиодов sk6812 для подсветки баллона, десяток транзисторов и управляющие ими 595е сдвиговые регистры. Вот так выглядит платка, несущая на себе одну лампу, но при этом их можно собирать в длинную колбасу для достижения нужного количества ламп:

Сказ про резисторы и неонки - 2

Спасибо ikaktys за помощь! Одновременно с неонками развёл и запаял троичный счётчик, который я до того собирал на макетке и подробно описывал.

Сказ про резисторы и неонки - 3

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

Казалось бы, этого вполне хватает, зачем тут неонки? Один мой приятель, который очень искренне интересуется, куда меня заведёт троичная дорожка, оказался дальтоником! Таким образом, мне пришлось думать над альтернативой двухцветным светодиодам. А тут и коробка неонок лежит, и десятичное отображение удобно.

Скрестить ужа и ежа, или как подружить двоичную и троичную логики

Поскольку у меня, как известно, ардуино головного мозга, то управляю неонками я при помощи ардуины. То есть, драйвер неонок работает на двоичной логике, а мне надо выводить информацию, основанную на троичной. Городить дешифраторы мне было лень, а поскольку у меня уже есть ардуина, то я решил троичный сигнал просто-напросто завести в АЦП ардуины, благо, свободных ног у неё более чем достаточно. Затем просто смотрим, в какой трети области АЦП текущая линия, и это нам даст троичное значение внутри ардуины.

Одна только незадача: ардуина хочет измерять аналоговый сигнал между землёй и пятью вольтами, а троичный сигнал имеет разброс от минус пяти до пяти. Кстати, измерить ардуиной напряжение от -5 до 5 В бывает нужно и в других областях. Например, недавно мне понадобилось измерять силу тока в обмотках двигателя постоянного тока, и датчик холла мне выдавал аккурат сигнал от -5 до 5.

То есть, мне нужно отмасштабировать уровень напряжения в два раза и сдвинуть его в положительную область. Самый простой способ это сделать — это на каждую троичную линию повесить по вот такому резисторному делителю:

Сказ про резисторы и неонки - 4

Троичный сигнал заходит в Vin (от -5 до +5 В), ардуиновское питание — это Vref (5 В), а Vout заводится на АЦП ардуины. Тут встаёт вопрос, как выбрать необходимые номиналы резисторов, чтобы Vout находился в рабочей зоне АЦП (от 0 до 5 В).

Наверное, есть люди, которые умеют это делать чуть ли не в уме, но я к ним не отношусь, да и физику знаю только на школьном уровне. Моё сокровенное знание — это то, что розетку лизать не надо. Но я умею читать, поэтому, начитавшись википедии, вооружимся законом Ома, законом Кирхгофа и умением решать линейные уравнения.

Для начала давайте поставим задачу так: зная сопротивления R1, R2 и R3, а также напряжения Vref и Vin, найти силу тока, протекающую через каждый резистор, а заодно выходное напряжение Vout.

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

Затем запишем закон Кирхгофа для узла цепи (тот, что жирной чёрной точкой обозначен на схеме): сумма вытекающих токов равняется сумме входящих, то есть I1+I3=I2.

Затем второе правило Кирхгофа для замкнутого контура нам говорит, что сумма напряжений на резисторах равна общей ЭДС контура.

У нас можно выбрать два контура, один с общим напряжением Vref, второй с напряжением Vin. Запишем все три уравнения:

Сказ про резисторы и неонки - 5

Перепишем эту же систему в матричном виде, руками мне её решать лень, а в софте для символьных вычислений матрицы явно удобнее:

Сказ про резисторы и неонки - 6

И тогда искомые токи I1, I2 и I3 можно найти, обратив матрицу 3х3 нашей системы:

Сказ про резисторы и неонки - 7

Тогда выходное напряжение Vout можно найти через только что найденный I2:

Сказ про резисторы и неонки - 8

Это прекрасно, но вообще наша задача не найти Vout по известным сопротивлениям и Vin, но наоборот, зная диапазон Vin, подобрать сопротивления так, чтобы Vout укладывался между нулём и Vref.

Давайте подставим 5 вольт питания ардуины вместо Vref в наших уравнениях, выберем произвольно резистор R1 в 100кОм (у нас же делитель напряжения, поэтому один из резисторов мы можем выбрать сами). Затем запишем два уравнения: для Vin=-5 В Vout должен быть равен нулю, а для Vin=5 В Vout должен быть равен, например, 4.9 В. То есть, получили следующую систему уравнений, я специально ничего ещё в ней не упрощал:

Сказ про резисторы и неонки - 9

В целом получается многочленное уравнение, можно посчитать руками, но зачем? Считать буду в sage, вот тут можно исполнить нижеприведённый код:

var("R1,R2,R3,Vin,Vout,Vref")
A=matrix([[1,-1,1],[R1,R2,0],[0,R2,R3]])
b=matrix([[0],[Vin],[Vref]])
I=(A.inverse()*b).simplify_full()
I2=I[1][0]
eq1=(4.9==(I2*R2).substitute(Vin= 5,Vref=5,R1=10^5))
eq2=(0  ==(I2*R2).substitute(Vin=-5,Vref=5,R1=10^5))
solve([eq1,eq2],R2,R3)

Вот вывод команды solve:
[[R2 == 0, R3 == 0], [R2 == 2450000, R3 == 100000]]

Наши резисторы должны иметь строго положительные значения номиналов, поэтому откинем заведомо невозможные ответы. Итого, решатель нам говорит, что если мы выберем R1=R3=100 кОм, а R2=2.45 мегаома, то при питании Vref=5 В диапазон входящих напряжений Vin=[-5 В,+5 В] будет отображён в узле Vout в диапазон [0 В, 4.9 В]. Ура!

Вопрос для внимательных читателей: а почему я выбрал выходной диапазон 0-4.9 В, а не 0-5 В?

Вот код, который я использую:

Скрытый текст

#define F_CPU 16000000L

#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/atomic.h>
#include <util/delay.h>

#include <stdlib.h>
#include <stdio.h>
#include <avr/pgmspace.h> // PSTR

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

#define  INPUT2(port,pin)   DDR ## port &= ~_BV(pin) 
#define OUTPUT2(port,pin)   DDR ## port |=  _BV(pin) 
#define  CLEAR2(port,pin)  PORT ## port &= ~_BV(pin) 
#define    SET2(port,pin)  PORT ## port |=  _BV(pin) 
#define   READ2(port,pin) ((PIN ## port & _BV(pin))?1:0)

#define  INPUT(x)  INPUT2(x) 
#define OUTPUT(x) OUTPUT2(x)
#define  CLEAR(x)  CLEAR2(x)
#define    SET(x)    SET2(x)
#define   READ(x)   READ2(x)
#define  WRITE(x,b) ((b)?(SET2(x)):(CLEAR2(x)))

#define SK6812_DATA_PIN        B,0
#define SHIFT_595_DATA_PIN     B,1
#define SHIFT_595_CLOCK_PIN    B,2
#define SHIFT_595_LATCH_PIN    B,3

// IN12b: 0 1 2 3 4 5 6 7 8 9 .
// IN15a: μ n % П k M m + - P nc
uint16_t nixie_pins[] = {(1<<8), (1<<11), (1<<9), (1<<3), (1<<4), (1<<5), (1<<0), (1<<7), (1<<2), (1<<6), (1<<10)};

void push_nixie_symbol(uint8_t i) {
    uint16_t data = nixie_pins[i];
    for (int8_t j=15; j>=0; j--) {
        CLEAR(SHIFT_595_CLOCK_PIN);
        _delay_us(10);
        if ((data>>j)&1) {
            SET(SHIFT_595_DATA_PIN);
        } else {
            CLEAR(SHIFT_595_DATA_PIN);
        }
        _delay_us(10);
        SET(SHIFT_595_CLOCK_PIN);
        _delay_us(10);
    }
}

void clock_nixie_latch() {
    SET(SHIFT_595_LATCH_PIN);
    _delay_us(10);
    CLEAR(SHIFT_595_LATCH_PIN);
    _delay_us(10);
}

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

void adc_init() {
    ADMUX = (1<<REFS0); // AREF = AVcc
    ADCSRA = (1<<ADEN)|(1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0); // ADC Enable and prescaler of 128
}

uint16_t adc_read(uint8_t ch) {
    ch &= 7;                     // prevent ch being >7
    ADMUX = (ADMUX & 0xF8) | ch; // clear 3 lower bits before ORing
    ADCSRA |= (1<<ADSC);         // start single convertion
    while (ADCSRA & (1<<ADSC));  // wait for the conversion to complete
    return ADC;
}

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

void uart_write(char x) {
    while ((UCSR0A & (1<<UDRE0))==0); // wait for empty receive buffer
    UDR0 = x; // send
}

uint8_t uart_char_is_waiting() { // returns 1 if a character is waiting, 0 otherwise
    return (UCSR0A & (1<<RXC0));
}

char uart_read() {
    while (!uart_char_is_waiting());
    char x = UDR0;
    return x;
}

int uart_putchar(char c, FILE *stream __attribute__((unused))) {
    uart_write(c);
    return 0;
}

int uart_getchar(FILE *stream __attribute__((unused))) {
    return uart_read();
}

void uart_init() {
    UBRR0H = 0;        // For divisors see table 19-12 in the atmega328p datasheet.
    UBRR0L = 16;       // U2X0, 16 -> 115.2k baud @ 16MHz. 
    UCSR0A = 1<<U2X0;  // U2X0, 207 -> 9600 baud @ 16Mhz.
    UCSR0B = 1<<TXEN0; // Enable  the transmitter. Reciever is disabled.
    UCSR0C = (1<<UDORD0) | (1<<UCPHA0);
    fdevopen(&uart_putchar, &uart_getchar);
}

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

#define ASM_STRIP_PIN2(port,pin)  "I" (_SFR_IO_ADDR(PORT ## port)), "I" (pin)
#define ASM_STRIP_PIN(x) ASM_STRIP_PIN2(x)  

void __attribute__((noinline)) led_strip_write(uint8_t *colors, uint16_t count) {
    cli();
    while (count--) {
        asm volatile(
                "ld __tmp_reg__, %a0+n"
                "rcall led_strip_send_byte%=n"
                "ld __tmp_reg__, %a0+n"
                "rcall led_strip_send_byte%=n"
                "ld __tmp_reg__, %a0+n"
                "rcall led_strip_send_byte%=n"
                "rjmp led_strip_asm_end%=n" 

                "led_strip_send_byte%=:n"
                "rcall led_strip_send_bit%=n" 
                "rcall led_strip_send_bit%=n"
                "rcall led_strip_send_bit%=n"
                "rcall led_strip_send_bit%=n"
                "rcall led_strip_send_bit%=n"
                "rcall led_strip_send_bit%=n"
                "rcall led_strip_send_bit%=n"
                "rcall led_strip_send_bit%=n"
                "retn"

                "led_strip_send_bit%=:n"
                "sbi %2, %3n"
                "rol __tmp_reg__n"
                "nopn" "nopn"
                "brcs .+2n" "cbi %2, %3n"
                "nopn" "nopn" "nopn" "nopn" "nopn"
                "brcc .+2n" "cbi %2, %3n"
                "retn"
                "led_strip_asm_end%=: "
                : "=b" (colors)
                : "0" (colors),
                ASM_STRIP_PIN(SK6812_DATA_PIN)
                       );
    }
    sei();
    _delay_us(80);
}

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

#define LED_COUNT 8
uint8_t red[]   = {0,128,0,0,128,0,0,128,0,0,128,0,0,128,0,0,128,0,0,128,0,0,128,0};
uint8_t green[] = {128,0,0,128,0,0,128,0,0,128,0,0,128,0,0,128,0,0,128,0,0,128,0,0};
uint8_t gray[]  = {128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128};


int main(void) {
    OUTPUT(SHIFT_595_DATA_PIN);
    OUTPUT(SHIFT_595_CLOCK_PIN);
    OUTPUT(SHIFT_595_LATCH_PIN);
    OUTPUT(SK6812_DATA_PIN);

    CLEAR(SHIFT_595_DATA_PIN);
    CLEAR(SHIFT_595_CLOCK_PIN);
    CLEAR(SHIFT_595_LATCH_PIN);
    CLEAR(SK6812_DATA_PIN);

    adc_init();
    uart_init();
    FILE uart_stream = FDEV_SETUP_STREAM(uart_putchar, uart_getchar, _FDEV_SETUP_RW);
    stdin = stdout = &uart_stream;

    while(1) {
        uint16_t v0 = adc_read(0);
        uint16_t v1 = adc_read(1);
        uint16_t v2 = adc_read(2);
        int8_t t0 = v0<341 ? -1 : (v0>682 ? 1 : 0);
        int8_t t1 = v1<341 ? -1 : (v1>682 ? 1 : 0);
        int8_t t2 = v2<341 ? -1 : (v2>682 ? 1 : 0);
        int8_t value = t0+t1*3+t2*9;
        uint8_t ns0 = abs(value)%10;
        uint8_t ns1 = abs(value)/10;
        if (!ns1) ns1 = 10;
        uint8_t ns2 = value>0?7:(value<0?8:10);

        if (value>0) {
            led_strip_write(green, LED_COUNT);
        } else if (value<0) {
            led_strip_write(red, LED_COUNT);
        } else {
            led_strip_write(gray, LED_COUNT);
        }
        push_nixie_symbol(ns0);
        push_nixie_symbol(ns1);
        push_nixie_symbol(ns2);
        clock_nixie_latch();

        fprintf_P(&uart_stream, PSTR("%d,%d,%d,%d, %d %d %drn"), adc_read(0), adc_read(1), adc_read(2), value, ns2, ns1, ns0);
        _delay_ms(100);
    }

    return 0;
}

А вот видео работы моего троичного счётчика с десятичным отображением текущего значения на лампах, на нём хорошо видны три одинаковых делителя, заведённые на АЦП ардуины:

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

Усложняем задачу, переходим к ЦАП

Цифро-аналоговое преобразование двоичного кода

Для начала вспомним резисторную матрицу R-2R для двоичного ЦАП, она выглядит примерно вот так:

Сказ про резисторы и неонки - 10

Теория нам говорит, что если мы выберем R4=R5=R, а R1=R2=R3=R6 = 2R, то, подав на входы V1, V2, V3 три бита двоичного числа, на узле Vout мы получим аналоговый уровень, соответствующий цифровому входу.

Читать энциклопедию это хорошо, но как были получены эти номиналы R и 2R? Давайте их найдём сами. Итак, топология ЦАП нам дана, как и прежде, произвольно выберем направления протекания тока через каждый резистор.

Метод расчёта у нас ровно такой же, что и в предыдущем примере: сначала посчитаем силы тока при заданных номиналах резисторов, а затем напишем несколько уравнений, которые свяжут Vout со входами V1, V2 и V3, что нам даст нужные номиналы.

Итак, узлов у нас три и контуров тоже три, в итоге шесть уравнений:

Сказ про резисторы и неонки - 11

Перепишем в матричном виде:
Сказ про резисторы и неонки - 12

И тогда силы токов можно найти, обратив матрицу 6х6:
Сказ про резисторы и неонки - 13

Vout может быть получена как сумма падений напряжения на трёх резисторах:
Сказ про резисторы и неонки - 14

Для наглядности давайте я покажу, как выглядит Vout как функция от R1,R2,R3,R4,R5,R6 и V1,V2,V3:
Сказ про резисторы и неонки - 15

Довольно неприятное выражение, правда? Ну и бог с ним, мы же не руками считать будем. Итак, при заданных номиналах резисторов у нас имеется семь ненулевых комбинаций входящих напряжений на нашем ЦАПе. Им должно соотвеетствовать семь разных значений Vout. Это даст семь уравнений, решив которые, мы получим нужные номиналы резисторов.

Как и прежде, считать будем в sage, вот код, можно запустить в браузере.

var("R1,R2,R3,R4,R5,R6,V1,V2,V3")
A=matrix([[0,0,1,-1,0,0],[0,1,0,1,-1,0],[1,0,0,0,1,-1],[0,0,R3,R4,R5,R6],[0,R2,0,0,R5,R6],[R1,0,0,0,0,R6]])
b=matrix([[0],[0],[0],[V3],[V2],[V1]])
I=(A.inverse()*b).simplify_full()

Vo=(I[5][0]*R6+I[4][0]*R5+I[3][0]*R4).simplify_full()

eq7=(7/8==Vo.substitute(V1=1,V2=1,V3=1))
eq6=(6/8==Vo.substitute(V1=0,V2=1,V3=1))
eq5=(5/8==Vo.substitute(V1=1,V2=0,V3=1))
eq4=(4/8==Vo.substitute(V1=0,V2=0,V3=1))
eq3=(3/8==Vo.substitute(V1=1,V2=1,V3=0))
eq2=(2/8==Vo.substitute(V1=0,V2=1,V3=0))
eq1=(1/8==Vo.substitute(V1=1,V2=0,V3=0))

solve([eq1,eq2,eq3,eq4,eq5,eq6,eq7],R2,R3,R4,R5,R6)

Вот вывод команды solve (я руками выбросил все решения с отрицательными и нулевыми номиналами резисторов):
[R2 == r11, R3 == r12, R4 == -1/2*r11 + r12, R5 == -1/2*R1 + r11, R6 == R1]

Это означает, что мы можем резисторы R1, R2, R3 выбрать (почти) произвольно, и наш ЦАП будет работать корректно. Если мы их возьмём все три одного номинала, то и получим известную R-2R матрицу.

Цифро-аналоговое преобразование троичного кода

А теперь мы подошли к самому интересному, к разработке цифро-аналогового преобразователя для троичной сбалансированной системы. Насколько я знаю, этого ещё никто не делал, дураков мало :)

В общем, резисторная матрица прекрасно работает для двоичного кода, но что будет, если ей на вход подать троичный сигнал? Если V1=V2=V3=-1, то на выходе матрицы будет примерно -1, если V1=V2=V3=0, то на выходе ноль, а если V1=V2=V3=1, то на выходе примерно 1. То есть, в первом приближении матрица работает как нам надо. Давайте попробуем подогнать номиналы резисторов, чтобы она работала совсем хорошо.

Топология матрицы остаётся та же, выражение для Vout не изменяется, нам нужно только подкорректировать систему уравнений для поиска номиналов. Если раньше у нас было 7 уравнений, то сейчас будет 13. Давайте пробовать!

var("R,R1,R2,R3,R4,R5,R6,V1,V2,V3")
A=matrix([[0,0,1,-1,0,0],[0,1,0,1,-1,0],[1,0,0,0,1,-1],[0,0,R3,R4,R5,R6],[0,R2,0,0,R5,R6],[R1,0,0,0,0,R6]])
b=matrix([[0],[0],[0],[V3],[V2],[V1]])
I=(A.inverse()*b).simplify_full()

Vo=(I[5][0]*R6+I[4][0]*R5+I[3][0]*R4).simplify_full()
Vo=Vo.substitute(R1==R,R2==R,R3==R)

eq13=(26/27==Vo.substitute(V1= 1,V2= 1,V3= 1))
eq12=(24/27==Vo.substitute(V1= 0,V2= 1,V3= 1))
eq11=(22/27==Vo.substitute(V1=-1,V2= 1,V3= 1))
eq10=(20/27==Vo.substitute(V1= 1,V2= 0,V3= 1))
eq09=(18/27==Vo.substitute(V1= 0,V2= 0,V3= 1))
eq08=(16/27==Vo.substitute(V1=-1,V2= 0,V3= 1))
eq07=(14/27==Vo.substitute(V1= 1,V2=-1,V3= 1))
eq06=(12/27==Vo.substitute(V1= 0,V2=-1,V3= 1))
eq05=(10/27==Vo.substitute(V1=-1,V2=-1,V3= 1))
eq04=( 8/27==Vo.substitute(V1= 1,V2= 1,V3= 0))
eq03=( 6/27==Vo.substitute(V1= 0,V2= 1,V3= 0))
eq02=( 4/27==Vo.substitute(V1=-1,V2= 1,V3= 0))
eq01=( 2/27==Vo.substitute(V1= 1,V2= 0,V3= 0))

sln=solve([eq01,eq02,eq03,eq04,eq05,eq06,eq07,eq08,eq09,eq10,eq11,eq12,eq13],R4,R5,R6)
show(sln)

Как обычно, этот код можно запустить в браузере.

Ну слушайте, а ведь система имеет решение, если мы возьмём R1=R2=R3=R, R4=R5=4/3R а R6=2R, то у нас получится настоящий троичный ЦАП!

Теория теорией, но давайте проверять на практике

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

Сказ про резисторы и неонки - 16

На макетке сверху тактирующие треугольники, потом счётчик, на макетке снизу резисторная матрица. В этой матрице три номинала резисторов, я выбрал 1 кОм, 1.33 кОм и 2 кОм. Трёхразрядный счётчик считает от -13 до +13, на выходе я ожидаю увидеть лесенку от (примерно) -5 В до (примерно) +5 В. Тыкаюсь осциллографом:

Сказ про резисторы и неонки - 17

Отлично видно, что каждый тактирующий треугольник нам генерирует очередную ступеньку лестницы. Работает!

Бонус: насколько можно доверять математическому софту?

Мы сегодня бодро считали крокодилы всякие софтом хорошим. А вообще насколько можно доверять тому, что мы насчитали? Я по работе много считаю всякого, находил баги практически во всех математических пакетах. Вот, к примеру, раз речь идёт о sage, скриншот, который я сделал два с половиной года назад, когда отправлял багрепорт:

Сказ про резисторы и неонки - 18

Кто прав, численный интеграл функции f или его символьное вычисление? Они же даже разного знака! Сейчас на дворе конец 2017го года, можете проверить текущее состояние вещей. Версии сменяются, а ошибка по-прежнему на месте. Поэтому математическим софтом пользоваться, конечно, можно, но проверять результаты надо точно так же, как и после вывода формул на бумаге.

Автор: haqreu

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js