Многозадачность на микроконтроллерах PIC


17

Многозадачность важна в наши дни. Интересно, как нам этого добиться в микроконтроллерах и встроенном программировании. Я разрабатываю систему, основанную на микроконтроллере PIC. Я разработал его прошивку в MplabX IDE, используя C, а затем разработал приложение для него в Visual Studio, используя C #.

Поскольку я привык использовать потоки в программировании на C # на рабочем столе для реализации параллельных задач, есть ли способ сделать то же самое в моем коде микроконтроллера? MplabX IDE предоставляет, pthreads.hно это просто заглушка без реализации. Я знаю, что есть поддержка FreeRTOS, но использование этого делает ваш код более сложным. На некоторых форумах говорится, что прерывания также можно использовать как многозадачные, но я не думаю, что прерывания эквивалентны потокам.

Я разрабатываю систему, которая отправляет некоторые данные в UART, и в то же время необходимо отправить данные на веб-сайт через (проводной) Ethernet. Пользователь может контролировать выход через веб-сайт, но выход включается / выключается с задержкой 2-3 секунды. Так что это проблема, с которой я сталкиваюсь. Есть ли решение для многозадачности в микроконтроллерах?


Потоки могут использоваться только на процессорах, на которых работает ОС, поскольку потоки являются частью процесса, а процессы используются только в ОС.
TicTacToe

@ Зола, да, ты прав. Но что в случае с контроллерами?
Самолет


1
Можете ли вы объяснить, почему вам нужна настоящая многозадачность, и вы не можете разумно реализовать свое программное обеспечение, основанное на циклическом подходе к задаче, цикле select () или подобном?
whatsisname

2
Ну, как я уже сказал, я отправляю и получаю данные в uart и одновременно отправляю и получаю данные в Ethernet. Помимо этого мне также необходимо сохранять данные на SD-карте вместе со временем, так что да, задействован DS1307 RTC и EEPROM. До сих пор у меня только 1 UART, но, возможно, через несколько дней я буду отправлять и получать данные из 3 модулей UART. Сайт также будет получать данные от 5 различных систем, установленных в удаленном месте. Все это должно быть параллельно, но не параллельно, а с задержкой в ​​несколько секунд. !
Самолет

Ответы:


20

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

Оба типа многозадачных ОС требуют отдельного стека для каждой задачи. Таким образом, это подразумевает две вещи: во-первых, процессор позволяет размещать стеки в любом месте ОЗУ и, следовательно, имеет инструкции для перемещения указателя стека (SP), то есть нет аппаратного стека специального назначения, как на младшем конце ПОС. Это исключает серии PIC10, 12 и 16.

Вы можете написать операционную систему почти целиком на C, но переключатель задач, на котором перемещается SP, должен быть в сборке. В разное время я писал переключатели задач для PIC24, PIC32, 8051 и 80x86. Кишки все сильно различаются в зависимости от архитектуры процессора.

Второе требование - наличие достаточного объема ОЗУ для нескольких стеков. Обычно для стека требуется не менее пары сотен байт; но даже при 128 байтах на задачу для восьми стеков требуется 1 КБ ОЗУ - хотя вам не нужно выделять один и тот же размер стека для каждой задачи. Помните, что вам нужно достаточно стека для обработки текущей задачи и любых вызовов ее вложенных подпрограмм, но также и стекового пространства для вызова прерывания, так как вы никогда не знаете, когда это произойдет.

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

Вы не говорите, какой PIC вы хотите использовать. Большинство PIC24 и PIC32 будут иметь достаточно места для запуска многозадачной ОС; PIC18 (единственный 8-битный PIC, имеющий стеки в ОЗУ) имеет максимальный размер ОЗУ 4K. Так что это довольно сомнительно.

