Работа с UART

Программирование микроконтроллеров

Работа с UART

alex83 » 22 июн 2015, 17:44

Данная статья является продолжением практического обучающего курса по программированию микроконтроллеров AVR.
Предыдущие статьи курса:
1. Микроконтроллеры - это же просто!
2. Работа с графическим LCD MT-12232A(C)
3. Виртуальные порты

Быстрые переходы:

После того, как на предыдущем этапе линии порта D, отвечающие за интерфейс UART микроконтроллера были освобождены посредством перераспределения шины данных индикатора на другие линии с помощью виртуальных портов, можно продолжить осваивать возможности модуля Freeduino2009 и научиться работать с UART.
Интерфейс UART может требоваться не только в мультипроцессорных системах для обмена данными между контроллерами, но и для сопряжения контроллера с ПК, причем не только с целью управления каким-то процессом с ПК, но и для любого взаимодействия с программой как таковой. Это может быть удобно для отладки и настройки, когда, например, есть система, которая работает с какими-либо аналоговыми датчиками и для корркетной обработки поступающей информации требуется вводить корректировочные коэффициенты. Не перепрошивать же в этом случае контроллер каждый раз, когда нужно подправить коэффициент, гораздо удобнее завести для него переменную в программе и менять динамически в терминале ПК, а результат работы программы получать также на терминал, последнее особенно важно, т.к. в некоторых случаях позволяет вообще отказаться от каких-либо дисплеев и индикаторов.
Благодаря наличию в составе модуля замечательной микросхемы FT232R, реализующей RS232 over USB и подключенной к интерфейсу UART микроконтроллера, для сопряжения последнего с ПК паять ничего не придется.

В микроконтроллерах AVR UART имеет несколько иную реализацию и называется USART(Universal Synchronous Asynchronous Receiver Transmitter), т.е. UART, который может работать в том числе и в синхронном режиме, когда сигнал синхронизации передается отдельно от данных и старт/стоп биты не используются, что позволяет добиться большей производительности, однако, данная возможность ниже рассматриваться не будет, ввиду её неприменимости для поставленной задачи.
По работе с UART, его конфигурации и настройке написано и рассказано очень много, например здесь или здесь, поэтому заострять внимание на некоторых общих моментах нет смысла, однако рассказать подробнее о настройке всё же придется. Дело в том, что все примеры в интернете, как правило, базируются на ATmega8, а в данном случае предстоит работать с ATmega168, в котором всё несколько иначе.


terminalТерминал
Работа с микроконтроллером будет осуществляться посредством RS232 over USB, т.е. по виртуальному COM-порту, для чего потребуется программа-терминал, с помощью которой будет осуществляться прием и передача данных.
В ОС WindowsXP такая программка установлена по дефолту(Стандартные -> Связь -> Терминал), а вот в Win7 и выше её уже убрали, поэтому владельцам этих ОС придется её скачать. Установки программа не требует, достаточно распаковать архив и можно работать.
HyperTerminal.zip
(230.37 Кб) Скачиваний: 286

  • При первом запуске программа с двумя прикольными телефонными аппаратами на значке(да, это не красный и желтый крокодил, это телефоны такие были, если кто не знает :mrgreen: )предложит создать новое подключение:

    Вводим имя подключения и нажимаем ОК.

  • В следующем окне мастера подключения вводить ничего ненужно, нужно выбрать COM-порт, с которым работает драйвер FT232R. В моем случае это COM14:

  • В последнем окне нужно ввести настройки COM-порта как на скриншоте ниже:

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

  • После создания подключения открываем меню Файл -> Свойства, переходим на вкладку Параметры и выбираем Эмуляция терминала: ANSIW.
    Там же нажимаем кнопку Параметры ASCII и выставляем галочки, как на скриншоте:


  • Открываем меню Вид -> Шрифт и устанавливаем шрифт, поддерживающий кириллицу:


  • Открываем меню Файл -> Сохранить, чтобы сохранить подключение.
    Теперь терминал готов к работе.


