系统功耗管理

模组支持的功耗模式

蜂窝通信模组支持多种工作模式,每种模式的功耗各不相同,常见的工作模式有以下几种:

ACTIVE:模组进行LTE数传、GSM通话或RTOS在运行逻辑时的状态,功耗受到具体业务和网络通信制式的影响,CPU本身功耗和网络射频功率都有所不同,故实际功耗在不同工况下会有较大差异。

IDLE:此时模组处于空闲状态,硬件正常在电,RTOS保持运行,但没有任何线程需要被执行。有业务启动或网络业务呼入时,会立即恢复运行。ECX00U系列模组会在IDLE模式下降低时钟频率,进入轻睡眠状态(关闭高速时钟,但CPU不休眠的状态)。

休眠:休眠模式的前提是模组处于空闲状态且使能autosleep,进入休眠模式后,RTOS暂停运行,模组的时钟频率会变慢,部分外设控制器(UART、SPI等)下电,同时只保留部分中断控制器,达到减小功耗的目的。

PSM:PSM模式是3GPP协议所规定的一种低功耗模式,这种模式下,模组只会周期性的唤醒并执行业务,其余时间都处于PSM休眠中。PSM休眠时,模组行为和功耗都近似于关机。

关机:模组完全下电的状态,此时BB芯片和外设控制器完全关闭,但PMIC仍是在电的。一般可由Powerkey或者RTC闹钟唤醒。

各平台工作模式支持情况和功耗:

IDLE即是模组空闲的状态,关机则是切断电源,这两种功耗模式我们不多做赘述。实际使用中,我们经常用的休眠模式就是休眠和PSM模式。

休眠

休眠是蜂窝通信模组最常用的一种低功耗模式。模组处于空闲状态时,若autosleep被使能,则模组会进入休眠状态。此时,模组会关闭部分IP核(如外设控制器和中断控制器等)并且降低时钟频率,从而实现功耗的降低。

autosleep是一个控制是否在空闲时进入休眠的标志,其作用和原理后续会详细介绍

RTOS休眠原理

RTOS的休眠机制,基本都是配合CPU的休眠模式使用的。这个模式会关闭CPU、高速时钟和大部分外设(关闭前会保存外设的上下文)。此时SRAM保持在电,保留休眠前CPU的运行状态,因此在唤醒时,可以立即恢复运行,不会有很大的延迟。

休眠检测机制:
RTOS在上电初始化时,会默认建立一个名为IDLE的TASK,优先级为0,也就是说,这个task只在RTOS不运行其它task时才会被调度到。休眠相关处理就在IDLE TASK中被执行。RTOS在IDLE TASK中,会检测模组的外设是否正在使用,网络是否有数据等休眠条件等,满足休眠条件且autosleep使能时,控制模组进入休眠模式。

休眠机制:
RTOS的休眠一般也会指令CPU进入睡眠模式,此时,高速时钟将会关闭,外设会在保存上下文后断电,ARM的内核会停止运行,但NVIC(中断控制器)和SRAM仍可保持运行,任意中断都可将内核唤醒。

唤醒机制:
任意中断均可将内核唤醒,唤醒后内核立即恢复运行,高速时钟恢复输出,为断电的外设恢复上下文,最后恢复应用程序的运行。这种休眠-唤醒模式不仅实现了功耗的降低,还能较快恢复应用程序的运行。

休眠流程图例:

不同的RTOS可能实现休眠的逻辑不同,但原理基本类似

典型耗流特征:

蜂窝通信模组休眠

Quecpython支持的蜂窝通信模组要进入休眠,需要先使能休眠模式。不使能休眠模式时,模组空闲时默认处于IDLE状态。模块空闲时保持IDLE状态还是进入休眠状态,完全取决于是否使能autosleep。换言之,模块空闲时进入休眠时没有任何硬性阻碍,而是完全由用户控制。那么,为什么不选择默认进入休眠呢?

原因是,某些外设需要一直刷新(如LCD)。然而当模组休眠时,这些外设便不能连续工作,而是不断进行掉电->恢复的过程,严重降低了外设的刷新效率和响应速度。同时,由于休眠时采用低速时钟,任何依赖高速时钟的设备即使不掉电也无法正常工作。故而,模组空闲时会默认保持在IDLE状态,维持所有设备的正常工作。

Autosleep机制

autosleep本质上是操作RTOS休眠检测机制中的一个flag,不使能autosleep时,模组的检测机制就会指令模组保持在IDLE状态。autosleep被使能时,检测机制才认为模组处于允许休眠的状态,从而进入休眠的逻辑。

