STM32通用低功耗组件——PM

2020-07-13 16:53:38 浏览数 (2)

电源管理组件

嵌入式系统低功耗管理的目的在于满足用户对性能需求的前提下,尽可能降低系统能耗以延长设备待机时间。高性能与有限的电池能量在嵌入式系统中矛盾最为突出,硬件低功耗设计与软件低功耗管理的联合应用成为解决矛盾的有效手段。现在的各种 MCU 都或多或少的在低功耗方面提供了管理接口。比如对主控时钟频率的调整、工作电压的改变、总线频率的调整甚至关闭、外围设备工作时钟的关闭等。有了硬件上的支持,合理的软件设计就成为节能的关键,一般可以把低功耗管理分为三个类别:

  • 处理器电源管理主要实现方式:对 CPU 频率的动态管理,以及系统空闲时对工作模式的调整。
  • 设备电源管理主要实现方式:关闭个别闲置设备
  • 系统平台电源管理主要实现方式:针对特定系统平台的非常见设备具体定制。

随着物联网 (IoT) 的兴起,产品对功耗的需求越来越强烈。作为数据采集的传感器节点通常需要在电池供电时长期工作,而作为联网的 SOC 也需要有快速的响应功能和较低的功耗。

在产品开发的起始阶段,首先考虑是尽快完成产品的功能开发。在产品功能逐步完善之后,就需要加入电源管理 (Power Management,以下简称 PM) 功能。为了适应 IoT 的这种需求,RT-Thread 提供了电源管理组件。电源管理组件的理念是尽量透明,使得产品加入低功耗功能更加轻松。

PM 组件介绍

RT-Thread 的 PM 组件采用分层设计思想,分离架构和芯片相关的部分,提取公共部分作为核心。在对上层提供通用的接口同时,也让底层驱动对组件的适配变得更加简单。

主要特点

RT-Thread PM 组件主要特点如下所示:

  • 基于模式来管理功耗,空闲时动态调整工作模式,支持多个等级的休眠。
  • 对应用透明,组件在底层自动完成电源管理。
  • 支持运行模式下动态变频,根据模式自动更新设备的频率配置,确保在不同的运行模式都可以正常工作。
  • 支持设备电源管理,根据模式自动管理设备的挂起和恢复,确保在不同的休眠模式下可以正确的挂起和恢复。
  • 支持可选的休眠时间补偿,让依赖 OS Tick 的应用可以透明使用。
  • 向上层提供设备接口,如果打开了 devfs 组件,那么也可以通过文件系统接口访问。

工作原理

低功耗的本质是系统空闲时 CPU 停止工作,中断或事件唤醒后继续工作。在 RTOS 中,通常包含一个 IDLE 任务,该任务的优先级最低且一直保持就绪状态,当高优先级任务未就绪时,OS 执行 IDLE 任务。一般地,未进行低功耗处理时,CPU 在 IDLE 任务中循环执行空指令。RT-Thread 的电源管理组件在 IDLE 任务中,通过对 CPU 、时钟和设备等进行管理,从而有效降低系统的功耗。

在上图所示,当高优先级任务运行结束或被挂起时,系统将进入 IDLE 任务中。在 IDLE 任务执行后,它将判断系统是否可以进入到休眠状态(以节省功耗)。如果可以进入休眠, 将根据芯片情况关闭部分硬件模块,OS Tick 也非常有可能进入暂停状态。此时电源管理框架会根据系统定时器情况,计算出下一个超时时间点,并设置低功耗定时器,让设备能够在这个时刻点唤醒,并进行后续的工作。当系统被(低功耗定时器中断或其他唤醒中断源)唤醒后,系统也需要知道睡眠时间长度是多少,并对OS Tick 进行补偿,让系统的OS tick值调整为一个正确的值。

低功耗状态和模式

RT-Thread PM 组件将系统划分为两种状态:运行状态(RUN)和休眠状态(Sleep)。运行状态控制 CPU 的频率,适用于变频场景;休眠状态根据 SOC 特性实现休眠 CPU,以降低功耗。两种状态分别使用不同的 API 接口,独立控制。

  • 休眠状态休眠状态也就是通常意义上的低功耗状态,通过关闭外设、执行 SOC 电源管理接口,降低系统功耗。休眠状态又分为六个模式,呈现为金字塔的形式。随着模式增加,功耗逐级递减的特点。下面是休眠状态下模式的定义,开发者可根据具体的 SOC 实现相应的模式,但需要遵循功耗逐级降低的特点。
  • 运行状态运行状态通常用于改变 CPU 的运行频率,独立于休眠模式。当前运行状态划分了四个等级:高速、正常、中速、低速,如下:

PM组件的实现接口

在 RT-Thrad PM 组件中,外设或应用通过投票机制对所需的功耗模式进行投票,当系统空闲时,根据投票数决策出合适的功耗模式,调用抽象接口,控制芯片进入低功耗状态,从而降低系统功耗。当未进行进行任何投票时,会以默认模式进入(通常为空闲模式)。

pm组件的控制块:

