Управление блокировкой прерываний с помощью с++ деструкторов.

Это будет короткая заметка…

Классическое построение атомарных функций на микроконтроллерах выглядит примерно так:

void func()
{
	ret_t ret;
	save_t temp = atomic();
	while(cond())
	{
		if (another_cond()) 
		{
			ret = do_anything();
			deatomic(temp); 
			return ret;
		};
	};
	ret = do_default();
	deatomic(temp);
	return ret;
} 

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

Разработчики avr-libc для решения этой проблемы даже вводят специальный макрос ATOMIC, который решает эту проблему, но который, впрочем, имеет несколько неинтуитивный синтаксис, и переписывание которого на блокировку отдельного прерывания требует третьего уровня посвящения в препроцессорную магию.

С++, же, как выясняется давно эту проблему решил. В стандартной библиотеке даже есть специальная концепция, именуемая guard_lock.

Смысл этого действа в использовании того факта, что деструктор объекта вызывается автоматически и уже после вычисления возвращаемого значения.

(псевдокод на базе avr)
class irq_lock
{
public:
	save_t temp;
	irq_lock(){ temp = SREG; cli(); };
	~irq_lock() { SREG = temp; };
};

использование:
void func()
{
	irq_lock lock();
	while(cond())
	{
		if (another_cond()) 
		{
			return do_anything();
		};
	};
	return do_default();	
};

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

class uartX_rx_lock()
{
public:
	bool temp;
	
	uartX_rx_lock() 
	{ 
		temp = uartX_rx_irq_is_enabled();  
		uartX_rx_irq_disable();
	}	

	~uartX_rx_lock() 
	{ 
		if (temp) uartX_rx_irq_enable();
	}	
}
использование аналогично.
  • +1
  • 27 мая 2016, 11:32
  • Mirmik

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

RSS свернуть / развернуть
Это будет короткий комментарий.
Реализация атомарных операций и блокирование прерываний — это совершенно не одно и то же, и то, что в некоторых библиотеках для некоторых МК они тождественны — еще не повод для таких обобщений.
+4
Да и хрен с ним…
0
Ты в дизассемблере смотрел во что это выливается, это было бы любопытно наверное.
-3
Вот если бы ты привел ассемблерный код в том и другом случае, вот было бы здорово а?
-2
… Каждый раз, когда я посщу какую-нибудь шаблонно-конструкторно-препроцессорную магию меня все сразу просят показать ассемблерный листинг… Нет… Я не смотрел, как это выглядит в ассемблере.
0
Ну так в чем же дело-) У тебя видимо все настроено, все под руками-)) Вопрос-то интересный ты поднял. Известно что на плюсах легче накосячить чем на си, но и удачный результат может быть поразительным. Раз С++ компиллятор сгенерировал мне ассемблерный код меньше чем я сам написал -(((
вот позорище то было -)) (на самом деле я для подстраховки напихал nop-ов, поэтому и больше) Так что не не ленитесь, молодой человек-))
-2
~irq_lock() { SREG = temp; };
А не перебор — сразу все флаги сбрасывать к тому, что было на момент запрета прерываний?

Ну и действительно, любопытно глянуть, что оно генерит.
+1
  • avatar
  • Vga
  • 27 мая 2016, 12:56
Вообще это псевдокод, а не руководство к действию…
Но, это стандартная конструкция для AVR. В регистре SREG нет глобальных флагов, кроме запрета прерываний. Все остальные флаги все равно будут инвалидированы по результатам операции присваивания.
0
Первой же следующей операцией…
0
Вообще, у AVR довольно много операций, не затрагивающих флаги. Не уверен, насколько гарантии есть, что компилятор не воткнет восстановление SREG ровно между ставящей флаг инструкцией и зависящей от него.
0
SREG объявлен как volatile… так что такого произойти не должно.
Да и потом… Все так делают…
0
Хором: Давай, давай )))
-2
А за что нынче минусят на сообществе? Так интересно стало, аж залогинился
0
Это следствие ээ — некоторых споров, вот если интересно)))
-1
Врядли. Скорее за настырность — ты выдал три комментария с требованием дизасма подряд.
+1
Довел код до состояния «кое-как компилируется». Забавно, что оба варианта выдают идентичный код (оптимизация -O3).
#include <stdint.h>
#include <avr/io.h>
#include <util/atomic.h>

typedef uint8_t save_t;

save_t atomic()
{
  save_t temp = SREG;
  cli();
  return temp;
}

void deatomic(save_t save)
{
  SREG = save;
}

class irq_lock
{
public:
  save_t temp;
  irq_lock(){ temp = SREG; cli(); };
  ~irq_lock() { SREG = temp; };
};

