Частотомер с БИХ‑фильтром НЧ в 1 Кбайте на ATtiny13 или как запрячь одну лошадь в три телеги

—

от автора

Появилась необходимость сделать частотомер для настройки бензинового генератора. Т.е. сама по себе задача простая: измерить частоту синусоидального напряжения в пределах 40..70 Гц, но при условии больших шумов и искажений, которые возникают при работе генератора, с точностью около ±0.1 Гц. Для этого уже сделаны много вариант приборов, но был «спортивный интерес» и азарт. На макетке был установлен один из самых простых и легких микроконтроллеров ATtiny13. От предыдущих проектов на нем уже был сделан индикатор на 3 7-сегментных разряда с использованием одного сдвигового регистра 74HC595 (под него был написан код, но на практике установлен регистр 74HC164).

Когда речь заходит о микроконтроллере ATtiny13, большинство представляет себе простые «мигалки», таймеры, или, в лучшем случае, драйверы фонариков. Но что, если бросить вызов и выжать из этого «малыша» максимум, превратив его в полноценный вычислительный узел.

Пожалуй, в рамках одной статьи не получится рассказать обо всем проекте. Прибор получился весьма нагруженный:
а) динамическая индикация на одном сдвиговом регистре 74HC164 на 3-х пинах;
б) ФНЧ с применением 2-х каскадного БИХ‑фильтра (фильтр с бесконечной импульсной характеристикой или IIR‑фильтр от англ. Infinite Impulse Response) — это линейный цифровой или электронный фильтр, в котором выходной сигнал зависит не только от текущих и прошлых входных значений, но и от предыдущих значений самого выхода);
в) аппроксимация точки перехода синусоиды через 0 (для увеличения точности до ±0.1Гц);
г) счетчик и табличный преобразователь для получения частоты со сглаживанием результата на 32 измерениях;
д) температурная подстройка частоты внутреннего RC‑генератора при помощи датчика ds18b20.

Эту статью посвящу тому, как удалось все процессы подружить на единственном 8-битном таймере ATtiny13.

Изначально была уже готовая макетная плата для экспериментов, но начал с создания симуляции прибора в симуляторе SimulIDE (отдельная благодарность создателям такого прекрасного инструмента).

Схема частотомера в программе SimulIDE

Схема частотомера в программе SimulIDE

Понятно, что писать такие инструменты на Си в 1 кбайте памяти не вариант. Так что, весь проект создавался в ассемблере.

Начну с выбора частоты и режима работы основного таймера. Изначально была идея делать выборки АЦП на частоте 8кГц, чтобы получить точность измерения частоты ±0.1Гц. Сам кибернетику подзабыл с далекого 90-го года, поэтому обратился за помощью к ИИ‑Gemini обитающему в Google Chrome. В результате был создан код БИХ‑фильтра с коэффициентами в степенях 2-ки и целочисленной 16-ти битной математикой. Но, при дискретизации синусоиды на частоте 8кГц, фильтр то «возбуждался», то «затухал» совсем, вел себя нестабильно. Пришлось понижать частоту дискретизации и был выбран простой режим таймера (со временем это дало свою выгоду) с пред делителем 8, с прерыванием по переполнению: Ft = 9.6МГц (Fosc) / 8 / 256 = 4687,5 Гц. На этой частоте удалось подобрать коэффициенты фильтра, которые устойчиво фильтровали входную синусоиду в интервале 40..70 Гц от более высоких частотных искажений.

