Работаем с MSP430 в Linux-е. Lesson 3

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

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

Во времена DOS люди писали свои программы примерно так:

void main (void)
{
  // начало работы, инициализация переменных
  ...
  // еще какие-то действия
  ...
  do
  {
    // узнаем, было-ли нажатие на клавиатуре
    if (keypress() != 0)
    {
      // Определить нажатую кнопочку и выполнить соответствующее действие
      switch (getkey())
      {
      case 'A':
        ...
        break;

      case 'B':
        ...
        break;

      default:
        ...
      }
      ...
    }

    // узнаем, истекло ли время
    if (timeout() == 0)
    {
      // Квант времени закончился. Нужно, допустим, перерисовать экран
      refresh_screen();
      ...
    }

    // пришел ли сигнал от последовательного порта
    if (COM_port_ready() == 1)
    {
      char = getCOM();
      ...
    }

    // проверим готовность еще какого-нибудь устройства
    if (a_ready() == 1)
    {
      ...
    }
  } while (exit_flag == 1);

  //завершающие действия
  return;
}

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

Но чем плохо такое построение программ?

Плохо оно тем, что, во первых, все делается последовательно. Пока мы не обработаем клавиатуру, мы не можем заняться перерисовкой экрана. Пока не перерисуем экран, мы не будем обрабатывать клавиатуру. Не знаю как вы, но я видел реакцию пользователей и результат этой реакции — продавленные насквозь кнопки. Пользователь ведь не знает почему программа не реагирует на нажатие. Он считает, что не сработала кнопка, поэтому увеличивает на нее давление. А кнопка давно сработала, просто процессор занят несколько другими делами и не может в данный момент не то, что заняться обработкой нажатия на кнопку, а даже хотя бы пикнуть, что «нажатие состоялось. Успокойся! Я зарегистрировал нажатие, но отработаю позже.»

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

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

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

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

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

Вернитесь ко второму уроку, посмотрите на программу. Что вы видите? Программа постоянно что-то делает, ни на секунду не останавливается. Зажгли светодиод, вызываем функцию delay() чтобы тупо убить время, потушили светодиод, снова убиваем время. И так по кругу. В функции-убийце времени процессор тоже не отдыхает — тупо молотит воду в ступке. Крутая программа! Но работает. Понятно, что профи так не пишут.

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

Под словом «замирает» я подразумеваю такое понятие как «процессор останавливается» (засыпает крепким сном). В это время потребление энергии минимальное, аккумуляторы не расходуются.

И только при возникновения каких-либо событий в системе (мы их сами определяем) процессор резко просыпается, быстро выполняет заданные действия и снова впадает в спячку.

Так, например, если событие возникает один раз в секунду, а время обработки этого события составляет 10 мкс, то правильно написав программу мы понизим энергопотребление в 100 тысяч раз. Ого!

Впечатляет? — А то!

Ну, реально такого, конечно, нет, потому что помимо ядра процессора есть и другие потребители. Но снижение энергопотребления в 100-1000 раз вполне посильная задача.

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

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

Пока мы не не написали и не залили новую версию программы в микроконтроллер, давайте померим потребляемый ток старой версии. Для этого нужно на плате LaunchPad удалить все джамперы J3. Миллиамперметр нужно подключить к контактам J3, которые обозначены «VCC». Не забудьте отключить светодиод джампером J5 (, иначе вы померите и его ток).

Я залил программу из второго урока в три экземпляра MSP340F2001 и померил у них потребляемый ток. Все они потребляли примерно 360-370 мкА. Все правильно, Texas Instruments сообщает, что при питании 3.3 В и тактовой частоте 1 МГц MSP430F2001 потребляют примерно 350 мкА.

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

Вообще в микроконтроллерной технике для этого дела применяются обычные таймеры, которые неизменно присутствуют во всех микроконтроллерах. В подавляющем большинстве микроконтроллеров таймеры исполнены в нескольких экземплярах. Например, в микроконтроллерах STM32Fxxx таймеров может быть аж 17 штук! Как привило, это универсальные таймеры. Но в некоторых микроконтроллерах имеются специальные таймеры как раз для генерации тиков (щелчков) системного времени. Например, в микроконтроллерах AT91SAM7xxx фирмы ATMEL имеется периодический интервальный таймер PIT. В микроконтроллерах, которые построены на базе ядра Cortex, системный таймер заложен даже в самом ядре. Называется он SYSTICK.