代码语言:javascript复制
static struct rt_pm _pm;

API接口:

  1. 请求休眠模式
代码语言:javascript复制
void rt_pm_request(uint8_t sleep_mode);

参数

模式

sleep_mode

请求的休眠模式等级

sleep_mode 取以下枚举值:

代码语言:javascript复制
enum
{
    /* sleep modes */
    PM_SLEEP_MODE_NONE = 0,    /* 活跃状态 */
    PM_SLEEP_MODE_IDLE,        /* 空闲模式(默认) */
    PM_SLEEP_MODE_LIGHT,       /* 轻度睡眠模式 */
    PM_SLEEP_MODE_DEEP,        /* 深度睡眠模式 */
    PM_SLEEP_MODE_STANDBY,     /* 待机模式 */
    PM_SLEEP_MODE_SHUTDOWN,    /* 关断模式 */
    PM_SLEEP_MODE_MAX,
};

调用该函数会将对应的模式计数加1,并锁住该模式。此时如果请求更低级别的功耗模式,将无法进入,只有释放(解锁)先前请求的模式后,系统才能进入更低的模式;向更高的功耗模式请求则不受此影响。该函数需要和 rt_pm_release 配合使用,用于对某一阶段或过程进行保护。下面是具体代码实现:

代码语言:javascript复制
void rt_pm_request(rt_uint8_t mode)
{
    rt_base_t level;
    struct rt_pm *pm;

    if (_pm_init_flag == 0)
        return;

    if (mode > (PM_SLEEP_MODE_MAX - 1))
        return;

    level = rt_hw_interrupt_disable();
    pm = &_pm;
    if (pm->modes[mode] < 255)
        pm->modes[mode]   ;//将对应的模式计数加1
    rt_hw_interrupt_enable(level);
}
  1. 释放休眠模式
代码语言:javascript复制
void rt_pm_release(uint8_t sleep_mode);

参数

模式

sleep_mode

释放的休眠模式等级

调用该函数会将对应的模式计数减1,配合 rt_pm_request 使用,释放先前请求的模式。下面是具体代码实现:

代码语言:javascript复制
void rt_pm_release(rt_uint8_t mode)
{
    rt_ubase_t level;
    struct rt_pm *pm;

    if (_pm_init_flag == 0)
        return;

    if (mode > (PM_SLEEP_MODE_MAX - 1))
        return;

    level = rt_hw_interrupt_disable();
    pm = &_pm;
    if (pm->modes[mode] > 0)
        pm->modes[mode] --;//将对应的模式计数减1
    rt_hw_interrupt_enable(level);
}

特殊情况下,比如某个阶段并不允许系统进入更低的功耗模式,此时可以通过 rt_pm_request 和 rt_pm_release 对该过程进行保护。如 I2C 读取数据期间,不允许进入深度睡眠模式(可能会导致外设停止工作),因此可以做如下处理:

代码语言:javascript复制
/* 请求轻度睡眠模式(I2C外设该模式下正常工作) */
rt_pm_request(PM_SLEEP_MODE_LIGHT);

/* 读取数据过程 */

/* 释放该模式 */
rt_pm_release(PM_SLEEP_MODE_LIGHT);
  1. 设置运行模式
代码语言:javascript复制
int rt_pm_run_enter(uint8_t run_mode);

参数

模式

run_mode

设置的运行模式等级

run_mode 可以取以下枚举值:

代码语言:javascript复制
enum
{
    /* run modes*/
    PM_RUN_MODE_HIGH_SPEED = 0,    /* 高速 */
    PM_RUN_MODE_NORMAL_SPEED,      /* 正常(默认) */
    PM_RUN_MODE_MEDIUM_SPEED,      /* 中速 */
    PM_RUN_MODE_LOW_SPEED,         /* 低速 */
    PM_RUN_MODE_MAX,
};

调用该函数改变 CPU 的运行频率,从而降低运行时的功耗。此函数只提供级别,具体的 CPU 频率应在移植阶段视实际情况而定。

下面是具体代码实现:

代码语言:javascript复制
int rt_pm_run_enter(rt_uint8_t mode)
{
    rt_base_t level;
    struct rt_pm *pm;

    if (_pm_init_flag == 0)
        return -RT_EIO;

    if (mode > PM_RUN_MODE_MAX)
        return -RT_EINVAL;

    level = rt_hw_interrupt_disable();
    pm = &_pm;
    if (mode < pm->run_mode)
    {
        /* change system runing mode */
        pm->ops->run(pm, mode);
        /* changer device frequency */
        _pm_device_frequency_change(mode);
    }
    else
    {
        pm->flags |= RT_PM_FREQUENCY_PENDING;
    }
    pm->run_mode = mode;
    rt_hw_interrupt_enable(level);

    return RT_EOK;
}
  1. 设置进入/退出休眠模式的回调通知
代码语言:javascript复制
void rt_pm_notify_set(void (*notify)(uint8_t event, uint8_t mode, void *data), void *data);

参数

模式

notify

应用的回调函数

