Уроки MSP430 LaunchPad. Урок 10: Постой паровоз... (Прерывания)

Я закончил последний урок на середине, т.к. нам нужно отойти слегка в сторону, и изучить прерывания. Это один из сложных аспектов микроконтроллеров, особенно для тех, кто не имеет запаса опыта работы с ними. Это похоже на дилемму курицы и яйца: Как изучать таймеры, без прерываний? Как изучать прерывания, не зная того, что их вызывает, например таймеров? Это требует терпения, но с приходом опыта и разбором большого количества примеров, все, постепенно, встанет на свои места. Хочу извиниться за резкую смену темы занятий, но обещаю, что мы вернемся к изучению портов ввода/вывода (GPIO), системы синхронизации и остального позже, на более высоком уровне понимания.

Итак, что такое прерывания? К счастью, вы уже знакомы с языком Си (если нет, настоятельно рекомендую почитать соответствующую литературу), и хорошо управляетесь с вызовом функций. Как вызываются функции в микроконтроллере, вы уже тоже поняли, это ничем не отличается от компьютера. Но микроконтроллеры, так же, взаимодействуют с внешним миром, и зачастую, им необходимо выполнять задачи связанные с событиями за пределами своей схемы. Это можно реализовать через непрерывный опрос входа, или по очереди несколько входов, и если на входе происходит некое событие, вызывается соответствующая функция. Проблема в том, что при такой реализации, микроконтроллер не сможет заниматься ничем другим. Жалко видеть, как он пыхтит, тратя энергию впустую, т.к. не занимается ничем, кроме ожидания сигнала на входе, и это в то время, когда для него полно всякой работы! Лучше бы было, если бы он мог делать свою работу, а когда приходит сигнал извне, прерывал её, обрабатывал сигнал, а затем возвращаться к недоделанной работе. Такая возможность реализована, и имеет логичное название – система обработки прерываний (Interrupt Service Routine (ISR)).

Процедура обработки прерываний


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

  1. Если произошло прерывание от периферийного устройства, соответствующий сигнал устанавливает флаг прерывания в одном из периферийных регистров.
  2. Если в процессорном модуле разрешены прерывания, то наличие флага от периферийного прерывания, устанавливает главный флаг прерываний в ЦПУ.
  3. ЦПУ завершает выполнение всех текущих инструкций.
  4. ЦПУ сохраняет место, где он остановился, поместив (push) адрес следующей инструкции, находящийся в регистре счетчика команд Program Counter (PC), в стек.
  5. ЦПУ сохраняет текущий статус, поместив регистр состояния (Status Register (SR)) в стек.
  6. Если поднято несколько флагов прерываний, выбирается наиболее приоритетное. Приоритет прерываний, можно узнать из спецификации (datasheet) вашего микроконтроллера.
  7. Флаг прерывания обнуляется, за исключением ситуации, когда одно прерывание имеет несколько флагов. В этом случае, флаг должен обнуляться программно, из обработчика прерывания.
  8. Регистр состояния SR очищается, что предотвращает запуск других прерываний, пока работает процедура обработки прерывания (ISR), ЦПУ выводится из режима пониженного энергопотребления (Low Power Mode (LPM)), если в нем находился. Надо заметить, что только маскируемые прерывания, отключаются очисткой регистра состояния SR, что значит, что немаскируемые прерывания, такие как сброс (reset), неисправность тактового генератора и нарушение доступа к флеш-памяти, продолжают обрабатываться.
  9. Адрес вектора прерывания копируется в счетчик команд PC, перенаправляя процессор, на исполнение кода, находящегося по этому адресу.


От появления сигнала прерывания, до начала его выполнения проходит 6-12 тактов. Это не особо большая задержка, но при обработке прерываний, критичных к временной точности, ее нужно учитывать. Если чип работает на частоте 1 мГц, то пройдет 6-12 мкс, прежде чем микроконтроллер среагирует на событие.