.def ZER=r3; Всегда 0 для всяких операция.def osc    =   r4; Регистр хранит начальное состояние OSCCAL.def calibr =r5.def ow_state = r7; Регистр состояния автомата работы с ds18b20.def ow_buf   = r8; Регистр чтения/записи датчика ds18b20.def ow_wait  = r9; Регистр пред делителя ожидания преобразования датчика ds18b20.def tmp_0    = r16; Регистр для временных данных 0.def tmp_1    = r17; Регистр для временных данных 1.def pseg  =r24; Регистр отображаемого разряда.def rdis  =r25; Регистр пред делителя частоты дисплея.equ DAT= PB2; PB2 (ножка 7 чипа) / она же разряд 1 .equ SH= PB0; PB0 (ножка 5 чипа) / она же разряд 0  .equ LC= PB1; PB1 (ножка 6 чипа) / она же разряд 2  .equ OW_BIT = PB3; PB3 (ножка 2 чипа) / датчик ds18b20.equ P_ADC= (1<<MUX1); PB4 (MUX1:0 = 00 -> Вход АЦП ADC2) (ножка 3 чипа) .equ period_disp = 31; частота дисплея f = 4687,5 / 31 = 151 Гц.org0x000    rjmpInit.org OVF0addrrjmpTimer_tick.org 0x000aInit:cliclrZERintmp_0, OSCCALmovosc, tmp_0; Настройка портовoutPortB, ZERlditmp_0, (1<<DAT|1<<SH|1<<LC)     outDDRB, tmp_0     ; Настройка таймераoutTCCR0A, ZERlditmp_0, (1<<CS01); Fпрерывания = 9.6МГц / 8 / 256 = 4687,5 Гц Tt = 213 мкс                                     ; (В периоде синуса 50Гц примерно 94 такта).outTCCR0B, tmp_0lditmp_0, (1<<TOIE0); Разрешение прерывания при переполнении таймераoutTIMSK0, tmp_0; --- НАСТРОЙКА АЦП (С автоматическим триггером от Таймера 0) ---; REFS0 = 0 -> Опорное напряжение VCClditmp_0, P_ADC                      outADMUX, tmp_0; ADATE = 1 -> Включить автотриггер; ADPS2:0 = 110 -> Делитель частоты АЦП на 64 (9.6 МГц / 64 = 150 кГц); Одно преобразование займет 13 тактов АЦП = 87 мксlditmp_0, (1<<ADEN|1<<ADATE|1<<ADSC|1<<ADPS2|1<<ADPS1)outADCSRA, tmp_0; ADCSRB: TS2:0 = 011 -> Источник триггера: переполнение таймера; Как только таймер дойдет до OVF, АЦП сам начнет замер, без участия ядра!lditmp_0, (1<<ADTS2)outADCSRB, tmp_0  sei; Разрешим прерывания

Сама программа, пустой цикл

Loop:    rjmpLoop

Прерывание по тику таймера

Timer_tick:; Запускаем чтение значения из АЦП inr20, ADCLinr21, ADCHsubir21, $02      ; Центрируем r21:r20 - 512 вычитая постоянную составляющую (r21:r20 = X0); выполнение БИХ-фильтра синусоиды 40-70Гц; На выходе y1 - r21:r20, y0 - r17:r16rcallfilter_tick; Расчет частоты в случае перехода синусоиды через ноль снизу в верх; На входе все те же y1 - r21:r20, y0 - r17:r16rcallCount_freqdecrdis; Уменьшаем предделитель для индикации на 150 Гц (50 Гц * 3 знакоместа)brneEnd; Пока не достигнут 0 не трогаем дисплейldirdis, period_disp;Восстанавливаем значение предделителя индикации; Установим на дисплее знакоместо psegrcallDispdecpseg; Переходим к меньшему знакоместуbrplEnd; Если положительное, то закончимldipseg, 2 ; Иначе вернемся к знакоместу 2End:reti

Динамическая индикация на 3 разряда требует для своей работы таймера не ниже 150 Гц (3 * 50Гц). Поэтому был сразу выделен регистр rdis, как пред делитель частоты для работы индикатора. Чтобы получить нужный делитель, делим 4687,5 Гц на 150 Гц и получаем приблизительно 31 такт. Зафиксировали это значение константой period_disp. Преобразования АЦП запускаются по таймеру при достижении переполнения. Это удобно потому, что на начало прерывания уже наверняка есть сформированное значение напряжения с предыдущего цикла. Да, это сдвиг фазы синусоиды на 213 мкс, но на измерение периода сигнала никак не влияет.