data

私有数据

event 为以下两个枚举值,分别标识进入/退出休眠模式。

代码语言:javascript复制
enum
{
    RT_PM_ENTER_SLEEP = 0,    /* 进入休眠模式 */
    RT_PM_EXIT_SLEEP,         /* 退出休眠模式 */
};

在应用进入/退出休眠模式会触发回调通知。下面是具体代码实现:

代码语言:javascript复制
void rt_pm_notify_set(void (*notify)(rt_uint8_t event, rt_uint8_t mode, void *data), void *data)
{
    _pm_notify.notify = notify;
    _pm_notify.data = data;
}
  1. 注册PM设备
代码语言:javascript复制
void rt_pm_device_register(struct rt_device *device, const struct rt_device_pm_ops *ops)

参数

模式

device

device设备

ops

pm设备的三个回调函数

与应用不同,某些外设可能在进入低功耗状态时执行特定操作,退出低功耗时采取措施恢复,此时可以通过注册PM设备来实现。通过注册 PM 设备,在进入低功耗状态之前,会触发注册设备的 suspend 回调,开发者可在回调里执行自己的操作;类似地,从低功耗状态退出时,也会触发 resume 回调。运行模式下的频率改变同样会触发设备的 frequency_change 回调。下面是具体代码实现:

代码语言:javascript复制
void rt_pm_device_register(struct rt_device *device, const struct rt_device_pm_ops *ops)
{
    rt_base_t level;
    struct rt_device_pm *device_pm;

    RT_DEBUG_NOT_IN_INTERRUPT;

    level = rt_hw_interrupt_disable();

    device_pm = (struct rt_device_pm *)RT_KERNEL_REALLOC(_pm.device_pm,
                (_pm.device_pm_number   1) * sizeof(struct rt_device_pm));
    if (device_pm != RT_NULL)
    {
        _pm.device_pm = device_pm;
        _pm.device_pm[_pm.device_pm_number].device = device;
        _pm.device_pm[_pm.device_pm_number].ops    = ops;
        _pm.device_pm_number  = 1;
    }

    rt_hw_interrupt_enable(level);
}

设置进入/退出休眠模式的回调通知和注册为设备的回调通知流程:

首先应用设置进出休眠状态的回调函数,然后调用 rt_pm_request 请求休眠模式,触发休眠操作;PM 组件在系统空闲时检查休眠模式计数,根据投票数给出推荐的模式;接着 PM 组件调用 notfiy 通知应用,告知即将进入休眠模式;然后对注册的 PM 设备执行挂起操作,返回 OK 后执行 SOC 实现的的休眠模式,系统进入休眠状态(如果使能时间补偿,休眠之前会先启动低功耗定时器)。此时 CPU 停止工作,等待事件或者中断唤醒。当系统被唤醒后,由于全局中断为关闭状态,系统继续从该处执行,获取睡眠时间补偿系统的心跳,依次唤醒设备,通知应用从休眠模式退出。如此一个周期执行完毕,退出,等待系统下次空闲。

模式的切换代码实现:当任务进入到空闲线程,最终是调用此函数进入低功耗和唤醒的

代码语言:javascript复制
static void _pm_change_sleep_mode(struct rt_pm *pm, rt_uint8_t mode)
{
    rt_tick_t timeout_tick, delta_tick;
    rt_base_t level;
    int ret = RT_EOK;

    if (mode == PM_SLEEP_MODE_NONE)
    {
        pm->sleep_mode = mode;
        pm->ops->sleep(pm, PM_SLEEP_MODE_NONE);
    }
    else
    {
        level = rt_pm_enter_critical(mode);

        /* Notify app will enter sleep mode */
        if (_pm_notify.notify)
            _pm_notify.notify(RT_PM_ENTER_SLEEP, mode, _pm_notify.data);

        /* Suspend all peripheral device */
        ret = _pm_device_suspend(mode);
        if (ret != RT_EOK)
        {
            _pm_device_resume(mode);
            if (_pm_notify.notify)
                _pm_notify.notify(RT_PM_EXIT_SLEEP, mode, _pm_notify.data);
            rt_pm_exit_critical(level, mode);

            return;
        }

        /* Tickless*/
        if (pm->timer_mask & (0x01 << mode))
        {
            timeout_tick = rt_timer_next_timeout_tick();
            if (timeout_tick == RT_TICK_MAX)
            {
                if (pm->ops->timer_start)
                {
                    pm->ops->timer_start(pm, RT_TICK_MAX);
                }
            }
            else
            {
                timeout_tick = timeout_tick - rt_tick_get();
                if (timeout_tick < RT_PM_TICKLESS_THRESH)
                {
                    mode = PM_SLEEP_MODE_IDLE;
                }
                else
                {
                    pm->ops->timer_start(pm, timeout_tick);
                }
            }
        }

        /* enter lower power state */
        pm->ops->sleep(pm, mode);

        /* wake up from lower power state*/
        if (pm->timer_mask & (0x01 << mode))
        {
            delta_tick = pm->ops->timer_get_tick(pm);
            pm->ops->timer_stop(pm);
            if (delta_tick)
            {
                rt_tick_set(rt_tick_get()   delta_tick);
                rt_timer_check();
            }
        }

        /* resume all device */
        _pm_device_resume(pm->sleep_mode);

        if (_pm_notify.notify)
            _pm_notify.notify(RT_PM_EXIT_SLEEP, mode, _pm_notify.data);

        rt_pm_exit_critical(level, mode);
    }
}

