[Из песочницы] Автоматический переключатель антенн с управлением на МК

image


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

image


Для оперативного переключения антенн к МК подключена одна кнопка. Её я же приспособил для запоминания предпочтительной антенны для каждого диапазона: при нажатии кнопки более 3 секунд выбранная антенна запоминается и выбирается правильно автоматически после очередного включения питания устройства. Информация о текущем диапазоне, выбранной антенне и состоянии её настройки выводится на однострочный LCD дисплей.

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

image


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

Кроме того, тюнер MFJ умеет передавать на пульт дистанционного управления сигналы о том, что он ведет настройку и о том, что антенна настроена. Для этого на фирменном пульте MFJ предусмотрено два контрольных светодиода. Я вместо светодиодов подключил оптроны и подал с них сигнал на МК, так чтоб всю информацию видеть на одном дисплее. Выглядит готовый девайс так.

image


Коротко о самоделке вроде всё. Теперь о программной части. Код написан в Atmel Studio (Свободно скачивается с сайта Atmel). В проекте для начинающих демонстрируются следующие возможности использования популярного МК Atmega8:

  1. Подключение кнопки
  2. Подключение линии вход для цифрового сигнала от трансивера и тюнера
  3. Подключение выхода управления реле переключения антенн
  4. Подключение однострочного LCD дисплея
  5. Подключение зуммера и вывод звука
  6. Подключение линии аналогового входа ADC и измерение напряжения
  7. Использование прерываний
  8. Использование таймера для отсчёта времени нажатия кнопки
  9. Использование сторожевого таймера
  10. Использование энергонезависимой памяти для хранения выбранных антенн
  11. Использование UART для отладочной печати
  12. Экономия энергии в простое МК


Итак, начнём. По ходу в тексте будут встречаться всякие названия регистров и константы, свойственные для применяемого МК. Это не ардуино, здесь к сожалению, придётся почитать даташит на МК. Иначе вам не понять, что значат все эти регистры и как можно поменять их значения. Но структура программы в целом останется той же.

Первым делом подключим к МК кнопку


Это самое простое. Один контакт подключаем к ноге МК, второй контакт кнопки — на землю. Чтобы кнопка работала, понадобится включить подтягивающий резистор в МК. Он соединит кнопку через сопротивление с шиной +5В. Сделать это совсем просто:

PORTB |= (1 << PB2); // pullup resistor для кнопки


Аналогично к шине +5В подтягиваются все цифровые входы, которые управляются замыканием на землю (оптроны, сигнальные линии от трансивера, сигнал PTT). Иногда лучше физически припаять такой резистор меньшего наминала (например 10к) между входом МК и шиной +5В, но обсуждение этого вопроса за рамками статьи. Поскольку все входные сигналы в проекте редко изменяют значения, то они для защиты от помех зашунтированы на землю конденсаторами в 10 нанофарад.

Теперь у нас на входе PB2 постоянно присутствует логическая 1, а при нажатии на кнопку будет логический 0. При нажатии\отжатии нужно отслеживать дребезг контактов кнопки, проверяя, что уровень сигнала не изменился за время, скажем 50 миллисекунд. Делается это в программе так:

 if(!(PINB&(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 


Подключаем к МК реле переключения антенн


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

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 
#include 
#include 

//настройка UART для отладочной печати в порт RS232
void uart_init( void )
{
/*      //настройка скорости обмена
        UBRRH = 0;
        UBRRL = 103; //9600 при кварце 16 МГц */
    #include 
    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 <


После настройки потока вывода, можно пользоваться обычным 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<= 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

© Geektimes