Pull to refresh

Comments 51

Так как микроконтроллеры предоставляют разработчику полную свободу во взаимодействии с железом

Допустим, нужно вычислить числа Фибоначчи

Кроме вычисления чисел Фибоначчи, мы хотим вычислять факториалы чисел.

Вы что с помощью микроконтроллеров задачи по математике школьникам решаете?

Идеальный пример из моей практики это отправка данных на монохромный 128x64 OLED дисплей с управлением по SPI

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

есть много вычислительных задачек для микроконтроллеров разной степени абстракции.

Лично я разрабатываю цифровые синтезаторы (по сути вычислители функций с вектором аргументов), и там я сталкиваюсь со множеством асинхронных проблем

цифровые синтезаторы

Звук/RF или еще какой? Если звук - то чего-б не взять контроллер с заточкой под звук с DSP внутри и SIMD на борту? И какие там особенные проблемы с асинхронным доступом? Вывести в i2s? Считать сэмплы? Принять по Midi пакет? Или опросить клавиатуру? При мощных контроллерах проц в нем простаивать будет.

Если RF - по там частоты повыше и на высоких без FPGA невозможно становится.

и там я сталкиваюсь со множеством асинхронных проблем

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

Обычно в микроконтроллерах использую freertos для таких целей. Один поток считает и складывает результаты в очередь, другой достает из очереди и отправляет. Ну и HAL никто не отменял вроде.

Обычно в микроконтроллерах использую freertos для таких целей

как-бы не факт. Оно не всегда оправдано. В данном случае - так точно нет. Просто у ТС не получилось сладить с контроллером i2c. И это не повод прибегать к кувалде/пушке FreeRTOS чтобы выстрелить в воробушка.

Ну а HAL - ну... такое себе. Если нужно сильно быстро либо мало места, - то от него таки придется отказаться. Но это точно не случай ТС - тут HAL помог бы сильно упростить код.

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

Ну у каждого своя лошадь.... Я HAL юзаю исходя из задачи и ресурсов (особено!!!). Если ресорсов дофига, тайминги не жмут - чего б и нет? По написанию диспетчера - ну без него немного проектов (у меня), это уж совсем что-то простенькое.

А по RTOS - ну вот иногда она очень помогает и сильно упрощает разработку. За счет добавления занятого места и уменьшения доступного времени проца.

паре десятков ассемблеров

Это-ж как,! Огласите список. Я вот в своей работе так до десятка, ну может чуток больше (уже многие забыл, помнится еще даже на VLIW что-то писал). Но диспетчер написать и свою SPL (ну или по регистрам лазить) можно и без знания асма вообще, - описание структуры целевого контроллера в помощь... Правда на сильно тяжелой и незнакомой архитектуре это будет не несколько часов а дай Бог всего несколько дней...

Ну, первым был 6502 на школьном Агате (кривом клоне Эпла-2), за ним -- СМ-4 и СМ-1420 (в девичестве PDP-11). Дальше точную хронологию уже не помню, но 8080 (куда ж без него), немного 8048, немного 8051, 8086, ЕС ЭВМ (System/360), немного Z80, немного Z8000, немного 68000, немного СМ-2М (HP21xx), AVR8 (один раз, зато коммерческий проект: МК выбрали до меня, и все хотелки на сях не полезли б в память :) ), обе 32-разрядные системы команд ARM (и собсно ARM, и Тумба), чуть-чуть MIPS, чуть-чуть PIC24 (но совсем чуть-чуть), сейчас вот начал поглядывать RISC-V... Ну, плюс теоретическое знакомство -- сходу 6800, IA-64 aka Itanium, БЭСМ-6, Минск-2/22/32, Минск-23... может, ещё кого забыл. В общем, если и не два десятка, то больше десятка точно наберётся, с чем прямо имел дело.

