RFID Door Access System (PIC MCU-based)

Thread Starter

Embededd

Joined Jun 4, 2025
145
Hello Experts,

I’m currently working on an RFID-Based Door Access Control System ( hobby project ) that manages room access through RFID authentication, time-based shift validation, and event logging. The system will use PIC Microcontroller and interfaces with several peripherals:

System Components:

  1. PIC18F45K22 microcontroller Unit (MCU)
  2. EM-18 RFID Reader (UART communication)
  3. RTC DS1307 (I2C communication)
  4. EEPROM (I2C communication)
  5. 16x2 LCD Display
  6. Relay Module
  7. Buzzer
  8. Red and Green LEDs
  9. Push Buttons
  10. PC for data loading via UART

Operational Flow:

1. Initialization:
Upon star-tup, the MCU initializes all peripherals including UART, I2C, GPIO, LCD, RTC, EEPROM, RFID reader, relays, LEDs, buzzer, and buttons.

2. RFID Authentication:
When an RFID card is scanned using the EM-18 reader, the card ID is transmitted to the MCU via UART. The MCU checks the card ID against the stored employee data.

3. Shift Validation:
The current time is retrieved from the RTC DS1307 via I2C. The system compares this time with the shift timings assigned to the cardholder. If the card is valid and the time falls within the allowed shift, access is granted.

4. Access Control:
- The relay is activated to unlock the door.
- A green LED lights up and the buzzer sounds briefly.
- The LCD displays a success message.
- The entry event is logged to EEPROM with card ID, timestamp, and status.

5. Access Denial:
If the card is invalid or the time is outside the allowed shift:
- A red LED lights up and the buzzer sounds differently.
- The LCD displays a denial message.
- No access is granted and the event may be logged for auditing.

6. Exit Mechanism:
- The user presses a button to exit the room.
- The relay is activated again to unlock the door.
- The exit event is logged to EEPROM with timestamp and status.

7. System Health Monitoring:
- The system periodically checks the status of critical peripherals (RTC, EEPROM, RFID reader).
- If a fault is detected, the system logs the error and alerts the user via LEDs or buzzer.

8. Debugging Support:
- Debug messages are sent via UART or displayed on the LCD.
- These messages help developers trace issues during development and deployment.

but as the project grows, maintaining all the logic inside a single or few files is getting messy. I want to structure the project for better modularity, portability, and maintainability. Ideally, if I switch from PIC to STM32, or upgrade a peripheral (for example, use a different EEPROM), I shouldn’t need to rewrite my main application just swap or modify the driver.

My plan is to clearly separate the project into layers so that:

  • The application logic (RFID authentication, shift checking, logging, etc.) is isolated from hardware-specific code.
  • The drivers are written in a generic way so they can be reused across platforms.
  • The MCU-specific implementations (like register-level UART, I2C, GPIO) live in a dedicated platform layer.

The goal is to keep the application layer hardware-independent. For example, the app would simply call EEPROM_Write() without caring whether it’s talking to an AT24C02 or another chip that detail is handled by the driver, and the driver itself calls the MCU-specific functions from the platform layer.

Here’s the structure I’m planning to follow:

Code:
RFID_Access_System/
src/          → application logic (RFID, logging, display, etc.)
drivers/      → generic hardware drivers (UART, I2C, LCD, EEPROM, etc.)
platform/     → MCU-specific code (PIC, ATmega, STM32, etc.)
include/      → all header files
config/       → pin mappings, constants, and macros
utils/        → diagnostics, error codes
README.md
I’d really appreciate some expert feedback on the structure I’ve come up with.

Does this kind of layered approach make sense for a large-sized embedded project like this?

And are there any better practices or commonly used approach to achieve s modularity, portability, and maintainability in such systems?

Thanks for any advice or suggestions. I’d like to hear how others usually handle this kind of project organization.
 
Last edited:

meth

Joined May 21, 2016
302
I don't want to discourage your attempt but it would be a lot easier and flexible if you use RPi instead of uC.

I understand that you want to practice your skills, this is actually a great project which involves many different technologies, but just saying, for real-life application all that ID checking and logging works with DB server. I don't think anyone uses local EPROMs anymore.

As I said, I dont want to discourage, these tasks might seem simple but they are labor intensive indeed until you get everything working. If you manage to complete everything you stated you are on a great path. Maybe if you want to really go next level you can add some kind of WiFi module to your design.

As for advices.. the best one I give everybody is: work with small steps. Do not write 500 lines of code because when you test it and nothing works you will have absolutely no idea what is wrong. Break everything in small steps or tasks and do not jump into the next until you are completely happy with the results you are having with the current.
 

