Skip to content

Commit

Permalink
add: input filtering
Browse files Browse the repository at this point in the history
- added input filter thread, that is responsible for actually receiving
  data (messages) from the CC IRQ
- the main thread now receives filtered data that is the average of the
  last _n_ messages

What I've essentially created is a rolling buffer, with one distinct
feature. If no messages are received in the configured lifespan, the
oldest messages are slowly removed from the queue & the sum/average is
also updated to reflect that.
This is to prevent cases where we stop receiving messages (stable or no
PWM) essentially leaving whatever the last _n_ messages were in the
buffer - as we want to reflect that the PWM has probably stabilized on
the last received value.

Hope everything makes sense, leave comments if it does not.
  • Loading branch information
niraami committed Oct 12, 2021
1 parent f54cc0a commit 3515fb2
Show file tree
Hide file tree
Showing 12 changed files with 181 additions and 41 deletions.
Binary file added .README/connector/PHD-jst-listing.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .README/fan-curves/input-raw.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .README/fan-curves/input-raw_small.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .README/fan-curves/input-smoothing.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .README/fan-curves/input-smoothing_small.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .README/fan-curves/input-vs-output-relation.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,20 @@ I personally don't think it needs or should be changed, but I guess there are us

</div>

The **green curve is the input**. I've just got another STM32 producing a PWM signal @25kHz, that linearly sweeps the duty cycle from 0 to 100%, waits, and then back. What you are seeing, it the actual value the board thinks is the input - which is why it is a bit noisy... I'm hoping to get rid of that with some smoothing algorithm (buffer) that I'm working on right now.
The **red curve is the target** PWM that is then generated by the STM32 to control the fans.
The **green curve is the input**. I've just got another STM32 producing a PWM signal @25kHz, that linearly sweeps the duty cycle from 0 to 100%, waits, and then back.
The **red curve is the target** PWM. This is generated by the STM32 to control the fans.

Both of these values on the graph are after being smoothed/filtered via a rolling average. This average is produced by taking the last *n* samples (8 by default), or max. of last *t* milliseconds of data (400ms by default). What this means is that no data older than 400ms is taken into account for the average, and a maximum of last 8 values are.
This technique gets rid of some of the signal uncertainty, but might not solve all of the issues that there are with scaling input values to different ranges - and also *maybe* introduces it's own artifacts, but none of those actually show up on my logic analyzer in a meaningful way, so it might just be issues with the STM32Monitor polling rate or whatever.

Here are the values before any post processing is applied (on the left) and after (on the right).

<div align="center">

[![](.README/fan-curves/input-raw_small.png)](.README/fan-curves/input-raw.png)
[![](.README/fan-curves/input-smoothing_small.png)](.README/fan-curves/input-smoothing.png)

</div>

Right now, you can manually configure the "fan curve" in [`curves.h`](src/Core/Src/curves.h).

Expand Down
2 changes: 1 addition & 1 deletion src/Core/Inc/main.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ extern "C" {
typedef struct {
__IO uint32_t CCR1;
__IO uint32_t CCR2;
} RegCCR;
} CCRPair;

/* USER CODE END ET */

Expand Down
187 changes: 157 additions & 30 deletions src/Core/Src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,31 @@
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/** Maximum number of messages/events stored in the TIM1 IRQ buffer */
#define TIM1_IRQ_BUFFER_LENGTH 2
/**
* Maximum number of messages that are pending from the TIM1 IRQ handler. This
* buffer is used to handle bursts of messages from the IRQ - ex. when the PWM
* signal is slowly changing.
*/
#define TIM1_IRQ_BUFFER_MAX_BACKLOG 4

/**
* Maximum number of messages/events stored in the TIM1 IRQ buffer
* This value directly affects how much smoothing is applied to the input signal
* @note High values may lead to artifacts
*/
#define TIM1_IRQ_BUFFER_LENGTH 8

/**
* Maximum lifespan of a buffer entry (in ms)
* Specifies the maximum amount of time an entry is going to be counted into
* the input average. After this time, the entry is guaranteed to be thrown
* out. This might also happen much sooner if the interval at which the entries
* are being added into the buffer is smaller than this value divided by the
* buffer's length.
* @note I recommend setting this value to be neatly divisible by the buffer's
* length, otherwise rounding will take place anyways
*/
#define TIM1_IRQ_BUFFER_LIFESPAN 400

/**
* This duty cycle is used on edge-cases where calculations fail or receive
Expand All @@ -59,10 +82,28 @@ osThreadId_t defaultTaskHandle;
const osThreadAttr_t defaultTask_attributes = {
.name = "defaultTask",
.stack_size = 128 * 4,
.priority = (osPriority_t) osPriorityNormal1,
};
/* Definitions for inputFilter */
osThreadId_t inputFilterHandle;
const osThreadAttr_t inputFilter_attributes = {
.name = "inputFilter",
.stack_size = 128 * 4,
.priority = (osPriority_t) osPriorityNormal,
};
/* USER CODE BEGIN PV */
MessageBufferHandle_t tim1_irq_buffer = NULL;

