Одна маленькая загадка про Cortex-M

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

    На днях, в прцессе портирования FreeRTOS на микроконтроллер с ядром Cortex-M1, о котором я уже писал, возник маленький вопрос, который совершенно неожиденно яростно сопротивлялся всем попыткам найти на него ответ при помощи ГУГЛА всемогущего. Причем в процессе поиска выяснилось, что этот вопрос интересовал не меня одного, а, значит, не может быть следствием врожденной (либо приобретенной) тупости вопрошающего, ну или, в крайнем случае, свидетельствует, что таковая встречается не столь уж редко. Слегка озадаченный невозможностью применить обычный способ поиска ответов, решил прибегнуть к более экзотическому и слегка забытому — подумать и найти ответ самостоятельно. К сожалению, тоже не получилось, равно как не помогла и попытка проконсультироваться с другими неглупыми людьми (сам себя не похвалишь — весь день ходишь как оплеванный). Поскольку на Хабре таковых должно быть в избытке, попробуем экстенсивный путь решения путем вовлечения в этот процесс еще большего количества специалистов. Поэтому вместо победного поста пишу пост жалобный — помогите, люди добрые, кто чем может. Итак, переходим к сути проблемы.

    В процессе переключения задачи возникает необходимость сохранения и последующего восстановления контекста процесса. Очевидно, что этот процесс является аппаратно-зависимым, и в процессе портирования к нему должно быть особое внимание. Поскольку за основу бралось решение для M0, на архитектуру M1, которая является подмножеством вышеуказанной, все встало без проблем. Тем не менее решил посмотреть коды данного участка, чтобы получить немного экспы. И вот тут меня ждала некоторая неожиданность, а именно: код мне показался замысловатым, поскольку вместо ожидаемых команд PUSH имелась следующая картина:
    xPortPendSVHandler:
    ; сохраняем контекст текущей задачи - комментарий мой
    	mrs r0, psp
    	ldr	r3, =pxCurrentTCB	/* Get the location of the current TCB. */
    	ldr	r2, [r3]
    	subs r0, r0, #32		/* Make space for the remaining low registers. */
    	str r0, [r2]			/* Save the new top of stack. */
    
    	stmia r0!, {r4-r7}		/* Store the low registers that are not saved automatically. */
    	mov r4, r8				/* Store the high registers. */
    	mov r5, r9							
    	mov r6, r10							
    	mov r7, r11							
    	stmia r0!, {r4-r7}
    ; определяем номер задачи, на которую переключимся
    	push {r3, r14}						
    	cpsid i								
    	bl vTaskSwitchContext				
    	cpsie i
    	pop {r2, r3}			/* lr goes in r3. r2 now holds tcb pointer. */
    ; восстанавливаем ее контекст и запускаем
    	ldr r1, [r2]						
    	ldr r0, [r1]			/* The first item in pxCurrentTCB is the task top of stack. */
    	adds r0, r0, #16		/* Move to the high registers. */
    	ldmia r0!, {r4-r7}		/* Pop the high registers. */
    	mov r8, r4							
    	mov r9, r5							
    	mov r10, r6							
    	mov r11, r7																		
    	msr psp, r0				/* Remember the new top of stack for the task. */											
    	subs r0, r0, #32		/* Go back for the low registers that are not automatically restored. */
    	ldmia r0!, {r4-r7}		/* Pop low registers.  */
    	bx r3		
    vPortSVCHandler;	
    ...		
    vPortStartFirstTask		
    ...
    
    Кстати, пользуясь случаем, еще до разбора собственно вопроса, хотел бы проклясть авторов этого кода. Обратите внимание, что три метки записаны в разном формате — с двоеточием в конце, без двоеточия в конце (что допускается описанием языка) и без двоеточия, но с точкой с запятой, открывающей отсутствующий комментарий. Если учесть, что в последнем случае метка еще и переопределялась директивой препроцессора, это мне стоило некоторого времени в попытке понять, почему сделано именно так. Ответ «потому что» был найден довольно-таки быстро и удовольствия не принес. Далее, в первой и четвертой строке кода вычисляют значение, которое в пятой строке отправляют по адресу, вычисляемому во второй и третьей строке. Ну зачем разрывать вычисление значения вычислением адреса? С одной стороны, отрадно, что пренебрежение стилем имеет международный характер, а не является нашей национальной особенностью, с другой стороны, оптимизма не добавляет. Вспоминается классическое «Не стоит искать злой умысел в том, что можно объяснить обычной глупостью». Но это так, лирическое отступление на тему яркости солнца и зелености травы. Вернемся собственно к задаче.
    Как нетрудно видеть, сохранение части контекста процесса, а именно регистров r4-r11, происходит в строках с 7 по 12, причем с использованием индексной множественной пересылки (остальная часть контекста, регистры r0-r3 и r12-r15, была сохранена в процессе обработки исключения. Почему же используются не команда PUSH, а команда длинной пересылки, причем с пересылками регистр-регистр (команда длинной пересылки работает не дальше регистра r7). Ну во-первых, к сожалению, команда PUSH в архитектуре M работает тоже недалеко, так что пересылок не избежать, но все равно было бы намного понятнее происходящее. Вот тут то и порылась собака.
    Дело в том, что в М архитектуре существуют два режима работы — threa_d_ mode (назовем его пользовательским) и Handler (назовем его системным). Такие названия вполне соответствуют духу, поскольку режим Handler включается для обработки прерывания, которая свойственна именно системному уровню. Есть еще привилегированный и непривилегированный режимы, но в M1 их все равно нет (они неразличимы). Далее, в архитектуре М существуют два указателя стека, MAIN (назовем его системным) и Process (назовем его пользовательским). Данное именование тоже вполне оправданно, поскольку после сброса используется MAIN указатель, а это явно уровень системы. При этом оба указателя имеют уникальные имена в пространстве специальных регистров, MSP и PSP соответственно, что использовано в первой строке кода. Помимо уникальных имен, для доступа к указателю стека есть и регистр (внезапно) указателя стека, который показывает нам один из вышеперечисленных двух под управлением бита в специальном регистре (за подробностями обращайтесь к документации ARM). Пока все выглядит логично, смотрим далее.
    В пользовательском режиме МК возможно переключение этого бита и, соответственно, доступ к обоим указателям стека. Ну лично я бы такого права этому режиму не дал во избежание, но кто я такой, чтобы спорить с фирмой ARM, проехали. А вот в системном режиме МК имеет доступ ТОЛЬКО к системному указателю стека и не может переключить значение этого бита. Поэтому он не может напрямую писать в пользовательский стек через команды обращения к стеку. При этом, конечно, остается возможность обращения к соответствующей области памяти через регистровое индексирование, что и делается в подпрограмме, но у меня возник вопрос «Почему так сделано»?.. Почему пользовательскому режиму разрешают переключать указатели и, возможно, выстрелить себе в ногу путем краха системного стека, а системному режиму, который должен быть спроектирован более тщательно специально обученными людьми, в такой возможности отказано? Если бы такое разрешение было бы дано обоим режимам, не было бы вопроса — разработчики не посчитали нужным делать защиту, это их право. НО для системного режима эта возможность сознательна запрещена, значит есть часть аппаратуры, за этот запрет отвечающая. Конечно, эта часть не слишком сложна и я сам могу предложить пару простеньких вариантов, но она не могла появиться сама собой. Значит, есть основания так делать, только я их не понимаю. Покрутил в голове варианты, связанные со вложенными прерываниями, ничего не придумал. К сожалению, на сайте ARM ответа не нашел, там пишут о том, КАК работает эта часть МК, а ПОЧЕМУ не сказано (может это сакральное знание и, получив его, можно научиться создавать архитектуры не хуже ARMовских). С тайной надеждой, что все именно так и выношу данный вопрос на суд Хабра-сообщества, жду Ваших вариантов ответа.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 19

      –10
      эта часть не слишком сложна и я сам могу предлоюить пару простеньких вариантов


      исправьте пожалуйста.
        +3
        Спасибо, исправил
        0
        Вопрос дилетанта: чем еще отличаются MAIN и Process?
        Может MAIN это все же пользовательский?
          0
          В том то и дело что MAIN системный.
          0
          Здесь же прерывание — вот и обсчитывают стек.
            0
            > Ну зачем разрывать вычисление значения вычислением адреса?
            Обычная оптимизация?
            Насколько я помню, у Cortex-M 3хстадийный конвейер с тремя «EU». Причём LDR-операции идут в 2 фазы, загрузки адреса и загрузки данных.
            Конкретно с Cortex-M не работал в плане оптимизаций, но думаю что вполне логично перемешивать инструкции «вручную», ведь OoO-то нет

            Основной вопрос пока внимательно не обдумывал, даже не вчитался до конца, но докапываться «почему» так сделано в процессоре довольно сложно.

            PS: У меня есть контакты Richard Barry, основного разработчика RTOS, если интересно, можно напрямую его спросить.
              0
              Ок, если сократить, то вопрос на самом деле звучит просто:
              «Почему нельзя из Handler mode писать напрямую в process stack.».

              Несогласен немного с тем, что требуется дополнительная аппаратура для реализации этой фичи, скорее наоборот, ибо сейчас можно сделать простую проверку:
              if (SPCR[1] && mode == ThreadMode)
                sp = processSP
              else
                sp = mainSP
              


              Думаю что так сделано просто исходя из задач этих двух типов стека. Process_SP в Handler mode может понадобиться как правило только для планировщика.
              На это ясно намекают строки в мануале:
              Using the process stack for the Thread mode and the main stack for exceptions supports
              Operating System (OS) scheduling. To reschedule, the kernel only requires to save the
              eight registers not pushed by hardware, r4-r11, and to copy SP_process into the Thread
              Control Block (TCB). If the processor saved the context on the main stack, the kernel
              would have to copy the 16 registers to the TCB
                0
                Ну так в планировщике то он напрямую не доступен, поскольку планировщик работает в Handler режиме.
                  0
                  Дык я о том и пишу, что в Handler mode нужен доступ к process_sp только в одном случае — планировщик.
                  А в нём уже можно и напрямую к памяти обратиться, а городить ради этого вырожденного случая доступ к PSP в Handler mode не обязательно.
                  0
                  Здесь вопрос не к RTOS, а к ядру МК.
                  0
                  Подумаю вслух :)

                  Допустим в режиме прерывания можно было бы переключать стеки.
                  Допустим код планировщика переключил стек с Main на Process.
                  В этот момент происходит другое прерывание, с более высоким приоритетом, оно отработает на стеке Process.
                  Казалось бы — что в этом плохого, ведь прерывание подчистит за собой и все будет хорошо? Нет, не будет. При таком раскладе придется в размер стек каждого потока закладывать не только его потребности, но и учитывать сколько съедят все прерывания — теряется вся прелесть двух указателей. Либо в начале каждого обработчика добавлять пролог переключающий стек на Main — и именно это инженеры ARM сделали за нас на аппаратном уровне.
                    0
                    Ну вот тут, пожалуй, соглашусь, что размер стека процесса существенен. Я остановился на фазе, что все подчистит и успокоился.
                    Конечно, можно запретить перед переключением стека прерывания, тем более что это делается несколькими командами ниже, но это как то не очень хорошо.
                    Но все равно на вопрос не отвечает — согласен, что нам не следует напрямую переключать стек в данном случае, особого выигрыша не будет, но почему нам это ЗАПРЕТИЛИ делать в режиме супервизора и оставили в пользовательском? Вот что непонятно.
                      0
                      А если подойти «с другой стороны» — запрещали-ли? Просто не предусмотрели возможность, т.к. не посчитали нужным. Прерывания всегда работают на стеке Main — это удобно. Основной код приложения может работать на стеке Main при отсутствии ОС, либо на стеке Process при наличии OS. Переключение кода приложения на стек Process — задача загрузчика ОС.

                      Почему регистр Control в режиме прерывания не пишется? Потому что их два и доступный в данный момент привязан к текущему режиму (приложение/обработчик).

                      PS Тут нельзя повернуть не потому что запрещено, а потому что дороги нет.
                        0
                        Если предположение выше верно — то вполне объясняет, почему. Если требовалось обеспечить, чтобы в режиме супервизора всегда стек указывал на Main — то все логично выглядит. Похоже, что при переходе из пользовательского режима в режим супервизора стек переключается принудительно, а вот при переходе из супервизова в супервизора (вложенные прерывания) — нет. А раз нет, значит, нужно обеспечить, чтобы стек оставался неизменным, пока мы в супервизоре.
                      +3
                      Ну зачем разрывать вычисление значения вычислением адреса?

                      Чтобы скрыть load-to-use латентность LDR. Но в случае Cortex-M это не нужно.

                      Почему же используются не команда PUSH, а команда длинной пересылки, причем с пересылками регистр-регистр

                      1) PUSH работает с регистром R13. Который в данном случае указывает не на пользовательский стек.
                      о чём как бы намекают push {r3, r14} / bl vTaskSwitchContext

                      2) Как вы видите, сначала сохраняются младшие регистры (ldmia), потом старшие. В случае PUSH нужно делать наоборот.
                        0
                        FreeRTOs весьма не оптимально это делает — они же под несколько архитектур заточены и этот код у них во много содран со старых ARM-7. И про переключение стеков — в user mode же эта команда запрещена. Так что в ногу выстрелить нельзя — единственный способ переключить стек и режим — SVC.
                          0
                          И это совершенно правильно — для переключения стека надо обратиться к супервизору.
                          Но когда супервизору это запретили, а юзеру — нет, я перестаю что либо пониматью
                          0
                          Возможно, это сделано для одностороннего вызова из пользовательского кода в код ядра: например, вы записываете аргументы в системный стек и вызываете программное прерывание — таким образом происходит системный вызов. Обратный же вызов должен быть запрещен — переход от ядра к пользовательскому коду должен быть только по команде reti — возврат из прерывания. Причина тому — возврат из пользовательской функции, который, в таком случае, должен был бы вернуться в ядро, а это плохая идея, поскольку, установка адреса возврата стала бы доступна пользовательскому коду.
                            +1
                            А по мне так всё логично. Переключение на работу в своём стеке должно производиться не в прерывании (в 90% случаев). Обычно это делается при инициализации. А значит переключать должен Thread, а не Handler. Поэтому ему и дали эту функцию. Предполагается, что переключение стека должно производиться один раз при инициализации.

                            А теперь посмотрим на мой любимый Cortex-M3. Там есть привилегированный режим. Включаем его и Handler, пусть и неудобно, но может всё (и правильно, что неудобно, раз неудобно — значит что-то делается неправильно), а вот Thread может быть ограничен в своей песочнице контроллером MMU. Поменять стек он больше не может, т.к. в непривилегированном режиме ему это запрещается. Вот вам и главенство Handler'а (ядра) над Thread'ом (приложением).

                            Cortex-M0/M1 разрабатывались не для исполнения ОС. Они под firmware сделаны с одним фоновым процессом и обработчиками. Просто там ко всему прочему дали возможность иметь разные стеки для обработчиков и фонового процесса — и всё. Остальных элементов — привилегированного режима и MMU -, присущих процессорам под ОС, там нету. Т.ч. это не MPU, а MCU и, строго говоря, ОС на них крутить — значит использовать не совсем по назначению. А раз так, то и не надо удивляться, что задачи приходится решать «криво».

                            Only users with full accounts can post comments. Log in, please.