Thread Starter

Embededd

Joined Jun 4, 2025
145
I don't want to discourage your attempt but it would be a lot easier and flexible if you use RPi instead of uC.
I totally get your point using an RPi would definitely make things a lot simpler in terms of development. But in my case, the main goal isn’t just to get the system running it’s to learn and improve my skills in embedded software design and development.

Working directly with a microcontroller helps me understand the underlying hardware behaviour, register-level control and how different peripherals interact. It’s a bit more work, but that’s exactly where the learning happens.

As I said, I dont want to discourage, these tasks might seem simple but they are labor intensive indeed until you get everything working. If you manage to complete everything you stated you are on a great path.
Thanks, I really appreciate that. You’re right, getting everything integrated is the hard part.
Most of the individual modules ( UART, LCD, RTC, RFID, I2C) are already working. I’ve tested them separately in my previous thread, so that part isn’t a concern for me right now

At this stage, my main focus is on structuring the project properly making it modular and portable

Maybe if you want to really go next level you can add some kind of WiFi module to your design.
Yes, that’s actually part of my long-term plan. I’ll be adding a WiFi module later. the idea is that the MCU will send “granted” or "denied" messages to an ESP32 over UART, and the ESP32 will then forward that data to a central server.

I know I could just use the ESP32 directly instead of a separate MCU, but I’ve intentionally avoided that. My goal here is to build a strong foundation in low-level embedded software development working closely with registers, drivers, and MCU-level design decisions.

For now, I’m using a PC for that communication, but eventually the ESP32 will take over that role wirelessly.

Overall, this is part of my effort to make better design decisions while structuring the hobby project, and I’d really appreciate any advice or suggestions from experienced developers on my structuring the project. There are quite a few features I’ve reserved for future updates, so I don’t want to mix everything up in this thread I’d like to keep the discussion focused purely on how to structure the project across multiple source and header files for better modularity and portability
 
Last edited:

panic mode

Joined Oct 10, 2011
4,947
and as previously stated that is a great idea. but keep at least in the back of your mind where this is supposed to lead to. and i strongly suggest getting familiar with SQL (even basics will be awesome, it is a powerful and easy to learn). then you can work on scaling things up...

think of a place that has many doors and a lot of traffic, like hotel:
entrances
parking lot (if secured)
guest rooms
dining area, dining supplies,
service areas (lobby, pool, meeting rooms, etc.)
maintenance areas (elevators, loundry, utilities rooms etc.)
and so on...

access rights can be managed in the central DB. each card reader simply forwards read code to central node and waits for result (access granted or not). each RFID reader will need to at least unlock the door and show status (green/red LED). optionally it may have more features, like panic or assistance button (to get attention of live person) and perhaps speaker/microphone if this is not already part of security/surveillance system. central system will need to manage things. this includes assigning id and description to each RFID, as well as managing tags (assigning what resources can be accessed, when etc.).
 

Thread Starter

Embededd

Joined Jun 4, 2025
145
and as previously stated that is a great idea. but keep at least in the back of your mind where this is supposed to lead to. and i strongly suggest getting familiar with SQL (even basics will be awesome, it is a powerful and easy to learn). then you can work on scaling things up...
You’re absolutely right about keeping the bigger picture in mind. I don’t think setting up the DB connection will be a big challenge for me, since I already have some idea about it. Earlier, I worked on a Raspberry Pi project where I was reading sensor data and storing it in an SQL database on my laptop, so I’ve got a basic understanding of how that part works.

For now, I just want to keep this thread focused on project structuring splitting the code into multiple files, improving modularity, and making the codebase more portable. That’s the area where I still feel I need more advice and guidance, and I’d really appreciate if someone could help me or some advice on how to approach it the right way.
 

nsaspook

Joined Aug 27, 2009
16,275
I wouldn't use the K22 processor family, upgrade to the Q84. Better interrupt handling, better 12-bit ADC with Context Switching extensions, Configurable Logic Cells (CLC) and CANFD. I'm currently using it as a DAQ frontend for a Linux based home assistant project.
https://ww1.microchip.com/downloads...Q84-Microcontroller-Data-Sheet-DS40002213.pdf

My current, in-progress project:
https://github.com/nsaspook/mqtt_comedi/tree/fixes
Q84 firmware source: https://github.com/nsaspook/mqtt_comedi/tree/fixes/Q84/bmc_slave.X

https://forum.allaboutcircuits.com/threads/home-assistant-devices-and-uses.206323/post-2000186

