Программирование AVR Введение во встраиваемую электронику, часть 5 Fri, March 29 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.

Введение во встраиваемую электронику, часть 5 Печать
Добавил(а) microsin   

Продолжение переводов руководства для начинающих от SparkFun [1]. Эта часть посвящена продолжению изучения компилятора AVR-GCC, использованию оператора printf для передачи данных через UART микроконтроллера AVR.

Предыдующая часть: Введение во встраиваемую электронику, часть 4 (RS-232, MAX232, UART и последовательный обмен данными).

Я знаю очень мало о всех возможностях компилятора AVR-GCC. Однако это не сильно мешает в повседневном использовании компилятора. Помогают готовые примеры опций командной строки и готовые файлы Makefile, которые легко найти с помощью Google и таких сайтов, как AVRfreaks.net или StackOverflow.com.

Ранее мы попробовали скомпилировать и запустить пример мигания светодиодом (см. Введение во встраиваемую электронику, часть 2). Снова откройте этот пример кода в редакторе Programmers Notepad 2 (далее PN2) и проверьте, что он все еще компилируется. Для этого выберите в меню Tools -> Make All. Окно в нижней части экрана должно показать сообщение "Process Exit Code: 0", что означает успешное завершение компиляции. Если это не так, то будут показаны номера строк и сами строки кода, где найдена ошибка, вместе с кратким описанием ошибки (эти сообщения генерирует компилятор AVR-GCC).

[Передача данных в окно терминала с помощью printf]

Давайте теперь рассмотрим и попробуем скомпилировать еще один пример кода basic-out-atmega168.zip (это файлы для ATmega168 или ATmega328. Если Вы используете ATmega8, то скачайте другой архив basic-out-atmega8.zip). Распакуйте закачанный архив в любую папку. У Вас будет 2 файла: Makefile (файл команд и макросов для утилиты make) и файл с расширением *.c (модуль исходного кода, на языке C, программа для микроконтроллера).

Сначала откройте файл с расширением *.c в редакторе PN2. В нем я собрал полезные макросы и функции. В самом начале файла Вы увидите "черную магию":

#define FOSC 16000000
#define BAUD 9600
#define MYUBRR FOSC/16/BAUD-1

Для чего нужны эти строки? Это три определения (их еще называют макроопределениями, макросами, макропеременными времени компиляции), которые вычисляют корректное значение переменной MYUBRR. Эта переменная будет использоваться для настройки порта UART микроконтроллера AVR. Поскольку обмен данными через последовательный канал связи привязан к скорости передачи бит, то в выражение вычисления переменной MYUBRR входит константа BAUD, равная 9600 (9600 бит в секунду). Кроме того, поскольку скорость работы ATmega168 и его периферии (в том числе и UART) строго синхронизирована с тактовой частотой ядра, то в выражение переменной MYUBRR входит также и таковая частота FOSC (16000000 Гц, что соответствует установленному кварцу на 16 МГц). Теперь переменная MYUBRR, которая будет загружена в аппаратуру UART микроконтроллера, вычислена строго с учетом используемой скорости UART и тактовой частоты микроконтроллера. MYUBRR будет вычислена при компиляции программы, и загружена во время выполнения программы в регистр UBRR для настройки UART.

Прим. переводчика: файл Makefile также может определять (и определяет!) макропеременные времени компиляции. Если Вы откроете файл Makefile, то увидите следующее определение переменной:

F_CPU = 16000000

Здесь также задается тактовая частота, и Вы можете использовать эту переменную в коде компилируемой программы. Например, Вы могли бы в коде программы (в файле с расширением *.c) указать только два макроопределения (потому что переменная F_CPU уже определена в Makefile, и передана компилятору AVR-GCC через его командную строку):

#define BAUD 9600
#define MYUBRR F_CPU/16/BAUD-1

В этом примере значение для F_CPU будет предоставлять файл Makefile.

Более точно вычислить значение переменной MYUBRR можно макросом (я это не проверял!):

#define MYUBRR (((((FOSC * 10) / (16L * BAUD)) + 5) / 10) - 1)

Кроме того, есть набор отличных таблиц на разные частоты кварца с указанием погрешности установки скорости: WormFood's AVR Baud Rate Calculator.

В программе Вы увидите еще одну непонятную строку:

static FILE mystdout = FDEV_SETUP_STREAM(uart_putchar, NULL, _FDEV_SETUP_WRITE);

