⚡ II. Rész • Gyakorlati Alkalmazási Feladatok

Laborgyakorlati Feladatok & Működő C Forráskódok

A Sapientia Villamosmérnöki Tanszék laboratóriumi tematikájában szereplő 8 alapszintű feladat komplett, regiszterszintű C implementációja, hardveres bekötési és oktatási magyarázatokkal (PIC18F4620 / XC8 és ARM Cortex-M).

Labor 1
Nyomógomb beolvasása és szoftveres prellmentesítés (Debounce) Állapotgéppel

Feladat Leírása & Működési Elv

A mechanikus nyomógombok érintkezőinek záródásakor és nyitásakor 5–20 ms-ig tartó mechanikai prell (pergés, kontaktus pattogás) lép fel, ami többszörös fals impulzusokat generál. A szoftveres prellmentesítés 10–20 ms-os periodikus mintavételezéssel vagy véges állapotgéppel (FSM) biztosítja, hogy egy lenyomás pontosan egyetlen eseményt generáljon.

PIC18 C (XC8) • FSM Debounce
#include <xc.h>
#include <stdint.h>
#include <stdbool.h>

typedef enum {
    BTN_RELEASED,
    BTN_DEBOUNCE_PRESS,
    BTN_PRESSED,
    BTN_DEBOUNCE_RELEASE
} btn_state_t;

// RB0 nyomógomb (aktív alacsony, belső pull-up engedélyezve)
// LATD0 LED kimenet
void init_gpio(void) {
    TRISBbits.TRISB0 = 1;     // RB0 Bemenet
    INTCON2bits.RBPU = 0;     // PORTB belső felhúzók engedélyezése (Weak Pull-Up)
    TRISDbits.TRISD0 = 0;     // RD0 Kimenet (LED)
    LATDbits.LATD0 = 0;       // LED lekapcsolva
}

// Ezt a függvényt egy 10 ms-os Timer megszakításból hívjuk meg!
void debounce_task_10ms(void) {
    static btn_state_t state = BTN_RELEASED;
    static uint8_t timer = 0;
    bool pin_raw = (PORTBbits.RB0 == 0); // true ha lenyomva (aktív alacsony)

    switch (state) {
        case BTN_RELEASED:
            if (pin_raw) {
                state = BTN_DEBOUNCE_PRESS;
                timer = 2; // 2 x 10ms = 20ms szűrés
            }
            break;

        case BTN_DEBOUNCE_PRESS:
            if (--timer == 0) {
                if (pin_raw) {
                    state = BTN_PRESSED;
                    // GOMB LENYOMÁSI ESEMÉNY (Éldetektálás)
                    LATDbits.LATD0 ^= 1; // LED állapotváltás (Toggle)
                } else {
                    state = BTN_RELEASED; // Téves zajimpulzus volt
                }
            }
            break;

        case BTN_PRESSED:
            if (!pin_raw) {
                state = BTN_DEBOUNCE_RELEASE;
                timer = 2;
            }
            break;

        case BTN_DEBOUNCE_RELEASE:
            if (--timer == 0) {
                if (!pin_raw) {
                    state = BTN_RELEASED; // Gomb sikeresen elengedve
                } else {
                    state = BTN_PRESSED;
                }
            }
            break;
    }
}
Labor 2
DC motor fordulatszám és forgásirány vezérlése PWM-mel és H-híddal (L293D/L298)

Hardver Felépítés & Működés

A DC motor fordulatszámát a CCP1 modul által generált PWM jel kitöltési tényezőjével, az irányát pedig két digitális kimenettel (IN1, IN2) vezéreljük egy H-hídon (pl. L293D) keresztül. Timer2 időalap (FOSC = 8 MHz, Prescaler = 4, fPWM ≈ 5 kHz).

PIC18 C • PWM Motor Control
#include <xc.h>
#include <stdint.h>

#define MOTOR_IN1  LATDbits.LATD1
#define MOTOR_IN2  LATDbits.LATD2

