LPCXpresso Урок 12. UART. Взаимодействуем с компьютером.

Предлагаю вашему вниманию поверхностное знакомство с UART интерфейсом в контроллере. Данный урок является вступлением к наиболее важной части (следующий урок) курса для новичков.

Что такое UART и как он выглядит можно почитать у DI HALT'а в статье AVR. Учебный курс. Передача данных через UART.

Схема

Основываться буден на схеме из пятого урока. Всё остальное можно и убрать и оставить, оно нам будет безразлично.
Для проверки связи с компьютером вам потребуется собрать и подключить один из следующих преобразователей интерфейсов:
  • при наличие у компьютера COM-порта проще собрать преобразователь на MAX3232 (такой же как на MAX232) запитать его можно от вывода 3V3 платы;
  • при желании подключить к USB-порту собрать преобразователь на FT232
  • так же подойдет большинство data-кабелей от старых телефонов.
Хорошая новость состоит в том, что выводы RXD и TXD у контроллера LCP1343 являются 5V tolerant, то есть вы можете спокойно подавать на эти выводы сигналы напряжением 5 вольт, и это не испортит контроллер. Так что можно применить преобразователь и на MAX232, но питание 5В надо будет брать где-нибудь с другого места.
Если у вас нет возможности подключить преобразователь интерфейса, то вам все равно следует выполнить данный урок.

Изучаем библиотеку

Код для работы с UART уже присутствует в библиотеке LPC13xx_Lib и расположен в файлах uart.c и uart.h. Давайте рассмотрим его упрощенную версию (это вам пригодится для понимания работы контроллера, для использования библиотеки вам этого знать не надо). Я исключил для статьи условную компиляцию, дабы не перегружать вас информацией. Будучи уверенными что, вы поняли данный материал, попробуйте самостоятельно разобраться в смысле исключенного кода.
Всё с чего-то начинается. Мы начнём с инициализации которой мы передаем один параметр – желаемая скорость работы UART:
void UARTInit(uint32_t baudrate)
{
  uint32_t Fdiv;		// Делитель частоты для получения требуемой скорости
  uint32_t regVal;	// Переменная для служебных нужд

  UARTCount = 0;		// Обнуляем счётчик принятых байт (см. ниже)
  
  NVIC_DisableIRQ(UART_IRQn);	// У контроллера прерываний запрещаем прерывания от UART что бы они не возникали, пока мы не выполним настройку

  LPC_IOCON->PIO1_6 &= ~0x07;	// настройка выводов UART
  LPC_IOCON->PIO1_6 |= 0x01;	// UART RXD	- вывод приёма данных
  LPC_IOCON->PIO1_7 &= ~0x07;	
  LPC_IOCON->PIO1_7 |= 0x01;	// UART TXD – вывод передачи данных

  LPC_SYSCON->SYSAHBCLKCTRL |= (1<<12);	// Разрешаем тактирование UART
  LPC_SYSCON->UARTCLKDIV = 0x1;	// Делитель тактовой частоты устанавливаем в 1

  LPC_UART->LCR =	  (3<<0)	// Формат пакета 8 бит данных (возможно 5, 6, 7, 8)
			| (0<<2)	// 1 стоп бит (возможно 1, 2)
			| (0<<3)	// без контроля чётности (возможно None, Even, Odd, One, Zero)
			| (1<<7)	// Разрешить доступ к регистрам коэффициента деления для скорости
;	// и так получили 0x83 (подробно по регистру таблица 195 UM10375)

  regVal = LPC_SYSCON->UARTCLKDIV;
  Fdiv = (((SystemFrequency/LPC_SYSCON->SYSAHBCLKDIV)/regVal)/16)/baudrate; // рассчитали коэффициент деления

  LPC_UART->DLM = Fdiv / 256;							
  LPC_UART->DLL = Fdiv % 256;
  LPC_UART->LCR = 0x03;		// То же что и выше, но запретили доступ к регистрам коэффициента деления для скорости
  LPC_UART->FCR =	  (1<<0)	// Разрешаем доступ к остальным полям в регистре
			| (1<<1)	// разрешение и очистка буфера приёма
			| (1<<2)	// разрешение и очистка буфера передачи
			| (0<<6)	// прерывание после приёма каждого байта 
			;	// и того 0x07 (подробно по регистру таблица 194 UM10375)

  regVal = LPC_UART->LSR;	// Читаем регистр статуса линии для его очистки

  // Что бы удостовериться в отсутствии мусора ждем очистки буфера передачи
  while (( LPC_UART->LSR & (LSR_THRE|LSR_TEMT)) != (LSR_THRE|LSR_TEMT) );
  while ( LPC_UART->LSR & LSR_RDR )
  {
	regVal = LPC_UART->RBR;	// и пока они есть считываем «в никуда» данные из буфера приёма
  }
 
  // Разрешаем прерывания от UART у контроллера прерываний
  NVIC_EnableIRQ(UART_IRQn);
  LPC_UART->IER = IER_RBR | IER_RLS;	// Разрешаем прерывания по приёму и по изменению состояния линии связи
  return;
}