/**
* Message queue used to store reported CCR values from the TIM1_CC_IRQ handler
*/
MessageBufferHandle_t tim1_irq_backlog = NULL;
/**
* A small buffer used to transfer the filtered signal from the input filter to
* the main thread.
*/
MessageBufferHandle_t tim1_filtered_buffer = NULL;


/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
Expand All @@ -71,6 +112,7 @@ static void MX_GPIO_Init(void);
static void MX_TIM1_Init(void);
static void MX_TIM2_Init(void);
void StartDefaultTask(void *argument);
void InputFilterTask(void *argument);

/* USER CODE BEGIN PFP */

Expand Down Expand Up @@ -109,14 +151,16 @@ static void setDutyCycle(TIM_HandleTypeDef* const htim,
int main(void)
{
/* USER CODE BEGIN 1 */
tim1_irq_buffer = xMessageBufferCreate(
TIM1_IRQ_BUFFER_LENGTH * (sizeof(RegCCR) + sizeof(size_t))
);
tim1_irq_backlog = xMessageBufferCreate(
TIM1_IRQ_BUFFER_MAX_BACKLOG * (sizeof(CCRPair) + sizeof(size_t)) );
tim1_filtered_buffer = xMessageBufferCreate(
(sizeof(CCRPair) + sizeof(size_t)) );
/**
* Assert will fail if there isn't sufficient FreeRTOS heap available for the
* semaphore to be created successfully.
* Asserts will fail if there isn't sufficient FreeRTOS heap available for the
* buffers to be created successfully.
*/
assert_param(tim1_irq_buffer != NULL);
assert_param(tim1_irq_backlog != NULL);
assert_param(tim1_filtered_buffer != NULL);
/* USER CODE END 1 */

/* MCU Configuration--------------------------------------------------------*/
Expand Down Expand Up @@ -153,7 +197,7 @@ int main(void)
* initialization
* @note This is mostly useful while using GDB for debugging
*/
xMessageBufferReset(tim1_irq_buffer);
xMessageBufferReset(tim1_irq_backlog);

/* USER CODE END 2 */

Expand All @@ -180,6 +224,9 @@ int main(void)
/* creation of defaultTask */
defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);

/* creation of inputFilter */
inputFilterHandle = osThreadNew(InputFilterTask, NULL, &inputFilter_attributes);

/* USER CODE BEGIN RTOS_THREADS */
/* add threads, ... */
/* USER CODE END RTOS_THREADS */
Expand Down Expand Up @@ -437,29 +484,29 @@ static void MX_GPIO_Init(void)
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN 5 */
RegCCR evt = { 0, 0 };
CCRPair evt = { 0, 0 };

/* Infinite loop */
for(;;)
{
/** If > 0, we've received a message and it was stored to `evt` */
size_t recv_ret = xMessageBufferReceive(tim1_irq_buffer, &evt,
sizeof(evt), portMAX_DELAY);
// Continue waiting if the specified timeout expired
/** @todo Decrease timeout & trigger failsafe if reached */
if (recv_ret == 0) {
continue;
}

/** Calculated input PWM frequency */
uint32_t pwm_freq = HAL_RCC_GetSysClockFreq() / (TIM1->PSC + 1) / evt.CCR2;
/** Ignore bogus input frequencies (outside of the PWM spec) */
if (pwm_freq > 28000 || pwm_freq < 21000) {
continue;
}

/** Calculated input PWM duty cycle as percentage */
float pwm_duty = (float) (evt.CCR2 - evt.CCR1) / (evt.CCR2 / 100.0f);
/** If > 0, we've received a message and it was stored to `evt` */
size_t recv = xMessageBufferReceive(tim1_filtered_buffer,
&evt, sizeof(evt), portMAX_DELAY);

if (recv == 0) {
// Continue waiting if the specified timeout expired
continue;
}

/** Calculated input PWM frequency */
uint32_t pwm_freq = HAL_RCC_GetSysClockFreq() / (TIM1->PSC + 1) / evt.CCR2;
/** Ignore bogus input frequencies (outside of the PWM spec) */
if (pwm_freq > 28000 || pwm_freq < 21000) {
continue;
}

/** Calculated input PWM duty cycle as percentage */
float pwm_duty = (evt.CCR2 - evt.CCR1) / (evt.CCR2 / 100.0f);
// Invert the duty cycle, as it's polarity is reversed (low instead of high)
pwm_duty = 100.0f - pwm_duty;

Expand All @@ -475,7 +522,7 @@ void StartDefaultTask(void *argument)
(point.out_end - point.out_start) * (pwm_duty - point.in_start) /
(point.in_end - point.in_start)
);

setDutyCycle(&htim2, TIM_CHANNEL_1, target);

break;
Expand All @@ -490,6 +537,86 @@ void StartDefaultTask(void *argument)
/* USER CODE END 5 */
}

