笔者从实践出发、本篇直接走读无线定位系统关键节点、网关 SX1302 源码框架,并在源码走读过程中、着重分析与无线定位相关的PPS时间的来龙去脉、并在后期文章中以实际代码讲解 TDOA 无线定位实现过程及多网关综合定位内容,敬请期待。
semtech 公司在 2020年06月份推出 LR1110\LR1120 两款GNSS、WIFI和Lora(LR-HFSS)混合定位芯片、并提供’定位云服务’的接入、国内与腾讯云合作,腾讯云也提供定位云服务接入,这是笔者对混合无线定位技术背景简单描述、此用意看官自行审度。
闲言少叙直奔主体、sx1302 网关数据处理逻辑在 lora_pkt_fwd.c 文件,看官阅读下面内容时、请打开源码对照阅读下、否则可能不知道笔者所云是何物。
本篇文章目标是快速建立起SX1302网关框架认识。
主线程基本功能:
<1>. 读取 *.conf.json 文件内容、并解析内容把变量赋值到相关全局变量中;
<2>. 启动各子线程、子线程清单如下所述;
<3>. 固定周期定时检测gps的时间戳、并上报网关的状态信息;
<4>. 等待退出信号量、网络断开信号量和各子线程退出.
子线程清单.
/* threads */
void thread_up(void); //> 上行线程:负责接收lora模块的数据、并把数据通过网络上传至网络服务器;
void thread_down(void); //> 下行线程:负责接收服务器的数据,并把数据通过lora无线下方给终端模块;
void thread_jit(void); //> jit 时间线程:
void thread_gps(void); //> gps 线程:
void thread_valid(void); //> 时钟校正线程
void thread_spectral_scan(void); //> SCAN扫描线程:
主程序源码基本功能就这么多,笔者就不贴出源码对照了。
此线程负责实时读取 Lora 模块的数据、并打包数据内容上传至网络服务器。
此线程代码逻辑如下:
1>. 配置网关网络上线通讯参数、并建立上行传输socket信道和下行传输的socket信道;
2>. 读取Lora sx1302 模块数据内容、并解码接收数据包内容;
3>. 配置上报数据帧内容和状态、发送数据包内容;
4>. 延时等待接收网络服务器回复 ACK 内容。
我们根据上面总体功能框架、逐步走读此部分代码;先看一下 lgw_pkt_rx_s 结构体内容定义:
/**
@struct lgw_pkt_rx_s
@brief Structure containing the metadata of a packet that was received and a pointer to the payload
*/
struct lgw_pkt_rx_s {
uint32_t freq_hz; /*!> central frequency of the IF chain */
int32_t freq_offset;
uint8_t if_chain; /*!> by which IF chain was packet received */
uint8_t status; /*!> status of the received packet */
uint32_t count_us; /*!> internal concentrator counter for timestamping, 1 microsecond resolution, 网关时间戳计数器 */
uint8_t rf_chain; /*!> through which RF chain the packet was received, 数据包来源链路 */
uint8_t modem_id; //> 模块ID号
uint8_t modulation; /*!> modulation used by the packet, 本包调制模式 */
uint8_t bandwidth; /*!> modulation bandwidth (LoRa only) */
uint32_t datarate; /*!> RX datarate of the packet (SF for LoRa) */
uint8_t coderate; /*!> error-correcting code of the packet (LoRa only) */
float rssic; /*!> average RSSI of the channel in dB, 接收信息RSSI平均值 */
float rssis; /*!> average RSSI of the signal in dB, 本包接收信息的RSSI 值 */
float snr; /*!> average packet SNR, in dB (LoRa only) */
float snr_min; /*!> minimum packet SNR, in dB (LoRa only) */
float snr_max; /*!> maximum packet SNR, in dB (LoRa only) */
uint16_t crc; /*!> CRC that was received in the payload */
uint16_t size; /*!> payload size in bytes */
uint8_t payload[256]; /*!> buffer containing the payload */
bool ftime_received; /*!> a fine timestamp has been received ,接收到精准时间戳标志位 */
uint32_t ftime; /*!> packet fine timestamp (nanoseconds since last PPS) 该报数据接收时间,TDOA 同步时间 */
};
根据此格式我们基本上得出、网关的通讯基本逻辑:
<1>. 数据通讯采用 json 格式、采用 key:value 模式组织数据;
<2>. 网关与Lora模块之间的数据帧、存放在 payload【】 数组中,此部分可以扩展私有协议内容;
<3>. 其他字段内容根据注释和字段名望文生义即可;
<4>. 与网络服务器之间采用异步通讯结构、上报数据后就接收ACK内容;网络服务器会根据上报内容
下发命令、是在下行线程中进行、与上行线程间无业务关联。
/* prepare hints to open network sockets */
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_INET; /* WA: Forcing IPv4 as AF_UNSPEC makes connection on localhost to fail */
hints.ai_socktype = SOCK_DGRAM; //> UDP 通讯方式
/* look for server address w/ upstream port */
i = getaddrinfo(serv_addr, serv_port_up, &hints, &result);
if (i != 0) {
MSG("ERROR: [up] getaddrinfo on address %s (PORT %s) returned %s\n", serv_addr, serv_port_up, gai_strerror(i));
exit(EXIT_FAILURE);
}
/* try to open socket for upstream traffic */
for (q=result; q!=NULL; q=q->ai_next) {
sock_up = socket(q->ai_family, q->ai_socktype,q->ai_protocol);
if (sock_up == -1) continue; /* try next field */
else break; /* success, get out of loop */
}
/* connect so we can send/receive packet with the server only */
i = connect(sock_up, q->ai_addr, q->ai_addrlen);
if (i != 0) {
MSG("ERROR: [up] connect returned %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
/* look for server address w/ downstream port */
i = getaddrinfo(serv_addr, serv_port_down, &hints, &result);
if (i != 0) {
MSG("ERROR: [down] getaddrinfo on address %s (port %s) returned %s\n", serv_addr, serv_port_down, gai_strerror(i));
exit(EXIT_FAILURE);
}
/* set upstream socket RX timeout, 此部分代码在 thread_up 线程中 */
i = setsockopt(sock_up, SOL_SOCKET, SO_RCVTIMEO, (void *)&push_timeout_half, sizeof push_timeout_half);
if (i != 0) {
MSG("ERROR: [up] setsockopt returned %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
网络服务器的ip地址是在 *.conf.json 文件中,如下:
"gateway_conf": {
"gateway_ID": "AA555A0000000000",
/* change with default server address/ports */
"server_address": "localhost",
"serv_port_up": 1730,
"serv_port_down": 1730,
/* adjust the following parameters for your network */
"keepalive_interval": 10,
"stat_interval": 30,
"push_timeout_ms": 100,
/* forward only valid packets */
"forward_crc_valid": true,
"forward_crc_error": false,
"forward_crc_disabled": false,
/* GPS configuration */
"gps_tty_path": "/dev/ttyS0",
/* GPS reference coordinates */
"ref_latitude": 0.0,
"ref_longitude": 0.0,
"ref_altitude": 0,
/* Beaconing parameters */
"beacon_period": 0,
"beacon_freq_hz": 869525000,
"beacon_datarate": 9,
"beacon_bw_hz": 125000,
"beacon_power": 14,
"beacon_infodesc": 0
},
此部分是网关的配置内容,其中 server_address 和 serv_port_up 是上行线程中的网络通讯参数,
此部分配置内容我们就望文生义呗、基本上可以理解差不多。
/* gateway unique identifier (aka MAC address) (optional) */
str = json_object_get_string(conf_obj, "gateway_ID");
if (str != NULL) {
sscanf(str, "%llx", &ull);
lgwm = ull;
MSG("INFO: gateway MAC address is configured to %016llX\n", ull);
}
/* process some of the configuration variables */
net_mac_h = htonl((uint32_t)(0xFFFFFFFF & (lgwm>>32)));
net_mac_l = htonl((uint32_t)(0xFFFFFFFF & lgwm ));
/* pre-fill the data buffer with fixed fields */
buff_up[0] = PROTOCOL_VERSION; //> 协议版本
buff_up[3] = PKT_PUSH_DATA; //> 上行数据标识
*(uint32_t *)(buff_up + 4) = net_mac_h; //> 网关 ID
*(uint32_t *)(buff_up + 8) = net_mac_l;
/* start composing datagram with the header */
token_h = (uint8_t)rand(); /* random token */
token_l = (uint8_t)rand(); /* random token */
buff_up[1] = token_h; //> token
buff_up[2] = token_l;
上行信道通讯协议格式定义如下:
/--------------------------------------------------------------
| 12 bytes 帧头 | JSON 内容 {“rxpk”:[],“stat”:{}} |
--------------------------------------------------------------/
帧头格式: 协议版本、token、上行数据标识 和网关 ID 地址,总共 12 bytes ;
json 串中存放两个数据集,
1>. rxpk : [{"jver":10, "tmst": , "time": , "tmms": , "ftime": , "chan": , "rfch": , "freq": , "mid": , "stat":-1, "modu":"FSK || LORA", "datr":"SF5",
"BW125":"", "codr":"4/5", "rssis":0.1, "lsnr":1.0, "foff":123, "rssi":0.123, "size": 5, "data":"base64_code"}]
是无线通讯物理参数,如频率、带宽、码速率和载荷数据等;
2>. "stat":{"time":"2022-10-08 01:50:20 GMT","rxnb":0,"rxok":0,"rxfw":0,"ackr":0.0,"dwnb":0,"txnb":0,"temp":0.0}
存放的是统计数据内容。
至此我们大致了解网关与网络服务器间的通讯协议格式, 网关 id 在帧头上传输、Lora 模块 id 在
json 串的 modem_id 中,基本通讯框架算是建立起来喽。
接下来看thread_up程序框架功能。
下面代码已删减、只保留主体功能.
void thread_up(void)
{
while (!exit_sig && !quit_sig)
{
//> 接收 Lora 模块数据内容
nb_pkt = lgw_receive(NB_PKT_MAX, rxpkt);
/* start composing datagram with the header,补充帧头 token 内容 */
token_h = (uint8_t)rand(); /* random token */
token_l = (uint8_t)rand(); /* random token */
buff_up[1] = token_h;
buff_up[2] = token_l;
/* start of JSON structure , 下面就是json串数据打包过程 */
buff_index = 12; /* 12-byte header */
memcpy((void *)(buff_up + buff_index), (void *)"{\"rxpk\":[", 9);
buff_index += 9;
for (i = 0; i < nb_pkt; ++i) {
p = &rxpkt[i];
/* Get mote information from current packet (addr, fcnt),数据区 payload 内容,lora模块上报过来的数据 */
/* FHDR - DevAddr */
if (p->size >= 8) {
mote_addr = p->payload[1];
mote_addr |= p->payload[2] << 8;
mote_addr |= p->payload[3] << 16;
mote_addr |= p->payload[4] << 24;
/* FHDR - FCnt */
mote_fcnt = p->payload[6];
mote_fcnt |= p->payload[7] << 8;
} else {
mote_addr = 0;
mote_fcnt = 0;
}
meas_nb_rx_rcv += 1;
switch(p->status) {
case STAT_CRC_OK:
meas_nb_rx_ok += 1;
if (!fwd_valid_pkt) {
pthread_mutex_unlock(&mx_meas_up);
continue; /* skip that packet */
}
break;
case STAT_CRC_BAD:
meas_nb_rx_bad += 1;
if (!fwd_error_pkt) {
pthread_mutex_unlock(&mx_meas_up);
continue; /* skip that packet */
}
break;
case STAT_NO_CRC:
meas_nb_rx_nocrc += 1;
if (!fwd_nocrc_pkt) {
pthread_mutex_unlock(&mx_meas_up);
continue; /* skip that packet */
}
break;
default:
MSG("WARNING: [up] received packet with unknown status %u (size %u, modulation %u, BW %u, DR %u, RSSI %.1f)\n", p->status, p->size, p->modulation, p->bandwidth, p->datarate, p->rssic);
pthread_mutex_unlock(&mx_meas_up);
continue; /* skip that packet */
// exit(EXIT_FAILURE);
}
meas_up_pkt_fwd += 1;
meas_up_payload_byte += p->size;
}
/* JSON rxpk frame format version, 8 useful chars,帧格式类型 */
j = snprintf((char *)(buff_up + buff_index), TX_BUFF_SIZE-buff_index, "\"jver\":%d", PROTOCOL_JSON_RXPK_FRAME_FORMAT );
if (j > 0) {
buff_index += j;
} else {
MSG("ERROR: [up] snprintf failed line %u\n", (__LINE__ - 4));
exit(EXIT_FAILURE);
}
/* RAW timestamp, 8-17 useful chars, 网关内部时钟 */
j = snprintf((char *)(buff_up + buff_index), TX_BUFF_SIZE-buff_index, ",\"tmst\":%u", p->count_us);
if (j > 0) {
buff_index += j;
} else {
MSG("ERROR: [up] snprintf failed line %u\n", (__LINE__ - 4));
exit(EXIT_FAILURE);
}
/* Packet RX time (GPS based), 37 useful chars, 接收Lora数据帧时间 */
if (ref_ok == true) {
/* convert packet timestamp to UTC absolute time */
j = lgw_cnt2utc(local_ref, p->count_us, &pkt_utc_time);
if (j == LGW_GPS_SUCCESS) {
/* split the UNIX timestamp to its calendar components */
x = gmtime(&(pkt_utc_time.tv_sec));
j = snprintf((char *)(buff_up + buff_index), TX_BUFF_SIZE-buff_index, ",\"time\":\"%04i-%02i-%02iT%02i:%02i:%02i.%06liZ\"",
(x->tm_year)+1900, (x->tm_mon)+1, x->tm_mday, x->tm_hour, x->tm_min, x->tm_sec, (pkt_utc_time.tv_nsec)/1000); /* ISO 8601 format */
if (j > 0) {
buff_index += j;
} else {
MSG("ERROR: [up] snprintf failed line %u\n", (__LINE__ - 4));
exit(EXIT_FAILURE);
}
}
/* convert packet timestamp to GPS absolute time */
j = lgw_cnt2gps(local_ref, p->count_us, &pkt_gps_time);
if (j == LGW_GPS_SUCCESS) {
pkt_gps_time_ms = pkt_gps_time.tv_sec * 1E3 + pkt_gps_time.tv_nsec / 1E6;
j = snprintf((char *)(buff_up + buff_index), TX_BUFF_SIZE-buff_index, ",\"tmms\":%" PRIu64 "", pkt_gps_time_ms); /* GPS time in milliseconds since 06.Jan.1980 */
if (j > 0) {
buff_index += j;
} else {
MSG("ERROR: [up] snprintf failed line %u\n", (__LINE__ - 4));
exit(EXIT_FAILURE);
}
}
}
/* Fine timestamp */
if (p->ftime_received == true) {
j = snprintf((char *)(buff_up + buff_index), TX_BUFF_SIZE-buff_index, ",\"ftime\":%u", p->ftime);
if (j > 0) {
buff_index += j;
} else {
MSG("ERROR: [up] snprintf failed line %u\n", (__LINE__ - 4));
exit(EXIT_FAILURE);
}
}
//> 省略打包内容项开始
p->if_chain
p->rf_chain
((double)p->freq_hz / 1e6)
p->modem_id
p->status
p->datarate
p->bandwidth
p->rssis
p->snr
p->freq_offset
status_report
//> 省略打包内容项结束
/* send datagram to server,打包数据上报到网络服务器 */
send(sock_up, (void *)buff_up, buff_index, 0);
clock_gettime(CLOCK_MONOTONIC, &send_time);
pthread_mutex_lock(&mx_meas_up);
meas_up_dgram_sent += 1;
meas_up_network_byte += buff_index;
/* wait for acknowledge (in 2 times, to catch extra packets),等待网络服务器器ACK内容 */
for (i=0; i<2; ++i) {
j = recv(sock_up, (void *)buff_ack, sizeof buff_ack, 0);
clock_gettime(CLOCK_MONOTONIC, &recv_time);
if (j == -1) {
if (errno == EAGAIN) { /* timeout */
continue;
} else { /* server connection error */
break;
}
} else if ((j < 4) || (buff_ack[0] != PROTOCOL_VERSION) || (buff_ack[3] != PKT_PUSH_ACK)) {
//MSG("WARNING: [up] ignored invalid non-ACL packet\n");
continue;
} else if ((buff_ack[1] != token_h) || (buff_ack[2] != token_l)) {
//MSG("WARNING: [up] ignored out-of sync ACK packet\n");
continue;
} else {
MSG("INFO: [up] PUSH_ACK received in %i ms\n", (int)(1000 * difftimespec(recv_time, send_time)));
meas_up_ack_rcv += 1;
break;
}
}
}
}
此线程主体内容就这么多,我们只了解程序基本框架功能,关于定位时间的描述会有专门文章来分析。
源码路径@libloragw/loragw_hal.c
int lgw_receive(uint8_t max_pkt, struct lgw_pkt_rx_s *pkt_data) {
/* Get packets from SX1302, if any */
res = sx1302_fetch(&nb_pkt_fetched);
/* WARNING: this needs to be called regularly by the upper layer */
res = sx1302_update();
/* Apply RSSI temperature compensation */
res = lgw_get_temperature(¤t_temperature);
res = sx1302_parse(&lgw_context, &pkt_data[nb_pkt_found]);
/* Appli RSSI offset calibrated for the board */
pkt_data[nb_pkt_found].rssic += CONTEXT_RF_CHAIN[pkt_data[nb_pkt_found].rf_chain].rssi_offset;
pkt_data[nb_pkt_found].rssis += CONTEXT_RF_CHAIN[pkt_data[nb_pkt_found].rf_chain].rssi_offset;
rssi_temperature_offset = sx1302_rssi_get_temperature_offset(&CONTEXT_RF_CHAIN[pkt_data[nb_pkt_found].rf_chain].rssi_tcomp, current_temperature);
pkt_data[nb_pkt_found].rssic += rssi_temperature_offset;
pkt_data[nb_pkt_found].rssis += rssi_temperature_offset;
res = merge_packets(pkt_data, &nb_pkt_found);
}
此代码删减后内容、如上,基本功能总结如下:
1>. 获取 sx1302 的数据报状态, sx1302_fetch();
2>. 更新接收数据包的时间戳, sx1302_update();
3>. 获取电路板温度值,用以RSSI 温度补偿使用;
4>. 解析lora数据包内容,sx1302_parse();
5>. 双包数据合并优化接收时间戳, merge_packets().
在 sx1302_fetch() 函数中、读取Lora模块上报数据帧内容,其程序调用过程如下:
sx1302_fetch(uint8_t * nb_pkt)
==> err = rx_buffer_fetch(&rx_buffer);
源码路径@libloragw/loragw_sx1302_rx.c
int rx_buffer_fetch(rx_buffer_t * self) {
int i, res;
uint8_t buff[2];
uint8_t payload_len;
uint16_t next_pkt_idx;
int idx;
uint16_t nb_bytes_1, nb_bytes_2;
/* Check input params */
CHECK_NULL(self);
/* Check if there is data in the FIFO */
lgw_reg_rb(SX1302_REG_RX_TOP_RX_BUFFER_NB_BYTES_MSB_RX_BUFFER_NB_BYTES, buff, sizeof buff);
nb_bytes_1 = (buff[0] << 8) | (buff[1] << 0);
/* Workaround for multi-byte read issue: read again and ensure new read is not lower than the previous one */
lgw_reg_rb(SX1302_REG_RX_TOP_RX_BUFFER_NB_BYTES_MSB_RX_BUFFER_NB_BYTES, buff, sizeof buff);
nb_bytes_2 = (buff[0] << 8) | (buff[1] << 0);
self->buffer_size = (nb_bytes_2 > nb_bytes_1) ? nb_bytes_2 : nb_bytes_1;
/* Fetch bytes from fifo if any */
if (self->buffer_size > 0) {
DEBUG_MSG ("-----------------\n");
DEBUG_PRINTF("%s: nb_bytes to be fetched: %u (%u %u)\n", __FUNCTION__, self->buffer_size, buff[1], buff[0]);
memset(self->buffer, 0, sizeof self->buffer);
res = lgw_mem_rb(0x4000, self->buffer, self->buffer_size, true); //> 读取数据内容
if (res != LGW_REG_SUCCESS) {
printf("ERROR: Failed to read RX buffer, SPI error\n");
return LGW_REG_ERROR;
}
/* print debug info */
DEBUG_MSG("RX_BUFFER: ");
for (i = 0; i < self->buffer_size; i++) {
DEBUG_PRINTF("%02X ", self->buffer[i]);
}
DEBUG_MSG("\n");
/* Sanity check: is there at least 1 complete packet in the buffer */
if (self->buffer_size < (SX1302_PKT_HEAD_METADATA + SX1302_PKT_TAIL_METADATA)) { //> 判断数据长度合法性
printf("WARNING: not enough data to have a complete packet, discard rx_buffer\n");
return rx_buffer_del(self);
}
/* Sanity check: is there a syncword at 0 ? If not, move to the first syncword found */
idx = 0;
while (idx <= (self->buffer_size - 2)) { //> 数据同步头合法性
if ((self->buffer[idx] == SX1302_PKT_SYNCWORD_BYTE_0) && (self->buffer[idx + 1] == SX1302_PKT_SYNCWORD_BYTE_1)) {
DEBUG_PRINTF("INFO: syncword found at idx %d\n", idx);
break;
} else {
printf("INFO: syncword not found at idx %d\n", idx);
idx += 1;
}
}
if (idx > self->buffer_size - 2) {
printf("WARNING: no syncword found, discard rx_buffer\n");
return rx_buffer_del(self);
}
if (idx != 0) {
printf("INFO: re-sync rx_buffer at idx %d\n", idx);
memmove((void *)(self->buffer), (void *)(self->buffer + idx), self->buffer_size - idx);
self->buffer_size -= idx;
}
/* Rewind and parse buffer to get the number of packet fetched */
idx = 0;
while (idx < self->buffer_size) {
if ((self->buffer[idx] != SX1302_PKT_SYNCWORD_BYTE_0) || (self->buffer[idx + 1] != SX1302_PKT_SYNCWORD_BYTE_1)) {
printf("WARNING: syncword not found at idx %d, discard the rx_buffer\n", idx);
return rx_buffer_del(self);
}
/* One packet found in the buffer */
self->buffer_pkt_nb += 1;
/* Compute the number of bytes for this packet, 获取数据包内容 */
payload_len = SX1302_PKT_PAYLOAD_LENGTH(self->buffer, idx);
next_pkt_idx = SX1302_PKT_HEAD_METADATA +
payload_len +
SX1302_PKT_TAIL_METADATA +
(2 * SX1302_PKT_NUM_TS_METRICS(self->buffer, idx + payload_len));
/* Move to next packet */
idx += (int)next_pkt_idx;
}
}
/* Initialize the current buffer index to iterate on */
self->buffer_index = 0;
return LGW_REG_SUCCESS;
}
从 SX1302 中读取 Lora 无线接收到的数据内容。
本篇内容就分析线程 thread_up 部分内容、下一篇介绍 thread_down 线程内容;然后笔者将参考loraWan的协议,进一步梳理对照此两块内容。
如果本篇文章对您有所启发或帮助、请给笔者点赞助力、鼓励笔者坚持把此系列内容尽快梳理、分享出来。谢谢。
今天 csdn 给发了一篇纪念开博三周年的短文,有点小感动,继续耕耘.