При совместной многозадачности (более простой из двух) переключение задач выполняется только тогда, когда задача «отдает» свой контроль обратно ОС. Это происходит всякий раз, когда задаче необходимо вызвать подпрограмму ОС для выполнения некоторой функции, которую она будет ожидать, например, запрос ввода-вывода или таймер. Это облегчает ОС переключение стеков, так как нет необходимости сохранять все регистры и информацию о состоянии, SP можно просто переключить на другую задачу (если нет других готовых к запуску задач, пустой стек данный контроль). Если текущая задача не требует выполнения вызова ОС, но выполняется некоторое время, она должна добровольно отказаться от управления, чтобы система реагировала.

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

Windows 3.1 и более ранние версии были совместными операционными системами, отчасти поэтому их производительность не была такой высокой.

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

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

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


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

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

1
В то время как кооперативная многозадачность требует дисциплины, обеспечение выполнения временных требований иногда проще в кооперативной многозадачности, чем в случае упреждающей. Поскольку очень мало блокировок необходимо будет удерживать на переключателе задач, системе переключателя задач с пятью задачами, в которой задачи должны проходить не более 10 мс без сдачи, в сочетании с небольшой логикой, которая говорит: «Если задача X срочно нужно запустить, запустить его дальше ", гарантирует, что задача X никогда не должна ждать более 10 мс, как только она подаст сигнал, прежде чем она запустится. Напротив, если задача требует блокировки, задача X ...
суперкат

1
... понадобится, но перед его выпуском переключится с помощью упреждающего переключателя, X может не получить ничего полезного, пока планировщик ЦП не приступит к выполнению первой задачи. Если планировщик не содержит логику для распознавания и обработки инверсии приоритетов, может пройти некоторое время, прежде чем он сможет приступить к выполнению первой задачи и завершить свою работу и снять блокировку. Такие проблемы не являются неразрешимыми, но их решение требует много сложностей, которых можно было бы избежать в кооперативной системе. Кооперативные системы работают отлично, за исключением одного гоча: ...
суперкат

3
вам не нужно несколько стеков в кооперативе, если вы кодируете в продолжениях. По сути, ваш код разделен на функции, void foo(void* context)которые логика контроллера (ядро) извлекает из пары указателей и указателей на функции и вызывает их по одному. Эта функция использует контекст для хранения своих переменных и тому подобного, а затем может добавить отправку продолжения в очередь. Эти функции должны быстро возвращаться, чтобы позволить другим задачам их момент в процессоре. Это основанный на событиях метод, требующий только одного стека.
фрик с трещоткой

16

Потоки обеспечиваются операционной системой. Во встроенном мире у нас обычно нет ОС («голый металл»). Так что это оставляет следующие варианты:

  • Классический основной цикл голосования. Ваша основная функция имеет while (1), которая выполняет задачу 1, затем выполняет задачу 2 ...
  • Основной цикл + флаги ISR: у вас есть ISR, который выполняет критичную по времени функцию и затем предупреждает основной цикл через переменную флага, что задача нуждается в обслуживании. Возможно, ISR помещает новый символ в кольцевой буфер, а затем сообщает основному циклу обработать данные, когда он будет готов сделать это.
  • Весь ISR: большая часть логики здесь выполнена из ISR. На современном контроллере типа ARM, который имеет несколько уровней приоритета. Это может обеспечить мощную «нитевидную» схему, но также может сбить с толку отладку, поэтому ее следует зарезервировать только для критических временных ограничений.
  • RTOS: ядро ​​RTOS (облегченное с помощью таймера ISR) позволяет переключаться между несколькими потоками выполнения. Вы упомянули FreeRTOS.

Я бы посоветовал вам использовать простейшие из вышеперечисленных схем, которые будут работать для вашего приложения. Из того, что вы описываете, у меня будет основной цикл генерации пакетов и помещения их в кольцевые буферы. Затем используйте драйвер на основе UART ISR, который запускается всякий раз, когда предыдущий байт завершает отправку до тех пор, пока не будет отправлен буфер, а затем ожидает дополнительного содержимого буфера. Подобный подход для Ethernet.


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

8

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

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

Вы упомянули, что прерывания обеспечивают уровень многозадачности. Это своего рода правда. Прерывание прервет вашу текущую программу в любой момент и выполнит там код, это сравнимо с системой с двумя задачами, где у вас есть 1 задача с низким приоритетом и другая с высоким приоритетом, которая заканчивается в одном временном интервале планировщика.

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

