Когда вы работаете с прерываниями это всегда асинхронное программирование. Прерывания асинхронны по отношению к фоновой программе по определению. Но если вы сосредоточитесь только на решении проблем асинхронного программирования вы скорее всего потеряете все остальные составляющие работоспособного, хорошего-надежного, расширяемого-управляемого решения вашей задачи.
Для примера рассмотрим вполне показательную задачу из моей молодости. Это задача управления коллекторным (однофазным) двигателем в системе с нестабильной нагрузкой. То есть вы задаете обороты двигателя при определенной нагрузке (усилии сопротивления вращению), двигатель выходит на заданные обороты, потом(в любой момент) нагрузка меняется и соответственно меняются обороты двигателя, потому что ему становится легче или тяжелее крутить ротор из-за изменившегося приложенного усилия сопротивления. При этом сам алгоритм управления двигателем должен управляться от пользовательского ввода, а также по времени — режимы работы должны переключаться по определенным периодам и должны зависеть от общего состояния системы, которое отдельно контролируется по датчикам, управляется дополнительными переключателями.
Как видите это самая первая фактически бытовая формулировка задачи, которая всем понятна на уровне дали газа, едем, нажали на тормоз тормозим, включаем стоп сигналы, нужны педали, газ и тормоз. Интересно что уже здесь заложена асинхронность: двигатель управляется, переключатели переключаются, пользовательский ввод контролируется, период до завершения текущего режима работы двигателя отсчитывается, еще индикация для пользователя должна при этом всем работать — забыл! Все это должно происходить параллельно. Нажатие кнопки, которая останавливает двигатель должно совершенно в любой момент остановить двигатель.
Но знаете ли вы что такое параллельно. Вот вам пример из жизни:
Время у нас одно для всех людей, для всех датчиков, для всех моторов. Время можно поделить: пол дня играть в футбол пол дня плавать, например.
А можно пол дня писать статьи для Хабра, а пол дня копать картошку. Итого, вы, скажем, за неделю сделаете, и запас картошки на зиму, и напишете статью на Хабр, вы будете делать это не одновременно, но когда по прошествии времени вы с другом будете уплетать вкусную жаренную картошечку длинным зимним вечером, вы ему сможете рассказать что: «Вот, ты можешь прочитать статью на Хабре, я ее написал ПАРАЛЛЕЛЬНО с выкапыванием вот этой самой вкусной картошки!», и ваш друг наверно даже вас с удивлением спросит: «Ты что одной рукой копал, другой статью писал?». То есть ПАРАЛЛЕЛЬНО это, иногда, синоним ПОПЕРЕМЕННО. И «одновременно» часто значит просто: в течении одного и того же времени, значит что в течении этого времени у вас было время и на то и на другое, было время внутри времени, время как бы раздвоилось, и эти два раздвоенных времени шли параллельно. В этом смысле человек многозадачное существо даже на смотря на одноголовость, которую в этом случае можно приравнять к одноядерности, шутка :). Ходят легенды что существовали люди как Цезарь которые могли делать одновременно два дела, но мне это больше напоминает гипер-трединг , голова то у них одна, опять шутка.
Вопрос в том можно ли назвать процессор многозадачным если наш процессор также делает две разных задачи, но по пол дня? А почему нет, если вам это зачем то нужно. Я когда-то написал что определения даются с какой то целью и я не передумал! Вы можете определить любую попеременную работу как многозадачность, а можете задать какие-то ограничения по времени, главное чтобы вы понимали зачем вы вводите этот термин и как вы этот термин будете использовать. Но что значит задать какие-то ограничения по времени? Ну например если мы выберем для разделения по времени интервалом не целый день, а одну тысячную секунды - миллисекунду? Представляете, пол миллисекунды копать картошку, пол миллисекунды писать статью на Хабр? Это конечно не возможно, хотя в фантастическом кино вы возможно видели как супер быстрый человек успевает и прически всем поправить и пули передвинуть за мгновения, но в мире электроники это реальность, процессоры выполняют миллионы и миллиарды операций в секунду и поэтому они могут делать что-то одно, потом другое,… пятое, … десятое… в неуловимый для обычного человека момент который для процессора выглядит как полноценный период времени в течении которого можно приложиться к разным активностям и как-то в них преуспеть так, что человек наблюдает как все процессы выполняются одновременно, хотя на самом деле они выполняются попеременно.
То же самое с асинхронностью: надо ли понимать написание статьи на Хабре как асинхронный процесс по отношению к процессу выкапывания картошки? Я не знаю! Может вы видите в этом какой-то глубокий смысл, а я не достаточно утонул чтобы этот смысл понять, но если мы договоримся считать этот процесс асинхронным я пожалуй не буду спорить, ведь он действительно в какой-то степени асинхронный, даже если вам просто нравится использовать это слово, чтобы это конкретное слово не забыть или чтобы как-то казаться умнее в глазах окружающих, что ли.
Самое главное, я не понимаю как можно излагать какую-то концепцию совершенно абстрактно, без примера? Мне всегда нужен пример на котором я сам, в первую очередь, могу убедиться что я не вру, и что то, что я рассказываю действительно имеет смысл и в каком-то приближении работает.
Второе определение задачи: инженерное
Итак мы познакомились с первым определением нашей задачи, мы выяснили что нам надо сделать, но это определение совершенно ничего не говорит нам КАК нам это что-то надо сделать с точки зрения программирования, что нужно запрограммировать? Поэтому, следующей задачей будет анализ и формулировка инженерных условий в которых это что-то может существовать как физический процесс, у которого есть набор параметров которые надо задать и контролировать и подстраивать, если они по внешним причинам склонны к нестабильности.
Управление всегда разделяется на измерение/вычисление/применение некоторых параметров, в нашем случае нужно измерять скорость вращения ротора мотора, посчитать отклонение скорости от заданной, пересчитать это отклонение по скорости в отклонение по напряжению подаваемому на двигатель и применить эту добавку по напряжению — изменить напряжение на двигателе в соответствии с этой добавкой (положительной или отрицательной), и все это надо сделать за непостижимые для восприятия человека миллисекунды. Такая теория управления известна как PID управление, но я учился радиотехнике и я ориентируюсь на другой стиль изложения той же самой теории управления-подстройки-слежения, поэтому я не особо владею именно терминологией PID управления. На самом деле задача управления/авто-подстройки для радио-электронных параметров или механических параметров математически — это одна и та же задача с той лишь разницей что механические изменения на порядок медленнее чем электронные, но электронные обычно являются-рассматриваются как более линейные.
Тут я хотел бы обратить ваше внимание на то, что только одна задача управления двигателя с подстройкой оборотов состоит как минимум из двух асинхронных задач управления-контроля аппаратных функций: одной для чтения некоторой величины пропорциональной значению оборотов двигателя и второй задачи управления этим двигателем через задание некоторой величины пропорциональной питающей мощности, которая в свою очередь тоже пропорциональна оборотам двигателя при постоянной нагрузке на него. И есть еще третья задача, вычислительная, которую мы должны втиснуть между периодическими измерениями скорости и постоянными обновлениями заданного уровня мощности на работающем двигателе.
Эти три задачи образуют цикл управления который укладывается в миллисекунды, как это выглядит:
1. Измерение R
2. Вычисление Pnew = f(R, Pprev);
3. Применение Pnew / возврат к пункту 1.
Тут есть один не очевидный вопрос который пропадает из поля нашего зрения когда мы пытаемся просто записать алгоритм действий процесса управления как последовательность действий зависимых друг от друга! Правильно рассматривать эти задачи не как последовательность действий, а как независимые по исполнению задачи которые зависимы только по данным которыми они обмениваются. Это может выглядеть например так:
1. инициализация аппаратной функции1 измерения
2. ожидание результата R от аппаратной функции1
3*. Вычисление Pnew = f(R, Pprev);
4. ожидание готовности к инициализации аппаратной функции2
5. инициализация аппаратной функции2 с Pnew
6. синхронизация циклов управления аппаратными функциями
7. возврат к пункту 1.
* - вообще говоря в этом конкретном случае мы имеем дело с вырожденным вариантом вычислений, который не нуждается в обращении к аппаратным функциям — в более сложных системах и/или задачах и на более сложных аппаратных платформах даже этап вычисления может обращаться к аппаратным функциям и, соответственно, может включать фазу ожидания результата.
Теперь разберем пример проектирования аппаратной функции.


