Существуют стандартные методы языка для настройки выводов микроконтроллера на вход или выход.

Ардуино программируется на C++ с некоторой адаптацией и упрощениями для новичков. Он называется Wiring. Изначально все порты ардуино определяются как входы, и нет нужды задавать это в коде. Порты обычно прописываются в функции инициализации переменных

void setup ()
{
// код
}


Для этого используется команда pinMode, у неё достаточно простой синтаксис, сначала указывается номер порта, затем его роль через запятую.

pinMode (nomer_porta, naznachenie)

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

Есть три режима в которых может работать порт: INPUT – вход, в этом режиме происходит считывание данных с датчиков, состояния кнопок, аналогового и цифрового сигнала. Порт находится в высокоимпедансном состоянии, простыми словами – у входа высокое сопротивление. Устанавливается это значение, на примере 13 пина платы, при необходимости так:

pinMode (13, INPUT);

OUTPUT – выход, в зависимости от команды прописанной в коде порт принимает значение единицы или нуля. Выход становится своего рода управляемым источником питания и выдаёт максимальный ток (в нашем случае 20 мА и 40 мА в пике) в нагрузку к нему подключенную. Чтобы назначить порт как выход на Arduino нужно ввести:

pinMode (13, OUTPUT);

INPUT_PULLUP – порт работает как вход, но к нему подключается подтягивающий резистор в 20 кОм.

Данные принимаются с портов и передают на них командами:

digitalWrite(пин, значение) – переводит выходной пин в логическую 1 или 0, соответственно на выходе появляется или исчезает напряжение 5В, например digitalWrite (13, HIGH) – подаёт 5 вольт (логическую единицу) на 13 пин, а digitalWrite (13, low) – переводит 13 пин в состояние логического ноля (0 вольт);

digitalRead(пин) – считывает значение со входа, пример digitalRead (10), считывает сигнал с 10 пина;

analogRead(пин) – считывает аналоговый сигнал с аналогового порта, вы получаете значение в диапазоне от 0 до 1023 (в пределах 10-битного АЦП), пример analogRead (3).

analogWrite(пин, значение) - Выдает аналоговую величину (ШИМ волну) на порт вход/выхода. Функция может быть полезна для управления яркостью подключенного светодиода или скоростью электродвигателя. После вызова analogWrite() на выходе будет генерироваться постоянная прямоугольная волна с заданной шириной импульса до следующего вызова analogWrite (или вызова digitalWrite или digitalRead на том же порту вход/выхода). Частота ШИМ сигнала приблизительно 490 Hz.

На большинстве плат Arduino (на базе микроконтроллера ATmega168 или ATmega328) ШИМ поддерживают порты 3, 5, 6, 9, 10 и 11, на плате Arduino Mega порты с 2 по 13. На более ранних версиях плат Arduino analogWrite() работал только на портах 9, 10 и 11.

Для вызова analogWrite() нет необходимости устанавливать тип вход/выхода функцией pinMode(). Функция analogWrite никак не связана с аналоговыми входами и с функцией analogRead.

Задание яркости светодиода пропорционально значению, снимаемому с потенциометра



int ledPin = 9;    // Светодиод подключен к выходы 9
int analogPin = 3; // потенциометр подключен к выходу 3
int val = 0;       // переменная для хранения значения

void setup()
{
  pinMode(ledPin, OUTPUT); // установка порта на выход
}

void loop()
{
  val = analogRead(analogPin);  // считываем значение с порта, подключенному к потенциометру
  analogWrite(ledPin, val / 4); // analogRead возвращает значения от 0 до 1023,
                                // analogWrite должно быть в диапазоне от 0 до 255
}

Регистры портов (Arduino)

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


Плата Arduino Nano (Uno) имеет три порта:
B цифровые выводы от D8 до D13
C аналоговые входы от A0 до А5
D цифровые выводы от D0 до D7


Управление портами осуществляется при помощи трех регистров:
DDR — определяет какой из выводов указанного порта будет выходом, а какой входом
PORT — переводит выводы порта в состояние HIGH или LOW
PIN — считывает состояние порта

