diff --git a/README_ru.md b/README_ru.md new file mode 100644 index 0000000000..918d03e3d3 --- /dev/null +++ b/README_ru.md @@ -0,0 +1,118 @@ +# Real-Time Interrupt-driven Concurrency + +Конкурентный фреймворк для создания систем реального времени. + +Также известный как Real-Time For the Masses. + +[![crates.io](https://img.shields.io/crates/v/cortex-m-rtic)](https://crates.io/crates/cortex-m-rtic) +[![docs.rs](https://docs.rs/cortex-m-rtic/badge.svg)](https://docs.rs/cortex-m-rtic) +[![book](https://img.shields.io/badge/web-rtic.rs-red.svg?style=flat&label=book&colorB=d33847)](https://rtic.rs/) +[![rustc](https://img.shields.io/badge/rustc-1.36+-lightgray.svg)](https://github.com/rust-lang/rust/releases/tag/1.36.0) +[![matrix](https://img.shields.io/matrix/rtic:matrix.org)](https://matrix.to/#/#rtic:matrix.org) +[![Meeting notes](https://hackmd.io/badge.svg)](https://hackmd.io/@xmis9JvZT8Gvo9lOEKyZ4Q/SkBJKsjuH) + +## Возможности + +- **Задачи** как единица конкуренции [^1]. Задачи могут *запускаться от событий* + (срабатывать в ответ на асинхронные воздействия) или вызываться по запросу программы. + +- **Передача сообщений** между задачами. Если точнее, сообщения можно передавать + программным задачам в момент вызова. + +- **Очередь таймера** [^2]. Программные задачи можно планировать на запуск в определенный + момент в будущем. Эту возможность можно использовать для создания периодических задач. + +- Поддержка приоритета задач, и, как результат, **вытесняющей многозадачности**. + +- **Эффективное, избавленное от гонок данных, разделение ресурсов** благодаря легкому + разбиению на *основанные на приоритетах* критические секции [^1]. + +- **Выполнение без Deadlock**, гарантируемое на этапе компиляции. Данная гарантия строже, + чем та, что предоставляется [стандартный абтракцией `Mutex`][std-mutex]. + +[std-mutex]: https://doc.rust-lang.org/std/sync/struct.Mutex.html + +- **Минимальные расходы на диспетчеризацию**. Диспетчер задач иммет минимальную программную + базу; основная работа по диспетчеризации происходит аппаратно. + +- **Высокоэффективное использование памяти**: Все задачи разделяют единый стек вызовов и + отсутствует ресурсоемкая зависисмость от динамического аллокатора. + +- **Все Cortex-M устройства полностью поддерживаются**. + +- К такой модели задач можно применять так называемый анализ WCET (Наихудшего времени выполнения), + а также техники анализа диспетчеризации. (Хотя мы еще не разработали дружественный к Rust'у + инструментарий для этого.) + +## Требования + +- Rust 1.51.0+ + +- Приложения должны быть написаны в редакции 2018. + +## [Документация пользователя](https://rtic.rs) + +## [Справочник по API](https://rtic.rs/stable/api/) + +## Чат + +Присоединяйтесь к нам, чтобы говорить о RTIC [в Matrix-комнате][matrix-room]. + +Записи еженедельных собраний можно найти в [HackMD][hackmd] + +[matrix-room]: https://matrix.to/#/#rtic:matrix.org +[hackmd]: https://hackmd.io/@xmis9JvZT8Gvo9lOEKyZ4Q/SkBJKsjuH + +## Внести вклад + +Новые возможности и большие изменения следует проводить через процесс RFC в +[соответствующем RFC-репозитории][rfcs]. + +[rfcs]: https://github.com/rtic-rs/rfcs + +## Благодарности + +Этот крейт основан на [языке Real-Time For the Masses][rtfm-lang], созданном Embedded +Systems group в [Техническом Университете Luleå][ltu], под руководством +[Prof. Per Lindgren][per]. + +[rtfm-lang]: http://www.rtfm-lang.org/ +[ltu]: https://www.ltu.se/?l=en +[per]: https://www.ltu.se/staff/p/pln-1.11258?l=en + +## Ссылки + +[^1]: Eriksson, J., Häggström, F., Aittamaa, S., Kruglyak, A., & Lindgren, P. + (2013, June). Real-time for the masses, step 1: Programming API and static + priority SRP kernel primitives. In Industrial Embedded Systems (SIES), 2013 + 8th IEEE International Symposium on (pp. 110-113). IEEE. + +[^2]: Lindgren, P., Fresk, E., Lindner, M., Lindner, A., Pereira, D., & Pinho, + L. M. (2016). Abstract timers and their implementation onto the arm cortex-m + family of mcus. ACM SIGBED Review, 13(1), 48-53. + +## Лицензия + +Все исходные тексты (включая примеры кода) лицензированы под одной из лицензий: + +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) или + [https://www.apache.org/licenses/LICENSE-2.0][L1]) +- MIT license ([LICENSE-MIT](LICENSE-MIT) или + [https://opensource.org/licenses/MIT][L2]) + +[L1]: https://www.apache.org/licenses/LICENSE-2.0 +[L2]: https://opensource.org/licenses/MIT + +на ваш выбор. + +Текст книги лицензирован по условиям лицензий +Creative Commons CC-BY-SA v4.0 ([LICENSE-CC-BY-SA](LICENSE-CC-BY-SA) или +[https://creativecommons.org/licenses/by-sa/4.0/legalcode][L3]). + +[L3]: https://creativecommons.org/licenses/by-sa/4.0/legalcode + +### Условия участия + +Если вы не укажете этого отдельно, любой вклад, который вы предоставите в эту работу, +как указано в тексте лицензии Apache-2.0, будет лицензирован по условиям, +указанным выше, без каких-либо дополнительных условий. diff --git a/book/ru/book.toml b/book/ru/book.toml index 6c3a5e64b2..98c5bf3f72 100644 --- a/book/ru/book.toml +++ b/book/ru/book.toml @@ -1,5 +1,9 @@ [book] -authors = ["Jorge Aparicio"] +authors = ["Jorge Aparicio, Per Lindgren and The Real-Time Interrupt-driven Concurrency developers"] multilingual = false src = "src" title = "Real-Time Interrupt-driven Concurrency" + +[output.html] +git-repository-url = "https://github.com/rtic-rs/cortex-m-rtic" +git-repository-icon = "fa-github" diff --git a/book/ru/src/README_RU.md b/book/ru/src/README_RU.md deleted file mode 100644 index 4cc24e6885..0000000000 --- a/book/ru/src/README_RU.md +++ /dev/null @@ -1,94 +0,0 @@ -# Real-Time Interrupt-driven Concurrency - -Конкурентный фреймворк для создания систем реального времени. - -## Возможности - -- **Задачи** - единица конкуренции [^1]. Задачи могут *запускаться по событию* - (в ответ на асинхронный стимул) или вызываться программно по желанию. - -- **Передача сообщений** между задачами. А именно, сообщения можно передавать - программным задачам в момент вызова. - -- **Очередь таймера** [^2]. Программные задачи можно планировать на запуск в - определенный момент в будущем. Это свойство можно использовать, чтобы - реализовывать периодические задачи. - -- Поддержка приоритетов задач, и таким образом, **вытесняющей многозадачности**. - -- **Эффективное, свободное от гонок данных разделение памяти** через хорошо - разграниченные критические секции на *основе приоритетов* [^1]. - -- **Выполнение без взаимной блокировки задач**, гарантированное на этапе - компиляции. Это более сильная гарантия, чем предоставляемая - [стандартной абстракцией `Mutex`][std-mutex]. - -[std-mutex]: https://doc.rust-lang.org/std/sync/struct.Mutex.html - -- **Минимальные затраты на диспетчеризацию**. Диспетчер задач имеет - минимальный след; основная часть работы по диспетчеризации делается аппаратно. - -- **Высокоэффективное использование памяти**: Все задачи используют общий стек - вызовов и нет сильной зависимости от динамического распределителя памяти. - -- **Все устройства Cortex-M полностью поддерживаются**. - -- Эта модель задач поддается известному анализу методом WCET (наихудшего - времени исполнения) и техникам анализа диспетчеризации. (Хотя мы еще не - разработали для дружественных инструментов для этого). - -## Требования - -- Rust 1.31.0+ - -- Программы нужно писать используя 2018 edition. - -## [User documentation](https://japaric.github.io/cortex-m-rtic/book) - -## [API reference](https://japaric.github.io/cortex-m-rtic/api/rtic/index.html) - -## Благодарности - -Эта библиотека основана на [языке RTIC][rtic-lang], созданном Embedded -Systems group в [Техническом Университете Luleå][ltu], под рук. -[Prof. Per Lindgren][per]. - -[rtic-lang]: http://www.rtic-lang.org/ -[ltu]: https://www.ltu.se/?l=en -[per]: https://www.ltu.se/staff/p/pln-1.11258?l=en - -## Ссылки - -[^1]: Eriksson, J., Häggström, F., Aittamaa, S., Kruglyak, A., & Lindgren, P. - (2013, June). Real-time for the masses, step 1: Programming API and static - priority SRP kernel primitives. In Industrial Embedded Systems (SIES), 2013 - 8th IEEE International Symposium on (pp. 110-113). IEEE. - -[^2]: Lindgren, P., Fresk, E., Lindner, M., Lindner, A., Pereira, D., & Pinho, - L. M. (2016). Abstract timers and their implementation onto the arm cortex-m - family of mcus. ACM SIGBED Review, 13(1), 48-53. - -## Лицензия - -Все исходные тексты (включая примеры кода) лицензированы либо под: - -- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) или - [https://www.apache.org/licenses/LICENSE-2.0][L1]) -- MIT license ([LICENSE-MIT](LICENSE-MIT) or - [https://opensource.org/licenses/MIT][L2]) - -[L1]: https://www.apache.org/licenses/LICENSE-2.0 -[L2]: https://opensource.org/licenses/MIT - -на Ваше усмотрение. - -Текст книги лицензирован по условиям лицензий -Creative Commons CC-BY-SA v4.0 ([LICENSE-CC-BY-SA](LICENSE-CC-BY-SA) или -[https://creativecommons.org/licenses/by-sa/4.0/legalcode][L3]). - -[L3]: https://creativecommons.org/licenses/by-sa/4.0/legalcode - -### Contribution - -Если вы явно не заявляете иначе, любой взнос, преднамеренно представленный -для включения в эту работу, как определено в лицензии Apache-2.0, лицензируется, как указано выше, без каких-либо дополнительных условий. diff --git a/book/ru/src/RTIC.svg b/book/ru/src/RTIC.svg new file mode 100644 index 0000000000..1c65cba059 --- /dev/null +++ b/book/ru/src/RTIC.svg @@ -0,0 +1,109 @@ + +image/svg+xml diff --git a/book/ru/src/SUMMARY.md b/book/ru/src/SUMMARY.md index 7df745ebc8..a387c9f05c 100644 --- a/book/ru/src/SUMMARY.md +++ b/book/ru/src/SUMMARY.md @@ -1,16 +1,25 @@ # Summary [Введение](./preface.md) + - [RTIC в примерах](./by-example.md) - [Атрибут `app`](./by-example/app.md) - [Ресурсы](./by-example/resources.md) - - [Задачи](./by-example/tasks.md) + - [Программные задачи](./by-example/tasks.md) - [Очередь таймера](./by-example/timer-queue.md) - - [Одиночки](./by-example/singletons.md) - [Типы, Send и Sync](./by-example/types-send-sync.md) - [Создание нового проекта](./by-example/new.md) - [Советы и хитрости](./by-example/tips.md) +- [Инструкции по миграции](./migration.md) + - [v0.5.x на v0.6.x](./migration/migration_v5.md) + - [v0.4.x на v0.5.x](./migration/migration_v4.md) + - [RTFM на RTIC](./migration/migration_rtic.md) - [Под капотом](./internals.md) - - [Ceiling analysis](./internals/ceilings.md) - - [Диспетчер задач](./internals/tasks.md) + - [Настройка прерываний](./internals/interrupt-configuration.md) + - [Нереентерабельнось](./internals/non-reentrancy.md) + - [Контроль доступа](./internals/access.md) + - [Поздние ресурсы](./internals/late-resources.md) + - [Критические секции](./internals/critical-sections.md) + - [Анализ приоритетов](./internals/ceilings.md) + - [Программные задачи](./internals/tasks.md) - [Очередь таймера](./internals/timer-queue.md) diff --git a/book/ru/src/by-example.md b/book/ru/src/by-example.md index 0e0fde2fcd..027716f120 100644 --- a/book/ru/src/by-example.md +++ b/book/ru/src/by-example.md @@ -1,16 +1,23 @@ # RTIC в примерах -Эта часть книги представляет фреймворк Real-Time Interrupt-driven Concurrency (RTIC) -новым пользователям через примеры с растущей сложностью. +В этой части книги фреймворк Real-Time Interrupt-driven Concurrency (RTIC) представляется +новым пользователям путем прохода по примерам от простых к более сложным. -Все примеры в этой книге можно найти в [репозитории] проекта на GitHub, -и большинство примеров можно запустить на эмуляторе QEMU, поэтому никакого -специального оборудования не требуется их выполнять. +Все примеры в этой части книги можно найти в [репозитарии] проекта. +Большинство из них можно пройти, запустив их на эмуляторе QEMU без специального оборудования. -[репозитории]: https://github.com/japaric/cortex-m-rtic +[репозитарии]: https://github.com/rtic-rs/cortex-m-rtic -Чтобы запустить примеры на Вашем ноутбуке / ПК, Вам нужна программа -`qemu-system-arm`. Инструкции по настройке окружения для разработки -встраиваемых устройств, в том числе QEMU, Вы можете найти в [the embedded Rust book]. +Для запуска примеров на вашем ПК, вам понадобится программа `qemu-system-arm`. +В [the embedded Rust book] есть инструкции по настройке среды для эмбеддед разработке, +в том числе QEMU. [the embedded Rust book]: https://rust-embedded.github.io/book/intro/install.html + +## Примеры из реальной жизни + +Ниже представлены примеры использования RTIC (RTFM) в реальных проектах. + +### RTFM V0.4.2 + +- [etrombly/sandbox](https://github.com/etrombly/sandbox/tree/41d423bcdd0d8e42fd46b79771400a8ca349af55). Аппаратный дзэн-сад, рисующий картинки на песке. Картинки передаются по последовательному порту с помощью G-кода. diff --git a/book/ru/src/by-example/app.md b/book/ru/src/by-example/app.md index 04dd5b24b3..628819adcd 100644 --- a/book/ru/src/by-example/app.md +++ b/book/ru/src/by-example/app.md @@ -1,77 +1,84 @@ -# The `app` attribute +# Атрибут `app` -Это наименьшая возможная программа на RTIC: +Это простейшая из возможных программ на RTIC: ``` rust {{#include ../../../../examples/smallest.rs}} ``` Все программы на RTIC используют атрибут [`app`] (`#[app(..)]`). Этот атрибут -нужно применять к `const`-элементам, содержащим элементы. Атрибут `app` имеет -обязательный аргумент `device`, в качестве значения которому передается *путь*. -Этот путь должен указывать на библиотеку *устройства*, сгенерированную с помощью -[`svd2rust`] **v0.14.x**. Атрибут `app` развернется в удобную точку входа, -поэтому нет необходимости использовать атрибут [`cortex_m_rt::entry`]. +должен применяться к элементу `mod`. Атрибут `app` имеет обязательный аргумент `device`, +который принимает *путь* как значение. Это должен быть полный путь, указывающий на +*крейт доступа к периферии* (PAC), сгенерированный с помощью [`svd2rust`] версии **v0.14.x** +или новее. Более подробно в разделе [Создание нового проекта](./new.md). + +Атрибут `app` будет раскрыт в подходящую точку входа программы, поэтому +атрибут [`cortex_m_rt::entry`] не нужен. [`app`]: ../../../api/cortex_m_rtic_macros/attr.app.html [`svd2rust`]: https://crates.io/crates/svd2rust [`cortex_m_rt::entry`]: ../../../api/cortex_m_rt_macros/attr.entry.html -> **ОТСТУПЛЕНИЕ**: Некоторые из вас удивятся, почему мы используем ключевое слово `const` как -> модуль, а не правильное `mod`. Причина в том, что использование атрибутов на -> модулях требует feature gate, который требует ночную сборку. Чтобы заставить -> RTIC работать на стабильной сборке, мы используем вместо него слово `const`. -> Когда большая часть макросов 1.2 стабилизируются, мы прейдем от `const` к `mod` и в конце концов в атрибуту уровне приложения (`#![app]`). - ## `init` -Внутри псевдо-модуля атрибут `app` ожидает найти функцию инициализации, обозначенную -атрибутом `init`. Эта функция должна иметь сигнатуру `[unsafe] fn()`. +Внутри модуля `app` атрибут ожидает найти функцию инициализации, помеченную +атрибутом `init`. Эта функция должна иметь сигнатуру +`fn(init::Context) [-> init::LateResources]` (возвращаемый тип нужен не всегда). -Эта функция инициализации будет первой частью запускаемого приложения. -Функция `init` запустится *с отключенными прерываниями* и будет иметь эксклюзивный -доступ к периферии Cortex-M и специфичной для устройства периферии через переменные -`core` and `device`, которые внедряются в область видимости `init` атрибутом `app`. -Не вся периферия Cortex-M доступна в `core`, потому что рантайм RTIC принимает владение -частью из неё -- более подробно см. структуру [`rtic::Peripherals`]. +Эта функция инициализации будет первой частью программы, выполняемой при запуске. +Функция `init` будет запущена *с отключенными прерываниями* и будет иметь эксклюзивный доступ +к Cortex-M, в котором токен `bare_metal::CriticalSection` доступен как `cs`. +Опционально, устройство-специфичные периферия доступна через поля `core` и `device` структуры +`init::Context`. -Переменные `static mut`, определённые в начале `init` будут преобразованы -в ссылки `&'static mut` с безопасным доступом. +`static mut` переменные, определенные в начале `init` будут преобразованы в +`&'static mut` ссылки, безопасные для доступа. Обратите внимание, данная возможность может +быть удалена в следующем релизе, см. `task_local` ресурсы. [`rtic::Peripherals`]: ../../api/rtic/struct.Peripherals.html -Пример ниже показывает типы переменных `core` и `device` и -демонстрирует безопасный доступ к переменной `static mut`. +Пример ниже показывает типы полей `core`, `device` и `cs`, и демонстрирует +безопасный доступ к `static mut` переменной. Поле `device` доступно только +когда аргумент `peripherals` установлен в `true` (по умолчанию). +В редких случаях, когда вы захотите создать приложение с минимальным потреблением ресурсов, +можно явно установить `peripherals` в `false`. ``` rust {{#include ../../../../examples/init.rs}} ``` -Запуск примера напечатает `init` в консоли и завершит процесс QEMU. +Запуск примера напечатате `init` в консоли, а затем завершит процесс QEMU. ``` console $ cargo run --example init -{{#include ../../../../ci/expected/init.run}}``` +{{#include ../../../../ci/expected/init.run}} +``` ## `idle` -Функция, помеченная атрибутом `idle` может присутствовать в псевдо-модуле -опционально. Эта функция используется как специальная *задача ожидания* и должна иметь -сигнатуру `[unsafe] fn() - > !`. +Функцию, помеченную атрибутом `idle` может опционально добавить в модуль. +Эта функция используется как специальная *задача ожидания* и должна иметь сигнатуру +`fn(idle::Context) - > !`. -Когда она присутствует, рантайм запустит задачу `idle` после `init`. В отличие от -`init`, `idle` запустится *с включенными прерываниями* и не может завершиться, -поэтому будет работать бесконечно. +Если она присутствует, задача `idle` будет запущена после `init`. В отличие от +`init`, `idle` будет запущена *с включенными прерываниями* и она не может вернуть результат, +а значит должна работать вечно. -Когда функция `idle` не определена, рантайм устанавливает бит [SLEEPONEXIT], после чего -отправляет микроконтроллер в состояние сна после выполнения `init`. +Если функция `idle` не определена, среда вполнения устанавливает бит [SLEEPONEXIT], а затем +отправляет микроконтроллер в сон после запуска `init`. [SLEEPONEXIT]: https://developer.arm.com/docs/100737/0100/power-management/sleep-mode/sleep-on-exit-bit -Как и в `init`, переменные `static mut`будут преобразованы в ссылки `&'static mut` -с безопасным доступом. +Как и в `init`, `static mut` переменные будут трансформированы в `&'static mut` ссылки, +безопасные для доступа. Обратите внимание, данная возможность может +быть удалена в следующем релизе, см. `task_local` ресурсы. -В примере ниже показан запуск `idle` после `init`. +Пример ниже показывает, что `idle` запускается после `init`. + +**Примечание:** Цикл `loop {}` в функци ожидания не может быть пустым, так как это сломает +микроконтроллер, из-за того, что LLVM компилирует пустые циклы в инструкцию `UDF` в release mode. +Чтобы избежать неопределенного поведения, цикл должен включать "side-effect" +путем вставки ассемблерной инструкции (например, `WFI`) или ключевого слова `continue`. ``` rust {{#include ../../../../examples/idle.rs}} @@ -79,23 +86,75 @@ $ cargo run --example init ``` console $ cargo run --example idle -{{#include ../../../../ci/expected/idle.run}}``` +{{#include ../../../../ci/expected/idle.run}} +``` -## `interrupt` / `exception` +## Аппаратные задачи -Как Вы бы сделали с помощью библиотеки `cortex-m-rt`, Вы можете использовать атрибуты -`interrupt` и `exception` внутри псевдо-модуля `app`, чтобы определить обработчики -прерываний и исключений. В RTIC, мы называем обработчики прерываний и исключений -*аппаратными* задачами. +Чтобы объявить обработчик прерывания, фреймворк предоставляет атрибут `#[task]`, +который можно применять к функциям. Этот атрибут берет аргумент `binds`, чье значение - +это имя прерывания, которому будет назначен обработчик; +функция, декорированная этим атрибутом становится обработчиком прерывания. +В фреймворке такие типы задач именуются *аппаратными*, потому что они начинают +выполняться в ответ на аппаратное событие. + +Пример ниже демонстрирует использование атрибута `#[task]`, чтобы объявить +обработчик прерывания. Как и в случае с `#[init]` и `#[idle]` локальные `static +mut` переменные безопасны для использования с аппаратной задачей. ``` rust -{{#include ../../../../examples/interrupt.rs}} +{{#include ../../../../examples/hardware.rs}} ``` ``` console -$ cargo run --example interrupt -{{#include ../../../../ci/expected/interrupt.run}}``` +$ cargo run --example hardware +{{#include ../../../../ci/expected/hardware.run}} +``` -До сих пор программы RTIC, которые мы видели не отличались от программ, которые -можно написать, используя только библиотеку `cortex-m-rt`. В следующем разделе -мы начнем знакомиться с функционалом, присущим только RTIC. +До сих пор все программы на RTIC, которые мы видели, не отличались от программ, +которые можно написать, используя лишь крейт `cortex-m-rt`. С этого момента мы +начинаем представлять возможности, уникальные для RTIC. + +## Приоритеты + +Статический приоритет каждого обработчика можно оределить в атрибуте `task`, используя +аргумент `priority`. Задачи могут иметь приоритет в диапазоне `1..=(1 << NVIC_PRIO_BITS)`, +где `NVIC_PRIO_BITS` - это константа, определенная в крейте `устройства`. +Когда аргумент `priority` не указан, предполагается, что приоритет равен `1`. +Задача `idle` имеет ненастраиваемый приоритет `0`, наименьший из возможных. + +> Более высокое значение означает более высокий приоритет в RTIC, что противоположно тому, +> что указано в периферии NVIC Cortex-M. +> Точнее, это значит, что число `10` обозначает приоритет **выше**, чем число `9`. + +Когда несколько задач готовы к запуску, задача с самым большим статическим +приоритетом будет запущена первой. Приоритезацию задач можно рассматривать по +такому сценарию: сигнал прерывания приходит во время выполнения задачи с низким приоритетом; +сигнал переключает задачу с высоким приоритетом в режим ожидания. +Разница в приоритетах приводи к тому, что задача с высоким приоритетом вытесняет задачу с низким: +выполнение задачи с низким приоритетом замораживается и задача с высоким приоритетом выполняется, +пока не будет завершена. Как только задача с высоким приоритетом будет остановлена, +продолжится выполнение задачи с низким приоритетом. + +Следующий пример демонстрирует диспетчеризацию на основе приоритетов задач. + +``` rust +{{#include ../../../../examples/preempt.rs}} +``` + +``` console +$ cargo run --example preempt +{{#include ../../../../ci/expected/preempt.run}} +``` + +Заметьте, что задача `gpiob` *не* вытесняет задачу `gpioc`, потому что ее приоритет +*такой же*, как и у `gpioc`. Однако, как только `gpioc` возвращает результат, +выполненяется задача `gpiob`, как более приоритетная по сравнению с `gpioa`. +Выполнение `gpioa` возобновляется только после выхода из `gpiob`. + +Еще одно замечание по поводу приоритетов: выбор приоритета большего, чем поддерживает устройство +(а именно `1 << NVIC_PRIO_BITS`) приведет к ошибке компиляции. +Из-за ограничений языка, сообщение об ошибке далеко от понимания: +вам скажут что-то похожее на "evaluation of constant value failed", а указатель на ошибку +*не* покажет на проблемное значение прерывания -- +мы извиняемся за это! diff --git a/book/ru/src/by-example/new.md b/book/ru/src/by-example/new.md index cba84c1615..fcf523701a 100644 --- a/book/ru/src/by-example/new.md +++ b/book/ru/src/by-example/new.md @@ -16,19 +16,19 @@ $ cargo generate \ $ # следуйте остальным инструкциям ``` -2. Добавьте крейт устройства, сгенерированный с помощью [`svd2rust`] **v0.14.x**, -или библиотеку отладочной платы, у которой в зависимостях одно из устройств. +2. Добавьте крейт доступа к периферии (PAC), сгенерированный с помощью[`svd2rust`] + **v0.14.x**, или крейт отладочной платы, у которой в зависимостях один из таких PAC'ов. Убедитесь, что опция `rt` крейта включена. [`svd2rust`]: https://crates.io/crates/svd2rust -В этом примере я покажу использование крейта устройства [`lm3s6965`]. +В этом примере я буду использовать крейт устройства [`lm3s6965`]. Эта библиотека не имеет Cargo-опции `rt`; эта опция всегда включена. [`lm3s6965`]: https://crates.io/crates/lm3s6965 Этот крейт устройства предоставляет линковочный скрипт с макетом памяти -целевого устройства, поэтому `memory.x` и `build.rs` не нужно удалять. +целевого устройства, поэтому `memory.x` и `build.rs` нужно удалить. ``` console $ cargo add lm3s6965 --vers 0.1.3 @@ -36,24 +36,40 @@ $ cargo add lm3s6965 --vers 0.1.3 $ rm memory.x build.rs ``` -3. Добавьте библиотеку `cortex-m-rtic` как зависимость, и если необходимо, -включите опцию `timer-queue`. +3. Добавьте крейт `cortex-m-rtic` как зависимость. ``` console -$ cargo add cortex-m-rtic --allow-prerelease --upgrade=none +$ cargo add cortex-m-rtic --allow-prerelease ``` -4. Напишите программу RTIC. +4. Напишите свою RTIC программу. -Здесь я буду использовать пример `init` из библиотеки `cortex-m-rtic`. +Здесь я буду использовать пример `init` из крейта `cortex-m-rtic`. + +Примеры находтся в папке `examples`, а содержание `init.rs` показано здесь: ``` console -$ curl \ - -L https://github.com/japaric/cortex-m-rtic/raw/v0.4.0-beta.1/examples/init.rs \ - > src/main.rs +{{#include ../../../../examples/init.rs}} ``` -Этот пример зависит от библиотеки `panic-semihosting`: +Пример `init` использует устройство `lm3s6965`. Не забудьте настроить аргумент `device` +в атрибуте макроса app так, чтобы он соответствовал пути к PAC-крейту, если он отличается, +а также добавить перифериб и другие аргументы если необходимо. +Несмотря на то, что в программе могут использоваться псевдонимы типов, +здесь необходимо указать полный путь (из корня крейта). Для многих устройств, +есть общий подход в крейтах реализации HAL (с псевдонимом `hal`) и крейтах поддержки +отладочных плат реекспортиорвать PAC как `pac`, что приводит нас к образцу, аналогичному +приведенному ниже: + +```rust +use abcd123_hal as hal; +//... + +#[rtic::app(device = crate::hal::pac, peripherals = true, monotonic = rtic::cyccnt::CYCCNT)] +mod app { /*...*/ } +``` + +Пример `init` также зависит от крейта `panic-semihosting`: ``` console $ cargo add panic-semihosting @@ -64,4 +80,5 @@ $ cargo add panic-semihosting ``` console $ # ПРИМЕЧАНИЕ: Я раскомментировал опцию `runner` в `.cargo/config` $ cargo run -{{#include ../../../../ci/expected/init.run}}``` +{{#include ../../../../ci/expected/init.run}} +``` diff --git a/book/ru/src/by-example/resources.md b/book/ru/src/by-example/resources.md index b53ef40ee6..70f798d25e 100644 --- a/book/ru/src/by-example/resources.md +++ b/book/ru/src/by-example/resources.md @@ -1,22 +1,27 @@ -## Ресурсы +# Ресурсы -Одно из ограничений атрибутов, предоставляемых библиотекой `cortex-m-rt` является -то, что совместное использование данных (или периферии) между прерываниями, -или прерыванием и функцией `init`, требуют `cortex_m::interrupt::Mutex`, который -*всегда* требует отключения *всех* прерываний для доступа к данным. Отключение всех -прерываний не всегда необходимо для безопасности памяти, но компилятор не имеет -достаточно информации, чтобы оптимизировать доступ к разделяемым данным. +Фреймворк предоставляет абстракцию для разделения данных между любыми контекстами, +с которыми мы встречались в предыдущей главе (задачами-обработчиками, `init` и `idle`): ресурсы. -Атрибут `app` имеет полную картину приложения, поэтому может оптимизировать доступ к -`static`-переменным. В RTIC мы обращаемся к `static`-переменным, объявленным внутри -псевдо-модуля `app` как к *ресурсам*. Чтобы получить доступ к ресурсу, контекст -(`init`, `idle`, `interrupt` или `exception`) должен сначала определить -аргумент `resources` в соответствующем атрибуте. +Ресурсы - это данные, видимые только функциями, определенными внутри модуля `#[app]`. +Фреймворк дает пользователю полный контроль за тем, какой контекст может +получить доступ к какому ресурсу. -В примере ниже два обработчика прерываний имеют доступ к одному и тому же ресурсу. -Никакого `Mutex` в этом случае не требуется, потому что оба обработчика запускаются -с одним приоритетом и никакого вытеснения быть не может. -К ресурсу `SHARED` можно получить доступ только из этих двух прерываний. +Все ресурсы определены в одной структуре внутри модуля `#[app]`. +Каждое поле структуры соответствует отдельному ресурсу. +`struct`-ура должна быть аннотирована следующим атрибутом: `#[resources]`. + +Ресурсам могут быть опционально даны начальные значения с помощью атрибута `#[init]`. +Ресурсы, которым не передано начально значение, называются +*поздними* ресурсами, более детально они описаны в одном из разделов на этой странице. + +Каждый контекс (задача-обработчик, `init` или `idle`) должен указать ресурсы, к которым +он намерен обращаться, в соответсятвующем ему атрибуте с метаданными, используя +аргумент `resources`. Этот аргумент принимает список имен ресурсов в качестве значения. +Перечисленные ресурсы становятся доступны в контексте через поле `resources` структуры `Context`. + +Пример программы, показанной ниже содержит два обработчика прерывания, которые разделяют +доступ к ресурсу под названием `shared`. ``` rust {{#include ../../../../examples/resource.rs}} @@ -27,68 +32,69 @@ $ cargo run --example resource {{#include ../../../../ci/expected/resource.run}} ``` -## Приоритеты +Заметьте, что к ресурсу `shared` нельзя получить доступ из `idle`. Попытка сделать это +приведет к ошибке компиляции. -Приоритет каждого прерывания можно определить в атрибутах `interrupt` и `exception`. -Невозможно установить приоритет любым другим способом, потому что рантайм -забирает владение прерыванием `NVIC`; также невозможно изменить приоритет -обработчика / задачи в рантайме. Благодаря этому ограничению у фреймворка -есть знание о *статических* приоритетах всех обработчиков прерываний и исключений. +## `lock` -Прерывания и исключения могут иметь приоритеты в интервале `1..=(1 << NVIC_PRIO_BITS)`, -где `NVIC_PRIO_BITS` - константа, определённая в библиотеке `device`. -Задача `idle` имеет приоритет `0`, наименьший. +Критические секции необходимы для разделения изменяемых данных таким образом, +чтобы избежать гонок данных. -Ресурсы, совместно используемые обработчиками, работающими на разных приоритетах, -требуют критических секций для безопасности памяти. Фреймворк проверяет, что -критические секции используются, но *только где необходимы*: например, -критические секции не нужны для обработчика с наивысшим приоритетом, имеющим -доступ к ресурсу. +Поле `resources`, передаваемого `Context` реализует трейт [`Mutex`] для каждого разделяемого +ресурса, доступного задаче. -API критической секции, предоставляемое фреймворком RTIC (см. [`Mutex`]), -основано на динамических приоритетах вместо отключения прерываний. Из этого следует, -что критические секции не будут допускать *запуск некоторых* обработчиков, -включая все соперничающие за ресурс, но будут позволять запуск обработчиков с -большим приоритетом не соперничащих за ресурс. +Единственный метод этого трейта, [`lock`], запускает свой аргумент-замыкание в критической секции. [`Mutex`]: ../../../api/rtic/trait.Mutex.html - -В примере ниже у нас есть 3 обработчика прерываний с приоритетами от одного -до трех. Два обработчика с низким приоритетом соперничают за ресурс `SHARED`. -Обработчик с низшим приоритетом должен заблокировать ([`lock`]) ресурс -`SHARED`, чтобы получить доступ к его данным, в то время как обработчик со -средним приоритетом может напрямую получать доступ к его данным. Обработчик -с наивысшим приоритетом может свободно вытеснять критическую секцию, -созданную обработчиком с низшим приоритетом. - [`lock`]: ../../../api/rtic/trait.Mutex.html#method.lock +Критическая секция, создаваемая интерфейсом `lock` основана на динамических приоритетах: +она временно повышает динамический приоритет контекста до *максимального* приоритета, +что не дает другим задачам возможности вытеснить критическую секцию. +Этот протокол синхронизации известен как [Протокол немедленного максимального приоритета +(ICPP)][icpp], и компилируется диспетчером RTIC с [Политикой ресурсов стека(SRP)][srp]. + +[icpp]: https://en.wikipedia.org/wiki/Priority_ceiling_protocol +[srp]: https://en.wikipedia.org/wiki/Stack_Resource_Policy + +В примере ниже у нас есть три обработчика прерываний с приоритетами от одного до трех. +Два из обработчиков с более низким приоритетом соревнуются за ресурс `shared`, +поэтому должны блокировать доступа к данным ресурса. +Обработчик с наивысшим приоритетом, который не имеет доступа к ресурсу `shared`, +может свободно вытеснять критическую секцию, созданную обработчиком с низким приоритетом. + ``` rust {{#include ../../../../examples/lock.rs}} ``` ``` console $ cargo run --example lock -{{#include ../../../../ci/expected/lock.run}}``` +{{#include ../../../../ci/expected/lock.run}} +``` + +## Множественное блокировка + +Это расширение к `lock`, чтобы уменьшить количесво отступов, блокируемые ресурсы можно объединять в кортежи. +Следующий пример это демонстрирует: + +``` rust +{{#include ../../../../examples/multilock.rs}} +``` ## Поздние ресурсы -В отличие от обычных `static`-переменных, к которым должно быть присвоено -начальное значение, ресурсы можно инициализировать в рантайме. -Мы называем ресурсы, инициализируемые в рантайме *поздними*. Поздние ресурсы -полезны для *переноса* (как при передаче владения) периферии из `init` в -обработчики прерываний и исключений. +Поздние ресурсы - такие ресурсы, которым не передано начальное значение во время компиляции +с помощью атрибута `#[init]`, но которые вместо этого инициализируются во время выполнения +с помощью значений из структуры `init::LateResources`, возвращаемой функцией `init`. -Поздние ресурсы определяются как обычные ресурсы, но им присваивается начальное -значение `()` (the unit value). `init` должен вернуть начальные значения для -всех поздних ресурсов, упакованные в структуру типа `init::LateResources`. +Поздние ресурсы полезны, например, для *move* (передача владения) периферии, +инициализированной в `init`, в задачи. -В примере ниже использованы поздние ресурсы, чтобы установить неблокированный, -односторонний канал между обработчиком прерывания `UART0` и функцией `idle`. -Очередь типа один производитель-один потребитель [`Queue`] использована как канал. -Очередь разделена на элементы потребителя и поизводителя в `init` и каждый элемент -расположен в отдельном ресурсе; `UART0` владеет ресурсом произодителя, а `idle` -владеет ресурсом потребителя. +Пример ниже использует поздние ресурсы, чтобы установить неблокируемый односторонний канал +между обработчиком прерывания `UART0` и задачей `idle`. Для канала использована очередь типа +один производитель-один потребитель [`Queue`]. Структура очереди разделяется на потребителя +и производителя в `init`, а затем каждая из частей располагается в отдельном ресурсу; +`UART0` владеет ресурсом производителя, а `idle` владеет ресурсом потребителя. [`Queue`]: ../../../api/heapless/spsc/struct.Queue.html @@ -98,25 +104,46 @@ $ cargo run --example lock ``` console $ cargo run --example late -{{#include ../../../../ci/expected/late.run}}``` +{{#include ../../../../ci/expected/late.run}} +``` -## `static`-ресурсы +## Только разделяемый доступ -Переменные типа `static` также можно использовать в качестве ресурсов. Задачи -могут получать только (разделяемые) `&` ссылки на ресурсы, но блокировки не -нужны для доступа к данным. Вы можете думать о `static`-ресурсах как о простых -`static`-переменных, которые можно инициализировать в рантайме и иметь лучшие -правила видимости: Вы можете контролировать, какие задачи получают доступ к -переменной, чтобы переменная не была видна всем фунциям в область видимости, -где она была объявлена. +По-умолчанию фреймворк предполагает, что все задачи требуют эксклюзивный доступ (`&mut-`) к ресурсам, +но возможно указать, что задаче достаточен разделяемый доступ (`&-`) к ресурсы с помощью синтакисиса +`&resource_name` в списке `resources`. -В примере ниже ключ загружен (или создан) в рантайме, а затем использован в двух -задачах, запущенных на разных приоритетах. +Преимущество указания разделяемого досупа (`&-`) к ресурсу в том, что для доступа к ресурсу +не нужна блокировка, даже если за ресурс соревнуются несколько задач, запускаемые с +разными приоритетами. Недостаток в том, что задача получает только разделяемую ссылку (`&-`) +на ресурс, и ограничена операциями, возможными с ней, но там, где разделяемой ссылки достаточно, +такой подход уменьшает количесво требуемых блокировок. +В дополнение к простым неизменяемым данным, такой разделяемый доступ может быть полезен для +ресурсов, безопасно реализующих внутреннюю мутабельность с самоблокировкой или атомарными операциями. + +Заметьте, что в этом релизе RTIC невозможно запросить и эксклюзивный доступ (`&mut-`) +и разделяемый (`&-`) для *одного и того же* ресурса из различных задач. +Попытка это сделать приведет к ошибке компиляции. + +В примере ниже ключ (например криптографический ключ) загружается (или создается) во время выполнения, +а затем используется двумя задачами, запускаемымы с различным приоритетом без каких-либо блокировок. ``` rust -{{#include ../../../../examples/static.rs}} +{{#include ../../../../examples/only-shared-access.rs}} ``` ``` console -$ cargo run --example static -{{#include ../../../../ci/expected/static.run}}``` +$ cargo run --example only-shared-access +{{#include ../../../../ci/expected/only-shared-access.run}} +``` + +## Неблокируемый доступ к изменяемым ресурсам + +Есть две других возможности доступа к ресурсам + +* `#[lock_free]`: могут быть несколько задач с одинаковым приоритетом, + получающие доступ к ресурсу без критических секций. Так как задачи с + одинаковым приоритетом никогда не могут вытеснить друг друга, это безопасно. +* `#[task_local]`: в этом случае должна быть только одна задача, использующая + этот ресурс, так же как локальный `static mut` ресурс задачи, но (опционально) устанавливаемая с в init. + diff --git a/book/ru/src/by-example/singletons.md b/book/ru/src/by-example/singletons.md deleted file mode 100644 index d6d60ef8c3..0000000000 --- a/book/ru/src/by-example/singletons.md +++ /dev/null @@ -1,26 +0,0 @@ -# Одиночки - -Атрибут `app` знает о библиотеке [`owned-singleton`] и её атрибуте [`Singleton`]. -Когда этот атрибут применяется к одному из ресурсов, рантайм производит для Вас -`unsafe` инициализацию одиночки, проверяя, что только один экземпляр одиночки -когда-либо создан. - -[`owned-singleton`]: ../../api/owned_singleton/index.html -[`Singleton`]: ../../api/owned_singleton_macros/attr.Singleton.html - -Заметьте, что когда Вы используете атрибут `Singleton`, Вым нужно иметь -`owned_singleton` в зависимостях. - -В примере ниже атрибутом `Singleton` аннотирован массив памяти, -а экземпляр одиночки использован как фиксированный по размеру пул памяти -с помощью одной из абстракций [`alloc-singleton`]. - -[`alloc-singleton`]: https://crates.io/crates/alloc-singleton - -``` rust -{{#include ../../../../examples/singleton.rs}} -``` - -``` console -$ cargo run --example singleton -{{#include ../../../../ci/expected/singleton.run}}``` diff --git a/book/ru/src/by-example/tasks.md b/book/ru/src/by-example/tasks.md index 3782804397..3c99d00efc 100644 --- a/book/ru/src/by-example/tasks.md +++ b/book/ru/src/by-example/tasks.md @@ -1,22 +1,20 @@ # Программные задачи -RTIC обрабатывает прерывания и исключения как *аппаратные* задачи. Аппаратные -задачи могут вызываться устройством в ответ на события, такие как нажатие кнопки. -RTIC также поддерживает *программные* задачи, порождаемые программой из любого -контекста выполнения. +В дополнение к аппаратным задачам, вызываемым в ответ на аппаратные события, +RTIC также поддерживает *программные* задачи, которые могут порождаться +приложением из любого контекста выполнения. -Программным задачам также можно назначать приоритет и диспетчеризовать из -обработчиков прерываний. RTIC требует определения свободных прерываний в блоке -`extern`, когда используются программные задачи; эти свободные прерывания будут использованы, чтобы диспетчеризовать программные задачи. Преимущество программных -задач перед аппаратными в том, что на один обработчик прерывания можно назначить -множество задач. +Программным задачам можно также назначать приоритет и, под капотом, они +диспетчеризуются обработчиками прерываний. RTIC требует, чтобы свободные +прерывания, были указаны в аргументе `dispatchers` модуля `app`, если используются +программные задачи; часть из этих свободных прерываний будут использованы для +управления программными задачами. Преимущество программных задач над аппаратными +в том, что множество задач можно назначить на один обработчик прерывания. -Программные задачи определяются заданием функциям атрибута `task`. Чтобы было -возможно вызывать программные задачи, имя задачи нужно передать в аргументе -`spawn` контекста атрибута (`init`, `idle`, `interrupt`, etc.). +Программные задачи также определяются атрибутом `task`, но аргумент `binds` опускается. -В примере ниже продемонстрированы три программных задачи, запускаемые на 2-х -разных приоритетах. Трем задачам назначены 2 обработчика прерываний. +Пример ниже демонстрирует три программные задачи, запускаемых 2-х разных приоритетах. +Три программные задачи привязаны к 2-м обработчикам прерываний. ``` rust {{#include ../../../../examples/task.rs}} @@ -24,15 +22,16 @@ RTIC также поддерживает *программные* задачи, ``` console $ cargo run --example task -{{#include ../../../../ci/expected/task.run}}``` +{{#include ../../../../ci/expected/task.run}} +``` ## Передача сообщений -Другое преимущество программных задач - возможность передавать сообщения задачам -во время их вызова. Тип полезной нагрузки сообщения должен быть определен в -сигнатуре обработчика задачи. +Другое преимущество программной задачи в том, что задачам можно передать сообщения +в момент их запуска. Тип передаваемого сообщения должен быть определен в сигнатуре +задачи-обработчика. -Пример ниже демонстрирует три задачи, две из которых ожидают сообщения. +Пример ниже демонстрирует три задачи, две из которых ожидают сообщение. ``` rust {{#include ../../../../examples/message.rs}} @@ -40,19 +39,23 @@ $ cargo run --example task ``` console $ cargo run --example message -{{#include ../../../../ci/expected/message.run}}``` +{{#include ../../../../ci/expected/message.run}} +``` -## Ёмкость +## Вместимость -Диспетчеры задач *не* используют динамическое выделение памяти. Память -необходимая для размещения сообщений, резервируется статически. Фреймворк -зарезервирует достаточно памяти для каждого контекста, чтобы можно было вызвать -каждую задачу как минимум единожды. Это разумно по умолчанию, но -"внутреннюю" ёмкость каждой задачи можно контролировать используя аргумент -`capacity` атрибута `task`. +RTIC *не* производит никакого рода аллокаций памяти в куче. +Память, необходимая для размещения сообщения резервируется статически. +По-умолчанию фреймворк минимизирует выделение памяти программой таким образом, +что каждая задача имеет "вместимость" для сообщения равную 1: +это значит, что не более одного сообщения можно передать задаче перед тем, как +у нее появится возможность к запуску. Это значение по-умолчанию можно +изменить для каждой задачи, используя аргумент `capacity`. +Этот аргумент принимает положительное целое, которое определяет как много +сообщений буфер сообщений задачи может хранить. -В примере ниже установлена ёмкость программной задачи `foo` на 4. Если ёмкость -не определена, тогда второй вызов `spawn.foo` в `UART0` вызовет ошибку. +Пример ниже устанавливает вместимость программной задачи `foo` равной 4. +Если вместимость не установить, второй вызов `spawn.foo` в `UART0` приведет к ошибке (панике). ``` rust {{#include ../../../../examples/capacity.rs}} @@ -60,4 +63,54 @@ $ cargo run --example message ``` console $ cargo run --example capacity -{{#include ../../../../ci/expected/capacity.run}}``` +{{#include ../../../../ci/expected/capacity.run}} +``` + +## Обработка ошибок + +Интерфейс `spawn` возвращает вариант `Err`, если для размещения сообщения нет места. +В большинстве сценариев возникающие ошибки обрабатываются одним из двух способов: + +- Паника, с помощью `unwrap`, `expect`, и т.п. Этот метод используется, чтобы обнаружить + ошибку программиста (например bug) выбора вместительности, которая оказалась недостаточна. + Когда эта паника встречается во время тестирования, выбирается большая вместительность, + и перекомпиляция программы может решить проблему, но иногда достаточно окунуться глубже + и провести анализ времени выполнения программы, чтобы выяснить, может ли платформа + обрабатывать пиковые нагрузки, или процессор необходимо заменить на более быстрый. + +- Игнорирование результата. В программах реального времени, как и в обычных, может быть + нормальным иногда терять данные, или не получать ответ на некоторые события в пиковых ситуациях. + В таких сценариях может быть допустимо игнорирование ошибки вызова `spawn`. + +Следует отметить, что повторная попытка вызова `spawn` обычно неверный подход, поскольку +такая операция на практике вероятно никогда не завершится успешно. +Так как у нас есть только переключения контекста на задачи с *более высоким* приоритетом, +повторение вызова `spawn` на задаче с низким приоритом никогда не позволит планировщику +вызвать задачу, что значит, что буфер никогда не будет очищен. Такая ситуация отражена в +следующем наброске: + +``` rust +#[rtic::app(..)] +mod app { + #[init(spawn = [foo, bar])] + fn init(cx: init::Context) { + cx.spawn.foo().unwrap(); + cx.spawn.bar().unwrap(); + } + + #[task(priority = 2, spawn = [bar])] + fn foo(cx: foo::Context) { + // .. + + // программа зависнет здесь + while cx.spawn.bar(payload).is_err() { + // повтор попытки вызова spawn, если произошла ошибка + } + } + + #[task(priority = 1)] + fn bar(cx: bar::Context, payload: i32) { + // .. + } +} +``` diff --git a/book/ru/src/by-example/timer-queue.md b/book/ru/src/by-example/timer-queue.md index 3c35e2900a..c8818d7d50 100644 --- a/book/ru/src/by-example/timer-queue.md +++ b/book/ru/src/by-example/timer-queue.md @@ -1,57 +1,76 @@ # Очередь таймера -Когда включена опция `timer-queue`, фреймворк RTIC включает -*глобальную очередь таймера*, которую приложения могут использовать, чтобы -*планировать* программные задачи на запуск через некоторое время в будущем. +В отличие от интерфейса `spawn`, который немедленно передает программную задачу +планировщику для немедленного запуска, интерфейс `schedule` можно использовать +для планирования задачи к запуске через какое-то время в будущем. -Чтобы была возможность планировать программную задачу, имя задачи должно -присутствовать в аргументе `schedule` контекста атрибута. Когда задача -планируется, момент ([`Instant`]), в который задачу нужно запустить, нужно передать -как первый аргумент вызова `schedule`. +Чтобы использовать интерфейс `schedule`, предварительно должен быть определен +монотонный таймер с помощью аргумента `monotonic` атрибута `#[app]`. +Этот аргумент принимает путь к типу, реализующему трейт [`Monotonic`]. +Ассоциированный тип, `Instant`, этого трейта представляет метку времени в соответствущих +единицах измерения и широко используется в интерфейсе `schedule` -- предлагается смоделировать +этот тип позднее [один из таких есть в стандартной библиотеке][std-instant]. -[`Instant`]: ../../../api/rtic/struct.Instant.html +Хотя это не отражено в определении трейта (из-за ограничений системы типов / трейтов), +разница двух `Instant`ов должна возвращать какой-то тип `Duration` (см. [`core::time::Duration`]) +и этот `Duration` должен реализовывать трейт `TryInto`. +Реализация этого трейта должна конвертировать значение `Duration`, которое +использует какую-то определенную единицу измерения времени, в единицы измерения "тактов системного таймера +(SYST)". Результат преобразований должен быть 32-битным целым. +Если результат не соответствует 32-битному целому, тогда операция должна возвращать ошибку любого типа. -Рантайм RTIC включает монотонный, растущий только вверх, 32-битный таймер, -значение которого можно запросить конструктором `Instant::now`. Время ([`Duration`]) -можно передать в `Instant::now()`, чтобы получить `Instant` в будущем. Монотонный -таймер отключен пока запущен `init`, поэтому `Instant::now()` всегда возвращает -значение `Instant(0 /* циклов тактовой частоты */)`; таймер включается сразу перед -включением прерываний и запуском `idle`. +[`Monotonic`]: ../../../api/rtic/trait.Monotonic.html +[std-instant]: https://doc.rust-lang.org/std/time/struct.Instant.html +[`core::time::Duration`]: https://doc.rust-lang.org/core/time/struct.Duration.html -[`Duration`]: ../../../api/rtic/struct.Duration.html +Для целевых платформ ARMv7+ крейт `rtic` предоставляет реализацию `Monotonic`, основанную на +встроенном CYCle CouNTer (CYCCNT). Заметьте, что это 32-битный таймер, работающий на +частоте центрального процессора, и поэтому не подходит для отслеживания интервалов времени в секундах. -В примере ниже две задачи планируются из `init`: `foo` и `bar`. `foo` - -запланирована на запуск через 8 миллионов тактов в будущем. Кроме того, `bar` -запланирован на запуск через 4 миллиона тактов в будущем. `bar` запустится раньше -`foo`, т.к. он запланирован на запуск первым. +Когда планируется задача, (определенный пользователем) `Instant`, в который задача должна быть +выполнена, должен передаваться в качестве первого аргумента вызова `schedule`. -> **ВАЖНО**: Примеры, использующие API `schedule` или абстракцию `Instant` -> **не** будут правильно работать на QEMU, потому что функциональность счетчика -> тактов Cortex-M не реализована в `qemu-system-arm`. +К тому же, выбранный `monotonic` таймер, необходимо сконфигурировать и инициализировать в +фазе работы `#[init]`. Заметьте, что *также* касается случая использования `CYCCNT`, +предоставляемого крейтом `cortex-m-rtic`. + +Пример ниже планирует к выполнению две задачи из `init`: `foo` и `bar`. `foo` запланирована +к запуску через 8 миллионов циклов в будущем. Далее, `bar` запланировано запустить через +4 миллиона циклов в будущем. Таким образом, `bar` запустится до `foo`, так как и запланировано. + +> **DF:YJ**: Примеры, использующие интерфейс `schedule` или абстракцию `Instant` +> **не будут** правильно работать на эмуляторе QEMU, поскольку счетчик циклов Cortex-M +> функционально не был реализован в `qemu-system-arm`. ``` rust {{#include ../../../../examples/schedule.rs}} ``` -Запуск программы на реальном оборудовании производит следующий вывод в консоли: +Запусе программы на реальном оборудовании создает следующий вывод в консоли: ``` text {{#include ../../../../ci/expected/schedule.run}} ``` +Когда интерфейс `schedule` используется, среда исполнения использует внутри +обработчик прерываний `SysTick` и периферию системного таймера (`SYST`), поэтому ни +тот ни другой нельзя использовать в программе. Это гарантируется изменением типа +`init::Context.core` с `cortex_m::Peripherals` на `rtic::Peripherals`. +Последняя структура содержит все поля из предыдущей кроме `SYST`. + ## Периодические задачи -Программные задачи имеют доступ к `Instant` в момент, когда были запланированы -на запуск через переменную `scheduled`. Эта информация и API `schedule` могут -быть использованы для реализации периодических задач, как показано в примере ниже. +Программные задачи имеют доступ к моменту времени `Instant`, в который они были запланированы +на выполнение переменной `scheduled`. Эта информация и интерфейс `schedule` можно использовать, +чтобы реализовать периодические задачи, как показано ниже. ``` rust {{#include ../../../../examples/periodic.rs}} ``` -Это вывод, произведенный примером. Заметьте, что есть смещение / колебание нуля -даже если `schedule.foo` была вызвана в *конце* `foo`. Использование -`Instant::now` вместо `scheduled` имело бы влияние на смещение / колебание. +Это вывод, создаваемый примером. Заметьте, что здесь пристствует небольшой дрейф / колебания +даже несмотря на то, что `schedule.foo` была вызвана в *конце* `foo`. Использование +`Instant::now` вместо `scheduled` вызвало бы дрейф / колебания. ``` text {{#include ../../../../ci/expected/periodic.run}} @@ -59,31 +78,30 @@ ## Базовое время -Для задач, планируемых из `init` мы имеем точную информацию о их планируемом -(`scheduled`) времени. Для аппаратных задач нет `scheduled` времени, потому -что эти задачи асинхронны по природе. Для аппаратных задач рантайм предоставляет -время старта (`start`), которе отражает время, в которое обработчик прерывания -был запущен. +Для задач, вызываемых из `init` мы имеем точную информацию о их `scheduled` времени. +Для аппаратных задач такого времени нет, поскольку они асинхронны по природе. +Для аппаратных задач среда исполнения предоставляет время запуска (`start`), которое отражает +время, в которое обработчик прерывания будет запущен. -Заметьте, что `start` **не** равен времени возникновения события, вызвавшего -задачу. В зависимости от приоритета задачи и загрузки системы время -`start` может быть сильно отдалено от времени возникновения события. +Заметьте, что `start` **не** равно времени прихода события, которое вызывает задачу. +В зависимости от приоритета задачи и загрузки системы, время `start` может сильно отдалиться от +времени прихода события. -Какое по Вашему мнению будет значение `scheduled` для программных задач которые -*вызываются*, вместо того чтобы планироваться? Ответ в том, что вызываемые -задачи наследуют *базовое* время контекста, в котором вызваны. Бызовым для -аппаратных задач является `start`, базовым для программных задач - `scheduled` -и базовым для `init` - `start = Instant(0)`. `idle` на сомом деле не имеет -базового времени но задачи, вызванные из него будут использовать `Instant::now()` -как их базовое время. +Какое по вашему мнению будет значение `scheduled` для программных задач, которые вызываются через +`spawn` вместо планирования? Ответ в том, что вызываемые задачи наследуют +*базовое* время того контекста, который их вызывает. Базовое время аппаратных задач - +это их время `start`, базовое время программных задач - их время `scheduled`, а +базовое время `init` - время старта системы, или нулевое +(`Instant::zero()`). `idle` на самом деле не имеет базового времени, но задачи вызываемые из нее, +используют `Instant::now()` в качестве базового. -Пример ниже демонстрирует разное значение *базового времени*. +Пример ниже демонстрирует разные смыслы *базового времени*. ``` rust {{#include ../../../../examples/baseline.rs}} ``` -Запуск программы на реальном оборудовании произведет следующий вывод в консоли: +Запуск программы на реальном оборудовании приведет к следующему выводу в консоли: ``` text {{#include ../../../../ci/expected/baseline.run}} diff --git a/book/ru/src/by-example/tips.md b/book/ru/src/by-example/tips.md index 249e8f4d1b..cf66c4b741 100644 --- a/book/ru/src/by-example/tips.md +++ b/book/ru/src/by-example/tips.md @@ -2,10 +2,15 @@ ## Обобщенное программирование (Generics) -Ресурсы, совместно используемые двумя или более задачами, реализуют трейт `Mutex` -во *всех* контекстах, даже в тех, где для доступа к данным не требуются -критические секции. Это позволяет легко писать обобщенный код оперирующий -ресурсами, который можно вызывать из различных задач. Вот такой пример: +Все объекты, предоставляющие ресурысы реализуют трейт `rtic::Mutex`. +Если ресурс не реализует его, можно обернуть его в новый тип [`rtic::Exclusive`], +который реализует трейт `Mutex`. С помощью этого нового типа +можно написать обобщенную функцию, которая работает с обобщенным ресурсом и +вызывать его из различных задач, чтобы производить однотипные операции над +похожим множеством ресурсов. +Вот один такой пример: + +[`rtic::Exclusive`]: ../../../api/rtic/struct.Exclusive.html ``` rust {{#include ../../../../examples/generics.rs}} @@ -13,12 +18,29 @@ ``` console $ cargo run --example generics -{{#include ../../../../ci/expected/generics.run}}``` +{{#include ../../../../ci/expected/generics.run}} +``` -Это также позволяет Вам изменять статические приоритеты задач без -переписывания кода. Если Вы единообразно используете `lock`-и для доступа -к данным в разделяемых ресурсах, тогда Ваш код продолжит компилироваться, -когда Вы измените приоритет задач. +## Условная компиляция + +Вы можете использовать условную компиляцию (`#[cfg]`) на ресурсах (полях структуры +`#[resources] struct Resources`) и задачах (элементах `fn`). +Эффект использования атрибутов `#[cfg]` в том, что ресурс/ задача +будут *не* доступны в соответствующих структурах `Context` если условие не выполняется. + +В примере ниже выводится сообщение каждый раз, когда вызывается задача `foo`, но только +если программы скомпилирова с профилем `dev`. + +``` rust +{{#include ../../../../examples/cfg.rs}} +``` + +``` console +$ cargo run --example cfg --release + +$ cargo run --example cfg +{{#include ../../../../ci/expected/cfg.run}} +``` ## Запуск задач из ОЗУ @@ -31,10 +53,10 @@ RTIC v0.4.x была возможность взаимодействия с др > очень мощные, но их легко использовать неправильно. Неверное использование > любого из этих атрибутов может вызвать неопределенное поведение; > Вам следует всегда предпочитать использование безопасных, высокоуровневых -> атрибутов вокруг них, таких как атрибуты `interrupt` и `exception` +> атрибутов вместо них, таких как атрибуты `interrupt` и `exception` > из `cortex-m-rt`. > -> В особых случаях функций RAM нет безопасной абстракции в `cortex-m-rt` +> В особых функций, размещаемых в ОЗУ нет безопасной абстракции в `cortex-m-rt` > v0.6.5 но создано [RFC] для добавления атрибута `ramfunc` в будущем релизе. [RFC]: https://github.com/rust-embedded/cortex-m-rt/pull/100 @@ -45,37 +67,105 @@ RTIC v0.4.x была возможность взаимодействия с др {{#include ../../../../examples/ramfunc.rs}} ``` -Запуск этой программы произведет ожидаемый вывод. +Запуск этой программы создаст ожидаемый вывод. ``` console $ cargo run --example ramfunc -{{#include ../../../../ci/expected/ramfunc.run}}``` +{{#include ../../../../ci/expected/ramfunc.run}} +``` Можно посмотреть на вывод `cargo-nm`, чтобы убедиться, что `bar` расположен в ОЗУ (`0x2000_0000`), тогда как `foo` расположен во Flash (`0x0000_0000`). ``` console $ cargo nm --example ramfunc --release | grep ' foo::' -{{#include ../../../../ci/expected/ramfunc.grep.foo}}``` +{{#include ../../../../ci/expected/ramfunc.grep.foo}} +``` ``` console $ cargo nm --example ramfunc --release | grep ' bar::' -{{#include ../../../../ci/expected/ramfunc.grep.bar}}``` +{{#include ../../../../ci/expected/ramfunc.grep.bar}} +``` -## `binds` +## Обходной путь для быстрой передачи сообщений -**ПРИМЕЧАНИЕ**: Требуется RTIC не ниже 0.4.2 +Передача сообщений всегда вызывает копирование от отправителя в +статическую переменную, а затем из статической переменной получателю. +Таким образом, при передаче большого буфера, например `[u8; 128]`, передача сообщения +вызывает два дорогих вызова `memcpy`. Чтобы минимизировать накладные расходы на передачу +сообщения, можно использовать обходной путь: вместо передачи буфера по значению, +можно передавать владеющий указатель на буфер. -Вы можете давать аппаратным задачам имена похожие на имена обычных задач. -Для этого нужно использовать аргумент `binds`: Вы называете функцию -по своему желанию и назначаете ей прерывание / исключение -через аргумент `binds`. `Spawn` и другие служебные типы будут размещены в модуле, -названном в соответствии с названием функции, а не прерывания / исключения. -Давайте посмотрим пример: +Можно использовать глобальный аллокатор, чтобы реализовать данный трюк (`alloc::Box`, +`alloc::Rc`, и т.п.), либо использовать статически аллоцируемый пул памяти, например [`heapless::Pool`]. + +[`heapless::Pool`]: https://docs.rs/heapless/0.5.0/heapless/pool/index.html + +Здесь приведен пример использования `heapless::Pool` для "упаковки" буфера из 128 байт. ``` rust -{{#include ../../../../examples/binds.rs}} +{{#include ../../../../examples/pool.rs}} ``` + ``` console -$ cargo run --example binds -{{#include ../../../../ci/expected/binds.run}}``` \ No newline at end of file +$ cargo run --example pool +{{#include ../../../../ci/expected/pool.run}} +``` + +## Инспектирование раскрываемого кода + +`#[rtic::app]` - это процедурный макрос, который создает код. +Если по какой-то причине вам нужно увидеть код, сгенерированный этим макросом, +у вас есть два пути: + +Вы можете изучить файл `rtic-expansion.rs` внутри папки `target`. Этот файл +содержит элемент `#[rtic::app]` в раскрытом виде (не всю вашу программу!) +из *последней сборки* (с помощью `cargo build` или `cargo check`) RTIC программы. +Раскрытый код не отформатирован по-умолчанию, но вы можете запустить `rustfmt` +на нем перед тем, как читать. + +``` console +$ cargo build --example foo + +$ rustfmt target/rtic-expansion.rs + +$ tail target/rtic-expansion.rs +``` + +``` rust +#[doc = r" Implementation details"] +mod app { + #[doc = r" Always include the device crate which contains the vector table"] + use lm3s6965 as _; + #[no_mangle] + unsafe extern "C" fn main() -> ! { + rtic::export::interrupt::disable(); + let mut core: rtic::export::Peripherals = core::mem::transmute(()); + core.SCB.scr.modify(|r| r | 1 << 1); + rtic::export::interrupt::enable(); + loop { + rtic::export::wfi() + } + } +} +``` + +Или, вы можете использовать подкоманду [`cargo-expand`]. Она раскроет +*все* макросы, включая атрибут `#[rtic::app]`, и модули в вашем крейте и +напечатает вывод в консоль. + +[`cargo-expand`]: https://crates.io/crates/cargo-expand + +``` console +$ # создаст такой же вывод, как выше +$ cargo expand --example smallest | tail +``` + +## Деструктуризация ресурса + +Если задача требует нескольких ресурсов, разбиение структуры ресурсов +может улучшить читабельность. Вот два примера того, как это можно сделать: + +``` rust +{{#include ../../../../examples/destructure.rs}} +``` diff --git a/book/ru/src/by-example/types-send-sync.md b/book/ru/src/by-example/types-send-sync.md index 85118897ca..755a379bb9 100644 --- a/book/ru/src/by-example/types-send-sync.md +++ b/book/ru/src/by-example/types-send-sync.md @@ -1,16 +1,15 @@ # Типы, Send и Sync -Атрибут `app` вводит контекст, коллекцию переменных в каждую из функций. -Все эти переменные имеют предсказуемые, неанонимные типы, поэтому Вы можете -писать простые функции, получающие их как аргументы. +Каждая функция в модуле `app` принимает структуру `Context` в качесте первого параметра. +Все поля этих структур имеют предсказуемые, неанонимные типы, +поэтому вы можете написать обычные функции, принимающие их как аргументы. -Описание API определяет как эти типы эти типы генерируются из входных данных. -Вы можете также сгенерировать документацию для Вашей бинарной библиотеки -(`cargo doc --bin `); в документации Вы найдете структуры `Context` -(например `init::Context` и `idle::Context`), чьи поля представляют переменные -включенные в каждую функцию. +Справочник по API определяет как эти типы генерируются на основе входных данных. +Вы можете также сгенерировать документацию к вашему крейту программы (`cargo doc --bin `); +в документации вы найдете структуры `Context` (например `init::Context` и +`idle::Context`). -В примере ниже сгенерированы разные типы с помощью атрибута `app`. +Пример ниже показывает различные типы, сгенерированные атрибутом `app`. ``` rust {{#include ../../../../examples/types.rs}} @@ -18,39 +17,30 @@ ## `Send` -[`Send`] - маркерный типаж (trait) для "типов, которые можно передавать через границы -потоков", как это определено в `core`. В контексте RTIC типаж `Send` необходим +[`Send`] - это маркерный трейт для "типов, которые можно передавать через границы +потоков", как это определено в `core`. В контексте RTIC трейт `Send` необходим только там, где возможна передача значения между задачами, запускаемыми на *разных* приоритетах. Это возникает в нескольких случаях: при передаче сообщений, -в совместно используемых `static mut` ресурсах и инициализации поздних ресурсов. +в разделяемых `static mut` ресурсах и при инициализации поздних ресурсов. [`Send`]: https://doc.rust-lang.org/core/marker/trait.Send.html -Атрибут `app` проверит, что `Send` реализован, где необходимо, поэтому Вам не -стоит волноваться об этом. Более важно знать, где Вам *не* нужен типаж `Send`: -в типах, передаваемых между задачами с *одинаковым* приоритетом. Это возникает -в двух случаях: при передаче сообщений и в совместно используемых `static mut` -ресурсах. - -В примере ниже показано, где можно использовать типы, не реализующие `Send`. - -``` rust -{{#include ../../../../examples/not-send.rs}} -``` +Атрибут `app` проверит, что `Send` реализован, где необходимо, поэтому вам не +стоит волноваться об этом. В настоящий момент все передаваемые типы в RTIC должны быть `Send`, но +это ограничение возможно будет ослаблено в будущем. ## `Sync` -Похожая ситуация, [`Sync`] - маркерный типаж для "типов, на которых можно -ссылаться в разных потоках", как это определено в `core`. В контексте RTIC -типаж `Sync` необходим только там, где возможны две или более задачи, -запускаемые на разных приоритетах, чтобы захватить разделяемую ссылку на -ресурс. Это возникает только совместно используемых `static`-ресурсах. +Аналогично, [`Sync`] - маркерный трейт для "типов, на которые можно безопасно разделять между потоками", +как это определено в `core`. В контексте RTIC типаж `Sync` необходим только там, +где возможно для двух или более задач, запускаемых на разных приоритетах получить разделяемую ссылку (`&-`) на +ресурс. Это возникает только (`&-`) ресурсах с разделяемым доступом. [`Sync`]: https://doc.rust-lang.org/core/marker/trait.Sync.html Атрибут `app` проверит, что `Sync` реализован, где необходимо, но важно знать, -где ограничение `Sync` не требуется: в `static`-ресурсах, разделяемых между -задачами с *одинаковым* приоритетом. +где ограничение `Sync` не требуется: в (`&-`) ресурсах с разделяемым доступом, за которые +соперничают задачи с *одинаковым* приоритетом. В примере ниже показано, где можно использовать типы, не реализующие `Sync`. diff --git a/book/ru/src/internals.md b/book/ru/src/internals.md index 4a47e77562..48495b04e5 100644 --- a/book/ru/src/internals.md +++ b/book/ru/src/internals.md @@ -1,7 +1,14 @@ # Под капотом -В этом разделе описывабтся внутренности фркймворка на *высоком уровне*. -Низкоуровневые тонкости, такие как парсинг и кодогенерация производимые -процедурным макросом (`#[app]`) здесь объясняться не будут. Мы сосредоточимся -на анализе пользовательской спецификации и структурах данных, используемых -рантаймом. +**Этот раздел в настоящий момент находится в разработке, +он появится снова, когда будет завершен** + +Этот раздел описывает внутренности фреймворка RTIC на *высоком уровне*. +Низкоуровневые детали, такие как парсинг и генерация кода, выполняемые процедурным макросом +(`#[app]`) объясняться не будут. Внимание будет сосредоточено на анализе +спецификации пользователя и структурах данных, используемых на этапе выполнения. + +Мы настоятельно рекомендуем вам прочитать раздел о [конкуренции] в embedonomicon +перед тем, как погружаться в материал. + +[конкуренции]: https://github.com/rust-embedded/embedonomicon/pull/48 diff --git a/book/ru/src/internals/access.md b/book/ru/src/internals/access.md new file mode 100644 index 0000000000..ea073a4d3e --- /dev/null +++ b/book/ru/src/internals/access.md @@ -0,0 +1,158 @@ +# Контроль доступа + +Одна из основ RTIC - контроль доступа. Контроль того, какая часть программы +может получить доступ к какой статической переменной - инструмент обеспечения +безопасности памяти. + +Статические переменные используются для разделения состояний между обработчиками +прерываний, или между обработчиком прерывания и нижним контекстом выполнения, `main`. +В обычном Rust коде трудно обеспечить гранулированный контроль за тем, какие функции +могут получать доступ к статическим переменным, поскольку к статическим переменным +можно получить доступ из любой функции, находящейся в той же области видимости, +в которой они определены. Модули дают частичный контроль над доступом +к статическим переменным, но они недостаточно гибкие. + +Чтобы добиться полного контроля за тем, что задачи могут получить доступ +только к статическим переменным (ресурсам), которые им были указаны в RTIC атрибуте, +фреймворк RTIC производит трансформацию структуры кода. +Эта трансформация состоит из размещения ресурсов (статических переменных), определенных +пользователем *внутри* модуля, а пользовательского кода *вне* модуля. +Это делает невозможным обращение пользовательского кода к статическим переменным. + +Затем доступ к ресурсам предоставляется каждой задаче с помощью структуры `Resources`, +чьи поля соответствуют ресурсам, к которым получает доступ задача. +Есть лишь одна такая структура на задачу и структура `Resources` инициализируется +либо уникальной ссылкой (`&mut-`) на статическую переменную, либо с помощью прокси-ресурса (см. +раздел [критические секции](critical-sections.html)). + +Код ниже - пример разных трансформаций структуры кода, происходящих за сценой: + +``` rust +#[rtic::app(device = ..)] +mod app { + static mut X: u64: 0; + static mut Y: bool: 0; + + #[init(resources = [Y])] + fn init(c: init::Context) { + // .. пользовательский код .. + } + + #[interrupt(binds = UART0, resources = [X])] + fn foo(c: foo::Context) { + // .. пользовательский код .. + } + + #[interrupt(binds = UART1, resources = [X, Y])] + fn bar(c: bar::Context) { + // .. пользовательский код .. + } + + // .. +} +``` + +Фреймворк создает код, подобный этому: + +``` rust +fn init(c: init::Context) { + // .. пользовательский код .. +} + +fn foo(c: foo::Context) { + // .. пользовательский код .. +} + +fn bar(c: bar::Context) { + // .. пользовательский код .. +} + +// Публичное API +pub mod init { + pub struct Context<'a> { + pub resources: Resources<'a>, + // .. + } + + pub struct Resources<'a> { + pub Y: &'a mut bool, + } +} + +pub mod foo { + pub struct Context<'a> { + pub resources: Resources<'a>, + // .. + } + + pub struct Resources<'a> { + pub X: &'a mut u64, + } +} + +pub mod bar { + pub struct Context<'a> { + pub resources: Resources<'a>, + // .. + } + + pub struct Resources<'a> { + pub X: &'a mut u64, + pub Y: &'a mut bool, + } +} + +/// Детали реализации +mod app { + // все, что внутри этого модуля спрятано от пользовательского кода + + static mut X: u64 = 0; + static mut Y: bool = 0; + + // настоящая точка входа в программу + unsafe fn main() -> ! { + interrupt::disable(); + + // .. + + // вызов пользовательского кода; передача ссылок на статические переменные + init(init::Context { + resources: init::Resources { + X: &mut X, + }, + // .. + }); + + // .. + + interrupt::enable(); + + // .. + } + + // обработчик прерывания,с которым связан `foo` + #[no_mangle] + unsafe fn UART0() { + // вызов пользовательского кода; передача ссылок на статические переменные + foo(foo::Context { + resources: foo::Resources { + X: &mut X, + }, + // .. + }); + } + + // обработчик прерывания,с которым связан `bar` + #[no_mangle] + unsafe fn UART1() { + // вызов пользовательского кода; передача ссылок на статические переменные + bar(bar::Context { + resources: bar::Resources { + X: &mut X, + Y: &mut Y, + }, + // .. + }); + } +} +``` diff --git a/book/ru/src/internals/ceilings.md b/book/ru/src/internals/ceilings.md index 2c645a4d43..df9901a2b4 100644 --- a/book/ru/src/internals/ceilings.md +++ b/book/ru/src/internals/ceilings.md @@ -1,3 +1,92 @@ -# Ceiling analysis +# Анализ приоритетов -**TODO** +*Поиск максимального приоритета* ресурса (*ceiling*) - поиск динамического +приоритета, который любая задача должна иметь, чтобы безопасно работать с +памятью ресурсов. Анализ приоритетов - относительно прост, +но критичен для безопасности памяти RTIC программ. + +Для расчета максимального приоритета ресурса мы должны сначала составить +список задач, имеющих доступ к ресурсу -- так как фреймворк RTIC +форсирует контроль доступа к ресурсам на этапе компиляции, он +также имеет доступ к этой информации на этапе компиляции. +Максимальный приоритет ресурса - просто наивысший логический приоритет +среди этих задач. + +`init` и `idle` не настоящие задачи, но у них есть доступ к ресурсам, +поэтому они должны учитываться при анализе приоритетов. +`idle` учитывается как задача, имеющая логический приоритет `0`, +в то время как `init` полностью исключается из анализа -- +причина этому в том, что `init` никогда не использует (не нуждается) критические +секции для доступа к статическим переменным. + +В предыдущем разделе мы показывали, что разделяемые ресусы +могут быть представлены уникальными ссылками (`&mut-`) или скрываться за +прокси в зависимости от того, имеет ли задача к ним доступ. +Какой из вариантов представляется задаче зависит от приоритета задачи и +максимального приоритета ресурса. +Если приоритет задачи такой же, как максимальный приоритет ресурса, тогда +задача получает уникальную ссылку (`&mut-`) на память ресурса, +в противном случае задача получает прокси -- это также касается `idle`. +`init` особеннвй: он всегда получает уникальные ссылки (`&mut-`) на ресурсы. + +Пример для иллюстрации анализа приоритетов: + +``` rust +#[rtic::app(device = ..)] +mod app { + struct Resources { + // доступен из `foo` (prio = 1) и `bar` (prio = 2) + // -> CEILING = 2 + #[init(0)] + x: u64, + + // доступен из `idle` (prio = 0) + // -> CEILING = 0 + #[init(0)] + y: u64, + } + + #[init(resources = [x])] + fn init(c: init::Context) { + // уникальная ссылка, потому что это `init` + let x: &mut u64 = c.resources.x; + + // уникальная ссылка, потому что это `init` + let y: &mut u64 = c.resources.y; + + // .. + } + + // PRIORITY = 0 + #[idle(resources = [y])] + fn idle(c: idle::Context) -> ! { + // уникальная ссылка, потому что + // приоритет (0) == максимальному приоритету ресурса (0) + let y: &'static mut u64 = c.resources.y; + + loop { + // .. + } + } + + #[interrupt(binds = UART0, priority = 1, resources = [x])] + fn foo(c: foo::Context) { + // прокси-ресурс, потому что + // приоритет задач (1) < максимальному приоритету ресурса (2) + let x: resources::x = c.resources.x; + + // .. + } + + #[interrupt(binds = UART1, priority = 2, resources = [x])] + fn bar(c: foo::Context) { + // уникальная ссылка, потому что + // приоритет задачи (2) == максимальному приоритету ресурса (2) + let x: &mut u64 = c.resources.x; + + // .. + } + + // .. +} +``` diff --git a/book/ru/src/internals/critical-sections.md b/book/ru/src/internals/critical-sections.md new file mode 100644 index 0000000000..e4c3d0abb1 --- /dev/null +++ b/book/ru/src/internals/critical-sections.md @@ -0,0 +1,521 @@ +# Критические секции + +Когда ресурсы (статические переменные) разделяются между двумя или более задачами, +которые выполняются с разными приоритетами, некая форма запрета изменений +необходима, чтобы изменять память без гонки данных. В RTIC мы используем +основанные на приоритетах критические секции, чтобы гарантировать запрет изменений +(см. [Протокол немедленного максимального приоритета][icpp]). + +[icpp]: https://en.wikipedia.org/wiki/Priority_ceiling_protocol + +Критическия секция состоит во временном увеличении *динамического* приоритета задачи. +Пока задача находится в критической секции, все другие задачи, которые могут +послать запрос переменной *не могут запуститься*. + +Насколько большим должен быть динамический приориткт, чтобы гарантировать запрет изменений +определенного ресурса? [Анализ приоритетов](ceilings.html) отвечает на этот вопрос +и будет обсужден в следующем разделе. В этом разделе мы сфокусируемся +на реализации критической секции. + +## Прокси-ресурсы + +Для упрощения, давайте взглянем на ресурс, разделяемый двумя задачами, +запускаемыми с разными приоритетами. Очевидно, что одна задача может вытеснить +другую; чтобы предотвратить гонку данных задача с *низким приоритетом* должна +использовать критическую секцию, когда необходимо изменять разделяемую память. +С другой стороны, высокоприоритетная задача может напрямую изменять +разделяемую память, поскольку не может быть вытеснена низкоприоритетной задачей. +Чтобы заставить использовать критическую секцию на задаче с низким приоритетом, +мы предоставляем *прокси-ресурсы*, в которых мы отдаем уникальную ссылку +(`&mut-`) высокоприоритетной задаче. + +Пример ниже показывает разные типы, передаваемые каждой задаче: + +``` rust +#[rtic::app(device = ..)] +mut app { + struct Resources { + #[init(0)] + x: u64, + } + + #[interrupt(binds = UART0, priority = 1, resources = [x])] + fn foo(c: foo::Context) { + // прокси-ресурс + let mut x: resources::x = c.resources.x; + + x.lock(|x: &mut u64| { + // критическая секция + *x += 1 + }); + } + + #[interrupt(binds = UART1, priority = 2, resources = [x])] + fn bar(c: bar::Context) { + let mut x: &mut u64 = c.resources.x; + + *x += 1; + } + + // .. +} +``` + +Теперь давайте посмотрим. как эти типы создаются фреймворком. + +``` rust +fn foo(c: foo::Context) { + // .. пользовательский код .. +} + +fn bar(c: bar::Context) { + // .. пользовательский код .. +} + +pub mod resources { + pub struct x { + // .. + } +} + +pub mod foo { + pub struct Resources { + pub x: resources::x, + } + + pub struct Context { + pub resources: Resources, + // .. + } +} + +pub mod bar { + pub struct Resources<'a> { + pub x: &'a mut u64, + } + + pub struct Context { + pub resources: Resources, + // .. + } +} + +mod app { + static mut x: u64 = 0; + + impl rtic::Mutex for resources::x { + type T = u64; + + fn lock(&mut self, f: impl FnOnce(&mut u64) -> R) -> R { + // мы рассмотрим это детально позднее + } + } + + #[no_mangle] + unsafe fn UART0() { + foo(foo::Context { + resources: foo::Resources { + x: resources::x::new(/* .. */), + }, + // .. + }) + } + + #[no_mangle] + unsafe fn UART1() { + bar(bar::Context { + resources: bar::Resources { + x: &mut x, + }, + // .. + }) + } +} +``` + +## `lock` + +Теперь давайте рассмотрим непосредственно критическую секцию. В этом примере мы должны +увеличить динамический приоритет минимум до `2`, чтобы избежать гонки данных. +В архитектуре Cortex-M динамический приоритет можно изменить записью в регистр `BASEPRI`. + +Семантика регистра `BASEPRI` такова: + +- Запись `0` в `BASEPRI` отключает его функциональность. +- Запись ненулевого значения в `BASEPRI` изменяет уровень приоритета, требуемого для + вытеснения прерывания. Однако, это имеет эффект, только когда записываемое значение + *меньше*, чем уровень приоритета текущего контекста выполнения, но обращаем внимание, что + более низкий уровень аппаратного приоритета означает более высокий логический приоритет + +Таким образом, динамический приоритет в любой момент времени может быть рассчитан как + +``` rust +dynamic_priority = max(hw2logical(BASEPRI), hw2logical(static_priority)) +``` + +Где `static_priority` - приоритет, запрограммированный в NVIC для текущего прерывания, +или логический `0`, когда текущий контекств - это `idle`. + +В этом конкретном примере мы можем реализовать критическую секцию так: + +> **ПРИМЕЧАНИЕ:** это упрощенная реализация + +``` rust +impl rtic::Mutex for resources::x { + type T = u64; + + fn lock(&mut self, f: F) -> R + where + F: FnOnce(&mut u64) -> R, + { + unsafe { + // начать критическую секцию: увеличить динамический приоритет до `2` + asm!("msr BASEPRI, 192" : : : "memory" : "volatile"); + + // запустить пользовательский код в критической секции + let r = f(&mut x); + + // окончить критическую секцию: восстановить динамический приоритет до статического значения (`1`) + asm!("msr BASEPRI, 0" : : : "memory" : "volatile"); + + r + } + } +} +``` + +В данном случае важно указать `"memory"` в блоке `asm!`. +Это не даст компилятору менять местами операции вокруг него. +Это важно, поскольку доступ к переменной `x` вне критической секции привело бы +к гонке данных. + +Важно отметить, что сигнатура метода `lock` препятствет его вложенным вызовам. +Это необходимо для безопасности памяти, так как вложенные вызовы привели бы +к созданию множественных уникальных ссылок (`&mut-`) на `x`, ломая правила заимствования Rust. +Смотреть ниже: + +``` rust +#[interrupt(binds = UART0, priority = 1, resources = [x])] +fn foo(c: foo::Context) { + // resource proxy + let mut res: resources::x = c.resources.x; + + res.lock(|x: &mut u64| { + res.lock(|alias: &mut u64| { + //~^ ошибка: `res` уже был заимствован уникально (`&mut-`) + // .. + }); + }); +} +``` + +## Вложенность + +Вложенные вызовы `lock` на *том же* ресурсе должны отклоняться компилятором +для безопасности памяти, однако вложенные вызовы `lock` на *разных* ресурсах - +нормальная операция. В этом случае мы хотим убедиться, что вложенные критические секции +никогда не приведут к понижению динамического приоритета, так как это плохо, +и мы хотим оптимизировать несколько записей в регистр `BASEPRI` и compiler fences. +Чтобы справиться с этим, мы проследим динамический приоритет задачи, с помощью стековой +переменной и используем ее, чтобы решить, записывать `BASEPRI` или нет. +На практике, стековая переменная будет соптимизирована компилятором, но все еще +будет предоставлять информацию компилятору. + +Рассмотрим такую программу: + +``` rust +#[rtic::app(device = ..)] +mod app { + struct Resources { + #[init(0)] + x: u64, + #[init(0)] + y: u64, + } + + #[init] + fn init() { + rtic::pend(Interrupt::UART0); + } + + #[interrupt(binds = UART0, priority = 1, resources = [x, y])] + fn foo(c: foo::Context) { + let mut x = c.resources.x; + let mut y = c.resources.y; + + y.lock(|y| { + *y += 1; + + *x.lock(|x| { + x += 1; + }); + + *y += 1; + }); + + // середина + + x.lock(|x| { + *x += 1; + + y.lock(|y| { + *y += 1; + }); + + *x += 1; + }) + } + + #[interrupt(binds = UART1, priority = 2, resources = [x])] + fn bar(c: foo::Context) { + // .. + } + + #[interrupt(binds = UART2, priority = 3, resources = [y])] + fn baz(c: foo::Context) { + // .. + } + + // .. +} +``` + +Код, сгенерированный фреймворком, выглядит так: + +``` rust +// опущено: пользовательский код + +pub mod resources { + pub struct x<'a> { + priority: &'a Cell, + } + + impl<'a> x<'a> { + pub unsafe fn new(priority: &'a Cell) -> Self { + x { priority } + } + + pub unsafe fn priority(&self) -> &Cell { + self.priority + } + } + + // repeat for `y` +} + +pub mod foo { + pub struct Context { + pub resources: Resources, + // .. + } + + pub struct Resources<'a> { + pub x: resources::x<'a>, + pub y: resources::y<'a>, + } +} + +mod app { + use cortex_m::register::basepri; + + #[no_mangle] + unsafe fn UART1() { + // статический приоритет прерывания (определено пользователем) + const PRIORITY: u8 = 2; + + // сделать снимок BASEPRI + let initial = basepri::read(); + + let priority = Cell::new(PRIORITY); + bar(bar::Context { + resources: bar::Resources::new(&priority), + // .. + }); + + // вернуть BASEPRI значение из снимка, сделанного ранее + basepri::write(initial); // то же, что и `asm!` блок, виденный ранее + } + + // так же для `UART0` / `foo` и `UART2` / `baz` + + impl<'a> rtic::Mutex for resources::x<'a> { + type T = u64; + + fn lock(&mut self, f: impl FnOnce(&mut u64) -> R) -> R { + unsafe { + // определение максимального приоритет ресурса + const CEILING: u8 = 2; + + let current = self.priority().get(); + if current < CEILING { + // увеличить динамический приоритет + self.priority().set(CEILING); + basepri::write(logical2hw(CEILING)); + + let r = f(&mut y); + + // восстановить динамический приоритет + basepri::write(logical2hw(current)); + self.priority().set(current); + + r + } else { + // динамический приоритет достаточно высок + f(&mut y) + } + } + } + } + + // повторить для ресурса `y` +} +``` + +Наконец, компилятор оптимизирует функцию `foo` во что-то наподобие такого: + +``` rust +fn foo(c: foo::Context) { + // ПРИМЕЧАНИЕ: BASEPRI содержит значение `0` (значение сброса) в этот момент + + // увеличить динамический приоритет до `3` + unsafe { basepri::write(160) } + + // две операции над `y` объединены в одну + y += 2; + + // BASEPRI не изменяется для доступа к `x`, потому что динамический приоритет достаточно высок + x += 1; + + // уменьшить (восстановить) динамический приоритет до `1` + unsafe { basepri::write(224) } + + // средина + + // увеличить динамический приоритет до `2` + unsafe { basepri::write(192) } + + x += 1; + + // увеличить динамический приоритет до `3` + unsafe { basepri::write(160) } + + y += 1; + + // уменьшить (восстановить) динамический приоритет до `2` + unsafe { basepri::write(192) } + + // ПРИМЕЧАНИЕ: было вы правильно объединить эту операцию над `x` с предыдущей, но + // compiler fences грубые и предотвращают оптимизацию + x += 1; + + // уменьшить (восстановить) динамический приоритет до `1` + unsafe { basepri::write(224) } + + // ПРИМЕЧАНИЕ: BASEPRI содержит значение `224` в этот момент + // обработчик UART0 восстановит значение `0` перед завершением +} +``` + +## Инвариант BASEPRI + +Инвариант, который фреймворк RTIC должен сохранять в том, что значение +BASEPRI в начале обработчика *прерывания* должно быть таким же, как и при выходе +из него. BASEPRI может изменяться в процессе выполнения обработчика прерывания, +но но выполнения обработчика прерывания в начале и конце не должно вызвать +наблюдаемого изменения BASEPRI. + +Этот инвариант нужен, чтобы избежать уеличения динамического приоритета до значений, +при которых обработчик не сможет быть вытеснен. Лучше всего это видно на следующем примере: + +``` rust +#[rtic::app(device = ..)] +mod app { + struct Resources { + #[init(0)] + x: u64, + } + + #[init] + fn init() { + // `foo` запустится сразу после завершения `init` + rtic::pend(Interrupt::UART0); + } + + #[task(binds = UART0, priority = 1)] + fn foo() { + // BASEPRI равен `0` в этот момент; динамический приоритет равен `1` + + // `bar` вытеснит `foo` в этот момент + rtic::pend(Interrupt::UART1); + + // BASEPRI равен `192` в этот момент (из-за бага); динамический приоритет равен `2` + // эта функция возвращается в `idle` + } + + #[task(binds = UART1, priority = 2, resources = [x])] + fn bar() { + // BASEPRI равен `0` (динамический приоритет = 2) + + x.lock(|x| { + // BASEPRI увеличен до `160` (динамический приоритет = 3) + + // .. + }); + + // BASEPRI восстановлен до `192` (динамический приоритет = 2) + } + + #[idle] + fn idle() -> ! { + // BASEPRI равен `192` (из-за бага); динамический приоритет = 2 + + // это не оказывает эффекта, из-за значени BASEPRI + // задача `foo` не будет выполнена снова никогда + rtic::pend(Interrupt::UART0); + + loop { + // .. + } + } + + #[task(binds = UART2, priority = 3, resources = [x])] + fn baz() { + // .. + } + +} +``` + +ВАЖНО: давайте например мы *забудем* восстановить `BASEPRI` в `UART1` -- из-за +какого нибудь бага в генераторе кода RTIC. + +``` rust +// код, сгенерированный RTIC + +mod app { + // .. + + #[no_mangle] + unsafe fn UART1() { + // статический приоритет этого прерывания (определен пользователем) + const PRIORITY: u8 = 2; + + // сделать снимок BASEPRI + let initial = basepri::read(); + + let priority = Cell::new(PRIORITY); + bar(bar::Context { + resources: bar::Resources::new(&priority), + // .. + }); + + // БАГ: ЗАБЫЛИ восстановить BASEPRI на значение из снимка + basepri::write(initial); + } +} +``` + +В результате, `idle` запустится на динамическом приоритете `2` и на самом деле +система больше никогда не перейдет на динамический приоритет ниже `2`. +Это не компромис для безопасности памяти программы, а влияет на диспетчеризацию задач: +в этом конкретном случае задачи с приоритетом `1` никогда не получат шанс на запуск. diff --git a/book/ru/src/internals/interrupt-configuration.md b/book/ru/src/internals/interrupt-configuration.md new file mode 100644 index 0000000000..5631b374a3 --- /dev/null +++ b/book/ru/src/internals/interrupt-configuration.md @@ -0,0 +1,72 @@ +# Настройка прерываний + +Прерывания - это основа работы программ на RTIC. Правильно настроить приоритеты +прерываний и убедиться, что они не изменяются во время выполнения обязательно +для безопасной работы программы. + +Фреймворк RTIC представляет приоритеты прерываний, как нечто, что должно быть определено +на этапе компиляции. Однако, статическая настройка должна быть зашита в соответствующие регистры +в процессе инициализации программы. Настройка прерываний происходит до запуска функции `init`. + +Этот пример дает представление о коде, запускаемом фреймворком RTIC: + +``` rust +#[rtic::app(device = lm3s6965)] +mod app { + #[init] + fn init(c: init::Context) { + // .. пользовательский код .. + } + + #[idle] + fn idle(c: idle::Context) -> ! { + // .. пользовательский код .. + } + + #[interrupt(binds = UART0, priority = 2)] + fn foo(c: foo::Context) { + // .. пользовательский код .. + } +} +``` + +Фреймворк генерирует точку входа в программу, которая выглядит примерно так: + +``` rust +// настоящая точку входа в программу +#[no_mangle] +unsafe fn main() -> ! { + // преобразует логические приоритеты в аппаратные / NVIC приоритеты + fn logical2hw(priority: u8) -> u8 { + use lm3s6965::NVIC_PRIO_BITS; + + // NVIC кодирует приоритеты верхними битами + // большие значения обозначают меньший приоритет + ((1 << NVIC_PRIORITY_BITS) - priority) << (8 - NVIC_PRIO_BITS) + } + + cortex_m::interrupt::disable(); + + let mut core = cortex_m::Peripheral::steal(); + + core.NVIC.enable(Interrupt::UART0); + + // значение, определенное пользователем + let uart0_prio = 2; + + // проверка на этапе компиляции, что определенный приоритет входит в поддерживаемый диапазон + let _ = [(); (1 << NVIC_PRIORITY_BITS) - (uart0_prio as usize)]; + + core.NVIC.set_priority(Interrupt::UART0, logical2hw(uart0_prio)); + + // вызов пользовательского кода + init(/* .. */); + + // .. + + cortex_m::interrupt::enable(); + + // вызов пользовательского кода + idle(/* .. */) +} +``` diff --git a/book/ru/src/internals/late-resources.md b/book/ru/src/internals/late-resources.md new file mode 100644 index 0000000000..0fad0aecf8 --- /dev/null +++ b/book/ru/src/internals/late-resources.md @@ -0,0 +1,114 @@ +# Поздние ресурсы + +Некоторые ресурсы инициализируются во время выполнения после завершения функции `init`. +Важно то, что ресурсы (статические переменные) полностью инициализируются +до того, как задачи смогут запуститься, вот почему они должны быть инициализированы +пока прерывания отключены. + +Ниже показан пример кода, генерируемого фреймворком для инициализации позних ресурсов. + +``` rust +#[rtic::app(device = ..)] +mod app { + struct Resources { + x: Thing, + } + + #[init] + fn init() -> init::LateResources { + // .. + + init::LateResources { + x: Thing::new(..), + } + } + + #[task(binds = UART0, resources = [x])] + fn foo(c: foo::Context) { + let x: &mut Thing = c.resources.x; + + x.frob(); + + // .. + } + + // .. +} +``` + +Код, генерируемы фреймворком выглядит примерно так: + +``` rust +fn init(c: init::Context) -> init::LateResources { + // .. пользовательский код .. +} + +fn foo(c: foo::Context) { + // .. пользовательский код .. +} + +// Public API +pub mod init { + pub struct LateResources { + pub x: Thing, + } + + // .. +} + +pub mod foo { + pub struct Resources<'a> { + pub x: &'a mut Thing, + } + + pub struct Context<'a> { + pub resources: Resources<'a>, + // .. + } +} + +/// Детали реализации +mod app { + // неинициализированная статическая переменная + static mut x: MaybeUninit = MaybeUninit::uninit(); + + #[no_mangle] + unsafe fn main() -> ! { + cortex_m::interrupt::disable(); + + // .. + + let late = init(..); + + // инициализация поздних ресурсов + x.as_mut_ptr().write(late.x); + + cortex_m::interrupt::enable(); //~ compiler fence + + // исключения, прерывания и задачи могут вытеснить `main` в этой точке + + idle(..) + } + + #[no_mangle] + unsafe fn UART0() { + foo(foo::Context { + resources: foo::Resources { + // `x` уже инициализирована к этому моменту + x: &mut *x.as_mut_ptr(), + }, + // .. + }) + } +} +``` + +Важная деталь здесь то, что `interrupt::enable` ведет себя как like a *compiler +fence*, которое не дает компилятору пореставить запись в `X` *после* +`interrupt::enable`. Если бы компилятор мог делать такие перестановки появились +бы гонки данных между этой записью и любой операцией `foo`, взаимодействующей с `X`. + +Архитектурам с более сложным конвейером инструкций нужен барьер памяти +(`atomic::fence`) вместо compiler fence для полной очистки операции записи +перед включением прерываний. Архитектура ARM Cortex-M не нуждается в барьере памяти +в одноядерном контексте. diff --git a/book/ru/src/internals/non-reentrancy.md b/book/ru/src/internals/non-reentrancy.md new file mode 100644 index 0000000000..98eb00fb3d --- /dev/null +++ b/book/ru/src/internals/non-reentrancy.md @@ -0,0 +1,79 @@ +# Нереентерабельность + +В RTIC задачи-обработчики *не* могут использоваться повторно. Переиспользование задачи-обработчика +может сломать правила заимствования Rust и привести к *неопределенному поведению*. +Задача-обработчик теоретически может быть переиспользована одним из двух способов: программно или аппаратно. + +## Программно + +Чтобы переиспользовать задачу-обработчик программно, назначенный ей обработчик прерывания +должен быть вызван с помощью FFI (смотрите пример ниже). FFI требует `unsafe` код, +что уменьшает желание конечных пользователей вызывать обработчик прерывания. + +``` rust +#[rtic::app(device = ..)] +mod app { + #[init] + fn init(c: init::Context) { .. } + + #[interrupt(binds = UART0)] + fn foo(c: foo::Context) { + static mut X: u64 = 0; + + let x: &mut u64 = X; + + // .. + + //~ `bar` может вытеснить `foo` в этом месте + + // .. + } + + #[interrupt(binds = UART1, priority = 2)] + fn bar(c: foo::Context) { + extern "C" { + fn UART0(); + } + + // этот обработчик прерывания вызовет задачу-обработчик `foo`, что сломает + // ссылку на статическую переменную `X` + unsafe { UART0() } + } +} +``` + +Фреймворк RTIC должен сгенерировать код обработчика прерывания, который вызывает +определенные пользователем задачи-обработчики. Мы аккуратны в том, чтобы обеспечить +невозможность вызова этих обработчиков из пользовательского кода. + +Пример выше раскрывается в: + +``` rust +fn foo(c: foo::Context) { + // .. пользовательский код .. +} + +fn bar(c: bar::Context) { + // .. пользовательский код .. +} + +mod app { + // все в этом блоке невидимо для пользовательского кода + + #[no_mangle] + unsafe fn USART0() { + foo(..); + } + + #[no_mangle] + unsafe fn USART1() { + bar(..); + } +} +``` + +## Аппаратно + +Обработчик прерывания также может быть вызван без программного вмешательства. +Это может произойти, если один обработчик будет назначен двум или более прерываниям +в векторе прерываний, но синтаксиса для такого рода функциональности в RTIC нет. diff --git a/book/ru/src/internals/tasks.md b/book/ru/src/internals/tasks.md index 85f783fbc3..665032515c 100644 --- a/book/ru/src/internals/tasks.md +++ b/book/ru/src/internals/tasks.md @@ -1,3 +1,399 @@ -# Task dispatcher +# Программные задачи -**TODO** +RTIC поддерживает программные и аппаратные задачи. Каждая аппаратная задача +назначается на отдельный обработчик прерывания. С другой стороны, несколько +программных задач могут управляться одним обработчиком прерывания -- +это сделано, чтобы минимизировать количество обработчиков прерывания, +используемых фреймворком. + +Фреймворк группирует задачи, для которых вызывается `spawn` по уровню приоритета, +и генерирует один *диспетчер задачи* для каждого уровня приоритета. +Каждый диспетчер запускается на отдельном обработчике прерывания, +а приоритет этого обработчика прерывания устанавливается так, чтобы соответствовать +уровню приоритета задач, управляемых диспетчером. + +Каждый диспетчер задач хранит *очередь* задач, *готовых* к выполнению; +эта очередь называется *очередью готовности*. Вызов программной задачи состоит +из добавления записи в очередь и вызова прерывания, который запускает соответствующий +диспетчер задач. Каждая запись в эту очередь содержит метку (`enum`), +которая идентифицирует задачу, которую необходимо выполнить и *указатель* +на сообщение, передаваемое задаче. + +Очередь готовности - неблокируемая очередь типа SPSC (один производитель - один потребитель). +Диспетчер задач владеет конечным потребителем в очереди; конечным производителем +считается ресурс, за который соперничают задачи, которые могут вызывать (`spawn`) другие задачи. + +## Дисметчер задач + +Давайте сначала глянем на код, генерируемый фреймворком для диспетчеризации задач. +Рассмотрим пример: + +``` rust +#[rtic::app(device = ..)] +mod app { + // .. + + #[interrupt(binds = UART0, priority = 2, spawn = [bar, baz])] + fn foo(c: foo::Context) { + foo.spawn.bar().ok(); + + foo.spawn.baz(42).ok(); + } + + #[task(capacity = 2, priority = 1)] + fn bar(c: bar::Context) { + // .. + } + + #[task(capacity = 2, priority = 1, resources = [X])] + fn baz(c: baz::Context, input: i32) { + // .. + } + + extern "C" { + fn UART1(); + } +} +``` + +Фреймворк создает следующий диспетчер задач, состоящий из обработчика прерывания и очереди готовности: + +``` rust +fn bar(c: bar::Context) { + // .. пользовательский код .. +} + +mod app { + use heapless::spsc::Queue; + use cortex_m::register::basepri; + + struct Ready { + task: T, + // .. + } + + /// вызываемые (`spawn`) задачи, выполняющиеся с уровнем приоритета `1` + enum T1 { + bar, + baz, + } + + // очередь готовности диспетчера задач + // `U4` - целое число, представляющее собой емкость этой очереди + static mut RQ1: Queue, U4> = Queue::new(); + + // обработчик прерывания, выбранный для диспетчеризации задач с приоритетом `1` + #[no_mangle] + unsafe UART1() { + // приоритет данного обработчика прерывания + const PRIORITY: u8 = 1; + + let snapshot = basepri::read(); + + while let Some(ready) = RQ1.split().1.dequeue() { + match ready.task { + T1::bar => { + // **ПРИМЕЧАНИЕ** упрощенная реализация + + // используется для отслеживания динамического приоритета + let priority = Cell::new(PRIORITY); + + // вызов пользовательского кода + bar(bar::Context::new(&priority)); + } + + T1::baz => { + // рассмотрим `baz` позднее + } + } + } + + // инвариант BASEPRI + basepri::write(snapshot); + } +} +``` + +## Вызов задачи + +Интерфейс `spawn` предоставлен пользователю как методы структурв `Spawn`. +Для каждой задачи существует своя структура `Spawn`. + +Код `Spawn`, генерируемый фреймворком для предыдущего примера выглядит так: + +``` rust +mod foo { + // .. + + pub struct Context<'a> { + pub spawn: Spawn<'a>, + // .. + } + + pub struct Spawn<'a> { + // отслеживает динамический приоритет задачи + priority: &'a Cell, + } + + impl<'a> Spawn<'a> { + // `unsafe` и спрятано, поскольку сы не хотит, чтобы пользователь вмешивался сюда + #[doc(hidden)] + pub unsafe fn priority(&self) -> &Cell { + self.priority + } + } +} + +mod app { + // .. + + // Поиск максимального приоритета для конечного производителя `RQ1` + const RQ1_CEILING: u8 = 2; + + // используется, чтобы отследить сколько еще сообщений для `bar` можно поставить в очередь + // `U2` - емкость задачи `bar`; максимум 2 экземпляра можно добавить в очередь + // эта очередь заполняется фреймворком до того, как запустится `init` + static mut bar_FQ: Queue<(), U2> = Queue::new(); + + // Поиск максимального приоритета для конечного потребителя `bar_FQ` + const bar_FQ_CEILING: u8 = 2; + + // приоритет-ориентированная критическая секция + // + // это запускае переданное замыкание `f` с динамическим приоритетом не ниже + // `ceiling` + fn lock(priority: &Cell, ceiling: u8, f: impl FnOnce()) { + // .. + } + + impl<'a> foo::Spawn<'a> { + /// Вызывает задачу `bar` + pub fn bar(&self) -> Result<(), ()> { + unsafe { + match lock(self.priority(), bar_FQ_CEILING, || { + bar_FQ.split().1.dequeue() + }) { + Some(()) => { + lock(self.priority(), RQ1_CEILING, || { + // помещаем задачу в очередь готовности + RQ1.split().1.enqueue_unchecked(Ready { + task: T1::bar, + // .. + }) + }); + + // вызываем прерывание, которое запускает диспетчер задач + rtic::pend(Interrupt::UART0); + } + + None => { + // достигнута максимальная вместительность; неудачный вызов + Err(()) + } + } + } + } + } +} +``` + +Использование `bar_FQ` для ограничения числа задач `bar`, которые могут бы вызваны, +может показаться искусственным, но это будет иметь больше смысла, когда мы поговорим +о вместительности задач. + +## Сообщения + +Мы пропустили, как на самом деле работает передача сообщений, поэтому давайте вернемся +к реализации `spawn`, но в этот раз для задачи `baz`, которая принимает сообщение типа `u64`. + +``` rust +fn baz(c: baz::Context, input: u64) { + // .. пользовательский код .. +} + +mod app { + // .. + + // Теперь мы покажем все содержимое структуры `Ready` + struct Ready { + task: Task, + // индекс сообщения; используется с буфером `INPUTS` + index: u8, + } + + // память, зарезервированная для хранения сообщений, переданных `baz` + static mut baz_INPUTS: [MaybeUninit; 2] = + [MaybeUninit::uninit(), MaybeUninit::uninit()]; + + // список свободной памяти: используется для отслеживания свободных ячеек в массиве `baz_INPUTS` + // эта очередь инициализируется значениями `0` и `1` перед запуском `init` + static mut baz_FQ: Queue = Queue::new(); + + // Поиск максимального приоритета для конечного потребителя `baz_FQ` + const baz_FQ_CEILING: u8 = 2; + + impl<'a> foo::Spawn<'a> { + /// Spawns the `baz` task + pub fn baz(&self, message: u64) -> Result<(), u64> { + unsafe { + match lock(self.priority(), baz_FQ_CEILING, || { + baz_FQ.split().1.dequeue() + }) { + Some(index) => { + // ПРИМЕЧАНИЕ: `index` - владеющий указатель на ячейку буфера + baz_INPUTS[index as usize].write(message); + + lock(self.priority(), RQ1_CEILING, || { + // помещаем задачу в очередь готовности + RQ1.split().1.enqueue_unchecked(Ready { + task: T1::baz, + index, + }); + }); + + // вызываем прерывание, которое запускает диспетчер задач + rtic::pend(Interrupt::UART0); + } + + None => { + // достигнута максимальная вместительность; неудачный вызов + Err(message) + } + } + } + } + } +} +``` + +А теперь давайте взглянем на настоящую реализацию диспетчера задач: + +``` rust +mod app { + // .. + + #[no_mangle] + unsafe UART1() { + const PRIORITY: u8 = 1; + + let snapshot = basepri::read(); + + while let Some(ready) = RQ1.split().1.dequeue() { + match ready.task { + Task::baz => { + // ПРИМЕЧАНИЕ: `index` - владеющий указатель на ячейку буфера + let input = baz_INPUTS[ready.index as usize].read(); + + // сообщение было прочитано, поэтому можно вернуть ячейку обратно + // чтобы освободить очередь + // (диспетчер задач имеет эксклюзивный доступ к + // последнему элементу очереди) + baz_FQ.split().0.enqueue_unchecked(ready.index); + + let priority = Cell::new(PRIORITY); + baz(baz::Context::new(&priority), input) + } + + Task::bar => { + // выглядит также как ветка для `baz` + } + + } + } + + // инвариант BASEPRI + basepri::write(snapshot); + } +} +``` + +`INPUTS` плюс `FQ`, список свободной памяти равняется эффективному пулу памяти. +Однако, вместо того *список свободной памяти* (связный список), чтобы отслеживать +пустые ячейки в буфере `INPUTS`, мы используем SPSC очередь; это позволяет нам +уменьшить количество критических секций. +На самом деле благодаря этому выбору код диспетчера задач неблокируемый. + +## Вместительность очереди + +Фреймворк RTIC использует несколько очередей, такие как очереди готовности и +списки свободной памяти. Когда список свободной памяти пуст, попытка выызова +(`spawn`) задачи приводит к ошибке; это условие проверяется во время выполнения. +Не все операции, произвожимые фреймворком с этими очередями проверяют их +пустоту / наличие места. Например, возвращение ячейки списка свободной памяти +(см. диспетчер задач) не проверяется, поскольку есть фиксированное количество +таких ячеек циркулирующих в системе, равное вместительности списка свободной памяти. +Аналогично, добавление записи в очередь готовности (см. `Spawn`) не проверяется, +потому что вместительность очереди выбрана фреймворком. + +Пользователи могут задавать вместительность программных задач; +эта вместительность - максимальное количество сообщений, которые можно +послать указанной задаче от задачи более высоким приоритетом до того, +как `spawn` вернет ошибку. Эта определяемая пользователем иместительность - +размер списка свободной памяти задачи (например `foo_FQ`), а также размер массива, +содержащего входные данные для задачи (например `foo_INPUTS`). + +Вместительность очереди готовности (например `RQ1`) вычисляется как *сумма* +вместительностей всех задач, управляемх диспетчером; эта сумма является также +количеством сообщений, которые очередь может хранить в худшем сценарии, когда +все возможные сообщения были посланы до того, как диспетчер задач получает шанс +на запуск. По этой причине получение ячейки списка свободной памяти при любой +операции `spawn` приводит к тому, что очередь готовности еще не заполнена, +поэтому вставка записи в список готовности может пропустить проверку "полна ли очередь?". + +В нашем запущенном примере задача `bar` не принимает входных данных, поэтому +мы можем пропустить проверку как `bar_INPUTS`, так и `bar_FQ` и позволить +пользователю посылать неограниченное число сообщений задаче, но если бы мы сделали это, +было бы невозможно превысить вместительность для `RQ1`, что позволяет нам +пропустить проверку "полна ли очередь?" при вызове задачи `baz`. +В разделе о [очереди таймера](timer-queue.html) мы увидим как +список свободной памяти используется для задач без входных данных. + +## Анализ приоритетов + +Очереди, использемые внутри интерфейса `spawn`, рассматриваются как обычные ресурсы +и для них тоже работает анализ приоритетов. Важно заметить, что это SPSC очереди, +и только один из конечных элементов становится ресурсом; другим конечным элементом +владеет диспетчер задач. + +Рассмотрим следующий пример: + +``` rust +#[rtic::app(device = ..)] +mod app { + #[idle(spawn = [foo, bar])] + fn idle(c: idle::Context) -> ! { + // .. + } + + #[task] + fn foo(c: foo::Context) { + // .. + } + + #[task] + fn bar(c: bar::Context) { + // .. + } + + #[task(priority = 2, spawn = [foo])] + fn baz(c: baz::Context) { + // .. + } + + #[task(priority = 3, spawn = [bar])] + fn quux(c: quux::Context) { + // .. + } +} +``` + +Вот как будет проходить анализ приоритетов: + +- `idle` (prio = 0) и `baz` (prio = 2) соревнуются за конечный потребитель + `foo_FQ`; это приводит к максимальному приоритету `2`. + +- `idle` (prio = 0) и `quux` (prio = 3) соревнуются за конечный потребитель + `bar_FQ`; это приводит к максимальному приоритету `3`. + +- `idle` (prio = 0), `baz` (prio = 2) и `quux` (prio = 3) соревнуются за + конечный производитель `RQ1`; это приводит к максимальному приоритету `3` diff --git a/book/ru/src/internals/timer-queue.md b/book/ru/src/internals/timer-queue.md index 7059285244..9f2dc37e00 100644 --- a/book/ru/src/internals/timer-queue.md +++ b/book/ru/src/internals/timer-queue.md @@ -1,3 +1,372 @@ -# Timer queue +# Очередь таймера -**TODO** +Функциональность очередь таймера позволяет пользователю планировать задачи на запуск +в опреленное время в будущем. Неудивительно, что эта функция также реализуется с помощью очереди: +очередь приоритетов, где запланированные задачи сортируются в порядке аозрастания времени. +Эта функция требует таймер, способный устанавливать прерывания истечения времени. +Таймер используется для пуска прерывания, когда настает запланированное время задачи; +в этот момент задача удаляется из очереди таймера и помещается в очередь готовности. + +Давайте посмотрим, как это реализовано в коде. Рассмотрим следующую программу: + +``` rust +#[rtic::app(device = ..)] +mod app { + // .. + + #[task(capacity = 2, schedule = [foo])] + fn foo(c: foo::Context, x: u32) { + // запланировать задачу на повторный запуск через 1 млн. тактов + c.schedule.foo(c.scheduled + Duration::cycles(1_000_000), x + 1).ok(); + } + + extern "C" { + fn UART0(); + } +} +``` + +## `schedule` + +Давайте сначала взглянем на интерфейс `schedule`. + +``` rust +mod foo { + pub struct Schedule<'a> { + priority: &'a Cell, + } + + impl<'a> Schedule<'a> { + // `unsafe` и спрятано, потому что мы не хотим, чтобы пользовать сюда вмешивался + #[doc(hidden)] + pub unsafe fn priority(&self) -> &Cell { + self.priority + } + } +} + +mod app { + type Instant = ::Instant; + + // все задачи, которые могут быть запланированы (`schedule`) + enum T { + foo, + } + + struct NotReady { + index: u8, + instant: Instant, + task: T, + } + + // Очередь таймера - двоичная куча (min-heap) задач `NotReady` + static mut TQ: TimerQueue = ..; + const TQ_CEILING: u8 = 1; + + static mut foo_FQ: Queue = Queue::new(); + const foo_FQ_CEILING: u8 = 1; + + static mut foo_INPUTS: [MaybeUninit; 2] = + [MaybeUninit::uninit(), MaybeUninit::uninit()]; + + static mut foo_INSTANTS: [MaybeUninit; 2] = + [MaybeUninit::uninit(), MaybeUninit::uninit()]; + + impl<'a> foo::Schedule<'a> { + fn foo(&self, instant: Instant, input: u32) -> Result<(), u32> { + unsafe { + let priority = self.priority(); + if let Some(index) = lock(priority, foo_FQ_CEILING, || { + foo_FQ.split().1.dequeue() + }) { + // `index` - владеющий укачатель на ячейки в этих буферах + foo_INSTANTS[index as usize].write(instant); + foo_INPUTS[index as usize].write(input); + + let nr = NotReady { + index, + instant, + task: T::foo, + }; + + lock(priority, TQ_CEILING, || { + TQ.enqueue_unchecked(nr); + }); + } else { + // Не осталось места, чтобы разместить входные данные / instant + Err(input) + } + } + } + } +} +``` + +Это очень похоже на реализацию `Spawn`. На самом деле одни и те же буфер +`INPUTS` и список сободной памяти (`FQ`) используются совместно интерфейсами +`spawn` и `schedule`. Главное отличие между ними в том, что `schedule` также +размещает `Instant`, момент на который задача запланирована на запуск, +в отдельном буфере (`foo_INSTANTS` в нашем случае). + +`TimerQueue::enqueue_unchecked` делает немного больше работы, чем +просто добавление записи в min-heap: он также вызывает прерывание +системного таймера (`SysTick`), если новая запись оказывается первой в очереди. + +## Системный таймер + +Прерывание системного таймера (`SysTick`) заботится о двух вещах: +передаче задач, которых становятся готовыми из очереди таймера в очередь готовности +и установке прерывания истечения времени, когда наступит запланированное +время следующей задачи. + +Давайте посмотрим на соответствующий код. + +``` rust +mod app { + #[no_mangle] + fn SysTick() { + const PRIORITY: u8 = 1; + + let priority = &Cell::new(PRIORITY); + while let Some(ready) = lock(priority, TQ_CEILING, || TQ.dequeue()) { + match ready.task { + T::foo => { + // переместить эту задачу в очередь готовности `RQ1` + lock(priority, RQ1_CEILING, || { + RQ1.split().0.enqueue_unchecked(Ready { + task: T1::foo, + index: ready.index, + }) + }); + + // вызвать диспетчер задач + rtic::pend(Interrupt::UART0); + } + } + } + } +} +``` + +Выглядит похоже на диспетчер задач, за исключением того, что +вместо запуска готовой задачи, она лишь переносится в очередь готовности, +что ведет к ее запуску с нужным приоритетом. + +`TimerQueue::dequeue` установит новое прерывание истечения времени, если вернет +`None`. Он сязан с `TimerQueue::enqueue_unchecked`, который вызывает это +прерывание; на самом деле, `enqueue_unchecked` передает задачу установки +нового прерывание истечения времени обработчику `SysTick`. + +## Точность и диапазон `cyccnt::Instant` и `cyccnt::Duration` + +RTIC предоставляет реализацию `Monotonic`, основанную на счетчике тактов `DWT` (Data Watchpoint and Trace). `Instant::now` возвращает снимок таймера; эти снимки +DWT (`Instant`ы) используются для сортировки записей в очереди таймера. +Счетчик тактов - 32-битный счетчик, работающий на частоте ядра. +Этот счетчик обнуляется каждые `(1 << 32)` тактов; у нас нет прерывания, +ассоциированног с этим счетчиком, поэтому ничего ужасного не случится, +когда он пройдет оборот. + +Чтобы упорядочить `Instant`ы в очереди, нам нужно сравнить 32-битные целые. +Чтобы учесть обороты, мы используем разницу между двумя `Instant`ами, `a - b`, +и рассматриваем результат как 32-битное знаковое целое. +Если результат меньше нуля, значит `b` более поздний `Instant`; +если результат больше нуля, значит `b` более ранний `Instant`. +Это значит, что планирование задачи на `Instant`, который на `(1 << 31) - 1` тактов +больше, чем запланированное время (`Instant`) первой (самой ранней) записи +в очереди приведет к тому, что задача будет помещена в неправильное +место в очереди. У нас есть несколько debug assertions в коде, чтобы +предотвратить эту пользовательскую ошибку, но этого нельзя избежать, +поскольку пользователь может написать +`(instant + duration_a) + duration_b` и переполнить `Instant`. + +Системный таймер, `SysTick` - 24-битный счетчик также работающий +на частоте процессора. Когда следующая планируемая задача более, чем в +`1 << 24` тактов в будущем, прерывание устанавливается на время в пределах +`1 << 24` тактов. Этот процесс может происходить несколько раз, пока +следующая запланированная задача не будет в диапазоне счетчика `SysTick`. + +Подведем итог, оба `Instant` и `Duration` имеют разрешение 1 такт ядра, и `Duration` эффективно имеет (полуоткрытый) диапазон `0..(1 << 31)` (не включая максимум) тактов ядра. + +## Вместительность очереди + +Вместительность очереди таймера рассчитывается как сумма вместительностей +всех планируемых (`schedule`) задач. Как и в случае очередей готовности, +это значит, что как только мы затребовали пустую ячейку в буфере `INPUTS`, +мы гарантируем, что способны передать задачу в очередь таймера; +это позволяет нам опустить проверки времени выполнения. + +## Приоритет системного таймера + +Приориет системного таймера не может быть установлен пользователем; +он выбирается фреймворком. +Чтобы убедиться, что низкоприоритетные задачи не препятствуют +запуску высокоприоритетных, мы выбираем приоритет системного таймера +максимальным из всех планируемых задач. + +Чтобы понять, почему это нужно, рассмотрим вариант, когда две ранее +запланированные задачи с приоритетами `2` и `3` становятся готовыми в +примерно одинаковое время, но низкоприоритетная задача перемещается +в очередь готовности первой. +Если бы приоритет системного таймера был, например, равен `1`, +тогда после перемещения низкоприоритетной (`2`) задачи, это бы привело +к завершению (из-за того, что приоритет выше приоритета системного таймера) +ожидания выполнения высокоприоритетной задачи (`3`). +Чтобы избежать такого сценария, системный таймер должен работать на +приоритете, равном наивысшему из приоритетов планируемых задач; +в этом примере это `3`. + +## Анализ приоритетов + +Очередь таймера - это ресурс, разделяемый всеми задачами, которые могут +планировать (`schedule`) задачи и обработчиком `SysTick`. +Также интерфейс `schedule` соперничает с интерфейсом `spawn` +за списки свободной памяти. Все это должно уситываться в анализе приоритетов. + +Чтобы проиллюстрировать, рассмотрим следующий пример: + +``` rust +#[rtic::app(device = ..)] +mod app { + #[task(priority = 3, spawn = [baz])] + fn foo(c: foo::Context) { + // .. + } + + #[task(priority = 2, schedule = [foo, baz])] + fn bar(c: bar::Context) { + // .. + } + + #[task(priority = 1)] + fn baz(c: baz::Context) { + // .. + } +} +``` + +Анализ приоритетов происходил бы вот так: + +- `foo` (prio = 3) и `baz` (prio = 1) планируемые задачи, поэтому + `SysTick` должен работать на максимальном из этих двух приоритетов, т.е. `3`. + +- `foo::Spawn` (prio = 3) и `bar::Schedule` (prio = 2) соперничают за + конечный потребитель `baz_FQ`; это приводит к максимальному приоритету `3`. + +- `bar::Schedule` (prio = 2) имеет экслюзивный доступ к + конечному потребителю `foo_FQ`; поэтому максимальный приоритет `foo_FQ` фактически `2`. + +- `SysTick` (prio = 3) и `bar::Schedule` (prio = 2) соперничают за + очередь таймера `TQ`; это приводит к максимальному приоритету `3`. + +- `SysTick` (prio = 3) и `foo::Spawn` (prio = 3) оба имеют неблокируемый + доступ к очереди готовности `RQ3`, что хранит записи `foo`; + поэтому максимальный приоритет `RQ3` фактически `3`. + +- `SysTick` имеет эксклюзивный доступ к очереди готовности `RQ1`, + которая хранит записи `baz`; поэтому максимальный приоритет `RQ1` фактически `3`. + +## Изменения в реализации `spawn` + +Когда интерфейс `schedule` используется, реализация `spawn` немного +изменяется, чтобы отслеживать baseline задач. Как можете видеть в +реализации `schedule` есть буферы `INSTANTS`, используемые, чтобы +хранить время, в которое задача была запланирована навыполнение; +этот `Instant` читается диспетчером задач и передается в пользовательский +код, как часть контекста задачи. + +``` rust +mod app { + // .. + + #[no_mangle] + unsafe UART1() { + const PRIORITY: u8 = 1; + + let snapshot = basepri::read(); + + while let Some(ready) = RQ1.split().1.dequeue() { + match ready.task { + Task::baz => { + let input = baz_INPUTS[ready.index as usize].read(); + // ADDED + let instant = baz_INSTANTS[ready.index as usize].read(); + + baz_FQ.split().0.enqueue_unchecked(ready.index); + + let priority = Cell::new(PRIORITY); + // ИЗМЕНЕНО instant передан как часть контекста задачи + baz(baz::Context::new(&priority, instant), input) + } + + Task::bar => { + // выглядит также как ветка для `baz` + } + + } + } + + // инвариант BASEPRI + basepri::write(snapshot); + } +} +``` + +И наоборот, реализации `spawn` нужно писать значение в буфер `INSTANTS`. +Записанное значение располагается в структуре `Spawn` и это либо +время `start` аппаратной задачи, либо время `scheduled` программной задачи. + +``` rust +mod foo { + // .. + + pub struct Spawn<'a> { + priority: &'a Cell, + // ADDED + instant: Instant, + } + + impl<'a> Spawn<'a> { + pub unsafe fn priority(&self) -> &Cell { + &self.priority + } + + // ADDED + pub unsafe fn instant(&self) -> Instant { + self.instant + } + } +} + +mod app { + impl<'a> foo::Spawn<'a> { + /// Spawns the `baz` task + pub fn baz(&self, message: u64) -> Result<(), u64> { + unsafe { + match lock(self.priority(), baz_FQ_CEILING, || { + baz_FQ.split().1.dequeue() + }) { + Some(index) => { + baz_INPUTS[index as usize].write(message); + // ADDED + baz_INSTANTS[index as usize].write(self.instant()); + + lock(self.priority(), RQ1_CEILING, || { + RQ1.split().1.enqueue_unchecked(Ready { + task: Task::foo, + index, + }); + }); + + rtic::pend(Interrupt::UART0); + } + + None => { + // достигнута максимальная вместительность; неудачный вызов + Err(message) + } + } + } + } + } +} +``` diff --git a/book/ru/src/migration.md b/book/ru/src/migration.md new file mode 100644 index 0000000000..b7f2fa1175 --- /dev/null +++ b/book/ru/src/migration.md @@ -0,0 +1,4 @@ +# Инструкции по миграции + +В этом разделе описывается как мигрировать между различными версиями RTIC. +Можно также использовать для сравнения версий. diff --git a/book/ru/src/migration/migration_rtic.md b/book/ru/src/migration/migration_rtic.md new file mode 100644 index 0000000000..28813fe291 --- /dev/null +++ b/book/ru/src/migration/migration_rtic.md @@ -0,0 +1,48 @@ +# Миграция с RTFM на RTIC + +В этом разделе описано, как обновить приложение, написанное на RTFM v0.5.x на RTIC той же версии. +Это необходимо из-за переименования фреймворка в соответствии с [RFC #33]. + +**Примечание:** Между RTFM v0.5.3 и RTIC v0.5.3 нет разниц в коде, это исключительно изменение имен. + +[RFC #33]: https://github.com/rtic-rs/rfcs/pull/33 + +## `Cargo.toml` + +Во-первых, зависимость `cortex-m-rtfm` должна быть изменена на `cortex-m-rtic`. + +``` toml +[dependencies] +# измените это +cortex-m-rtfm = "0.5.3" + +# на это +cortex-m-rtic = "0.5.3" +``` + +## Изменения в коде + +Единственное изменение в коде, которое нужно сделать - поменять все ссылки на `rtfm`, +чтобы они указывали на `rtic`: + +``` rust +// +// Измените это +// + +#[rtfm::app(/* .. */, monotonic = rtfm::cyccnt::CYCCNT)] +const APP: () = { + // ... + +}; + +// +// На это +// + +#[rtic::app(/* .. */, monotonic = rtic::cyccnt::CYCCNT)] +const APP: () = { + // ... + +}; +``` diff --git a/book/ru/src/migration/migration_v4.md b/book/ru/src/migration/migration_v4.md new file mode 100644 index 0000000000..0ff8039600 --- /dev/null +++ b/book/ru/src/migration/migration_v4.md @@ -0,0 +1,230 @@ +# Миграция с v0.4.x на v0.5.0 + +Этот раздел описывает как обновить программы, написанные на RTIC v0.4.x +на версию v0.5.0 фреймворка. + +## `Cargo.toml` + +Во-первых, нужно обновить версию зависимости `cortex-m-rtic` до +`"0.5.0"`. Опцию `timer-queue` нужно удалить. + +``` toml +[dependencies.cortex-m-rtic] +# изменить это +version = "0.4.3" + +# на это +version = "0.5.0" + +# и удалить Cargo feature +features = ["timer-queue"] +# ^^^^^^^^^^^^^ +``` + +## Аргумент `Context` + +Все функции внутри элемента `#[rtic::app]` должны принимать первым аргументом +структуру `Context`. Этот тип `Context` будет содержать переменные, которые были магически +инъецированы в область видимости функции версией v0.4.x фреймворка: +`resources`, `spawn`, `schedule` -- эти переменные станут полями структуры `Context`. +Каждая функция элемента `#[rtic::app]` получит отдельный тип `Context`. + +``` rust +#[rtic::app(/* .. */)] +const APP: () = { + // change this + #[task(resources = [x], spawn = [a], schedule = [b])] + fn foo() { + resources.x.lock(|x| /* .. */); + spawn.a(message); + schedule.b(baseline); + } + + // into this + #[task(resources = [x], spawn = [a], schedule = [b])] + fn foo(mut cx: foo::Context) { + // ^^^^^^^^^^^^^^^^^^^^ + + cx.resources.x.lock(|x| /* .. */); + // ^^^ + + cx.spawn.a(message); + // ^^^ + + cx.schedule.b(message, baseline); + // ^^^ + } + + // change this + #[init] + fn init() { + // .. + } + + // into this + #[init] + fn init(cx: init::Context) { + // ^^^^^^^^^^^^^^^^^ + // .. + } + + // .. +}; +``` + +## Ресурсы + +Синтаксис, используемый, для определения ресурсов был изменен с переменных `static mut` +на структуру `Resources`. + +``` rust +#[rtic::app(/* .. */)] +const APP: () = { + // измените это + static mut X: u32 = 0; + static mut Y: u32 = (); // поздний ресурс + + // на это + struct Resources { + #[init(0)] // <- начальное значение + X: u32, // ПРИМЕЧАНИЕ: мы предлагаем изменить стиль именования на `snake_case` + + Y: u32, // поздний ресурс + } + + // .. +}; +``` + +## Периферия устройства + +Если ваша программа получала доступ к периферии в `#[init]` через +переменну `device`, вам нужно будет добавить `peripherals = true` в атрибут +`#[rtic::app]`, чтобы и дальше получать доступ к периферии через поле `device` структуры `init::Context`. + +Измените это: + +``` rust +#[rtic::app(/* .. */)] +const APP: () = { + #[init] + fn init() { + device.SOME_PERIPHERAL.write(something); + } + + // .. +}; +``` + +На это: + +``` rust +#[rtic::app(/* .. */, peripherals = true)] +// ^^^^^^^^^^^^^^^^^^ +const APP: () = { + #[init] + fn init(cx: init::Context) { + // ^^^^^^^^^^^^^^^^^ + cx.device.SOME_PERIPHERAL.write(something); + // ^^^ + } + + // .. +}; +``` + +## `#[interrupt]` и `#[exception]` + +Атрибуты `#[interrupt]` и `#[exception]` были удалены. Чтобы определять аппаратные задачи в v0.5.x +используте атрибут `#[task]` с аргументом `binds`. + +Измените это: + +``` rust +#[rtic::app(/* .. */)] +const APP: () = { + // аппаратные задачи + #[exception] + fn SVCall() { /* .. */ } + + #[interrupt] + fn UART0() { /* .. */ } + + // программные задачи + #[task] + fn foo() { /* .. */ } + + // .. +}; +``` + +На это: + +``` rust +#[rtic::app(/* .. */)] +const APP: () = { + #[task(binds = SVCall)] + // ^^^^^^^^^^^^^^ + fn svcall(cx: svcall::Context) { /* .. */ } + // ^^^^^^ мы предлагаем использовать `snake_case` имя здесь + + #[task(binds = UART0)] + // ^^^^^^^^^^^^^ + fn uart0(cx: uart0::Context) { /* .. */ } + + #[task] + fn foo(cx: foo::Context) { /* .. */ } + + // .. +}; +``` + +## `schedule` + +Интерфейс `schedule` больше не требует cargo опции `timer-queue`, которая была удалена. +Чтобы использовать интерфес `schedule`, нужно сначала определить +монотонный тамер, который будет использоваьт среды выполнения, с помощью аргумента `monotonic` +атрибута `#[rtic::app]`. Чтобы продолжить использовать счетчик циклов +(CYCCNT) в качестве монотонного таймера, как было в версии v0.4.x, добавьте +аргумент `monotonic = rtic::cyccnt::CYCCNT` в атрибут `#[rtic::app]`. + +Также были добавлены типы `Duration` и `Instant`, а трейт `U32Ext` был перемещен в модуль `rtic::cyccnt`. +Этот модуль доступен только на устройствах ARMv7-M+. +Удаление `timer-queue` также возвращает периферию `DWT` в структуру периферии ядра, +включить ее в работу можно внутри `init`. + +Измените это: + +``` rust +use rtic::{Duration, Instant, U32Ext}; + +#[rtic::app(/* .. */)] +const APP: () = { + #[task(schedule = [b])] + fn a() { + // .. + } +}; +``` + +На это: + +``` rust +use rtic::cyccnt::{Duration, Instant, U32Ext}; +// ^^^^^^^^ + +#[rtic::app(/* .. */, monotonic = rtic::cyccnt::CYCCNT)] +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +const APP: () = { + #[init] + fn init(cx: init::Context) { + cx.core.DWT.enable_cycle_counter(); + // опционально, настройка запуска DWT без подключенного отладчика + cx.core.DCB.enable_trace(); + } + #[task(schedule = [b])] + fn a(cx: a::Context) { + // .. + } +}; +``` diff --git a/book/ru/src/migration/migration_v5.md b/book/ru/src/migration/migration_v5.md new file mode 100644 index 0000000000..04aedc5f81 --- /dev/null +++ b/book/ru/src/migration/migration_v5.md @@ -0,0 +1,208 @@ +# Миграция с v0.5.x на v0.6.0 + +Этот раздел описывает как обновиться с версии v0.5.x на v0.6.0 фреймворка RTIC. + +## `Cargo.toml` - увеличьте версию + +Измените версию `cortex-m-rtic` на `"0.6.0"`. + +## `mod` вместо `const` + +С поддержкой атрибутов над модулями трюк с `const APP` теперь не нужен. + +Измените + +``` rust +#[rtic::app(/* .. */)] +const APP: () = { + [код здесь] +}; +``` + +на + +``` rust +#[rtic::app(/* .. */)] +mod app { + [код здесь] +} +``` + +Так как теперь используется обычный модуль Rust, это значит, что можно использовать +обычный пользовательский код в этом модуле. +Также жто значит, что `use`-выражения для ресурсов (и т.п.) могут понадобиться. + +## Перенос диспетчеров из `extern "C"` в аргументы app. + +Измените + +``` rust +#[rtic::app(/* .. */)] +const APP: () = { + [код здесь] + + // RTIC требует, чтобы неиспользуемые прерывания были задекларированы в блоке extern, когда + // используются программные задачи; эти свободные прерывания будут использованы для управления + // программными задачами. + extern "C" { + fn SSI0(); + fn QEI0(); + } +}; +``` + +на + +``` rust +#[rtic::app(/* .. */, dispatchers = [SSI0, QEI0])] +mod app { + [код здесь] +} +``` + +Это работает и для ОЗУ-функций, см. examples/ramfunc.rs + + +## Init всегда возвращает поздние ресурсы + +С целью сделать API более симметричным задача #[init] всегда возвращает поздние ресурсы. + +С этого: + +``` rust +#[rtic::app(device = lm3s6965)] +mod app { + #[init] + fn init(_: init::Context) { + rtic::pend(Interrupt::UART0); + } + + // [еще код] +} +``` + +на это: + +``` rust +#[rtic::app(device = lm3s6965)] +mod app { + #[init] + fn init(_: init::Context) -> init::LateResources { + rtic::pend(Interrupt::UART0); + + init::LateResources {} + } + + // [еще код] +} +``` + +## Структура Resources - `#[resources]` + +Ранее ресурсы RTIC должны были располагаться в структуре с именем "Resources": + +``` rust +struct Resources { + // Ресурсы определены здесь +} +``` + +В RTIC v0.6.0 структура ресурсов аннотируется также, как и +`#[task]`, `#[init]`, `#[idle]`: атрибутом `#[resources]` + +``` rust +#[resources] +struct Resources { + // Ресурсы определены здесь +} +``` + +На самом деле, имя структуры предоставлено на усмотрение разработчика: + +``` rust +#[resources] +struct Whateveryouwant { + // Ресурсы определены здесь +} +``` + +будет работать так же хороршо. + +## Вызов/планирование откуда угодно + +С этой новой возвожностью, старый код, такой как: + + +``` rust +#[task(spawn = [bar])] +fn foo(cx: foo::Context) { + cx.spawn.bar().unwrap(); +} + +#[task(schedule = [bar])] +fn bar(cx: bar::Context) { + cx.schedule.foo(/* ... */).unwrap(); +} +``` + +Теперь будет выглядеть так: + +``` rust +#[task] +fn foo(_c: foo::Context) { + bar::spawn().unwrap(); +} + +#[task] +fn bar(_c: bar::Context) { + foo::schedule(/* ... */).unwrap(); +} +``` + +Заметьте, что атрибуты `spawn` и `schedule` больше не нужны. + +## Симметричные блокировки + +Теперь RTIC использует симметричные блокировки, это значит, что метод `lock` нужно использовать для +всех доступов к ресурсам. Поскольку высокоприоритетные задачи имеют эксклюзивный доступ к ресурсу, +в старом коде можно было следующее: + +``` rust +#[task(priority = 2, resources = [r])] +fn foo(cx: foo::Context) { + cx.resources.r = /* ... */; +} + +#[task(resources = [r])] +fn bar(cx: bar::Context) { + cx.resources.r.lock(|r| r = /* ... */); +} +``` + +С симметричными блокировками нужно вызывать `lock` для обоих задач: + +``` rust +#[task(priority = 2, resources = [r])] +fn foo(cx: foo::Context) { + cx.resources.r.lock(|r| r = /* ... */); +} + +#[task(resources = [r])] +fn bar(cx: bar::Context) { + cx.resources.r.lock(|r| r = /* ... */); +} +``` + +Заметьте, что скорость работы не изменяется благодаря оптимизациям LLVM, которые убирают ненужные блокировки. + +--- + +## Дополнительно + +### Внешние задачи + +Как программные, так и аппаратные задачи теперь можно определять вне модуля `mod app`. +Ранее это было возможно только путем реализации обертки, вызывающей реализацию задачи. + +Смотреть примеры `examples/extern_binds.rs` и `examples/extern_spawn.rs`. + diff --git a/book/ru/src/preface.md b/book/ru/src/preface.md index bfae004358..700560f4aa 100644 --- a/book/ru/src/preface.md +++ b/book/ru/src/preface.md @@ -1,12 +1,26 @@ +
+
RTIC
+

Real-Time Interrupt-driven Concurrency

Конкурентный фреймворк для создания систем реального времени

# Введение -Эта книга содержит документацию уровня пользователя фреймворком Real-Time Interrupt-driven Concurrency -(RTIC). Описание API можно найти [здесь](../../api/rtic/index.html). +Эта книга содержит документацию пользовательского уровня о фреймворке Real-Time Interrupt-driven Concurrency +(RTIC). Справочник по API можно найти [здесь](../../api/). -{{#include README_RU.md:5:44}} +Также известен как Real-Time For the Masses. -{{#include README_RU.md:50:}} + + + + +Это документация по RTIC версии v0.6.x; за документацией по другим версиям: + +* v0.5.x [сюда](/0.5). +* v0.4.x [сюда](/0.4). + +{{#include ../../../README_ru.md:7:45}} + +{{#include ../../../README_ru.md:51:}}