CraftDuino v2.0
  • - это CraftDuino - наш вариант полностью Arduino-совместимой платы.
  • CraftDuino - настоящий конструктор, для очень быстрого прототипирования и реализации идей.
  • Любая возможность автоматизировать что-то с лёгкостью реализуется с CraftDuino!
Просто добавьте CraftDuino!

Вытесняющая многозадачность для Arduino, том второй

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

7. Часть седьмая, препроцессорно-ассемблерная.

Компилятор gcc — далеко не самая удобная вещь. В частности, когда дело касается ассемблерных вставок. Вы помните, сколько \n\t пришлось написать. Когда речь идет о передаче параметров в ассемблерную вставку и возврате значения, все становится еще муторнее (сюды читать)… Не то, чтобы неподъёмно, но не интересно.

Для написания фунции int copyContext(int) мы воспользуемся другим способом ассемблеро вставления, а именно подключением функций на языке ассемблера (читать сюды).

Создадим в нашей пользовательской библиотеке файлы MirmPS_assemf.h и Mirm_as.S.
Заголовочный файл «MirmPS_assemf.h», если помните, мы уже подключили в файле MirmPS.h.

MirmPS_assemf.h будет содержать заголовки, необходимые для того, чтобы функция copyContext, описанная в файле Mirm_as.S могла быть распознана препроцессором, линковщиком и компилятором.

Обратите внимание, на расширение Mirm_as.S. По соглашениям, принятым для среды Win никакой разницы между расширениями .S и .s не существует. Но дело в том, что .S — это не расширение, это окончание и ноги у него растут откуда-то со стороны линукса. Компилятор gcc в среде Arduino, настроен так, что файлы .s будут проигнорированы. Поэтому правильный вариант заглавный, Mirm_as.S.

Код файла MirmPS_assemf.h практически полностью состоит из директив препроцессора:

#ifndef _Mirm_Assembler_     //Защита от 
#define _Mirm_Assembler_     //двойного подключения 

//Общие определения 

#ifdef __ASSEMBLER__

//Определения для ассемблера
.equ UDR0, 0xc6    // Эти определения в программе
.equ UCSR0A, 0xc0  // не используются. Приведены,
.equ TXC0, 6       // как пример.

#endif    //#ifdef __ASSEMBLER__

#ifndef __ASSEMBLER__

//Определения не для ассемблера.

#ifdef __cplusplus   
extern "C"{             //extern "C" декларирует, что  
			//передача параметров 
			//в функцию и возвращение результата
	                //должны вестись по соглашениям
	                //языка Си, а не C++.
#endif    //#ifdef __cplusplus

int volatile copyContext(int)__attribute__((naked));

// copyContext:
// Определена, как naked. В данном случае это не обязательно.
// Определена, как volatile. На всякий случай.
// принимает и возвращает int по соглашениям языка Си. 

#ifdef __cplusplus
} // extern "C"
#endif    //#ifdef __cplusplus
	
#endif    //#if not def __ASSEMBLER__

#endif    //#ifndef _Mirm_Assembler_


Это стандартное описание. Так все делают. Комментировать тут можно многое, но лучше поискать в интернетах. В интернетах все подробно и доходчиво.
Единственное, что нас по настоящему интересует, так это конструкция
extern "C"{ 
       ......
} 


Чтобы ее понять, надо знать, что разные языки программирования при передаче параметров и извлечении результата используют разные способы. Класификатор extern «C» говорит компилятору, передавать в функцию параметры надо как в языке «С» и также возвращать её результат. Называется это — порядок связывания языка «C». Важно понимать, что больше ни на что extern «C» не влияет. Писать функцию при этом можно на любом языке. И такое описание используют для связи с Ассемблером или же Фортраном.

Как же выглядит порядок связывания языка «C»? Для AVR данные будут передаваться в регистрах, с R25 (первый) по R8 (При этом очередной параметр всегда начинается в регистре с четным номером.). То, что не влезает, хитрым образом передается в стеке. Наш int будет лежать в R25:R24. И возвращенный результат, кстати, забираться будет оттуда же. Старший байт будет в R25. Младший в R24.

Теперь сама функция. она хранится в Mirm_as.S:
#include "MirmPS_assemf.h"

.global copyContext  ; Объявление о том, что на точку 
                     ; copyContext будут ссылаться извне

copyContext:         ;метка входа в функцию.
mov r30,r24          ;перемещаем полученное значение
mov r31,r25          ;в регистровую пару Z
ldi r29,0x05         ;устанавливаем в регистровую пару 
ldi r28,0x00         ;Y адрес 0x0500
ldi r18,35           ;Нам надо отщелкать 35 байтов
		     ;в r18 будет счетчик.

adiw r30,1           ;Инкрементация регистровой пары Z
                     ;нужна по соображениям хитрой 
		     ;адресной математики 

		     ;С инициализацией закончили.

copyContext_1:	     ;Точка входа рабочего цикла

