This project is a room-monitoring device built on an STM32F103C8 "Blue Pill" and simulated in Wokwi. It measures temperature, humidity, ambient light and motion. It shows one reading at a time on an OLED, sounds a buzzer when the room is too cold or too hot, and turns the display off when nobody is in the room.
It was built with PlatformIO, the STM32Cube HAL and native FreeRTOS APIs for BCA182 Embedded Systems Programming (Laboratory Activity No. 1) at MSU-IIT.
MotivationA room monitor has to do several things at different speeds: sample sensors every few seconds, react right away when a person moves or turns a knob, and raise an alarm when the temperature is out of range. Writing all of that in one big loop makes the timing hard to reason about. The goal of this project was to learn how an RTOS solves this: split the work into tasks, give each task a priority based on how fast it must respond, and make the tasks communicate safely.
Features- Temperature and humidity from a DHT22, every 2 seconds
- Relative ambient-light level (0-100 %) from a photoresistor through the ADC
- Motion detection with a PIR sensor
- SSD1306 OLED showing one selected reading
- Rotary encoder to switch pages: Temperature, Humidity, Light, Motion (both directions, with wraparound)
- Buzzer alarm below 18 °C or above 30 °C
- ACTIVE / INACTIVE state machine: after 15 s with no motion the OLED turns off, and motion wakes it up again
- 6 FreeRTOS tasks, 3 queues, 1 event group, 1 mutex, and vTaskDelayUntil() for periodic sampling
- 13 unit tests and static analysis with cppcheck
Pin connections:
- Photoresistor AO to PA0 (ADC1)
- DHT22 data to PA1
- Buzzer to PA2 (TIM2 PWM, 1 kHz)
- PIR out to PA3
- Encoder CLK / DT to PA4 (interrupt) / PA5
- Serial TX / RX to PA9 / PA10 (USART1, 115200)
- OLED SCL / SDA to PB6 / PB7 (I2C1)
Four inputs (DHT22, photoresistor, PIR and encoder) go into the STM32, which runs FreeRTOS with six tasks and drives three outputs: the OLED, the buzzer and a serial log.
The code is split into modules:
- Drivers talk to hardware (dht22.c, ldr.c, ssd1306.c, buzzer.c, log.c).
- Task files each own one job (sensors.c, display.c, input.c, alarm.c, motion.c, system_state.c).
- logic.c holds the decisions (alarm, page navigation, ACTIVE/INACTIVE) with no hardware code, so it can be unit-tested on a PC.
- main.c only initializes hardware and calls app_main(), which creates the tasks.
Tasks and priorities (a higher number is more urgent):
- MotionTask (priority 3, every 100 ms): reads the PIR and signals motion
- InputTask (priority 3, every 50 ms): turns encoder steps into a page change
- StateTask (priority 3, on motion or every 250 ms): runs the ACTIVE/INACTIVE state machine
- SensorTask (priority 2, every 2 s with vTaskDelayUntil): reads the DHT22 and photoresistor
- AlarmTask (priority 2, on each new reading): checks the temperature and drives the buzzer
- DisplayTask (priority 1, on new data or a page change): the only task that draws on the OLED
Why these priorities: tasks that react to people (motion, the encoder, the state change) get the highest priority, because a late response is noticeable. Sensor and alarm work run on a 2-second cycle, so a few milliseconds of delay does not matter. Drawing the OLED is the slowest job and the least urgent, so it has the lowest priority and never delays user input.
How the tasks communicate:
- Queues: SensorTask sends a SensorData struct (temperature, humidity, light, motion) to DisplayTask and AlarmTask through two queues. InputTask sends the selected page to DisplayTask through a third queue. Each queue holds only the newest value.
- Event group: bits for ACTIVE, MOTION, ALARM and PIR level. StateTask sets ACTIVE, and the display, input and alarm tasks read it.
- Mutex: protects the serial port, which all six tasks use for log messages, so two tasks never print over each other.
- Every 2 seconds, SensorTask reads the DHT22 and the photoresistor and sends the result through the queues. It uses vTaskDelayUntil() so readings stay exactly 2 s apart and do not drift.
- DisplayTask waits for new data and shows the selected page on the OLED.
- Turning the encoder triggers a short interrupt that only counts the steps. InputTask reads the count and changes the page.
- AlarmTask checks each new temperature. Below 18 °C or above 30 °C, the buzzer turns on and the OLED shows "! TEMP ALARM".
- MotionTask watches the PIR. After 15 seconds with no motion, StateTask switches to INACTIVE: the OLED turns off, the encoder is ignored and the buzzer is silenced. The next motion switches back to ACTIVE.
Unit tests: 13 tests run on the PC with "pio test -e native", and all pass. They check the alarm limits (including exactly 18 °C and exactly 30 °C), page navigation in both directions with wraparound, and all four ACTIVE/INACTIVE cases.
Static analysis: "pio check" (cppcheck) found 0 high and 0 medium issues. The low findings were reviewed and explained. Most were false positives, because the tool cannot see functions called through FreeRTOS or the interrupt table.
Functional tests in Wokwi: all 10 tests passed. They included changing temperature, humidity and light, turning the encoder both ways, the alarm turning on above 30 °C and off again, and the system going INACTIVE after 15 s and waking on motion.
Fault experiments: I also broke the system on purpose to see what would happen:
- Removing the delay from MotionTask made it take over the CPU, and every other task stopped running.
- Giving DisplayTask the highest priority caused no visible failure here, but it means a screen redraw could delay user input.
- Removing the mutex caused no mixed-up output in this simulator build, but printing would be unsafe with a standard port on real hardware, so the mutex stays.
The running system: the OLED shows the temperature with the alarm banner, and the serial log shows the task activity.
Challenges EncounteredThe biggest challenge was getting FreeRTOS to run in Wokwi at all. The firmware built and started, but no task ever ran. By printing FreeRTOS assert messages over the serial port, I found that the standard Cortex-M ports rely on processor features the Wokwi STM32F103 model does not fully support: interrupt priority bits, and the SVC/PendSV exceptions FreeRTOS uses to switch tasks.
The final build uses a Wokwi-compatible version of FreeRTOS adapted from a classmate's project (credited below). I fixed two more problems in it: it now blocks interrupts in a way that works in the simulator, and it saves each task's critical-section count separately, which stopped the system from freezing.
Other problems: the DHT22 failed every read until the data pin was switched to input mode with a pull-up, and the buzzer had to move to a different pin because its original pin was used by the RTOS timer.
Lessons Learned- A task that never blocks takes over the processor, and lower-priority tasks starve.
- A task's priority only matters when it is ready to run. Most of the time, tasks are blocked waiting.
- Queues are safer than global variables because the whole struct is copied at once.
- Keeping decision logic separate from hardware code makes unit testing easy.
- When something freezes silently, making it print where it failed is much faster than guessing.
- The FreeRTOS port used here only works in the Wokwi simulator. Real hardware should use the standard ARM_CM3 port.
- The light value is a relative percentage, not calibrated lux.
- The 15 s inactivity timeout is short on purpose for testing.
- The project has only been tested in simulation, not on a physical board.
- Test on a real Blue Pill with the standard FreeRTOS port
- Use task notifications from interrupts for the encoder and PIR
- Calibrate the light sensor
- Show more status on the OLED and use a beeping alarm pattern
Full source code, unit tests, static-analysis reports and the lab report:https://github.com/monxx-ie/BCA182-freetos-multisensor
References and Acknowledgments- BCA182 Laboratory Activity No. 1, Asst. Prof. Paul Rodolf P. Castor, MSU-IIT
- FreeRTOS Kernel (MIT license): https://github.com/FreeRTOS/FreeRTOS-Kernel
- Wokwi-compatible FreeRTOS port adapted from classmate Ni-ear: https://github.com/Ni-ear/bca182-freertos-multisensor
- STMicroelectronics STM32CubeF1 HAL and STM32CubeMX
- PlatformIO, the Unity test framework and cppcheck
- Wokwi simulator: https://wokwi.com
- Claude (Anthropic) was used as an assistant for debugging and documentation















Comments