Насчёт нескольких дней -- ну, в определённых ситуациях так и есть, тут Вы правы. Скажем, для 8086, любого ARMа или той же Системы 360 простую переключалку можно-таки за несколько часов написать, ничего до этого не зная об их архитектуре, но уже имея опыт решения такой задачи на других архитектурах, а вот для IA-32 (80386 и его последыши) -- вряд ли, ведь придётся кучу всяких структур данных заполнять (таблицы дескрипторов и всё такое прочее), чтоб заставить его работать в нормальном 32-разрядном режиме (т.е. в защищённом).

О!!! Минск-32 - был у меня опыт работы с ней, запускал в многозадачном режиме. Ну и молоток резиновый возле шкафа ОЗУ помнится....

Ну я начинал с НАИРИ-2 (было такое когда-то)... Потом информатику вел - там со студентами разбирались на Z80, плюс была разработка промкомпа на 580 серии. Запомнились 68000 Моторола (уже и не помню чем).

Ну, с минсками у меня чисто теоретическое знакомство: в живом виде уже не застал (живое -- ЕСки и СМки, если самое старое).

Ну а 68000, как и Z8000 -- куда приятней в плане системы команд, чем 8086. Может, этим запомнился -- в сравнении, так сказать?..

 Может, этим запомнился -- в сравнении

Может....Но это так давно было. Вот с МИПСом не сталкивался всеръез. А на ПИКах много чего наделано было....

Спасибо за статью, всё по полочкам и на примерах.

Про гейткипер (когда один ресурс хотят несколько процессов): это стандартная и общепринятая модель, которая применима к динамическому количеству процессов. По сути она превращается в паттерн издатель/подписчик.

Про квазиблокирующий обмен данными. Как я понял, это это получается что-то типа продолжений (continuations): есть стадии подготовки, передачи и обработки данных (похоже на концепт top-bottom halves), и хочется не размазывать кодовую базу для удобства понимания и редактирования. Можно попробовать реализовать в лоб, когда состояние (например, порядковый номер вызова функции) хранится внутри функции и есть множественные точки входа и выхода в зависимости от этого состояния, а можно через кооперативную многозадачность. Мне больше нравится последний вариант, с явной реализацией на конечном автомате, но можно сделать легковеснее, шустрее и накуренней на protohreads.

Про гейткипер (когда один ресурс хотят несколько процессов)

Так вроде у ТС один процесс...?

Про квазиблокирующий обмен данными. Как я понял, это это получается что-то типа продолжений (continuations)

Кооперативный доступ, что-то по типу asyncio(). У ТС, имеется в ввиду.

а можно через кооперативную многозадачность.

Так у автора так и реализовано, просто свой планировщик, который по факту именно по такой схеме работает.

Так вроде у ТС один процесс

Случай нескольких под спойлером

свой планировщик, который по факту именно по такой схеме работает.

Не, я про более сложный случай, полностью асинхронный, когда у нас одна сопрограмма сидит в обработчике, а другая в бесконечном цикле; это может быть полезно, когда или обработка посылки, или обработка ошибки тяжеловесная. Вот старая статья на easyelectronics, там примерно как у ТС, но поразвесистее, и легче пилится на верхнюю и нижнюю части.

Случай нескольких под спойлером

Это который "Мое скромное мнение, с которым можно не согласиться ", - ну так там конкурентов может быть больше 10-ка. Тогда уж средствами ОС решать. Ну либо писать свой мост - собственно вы так и написали.

Не, я про более сложный случай, полностью асинхронный, когда у нас одна сопрограмма сидит в обработчике,

В смысле - демона написать? А если сильно посеръезнее то там в демоне (обработчик прерывания или события) запускаем спящую обработку (отдельная нить в RTOS) с нужным приоритетом, которая после обработки снова засыпает. Либо все полностью повесить на ОС, если она это позволяет.

В смысле - демона написать?

По сути да. РТОС тут не сделает жизнь сильно проще, да и смысла в ней нет, когда достаточно флагового автомата. Deferred interrupt processing техника хорошая, но когда логика сложная и нелинейная, как в том же I2C, ее немного не хватает.

