Operating systems from scratch; Level 1 (younger half)

  • Tutorial

This part is devoted to improving the skills of working with Rust and writing a couple of useful utilities and libraries. We will write drivers for GPIO, UART and the built-in timer. We implement the XMODEM protocol. Using all this, we will write a simple shell and bootloader. Before reading, it is strongly recommended that you read the Book . At least from beginning to end. For the lazy, but a little more experienced you can recommend this . In Russian, you can dig here .


Well, of course, bypassing the zero level is completely not worth it. Also somewhere half of this part does not require raspberries.


Useful materials


  • Book v2.0 by Rust. Translation into Russian on the way . Teasing the team of translators (and help them). This booklet is definitely worth reading at least to those who have not read it.
  • Rust Standard Library
  • . Everything is prepared there, which is in the standard delivery.
  • docs.rs - there you can read the documentation for various libraries.
  • Mana online free download without registration and SMS. RTFM!
  • Wiki article on the XMODEM
  • protocol . For general development and those who are interested in the history of origin. Little will help for implementation, i.e. almost nothing.
  • Another XMODEM
  • document .
  • BCM2837 - this is about the percentage of raspberries. There, where the last time.

Phase 0: Getting Started


Just in case, make sure once again that you are using course-compatible firmware and hardware:


  • Modern 64-bit Unix-like OS: Linux, macOS or BSD
  • You have a suitable USB port (unauthorized ones can use attachments)

Also to be following a program set: git , wget , tar , screen , make and all that was required for the zero level . For this part you will need to reinstall socat .


If last time you managed to run everything you need under Windows, then this time everything should work. But if suddenly there is no support. Neither I nor the author of the original has both a Venda at hand and a desire to delve into it.


Code retrieval


You can clone the code for this part like this:


git clone https://web.stanford.edu/class/cs140e/assignments/1-shell/skeleton.git 1-shell

Feel free to explore the contents of the repository yourself.


Questions


In this and all the following labs there will be questions . You can answer them directly in the comments using spoilers . Here is an example:


Как мы настраиваем и используем другие GPIO-контакты? [assignment0]

В прошлый раз мы использовали 16 пин GPIO во имя мигания светодиодом. Используя при этом регистры GPFSEL1 , GPSET0 и GPCLR0 . А если будем использовать пин 27, то какие регистры нам пригодятся? И какой физический контакт у этого самого 27 GPIO-пина?

The square brackets indicate the name of the file inside the directories questions/ . This is not particularly important for us, because it should be answered in the comments. Do not read other people's answers until you are sure that you answered yourself. Otherwise, it’s not interesting. But these tags can be used as headers for spoilers. However, I advise you first to write in these files. For comfort.


Phase 1: Ferris Wheel (puns)



(This part can be completely and completely skipped if you already have sufficiently deep knowledge of the rasta.)


For the sake of training, we will edit programs in Rust with some selfish goals. Some should compile after editing. Others should not compile. For the third, tests should be completed successfully.


In the bowels of the catalog ferris-wheel/ you can find the following:


  • compile-fail - contains code that must be broken so that it does not compile
  • compile-pass - contains code that must be fixed exactly before compilation
  • run-pass - contains code with tests that should become green
  • questions - in theory for questions, but we have already agreed that you can put all this in a comment

There is still a script test.sh . He checks the correctness of the tasks. If you run it, it will explain quite popularly where and what is not quite as expected. Something like:


$ ./test.sh
ERROR: compile-pass/borrow-1.rs failed to compile!

ERROR: compile-pass/const.rs failed to compile!

...

0 passes, 25 failures

In addition, the script takes the flag -v . If this flag is passed to the script, then the errors that the compiler spits out will be shown:


$ ./test.sh -v
ERROR: compile-pass/borrow-1.rs failed to compile!
---------------------- stderr --------------------------
warning: unused variable: `z`
 --> /.../ferris-wheel/compile-pass/borrow-1.rs:9:9
  |
9 |     let z = *y;
  |         ^
  |
  = note: #[warn(unused_variables)] on by default
  = note: to avoid this warning, consider using `_z` instead

error[E0507]: cannot move out of borrowed content
 --> /.../ferris-wheel/compile-pass/borrow-1.rs:9:13
  |
9 |     let z = *y;
  |             ^^
  |             |
  |             cannot move out of borrowed content
  |             help: consider using a reference instead: `&*y`

