⚡ 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ő 18 gyakorlati 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!
    }
}
Labor 9
Potenciométer kiolvasása és LED fényerő vezérlése (ADC + PWM összekötése)
▼

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

Egy potenciométer harmadik (csúszka) lába az AN0 (RA0) analóg bemenetre csatlakozik, a két végpontja pedig VDD és GND közé van kötve — így egy feszültségosztót alkotva, amelynek kimenete a potméter állásától függően 0V és 5V között tetszőlegesen állítható. A feladat a 10 bites ADC eredményét (0..1023) közvetlenül átskálázni a CCP1 modul 10 bites PWM kitöltési tartományára, hogy egy LED fényereje folyamatosan, simán kövesse a potméter elforgatását.

PIC18 C • ADC-vezérelt PWM Fényerő
#include <xc.h>
#include <stdint.h>

void init_adc_pot(void) {
    TRISAbits.TRISA0 = 1;     // RA0/AN0 bemenet (potméter csúszkája)
    ADCON1 = 0b00001110;      // Csak AN0 analóg, a többi digitális
    ADCON2 = 0b10010010;      // ADFM=1 (jobbra igazított), ACQT=8 TAD, ADCS=FOSC/16
    ADCON0 = 0b00000001;      // CH0 kiválasztva, ADON=1
}

void init_pwm_led(void) {
    TRISCbits.TRISC2 = 0;     // RC2/CCP1 kimenet
    PR2 = 249;                // f_PWM = Fosc/(4*(PR2+1)*prescaler), 20MHz-nél kb. 5kHz 1:4 előosztóval
    T2CON = 0b00000101;       // Timer2 ON, Prescaler 1:4
    CCP1CON = 0b00001100;     // CCP1 PWM mód
    CCPR1L = 0;
}

uint16_t read_adc_pot(void) {
    ADCON0bits.GO = 1;
    while (ADCON0bits.GO_nDONE);
    return ((uint16_t)ADRESH << 8) | ADRESL; // 0..1023
}

// A 10 bites ADC érték (0..1023) közvetlenül a 10 bites PWM
// kitöltés tartományába esik, nincs szükség külön átskálázásra!
void update_led_brightness(void) {
    uint16_t adc_val = read_adc_pot();
    CCPR1L = (uint8_t)(adc_val >> 2);
    CCP1CON = (uint8_t)((CCP1CON & 0xCF) | ((adc_val & 0x03) << 4));
}
Labor 10
Digitális hőmérsékletmérés DS18B20 szenzorral (szoftveresen implementált 1-Wire protokoll)
▼

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

A Labor 3 feladattal ellentétben (ahol egy analóg LM35 szenzort ADC-vel olvastunk ki), a DS18B20 egy digitális hőmérsékletszenzor, amely egyetlen adatvonalon (DQ), szoftveresen implementált 1-Wire időzítéssel kommunikál — nincs szükség ADC-re, mert a szenzor maga végzi a mérést és digitális formában szolgáltatja az eredményt. A DQ vonalra kötelező egy kb. 4.7kΩ-os külső felhúzó ellenállás, mert a vonal Open-Drain jellegű. Az időzítést az XC8 __delay_us() makróival valósítjuk meg, ezért az _XTAL_FREQ definiálása kötelező a fordítás előtt.

PIC18 C • Bit-bangelt 1-Wire Protokoll
#include <xc.h>
#include <stdint.h>

#define _XTAL_FREQ 8000000
#define DQ_TRIS  TRISCbits.TRISC2
#define DQ_LAT   LATCbits.LATC2
#define DQ_READ  PORTCbits.RC2

// Reset impulzus + a szenzor "presence" (jelenlét) válaszának ellenőrzése
uint8_t ds18b20_reset(void) {
    uint8_t presence;
    DQ_TRIS = 0; DQ_LAT = 0;
    __delay_us(480);          // Reset impulzus: minimum 480us alacsony szint
    DQ_TRIS = 1;               // Busz elengedése (felhúzó ellenállás húzza fel)
    __delay_us(70);            // Várakozás a presence pulse-ra (adatlap: 15-60us + ráhagyás)
    presence = !DQ_READ;       // 0 = a szenzor lehúzta a vonalat, tehát jelen van
    __delay_us(410);           // Az időszelet hátralévő része (min. 480us összesen)
    return presence;
}

