Частотомер с БИХ‑фильтром НЧ в 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 (отдельная благодарность создателям такого прекрасного инструмента).
Понятно, что писать такие инструменты на Си в 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 Гц
.org 0x000
rjmp Init
.org OVF0addr
rjmp Timer_tick
.org 0x000a
Init:
cli
clr ZER
in tmp_0, OSCCAL
mov osc, tmp_0
; Настройка портов
out PortB, ZER
ldi tmp_0, (1<<DAT|1<<SH|1<<LC)
out DDRB, tmp_0
; Настройка таймера
out TCCR0A, ZER
ldi tmp_0, (1<<CS01) ; Fпрерывания = 9.6МГц / 8 / 256 = 4687,5 Гц Tt = 213 мкс
; (В периоде синуса 50Гц примерно 94 такта).
out TCCR0B, tmp_0
ldi tmp_0, (1<<TOIE0) ; Разрешение прерывания при переполнении таймера
out TIMSK0, tmp_0
; --- НАСТРОЙКА АЦП (С автоматическим триггером от Таймера 0) ---
; REFS0 = 0 -> Опорное напряжение VCC
ldi tmp_0, P_ADC
out ADMUX, tmp_0
; ADATE = 1 -> Включить автотриггер
; ADPS2:0 = 110 -> Делитель частоты АЦП на 64 (9.6 МГц / 64 = 150 кГц)
; Одно преобразование займет 13 тактов АЦП = 87 мкс
ldi tmp_0, (1<<ADEN|1<<ADATE|1<<ADSC|1<<ADPS2|1<<ADPS1)
out ADCSRA, tmp_0
; ADCSRB: TS2:0 = 011 -> Источник триггера: переполнение таймера
; Как только таймер дойдет до OVF, АЦП сам начнет замер, без участия ядра!
ldi tmp_0, (1<<ADTS2)
out ADCSRB, tmp_0
sei ; Разрешим прерыванияСама программа, пустой цикл
Loop:
rjmp LoopПрерывание по тику таймера
Timer_tick:
; Запускаем чтение значения из АЦП
in r20, ADCL
in r21, ADCH
subi r21, $02 ; Центрируем r21:r20 - 512 вычитая постоянную составляющую (r21:r20 = X0)
; выполнение БИХ-фильтра синусоиды 40-70Гц
; На выходе y1 - r21:r20, y0 - r17:r16
rcall filter_tick
; Расчет частоты в случае перехода синусоиды через ноль снизу в верх
; На входе все те же y1 - r21:r20, y0 - r17:r16
rcall Count_freq
dec rdis ; Уменьшаем предделитель для индикации на 150 Гц (50 Гц * 3 знакоместа)
brne End ; Пока не достигнут 0 не трогаем дисплей
ldi rdis, period_disp ;Восстанавливаем значение предделителя индикации
; Установим на дисплее знакоместо pseg
rcall Disp
dec pseg ; Переходим к меньшему знакоместу
brpl End ; Если положительное, то закончим
ldi pseg, 2 ; Иначе вернемся к знакоместу 2
End:
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.
; Настройка таймера
ldi tmp_0, (1<<WGM01)
out TCCR0A, tmp_0
ldi tmp_0, (1<<CS01) ; Fпрерывания = 9.6МГц / 8 / 256 = 4687,5 Гц Tt = 213 мкс
; (В периоде синуса 50Гц примерно 94 такта).
out TCCR0B, tmp_0
ldi tmp_0, 213
out OCR0A, tmp_0
ldi tmp_0, (1<<OCIE0A)
out TIMSK0, tmp_0
; ADCSRB: TS2:0 = 011 -> Источник триггера: компаратор A таймера
; Как только таймер дойдет до OCR0A, АЦП сам начнет замер, без участия ядра!
ldi tmp_0, (1<<ADTS1|1<<ADTS0)
out ADCSRB, tmp_0Прерывание, конечно, надо было «пересадить» с переполнения на срабатывание компаратора A.
.org OC0Aaddr
rjmp Timer_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.
Структура теперь выглядела так:
Прерывание по переполнению таймера опрашивает АЦП, выполняет БИХ‑фильтр и при совпадении условий «ненагруженного такта» запускает очередной шаг автомата работы с датчиком DS18b20.
Автомат рассчитывает тайминг следующего действия автомата и заносит его в регистр OCR0A.
Автомат начинает выполнять текущее действие, которое в большинстве случаев заключается в притягивании шины в «0».
Выходим из автомата работы с датчиком разрешаем работу прерываний(! прерывания были запрещены при входе в обработку прерывания переполнения) и продолжаем дорабатывать прерывание.
Через установленное время происходит прерывание компаратора A (не зависимо от того закончилось ли предыдущее) и заканчивается такт работы с датчиком.
Прерывание компаратора A запрещает само себя перед выходом.
«Быстро сказка сказывается…». Тут и начались расхождения архитектуры AVR и симулятора SimulIDE. В архитектуре таймера AVR заложено буферирование регистров OCR0A и OCR0B. Как бы я не менял их значение в текущем прерывании таймера, эти значения будут применены только в следующем такте после перехода регистра счетчика TNT0 в «0». Т.е. готовить длительность передачи 0-го бита команды надо еще в такте сброса протокола и ожидания ответа от датчика, длительность 1-го бита при передаче 0-го, и так далее. А в симуляторе буферирование отсутствует и значения регистров компаратора применяются в текущем такте (если, конечно, они не превышают значения счетчика TNT0). Для дальнейшей отладки автомата работы с датчиком в симуляторе понадобилось ввести переменную условной компиляции SIMULATION = 1.