error: aborting due to previous error

...

0 passes, 25 failures

This script also accepts the string as a filter. If it is available, only those file paths ($ directory / $ filename) that match this filter will be checked. For example:


./test.sh trait
ERROR: compile-pass/trait-namespace.rs failed to compile!

ERROR: run-pass/trait-impl.rs failed to compile!

0 passes, 2 failures

One does not interfere with the other and you can combine a filter and a key -v . Something like this: ./test.sh -v filter .


How much can I change?


Each file contains a comment, which indicates how much it can be spoiled (diff budget). Т.е. the maximum number of changes that can be made to fix the program. Decisions that do not fit into this framework can be considered unsuccessful.


For example. There compile-pass/try.rs is a comment in the file compile-pass/try.rs :


// FIXME: Make me compile. Diff budget: 12 line additions and 2 characters.

It says that you can add no more than 12 lines of code (empty lines are also considered). And change (add / change / delete) 2 characters. It can be used git diff to see line-by-line changes. And git diff --word-diff-regex=. for the same, but character by character.


Another example:


// FIXME: Make me compile! Diff budget: 1 line.

It kakbe tells us that only one line of code can be changed (add / change / extend).


General rules


After the changes, the intended functionality of the programs should be preserved. Suppose if the body of a certain function needs to be changed so that it compiles, it will not be enough to add there unimplemented!() . If in doubt, try the best of what you are capable of. Well, or ask in the comments.


In addition, it is completely not recommended to do the following dirty methods:


  • Change all these assert! s
  • Modify everything that is marked as "do not modify"
  • Change comments about how much and what can be changed
  • Move, rename or add any files

When all tasks are completed it test.sh will output 25 passes, 0 failures


Подсказка: в имени файла может содержаться ключ к решению.

Подсказка: в этом уютном чатике ответят быстрее на вопросы о Rust. Быстрее, чем в комментариях к этой статье.

Что случилось? Чем чинили? Почему это работает? [имя_файлика]

Для каждой проги из этой части следует объяснить, что было не так с исходным кодом. Затем пояснить по хардкору , какие были внесены изменения и чому эти исправления делают своё грязное дело. Хорошие годные объяснения приветствуются. Если считаете, что всё для вас итак очевидно, то можно не писать. Если лень — можно не писать ничего вообще.

Phase 2: Oxidation



At this stage, we will write a couple of libraries and one utility for the command line. Work will be in subdirectories stack-vec , volatile , ttywrite and xmodem . There will also be a number of questions that can be answered if not broken. Each part is controlled by Cargo. At least these commands can be called useful:


  • cargo build - assembly program or library
  • cargo test - run tests
  • cargo run - application launch
  • cargo run -- $флаги - approximately in this way it is possible to pass flags at application launch

About Cargo there is a separate booklet: Cargo Book . From there, you can learn the necessary info about how it all works in detail.


Subphase A: StackVec


One of the most important features that operating systems deal with is the allocation of memory. Когда C, Rust , Java, Python или вообще практически любое приложение вызывает malloc() , то при нехватке памяти в конечном итоге используется системный вызов, который запрашивает у операционной системы дополнительную память. The operating system determines if there is still no memory occupied by anyone. If so, then from this memory the OS will sprinkle a little bit on the processor.


Распределение памяти — non penis canis est

Современные операционки типа всяких линупсов содержат достаточно много ухищрений, связанных с управлением памятью. Например, в порядке оптимизационных костылей, при запросе некоторого количества памяти оная выделяется виртуально. При этом физическая память не выделяется до тех пор, пока приложение не попытается эту самую память использовать. С другой стороны для приложения создаётся иллюзия упрощённого распределения. Операционные системы умеют мастерски лгать ( ).

Like structure Vec , String and the Box inside is used malloc() to allocate memory for your own needs. This means that these structures require support from the operating system. In particular, they require that the OS know how to allocate memory. We have not even started this part (see in the next series), so we do not have memory management in any form. Accordingly, Vec we cannot (yet) use all of these Vec .


This is a concentrated mess for Vec - a good abstraction suitable in all respects! She allows us to think in terms .push() and .pop() without the need to remember all sorts of subtleties. Can we get something similar Vec without a full memory allocator?



