Pull to refresh

Примитивы для реализации 1-Wire master при помощи PWM и ICP на микроконтроллерах AVR AtMega

Reading time 22 min
Views 17K
Если кто-нибудь использовал linux-драйвер шины 1-Wire на основе GPIO, то наверняка замечал, что в момент выполнения обмена данными загрузка SY% подскакивает чуть ли не до ста 100% (что вполне логично для bitbang-реализации). Конечно, можно поставить специализированный контроллер шины 1-Wire с подключением через I2C DS28E17 или использовать UART, но… Все-таки использовать linux для приложений реального времени не лучшая идея. Пусть контроллер на нем занимается высокоуровневой логикой, а весь реалтайм можно вынести на отдельный процессор. Тем более, что задач для этого отдельного процессора в моем проекте более чем достаточно.

Нет, я не буду брать распиаренные сейчас STM32 (все-таки сделал реализацию для STM8). Возьмем старую добрую AtMega328P (ну или какую-нибудь Arduino, если так станет кому-либо легче) и соберем все на ней. Вот только делать все будем «по-взрослому», с работой в режиме разрешенных прерываний и с расчетом на последующую реализацию более высоких уровней при помощи protothreads (о самих protothreads см. оригинал и разжеванные примеры на русском языке). Т.е. от bitbang-а отказываемся сразу и бесповоротно.

Также сохраним UART для будущего использования под другую периферию. Ибо он в нашем случае один, а мне бы еще протокол eBus сюда запихнуть. Ибо это тоже реалтайм.
I2C хорошо, но это еще один не самый дешевый кристалл с обвязкой, а оно нам надо? Итак, из интересного остается PWM и ICP.

Что такое PWM, я думаю, объяснять не надо. Это просто генерация импульсов заданной фазы и длительности, которая производится аппаратно (т.е. с точки зрения программы «в фоновом режиме»), но параметры которой можно изменять программно. А вот ICP достаточно интересная вещь: она позволяет аппаратно сохранять значение таймера в момент изменения уровня сигнала на определенном выводе микроконтроллера. Таким образом возможно достаточно точно получить момент возникновения события (изменения состояния вывода микроконтроллера) и/или измерить его продолжительность.

Итак, будем использовать аппаратный TIMER1 ATmega328 (таймеры 0 и 2 у меня используются для других целей). Для PWM можно использовать выводы OC1A и OC1B. Однако OC1B также используется в качестве сигнала SS, а подключать ATmega328 к целевому устройству я планировал именно по шине SPI. Это удобно с точки зрения апгрейда прошивок ATmega — делаем их при помощи целевого устройства, только один из CE-сигналов с его контроллера SPI заведем на контакт SS Atmega, а другой CE-сигнал заведем на контакт RESET. Да и с согласованием скоростей обмена возиться не нужно. Ну и в качестве еще одного довода против использования OC1B является то, что на плате Arduino MEGA 2560 этот сигнал вообще не выведен во внешний мир.

Итак, для генерации PWM используем контакт OC1A. Для ICP вариантов вообще нет — вывод такого типа у этой микросхемы только один. Для режима «ACTIVE PULLUP» используем любой свободный IO port и настроим его на работу в режиме «OUT».

Так, со схемотехникой разобрались. Теперь освежим в памяти физику работы шины 1-wire. В двух словах ее можно описать так:

  • Прием и передача тактируется master-устройством (т.е. нами)
  • Временной интервал между передаваемыми и/или принимаемыми битами не критичен (главное, чтобы он был не меньше минимально допустимого)
  • Критичным является длительность передаваемого импульса и длительность принимаемого (измеряемого) сигнала

Отлично, тогда выбираем режим работы Phase Correct PWM Mode (счетчик сначала считает «вверх» от значения BOTTOM до значения TOP, а после достижения значения TOP начинает считать «вниз» до значения BOTTOM). При этом значение BOTTOM фиксировано и всегда равно 0, а значение TOP может быть как одним из фиксированных значений (0xFF, 0x1FF, 0x3FF), так и значением из регистров OCR1A или ICR1. Два последних варианта нам не подходят, т.к. OCR1A мы будем использовать для формирования импульсов на выводе OC1A, а регистр ICR1 будет использоваться для измерения длительности низкого уровня на шине 1-Wire. Т.е. остается только использовать вариант с фиксированными значениями TOP, благо временной интервал между битами для нас не критичен.

В соответствии со схемой выход OC1A включен в инверсном режиме (т.е. установка на нем высокого уровня приведет к установке низкого уровня на шине 1-Wire и наоборот), будем использовать следующий режим PWM:

  • когда таймер считает «вниз» и его значение совпало со значением из регистра OCR1A, установим сигнал на выводе OC1A в высокий уровень (на шине 1-Wire будет низкий уровень).

  • когда таймер считает «вверх» и его значение совпало со значением из регистра OCR1A, установим сигнал на выводе OC1A в низкий уровень (и, возможно, начнем измерять момент перехода сигнала на выводе ICP с низкого уровня на высокий).



Остальное все просто. Новое значение (длительность следующего передаваемого бита) в регистр OCR1A мы будем загружать по прерыванию TIMER1_OVF (в данном режиме оно генерируется при достижения счетчиком значения BOTTOM а активируется затем после достижения значения TOP), а по прерыванию TIMER1_ICP будем вычислять длительность нахождения шины 1-Wire в состоянии низкого уровня и, в зависимости от этого, делать вывод о принятии бита «1» или бита «0».

Таким образом, на каждую операцию приема или передачи одного бита у нас будут генерироваться одно прерывание OVF и одно прерывание ICP. Соответственно их ISR лучше бы оптимизировать, но пока пусть они будут написаны на языке C.

Ну а операцию RESET для шины 1-Wire будем выполнять, перепрограммировав TIMER1 в режим NORMAL (т.к. длительность импульса RESET и последующего возможного импульса PRESENCE достаточно большая, нам необходимо расширить «динамический диапазон» работы таймера. В режиме NORMAL он составляет значение 0xFFFF, чего вполне достаточно в нашем случае. Более того, мы его даже специально ограничим значением из регистра OCR1B, чтобы в случае отсутствия подключенных к шине 1-Wire устройств не ждать, пока таймер дойдет до 0xFFFF, а прервать операцию сразу после достижения максимального критического временного интервала).