void init_motor_pwm(void) {
    // 1. Irány bitek beállítása
    TRISCbits.TRISC2 = 0; // RC2 / CCP1 kimenet (PWM Enable a H-hídhoz)
    TRISDbits.TRISD1 = 0; // RD1 IN1 kimenet
    TRISDbits.TRISD2 = 0; // RD2 IN2 kimenet

    // 2. Timer2 konfigurálása 5 kHz PWM-hez (Fosc = 8MHz esetén)
    // T_pwm = (PR2 + 1) * 4 * Tosc * T2_prescaler = (99 + 1) * 4 * (1/8MHz) * 4 = 200us (5 kHz)
    PR2 = 99;
    T2CON = 0b00000101; // Timer2 ON, Prescaler 1:4

    // 3. CCP1 konfigurálása PWM módba
    CCP1CON = 0b00001100; // PWM mód
    CCPR1L = 0;           // Kezdeti 0% kitöltés
}

// duty_10bit: 0 .. 400 (400 = 100% kitöltés, mert 4 * (PR2 + 1) = 400)
void set_motor_speed_and_dir(int16_t speed) {
    if (speed > 0) {
        MOTOR_IN1 = 1; MOTOR_IN2 = 0; // Előre forgás
    } else if (speed < 0) {
        MOTOR_IN1 = 0; MOTOR_IN2 = 1; // Hátra forgás
        speed = -speed;
    } else {
        MOTOR_IN1 = 0; MOTOR_IN2 = 0; // Motor fék (Stop)
    }

    if (speed > 400) speed = 400; // Korlátozás

    // 10 bites kitöltés beírása (felső 8 bit -> CCPR1L, alsó 2 bit -> CCP1CON<5:4>)
    CCPR1L = (uint8_t)(speed >> 2);
    CCP1CON = (uint8_t)((CCP1CON & 0xCF) | ((speed & 0x03) << 4));
}
Labor 3
Hőmérséklet mérése ADC-vel és digitális mozgóátlag szűrővel (LM35 / NTC)

Működés & Kvantálás

Az LM35 analóg hőmérsékletszenzor 10 mV/°C karakterisztikájú kimenetet ad. A 10 bites ADC 5V referencia mellett 1 LSB ≈ 4.882 mV. A zajcsökkentés érdekében 16 egymást követő minta mozgóátlagát (Moving Average Filter) számítjuk ki.

PIC18 C • ADC & Moving Average
#include <xc.h>
#include <stdint.h>

void init_adc_an0(void) {
    TRISAbits.TRISA0 = 1;     // RA0/AN0 Bemenet
    ADCON1 = 0b00001110;      // AN0 Analóg, többi digitális, Vref+ = VDD, Vref- = VSS
    // ADFM=1 (Jobbra igazított 10 bit), ACQT = 8 TAD (4us Tacq), ADCS = FOSC/16
    ADCON2 = 0b10010010;
    ADCON0 = 0b00000001;      // CH0 kiválasztva, ADC Bekapcsolva (ADON=1)
}

uint16_t read_adc_raw(void) {
    ADCON0bits.GO = 1;                 // Konverzió indítása
    while (ADCON0bits.GO_nDONE);       // Várakozás a befejezésre (Hardware törli a bitet)
    return ((uint16_t)ADRESH << 8) | ADRESL; // 10 bites eredmény
}

// 16 elemű mozgóátlag számítás
uint16_t get_filtered_adc(void) {
    #define FILTER_SIZE 16
    static uint16_t buffer[FILTER_SIZE] = {0};
    static uint8_t idx = 0;
    static uint32_t sum = 0;

    sum -= buffer[idx];
    buffer[idx] = read_adc_raw();
    sum += buffer[idx];
    idx = (idx + 1) % FILTER_SIZE;

    return (uint16_t)(sum / FILTER_SIZE);
}

// Hőmérséklet számítása tizedfokban (pl. 254 = 25.4 °C)
int16_t get_temperature_deci_celsius(void) {
    uint16_t adc = get_filtered_adc();
    // V_in (mV) = (adc * 5000) / 1024
    // Temp (°C) = V_in / 10 mV
    // Temp (0.1 °C) = (adc * 50000) / 10240
    return (int16_t)(((uint32_t)adc * 5000) / 1024);
}
Labor 4
UART megszakítás-alapú aszinkron kommunikáció Körpufferrel (Ring Buffer)

