A 4-wheel, Bluetooth-controlled robot built entirely bare-metal on an STM32F407 — every peripheral driven through direct register access, no HAL, no generated code. Motor PWM, Bluetooth command parsing, multi-channel ADC sensing over DMA, an I²C LCD, and a full cloud supervision layer (ThingSpeak, MQTT, Node-RED dashboard).
Team project (ENSIT). This repository covers my contribution: the complete bare-metal firmware and the cloud supervision stack (ThingSpeak, Mosquitto, Node-RED). The ESP32 AT-command layer was developed by a teammate.
(Bilingual — English first, version française plus bas.)
Node-RED dashboard — live sensor gauges
ThingSpeak — cloud history of the three sensor channels
Serial trace (HTerm) — AT / MQTT traffic during operation
A phone sends text commands (AVANCE, ARRIERE, GAUCHE, DROITE, STOP) over Bluetooth. The STM32 parses them and drives four PWM channels to steer a 4-wheel chassis, ramping the speed up on each successive movement command. In parallel, three analog sensors are sampled continuously through ADC + DMA, shown on a local I²C LCD, echoed back to the phone, and pushed to the cloud every 30 seconds — alternating between ThingSpeak and a local MQTT broker. A Node-RED dashboard subscribes to the broker and displays the values as live gauges.
Everything runs from a single bare-metal main.c: no HAL, no CubeMX-generated code, just CMSIS register access.
This is the part I care about most. Every peripheral is configured by writing directly to its registers — clocks, alternate functions, timers, DMA streams, interrupt priorities. For example, the motor PWM is set up by hand:
void init_tim3_pwm(void)
{
RCC->APB1ENR |= (1 << 1);
TIM3->PSC = 15; // 16 MHz / 16 = 1 MHz timer clock
TIM3->ARR = 999; // → 1 kHz PWM
TIM3->CCMR1 |= (6 << 4) | (6 << 12); // PWM mode 1 on CH1/CH2
TIM3->CCMR2 |= (6 << 4) | (6 << 12); // PWM mode 1 on CH3/CH4
TIM3->CCER |= (1 << 0) | (1 << 4) | (1 << 8) | (1 << 12);
TIM3->CR1 |= (1 << 0); // enable counter
}No abstraction layer hides what the hardware is doing — which makes the code compact, fast, and a genuine demonstration of how the MCU works underneath the HAL.
The MCU runs on its default HSI 16 MHz clock (no PLL setup), which is plenty for this application and keeps the timing math simple.
- STM32F407VG-DISC1 — ARM Cortex-M4, running on the internal 16 MHz HSI oscillator
- HC-06 Bluetooth module — wireless command link from a phone
- H-bridge motor driver + 4-wheel chassis with DC motors
- 3 analog sensors read on ADC channels (e.g. light / IR / potentiometer)
- 16×2 I²C LCD (PCF8574 backpack, address 0x27) for local status
- ESP32 (AT firmware) acting as the Wi-Fi gateway to the cloud
| Peripheral | Pins | Role |
|---|---|---|
| TIM3 PWM (CH1–CH4) | PA6, PA7, PB0, PB1 | 4-channel motor PWM (~1 kHz) |
| USART2 + DMA1_S6 | PA2 / PA3 | Bluetooth HC-06 (9600 baud), non-blocking TX via DMA |
| USART3 | PC10 / PC11 | ESP32 link (115200 baud) |
| ADC1 + DMA2_S0 | PC0 / PC1 / PC2 | 3 sensors, scan mode + circular DMA |
| TIM2 | — | Triggers ADC conversions (TRGO) |
| TIM4 | — | 1 Hz tick → IoT scheduler (every 30 s) |
| EXTI0 | PA0 | User button — starts sampling |
| I2C2 | PB10 / PB11 | 16×2 LCD (PCF8574) |
Interrupt priorities are set explicitly so Bluetooth command reception (USART2) always preempts the slower background tasks.
Bluetooth bytes are accumulated in an ISR and a full command is detected on the UART IDLE line (end-of-frame), then handled in the main loop:
if (strstr(cmd_buf, "AVANCE")) { m1_avant = vitesse; m2_avant = vitesse; }
else if (strstr(cmd_buf, "GAUCHE")) { m1_avant = vitesse; m2_avant = vitesse / 2; } // differential turn
else if (strstr(cmd_buf, "STOP")) { vitesse = 200; }
TIM3->CCR1 = m1_avant; TIM3->CCR2 = m1_arriere;
TIM3->CCR3 = m2_avant; TIM3->CCR4 = m2_arriere;Each movement command nudges the speed up (capped), turns are done by running one side at half speed, and STOP resets everything. The current command and speed are echoed back to the phone over Bluetooth.
Beyond the firmware, I built the whole monitoring side, running three channels in parallel:
- ThingSpeak — the three ADC values are pushed to a cloud channel over HTTP, giving a historical chart accessible from anywhere.
- Mosquitto (MQTT) — the same values are published to a local broker on per-channel topics.
- Node-RED — subscribes to the broker and renders the values as live gauges on a dashboard (
/ui).
Since I no longer had the hardware on hand, I validated the supervision chain by simulating the sensor data via MQTT injection — the Node-RED flow and dashboard you see above run end-to-end against the broker, independent of the board.
The firmware is a single main.c using only CMSIS register definitions (<stm32f4xx.h>). To compile: create an STM32F407 project in Keil µVision or STM32CubeIDE and drop this main.c in place of the generated one. Set your own Wi-Fi / ThingSpeak / broker credentials in the #define block at the top before building.
Add closed-loop speed control with wheel encoders instead of open-loop PWM, move the blocking delai_ms() calls off the hot path, and replace the polled ESP32 exchange with an interrupt- or DMA-driven one so the robot stays fully responsive during cloud uploads.
STM32F407 · Bare-metal (CMSIS, register access) · PWM · UART + DMA · ADC + DMA · I²C · EXTI · Bluetooth HC-06 · ESP32 · MQTT (Mosquitto) · ThingSpeak · Node-RED
Un robot 4 roues piloté en Bluetooth, développé entièrement en bare-metal sur STM32F407 — chaque périphérique configuré en accès registre direct, sans HAL, sans code généré. PWM moteurs, parsing des commandes Bluetooth, acquisition multi-capteurs ADC en DMA, afficheur LCD I²C, et une couche complète de supervision cloud (ThingSpeak, MQTT, dashboard Node-RED).
Projet d'équipe (ENSIT). Ce dépôt couvre ma contribution : tout le firmware bare-metal et la couche de supervision (ThingSpeak, Mosquitto, Node-RED). Le volet commandes AT vers l'ESP32 a été développé par un binôme.
Dashboard Node-RED — jauges capteurs en temps réel
ThingSpeak — historique cloud des trois canaux capteurs
Trace série (HTerm) — trafic AT / MQTT en fonctionnement
Un téléphone envoie des commandes texte (AVANCE, ARRIERE, GAUCHE, DROITE, STOP) en Bluetooth. Le STM32 les interprète et pilote quatre canaux PWM pour diriger un châssis 4 roues, en augmentant la vitesse à chaque commande de mouvement successive. En parallèle, trois capteurs analogiques sont échantillonnés en continu via ADC + DMA, affichés sur un LCD I²C local, renvoyés au téléphone, et publiés vers le cloud toutes les 30 secondes — en alternant entre ThingSpeak et un broker MQTT local. Un dashboard Node-RED s'abonne au broker et affiche les valeurs sous forme de jauges en temps réel.
Tout tourne depuis un seul main.c bare-metal : pas de HAL, pas de code généré par CubeMX, uniquement de l'accès registre CMSIS.
C'est la partie qui compte le plus pour moi. Chaque périphérique est configuré en écrivant directement dans ses registres — horloges, fonctions alternées, timers, flux DMA, priorités d'interruption. Par exemple, la PWM des moteurs est mise en place à la main :
void init_tim3_pwm(void)
{
RCC->APB1ENR |= (1 << 1);
TIM3->PSC = 15; // 16 MHz / 16 = 1 MHz horloge timer
TIM3->ARR = 999; // → PWM 1 kHz
TIM3->CCMR1 |= (6 << 4) | (6 << 12); // mode PWM 1 sur CH1/CH2
TIM3->CCMR2 |= (6 << 4) | (6 << 12); // mode PWM 1 sur CH3/CH4
TIM3->CCER |= (1 << 0) | (1 << 4) | (1 << 8) | (1 << 12);
TIM3->CR1 |= (1 << 0); // activation du compteur
}Aucune couche d'abstraction ne masque ce que fait le matériel — ce qui rend le code compact, rapide, et démontre vraiment comment fonctionne le MCU sous la HAL.
Le MCU tourne sur son horloge HSI 16 MHz par défaut (pas de PLL), largement suffisante pour cette application et qui simplifie les calculs de timing.
- STM32F407VG-DISC1 — ARM Cortex-M4, sur l'oscillateur interne HSI 16 MHz
- Module Bluetooth HC-06 — liaison sans fil de commande depuis un téléphone
- Pont en H + châssis 4 roues à moteurs DC
- 3 capteurs analogiques lus sur les canaux ADC (ex. lumière / IR / potentiomètre)
- LCD I²C 16×2 (backpack PCF8574, adresse 0x27) pour l'état local
- ESP32 (firmware AT) servant de passerelle Wi-Fi vers le cloud
| Périphérique | Broches | Rôle |
|---|---|---|
| TIM3 PWM (CH1–CH4) | PA6, PA7, PB0, PB1 | PWM moteurs 4 canaux (~1 kHz) |
| USART2 + DMA1_S6 | PA2 / PA3 | Bluetooth HC-06 (9600 bauds), TX non bloquant en DMA |
| USART3 | PC10 / PC11 | Liaison ESP32 (115200 bauds) |
| ADC1 + DMA2_S0 | PC0 / PC1 / PC2 | 3 capteurs, mode scan + DMA circulaire |
| TIM2 | — | Déclenche les conversions ADC (TRGO) |
| TIM4 | — | Tick 1 Hz → ordonnanceur IoT (toutes les 30 s) |
| EXTI0 | PA0 | Bouton utilisateur — lance l'acquisition |
| I2C2 | PB10 / PB11 | LCD 16×2 (PCF8574) |
Les priorités d'interruption sont fixées explicitement pour que la réception des commandes Bluetooth (USART2) préempte toujours les tâches de fond plus lentes.
Les octets Bluetooth sont accumulés dans une ISR et une commande complète est détectée sur la ligne IDLE de l'UART (fin de trame), puis traitée dans la boucle principale :
if (strstr(cmd_buf, "AVANCE")) { m1_avant = vitesse; m2_avant = vitesse; }
else if (strstr(cmd_buf, "GAUCHE")) { m1_avant = vitesse; m2_avant = vitesse / 2; } // virage différentiel
else if (strstr(cmd_buf, "STOP")) { vitesse = 200; }
TIM3->CCR1 = m1_avant; TIM3->CCR2 = m1_arriere;
TIM3->CCR3 = m2_avant; TIM3->CCR4 = m2_arriere;Chaque commande de mouvement augmente la vitesse (plafonnée), les virages se font en faisant tourner un côté à mi-vitesse, et STOP réinitialise tout. La commande courante et la vitesse sont renvoyées au téléphone en Bluetooth.
Au-delà du firmware, j'ai construit toute la partie monitoring, sur trois canaux en parallèle :
- ThingSpeak — les trois valeurs ADC sont envoyées vers un canal cloud en HTTP, ce qui donne un historique graphique accessible de partout.
- Mosquitto (MQTT) — les mêmes valeurs sont publiées sur un broker local, un topic par canal.
- Node-RED — s'abonne au broker et affiche les valeurs sous forme de jauges en temps réel sur un dashboard (
/ui).
N'ayant plus le matériel sous la main, j'ai validé la chaîne de supervision en simulant les données capteurs par injection MQTT — le flux et le dashboard Node-RED visibles plus haut fonctionnent de bout en bout face au broker, indépendamment de la carte.
Le firmware tient dans un seul main.c n'utilisant que les définitions registre CMSIS (<stm32f4xx.h>). Pour compiler : créer un projet STM32F407 dans Keil µVision ou STM32CubeIDE et y placer ce main.c à la place de celui généré. Renseigner ses propres identifiants Wi-Fi / ThingSpeak / broker dans le bloc #define en haut avant compilation.
Ajouter un asservissement de vitesse en boucle fermée avec des encodeurs de roues plutôt qu'une PWM en boucle ouverte, sortir les appels bloquants delai_ms() du chemin critique, et remplacer l'échange ESP32 par scrutation par une version sur interruption ou DMA pour que le robot reste pleinement réactif pendant les envois cloud.
STM32F407 · Bare-metal (CMSIS, accès registre) · PWM · UART + DMA · ADC + DMA · I²C · EXTI · Bluetooth HC-06 · ESP32 · MQTT (Mosquitto) · ThingSpeak · Node-RED



