目录
- `LoRa`调制与解调模式
- 代码分析
- 主程序
- 开始发包
- `Packet Structure`
- 数据传输时间
- 重置FIFO地址与payload长度
- 结束发包
- 中断源
- `Receiver`
- `parsePacket`
- 是否存在数据包
- 数据读取
- `RSSI`
前述文章链接在此~~
LoRa协议在Arduino上的应用——原理及代码分析(一).
LoRa
调制与解调模式
扩频与循环纠错编码相结合
可以看到,LoRa调制模式下,有一个独立的双端口数据缓冲区FIFO,可通过所有模式共有的SPI接口进行访问。
可针对特定的应用优化LoRa调制:扩频因子、调制带宽、错误编码率
先说一下扩频:其实通俗来讲,原来我发送一个信号,这个信号中只包含两个bit10,现在我发送一个信号,这个信号中包含16个bit,这样我就相当于将信号拓宽,扩频因子就是16/2=8。那么我们就可以理解为什么扩频技术是扩展信号的带宽:把信道想象成一个通道,原来一次只能通过两个bit,现在一次性可以通过16个bit,信道是不是变宽了?
但有一点需要注意,就是扩频前后信号的能量是不变的
至于扩频的好处:可以根据香农公式,在相同的信息速率下,带宽和信噪比可以互换,扩频就是用大带宽,换接收端信噪比的低要求
再来看下循环纠错编码:
其实就是使用纠错编码中的循环码:网上资料很多,不赘述了
代码分析
主程序
int counter = 0;
...
void loop() {...// send packetLoRa.beginPacket();LoRa.print("hello ");LoRa.print(counter);LoRa.endPacket();}
开始发包
int LoRaClass::beginPacket(int implicitHeader)
{if (isTransmitting()) {return 0;}// put in standby modeidle();if (implicitHeader) {implicitHeaderMode();} else {explicitHeaderMode();}// reset FIFO address and paload lengthwriteRegister(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 modewriteRegister(REG_OP_MODE, MODE_LONG_RANGE_MODE | MODE_TX);if (!async) {// wait for TX donewhile ((readRegister(REG_IRQ_FLAGS) & IRQ_TX_DONE_MASK) == 0) {yield();}// clear IRQ'swriteRegister(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 packetint packetSize = LoRa.parsePacket();if (packetSize) {// received a packetSerial.print("Received packet '");// read packetwhile (LoRa.available()) {Serial.print((char)LoRa.read());}// print RSSI of packetSerial.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'swriteRegister(REG_IRQ_FLAGS, irqFlags);if ((irqFlags & IRQ_RX_DONE_MASK) && (irqFlags & IRQ_PAYLOAD_CRC_ERROR_MASK) == 0) {// received a packet_packetIndex = 0;// read packet lengthif (_implicitHeaderMode) {packetLength = readRegister(REG_PAYLOAD_LENGTH);} else {packetLength = readRegister(REG_RX_NB_BYTES);}// set FIFO address to current RX addresswriteRegister(REG_FIFO_ADDR_PTR, readRegister(REG_FIFO_RX_CURRENT_ADDR));// put in standby modeidle();} else if (readRegister(REG_OP_MODE) != (MODE_LONG_RANGE_MODE | MODE_RX_SINGLE)) {// not currently in RX mode// reset FIFO addresswriteRegister(REG_FIFO_ADDR_PTR, 0);// put in single RX modewriteRegister(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));
}