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

Изначально была уже готовая макетная плата для экспериментов, но начал с создания симуляции прибора в симуляторе SimulIDE (отдельная благодарность создателям такого прекрасного инструмента). Обратите внимание на то, что в симуляторе сразу внесена помеха "пила" амплитудой 1 В и частотой 130 Гц. Чтобы увидеть и оценить работу фильтра (форма полученного напряжения на осциллографе).

Понятно, что писать такие инструменты на Си не вариант. Так что, весь проект создавался в ассемблере. Изначально, представлялся простой АЦП, небольшая математика фильтра НЧ и счетчика продолжительности периода синусоиды, а также динамическая индикация 3-х разрядов частоты (2 – десятки, единицы и 1 десятые).

Для стабильной работы частотомера был применен внешний генератор на кварцевом резонаторе 8МГц. Соответственно, для работы микроконтроллера надо изменить фьюзы: 1. убрать фьюз CKDIV8 (чтобы тактовая частота не делилась на 8); 2. убрать фьюзы CKSEL1..0 в "00" (внешний генератор, БУДЬТЕ БДИТЕЛЬНЫ! если у Вас не будет внешнего генератора - то не сможете более "достучаться" до микроконтроллера). Фьюзы отвечающие за старт микроконтроллера BODLELVEL1..0 и SUT1..0 на свое усмотрение.

Начну с выбора частоты и режима работы таймера. Планировалось делать выборки АЦП на частоте 8 кГц, чтобы получить точность измерения частоты ±0.1Гц. Т.е. продолжительность одного такта таймера T = 1/8000 Гц = 125 мкс и в период синусоиды 50 Гц (20000 мкс) вошло бы 160 тактов. Это значение отлично помещается в 8-ми битный регистр и обеспечило бы желаемую точность.

Кибернетику я подзабыл, с далекого 90-го года, поэтому обратился за помощью к ИИ-Gemini обитающему в Google Chrome. В результате был создан код БИХ-фильтра с коэффициентами в степенях 2-ки и целочисленной 16-ти битной математикой. По-другому на ATtiny13 не получится, потому что аппаратного умножения нет и действий с плавающей запятой тоже.  Скоро выяснилось, что при дискретизации синусоиды на частоте 8 кГц, фильтр то «возбуждался», то «затухал» совсем, вел себя нестабильно. Пришлось понижать частоту дискретизации и был выбран режим таймера CTC (Clear Timer on Compare - счетчик доходит до значения в компараторе A, обнуляется и начинает новый цикл) с пред делителем 8: Ft = 8МГц (Fosc) / 8 / 213 (значение компаратора A) = 4687,5 Гц. На этой частоте удалось подобрать коэффициенты фильтра, которые устойчиво фильтровали входную синусоиду в интервале 40..70 Гц от более высоких частотных искажений.

И так, приступим:

; ATtiny13_Freq_new.asm
; Created: 08.10.2026 15:32:29
; Author : Андрей П.
;
Описал используемые регистр, константы и макросы.
.def ZER	=	r3		; Всегда НОЛЬ для всяких операция
.def tmp_0	=   r16		; Регистр для временных данных 0
.def tmp_1	=   r17		; Регистр для временных данных 1
.def r_tens	=   r22		; Регистр для десятых частоты
.def obnov	=   r23		; Регистр пред делитель сглаживания за 32 измерения
.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 P_ADC	= (1<<MUX1)	; ADC2 (ножка 3 чипа) вход АЦП

.equ period_disp = 31	; частота дисплея f = 4687,5 / 31 = 151 Гц
.equ Disp_new = $20		; 32 - периода синусоиды на обновление результата
.equ calibr = -2		; Калибровка полученная опытным путем со знаком минус для subi

.MACRO DISP_HIGH		; Макросы «дрыга» пином DAT регистра сдвига 
	sbi		PORTB, DAT	; для простого изменения индикатора
.ENDM				    ; с общим катодом/анодом
.MACRO DISP_LOW
	cbi		PORTB, DAT
.ENDM

Таблица векторов прерываний:

.cseg
.org 0x0000		; Прерывание Reset при старте работы
    rjmp	Init
