Привет всем пользователям Geektimes! Как-то раз по долгу службы мне потребовалось реализовать программный UART на популярном микроконтроллере ATtiny13. Загуглив, я нашел большое количество статей на эту тему, многие из них выложены здесь:


А есть и на других ресурсах:


Последняя реализация, в общем-то, удовлетворяет моим потребностям (полнодуплексная связь). Но, во-первых, код написан в CodeVision AVR, который я не использую по сугубо религиозным соображениям, во-вторых, слабо комментированные вставки на ассемблере тем более отбивают желание разбираться в коде. Я же поставил себе целью написать на чистом C понятную пользователям библиотеку полнодуплексного UART-а. А заодно написать про это статью, потому что задачка достаточно интересная ввиду очень ограниченного объема памяти и ресурсов контроллера (всего один 8-битный таймер). Для новичков в программировании микроконтроллеров это будет неплохой учебный материал, т.к. я сам в процессе написания библиотеки, практически с нуля освоил архитектуру AVR.

Кому интересно — добро пожаловать под кат, текста будет много, много исходников с комментариями в коде.

Итак, начнем. Заголовочный файл uart13.h я просто выложу в том виде, в котором он есть с комментариями в коде, там все просто.

Заголовочный файл uart13.h
/* Библиотека программной реализации UART для микроконтроллеров ATtiny */

#ifndef _UART13_H_
#define _UART13_H_ 1

#include <avr/io.h>
#include <avr/interrupt.h>

/*
*	Ниже настраиваются порты и пины портов которые будут использоваться
*	как передатчик и приемник UART.
*/

#define TXPORT PORTB		// Имя порта для передачи
#define RXPORT PINB		// Имя порта на прием
#define TXDDR DDRB		// Регистр направления порта на передачу
#define RXDDR DDRB		// Регистр направления порта на прием
#define TXD 0			// Номер бита порта для использования на передачу
#define RXD 1			// Номер бита порта для использования на прием

/*
*	Ниже задаются константы, определяющие скорость передачи данных (бодрейт)
*	расчет BAUD_DIV осуществляется следующим образом:
*	BAUD_DIV = (CPU_CLOCK / DIV) / BAUD_RATE
*	где CPU_CLOCK - тактовая частота контроллера, BAUD_RATE - желаемая скорость UART,
*	а DIV - значение делителя частоты таймера, задающееся регистром TCCR0B.
*	Например, тактовая частота 9.6 МГц, делитель на 8, скорость порта 9600 бод:
*	BAUD_DIV = (9 600 000 / 8) / 9600 = 125 (0x7D).
*/

//#define T_DIV		0x01	// DIV = 1
#define T_DIV		0x02	// DIV = 8
//#define T_DIV		0x03	// DIV = 64
#define BAUD_DIV	0x7D	// Скорость = 9600 бод

/*
*	Ниже идут объявления глобальных переменных и функций для работы UART
*/

volatile uint16_t txbyte;
volatile uint8_t rxbyte;
volatile uint8_t txbitcount;
volatile uint8_t rxbitcount;

void uart_init();
void uart_send(uint8_t tb);
int16_t uart_recieve(uint8_t* rb);

#endif /* _UART13_H_ */



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

Прерывание TIM0_COMPA
ISR(TIM0_COMPA_vect)
{
	TXPORT = (TXPORT & ~(1 << TXD)) | ((txbyte & 0x01) << TXD); // Выставляем в бит TXD младший бит txbyte
	txbyte = (txbyte >> 0x01) + 0x8000;	// Двигаем txbyte вправо на 1 и пишем 1 в старший разряд (0x8000)
	if(txbitcount > 0)			// Если идет передача (счетик бит больше нуля),
	{
		txbitcount--;			// то уменьшаем его на единицу.
	}
}


Здесь мы видим код обработчика прерывания таймера по сравнению с регистром OCR0A. Оно работает постоянно и случается каждый раз, когда таймер достигает значения, записанного в регистре OCR0A. Когда это происходит, значение таймера в регистре TCNT0 автоматически обнуляется (режим таймера CTC, задается в регистре TCCR0A). Это прерывание используется для отправки данных по UART. Переменная txbyte используется как сдвиговый регистр: каждый раз, когда происходит прерывание, младший разряд переменной txbyte выставляется на вывод TXD микросхемы, после чего происходит сдвиг содержимого переменной вправо, а в освободившийся старший разряд записывается единица.

Пока данных для передачи нет, в переменной хранится число 0xFFFF и, тем самым, на выходе TXD непрерывно поддерживается высокий логический уровень. Когда мы хотим передать данные, мы должны записать в счетчик бит число бит для передачи: 1 стартовый, 8 бит данных и 1 стоповый, итого 10 (0x0A), и записать в txbyte данные для передачи вместе со стартовым битом. После этого они немедленно начнут передаваться. Формированием посылки занимается функция void uart_send(uint8_t tb).

