В радиолюбительской практике иногда возникает потребность сделать что-нибудь на микроконтроллере. Если не занимаешься такого рода поделками постоянно, то приходится долго гуглить нужное схемное решение и подходящие библиотеки для МК, позволяющие быстро решить задачу. Недавно захотелось мне сделать автоматический антенный переключатель. В процессе работы пришлось использовать многие возможности МК 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 = %urn", 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.2frn", 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 = %urn", mcusr_mirror );
Экономия энергии для любителей экологии
Пока МК ничем не занят, он может заснуть и ждать наступления очередного прерывания. В этом случае экономится немного электрической энергии. Пустяк, но почему бы его не использовать в проекте. Тем более, что это очень просто. Подключите заголовочный файл avr/sleep.h. Тело программы состоит из одного бесконечного цикла, в котором нужно вызывать функцию sleep_cpu(), после чего МК немного засыпает и основной цикл останавливается до возникновения следующего прерывания. Они возникают при работе таймера и ADC, так что долго спать МК не будет. Режим спячки определяется при инициализации МК вызовом двух функций:
set_sleep_mode(SLEEP_MODE_IDLE); // разрешаем сон в режиме IDLE
sleep_enable();
На этом пока всё. Переключатель я сделал, он успешно трудится на моей любительской радиостанции без сбоев. Надеюсь, предоставленный материал будет полезен начинающим.
73 de R2AJP
Автор: lesha108