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

Комментарии 0