Что такое параметры устройства
Параметр (техника)
Технический пара́метр — физическая величина, характеризующая какое-нибудь свойство технического устройства, системы, явления или процесса. Число, характеризующее этот параметр (величину), является его значением.
Параметр — это обобщенное название определенного физического, геометрического или иного свойства устройства (процесса). Это могут быть, например, размер, скорость, напряжение и т. д. Изучением видов параметров, измерений, методов и средств обеспечения их единства и способов достижения требуемой точности занимается метрология.
Содержание
Виды технических параметров
Параметры подразделяются на входные, внутренние и выходные.
Входные (внешние) параметры отражают внешние требования к техническому устройству (процессу), их значения или характер изменения с той или иной точностью известны. Часть этих параметров, существенно влияющих на состояние и характеристики устройства (процесса), называют управляющими.
Часть входных параметров, которые характеризуют выполняемую устройством (процессом) функцию, относят к функциональным параметрам. Эти параметры в процессе проектирования известны.
Внутренние параметры характеризуют состояние и свойства самого устройства (процесса). Их значения определяются или уточняются в процессе проектирования. Они необходимы для обоснования принимаемых решений, характеристики свойств устройства и других целей.
Часть входных параметров и рассчитанных внутренних параметров устройства (процесса) может использоваться в качестве исходных данных для другого, взаимосвязанного устройства (процесса) или его модели. Такие параметры называются выходными параметрами для рассмотренного устройства (процесса) и входными — для вновь рассматриваемого.
Например, для устройства «лифт» входными параметрами будут, например, масса груза (функциональный параметр) и высота его подъёма, срок службы (они задаются, приходят извне), а внутренними, например, диаметр и материал троса, размеры кабины лифта (они определяются, характеризуют устройство и вначале неизвестны). Для устройства «шахта лифта» ранее найденные размеры кабины лифта будут входными параметрами и, следовательно, — выходными параметрами для устройства «лифт».
Некоторые параметры могут выступать в виде обобщённых параметров, объединяющих в себе ряд свойств. Эти параметры применяют, когда излишняя конкретизация при решении задачи не требуется, либо вызывает потребность в дополнительных специальных знаниях. Однако при таком параметре должна быть ссылка на документ, однозначно раскрывающий его содержание.
Например, марка (название) материала: сталь 45 ГОСТ 1050-88 «Прокат сортовой, калиброванный, со специальной отделкой поверхности из углеродистой качественной конструкционной стали. Общие технические условия». Она содержит данные о составе, условиях изготовления и иных свойствах материала и является обобщённым параметром, скажем, для проектировщика, но не для материаловеда или металлурга.
В зависимости от того, что характеризуют параметры — реальное устройство (процесс) или его модель, параметры подразделяют на нормированные и действительные.
Нормированный параметр
Нормированный параметр (или, более правильно, нормированное значение параметра) — это теоретическая величина, значение которой устанавливается нормативно-техническими документами и характеризует признаки модели соответствующего технического устройства. Выражается предельными допустимыми значениями параметра. Изделие, параметры которого будут находиться внутри интервала, образованного этими предельно-допустимыми значениями, считается работоспособным и может использоваться по назначению.
Например, длина стержня, указанная на чертеже, составляет 98…104 мм. Это — нормированное значение параметра, установленное чертежом, а 98 и 104 — предельно-допустимые его значения (наименьшее и наибольшее предельно-допустимые значения параметра).
Если одно из предельных значений равно нулю или бесконечности, то оно не указывается, а подразумевается. Например, твёрдость поверхности детали не менее НВ180, что означает 180…∞. Или, например, поднимаемый груз — 200 кг, что соответствует 0…200.
Для марки материала, например, стали, предельно-допустимые значения содержатся в соответствующем ей ГОСТе.
Величина интервала, ограниченного предельными значениями параметров, называется допуском параметра. Он обозначается буквой T (в предыдущем примере Т = 104–98 = 6 мм). Сама же область допустимых значений параметров называется полем допуска.
Действительный параметр
Действительный параметр (или действительное значение параметра) характеризует признаки конкретного реального изделия. Его определяют путем испытаний [1] или измерительного эксперимента с точностью, достаточной для контроля этого параметра.
Обычно каждое замеренное действительное значение уникально, так как его величина зависит от внешних условий, условий изготовления, способа и точности измерения и многих других факторов. С целью повышения достоверности знания значения параметра проводят ряд измерений, результаты которых будут иметь разброс внутри какого-то интервала. По этой причине действительное значение параметра задают диапазоном. Совпадение действительных значений одних и тех же параметров изделий из их партии возможно только в пределах точности измерения.
Например, измерениями была установлена длина стержня 97…98 мм. Это — действительное значение параметра, истинное значение которого лежит внутри диапазона, заданного суммарной погрешностью измерения. Повышение точности измерений сузит данный диапазон, например, до 97,6…98,1 мм.
Точность оценивается погрешностью измерения, которая представляет собой разность между действительным и истинным значениями параметра. За истинное значение параметра принимается идеальное значение, к которому стремится действительное значение параметра при повышении точности измерения. Истинное значение не может быть определено экспериментально, поскольку все средства измерения имеют некоторую погрешность измерения. Вместо истинного значения для оценки погрешности измерения берут действительное значение параметра, определенное другим средством измерения, погрешность которого на порядок меньше допустимого значения для данной цели.
Погрешность измерения включает в себя составляющие, причинами возникновения которых являются средства измерения, метод измерения и оператор (субъект).
Номинальный параметр
Для удобства записи параметров используют номинальный [2] параметр (номинальное значение параметра), то есть такое его значение, которое служит началом отсчета действительных и предельно допустимых отклонений. Субъективно назначается человеком либо является результатом операций с такими же номинальными параметрами.
Например, длину стержня, указанную на чертеже, можно записать как 101±3 мм. Здесь 101 — номинальное значение, ±3 — отклонения, задающие предельные значения параметра (98…104). В приведенном примере номинальное значение выбрано из середины интервала и, как следствие, отклонения будут симметричными. Если в качестве номинального значения принять «круглую» величину 100, то форма записи данного нормированного параметра примет, например, следующий вид , где +4 — величина верхнего предельного отклонения (100+4), −2 — нижнего (100+(-2)).
Номинальным параметром можно считать марку материала, приведённую без ссылки на соответствующий ГОСТ, например, сталь 45.
Часто оперируют только с номинальными значениями параметров, например, указывают длину стержня как 100 мм. Решать уравнения с параметрами, заданными в таком виде, удобнее, хотя теряется ощущение точности не только исходных данных, но и результата вычислений.
Однако изделие считается годным, если действительные значения его параметров попадают в интервал, задаваемый предельными значениями нормируемого параметра. Если указано только номинальное значение нормируемого параметра, то формально значение интервала равно нулю и попасть в такой интервал практически невозможно и, следовательно, каждое изделие по этому параметру будет бракованным. Поэтому в документации (особенно предназначенной для других пользователей — заказчика, исполнителя, покупателя, других специалистов) принято приводить нормированные значения параметров, а не указывать только их номинальные значения.
Для устранения излишнего многообразия номинальных значений параметров их рекомендуют нормировать, то есть приводить в соответствие (например, округлять расчетные значения) с предпочтительными числами.
Оценка значения технического параметра
Значения параметров могут оцениваются следующим образом:
Храним настройки правильно или реестр параметров для встраиваемых систем
Привет, Хабр! В нашей практике разработчиков электронных устройств и встраиваемых систем мы часто сталкиваемся с необходимостью хранить параметры устройства. Это могут быть, например, такие параметры как яркость дисплея, язык, рабочая частота радиоканала или IP адрес – да что угодно. Единого общепринятого подхода для решения этой задачи нет, и я предлагаю обсудить ниже наиболее очевидные варианты, их достоинства и недостатки, а также предложить реализацию, к которой я пришел в результате работы над несколькими проектами. Поехали!
Немного общей информации о ПЗУ.
Для того, чтобы хранить данные при выключенном питании люди придумали ПЗУ (постоянное запоминающее устройство). В мире Embedded зачастую используется встроенное в микроконтроллер ПЗУ либо внешнее (по отношению к процессору или микроконтроллеру) ПЗУ в виде микросхемы, подключаемой к по интерфейсам SPI или I2C. Физические особенности реализации ПЗУ в кремнии таковы, что из ПЗУ можно читать информацию быстро и по произвольным адресам, а вот для записи данных нужно ПЗУ сначала стереть. Стирать можно всю микросхему или же фрагмент. Фрагмент зависит от организации памяти внутри, это может быть сектор, страница или даже байт. Впрочем, время стирания одного байта, как правило, равняется времени стирания страницы, что наталкивает на мысль о том, что внутри все-таки стирается полностью страница, просто неявно. Вот операция стирания как раз самая долгая. Для того, чтобы программа не ждала готовности ПЗУ, придумывают разные ухищрения – кеш в ОЗУ и отложенную запись.
Предположим, у нас есть некое ПЗУ, из которого мы можем считывать информацию, записывать, и программный интерфейс которого представлен двумя функциями:
На данном этапе будем считать, что нас не волнуют особенности записи в ПЗУ и функция записи делает все, что нужно, включая стирание. Имея эти две функции, мы можем прочитать или записать произвольное количество байт по произвольному адресу. С другой стороны, есть задача хранить некие параметры в этом ПЗУ и иметь возможность работать с ними (читать / записывать). Давайте подумаем, как это можно сделать.
Вариант, пожалуй, самый очевидный. Давайте подумаем, что здесь плохо:
Адреса захардкожены. Нужно внимательно контролировать где какой параметр лежит и сколько он занимает байт. При большом количестве параметров очень трудно не допустить ошибку.
Для исправления адресов нужно искать обращения к ПЗУ по коду всего проекта
Информация, хранящаяся в ПЗУ, неструктурирована
Отсутствует контроль перекрытия адресов. Можно невозбранно записать байт в середину 4-байтового числа
Нет контроля ошибок. Валидность записываемых данных никак не проверяется
Сериализация / десериализация многобайтовых данных возлагается на компилятор и зависит от endianness вычислительной платформы
В общем случае код непереносим на другую платформу
Ну а что же хорошего?
Самый простой и быстрый способ.
Самая эффективная реализация сериализации / десериализации данных – благодаря компилятору.
Такой подход годится, если у вас 1-2 переменные в коде. Ладно, я пошутил, так, скорее всего, никто писать не будет. Даже самый начинающий программист быстро поймет, что лучше бы адреса определить где-нибудь в виде набора макроопределений. Вот так:
Ну и коде вместо магического числа будет красоваться чуть более осмысленное выражение:
Можно еще размер параметров в байтах аналогично объявить. Что это нам дает по сравнению с первым вариантом:
Адреса переменных и их размер сосредоточены в одном месте, их теперь проще редактировать
Меньше магических чисел
Но в целом все те же недостатки. А что если все данные объединить в структуру? Например, такую:
Уже лучше. Что хорошего нам дает объединение данных в структуру:
Лаконичность. Одной строчкой мы записываем или считываем множество данных
Сохраняется эффективность сериализации / десериализации, унаследованная от первого варианта
Весьма простая реализация
Появляется возможность очень лихо мультиплицировать однотипные параметры (достаточно объявить в структуре массив элементов, при этом сами элементы уже могут быть достаточно сложными)
Контроль за адресами параметров теперь возложен на компилятор. Нам только остается определить адрес в ПЗУ, по которому располагается структура и ее размер, если мы планируем что-то еще хранить там же
При модификации одного поля требуется перезаписать полностью все данные
Увеличенное время работы с ПЗУ. Недостаток усугубляется, если параметров много
По-прежнему нет никакого контроля ошибок. Требуется вручную проверять все поля на допустимость значений
Давайте подумаем, что здесь можно улучшить. Во-первых, логично держать параметры в ОЗУ, считывая их при запуске и записывая при выключении устройства или по какому-нибудь событию. Во-вторых, можно добавить контроль целостности для всех параметров, вычисляя CRC16 или CRC32 побайтно и записывая в ту же ПЗУ.
Что общего у всех перечисленных вариантов:
Работа с ПЗУ ведется непосредственно из кода приложения. При необходимости заменить названия функций или целиком модуль работы с ПЗУ, требуется довольно бесполезная правка application кода
Много информационного шума. Названия функций доступа к ПЗУ, адреса и размеры переменных, приведения типов – все это лишняя информация, который мешает сосредоточиться на функциональном коде
Требуется отдельная явная проверка записываемых и считанных данных на валидность
Затруднен доступ к параметрам через внешний интерфейс. Если нужно считывать или записывать отдельный параметр, в протоколе обмена с внешним миром придется так или иначе дублировать описание структуру вашего набора параметров. Ну или гонять по интерфейсу полный набор байт – сериализованное представление нашей структуры с параметрами
В целом все вышеперечисленные варианты вполне приемлемы, если количество параметров относительно невелико. Это правда, если у вас простая программа, которую не предполагается переносить на другой МК или менять компилятор, лучше не усложнять. Но что делать, если код ответственный, параметров много, и их организация сложнее чем просто массив? Давайте подумаем, что хотелось бы от идеального решения (в чем-то перекликается с принципами ООП, да):
Изолированность от железа. Программный модуль не должен включать в себя функции работы с ПЗУ
Переносимость. Модуль должен одинаково работать на микроконтроллере и ПК в каком-нибудь Qt Creator
Изолированность внутренней реализации. Для работы с модулем должен быть определен интерфейс из набора функций
Возможность сложной организации данных. Возможность объединения параметров в группы, создание массивов групп в произвольном виде
Автоматическое распределение памяти ПЗУ для хранения переменных
Отсутствие избыточности при хранении данных. Упаковка в памяти должна быть плотной – если параметр состоит из 3 байт, он и в ПЗУ должен занимать 3 байта
Удобный API для передачи параметров через коммуникационные интерфейсы
Возможность хранения разных типов данных. Целые числа, числа с плавающей запятой, строки + возможность расширения новыми типами данных
контроль целостности данных
контроль валидности данных
наличие callback при изменении параметра
детерминированное время доступа к параметру
язык С для применения в чисто C проектах
Итак, переходим к самому интересному. Определим основные постулаты реализации:
В основе модуля лежит древовидная структура дескрипторов и линейный массив байт, на который дескрипторы ссылаются
В дереве существуют терминальные и нетерминальные узлы
Терминальные узлы дерева описывают некий параметр. Описание состоит из адреса сериализованного представления в ОЗУ, указателей на функцию – обработчик запросов, значений по-умолчанию, минимального и максимального значения и других данных. Определим полный набор описания позже
Нетерминальные узлы описывают терминальные и нетерминальные узлы, находящиеся ниже по иерархии (входящие в данный узел)
Доступ к параметрам осуществляется путем перехода по ветвям дерева, при этом ветвь определяется индексом на текущем уровне иерархии
Данные хранятся в ОЗУ в сериализованном виде. При чтении данные десериализуются в значение нужного типа. При записи – сериализуются
В принципе все. На этом списке статью можно было бы на этом закончить. Но давайте посмотрим в деталях, как это можно реализовать.
Перечислим типы узлов:
sNode – (сокр. Simple node) это терминальный узел. hNode (hierarchy node) – узел, включающий в себя узлы разных типов (структура). lNode – узел, включающий в себя узлы одного типа (массив). Имея эти три типа, мы можем описать сколь угодно сложное дерево. Проиллюстрируем на примере (буквенно-цифровые обозначения нужны только для пояснения, но они же будут использованы в примере для пущей ясности):
Это простое дерево, описывающее несколько терминальных узлов и переходы к ним. Дерево начинается с корневого узла А0, на следующем уровне иерархии содержит три узла – B0, B1 и B2. Из них B2 – терминальный узел, остальные содержат вложенные узлы внутри себя. B0 – промежуточный узел иерархии, B1 – тоже промежуточный, но содержащий массив однотипных элементов внутри. Узлы C – терминальные. Например, если мы хотим обратиться к узлу C25, нужно выбрать последовательность переходов (1, 5), начиная от корня. Вроде не rocket science, едем дальше.
Итак, узлами дерева у нас являются дескрипторы. Определим общий для всех дескрипторов набор полей:
У любого узла есть тип, а также смещение в ОЗУ и в ПЗУ. Это смещение мы руками задавать не будем, иначе теряется весь смысл. Оно будет вычисляться один раз при инициализации.
Описание узла иерархии (hNode):
В hNode входят общие для всех узлов поля (совершенно верно, такое вот «наследование» в С), размер списка вложенных узлов и указатель на список. Заранее количества вложенных узлов мы не знаем, поэтому оставим просто указатель.
Здесь мы указываем размер массива, дескриптор вложенных элементов (они все одного типа, поэтому нужен только один дескриптор), размер элементов в ОЗУ и в ПЗУ. Тут нужно пояснить, зачем нужны отдельные смещения для ОЗУ и ПЗУ. Дело в том, что встречаются параметры, которые должны существовать, но их не нужно хранить в ПЗУ. Простейший (хотя, возможно, и не самый удачный) пример – версия ПО. На такие элементы расходовать драгоценное ПЗУ мы не будем. Прежде чем перейти к дескриптору sNode, давайте еще определим, как у нас будут описываться свойства параметров, в зависимости от их типа. Сделаем это для целых чисел и строк. При желании этот список можно будет расширить.
Для целых у нас есть значение по-умолчанию, минимальное и максимальное значения. Для строк и того меньше – только значение по-умолчанию.
Итак, дескриптор параметра:
У любого параметра есть его размер в байтах (size), признак хранения (хранится он в ПЗУ или нет), callback на изменение, обработчик запросов, а также специфичные для типа данные. Кроме того, есть еще указатель уровня доступа – пользователь / разработчик / whatever. Пригодится для защиты от несанкционированного изменения. Ну и само собой тип и смещение в ОЗУ / ПЗУ.
Целостность обеспечивается контрольной суммой CRC16, которая вычисляется для hNode или lNode по всем терминальным узлам, входящим в него (и только по ним – вложенные узлы не используются в расчете CRC для данного узла). CRC16 кажется разумным выбором, но ничто не мешает использовать CRC32 или сумму Флетчера или другой тип контрольной суммы.
Для hNode или lNode в ОЗУ хранятся два байта CRC16, а затем все данные всех терминальных узлов на данном уровне иерархии, пусть даже они описаны в дереве с промежутками.
Все иерархические узлы, входящие в данный hNode, хранятся аналогично, но уже на своем уровне.
Смещение для всех узлов вычисляется при инициализации дерева. Потребуется функция, которая обойдет все дерево и всем узлам задаст смещение. Потом, при обработке запроса, мы будем переходить по узлам и просто суммировать все смещения, получая в итоге нужный адрес.
Изобразим карту памяти для дерева выше:
В первый уровень иерархии дерева входят узлы B0, B1 и B2. Из них B0 и B1 пропускаем, потому что они нетерминальные. Значит, для первого уровня записываем только данные узла B2. А дальше по порядку анализируем узлы B0 и B1 и действуем аналогично. Проходя таким образом все дерево, получаем карту памяти (смещения) для всех узлов дерева. Эти смещения мы записываем в соответствующие поля дескрипторов чтобы иметь их под рукой при обращении к параметрам. Алгоритм достаточно прост, поэтому блок-схему приводить не буду. Сразу код.
Функция рекурсивная, для ограничения глубины (читай, количества вложенных вызовов) в случае, если что пошло не так, а также подсчета максимальной глубины дерева в качестве параметра передается структура:
При вызове из основной программы в качестве узла передается дескриптор корневого узла дерева (в нашем примере это дескриптор узла А0), указатели на переменные смещения ОЗУ и ПЗУ (да, можно было бы их упаковать в nodeInitContext_t, но так уж сложилось) и собственно контекст инициализации. Например:
После вызова этой функции карта памяти размечена, мы можем легко узнать адрес сериализованного представления каждого параметра и можем приступать к валидации данных. Для валидации мы снова пробегаем по дереву, для каждого терминального узла вызываем его обработчик с командой восстановить и проверить данные (здесь идет массовое чтение данных из ПЗУ). Если все данные узлов в hNode валидны, в заключение проверяется контрольная сумма. Если она совпадает, то все хорошо, считаем, что параметры для конкретной hNode валидны. Если нет, записываем рассчитанную контрольную сумму в ПЗУ вместо старой. Кроме того, может понадобиться сбросить все параметры на значения по-умолчанию. Тогда принудительно всем параметрам задаем значение по-умолчанию и обновляем контрольную сумму. Функция валидации также вызывается рекурсивно.
Вызывать мы ее будем вот так:
Здесь еще нужно обратить внимание вот на что. Мы говорим, что у нас есть callback-функции, которые вызываются при выполнении запроса, например, записи нового значения. Может случиться так, что удобно будет использовать одну callback функцию для нескольких терминальных узлов, например, массива. Но этой функции может потребоваться знать, с каким именно параметром она имеет дело и какое значение было записано. Я решил не мелочиться и записывать все индексы при переходе по дереву в отдельный массив argHistory, а значение в union callbackCache. Callback знает для какого параметра его вызвали и может правильно привести тип callbackCache.
Итак, с деревом и внутренней структурой более-менее все. Приведу два обработчика для определенных нами в начале типов – целого 32-битового числа и строк.
Обработчик для 32-битных целых чисел
Обработчик для строк
Определены следующие типы запросов (не фантазия, продиктованы требованиями одного из проектов):
В общем случае параметр можно прочитать, получить его максимальное и минимальное значения, получить размер в байтах, проверить на валидность. Также можно записать (в ОЗУ и ПЗУ), применить (только в ОЗУ), записать или применить без вызова callback или записать только в ПЗУ. Как мы ранее говорили, доступ к модулю осуществляется через функцию, обрабатывающую запрос. Сам запрос определен как структура:
При обращении к модулю мы указываем тип запроса, перечисляем индексы для доступа к параметру и говорим, нужно ли нам сериализованное представление или десериализованное. Последнее позволяет легко передавать параметры через интерфейсы связи. Сама функция обработки запроса довольно тривиальна:
Функция обработки запроса
По сути, в ней мы переходим по элементам массива, вычисляем элементарным сложением смещение параметра, выполняем различные параноидальные и не очень проверки и в конце концов доходим до терминального узла и вызываем его обработчик запроса. Все просто 🙂
Ладно, давай уже как этим пользоваться…
Перед использованием нужно сконструировать это самое дерево дескрипторов. В начале я написал несколько функций, с помощью которых удобно создавать дескрипторы. Типа такой:
А sNode создавалась при помощи вот этого:
SETTINGS_ALLOCATE можно определить в заголовочном файле как обычный malloc или же чуть сэкономить на дескрипторах и воспользоваться незатейливым менеджером памяти, который умеет только выделять – кто же не хочет изобрести свой велосипед менеджер памяти:
Все эти функции нужны чтобы легко и непринужденно создавать описание дерева параметров в таком духе:
И все бы ничего, но эти рюшечки могут съесть довольно значимый кусок памяти (и таки съели в одном проекте, где с памятью совсем плохо), так что я их вынес под условную компиляцию ENABLE_NODE_CONSTRUCTORS и запасся вместо них такого рода макроопределениями:
Давайте посмотрим, как будет выглядеть описание дерева параметров для нашего примера выше (пусть у нас будет C2_NODES_COUNT штук узлов C2), определенное с помощью этих макросов:
Как обращаться из кода.
Допустим, нам нужно получить значение параметра С0. Мы должны сделать следующее:
Запись 12-го элемента из массива элементов C2:
Проверка допустимости значения элемента B2:
Длинно? Длинно. Тут есть простор для фантазии. Можно написать функцию – обработчик запросов с неопределенным количеством аргументов, или с++ обертку с предопределенным количеством аргументов со значениями по-умолчанию. В одном из проектов максимальная глубина дерева параметров была равна 4 и я использовал такую функцию:
Возвращаемся к началу. Стратегии работы с ПЗУ.
Ну что же, вроде все поставленные цели достигнуты. Давайте, однако, будем справедливы и отметим, чем может быть плох или неудобен получившийся программный модуль:
Накладные расходы. Все-таки, хотя вычислительная сложность при обращении к параметру стремится к О(n) (где n – это глубина дерева), все-таки они есть. Ну что же, неизбежная расплата.
Несколько перегруженный интерфейс для обращения (нужно заполнить поля структуры). Решается введением удобных функций – оберток.
Определенные ограничения на алгоритм работы с ПЗУ. Модуль никак не учитывает особенности работы ПЗУ, перечисленные в начале статьи. Например, ПЗУ может уметь стирать только сектор, понадобится теневое копирование всего сектора. Но это есть следствие разделения обязанностей модулей. Вопрос на будущую проработку.