Прерывание TIM0_COMPB
ISR(TIM0_COMPB_vect)
{
	if(RXPORT & (1 << RXD))			// Проверяем в каком состоянии вход RXD
		rxbyte |= 0x80;			// Если в 1, то пишем 1 в старший разряд rxbyte
	
	if(--rxbitcount == 0)			// Уменьшаем на 1 счетчик бит и проверяем не стал ли он нулем
	{
		TIMSK0 &= ~(1 << OCIE0B);	// Если да, запрещаем прерывание TIM0_COMPB
		TIFR0 |= (1 << OCF0B);		// Очищаем флаг прерывания TIM0_COMPB
		GIFR |= (1 << INTF0);		// Очищаем флаг прерывания по INT0
		GIMSK |= (1 << INT0);		// Разрешаем прерывание INT0
	}
	else
	{
		rxbyte >>= 0x01;		// Иначе сдвигаем rxbyte вправо на 1
	}
}


Здесь мы видим обработчик прерывания таймера по сравнению с регистром OCR0B. Оно работает аналогично прерыванию TIM0_COMPA, но, в отличие от него, при выполнении этого прерывания не происходит обнуления таймера TCNT0. Это прерывание разрешается только тогда, когда мы принимаем данные, в остальное время оно запрещено. Когда оно случается, мы проверяем логическое состояние входа RXD и, если оно в единице, то пишем единицу в старший разряд переменной приема rxbyte, затем мы уменьшаем на единицу счетчик принятых бит и, если он стал нулем, заканчиваем прием. Иначе сдвигаем вправо переменную rxbyte, чтобы подготовить ее к приему следующего бита.

Прерывание INT0
ISR(INT0_vect)
{
	rxbitcount = 0x09;			// 8 бит данных и 1 стартовый бит
	rxbyte = 0x00;				// Обнуляем содержимое rxbyte
	if(TCNT0 < (BAUD_DIV / 2))		// Если таймер не досчитал до середины текущего периода
	{
		OCR0B = TCNT0 + (BAUD_DIV / 2);	// То прерывание произойдет в текущем периоде спустя пол периода
	}
	else
	{
		OCR0B = TCNT0 - (BAUD_DIV / 2);	// Иначе прерывание произойдет уже в следующем периоде таймера
	}
	GIMSK &= ~(1 << INT0);			// Запрещаем прерывание по INT0
	TIFR0 |= (1 << OCF0A) | (1 << OCF0B);	// Очищаем флаги прерываний TIM0_COMPA (B)
	TIMSK0 |= (1 << OCIE0B);		// Разрешаем прерывание по OCR0B
}


Прерывание INT0. Срабатывает по заднему фронту импульса на входе INT0, используется для отслеживания начала приема информации. Выставляет счетчик бит равным 9, обнуляет содержимое переменной rxbyte. Задает значение для регистра OCR0B, определяющего периодичность срабатывания прерывания TIM0_COMPB, оно должно приходиться по времени на середину принимаемого бита. После чего прерывание TIM0_COMPB разрешается, а прерывание INT0 запрещается.

Далее идут пользовательские функции для работы с UART.

Функция uart_send
void uart_send(uint8_t tb)
{
	while(txbitcount);		// Ждем пока закончится передача предыдущего байта
	txbyte = (tb + 0xFF00) << 0x01; // Пишем в младшие разряды txbyte данные для передачи и сдвигаем влево на 1
	txbitcount = 0x0A;		// Задаем счетчик байт равным 10
}


Функция передачи байта по UART. Принимает в качестве аргумента байт для передачи, возвращаемого значения нет. Если в момент вызова функции идет передача байта, то она ждет пока передача закончится, после чего записывает в младшие 8 бит переменной txbyte байт для передачи, а старшие 8 бит остаются 0xFF, затем сдвигает переменную влево, создавая таким образом стартовый бит в младшем разряде. Задает счетчик бит равным 10.

Функция uart_recieve
int16_t uart_recieve(uint8_t* rb)
{
	if(rxbitcount < 0x09)		// Если счетчик бит на прием меньше 9
	{
		while(rxbitcount);	// Ждем пока завершится текущий прием
		*rb = rxbyte;		// Пишем по адресу указателя принятый байт
		rxbitcount = 0x09;	// Восстанавливаем значение счетчика бит
		return (*rb);		// Возвращаемся
	}
	else
	{
		return (-1);		// Иначе возвращаем -1 (принимать нечего)
	}
}