Когда процедура обработки прерывания ISR, завершает свою работу, процессор инициирует возврат к выполнению основного кода, который состоит из двух шагов:

  1. Перемещение (pop) сохраненного содержимого регистра состояния из стека в регистр состояния SR.
  2. Перемещение сохраненного адреса следующей команды в счётчик команд PC.
Эти шаги занимают еще 5 тактов, после чего продолжается выполнение основного кода программы. Если микроконтроллер находился в одном из режимов пониженного энергопотребления LPM, до возникновения прерывания, он возвращается в тот же режим LPM автоматически. Если вам не нужен возврат в LPM, вы должны прописать соответствующие инструкции в своей программе. Как это делается, мы узнаем на уроке, посвященном режимам LPM.

Программирование прерываний


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

#pragma vector = <VECTOR_NAME>
__interrupt void <ISR_NAME> (void) {
    // сюда вписывается код обработчика прерывания
}

Ключевое слово #pragma используется в Cи для записи специальных команд конкретного компилятора, что означает, что в другом компиляторе, например gcc, данная строка будет просто проигнорирована. В данном случае, компилятор MSP430, получает команду прикрепить последующий код к вызову прерывания по имени VECTOR_NAME, это имя должно точно повторять, то, что прописано в заголовочном файле вашей модели микроконтроллера, и является удобочитаемой формой записи значения адреса из таблицы векторов прерываний. Заметьте, что строка с ключевым словом #pragma, не имеет точки с запятой в конце.

__interrupt означает начало функции обработчика прерываний. Вы можете написать любое имя обработчика на месте ISR_NAME, но обычно используются описательные имена, такие как P1_ISR, для обработчика прерываний порта P1, или TA0_ISR для обработчика прерывания таймера А0 (в микроконтроллере может быть два таймера А, таймер А0 и таймер А1 соответственно).

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

(В IAR, запись обработчика прерываний выглядит точно так же. – Прим. пер.)

Неиспользуемые прерывания


Что случится, если произойдет прерывание, для которого не записан обработчик? Результат не предсказуем, зависит от того, какие числа окажутся в векторе прерывания. Не контролируемые прерывания, могут стать источниками серьёзных проблем. Самый надёжный способ защититься от этого, написать пустые обработчики для всех неиспользуемых прерываний (или даже запускающие бесконечный цикл, такой способ позволит легко обнаруживать место, где код работает не так, как ожидалось). Это не сильно увеличит размер программы, правда усложнит читабельность кода для других. Другой способ, предотвратить это, индивидуально запретить все не используемые прерывания.

Пример обработки портов GPIO


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

Первое, что нам нужно, это сконфигурировать порт на генерацию прерывания. В LaunchPad кнопка на P1.3, так что будем использовать этот вывод порта. Вывод должен быть настроен как вход. Т.к., благодаря подтяжке вверх, нормальное состояние этого вывода, логическая 1, прерывание должно генерироваться переходом из 1 в 0. Пишем код:

P1IES |= BIT3;  // прерывание по переходу из 1 в 0, 
                // устанавливается соответствующим битом IES.x = 1.
P1IFG &= ~BIT3; // Для предотвращения немедленного срабатывания прерывания,
                // обнуляем его флаг для P1.3 до разрешения прерываний
P1IE |= BIT3;   // Разрешаем прерывания для P1.3

Здесь мы разрешили прерывания для порта P1, по переходу из высокого в низкое состояние, который произойдет при нажатии кнопки, и установит флаг произошедшего прерывания в P1IFG. Однако, вычислительное ядро микроконтроллера, пока не распознает маскируемые прерывания, такие как P1IFG. Нужно разрешить обработку прерываний командой:

_BIS_SR(GIE);

Или эквивалентной ей:

_enable_interrupt();  // Это специальная команда!  Это не прерывание.

Первая команда, намекает своим именем на инструкцию ассемблера bis.w, которая используется для тех же целей. Еще эта команда позволяет, в одну строчку, настроить конфигурацию регистра состояния SR, например можно добавить включение режима экономии энергии (LPM).

Дальше нам необходимо написать обработчик прерывания ISR:

#pragma vector = PORT1_VECTOR
__interrupt void P1_ISR(void) {
    switch(P1IFG&BIT3) {
        case BIT3:
            P1IFG &= ~BIT3;    // обнуляем флаг прерывания для P1.3
            BCSCTL1 = bcs_vals[i];
            DCOCTL = dco_vals[i];
            if (++i == 3)
                i = 0;
            return;
        default:
            P1IFG = 0;   // Возможно в этом нет необходимости, но обнуляем
                         // флаги всех прерываний в P1, на всякий случай. 
                         // Хотя лучше было бы добавить обработчик ошибки.
            return;
    }
} // P1_ISR

Прерывание порта, может возникнуть от восьми отдельных выводов микроконтроллера. И обработчик прерывания, должен выбрать, что делать в каждом из восьми случаев. Оператор выбора switch(), языка Си, идеально подходит в такой ситуации. Мы его используем только для обработки флага BIT3. Если вдруг, по каким-то причинам будут подняты другие флаги прерывания порта P1, мы просто обнулим их все, перед выходом. Т.к. остальные флаги не разрешены, этого не может произойти, но лучше исключить любую возможность, такие вещи полезно делать, при программировании микроконтроллеров. В идеале вставить обработку ошибки, но мы не станем этого делать здесь.

Заметьте, что, так как прерывание порта P1, имеет восемь разных источников, с отдельным флагом для каждого, нам нужно обнулять эти флаги самим. Не забывайте об этом, иначе ваша программа зависнет внутри обработчика прерывания, т.к. он будет вызываться вновь и вновь. Эта программа содержит по три, сохраненных в массивах, значения для регистров тактового генератора BCSCTL1 и DCOCTL, и переключается между ними по очереди, во время каждого прерывания. Эти переменные, как и переменная счётчика i, должны быть глобальными, что бы обработчик прерывания видел их, поэтому они объявляются за границами функции main(). Вот, полный текст программы (оригинал interrupted-1_G2211.c):

/* interrupted-1_G2211: Эта программа, переделанная версия dcodemo
 * с использованием прерываний.  Нажатие кнопки P1.3
 * запускает цикл смены частоты тактового генератора DCO.
 */

#include <msp430g2211.h>

#define LED1  BIT0


/*  Глобальные переменные  */
char i=0;                   // резервируем память типа char (один байт)
char bcs_vals[3] = {7,9,2};
char dco_vals[3] = {3,5,6};

/*  Объявление функций  */
void delay(void);

void main(void) {
	
	WDTCTL = WDTPW + WDTHOLD;    // отключаем сторожевой таймер
	
	P1OUT = 0;
	P1DIR = LED1;  // P1.0 выход на светодиод, P1.3 вход для кнопки
      // следующих две строчки для LaunchPad версии 1.5
      //P1REN |= BIT3; //разрешаем подтяжку
      //P1OUT |= BIT3; //подтяжка вывода P1.3 вверх
	P1IES |= BIT3; // прерывание по переходу 1->0 выбирается битом IESx = 1.
	P1IFG &= ~BIT3; // Для предотвращения немедленного вызова прерывания,
                        // обнуляем его флаг для P1.3 до разрешения прерываний
	P1IE |= BIT3;   // Разрешаем прерывания для P1.3
	
	_enable_interrupt();
	
	for (;;) {             // Изменения в логике, вместо мигания пять раз
		P1OUT ^= LED1; // эта программа, мигает непрерывно
		delay();
	}
} // main

void delay(void) {
	unsigned int n;
	for (n=0; n<60000; n++);
} // задержка

/*  Обработчики прерываний  */
#pragma vector = PORT1_VECTOR
__interrupt void P1_ISR(void) {
	switch(P1IFG & BIT3) {
		case BIT3:
			P1IFG &= ~BIT3;  // обнуляем флаг прерывания
			BCSCTL1 = bcs_vals[i];
			DCOCTL = dco_vals[i];
			if (++i == 3)
				i = 0;
			return;
		default:
			P1IFG = 0;// Возможно в этом нет необходимости, но обнуляем
                                  // флаги всех прерываний в P1, на всякий случай. 
                                  // Хотя лучше было бы добавить обработчик ошибки.
			return;
	}
} // P1_ISR

