Как настроить выходы без использования digitalWrite?

Обратите внимание на следующее:

#define IN1 9
#define IN2 10

pinMode(IN1, OUTPUT);
pinMode(IN2, OUTPUT);

void LeftMotor(Direction direction) {
  digitalWrite(IN1, direction == Forward ? LOW : HIGH);
  digitalWrite(IN2, direction == Forward ? HIGH : LOW);
}

Как изменить метод LeftMotor, чтобы добиться того же результата без использования digitalWrite?

, 👍4

Обсуждение

Неа! Я хочу, чтобы он реагировал быстрее., @OrElse

https://www.arduino.cc/en/Reference/PortManipulation, @dandavis

Это не заставит его реагировать быстрее. Добавьте больше своего скетча, если вам нужна помощь с этим. Ваше здоровье!, @Mikael Patel

@MikaelPatel: согласно ссылке, которую я разместил выше, это будет быстрее, потому что «_можно очень быстро включать и выключать контакты, то есть в течение долей микросекунды ... Прямой доступ к порту может выполнять ту же работу во многих случаях. меньше тактовых циклов._" (чем digitalWrite). Теперь, что касается моста, это не приведет к разнице между горой бобов, но вопрос ОП является законным., @dandavis

Почему вы думаете, что digitalWrite недостаточно быстр для вашего приложения? Прямая манипуляция с портом будет значительно быстрее, чем использование digitalWrite, но для большинства вещей (например, для управления мотором) digitalWrite достаточно быстр. Манипуляции с портами сложнее настроить, сложнее понять, и они не переносимы между устройствами (назначения портов различаются для разных моделей Arduino)., @Duncan C

@DuncanC digitalWrite работает медленно. Научите ребенка, как устанавливать порты напрямую. Ваш совет ужасен. Настроить регистры не сложно., @PhillyNJ

@PhillyNJ, конечно, если вариант использования требует быстрого кода. Если нет, я бы посоветовал придерживаться digitalWrite(). В конце концов, преждевременная оптимизация — корень всех зол., @Duncan C

Я писал коммерческое программное обеспечение на ассемблере, когда это было необходимо для получения приемлемой производительности. Однако я бы не рекомендовал его сегодня по той же причине, по которой я бы не рекомендовал прямое манипулирование портом **если вам действительно не нужен такой уровень производительности**. Я написал приложение Arduino для высокоскоростной фотосъемки объектов с пневматическими винтовками , и для **этого** я использовал прямое управление портом., @Duncan C

что неясно в руководстве по Arduino о прямом управлении портом?, @Juraj

@DuncanC - ОП не спрашивал, когда ему / ей следует использовать digitalWrite или устанавливать регистры напрямую, они спрашивали, как напрямую устанавливать порты. Совершенно правильный вопрос., @PhillyNJ

@PhillyNJ: я думаю, что понимаю точку зрения Дункана С. Если ОП обнаружит, что его программа недостаточно быстрая, это, скорее всего, _не_ из-за digitalWrite(). Особенно, если задействован двигатель, который в тысячи раз медленнее, чем digitalWrite(). Затем мы можем либо напрямую ответить на вопрос ОП (что вы и сделали), либо попытаться помочь ему понять его настоящую проблему (что пытается сделать Дункан)., @Edgar Bonet

@EdgarBonet - Спасибо за вашу точку зрения. Я боюсь, что подобные комментарии навредят сообществу. В этом случае мы не совсем уверены в том, каковы ограничения двигателя. Горлышком бутылки может быть двигатель или его/ее код. ОП узнает достаточно скоро, если они попробуют оба пути. Это хороший обучающий момент. Я считаю, что digitalWrite очень медленный, особенно при записи на что-то вроде TFT, который ожидает много данных. Обучение этому OP, что находится под капотом библиотеки Arduino, всегда хорошо. Давайте не обескураживать это., @PhillyNJ

