В этой статье приводится пример простейшей реализации цифрового «умножения на два» частоты ШИМ-сигнала с сохранением скважности (по буржуйски такие штуки называются PWM-converter).
Нафига оно надо? Ну, например, можно увеличить частоту ШИМ контроллера светодиодов какой-нибудь подсветки в автомобиле или ещё где-нибудь.
В качестве сердца (а точнее мозга) нашего умножителя используется микроконтроллер ATTiny13. Он оцифровывает входной сигнал, определяет его частоту и скважность, вычисляет параметры и генерирует выходной сигнал.
Для оцифровки входного сигнала используется встроенный в ATtiny13 таймер. В режиме FastPWM этот таймер каждый раз при достижении значения, записанного в регистр OCR0A, генерирует прерывание, а сам при этом обнуляется. Это позволяет изменяя записанное в регистр OCR0A значение генерировать прерывания через нужные промежутки времени. Например, если OCR0A=127, предделитель таймера равен 1 и контроллер работает от встроенного генератора на 9,6 МГц, то прерывания будут генерироваться с частотой 75 кГц (9600/128=75). Эту частоту дискретизации мы и будем использовать.
Для отсчёта периода и длительности импульса входного сигнала будем использовать два 16-ти битных счётчика (выделим для этого две пары 8-ми битных регистров). Это позволит нам оцифровать с разрешением не менее 7 бит сигналы с частотой примерно от 1 Гц (75000/216) до примерно 600 Гц (75000/27). В этом случае минимальное разрешение выходного сигнала будет 6 бит. Таким образом минимальная точность нашего умножителя составит около 1,5% (100/26).
Ещё два 16-ти битных регистра (два раза по два регистра по 8 бит разумеется) будем использовать для хранения вычисленных значений периода и длительности импульса выходного сигнала и один 16-ти битный счётчик для отсчёта этих значений.
Вот в общем-то и вся идея, но прежде чем оформить её в виде алгоритма, давайте нарисуем графики входных и выходных сигналов, разделим все важные точки на этих графиках на несколько типов и для каждого типа точек распишем, что нам нужно в этих точках делать. Итак, график:
Как видно из графика, у нас есть 3 типа точек для входного сигнала:
Точки 1 на нашем графике могут обозначать сразу два события: «конец очередного периода» и «начало нового периода». Для каждого из этих собый нам необходимо выполнить определённые действия.
Для события «конец очередного периода» нужно рассчитать новые значения параметров выходного сигнала, а для события «начало нового периода» нужно обнулить счётчики периода и длительности входного сигнала, то есть засечь параметры входного сигнала.
Нужно понимать, что изменение сигнала с низкого до высокого уровня не всегда соответствует событию «конец очередного периода». Например, при первом после включении контроллера изменении сигнала с низкого до высокого уровня, никакого предыдущего периода у нас ещё нет, соответственно, и никакого события «конец очередного периода» тоже нет, и вычислять параметры выходного сигнала нам не из чего.
Для того, чтобы отличать, наступило ли событие «конец очередного периода» будем использовать специальный флаг STARTF. Будем устанавливать этот флаг каждый раз при обработке события «начало нового периода» и проверять перед обработкой события «конец очередного периода» (о том, когда этот флаг будет сбрасываться поговорим немного позже).
Точки 2 на нашем графике отмечают «конец импульса» входного сигнала. В этих точках нам нужно останавливать счётчик, отсчитывающий длительность импульса входного сигнала. Длительность паузы мы засекать не будем, поскольку засекаем длмтельность периода целиком. Признаком остановки счётчика, отсчитывающего длительность импульса, также назначим специальный флаг — IINCEN. В точках 2 будем этот флаг поднимать и считать это признаком остановки счётчика. Сбрасывать флаг будем в начале нового периода (то есть в точках 1).
В точках 3 не происходит ничего особенного, тут нужно просто увеличивать нужные счётчики. Либо только счётчик периода входного сигнала, либо вместе со счётчиком времени импульса входного сигнала.
Тут есть один важный момент. В любой из описанных выше точек может произойти переполнение счётчика периода входного сигнала. Это будет означать, что сигнал не изменялся в течении всего времени, которое мы можем засечь. В этом случае будем считать, что наш сигнал имеет коэффициент заполнения 0% или 100% в зависимости от того, при каком уровне сигнала произошло переполнение. Как раз в этом случае нужно будет сбросить флаг STARTF. Кроме того будем устанавливать в ноль время импульса выходного сигнала для случая, когда коэффициент заполнения равен нулю, и время импульса выходного сигнала больше периода выходного сигнала для случая, когда коэффициент заполнения равен 100%.
Признаком переполнения счётчика периода входного сигнала будет установка специального флага TINCF.
Теперь перейдём к точкам на графике выходного сигнала.
Точки 4 соответствуют времени импульса выходного сигнала. В этих точках должен быть установлен высокий уровень выходного сигнала. Определять их будем по факту того, что в этих точках значение счётчика для выходного сигнала меньше, чем вычисленное значение времени импульса выходного сигнала.
Ну и наконец точки 5 соответствуют времени паузы выходного сигнала. Эти точки будем определять по факту того, что в них значение счётчика для выходного сигнала больше или равно вычисленному времени импульса выходного сигнала.
Обнулять счётчик выходного сигнала будем в начале каждого нового периода входного сигнала, а также при достижении им вычисленного значения периода выходного сигнала.
Описанные для точек 4 и 5 действия будем выполнять только для случаев, когда коэффициент заполнения выходного сигнала не установлен равным 0% или 100%. В последних двух случаях будем просто устанавливать выход равным нулю или единице без всяких дополнительных манипуляций.
Всё, теперь можно приступать к написанию привычного алгоритма в виде блок-схемы. Весь алгоритм рисовать смысла нет, всё равно всё интересное происходит только по прерыванию таймера, вот алгоритм обработчика этого прерывания и нарисуем.
Алгоритм:
.device ATtiny13 .include "tn13def.inc" .list ;-- определяем свои переменные .def w1=r16 ; это будет наш аккумулятор .def w2=r17 ; а это - ещё один .def Flags=r18 ; флаги .def TIN_H=r19 ; счётчик периода входного сигнала, старший регистр .def TIN_L=r20 ; счётчик периода входного сигнала, младший регистр .def IIN_H=r21 ; счётчик времени импульса вх-го сигнала, старший рег. .def IIN_L=r22 ; счётчик времени импульса вх-го сигнала, младший рег. .def TOUT_H=r23 ; расчёт.значение периода вых-го сигнала, старший рег. .def TOUT_L=r24 ; расчёт.значение периода вых-го сигнала, младший рег. .def IOUT_H=r25 ; расчёт.знач-е длит-ти имп-са вых-го сигнала, ст.рег. .def IOUT_L=r26 ; расчёт.знач-е длит-ти имп-са вых-го сигнала, мл.рег. .def OC_H=r27 ; счётчик для выходного сигнала .def OC_L=r28 ; счётчик для выходного сигнала .equ TINCF=0 ; флаг переполнения счётчика периода вх-го сигнала .equ IINCEN=1 ; флаг отсчёта времени импульса (сброшен - считаем) .equ STARTF=2 ; флаг начала цикла .equ TOprosa=127 ; 9,6 МГц/128 = 75 кГц ;-------------------------- .equ PWM_IN=1 ; вход ШИМ .equ PWM_OUT=0 ; выход ШИМ ;-- Используемые регистры ; SPL - указатель вершины стека ; ACSR - управление компаратором ; DDRB - направление работы ног ; PORTB - выходы порта ; TCCR0B - управление таймером ; TIMSK0 - прерывания от таймера ; TCNT0 - значение таймера ; OCR0A - значение модуля сравнения ; TIFR0 - флаги прерываний от таймера ; PB0 - выход ШИМ, PB1 - вход ;-- начало программного кода .cseg .org 0 rjmp Init ; переход на начало программы ;-- вектора прерываний reti ; INT0 reti ; Pin Change rjmp TimerOverflow ; Timer reti ; EEPROM reti ; comparator reti ; timer compare match A reti ; timer compare match B reti ; watchdog reti ; ADC ;-- начало программы Init: ldi w1,RAMEND ; устанавливаем указатель вершины out SPL,w1 ; стека на старший байт RAM sbi ACSR,7 ; выключаем компаратор ldi w1,1<<PWM_OUT ; определяем входы и выходы порта out DDRB,w1 clr w1 ; подтягивающие резисторы выключены, начальное out PORTB,w1 ; состояние выхода = 0 ;-- настраиваем таймер ldi w1,(1<<WGM00)|(1<<WGM01) out TCCR0A,w1 ; выходы OCR0A, OCR0B отключены, Fast PWM ldi w1,TOprosa ; значение, по которому будет срабат.прерыв. out OCR0A,w1 ldi w1, 1<<TOIE0 ; прерывание по переполнению out TIMSK0,w1 ;--- запрещаем считать время импульса sbr Flags,1<<IINCEN ; устанавливаем флаг ;-- ШИМ=0% соответствует случаю, когда (IOUT_H OR IOUT_L)=0 ;-- ШИМ=100% соответствует случаю, когда (IOUT_H OR IOUT_L) ;-- не равно 0, а (TOUT_H OR TOUT_L)=0 ;-- Первоначально ШИМ=0% ;-- включаем таймер и прерывания ldi w1,0b00001001 ; включить таймер, Fast PWM, без предделителя out TCCR0B,w1 sei ;-- начало работы Start: rjmp Start ;---------------------------------------------------------------------- ;--- Interrupt --- TimerOverflow: ;-- читаем значение на входе и определяем изменилось оно или нет in w2,PINB ; читаем входы порта rcall IncCounterTIN ; увеличиваем счётчик периода вх.сигнала clr w1 bld w1,PWM_IN ; выгружаем бит T в бит PWM_IN регистра W bst w2,PWM_IN ; загружаем новое значение в бит T andi w2,1<<PWM_IN ; стираем в w2 все биты, кроме вх.пина eor w1,w2 ; изменилось ли состояние? breq NoChanged ; прыгаем, если не изменилось ;--- Changed: brts FromLowToHi ; если T=1 - уров.измен-ся с низк.на выс. ;-- с высокого на низкий FromHiToLow: ; конец импульса sbrc Flags, TINCF ; если переполн.нет - пропустить команду rjmp Changed_HL_OVF Changed_HL_No_OVF: rcall IncCounterIIN ; увелич.счётчик длительности импульса sbr Flags,1<<IINCEN ; прекращаем отсчёт времени импульса rjmp Update_Output Changed_HL_OVF: ser IOUT_L clr TOUT_H clr TOUT_L ; ШИМ=100% rcall ClrCounters ; сбрасываем счётчики clr Flags ; сбрасываем флаги rjmp Update_Output ;-- с низкого на высокий FromLowToHi: ; конец цикла, начало нового цикла sbrc Flags, TINCF ; если переполн.нет - пропустить команду rjmp Changed_LH_OVF Changed_LH_No_OVF: sbrc Flags, STARTF ; пропустить команду, если флаг STARTF=0 rcall CalcToutIout Changed_LH_No_Calc: clr Flags sbr Flags, 1<<STARTF ; поднимаем флаг rcall ClrCounters rjmp Update_Output Changed_LH_OVF: clr IOUT_H ; ШИМ=0 clr IOUT_L rjmp Changed_LH_No_Calc ;-- не изменился NoChanged: sbrc Flags, TINCF ; если переполн.нет - пропустить команду rjmp NoChanged_OVF NoChanged_No_OVF: sbrs Flags,IINCEN ; пропустить команду, если флаг установлен rcall IncCounterIIN rjmp Update_Output NoChanged_OVF: clr IOUT_L clr IOUT_H clr TOUT_H clr TOUT_L ; ШИМ=0% brtc Next1 ; если флаг сброшен - оставляем ШИМ=0% ser IOUT_L ; ШИМ=100% Next1: rcall ClrCounters clr Flags ; сбрасываем флаги ;-- обрабатываем выход Update_Output: mov w1, IOUT_L or w1, IOUT_H ; IOUT_H v IOUT_L brne NotNull ; если ШИМ не 0% cbi PORTB,PWM_OUT ; clear PWM_OUT to 0 reti NotNull: mov w1, TOUT_H or w1, TOUT_L ; TOUT_H v TOUT_L brne NotFull ; если ШИМ не 100% sbi PORTB,PWM_OUT ; set PWM_OUT to 1 reti NotFull: rcall IncCounterOC cp OC_L, TOUT_L brne Next2 cp OC_H, TOUT_H brne Next2 clr OC_L clr OC_H Next2: cp OC_H, IOUT_H ; OC_H - TOUT_H brlo Lower ; jump if OC_H lower IOUT_H brne HierOrEqual ; jump if OC_H bigger IOUT_H cp OC_L, IOUT_L ; compare if OC_H equal IOUT_H brlo Lower HierOrEqual: cbi PORTB,PWM_OUT ; clear PWM_OUT to 0 reti Lower: sbi PORTB,PWM_OUT ; set PWM_OUT to 1 reti ;----------------------- ;--- Процедурки -------- IncCounterTIN: inc TIN_L brne No_Z1 inc TIN_H brne No_Z1 sbr Flags, 1<<TINCF ; set TINCF No_Z1: ret IncCounterIIN: inc IIN_L brne No_Z2 inc IIN_H No_Z2: ret IncCounterOC: inc OC_L brne No_Z3 inc OC_H No_Z3: ret CalcToutIout: mov TOUT_H,TIN_H ; copy TIN_H->T_OUTH, TIN_L->T_OUTL mov TOUT_L,TIN_L clc ; clear carry flag ror TOUT_H ; делим общую частоту на 2 ror TOUT_L mov IOUT_H,IIN_H ; copy IIN_H->IOUT_H, IIN_L->IOUT_L mov IOUT_L,IIN_L clc ror IOUT_H ; делим время импульса на 2 ror IOUT_L ret ClrCounters: clr TIN_L clr TIN_H clr IIN_L clr IIN_H ser OC_H ; устанавливаем OC = -1 ser OC_L ; (set OC to -1) ret |
Вот и вся программа. Чтобы всё корректно работало — в контроллере должны быть «запрограммированы» фьюзы SPIEN, SUT0 и CKSEL0 (то есть в PonyProg напротив них должны стоять галочки).
Скачать одним архивом алгоритм, исходники и прошивку
P.S. Хочу кое-что уточнить относительно частоты дискретизации и точности.
Как я писал выше, изменять частоту дискретизации можно изменяя значение, загружаемое в регистр OCR0A. Однако, нужно понимать, что бездумно уменьшать это значение для увеличения частоты дискретизации нельзя. Это связано с тем, что для нормальной работы устройства обработчик прерывания от таймера должен успеть полностью отработать до того момента, когда произойдёт следующее прерывание. Другими словами, минимальное время между прерываниями таймера должно быть несколько больше, чем время, необходимое для работы обработчика.
Что касается точности, то 1,5% мы получили бы в идеальном случае, если бы обработчик прерывания выполнялся мгновенно. Поскольку в реальности на его выполнение затрачивается некоторое время, то точность будет несколько ниже. Однако, поскольку мы точно знаем, что выполняться обработчик должен ни в коем случае не дольше одного цикла таймера (от прерывания до прерывания), значит в реальности наихудшая точность, которую мы можем получить, будет равна не одному, а двум циклам нашего таймера, т.е. не полтора, а 3%.
P.P.S. Ниже можно увидеть осциллограммы, полученные в процессе тестирования. Слева — вход, справа — выход.
Хорошо написанная теоритическая статья. На практике все не так радужно. Устройство собрано, протестированно. Результат: выходной сигнал имеет очень большой джиттер. При подаче входного сигнала, сигнал на выходе появляется ~ через ~900мс, при снятии входного сигнала — выход пропадает через ~900мс/ Неустойчивая работа при Кзап 1% и 99% (связано напрямую с джиттером)
Вот такая арифметика.
Вывод — опубликованный проект «сырой» и не доработанный.
p.s. и неплохо указать, для начинающих какой вывод вход, а какой выход. Другими словами схема точно не обременит.
Спасибо.
🙂 Ну, в общем, согласен. То, что доработано обычно попадает в раздел «Магазин».
С другой стороны, мне кажется логичной задержка в один цикл. А как ещё узнаешь, какая должна быть частота на выходе, пока хотя бы один полный цикл не пройдёт?
Как узнать, что сигнал на выходе уже нужно переключать в ноль, если мы ещё даже не знаем сколько времени входной сигнал будет находится в единице? Пока хотя бы один полный цикл не пройдёт — у нас просто нет достаточных данных о сигнале.
Ну а неустойчивая работа на границе — это тоже нормально. Это напрямую связано с частотой дискретизации. Возьмите чип побыстрее и область устойчивой работы расширится. Но, опять же, что-то я не очень вижу вариант, при котором её можно расширять чтобы охватить весь диапазон, прямо от 0 до 100%.
Так что не всё так уж плохо. Просто надо понимать области применимости и логику работы.
Ознакомьтесь с проектом https://github.com/pyrotron/FreqMul. Все отлично работает. Программа «вылизана». Никаких косяков и задержек! На входе 180 гц, на выходе 23 кгц! Коэффициент любой. Вот и вся логика. Человек сделал ПРОЕКТ.
Ознакомился. Полезная ссылка, пусть будет тут. Жаль комментариев нет. Может опишите в чём там идея? Как это работает? 🙂
Теперь вернёмся к тому, откуда начали. Вам нужно переключать выходной сигнал во сколько то раз быстрее входного. Как узнать когда именно нужно делать переключения на выходе, не зная параметров входного сигнала? Как узнать эти параметры пока на входе не прошло ни одного цикла?
Получается, что вам нужно несколько раз переключить выход перед тем, как вы полностью измерите входной сигнал. Это прям чудеса прогнозирования.
Вот, все подробно разжевано https://habr.com/ru/post/448730/, задержка — 1 период входного сигнала. Джиттера нет!
Вот видите, задержка всё таки есть.
Теперь по поводу джиттера. В проекте заранее известно какими ступеньками меняется скважность и введён порог для нормального распознавания очередной ступеньки. Потому и нет джиттера.
Видите как удобно, — в интернете куча разных вариантов, вы можете всё изучить, хорошенько обдумать и выбрать тот, который вам больше подойдёт.
Задержка 1 период входного сигнала, а не 1200мс. Джиттер — это «дрожание» фронта, никакого отношения к тому, что Вы написали не имеет отношения. Насчет обучения: в течении 67 лет я достаточно все изучил. Так что изучайте мат. часть и создавайте практически работающие проекты. Удачи!
Спасибо. Я постоянно учусь и практически работающие проекты тоже постоянно создаю. При этом некоторые даже бесплатно отдаю.
Ваши глубокие знания не должны пропасть. Поделитесь анализом, в чём принципиальные различия двух проектов, за счёт чего один работает лучше, чем другой, откуда появляется джиттер и как его избежать. Вот прям цены такому анализу не будет. Допишу прямо к статье постскриптумом.