АППАРАТНЫЙ UART В MICROCHIP STUDIO

 

 Для проверки программы потребуется UART-монитор и USB-TTL конвертр:

 

USB TTL

 

📘 Общие сведения об UART

UART (Universal Asynchronous Receiver/Transmitter) — это асинхронный протокол передачи данных, который использует две линии:

  • TX (передача),
  • RX (приём).

⚙️ Состояние линии и уровни сигнала

  • Базовое состояние (idle) — это логическая 1.
  • На 5 В логическая 1 бывает только в системах с уровнем TTL (обычно 5 В или 3.3 В).
  • Для RS-232, например, логическая 1 — это -12 В, а логический 0 — +12 В. Поэтому уровень зависит от стандарта физического уровня.

Если вы работаете с TTL UART, то:

  • Лог. 1 — это 5 В или 3.3 В (зависит от питания микросхемы).
  • Лог. 0 — это 0 В.

🧱 Структура кадра UART

Обычно:

| Idle | Start bit | Data bits (LSB first) | Optional Parity | Stop bit(s) |

Пример (на 8N1: 8 бит данных, No parity, 1 стоп-бит):

  1. Idle — линия на лог.1 (5 В или 3.3 В).
  2. Start bit — лог.0 (переход в 0 В).
  3. 8 бит данных — передаются от младшего к старшему (LSB first).
  4. Стоп-бит — лог.1 (возврат к 5 В или 3.3 В).
  5. Idle — продолжается, пока нет новых данных.

📈 Пример на 8N1

Допустим, мы передаём байт 0b01010101 (0x55):

Биты: 1 0 1 0 1 0 1 0 (LSB first → передаётся: 1,0,1,0,1,0,1,0)

Последовательность по времени будет:

Idle (1) → Start (0) → 1 0 1 0 1 0 1 0 → Stop (1)

🛠️ Частые параметры UART:

  • Скорость (baud rate): например, 9600, 115200 и т.п.
  • Бит данных: 7 или 8 (реже 5, 6, 9)
  • Parity: None, Even, Odd
  • Stop bits: 1 или 2

✅ Подытожим

  • Линия в покое — лог.1 (обычно 5 В или 3.3 В).
  • Старт-бит — лог.0.
  • Затем бит(ы) данных, начиная с младшего.
  • Затем стоп-бит(ы)лог.1.

🚀А теперь код. Итак, поехали!

Начнем с простенькой реализации базового функционала. Тут нет ничего лишнего. Просто связь по двум проводам... В данном случае скорость мегабит, но можно поменять BAUD на любой удобный и позьзоваться наздоровье в своих проектах.

#define F_CPU 16000000UL // Определяем частоту процессора как 16 МГц
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
 
// Настройки UART
#define BAUD 1000000
#define MYUBRR F_CPU/16/BAUD-1
 
// Прототипы функций
void UART_init(unsigned int ubrr);
void UART_transmit(unsigned char data);
void UART_putstring(const char *s);
void setup(void);
void loop(void);
 
// Функция для инициализации UART
void UART_init(unsigned int ubrr) {
    // Устанавливаем скорость передачи
    UBRR0H = (unsigned char)(ubrr >> 8);
    UBRR0L = (unsigned char)ubrr;
    // Разрешаем передачу и прием
    UCSR0B = (1 << TXEN0) | (1 << RXEN0);
    // Устанавливаем формат кадра: 8 бит данных, 1 стоп-бит
    UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);
}
 
// Функция для передачи одного байта данных
void UART_transmit(unsigned char data) {
    // Ожидаем, пока освободится буфер передачи
    while (!(UCSR0A & (1 << UDRE0)));
    // Помещаем данные в буфер
    UDR0 = data;
}
 
// Функция для передачи строки
void UART_putstring(const char *s) {
    while (*s) {
        UART_transmit(*s++);
    }
}
 
