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

Andrey_314 Автор
10.11.2026 11:15Под рукой был именно ATtiny13, а не другие чипы. И учился я задолго до лозунга "Медленно работает программа - купите новый компьютер!". Суп получился когда я прикрутил к нему DS18B20 для компенсации температурного дрейфа и запустил на внутреннем RC-генераторе 9.6МГц.
Да. Весь смысл был обойтись минимумом без "железных" фильтров. Подключение к прибору вместо синусоиды "триугольников", "квадратов" и "миандров" не вывело его из душевного равновесия и показания остались в пределах заданной точности.
На картинке симулятора, на осциллографе, как-раз, заметно как справляются штатные измерители частоты. :-) И да, я могу себе позволить заниматься искусством программирования и применения математики в практических целях. :-)

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

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

VT100
10.11.2026 11:15; Сдвиг на 4 (деление на 16)
asr r17
ror r16
…Предполагаю, что колебания происходят из-за потери точности внутри фильтра. См. раздел 4 (с таблицей) в проекте с фиксированной точкой на ATtiny15. Попробуйте оставить полную разрядность внутри фильтра, а деление перенести на выдачу данных в расчёт частоты. Или смоделируйте в каком-нибудь математическом САПР’е.
reset_y1:
mov r18, ZER ; Насильно гасим автоколебания в ноль
mov r19, ZER
…При переполнении умножения с накоплением, ЕМНИП, ЦСП от БезналоговыхДевиц фиксируются на максимум или минимум, а никак не на 0.
zambezi9
Суп из топора получается. Если самый простой и легкий микроконтроллер оказался слишком простым из-за отсутствия возможности подключить кварц, то берут самый простой в котором есть такая возможность, например ATtiny25. Генератор скорее всего дороже и ATtiny13, и ATtiny25 вместе с кварцем.
У вас полезная полоса 40..70 Гц и нигде не видно следов "железного" фильтра, который бы пропускал 55 Hz, а по краям срезал бы амплитуду скажем в два раза. Нельзя пропускать на вход что угодно даже, если сильно хочется поупражняться с программными фильтрами.
При чем здесь АЦП? Детектировать переход через ноль можно проще и точнее, а дребезг перехода можно избежать просто не детектируя контроллером переход раньше чем должен был бы прийти следующий переход на верхней частоте. Можно просто считать сколько пустых циклов кода поместится между переходами сигнала через ноль, раз уж эти микроконтроллеры не имеют Capture/Compare возможностей.