Вручную считать параметры настройки таймера в зависимости от тактовой частоты ATmega это моветон (да и лениво проверять все граничные условия), поэтому заставим это делать preprocessor компилятора C воспользовавшись тем фактом, что у нас должна быть определенная константа F_CPU, значение которой равно используемой тактовой частоте в герцах (unsigned long, т.е. константа должна быть определена как 8000000UL либо 16000000UL, иначе preprocessor будет считать ее как int и потеряет значащие разряды).

Определяем параметры используемой платы
/* Arduino Pro Mini 1-Wire connection */
#define WIRE1_DDR       DDRB
#define WIRE1_PORT      PORTB
#define WIRE1_PIN       PINB
/* OC1A/PCINT1 */
#define WIRE1_OUT       PB1
/* (PCINT0/CLKO/ICP1) */
#define WIRE1_ICP       PB0
#define WIRE1_IN_ICP    PINB0

/* Active pullup PD7 */
#define WIRE1_PULLUP_DDR        DDRD
#define WIRE1_PULLUP_PORT       PORTD
#define WIRE1_PULLUP_OUT        PD7

/* Presence detection in idle mode: external interrupt #0 */
#define WIRE1_IDLE_PRESENCE_DETECT_IRQ           INT0_vect
#define WIRE1_IDLE_PRESENCE_DETECT_EICRA_MASK    (_BV(ISC01) | _BV(ISC00))
/* Presence detection in idle mode: low level generate IRQ */
#define WIRE1_IDLE_PRESENCE_DETECT_EICRA_ISC     0
#define WIRE1_IDLE_PRESENCE_DETECT_EIMSK_INT     INT0
#define WIRE1_IDLE_PRESENCE_DETECT_EIFR_INTF     INTF0
#define WIRE1_IDLE_PRESENCE_DETECT_DDR           DDRD
#define WIRE1_IDLE_PRESENCE_DETECT_PORT          PORTD
#define WIRE1_IDLE_PRESENCE_DETECT_PIN           PIND
#define WIRE1_IDLE_PRESENCE_DETECT_BIT           PD2


#define WIRE1_TCNT      TCNT1
#define WIRE1_TCCRA     TCCR1A
#define WIRE1_TCCRB     TCCR1B
#define WIRE1_TCCRC     TCCR1C
#define WIRE1_OCRA      OCR1A
#define WIRE1_OCRB      OCR1B
#define WIRE1_ICR       ICR1
#define WIRE1_TIFR      TIFR1
#define WIRE1_TIMSK     TIMSK1

#define WIRE1_ICP_IRQ   TIMER1_CAPT_vect
#define WIRE1_OVF_IRQ   TIMER1_OVF_vect
#define WIRE1_COMPA_IRQ TIMER1_COMPA_vect
#define WIRE1_COMPB_IRQ TIMER1_COMPB_vect

#define WIRE1_POWER_ON()                                                    \
    power_timer1_enable()


Определяем временные параметры протокола 1-Wire
/* Продолжительность фаз RESET в единицах TSLOT */
#define RESET_DURATION_TSLOTS           8

/* Временные параметры обмена в мкс */
#define NORMAL_TLOWR_MIN                1
#define NORMAL_TLOWR_MAX                15
#define NORMAL_TSLOT_MIN                60
#define NORMAL_TSLOT_MAX                120
#define NORMAL_BIT1_MIN                 NORMAL_TLOWR_MIN
#define NORMAL_BIT1_MAX                 NORMAL_TLOWR_MAX
#define NORMAL_BIT0_MIN                 NORMAL_TSLOT_MIN
#define NORMAL_BIT0_MAX                 NORMAL_TSLOT_MAX
#define NORMAL_TREC_MIN                 1
#define NORMAL_RESET                    (RESET_DURATION_TSLOTS * NORMAL_TSLOT_MIN)
#define NORMAL_PRESENCE_START_MIN       15
#define NORMAL_PRESENCE_START_MAX       60
#define NORMAL_PRESENCE_MIN             60
#define NORMAL_PRESENCE_MAX             240
#define OVERDRIVE_TLOWR_MIN             1
#define OVERDRIVE_TLOWR_MAX             2
#define OVERDRIVE_TSLOT_MIN             6
#define OVERDRIVE_TSLOT_MAX             16
#define OVERDRIVE_BIT1_MIN              OVERDRIVE_TLOWR_MIN
#define OVERDRIVE_BIT1_MAX              OVERDRIVE_TLOWR_MAX
#define OVERDRIVE_BIT0_MIN              OVERDRIVE_TSLOT_MIN
#define OVERDRIVE_BIT0_MAX              OVERDRIVE_TSLOT_MAX
#define OVERDRIVE_TREC_MIN              1
#define OVERDRIVE_RESET                 (RESET_DURATION_TSLOTS * OVERDRIVE_TSLOT_MIN)
#define OVERDRIVE_PRESENCE_START_MIN    2
#define OVERDRIVE_PRESENCE_START_MAX    6
#define OVERDRIVE_PRESENCE_MIN          8
#define OVERDRIVE_PRESENCE_MAX          24

/* Максимальный требуемый период сигнала */
#define WIRE1_MAX_PERIOD                (NORMAL_TSLOT_MAX + NORMAL_TREC_MIN)

/* Минимально требуемая точность (д.б. лучше двойного минимального импульса) */
#define WIRE1_MIN_PRECISION             (OVERDRIVE_TLOWR_MAX / 2)

/* Максимально возможная продолжительность процедуры RESET */
#define WIRE1_RESET_PROCEDURE_DURATION  (RESET_DURATION_TSLOTS * NORMAL_TSLOT_MIN * 2)


Расчет параметров настройки таймера
/* Значение TOP в NORMAL MODE */
#define PWM_NORMAL_MODE_TOP             0xFFFF

/* Фиксированные значения TOP, поддерживаемые оборудованием */
#define PWM_FIXED_TOP_1                 0xFF
#define PWM_FIXED_TOP_2                 0x1FF
#define PWM_FIXED_TOP_3                 0x3FF

/* Длительность одного подсчета в мкс для заданного значения делителя */
#define PWM_PRECISION(_n)                                                   \
    ((_n) * 1000000UL / F_CPU)

/* Продолжительность _cnt отсчетов в мкс */
#define PWM_DURATION(_n, _cnt)                                              \
    ((_cnt) * (_n) * 1000000UL / F_CPU)

