Код запуска (startup) на PIC32

PIC
Продолжаем углубляться в процесс загрузки контроллеров PIC32MX. В этой части будет станет понятно как запускается функция main.

Это пятая статья из цикла: один, два, три, четыре.
Мы уже разобрали часть скрипта линковщика и сейчас готовы приступить к разбору кода.
Начало работы пойдёт с первой инструкцией секции ".startup".
Эта секция находится в файле crt0.S

.section .startup,"ax",@progbits
        .set noreorder
        .ent _startup
_startup:
        mfc0    k0,_CP0_STATUS
        ext     k0,k0,19,1              # Extract NMI bit
        beqz    k0,_no_nmi
        nop
        la      k0,_nmi_handler
        jr      k0
        nop

Сразу видим функцию _startup, эта функция используется в том числе для обработки некоторых прерываний, например reset или немаскируемых прерываний (non-maskable interrupt (NMI)).

В качестве NMI может выступать сторожевой таймер (watchdog timer) или програмный reset
_nmi_handler:
9d0015b4 <_nmi_handler>:
9d0015b4:       401a6000        mfc0    k0,$12 # Register 2-5: STATUS: Status Register; CP0 Register 12, Select 0
9d0015b8:       3c1bffbf        lui     k1,0xffbf
9d0015bc:       377bffff        ori     k1,k1,0xffff
9d0015c0:       035bd024        and     k0,k0,k1 # k0 = 0xFFBF FFFF
9d0015c4:       409a6000        mtc0    k0,$12 # Выключаем BEV
9d0015c8:       42000018        eret

Открываем PIC32MX Family Reference Manual

Register 2-5: STATUS: Status Register; CP0 Register 12, Select 0 (Continued)
bit 22 BEV: Control bit. Controls the location of exception vectors.
0 = Normal
1 = Bootstrap

Что такое этот Bootstrap?
Описано в «8.6 INTERRUPT VECTOR ADDRESS CALCULATION».
После любого резета мы входим в режим Bootstrap (самозагрузка, поднятие себя из болота за волосы).
Бит BEV при этом включен (значение 1). Пока мы находимся в режиме Bootstrap, все прерывания выключены, а исключения всегда отправляются по адресу 0xBFC00380. После того, как мы настроили прерывания (один обработчик прерывания или несколько, базовый адрес обработчиков прерываний EBASE) надо выключить BEV.

Нормальный старт (не NMI)
_no_nmi:
    la      sp,_stack # устанавливаем указатель стека (stack pointer)
    la      gp,_gp # устанавливаем global pointer (глобальный указатель) на середину региона данных


Переменная _stack вычисляется на стадии линковки с помощью скрипта линковки
chipKIT-UNO32-application-32MX320F128L.ld

  _stack = (_ramfunc_length > 0)
         ? _ramfunc_begin - 4
         : ORIGIN(kseg1_data_mem) + LENGTH(kseg1_data_mem) ;

Если отбросить функции, код которых может находиться в памяти, то адрес в sp будет первым адресом после сегмента памяти. Т.е. sp указывает на последний занятый элемент в стеке. Стек очень важная вещь при вызове функции из функции (сохраняем $ra), обработчики прерываний так же нуждаются в стеке, нужен для реализации многозадачности и локальных переменных (не регистровые, неглобальные переменные внутри функции).

Global Pointer
Команды загрузки mips (lb, lw) могут использовать базовый адрес в регистре + 16битное смещение.
Значит без загрузки числа в регистр мы можем адресовать только 64 килобайта данных. Смещения могут быть как положительные, так и отрицательные.
В каждой программе есть глобальные переменные, которые могут быть часто использованы. Чтобы ускорить загрузку таких переменных из памяти в регистр, выделяют спецальный регистр для хранения адреса середины сегмента глобальных переменных.
Глобальные переменные находятся в сегменте .data

