
Комментарии 62
«[язык ]C позволяет легко выстрелить себе в ногу; с C++ это сделать сложнее, но, когда вы это делаете, вы отстреливаете себе ногу целиком» .. а rust, получается, отстрелит все тело :)
Дочитал до места "let transfer = dma.start(buffer);" и ничего не понял: что именно в этой конструкции обеспечивает дальнейшую недоступность buffer. А если бы я хотел, чтобы он оставался доступным - тогда как я должен написать?
По растовым (явным) соглашениям такая запись означает, что владение buffer полностью отдано функции dma.start. Если такая передача не предполагается, то запись должна быть dma.start(&buffer), и у функции должна быть соответствующая сигнатура со всеми вытекающими.
То есть вызов функции max(a, b) "сожрет" обе переменных? Или дело именно в сигнатуре функции? Тогда этого явно не хватает в популярном тексте для привыкших лабать на си.
По умолчанию да. Но если тип переменных a и b реализует трейт Copy (а он реализован для базовых, небольших типов), то будет применена не семантика перемещения, а копирования, и переменные не будут “съедены”.
Для массива (тех же i32), как я понимаю, такого уже не будет?
Если под массивами в Rust подразумевать [i32, 1000], то они тоже Copy. В Rust массивы это дженерик тип [T; N] у которого есть “условная” реализация Copy:
impl<T, const N: usize> Copy for [T; N]
where
T: Copy,
{}
Которая говорит, что если тип T - Copy то и весь массив Copy
Ну тогда еще больше вопросов к автору: что же такое buffer и что такое .start()
В статье на C указывают код, аналог которого реализуют:
HAL_ADC_Start_DMA(
&hadc1,
(uint32_t *)adc_buffer,
1024
);
buffer, очевидно, предполагается с move-семантикой (то есть оно не реализует трэйт Copy) так как мы хотим получить этот буффер во владение, а при вызове wait() - отдать владение обратно вызывающему.
buffer, очевидно, предполагается с move-семантикой (то есть оно не реализует трэйт Copy)
Автор этого явно не обозначил. Тему он не раскрыл, и нюансы не высветил. (И теперь комментаторы дописывают статью за автора, как это часто бывает. Отобрать у автора плюсы, и отдать комментаторам.)
В статье на C указывают код, аналог которого реализуют HAL_ADC_Start_DMA( &hadc1, (uint32_t *)adc_buffer, 1024);
Это не аналог. В HAL_ADC_Start_DMA передается указатель на массив. Это соответствует dma.start(&buffer). Но автор говорит что передавать указатель небезопасно, так как это не блокирует доступ к массиву со стороны других процессов. Вместо этого он предлагает использовать dma.start(buffer), чтобы обезопасить буфер. И здесь якобы проявляется преимущество Rust.
Дальше идет обоснование, с оговорками - При этом компилятор, разумеется, ничего не знает о состоянии аппаратного DMA и не определяет момент окончания передачи сам. Эту связь обеспечивает реализация драйвера: низкоуровневый код проверяет флаг или прерывание периферии и внутри небольшой границы unsafe связывает состояние железа с безопасным API. То есть к dma.start(buffer) нужно приложить еще алгоритм обеспечение безопасности. (Немного похоже на кашу из топора)
А смысл подхода в этом - Идея здесь не в том, что Rust «понимает DMA», а в том, что корректно написанный драйвер может представить аппаратный протокол в виде таких состояний и правил владения, которые компилятор уже способен проверять в пользовательском коде.
По умолчанию да. Но если тип переменных a и b реализует трейт Copy (а он реализован для базовых, небольших типов)
Так что будет применено по умолчанию для dma.start(buffer), если этот buffer для “базовых, небольших типов”? Copy или Del?
Очень много и странно написали тут. Но если коротко то в Rust требуется явно прописывать если переменная передается по ссылке. При этом действуют ссылки мутабельные или не мутабельные.. по сути тебе не нужно помнить скрытые соглашения. Ты сразу по сигнатуре можешь понять как данные передаются внутрь функции (в отличие от какого ни будь где тебе нужно помнить какие данные будут скопированы а какие пойдут по ссылке).
Дочитал до места "
let transfer = dma.start(buffer);" и ничего не понял: что именно в этой конструкции обеспечивает дальнейшую недоступность buffer
а ничего.
обработчик аппаратного прерывания ничего об этом знать не будет. где-то уже обсуждалось реальными разработчиками устр-в
При таком вызове buffer посылается в функцию без права переписки, и по окончанию функции будет сразу деаллоцирован. Не когда-нибудь потом, а сразу.
Чтобы оставить его в своём контексте, нужно подать в функцию ссылку: dma.start(&buffer) Но тогда уже функция должна явно принимать ссылку, а не сам объект. И уже функция не может эту ссылку куда-то сохранить и т.д. Короче, если по логике требуется сделать копию buffer, то где-то придётся явно её сделать.
Ну или использовать трейт Copy.
Из всего сказанного легко сделать слишком сильный вывод: будто Rust превращает прошивку в формально проверенную систему. Нет.
Компилятор не обнаружит:
неверную частоту ШИМ;
ошибку в коэффициентах регулятора;
неправильную последовательность включения силовой части;
ошибку протокола;
выход физической величины за допустимый диапазон.
Вопрос дилетанта: а если вендор предоставит типизированное API это же решит эти проблемы? Если да, то по чему он этого не делает?
Есть замечательная микросхема, например, ADRV9025, где производитель так и делает - есть набор функций (на си, конечно), а набор регистров просто скрыт (не совсем, но почти). Когда начинаешь в этом разбираться - через некоторое время хронически дергается глаз и вспоминаешь про старые добрые времена, когда был набор регистров и описание необходимых процедур инициализации-переключения и т.п. писалось прямо в документации на чип.
спасибо, было интересно читать статью.
а что изменится, если пишем сложную многопоточную программу, в этом случае как будут работать эти механизмы "владения"?
и в С можно же ограничить видимость переменной (того же буфера), написав слово static, это не будет эквивалентом владения в rust?
Если не привлекать дополнительные сущности, то многопоточная программа не сможет обмениваться ресурсами между потоками внутри себя. Если привлекать сущности, ориентированные на многопоточное программирование, то владение будет передаваться между потоками.
в этом случае как будут работать эти механизмы “владения”?
Точно так же, как и в однопотоке. Правила не меняются. Любой ресурс (переменная) имеет в один момент времени:
единственного владельца
либо N читателей, либо 1 писатель
Для одновременного доступа к переменным из разных потоков используются мьютексы, которые в рантайме проверяют правило “exactly one writer XOR many readers”.
и в С можно же ограничить видимость переменной (того же буфера), написав слово static, это не будет эквивалентом владения в rust?
Нет, не будет. Код в пределах видимости этой переменной может по-прежнему делать всякое. Классический пример - попытка добавить в массив (allocable vector \ C# List) нового элемента во время итерации по этому массиву. C# выбросит исключение в рантайме при такой попытке. В C\C++ такое скомпилируется, но это будет UB. Rust откажется такой код компилировать в принципе.
let mut array = vec![1, 2, 3, 4];
for item in &array {
println!("item: {item}");
if *item == 4 {
array.push(5); // forbidden
}
}
а ктото из эмбеддеров такое пишет в здравом уме?
А кто ж тогда пишет UB в здравом уме?
ну если заменить push на swap то будет некий алгоритм сортировки и нет, array.swap rust тоже скомпилировать не даст.
@Fenex привёл пример с использованием итераторов, где берётся const-ссылка на array, что запрещает дальнейшее взятие mut-ссылки на array при вызове push. Если, однако, использовать обычные индексы
for i in 0..array.len() {
println!("item: {}", array[i]);
if array[i] == 4 {
array.push(5); // now it's OK
}
}
println!("item: {array:?}");
то array.len() вызовется один раз для формирования range, не создавая const-ссылку. И в массив действительно добавится новый элемент, но цикл будет до 4.
item: 1
item: 2
item: 3
item: 4
item: [1, 2, 3, 4, 5]
Можно перейти к циклу while, чтобы включить ещё и добавленный элемент в вывод:
let mut j = 0;
while j < array.len() {
println!("item: {}", array[j]);
if array[j] == 5 {
array.push(6);
}
j += 1;
}
В С++ это версии зависит. В новых стандартах, компилятор, вроде как, может warning дать, но только если это будут очевидно заметно.
Чтобы небыло УБ, надо итератор раскрыть и обходить вектор по индексу.
>> Rust для микроконтроллеров: что он даёт по сравнению с С
Боль
Я не гуру раста, но вроде же все проверки идут на этапе компиляции. С DMA возникает логичный вопрос - а как компилятор узнает когда передача завершится?
Плюс большинство сценариев подразумевают циклическую работу DMA, когда DMA не заканчивает передачу никогда, просто есть два прерывания, на заполнение первой половины буфера и второй. И тут данные надо или скопировать куда-то для медленной обработки или успеть обработать на месте. Насколько быстро идут процессы, компилятор также не имеет никакого понятия.
Так же, как в C/C++ - никак. Более того, компилятор даже не узнает, что данные в буфере обновились - это нужно либо в C/C++ volatile использовать (дико не эффективно), либо компиляторо-зависимый read/write barrier использовать. А если ещё и про аппаратный кэш процессора вспомнить, который может быть в наличии даже на простых архитектурах... В rust будет ровно та же проблема и те же грабли с оптимизацией, сбросом буферов записи, кэшем, владением и прочим. Совершенно везде будет небезопасный код и ручное управление владением объектами.
Компилятор никак не узнает, раст дает возможность человеку пищущему на нем запретить использовать буфер. Как он этим воспользуется и воспользуется ли зависит от человека, но на Си такой запрет сделать не получится (если мы где-то получили буфер на запись то никто не помешает сохранить указатель на него и потом записать по указателю).
И тут данные надо или скопировать куда-то для медленной обработки или успеть обработать на месте.
Это когда DMA на прием. А тут скорее речь про DMA на отправку.
Ну, в примерах всё довольно лаконично, а вот в реальности как-то так всё получается.
Скрытый текст
#![no_std]
#![no_main]
use arduino_hal::hal::i2c;
use arduino_hal::simple_pwm::*;
use panic_halt as _; // Required traits for PWM
//use ufmt::{uwriteln};
use core::cell::RefCell;
use core::cmp::{max, min};
use embedded_hal_bus::i2c::RefCellDevice;
use mpu6050_driver::{raw_to_imu_sample, AccelRange, Address, Dlpf, GyroRange, Mpu6050};
use oled_i2c::{Oled, OledConfig, FONT_5X7};
const W: u16 = 128;
const H: u16 = 32;
const MAX_LOOP_COUNT: u32 = 1000; //millis
#[arduino_hal::entry]
fn main() -> ! {
let dp = arduino_hal::Peripherals::take().unwrap();
let pins = arduino_hal::pins!(dp);
//let mut serial = arduino_hal::default_serial!(dp, pins, 115200);
let timer1 = arduino_hal::simple_pwm::Timer1Pwm::new(dp.TC1, Prescaler::Prescale64);
let mut led0 = pins.led_rx.into_output();
led0.set_low();
let mut led1 = pins.led_tx.into_output();
led1.set_low();
let mut adc = arduino_hal::Adc::new(dp.ADC, Default::default());
let a1 = pins.a1.into_analog_input(&mut adc); //throttle input
//let a0 = pins.a0.into_analog_input (&mut adc); //regen input
let mut pwm_pin = pins.d10.into_output().into_pwm(&timer1); //throttle output
pwm_pin.enable();
let safe_pin = pins.d4.into_pull_up_input(); //safeguard pin
let mut i2c = arduino_hal::I2c::new(
dp.TWI,
pins.d2.into_pull_up_input(), // Pin 2 = SDA
pins.d3.into_pull_up_input(), // Pin 3 = SCL
400_000, // Clock speed config (50kHz - 400kHz supported)
);
let i2c_bus = RefCell::new(i2c);
let i2c1 = RefCellDevice::new(&i2c_bus);
let i2c2 = RefCellDevice::new(&i2c_bus);
let mut mpu = Mpu6050::new(i2c1, Address::Ad0Low);
// Wake up and configure the sensor
if mpu.wake().is_ok() {
let _ = mpu.set_accel_range(AccelRange::G2);
let _ = mpu.set_gyro_range(GyroRange::Dps250);
}
let mut display = Oled::new(
i2c2, // any embedded_hal::i2c::I2c
0x3C, // 7-bit I2C address
OledConfig::ssd1306_128x32().with_contrast(150),
)
.expect("OLED init failed");
loop {...}
}После MicroPython тяжеловато, а уж фокусы с i2c на пустом месте. Впрочем, тут скорее вопросы к авторам библиотек и их решениям.
Ещё вот трудно передавать значения в функции, которые как бы и borrowed и не borrowed одновременно. Получается замкнутый круг, если сделать mut переменную, то её не передать, а если сделать let без mut, то уже не изменить в коде, например, цикле отрисовки кадра, приходится городить огород ради принципа, с непонятными последствиями для производительности.
И ещё такой момент с разными типами данных, тут вопрос опять к авторам библиотек, но, математика превращается в набор кастов или в лишние переменные.
Прямо сейчас идёт работа над тем, чтобы компилятор более гранулярно определял область владения переменными. А пока это не введут, нужно пользоваться lifetimes.
Для embedded это мало поможет, так как там лютая и махровая многозадачность с совершенно несвязанными задачами и массивами данных. Один и тот набор данных в разным местах когда представлен через разные типы и с точки зрения компилятора ещё никак не связан по коду (если речь про обработчки прерываний, вызовы ядра и т.п.).
А сверху этого ещё и железо работает (через DMА и отображение устройств в память). В этих случаях с точки зрения компилятора вообще нет и быть не может никакой сущности, которая данными владеет и меняет их (так как эта сущность вне программы, системы и кода).
Короче, изначально порочная для embedded идея.
На CNX вкладывали перевод статьи, где сравнивали размеры прошивок более менее живого проекта на C и Rust. Получилось весьма занимательно. Особенно то, что Rust потребовалось заметно меньше ОЗУ
Скрытый текст
С Rust еще незнаком, но почему то думал, что там уже есть встроенная многопоточность, а в статье выше зачем то использовали некую Ariel OS
Так кто-то должен управлять потоками и обеспечивать примитивы синхронизации потоков, а это задача ОС, а не языка (с оговоркой, что на выходе получаем сразу готовый набор инструкций в результате компиляции). ОСРВ обеспечивают управление потоками с помощью планировщика завязанного на systick в ARM например, т.е. буквально отдельное прерывание управляющее переключением контекста. Хоть технически те же обработчики прерываний можно считать "потоками", но их обслуживание и управление ресурсами остаётся за разработчиком независимо от языка. Вероятно это не так для микропитона, т.к. насколько понимаю он крутит код условно внутри своей виртуальной машины (поправьте, если не прав), которая уже внутри себя действительно может в многопоточность.
тоже поначалу решил, что многопоточность уже на уровне языка rust реализована.
а если это не так, то возможные преимущества rust над С как бы и испаряются.
сравнение потребления ОЗУ в статье некорректно (о чем там и сказано, собственно) из-за абсолютных разных релизаций либы сериализации, которая на С требует динамического выделения памяти (поди еще и с рекурсией :)))) )
А что значит “многопоточность на уровне языка реализована”? И там и там это будет pthread, но раст из коробки даёт тебе инструменты (sync, send), которые на этапе компиляции проверят, безопасно ли передавать значения между потоками.
И в целом это история даже не про потоки, это часть системы типов - компилятору-то все равно, потоки ОС там, прерывания или вообще ничего нет, он тупо проверяет, “а если вдруг придется это сделать - это безопасно?”. И если нет - просто запрещает…
И как по мне - это как раз и есть то самое важное преимущество раста над с.
ну в java например
В java “многопоточность на уровне языка реализована” потому что есть jvm которая обеспечивает выполнение спецификации для работы с потоками, являясь прослойкой над ОС, и спроектирована специально для java (то самое happens-before). А ОС просто предоставляет API для работы с многопоточностью, которое языки используют. В том числе и jvm.
“Ну в java например” под капотом используются ровно те же системные потоки что и в Rust.
только программеру не надо дополнительно изучать ни pthreads, ни "некую ArielOS"
На rust тоже изучать детали pthreads не обязательно. А там где используется ArielOS ваша Java просто не будет работать, и я не вижу почему это является недостатком Rust.
ну тоесть таки с растом изучать надо? но.. несильно, на пол-шишечки?
давайте уточним? в случае java не надо совсем, все реализовано в языке.
и еще про то, что как и где работает.
для java есть (было на момент когда я ей интересовался) расширение
realtime java. И библиотеки, инкапсулирующие работу с железом (точно была для COM LPT портов).
И, минимум, один микропроцессор, АППАРАТНО (то есть БЕЗ jvm) исполняющий байткод. Почему не взлетело, мне непонятно. Ибо это выглядело интереснее, нежели все это вот
Rust потребовалось заметно меньше ОЗУ
Реализация на C использует parson который требует json файла целиком и использует динамическую память, поэтому там потребление кучи 24кб. Реализация на rust использует sedre который может json парсить кусками и не использует динамическую память. На C тоже такая либа есть, lwjson, странно что её не использовали.
В статье не хватает примеров дизассмемблированного кода, особенно когда десяток строк на Расте схлопываются в одну out 0x26, r16.
точный контроль над памятью.
А бывает не точный контроль над памятью?
А ещё интересно где вы видели драйвер ДМА?
Rust для микроконтроллеров: что он даёт по сравнению с С