Помощь - Поиск - Пользователи - Календарь
Полная версия этой страницы: Как писать хороший код
Форум разработчиков электроники ELECTRONIX.ru > Сайт и форум > В помощь начинающему > Программирование
amaora
Не могу выбрать подходящий способ написания кода для своих задач. Хочется обойтись без стороннего кода и без лишней сложности.

Сейчас пытаюсь делать всю работу требующую немедленного реагирования в прерываниях, без дополнительных слоев абстракции. Но есть одна медленная задача, это интерфейс командной строки. Эта задача выполняется в главном цикле, в единственном контексте который может вызывать блокирующие функции т.к. вытесняющего планировщика и множества нитей нет. Пока единственная блокирующая функция это отправка символа в USART через несколько буферов. Блокировка выполняется в виде цикла ожидания в котором вызывается "планировщик". Последний может выполнять еще какие-то задачи или перевести МК в режим простоя. Еще какие-то задачи это переброс данных между USART и FIFO структурами с которыми удобно работать задаче интерфейса командной строки. То есть если выполнилась какая-то команда которая вызвала printf с очень длинным текстом который забил весь буфер, то putc вызванный внутри printf встанет на цикле ожидания и будет вызывать задачи которые могут вычистить выходной буфер. Для этого соответственно, те задачи работающие с FIFO не должны и не могут ни на чем блокироваться, что создает некоторые неприятности в их реализации.

Дальше возникают вопросы эффективности такого метода, сейчас все задачи запускаются по каждому поводу. То есть по каждому выходу из режима простоя, который происходит на каждом прерывании. Можно обложить все задачи флагами которые выставлять в тех местах где образуется работа для соответствующей задачи. Или сделать динамическую очередь задач, чтобы не проверять кучу флагов. Несколько раз уже переписывать код по разному, обдумывал разные варианты. Но хорошего не приходит, всегда что-то не так. Например, очередь потребует нетривиальной реализации, чтобы можно было добавлять в нее задачи из прерываний. А запрета этих прерываний нужно избежать. Вопросы так же возникают по поводу того, что делать если понадобится вторая задача которая тоже захочет блокироваться.

Как сделать хорошо и без вытесняющего переключения задач? smile3046.gif
_pv
Protothreads?
не то чтобы совсем хорошо, но зато без вытесняющего переключения
amaora
Цитата(_pv @ Dec 17 2014, 21:09) *
Protothreads?
не то чтобы совсем хорошо, но зато без вытесняющего переключения


Это только в пределах одной функции? А если у меня такой стек shTask->shEval->shExactMatch->someCMD->printf->putc->shSend ? И на месте someCMD может быть одна из многих команд.
menzoda
Стандартное решение для простых задач:
- Расчеты реального времени в прерываниях (ШИМ, модуляция, фильтрация и т.п.).
- В фоне "super loop" с асинхронными вызовами.
- Сведенный к минимуму функционал обслуживания асинхронных вызовов тоже в прерываниях.
_pv
Цитата(amaora @ Dec 18 2014, 00:32) *
Это только в пределах одной функции? А если у меня такой стек shTask->shEval->shExactMatch->someCMD->printf->putc->shSend ? И на месте someCMD может быть одна из многих команд.

что в пределах одной функции? стэк чего?
protothreads это, грубо говоря, тупо goto, очень красиво обёрнутные в макросы препроцессора С, которые позволяют писать код так, как будто это почти настоящие независимые потоки
http://dunkels.com/adam/pt/expansion.html
при этом когда у одного "потока", например shSend, который данные в буфер uartа складывает вдруг закончится буфер и ему надо ждать, в этом месте макрос незаметно сделает возврат из функции обратно в главный цикл, там поделается еще что-то следующее, а когда управление по циклу опять дойдёт до функции shSend, то он вернётся в то место где проверялась занятость буфера и продолжит как будто никуда и не отходил. вместо того чтобы тупо сидеть и ждать когда освободится буфер.
=AK=
Цитата(amaora @ Dec 18 2014, 04:29) *
Как сделать хорошо и без вытесняющего переключения задач?

