АППАРАТНЫЙ UART В MICROCHIP STUDIO

 

 Для проверки программы потребуется UART-монитор и USB-TTL конвертр:

 

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 - контроля целостности данных...