АППАРАТНЫЙ UART В MICROCHIP STUDIO
Для проверки программы потребуется UART-монитор и USB-TTL конвертр:
Начнем с простенькой реализации базового функционала. Тут нет ничего лишнего. Просто связь по двум проводам... В данном случае скорость мегабит, но можно поменять 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 - контроля целостности данных...