- Add callback_task with ISR-based UART command parser (@command\n protocol) - Add log_printf with mutex protection to prevent printf interleaving - Add per-motor enable/disable (motor enable yaw|pitch) - Add PID tuning via UART (pid roll kp 15) - Add cmd_parser module (registration + tokenize + dispatch) - Add UART layer architecture (interface → HAL → BSP) - Add filter modules (lowpass, moving_average, notch, rate_limiter) - Rewrite I2C bus and UART bus modules - Rewrite PID controller and MF4010V2 motor driver - Fix soft I2C driver Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
4.9 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
CloudPlant — a 2-axis gimbal stabilization controller for STM32F407xx (Cortex-M4F). Uses FreeRTOS (CMSIS V2 API) with a 100Hz PID control loop reading IMU angles over soft I2C and driving MF4010V2 brushless motors over CAN bus.
Build Commands
# Configure (one-time; Debug is the default preset)
cmake --preset Debug
# Build
cmake --build build/Debug
# Or build with ninja directly
ninja -C build/Debug
# Build a specific object file only
ninja -C build/Debug CMakeFiles/CloudPlant.dir/path/to/file.c.obj
The project uses CMake with Ninja as the generator and the arm-none-eabi-gcc toolchain (cmake/gcc-arm-none-eabi.cmake). A starm-clang toolchain file also exists as an alternative. Compile commands are exported to build/Debug/compile_commands.json for clangd.
Flash
# Flash the Debug ELF (requires STM32CubeProgrammer CLI)
python flash.py build/Debug/CloudPlant.elf
# Erase chip only
python flash.py --erase-only
# Flash a release build
python flash.py build/Release/CloudPlant.elf
The script converts ELF to BIN with arm-none-eabi-objcopy, then flashes via STM32_Programmer_CLI at 0x08000000 over SWD.
Architecture: Strict 6-Layer Stack
Every hardware access must follow this chain. Cross-layer calls are forbidden.
APP → Business logic, task orchestration
↓
Module → Device/algorithm encapsulation (motor, PID, LED, soft_i2c)
↓
Interface → Abstract ops-based API (_if.h with function pointer structs)
↓
HAL → Hardware operations using STM32 HAL, registers ops to Interface
↓
BSP → Hardware description only — pin maps, CAN handles, no logic
↓
Driver → STM32 HAL / CMSIS (generated by CubeMX, in Core/, Drivers/, Middlewares/)
Key rules:
- Module must NOT call HAL/BSP directly — it goes through Interface APIs.
- Interface defines a
xxx_ops_tstruct with function pointers; HAL implements and registers ops viaxxx_register_ops(). - BSP files contain only static data arrays (pin mappings, handle arrays) — no
if/for/whileor business logic. - APP never calls HAL or BSP directly; it calls Module functions, which call Interface APIs.
Key Directories (User Code)
| Directory | Purpose |
|---|---|
app/ |
RTOS tasks: app_init, control_task, sensor_task, monitor_task |
modules/device/motor/ |
MF4010V2 brushless motor driver (CAN-based, speed/position/torque modes) |
modules/device/led/ |
LED device abstraction |
modules/control/pid/ |
Generic PID controller (float-based, anti-windup via output clamping) |
modules/bus/soft_i2c/ |
Bit-banged I2C master (frame-level API) |
interfaces/ |
Abstract APIs: can_if, led_if, gpio_if, i2c_if, soft_i2c_if |
hal/stm32f4/ |
HAL implementations per peripheral — registers ops with interfaces |
bsp/stm32f4/ |
Board-specific pin/instance mappings |
3rdparty/jy901p/ |
JY901P IMU SDK (I2C protocol) |
3rdparty/cjson/ |
cJSON library |
CubeMX Integration
CubeMX-generated code lives in Core/, Drivers/, and Middlewares/. It is compiled as three static libraries defined in cmake/stm32cubemx/CMakeLists.txt:
stm32cubemx(INTERFACE — headers and defines)STM32_Drivers(OBJECT — HAL driver sources)FreeRTOS(OBJECT — FreeRTOS kernel sources)
User code is added to the main CMakeLists.txt via target_sources() and target_include_directories(). Global defines USE_HAL_DRIVER, STM32F407xx, and STM32_THREAD_SAFE_STRATEGY=2 come from the CubeMX CMake.
FreeRTOS task creation happens in Core/Src/freertos.c (CubeMX-managed). The tasks delegate to app/ entry points: StartSensorTask → sensor_task(), StartControlTask → control_task(), StartMonitorTask → monitor_task().
Data Flow (Gimbal Stabilization)
sensor_task (IMU poll via soft I2C)
→ osMessageQueue (imu_data_t: acc, gyro, angle)
→ control_task (100Hz PID loop)
→ CAN bus (motor speed commands to MF4010V2)
The IMU queue (g_imu_queue) is created in app_init() with depth 4. sensor_task reads JY901P IMU registers via soft I2C and pushes data into the queue. control_task reads the queue with a 20ms timeout, runs two PID controllers (Roll → Yaw motor, Pitch → Pitch motor), and sends speed commands over CAN.
Interface Pattern
Every hardware peripheral follows this pattern:
interfaces/<periph>/<periph>_if.h — ops struct + register function + API declarations
interfaces/<periph>/<periph>_if.c — dispatching through registered ops
hal/stm32f4/<periph>/hal_<periph>.c — implements ops, registers them at init
bsp/stm32f4/<periph>/bsp_<periph>.c — static hardware mapping
Module code (e.g., modules/device/led/led.c) calls led_if_init(), led_if_write(), etc. — never hal_led_* or bsp_led_* directly.