.data   :
  {
    _data_begin = . ;
    *(.data .data.* .gnu.linkonce.d.*)
    KEEP (*(.gnu.linkonce.d.*personality*))
    
    *(.data1)
  } >kseg1_data_mem AT>kseg0_program_mem
  _data_image_begin = LOADADDR(.data) ;
  
  . = .;
  _gp = ALIGN(16) + 0x7ff0;


_gp вычисляется линковщиком как конец начало сегмента(.data) + 32000

Следующий кусок кода можно понять прочитав описание из:
«INTERRUPTS AND REGISTER SETS»
mfc0    t1,_CP0_SRSCTL          # Read SRSCtl register
        add     t3,t1,zero              # Save off current SRSCtl
        ext     t2,t1,26,4              # to obtain HSS field
        ins     t1,t2,6,4               # Put HSS field
        mtc0    t1,_CP0_SRSCTL          # into SRSCtl<PSS>
        wrpgpr  gp,gp                   # Set global pointer in PSS
        mtc0    t3,_CP0_SRSCTL          # Restore SRSCtl

В семействе PIC32MX есть два набора регистров:
  • нормальный набор
  • набор для обработки прерываний

С помощью такого хитрого кода, мы можем сделать копию gp в теневом наборе регистров и прерывания будут правильно обрабатываться (будет доступ к глобальным переменным программы).

la      t0,_on_reset
        jalr    t0
        nop

Мы можем сделать обработчик резета, функция _on_reset (или в C on_reset). Вызываться он будет здесь.

Better Save Space
la      t0,_bss_begin
        la      t1,_bss_end
        b       _bss_check
        nop

_bss_init:
        sw      zero,0x0(t0)
        sw      zero,0x4(t0)
        sw      zero,0x8(t0)
        sw      zero,0xc(t0)
        addu    t0,16
_bss_check:
        bltu    t0,t1,_bss_init
        nop

В сегмент .bss попадают переменные, которые должны сразу после старта иметь нулевое значение.

Зачем это нужно?
Если мы инициализируем переменные, мы должны сохранить значения во флеше и перед стартом прошивки их загрузить. BSS позволяет сократить использование флеша тем, что мы не сохраняем нули во флеше, и эти нули существуют только в памяти.

Загружаем иннициализированные данные:
la      t0,_data_image_begin
        la      t1,_data_begin
        la      t2,_data_end
        b       _init_check
        nop

_init_data:
        lw      t3,(t0)
        sw      t3,(t1)
        addu    t0,4
        addu    t1,4
_init_check:
        bltu    t1,t2,_init_data
        nop


Здесь мы берём данные из флеша по адресу _data_image_begin закидываем их в _data_begin, пока все инициализированные данные не кончатся.

Код вызываемый из памяти (а не из флеша)
la      t1,_ramfunc_length
        beqz    t1,_ramfunc_done
        nop
        la      t0,_ramfunc_image_begin
        la      t1,_ramfunc_begin
        la      t2,_ramfunc_end

_init_ramfunc:
        lw      t3,(t0)
        sw      t3,(t1)
        addu    t0,4
        addu    t1,4
_ramfunc_check:
        bltu    t1,t2,_init_ramfunc
        nop
        la      t1,_bmxdkpba_address
        la      t2,BMXDKPBA
        sw      t1,0(t2)
        la      t1,_bmxdudba_address
        la      t2,BMXDUDBA
        sw      t1,0(t2)
        la      t1,_bmxdupba_address
        la      t2,BMXDUPBA
        sw      t1,0(t2)
_ramfunc_done:

Если есть функции, которые вызываются прямо из памяти, то записываем их по нужному адресу.
BMXDKPBA — базовый адрес пространства ядра
BMXDUDBA — базовый адрес пространства пользователя

У нас есть только пространство пользователя, потому что мы пишем под контроллер без защиты памяти (MMU), используем только kernel space адреса.

mtc0    zero,_CP0_COUNT # у нас только один процессор
        li      t2,-1
        mtc0    t2,_CP0_COMPARE
        la      t1,_ebase_address
        mtc0    t1,_CP0_EBASE # устанавливаем базовый адрес для обработчиков прерываний
        li      t2,-1
        mtc0    t2,_CP0_COMPARE # системный таймер на начало


