Рубрики
Без рубрики

Загрузка собственного ядра на Raspberry Pi через UART

В этом посте мы проходим процесс отправки ядра над Уарт, задачи, которые я столкнулся, делая это, и как я их преодолел. Помечено с C, ядром, раскладными, Python.

Перекрестные от моего личного блога https://blog.nicolasmesa.co .

За последние несколько недель я читал отличный книга О создании собственной операционной системы для малины PI с нуля. Это был отличный опыт, и я многому научился. Одна вещь, которую мне не понравилось, был мой рабочий процесс развития, который выглядел так:

  1. Сделайте изменение кода.
  2. Создайте изменение.
  3. Отключите Raspberry Pi.
  4. Снимите SD-карту.
  5. Подключите SD-карту к моему ноутбуку.
  6. Скопируйте недавно построенное ядро на SD-карту.
  7. Извлечь SD-карту.
  8. Поместите SD-карту обратно на Raspberry Pi.
  9. Подключите Raspberry PI к своему компьютеру.
  10. Закрыть предыдущий терминал с момента экран Команда сделала это довольно бесполезным.
  11. Подключитесь к Raspberry PI, используя экран команда.
  12. Проверьте мои изменения.

Все это было довольно много времени, затрагивающее обратную связь. Я искал, чтобы посмотреть, был ли способ улучшить мой рабочий процесс и нашел проблему в репо Github под названием UART Boot. . Я прочитал через него и решил построить сам UART в качестве учебного упражнения. В этом сообщении я проходил процесс, который я взял, чтобы сделать работу загрузки UART, включая мои неправильные предположения/неудачи и как я их преодолел.

Требования

Чтобы этот проект стоит мое время, я решил, что нужно было сделать следующее:

  1. Ядро должно действовать как загрузчик UART, так и сама ядра (один исполняемый файл).
  2. Я должен быть в состоянии отправить недавно скомпилированное ядро над UART, а Raspberry Pi должен загружаться.
  3. Должен иметь интерактивный сеанс через UART, используя мою программу (в основном, заменить экран ).
  4. экран замена должна оставить мой терминал в рабочем состоянии (я предполагаю, что есть способ сделать эту работу в экран Но я хотел сделать это как учебное упражнение).

Предварительные условия

Если вы хотите следовать, вам понадобится:

  1. Чтобы встретить все Предпосылки из книги Отказ
  2. Чтобы закончить Первый урок Отказ
  3. Чтобы закончить Второе упражнение Чтобы использовать UART вместо мини UART (это может работать с мини UART, но я не пробовал).
  4. Python 3.7 (это версия, которую я использовал).

Примечание Если вы еще не завершили упражнения и все равно хотелось бы следовать, следуйте инструкциям по установке ниже. Я также пометил каждый коммит, чтобы облегчить следить/прыгать вперед.

Настраивать

Если у вас уже есть свой проект, убедитесь, что вы построили свое ядро и имейте его в SD-карте. В противном случае вы можете клонировать мои Репозиторий и может следовать вдоль:

$ git clone https://github.com/nicolasmesa/PiOS
$ git checkout tags/uart-boot-initial

Git Tags.

Я создал теги для каждого раздела, если вы хотите экспериментировать с определенной частью проекта. Вы можете оформить оформление тегов, запустив команду:

$ git checkout tags/

Я ссылаюсь на имя тега в конце каждого раздела.

Git Tag: uart-boot-paralссылка на сайт

Замена экрана

Чтобы иметь возможность отправлять ядро над UART, нам нужно установить последовательное соединение с Raspberry Pi и отправлять байты ядра. экран Замена сделает то же самое с разницей, что он также будет читать из серийного соединения и stdin и напишу на stdout. . Если мы можем сделать эту работу, отправка ядра должна быть легкой.

Давайте начнем при установке пизериал (Я предлагаю вам сделать это в Виртуальная среда ):