Сделать кооперативную ось. Благо она делается на чистом С (без ассемблера) и занимает шиш да кумыш места в памяти.
amaora
Цитата(_pv @ Dec 18 2014, 12:55) *
что в пределах одной функции? стэк чего?
protothreads это, грубо говоря, тупо goto, очень красиво обёрнутные в макросы препроцессора С, которые позволяют писать код так, как будто это почти настоящие независимые потоки
http://dunkels.com/adam/pt/expansion.html
при этом когда у одного "потока", например shSend, который данные в буфер uartа складывает вдруг закончится буфер и ему надо ждать, в этом месте макрос незаметно сделает возврат из функции обратно в главный цикл, там поделается еще что-то следующее, а когда управление по циклу опять дойдёт до функции shSend, то он вернётся в то место где проверялась занятость буфера и продолжит как будто никуда и не отходил. вместо того чтобы тупо сидеть и ждать когда освободится буфер.


Полный контекст не сохраняется, то есть хранить что-то в локальных переменных в одной из функций в той цепочке уже нельзя себе позволить. Да и как это будет работать если есть стек вызовов, а не одна функция как в примерах по ссылке, я не представляю.

Мне видится, что лучше привести весь код задачи с блокировкой в автоматный вид, чем скрываться за макросами с неявными case/goto.

Цитата
Сделать кооперативную ось. Благо она делается на чистом С (без ассемблера) и занимает шиш да кумыш места в памяти.


Имею привычку делать свою libc когда пишу код для МК, и у меня нет setjmp/longjmp. Никогда ими не пользовался и возможно неверно понимаю, что они должны делать. Почитаю еще, непонятно что будет если "прыгнуть" в то место из которого уже вышли и соответствующий фрейм стека уже освобожден и возможно затерт.

Пока остановился на глобальном цикле и флагах задач которые выставляются в прерываниях или других задачах. Во время блокировки в shSend происходит вложенный вызов этого же цикла но уже с запретом повторного (рекурсивного) выполнения заблокированной задачи. Но так можно позволить делать только одной задаче, иначе возможны взаимоблокировки. И что-то сомневаюсь как сделать например текстовый вывод по запросу из прерывания, нужна еще одна задача которая сможет блокироваться на shSend.
HardEgor
Цитата(amaora @ Dec 17 2014, 23:59) *
Как сделать хорошо и без вытесняющего переключения задач?

А почему не использовать разный приоритет для прерываний?
У меня прерывания работы с железом(обработка различных сигналов) имеют высокий приоритет, а так же есть терминалка которая срабатывает по прерыванию от UART - принимает по одному символу, последовательно анализирует и выполняет какие-то команды, что-то назад отправляет. Но так как приоритет прерывания UART самый низкий, он никому не мешает.
_Pasha
я вообще без оберток пишу либо
Код
void *pc;
//
if pc != NULL goto *pc;

либо смешанные стейт машины, где переменная у switch(pc)
принимает не только безымянные значения с помощью __LINE__ и такой-то матери
но и предопределенные, и стейт машину можно без гемора переключить в другое состояние.
При этом, выполнение прерываний на стейт машинах это тоже часть задумки.
Красиво, безобразно и единообразно sm.gif
В процессе эксплуатации прототред-лайк сопрограмм юзер-программист sm.gif сталкивается с соблазном запилить
абсолютно все прототредами либо сильно извращаться на длинных вычислениях.
так я Вам скажу, делать этого абсолютно не нужно, если у Вас есть какая-то приоритетная часть - ее можно из прерывания по таймеру вызвать.
все решаемо, в общем. И у такой программно-аппаратной РТОС хорошие характеристики.