Задаём расстояние между обработчиками прерываний
Сокласно схеме в руководстве: «Register 2-6: Intctl: Interrupt Control Register; CP0 Register 12, Select 1», для значения _vector_spacing = 1, расстояние между обработчиками прерываний составляет 32 байта.
la      t1,_vector_spacing
        li      t2,0                    # Clear t2 and
        ins     t2,t1,5,5               # shift value to VS field
        mtc0    t2,_CP0_INTCTL


Сбрасываем флаги необработанных прерываний
Подробнее можн посмотреть в руководстве в разделе:
«2.12.9 CAUSE Register (CP0 Register 13, Select 0)»
li      t1,0x00800000
        mtc0    t1,_CP0_CAUSE


Настраиваем режим обработки прерываний
mfc0    t0,_CP0_CONFIG
        ext     t1,t0,22,1              # Extract UDI from Config register
        sll     t1,t1,17                # Move UDI to Status.CEE location
        mfc0    t0,_CP0_STATUS
        and     t0,t0,0x00580000        # Preserve SR, NMI, and BEV
        or      t0,t1,t0                # Include Status.CEE (from UDI)
        mtc0    t0,_CP0_STATUS


Вызываем обработчик бутстрапа (если таковой требуется).
la      t0,_on_bootstrap
        jalr    t0
        nop


Выходим из режима бутстрапа
mfc0    t0,_CP0_STATUS
        and     t0,t0,0xffbfffff        # Clear BEV
        mtc0    t0,_CP0_STATUS


Запускаем функцию main
and     a0,a0,0
        and     a1,a1,0
        la      t0,_main_entry
        jr      t0
        nop


        .end _startup


Вызов сишной функции main
Я так же использовал функцию main в ассемблерных примерах в первой-третьей части.
_main_entry:

        # call .init section to run constructors etc
        jal     _init
        nop
        and     a0,a0,0
        and     a1,a1,0
        jal main # Сишный код будет стартовать отсюда
        nop

        jal    exit # после выхода из main должена выполнится функция exit
        nop

1:
        b       1b
        nop
        .end _main_entry


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

void init()
Функция _init должна подготовить почву для выполнения main находится в crti.S
.section .init,"ax",@progbits
        .globl  _init
        .type   _init,@function
_init:
        addu    $sp,$sp,-32
        sw      $31,20($sp)
        .section .fini,"ax",@progbits
        .globl  _fini
        .type   _fini,@function
_fini:
        addu    $sp,$sp,-32
        sw      $31,20($sp)


Как видно, _init сохраняет $ra, чтобы мы смогли вернуться в _main_entry и выполнить функцию exit.

Выводы и полезные факты
  • Основное в стартовом коде это обработчик сигнала reset
  • PIC32MX имеет теневой набор регистров для обработки прерываний
  • Архитектура приспособлена к многопроцессорной работе
  • Загрузку переменных из памяти можно ускорить, если использовать регистр GP.
  • Можно экономить место во флеше, размещая нулевые данные в сегменте .bss

В следущей серии

Мы уйдём от компиляторного хардкора и переделаем моргание светодиода с использованием прерывания таймера.

Код, использованный в статье (не мой):
github.com/rkujawa/chipKIT-minimal-application
  • +4
  • 15 апреля 2014, 08:46
  • ihanick

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

RSS свернуть / развернуть
похоже пик так и не заслужил внимания…
0
  • avatar
  • xar
  • 16 апреля 2014, 10:23
поясню немного, если кому интересно, зачем NOP после переходов:
у мипса 5-этапный конвейер IF ID EX MEM WB
предикат условного перехода выполняется на стадии EX
поэтому, если не вставить NOP после инструкции перехода, будет загружена инструкция (instruction fetch), следующая за инструкцией перехода и начнутся косяки.

хотя, кому это сейчас надо :)
0
А оно само разве не сбрасывает конвеер?
0
По поводу конвеера на PIC32:
Подробнее о конвеере MIPS32MX

