AVR. Учебный Курс. Программирование на Си. Атомарные операции.

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

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

Итак, есть у нас такой код (не ищите в нем практического смысла, я его просто как пример работы с разными операндами написал):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
volatile char flag_byte; 	
/*Просто флаговая переменная, на разные случаи жизни. Разные события там 
выставляют флажки, опираясь на которые потом работает логика программы. 
Один из способов организации псевдомногозадачности. Когда у нас главный цикл 
анализирует флажки и делает переходы на подпрограммы, а вызов подпрограмм 
осуществляется не напрямую, а установкой соответствющих флажков. Своего 
рода диспетчер переходов. О такой архитектуре я скоро расскажу)*/
 
ISR (USART_RXC_vect)	// Обработчик прерывания, самый обычный.
{
flag_byte|=1<<rcv_buff;
...
...
}
 
int main (void)		// Главная программа
{
INIT_ALL();
SEI();
...
...
...
TCCR0A  	|=1<<WGM01;
...
flag_byte 	|=1<<options;
...
PORTB 	&=~(2<<1);
...
}


Человек ни разу не писавший под МК на ассемблере, начавший изучать МК сразу с Си, схавает такую конструкцию и даже не поморщится. Прожженый ассемблерщик же нецензурно матюгнется, обзовет первого быдлокодером и исправит исходник следующим образом:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
volatile char flag_byte; 	// Просто флаговая переменная под флаги на разные случаи жизни
 
ISR (USART_RXC_vect)	// Обработчик прерывания
{
flag_byte|=1<<rcv_buff;
...
...
}
 
int main (void)		// Главная программа
{
INIT_ALL();
SEI();
...
...
...
TCCR0A  |=1<<WGM01;
...
CLI();
flag_byte |=1<<options;
SEI();
...
PORTB &=~(2<<1);
...
}

А еще проверит не юзается ли где еще в прерываниях регистр TCCR0A. И внимательно посмотрит на работу с регистрами периферии.

В чем же дело? Какая разница между

1
2
3
TCCR0A  |=1<<WGM01;
flag_byte |=1<<options;
PORTB &=~(1<<2);

Или между

1
PORTB &=~(1<<2|1<<3);

и

1
PORTB &=~(1<<3);

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

А для ассемблерщика между этими строками заложена огромная разница.

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

1
flag_byte |=1<<options;

Компилируется во что то вроде:

1
2
3
 	LDS	R16,flag_byte	; Чтение
	ORI	R16,1<<options	; Модификация
	STS	flag_byte,R16	; Запись

Во первых TCCR0A это аппаратный регистр, причем имеющий адрес в пределах 00-3F (адресное пространство на котором возможна работа команд IN/OUT), поэтому доступ к нему может быть через команду OUT, причем напрямую в регистр I/O сделать OUT константы нельзя, только через промежуточный регистр R16…R31 так что конструкция

1
TCCR0A  |=1<<WGM01;

Будет подобна предыдущей:

1
2
3
 	IN	R16,TCCR0A	; Чтение
	ORI	R16,1<<WGM01	; Модификация
	OUT	TCCR0A,R16	; Запись

А вот регистр PORTB расположен в адресном пространстве от 00…1F, поэтому кроме команды OUT до него могут дотянуться также команды работы с битами CBI и SBI.

Так что конструкция

1
PORTB &=~(1<<2);

Скомпилируется в

1
	CBI 	PORTB,2

А вот :

1
PORTB &=~(1<<2|1<<3);

Может уже быть как

1
2
	CBI  	PORTB,2
	CBI	PORTB,3

так и

1
2
3
	IN	R16,PORTB		; Чтение
	ANDI	R16,~(1<<2|1<<3)	; Модификация
	OUT	PORTB,R16		; Запись

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

Да, это все здорово. Но к чему я это?

А к тому, что у нас тут есть еще и прерывание. В котором может быть изменены наши значения, а еще тот милый факт что прерывание, будучи разрешенным, может выскочить между двух любых инструкций. И при этом родить очень адский глюк которой может ВНЕЗАПНО вылезти через пару лет безглючной работы, а потом также бесследно исчезнуть, оставив разработчика задумчиво чесать репу на предмет «ЧТО ЭТО БЫЛО»? Часто тут грешат на глюки железа и дырявые процы. Хотя на самом то деле просто программа была корявая.