У нас с вами микроконтроллер MSP430F2001, и он намного проще, чем эти. В нем нет выделенного системного таймера. Поэтому нам пришлось бы в качестве системного таймера использовать таймер общего назначения Timer_A. Но, к счастью, Texas Instruments предоставила нам возможность использовать сторожевой таймер WDT в качестве системного.

В традиционном применении сторожевой таймер применяется для перезапуска микроконтроллера. Представьте себе ситуацию, когда какое-нибудь автономное устройство (до которого к тому же еще и очень сложно добраться) «словило» помеху и программа зависла. Устройство перестало правильно реагировать на внешние воздействия. Кнопка «Reset» поможет. Но кто ж ее нажмет!? Рядом никого нет.

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

В общем, получается вроде гонки — кто кого сбросит. Если прога работает нормально, у сторожевого таймера нет шансов. Но если прога отрубилась, то теперь шансов нет уже повесившейся проги. Таким образом, мы обеспечиваем «непотопляемость» устройства. По-моему, это очень замечательно!

Нам пока не нужна эта чудесная функциональность. Нам требуется простая «нарезка» времени. Поэтому мы должны перевести сторожевой таймер WDT в режим интервального таймера. В этом режиме WDT по истечение времени не будет перезагружать микроконтроллер, но будет вызывать прерывание в его работе.

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

Прерывание — это событие в системе. Что мы должны сделать при возникновении события? — нам нужно переключить состояние светодиода. Вот эту работу по переключению мы и будем выполнять в обработчике прерывания.

Хорошо. А что нам нужно делать еще, кроме этого прерывания? — В этой программе нам ничего больше не требуется делать. А раз делать нечего, мы можем смело микроконтроллер отправлять в спячку.

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

Вот код этой классной программы:

#include <msp430f2001.h>

#define GREEN_LED (BIT6)

void wdt_isr(void) __attribute((interrupt(WDT_VECTOR)));
void wdt_isr(void)
{
  if ((P1OUT & GREEN_LED) == 0x00)
    P1OUT |= GREEN_LED;
  else
    P1OUT &= ~GREEN_LED;
}

int main(void)
{
  WDTCTL = WDTPW + WDTTMSEL + WDTCNTCL + WDTSSEL + WDTIS0;
  BCSCTL3 = LFXT1S1;

  P1DIR = 0xFF;

  IE1 |= WDTIE;
  __bis_SR_register(LPM3_bits + GIE);
  while (1)
  {
  }
}

Код программы несколько изменился. И я сейчас следуя традиции поясню его.

Следующий фрагмент программы:

void wdt_isr(void)
{
  if ((P1OUT & GREEN_LED) == 0x00)
    P1OUT |= GREEN_LED;
  else
    P1OUT &= ~GREEN_LED;
}

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

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

isr — это сокращение от Interrupt Service Routine — Подпрограмма Обработки Прерывания или короче — обработчик прерывания. Вы наверняка уже встречались с этой аббревиатурой. Так что можете смело использовать ее, другие программисты вас поймут однозначно.

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

void wdt_isr(void) __attribute((interrupt(WDT_VECTOR)));

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

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

Теперь, если вы не поленитесь открыть файл
/usr/local/msp430/include/msp430f2001
, то в конце файла вы увидите вот такой фрагмент:

/************************************************************
* Interrupt Vectors (offset from 0xFFE0)
************************************************************/

#define PORT1_VECTOR        (0x0004)  /* 0xFFE4 Port 1 */
#define PORT2_VECTOR        (0x0006)  /* 0xFFE6 Port 2 */
#define TIMERA1_VECTOR      (0x0010)  /* 0xFFF0 Timer A CC1-2, TA */
#define TIMERA0_VECTOR      (0x0012)  /* 0xFFF2 Timer A CC0 */
#define WDT_VECTOR          (0x0014) /* 0xFFF4 Watchdog Timer */
#define COMPARATORA_VECTOR  (0x0016) /* 0xFFF6 Comparator A */
#define NMI_VECTOR          (0x001C) /* 0xFFFC Non-maskable */
#define RESET_VECTOR        (0x001E) /* 0xFFFE Reset [Highest Priority] */