Of course. The first thing that comes to mind is the preliminary allocation of memory and its subsequent transfer to a certain structure that implements the necessary abstractions on top of it. We can allocate memory statically directly in a binary file, or somewhere on a stack. In both cases, such a memory must have a fixed predetermined size.


In this subphase, we will implement a structure StackVec that provides api similar to the one provided Vec from the standard library. But it uses a pre-allocated piece of memory. This one StackVec will come in handy when implementing the command line (in phase 3). We will work in a subdirectory stack-vec . In it you can already find the following things:


  • Cargo.toml - configuration file for Cargo
  • src/lib.rs - here we will add the necessary code
  • src/tests.rs - tests that will be run at startup cargo test
  • questions/ - blanks for files with questions (we are not very interested)

Interface StackVec


StackVec<T> created by calling StackVec::new() . The argument for f-ii new is a slice of the type T . A type StackVec<T> implements many methods that are used in almost the same way as similar ones Vec . For example. возьмём StackVec<u8> :


let mut storage = [0u8; 1024];
let mut vec = StackVec::new(&mut storage);

for i in 0..10 {
    vec.push(i * i).expect("can push 1024 times");
}

for (i, v) in vec.iter().enumerate() {
    assert_eq!(*v, (i * i) as u8);
}

let last_element = vec.pop().expect("has elements");
assert_eq!(last_element, 9 * 9);

The type is StackVec already declared in this form:


pub struct StackVec<'a, T: 'a> {
    storage: &'a mut [T],
    len: usize,
}

Understanding StackVec


There are a couple of questions about the device StackVec :


Почему push возвращает Result ? [push-fails]

Метод push из Vec , который из стандартной библиотеки, не имеет какого либо возвращаемого значения. Однако push из StackVec имеет: он возвращает результат, указывающий, что может быть какая-то ошибка. Почему StackVec::push() может завершаться ошибочно в отличии от Vec ?

Почему нам надо ограничивать T временем жизни 'a ? [lifetime]

Компилятор отклонит вот такое объявление StackVec :
struct StackVec<'a, T> { buffer: &'a mut [T], len: usize }


