FreeRTOS lets an STM32PriceAliExpress price application wait for work without keeping the processor in a polling loop. In this Blue PillPriceAliExpress price lab, one task produces a sample every 100 ms and another waits on a queue. Each time the consumer takes a sample it toggles the onboard LEDPrice. You can slow the consumer in the debugger and watch the queue fill, samples get dropped and queued items get older.
In this esp32AliExpress price.co.uk/ guide, you'll integrate the native FreeRTOS kernel with STM32CubeF1 HAL, allocate the tasks and queue at compile time, and check the Cortex-M3 interrupt handlers. The lab uses known synthetic samples so you can diagnose the scheduling and queue behaviour before connecting a sensor.
Quick answer. Start with an STM32F103C8 project using the 8 MHz HSI clock and PC13 as an output. Give FreeRTOS SysTick and move the HAL millisecond timebase to TIM2. Add the pinned FreeRTOS Cortex-M3 port, create a four-item static queue and let the consumer block on it. The producer waits until its next scheduled release and counts any sample it cannot enqueue.

What the FreeRTOS lab needs
Use an STM32F103C8 Blue Pill with a working SWD connection. The C8 gives this lab 64 KiB of Flash and 20 KiB of SRAM. You need a supported SWD probe and a project generated for the actual MCU. A debugger is enough to see the counters and change the service delay. A logic analyser is optional if you want to observe PC13 timing. Many Blue Pills have an active-low user LED on PC13. Board variants can differ, so verify the pin and polarity against your board. The code toggles the pin and remains observable in the debugger regardless of the LED’s polarity. STM32F103C8 Blue Pill pinout
| Component | Lab choice | What it does |
|---|---|---|
| MCU and port | STM32F103C8, GCC ARM_CM3 | Use the Cortex-M3 exception and stack conventions |
| Clock | 8 MHz HSI, AHB and both APB buses undivided | Keep the clock tree simple |
| Output | PC13, push-pull output | Toggle once per dequeued sample |
| Debug connection | Serial Wire | Inspect counters and FreeRTOS integration |
| Kernel | Native FreeRTOS Kernel V11.2.0 | Create tasks and queue directly |
| HAL timebase | TIM2 | Maintain HAL milliseconds independently |
| Kernel timebase | SysTick, 1,000 Hz | Schedule releases, delays and wakeups |
A task is a function with its own execution context and stack. The scheduler runs the highest-priority Ready task. A task that waits for a timer or an empty queue becomes Blocked, so another Ready task can run. The idle task runs when no application task is Ready. This makes the difference between sleeping and busy waiting visible in a small application. A task polling a flag in a tight loop can consume CPU time or starve a lower-priority task, even if the polling does very little work.
The lab has one queue owner on each side and one consumer for the LED. That separation gives you a clean place to add a sensor later. A control task can own the sensor or measurement schedule and send a small value by copy. A second task can own a peripheral or file handle and choose how it handles bursts. In a real system, define what each task owns and how much time it can spend waiting before choosing its priority.
Generate the STM32CubeMX project
Select STM32F103C8Tx and keep Serial Wire debugging. Configure the 8 MHz HSI as SYSCLK with AHB, APB1 and APB2 divided by 1. Configure PC13 as a low-speed GPIO output with initial level High if the board has the typical active-low LED. The project uses HSI so the setup is independent of the board’s external oscillator. Check that the clock tree and the generated SystemCoreClock update agree. The STM32CubeMX and CubeIDE setup guide covers generation and SWD debugging.
Select TIM2 as the HAL timebase in the system configuration. Menu labels can vary with the CubeMX release. The generated project should contain a timer-based HAL timebase source that overrides the HAL’s weak default HAL_InitTick(). Keep its TIM2 IRQ and the path from the timer update callback to HAL_IncTick(). Use TIM2 tick pre-emption priority 0 for the configuration shown here, and verify the value applied by the generated code. Some project layouts place the callback in main.c. If you add your own timer work to that callback later, preserve the TIM2 branch. Leave TIM2 reserved for this job.
Leave the generated FreeRTOS middleware disabled for this project. The lab adds the native kernel separately and uses one copy of its headers, source, port and configuration. A project with existing CMSIS-RTOS wrappers can still use equivalent task and queue ideas, but the wrapper project has its own integration path and startup call. For this lab, keep the Cube HAL and device startup generated for the board and add the native API files described below.
Keep the HAL tick and the kernel tick separate
HAL timeouts use the value returned by HAL_GetTick(). The kernel tracks delays and timeouts in its own tick count. In this project they both advance at a nominal 1 kHz, through separate interrupt sources. TIM2 starts during HAL initialisation, so HAL timeouts work before the scheduler starts. The FreeRTOS port sets up SysTick when it starts the scheduler. Application tasks use FreeRTOS delay functions so the scheduler can run another task during the wait.
| Timebase or exception | Owner | Purpose |
|---|---|---|
| TIM2 update IRQ | HAL timer timebase | Call HAL_IncTick at 1 ms intervals |
| SysTick | FreeRTOS Cortex-M3 port | Advance the scheduler tick |
| SVC | FreeRTOS Cortex-M3 port | Start the first task context |
| PendSV | FreeRTOS Cortex-M3 port | Save and restore task contexts at a switch |
At an 8 MHz core clock and a 1,000 Hz tick, the Cortex-M3 SysTick reload is 8,000,000 ÷ 1,000 − 1 = 7,999. The port uses SystemCoreClock when setting up the reload. If you later use 72 MHz, the reload becomes 71,999 at the same tick rate. Both fit the 24-bit counter. A correct reload gives the intended ratio to the oscillator. The real period also follows the oscillator’s accuracy.
For the HAL tick at 8 MHz APB1 with no prescaler, the TIM2 input clock is 8 MHz. A prescaler register of 7 gives a 1 MHz counter, and an auto-reload value of 999 gives one update every 1,000 counter ticks, or 1 ms. If you move to a common 72 MHz clock tree with APB1 divided by 2, the timer gets 72 MHz even though the APB1 bus runs at 36 MHz. At a 1 MHz counter target that requires a prescaler of 71. Read the actual clock tree and generated timebase when changing clock rates. STM32 timer clock and prescaler guide