Зачем эта морока с разрешением доступа к регистрам коэффициента деления для скорости передачи, спросите вы. Таблица 186 UM10375 служит ответом. Просто регистр DLM делит один адрес с регистром IER, а регистр DLL с регистрами RBR и THR (Чем-то похоже на PIC’и, может MicroChip подрабатывал у NXP?).
По расчету коэффициент деления существует раздел 11.6.15 в UM10375. На самом деле он посвящен регистру FDR, позволяющему получить дробные коэффициенты деления. Но в нем приведены формула расчета скорости и примеры расчета. Поскольку мы не меняли значение регистра FDR, мы воспользовались простой формулой: системная частота делится на делитель системной шины, затем на делитель тактовой частоты UART, затем на 16 (так зашито в железку) и на желаемую скорость.
UART, как и SPI, имеет собственные буферы приёма и передачи. Размеры их одинаковые и составляет 16 байт. Очистка их проводится «для надежности».
Далее следовало бы упомянуть об описания «глобальных» переменных
volatile uint32_t UARTStatus;		// Состояние UART контроллера
volatile uint8_t  UARTBuffer[BUFSIZE];	// Буфер для принятых данных
volatile uint32_t UARTCount = 0;		// Индекс следующей позиции для записи в кольцевой буфер

Здесь применяется свой кольцевой буфер приёма (не путать его с внутренним RX буфером приёма). Прочитать что это такое можно, например, здесь.
Поскольку разрешены прерывания от модуля UART нужна и функция обработки этих прерываний:
void UART_IRQHandler(void)
{
  uint8_t IIRValue, LSRValue;
  uint8_t Dummy = Dummy;

  IIRValue = LPC_UART->IIR;	// Считали источник прерывания 
    
  IIRValue >>= 1;			// Отбросили флаг прерывания
  IIRValue &= 0x07;		// Оставили только биты 1-3 исходного значения – идентефикатор прерывания
  if (IIRValue == IIR_RLS)	// Прерывание изменения статуса линии (Receive Line Status)
  {
    LSRValue = LPC_UART->LSR;	// Считали состояние линии (и сбросили прерывание)
    if (LSRValue & (LSR_OE | LSR_PE | LSR_FE | LSR_RXFE | LSR_BI))
    {
      // Если была ошибка или прерывание
      UARTStatus = LSRValue;	// Сохранили новое состояние линии
      Dummy = LPC_UART->RBR;	// Чтение регистра RX сбрасывает флаг прерывания
      return;	// Завершаем обработку
    }
    if (LSRValue & LSR_RDR)	// В RBR есть данные (Receive Data Ready)
    {
	// Если не было ошибок при прерывании от RLS, то приём нормально завершен, сохраняем принятые данные в свой буфер
	// чтение регистра RBR (чтение из буфера приёма) сбросит флаг прерывания
      UARTBuffer[UARTCount++] = LPC_UART->RBR;
      if (UARTCount == BUFSIZE)
      {
        UARTCount = 0;	// Если достигли конца буфера, перемещаемся в его начало
      }	
    }
  }
  else if (IIRValue == IIR_RDA)	// Прерывание по доступности данных для чтения (Receive Data Available)
  {
    UARTBuffer[UARTCount++] = LPC_UART->RBR;	// Читаем данные из буфера приёма в свой буфер
    if (UARTCount == BUFSIZE)
    {
      UARTCount = 0; 	// Если достигли конца буфера, перемещаемся в его начало
    }
  }
  return;
}