Единственный реальный способ одновременно выполнять несколько задач на одноядерном MCU - это использовать DMA и периферийные устройства, поскольку они работают независимо от ядра (DMA и MCU используют одну и ту же шину, поэтому они работают немного медленнее, когда оба активны). Таким образом, пока DMA перетасовывает байты в UART, ваше ядро ​​может свободно отправлять данные в Ethernet.


2
спасибо, DMA звучит интересно. Я определенно буду искать это!
Самолет

Не все серии PIC имеют DMA.
Мэтт Янг

1
Я использую PIC32;)
Самолет

6

В других ответах уже описаны наиболее часто используемые параметры (основной цикл, ISR, RTOS). Вот еще один вариант в качестве компромисса: протопотоки . Это в основном очень легкая библиотека для потоков, которая использует основной цикл и некоторые макросы C, чтобы «эмулировать» ОСРВ. Конечно, это не полная ОС, но для «простых» потоков это может быть полезно.


откуда я могу скачать его исходный код для windows? Я думаю, что это доступно только для Linux.
Самолет

@CZAbhinav Он должен быть независимым от ОС, и вы можете скачать последнюю версию здесь .
Erebos

Я сейчас в Windows и использую MplabX, я не думаю, что это полезно здесь. В любом случае, спасибо.!
Самолет

Не слышал про протопотоки, звучит как интересная техника.
Арсенал

@CZAbhinav О чем ты говоришь? Это код на C и не имеет ничего общего с вашей операционной системой.
Мэтт Янг

3

Мой базовый дизайн для минимальной временной RTOS не сильно изменился за несколько микросемей. Это в основном прерывание по таймеру за рулем конечного автомата. Процедура обслуживания прерываний - это ядро ​​ОС, а оператор switch в основном цикле - задачи пользователя. Драйверы устройств являются процедурами обслуживания прерываний для прерываний ввода-вывода.

Основная структура выглядит следующим образом:

unsigned char tick;

void interrupt HANDLER(void) {
    device_driver_A();
    device_driver_B();
    if(T0IF)
    {
        TMR0 = TICK_1MS;
        T0IF = 0;   // reset timer interrupt
        tick ++;
    }
}

void main(void)
{
    init();

    while (1) {
        // periodic tasks:
        if (tick % 10 == 0) { // roughly every 10 ms
            task_A();
            task_B();    
        }
        if (tick % 55 == 0) { // roughly every 55 ms
            task_C();
            task_D();    
        }

        // tasks that need to run every loop:
        task_E();
        task_F();
    }
}

Это в основном кооперативная многозадачная система. Задачи написаны так, чтобы никогда не входить в бесконечный цикл, но нам все равно, потому что задачи выполняются внутри цикла событий, поэтому бесконечный цикл неявный. Это стиль программирования, подобный событиям / неблокирующим языкам, таким как javascript или go.

Вы можете увидеть пример этого стиля архитектуры в моем программном обеспечении передатчика RC (да, я фактически использую его для полета самолетов RC, так что это несколько критично для безопасности, чтобы предотвратить падение самолетов и потенциально убивать людей): https://github.com / slebetman / pic-txmod . В основном это 3 задачи - 2 задачи в реальном времени, реализованные в виде драйверов устройств с состоянием (см. Ppmio) и 1 фоновая задача, реализующая логику микширования. Так что в основном он похож на ваш веб-сервер в том, что он имеет 2 потока ввода / вывода.


1
Я бы не стал называть это «совместная многозадачность», поскольку она на самом деле существенно не отличается от любой другой микроконтроллерной программы, которая должна выполнять несколько задач.
whatsisname

2

Хотя я понимаю, что вопрос конкретно касается использования встроенной ОСРВ, мне кажется, что более широкий вопрос, который задают, - это «как добиться многозадачности на встроенной платформе».