Add the native FreeRTOS kernel and configuration
Get the FreeRTOS Kernel V11.2.0 release. Add tasks.c, queue.c, list.c and portable/GCC/ARM_CM3/port.c to the project as compiled C sources. Add the release’s include and portable/GCC/ARM_CM3 directories to the include search paths. Put FreeRTOSConfig.h in a project include directory and copy in rtos_lab.c and rtos_lab.h. Compile the project in C11 mode. Keep the upstream licence files and notices with the kernel sources.
This configuration uses static allocation. xTaskCreateStatic() receives its task control block and stack, and xQueueCreateStatic() receives its control block and item storage. With the V11.2.0 non-MPU port, configKERNEL_PROVIDED_STATIC_MEMORY supplies the idle task’s static memory. Software timers are disabled, so this lab needs no timer service task. Build the three kernel C files and the port without adding a heap implementation.
#ifndef STM32_035_FREERTOS_CONFIG_H
#define STM32_035_FREERTOS_CONFIG_H
#include <stdint.h>
extern uint32_t SystemCoreClock;
void RtosLab_Assert(const char *file, unsigned line);
#define configCPU_CLOCK_HZ SystemCoreClock
#define configTICK_RATE_HZ 1000U
#define configTICK_TYPE_WIDTH_IN_BITS TICK_TYPE_WIDTH_32_BITS
#define configUSE_PREEMPTION 1
#define configUSE_TIME_SLICING 1
#define configMAX_PRIORITIES 4U
#define configMAX_TASK_NAME_LEN 12U
#define configMINIMAL_STACK_SIZE 128U
#define configUSE_IDLE_HOOK 0
#define configUSE_TICK_HOOK 0
#define configUSE_TICKLESS_IDLE 0
#define configUSE_TIMERS 0
#define configUSE_MUTEXES 0
#define configUSE_TASK_NOTIFICATIONS 0
#define configSUPPORT_STATIC_ALLOCATION 1
#define configSUPPORT_DYNAMIC_ALLOCATION 0
#define configKERNEL_PROVIDED_STATIC_MEMORY 1
#define configCHECK_FOR_STACK_OVERFLOW 2
#define INCLUDE_vTaskDelay 1
#define INCLUDE_xTaskDelayUntil 1
#define INCLUDE_vTaskSuspend 1
#define INCLUDE_uxTaskGetStackHighWaterMark 1
#define LAB_NVIC_PRIORITY_BITS 4U
#define LAB_MAX_SYSCALL_PRIORITY 5U
#define configMAX_SYSCALL_INTERRUPT_PRIORITY \
(LAB_MAX_SYSCALL_PRIORITY << (8U - LAB_NVIC_PRIORITY_BITS))
#define vPortSVCHandler SVC_Handler
#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler SysTick_Handler
#define configASSERT(condition) \
do { \
if (!(condition)) { \
RtosLab_Assert(__FILE__, __LINE__); \
} \
} while (0)
#endif
The configuration maps the port’s three handler names to the vector names used by the STM32F103 startup assembly. Check the generated stm32f1xx_it.c for definitions of SVC_Handler, PendSV_Handler and SysTick_Handler. Disable generation for those three where your project offers that control. If the generated file still defines them, remove those definitions from the build so the port supplies the only strong definitions. Keep the startup vector table, fault handlers, TIM2 IRQ and other peripheral handlers. Check the three definitions again after regenerating the project.
Inspect the ELF or map after linking. The three handlers should resolve to the Cortex-M3 port and the TIM2 vector should resolve to the HAL timebase IRQ. Cortex-M vector entries for executable handlers have bit zero set to indicate Thumb state. A linker map can identify a handler that still resolves to an empty stub. A duplicate-symbol error usually means the generated source still defines an exception that the port also defines. An empty vector at run time can give a different symptom, such as entering the default handler when the scheduler attempts to start.
Keep SystemCoreClock accurate before vTaskStartScheduler(). The kernel configuration sets the tick calculation at scheduler start. If you change clocks during a running application, plan how both tick sources will be reconfigured and what happens to outstanding delays. The lab uses one fixed clock. It also leaves tickless idle disabled, so measuring this demonstration tells you about scheduling and queue behaviour with a periodic tick. STM32 low-power and wake-up guide
Create the producer, consumer and queue
The queue stores four copies of LabSample. Each item contains a 32-bit sequence number and a 32-bit enqueue timestamp. That is eight bytes per sample and 32 bytes of payload storage in total. The queue control block is separate. After a successful send the producer can reuse its local variable, because the queue copies the value before returning. A queue of pointers would copy the pointer and require the application to manage the referenced memory’s lifetime.
The producer is task priority 1. It uses xTaskDelayUntil() to schedule each release relative to the previous planned release. With a 100-tick period, a release scheduled for tick 100 is followed by releases at 200 and 300, even if the work after release takes a few ticks. If work regularly runs past the next release, the task can stop blocking and produce catch-up attempts. A real producer should define what to do with missed releases, such as counting them and deliberately rescheduling from the current time.
The consumer is task priority 2. It calls xQueueReceive() with portMAX_DELAY, which is an indefinite wait in this configuration because INCLUDE_vTaskSuspend is enabled. When the producer sends to an empty queue, the send can unblock the higher-priority consumer. The consumer takes the oldest queued item, toggles PC13 and delays for its simulated service interval. It can then receive another item or block again if the queue has emptied.
The producer sends with zero wait. If the queue is full, it increments dropped and keeps its periodic schedule. This policy is useful for a source that should stay on its time grid. An application that needs each item may instead wait for a bounded time or reduce the source rate. Consider how waiting affects deadlines, and handle a send timeout explicitly. Adding queue space absorbs a larger burst. Sustained overload still needs a change in the production rate, service rate or loss policy.
#ifndef STM32_035_RTOS_LAB_H
#define STM32_035_RTOS_LAB_H
void RtosLab_Start(void);
#endif
#include "rtos_lab.h"
#include "FreeRTOS.h"
#include "queue.h"
#include "task.h"
#include "stm32f1xx_hal.h"
enum { QUEUE_DEPTH = 4, LAB_STACK_WORDS = 128 };
typedef struct {
uint32_t sequence;
TickType_t queued_at;
} LabSample;
typedef struct {
uint32_t attempted;
uint32_t dropped;
uint32_t received;
uint32_t last_sequence;
TickType_t last_age_ticks;
UBaseType_t producer_free_words;
UBaseType_t consumer_free_words;
} LabStats;
_Static_assert(__NVIC_PRIO_BITS == LAB_NVIC_PRIORITY_BITS, "Check NVIC priority width");
_Static_assert(sizeof(LabSample) == 8, "Expected two 32-bit fields");
volatile LabStats lab_stats;
volatile uint32_t lab_slow_consumer;
volatile uint32_t lab_fault;
volatile unsigned lab_fault_line;
const char *volatile lab_fault_file;
static QueueHandle_t samples;
static StaticQueue_t queue_control;
static uint8_t queue_storage[QUEUE_DEPTH * sizeof(LabSample)];
static StaticTask_t producer_control, consumer_control;
static StackType_t producer_stack[LAB_STACK_WORDS], consumer_stack[LAB_STACK_WORDS];
static void stop_lab(uint32_t reason)
{
__disable_irq();
lab_fault = reason;
for (;;) {
__NOP();
}
}
void RtosLab_Assert(const char *file, unsigned line)
{
lab_fault_file = file;
lab_fault_line = line;
stop_lab(1U);
}
void vApplicationStackOverflowHook(TaskHandle_t task, char *name)
{
(void)task;
(void)name;
stop_lab(2U);
}
static void produce(void *argument)
{
(void)argument;
TickType_t release = xTaskGetTickCount();
uint32_t sequence = 0;
for (;;) {
(void)xTaskDelayUntil(&release, pdMS_TO_TICKS(100U));
LabSample sample = {sequence++, xTaskGetTickCount()};
++lab_stats.attempted;
if (xQueueSend(samples, &sample, 0) != pdPASS) {
++lab_stats.dropped;
}
lab_stats.producer_free_words = uxTaskGetStackHighWaterMark(NULL);
}
}
static void consume(void *argument)
{
(void)argument;
for (;;) {
LabSample sample;
if (xQueueReceive(samples, &sample, portMAX_DELAY) == pdPASS) {
++lab_stats.received;
lab_stats.last_sequence = sample.sequence;
lab_stats.last_age_ticks = xTaskGetTickCount() - sample.queued_at;
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
lab_stats.consumer_free_words = uxTaskGetStackHighWaterMark(NULL);
const uint32_t service_ms = lab_slow_consumer ? 350U : 20U;
vTaskDelay(pdMS_TO_TICKS(service_ms));
}
}
}
void RtosLab_Start(void)
{
samples = xQueueCreateStatic(QUEUE_DEPTH, sizeof(LabSample), queue_storage, &queue_control);
configASSERT(samples != NULL);
TaskHandle_t receiver = xTaskCreateStatic(consume, "Receive", LAB_STACK_WORDS, NULL,
2U, consumer_stack, &consumer_control);
TaskHandle_t sender = xTaskCreateStatic(produce, "Produce", LAB_STACK_WORDS, NULL,
1U, producer_stack, &producer_control);
configASSERT(receiver != NULL && sender != NULL);
vTaskStartScheduler();
stop_lab(3U);
}
Call RtosLab_Start() once in main.c, after HAL_Init(), the generated clock setup and MX_GPIO_Init(). Include the header in the generated user include section and place the call in the user section after peripheral initialisation. The function creates the queue and both application tasks, then starts the scheduler. The ordinary bare-metal foreground loop below that call will only be reached if you change the failure handling. This lab records a fault and stops if scheduler startup returns.
/* In the user include section */
#include "rtos_lab.h"
/* In USER CODE 2, after generated initialisation */
RtosLab_Start();

