Skip to content

EMC2305 PWM Fan Controller Driver Documentation

Overview

The EMC2305 is a 5‑channel PWM fan controller with optional closed‑loop RPM control using tachometer feedback. Driver functions must be called while the FreeRTOS scheduler is active.


Minimal Working Example

WARNING. Don't just copy this code without reading the rest of this page. Lakshay Gupta takes all responsibility for incorrect information regarding this driver. Happy cooling :D

// Assuming I2C was initialized and interrupt callbacks overridden
// Reference 

#include "EMC2305.h"

I2C_HandleTypeDef hi2c1;
EMC2305_HandleTypeDef chip;

void Init_Task(void* argument) {
    // Initialize EMC2305
    // Only call from ONE task!
    if (EMC2305_Init(&chip, &hi2c1, 0x4D) != EMC2305_OK) {
        Error_Handler();
    }

    // Task kills itself
    vTaskDelete(NULL);
}

void EMC2305_Task_1(void* argument) {
    // Allow chip to power on
    vTaskDelay(pdMS_TO_TICKS(250));

    // Set global config
    EMC2305_Global_Config config = { 0 };
    config.watchdog_enable = true;

    if (EMC2305_SetGlobalConfig(&chip, &config) != EMC2305_OK) {
        Error_Handler();
    }

    // Set config1 and config2
    EMC2305_Fan_Config1 config1 = { 0 };
    config1.enable_closed_loop = false; // Set this to true if using FSC (Closed Loop RPM Control). False for using PWM directly
    config1.edges = EMC2305_EDG_5; // 5 edges is default for 2 pole fans
    config1.range = EMC2305_RNG_2000;

    EMC2305_Fan_Config2 config2 = { 0 };
    config2.enable_ramp_rate_ctl = true;
    config2.enable_glitch_filter = true;
    config2.error_window = EMC2305_ERG_200RPM;
    config2.derivative_options = EMC2305_DPT_BOTH;

    if (EMC2305_SetFanConfig(&chip, EMC2305_FAN2, &config1, &config2) != EMC2305_OK) {
        Error_Handler();
    };

    // Depends on the fan lol (should be in fan datasheet)
    if (EMC2305_SetPWMBaseFrequency(&chip, EMC2305_FAN2, EMC2305_PWM_19k53) != EMC2305_OK) {
        Error_Handler();
    };

    // Set minimum drive to 0%
    if (EMC2305_WriteReg(&chip, EMC2305_FAN_REG_ADDR(EMC2305_FAN2, EMC2305_REG_FAN1_MIN_DRIVE), 0x00) != EMC2305_OK) {
        Error_Handler();
    };

    // Set PID Gain to lowest (1x)
    if (EMC2305_WriteReg(&chip, EMC2305_FAN_REG_ADDR(EMC2305_FAN2, EMC2305_REG_GAIN1), 0x00) != EMC2305_OK) {
        Error_Handler();
    };

    // Set PWM output mode to open-drain (use false for push-pull)
    if (EMC2305_SetPWMOutputMode(&chip, EMC2305_FAN2, true) != EMC2305_OK) {
        Error_Handler();
    };

    // Control with direct PWM
    // Set PWM2 duty cycle to 25%
    if (EMC2305_SetFanPWM(&chip, EMC2305_FAN2, 25) != EMC2305_OK) {
        Error_Handler();
    };

    // Control with closed-loop FSC
    // Set RPM to 3000
    if (EMC2305_SetFanRPM(&chip, EMC2305_FAN2, 3000) != EMC2305_OK) {
        Error_Handler();
    };
}

int main(void) {
    // Init your HAL, System Clock, and Peripherals here

    // Create tasks
    xTaskCreateStatic(Init_Task,
        "Init Task",
        configMINIMAL_STACK_SIZE,
        NULL,
        tskIDLE_PRIORITY + 1,
        initTaskStack,
        &initTaskBuffer);

    xTaskCreateStatic(EMC2305_Task_1,
        "EMC2305 Task 1",
        configMINIMAL_STACK_SIZE,
        NULL,
        tskIDLE_PRIORITY + 2,
        emc2305TaskStack_1,
        &emc2305TaskBuffer_1);

    vTaskStartScheduler();

    while (1) {
    }

    return 0;
}

// I2C Transmit Interrupt Callback
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef* hi2c) {
    EMC2305_I2C_MasterTxCpltCallback(hi2c);
}

// I2C Receive Interrupt Callback
void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef* hi2c) {
    EMC2305_I2C_MasterTxCpltCallback(hi2c);
}

Required Hardware Setup

MCU Requirements

Your board must have:

  • An STM32 MCU using STM32 HAL (PSOM, LSOM, or any of our other custom STM32 boards)
  • A configured I2C peripheral (interrupt‑driven)
  • FreeRTOS enabled in your project (all Embedded-Sharepoint projects use RTOS by default)