/* USER CODE BEGIN Header_InputFilterTask */
/**
* @brief Function implementing the inputFilter thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_InputFilterTask */
void InputFilterTask(void *argument)
{
/* USER CODE BEGIN InputFilterTask */
CCRPair data[TIM1_IRQ_BUFFER_LENGTH] = {0};
CCRPair buffer = {0, 0};
CCRPair sum = {0, 0};

CCRPair* head_ptr = data;

size_t recv = 0;
size_t n_entries = 0;

/** Used to optimize CPU cycles when no action is required */
uint32_t cycle_ms = portMAX_DELAY;

/* Infinite loop */
for(;;)
{
// If > 0, we've received a message and it was stored to the buffer
recv = xMessageBufferReceive(tim1_irq_backlog, &buffer,
sizeof(CCRPair), cycle_ms);

if (!recv && n_entries <= 1) {
// Wait for a message indefinitely
cycle_ms = portMAX_DELAY;

continue;
} else {
cycle_ms = pdMS_TO_TICKS(
TIM1_IRQ_BUFFER_LIFESPAN / TIM1_IRQ_BUFFER_LENGTH );
}

// Remove the entry that is about to get overwritten from the sum
sum = (CCRPair) {
sum.CCR1 - head_ptr->CCR1,
sum.CCR2 - head_ptr->CCR2
};

if (!recv) {
// Clear the current entry if no message was received
*head_ptr = (CCRPair) {0, 0};
// Decrement the number of available entries
n_entries--;
} else {
// Append the new entry to the buffer & the rolling sum
*head_ptr = buffer;

sum = (CCRPair) {
sum.CCR1 + head_ptr->CCR1,
sum.CCR2 + head_ptr->CCR2
};

// Only increment the number of entries if the buffer isn't already full
if (n_entries < TIM1_IRQ_BUFFER_LENGTH) {
n_entries++;
}
}

// Push the head_ptr address forward & check for bounds
if (++head_ptr == &data[TIM1_IRQ_BUFFER_LENGTH]) {
head_ptr = data;
}

// Send new input value to the main task
CCRPair avg = {
sum.CCR1 / n_entries,
sum.CCR2 / n_entries
};
xMessageBufferSend(tim1_filtered_buffer, &avg, sizeof(CCRPair), 0);
}
/* USER CODE END InputFilterTask */
}

/**
* @brief Period elapsed callback in non blocking mode
* @note This function is called when TIM4 interrupt took place, inside
Expand Down
10 changes: 5 additions & 5 deletions src/Core/Src/stm32f1xx_it.c
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
/* USER CODE BEGIN PV */

/** Stores the last known value of the TIM1 CCRx registers */
RegCCR TIM1_last = { 0, 0 };
CCRPair TIM1_last = { 0, 0 };

/* USER CODE END PV */

Expand All @@ -66,7 +66,7 @@ extern TIM_HandleTypeDef htim4;
/* USER CODE BEGIN EV */

/** Buffer for TIM1 PWM input changes */
extern MessageBufferHandle_t tim1_irq_buffer;
extern MessageBufferHandle_t tim1_irq_backlog;

/* USER CODE END EV */

Expand Down Expand Up @@ -183,13 +183,13 @@ void TIM1_CC_IRQHandler(void)
* @note CCR2 value changes only in relation to the PWM signal's frequency
*/
if (TIM1->CCR1 != TIM1_last.CCR1 || TIM1->CCR2 != TIM1_last.CCR2) {
TIM1_last = (RegCCR) {
TIM1_last = (CCRPair) {
TIM1->CCR1,
TIM1->CCR2
};

xMessageBufferSendFromISR(tim1_irq_buffer, &TIM1_last,
sizeof(RegCCR), NULL);
xMessageBufferSendFromISR(tim1_irq_backlog, &TIM1_last,
sizeof(CCRPair), NULL);
}

/* USER CODE END TIM1_CC_IRQn 0 */
Expand Down
5 changes: 3 additions & 2 deletions src/HP-server-silencer.ioc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#MicroXplorer Configuration settings - do not modify
FREERTOS.IPParameters=Tasks01
FREERTOS.Tasks01=defaultTask,24,128,StartDefaultTask,Default,NULL,Dynamic,NULL,NULL
FREERTOS.FootprintOK=true
FREERTOS.IPParameters=Tasks01,FootprintOK
FREERTOS.Tasks01=defaultTask,25,128,StartDefaultTask,Default,NULL,Dynamic,NULL,NULL;inputFilter,24,128,InputFilterTask,Default,NULL,Dynamic,NULL,NULL
File.Version=6
GPIO.groupedBy=
KeepUserPlacement=false
Expand Down
2 changes: 1 addition & 1 deletion src/Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
##########################################################################################################################
# File automatically-generated by tool: [projectgenerator] version: [3.14.1] date: [Sat Oct 09 12:16:50 CEST 2021]
# File automatically-generated by tool: [projectgenerator] version: [3.14.1] date: [Tue Oct 12 00:55:59 CEST 2021]
##########################################################################################################################

# ------------------------------------------------
Expand Down

0 comments on commit 3515fb2

Please sign in to comment.