Наш магазин на eBay Наш магазин на AliExpress Наш канал в telegram

Цифровое умножение частоты ШИМ-сигнала на два
(на микроконтроллере ATtiny13)

В этой статье приводится пример простейшей реализации цифрового «умножения на два» частоты ШИМ-сигнала с сохранением скважности (по буржуйски такие штуки называются 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. Ниже можно увидеть осциллограммы, полученные в процессе тестирования. Слева — вход, справа — выход.

Осциллограммы работы цифрового умножителя частоты ШИМ

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

  • Хорошо написанная теоритическая статья. На практике все не так радужно. Устройство собрано, протестированно. Результат: выходной сигнал имеет очень большой джиттер. При подаче входного сигнала, сигнал на выходе появляется ~ через ~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 лет я достаточно все изучил. Так что изучайте мат. часть и создавайте практически работающие проекты. Удачи!

    • Спасибо. Я постоянно учусь и практически работающие проекты тоже постоянно создаю. При этом некоторые даже бесплатно отдаю.

      Ваши глубокие знания не должны пропасть. Поделитесь анализом, в чём принципиальные различия двух проектов, за счёт чего один работает лучше, чем другой, откуда появляется джиттер и как его избежать. Вот прям цены такому анализу не будет. Допишу прямо к статье постскриптумом.

  • Можно ли «допилить» проект, чтоб умножал входящую частоту на 5 и делал скважность 2. Входной сигнал — скважность примерно 3, частота изменяется от 6 до 60 Гц. Раздел «Магазин». Спасибо!

Добавить комментарий