Зачем 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 с внешним миром. Дальше по плану — АЦП: почти каждый проект с датчиками упирается в чтение аналоговых сигналов.