Немного про ARM ассемблер. Пишем многопоточную программу.
Пишем простую многопоточную программку на ARM ассемблере.
Ассемблер не плохой язык программирования. Одновременно и мощный и сложный, но не такой сложный как можно подумать. Предлагаю развенчать мифы об этом. Вспомнить наши корни и попробовать написать программу на ассемблере под ARM Cortex-M3
, в качестве подопытного будем использовать отладку с алиэкспрес для микроконтроллера stm32f103c8
.
Если заинтересовал - читайте дальше.
Проект будем делать в Keil. Создаем пустой проект и не добавляем ни одного пакета.
Код простейшей программы
Окунёмся же сразу в пучину ассемблерных команд. И по ходу пьессы буду объяснять некоторые тонкости.
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE 0x00000100
__initial_sp
PRESERVE8
THUMB
AREA RESET, DATA, READONLY
__Vectors DCD __initial_sp ; Указатель вершины основного стека
DCD Start ; Указатель на начало программы
SPACE (12 * 4) ; Пропуск 12-и ненужных векторов
DCD PendSV_Handler ;
DCD SysTick_Handler ;
; тут ещё могут быть прерывания
PRESERVE8
AREA |.text|, CODE, READONLY
Start PROC
BL INIT_MCU ; Инициализация микроконтроллера
INF_LOOP B . ; Вечный цикл
ENDP
Этот код ничего не делает.
Читайте сверху вниз.
Сначала размечается область для стэка в оперативной памяти, за подробностями обращайтесь к документации (ссылки оставлю в конце заметки). В области STACK
резервируются 0x100
байт памяти. __initial_sp
- это метка(указатель) на вершину стэка. Стек здесь растёт в сторону уменьшения адреса.
Следующая область - это область с данными доступными только для чтения (располагается в ПЗУ). Порядок констант должен быть именно таким и ни каким иначе. Перным словом идёт указатель на стэк, вторым указатель на точку входа в программу, затем таблица векторов прерываний.
PRESERVE8
- необходимо для выравнивания.
Далее идёт область с кодом, что и понятно исходя из описания области. В ней как раз располагаются все алгоритмы, подпрограммы и функции. И наша функция не исключения, в ней вызывается функция для инициализации микроконтроллера, а затем переходим в бесконечный цикл. Метка INF_LOOP
здесь не обвязательно, так как переход осуществляется командой B .
, она нужна будет в будущём при работе с несколькими потоками.
Разобрали простейщий код - пора приступать к более значительной задаче, будем управлять 4-я потоками, два из них будут выводить в USART1
разный текст, и два будут мигать светодиодами с разной частотой.
Сразу говорю, полный код я не дам, я лишь расскажу как всё это делается.
Потоки (задачи)
И так, как же будет выглядеть подпрограмма реализующая наш поток:
TASK_LED PROC
; Для BitBand под рукой всегда должны быть нолик и единичка
MOV R11, #0
MOV R12, #1
; BitBand для PC13.ODR
MOV32 R0, (GPIOC_ODR & 0x00FFFFFF) * 0x20 + 0x42000000 + 13 * 4
; Цикл с мигалкой
SCL STR R12, [R0] ; Выключаем светодиод
BL Delay ; Вызов функции задержки
STR R11, [R0] ; Включаем светодиод
BL Delay ; Вызов функции задержки
B SCL
BX LR
ENDP
Здесь тотже самый вечный цикл. Это необходимо, что бы задача не прекращала своё выполнение, если этого не будет, то функция выполниться и вернётся по адресу записанному в LR
.
Для того, чтобы переход в функцию при переключени контекста сработал нормально необходимо понимать, что происходит при возврате из прерывания, так как именно из прерыванию будет передаваться управления задачам.
А именно, при возникновении прерывания в стек автоматически сохраняются некоторые регистры, в таком порядке: xPSR
, PC
, LR
, R12
, R3
, R2
, R1
, R0
. Следовательно стек задачи нужно стразу инициализировать нужными значениями:
FILL_STACK_TASK PROC
PUSH {LR}
; Значения по-умолчанию для заполнения стэков
MOV32 R1, #0x01000000 ; Значение для xPSR
MOV32 R3, #0x0 ;
LDR R4, =INF_LOOP ; Возврат из задачи в бесконечный цикл
; Регистры сохраняемые при возникновении прерывания
STR R1, [R0, #-0x04] ! ; xPSR = 0x01000000
STR R2, [R0, #-0x04] ! ; PC = &TASK & 0xfffffffe
STR R4, [R0, #-0x04] ! ; LR = INF_LOOP
STR R3, [R0, #-0x04] ! ; R12 = 0
STR R3, [R0, #-0x04] ! ; R3 = 0
STR R3, [R0, #-0x04] ! ; R2 = 0
STR R3, [R0, #-0x04] ! ; R1 = 0
STR R3, [R0, #-0x04] ! ; R0 = 0
; Дополнительные регистры тоде можно сохранить
POP {PC}
ENDP
Здесь в R0
хранится указатель на стек этой задачи, в ресистре R2
- указатель на процедуру задачи, с наложеной маской 0xfffffffe
(Это магия возврата из прерываний, в документации на ядро можно почитать более подробно), как уже заметили ригистр связи LR
инициализируем указателем на наш бесконечный цикл, на случай если выйдем из процедуры, если этого не сделать можно словить фатальную ошибку, а так нет, просто будем вхолостую гонять вечный цикл вместо выполнения этой задачи.
Переключение контекста
Идём дальше, мы реализовали одну из задач, реализовали процедуру инициализации стека, осталось научиться переключать контекст. Первое что приходит на ум, это переключать его в прерывании с определенным интервалом времени, пусть будет милисекунда. Системный таймер? И да и нет. Если просто переключить контекст в прерывании системного таймера, то можно также словить фатальную ошибку. Необходимо использовать специальное прерывание, которое есть в ядре ARM Cortex-M3
- PendSV
.
PendSV
прерывание должно иметь самый низкий приоритет, что бы оно гарантированно было выполнено после всех остальных прерываний. И ещё это программное прерывание, это означает что его нужно принудительно вызывать:
MOV32 R0, ICSR
MOV32 R1, SCB_ICSR_PENDSVSET
STR R1, [R0]
Вызвать можно из другого прерывания или нет. Но мы будем вызыватьего из прерывания системного таймера.
Переключение контекста будет выполняться по следующему алгоритму:
- Если мы первый раз вошли в планировщик, то мы просто меняем значение регистра
SP
на указатель стека первой задачи и завершаем прерывание, всё, теперь мы выполняем первую задачу до возникновения следующего прерывания переключения контекста. - Теперь нам необходимо сохранить адрес стэка из регистра
SP
, взять адрес следующего стека и применить его, выйти из прерывания. Теперь мы выполняем следующую задачу и так далее.
Код реализующий это может выглядить примерно так:
PendSV_Handler PROC
PUSH {r4-r11} ; Сохраняем дополнительные регистры
; Сбрасываем флаг прерывания
MOV32 R0, ICSR
MOV32 R1, SCB_ICSR_PENDSVCLR
STR R1, [R0]
; Получаем ячейку с нужным стэком
LDR R0, =Task_Runing
LDR R1, [R0] ; Номер текущего стэка
AND R1, R1, #0x3 ; Очищаем лишнее по маске
; Получаем указатель на указатель на текущий стек
LDR R2, =Task_SP ; Указатель на массив указателей стеков
MOV R12, #4 ; Количество байт в слове
MLA R12, R12, R1, R2 ; ADR = 4* N + ADRSP[]
LDR R3, [R12] ; Адрес стека для нужной задачи
; Проверяем первый ли это запуск планировщика
LDR R4, =Task_IsRuning
LDR R5, [R4]
CMP R5, #1
BEQ IS_NOFIRST
; Это первый запуск планировщика
MOV R5, #1
STR R5, [R4]
MSR MSP, R3 ; Переключаем стэк на первую задачу
B EXIT_PSV
IS_NOFIRST ; Не первый раз мы здесь
MRS R6, MSP ; получаем SP
STR R6, [R12]
; Номер следующего стэка
ADD R1, R1, #1
AND R1, R1, #0x3
STR R1, [R0]
MOV R12, #4
MLA R12, R12, R1, R2
; Адрес стека для задачи
LDR R3, [R12]
; Переключаем стэк на первую задачу
MSR MSP, R3
EXIT_PSV
POP {r4-r11} ; Восстанавливаем дополнительные регистры
BX LR
ALIGN 4
ENDP
PRESERVE8
Да и кстати нужно не забыть выделить память для всех наших переменных, что-то вроде этого:
;-----------------------СЕКЦИЯ СО СТЭКАМИ ЗАДАЧ----------------------------------------------------
AREA STACK_TASK, DATA, READWRITE, ALIGN=3 ; Секция со стеками задач
Stack_A_Mem SPACE 0x00000100 ;
Stack_A ; указатель на вершину A
Stack_B_Mem SPACE 0x00000100 ;
Stack_B ; указатель на вершину B
Stack_C_Mem SPACE 0x00000100 ;
Stack_C ; указатель на вершину C
Stack_D_Mem SPACE 0x00000100 ;
Stack_D ; указатель на вершину D
;-----------------------СЕКЦИЯ С ПЕРЕМЕННЫМИ-------------------------------------------------------
AREA VAR, DATA, READWRITE, ALIGN=3
Task_IsRuning DCD 0 ; Номер текущей задачи
Task_Runing DCD 0 ; Номер текущей задачи
Task_SP DCD Stack_A, Stack_B, Stack_C, Stack_D ; Указатели стека
ALIGN
Литература:
- RM0008 Reference manual, - Описание микроконтроллера
- PM0056 Programming manual - Описание ядра Cortex®-M3 и его команд.
- ARM and Thumb instruction
PS
Вроде это всё,если есть вопросы, пишите. Повторю ещё раз - код привезенный в этой заметке не полный проект, а лишь небольшой материал для дальнейшего изучения.
Как обычно, хорошего кодинга и поменьше багов.