void ds18b20_write_bit(uint8_t bit) {
    DQ_TRIS = 0; DQ_LAT = 0;    // Időszelet indítása: buszt lehúzzuk
    if (bit) {
        __delay_us(2);
        DQ_TRIS = 1;            // '1' írásakor korán elengedjük (15us-on belül)
        __delay_us(58);
    } else {
        __delay_us(60);         // '0' írásakor a teljes időszelet alatt lent tartjuk
        DQ_TRIS = 1;
    }
    __delay_us(2);              // Recovery idő a következő időszelet előtt
}

uint8_t ds18b20_read_bit(void) {
    uint8_t bit;
    DQ_TRIS = 0; DQ_LAT = 0;
    __delay_us(2);               // Rövid impulzus indítja az olvasási időszeletet
    DQ_TRIS = 1;                  // Busz elengedése, a szenzor veszi át
    __delay_us(8);                // Mintavétel a felfutás után, 15us-on belül
    bit = DQ_READ;
    __delay_us(50);               // Az időszelet hátralévő része
    return bit;
}

void ds18b20_write_byte(uint8_t data) {
    for (uint8_t i = 0; i < 8; i++) {
        ds18b20_write_bit(data & 0x01);  // LSB-first
        data >>= 1;
    }
}

uint8_t ds18b20_read_byte(void) {
    uint8_t data = 0;
    for (uint8_t i = 0; i < 8; i++) {
        data |= (ds18b20_read_bit() << i);  // LSB-first
    }
    return data;
}

// Teljes mérési ciklus: konverzió indítása, majd a Scratchpad kiolvasása
int16_t ds18b20_read_temp_deci_celsius(void) {
    uint8_t lsb, msb;
    int16_t raw;

    ds18b20_reset();
    ds18b20_write_byte(0xCC);   // SKIP ROM (egyetlen szenzor a buszon)
    ds18b20_write_byte(0x44);   // CONVERT T (mérés indítása)
    __delay_ms(750);            // Max. konverziós idő 12 bites felbontásnál

    ds18b20_reset();
    ds18b20_write_byte(0xCC);   // SKIP ROM
    ds18b20_write_byte(0xBE);   // READ SCRATCHPAD
    lsb = ds18b20_read_byte();
    msb = ds18b20_read_byte();

    raw = (int16_t)((msb << 8) | lsb);  // 12 bites, előjelesen kiterjesztett érték
    return (int16_t)((raw * 10) / 16);    // 1 LSB = 1/16 °C -> tizedfok (pl. 250 = 25.0°C)
}
Labor 11
Többdigites 7-szegmenses kijelző multiplexelt (időosztásos) vezérlése
▼

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

Négy közös katódos 7-szegmenses kijelző szegmensvonalai (a-g, dp) közösen a PORTD-re, míg mindegyik digit közös katódja külön-külön a PORTC alsó 4 bitjére csatlakozik. Mivel egyszerre csak EGY digitet szeretnénk világítani (a szegmensvonalak közösek), a 4 számjegyet gyors egymásutánban, multiplexelve (időosztásosan) jelenítjük meg: minden digitet csak néhány milliszekundumig kapcsolunk be, de olyan gyorsan váltogatva (Timer0 megszakításból, kb. 2 ms-onként), hogy az emberi szem tehetetlensége (persistence of vision) miatt a néző mind a 4 számjegyet folyamatosan égve látja.

PIC18 C • 4 Digites Multiplexelt Kijelző
#include <xc.h>
#include <stdint.h>

// Közös katódos 7-szegmenses kódtábla (0-9), Flash-ben tárolva
const uint8_t SEGMENT_TABLE[10] = {
    0b00111111, 0b00000110, 0b01011011, 0b01001111, 0b01100110,
    0b01101101, 0b01111101, 0b00000111, 0b01111111, 0b01101111
};

static volatile uint8_t digit_values[4] = {0, 0, 0, 0}; // Kijelzendő 4 számjegy
static volatile uint8_t active_digit = 0;