.org OVF0addr		; Прерывание по совпадению компаратора A
	rjmp	Timer_tick

Обработчик прерывания Reset – инициализация режимов микроконтроллера:

.org 0x000a
Init:
	сli
	ldi		tmp_0, LOW(RAMEND)
	out		SPL, tmp_0
	
	clr		ZER
; Настройка портов	
	out		PortB, ZER
	ldi		tmp_0, (1<<DAT|1<<SH|1<<LC)  
    out		DDRB, tmp_0

; Настройка таймера
	ldi		tmp_0, $00		
	out		TCCR0A, tmp_0
	ldi		tmp_0, (1<<CS01)	; Fпрерывания = f / 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)	; Разрешение прерывания при совпадении компаратора A
	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 -> Источник триггера: переполнение таймера
; Как только таймер дойдет до CompA, АЦП сам начнет замер, без участия программы!
	ldi		tmp_0, (1<<ADTS1|1<<ADTS0)
	out		ADCSRB, tmp_0
; Ну и наконец исходные значения переменных
	ldi		rdis, period_disp
	ldi		pseg, 2
	ldi		obnov, Disp_new

	clr		xl

	sts		Smooth_l, ZER
	sts		Smooth_h, ZER
	sei

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

Loop:
    rjmp	Loop

Теперь, обработчик прерывания единственного таймера при срабатывании компаратора A:

; Прерывание по тику таймера    
Timer_tick:
	
	; Читаем АЦП
	in	r20, ADCL
	in	r21, ADCH
	subi	r21, $02      ; Центрируем АЦП - 512 (r21:r20 = X0)

; Запускаем выполнение БИХ-фильтра синусоиды 40-70Гц
; На входе x0 - r21:r20
; На выходе 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. Преобразования АЦП запускаются по таймеру при совпадении компаратора A. Это удобно потому, что на начало прерывания уже наверняка есть сформированное значение напряжения с предыдущего цикла. Да, это сдвиг фазы синусоиды на 213 мкс, но на измерение периода сигнала никак не влияет.

Подпрограмма отображения разряда на индикаторе:

Disp:
; На входе регистр pseg = разряду, который будет показываться
; В адресе Flash Pos -  два байта с двоично-десятичными данными индикатора в формате:  10  х2 

	ldi yh, HIGH(Pos)	; Заполняем регистр Y ссылкой на значение частоты
	ldi yl, LOW(Pos)

	mov tmp_0, pseg	; tmp_0 = разряд
	lsr tmp_0		; tmp_0 = разряд / 2

	add yl, tmp_0		; Сдвигаем ссылку на разряд / 2 (в одном байте два разряда)
	adc yh, ZER
	ld tmp_0, y		    ; tmp_0 = байту из двух разрядов для отображения

	sbrc pseg, 0		; Если разряд нечетный, то продолжаем
	swap	tmp_0		; иначе меняем местами полубайты
	andi tmp_0, $0f	    ; Убираем старший ненужный  полубайт.
	
	ldi zh, HIGH(Dig*2)	; Заполняем регистр Z ссылкой на знакогенератор
	ldi zl, LOW(Dig*2)
	add zl, tmp_0		; Смещаем ссылку на символ с кодом для отображения
	adc zh, ZER
	lpm tmp_0, z		; Получаем матрицу для 7 сегментного индикатора нужного символа

; Всегда рисуем точку в среднем сегменте	
	cpi	pseg, 1
	brne	wo_point
	sbr	tmp_0, $08

; Продвинем биты из tmp_0 в 164/595 регистр 
wo_point:
	cbi PortB, LC	; Убираем сигнал продвижения байта на выход для 595 регистра
	ldi tmp_1, 8	; Цикл на 8 тактов
NextS:
	cbi PortB, SH	; Убираем сигнал сдвига с регистра 595
	sbrc tmp_0, 7	; Выставляем значение DAT по 7-мо биту
	rjmp SetBit
	DISP_LOW
	rjmp Shift
SetBit:
	DISP_HIGH
Shift:
	sbi	PortB, SH	; Выставляем строб сдвига регистра 595
	lsl tmp_0		; Сдвигаем данные на 1 бит влево
	dec tmp_1		; уменьшаем значение счетчика
	brne NextS		; если не 0, то повторяем цикл
	sbi PortB, LC		; Продвигаем байт 595 регистра на выход