Здесь задается буфер для оператора printf, куда он будет помещать выводимые данные. Не буду объяснять, что тут происходит, потому что сам не очень это понимаю. Когда я начинаю работу над новым проектом, то никогда не запускаю его с пустой страницы, я ВСЕГДА беру за основу готовый проверенный, и гарантированно рабочий проект, и постепенно вношу туда нужные изменения, проверяя себя на каждом новом шаге. Пожалуйста, возьмите для себя в качестве такого стартового примера этот проект; наша конечная цель - вывести строку текста в окно программы терминала на компьютере.

Посмотрите код функции ioinit(), она настраивает необходимую аппаратуру микроконтроллера AVR. В ней Вы увидите несколько новых операторов.

UCSR0B = (1 << RXEN0)|(1 << TXEN0);

Это выражение может поначалу несколько напугать, однако тут иллюстрируется весьма практичный способ установки нужных бит в нужном регистре. Переменная RXEN0 определена в каком-то заголовочном файле как '4'.

Прим. переводчика: на самом деле файл, где определены номера бит RXEN0, TXEN0 и другие (а также имена регистров), следующий: C:\WinAVR-20100110\avr\include\avr\iom168p.h (в случае микроконтроллера ATmega168). Также это может быть файл iom328p.h для ATmega328, или любой другой заголовочный файл - в зависимости от используемого в проекте микроконтроллера. Тип микроконтроллера в проекте задается переменной MCU, которая задана в Makefile:

# MCU name
MCU = atmega168

1 << RXEN0 вычисляется как сдвиг 1 влево 4 раза, в результате получится константа 0b00010000. Полезность такого выражения в том, что для составления маски бит не нужно помнить номера бит, достаточно знать его легко запоминающееся имя (RXEN0). Точно так же вычисляется маска для бита TXEN0 (номер бита 3), в результате получится маска 0b00001000. Логический оператор OR (вертикальная палочка |) объединяет эти маски воедино, и получается значение 0b00011000, которое и будет записано в регистр UCSR0B. Это действие (установка бит RXEN0 и TXEN0 в регистре UCSR0B) разрешает работу аппаратуры UART ATmega168 на прием и передачу.

И наконец, мы увидим следующую замечательную строчку кода:

printf("Test it! x = %d", x);

Тут используется знаменитый printf [3] - универсальная функция, которая может преобразовать некоторое количество строк и переменных в последовательность символов, и затем передать их во внешний мир. В этом примере часть строки "Test it! x =" передается в последовательный порт UART без каких-то изменений, как есть, и будет отображена в окне программы терминала. После этого спецификатор формата %d даст команду преобразовать значение переменной x (второй параметр функции printf в нашем примере) в текстовый вид как десятичное число, которое также будет напечатано на экране терминала. Понимаете, что это значит? Вы можете использовать оператор printf для отладки - распечатать значение нужной переменной в программе, и таким образом понять, что происходит в программе, когда выполняются операторы кода на языке C. Это очень полезная возможность, которую я постоянно использую. Точно такая же техника отладки используется в среде разработки Adrduino IDE. См. также [2, 3].

Теперь давайте скомпилируем модуль программы basic-out-atmega168.c, и запишем полученный HEX-файл в память микроконтроллера ATmega168 (как это делается, подробно объясняется в статье Введение во встраиваемую электронику, часть 2). Скомпилируйте программу, для чего в редакторе PN2 выберите Tools -> Make All, подключите программатор к макету и подайте на макет питание. Затем в редакторе PN2 запустите команду программирования микроконтроллера, выбрав в меню Tools -> Program. Код должен успешно загрузиться в память микроконтроллера. Затем подсоедините кабелем RS-232 макет к компьютеру и откройте окно терминала, настроенное на соединение с параметрами 9600 bps, 8-N-1, как мы это было описано в предыдущей статье Введение во встраиваемую электронику, часть 4.

AVR-printf-output

Отлично! Мы добились передачи и отображение текстовых данных из программы ATmega168. Давайте также рассмотрим и другие полезные операторы в коде.

sbi(PORTC, STATUS_LED);

Это еще один удобный макрос, который можно использовать для манипуляции битом регистра. Чтобы переключить ножку порта GPIO микроконтроллера (GPIO расшифровывается как general purpose input/output pin, вывод общего назначения, работающий на вход или выход), Вам нужно прочитать состояние порта, наложить маску изменения состояния на считанное из регистра значение, и затем записать полученное значение обратно в регистр. Вместо того, чтобы производить все эти скучные действия, чтобы всего лишь переключить ножку порта, используйте макросы sbi и cbi. Вот так, к примеру, определен полезный макрос sbi:

#define sbi(var, mask) ((var) |= (uint8_t)(1 << mask))