void init_display_mux(void) {
    TRISD = 0x00;              // PORTD: szegmensvonalak, mind kimenet
    TRISC &= 0xF0;             // PORTC alsó 4 bitje: digitválasztó kimenetek
    LATD = 0x00;
    LATC &= 0xF0;
}

// Timer0 megszakításból hívandó, kb. 2 ms-onként (kb. 500 Hz-es váltás)
void display_mux_refresh(void) {
    LATC &= 0xF0;                                   // Minden digit kikapcsolása (villódzás-mentesítés)
    LATD = SEGMENT_TABLE[digit_values[active_digit]]; // Aktuális digit szegmensmintája
    LATC |= (1 << active_digit);                     // Csak az aktuális digit közös katódja aktív
    active_digit = (active_digit + 1) % 4;            // Következő digitre lépés körkörösen
}

// Egy 4 jegyű szám (0-9999) szétbontása a 4 digit pufferbe
void display_set_number(uint16_t number) {
    digit_values[0] = number / 1000;
    digit_values[1] = (number / 100) % 10;
    digit_values[2] = (number / 10) % 10;
    digit_values[3] = number % 10;
}
Labor 12
Ultrahangos távolságmérés HC-SR04 szenzorral (CCP Capture mód gyakorlati alkalmazása)
▼

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

A HC-SR04 modul Trig lábára egy legalább 10 µs széles magas impulzust kell adni, amire a szenzor 8 db 40 kHz-es ultrahang-impulzust bocsát ki, majd az Echo lábon egy olyan hosszúságú magas impulzussal válaszol, amely PONTOSAN arányos a hang oda-vissza terjedési idejével. Az Echo lábat a CCP1 (RC2) bemenetre kötve, Capture módban mérjük meg ennek az impulzusnak a pontos szélességét: a felfutó élnél elmentjük a Timer1 értékét, majd átváltunk lefutó élre figyelésre, és a következő Capture eseménynél ismét elmentjük — a két érték különbsége adja a pontos impulzusidőt mikroszekundumban (Fosc=8MHz, Timer1 1:1 előosztóval, ahol 1 tick = 0.5 µs).

PIC18 C • HC-SR04 Trigger + CCP1 Capture
#include <xc.h>
#include <stdint.h>

#define _XTAL_FREQ 8000000
#define TRIG_PIN LATDbits.LATD3

static volatile uint16_t capture_start = 0;
static volatile uint16_t capture_end = 0;
static volatile bool waiting_for_falling_edge = false;
static volatile bool echo_ready = false;

void init_ultrasonic(void) {
    TRISDbits.TRISD3 = 0;      // Trig kimenet
    TRISCbits.TRISC2 = 1;      // CCP1/Echo bemenet

    T1CON = 0b00000001;        // Timer1 ON, előosztó 1:1, belső órajel (Fosc/4)
    CCP1CON = 0b00000101;      // Capture mód: minden FELFUTÓ élre rögzít
    waiting_for_falling_edge = false;

    PIE1bits.CCP1IE = 1;
    INTCONbits.PEIE = 1;
    INTCONbits.GIE = 1;
}

void trigger_measurement(void) {
    TRIG_PIN = 1;
    __delay_us(10);             // Minimum 10us széles trigger impulzus
    TRIG_PIN = 0;
    echo_ready = false;
}

// CCP1 ISR-ből hívandó rész: egy saját állapotjelzővel (nem a CCP1CON
// visszaolvasásával!) követi nyomon, hogy épp a felfutó vagy lefutó élre vár
void handle_ccp1_capture(void) {
    if (PIR1bits.CCP1IF) {
        uint16_t captured_value = (uint16_t)(((uint16_t)CCPR1H << 8) | CCPR1L);

        if (!waiting_for_falling_edge) {
            // Ez a START időbélyeg (felfutó él)
            capture_start = captured_value;
            CCP1CON = (CCP1CON & 0xF0) | 0b0100; // Átváltás LEFUTÓ élre
            waiting_for_falling_edge = true;
        } else {
            // Ez a STOP időbélyeg (lefutó él)
            capture_end = captured_value;
            CCP1CON = (CCP1CON & 0xF0) | 0b0101; // Vissza FELFUTÓ élre a következő méréshez
            waiting_for_falling_edge = false;
            echo_ready = true;
        }
        PIR1bits.CCP1IF = 0;
    }
}

