Всем привет. Довелось мне писать довольно большой проект на 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)


  1. Serge78rus
    02.04.2022 12:35
    +16

    Если счетчик отправленных байт сделать не возрастающим, а убывающим, то можно его использовать для хранения длины строки для передачи: в прерывании передаем очередной символ и делаем декремент счетчика, пока он не обнулится. Это позволит избавиться от использования специальных символов конца строки. Правда, придется завести еще одну переменную — указатель на очередной символ в буфере для передачи.

    П.С. А причем здесь DMA? Это же классическая работа по прерываниям.


    1. Ramzess_II Автор
      02.04.2022 13:23
      -6

      шикарная идея, спасибо)

      DMA просто привел как аналогию..


  1. gleb_l
    02.04.2022 12:36
    +22

    Извините, уважаемый Рамзес, но если Вы не понимаете, как работают прерывания в SoC-системах, лучше не изобретать велосипед, а отставить в сторону этот “большой проект на AVR”, разобраться с прерываниями, и только потом продолжить.

    Это самое политкорректное, что можно сказать по этому поводу.


    1. Ramzess_II Автор
      02.04.2022 13:22

      что именно вы имеете ввиду? можно вкратце?


      1. gleb_l
        02.04.2022 17:20
        +11

        Такие задачи типично решаются одним буфером и двумя указателями - по одному пишем в буфер, по другому - достаём и отправляем. Чтобы не возиться с переносами, в микросистемах буфер делается а) кольцевым, б) с длиной, кратной степени двойки - угадайте, зачем ;). Если у системы есть аппаратный уарт, обычно на передачу у него как минимум два регистра - буфер текущего передаваемого байта и защёлка следующего. Прерывание может генерироваться по опустошению второго, а может и по последнему биту первого - зависит от аппаратной реализации. В последнем случае у вас мало времени ;), но для 9600 хватит с головой взять байт из буфера по указателю чтения, отправить в уарт, и увеличить указатель. Осмыслить все это неспеша, прочитав спеку, и опционально построив прототип на эмуляторе, крайне желательно перед тем, как строить саму большую систему.

        PS - я видел прекрасный пример обмена между двумя МК, при котором разработчик наивно полагал, что приёмник магически знает, когда передатчику вздумается начать с ним обмен ;)


        1. Ramzess_II Автор
          02.04.2022 18:42

          понял. спасибо

          просто обычно в обучающих уроках/видео не упоминают такого. Там просто рассказано как передать или принять или еще какие ни будь простые операции. А вот как делать что то серьезное мало кто пишет..


          1. aamonster
            02.04.2022 23:11
            +2

            Просто крайне полезно освоить основы до того, как лезть в микроконтроллеры. Ну там, Кнута прочитать, к примеру.

            А все эти видео – они по большей части для обезьянок, тем это слишком сложно (собственно, и задача этих видео – не обучить, а почесать ЧСВ автора). Редкие исключения, где и правда чему-то учат, требуют наличия какой-то базы. Ну нельзя впихнуть в получасовую (или сколько там) лекцию не только материал по конкретной задачке, но и два-три семестра обучения, которые нужны для её понимания.


        1. RTFM13
          03.04.2022 15:42

          но для 9600 хватит с головой взять байт из буфера по указателю чтения, отправить в уарт, и увеличить указатель.

          если протокол не основан на таймингах, то пауза между байтами ничем не мешает. но так то конечно лучше лишних пауз не делать.


          1. gleb_l
            03.04.2022 16:12

            Он запросто может быть синхронным, или поверх наложен какой-нибудь свой уровень - типа, более 1.5 стоп-битов - утеря арбитража. Автору же нужно имитировать DMA - под этими словами, видимо, имелся в виду скоростной обмен с минимальным участием ЦП ;)


  1. Sekira
    02.04.2022 13:10
    +3

    Так как полного кода нет, проверьте, что переменные, которые изменяются в прерывании, должны быть помечены как volatile, например uart0_tx_counter.


    1. Serge78rus
      02.04.2022 13:19
      +1

      Может лучше переформулировать на «переменные, которые используются как в основном потоке, так и в обработчике прерывания». Неважно, где они изменяются, а где используется их значение, просто надо сказать компиляторы, чтобы он при любых оптимизациях не делал никаких предположений о текущем значении переменной и о том, могла ли она измениться или нет, что и делает ключевое слово volatile.


      1. aamonster
        02.04.2022 23:15
        -2

        Там много приколов с этим volatile, но, боюсь, если объяснять автору, когда нужен volatile, а когда memory barrier (и желательно так, чтобы он смог пользоваться этими знаниями и после перехода с avr на что-то более сложное) – он порвётся.


        1. aamonster
          03.04.2022 20:30

          Для тех, кто не понял: чтобы корректно использовать volatile – вообще говоря, надо покрывать им реально всё, что используется в прерывании (например, сам массив буфера). И, во-первых, про это обычно забывают (делая volatile только флаги, указатели, счётчики, но не сами данные), во-вторых, это ломает оптимизацию.

          Конечно, avr-gcc многое простит), но imho если уж писать код, который по стандарту UB, а в реальности правильно работает в конкретном компиляторе – то делать это осознанно и прокомментировав тонкости. Но лучше, конечно, писать по стандарту, а то случаются проблемы (классический пример для C++ – проверка "if (this)" в статическом методе, которая широко использовалась под msvc, но gcc и clang емнип её выпиливают с warning и дальнейшим возможным обращением по нулевому адресу)


  1. Bobovor
    02.04.2022 13:11
    +3

    Автор, почитайте про кольцевые буферы. Будет полезно.


    1. Ramzess_II Автор
      02.04.2022 13:24

      хорошо, спасибо


  1. SpiderEkb
    02.04.2022 14:08

    Честно скажу, под AVR не писал. Но в свое время приходилось писать много подобных вещей под DOS (в т.ч. и для промконтроллеров с ROM-DOS). Именно на прерываниях.

    Непонятно почему не используется прерывание по отправка байта. Самый просто вариант - есть буфер, есть указатель на байт к отправке в буфере, есть счетчик (оставшихся байт) или символ-терминатор (который не посылается а только служит индикатором конца отправки) в буфере.

    А дальше все просто - заполнили буфер, установили указатель на второй байт в буфере и отправили первый. А дальше оно "само себя вытянет" - после отправки первого символа возникнет прерывание, обработчик его проверит что счетчик не нулевой или указатель указывает не на символ-терминатор, пошлет байт по указателю, передвинет указатель на следующий байт в буфере (и если есть счетчик, уменьшит его значение). Все.

    Если счетчик обнулился или по указатель расположен символ-терминатор, обработчик прерывания просто не посылает очередной байт и процесс завершается.


    1. Ramzess_II Автор
      02.04.2022 14:18

      так тут точно так же)


  1. lamerok
    02.04.2022 17:26
    +1

    Не взирая ни на что, Просто замечания

    1. Переменные, которые используются в прерывания надо делать volatile

    2. Надо блокировать вызов uart0_send_string, пока предыдущая строкн не отправилась до конца.

    3. Зачем разрешать прерывание по приёму и вообще приём, если вы только в одну сторону отсылаете?

    4. Лучше передавать не просто указатель на char, но и ещё и размер, нет гарантии, что у вас строка с конечным символом будет, это не безопасно. Надо поставить ассерт на то, что длина строки меньше размера буфера передачи и ассерт, что указатель не равен NULL.

    5. Поправить орф. Ошибки, типа caunter - > counter.

    6. Флаг прерывания надо скинуть. Прерывание можно не запрещать.

    7. Убрать меджик намберы, типа 20.

      И тогда для небольшого домашнего проекта пойдёт, главное никуда в промышленный продукт не прошивать.


    1. Ramzess_II Автор
      02.04.2022 18:49

      спасибо, учту.

      интересно какие решения используют в промышленных устройствах? Или это уже сугубо коммерческие решения?


    1. DungeonLords
      02.04.2022 22:02

      Если было пряното n байт, но не целый пакет и более новых данных не приходило в теченит длительного вре при, скажем секунды. Нужно ли очищать входной буфер? Ведь то, что пришла полупосвлка намекает, что на той стороне устройство перезагрузилась? Или как это по-английски говорится, нужно ли делать "3.5 characters between each message"?


      1. aamonster
        02.04.2022 23:18

        Нет общего правила. Какой установите протокол – так и будет.


        1. DungeonLords
          02.04.2022 23:23

          Правило всегда одно: чтоб работало и не стоило. Нет?


          1. aamonster
            03.04.2022 09:57

            Это очень общее правило, а тому, как его выполнить, посвящена вся CS)


      1. lamerok
        03.04.2022 17:37

        Буфет просто невалидный, нужно по таймеру скинуть итератор на начало буфера. Т. е. сам буфер очищать смысла нет.


  1. 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;
    }


  1. srg27y
    04.04.2022 11:27

    Как известно у них не очень большие скорости 16 МГц, у тех же STM32 можно гнать 72МГц и выше.

    по этой фразе легко определить что автор молод и неопытен.

    интересно, а смог бы он на каком нибудь PIC12F509 контроллер для RGB ленты с управлением от IR написать?


    1. Ramzess_II Автор
      05.04.2022 08:59

      скорее нет)