As far as making the code-base more portable, good in theory, poor in practice at the 8-bit controller level. Most of the working code will be I/O specific functions that should use the hardware capabilities to the fullest. Use structure to make data, transformation and outputs easy to understand, debug and read directly from the source code first, too much portable code adds abstractions that obscure details that important for correctness.
 
Last edited:

lichurbagan

Joined Jul 4, 2025
121
Hello Experts,

I’m currently working on an RFID-Based Door Access Control System ( hobby project ) that manages room access through RFID authentication, time-based shift validation, and event logging. The system will use PIC Microcontroller and interfaces with several peripherals:

System Components:

  1. PIC18F45K22 microcontroller Unit (MCU)
  2. EM-18 RFID Reader (UART communication)
  3. RTC DS1307 (I2C communication)
  4. EEPROM (I2C communication)
  5. 16x2 LCD Display
  6. Relay Module
  7. Buzzer
  8. Red and Green LEDs
  9. Push Buttons
  10. PC for data loading via UART

Operational Flow:

1. Initialization:
Upon star-tup, the MCU initializes all peripherals including UART, I2C, GPIO, LCD, RTC, EEPROM, RFID reader, relays, LEDs, buzzer, and buttons.

2. RFID Authentication:
When an RFID card is scanned using the EM-18 reader, the card ID is transmitted to the MCU via UART. The MCU checks the card ID against the stored employee data.

3. Shift Validation:
The current time is retrieved from the RTC DS1307 via I2C. The system compares this time with the shift timings assigned to the cardholder. If the card is valid and the time falls within the allowed shift, access is granted.

4. Access Control:
- The relay is activated to unlock the door.
- A green LED lights up and the buzzer sounds briefly.
- The LCD displays a success message.
- The entry event is logged to EEPROM with card ID, timestamp, and status.

5. Access Denial:
If the card is invalid or the time is outside the allowed shift:
- A red LED lights up and the buzzer sounds differently.
- The LCD displays a denial message.
- No access is granted and the event may be logged for auditing.

6. Exit Mechanism:
- The user presses a button to exit the room.
- The relay is activated again to unlock the door.
- The exit event is logged to EEPROM with timestamp and status.

7. System Health Monitoring:
- The system periodically checks the status of critical peripherals (RTC, EEPROM, RFID reader).
- If a fault is detected, the system logs the error and alerts the user via LEDs or buzzer.

8. Debugging Support:
- Debug messages are sent via UART or displayed on the LCD.
- These messages help developers trace issues during development and deployment.

but as the project grows, maintaining all the logic inside a single or few files is getting messy. I want to structure the project for better modularity, portability, and maintainability. Ideally, if I switch from PIC to STM32, or upgrade a peripheral (for example, use a different EEPROM), I shouldn’t need to rewrite my main application just swap or modify the driver.

My plan is to clearly separate the project into layers so that:

  • The application logic (RFID authentication, shift checking, logging, etc.) is isolated from hardware-specific code.
  • The drivers are written in a generic way so they can be reused across platforms.
  • The MCU-specific implementations (like register-level UART, I2C, GPIO) live in a dedicated platform layer.

The goal is to keep the application layer hardware-independent. For example, the app would simply call EEPROM_Write() without caring whether it’s talking to an AT24C02 or another chip that detail is handled by the driver, and the driver itself calls the MCU-specific functions from the platform layer.

Here’s the structure I’m planning to follow:

Code:
RFID_Access_System/
src/          → application logic (RFID, logging, display, etc.)
drivers/      → generic hardware drivers (UART, I2C, LCD, EEPROM, etc.)
platform/     → MCU-specific code (PIC, ATmega, STM32, etc.)
include/      → all header files
config/       → pin mappings, constants, and macros
utils/        → diagnostics, error codes
README.md
I’d really appreciate some expert feedback on the structure I’ve come up with.

Does this kind of layered approach make sense for a large-sized embedded project like this?

And are there any better practices or commonly used approach to achieve s modularity, portability, and maintainability in such systems?

Thanks for any advice or suggestions. I’d like to hear how others usually handle this kind of project organization.
Sounds good. Have you printed any prototype PCB yet?
 

Thread Starter

Embededd

Joined Jun 4, 2025
145
Sounds good. Have you printed any prototype PCB yet?
Not yet, my whole setup is still on a breadboard. The main reason is that I haven’t finalized all the components for the project yet, so keeping it on a breadboard makes it easy for me to add or remove parts as needed. Once everything is finalized and stable, I’ll definitely move on to designing the PCB