; А вот тепрь установим сигналы управления разрядами
	cpi pseg, 2	; Если разряд != 2, то переходим к разряду 1
	brne Vis1	; иначе включаем разряд 2
;	sbi PortB, LC	; Разряд 2 не трогаем, экономим. При передаче сигнал остался в 1
	cbi PortB, SH	; Гасим разряд 0 
	cbi PortB, DAT	; Гасим разряд 1
	rjmp EndDisp
Vis1:
	cpi pseg, 1	; Если разряд != 1, то переходим к разряду 0
	brne Vis0	; иначе включаем разряд 1
	sbi PortB, DAT	; Включаем разряд 1
	cbi PortB, LC	; Гасим разряд 2
	cbi PortB, SH	; Гасим разряд 0
	rjmp EndDisp
Vis0:
    cpi pseg, 0	; Если разряд != 0 – что-то пошло не так! Разряд = 2
	breq Vis2	; иначе включаем разряд 0
	ldi	pseg, $02
Vis2:
;	sbi PortB, SH		; Разряд 0 не трогаем, экономим. При передаче сигнал остался в 1
	cbi PortB, DAT		; Гасим разряд 1
	cbi PortB, LC		; Гасим разряд 2
ret
; ======== Знакогенератор =========
Dig:
	.db	$e7, $21, $d3, $73		; 0 1 2 3
	.db	$35, $76, $f6, $23		; 4 5 6 7
	.db	$f7, $77, $00, $f0, $17		; 8 9 Пробел min max

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

На этом заканчивается логика и начинается математика:

; Подпрограмма выполнения БИХ-фильтра
filter_tick:

	; === КАСКАД 1: Y1 = Y1 + (X0 - Y1) >> 4 ===
	lds	r18, Y1_L
	lds	r19, Y1_H     ; Достали старый Y1

	movw	r17:r16, r21:r20  ; Копируем X0
	sub	r20, r18
	sbc	r21, r19      ; r21:r20 = (X0 - Y1)

	; Сдвиг на 4 (деление на 16)
    asr	r21
    ror	r20
    asr	r21
    ror	r20
    asr	r21
    ror	r20
    asr	r21
    ror	r20

	add	r18, r20
	adc	r19, r21          ; r19:r18 = Новый Y1

    ; ===--- ЛИМИТЕР ДЛЯ Y1 (защита от высокочастотного возбуждения) ---===
    cpi   r19, $02
    brge  reset_y1        ; Если старший байт >= 2, улетели вверх
    cpi   r19, $fd
    brlt  reset_y1        ; Если старший байт < 0xFD (-3), улетели вниз
    rjmp  save_y1

reset_y1:
    mov   r18, ZER        ; Насильно гасим автоколебания в ноль
    mov   r19, ZER

save_y1:
	sts	Y1_L, r18
	sts	Y1_H, r19

	; === КАСКАД 2: Y2 = Y2 + (Y1 - Y2) >> 4 ===
	lds	r20, Y2_L
	lds	r21, Y2_H     ; Достали старый Y2

	movw	r17:r16, r19:r18 ; Берем результат первого каскада (Y1)
	sub	r16, r20
	sbc	r17, r21      ; r17:r16 = (Y1 - Y2)

	; Сдвиг на 4 (деление на 16)
    asr	r17
    ror	r16
    asr	r17
    ror	r16
    asr	r17
    ror	r16
    asr	r17
    ror	r16

	add	r20, r16
	adc	r21, r17      ; r21:r20 = Новый Y2 (Финальный выход фильтра!)

	; ===--- ЛИМИТЕР ДЛЯ Y2 (защита от высокочастотного возбуждения) ---===
    cpi   r21, $02
    brge  reset_y2
    cpi   r21, $fd
    brlt  reset_y2
    rjmp  save_y2

reset_y2:
    mov   r20, ZER        ; Если второй каскад пошел в разнос — гасим его
    mov   r21, ZER

