Перейти к содержанию
    

Система, управляемая событиями и SST(super-simple tasker)

Вот эта статья http://embeddedgurus.com/state-space/categ...en-programming/ и картинка с нее

perils_of_blocking-768x432.jpg

меня заразила в свое время переходом с традиционной блокирующей модели к event-driven. По мере отвыкания от блокинга количество тредов стало гораздо меньше, иногда и вовсе один. Со временем появились active-objectы, тк одного треда было не достаточно. И так я плавно двигался от блокирующей модели к неблокирующей. А когда увидел, что блокировок вовсе не осталось, вспомнил, что когда-то читал про некий SST, которому не уделил должного внимания из за привычки к блокингу, и начал с ним эксперименты, что в итоге привело к полному отказу от традиционной RTOS. Таким образом я потерял тайм-кванты, которые и так ни разу не пригодились, разве что в качестве костылей, которых SST-стиль не требует вовсе. Но получил взамен очень много, главное - избавился от кучи багов, накладных расходов и кучи ручной работы.

Код по мере опыта ставал меньше и проще, горбатые конструкции, которые перекочевали из блокирующего стиля были заменены на красивые чисто асинхронные(пример горбатой и нормальной очереди я уже приводил в этой теме).

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Попробую нарисовать задачу, словами:

 

1. Основная часть работы в устройстве делается на прерываниях с высоким приоритетом, с этом вопросов нет, здесь никакая ОС не нужна.

2. Вспомогательная задача обработки текстовых команд приходящих по USART или CAN. Она может вызывать множество разных функций, большинство из которых вызывает printf. Который далее вызывает отправку в драйвер интерфейса (USART или CAN). В этом месте можно заблокироваться, данных выводится много а памяти под буфер мало.

3. Вспомогательная задача низкого приоритета, чисто вычислительная и очень тяжелая (считается ~10 с). Ее не нужно успевать считать в заданное время, достаточно как можно скорее. Из-за нее не должна остановится обработка текстовых команд.

 

Вопрос, как нужно будет написать код функции printf при использовании SST?

 

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Задача отлично ложится на SST. Понадобится неблокирующий printf, о его реализации позже.

enum{
   TASK2_PRIORITY = 2, // higher
   TASK3_PRIORITY = 1, // lower
}

// Собственно задача 2.
class Task2{
public:
Task2();
void operator()(uint8_t ch){	
	parse(ch);
} // можно обрабатывать не только по одному байту, но и сразу все, что есть в очереди, это лишь пример

private:
// это функция обработки, реализуем ее позже
void parse(uint8_t ch);
};

// Определяем очередь uart
struct UartQueue:
// размер нашей очереди - 32 байта. Он зависит от скорости работы порта и времени отработки остальных прерываний.
// Зачастую 16-32 байта вполне достаточно, но можем увеличить, все равно отказавшись от RTOS у нас появилось много памяти
	public UmFifo_spsc_pop<uint8_t,Task2,TASK2_PRIORITY,32>
{
	UartQueue():
		UmFifo_spsc_pop(Task2())
	{}
};

// Создаем очередь
// Данные в эту очередь можно пихать прямо из прерывания UART, в том числе через DMA
UartQueue uart_queue;
// Готово, теперь при поступлении данных Uart(или CAN) в очередь автоматически запустится 
// обработчик Task2::operator() для их обработки.


// Низкоприоритетная задача
// Допустим, мы ее запускаем, когда у нас есть данные для обработки. Работает она 10 секунд и выходит
void run_task3(const Data* data){
SST::postMessage([data](){
	// тут делаем очень тяжелую работу

}, TASK3_PRIORITY);
}

С этим думаю все понятно. Сейчас расскажу про printf.

 

Теперь printf.

Признаюсь, у меня пока нет реализации последовательного printf, я просто накидываю аргументы в очередь, память под которую получил за счет отказа от RTOS, от многостековости :)

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

Тут хорошо бы применить Promise https://promisesaplus.com/ , но я еще не дорос до их нормальной реализации на C++.

Поэтому паттерн будет пока такой:

class Printf{
    Printf();

    Printf& operator,(const PrintfArgument& arg); // этой штукой я заряжаю аргументы в очередь,
// но мы хотим супер-экономии памяти, по этому будем делать по-другому
private:
    bool first; // понадобится позже, для определения первый аргумен(формат) или последующие
};

#define printf(args...) (Printf(), ##args)

В итоге мы сможем писать обычные printf-ы, но с callback-ом в конце:

printf("%d %X\n", 123, 456, [](){
    // все, принтф отработал, можем делать что-то другое
});

Осталось реализовать наш operator, PrintfArgument и конструктор Printf.

продолжение следует.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

А где самое главное, что будет вместо блокирующего putc в функциях печати чисел и строк?

 

По верхнему уровню, где вызывается printf. Что делать, если уровень вложенности становится большим? Так будет если раньше в коде было вот такое:

 

do_something();
wait_for_something();
printf("something ...");

do_something();
wait_for_something();
printf("something ...");

do_something();
wait_for_something();
printf("something ...");

...

 

Изменено пользователем amaora

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

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

Только в очереди будем хранить не сами печатаемые символы, а указатели на строки, числа итд, в общем аргументы функции, их у нас не много.

Тут как-раз рулит Раст, он не даст нам закинуть указатель на стековую строку, например, но мы делаем на плюсах, поэтому будем обходится тем, что есть.

В принципе такая реализация у меня есть. Приведу в удобоваримый вид и покажу.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Ну допустим будем хранить аргументы printf. Проблемы языков C и C++ не рассматриваем.

 

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

 

Как-то стало ясно, ничего иного не ожидал, сам долго думал как обойтись без вытесняющей rtos.

Изменено пользователем amaora

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

И так вот. У нас появилась еще одна задача - printf. Назначим ей приоритет Printf_TASK_PRIORITY=3, тк она отрабатывает очень быстро, но можно и 2.

class PrintfTaskQueue : public TaskQueue{
public:
    PrintfTaskQueue(): TaskQueue(Printf_TASK_PRIORITY){}
};

PrintfTaskQueue printf_queue;

Printf::Printf(){
    printf_queue.enqueueTask([this](){
        first = true;
    });
}

Printf& Printf::operator,(const PrintfArgument& arg){
    printf_queue.enqueueTask(arg);
    return *this;
}

Далее нам останется описать сам PrintfArgument, этим сейчас и займемся.

 

Ну допустим будем хранить аргументы printf. Проблемы языков C и C++ не рассматриваем.

 

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

 

Как-то стало ясно, ничего иного не ожидал, сам долго думал как обойтись без вытесняющей rtos.

Все эти флоаты у РТОС хранятся на стеке, а у нас будут в очереди. Места займут меньше, чем в стеке, это естественно.

 

Блокинг:

do_something();
wait_for_something();
printf("something ...");

do_something();
wait_for_something();
printf("something ...");

do_something();
wait_for_something();
printf("something ...");

Тут либо надо использовать Promise, который реализовать на плюсах не просто, либо просто callback hell(куча вложенних лямбд)

do_something([this](){
    printf("something ...",[this](){
        do_something([this](){
            printf("something ...", [this](){
                do_something([this](){
                    printf("something ...", on_finish);
                });
            });
        });
      });
});

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

Я пока использую hell, или разделение цепей вложенных вызовов на фанкции, чтобы код не разростался вправо. Возможно в будущем удастся реализовать compile-time промисы.

Togda буде что-то типа этого:

Promise(
    do_something_nonblocking()).then(
    printf_nonblocking("something...")).then(
    do_something_else_nonblocking()).then(finish);

Для удобной работы нужна компиляторная языковая поддержка, компиляторы только недавно начали двигаться в асинхронный стиль, новшества С++ 11 хоть как-то помогают работать, но это еще очень слабая поддержка. Возможно у Rust она будет мощнее https://github.com/alexcrichton/futures-rs .

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

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

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

 

