Здравствуйте, коллеги! Готова очередная публикация из неформальной «Книги знаний» ОСРВ МАКС.

Просьба к постоянным читателям отнестись ко мне лояльно и не минусить за небольшое повторение части материала из предыдущей статьи (про защиту стека) — здесь оно оказалось логичней. А там я уже удалил.

Общее содержание (опубликованные и пока неопубликованные статьи):

Часть 1. Общие сведения
Часть 2. Ядро ОСРВ МАКС
Часть 3. Структура простейшей программы
Часть 4. Полезная теория (настоящая статья)
Часть 5. Первое приложение
Часть 6. Средства синхронизации потоков
Часть 7. Средства обмена данными между задачами
Часть 8. Работа с прерываниями

Некоторые неочевидные сведения о данных


Несколько фактов о куче


Многие программисты почему-то считают, что операции new и delete достаточно легковесны и просты. Поэтому код часто изобилует выделением и освобождением динамической памяти. Это более-менее приемлемо на мощных системах (гигабайты ОЗУ и гигагерцы тактовой частоты), но при ограниченных ресурсах может создавать некоторые проблемы, особенно для программ, работающих в режиме 24/7.

  • Самая очевидная проблема — фрагментация адресного пространства. В современной среде .NET массивы выделяются на куче по ссылке. Поэтому в любой момент времени, система может остановить работу программы и заняться сборкой мусора. При этом массивы вполне могут быть сдвинуты внутри памяти, ведь обращение к ним будет идти по ссылке, а таблица ссылок будет скорректирована. Язык С++, на котором чаще всего пишут для микроконтроллеров (как, собственно, и чистый Си, да и ассемблер) работает с массивами через указатели. Сколько указателей имеется в памяти — никто не знает (программа пользователя имеет право их копировать, передавать в качестве аргументов, даже складывать и вычитать). Значит данные двигать нельзя. Где массив или структура были выделены, там они и будут закреплены до самого удаления. Ну, и дальше возникает классический случай, когда в результате выделений и освобождений куча примет, скажем, такой вид:

    Иллюстрация классического случая проблем, возникающих из-за фрагментации адресного пространства

    Рис. 1. Иллюстрация классического случая проблем, возникающих из-за фрагментации адресного пространства

    В этом случае, суммарный объём свободной памяти больше, чем требуется для выделения, но выделить его невозможно — нет фрагмента требуемого размера. И дефрагментацию произвести невозможно;
  • Однако, есть более серьёзная проблема. На самом деле, для того, чтобы выделить память, следует перебрать достаточно большое количество таблиц (будем называть это таблицами, хотя разумеется, реализация менеджера памяти может быть и на списках), чтобы найти фрагмент подходящей длины (само собой, речь идёт о случае, когда система работает давно, а выделения и удаления происходят часто — чем больше фрагментов, тем больше элементов таблицы следует перебрать). То есть, операция выделения памяти не такая и дешёвая как по быстродействию, так и по ресурсоёмкости — таблицу выделения памяти где-то следует хранить;
  • Более того, операцию выделения памяти нельзя отнести к операциям реального времени. Её быстродействие невозможно предсказать. Возможно, она выполнится достаточно быстро (требуемая запись будет найдена быстро), но может быть — и достаточно медленно, а системы реального времени подразумевают гарантированное быстродействие. Само собой разумеется, это не проблема операционной системы, это проблема плохо спроектированной прикладной программы, так как её автор не учёл этого стороннего эффекта;
  • Наконец, на время выделения памяти блокируется переключение потоков, чтобы обеспечить потокобезопасность операции.

Из всего этого следует, что выделять память на куче следует с крайней осторожностью. В идеале, это следует делать на этапе инициализации программы. Если же требуется выделять память по ходу работы, то лучше это делать как можно реже. Не стоит увлекаться постоянным выделением и освобождением. Также стоит опасаться операций, которые выделяют память неявно, внутри себя. Меня коробит от кода подобного вида, особенно если учесть, что он исполняется в системе, где на всё про всё выделено 50 килобайт ОЗУ:

    String output;
    if (cnt > 0)
      output = ',';

    output += "{\"type\":\"";
    output += (entry.isDirectory()) ? "dir" : "file";
    output += "\",\"name\":\"";
    output += entry.name();
    output += "\"";
    output += "}";

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