Все режимы, на этот момент, прекрасно укладывались в рассчитанную работу таймера. Код был написан и прибор запущен в железе. И тут остро обозначилась проблема температурной нестабильности внутреннего RC‑генератора. При изменении температуры на 10 градусов показания прибора менялись в единицах Гц. Естественно, это не устраивало. Было выбрано два направления устранения проблемы.

1. Внешний стабильный генератор на кварцевом резонаторе

Первым направлением решения проблемы «уплывающей» частоты было применение внешнего генератора на кварцевом резонаторе на пине PB3 и переключение фьюзов CKSEL1..0 = 00 (внешний генератор). Изначально планировался внешний генератор на частоту 8МГц, как самый близкий к существующей частоте. Программа получила минимальные изменения. Таймер был переведен в режим CTC (Clear Timer on Compare Match) и пересчитано значение регистра OCR0A: 8МГц (Fosc) / 8 / 4687,5 Гц = 213 тактов. А также, АЦП переключен с прерывания по переполнению на прерывания по совпадению компаратора A.

; Настройка таймера   lditmp_0, (1<<WGM01)outTCCR0A, tmp_0lditmp_0, (1<<CS01); Fпрерывания = 9.6МГц / 8 / 256 = 4687,5 Гц Tt = 213 мкс ; (В периоде синуса 50Гц примерно 94 такта).outTCCR0B, tmp_0lditmp_0, 213outOCR0A, tmp_0lditmp_0, (1<<OCIE0A)outTIMSK0, tmp_0; ADCSRB: TS2:0 = 011 -> Источник триггера: компаратор A таймера; Как только таймер дойдет до OCR0A, АЦП сам начнет замер, без участия ядра!lditmp_0, (1<<ADTS1|1<<ADTS0)outADCSRB, tmp_0

Прерывание, конечно, надо было «пересадить» с переполнения на срабатывание компаратора A.

.org OC0AaddrrjmpTimer_tick

В последствии, под руку попался гибридный генератор на кварце 20 МГц в одном корпусе с 4-мя выводами и программа с легкостью приспособилась и под него. Изменился только пред делитель с 8 на 64, и регистр OCR0A с 213 на 66.

2. Калибровка внутреннего RC‑генератора по данным датчика температуры ds18b20

А вот вторым направлением — было сделать компенсацию температурной нестабильности частоты при помощи датчика ds18b20 и регистра подстройки частоты OSCCAL. Скажу сразу, что пару раз готов был отложить этот вариант. Уж очень большой диапазон интервалов времени положен в основу работы датчика. Передача прием битов в десятках микросекунд, а ожидание измерения — в сотнях миллисекунд. Да и нагрузку разделить между, достаточно, тяжелой математикой и работой с датчиком температуры было сложно. Спасало то, что датчик ds18b20 имеет асинхронный протокол обмена данными. И между передачей/приемом битов он в состоянии «отпущенной» шины может терпеливо ждать продолжения.

Для обеспечения устойчивой работы с датчиком надо было выбрать такты прерываний таймера, которые наверняка будут свободны от сложных и долгих вычислений. И такой алгоритм был разработан. Индикатор обновляется только на каждом 31-м такте таймера, с этим просто. БИХ‑фильтр равномерно нагружает каждый такт. Но как не попасть на непредсказуемый переход синусоиды через 0 (в этом такте резко вырастает нагрузка)? И тут сработала следующая идея. Мы не можем предсказать, когда наступит следующий переход через 0. Но мы точно знаем, что в течении первых 10 тактов счетчика периода синусоиды перехода через 0 не произойдет. Т.к. высокую частоту (что‑то около 500 Гц) погасил сам БИХ‑фильтр. Вот эти такты и были приняты в работу. С некоторым запасом на то, что для сброса протокола датчика необходимо 4 такта счетчика «к ряду» (то есть около 800 микросекунд).

Осциллограмма тактов работы таймера

Осциллограмма тактов работы таймера