移植的实现原理

RT-Thread 低功耗管理系统从设计上分离运行模式和休眠模式,独立管理,运行模式用于变频和变电压,休眠调用芯片的休眠特性。对于多数芯片和开发来说,可能并不需要考虑变频和变电压,仅需关注休眠模式。底层功能的实现已经有Sunwancn大神对STM32做了全系列的适配,以下是底层实现原理,用户也可以自行根据自身情况对底层进行裁剪或增强。(注意: 驱动可能有更新,移植请到gitee下载最新pm驱动。地址在pm-ports-stm32-new 分支:https://gitee.com/sunwancn/rt-thread/tree/pm-ports-stm32-new)

PM 组件的底层功能都是通过struct rt_pm_ops结构体里的函数完成:

代码语言:javascript复制
/**
 * low power mode operations
 */
struct rt_pm_ops
{
    void (*sleep)(struct rt_pm *pm, uint8_t mode);
    void (*run)(struct rt_pm *pm, uint8_t mode);
    void (*timer_start)(struct rt_pm *pm, rt_uint32_t timeout);
    void (*timer_stop)(struct rt_pm *pm);
    rt_tick_t (*timer_get_tick)(struct rt_pm *pm);
};
  • 移植休眠模式移植休眠模式仅需关注 sleep 接口,下面是具体的实现:
代码语言:javascript复制
void stm32_sleep(struct rt_pm *pm, rt_uint8_t mode)
{
    switch (mode)
    {
    case PM_SLEEP_MODE_NONE:
        break;

    case PM_SLEEP_MODE_IDLE:
        if (pm->run_mode == PM_RUN_MODE_LOW_SPEED)
        {
            /* Enter LP SLEEP Mode, Enable low-power regulator */
            HAL_PWR_EnterSLEEPMode(PWR_LOWPOWERREGULATOR_ON, PWR_SLEEPENTRY_WFI);
        }
        else
        {
            /* Enter SLEEP Mode, Main regulator is ON */
            HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI);
        }
        break;

    case PM_SLEEP_MODE_LIGHT:
        if (pm->run_mode == PM_RUN_MODE_LOW_SPEED)
        {
            __HAL_FLASH_SLEEP_POWERDOWN_ENABLE();
            /* Enter LP SLEEP Mode, Enable low-power regulator */
            HAL_PWR_EnterSLEEPMode(PWR_LOWPOWERREGULATOR_ON, PWR_SLEEPENTRY_WFI);
            __HAL_FLASH_SLEEP_POWERDOWN_DISABLE();
        }
        else
        {
            /* Enter SLEEP Mode, Main regulator is ON */
            HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI);
        }
        break;

    case PM_SLEEP_MODE_DEEP:
        /* Disable SysTick interrupt */
        CLEAR_BIT(SysTick->CTRL, (rt_uint32_t)SysTick_CTRL_TICKINT_Msk);

        if (pm->run_mode == PM_RUN_MODE_LOW_SPEED)
        {
            /* Clear LPR bit to back the normal run mode */
            CLEAR_BIT(PWR->CR1, PWR_CR1_LPR);
            /* Enter STOP 2 mode  */
            HAL_PWREx_EnterSTOP2Mode(PWR_STOPENTRY_WFI);
            /* Set Regulator parameter to lowpower run mode */
            SET_BIT(PWR->CR1, PWR_CR1_LPR);
        }
        else
        {
            /* Enter STOP 2 mode  */
            HAL_PWREx_EnterSTOP2Mode(PWR_STOPENTRY_WFI);
        }

        /* Enable SysTick interrupt */
        SET_BIT(SysTick->CTRL, (rt_uint32_t)SysTick_CTRL_TICKINT_Msk);
        /* Re-configure the system clock */
        systemclock_reconfig(pm->run_mode);
        break;

    case PM_SLEEP_MODE_STANDBY:
        __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);
        /* Enter STANDBY mode */
        HAL_PWR_EnterSTANDBYMode();
        break;

    case PM_SLEEP_MODE_SHUTDOWN:
        __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);
        /* Enter SHUTDOWNN mode */
        HAL_PWREx_EnterSHUTDOWNMode();
        break;

    default:
        RT_ASSERT(0);
        break;
    }
}
  • 移植时间补偿接口某些情况下,我们可能需要系统在空闲时进入 Stop 模式,以达到更低的降功耗效果。根据手册可知,Stop 2 模式会关闭系统时钟,当前的 OS Tick 基于内核的 Systick 定时器。那么在系统时钟停止后,OS Tick 也会停止,对于某些依赖 OS Tick 的应用,在进入 Stop 2 模式,又被中断唤醒后,就会出现问题,因此需要在系统唤醒后,对 OS Tick 进行补偿。Stop 2 模式下,绝大多数外设都停止工作,仅低功耗定时器 1(LP_TIM1)和RTC,选择 LSI 作为时钟源后,仍然能正常运行,所以可以选择 LP_TIM1 或者RTC 作为 Stop 2 模式的时间补偿定时器。