/* Длительность одного периода в мск для заданных значений делителя */
#define PWM_PERIOD(_n, _t)                                                  \
    ((_t) * 2 * (_n) * 1000000UL / F_CPU)

/* Определение значения счетчика в зависимости от требуемого времени */
#define PWM_COUNT_VALUE(_n, _t)                                             \
    (((F_CPU / (_n)) / 1000000UL) * (_t))

/* Определение значения OCRA в зависимости от желаемой длительности ямы */
#define PWM_BOTTOM_VALUE(_n, _t)                                            \
    ((F_CPU / 2 / (_n) / 1000000UL) * (_t))


/*
 * Алгоритм выбора значения делителя и TOP:
 * 
 * A. Обязательные условия
 * - период сигнала д.б. >= WIRE1_MAX_PERIOD
 * - разрешающая способность д.б. < WIRE1_MIN_PRECISION
 * 
 * B. Желательные условия
 * - период сигнала должен быть как можно меньше (т.е. минимальный TOP)
 */
#undef PWM_TOP

/* Проверяем возможность использования делителя 8 */
#if PWM_DURATION(8, PWM_FIXED_TOP_3) >= WIRE1_MAX_PERIOD
#if PWM_PRECISION(8) < WIRE1_MIN_PRECISION
#undef PWM_TOP
#define PWM_TOP PWM_FIXED_TOP_3
#undef PWM_DIVIDER
#define PWM_DIVIDER 8 
#undef PWM_TCCRA
#define PWM_TCCRA (_BV(WGM11) | _BV(WGM10))
#undef PWM_TCCRB
#define PWM_TCCRB _BV(CS11)
#endif
#endif

#if PWM_DURATION(8, PWM_FIXED_TOP_2) >= WIRE1_MAX_PERIOD
#if PWM_PRECISION(8) < WIRE1_MIN_PRECISION
#undef PWM_TOP
#define PWM_TOP PWM_FIXED_TOP_2
#undef PWM_DIVIDER
#define PWM_DIVIDER 8 
#undef PWM_TCCRA
#define PWM_TCCRA _BV(WGM11)
#undef PWM_TCCRB
#define PWM_TCCRB _BV(CS11)
#endif
#endif

#if PWM_DURATION(8, PWM_FIXED_TOP_1) >= WIRE1_MAX_PERIOD
#if PWM_PRECISION(8) < WIRE1_MIN_PRECISION
#undef PWM_TOP
#define PWM_TOP PWM_FIXED_TOP_1
#undef PWM_DIVIDER
#define PWM_DIVIDER 8 
#undef PWM_TCCRA
#define PWM_TCCRA _BV(WGM10)
#undef PWM_TCCRB
#define PWM_TCCRB _BV(CS11)
#endif
#endif

/* Проверяем возможность использования делителя 1 */
#if PWM_DURATION(1, PWM_FIXED_TOP_3) >= WIRE1_MAX_PERIOD
#if PWM_PRECISION(1) < WIRE1_MIN_PRECISION
#undef PWM_TOP
#define PWM_TOP PWM_FIXED_TOP_3
#undef PWM_DIVIDER
#define PWM_DIVIDER 1
#undef PWM_TCCRA
#define PWM_TCCRA (_BV(WGM11) | _BV(WGM10))
#undef PWM_TCCRB
#define PWM_TCCRB _BV(CS10)
#endif
#endif

#if PWM_DURATION(1, PWM_FIXED_TOP_2) >= WIRE1_MAX_PERIOD
#if PWM_PRECISION(1) < WIRE1_MIN_PRECISION
#undef PWM_TOP
#define PWM_TOP PWM_FIXED_TOP_2
#undef PWM_DIVIDER
#define PWM_DIVIDER 1
#undef PWM_TCCRA
#define PWM_TCCRA _BV(WGM11)
#undef PWM_TCCRB
#define PWM_TCCRB _BV(CS10)
#endif
#endif

#if PWM_DURATION(1, PWM_FIXED_TOP_1) >= WIRE1_MAX_PERIOD
#if PWM_PRECISION(1) < WIRE1_MIN_PRECISION
#undef PWM_TOP
#define PWM_TOP PWM_FIXED_TOP_1
#undef PWM_DIVIDER
#define PWM_DIVIDER 1
#undef PWM_TCCRA
#define PWM_TCCRA _BV(WGM10)
#undef PWM_TCCRB
#define PWM_TCCRB _BV(CS10)
#endif
#endif

#if !defined(PWM_TOP)
#error "Can't build I/O procedure with current F_CPU value"
#endif

/*
 * Процедуру RESET мы будем делать в normal mode, но требуется чтобы
 * время достижения TOP было не меньше удвоенного значения RESET
 * 
 */
#undef PWM_RESET_TCCRB

/* Проверяем возможность использования делителя 8 */
#if PWM_PERIOD(8, PWM_NORMAL_MODE_TOP) >= WIRE1_RESET_PROCEDURE_DURATION
#undef PWM_RESET_DIVIDER
#define PWM_RESET_DIVIDER 8
#undef PWM_RESET_TCCRB
#define PWM_RESET_TCCRB _BV(CS11)
#endif

/* Проверяем возможность использования делителя 1 */
#if PWM_PERIOD(1, PWM_NORMAL_MODE_TOP) >= WIRE1_RESET_PROCEDURE_DURATION
#undef PWM_RESET_DIVIDER
#define PWM_RESET_DIVIDER 1
#undef PWM_RESET_TCCRB
#define PWM_RESET_TCCRB _BV(CS10)
#endif

#if !defined(PWM_RESET_TCCRB)
#error "Can't build reset procedure with current F_CPU value"
#endif


Определяем некоторые полезные макрокоманды
/* Остановка таймера без изменения его остальных настроек */
#define DRV_STOP_CLOCK() do {                                               \
    _SFR_BYTE(WIRE1_TCCRB) &= ~(_BV(CS12) | _BV(CS11) | _BV(CS10));         \
    } while(0)

/* Установка режима работы "NORMAL MODE" */
#define DRV_TIMER_NORMAL_MODE() do {                                        \
    _SFR_BYTE(WIRE1_TCCRB) &= ~(_BV(WGM13) | _BV(WGM12));                   \
    _SFR_BYTE(WIRE1_TCCRA) &= ~(_BV(WGM11) | _BV(WGM10));                   \
    } while(0)

