Phipps Electronics

Order within the next 

FREE SHIPPING OVER $199

50,000+ ORDERS

WORLDWIDE SHIPPING

SSL SECURED

Fading LEDs with Binary Semaphores in STM32: Part 2

Contents

This blog continues from fading a single LED to two LEDs in a sequence on an STM32 using alternating binary semaphores.

Introduction

Last time, you were able to fade a single LED in sequence using your STM32s and the STM32CubeIDE using only a binary semaphore as an inter-task signaling mechanism. This time, you’ll learn how to fade two LEDs in an alternate fashion using more than one binary semaphore. This process should make you appreciate how binary semaphores work in code.

Circuit Setup

You’ll be using the same circuit setup as in part 1 of this blog. This is composed of two LEDs with limiting resistors connected to Channels 1 and 2 of Timer 1.

fading LEDs using STM32 binary semaphore

STM32CubeIDE Setup

Setting up the Timers

The code setup is almost the same previously. For the timer, you’ll have to setup Timer1 as PWM using CH1 and CH2 same as before. 

Setting up the Tasks

However, this time around, you name the two tasks as StartLED1FadeTask and StartLED2FadeTask: each task for each LEDs. 

StartLED1FadeTask and StartLED2FadeTask

Setting up the Semaphores

Here, you are going to use two binary semaphores instead of just one, mBinarySem01 and myBinarySem02. The reason for this will be apparent as you make the code.

myBinarySemaphore1 and 2

Code Setup

This is the generated code for main.c.

				
					/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2024 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "cmsis_os.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/
TIM_HandleTypeDef htim1;

osThreadId defaultTaskHandle;
osThreadId myLED1FadeTaskHandle;
osThreadId myLED2FadeTaskHandle;
osSemaphoreId myBinarySem01Handle;
osSemaphoreId myBinarySem02Handle;
/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_TIM1_Init(void);
void StartDefaultTask(void const * argument);
void StartLED1FadeTask(void const * argument);
void StartLED2FadeTask(void const * argument);

/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_TIM1_Init();
  /* USER CODE BEGIN 2 */
  HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
  HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2);
  /* USER CODE END 2 */

  /* USER CODE BEGIN RTOS_MUTEX */
  /* add mutexes, ... */
  /* USER CODE END RTOS_MUTEX */

  /* Create the semaphores(s) */
  /* definition and creation of myBinarySem01 */
  osSemaphoreDef(myBinarySem01);
  myBinarySem01Handle = osSemaphoreCreate(osSemaphore(myBinarySem01), 1);

  /* definition and creation of myBinarySem02 */
  osSemaphoreDef(myBinarySem02);
  myBinarySem02Handle = osSemaphoreCreate(osSemaphore(myBinarySem02), 1);

  /* USER CODE BEGIN RTOS_SEMAPHORES */
  /* add semaphores, ... */
  osSemaphoreWait(myBinarySem01Handle, 1);
  osSemaphoreWait(myBinarySem02Handle, 1);
  /* USER CODE END RTOS_SEMAPHORES */

  /* USER CODE BEGIN RTOS_TIMERS */
  /* start timers, add new ones, ... */
  /* USER CODE END RTOS_TIMERS */

  /* USER CODE BEGIN RTOS_QUEUES */
  /* add queues, ... */
  /* USER CODE END RTOS_QUEUES */

  /* Create the thread(s) */
  /* definition and creation of defaultTask */
  osThreadDef(defaultTask, StartDefaultTask, osPriorityNormal, 0, 128);
  defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);

  /* definition and creation of myLED1FadeTask */
  osThreadDef(myLED1FadeTask, StartLED1FadeTask, osPriorityNormal, 0, 128);
  myLED1FadeTaskHandle = osThreadCreate(osThread(myLED1FadeTask), NULL);

  /* definition and creation of myLED2FadeTask */
  osThreadDef(myLED2FadeTask, StartLED2FadeTask, osPriorityNormal, 0, 128);
  myLED2FadeTaskHandle = osThreadCreate(osThread(myLED2FadeTask), NULL);

  /* USER CODE BEGIN RTOS_THREADS */
  /* add threads, ... */
  /* USER CODE END RTOS_THREADS */

  /* Start scheduler */
  osKernelStart();

  /* We should never get here as control is now taken by the scheduler */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}