Miért szükséges a Körpuffer és a Megszakítás?

Ha a mikrokontroller éppen más feladatot végez, a beérkező UART bájtok elvesznének (Overrun error - `OERR`). Az UART vételi megszakítás (`RCIE`) minden beérkező bájtot azonnal elment egy FIFO körpufferbe a háttérben.

PIC18 C • UART Interrupt & Ring Buffer
#include <xc.h>
#include <stdint.h>
#include <stdbool.h>

#define RX_BUFFER_SIZE 64
static volatile uint8_t rx_buf[RX_BUFFER_SIZE];
static volatile uint8_t rx_head = 0;
static volatile uint8_t rx_tail = 0;

void init_uart(uint32_t baud, uint32_t fosc) {
    TRISCbits.TRISC7 = 1; // RX bemenet
    TRISCbits.TRISC6 = 0; // TX kimenet

    // Baud rate beállítása BRGH=1 mellett: SPBRG = (fosc / (16 * baud)) - 1
    SPBRG = (uint8_t)((fosc / (16 * baud)) - 1);
    TXSTA = 0b00100100;   // TXEN=1, BRGH=1, 8-bit
    RCSTA = 0b10010000;   // SPEN=1, CREN=1 (Folyamatos vétel)

    // Vételi megszakítás engedélyezése
    PIE1bits.RCIE = 1;
    INTCONbits.PEIE = 1;
    INTCONbits.GIE = 1;
}

// Magas prioritású megszakítási rutin (ISR)
void __interrupt(high_priority) HighISR(void) {
    if (PIR1bits.RCIF && PIE1bits.RCIE) {
        // Hibakezelés (Overrun esetén a CREN resetelése kötelező!)
        if (RCSTAbits.OERR) {
            RCSTAbits.CREN = 0;
            RCSTAbits.CREN = 1;
        }
        
        uint8_t data = RCREG; // RCREG olvasása hardveresen törli a RCIF flaget!
        uint8_t next_head = (rx_head + 1) % RX_BUFFER_SIZE;
        if (next_head != rx_tail) { // Ha a puffer nincs tele
            rx_buf[rx_head] = data;
            rx_head = next_head;
        }
    }
}

bool uart_read_byte(uint8_t *byte) {
    if (rx_head == rx_tail) return false; // Üres a puffer
    *byte = rx_buf[rx_tail];
    rx_tail = (rx_tail + 1) % RX_BUFFER_SIZE;
    return true;
}

void uart_write_byte(uint8_t byte) {
    while (!TXSTAbits.TRMT); // Várakozás, míg az adó Shift regiszter kiürül
    TXREG = byte;
}
Labor 5
I2C Master kommunikáció regiszterszinten (MSSP modul) digitális szenzorhoz

Működés & Regiszterek (MSSP I2C Mód)

Az I2C buszon az adatok átvitele szigorú állapotgéppel vezérelt. Az alábbi kód bemutatja az I2C START, STOP, Címzés, Regisztermutató írás és Szenzorolvasás folyamatát 100 kHz órajelen.

PIC18 C • Hardware MSSP I2C Master
#include <xc.h>
#include <stdint.h>

void init_i2c_master(uint32_t fosc) {
    TRISCbits.TRISC3 = 1; // SCL bemenetként konfigurálandó (az MSSP modul vezérli)
    TRISCbits.TRISC4 = 1; // SDA bemenetként konfigurálandó
    
    // I2C Master mód, órajel = FOSC / (4 * (SSPADD + 1))
    SSPCON1 = 0b00101000; // SSPEN=1, I2C Master mode (clock = FOSC/(4 * (SSPADD+1)))
    SSPCON2 = 0x00;
    SSPSTAT = 0x80;       // Slew rate disabled for 100 kHz
    SSPADD = (uint8_t)((fosc / (4 * 100000)) - 1); // 100 kHz Baud
}

void i2c_wait_idle(void) {
    // Várakozás, míg a busz szabad (START, STOP, RESTART, ACKEN, RCEN vagy R/W folyamatban)
    while ((SSPCON2 & 0x1F) || (SSPSTATbits.R_nW));
}

