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