Run the normal and overloaded cases
Build and flash over SWD. Let the firmware run for several seconds. At the normal 20 ms service setting, the consumer can finish its simulated delay before the next 100 ms producer release. PC13 should toggle approximately ten times per second after startup, which is five complete high-low cycles per second. Check that lab_stats.attempted and lab_stats.received increase and lab_stats.dropped stays at zero. Inspect the pin and the statistics separately if the board’s LED is on another pin.
The counters have one writer each. The producer owns the attempt and drop counters, while the consumer owns the receive count, last sequence and last age. A debugger can stop the core between two updates, so a snapshot can contain an attempt that is still in flight. Treat the counters as diagnostics and compare changes over a run. If another task needs an internally consistent snapshot for reporting, design an explicit handoff or use a short, correctly protected copy.
| Debug symbol | What to look for |
|---|---|
| lab_slow_consumer | 0 selects 20 ms. Any nonzero value selects 350 ms |
| lab_stats.attempted | Count of producer send attempts |
| lab_stats.dropped | Count of attempts rejected by the full queue |
| lab_stats.received | Count of samples taken by the consumer |
| lab_stats.last_sequence | Sequence of the most recently received sample |
| lab_stats.last_age_ticks | Kernel ticks from the send attempt to receipt |
| lab_stats.producer_free_words and consumer_free_words | Lowest observed free stack space for each task |
| lab_fault, lab_fault_file and lab_fault_line | Fault reason and assertion source location when available |
Pause the target away from an interrupt or a fault handler. Set lab_slow_consumer to 1 in the debugger and resume. Give the slow run time to fill the queue. The consumer now delays for 350 ms after each receive. dropped should increase, and last_age_ticks should become larger as items spend time waiting. The LED toggles at the rate the consumer takes items. Gaps in received sequence numbers appear after earlier queued items have been delivered.
Set lab_slow_consumer back to zero and resume again. If a 350 ms delay is already in progress, it finishes before the new value affects a receive. The consumer can then drain the backlog faster than the producer refills it. The drop counter retains its previous total. Compare two later readings to see whether new drops have stopped.
Work through the first full queue
With a 100 ms period the producer attempts ten sends per second. A consumer that takes a sample and then waits 350 ms can receive at most about 2.86 samples per second in the ideal slow case. Once the queue stays near full, the difference is about 7.14 dropped samples per second. These are calculated rates. Real observations depend on tick timing and execution overhead.
Here is the ideal timeline when the queue starts empty and the 350 ms service setting is active before the first sample. Times are measured from the producer’s initial release reference. A receive takes one item out of the queue at the start of service.
| Time | Event | Queued items afterwards |
|---|---|---|
| 100 ms | Sequence 0 sent and consumed | 0 |
| 200 ms | Sequence 1 sent | 1 |
| 300 ms | Sequence 2 sent | 2 |
| 400 ms | Sequence 3 sent | 3 |
| 450 ms | Consumer receives sequence 1 | 2 |
| 500 ms | Sequence 4 sent | 3 |
| 600 ms | Sequence 5 sent | 4 |
| 700 ms | Sequence 6 dropped because the queue is full | 4 |
At 450 ms the consumer receives sequence 1, which was sent at 200 ms. Its ideal age is 250 ticks, or 250 ms with this configuration. The age excludes the subsequent service delay. A 32-bit tick count at 1,000 Hz wraps after about 49.71 days. Unsigned subtraction handles the wrap for a sample whose actual waiting time fits within one wrap. Use the same rule when you add time differences to the application.