There are quite a few features I’ve reserved for future updates, so I don’t want to mix everything up in this thread I’d like to keep the discussion focused purely on how to structure the project
 

Thread Starter

Embededd

Joined Jun 4, 2025
145
I wouldn't use the K22 processor family, upgrade to the Q84.
Thanks for the suggestion! I’m currently using the K22 because it’s what I have on hand, and I’m trying to build the project with components that are available to me. My main focus right now is on learning embedded software design and development rather than perfecting the hardware. I will definitely plan to upgrade the MCU and other components in the next revision
 

Irving

Joined Jan 30, 2016
5,073
Some thoughts:
1. Are you wedded to the PIC for some reason? An RPi is an expensive overkill, IMHO, but something like an ESP32-S3 is ideal for this, with libraries for all the peripherals, most of which have standardised interfaces. This project is complex enough to get the basic logic working without giving yourself the task of writing drivers etc. No point in reinventing the wheel....

2. Ditch the EEPROM. Instead use a i2c or SPI connected SD Card. There are a few libraries around to implement TinyDB or similar but maybe not for the PIC. Having a DB with a structured interface removes a whole layer of pain, both for configuration data eg shift times, RFID mapping, etc., but also for the log file. And enough space not to have to worry about capacity, even a lowly 2Gb card gives room for a few 100,000 days of logging! Write, and have unit test harnesses for, your own application specific data entities store/search/retrieve/delete functions as needed. (If you don't know about the unit testing approach look it up; its the best way to ensure that every change you make hasn't broken something downstream - run a complete suite of automated tests every time before checking code back into the repo).

3. Set up a baseline hardware configuration with working peripherals, and a test harness for each to validate each individually in situ, and then a controller that runs those tests so you can quickly and easily validate your hardware every time you make a hardware change. Again, write, and have unit test harnesses for, your own application specific calls to hardware, including, for example, mapping door lock ID to i2c address/bit #.

4. Don't confuse the structure of the repository with the structure of the code. Try to avoid inline code if possible. Use state machine and/or parallel tasks to isolate functionality. This might seem like overkill initially but as complexity grows you'll be glad you did it that way. There's a great temptation with embedded code to have a huge loop of "if it's time, do this..." or "if this switch is closed, do this", which rapidly becomes near impossible to test/debug.
 

Thread Starter

Embededd

Joined Jun 4, 2025
145
1. Are you wedded to the PIC for some reason? An RPi is an expensive overkill, IMHO, but something like an ESP32-S3 is ideal for this, with libraries for all the peripherals, most of which have standardised interfaces. This project is complex enough to get the basic logic working without giving yourself the task of writing drivers etc. No point in reinventing the wheel
I don’t really want to rely on ready-made libraries my goal is to write my own from scratch. I’ve already developed and tested common libraries for LCD, I2C, UART, RFID, etc., which I’ve shared in my other thread. This project is mainly about strengthening my understanding of embedded systems by building and integrating each module myself, rather than relying on existing libraries.

2. Ditch the EEPROM. Instead use a i2c or SPI connected SD Card. There are a few libraries around to implement TinyDB or similar but maybe not for the PIC. Having a DB with a structured interface removes a whole layer of pain, both for configuration data eg shift times, RFID mapping, etc., but also for the log file. And enough space not to have to worry about capacity, even a lowly 2Gb card gives room for a few 100,000 days of logging!
As I mentioned earlier, I’m currently working with the components I already have available, which is why I’m using the EEPROM for now. But I really like your suggestion about using an SD card that would definitely make data handling and logging much easier. I’ll plan to include that in my next hardware setup

Don't confuse the structure of the repository with the structure of the code.
I understand the difference between the structure of the repository and the structure of the code. In this discussion, I’m specifically talking about the code structure the main focus is to make the code modular and portable so that changes in peripherals or even the MCU don’t require major modifications.

The main goal of this project is to gain hands-on experience with low-level embedded development and to learn how to design modular and portable code that can be reused across different peripherals or even different MCUs. That’s why I’m using a bare-metal microcontroller instead of something like an RPi or ESP32. Those platforms are great for rapid development, but they abstract away many hardware details, which makes them less suitable for learning how to build things from the ground up
 

nsaspook

Joined Aug 27, 2009
16,275
I think he wants to reinvent the wheel as a learning experience at low-level integration. That's a good thing that helps to understand how things work at a higher level.
I know I could just use the ESP32 directly instead of a separate MCU, but I’ve intentionally avoided that. My goal here is to build a strong foundation in low-level embedded software development working closely with registers, drivers, and MCU-level design decisions.
Bravo.
 