Если коротко, то почти для всех команд MIPS32 выполняет следующую за командой прыжка (PC+4) инструкцию, а потом уже прыгает. Это такой метод оптимизации. При вычислениях не редка ситуация, когда у нас есть прыжок на каждые 6-8 команд, при длинном конвеере, сбрасывать его накладно, можно его тормознуть на пару команд.

В MIPS решили, что было бы неплохо экономить время на каждой инструкции ветвления, если спаривать эту инструкцию с другими (не прыжками). Всё равно пока мы исполняем прыжок можно что-то декодировать.

Ещё особенность, что в ассемблере mips есть «псевдо» команды типа load address, которые являются несколькими командами. Такие псевдо команды нельзя помещать в следующую за инструкцией ветвления команду.
0
Перерыл весь Reference Manual не нашёл ни чего про начальную загрузку.
Есть PIC32MX795 с минимальной обвязкой.
Что происходит с МК при подаче питания?
Какой порядок запуска? Пошагово.

ПРИМЕР:
1. Включается внутренний тактовый генератор на частоте…
2. Проверяются регистры такие-то…
3. Переход на адресс такой-то…
4. итд.

Где это можно узнать? Прочитать?
Спасибо.
0
Обычно МК держится внутренними схемами под RESET'ом, пока не запустятся необходимые аппаратные узлы (тактовый генератор, например), при этом все регистры сбрасываются в on-reset value, а дальше проц начинает исполнение кода с фиксированного адреса или фиксированного IV (в большинстве архитектур это одно и то же).
Этот адрес обычно находится в флеше и в прошивке по этому адресу располагается стартап-код. Хотя в некоторых случаях исполнение может начинаться с другого адреса — например, с бутлоадера.
0
Это всё понятно, но это абстракция.
А я спрашивал конкретно для PIC32.
Где можно узнать эту информацию по каждому случаю:

или фиксированного IV
что такое IV?
0
Interrupt Vector. Обычно при запуске проц начинает выполнять код с IV RESET.
0
Читать в PIC32MX Reference manual, 7ая глава.

Как видно из таблицы 7.2, мы стартуем с BFC0_0000h, практически для всех резетов.

Потом на моей плате исполняется бутлоадер для avrdude,
github.com/chipKIT32/PIC32-avrdude-bootloader/tree/master/bootloaders
на других платах либо бутлоадера нет вообще, либо он может быть другим.

Где читать:
Апноут по бутлоадерам
+ референс мануал, чтобы смотреть конкретные регистры.
Про железячную часть написано тоже в reference manual

Ваш контролер используется в code.google.com/p/retrobsd/wiki/Board_Digilent_Max32
Значит код бутлоадера будет аналогичным (можно взять из репозитория выше)
0
Спасибо.
Reference manual какой-то запутанный.
Описали бы сначала общую логику работы.
А так каждый модуль отдельно описан хорошо, но общей картины не складывается.
Буду разбираться.
Спасибо.
0
Подскажите новичку, почему startup файлы пишут на ассемблере.
0
Потому что требуется доступ к возможностям, которые не достуны из ЯВУ.
0
код инициализации специфичный для конкретной платформы. Допустим мы написали этот код на С, тогда уникальность кода не будет позволять его повторно использовать на других платформах или даже на другой версии платформы. Производители даже могут предлагать генераторы стартовых файлов, если платформа сложная.

