Часы с интерфейсом I2C на микроконтроллере

June 27, 2010 by admin Комментировать »

Моделей микросхем RTC, о которых мы уже неоднократно упоминали, су­ществует множество. Все они внутри устроены примерно одинаково и имеют вЬтроенный счет времени и календарь, а также функции будильника и/или таймера. Заметим, что все RTC имеют то или иное количество ячеек памяти SRAM (в нее, например, традиционно записываются установки BIOS в ком­пьютере). Подавляющее большинство RTC имеют возможность автономной работы от батарейки в течение длительного времени, без потери однажды установленного времени. Такие часы используют обычно кварц на 32 768 Гц, иногда даже встроенный в микросхему. Кроме этого, значительная часть мо­делей имеет дополнительный выход (иногда и не один), на котором форми­руется некая частота, задаваемая программно. Этот выход можно использо­вать для управления прерыванием микроконтроллера, и таким образом организовать счет времени и его индикацию.

Еще одна особенность микросхем RTC — величины времени в них традици­онно представлены в десятичном виде (то есть в упакованном BCD-формате). В том числе, именно так выдаются значения времени в RTC, встроенных в ПК. Например, число минут, равное 59, так и выдается, как байт со значени­ем $59. BCD-представление удобно для непосредственной индикации, но при выполнении арифметических операций со временем (или, например, опера­ций сравнения) его приходится преобразовывать к обычному двоичному ви­ду. На самом деле это почти не доставляет неудобств, скорее наоборот.

Для наших целей выберем модель RTC под названием DS1307. Это простей­шие часы с I2C-интерфейсом, в 8-выводном корпусе с внешним резонатором на 32 768 Гц, питанием 5 В и возможностью подключения резервной бата­рейки на 3 В (то есть обычной «таблетки»). DS1307 без изменений в схеме и программе можно заменить более современной моделью’DS1338, которая выпускается оптимизированной под различные напряжения питания (напри­мер, DS1338-33 может работать при напряжении от 3 до 5,5 В, а DS1338-3 — только от 2,7 до 3,3 В). Схема переключения питания на батарейку встроена в микросхему и не требует внешних элементов.

В DS1307 имеется выход внешней частоты SQW, который может программи­роваться с различным коэффициентом деления частоты кварца. Мы его за­программируем на выдачу импульсов с периодом 1 с, по внешнему прерыва­нию от этих импульсов в МК будем считать секунды, обновлять значение времени и производить всякие другие полезные действия — точно так же, как мы это делали в часах из главы 14, только там отсчет времени произво­дился внутренним таймером. Но здесь мы можем быть уверены, что при лю­бых сбоях в МК время у нас будет отсчитываться верно.

Схема подсоединения DS1307 к нашему измерителю приведена на рис. 21.9. Обратите внимание, что выводы интерфейса I2C (5 и 6) здесь те же самые, что и для памяти. Выход программируемой частоты SQW у нас подсоединен к выводу внешнего прерывания INTO, который в измерителе не задействован.

Основное неудобство обращения с часами DS1307— то, что у них нет со­стояния «по умолчанию», и внутренние регистры могут при включении пита­ния иметь произвольные значения. В частности, в этих часах в одном из ре­гистров (в том же, что хранит значения секунд) предусмотрен бит СН, который может погружать часы в «спячку» — если он установлен в 1, то не работает генератор и даже невозможно определить правильность подключе­ния. Есть и бит (в регистре управления), который отключает выход частоты на прерывания МК. По этим причинам после первого включения (если бата­рейка подсоединена — то только после первого), часы приходится инициали­зировать. Логика разработчиков проста: зачем кому-то нужны часы, которые не установлены на правильное время? Ну а если их устанавливать, то нетруд­но и установить эти биты.

clip_image002

Рис. 21.9. Присоединение часов DS1307 к измерителю температуры и давления

Так что сначала нам придется написать процедуру инициализации часов. Для этого в регистре управления DS1307 (он имеет номер 7) нужно установить бит 4, который разрешает выход частоты для прерывания, и обнулить млад­шие два бита в этом регистре, что означает частоту на этом выходе 1 Гц (подробности см. в описании DS1307, которое можно скачать с сайта maxim-ic.com). Но это еще не все: ранее мы говорили, что необходимо вообще завести часы, установив бит, который отвечает за работу задающего генератора. Это бит номер 7 в регистре секунд— здесь используется тот факт, что максимальное значение секунд равно 59 (напомним, что оно в BCD-форме, потому это равносильно значению $59), и старший бит всегда будет равен 0. А если мы его установим, то часы стоят, и значение секунд не имеет значения. Потому мы совместим сброс этого бита с установкой секунд в нужное значение (регистр секунд самый первый, и имеет адрес $00):