lichurbagan

Joined Jul 4, 2025
121
Not yet, my whole setup is still on a breadboard. The main reason is that I haven’t finalized all the components for the project yet, so keeping it on a breadboard makes it easy for me to add or remove parts as needed. Once everything is finalized and stable, I’ll definitely move on to designing the PCB
Good plan. What did you use for coding? MPLAB or something else?
 

Thread Starter

Embededd

Joined Jun 4, 2025
145
Good plan. What did you use for coding? MPLAB or something else?
I initially used an ATmega8A with Atmel Studio for development. you can refer to this Thread for more details. Later, I decided to switch from the ATmega to a PIC microcontroller because I needed two UARTs, and I didn’t have a hardware debugger for the ATmega. For the PIC, I do have a debugger, which allows me to debug and test things.
 
Last edited:

Thread Starter

Embededd

Joined Jun 4, 2025
145
I think he wants to reinvent the wheel as a learning experience at low-level integration. That's a good thing that helps to understand how things work at a higher level.
Yes, exactly the reason is that when you develop any product, it should be modular and portable. I wanted to learn how to write code that’s truly modular and portable so that it can run on different hardware with minimal changes, without needing to rewrite everything from scratch. That’s why I started working with the hardware setup I already have.
 

Thread Starter

Embededd

Joined Jun 4, 2025
145
I’d like to get everyone’s thoughts on whether the following structure makes sense for a modular and portable project design or if something could be improved.
Code:
RFID_Access_System/
src/          → application logic (RFID, logging, display, etc.)
drivers/      → generic hardware drivers (UART, I2C, LCD, EEPROM, etc.)
platform/     → MCU-specific code (PIC, ATmega, STM32, etc.)
include/      → all header files
config/       → pin mappings, constants, and macros
utils/        → diagnostics, error codes
README.md
 

nsaspook

Joined Aug 27, 2009
16,275
Yes, exactly the reason is that when you develop any product, it should be modular and portable. I wanted to learn how to write code that’s truly modular and portable so that it can run on different hardware with minimal changes, without needing to rewrite everything from scratch. That’s why I started working with the hardware setup I already have.
It's really hard to totally isolate hardware dependencies at the low-level API if you also want to optimize the hardware resources of the controller. The Linux protocol and device drivers are a good examples.

With a typical 8-bit super-loop FSM you need to know when to use callbacks for data processing. From my MX (FM80/60 interface) this an example where there is no library of code to handle an odd-ball hardware interface.
C:
/*
* process received data from the FM80 9n1 serial in abuf 16-bit buffer array with callbacks
*/
static void rec_mx_cmd(void (* DataHandler)(void), const uint8_t rec_len)
{
    static uint16_t online_count = 0;

    if (FM_rx_ready()) {
        if (FM_rx_count() >= rec_len) {
            online_count = 0;
            if (rec_len == REC_LOG_LEN) {
                FM_rx(cbuf);
            } else {
                FM_rx(abuf);
            }
            BM.FM80_io = false;
            DataHandler(); // execute callback to process data in abuf
        } else {
            if (online_count++ > ONLINE_TIMEOUT) {
                online_count = 0;
                BM.FM80_online = false;
                BM.FM80_io = false;
                cc_mode = STATUS_LAST;
                state = state_init;
            }
        }
    }
    if ((BM.FM80_online == false) && online_count++ > ONLINE_TIMEOUT) {
        online_count = 0;
        BM.FM80_online = false;
        BM.FM80_io = false;
        cc_mode = STATUS_LAST;
        state = state_watts;
        mx_code = 0x0;
        DataHandler();
    }
}

/*
* transmit the cmd data
*/
static void send_mx_cmd(const uint16_t * cmd)
{
    if (FM_tx_empty()) {
        if (BM.pacing++ > PACE) {
            FM_tx(cmd, CMD_LEN); // send 9-bit command data stream
            BM.pacing = 0;
        }
    }
}

