CUSTOMELECTRONICS.RU
Информационно-учебный блог о разработке электроники
Эл. почта: info@customelectronics.ru

AVR. Работа с UART

UART — самый популярный интерфейс микроконтроллера. Так сложилось потому, что он может использован для связи с персональным компьютером. В этой статье мы постараемся познакомить вас основными сценариями использования UART.

Подготовка к работе

Arduino-совместимые платы имеют на борту переходник USB-to-COM, и наша EduBoard не исключение. Как вы уже знаете, при подключении, в системе она определяется как COM-порт. То есть для исследования работы нам не понадобится никакого дополнительного оборудования. Только любая Arduino-совместимая плата с микроконтроллером Atmega8/168/328. Мы проверили это только с микроконтроллером Atmega8.
Помимо аппаратного обеспечения и среды для программирования, которую мы разворачивали в другой части цикла. Дополнительно, вам понадобится терминальная программа, которая на стороне ПК будет подключаться к COM-порту, отправлять и принимать байты.
Можно скачать готовые самостоятельные терминальные программы, а можно добавить такую возможность в Atmel Studio. Второй способ нам кажется более логичным, поэтому мы будем использовать именно его в статье.
В запущенной среде выберите пункт меню Tools->Extension Manager… и найдите дополнение Terminal for Atmel Studio (это очень популярное дополнение и, скорее всего, оно будет вам предложено сразу).

Extension Manager

Extension Manager

После его установки, в пункте меню View появится дополнительный пункт Terminal Window.

View -> TerminalWindow

View -> TerminalWindow

Если вы запустите его, то увидите само окно для работы с COM-портом.

Terminal Window

Terminal Window

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

  1. Кнопка для подключения к COM-порту
  2. Выбор номера порта (проверьте к какому порту подключена ваша плата)
  3. Скорость передачи. Поскольку передача асинхронная, необходимо на обоих сторонах одинаково задать скорость передачи данных
  4. Способ отображения принятых данных
  5. Очистка истории приема
  6. Очистка истории передачи
  7. Дополнительные настройки
  8. Поле с полученными данными
  9. Поле с отправленными данными
  10. Поле для ввода данных на отправку
  11. Формат отправки
  12. Кнопка "отправить"

Итак, вся настройка выполнена и можно попробовать отправить что-нибудь в COM-порт из памяти микроконтроллера.

Первая программа

Для начала сделаем самое простое — попробуем отправлять в UART данные с микроконтроллера. Заведем переменную i, а затем будем в раз секунду увеличивать ее и отправлять через UART. Все это делает следующий код:

#include <avr/io.h>
#define F_CPU 16000000UL //16MHz
#include <util/delay.h>

uint8_t i = 0;

void UARTInit(void) {
    UBRRH = 0;
    UBRRL = 103; //baud rate 9600
    UCSRB = (1<<RXEN)|(1<<TXEN);
    UCSRC = (1<<URSEL)|(1<<UCSZ1)|(1<<UCSZ0); //8 bit, 1 stop bit
}

void UARTSend(uint8_t data) {
    while(!(UCSRA & (1<<UDRE)));
    UDR = data;
}

int main(void) {
    UARTInit();
    while(1) {
        i++;
        UARTSend(i);
        _delay_ms(1000);
    }
}

Если все сделано правильно, то после открытия Terminal Window и нажатии кнопки "Connect" вы увидите как раз в секунду в окне приема будут появляться числа:

Terminal Window для первой проаграммы

Terminal Window для первой проаграммы