IniSek: /секунды – в tenp

/если бит 7=1, то остановить, иначе завести часы sbis PinC,pSDA /линия занята rcall err_i2c

Idi ClkA,О /адрес регистра секунд mov DATA, tenp rcall write_i2c brcs stopW

Idi temp,$AA /все отлично rcall out_com ret

IniClk: /установить выход SQW

Idi ClkA,7 /адрес регистра управления

Idi DATA,ObOOOlOOOO /выход SQW с частотой 1 Гц

rcall write_i2c

brcs StopW

Idi temp,$AA /все отлично rcall out_com ret

StopW:

Idi temp,$EE /подтверждение не получено rcall out_com ret

err_i2c:

ldi temp,$AE ;линия занята

rcall out_com sei ret

Напомню, что процедура write_i2c (как и использующаяся далее read_i2c) для доступа к часам уже имеется в файле i2c.prg (см. пршожение 4). Про­цедурой IniSek мы можем, при желании, и остановить, и запустить часы. Ес­ли нужно остановить, то следует в temp записать значение, большее 127. Ес­ли temp меньше 128, то в часы запишется значение секунд и они пойдут. При обнаружении ошибок в компьютер (без запроса с его стороны) выдается оп­ределенный код: $АЕ, если линия занята, и $ЕЕ, если подтверждение со сто­роны часов (АСК) не получено. Если все в порядке, то выдается код $АА. Те же самые вызовы для выдачи кодов мы используем в других процедурах об­ращения к часам.

Раздельные процедуры нам понадобились потому, что иногда часы идут, а выход на прерывание МК у них может оказаться отключенным. Тогда нам надо только его подключить, а время сбивать не следует. Вызывать эти про­цедуры мы будем при включении контроллера: мы помним, что при самом первом запуске часы следует заводить обязательно. Для того чтобы правиль­но организовать процедуру, нам следует сначала выяснить, в каком состоя­нии часы находятся: ReadSet:

sbis PinC,pSDA ;линия занята rcall err_i2c

ldi ClkA,7 ;адрес регистра управления rcall read_i2c

mov temp,data ;в temp – значение регистра управления ldi ClkA, О ‘/адрес регистра секунд rcall read_i2c ;в data – значение регистра секунд brcs stopW ret

Записав все эти процедуры в любом месте программы (но поблизости друг от друга, чтобы обесйечить беспроблемный переход на метку stopw), мы вклю­чаем в процедуру начального запуска такой фрагмент:

;===========инициализация часов ==========================

rcall ReadSet /прочли установочные байты

;в temp регистр установок, в data секунды cpi DATA,$80 ;если больше или равно 128 brsh setsek ;то завести часы cpi temp,$10 ;если выход не установлен brne st_clk ;тогда только его установка rjmp setRAM

setsek: clr temp

rcall IniSek /устанавливаем секунды = О st_clk:

rcall IniClk /установка выхода setRAM:

rcall Rclockini /в любом случае чтение часов в память

Значение $10 регистр установок должен иметь, если мы ранее устанавливали часы. Процедура чтения значений часов Rclockini в память у нас отсутству­ет, и мы поспешим исправить это, включив в текст туда же, где находятся остальные процедуры для часов, еще две: Readcik для чтения BCD-значений из часов и Rclockini для преобразования их в распакованный формат. Пред­варительно зададим место в SRAM, куда мы будем складывать значения всех разрядов времени (включая календарь), и отдельно только часы и минуты, но распакованные (они могут пригодиться для индикации):

/SRAM старший байт адреса SRAM=0x01

.equ Sek = 0x10 /текущие сек BCD-значение

.equ Min = 0x11 /текущие минуты

.equ Hour = 0x12 /текущие часы

.equ Date = 0x13 /текущая дата

.equ Month = 0x14 /текущий месяц

.equ Year = 0x15 /текущий год

/распакованные часы