Функция приема байта по UART. Принимает в аргумент указатель на 8-битную переменную, где будет содержаться принятый байт. Возвращает принятый байт, если байт принят, а если принимать нечего, возвращает (-1). Если в момент вызова функции идет прием, функция будет ждать его завершения. Если функцию вызвать дважды, то первый раз она возвратит принятый байт, а во второй раз (-1).

Функция uart_init
void uart_init()
{
	txbyte = 0xFFFF;		// Значение буфера на передачу - все единицы
	rxbyte = 0x00;			// Значение буфера на прием - все нули
	txbitcount = 0x00;		// Значение счетчика преедаваемых бит - ноль (ничего пока не передаем)
	rxbitcount = 0x09;		// Значение счетчика бит на прием - 9 (ожидаем возможного приема)
	
	TXDDR |= (1 << TXD);		// Задаем направление порта на передачу как выход
	RXDDR &= ~(1 << RXD);		// Задаем направление порта на прием как вход
	TXPORT |= (1 << TXD);		// Пишем единицу в выход TXD
	RXPORT |= (1 << RXD);		// Подтягиваем к единице вход RXD
	OCR0A = BAUD_DIV;		// Задаем значение регистра OCR0A в соответствии с бодрейтом
	TIMSK0 |= (1 << OCIE0A);	// Разрешаем прерывание TIM0_COMPA
	TCCR0A |= (1 << WGM01);		// Режим таймера CTC (очистка TCNT0 по достижению OCR0A)
	TCCR0B |= T_DIV;		// Задаем скорость счета таймера в соответствии с делителем
	MCUCR |= (1 << ISC01);		// Задаем прерывание INT0 по заднему фронту импульса
	GIMSK |= (1 << INT0);		// Разрешаем прерывание INT0
	sei();				// Разрешаем прерывания глобально
}


Функция инициализации UART. Аргументов нет, возвращаемого значения нет. Инициализирует глобальные переменные и регистры микроконтроллера. Из комментариев в коде должно быть все понятно.

Итак, пришло время написать какой нибудь простой main() с использованием нашей библиотеки и посмотреть что получилось в плане объема кода и проверить работоспособность.

main.c
#include "uart13.h"

int main(void)
{
	uint8_t b = 0;
	uart_init();
	while (1) 
	{
		if(uart_recieve(&b) >= 0)	// Если ничего не приняли, ничего и не передаем
			uart_send(b);		// А если приняли, передаем принятое
	}
	return (0);
}


Компилируем:
Program Memory Usage: 482 bytes 47,1 % Full
Data Memory Usage: 5 bytes 7,8 % Full

Неплохо, у нас в запасе еще больше половины памяти микроконтроллера!
Проверяем в Proteus:

Симуляция ОК!


Итог: реализация получилась вполне годная к использованию, данные передаются и принимаются независимо, библиотека delay.h вообще не использована, а в запасе осталось больше половины памяти микроконтроллера.