uint8_t do_anything()
{
  return PORTB;
}

uint8_t do_default()
{
  return 0;
}

uint8_t cond()
{
  return PINB & 0x01;
}

uint8_t another_cond()
{
  return PINB & 0x02;
}

uint8_t func()
{
  uint8_t ret;
  save_t temp = atomic();
  while(cond())
  {
    if (another_cond()) 
    {
      ret = do_anything();
      deatomic(temp); 
      return ret;
    };
  };
  ret = do_default();
  deatomic(temp);
  return ret;
} 


uint8_t func2()
{
  irq_lock lock = irq_lock();
  while(cond())
  {
    if (another_cond())
    {
      return do_anything();
    };
  };
  return do_default();    
};

int main(void)
{
  func();
  func2();
  return 0;
}

00000056 <_Z4funcv>:
  56:	2f b7       	in	r18, 0x3f	; 63
  58:	f8 94       	cli
  5a:	02 c0       	rjmp	.+4      	; 0x60 <_Z4funcv+0xa>
  5c:	b1 99       	sbic	0x16, 1	; 22
  5e:	07 c0       	rjmp	.+14     	; 0x6e <_Z4funcv+0x18>
  60:	96 b3       	in	r25, 0x16	; 22
  62:	89 2f       	mov	r24, r25
  64:	81 70       	andi	r24, 0x01	; 1
  66:	90 fd       	sbrc	r25, 0
  68:	f9 cf       	rjmp	.-14     	; 0x5c <_Z4funcv+0x6>
  6a:	2f bf       	out	0x3f, r18	; 63
  6c:	08 95       	ret
  6e:	88 b3       	in	r24, 0x18	; 24
  70:	2f bf       	out	0x3f, r18	; 63
  72:	08 95       	ret

00000074 <_Z5func2v>:
  74:	2f b7       	in	r18, 0x3f	; 63
  76:	f8 94       	cli
  78:	02 c0       	rjmp	.+4      	; 0x7e <_Z5func2v+0xa>
  7a:	b1 99       	sbic	0x16, 1	; 22
  7c:	07 c0       	rjmp	.+14     	; 0x8c <_Z5func2v+0x18>
  7e:	96 b3       	in	r25, 0x16	; 22
  80:	89 2f       	mov	r24, r25
  82:	81 70       	andi	r24, 0x01	; 1
  84:	90 fd       	sbrc	r25, 0
  86:	f9 cf       	rjmp	.-14     	; 0x7a <_Z5func2v+0x6>
  88:	2f bf       	out	0x3f, r18	; 63
  8a:	08 95       	ret
  8c:	88 b3       	in	r24, 0x18	; 24
  8e:	2f bf       	out	0x3f, r18	; 63
  90:	08 95       	ret
+3
  • avatar
  • Vga
  • 27 мая 2016, 14:24
Весьма благодарен. Я бы разве что вечером до этого добрался…
0
А вы молодой человек на будующее учтите интерес к ассемблеру — без него на мк сложно)))
-2
Это без понимания работы процессора на МК сложно. А без ассемблера вполне можно обойтись.
+3
Вы забыли еще знание работы компиллятора, будете знать — плюньте на ассемблер-))) Он у вас и так в голове будет))
0
Ну, вообще я перестал постоянно лазить в ассемблер как раз тогда, когда стал примерно представлять, как компилятор работает… Так что да…
0
Респект!
-1
Кстати говоря, баг репорт…

имеет смысл писать…
irq_lock lock();

Зачем я там поставил присваивание несовсем понятно…
0
Хм, а разве стандарт гарантирует это? ЕМНИП стандарт гарантирует, что будет вызван деструктор, когда объект покинет область видимости. Но стандарт не требует, чтобы вызов дестуктора был «особенным» (и выполнялся именно в последним в списке вызовов). Или я ошибаюсь?
0
Ну область видимости объект покинет уже после вычисления возвращаемого значения. Так что вроде бы всё логично. Не?
0
Я не уверен в ответе, поэтому и спрашиваю. Оптимизатор «видит», что к объекту никто не обращается. Может ли он вызвать деструктор раньше, чем выполнить самую последнюю инструкцию в стеке вызовов для данной области видимости?
0
Нет. Вызов деструктора происходит в момент высвобождения памяти, отведенной под объект. С++ не уничтожает стековые объекты посреди функции.
0
Причем здесь освобождение памяти и прочее? Вот например в выражении

a = foo() + bar();