Кроме того, волшебные коллбеки повышают универсальность написанного.
=AK=
Цитата(amaora @ Dec 18 2014, 23:35) *
непонятно что будет если "прыгнуть" в то место из которого уже вышли и соответствующий фрейм стека уже освобожден и возможно затерт.

Ну так именно для этого и используются библиотечные функции: setjmp сохраняет контекст и возвращает управление ядру, а longjmp восстанавливает контекст и прыгает в задачу, в то самое место, из которого задача возвращала управление ядру. А ядро в основном занято тем, что отсчитывает для каждой задачи задержки между моментом возврата в ядро и моментом, когда снова можно передавать управление задаче. При написании кода кажется, что каждая задача работает сама по себе, а команда DELAY(X) просто задает задержку X, на время которой задача останавливается.
jhm
Цитата
Дальше возникают вопросы эффективности такого метода, сейчас все задачи запускаются по каждому поводу. То есть по каждому выходу из режима простоя, который происходит на каждом прерывании. Можно обложить все задачи флагами которые выставлять в тех местах где образуется работа для соответствующей задачи. Или сделать динамическую очередь задач, чтобы не проверять кучу флагов. Несколько раз уже переписывать код по разному, обдумывал разные варианты. Но хорошего не приходит, всегда что-то не так.

Зачастую более простое и прогнозируемое решение лучше чем самое эффективное
Я делаю вызов всех задач по кругу. Внутри задач стоят таймера на периодичность вызова. В результате задача сама решает - пришло время работать или сделать return. Просто и эффективно, не нужны сложные планировщики.

Цитата
Например, очередь потребует нетривиальной реализации, чтобы можно было добавлять в нее задачи из прерываний. А запрета этих прерываний нужно избежать. Вопросы так же возникают по поводу того, что делать если понадобится вторая задача которая тоже захочет блокироваться.

решил такую проблему двухуровневыми прерываниями:
ISR1 - высший приоритет - не блокируется никогда, выполняет только самое необходимое, и затем вызывает ISR2
ISR2 - записывает данные в FIFO, и может блокироваться из main когда там идет чтение FIFO.
Make_Pic
Цитата(_Pasha @ Dec 18 2014, 22:11) *
я вообще без оберток пишу либо
Код
void *pc;
//
if pc != NULL goto *pc;

либо смешанные стейт машины, где переменная у switch(pc)
...
Кроме того, волшебные коллбеки повышают универсальность написанного.

А можно пример вашей машины с применением callback функций?
Dog Pawlowa
Цитата(amaora @ Dec 17 2014, 20:59) *
если выполнилась какая-то команда которая вызвала printf с очень длинным текстом который забил весь буфер, то putc вызванный внутри printf встанет на цикле ожидания и будет вызывать задачи которые могут вычистить выходной буфер.


Проблему нужно решать в месте ее появления, в данном случае не допускать запуска printf с "ооочень длинным текстом".
Получится стандартная логика иерархии - если одна задача/автомат не может разрулить свою работу, она не должна спрашивать разрешения у нижнего уровня, а этой задачей должен рулить вышестоящий уровень.
_Pasha
Цитата(Make_Pic @ Sep 25 2015, 10:14) *
А можно пример вашей машины с применением callback функций?

Да там собственно и показывать нечего.
Предположим у нас есть грубо говоря - драйвер, он чего-то делает по SPI, но это "что-то" состоит из команды, статуса и блока данных
У нас есть одна и та же функция, protothread - like для того, чтобы:
1. Выбрать CS ведомого, пнуть spi, послав команду - выполняется из основного потока
2. Забрать статус - выполняется уже из прерывания. Тут же настраивается DMA на блок данных
3. Коллбэк из прерывания TCIF DMA - вызывает абсолютно ту же функцию, которая выставляет флаги готовности и выключает CS ведомого.

Но если нужно - он может вызвать того, кто обработает результат. Это тоже несложно, если позаботиться о механизмах остановки и возобновления задач. То бишь активные задачи у нас в списке карусели, а неактивные - могут вызываться откуда угодно - либо на один шаг либо с восстановлением в списке активных.