/**
  * @brief TIM1 Initialization Function
  * @param None
  * @retval None
  */
static void MX_TIM1_Init(void)
{

  /* USER CODE BEGIN TIM1_Init 0 */

  /* USER CODE END TIM1_Init 0 */

  TIM_ClockConfigTypeDef sClockSourceConfig = {0};
  TIM_MasterConfigTypeDef sMasterConfig = {0};
  TIM_OC_InitTypeDef sConfigOC = {0};
  TIM_BreakDeadTimeConfigTypeDef sBreakDeadTimeConfig = {0};

  /* USER CODE BEGIN TIM1_Init 1 */

  /* USER CODE END TIM1_Init 1 */
  htim1.Instance = TIM1;
  htim1.Init.Prescaler = 16;
  htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim1.Init.Period = 100;
  htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim1.Init.RepetitionCounter = 0;
  htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
  if (HAL_TIM_Base_Init(&htim1) != HAL_OK)
  {
    Error_Handler();
  }
  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  if (HAL_TIM_ConfigClockSource(&htim1, &sClockSourceConfig) != HAL_OK)
  {
    Error_Handler();
  }
  if (HAL_TIM_PWM_Init(&htim1) != HAL_OK)
  {
    Error_Handler();
  }
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  if (HAL_TIMEx_MasterConfigSynchronization(&htim1, &sMasterConfig) != HAL_OK)
  {
    Error_Handler();
  }
  sConfigOC.OCMode = TIM_OCMODE_PWM1;
  sConfigOC.Pulse = 0;
  sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
  sConfigOC.OCNPolarity = TIM_OCNPOLARITY_HIGH;
  sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
  sConfigOC.OCIdleState = TIM_OCIDLESTATE_RESET;
  sConfigOC.OCNIdleState = TIM_OCNIDLESTATE_RESET;
  if (HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_1) != HAL_OK)
  {
    Error_Handler();
  }
  if (HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_2) != HAL_OK)
  {
    Error_Handler();
  }
  sBreakDeadTimeConfig.OffStateRunMode = TIM_OSSR_DISABLE;
  sBreakDeadTimeConfig.OffStateIDLEMode = TIM_OSSI_DISABLE;
  sBreakDeadTimeConfig.LockLevel = TIM_LOCKLEVEL_OFF;
  sBreakDeadTimeConfig.DeadTime = 0;
  sBreakDeadTimeConfig.BreakState = TIM_BREAK_DISABLE;
  sBreakDeadTimeConfig.BreakPolarity = TIM_BREAKPOLARITY_HIGH;
  sBreakDeadTimeConfig.AutomaticOutput = TIM_AUTOMATICOUTPUT_DISABLE;
  if (HAL_TIMEx_ConfigBreakDeadTime(&htim1, &sBreakDeadTimeConfig) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN TIM1_Init 2 */

  /* USER CODE END TIM1_Init 2 */
  HAL_TIM_MspPostInit(&htim1);

}

/**
  * @brief GPIO Initialization Function
  * @param None
  * @retval None
  */
static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
/* USER CODE BEGIN MX_GPIO_Init_1 */
/* USER CODE END MX_GPIO_Init_1 */

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOC_CLK_ENABLE();
  __HAL_RCC_GPIOD_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);

  /*Configure GPIO pin : LED_Pin */
  GPIO_InitStruct.Pin = LED_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(LED_GPIO_Port, &GPIO_InitStruct);

/* USER CODE BEGIN MX_GPIO_Init_2 */
/* USER CODE END MX_GPIO_Init_2 */
}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/* USER CODE BEGIN Header_StartDefaultTask */
/**
  * @brief  Function implementing the defaultTask thread.
  * @param  argument: Not used
  * @retval None
  */
/* USER CODE END Header_StartDefaultTask */
void StartDefaultTask(void const * argument)
{
  /* USER CODE BEGIN 5 */
	osSemaphoreRelease(myBinarySem01Handle);
  /* Infinite loop */
  for(;;)
  {
    osDelay(1);
  }
  /* USER CODE END 5 */
}