Если мы добавим ограничение 'a к типу T , то всё заработает:
struct StackVec<'a, T: 'a> { buffer: &'a mut [T], len: usize }


Зачем это ограничение требуется? Что будет происходить, если Rust не будет следовать этому ограничению?

Почему StackVec требует T: Clone для метода pop ? [clone-for-pop]

Метод pop из Vec<T> стандартной библиотеки реализован для любого T , однако метод pop для нашего StackVec реализуется только когда T реализует свойство Clone . Почему это должно быть так? Что не так, если удолить это ограничение?

Implementation StackVec


Implement all of the unimplemented!() methods StackVec in the file stack-vec/src/lib.rs . Each method already has documentation (from it it is clear what is required of you, for example). In addition to this, there are tests in the file src/tests.rs that help ensure that your implementation is correct. You can run tests using the command cargo test . In addition, you need to implement traits Deref , DerefMut and IntoIterator for the class StackVec . And the trait IntoIterator for &StackVec . Without the implementation of these traits, tests will fail. As soon as you are sure that your implementation is correct and you are able to answer the questions asked, proceed to the next sub-phase.


Какие тесты требуют реализации Deref ? [deref-in-tests]

Прочитайте весь код тестов из файлика str/tests.rs . Какие тесты не хотели компиляться, если не было реализации Deref ? А что на счёт DerefMut ? Why?

На самом деле тесты являются не полными

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

Подсказка: решение из задания liftime нулевой фазы может оказаться полезным.

Subphase B: volatile



In this part, we will talk about volatile memory accesses and read the code in a subdirectory volatile/ . We will not write our own code, but there are questions for self-testing.


Like typical operating systems, compilers masterfully perform tricky tricks. In the name of optimization, they are doing something that only looks like what you intended. In fact, there will be very strong sorcery inside. A good example of such witchcraft is the removal of dead code. When the compiler can prove that the code has no effect on execution, dead code is quickly and decisively cut out. Suppose there is such a code:


fn f() {
    let mut x = 0;
    let y = &mut x;
    *y = 10;
}

The compiler may think a little and reasonably reason that it is *y never read after recording. For this reason, the compiler can simply exclude this part from the resulting binary file. Continuing to reason in this way, the compiler finds it suitable to cut out the declaration itself y and then x . In the end, the challenge f() will go under the knife.


Optimizations of this kind are very useful and valuable. Thanks to them, programs are accelerated without affecting the results. True, in some cases, such fraud can have unforeseen consequences. For example. y будет указывать на какой либо регистр, доступный только на запись. In this case, the record in *y will have quite observable effects without the need for reading *y . If the compiler does not know this, then it will simply get this part at the optimization stage and our program will not work as expected.


How can we convince the compiler that reading / writing something like this affects our cozy world by itself? This is exactly what volatile memory accesses mean. The compiler swears not to optimize access to such sites.


Rusty volatile


In Rust We can use the techniques read_volatile and write_volatile to read and write raw pointers.


Что за сырые указатели такие?

До текущего момента мы успели близко познакомиться со ссылками (которые &T и &mut T ). Сырые (raw) указатели в Rust ( *const T и *mut T ) — это суть те же самые ссылки без отслеживания времени жизни borrow checker'ом. Чтения/записи с использованием этих самых сырых указателей может приводить к тем же самым травмам ног, какие можно часто наблюдать у любителей C и C++. Rust считает такие операции небезопасными. Соответсвенно это всё в обязательном порядке помечать unsafe -меткой. Подробнее о сырых указателях есть в документации .

Write calls read_volatile and write_volatile every time quite sad (besides the fact that it could lead to an unfortunate mistake on the basis of depression). Fortunately, Rust provides us with the opportunity to make our lives easier and safer. On the one hand, we can simply do a volatile wrapper (almost like a keyword volatile in a nice C) and guarantee that every read / write will remain in our code. As a bonus, we can define a wrapper for reading only or for writing only (in the present one there isn’t one, they gave the trunk and spin as you want).


Introduction to the Volatile , ReadVolatile , WriteVolatile and UniqueVolatile


The volatile catalog crate volatile/ (who would have thought?) Implements these four types, which do something that is obvious from their name. Read more in the documentation. Call cargo doc --open directly in the directory volatile/ to actually read this very documentation in a convenient way.


Почему тут есть UniqueVolatile ? [unique-volatile]

Как Volatile , так и UniqueVolatile позволяют работать с volatile-обращениями к памяти. Исходя из документации, в чём между этими двумя типами разница?

Open the code src/lib.rs . Read the code in the spirit of your own skills. After that (reading the code) answer the following couple of questions. How to finish - you can proceed to the next subphase.


Как организованно ограничение на чтение и запись? [enforcing]

Типы ReadVolatile и WriteVolatile делают невозможными соответственно чтение и запись указателя. Каким способом это осуществляется?

В чём преимущество использования трейтов вместо обычных методов? [traits]

При внимательном рассмотрении можно заменить, что каждый из типов реализует только один собственный метод new . Все остальные методы так или иначе относятся к реализациям Readable , Writeable и ReadableWriteable . Какой от этого всего профит? Опишите по крайней мере два плюса такого подхода.

Почему read и write безопасны, а new небезопасно? [safety]

Что должно быть верно в отношении к new чтоб read и write можно было считать безопасными? Было бы безопасно вместо этого помечать new как безопасный, а read и write напротив небезопасными?
Подсказка: прочтите документацию ко всем этим методам.

Почему мы принуждаем использовать new ? [pub-constructor]


If the type Volatile were declared as follows:


struct Volatile<T>(pub *mut T);

then a type value Volatile could be created using Volatile(ptr) instead of a call new . What is the use of creating our wrapper using a static call new ?


Подсказка: рассмотрите последствия на утверждения о безопасности для обоих вариантов.




Что делают макросы? [macros]

Что делают макросы readable! , writeable! и readable_writeable! ?

Subphase C: xmodem


In this subphase, we implement the XMODEM file transfer protocol (subdirectory xmodem/ ). The main work is in the file xmodem/src/lib.rs .


XMODEM is a simple file transfer protocol developed in 1977. It has packet checksums, cancellation of transmission, and the ability to automatically repeat transmission in case of errors. It is widely used to transmit information via serial interfaces like UART. The main bun of the protocol is simplicity. You can read more in the wiki: XMODEM (anyone can translate the article into Russian).


Protocol


The protocol itself is described in sufficient detail in the text file Understanding The X-Modem File Transfer Protocol . We will repeat some of the description right here.


Не основывайте свою имплементацию на объяснении из Википедии!

Хотя объяснение из педевикии будет полезно на высоком уровне, многие детали будут отличаться от того, чего мы будем реализовывать тут. Используйте педевикию только как обзор протокола.

XMODEM is quite a binary protocol: raw bytes are received and sent. In addition, the protocol is half-duplex: at any time, the sender or receiver sends data, but never both at once. And finally, this is a packet protocol: data is divided into blocks (packets) of 128 bytes each. The protocol determines which bytes to send, when to send, what they will indicate and how to read them later.


First, let's define some constants:


const SOH: u8 = 0x01;
const EOT: u8 = 0x04;
const ACK: u8 = 0x06;
const NAK: u8 = 0x15;
const CAN: u8 = 0x18;

To start the transfer, the receiver sends a byte NAK , and this sender NAK is waiting at the same time. After the sender receives the byte NAK , it can begin to transmit packets. The receiver sends NAK only to start transmission, but not every time for each packet.


After the transmission has begun, the reception and transmission of packets are identical. Packets are numbered sequentially starting with 1. When the size of one byte is exhausted (i.e., after 255), then we start counting from 0.



To send a package, the sender:


  1. Sends a byte SOH
  2. Sends a packet number
  3. Sends the return value of the packet number ( 255 - $номер_пакета )
  4. Sends the packet itself
  5. Sends a packet checksum
    • The checksum is the sum of all bytes modulo 256
  6. Waiting for one byte from the receiving side:
    • If this is a NAK byte, then try to send the packet again (up to 10 times)
    • If it is a ACK byte, then you can send the next packet

At the same time, to receive the packet, the recipient performs the inverse task:


  1. Expects bytes SOH or bytes
  2. from the sender EOT
    • If another byte is received, the receiver cancels the transmission.
    • If a EOT byte is
  3. received EOT , transfer ends.
  4. Reads the next byte and compares it with the current packet number
    • If the wrong packet number is received, cancel the transfer.
  5. Read the byte and compare it with the return packet number
    • If the wrong number is received, then cancel the transfer
  6. Read the packet itself (128 bytes)
  7. We calculate the checksum for the package.
    • Т.е. the sum of all bytes in a packet modulo 256
  8. We read one more byte and compare it with the checksum
    • If the checksums are different, then send the byte NAK and repeat the packet
    • If the checksums are the same, then send the byte ACK and get the next packet

In order to cancel the transfer, either a sender or a receiver sends a byte CAN . When one of the parties receives a byte CAN , we throw an error and terminate the connection.


To complete the transfer, the sender:


  1. Sends a byte EOT
  2. Waits for a byte NAK (If another byte is received - sender error)
  3. Sends the second byte EOT
  4. Waits for a byte ACK (If another byte is received - sender error)

To complete the transmission, the receiver (after receiving the first EOT ):