/*
* serial I/O ISR, TMR6 500us I/O sample rate
* polls the required UART registers for 9-bit send and receive into 16-bit arrays
*/
void FM_io(void)
{
    IO_RF4_SetHigh();
    if (pace++ > BUFFER_SPACING) {
        if (dcount-- > 0) {
            if (tbuf[dstart] > 0xff) { // Check for bit-9
                U2P1L = (uint8_t) tbuf[dstart]; // send with bit-9 high, start of packet
            } else {
                UART2_Write((uint8_t) tbuf[dstart]); // send with bit-9 low
            }
            dstart++;
        } else {
            dstart = 0;
            dcount = 0;
        }
        pace = 0;
    }

    /*
     * handle framing errors
     */
    if (U2ERRIRbits.RXFOIF) {
        rbuf[0] = U2RXB; // read bad data to clear error
        U2ERRIRbits.RXFOIF = 0;
        rdstart = 0; // reset buffer to start
    }

    /*
     * read serial data if polled interrupt flag is set
     */
    if (PIR8bits.U2RXIF) {
        if (U2ERRIRbits.FERIF) {
            // do nothing, will clear auto
        }

        if (rdstart > FM_BUFFER - 1) { // overload buffer index
            rdstart = 0; // reset buffer to start
        }
        if (U2ERRIRbits.PERIF) {
            rdstart = 0; // restart receive buffer when we see a 9-th bit high
            rbuf[rdstart] = 0x0100; // start of packet, bit 9 set
        } else {
            rbuf[rdstart] = 0x00;
        }
        rbuf[rdstart] += U2RXB;
        rdstart++;
    }

    timer_ms_tick(0, 0); // software timers update
    IO_RF4_SetLow();
}
FM_io handles the UART low-level details that are processor dependent using a timer ISR.

In the main loop state machine for the FM80
C:
            /*
             * FM80 processing state machine
             */
            TP1_SetLow();
            switch (state) {
            case state_init:
                send_mx_cmd(cmd_id);
                rec_mx_cmd(state_init_cb, REC_LEN);
                break;
            case state_status:
                if (!BM.fm80_restart) {
                    send_mx_cmd(cmd_status);
                    rec_mx_cmd(state_status_cb, REC_LEN);
                } else {
                    send_mx_cmd(cmd_restart);
                    rec_mx_cmd(state_restart_cb, REC_LEN);
                }
                break;
            case state_panel:
                send_mx_cmd(cmd_panelv);
                rec_mx_cmd(state_panelv_cb, REC_LEN);
                break;
            case state_batteryv:
                send_mx_cmd(cmd_batteryv);
                rec_mx_cmd(state_batteryv_cb, REC_LEN);
                break;
            case state_batterya:
                send_mx_cmd(cmd_batterya);
                rec_mx_cmd(state_batterya_cb, REC_LEN);
                break;
            case state_watts:
                send_mx_cmd(cmd_watts);
                rec_mx_cmd(state_watts_cb, REC_LEN);
                break;
            case state_mx_status: // wait for ten second flag in this state for logging
                send_mx_cmd(cmd_mx_status);
                rec_mx_cmd(state_mx_status_cb, REC_STATUS_LEN);
                break;
            case state_fwrev:
                switch (fw_state) {
                case 0:
                    send_mx_cmd(cmd_fwreva);
                    rec_mx_cmd(state_fwrev_cb, REC_LEN);
                    break;
                case 1:
                    send_mx_cmd(cmd_fwrevb);
                    rec_mx_cmd(state_fwrev_cb, REC_LEN);
                    break;
                case 2:
                    send_mx_cmd(cmd_fwrevc);
                    rec_mx_cmd(state_fwrev_cb, REC_LEN);
                default:
                    fw_state = 0;
                    break;
                }
                break;
            case state_mx_log: // FM80 log data
                send_mx_cmd(cmd_mx_log);
                rec_mx_cmd(state_mx_log_cb, REC_LOG_LEN);
                break;
            case state_time: // FM80 send time data
                send_mx_cmd(cmd_time);
                rec_mx_cmd(state_time_cb, REC_LEN);
                break;
            case state_date: // FM80 send date data
                send_mx_cmd(cmd_date);
                rec_mx_cmd(state_date_cb, REC_LEN);
                break;
            case state_misc:
                send_mx_cmd(cmd_misc);
                rec_mx_cmd(state_misc_cb, REC_LEN);
                break;
            default:
                send_mx_cmd(cmd_id);
                rec_mx_cmd(state_init_cb, REC_LEN);
                break;
            }
            TP1_SetHigh();