// Fosc=8MHz, Timer1 1:1 -> 1 tick = 0.5us. distance_cm = idő_us / 58
uint16_t get_distance_cm(void) {
    uint16_t pulse_ticks = capture_end - capture_start;
    uint16_t pulse_us = pulse_ticks / 2; // 1 tick = 0.5us, tehát /2 -> us
    return pulse_us / 58;
}
Labor 13
4×4-es mátrix billentyűzet beolvasása soros-oszlopos szkenneléssel
▼

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

Egy 4×4-es mátrix billentyűzetnél a 16 gomb csak 8 GPIO lábbal (4 sor + 4 oszlop) kezelhető, mert a gombok a sor- és oszlopvonalak metszéspontjaiban helyezkednek el. A szkennelési technika lényege: a 4 sorvonalat kimenetnek, a 4 oszlopvonalat bemenetnek (belső felhúzó ellenállással) állítjuk be. Egyesével LOW szintre húzzuk az egyes sorokat (a többi sor közben magas/High-Z marad), majd megvizsgáljuk, mely oszlopvonal esett LOW szintre — ha egy oszlop LOW-t mutat, az adott sor-oszlop metszéspontjában lévő gomb éppen le van nyomva.

PIC18 C • 4x4 Mátrix Billentyűzet Szkennelés
#include <xc.h>
#include <stdint.h>

// Sorok: RB0-RB3 (kimenet), Oszlopok: RB4-RB7 (bemenet, belső felhúzóval)
#define ROWS_MASK 0x0F
#define COLS_MASK 0xF0

const char KEYMAP[4][4] = {
    {'1','2','3','A'},
    {'4','5','6','B'},
    {'7','8','9','C'},
    {'*','0','#','D'}
};

void init_keypad(void) {
    TRISB = 0xF0;               // RB0-3 kimenet (sorok), RB4-7 bemenet (oszlopok)
    INTCON2bits.RBPU = 0;       // PORTB belső felhúzók engedélyezése
    LATB |= ROWS_MASK;          // Minden sor alapból magas (inaktív)
}

// Visszaadja a lenyomott billentyű karakterét, vagy 0-t, ha nincs lenyomva gomb
char scan_keypad(void) {
    for (uint8_t row = 0; row < 4; row++) {
        LATB |= ROWS_MASK;             // Minden sor magasra (inaktív)
        LATB &= ~(1 << row);          // Csak az aktuális sor LOW
        __delay_us(50);                 // Rövid várakozás a jel stabilizálódására

        for (uint8_t col = 0; col < 4; col++) {
            if ((PORTB & (1 << (col + 4))) == 0) {
                while ((PORTB & (1 << (col + 4))) == 0); // Várakozás elengedésig (egyszerű debounce)
                return KEYMAP[row][col];
            }
        }
    }
    return 0; // Nincs lenyomott gomb
}
Labor 14
Piezo hangjelző vezérlése: hangfrekvenciás jel generálása a CCP/PWM modullal
▼

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

Egy passzív piezo hangszóró egyszerű négyszögjellel (50%-os kitöltési tényezővel) megszólaltatható tetszőleges, hallható frekvencián (kb. 20 Hz - 20 kHz). Ehhez ugyanazt a CCP1/Timer2 PWM hardvert használjuk fel, mint a Labor 2 és Labor 9 feladatokban, csak itt a PWM frekvenciája maga a hangmagasság, a kitöltési tényék pedig fixen 50% marad. Egy adott hang megszólaltatásához a PR2 regisztert kell újraszámolni az adott frekvenciára, majd a kitöltést a periódus felére állítani.

PIC18 C • Piezo Hangjelző Tónusgenerálás
#include <xc.h>
#include <stdint.h>

#define _XTAL_FREQ 8000000

void init_buzzer(void) {
    TRISCbits.TRISC2 = 0;      // RC2/CCP1 kimenet a piezóhoz
    T2CON = 0b00000101;        // Timer2 ON, Prescaler 1:4
    CCP1CON = 0b00001100;      // PWM mód
}