void i2c_start(void) {
    i2c_wait_idle();
    SSPCON2bits.SEN = 1; // START feltétel indítása
}

void i2c_stop(void) {
    i2c_wait_idle();
    SSPCON2bits.PEN = 1; // STOP feltétel indítása
}

void i2c_write(uint8_t data) {
    i2c_wait_idle();
    SSPBUF = data; // Bájt beírása a shift regiszterbe
}

uint8_t i2c_read(uint8_t ack) {
    uint8_t data;
    i2c_wait_idle();
    SSPCON2bits.RCEN = 1; // Fogadás engedélyezése
    i2c_wait_idle();
    data = SSPBUF;
    i2c_wait_idle();
    SSPCON2bits.ACKDT = (ack ? 0 : 1); // 0 = ACK, 1 = NACK (utolsó bájtnál NACK kötelező!)
    SSPCON2bits.ACKEN = 1;             // Nyugtázás kiküldése
    return data;
}

// 1 bájtos szenzorregiszter kiolvasása I2C eszközből
uint8_t i2c_read_sensor_reg(uint8_t dev_addr_7bit, uint8_t reg_addr) {
    uint8_t val;
    i2c_start();
    i2c_write((uint8_t)(dev_addr_7bit << 1)); // Írás cím (R/W = 0)
    i2c_write(reg_addr);                       // Kívánt regiszter címe
    
    i2c_start();                               // Repeated START
    i2c_write((uint8_t)((dev_addr_7bit << 1) | 1)); // Olvasás cím (R/W = 1)
    val = i2c_read(0);                         // 0 = NACK küldése (utolsó bájt)
    i2c_stop();
    return val;
}
Labor 6
Karakteres LCD vezérlése SPI-n keresztül 74HC595 shiftregiszterrel (lábszám-takarékos kijelzés)

Hardver Felépítés & Működés

A szabványos HD44780 kompatibilis karakteres LCD 4 bites módban is 6 vezérlővonalat (RS, E, D4-D7) igényel. Ennek kiváltására a PIC18 hardveres SPI perifériáját (MSSP) egy 74HC595 shiftregiszter meghajtására használjuk, amely a soros bájtot 8 párhuzamos kimenetté alakítja. Mivel a hardveres SPI modul csak az adatot (SDO) és az órajelet (SCK) biztosítja, a 74HC595 tároló regiszter („Latch”/RCLK) vonalát egy különálló, szoftveresen vezérelt GPIO lábbal kell impulzálni minden bájt kiküldése után.

Vezetékezés (saját, a kódban rögzített kiosztás): a 74HC595 kimenetei Q0=RS, Q1=R/W (folyamatosan 0-ra kötve), Q2=E, Q3=háttérvilágítás, Q4..Q7=D4..D7 (a HD44780 4 bites adatbusza).

PIC18 C • SPI → 74HC595 → HD44780 LCD
#include <xc.h>
#include <stdint.h>

#define LCD_LATCH LATAbits.LATA0   // 74HC595 RCLK (Storage Register Clock / Latch)
#define BIT_RS 0
#define BIT_E  2

void init_spi_lcd(void) {
    TRISCbits.TRISC5 = 0;   // SDO/MOSI kimenet -> 74HC595 SER (soros adat)
    TRISCbits.TRISC3 = 0;   // SCK kimenet      -> 74HC595 SRCLK (shift órajel)
    TRISAbits.TRISA0 = 0;   // Latch kimenet    -> 74HC595 RCLK (tároló órajel)
    LCD_LATCH = 0;

    SSPSTAT = 0x00;         // SMP=0, CKE=0 (SPI Mode 0,0)
    SSPCON1 = 0b00100001;   // SSPEN=1, CKP=0, SPI Master mode, clock = FOSC/16
}

// Egy bájt kiküldése SPI-n a 74HC595-be, majd Latch impulzussal a Q0..Q7 kimenetre másolása
void shiftreg_write(uint8_t data) {
    PIR1bits.SSPIF = 0;
    SSPBUF = data;
    while (!PIR1bits.SSPIF);   // Várakozás, míg a 8 bit ki nem shiftelődött
    LCD_LATCH = 1;             // A shift regiszter tartalma megjelenik a kimeneteken
    LCD_LATCH = 0;
}