This driver relies on:

  • HAL_I2C_Master_Transmit_IT()
  • HAL_I2C_Master_Receive_IT()
  • FreeRTOS queues, semaphores, and static task creation

Fan Electrical Requirements

Fans may be controlled via:

  • Push‑pull PWM (driven to high and low logic levels)
  • Open‑drain PWM (only driven low, requires an external pull‑up)

This is configured per‑fan using:

EMC2305_SetPWMOutputMode()

Initialization Process (Required Order)

Prerequisites

Before calling any EMC2305 function:

  • FreeRTOS scheduler must be running
  • The STM32 I2C peripheral must be initialized
  • I2C interrupts must be enabled

Required HAL Callbacks

You must forward these HAL callbacks:

void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c) {
    EMC2305_I2C_MasterTxCpltCallback(hi2c);
}

void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c) {
    EMC2305_I2C_MasterRxCpltCallback(hi2c);
}

Without this, all I2C operations will hang.


Driver Initialization

The first thing you need to do is initialize the chip using this function. It may only be called from one RTOS task (recommended to do this in your init task). An example using I2C bus 1 and the default chip address (0x4D):

EMC2305_HandleTypeDef chip;

EMC2305_Init(&chip, &hi2c1, 0x4D);

What this does internally:

  • Stores the HAL I2C handle
  • Converts the 7‑bit address to HAL format (<< 1)
  • Creates RTOS objects
  • Spawns the I2C worker task
  • Associates the chip with a specific I2C instance
  • Verifies:

  • Product ID (PID == 0b00)

  • Manufacturer ID (0x5D)

If any step fails, EMC2305_ERR is returned.


Core Configuration Functions

Global Chip Configuration

EMC2305_Global_Config cfg = {
    .alert_mask = false,
    .disable_smbus_timeout = true,
    .watchdog_enable = false,
    .drive_ext_clk = false,
    .use_ext_clk = false,
};

EMC2305_SetGlobalConfig(&chip, &cfg);

Controls global chip behavior.


Software Lock (Optional)

EMC2305_EnableSWLock(&chip);
  • Makes all SWL registers read‑only
  • Lock persists until power cycle
  • Use only after configuration is complete
  • Do not use this unless you are very confident in your settings

Operating Modes

Open‑Loop PWM Mode (Simple)

When to use:

  • You just want to set a duty cycle
  • Fan tach feedback is unavailable or unnecessary

Required configuration:

  • enable_closed_loop = false

Control function:

Example to set Fan 1 PWM output to 60%:

EMC2305_SetFanPWM(&chip, EMC2305_FAN1, 60);

When to use:

  • You need stable RPM across voltage/temperature changes
  • You have tach feedback connected

Required configuration:

EMC2305_Fan_Config1 cfg1 = {
    .enable_closed_loop = true,
    .range = EMC2305_RNG_2000,
    .edges = EMC2305_EDG_5,
    .update_time = EMC2305_UDT_100,
};

EMC2305_Fan_Config2 cfg2 = {
    .enable_ramp_rate_ctl = true,
    .enable_glitch_filter = true,
    .derivative_options = EMC2305_DPT_BOTH,
    .error_window = EMC2305_ERG_200RPM,
};

EMC2305_SetFanConfig(&chip, EMC2305_FAN1, &cfg1, &cfg2);

Control function:

EMC2305_SetFanRPM(&chip, EMC2305_FAN1, 3000);

Fan Control API Summary

Purpose Function
Set PWM duty EMC2305_SetFanPWM()
Set RPM target EMC2305_SetFanRPM()
Read RPM EMC2305_GetFanRPM()
Read PWM EMC2305_GetFanPWM()
Read fault status EMC2305_GetFanStatus()

Common Pitfalls

Using APIs Before RTOS Starts

Wrong

EMC2305_SetFanPWM(...);

before vTaskStartScheduler()

Correct

Call all APIs from tasks only.


Missing HAL I2C Callbacks

Symptoms:

  • Functions block forever
  • Timeouts occur

Fix:

  • Forward HAL callbacks exactly as shown above

Queue / Semaphore Exhaustion

If too many tasks call EMC2305 APIs simultaneously:

  • EMC2305_ReadReg() returns EMC2305_ERR

Fix:

  • Increase EMC2305_QUEUE_LENGTH
  • Avoid excessive polling

Wrong Control Mode

Calling:

EMC2305_SetFanPWM()

while closed‑loop control is enabled will not work as expected (and vice-versa).


Debugging Guide

Chip Not Detected

Check:

  • I2C address (passed as 7‑bit, not shifted)
  • SDA/SCL wiring
  • Pull‑ups present

RPM Always Reads UINT16_MAX

Causes:

  • Invalid fan index
  • I2C read failure
  • Tach reading = 0

Fan Status Flags

Use:

EMC2305_Fan_Status status = EMC2305_GetFanStatus(&chip);

Flags indicate:

  • Watchdog expiration
  • Spin failure
  • Stall detection
  • Drive failure