char xml [768];
...
      xml[0] = 0;
      if (cnt > 0)
      {
        strcat (xml,",");
      }
  
      strcat (xml,"{\"type\":\"");
      if (entry.isSubDir())
      {
        strcat (xml,"dir");
      } else
      {
        strcat (xml,"file");
      }
      strcat (xml,"\",\"name\":\"");
      entry.getName(xml+strlen(xml),255);
      strcat (xml,"\"}");


Этот код специально сделан «в лоб», чтобы чётко показать, что после его добавления, не стало опасности фрагментации адресного пространства, а также было бы точно видно, что на что заменено. Но, до совершенства ему ещё далеко. Начнём с того, что в нём в угоду наглядности попраны принципы ООП, а продолжим тем, что функция strcat каждый раз перебирает строку-приёмник с начала, что отрицательно сказывается на быстродействии. Чисто теоретически, строка-приёмник также может переполниться (хотя в данном конкретном примере, защита от переполнения находится в функции entry.getName).

Приведём вариант, предложенный comdiv, лишённый указанных недостатков.

Опишем класс для работы со статической строкой, содержащий, среди прочего, указание текущей длины, что позволит не начинать осмотр строки каждый раз с начала. Для простоты, реализуем в этом классе только оператор "+=". Именно на этот класс ляжет смысловая нагрузка нового варианта примера.

class StaticString
{
protected:
	char*	m_buf;		// Указатель на строку
	int		m_size;		// Максимальный размер строки
	int		m_len;		// Текущий размер строки

public:
	StaticString (char* buf,int size)
	{
		_ASSERT(NULL != buf);
		_ASSERT(size > 0);

		m_buf = buf;		// Указатель на статический буфер
		buf[0] = '\0';		// Терминатор
		m_size = size;		// Размер буфера
		m_len = 0;			// Пока длина строки - нулевая
	}
	StaticString& operator+=(const char *str) 
	{
		int i = 0;
		// Пока есть, куда складывать и пока в источнике не терминатор
		while ((m_len < m_size - 1) && (str[i] != '\0')) 
		{
			// Скопировали очередной символ
			m_buf[m_len++] = str[i++];
		}
		// Терминировали, ведь у нас всё-таки сишная строка
		m_buf[m_len] = '\0';
		return *this;
	}

};


А основной код снова примет знакомый вид, отличающийся только объявлением переменной output, «оборачивающей» строку xml:

char xml [768];
...
      StaticString output (xml,sizeof(xml));
    if (cnt > 0)
      output += ',';
    output += "{\"type\":\"";
    output += (entry.isDirectory()) ? "dir" : "file";
    output += "\",\"name\":\"";


Но за счёт применения другого класса, опасность фатальной фрагментации адресного пространства — миновала. И в отличие от «лобового» решения — оптимизировано быстродействие и устранена опасность переполнения буфера строки.

Совершенствовать класс можно долго (сейчас в нём перекрыт только один вариант оператора "+="), но это уже скорее относится к руководствам по программированию вообще, а не к руководству по ОСРВ МАКС. А пока — просто отмечу, что какой бы из вариантов замены («на скорую руку, но наглядный» или «правильный, но более сложный») ни был бы выбран, они иллюстрируют одну и ту же идею:

Если можно отказаться от постоянного обращения к new/delete, то лучше это сделать.

Кратко про стековые переменные


Мне часто приходилось встречаться с программистами, которые не знают, как именно реализуются локальные переменные в языках Си и С++. При этом, те программисты прекрасно осведомлены, что такое стек, а также — о том, как в него сохраняется содержимое регистров (которые будут испорчены) и адреса возврата из подпрограмм (правда, в архитектуре ARM адрес возврата попадает в регистр LR). Возможно, это связано с тем, что все эти программисты закончили один и тот же ВУЗ (чего уж греха таить, я сам закончил его же, и ещё лет 10 назад тоже до конца не представлял себе, что такое стековый кадр). Тем не менее, будет полезным кратко обрисовать, как же эти загадочные локальные переменные хранятся. В конце раздела, будет раскрыта интрига, каким образом это относится к ОСРВ МАКС.