Вообще, отложенной обработки хватает всегда -- просто не всегда удобно обходиться только ей. Для большинства удобней вообще писать абсолютно последовательный код в виде обычных потоков, который приостанавливается при вызове чего-то долгоиграющего, а затем возобновляется с точки останова, когда ожидание закончилось. Но это возможно, только если за тебя кто-то сделал "нижний уровень" -- например, ОС :)

Ну так у автора то ОСы нету и он пытается сам диспетчер написать.

А так - да, на системных вызовах все сделал - и пускай себе ОС обрабатывает. Просто ввод/вывод в отдельный поток (и приходим снова к работе по типу asincio()) либо повесить все на вызовы ОС и ждать.

Ну как по мне, то "идеальный случай для DMA" - это когда нужно передать по SPI, работающему на 65 МГц пару десятков (или сотен) килобайт. Ибо в таком случае прерывания будут занимать в несколько раз больше времени чем передача (ну и скорость порежется сильно). Ну и ресурсов проца немало уйдет на это (если проц не многоядерный).

На самом деле, даже для какого-нибудь там UARTа на 9600 DMA тоже выгодней, если нужно переслать достаточно большой объём данных: не отвлекать проц от работы (или от сна :) ) по пустякам. Вот для передачи двух байтов его задействовать глупо: проц больше времени потратит на настройку DMA, чем на "ручную" пересылку этих самых двух байтов.

Ну в статье - 4 байта.

Просто DMA можно задействовать на что-то более толковое, каналов DMA не настолько много, чтобы ими раскидаться на медленные интерфейсы.

Ну, что 2, что 4... Если весь объём лезет во внутренний буфер самого UARTа, то точно проще и быстрей сделать на проце, если данные требуется досылать (или допринимать) отдельно -- уже вопрос.

Каналов DMA не очень много, но: а) иногда устройства, даже примитивные, сами умеют обращаться к памяти (например, в классических АТМЕЛовских армах так было -- не путать с современными микрочиповскими), либо центральное железо умеет работать с любыми устройствами (каналы ввода-вывода Системы 360); б) зависит от количества используемых устройств: если ДМА на всех хватает, то почему б (обычно) не использовать? Вот если не хватает -- тады конечно.

Я 100 лет как не настоящий сварщик эмбеддер, и возможно, напишу глупость, но разве не надо в примерах неблокирующего ввода-вывода в main() запрещать прерывания на время операций с очередями??? Потому что например, операция tx_queue_sz++; на stm32 не атомарная, и, если она прервётся в середине, при этом управление получит USART_IRQ_Handler() и сбросит очередь - интересные спецэффекты в дальнейшем гарантированы!
И ещё кажется, что все переменные, доступ к которым происходит и из main(), и из обработчиков прерываний, стоило бы объявить volatile - во избежание самодеятельности оптимизирующих компиляторов.

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

Я не уверен, что компилятор прям обязательно поместит неволатильную переменную в статическую память.

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

А про атомарность я в статье рассказываю, что в асинхронном мире все строится с предположением, что в любой момент процесс может быть прерван (в том числе и операционной системой)

не надо располагать в статической памяти

В stm32 все встроеное ОЗУ статическое. Динамического тама не наблюдается.

Возможно вы нечто другое имели в виду? А вот об volatile  - вы разверните свою мысль. Ибо я как-то привык, что это просто указание компилятору отменить оптимизации по этой переменной ибо она в любой момент и непонятно кем может быть изменена. Не более того. В какой секции вы ее объявите тама она и будет.

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

Ну, при настроенной иерархии возможно и так. А по поводу "плохих вещей" - ну вы их либо не заметили еще либо проекты несложные.

Все-ж про критические секции не стоит забывать и их таки объявлять.

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