休眠的时间补偿需要实现三个接口,分别用于启动低功耗定时器、停止定时器、唤醒后获取休眠的 Tick,下面是具体的实现:

代码语言:javascript复制
static void stm32_pm_timer_start(struct rt_pm *pm, rt_uint32_t timeout)
{
    RT_ASSERT(pm != RT_NULL);
    RT_ASSERT(timeout > 0);

    if (timeout != RT_TICK_MAX)
    {
        /* Convert OS Tick to PM timer timeout value */
        timeout = stm32_pm_tick_from_os_tick(timeout);
        if (timeout > stm32_pmtim_get_tick_max())
        {
            timeout = stm32_pmtim_get_tick_max();
        }

        /* Enter PM_TIMER_MODE */
        stm32_pmtim_start(timeout);
    }
}
代码语言:javascript复制
static void stm32_pm_timer_stop(struct rt_pm *pm)
{
    RT_ASSERT(pm != RT_NULL);

    /* Reset PM timer status */
    stm32_pmtim_stop();
}
代码语言:javascript复制
static rt_tick_t stm32_pm_timer_get_tick(struct rt_pm *pm)
{
    rt_uint32_t timer_tick;

    RT_ASSERT(pm != RT_NULL);

    timer_tick = stm32_pmtim_get_current_tick();

    return stm32_os_tick_from_pm_tick(timer_tick);
}

休眠时间补偿的移植相对并不复杂,根据 Tick 配置低功耗定时器超时,唤醒后获取实际休眠时间并转换为OS Tick,告知 PM 组件即可。

  • 移植运行模式移植休眠模式仅需关注 run接口,下面是具体的实现:
代码语言:javascript复制
void stm32_run(struct rt_pm *pm, rt_uint8_t mode)
{
    static rt_uint32_t last_mode;
    static char *run_str[] = PM_RUN_MODE_NAMES;
    struct rcc_conf_struct sconf = _rcc_conf[mode];

    if (mode == last_mode)
        return;

    if (stm32_run_freq[mode][0] != stm32_run_freq[last_mode][0])
    {
        if (_rcc_conf[last_mode].low_pow_run_en && !sconf.low_pow_run_en)
        {
            /* Disable the Low-power Run mode */
            HAL_PWREx_DisableLowPowerRunMode();
        }

        systemclock_msi_on(last_mode);

        if (mode < last_mode)
        {
            /* Frequency increase */
            HAL_PWREx_ControlVoltageScaling(sconf.volt_scale);

            _set_sysclock[mode]();
        }
        else
        {
            /* Frequency reduce */
            _set_sysclock[mode]();

            HAL_PWREx_ControlVoltageScaling(sconf.volt_scale);
        }

        if (sconf.volt_scale == PWR_REGULATOR_VOLTAGE_SCALE2 || _osc_conf.osc_type == RCC_OSCILLATORTYPE_MSI)
        {
            /* Configure the wake up from stop clock to MSI */
            __HAL_RCC_WAKEUPSTOP_CLK_CONFIG(RCC_STOP_WAKEUPCLOCK_MSI);
        }
        else
        {
            /* Configure the wake up from stop clock to HSI */
            __HAL_RCC_WAKEUPSTOP_CLK_CONFIG(RCC_STOP_WAKEUPCLOCK_HSI);
        }

        if (sconf.low_pow_run_en)
        {
            /* Enable the Low-power Run mode */
            HAL_PWREx_EnableLowPowerRunMode();
        }

        systemclock_msi_off(mode);

#if defined(RT_USING_SERIAL)
        /* Re-Configure the UARTs */
        uart_console_reconfig();
#endif
        /* Re-Configure the Systick time */
        HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq() / RT_TICK_PER_SECOND);
        /* Re-Configure the Systick */
        HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
    }

    last_mode = mode;
    rt_kprintf("switch to %s mode, frequency = %d %sHzn",
               run_str[mode], stm32_run_freq[mode][0], (stm32_run_freq[mode][1] == 1) ? "M" : "K");

    if ((stm32_run_freq[mode][0] / stm32_run_freq[mode][1]) > OSC_CONF_SYS_FREQ_MAX)
        rt_kprintf("warning: The frequency has over than %d MHzn", OSC_CONF_SYS_FREQ_MAX);
}
  • 自定义运行级别时钟树配置函数PM 组件驱动在给定运行频率时,已经尽量自动最优化配置时钟树,但有时外设时钟还是没有达到自己想要的频率,这时可以自己配置时钟树,在 board.c 添加以下单个或所有函数,代码可参考 SystemClock_Config() 函数:
代码语言:javascript复制
rt_uint16_t stm32_run_freq[PM_RUN_MODE_MAX][2] =
{
    /* The actual frequency is 1/divisor MHz, divisor = {1, 1000} */
    /* {sysclk frequency, divisor} */
    {/* 配置高频运行时的时钟 */, /* 分频系数 */},    /* High speed */
    {/* 配置普通运行时的时钟 */, /* 分频系数 */},    /* Normal speed */
    {/* 配置中低运行时的时钟 */, /* 分频系数 */},    /* Medium speed */
    {/* 配置低频运行时的时钟 */, /* 分频系数 */},    /* Low speed, MSI clock 2.0 MHz */
};
void stm32_systemclock_high(void)
{
    /* 添加代码,配置高频运行时的时钟树 */
}
void stm32_systemclock_normal(void)
{
    /* 添加代码,配置普通速度运行时的时钟树 */
}
void stm32_systemclock_medium(void)
{
    /* 添加代码,配置中低频运行时的时钟树 */
}
void stm32_systemclock_low(void)
{
    /* 添加代码,配置低频运行时的时钟树 */
}

当低速的频率小于2MHz时,要注意以下2点:

  1. 串口波特率如果设置过高,将不能正常通信
  2. 在时钟频率很低时,要适当减小 RT_TICK_PER_SECOND 值,不然由于 OS_tick 过短,某些线程将不能完成任务,从而不能进入低功耗模式
  • 初始化PM组件注意:休眠模式的时间补偿需要在初始化阶段通过设置 timer_mask 的对应模式的 bit 控制开启。例如需要开启 Deep Sleep 模式下的时间补偿,在实现 timer 相关的 ops 接口后,初始化时设置相应的bit:
代码语言:javascript复制
int drv_pm_hw_init(void)
{
    static const struct rt_pm_ops _ops =
    {
        stm32_sleep,
        stm32_run,
        stm32_pm_timer_start,
        stm32_pm_timer_stop,
        stm32_pm_timer_get_tick
    };

    rt_uint8_t timer_mask = 0;

    /* Enable Power Clock */
    __HAL_RCC_PWR_CLK_ENABLE();

    /* initialize timer mask */
    timer_mask = 1UL << PM_SLEEP_MODE_DEEP;

    /* initialize system pm module */
    rt_system_pm_init(&_ops, timer_mask, RT_NULL);

    return 0;
}

INIT_BOARD_EXPORT(drv_pm_hw_init);
代码语言:javascript复制
void rt_system_pm_init(const struct rt_pm_ops *ops,
                       rt_uint8_t              timer_mask,
                       void                   *user_data)
{
    struct rt_device *device;
    struct rt_pm *pm;

    pm = &_pm;
    device = &(_pm.parent);

    device->type        = RT_Device_Class_PM;
    device->rx_indicate = RT_NULL;
    device->tx_complete = RT_NULL;

#ifdef RT_USING_DEVICE_OPS
    device->ops = &pm_ops;
#else
    device->init        = RT_NULL;
    device->open        = RT_NULL;
    device->close       = RT_NULL;
    device->read        = _rt_pm_device_read;
    device->write       = _rt_pm_device_write;
    device->control     = _rt_pm_device_control;
#endif
    device->user_data   = user_data;

    /* register PM device to the system */
    rt_device_register(device, "pm", RT_DEVICE_FLAG_RDWR);

    rt_memset(pm->modes, 0, sizeof(pm->modes));
    pm->sleep_mode = _pm_default_sleep;
    pm->run_mode   = RT_PM_DEFAULT_RUN_MODE;
    pm->timer_mask = timer_mask;

    pm->ops = ops;

    pm->device_pm = RT_NULL;
    pm->device_pm_number = 0;

    _pm_init_flag = 1;
}

STM32L4 移植 PM

STM32L4 的低功耗模式简介:

STM32L4系列 是 ST 公司推出的一款超低功耗的 Crotex-M4 内核的 MCU,支持多个电源管理模式,其中最低功耗 Shutdown 模式下,待机电流仅 30 nA。ST 公司 把 L4系列 的电管管理分为很多种,但各个模式的并非功耗逐级递减的特点,下面是各个模式之间的状态转换图:

尽管 STM32L4系列 的低功耗模式很多,但本质上并不复杂,理解它的原理有助于我们移植驱动,同时更好的在产品中选择合适的模式。最终决定 STM32L4系列 系统功耗的主要是三个因素:稳压器(voltage regulator)、CPU 工作频率、芯片自身低功耗的处理,下面分别对三个因素进行阐述。

  • 稳压器L4 使用两个嵌入式线性稳压器为所有数字电路、待机电路以及备份时钟域供电,分别是主稳压器(main regulator,下文简称 MR)和低功耗稳压器(low-power regulator,下文简称 LPR)。稳压器在复位后处于使能状态,根据应用模式,选择不同的稳压器对 Vcore 域供电。其中,MR 的输出电压可以由软件配置为不同的范围(Range 1 和 Rnage 2)。稳压器 应用场合