(rpi_os) $ pip install pyserial==3.4

Давайте создадим файл под названием boot_send.py и добавьте следующий код:

import os
import select
import serial
import sys
import time
import tty

class UartConnection:

    def __init__ (self, file_path, baud_rate):
        self.serial = serial.Serial(file_path, baud_rate)

    def send_string(self, string):
        return self.send_bytes(bytes(string, "ascii"))

    def send_bytes(self, bytes_to_send):
        return self.serial.write(bytes_to_send)

    def read(self, max_len):
        return self.serial.read(max_len)

    def read_buffer(self):
        return self.read(self.serial.in_waiting)

    def read_buffer_string(self):
        return self._decode_bytes(self.read_buffer())

    def start_interactive(self, input_file, output_file):
        try:
            tty.setcbreak(input_file.fileno())
            while True:
                rfd, _, _ = select.select([self.serial, input_file], [], [])

                if self.serial in rfd:
                    r = self.read_buffer_string()
                    output_file.write(r)
                    output_file.flush()

                if input_file in rfd:
                    r = input_file.read(1)
                    self.send_string(r)
        except KeyboardInterrupt:
            print("Got keyboard interrupt. Terminating...")
        except OSError as e:
            print("Got OSError. Terminating...")
        finally:
            os.system("stty sane")

    def _decode_bytes(self, bytes_to_decode):
        return bytes_to_decode.decode("ascii")

Вау, это много кода! Давайте пройдем через это.

Конструктор

Конструктор ( __init__ ) получает два аргумента, которые мы используем для создания последовательной связи:

  • file_path : Путь к файлу, где устройство UART ( /dev/cu. SLAB_USBTOUART на моем компьютере).
  • baud_rate : Скорость передачи для использования в последовательном соединении.
Методы полезности

У нас есть куча методов утилиты с различными уровнями абстракции, но все они в конечном итоге вызывают либо send_bytes (который звонит serial.write ) или читать (который звонит Serial.read ).

start_interactive.

start_interactive Метод получает два аргумента ( input_file и emput_file ). Мы будем использовать эти аргументы как stdin и stdout , соответственно. Первое, что этот метод имеет звонок tty.setcreak (input_file.fileno ()) на stdin файл. tty.setcreak повороты TTY в режим «сырой» И позволяет нам читать символы, которые они набираются вместо буферизации их, пока пользователь не нажимает в Enter.

Выберите Функция получает набор файловых дескрипторов для мониторинга. При вызове, он блокирует, пока один из этих файлов не имеет данных, которые мы можем прочитать. Когда Выберите Функция возвращается, RFD Переменная имеет список дескрипторов файлов, имеющих доступные данные. Далее мы проверяем, если серийный Файловый дескриптор имеет доступные данные, и, если он это делает, мы все прочитаем все и отправьте вывод на наш терминал ( Вывод_file ). Мы также проверяем, если наш input_file Имеет данные, и, если это так, мы отправляем его через последовательное соединение с Raspberry Pi. Обратите внимание, что в этом случае мы ничего не пишу к emput_file И мы полагаемся на Raspberry Pi эхо обратно персонаж, который мы отправили.

OS.System («STTY SANE») Вызов делает наши TTY не в режиме RAW. Эта функция позволяет нам продолжать использование одного и того же терминала (вместо проблемы, с которой у меня есть Экран ).

def main():
    import sys
    uart_connection = UartConnection(
        # Change these to match your setup
        file_path='/dev/cu.SLAB_USBtoUART',
        baud_rate=115200
    )
    time.sleep(1)
    uart_connection.start_interactive(sys.stdin, sys.stdout)

if __name__ == ' __main__':
    main()

Теперь давайте выполним скрипт. Предполагая, что все сработало, вы должны увидеть что-то вроде этого:

(rpi_os) $ python boot_send.py
Hello world!