Какая функция ( foo() или bar()) будет вызвана первой? В стандарте нет понятий « высвобождения памяти » есть точки следования…
0
С точки зрения компилятор пофиг, но если они связаны тогда надо самому думать. или используйте компилятор для многопроцессорных систем — он умнее и отслеживает часть автоматом, но всё равно всё предусмотреть автоматом невозможно. Даже как то стыыдно писать такие прописные истины.
0
С точки знания С — между ними нет точки следования, и порядок не определен. В С#, например, будет  foo()  потом bar(). Не важно сколько там процессоров и какая там система — это гарантия стандарта ЯП.
0
не совсем так — я могу переопределить ~ и dispose + еще suppressfinalize. так что тоже не всё прозрачно будет. Дискуссия какая то бессмысленная. Уйду — никто не огорчится.
0
Дискуссия какая то бессмысленная. Уйду — никто не огорчится.

Зря, я огорчусь:)

не совсем так — я могу переопределить ~ и dispose
 

Вы можете переопределить сами методы, (что именно будет делать деструктор или там dispose ) но не момент их вызова (а в данной задаче это, ИМХО, критически важно)
0
Момент их вызова зависит от реализации планировщика — тут я никак не участвую. Поэтому я не могу прогнозировать когда он наступит. хоть в С++ хоть в С№ (в RSX-11, RT-11, VAX/VMS, Windows, BeOS, OS/2,… тоже самое)
Мы говорим о проблемах, которые в embedded не должны быть. Лампочкой поморгать. пакет по wifi. Если посмотреть на код от ST — там вообще не думают о прерывания и транзакционности. Да и mutex с semaphore никто не отменял. ну и на крайняк __disable_irq есть :)
0
с добавлением счетчика запрещений и разрешений
0
Какой планировщик в С или С++? В С# — там да, финализаторы вызываются средой из отдельного потока, притом куча своих нюансов. Но в С++ есть гарантии языка на этот случай (как уже подсказали ниже — деструкторы вызовутся в обратном порядке), компилятор должен позаботиться об этом, не важно какая там ОС, какой у этой ОС планировщик и вообще, есть ли там ОС. А мютексы и семафоры — это из другой оперы, они нужны когда у нас код выполняется параллельно (например, в нескольких потоках).
+1
И потом, даже если такое поведение и небыло стандартизовано, С++11 его фактически узаконил, ибо этот приём используется в стандартной библиотеке с++ версии 11-ого стандарта для управления мьютексами.
0
Ну область видимости объект покинет уже после вычисления возвращаемого значения.
Но ведь на данный объект нет ссылок, почему оптимизатор не может вызвать деструктор до того как вычислит «возвращаемое значение» которое от объекта никак не зависит?
0
Наверное потому, что объект еще не покинул область видимости, а по стандарту деструктор не может быть вызван раньше.

Я стандарт не читал… И даже о точках следования ничего не слышал. Я просто знаю, что оно работает именно так. Вот если мне покажут пример, в котором оно работать не будет, тогда я уже пойду разбираться, почему оно не работает…
0
Возможно потому, что конструктор/деструктор могут иметь сайд-эффекты?
0
Это понятно. Просто в «класическом случае» у нас есть в коде вызов функции, дальше все понятно (точки следования и т. д.). А в данном случае — вызов есть но в коде не отображается явно. Поэтому интерестно, что говорит стандарт в этом случае. Например, если таких обектов в области видимости не один, а несколько — гарантирует ли стандарт последовательность (подозреваю, что последовательность должна быть обратная) вызовова деструкторов. Я быстро полистал стандарт — сходу не нащел ответа, нужно будет почитать внимательнее.
+1
Насколько я помню, порядок вызова деструкторов гарантируется как обратный вызовам конструкторов, но это не из стандарта, а из книжек «изучаем С++».
0
Это и в стандарте прописано.
0
Для подобных вопросов часто удобнее смотреть в FAQ. В частности этот вопрос расписан тут.
0
В .net деструктор может быть вызван не в момент освобождения объекта, а когда понадобится память, или будет мусорок собираться. в С++ от реализации зависит — сильно на С++ деструкторы расчитывать я бы не стал.
0
Тем не менее, порядок вызова деструкторов гарантирован. Как и то, что деструкторы будут вызваны именно в момент покидания области видимости, а не в произвольный момент внутри нее (даже если к переменной нет обращений). Собственно, вся эта система прибита гвоздями и не меняется еще со времен первых версий С++. Причина предельно проста — одна из очень важных концепций в C++ это RAII, а для ее работоспособности описанное выше поведение конструкторов и деструкторов жизненная необходимость.
+1
Добавлю, что описанное в этой заметки именно RAII и есть…
0
В .NET нет деструкторов, есть финалайзеры. Это концептуально разные вещи, хоть и оба определяются через ~.
Вызов деструктора в С++ как раз детерминирован, в отличии от финалайзера в .NET.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.