稳压器

应用场合

MR(Range 1)

Vcore = 1.2V,用于运行模式、睡眠模式和停止模式0,MR 未 Vcore 域提供全功率

MR(Range 2)

Vcore = 1.0V,使用的场景同上

LPR

用于低功耗运行模式、低功耗休眠模式、停止模式 1、停止模式2

OFF

Standby 和 Shutdown 模式下,MR 和 LPR 都被关闭

  • CPU 工作频率通过降低 CPU 的主频达到降低功耗的目的:MR 工作在 Range 1 正常模式时,SYSCLK 最高可以工作在 80M;MR 工作在 Range 2 时,SYSCLK 最高不能超过 26 M;低功耗运行模式和低功耗休眠模式,即 Vcore 域由 LPR 供电,SYSCLK 必须小于 2M。
  • 芯片本身的低功耗处理芯片本身定义了一系列的休眠模式,如 Sleeep、Stop、Standby 和 Shutdown,前面的四种模式功耗逐渐降低,实质是芯片内部通过关闭外设和时钟来实现。

配置工程

配置 PM 组件:

配置内核选项:使用 PM 组件需要更大的 IDLE 线程的栈,这里使用了1024 字节

在空闲线程中会调用rt_system_power_manager接口来进入低功耗模式:

代码语言:javascript复制
/**
 * This function will enter corresponding power mode.
 */
void rt_system_power_manager(void)
{
    rt_uint8_t mode;

    if (_pm_init_flag == 0)
        return;

    /* CPU frequency scaling according to the runing mode settings */
    _pm_frequency_scaling(&_pm);

    /* Low Power Mode Processing */
    mode = _pm_select_sleep_mode(&_pm);
    _pm_change_sleep_mode(&_pm, mode);
}

保存后,可以看到pm.c已经被添加到了工程:

然后添加PM组件的设备驱动,驱动的最新地址:pm-ports-stm32-new 分支:https://gitee.com/sunwancn/rt-thread/tree/pm-ports-stm32-new注意: 目前所使用的驱动不是最新版本,移植请到gitee下载最新pm驱动。

从rt-threadbspstm32librariesHAL_Drivers,拷贝如下四个文件到工程的drivers文件夹下:

本项目选择的是使用RTC作为STOP后的时间补偿,所以需要打开rtc设备和所使用的宏:

注: 如果没有使用RTT的自身的RTC函数的话,前面2个宏可以不要。

应用示例:此程序主要实现开机后经过10秒后进入 STOP 模式,然后每经过5秒 SLEEP 模式和 STOP 模式互相切换,如此循环往复,同时经过一个循环后,切换 MCU 的运行频率,验证运行的稳定性。并且打开了回调和中断唤醒,在进入睡眠和唤醒后会分别熄灭和点亮LED灯,在睡眠时间可以通过外部中断唤醒:

代码语言:javascript复制
/*
 * Copyright (c) 2006-2018, RT-Thread Development Team
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Change Logs:
 * Date           Author       Notes
 * 2018-11-06     SummerGift   first version
 */

#include <rtthread.h>
#include <rtdevice.h>
#include <board.h>
#include <drivers/pm.h>
#define LOG_TAG              "PM.test"
#define LOG_LVL              LOG_LVL_DBG
#include <ulog.h>
/* defined the LED0 pin: PB13 */
#define LED0_PIN    GET_PIN(C, 6)
#define MCU_IRQ_WAKE_PIN   GET_PIN(C, 9)
static RTC_HandleTypeDef hrtc;
static RTC_TimeTypeDef curtime = {0};
static RTC_TimeTypeDef alarmtime = {0};
static RTC_AlarmTypeDef alarm = {0};
static struct rt_semaphore  wake_sem;
static rt_uint8_t sleep_mode = PM_SLEEP_MODE_DEEP; /* STOP 模式 */

static rt_uint8_t run_mode = PM_RUN_MODE_NORMAL_SPEED;

static rt_uint32_t get_interval(void);
static void get_rtc_time(RTC_TimeTypeDef *time);
static rt_uint8_t mode_loop(void);
static rt_uint8_t issleep = 0;
/* 中断回调函数 */
static void MCU_IRQ_WAKE(void *args)
{

    if(issleep)
    {
        rt_kprintf("MCU_IRQ_WAKE!n");
        rt_sem_release(&wake_sem);
        issleep = 0;
    }

}
static void pm_botify(uint8_t event, uint8_t mode, void *data)
{

    if(event == RT_PM_ENTER_SLEEP && mode == PM_SLEEP_MODE_DEEP)
    {
        rt_pin_write(LED0_PIN, PIN_HIGH);
        issleep = 1;
    }
    else if (event == RT_PM_EXIT_SLEEP && mode == PM_SLEEP_MODE_DEEP )
    {
        rt_pin_write(LED0_PIN, PIN_LOW);
    }
}

