Предыдущие статьи курса:
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, в котором всё несколько иначе.
Терминал
Работа с микроконтроллером будет осуществляться посредством RS232 over USB, т.е. по виртуальному COM-порту, для чего потребуется программа-терминал, с помощью которой будет осуществляться прием и передача данных.
В ОС WindowsXP такая программка установлена по дефолту(Стандартные -> Связь -> Терминал), а вот в Win7 и выше её уже убрали, поэтому владельцам этих ОС придется её скачать. Установки программа не требует, достаточно распаковать архив и можно работать.
- При первом запуске программа с двумя прикольными телефонными аппаратами на значке(да, это не красный и желтый крокодил, это телефоны такие были, если кто не знает )предложит создать новое подключение:
Вводим имя подключения и нажимаем ОК. - В следующем окне мастера подключения вводить ничего ненужно, нужно выбрать COM-порт, с которым работает драйвер FT232R. В моем случае это COM14:
- В последнем окне нужно ввести настройки COM-порта как на скриншоте ниже:
В последствии, в зависимости от задачи и программы эти настройки могут меняться, однако управление потоком всегда должно быть отключено. - После создания подключения открываем меню Файл -> Свойства, переходим на вкладку Параметры и выбираем Эмуляция терминала: ANSIW.
Там же нажимаем кнопку Параметры ASCII и выставляем галочки, как на скриншоте: - Открываем меню Вид -> Шрифт и устанавливаем шрифт, поддерживающий кириллицу:
- Открываем меню Файл -> Сохранить, чтобы сохранить подключение.
Теперь терминал готов к работе.
Программа
Для того, чтобы разобраться с UARTом была написана простенькая программа, которая выводит текст на уже освоенный ранее графический дисплей MT-12232A. Основной код программы в файле main.c, который снабжен большим количеством комментариев. Остальные файлы - это уже разобранные ранее библиотека и заголовочные файлы для дисплея. Замечания по оптимизации кода, дополнения и исправления приветствуются.
Возможности программы:
- вывод на индикатор посимвольно текста вводимого в терминале
- автопереход на новую строку
- принудительный переход на новую строку клавишей Enter
- забой клавишей Backspace в пределах одного экрана
- построчное автоперелистывание экрана
- сдвиговый буфер приемника
- пакетная передача данных
- мигающий курсор
- простая настройка: скорость порта, максимальная длина строки и количество используемых строк, размер сдвигового буфера
Установки
Откроем main.c и рассмотрим код программы более подробно. Начнем с заголовочных файлов двух библиотек, которые ранее не были рассмотрены:
- Код: Выделить всё
#include <util/atomic.h> // подключаем библиотеку для атомарных операций
#include <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 // символ забоя
- F_CPU - первым делом задается тактовая частота в герцах(в данном случае 16МГц), на которой работает контроллер.
- BAUD - скорость передачи данных UART. И в программе и в терминале должна быть задана одинаковая.
- LINE_SIZE - используемая максимальная длина строки в символах. При заполнении строки будет осуществлен автоматический переход на новую строку. Для дисплея MT-12232 можно указать значение от 1 до 20.
- DYSPLAY_SIZE - количество используемых строк. При превышении значения экран будет смещаться вверх на одну строку. Для дисплея MT-12232 можно указать значение от 1 до 4.
- BUFFER_SIZE - размер сдвигового буфера в байтах(символах).
- RESPONSE - ответ, который будет отправлен терминалу ПК при нажатии клавиши Enter для перехода на новую строку.
Ниже в коде есть ещё две установки:
- Код: Выделить всё
#define NEWLINE_CHR 0x0dU // символ перевода строки
#define BACKSPACE_CHR 0x08U // символ забоя
Функции
Помимо основной сишной функции main с бесконечным циклом, которая реализует основную логику работы, в программе есть ещё четыре более интересные для рассматриваемой темы функции: uartInit, pushChar, shiftChar и sendData. Рассмотрим каждую из них подробно.
- uartInit - функция инициализации 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 подробно.
В качестве параметра функция принимает расчетное значение baudrate, которое нужно для задания скорости передачи данных. Откуда же берется это значение? Оно рассчитывается по формуле и зависит от тактовой частоты, требуемой скорости передачи и установки коэффициента предделителя(бита U2X0 регистра UCSR0A):
Для заданных условий подойдет предделитель 16, поэтому будет использоваться первая формула. По умолчанию бит U2X0 установлен в ноль, поэтому дополнительно сбрасывать его не нужно.
Данная функция вызывается в начале функции main, до бесконечного цикла, т.к. инициализация и настройка должны быть произведены только один раз.- Код: Выделить всё
uartInit(BAUDRATE); // инициализация UART
- Код: Выделить всё
#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 бит
- Код: Выделить всё
UBRR0L = (unsigned char)baudrate; // устанавливаем скорость передачи
Для упрощения расчетов скорости UART и не только, можно воспользоваться удобным online-avr-калькулятором, либо скачать и установить аналогичный, но более мощный бесплатный калькулятор AVRCalc.
Следующая строка- Код: Выделить всё
UCSR0B|= (1<<RXCIE0); // разрешаем прерывание по приему
Строка- Код: Выделить всё
UCSR0B|= (1<<TXEN0)|(1<<RXEN0); // включаем приемник и передатчик
В последней строке- Код: Выделить всё
UCSR0C|= (2<<UPM00)|(3<<UCSZ00); // проверка на четность even parity (UPM1,UPM0), формат данных 8бит
По умолчанию проверка на четность отключена. Для её включения нужно сдвинуть число 2 на UPM00 разряда влево и применить поразрядное "или", что будет равносильно сбросу бита UPM00 и установке бита UPM01. В ATmega168 нет того костыля, когда регистры UCSRC и UBRRH расположены по одному адресу, поэтому при обращении к регистру UCSR0C никакого бита URSEL устанавливать ненужно.
8-битный формат данных включен по умолчанию, но надежнее будет включить его явно в программе. Для включения 8-битного режима нужно сдвинуть число 3 на UCSZ00 разряда влево и применить поразрядное "или", что будет равносильно установке битов UCSZ00 и UCSZ01. Биты UCSZ00 и UCSZ01 скомбинированы с битом UCSZ02 регистра UCSR0B. Для включения 9-битного формата данных нужно будет установить и его.
Для включения режима с двумя стоповыми битами можно дополнительно установить бит USBS0 регистра UCSR0C:
На этом настройка UART может быть закончена, больше в данном случае с ним ничего делать ненужно. - pushChar - функция записи символа в сдвиговый буфер. Записывает поступающие с приемника UART символы в сдвиговый буфер, заполняя его. Если буфер переполняется, то смещает символы влево, затирая первый символ и записывает поступивший символ в последнюю освободившуюся ячейку.
Для сдвигового буфера отведена volatile-переменная- Код: Выделить всё
volatile char c_buf[BUFFER_SIZE + 1] = "\0"; // сдвиговый буфер UART
С функцией pushChar связан вектор прерывания по завершению приема:- Код: Выделить всё
ISR(USART_RX_vect) // вектор прерывания UART - завершение приема
{
usartRxBuf = UDR0;
pushChar(usartRxBuf);
usartRxBuf = 0;
}
Сдвиговый буфер нужен для того, чтобы данные не терялись, когда основная программа отвлекается на какие-то свои дела и не может своевременно забирать данные из регистра приемника. В данной программе в качестве такого примера применена реализация мигающего курсора на "тяжелых" вызовах задержек посредством библиотеки 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);
В случае же применения сдвигового буфера данные будут накапливаться в нем и забираться программой по мере возможности. Таким образом, размер сдвигового буфера нужно выбирать исходя из того, сколько данных может быть принято в единицу времени, чем больше, тем больше должен быть размер сдвигового буфера, чтобы он успевал опустошаться. В качестве эксперимента предлагаю поиграться с константой BUFFER_SIZE и подобрать оптимальный размер буфера, при котором данные не будут теряться. - shiftChar - функция извлечения символа из сдвигового буфера. Извлекает первый символ и смещает все последующие за ним влево. Последняя освободившаяся ячейка затирается.
Эта функция примечательна тем, что в ней есть блок кода, заключенный в макрос заголовочного файла atomic.h:- Код: Выделить всё
ATOMIC_BLOCK(ATOMIC_FORCEON){ // выделяем блок кода, прерывания в котором запрещены
Представим ситуацию, когда из кода основного цикла программы вызвана функция shiftChar для получения очередного символа:- Код: Выделить всё
if ((c[0] = shiftChar())){ // если получен символ, то...
- Код: Выделить всё
l = strlen((char*)c_buf); // считываем кол-во символов в буфере
Для предотвращения таких ситуаций, перед работой с переменной, значение которой может измениться в результате прерывания, нужно прерывания отключать, а по завершению работы снова включать, т.е. обеспечивать атомарный доступ.
На самом деле можно обойтись и без хидера atomic.h, т.к. код:- Код: Выделить всё
ATOMIC_BLOCK(ATOMIC_FORCEON){ // выделяем блок кода, прерывания в котором запрещены
................
}
- Код: Выделить всё
cli(); // выключить прерывания
................
sei(); // включить прерывания
Поскольку в данном конкретном случае массив c_buf может быть модифицирован только по прерыванию завершения приема, то достаточно будет отключить и включить только его:- Код: Выделить всё
UCSR0B &=~(1<<RXCIE0); // запрещаем прерывание по приему
................
UCSR0B|= (1<<RXCIE0); // разрешаем прерывание по приему
Подробнее обо всех возможностях atomic.h можно узнать здесь. - sendData - функция передачи данных. Реализует пакетную отправку данных. Отправляет массив данных или строку на терминал ПК.
Функция принимает два параметра: массив данных(в данном случае строковую константу RESPONSE) и размер массива данных:- Код: Выделить всё
sendData(RESPONSE, A_SIZE(RESPONSE)); // отправляем терминалу "OK"
- Код: Выделить всё
#define A_SIZE(a) (sizeof(a)/sizeof(*(a))) // макрос расчета числа элементов массива
Работа этой функции тесно связана с обработчиком прерывания по опустошению регистра передатчика, а именно вектором прерывания- Код: Выделить всё
ISR(USART_UDRE_vect) // вектор прерывания UART - регистр данных на передачю пуст
При вызове функции sendData первым делом проверяется не нулевой размер массива данных, после чего идет ожидание окончания передачи предыдущего пакета данных в цикле- Код: Выделить всё
while((UCSR0B & (1<<UDRIE0))); // ждем пока завершится предыдущая передача
Как только регистр передатчика окажется пуст отработает вектор прерывания- Код: Выделить всё
ISR(USART_UDRE_vect) // вектор прерывания UART - регистр данных на передачю пуст
{
if(tx_i < tx_size){ // если данные на передачу ещё есть, то...
UDR0 = tx_data[tx_i]; // передаем и
tx_i++; // инкрементируем счетчик передачи
} else {
UCSR0B &=~(1<<UDRIE0); // если данные закончились, то запрещаем прерывание по опустошению регистра передатчика
}
}
На самом деле такой способ передачи не достаточно эффективен, т.к. в случае передачи нескольких больших пакетов данных подряд может возникать остановка в цикле- Код: Выделить всё
while((UCSR0B & (1<<UDRIE0)));
Прошивка
Теперь прошиваем в контроллер hex-файл прошивки, запускаем терминал и открываем ранее созданное подключение.
Далее нажимаем на значок телефона для установки соединения. Если значок не активен, то сначала нужно нажать на соседний значок и "положить трубку".
Схема модуля Freeduino2009 такова, что по умолчанию при каждой установке соединения будет осуществляться сброс контроллера. Проблемы для данной программы в этом никакой нет, но если необходимо исключить такое поведение, то нужно снять перемычку(джампер) JRS с платы модуля, только важно не забыть поставить её на место перед очередной прошивкой.
Теперь, при наборе текста в терминале, он будет печататься на экране дисплея, а при переводе строки клавишей "Ввод" в терминал будет отправляться ответ "ОК".
phpBB [media]
В качестве тренировки можно расширить функционал программы и сделать, например, поддержку клавиши "TAB", при нажатии которой будут вставлены 4 пробела, а также комбинации клавиш Ctrl-L для очистки всего экрана с подтверждением действия соответствующим ответом терминалу.