Прилагаю исходники библиотеки, они компилируются в avr-gcc: Исходники на GitHub

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


  1. Alexeyslav
    09.11.2015 11:25

    Применение прерывания INT0/INT1 сильно ограничивают положение вывода под приём…
    Да и сама функция UART без внешнего тактового сигнала сомнительна. Внутренний генератор обладает очень низкой точностью и даже с калибровкой уплывает от температуры и напряжения питания. Только в TYNY45/85 есть температурная стабилизация встроенного генератора.
    Как-то пробовал UART аппаратный но с внутренним генератором на ATMEGA48, точности не хватает. Даже после калибровки во время работы частота уходит и на другом конце начинают лезть ошибки приёма, и не на всех переходниках USB-UART есть возможность подбирать кастомную скорость передачи с необходимым шагом.


    1. wiseholder
      09.11.2015 11:51

      Да, действительно, я совершенно упустил из виду то, что INT0 жестко привязывает пин PB1 к использованию на вход. Исправлю на использование прерывания PCINT. Насчет нестабильности — лично я не проверял в температуре, но на моем тестовом образце я поставил значение OCR0A равным 0х70 — вроде бы все работает как положено, ошибок нет, осциллографом проверял длительность бита — все в порядке. Ну, а выбирать в общем-то и не приходится, статья то про ATtiny13, и если на ней необходимо сделать UART, то других вариантов нет.


      1. Alexeyslav
        09.11.2015 13:48

        PCINT не различает фронт/спад… там только одно понятие: изменение состояния.

        ATTINY13 может тактироваться внешним сигналом.

        По даташиту, зависимости частоты внутреннего генератора от напряжения и температуры просто жуткие. Скорей всего хорошо отрабатывает именно другая сторона подстраиваясь под скорость передачи вашего контроллера.

        Погрешность скорости в 2% и последний бит уже находится не в своём фрейме, а подстройка внутреннего генератора если не ошибаюсь происходит шагами по 0.5-1% запас очень небольшой, и надо тщательно калибровать чтобы не выйти за пределы допустимого отклонения.


    1. DIHALT
      10.11.2015 07:21

      Сколько использовал внутренний RC генератор никогда с уартом не было проблем вплоть до 38400. Вы часом не на али контроллеры покупаете?


      1. Alexeyslav
        10.11.2015 12:46

        Может не повезло и калибровка оказалась как раз на грани сбоя. На 38400 иногда получал 0xFE вместо 0xFF, и это иногда было слишком частым — навскидку с вероятностью около 1/20. Устал с этим бороться и бросил эту затею.


  1. vladimir_open-dev
    09.11.2015 16:08
    -1

    Предвижу гнев, но все же чем ATtiny13 лучше STM32F030F4P6 за 0,5 доллара в китае или 55р в чипдипе(от 25штук)?


    1. Alexeyslav
      09.11.2015 16:44
      +1

      Не всё измеряется деньгами простоту, например, ни за какие деньги не купишь.
      Более того, есть еще ATTINY10 в SOT-23 корпусе, вообще 6-ногий товарищ.

      Цена начинает играть роль только при крупносерийном производстве а в остальном — доступность и качество документации.

      Под STM32 нужно еще учится, искать документацию, разобраться в бардаке множества сред разработки — чтобы разобраться в STM-ках нужна очень сильная мотивация и направляющий импульс.


    1. wiseholder
      09.11.2015 18:27
      +1

      Просто потому что 8-ногий DIP значительно проще использовать чем 20-ногий TSSOP или еще хуже LQFP. Да и глупо как-то выглядит использование STM32 для принятия по UART пары байт и управления нагревательным элементом. (Для этого я использовал эту ATtiny — нагрев и датчик температуры с передачей данных по UART)


  1. aloika
    09.11.2015 22:25

    Посмотрите ATtiny441/841. Два настоящих UART, термостабилизация внутреннего генератора и всего 14 ног, правда в SOIC-корпусе. Эволюция ATtiny 24/44/84.


    1. Alexeyslav
      10.11.2015 14:06

      Стоимость? оно то конечно не так важно, но обидно использовать дорогой контроллер и сознательно не пользоваться избыточной периферией когда это можно сделать на более дешёвом.


      1. aloika
        10.11.2015 15:24

        Если не в ущерб качеству, то конечно.

        Просто если в дешевом частота внутреннего генератора уплывает, а кварц или внешнее тактирование делать не хочется, или еще какие ограничения выявляются, то вот можно взять ATTiny441 с аппаратным уартом (даже с двумя).


        1. Alexeyslav
          10.11.2015 16:42

          В одном магазине посмотрел — в 2 раза дороже стоят чем ATTiny44.
          UART конечно хорошо, но в таких случаях ИМХО имеет смысл использовать SPI не зависящий от тактовой частоты.


  1. KonstantinSoloviov
    10.11.2015 18:02

    Если передачу оборвать, то вот здесь:

    int16_t uart_recieve(uint8_t* rb)
    ...
    while(rxbitcount);
    ...
    

    мы и останемся, навсегда.
    Может лучше, что бы функция приема возвращала байт если он принят или ничего если нет?


    1. KonstantinSoloviov
      11.11.2015 10:51

      Был не прав, поторопился с выводами, rxbitcount обнулится таймером.

      Мне не нравится, что uart_recieve() вынуждает задерживаться в ней если начат прием байта. Лучше иметь буфер хотя бы на один уже принятый байт, а принимаемый собирать отдельно (вроде в аппаратном UARTе так и сделано).

      И еще, если не «повезет»:

      int16_t uart_recieve(uint8_t* rb)
      {
      	if(rxbitcount < 0x09)		// Если счетчик бит на прием меньше 9
      	{
      		while(rxbitcount);	// Ждем пока завершится текущий прием
      // !!!!!!!!!!!!!!!!!!!!!
      // если в этот момент случится прерывание INT0, то в rxbyte обнулится
      // !!!!!!!!!!!!!!!!!!!!!
      		*rb = rxbyte;		// Пишем по адресу указателя принятый байт
      		rxbitcount = 0x09;	// Восстанавливаем значение счетчика бит
      		return (*rb);		// Возвращаемся
      	}
      	else
      	{
      		return (-1);		// Иначе возвращаем -1 (принимать нечего)
      	}
      }
      

      можно получить ноль вместо реально переданного байта


      1. wiseholder
        12.11.2015 21:35

        Ну, я сознательно не стал добавлять буфер, это увеличило бы объем кода.
        По поводу «повезет» — это вряд ли, если rxbitcount стала нулем, значит мы только что получили стоповый бит и у нас уж точно есть время, чтобы выполнить одну операцию.