save_y2:
	lds	r16, Y2_L
	lds	r17, Y2_H
	sts	Y2_L, r20
	sts	Y2_H, r21

	; На выходе: в r21:r20 находится y1 (текущий отфильтрованный отсчет)
    ;              r17:r16 находится y2 (предыдущий отфильтрованный отсчет)
	ret

Полученные значения y2 (предыдущее) и y1 (текущее) являются 16-ти битными знаковыми значениями отфильтрованного входного сигнала. И теперь можно определять момент когда y2 < 0, а y1 > 0, который и является отсчетом одного периода синусоиды. Однако, один такт таймера - это 213 мкс. При частоте 50 Гц имеем 1 / 50 Гц / 0.000213 с = 94 такта, а в случае 93 тактов получим частоту  1/(0.000213 с * 93) = 50.4 Гц. Т.е. точность всего ±0.2 Гц и с увеличением частоты точность будет еще уменьшаться.

И тут опять пришлось призывать на помощь математику. На отрезке синусоиды, максимально приближённой к точке перехода через 0, функция очень близка к линейной y=k*x. Т.е. отношению |Y2| к Y1 будет пропорционально X2 к X1. Этим я и воспользовался для увеличения точности расчета. Теперь умножив полученное количество тактов на 4, увеличив произведение на уточненный "перелет" нуля на старте периода и уменьшив его на уточненный "перелет" в конце периода, получаем значение продолжительности периода в 4 раза точнее. И вот это уже обеспечивает желаемую точность.

Значение продолжительности периода получено. Но, т.к., измерение проводятся на каждом периоде синусоиды, то и значения могут «скакать» с частотой 50 Гц. Чтобы этого избежать в течении 32-х измерений значения складываются и на каждом 32-ом такте делятся на 32 (путем сдвига в право на 5).

Вот мы и добрались до преобразования длительности периода синусоиды в частоту… В этом месте пришлось отложить математику и вернутся к обычной логике. Возможности ATtiny13 сильно ограничивают в расчете функции F=1/T. Поэтому было принято решение сделать табличное преобразование. Но и тут не все гладко. Все значения по условию задачи – это 3 разряда десятки, единицы и десятые герц. Для индикатора они подаются 3-мя полубайтами в двоично-десятичном формате. Адресация в такой таблице сразу усложняется умножением на 1.5 (можно и на, 2 но это сильно увеличивает таблицу). Поэтому было принято решение урезать таблицу до единиц и десятых герца. А значения десятков 4..6 (от 40.0 до 69.9) получать сравнением диапазонов полученного значения.

При таком решении каждый байт таблицы - это однозначное преобразование из продолжительности периода в частоту. А для приведения адресации к начальному адресу 0, из полученного значения продолжительности вычитается продолжительность периода в 70 Гц (268).

; Вход:  y1 - r21:r20, y0 - r17:r16	
; --- Расчет частоты синусоиды при переходе через ноль снизу в верх
Count_freq:
	inc	xl					; ticks++
	brne	next_if			; Если не произошло переполнение счетчика продолжим выполнение
	rjmp	too_little		; в противном случае индикация малой частоты
next_if:
; --- Проверка перехода через ноль (снизу вверх) ---
    sbrs r21, 7             ; Проверяем знаковый (7-й) бит старшего байта y1. Если 1 (минус) — идем дальше
    rjmp	end_zero_cross  ; Если y1 >= 0, перехода снизу вверх быть не может
    
    sbrc r17, 7             ; Проверяем знаковый бит y0. Если 0 (плюс) — переход есть
    rjmp	end_zero_cross  ; Если y0 < 0, мы все еще в минусе

next_count:
; === ПЕРЕХОД ЧЕРЕЗ НОЛЬ ЗАФИКСИРОВАН ===
; Теперь увеличим точность обнаружения 0, разделив обнаруженный интервал на 4
; Нам нужны модули: |y1| и |y0|. 
; Так как y1 гарантированно отрицательный, его модуль — это инверсия + 1
	com r20
	com r21
	subi r20, -1             ; Вычесть -1 означает прибавить 1
	sbci r21, -1

; Вычисляем знаменатель: dy = (|y1| + |y0|). y0 положительный, берем как есть.
    movw r19:r18, r17:r16   ; r19:r18 = |y0|
    add  r18, r20
    adc  r19, r21           ; r19:r18 = Знаменатель dy = (|y1| + |y0|)