Почему в двух местах чтение принятых данных? Прерывание RDR (Receive Data Ready) возникает, когда в регистре RBR (Receiver Buffer Register), а значит и в буфере приёма есть не считанные данные. Прерывание RDA (Receive Data Available) возникает по достижению в буфере приёма установленной границы или по доступности данных. В нашем случае, как можете наблюдать, срабатывает RDA.
И нам осталось разобраться только с передачей данных. Функция принимает в качестве параметров указатель на данные для передачи и количество байт, которые надо передать:
void UARTSend(uint8_t *BufferPtr, uint32_t Length)
{
  
  while ( Length != 0 )	// пока есть данные для передачи
  {
// Ждем освобождения места в буфере передачи
	while ( !(LPC_UART->LSR & LSR_THRE) );
	LPC_UART->THR = *BufferPtr;	// Помещаем очередной байт в буфер передачи
      BufferPtr++;	// переходим к следующему байту
      Length--;		// Уменьшаем счетчик оставшихся для передачи данных
  }
  return;
}

Флаг THRE (Transmitter Holding Register Empty) ни что иное, как признак возможности записи в регистр THR (Transmitter Holding Register) – регистр записи в буфер передачи.
Вот собственно и всё. Как видите, для чтения принятых данных нам предлагается некий кольцевой буфер. Но зато настройка порта и передача данных просты в применении.

Дорабатываем библиотеку

Нам надо сделать библиотеку чуть более удобной для использования. В частности добавить функцию чтения данных из кольцевого буфера приема:
uint32_t LastRead = 0;	// Последняя позиция чтения из буффера

uint32_t UARTRecive(uint8_t *BufferPtr, uint32_t Size)
{
	int count = 0;
	while(count < Size) {			// Цикл пока считаны не все данные
		if(LastRead == UARTCount)	// В буффере больше нет данных - завершаем цикл
			break;
		BufferPtr[count++] = UARTBuffer[LastRead++];	// читаем
		if(LastRead >= BUFSIZE)		// При выходе указателя за буффер перемещаемся в начало
			LastRead = 0;
	}
	return count;	// Вернули количество считанных данных
}

Приведённая функция считает в буфер по адресу переданному в параметре BufferPtr все имеющиеся в буфере приема данные, но не более количества, указанного в параметре Size. Функция возвращает количество сохраненных байт. Если в буфере приёма нет данных (ничего не было принято), то функция просто вернёт нуль.
Так же напишем функцию получения одного символа:
char UARTGetChar(void)
{
	char с;
	while(UARTRecive((void*)&с, 1) != 1);	// Ждем, пока байт считается
	return с;
}

Она сводится к вызову предыдущей функции, но в отличие от неё будет блокировать выполнение программы, пока символ не будет считан.
Так же добавлена функция UARTPutChar, для передачи по UART одного символа, она элементарна и сводится к вызову уже имеющейся функции UARTSend.

Пишем код

