Продолжаем цикл статей по написанию операционной системы с вытесняющей многозадачностью для Arduino. Для тех, кто еще не в теме:
В контроллерах AVR целая куча всяких регистров. Это регистры общего назначения, который позволяют производить вычисления и общаться с оперативной памятью, регистр флагов SREG
, регистры указателя стека (SPL
, SPH
), регистры, управляющие портами ввода-вывода и целая гора регистров для управления встроенными устройствами (таймерами, UART, АЦП, watchdog'ом и т.д.). Процесс ОС может работать с любыми из этих регистров. Но если один процесс выставляет значение какого-то регистра в единицу, а потом управление передается другому процессу, который это значение меняет, например, на 3, то первый может очень удивиться, увидев неожиданно изменившиеся данные. Для того, чтобы избежать подобных ситуаций, следует ввести понятие контекста. Под контекстом процесса мы будем понимать набор регистров, которые перед передачей управления другому процессу будут ядром системы сохраняться. Когда управление будет возвращаться к процессу, мы его контекст будем восстанавливать.
Какие же регистры мы включим в контекст? Их же целая куча! Конечно, нужно сохранять и восстанавливать регистры общего назначения, потому что они используются компилятором GCC/G++ постоянно во всех операциях (в вычислениях, при вызове функций, в циклах и условиях...). Также стоит сохранять регистр SREG
. Этот регистр содержит набор битовых флагов, которые также применяются в математических вычислениях и условных операциях. Кроме того, он содержит флаг, указывающий контроллеру, разрешать ли прерывания, и это состояние тоже неплохо бы запоминать отдельно для каждого процесса. Еще нам надо будет хранить указатель на стек, который хранится в регистрах SPH
и SPL
. Ведь у каждого процесса будет свой личный стек. Так как указатель на вершину стека 16-разрядный, а регистры 8-разрядные, их у нас два штуки: первый хранит старшие 8 байтов указателя, а второй - младшие 8 байтов. Если у контроллера 255 байтов оперативной памяти или меньше, то стековый указатель помещается в один байт, и регистр будет всего один - SPL
.
А вот другие регистры, например, ввода-вывода, сохранять мы не будем. Если какой-то процесс выставила на вывод контроллера PC0
+5 вольт, а другой вдруг выставляет уровень 0 вольт на тот же вывод, то это скорее конфликт и ошибка в программе. Если бы мы сохраняли значения портов ввода-вывода, то при переключениях между этими двумя процессами уровень на этой ножке контроллера бы постоянно переключался с 0 на +5 вольт и обратно. Это не то, что задумывалось. Поэтому подобные регистры не войдут в наш контекст, и пока что синхронизация доступа к таким регистрам будет оставаться работой самих процессов. В будущем мы, возможно, запилим некий драйвер менеджера портов, который будет выполнять эту работу.
Итак, наш контекст - это все регистры общего назначения, регистр флагов SREG
и регистры-указатели стека SPH
и SPL
. Кроме того, нас будет интересовать, какую инструкцию выполнял процесс в тот момент, когда его прервала операционная система, чтобы потом можно было бы вернуться к ней. Эта информация будет сохраняться в контексте естественным образом: когда прерывание таймера, который мы настроили в первой части статьи, будет прерывать выполнение процесса, контроллер будет автоматически класть на стек адрес возврата к инструкции, с которой нам нужно будет продолжать выполнение после возврата из прерывания. Мы будем этим пользоваться.
Но перед тем, как все это сохранять и восстанавливать, нам нужно определиться, где мы будем все данные сохранять. Для каждого процесса мы будем выделять личную область памяти для хранения контекста. Эта же область памяти будет вмещать дополнительно столько байтов для стека процесса, сколько он сам попросит.
Здесь по шагам изображено то, что лежит в области памяти, принадлежащей процессу. Когда процесс только создан (1), но не запущен, в этой памяти у нас лежит только указатель на стек процесса. Мы будем его использовать, чтобы сохранить текущий указатель стека процесса в момент переключения управления на другой процесс. Когда процесс запустился и работает (2), он может пользоваться предоставленной памятью, чтобы хранить в ней какие-то стековые переменные (а может и не пользоваться, конечно). В момент, когда процесс прерывается планировщиком (3), у нас на стеке оказывается адрес возврата на ту инструкцию, к которой мы вернемся, когда этот процесс снова получит управление. Туда его кладет сам контроллер, так работают прерывания. В этот момент выполняется ядро ОС, а процесс приостановлен. Наконец, когда ядро сохранит контекст процесса, в памяти процесса будут лежать еще и содержимое регистров общего назначения и регистра SREG
(4). Указатель на стек процесса будет сохраняться в ячейку из пункта (1), этот момент мы рассмотрим в последующих статьях. Наконец, планировщик будет восстанавливать контекст другого процесса в обратном порядке: восстановление указателя на стек и контекста (4), переход на сохраненный адрес возврата (3), выполнение процесса (2). Остановку процессов мы пока поддерживать не будем, но потом запилим и эту фишку тоже.
Напишем код, который будет сохранять на стеке процесса и загружать оттуда же содержимое регистров общего назначения и регистра SREG
. Этот код придется писать на ассемблере, потому что в C++/C попросту нет подходящих средств. Сохранять или восстанавливать контекст мы будем в два этапа, напишем для этого два макроса. Первый макрос будет сохранять/восстанавливать только регистры R31
и SREG
(и при необходимости выключать прерывания), а второй - все остальные. Такое разделение нужно, чтобы между вызовом этих двух макросов мы могли в коде планировщика задач выполнить еще какие-то действия, например, решить, какой процесс запустится следующим. После сохранения R31
и SREG
у нас будет один свободный регистр, а также отключенные прерывания. При этом мы сможем воспользоваться памятью, выделенной для стека текущего процесса, в своих целях до того, как сохраним в ней все оставшиеся регистры. Если бы мы сохраняли все регистры сразу, то свободной памяти могло и не остаться (она была бы занята содержимым этих регистров).
Итак, добавим в наш проект atmos
новый файл context_switch.h
. Начнем так:
1 2 3 4 5 6 7 8 9 |
#pragma once #include "config.h" //Номер бита "I" в регистре SREG. #define ATMOS_AVR_INTERRUPT_BIT 7 //Значение встроенного макроса __AVR_ARCH__ //для архитектуры avrtiny. #define ATMOS_AVRTINY_ARCH_ID 100 |
Тут мы определили макрос, содержащий номер бита I
, отвечающего за включение/отключение прерываний. Это нам понадобится, чтобы управлять прерываниями, устанавливая или сбрасывая этот бит в регистре SREG
. Также мы определили значение, которое принимает встроенный макрос __AVR_ARCH__
на архитектуре avrtiny
(или reduced tiny). Это архитектура некоторых контроллеров с малым объемом памяти, у которых отсутствуют регистры общего назначения R0
- R15
, а присутствуют только R16
- R31
. Запилим первый макрос, сохраняющий R31
и SREG
и отключающий прерывания, он у нас будет в двух вариантах:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#define save_r31_and_sreg_from_scheduler() __asm__ __volatile__ ( \ "push r31 \n\t" \ "in r31, __SREG__ \n\t" \ "ori r31, (1 << %0) \n\t" \ "push r31 \n\t" \ :: "I" (ATMOS_AVR_INTERRUPT_BIT) \ ) #define save_r31_and_sreg_from_task() __asm__ __volatile__ ( \ "push r31 \n\t" \ "in r31, __SREG__ \n\t" \ "cli \n\t" \ "push r31 \n\t" \ :: \ ) |
Первый макрос мы будем вызывать из планировщика, а второй - из самого процесса (если процесс, например, вызовет системную функцию sleep
). Второй макрос нам потребуется не сразу, но позже мы его обязательно применим. Что же у нас происходит? Мы сохраняем значение регистра R31
на стеке текущего процесса, затем считываем значение регистра SREG
, используя регистр R31
, выставляем в нем значение бита I
в единицу и тоже сохраняем на стеке. Выставив бит в единицу, мы сохраним на стеке значение SREG
, как если бы у нас были включены прерывания. Но так как макрос save_r31_and_sreg_from_scheduler
вызывается только из планировщика, то и попадаем мы в планировщик по прерыванию таймера, а это значит, что прерывания на момент входа в планировщик были включены. Потом, правда, контроллер их автоматически отключает, поэтому мы вручную выставляем значение бита I
, чтобы прерывания снова были включены, когда мы контекст процесса будем восстанавливать. Строка "I" (ATMOS_AVR_INTERRUPT_BIT)
служит для того, чтобы пробросить константу ATMOS_AVR_INTERRUPT_BIT
в ассемблерный код из кода C++. Эта константа потом будет там доступна под именем %0
(потому что она первая по счету, а нумерация начинается с нуля).
А вот в макросе save_r31_and_sreg_from_task
, который будет вызываться непосредственно из процесса, который хочет передать управление другому процессу самовольно, мы состояние бита I
не меняем, зато как можно быстрее выключаем прерывания, чтобы дальше заняться сохранением оставшейся части контекста процесса, и нас бы не прерывал планировщик.
Здесь самое время напомнить, почему мы не будем поддерживать архитектуру AVR XMega
. Дело в том, что эта архитектура имеет три уровня прерываний, и более приоритетные прерывания могут прерывать менее приоритетные. Настройка прерываний тоже отличается. Это лишние сложности, которые нам сейчас не нужны. В архитектурах avr
или avrtiny
уровень прерываний только один, и при входе в прерывание никакое другое прерывание не может сработать, пока мы не закончим обработку и не включим прерывания.
Перейдем к коду, сохраняющему оставшийся контекст. Он будет сохранять на стеке процесса регистры R0
- R30
для архитектур avr
и R16
- R30
для архитектуры avrtiny
. Отмечу, что я пишу "архитектуры avr
" во множественном числе не случайно: их много (avr2
, avr51
, avr6
и т.д.).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
#if __AVR_ARCH__ == ATMOS_AVRTINY_ARCH_ID # define save_context_except_r31_and_sreg() __asm__ __volatile__ ( \ "push r16 \n\t" \ "push r17 \n\t" \ "clr r17 \n\t" \ "push r18 \n\t" \ "push r19 \n\t" \ "push r20 \n\t" \ "push r21 \n\t" \ "push r22 \n\t" \ "push r23 \n\t" \ "push r24 \n\t" \ "push r25 \n\t" \ "push r26 \n\t" \ "push r27 \n\t" \ "push r28 \n\t" \ "push r29 \n\t" \ "push r30 \n\t" \ :: \ ) #else //not avrtiny # define save_context_except_r31_and_sreg() __asm__ __volatile__ ( \ "push r0 \n\t" \ "push r1 \n\t" \ "clr r1 \n\t" \ "push r2 \n\t" \ "push r3 \n\t" \ "push r4 \n\t" \ "push r5 \n\t" \ "push r6 \n\t" \ "push r7 \n\t" \ "push r8 \n\t" \ "push r9 \n\t" \ "push r10 \n\t" \ "push r11 \n\t" \ "push r12 \n\t" \ "push r13 \n\t" \ "push r14 \n\t" \ "push r15 \n\t" \ "push r16 \n\t" \ "push r17 \n\t" \ "push r18 \n\t" \ "push r19 \n\t" \ "push r20 \n\t" \ "push r21 \n\t" \ "push r22 \n\t" \ "push r23 \n\t" \ "push r24 \n\t" \ "push r25 \n\t" \ "push r26 \n\t" \ "push r27 \n\t" \ "push r28 \n\t" \ "push r29 \n\t" \ "push r30 \n\t" \ :: \ ) #endif //avrtiny |
Здесь все совсем просто. Берем и сохраняем все регистры. Неясным может быть только обнуление командой clr
регистра R17
для avrtiny
и R1
для avr
. Это делается для того, чтобы GCC/G++ не снесло башку от наших ассемблерных манипуляций. По конвенции вызовов AVR-GCC эти регистры должны быть всегда нулевыми. В то же время, некоторые инструкции могут менять значение этих регистров, а потом GCC их будет восстанавливать, однако, если планировщик прервет такую операцию посередине выполнения, может оказаться так, что значение этих регистров внутри планировщика будет ненулевым. Поэтому мы должны обнулить их значения, чтобы внутри планировщика мы могли писать код на C++, а не только на ассемблере.
А можем ли мы как-нибудь укоротить код для архитектур avr
? Имеющийся код занимает аж 64 байта программной памяти. Оказывается, можно его сделать более компактным, но ценой уменьшенного быстродействия. Укороченный код будет выглядеть так:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# define save_context_except_r31_and_sreg() __asm__ __volatile__ ( \ "push r30 \n\t" \ "push r29 \n\t" \ "clr r30 \n\t" \ "clr r31 \n\t" \ "1: \n\t" \ "ld r29, Z+ \n\t" \ "push r29 \n\t" \ "cpi r30, 29 \n\t" \ "brne 1b \n\t" \ "clr r1 \n\t" \ :: \ ); |
Тут уже логика будет посложнее, но делает она ровно то же, что и длинный вариант. Основывается этот код на той хитрости, что регистры R0
- R31
в архитектуре avr
замаплены в оперативную память и имеют адреса от 0 до 31 соответственно. То есть, если мы посмотрим, что лежит по адресу 0, то увидим значение регистра R0
, если запишем в адрес 1 некоторое число, то это число окажется в регистре R1
, ну и так далее. В коде выше мы сначала сохраняем на стеке регистры R30
и R29
(R31
у нас уже сохранен к этому моменту), потом обнуляем значения регистров R30
и R31
. Затем мы с помощью команды ld
начинаем в цикле загружать содержимое ячеек памяти по адресам от нулевого до 28-го. Это будет как раз содержимое регистров R0
- R28
. Строка ld r29, Z+
используется для косвенной адресации памяти и означает буквально "считать значение по адресу Z
, записать это значение в регистр R29
, а потом увеличить значение регистра Z
на единицу". Регистр Z
- это название 16-разрядного регистра, который составлен из двух частей: R31
(старший байт) и R30
(младший байт). Так как мы уже сохранили и обнулили эти два регистра, мы можем их использовать как указатель на память, который каждую итерацию нашего цикла будет увеличиваться на единицу. После выполнения команды ld
мы кладем значение на стек, затем сравниваем, не достиг ли наш счетчик значения 29. Когда он достигнет этого значения, это будет обозначать, что мы считали все регистры от R0
до R28
включительно и сложили их значения на стек. Регистры R29
, R30
и R31
уже были сохранены ранее. Команда brne 1b
(branch if not equal - перейти, если не равно) осуществляет переход на метку 1:
по коду выше в том случае, если значение регистра R30
(он же - младшая часть регистра Z
) оказывается не равным 29. В конце мы также очищаем регистр R1
, как и в длинном варианте макроса.
Оптимизированный по размеру код занимает всего 18 байтов программной памяти. Мы сэкономили аж 46 байтов! Но выполняться этот вариант будет в несколько раз медленнее: 63 такта для длинного кода, 209 тактов для длинного.
В файле context_switch.h
я оформил эти макросы так, чтобы пользователь сам мог выбрать, каким вариантом пользоваться. Для этого я в файл config.h
внес новую конфигурационную опцию ATMOS_OPTIMIZE_CONTEXT_SWITCH_SIZE
, которая при активации будет на архитектурах avr
включать более короткий вариант кода сохранения и загрузки контекста, но более медленный.
Давайте теперь сделаем макросы, которые будут восстанавливать сохраненный на стеке контекст. Делать это мы будем точно так же в два этапа, но в обратном порядке: первый макрос будет восстанавливать регистры R0
- R30
, а второй - R31
и SREG
. Начнем с первого макроса. Здесь я сразу приведу оба варианта (короткий и медленный, а также длинный и быстрый) для архитектур avr
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
#if __AVR_ARCH__ == ATMOS_AVRTINY_ARCH_ID //Макрос для архитектуры avrtiny, //у которой нет регистров R0-R15 # define restore_context_except_r31_and_sreg() __asm__ __volatile__ ( \ "pop r30 \n\t" \ "pop r29 \n\t" \ "pop r28 \n\t" \ "pop r27 \n\t" \ "pop r26 \n\t" \ "pop r25 \n\t" \ "pop r24 \n\t" \ "pop r23 \n\t" \ "pop r22 \n\t" \ "pop r21 \n\t" \ "pop r20 \n\t" \ "pop r19 \n\t" \ "pop r18 \n\t" \ "pop r17 \n\t" \ "pop r16 \n\t" \ :: \ ) #else //not avrtiny # if ATMOS_OPTIMIZE_CONTEXT_SWITCH_SIZE //Короткий и медленный вариант восстановления контекста //для архитектур avr # define restore_context_except_r31_and_sreg() __asm__ __volatile__ ( \ "ldi r30, 29 \n\t" \ "clr r31 \n\t" \ "1: \n\t" \ "pop r29 \n\t" \ "st -Z, r29 \n\t" \ "tst r30 \n\t" \ "brne 1b \n\t" \ "pop r29 \n\t" \ "pop r30 \n\t" \ :: \ ); # else //ATMOS_OPTIMIZE_CONTEXT_SWITCH_SIZE //Длинный и быстрый вариант восстановления контекста //для архитектур avr # define restore_context_except_r31_and_sreg() __asm__ __volatile__ ( \ "pop r30 \n\t" \ "pop r29 \n\t" \ "pop r28 \n\t" \ "pop r27 \n\t" \ "pop r26 \n\t" \ "pop r25 \n\t" \ "pop r24 \n\t" \ "pop r23 \n\t" \ "pop r22 \n\t" \ "pop r21 \n\t" \ "pop r20 \n\t" \ "pop r19 \n\t" \ "pop r18 \n\t" \ "pop r17 \n\t" \ "pop r16 \n\t" \ "pop r15 \n\t" \ "pop r14 \n\t" \ "pop r13 \n\t" \ "pop r12 \n\t" \ "pop r11 \n\t" \ "pop r10 \n\t" \ "pop r9 \n\t" \ "pop r8 \n\t" \ "pop r7 \n\t" \ "pop r6 \n\t" \ "pop r5 \n\t" \ "pop r4 \n\t" \ "pop r3 \n\t" \ "pop r2 \n\t" \ "pop r1 \n\t" \ "pop r0 \n\t" \ :: \ ) # endif //ATMOS_OPTIMIZE_CONTEXT_SWITCH_SIZE #endif //avrtiny |
С вариантами, состоящими из одних только pop
'ов, все понятно: мы просто восстанавливаем значения регистров со стека процесса, которому собираемся передать управление. А вот с коротким вариантом для архитектур avr
опять все хитрее. Сначала мы записываем в регистр R30
значение 29, затем очищаем регистр R31
. Таким образом, регистр Z
(состоящий из пары R31:R30
, как вы помните) будет указывать на 29-й байт оперативной памяти. Далее, как и при сохранении контекста, в цикле мы со стека берем одно за другим сохраненные значения регистров и начинаем их записывать в ячейки памяти, на которые мапятся регистры R28
- R0
. Команда st -Z, r29
делает следующее: уменьшает на единицу значение регистра Z
, затем берет значение из регистра R29
и записывает его в байт оперативной памяти по адресу Z
. R29
у нас сейчас выступает временным регистром для переброски данных. Потом мы проверяем, не дошли ли мы уже до нулевого адреса (командой tst r30
), и если дошли, то прекращаем цикл, а затем восстанавливаем оставшиеся регистры R29
и R30
.
Ну и второй макрос, восстанавливающий значения R31
и SREG
:
1 2 3 4 5 6 |
#define restore_r31_and_sreg() __asm__ __volatile__ ( \ "pop r31 \n\t" \ "out __SREG__, r31 \n\t" \ "pop r31 \n\t" \ :: \ ) |
Здесь у нас совсем все просто. Но нам потребуется еще такой же макрос, который помимо восстановления этих двух регистров будет еще и осуществлять переход по адресу возврата, лежащему на вершине стека после того, как мы восстановили все регистры. Этот адрес как раз будет указывать на ту инструкцию процесса, на которой мы остановились, когда забирали у него управление.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#define restore_r31_and_sreg_and_switch_context() __asm__ __volatile__ ( \ "pop r31 \n\t" \ "sbrc r31, %0 \n\t" \ "rjmp 1f \n\t" \ "out __SREG__, r31 \n\t" \ "pop r31 \n\t" \ "ret \n\t" \ "1: \n\t" \ "andi r31, ~(1 << %0) \n\t" \ "out __SREG__, r31 \n\t" \ "pop r31 \n\t" \ "reti \n\t" \ :: "I" (ATMOS_AVR_INTERRUPT_BIT) \ ) |
Здесь у нас код будет немного сложнее. Сначала мы со стека берем сохраненное значение SREG
и кладем его в регистр R31
. Потом смотрим, установлен ли там бит, отвечающий за включение прерываний. Если не установлен, то мы просто записываем это значение напрямую в SREG
, затем восстанавливаем лежащий на стеке регистр R31
и возвращаемся по адресу возврата, который у нас остался на стеке, выполняя инструкцию ret
. Если же бит установлен, то мы его сбрасываем командой andi
и только потом пишем значение в SREG
. Это делается для того, чтобы нас не оборвало какое-нибудь прерывание сразу после того, как мы установили значение SREG
, включив прерывания. Далее мы также восстанавливаем R31
и выполняем инструкцию reti
, которая не только перекинет нас по адресу возврата, который лежит на стеке, но и атомарно включит прерывания. Такой финт не прокатит на архитектуре AVR XMega
, и это еще одна причина, почему мы ее не поддерживаем.
Давайте теперь протестируем весь этот код. Проверять будем, используя симулятор, встроенный в Atmel Studio. Напишем в файле main.cpp
следующий код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
#include "kernel/context_switch.h" int main() { __asm__ __volatile__ ( "clr r30 \n\t" "clr r31 \n\t" "ldi r29, 1 \n\t" "1: \n\t" "st Z+, r29 \n\t" "inc r29 \n\t" "cpi r29, 30 \n\t" "brne 1b \n\t" "ldi r31, 123 \n\t" "out __SREG__, r31 \n\t" "ldi r29, 30 \n\t" "ldi r30, 31 \n\t" "ldi r31, 32 \n\t" :: ); save_r31_and_sreg_from_task(); save_context_except_r31_and_sreg(); __asm__ __volatile__ ( "clr r30 \n\t" "clr r31 \n\t" "clr r29 \n\t" "1: \n\t" "st Z+, r29 \n\t" "cpi r30, 29 \n\t" "brne 1b \n\t" "clr r30 \n\t" "out __SREG__, r31 \n\t" :: ); restore_context_except_r31_and_sreg(); restore_r31_and_sreg(); while(true) {} } |
Кратко поясню, что здесь происходит.
- Сначала мы записываем в регистры
R0
-R31
значения от 1 до 32, значение 123 вSREG
. - Сохраняем контекст, в который как раз входят все эти регистры. Сохранять его будем на стеке текущей функции, так как никаких процессов у нас пока нет, и мы тестируем сам функционал сохранения и загрузки регистров.
- Очищаем все регистры -
R0
-R31
иSREG
. - Восстанавливаем их из сохраненного ранее контекста.
Ставим точку остановка на самое начало функции main
, кликнув на полосе слева от кода:
Выбираем конфигурацию Debug
и запускаем проект. Когда мы нажмем F5
, Atmel Studio ругнется, что нужно выбрать, как мы хотим отлаживать код, и откроет окно выбора. Нужно выбрать Simulator
и сохранить настройки проекта:
Снова жмем F5
. Исполнение будет остановлено в самом начале функции main на нашем брейкпоинте. Далее нам нужно открыть окно Processor Status, чтобы видеть содержимое регистров контроллера. Жмем Debug - Windows - Processor Status. Теперь нажимаем F10
, выполняя таким образом первую ассемблерную вставку. Видим, что во всех регистрах R0
- R31
теперь записаны числа от 1 до 32, соответственно. Также можно обратить внимание на флаги, выставленные в регистре SREG
.
Нажимаем F10
трижды, выполняя сохранение контекста и ассемблерную вставку, очищающую все регистры. Видим, что везде теперь нули, а в SREG
все флаги сброшены:
Снова нажимаем дважды F10
. Наша программа перейдет на бесконечный цикл и начнет выполняться, мы ее остановим нажатием кнопки "Пауза". Контекст должен быть восстановлен, проверим это:
Видим, что значения регистров R0
- R31
и SREG
восстановились до первоначальных значений. Наши макросы работают правильно! Теперь можно выполнить такую же проверку, переключив значение макроса ATMOS_OPTIMIZE_CONTEXT_SWITCH_SIZE
в файле config.h
с нуля на единицу (или наоборот), чтобы задействовать другой набор макросов. Я также выполнил проверку макросов для архитектуры avrtiny
, немного изменив код ассемблерных вставок, записывающих в регистры общего назначения разные числа, а потом обнуляющих их.
На этом на сегодня все: мы написали код, который умеет сохранять и восстанавливать контекст процесса и успешно проверили его с разными настройками на разных архитектурах. Скачать полный код солюшена можно здесь: atmos full solution, актуальные изменения также доступны на GitHub проекта atmos.
Теория понятна.
Добрый день!!! Очень интересно. Конечно, лепить операционку для Arduino - это круто !!! Но познавательно со всех сторон. Типа как из гомна пулю слепить .... Еще раз - спасибо !!! ... Лепить пули - это мая карма ... Жаль, только, жизнь прошла ...