Serial изнутри: буферы, UBRR0 и U2X
Serial — это USART0 с двумя кольцевыми буферами и двумя обработчиками прерываний. Serial.begin сам выбирает делитель и режим U2X, и для одной скорости делает это хуже, чем мог бы. Разберём HardwareSerial по исходнику и посчитаем, когда буфер приёма переполняется.
Обновлено Редакция GAW
От Arduino к регистрамурок 7 из 835 мин
Цель урока: Знать, какие значения Serial.begin записывает в регистры USART0 и с какой ошибкой скорости, рассчитывать время переполнения буфера приёма и при необходимости работать с USART0 регистрами без HardwareSerial.
На Arduino Uno выводы 0 и 1 — это RXD (PD0) и TXD (PD1) модуля USART0 (раздел 14.3.3 даташита), подключённые к мосту USB-UART. Как работать с USART0 регистрами — расчёт UBRR0, флаги UDRE0 и RXC0, printf, — разобрано в уроке о UART трека «AVR с нуля». Здесь — что поверх этого строит класс HardwareSerial ядра ArduinoCore-avr 1.8.8.
Что делает Serial.begin
// ArduinoCore-avr 1.8.8, cores/arduino/HardwareSerial.cpp, lines 117-150
void HardwareSerial::begin(unsigned long baud, byte config)
{
// Try u2x mode first
uint16_t baud_setting = (F_CPU / 4 / baud - 1) / 2;
*_ucsra = 1 << U2X0;
// hardcoded exception for 57600 for compatibility with the bootloader
// shipped with the Duemilanove and previous boards and the firmware
// on the 8U2 on the Uno and Mega 2560. Also, The baud_setting cannot
// be > 4095, so switch back to non-u2x mode if the baud rate is too
// low.
if (((F_CPU == 16000000UL) && (baud == 57600)) || (baud_setting > 4095))
{
*_ucsra = 0;
baud_setting = (F_CPU / 8 / baud - 1) / 2;
}
*_ubrrh = baud_setting >> 8;
*_ubrrl = baud_setting;
_written = false;
*_ucsrc = config;
sbi(*_ucsrb, RXEN0);
sbi(*_ucsrb, TXEN0);
sbi(*_ucsrb, RXCIE0);
cbi(*_ucsrb, UDRIE0);
}
Формула (F_CPU / 4 / baud − 1) / 2 — это UBRR0 = f / (8 · BAUD) − 1 из табл. 20-1 для режима U2X0, записанная в целых числах с округлением к ближайшему. Ядро сначала пробует удвоенную скорость: делитель 8 вместо 16 даёт более мелкий шаг подстройки. Режим без U2X0 включается в двух случаях — если UBRR0 не помещается в 12 бит (медленные скорости) и для одной конкретной скорости, 57 600 бит/с на 16 МГц.
Serial.begin(9600) — это begin(9600, SERIAL_8N1), а SERIAL_8N1 = 0x06: в UCSR0C это UCSZ01:00 = 11 — 8 бит данных, без чётности, 1 стоп-бит. Затем включаются приёмник, передатчик и прерывание по приёму. До begin() выводы 0 и 1 свободны: init() записала UCSR0B = 0, отключив USART0, который оставил включённым загрузчик (строки 384–391 wiring.c).
Что выбирает ядро и чего это стоит
Значения UBRR0 и ошибки скорости — из табл. 20-7 даташита для 16 МГц, выбор ядра — по формулам выше (расчёт наш):
| Скорость, бит/с | Ядро выбирает | Ошибка | Другой режим | Ошибка |
|---|---|---|---|---|
| 9600 | U2X0 = 1, UBRR0 = 207 | 0,2 % | U2X0 = 0, 103 | 0,2 % |
| 57 600 | U2X0 = 0, UBRR0 = 16 | 2,1 % | U2X0 = 1, 34 | −0,8 % |
| 115 200 | U2X0 = 1, UBRR0 = 16 | 2,1 % | U2X0 = 0, 8 | −3,5 % |
| 250 000 | U2X0 = 1, UBRR0 = 7 | 0,0 % | U2X0 = 0, 3 | 0,0 % |
| 1 000 000 | U2X0 = 1, UBRR0 = 1 | 0,0 % | U2X0 = 0, 0 | 0,0 % |
Рекомендуемая ошибка скорости приёмника для 8 бит данных — ±2,0 % без U2X0 и ±1,5 % с ним (табл. 20-2 и 20-3). На 57 600 ядро сознательно выбирает вариант с ошибкой 2,1 % вместо −0,8 % — комментарий в коде объясняет это совместимостью со старым загрузчиком Duemilanove и прошивкой моста 8U2 на Uno и Mega 2560. На 115 200 бит/с 2,1 % тоже выше рекомендации для U2X0 = 1; связь обычно работает, потому что ошибка партнёра — моста USB-UART — мала, но с другим микроконтроллером на том же кварце запаса может не хватить. Скорости 250 000, 500 000 и 1 000 000 бит/с делятся из 16 МГц без ошибки. Любую комбинацию частоты и скорости проверит калькулятор UART.
Буферы и прерывания
HardwareSerial держит два кольцевых буфера — на приём и на передачу. Их размер задаёт HardwareSerial.h (строки 43–55): 64 байта, если RAMEND − RAMSTART не меньше 1023, то есть ОЗУ не меньше 1024 байт, иначе 16. У ATmega328P RAMSTART = 0x100 и RAMEND = 0x8FF, то есть 2 КБ, — буферы по 64 байта.
Приём. Обработчик ISR(USART_RX_vect) вызывает _rx_complete_irq() (HardwareSerial_private.h, строки 101–121): читает UDR0, и если следующая позиция головы не совпадает с хвостом, кладёт байт в буфер. Если совпадает — буфер полон, и байт молча теряется. Байт с ошибкой чётности тоже отбрасывается. Поскольку одна ячейка всегда пуста, в буфере помещается 63 байта. Кадр 8N1 — это 10 бит, и время заполнения буфера при непрерывном потоке (расчёт наш):
| Скорость, бит/с | Время на байт | 63 байта приходят за |
|---|---|---|
| 9600 | 1,04 мс | 66 мс |
| 115 200 | 86,8 мкс | 5,5 мс |
Если loop() с delay(100) читает порт реже, часть данных пропадёт без всякого сообщения. serialEvent() здесь не помогает: main() вызывает её после каждого прохода loop(), а не по прерыванию.
Передача. Serial.write() (строки 225–279) сначала пробует короткий путь: если буфер пуст и регистр UDR0 свободен, байт пишется сразу в UDR0. Иначе байт ложится в буфер и включается прерывание «регистр данных пуст» (UDRIE0), обработчик которого отправляет байты по одному. Когда буфер полон, write() ждёт освобождения места — поэтому длинный Serial.print() на 9600 бит/с тормозит программу примерно на 1 мс за каждый символ сверх буфера; подробнее — в ответе «Почему Serial.print замедляет программу».
Цена такой схемы — по прерыванию на каждый байт в каждую сторону: на 115 200 бит/с это до 11 520 прерываний в секунду на приём и столько же на передачу (расчёт наш).
USART0 без HardwareSerial
Если в скетче нет ни одного обращения к Serial, файл HardwareSerial0.cpp с обработчиками USART0 в программу не попадает — его комментарий так и объясняет, зачем каждый порт вынесен в отдельный файл. Тогда USART0 можно взять себе целиком, например, чтобы на 57 600 бит/с получить ошибку −0,8 %:
// USART0 at 57600 baud, U2X0 = 1: UBRR0 = 34, error -0.8 % (table 20-7)
void uartInit(void)
{
UCSR0A = _BV(U2X0);
UBRR0 = 34;
UCSR0C = _BV(UCSZ01) | _BV(UCSZ00); // 8N1
UCSR0B = _BV(RXEN0) | _BV(TXEN0); // no interrupts: polling
}
void uartPut(uint8_t c)
{
while (!(UCSR0A & _BV(UDRE0))) {} // wait until UDR0 is free
UDR0 = c;
}
void setup() {
uartInit();
for (const char *p = "hello\r\n"; *p; p++) uartPut(*p);
}
void loop() {}
Такой вывод занимает процессор, пока байт не уйдёт в регистр, зато в нём нет буферов и прерываний. Свой приём с кольцевым буфером любого размера в обработчике ISR(USART_RX_vect) разобран в статье «Кольцевой буфер для UART». Смешивать такой код с Serial нельзя: Serial.begin() перепишет UCSR0A, UBRR0, UCSR0C и UCSR0B своими значениями и включит прерывания USART0, а Serial.write() будет писать в тот же UDR0.
Уровни на выводах 0 и 1
Выводы RX и TX Uno подключены к мосту USB-UART платы. Внешний модуль с UART подключают к ним только с общим проводом GND и только с логическими уровнями 0–5 В, не выше VCC + 0,5 В (раздел 29.1). У интерфейса RS-232 другие уровни, и он подключается только через преобразователь. Пока к выводам 0 и 1 подключено внешнее устройство, загрузка скетча через USB может не работать — загрузчик использует тот же USART0.
Задание
- Какие значения запишет
Serial.begin(38400)в UCSR0A и UBRR0 на 16 МГц и какая будет ошибка скорости? - Датчик шлёт поток данных на 9600 бит/с без пауз, а
loop()читает порт раз в 200 мс. Сколько байтов теряется за каждый проход? - На плате с кварцем 8 МГц скетч вызывает
Serial.begin(57600). Сработает ли исключение для 57 600? Какие UCSR0A и UBRR0 получатся?
Ответ и решение
-
С U2X0: 16 000 000 / 4 / 38 400 = 104 (целое), (104 − 1) / 2 = 51. Условие исключения не выполнено, поэтому UCSR0A =
_BV(U2X0), UBRR0 = 51, ошибка 0,2 % — по табл. 20-7. -
Байт 8N1 — 10 бит, 1,04 мс. За 200 мс приходит около 192 байт, в буфер помещается 63 — теряется около 129 байт за проход (расчёт наш).
-
Нет: исключение проверяет
F_CPU == 16000000UL. Ядро оставит U2X0 = 1 и посчитает (8 000 000 / 4 / 57 600 − 1) / 2 = (34 − 1) / 2 = 16. Реальная скорость 8 000 000 / (8 · 17) ≈ 58 824 бит/с — ошибка +2,1 % (расчёт наш).
Что дальше
В уроке 8 сравним библиотеки Wire и SPI с регистрами TWI и SPI: где библиотека почти бесплатна, а где добавляет буферы и ожидание. Все регистры USART0 с битами — на странице регистров USART0 ATmega328P. Принять и отправить данные с компьютера можно в терминале UART прямо в браузере, а что такое скорость в бодах — в термине «Скорость передачи (бод)».
Частые вопросы
Почему Serial теряет байты при приёме?
Обработчик приёма кладёт байты в кольцевой буфер на 64 байта, из которых занято может быть 63: одна ячейка остаётся пустой, чтобы отличать полный буфер от пустого. Если loop() не вызывает Serial.read() достаточно часто, буфер заполняется, и следующие байты ядро молча отбрасывает. На 9600 бит/с 63 байта приходят за 66 мс, на 115 200 — за 5,5 мс, так что delay(100) в loop() при непрерывном потоке данных гарантирует потери.
Что делает Serial.flush()?
В ядре Arduino для AVR flush() ждёт, пока все байты из буфера передачи уйдут в линию и аппаратура выставит флаг TXC0 — конец передачи последнего бита. Входной буфер flush() не трогает. Чтобы выбросить принятые байты, читайте их в цикле, пока available() больше нуля.
Можно ли увеличить буфер Serial из скетча?
Строкой #define в скетче — нет: файлы ядра компилируются отдельно, и макрос SERIAL_RX_BUFFER_SIZE из скетча до них не доходит. Размер задают ключом -DSERIAL_RX_BUFFER_SIZE=… для всей сборки — через дополнительные флаги компилятора платформы (compiler.cpp.extra_flags в platform.txt ядра). Комментарий в HardwareSerial.h предупреждает, что при размере больше 256 байт защита индексов от прерываний не реализована, и выбирать лучше степень двойки.
Источники
- Arduino. ArduinoCore-avr 1.8.8: cores/arduino/HardwareSerial.cpp, HardwareSerial.h, HardwareSerial_private.h, HardwareSerial0.cpp, wiring.c, main.cpp — тег 1.8.8 — коммит 86df345 от 21 мая 2026 (GitHub API, tags и commits); HardwareSerial.cpp, стр. 117–150: begin — сначала U2X0 и baud_setting = (F_CPU / 4 / baud − 1) / 2, для 57 600 бит/с при 16 МГц и при baud_setting > 4095 — без U2X0 и (F_CPU / 8 / baud − 1) / 2, затем UCSR0C = config, RXEN0, TXEN0, RXCIE0; стр. 89–113: обработчик UDRE отправляет байт из буфера и выключает UDRIE0, когда буфер пуст; стр. 166–190: available и read; стр. 205–223: flush ждёт конца передачи; стр. 225–279: write — байт сразу в UDR0, если буфер и регистр пусты, иначе в буфер с ожиданием места, при запрещённых прерываниях — опрос UDRE0; HardwareSerial.h, стр. 43–55: буферы по 64 байта, если RAMEND − RAMSTART не меньше 1023, то есть ОЗУ от 1024 байт, иначе по 16; стр. 71: SERIAL_8N1 = 0x06; стр. 121: begin(baud) = begin(baud, SERIAL_8N1); HardwareSerial_private.h, стр. 101–121: приём — байт с ошибкой чётности отбрасывается, при полном буфере новый байт не записывается; HardwareSerial0.cpp, стр. 40–70: ISR(USART_RX_vect), ISR(USART_UDRE_vect) и объект Serial на регистрах USART0; wiring.c, стр. 384–391: init() пишет UCSR0B = 0; main.cpp: serialEventRun() вызывается после каждого loop(); platform.txt, стр. 28 и 58: флаги компилятора и compiler.cpp.extra_flags; boards.txt: загрузка Uno через загрузчик на том же последовательном порту, 115 200 бит/с
- Microchip. ATmega48A/PA/88A/PA/168A/PA/328/P Data Sheet (DS40002061B) — табл. 20-1: UBRRn = fosc / (16 · BAUD) − 1, при U2Xn = 1 — fosc / (8 · BAUD) − 1; разд. 20.3.2: U2Xn уменьшает делитель с 16 до 8; табл. 20-2 и 20-3: рекомендуемая ошибка скорости приёмника для 8 бит данных — ±2,0 % при U2Xn = 0 и ±1,5 % при U2Xn = 1; табл. 20-7: UBRRn и ошибки для 16 МГц (9600 — 103/0,2 % и 207/0,2 %; 38 400 — 25/0,2 % и 51/0,2 %; 57 600 — 16/2,1 % и 34/−0,8 %; 115 200 — 8/−3,5 % и 16/2,1 %; 250 000 — 3/0,0 % и 7/0,0 %; 1 Мбит/с — 0 и 1, 0,0 %); разд. 20.11: UDR0, UCSR0A (UDRE0, U2X0, UPE0), UCSR0B (RXCIE0, UDRIE0, RXEN0, TXEN0), UCSR0C (UCSZ01:00); разд. 14.3.3: RXD — PD0, TXD — PD1; разд. 29.1: напряжение на выводе от −0,5 В до VCC + 0,5 В
- AVR-LibC 2.3.2: avr/interrupt.h, util/atomic.h, iom328p.h (github.com/avrdudes/avr-libc) — векторы USART_RX_vect (18) и USART_UDRE_vect (19); ATOMIC_BLOCK(ATOMIC_RESTORESTATE), которым HardwareSerial защищает запись в UDR0 и голову буфера; RAMSTART = 0x100, RAMEND = 0x8FF у ATmega328P
Нашли ошибку?
Смотрите также
- UART и printfUBRR0, U2X0, UDRE0, RXC0 и printf через поток avr-libc
- Скорость UARTUBRR, BRR, TH1 и ошибка скорости
- Кольцевой буфер UARTБуфер SPSC с маской, ISR приёма и передачи на AVR и STM32, ORE и DOR, размер
- Режим удвоенной скорости USART (U2X)Бит AVR, который уменьшает делитель USART с 16 до 8
- Почему Serial.print замедляет программу Arduino?Буфер 64 байта полон — print ждёт линию; на 9600 бод ~1 мс на байт
- Почему Serial.readString() ждёт целую секунду?readString читает до таймаута 1000 мс; помогает readStringUntil или свой разбор
- Как вывести float с нужным числом знаков на Arduino?Serial.print(x, 4); sprintf %f даёт «?» без printf_flt; dtostrf для строк