// Функция для начальных настроек
void setup(void) {
    UART_init(MYUBRR); // Инициализируем UART
    // Настройка PD2 как вход для кнопки
    DDRD &= ~(1 << DDD2);    // PD2 как вход
    PORTD |= (1 << PORTD2);  // Подтяжка вверх для кнопки
    // Настройка внешнего прерывания
    EIMSK |= (1 << INT0);        // Разрешаем прерывание INT0
    EICRA |= (1 << ISC01);       // Прерывание по спаду сигнала на PD2
    sei();                       // Разрешаем глобальные прерывания
}
 
// Основной цикл
void loop(void) {
    // Основной цикл программы
    while (1) {
        UART_putstring("Hello World!\n"); // Отправляем "Hello World" по UART
        _delay_ms(10); // Задержка 10 миллисекунд
    }
}
 
// Основная функция
int main(void) {
    setup(); // Вызываем функцию для начальных настроек
    loop();  // Запускаем основной цикл
    return 0;
}
 
// Обработчик прерывания INT0
ISR(INT0_vect) {
    UART_putstring("Pulse!\n"); // Отправляем "Pulse" по UART при нажатии кнопки
}

Далее рассмотрим пару вариантов с механизмом "flow control", для обеспечения точности передаяи данных. По сути, речь идет о механизме контроля заполнения буфера.

№1: RTS/CTS

В данной реализации UART работает и без настройки механизма RTS/CTS на стороне клиента, но при должной настройке он должно давать приемущество в точности обмена данными. Вся хитрость упирается в задействовании двух дополнительных пинов, предназначенных для контроля отправки и приема. Здесь у нас довольно быстро сыпется "Hello World!" и по нажатию кнопки PD2 вылезает "Pulse!". Скорость специально сделана такой, чтобы проверить, нет ли там серева из всяких кубиков или иных ошибок, а еще реализован функционал "flow control" типа RTS/CTS. Хуй знает, насколько он эффективен, но, судя по всему, передача данных происходит без ошибок. Бывают ошибки лишь по нажатию кнопки и похоже, что они происходят в связи с прерыванием, то есть, там образуется мешанина из букв. Однако сами буквы приходят все. Что, собственно, и требуется. Вообще, конечно, можно было бы использовать cli(); sei();, чтобы отключить прерывания на время отправки данных из loop(), но это уже не моя забота - для примера сойдет и так.

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

