Работа с GPIO

Опубликовано в рубрике "Статьи", 06.10.2009.

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

gpio

Обзор

Когда я начал программировать контроллеры на С (до этого был еще ассемблер), с ножками контроллеров я работал примерно так:

PORTB |= (1<<PB7);
PORTB &= ~(1<<PB7);
 
И все, вроде-бы было хорошо, пока проект не стал более-менее большим, а количество ножек контроллера перевалило за 100. После изменения разводки платы приходилось долго выискивать все эти "PORTB" по проекту. Периодически я забывал писать “1<<” и проводил часы свой жизни в поисках таких, казалось бы простых ошибок.

И я начал искать более красивый способ работы с GPIO. Поиски привели меня к так называемому макросу
Аскольда Волкова.

Я аж пищал от восторга. Казалось — вот он святой грааль 🙂 Все ножки можно было объявить в одном файле таким красивым образом

#define LED PORTB, 1, H

А потом использовать:

on(LED);
off(LED);

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

Первое мое разочарование этим макросом было когда я делал индикацию на 7-сегментных индикаторах. Код не только выглядел весьма уродливо, но еще и тратил кучу машинного времени. Ведь каждая on и off — это 3 ассемблерных комманды, которые компилятор не оптимизирует, так как все регистры объявлены как volatile !

К примеру, цифра 8 рисуется с помощью макроса Аскольда так:

on(Segment_A); on(Segment_B); on(Segment_C); on(Segment_D);
on(Segment_E); on(Segment_F); on(Segment_G);

 

 

Мой метод

Немного подумав, я "изобрел" еще один метод, который и использую по сегодняшний день:

Объявляем ножку как

#define LED_PB BIT1

Суффикс _PB (PORTB) обозначает порт на котором находится ножка. Макрос BIT1 нужно использовать чтобы не забыть про “1<<”, про это я рассказывал раньше.

Работаем с этой ножкой:

PORTB |= LED_PB;
PORTB &= ~LED_PB;

Если мы изменим только номер ножки, но порт останется тем-же

#define LED_PB BIT5

То ничего в остальном коде менять не нужно.

Если-же мы изменим порт, то мы поменяем суффикс, к примеру,

#define LED_PD BIT5

И везде, где используется эта ножка произойдет ошибка компиляции. Среда разработки "подсветит" нам эти места.
Заменить PORTB на PORTD будет очень просто.
 

Несколько ножек порта можно включить одним махом:

PORTB |= LED1_PB | LED2_PB | LED3_PB | LED4_PB;

На это будет тратится минимум процессорного времени. Конечно, у этой технологии есть косяк (куда-же без него). Если написать
 
PORTB &= ~LED1_PB | LED2_PB | LED3_PB | LED4_PB;
 

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

PORTB &= ~(LED1_PB | LED2_PB | LED3_PB | LED4_PB);

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

#define PORTB_CLR(x) PORTB &=~(x)
#define PORTB_SET(x) PORTB |=(x)

 
и использовать уже их
 
PORTB_CLR( LED1_PB | LED2_PB | LED3_PB | LED4_PB );
Я себя пока не приучил так делать, а зря.
 
Еще можно вместо конструкции  “&=~” писать просто “-”.
 
PORTB -= LED1_PB + LED2_PB + LED3_PB + LED4_PB;
 
С одной стороны, последствия ошибки в такой конструкции весьма плачевны, а с другой – есть принцип “усиления ошибки”, говорящий о том, что большую ошибку проще обнаружить.
 
Для минимизации энергопотребления, необходимо чтобы неиспользуемые ножки были сконфигурированы на "выход". В рамках данного подхода это сделать очень просто:
 
DDRB = 0xFF — INPUT_PIN1_PB — INPUT_PIN1_PB;
 

Дополнение

Добрый человек с ником miv поделился своими макросами для работы с ножками LPC2368. При желании, можно эти макросы можно модифицировать под любой другой процессор.

 

Комментарии, как всегда, приветствуются.

