3-Thread Air-Quality Monitor

Created at 2026-06-22 16:31:54

I built a small air-quality alert node on RT-Thread and wanted to share how I split it across threads, since the threading model is what made it clean. The idea is simple: read a gas sensor continuously, raise a red LED and log a warning when the reading crosses a threshold, and keep a heartbeat LED blinking so I always know the node is alive. Three jobs that should not block each other, which is exactly what threads are for.

The sensor is an analog MQ-style gas sensor wired to an ADC channel. The board has two LEDs — green for "alive", red for "alarm". Instead of polling everything in one loop, I gave each job its own thread and used a semaphore to signal between the sensor and the alarm.

How it works

I used 3 threads:

1 Sensor thread — reads the ADC every 500 ms, stores the latest value, and releases a semaphore when the reading goes above the threshold.
2 Alarm thread — blocks on that semaphore. It does nothing until the sensor signals, then turns on the red LED, prints the value, and clears after a couple of seconds.
3 Status thread — just blinks the green LED at 1 Hz as a heartbeat, completely independent of the rest.

The semaphore matters here. The alarm thread is not busy-waiting or polling a flag — it sleeps until there is actually something to do, so it costs nothing when air is clean. I also gave the alarm thread a higher priority than the sensor thread (lower number in RT-Thread), so an alarm preempts the next sensor read instead of waiting in line.

The code

#include <rtthread.h>
#include <rtdevice.h>

#define GREEN_LED       GET_PIN(F, 9)
#define RED_LED         GET_PIN(F, 10)
#define GAS_THRESHOLD   600

static rt_sem_t          alarm_sem;
static rt_adc_device_t   adc_dev;
static volatile rt_uint32_t gas_level = 0;

/* Thread 1: read the gas sensor, signal on threshold */
static void sensor_thread_entry(void *parameter)
{
    rt_adc_enable(adc_dev, 0);
    while (1)
    {
        gas_level = rt_adc_read(adc_dev, 0);
        if (gas_level > GAS_THRESHOLD)
            rt_sem_release(alarm_sem);
        rt_thread_mdelay(500);
    }
}

/* Thread 2: wait for the alarm, drive the red LED */
static void alarm_thread_entry(void *parameter)
{
    while (1)
    {
        rt_sem_take(alarm_sem, RT_WAITING_FOREVER);
        rt_pin_write(RED_LED, PIN_HIGH);
        rt_kprintf("ALARM: gas level %d\n", gas_level);
        rt_thread_mdelay(2000);
        rt_pin_write(RED_LED, PIN_LOW);
    }
}

/* Thread 3: heartbeat so I know the node is alive */
static void status_thread_entry(void *parameter)
{
    while (1)
    {
        rt_pin_write(GREEN_LED, PIN_HIGH);
        rt_thread_mdelay(500);
        rt_pin_write(GREEN_LED, PIN_LOW);
        rt_thread_mdelay(500);
    }
}

int gas_monitor_init(void)
{
    rt_pin_mode(GREEN_LED, PIN_MODE_OUTPUT);
    rt_pin_mode(RED_LED,   PIN_MODE_OUTPUT);

    adc_dev   = (rt_adc_device_t)rt_device_find("adc1");
    alarm_sem = rt_sem_create("alarm", 0, RT_IPC_FLAG_PRIO);

    rt_thread_t t_sensor = rt_thread_create("sensor", sensor_thread_entry, RT_NULL, 1024, 12, 10);
    rt_thread_t t_alarm  = rt_thread_create("alarm",  alarm_thread_entry,  RT_NULL, 1024, 11, 10);
    rt_thread_t t_status = rt_thread_create("status", status_thread_entry, RT_NULL, 512,  20, 10);

    rt_thread_startup(t_sensor);
    rt_thread_startup(t_alarm);
    rt_thread_startup(t_status);
    return RT_EOK;
}
MSH_CMD_EXPORT(gas_monitor_init, start the gas monitor);

Calling gas_monitor_init from the msh shell starts all three threads. The green LED starts blinking immediately, and breathing on the sensor (or a lighter near it, carefully) pushes the reading over the threshold and trips the red LED.

Notes

  • The threshold is a hard-coded constant for now. Next step is reading it from a config so I do not reflash to retune.
  • A message queue would be better than a bare semaphore if I want to pass the actual reading to the alarm thread instead of reading the global. For one value the semaphore is enough.
  • Everything runs concurrently, so the heartbeat never stutters even while an alarm is active.

My experience

This was a small project but a good one for getting the RTOS habits right — letting threads block instead of polling, and using priorities deliberately. I built it in RT-Thread Studio, which made the BSP and ADC setup painless.

One honest note on workflow: I drafted the thread scaffolding and the init function with ai augmented development tooling, which saved time on the boilerplate. It got the structure right but invented a couple of pin and ADC calls that do not exist, so I checked everything against the RT-Thread docs before it ran. Useful for the repetitive parts, not a substitute for reading the API.

Curious how others structure this kind of node — would you use a message queue from the start, or keep the semaphore until you actually need to pass data?

更多

Follower
0
Views
13
0 Answer
There is no answer, come and add the answer

Write Your Answer

Log in to publish your answer,Click here to log in.

Create
Post

Share