Вы должны быть в состоянии ввести что-нибудь, что вы хотите, и Raspberry Pi должен отозвать его обратно. Нажмите Ctrl-C Чтобы выйти, и ваш терминал все равно должен работать нормально.

Примечание: Если вы используете ветвь GIT I, вам нужно будет ударить Введите Чтобы увидеть «Hello World!» сообщение. Я сделал это, чтобы убедиться, что ядро отправляет только Привет мир! сообщение после boot_send успешно подключился.

Git Tag: UART-Boot-экран-заменассылка на сайт

Отправка ядра над UART

Отправка ядра не должно быть отличным, чем отправки символов через серийный; Ведь все переводится в байты.

Протокол

Мы будем использовать следующий протокол для отправки ядра:

  1. Raspberry Pi сапоги в ядро и блокирует, ожидая читать одну строку.
  2. Если линия это читает, совпадает с словом Ядро Затем мы отправляемся в режим загрузки UART, если нет, мы пропускаем загрузку UART.
  3. Когда в режиме загрузки UART boot_send Сценарий отправляет целое число (4 байта), содержащее размер ядра, который мы собираемся отправлять.
  4. Raspberry Pi принимает к сведению этот размер и отправляет его обратно.
  5. boot_send Сценарий проверяет, что Raspberry Pi получил правильный номер.
  6. boot_send Сценарий начинает читать ядро и отправляет его байт byte по последовательному соединению.
  7. Raspberry PI получает каждый байт и …
    1. Держит бегущую сумму всех байтов (контрольная сумма).
    2. Места, которые байт в памяти (перезаписывают текущее ядро).
  8. Когда Raspberry Pi получает количество байтов, равных размеру ядра (отправлено на предыдущем шаге), он отправляет контрольную сумму над UART.
  9. boot_send Скрипт проверяет, что контрольные суммы совпадают (для целей обнаружения ошибок).
  10. Raspberry Pi отвечает строкой «Готово» Чтобы обозначить, что теперь он будет прыгать на новое ядро.
  11. Raspberry Pi прыгает назад к адресу 0x00 начать выполнение в новом ядре.
  12. boot_send Скрипт начинает интерактивный сеанс.

Одна вещь, которую мы должны иметь в виду, это Эндианс при отправке размера. Я решил использовать Big-Endian для всех коммуникаций.

модификации boot_send

Нам нужно добавить некоторые функциональные возможности нашему boot_send Скрипт для реализации протокола:

Уартнация

Давайте добавим эти функции нашему Уартнация класс:

class UartConnection:
    # ...
    def send_line(self, line):
        if not line.endswith("\n"):
            line += "\n"
        return self.send_string(line)

    def send_int(self, number):
        if number > 2 ** 32 - 1:
            raise 'Number can only be 4 bytes long'
        number_in_bytes = number.to_bytes(4, byteorder='big')
        return self.send_bytes(number_in_bytes)

    def read_int(self):
        bytes_to_read = 4
        number_bytes = self.read(bytes_to_read)
        return int.from_bytes(number_bytes, byteorder='big')

    def read_line(self):
        return self._decode_bytes(self.serial.readline())

Самое важное, что нужно назвать здесь BYTEORDER Что мы решили использовать ( Big стоит за Big-Endian ).

Контрольная сумма ядра

Мы вычисляем контрольную сумму, чтобы подтвердить некоторые ошибки. Я не думаю, что это лучший способ проверить на наличие ошибок, так как что-то простое, как изменение порядка двух байтов, все равно сделают этот контрольный пропуск. Я использую это, потому что это дает мне немного больше уверенности, что вещи работают, как ожидалось.

def compute_kernel_checksum(kernel_bytes):
    num = 0
    for b in kernel_bytes:
        num = (num + b) % (2 ** 32)
    return num

Мы обернуемся в 2 ^ 32 потому что это максимальный размер целого числа.

send_kernel.

Добавим функцию для отправки ядра на Raspberry Pi для нашего протокола:

def send_kernel(path, uart_connection):
    with open(path, mode='rb') as f:
        uart_connection.send_line("kernel")
        kernel = f.read()
        size = len(kernel)
        checksum = compute_kernel_checksum(kernel)

        print("Sending kernel with size", size, "and checksum", checksum)
        uart_connection.send_int(size)
        time.sleep(1)
        size_confirmation = uart_connection.read_int()
        if size_confirmation != size:
            print("Expected size to be", size, "but got", size_confirmation)
            return False

        print("Kernel size confirmed. Sending kernel")
        uart_connection.send_bytes(kernel)
        time.sleep(1)

        print("Validating checksum...")
        checksum_confirmation = uart_connection.read_int()
        if checksum_confirmation != checksum:
            print("Expected checksum to be", checksum,
                  "but was", checksum_confirmation)
            return False

        line = uart_connection.read_line()
        if not line.startswith("Done"):
            print("Didn't get confirmation for the kernel. Got", line)
            return False

        return True

Наша функция получает путь к файлу ядра и Уартнация объект. Мы используем их, чтобы следовать протоколу, описанному выше для отправки ядра на Raspberry Pi.

Вызов функции send_kernel

Давайте сделаем небольшой твик нашему Главная функция.

def main():
    # ...
    uart_connection = UartConnection(...)
    time.sleep(1)
    result = send_kernel(
        path="kernel8.img",
        uart_connection=uart_connection
    )
    if result:
        print("Done!")
        uart_connection.start_interactive(sys.stdin, sys.stdout)
    else:
        print("Error sending kernel :(")

Наш сценарий сначала отправляет ядро, а затем начинает интерактивный сеанс.

Git Tag: UART-BOOT-BOOT-TEST-КЛИЕНТ-КЛИЕНТ-КЛИЕНТ-КЛИЕНТссылка на сайт

Модификации боковых ядров

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

Flather_to_address.

Последняя часть нашего протокола загрузки включает в себя прыжки назад к адресу 0x00 и начать исполнение из этого местоположения памяти. Давайте начнем с добавления функции, которая может сделать это.

Открыть Utils.h Файл и добавьте объявление функции:

extern void branch_to_address( void * );

Теперь иди к Utils. S Файл и добавьте определение:

global branch_to_address
branch_to_address:
    br x0
uart_send_int/uart_read_int.

Нам нужно будет отправлять и читать целые числа через UART (для размера ядра и контрольную сумму). Давайте откроем uArt.h Файл и добавьте следующие объявления:

void uart_send_int(int number);
int uart_read_int();

Давайте реализуем функции в uArt.c файл:

int uart_read_int() {
    int num = 0;
    for (int i = 0; i < 4; i++) {
        char c = uart_recv();
        num = num << 8;
        num += (int)c;
    }
    return num;
}

void uart_send_int(int number) {
    uart_send((char)((number >> 24) & 0xFF));
    uart_send((char)((number >> 16) & 0xFF));
    uart_send((char)((number >> 8) & 0xFF));
    uart_send((char)(number & 0xFF));
}

В uart_read_int , мы читаем один байт за раз (сначала самые значительные байты) и продолжайте переносить эти байты влево, пока не получим 4 байта, которые представляют целое число. В uart_send_int.int. Мы отправляем один байт за раз (сначала самый значительный байт). Обратите внимание, что использование указателей здесь не хорошая идея, так как мы не должны делать предположения о том, как манеры Pi Moits int S в памяти ( Big-Endian vs. Little-Endian ).

включить Utils.h.

Открыть kernel.c Файл и добавьте следующее #include утверждение:

#include "utils.h"
readline.

Здесь мы пишем довольно простой readline Функция:

int readline(char *buf, int maxlen) {
    int num = 0;
    while (num < maxlen - 1) {
        char c = uart_recv();
        if (c == '\n' || c == '\0' || c == '\r') {
            break;
        }
        buf[num] = c;
        num++;
    }
    buf[num] = '\0';
    return num;
}
strcmp.