#define F_CPU 16000000UL // Определяем частоту процессора как 16 МГц
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
// Настройки UART
#define BAUD 1000000 //скорость - мегабит! 
#define MYUBRR F_CPU/16/BAUD-1
// Прототипы функций
void UART_init(unsigned int ubrr);
void UART_transmit(unsigned char data);
void UART_putstring(const char *s);
void settings(void);
void loop(void);
char* utoa(unsigned int value, char* buffer, int base);
// Функция для преобразования целого числа в строку (itoa)
char* itoa(int value, char* buffer, int base) {
    if (base < 2 || base > 36) {
        *buffer = '\0';
        return buffer;
    }
    char* ptr = buffer, *ptr1 = buffer, tmp_char;
    int tmp_value;
    if (value < 0 && base == 10) {
        value = -value;
        *ptr++ = '-';
    }
    tmp_value = value;
    do {
        int remainder = tmp_value % base;
        *ptr++ = (remainder < 10) ? (remainder + '0') : (remainder - 10 + 'a');
    } while (tmp_value /= base);
    if (*buffer == '-') {
        ptr1++;
    }
    *ptr-- = '\0';
    while (ptr1 < ptr) {
        tmp_char = *ptr;
        *ptr-- = *ptr1;
        *ptr1++ = tmp_char;
    }
    return buffer;
}
// Функция для инициализации UART
void UART_init(unsigned int ubrr) {
    // Устанавливаем скорость передачи
    UBRR0H = (unsigned char)(ubrr >> 8);
    UBRR0L = (unsigned char)ubrr;
    // Разрешаем передачу и прием
    UCSR0B = (1 << TXEN0) | (1 << RXEN0) | (1 << UCSZ02);
    // Устанавливаем формат кадра: 8 бит данных, 1 стоп-бит
    UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);
    // Настраиваем пины для RTS/CTS
    DDRB |= (1 << DDB0);     // RTS как выход
    PORTB |= (1 << PORTB0);  // RTS высокий по умолчанию
    DDRB &= ~(1 << DDB1);    // CTS как вход
}
// Функция для передачи одного байта данных
void UART_transmit(unsigned char data) {
    // Ожидаем, пока освободится буфер передачи
    while (!(UCSR0A & (1 << UDRE0)));
    // Устанавливаем RTS низкий (отправка данных)
    PORTB &= ~(1 << PORTB0);
    // Помещаем данные в буфер
    UDR0 = data;
    // Ожидаем завершения передачи
    while (!(UCSR0A & (1 << TXC0)));
    // Устанавливаем RTS высокий (готов к следующей передаче)
    PORTB |= (1 << PORTB0);
}
// Функция для передачи строки
void UART_putstring(const char *s) {
    while (*s) {
        UART_transmit(*s++);
    }
}
// Функция для начальных настроек
void setup(void) {
    UART_init(MYUBRR); // Инициализируем UART
    // Настройка PD2 как вход для кнопки
    DDRD &= ~(1 << DDD2);    // PD2 как вход //Кстати, так я обычно не пишу. Сначала определяю кнопку через дефайн, а затем указываю ее через соответствующую переменную. Такой подход помогает в случае необходимости быстро переопределить рабочие пины, на нарушая при этом логику программы.
    PORTD |= (1 << PORTD2);  // Подтяжка вверх для кнопки
    // Настройка внешнего прерывания
    EIMSK |= (1 << INT0);        // Разрешаем прерывание INT0
    EICRA |= (1 << ISC01);       // Прерывание по спаду сигнала на PD2
    sei();                       // Разрешаем глобальные прерывания
}
// Основной цикл
void loop(void) {
    // Основной цикл не требуется в данном примере
    while(1){
        UART_putstring("Hello World!\n"); // Отправляем "Hello World" по UART
        _delay_ms(10); // Ждем 10 миллисекунд
    }
}
// Основная функция. Такая конструкция обеспечивает совместимость с Arduino. Специально для маленьких и тупых.
int main(void) {
    setup(); // Вызываем функцию для начальных настроек
    loop();
}
// Обработчик прерывания INT0
ISR(INT0_vect) {
    UART_putstring("Pulse!\n"); // Отправляем "Pulse" по UART при нажатии кнопки
}

Ну лично я не любитель подключать лишние пины... Обычно их всегда не хватает. Поэтому хочется все же реализовать какой-то "flow control" на двух проводах. На такой случай есть XON/XOFF. Это программный способ.

#define F_CPU 16000000UL // Определяем частоту процессора как 16 МГц
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
 
// Настройки UART
#define BAUD 1000000
#define MYUBRR F_CPU/16/BAUD-1
#define XON  0x11  // XON символ (DC1)
#define XOFF 0x13  // XOFF символ (DC3)
 
// Прототипы функций
void UART_init(unsigned int ubrr);
void UART_transmit(unsigned char data);
void UART_putstring(const char *s);
void setup(void);
void loop(void);
 
// Глобальная переменная для остановки передачи по XOFF
volatile uint8_t transmission_enabled = 1;
 
// Функция для инициализации UART
void UART_init(unsigned int ubrr) {
    // Устанавливаем скорость передачи
    UBRR0H = (unsigned char)(ubrr >> 8);
    UBRR0L = (unsigned char)ubrr;
    // Разрешаем передачу и прием
    UCSR0B = (1 << TXEN0) | (1 << RXEN0) | (1 << RXCIE0);  // Включаем прерывание на прием данных
    // Устанавливаем формат кадра: 8 бит данных, 1 стоп-бит
    UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);
}
 
// Функция для передачи одного байта данных
void UART_transmit(unsigned char data) {
    // Ожидаем, пока освободится буфер передачи
    while (!(UCSR0A & (1 << UDRE0)));
    // Помещаем данные в буфер
    UDR0 = data;
}
 
// Функция для передачи строки
void UART_putstring(const char *s) {
    while (*s) {
        // Ждем, пока разрешена передача данных
        while (!transmission_enabled);
        UART_transmit(*s++);
    }
}
 