В итоге получается, что текст пестрит "командами" типа yield, sleep, wake - но читается как линейный код.

CODE

void *thread(void *pc)
{
if(pc != NULL) goto *pc;
if(pending == NULL) return NULL;
// transaction start
slave_CS(0);
// put the 1 st byte (command)
slave_put(pending->command);
if(!pending->block)
{//non-blocking mode
spi_enable_rx_buffer_not_empty_interrupt(spi.port);
if(pending->size)
{//setup dma for data block
provide_dma();
return &&Get_Status;
}
else
{// transaction end
return &&Zero_Data;
}
}
else
{// blocking
my_wait(SPI_SR_RXNE);
}
Get_Status:
pending->status = slave_get();
if(pending->block)
{// block mode
char *ptr = pending->data;
while(pending->size)
{
pending->size -= 1;
slave_put(pending->read? 0xFF: *ptr++);
my_wait(SPI_SR_RXNE);
if(pending->read)
{
*ptr++ = slave_get();
}
}
goto Trans_END;
}
/*state mashine for non-block*/
spi_disable_rx_buffer_not_empty_interrupt(spi.port);
/*enable dma*/
if(pending->read) spi_enable_rx_dma(spi.port);
spi_enable_tx_dma(spi.port);
return &&Trans_END_Irq;
Trans_END_Irq:
if(pending->read) spi_disable_rx_dma(spi.port);
spi_disable_tx_dma(spi.port);
Trans_END:
slave_CS(1);//close
if(pending->chain_CB != NULL) pending->chain_CB();
pending = NULL; // trans. end
status_unread=1; // got status
return NULL;

Zero_Data:
pending->status = slave_get();
spi_disable_rx_buffer_not_empty_interrupt(spi.port);
goto Trans_END;
}

вот. а вызовы - разбросаны по всему тексту.
кроме того, там же есть и неблокирующий вариант - в зависимости от выставленного флага pending->block
единственно, усложнение - тогда, когда в жесткую стейт-машину возможны вклинивания "сторонних сил" sm.gif
там нужно предусмотреть чтобы вызов шел с конкретным указателем.
Make_Pic
Мне очень понравился ваш подход к написанию кода, но из вырванного куска кода из контекста, с моим уровнем знаний сложно разобраться - Решил вернуться к вашему примеру - pls можно более подробнее описание вашей псевдооперационки с объявления и описанием переменных, функций, прототипов.

Если не трудно, хотя бы на коротком примере?
_Pasha
я в личку отвечу сейчас.
DASM
И года не прошло))
Njalenoc
код, который согласован со стандартом
Smoky
Коллеги, допустимо ли использовать функции таким образом?
Объявлена функция:

// Передаёт и принимает 1 байт по SPI, возвращает полученное значение
uint8_t spi_send_recv(uint8_t data) {
SPDR = data;
while (!(SPSR & (1 << SPIF)));
return SPDR;
}

а используется без возврата значения:

// Записывает значение однобайтового регистра reg (от 0 до 31), возвращает регистр статуса
uint8_t radio_writereg(uint8_t reg, uint8_t val) {
csn_assert();
uint8_t status = spi_send_recv((reg & 31) | W_REGISTER);
spi_send_recv(val);
csn_deassert();
return status;
Сергей Борщ
QUOTE (Smoky @ Dec 18 2016, 14:40) *
допустимо ли
Да. Что вас смущает? И что мешает попробовать откомпилить?

P.S. printf() тоже значение возвращает, но не попадалось ни одного примера, где это значение использовалось бы.
Smoky
Цитата(Сергей Борщ @ Dec 18 2016, 18:53) *
Да. Что вас смущает? И что мешает попробовать откомпилить?


До компиляции ещё далеко, я только изучаю, как это делается. Использование функций таким образом встречаю впервые...
zltigo
Цитата(Smoky @ Dec 18 2016, 15:03) *
Использование функций таким образом встречаю впервые...

Этого не может быть, если только это не вообще первая функция, которую Вы увидели sm.gif
Smoky
Цитата(zltigo @ Dec 18 2016, 19:29) *
Этого не может быть, если только это не вообще первая функция, которую Вы увидели sm.gif


Си начал использовать с 2010 года, осваиваю самостоятельно но честное слово, такое использование функций не встречал. С самого начала учился по учебнику Бредли Л.Джонса и Питера Эйткена, там всё "жёстко", такой вольности нет. И всё же интересно, куда "улетает" возвращаемое значение функции которое не ждут?
zltigo
Цитата(Smoky @ Dec 18 2016, 16:53) *
Си начал использовать с 2010 года, осваиваю самостоятельно но честное слово, такое использование функций не встречал. С самого начала учился по учебнику Бредли Л.Джонса и Питера Эйткена, там всё "жёстко", такой вольности нет. И всё же интересно, куда "улетает" возвращаемое значение функции которое не ждут?

Никуда не улетает. Возвращаемое значение в регистре находится. Хочешь пользуй, хочешь нет. В случае функции "ничего" не возвращающей, там наверняка мусор, а так некоторое определенное значение. Вот и вся разница sm.gif
Smoky
Цитата(zltigo @ Dec 18 2016, 21:01) *
Никуда не улетает. Возвращаемое значение в регистре находится. Хочешь пользуй, хочешь нет. В случае функции "ничего" не возвращающей, там наверняка мусор, а так некоторое определенное значение. Вот и вся разница sm.gif


Спасибо за разъяснение, буду использовать эту возможность.
AlexandrY
Цитата(Smoky @ Dec 18 2016, 15:03) *
До компиляции ещё далеко, я только изучаю, как это делается. Использование функций таким образом встречаю впервые...


Я тоже, честно говоря.
Обычно явно задают некую переменную куда читают из SPI.

Иначе оптимизатор может выкинуть операцию чтения и будете неделями искать откуда в SPI лишние данные берутся.
DASM
Господи, обычный side effect.. железячники одни собрались что ли?

Цитата(AlexandrY @ Dec 18 2016, 21:16) *
Я тоже, честно говоря.
Обычно явно задают некую переменную куда читают из SPI.

Иначе оптимизатор может выкинуть операцию чтения и будете неделями искать откуда в SPI лишние данные берутся.

Неиспользуемую дальше переменную он выкинет куда быстрее. Тут он ничего не выкинет, все регистры объявлены volatile , реже прагмами
И тут даже к стилю не придраться, в названии все сказано, так более чем можно писать. Даже нужно по смыслу, чтение спи это всегда запись, дажее если запись не имеет смысла. А две отдельные функции тут будут источником глюков, особенно если юнит тест на одной. Разве что можно рид сделать оберткой send_recv
zltigo
Цитата(AlexandrY @ Dec 18 2016, 20:16) *
Иначе оптимизатор может выкинуть операцию чтения и будете неделями искать откуда в SPI лишние данные берутся.

Невежество sad.gif. Ни при каких обстоятельсвах обращение volatilе переменной не может быть выброшено.
Цитата
Обычно явно задают некую переменную куда читают из SPI.

Даже просто прочитать в никуда можно и нужно, так:
Код
SPDR;
DASM
Вы наконец то стали говорить вещи с которыми я согласен sm.gif
Или я стал мыслить иначе bb-offtopic.gif
AlexandrY
Цитата(DASM @ Dec 18 2016, 22:47) *
Вы наконец то стали говорить вещи с которыми я согласен sm.gif
Или я стал мыслить иначе bb-offtopic.gif


Ладно, ради такой дружбы признаю свою ошибку. Юзайте volatile и будьте счастливы! biggrin.gif
Для просмотра полной версии этой страницы, пожалуйста, пройдите по ссылке.
Invision Power Board © 2001-2025 Invision Power Services, Inc.