Итак. Оказывается, стек используется не только для хранения адресов возврата (правда, не у ARM) и временного сохранения содержимого регистров процессора. Стек используется также для хранения локальных переменных.

Посмотрим, как выглядит типичная преамбула функции, у которой этих локальных переменных настолько много, что они не помещаются в регистрах

;;;723 static void _CopyRect(int LayerIndex, int x0, int y0, int x1, int y1, int xSize, int ySize) {
000000 e92d4ff0 PUSH {r4-r11,lr}
000004 b087 SUB sp,sp,#0x1c

Первая инструкция PUSH — с нею всё ясно. Она как раз сохраняет регистры в стеке, чтобы перед выходом их восстановить. А что это за вычитание константы 0x1C из SP? А это как раз выделение стекового кадра. Из курса информатики известно, что стек — это такая вещь, которая адресуется не непосредственно, а относительно указателя на вершину стека. Рассмотрим графически, что сделают эти две строки.

Рис. 2. Влияние преамбулы функции на стек

Рис. 2. Влияние преамбулы функции на стек

Что это за стековый кадр? Всё просто. Его размер таков, чтобы в нём уместились все локальные переменные функции (кроме тех, которые оптимизатор положит в регистры). Размер стекового кадра вычисляется компилятором. Каждая переменная получает своё смещение относительно начала кадра, а обращение к ним идёт примерно так:

;;;728 BufferSize = _GetBufferSize(LayerIndex);
000016 4620 MOV r0,r4
000018 f7fffffe BL _GetBufferSize
00001c 9006 STR r0,[sp,#0x18]

Очевидно, что переменная BufferSize имеет смещение относительно начала кадра на 0x18 байт.

;;;730 SrcAddr = Offset + (y0 * _xSize[LayerIndex] + x0) * _BytesPerPixel[LayerIndex];
000030 4816 LDR r0,|L8.140|
000032 f8500024 LDR r0,[r0,r4,LSL #2]
000036 fb076000 MLA r0,r7,r0,r6
00003a 4915 LDR r1,|L8.144|
00003c 5d09 LDRB r1,[r1,r4]
00003e fb005001 MLA r0,r0,r1,r5
000042 9005 STR r0,[sp,#0x14]

А переменная SrcAddr — смещение 0x14

Ну, и так далее. Переменная же LayerIndex явно помещена не в стековый кадр, а в регистр R4.

Само собой, в конце своей работы, компилятор всё быстренько восстанавливает (а также помещает бывшее содержимое LR в PC, тем самым, переходя на адрес возврата)

00007e b007 ADD sp,sp,#0x1c
000080 e8bd8ff0 POP {r4-r11,pc}

Из всего этого, становятся ясны некоторые вещи:

  • Понятно, почему локальные переменные видны только внутри функции. Они адресуются относительно регистра SP, а во вложенных функциях (как и в функциях более верхнего уровня) SP будет другой.
  • Понятно, почему отладчик среды разработки KEIL не отображает некоторые переменные — почему-то разработчики не умеют показывать содержимое переменных, размещённых в регистрах.
  • Понятно, почему выход за границы массива, размещённого в локальных переменных может привести к полной неработоспособности программы — адрес возврата из функции хранится в том же стеке и вполне может быть испорчен.
  • Понятно, что рекурсивные функции тратят стек не только на адреса возврата, но и на стековые кадры. Чем больше локальных переменных, тем больше стека расходуется при рекурсивных вызовах. Этого лучше избегать при работе в условиях ограниченной памяти.

Самый главный вывод — после некоторых тренировок, программист сможет начать прикидывать, какой объём стека будет необходим задаче (исходя из прикидок о самой глубокой вложенности взаимного вызова функций и набора их локальных переменных). ОС Windows по умолчанию выделяет каждому потоку по мегабайту стека. При работе с микроконтроллерами, речь идёт о килобайтах. Причём эти килобайты выделяются на куче, поэтому уменьшают объёмы свободной динамической памяти и глобальных переменных, так что знание физики не просто полезно, а часто жизненно необходимо.

Защита стека задачи от переполнения


При создании задачи, определяется размер стека для неё. После этого, размер не может быть динамически изменён. Если он был выбран неудачно (по ходу работы образовалась большая вложенность вызовов, либо число локальных переменных оказалось высоко, что могло произойти уже при сопровождении программы), данные могут выскочить за выделенные пределы, повредив данные в стеках других задач, в куче, либо иные данные и производя прочие непредсказуемые действия. Такую ситуацию желательно выявить и сообщить разработчику, что она требует устранения.

Идеальным методом предотвращения такой ситуации была бы проверка на уровне компилятора, без участия ОС, но к сожалению, такой механизм как минимум, создаёт большие накладные расходы. Основная задача для контроллеров — не проверять программиста, а производить управление. При тактовой частоте в районе ста-двухсот мегагерц (а иногда — и десятков мегагерц), такой метод контроля уже неприемлем.

На уровне ОС также можно производить контроль стека на переполнение. В ОСРВ МАКС используются следующие методы защиты:

  • Проверка текущего положения указателя стека при переключении задач. Почти не влияет на производительность, но обладает низкой надежностью. Во-первых, разрушение стека уже произошло, а во-вторых – за время системного такта программа могла не только войти в функцию, вызвавшую переполнение, но и выйти из неё, а значит — указатель мог успеть вернуться обратно в разрешенный диапазон.
  • Если установлен размер стека больше минимального, то к нему автоматически добавляется одно слово на вершине, куда записывается «magic number» — 32 разрядное число случайного вида, которое вряд ли встретится при работе программы. При переполнении стека это число будет затерто данными приложения, что почти наверняка позволит зафиксировать факт переполнения стека даже после возвращения указателя в рабочую область.
  • В том случае, когда процессор содержит блок MPU (Memory Protection Unit), сразу за границей стека помещается область памяти минимально допустимого размера с защитой от доступа. Это самый совершенный способ контроля, так как при любом обращении к защищённой области, произойдет аппаратное прерывание. Следует, однако, помнить, что в некоторых случаях, защитная зона может оказаться не тронутой. Например, если часть локальных переменных, которые попали именно в эту зону, зарезервированы, но не используются. Защита сделана для самоконтроля и не должна идти в ущерб основным задачам.

Детали для работы с защитой стека можно найти среди констант, заданы в классе Task (в файле MaksTask.h). Изучая комментарии к этим константам, можно понять конкретные величины параметров «минимальный стек», «защищаемая область» и т.п. При желании, этим параметры можно и изменить. Следует только помнить, что размер защищаемой области должен быть степенью двойки.

Всё, наконец-то необходимый минимум теории, без которой невозможно начинать практические опыты, закончен.

Вообще, обычно авторы сначала описывают всё про предметную область, а уже в последней главе (или вообще, в приложении) приводят сведения о практической работе. Видимо, они считают, что читатели сначала всё запомнят, а уже потом — начнут эксперименты. Наивно предполагать, что средний человек запомнит всю массу вываленных на него знаний. Удобнее пробовать все знания постепенно.

Поэтому перед тем, как начать пичкать читателя дальнейшей теорией, стоит немного попрактиковаться. Но для этого следует рассмотреть, как же правильно начать работу с ОСРВ МАКС. Давайте будем считать, что читатель знаком с тем, как скомпилировать и запустить программу под имеющийся у него микроконтроллер, иначе текст будет сильно перегружен.

Если это не так, то крайне рекомендую ознакомиться с замечательными руководствами от среды разработки Keil по работе с отладочными платами ST (к сожалению, на английском языке, но многое понятно и из рисунков):

http://www.keil.com/appnotes/files/apnt_253.pdf

http://www.keil.com/appnotes/files/apnt_261.pdf

И в следующей статье мы приступим к первому практическому опыту.

Комментарии (16)


  1. Indemsys
    14.09.2017 21:06
    +1

    Классно вы переписали код с С++ на C-и при работе со строками.
    А я ведь говорил, что скатитесь к C-и и никуда не денетесь.

    Про функции работы с heap вы тоже под воздействием каких-то мифов подобранных из сомнительной литературы. Если бы сподобились измерить время выполнения malloc и free, то увидели бы что они в среднем всегда выполняется за 3 мкс. И разброс этого времени не превышает 2 раз при выделении 2000! блоков одновременно. Это наверняка больше чем вы когда либо выделяли в STM32.

    Программирование микроконтроллеров быстро прогрессирует, поэтому я бы на вашем месте не рисковал транслировать устаревшие знания.
    Давайте лучше цифры и результаты тестов.


    1. EasyLy Автор
      14.09.2017 22:00

      Классно вы переписали код с С++ на C-и при работе со строками.
      А я ведь говорил, что скатитесь к C-и и никуда не денетесь.

      По этому поводу, мне всегда вспоминается одно индийское видео. Там про программирование STM32. В куче частей. И вдруг к середине первой части автор начинает вместо рассказа про STM32 рассказывать про один приём программирования. И вся вторая часть посвящена ещё одному приёму программирования, никак не связанному с микроконтроллером. А надо-то про STM32 было! Видать, как и с индусским кодом, ему за хронометраж платят.

      Тут мне надо было показать суть замены. Вместо динамики — статика. А про то, что это лучше обернуть в класс — написано ниже. Если бы я начал рассказывать про класс, показывать его тело — всё было бы уже не так. Здесь же можно наблюдать точное соответствие «старое-суть нового». А в класс — ну разумеется, упаковать. И об этом в тексте сказано.


      Если бы сподобились измерить время выполнения malloc и free, то увидели бы что они в среднем всегда выполняется за 3 мкс.

      Это Вы крючкотворцам расскажите, которые будут доказывать, что ОС при этом станет не реального времени, так как производительность уже не специфицирована. Посему об этом в документации предупредить — обязательно нужно.

      Тем не менее, эта глава — всего лишь рекомендация. Операции new и delete как были, так и есть. Кто хочет ими пользоваться — может продолжать. Но пусть в уме держит возможные последствия. Мы предупредили, дальше — каждый сам решает.

      Результаты тестов — я у ребят спрошу, проводили ли они. Тем не менее, возможную фрагментацию адресного пространства никто не отменял, независимо от результатов тестов быстродействия. А приведённый пример, который мы обсуждаем — это кусок найденного в сети http сервера для IoT. То есть, он крутится круглосуточно без перезапусков.


      1. Indemsys
        15.09.2017 16:13

        Думаю сервисы вашей библиотеки функций RTOS будут менее детерминированными чем free и malloc.
        Хотелось бы посмотреть пример от вас где реально имела бы значение детерминированность вашей библиотеки функций реального времени
        Уже много лет не могу найти в интернете кто-бы действительно показал, как он учитывает детерминированность RTOS. Кроме RTOS MQX да ThreadX я не видел серьезных тестов детерминированности.


        1. EasyLy Автор
          15.09.2017 16:32

          В комментариях к предыдущим публикациям, я уже отмечал, что отвечаю за «художественное» руководство по ней. И данная публикация — именно «художественное» описание. Не будем пытаться объять необъятное здесь.

          Про new/delete в тексте сказано — за эти слова мне нужно отвечать, сейчас как раз уже будут метрики. А про такие серьёзные вещи — можете хоть сейчас на нашем сайте связаться с теми, кто за это дело отвечает. Ну, или когда мне поручат технический текст сделать — тогда я подготовлюсь и буду готов отвечать на такие вещи. Сейчас — не готов.


    1. Comdiv
      15.09.2017 00:34

      Я бы, скорее, сказал, что подобный код является отрицательным примером. strcat каждый раз подсчитывает длину строки, к которой он добавляет второй фрагмент, поэтому вычислительная сложность такого подхода оказывается квадратичной вместо линейной, в результате чего время выполнения этого кода может даже превысить время выполнения С++ кода. Второй недостаток заключается в отсутствии проверок границ, что может привести к тому самому нарушению стека. Конечно, в примере размер взят с избытком, но этого его не оправдывает, так как этот избыток тоже может послужить причиной повреждения стека, а документация должна быть всё-таки образцовой.


      1. EasyLy Автор
        15.09.2017 00:59

        «Выигрываем в скорости — проигрываем в пути», закон физики. В этом примере я специально указал, что он «крутится» в системе с 50К свободной кучи. Там я больше боялся, что рано или поздно произойдёт фатальная фрагментация адресного пространства. Получили гарантированное отсутствие фрагментации, проиграли в производительности, которая для Web-сервера не настолько критична.

        Если зайти совсем в разговоры о жизни, то пример относится к любительской разработке WiFi модуля для 3D принтера. ESP8266 постоянно вис. На скорую руку, я заменил этот кусочек. Был бы виноват он — сделал бы класс. Но выяснилось, что виновата работа в контексте прерывания, которая была сделана для устранения подвисаний… Короче, долгая история, описанная мною в блоге про 3D печать. А данный кусок был реабилитирован… Но в силу его показательности (соответствие старое-новое 1:1), пошёл в книжку про нашу ОС.

        Надо будет подумать над другим примером, в дополнение к этому. Этот оставить для фрагментации, а другой — для скорости. Осталось придумать что-то красивое.


        1. Comdiv
          15.09.2017 01:28
          +1

          Да что-то вроде этого:

          typedef struct {
              char *buf;
              int size, len;
              bool success;
          } buf_t;
          
          void init(buf_t *s, char *buf, int size) {
              assert(NULL != buf);
              assert(size > sizeof(void *));
          
              s->buf = buf;
              buf[0] = '\0';
              s->size = size;
              s->len = 0;
              s->success = true;
          }
          
          bool cat(buf_t *s, char const *str) {
            int i;
            i = 0;
            while ((s->len < s->size - 1) && (str[i] != '\0')) {
              s->buf[s->len] = str[i];
              s->len += 1;
              i += 1;
            }
            s->buf[s->len] = '\0';
            s->success = str[i] == '\0';
            return s->success;
          }
          
              char xml[768];
              buf_t buf;
              bool success;
              
              init(&buf, xml, sizeof(xml));
              if (cnt > 0)
              {
                cat(&buf, ",");
              }
            
              cat(&buf, "{\"type\":\"");
              if (entry.isSubDir())
              {
                cat(&buf, "dir");
              } else
              {
                cat(&buf, "file");
              }
              cat(&buf, "\",\"name\":\"");
              if (s->success && entry.getName(s->buf, s->size, &s->len)) {
                cat(&buf, "\"}");
              }
          


          1. EasyLy Автор
            15.09.2017 11:50

            Немного творчески переработал и вставил. Спасибо.


      1. EasyLy Автор
        15.09.2017 01:09

        Тьфу, в силе выигрываем, проигрываем в пути. Давно физику не повторял. Прошу прощения.


    1. EasyLy Автор
      15.09.2017 17:44

      По моей просьбе, разработчик, отвечающий за профилирование, провёл исследование. От себя добавлю, что на STM32 с частотой 168 МГц, на двух тысячах блоков среднее время составит 4.5 мкс. Хотя, максимальное — уже 10 мкс. Но есть и другие чипы. В частности, Миландровские. А ромбик военной приёмки — на них. И там пока частоты другие. А применительно к тому, Реальное Время у операционки или нет — лучше я дам прямую цитату от автора метрики:

      Случайным образом захватывались и освобождались блоки памяти.
      Профилировщик дал следующую картину:
      Qty – количество блоков
      Size – максимальный размер байтах (случайно, от 1 до Size)
      Min – минимальное время new (здесь и далее такты процессора)
      Max – максимальное время new
      Dev – стандартное отклонение
      Avg – среднее значение.

      image

      Итак, пока мы захватываем и освобождаем 1 байт – new стабильно занимает 459 тактов.
      Для 20 блоков уже есть некоторый разброс.
      По мере увеличения количества блоков (размер уменьшается, чтобы влезало в память) разброс растет.
      Для 5000 блоков максимальное время увеличилось почти в 5 (!) раз.

      Отдельно была искусственно создана довольно сильная фрагментация.
      В таких условиях время выполнения new выросло в 8 (!) раз.
      При этом никакой уверенности, что достигнут предел, быть не может.

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


      1. Indemsys
        16.09.2017 00:32

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

        Но даже приняв это все, скажите почему вам так принципиально за 3 мкс или за 24 мкс выполнится выделение памяти? Вы что память в обработчиках прерывания выделяете?
        Всетаки где вам важен такой детерминизм?


        1. EasyLy Автор
          16.09.2017 00:46

          Мы сейчас не научную работу защищаем.

          В книге заявлено: «Опасайтесь операций new. Если можно не использовать её при работе — не используйте».

          Именно опасайтесь, и именно при возможности…

          Вы попросили подтверждений, что всё это — не вчерашний день. Я их предоставил.

          Если бы речь шла о том, что «мы сделали так, что операцию new нельзя использовать совсем, так что теперь никто никогда не сможет ею воспользоваться» — имело бы смысл опускаться до подтверждения всего, что Вы запросили. Пока же — просто добавлено подтверждение того, что рекомендация — не голословная. Дальше — каждый читатель самостоятельно решает, как ему поступить.

          Но если вдруг из-за использования оператора new из стандартной библиотеки что-то случится — никто не скажет, что его не предупреждали. Возможные последствие должны быть задокументированы.

          На этом предлагаю свернуть обсуждение данного раздела книги в рамках этой публикации.


        1. EasyLy Автор
          16.09.2017 01:09

          Прикинул… Допустим, принимается блок по SPI с частотой 1 МГц (я специально взял малую частоту). В STM32 размер FIFO равен двум байтам, так что забрать данные надо уже через 16 мкс.

          Допустим, прикладной программист запросил буфер на стороне, а потом — начал выделять под него память оператором new. Может успеть, может — не успеть к моменту переполнения буферного регистра. Зависит от того, как оператор отработает. И при отладке может всегда отработать, а уже у Заказчика — нет.

          Вы скажете: «Кто ж так делает?». Вот я и отвечу, что я в тексте и написал, что так делать не стоит. Текст показывает основные отличия программирования под железо относительно общих принципов программирования. Считается, что читатель знаком с информатикой, но не очень глубоко знаком с аппаратурой.

          Неопытные программисты под железо и не такое могут. Откуда им опыта набраться? Вот и написано. Опытные же могут скачать с нашего сайта документ, соответствующий всем требованиям ЕСПД и пользоваться им.


  1. bolick
    15.09.2017 02:29

    Язык С++, на котором чаще всего пишут для микроконтроллеров...

    Это мило. Микроконтроллер микроконтроллеру, конечно, рознь, но мне думалось, что на AVR или PIC16 с парой килобайт памяти все-таки больше используют старый добрый Си. Возможно, с элементами ООП типа имитации классов и т. п.


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


    1. Armleo
      15.09.2017 08:03

      Не используйте PIC16. Самый дешевый PIC16 стоит 2.5$. STM32 с теми же характеристиками 1$.
      У AVR нет нормальных отладчиков. Не используйте AVR. (5 секунд на загрузку 256кб. RLY?!)


      Это мое личное мнение.


    1. EasyLy Автор
      15.09.2017 11:58

      Моё личное мнение: Свои проекты для AVR я писал исключительно на ассемблере. Рука не поднималась писать даже на Сях. Сейчас я уже не делаю новых проектов на AVR. Есть Cortex M разных мощностей и цен.

      Сегодняшние реальности по AVR: Arduino — вполне себе плюсовая библиотека. И, как ни странно, многие задачи прекрасно пашут. Если посмотрите статьи про библиотеку mcucpp, которая вообще использует подходы метапрограммирования, то увидите, что современные оптимизаторы делают код для AVR не хуже по оптимизации, чем ручная разработка на ассемблере. Я эту mcucpp для ARM использую, так что знаком с нею не только по статьям. Но исходно она явно делалась именно под AVR, на плюсах.

      Так что сегодня для AVR многие пишут именно на плюсах. И это даже работает. У меня на столе стоит 3D принтер, в котором исходно была AtMega.

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