使用方法参见:自动休眠模式控制

休眠锁机制

在某些场景下,我们既要使模组能够进入休眠,又需要在特定代码段保护某些外设的正常工作,这时,我们就要引入休眠锁机制。休眠锁本质上也是一个flag,允许创建多个。生效机制是:只要有任意一个休眠锁处于lock状态,模组就不会进入休眠。

使用方法参见:创建wake_lock锁

影响蜂窝通信模组休眠的因素

底电流

影响底电流的功耗的原因主要是系统的主频、打开的IP核的功耗,这块和PMIC 相关。
底电流的影响因素主要来源于硬件,包括模组本身和外设两部分:模组本身的耗流因素主要包括:系统主频、打开的IP核。
外设耗电:部分外设使用模组PMIC进行供电,此时其耗流就会由模组负担。

一般来说,测量底电流时,需要将网络置为CFUN0,使射频停止工作,此时测试出的电流即为模组底电流,正常情况下其特征一般近似直线,如下图:

DRX 周期&Paging

基站寻呼(paging):当网络状态产生变化/需要对UE进行呼叫/ETWS或CMAS系统发送预警时,基站会对模组发起寻呼,通知处于IDLE状态的模组开始进行通信。

DRX周期:模组网络在没有RRC连接的情况下,会周期性的监听基站寻呼,保证基站寻呼到来时能及时响应。这个监听的周期就叫做DRX周期。DRX周期一般有32ms、64ms、128ms几种,DRX周期越短,同样时间内监听基站信号的次数就越多,功耗就越高,但响应寻呼就更及时。反之,DRX周期越长,功耗越低,但响应寻呼的速度越慢。

附上Sleep(CFUN1)模式下耗流图,图中规律性的凸起即为paging,其周期即DRX周期

信号质量

信号质量差的时候,在完成相同的网络行为时,模组需要更大的发射功率。这会在两个场景下影响模组功耗:

1.进行网络业务时,耗流会上升,除上文描述的发射功率外,如果网络业务出现了因通信质量差导致的重连或重传,整体的业务时间会被拉长,导致整体耗流进一步上升。
2.模组休眠时,每次寻呼的峰值耗流会上升,导致整体休眠电流上升。

业务数据

模组在进行网络通信时,射频和CPU都会工作,从而产生较大的耗流。一般实际业务中,不会一直进行业务数据传输,常见的业务模式是心跳包。

由于在使用长连接时,如果长期不进行通信,可能会被对端认为已经离线,从而断开连接。所以业务中一般会以固定周期向对端发送一包数据,用来保活长连接。
各心跳周期下的耗流数据(LTE-FDD@64):

RRC 释放时间

无线资源控制(Radio Resource Control,RRC),又称为无线资源管理(RRM)或者无线资源分配(RRA),是包括呼叫准入控制、切换、功率控制、信道分配、分组调度、端到端QoS保障等各自独立的调配和管理算法。模组要进行业务时,需要和基站建立RRC连接,只有当RRC连接被释放时,模组才能进入休眠。

但如下图所示,这个RRC连接的生命周期中,只有最开始数秒在进行真正的数据业务。从业务完成到RRC连接释放之间,有一段耗流近似IDLE,却无法休眠的时间,这段耗流的形成和RRC的运营商策略有关:

为了避免乒乓效应(指网络状态反复切换的情况,会导致更多的系统资源占用和网络性能下降),基站并不会在网络业务完成后立即释放RRC连接,而是会等待一段时间,保证短时间内再次进行业务时无需重新建立RRC连接。RRC连接的时长会因网络环境和运营商策略而不同产生差别,在没有网络数据而RRC未释放的条件下,模组并不会进入休眠。因此RRC连接的时长会显著影响模组功耗。

休眠模式唤醒源

蜂窝通信模组进入休眠模式后,有多种模式可以唤醒模组,包括网络数据、电话、GPIO 中断、Timer等,各种方式实际上都是通过中断来唤醒系统。

Soc 唤醒机制

模组的休眠状态可以由任意中断唤醒(前提是有效的,部分中断控制器处于功耗考虑也会被关闭),唤醒后会在临界区中恢复设备上下文。此时中断控制器中存有此中断的标志,但是不会运行。等待外设恢复完毕,CPU会退出临界区,此时中断的ISR会立即运行。

网络数据&电话唤醒

网络数据&电话:基站通过paging通知模组有呼叫请求,之后cp(或DSP)通过回调唤醒CPU,并通知CPU有网络数据到来,CPU开始将空口数据搬运进协议栈的buffer,此时上层应用(socket或电话)可从协议栈中获取到网络数据。