programПрограмма
Для того, чтобы разобраться с UARTом была написана простенькая программа, которая выводит текст на уже освоенный ранее графический дисплей MT-12232A. Основной код программы в файле main.c, который снабжен большим количеством комментариев. Остальные файлы - это уже разобранные ранее библиотека и заголовочные файлы для дисплея. Замечания по оптимизации кода, дополнения и исправления приветствуются.
Возможности программы:
  • вывод на индикатор посимвольно текста вводимого в терминале
  • автопереход на новую строку
  • принудительный переход на новую строку клавишей Enter
  • забой клавишей Backspace в пределах одного экрана
  • построчное автоперелистывание экрана
  • сдвиговый буфер приемника
  • пакетная передача данных
  • мигающий курсор
  • простая настройка: скорость порта, максимальная длина строки и количество используемых строк, размер сдвигового буфера
Скачать архив с проектом Atmel Studio 6:
MT-12232.zip
(77 Кб) Скачиваний: 331
В программе конечно же раскрыт далеко не весь её потенциал, но она всего лишь призвана помочь понять, что такое UART и как с ним работать, а за одно и узнать побольше о языке Си. Тем не менее, если у кого-то возникнет практическая потребность в такой программе, то при желании её можно легко усовершенствовать.

settingsУстановки
Откроем main.c и рассмотрим код программы более подробно. Начнем с заголовочных файлов двух библиотек, которые ранее не были рассмотрены:
Код: Выделить всё
#include <util/atomic.h>               // подключаем библиотеку для атомарных операций
#include <string.h>                     // подключаем библиотеку для работы со строками
Первая нужна для удобной организации атомарного доступа к переменным, о котором подробно будет рассказано ниже, а вторая string.h для работы со строками(копирования, конкатенации, вычисления длины и т.д.).

Далее идет раздел "установки", в котором объявлены несколько констант с параметрами:
Код: Выделить всё
#ifndef F_CPU
   #define F_CPU 16000000UL            // тактовая частота
#endif               
#define BAUD 9600UL                  // скорость передачи данных по UART
#define LINE_SIZE 15U                  // длина строки дисплея (для MT-12232 макс. 20 символов)
#define DYSPLAY_SIZE 4U               // количество строк на экране (для MT-12232 макс. 4)
#define BUFFER_SIZE 255U               // размер буфера приемника
#define RESPONSE "\nOK\n"               // ответ терминалу при нажатии клавиши Ввод

#define NEWLINE_CHR 0x0dU            // символ перевода строки
#define BACKSPACE_CHR 0x08U            // символ забоя

  1. F_CPU - первым делом задается тактовая частота в герцах(в данном случае 16МГц), на которой работает контроллер.
  2. BAUD - скорость передачи данных UART. И в программе и в терминале должна быть задана одинаковая.
  3. LINE_SIZE - используемая максимальная длина строки в символах. При заполнении строки будет осуществлен автоматический переход на новую строку. Для дисплея MT-12232 можно указать значение от 1 до 20.
  4. DYSPLAY_SIZE - количество используемых строк. При превышении значения экран будет смещаться вверх на одну строку. Для дисплея MT-12232 можно указать значение от 1 до 4.
  5. BUFFER_SIZE - размер сдвигового буфера в байтах(символах).
  6. RESPONSE - ответ, который будет отправлен терминалу ПК при нажатии клавиши Enter для перехода на новую строку.
Модификаторы типов U и L в значениях констант означают unsigned и long соответственно.
Ниже в коде есть ещё две установки:
Код: Выделить всё
#define NEWLINE_CHR 0x0dU               // символ перевода строки
#define BACKSPACE_CHR 0x08U               // символ забоя
Они задают hex-коды символов ввода(Enter) и забоя(Backspace), которые программа обрабатывает отдельно. Как правило они везде одинаковые и менять ничего не понадобиться, однако для лучшей читаемости и переносимости кода такие вещи нужно объявлять константами.