Наш протокол должен прочитать линию текста и сравнить его против слов ядро . Для этого нам нужна функция сравнения строки:

int strcmp(char *str1, char *str2) {
    while (1) {
        if (*str1 != *str2) {
            return *str1 - *str2;
        }

        if (*str1 == '\0') {
            return 0;
        }

        str1++;
        str2++;
    }
}
copy_and_jump_to_kernel.

Теперь у нас есть все куски для реализации протокола, который мы подняли ранее:

void copy_and_jump_to_kernel() {
    int kernel_size = uart_read_int();
    uart_send_int(kernel_size);

    char *kernel = (char *)0;
    int checksum = 0;
    for (int i = 0; i < kernel_size; i++) {
        char c = uart_recv();
        checksum += c;
        kernel[i] = c;
    }
    uart_send_int(checksum);

    uart_send_string("Done copying kernel\r\n");
    branch_to_address((void *)0x00);
}

Эта функция получает размер ядра и отправляет его обратно. Затем получает каждый байт из нового ядра и помещает его в память, начиная с адреса 0x00 Отказ Далее он отправляет рассчитанную контрольную сумму и строку, говорящую «Сделано копирование ядра» Отказ Наконец, это ветви, чтобы начать выполнение по адресу 0x00 (где проживает новое ядро).

kernel_main.

Давайте добавим логику, чтобы прочитать строку на загрузке и сравнить его против Ядро решить, хочу ли мы загружать на UART или нет:

void kernel_main(void) {
    int buff_size = 100;
    uart_init();

    char buffer[buff_size];
    readline(buffer, buff_size);
    if (strcmp(buffer, "kernel") == 0) {
        copy_and_jump_to_kernel();
    }

    uart_send_string("Hello world!\r\n");
    while (1) {
        uart_send(uart_recv());
    }
}

Обратите внимание, что звонок на copy_and_jump_to_kernel Никогда не возвращается.

Git Tag: uArt-boot-boot-red-jernel-сторонассылка на сайт

Тестирование нашего загрузки UART

Создайте ядро и скопируйте его на SD-карту

Вот шаги, которые я взял, чтобы построить ядро и скопировать его на SD-карту на моем Mac:

$ ./build.sh
$ cp kernel8.img /Volumes/boot/

Тест загрузки: отправка одно и то же ядро

Теперь извлеките SD-карту и положите в свою малину PI.

Давайте запустим наш boot_send Сценарий:

(rpi_os) $ python boot_send.py 
Sending kernel with size 1991 and checksum 201267
Kernel size confirmed. Sending kernel
Validating checksum...
Done!

Если вы видите это, это означает, что вы только что отправили новое ядро (то же ядро, которое вы установили на SD-карте) через UART! На данный момент Ядро застрял в readline Функция, чтобы проверить, хотите ли вы отправить новое ядро или нет. Давайте нажмем ввод:

Hello world!
Got keyboard interrupt. Terminating...

Нажмите Ctrl-C выходить.

Тест загрузки: сделать небольшое изменение

Отправка такого же ядра – не поэтому мы сделали всю эту работу! Давайте сделаем небольшое изменение в наш код ядра, компилируйте его и отправьте его через UART (вместо того, чтобы поместить его в SD-карту). Перейти к Kernel_Main Функция и изменить строку, которую мы отправляем:

int kernel_main(void) {
    // ...
   uart_send_string("Hello from a new kernel!!!\r\n"); 
   // ...
}

Давайте комбинируем его и выполните свой скрипт Python (не нужно больше копировать на SD-карту!):

(rpi_os) $ ./build.sh
(rpi_os) $ python boot_send.py 
Sending kernel with size 2005 and checksum 202127
Kernel size confirmed. Sending kernel
Validating checksum...
Done!