Покажу на примере, вот наш первоначальный код:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
volatile char flag_byte; 	// Просто флаговая переменная под флаги на разные случаи жизни
ISR (USART_RXC_vect)	// Обработчик прерывания
{
flag_byte|=1<<rcv_buff;
...
...
}
 
int main (void)		// Главная программа
{
INIT_ALL();
SEI();
...
...
...
TCCR0A  |=1<<WGM01;
flag_byte |=1<<options;
PORTB &=~(2<<1);
...
}

Программа выполняется себе, готовится записать бит options в переменную flag_byte…

1
2
; начало операции flag_byte |=1<<options;
 	LDS	R16,flag_byte	; Чтение предыдущего значения

А тут хопа, так фаза Луны совпала и вдруг пришло прерывание именно в этот момет. А что у нас в прерывании? Правильно! Сохранение регистров, что то вида::

1
2
3
4
5
; Начало обработчика вектора ISR (USART_RXC_vect)
	PUSH	R17
	IN	R17,SREG
	PUSH	R17
	PUSH 	R16

А дальше наше тело прерывания:

1
2
3
4
5
; Начало операции flag_byte|=1<<rcv_buff;
	LDS	R16,flag_byte	; Чтение
	ORI	R16,1<<rcv_buff	; Модификация
	STS	flag_byte,R16	; Запись
; Конец операции flag_byte|=1<<rcv_buff;

Выставили мы битик rcv_buff и записали в ту же переменную флагов. А что потом? Правильно — возврат регистров из стека и возврат:

1
2
3
4
5
6
	POP 	R16
	POP	R17
	OUT	SREG,R17
	POP	R17
	RETI
; Конец прерывания ISR (USART_RXC_vect)

Куда возврат? Туда же где прервались:

1
2
3
	ORI	R16,1<<options	; Модификация
	STS	flag_byte,R16	; Запись
; конец операции flag_byte |=1<<options;

Замечательно, а что же у нас в регистре R16 в данный момент?

Очевидно то же самое, что и до входа в прерывание — состояние флаговой переменной flag_byte. Вот только заковыка — это состояние флаговой переменной уже не актуально, устарело! Т.к. его только что изменило прерывание, выставив там какой то другой свой флаг. И теперь, произведя модификацию и запись у нас в flag_byte будет записана не актуальная инфа уже с двумя стоящими флагами rcv_buff и options, а восстановленная из «бэкапа» предыдущая копия с одним лишь options. А событие которое нам пыталось донести прерывание, выставив свой флаг, было забыто вообще.

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

Лирическое отступление, можно пропустить:
Когда то давно отлаживал я программу одну, она была на ассемблере, но там я просто забыл что у меня есть переменная — адрес перехода. Которая может меняться и в прерывании тоже. Так вот, все работало идеально, но примерно на 30 минуте работы программа вставала колом или начинала гнать полную ересь. Я убил тогда на отладку около недели. Программа была довольно большая, там могло быть что угодно, начиная от переполнения стека до срыва очередей диспетчера задач. В ход пошла уже тяжелая артиллерия в лице логического анализатора и JTAG — без толку. Попробуй поймай багу на хрен знает какой итерации главного цикла, да еще при совпадении условий внешних воздействий в фиг знает каком порядке. Трассировать или отлаживать в протеусе это почти бесполезно.
Причем малейшие изменения в коде приводили к видоизменению баги. Добавил в код парочку NOP — клиническая картина резко меняется. Чудом мне удалось поймать момент, когда прогу перекашивало точно на 25 итерации главного цикла после отпускания кнопки. Выглядело это примерно так: послать в USART точно 10 байт, причем последний должен быть непременно «R», потом нажать кнопку и не отпускать пока не будет нажата вторая. И вот когда вторую отпустить прога встает колом.

Дотрассировав до этого места в JTAG я наконец понял где собака порылась — адрес перехода состоял из двух байт и лежал в ОЗУ. Первый байт менялся в фоне программы, и мог быть изменен в прерывании. И вот когда это прерывание вклинивалось в между первым и вторым байтом при занесением нового адреса, то в результате получался гибрид состоящий из двух разных половинок адреса, ясен фиг что переход получался черти куда и выглядело это как срыв стека, хотя стек тут был вообще не причем. Впрочем, про характерную клиническую картину ошибок я тоже собираюсь потом написать.