GPIO 中断唤醒

GPIO可用的前提:GPIO在模组休眠时保持供电,且其连接的中断的控制器也未被关闭。GPIO硬件被触发时,其连接的中断控制器会立即响应并唤醒CPU,CPU会将外设的上下文恢复,然后退出临界区。此时ISR检测到GPIO的中断已经触发,会立即执行GPIO的中断函数(一般是发消息触发GPIO绑定的回调),最后触发该GPIO绑定的回调函数。

timer 唤醒

Timer超时会触发系统定时器绑定的中断,中断控制器会立即响应并唤醒CPU,CPU会将外设的上下文恢复,然后退出临界区。最后通过ISR触发timer绑定的回调。

唤醒源获取

实际上,从上面三条可以看到,凡是涉及到唤醒行为,都要通过中断控制器进行,这正是休眠模式的特征:由任意中断唤醒。由此,我们可以得出结论:不需要刻意去获取唤醒源,唤醒行为必然对应着特定中断的触发。

唤醒后业务处理

上一条已经做出结论:不需要刻意去获取唤醒源,唤醒行为必然对应着特定中断的触发。那么业务处理的原则也就很简单了:模组出休眠逻辑中,设备的上下文恢复后退出临界区,唤醒中断的ISR就会立即去执行,我们需要的业务逻辑直接由此触发即可,一般都是采用在ISR发送消息或者信号量,来触发对应业务的执行。

弱信号休眠方案

模组在网络未连接时,会主动搜索网络并尝试连接。这种机制保证了模组能以最快速度附着到网络,但也会带来一个功耗问题:当模组所在地信号弱或者无网络时,模组会一直进行搜网,此时无法进入休眠,且射频功耗也较高。

对此问题的解决方案有以下两个:

1.OOS(Out of service)机制:在模组开机/服务中断/射频从关闭状态切换到打开时会尝试驻留小区。如果网络质量较好,模组驻留到当前小区;如果网络质量不好,模组可能驻留失败。 除此之外,驻留小区在射频条件不佳的情况下,

会造成下行失步,从而模组执行重新驻留。 OoS 算法定义了模组尝试重新驻留的时间,如果未能驻留成功,则模组会进入睡眠状态,睡眠一定时间后,再次尝试进行网络驻留。通常的OSS机制随着尝试网络驻留的次数增加,休眠时间也会相应增长(增加到上限就不会再增加了,按最大的睡眠时间往复尝试)。例如:

第一阶段:睡眠30s,尝试驻留到小区,重复10次;

第二阶段:睡眠45s,尝试驻留到小区,重复10次;

第三阶段:睡眠60s,尝试驻留到小区,从此依次往复此过程。

模组的的OOS机制一般会尝试多次后才停止,且该时间段内射频一直处于工作状态,相较于正常网络环境,该机制可能会产生更多的的耗流。

2.在业务中自行执行退避和重连的流程。在一定时间内网络未连接时,可手动停止搜网,如指令模组进入飞行模式等,需要重连时,恢复全功能模式即可,实际上就是业务中可自行控制时间和处理逻辑的退避模式,代码示例如下:

import net
import checkNet %%
def check_net_status():
    stage, state = checkNet.waitNetworkReady(30)
    if stage == 3 and state == 1:
        print('Network connection successful.')
    else
        net.setModemFun(4)

def reconnect():
    net.setModemFun(1)

典型应用

网络例程:网络主动发送心跳包&模块接收寻呼唤醒

#利用MQTT来实现心跳包和接受寻呼
from umqtt import MQTTClient
import utime
import net
import sim
import sms
import _thread
import gc
import pm

sub_path = '/a1A5W32fexl/test2/user/Text'
pub_path = '/a1A5W32fexl/test2/user/Text'
state = 0
a = 0
global c

def sub_cb(topic, msg):
    global state
    print("subscribe recv:")
    print(topic, msg)

def err_cb(err):
    print("thread err:")
    print(err)

def wait_msg():
    global c
    while True:
        try:
            c.wait_msg()
        except OSError as e:
            print("wait_msg thread err: %s"%str(e))
            break

global c
c = MQTTClient(    client_id="a1A5W32fexl.test2|securemode=2,signmethod=hmacsha256,timestamp=1675836667378|",
    server="a1A5W32fexl.iot-as-mqtt.cn-shanghai.aliyuncs.com",
    port=1883,
    user="test2&a1A5W32fexl",
    password="a5882fb77108cbd93f1413a403b31ed06d0c0e97c0ebca4b0b2f8dffe286da77",
    ssl=False,
    keepalive=600) #keepalive:MQTT的保活机制,此处为每10min发送一次心跳包