Это — названия векторов прерываний.

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

Вот подсказка тем, кто не сообразил. Здесь приведен шаблон для обработчика прерываний от порта P1:

void port1_isr(void) __attribute((interrupt(PORT1_VECTOR)));
void port1_isr(void)
{
  // тут что-то делается при возникновении прерывания от порта
  ...
}

А вот обработчик прерываний от компаратора:

void comparator_isr(void) __attribute((interrupt(COMPARATORA_VECTOR)));
void comparator_isr(void)
{
  // тут что-то делается при возникновении прерывания от компаратора
  ...
}

Уловили суть? — Отлично!

Как видите, количество прерываний у MSP430F не такое уж и большое. Для сравнения у микроконтроллеров с ядром Cortex-M3 количество прерываний исчисляется чуть ли не сотнями. Даже у микроконтроллеров AVR прерываний десятки. Но переживать из-за этого не стоит. Позже вы поймете, даже с этим количеством можно весьма успешно работать.

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

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

Каждое из периферийных устройств микроконтроллера по своему настраивается на генерацию прерываний. Давайте посмотрим, как это делается в отношении сторожевого таймера.

Вы будете удивлены, но все делается одной строкой:

  WDTCTL = WDTPW + WDTTMSEL + WDTCNTCL + WDTSSEL + WDTIS0;

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

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

WDTPW — это так называемый «пароль» для обращения к сторожевому таймеру. Если вы его укажите не правильно, то WDT перезагрузит микроконтроллер.

WDTTMSEL — этим мы задаем режим работы сторожевого таймера. Устанавливая это бит мы говорим, что сторожевой таймер будет работать в как интервальный таймер.

WDTCNTCL — Устанавливая этот бит мы обнуляем сторожевой таймер. Дело в том, что пока наша программа добиралась до этой строки, сторожевой таймер тикал. И наверняка уже натикал какое-то ненулевое значение. Чтобы не случилось худа, мы на всякий случай обнуляем счетчик сторожевого таймера.

WDTSSEL — этот бит выбирает источник тактирования сторожевого таймера. В нашем распоряжении таких источников два. Первый из них — это дополнительный тактовый сигнал SMCLK, а второй — вспомогательный тактовый сигнал ACLK.

Пока глубоко в дебри не полезем, я только скажу чем они отличаются. ACLK — это низкочастотный тактовый сигнал, его частота равна 12 кГц. SMCLK — высокочастотный. Его частота — около 1 МГц. Если мы не вмешиваемся с систему тактирования ядра процессора, то оно будет тактироваться от SMCLK.

WDTIS1 и WDTIS0 — эти два бита (в программе установлен только один — WDTS0) определяют до скольки должен сосчитать сторожевой таймер, прежде чем  перезагрузить проц или вызвать прерывание. если оба бита установлены, то WDT будет считать до 64, если оба сброшены в ноль, то до 32768. Другие значения вы может посмотреть в документации микроконтроллера.

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

Мы выбрали источник с тактовой частотой 12 кГц и установили бит WDTS0. Таким образом сторожевой таймер будет считать до значения 8192 со скоростью 12 тысяч раз в секунду. Заданного значения таймер достигнет за время чуть менее одной секунды. Если быть более точным то через 863 мс. После чего возникнет прерывание и светодиод изменит свое состояние на противоположное.

Следующая строка в программе

  BCSCTL3 = LFXT1S1;

задает в качестве источника для сигнала ACLK встроенный в микроконтроллер микромощный RC-генератор с частотой 12 кГц. Если это не сделать, в качестве сигнала ACLK будет фигурировать сигнал от низкочастотного кварцевого генератора XTS1, который, как правило, имеет частоту 32768 Гц. Это тактовая частота почти всех часов реального времени. Но для того чтобы мы могли воспользоваться этой возможностью, нам нужно включить паяльник и установить на плате кварцевый резонатор и пару конденсаторов. Мы сейчас не будем этим заниматься, а воспользуемся генератором на 12 кГц.