Здорово! Мы отправили ядро! Теперь момент истины! Давайте нажмем ввод:

Hello from a new kernel!!!
Got keyboard interrupt. Terminating...

Git Tag: uart-boot-string-stract-test-uart-boot-bootссылка на сайт

Тест загрузки: добавление функции

Мы сделали это! Мы закончили, верно? Ну, не так быстро! Давайте попробуем сделать разные изменения. Давайте добавим функцию и позвоните в это из Kernel_Main Отказ Открыть kernel.c Файл и добавьте следующий код:

// ...
void my_test_function(void) {
    uart_send_string("Sending a test message!\r\n"); 
}

void kernel_main(void) {
    // ...
   uart_send_string("Hello from a new kernel!!!\r\n");  
   my_test_function();
   // ...
}

Мы добавили новую функцию под названием my_test_function и мы называем это из Kernel_Main Отказ Давайте компилируем и отправьте это через UART:

(rpi_os) $ ./build.sh
(rpi_os) $ python boot_send.py Sending kernel with size 2077 and checksum 208704
Kernel size confirmed. Sending kernel
Validating checksum...

Это висит в Проверка контрольной суммы … часть! Так что же происходит? Давайте подумаем на минуту о том, что мы делаем. Raspberry Pi загружает наше ядро с SD-карты на адрес 0x00 и начинает выполнять код в этом адресе. Когда мы копируем ядро над (по UART), мы перезаписываем ядро с SD-карты с новым ядром. Если ядра одинаковы, это не имеет значения, так как мы эффективно оставляем код как есть. Наше изменение строки не повлияло на нас либо потому, что струны идут в .Rodata (Данные только для чтения) Раздел исполняемого файла, который идет после .Text Раздел (где живет исполняемый код), поэтому изменение строки не изменяет адреса исполняемого кода. Добавление функции, однако, меняет .Text макет Приведя ядро «испорченным» сам, пока он копирует новое ядро.

Git Tag: UART-BOOT-FAIL-BY-ADILING-ФУНКЦИЯссылка на сайт

Исправление

Перед внедрением исправления давайте удалим my_test_function вернуться в «хорошее» состояние.

Git Tag: UART-Boot-Remove-Test-Functionссылка на сайт

Опции

Итак, как мы исправим эту проблему? Давайте рассмотрим эти два варианта:

Вариант 1: Скопируйте ядро в другое место и прыгайте там

Использование этой опции не перезаписывало бы ядро, которое в данный момент работает, и мы бы перешли на новое ядро по другому адресу. Для этого подхода есть несколько недостатков:

  1. Код ядра не начнется в адресе 0x00. . Курс может сделать некоторые предположения о ядре в адресе 0x00 И мы будем нарушать эти предположения.
  2. Курс может использовать диапазон адресов, который мы выбираем, чтобы поставить наше новое ядро.
  3. Мы не сможем отправить ядро несколько раз на UART в том же сеансе (как только мы копируем на ядро, и мы работаем в новом адресе, у нас не было бы нового места для копирования другого нового ядра).
Вариант 2: Скопируйте текущее запускное ядро на новый диапазон адресов, прыгайте к нему, скопируйте новое ядро на исходный диапазон адресов, перейдите к нему

Для реализации этого варианта нам нужно:

  1. Скопируйте текущее ядро на новое местоположение памяти (скажем, 0x8000 ).
  2. Перейти к функции в новом диапазоне адресов.
  3. Функция в новом местоположении памяти реализует протокол, описанный выше (наш COPY_AND_JUMP_TO_KERNEL ФУНКЦИЯ).

    • Это копирует ядро над UART, начиная с местоположения памяти 0x00 Отказ
  4. Прыгать назад к адресу 0x00 Отказ
  5. Начните запускать новое ядро.

Мы рассматриваем диапазон адресов, начиная с 0x8000 Как временное хранилище, которое мы можем использовать для других целей, как только мы вернемся к адресу 0x00 Отказ