Каждый бит в регистре отвечает за соответствующий вывод порта, например за вывод PD0 отвечает самый младший байт, за PD7 самый старший.

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

Таблица соответствия портов ардуино и атмеги:



Пример:

void setup()
{
   DDRB = 0b00100000; // 5 бит соответствует PB5 или D13
                      // 1 - вывод сконфигурирован как выход (0 - вход)
   PORTB = 0b00100000; // 5 бит соответствует PB5 или D13
                       // установить на PB5 HIGH
}

void loop(){}

В данном примере регистр DDR обращается к порту B, лог. 1 в 5 байте переводит вывод в режим выхода, регистр PORT так же обращается к порту B, лог. 1 в 5 байте переводит вывод в HIGH. После загрузки скетча должен засветится светодиод который расположен на плате Arduino.

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

Если необходимо установить для примера на выходе D13 лог. 1 и не менять состояние других выходов, то можно воспользоваться следующим примером:

void setup(){ 
  DDRB = 0B00100000;
  PORTB |= (1 << 5); // см. пример http://rcl-radio.ru/?p=80784
}

void loop(){}

или установить лог. ноль:

void setup(){ 
  DDRB = 0B00100000;
  PORTB &= ~(1 << 5); // см. пример http://rcl-radio.ru/?p=80784
}

void loop(){}

Для чтения состояния порта используется регистр PIN:

void setup(){ 
  Serial.begin(9600);
  DDRB =  0b00111111; // назначить выводы PB0...PB5 как выходы (D8...D13)
  PORTB = 0b00111011; // PB0...PB1 и PB3...PB5 = HIGH, PB2 = LOW
}

void loop(){
  Serial.println(PINB,BIN); // чтение порта и вывод в монитор порта
  delay(1000);
  }

Информация о состоянии порты выводится в монитор порта:

Для чтения одного вывода порта используйте следующий пример:

void setup(){ 
  Serial.begin(9600);
  DDRB =  0b00111111;
  PORTB = 0b00111011;
}

void loop(){
  Serial.println(((PINB >> 5) & 1),BIN); // чтение состояния PB5 
  delay(1000);
  }

Далее переведем все выводы порта в режим чтения:

void setup(){ 
  Serial.begin(9600);
  DDRB =  0b00000000; // все выводы порта в режиме чтения
}

void loop(){
  Serial.println(((PINB >> 0) & 1),BIN); // чтение состояния PB0 (D8)
  delay(1000);
  }

На вход D8 подавать уровни LOW или HIGH, в мониторе порта Вы увидите 0 или 1.

Так же в цифровом входе можно использовать подтягивающий резистор, при этом на входе будет HIGH. Этот режим удобно использовать для подключения кнопки (коммутация кнопки на GND).

void setup(){ 
  Serial.begin(9600);
  DDRB =  0b00000000;// все выводы порта в режиме чтения
  PORTB = 0b00000001;// подключить подтягивающий резистор к выводу PB0 (D8)
}

void loop(){
  Serial.println(((PINB >> 0) & 1),BIN);
  delay(1000);
  }

У Вас может возникнуть вопрос: — «Зачем все это? Ведь существуют такие функции как pinMode(), digitalWrite() и digitalRead(). Эти функции понятны и просты в применении.»

Как говорилось в начале статьи, что использование регистров портов позволяют выполнять низкоуровневые высокоскоростные манипуляции с выводами микроконтроллера. Помимо увеличения скорости чтения и переключения цифровых выводов, использование памяти контроллера значительно уменьшается. Если у Вас простой скетч не требовательный к размеру памяти и ресурсам микроконтроллера, то проще использовать стандартные функции. Но если скетч большой и требовательный к скорости переключения цифровых выводов, то использование регистров будет самым оптимальным для Вас решением.

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

Мигание светодиодом D13 (Blink)

void setup() {
  pinMode(13,OUTPUT);
}

void loop() {
  digitalWrite(13,HIGH);
  delay(1000);
  digitalWrite(13,LOW);
  delay(1000);
}