LD r19,-Y            ;Декрементировать Y, после чего
		     ;ЗАГРУЗИТЬ В регистр r19 
		     ;содержимое ИЗ ячейки на которую Y
		     ;ссылается
					  	 
ST -Z,r19            ;Декрементировать Z, после чего
		     ;ЗАПИСАТЬ ИЗ регистра r19 
		     ;содержимое В ячейку на которую Z
		     ;ссылается
					  
dec r18              ;Декрементировать счетчик.
brne copyContext_1   ;Если r18 не равно нулю
		     ;вернуться к точке copyContext_1
		     ; иначе

sbiw r30,1           ;Декрементация регистровой пары Z
                     ;нужна по соображениям хитрой 
		     ;адресной математики

mov r24,r30          ;Записываем назад в R25:R24
mov r25,r31          ;Тот адрес на котором мы остановились

ret                  ;Директива возврата из функции 


Как работает эта функция? Копирование осуществляется с помощью механизма косвенной адресации, реализованной в AVR. Этот механизм позволяет регистрам обращаться к ячейке памяти по ее адресу. При этом адрес храниться в одной из регистровых пар X,Y,Z. Это специальные регистровые пары, предназначенные для работы с механизмом косвенной адресации. X это R27:R26. Y это R29:R28. Z это R31:R30.

В Y кладем те адреса, откуда будем копировать. В Z те адреса, куда.
Есть хитрый момент. При косвенном чтении регистровую пару можно автоматически инкрементировать и декрементировать. Но почему-то инкрементация проходит как пост, а декрементация как пред. То есть, значение сперва декрементируется, а уже потом косвенно считывается. А пользуемся мы именно декрементацией.
Поэтому регистр Y ручками устанавливаем не 0x04FF, а в 0x0500 (дабы считать 0x04FF), и к регистру Z также предварительно прибавляем единичку.

Конструкция
dec r18            
brne copyContext_1 

Это счетчик цикла.

dec r18 Декрементирует r18.
brne copyContext_1 интересуется, возвращала ли предыдущая операция нулевой флаг (флаги — это биты регистра состояния SREG. Один из них устанавливается при нулевом исходе арифметических операций, таких как dec). Если флаг не установлен, то осуществляет переход назад на начало цикла. Если установлен, то прыжок на copyContext_1 игноририруется и программа покидает цикл.

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

Наконец возвращаем значение в R25:R24 (напоминаю про соглашение связывания языка Си.) и возвращаемся из функции.

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

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

Файлы MirmPS_assemf.h и Mirm_as.S готовы.

Всё… Библиотека написана.
Осталось написать тестовый скетч.

Но прежде немного отвлечеммся и поговорим про оператор goto.

8. Часть восьмая, в которой… Магия…

Функция

void branching(void)
{       setStackPointer(0x04,0xFF);
        branching_2();
        if (Taskcount==0) {Taskcount++;goto *programm1;}
        if (Taskcount==1) {Taskcount++;goto *programm2;}
}

использует оператор goto в форме goto *(func). Не то, чтобы programm1 было обязательно вызывать через goto, но здесь это уместно.
Оператор goto — это самый многострадальный оператор языка Си… В чем его только не обвиняет.
И в том, что из-за него программы не читабельны. И в том, что из-за него компилятор не может эфективно проводить оптимизацию. В том, что он не укладывается в парадигму структурного программирования. В том, что его использование может привести к нарушению логической целостности программы. Последнее, кстати — абсолютная правда я это даже продемонстрирую.

Оператор goto это страшилка. Это то чем матёрые преподаватели пугают бедных студентов. Это как дети и спички. И не надо, мол, лазать там, где не надо лазать.

Но есть вещи пострашнее goto… Например ассемблерные вставки. Те самые, которые мы столь активно используем.

Тут получается какая-то несуразность. Если программист юзает ассемблер — он крут. Если оператор goto — он нуб. Интересно.

goto — это оператор безусловного перехода. Его операндом являются или метка или адрес функции. По сути, и то и другое транслируется адресом в программной памяти. Разница небольшая.

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

От ВЫЗОВОВ call,icall,rcall (на основе которых строятся функции) ПЕРЕХОДЫ jmp, rjmp, ijmp отличаются тем, что вызовы возвращают точку вызова в стек, а переходы не возвращают. В этом вся разница.

То есть переход
goto *programm1;
отличается от вызова
programm1();
, тем, что из вызова можно вернуться, а из перехода нет.

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

А что-бы вы получше поняли, чем так опасен оператор goto я приведу пример.

Рассмотрим скетч Ардуино (ту замечательную библиотеку, что мы написали не подключаем. Сейчас она не нужна).

Вот код
void setup(void)
{goto *k;
while(1){};//цикл-ловушка, призванный подвесить программу по выходу из k.
}

void loop(void)
{digitalWrite(13,1);}

void k (void)
{
digitalWrite(13,1);delay(200);digitalWrite(13,0);delay(200);
}


Как вы думаете, что произойдет?.. Если бы на месте goto *k; был бы вызов k() ответ был бы очевиден. Программа мигнет диодом и уснет на строке while(1){}, так никогда и не попадя в тело функции loop.

