C语言实现呼吸灯

1,234 阅读2分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

1. 呼吸灯原理

呼吸灯的实现可以通过控制灯的亮度连续变化,当变化的频率大于24帧时,肉眼看上去就会逐渐变暗,逐渐变亮。

2. PWM控制亮度

PWM通过设置亮度在一段时间内的占空比,亮的百分比多,人眼看到的就亮,反之就是暗。
关于PWM的块不打算展开说,这里针对呼吸灯的PWM详细说明。

/*
 * 描述  :呼吸灯PWM初始化
 * 参数  :
 *        无
 * 返回  :
 *        无
 */
void bspBreathLedTIMInit(void)
{
  TIM_ClockConfigTypeDef sClockSourceConfig = {0};
  TIM_MasterConfigTypeDef sMasterConfig = {0};
  TIM_OC_InitTypeDef sConfigOC = {0};

  g_breathled_tim_handle.Instance = TIM2;
  g_breathled_tim_handle.Init.Prescaler = 83;
  g_breathled_tim_handle.Init.CounterMode = TIM_COUNTERMODE_UP;
  g_breathled_tim_handle.Init.Period = (BREATHLED_PWM_VALUE - 1);
  g_breathled_tim_handle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  g_breathled_tim_handle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
  if (HAL_TIM_Base_Init(&g_breathled_tim_handle) != HAL_OK)
  {
    Error_Handler();
  }
  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  if (HAL_TIM_ConfigClockSource(&g_breathled_tim_handle, &sClockSourceConfig) != HAL_OK)
  {
    Error_Handler();
  }
  if (HAL_TIM_PWM_Init(&g_breathled_tim_handle) != HAL_OK)
  {
    Error_Handler();
  }
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  if (HAL_TIMEx_MasterConfigSynchronization(&g_breathled_tim_handle, &sMasterConfig) != HAL_OK)
  {
    Error_Handler();
  }
  sConfigOC.OCMode = TIM_OCMODE_PWM1;
  sConfigOC.Pulse = 0;
  sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
  sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
  if (HAL_TIM_PWM_ConfigChannel(&g_breathled_tim_handle, &sConfigOC, TIM_CHANNEL_4) != HAL_OK)
  {
    Error_Handler();
  }

  GPIO_InitTypeDef GPIO_InitStruct;
  BREATHLED_GPIO_CLK_ENABLE();
  /* TIM2 GPIO Configuration
   * PA3     ------> TIM2_CH4
   */
  GPIO_InitStruct.Pin = BREATHLED_GPIO_GREEN_PIN;
  GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
  GPIO_InitStruct.Alternate = GPIO_AF1_TIM2;
  HAL_GPIO_Init(BREATHLED_GPIO_TYPE, &GPIO_InitStruct);

  HAL_TIM_Base_Start_IT(&g_breathled_tim_handle);
}

我这里使用了PA3引脚,定时器2做实验。时钟频率是84MHz,Prescaler设置(84 - 1) = 83,Period这里设置是(10000 - 1) = 9999,所以定时器是 (83 + 1) * (9999 + 1) / 84000000 = 0.01s = 10ms一个周期。
这个频率各位可以根据实际情况去计算,尽量设置成看上去平滑,而且对系统也不会造成产生过多中断。

/*
 * 描述  :定时器底层回调初始化
 * 参数  :
 *        无
 * 返回  :
 *        无
 */
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim_base)
{
  if (htim_base->Instance == TIM2)
  {
    __HAL_RCC_TIM2_CLK_ENABLE();

    HAL_NVIC_SetPriority(TIM2_IRQn, 5, 0);
    HAL_NVIC_EnableIRQ(TIM2_IRQn);
  }
}

/*
 * 描述  :定时器中断处理函数
 * 参数  :
 *        无
 * 返回  :
 *        无
 */
void TIM2_IRQHandler(void)
{
  HAL_TIM_IRQHandler(&g_breathled_tim_handle);	
  HAL_TIM_PeriodElapsedCallback(&g_breathled_tim_handle);	
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  /* USER CODE BEGIN Callback 0 */

  /* USER CODE END Callback 0 */
  if (htim->Instance == TIM11) {
    HAL_IncTick();
  }
  //呼吸灯
  else if (htim->Instance == TIM2)
  {
    //注意由于产生中断过快,反转实现不了
    //LED电平反转
    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_3);
  }
  /* USER CODE BEGIN Callback 1 */

  /* USER CODE END Callback 1 */
}

