Урок OK01

Начальная страница курса
Урок OK01 объясняет с чего начать, как включить ‘OK’ или ‘ACT’ светодиод на плате Raspberry Pi. Светодиод изначально был назван OK, но позже был переименован в ACT в ревизии 2 платы Raspberry Pi.

1. Первые шаги

Я предполагаю, что вы уже скачали и установили GNU Toolchain. Также раздел загрузок содержит шаблон «OS Template». Пожалуйста, скачайте его и распакуйте содержимое в новую папку.

2. Введение

После того, как вы распаковали шаблон, создайте новый файл в папке ‘source’ с названием ‘main.s’. Файл будет содержать исходный код операционной системы. Структура файлов должна выглядеть так:

build/
   (empty)
source/
   main.s
kernel.ld
LICENSE
Makefile

Откройте ‘main.s’ текстовым редактором, теперь мы можем начинать писать код на языке ассемблера. Raspberry Pi использует процессор с архитектурой ARMv6, так что именно для него мы и будем писать.
Скопируйте первые 3 команды:

.section .init
.globl _start
_start:

Как это бывает, ни одна из этих команд ничего не делает на Raspberry Pi, эти команды — инструкции для ассемблера. Ассемблер — это программа, которая преобразовывает исходные коды, которые мы понимаем в бинарные машинные коды, которые понимает Raspberry Pi. В исходном коде ассемблера каждая строка — новая команда. Первая строка говорит ассемблеру[1] куда поместить наш код. В шаблоне, который я предоставил, данные в секции ‘.init’ помещаются в начало выходного файла. Это очень важно т.к. мы хотим убедиться, что можем контролировать порядок выполнения кода. Если же мы не сделаем этого, то код будет выполняться в алфавитном порядке названий файлов исходных кодов. Команда ‘.section’ просто говорит ассемблеру в какую секцию «положить» код от текущей позиции до следующей команды ‘.section ‘ или конца файла.
Следующие две строки тут для того, чтобы убрать предупреждение и на так уж важны[2].

3. Первая строка

Теперь мы наконец собираемся закодировать что-нибудь. Компьютер просто выполняет команды одну за одной по очереди. Каждая инструкция начинается с новой строки. Скопируйте следующую команду:

ldr r0,=0x20200000 

Это наша первая команда, она говорит процессору загрузить число 0x20200000 в регистр r0. Теперь я должен ответить на два вопроса: что такое регистр и что такое 0x20200000?

Регистр — это маленький кусочек памяти в процессоре, в который процессор загружает числа с которыми работает прямо сейчас. Их довольно мало, многие из них имеют специальное назначение, которое мы рассмотрим позже. Важно что есть 13 (названы r0,r1,r2,…,r11,r12), которые называются «Общего назначения» (General Purpose), Вы можете их использовать для любых вычислений, которые Вам нужно выполнить. Я выбрал r0 из-за того, что он первый, но я мог использовать любой из них. До тех пор пока вы последовательны, это не имеет значения.

0x20200000 — это число. Оно записано в шестнадцатеричном (hex) формате.

Итак, наша первая команда загружает число 0x20200000 в регистр r0. Эта операция не выглядит важной, но это именно так. В компьютерах очень много участков памяти и устройств. Для доступа к каждому из них им присваивают имена. Почти как почтовый адрес или адрес веб страницы это просто средство идентификации местоположения устройства или куска памяти, который нам необходим. Адреса в компьютерах — это просто числа, так сложилось, что 0x20200000 — это адрес контроллера GPIO (входов/выводов общего назначения). Это число — просто решение производителя процессора, мог быть использован любой другой адрес. Я знаю его только по тому, что посмотрел его в документации[3]. Нет никакой системы при образовании адресов (кроме того, что это шестнадцатеричное число).

4. Включение вывода

Прочитав документацию, я знаю, что нужно послать два сообщения контроллеру GPIO. Мы должны говорить на его языке, и если мы сделаем так, то он услужливо сделает все что нам нужно и включит светодиод. К счастью это очень простой чип, ему нужно всего несколько чисел чтобы понять что делать.