Архив с программами из урока.

Примечания переводчика: Как и в предыдущих уроках, я подправил пример, добавив туда пару строк, без которых кнопка не заработает в LaunchPad версии 1.5, т.к. там нет резистора подтяжки на кнопке. Для того, что бы заработало в IAR, нужно заменить _enable_interrupt(); на __enable_interrupt(); в IAR, эта команда с двумя прочерками вначале. Так же особенность этого примера в том, что не обрабатывается дребезг контактов, поэтому нажатие кнопки часто приводит к переключению сразу через несколько режимов работы программы.


Пример работы с таймером А


Для второго примера, сделаем нечто подобное тому, что я делал в своем проекте выключателя света на реле. Я написал программу с использованием выхода P1.6, так что можете наблюдать логику её работы по поведению зеленого светодиода, вместо реле. Если у вас есть реле, убедитесь что правильно его подключили, дабы избежать чрезмерной нагрузки на порт микроконтроллера. (Порты у MSP430 довольно слабенькие, например, согласно спецификации, максимальный выходящий ток на один порт G2553 = 48 мА, это плата за низковольтность и экономичность – Прим. пер.). Используется откалиброванная частота тактового генератора DCO 1 мГц, с делителем 8, что приводит к скорости счёта таймером А до 62500 примерно за 0.5 с. Это мое реальное приложение, мне нужно включать свет на несколько часов, затем выключать на несколько часов (Коноплю он что ли выращивает? – Прим. пер.). Это проще делать на низкой тактовой частоте, и идеальное приложение для использования низкочастотного тактового генератора LFXT1. Но, так как, он не у всех подключен, и у многих не припаян часовой кварц к LaunchPad, мы просто сделаем больше коротких циклов включения/выключения, для эмуляции реальной работы. Будем использовать генератор DCO для включения и выключения светодиода с интервалом в одну минуту. Для этого включим таймер A в режиме прямого счёта, с помощью регистра CCR0 и включим прерывание таймера регистром CCTL0. (Да, это более навороченная версия программы-мигалки. Оказывается микроконтроллерный «Привет мир!», может иметь реальное применение!).

Сконфигурируем таймер, с помощью таких строк:

TACCR0 = 62500 - 1;  // период 62,500 циклов, от 0 до 62,499.
TACCTL0 = CCIE;  // Разрешаем прерывание таймера по достижению значения CCR0.
TACTL = TASSEL_2 + ID_3 + MC_1 + TACLR;

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

  • TASSEL_2 — источник тактов таймера SMCLK (SubMainCLocK), по умолчанию он работает от DCO.
  • ID_3 — делитель частоты на 8, от 1MHz это будет 125kHz
  • MC_1 — режим прямого счёта, от 0 до значения, записанного в TACCR0
  • TACLR — начальное обнуление таймера
(Я записал бы строку присвоения так: TACTL |= TASSEL_2|ID_3|MC_1|TACLR; это более Ъ вариант. – Прим. пер.). Все эти мнемонические сокращения, определены в заголовочном файле конкретной модели микроконтроллера, поэтому нам не приходиться манипулировать «волшебными» числами, назначая значения битов вручную. Полезно заглядывать в заголовочный файл, что бы узнавать остальные мнемоники для битов конфигурации.

Обработчик прерывания таймера, будет выглядеть как-то так:

#pragma vector = TIMERA0_VECTOR
__interrupt void CCR0_ISR(void) {
    // Сброс флага прерывания таймера происходит автоматически, ручное обнуление не нужно
    if (++i == 120) {
        P1OUT ^= RLY1;
        i = 0;
    }
} // CCR0_ISR

Опять, таки мы используем глобальную переменную i как счётчик. А RLY1 (реле 1) определено как BIT6 в заголовке программы. Каждый раз (дважды в секунду), когда поднимается флаг прерывания таймера, счётчик увеличивается. Когда он достигает значения 120 (за 60 секунд), реле (у нас светодиод) переключается, и счётчик сбрасывается.