А если goto *k;
Правильный ответ — loop будет вызван. И светодиод, мигнув загорится на постоянной основе.

Механизм работы такой. Ядро Ардуино вызывает функцию
setup()

Которая тут же выполняет переход
goto *k;

Функция
void k (void)
{digitalWrite(13,1);delay(200);digitalWrite(13,0);delay(200);}

Мигает светодиодом и… ВНИМАНИЕ… осуществляет возврат из функции.

«Куда?»,- воскликните вы. Ведь функция k() никогда не вызывалась и в стеке не содержится обратного адреса. Но что-то в стеке есть?

В стеке лежит адрес возврата из последней вызванной функции… функции setup()…

Круто, да?.. А после функции setup(), как известно выполняется функция loop().

Вот такая магия.

Так что же… Получается, что мы вернулись из функции setup() по возврату функции k(). А теперь представьте, что функция k() определена как int возвращает какое-то значение. Кому она чего вернет? Функции setup, которая void? Интересно, не так ли…

А давайте наоборот…

Усложним пример:
int a;

void setup() {
Serial.begin(9600);
a=func_int();
Serial.println(a);  
}

void loop() 
{}

void func_void () 
{}

int func_int () {
 goto *func_void;
 return(222);}


Что мы увидим по Serial интерфейсу?

Как вы, наверное, поняли, я подменил возврат из функции func_int(), который возвращает int возвратом из функции func_void(), которая не возвращает ничего.
Но…
a=func_int();

Переменная «a» думает, что ей возвращают число int… И ищет его в тех регистрах, куда оно должно прийти.

Так что же мы увидим по Serial интерфейсу? Правильный ответ такой: «Всё, что угодно».

Заглянув в Serial, мы можем увидеть электронных человечков, следы информационного шторма, комнату, где малыш компилятор хранит свои игрушки… Может даже ответ на Главный вопрос жизни, Вселенной и всего всего такого.

Лично я увидел «152»…

Ну не магия ли? А магия может быть как вредной, так и полезной. goto — это остро отточенный меч. Им можно сокрушать врагов. С его помощью можно защищать друзей. А можно и зарезаться… Случайно.

Если дать меч в руки невежды, он много не навоюет. Он лишь покалечит себя и своих товарищей. Только понимая законы работы микроконтроллера, чувствуя и предсказывая действия союзника компилятора, можно полностью отдавшись чувству момента гордо написать goto и единым взмахом разрубить Гордиев Узел логической связности. Ибо величайшая структура — это отсутствие всякой структуры.

8. Часть восьмая, в которой программа запускается.

Повествование подходит к концу.
У нас есть библиотека, в которой лежат 4-ре файла.

И сейчас мы напишем скетч, который проиллюстрирует ее работу.

Стандартный скетч Ардуино состоит из функций
void setup() {
}

void loop() {
}


Но мы функцию main переписали, а потому теперь основа скетча выглядит так:

#include <MirmPS.h>

void setup() {
}

void loop1() {
}

void loop2() {
}

Можете проверить, компилятор ругаться не должен. Что мы будем запускать?

Простейший вариант, проверяющий правильность многопоточной работы.


#include <MirmPS.h>

void setup() 
{  Serial.begin(9600);
  TIMSK2=1;}

void loop2() 
{digitalWrite(13,1);delay(500);digitalWrite(13,0);delay(500);}

void loop1()
{Serial.println("HelloWorld");delay(2000);}


Видите. И светодиод мигает. И HelloWorld отписывается. Обратите внимания, что функции delay() повели себя неожиданно в том плане, что многопоточность совершенно не мешает им корректно отрабатываться. Эта приятная неожиданность объясняется тем, что в среде Arduino delay реализован на системном таймере, а не на пустом цикле.

Что до команды
TIMSK2=1;
, то эта строка включает в работу переключатель контекста, разрешая соответствующее прерывание.

Но это маленькая демонстрация.

А теперь о настоящей двупоточной программе.

Мне понравился осциллограф, представленный в статье.
Он бесподобен. Он состоит из небольшого скетча Ардуино и большого скетча для Processing.