/* USER CODE BEGIN Header_StartLED1FadeTask */
/**
* @brief Function implementing the myLED1FadeTask thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartLED1FadeTask */
void StartLED1FadeTask(void const * argument)
{
  /* USER CODE BEGIN StartLED1FadeTask */
	uint32_t dutyCycle;
	uint32_t pulseLength;

	/* Infinite loop */
  for(;;)
  {
	  // Wait for release from StartLED2FadeTask end
	  osSemaphoreWait(myBinarySem01Handle, osWaitForever);

	  // Fade Up Phase
	  for(dutyCycle=1;dutyCycle<100;dutyCycle++)
	  {
		  pulseLength = (htim1.Init.Period + 1) * dutyCycle / 100 - 1;
		  __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, pulseLength);
		  osDelay(50);
	  }

	  // Fade Down Phase
	  for(dutyCycle=100;dutyCycle>0;dutyCycle--)
	  {
		  pulseLength = (htim1.Init.Period + 1) * dutyCycle / 100 - 1;
		  __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, pulseLength);
		  osDelay(50);
	  }

	  // Release to StartLED2FadeTask begin
	  osSemaphoreRelease(myBinarySem02Handle);
  }

  /* USER CODE END StartLED1FadeTask */
}

/* USER CODE BEGIN Header_StartLED2FadeTask */
/**
* @brief Function implementing the myLED2FadeTask thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartLED2FadeTask */
void StartLED2FadeTask(void const * argument)
{
  /* USER CODE BEGIN StartLED2FadeTask */
	uint32_t dutyCycle;
	uint32_t pulseLength;

	/* Infinite loop */
  for(;;)
  {
	  // Wait for release from StartLED1FadeTask end
	  osSemaphoreWait(myBinarySem02Handle, osWaitForever);

	  // Fade Up Phase
	  for(dutyCycle=1;dutyCycle<100;dutyCycle++)
	  {
		  pulseLength = (htim1.Init.Period + 1) * dutyCycle / 100 - 1;
		  __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, pulseLength);
		  osDelay(50);
	  }

	  // Fade Down Phase
	  for(dutyCycle=100;dutyCycle>0;dutyCycle--)
	  {
		  pulseLength = (htim1.Init.Period + 1) * dutyCycle / 100 - 1;
		  __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, pulseLength);
		  osDelay(50);
	  }

	  // Release to StartLED1FadeTask begin
	  osSemaphoreRelease(myBinarySem01Handle);
  }
  /* USER CODE END StartLED2FadeTask */
}

/**
  * @brief  Period elapsed callback in non blocking mode
  * @note   This function is called  when TIM4 interrupt took place, inside
  * HAL_TIM_IRQHandler(). It makes a direct call to HAL_IncTick() to increment
  * a global variable "uwTick" used as application time base.
  * @param  htim : TIM handle
  * @retval None
  */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  /* USER CODE BEGIN Callback 0 */

  /* USER CODE END Callback 0 */
  if (htim->Instance == TIM4) {
    HAL_IncTick();
  }
  /* USER CODE BEGIN Callback 1 */

  /* USER CODE END Callback 1 */
}

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

				
			

Each PWM channel is started through the HAL_TIM_PWM_Start(&htim, TIM_CHANNEL_x) API.

				
					  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_TIM1_Init();
  /* USER CODE BEGIN 2 */
  HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
  HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2);
  /* USER CODE END 2 */

				
			

Next, the binary semaphores are created, myBinarySem01 and myBinarySem02. To ensure that these semaphores are depleted, two osSemaphoreWait() commands are executed.

				
					  /* Create the semaphores(s) */
  /* definition and creation of myBinarySem01 */
  osSemaphoreDef(myBinarySem01);
  myBinarySem01Handle = osSemaphoreCreate(osSemaphore(myBinarySem01), 1);

  /* definition and creation of myBinarySem02 */
  osSemaphoreDef(myBinarySem02);
  myBinarySem02Handle = osSemaphoreCreate(osSemaphore(myBinarySem02), 1);

  /* USER CODE BEGIN RTOS_SEMAPHORES */
  /* add semaphores, ... */
  osSemaphoreWait(myBinarySem01Handle, 1);
  osSemaphoreWait(myBinarySem02Handle, 1);
  /* USER CODE END RTOS_SEMAPHORES */

				
			

The StartDefaultTask() has an osSemaphoreRelease() statement that releases (or gives) myBinarySemaphore01. This ensures the entire process starts somewhere.

				
					/* USER CODE BEGIN Header_StartDefaultTask */