Названия макросов sbi и cbi взяты из соответствующих мнемоник инструкций ассемблера SBI и CBI. SBI устанавливает бит (легко запомнить, тут применено сокращение от Set Bit Index, установить бит по индексу). CBI очищает бит (мнемоника CBI расшифровывается как Clear Bit Index, очистить бит по индексу). Для работы с макросами sbi и cbi Вы должны выбрать порт, в котором находится изменяемый бит, и номер изменяемого бита. Добавьте в код еще одно определение #define, указав тем самым номер бита порта C:

#define STATUS_LED 0

Теперь вы можете управлять состоянием светодиода LED D3 (горит он или погашен) простыми командами:

sbi(PORTC, STATUS_LED); //этот оператор включит светодиод
cbi(PORTC, STATUS_LED); //этот оператор погасит светодиод

Конечно же, светодиод должен быть подключен к ножке 23 микроконтроллера ATmega168, как было показано в предыдущих статьях. Светодиод, как и оператор printf, также удобно использовать для отладки. Когда Вы сомневаетесь в работе кода, или не можете найти место, где зависла программа, просто добавьте в программу оператор, управляющий состоянием светодиода.

Стоит еще уделить внимание подпрограмме генерации задержки delay_ms(). Раньше для формирования задержек я вручную пересчитывал количество итераций циклов при изменении тактовой частоты микроконтроллера (помните, как мы увеличили тактовую частоту микроконтроллера с 1 до 8, а потом до 16 МГц?). Это было очень неудобно, и приходилось делать каждый раз, когда надо было задержать работу программы на заданное время. Но если применить функцию delay_ms, то пересчитывать количество итераций для получения нужного времени задержки уже не нужно, это делается автоматически в теле подпрограммы delay_ms. Например, delay_ms(1000) сформирует задержку примерно на 1 секунду.

Прим. переводчика: я уже писал про F_CPU, что это определение тактовой частоты микроконтроллера в файле Makefile. Корректная работа подпрограммы delay_ms (правильность формирования длительности задержки) зависит от того, насколько реальная тактовая частота, на которой работает микроконтроллер, совпадает со значением макропеременной F_CPU. Например, если Ваш микроконтроллер работает на тактовой частоте 16 МГц, то в файле Makefile должно быть задано следующее определение:

F_CPU = 16000000

Код, который прокручивается в бесконечно цикле main примера basic-out-atmega168.c, делает задержки на 500 мс, благодаря чему период мигания светодиода получатся равным 1 секунде:

int main (void)
{
   uint8_t x = 0;
   
   ioinit(); //настройка аппаратуры: ножек IO и т. п.
   
   while(1)
   {
      x++;
      
      printf("Test it! x = %d", x);
      
      sbi(PORTC, STATUS_LED);    //зажечь LED
      delay_ms(500);             //задержка 0.5 сек
      
      cbi(PORTC, STATUS_LED);    //погасить LED
      delay_ms(500);             //задержка 0.5 сек
   }
   
   return(0);
}

[Прием и анализ принимаемых через UART данных]

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

int main (void)
{
   uint8_t key_press = 0;
   
   ioinit(); //настройка аппаратуры: ножек IO и т. п.
   
   while(1)
   {
      key_press = uart_getchar();
      
      printf("I heard : %c\n", key_press);
      
      if(key_press == 'g') printf(" GO!\n");
      if(key_press == 'X') printf(" EXIT\n");
      if(key_press == 13) printf(" RETURN\n");
   }
   
   return(0);
}

Теперь снова откройте окно терминала и попробуйте понажимать на клавиатуре компьютера разные кнопки. В результате увидите примерно следующее:

AVR-uart getchar

Здесь продемонстрировано, как микроконтроллер может принимать команды консоли и отвечать на них. Каждый принятый через UART символ (символы передаются программой терминала при нажатии на соответствующие клавиши) будет проанализирован, и в ответ выполнятся определенные действия. Так, к примеру, на все принятые символы программа ответит 'I heard : ' и выведет принятый символ. Символы X, g, возврат каретки 13 воспринимаются как команды.

За получение принятых символов от UART отвечает функция uart_getchar. Каждый принятый символ ATmega168 выводит с помощью спецификатора формата %c в операторе printf, каждый раз переводя позицию вывода на новую строку (для этого передается символ перевода строки \n).

Теперь, как я надеюсь, Вы оценили возможность ввода / обработки / вывода, которые может производить микроконтроллер AVR в ответ на действия пользователей в окне терминала. Можно даже написать текстовую приключенческую игру (text-based adventure game).

Следующая часть: Введение во встраиваемую электронику, часть 6 (как собирать радиолюбительские конструкции на печатных платах).

[Ссылки]

1. Beginning Embedded Electronics site:sparkfun.com.
2. AVR: отладочный вывод через UART (RS232 debug).
3. Секреты printf.

 

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


Защитный код
Обновить

Top of Page