Скетч для процессинга вот (не забудьте вставить в скетч номер своего COM-порта.
Для этого надо модифицировать соответствующим образом строчку
port = new Serial(this,«COM1», 38400); // Serial.list()[0])
:

import processing.serial.*;

Serial port; // Create object from Serial class
int valA;
int valB;
int valC;
int valD;
int valE;
int valF;
// this should have been some kind of 2-diminsional array, I guess, but it works.
int[] valuesA;
int[] valuesB;
int[] valuesC;
int[] valuesD;
int[] valuesE;
int[] valuesF;

PFont fontA;
PFont fontB;

void setup()
{

// make sure you have these fonts made for Processing. Use Tools...Create Font.
// "fontA" is a 48 pt font of some sort. It's what we use to show the "now" value.
  fontA = loadFont("CourierNewPSMT-48.vlw");

// "fontB" is a 14 pt font of some sort. It's what we use to show the min and max values.
  fontB = loadFont("CourierNewPSMT-14.vlw");

// I wouldn't change the size if I were you. There are some functions that don't use
// the actual sizes to figure out where to put things. Sorry about that.
  size(550, 600);

// Open the port that the board is connected to and use the same speed
// anything faster than 38.4k seems faster than the ADC on the Arduino can keep up with.
// So, if you want it to be smooth, keep it at or below 38400. 28800 doesn't work at all,
// I do not know why. If you turn on smooth() you need to drop the rate to 19.2k or lower.
// You will probably have to adjust Serial.list()[1] to get your serial port.
  port = new Serial(this,"COM1", 38400); // Serial.list()[0]

// These are 6 arrays for the 6 analog input channels.
// I'm sure it could have just as well been a 2d array, but I'm not that familiar
// with Processing yet and this was the easy way out.
  valuesA = new int[width-150];
  valuesB = new int[width-150];
  valuesC = new int[width-150];
  valuesD = new int[width-150];
  valuesE = new int[width-150];
  valuesF = new int[width-150];
// the -150 gives us room on the side for our text values.

// this turns on anti-aliasing. max bps is about 19.2k.
// uncomment out the next line to turn it on. Personally, I think it's better left off.
//smooth();
}

int getY(int val) 
{
  // I added -40 to this line to keep the lines from overlapping, to
  // keep the values within their gray boxes.
  return (int)(val / 1023.0f * (height-40)) - 1;
}

void draw()
{
  String decoder = "";
  while (port.available() >= 3)
  {
    // read serial until we get to an "A".
    decoder = port.readStringUntil(65);
  }
// sanity check. make sure the string we got from the Arduino has all the values inside.
if ((decoder.indexOf("B")>=1) & (decoder.indexOf("C")>=1) &
(decoder.indexOf("D")>=1) & (decoder.indexOf("E")>=1) &
(decoder.indexOf("F")>=1))
{
  // decoder string doesn't contain an A at the beginning. it's at the end.
  valA=int(decoder.substring(0,decoder.indexOf("B")));
  //println("A" + str(valA));
  valB=int(decoder.substring(decoder.indexOf("B")+1,decoder.indexOf("C")));
  //println("B" + str(valB));
  valC=int(decoder.substring(decoder.indexOf("C")+1,decoder.indexOf("D")));
  //println("C" + str(valC));
  valD=int(decoder.substring(decoder.indexOf("D")+1,decoder.indexOf("E")));
  //println("D" + str(valD));
  valE=int(decoder.substring(decoder.indexOf("E")+1,decoder.indexOf("F")));
  //println("E" + str(valE));
  valF=int(decoder.substring(decoder.indexOf("F")+1,decoder.indexOf("A")));
  //println("F" + str(valF));
  }

  //shift the new values into the array, move everything else over by one
  for (int i=0; i<width-151; i++) 
  {
    valuesA[i] = valuesA[i+1];
    valuesB[i] = valuesB[i+1];
    valuesC[i] = valuesC[i+1];
    valuesD[i] = valuesD[i+1];
    valuesE[i] = valuesE[i+1];
    valuesF[i] = valuesF[i+1];
  }

  // -151 because the array is 151 less than the width. remember we
  // saved room on the side of the screen for the actual text values.
  valuesA[width-151] = valA;
  valuesB[width-151] = valB;
  valuesC[width-151] = valC;
  valuesD[width-151] = valD;
  valuesE[width-151] = valE;
  valuesF[width-151] = valF;

  background(0);

  textFont(fontA);

  // I'm sure these c/should have been determined using height math, but I don't have the time really.
  // Draw out the now values with the big font.
  text(valA + 1, (width-140), 108-5);
  text(valB + 1, (width-140), 206-5);
  text(valC + 1, (width-140), 304-5);
  text(valD + 1, (width-140), 402-5);
  text(valE + 1, (width-140), 500-5);
  text(valF + 1, (width-140), 598-5);

  textFont(fontB);
  // Draw out the min and max values with the small font.
  // the h value (30,128,266,etc) is a function of height,
  // but I didn't bother to actually do the math.
  // I guess it's (98*n)+30 where n is 0,1,2,3,4,5, but I don't know
  // exactly how height (600) relates to 98... ((h/6)-2??)
  drawdata("0", width-90, 30, valuesA);
  drawdata("1", width-90, 128, valuesB);
  drawdata("2", width-90, 226, valuesC);
  drawdata("3", width-90, 324, valuesD);
  drawdata("4", width-90, 422, valuesE);
  drawdata("5", width-90, 520, valuesF);

for (int x=150; x<width-1; x++) 
{
  // next line adjusts the color of the stroke depending on the x value. (fades out the end of the line)
  check(x,255,0,0);

  // next line draws the line needed to get this value in the array to the next value in the array.
  // the offsets (6+ in the next line) were used to get the values where I wanted them without
  // having to actually do real spacial math. There's a hack in getY that offsets a little, too.
  line((width)-x,
  6+((height/6)*0)+((height-1-getY(valuesA[x-150]))/6), (width)-1-x,
  6+((height/6)*0)+((height-1-getY(valuesA[x-149]))/6));
  check(x,0,255,0);
  line((width)-x,
  4+((height/6)*1)+((height-1-getY(valuesB[x-150]))/6), (width)-1-x,
  4+((height/6)*1)+((height-1-getY(valuesB[x-149]))/6));
  check(x,0,0,255);
  line((width)-x,
  2+((height/6)*2)+((height-1-getY(valuesC[x-150]))/6), (width)-1-x,
  2+((height/6)*2)+((height-1-getY(valuesC[x-149]))/6));
  check(x,255,255,0);
  line((width)-x,
  0+((height/6)*3)+((height-1-getY(valuesD[x-150]))/6), (width)-1-x,
  0+((height/6)*3)+((height-1-getY(valuesD[x-149]))/6));
  check(x,0,255,255);
  line((width)-x,
  -2+((height/6)*4)+((height-1-getY(valuesE[x-150]))/6), (width)-1-x,
  -2+((height/6)*4)+((height-1-getY(valuesE[x-149]))/6));
  check(x,255,0,255);
  line((width)-x,
  -4+((height/6)*5)+((height-1-getY(valuesF[x-150]))/6), (width)-1-x,
  -4+((height/6)*5)+((height-1-getY(valuesF[x-149]))/6));
}

  // draw the boxes in gray.
  stroke(170,170,170);

  // these 5 lines divide the 6 inputs
  line(0,108,width-1,108);
  line(0,206,width-1,206);
  line(0,304,width-1,304);
  line(0,402,width-1,402);
  line(0,500,width-1,500);

  // these four lines make up the outer box
  line( 0, 0, width-1, 0); // along the top
  line(width-1, 0, width-1, height-1); // down the right
  line(width-1, height-1, 0, height-1); // along the bottom
  line( 0, height-1, 0, 0); // up the left
}

void drawdata(String pin, int w, int h, int[] values)
{
  text("pin: " + pin, w, h);
  text("min: " + str(min(values) + 1), w, h + 14);
  text("max: " + str(max(values) + 1), w, h + 28);
}

void check(int xx, int rr, int gg, int bb)
{

// floating point operations in Processing are expensive.
// only do the math for the float (fading out effect) if
// we have to. You can change 170 to 160 if you want it to
// fade faster, but be sure to change the other 170 to 160
// and the 20 to 10.
// (20 is the difference between 170 and 150)
  if (xx<=170)
  {
    float kick = (parseFloat(170-xx)/20)*255;
    // invert kick so the brighter parts are on the left side instead of the right.
    stroke(rr,gg,bb,255-kick);
  }
  else
  {
    stroke(rr,gg,bb);
  }
}


(не забудьте вставить в скетч номер своего COM-порта.
Для этого надо модифицировать соответствующим образом строчку
port = new Serial(this,«COM1», 38400); // Serial.list()[0])


Всё. Дело за малым, запустить его на Processing-е.

А что до скетча Ардуино, то он вот:

//
// Oscilloscope
// http://accrochages.drone.ws/en/node/90
//

#define ANALOGA_IN 0
#define ANALOGB_IN 1
#define ANALOGC_IN 2
#define ANALOGD_IN 3
#define ANALOGE_IN 4
#define ANALOGF_IN 5

int counter = 0;

void setup() 
{
  Serial.begin(38400);
}

void loop() 
{
  int val[5];

  val[0] = analogRead(ANALOGA_IN);
  val[1] = analogRead(ANALOGB_IN);
  val[2] = analogRead(ANALOGC_IN);
  val[3] = analogRead(ANALOGD_IN);
  val[4] = analogRead(ANALOGE_IN);
  val[5] = analogRead(ANALOGF_IN);


  Serial.print( "A" );
  Serial.print( val[0] );

  Serial.print( "B" );
  Serial.print( val[1] );
  Serial.print( "C" );
  Serial.print( val[2] );
  Serial.print( "D" );
  Serial.print( val[3] );
  Serial.print( "E" );
  Serial.print( val[4] );
  Serial.print( "F" );
  Serial.print( val[5] );
}


Маленький, но удаленький…

Чуть чуть изменим его.

Подключим #include MirmPS.h.
В функции setup() добавим строку TIMSK2=1;
Функцию loop переименуем в loop1, а в конец файла добавим еще чуть чуть кода — наш второй процесс.

Вот таким образом:


#include <MirmPS.h>



//
// Oscilloscope
// http://accrochages.drone.ws/en/node/90
//

#define ANALOGA_IN 0
#define ANALOGB_IN 1
#define ANALOGC_IN 2
#define ANALOGD_IN 3
#define ANALOGE_IN 4
#define ANALOGF_IN 5

int counter = 0;

void setup() 
{
  Serial.begin(38400);
  TIMSK2=1;
}

void loop1() 
{
  int val[5];

  val[0] = analogRead(ANALOGA_IN);
  val[1] = analogRead(ANALOGB_IN);
  val[2] = analogRead(ANALOGC_IN);
  val[3] = analogRead(ANALOGD_IN);
  val[4] = analogRead(ANALOGE_IN);
  val[5] = analogRead(ANALOGF_IN);


  Serial.print( "A" );
  Serial.print( val[0] );
  Serial.print( "B" );
  Serial.print( val[1] );
  Serial.print( "C" );
  Serial.print( val[2] );
  Serial.print( "D" );
  Serial.print( val[3] );
  Serial.print( "E" );
  Serial.print( val[4] );
  Serial.print( "F" );
  Serial.print( val[5] );
}

float j=0;float i=0;

void loop2()
{analogWrite(5,(sin(j)+1)*128);
j=j+PI/720/6; if (j>2*PI) j=0;
analogWrite(6,i);
i=i+0.05; if (i>255) i=0;
}


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

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

Однако непосредственно считать не получится, ибо analogWrite, как вы знаете — не аналоговый сигнал, а ШИМ сигнал… Если мы подадим его на осциллограф, то в лучшем случае увидим скачки разной кучности. В худшем — не увидим ничего. Для того, чтобы ШИМ сигнал превратить в настоящий аналоговый сигнал, его надо отфильтровать. В сумме это уже будем цифро-аналоговый преобразователь.

Схема.

… Это половина схемы. Вывод номер 5 и еще 3 аналоговых входа подключаются анлогично… Конденсатор у меня на 470 микроФарад резисторы по 100 Ом.

Вот результат работы всей системы:


Что мы тут видим?.. Ну во первых, что наш ЦАП — дерьмо. Как только светодиод открывается, он тут же проседает.

Ну, и конечно, что многопоточность работает. Это не может не радовать.

Тоже самое вживую:


Собственно, основное преимущество многопоточности было продемонстрировано. Две не связанные программы без лишних раздумий запустились на одной платформе.

9. Часть девятая. Последняя.

Прежде чем отпустить ваше внимание (если оно еще сохраняется)…

Я должен отметить еще две-три вещи:

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

Если есть переменные, с которыми работают сразу оба процесса, их следует описывать как volatile, а обращения к ним делать атомарными.

О динамическом распределении памяти. Я писал второй стек в кучу. А куча распределяется при динамическом распределении… Если подключить распределение, будет коллапс. Что делать, если нужно динамическое распределение? Это очевидно. Динамически распределить место под стек и писать туда, не забывая, что стек пишется сверху вниз.

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

И вообще… Мы вольны делать все, что захотим. И это хорошо.

Спасибо.

Баг репорт №1: Чтобы можно было вызывать функции, работающие на основе прерываний (например Wire.endTransmision) из тела функции setup(), необходимо в функции programm1 поменять местами вызовы функции setup() и макроса sei(). Глобальное разрешение прерываний должно идти раньше вызова функции setup().
  • +5
  • 21 июля 2012, 19:14
  • Mirmik

Комментарии (33)

RSS свернуть / развернуть
+
0
Спасибо! мои мозги разбухли и сыто урчат)
avatar

deQU

  • 22 июля 2012, 18:16
+
0
Приятного аппетита.
avatar

Mirmik

  • 22 июля 2012, 18:20
+
0
>>Тут получается какая-то несуразность. Если программист юзает ассемблер — он крут. Если оператор goto — он нуб.
Точно — несуразность. Я сам не раз использовал asm-вставки или asm-функции и часто, по прошествию времени, понимал что сделано это по незнанию-или потому что лениво делать как надо(без asm-а).
А goto — это ничего и иногда очень даже удобно и правильно.
avatar

artjom

  • 25 июля 2012, 00:51
+
0
Очень хорошая статья. Давно искал о много поточность. Самому мне не додумать вообще никогда как это реализовать! А тут всё рассказывается подробно но всё равно я в ступоре и пока после одного раза прочтения мало что понял конкретно. Я не нашёл (или не увидел) где можно скачать библиотеку уже готовую?
avatar

LEVVARRR

  • 25 июля 2012, 07:32
+
0
Готовую… Ну, её тут вообще-то нет.

Можете скачать отсюда. rusfolder.com/31804206
avatar

Mirmik

  • 25 июля 2012, 09:12
+
0
Спасибо. Как буду дома попробую. А будет ли ещё статьи на тему увеличения потоков? Ведь в данном примере их только 2. И пока я не очень понял как добавить ещё (например) 2
avatar

LEVVARRR

  • 25 июля 2012, 09:35
+
0
Не знаю… Может и будут…

avatar

Mirmik

  • 25 июля 2012, 09:41
+
0
Это так круто!!! Всё заработало!!! Спасибо!!! Вот бы ещё узнать как увеличить число LOOP Почему то не работает с библиотекой Wire?
avatar

LEVVARRR

  • 25 июля 2012, 16:47
+
0
Мне кажется что вся проблема связана с подставным main2. При использовании дополнительной библиотекой используется стандартный main.
Чтобы не флудить перепиской. Levvarrr собака ya.ru
avatar

LEVVARRR

  • 25 июля 2012, 17:20
+
0
отписался…
avatar

Mirmik

  • 25 июля 2012, 17:29
+
0
Скачал на пробу, надо было по-быстренькому реализовать как раз 2 задачи… Ругается: «MirmPS.h» no such file or directory". Если сунуть файлы библы к pde файлу программы, та же ошибка но уже ругается на Arduino.h (как это вообще возможно?!). Кто-нибудь знает, в чем дело.
avatar

MAFia

  • 27 июля 2012, 15:47
+
0
А вы правильно библиотеку добавляете?
avatar

Mirmik

  • 27 июля 2012, 22:25
+
0
#include <MirmPS.h>
Как в примере. И не работает.
avatar

MAFia

  • 30 июля 2012, 13:05
+
0
В смысле того, что она у вас лежит в папке libraries в корне ардуино?
avatar

Mirmik

  • 31 июля 2012, 13:38
+
0
\arduino\libraries\mirmPS
avatar

MAFia

  • 31 июля 2012, 14:37
+
0
Интересно… Я не знаю…
avatar

Mirmik

  • 31 июля 2012, 14:45
+
0
Превосходно!
Обязательно попробую на досуге.
avatar

mishmash

  • 25 июля 2012, 10:25
+
0
По поводу бага, найденного камрадом LEVVARRR в конец статьи добавлен багрепорт…
avatar

Mirmik

  • 25 июля 2012, 19:41
+
0
вторая статья еще на полгода.
большое спасибо.
увлекательно написано.
avatar

zorbo

  • 7 августа 2012, 22:00
+
0
это очень круто, автор супер молодец. Но у меня не выходит запустить =( беру пример с файлообменника по ссылке из коментария выше, делаю правку как в конце статьи использую код
#include <MirmPS.h>
void setup(){
  Serial.begin(115200);
  Serial.println("start"); 
}  
void loop1() {
  Serial.println("1");
  delay(100);
}
void loop2() {
  Serial.println("2");
  delay(50);
}

на выходе работает только loop1, loop2 молчит =( в чем проблема может быть? Использую ардуину мега
avatar

versoul

  • 28 августа 2012, 20:19
+
+1
В функции setup должна фигурировать строчка

TIMSK2=1;

Она включает прерывания от таймера 2, который отвечает за многозадачность…

Я хочу обратить внимание, что это отнюдь не готовое решение, которое удобно для пользования и в котором отловлены всяческие баги… Это была небольшая демонстрация того, что можно делать…

avatar

Mirmik

  • 29 августа 2012, 14:25
+
0
Здравствуйте.
Можете скачать отсюда. rusfolder.com/31804206

К сожалению, не дает скачать.
Прохожу все, ввожу код, он дает ссылку, но она не рабочая.

Не могли бы вы перезалить или сбросить мне на емайл?
avatar

tpovin

  • 15 февраля 2013, 14:56
+
0
Прошу прощения, что заставил ждать. Лазил по всяческим дебрям и давно здесь не появлялся здесь.
Вот библиотека: yadi.sk/d/ulVShcC-3mFTK

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

Mirmik

  • 4 апреля 2013, 01:52
+
0
Я бы посоветовал лучше изучить FreeRTOS, и использовать — и опыта будет больше и пользы, можно будет потом применити и на другом микроконтроллере, чем каждый раз использовать самописние псевдо RTOS. Но это только мое субъективное мнение…
avatar

Nemo

  • 5 апреля 2013, 09:46
+
0
Пользы больше… Опыта вряд ли. По крайней мере для меня.
avatar

Mirmik

  • 5 апреля 2013, 22:02
+
0
Делал все по статье, но выдает ошибку. Что с этим делать?

/media/HDS/Process/Arduino/libraries/MirmPS/main2.cpp: In function 'void branching()':
/media/HDS/Process/Arduino/libraries/MirmPS/main2.cpp:44:39: error: cannot convert 'programm1' from type 'volatile void()' to type 'void*'
if (Taskcount==0) {Taskcount++;goto *programm1;}
^
/media/HDS/Process/Arduino/libraries/MirmPS/main2.cpp:45:39: error: cannot convert 'programm2' from type 'volatile void()' to type 'void*'
if (Taskcount==1) {Taskcount++;goto *programm2;}
^
Ошибка компиляции.
avatar

leon_3

  • 15 мая 2015, 13:05
+
0
Уточнение

Arduino IDE 1.6.4, OS Kubuntu 14.04

Папка для библиотек: /media/HDS/Process/Arduino/libraries/
avatar

leon_3

  • 15 мая 2015, 14:19
+
0
Наверное глупо надеяться на ответ…
avatar

leon_3

  • 18 мая 2015, 13:52
+
0
Вем привет. Автору большое спасибо! Мне очень понравилось! Взял на себя смелость немного допилить исходный код для поддержки новых версий Arduino IDE (тестировал на 1.6.4 и 1.6.9), а также добавил ещё один поток исполнения. Ну и как полагается, git-репозиторий под это дело организовал: https://github.com/pi-null-mezon/Arduinomultitask, — надеюсь автор не против (ссылка на эту статью приведена). Ещё раз спасибо.
avatar

pi-null-mezon

  • 16 августа 2016, 11:34
комментарий был удален

+
0
Здравствуйте. Решил переделать эту библиотеку под atmega2560. Поменял адрес первого стека на 0x21ff(RAMEND) согласно m2560def.inc, второго на 0x20a1, в обработчике прерывания поставил границу 0x20ff, а в файле MirmsPS_as.S регистровую пару y на 0x2200. Но на ардуино не работает ни один ни другой поток. При этом если закоментировать включение таймера в setupе, то выполняется первый поток. Может быть кто-нибудь знает в чём дело? Буду рад любой помощи!
avatar

Leopold

  • 13 августа 2017, 01:31
+
0
Здравствуйте! может кто поможет лузеру? что не так и как чтобы так?

// термометр, датчик DS18B20
#include <MsTimer2.h>
#include <Led4Digits.h>
#include <OneWire.h>
#include <MirmPS.h>
#include <Adafruit_BMP085.h>     //Библиотека для датчика давления

Adafruit_BMP085 press_data;     //переменная для работы с датчиком

byte mA = 3;

int tmp;

float Pmm;     //для хранения показаний давления

float Tc;        //для хранения температуры

#define POWER_MODE  0 // режим питания, 0 - внешнее, 1 - паразитное
#define MEASURE_PERIOD 500  // время измерения, * 2 мс

// тип индикатора 1; выводы разрядов 5,4,16,2; выводы сегментов 6,7,8,9,10,11,12,13
Led4Digits disp(1, 5,4,16,2, 6,7,8,9,10,11,12,13);

OneWire sensDs (15);  // датчик подключен к выводу 15

int timeCount;     // счетчик времени измерения
boolean flagSensReady;  // признак готовности данных с датчика
byte bufData[9];  // буфер данных
float temperature;  // измеренная температура

void setup() {
  pinMode(mA, OUTPUT);  
   press_data.begin(); 
    press_data.begin();     //подключаемся к датчику  
  MsTimer2::set(2, timerInterrupt); // задаем период прерывания по таймеру 2 мс 
  MsTimer2::start();               // разрешаем прерывание по таймеру
  Serial.begin(9600);
  TIMSK2=1;
 
  
}


void loop1()

{

  Tc=press_data.readTemperature();      //читаем температуру

  Pmm=press_data.readPressure()/133.322;      //читаем и пересчитываем давление

  
  
   Pmm=map(Pmm, 700, 800, 0, 255);

  analogWrite(mA, Pmm);

delay(100);

}
float j=0;float i=0;
void loop2() { 

  if ( flagSensReady == true ) {
    flagSensReady= false;
    // данные готовы

   if ( OneWire::crc8(bufData, 8) == bufData[8] ) {  // проверка CRC
      // данные правильные
      temperature=  (float)((int)bufData[0] | (((int)bufData[1]) << 8)) * 0.0625 + 0.03125; 
  
      // вывод измеренной температуры на индикаторы
      if (temperature >= 0) {
        // температура положительная
        disp.print((int)(temperature * 10.), 4, 1);         
      }
      else {
        // температура отрицательная
        disp.print((int)(temperature * -1 * 10.), 3, 1);         
        disp.digit[3]= 0x40;  // отображается минус
      }
      disp.digit[1] |= 0x80;  // зажечь точку второго разряда                 
        
      // передача температуры на компьютер
      Serial.println(temperature);    
    }
    else {  
      // ошибка CRC, отображается ----
        disp.digit[0]= 0x40; 
        disp.digit[1]= 0x40; 
        disp.digit[2]= 0x40; 
        disp.digit[3]= 0x40;         
    }    
 
  } 
  
}


//-------------------------------------- обработчик прерывания 2 мс
void  timerInterrupt() {
  disp.regen(); // регенерация индикатора

  // управление датчиком DS18B20 паралллельным процессом
  timeCount++; if ( timeCount >= MEASURE_PERIOD ) { timeCount=0; flagSensReady=true; } 
  
  if (timeCount == 0) sensDs.reset();  // сброс шины   
  if (timeCount == 1) sensDs.write(0xCC, POWER_MODE); // пропуск ROM
  if (timeCount == 2) sensDs.write(0x44, POWER_MODE); // инициализация измерения

  if (timeCount == 480) sensDs.reset();  // сброс шины
  if (timeCount == 481) sensDs.write(0xCC, POWER_MODE); // пропуск ROM  
  if (timeCount == 482) sensDs.write(0xBE, POWER_MODE); // команда чтения памяти датчика  
    
 if (timeCount >= 483 && timeCount <= 491) bufData[timeCount - 483 ] = sensDs.read(); 
}
avatar

RF68

  • 16 октября 2017, 20:00

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.