上面代码是中断处理函数,对呼吸灯的引脚电平反转。是通过设置占空比的参数,定时器自动触发中断后反转电平。

3. 呼吸灯亮度曲线

//PWM设置的Period值
#define BREATHLED_PWM_VALUE                10000
//呼吸灯x轴变暗或变亮步数
#define BREATHLED_X_DIVIDE                 100
//呼吸灯x轴总步数
#define BREATHLED_X_TOTAL                 (BREATHLED_X_DIVIDE * 2)
//呼吸灯y轴比例
#define BREATHLED_Y_RATIO                 (BREATHLED_PWM_VALUE / BREATHLED_X_DIVIDE)
//亮度变化频率,也就是帧数,单位毫秒
#define BREATHLED_FRAMES_MS											10

一些宏定义,BREATHLED_PWM_VALUE值是上面PWM设置的Period值。
BREATHLED_X_DIVIDE是x轴,变暗或变亮时,x轴的步数。
BREATHLED_X_TOTAL是x轴变暗加上变亮一个周期的总步数。
BREATHLED_Y_RATIO是y轴比例。
BREATHLED_FRAMES_MS亮度变化频率,注意这个值必须大于24帧,也就是小于40毫秒的时间,为了更加平滑,可以适量提高帧数,这样看起来呼吸灯更加流畅。

3.1 线性折线

image.png y = 10000时代表占空比100%,x轴为时间。

/*
 * 描述  :呼吸灯函数,由x轴得出y轴值
 * 参数  :
 *        [in]  x     x轴,时间
 * 返回  :
 *        y轴值,PWM值
 */
static unsigned int breathLedCurve(unsigned int x)
{
  unsigned int y;
  if (x < BREATHLED_X_DIVIDE)
  {
    y = BREATHLED_Y_RATIO * x;
  }
  else
  {
    y = -BREATHLED_Y_RATIO * (x - BREATHLED_X_TOTAL);
  }
  return y;
}

/*
 * 描述  :呼吸灯过程
 * 参数  :
 *        无
 * 返回  :
 *        无
 */
void breathLedProgress(void)
{
  static unsigned int time_x = 0;
  unsigned int pwm_value_y = 0;

  while (1)
  {
    time_x = (time_x + 1) % BREATHLED_X_TOTAL;
    pwm_value_y = breathLedCurve(time_x);

    //修改占空比
    __HAL_TIM_SetCompare(&g_breathled_tim_handle, TIM_CHANNEL_4, pwm_value_y);
            
     sysDelay(BREATHLED_FRAMES_MS);

    if (time_x % BREATHLED_X_DIVIDE == 0)
    {
      sysDelay(500);
    }
  }
}

实现方式可以参考上面的,但是人眼感受的却不是线性的,是由于在灯光微亮区,很小的光通量改变也让人眼感到光通量变化很大,而在光通量比较大的区域,很大的光通量跳跃,人眼感觉到的光通量变化不大,简单理解为,人眼对亮度暗的比较敏感,而对亮度量的不敏感。
所以你尝试后发现,当由亮变暗时感觉时间长,由暗变亮时时间短,会有突然变亮的感觉。
这个曲线是有缺陷的。

3.2 呼吸灯曲线

image.png 由上图可知,当亮度由暗变亮时,程序的呼吸灯曲线需要如同图3。
为了方便我这里使用一元二次曲线,有条件的可以使用sin函数,但是效果或许看起来会差不多。但是sin函数计算量会比一元二次方程大的多,所以衡量之下,选择通过一元二次曲线来实现。

image.png 通过这个曲线,你可能感觉到和线性折线差不多的效果。所以还需要降低由暗变亮时的亮度变化频率,即可让人眼看上去平滑。
注意的是,单单这个曲线可能看上去效果不明显,这时我们把由暗到亮的亮度变化频率降低,让人眼感觉到,所以在由暗到亮过程中,亮度变化后延迟时间改为由亮变暗的2倍。

/*
 * 描述  :呼吸灯函数,由x轴得出y轴值
 * 参数  :
 *        [in]  x     x轴,时间
 * 返回  :
 *        y轴值,PWM值
 */