Если что-то идет не так, убедитесь, что правильно выбраны параметры передачи — номер порта, скорость, формат отображения. Более подробно параметры указаны в меню "Options".
Теперь разберемся с самой программой.
В первую очередь интересует функция инициализации UARTInit(). В ней необходимо выставить следующие обязатльные настройки:

  • UBRR. Этот регистр состоит из двух байт — UBRRH и UBRRL. Они отвечают за скорость передачи. Чаще всего, достаточно обратиться к таблицам 60-63 на страницах 153-156 datasheet’а и посмотреть, что нужно записать в этот регистр для корректной настройки скорости. В нашем случае тактовая частота 16МГц, работа происходит без удвоения на скорости 9600 Бод. Следовательно, в UBRR нужно записать число 103. Это число помещается в один байт. В итоге UBRRH = 0, UBRRL = 103.
  • Выбор скорости передачи

    Выбор скорости передачи

  • UCSRB. В этом регистре необходимо выставить два бита, которые включают прием и передачу: RXEN и TXEN
  • UCSRC. В этом регистре собраны основные параметры передачи. Нам необходимо установить только два бита: UCSZ1 и UCSZ2. Они задают количество бит при передаче равным восьми. Так остальные биты не тронуты, передача будет асинхронной, без проверочной суммы и с одним стоп-битом. Обратите внимание на бит URSEL. Это специальный защитный бит. Если вы будете пытаться изменить состояние регистра, не выставив этот бит в "1", то изменений не произойдет. Поэтому при настройке его необходимо установить вместе с другими настройками.

В завершении настройки нужно указать еще один важный момент. Обычно при выставлении настроек в регистре, не присваивают значение всему регистру, а меняют только нужные биты. Из-за того, что мы используем бутлоадер от Arduino (а он, в свою очередь, UART для загрузки), то лучше защитить себя от того, что могло остаться в этих регистрах после бутлоадера и сконфигурировать UART именно присваиванием.
Если вы пишите программу для чистого микроконтроллера и загружаете программу программатором, то тело функции настройки будет выглядеть так:

UBRRH = 0;
UBRRL = 103; //baud rate 9600
UCSRB |= (1<<RXEN)|(1<<TXEN);
CSRC |= (1<<URSEL)|(1<<UCSZ1)|(1<<UCSZ0); //8 bit, 1 stop bit

Теперь разберем функцию UARTSend(uint8_t data).
Строка

while(!(UCSRA & (1<<UDRE)));

…означает, что пока в бите UDRE регистра UCSRA "1" — будет выполняться пустой цикл. Бит UDRE (UART Data Register Empty) следит за состоянием регистра, в котором хранятся данные, принятые из UART. Фактически, программа дожидается того, что буфер на отправку чист (предыдущий байт передан) и можно передавать следующий байт.
Следующим шагом мы помещаем в регистр UDR (UART Data Register) данные. То что оказывается в это регистре — немедленно отправляется в COM-порт. Вот и все, что нужно для простейшей передачи восьмибитного числа.

Прием данных

Самый простой способ принимать байты через UART — делать это при помощи прерывания. Прерывание — очень мощный инструмент микроконтроллера, который мы до сих пор не применяли. Прерывание позволяет остановить выполнение основной программы и обработать какое-либо событие. Посмотрим, как это работает на практике.
В первую очередь необходимо подключить заголовочный файл прерываний:

#include <avr/interrupt.h>

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

sei();

Далее необходимо разрешить конкретное прерывание. Вообще их много, но нас интересует прерывание UART. За них отвечает бит RXCIE (Receive Complete Interrupt Enable — прерывание по завершению приема) в регистре UCSRB.

UCSRB = 1<<RXCIE;

Когда срабатывает прерывание, то вызывается вектор прерывания. Каждое прерывание имеет свое имя. Конкретно вектор прерывания UART на прием называется USART_RXC_vect. Обработчик прерывания выглядит следующим образом:

ISR(USART_RXC_vect) {
    rx_data = UDR;
    rx_flag = 1;
}

То есть когда прием через UART будет завершен, вызовется и выполнится этот кусок кода. В нем данные из приемного буфера UDR переносятся в глобальную переменную rx_data и выставляется флаг rx_flag, говорящий о том, что прием завершен.
Теперь добавим все эти доработки в первую программу и получим следующее:

#include <avr/io.h>
#define F_CPU 16000000UL //16MHz
#include <util/delay.h>
#include <avr/interrupt.h>

uint8_t receive = 0;
uint8_t rx_data = 0;
volatile uint8_t rx_flag = 0;

void UARTInit(void) {
    UBRRH = 0;
    UBRRL = 103; //baud rate 9600
    UCSRB = (1<<RXEN)|(1<<TXEN)|(1<<RXCIE);
    UCSRC = (1<<URSEL)|(1<<UCSZ1)|(1<<UCSZ0); //8 bit, 1 stop bit
}