functionsФункции
Помимо основной сишной функции main с бесконечным циклом, которая реализует основную логику работы, в программе есть ещё четыре более интересные для рассматриваемой темы функции: uartInit, pushChar, shiftChar и sendData. Рассмотрим каждую из них подробно.

  • uartInituartInit - функция инициализации UART. Осуществляет настройку UART для работы на прием и передачу с заданной скоростью, проверкой четности и 8-ми битным форматом данных с одним стоповым битом.
    Код: Выделить всё
    // инициализация UART
    void uartInit (unsigned int baudrate)
    {
       UBRR0H = (unsigned char)(baudrate>>8);   // сдвигаем число вправо на 8 бит
       UBRR0L = (unsigned char)baudrate;      // устанавливаем скорость передачи
       UCSR0B|= (1<<RXCIE0);            // разрешаем прерывание по приему
       UCSR0B|= (1<<TXEN0)|(1<<RXEN0);     // включаем приемник и передатчик
       UCSR0C|= (2<<UPM00)|(3<<UCSZ00);    // проверка на четность even parity (UPM1,UPM0), формат данных 8бит
    }
    Как уже было сказано выше, настройка UART для ATmega168 несколько отличается от ATmega8 или ATmega16. Для того, чтобы хорошо во всем разобраться нужно открыть оригинальную документацию, раздел USART0. Одно из главных отличий - имена регистров настройки и их битов, в каждое имя нужно добавить ноль.

    Рассмотрим настройку UART подробно.
    В качестве параметра функция принимает расчетное значение baudrate, которое нужно для задания скорости передачи данных. Откуда же берется это значение? Оно рассчитывается по формуле и зависит от тактовой частоты, требуемой скорости передачи и установки коэффициента предделителя(бита U2X0 регистра UCSR0A):

    Для заданных условий подойдет предделитель 16, поэтому будет использоваться первая формула. По умолчанию бит U2X0 установлен в ноль, поэтому дополнительно сбрасывать его не нужно.
    Данная функция вызывается в начале функции main, до бесконечного цикла, т.к. инициализация и настройка должны быть произведены только один раз.
    Код: Выделить всё
    uartInit(BAUDRATE);            // инициализация UART
    В функцию передается параметр BAUDRATE, который на самом деле является макросом, реализующим ту самую формулу и объявленным в начале раздела "объявление глобальных переменных и функций" :
    Код: Выделить всё
    #define BAUDRATE ((F_CPU)/(BAUD*16UL)-1)    // макрос расчета скорости передачи для UBRR

    Вернемся к функции uartInit.
    Две первые строки кода записывают в регистр UBRR0 расчетное число baudrate. UBRR0 - это двухбайтный 12-ти разрядный регистр, первые 8 разрядов которого соответствуют младшему байту UBRR0L, а остальные 4 разряда(младший полубайт старшего байта) старшему байту UBRR0H. В отличие от ATmega8(16), где младший и старший байты UBRR расположены по разным адресам, в ATmega168 для доступа к UBRR0 используется один начальный адрес 0xC4.

    Строка
    Код: Выделить всё
    UBRR0H = (unsigned char)(baudrate>>8);   // сдвигаем число вправо на 8 бит
    сдвигает старший байт числа вправо на 8 бит, т.е. перемещает его на место младшего и записывает его в старший байт UBRR0H, а строка
    Код: Выделить всё
    UBRR0L = (unsigned char)baudrate;      // устанавливаем скорость передачи
    записывает младший байт числа в младший байт UBRR0L. Вот и вся математика :) . Подробнее с битовыми операциями можно познакомиться здесь.
    Для упрощения расчетов скорости UART и не только, можно воспользоваться удобным online-avr-калькулятором, либо скачать и установить аналогичный, но более мощный бесплатный калькулятор AVRCalc.

    Следующая строка
    Код: Выделить всё
    UCSR0B|= (1<<RXCIE0);            // разрешаем прерывание по приему
    посредством сдвига влево и поразрядного "или" устанавливает бит RXCIE0 регистра UCSR0B, тем самым разрешая прерывание по приему, которое будет срабатывать каждый раз, когда приемник UART примет байт информации.

    Строка
    Код: Выделить всё
    UCSR0B|= (1<<TXEN0)|(1<<RXEN0);     // включаем приемник и передатчик
    аналогичным образом устанавливает биты TXEN0 и RXEN0 регистра UCSR0B для включения приемника и передатчика UART. При включении приемника и передатчика их порты будут сконфигурированы соответствующим образом, поэтому дополнительно их настраивать ненужно.

    В последней строке
    Код: Выделить всё
    UCSR0C|= (2<<UPM00)|(3<<UCSZ00);    // проверка на четность even parity (UPM1,UPM0), формат данных 8бит
    в регистре UCSR0C настраивается проверка четности и 8-битный формат данных.
    По умолчанию проверка на четность отключена. Для её включения нужно сдвинуть число 2 на UPM00 разряда влево и применить поразрядное "или", что будет равносильно сбросу бита UPM00 и установке бита UPM01. В ATmega168 нет того костыля, когда регистры UCSRC и UBRRH расположены по одному адресу, поэтому при обращении к регистру UCSR0C никакого бита URSEL устанавливать ненужно.

    8-битный формат данных включен по умолчанию, но надежнее будет включить его явно в программе. Для включения 8-битного режима нужно сдвинуть число 3 на UCSZ00 разряда влево и применить поразрядное "или", что будет равносильно установке битов UCSZ00 и UCSZ01. Биты UCSZ00 и UCSZ01 скомбинированы с битом UCSZ02 регистра UCSR0B. Для включения 9-битного формата данных нужно будет установить и его.

    Для включения режима с двумя стоповыми битами можно дополнительно установить бит USBS0 регистра UCSR0C:

    На этом настройка UART может быть закончена, больше в данном случае с ним ничего делать ненужно.

  • pushCharpushChar - функция записи символа в сдвиговый буфер. Записывает поступающие с приемника UART символы в сдвиговый буфер, заполняя его. Если буфер переполняется, то смещает символы влево, затирая первый символ и записывает поступивший символ в последнюю освободившуюся ячейку.
    Для сдвигового буфера отведена volatile-переменная
    Код: Выделить всё
    volatile char c_buf[BUFFER_SIZE + 1] = "\0";   // сдвиговый буфер UART
    , представляющая собой строковый массив размером BUFFER_SIZE+1. Единицу к размеру нужно прибавлять для того, чтобы зарезервировать в массиве ячейку для escape-последовательности \0, означающей символ с кодом ноль(не путать с нолем) и необходимой для обозначения конца строки. Этот символ также необходим для правильной работы стандартных функций обработки строк. Спецификатор volatile для c_buf в данном конкретном коде не обязателен, т.к. чтение и модификация этой переменной происходит либо в обработчике прерывания, либо в функции shiftChar(см. ниже), в блоке кода, где прерывания запрещены, однако, поскольку переменная является глобальной, то на случай какого-либо изменения кода лучше подстраховаться.

    С функцией pushChar связан вектор прерывания по завершению приема:
    Код: Выделить всё
    ISR(USART_RX_vect)      // вектор прерывания UART - завершение приема
    {
       usartRxBuf = UDR0;
       pushChar(usartRxBuf);
       usartRxBuf = 0;
    }
    При возникновении прерывания, байт данных(символ) из регистра приемника записывается в volatile-переменную однобайтового буфера uartRxBuf, которая в свою очередь передается в функцию pushChar, где байт данных записывается в сдвиговый буфер.
    Сдвиговый буфер нужен для того, чтобы данные не терялись, когда основная программа отвлекается на какие-то свои дела и не может своевременно забирать данные из регистра приемника. В данной программе в качестве такого примера применена реализация мигающего курсора на "тяжелых" вызовах задержек посредством библиотеки delay.h:
    Код: Выделить всё
    LCDG_SendSymbol(c_pos, c_line, '_');
    _delay_ms(200);
    LCDG_SendSymbol(c_pos, c_line, ' ');
    _delay_ms(200);

    Представим код, в котором в векторе прерывания по приему нет вызова функции pushChar записи данных в сдвиговый буфер и данные просто сохраняются в однобайтовом буфере uartRxBuf, а программа забирает их из него некой функцией getChar. В этом случае возможна ситуация, когда за время "пребывания" программы в вызове длительной задержки
    Код: Выделить всё
    _delay_ms(200);
    будет принят не один, а несколько байт данных, т.е. прерывание по приемнику сработает несколько раз и данные в однобайтовом буфере uartRxBuf просто затрут друг друга, а программа, "вернувшись" из задержки заберет байт, который был записан последним - часть данных будет потеряна.
    В случае же применения сдвигового буфера данные будут накапливаться в нем и забираться программой по мере возможности. Таким образом, размер сдвигового буфера нужно выбирать исходя из того, сколько данных может быть принято в единицу времени, чем больше, тем больше должен быть размер сдвигового буфера, чтобы он успевал опустошаться. В качестве эксперимента предлагаю поиграться с константой BUFFER_SIZE и подобрать оптимальный размер буфера, при котором данные не будут теряться.

  • shiftCharshiftChar - функция извлечения символа из сдвигового буфера. Извлекает первый символ и смещает все последующие за ним влево. Последняя освободившаяся ячейка затирается.
    Эта функция примечательна тем, что в ней есть блок кода, заключенный в макрос заголовочного файла atomic.h:
    Код: Выделить всё
    ATOMIC_BLOCK(ATOMIC_FORCEON){   // выделяем блок кода, прерывания в котором запрещены
    , с помощью которого удобно обеспечивать атомарный доступ к переменным. Атомарная операция - это операция, которая выполняется за один раз, не прерываясь ничем. Т.е. пока она выполняется - программа не будет отвлекаться на выполнение каких-либо других операций. Под атомарностью применительно к микроконтроллерам понимается то, что переменная не может быть модифицирована в обработчике прерывания. Такой переменной в коде программы является массив c_buf сдвигового буфера приемника, который модифицируется как в функции shiftChar, так и по прерыванию завершения приема в функции pushChar.
    Представим ситуацию, когда из кода основного цикла программы вызвана функция shiftChar для получения очередного символа:
    Код: Выделить всё
    if ((c[0] = shiftChar())){   // если получен символ, то...
    , где для модификации буфера вычисляется длина очереди:
    Код: Выделить всё
    l = strlen((char*)c_buf);         // считываем кол-во символов в буфере
    , а в это время происходит прерывание по завершению приема, из вектора которого вызывается функция pushChar, в которой в буфер добавляется символ и длина очереди меняется. В результате, по возвращению из прерывания в функцию shiftChar фактическая длина буфера будет больше на 1, а в переменной l останется старое значение, что приведет к сбою.
    Для предотвращения таких ситуаций, перед работой с переменной, значение которой может измениться в результате прерывания, нужно прерывания отключать, а по завершению работы снова включать, т.е. обеспечивать атомарный доступ.
    На самом деле можно обойтись и без хидера atomic.h, т.к. код:
    Код: Выделить всё
    ATOMIC_BLOCK(ATOMIC_FORCEON){   // выделяем блок кода, прерывания в котором запрещены
    ................      
    }
    эквивалентен:
    Код: Выделить всё
    cli();    // выключить прерывания
    ................
    sei();    // включить прерывания

    Поскольку в данном конкретном случае массив c_buf может быть модифицирован только по прерыванию завершения приема, то достаточно будет отключить и включить только его:
    Код: Выделить всё
    UCSR0B &=~(1<<RXCIE0);    // запрещаем прерывание по приему
    ................
    UCSR0B|= (1<<RXCIE0);      // разрешаем прерывание по приему

    Подробнее обо всех возможностях atomic.h можно узнать здесь.


  • sendDatasendData - функция передачи данных. Реализует пакетную отправку данных. Отправляет массив данных или строку на терминал ПК.
    Функция принимает два параметра: массив данных(в данном случае строковую константу RESPONSE) и размер массива данных:
    Код: Выделить всё
    sendData(RESPONSE, A_SIZE(RESPONSE));   // отправляем терминалу "OK"
    Для чего нужно передавать отдельным параметром размер, ведь его можно вычислить и в самой функции? Всё дело в том, что в языке Си, в функцию будет передан не массив, а указатель на его нулевой элемент, поэтому попытка узнать размер массива внутри тела функции даст не верный результат. Для расчета числа элементов массива используется макрос A_SIZE:
    Код: Выделить всё
    #define A_SIZE(a)  (sizeof(a)/sizeof(*(a)))   // макрос расчета числа элементов массива

    Работа этой функции тесно связана с обработчиком прерывания по опустошению регистра передатчика, а именно вектором прерывания
    Код: Выделить всё
    ISR(USART_UDRE_vect)    // вектор прерывания UART - регистр данных на передачю пуст

    При вызове функции sendData первым делом проверяется не нулевой размер массива данных, после чего идет ожидание окончания передачи предыдущего пакета данных в цикле
    Код: Выделить всё
    while((UCSR0B & (1<<UDRIE0)));   // ждем пока завершится предыдущая передача
    , который ждет когда прерывание по опустошению регистра передатчика будет запрещено, что будет означать окончание передачи предыдущего массива данных(см. ниже). Далее идет сохранение массива данных и его размера в глобальные переменные, обнуление volatile-переменной tx_i счетчика передачи и разрешение прерывания по опустошению регистра передатчика. С этого момента начинается процесс передачи.
    Как только регистр передатчика окажется пуст отработает вектор прерывания
    Код: Выделить всё
    ISR(USART_UDRE_vect)    // вектор прерывания UART - регистр данных на передачю пуст
    {
       if(tx_i < tx_size){         // если данные на передачу ещё есть, то...
          UDR0 = tx_data[tx_i];   // передаем и
          tx_i++;               // инкрементируем счетчик передачи
       } else {
          UCSR0B &=~(1<<UDRIE0);   // если данные закончились, то запрещаем прерывание по опустошению регистра передатчика
       }
    }
    , где будет передан первый элемент массива. Как только он будет передан и регистр передатчика станет пуст, прерывание сработает снова и будет передан второй элемент массива данных, и т.д. пока не будет передан весь массив. Как только данные закончатся прерывание по опустошению регистра передатчика будет снова запрещено до следующего вызова функции sendData.
    На самом деле такой способ передачи не достаточно эффективен, т.к. в случае передачи нескольких больших пакетов данных подряд может возникать остановка в цикле
    Код: Выделить всё
    while((UCSR0B & (1<<UDRIE0)));
    , поэтому в качестве тренировки код можно усовершенствовать и дополнить сдвиговым или кольцевым буфером передачи.
Таким образом, с настройкой UART и подходами при работе с ним разобрались. Что касаемо основного цикла программы, то подробно на нем останавливаться не будем, т.к. код достаточно хорошо комментирован.

testПрошивка
Теперь прошиваем в контроллер hex-файл прошивки, запускаем терминал и открываем ранее созданное подключение.
Далее нажимаем на значок телефона для установки соединения. Если значок не активен, то сначала нужно нажать на соседний значок и "положить трубку".

Схема модуля Freeduino2009 такова, что по умолчанию при каждой установке соединения будет осуществляться сброс контроллера. Проблемы для данной программы в этом никакой нет, но если необходимо исключить такое поведение, то нужно снять перемычку(джампер) JRS с платы модуля, только важно не забыть поставить её на место перед очередной прошивкой.
Теперь, при наборе текста в терминале, он будет печататься на экране дисплея, а при переводе строки клавишей "Ввод" в терминал будет отправляться ответ "ОК".
phpBB [media]


В качестве тренировки можно расширить функционал программы и сделать, например, поддержку клавиши "TAB", при нажатии которой будут вставлены 4 пробела, а также комбинации клавиш Ctrl-L для очистки всего экрана с подтверждением действия соответствующим ответом терминалу.
Аватара пользователя
alex83
Имя: Александр

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 1