Код инициализации очень маленький и простой, его могут написать разработчики железа, без привлечения дополнительных программистов. Код не содержит вычислительной логики и состоит из записывания значений в регистры в памяти или вызов редко используемых команд процессора (настолько редко и специфично для платформы, что нет сишных обёрток). Код, который пишут энтузиасты бывает и сишный.
0
Спасибо большое.
0
1) «Эта секция находится в файле crt0.S» — ну да. или еще в каком-нибудь. исполнение начинается с 0xBFC00000 и по сути секция по этому адресу может называться как угодно.
2) "_gp вычисляется линковщиком как конец начало сегмента(.data) + 32000" — он вообще-то задан линковщиком. что видно в примере выше. и в целом нет никаких ограничений по тому, какой будет глобальный указатель. будет указывать в другое место — %gp_rel будет действовать от другого места.
3) «В семействе PIC32MX есть два набора регистров: нормальный набор набор для обработки прерываний» — теневой набор регистров можно использовать по своему усмотрению и не во всех pic32mx есть такое. в дешевых pic32mx такого нет. pic32mx1xx2xx5xx обладают одним набором. и дешевые pic32mx закономерно самые популярные.
4) «У нас есть только пространство пользователя, потому что мы пишем под контроллер без защиты памяти (MMU), используем только kernel space адреса.» — MMU кстати там fixed mapping. использовать или нет usermode исключительно дело программиста. более того, использовать ioctl из usermode для application очень даже трезвая идея.

в двух слова — домыслы с откровенно говоря странными выводами.
0
Здравствуйте egan-ru
1) «Эта секция находится в файле crt0.S» — ну да. или еще в каком-нибудь. исполнение начинается с 0xBFC00000 и по сути секция по этому адресу может называться как угодно.
в стартовом коде gcc это так. Если используется nostartfiles или другой компилятор и названия секций и их количество может быть любое.

2) "_gp вычисляется линковщиком как конец начало сегмента(.data) + 32000" — он вообще-то задан линковщиком. что видно в примере выше. и в целом нет никаких ограничений по тому, какой будет глобальный указатель. будет указывать в другое место — %gp_rel будет действовать от другого места.
Это опять же оптимизация связанная с gcc. Если есть очень много глобальных переменных, данная оптимизация будет не эфективна. Написано криво, для заданного значения _gp можно адресовать 32кб до и 32кб после значения в gp

теневой набор регистров можно использовать по своему усмотрению и не во всех pic32mx есть такое. в дешевых pic32mx такого нет. pic32mx1xx2xx5xx обладают одним набором. и дешевые pic32mx закономерно самые популярные.
логично, если теневого набора нет, то и проблем от gp, который не указывает на глобальные переменные тоже нет.
У меня, к сожалению, есть только Chipkit Uno32, который я брал в нагрузку к Zybo board, мне было интересно понять как работает mips архитектура и какой код генерируется gcc для неё.

Огромное спасибо Вам за ваши коментарии, «Virtual to Physical Fixed Memory Mapping» выглядит интересной и необычной для микроконтроллеров возможностью.
0
[i]в стартовом коде gcc это так. Если используется nostartfiles или другой компилятор и названия секций и их количество может быть любое.[/i] — это все определяется скриптом линкера. для pic32mx делать сколь угодно серьезные проекты не получится, если не разобраться с яул. если использовать xc32 — там вылезет еще вагон откровенно говоря кривых микрочиповских вещей.
в файле линкера
[code]
ENTRY(_reset)

_RESET_ADDR = 0xBFC00000;

.reset _RESET_ADDR:
{
KEEP(*(.reset))
KEEP(*(.reset.startup))
} > kseg1_boot_mem
[/code]
вообще говоря трезвая мысль разбить boot на несколько секций — одна — та часть, что меняться не будет, другая — та что будет меняться в случае если Вы наскоро организовали перепрошивку по одному из интерфейсов.
[i]Если есть очень много глобальных переменных, данная оптимизация будет не эфективна[/i] — почему? у многих микрочиповских мк меньше 64КiB SRAM. учитывая fmm mmu то имеет смысл указать на середину памяти. любое обращение gp-rel. (при переключении потоков на аpplication, которые в useg нужно просто поменять первые цифры gp, а при вызовах ядра — восстановить).
[i]как работает mips архитектура и какой код генерируется gcc для неё.[/i] — в среднем для разработки под pic32mx значительную часть проекта проще писать на ассемблере, чем на С.

pic32mx1xx2xx5xx очень экономически целесообразные. цены старших вызывают вопросы.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.