; Вычисляем числитель: y0 * 4
    lsl  r16
    rol  r17                ; * 2
    lsl  r16
    rol  r17                ; * 4 (теперь r21:r20 = Числитель y0 * 4 )

; Быстрое деление числителя на знаменатель через вычитание (результат от 0 до 3 в r22)
    clr  r20                ; Инициализируем суб-тик = 0
div_loop:
    cp   r16, r18
    cpc  r17, r19           ; Сравниваем числитель со знаменателем
    brlt div_done           ; Если числитель меньше, деление окончено
    sub  r16, r18
    sbc  r17, r19           ; Числитель -= Знаменатель
    inc  r20                ; суб-тик++
    cpi  r20, 3
    brne div_loop           ; Ограничение цикла (макс 3)
div_done:					; dx = y0 * 4 / dy

	clr xh
; Вычисляем финальный T_quad для текущего измерения:
; T_quad = (ticks * 4) - r20 (текущий суб-тик) + остаток от прошлого шага

    lsl  xl
    rol  xh
    lsl  xl
    rol  xh				   ; xh:xl = ticks * 4
    
    sub  xl, r20           ; Вычитаем "перелет" (текущий суб-тик) из общего кол-ва тиков
    sbc  xh, ZER

	lds	r21, RPE	; Считываем "перелет" с прошлого шага
    add  xl, r21	; Добавляем накопленный остаток от прошлого периода
    adc  xh, ZER    ; Теперь xh:xl — абсолютно точный T_quad!

    sts	RPE, r20	; Сохраним текущий "перелет" для следущего шага

    subi   xl, calibr ; Точка калибровки (тут не совсем верно, но для нашего случая прокатит)
    adc   xh, ZER     ; Прибавляем старший байт знака с учетом переноса

; --- СДВИГ ШКАЛЫ В НОЛЬ  ---
    subi xl, low(268)		; Сдвиг шкалы на 0 для 70 Гц
    sbci xh, high(268)
    brcs too_much		    ; Если перенос (С=1), T_quad был меньше 268 (частота > 70 Гц)

; Измерение успешно! В xl у нас чистый 8-битный индекс для таблицы Freq_Table.
; Теперь в xl находится чистый Индекс = T_quad - 268
; ===========================================================================

; --- УЛЬТРА-ЛЕГКИЙ ФИЛЬТР СГЛАЖИВАНИЯ ПОКАЗАНИЙ ---
; На входе: xl = свежий вычисленный индекс (0..201)
; Получаем среднее значение за 32 цикла измерения

	lds	r18, Smooth_l
	lds	r19, Smooth_h
	add	xl, r18
	adc	xh, r19

	dec	obnov
	breq	next_sr

	sts	Smooth_l, xl
	sts	Smooth_h, xh
	rjmp	end_step

next_sr:		; Если 32-ой измерение, то делим результат на 32 и на индикатор
	ldi obnov, Disp_new	    ; Восстанавливаем счетчик циклов измерения
	sts	Smooth_l, ZER		; Очищаем память под накопительное значение
	sts	Smooth_h, ZER

	lsr   xh				; Делим накопленное значение на 32
	ror   xl	; /2
	lsr   xh
	ror   xl	; /4
	lsr   xh
	ror   xl	; /8
	lsr   xh
	ror   xl	; /16
	lsr   xh
	ror   xl	; /32
; ============================================================================
; И вот теперь мы имеем измеренную продолжительность периода синусоиды
; --- ОПРЕДЕЛЕНИЕ ДЕСЯТКОВ ---
    cpi  xl, $2d          ; Проверяем границу 60 Гц 
    brsh check_50
    ldi  r_tens, $a6        ; Индекс 0..44 -> частота 60..70 Гц
    rjmp	set_disp

check_50:
    cpi  xl, $6c         ; Проверяем границу 50 Гц 
    brsh check_40
    ldi  r_tens, $a5        ; Индекс 45..106 -> частота 50..60 Гц
    rjmp	set_disp