.equ DdH = 0x16 /часы старший разряд

.equ DeH = 0x17 /часы младший разряд

.equ DdM = 0x18 /минуты старший разряд

.equ DeM = 0x19 /минуты младший разряд

/<начиная с адреса $20 у нас хранятся коэффициенты>

Rclockini: /инициализация часов

rcall ReadClk /сложили часы в память ldi ZH,0x01/

ldi ZL,Sek /адрес секунд в памяти

Id temp,Z /извлекаем из памяти упакованные sek

mov count_sek,temp

andi temp,ObllllOOOO /распаковываем старшую тетраду swap temp /старший разряд в младшей тетраде ldi data,10

mov multlO,data /в multlO всегда будет 10

mul temp,multlO /умножаем на 10, в rl:rO – результат умножения

andi count_sek,ObOOOOllll /младший

add count_sek,rO /получили hex-секунды

ldi ZL,Hour /распакованные в память

Id temp,Z

mov data,temp

andi temp,ObOOOOllll /младший разряд часов ldi ZL,DeH St Z,temp

andi data,ObllllOOOO /старший разряд часов swap data /старший разряд в младшей тетраде ldi ZL,DdH St Z,data

ldi ZL,Min /распакованные минуты загружаем в память Id temp,Z mov data,temp

andi temp,ObOOOOllll /младший разряд минут ldi ZL,DeM St Z,temp

andi data,ObllllOOOO / старший разряд минут swap data /старший разряд в младшей тетраде ldi ZL,DdM St Z,data ret

ReadClk: /чтение часов ldi ZH,1 /старший RAM

Idi ZL,Sek ;адрес секунд в памяти Idi ClkA,О ;адрес секунд в часах sbis PinC,pSDA rcall err_i2c rcall start

Idi DATA,0bll010000 ;12С-адрес часов+запись rcall write

brcs stopR ;C=1 если ошибка

mov DATA,ClkA /адрес регистра секунд

rcall write

brcs stopR ; C«l если ошибка rcall start

Idi DATA,ObllOlOOOl /адрес часов+чтение rcall write

brcs StopR /C=l если ошибка set /СК

rcall read /читаем секунды

brcs StopR /C=l если ошибка

St Z+,DATA /записываем секунды в память

rcall read /читаем минуты

brcs StopR /С=1 если ошибка

St Z+,DATA /записываем минуты

rcall read /читаем часы

brcs StopR /С«1 если ошибка

St Z+,DATA /пишем часы в память

rcall read /день недели читаем, но никуда не пишем

brcs StopR /С=1 если ошибка

rcall read /-читаем дату

brcs StopR / С=1 если ошибка

St Z+,DATA /записываем дату в память

rcall read /читаем месяц

brcs StopR / С=1 если ошибка

St Z+,DATA / записываем месяц в память

clt /НЕ давать АСК – конец чтения

rcall read / читаем год

brcs StopR / С=1 если ошибка

St Z+,DATA ; записываем год в память rcall Stop ret

stopR:

ldi temp,$EE /подтверждение не получено rcall out_com ret

Здесь нам пришлось оформить процедуру чтения из часов отдельно, прямым обращением к процедурам чтения через I2C, так как часы имеют специаль­ный и очень удобный протокол. Если вы им один раз даете команду на чте­ние (значение адреса Obiioioool), то они начинают выдавать последователь­но все значения регистров, начиная с того, к которому было последнее * обращение прошлый раз. Здесь мы начинаем с регистра секунд и заканчива­ем регистром года. Чтобы остановить выдачу, надо в последнем чтении не выдавать подтверждение (АСК).

Прочитанные значения складываются в память (в исходном BCD-виде) и от­дельно, в процедуре Rclockini, распаковываются для индикации. Об индика­ции мы тут подробно говорить не будем, вы уже знаете, как ее организовать (для этого надо добавить еще четыре разряда ЧЧ:ММ в обработчик прерыва­ния по таймеру TIMO), остановимся на применении полученных значений вре­мени для наших целей своевременной записи температуры и давления.

Сначала нам еще надо обеспечить ход времени в МК (в память МК должны все время попадать текущие значения времени) и научиться устанавливать часы: пока мы их только «заводили» и устанавливали секунды. Для счета времени установим отдельный регистр-счетчик секунд (не читать же каждую секунду значения часов) и запомним, что его нельзя трогать: .def count_sek = г2б /счетчик секунд

