Як приручити ядро процесора*

imageУ цій статті розповідається про етапи завантаження ядер процесорів серії QoriQ та участь в цьому завантажувача u-boot, а також про виконання окремо взятої програми на окремому процесорному ядрі без участі ОС. Стаття може зацікавити системних програмістів, що прагнуть осягнути все розмаїття процесорних архітектур. Також слід розуміти, що деякі визначення та прийоми актуальні і для інших процесорів і систем.

* на прикладі процесорів freescale qoriq c ядрами e500mc і ppc booke isa.


До справи !

Життя починається зі старту ядра* номер 0, яке виконує з фіксованого адреси код завантажувача. В нашому випадку це є u-boot, розташований в flash пам'яті і доступний за фізичним адресами 0x3ffff000. Відразу після старту ця адреса відображається на віртуальний 0xfffff000, про що є запис в таблиці відображення (див. документацію на e500).
*тут і далі під ядром розуміється ядро процесора (core), якщо не зазначено іншого.
Першою командою, що виконується процесором є команда, яка розташована за адресою 0xfffffffc. Як ви, напевно, вже здогадалися цією командою обов'язково повинна бути команда переходу в точку старту u-boot на цій сторінці. Якось так:

/* */
.section .resetvec,"ax"
b _start_e500



для тих, хто в Интлеb ≈ jmp
для тих, хто в Javaце команда безумовного переходу на мітку _start_e500

Далі в задачі u-boot входить включення і налаштування кешу, механізмів розмежування доступу, таблиці відображення адрес і, звичайно, треба відобразити іншу частину себе в адресний простір ядра процесора. Взагалі u-boot — молодець: бере на себе всю брудну роботу, на відміну від деяких (не будемо тикати пальцем, це не його вина).

А як же інші ядра? До інших ядер переходимо, коли закінчили з основним. Якщо їх запустити, то вони повторять послідовність дій нульового ядра. Для запобігання цього uboot змінять адресу трансляції завантажувальної області на місце розташування додаткового завантажувача інших ядер(див. ccsr boot space translation registerы).

До речі, в налаштуваннях u-boot існують можливість не ініціалізувати роботу інших ядер процесора, але в цьому випадку виконання брудної роботи по їх ініціалізації стає нашої подальшої турботою. Яку, втім, можна вирішити копіюванням коду з u-boot, а сам запуск ядра здійснюється записом відповідного біта в регістр ccsr.

У завдання додаткового завантажувача також входить налаштування кеш і додавання запису до таблиці відображення адрес, після чого ядра крутяться в такій веселій карусельки:

/* spin waiting for addr */
2:
lwz r4,ENTRY_ADDR_LOWER(r10) /* завантажити дані за адресою*/
andi. r11,r4,1 /* якщо молодший біт = 1,...*/
bne 2b /* ...перехід вище, до мітці 2*/


Крутяться вони до тих пір, поки в певний u-boot адресу не запишуть адресу точки старту з 0 молодшим бітом. Місце, куди треба записати цю команду u-boot називається spin-таблиця і розташована вона за фіксованим адресою (0xfffff000 + ENTRY_ADDR_LOWER). Крім адреси старту, в цю таблицю можна записати значення регістрів і r3 r6, які будуть завантажені перед виконанням команди переходу в точку старту.

На точку старту накладається обмеження на розмір вже відображеної u-boot 64Мб сторінки, це пов'язано з внутрішніми тарганами uboot.

Для тих, хто навчався на сучасних підручниках інформатикиPowerPC і деяких інших архітектурах сторінка є абстрактним поняттям. Зокрема, на процесорах e500* вона може розтягнутися в діапазоні від 4К до 4Гб (подробиці і обмеження див. в документації).

Створення додатків для ядра.

Давайте розробимо програму для нашого ядра, яка традиційно буде друкувати «hello world». Будемо вважати, що крос компілятор і легковажну бібліотеки libc ми вже зібрали, а також ми вміємо завантажувати ОС без підтримки багатоядерності на одному з ядер (в якості ОС можна, наприклад, використовувати скомпільований відповідним чином linux або lynxos-178). Тому приступимо до найважчого — програмування:

#include < stdio.h>
int main (int argc, char *argv[])
{
printf («hello world \n»);
return 0;
}


Готове. А куди і як буде виводити printf ?! Для цього доведеться написати деякі заглушки для libc, які можна підглянути в исходниках u-boot. Я використовую спрощений варіант:

int write (int fildes, char *buf, int nbyte) 
{
int wbyte = 0;
while (nbyte > 0) {
__putc(*buf);
buf++;
nbyte--;
wbyte++;
}
return wbyte;
}

int fstat(int fildes, struct stat *buf); {
buf->st_mode = S_IFCHR;
return 0;
}


І функція __putc повинна забезпечити виведення одного символу в послідовний порт.

