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).
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.
#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;
}
}
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).
#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));
}
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.
#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);
}
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.
#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;
}
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.
#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;
}
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).
#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
}
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.
#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!
}
}
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.
#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)
}
#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!
}
}
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.
#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));
}
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.
#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)
}
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.
#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;
}
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).
#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;
}
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.
#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
}
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.
#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;
}
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.
#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!
}
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.
#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)
}
}
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.
#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
}
}
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.
#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);
}