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