A fully interrupt-driven embedded system that detects products on a moving conveyor, classifies them by color, and sorts them into the correct bin — built on an ARM Cortex-M33 microcontroller.
Platform: EFM32PG28 (ARM Cortex-M33) · BRD2506A dev board · Language: C (bare-metal) · SDK: Simplicity SDK / emlib
A product travels along the conveyor belt. An IR sensor detects and counts it, a color sensor reads its color, and — based on that color — one of two servo arms pushes it into the matching bin. An on-board LCD cycles through live counters, two push-buttons control the belt, and the whole system can be monitored and controlled from a phone over Bluetooth.
The defining constraint of the project: every action is interrupt-driven. There are no busy-wait loops and no delay loops anywhere in the firmware. The CPU sleeps in low-power EM1 and wakes only when an event actually happens.
- 100% event-driven architecture — GPIO, timer, and USART interrupts only. The main loop does nothing but dispatch flags and return to sleep (
EMU_EnterEM1()). - Software PWM generated from timer interrupts — ~1 kHz for the DC motor and 50 Hz for two servos, implemented via overflow/compare ISRs after the hardware PWM path proved unusable on this board.
- Frequency-based color sensing — the TCS3200's output frequency is counted inside a 30 ms gate, cycling through red/green/blue filters, then classified.
- Hardware debounce — one-shot timers filter mechanical button bounce and electrical noise on the IR line, preventing double counts.
- Interrupt-driven UART (both directions) — RX maps incoming characters to motor commands; TX feeds one byte per
TXBLinterrupt, so the CPU never spins waiting on the transmitter. - Modular firmware — one
.c/.hmodule per subsystem, with the application logic inapp_process_action()rather thanmain().
- Detection — the product breaks the IR beam, raising a GPIO edge interrupt. A 30 ms one-shot debounce timer starts.
- Confirmation — when the timer fires, the firmware samples the stable pin state. A new arrival triggers a color read; a departure increments the counters exactly once.
- Classification — the color sensor runs continuously in the background, sweeping R→G→B filters and updating the current color label.
- Sorting —
servo_sort()selects the arm and angle for that color, holds the arm out for ~4 seconds, then returns to rest once the product has passed. - Reporting — every 2 seconds the LCD advances to the next counter screen and a status line is transmitted over Bluetooth.
The dominant color is accepted only if it is both strong (above a minimum frequency threshold) and distinct (at least 1.2× the runner-up). Anything ambiguous is deliberately classified as INVALID and diverted to a separate bin, rather than guessed.
| Component | Part | Role |
|---|---|---|
| MCU | EFM32PG28 (Cortex-M33), BRD2506A | System controller |
| Motor driver | L293D H-bridge | Conveyor belt drive |
| DC motor | — | Belt movement |
| IR sensor | TCRT5000 | Product detection & counting |
| Color sensor | TCS3200 | Red / green / blue classification |
| Servos | 2 × MG90S | Sorting arms |
| Display | On-board segment LCD | Live counters & speed |
| Bluetooth | HC-06 | Wireless monitoring & control |
| Buttons | PB0 / PB1 | Direction & speed control |
| Signal | Pin | Notes |
|---|---|---|
| Motor PWM (EN) | PD7 | Software PWM, ~1 kHz |
| Motor IN1 / IN2 | PD14 / PD15 | Direction via H-bridge |
| Servo 1 (blue/red arm) | PD6 | TIMER3 CC0 |
| Servo 2 (invalid arm) | PD8 | TIMER3 CC1 |
| IR sensor | PD12 | Both-edge interrupt |
| Color OUT | PD11 | Rising-edge counting |
| Color S2 / S3 | PD9 / PD10 | Filter select |
| Buttons | PB1 / PB6 | Active-low, pull-up |
| UART TX / RX | PA12 / PA3 | 9600 8-N-1 |
| Peripheral | Purpose | Configuration |
|---|---|---|
LETIMER0 |
LCD refresh & status tick | 2 s, LFXO 32768 Hz |
TIMER0 |
Motor PWM | ~1 kHz, prescale /1 |
TIMER1 |
Button debounce | 40 ms one-shot |
TIMER2 |
Color measurement gate | 30 ms window |
TIMER3 |
Servo PWM | 50 Hz, CC0 + CC1 |
TIMER4 |
IR debounce | 30 ms one-shot |
USART0 |
Bluetooth link | 9600 baud, RX + TXBL interrupts |
src/
├── app.c / app.h Application layer: app_init() + app_process_action()
├── lcd.c Segment LCD display & 2 s cyclic refresh
├── motor.c DC motor: software PWM + direction control
├── buttons.c Push-button handling, debounce, shared command API
├── ir.c IR detection, debounce, product counting
├── color.c TCS3200 frequency measurement & classification
├── servo.c Dual-servo 50 Hz PWM & sorting logic
└── uart.c Interrupt-driven Bluetooth UART (RX + TX)
main() only calls app_init() and then loops on app_process_action(). All timing-critical work happens in ISRs; the loop handles deferred rendering and transmission, then sleeps.
Software PWM. The hardware compare/PWM output was dead on the original pin (verified flat on a scope). Rather than work around it in a fragile way, PWM was reimplemented in software: a timer overflow ISR toggles the pin and reloads the next TOP value, splitting each period into HIGH and LOW segments. The motor runs at ~1 kHz; the servos use a 50 Hz frame with 0.5–2.5 ms pulses mapped linearly to 0–180°.
Timer values derived, not guessed. Every prescaler and TOP value was calculated from the clock tree and then verified against the actual registers in the debugger. For example, the servo frame: 39 MHz ÷ 16 = 2,437,500 Hz, and 2,437,500 ÷ 50 Hz = 48,750 ticks per 20 ms frame.
UART frame verified on the scope. Incoming Bluetooth commands were captured and decoded bit by bit to confirm correct framing and baud rate.
Interrupt-driven transmit. uart_send_status() builds the status line into a buffer and enables the TX-buffer-empty (TXBL) interrupt. Each interrupt feeds exactly one byte, so the CPU is free (and can sleep) between characters — no blocking while loop on the transmitter.
Three problems shaped the final design, and each one taught more than the working parts did:
Dead PWM output. The initial hardware PWM produced a flat line instead of a square wave. Tracing it pin by pin on the oscilloscope — rather than re-reading the code — located the problem and led to the software PWM implementation. Lesson: measure the signal before trusting the abstraction.
Double product counts. Electrical noise, mostly from the servos, generated spurious edges on the IR line. Adding a hardware input filter plus a debounce window, and counting only on a confirmed present→absent transition, made the count reliable.
Bluetooth dropping out mid-sort. Every servo actuation caused a supply dip that browned out the HC-06. The fix was electrical rather than firmware: separating the motor/servo power rail from the logic rail in a star topology and adding bulk and decoupling capacitors.
- Install Simplicity Studio 5 with the Simplicity SDK.
- Create an Empty C Project targeting the BRD2506A / EFM32PG28 board.
- Copy the contents of
src/into the project's source directory. - Build and flash to the board via the on-board debugger.
Wire the peripherals according to the pin map above. Power the motor and servos from a separate supply rail from the logic and Bluetooth module, with a common ground — this is required for stable operation.
| Character | Action |
|---|---|
R |
Run belt right |
L |
Run belt left |
S |
Stop / resume |
The board transmits a periodic status line containing all counters, current speed, and direction.
Mofeed Hamdan — Electrical & Electronics Engineering student, Braude Academic College Built as the final project for the Microcontrollers course (31226).



