Best Practices for STM32 Project Structure and Code Organization

Quick Answer

A well-organized STM32 project should have separate directories for drivers, middleware, application code, and configuration; modular design with clear interfaces; consistent naming conventions; and comprehensive documentation. Use HAL for peripheral abstraction, isolate hardware-dependent code, and implement layered architecture. This approach improves maintainability, enables code reuse, and facilitates team collaboration.

Introduction

Good project structure and code organization are essential for developing maintainable, scalable embedded systems. As projects grow in complexity, proper organization becomes increasingly important for managing dependencies, enabling code reuse, and facilitating team collaboration. This guide presents best practices for structuring STM32 projects.

Core Content

1. Directory Structure

Recommended Structure

project/
├── Core/
│   ├── Inc/           # HAL initialization headers
│   └── Src/           # HAL initialization source
├── Drivers/
│   ├── STM32F4xx_HAL_Driver/
│   ├── CMSIS/
│   └── BSP/           # Board support packages
├── Middlewares/
│   ├── FreeRTOS/
│   ├── USB_Device/
│   └── FatFS/
├── App/
│   ├── Inc/           # Application headers
│   │   ├── main.h
│   │   ├── config.h
│   │   └── app_tasks.h
│   └── Src/           # Application source
│       ├── main.c
│       ├── app_tasks.c
│       └── sensor_handler.c
├── Hardware/
│   ├── Inc/           # Hardware abstraction
│   │   ├── motor_driver.h
│   │   └── sensor_interface.h
│   └── Src/
│       ├── motor_driver.c
│       └── sensor_interface.c
├── Utils/
│   ├── Inc/           # Utility functions
│   │   ├── circular_buffer.h
│   │   └── debug_print.h
│   └── Src/
│       ├── circular_buffer.c
│       └── debug_print.c
├── Build/             # Build outputs
├── Doc/               # Documentation
└── Tests/             # Unit tests

2. Modular Design Principles

Separation of Concerns

  • Hardware Layer: Direct peripheral access, BSP functions
  • Driver Layer: Abstract hardware details, provide clean APIs
  • Middleware Layer: RTOS, file systems, communication stacks
  • Application Layer: Business logic, state machines, algorithms

Interface Design

// Good: Hardware-independent interface
// motor_driver.h
typedef struct {
    void (*setSpeed)(uint8_t speed);
    void (*setDirection)(bool forward);
    void (*stop)(void);
} MotorDriver;

// Application code uses interface, not implementation
MotorDriver motor = MotorDriver_Init();
motor.setSpeed(50);

3. Naming Conventions

Consistent Naming

  • Functions: Module_Action (e.g., Motor_SetSpeed, UART_SendData)
  • Variables: camelCase (e.g., motorSpeed, sensorValue)
  • Constants: UPPER_SNAKE_CASE (e.g., MAX_BUFFER_SIZE, DEFAULT_TIMEOUT)
  • Types: PascalCase with _t suffix (e.g., MotorConfig_t, SensorData_t)
  • Macros: UPPER_SNAKE_CASE (e.g., ENABLE_DEBUG, CALCULATE_AVERAGE(x,y))

4. Configuration Management

Centralized Configuration

// config.h - Centralized configuration
#ifndef CONFIG_H
#define CONFIG_H

// System configuration
#define SYSTEM_CLOCK_HZ         168000000
#define TICK_RATE_HZ            1000

// Peripheral configuration
#define UART_BAUD_RATE          115200
#define SPI_CLOCK_SPEED_HZ      1000000

// Application configuration
#define SENSOR_SAMPLE_RATE_MS   100
#define MOTOR_PWM_FREQUENCY_HZ  20000

// Feature toggles
#define ENABLE_DEBUG_PRINT      1
#define ENABLE_WATCHDOG         1

#endif

5. Documentation Practices

Code Documentation

/**
 * @brief Initialize the motor driver
 * @param config Pointer to motor configuration structure
 * @retval MotorStatus_t Status of initialization
 * 
 * @note This function must be called before any other motor functions
 * @warning Do not call from ISR context
 */
MotorStatus_t Motor_Init(MotorConfig_t *config);

6. Version Control

.gitignore for STM32

# Build outputs
Build/
*.o
*.elf
*.hex
*.bin
*.map

# IDE files
.vscode/
.idea/
*.uvprojx.user
*.ioc

# Generated files
Drivers/STM32F4xx_HAL_Driver/

FAQ

Should I use HAL or write my own drivers?

Use HAL for most projects. HAL provides tested, portable code that speeds development. Write custom drivers only when you need performance optimization or features not available in HAL.

How do I handle multiple build configurations?

Use preprocessor defines. Create Debug and Release configurations in your IDE with different optimization levels and defines (e.g., DEBUG, RELEASE). Use #ifdef to enable/disable features.

Should I use a static analysis tool?

Yes! Static analysis tools (clang-tidy, Cppcheck, PC-lint) catch bugs early and enforce coding standards. Integrate them into your build system for automatic checking.

Conclusion

Well-organized STM32 projects:

  • Directory Structure: Clear separation of modules
  • Modular Design: Hardware-independent interfaces
  • Naming Conventions: Consistent across the project
  • Configuration: Centralized, easy to modify
  • Documentation: Clear, complete, and maintained
  • Version Control: Proper .gitignore, meaningful commits

Need Help with Project Architecture?

InnovChip provides embedded software architecture and development services. Contact us today for assistance with project structure, code review, and professional firmware development.

Leave a Reply

Your email address will not be published. Required fields are marked *