Теперь начнем с последней задачи: как установить нужное время? Для этого напишем процедуру, которая будет вызываться из компьютера по команде $А1. А по команде $А2 будем читать значение часов:

Gcykle

cpi temp,OxAl /установить RTC + б байт BCD, начиная с секунд breq ргос_А1

cpi temp,0xA2 /читать часы в компьютер breq ргос_А2 rjmp Gcykle

proc_Al: ;А1 установка часов

rcall SetTime rjnp Gcykle

proc_a2: ;a2 читать часы в компьютер из памяти

rcall ReadTime; rjmp Gcykle

/Процедура преобразования BCD в HEX, специально для времени HEX_time: ;на входе в ZL адрес сек, часы или минуты ;на выходе в temp hex-значение. Id temp,Z ;

andi temp,ObllllOOOO /распаковываем старший разряд swap temp /старший в младшей тетраде

mul temp, mult 10 /умножаем на 10 и помещаем в г О результат Id temp,Z /

andi temp,ObOOOOllll /младший add temp,rO /получили hex ret

Sclock: /получить из компьютера 6 байт и записать в память ldi ZH, 0x01 /старший RAM ldi ZL,Sek /Ram rcall in_com St Z+,temp /секунды rcall in_com St Z+,temp /минуты rcall in_com St Z+,temp /часы rcall in_com St Z+,temp /дата rcall in_com St Z+,temp /месяц rcall in_com St Z,temp /год

push cnt /сохраняем cnt на всякий случай

rcall SetClk /переписываем в часы pop cnt ret

Setclk: /установить часы sbis PinC,pSDA /линия занята rcall err_i2c Idi ZH,0x01

Idi ZL,Sek /адрес секунд в памяти Idi ClkA,О /регистр секунд Id DATA,Z+ /извлекаем секунды rcall write_i2c /секунды записываем brcs stops

Idi ClkA,1 /регистр минут Id DATA,Z+ /извлекаем минуты rcall write_i2c /минуты записываем brcs stops

Idi ClkA,2 /регистр часов Id DATA,Z+

rcall write_i2c /записываем часы brcs stops

Idi ClkA,4 /регистр даты (день недели пропускаем) Id DATA,Z+

rcall write_i2c /записываем дату brcs stops

Idi ClkA,5 /регистр месяца Id DATA,Z+

rcall write_i2c /записываем месяц brcs stops

Idi ClkA,6 /регистр года Id DATA,Z

rcall write_i2c /записываем год brcs stops

Idi ClkA,7 /регистр установок – на всякий случай Idi DATA,ObOOOlOOOO rcall write__i2c brcs stops

ldi temp,$AA ;все отлично rcall out_cam ret

stops:

ldi temp,$EE /подтверждение не получено rcall out_com ret

SetTime: /установка текущих значений в МК cli

rcall Sclock /записали из компа BCD-значения ldi ZL,Sek /упакованные секунды rcall HEX_time /имеем hex-секунды в tenp mov count_sek,temp /переписываем в счетчик /далее распаковьшаем для индикации:часы-минуты ldi ZL,Hour /распакованные в память Id temp,Z mov data,temp

andi temp,ObOOOOllll /младший разряд часов ldi ZL,DeH St Z,temp

andi data,ObllllOOOO / старший разряд часов swap data /старший разряд в младшей тетраде ldi ZL,DdH St Z,data

ldi ZL,Min /распакованные в память Id temp,Z mov data,temp

andi temp,ObOOOOllll /младший разряд минут ldi ZL,DeM st Z,temp

andi data,ObllllOOOO /старший разряд минут swap data /старший разряд в младшей тетраде

ldi ZL,DdM

st Z,data sei ret

ReadTime: /чтение часов из памяти в порядке ЧЧ:ММ ДЦ.мм.ГГ cli

rcall ReadClk /сначала читаем из часов ldi ZH,1 /старший RAM ldi ZL,Hour Id temp, Z

rcall out_com /часы

ldi ZL,Min

Id temp,Z /

rcall out_com /минуты

ldi ZL,Sek

Id temp,Z /

rcall out_com /секунды

ldi ZL,Date

Id temp,Z+ /

rcall out_com /дата