С этой реализацией мы можем сохранить все предположения, которые можно сделать курс, и мы можем копировать ядро несколько раз на одном сеансе! Этот подход называется цепной загрузкой. Вы можете прочитать больше о Цепь загрузки здесь Отказ

Исправлена реализация

Позиция независимый код

В настоящее время компилятор компиляторует наше ядро предположением, что его код начинается по адресу 0x00. . Компилятор может затем жесткокодировать абсолютные адреса каждой функции, используя это. Абсолютные адреса становятся проблемой, как только мы начнем выполнять инструкции в ядре, который начинается в расположении 0x8000 Потому что он все равно будет ссылаться на старые абсолютные места памяти.

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

Чтобы включить это, нам нужно настроить нашу Makefile Чтобы добавить -FIC флаг. Вы можете прочитать больше об этом флаге Этот стек переполняет пост Отказ

# -fPIC makes the addresses relative instead of absolute allowing
# us to place the kernel anywhere in memory.
COPS = -fPIC -Wall -nostdlib -nostartfiles -ffreestanding -Iinclude -mgeneral-regs-only
ASMOPS = -fPIC -Iinclude

Это изменение – это все, что нужно, чтобы наш код независимой!

Определение текущего размера ядра

Ядро нужно знать его размер, чтобы иметь возможность копировать себя. Мы используем BSS_END раздел Linker.ld Файл для определения размера ядра. Давайте добавим ссылку на этот раздел в нашем kernel.c файл:

#include "utils.h"
// ...
extern char bss_end[];

Мы определяем это как массив, хотя он технически не (вы можете прочитать больше о том, почему в это пост ). Теперь мы можем использовать BSS_END. Как указатель на адрес, где заканчивается ядро (это включает в себя .bss ).

copy_current_kernel_and_jump.

Давайте реализуем функцию, которая:

  1. Копирует текущее ядро (начиная с адреса 0x00 ) на новый диапазон адресов (начиная с 0x8000 ).
  2. Прыгает к copy_and_jump_to_kernel Функция, что мы писали ранее, в новом диапазоне адресов.

Примечание: Обязательно определите эту функцию ниже copy_and_jump_to_kernel Отказ

void copy_current_kernel_and_jump(char *new_address) {
    char *kernel = (char *)0x00;
    char *end = bss_end;

    char *copy = new_address;

    while (kernel <= end) {
        *copy = *kernel;
        kernel++;
        copy++;
    }

    // Cast the function pointer to char* to deal with bytes.
    char *original_function_address = (char *)©_and_jump_to_kernel;

    // Add the new address (we're assuming that the original kernel resides in
    // address 0). copied_function_address should now contain the address of the
    // original function but in the new location.
    char *copied_function_address =
        original_function_address + (long)new_address;

    // Cast the address back to a function and call it.
    void (*call_function)() = (void (*)())copied_function_address;
    call_function();
}

Эта функция копирует текущий рабочий ядр на диапазон адресов, начиная с new_address Отказ Тогда это делает какую-то указатель по математике для определения адреса copy_and_jump_to_kernel Функция в новом диапазоне адресов. Наконец, он вызывает эту функцию в новом диапазоне адресов.

Позвоните copy_current_kernel_and_jump из kernel_main

Все, что осталось, это позвонить в copy_current_kernel_and_jump от kernel_main. И мы должны быть готовы проверить:

void kernel_main(void) {
    // ...
    if (strcmp(buffer, "kernel") == 0) {
        copy_current_kernel_and_jump((char *)0x8000);
    }
    // ...
}

Git Tag: UART-Boot-Fix-Реализацияссылка на сайт

Тестирование исправления

Во-первых, нам нужно создать наше ядро и скопировать его на SD-карту. Обратитесь к Build Ядро и скопируйте его в раздел SD-карты.

