Порти мікроконтролерів

В цій статті ми познайомимось з основами роботи паралельних портів GPIO

Мікроконтролери призначені для керування електронікою пристроїв найрізноманітнішого призначення — від побутових приладів до дронів і роботів. Вони отримують інформацію від датчиків, кнопок чи інших елементів, обробляють її за допомогою програми й віддають команди виконавчим пристроям, наприклад двигунам, світлодіодам або дисплеям. Інколи мікроконтролери також передають дані іншим системам, наприклад через інтерфейси UART, I²C або SPI.
Зв’язок чіпа мікроконтролера з іншими електронними компонентами забезпечують порти вводу-виводу (I/O-порти).

Порт мікроконтролера — це набір виводів (пінів), якими можна керувати з програми. Кожен пін може працювати в одному з двох основних режимів:

  • вивід (output) — коли програма встановлює на ньому певний рівень напруги (наприклад, щоб увімкнути світлодіод),

  • ввід (input) — коли пін «слухає» зовнішній сигнал, наприклад натискання кнопки.

Таким чином, через порти мікроконтролер обмінюється інформацією з зовнішнім світом. Зазвичай він має кілька портів (A, B, C тощо), кожен з яких містить певну кількість пінів.

На малюнку показано умовний приклад мікроконтролера, праворуч якого розташовані піни одного з портів — порту A, що працює в режимі виводу. До кожного піна через резистор під’єднано світлодіод. У програмі можна керувати цими пінами, вмикаючи або вимикаючи відповідні світлодіоди. Зверніть увагу, що назви пінів складаються з літери порту та номера піна — наприклад, PA0 означає «пін 0 порту A». У реальних мікроконтролерах позначення можуть відрізнятися, тому для кожного чіпа необхідно звертатися до його документації — datasheet, де вказано повну розпіновку, характеристики портів і можливі режими роботи.

Режими вводу і виводу

Робота з портами — це основа взаємодії програми з "реальним світом" електроніки. Знання принципів вводу-виводу дозволяє під’єднувати до мікроконтролера будь-які пристрої — від простих кнопок до складних сенсорів і модулів зв’язку.

Розглянемо, як саме здійснюється керування портами на прикладі мікроконтролера ATmega328P.
Він має три основні порти: порт B, порт C та порт D. Кожен із них містить кілька пінів:

  • порт B — PB0–PB7 (8 пінів),

  • порт C — PC0–PC6 (7 пінів),

  • порт D — PD0–PD7 (8 пінів).

Кожен порт обслуговується трьома основними регістрами:

  1. DDRxData Direction Register, задає напрямок пінів (ввід чи вивід).

  2. PORTx — використовується для встановлення логічних рівнів на вихідних пінах або вмикання внутрішніх підтягувальних резисторів у режимі вводу.

  3. PINx — дозволяє зчитувати стан пінів у режимі вводу.

Тут x — літера порту: B, C або D.

Наприклад, щоб налаштувати пін PB0 як вихід, потрібно в регістрі DDRB встановити в 1 нульовий біт:

DDRB |= (1 << PB0);

Тепер, щоб увімкнути світлодіод, під’єднаний до PB0, подаємо логічну «1» на цей пін:

PORTB |= (1 << PB0);

А щоб вимкнути світлодіод — подаємо логічний «0»:

PORTB &= ~(1 << PB0);

Таким чином, ми безпосередньо керуємо рівнями напруги на ніжках мікроконтролера.
У режимі вводу все працює подібно, але з деякими нюансами. Наприклад, щоб налаштувати пін PD2 як вхід із внутрішнім підтягувальним резистором, виконуємо:

DDRD &= ~(1 << PD2);   // режим вводу
PORTD |= (1 << PD2);   // увімкнення підтягувального резистора

Після цього зчитати стан кнопки, підключеної до PD2, можна так:

if (PIND & (1 << PD2)) {
    // кнопка не натиснута (на піні логічна 1)
} else {
    // кнопка натиснута (на піні логічний 0)
}

Така робота з портами є основою для створення будь-якої взаємодії мікроконтролера з навколишнім світом — від простої індикації до складних систем керування.

Режим виводу

Припустимо, що до пінів PB0–PB3 під’єднано чотири світлодіоди. Ми хочемо по черзі вмикати кожен із них.
Для цього достатньо записувати різні значення до регістра PORTB:

#define F_CPU 16000000UL
#include <avr/io.h>
#include <util/delay.h>

int main(void) {
    DDRB = 0b00001111; // PB0–PB3 як виходи

    while (1) {
        PORTB = 0b00000001; // увімкнено лише PB0
        _delay_ms(200);

        PORTB = 0b00000010; // PB1
        _delay_ms(200);

        PORTB = 0b00000100; // PB2
        _delay_ms(200);

        PORTB = 0b00001000; // PB3
        _delay_ms(200);
    }
}

Функція _delay_ms() створює затримку в мілісекундах, використовуючи точний тактовий генератор мікроконтролера (у цьому випадку 16 МГц).
Таким способом можна задавати потрібний ритм перемикання, і світлодіоди будуть по черзі вмикатись — утворюючи ефект руху.