Size the queue for a burst and define what can be lost
Consider a source running at ten samples per second and a consumer that occasionally cannot run for 450 ms. An empty queue can see as many as five new arrivals during that interval, depending on the phase. Four slots leave at least one possible loss. Add the number of items already waiting when the stall starts and allow headroom for multiple stalls. When the consumer resumes, check that it can catch up before the next burst.
The policy in this lab discards a new item on a full queue and preserves the queued items in order. That makes sense if the receiver wants the oldest available history. For a display that only needs the latest state, a one-element overwrite queue can express a different policy. A measurement or control path that cannot lose samples needs a verified bound on service time, storage or a way to throttle production. Pick the policy according to what downstream code needs.
If the slow work is a storage write or serial operation, give that resource a clear owner. Record a fast sample in RAM and let the owning task handle the long operation. A higher task priority cannot make a card finish programming or free a full output buffer. It can also delay other Ready tasks if the high-priority task stays runnable. Start with an acquisition interval and a measured service budget, then decide where backpressure belongs. STM32 UART, interrupts and DMA guide
Replace the test data with an ADC sample
Keep the timestamp and sequence, then add a field for the value that was actually measured. If you use a third 32-bit field for a raw reading, each item becomes 12 bytes and four slots require 48 bytes. Update the compile-time size check and the expected queue budget together. A value can be copied into the queue by the producer while the consumer is still processing the preceding value. Record the time at the point you need to identify, such as the conversion completion or the enqueue attempt.
A polling ADC read in the producer contributes to that task’s execution time. If it can take nearly the entire period, check the rate under the longest conversion and error path. A timer-triggered ADC with DMA can establish a sampling cadence independently. Keep TIM2 reserved for HAL and choose a separate trigger that the MCU supports. If a DMA callback hands work to the RTOS, set its priority and use the interrupt-safe API. The callback can copy a small value to a queue or hand off a completed buffer with an ownership rule. A pointer to a DMA buffer is useful only while that buffer remains valid and the producer has stopped writing into it. STM32 ADC, calibration and DMA guide
The queue age measures a specific stage of the path. If a sensor is sampled before the producer sends, the age in this lab excludes that earlier conversion interval. If the consumer writes the sample to removable storage, receipt also precedes the final write. Capture separate timestamps for those stages when you need the end-to-end delay. A queue can be empty while a long write is still in progress, so an empty count alone gives limited information about completion.
Keep interrupt priorities and task priorities separate
Task priorities decide which Ready task runs. Higher task priority numbers win in FreeRTOS. On the Cortex-M NVIC, smaller pre-emption priority numbers denote more urgent interrupts. An interrupt at NVIC priority 0 is therefore the most urgent, while the idle task runs at FreeRTOS task priority 0. These are different priority spaces.
The STM32F103 implements four NVIC priority bits. The configuration uses a library-level syscall ceiling of 5. With four implemented bits that becomes 5 << 4 = 0x50 in the value used by the Cortex-M3 port. STM32 HAL priority APIs take the unshifted priority number. The lab assumes the all-pre-emption grouping selected by HAL_Init(). The port uses BASEPRI to mask the interrupt priorities that are allowed to access the kernel while it updates its state.
| NVIC priority on this lab | Kernel call from an ISR |
|---|---|
| 0 through 4 | Keep the handler independent of FreeRTOS APIs |
| 5 through 15 | Use the interrupt-safe API variant when needed |
| TIM2 at 0 | Update the HAL tick only |
| SysTick and PendSV | Reserved for the Cortex-M3 port at the lowest effective priority |
When adding a button or DMA interrupt, choose an NVIC priority within the allowed range before it uses a FreeRTOS API. Use an interrupt-safe function such as xQueueSendFromISR(), check its result, and request a context switch with portYIELD_FROM_ISR() if the call unblocks a higher-priority task. The flag passed to the send starts as pdFALSE. Keep the ISR short, clear the peripheral’s interrupt cause as required and let a task do the longer work.
The existing lab sends from task context. Its queue has one producer and one consumer. If you add an interrupt producer, decide whether to use a separate queue or change the ownership of the counters. A read-modify-write increment by two contexts can lose an update. The same rule applies when a timer interrupt and a task share a result variable. STM32 EXTI and button interrupts guide
Check stack use and the linked memory map
The two application stacks each contain 128 StackType_t entries. On the Cortex-M3 port an entry is four bytes, so each stack reserves 512 bytes. The kernel’s static idle stack has the same size in this example. The three stack arrays therefore take 1,536 bytes, and the queue item buffer takes another 32 bytes. Task and queue control blocks, scheduler state, application data and the main exception stack are additional. Use the final link map for the whole firmware when checking the C8’s 20 KiB of SRAM.
In task context, the lab calls uxTaskGetStackHighWaterMark(NULL) to inspect its own stack. The result is the minimum unused stack measured since that task began, in stack entries. On the Cortex-M3, a reading of 36 means 144 bytes of observed headroom. The measurement says nothing about a code path that hasn’t run. Exercise error paths, peak queue activity and any library calls that will run in the task before using the value to reduce a stack size.
The high-water check scans stack fill and costs time, so treat the per-iteration calls as lab instrumentation. If you extend the example with fast sampling, consider moving diagnostics to a slower monitoring path. Keep the overflow hook while bringing up the application. In this lab it records fault reason 2 and stops. A check during a context switch can detect some overflows. The handler and main stack need their own budget, and a severely damaged stack can affect what the debugger can recover.
| Memory item | Lab allocation | What the number excludes |
|---|---|---|
| Producer stack | 128 words, 512 bytes | Task control block |
| Consumer stack | 128 words, 512 bytes | Task control block |
| Idle stack | 128 words, 512 bytes | Kernel state and idle task control block |
| Queue item storage | 4 × 8 bytes, 32 bytes | Queue control block |
| Main exception stack | Set by the project linker and startup | Task stacks and general static data |
The STM32 build check linked the lab with the pinned kernel, ST’s STM32F103 startup and its TIM2 HAL timebase template. The linked vector words were checked against the SVC, PendSV, SysTick and TIM2 symbols. A host run using FreeRTOS’s Windows port exercised normal operation, queue overload and recovery. Use the target’s debugger and optional pin trace to verify the physical board’s tick rates and stack usage.

