Pull to refresh

Comments 28

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Думаю, кто во что горазд ))) И велосипед у каждого свой )))

У меня почти вся работа с портами строится на прерываниях, и ещё диспетчер задач разруливает некоторые моменты, например тайм-ауты. Протокол передачи похож на Wake, плюс контрольная сумма.

Проект вполне промышленный - централизованный сбор показаний с приборов учёта на заводе. Сторонние библиотеки не использую, пишу свои. Всё на асме, потому что прямо кайф ловлю от этого языка ))

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

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

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

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

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

Я нечто подобное реализовал давно уже и таскаю с проекта в проект. Единственное, я сделал один буфер для приема, один для передачи и 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;
}

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

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

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

Sign up to leave a comment.

Articles