Ми також можемо встановити стани всіх пінів порту одним записом, наприклад:

PORTB = 0b00001111; // увімкнути 4 світлодіоди одночасно
PORTB = 0b00000000; // вимкнути всі

Робота з портами у режимі вводу

Окрім керування світлодіодами чи іншими виконавчими елементами, мікроконтролер повинен отримувати інформацію від користувача або зовнішніх сенсорів. Найпростіший приклад — це кнопка.

Коли кнопку під’єднано до піна мікроконтролера, вона зазвичай або замикає лінію на «землю» (логічний 0), або на живлення (логічна 1). Щоб контролер міг правильно зчитувати стан кнопки, потрібно налаштувати пін у режим вводу.

Розглянемо приклад: до піна PD2 під’єднано кнопку, яка при натисканні з’єднує пін із землею.
Щоб не залишати пін у «підвішеному стані», коли кнопка не натиснута, використовують підтягувальний резистор (pull-up). У ATmega328P такий резистор можна увімкнути програмно, просто записавши «1» у відповідний біт регістра PORTD при вимкненому бітові DDRD.

#define F_CPU 16000000UL
#include <avr/io.h>
#include <util/delay.h>

int main(void) {
    DDRB = 0x0F;        // PB0–PB3 — виходи (світлодіоди)
    DDRD &= ~(1 << PD2); // PD2 — вхід
    PORTD |= (1 << PD2); // увімкнення внутрішнього підтягувального резистора

    while (1) {
        if (PIND & (1 << PD2)) {
            // кнопка не натиснута
            PORTB = 0x01;
        } else {
            // кнопка натиснута (пін з'єднано із землею)
            PORTB = 0x08;
        }
    }
}

Тепер при натисканні кнопки мікроконтролер змінює стан світлодіодів.

Комбінування вводу та виводу

У більшості реальних проектів мікроконтролер не лише «щось показує», а й реагує на дії користувача.
Класичний приклад — кілька кнопок, які змінюють режим роботи пристрою. Наприклад, одна кнопка може вмикати бігуче світло, інша — робити всі світлодіоди миготливими одночасно, а третя — вимикати індикацію.

Розглянемо приклад для ATmega328P, де:

  • до PB0–PB3 підключено 4 світлодіоди,

  • до PD2–PD4 — 3 кнопки.

#define F_CPU 16000000UL
#include <avr/io.h>
#include <util/delay.h>

int main(void) {
    // --- Налаштування ---
    DDRB = 0x0F;                // PB0–PB3 як виходи
    DDRD &= ~((1 << PD2) | (1 << PD3) | (1 << PD4)); // PD2–PD4 як входи
    PORTD |= (1 << PD2) | (1 << PD3) | (1 << PD4);   // підтягувальні резистори

    uint8_t mode = 0; // режим роботи

    while (1) {
        // --- Обробка кнопок ---
        if (!(PIND & (1 << PD2))) {  // кнопка 1
            _delay_ms(20);
            if (!(PIND & (1 << PD2))) mode = 0;
        }
        if (!(PIND & (1 << PD3))) {  // кнопка 2
            _delay_ms(20);
            if (!(PIND & (1 << PD3))) mode = 1;
        }
        if (!(PIND & (1 << PD4))) {  // кнопка 3
            _delay_ms(20);
            if (!(PIND & (1 << PD4))) mode = 2;
        }

        // --- Реакція на вибраний режим ---
        if (mode == 0) {
            PORTB = 0x00; // все вимкнено
        }
        else if (mode == 1) {
            // Бігуче світло
            for (uint8_t i = 0; i < 4; i++) {
                PORTB = (1 << i);
                _delay_ms(150);
            }
        }
        else if (mode == 2) {
            // Усі блимають одночасно
            PORTB = 0x0F;
            _delay_ms(200);
            PORTB = 0x00;
            _delay_ms(200);
        }
    }
}

У цьому прикладі мікроконтролер постійно зчитує стан кнопок і змінює змінну mode, яка визначає поточний режим роботи. Основний цикл реагує на цю змінну, вмикаючи світлодіоди відповідно до обраного режиму.

Альтернативні функції портів

Так як всі піни порту керуються одночасно, розглянуті вище порти називаються паралельними, або портами вводу-виводу загального призначення - GPIO (від General Purpose Input/Output)

Кожен пін порту може виконувати не лише базові функції вводу-виводу, а й додаткові спеціальні функції, які залежать від конкретного мікроконтролера. Наприклад, деякі піни можуть працювати як аналогові входи для вимірювання напруги за допомогою вбудованого АЦП (аналогово-цифрового перетворювача). Інші можуть виконувати роль ШІМ-виходів (PWM) — це коли мікроконтролер швидко вмикає та вимикає сигнал, створюючи "псевдоаналогову" напругу, наприклад для керування яскравістю світлодіода або швидкістю двигуна.

Є також піни, які можуть працювати як комунікаційні лінії — наприклад, передавати дані через UART, I²C або SPI. Таким чином, ті самі фізичні піни можуть мати кілька призначень, і потрібну функцію зазвичай обирають у програмі під час налаштування портів, але це вже тема для іншої статті.