// Adott frekvenciájú (Hz) hang megszólaltatása kb. duration_ms ideig, 50% kitöltéssel
// f_PWM = Fosc / (4 * (PR2+1) * prescaler)  =>  PR2 = Fosc / (4 * prescaler * freq) - 1
// Prescaler = 4 (T2CON alapján), tehát PR2 = Fosc / (16 * freq) - 1
void play_tone(uint16_t freq_hz, uint16_t duration_ms) {
    uint16_t pr2_value = (uint16_t)((_XTAL_FREQ / (16UL * freq_hz)) - 1);
    uint16_t duty_10bit = (uint16_t)(((uint32_t)(pr2_value + 1) * 4) / 2); // 50% kitöltés

    PR2 = (uint8_t)pr2_value;
    CCPR1L = (uint8_t)(duty_10bit >> 2);
    CCP1CON = (uint8_t)((CCP1CON & 0xCF) | ((duty_10bit & 0x03) << 4));

    __delay_ms(1); // (a tényleges várakozást egy Timer/loop valósítja meg duration_ms-ig)
    // Egyszerűsített példa: valós alkalmazásban egy szoftveres számláló
    // méri a duration_ms-t, majd a hang leállításához CCPR1L = 0; állítandó.
}

void stop_tone(void) {
    CCPR1L = 0; // 0% kitöltés = néma
    CCP1CON &= 0xCF;
}
Labor 15
Fényerő-érzékelés LDR-rel és automatikus világításvezérlés hiszterézissel
▼

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

Egy LDR (fényfüggő ellenállás) egy fix ellenállással feszültségosztót alkotva az AN2 bemenetre csatlakozik: sötétben az LDR ellenállása magas (a feszültségosztó kimenete alacsony), fényben alacsony (a kimenet magas — a pontos irány a bekötéstől függ). A feladat egy egyszerű küszöbérték-vezérlésnek TŰNIK, de ha csak EGYETLEN küszöböt használnánk, a fényerő határértéken (pl. alkonyatkor) történő apró ingadozása a kimenet (pl. egy relé) folyamatos villogását (kattogását) okozná. A megoldás a hiszterézis: két KÜLÖNBÖZŐ küszöböt definiálunk — egyet a bekapcsoláshoz, egyet a kikapcsoláshoz —, így a kimenet csak akkor vált állapotot, ha a fényerő ÉRDEMBEN (a két küszöb közötti sávot átlépve) megváltozik.

PIC18 C • LDR + Hiszterézises Világításvezérlés
#include <xc.h>
#include <stdint.h>
#include <stdbool.h>

#define LIGHT_ON_THRESHOLD   300  // Ez alatt (sötétebb) bekapcsol
#define LIGHT_OFF_THRESHOLD  450  // Ez fölött (világosabb) kikapcsol
#define RELAY_PIN LATDbits.LATD4

static bool light_is_on = false;

void init_ldr(void) {
    TRISAbits.TRISA2 = 1;      // RA2/AN2 bemenet
    ADCON1 = 0b00001100;       // PCFG=1100: AN0, AN1 és AN2 analóg, a többi digitális
                                 // (a PIC18F4620 PCFG táblája nem tesz lehetővé "csak AN2" konfigurációt,
                                 // az AN0..AN2 tartomány mindig együtt válik analóggá)
    ADCON2 = 0b10010010;
    ADCON0 = 0b00001001;       // CH2 kiválasztva (CHS=0010), ADON=1
    TRISDbits.TRISD4 = 0;      // Relé/LED kimenet
}

uint16_t read_ldr(void) {
    ADCON0bits.GO = 1;
    while (ADCON0bits.GO_nDONE);
    return ((uint16_t)ADRESH << 8) | ADRESL;
}