Подитожим немного. Sizeof объектов уже растет для сохранения состояния, это мы выяснили. Очереди памяти требуют это мы тоже выяснили. Изобретать надо всё своё, в том числе принтф. А из-за небольшого прикольчика с разным кол-вом и типами аргументов тут придется задуматься... Классно. Продолжаем. Попкорна еще много ))

 

Вам не кажется, что у вас дело дошло до того, что вы реализуете всё на SST только ради того, чтобы реализовать что-нибудь на SST?

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Понеслааась.

Зря вы так ополчились на brag-а. Он говорит весьма интересные вещи. Постарайтесь их вычленить из странных высказываний про JS и Rust:)

Что касаемо обсуждаемой задачи с printf - сравните 2 варианта:

  1. N задач висят в ожидании освобождения устройства печати
  2. N задач поочерёдно быстренько запулили свои сообщения в очередь и завершились.

Думаю, что второй вариант гораздо выигрышнее и по памяти и по быстродействию. К тому же есть отличные, типобезопасные реализации printf на плюсах, которые будет проще сделать неблокирующимися, чем штатный printf.

 

Я почему этой темой заинтересовался. Просто стал обращать внимание, что у меня в проектах с использованием RTOS процессы используют в основном два сценария:

  1. подождал флага - быстро обработал
  2. нечто вроде Active Object (это я сейчас прочитал, что такая техника называется Active Object, до этого не знал, что она так называется).

Судя по описанию, такие проекты очень хорошо должны лечь на SST. Жду примера с мигалкой светодиодом, чтоб пощупать:)

 

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

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

Моя придирчивость это противовес фанатичному переписыванию всего и вся, как предлагает нам пропагандист SST )))

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

sizeof растет, но памяти в целом надо меньше, раз в 8, когда потоков много. А когда парочка - раза в 2 меньше.

printf и так давно свой и не потому что SST, а потому что он типобезопасный, в отличии от стандартного сишного. Сначала у меня был блокирующий, а потом стал неблокирующий printf.

 

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

 

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

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

 

Такой простой принтф пишется за пол дня, не думаю, что это много. Остальной код приведу(класса PrintfArgument), просто сейчас нет времени выдрать и привести в удобоваримый вид. Но многие, уже наверное догадались что там - создание обработчика события и добавление его в очередь.

 

Судя по описанию, такие проекты очень хорошо должны лечь на SST. Жду примера с мигалкой светодиодом, чтоб пощупатьsm.gif

Да, ложится, как влитое, правда к стилю придется привыкнуть ну и местами может быть callback-hell (пирамидка), хотя много где в продакшн-коде он тоже есть, так что это не страшно.

 

Демо-проект будет. Какая платформа предпочтительнее? ПК или какой-то МК? Может демо-плата какая-то стандартная? Помогите определится..

 

Моя придирчивость это противовес фанатичному переписыванию всего и вся, как предлагает нам пропагандист SST )))

Переписывать приходится не только ради SST, но и ради других вкусностей, например типобезопасности или универсальности.

 

Например я недавно функцию sqrt сам написал(извлечение квадратного корня), но она не простая, она шаблонная и понимает как обычные int, так и большие числа (big-integer), до 65536 бит длиной, в том числе и комплексные :) И сделал это не ради того, чтобы сделать, а задолбался от использования кривых и глючных библиотек и постоянного контроля типов.

А рядом с ней есть еще специализировання для float/double, которая завернута на стандартную. Вызвал sqrt для любого типа и готово, да и работает моя реализация быстрее этих кривых сишных поделок, местами раза в 2-4.

 

Я много чего пишу своего ради возможностей, ради компиляторной проверки. Многое оборачиваю в плюсы, если есть что оборачивать. Иногда проще заново написать, чем возится.

Времени хоть и требует немного, зато потом оно возвращается в виде более быстрой разработки проектов.

 

Повторюсь еще раз - блокинг-код тут не работает, если в предполагаемой либе нет async-io, значит на sst она работать не будет. Тут чисто асинхронный стиль в духе NodeJS и др.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

[*] N задач поочерёдно быстренько запулили свои сообщения в очередь и завершились.

