目录
前述文章链接在此~~
LoRa协议在Arduino上的应用——原理及代码分析(一).
LoRa
调制与解调模式
扩频与循环纠错编码相结合
可以看到,LoRa调制模式下,有一个独立的双端口数据缓冲区FIFO,可通过所有模式共有的SPI接口进行访问。
可针对特定的应用优化LoRa调制:扩频因子、调制带宽、错误编码率
先说一下扩频:其实通俗来讲,原来我发送一个信号,这个信号中只包含两个bit10,现在我发送一个信号,这个信号中包含16个bit,这样我就相当于将信号拓宽,扩频因子就是16/2=8。那么我们就可以理解为什么扩频技术是扩展信号的带宽:把信道想象成一个通道,原来一次只能通过两个bit,现在一次性可以通过16个bit,信道是不是变宽了?
但有一点需要注意,就是扩频前后信号的能量是不变的
至于扩频的好处:可以根据香农公式,在相同的信息速率下,带宽和信噪比可以互换,扩频就是用大带宽,换接收端信噪比的低要求
再来看下循环纠错编码:
其实就是使用纠错编码中的循环码:网上资料很多,不赘述了
代码分析
主程序
int counter = 0;
...
void loop() {
...
// send packet
LoRa.beginPacket();
LoRa.print("hello ");
LoRa.print(counter);
LoRa.endPacket();
}
开始发包
int LoRaClass::beginPacket(int implicitHeader)
{
if (isTransmitting()) {
return 0;
}
// put in standby mode
idle();
if (implicitHeader) {
implicitHeaderMode();
} else {
explicitHeaderMode();
}
// reset FIFO address and paload length
writeRegister(REG_FIFO_ADDR_PTR, 0);
writeRegister(REG_PAYLOAD_LENGTH, 0);
return 1;
}
在头文件中,已经设置implicitHeader
默认为false
,因此会进入explicitHeaderMode()
函数
void LoRaClass::explicitHeaderMode()
{
_implicitHeaderMode = 0;
writeRegister(REG_MODEM_CONFIG_1, readRegister(REG_MODEM_CONFIG_1) & 0xfe);
}
可以看到这个函数就是把原来调制模式配置寄存器中的数值读出来,将最后一位设置为0,表示Explicit Header mode
,然后写回。
Packet Structure
LoRa的数据包包含三个部分:
A preamble
:前导码
用于保持接收机与输入的数据流同步,作用是提醒接收芯片,即将发送的是有效信号。默认size是12个符号长度,有时为了缩短接收机占空比,可以缩短前导码长度。接收机会定期检测前导码,因此接收和发射前导码长度需要一致。An optional header
可以选择显示(explicit
)或隐式(implicit
)两种类型。
显式报头:包括Payload长度,前向纠错编码率,是否使用CRC(16bit)
隐式报头:需要手动设置无线链路两端的Payload长度、错误编码率、CRC(如果扩频因子SF=6,只能使用隐式报头模式)The data payload
长度不固定,在FIFO中读写
数据传输时间
LoRa数据包总传输时间 = 前导码传输时间Tpre
+ 数据包传输时间Tpay
前导码传输时间为:Tpre = (Npre + 4.25)*Tpay
Npre
表示已设定的前导码长度(读取RegPreambleMsb
和RegPreambleLsb
寄存器得到)
计算Payload符号数的公式为:
PL
表示Payload字节数,H=0
表示header使能,DE=1
表示LowDataRateOptimize=1
,CR
表示编码速率
重置FIFO地址与payload长度
结束发包
int LoRaClass::endPacket(bool async)
{
if ((async) && (_onTxDone))
writeRegister(REG_DIO_MAPPING_1, 0x40); // DIO0 => TXDONE
// put in TX mode
writeRegister(REG_OP_MODE, MODE_LONG_RANGE_MODE | MODE_TX);
if (!async) {
// wait for TX done
while ((readRegister(REG_IRQ_FLAGS) & IRQ_TX_DONE_MASK) == 0) {
yield();
}
// clear IRQ's
writeRegister(REG_IRQ_FLAGS, IRQ_TX_DONE_MASK);
}
return 1;
}
默认async = false
,_onTxDone(NULL)
SX112系列的6个通用DIO引脚在LoRa模式下均可用,其映射关系取决于RegDioMapping1
和RegDioMapping2
这两个寄存器
第一句代码的意思就是如果处于异步通信状态,且在发射完成状态(此处有疑义),就将DIO0引脚映射为发射完成指示符
设置为发射模式,如果不处于异步通信状态,就等待发送完成,然后清除中断。
这里讲的不是很清楚,我们再来具体看一下:
async
:意思是异步,是在non-blocking mode
下使用的一个变量。由于网关可能不会向后发送任何数据,因此我们在尝试接收之前使套接字成为非阻塞状态,以防止卡在等待永远不会到达的数据包中
同步、异步、阻塞、非阻塞这四个是进程间通信的概念,进程间通信通过send()
和receive()
两种基本操作完成:
阻塞式发送:发送方进程会一直被阻塞,直到消息被接收方进程收到;
非阻塞式发送:发送方调用send()
后,可以一直进行其他操作;
阻塞式接收:接收方进程会一直被阻塞,直到消息到达可用;
非阻塞式接收:接收方调用receive()
后,要么得到一个有效的效果,要么得到一个空值
(此处参考文章)void (*_onTxDone)();
可以看到,这是一个类内私有的函数指针,_onTxDone
是一个指针,该指针指向的函数的返回值是void
初始化时,该指针指向的确实是一个NULL
,但是如果执行了这个函数:
void LoRaClass::onTxDone(void(*callback)())
{
_onTxDone = callback;
.......
}
那么这个指针就不为空,也就是满足这个判断条件:
if ((async) && (_onTxDone))
writeRegister(REG_OP_MODE, MODE_LONG_RANGE_MODE | MODE_TX);
这一句对应的就是上图的Mode Request Tx
while ((readRegister(REG_IRQ_FLAGS) & IRQ_TX_DONE_MASK) == 0)
这一句就是在等待中断
中断源
FifoEmpty
:当整个FIFO为空,中断源为高,从FIFO检索数据的时候,FifoEmpty
在NSS下降沿更新。也就是说每次读取操作之后检查FifoEmpty
状态,以决定是否读取下一个字节,FifoEmpty
=1表示不需要读取更多字节FifoFull
FifoOverrunFlag
:is set when a new byte is written by the user (in Tx or Standby modes) or the SR (in Rx mode) while the FIFO is already full,在FIFO已满的情况下,有一个新的字节被写入的时候。数据丢失,并且标志应通过写1清除,同时FIFO也将被清除。PacketSent
:when the SR’s last bit has been sentFifoLevel
#define IRQ_TX_DONE_MASK 0x08
只把TxDoneMask
置为1,其余全部置为0。而如果要满足while中的循环条件,则REG_IRQ_FLAGS
这个寄存器中的第3bit位TxDone
必须为0,如果不满足就yield()
就是一直执行这个空循环
跳出这个循环之后,就清除中断标志,并且设置TX完成标志位
Receiver
void loop() {
// try to parse packet
int packetSize = LoRa.parsePacket();
if (packetSize) {
// received a packet
Serial.print("Received packet '");
// read packet
while (LoRa.available()) {
Serial.print((char)LoRa.read());
}
// print RSSI of packet
Serial.print("' with RSSI ");
Serial.println(LoRa.packetRssi());
}
}
parsePacket
int LoRaClass::parsePacket(int size)
{
int packetLength = 0;
int irqFlags = readRegister(REG_IRQ_FLAGS);
if (size > 0) {
implicitHeaderMode();
writeRegister(REG_PAYLOAD_LENGTH, size & 0xff);
} else {
explicitHeaderMode();
}
// clear IRQ's
writeRegister(REG_IRQ_FLAGS, irqFlags);
if ((irqFlags & IRQ_RX_DONE_MASK) && (irqFlags & IRQ_PAYLOAD_CRC_ERROR_MASK) == 0) {
// received a packet
_packetIndex = 0;
// read packet length
if (_implicitHeaderMode) {
packetLength = readRegister(REG_PAYLOAD_LENGTH);
} else {
packetLength = readRegister(REG_RX_NB_BYTES);
}
// set FIFO address to current RX address
writeRegister(REG_FIFO_ADDR_PTR, readRegister(REG_FIFO_RX_CURRENT_ADDR));
// put in standby mode
idle();
} else if (readRegister(REG_OP_MODE) != (MODE_LONG_RANGE_MODE | MODE_RX_SINGLE)) {
// not currently in RX mode
// reset FIFO address
writeRegister(REG_FIFO_ADDR_PTR, 0);
// put in single RX mode
writeRegister(REG_OP_MODE, MODE_LONG_RANGE_MODE | MODE_RX_SINGLE);
}
return packetLength;
}
这个函数很长,我们逐步分析。
首先观察返回值,可以知道这个函数是计算数据包的长度
在头文件中默认定义int parsePacket(int size = 0);
也就是说会进入显式数据包模式
#define IRQ_PAYLOAD_CRC_ERROR_MASK 0x20
#define IRQ_RX_DONE_MASK 0x40
接收一个数据包的条件是:从中断寄存器中的读取的数据第6位为0或者第5位为0
读取完数据包长度之后,还需要设置FIFO的地址
具体就是跟着这张图来
是否存在数据包
int LoRaClass::available()
{
return (readRegister(REG_RX_NB_BYTES) - _packetIndex);
}
这个看起来有点像串口检测字符的那个函数。。。。。
如果存在数据包,REG_RX_NB_BYTES
这个寄存器的值就不为0,就会开始读取数据
数据读取
int LoRaClass::read()
{
if (!available()) {
return -1;
}
_packetIndex++;
return readRegister(REG_FIFO);
}
返回从FIFO中读取的数据
RSSI
received signal strength indicator:接收信号强度指示符
常规:
RSSI (dBm) = -157 + Rssi
, (高频口)
RSSI (dBm) = -164 + Rssi
, (低频口)
int LoRaClass::packetRssi()
{
return (readRegister(REG_PKT_RSSI_VALUE) - (_frequency < 868E6 ? 164 : 157));
}