LinuxCard — це унікальний DIY-проєкт, де звичайна друкована плата у формі візитки здатна запускати повноцінний Linux. Автор використав мікроконтролер ATSAMD21, зовнішню пам’ять і слот microSD, щоб відтворити роботу старого DECstation на мінімалістичному обладнанні. Завдяки нестандартному підходу вдалося досягти завантаження ядра Linux усього за кілька хвилин і отримати робочий shell прямо з візитки. Проєкт показує, як із доступних компонентів можна побудувати вражаючий емульований комп’ютер, що виконує базові програми, компілює код і навіть завантажує інші ОС, як-от Ultrix. LinuxCard — це приклад інженерної креативності та доказ того, що Linux може працювати майже на будь-чому.
Давним-давно (у 2012 році) я запускав Linux на 8-бітному AVR. На той час це був досить крутий рекорд. Не думаю, що хтось його побив — нікому не вдавалося запустити Linux на пристрої нижчого класу, ніж цей 8-бітний AVR. Основна проблема полягала в тому, що він був занадто повільним, щоб бути практичним. Ефективна швидкість становила 10 кГц, час завантаження — 6 годин. Круто, але сумніваюся, що хтось із тих людей, які створили один із цих пристроїв на основі мого дизайну, коли-небудь чекав, поки пристрій завантажиться більше одного разу. Настав час його вдосконалити!
Отже, що я міг би покращити? Кілька речей. По-перше, я хотів, щоб новий дизайн був достатньо швидким, щоб завантажуватися за кілька хвилин і відповідати на команди за лічені секунди. Це зробило б використання пристрою практичним, а не випробуванням терпіння. По-друге, я хотів, щоб його було легко зібрати будь-кому. Це означало відсутність компонентів з щільним розташуванням контактів, компонентів із занадто великою кількістю контактів і компонентів із прихованими під ними контактами. Частиною цього бажання також було те, щоб хтось міг насправді зібрати його, а це означало, що мені довелося вибирати компоненти, які насправді можна купити посеред поточного дефіциту, ну, всього. Крім того, я хотів, щоб пристрій було легко взаємодіяти. Початковий проект вимагав адаптера USB-послідовний порт. Це було неможливо. І, нарешті, я хотів, щоб усе це було дешевим і достатньо компактним, щоб служити моєю візитною карткою.
Деякі речі було досить легко визначитися. Наприклад, для зберігання даних ідеально підходить microSD — легко інтерфейсується, широко доступна, дешева. Я вибрав простий слот microSD, який легко паяти та легко купити: Amphenol 1140084168.
Деякі варіанти були трохи складнішими, але не надто. Наприклад, я точно не збирався знову використовувати DRAM. Вона вимагає забагато контактів, що вимагає більше паяння, ніж я вважав би прийнятним, враховуючи, що я хотів, щоб цей пристрій було легко зібрати. SRAM у мегабайтних розмірах насправді не існує. Але є класна річ під назвою PSRAM. Це, по суті, DRAM, але в спрощеному режимі. Вона сама піклується про все оновлення та зовні діє так само, як SRAM. Гаразд, круто, але все одно це зазвичай багато контактів. Правильно? Ось такі “AP Memory” та “ISSI”. Вони виготовляють мікросхеми QSPI PSRAM у гарних корпусах SOIC-8. AP Memory має моделі з 2 МБ та 8 МБ оперативної пам’яті на мікросхему, ISSI має їх розміром 1 МБ, 2 МБ та 4 МБ. Я вирішив використовувати ці. Вони доступні, і мій код підтримує їх усі!
Було кілька різних варіантів, наприклад, який регулятор використовувати. Я обрав MIC5317-3.3YM5TR, оскільки вже працював з ним раніше, і він був у моїй коробці “випадкових мікросхем”. Його також легко купити.
Роз’єм USB також був цікавим вибором. Я зупинився на: жодному. За належної товщини друкованої плати можна підігнути край плати так, щоб він помістився в кінець кабелю USB-C. Я вже бачив, як це роблять для micro-USB, і подумав, що це можна зробити і для USB-C. Зрештою, мені навіть не довелося цього робити, оскільки хтось інший уже заощадив мені 30 хвилин, які б це зайняло. Мені просто потрібно було пам’ятати, що товщина плати має бути 0,8 мм, щоб це працювало.
Останній вибір був найскладнішим – який мікроконтролер використовувати. Критерії були такими: вбудований USB, не більше 32 контактів з відстанню між контактами щонайменше 0,65 мм, відсутність безконтактних корпусів, реальна доступність для покупки, підтримка QSPI, якомога швидше. Я не отримав своїх останніх двох бажань. Після довгих пошуків та фільтрації за запитом «в наявності» я був змушений зупинитися на мікросхемі серії ATSAMD21, а саме на ATSAMDA1E16. Він нешвидкий (за характеристиками 48 МГц, я тактую його на 90 МГц), має багато помилок (особливо в механізмі DMA), але його можна купити, його легко паяти, і доведеться… ОНОВЛЕННЯ: тепер підтримується ще один мікросхема, див. далі в цій статті.
Я міг би просто взяти свій старий емулятор ARM (uARM) і використовувати його. Але що тут цікавого? Я вирішив вибрати нову ціль. Ідеальна ціль для емуляції буде: (1) бути RISC-чіпом, щоб мені доводилося витрачати менше циклів на декодування інструкцій, (2) не мати кодів умов (як MIPS) або встановлювати їх лише на вимогу (як ARM), щоб я не витрачав час на їх обчислення в кожному віртуальному циклі, (3) бути 32-розрядною, оскільки 16-розрядні машини — це просто дрібниця, а 64-розрядну емулювати важко, (4) бути відомою та (5) мати робочий набір інструментів GNU та доступних просторів користувача Linux. Цей набір вимог насправді залишає лише кілька кандидатів: PowerPC, ARM, MIPS. Я працював з ARM і не мав бажання возитися з процесором з перемиканням байтів, тому обрав MIPS! Звідси походить внутрішня назва проекту: uMIPS.
MIPS — це старий чіп, один з оригінальних RISC-конструкцій. Якщо ви фанат (фанат/фанатка) RISC-V, MIPS виглядатиме знайомо — саме з нього було скопійовано 99,9994% початкової специфікації RISC-V. Оригінальний MIPS мав 32-бітну конструкцію, оптимізовану для зручності проектування. Він має (і не приховує) слот затримки, має багато регістрів, включаючи апаратно-програмований нульовий регістр, і не використовує коди умов. Оригінальна конструкція була R2000, ще в 1986 році, а незабаром у 1988 році з’явився покращений R3000. Це були останні мікросхеми, що реалізували набір інструкцій MIPS-I. MIPS-II проіснував недовго і включав лише R6000, який ледве побачив світло. Справжніми наступниками стали мікросхеми MIPS-III серії R4000, випущені в 1991 році. Вони вже були 64-бітними в 1991 році! Зрозуміло, що найлегшою ціллю були б чіпи R2000/R3000 з їхнім простим набором інструкцій MIPS-I.
MIPS-I — це досить простий набір інструкцій. Настільки, що повний емулятор, що складається лише з інструкцій, можна написати менш ніж у 1000 рядках коду на C без будь-яких брудних трюків. Блок обчислень з плаваючою комою необов’язковий, тому його можна пропустити (поки що). MMU — це дивний варіант. Це просто TLB, який програмне забезпечення має заповнювати вручну. Це може здатися досить незвичайним вибором, але насправді це розумний підхід, якщо ви живете в 1986 році та намагаєтеся мінімізувати кількість транзисторів у своєму чіпі. Навіщо використовувати апаратний ходунковий механізм сторінкового перегляду, коли можна змусити це робити програмне забезпечення? Ви можете запитати, як воно справляється з ситуацією, коли код, який виконує перегляд, сам не відображається? Ну, частина фізичної пам’яті завжди жорстко відображається за певною адресою, і всі обробники винятків знаходяться там. Навіть якби це було не так, оскільки програмне забезпечення керує TLB, неважко було б зарезервувати запис для цієї мети. Апаратне забезпечення навіть підтримує деякі “дротові” записи, які призначені для постійного використання. Докладніше про все це пізніше.
MIPS R2000/R3000 – це процесор. Процесор не створює повноцінної системи. Яку систему емулювати? Я пошукав класну систему та зупинився на DECstation2100 (або його старшому браті – DECstation3100). Навіщо взагалі морочитися? Здавалося, що це проста система, яку підтримує Linux. Спочатку я не планував емулювати все повністю. Чому? Я не планував емулювати мережевий адаптер LANCE чи адаптер SII SCSI. Остання частина може вас здивувати, оскільки нам знадобиться диск, який ми використовуватимемо як кореневу файлову систему. Пізніше я додав емуляцію обох цих частин, щоб порадувати Ultrix.
MIPS — це досить старий набір інструкцій, що проявляється в кількох місцях. Головна з них полягає в тому, що він намагається запобігти переповненню зі знаком. Звичайні інструкції, що використовуються для додавання та віднімання, спричиняють фактичний виняток, якщо вони викликають переповнення. Це не відповідає тому, як використовуються процесори сьогодні, тому нікого це не хвилює, але мені все одно довелося емулювати це. Існують “беззнакові” версії інструкцій для додавання та віднімання, які цього не роблять, і саме це видають усі сучасні компілятори на MIPS.
Спочатку я написав емулятор для процесора на C, щоб забезпечити легке тестування на моєму робочому столі, поки виготовлялися друковані плати. Він не був швидким і не задумувався як такий, але дозволяв тестувати. Ви можете побачити цей емулятор у cpu.c. По дорозі сюди я реалізував деякі функції процесора R4000 за бажанням. Виявилося, що для завантаження Linux, скомпільованого сучасними компіляторами, це необхідно, оскільки компілятори припускають існування цих інструкцій. Технічно це помилка. Реалістично кажучи, я, мабуть, єдина людина, яка це помітила. Отже, які функції мені потрібно було додати? Ймовірно, гілки ( інструкції BxxL ), умовні перехоплення ( інструкції Tcc/TccI ) та атомні перехоплення ( інструкції LL/CC ).
Звісно, C — це не та мова, яку використовують, коли хочеться швидко працювати. Я також написав емулятор на асемблері, орієнтований на ARMv6-M (для обраного мною мікроконтролера Cortex-M0). Пізніше я додав кілька покращень для ARMv7-M (на випадок, якщо я коли-небудь оновлю проєкт до потужнішого процесора). Це було протестовано на Cortex-M7 і також працювало добре. Ядро емулятора асемблера міститься в cpuAsm.S , а специфічні для ARMv6-M частини — у cpuM0.inc.
Я вже згадував про слоти затримки раніше. Що таке слот затримки? Ну, раніше вважалося крутим відкрити конвеєр процесора для всього світу. Жартую, це був просто спосіб заощадити ще трохи транзисторів. По суті, інструкція після стрибка буде виконана, навіть якщо стрибок відбудеться. Це називається слотом затримки. Наївний спосіб уникнути цього – розмістити NOP після кожної інструкції переходу. Але з хорошим компілятором слот затримки можна добре використовувати майже у всіх випадках. Очевидно, що не можна розмістити інструкцію переходу в слоті затримки, оскільки процесор вже кудись стрибає. Робити це незаконно та невизначено. Однак виникає проблема, якщо інструкція в слоті затримки викликає будь-який виняток. Процесор запише, що інструкція була в слоті затримки, і вкаже обробнику винятків на стрибок , у слоті затримки якого ми знаходимося. Немає способу повернутися до цього стану “в слоті затримки”, тому очікується, що обробник винятків вживе заходів, щоб якимось чином виконати інструкцію слота затримки, а потім завершити стрибок.
DECstation постачалася з FPU, щоб операції з плаваючою комою були швидкими. Тоді це був окремий чіп, який був необов’язковим у системі MIPS R2000/R3000. Linux, фактично, більш-менш коректно емулюватиме FPU, якщо його немає, але це повільно. Спочатку я використовував цей режим і навіть виправив кілька помилок в емуляції Linux, але зрештою я реалізував емулятор FPU. Це було необхідно, оскільки, здається, багато бінарних файлів MIPS, які я зміг знайти, припускають, що FPU доступний, і використовують його вільно. Я ніколи не перереалізував емулятор FPU на асемблері, натомість викликав емулятор C FPU, коли це було потрібно. Я вважаю, що вичавлювати кілька циклів з кожної інструкції не має сенсу, коли фактична операція FPU займає сотні.
Код для цього знаходиться в fpu.c. Я додаю патчі Linux, щоб видалити підтримку емуляції FPU з ядра. Це економить трохи оперативної пам’яті. Пізніше я також додав підтримку “мінімального” FPU – він підтримує регістри, але не операції. Це дозволено специфікацією, оскільки FPU може відмовитися виконувати будь-яку операцію, яку він «не впевнений, що може виконати ідеально правильно», тому будь-яка сумісна ОС повинна реалізувати повний резервний варіант FPU в будь-якому випадку. Чому? Це заощаджує 16 Кб коду у двійковому файлі, відкриваючи можливість запуску uMIPS на менших пристроях.
(це дуже спрощений виклад, сміливо пропускайте, якщо ви це знаєте, і не скаржтеся мені, що це не зовсім точно!)
Більшість процесорів звертаються до пам’яті за допомогою віртуальних адрес ( VA ). Апаратне забезпечення працює з точки зору фізичних адрес ( PA ). Можливість зіставлення однієї з іншою є основою безпеки пам’яті в сучасних операційних системах. Метою MMU (блоку керування пам’яттю) є перетворення віртуальних адрес у фізичні адреси, щоб забезпечити це зіставлення. Зазвичай це робиться за допомогою деревоподібної структури в оперативній пам’яті, яка називається таблицею сторінок . Більшість процесорів мають компонент, завдання якого полягає в тому, щоб пройтися цією структурою, щоб визначити, на яку фізичну адресу відповідає задана віртуальна адреса. Цей компонент є обхідником таблиці сторінок . У більшості випадків таблиця сторінок має 3 або 4 рівні, що означає, що для перетворення VA на PA потрібно зчитувати 3 або 4 слова з основної пам’яті. Зрозуміло, що ви не хочете робити 3 непотрібних звернення до пам’яті для кожного корисного. Тому зазвичай до MMU включено ще один компонент – TLB (буфер пошуку по стороні перекладу). По суті, ви можете уявити TLB як кеш деякого вмісту поточної таблиці сторінок . Ідея полягає в тому, що перед тим, як виконати ці 3-4 зчитування з пам’яті в таблиці сторінок , ви можете перевірити, чи є в TLB відповідний запис. Якщо так, ви можете пропустити обхід таблиці сторінок .
Зрозуміло, що, як і будь-який кеш, TLB повинен синхронізуватися з тим, що він кешує (поточними таблицями сторінок ). Тому, якщо ОС змінює таблиці сторінок , їй потрібно очистити TLB , оскільки в ньому можуть бути застарілі записи. Зазвичай TLB надають дуже мало інтерфейсу процесору, тому немає способу прочитати всі записи та видалити лише нові недійсні. Крім того, це було б повільно, тому зазвичай цього не роблять. Однак, анулювання всього TLB також має свої витрати – його потрібно повторно заповнювати, що призводить до 3-4 звернень до пам’яті на запис. Це може погіршити продуктивність. Зазвичай використовується рішення, яке називається ASID .
Які чотири основні випадки, коли таблиці сторінок можуть бути змінені? (1) Додавання нового відображення поверх віртуальної адреси, яка раніше ні на що не була відображена, (2) зміна дозволів на існуюче відображення, (3) видалення відображення та (4) повна зміна карти пам’яті (наприклад, для перемикання на зовсім інший процес). У випадку 1 очищення TLB не потрібне, оскільки не може існувати жодного застарілого запису TLB . Випадки 2 та 3 дійсно вимагають очищення TLB , але вони не такі поширені. Однак випадок 4 є досить поширеним. Це робиться при кожному перемиканні контексту. Можна зазначити, що оскільки ми змінюємо всю карту пам’яті, весь TLB буде недійсним, і тому його очищення не є проблемою. Це неправильно. Окрім відображення елементів простору користувача, MMU також відображає різні структури ядра, і немає сенсу карати їх.
Якби ми могли якимось чином позначати, які записи в TLB йдуть до якого процесу, і тимчасово вимикати їх, коли виконується інший процес, ми могли б уникнути великої кількості змивання контексту та витрат на продуктивність, пов’язаних з цим. Також було б круто, якби ми могли позначати цілі записи, що належать до ядра та є дійсними в кожному процесі. Що ж, саме ця технологія існує в багатьох MMU . Ідея полягає в тому, що кожен запис таблиці сторінок матиме біт, який позначає його як “глобальний” (дійсний у всіх картах пам’яті) або ні. Також у процесорі має бути регістр, який встановлює поточний ASID (ідентифікатор адресного простору). Коли запис TLB заповнюється з таблиць сторінок, поточний ASID записується в нього. Коли виконується пошук у TLB, збігатимуться лише записи, що відповідають поточному ASID або ті, що позначені як “глобальний”. Круто!
Ідея на той час полягала в тому, щоб заощадити транзистори. Що з перерахованого вище можна було б вирізати? Ну, виключення TLB гарантує жахливу продуктивність у всіх випадках. Але чи справді нам потрібен цей обхідник таблиці сторінок ? Що, якби ми змусили програмне забезпечення робити це? Ми могли б додати трохи допомоги, наприклад, можливість ефективно керувати TLB , але пропустити апаратне забезпечення обхідника таблиці сторінок . Саме це й зробив MIPS. Ось віртуальний адресний простір MIPS:
Адреси Зіставлення назв 0x00000000..0x7fffffff kuseg зіставлено через MMU 0x80000000..0x9fffffff kseg0 зіставлено з фізичним 0x00000000..0x1ffffffff, кешовано, якщо кеш є, доступно лише в привілейованому режимі 0xa0000000..0xbffffffff kseg1 зіставлено з фізичним 0x00000000..0x1ffffffff, не кешовано, доступно лише в привілейованому режимі 0xc0000000..0xffffffff kseg2 зіставлено з MMU, доступно лише в привілейованому режимі
Отже, як бачите, деякі віртуальні пристрої взагалі не відображаються через MMU . Це означає, що код, що знаходиться там, може працювати незалежно від стану MMU . Linux та Ultrix, як і очікувалося, розміщують ядро в kseg0 . Однак ядро також повинно мати можливість динамічно відображати об’єкти. kseg2 — це один гігабайт адресного простору, який можна відобразити через MMU , який може використовувати ядро. Доступ до пристроїв, відображених у пам’ять, зазвичай здійснюється через kseg1 . 2 гігабайти внизу діапазону адрес ( kuseg ) призначені для завдань простору користувача.
Який запис у TLB слід замінити, коли потрібно вставити новий запис? Очевидною відповіддю може бути «той, що найменше використовувався», але це вимагатиме відстеження використання, що також коштує транзисторів. Спрощення — «той, що найменше додано». Це просто, але приховує фатальний недолік. Уявіть, що ваш TLB має N записів, і ваше робоче навантаження послідовно використовує N + 1 адрес, таким чином, що кожній потрібен запис TLB . Тепер ви завжди замінюватимете запис, який вам знадобиться, гарантуючи, що ви НІКОЛИ не дійдете до TLB і не зробите багато безглуздих прогулянок по таблиці сторінок . Як цього уникнути? Найпростіший метод — замінити випадковий запис. Звичайно, це може бути саме той запис, який вам знадобиться, але для TLB з N записами ймовірність становить 1/N.
Генерація випадкових чисел у програмному забезпеченні відбувається повільно, тому MIPS R2000/R3000 надають певну допомогу. Процесор має регістр під назвою, буквально RANDOM , який повинен постійно збільшуватися на нуль кожного циклу. Оскільки “коли” або “коли вам наступного разу знадобиться новий запис TLB ” непередбачувано, це так само добре, як випадковий регістр, і вимагає дуже мало транзисторів. Ідея полягає в тому, що щоразу, коли вам потрібно замінити запис TLB , ви використовуєте спеціальну інструкцію TLBWR для запису у випадковий запис. Я також не випадково розповів вам про ASID . MIPS R3000 MMU реалізує 6-бітний ASID .
Емуляція MMU R3000 дещо складна. Оскільки будь-який запис може бути в будь-якому місці, правильний спосіб пошуку — перевірити кожен з них. Виконання 64-тактного циклу для кожного звернення до пам’яті, звичайно, не є швидкісним засобом для початку. Я використовую хеш-таблицю, індексовану віртуальною адресою, щоб зберігати всі записи TLB у відрах для швидшої перевірки. Використання 128 відер практично гарантує, що більшість відер матимуть нуль або один запис, що дозволяє набагато швидший пошук. Спочатку це була проста таблиця вказівників, але вона використовувала забагато оперативної пам’яті, тому тепер це таблиця індексів.
DECstation мала кілька способів зв’язку із зовнішнім світом. Вона мала вбудовану мережеву карту , яку я не емулюю . Вона була додатковою , і я ще не знайшов для неї застосування . Можливо, я знайшов це пізніше — вона не виглядає складною. Вона також мала SCSI-контролер, до якого можна було підключити жорсткі диски та інші периферійні пристрої SCSI. Емуляція цього була б цікавим завданням, і я, мабуть, повернуся до цього пізніше, але я не робив цього зараз — у цьому не було потреби — я написав паравіртуалізований драйвер диска для Linux, використовуючи гіпервиклики, про це пізніше. Також була додаткова плата буфера кадрів, яку можна було встановити, що додавала підтримку монохромного або кольорового дисплея. Емуляція їх також не була б надто складною, але на моїй візитній картці відсутній дисплей, тому я також цього не робив — до того ж я навіть не впевнений, що Linux може це використовувати.
Останнім методом зв’язку, який мала DECstation, був DC7085 – контролер послідовного порту, який по суті є клоном DZ-11 епохи PDP11 . Він підтримує чотири послідовні порти з приголомшливою швидкістю 9600 біт/с (або будь-яким цілочисельним діленням). Кожному послідовному порту було призначено певне призначення, і вони були підключені до різних роз’ємів, що вказували на це призначення. №0 був для клавіатури, №1 для миші, №2 для модему та №3 для принтера. Для машини вони всі однакові, це було просто призначення, яке їм призначив DEC. Штатний PROM використовував би №3 як послідовну консоль, якщо не виявляв клавіатуру в №0, тому прийнято використовувати №3 як послідовну консоль для Linux на DECstation. Мій сурогат PROM не шукає або не підтримує зовнішню клавіатуру і просто за замовчуванням встановлює послідовну консоль на №3. З огляду на це, оскільки можливість кількох сеансів входу – це круто, я також експортую #0 #2 як другий віртуальний послідовний порт, щоб ви могли входити в систему з двох послідовних консолей одночасно та робити дві речі одночасно. Ну як же це круто?
Отже, як експортувати ці послідовні порти? Коли ви підключаєте карту до комп’ютера, вона відображатиметься як складений USB-пристрій, що складається з двох віртуальних послідовних портів CDC-ACM. Один з них – порт №3, інший – порт №0 №2 на віртуальному DZ-11 . Як ви дізнаєтесь, який є який? Порт №3 має вивід консолі завантаження та початковий запит sh . Якщо ви цього не бачите, спробуйте інший, комп’ютери не завжди нумерують їх у порядку експорту.
У реальному світі PROM мав зондувати реальне обладнання, щоб визначити, що де присутнє. Оскільки мій PROM працює в емуляторі, немає потреби в такому безладі. Ми можемо просто запитувати речі з емулятора узгодженим способом. Цей спосіб називається гіпервикликом – спеціальною недійсною інструкцією, яка, якщо вона зустрічається в режимі супервізора, емулятор оброблятиме як запит на певний вид послуги. Інструкція, яку я обрав, – 0x4f646776 , яка знаходиться в просторі декодування COP3 (співпроцесор 3), який не був виділений для жодної реальної мети в цих чіпах. Конвенція викликів близька до звичайної конвенції викликів C на MIPS: параметри передаються в $a0 , $a1 , $a2 та $a3 , повертані значення знаходяться в $v0 та $v1 . Регістр $at отримує “номер гіпервиклику” – конкретну послугу, яку ми запитуємо.
Реалізовано кілька гіпервикликів. #0 використовується для отримання карти пам’яті. Параметр — це індекс слова карти пам’яті для зчитування. Слово 0 — це «скільки бітів містить растрова карта карти пам’яті», слово 1 — це «скільки байтів оперативної пам’яті представляє кожен біт», слова 2 і далі — це біти карти, до загальної кількості, зазначеної в слові 0. Це можна використовувати для побудови карти пам’яті, яку PROM може надати працюючій ОС, і це дозволяє мені мати переривчасту оперативну пам’ять. Linux підтримує це, і я спробував це, але зрештою це мені не знадобилося. Це тут, на випадок, якщо я передумаю і це знову знадобиться.
Гіпервиклик №1 виводить один байт на консоль налагодження (що те саме, що й порт 3 DZ-11 ). Він використовується PROM та mbrboot для виведення рядків налагодження без необхідності встановлення повного драйвера DZ-11. Гіпервиклик №5 завершить емуляцію. Його можна використовувати у версії емулятора для ПК для спокійного завершення роботи.
Гіпервиклики №2, №3 та №4 використовуються для доступу до SD-карти. №2 поверне розмір карти в секторах, №3 запитає зчитування заданого сектора за заданою фізичною адресою оперативної пам’яті та відповідає ненульовим значенням, якщо це спрацювало. №4 зробить те саме для запису на карту.
Перша ревізія цієї плати спочатку пройшла добре, після того, як я розібрався з безладом, який є системою тактування ATSAMD21. Я ціную гнучкість так само, як і будь-хто інший, але ця штука НАДТО гнучка. Знадобилося набагато більше часу, ніж я хотів би визнати, щоб запустити цю штуку на нормальній швидкості та активувати деякі периферійні пристрої. Документація також була надто мізерною, щоб бути корисною. Atmel, що з тобою сталося? У тебе колись була найкраща документація!
Перша ревізія плати мала два чіпи пам’яті, кожен на власній шині SPI, SD-карту на шині SPI та USB з відповідними резисторами. USB був ідеальним. На відміну від усіх та їхньої бабусі (STMicro, я дивлюся на тебе), Atmel не ліцензувала надокучливий Synopsis USB IP. Вони зробили свій власний. Він простий у використанні, елегантний і добре працює. Серйозно, він просто працював. За два дні я змусив апаратне забезпечення працювати та написав стек USB-пристроїв. Я знімаю капелюха перед командою, яка працювала над USB-контролером. Тим не менш, у мене є занепокоєння. Моя головна проблема: USB-дескриптори не малі. Вони постійні. Я б волів зберігати їх у флеш-пам’яті. Я б волів, але не можу. USB-блок використовує вбудований блок DMA для зчитування даних для надсилання. Цей блок DMA МОЖЕ отримувати доступ до флеш-пам’яті, але якщо у вас увімкнено будь-які стани очікування флеш-пам’яті, він надсилає сміття. Я підозрюю, що Atmel тестував його лише для читання з оперативної пам’яті, забув, що деякі пам’яті мають стани очікування, і не врахував це. Зберігання всіх моїх дескрипторів в оперативній пам’яті — це колосальна трата оперативної пам’яті, якої там лише 8 КБ. Пам’ятаєте той капелюх-зачіску? Я його скасував, Atmel. Мені довелося обійти цю проблему, надсилаючи дескриптори по одному (замість того, щоб дозволяти апаратному доступу до прямого доступу до мережі автоматично виконувати все це), просто щоб заощадити цінну оперативну пам’ять.
Використання SPI-блоків безпосередньо працювало досить добре, доки я не спробував їх пришвидшити. Після приблизно 18 МГц отримані дані були спотворені (один чи два пропущені біти, всі наступні біти зміщувалися). Жоден пошук не виявив проблеми в моєму коді, і весь зразковий код робив більш-менш одне й те саме. Мій аналізатор шини не виявив жодних проблем. Що дає? ЦЕ ДАЄ ( архівовано )! Я був у нестямі від люті, коли знайшов це повідомлення на форумі. Ось я намагався зібрати швидкий пристрій, а моя SPI-шина мала бути обмежена швидкістю втомленого равлика, який спокійно прогулюється по арахісовому маслу! Після ще кількох тестів я виявив, що SPI-блоки будуть добре працювати приблизно до 16 МГц, з чим мені доведеться жити.
Пристрої SPI не мають FIFO, тому код повинен вручну передавати їм по одному байту за раз і зчитувати по одному байту за раз. Це означає, що між байтами на шині є проміжок, оскільки код переміщує байти в регістри та пам’ять і назад. Це марна трата потенційної швидкості. Рішення – DMA. На щастя, цей чіп має DMA. На жаль, він неймовірно зіпсований, до такої міри, що я починаю підозрювати, що його розробив якийсь недосипаний божевільний.
Звичайний пристрій DMA має мінімальну глобальну конфігурацію та кілька каналів, кожен незалежний від решти. Кожен канал зазвичай має адресу джерела, адресу призначення, довжину та певну конфігурацію для зберігання таких речей, як розмір блоку передачі, тригер, біти дозволу переривання тощо. Таким чином, у мікроконтролерах ARM поширена практика, коли кожен канал має саме ці 4 32-бітні регістри конфігурації: SRC, DST, LEN, CFG. Це 16 байт SRAM на канал. ATSAMD21 має 12 каналів DMA, тож це буде 192 байти конфігураційних даних для пристрою DMA в цілому. Не так багато. Що ж, Atmel нічого з цим не робила! Натомість сам пристрій має лише ВКАЗІВНИК на те, де в оперативній пам’яті користувача знаходяться всі ці конфігураційні дані. Для кожної передачі пристрій DMA завантажує свій внутрішній стан для активного каналу з цієї структури в оперативній пам’яті, а потім працює з каналом. Якщо дані іншого каналу вже були завантажені, вони спочатку будуть записані в оперативну пам’ять. Залежно від вашого рівня досвіду, ви, можливо, вже чуєте третє чи четверте «о, чорт забирай, ні», поки читаєте це…
Чому це погано? Уявімо, що два пристрої SPI живляться від DMA. Кожен з них матиме два канали DMA, один для прийому, один для передачі. Всього активними будуть чотири канали. Що ж відбувається, коли обидва пристрої SPI увімкнені? Два канали DMA (передавальні) стануть активними та спробують надіслати байт. Першим піде один, потім другий. Це згенерує 14 (!!) транзакцій шини до оперативної пам’яті! Чотири для зчитування конфігураційних даних для одного каналу, один для зчитування байта для надсилання, чотири для запису цих конфігураційних даних, чотири для зчитування конфігураційних даних для наступного каналу та ще одна для зчитування байта для надсилання. Отже, щоб надіслати 2 байти, пристрій DMA виконав 14 звернень до оперативної пам’яті. Не дуже добре. Але зачекайте… це ще не все. Давайте подивимося, що відбувається далі, коли пристрої SPI завершують надсилання цього байта та фіксують отриманий байт, але також готові до надсилання наступного байта! На даний момент логічно потрібно перемістити лише чотири байти (два з пристроїв до буферів прийому, два з буферів передачі до пристроїв). Подивимося, як це буде відбуватися. Пам’ятайте, що внутрішні конфігураційні дані пристрою DMA наразі завантажені до другого каналу передачі. Спочатку йому потрібно буде виконати 4 записи, щоб вивести ці дані, потім 4 читання, щоб завантажити структури першого каналу прийому, один запис у пам’ять, щоб записати отриманий байт в оперативну пам’ять, 4 записи, щоб вивести структури цього каналу, 4 читання, щоб завантажити структури для каналу прийому номер 2, один запис отриманого байта в оперативну пам’ять, 4 байти, щоб записати структуру конфігурації для цього каналу назад в оперативну пам’ять, а потім 14, які ми вже обговорювали, щоб відправити наступні два байти. Це дає 36 звернень до оперативної пам’яті, щоб просто прочитати два байти та записати два байти. Весь цей клопіт, просто щоб заощадити транзистори на 192 байтах SRAM, які знадобилися б блоці DMA для зберігання всіх конфігураційних даних внутрішньо.
Отже, чому це погано? Припустимо, наш мікроконтролер працює на проектній швидкості 48 МГц, а його SPI-блоки працюють на проектній максимальній швидкості 12 МГц. У момент, коли потрібно відправити другий байт і отримати перший прийнятий байт, нам потрібно буде виконати 36 звернень до оперативної пам’яті, а також 4 звернення до SPI-блоку. SPI-блок знаходиться на шині APB, що означає, що будь-який доступ до нього займає щонайменше 4 цикли. Це означає, що між кожним відправленим і прийнятим байтом нам знадобиться 36 + 4 * 4 = 52 цикли. Якщо SPI-блок працює на швидкості, що становить 1/4 швидкості процесора, то він надсилатиме/отримуватиме байт кожні 8 * 4 = 32 цикли. Отже, кожні 32 цикли нам потрібно буде виконати 52 цикли роботи. Коли вони не отримують достатньо циклів, канали DMA здаються і перестають працювати… Упс…
Отже, що можна зробити? Я розробив гібридний метод, де я надсилаю дані за допомогою запису процесора, а приймаю їх за допомогою прямого доступу до пам’яті (DMA). Це працювало для двох каналів, але не для більшої кількості. Як тільки я отримав плати rev2 з 4 мікросхемами оперативної пам’яті, навіть це не вдалося, оскільки лише 4 блоки DMA для прийому позбавляли один одного пропускної здатності та були скасовані. Чому Atmel був таким скупим на внутрішню SRAM? Ми, мабуть, ніколи не дізнаємося. Але вони могли б вирішити цю проблему простіше, ніж зі 192 байтами SRAM у блоці DMA. Просто додавання 4-байтових FIFO до блоків SPI також мало б значення, тоді кожна транзакція DMA могла б передавати більше одного байта, що зменшило б цей затор. На жаль, очевидно, ніхто в Atmel навіть не намагався насправді використовувати свій чіп для чогось. Atmel, що з тобою сталося?
Мої проблеми з тактуванням на цьому не закінчилися. Цей чіп має кілька внутрішніх генераторів, один з яких має бути досить точним 32 кГц генератором під назвою OSC32K . Я хотів використати його як джерело тактової частоти для таймера, щоб реалізувати свій віртуальний годинник реального часу. Що ж, незважаючи на багато зусиль і багато сліз, цей клятий годинник так і не запустився… ніколи. Код має бути простим: SYSCTRL->OSC32K.bit.EN32K = 1; SYSCTRL->OSC32K.bit.ENABLE = 1; while (!SYSCTRL->PCLKSR.bit.OSC32KRDY); Так… цього не сталося. Зрештою, я вирішив, що можу використовувати менш точний OSC32KULP для тактування свого таймера. Він запустився, і я зміг його використовувати. На цьому етапі проєкту я вже був виснажений, знечутливий до численних недоліків цього чіпа та геть втратив свідомість, тому змирився з дещо неточним годинником реального часу та продовжив працювати.
Про підтримку SD-карт багато чого сказати не можна. Був там, зробив це, отримав футболку. У моєму початковому коді для прототипу використовувалося багатоблочне читання та запис для кращої швидкості доступу до карти, але в остаточному прототипі я був змушений відмовитися від цього, оскільки один з чіпів оперативної пам’яті на платах b2 використовував спільну шину SPI з SD-картою, тому залишати карту вибраною було неможливо. Це не було такою вже й великою проблемою, оскільки доступ до SD-карт рідко, якщо взагалі коли-небудь, є вузьким місцем. Підтримується будь-яка карта об’ємом до 2 ТБ.
У версії плати v2 я підключив контакт виявлення карти до мікроконтролера. Він не використовувався, але я подумав, що міг би знайти для нього застосування. Я цього не зробив, тому в платах v3 його видалили. Я також додав світлодіод “активності” карти, який засвічується під час звернення до карти. Це просто світлодіод між лінією вибору чіпа карти та Vcc. Щоразу, коли карта вибрана, вона ввімкнена. Цей світлодіод також виконує другу функцію. Якщо під час завантаження SD-карта або SPI SRAM не вдається ініціалізувати, система видасть код помилки, щоб допомогти визначити проблему.
Тепер, коли прототип запрацював, і я робив макет для фінальної версії, я вирішив дещо зробити, щоб він виглядав круто. Я закопав усі доріжки в шарах 2 та 3, залишивши шари 1 та 4 безперервною міддю. Виглядає супер круто! Звичайно, верхній шар міді перерваний для власне SMT-площадок, але крім цього, все ідеально гладко і виглядає приголомшливо!