Частотомер с БИХ-фильтром НЧ в 1 Кбайте на ATtiny13 и на что способен этот «малыш»

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

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

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

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

.org 0x000aInit:сlilditmp_0, LOW(RAMEND)outSPL, tmp_0clrZER; Настройка портовoutPortB, ZERlditmp_0, (1<<DAT|1<<SH|1<<LC) outDDRB, tmp_0; Настройка таймераlditmp_0, $00outTCCR0A, tmp_0lditmp_0, (1<<CS01); Fпрерывания = f / 8 / 256 = 4687,5 Гц Tt = 213 мкс (В периоде синуса 50Гц примерно 94 такта).outTCCR0B, tmp_0 lditmp_0, 213outOCR0A, tmp_0 lditmp_0, (1< Опорное напряжение VCC; MUX1:0 = 00 -> Вход ADC2 (пин PB4)lditmp_0, P_ADC outADMUX, tmp_0; ADATE = 1 -> Включить автотриггер; ADPS2:0 = 110 -> Делитель частоты АЦП на 64 (9.6 МГц / 64 = 150 кГц); Одно преобразование займет 13 тактов АЦП = 87 мксlditmp_0, (1<<ADEN|1<<ADATE|1<<ADSC|1<<ADPS2|1< Источник триггера: переполнение таймера; Как только таймер дойдет до CompA, АЦП сам начнет замер, без участия программы!lditmp_0, (1<<ADTS1|1<<ADTS0)outADCSRB, tmp_0; Ну и наконец исходные значения переменныхldirdis, period_displdipseg, 2ldiobnov, Disp_newclrxlstsSmooth_l, ZERstsSmooth_h, ZERsei

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

Loop: rjmpLoop

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

; Прерывание по тику таймера Timer_tick:; Читаем АЦПinr20, ADCLinr21, ADCHsubir21, $02 ; Центрируем АЦП - 512 (r21:r20 = X0); Запускаем выполнение БИХ-фильтра синусоиды 40-70Гц; На входе x0 - r21:r20; На выходе y1 - r21:r20, y0 - r17:r16rcallfilter_tick; Расчет частоты в случае перехода синусоиды через ноль снизу в верх; На входе все те же y1 - r21:r20, y0 - r17:r16rcallCount_freqdecrdis; Уменьшаем предделитель для индикации на 150 Гц (50 Гц * 3 разряда)brneEnd; Пока не достигнут 0 не трогаем дисплейldirdis, period_disp;Восстанавливаем значение пред делителя индикации; Установим на дисплее разряд psegrcallDispdecpseg; Переходим к меньшему разряду brplEnd ; Если положительное, то закончимldipseg, 2 ; Иначе вернемся к разряду 2End: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 = разряд / 2add yl, tmp_0; Сдвигаем ссылку на разряд / 2 (в одном байте два разряда)adc yh, ZERld tmp_0, y ; tmp_0 = байту из двух разрядов для отображенияsbrc pseg, 0; Если разряд нечетный, то продолжаемswaptmp_0; иначе меняем местами полубайтыandi tmp_0, $0f ; Убираем старший ненужный полубайт.ldi zh, HIGH(Dig*2); Заполняем регистр Z ссылкой на знакогенераторldi zl, LOW(Dig*2)add zl, tmp_0; Смещаем ссылку на символ с кодом для отображенияadc zh, ZERlpm tmp_0, z; Получаем матрицу для 7 сегментного индикатора нужного символа; Всегда рисуем точку в среднем сегментеcpipseg, 1brnewo_pointsbrtmp_0, $08; Продвинем биты из tmp_0 в 164/595 регистр wo_point:cbi PortB, LC; Убираем сигнал продвижения байта на выход для 595 регистраldi tmp_1, 8; Цикл на 8 тактовNextS:cbi PortB, SH; Убираем сигнал сдвига с регистра 595sbrc tmp_0, 7; Выставляем значение DAT по 7-мо битуrjmp SetBitDISP_LOWrjmp ShiftSetBit:DISP_HIGHShift:sbiPortB, SH; Выставляем строб сдвига регистра 595lsl tmp_0; Сдвигаем данные на 1 бит влевоdec tmp_1; уменьшаем значение счетчикаbrne NextS; если не 0, то повторяем циклsbi PortB, LC; Продвигаем байт 595 регистра на выход; А вот тепрь установим сигналы управления разрядамиcpi pseg, 2; Если разряд != 2, то переходим к разряду 1brne Vis1; иначе включаем разряд 2;sbi PortB, LC; Разряд 2 не трогаем, экономим. При передаче сигнал остался в 1cbi PortB, SH; Гасим разряд 0 cbi PortB, DAT; Гасим разряд 1rjmp EndDispVis1:cpi pseg, 1; Если разряд != 1, то переходим к разряду 0brne Vis0; иначе включаем разряд 1sbi PortB, DAT; Включаем разряд 1cbi PortB, LC; Гасим разряд 2cbi PortB, SH; Гасим разряд 0rjmp EndDispVis0: cpi pseg, 0; Если разряд != 0 – что-то пошло не так! Разряд = 2breq Vis2; иначе включаем разряд 0ldipseg, $02Vis2:;sbi PortB, SH; Разряд 0 не трогаем, экономим. При передаче сигнал остался в 1cbi PortB, DAT; Гасим разряд 1cbi PortB, LC; Гасим разряд 2ret; ======== Знакогенератор =========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 ===ldsr18, Y1_Lldsr19, Y1_H ; Достали старый Y1movwr17:r16, r21:r20 ; Копируем X0subr20, r18sbcr21, r19 ; r21:r20 = (X0 - Y1); Сдвиг на 4 (деление на 16) asrr21 rorr20 asrr21 rorr20 asrr21 rorr20 asrr21 rorr20addr18, r20adcr19, r21 ; r19:r18 = Новый Y1 ; ===--- ЛИМИТЕР ДЛЯ Y1 (защита от высокочастотного возбуждения) ---=== cpi r19, $02 brge reset_y1 ; Если старший байт >= 2, улетели вверх cpi r19, $fd brlt reset_y1 ; Если старший байт > 4 ===ldsr20, Y2_Lldsr21, Y2_H ; Достали старый Y2movwr17:r16, r19:r18 ; Берем результат первого каскада (Y1)subr16, r20sbcr17, r21 ; r17:r16 = (Y1 - Y2); Сдвиг на 4 (деление на 16) asrr17 rorr16 asrr17 rorr16 asrr17 rorr16 asrr17 rorr16addr20, r16adcr21, r17 ; r21:r20 = Новый Y2 (Финальный выход фильтра!); ===--- ЛИМИТЕР ДЛЯ Y2 (защита от высокочастотного возбуждения) ---=== cpi r21, $02 brge reset_y2 cpi r21, $fd brlt reset_y2 rjmp save_y2reset_y2: mov r20, ZER ; Если второй каскад пошел в разнос — гасим его mov r21, ZERsave_y2:ldsr16, Y2_Lldsr17, Y2_HstsY2_L, r20stsY2_H, r21; На выходе: в r21:r20 находится y1 (текущий отфильтрованный отсчет) ; r17:r16 находится y2 (предыдущий отфильтрованный отсчет)ret

Полученные значения y2 (предыдущее) и y1 (текущее) являются 16-ти битными знаковыми значениями отфильтрованного входного сигнала. И теперь можно определять момент когда y2 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:incxl; ticks++brnenext_if; Если не произошло переполнение счетчика продолжим выполнениеrjmptoo_little; в противном случае индикация малой частотыnext_if:; --- Проверка перехода через ноль (снизу вверх) --- sbrs r21, 7 ; Проверяем знаковый (7-й) бит старшего байта y1. Если 1 (минус) — идем дальше rjmpend_zero_cross ; Если y1 >= 0, перехода снизу вверх быть не может sbrc r17, 7 ; Проверяем знаковый бит y0. Если 0 (плюс) — переход есть rjmpend_zero_cross ; Если y0  70 Гц); Измерение успешно! В xl у нас чистый 8-битный индекс для таблицы Freq_Table.; Теперь в xl находится чистый Индекс = T_quad - 268; ===========================================================================; --- УЛЬТРА-ЛЕГКИЙ ФИЛЬТР СГЛАЖИВАНИЯ ПОКАЗАНИЙ ---; На входе: xl = свежий вычисленный индекс (0..201); Получаем среднее значение за 32 цикла измеренияldsr18, Smooth_lldsr19, Smooth_haddxl, r18adcxh, r19decobnovbreqnext_srstsSmooth_l, xlstsSmooth_h, xhrjmpend_stepnext_sr:; Если 32-ой измерение, то делим результат на 32 и на индикаторldi obnov, Disp_new ; Восстанавливаем счетчик циклов измеренияstsSmooth_l, ZER; Очищаем память под накопительное значениеstsSmooth_h, ZERlsr xh; Делим накопленное значение на 32ror xl; /2lsr xhror xl; /4lsr xhror xl; /8lsr xhror xl; /16lsr xhror xl; /32; ============================================================================; И вот теперь мы имеем измеренную продолжительность периода синусоиды; --- ОПРЕДЕЛЕНИЕ ДЕСЯТКОВ --- cpi xl, $2d ; Проверяем границу 60 Гц brsh check_50 ldi r_tens, $a6 ; Индекс 0..44 -> частота 60..70 Гц rjmpset_dispcheck_50: cpi xl, $6c ; Проверяем границу 50 Гц brsh check_40 ldi r_tens, $a5 ; Индекс 45..106 -> частота 50..60 Гц rjmpset_dispcheck_40: ; Так как мы уже проверили верхнюю границу при вычитании, ; здесь гарантированно диапазон 40..50 Гц (Индекс 107..201) cpixl, $c9 brsh too_little ; Если старший байт не 0, T_quad > 268+255 (частота < 40 Гц) ldi r_tens, $a4rjmpset_disp; === Индикация выхода за границы измеренийtoo_little:ldi xl, $c9rjmpout_of_boundstoo_much:ldi xl, $caout_of_bounds:ldi r_tens, $aaset_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; Сдвигаем ссылку на измеренное значениеadczh, ZERlpmtmp_1, ZstY+, tmp_1;Сохраняем полученную частоту для индикацииstY, r_tensend_step: clr xl clr xh ; Сбрасываем основной счетчик `ticks` в 0end_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

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

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

Источник: habr.com

0 0 голоса
Рейтинг новости
1
0
Подписаться
Уведомить о
0 комментариев