  1. Sends a byte NAK
  2. Waits for the second byte EOT (If another byte is received, the receiver cancels the transfer)
  3. Sends a byte ACK

XMODEM implementation


An incomplete implementation of the XMODEM protocol in the directory of the same name is provided. The task is to complete this very implementation. Append methods expect_byte , expect_byte_or_cancel , read_packet and write_packet in a file src/lib.rs . The implementation should use an internal state of the type Xmodem : packet and started . Before you begin, it is highly recommended that you read the code that is already there.


I advise you to start with the implementation of methods expect_byte and expect_byte_or_cancel . Then use all four helper methods (including read_byte and write_byte ) to implement read_packet and write_packet . To learn how these methods can be exploited, read the functions transmit and receive . They transmit / receive the full data stream using our protocol. Do not forget that the comments contain a lot of useful information. You can test your own implementation with cargo test . After everything in this part works, move on to the next part.


Не используйте дополнительные элементы из std.

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

Подсказки:
Моя эталонная реализация для {read, write}_packet содержит примерно 33 строки кода.

Документация по io::Read и io::Write может быть весьма полезна (как, тащемто, и любая другая документация для малознакомых штук).

Побольше используйте оператор ? .

Чтение кода тестов поможет понять, что оные от вас хотят.

Subphase D: ttywrite


In this subphase, we will write a utility for the command line ttywrite . It will allow us to send data to raspberries in raw raw form and using the XMODEM protocol. Here we just come in handy the library xmodem from the last part. All code is written in ttywrite/src/main.rs . A script is provided for testing needs test.sh . For this script to work, you will need the one mentioned somewhere in the beginning socat .


Что такое последовательное устройство?

Последовательным устройством является любое устройство, которое принимает сообщения по одному биту за раз. Такое называется последовательной передачей данных . С другой стороны есть ещё параллельная передача данных , где одновременно могут передаваться сразу несколько бит. С малинкой мы общаемся через UART, который является примером последовательного устройства.

Что такое TTY?
TTY — это телетайп (TeltTYpe writer). Это рудиментарный олдфажный термин, который изначально относился к компьютерным терминалам. Термин позже (в силу привычки) стал более общим и теперь означает любые устройства связи с последовательным интерфейсом. Именно по этой причине имя файлика из /dev/ , который закреплён за малинкой начинается с tty.

Command line interface


Procurement of code for ttywrite already analyzes and verifies the suitability of command line arguments for professional suitability. This uses the crate structopt , which internally uses clap . If you really use the advice to freely study the internals of the repository, you will notice that this thing is present as a dependency in Cargo.toml . In general, structopt sets code generation as its main goal. We stupidly describe the structure of what we want to get and declare the necessary fields, and structopt generates all the necessary code.


If you want to see which flags are generated there, you can call the application with the flag --help . It will not be superfluous to repeat that when using the same cargo run for passing flags, the application itself must be used -- as a separator. Like so: cargo run -- --help . Take a look at this help now. After look at the contents main.rs . Most of all, we are interested in structure Opt . Compare this with the help output for our application keys.


Что будет, ежели будут переданы неправильный флаги? [invalid]

Попробуйте передать какие либо недопустимые флаги с некорректными параметрами. Например установите -f как idk . Откуда structopt знает, что на подобное надобно обругать пользователя?

As you can see, there are many different variations for every taste. All of them correspond to various settings of serial devices. It is not yet necessary to know exactly what all these settings do.


Serial Communication


In main you can see the call to serial :: open . This is a function open from the serial open crate , which is obvious from the name. The function open returns a TTYPort , which allows us to read / write from / to the serial device (for it implements io::Read and io::Write ). Well, it allows you to set various settings for the serial port (through the implementation of the trait SerialDevice ).


Code writing


Implement the utility ttywrite . The implementation should at least set all the necessary parameters passed through the command line stored in the variable opt from main . If the input file name has not been transferred, then read from stdin . Well, or from the input file otherwise. Data should be redirected to the declared serial device. If the flag is set -r , then the data should be transmitted as is without any fraud. If this flag is not present, then it will be necessary to use the implementation xmodem from the previous subphase. After all this, it is necessary to print the number of carefully transmitted bytes (with a successful transfer).


To transmit via the XMODEM protocol, the code must use methods Xmodem::transfer or Xmodem::transmit_with_progress from obviously which library. I recommend it transmit_with_progress because it’s possible to record the calculation of the transmission speed. In the very wretched In its simplest form, it will look something like this:


fn progress_fn(progress: Progress) {
    println!("Progress: {:?}", progress);
}

Xmodem::transmit_with_progress(data, to, progress_fn)

You can check the minimum correctness of the implementation using the script test.sh from the directory ttywrite . When your implementation remotely resembles the correct one, you can see something like this:


Opening PTYs...
Running test 1/10.
wrote 333 bytes to input
...
Running test 10/10.
wrote 232 bytes to input
SUCCESS

Подсказки

Получить обёртку дескриптора stdin можно соответсвующей функцией io::stdin() .

Скорее всего io::copy() окажется весьма юзабельной.

Функция main() в конечной реализации вполне может уложиться в примерно 35 строчек кода.

Докуметация по TTYPort можно не закрывать во время написания кода.



Почему скрипт test.sh всегда устанавливает ключик -r ? [bad-test]

Предоставленый тестовый скриптик всегда устанавливает нашей проге ключик -r . Другими словами он не проверяет на вшивость использование протокола XMODEM. Почему это должно быть таким, каким оно есть? Почему на самом деле тестирование XMODEM не сильно нужно? Почему такое тестирование может быть сложнее?



UPD The older half is available for passing. There will be the most delicious from this part. About shell and bootloader.