Передавать по COM-порту в компьютер данные с АЦП я не стану, это не интересно.
Итак, функция main кроме инициализации портов ввода-вывода обзавелась следующим функционалом:
UARTInit(9600);			// Настройка UART на скорость 9600
UARTPutChar('>');
while(1) {
	c = UARTGetChar();
	switch(C) {
	case '?':	// Запросили состояние кнопки
		if(GPIOGetValue(BUTTON_PORT, BUTTON_BIT) == BUTTON_DOWN) {
			UARTSend((void*)"down\n", 5);
		} else {
			UARTSend((void*)"up\n", 3);
		}
		break;
	case '+':	// Запросили включение светодиода
		GPIOSetValue(LED_PORT, LED_BIT, LED_ON);
		break;
	case '-':	// Запросили выключение светодиода
		GPIOSetValue(LED_PORT, LED_BIT, LED_OFF);
		break;
	default:	// Нестандартный запрос
		if(c >= '0' && c < '9') {	// Запросили следующую цифру
			UARTPutChar(c + 1);		// Генерируем и отправляем следующую цифру
		}
	}
}

Сразу после настройки UART мы передаем по нему символ ’>’ с простой целью – что-то же надо передать. Далее уходим в бесконечный цикл обработки принятых по UART данных:
  • приняв символ вопроса – проверяем состояние кнопки и передаем по UART строку “down” с переводом строки если кнопка нажата или “up” с переводом строки если нет. Приведение типа (void*) нужно только что бы компилятор ни выдавал предупреждение о несоответствии типов;
  • приняв символ плюс – зажигаем светодиод;
  • приняв символ минус – гасим светодиод;
  • приняв цифру от 0 до 8 – передаем по UART цифру на единицу больше.
Да, я долго думал над составлением самого «бесполезного и ненужного» примера.

Запускаем

Обладатели преобразователей интерфейсов для подключения к компьютеру могут запустить на выполнение полученные 4325 байт «бесполезного» кода. На компьютере можно воспользоваться программой Terminal v1.9b, стандартной для Windows программой Hyper Terminal или любой другой пригодной. Настраиваете COM-порт, к которому подключен контроллер, на скорость 9600 бод, с 8 битами данных, 1 стоп битом, без контроля четности, без аппаратного контроля передачи.
Выполнив обмен данными, вы увидите, что кроме демонстрации наличия обмена, ничего больше и не получите от данного примера.
Тех, у кого указанных средств не оказалось, я приглашаю в следующий урок (в процессе подготовки).

Вместо завершения

В этой статье UART оказался практически неописан. В контроллере LPC1343 воплощено много «аппетитных» вещей. Так совершенно нерассмотренным оказалось прерывание CTI (Character Time-out indication) которое полезно при обмене пакетами постоянного размера. И сама возможность приёма пакетов осталась за кадром. Не рассмотрено прерывание THRE (Transmit Holding Register Empty) полезное для фоновой передачи длинных последовательностей. Не рассматривался регистр FDR позволяющий добиться ошибки установки скорости передачи не более 1.1% (как заявлено), что позволяет использовать кварцевые резонаторы не кратные RS-232 скорости. Совершенно не рассмотрены возможности автоматического определения скорости передачи, аппаратное управление передачей, работа в режиме RS-485.
В общем, можно сказать смело, ничего мы не рассмотрели. UART обладает на столько широкими возможностями, что только по нему, следовало бы писать курс. Однако самый популярный и простой способ его применения мы всё же рассмотрели.
  • +3
  • 21 сентября 2011, 13:05
  • angel5a
  • 1
Файлы в топике: blinky_uart.zip

Комментарии (19)

RSS свернуть / развернуть
А printf() к UART не пробовал прикрутить?
0
  • avatar
  • Zov
  • 21 сентября 2011, 14:45