extern volatile unsigned char *uart_data;
extern volatile unsigned char *uart_status;

static void __putc(unsigned char c)
{
unsigned char v;

do {
v = *uart_status;
} while (!(v & (1 << 5)));
*uart_data = c;
}


Повноцінний драйвер для цього писати необов'язково, достатньо скористатися налаштуваннями за замовчуванням і записувати символ за описаним у документації адресою. Фізичній адресою. Який потрібно ще відобразити. Функцію відображення я наводити не буду в силу її специфічності, але готовий поділитися їй за запитом.

І обрізаємо реальну точку старту виконуваного файлу — функцію _start. Для ініціалізації можна залишити тільки обнулення bss сегмента:

int _start(int argv, char **argc)
{
unsigned char * cp = (unsigned char *) &__sbss;

while (cp < (unsigned char *)&__ebss) {
*cp++ = 0;
}

return main(argv, argc);
}


Отже, тепер ми можемо обходиться без ОС і функція printf знає куди їй виводити інформацію. Компілюємо:
$ powerpc-eabi-gcc-o hello hello.c start.c 

Буде працювати? Ні! Процесор не розуміє формат виконуваного файлу elf. Треба відрізати зайве. Обрізаємо:
$ powerpc-eabi-objcopy-O binary hello hello.bin 

Буде працювати? Ні! Раніше точка старту задавалася в elf, а тепер вона чорт знає де. Більш детальне місце розташування можна подивитися утилітка powerpc-eabi-objdump. Звичайно, можна в якості точки старту вказати u-boot і туди, але краще написати вказівки линковщику про розміщення точки старту на початку файлу:
OUTPUT_ARCH(powerpc:common)
ENTRY(_start)
STARTUP(start.o)
...

Подальше вміст файлу буде залежати від версії засобів складання і ви це можете підглянути в сценарії додаються до компілятору.

Збираємо ще раз і думаємо як зручно це робити з makefile:
$ powerpc-eabi-gcc-T hello.ld-o hello hello.c start.c 

Тепер, якщо ми зібрали правильно, у нас є готова для запуску на одному ядрі, без ОС програма. Але краще додатково перевірити з допомогою objdump. До речі, ви помітили адреси сегментів збоку? Якщо не вказано іншого, то наша програма має позиційно залежний код і запускати її звідки потрапило не можна. Але зараз це нас не повинно хвилювати, а виправити зміщення можна за допомогою сценарію зв'язування. Правильно зібрана програма повинна запускатися навіть через u-boot.

Запуск ядра

З точки зору ОС, додаткове ядро нічим не буде відрізнятися від інших периферійних пристроїв використовують DMA. І для його роботи нам потрібно виділити пам'ять. Загальна схема відображення пам'яті буде виглядати приблизно так:

Якщо ми не хочемо відстрелити ОС ногу, то буде розумно, якщо розмір виділеної ОС пам'яті буде відповідати розміру відображуваної пам'яті для іншого ядра. Крім того, фізична адреса початку цієї пам'яті повинен бути вирівняно за розміром сторінки.

Отже, ми виділили пам'ять, записали туди нашу програму і записали фізична адреса точки старту програми в spin-таблицю, де крутиться вже налаштоване ядро.
насправді,після запису адреси u-boot зробить стрибок з віртуального адресою, але оскільки на даному етапі вони рівні, то можна вважати це дивом, але розуміти як воно працює.
А про обмеження на розмір точки старту забули?
Не можна бути впевненим у якому діапазоні адрес у системи залишиться вільна пам'ять і може так статися, що виділена пам'ять доведеться на неотображаемий u-boot діапазон адрес. Це призведе до виконання виключення помилки сторінки, що у нас не налаштовано, що в кінцевому рахунку призведе до останову ядра процесора.
Рішень цього може бути декілька, і мені найбільше сподобалося зробити в ОС свій инициализатор ядра, розташований в початкових адрес, що:
  • подолає обмеження точки старту;
  • обмежить наш додаток до використовуваної пам'яті так, щоб було видно тільки ділянку пам'яті виділений ОС;
  • налаштує доступні для ядра пристрої;
  • здійснить стрибок на початок нашої програми;
Після передачі управління програмою в консоль має вывестись «hello world
»
Надалі, використовуючи регістри управління, ядра процесора можна зупиняти, перезапускати, змінювати частоту, видавати їм переривання (це цікава, але дуже специфічна тема) і багато іншого.

Висновок

Звичайно, багато сучасні ОС надають можливість ізолювати ядра процесора для окремих програм та необхідність писати свій код для підтримки багатоядерності сумнівна. Однак, існують завдання, пов'язані з твердим реальним часом, для яких затримки в 2 мс, які може вносити багатоядерність в стандартній конфігурації, є досить критичними. І вони вимагають нестандартних підходів до конфігурації системи. Але це вже матеріал для іншої статті.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.