Мы видим, что в нашей измененной программе появились еще две строки:

  IE1 |= WDTIE;
  __bis_SR_register(LPM3_bits + GIE);

В первой из них мы разрешаем прерываниям от сторожевого таймера приходить («стучаться в двери») к ядру микроконтроллера. Но для того, чтобы микроконтроллер мог реагировать на прерывания нужно еще поднять флаг разрешения прерываний — GIE.

Это происходит во второй строке. Кроме поднятия флага GIE, мы так же приказываем микроконтроллеру заснуть крепким сном — устанавливаем с помощью одноименных битов режим работы микроконтроллера LPM3. Во время этого сна вообще все выключается, даже тактовый генератор, который используется для тактового сигнала SMCLK. Таким образом, останавливается и само ядро процессора. Все замерзло. И только внутри сторожевого таймера еще теплится жизнь с очень-очень маленькой скоростью.

Если вы померите потребляемый микроконтроллером ток, … если вы вообще что-то сможете увидеть,… то окажется, что он менее 0.1 мкА. Не всякий тестер может его почувствовать. Процессор MSP430F почти ничего не потребляет, и в то же время программа работает!

Вы видите, что в основном цикле нашей программы ничего нет:

  ...
  while (1)
  {
  }

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

Следует отметить еще одну характерную особенность микроконтроллера MSP430. Выражается это в том, что после окончания работы в прерывании микроконтроллер попадает в то же самое состояние, в каком он был за мгновение до этого прерывания. И если микроконтроллер спал, то после обработки прерывания он снова заснет. Например, в микроконтроллерах AVR этого не случается и приходится микроконтроллер снова искусственно усыплять.

У многих микроконтроллеров имеются биты состояния. Они определяют должен ли микроконтроллер бодрствовать или он должен спать. И если он должен спать, то насколько глубокий будет у него сон.

Так вот, эти биты у MSP430 расположены непосредственно в регистре состояния. У других же микроконтроллеров они располагаются в других регистрах. Когда возникает прерывание, регистр состояния процессора сохраняется. Но у MSP430 сохранение регистра состояния происходит с теми флагами, которые были до момента возникновения прерывания. А у других микроконтроллеров, флаги режимов работы процессора находятся вообще в других регистрах, которые, разумеется не сохраняются.

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

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

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

Большой разницы между MSP430 и другими процами — нет. Но в других процах приходится делать немного лишней работы, которая пожирает аккумуляторы.

Ну вот и всё, ребята! Вы познакомились с системой, которая управляется событиями. Кстати, эти системы так и называются — Event Driven Systems.

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

  if ((P1OUT & GREEN_LED) == 0x00)
    P1OUT |= GREEN_LED;
  else
    P1OUT &= ~GREEN_LED;

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

Давайте воспользуемся замечательными способностями языка С и напишем наш код вот так:

  P1OUT ^= GREEN_LED;

Да-да, всего одна строка! Что она делает? Она считывает содержимое порта, затем «переворачивает» бит GREEN_LED в противоположное состояние и записывает это обратно в порт. Не правда ли, красивое решение?

Я еще раз перепишу нашу программу

#include <msp430f2001.h>

#define GREEN_LED (BIT6)

void wdt_isr(void) __attribute((interrupt(WDT_VECTOR)));
void wdt_isr(void)
{
  P1OUT ^= GREEN_LED;
}

int main(void)
{
  WDTCTL = WDTPW + WDTTMSEL + WDTCNTCL + WDTSSEL + WDTIS0;
  BCSCTL3 = LFXT1S1;

  P1DIR = 0xFF;

  IE1 |= WDTIE;
  __bis_SR_register(LPM3_bits + GIE);
  while (1)
  {
  }
}

и пожелаю вам терпения в изучении науки программирования микроконтроллеров!

Будьте рядом, всё только начинается!

Advertisements

2 responses to “Работаем с MSP430 в Linux-е. Lesson 3

  1. ещё забыли выкинуть:
    20 while (1)
    21 {
    22 }
    поскольку контроллер никогда не выйдет из прерывания в не LPM3

    • Можно и выкинуть.

      Мне с этим пустым циклом как-то ментально спокойнее. Кому как нравится.
      Впрочем, оптимизатор этот цикл и без нашей помощи сам выкидывает.

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

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

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s