check_40:
    ; Так как мы уже проверили верхнюю границу при вычитании, 
    ; здесь гарантированно диапазон 40..50 Гц (Индекс 107..201)
    cpi	xl, $c9
    brsh too_little		      ; Если старший байт не 0, T_quad > 268+255 (частота < 40 Гц)
    ldi  r_tens, $a4
	rjmp	set_disp

; === Индикация выхода за границы измерений
too_little:
	ldi  xl, $c9
	rjmp	out_of_bounds
too_much:
	ldi  xl, $ca
out_of_bounds:
	ldi  r_tens, $aa

set_disp:
;	Здесь надо xl и r_tens перенести в показания индикатора

	ldi yh, HIGH(Pos)
	ldi yl, LOW(Pos)

	ldi zh, HIGH(Freq_Table*2)	; Ссылка на таблицу частот
	ldi zl, LOW(Freq_Table*2)

	add zl, xl			; Сдвигаем ссылку на измеренное значение
	adc	zh, ZER
	lpm	tmp_1, Z

	st	Y+, tmp_1		;Сохраняем полученную частоту для индикации
	st	Y, r_tens

end_step:
    clr  xl
    clr  xh                ; Сбрасываем основной счетчик `ticks` в 0

end_zero_cross:
	ret
; --- Упакованная BCD Таблица единиц и десятых частот
Freq_Table:
    .db $99, $97, $94, $92, $89, $87, $84, $82	; 000 60+ (0..2c)
    .db $79, $77, $74, $72, $70, $67, $65, $63	; 008
    .db $60, $58, $56, $53, $51, $49, $47, $44	; 010
    .db $42, $40, $38, $36, $33, $31, $29, $27	; 018
    .db $25, $23, $21, $19, $17, $15, $13, $11	; 020
    .db $09, $07, $05, $03, $01, $99, $97, $95	; 028 60+ ; 50+ (2d..6b)
    .db $93, $91, $90, $88, $86, $84, $82, $80	; 030
    .db $79, $77, $75, $73, $72, $70, $68, $66	; 038
    .db $65, $63, $61, $60, $58, $56, $55, $53	; 040
    .db $51, $50, $48, $47, $45, $43, $42, $40	; 048
    .db $39, $37, $36, $34, $33, $31, $30, $28	; 050
    .db $27, $25, $24, $22, $21, $19, $18, $17	; 058
    .db $15, $14, $12, $11, $10, $08, $07, $05	; 060
    .db $04, $03, $01, $00, $99, $97, $96, $95	; 068 50+ ; 40+ (6c..c8)
    .db $93, $92, $91, $90, $88, $87, $86, $84	; 070
    .db $83, $82, $81, $80, $78, $77, $76, $75	; 078
    .db $73, $72, $71, $70, $69, $68, $66, $65	; 080
    .db $64, $63, $62, $61, $60, $58, $57, $56	; 088
    .db $55, $54, $53, $52, $51, $50, $49, $47	; 090
    .db $46, $45, $44, $43, $42, $41, $40, $39	; 098
    .db $38, $37, $36, $35, $34, $33, $32, $31	; 0a0
    .db $30, $29, $28, $27, $26, $25, $24, $23	; 0a8
    .db $22, $21, $20, $19, $19, $18, $17, $16	; 0b0
    .db $15, $14, $13, $12, $11, $10, $09, $08	; 0b8
    .db $08, $07, $06, $05, $04, $03, $02, $01	; 0c0 
    .db $00, $ba, $cc							; 0c8 40+ (... c8) min max

Потерял расположение переменных в SRAM:

.dseg
Pos:	.byte 2		; 3 знакоместа в двоично десятичном формате байт 0 - 01, байт 1 - x2
Y1_L:	.byte 1
Y1_H:	.byte 1
Y2_L:	.byte 1
Y2_H:	.byte 1
RPE:	.byte 1		; Корекция ошибки с апроксимацией на последнем шаге.
Smooth_l: .byte 1
Smooth_h:  .byte 1

Предвижу критику на тему точности. Да, точность ±0.1 Гц достигнута только в области 40..55 Гц, далее начинает прихрамывать. Но мне этого достаточно.