Думаю, что второй вариант гораздо выигрышнее и по памяти и по быстродействию. К тому же есть отличные, типобезопасные реализации printf на плюсах, которые будет проще сделать неблокирующимися, чем штатный printf.

 

А не смущает ли то, что ради этой фишки с printf-ом вам надо все! задачи в своей системе сделать без циклов.

Т.е. все задачи должны вызываться как функции и возвращать управление за конечное время.

 

А известно ли уважаемому brag-у что у нас в отрасли встраиваемых систем многие пакеты идут в виде скомпилированных либ с уже реализованными там циклическими задачами.

 

Но я на 100% уверен что, brag не перепишет и обычные открытые стеки TCP или USB где всегда есть циклическая задача на прием.

Эт я еще не упоминаю циклические задачи серверов, сессий, всяких драйверов и т.д.

 

Что интересно, стек то не сильно экономится в этом АКОП-е.

Общий-то он общий, но за то надо думать как поместить в него все стеки задач и прерываний во всех мыслимых комбинациях.

Из-за этого будет сделан наверняка огромный запас, процентально больший чем запас в стеках обычных задач RTOS.

 

Хуже того, если в RTOS стек то можно еще и просто посчитать и прецизионно выделить, то в AKOП-е это уже практически нереализуемо, когда более десятка разнообразных задач.

 

 

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Но я на 100% уверен что, brag не перепишет и обычные открытые стеки TCP или USB где всегда есть циклическая задача на прием.

Эт я еще не упоминаю циклические задачи серверов, сессий, всяких драйверов и т.д.

Это написани давно, циклов там нет. Например в USB данные ловятся в обработчике прерываний без всяких ожиданий, и в конце стека(например CDC) отправляются в очередь пользователю.

Например вот функция чтения с CDC.

uint32_t UsbCdc::read(uint32_t *data32, uint32_t len8, const delegate<void(int)>& rdXfrc){
  // must never happen
    if(!isReady())return Error::NOT_READY;
    if(isReadBusy())return Error::BUSY;

    this->rdXfrc = rdXfrc;

        // добавление задачи в очередь конкретного endpoint
//  По окончанию чтения(фактически это просто прерывание) будет вызвана rdXfrc
    Usb::getInst()->core()->epOutXfr(USB_CDC_EP_OUT,data32,len8, this);
    return 0;
}

Аналогичное и в коде MSC(mass storage) и других классах. Работает изумительно и без тормозов, на RTOS была производительность ниже из за накладных расходов на циклы и переключение контекста.

Код да, не публичный, но какие-то части его публиковать имею право.

 

Задач в сложном проекте не десяток, их там несколько десятков, а то и сотня.

Стек да, с одной стороны считается еще труднее, чем в RTOS, а с другой - время жизни задач очень короткое, вся рекурсия идет все равно через очереди.

Я как обычно делаю: Выбираю самую тяжелую по стеку задачу для каждого уровня приоритета (не вручную ессно, этим занимается софт), потом эти числа суммирую и накидываю сверху процентов 20 запаса.

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

 

Цифры получаются гораздо меньше, чем в аналогичных проектах в блокинг-стиле, там все то, что в SST идет в очереди, в строго определенном формате, в RTOS идет в стеки в таком виде, который вздумается компилятору, и зачастую далеко не оптимальном, с копиями или лишними указателями/ссылками. Время жизни этих стеков велико и стек очень трудно просчитывается, особенно когда куча вложенных виртуальных блокирующих функций.

 

Потом ОЗУ заполняется данными сверху, а стеком снизу. Нет необходимости вообще что-то выделять. Если есть MPU - данные защищаю им, если стек туда долезет - будет глюк, но он залогируется MPU. РТОС по любому больше расходует памяти, тк у SST стек плотный, а у РТОС каждый стек имеет пустоты, и это без учета того, что есть еще TCB и всякие другие конструкции(те же семафоры тоже память занимают), которых в SST нет. В SST есть очереди, но и в РТОС-коде они тоже обычно есть. Без них - это полный ад.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Демо-проект будет. Какая платформа предпочтительнее? ПК или какой-то МК? Может демо-плата какая-то стандартная? Помогите определится..

