From 05bda2b1bd2e15f5a20cda1444992eb9b6c8887e Mon Sep 17 00:00:00 2001 From: Andrey Zgarbul Date: Sun, 4 Apr 2021 08:15:13 +0300 Subject: update russian translation of the book --- book/ru/src/internals/access.md | 158 +++++++ book/ru/src/internals/ceilings.md | 93 +++- book/ru/src/internals/critical-sections.md | 521 +++++++++++++++++++++++ book/ru/src/internals/interrupt-configuration.md | 72 ++++ book/ru/src/internals/late-resources.md | 114 +++++ book/ru/src/internals/non-reentrancy.md | 79 ++++ book/ru/src/internals/tasks.md | 400 ++++++++++++++++- book/ru/src/internals/timer-queue.md | 373 +++++++++++++++- 8 files changed, 1804 insertions(+), 6 deletions(-) create mode 100644 book/ru/src/internals/access.md create mode 100644 book/ru/src/internals/critical-sections.md create mode 100644 book/ru/src/internals/interrupt-configuration.md create mode 100644 book/ru/src/internals/late-resources.md create mode 100644 book/ru/src/internals/non-reentrancy.md (limited to 'book/ru/src/internals') diff --git a/book/ru/src/internals/access.md b/book/ru/src/internals/access.md new file mode 100644 index 0000000..ea073a4 --- /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 2c645a4..df9901a 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 0000000..e4c3d0a --- /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 0000000..5631b37 --- /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 0000000..0fad0ae --- /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 0000000..98eb00f --- /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 85f783f..6650325 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 7059285..9f2dc37 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) + } + } + } + } + } +} +``` -- cgit v1.2.3