/* Перевод пина OCRA в состояние по умолчанию (low level) */
#define DRV_DISCONNECT_OCRA_PIN() do {                                      \
    _SFR_BYTE(WIRE1_TCCRA) &= ~(_BV(COM1A1) | _BV(COM1A0)); } while(0)

/* Перевод пина OCRA в состояние low level при достижении значения OCRA */
#define DRV_MATCH_OCRA_PIN_LOW() do {                                       \
    _SFR_BYTE(WIRE1_TCCRA) =                                                \
        (_SFR_BYTE(WIRE1_TCCRA) & ~_BV(COM1A0)) | _BV(COM1A1); } while(0)

/* Перевод пина OCRA в состояние high level при достижении значения OCRA */
#define DRV_MATCH_OCRA_PIN_HIGH() do {                                      \
    _SFR_BYTE(WIRE1_TCCRA) |= _BV(COM1A1) | _BV(COM1A0); } while(0)

/* Отлавливаем переход с высокого на низкий уровень (1 => 0) */
#define DRV_CAPTURE_FALLING_EDGE() do {                                     \
    _SFR_BYTE(WIRE1_TCCRB) &= ~_BV(ICES1); } while(0)

/* Отлавливаем переход с низкого на высокий уровень (0 => 1) */
#define DRV_CAPTURE_RISING_EDGE() do {                                      \
    _SFR_BYTE(WIRE1_TCCRB) |= _BV(ICES1); } while(0)

/* Возвращает != 0, если отлавливается переход с низкого уровня на высокий */
#define DRV_IS_CAPTURE_RISING()                                             \
    (_SFR_BYTE(WIRE1_TCCRB) & _BV(ICES1))

/* Значение OCRA при передаче значения бита 1 в режиме normal */
#define NORMAL_BIT1_VALUE                                                   \
    PWM_BOTTOM_VALUE(PWM_DIVIDER, (NORMAL_BIT1_MIN + NORMAL_BIT1_MAX) / 2)

/* Значение OCRA при передаче значения бита 1 в режиме overdrive */
#define OVERDRIVE_BIT1_VALUE                                                \
    PWM_BOTTOM_VALUE(PWM_DIVIDER, (OVERDRIVE_BIT1_MIN + OVERDRIVE_BIT1_MAX) / 2)

/* Значение OCRA при передаче значения бита 0 в режиме normal */
#define NORMAL_BIT0_VALUE                                                   \
    PWM_BOTTOM_VALUE(PWM_DIVIDER, (NORMAL_BIT0_MIN + NORMAL_BIT0_MAX) / 2)

/* Значение OCRA при передаче значения бита 0 в режиме overdrive */
#define OVERDRIVE_BIT0_VALUE                                                \
    PWM_BOTTOM_VALUE(PWM_DIVIDER, (OVERDRIVE_BIT0_MIN + OVERDRIVE_BIT0_MAX) / 2)

/* Минамально возможная длительность бита 1 в режиме normal в тиках таймера */
#define NORMAL_BIT1_MIN_VALUE                                               \
    PWM_COUNT_VALUE(PWM_DIVIDER, NORMAL_BIT1_MIN)

/* Минамально возможная длительность бита 1 в режиме overdrive в тиках таймера */
#define OVERDRIVE_BIT1_MIN_VALUE                                            \
    PWM_COUNT_VALUE(PWM_DIVIDER, OVERDRIVE_BIT1_MIN)

/* Максимально возможная длительность бита 1 в режиме normal в тиках таймера */
#define NORMAL_BIT1_MAX_VALUE                                               \
    PWM_COUNT_VALUE(PWM_DIVIDER, NORMAL_BIT1_MAX)

/* Максимально возможная длительность бита 1 в режиме overdrive в тиках таймера */
#define OVERDRIVE_BIT1_MAX_VALUE                                            \
    PWM_COUNT_VALUE(PWM_DIVIDER, OVERDRIVE_BIT1_MAX)

/* Максимально возможная длительность бита 0 в режиме normal в тиках таймера */
#define NORMAL_BIT0_MAX_VALUE                                               \
    PWM_COUNT_VALUE(PWM_DIVIDER, NORMAL_BIT0_MAX)

/* Максимально возможная длительность бита 0 в режиме overdrive в тиках таймера */
#define OVERDRIVE_BIT0_MAX_VALUE                                            \
    PWM_COUNT_VALUE(PWM_DIVIDER, OVERDRIVE_BIT0_MAX)


Определяем типы данных для облегчения экономии RAM
/* Тип контекста драйвера */
typedef struct {
    /* Состояние драйвера, линии и т.п. */
    volatile uint8_t state;
    /* Параметры, зависящие от выполняемой задачи */
    union {
        /* Параметры для задачи выполнения операции RESET */
        struct {
            /* Измеренное значение presence TPDH */
            volatile uint16_t tpdhMeasure;
            /* Измеренное значение presence TPDL*/
            volatile uint16_t tpdlMeasure;
        } reset;
        /* Параметры для задачи передачи либо приема байта данных */
        struct {
            /* Значение, которое действует в compare unit при capture event */
            volatile uint16_t icpOCRA;
            /* На входе - передаваемое значение, на выходе - принятое значение */
            volatile uint8_t value;
            /* Кол-во битов, оставшихся до завершения операции */
            volatile uint8_t pending;
        } io;
    } param;
} drv_1wire_context_t;

/* state: Операция RESET завершена */
#define DRV_1WIRE_STATE_RESET_COMPLETE      0b10000000
/* state: Был обнаружен импульс PRESENCE и значения tpdhMeasure/tpdlMeasure валидны */
#define DRV_1WIRE_STATE_PRESENCE_DETECTED   0b01000000
/* state: Устройства могут работать в режиме overdrive */
#define DRV_1WIRE_STATE_OVERDRIVE_DETECTED  0b00100000
/* state: процедура обмена байтом завершена */
#define DRV_1WIRE_STATE_IO_COMPLETE         0b00010000
/* state: при выполнении обмена данными обнаружена ошибка */
#define DRV_1WIRE_STATE_IO_ERROR            0b00001000
/* state: как минимум одно устройство на шине использует parasite power */
#define DRV_1WIRE_STATE_BUS_PARASITE_POWER  0b00000100
/* state: после завершения передачи заданного кол-ва битов требуется активация pullup.
 * Имеет смысл только при выполнении операции передачи либо приема битов */
#define DRV_1WIRE_STATE_ACTIVATE_PULLUP     0b00000010
/* state: Проверка управления: установка OCA в high привела к переходу ICP в low. 
 * Имеет смысл только после завершения операции RESET. */