Давайте выполним те же тесты, которые мы побежали, прежде чем, чтобы убедиться, что мы все еще можем отправить ядро над UART. Обратитесь к следующим разделам:

  • Тест загрузки: отправка одно и то же ядро: Это поможет нам убедиться, что процесс загрузки работает с конца до конца.
  • Тест загрузки: сделать небольшое изменение: Этот тест поможет нам убедиться, что все работает как минимум, а также работало до исправления.
Тестирование предыдущей ошибки

Теперь момент истины! Давайте сделаем изменение, которое добавляет функцию, как в разделе «Добавление функции». Давайте проверим это!

(rpi_os) $ ./build.sh
(rpi_os) $ python boot_send.py 
Sending kernel with size 2280 and checksum 223784
Kernel size confirmed. Sending kernel
Validating checksum...
Done!

Этот вывод обещается! По крайней мере, наше boot_send Сценарий не висел в Проверка контрольной суммы … Шаг, как это сделал в прошлый раз. Новое ядро, вероятно, загружено и застрял в readline функция. Давайте нажмем Enter, чтобы отправить пустую строку:

Hello from a new kernel!!!
Sending a test message!
Got keyboard interrupt. Terminating...

Мы сделали это! Мы смогли добавить новую функцию в ядро, и она все еще смогла загрузиться!

Отправка одно и то же ядро несколько раз

Теперь, что, если мы попытаемся отправить одно и то же ядро снова и снова? Мы сможем отправить новое ядро, которое обрабатывает следующую багажник UART иначе без необходимости положить его в SD-карту!

(rpi_os) $ python boot_send.py 
Sending kernel with size 2440 and checksum 234324
Kernel size confirmed. Sending kernel
Validating checksum...
Done!

Здесь ничего не удивительно, наше ядро застряло в readline функция снова. На этот раз вместо нажатия в Enter, давайте нажмем Ctrl-C выйти из нашего boot_send Программа и повторно ее.

Got keyboard interrupt. Terminating...
(rpi_os) $ python test.py 
Sending kernel with size 2440 and checksum 234324
Kernel size confirmed. Sending kernel
Validating checksum...
Done!

Мы снова отправили ядро на той же сессии! Давайте нажмем Enter, чтобы убедиться, что наш новый ядр загрузился:

Hello from a new kernel!!!
Sending a test message!
Got keyboard interrupt. Terminating...

Git Tag: UART-Boot-успех - когда добавляют функциюссылка на сайт

Вывод

В этом посте мы построили ядро, которое можно загрузить через UART, чтобы проверить, или с SD-карты (для более постоянных решений). Для этого мы придумали протокол для связи UART и реализовали его. После нескольких испытаний мы заметили, что возникла проблема, когда мы пытались загрузить ядро с дополнительной функцией. Проблема заключалась в том, что ядро перезаписывало/испортила свои инструкции. Чтобы преодолеть эту проблему, мы сделали первую копию ядра в новый диапазон адресов и выполните копию UART при выполнении диапазона адресов. После того, как ядро заканчивает копирование нового ядра через UART, он может вернуться назад к исходному адресу запуска ( 0x00 ) и выполните новое ядро.

Улучшения

Вот небольшой список улучшений, которые вы можете сделать в проект:

  • Добавить аргументы на boot_send скрипт Я сделал это в Мой проект , но этот пост уже давно, как есть.
  • Чистить kernel.c Файл и извлечь функции в файл помощника.
  • Используйте C PreProcessor, чтобы исключить раздел загрузки UART, если вы не передадите Uartboot флаг.
  • Сделайте загрузку UART с несколькими процессорами. Я реализовал это Также в несколько хаки (я могу написать сообщение об этом позже).
  • Добавьте лучшую контрольную сумму ( CRC , например).

Ссылки/ссылки

Оригинал: “https://dev.to/nicolasmesa/booting-your-own-kernel-on-raspberry-pi-via-uart-565f”