
В радиолюбительской практике иногда возникает потребность сделать что-нибудь на микроконтроллере. Если не занимаешься такого рода поделками постоянно, то приходится долго гуглить нужное схемное решение и подходящие библиотеки для МК, позволяющие быстро решить задачу. Недавно захотелось мне сделать автоматический антенный переключатель. В процессе работы пришлось использовать многие возможности МК Atmega в одном компактном проекте. Тем, кто начинает изучать AVR, переходит с ардуино или эпизодически программирует МК могут быть полезны куски кода, использованные мной в проекте.
Антенный переключатель задумывался мной как устройство, автоматически подключающее к трансиверу антенну, которая наилучшим образом подходит для рабочего диапазона коротких волн. У меня есть две антенны: Inverted V и Ground Plane, подключены они к антенному тюнеру MFJ, в котором их можно дистанционно переключать. Есть фирменный ручной переключатель MFJ, который хотелось заменить.

Для оперативного переключения антенн к МК подключена одна кнопка. Её я же приспособил для запоминания предпочтительной антенны для каждого диапазона: при нажатии кнопки более 3 секунд выбранная антенна запоминается и выбирается правильно автоматически после очередного включения питания устройства. Информация о текущем диапазоне, выбранной антенне и состоянии её настройки выводится на однострочный LCD дисплей.
О том, на каком сейчас диапазоне работает трансивер, можно узнать разными способами: можно измерять частоту сигнала, можно получать данные по интерфейсу CAT, но самое простое для меня – использовать интерфейс трансивера YAESU для подключения внешнего усилителя. В нём есть 4 сигнальных линии, в двоичном коде указывающие на текущий диапазон. Они выдают логический сигнал от 0 до 5 вольт и их можно через пару согласующих резисторов соединить с ногами МК.

Это еще не всё. В режиме передачи через тот же интерфейс передаются сигналы PTT и ALC. Это логический сигнал о включении передатчика (подтягивается к земле) и аналоговый сигнал от 0 до -4В о работе системы автоматического управления мощностью передатчика. Его я тоже решил измерять и выводить на LCD в режиме передачи.
Кроме того, тюнер MFJ умеет передавать на пульт дистанционного управления сигналы о том, что он ведет настройку и о том, что антенна настроена. Для этого на фирменном пульте MFJ предусмотрено два контрольных светодиода. Я вместо светодиодов подключил оптроны и подал с них сигнал на МК, так чтоб всю информацию видеть на одном дисплее. Выглядит готовый девайс так.

