diff --git a/CMakeLists.txt b/CMakeLists.txt
index dfd11baca973536f6d83df0f6e0c5afede5e1639..3fa58b7393c826c0c00246d49efbc5ad066061a9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -33,6 +33,7 @@ target_sources(app PRIVATE
 	${KESTREL_SOURCE_DIR}/src/flash_filesystem.c
 	${KESTREL_SOURCE_DIR}/src/fsi.c
 	${KESTREL_SOURCE_DIR}/src/kestrel.c
+	${KESTREL_SOURCE_DIR}/src/simple_pwm.c
 	${KESTREL_SOURCE_DIR}/src/webservice.c
 	${KESTREL_SOURCE_DIR}/src/webportal.c
 	${KESTREL_SOURCE_DIR}/src/direct_uart.c
@@ -60,4 +61,4 @@ add_custom_target(
     COMMAND COMMAND ${CMAKE_COMMAND} -D KESTREL_SOURCE_DIR="${KESTREL_SOURCE_DIR}" -D embedded_file_sources="${embedded_file_sources_list}" -P ${CMAKE_CURRENT_SOURCE_DIR}/generate_static_files.cmake
     DEPENDS ${embedded_file_sources}
     COMMENT "Generating C header file from static files"
-    )
\ No newline at end of file
+    )
diff --git a/kestrel/src/kestrel.c b/kestrel/src/kestrel.c
index c93e5f97aa3670264f6525744d75e38ddcac4243..c25afbf4d6b32d2471fe959009e855f02e1f67f7 100644
--- a/kestrel/src/kestrel.c
+++ b/kestrel/src/kestrel.c
@@ -32,6 +32,7 @@ LOG_MODULE_REGISTER(kestrel_core, LOG_LEVEL_DBG);
 #include "aquila.h"
 #include "ipmi_bt.h"
 #include "opencores_i2c.h"
+#include "simple_pwm.h"
 
 #include "kestrel.h"
 
@@ -3455,6 +3456,10 @@ int kestrel_init(void)
     // initialize_i2c_master((uint8_t*)I2CMASTER3_BASE, 100000);
     initialize_i2c_master((uint8_t *)I2CMASTER4_BASE, 100000);
 
+#ifdef SIMPLEPWM_BASE
+    initialize_pwm_controller(SIMPLEPWM_BASE);
+#endif
+
     // Check for Aquila core presence
     if ((read_aquila_register(HOSTLPCSLAVE_BASE, AQUILA_LPC_REG_DEVICE_ID_HIGH) == AQUILA_LPC_DEVICE_ID_HIGH) &&
         (read_aquila_register(HOSTLPCSLAVE_BASE, AQUILA_LPC_REG_DEVICE_ID_LOW) == AQUILA_LPC_DEVICE_ID_LOW))