// Hiszterézises döntéshozatal: a kimenet csak akkor vált,
// ha a mért érték ténylegesen átlépi a MÁSIK küszöböt
void update_light_control(void) {
    uint16_t brightness = read_ldr();

    if (!light_is_on && brightness < LIGHT_ON_THRESHOLD) {
        light_is_on = true;
        RELAY_PIN = 1;
    } else if (light_is_on && brightness > LIGHT_OFF_THRESHOLD) {
        light_is_on = false;
        RELAY_PIN = 0;
    }
    // A 300 és 450 közötti "holt sávban" a kimenet szándékosan NEM változik!
}
Labor 16
Egyszerű szöveges parancsértelmező (Command Parser) UART körpuffer felett
▼

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

A Labor 4 feladat már megvalósítja a megszakítás-alapú UART vételt egy körpufferbe — ez a feladat egy magasabb szintű alkalmazási réteget épít erre: a beérkező bájtokat egy sorpufferbe gyűjti, amíg egy sorvég-karakter (\n) nem érkezik, majd az összegyűjtött szöveges parancsot (pl. "LED ON" vagy "LED OFF") egy egyszerű strcmp-alapú összehasonlítással értelmezi és végrehajtja. Ez a minta (bájt-szintű vétel + sorpuffer + parancsértelmezés) az alapja a legtöbb soros konzolos beágyazott alkalmazásnak.

PIC18 C • Sorgyűjtés & Parancsértelmezés
#include <xc.h>
#include <stdint.h>
#include <string.h>

#define LINE_BUFFER_SIZE 32
static char line_buffer[LINE_BUFFER_SIZE];
static uint8_t line_index = 0;

// A Labor 4 uart_read_byte() függvényét használja fel a körpufferből olvasáshoz
extern bool uart_read_byte(uint8_t *byte);
extern void uart_write_byte(uint8_t byte);

void uart_write_string(const char *str) {
    while (*str) {
        uart_write_byte((uint8_t)*str++);
    }
}

void execute_command(const char *cmd) {
    if (strcmp(cmd, "LED ON") == 0) {
        LATDbits.LATD0 = 1;
        uart_write_string("OK\r\n");
    } else if (strcmp(cmd, "LED OFF") == 0) {
        LATDbits.LATD0 = 0;
        uart_write_string("OK\r\n");
    } else {
        uart_write_string("ERR\r\n");
    }
    // ...további parancsok bővíthetők else-if ágakkal...
}

// A main() ciklusból hívandó, minden egyes körben ellenőrzi a puffert
void process_uart_commands(void) {
    uint8_t byte;
    while (uart_read_byte(&byte)) {
        if (byte == '\n' || byte == '\r') {
            if (line_index > 0) {
                line_buffer[line_index] = '\0'; // Lezáró null karakter
                execute_command(line_buffer);
                line_index = 0;                  // Puffer nullázása a következő sorhoz
            }
        } else if (line_index < (LINE_BUFFER_SIZE - 1)) {
            line_buffer[line_index++] = (char)byte;
        }
        // Ha a sor túl hosszú lenne, a felesleges karakterek eldobódnak (nincs túlcsordulás)
    }
}
Labor 17
Léptetőmotor (28BYJ-48) vezérlése ULN2003 meghajtóval, fél-lépéses szekvenciával
▼

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

A léptetőmotor a DC motortól (Labor 2) eltérően NEM folyamatosan forog, hanem diszkrét, pontos szögelfordulásokkal ("lépésekkel") mozog — ez pontos pozicionálást tesz lehetővé visszacsatolás (pl. enkóder) nélkül is. A 28BYJ-48 unipoláris léptetőmotort egy ULN2003 tranzisztortömb-meghajtón keresztül 4 GPIO lábbal (IN1-IN4) vezéreljük. A "fél-lépéses" (half-step) üzemmódban a 4 tekercset egy 8 elemű, felváltva 1 és 2 aktív tekercset tartalmazó szekvencia szerint kapcsolgatjuk — ez a gyártó által ajánlott, legsimább (dupla felbontású) mozgást biztosító vezérlési mód.

PIC18 C • Léptetőmotor Fél-lépéses Vezérlés
#include <xc.h>
#include <stdint.h>

// IN1-IN4: RD0-RD3. A 8 lépéses fél-lépéses szekvencia (28BYJ-48 adatlap szerint)
const uint8_t HALF_STEP_SEQUENCE[8] = {
    0b0001, 0b0011, 0b0010, 0b0110,
    0b0100, 0b1100, 0b1000, 0b1001
};