И насчёт "выключения" прерывания. Во-первых, их не выключают, а запрещают (да, придираюсь к терминам -- но от слишком вольного их использования возникают проблемы). А во-вторых, в любых ARMах М-профиля, в т.ч. в STM32, при входе в обработчик прерывания автоматом запрещаются прерывания с тем же и более низким (численно -- более высоким) приоритетом, а соответственно, до завершения данного обработчика прерывания они уже не могут быть обслужены. Это одна из причин, почему в обработчиках прерываний надо выполнять минимально необходимую работу, всё остальное перекладывая на код, выполняющийся "снаружи" (например, посредством механизма отложенной обработки, который использует Винда, а до неё использовала её "мамаша" -- VAX/VMS, а до этого -- её "бабка" -- RSX-11M, а наверняка и много какие другие системы).

И насчёт "выключения" прерывания.  Во-первых, их не выключают

Ну чего-ж так сразу. Можно и выключить, вместе с ядром. :)

А в какую память он ее может ещё поместить, кроме статической? Куча на микроконтроллерах, как правило, не используется. Ввод-вывод-это не тема потоков, это тема записи/чтения файла.

Автор, походу, спутал статическую память (это к железу) со статическим размещением в памяти. А куча используется, хоть и не везде и не всеми компиляторами.

Компиляторы все умеют её использовать -- другое дело, что не всегда используется на практике, но это уже не к компилятору, а к программисту. А у автора, да, терминология хромает.

volatile - это намёк компилятору, что нельзя нарушать последовательность или игнорировать доступ к переменным.

В данной задаче с прерываниями нужно сначала положить новое число в буферtx_queue[], а потом увеличить голову буфера tx_queue_sz . Без volatile компилятор имеет право выполнить эти дейстрвия и в обратном порядке. А может и "вЫоптимизировать" напрочь обращение к переменной, если увидит, что результат не используется явно.

Volatile запрещает компилятору кешировать значение в регистре. Порядок менять запрещается в std::atomic

Как в плюсах - не знаю. В си atomic_t это просто тип данных, которые записываются и читаются за один такт, грубо говоря. Не разваливаются по пути из ядра в память и обратно. Для данного мк атомиком будует int32. А вот int64, или невыровненный int32 - уже не будут.

typedef struct { volatile int counter; } atomic_t;

volatile запрещает оптимизацию доступа, если быть точнее. Я сказал, в память, значит в память. Я сказал, в таком порядке, значит, в таком.

Вполне используется volatile, например, при объявлении портов ввода-вывода у ARMовских мк. Порядок доступа к ним ой как важен.

И ещё кажется, что все переменные, доступ к которым происходит и из main(), и из обработчиков прерываний, стоило бы объявить volatile - во избежание самодеятельности оптимизирующих компиляторов.

Автор к этому еще придет.... Видно, что развивается...

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

Вы тоже заметили эту ошибку! А автор так и не понял, что это ошибка! @vadjuse, обратите внимание, что здесь совершенно справедливо заметили: переменная tx_queue_sz меняется (проходит через процедуру: чтение-модификация-запись) и в основном потоке, и в потоке прерываний. То что ваши примеры не выявляют данную коллизию проблема исключительно ваших примеров. Коллизия есть и она вполне реальная. Довольно легко смоделировать ситуацию, когда ваш код продублирует отправку некоторых байтов. Задача для вас - найти и описать эту ситуацию (ждем правильный ответ)! Спасибо за материал, потренировал свои извилины.

Вообще-то, блокирующий и неблокирующий ввод-вывод -- это извраты из Униха, а не из реальной жизни. Реальный ввод-вывод либо синхронный -- когда каждый байтик должен передать процессор, причём непрерывно опрашивая устройство, а соответственно, не имея возможности отвлечься на что-то другое, -- либо асинхронный, когда устройство либо само передаёт данные посредством DMA, либо дёргает-таки процессор, но через прерывания, а не удерживает его внимание непрерывно. Соответственно, при асинхронном вводе-выводе процессор работает в целом параллельно и независимо от работы устройства ввода-вывода.