Основные такты работы с датчиком ds18b20 легко уложились в существующий таймер. Но осталась пауза в 750 миллисекунд для измерения и преобразования температуры. Вот тут пригодился уже существующий делитель для индикатора. Индикатор работает на частоте 151 Гц, то есть один такт работы что‑то около 6.6 миллисекунд. Поэтому был введен еще один регистр ow_wait для делителя частоты 151 Гц на 116 (6.6 * 116 = 765 миллисекунд).

Осциллограмма тактов ожидания измерения температуры

Осциллограмма тактов ожидания измерения температуры

Проблема ожидания датчика решена, а что делать с обменом информацией с датчиком? Задержки ожидания бита или передачи «0» около 60 микросекунд. Это около четверти времени такта таймера. Хотя мною выбраны самые ненагруженные такты, но зацикливать код внутри прерывания на такое время — решение не самое элегантное. Да, и можно где‑то вылететь за пределы времени, а это пропуск такта. Нарушение работы фильтра и сбой подсчета длительности периода. Поэтому было принято решение перевести счетчик в режим быстрого ШИМ (fast PWM). Вот тут и сработал бонус выбора полного такта счетчика. Потому что теперь такты на переполнении счетчика остались те же. А временем передачи 0/1 или получения бита с датчика можно управлять компаратором A.

Структура теперь выглядела так:

  1. Прерывание по переполнению таймера опрашивает АЦП, выполняет БИХ‑фильтр и при совпадении условий «ненагруженного такта» запускает очередной шаг автомата работы с датчиком DS18b20.

  2. Автомат рассчитывает тайминг следующего действия автомата и заносит его в регистр OCR0A.

  3. Автомат начинает выполнять текущее действие, которое в большинстве случаев заключается в притягивании шины в «0».

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

  5. Через установленное время происходит прерывание компаратора A (не зависимо от того закончилось ли предыдущее) и заканчивается такт работы с датчиком.

  6. Прерывание компаратора A запрещает само себя перед выходом.

«Быстро сказка сказывается…». Тут и начались расхождения архитектуры AVR и симулятора SimulIDE. В архитектуре таймера AVR заложено буферирование регистров OCR0A и OCR0B. Как бы я не менял их значение в текущем прерывании таймера, эти значения будут применены только в следующем такте после перехода регистра счетчика TNT0 в «0». Т.е. готовить длительность передачи 0-го бита команды надо еще в такте сброса протокола и ожидания ответа от датчика, длительность 1-го бита при передаче 0-го, и так далее. А в симуляторе буферирование отсутствует и значения регистров компаратора применяются в текущем такте (если, конечно, они не превышают значения счетчика TNT0). Для дальнейшей отладки автомата работы с датчиком в симуляторе понадобилось ввести переменную условной компиляции SIMULATION = 1.

Осциллограмма протокола работы датчика DS18B20

Осциллограмма протокола работы датчика DS18B20

И вот наконец, на таймере все процессы синхронизировались. Учтены все точки расчета и применения значений регистров, как для «железа», так и для симулятора. Программа приводится не целиком, а только в обсуждаемой части. Вся программа заняла 984 из 1024 байт, что составило 96% и выбивается за пределы одной статьи.