Так вы же написали, что проект будет под PC. Думаю, что это наиболее приемлемый вариант для всех.

 

А не смущает ли то, что ради этой фишки с printf-ом вам надо все! задачи в своей системе сделать без циклов.

Т.е. все задачи должны вызываться как функции и возвращать управление за конечное время.

Я ведь написал, что у меня сейчас практически так и есть.

Что касаемо либ без исходников, и прочих непреодолимых препятствий - наверное можно сделать гибридный проект. Несколько процессов останутся как есть, а те процессы, которые можно сделать неблокирующимися, свернём в один процесс, и их будет крутить SST.

(Но это будет временное решение, ненадолго, просто до того момента, когда brag перепишет все либы в неблокирующем стиле:)))

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Так вы же написали, что проект будет под PC. Думаю, что это наиболее приемлемый вариант для всех.

Ок, так и сделаем. Кто захочет - запросто под МК можно будет присобачить, без правки кода. Заглушки только поменять на работу с железом

 

Несколько процессов останутся как есть, а те процессы, которые можно сделать неблокирующимися, свернём в один процесс, и их будет крутить SST.

Трудно совместить SST-планировщик с готовой РТОС, хотя, думаю можно(от камня сильно зависит и от самой РТОС). Но можно просто актив-object-ов наклепать - будет асинхронный стиль, как у SST, но можно будет пользоваться и блокинг-тредами - для либ, которые не поддерживают асинк. Цена - много памяти под стеки. Общаться между всем этим только через очереди - никаких shared-переменных и мютексов.

 

(Но это будет временное решение, ненадолго, просто до того момента, когда brag перепишет все либы в неблокирующем стилеsm.gif))

И получит права на их публикацию :) Да куча сейчас либ на самом деле асинхронных или просто независимых от РТОС/блокинга, хотя согласен, есть далеко не все.

 

Для кортекс вряд ли просто так присобачишь SST к РТОС. В виду его архитектуры пришлось использовать PendSV и SVC, да еще и с трюками. Обычно эти исключения уже заняты РТОС, а рекурсивные прерывания в Кортексе просто так не сделаешь.

__attribute__((naked)) void ePendSV(){
    asm volatile(
        // "push {r0,r4-r11, lr} \n" // not necessary, callee saved
        // create new stack frame for scheduler
        "mov  r2, %0    \n" // PSR
        "movw r1, #:lower16:SST_sync_scheduler  \n" // PC
        "movt r1, #:upper16:SST_sync_scheduler  \n"
        "movw r0, #:lower16:async_scheduler_return  \n" // LR
        "movt r0, #:upper16:async_scheduler_return  \n"
        "push {r0, r1, r2} \n" // push {lr, pc, xPSR}
        "sub sp, #5*4       \n" // push {r0,r1,r2,r3,r12} - undefined values
        // return from exception to scheduler
        "bx lr    \n"
    : :"i"(xPSR_RESET_VALUE));
}

// return to preemtded task through SVC
extern "C" __attribute__((naked)) void async_scheduler_return(){
    asm volatile("svc #0");
}

__attribute__((naked)) void eSVCall(){
    asm volatile(
        // kill current stack frame
        "add sp, #8*4 \n"
        // "pop {r4-r11, lr} \n" // not necessary
        // perform an EXC_RETURN (unstacking)
        "bx lr \n"
    );
}

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Присоединяйтесь к обсуждению

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

Гость
К сожалению, ваш контент содержит запрещённые слова. Пожалуйста, отредактируйте контент, чтобы удалить выделенные ниже слова.
Ответить в этой теме...

×   Вставлено с форматированием.   Вставить как обычный текст

  Разрешено использовать не более 75 эмодзи.

×   Ваша ссылка была автоматически встроена.   Отображать как обычную ссылку

×   Ваш предыдущий контент был восстановлен.   Очистить редактор

×   Вы не можете вставлять изображения напрямую. Загружайте или вставляйте изображения по ссылке.

×
×
  • Создать...