The Jerusalem PostThe alliance with America is changing, and Israel cannot afford to ignore it - opinionInquirerMunicipal employee shot dead in Bukidnon; manhunt on for suspectsDaily MaverickThe varsity research subsidy game is up — the education department must not back downInquirer EntertainmentBea Millan-Windorski flies to Puerto Rico ahead of 2026 Miss UniversePunch2026 set to be second-hottest year on record — EUColliderApple TV Officially Resurrects Christopher Nolan’s 169-Minute Sci-Fi MasterpieceCBS NewsDemocrats could dust off plans for court reforms after capturing CongressZDF heuteAktuelle Pressemitteilungen des ZDFThe Hollywood Reporter“Saviors of Cinema” and “Money Machine”: Tallinn Puts Spotlight on Kids, Youth Films and Social Media BansThe RegisterUK ministers seek powers to investigate when AI agents are let off the leashGIGAZINEAIのオープンモデル利用率が大きく拡大中、オープンウェイトモデルとクローズドウェイトモデルの違いは何か?BillboardInterscope Expands Into Brazil With Hip-Hop Artist BK, Supernova Entertainment Joint Venture
The Daily Newsstand · Free, Always
Friday, November 6, 2026

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

Translate

Появилась необходимость сделать частотомер для настройки бензинового генератора. Т.е. сама по себе задача простая: измерить частоту синусоидального напряжения в пределах 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.

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

  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			; Макрос управления пином отладки
	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

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

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.