Пы.Сы. Претензии -- к заголовку, ибо термины "блокирующий" и "неблокирующий" -- из Унихов-Линухов, но означают, скажем так, немного не то, что синхронный и асинхронный.

термины "блокирующий" и "неблокирующий" -- из Унихов-Линухов

Это к потокам (предполагаю что это ввод/вывод блокирующий или не блокирующий дальнейшее исполнение). Ну и к линуксу - там тоже есть потоки. И в винде. И везде, где более-менее сложная структура. Как это реализовывается? Ну так можно написать неблокирующий ввод/вывод и через поллинг через прерывание от таймера, но медленно будет. А можно пакетом на DMA. а можно в прерываниях. А еще можно и отдельным ядром или процессором ввода-вывода.

По поводу "синхронного" и "асинхронного" ввода/вывода - так там 2 взгляда: если именно к железу (интерфейсы), - то синхронный - это с тактовым сигналом и строго определенными таймингами по отношению к тактовому сигналу (пример - SPI) или асихронный - нету тактового сигнала (вернее он "вложен" в полезный сигнал) и неизвестно когда начнется передача и паузы между байтами не лимитированы так что-б уж сильно (пример UART). Ну и взгляд со стороны программиста: синхронный - требующий постоянного контроля и асинхронный - требующий только инициации. Ну, на мой взгяд.

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

Ну а что взгляд программиста и электронщика различается -- это да. Я говорил чисто про программную сторону вопроса.

И да, потоков как таковых в Линухе нет. Там изврат в виде процессов с общим адресным пространством (в Винде потоки -- это потоки, принадлежащие процессу, т. е. чётко отличается одно от другого).

Пы. Сы. Строго говоря, сравнительно недавно в Линухе появился асинхронный ввод-вывод -- IO_URING, если не ошибаюсь. Мне лично он показался переусложнённым; как по мне, ядро оси должно предоставлять максимально простые и низкоуровневые сервисы, а уж навороты поверх, если они нужны, должны строиться на прикладном уровне.

Статью не читал, но поиск показывает что в ней нет слов FIFO или фифо.
Странно как это можно проигнорить касаясь темы DMA.

касаясь темы DMA.

Так в статье про DMA ни слова...

Вообще, можно. Я, например, склонен использовать термин "буфер", а не FIFO, и если б писал про DMA, то вряд ли FIFO упоминал бы -- хотя буферы были б оптом и в розницу.

Вы в статье говорите, что ситуации чтения и записи ассиметричны. Это заблуждение, они очень даже симметричны. Просто ваш пример кода неудачный. Он выявляет коллизию только на чтении, а на записи не выявляет. Коллизия есть в обоих случаях в вашем псевдокоде. И на записи и на чтении. Проблема в том что tx_queue_sz читается-модифицируется-записывается с двух мест. Про volatile вам тоже верно сказали. Например в функции main компилятор может оптимизировать код так, что скопирует значение tx_queue_sz или tx_queue_ind в регистр и будет пользоваться регистром совершенно не предполагая о том, что надо обновлять значение из переменной. Особенно, если код обработки прерывания будет в другом модуле трансляции.

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

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

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

Асинхронность это явно не тема для одной статьи.

По проблеме с работой по I2C на прерываниях. Вы это на STM32 делали? Случайно не F10x серия?

Случайно не F10x серия

Если внимательно почитать, то автор указывает stm32F446. То есть таки странно, что у него не получилось. Если б была F103 - тогда хоть как-то ситуация оправдана была-бы.

Что касается самого простого блокирующего вызова, насколько мне известно байты в уарт пишутся по событию USART_SR_TXE . Это означает, что приемный буфер пуст и данные ушли в сдвиговый регистр USART.USART_SR_TC событие, когда байт покинул сдвиговый регистр.

Можно и так и так. В случае TXE вы просто немного быстрее быстрее избавляетесь от данных, на суммарное время отправки влияет не сильно.

Sign up to leave a comment.

Articles