// Функция для начальных настроек
void setup(void) {
    UART_init(MYUBRR); // Инициализируем UART
    // Настройка PD2 как вход для кнопки
    DDRD &= ~(1 << DDD2);    // PD2 как вход
    PORTD |= (1 << PORTD2);  // Подтяжка вверх для кнопки
    // Настройка внешнего прерывания
    EIMSK |= (1 << INT0);        // Разрешаем прерывание INT0
    EICRA |= (1 << ISC01);       // Прерывание по спаду сигнала на PD2
    sei();                       // Разрешаем глобальные прерывания
}
 
// Основной цикл программы
void loop(void) {
    while (1) {
        UART_putstring("Hello World!\n"); // Отправляем "Hello World" по UART
        _delay_ms(10); // Задержка 10 миллисекунд
    }
}
 
// Основная функция
int main(void) {
    setup(); // Вызываем функцию для начальных настроек
    loop();  // Запускаем основной цикл
    return 0;
}
 
// Обработчик прерывания INT0 (по нажатию кнопки)
ISR(INT0_vect) {
    UART_putstring("Pulse!\n"); // Отправляем "Pulse" по UART при нажатии кнопки
}
 
// Обработчик прерывания по приему данных
ISR(USART_RX_vect) {
    unsigned char received = UDR0;  // Читаем принятый байт
    if (received == XOFF) {
        transmission_enabled = 0;  // Останавливаем передачу данных
        } else if (received == XON) {
        transmission_enabled = 1;  // Включаем передачу данных
    }
}
 

К слову, вот этот вариант реализации аппаратного UART-а работает очень хорошо. Никаких ошибок в мониторе не заметно, все четенько. Во всяком случае, в пределах верстака. Но я думаю, что этого все же мало. Мы же тут не хуйней какой-нибудь страдаем; итоговая цель - это добиться максимально-надежного результата, применимого в промышленных решениях. И поэтому надо бы разобрать вариант использования системы CRC - контроля целостности данных... (Это позже)

🔧Новый UART с механизмом ЭХО и двойной буферизацией по линии RX

Ниже — полное описание работы системы UART на микроконтроллере AVR (например, ATmega328P), с применением:

  • аппаратного прерывания RX,

  • таймера с отслеживанием «тишины»,

  • двойной буферизации приёма,

  • поддержки программного управления потоком (XON/XOFF),

  • гарантированной обработки данных без потерь,

  • и функции «эхо»-ответа.

#define F_CPU 16000000UL  // Тактовая частота МК — 16 мегагерц
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#include <string.h>
 
// Настройки UART
#define BAUD 9600
#define MYUBRR (F_CPU / 16 / BAUD - 1)
 
// Управляющие символы XON/XOFF
#define XON  0x11  // Разрешение передачи
#define XOFF 0x13  // Запрет передачи
 
// Размер буфера приёма
#define BUFFER_SIZE 64
 
// Таймаут, через сколько мс считать сообщение завершённым
#define TIMEOUT_LIMIT 2  // 2 мс без новых байтов = конец сообщения
 
// ==== Глобальные переменные ====
volatile uint8_t transmission_enabled = 1;         // Флаг: можно ли передавать
char rx_buffer1[BUFFER_SIZE];                     // Первый приёмный буфер
char rx_buffer2[BUFFER_SIZE];                     // Второй приёмный буфер
volatile char *current_rx_buffer = rx_buffer1;     // Указатель на текущий буфер
volatile char *sending_buffer = NULL;              // Указатель на буфер для отправки
volatile uint8_t rx_index = 0;                    // Индекс в текущем буфере
volatile uint8_t message_length = 0;              // Длина завершенного сообщения
volatile uint8_t message_complete = 0;            // Флаг: получено полное сообщение
volatile uint8_t timeout_counter = 0;             // Счётчик миллисекунд до окончания приёма
 
// ==== Прототипы функций ====
void UART_init(unsigned int ubrr);
void UART_transmit(unsigned char data);
void UART_putstring(const char *s);
void UART_transmit_buffer(const volatile char *buffer, uint8_t length);
void Timer1_init(void);
void Timer1_reset(void);
void setup(void);
void loop(void);
 
