АППАРАТНЫЙ UART В MICROCHIP STUDIO
Для проверки программы потребуется UART-монитор и 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 стоп-бит):
- Idle — линия на лог.1 (5 В или 3.3 В).
- Start bit — лог.0 (переход в 0 В).
- 8 бит данных — передаются от младшего к старшему (LSB first).
- Стоп-бит — лог.1 (возврат к 5 В или 3.3 В).
- 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