#define DRV_1WIRE_STATE_OCA_LOW_PASSED      0b00000010
/* state: Проверка управления: установка OCA в low привела к переходу ICP в high. 
 * Имеет смысл только после завершения операции RESET. */
#define DRV_1WIRE_STATE_OCA_HIGH_PASSED     0b00000001
/* Был обнаружен адаптер 1-wire. 
 * Имеет смысл только после завершения операции RESET. */
#define DRV_1WIRE_STATE_1WIRE_DETECTED                                           \
    (DRV_1WIRE_STATE_OCA_LOW_PASSED | DRV_1WIRE_STATE_OCA_HIGH_PASSED)

/* Условие состояния, при котором после операции сброса на шине обнаружено устройство */
#define DRV_1WIRE_STATE_DEVICE_DETECTED(_s)                                 \
    ((_s) & DRV_1WIRE_STATE_PRESENCE_DETECTED)

/* Условие состояния, при котором операция ввода/вывода завершилась успешно */
#define DRV_1WIRE_STATE_IO_OK(_s)                                           \
    (((_s) & (DRV_1WIRE_STATE_PRESENCE_DETECTED | DRV_1WIRE_STATE_IO_ERROR)) == DRV_1WIRE_STATE_PRESENCE_DETECTED)

#define DRV_1WIRE_TXBITS(_v, _n, _p)                                        \
    drv1WireStartIo((_v), (_n), (_p))

#define DRV_1WIRE_RXBITS(_n)                                                \
    DRV_1WIRE_TXBITS(0xFF, (_n), 0)

/* Процедура передачи байта данных на линию */
#define DRV_1WIRE_TXBYTE(_v, _p)                                            \
    DRV_1WIRE_TXBITS((_v), 8, (_p))

#define DRV_1WIRE_RXBYTE()                                                  \
    DRV_1WIRE_RXBITS(8)

#define DRV_1WIRE_RESET()                                                   \
    drv1WireStartReset(DRV_1WIRE_STATE() & DRV_1WIRE_STATE_OVERDRIVE_DETECTED)

#if defined(WIRE1_PULLUP_OUT)
#define DRV_1WIRE_PULLUP_ON()                                               \
    drv1WirePullupOn()

#define DRV_1WIRE_PULLUP_OFF()                                              \
    drv1WirePullupOff()
#else /* defined(WIRE1_PULLUP_OUT) */
#define DRV_1WIRE_PULLUP_ON()
#define DRV_1WIRE_PULLUP_OFF()
#endif /* defined(WIRE1_PULLUP_OUT) */



Исходный код реализации примитивов
/* Контекст драйвера */
drv_1wire_context_t ctx1WireDriver;

/**
 * Завершение процедуры RESET
 * 
 * @param _flags - флаги по результатам завершения
 */
__INLINE void resetOperationComplete(uint8_t _flags) {
    /* Запрещаем все прерывания, т.к. процедура завершена */
    _SFR_BYTE(WIRE1_TIMSK) = 0;
    
    /* Отмечаем достигнутое состояние */
    ctx1WireDriver.state |= _flags | DRV_1WIRE_STATE_RESET_COMPLETE;
    
    /* !? */
    DRV_STOP_CLOCK();
    
    /* Переводим пин OCA в режим low (на шине получаем high level) */
    DRV_MATCH_OCRA_PIN_LOW();
    _SFR_BYTE(WIRE1_TCCRC) |= _BV(FOC1A);
    
    /* Процедура обнаружения precence завершена */
    VOS_KERNEL_FIRE_SIGNIFICANT_EVENT(VOS_SE_REASON_1WIRE_PRESENCE_DETECTOR);
}

/**
 * Завершение процедуры обмена
 * 
 * @param _flags
 */
__INLINE void ioOperationComplete(uint8_t _flags) {
    /* Запрещаем все прерывания, т.к. процедура завершена */
    _SFR_BYTE(WIRE1_TIMSK) = 0;

    /* Отмечаем достигнутое состояние */
    ctx1WireDriver.state |= _flags | DRV_1WIRE_STATE_IO_COMPLETE;

    /* Останавливаем таймер и начинаем приведение его режима к normal */
    _SFR_BYTE(WIRE1_TCCRB) = 0;

    /* Таймер в режиме normal, мы готовы к принудительной установки OCA в low */
    _SFR_BYTE(WIRE1_TCCRA) = _BV(COM1A1);

    /* Убеждаемся в том, что сигнал OCA имеет низкий уровень */
    _SFR_BYTE(WIRE1_TCCRC) |= _BV(FOC1A);
    
    /* Процедура приема или передачи байта данных завершена */
    VOS_KERNEL_FIRE_SIGNIFICANT_EVENT(VOS_SE_REASON_1WIRE_IO_BYTE_COMPLETE);
}

/**
 * Выполнение подготовки к передаче очередного бита
 */
__INLINE __OPTIMIZE_SPEED void txBit() {
    if(ctx1WireDriver.param.io.value & 0x01) {
        /* Будет передаваться бит со значением 1 */
        _SFR_WORD(WIRE1_OCRA) = 
                (ctx1WireDriver.state & DRV_1WIRE_STATE_OVERDRIVE_DETECTED) ?
                OVERDRIVE_BIT1_VALUE : NORMAL_BIT1_VALUE;
    } else {
        /* Будет передаваться бит со значением 0 */
        _SFR_WORD(WIRE1_OCRA) = 
                (ctx1WireDriver.state & DRV_1WIRE_STATE_OVERDRIVE_DETECTED) ?
                OVERDRIVE_BIT0_VALUE : NORMAL_BIT0_VALUE;
    }
}