Итак, враг найден. Но что же с ним делать? А делать тут собственно нечего. Единственный способ избежать этой напасти это либо не использовать раздельно используемые переменные, либо запрещать прерывания при их модификации в фоновой программе — делать атомарными.

Атомарная — значит не делимая. Не делимая прерыванием. Поэтому то команды запрета/разрешения прерываний

1
2
3
cli();
flag_byte |=1<<options;
sei();

Спасают ситуацию.

А остальные? Они ведь тоже делаются не за одну операцию! Ну в данном случае в прерывании они не используются и поэтому пока можно спать спокойно, но держать на контроле надо.

А вот операции вроде SBI/CBI делаются за одну команду и являются атомарными от природы.

Страшно да? А ведь многие об этом даже не задумывались.

Да и в самом деле, что считать атомарным а что нет? Не закрывать же в конце концов параноидально все операции с разделяемыми переменными и IO регистрами командами cli/sei. Как никак прерывания вещь важная и созданная именно для того, чтобы реагировать быстро.
И, например, на AVR нельзя сделать атомарную операцию на регистры ввода вывода старше 1F — там просто нет команд логических операций на IO, а вот на PIC24 (а может и на более младших, хз не знаю я PIC) можно, через битовые маски по XOR, т.к. там есть команда xor по IO. Подробней можно прочитать в одной замечательной статье, она хоть и про PIC24, но полезна будет всем.

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

Отлично, враг известен, чем его мочить тоже выяснили. Но любое оружие может замочить и хозяина. Если им неумело пользоваться. Поясню на примере:

Вот сделали мы какую нибудь функцию, а чтобы прерывания нам не нагадили мы добавили в них конструкцию cli/sei на критичные места. Все довольны, все счастливы. Ага, до тех пор пока в целях оптимизации и универсальности кода мы не вкатим эту функцию в обработчик прерывания. В принципе, ничего страшного нет, пользоваться в прерыванях функциями можно. Вот только надо учитывать что в прерываниях прерывания же аппаратно запрещены, а наша функция по выходу из критических мест их разрешает. И тут у нас вылезают другие вилы — вложенные прерывания. Это бага не столь законспирированная как неатомарный доступ, но вылезти тоже может ой как не сразу.

Что делать? А тут все просто. Если у нас есть функция с атомарными операциями и юзается она и в прерываниях и в основном теле программы, то надо бы это учитывать и не менять флаг прерывания зря. Т.е. если он был сброшен (в прерывании) то мы его и не возвращаем обратно — незачем.

На ассемблере это может выглядеть, например, так:

1
2
3
4
5
6
7
8
9
10
11
12
; Begin Atomic Block
	IN	R17,SREG	; Сохранили регистр SREG, а в нем и флаг I
	PUSH	R17		; можно в стеке, можно еще где. Не принципиально
	CLI			; Запретили прерывания
 
	... 			; Тут у нас код который должен быть атомарным
	...			;
	...			;
 
	POP 	R17		; Достали сохраненное в стеке
	OUT	SREG,R17	; Вернули SREG на место
; End Atomic Block

Разрешать прерывания тут не надо. Т.к. мы сохранили сразу весь SREG. Если прерывания были разрешены, то по доставании его из стека они достанутся разрешенными. Ну а если не были разрешены то ничего и не изменится.

На Си же можно тупо в лоб проверять условием наличие флага I регистра SREG и в зависимости от этого делать потом разрешение прерывания или нет. Запрещать их в любом случае. Либо применить инлайновый ассемблер своего Си компилятора.

А еще можно поискать уже готовые макросы в составе компилятора.

В WinAVR GCC например есть такой хидер как util/atomic.h

где есть макросы:

1
2
3
4
     ATOMIC_BLOCK(type)
     {
       код который будет атомарным;
     }

Где в качестве type возможны два варианта: ATOMIC_FORCEON — прерывания по выходу из атомарного блока будут включены и ATOMIC_RESTORESTATE в котором состояние флага I будет таким же какое и на входе в блок. Запрещены — так запрещены, разрешены так разрешены. Но работает чуть медленней и памяти требует больше.

