概述
我现在的一个项目里面对的需求是:设备最好能够连续待机1个月到3个月,不正常移动时要能够马上发出报警信息。NB-IoT里的PSM省电模式(Power Saving Mode)是必须要重点考虑的。
NB-IoT支持三种省电模式:
- PSM (Power Saving Mode,省电模式)
- DRX(Discontinuous Reception,不连续接收模式)
- eDRX(Extended DRX,扩展不连续接收模式)
这篇文章主要讨论第一种,PSM。
查了不少文章,感觉这篇文章里的一张图比较清晰地说明PSM的原理。
这里又遇到一个关键的术语,TAU(Tracking Area Update),好像是LTE里的一个基本概念,这篇文章里也有详细介绍,我也没有看懂。我个人的理解是:
我手里有一张中国移动的NB-IoT卡,它会定期与中国移动运营商的基站进行通讯,确定它在蜂窝网络中的位置等信息,并且要不断更新这组信息。
图中的T3412和T3324是个两个定时器,T3412就是对应着TAU这段时间周期。
而T3324在图中称为IDLE空闲状态,在AT指令手册里称为Active Time,在这段时间里用户设备(UE)可以收到移动运营商的短信通知等,也就是说运营商可以找到设备。
随后进入PSM状态,也可以叫睡眠状态,这时移动运营商无法主动呼叫设备,而设备可以主动醒来,也可以等PSM定时器超时后唤醒设备,PSM的时间 = T3412 - T3324。
AT指令里的 AT+CPSMS 可以设置这2个时间值,它的语法:
AT+CPSMS=1,,,<Requested Periodic TAU>,<Requested Active Time>
第一个参数1表示启用PSM模式,随后的两个参数对于NB-IoT不适用,所以是三个逗号,后面两个参数分别对应于T3412和T3324。注意这里有Requested(请求)这个单词,意思是T3412和T3324不是设备自行确定的,它发给移动运营商,移动运营商为给出反馈,以运营商设定的数值为准。
T3412和T3324都用8个二进制的字符串来表示,高3位表示不同的时间单位,低5位为小于32的整数,如下图所示。
例如:
AT+CPSMS=1,,,"00100001","00000111"
表示T3412为1小时(1小时 * 1),T3324为14秒(2秒 * 7)
如果设置成功,系统会反馈+CEREG信息(需提前设置AT+CEREG=5)。
我得到的反馈是:
+CEREG: 1,"2A22","0202EE2A",9,"00",0,0,"00000111","00100001"
这里的9表示NB-IoT,最后的两个参数就是T3324和T3412,需要注意的是它们的顺序与CPSMS指令的顺序正好颠倒了。
我尝试了多组T3412和T3324参数组合,我手里的中国移动NB-IoT卡不支持1小时以内的T3412设置,但调高它还是允许的,最高可以设置为320小时*31=413天!而T3324则没有限制,只要小于T3412即可。
sim7020.py程序代码:
import machine
import utime
import ubinascii
from ulogging import *
# 从字符串中查找以指定的key开头的文本
# 比如:find_by_key(b'rn+CSQ: 12rn', b'+CSQ:')
# 将返回"12",移除了前后的空白符
# 如果查找不到,则返回""
def find_by_key(s, key_string):
for line in s.split('rn'):
if line.startswith(key_string):
return line[len(key_string):].strip()
return ""
def get_client_id(resp):
str_id = find_by_key(resp, '+CHTTPCREATE:')
if str_id != "":
return int(str_id)
return -1
class SIM7020:
HTTP_GET = 0
HTTP_POST = 1
def __init__(self, uart, power_pin=14):
self.uart = uart
self.power_enable = machine.Pin(power_pin, machine.Pin.OUT)
def read_uart(self, timeout = 2000, fast_return = False):
ticks_start = utime.ticks_ms()
resp = b''
while utime.ticks_diff(utime.ticks_ms(), ticks_start) < timeout:
if self.uart.any():
line = self.uart.readline()
resp += line
debug("■■■■", utime.ticks_diff(utime.ticks_ms(), ticks_start), line)
if fast_return:
if resp.endswith(b'rnERRORrn') or resp.endswith(b'rnOKrn'):
break
return resp
def exec_cmd(self, cmd, timeout=2000):
if cmd != "":
debug("AT命令>>>", cmd)
self.uart.write((cmd + 'rn').encode())
resp = self.read_uart(timeout).decode()
debug("响应<<<", resp)
return resp
# 读串口时,遇到OK或ERROR时就直接返回结果,不再等待超时
def exec_cmd_fast(self, cmd):
self.uart.write((cmd + 'rn').encode())
raw = self.read_uart(1000, fast_return = True)
resp = ""
try:
resp = raw.decode()
except:
resp = "ERROR: UnicodeError: " + ubinascii.hexlify(raw).decode()
print("raw data:", raw)
return resp
def exec_cmd_find_key(self, cmd, key):
resp = self.exec_cmd_fast(cmd)
return find_by_key(resp, key)
def soft_boot(self):
self.exec_cmd_fast('AT+CFUN=0')
utime.sleep(5)
self.exec_cmd_fast('AT+CFUN=1')
def reboot(self, force = False):
if force:
self.power_enable.off()
utime.sleep(0.1)
self.power_enable.on()
self.read_uart() #清除串口里可能残留的数据
def get_signal_strength(self):
s = self.exec_cmd_find_key("AT+CSQ", '+CSQ:')
if s != "":
return int(s.split(',')[0])
return -1
### 自检,等待设备联网
def wait_ready(self):
#self.exec_cmd_fast("ATE1")
print("===== 自检开始 =====")
print(self.exec_cmd_fast("AT+CGMM"))
print(self.exec_cmd_fast("AT+CPIN?"))
for i in range(1, 10):
strength = self.get_signal_strength()
print(i, "信号强度:", strength)
if strength > 5 and strength != 99:
break
utime.sleep(1)
for i in range(1, 10):
ip = self.get_IP()
print(i, "IP地址:", ip)
if ip != "":
break
utime.sleep(1)
print("===== 自检结束 =====")
def get_IP(self):
# +CGCONTRDP: 1,5,"cmnbiot","100.1.2.3.255.255.255.0"
s = self.exec_cmd_find_key("AT+CGCONTRDP", "+CGCONTRDP:")
paras = s.split(',')
if len(paras) >= 4:
ip_and_mask = paras[3].replace('"', '')
ip = ip_and_mask.split('.')
if len(ip) >= 4:
return '.'.join(ip[0:4])
return ""
def is_cpin_ready(self):
s = self.exec_cmd_find_key("AT+CPIN?", "+CPIN:")
return s == "READY"
def get_clock(self):
clock = self.exec_cmd_find_key('AT+CCLK?', '+CCLK:')
return clock
def http_get(self, url):
index_slash = url.find('/', 8) # 跳过http://或https:// 找下一个斜杠的位置
host = url[:index_slash]
path = url[index_slash:]
# CHTTPCREATE会返回一个ID,拿着这个ID,进行访问网页的手续操作
resp = self.exec_cmd_fast('AT+CHTTPCREATE="' + host + '"')
id = get_client_id(resp)
if id < 0:
print(resp)
return ""
print("■■■■client id: ", id)
self.exec_cmd_fast('AT+CHTTPCON=' + str(id))
resp = self.exec_cmd('AT+CHTTPSEND=' + str(id) + ',' + str(self.HTTP_GET) + ',"' + path + '"')
print("HTTP SEND的反馈:", resp)
self.exec_cmd_fast('AT+CHTTPDISCON=' + str(id))
self.exec_cmd_fast('AT+CHTTPDESTROY=' + str(id))
return resp
测试PSM的主程序:
import sim7020
import utime
import ubinascii
from ulogging import *
def curTime():
now = machine.RTC().datetime()
return "{:02d}:{:02d}:{:02d}".format(now[4], now[5], now[6])
uart = machine.UART(0, 115200, txbuf=1024, rxbuf=1024)
print(uart)
sim = sim7020.SIM7020(uart)
sim.reboot(force=True)
#sim.reboot()
sim.wait_ready()
print(sim.exec_cmd('AT+CPSMS=0')) #禁止PSM模式
print(sim.exec_cmd('AT+CEREG=5'))
print(sim.exec_cmd('AT+CPSMS=1,,,"11000001","00000111"'))
print("========开始==============", curTime())
for i in range(1, 100000):
resp = sim.read_uart(1000).decode()
if resp != "":
print(curTime(), i, resp)
if i % 100 == 0:
print(curTime(), sim.exec_cmd('AT+CEREG?', 5000))
resp = sim.http_get('http://api.seniverse.com/v3/weather/now.json?key=SwwwfskBjB6fHVRon&location=nanjing&language=en&unit=c')
print(resp)
在进入和退出PSM状态的时候,串口里还可以收到状态报告:
+CPSMSTATUS: “ENTER PSM”
+CPSMSTATUS: “EXIT PSM”
推荐阅读:
树莓派Pico开发系列文章
最后
以上就是腼腆柠檬为你收集整理的树莓派62/100 - NB-IoT的PSM省电模式的全部内容,希望文章能够帮你解决树莓派62/100 - NB-IoT的PSM省电模式所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复