PICSim.js - сопрограммы и простейшая многозадачность

Микроконтроллеры выполняют только одну машинную инструкцию в каждый момент времени (многоядерные микроконтроллеры я пока не встречал). Традиционно простейшая программа для микроконтроллера это суперцикл - одна точка входа main с бесконечным циклом, где крутится какая-то задача. Архитектура большинства микроконтроллеров также предусматривает механизм прерываний - немедленную обработку событий, что позволяет решать более сложные задачи, причём в случае PIC18 это даже двухуровневая система прерываний. Обычно всего этого достаточно для реализации простой логики, однако при увеличении требований к системе традиционный подход теряет привлекательность. Альтернативный метод структурирования приложений состоит в том, чтобы выделить и отделить независимые подзадачи друг от друга и использовать некий программный каркас, который управляет этими подзадачами согласно набору ясно определенных правил. При таком подходе могут одновременно выполняться несколько (псевдо)параллельных задач, сообщающихся между собой и управляемых единым ядром. Этот подход к программированию - RTOS (операционная система реального времени), а многозадачность бывает 2 видов:

  • кооперативная - следующая задача выполняется только после того, как текущая задача явно передаст управление

  • вытесняющая - планировщик операционной системы сам передает управление от одной задачи другой

Кооперативная многозадачность является наиболее простой с точки зрения реализации и хорошо подходит для слабых микроконтроллеров.

config.h | coroutine.h | hex | picsim.js

screenshot

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

task_r.c

#include <xc.h>
#include <stdint.h>
#include "coroutine.h"

#define YIELD scrReturnV // just readability

static void task_leds_r() {
  scrBegin;

  static uint8_t i, port;

  // infinite task loop
  while (1) {
    // chasing leds
    if (!port) {
      port = (1 << 7);
    } else if (port == (1 << 5)) {
      port = (1 << 2);
    } else {
      port >>= 1;
    }
    if (port) {
      PORTA &= ~0x80;
    } else {
      PORTA |= 0x80;
    }
    PORTC = (PORTC & TRISC) | port;
    for (i = 0; i < 240; i++) { 
      YIELD; /* cooperate delay */ 
    }
  }

  scrFinishV;
}

task_l.c

#include <xc.h>
#include <stdint.h>
#include <stdlib.h>
#include "coroutine.h"

#define YIELD scrReturnV // just readability

static void task_leds_l() {
  scrBegin;

  static uint8_t i, port;

  // infinite task loop
  while (1) {
    // chasing leds
    if (!port) {
      port = 1;
    } else {
      port <<= 1;
    }
    PORTA = (PORTA & 0x80) | port;
    for(i = 0; i < 240; i++) { 
      YIELD; /* cooperate delay */ 
    }

    // blink random leds
    if (port == (1 << 6)) {
      for (port = 7; port; port--) {
        uint8_t random6;
        do {
          random6 = 1 << /* 0..6 */ rand() % 7;
        } while (random6 == (PORTA & ~0x80));
        PORTA = (PORTA & 0x80) | random6;
        for (i = 0; i < 240; i++) { 
          YIELD; /* cooperate delay */ 
        }
      }
    }
  }

  scrFinishV;
}

main.c

/*
  xc8 --chip=18f4620 main.c
*/

#include <xc.h>
#include "config-4620.h"

// non-reentrant tasks examples

#include "task_l.c"
#include "task_r.c"

int main() {
  // leds
  PORTA = TRISA = 0;
  PORTC = TRISC = 0x18;

  // simplest cooperative scheduler
  while(1) {
    task_leds_l();
    task_leds_r();
  }

  return 0;
}

Тут две задачи - левые светодиоды и правые. Каждая задача представляет из себя бесконечный цикл - и будь это обычные функции всё процессорное время ушло бы на task_leds_l(), тогда как до task_leds_r() очередь бы так и не дошла. Но мы имеем дело не с простыми функциями, а сопрограммами - макрос YIELD возвращает управление обратно вызывающей функции, а она в свою очередь просто поочерёдно вызывает задачи, выполняя тем самым функцию простейшего планировщика.

При работе с сопрограммами следует особое внимание уделять переменным. Если переменная должна сохранять своё значение между вызовами, то необходимо объявлять её static - например static uint8_t i, port, в то время как к uint8_t random6 это самая обычная (автоматическая) переменная.

А что произойдёт, если к этим двум задачам досыпать ещё парочку ? Очевидно, что тогда временные интервалы придётся перебирать заново:

// delay
for(i = 0; i < 240 /* new value */; i++) { 
  YIELD; /* cooperate delay */ 
}

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

hex | picsim.js

isr.c

#include <xc.h>
#include <stdint.h>

// up time since the start of the system
static uint32_t _uptime;

// http://www.microchip.com/forums/m890404.aspx
uint32_t get_uptime() {
  if (GIE) {
    GIE = 0; // critical section
    const uint32_t copy = _uptime;
    GIE = 1; // critical section end
    return copy;
  } else {
    return _uptime;
  }
}

// High priority interrupt
void interrupt isr() {
  if (INTCONbits.T0IF && INTCONbits.T0IE) {                               
    INTCONbits.T0IF = 0;
    _uptime++;
  }
}

Поскольку разрядность счётчика времени uint32_t больше разрядности микроконтроллера - операции над переменной _uptime не атомарны, поэтому в функцию get_uptime() вводится простейший механизм синхронизации через GIE. Теперь во всех задачах при формировании временных интервалов более целесообразно пользоваться функцией get_uptime():

static uint32_t start;

// delay
// https://arduino.stackexchange.com/a/12588
// while (millis() < start + ms) ;  // BUGGY version
// while (millis() - start < ms) ;  // CORRECT version
for (start = get_uptime(); get_uptime() - start < 10;) {
  YIELD; /* cooperate delay */ 
}

main.c

/*
  xc8 --chip=18f4620 main.c isr.c
*/

#include <xc.h>
#include "config-4620.h"

// non-reentrant tasks examples

#include "task_l.c"
#include "task_r.c"

int main() {
  // leds
  PORTA = TRISA = 0;
  PORTC = TRISC = 0x18;

  // TMR0 high priority Interrupt, 8 bit, 1:32 Prescale
  RCONbits.IPEN = 1;
  T0CON = 0;
  T0CONbits.T08BIT = 1;
  T0CONbits.T0PS2 = 1;
  INTCONbits.T0IE = 1;
  INTCONbits.T0IF = 0;
  INTCON2bits.TMR0IP = 1;        
  T0CONbits.TMR0ON = 1;
  INTCONbits.GIE = 1;

  // simplest cooperative scheduler
  while(1) {
    task_leds_l();
    task_leds_r();
  }

  return 0;
}

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

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

Далее калькулятор выражений в обратной польской нотации.

links

social