Вот теперь проект закончен. Реализован в «железе», на останках сгоревшего китайского вольтметра. Удалось даже ограничить ток в сегментах 1кОм резисторами и избавиться от буферных транзисторов, ну и конечно, инвертировать логику отображения разряда. Дальнейшие труды в попытке заставить работать проект на внутреннем RC-генераторе с компенсацией температурного дрейфа в предыдущей статье https://habr.com/ru/articles/1104396/.

Комментарии (6)


  1. zambezi9
    10.11.2026 11:15

    На макетке был установлен один из самых простых и легких микроконтроллеров ATtiny13

    Для стабильной работы частотомера был применен внешний генератор на кварцевом резонаторе 8МГц.

    Суп из топора получается. Если самый простой и легкий микроконтроллер оказался слишком простым из-за отсутствия возможности подключить кварц, то берут самый простой в котором есть такая возможность, например ATtiny25. Генератор скорее всего дороже и ATtiny13, и ATtiny25 вместе с кварцем.

    в симуляторе сразу внесена помеха "пила" амплитудой 1 В и частотой 130 Гц

    У вас полезная полоса 40..70 Гц и нигде не видно следов "железного" фильтра, который бы пропускал 55 Hz, а по краям срезал бы амплитуду скажем в два раза. Нельзя пропускать на вход что угодно даже, если сильно хочется поупражняться с программными фильтрами.

    Начну с выбора частоты и режима работы таймера. Планировалось делать выборки АЦП на частоте 8 кГц, чтобы получить точность измерения частоты ±0.1Гц. 

    При чем здесь АЦП? Детектировать переход через ноль можно проще и точнее, а дребезг перехода можно избежать просто не детектируя контроллером переход раньше чем должен был бы прийти следующий переход на верхней частоте. Можно просто считать сколько пустых циклов кода поместится между переходами сигнала через ноль, раз уж эти микроконтроллеры не имеют Capture/Compare возможностей.


  1. Andrey_314 Автор
    10.11.2026 11:15

    1. Под рукой был именно ATtiny13, а не другие чипы. И учился я задолго до лозунга "Медленно работает программа - купите новый компьютер!". Суп получился когда я прикрутил к нему DS18B20 для компенсации температурного дрейфа и запустил на внутреннем RC-генераторе 9.6МГц.

    2. Да. Весь смысл был обойтись минимумом без "железных" фильтров. Подключение к прибору вместо синусоиды "триугольников", "квадратов" и "миандров" не вывело его из душевного равновесия и показания остались в пределах заданной точности.

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


    1. Dozer88
      10.11.2026 11:15

      Железный фильтр нужен, чтобы избежать алиасинга при наличии в спектре частот больше Fs/2, а особенно Fs*2*n+-50


      1. Andrey_314 Автор
        10.11.2026 11:15

        Частота дискретизации 4.6 кГц, а фильтр работает на частотах 30..70 Гц. Частоту 130 Гц он уже режет хорошо. Так что нарваться на алиасинг сложно. Проблема скорее в сложении шумов на низких частотах и получение частоты в измеряемом диапазоне, но это и "железным" фильтром тяжело выловить. В общем, прибор справился с поставленной целью. Мы им меряли и 50 Гц и 60 Гц во всяких разных генерирующих устройствах. Поначалу, иногда возбуждался. Но после встраивания ограничителей в каскады фильтра, работает очень стабильно.


        1. Dozer88
          10.11.2026 11:15

          Подайте 4550 или 9150 Гц...


  1. VT100
    10.11.2026 11:15

    ; Сдвиг на 4 (деление на 16)
    asr r17
    ror r16
    …

    Предполагаю, что колебания происходят из-за потери точности внутри фильтра. См. раздел 4 (с таблицей) в проекте с фиксированной точкой на ATtiny15. Попробуйте оставить полную разрядность внутри фильтра, а деление перенести на выдачу данных в расчёт частоты. Или смоделируйте в каком-нибудь математическом САПР’е.

    reset_y1:
    mov r18, ZER ; Насильно гасим автоколебания в ноль
    mov r19, ZER
    …

    При переполнении умножения с накоплением, ЕМНИП, ЦСП от БезналоговыхДевиц фиксируются на максимум или минимум, а никак не на 0.