void drv1WireAttach() {
    /* Разрешаем питание устройства */
    WIRE1_POWER_ON();

    /* Убеждаемся в остановке таймера */
    DRV_STOP_CLOCK();

    /* Z-состояние без pullup */
    _SFR_BYTE(WIRE1_PORT) &= ~(_BV(WIRE1_OUT) | _BV(WIRE1_ICP));

    /* Переводим пин OCA в режим low (на шине получаем high level) */
    DRV_MATCH_OCRA_PIN_LOW();
    _SFR_BYTE(WIRE1_TCCRC) |= _BV(FOC1A);

    /* Set WIRE1_OUT out direction and low signal level, force ICP is in direction */
    _SFR_BYTE(WIRE1_DDR) = (_SFR_BYTE(WIRE1_DDR) & ~_BV(WIRE1_ICP)) | 
                           _BV(WIRE1_OUT);

#if defined(WIRE1_IDLE_PRESENCE_DETECT_BIT)
    /* Z-состояние без pullup */
    _SFR_BYTE(WIRE1_IDLE_PRESENCE_DETECT_PORT) &= ~_BV(WIRE1_IDLE_PRESENCE_DETECT_BIT);

    /* Убедимся в том, что пин детектирования presence в idle mode является входом */
    _SFR_BYTE(WIRE1_IDLE_PRESENCE_DETECT_DDR) &= ~_BV(WIRE1_IDLE_PRESENCE_DETECT_BIT);
#endif /* defined(WIRE1_IDLE_PRESENCE_DETECT_BIT) */

#if defined(WIRE1_PULLUP_OUT)
    /* Z-состояние без pullup */
    drv1WirePullupOff();

    /* pullup pin всегда является выходом */
    _SFR_BYTE(WIRE1_PULLUP_DDR) |= _BV(WIRE1_PULLUP_OUT);
#endif /* defined(WIRE1_PULLUP_OUT) */
    
    /* Начальное состояние драйвера */
    ctx1WireDriver.state = 0;
}

#if defined(WIRE1_PULLUP_OUT)
void drv1WirePullupOn() {
    _SFR_BYTE(WIRE1_PULLUP_PORT) |= _BV(WIRE1_PULLUP_OUT);
}

void drv1WirePullupOff() {
    _SFR_BYTE(WIRE1_PULLUP_PORT) &= ~_BV(WIRE1_PULLUP_OUT);
}
#endif /* defined(WIRE1_PULLUP_OUT) */


/**
 * Реализация процедуры "RESET"
 * 
 * @param _overdrive - используется режим OVERDRIVE ( != 0 - да)
 * 
 * Идея следующая:
 * 1. Переводим таймер в NORMAL MODE с исходным значением TCNT = 0
 * 2. Устанавливаем OCRA = 0 и при помощи FOCA принудительно выставляем
 * значение пина OCRA в HIGH (шина будет переведена в состояние LOW)
 * 3. Устанавливаем значение OCRA на длительность периода LOW
 * 4. Устанавливаем функцию сброса пина OCRA по достижению значения
 * (т.е. шина будет переведена в состояние HIGH)
 * 5. Запускаем таймер
 * 
 * По достижению таймером значения OCRA пин OCRA будет автоматом переведен
 * в состояние LOW (на шине выставится уровень high) и произойдет прерывание
 * 
 */
void drv1WireStartReset(uint8_t _overdrive) {
    /* Убеждаемся в отключении strong pullup, чтобы не жечь лишний раз MOSFET */
    DRV_1WIRE_PULLUP_OFF();

    /* Текущее состояние */
    ctx1WireDriver.state &= 
            ~(DRV_1WIRE_STATE_RESET_COMPLETE | 
              DRV_1WIRE_STATE_PRESENCE_DETECTED | 
              DRV_1WIRE_STATE_1WIRE_DETECTED);

    /* Устанавливаем длительность периода LOW, генерируемого нами */
    if(_overdrive) {
        ctx1WireDriver.param.reset.tpdhMeasure = PWM_COUNT_VALUE(PWM_RESET_DIVIDER, OVERDRIVE_RESET);
    } else {
        ctx1WireDriver.param.reset.tpdhMeasure = PWM_COUNT_VALUE(PWM_RESET_DIVIDER, NORMAL_RESET);
    }
    
    /* Запрещаем прерывания */
    VOS_KERNEL_ENTER_MODE(VOS_KERNEL_RING_HARD);
    
    /* Убеждаемся в остановке таймера */
    DRV_STOP_CLOCK();

    /* Переводим таймер в режим NORMAL MODE */
    DRV_TIMER_NORMAL_MODE();
    
    /* Инициализируем счетчик */
    _SFR_WORD(WIRE1_TCNT) = 0;
    
    /* Переводим пин OCRA в режим high (на шине получаем low level) */
    DRV_MATCH_OCRA_PIN_HIGH();
    _SFR_BYTE(WIRE1_TCCRC) |= _BV(FOC1A);
    
    /* Устанавливаем длительность периода LOW */
    _SFR_WORD(WIRE1_OCRA) = ctx1WireDriver.param.reset.tpdhMeasure;
    
    /* Устанавливаем длительность всей процедуры */
    _SFR_WORD(WIRE1_OCRB) = (ctx1WireDriver.param.reset.tpdhMeasure << 1);
    
    /* По достижению OCRA пин OCRA переведем в low (на шине станет high) */
    DRV_MATCH_OCRA_PIN_LOW();
    
    /* Убеждаемся в том, что признаки достижения значения OCRA и OCRB сброшены (производится записью 1) */
    _SFR_BYTE(WIRE1_TIFR) |= _BV(OCF1A) | _BV(OCF1B);
    
    /* Разрешаем прерывания по достижению значения OCRA и OCRB */
    _SFR_BYTE(WIRE1_TIMSK) |= _BV(OCIE1A) | _BV(OCIE1B);
    
    /* Отслеживать будем переход сигнала ICP с уровня HIGH в уровень LOW */
    DRV_CAPTURE_FALLING_EDGE();
    
    /* Запускаем таймер в работу (также разрешим noise canceller) */
    _SFR_BYTE(WIRE1_TCCRB) |= PWM_RESET_TCCRB | _BV(ICNC1);
    
    /* Было выпоолнено много команд, уровень на входной шине уже должен измениться */
    if(!(_SFR_BYTE(WIRE1_PIN) & _BV(WIRE1_ICP))) {
        /* Входной сигнал низкого уровня, так и должно быть */
        ctx1WireDriver.state |= DRV_1WIRE_STATE_OCA_LOW_PASSED;
    }
    
    /* Разрешаем прерывания */
    VOS_KERNEL_ENTER_MODE(VOS_KERNEL_RING_SOFT);
}

/**
 * Инициирование процедуры обмена байтом (прием или передача)
 * 
 * @param _value - передаваемое значение
 * @param _bits - кол-во передаваемых битов (обычно 8)
 * @param _pullup - требуется активация pullup после завершения передачи всех битов
 * 
 * Подразумевается, что вызов данной функции возможен только при остановленном
 * таймере и сигнале OCA == 0 (исходное состояние).
 */