diff --git a/kestrel/src/simple_pwm.c b/kestrel/src/simple_pwm.c
new file mode 100644
index 0000000000000000000000000000000000000000..761cbe19b71884d565ad2c6ecfed967aed98c356
--- /dev/null
+++ b/kestrel/src/simple_pwm.c
@@ -0,0 +1,86 @@
+// © 2020 - 2021 Raptor Engineering, LLC
+//
+// Released under the terms of the GPL v3
+// See the LICENSE file for full details
+
+#include <logging/log.h>
+LOG_MODULE_REGISTER(kestrel_pwm, LOG_LEVEL_DBG);
+
+#include "simple_pwm.h"
+
+#include "utility.h"
+
+#include <generated/csr.h>
+#include <generated/soc.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#define KESTREL_LOG(...) LOG_INF(__VA_ARGS__)
+
+int initialize_pwm_controller(uint8_t *base_address)
+{
+    KESTREL_LOG("Configuring PWM controller at address %p...", base_address);
+
+    if ((*((volatile uint32_t *)(base_address + SIMPLE_PWM_MASTER_DEVICE_ID_HIGH)) != SIMPLE_PWM_DEVICE_ID_HIGH) ||
+        (*((volatile uint32_t *)(base_address + SIMPLE_PWM_MASTER_DEVICE_ID_LOW)) != SIMPLE_PWM_DEVICE_ID_LOW))
+    {
+        return -1;
+    }
+    uint32_t opencores_spi_version = *((volatile uint32_t *)(base_address + SIMPLE_PWM_MASTER_DEVICE_VERSION));
+    KESTREL_LOG("OpenCores I2C master found, device version %0d.%0d.%d",
+           (opencores_spi_version >> SIMPLE_PWM_VERSION_MAJOR_SHIFT) & SIMPLE_PWM_VERSION_MAJOR_MASK,
+           (opencores_spi_version >> SIMPLE_PWM_VERSION_MINOR_SHIFT) & SIMPLE_PWM_VERSION_MINOR_MASK,
+           (opencores_spi_version >> SIMPLE_PWM_VERSION_PATCH_SHIFT) & SIMPLE_PWM_VERSION_PATCH_MASK);
+    {
+        KESTREL_LOG("Disabling PWM outputs");
+
+        set_pwm_value(base_address, 0, 0x00);
+        set_pwm_value(base_address, 1, 0x00);
+        set_pwm_value(base_address, 2, 0x00);
+        set_pwm_value(base_address, 3, 0x00);
+
+        return 0;
+    }
+
+    return 1;
+}
+
+int set_pwm_value(uint8_t *base_address, uint8_t channel, uint8_t value)
+{
+    uint32_t dword;
+
+    dword = *((volatile uint32_t *)(base_address + SIMPLE_PWM_MASTER_PWM_CTL));
+    dword &= ~(0xff << (channel * 8));
+    dword |= (value & 0xff) << (channel * 8);
+    *((volatile uint32_t *)(base_address + SIMPLE_PWM_MASTER_PWM_CTL)) = dword;
+
+    return 0;
+}
+
+uint8_t get_pwm_value(uint8_t *base_address, uint8_t channel)
+{
+    return (*((volatile uint32_t *)(base_address + SIMPLE_PWM_MASTER_PWM_CTL)) >> (channel * 8)) & 0xff;
+}
+
+int get_tach_value(uint8_t *base_address, uint8_t channel)
+{
+    uint16_t raw_tach_value = 0;
+    int rps = 0;
+    int rpm = 0;
+
+    if ((channel == 0) || (channel == 1))
+    {
+        raw_tach_value = (*((volatile uint32_t *)(base_address + SIMPLE_PWM_MASTER_TACH_01)) >> ((channel & 0x1) * 16)) & 0xffff;
+    }
+    else if ((channel == 2) || (channel == 3))
+    {
+        raw_tach_value = (*((volatile uint32_t *)(base_address + SIMPLE_PWM_MASTER_TACH_23)) >> ((channel & 0x1) * 16)) & 0xffff;
+    }
+
+    // Compute RPM
+    rps = raw_tach_value * SIMPLE_PWM_TACH_SAMPLE_RATE_HZ;
+    rpm = rps * 60;
+
+    return rpm;
+}
diff --git a/kestrel/src/simple_pwm.h b/kestrel/src/simple_pwm.h
new file mode 100644
index 0000000000000000000000000000000000000000..156d5dcb904035350cf921406c829f92bf437f09
--- /dev/null
+++ b/kestrel/src/simple_pwm.h
@@ -0,0 +1,53 @@
+// © 2020 - 2021 Raptor Engineering, LLC
+//
+// Released under the terms of the GPL v3
+// See the LICENSE file for full details
+
+#ifndef _SIMPLE_PWM_H
+#define _SIMPLE_PWM_H
+
+#include <stdint.h>
+
+#define SIMPLE_PWM_MASTER_DEVICE_ID_LOW  0x0
+#define SIMPLE_PWM_MASTER_DEVICE_ID_HIGH 0x4
+#define SIMPLE_PWM_MASTER_DEVICE_VERSION 0x8
+#define SIMPLE_PWM_MASTER_PWM_CTL        0xc
+#define SIMPLE_PWM_MASTER_TACH_01        0x10
+#define SIMPLE_PWM_MASTER_TACH_23        0x14
+
+#define SIMPLE_PWM_DEVICE_ID_HIGH 0x4932434d
+#define SIMPLE_PWM_DEVICE_ID_LOW  0x4f50574d
+
+#define SIMPLE_PWM_VERSION_MAJOR_MASK  0xffff
+#define SIMPLE_PWM_VERSION_MAJOR_SHIFT 16
+#define SIMPLE_PWM_VERSION_MINOR_MASK  0xff
+#define SIMPLE_PWM_VERSION_MINOR_SHIFT 8
+#define SIMPLE_PWM_VERSION_PATCH_MASK  0xff
+#define SIMPLE_PWM_VERSION_PATCH_SHIFT 0
+
+#define SIMPLE_PWM_MASTER_PWM_0_MASK   0xff
+#define SIMPLE_PWM_MASTER_PWM_0_SHIFT  0
+#define SIMPLE_PWM_MASTER_PWM_1_MASK   0xff
+#define SIMPLE_PWM_MASTER_PWM_1_SHIFT  8
+#define SIMPLE_PWM_MASTER_PWM_2_MASK   0xff
+#define SIMPLE_PWM_MASTER_PWM_2_SHIFT  16
+#define SIMPLE_PWM_MASTER_PWM_3_MASK   0xff
+#define SIMPLE_PWM_MASTER_PWM_3_SHIFT  24
+#define SIMPLE_PWM_MASTER_TACH_0_MASK  0xffff
+#define SIMPLE_PWM_MASTER_TACH_0_SHIFT 0
+#define SIMPLE_PWM_MASTER_TACH_1_MASK  0xffff
+#define SIMPLE_PWM_MASTER_TACH_1_SHIFT 16
+#define SIMPLE_PWM_MASTER_TACH_2_MASK  0xffff
+#define SIMPLE_PWM_MASTER_TACH_2_SHIFT 0
+#define SIMPLE_PWM_MASTER_TACH_3_MASK  0xffff
+#define SIMPLE_PWM_MASTER_TACH_3_SHIFT 16
+
+// This is currently hard-wired into the HDL to save die area
+#define SIMPLE_PWM_TACH_SAMPLE_RATE_HZ 5
+
+int initialize_pwm_controller(uint8_t *base_address);
+int set_pwm_value(uint8_t *base_address, uint8_t channel, uint8_t value);
+uint8_t get_pwm_value(uint8_t *base_address, uint8_t channel);
+int get_tach_value(uint8_t *base_address, uint8_t channel);
+
+#endif // _SIMPLE_PWM_H