Также там есть макрос разатомаривания.

1
2
3
4
NONATOMIC_BLOCK(type)
     {
       код который будет неатомарным;
     }

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

Синтаксис этих макросов такой же, а опции по аналогии NONATOMIC_RESTORESTATE — оставляет как было. NONATOMIC_FORCEOFF — принудительно выключает прерывания по выходу.

Так что можно вместо:

1
2
3
cli();
flag_byte |=1<<options;
sei();

Написать

1
2
3
4
ATOMIC_BLOCK(ATOMIC_FORCEON)
{
flag_byte |=1<<options;
}

И это будет эквивалентно. Единственное что код потеряет часть переносимости. По хорошему бы вообще вынести все эти cli(); sei(); в отдельный хидер где заменить из на что нибудь вроде Interrupt_Enable(); Interrupt_Disable(); и тогда этот код можно будет перетащить на любой контроллер, лишь бы там был Си компилятор. А уж привязать к Interrupt_Enable(); местный аналог sei(); куда проще в одном файле в одном месте чем перелопачивать весь сырок.

З.Ы.
Также недавно появилась отличная статья Виктора Тимофеева про то как надо правильно писать код. Настоятельно рекомендую к прочтению.

73 thoughts on “AVR. Учебный Курс. Программирование на Си. Атомарные операции.”

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

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

      2. «Там», как правило, такие грабли вылезают при первой же попытке поковыряться в драйвере устройства или ещё в каком-нибудь аппаратно-зависимом модуле. :)

  1. познавательно,…

    ИМХО:
    В статье как-то расплывчато описано понятие «атомарная операция», это выполнение команды за один такт или код в блоке CLI/SEI ?

    1. P.S. если не ошибаюсь касательно ОС «атомарные операции» — это код выполняемый на уровне ядра самой ОС.

    2. Атомарная операция — это операция, которая выполняется за один раз, не прерываясь ничем. Т.е. пока она выполняется — система не будет отвлекаться на выполнение каких-либо других операций.

    1. Ну, в отличие от статей про электронику, эта статья у ДиХальта вышла и правда несколько сумбурной и слабострукурированной. Но общая идея, мне кажется, донесена. Или, может, мне так кажется, потому что я про эти грабли и так давно знаю? :)

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

    если бы я думал, что мне надо вдыхать и выдыхать, то я бы задохнулся уже давно

    1. Ну таки не все. У ARM7 ЕМНИП все периферийные регистры находятся в той же области памяти и допускают любые операции с ними. Тут их можно по маскам ксорить и получать полную атомарностью

      1. На арме вообще нет понятия ‘адресного пространства портов ввода-вывода’. Впрочем, на авр его тоже нет, есть ограничение у некоторых команд по диапазону адресов операндов.

    2. Люблю такие глобальные заявления про «убогость всех архитектур всех машин» :)
      Ежели всё так убого, сделайте лучше.

      Создание процессора (да и вообще, наверное, чего угодно) это непрерывный поиск компромиссов.

      Хотите атомарную замену бита где угодно, а не только в начале адресного пространства?
      Легко! Но длину инструкции придётся увеличить, чтобы запихнуть в неё более длинный адрес. Жертвуем память.

      Хотите in, out и сделать ещё что-нибудь в промежутке за одну команду?
      Да запросто. Только выполняться она уже будет не за один такт. Или за один, но на более низкой тактовой частоте. Или за один и на той же частоте, но чип придётся делать по более дорогому техпроцессу.
      Некоторые вещи можно делать одновременно и без потери скорости, но дополнительные операционные блоки будут кушать дополнительное электричество. Плюс занимать площадь кристалла, который от этого тоже не подешевеет.

      Хотите всё и сразу, да ещё совместимость со всем подряд?
      Тоже не проблема. Загляните в системный блок — утюг за несколько сотен баксов внутри него и есть ответ на все ваши пожелания.

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

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

    2. Фигня только в том, что в «очень больших проектах» не бывает чистого Си++ с его инкапсуляциями и прочей хренью. А простой Си, на котором написано дофига всего «очень большого» (например, все юниксы, включая Linux), в том числе и очень даже разными «гуру программирования», без глобальных переменных (например, глобальных массивов данных типа таблицы файловых дескрипторов или дескрипторов памяти) существует с трудом. :)

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

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

    Ну а где зарывание? То, что существуют криворукие программеры — это совсем не аргумент.

    1. Об этом то и речь, что изучая что либо сверху не видишь железных проблем самого дна которые могут доставить косяк там где его не ждешь. Об этом собственно и утверждение. А таких вот «любителей» существует целый легион. Именно потому, что начали с Си, а на асм даже не заморочились. Конечно, и в Си среде можно найти инфу по атомарным операциям и тому что они значат. Вот только это должно на глаза попасться либо нужно целенаправленно про это знать и искать, а ведь все ленивые. Получилось диодиком помигать и вперед — осваивать больше и дальше. Толком не понимая как же оно на самом деле работает. А когда изучаешь МК снизу, то такие проблемы встают сразу же, по крайней мере они очевидны и понятны.

      1. Не скажи, даже среди асемблерщиков попадаются быдлокодеры, которые не заморачиваются на «внезапно и случайно вылезшие» прерывания.

        1. Разумеется есть. Просто там это сразу видно. И если чел не даун то он это просекет и учтет. А вот в Си ты это поймешь только когда наткнешься сам. А мне попадались вполне неглупые люди, пишущие на си и считаювшие что i++ это минимальная операция. Пока я не открыл дизассемблер и не показал что это на самом деле. Надо было видеть их шок когда они осознали что десятки уже написанных и работающих то тут то там программ могут внезапно дать сбой.

          1. На самом деле тут еще сильно зависит от того, на чем эти люди учились. Потому что на архитектуре x86 операция i++ является действительно минимально возможной и транслируется в иструкцию типа «inc cx». Правда, если это вдруг не i (не счетчик, то бишь) и вообще переменных в функции больше одной, то легко может оттранслироваться и в чтение-модификацию-запись. :)

            А «труъ» минимально-возможная операция в Си — это «точка-с-запятой», которая в идеальном случае вообще ни во что не транслируется, хотя с точки зрения языка операцией является. :)

            1. Ну что i++ даже в случае счетчика, даже на х86 далеко не гарантирует что это всегда будет один inc тут уж как компилятор извернется.

      2. Дело в том, что сишников учат работать в операционных средах, где кроме прерываний ссуществует еще куча опасностей. Даже нифига незная асма, любой нормальный сишник думает как будет обращаться к общим переменным когда пишет fork() или CreateProcess(…).

        Кроме описаного, в могозадачных средах есть еще явление состояния гонки
        http://ru.wikipedia.org/wiki/Состояние_гонки

        и инверсии приоритетов
        (см книгу uC/OS-II The Realtime Kernel, к примеру)

        есть еще синхронизация использования ресурсов

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

        1. Если где то и учат теории правильного программирования, то я очень рад за них. Потому как это действительно ценно. Я пока не встречал.

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

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

          Собственно этих начинающих я и предостерегаю о опасности легкого пути. Ну и применительно к мелким МК вроде PIC или AVR знание ассемблера никогда не повредит. Хотя бы для того чтобы запихать прогу в какую нибудь Тини2313.

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

          1. >Вы никогда не писали на ассемблере?
            Я писал на асме (и сейчас немного пишу) под множество разных архитектур. Но основной язык — это си, местами — плюсы.

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

            Хотя, каждый дрочит как он хочет. Я никого не агитирую переходить на си.

            1. Язык это инструмент. Не более того. Не религия. Хочешь быть профи умей орудовать и надфилем и токарным станком. А инструмент выбирай под масштаб задачи.

          2. для начала можно почитать статью на википедии «философия unix». русское издание книги Эрика С. Рэймонда (англ. Eric S. Raymond) «Искусство программирования в UNIX» можно найти в сети. слово unix пусть не пугает. начнете читать — поймете почему. хотя если ограничиться программированием мигающих светодиодов то это все не нужно.

  5. Собственно тут есть две проблемы.
    1. Кривой код. Нельзя так экономить память. Вначале добиваемся безглючности, потом оптимизируем.
    2. Абсолютно согласен с тем, что для хорошего программиста необходимо знать архитектуру железки/виртуальной машины под которую он пишет.

    ЗЫ Для того чтобы избавиться от описаных проблем, я бы поступил по-другому: разделил переменные (ну и вообще, область памяти) на модифицируемые только в прерываниях и модифицируемые в основном коде.
    Соответственно, то что меняется в прерываниях — read only для другого кода, то что меняется в коде — read only для прерываний.

    1. Не всегда и не везде это возможно. Например в том же прерывании может потребоваться установка флага для чего либо, да даже для того же флагового автомата. Или подправить периферийный регистр. Да мало ли для чего. Делать под каждый чих разные переменные это не выход. Памяти тут мало.

      Плюс такие вещи как диспетчеры ос. Они работают с глобальынми очередями. И эти очереди могут менятся в прерываниях. Например когда обработчик прерывания закидвывает в очередь очередную задачу.

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

      1. Не всегда и не везде, но к этому надо стремиться.

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

          1. Ок, тут как реализовывать очередь. Я бы это сделал либо Linked List, либо большим массивом. Массив бы сдвигал только когда полностью заполняю очередь.
            В обоих случаях новые данные только добавляются, но не редактируются.

            1. Не спасет. Тут в любом случае надо будет атомировать операцию записи в массив или очередь. Т.к. это минимум 3-4 команды. Так что у тебя может быть уже индекс нацелился а тут опа и прерывание и по этому же индексу записалось, а по возврату в ту же точку пройзойдет запись из фона.

    2. Опять же по оптимизации. Сначала надо сразу продумать архитектуру. То КАК это будет работать. Потому что потом может сложиться ситуация что для оптимизации надо будет переделывать ВСЕ. Так что оптимальный код надо учиться писать сразу же.

  6. По поводу портов пользуюсь такой штукой:

    #define sbi(port, pin) asm volatile («sbi %0,%1»:: «I» _SFR_IO_ADDR(port), «M» (pin));
    #define cbi(port, pin) asm volatile («cbi %0,%1»:: «I» _SFR_IO_ADDR(port), «M» (pin));

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

  7. 1. Нормальная статья. Народ должен понимать, что он пишет. А привыкли копипастить, а потом валить на то, что проц/компайлер насквозь глючный. (Бубуен дырявый, а не проц!)

    2. Добавлю 5 копеек. Скажу про IAR.

    a). Если нужно запретить прерывания в какой-то функции, то делай так:

    __monitor void myFunc(…)
    {
    … // небезопасный код
    }

    b). Если функция большая, то блокировать прерывания нужно не во всей функции, а в конкретном ее месте. Делается это так:

    void myFunc2(…)
    {
    uint8_t savedIF;

    // начало критического участка
    savedIF = __save_intreerupt();

    … // небезопасный код

    __restore_interrupt(savedIF);
    // конец критического участка


    }

    с). Для любознательных там есть еще пара функций
    void __diasable_interrupt(void);
    void __enable_interrupt(void);

    d). Функции объявлены в хэдере intrinsics.h . (Не забывайте заинклюдить его.)

    3). Не знаю кому как, но я всегда стараюсь разделять такие проекты (о которых говориться в теме) сразу на два уровня — системный и юзеровский. Разумеется, это не мое «изобретение». Проекты имеют свойство разрастаться, поэтому это делать лучше сразу. Системный слой я прописываю весьма тщательно, а вот в юзеровский уровень выношу всю бизнес-логику и уже не заморачиваюсь, что там кто-то что-то сожрет или потеряет.

    4). Использование volatile это не способ разрулить проблемы совместного доступа к переменным. Это всего лишь команда компилятору — не размещать переменную в регистрах, даже если будет такая возможность. Хотя изначально задумывалось примерно так.

  8. Очень интересная статья, но я что то не понял, зачем делать атомарными операции обращения к порту, скажем
    IN R16,PORTB ; Чтение
    ANDI R16,~(1<<2|1<<3) ; Модификация
    OUT PORTB,R16 ; Запись
    нормально сработает потом восстановится R16 и все норм будет или не так?

    1. если между in и out значение какого либо бита (кроме тех что мы меняем) в portb будет изменено в прерывании то это изменение потеряется.

  9. О Было!. Нужно было сделать переход по результатам сравнения. И как раз во время сравнения выскакивало прерывание по перееполнению таймера и в нем портился флаг SREG. Естесвенно все сравние шло наперекосяк… Будем знать.

  10. Впервые столкнулся с понятием атомарности на многопоточных приложениях и работе с IPC, только, благо, не отгреб, вопрос был новый, поэтому пришлось почитать литературу (писали на C, без ++), а там подобное рассматривается, так что статья правильная, хотя по мне так уже само собой разумеющееся :)

  11. Еще пример атомарной операции, работа с двух- и более -байтовыми величинами. Пока дошел до старшего байта, смотри чтоб младший не поменялся :)

  12. Вставлю свои пять копеек) Вспомнил, что в своем проекте так же хотел сделать запрет прерывания пока выполняются две операции присваивания. Начал тестить (к слову контроллер NEC на ядре V850/ES, среда IAR. Основная задача принимать сообщения по CAN интерфейсу, в прерывании происходит считывание сообщений с физического буфера, при этом в последних двух байтах посылки находится счетчик по которому определяется актуальность сообщения), 100, 200, 500, 3000, 5000 сообщений — полет нормальный, 6000 с хвостиком и опа… пошли сообщения с одинаковым значение счетчика. Хм…не порядок, ломал голову дня два все думал куда еще влепить запрет прерывания. И только когда установил количество физических буферов до 1-го после 2000 с лишним сообщений, прерывания вообще перестали возникать и все тайное стало явным. Оказывается до поры до времени прерывание обходило стороной мой запрет, но в один момент попалось и оставило в буфере непрочитанное сообщение. Контроллер ждет пока считаем и не принимает следующие сообщения.
    Пока на уме два варианта решения:
    1. Убрать запрет > (+) не теряем прерывание, (-) из выше описанной статьи думаю понятно
    2. Сделать проверку флага прерывания после запрета и вызывать процедуру прерывания вручную > (+) все сообщения наши, даже если прерывание потеряно, (-) пока не нашел, но чую есть. Какие есть мнения на этот счет?

    Чет понесло меня)) ну да ладно, надеюсь смысл понятен. Оказывается не все запреты одинаково полезны.

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

      1. ВСЕХ С НОВЫМ ГОДОМ!!! Желаю DI HALT’у успехов и процветания в наступившем году!

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

  13. че то у меня фобия на С появилась походу.
    и размер тебе больше и хрен поймешь чо оно там напрограммируется и вообще….

  14. Приветствую! Надеюсь на вашу помощь и совет :)

    В связи с попыткой впервые сделать более серьезный проект на AVR пытаюсь начать изучать Си. До этого был опыт напсиания программ на асме под AVR и на Delfi под комп. Так что знаний почти никаких. Прочитал только серию статей про Си тут на сайте.

    Появились следующие вопросы… Буду очень благодарен ответам!

    1. Очень заманчиво пользоваться конструкциями типа Blalala.Tusumbreks = doloto;
    я так понимаю, что для этого мне нужно объявить переменную типа struct, верно?
    Очень удобно было бы работать с флагами. Например flags.xren. Где flags — байт, а xren — бит в нем.
    Насколько это хорошо и правильно так делать? Или как лучше?

    2. Допустим, я пишу программу, но еще не развел плату. И не знаю, где именно мне будет удобнее прицепит к контроллеру кнопку. Но я знаю, что у меня будет кнопка.
    Могу ли я каким-нибудь образом сделать код настолько высокоуровневым, чтобы мне было достаточно просто написать:
    if Knopka1==1
    {
    }
    Чтобы я только лишь один раз указал на каком пину и в каком порту у меня эта кнопка и забыл думать об этом.

    3. Тоже самое касается портов. Допустим, у меня три диода на трех пинах одного порта.
    Можно ли автоматизировать все до такой степени чтобы я мог смело писать
    Diod1==1;
    Diod2==0;
    ну или что-то в этом роде
    и чтобы при этом автоматически учитывалось необходимость взять текущее значение порта, изменить и записать новое обратно.

    Благодарю за удаленное мне время!

Добавить комментарий

Ваш e-mail не будет опубликован.

Перед отправкой формы:
Human test by Not Captcha