Надеюсь, эти два примера программ, позволят вам разобраться с работой прерываний в MSP430. Вот полный листинг второй программы, можете менять значение периода таймера на любую разумную величину (оригинал interrupted-2_G2211.c):

/* interrupted-2_G2211: Эта программа демонстрирует использование прерываний 
 * на примере переключения реле (или светодиода, в демонстрации) 
 * за период в одну минуту. Обычная программа-мигалка «Привет мир!».
 */

#include <msp430g2211.h>

#define RLY1  BIT6;

/*  Глобальные переменные  */
char i=0;


/*  Объявление функций  */

void main(void) {
	WDTCTL = WDTPW + WDTHOLD;    // отключаем сторожевой таймер
	
	P1OUT = 0;                  
	P1DIR = RLY1;  // P1.6 выход на реле (или светодиод)
	
	BCSCTL1 = CALBC1_1MHZ; // Устанавливаем частоту DCO на калиброванные 1 MHz.
	DCOCTL = CALDCO_1MHZ;
	
	TACCR0 = 62500 - 1;    // Период в 62,500 цикла, от 0 до 62,499.
	TACCTL0 = CCIE;        // Разрешаем прерывание таймера по достижению значения CCR0.
	TACTL = TASSEL_2 + ID_3 + MC_1 + TACLR; // Настройка режима работы таймера Timer_A:
                                        // TASSEL_2 - источник тактов SMCLK (SubMainCLocK),
                                        // по умолчанию настроенных на работу от DCO
                                        // ID_3 - делитель частоты на 8, от 1MHz это будет 125kHz
                                        // MC_1 - режим прямого счёта (до TACCR0)
                                        // TACLR - начальное обнуление таймера	
	_enable_interrupt();
	
	for(;;) { // Ничего не делаем, ждем прерывание. Идеальное место для использования
	}         // экономных режимов работы микроконтроллера!
} // main

/*  Обработчики прерываний  */
#pragma vector = TIMERA0_VECTOR  
__interrupt void CCR0_ISR(void) {
	if (++i == 120) {
		P1OUT ^= RLY1;
		i=0;
	}
} // CCR0_ISR

Архив с программами из урока.

Примечание переводчика: Для того, что бы заработало в IAR, нужно заменить _enable_interrupt(); на __enable_interrupt(); в IAR, эта команда с двумя прочерками вначале. Если у вас другой микроконтроллер, посмотрите в конец его заголовочного файла, что бы узнать правильное название вектора прерывания таймера. Например у G2553, он называется TIMER0_A0_VECTOR.


Оригиналы статей на английском: Tutorial 10-a: Something Completely Different (Interrupts) и mspsci.blogspot.fr/2010/08/tutorial-10-b-interrupt-examples.html

Предыдущий урок этого цикла: Урок 09: Таймеры
Следующий урок этого цикла: Урок 11: Экономия, должна быть экономной!
  • +17
  • 17 ноября 2012, 05:49
  • Tabke
  • 1
Файлы в топике: lesson10.zip

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

RSS свернуть / развернуть
Спасибо за перевод. Читаю с удовольствием хотя и читал оригинал.

Дальнейшее к переводу отношения не имеет.
Пробую всё под msp430-gcc.

Для того что бы заработало в msp430-gcc нужно:
— в начале

#ifdef MSP430
#include <legacymsp430.h> //for msp430-gcc interrupts defenition
#endif

— прерывания разрешить командой

        //_enable_interrupt();
	_BIS_SR(GIE);

— прерывания определить так:

/*  Обработчики прерываний  */
#ifdef MSP430
interrupt(TIMER0_A0_VECTOR) CCR0_ISR(void) {
#else
#pragma vector = TIMERA0_VECTOR  
__interrupt void CCR0_ISR(void) {
#endif

        if (++i == 120) {
                P1OUT ^= RLY1;
                i=0;
        }
} // CCR0_ISR
+1
  • avatar
  • mvb
  • 27 ноября 2012, 16:56
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.