We then loop the FM80 FSM to set and get the date we need for higher level processing in a set of callbacks that advance the FSM to the next stage.
C:
void state_watts_cb(void)
{
#ifdef debug_data
    printf("%5d: %3x %3x %3x %3x %3x   DATA: Panel Watts %iW\r\n", rx_count++, abuf[0], abuf[1], abuf[2], abuf[3], abuf[4], (abuf[2] + (abuf[1] << 8)));
#endif
    panel_watts = (abuf[2] + (abuf[1] << 8));
    if (BM.FM80_online) {
        state = state_mx_log; // only get log data once state_init_cb has run
    } else {
        state = state_mx_status;
    }
}
That device specific data is then converted to standard CSV data stream buffer for usage in the next processor in the data chain.
C:
    const char log_format1[] = "^,%d,%3.2f,%3.2f,%3.2f,%3.2f,%3.2f,%3.2f,%3.2f,%3.2f,%d.%01d,%d.%01d,%d.%01d,%d,%d,%d,%d,%d,%llu,%7.4f,%7.4f,%6.4f,%7.4f,1957,~EOT                                                  \r\n";
#define LOG_VARS1    BMC4.d_id,((float) em.vl3l1) / 10.0f,((float) em.al1) / 1000.0f, ((float) em.wl1) / 10.0f, ((float) em.wl2) / 10.0f, \
    ((float) em.val1) / 10.0f, ((float) em.varl1) / 10.0f,  ((float) em.pfsys) / 10.0f, ((float) emt.hz) / 1000.0f,vw, vf, pvw, pvf, bat_amp_whole - 128, \
    bat_amp_frac - 128, bat_amp_panel - 128, panel_watts, BM.FM80_online, cc_mode, C.data_ok,BM.node_id, ha_daq_calib.scaler4, \
    ha_daq_calib.scaler5, ha_daq_calib.A200_Z, ha_daq_calib.A200_S