void drv1WireStartIo(uint8_t _value, uint8_t _bits, uint8_t _pullup) {
    /* Убеждаемся в отключении strong pullup, чтобы не жечь лишний раз MOSFET */
    DRV_1WIRE_PULLUP_OFF();

    /* Состояние: процедура обмена только что начата и ошибок пока не обнаружено */
    ctx1WireDriver.state &= 
            ~(DRV_1WIRE_STATE_IO_COMPLETE | DRV_1WIRE_STATE_IO_ERROR);

    /* Признак необходимости активации pullup после завершения передачи */
    if(_pullup) {
        ctx1WireDriver.state |= DRV_1WIRE_STATE_ACTIVATE_PULLUP;
    } else {
        ctx1WireDriver.state &= ~DRV_1WIRE_STATE_ACTIVATE_PULLUP;
    }

    /* Передаваемое значение */
    ctx1WireDriver.param.io.value = _value;
    
    /* Кол-во битов, подлежащих обмену */
    ctx1WireDriver.param.io.pending = _bits;
    
    /* Запрещаем прерывания */
    VOS_KERNEL_ENTER_MODE(VOS_KERNEL_RING_HARD);

    /* Убеждаемся в том, что признаки capture и bottm reach сброшены (производится записью 1) */
    _SFR_BYTE(WIRE1_TIFR) |= _BV(ICF1) | _BV(TOV1);

    /* Нас интересуют прерывания capture и достижение bottom */
    _SFR_BYTE(WIRE1_TIMSK) |= _BV(ICIE1) | _BV(TOIE1);
    
    /* Значение OCRA при передаче первого бита */
    txBit();
    
    /* Значение счетчика выберем на 1 меньше текущего top,
     * чтобы через 1 clock значение OCRA применилось в схеме сравнения
     */
    _SFR_WORD(WIRE1_TCNT) = PWM_TOP - 1;
    
    /*
     * Режим работы:
     * Phase Correct PWM с фиксированным значением TOP,
     * установка OCA=1 при совпадении с OCRA при подсчете вниз и
     * установка OCA=0 при совпадении с OCRA при подсчете вверх
     */
    _SFR_BYTE(WIRE1_TCCRA) = PWM_TCCRA | _BV(COM1A1);
    
    /*
     * Остаток настроек:
     * WGM13 и WGM12 == 0 для Phase Correct PWM с фиксированным значением TOP
     * установка делителя частоты
     * установка Input Capture Noise Canceler
     * capture будет происходить по переходу ICP с низкого на высокий уровень
     * 
     * Таймер запущен и началась процедура передачи первого бита.
     */
    _SFR_BYTE(WIRE1_TCCRB) = PWM_TCCRB | _BV(ICNC1) | _BV(ICES1);
    
    /* Разрешаем прерывания */
    VOS_KERNEL_ENTER_MODE(VOS_KERNEL_RING_SOFT);
}

/**
 * Прерывание по достижению BOTTOM, т.к. для обмена данными мы используем режим
 * Phase Correct PWM Mode.
 * В этом режиме в точке BOTTOM мы можем загрузить значение OCRA, которое
 * будет автоматически передано на схему сравнения только после достижения
 * точки TOP.
 */
__OPTIMIZE_SPEED ISR(WIRE1_OVF_IRQ) {
    if(ctx1WireDriver.param.io.pending--) {
        /* Выталкиваем переданный бит и подготавливаем место для записи принятого */
        ctx1WireDriver.param.io.value >>= 1;

        /* Действующее в данный момент значение в compare unit */
        ctx1WireDriver.param.io.icpOCRA = _SFR_WORD(WIRE1_OCRA);
        
        /* Загружаем в OCRA значение, которое будет использоваться при передаче
         * следующего бита. Реально оно начнет использоваться только после
         * достижения очередного TOP.
         */
        if(ctx1WireDriver.param.io.pending) {
            txBit();
        } else {
            /* Передан последний бит, сдвиг делать не нужно */
        }
    } else {
        /* Процедура обмена не завершилась на capture после всех битов.
         * Это означает ошибку обмена.
         */
        ioOperationComplete(DRV_1WIRE_STATE_IO_ERROR);
    }
}

/**
 * Прерывание по capture
 */
__OPTIMIZE_SPEED ISR(WIRE1_ICP_IRQ) {
    if(ctx1WireDriver.state & DRV_1WIRE_STATE_RESET_COMPLETE) {
        /*
         * Процедура RESET завершена. Раз мы сюда все-таки попали, значит
         * идет стандартная процедура обмена данными. В этом случае мы
         * отлавливаем момент перехода сигнала ICP с уровня 0 в уровень 1.
         * Продолжительность низкого уровня (общая, включая длительность строба)
         * составляет io.icpOCRA + _SFR_WORD(WIRE1_ICR) единиц таймера.
         * 
         * Т.к. все имеющиеся у нас длительности в мкс не превышают 255,
         * будем использовать арифметику uint8_t.
         */

        /* Длительность низкого уровня входного сигнала в тиках таймера */
        uint16_t lowDuration = 
                ctx1WireDriver.param.io.icpOCRA + _SFR_WORD(WIRE1_ICR);
        
        if(ctx1WireDriver.state & DRV_1WIRE_STATE_OVERDRIVE_DETECTED) {
            /* Режим OVERDRIVE */
            if((lowDuration < OVERDRIVE_BIT1_MIN_VALUE) || 
               (lowDuration > OVERDRIVE_BIT0_MAX_VALUE)) {
                /* Не выдержаны временные параметры протокола обмена */
                ioOperationComplete(DRV_1WIRE_STATE_IO_ERROR);
                
                /* Дальнейшая работа смысла не имеет */
                return;
            }
            
            /* Выполняем декодирование бита */
            if(lowDuration < OVERDRIVE_BIT1_MAX_VALUE) {
                /* Принята нормальная лог. 1*/
                ctx1WireDriver.param.io.value |= 0x80;
            }
        } else {
            /* Нормальный режим */
            if((lowDuration < NORMAL_BIT1_MIN_VALUE) || 
               (lowDuration > NORMAL_BIT0_MAX_VALUE)) {
                /* Не выдержаны временные параметры протокола обмена */
                ioOperationComplete(DRV_1WIRE_STATE_IO_ERROR);
                
                /* Дальнейшая работа смысла не имеет */
                return;
            }
            
            /* Выполняем декодирование бита */
            if(lowDuration < NORMAL_BIT1_MAX_VALUE) {
                /* Принята нормальная лог. 1*/
                ctx1WireDriver.param.io.value |= 0x80;
            }
        }
        
        if(!ctx1WireDriver.param.io.pending) {
            /* Было передано (и, возможно, принято) все заданное кол-во битов */
            if(ctx1WireDriver.state & DRV_1WIRE_STATE_ACTIVATE_PULLUP) {
                /* После завершения обмена была запрошена активация pullup */
                DRV_1WIRE_PULLUP_ON();
            }

            /* Операция ввода/вывода завершена успешно */
            ioOperationComplete(0);
        }
    } else {
        /* Выполнение процедуры RESET.
         * Сигнал ICP только что перешел из состояния HIGH в состояние LOW.
         * Возможно это начало presence pulse.
         */
        if(DRV_IS_CAPTURE_RISING()) {
            /* ICES установлен, захват по возрастанию уровня */

            /* Продолжительность TPDL */
            ctx1WireDriver.param.reset.tpdlMeasure = _SFR_WORD(WIRE1_ICR) - ctx1WireDriver.param.reset.tpdlMeasure;

            /* Т.к. обнаружен переход на высокий уровень, то и тестирование
             * адаптера на переход на высокий уровень при снятии сигнала OCR
             * также прошла успешно.
             */
            ctx1WireDriver.state |= DRV_1WIRE_STATE_OCA_HIGH_PASSED;

            /* Операция завершена, presence обнаружен */
            resetOperationComplete(DRV_1WIRE_STATE_PRESENCE_DETECTED);
        } else {
            /* ICES сброшен, захват по спаду уровня */
            ctx1WireDriver.param.reset.tpdlMeasure = _SFR_WORD(WIRE1_ICR);

            /* Продолжительность TPDH */
            ctx1WireDriver.param.reset.tpdhMeasure = ctx1WireDriver.param.reset.tpdlMeasure - ctx1WireDriver.param.reset.tpdhMeasure;

            /* Теперь будем делать захват по возрастанию уровня сигнала */
            DRV_CAPTURE_RISING_EDGE();
        }
    }
}