mov r1,#1
lsl r1,#18
str r1,[r0,#4]

Эти команды включат вывод 16. Сперва мы вводим необходимое значение в регистр r1, а затем отправляем его GPIO контроллеру. Первые две команды необходимы для получения нужного значения в регистре r1, мы могли бы просто воспользоваться командой ldr для получения этого значения, но будет полезно в дальнейшем включать любой GPIO, для этого нужно получать значение по формуле, чем вписать определенное значение. Светодиод OK подключен к GPIO 16, поэтому нам нужно послать команду для включения GPIO 16.

Значение в r1 необходимо для включения светодиода. Первая команда помещает число 1 в регистр r1. Команда mov быстрее чем ldr, потому что она не взаимодействует с памятью, тогда как ldr загружает значение, которое мы хотим поместить в регистр из памяти. Тем не менее, mov может быть использована только для загрузки ограниченного набора значений[4]. В языке ассемблера ARM практически все команды имеют 3ех буквенное сокращение — мнемонику. Предполагается, что мнемоника подсказывает что делает команда. mov — сокращение от move, а ldr от load register. mov — перемещает второй аргумент #1 в первый — регистр r1. Символ # чаще всего используется для обозначения числа, но мы уже видели контрпример для этого правила.
Вторая инструкция — lsl или логический сдвиг влево (logical shift left). Это значит сдвинуть двоичное представление первого аргумента на второй аргумент. В данном случае lsl сдвинет 1 влево на 18 (1 << 18 = 0b1000000000000000000 = 262144).

Еще раз, я что нам нужно это значение только из документации[3]. В документации указано что в GPIO есть 24 байта, которые определяют настройки GPIO. Первые 4 относятся к первым 10 GPIO, следующие 4 к следующим 10 и так далее. Всего 54 GPIO пина, т.е. нам нужно 6 наборов по 4 байта, итого 24 байта. Внутри каждой 4 байтной секции, каждые 3 бита относятся к определенному GPIO пину. Мы хотим использовать 16 GPIO, т.е. нам нужен второй четырехбайтный набор (который управляет пинами 10-19) и шестой набор из 3ех бит, это объясняет число 18 (6×3) из приведенного кода.

В конце команда str (загрузить регистр — store register) загружает значение r1 в память по адресу вычисленному из конструкции [r0,#4]. В данном случае r0 содержит адрес GPIO контроллера, а значение может быть вычислено как значение r0 + второй параметр, в данном случае #4. Т.е. мы прибавляем 4 к адресу GPIO контроллера и записываем содержимое r1 в память по этому адресу. Происходит запись во второй набор из 4 байт, который я упоминал ранее. Итак, мы отправили наше первое сообщение GPIO контроллеру, которое означает приготовить 16 вывод для работы в качестве выхода.

5. Признак жизни

Теперь, когда светодиод готов к включению, нам нужно собственно его включить. Это означает, что нужно послать сообщение GPIO контроллеру, чтобы он выключил вывод (подать на него 0). Производители чипов решили, что лучше когда светодиод включается низким уровнем сигнала[5] (т.е. вывод выключен). Аппаратные инженеры часто принимают такие решения, казалось бы, просто для того чтобы держать разработчиков операционных систем на коротком поводке. Считайте себя предупрежденными.

mov r1,#1
lsl r1,#16
str r1,[r0,#40]  

Надеюсь вы можете понять все команды выше, если не их значения. Первая загружает значение 1 в r1, как и ранее. Вторая — сдвигает значение в r1 на 16 влево. Т.к. мы хотим выключить 16 пин, нам нужно установить 16 бит в сообщении в 1 (для других выводов необходимо другое значение. Последней командой мы записываем полученное значение по адресу GPIO контроллера + 40, это адрес регистра в который нужно записать полученное число для отключения вывода (28 — для включения).


В Raspberry Pi B+ ACT светодиод подключен к выводу GPIO47. Причем светодиод подключен без инверсии. Т.е. между выводом и GND. Подав 1 на выход, вы включите светодиод, 0 — выключите.
Для установки функции нужно использовать регистр GPIO Function Select 4 (смещение 16). А 001 нужно установить в биты 21-23 (т.е. сдвиг на 21).
Для установки 1 — GPIO Pin Output Set 1 (смещение 32, а бит в регистре 15).
Для установки 0 — GPIO Pin Output Clear 1 (смещение 44, бит в регистре 15)

mov r1,#1
lsl r1,#21

/*
* Set the GPIO function select.
*/
str r1,[r0,#16]

/* 
* Set the 15th bit of r1.
*/
mov r1,#1
lsl r1,#15

/* 
* Set GPIO 15 to high, causing the LED to turn on.
*/
str r1,[r0,#32]

6. Happily Ever After

Может быть заманчиво закончить сейчас, но к несчастью процессор не знает что мы закончили, процессор никогда не останавливается. Он будет работать, до тех пор пока у него есть питание. Поэтому мы должны задать Raspberry Pi бесконечную задачу, иначе возникнет ошибка (не большая проблема для данного примера, светодиод то уже включен).

loop$:
b loop$

Первая строка — это не команда, а метка. Она называет следующую строку loop$. Теперь мы можем сослаться на строку по имени. Метки преобразовываются в адреса во время преобразования кода в бинарный вид. Они очень полезны. По соглашению мы используем символ $ в конце метки, чтобы дать понять другим, что эта метка не важна для других участков программы. Команда ветвления (branch) b заставляет процессор выполнить следующей команду в строке loop$, вместо следующей после неё. Следующей будет выполнена команда b loop$, т.е. она будет выполнена еще раз и так далее до бесконечности. Таким образом процессор будет выполнять эту команду до безопасного отключения питания.

Пустая строка в конце необходима. Ассемблер GNU ожидает, что все файлы с кодами ассемблера заканчиваются пустой строкой, чтобы убедиться, что мы действительно закончили, а файл не обрезан. Если это не так, Вы получите предупреждение при работе ассемблера.

7. Время Pi

Итак, мы написали код, время запустить его на Raspberry Pi. Откройте терминал на вашем компьютере и перейдите в каталог с исходными кодами. Напишите make и нажмите enter. Если у Вас возникли какие либо проблемы обратитесь в раздел решения проблем. Если нет, то вы сгенерировали 3 файла. kernel.img — скомпилированный образ вашей операционной системы. kernel.list — листинг кодов ассемблера, которые мы написали, так как они были сгенерированы. Полезно будет проверить что все сгенерировано правильно в будущем. kernel.map содержит карту на которой расположены все метки, может быть полезно при проверке значений.

Чтобы установить операционную систему, воспользуемся картой памяти для Raspberry Pi с уже готовой операционной системой. Если Вы просмотрите файлы на SD карте, то увидите файл kernel.img. Переименуйте этот файл в что-то вроде kernel_linux.img. Затем скопируйте созданный нами kernel.img. Вы только что заменили существующую операционную систему своей. Чтобы вернуть все назад, просто удалите kernel.img и переименуйте kernel_linux.img в kernel.img. Я считаю полезным всегда держать бекап оригинальной операционной системы Raspberry Pi, на случай если она понадобиться.

Вставьте SD карту в Raspberry Pi и включите его. Светодиод OK должен загореться. Если это не так, пожалуйста посетите страницу решения проблем. Если так, поздравляю, вы только что написали свою первую операционную систему. Просмотрите урок 2, чтобы заставить светодиод мигать.


[1] Ок, я вру, это говорит компоновщику, другой программе, которая используется для объединения нескольких файлов ассемблера в одну программу. Это имеет большого значения.
[2] Они важны для Вас. Т.к. GNU toolchain используется для создания программы, он ожидает точку входа с меткой _start. Т.к. мы делаем операционную систему, _start это всегда то, что идет вначале программы, мы настроили это при помощи команды .section .init. Тем не менее если не указать точку входа toolchain будет огорчен. Вот почему, первая строка говорит что мы определяем символ _start глобальным, а вторая строка присваивает значение адреса следующей строки этому символу. Мы рассмотрим адреса вскоре.
[3] Это руководство сделано так, чтобы избавить вас от боли чтения этой документации. Но если это нужно вы можете найти её сдесь:
https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/downloads/So…
Добавляет неудобства то, что документация использует разные системы адресов. Адрес обозначенный как 0x7E200000 будет равен 0x20200000 в вашей ОС.
[4] Таким образом можно загрузить только 8 битное число. Т.е. число в диапазоне от 0 до 255.
[5] Аппаратные инженеры любезно объяснили мне это так:
Причина по которой современные производители чипов делают так, технология — CMOS, комплиментарный метал-окисел-полупроводник (Complementary Metal Oxide Semiconductor). Комплиментарный — значит, что каждый сигнал соединен с двумя транзисторами. Один из них сделан из полупроводника N-типа, который используется для создания низкого напряжения на выходе, другой из полупроводника P-типа, который используется для создания высокого напряжения. Только один транзистор из пары открыт в один промежуток времени, иначе мы бы получили короткое замыкание. Полупроводник P-типа имеет более низкую проводимость чем N-типа, что значит что транзистор P-типа должен быть по крайней мере в 3 раза больше чтобы проводить такой же ток. Вот почему светодиоды обычно подключают так, чтобы они светились при создании низкого напряжения на выходе, т.к. транзистор N-типа сильнее при создании низкого напряжения, чем P-типа при создании высокого.
Есть и другая причина. Ранее в 1970-х чипы производили из полупроводников N-типа (NMOS), полупроводник P-типа заменен резистором. Это значит, что когда сигнал в низком уровне, чип потребляет энергию (и нагревается), даже в то время когда ничего не делает. Телефон который нагревается и разряжает батарею, когда он ничего не делает, это не хорошо. Для избежания этого сигналы проектировались так, чтобы активным был низкий уровень. Т.е. высокий уровень — не активный, а энергия не потребляется в не активном состоянии. Даже сейчас, когда мы уже не используем NMOS технологию, зачастую лучше использовать низкой уровень сигнала созданный транзистором N-типа, чем высокий P-типа. Обычно сигналы с активным низким уровнем обозначают чертой над названием или записью SIGNAL_n или /SIGNAL. Но это по-прежнему может сбивать с толку, даже аппаратных инженеров!

 

Похожий код:

Фото аватара
Алексей Петров

Программист, разработчик с 5 летним опытом работы. Учусь на разработчика игр на Unity и разработчика VR&AR реальности (виртуальной реальности). Основные языки программирования: C#, C++.

Оцените автора
Бла, бла код
Добавить комментарий

  1. TAVi

    Дмитрий, спасибо за данную тематику и ваш сайт. Не хватает только дополнительной информации (хотя бы в виде ссылок на другие источники), например как установить GNU Toolchain. При чём это почти во всех ваших постах, всегда приходится что-то искать дополнительно =)

    Ответить