Troubleshoot scheduler and queue problems
| Symptom | Check first | What it can reveal |
|---|---|---|
| Duplicate SVC, PendSV or SysTick handler | Generated stm32f1xx_it.c and linked object list | Generated handler and port handler both compiled |
| Scheduler enters Default_Handler or a fault | ELF vector entries and active VTOR | Wrong vector table, unbound handler or wrong port |
| HAL tick stays frozen | TIM2 IRQ, its callback and HAL_GetTick | Timer timebase source excluded or callback path missing |
| First sample never appears | SysTick handler, reload and SystemCoreClock | Kernel tick source or clock mismatch |
| lab_fault equals 1 | lab_fault_file and lab_fault_line | Configuration or kernel assertion |
| lab_fault equals 2 | Check task stacks and recent code changes | Stack overflow check fired |
| lab_fault equals 3 | Scheduler startup and kernel configuration | vTaskStartScheduler returned |
| Drops increase during the normal run | Consumer delay, execution time and debugger state | Rate mismatch or consumer starvation |
| PC13 is quiet while received increases | Pin mode, board wiring and LED | Output issue after a working queue |
When the application stops near scheduler start, inspect the active vector table and the first exception that fired. A correct symbol in the link map helps, but the processor also needs to use the matching vector table at runtime. Check SCB->VTOR, especially if the firmware starts after a bootloader. Use the Cortex-M3 port from the same kernel release as the other source files and check that the project targets Cortex-M3 Thumb code.
If the HAL clock advances and the kernel clock does not, focus on SysTick and the port. If the kernel schedules tasks but a HAL timeout never finishes, check TIM2 and the HAL callback. With two tick sources it is possible for one side to work while the other fails. Check both counters during a running interval. A debug halt changes when the core and attached timers can run, so use continuous execution for timing observations.
Verify clock and queue behaviour on the board
Before the scheduler starts, the HAL should already have configured TIM2. Inspect its prescaler, auto-reload, counter-enable and update-interrupt enable fields. For the 8 MHz clock tree, expect prescaler 7 and reload 999. Check that the HAL tick advances across a period of continuous execution. After scheduler start, inspect the SysTick reload for the expected 7,999. A reload appropriate to a 72 MHz project on an 8 MHz system would slow the RTOS tick by a factor of nine.
Use a scope or logic analyser on PC13 to check the release cadence in normal mode. Adjacent edges should be about 100 ms apart under light load. The higher-priority consumer typically runs soon after the send, so the pin gives a visible proxy for receipt. It does not expose the exact producer timestamp. If you need that comparison, instrument the producer with a separate GPIO and trigger on both traces. The slow mode should show longer receive intervals and accumulated queue age even though the producer keeps attempting at the original rate.
Keep snapshots from the normal, slow and recovered runs, along with the clock configuration used to build them. Compare the increase in the drop counter over each interval. A reset clears the counters and discards any items waiting in the static queue. Treat a reset during the test as a new session so the results describe one scheduler run.
If a task appears to run only once, look at what it waits for. A consumer blocked on an empty queue is doing what the code asked. Check whether the producer is alive and whether any send succeeds. A producer spending long intervals in a peripheral call can release late. Record both the scheduled period and the measured gaps to separate clock configuration problems from execution time.
Questions about FreeRTOS on the Blue Pill
Does static allocation require a FreeRTOS heap file?
This build has dynamic allocation disabled. The application supplies storage for its queue and task objects, and the pinned kernel supplies idle task storage. If you later enable features that allocate dynamically, choose and configure a heap implementation for that build and handle allocation failures.
Can I use HAL_Delay inside a task?
HAL’s delay function waits for the HAL tick and typically occupies the calling task until it finishes. For ordinary RTOS task delays, use vTaskDelay() or xTaskDelayUntil() so the task blocks and gives other Ready tasks a chance to run. HAL driver calls can still use the TIM2-backed tick for internal timeouts.
Why are received samples older when I make the queue deeper?
A deeper FIFO can retain more old samples during a stall. When the consumer catches up, it reads those older samples first. Decide whether preserving history or delivering fresh values matters more. Use sequence numbers and age measurements to see the effect of the selected queue policy.
Datasheets & External Resources
| Source | What to check |
|---|---|
| ST DS5319 STM32F103x8/xB datasheet | STM32F103C8 Flash, SRAM, HSI and PC13 characteristics |
| ST RM0008 reference manual | APB clocking, timer inputs, prescaler and update timing |
| ST PM0056 Cortex-M3 programming manual | SysTick, exception vectors, Thumb state and BASEPRI |
| ST CubeMX user manual UM1718 | HAL timebase selection and code generation |
| ST STM32F1 HAL TIM2 timebase template v1.1.10 | Timer setup, IRQ and HAL tick increment |
| FreeRTOS V11.2.0 ARM_CM3 port | Handler bindings, scheduler startup and tick configuration |
| FreeRTOS Cortex-M interrupt documentation | Priority encoding and interrupt-safe API restrictions |
| FreeRTOS V11.2.0 task API header | Static task allocation, periodic delays and stack diagnostics |
| FreeRTOS V11.2.0 queue API header | Static queues, copied items and ISR sends |