На рисунках показан способ управления мотором с помощью усечения полупериодов сетевого напряжения до определенного процента по времени, которое, соответственно, меняет питающую мощность создающую усилие вращения в двигателе.
Человек который профессионально занимается программированием, а значит вычислениями, и значит знает математику, должен сразу заметить что значение периода (момента) включения, который определяет подаваемую на двигатель мощность, связан со значением этой самой мощности не линейно! Но вся математика систем управления строится на линейных преобразованиях, поэтому в разделе вычислений нам придется что-то придумать чтобы процент изменения времени включения (отсечения) приложенного напряжения линейно пересчитывался в процент изменения приложенной мощности. В принципе это задача старших классов школьного уровня, хотя проблема всегда в том, что это школьное решение должно быть реализовано-внедрено в систему реального времени с гарантированной надежностью, которая, каким то образом, надежно подтверждена не только как решение математической задачи линеаризации зависимости, но и как задача которая гарантированно выполняется (успевает вычисляться) в рамках заданного периода времени или слота времени.
Но что это за заданный слот времени в который надо уместить вычисление с линеаризацией? Все есть на представленном рисунке: диапазон изменения времени включения напряжения ограничен полупериодом сетевого напряжения частота которого равна 50 Гц, полупериод соответственно равен 10 мс. Можно обратить внимание почему выбран именно такой метод управления мощностью двигателя и значит его оборотами. Для такого управления процессору физически достаточно одного единственного пина (вывода) управления на котором в нужный момент формируется фронт включающий напряжение на милли-микро секунды до момента пересечения силовым напряжением нуля, который (момент пересечения) автоматически закрывает симистор/тиристор (управляемый и силовым напряжением и сигналом процессора) электронный ключ. Процессоры в общем то и не умеют ничего другого в реальном мире, кроме как формировать фронты и реагировать на входящие фронты тока и напряжения на своих выводах. Их преимущество в том, что делают они это с невероятной скоростью и выводов у них может быть много и все одновременно(!) рабочие.
Надеюсь теперь нам вполне понятна задача которую надо реализовать в коде который будет выполняться в процессоре и управлять ВРЕМЕНЕМ-МГНОВЕНИЕМ подачи напряжения каждые 10 мс связанные с очередным полупериодом сетевого напряжения!
Внимательный читатель и знаток параметров сетевого напряжения обязательно должен задать вопрос примерно такого содержания: «Но мы же не можем в процессоре определить момент включения заданного процента от полупериода пока мы не знаем момент начала этого полу периода?!» И это очень важный вопрос!
Перефразируя древнего мыслителя можно заметить, что без точки опоры сделать ничего нельзя, обычно. Конечно, процессор должен «знать» момент начала полупериода СН (Сетевого Напряжения). Для этого в процессор должна быть заведена вторая линия, на которой процессор как раз и будет считывать момент переключения, например, полярности сетевого напряжения, схемотехнически это может быть реализовано по разному, в любом случае схемотехника обеспечивает фронт напряжения В НУЖНЫЙ МОМЕНТ ВРЕМЕНИ, на который только и способен реагировать процессор. И тут мы подходим к очень важному и никогда и нигде не сформулированному вопросу о том, какие сигналы должны управлять прерываниями в процессоре, потому что это как раз такие сигналы, это:
1. сигнал (фронт) связанный с началом полупериода СН и
2. сигнал открытия силового ключа, задающий процент поступающей на двигатель мощности.
Эти сигналы, входной и выходной, должны соответственно один генерировать прерывание, второй генерироваться через прерывание или аппаратную функцию аппаратного таймера процессора. Я попробую обосновать эту необходимость, которая хоть и не является в некоторых случаях абсолютной, всегда является абсолютно предпочтительной.
Поскольку речь тут идет уже об очень малых интервалах времени, точность формирования этих интервалов имеет фактически решающее значение, вы можете посчитать какой процент мощности убавляет 1 мс задержки включения силового ключа. С учетом нелинейности пересчета это больше 10% в центре рабочей зоны диапазона управления! Это не допустимая погрешность в системе управления, это может привести к детонациям когда мощность подаваемая на двигатель скачет от нуля до максимума, двигатель работает в режиме отбойного молотка.
Максимально снизить погрешность определяемых и формируемых моментов и интервалов времени процессором позволяет использование аппаратных функций и специальных периферийных модулей процессора, которые как раз предназначены для таких задач, для задач взаимодействия с внешней аппаратурой (со схемотехникой и исполнительными устройствами). В нашем случае такими периферийными модулями являются таймеры-счетчики с разнообразными функциями запуска счета от внешних сигналов (фронтов), и формирования сигналов по совпадению с заданным значением, формирователи ШИМ сигналов.
Описательный пример аппаратной функции из периферии процессора
В идеальном случае аппаратные средства позволяют обойтись вообще без прерываний. В системе достаточно иметь периферийный модуль счетчика который запускается по фронту полученному с внешней входной линии (входного пина) и имеет регистр для хранения значения по совпадению с которым формируется управляющий фронт. При наличии модуля таймера-счетчика в процессоре с такими функциями и с подключением к входному и выходному пинам (выводам) процессора в коде не нужно будет обрабатывать прерывания с целью управления двигателем! Значение периода времени задержки открывания силового ключа пропорциональное требуемой мощности просто записывается в регистр сравнения таймера, собственно, все управление сводится к этой записи в регистр. Хотя у этого решения есть некоторые очень хитрые нюансы в которые сейчас я не буду углубляться, например не очень понятно, что будет если мы поменяем момент осечки «назад» или «влево» когда счетчик уже считает дальше этого нового значения, совпадение уже будет пропущено! Такие нюансы тоже вполне успешно разрешаются, хотя требуют некоторого напряжения извилин.
Таким образом код будет состоять из инициализации-настройки периферийных-аппаратных модулей процессора: таймера и соответствующих портов ввода-вывода, а дальше можно писать код который вычисляет текущее значение управляющего интервала, например от заданных пользователем оборотов двигателя и передает пересчитанное значение в регистр сравнения таймера. Таким образом вам не надо будет писать асинхронный код! Всю асинхронную работу мы убрали в аппаратную функцию, которая действительно выполняется асинхронно с кодом, но на код эта асинхронность совершенно не влияет.
Таким образом мы выяснили что при определенных возможностях периферии процессора нам, кажется, не нужно отслеживать моменты запуска полупериодов СН в коде для управления двигателем, но, оказывается, нам все равно, будет очень полезно следить за моментами запуска полупериодов как минимум с точки зрения отслеживания работоспособности системы, то есть САМО-ДИАГНОСТИКИ! Контроль периода этих прерываний позволяет нам получить незаменимую информацию о исправности схемы питания и схемы управления питанием, поэтому будет логично разрешить эти прерывания и и следить чтобы интервал времени между этими прерываниями в функции этого прерывания не превышал максимально допустимого:
signalRisingFront()
{
resetPhaseFaultTimer();
}
Как видите в этом случае нам необходимо просто сбросить таймер (еще один аппаратный таймер!) прерывание от которого сообщит нам что пропало напряжение или пропал контроль за напряжением, в обоих случаях систему надо будет аварийно останавливать, так как в такой ситуации нормальная работа системы не возможна!
И у нас появляется еще один аппаратный таймер, который должен отсчитывать аварийный интервал отсутствия сетевого напряжения для перевода системы в аварийное состояние и запуска генерации аварийных сигналов для оператора. Не зря в современных процессорах количество аппаратных таймеров измеряется чуть ли не десятками. Но этот дополнительный аппаратный таймер никак не будет влиять на наш код управления в нормальном состоянии системы! Он инициализируется в самом начале при включении двигателя и только в случае аварии генерирует прерывание, которое в свою очередь должно, например, выставить флаг аварии, записать где-то код аварии, таким образом сообщить фоновой программе что нужно отключить управление и всю работу с ним связанную и перейти в режим индикации этой самой аварии.
От понятия о фоновой программе к терминам из ядра Линукса.
Вот мы и добрались до фоновой программы. На любых процессорах есть смысл различать прерывания и фоновую программу — код который исполняется в отсутствии прерываний. Когда у нас нет разделения на код ядра и код пользовательских программ у нас есть только прерывания и фоновая программа, хотя с точки зрения исходников это одна программа — прерывания это просто функции общей программы которые вызываются с помощью запрограммированных — правильно проинициализированных аппаратных функций. Код ядра Линукс реализует гораздо более сложные концепции потоков, файловой системы, и еще много чего, но в основе своей также состоит из прерываний и очень сложно организованной фоновой программы, сложность которой соответствует разнообразию и сложности функциональности, которую эта программа должна обеспечить.
В терминологии ядра Линукс широко используется пара специальных терминов, top halves и bottom halves (дословно верхние половины и нижние половины) таким образом работа по обработке прерываний делится на
1. top halves, немедленную, которая выполняется внутри функции прерывания (хендлера) вызванной аппаратной функцией и
2. bottom halves, отложенную, которая может быть выполнена позднее.
В одном из руководств по программированию в ядре Линукса можно найти примерно такие слова:
Крайне важно понимать, почему и когда именно следует откладывать выполнение работы.
Необходимо ограничивать объём работы, выполняемой в обработчике прерываний, поскольку при вызове этих обработчиков может выставляться запрет на все остальные прерывания. Минимизация времени работы с отключёнными прерываниями имеет важное значение для отклика системы и её производительности.
Добавьте к этому тот факт, что обработчики прерываний выполняются асинхронно по отношению к другому коду — даже к другим обработчикам прерываний — и станет ясно, что необходимо стремиться к минимизации времени работы обработчиков прерываний. Решение заключается в том, чтобы отложить часть работы на «потом».
Но что значит «потом»? Важно понимать, что «потом» часто означает просто «не сейчас». Суть концепции отложенных работ (bottom half) заключается не в том, чтобы выполнять работу в какой-то конкретный момент в будущем, а просто в том, чтобы отложить работу до любого момента в будущем, когда система будет менее загружена и прерывания снова будут включены. Часто нижние половины выполняются сразу после возврата из прерывания. Ключевой момент заключается в том, что они выполняются при включённых всех прерываниях.
Таким образом в ядре Линукса период в течении которого запускается продолжение работы по завершению процедуры обработки прерывания определено самым абстрактным образом «потом» = «не сейчас», и это вполне объяснимо, Линукс это огромная система для больших процессоров, которая претендует на универсальность решений на все случаи жизни. Но в конкретной задаче связанной с конкретной предметной областью это время всегда можно вполне точно определить, в нашем случае все крутится вокруг полупериода сетевого напряжения который равен 10 мс. Крайне желательно организовать работу аппаратных функций и соответствующих прерываний таким образом чтобы очередное измерение завершалось в течении этого полупериода и у процессора было достаточно времени чтобы посчитать корректировку и выдать обновленное значение мощности (момента отсечки) в управляющую схему.
Тем не менее разделение работы обработки прерывания на немедленную и отложенную части это всегда не тривиальная задача, решение которой требует знания предметной области, возможностей аппаратных функций-периферии процессора, специфики работы исполнительных устройств/датчиков. Мне кажется эту задачу вполне можно отнести к основным задачам организации асинхронного взаимодействия процессов (в смысле активностей управляемых-контролируемых из кода, а не процессами некоторой Операционной Системы, хотя, зачастую, и их тоже) эта задача не решается средствами некоторого языка программирования! Это задача анализа предметной области или задействованных смежных областей, анализа и проектирования аппаратной части системы, возможностей взаимодействия аппаратной части и программного обеспечения системы. Другими словами это скорее инженерная задача, а не задача чистого программирования.
В нашем случае хорошим примером отложенной работы будет как раз вычисление очередного значения управления для передачи в схему управления двигателем для очередного периода управления (полупериода СН). Но мы не описали сам процесс измерения!
Задача измерения оборотов двигателя (по памяти)
Насколько я помню, измерение значения оборотов двигателя осуществлялось с помощью герконового датчика который генерировал импульсы при вращении ротора. Чтобы преобразовать последовательность импульсов в значение нам опять понадобится аппаратный таймер который инкрементируется от импульсов на заданной входной линии, но нам нужен еще и постоянный период, в течении которого нам надо регистрировать эти импульсы. И тогда по прерыванию от задающего период таймера мы должны прочитать значение счета регистрирующего таймера и сбросить его для нового измерения. Это полученное значение количества оборотов мы должны использовать для вычисления корректировки уровня мощности для двигателя.
На мой взгляд, в этом случае, почти очевидно, что чтение значения регистрирующего таймера необходимо выполнить немедленно, то есть внутри функции прерывания, а вот вычисление корректировки можно выполнять в фоновой программе, даже если мы не успеем закончить вычисления до получения нового значения мы сможем использовать результат вычислений, а это новое полученное значение мы можем просто пропустить, то есть таким образом можно запускать вычисления по каждому результату измерения или, например, по каждому второму результату измерения. Но самое главное фоновая программа, которая выполняет вычисления сможет контролировать, успевает она закончить работу до завершения очередного измерения или нет, просто проверив наличие нового принятого значения и если оно уже поступило, значит вычисления не успевают выполняться за период измерения, в этом случае можно просто ждать следующего результата, чтобы система не пропускала результаты измерения по непонятному закону, а просто работала с пониженной частотой измерений. Для алгоритмов управления стабильность частоты измерения критически важна, если она будет нарушаться каким-то непредсказуемым образом система управления скорее всего в определенные моменты времени будет вести себя неадекватно, поэтому очень важно контролировать стабильность частоты измерений и вообще стабильность повторения всего цикла измерение/вычисление/применение! Контролировать стабильность повторения всего цикла можно только при правильном разделении процессов обработки прерываний на немедленную и отложенную части.
Промежуточное заключение
На этом пожалуй надо, как минимум, сделать перерыв, потому что без обратной связи писать на такие темы не только тяжело, но даже в некотором смысле рискованно. Есть риск что изложение уходит в какие-то дебри или тонет в каких-то никому не нужных, не интересных деталях. Я конечно собирался рассказать и о том как эта задача, управление двигателя (несколько подзадач), совмещаются например с задачей обработки пользовательского ввода-управления, задачей индикации, опроса остальных датчиков или управления другими, более простыми исполнительными устройствами на фоне этой самой сложной задачи, но материала получилось уже слишком много и мне хотелось бы понять стоит ли продолжать этот пересказ программы и схемотехники словами :).
Всем добра и успехов в программировании!
Комментарии (3)
apcs660
29.09.2025 00:22Выкапывание картошки скорее, конкурентно, а не параллельно, но это вопрос терминологии, от меня плюс
STMshchik
29.09.2025 00:22Сильно в дебри не зашли. Конечно же стоит продолжить изложение этих идей. По ощущениям вы приближаетесь к описанию операционной системы реального времени, либо неких самопальных планировщиков задач. Мне лично интересно почитать о подходах в программировании и держать руку на пульсе, проверить самого себя, насколько "адекватно" я всё организую в своих проектах и не забрел ли я там в дебри.
vadimr
Выкапывание картошки асинхронно по отношению к написанию статьи вообще независимо ни от чего, так как они не требуют синхронизации между собой. При этом они могут выполняться параллельно или нет (по очереди).