/**
 * Прерывание по достижению значения OCRA
 */
__OPTIMIZE_SPEED ISR(WIRE1_COMPA_IRQ) {
    /* Значение OCRA достигнуто при выполнении процедуры RESET.
     * Это означает, что сигнал на пине OCRA только что был переведен
     * в состояние low, а сигнал на шине начал переводиться в состояние high.
     * Теперь нам надо определить интервал, через который сигнал на шине
     * будет вновь переходить из состояния high в состояние low (т.е. обнаружить
     * начало presence pulse, если таковой вообще будет).
     */

    /* Принудительно сбрасываем флаг ICF (производится записью 1) */
    _SFR_BYTE(WIRE1_TIFR) |= _BV(ICF1);

    /* Разрешаем прерывания по capture */
    _SFR_BYTE(WIRE1_TIMSK) |= _BV(ICIE1);    
}

/**
 * Прерывание по достижению значения OCRB
 * 
 * Нами оно используется только в процедуре выполнения операции RESET для
 * ограничения максимально допустимого времени ожидания сигнала PRESENCE.
 */
__OPTIMIZE_SPEED ISR(WIRE1_COMPB_IRQ) {
    /* Значение OCRB достигнуто при выполнении процедуры RESET.
     * Это означает, что сигнал presence обнаружен не был и устройства на
     * шине отсутствуют.
     * Однако мы можем проверить функционирование интерфейса 1-wire с точки
     * зрения корректной передачи сигналов.
     */

    if(_SFR_BYTE(WIRE1_PIN) & _BV(WIRE1_ICP)) {
        /* Входной сигнал высокого уровня, так и должно быть */
        ctx1WireDriver.state |= DRV_1WIRE_STATE_OCA_HIGH_PASSED;
    }
    
    /* Операция завершена, presence не обнаружен */
    resetOperationComplete(0);
}


Вот пример осциллограммы выполнения приема:


Канал C подключен к выходу OC1A (импульсы одинаковой длительности), а канал B подключен к шине 1-Wire. Видно, что длительность 1-го и 3-го импульса на канале B больше, чем длительность соответствующего синхроимпульса на канале C. Т.е. slave-устройство в битах 1 и 3 передает значение «0». А длительность импульса 2 на каналах B и C приблизительно равна, что соответствует биту «1».

Так как используемый драйвер шины 1-Wire поддерживает также режим «ACTIVE PULLUP» (что также реализовано в примитивах), ниже приводятся соответствующие осциллограммы:


Активация режима «ACTIVE PULLUP» после завершения передачи последнего бита команды (сигнал канала B переходит из низкого уровня в высокий, после чего в течении не больше 10мкс сигнал активации PULLUP также переходит в активное состояние, подавая через транзистор Q5 напряжение питания на сигнальную шину).


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


Окончание режиме «ACTIVE PULLUP» с передачей команды на чтение результатов в укрупненном виде.


Процедура RESET (первый импульс низкого уровня на канале B, совпадающий по длительности с импульсом высокого уровня на канале C) с последующим PRESENCE от устройства, подключенного к шине 1-Wire (второй импульс низкого уровня на канале B, когда на канале C также присутствует низкий уровень).


То же самое, но после окончания режима «ACTIVE PULLUP».

Вопрос к читателям: имеет ли смысл писать продолжение, где будет реализация команд протокола обмена 1-Wire на основе этих примитивов и библиотеки protothreads? В принципе там все очень просто и пишется просто сходу, подглядывая одним глазком в соответствующий datasheet.

Список литературы

  1. Драйвер шины 1-Wire для контроллеров питанием меньше 5V
  2. Четыре метода для подключения 1-Wire устройств к Raspberry Pi
  3. 1-Wire Communication Through Software
  4. 1-Wire-to-I2C Master Bridge
  5. Single-Channel 1-Wire Master
  6. Using a UART to Implement a 1-Wire Bus Master
  7. А здесь на пальцах объясняется функционирование устройств 1-Wire (на русском языке)
  8. Protothreads by Adam Dunkels
  9. Protothread и кооперативная многозадачность
  10. ATmega328/P datasheet
  11. Arduino PRO mini
  12. Arduino MEGA 2560
Tags:
Hubs:
+21
Comments 21
Comments Comments 21

Articles