Коротко о самоделке вроде всё. Теперь о программной части. Код написан в Atmel Studio (Свободно скачивается с сайта Atmel). В проекте для начинающих демонстрируются следующие возможности использования популярного МК Atmega8:
- Подключение кнопки
- Подключение линии вход для цифрового сигнала от трансивера и тюнера
- Подключение выхода управления реле переключения антенн
- Подключение однострочного LCD дисплея
- Подключение зуммера и вывод звука
- Подключение линии аналогового входа ADC и измерение напряжения
- Использование прерываний
- Использование таймера для отсчёта времени нажатия кнопки
- Использование сторожевого таймера
- Использование энергонезависимой памяти для хранения выбранных антенн
- Использование UART для отладочной печати
- Экономия энергии в простое МК
Итак, начнём. По ходу в тексте будут встречаться всякие названия регистров и константы, свойственные для применяемого МК. Это не ардуино, здесь к сожалению, придётся почитать даташит на МК. Иначе вам не понять, что значат все эти регистры и как можно поменять их значения. Но структура программы в целом останется той же.
Первым делом подключим к МК кнопку
Это самое простое. Один контакт подключаем к ноге МК, второй контакт кнопки – на землю. Чтобы кнопка работала, понадобится включить подтягивающий резистор в МК. Он соединит кнопку через сопротивление с шиной +5В. Сделать это совсем просто:
PORTB |= (1 << PB2); // pullup resistor для кнопки
Аналогично к шине +5В подтягиваются все цифровые входы, которые управляются замыканием на землю (оптроны, сигнальные линии от трансивера, сигнал PTT). Иногда лучше физически припаять такой резистор меньшего наминала (например 10к) между входом МК и шиной +5В, но обсуждение этого вопроса за рамками статьи. Поскольку все входные сигналы в проекте редко изменяют значения, то они для защиты от помех зашунтированы на землю конденсаторами в 10 нанофарад.
Теперь у нас на входе PB2 постоянно присутствует логическая 1, а при нажатии на кнопку будет логический 0. При нажатии\отжатии нужно отслеживать дребезг контактов кнопки, проверяя, что уровень сигнала не изменился за время, скажем 50 миллисекунд. Делается это в программе так:
if(!(PINB&(1<<PINB2)) && !timer_on) { // только нажали кнопку _delay_ms(50); if( !(PINB&(1<<PINB2)) ) { // проверили на дребезг и она всё нажата - запускаем таймер passed_secs = 0; timer_on = 1; } }
Теперь подключаем пищалку
Она будет давать звуковой сигнал подтверждения, что антенна записана в память МК. Пищалка это просто пьезоэлемент. Он подключается через небольшое сопротивление к ноге МК, а вторым контактом к +5В. Для работы этого зуммера нужно сначала настроить ногу МК на вывод данных.
void init_buzzer(void) { PORTB &= ~(1 << PB0); // buzzer DDRB |= (1 << PB0); // output PORTB &= ~(1 << PB0); }
Теперь ею можно пользоваться. Для этого написана небольшая функция, использующая временные задержки для переключения ноги МК из 0 в 1 и обратно. Переключение с необходимыми задержками позволяет формировать на выходе МК сигнал звуковой частоты 4 кГц длительностью около четверти секунды, который и озвучивает пьезоэлемент.
void buzz(void) { // должен пикать около 4кГц 0,25 сек for(int i=0; i<1000; i++) { wdt_reset(); // сбрасываем сторожевой таймер PORTB |= (1 << PB0); _delay_us(125); PORTB &= ~(1 << PB0); _delay_us(125); } }
Для работы функций задержек не забудьте подключить заголовочный файл и настроить константу скорости работы процессора. Она равна частоте подключенного к МК кварцевого резонатора. В моём случае был кварц на 16МГц.
#ifndef F_CPU # define F_CPU 16000000UL #endif #include <util/delay.h>
Подключаем к МК реле переключения антенн
Здесь нужно просто настроить ногу МК для работы на выход. К этой ноге через усиливающий транзистор по стандартной схеме подключено герконовое реле.
void init_tuner_relay(void) { PORTB &= ~(1 << PB1); // relay DDRB |= (1 << PB1); // output PORTB &= ~(1 << PB1); }
Подключение дисплея
Я использовал однострочный 16 символьный LCD дисплей 1601, добытый из старой аппаратуры. Он использует широкоизвестный контроллер HD44780, для управления которым в сети доступна масса библиотек. Какой-то добрый человек написал легкую библиотеку управления дисплеем, которую я и использовал в проекте. Настройка библиотеки сводится к указанию в заголовочном файле HD44780_Config.h номеров ног МК, подключенных нужным выводам дисплея. Я применил подключение дисплея по 4 линиям данных.
#define Data_Length 0 #define NumberOfLines 1 #define Font 1 #define PORT_Strob_Signal_E PORTC #define PIN_Strob_Signal_E 5 #define PORT_Strob_Signal_RS PORTC #define PIN_Strob_Signal_RS 4 #define PORT_bus_4 PORTC #define PIN_bus_4 0 #define PORT_bus_5 PORTC #define PIN_bus_5 1 #define PORT_bus_6 PORTC #define PIN_bus_6 2 #define PORT_bus_7 PORTC #define PIN_bus_7 3
Особенностью моего экземпляра дисплея стало то, что одна строка на экране выводилась как две строки по 8 символов, поэтому в программе был сделан промежуточный экранный буфер для более удобной работы с экраном.
void init_display(void) { PORTC &= ~(1 << PC0); // display DDRC |= (1 << PC0); // output PORTC &= ~(1 << PC0); PORTC &= ~(1 << PC1); // display DDRC |= (1 << PC1); // output PORTC &= ~(1 << PC1); PORTC &= ~(1 << PC2); // display DDRC |= (1 << PC2); // output PORTC &= ~(1 << PC2); PORTC &= ~(1 << PC3); // display DDRC |= (1 << PC3); // output PORTC &= ~(1 << PC3); PORTC &= ~(1 << PC4); // display DDRC |= (1 << PC4); // output PORTC &= ~(1 << PC4); PORTC &= ~(1 << PC5); // display DDRC |= (1 << PC5); // output PORTC &= ~(1 << PC5); LCD_Init(); LCD_DisplEnable_CursOnOffBlink(1,0,0); } /* Дисплей из 16 символов 0-3 символы диапазон 40M и пробел в конце 4-8 символы антенна A:GP или A:IV и пробел в конце 9-15 символы статус настройки тюнера: TUNING=, TUNED==, HI-SWR= */ uchar display_buffer[]={' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' '}; // 16 пробелов для начала void update_display() { LCD_Init(); LCD_DisplEnable_CursOnOffBlink(1,0,0); // преобразование строки 16 символов в две стороки по 8 символов и вывод их в одну строку на LCD for (uchar i=0; i<8; i++){ LCD_Show(display_buffer[i],1,i); LCD_Show(display_buffer[i+8],2,i); } }
Функция update_display() позволяет выводить содержимое буфера на экран. Значения байтов в буфере это коды ASCII выводимых символов.
Вывод отладочной печати в COM порт
В МК есть UART и я его использовал для отладки программы. При подключении МК компьютеру надо только помнить, что уровни сигнала на выходе МК в стандарте TTL, а не RS232, так что понадобится простейший переходник. Я использовал адаптер USB-Serial, аналогичных полно на aliexpress. Для чтения данных подойдет любая терминальная программа, например от ардуино. Код настройки порта UART:
#define BAUD 9600 #include <stdio.h> #include <stdlib.h> #include <avr/io.h> //настройка UART для отладочной печати в порт RS232 void uart_init( void ) { /* //настройка скорости обмена UBRRH = 0; UBRRL = 103; //9600 при кварце 16 МГц */ #include <util/setbaud.h> UBRRH = UBRRH_VALUE; UBRRL = UBRRL_VALUE; #if USE_2X UCSRA |= (1 << U2X); #else UCSRA &= ~(1 << U2X); #endif //8 бит данных, 1 стоп бит, без контроля четности UCSRC = ( 1 << URSEL ) | ( 1 << UCSZ1 ) | ( 1 << UCSZ0 ); //разрешить передачу данных без приёма // UCSRB = ( 1 << TXEN ) | ( 1 <<RXEN ); UCSRB = ( 1 << TXEN ); } int uart_putc( char c, FILE *file ) { //ждем окончания передачи предыдущего байта while( ( UCSRA & ( 1 << UDRE ) ) == 0 ); UDR = c; wdt_reset(); return 0; } FILE uart_stream = FDEV_SETUP_STREAM( uart_putc, NULL, _FDEV_SETUP_WRITE ); stdout = &uart_stream;
После настройки потока вывода, можно пользоваться обычным printf для печати в порт:
printf( "Start flag after reset = %u\r\n", mcusr_mirror );
Программа использует печать вещественных чисел. Обычные библиотеки не поддерживают такой режим вывода, поэтому пришлось подключить полноценную библиотеку при линковке проекта. Она, правда, увеличивает серьёзно объем кода, но у меня был большой запас памяти, так что это было некритично. В опциях линкера нужно указать строку:
-Wl,-u,vfprintf -lprintf_fltРабота с таймером и прерываниями
Для отсчёта интервалов времени в программе важно иметь счётчик времени. Он нужен для отслеживания, что кнопка нажата более 3 секунд и, следовательно, нужно запомнить в энергонезависимой памяти новые настройки. Чтоб измерить время в стиле AVR нужно настроить счётчик импульсов тактового генератора и прерывание, которое будет выполняться при достижении счётчиком заданного значения. Я настроил таймер так, чтоб он примерно раз в секунду выдавал прерывание. В самом обработчике прерывания подсчитывается количество прошедших секунд. Управляет включением\отключением таймера переменная timer_on. Важно не забывать объявлять все переменные, которые обновляются в обработчике прерывания, как volatile, иначе компилятор может их «оптимизировать» и программа работать не будет.
// настройка счетчика 1 для счета секунд - главный таймер в программе void timer1_init( void ) { TCCR1A = 0; // регистр настройки таймера 1 - ничего интересного /* 16000000 / 1024 = 15625 Гц, режим СТС со сбросом 15625 должен давать прерывания раз в 1 сек */ // режим CTC, ICP1 interrupt sense (falling)(not used) + prescale /1024 + без подавления шума (not used) TCCR1B = (0 << WGM13) | (1 << WGM12) | (0 << ICES1) | ((1 << CS12) | (0 << CS11) | (1 << CS10)) | (0 << ICNC1); OCR1A = 15625; // прерывание TIMSK |= (1 << OCIE1A); } uchar timer_on = 0; volatile uchar passed_secs = 0; // прерывание для подсчета секунд в таймерe ISR(TIMER1_COMPA_vect) { if (timer_on) passed_secs++; }
Значение переменной passed_secs проверяется в главном цикле программы. При нажатии кнопки таймер запускается и далее в главном цикле программы проверяется значение таймера при нажатой кнопке. Если это значение превысит 3 секунды, то производится запись в EEPROM, а таймер останавливается.
Последнее, но самое главное – после всех инициализаций нужно разрешить выполнение прерываний командой sei().
Измерение уровня ALC
Производится с помощью встроенного аналого-цифрового преобразователя (ADC). Я измерял напряжение на входе ADC7. Надо помнить, что можно измерить значение от 0 до 2.5В. а у меня входное напряжение было от -4В до 0В. Поэтому я подключил МК через простейший делитель напряжения на резисторах, так чтобы уровень напряжения на входе МК был на заданном уровне. Далее, мне не нужна была высокая точность, поэтому я применил 8 битное преобразование (достаточно читать данные только из регистра ADCH). В качестве опорного источника использовал внутренний ИОН на 2.56В, это чуть упрощает расчёты. Для работы ADC не забудьте подключить на землю конденсатор 0.1 мкФ к ноге REF.
ADC в моем случае работает непрерывно, сообщая об окончании преобразования вызовом прерывания ADC_vect. Хорошим тоном является усреднять значения нескольких циклов преобразования для уменьшения погрешности. В моём случае я вывожу среднее из 2500 преобразований. Весь код работы с ADC выглядит так:
// количество семплов для усреднения значения датчика напряжения ALC #define SAMPLES 2500 // используемое опорное напряжение #define REFERENCEV 2.56 // экспериментальный коэффициент пересчета для делителя напряжения #define DIVIDER 2.0 double realV = 0; // здесь итоговое зхначение измерения ALC double current_realV = 0; volatile int sampleCount = 0; volatile unsigned long tempVoltage = 0; // переменные для накопления суммы volatile unsigned long sumVoltage = 0; // переменные для передачи суммы семплов в основной цикл void ADC_init() // ADC7 { // внутренний ИОН 2,56В, 8 bit преобразование - результат в ADCH ADMUX = (1 << REFS0) | (1 << REFS1) | (1 << ADLAR) | (0 << MUX3) | (1 << MUX2) | (1 << MUX1) | (1 << MUX0); // ADC7 // включить, free running, с прерываниями ADCSRA = (1 << ADEN) | (1 << ADFR) | (1 << ADIE) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0); // делитель 128 ADCSRA |= (1 << ADSC); // Start ADC Conversion } ISR(ADC_vect) // должен накапливать измерения по 2500 семплам { if (sampleCount++) // пропускаем первое измерение tempVoltage += ADCH; if (sampleCount >= SAMPLES) { sampleCount = 0; sumVoltage = tempVoltage; tempVoltage = 0; } ADCSRA |=(1 << ADIF); // Acknowledge the ADC Interrupt Flag } realV = -1.0*(DIVIDER * ((sumVoltage * REFERENCEV) / 256) / SAMPLES - 5.0); // рассчитываем напряжение ALC if (realV < 0.0) realV = 0.0; printf("ALC= -%4.2f\r\n", realV); // вывод напряжения в последовательный порт
Использование EEPROM
Это энергонезависимая память в МК. Её удобно использовать для хранения всяких настроек, корректировочных значений и т.п. В нашем случае она используется только для хранения выбранной антенны для нужного диапазона. С этой целью в EEPROM выделен 16 байтный массив. Но обращаться к нему можно через специальные функции, определенные в заголовочном файле avr/eeprom.h. При запуске МК считывает информацию о сохранённых настройках в оперативную память и включает нужную антенну в зависимости от текущего диапазона. При длительном нажатии на кнопку в память записывается новое значение, сопровождаемое звуковым сигналом. Во время записи в EEPROM на всякий случай запрещаются прерывания. Код инициализации памяти:
EEMEM unsigned char ee_bands[16]; // переменные для хранения по каждому диапазону дефолтной антенны unsigned char avr_bands[16]; void EEPROM_init(void) { for(int i=0; i<16; i++) { avr_bands[i] = eeprom_read_byte(&ee_bands[i]); if (avr_bands[i] > 1) avr_bands[i] = ANT_IV; // если в память EEPROM еще не писали, то там может быть FF } }
Фрагмент кода обработки нажатия кнопки 3 сек и записи в память:
if (!(PINB&(1<<PINB2)) && passed_secs >= 3) { // кнопка нажата более 3 сек timer_on = 0; // остановли таймер read_ant = avr_bands[read_band]; // запоминаем текущую выбранную антенну cli(); EEPROM_init(); // восстанавливаем значение из памяти чтоб не затереть другие диапазоны sei(); if (read_ant) { avr_bands[read_band] = ANT_GP; } else { avr_bands[read_band] = ANT_IV; } cli(); eeprom_write_byte(&ee_bands[read_band], avr_bands[read_band]); // сохранили значение в EEPROM sei(); buzz(); }
Использование сторожевого таймера
Не секрет, что в условиях сильных электромагнитных помех МК может зависнуть. При работе радиостанции бывают такие помехи, что «утюги начинают разговаривать», так что нужно обеспечить аккуратную перезагрузку МК в случае зависания. Этой цели служит сторожевой таймер. Использовать его очень просто. Подключите сначала в проект заголовочный файл avr/wdt.h. В начале работы программы после выполнения всех настроек нужно запустить таймер вызовом функции wdt_enable(WDTO_2S), а потом не забывать периодически сбрасывать вызовом wdt_reset(), иначе он сам перезапустит МК. Для отладки чтоб узнать по какой причине был перезапущен МК, можно использовать значение специального регистра MCUSR, значение которого можно запомнить и затем выдать в отладочную печать.
// переменные для сохранения состояния контроллера после запуска // используются только для отладки uint8_t mcusr_mirror __attribute__ ((section (".noinit"))); void get_mcusr(void) \ __attribute__((naked)) \ __attribute__((section(".init3"))); void get_mcusr(void) { mcusr_mirror = MCUSR; MCUSR = 0; wdt_disable(); } printf( "Start flag after reset = %u\r\n", mcusr_mirror );
Экономия энергии для любителей экологии
Пока МК ничем не занят, он может заснуть и ждать наступления очередного прерывания. В этом случае экономится немного электрической энергии. Пустяк, но почему бы его не использовать в проекте. Тем более, что это очень просто. Подключите заголовочный файл avr/sleep.h. Тело программы состоит из одного бесконечного цикла, в котором нужно вызывать функцию sleep_cpu(), после чего МК немного засыпает и основной цикл останавливается до возникновения следующего прерывания. Они возникают при работе таймера и ADC, так что долго спать МК не будет. Режим спячки определяется при инициализации МК вызовом двух функций:
set_sleep_mode(SLEEP_MODE_IDLE); // разрешаем сон в режиме IDLE sleep_enable();
На этом пока всё. Переключатель я сделал, он успешно трудится на моей любительской радиостанции без сбоев. Надеюсь, предоставленный материал будет полезен начинающим.
73 de R2AJP