// Egy 4 bites adat/parancs nibble kiküldése az E vonal fel- majd lefuttatásával
// (a HD44780 a lefutó élen olvassa be az adatot)
void lcd_pulse_nibble(uint8_t rs, uint8_t nibble4bit) {
    uint8_t frame = (uint8_t)((nibble4bit & 0x0F) << 4) | (rs ? (1 << BIT_RS) : 0);
    shiftreg_write(frame | (1 << BIT_E));  // E = 1: adat érvényes
    shiftreg_write(frame);                    // E = 0: HD44780 beolvassa a nibble-t
}

void lcd_send_byte(uint8_t rs, uint8_t data) {
    lcd_pulse_nibble(rs, data >> 4);    // Felső nibble előbb (4 bites mód)
    lcd_pulse_nibble(rs, data & 0x0F);  // Alsó nibble másodszor
}

#define lcd_command(cmd) lcd_send_byte(0, (cmd))
#define lcd_data(ch)     lcd_send_byte(1, (ch))

// HD44780 szabványos 4 bites inicializálási szekvencia
void lcd_init_4bit(void) {
    lcd_command(0x28); // Function Set: 4 bites busz, 2 sor, 5x8 pontmátrix
    lcd_command(0x0C); // Display ON, kurzor és villogás KI
    lcd_command(0x06); // Entry Mode: automatikus jobbra léptetés íráskor
    lcd_command(0x01); // Kijelző törlése, kurzor a (0,0) pozícióba
}
Labor 7
Megszakítás-vezérelt LED villogtatás Timer0 túlcsordulással (blokkoló delay() nélkül)

Miért Timer megszakítás, és nem __delay_ms()?

Egy blokkoló __delay_ms(250) hívás a teljes időre lefagyasztja a main() ciklust, ezalatt semmilyen más feladat (gombfigyelés, kommunikáció) nem futhat. A Timer0 túlcsordulási megszakítása ezzel szemben a háttérben, pontos időzítéssel jelez, míg a main() szabadon végezhet más munkát — ez a 14. Tétel ISR aranyszabályainak gyakorlati alkalmazása.

PIC18 C • Timer0 Interrupt Blink
#include <xc.h>
#include <stdint.h>

#define LED_PIN LATDbits.LATD3

// Fosc = 8 MHz -> utasításciklus Tcy = 0.5us. Prescaler 1:32 -> 1 Timer0 tick = 16us.
// Kívánt megszakítási periódus: 250 ms = 15625 tick.
// Előtöltési érték (16 bites, felfelé számol 0xFFFF-ig): 65536 - 15625 = 49911 = 0xC2F7
#define TMR0_RELOAD_H 0xC2
#define TMR0_RELOAD_L 0xF7

void init_timer0_blink(void) {
    TRISDbits.TRISD3 = 0;    // LED kimenet
    LED_PIN = 0;

    T0CON = 0b00000100;      // TMR0 egyelőre KI, 16 bites mód, belső órajel, Prescaler 1:32
    TMR0H = TMR0_RELOAD_H;   // A felső bájt előbb a rejtett buffer regiszterbe kerül...
    TMR0L = TMR0_RELOAD_L;   // ...az alsó bájt írása aktiválja a tényleges 16 bites betöltést

    INTCONbits.TMR0IF = 0;
    INTCONbits.TMR0IE = 1;   // Timer0 megszakítás engedélyezése
    INTCONbits.GIE = 1;      // Globális megszakítás engedélyezése
    T0CONbits.TMR0ON = 1;    // Timer0 indítása
}

// Timer0 túlcsordulási ISR-ből hívandó kezelő
void handle_timer0_interrupt(void) {
    if (INTCONbits.TMR0IF) {
        TMR0H = TMR0_RELOAD_H;   // Újratöltés a következő pontos 250 ms-hoz
        TMR0L = TMR0_RELOAD_L;
        LED_PIN ^= 1;             // LED állapotváltás — a main() eközben szabadon fut tovább!
        INTCONbits.TMR0IF = 0;    // Kötelező flag törlés, különben végtelen ISR-hurok!
    }
}
Labor 8
Több mikrovezérlős szinkronizáció hardveres SPI Master-Slave móddal