int pm_test(void)
{
    hrtc.Instance = RTC;

    rt_sem_init(&wake_sem, "wake_sem", 0, RT_IPC_FLAG_FIFO);
    rt_pm_notify_set(pm_botify,0);
    /* 按键0引脚为输入模式 */
    rt_pin_mode(MCU_IRQ_WAKE_PIN, PIN_MODE_INPUT_PULLDOWN);
    /* 绑定中断,上升沿模式,回调函数名为beep_on */
    rt_pin_attach_irq(MCU_IRQ_WAKE_PIN, PIN_IRQ_MODE_RISING, MCU_IRQ_WAKE, RT_NULL);
    /* 使能中断 */
    rt_pin_irq_enable(MCU_IRQ_WAKE_PIN, PIN_IRQ_ENABLE);

    rt_thread_mdelay(10000);


#ifdef RT_USING_PM
    /* 申请低功耗模式 */
    rt_pm_request(sleep_mode);
#endif

    get_rtc_time(&curtime);

    if (sleep_mode == PM_SLEEP_MODE_STANDBY)
    {
        /* 设置休眠,闹钟 20 秒后唤醒,简化版闹钟,只支持 1分钟内有效 */
        alarmtime.Hours = curtime.Hours;
        alarmtime.Minutes = curtime.Minutes;
        alarmtime.SubSeconds = curtime.SubSeconds;
        alarmtime.Seconds = curtime.Seconds   20;
        if (alarmtime.Seconds >= 60)
        {
            alarmtime.Seconds -= 60;
            alarmtime.Minutes   ;
            if (alarmtime.Minutes >= 60)
                alarmtime.Minutes -= 60;
        }

        alarm.Alarm = RTC_ALARM_A;
        alarm.AlarmTime = alarmtime;
        alarm.AlarmTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
        alarm.AlarmTime.StoreOperation = RTC_STOREOPERATION_RESET;
        alarm.AlarmMask = RTC_ALARMMASK_DATEWEEKDAY | RTC_ALARMMASK_HOURS;
        alarm.AlarmSubSecondMask = RTC_ALARMSUBSECONDMASK_NONE;
        alarm.AlarmDateWeekDaySel = RTC_ALARMDATEWEEKDAYSEL_DATE;
        alarm.AlarmDateWeekDay = 0x1;

        /* 开启闹钟 */
        HAL_RTC_SetAlarm_IT(&hrtc, &alarm, RTC_FORMAT_BIN);
    }

    while (1)
    {
        /* 开始进入低功耗模式 */
        rt_sem_take(&wake_sem, rt_tick_from_millisecond(5000));

        /* 退出低功耗模式 */
        rt_kprintf("Sleep %d msn", get_interval());

#ifdef RT_USING_PM
        /* 申请正常模式 */
        rt_pm_request(PM_SLEEP_MODE_NONE);
#endif

        rt_thread_mdelay(5000);

        rt_kprintf("Wakeup %d msn", get_interval());

        /* 运行模式切换 */
        rt_pm_run_enter(mode_loop());

#ifdef RT_USING_PM
        rt_pm_release(PM_SLEEP_MODE_NONE);
#endif
    }

    return RT_EOK;
}
//MSH_CMD_EXPORT(pm_test, PM TEST);

static rt_uint32_t get_interval(void)
{
    rt_uint32_t seconds;

    rt_uint32_t last_seconds = curtime.Seconds;
    rt_uint32_t last_subseconds = curtime.SubSeconds;

    get_rtc_time(&curtime);

    if (curtime.Seconds < last_seconds)
        seconds = 60   curtime.Seconds - last_seconds;
    else
        seconds = curtime.Seconds - last_seconds;

    return (rt_uint32_t)(seconds * 1000   ((int32_t)last_subseconds - (int32_t)curtime.SubSeconds) * 1000 
                         / (int32_t)(((RTC->PRER & RTC_PRER_PREDIV_S) >> RTC_PRER_PREDIV_S_Pos)   1U));
}

static void get_rtc_time(RTC_TimeTypeDef *time)
{
    rt_uint32_t st, datetmpreg;

    HAL_RTC_GetTime(&hrtc, time, RTC_FORMAT_BIN);
    datetmpreg = RTC->DR;
    if (HAL_RCC_GetPCLK1Freq() < 32000U * 7U)
    {
        st = time->SubSeconds;
        HAL_RTC_GetTime(&hrtc, time, RTC_FORMAT_BIN);
        datetmpreg = RTC->DR;

        if (st != time->SubSeconds)
        {
            HAL_RTC_GetTime(&hrtc, time, RTC_FORMAT_BIN);
            datetmpreg = RTC->DR;
        }
    }
    (void)datetmpreg;
}

rt_uint8_t mode_loop(void)
{
    rt_uint8_t mode = 1;

    run_mode  ;
    switch (run_mode)
    {
    case 0:
    case 1:
    case 2:
    case 3:
        mode = run_mode;
        break;
    case 4:
        mode = 2;
        break;
    case 5:
        mode = 1;
        break;
    case 6:
        mode = run_mode = 0;
        break;
    }

    return mode;
}

运行效果:

0 人点赞