Привет всем пользователям Geektimes! Как-то раз по долгу службы мне потребовалось реализовать программный UART на популярном микроконтроллере ATtiny13. Загуглив, я нашел большое количество статей на эту тему, многие из них выложены здесь:
А есть и на других ресурсах:
Последняя реализация, в общем-то, удовлетворяет моим потребностям (полнодуплексная связь). Но, во-первых, код написан в CodeVision AVR, который я не использую по сугубо религиозным соображениям, во-вторых, слабо комментированные вставки на ассемблере тем более отбивают желание разбираться в коде. Я же поставил себе целью написать на чистом C понятную пользователям библиотеку полнодуплексного UART-а. А заодно написать про это статью, потому что задачка достаточно интересная ввиду очень ограниченного объема памяти и ресурсов контроллера (всего один 8-битный таймер). Для новичков в программировании микроконтроллеров это будет неплохой учебный материал, т.к. я сам в процессе написания библиотеки, практически с нуля освоил архитектуру AVR.
Кому интересно — добро пожаловать под кат, текста будет много, много исходников с комментариями в коде.
Итак, начнем. Заголовочный файл uart13.h я просто выложу в том виде, в котором он есть с комментариями в коде, там все просто.
А вот описание кода реализации библиотеки я разобью на части, чтобы не превратить статью в один огромный спойлер с кодом.
Здесь мы видим код обработчика прерывания таймера по сравнению с регистром OCR0A. Оно работает постоянно и случается каждый раз, когда таймер достигает значения, записанного в регистре OCR0A. Когда это происходит, значение таймера в регистре TCNT0 автоматически обнуляется (режим таймера CTC, задается в регистре TCCR0A). Это прерывание используется для отправки данных по UART. Переменная txbyte используется как сдвиговый регистр: каждый раз, когда происходит прерывание, младший разряд переменной txbyte выставляется на вывод TXD микросхемы, после чего происходит сдвиг содержимого переменной вправо, а в освободившийся старший разряд записывается единица.
Пока данных для передачи нет, в переменной хранится число 0xFFFF и, тем самым, на выходе TXD непрерывно поддерживается высокий логический уровень. Когда мы хотим передать данные, мы должны записать в счетчик бит число бит для передачи: 1 стартовый, 8 бит данных и 1 стоповый, итого 10 (0x0A), и записать в txbyte данные для передачи вместе со стартовым битом. После этого они немедленно начнут передаваться. Формированием посылки занимается функция void uart_send(uint8_t tb).
Здесь мы видим обработчик прерывания таймера по сравнению с регистром OCR0B. Оно работает аналогично прерыванию TIM0_COMPA, но, в отличие от него, при выполнении этого прерывания не происходит обнуления таймера TCNT0. Это прерывание разрешается только тогда, когда мы принимаем данные, в остальное время оно запрещено. Когда оно случается, мы проверяем логическое состояние входа RXD и, если оно в единице, то пишем единицу в старший разряд переменной приема rxbyte, затем мы уменьшаем на единицу счетчик принятых бит и, если он стал нулем, заканчиваем прием. Иначе сдвигаем вправо переменную rxbyte, чтобы подготовить ее к приему следующего бита.
Прерывание INT0. Срабатывает по заднему фронту импульса на входе INT0, используется для отслеживания начала приема информации. Выставляет счетчи�� бит равным 9, обнуляет содержимое переменной rxbyte. Задает значение для регистра OCR0B, определяющего периодичность срабатывания прерывания TIM0_COMPB, оно должно приходиться по времени на середину принимаемого бита. После чего прерывание TIM0_COMPB разрешается, а прерывание INT0 запрещается.
Далее идут пользовательские функции для работы с UART.
Функция передачи байта по UART. Принимает в качестве аргумента байт для передачи, возвращаемого значения нет. Если в момент вызова функции идет передача байта, то она ждет пока передача закончится, после чего записывает в младшие 8 бит переменной txbyte байт для передачи, а старшие 8 бит остаются 0xFF, затем сдвигает переменную влево, создавая таким образом стартовый бит в младшем разряде. Задает счетчик бит равным 10.
Функция приема байта по UART. Принимает в аргумент указатель на 8-битную переменную, где будет содержаться принятый байт. Возвращает принятый байт, если байт принят, а если принимать нечего, возвращает (-1). Если в момент вызова функции идет прием, функция будет ждать его завершения. Если функцию вызвать дважды, то первый раз она возвратит принятый байт, а во второй раз (-1).
Функция инициализации UART. Аргументов нет, возвращаемого значения нет. Инициализирует глобальные переменные и регистры микроконтроллера. Из комментариев в коде должно быть все понятно.
Итак, пришло время написать какой нибудь простой main() с использованием нашей библиотеки и посмотреть что получилось в плане объема кода и проверить работоспособность.
Компилируем:
Неплохо, у нас в запасе еще больше половины памяти микроконтроллера!
Проверяем в Proteus:
Итог: реализация получилась вполне годная к использованию, данные передаются и принимаются независимо, библиотека delay.h вообще не использована, а в запасе осталось больше половины памяти микроконтроллера.
Прилагаю исходники библиотеки, они компилируются в avr-gcc: Исходники на GitHub
А есть и на других ресурсах:
Последняя реализация, в общем-то, удовлетворяет моим потребностям (полнодуплексная связь). Но, во-первых, код написан в 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