Всем привет. Довелось мне писать довольно большой проект на AVRках. Как известно у них не очень большие скорости 16 МГц, у тех же STM32 можно гнать 72МГц и выше. Но опыта на STM мало, по этому пока AVR. Так вот мне нужно было в проекте передавать данные по UART, посылки не большие 10-15 байт, скорость 9600. Если все это дело реализовать в основном цикле, то это очень сильно тормозит систему. А у меня помимо этого есть еще куча других задач. Единственный выход использовать прерывания. Посмотрел несколько примеров в интернете, некоторые из них сложные, другие я даже не понял как работают, и по итогу сделал как понимал, и теперь делюсь с вами.
И так в первую очередь нам необходимо сформировать то что хотим отправить, то есть получить буфер для отправки, не важно как sprintf или itoa. Допустим определим буфер:
char sprintf_buf [15];
Теперь создаем еще один буфер из которого в UART будут отправляться данные, так как данные в первом буфере могут изменятся в произвольный момент времени, и это может изменить данные для отправки.
char uart0_tx_buf [20];
Теперь напишем функцию которая будет переписывать данные из одного буфера в другой. Тут есть момент, если вы пользуетесь strcpy то она при копировании строки в буфер обязательно добавить в конце 0, что и будет свидетельствовать о том что это последний символ. Но если у вас данные в буфере теоретически могут хранить число 0 не в символьном виде то лучше воспользоваться специальным символом. Я решил использовать '$'. Буфер будет переписывать пока не встретит символ '$' и сразу остановиться. Функция выглядит так:
void uart0_send_string ( char *send0) // сюда передаем строку в конце которой символ $
{
if (uart0_tx_counter == 0) // если счетчик на нуле можем обновить буфер, если нет то пропускаем
{
for (int i = 0; i < 20; i ++) // забиваем буффер отправки символами из переданого буфера
{
uart0_tx_buf[i] = send0[i];
if (uart0_tx_buf[i] == '$') break; // пока не встретим символ $. но его мы тоже заносим
}
UDR0 = uart0_tx_buf[uart0_tx_counter]; // отправляем нулевой байт UART
uart0_tx_counter ++; // увеличиваем счетчик
UCSR0B |= ( 1<<UDRIE0); // включаем прерывание по опустрошению буффера передачи
}
}
Вкратце что тут происходит. Допустим мы уже заполнили наш буфер на передачу sprintf_buf и теперь вызываем функцию uart0_send_string ( sprintf_buf ), передавая ей наш буфер. Как вы могли заметить есть еще какой то счетчик: uart0_tx_counter. Он нужен для нескольких задач, первая если мы попробуем отправить данные пока UART не закончил передачу, у нас ничего не получится, так как if(uart0_tx_counter == 0) нас тупо не пустит. А во вторых это счетчик позволяет знать UARTу какой элемент буфера нужно отправлять.
Еще я забыл рассказать про главное, а именно инициализацию UART. Не буду сильно расписывать, предполагается что вы знаете как настраивать скорость, прерывания, и просто включение:
void init_Uart_0 (void)
{
UCSR0B=(1<<RXEN0)|( 1<<TXEN0); //Включаем прием и передачу по USART
UCSR0B |= (1<<RXCIE0); //Разрешаем прерывание при приеме
UCSR0C = (1<<UPM01)|(1<<UCSZ01)|(1<<UCSZ00); //паритет 1, 8 бит
}
Как видим прерывание при передаче (а точнее по опустошению буфера передачи UCSR0B |= ( 1<<UDRIE0)) тут не включаем, иначе все поломается. А включаем прерывания только после того как отправили 0 байт по UARTу. Что произойдет дальше, мы запустили функцию которая переписала в буфер UARTа данные, отправила нулевой байт и вышла. Теперь мы можем заниматься своими делами, до тех пор пока не сработает прерывания по опустошению буфера. Вот его нужно обработать.
//----------обработчики прерываний на передачу ----------//
ISR (USART0_UDRE_vect) // прерывание по опустошению буффера передачи
{
if(uart0_tx_buf[uart0_tx_counter] != '$') // пока символ в буффере не равен $
{
UDR0 = uart0_tx_buf[uart0_tx_counter]; // продолжаем копировать из буффера байты в юарт
uart0_tx_counter ++; // ну и естественно передвигаемся по массиву
}
else // если дошли до символа $
{
uart0_tx_counter = 0; // сбрасываем счетчик
UCSR0B &= ~ ( 1<<UDRIE0); // вырубаем прерывания по опустошению буфера
}
}
Тут логика похожая, зашли в прерывания проверили символ, если это не '$' то записываем символ, увеличиваем счетчик и снова выходим из прерывания. У нас опять есть куча свободного времени чтоб делать свои дела. И так будет до тех пор пока прерывание не встретит '$'. Тогда мы сбрасываем счетчик, выключаем прерывание и готовы ждать новую посылку.
Что дает '$', это позволяет отправлять посылки разной длины (главное чтоб в буфер влезло). Если посылка фиксирована то можно сделать еще проще, но я делал универсальный код.
Вообщем сильно не пинайте, это мой первый пост. Конструктивной критике рад:)
Комментарии (27)
gleb_l
02.04.2022 12:36+22Извините, уважаемый Рамзес, но если Вы не понимаете, как работают прерывания в SoC-системах, лучше не изобретать велосипед, а отставить в сторону этот “большой проект на AVR”, разобраться с прерываниями, и только потом продолжить.
Это самое политкорректное, что можно сказать по этому поводу.
Ramzess_II Автор
02.04.2022 13:22что именно вы имеете ввиду? можно вкратце?
gleb_l
02.04.2022 17:20+11Такие задачи типично решаются одним буфером и двумя указателями - по одному пишем в буфер, по другому - достаём и отправляем. Чтобы не возиться с переносами, в микросистемах буфер делается а) кольцевым, б) с длиной, кратной степени двойки - угадайте, зачем ;). Если у системы есть аппаратный уарт, обычно на передачу у него как минимум два регистра - буфер текущего передаваемого байта и защёлка следующего. Прерывание может генерироваться по опустошению второго, а может и по последнему биту первого - зависит от аппаратной реализации. В последнем случае у вас мало времени ;), но для 9600 хватит с головой взять байт из буфера по указателю чтения, отправить в уарт, и увеличить указатель. Осмыслить все это неспеша, прочитав спеку, и опционально построив прототип на эмуляторе, крайне желательно перед тем, как строить саму большую систему.
PS - я видел прекрасный пример обмена между двумя МК, при котором разработчик наивно полагал, что приёмник магически знает, когда передатчику вздумается начать с ним обмен ;)
Ramzess_II Автор
02.04.2022 18:42понял. спасибо
просто обычно в обучающих уроках/видео не упоминают такого. Там просто рассказано как передать или принять или еще какие ни будь простые операции. А вот как делать что то серьезное мало кто пишет..
aamonster
02.04.2022 23:11+2Просто крайне полезно освоить основы до того, как лезть в микроконтроллеры. Ну там, Кнута прочитать, к примеру.
А все эти видео – они по большей части для обезьянок, тем это слишком сложно (собственно, и задача этих видео – не обучить, а почесать ЧСВ автора). Редкие исключения, где и правда чему-то учат, требуют наличия какой-то базы. Ну нельзя впихнуть в получасовую (или сколько там) лекцию не только материал по конкретной задачке, но и два-три семестра обучения, которые нужны для её понимания.
RTFM13
03.04.2022 15:42но для 9600 хватит с головой взять байт из буфера по указателю чтения, отправить в уарт, и увеличить указатель.
если протокол не основан на таймингах, то пауза между байтами ничем не мешает. но так то конечно лучше лишних пауз не делать.
gleb_l
03.04.2022 16:12Он запросто может быть синхронным, или поверх наложен какой-нибудь свой уровень - типа, более 1.5 стоп-битов - утеря арбитража. Автору же нужно имитировать DMA - под этими словами, видимо, имелся в виду скоростной обмен с минимальным участием ЦП ;)
Sekira
02.04.2022 13:10+3Так как полного кода нет, проверьте, что переменные, которые изменяются в прерывании, должны быть помечены как volatile, например uart0_tx_counter.
Serge78rus
02.04.2022 13:19+1Может лучше переформулировать на «переменные, которые используются как в основном потоке, так и в обработчике прерывания». Неважно, где они изменяются, а где используется их значение, просто надо сказать компиляторы, чтобы он при любых оптимизациях не делал никаких предположений о текущем значении переменной и о том, могла ли она измениться или нет, что и делает ключевое слово volatile.
aamonster
02.04.2022 23:15-2Там много приколов с этим volatile, но, боюсь, если объяснять автору, когда нужен volatile, а когда memory barrier (и желательно так, чтобы он смог пользоваться этими знаниями и после перехода с avr на что-то более сложное) – он порвётся.
aamonster
03.04.2022 20:30Для тех, кто не понял: чтобы корректно использовать volatile – вообще говоря, надо покрывать им реально всё, что используется в прерывании (например, сам массив буфера). И, во-первых, про это обычно забывают (делая volatile только флаги, указатели, счётчики, но не сами данные), во-вторых, это ломает оптимизацию.
Конечно, avr-gcc многое простит), но imho если уж писать код, который по стандарту UB, а в реальности правильно работает в конкретном компиляторе – то делать это осознанно и прокомментировав тонкости. Но лучше, конечно, писать по стандарту, а то случаются проблемы (классический пример для C++ – проверка "if (this)" в статическом методе, которая широко использовалась под msvc, но gcc и clang емнип её выпиливают с warning и дальнейшим возможным обращением по нулевому адресу)
SpiderEkb
02.04.2022 14:08Честно скажу, под AVR не писал. Но в свое время приходилось писать много подобных вещей под DOS (в т.ч. и для промконтроллеров с ROM-DOS). Именно на прерываниях.
Непонятно почему не используется прерывание по отправка байта. Самый просто вариант - есть буфер, есть указатель на байт к отправке в буфере, есть счетчик (оставшихся байт) или символ-терминатор (который не посылается а только служит индикатором конца отправки) в буфере.
А дальше все просто - заполнили буфер, установили указатель на второй байт в буфере и отправили первый. А дальше оно "само себя вытянет" - после отправки первого символа возникнет прерывание, обработчик его проверит что счетчик не нулевой или указатель указывает не на символ-терминатор, пошлет байт по указателю, передвинет указатель на следующий байт в буфере (и если есть счетчик, уменьшит его значение). Все.
Если счетчик обнулился или по указатель расположен символ-терминатор, обработчик прерывания просто не посылает очередной байт и процесс завершается.
lamerok
02.04.2022 17:26+1Не взирая ни на что, Просто замечания
Переменные, которые используются в прерывания надо делать volatile
Надо блокировать вызов uart0_send_string, пока предыдущая строкн не отправилась до конца.
Зачем разрешать прерывание по приёму и вообще приём, если вы только в одну сторону отсылаете?
Лучше передавать не просто указатель на char, но и ещё и размер, нет гарантии, что у вас строка с конечным символом будет, это не безопасно. Надо поставить ассерт на то, что длина строки меньше размера буфера передачи и ассерт, что указатель не равен NULL.
Поправить орф. Ошибки, типа caunter - > counter.
Флаг прерывания надо скинуть. Прерывание можно не запрещать.
-
Убрать меджик намберы, типа 20.
И тогда для небольшого домашнего проекта пойдёт, главное никуда в промышленный продукт не прошивать.
Ramzess_II Автор
02.04.2022 18:49спасибо, учту.
интересно какие решения используют в промышленных устройствах? Или это уже сугубо коммерческие решения?
DungeonLords
02.04.2022 22:02Если было пряното n байт, но не целый пакет и более новых данных не приходило в теченит длительного вре при, скажем секунды. Нужно ли очищать входной буфер? Ведь то, что пришла полупосвлка намекает, что на той стороне устройство перезагрузилась? Или как это по-английски говорится, нужно ли делать "3.5 characters between each message"?
aamonster
02.04.2022 23:18Нет общего правила. Какой установите протокол – так и будет.
lamerok
03.04.2022 17:37Буфет просто невалидный, нужно по таймеру скинуть итератор на начало буфера. Т. е. сам буфер очищать смысла нет.
DX168B
03.04.2022 12:59Я нечто подобное реализовал давно уже и таскаю с проекта в проект. Единственное, я сделал один буфер для приема, один для передачи и enum, сигнализирующий о текущем состоянии интерфейса:
1. UartState::FREE - Интерфейс свободен, принятых данных нет
2. UartState::RECEIVING - На данный момент, интерфейс принимает данные
3. UartState::TRANSMITTING - На данный момент, интерфейс передает данные
4. UartState::RECEIVED_DATA - Прием данных завершен, в буфере приема есть данные
5. UartState::TRANSMITTED_DATA - Передача данных завершена
6. UartState::ERROR - Сигнализирует об ошибке
Окончание приема определяется таймаутом. Как только начинают поступать байты, интерфейс автоматом переходит в состояние RECEIVING. Как данные перестанут передаваться, по истечению таймаута или если переполнится буфер, переход в RECEIVED_DATA. После обработки принятых данных, нужно вызвать функцию, которая переведет интерфейс в состояние FREE.
С передачей данных все то же самое.
А далее пишется автомат, который будет обслуживать интерфейс и реализует протокол обмена.
Hidden text
////////////////////////////////////////////////////////////////////////// // Процесс обработки интерфейса void CUart::Process() { if(uartState == UartState::RECEIVING) // Если запущен сеанс приема { if(rxTimer.CheckTimer() == true) // Проверяем таймаут. Если истек, то ... { if(uartRxCount == 0) // На всякий случай: Если счетчик принятых байтов равен нулю { uartState = UartState::FREE; // UART свободен } else { uartState = UartState::RECEIVED_DATA; // Иначе, выставляем флаг "Есть принятые данные" UCSR0B = 0; // Блокируем UART полностью } } } } /////////////////////////////////////////////////////////////////////// // Process void CProto::Process() { // UART process CUart::Process(); // If data is received if(CUart::GetStatus() == UartState::RECEIVED_DATA) { ProtoPackHead* pHead = (ProtoPackHead*)rxBuff; // Packets to device if(pHead->recipientAddress == deviceAddress) { // Check CRC uint8_t rxSize = CUart::GetRxCount() - 1; if(rxBuff[rxSize] != Crc8Calc(rxBuff, rxSize)) { // "Bad CRC" response dataBuff[0] = PROTO_RESP_BAD_CRC; SendResponse(PROTO_CMD_ERR_CODE, 1); } else { // Packet is good ProcessUnicastPacket(pHead); } } // Broadcast packets else if(pHead->recipientAddress == PROTO_BROADCAST_ADDRESS) { // Check CRC uint8_t rxSize = CUart::GetRxCount() - 1; if(rxBuff[rxSize] == Crc8Calc(rxBuff, rxSize)) { // Packet is good ProcessBroadcastPacket(pHead); } CUart::ResetState(); } // Ignore unrecognized packets else { CUart::ResetState(); } } // If data is transmitted if(CUart::GetStatus() == UartState::TRANSMITTED_DATA) { CUart::ResetState(); } }
В основном цикле постоянно вызываем метод CProto::Process(); и он уже сам делает всю "магию" обмена по конкретному протоколу. Главное, чтобы главный цикл постоянно "крутился". Для этого нужно всю программу продумывать так, чтобы она нигде не висела в ожидании чего-то. Реализовать всю программу по принципу конечного автомата. Или разбить на задачи, а уже задачи реализовать в виде автоматов. Разбить алгоритм задачи, где есть места, требующие ожиданий чего-то, на этапы выполнения. Типа, "Этап 1" - делаем что-то, что потом требует ожидания и переключаем состояние на "Этап 2". Выходим из автомата и при следующем обороте цикла попадаем в "Этап 2", где проверяем, не выполнилось ли действие или не истек ли таймаут. Если нет, то остаемся на этом же этапе и выходим. Если да, то что-то делаем и переключаемся на другие этапы. Для реализации задержек нужно использовать один из аппаратных таймеров. Таким образом главный цикл всегда в работе, нигде не стопорится и успевает обрабатывать все задачи.
Пример автомата, обслуживающего дисплей HD44780, подключенный к МК через I2C расширитель. (между прочим, на той же шине сидит еще и чип RTC, что не мешает им уживаться вместе, так как у драйвера I2C тоже есть флаги состояний)
Hidden text
/////////////////////////////////////////////////////////////////////// // Display update process bool CHD44780::Process() { // Stage 1 - send upstring address command if((procState == DispProcState::FREE) && (CI2Cbus::GetState() == I2Cstate::FREE)) { dispBuffIndex = 0; WriteCmd(HD44780_CMD_DDRAM | HD44780_ADDR_1STR); // Set DDRAM pointer to up string procState = DispProcState::DDRAM_CMD_WAIT; return false; } // Stage 2 - is command was sended, then start command delay if(procState == DispProcState::DDRAM_CMD_WAIT) { if(CI2Cbus::IsAvailableAndNotReset() == true) { // Set timer for command delay cmdTimer.SetTimer(HD44780_COMMANDS_DELAY_MS); // delay procState = DispProcState::DDRAM_CMD_DELAY; return false; } else { // Wait for command send procState = DispProcState::DDRAM_CMD_WAIT; return false; } } // Stage 3 - wait command delay if(procState == DispProcState::DDRAM_CMD_DELAY) { if(cmdTimer.CheckTimer() == true) { // Delay expired, send display buffer procState = DispProcState::WRITE_BUFFER; return false; } else { // Delay is not expired, wait... procState = DispProcState::DDRAM_CMD_DELAY; return false; } } // Stage 4 - write data if(procState == DispProcState::WRITE_BUFFER) { WriteData(dispBuff[dispBuffIndex]); dispBuffIndex++; procState = DispProcState::WRITE_BUFFER_WAIT; return false; } // Stage 5 - write data wait if(procState == DispProcState::WRITE_BUFFER_WAIT) { if(CI2Cbus::IsAvailableAndNotReset() == true) { if(dispBuffIndex == (HD44780_DISPLAY_BUFFER_SIZE / 2)) { // To down string WriteCmd(HD44780_CMD_DDRAM | HD44780_ADDR_2STR); // Set DDRAM pointer to down string procState = DispProcState::DDRAM_CMD_WAIT; return false; } else if(dispBuffIndex > (HD44780_DISPLAY_BUFFER_SIZE - 1)) { // Write end CI2Cbus::ResetState(); dispBuffIndex = 0; procState = DispProcState::FREE; return true; } else { // Write next symbol procState = DispProcState::WRITE_BUFFER; return false; } } else { procState = DispProcState::WRITE_BUFFER_WAIT; return false; } } return true; }
srg27y
04.04.2022 11:27Как известно у них не очень большие скорости 16 МГц, у тех же STM32 можно гнать 72МГц и выше.
по этой фразе легко определить что автор молод и неопытен.
интересно, а смог бы он на каком нибудь PIC12F509 контроллер для RGB ленты с управлением от IR написать?
Serge78rus
Если счетчик отправленных байт сделать не возрастающим, а убывающим, то можно его использовать для хранения длины строки для передачи: в прерывании передаем очередной символ и делаем декремент счетчика, пока он не обнулится. Это позволит избавиться от использования специальных символов конца строки. Правда, придется завести еще одну переменную — указатель на очередной символ в буфере для передачи.
П.С. А причем здесь DMA? Это же классическая работа по прерываниям.
Ramzess_II Автор
шикарная идея, спасибо)
DMA просто привел как аналогию..