Id temp,Z+ /

rcall out_com /месяц

Id temp,Z /

rcall out_com /год sei ret

Как видите, получилось довольно длинно, но ничего не поделаешь. Теперь мы находимся в следуюш1ей ситуации: часы установлены и идут сами по себе, в памяти МК имеются значения времени, которые туда записали при установке, есть еще регистр countsek, в котором отдельно хранятся зна­чения секунд в нормальном (а не BCD) цифровом формате. Осталось заста­вить МК отсчитывать время — сам контроллер никогда не «узнает», кото­рый сейчас час.

Для этого мы и припасли прерывание от часов, которое происходит раз в се­кунду. В принципе мы могли бы при каждом поступлении этого прерывания читать значения времени из часов процедурой Readcik, но это неудобно — процедура длинная и будет тормозить индикацию. Синхронизация хода часов обеспечена тем, что прерывания управляются от RTC, а считать секунды, минуты и часы совсем нетрудно и много времени не займет.

Итак, вычеркнем опять из начального запуска процедуру инициализации Timer 1 (всю секцию Set Timer 1, вернув вместо нее инициализацию только Timer О, см. первоначальный текст в приложении 4\ уберем из текста обра­ботчик прерывания timicompa и вместо ссылки rjmp timicompa в секции прерываний опять поставим команду reti.

Вместо этого в секции прерываний для внешнего прерывания INTO (во вто­рой строке, сразу после rjmp reset) заменим reti на rjmp extinto, а в на­чальную загрузку впишем инициализацию внешнего прерывания INTO:

;====== внешнее прерывание into

Idi temp, (1«ISC01) /прерывание. INTO по спаду out mcucr,temp

Idi temp, (1«INT0) /разрешение. into out gicr,temp

Idi temp,$ff /на всякий случай сбросить все прерывательные флаги out gifr,temp

Теперь, если часы работают, у нас каждую секунду будет происходить пре­рывание INTO. В нем мы сначала займемся счетом времени, а потом записью во внешнюю flash каждые три часа. Для этого нам придется организовать до­вольно громоздкую процедуру сравнения времени с заданным. В нашем из­мерителе мы будем писать с т. н. метеорологическим интервалом — каждые три часа, начиная с О часов.

Но писать в память в определенные моменты времени — это еще не все. Ме­теоданные имеют смысл только, если они привязаны к абсолютному време­ни. Если же мы будем просто писать в память, как сейчас, то при чтении дан­ных мы никогда не узнаем, когда именно была произведена первая запись. Но даже если мы запишем время включения прибора на бумажке (точно зная интервал, остальные кадры нетрудно привязать к абсолютному времени), то учесть отключения питания мы все равно не сможем. Зачем тогда было при­думывать такой хитрый механизм сохранения адреса при сбоях?

Писать в память время каждого измерения нецелесообразно — оно займет минимум 5 байт, в нашем случае больше, чем сами данные. Потому мы по­ступим следующим образом: при начальной загрузке устанавливаем некий флаг (назовем его «флаг первичной записи»), который покажет, что это пер­вая запись после включения питания. Если этот флаг установлен, то мы бу­дем писать время в виде отдельного кадра, а точнее — двух кадров, потому что в один 4-байтовый кадр время + дата у нас не уместится. Можно в прин­ципе и сэкономить, но сделать размер вспомогательного кадра времени крат­ным кадру данных удобно с точки зрения отсчета адресов во flash. Два кадра займут 8 байт, пять из них есть значение времени, а оставшиеся три мы ис­пользуем так: будем придавать самым первым двум определенное значе­ние — $FA. Тогда считывающая программа, встретив два $FA подряд, будет «знать», что перед ней кадры времени, а не данных,» и их нужно интерпрети­ровать соответствующим образом.

Тут мы используем тот факт, что ни данные (10-битовые), ни значения вре­мени не могут содержать байтов, имеющих величину, когда старшая тетрада имеет значение $F. Так что в принципе хватило бы и одного такого байта, но для надежности мы их вставим два подряд (благо количество «лишних» бай­тов позволяет), и у нас даже еще один байт останется в запасе. И его мы так­же используем: будем писать в него значение регистра mcucsr, в котором со­держатся сведения о том, откуда ранее пришла команда на сброс. Отдельные биты в этом байте сбоев (БС) означают следующее:

? бит 3 — Watchdog Reset Flag (БС = 08) устанавливается, если сброс по­ступил от сторожевого таймера;

? бит 2 — Brown-out Reset Flag (БС = 04) устанавливается, если сброс был от снижения питания ниже 4 В;

? бит 1 — External Reset Flag (БС = 02) устанавливается, если сброс про­изошел от внешнего сигнала Reset (характерно для перепрограммирова­ния);

? бит О — Power-on Reset Flag (БС = 01) устанавливается, если было вклю­чение питания МК.

Эта информация пригодится для сбора статистики сбоев. После записи кад­ров времени флаг первичной записи сбрасывается.

Таким образом, после каждого включения МК у нас будет записываться кадр времени, и мы всегда сможем привязать данные к абсолютному времени и дате, даже если в записи был длительный перерыв. Это немного уменьшит полезный объем памяти, но в силу относительной редкости сбоев уменьше­ние это можно не принимать во внимание.

Есть еще один момент, который связан с процедурой чтения данных из flash-памяти — раз мы считаем время в МК отдельно, то при такой длительной процедуре счет неизбежно собьется. Чтобы исправить этот момент, нам тре­буется производить в самом конце процедуры ReadFullFlash инициализацию часов заново:

rcall Rclockini /заново инициализируем часы

Окончательный вариант программы измерителя с часами, суммирующий все, описанное ранее, довольно велик по объему (он содержит порядка 1300 строк, без учета включаемого файла i2c.prg), потому я его в книге не разме­щаю. Его можно воссоздать, если последовательно делать в исходной про­грамме измерителя из приложения 4 все рекомендованные мной изменения. Не доверяющие своей внимательности и просто ленивые могут его скачать с моей домашней странички по адресу revich.Ub.ru/AVR/ctp.zip. В ар­хиве содержатся оба необходимых файла: собственно программа ctp.asm и файл с процедурами I2C, который называется i2c.prg и полностью совпадает с тем текстом, что приведен в приложении 4. Распакуйте их в одну папку и не забудьте еще приложить фирменный файл макроопределений m8535def.inc, после чего программу можно компилировать.

Заметки на полях

в этой программе есть потенциальная ошибка, хотя и не очень серьезная: ес­ли мы обратимся к какой-либо длительной процедуре (в данном случае это чтение содержимого flash), то при совпадении ее по времени с необходимо­стью записи данных последняя осуществлена не будет, и данные пропадут. Чтобы такое исключить полностью, надо отслеживать время, и вблизи значе­ния часа, кратного трем, запрещать такие процедуры. Можно поступить еще проще — устанавливать при чтении флаг первичной записи, и тогда пропу­щенная запись не приведет к сбою при анализе информации. Но здесь я не стал в такие моменты углубляться, так как совпадение все же крайне малове­роятно (чтение занимает максимум полминуты, можно и вручную отследить момент), да и навредить оно вряд ли сможет — обычно после чтения операто­ром счетчик обнуляется и запись начинается заново.

Схема измерителя совпадает со схемой, приведенной на рис. 20.4, если доба­вить к ней преобразователь RS-232 с разъемом, как описано в разделе про UART, изменения, представленные на рис. 21.9 и, конечно, индикацию. Для управления разрядами часов в программе предусмотрены незанятые выводы порта А (с 4 по 7). Их следует присоединить к индикаторам по схеме, анало­гичной остальным — как в часах из главы 20. Если вы не хотите использо­вать индикацию часов, а только писать по часам данные, то лучше вернуть процедуру по прерыванию Timer О в то состояние, которое она имеет в про­грамме измерителя без часов {приложение 4\ тогда на каждый разряд будет приходиться относительно большая часть времени индикации.

Обращу ваше внимание также, что в тексте программы ctp.asm, процедуре записи во flash (строка 496), есть закомментированный оператор rjmp mml. Если его раскомментировать, то запись будет происходить каждую минуту. Это можно использовать для проверки записи во flash, в том числе коррект­ности отслеживания последнего адреса.

Оставить комментарий

микросхемы мощности Устройство импульсов питания пример приемника провода витков генератора выходе напряжение напряжения нагрузки радоэлектроника работы сигнал сигнала сигналов управления сопротивление усилитель усилителя усиления устройства схема теория транзистора транзисторов частоты