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

После вынужденного перерыва я продолжаю свои уроки. Тех, кто еще не охладел, приглашаю — welcome!

Разглядывая текст программы, вы наверняка обратили внимание, что там присутствуют известные Си-шные лексемы (слова) такие как include, int, return, for, main, while. Но в тексте так же присутствуют незнакомы лексемы — WDTCTL, WDTPW, WDTHOLD, P1DIR, P1OUT. Откуда они? Что они из себя представляют?

Давайте не спеша по порядку разберемся откуда у них растут ноги.

Вы наверняка уже знаете, что все семейство микроконтроллеров MSP430 — это микроконтроллеры Фон-Неймановского класса. Это значит, что имеется единое адресное пространство, в котором размещаются коды программ, константы и переменные, стек, а так же регистры периферийных устройств.

Рассматривайте адресное пространство как некий аналог улицы. На одной улице не бывает двух домов одинаковым номером. Но какой-то номер дома может повторяться на разных улицах. Так и с памятью, если бы имели дело с микроконтроллерным ядром Гарвардского класса (типичные его представители — 51-ое ядро, AVR), то мы бы имели несколько никак не связанных областей памяти. У каждой области память своя адресация. И байт, например, находящийся по адресу 1000 в какой-то одной области, это совсем не тот байт, который находится в другой области памяти. Области памяти и данные, которые находятся по этим адресам — разные, но адреса могут чисто математически совпадать.

Так вот, у MSP430 на все про все единое адресное пространство. И если мы пишем что-то типа

int a;

то это значит, что где-то этом адресном пространстве для переменной a будет выделено место. Переменную a мы сами придумали. Но компилятор сам неплохо разбирается в вопросах размещения переменных в памяти. Нам нет никакой необходимости знать ее точный адрес. Мы ведь к переменной обращаемся не по адресу, а по ее имени. Это после компиляции исходных кодов имена переменных (и имена функций) перестают существовать. В этом случае вместо имен в командах фигурируют адреса переменных (и функций).

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

WDTCTL — регистр управления сторожевым таймером

IE1 — регистр разрешения прерываний

IFG1 — регистр флагов прерываний.

Эти регистры также располагаются в едином адресном пространстве. Но в отличие от нашей переменнй a, они имеют строго фиксированное расположение. Так, например, регистр WDTCTL располагается по адресу 0x0120, регистр IE1 — по адресу 0x0000, а регистр IFG1 — по адресу 0x0002.

Обращаться к регистрам по имени куда легче, чем запоминать их адреса! К счастью разработчики уже сделали за нас работу — составили таблицы соответствия имен регистров и их адресов размещения. Эти таблицы находятся в файлах типа msp430f2001.h, который мы подключаем к нашей программе в самом начале с помощью строки

#include <msp430f2001.h>

Файл находится в директории /usr/local/msp430/include .

Если вы не поленитесь и откроете его, то увидете много чего интересного. Та, например, ближе к концу файла (строка 486-я) имеется такая запись:

#define WDTCTL_               0x0120    /* Watchdog Timer Control */
sfrw(WDTCTL, WDTCTL_);

Это ничто иное, как соответствие имени WDTCTL и числа 0x0120. Вы уже знаете как это понимать.

Посмотрите на следующие строки:

#define WDTIS0              (0x0001)
#define WDTIS1              (0x0002)
#define WDTSSEL             (0x0004)
#define WDTCNTCL            (0x0008)
#define WDTTMSEL            (0x0010)
#define WDTNMI              (0x0020)
#define WDTNMIES            (0x0040)
#define WDTHOLD             (0x0080)

#define WDTPW               (0x5A00)

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

  WDTCTL = WDTPW + WDTHOLD;

которую после подстановок можно интерпретировать так

  (void *) 0x0120 = 0x5A00 + 0x0080;

то есть по адресу 0x0120 записать число 0x5A80.

Удобно? —Конечно! Давайте и в нашей программе с нашими переменными будем поступать так же!

Мы обращаемся к порту P1 и зажигаем или гасим светодиод. Название порта уже имеется в описании (в файле msp430f2001.h), а вот дать очеловеченное название цепи светодиода — это наша задача. Никто, кроме нас, не знает что, на порт P1 к 6-ому биту подключен зеленый светодиод. И, согласитесь, строка

  P1OUT = 0x40;

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

  P1OUT = GREEN_LED;

Вместо безликой константы 0x40 у нас появилось очеловеченное имя GREEN_LED. Но он пока еще не определено, ему еще ничто не соответствует. Это соответствие должны указать мы, и лучше это сделать в самом начале программы.

Мы могли бы написать

#define GREEN_LED (0x40)

и это было бы верным определением. Но оно тоже не особо информативно — попробуй-ка быстро сообрази какой бит соответствует числу 0x40 !

Разработчики уже позаботились дать даже битам очеловеченные имена. Откройте еще раз файл msp430f2001.h и прокрутите его в начало. Со строки 66 имеются определения для всех битов:

/************************************************************
* STANDARD BITS
************************************************************/

#define BIT0                (0x0001)
#define BIT1                (0x0002)
#define BIT2                (0x0004)
#define BIT3                (0x0008)
#define BIT4                (0x0010)
#define BIT5                (0x0020)
#define BIT6                (0x0040)
#define BIT7                (0x0080)
#define BIT8                (0x0100)
#define BIT9                (0x0200)
#define BITA                (0x0400)
#define BITB                (0x0800)
#define BITC                (0x1000)
#define BITD                (0x2000)
#define BITE                (0x4000)
#define BITF                (0x8000)

Таким образом, мы может дать свое шикарное определение нашей цепи зеленого светодиода:

#define GREEN_LED (BIT6)

Для того чтобы «засинхронизировать» наши изменения в программе я еще раз приведу ее (уже измененный) текст:

#include <msp430f2001.h>

#define GREEN_LED (BIT6)

int delay(int t)
{
  int i, s;

  s = 0;
  for (i = 0; i < t; i++)
    s += i;

  return s;
}

int main(void)
{
  WDTCTL = WDTPW + WDTHOLD;
  P1DIR = 0xFF;

  while (1)
  {
    P1OUT = GREEN_LED;
    delay(10000);
    P1OUT = 0x00;
    delay(10000);
  }
}

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

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

  P1DIR = 0xFF;
...

    P1OUT = 0x00;

В первом случае мы должны развернуть все линии порта на выход. Это осуществляется записью в них единицы. По идее нам нужно было бы употребить такую команду:

  P1DIR = BIT7 + BIT6 + BIT5 + BIT4 + BIT3 + BIT2 + BIT1 + BIT0;

или

  P1DIR = BIT7 + GREEN_LED + BIT5 + BIT4 + BIT3 + BIT2 + BIT1 + BIT0;

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

Следующая команда, которая вообще-то должна выключать только светодиод, не задевая другие биты, выключает (обнуляет) вообще весь порт. Это не допустимо! Но поскольку в нашем крошечном проекте другие биты не используются, мы можем схалтурить. Но вообще, правильно писать так:

    P1OUT = P1OUT & ~GREEN_LED;

Эта команда не просто выводит в порт какое-то число, а делает более сложное действие. Она сначала считывает из порта его состояние, затем обнуляет 6-ой бит.

Для обнуления бита мы используем следующий прием. Мы берем исходную маску GREEN_LED (равную 0x40) и с помощью операции ~ переворачиваем биты (то есть там, где был нуль, будет единичка, а вместо единички — нуль). В результате этой операции у нас получится маска (число) 0xBF. Вы видите, что все биты кроме 6-го находятся в единичном состоянии. Теперь мы объединяем считанное из порта значение и эту маску по «И». В результате все биты, за исключением 6-го, останутся в прежнем состоянии, а 6-ой обнулиться.

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

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

    P1OUT = P1OUT | GREEN_LED;

Работает она следующим образом. Как и в предыдущем случае мы считываем содержимое порта, затем объединяем его по ИЛИ с маской GREEN_LED. В результате все биты, за исключением 6-го, остаются в прежнем состоянии, а 6-ой устанавливается в единицу. Полученное значение мы записываем обратно в порт. Таким образом, мы работаем только с нашим битом, другие биты мы никак не изменяем.

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

    P1OUT |= GREEN_LED;
...
    P1OUT &= ~GREEN_LED;

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

#include <msp430f2001.h>

#define GREEN_LED (BIT6)

int delay(int t)
{
  int i, s;

  s = 0;
  for (i = 0; i < t; i++)
    s += i;

  return s;
}

int main(void)
{
  WDTCTL = WDTPW + WDTHOLD;
  P1DIR = 0xFF;

  while (1)
  {
    P1OUT |= GREEN_LED;
    delay(10000);
    P1OUT &= ~GREEN_LED;
    delay(10000);
  }
}

Вы можете задаться вопросом «А зачем вообще нужны эти танцы с бубном?» Дело в том, что в жизни нередко приходится переназначать ножки микроконтроллера. Допустим, нам пришлось изменить печатную плату и мы переместили зеленый светодиод с 6-го бита на 3-ий. Если мы написали программу согласно этому стилю, то нам нужно будет изменить всего одну строку:

#define GREEN_LED (BIT3)

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

Ну и как бонус можно говорить о том, что текст нашей программы «самодокументирован». Нам нет нужды расшифровывать что за константа такая используется — 0x40. Из текста программы итак ясно, что это линия управления зеленым светодиодом, который подключен к 6-му биту порта P1. Остается прокомментировать только туманные места где не совсем понятно что и для чего мы делаем. Но это, опять же, специфично для больших программ. Мелкие проги типа этой, если их писать в этом стиле, то они вообще в комментариях не нуждаются.

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

А на сегодня — всё!
Увидимся!

Advertisements

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

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

Логотип WordPress.com

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

Фотография Twitter

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

Фотография Facebook

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

Google+ photo

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

Connecting to %s