Добавить в избранное

UART на AVR на чистом C: настройка USART, printf, кольцевой буфер и приём по прерыванию

Чтение ≈ 5 мин

Полный разбор USART в ATmega328P на чистом C: расчёт UBRR и скорости обмена, посимвольная передача и приём, подключение printf через fdevopen, неблокирующий приём с кольцевым буфером по прерыванию RX, передача из буфера по UDRE-прерыванию и разбор текстовых команд.

Зачем AVR-у UART

UART — главный отладочный канал и способ общения с ПК, GPS-модулями, GSM-модемами, Bluetooth (HC-05/06), ESP-шками и другими контроллерами. Через USB-UART переходник (CH340, CP2102, FT232) за пару долларов ATmega разговаривает с компьютером как обычный COM-порт.

В ATmega328P один аппаратный USART на ножках PD0 (RX) и PD1 (TX). Умеет 8/9 бит, чётность, 1–2 стоп-бита, скорости до 1 Мбод и прерывания на приём/передачу/очистку буфера.

Расчёт скорости: UBRR

Регистр UBRR0 задаёт делитель тактовой частоты под baud rate:

// асинхронный режим, U2X=0:
UBRR = F_CPU / (16 * BAUD) - 1
// асинхронный режим, U2X=1 (точнее на высоких скоростях):
UBRR = F_CPU / (8 * BAUD) - 1

Для 16 МГц и 9600 бод: 16000000/(16×9600)-1 = 103,166 → 103, ошибка ~0,16% — отлично. Для 115200 с U2X=0 ошибка уже ~3,7% (UBRR=8) — терпимо, но лучше U2X=1 и UBRR=16 (ошибка ~2,1%) либо кварц 14,7456 МГц, где стандартные скорости делятся без остатка.

Ошибка выше ~2% — кирпич на приёме: биты «уезжают» и вместо текста приходит мусор. Внутренний RC-генератор без калибровки гуляет до ±10% — для UART берите кварц.

Минимальный UART: init + putchar/getchar

#define F_CPU 16000000UL
#include <avr/io.h>

#define BAUD 9600UL
#define UBRR_VAL (F_CPU / 16 / BAUD - 1)

static void uart_init(void)
{
    UBRR0H = (uint8_t)(UBRR_VAL >> 8);
    UBRR0L = (uint8_t)UBRR_VAL;
    UCSR0B = (1 << RXEN0) | (1 << TXEN0);      // приёмник + передатчик
    UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);    // 8N1: 8 бит, без чётности, 1 стоп
}

static void uart_putc(char c)
{
    while (!(UCSR0A & (1 << UDRE0)))           // ждём: буфер пуст
        ;
    UDR0 = c;
}

static char uart_getc(void)
{
    while (!(UCSR0A & (1 << RXC0)))            // ждём: байт пришёл
        ;
    return UDR0;
}

static void uart_puts(const char *s)
{
    while (*s)
        uart_putc(*s++);
}

int main(void)
{
    uart_init();
    uart_puts("Hello from ATmega328P!\r\n");

    while (1) {
        char c = uart_getc();                  // эхо
        uart_putc(c);
    }
}

Заметьте \r\n — терминалы ждут возврат каретки. Это polling-вариант: простой, но uart_getc вечно ждёт байт, блокируя main. Для отладочного echo — нормально, для реального устройства — нет.

printf через fdevopen

avr-libc позволяет перенаправить стандартные потоки на свою функцию — и printf заработает как на ПК:

#include <stdio.h>

static int uart_stdio_put(char c, FILE *stream)
{
    (void)stream;
    if (c == '\n')
        uart_putc('\r');
    uart_putc(c);
    return 0;
}

static FILE uart_out = FDEV_SETUP_STREAM(uart_stdio_put, NULL,
                                         _FDEV_SETUP_WRITE);

int main(void)
{
    uart_init();
    stdout = &uart_out;

    uint16_t adc = 723;
    printf("ADC=%u, V=%lu mV\r\n", adc, (uint32_t)adc * 5000 / 1023);
}

printf тянет ~1,5 КБ Flash (с float — ещё больше и нужен -Wl,-u,vfprintf -lprintf_flt). Для маленьких чипов используйте itoa()/utoa() + uart_puts.

Приём по прерыванию + кольцевой буфер

Правильная архитектура: ISR на RX складывает байты в кольцевой буфер, main забирает, когда удобно. Ни один байт не теряется, пока ISR успевает между байтами.

#define RX_BUF_SIZE 64                        // степень двойки!

static volatile uint8_t rx_buf[RX_BUF_SIZE];
static volatile uint8_t rx_head = 0;
static volatile uint8_t rx_tail = 0;

ISR(USART_RX_vect)
{
    uint8_t data = UDR0;                      // прочитать обязательно — иначе флаг не сбросится
    uint8_t next = (rx_head + 1) & (RX_BUF_SIZE - 1);
    if (next != rx_tail) {                    // буфер не полон
        rx_buf[rx_head] = data;
        rx_head = next;
    }
    // переполнение — байт молча выброшен; при желании ставим флаг ошибки
}

uint8_t uart_rx_available(void)
{
    return (rx_head - rx_tail) & (RX_BUF_SIZE - 1);
}