/*
* format data to the proper CSV format selected by the d_id variable
*/
void bmc_logger(void)
{
    static uint8_t d_id = DC1_CMD;

    bmc_newtime = localtime((void *) &V.utc_ticks);
    snprintf((char*) buffer, 25, "%s", asctime(bmc_newtime)); // the log_buffer uses this string in LOG_VARS
    buffer[DTG_LEN] = 0; // remove newline
    switch (d_id) {
    case STX:
    case DC1_CMD:
        BMC4.d_id = d_id;
        snprintf((char*) log_buffer, MAX_B_BUF, log_format1, LOG_VARS1);
        d_id = DC2_CMD;
        break;
    case DC2_CMD:
        BMC4.d_id = d_id;
        snprintf((char*) log_buffer, MAX_B_BUF, log_format2, LOG_VARS2);
        d_id = DC_NEXT;
        break;
    case DC3_CMD:
        BMC4.d_id = d_id;
        snprintf((char*) log_buffer, MAX_B_BUF, log_format3, LOG_VARS3);
        d_id = DC4_CMD;
        break;
    case DC4_CMD:
        BMC4.d_id = d_id;
        snprintf((char*) log_buffer, MAX_B_BUF, log_format4, LOG_VARS4);
        d_id = DC1_CMD;
        break;
    default:
        d_id = DC1_CMD;
        BMC4.d_id = d_id;
        snprintf((char*) log_buffer, MAX_B_BUF, log_format1, LOG_VARS1);
        break;
    }
Finally the SPI interrupt ISR handles the actual data transfers to the Linux protocol driver that then uses the SPI device driver for physical layer I/O. Processing happens in the 8-bit link ISR because the input data stream modifies the output data stream dynamically byte by byte as they are received by the SPI slave.
 
Last edited:

Thread Starter

Embededd

Joined Jun 4, 2025
145
It's really hard to totally isolate hardware dependencies at the low-level API if you also want to optimize the hardware resources of the controller. The Linux protocol and device drivers are a good examples.
If at some point there’s a need to upgrade your project. say, moving from a PIC to an ARM MCU. Do you have a plan for that, or would you end up rewriting the entire code from scratch?

And please don’t say that situation will never come. I’m just asking hypothetically to understand your approach in such a case
 

Ya’akov

Joined Jan 27, 2019
10,226
Hello Experts,

I’m currently working on an RFID-Based Door Access Control System ( hobby project ) that manages room access through RFID authentication, time-based shift validation, and event logging. The system will use PIC Microcontroller and interfaces with several peripherals:

System Components:

  1. PIC18F45K22 microcontroller Unit (MCU)
  2. EM-18 RFID Reader (UART communication)
  3. RTC DS1307 (I2C communication)
  4. EEPROM (I2C communication)
  5. 16x2 LCD Display
  6. Relay Module
  7. Buzzer
  8. Red and Green LEDs
  9. Push Buttons
  10. PC for data loading via UART

Operational Flow:

1. Initialization:
Upon star-tup, the MCU initializes all peripherals including UART, I2C, GPIO, LCD, RTC, EEPROM, RFID reader, relays, LEDs, buzzer, and buttons.

2. RFID Authentication:
When an RFID card is scanned using the EM-18 reader, the card ID is transmitted to the MCU via UART. The MCU checks the card ID against the stored employee data.

3. Shift Validation:
The current time is retrieved from the RTC DS1307 via I2C. The system compares this time with the shift timings assigned to the cardholder. If the card is valid and the time falls within the allowed shift, access is granted.

4. Access Control:
- The relay is activated to unlock the door.
- A green LED lights up and the buzzer sounds briefly.
- The LCD displays a success message.
- The entry event is logged to EEPROM with card ID, timestamp, and status.

5. Access Denial:
If the card is invalid or the time is outside the allowed shift:
- A red LED lights up and the buzzer sounds differently.
- The LCD displays a denial message.
- No access is granted and the event may be logged for auditing.

6. Exit Mechanism:
- The user presses a button to exit the room.
- The relay is activated again to unlock the door.
- The exit event is logged to EEPROM with timestamp and status.

7. System Health Monitoring:
- The system periodically checks the status of critical peripherals (RTC, EEPROM, RFID reader).
- If a fault is detected, the system logs the error and alerts the user via LEDs or buzzer.

8. Debugging Support:
- Debug messages are sent via UART or displayed on the LCD.
- These messages help developers trace issues during development and deployment.

but as the project grows, maintaining all the logic inside a single or few files is getting messy. I want to structure the project for better modularity, portability, and maintainability. Ideally, if I switch from PIC to STM32, or upgrade a peripheral (for example, use a different EEPROM), I shouldn’t need to rewrite my main application just swap or modify the driver.

My plan is to clearly separate the project into layers so that:

  • The application logic (RFID authentication, shift checking, logging, etc.) is isolated from hardware-specific code.
  • The drivers are written in a generic way so they can be reused across platforms.
  • The MCU-specific implementations (like register-level UART, I2C, GPIO) live in a dedicated platform layer.

The goal is to keep the application layer hardware-independent. For example, the app would simply call EEPROM_Write() without caring whether it’s talking to an AT24C02 or another chip that detail is handled by the driver, and the driver itself calls the MCU-specific functions from the platform layer.

Here’s the structure I’m planning to follow:

Code:
RFID_Access_System/
src/          → application logic (RFID, logging, display, etc.)
drivers/      → generic hardware drivers (UART, I2C, LCD, EEPROM, etc.)
platform/     → MCU-specific code (PIC, ATmega, STM32, etc.)
include/      → all header files
config/       → pin mappings, constants, and macros
utils/        → diagnostics, error codes
README.md
I’d really appreciate some expert feedback on the structure I’ve come up with.

Does this kind of layered approach make sense for a large-sized embedded project like this?

And are there any better practices or commonly used approach to achieve s modularity, portability, and maintainability in such systems?

Thanks for any advice or suggestions. I’d like to hear how others usually handle this kind of project organization.
You are too focused on the hardware if you intend to learn how to write good software as well.

An access control system must be a security first project. That is, you must operate from principles in the design. What you are doing is letting the hardware drive development. You hardware should only be considered when writing drivers or what will act as an abstraction layer so that your program's logic is unaffected by a the hardware choices of implementation.

Several other people have mentioned a database, and a central server. These are important things. You should research "triple A" (AAA or Authentication, Authorization, and Accounting). This is the well understood basis for secure access control. It is paired up with things like LDAP for user records and RADIUS for authorization.

Even if you don't want to implement a proper back end, you should still abstract the functions in your program so that it is possible to simply plug in new back end services as required.

Structure your program so that it respects the AAA principles with discrete, service independent calls to functions like getCredentials(), Authenticate(), Authorize(), and updateAccounting(). Those functions should use drivers that provide hardware/service independent drivers that return standardized values such as tokenID, tokenType, userPIN, userID, authenticateResult, authorizeResult, &c.

You should also include an encryption mechanism for all traffic to the AAA stack, even if it is just a place holder, because you will need one.

You don't have to do all of the right pieces at first, but you have to have some idea what they are and accommodate them in your architecture—if you want to do this right.
 

lichurbagan

Joined Jul 4, 2025
121
I initially used an ATmega8A with Atmel Studio for development. you can refer to this Thread for more details. Later, I decided to switch from the ATmega to a PIC microcontroller because I needed two UARTs, and I didn’t have a hardware debugger for the ATmega. For the PIC, I do have a debugger, which allows me to debug and test things.
I see. Did you face any problem in sourcing ATmega or PIC chips?
 
Top