Скетч использует 924 байт (2%) памяти устройства. Всего доступно 32256 байт.
Глобальные переменные используют 9 байт (0%) динамической памяти, оставляя 2039 байт для локальных переменных. Максимум: 2048 байт.

void setup(){ 
  DDRB =  0B00100000;
}

void loop(){
  PORTB |= (1 << 5);
  delay(1000);
  PORTB &= ~(1 << 5);
  delay(1000);
}

Скетч использует 642 байт (1%) памяти устройства. Всего доступно 32256 байт.
Глобальные переменные используют 9 байт (0%) динамической памяти, оставляя 2039 байт для локальных переменных. Максимум: 2048 байт.

Оба скетча выполняют одно и тоже действие, второй скетч занимает меньше памяти. А если Вам не важна точность задержки функции delay(), то можно применить _delay_ms() и использование памяти микроконтроллера станет еще меньше:

void setup(){ 
  DDRB = 0b00100000;
}

void loop(){
 PORTB |= (1 << 5);
 _delay_ms(1000);
 PORTB &= ~(1 << 5);
 _delay_ms(1000);
}

Скетч использует 488 байт (1%) памяти устройства. Всего доступно 32256 байт.
Глобальные переменные используют 9 байт (0%) динамической памяти, оставляя 2039 байт для локальных переменных. Максимум: 2048 байт.

Скорость переключения выхода D13

void setup() {
  Serial.begin(9600);
  pinMode(13,OUTPUT);
}

void loop() {
  unsigned long times = micros();
  for(int i=0;i<100;i++){
  digitalWrite(13,HIGH);
  digitalWrite(13,LOW);
  }
  Serial.println(micros()-times);
  delay(1000);
}

Скетч использует 2104 байт (6%) памяти устройства. Всего доступно 32256 байт.
Глобальные переменные используют 188 байт (9%) динамической памяти, оставляя 1860 байт для локальных переменных. Максимум: 2048 байт.

void setup(){ 
  Serial.begin(9600);
  DDRB =  0B00100000;
}

void loop(){
  unsigned long times = micros();
  for(int i=0;i<100;i++){
  PORTB |= (1 << 5);
  PORTB &= ~(1 << 5);
  }
  Serial.println(micros()-times);
  delay(1000);
}

Скетч использует 1820 байт (5%) памяти устройства. Всего доступно 32256 байт.
Глобальные переменные используют 188 байт (9%) динамической памяти, оставляя 1860 байт для локальных переменных. Максимум: 2048 байт.

Как видно на скриншотах, переключение цифрового выхода D13 100 раз занимает в среднем 680 мкс, тот же процесс во втором скетче занимает  в среднем 54 мкс.

Аналогичная ситуация и при чтении выхода:

void setup() {
  Serial.begin(9600);
  pinMode(13, OUTPUT);
  digitalWrite(13, HIGH);
}

void loop() {
  unsigned long times = micros();
  int x = 0;
  for (int i = 0; i < 1000; i++) {
    if (digitalRead(13) == 1) {
      x++;
    };
  }
  Serial.println(micros() - times);
  Serial.println(x);
  delay(1000);
}

Скетч использует 2242 байт (6%) памяти устройства. Всего доступно 32256 байт.
Глобальные переменные используют 188 байт (9%) динамической памяти, оставляя 1860 байт для локальных переменных. Максимум: 2048 байт.

1000 — кол-во измерений, 2576 — затраченное время в мкс

void setup() {
  Serial.begin(9600);
  DDRB =  0B00111111;
  PORTB |= (1 << 5);
}

void loop() {
  unsigned long times = micros();
  int x = 0;
  for (int i = 0; i < 1000; i++) {
    if (((PINB >> 5) & 1) == 1) {
      x++;
    }
  }
  Serial.println(micros() - times);
  Serial.println(x);
  delay(1000);
}

Скетч использует 1906 байт (5%) памяти устройства. Всего доступно 32256 байт.
Глобальные переменные используют 188 байт (9%) динамической памяти, оставляя 1860 байт для локальных переменных. Максимум: 2048 байт.

1000 — кол-во измерений, 444 — затраченное время в мкс