int16_t uart_rx_get(void)
{
    if (rx_head == rx_tail)
        return -1;
    uint8_t c = rx_buf[rx_tail];
    rx_tail = (rx_tail + 1) & (RX_BUF_SIZE - 1);
    return c;
}

Включить прерывание приёма:

UCSR0B |= (1 << RXCIE0);
sei();

Индексы uint8_t + маска по степени двойки — самая быстрая реализация на AVR. Чтение head/tail однобайтное — атомарно, cli/sei для них не нужен (это стандартный lock-free кольцевой буфер «один писатель — один читатель»).

Передача из буфера: неблокирующий TX

Аналогично: main кладёт байты в TX-буфер, а ISR по событию «регистр данных пуст» (UDRE) выгружает их в UART — процессор не ждёт.

#define TX_BUF_SIZE 64
static volatile uint8_t tx_buf[TX_BUF_SIZE];
static volatile uint8_t tx_head = 0;
static volatile uint8_t tx_tail = 0;

ISR(USART_UDRE_vect)
{
    if (tx_head == tx_tail) {
        UCSR0B &= ~(1 << UDRIE0);             // буфер пуст — выключаем прерывание
    } else {
        UDR0 = tx_buf[tx_tail];
        tx_tail = (tx_tail + 1) & (TX_BUF_SIZE - 1);
    }
}

void uart_tx_put(uint8_t c)
{
    uint8_t next = (tx_head + 1) & (TX_BUF_SIZE - 1);
    while (next == tx_tail)
        ;                                     // буфер полон — подождать
    tx_buf[tx_head] = c;
    tx_head = next;
    UCSR0B |= (1 << UDRIE0);                  // «пнуть» передатчик
}

Трюк в том, что прерывание UDRE само не выстреливает, пока его не разрешить: uart_tx_put кладёт байт и разрешает UDRIE0 — цепочка прерываний выгружает буфер сама.

Разбор текстовых команд

Классика отладки устройства: принимаем строку до \n, сравниваем с командами.

static char line[32];
static uint8_t lpos = 0;

static void uart_poll(void)
{
    int16_t c;
    while ((c = uart_rx_get()) >= 0) {
        if (c == '\r')
            continue;
        if (c == '\n') {
            line[lpos] = '\0';
            handle_command(line);
            lpos = 0;
        } else if (lpos < sizeof(line) - 1) {
            line[lpos++] = (char)c;
        }
    }
}

static void handle_command(const char *cmd)
{
    if (!strcmp(cmd, "led on")) {
        PORTB |= (1 << PB5);
        uart_puts("OK\r\n");
    } else if (!strcmp(cmd, "led off")) {
        PORTB &= ~(1 << PB5);
        uart_puts("OK\r\n");
    } else {
        uart_puts("ERR\r\n");
    }
}

uart_poll() вызывается каждый проход main loop — обработка неблокирующая, устройство остаётся отзывчивым.

Флаги ошибок: FE, DOR, PE

Регистр UCSR0A содержит флаги ошибок кадра — читать их нужно до UDR0, иначе они сбросятся:

  • FE0 (Frame Error) — неверный стоп-бит: не та скорость или помехи.
  • DOR0 (Data OverRun) — не успели прочитать UDR0, байт затёрт: увеличивайте буфер или поднимайте приоритет опроса.
  • PE0 (Parity Error) — ошибка чётности, если она включена.
ISR(USART_RX_vect)
{
    uint8_t status = UCSR0A;
    uint8_t data   = UDR0;
    if (status & ((1 << FE0) | (1 << DOR0) | (1 << PE0)))
        return;                               // кривой байт — отбросить
    // ... положить в буфер ...
}

UART на других чипах

  • ATmega2560 — 4 USART (USART0..3), регистры с суффиксом номера: UBRR0H, UBRR1H и т.д.
  • ATtiny85/2313 — есть USI (упрощённый последовательный интерфейс) вместо полного USART; USI умеет SPI и I2C, а UART придётся эмулировать программно (soft UART) либо на 2313 есть полноценный USART.
  • Программный UART — пригоден для низких скоростей (2400–9600) и одного дополнительного канала; велосипедить самому не нужно — есть проверенные реализации.

Типичные проблемы

  • Мусор вместо текста — расхождение baud rate: проверьте F_CPU, формулу UBRR и ошибку в процентах.
  • «Тишина» — TX/RX перепутаны местами (TX чипа → RX адаптера!) или общий GND не подключён.
  • Первый байт после старта битый — bootloader Arduino держит порт; добавьте паузу 100 мс перед первым выводом.
  • Потеря байтов на скорости — polling не успевает; переведите приём на прерывания с буфером.
  • printf не выводит — забыли stdout = &uart_out или флаг _FDEV_SETUP_WRITE.

Итог

Связка «RX-прерывание + кольцевой буфер + разбор команд в main» — эталонный каркас общения AVR с внешним миром. Дальше по плану — АЦП: почти каждый проект с датчиками упирается в чтение аналоговых сигналов.

Поделиться
Понравился материал?

Комментарии

Свернуть форму

Необходимо Войти или Зарегистрироваться, чтобы оставлять комментарии