A bare-metal embedded systems project built in C on the ATmega328P featuring a non-blocking state machine, real-time sensor monitoring, interrupt-driven architecture, and UART telemetry.
The Background
This project started as an exploration into low-level embedded systems programming and real-time firmware architecture. I wanted to move beyond high-level Arduino abstractions and understand how microcontrollers actually work at the register level.
The result was a standalone environmental monitoring system for houseplants built entirely in bare-metal C on the ATmega328P microcontroller.
The system continuously monitors soil moisture, ambient light, temperature, and humidity while displaying live data on a 16×2 LCD and streaming telemetry over UART.
The Goal
The objective was not only to build a useful monitoring device, but also to design a responsive embedded firmware architecture without relying on blocking delays or high-level frameworks.
I wanted the system to handle multiple sensors, interrupts, user input, LCD updates, and serial logging simultaneously while remaining responsive and modular.
The Stack
The project was built in C using PlatformIO targeting the ATmega328P running at 16 MHz on an Arduino Uno.
Sensor data was collected through analog ADC reads and a custom-written DHT11 single-wire communication driver implemented entirely through direct register manipulation.
The firmware uses Timer1 interrupts in CTC mode to coordinate system timing, while UART telemetry streams formatted sensor data at 9600 baud.
The LCD runs in 4-bit mode using direct PORTB communication to reduce GPIO usage and improve hardware efficiency.
The Architecture
The entire system follows a non-blocking, flag-driven architecture.
Instead of using delay-based programming, a Timer1 interrupt fires once per second and updates a set of volatile flags shared with the main loop.
The main loop continuously polls these flags and executes short tasks such as sensor reads, LCD updates, error handling, and UART logging.
This design keeps the system responsive even while juggling multiple interrupt sources, user inputs, and sensor updates simultaneously.
The Challenge
One of the hardest parts of the project was implementing reliable timing-sensitive communication with the DHT11 sensor.
The DHT11 uses a custom single-wire protocol where bits are encoded through pulse timing measured in microseconds. I implemented the protocol manually using direct AVR register access and carefully timed signal reads.
Another challenge was coordinating several interrupt sources cleanly without introducing blocking behavior or race conditions between the interrupt context and the main application loop.
Features
👾 Real-time monitoring of soil moisture, light, temperature, and humidity.
👾 Non-blocking firmware architecture using Timer1 interrupts and flag polling.
👾 Custom DHT11 bit-banged communication driver.
👾 UART telemetry logging at 9600 baud.
👾 HD44780 LCD operating in 4-bit mode.
👾 Multiple interrupt sources including Timer1, external interrupts, and pin-change interrupts.
👾 LED and buzzer based environmental alerts.
👾 Manual sensor selection through hardware buttons.
Hardware
The hardware stack includes an ATmega328P microcontroller, a DHT11 temperature and humidity sensor, a soil moisture probe, a photoresistor for ambient light detection, a 16×2 LCD, LEDs, push buttons, and a piezo buzzer.
All peripherals were connected directly through AVR GPIO and configured manually through register-level programming rather than Arduino libraries.
Results
The final system successfully monitored environmental conditions in real time while maintaining responsive performance through a fully non-blocking firmware architecture.
The Timer1-driven state machine allowed multiple sensors, interrupts, display updates, and UART communication to coexist without freezing the application loop.
Most importantly, this project became a deep introduction to embedded systems engineering, low-level firmware design, hardware communication protocols, and real-time software architecture.
What I Learned
This project taught me how embedded systems work beneath high-level frameworks. I gained hands-on experience with timers, interrupts, ADCs, UART communication, GPIO control, hardware protocols, and memory-safe firmware design.
It also reinforced the importance of architecture in embedded software. Small design choices — like using flags instead of delays or keeping ISRs lightweight — dramatically affect responsiveness, scalability, and reliability.
Building everything at the register level gave me a much deeper understanding of how microcontrollers operate internally and how software interacts directly with hardware.