Я настоятельно советую вам забыть об использовании встроенной ОСРВ хотя бы на время. Я советую это, потому что считаю необходимым сначала узнать о том, как достичь «параллелизма» задачи с помощью чрезвычайно простых методов программирования, состоящих из простых планировщиков задач и конечных автоматов.

Чтобы предельно кратко объяснить концепцию, каждый модуль работы, который необходимо выполнить (т. Е. Каждое «задание»), имеет определенную функцию, которую необходимо периодически вызывать («ставить галочку»), чтобы этот модуль выполнял некоторые действия. Модуль сохраняет свое текущее состояние. Затем у вас есть основной бесконечный цикл (планировщик), который вызывает функции модуля.

Грубая иллюстрация:

for(;;)
{
    main_lcd_ui_tick();
    networking_tick();
}


...

// In your LCD UI module:
void main_lcd_ui_tick(void)
{
    check_for_key_presses();
    update_lcd();
}

...

// In your networking module:
void networking_tick(void)
{
    //'Tick' the TCP/IP library. In this example, I'm periodically
    //calling the main function for Keil's TCP/IP library.
    main_TcpNet();
}

Подобная однопотоковая структура программирования, при которой вы периодически вызываете функции основного конечного автомата из цикла главного планировщика, является повсеместной во встроенном программировании, и поэтому я настоятельно рекомендовал бы OP сначала ознакомиться с ним и освоиться с ним, прежде чем углубляться в использование. RTOS задачи / темы.

Я работаю на типе встроенного устройства, которое имеет аппаратный ЖК-интерфейс, внутренний веб-сервер, почтовый клиент, DDNS-клиент, VOIP и многие другие функции. Хотя мы используем RTOS (Keil RTX), количество используемых отдельных потоков (задач) очень мало, и большая часть «многозадачности» достигается, как описано выше.

Чтобы дать несколько примеров библиотек, которые демонстрируют эту концепцию:

  1. Сетевая библиотека Keil. Весь стек TCP / IP может быть запущен однопоточным; Вы периодически вызываете main_TcpNet (), который выполняет итерацию стека TCP / IP и любой другой сетевой параметр, скомпилированный из библиотеки (например, веб-сервер). См. Http://www.keil.com/support/man/docs/rlarm/rlarm_main_tcpnet.htm . Следует признать, что в некоторых ситуациях (возможно, выходящих за рамки этого ответа) вы достигаете точки, когда становится полезным или необходимо использовать потоки (особенно при использовании блокирующих сокетов BSD). (Далее: новое V5 MDK-ARM фактически порождает выделенный поток Ethernet - но я просто пытаюсь привести иллюстрацию.)

  2. Библиотека Linphone VOIP. Сама библиотека linphone является однопоточной. Вы вызываете iterate()функцию с достаточным интервалом. См. Http://www.linphone.org/docs/liblinphone-javadoc/org/linphone/core/LinphoneCore.html#iterate () . (Немного плохой пример, потому что я использовал это на встроенной платформе Linux, и библиотеки зависимостей linphone, несомненно, порождают потоки, но опять-таки, чтобы проиллюстрировать это.)

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


2
Спасибо за это хорошее объяснение, я использую библиотеку TCP / IP, предоставляемую микрочипом, и это очень сложный код. Мне как-то удалось разбить его на части и сделать его пригодным для использования в соответствии с моими требованиями. Я обязательно попробую один из ваших подходов.
Самолет

Веселитесь :) Использование RTOS определенно облегчает жизнь во многих ситуациях. На мой взгляд, использование потока (задачи) значительно облегчает программирование в одном смысле, так как вы можете избежать необходимости разбивать вашу задачу на конечный автомат. Вместо этого вы просто пишете свой код задачи так же, как в своих программах на C #, и ваш код задачи создается так, как будто это единственное, что существует. Важно изучить оба подхода, и, по мере того, как вы будете больше заниматься встроенным программированием, вы начинаете понимать, какой подход лучше всего подходит в каждой ситуации.
Тревор Пейдж

Я также предпочитаю использовать параметр потоков. :)
Самолет
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.