И вот наконец, на таймере все процессы синхронизировались. Учтены все точки расчета и применения значений регистров, как для «железа», так и для симулятора. Программа приводится не целиком, а только в обсуждаемой части. Вся программа заняла 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 ; Макрос управления пином отладки
sbi PORTB, LED
.ENDM
.MACRO LED_OFF
cbi PORTB, LED
.ENDM
.cseg
.org 0x0000 ; Таблица векторов прерывания
rjmp Init
.org OVF0addr
rjmp Timer_tick
.org OC0Aaddr
rjmp End_bit_1wire
; ==== Начальный сброс микроконтроллера =====
.org 0x000a
Init:
cli
ldi tmp_0, LOW(RAMEND)
out SPL, tmp_0
clr ZER
in tmp_0, OSCCAL ; Сохраним начальное значение OSCCAL
mov osc, tmp_0
; Настройка портов
out PortB, ZER
.if SIMULATION == 1
ldi tmp_0, (1<<DAT|1<<SH|1<<LC|1<<LED) ;
.else
ldi tmp_0, (1<<DAT|1<<SH|1<<LC)
.endif
out DDRB, tmp_0
; Настройка таймера
out TCCR0A, ZER
ldi tmp_0, (1<<CS01) ; Fпрерывания = f / 8 / 256 = 4687,5 Гц Tt = 213 икс
;(В периоде синуса 50Гц примерно 94 такта).
out TCCR0B, tmp_0
ldi tmp_0, (1<<TOIE0)
out TIMSK0, tmp_0
; --- НАСТРОЙКА АЦП (С автоматическим триггером от Таймера 0) ---
; REFS0 = 0 -> Опорное напряжение VCC
; MUX1:0 = 00 -> Вход ADC2 (пин PB4)
ldi tmp_0, P_ADC
out ADMUX, tmp_0
; ADATE = 1 -> Включить автотриггер
; ADPS2:0 = 110 -> Делитель частоты АЦП на 64 (9.6 МГц / 64 = 150 кГц)
; Одно преобразование займет 13 тактов АЦП = 87 мкс
ldi tmp_0, (1<<ADEN|1<<ADATE|1<<ADSC|1<<ADPS2|1<<ADPS1)
out ADCSRA, tmp_0
; ADCSRB: TS2:0 = 011 -> Источник триггера: переполнение таймера
; Как только таймер дойдет до OVF, АЦП сам начнет замер, без участия ядра!
ldi tmp_0, (1<<ADTS2)
out ADCSRB, tmp_0
; Ну и наконец исходные значения переменных
ldi rdis, period_disp
ldi pseg, 2
clr ow_state
inc ow_state
clr xl
; Разрешим прерывания
sei
; Сама программа, пустой цикл
Loop:
rjmp Loop
; Прерывание по тику переполнения таймера
Timer_tick:
; 1. Читаем АЦП
in r20, ADCL
in r21, ADCH
subi r21, $02 ; Центрируем -512 постоянную составляющую (r21:r20 = X0)
; =====================================================================
; Сюда встраиваем автомат опроса датчика ds18b20 на «расслабленных» тактах
;======================================================================
tst ow_state ; Если статус 1-Wire = 0,
breq no_1wire ; то ничего не делаем просто идем к измерениям
tst ow_wait ; Если ow_wait == 0, то переходим к работе шины
breq prod_ow ; иначе проверим не в ожидании ли..
mov tmp_1, ow_wait
cpi tmp_1, ds18b20_wait ; Если ow_wait != 116, то сваливаем с обработки шины
brlo no_1wire ; Иначе переходим к обработке шины
prod_ow:
mov tmp_0, ow_state
cpi tmp_0, 6 ; Если статус 1-Wire < 6,
brlo wire_proc ; то это сброс (он время зависим поэтому выполняем его)
cpi xl, 8 ; Если счетчик периода синусоиды >= 8, (чтобы не нарваться на такт 0 синусоиды)
brsh no_1wire ; то ничего не делаем просто идем к измерениям
cpi rdis, 8 ; Если счетчик тактов дисплея < 8, (чтобы не нарваться на такт обновления)
brlo no_1wire ; то ничего не делаем просто идем к измерениям
wire_proc:
inc ow_state
rcall ds18b20
no_1wire:
;===============================================================
; === [Базовая математика: БИХ-фильтр, переход через 0, PLL] ===
;===============================================================
; Запускаем выполнение БИХ-фильтра синусоиды 40-70Гц
; На выходе y1 - r21:r20, y0 - r17:r16
rcall filter_tick
; Расчет частоты в случае перехода синусоиды через ноль снизу в верх
; На входе все те же y1 - r21:r20, y0 - r17:r16
rcall Count_freq
;==============================================================
; ====== 4. ПЛАНОВАЯ ИНДИКАЦИЯ =======
;==============================================================
dec rdis ; Предделитель для индикации (31...1)
breq s_disp
rjmp End_OW_Tick
s_disp:
ldi rdis, period_disp
; === ЗОНА ДИСПЛЕЯ (Раз в 31 тик) ===
rcall Disp
; Автоматика пред делителей для разрядов индикатора и датчика ds18b20
tst ow_wait ; Если ow_wait = 0, то отправляемся нв смену разряда дисплея
breq step_pseg ; Иначе продолжим проверки
mov tmp_0, ow_wait
cpi tmp_0, ds18b20_wait ; Если Такт ожидания не равен 116 такту (750 мс)
breq step_pseg ; то уменьшаем такты ожидания. Иначе на смену разряда
dec ow_wait
step_pseg:
dec pseg
brpl End_OW_Tick
ldi pseg, 2
End_OW_Tick:
reti
;========================================================================
; Прерывание для окончания записи/чтения датчика ds18b20 на компараторе A
;========================================================================
End_bit_1wire:
push tmp_0
in tmp_0, SREG
push tmp_0
mov tmp_0, ow_state
cpi tmp_0, 5 ; Если автомат в состоянии ==5, то читаем присутствие датчика
brne next_gr_wr ; Иначе проверяем дальше
sbis PINB, OW_BIT
rjmp End_wire
clr ow_state
ldi tmp_0, 7
add tmp_0, osc
out OSCCAL, tmp_0
rjmp End_wire
next_gr_wr:
cpi tmp_0, 22 ; Если автомат в состоянии <22, то записываем бит из буфера
brsh next_gr_rd ; Иначе проверяем дальше
cbi DDRB, OW_BIT
rjmp End_wire
next_gr_rd:
cpi tmp_0, 34 ; Если автомат в состоянии <34, то читаем бит в буфер
brsh End_wire ; Иначе на выход и игнор
lsr ow_buf
ldi tmp_0, $80
sbic PINB, OW_BIT
or ow_buf, tmp_0
End_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_0
reti
;===================================================
; ======== Подпрограмма автомата датчика ds18b20 ======
;===================================================
ds18b20:
mov tmp_0, ow_state
cpi tmp_0, 2 ; Если tmp_0=ow_state == 2, то продолжаем
brne next_rs_3 ; иначе скачем на проверку других тиков
sbi DDRB, OW_BIT ; шину 1-wire тянем в «0»
ldi tmp_0, $CC ; передача команды сброса ROM 0xCC
mov ow_buf, tmp_0
ldi tmp_0, 79 ; Время контроля сигнала присутствия датчика
out OCR0A, tmp_0
rjmp Skip_1Wire_Phase
next_rs_3:
cpi tmp_0, 5 ; Если tmp_0=ow_state < 5, то заканчиваем этот такт
brlo Skip_1Wire_Phase ; иначе - продолжаем
next_rs_5:
cbi DDRB, OW_BIT ; Отпускаем шину 1-wire в «1»
cpi tmp_0, 5 ; Если tmp_0=ow_state != 5, то продолжаем
breq st_wait ; иначе переходим к ожиданию ответа
.if SIMULATION == 1
cpi tmp_0, 14
.else
cpi tmp_0, 13 ; Если tmp_0=ow_state != 13(последний такт вывода 0xСС), то проверяем следующий такт
.endif
brne next_wr_44 ; иначе меняем ow_buf на команду датчику
ldi tmp_1, $44 ; Установим в tmp_1 команду 0х44 (измерение температуры)
mov tmp_0, ow_wait
cpi tmp_0, ds18b20_wait ; Если мы находимся в инициализации измерения, то скачим на передачу
breq set_ow_buf
ldi tmp_1, $be ; иначе установим в tmp_1 команду 0хbe (читать температуру из ROM)
set_ow_buf:
mov ow_buf, tmp_1 ; Записываем команду в регистр вывода
rjmp st_bit_wr ; Переходим к передаче байта
next_wr_44:
.if SIMULATION != 1
cpi tmp_0, 21 ; Если tmp_0=ow_state != 21(последний такт вывода команды), то проверяем следующий такт
brne next_wr_22 ; иначе готовим таймер к дальнейшему чтению
ldi tmp_0, 17 ; Пауза для последующего чтения первого бита
rjmp st_read
.endif
next_wr_22:
cpi tmp_0, 22
brne prod_rd
.if SIMULATION == 1
ldi tmp_0, 17 ; Пауза для последующего чтения первого бита
out OCR0A, tmp_0
.endif
mov tmp_1, ow_wait
cpi tmp_1, ds18b20_wait ; Если находимся не в ожидании преобразования, то читаем бит
brne next_rd ; иначе продолжаем ожидание
dec ow_wait
clr ow_state
inc ow_state
rjmp Skip_1Wire_Phase
prod_rd:
brsh next_rd
st_bit_wr:
.if SIMULATION == 1
mov tmp_1, ow_state
cpi tmp_1, 6
breq st_rep_wr
cpi tmp_1, 14
breq st_rep_wr
.endif
lsr ow_buf ; сдвигаем передаваемый байт вправо
st_rep_wr:
sbi DDRB, OW_BIT ; шину 1-wire тянем в «0»
st_wait:
ldi tmp_0, 79 ; Идеальный "0" (~65 мкс с учетом всех задержек)
sbrc ow_buf, 0 ; Проверяем нулевой бит
ldi tmp_0, 13 ; Идеальная "1" (~9 мкс с учетом всех задержек)
st_read:
out OCR0A, tmp_0
next_rd_bit:
ldi tmp_0, (1<<OCIE0A|1<<TOIE0) ; Разрешим прерывание от компаратора A
out TIFR0, tmp_0
out TIMSK0, tmp_0
.if SIMULATION == 1
LED_ON ; фиксируем сигнал на пине отладки
.endif
rjmp Skip_1Wire_Phase
next_rd:
cpi tmp_0, 38 ; Если так < 38, то продолжаем чтение
brsh ow_calculate_osccal ; иначе переходим к калибровке
sbi DDRB, OW_BIT ; шину 1-wire в «0»
nop
nop ; Держим 2 мкс
cbi DDRB, OW_BIT ; шину 1-wire в «1»
rjmp next_rd_bit
; === ТИК 14: МАТЕМАТИКА КОРРЕКЦИИ OSCCAL ===
ow_calculate_osccal:
cpi tmp_0, 38
brne Skip_1Wire_Phase
ldi tmp_1, 52
sub tmp_1, ow_buf ; Получаем смещение от 52 градусов 52 – байт температуры
mov tmp_0, tmp_1
andi tmp_0, $03 ; Отсекаем младшие 2 бита для калибровки алгоритма счета
mov calibr, tmp_0
lsr tmp_1 ; Значение для калибровки OSCCAL (52-temp)/4
lsr tmp_1
add tmp_1, osc ; Вычисляем новое значение регистр OSCCAL
out OSCCAL, tmp_1
dec ow_wait ; ow_wait = 0-1 = 0xFF цикл ожидания нового измерения температуры
clr ow_state ; сброс автомата работы с датчиком
inc ow_state
Skip_1Wire_Phase:
retПроект, который начинался просто как интересная идея, показал, что наличие только одного таймера в микроконтроллере — не повод отказываться от намеченных целей, а повод синхронизировать все процессы и провести тотальную оптимизацию кода. И все получится!
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.