// ==== UART: инициализация ====
void UART_init(unsigned int ubrr) {
    UBRR0H = (unsigned char)(ubrr >> 8); // Старший байт скорости
    UBRR0L = (unsigned char)ubrr;        // Младший байт скорости
 
    UCSR0B = (1 << RXEN0) | (1 << TXEN0) | (1 << RXCIE0); // Включить приёмник, передатчик и прерывание приёма
    UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);              // Формат кадра: 8 бит данных, 1 стоп-бит
}
 
// ==== UART: передача одного байта ====
void UART_transmit(unsigned char data) {
    while (!(UCSR0A & (1 << UDRE0)));  // Ждём, пока передатчик освободится
    UDR0 = data;                       // Отправляем байт
}
 
// ==== UART: передача строки (заканчивается '\0') ====
void UART_putstring(const char *s) {
    while (*s) {
        while (!transmission_enabled); // Ждём, пока передача разрешена (XON)
        UART_transmit(*s++);
    }
}
 
// ==== UART: передача массива без '\0' ====
void UART_transmit_buffer(const volatile char *buffer, uint8_t length) {
    for (uint8_t i = 0; i < length; i++) {
        while (!transmission_enabled); // Ждём разрешения
        UART_transmit(buffer[i]);
    }
}
 
// ==== Настройка Timer1 на прерывание каждые 1 мс ====
void Timer1_init(void) {
    TCCR1B = (1 << WGM12); // CTC режим (сброс по совпадению с OCR1A)
    OCR1A = 249;           // При F_CPU=16 МГц и делителе 64: 16_000_000 / 64 / 1000 - 1 = 249
    TCCR1B |= (1 << CS11) | (1 << CS10); // Предделитель = 64
    TIMSK1 |= (1 << OCIE1A);             // Разрешаем прерывание по совпадению
}
 
// ==== Сброс счётчика таймера (не влияет на интервал) ====
void Timer1_reset(void) {
    TCNT1 = 0;
}
 
// ==== Инициализация системы ====
void setup(void) {
    UART_init(MYUBRR);  // Настраиваем UART
    Timer1_init();      // Настраиваем таймер
    sei();              // Включаем глобальные прерывания
 
    // Очищаем буферы
    memset((void *)rx_buffer1, 0, BUFFER_SIZE); // Приведение к void *
    memset((void *)rx_buffer2, 0, BUFFER_SIZE); // Приведение к void *
    UART_putstring("madmentat.ru");
}
 
// ==== Главный цикл ====
void loop(void) {
    while (1) {
        // Если сообщение полностью получено — можно отправить эхо
        if (message_complete) {
            cli(); // Защита от прерываний во время работы с буферами
 
            if (sending_buffer != NULL) {
                UART_putstring("echo: ");
                UART_transmit_buffer(sending_buffer, message_length); // Используем message_length
                memset((void *)sending_buffer, 0, BUFFER_SIZE); // Приведение к void *
                sending_buffer = NULL;
                message_complete = 0;
                message_length = 0; // Сбрасываем длину
            }
 
            sei(); // Включаем прерывания обратно
        }
    }
}
 
// ==== Точка входа ====
int main(void) {
    setup();
    loop();
    return 0;
}
 
// ==== Прерывание по приёму байта ====
ISR(USART_RX_vect) {
    unsigned char received = UDR0; // Считываем байт из UART
    if (received == XOFF) {
        transmission_enabled = 0; // Запрет передачи
    } else if (received == XON) {
        transmission_enabled = 1; // Разрешение передачи
    } else {
        // Сохраняем байт в текущий буфер, если есть место
        if (rx_index < BUFFER_SIZE - 1) {
            current_rx_buffer[rx_index++] = received;
            timeout_counter = 0; // Сбрасываем таймаут — сообщение продолжается
            Timer1_reset();      // Сбрасываем таймер
        }
    }
}
 