.set SIMULATION = 1  ; Поменяйте на 0 для реального чипа.def ZER= r3; Всегда НОЛЬ для всяких операция.def osc= r4; Регистр хранит начаоьное состояние OSCCAL.def calibr    = r5; Регистр калибровочного значения по температуре.def ow_state= r7; Регистр состояния автомача работы с ds18b20.def ow_buf    = r8; Регистр чтения/записи датчика ds18b20.def ow_wait= r9; Регистр предделителя ожидания преобразования датчика ds18b20.def tmp_0    = r16; Регистр для временных данных 0.def tmp_1    = r17; Регистр для временных данных 1.def pseg= r24; Регистр отображаемого разряда.def rdis= r25; Регистр предделителя частоты дисплея.equ DAT= PB2; PB2 (ножка 7 чипа) / она же разряд 1 .equ SH    = PB0; PB0 (ножка 5 чипа) / она же разряд 0  .equ LC    = PB1; PB1 (ножка 6 чипа) / она же разряд 2  .equ OW_BIT = PB3; PB3 (ножка 2 чипа) / датчик ds18b20.equ P_ADC= (1<<MUX1)    ; PB4 (MUX1:0 = 00 -> Вход АЦП ADC2) (ножка 3 чипа) .equ LED = PB5        ; пин для отладки окончания команды one ware.equ period_disp = 31; частота дисплея f = 4687,5 / 31 = 151 Гц.equ ds18b20_wait = 116; Время ожидания преобразования датчика ds18b20.MACRO LED_ON; Макрос управления пином отладкиsbiPORTB, LED.ENDM.MACRO LED_OFFcbiPORTB, LED.ENDM.cseg.org 0x0000; Таблица векторов прерывания    rjmpInit.org OVF0addrrjmpTimer_tick.org OC0AaddrrjmpEnd_bit_1wire; ==== Начальный сброс микроконтроллера =====.org 0x000aInit:clilditmp_0, LOW(RAMEND)outSPL, tmp_0clrZERintmp_0, OSCCAL; Сохраним начальное значение OSCCALmovosc, tmp_0; Настройка портовoutPortB, ZER.if SIMULATION == 1lditmp_0, (1<<DAT|1<<SH|1<<LC|1<<LED) ; .elselditmp_0, (1<<DAT|1<<SH|1<<LC)  .endif    outDDRB, tmp_0; Настройка таймераoutTCCR0A, ZERlditmp_0, (1<<CS01); Fпрерывания = f / 8 / 256 = 4687,5 Гц Tt = 213 икс ;(В периоде синуса 50Гц примерно 94 такта).outTCCR0B, tmp_0lditmp_0, (1<<TOIE0)outTIMSK0, tmp_0; --- НАСТРОЙКА АЦП (С автоматическим триггером от Таймера 0) ---; REFS0 = 0 -> Опорное напряжение VCC; MUX1:0 = 00 -> Вход ADC2 (пин PB4)lditmp_0, P_ADC                      outADMUX, tmp_0; ADATE = 1 -> Включить автотриггер; ADPS2:0 = 110 -> Делитель частоты АЦП на 64 (9.6 МГц / 64 = 150 кГц); Одно преобразование займет 13 тактов АЦП = 87 мксlditmp_0, (1<<ADEN|1<<ADATE|1<<ADSC|1<<ADPS2|1<<ADPS1)outADCSRA, tmp_0; ADCSRB: TS2:0 = 011 -> Источник триггера: переполнение таймера; Как только таймер дойдет до OVF, АЦП сам начнет замер, без участия ядра!lditmp_0, (1<<ADTS2)outADCSRB, tmp_0; Ну и наконец исходные значения переменныхldirdis, period_displdipseg, 2clrow_stateincow_stateclrxl; Разрешим прерыванияsei; Сама программа, пустой циклLoop:    rjmpLoop; Прерывание по тику переполнения таймера    Timer_tick:; 1. Читаем АЦПinr20, ADCLinr21, ADCHsubir21, $02      ; Центрируем -512 постоянную составляющую (r21:r20 = X0); =====================================================================; Сюда встраиваем автомат опроса датчика ds18b20 на «расслабленных» тактах;======================================================================tstow_state; Если статус 1-Wire = 0,breq no_1wire; то ничего не делаем просто идем к измерениямtst ow_wait; Если ow_wait == 0, то переходим к работе шиныbreq prod_ow; иначе проверим не в ожидании ли..movtmp_1, ow_waitcpi tmp_1, ds18b20_wait; Если ow_wait != 116, то сваливаем с обработки шиныbrlo no_1wire; Иначе переходим к обработке шиныprod_ow:mov tmp_0, ow_statecpitmp_0, 6; Если статус 1-Wire < 6,brlo wire_proc; то это сброс (он время зависим поэтому выполняем его)    cpixl, 8; Если счетчик периода синусоиды >= 8, (чтобы не нарваться на такт 0 синусоиды)brsh  no_1wire; то ничего не делаем просто идем к измерениямcpi rdis, 8; Если счетчик тактов дисплея < 8, (чтобы не нарваться на такт обновления)brlo no_1wire; то ничего не делаем просто идем к измерениямwire_proc:incow_statercall ds18b20no_1wire:;===============================================================; === [Базовая математика: БИХ-фильтр, переход через 0, PLL] ===;===============================================================; Запускаем выполнение БИХ-фильтра синусоиды 40-70Гц; На выходе y1 - r21:r20, y0 - r17:r16rcallfilter_tick; Расчет частоты в случае перехода синусоиды через ноль снизу в верх; На входе все те же y1 - r21:r20, y0 - r17:r16rcallCount_freq;==============================================================; ====== 4. ПЛАНОВАЯ ИНДИКАЦИЯ =======;==============================================================dec   rdis        ; Предделитель для индикации (31...1)breq  s_disprjmp End_OW_Ticks_disp:ldi   rdis, period_disp; === ЗОНА ДИСПЛЕЯ (Раз в 31 тик) ===rcall Disp; Автоматика пред делителей для разрядов индикатора и датчика ds18b20 tst ow_wait; Если ow_wait = 0, то отправляемся нв смену разряда дисплеяbreq step_pseg; Иначе продолжим проверкиmovtmp_0, ow_waitcpi tmp_0, ds18b20_wait; Если Такт ожидания не равен 116 такту (750 мс)breq step_pseg; то уменьшаем такты ожидания. Иначе на смену разрядаdec ow_waitstep_pseg:decpsegbrplEnd_OW_Tickldipseg, 2End_OW_Tick:reti;========================================================================; Прерывание для окончания записи/чтения датчика ds18b20 на компараторе A;========================================================================End_bit_1wire:push tmp_0in tmp_0, SREGpush tmp_0movtmp_0, ow_statecpitmp_0, 5; Если автомат в состоянии ==5, то читаем присутствие датчикаbrnenext_gr_wr; Иначе проверяем дальшеsbisPINB, OW_BITrjmp End_wireclrow_stateldi tmp_0, 7add tmp_0, oscout OSCCAL, tmp_0rjmp End_wirenext_gr_wr:cpitmp_0, 22; Если автомат в состоянии <22, то записываем бит из буфераbrshnext_gr_rd; Иначе проверяем дальшеcbiDDRB, OW_BITrjmp End_wirenext_gr_rd:cpitmp_0, 34; Если автомат в состоянии <34, то читаем бит в буферbrsh End_wire; Иначе на выход и игнорlsrow_bufldi tmp_0, $80 sbicPINB, OW_BITorow_buf, tmp_0End_wire:    ldi tmp_0, (1<<TOIE0) ; Запрещаем прерывания все, кроме переполнения таймера    out  TIMSK0, tmp_0    ; Загоняем в регистр    pop tmp_0             ; Восстанавливаем SREG из стека в tmp_0    out SREG, tmp_0       ; Записываем обратно в SREG    pop tmp_0             ; Восстанавливаем регистр tmp_0reti;===================================================; ======== Подпрограмма автомата датчика ds18b20 ======;===================================================ds18b20:mov tmp_0, ow_statecpitmp_0, 2    ; Если tmp_0=ow_state == 2, то продолжаемbrne next_rs_3; иначе скачем на проверку других тиковsbi   DDRB, OW_BIT; шину 1-wire тянем в «0»lditmp_0, $CC    ; передача команды сброса ROM 0xCCmovow_buf, tmp_0ldi tmp_0, 79; Время контроля сигнала присутствия датчика    outOCR0A, tmp_0rjmpSkip_1Wire_Phasenext_rs_3:cpi tmp_0, 5; Если tmp_0=ow_state < 5, то заканчиваем этот тактbrlo Skip_1Wire_Phase; иначе - продолжаемnext_rs_5:   cbiDDRB, OW_BIT; Отпускаем шину 1-wire в «1»cpitmp_0, 5; Если tmp_0=ow_state != 5, то продолжаемbreq st_wait; иначе переходим к ожиданию ответа.if SIMULATION == 1cpi tmp_0, 14.elsecpi tmp_0, 13; Если tmp_0=ow_state != 13(последний такт вывода 0xСС), то проверяем следующий такт.endifbrne next_wr_44; иначе меняем ow_buf на команду датчикуlditmp_1, $44; Установим в tmp_1 команду 0х44 (измерение температуры)movtmp_0, ow_waitcpi tmp_0, ds18b20_wait; Если мы находимся в инициализации измерения, то скачим на передачуbreq set_ow_buflditmp_1, $be; иначе установим в tmp_1 команду 0хbe (читать температуру из ROM)set_ow_buf:movow_buf, tmp_1; Записываем команду в регистр выводаrjmp st_bit_wr; Переходим к передаче байтаnext_wr_44:.if SIMULATION != 1cpi tmp_0, 21; Если tmp_0=ow_state != 21(последний такт вывода команды), то проверяем следующий такт brne next_wr_22; иначе готовим таймер к дальнейшему чтению lditmp_0, 17; Пауза для последующего чтения первого бита rjmpst_read.endifnext_wr_22:cpitmp_0, 22brne prod_rd.if SIMULATION == 1lditmp_0, 17; Пауза для последующего чтения первого бита out OCR0A, tmp_0.endifmovtmp_1, ow_waitcpitmp_1, ds18b20_wait;  Если находимся не в ожидании преобразования, то читаем битbrne next_rd; иначе продолжаем ожиданиеdec ow_waitclrow_stateinc ow_staterjmpSkip_1Wire_Phaseprod_rd:brshnext_rdst_bit_wr:.if SIMULATION == 1    mov tmp_1, ow_statecpitmp_1, 6breq st_rep_wrcpi tmp_1, 14breq st_rep_wr.endiflsrow_buf   ; сдвигаем передаваемый байт вправоst_rep_wr:sbiDDRB, OW_BIT; шину 1-wire тянем в «0»st_wait:lditmp_0, 79          ; Идеальный "0" (~65 мкс с учетом всех задержек)sbrc ow_buf, 0   ; Проверяем нулевой битlditmp_0, 13          ; Идеальная "1" (~9 мкс с учетом всех задержек)st_read:outOCR0A, tmp_0next_rd_bit:    ldi tmp_0, (1<<OCIE0A|1<<TOIE0); Разрешим прерывание от компаратора Aout TIFR0, tmp_0out TIMSK0, tmp_0.if SIMULATION == 1LED_ON    ; фиксируем сигнал на пине отладки.endifrjmpSkip_1Wire_Phasenext_rd:cpitmp_0, 38; Если так < 38, то продолжаем чтениеbrsh ow_calculate_osccal ; иначе переходим к калибровкеsbi   DDRB, OW_BIT; шину 1-wire в «0»nopnop                    ; Держим 2 мксcbi   DDRB, OW_BIT; шину 1-wire в «1»rjmp next_rd_bit; === ТИК 14: МАТЕМАТИКА КОРРЕКЦИИ OSCCAL ===ow_calculate_osccal:cpitmp_0, 38brne Skip_1Wire_Phaselditmp_1, 52subtmp_1, ow_buf; Получаем смещение от 52 градусов 52 – байт температурыmov tmp_0, tmp_1andi tmp_0, $03; Отсекаем младшие 2 бита для калибровки алгоритма счетаmovcalibr, tmp_0lsr tmp_1; Значение для калибровки OSCCAL (52-temp)/4lsr tmp_1addtmp_1, osc; Вычисляем новое значение регистр OSCCALout OSCCAL, tmp_1decow_wait    ; ow_wait = 0-1 = 0xFF цикл ожидания нового измерения температурыclrow_state; сброс автомата работы с датчикомinc ow_stateSkip_1Wire_Phase:ret

Проект, который начинался просто как интересная идея, показал, что наличие только одного таймера в микроконтроллере — не повод отказываться от намеченных целей, а повод синхронизировать все процессы и провести тотальную оптимизацию кода. И все получится!

ссылка на оригинал статьи https://habr.com/ru/articles/1104396/