static unsigned int breathLedCurve(unsigned int x)
{
  unsigned int y;
  if (x < BREATHLED_X_DIVIDE)
  {
    y = x * x;
  }
  else
  {
    y = BREATHLED_Y_RATIO * (BREATHLED_X_TOTAL - x);
  }
  return y;
}

/*
 * 描述  :呼吸灯过程
 * 参数  :
 *        无
 * 返回  :
 *        无
 */
void breathLedProgress(void)
{
  static unsigned int time_x = 0;
  unsigned int pwm_value_y = 0;

  while (1)
  {
    time_x = (time_x + 1) % BREATHLED_X_TOTAL;
    pwm_value_y = breathLedCurve(time_x);

    //修改占空比
    __HAL_TIM_SetCompare(&g_breathled_tim_handle, TIM_CHANNEL_4, pwm_value_y);
    
    //由暗变亮时,延时时间需要比较长
    if (time_x < BREATHLED_X_DIVIDE)
    {
      sysDelay(BREATHLED_FRAMES_MS * 2);
    }
    else
    {
      sysDelay(BREATHLED_FRAMES_MS);
    }

    if (time_x % BREATHLED_X_DIVIDE == 0)
    {
      sysDelay(500);
    }
  }
}

3.3 gama曲线

gama曲线是一种特殊的色调曲线,具体可以网上找资料了解。我们把gamma曲线的值弄成数组,程序可以减少计算的消耗。
最后在我这的硬件测试,使用了一下的代码效果是最好的,各位可以根据实际情况,调整步数和变化的频率,可各位一个参考。

//呼吸灯x轴总步数
#define BREATH_LED_X_TOTAL  						27
//亮度变化频率,也就是帧数,单位毫秒
#define BREATH_LED_FRAMES_MS						50

static int g_breath_led_time_x = BREATH_LED_X_TOTAL;
//pwm的gamma值
static const unsigned short breath_led_pwmtable[BREATH_LED_X_TOTAL] =
{
  /* 0, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 4, 4, 5, 5, 6, 6, 7, 8, 9, 10,
  11, 12, 13, 15, 17, 19, 21, 23, 26, 29, 32, 36, 40, 44, */
  49, 55,
  61, 68, 76, 85, 94, 105, 117, 131, 146, 162, 181, 202, 225, 250,
  279, 311, 346, 386, 430, 479, 534, 595, 663, 739, 824
  /*, 918, 1023 */
};

/*
 * 描述  :呼吸灯过程
 * 参数  :
 *        无
 * 返回  :
 *        无
 */
void breathLedProgress(void)
{
  unsigned int pwm_value_y = 0;

  if (g_breath_led_time_x == BREATH_LED_X_TOTAL)
  {
    for (g_breath_led_time_x = BREATH_LED_X_TOTAL - 1; g_breath_led_time_x >= 0; g_breath_led_time_x--)
    {
      pwm_value_y = breath_led_pwmtable[g_breath_led_time_x];
      //修改占空比
      __HAL_TIM_SetCompare(&g_indicaotr_tim_handle, TIM_CHANNEL_4, pwm_value_y);
      //状态不在充电中,返回
      if (g_system_state != SYSTEM_STATE_CHARGING)
      {
        g_breath_led_time_x = BREATH_LED_X_TOTAL;
        return;
      }

      sysDelay(BREATH_LED_FRAMES_MS + g_breath_led_time_x / 8);
    }

    g_breath_led_time_x = 0;
    return;
  }

  if (g_breath_led_time_x == 0)
  {
    for (; g_breath_led_time_x < BREATH_LED_X_TOTAL; g_breath_led_time_x++)
    {
      pwm_value_y = breath_led_pwmtable[g_breath_led_time_x];
      //修改占空比
      __HAL_TIM_SetCompare(&g_indicaotr_tim_handle, TIM_CHANNEL_4, pwm_value_y);
      //状态不在充电中,返回
      if (g_system_state != SYSTEM_STATE_CHARGING)
      {
        g_breath_led_time_x = BREATH_LED_X_TOTAL;
        return;
      }

      sysDelay(BREATH_LED_FRAMES_MS);
    }

    return;
  }
}