├── .editorconfig ├── LICENSE ├── LoRaPHY.m ├── README.md └── examples ├── calc_time_on_air.md ├── gen_specified_symbols.md ├── load_signal_from_file.m ├── save_signal_to_file.m └── test_fast_mode.m /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (C) 2020-2022 jkadbear, jkadbear@gmail.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /LoRaPHY.m: -------------------------------------------------------------------------------- 1 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2 | % \file LoRaPHY.m 3 | % 4 | % \brief Physical Layer LoRa Modulator/Demodulator/Encoder/Decoder 5 | % 6 | % \version 0.2.1 7 | % 8 | % \repo https://github.com/jkadbear/LoRaPHY 9 | % 10 | % \copyright MIT License, 2020-2022 11 | % 12 | % \author jkadbear 13 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 14 | 15 | classdef LoRaPHY < handle & matlab.mixin.Copyable 16 | %LORAPHY LoRa physical layer implementation 17 | %%% Example %%% 18 | % rf_freq = 470e6; 19 | % sf = 12; 20 | % bw = 125e3; 21 | % fs = 1e6; 22 | % phy = LoRaPHY(rf_freq, sf, bw, fs); 23 | % phy.has_header = 1; % explicit header mode 24 | % symbols = [2541,1153,673,2397,1189,3509,41,3089,3237,3917,2729,2765,1417,2833,1389,801,3197,345,961,745,3101,297,1893,469]'; 25 | % [data, checksum] = phy.decode(symbols); 26 | % disp(data); % CODE: 09 90 40 01 02 03 04 05 06 07 08 09 BA 2E 27 | % disp(checksum); 28 | 29 | properties 30 | rf_freq % carrier frequency 31 | sf % spreading factor (7,8,9,10,11,12) 32 | bw % bandwidth (125kHz 250kHz 500kHz) 33 | fs % sampling frequency 34 | cr % code rate: (1:4/5 2:4/6 3:4/7 4:4/8) 35 | payload_len % payload length 36 | has_header % explicit header: 1, implicit header: 0 37 | crc % crc = 1 if CRC Check is enabled else 0 38 | ldr % ldr = 1 if Low Data Rate Optimization is enabled else 0 39 | whitening_seq % whitening sequence 40 | crc_generator % CRC generator with polynomial x^16+x^12+x^5+1 41 | header_checksum_matrix % we use a 12 x 5 matrix to calculate header checksum 42 | preamble_len % preamble length 43 | 44 | sig % input baseband signal 45 | downchirp % ideal chirp with decreasing frequency from B/2 to -B/2 46 | upchirp % ideal chirp with increasing frequency from -B/2 to B/2 47 | sample_num % number of sample points per symbol 48 | bin_num % number of bins after FFT (with zero padding) 49 | zero_padding_ratio % FFT zero padding ratio 50 | fft_len % FFT size 51 | preamble_bin % reference bin in current decoding window, used to eliminate CFO 52 | cfo % carrier frequency offset 53 | 54 | fast_mode % set `true` for fast execution (ignore low-pass filter) 55 | is_debug % set `true` for debug information 56 | hamming_decoding_en % enable hamming decoding 57 | end 58 | 59 | methods 60 | function self = LoRaPHY(rf_freq, sf, bw, fs) 61 | %LORAPHY Construct an instance of this class 62 | 63 | % Hexadecimal or binary values representation require at least 64 | % MATLAB R2019b 65 | % https://www.mathworks.com/help/matlab/matlab_prog/specify-hexadecimal-and-binary-numbers.html 66 | if verLessThan('matlab', '9.7') 67 | error('Error. Newer version of MATLAB is required ( >=R2019b ).'); 68 | end 69 | 70 | self.rf_freq = rf_freq; 71 | self.sf = sf; 72 | self.bw = bw; 73 | self.fs = fs; 74 | self.has_header = 1; 75 | self.crc = 1; 76 | self.fast_mode = false; 77 | self.is_debug = false; 78 | self.hamming_decoding_en = true; 79 | self.zero_padding_ratio = 10; 80 | self.cfo = 0; 81 | 82 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 83 | % The whitening sequence is generated by an LFSR 84 | % x^8+x^6+x^5+x^4+1 85 | % Use the code below to generate such sequence 86 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 87 | % reg = 0xFF; 88 | % for i = 1:255 89 | % fprintf("0x%x, ", reg); 90 | % reg = bitxor(bitshift(reg,1), bitxor(bitget(reg,8), bitxor(bitget(reg,6), bitxor(bitget(reg,5), bitget(reg,4))))); 91 | % end 92 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 93 | self.whitening_seq = uint8([0xff, 0xfe, 0xfc, 0xf8, 0xf0, 0xe1, 0xc2, 0x85, 0xb, 0x17, 0x2f, 0x5e, 0xbc, 0x78, 0xf1, 0xe3, 0xc6, 0x8d, 0x1a, 0x34, 0x68, 0xd0, 0xa0, 0x40, 0x80, 0x1, 0x2, 0x4, 0x8, 0x11, 0x23, 0x47, 0x8e, 0x1c, 0x38, 0x71, 0xe2, 0xc4, 0x89, 0x12, 0x25, 0x4b, 0x97, 0x2e, 0x5c, 0xb8, 0x70, 0xe0, 0xc0, 0x81, 0x3, 0x6, 0xc, 0x19, 0x32, 0x64, 0xc9, 0x92, 0x24, 0x49, 0x93, 0x26, 0x4d, 0x9b, 0x37, 0x6e, 0xdc, 0xb9, 0x72, 0xe4, 0xc8, 0x90, 0x20, 0x41, 0x82, 0x5, 0xa, 0x15, 0x2b, 0x56, 0xad, 0x5b, 0xb6, 0x6d, 0xda, 0xb5, 0x6b, 0xd6, 0xac, 0x59, 0xb2, 0x65, 0xcb, 0x96, 0x2c, 0x58, 0xb0, 0x61, 0xc3, 0x87, 0xf, 0x1f, 0x3e, 0x7d, 0xfb, 0xf6, 0xed, 0xdb, 0xb7, 0x6f, 0xde, 0xbd, 0x7a, 0xf5, 0xeb, 0xd7, 0xae, 0x5d, 0xba, 0x74, 0xe8, 0xd1, 0xa2, 0x44, 0x88, 0x10, 0x21, 0x43, 0x86, 0xd, 0x1b, 0x36, 0x6c, 0xd8, 0xb1, 0x63, 0xc7, 0x8f, 0x1e, 0x3c, 0x79, 0xf3, 0xe7, 0xce, 0x9c, 0x39, 0x73, 0xe6, 0xcc, 0x98, 0x31, 0x62, 0xc5, 0x8b, 0x16, 0x2d, 0x5a, 0xb4, 0x69, 0xd2, 0xa4, 0x48, 0x91, 0x22, 0x45, 0x8a, 0x14, 0x29, 0x52, 0xa5, 0x4a, 0x95, 0x2a, 0x54, 0xa9, 0x53, 0xa7, 0x4e, 0x9d, 0x3b, 0x77, 0xee, 0xdd, 0xbb, 0x76, 0xec, 0xd9, 0xb3, 0x67, 0xcf, 0x9e, 0x3d, 0x7b, 0xf7, 0xef, 0xdf, 0xbf, 0x7e, 0xfd, 0xfa, 0xf4, 0xe9, 0xd3, 0xa6, 0x4c, 0x99, 0x33, 0x66, 0xcd, 0x9a, 0x35, 0x6a, 0xd4, 0xa8, 0x51, 0xa3, 0x46, 0x8c, 0x18, 0x30, 0x60, 0xc1, 0x83, 0x7, 0xe, 0x1d, 0x3a, 0x75, 0xea, 0xd5, 0xaa, 0x55, 0xab, 0x57, 0xaf, 0x5f, 0xbe, 0x7c, 0xf9, 0xf2, 0xe5, 0xca, 0x94, 0x28, 0x50, 0xa1, 0x42, 0x84, 0x9, 0x13, 0x27, 0x4f, 0x9f, 0x3f, 0x7f]'); 94 | 95 | self.header_checksum_matrix = gf([ 96 | 1 1 1 1 0 0 0 0 0 0 0 0 97 | 1 0 0 0 1 1 1 0 0 0 0 1 98 | 0 1 0 0 1 0 0 1 1 0 1 0 99 | 0 0 1 0 0 1 0 1 0 1 1 1 100 | 0 0 0 1 0 0 1 0 1 1 1 1 101 | ]); 102 | 103 | self.crc_generator = comm.CRCGenerator('Polynomial','X^16 + X^12 + X^5 + 1'); 104 | 105 | self.preamble_len = 6; 106 | 107 | self.init(); 108 | end 109 | 110 | function init(self) 111 | % init Initialize some parameters 112 | 113 | self.bin_num = 2^self.sf*self.zero_padding_ratio; 114 | self.sample_num = 2*2^self.sf; 115 | self.fft_len = self.sample_num*self.zero_padding_ratio; 116 | 117 | self.downchirp = LoRaPHY.chirp(false, self.sf, self.bw, 2*self.bw, 0, self.cfo, 0); 118 | self.upchirp = LoRaPHY.chirp(true, self.sf, self.bw, 2*self.bw, 0, self.cfo, 0); 119 | 120 | % Low Data Rate Optimization (LDRO) mode in LoRa 121 | % If the chirp peird is larger than 16ms, the least significant 122 | % two bits are considered unreliable and are neglected. 123 | if 2^(self.sf)/self.bw > 16e-3 124 | self.ldr = 1; 125 | else 126 | self.ldr = 0; 127 | end 128 | end 129 | 130 | function pk = dechirp(self, x, is_up) 131 | % dechirp Apply dechirping on the symbol starts from index x 132 | % 133 | % input: 134 | % x: Start index of a symbol 135 | % is_up: `true` if applying up-chirp dechirping 136 | % `false` if applying down-chirp dechirping 137 | % output: 138 | % pk: Peak in FFT results of dechirping 139 | % pk = (height, index) 140 | 141 | if nargin == 3 && ~is_up 142 | c = self.upchirp; 143 | else 144 | c = self.downchirp; 145 | end 146 | ft = fft(self.sig(x:x+self.sample_num-1).*c, self.fft_len); 147 | ft_ = abs(ft(1:self.bin_num)) + abs(ft(self.fft_len-self.bin_num+1:self.fft_len)); 148 | pk = LoRaPHY.topn([ft_ (1:self.bin_num).'], 1); 149 | end 150 | 151 | function x = detect(self, start_idx) 152 | % detect Detect preamble 153 | % 154 | % input: 155 | % start_idx: Start index for detection 156 | % output: 157 | % x: Before index x, a preamble is detected. 158 | % x = -1 if no preamble detected 159 | 160 | ii = start_idx; 161 | pk_bin_list = []; % preamble peak bin list 162 | while ii < length(self.sig)-self.sample_num*self.preamble_len 163 | % search preamble_len-1 basic upchirps 164 | if length(pk_bin_list) == self.preamble_len - 1 165 | % preamble detected 166 | % coarse alignment: first shift the up peak to position 0 167 | % current sampling frequency = 2 * bandwidth 168 | x = ii - round((pk_bin_list(end)-1)/self.zero_padding_ratio*2); 169 | return; 170 | end 171 | pk0 = self.dechirp(ii); 172 | if ~isempty(pk_bin_list) 173 | bin_diff = mod(pk_bin_list(end)-pk0(2), self.bin_num); 174 | if bin_diff > self.bin_num/2 175 | bin_diff = self.bin_num - bin_diff; 176 | end 177 | if bin_diff <= self.zero_padding_ratio 178 | pk_bin_list = [pk_bin_list; pk0(2)]; 179 | else 180 | pk_bin_list = pk0(2); 181 | end 182 | else 183 | pk_bin_list = pk0(2); 184 | end 185 | ii = ii + self.sample_num; 186 | end 187 | x = -1; 188 | end 189 | 190 | function [symbols_m, cfo_m, netid_m] = demodulate(self, sig) 191 | % demodulate LoRa packet demodulation 192 | % 193 | % input: 194 | % sig: Baseband signal in complex 195 | % output: 196 | % symbols_m: A matrix containing the demodulated results. 197 | % Each column vector represents the symbols of 198 | % a successfully demodulated packet. 199 | % cfo_m: A vector containing the carrier frequency offset 200 | % results. Each element represents the CFO of the 201 | % packet in symbols_m. 202 | 203 | self.cfo = 0; 204 | self.init(); 205 | 206 | if ~self.fast_mode 207 | sig = lowpass(sig, self.bw/2, self.fs); 208 | end 209 | % resample signal with 2*bandwidth 210 | self.sig = resample(sig, 2*self.bw, self.fs); 211 | 212 | symbols_m = []; 213 | cfo_m = []; 214 | netid_m = []; 215 | x = 1; 216 | while x < length(self.sig) 217 | x = self.detect(x); 218 | if x < 0 219 | break; 220 | end 221 | 222 | % align symbols with SFD 223 | x = self.sync(x); 224 | 225 | % NetID 226 | pk_netid1 = self.dechirp(round(x-4.25*self.sample_num)); 227 | pk_netid2 = self.dechirp(round(x-3.25*self.sample_num)); 228 | netid_m = [netid_m; 229 | [mod((pk_netid1(2)+self.bin_num-self.preamble_bin)/self.zero_padding_ratio, 2^self.sf), ... 230 | mod((pk_netid2(2)+self.bin_num-self.preamble_bin)/self.zero_padding_ratio, 2^self.sf)] 231 | ]; 232 | 233 | % the goal is to extract payload_len from PHY header 234 | % header is in the first 8 symbols 235 | symbols = []; 236 | pk_list = []; 237 | if x > length(self.sig) - 8*self.sample_num + 1 238 | return; 239 | end 240 | for ii = 0:7 241 | pk = self.dechirp(x+ii*self.sample_num); 242 | pk_list = [pk_list; pk]; 243 | symbols = [symbols; mod((pk(2)+self.bin_num-self.preamble_bin)/self.zero_padding_ratio, 2^self.sf)]; 244 | end 245 | if self.has_header 246 | is_valid = self.parse_header(symbols); 247 | if ~is_valid 248 | x = x + 7*self.sample_num; 249 | continue; 250 | end 251 | end 252 | 253 | % number of symbols in the packet 254 | sym_num = self.calc_sym_num(self.payload_len); 255 | 256 | % demodulate the rest LoRa data symbols 257 | if x > length(self.sig) - sym_num*self.sample_num + 1 258 | return; 259 | end 260 | for ii = 8:sym_num-1 261 | pk = self.dechirp(x+ii*self.sample_num); 262 | pk_list = [pk_list; pk]; 263 | symbols = [symbols; mod((pk(2)+self.bin_num-self.preamble_bin)/self.zero_padding_ratio, 2^self.sf)]; 264 | end 265 | x = x + sym_num*self.sample_num; 266 | 267 | % compensate CFO drift 268 | symbols = self.dynamic_compensation(symbols); 269 | 270 | symbols_m = [symbols_m mod(round(symbols),2^self.sf)]; 271 | cfo_m = [cfo_m self.cfo]; 272 | end 273 | 274 | if isempty(symbols_m) 275 | warning('No preamble detected!'); 276 | end 277 | end 278 | 279 | function is_valid = parse_header(self, data) 280 | % parse_header Parse LoRa PHY header and set parameters 281 | % 282 | % input: 283 | % data: An eight elements vector containing header symbols 284 | % output: 285 | % is_valid: `true` if the header is valid 286 | % `false` if the header is invalid 287 | 288 | % compensate CFO drift 289 | symbols = self.dynamic_compensation(data); 290 | 291 | % gray coding 292 | symbols_g = self.gray_coding(symbols); 293 | 294 | % deinterleave 295 | codewords = self.diag_deinterleave(symbols_g(1:8), self.sf-2); 296 | % parse header 297 | nibbles = self.hamming_decode(codewords, 8); 298 | self.payload_len = double(nibbles(1)*16 + nibbles(2)); 299 | self.crc = double(bitand(nibbles(3), 1)); 300 | self.cr = double(bitshift(nibbles(3), -1)); 301 | % we only calculate header checksum on the first three nibbles 302 | % the valid header checksum is considered to be 5 bits 303 | % other 3 bits require further reverse engineering 304 | header_checksum = [bitand(nibbles(4), 1); de2bi(nibbles(5), 4, 'left-msb')']; 305 | header_checksum_calc = self.header_checksum_matrix * gf(reshape(de2bi(nibbles(1:3), 4, 'left-msb')', [], 1)); 306 | if any(header_checksum ~= header_checksum_calc) 307 | warning('Invalid header checksum!'); 308 | is_valid = 0; 309 | else 310 | is_valid = 1; 311 | end 312 | end 313 | 314 | function s = modulate(self, symbols) 315 | % modulate Modulate a baseband signal 316 | % 317 | % input: 318 | % symbols: A vector of chirp symbols to be modulated 319 | % valid symbol range: 0 to 2^sf-1 320 | % output: 321 | % s: A valid LoRa baseband signal 322 | 323 | uc = LoRaPHY.chirp(true, self.sf, self.bw, self.fs, 0, self.cfo, 0); 324 | dc = LoRaPHY.chirp(false, self.sf, self.bw, self.fs, 0, self.cfo, 0); 325 | preamble = repmat(uc, self.preamble_len, 1); 326 | netid = [LoRaPHY.chirp(true, self.sf, self.bw, self.fs, 24, self.cfo, 0); LoRaPHY.chirp(true, self.sf, self.bw, self.fs, 32, self.cfo, 0)]; 327 | 328 | chirp_len = length(uc); 329 | sfd = [dc; dc; dc(1:round(chirp_len/4))]; 330 | data = zeros(length(symbols)*chirp_len, 1); 331 | for i = 1:length(symbols) 332 | data((i-1)*chirp_len+1:i*chirp_len) = LoRaPHY.chirp(true, self.sf, self.bw, self.fs, symbols(i), self.cfo, 0); 333 | end 334 | s = [preamble; netid; sfd; data]; 335 | end 336 | 337 | function symbols = encode(self, payload) 338 | % encode Encode bytes to symbols 339 | % 340 | % input: 341 | % payload: Payload of LoRa packet 342 | % output: 343 | % symbols: A vector representing the symbols of the packet 344 | 345 | if self.crc 346 | data = uint8([payload; self.calc_crc(payload)]); 347 | else 348 | data = uint8(payload); 349 | end 350 | 351 | plen = length(payload); 352 | sym_num = self.calc_sym_num(plen); 353 | % filling all symbols needs nibble_num nibbles 354 | nibble_num = self.sf - 2 + (sym_num-8)/(self.cr+4)*(self.sf-2*self.ldr); 355 | data_w = uint8([data; 255*ones(ceil((nibble_num-2*length(data))/2), 1)]); 356 | data_w(1:plen) = self.whiten(data_w(1:plen)); 357 | data_nibbles = uint8(zeros(nibble_num, 1)); 358 | for i = 1:nibble_num 359 | idx = ceil(i/2); 360 | if mod(i, 2) == 1 361 | data_nibbles(i) = bitand(data_w(idx), 0xf); 362 | else 363 | data_nibbles(i) = bitshift(data_w(idx), -4); 364 | end 365 | end 366 | 367 | if self.has_header 368 | header_nibbles = self.gen_header(plen); 369 | else 370 | header_nibbles = []; 371 | end 372 | codewords = self.hamming_encode([header_nibbles; data_nibbles]); 373 | 374 | % interleave 375 | % first 8 symbols use CR=4/8 376 | symbols_i = self.diag_interleave(codewords(1:self.sf-2), 8); 377 | ppm = self.sf - 2*self.ldr; 378 | rdd = self.cr + 4; 379 | for i = self.sf-1:ppm:length(codewords)-ppm+1 380 | symbols_i = [symbols_i; self.diag_interleave(codewords(i:i+ppm-1), rdd)]; 381 | end 382 | 383 | symbols = self.gray_decoding(symbols_i); 384 | end 385 | 386 | function header_nibbles = gen_header(self, plen) 387 | % gen_header Generate a valid LoRa header 388 | % 389 | % input: 390 | % plen: Payload length 391 | % output: 392 | % header_nibbles: Header in nibbles 393 | 394 | header_nibbles = zeros(5, 1); 395 | header_nibbles(1) = bitshift(plen, -4); 396 | header_nibbles(2) = bitand(plen, 15); 397 | header_nibbles(3) = bitor(2*self.cr, self.crc); 398 | header_checksum = self.header_checksum_matrix * gf(reshape(de2bi(header_nibbles(1:3), 4, 'left-msb')', [], 1)); 399 | x = header_checksum.x; 400 | header_nibbles(4) = x(1); 401 | for i = 1:4 402 | header_nibbles(5) = bitor(header_nibbles(5), x(i+1)*2^(4-i)); 403 | end 404 | end 405 | 406 | function checksum = calc_crc(self, data) 407 | % calc_crc Calculate payload CRC 408 | % 409 | % input: 410 | % data: Data in bytes 411 | % output: 412 | % checksum: CRC result 413 | 414 | switch length(data) 415 | case 0 416 | checksum = [0; 0]; 417 | case 1 418 | checksum = [data(end); 0]; 419 | case 2 420 | checksum = [data(end); data(end-1)]; 421 | otherwise 422 | input = data(1:end-2); 423 | seq = self.crc_generator(reshape(logical(de2bi(input, 8, 'left-msb'))', [], 1)); 424 | checksum_b1 = bitxor(bi2de(seq(end-7:end)', 'left-msb'), data(end)); 425 | checksum_b2 = bitxor(bi2de(seq(end-15:end-8)', 'left-msb'), data(end-1)); 426 | checksum = [checksum_b1; checksum_b2]; 427 | end 428 | end 429 | 430 | function data_w = whiten(self, data) 431 | % whiten Whitening process in LoRa 432 | % 433 | % input: 434 | % data: Data in bytes 435 | % output: 436 | % data_w: Data after whitening 437 | 438 | len = length(data); 439 | data_w = bitxor(data(1:len), self.whitening_seq(1:len)); 440 | self.print_bin("Whiten", data_w); 441 | end 442 | 443 | function codewords = hamming_encode(self, nibbles) 444 | % hamming_encode Hamming encoding process in LoRa 445 | % 446 | % input: 447 | % nibbles: Data in nibbles 448 | % output: 449 | % codewords: Data after hamming encoding 450 | 451 | nibble_num = length(nibbles); 452 | codewords = uint8(zeros(nibble_num, 1)); 453 | for i = 1:nibble_num 454 | nibble = nibbles(i); 455 | 456 | p1 = LoRaPHY.bit_reduce(@bitxor, nibble, [1 3 4]); 457 | p2 = LoRaPHY.bit_reduce(@bitxor, nibble, [1 2 4]); 458 | p3 = LoRaPHY.bit_reduce(@bitxor, nibble, [1 2 3]); 459 | p4 = LoRaPHY.bit_reduce(@bitxor, nibble, [1 2 3 4]); 460 | p5 = LoRaPHY.bit_reduce(@bitxor, nibble, [2 3 4]); 461 | if i <= self.sf - 2 462 | % the first SF-2 nibbles use CR=4/8 463 | cr_now = 4; 464 | else 465 | cr_now = self.cr; 466 | end 467 | switch cr_now 468 | case 1 469 | codewords(i) = bitor(uint8(p4)*16, nibble); 470 | case 2 471 | codewords(i) = LoRaPHY.word_reduce(@bitor, [uint8(p5)*32 uint8(p3)*16 nibble]); 472 | case 3 473 | codewords(i) = LoRaPHY.word_reduce(@bitor, [uint8(p2)*64 uint8(p5)*32 uint8(p3)*16 nibble]); 474 | case 4 475 | codewords(i) = LoRaPHY.word_reduce(@bitor, [uint8(p1)*128 uint8(p2)*64 uint8(p5)*32 uint8(p3)*16 nibble]); 476 | otherwise 477 | % THIS CASE SHOULD NOT HAPPEN 478 | error('Invalid Code Rate!'); 479 | end 480 | end 481 | end 482 | 483 | function symbols_i = diag_interleave(self, codewords, rdd) 484 | % diag_interleave Diagonal interleaving 485 | % 486 | % input: 487 | % codewords: Data in nibbles 488 | % rdd: Bits with redundancy 489 | % For example, code rate 4/5 means rdd = 5 490 | % output: 491 | % symbols_i: Symbols after diagonal interleaving 492 | 493 | tmp = de2bi(codewords, rdd, 'right-msb'); 494 | symbols_i = uint16(bi2de(cell2mat(arrayfun(@(x) circshift(tmp(:,x), 1-x), 1:rdd, 'un', 0))')); 495 | self.print_bin("Interleave", symbols_i); 496 | end 497 | 498 | function symbols = gray_decoding(self, symbols_i) 499 | % gray_decoding Gray decoding 500 | % `gray_decoding` is used in the ENCODING process 501 | % 502 | % input: 503 | % symbols_i: Interleaved symbols 504 | % output: 505 | % symbols: Final symbols to be modulated in a packet 506 | 507 | symbols = zeros(length(symbols_i), 1); 508 | for i = 1:length(symbols_i) 509 | num = uint16(symbols_i(i)); 510 | mask = bitshift(num, -1); 511 | while mask ~= 0 512 | num = bitxor(num, mask); 513 | mask = bitshift(mask, -1); 514 | end 515 | if i <= 8 || self.ldr 516 | symbols(i) = mod(num * 4 + 1, 2^self.sf); 517 | else 518 | symbols(i) = mod(num + 1, 2^self.sf); 519 | end 520 | end 521 | end 522 | 523 | function sym_num = calc_sym_num(self, plen) 524 | % calc_sym_num Calculate number of symbols 525 | % 526 | % input: 527 | % plen: Payload length 528 | % output: 529 | % sym_num: Number of symbols 530 | 531 | sym_num = double(8 + max((4+self.cr)*ceil(double((2*plen-self.sf+7+4*self.crc-5*(1-self.has_header)))/double(self.sf-2*self.ldr)), 0)); 532 | end 533 | 534 | function plen = calc_payload_len(self, slen, no_redundant_bytes) 535 | % calc_payload_len Calculate payload length 536 | % 537 | % input: 538 | % slen: Number of symbols 539 | % no_redundant_bytes: `true` to fill the 0.5 redundant bytes 540 | % output: 541 | % plen: Payload length 542 | 543 | if nargin < 3 544 | no_redundant_bytes = false; 545 | end 546 | % plen_float possibly has fractional part 0.5, which means 547 | % there would be 0.5 uncontrollable redundant byte in a packet. 548 | % The 0.5 byte results in unexpected symbols when called by 549 | % function `symbols_to_bytes`. To make all specified symbols 550 | % controllable, we use `ceil` instead of `floor` when 551 | % no_redundant_bytes is true. 552 | plen_float = (self.sf-2)/2 - 2.5*self.has_header + (self.sf-self.ldr*2)/2 * ceil((slen-8)/(self.cr+4)); 553 | if no_redundant_bytes 554 | plen = ceil( plen_float ); 555 | else 556 | plen = floor( plen_float ); 557 | end 558 | end 559 | 560 | function x_sync = sync(self, x) 561 | % sync Packet synchronization 562 | % 563 | % input: 564 | % x: Start index for synchronization 565 | % output: 566 | % x_sync: Index after up-down alignment 567 | 568 | % find downchirp 569 | found = false; 570 | while x < length(self.sig) - self.sample_num 571 | up_peak = self.dechirp(x); 572 | down_peak = self.dechirp(x, false); 573 | if abs(down_peak(1)) > abs(up_peak(1)) 574 | % downchirp detected 575 | found = true; 576 | end 577 | x = x + self.sample_num; 578 | if found 579 | break; 580 | end 581 | end 582 | 583 | if ~found 584 | return; 585 | end 586 | 587 | % Up-Down Alignment 588 | % NOTE preamble_len >= 6 589 | % NOTE there are two NETID chirps between preamble and SFD 590 | % NOTE `detect` has already shifted the up peak to position 0 591 | pkd = self.dechirp(x, false); 592 | if pkd(2) > self.bin_num / 2 593 | to = round((pkd(2)-1-self.bin_num)/self.zero_padding_ratio); 594 | else 595 | to = round((pkd(2)-1)/self.zero_padding_ratio); 596 | end 597 | x = x + to; 598 | 599 | % set preamble reference bin for CFO elimination 600 | pku = self.dechirp(x - 4*self.sample_num); 601 | self.preamble_bin = pku(2); 602 | 603 | if self.preamble_bin > self.bin_num / 2 604 | self.cfo = (self.preamble_bin-self.bin_num-1)*self.bw/self.bin_num; 605 | else 606 | self.cfo = (self.preamble_bin-1)*self.bw/self.bin_num; 607 | end 608 | 609 | % set x to the start of data symbols 610 | pku = self.dechirp(x-self.sample_num); 611 | pkd = self.dechirp(x-self.sample_num, false); 612 | if abs(pku(1)) > abs(pkd(1)) 613 | % current symbol is the first downchirp 614 | x_sync = x + round(2.25*self.sample_num); 615 | else 616 | % current symbol is the second downchirp 617 | x_sync = x + round(1.25*self.sample_num); 618 | end 619 | end 620 | 621 | function [data_m, checksum_m] = decode(self, symbols_m) 622 | % decode Decode data from symbols 623 | % 624 | % input: 625 | % symbols_m: A matrix of symbols to be decoded. Each column 626 | % vector represents demodulated symbols from a 627 | % LoRa packet. 628 | % output: 629 | % data_m: A matrix of bytes representing the decoding 630 | % result of `symbols_m`. The last two bytes are the 631 | % decoded CRC checksum if CRC is enabled. 632 | % checksum_m: A vector of checksum based on the decoded 633 | % payload.Checksum is empty if CRC is disabled. 634 | 635 | data_m = []; 636 | checksum_m = []; 637 | 638 | for pkt_num = 1:size(symbols_m, 2) 639 | % gray coding 640 | symbols_g = self.gray_coding(symbols_m(:, pkt_num)); 641 | 642 | % deinterleave 643 | codewords = self.diag_deinterleave(symbols_g(1:8), self.sf-2); 644 | if ~self.has_header 645 | nibbles = self.hamming_decode(codewords, 8); 646 | else 647 | % parse header 648 | nibbles = self.hamming_decode(codewords, 8); 649 | self.payload_len = double(nibbles(1)*16 + nibbles(2)); 650 | self.crc = double(bitand(nibbles(3), 1)); 651 | self.cr = double(bitshift(nibbles(3), -1)); 652 | % we only calculate header checksum on the first three nibbles 653 | % the valid header checksum is considered to be 5 bits 654 | % other 3 bits require further reverse engineering 655 | header_checksum = [bitand(nibbles(4), 1); de2bi(nibbles(5), 4, 'left-msb')']; 656 | header_checksum_calc = self.header_checksum_matrix * gf(reshape(de2bi(nibbles(1:3), 4, 'left-msb')', [], 1)); 657 | if any(header_checksum ~= header_checksum_calc) 658 | error('Invalid header checksum!'); 659 | end 660 | nibbles = nibbles(6:end); 661 | end 662 | rdd = self.cr + 4; 663 | for ii = 9:rdd:length(symbols_g)-rdd+1 664 | codewords = self.diag_deinterleave(symbols_g(ii:ii+rdd-1), self.sf-2*self.ldr); 665 | % hamming decode 666 | nibbles = [nibbles; self.hamming_decode(codewords, rdd)]; 667 | end 668 | 669 | % combine nibbles to bytes 670 | bytes = uint8(zeros(min(255, floor(length(nibbles)/2)), 1)); 671 | for ii = 1:length(bytes) 672 | bytes(ii) = bitor(uint8(nibbles(2*ii-1)), 16*uint8(nibbles(2*ii))); 673 | end 674 | 675 | % dewhitening 676 | len = self.payload_len; 677 | if self.crc 678 | % The last 2 bytes are CRC16 checkcum 679 | data = [self.dewhiten(bytes(1:len)); bytes(len+1:len+2)]; 680 | % Calculate CRC checksum 681 | checksum = self.calc_crc(data(1:len)); 682 | else 683 | data = self.dewhiten(bytes(1:len)); 684 | checksum = []; 685 | end 686 | data_m = [data_m data]; 687 | checksum_m = [checksum_m checksum]; 688 | end 689 | end 690 | 691 | function symbols = dynamic_compensation(self, data) 692 | % dynamic_compensation Compensate bin drift 693 | % 694 | % input: 695 | % data: Symbols with bin drift 696 | % output: 697 | % symbols: Symbols after bin calibration 698 | 699 | % compensate the bin drift caused by Sampling Frequency Offset (SFO) 700 | sfo_drift = (1 + (1:length(data))') * 2^self.sf * self.cfo / self.rf_freq; 701 | symbols = mod(data - sfo_drift, 2^self.sf); 702 | 703 | if self.ldr 704 | bin_offset = 0; 705 | v_last = 1; 706 | 707 | for i = 1:length(symbols) 708 | v = symbols(i); 709 | bin_delta = mod(v-v_last, 4); 710 | if bin_delta < 2 711 | bin_offset = bin_offset - bin_delta; 712 | else 713 | bin_offset = bin_offset - bin_delta + 4; 714 | end 715 | v_last = v; 716 | symbols(i) = mod(v+bin_offset, 2^self.sf); 717 | end 718 | end 719 | end 720 | 721 | function symbols = gray_coding(self, din) 722 | % gray_coding Gray coding 723 | % `gray_coding` is used in the DECODING process 724 | % 725 | % input: 726 | % data: Symbols with bin drift 727 | % output: 728 | % symbols: Symbols after bin calibration 729 | 730 | din(1:8) = floor(din(1:8)/4); 731 | if self.ldr 732 | din(9:end) = floor(din(9:end)/4); 733 | else 734 | din(9:end) = mod(din(9:end)-1, 2^self.sf); 735 | end 736 | s = uint16(din); 737 | symbols = bitxor(s, bitshift(s, -1)); 738 | self.print_bin("Gray Coding", symbols, self.sf); 739 | end 740 | 741 | function codewords = diag_deinterleave(self, symbols, ppm) 742 | % diag_deinterleave Diagonal deinterleaving 743 | % 744 | % input: 745 | % symbols: Symbols after gray coding 746 | % ppm: Size with parity bits 747 | % output: 748 | % codewords: Codewords after deinterleaving 749 | 750 | b = de2bi(symbols, double(ppm), 'left-msb'); 751 | codewords = flipud(bi2de(cell2mat(arrayfun(@(x) ... 752 | circshift(b(x,:), [1 1-x]), (1:length(symbols))', 'un', 0))')); 753 | self.print_bin("Deinterleave", codewords); 754 | end 755 | 756 | function bytes_w = dewhiten(self, bytes) 757 | % dewhiten Data Dewhitening 758 | % 759 | % input: 760 | % bytes: Bytes after deinterleaving 761 | % output: 762 | % bytes_w: Bytes after dewhitening 763 | 764 | len = length(bytes); 765 | bytes_w = bitxor(uint8(bytes(1:len)), self.whitening_seq(1:len)); 766 | self.print_bin("Dewhiten", bytes_w); 767 | end 768 | 769 | function nibbles = hamming_decode(self, codewords, rdd) 770 | % hamming_decode Hamming Decoding 771 | % 772 | % input: 773 | % codewords: Codewords after deinterleaving 774 | % rdd: Bits with redundancy 775 | % output: 776 | % nibbles: Nibbles after hamming decoding 777 | 778 | p1 = LoRaPHY.bit_reduce(@bitxor, codewords, [8 4 3 1]); 779 | p2 = LoRaPHY.bit_reduce(@bitxor, codewords, [7 4 2 1]); 780 | p3 = LoRaPHY.bit_reduce(@bitxor, codewords, [5 3 2 1]); 781 | p4 = LoRaPHY.bit_reduce(@bitxor, codewords, [5 4 3 2 1]); 782 | p5 = LoRaPHY.bit_reduce(@bitxor, codewords, [6 4 3 2]); 783 | function pf = parity_fix(p) 784 | switch p 785 | case 3 % 011 wrong b3 786 | pf = 4; 787 | case 5 % 101 wrong b4 788 | pf = 8; 789 | case 6 % 110 wrong b1 790 | pf = 1; 791 | case 7 % 111 wrong b2 792 | pf = 2; 793 | otherwise 794 | pf = 0; 795 | end 796 | end 797 | if self.hamming_decoding_en 798 | switch rdd 799 | % TODO report parity error 800 | case {5, 6} 801 | nibbles = mod(codewords, 16); 802 | case {7, 8} 803 | parity = p2*4+p3*2+p5; 804 | pf = arrayfun(@parity_fix, parity); 805 | codewords = bitxor(codewords, uint16(pf)); 806 | nibbles = mod(codewords, 16); 807 | otherwise 808 | % THIS CASE SHOULD NOT HAPPEN 809 | error('Invalid Code Rate!'); 810 | end 811 | else 812 | nibbles = mod(codewords, 16); 813 | end 814 | self.print_bin("Hamming Decode", codewords); 815 | end 816 | 817 | function bytes = symbols_to_bytes(self, symbols) 818 | % symbols_to_bytes Derive the payload for a given symbol series 819 | % 820 | % input: 821 | % symbols: Symbols to appear in the demodulation level 822 | % output: 823 | % bytes: Payload bytes 824 | 825 | symbols = reshape(symbols, [length(symbols), 1]); 826 | self.init(); 827 | self.hamming_decoding_en = false; 828 | payload_len_ = self.payload_len; 829 | 830 | if length(symbols) <= 4 831 | slen_tmp = 8 + self.has_header*(self.cr+4); 832 | else 833 | slen_tmp = 8 + ceil((length(symbols)-4*(1-self.has_header))/4) * (self.cr+4); 834 | end 835 | self.payload_len = self.calc_payload_len(slen_tmp, true); 836 | symbols_ = zeros(self.calc_sym_num(self.payload_len), 1); 837 | if self.has_header 838 | jj = 9; 839 | else 840 | jj = 1; 841 | end 842 | for ii = 1:4:length(symbols) 843 | if ii+3 <= length(symbols) 844 | symbols_(jj:jj+3) = symbols(ii:ii+3); 845 | else 846 | symbols_(jj:jj+3) = [symbols(ii:end); zeros(ii-length(symbols)+3, 1)]; 847 | end 848 | if jj == 1 849 | jj = 9; 850 | else 851 | jj = jj + self.cr + 4; 852 | end 853 | end 854 | if self.has_header 855 | % construct header 856 | symbols_tmp = self.encode(zeros(self.payload_len, 1)); 857 | symbols_(1:8) = symbols_tmp(1:8); 858 | end 859 | [bytes, ~] = self.decode(symbols_); 860 | if self.crc 861 | bytes = bytes(1:end-2); 862 | end 863 | 864 | self.hamming_decoding_en = true; 865 | self.payload_len = payload_len_; 866 | end 867 | 868 | function time_ms = time_on_air(self, plen) 869 | % time_on_air Calculate the flying time of a LoRa packet 870 | % 871 | % input: 872 | % plen: Payload length 873 | % output: 874 | % time_ms: Flying time (in milliseconds) 875 | 876 | sym_num = self.calc_sym_num(plen); 877 | % milliseconds 878 | time_ms = (sym_num + 4.25 + self.preamble_len) * (2^self.sf/self.bw) * 1000; 879 | end 880 | 881 | function print_bin(self, flag, vec, size) 882 | if self.is_debug 883 | if nargin == 3 884 | size = 8; 885 | end 886 | len = length(vec); 887 | fprintf("%s:\n", flag); 888 | for i = 1:len 889 | fprintf("%s\n", dec2bin(round(vec(i)), size)); 890 | end 891 | fprintf("\n"); 892 | end 893 | end 894 | 895 | function print_hex(self, flag, vec) 896 | if self.is_debug 897 | len = length(vec); 898 | fprintf("%s: ", flag); 899 | for i = 1:len 900 | fprintf("%s ", dec2hex(round(vec(i)))); 901 | end 902 | fprintf("\n"); 903 | end 904 | end 905 | 906 | function log(self, flag, data) 907 | if self.is_debug 908 | fprintf("%s: ", flag); 909 | len = length(data); 910 | for i = 1:len 911 | fprintf("%d ", data(i)); 912 | end 913 | fprintf("\n"); 914 | end 915 | end 916 | 917 | function plot_peak(self, x) 918 | figure; 919 | c = [self.downchirp self.upchirp]; 920 | for jj = 1:9 921 | for ii = 1:2 922 | ft = fft(self.sig(x:x+self.sample_num-1).*c(:,ii), self.fft_len); 923 | ft_ = abs(ft(1:self.bin_num)) + abs(ft(self.fft_len-self.bin_num+1:self.fft_len)); 924 | subplot(2, 9, (ii-1)*9+jj); 925 | plot(ft_); 926 | end 927 | x = x + self.sample_num; 928 | end 929 | end 930 | end 931 | 932 | methods(Static) 933 | function b = bit_reduce(fn, w, pos) 934 | b = bitget(w, pos(1)); 935 | for i = 2:length(pos) 936 | b = fn(b, bitget(w, pos(i))); 937 | end 938 | end 939 | 940 | function w = word_reduce(fn, ws) 941 | w = ws(1); 942 | for i = 2:length(ws) 943 | w = fn(w, ws(i)); 944 | end 945 | end 946 | 947 | function y = topn(pks, n, padding, th) 948 | [y, p] = sort(abs(pks(:,1)), 'descend'); 949 | if nargin == 1 950 | return; 951 | end 952 | nn = min(n, size(pks, 1)); 953 | if nargin >= 3 && padding 954 | y = [pks(p(1:nn), :); zeros(n-nn, size(pks, 2))]; 955 | else 956 | y = pks(p(1:nn), :); 957 | end 958 | if nargin == 4 959 | ii = 1; 960 | while ii <= size(y,1) 961 | if abs(y(ii,1)) < th 962 | break; 963 | end 964 | ii = ii + 1; 965 | end 966 | y = y(1:ii-1, :); 967 | end 968 | end 969 | 970 | function y = chirp(is_up, sf, bw, fs, h, cfo, tdelta, tscale) 971 | % chirp Generate a LoRa chirp symbol 972 | % 973 | % input: 974 | % is_up: `true` if constructing an up-chirp 975 | % `false` if constructing a down-chirp 976 | % sf: Spreading Factor 977 | % bw: Bandwidth 978 | % fs: Sampling Frequency 979 | % h: Start frequency offset (0 to 2^SF-1) 980 | % cfo: Carrier Frequency Offset 981 | % tdelta: Time offset (0 to 1/fs) 982 | % tscale: Scaling the sampling frequency 983 | % output: 984 | % y: Generated LoRa symbol 985 | 986 | if nargin < 8 987 | tscale = 1; 988 | end 989 | if nargin < 7 990 | tdelta = 0; 991 | end 992 | if nargin < 6 993 | cfo = 0; 994 | end 995 | N = 2^sf; 996 | T = N/bw; 997 | samp_per_sym = round(fs/bw*N); 998 | h_orig = h; 999 | h = round(h); 1000 | cfo = cfo + (h_orig - h) / N * bw; 1001 | if is_up 1002 | k = bw/T; 1003 | f0 = -bw/2+cfo; 1004 | else 1005 | k = -bw/T; 1006 | f0 = bw/2+cfo; 1007 | end 1008 | 1009 | % retain last element to calculate phase 1010 | t = (0:samp_per_sym*(N-h)/N)/fs*tscale + tdelta; 1011 | snum = length(t); 1012 | c1 = exp(1j*2*pi*(t.*(f0+k*T*h/N+0.5*k*t))); 1013 | 1014 | if snum == 0 1015 | phi = 0; 1016 | else 1017 | phi = angle(c1(snum)); 1018 | end 1019 | t = (0:samp_per_sym*h/N-1)/fs + tdelta; 1020 | c2 = exp(1j*(phi + 2*pi*(t.*(f0+0.5*k*t)))); 1021 | 1022 | y = cat(2, c1(1:snum-1), c2).'; 1023 | end 1024 | 1025 | function spec(sig, fs, bw, sf) 1026 | x = 0:1/sf:2^sf/bw; 1027 | y = -bw/2:bw/2; 1028 | p = fs/bw; 1029 | win_length = 2^(sf-2); 1030 | N = p*2^sf; 1031 | s = spectrogram(sig,win_length,round(win_length*0.8),N); 1032 | valid_data_len = round(2^sf/2*1.5); 1033 | b = abs(s(valid_data_len:-1:1,:)); 1034 | c = abs(s(end:-1:end-valid_data_len+1,:)); 1035 | d = [b;c]; 1036 | figure; 1037 | imagesc(x,y,d); 1038 | colormap summer; 1039 | title('Spectrogram'); 1040 | xlabel('Time'); 1041 | ylabel('Frequency'); 1042 | set(gcf,'unit','normalized','position',[0.05,0.2,0.9,0.1]); 1043 | end 1044 | 1045 | % Method `read` and `write` are copied from 1046 | % https://github.com/gnuradio/gnuradio/blob/master/gr-utils/octave/read_complex_binary.m 1047 | % https://github.com/gnuradio/gnuradio/blob/master/gr-utils/octave/write_complex_binary.m 1048 | function v = read(filename, count) 1049 | 1050 | % usage: read(filename, [count]) 1051 | % 1052 | % open filename and return the contents as a column vector, 1053 | % treating them as 32 bit complex numbers 1054 | % 1055 | 1056 | m = nargchk (1,2,nargin); 1057 | if (m) 1058 | usage (m); 1059 | end 1060 | 1061 | if (nargin < 2) 1062 | count = Inf; 1063 | end 1064 | 1065 | f = fopen (filename, 'rb'); 1066 | if (f < 0) 1067 | v = 0; 1068 | else 1069 | t = fread (f, [2, count], 'float'); 1070 | fclose (f); 1071 | v = t(1,:) + t(2,:)*1i; 1072 | [r, c] = size (v); 1073 | v = reshape (v, c, r); 1074 | end 1075 | end 1076 | 1077 | function v = write(data, filename) 1078 | 1079 | % usage: write(data, filename) 1080 | % 1081 | % open filename and write data to it 1082 | % Format is interleaved float IQ e.g. each 1083 | % I,Q 32-bit float IQIQIQ.... 1084 | % This is compatible with read_complex_binary() 1085 | % 1086 | 1087 | m = nargchk (2,2,nargin); 1088 | if (m) 1089 | usage (m); 1090 | end 1091 | 1092 | f = fopen (filename, 'wb'); 1093 | if (f < 0) 1094 | v = 0; 1095 | else 1096 | re = real(data); 1097 | im = imag(data); 1098 | re = re(:)'; 1099 | im = im(:)'; 1100 | y = [re;im]; 1101 | y = y(:); 1102 | v = fwrite (f, y, 'float'); 1103 | fclose (f); 1104 | end 1105 | end 1106 | end 1107 | end 1108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LoRaPHY 2 | 3 | **LoRaPHY** is a complete MATLAB implementation of [LoRa](https://en.wikipedia.org/wiki/LoRa) physical layer, including baseband modulation, baseband demodulation, encoding and decoding. 4 | **LoRaPHY** is organized as a single file `LoRaPHY.m` for ease of use (copy it and run everywhere). 5 | 6 | This repo is the implementation of the following paper: 7 | > Zhenqiang Xu, Pengjin Xie, Shuai Tong, Jiliang Wang. From Demodulation to Decoding: Towards Complete LoRa PHY Understanding and Implementation. ACM Transactions on Sensor Networks 2022. [[pdf]](https://dl.acm.org/doi/10.1145/3546869) 8 | 9 | The real-time SDR implementation based on GNU Radio can be accessed via [gr-lora](https://github.com/jkadbear/gr-lora). 10 | 11 | ## Prerequisites 12 | MATLAB >= R2019b 13 | 14 | ## Components 15 | 16 | - LoRa Modulator 17 | - LoRa Demodulator 18 | - LoRa Encoder 19 | - LoRa Decoder 20 | 21 | ## Supported features 22 | 23 | - Extremely low SNR demodulation (**-20 dB**) 24 | - Clock drift correction 25 | - All spreading factors (SF = 7,8,9,10,11,12) 26 | - All code rates (CR = 4/5,4/6,4/7,4/8) 27 | - Explicit/Implicit PHY header mode 28 | - PHY header/payload CRC check 29 | - Low Data Rate Optimization (LDRO) 30 | 31 | ## Usage 32 | 33 | Git clone this repo or just download [`LoRaPHY.m`](https://raw.githubusercontent.com/jkadbear/LoRaPHY/master/LoRaPHY.m). 34 | Put your MATLAB script, e.g., `test.m`, in the same directory of `LoRaPHY.m`. 35 | Below is an example showing how to generate a valid baseband LoRa signal and then extract the data with the decoder. 36 | See more examples in directory [examples](./examples). 37 | (LoRaPHY provides `fast mode` which enables 10x speedup comparing to normal demodulation with a little sensitivity degradation. See `./examples/test_fast_mode.m`.) 38 | 39 | ```matlab 40 | % test.m 41 | 42 | rf_freq = 470e6; % carrier frequency 470 MHz, used to correct clock drift 43 | sf = 7; % spreading factor SF7 44 | bw = 125e3; % bandwidth 125 kHz 45 | fs = 1e6; % sampling rate 1 MHz 46 | 47 | phy = LoRaPHY(rf_freq, sf, bw, fs); 48 | phy.has_header = 1; % explicit header mode 49 | phy.cr = 4; % code rate = 4/8 (1:4/5 2:4/6 3:4/7 4:4/8) 50 | phy.crc = 1; % enable payload CRC checksum 51 | phy.preamble_len = 8; % preamble: 8 basic upchirps 52 | 53 | % Encode payload [1 2 3 4 5] 54 | symbols = phy.encode((1:5)'); 55 | fprintf("[encode] symbols:\n"); 56 | disp(symbols); 57 | 58 | % Baseband Modulation 59 | sig = phy.modulate(symbols); 60 | 61 | % Demodulation 62 | [symbols_d, cfo, netid] = phy.demodulate(sig); 63 | fprintf("[demodulate] symbols:\n"); 64 | disp(symbols_d); 65 | 66 | % Decoding 67 | [data, checksum] = phy.decode(symbols_d); 68 | fprintf("[decode] data:\n"); 69 | disp(data); 70 | fprintf("[decode] checksum:\n"); 71 | disp(checksum); 72 | ``` 73 | 74 | ## TODO 75 | 76 | - Decoding multiple channels simultaneously 77 | 78 | ## References 79 | 80 | 1. Zhenqiang Xu, Pengjin Xie, Jiliang Wang. Pyramid: Real-Time LoRa Collision Decoding with Peak Tracking. In Proceedings of IEEE INFOCOM. 2021: 1-9. 81 | 2. Zhenqiang Xu, Shuai Tong, Pengjin Xie, Jiliang Wang. FlipLoRa: Resolving Collisions with Up-Down Quasi-Orthogonality. In Proceedings of IEEE SECON. 2020: 1-9. 82 | 3. Shuai Tong, Zhenqiang Xu, Jiliang Wang. CoLoRa: Enabling Multi-Packet Reception in LoRa. In Proceedings of IEEE INFOCOM. 2020: 2303-2311. 83 | 4. Shuai Tong, Jiliang Wang, Yunhao Liu. Combating packet collisions using non-stationary signal scaling in LPWANs. In Proceedings of Proceedings of ACM MobiSys. 2020: 234-246. 84 | 5. Yinghui Li, Jing Yang, Jiliang Wang. DyLoRa: Towards Energy Efficient Dynamic LoRa Transmission Control. In Proceedings of IEEE INFOCOM. 2020: 2312-2320. 85 | 6. Qian Chen, Jiliang Wang. AlignTrack: Push the Limit of LoRa Collision Decoding. In Proceedings of IEEE ICNP. 2021. 86 | 7. Jinyan Jiang, Zhenqiang Xu, Jiliang Wang. Long-Range Ambient LoRa Backscatter with Parallel Decoding. In Proceedings of ACM MobiCom. 2021. 87 | 8. Shuai Tong, Zilin Shen, Yunhao Liu, Jiliang Wang. Combating Link Dynamics for Reliable LoRa Connection in Urban Settings. In Proceedings of ACM MobiCom. 2021. 88 | 9. Chenning Li, Hanqing Guo, Shuai Tong, Xiao Zeng, Zhichao Cao, Mi Zhang, Qiben Yan, Li Xiao, Jiliang Wang, Yunhao Liu. NELoRa: Towards Ultra-low SNR LoRa Communication with Neural-enhanced Demodulation. In Proceedings of ACM SenSys. 2021. 89 | -------------------------------------------------------------------------------- /examples/calc_time_on_air.md: -------------------------------------------------------------------------------- 1 | # Calculating the flying time of a LoRa packet 2 | 3 | ```matlab 4 | rf_freq = 470e6; % carrier frequency 5 | sf = 8; % spreading factor 6 | bw = 125e3; % bandwidth 7 | fs = 1e6; % sampling rate 8 | 9 | phy = LoRaPHY(rf_freq, sf, bw, fs); 10 | phy.has_header = 1; % explicit header mode 11 | phy.cr = 1; % code rate = 4/5 (1:4/5 2:4/6 3:4/7 4:4/8) 12 | phy.crc = 0; % disable payload CRC checksum 13 | phy.preamble_len = 8; % preamble: 8 basic upchirps 14 | 15 | bytes_num = 10; 16 | fprintf("SF%d, BW %dkHz, %d bytes packet, time on air: %.1fms\n", sf, bw/1e3, bytes_num, phy.time_on_air(bytes_num)); 17 | ``` 18 | -------------------------------------------------------------------------------- /examples/gen_specified_symbols.md: -------------------------------------------------------------------------------- 1 | # Generating a LoRa packet with specified symbols 2 | 3 | ```matlab 4 | rf_freq = 470e6; % carrier frequency 5 | sf = 8; % spreading factor 6 | bw = 125e3; % bandwidth 7 | fs = 1e6; % sampling rate 8 | 9 | phy = LoRaPHY(rf_freq, sf, bw, fs); 10 | phy.has_header = 1; % explicit header mode 11 | phy.cr = 1; % code rate = 4/5 (1:4/5 2:4/6 3:4/7 4:4/8) 12 | phy.crc = 0; % disable payload CRC checksum 13 | phy.preamble_len = 8; % preamble: 8 basic upchirps 14 | 15 | % The first 8 data symbols always have form `4n+1` and are encoded with 16 | % code rate = 4/8 17 | d1 = phy.symbols_to_bytes(ones(9,1)); 18 | fprintf("bytes d1:\n"); 19 | disp(d1); 20 | fprintf("symbols:\n"); 21 | disp(phy.encode(d1)); 22 | d2 = phy.symbols_to_bytes([1 5 9 13 1 2 3 4 5 6 7 8]'); 23 | fprintf("bytes d2:\n"); 24 | disp(d2); 25 | fprintf("symbols:\n"); 26 | disp(phy.encode(d2)); 27 | ``` 28 | -------------------------------------------------------------------------------- /examples/load_signal_from_file.m: -------------------------------------------------------------------------------- 1 | addpath('..'); 2 | % load complex signal from file 3 | sig = LoRaPHY.read("./sig.cfile"); 4 | 5 | rf_freq = 470e6; % carrier frequency, used to correct clock drift 6 | sf = 7; % spreading factor 7 | bw = 125e3; % bandwidth 8 | fs = 1e6; % sampling rate 9 | 10 | phy = LoRaPHY(rf_freq, sf, bw, fs); 11 | phy.has_header = 1; % explicit header mode 12 | phy.cr = 4; % code rate = 4/8 (1:4/5 2:4/6 3:4/7 4:4/8) 13 | phy.crc = 1; % enable payload CRC checksum 14 | phy.preamble_len = 8; % preamble: 8 basic upchirps 15 | 16 | % Demodulation 17 | [symbols_d, cfo] = phy.demodulate(sig); 18 | fprintf("[demodulate] symbols:\n"); 19 | disp(symbols_d); 20 | 21 | % Decoding 22 | [data, checksum] = phy.decode(symbols_d); 23 | fprintf("[decode] data:\n"); 24 | disp(data); 25 | fprintf("[decode] checksum:\n"); 26 | disp(checksum); 27 | -------------------------------------------------------------------------------- /examples/save_signal_to_file.m: -------------------------------------------------------------------------------- 1 | addpath('..'); 2 | rf_freq = 470e6; % carrier frequency, used to correct clock drift 3 | sf = 7; % spreading factor 4 | bw = 125e3; % bandwidth 5 | fs = 1e6; % sampling rate 6 | 7 | phy = LoRaPHY(rf_freq, sf, bw, fs); 8 | phy.has_header = 1; % explicit header mode 9 | phy.cr = 4; % code rate = 4/8 (1:4/5 2:4/6 3:4/7 4:4/8) 10 | phy.crc = 1; % enable payload CRC checksum 11 | phy.preamble_len = 8; % preamble: 8 basic upchirps 12 | 13 | % Encode payload [1 2 3 4 5] 14 | symbols = phy.encode((1:5)'); 15 | fprintf("[encode] symbols:\n"); 16 | disp(symbols); 17 | 18 | % Baseband Modulation 19 | sig = phy.modulate(symbols); 20 | 21 | % save the complex signal to file 22 | LoRaPHY.write(sig, './sig.cfile'); 23 | -------------------------------------------------------------------------------- /examples/test_fast_mode.m: -------------------------------------------------------------------------------- 1 | addpath('..'); 2 | rf_freq = 470e6; % carrier frequency, used to correct clock drift 3 | sf = 7; % spreading factor 4 | bw = 125e3; % bandwidth 5 | fs = 1e6; % sampling rate 6 | 7 | phy = LoRaPHY(rf_freq, sf, bw, fs); 8 | phy.has_header = 1; % explicit header mode 9 | phy.cr = 4; % code rate = 4/8 (1:4/5 2:4/6 3:4/7 4:4/8) 10 | phy.crc = 1; % enable payload CRC checksum 11 | phy.preamble_len = 8; % preamble: 8 basic upchirps 12 | 13 | % Encode payload [1 2 3 4 5] 14 | origin_data = (1:16)'; 15 | symbols = phy.encode(origin_data); 16 | % Baseband Modulation 17 | sig = phy.modulate(symbols); 18 | 19 | snr_list = -9:1:-6; 20 | total_rounds = 50; 21 | 22 | warning('off','all') 23 | 24 | % Fast mode runs faster but has lower sensitivity 25 | fprintf("Fast Mode\n"); 26 | phy.fast_mode = true; 27 | ber_list1 = benchmark(phy, snr_list, sig, fs, bw, total_rounds, origin_data); 28 | 29 | % Non-Fast mode runs slower but has higher sensitivity 30 | fprintf("Non-Fast Mode\n"); 31 | phy.fast_mode = false; 32 | ber_list2 = benchmark(phy, snr_list, sig, fs, bw, total_rounds, origin_data); 33 | 34 | markersize = 8; 35 | linewidth = 3; 36 | plot(snr_list, ber_list1, 'k', 'MarkerSize', markersize, 'LineWidth', linewidth); 37 | hold on 38 | plot(snr_list, ber_list2, 'k--', 'MarkerSize', markersize, 'LineWidth', linewidth); 39 | xlabel('SNR (dB)'); 40 | ylabel('Bit Error Rate'); 41 | set(gca, 'FontName', 'Arial', 'FontSize', 16, 'GridLineStyle', '--'); 42 | lgnd = legend('Fast Mode', 'Non-Fast Mode', 'Location', 'Northeast'); 43 | grid on 44 | 45 | warning('on','all') 46 | 47 | function ber_list = benchmark(phy, snr_list, sig, fs, bw, total_rounds, origin_data) 48 | tic 49 | ber_list = []; 50 | for snr = snr_list 51 | tmp_ber_list = zeros(total_rounds, 1); 52 | for ii = 1:total_rounds 53 | new_sig = awgn(sig, snr-10*log10(fs/bw)); 54 | try 55 | [symbols_d, ~, ~] = phy.demodulate(new_sig); 56 | [data, ~] = phy.decode(symbols_d); 57 | tmp_ber_list(ii) = calc_ber(data, origin_data); 58 | catch 59 | tmp_ber_list(ii) = 0.5; 60 | end 61 | 62 | end 63 | ber_list = [ber_list; mean(tmp_ber_list)]; 64 | end 65 | toc 66 | end 67 | 68 | function ber = calc_ber(res, ground_truth) 69 | len1 = length(res); 70 | len2 = length(ground_truth); 71 | len = min(len1, len2); 72 | if len > 0 73 | res = res(1:len); 74 | ground_truth = ground_truth(1:len); 75 | err_bits = sum(xor(de2bi(res, 8), de2bi(ground_truth, 8)), 'all') + 8 * (len2 - len); 76 | ber = err_bits / (len2 * 8); 77 | else 78 | ber = 0.5; 79 | end 80 | end 81 | --------------------------------------------------------------------------------