static int8_t current_step = 0;

void init_stepper(void) {
    TRISD &= 0xF0;   // RD0-RD3 kimenet (IN1-IN4)
    LATD &= 0xF0;
}

// steps > 0: előre forgás, steps < 0: hátra forgás
// delay_ms a lépések közötti várakozás (tipikusan 2-15 ms a motor sebességétől függően)
void stepper_move(int16_t steps, uint16_t delay_ms) {
    int8_t direction = (steps > 0) ? 1 : -1;
    uint16_t abs_steps = (steps > 0) ? steps : -steps;

    for (uint16_t i = 0; i < abs_steps; i++) {
        current_step = (int8_t)((current_step + direction + 8) % 8); // Körkörös index
        LATD = (LATD & 0xF0) | HALF_STEP_SEQUENCE[current_step];
        __delay_ms(1);
        // A delay_ms fennmaradó részét egy Timer-alapú várakozás adná valós alkalmazásban
    }
}
Labor 18
Beállítások megőrzése tápkimaradás után: Data EEPROM olvasás és írás
▼

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

A PIC18F4620 beépített, néhány száz bájtos Data EEPROM memóriája — szemben az SRAM-mal — megőrzi tartalmát a tápfeszültség kikapcsolása után is. A feladat egy gyakorlati alkalmazása: a Labor 9-ben beállított PWM fényerő-szintet (vagy bármilyen felhasználói beállítást) minden módosításkor elmentjük az EEPROM-ba, majd bekapcsoláskor onnan töltjük vissza — így az eszköz "emlékszik" az utoljára beállított értékre, ahelyett hogy minden indításkor egy alapértelmezett értékkel kezdene.

PIC18 C • Data EEPROM Perzisztens Beállítás
#include <xc.h>
#include <stdint.h>

#define EEPROM_ADDR_BRIGHTNESS  0x00
#define EEPROM_ADDR_MAGIC       0x01
#define EEPROM_MAGIC_VALUE      0xA5  // Jelzi, hogy már volt érvényes mentés

uint8_t eeprom_read(uint8_t address) {
    EEADR = address;
    EECON1bits.EEPGD = 0;   // Data EEPROM (nem programmemória)
    EECON1bits.CFGS  = 0;   // EEPROM/Flash elérés (nem config bit)
    EECON1bits.RD    = 1;   // Olvasás indítása
    return EEDATA;
}

void eeprom_write(uint8_t address, uint8_t data) {
    EEADR  = address;
    EEDATA = data;
    EECON1bits.EEPGD = 0;
    EECON1bits.CFGS  = 0;
    EECON1bits.WREN  = 1;   // Írás engedélyezése

    INTCONbits.GIE = 0;     // Megszakítások letiltása az unlock szekvencia alatt
    EECON2 = 0x55;          // Kötelező unlock szekvencia
    EECON2 = 0xAA;
    EECON1bits.WR  = 1;     // Írás indítása
    INTCONbits.GIE = 1;

    EECON1bits.WREN = 0;    // Írás letiltása (véletlen írás elleni védelem)
}

// Induláskor hívandó: ha van érvényes korábbi mentés, visszatölti azt,
// egyébként egy alapértelmezett fényerő-értéket ír be és ment el
uint8_t load_brightness_setting(void) {
    if (eeprom_read(EEPROM_ADDR_MAGIC) == EEPROM_MAGIC_VALUE) {
        return eeprom_read(EEPROM_ADDR_BRIGHTNESS);
    } else {
        uint8_t default_brightness = 128;
        eeprom_write(EEPROM_ADDR_BRIGHTNESS, default_brightness);
        eeprom_write(EEPROM_ADDR_MAGIC, EEPROM_MAGIC_VALUE);
        return default_brightness;
    }
}

// A potméter (Labor 9) új értékének elmentése, pl. néhány másodpercenkénti
// változás-ellenőrzés után hívva (nem minden ciklusban, hogy kímélje az EEPROM-ot!)
void save_brightness_setting(uint8_t brightness) {
    eeprom_write(EEPROM_ADDR_BRIGHTNESS, brightness);
}