Я перешел в это сообщество после проверки ответов на форуме arduino. Я полагаю, вы были там и в прошлом. На самом деле проект связан с декодированием радиочастотных сигналов от приемника (6 каналов, каждый на входе от 1 до 2 мс) плюс 4 ШИМ на выходе, что критично по времени. Мне жаль, что название метода вводит в заблуждение, но я просто попытался максимально упростить свой вопрос., @OrElse

Для двигателей, особенно в направлении пуска/остановки/инвертирования, как следует из остальной части кода и комментариев, digitalWrite **определенно достаточно быстр**., @DataFiddler


4 ответа


Лучший ответ:

2

Настройка портов напрямую — отличный способ научиться программировать AVR. Я мог бы порекомендовать эту книгу Make: Avr Programming.

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

#define HIGH 1
#define LOW 0

#define  SWI_PIN PORTB0
#define  BUTTON PORTB4


int main (void)
{
    // устанавливаем направления выводов
    DDRB |=  (1 << SWI_PIN); // выход
    DDRB &= ~(1<<PORTB4); //вход
    while(true){

      if ((PINB & (1<<BUTTON)) == 0){ // вывод высокий! Поднимающийся край
          PORTB &= ~(1<<SWI_PIN); // НИЗКИЙ уровень отключения swi_pin
      }else {
          PORTB |= (1<<SWI_PIN); // ВЫСОКОЕ включение swi_pin
      }

}
,

Не все контакты Arduino подключены к порту B. Существуют макросы, которые сделают ваше решение более универсальным и таким же быстрым, как ваше., @DataFiddler

@DataFiddler нет, конечно. Это пример, а не полноценный урок по настройке портов., @PhillyNJ


6

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

Внутри микроконтроллера Arduino все специальные функции для периферийных устройств и т. д. (цифровой ввод-вывод, АЦП, последовательный порт (UART), SPI, таймер и т. д.) настраиваются с помощью регистров специальных функций (SFR). Это определенные места памяти, которые напрямую связаны с периферийными устройствами. Вы можете настроить весь микроконтроллер, установив для этих регистров определенные значения.

Для цифрового ввода-вывода вам сначала нужно знать, что выводы микроконтроллера упорядочены в группы не более чем по 8 контактов. Эти группы называются портами. Каждый доступный порт микроконтроллера получает символ, например PORTB. Например, на Arduino Nano PORTD включает контакты с 0 по 7.

Для каждого порта у нас есть 3 разных SFR (где x — символ соответствующего порта), где каждый бит внутри регистров соответствует одному контакту:

  • DDRx управляет направлением контактов. Если вы установите бит в 1, соответствующий вывод будет установлен как выход. Если вы установите его равным нулю, контакт будет входом (который также называется High-Z из-за высокого импеданса, поскольку контакт цифрового входа имеет очень высокое сопротивление).

  • PORTx управляет оборудованием вывода цифровых контактов. Если вы установили контакт как выход (как описано выше), вы можете использовать соответствующий бит в регистре PORTx, чтобы установить его состояние. Если вы запишете в бит 1, контакт станет ВЫСОКИМ (это означает, что напряжение на контакте станет равным Vcc, напряжению питания микроконтроллера). Если вы напишете ноль, контакт перейдет в состояние LOW (что означает тот же уровень, что и земля). Регистр PORTx имеет другую функцию, когда вы настроили контакт как вход. В этом случае 1 включит внутренний подтягивающий резистор вывода, который подтянет уровень вывода до ВЫСОКОГО, когда ничто не притягивает его к земле (например, переключатель). Нуль отключает подтягивание.

  • PINx отражает текущее состояние контактов. Если вы настроили выводы как входные, то это уровень напряжения, который в данный момент находится на соответствующем выводе (о реальных уровнях напряжения см. ниже). 1 в бите означает, что соответствующий вывод ВЫСОКИЙ, иначе он НИЗКИЙ.

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

Теперь давайте перенесем эти знания в реальный код. Здесь я предполагаю Arduino Nano, хотя вы можете заменить его на большинство других микроконтроллеров.

// Настроить контакт D3 как выход
DDRD |= (1 << PD3);
// Настроить контакт D2 как вход
DDRD &= ~(1 << PD2);
// Проверяем, имеет ли контакт D2 ВЫСОКИЙ уровень
if(PIND & (1 << PD2)){
    // Установить вывод D3 в состояние ВЫСОКИЙ
    PORTD |= (1 << PD3);
} else {
    // Установить контакт D3 в LOW
    PORTD &= ~(1 << PD3);
}

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

  • Сначала у нас есть PD2 и PD3. Это определения из ядра Arduino. Им присваивается индекс бита, соответствующего названному выводу. Таким образом, PD2 определяется как 2, PD3 определяется как 3, PD0 как 0.

  • 1 << PD3: оператор << выполняет побитовый сдвиг влево, что означает, что все биты значения слева сдвигаются влево на величину, стоящую справа от оператора . Здесь мы сдвигаем значение 1 на три цифры влево. В двоичном виде это выглядит так:

    0b00000001 << 3 = 0b00001000
    

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

  • DDRD |= (1 << PD3): это то же самое, что и DDRD = DDRD | (1 << PD3), поэтому оператор |= является просто его сокращением. Побитовый оператор ИЛИ | объединит биты с обеих сторон. Если бит установлен на 1 с любой стороны, результат также установит этот бит на 1. Это означает, что мы можем установить бит на 1 с помощью this.

  • ~(1 << PD2): тильда ~ принимает инверсию данных. Как и выше, значение внутри круглых скобок может быть записано как двоичное значение 0b00000100. Инверсия перевернет все биты на другое значение: 0b11111011

  • DDRD &= ~(1 << PD2): Аббревиатура аналогична приведенной выше. Побитовый оператор AND & также объединит биты значений вокруг него, но он установит результирующие биты в 1 только в том случае, если оба соответствующих бита значений также установлены. Здесь это приводит к тому, что третий бит (тот, что для D2) очищается до нуля. Все остальные биты (где справа стоит 1) остаются нетронутыми.

  • PIND & (1 << PD2): аналогично предыдущему, но теперь у нас нет инверсии. Мы делаем PIND & 0b00000100 здесь. Все биты результата будут установлены в ноль, кроме бита для D2. Он будет равен нулю только в том случае, если соответствующий бит равен нулю в PIND. Таким образом мы выделили нужный бит.

Вы также можете комбинировать одни и те же операции в одном регистре. Допустим, мы хотим установить как D2, так и D3 в качестве выходных данных. Затем мы можем записать это как два отдельных оператора

DDRD |= (1 << PD2);
DDRD |= (1 << PD3);

или мы можем объединить их в один оператор, соединив их побитовым ИЛИ

DDRD |= (1 << PD2) | (1 << PD3);

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


Как изменить метод LeftMotor, чтобы добиться того же результата без использования digitalWrite?

После всего этого текста теперь к конкретному вопросу. На выводе 9 и 10 Nano находятся PB1 и PB2. Вы использовали условное выражение внутри своего оператора digitalWrite(), которое я заменю здесь полным оператором if. Таким образом, вы можете использовать

if(direction == Forward){
    PORTB &= ~(1 << PB1);
    PORTB |= (1 << PB2);
} else {
    PORTB |= (1 << PB1);
    PORTB &= ~(1 << PB2);
}

вместо двух операторов digitalWrite().

,

Я не знаю, почему этот вопрос заминусован. Что касается «канонического» ответа, то он лучше другого. Может быть, у кого-то проблемы с чтением..., @smajli


4

Крисл дал очень хороший справочный ответ. Здесь я просто добавляю немного информации о назначении контактов и электрическом контакте состояния.

Сопоставление пинов

Одним из первых вещей, которые вам понадобятся при работе с прямым доступом к порту, является сопоставление между именами выводов Arduino и именами выводы микроконтроллера (MCU) согласно данным производителя. Для Ардуино Nano сопоставление следующее:

 Arduino │ MCU
─────────┼─────
   RX0   │ PD0
   TX1   │ PD1
   D2    │ PD2
        ... 
   D7    │ PD7
   D8    │ PB0
        ...
   D13   │ PB5
   A0    │ PC0
        ...
   A5    │ PC5

где «PD0» означает «номер контакта 0 порта D» и т. д. Обратите внимание, что выводы A6 и A7 здесь не указаны, так как они только аналоговые, без цифровых Возможность ввода/вывода.

Информацию выше можно найти на странице продукта платы, во вкладке «Документация». В качестве альтернативы, поиск изображения для «Arduino <название платы> распиновка» найдет несколько хороших схем, таких как тот, что ниже:

Распиновка Arduino Nano

Состояния вывода

Это уже было рассмотрено в ответе Крисла в описании регистры DDRx и PORTx. Ниже представлена сводная таблица, показывающая четыре доступных электрических состояния контактов и способ их выбора с битами DDR и PORT:

 DDR │ PORT │ electrical state │ equation
─────┼──────┼──────────────────┼─────────────────
  0  │   0  │  INPUT (high-Z)  │ I = 0
  0  │   1  │  INPUT_PULLUP    │ V = Vcc − Rpu I
  1  │   0  │  OUTPUT LOW      │ V = 0
  1  │   1  │  OUTPUT HIGH     │ V = Vcc

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

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

1

Как настроить выходы без использования digitalWrite?

Таким образом, этот вопрос на самом деле касается максимально быстрой реализации digitalWrite.

Как описано в других ответах, этого можно добиться с помощью прямого управления портом. Но на самом деле есть библиотеки (например, https://github.com/mikaelpatel/Arduino-GPIO), которые делают это. портативным способом и позволяет избежать некоторых ошибок, которые могут возникнуть. Библиотека работает в 10-100 раз быстрее, чем основные функции цифровых выводов Arduino.

Выводы в MCU организованы в виде портов. На AVR имеется набор 8-битных регистров порта, в которых хранится текущая настройка вывода. Чтобы изменить значение вывода, ЦП должен прочитать регистр и изменить соответствующий бит, связанный с выводом. Это должно выполняться как атомарная операция, т. е. во время обновления регистра порта не должно возникать прерывание.

Чтобы обеспечить очень быстрое обновление, т.е. одна инструкция/1-2 такта, ЦП имеет набор специальных инструкций. Для AVR есть набор битов и четкие инструкции для 32 первых специальных регистров. На Arduino Mega некоторые контакты/порты имеют специальные регистры с более высоким адресом и не могут использовать специальные инструкции. Чтобы сделать операцию вывода атомарной, необходимо добавить явную обработку прерываний.

Другая проблема с прямым управлением портами связана с переносимостью. Код, написанный с использованием обработки ввода-вывода AVR, нельзя использовать на SAM (например, Arduino Due). На самом деле существуют проблемы даже при переключении между микроконтроллерами AVR.

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

Ниже приведен код, написанный с помощью GPIO. Он портативный и такой же быстрый, как прямое управление портом.

#include "GPIO.h"

GPIO<BOARD::D9> IN1;
GPIO<BOARD::D10> IN2;

IN1.output();
IN2.output();

void LeftMotor(Direction direction) {
  IN1 = (direction == Forward ? LOW : HIGH);
  IN2 = (direction == Forward ? HIGH : LOW);
}

или

void LeftMotor(Direction direction) {
  IN1 = direction != Forward;
  IN2 = direction == Forward;
}

или

void LeftMotor(Direction direction) {
  if (direction == Forward) {
    IN1.low();
    IN2.high();      
  }
  else {
    IN1.high();
    IN2.low();
  }
}
,