// ==== Прерывание таймера раз в 1 мс ====
ISR(TIMER1_COMPA_vect) {
    if (rx_index > 0) {
        timeout_counter++; // Увеличиваем счётчик «тишины»
        if (timeout_counter >= TIMEOUT_LIMIT) {
            // Сообщение завершилось (тихо более 2 мс)
            sending_buffer = current_rx_buffer;
            current_rx_buffer = (current_rx_buffer == rx_buffer1) ? rx_buffer2 : rx_buffer1;
            message_complete = 1;
            message_length = rx_index; // Сохраняем длину сообщения
            rx_index = 0; // Сбрасываем индекс
            timeout_counter = 0;
        }
    } else {
        timeout_counter = 0; // Нет данных — сбрасываем
    }
}

⚙️ Частота: МК работает на 16 МГц (максимальная штатная частота для AVR ATmega328P). Это важно для точного расчёта таймеров и делителей частоты UART.

📡 UART: интерфейс работает на скорости 9600 бод — одна из самых медленных, но надёжных и совместимых скоростей. Для этой скорости символ (байт) передаётся примерно за 1.04 миллисекунды. Это даёт до 960 байт в секунду максимум.

🛠 Принцип работы UART приёма

  • 🧲 UART сконфигурирован с помощью регистра UBRR0 на заданную скорость обмена.
  • 📥 Прерывание USART_RX_vect обрабатывает каждый принятый байт по приходу.
  • 📦 Каждый байт записывается в текущий активный буфер rx_buffer1 или rx_buffer2 (см. current_rx_buffer).
  • 🔁 Приём продолжается, пока буфер не заполнен или не наступит таймаут «тишины» между байтами.

⏲ Таймаут приёма данных

Используется Timer1 в режиме CTC (Clear Timer on Compare Match) для отслеживания пауз между байтами.

  • 💡 Таймаут выбран 2 мс (в два раза дольше, чем передача 1 байта на 9600 бод), чтобы гарантированно определить «конец сообщения» при паузе.
  • ⏱ При каждом новом байте таймер сбрасывается, если данных нет 2 мс — считается, что приём закончен.
  • 🔀 После таймаута: указатель на буфер переключается, активный буфер передаётся на отправку («эхо»), и начинается новый приём.

📂 Двойной буфер — почему это важно

🧠 Использование двух буферов (текущий и отправляемый) позволяет:

  • 🚫 избежать потери данных, если новые байты поступают во время обработки предыдущего пакета,
  • ⚡ позволить главному циклу loop() не спешить с отправкой — приём продолжается независимо,
  • 🔁 избежать блокировки ISR (прерывания короткие и не мешают другим),
  • 🧹 эффективно очищать отправленный буфер с помощью memset().

📤 Механизм ЭХО и вывод

  • 💬 По завершении приёма, активируется флаг message_complete.
  • ⚙ Главный цикл (loop()) замечает это и передаёт строку "echo: " + содержимое буфера.
  • 🧼 Затем буфер очищается, и указатель сбрасывается.

🔐 Управление потоком: XON / XOFF

Система поддерживает простейший протокол программного управления потоком:

  • 0x13 (XOFF) — остановка передачи. Главный цикл приостанавливает UART_transmit().
  • 0x11 (XON) — возобновление передачи. Используется в условиях переполнения буфера на стороне приёмника.

⚠️ Прерывания и защита данных

Чтобы избежать конфликтов между прерыванием и главным циклом:

  • cli() — отключает глобальные прерывания при доступе к общим переменным (message_complete, sending_buffer).
  • sei() — снова включает прерывания.
  • 💡 Это особенно важно для переменных типа volatile, доступ к которым может быть нарушен асинхронными ISR.

🧪 Диагностика и расширения

Что можно добавить:

  • 📏 Логика подсчёта длины пакета и контроль допустимого размера.
  • 🔐 CRC или контрольная сумма для надёжности (если шина «шумная»).
  • 💤 Переход в спящий режим sleep() между сообщениями (экономия энергии).
  • 📶 Индикация через LED или порт о статусе приёма/передачи.

🚀 Разработка: madmentat.ru — практическая электроника и AVR