void UARTSend(uint8_t data) {
    while(!(UCSRA & (1<<UDRE)));
    UDR = data;
}

unsigned char UARTGet() {
    while(!rx_flag);
    rx_flag = 0;
    return rx_data;
}

int main(void) {
    sei();
    UARTInit();
    while(1) {
        receive = UARTGet();
        receive++;
        UARTSend(receive);
    }
}

ISR(USART_RXC_vect) {
    rx_data = UDR;
    rx_flag = 1;
}

Обратите внимание на объявление переменных. Перед типом переменной rx_flag стоит служебное слово volatile. Это сигнал для компилятора, что эту переменную оптимизировать нельзя. В противном случае, компилятор выкидывает вот этот цикл:

while(!rx_flag);

Теперь перейдем к функции UARTGet(). Она ждет, пока значение флага не станет равно "1" и затем возвращет состояние переменной rx_data. Она нужна просто для того, чтобы сделать main удобочитаемым.
То есть сама программа доходит до строчки

receive = UARTGet();

и останавливает свое выполнение до тех пор, пока не придут данные. Как только МК что-то принимает, он увеличивает это значение на 1 и отправляет обратно. Откройте Terminal Window и убедитесь в этом (чтобы отправить что-то необходимо ввести цифру в нижнюю строку и нажать Enter).

Terninal Window с отправленными данными

Terninal Window с отправленными данными

Управление периферией

В последнем примере хочется уже начать чем-то управлять, чтобы наши команды с ПК оказывали видимое воздействие на реальный мир.
На плате EduBoard (как и на большинстве Arduino-совместимых плат) есть светодиод, подключенный к 13му выводу. В нашей следующей программе мы будем будем включать его, если пользователь отправил "0" и выключать в любом другом случае.
Доработки предельно простые, но мы приведем весь код целиком:

#include <avr/io.h>
#define F_CPU 16000000UL //16MHz
#include <util/delay.h>
#include <avr/interrupt.h>

uint8_t receive = 0;
uint8_t rx_data = 0;
volatile uint8_t rx_flag = 0;

void UARTInit(void) {
    UBRRH = 0;
    UBRRL = 103; //baud rate 9600
    UCSRB = (1<<RXEN)|(1<<TXEN)|(1<<RXCIE);
    UCSRC = (1<<URSEL)|(1<<UCSZ1)|(1<<UCSZ0); //8 bit, 1 stop bit
}

void UARTSend(uint8_t data) {
    while(!(UCSRA & (1<<UDRE)));
    UDR = data;
}

unsigned char UARTGet() {
    while(!rx_flag);
    rx_flag = 0;
    return rx_data;
}

int main(void) {
    DDRB |= 1<<PB5;
    PORTB |= 1<<PB5;
    sei();
    UARTInit();
    while(1) {
        receive = UARTGet();
        if(receive == 0) PORTB |= 1<<PB5;
        else PORTB &= ~(1<<PB5);
    }
}

ISR(USART_RXC_vect) {
    rx_data = UDR;
    rx_flag = 1;
}

Итоги

В этой статье мы описали самые простые случаи обмена. Недостатки в том, что на время ожидания команды программа останавливается и трудно наладить передачу блоков данные.
В реальных системах чаще всего организуют приемный буфер, а затем его разбирают. Информацию при этом дополнительно кодируют для детектирования ошибок передачи.
Тем не менее, описанных в статье примеров будет более чем достаточно для организации отладочного вывода или управления домашним роботом.

Индивидуальные задания

  1. Напишите программу, которая будет подсчитывать количество единиц в принятом из COM-порта байте и отправлять их обратно
  2. Подключите светодиод к выводу PB1 (как в этой статье) и напишите программу, которая будет выставлять яркость свечения светодиода, значение которой будет приниматься из COM-порта

Остальные статьи цикла можно найти здесь.

Мы будем очень рады, если вы поддержите наш ресурс и посетите магазин наших товаров shop.customelectronics.ru.

Метки: , , , , , Просмотров: 26600