c.error_register_cb(err_cb)
c.set_callback(sub_cb)
print('set_callback')
pm.autosleep(1)#打开休眠模式
c.connect()#连接MQTT,本质上建立TCP长连接
print('connect')
print("MQTT is connecting")
c.subscribe(sub_path)
print("Connected to mq.tongxinmao.com, subscribed to %s" % sub_path)
c.publish(pub_path, b"hello")
print("Publish topic: %s, msg: hello" % pub_path)
_thread.start_new_thread(wait_msg, ())#监听MQTT,等待数据到来,本质上就是等待寻呼,低功耗无需做特殊处理,心跳包发送、接收时会自动唤醒模组

硬件例程1:GPIO/keypad 等外部硬件中断唤醒

import pm
pm.autosleep(1)#开启休眠

# 创建ExtInt对象
from machine import ExtInt
def fun(args):
    f = open("/usr/log.txt", "a+") #以追加模式打开一个文件,低功耗时无法连接交互口,在文件中保存调试信息
    f.write('### interrupt  {} ###'.format(args)) # args[0]:gpio号 args[1]:上升沿或下降沿,写入文件
    f.close()#关闭文件,保存写入信息

extint = ExtInt(ExtInt.GPIO1, ExtInt.IRQ_FALLING, ExtInt.PULL_PU, fun)#给GPIO1绑定回调fuction

extint.enable()#使能GPIO中断

#进入低功耗后测试触发GPIO1,然后连接USB,查看文件是否写入了正确信息。
#若文件中写入了正确的信息,说明低功耗模式下GPIO中断有效唤醒了模组,并且执行了其绑定的中断。

硬件例程2:UART唤醒和接收

"""
运行本例程,需要通过串口线连接开发板的 MAIN 口和PC,在PC上通过串口工具
打开 MAIN 口,并向该端口发送数据,即可看到 PC 发送过来的消息。
"""

import _thread
import utime
import pm
from machine import UART

'''
将主串口接到串口小板上,连接到PC
 * 参数1:端口
        注:选择主串口,所有平台的主串口都支持低功耗唤醒机制,其它串口具有不确定性
        UART2 – MAIN PORT
 * 参数2:波特率
 * 参数3:data bits  (5~8)
 * 参数4:Parity  (0:NONE  1:EVEN  2:ODD)
 * 参数5:stop bits (1~2)
 * 参数6:flow control (0: FC_NONE  1:FC_HW)
'''

class Example_uart(object):
    def __init__(self, no=UART.UART2, bate=115200, data_bits=8, parity=0, stop_bits=1, flow_control=0):
        self.uart = UART(no, bate, data_bits, parity, stop_bits, flow_control)
        self.uart.set_callback(self.callback)

    def callback(self, para):
        if(0 == para[0]):
            self.uart.write("UART WAKRUP!")#在串口RX回调中发送特定数据 

    def uartWrite(self, msg):
        self.uart.write(msg)

    def uartRead(self, len):
        msg = self.uart.read(len)
        utf8_msg = msg.decode()
        return utf8_msg

if __name__ == "__main__":
    uart_test = Example_uart()
    pm.autosleep(1)

# 进入低功耗后向主串口发送数据,如果返回了"UART WAKRUP!",则串口唤醒低功耗成功,并执行了发送
# 注:部分平台会丢一包数据

硬件例程3:利用功耗锁在休眠唤醒时保护硬件时序和状态

from machine import SPI
import utime
import pm

spi_obj = SPI(0, 0, 1)

if __name__ == '__main__':
    pm.autosleep(1)

    lpm_fd = pm.create_wakelock("test", len("test"))#创建功耗锁,保护SPI读写

    pm.wakelock_lock(lpm_fd) #功耗锁上锁,开始spi读写

    r_data = bytearray(5)  # 创建接收数据的buff
    data = b"world"  # 测试数据

    ret = spi_obj.write_read(r_data, data, 5)  # 写入数据并接收
    spi_log.info(r_data)

    pm.wakelock_unlock(lpm_fd)#释放功耗锁,允许在SPI不进行读写时进入低功耗

常见问题

1.无法正常进入休眠:排查唤醒源

2.正常进入休眠后底电流高,检测硬件和网络环境,需要检查的部分包括:

3.网络环境导致功耗高