Működés: Két PIC18F4620 Szinkronizálása SPI-n

Az eddigi feladatok az MSSP modult mindig Master módban használták (I2C Master, SPI shiftregiszter-meghajtás). Ez a feladat bemutatja a modul SPI Slave üzemmódját is: két PIC18F4620 kártya közül az egyik Master ként egy közös számlálót küld ki periodikusan, a másik Slave-ként fogadja azt megszakításban, és azonnal megjeleníti a saját LED-jein — így a két önálló mikrovezérlő kijelzése folyamatosan szinkronban marad.

PIC18 C • MASTER kártya (SPI Master)
#include <xc.h>
#include <stdint.h>

void init_spi_master_sync(void) {
    TRISCbits.TRISC5 = 0;  // SDO kimenet
    TRISCbits.TRISC4 = 1;  // SDI bemenet (nem használt ebben a feladatban)
    TRISCbits.TRISC3 = 0;  // SCK kimenet (a Master hajtja az órajelet)
    TRISAbits.TRISA5 = 0;  // SS (Slave Select) kimenet, kézi (szoftveres) vezérléssel
    LATAbits.LATA5 = 1;    // SS alaphelyzetben inaktív (magas szint)

    SSPSTAT = 0x00;         // SMP=0, CKE=0 (SPI Mode 0,0)
    SSPCON1 = 0b00100001;   // SSPEN=1, CKP=0, SPI Master mode, clock = FOSC/16
}

uint8_t spi_master_transfer(uint8_t data) {
    PIR1bits.SSPIF = 0;
    SSPBUF = data;
    while (!PIR1bits.SSPIF);
    return SSPBUF;
}

// Egy közös számláló periodikus szétküldése a Slave kártyának, hogy mindkét
// mikrovezérlő állapota (pl. a kijelzett LED-mintázat) szinkronban maradjon
void master_broadcast_sync_counter(uint8_t counter) {
    LATAbits.LATA5 = 0;              // SS aktiválása (Slave kiválasztása)
    spi_master_transfer(counter);    // Számlálóérték kiküldése
    LATAbits.LATA5 = 1;              // SS deaktiválása (átvitel vége, adat nyugtázva)
}
PIC18 C • SLAVE kártya (SPI Slave + ISR)
#include <xc.h>
#include <stdint.h>

void init_spi_slave_sync(void) {
    TRISCbits.TRISC5 = 1;  // SDO láb bemenetként (ebben a feladatban a Slave nem küld vissza adatot)
    TRISCbits.TRISC4 = 1;  // SDI bemenet (a Mastertől érkező adat)
    TRISCbits.TRISC3 = 1;  // SCK bemenet (az órajelet a Master hajtja)
    TRISAbits.TRISA5 = 1;  // SS bemenet (a Master vezérli, mikor aktív a Slave)

    SSPSTAT = 0x00;         // SMP=0 (Slave módban kötelező törölni), CKE=0
    SSPCON1 = 0b00100100;   // SSPEN=1, CKP=0, SPI Slave mode, SS pin vezérlés ENGEDÉLYEZVE

    PIE1bits.SSPIE = 1;     // MSSP (SPI) megszakítás engedélyezése
    INTCONbits.PEIE = 1;    // Perifériamegszakítások engedélyezése
    INTCONbits.GIE = 1;     // Globális megszakítás engedélyezése
}

// A Slave SPI megszakítási rutinja: minden beérkező bájtot azonnal
// megjelenít a PORTD LED-eken, így a két kártya kijelzése szinkronban marad
void slave_handle_ssp_interrupt(void) {
    if (PIR1bits.SSPIF) {
        uint8_t received = SSPBUF;   // A Mastertől kapott szinkronizációs bájt
        LATD = received;             // Azonnali, tükrözött megjelenítés
        PIR1bits.SSPIF = 0;          // Kötelező flag törlés!
    }
}