Пока ещё нет.
0
Здравствуйте. При выполнении данного урока возникла некоторая проблема: приведенный код весь нормально работает, но при некоторой модификации, приведенной ниже что то не получается.
case '+': // Запросили включение светодиода
GPIOSetValue(LED_PORT, LED_BIT, LED_ON);
for(i = 0; i < 5; i++)
{
UARTSend((void*)«up\n», 3);
}
break;
Поидее, как я понимаю, если приемник получает символ '+', он должен 5 раз отправить строку up, но на компьютере я получаю только 1 раз up. Если вместо for написать while(1), тогда будет все нормально бесконечно отправляться строка.
Не знаете как такое может быть?
0
Просмотрел код библиотеки, явных ошибок не заметил. Переполнения быть не должно. Проверить не смогу, системы нет.
Попробуйте разные режимы библиотеки, комментированием определения TX_INTERRUPT (не забывая пересобрать библиотеку при этом).
0
все норм с вашим кодом, проблема решилась. Но возникла другая:
uint8_t wr[2112];
while(1) {
c = UARTGetChar();
switch(C) {
case '?':
uint16_t tmp = UARTRecive(wr, 2112);
UARTSend((uint8_t*)&wr, tmp);
break;
}

Cтранное поведение представленного кода: если ожидать прихода символа '?' то ничего не вернется на компьютер. Если строку GetChar заменить на c = '?', то на компьютер возвращается отправленный массив. Не знаете с чем это связано?
И второй вопрос если отправлять например 2200 байтовый массив, то он такой и возвращается, а не 2112 как указано в UARTRecive. Это биг в отправлении или уже что то с библиотекой UART микроконтроллера?
0
Если строку GetChar заменить на c = '?', то на компьютер возвращается отправленный массив.
При замене чтения байта принятых данных на с = '?', изменяется алгоритм:
программа всегда попадает в case '?' и приняв 2112 байт выдаёт их же в уарт.
если отправлять например 2200 байтовый массив, то он такой и возвращается, а не 2112 как указано в UARTRecive.
Это странно. Может передаётся не 2200 байт, а больше?

P.S. Непонятно зачем функция UARTRecive возвращает число принятых байт. Ведь принять больше или меньше она не может, только Size байт.
0
C символом '?' объяснил artjom.

Логика моей функции UARTRecive — считать всё что есть на данный момент, но не более указанного размера буффера. Т.е. если на момент чтения доступен только 1 символ, то функция и вернет 1 символ. Что и происходит у вас.
Если требуется читать ровно столько байт сколько указано, то либо доработайте функцию UARTRecive
if(LastRead == UARTCount)
			break;
заменить на
while(LastRead == UARTCount);

либо используйте посимвольное чтение с помощью UARTGetChar взамен UARTRecive.
0
P.S. Был неправ — UARTRecive может принять данных меньше чем задано переменной Size, в том случае если принято меньше чем Size. Так что нет ничего удивительного что отправляется 2200 байт.
0
всем спасибо, да я и решил использовать посимвольное чтение.
Вопрос такой, когда я получил данные и считал их getcharom буфер очищается или его нужно вручную очищать что бы не было мусора?
0
Вы про какой буфер? UARTBuffer?
Не очищается, но это и не нужно — признаком отсутствия данных является равенство LastRead и UARTCount. Если они равны — значит данных нет, и мусора тоже нет.
+1
отлично, спасибо
0
Почему-то нигде не нашел код UARTPutChar(). Сваял свой, может кому пригодиться…

void UARTPutChar(uint8_t c)
{
UARTSend(&c, 1);
}
0
Почему-то нигде не нашел код UARTPutChar()
Видимо потому что УАРТов в МК нынче много и писателям библиотек не угадать куда посылать байт.
void UARTPutChar(uint8_t c)
{
UARTSend(&c, 1);
}
Мощно…
А UARTSend какой тогда нужен?
0
Там есть уже. Если что, я использую LPC11xx_cmsis2_Lib и соответствующий камень.
0
А надо на другой UART перенаправить вывод? Как тогда делать?
0
Ответ же очевиден: Пишите свою библиотеку.

При создании данной библиотеки упор ставился на простоту и понятность, нежели на полноту и универсальность.
Да и сколько универсальных средств не делай, все равно кто-то будет затачивать под себя, а кто то будет писать просто свое. Комикс по этому поводу был.
0
Напомни, который. Случаем не про «15 конкурирующих стандартов»?
0
Он самый
0
Она в прикрепленных исходниках есть. Но теперь будет и тут :)
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.