/**
  * @brief  Function implementing the defaultTask thread.
  * @param  argument: Not used
  * @retval None
  */
/* USER CODE END Header_StartDefaultTask */
void StartDefaultTask(void const * argument)
{
  /* USER CODE BEGIN 5 */
	osSemaphoreRelease(myBinarySem01Handle);
  /* Infinite loop */
  for(;;)
  {
    osDelay(1);
  }
  /* USER CODE END 5 */
}
				
			

For the LED tasks, two LED Fade tasks are created, StartLED1FadeTask() and StartLED2FadeTask(). Note that each task already incorporates a fade up and fade down event, unlike the first part which does the fade up and fade down separately.

Inside the StartLED1FadeTask(), you’ll see an osSemaphoreWait() function waiting for a token from myBinarySemaphore01. Congruently, at the end of the fade up and fade down sequence, you’ll see an osSemaphoreRelease() for myBinarySemaphore02.

				
					/* USER CODE END Header_StartLED1FadeTask */
void StartLED1FadeTask(void const * argument)
{
  /* USER CODE BEGIN StartLED1FadeTask */
	uint32_t dutyCycle;
	uint32_t pulseLength;

	/* Infinite loop */
  for(;;)
  {
	  // Wait for release from StartLED2FadeTask end
	  osSemaphoreWait(myBinarySem01Handle, osWaitForever);

	  // Fade Up Phase
	  for(dutyCycle=1;dutyCycle<100;dutyCycle++)
	  {
		  pulseLength = (htim1.Init.Period + 1) * dutyCycle / 100 - 1;
		  __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, pulseLength);
		  osDelay(50);
	  }

	  // Fade Down Phase
	  for(dutyCycle=100;dutyCycle>0;dutyCycle--)
	  {
		  pulseLength = (htim1.Init.Period + 1) * dutyCycle / 100 - 1;
		  __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, pulseLength);
		  osDelay(50);
	  }

	  // Release to StartLED2FadeTask begin
	  osSemaphoreRelease(myBinarySem02Handle);
  }

				
			

Inside the StartLED2FadeTask(), you’ll see an osSemaphoreWait() function waiting for a token from myBinarySemaphore02. Congruently, at the end of the fade up and fade down sequence, you’ll see an osSemaphoreRelease() for myBinarySemaphore01.

				
					/* USER CODE BEGIN Header_StartLED2FadeTask */
/**
* @brief Function implementing the myLED2FadeTask thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartLED2FadeTask */
void StartLED2FadeTask(void const * argument)
{
  /* USER CODE BEGIN StartLED2FadeTask */
	uint32_t dutyCycle;
	uint32_t pulseLength;

	/* Infinite loop */
  for(;;)
  {
	  // Wait for release from StartLED1FadeTask end
	  osSemaphoreWait(myBinarySem02Handle, osWaitForever);

	  // Fade Up Phase
	  for(dutyCycle=1;dutyCycle<100;dutyCycle++)
	  {
		  pulseLength = (htim1.Init.Period + 1) * dutyCycle / 100 - 1;
		  __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, pulseLength);
		  osDelay(50);
	  }

	  // Fade Down Phase
	  for(dutyCycle=100;dutyCycle>0;dutyCycle--)
	  {
		  pulseLength = (htim1.Init.Period + 1) * dutyCycle / 100 - 1;
		  __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, pulseLength);
		  osDelay(50);
	  }

	  // Release to StartLED1FadeTask begin
	  osSemaphoreRelease(myBinarySem01Handle);
  }
  /* USER CODE END StartLED2FadeTask */
}
				
			

If you were to map the two binary semaphore inter-task synchronization scheme, you’ll end up with the image below.

The map implies that:

StartDefaultTask() begins the first fade sequence by giving or releasing a semaphore (myBinarySemaphore01 here).

When StartLED1FadeTask() ends StartLED2FadeTask() starts by releasing and waiting for the myBinarySempahore01 token.

when StartLED2FadeTask() ends, StartLED1FadeTask() starts by releasing and waiting for the myBinarySempahore02token.

Cicuit in Action

To see the behavior clearly, see the circuit in action below. This is just a use case of using binary semaphores in an STM32.

SUBSCRIBE FOR NEW POST ALERTS

Subscribe to be the first to know when we publish a new article!
List Subscriptions(Required)

POPULAR POSTS

Scroll to Top