Комментарии
  1. DI HALT написал(а) 4th Ноябрь, 2009 в 18:56

    Если-же мы изменим порт, то мы поменяем суффикс, к примеру,

    А что мешает дать и PORTB человеческое имя? скажем LED_PORT

    Тогда смена порта будет делаться лишь в одном месте программы — в дефайне этого порта.

    Неоднократно уже с этим сталкиваюсь — люди видят что у порта уже есть символическое имя (PORTB) и стремаются присвоить ему свое имя. У мну так еще с ассемблера сложилось, что я все системные имена сразу же переименовываю по своему.

  2. BSVi написал(а) 4th Ноябрь, 2009 в 19:15

    Первая причина в том, что один порт может использоваться кучей разных функций — кроме светодиодов там будут весеть еще и кнопки и индикатор и софт-spi и еще черт-знает что. Получится огромная куча дефайнов портов. Помножим это на количество регистров работы с портом — для АВР это 3, а для LPC — уже 7. Получится немыслимый файл с определениями, часть которых прийдется держать в голове.

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

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

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

  3. ReAl написал(а) 17th Декабрь, 2009 в 21:54

    «Ведь каждая on и off – это 3 ассемблерных комманды» — это почему?
    Использую «Волковские» макросы очень давно и никогда они для AVR не давали трёх команд (ну если не -O0, конечно). Куча команд только на инверсию, но это архитектурное.

    #include
    #include «_useful_gpio.h»
    #define LED PORTB,1,L
    void tst_on() { on(LED); }
    void tst_off() { off(LED); }
    void tst_cpl() { toggle_pin(LED); }

    .text
    .global tst_on
    tst_on:
    cbi 56-0x20,1
    ret
    .global tst_off
    tst_off:
    sbi 56-0x20,1
    ret
    .global tst_cpl
    tst_cpl:
    in r24,56-0x20
    ldi r25,lo8(2)
    eor r24,r25
    out 56-0x20,r24
    ret

    В новых кристаллах инвертировать можно записью в PIN и это реализовано в моей модификации макросов (лежат в avr-gcc порте scmRTOS и на электрониксе год-два назад выкладывались), в ней же уже используется один макрос для ножки
    #define LED B,1,L
    а нужные PORT/PIN/DDR подставляются в макросах ON/OFF/TOGGLE/ACTIVE/LATCH/DRIVER

  4. BSVi написал(а) 17th Декабрь, 2009 в 22:00

    >это почему?
    Вы правы.

  5. ZiB написал(а) 29th Март, 2010 в 9:56

    Эх не знал что такие макросы уже есть 🙂
    Писали с корешем свои, для авр объявление выглядит вот так

    #define Pin_SPI_SS B, 2, High, Off
    // B — литера порта
    // 2 — ид вывода
    // High — уровень логической единицы (по сути инверсия)
    // Off — наличие Pull-Up сопротивления

    для арм

    #define Pin_PWM2 A, 2, High, NoPullUp, NoFiltr, NoMultiDrive, PerA, Out, NoIT
    // A — литера порта
    // 2 — ид вывода
    // High — уровень логической единицы (по сути инверсия)
    // NoPullUp — наличие Pull-Up сопротивления
    // NoFiltr, NoMultiDrive, PerA, Out, NoIT — режимы работы линии

  6. BSVi написал(а) 29th Март, 2010 в 10:05

    Как говориться, все хорошее придумали до нас )

  7. ZiB написал(а) 29th Март, 2010 в 10:12

    Это точно 🙂
    Но мы и писали его в 2001, может плохо искали….

  8. MrYuran написал(а) 12th Июль, 2011 в 10:09

    Немного модифицировал макросы с целью автоматической инициализации портов.
    Теперь у меня (для MSP430) базовый макрос для ноги выглядит как P1,1,H (вместо полного P1OUT,,,) и появилось два новых макроса InitAsIn(pin_macro) и InitAsOut(pin_macro).
    Хотел замутить более общий макрос init() и макроопределение ноги как port,dir,pin,active_level , то есть P1,OUT,1,H , но не прокатило.
    Вместо OUT подставляет в процессе преобразования 0х0004, получается полная шляпа. Но и так гламурненько получается.

    #define SYS_WDT P4,7,H
    #define PWR_WDT P6,0,H

    static __inline__ void WatchDogInit(void)
    {
    InitAsOut(PWR_WDT);
    InitAsOut(SYS_WDT);
    }

    static __inline__ void ClearWatchDog(void)
    {
    toggle_pin(SYS_WDT);
    }

    static __inline__ void ClearPWRWatchDog(void)
    {
    toggle_pin(PWR_WDT);
    }

    Ещё групповые операции осталось добавить — и вообще красота.
    Главное, чтобы единообразно.

  9. BSVi написал(а) 12th Июль, 2011 в 17:59

    Ага, я давно таким пользуюсь. У меня (на lpc17xx) это выглядит так:

    SetPinProps(GSM_GPIO1, DIR_OUT, MODE_NONE, FUNCTION_00, false );

    А вообще, меня иногда порывает написать какой-то жудко высокоуровневый(для эмбеда) модуль для инициализации ножек, в который можно будет писать что-то типа

    IntPins(«Default:out; P1: 1-3:in; 5, 6, 7:in pullup; P2: 3-31: in pulldown»);

    Над конкретным синтаксисом нужно еще подумать, но суть, думаю. ясна.

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт защищен reCAPTCHA и применяются Политика конфиденциальности и Условия обслуживания применять.