diff --git a/BUILD.MD b/BUILD.MD index bedeca0..34d311d 100644 --- a/BUILD.MD +++ b/BUILD.MD @@ -8,3 +8,9 @@ lazbuild -B --ws=cocoa ewsdr.lpr # Optional DMR AMBE+2 adapter (requires mbelib development files) cmake -S native/dmr -B build/dmr -DCMAKE_BUILD_TYPE=Release cmake --build build/dmr + +# DMR receive/protocol tests +fpc -Fu. -FE/tmp tests/dmr_protocol_test.pas +/tmp/dmr_protocol_test +fpc -Fu. -FE/tmp tests/dmr_frontend_test.pas +/tmp/dmr_frontend_test diff --git a/DMRDecoder.pas b/DMRDecoder.pas index 01ea46d..78f5191 100644 --- a/DMRDecoder.pas +++ b/DMRDecoder.pas @@ -18,7 +18,7 @@ unit DMRDecoder; interface uses - Classes, SysUtils, SyncObjs, Math, DMRBindings; + Classes, SysUtils, SyncObjs, Math, DMRBindings, DMRProtocol; const DMR_INPUT_RATE = 48000; @@ -49,6 +49,13 @@ type SignalRMS: Single; LastSyncAgeMs: QWord; VocoderAvailable: Boolean; + CACHValid: Boolean; + Slot: Integer; + LCSS: Integer; + SlotTypeValid: Boolean; + ColorCode: Integer; + DataType: Integer; + SlotTypeCorrectedBits: Integer; end; TDMRDecoder = class; @@ -90,9 +97,10 @@ type FMagSum: array[0..DMR_SAMPLES_PER_SYM - 1] of Double; FBestSyncQuality: Double; FTrackPhase: Integer; - FCurrentBurst: array[0..DMR_BURST_DIBITS - 1] of Byte; + FCurrentBurst: TDMRDibitBurst; FCurrentBurstPos: Integer; - FLastBurst: array[0..DMR_BURST_DIBITS - 1] of Byte; + FLastBurst: TDMRDibitBurst; + FLastBurstInfo: TDMRBurstInfo; FBurstCount: QWord; FRMSSq: Double; @@ -250,6 +258,7 @@ begin FBestSyncQuality := 0; FillChar(FCurrentBurst, SizeOf(FCurrentBurst), 0); FillChar(FLastBurst, SizeOf(FLastBurst), 0); + FillChar(FLastBurstInfo, SizeOf(FLastBurstInfo), 0); FTrackPhase := -1; FCurrentBurstPos := 0; FBurstCount := 0; @@ -435,6 +444,7 @@ end; procedure TDMRDecoder.CompleteBurst; begin Move(FCurrentBurst[0], FLastBurst[0], SizeOf(FLastBurst)); + DMRParseBurst(FLastBurst, FLastBurstInfo); Inc(FBurstCount); FCurrentBurstPos := 0; end; @@ -494,6 +504,21 @@ begin (S.LastSyncAgeMs <= DMR_SYNC_HOLD_MS); S.SignalRMS := Sqrt(FRMSSq); S.VocoderAvailable := FMbe <> nil; + // CACH exists on base-station bursts. On MS/direct bursts the same + // positions are payload/guard symbols and can accidentally form a + // syntactically valid Hamming word. + S.CACHValid := FLastBurstInfo.CACH.Valid and + (FSyncKind in [dskBSData, dskBSVoice]); + S.Slot := FLastBurstInfo.CACH.Slot; + S.LCSS := FLastBurstInfo.CACH.LCSS; + // Voice sync replaces the centre Slot Type field. Do not expose a + // chance Golay match from AMBE bits as a real colour/data type. + S.SlotTypeValid := FLastBurstInfo.SlotType.Valid and + (FSyncKind in [dskBSData, dskMSData, + dskDirectTS1Data, dskDirectTS2Data]); + S.ColorCode := FLastBurstInfo.SlotType.ColorCode; + S.DataType := FLastBurstInfo.SlotType.DataType; + S.SlotTypeCorrectedBits := FLastBurstInfo.SlotType.CorrectedBits; finally FStatusLock.Leave; end; diff --git a/DMRProtocol.pas b/DMRProtocol.pas new file mode 100644 index 0000000..5c6ace0 --- /dev/null +++ b/DMRProtocol.pas @@ -0,0 +1,231 @@ +unit DMRProtocol; + +{ + Small, allocation-free DMR burst protocol layer. + + The CACH interleave and Hamming/Golay generator matrices are derived from + dsd-fme (src/dmr_bs.c and src/fec.c). The original code is distributed + under the ISC licence; see the dsd-fme source tree for its full notice. +} + +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} + +interface + +uses + SysUtils; + +const + DMR_PROTOCOL_BURST_DIBITS = 144; + +type + TDMRDibitBurst = array[0..DMR_PROTOCOL_BURST_DIBITS - 1] of Byte; + TDMRBit7 = array[0..6] of Byte; + TDMRBit20 = array[0..19] of Byte; + + TDMRCACH = record + Valid: Boolean; + CorrectedBits: Integer; + AccessType: Boolean; + Slot: Integer; + LCSS: Integer; + end; + + TDMRSlotType = record + Valid: Boolean; + CorrectedBits: Integer; + ColorCode: Integer; + DataType: Integer; + end; + + TDMRBurstInfo = record + CACH: TDMRCACH; + SlotType: TDMRSlotType; + end; + +function DMRHamming74Encode(Value: Byte): TDMRBit7; +function DMRGolay208Encode(Value: Byte): TDMRBit20; +function DMRDecodeCACH(const Burst: TDMRDibitBurst; out CACH: TDMRCACH): Boolean; +function DMRDecodeSlotType(const Burst: TDMRDibitBurst; + out SlotType: TDMRSlotType): Boolean; +procedure DMRParseBurst(const Burst: TDMRDibitBurst; out Info: TDMRBurstInfo); +function DMRDataTypeName(DataType: Integer): string; + +implementation + +const + CACH_INTERLEAVE: array[0..23] of Byte = ( + 0, 7, 8, 9, 1, 10, + 11, 12, 2, 13, 14, 15, + 3, 16, 4, 17, 18, 19, + 5, 20, 21, 22, 6, 23 + ); + + HAMMING_7_4_G: array[0..3, 0..6] of Byte = ( + (1, 0, 0, 0, 1, 0, 1), + (0, 1, 0, 0, 1, 1, 1), + (0, 0, 1, 0, 1, 1, 0), + (0, 0, 0, 1, 0, 1, 1) + ); + + GOLAY_20_8_G: array[0..7, 0..19] of Byte = ( + (1,0,0,0,0,0,0,0, 0,0,1,1,1,1,0,1,1,0,1,0), + (0,1,0,0,0,0,0,0, 1,1,0,1,1,0,0,1,1,0,0,1), + (0,0,1,0,0,0,0,0, 0,1,1,0,1,1,0,0,1,1,0,1), + (0,0,0,1,0,0,0,0, 0,0,1,1,0,1,1,0,0,1,1,1), + (0,0,0,0,1,0,0,0, 1,1,0,1,1,1,0,0,0,1,1,0), + (0,0,0,0,0,1,0,0, 1,0,1,0,1,0,0,1,0,1,1,1), + (0,0,0,0,0,0,1,0, 1,0,0,1,0,0,1,1,1,1,1,0), + (0,0,0,0,0,0,0,1, 1,0,0,0,1,1,1,0,1,0,1,1) + ); + +function DMRHamming74Encode(Value: Byte): TDMRBit7; +var + i, j: Integer; +begin + FillChar(Result, SizeOf(Result), 0); + for i := 0 to 3 do + if ((Value shr (3 - i)) and 1) <> 0 then + for j := 0 to 6 do Result[j] := Result[j] xor HAMMING_7_4_G[i, j]; +end; + +function DMRGolay208Encode(Value: Byte): TDMRBit20; +var + i, j: Integer; +begin + FillChar(Result, SizeOf(Result), 0); + for i := 0 to 7 do + if ((Value shr (7 - i)) and 1) <> 0 then + for j := 0 to 19 do Result[j] := Result[j] xor GOLAY_20_8_G[i, j]; +end; + +function DecodeHamming74(const Received: TDMRBit7; out Value: Byte; + out Corrected: Integer): Boolean; +var + Candidate: TDMRBit7; + V, i, Distance, BestDistance: Integer; +begin + Value := 0; + BestDistance := 8; + for V := 0 to 15 do + begin + Candidate := DMRHamming74Encode(V); + Distance := 0; + for i := 0 to 6 do Inc(Distance, Candidate[i] xor (Received[i] and 1)); + if Distance < BestDistance then + begin + BestDistance := Distance; + Value := V; + end; + end; + Corrected := BestDistance; + Result := BestDistance <= 1; +end; + +function DecodeGolay208(const Received: TDMRBit20; out Value: Byte; + out Corrected: Integer): Boolean; +var + Candidate: TDMRBit20; + V, i, Distance, BestDistance: Integer; +begin + Value := 0; + BestDistance := 21; + for V := 0 to 255 do + begin + Candidate := DMRGolay208Encode(V); + Distance := 0; + for i := 0 to 19 do Inc(Distance, Candidate[i] xor (Received[i] and 1)); + if Distance < BestDistance then + begin + BestDistance := Distance; + Value := V; + if Distance = 0 then Break; + end; + end; + Corrected := BestDistance; + Result := BestDistance <= 2; +end; + +function DMRDecodeCACH(const Burst: TDMRDibitBurst; out CACH: TDMRCACH): Boolean; +var + Deinterleaved: array[0..23] of Byte; + TACT: TDMRBit7; + Value: Byte; + i: Integer; +begin + FillChar(CACH, SizeOf(CACH), 0); + FillChar(Deinterleaved, SizeOf(Deinterleaved), 0); + for i := 0 to 11 do + begin + Deinterleaved[CACH_INTERLEAVE[2 * i]] := (Burst[i] shr 1) and 1; + Deinterleaved[CACH_INTERLEAVE[2 * i + 1]] := Burst[i] and 1; + end; + for i := 0 to 6 do TACT[i] := Deinterleaved[i]; + CACH.Valid := DecodeHamming74(TACT, Value, CACH.CorrectedBits); + if CACH.Valid then + begin + CACH.AccessType := (Value and 8) <> 0; + CACH.Slot := (Value shr 2) and 1; + CACH.LCSS := Value and 3; + end; + Result := CACH.Valid; +end; + +function DMRDecodeSlotType(const Burst: TDMRDibitBurst; + out SlotType: TDMRSlotType): Boolean; +var + Bits: TDMRBit20; + Value: Byte; + i, k: Integer; +begin + FillChar(SlotType, SizeOf(SlotType), 0); + k := 0; + for i := 61 to 65 do + begin + Bits[k] := (Burst[i] shr 1) and 1; Inc(k); + Bits[k] := Burst[i] and 1; Inc(k); + end; + for i := 90 to 94 do + begin + Bits[k] := (Burst[i] shr 1) and 1; Inc(k); + Bits[k] := Burst[i] and 1; Inc(k); + end; + SlotType.Valid := DecodeGolay208(Bits, Value, SlotType.CorrectedBits); + if SlotType.Valid then + begin + SlotType.ColorCode := (Value shr 4) and $0F; + SlotType.DataType := Value and $0F; + end; + Result := SlotType.Valid; +end; + +procedure DMRParseBurst(const Burst: TDMRDibitBurst; out Info: TDMRBurstInfo); +begin + FillChar(Info, SizeOf(Info), 0); + DMRDecodeCACH(Burst, Info.CACH); + DMRDecodeSlotType(Burst, Info.SlotType); +end; + +function DMRDataTypeName(DataType: Integer): string; +begin + case DataType of + 0: Result := 'PI header'; + 1: Result := 'voice LC'; + 2: Result := 'terminator LC'; + 3: Result := 'CSBK'; + 4: Result := 'MBC header'; + 5: Result := 'MBC continuation'; + 6: Result := 'data header'; + 7: Result := 'rate 1/2 data'; + 8: Result := 'rate 3/4 data'; + 9: Result := 'idle'; + 10: Result := 'rate 1 data'; + 11: Result := 'unified single block'; + else + Result := 'reserved ' + IntToStr(DataType); + end; +end; + +end. diff --git a/MainForm.pas b/MainForm.pas index 7b07eb2..aa127af 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -3587,14 +3587,19 @@ end; function TMainForm.DMRStatusText: string; var S: TDMRStatus; - Inv: string; + Inv, Details: string; begin if not FController.GetDMRStatus(S) then Exit('DMR unavailable'); if not S.Enabled then Exit('DMR off'); if not S.Synced then Exit(Format('DMR sync... %.3f', [S.SignalRMS])); if S.Inverted then Inv := ' INV' else Inv := ''; - Result := Format('DMR %s%s #%d', - [TDMRDecoder.SyncKindName(S.SyncKind), Inv, S.SyncCount]); + Details := ''; + if S.CACHValid then Details := Format(' TS%d', [S.Slot + 1]); + if S.SlotTypeValid then + Details := Details + Format(' CC%d/DT%d', [S.ColorCode, S.DataType]); + Result := Format('DMR %s%s%s #%d', + [TDMRDecoder.SyncKindName(S.SyncKind), Details, Inv, + S.SyncCount]); end; procedure TMainForm.UpdateStatusField7; diff --git a/tests/dmr_protocol_test.pas b/tests/dmr_protocol_test.pas new file mode 100644 index 0000000..d18dd4d --- /dev/null +++ b/tests/dmr_protocol_test.pas @@ -0,0 +1,94 @@ +program dmr_protocol_test; + +{$MODE Delphi} + +uses + SysUtils, DMRProtocol; + +const + CACH_INTERLEAVE: array[0..23] of Byte = ( + 0, 7, 8, 9, 1, 10, + 11, 12, 2, 13, 14, 15, + 3, 16, 4, 17, 18, 19, + 5, 20, 21, 22, 6, 23 + ); + +procedure PutCACH(var Burst: TDMRDibitBurst; Value: Byte); +var + Code: TDMRBit7; + Deinterleaved, Transmitted: array[0..23] of Byte; + i: Integer; +begin + FillChar(Deinterleaved, SizeOf(Deinterleaved), 0); + Code := DMRHamming74Encode(Value); + for i := 0 to 6 do Deinterleaved[i] := Code[i]; + for i := 0 to 23 do Transmitted[i] := Deinterleaved[CACH_INTERLEAVE[i]]; + for i := 0 to 11 do + Burst[i] := (Transmitted[2 * i] shl 1) or Transmitted[2 * i + 1]; +end; + +procedure PutSlotType(var Burst: TDMRDibitBurst; Value: Byte); +var + Code: TDMRBit20; + i, k: Integer; +begin + Code := DMRGolay208Encode(Value); + k := 0; + for i := 61 to 65 do + begin + Burst[i] := (Code[k] shl 1) or Code[k + 1]; + Inc(k, 2); + end; + for i := 90 to 94 do + begin + Burst[i] := (Code[k] shl 1) or Code[k + 1]; + Inc(k, 2); + end; +end; + +procedure Check(Condition: Boolean; const Message_: string); +begin + if not Condition then + begin + WriteLn(Message_); + Halt(1); + end; +end; + +var + Burst: TDMRDibitBurst; + Info: TDMRBurstInfo; +begin + FillChar(Burst, SizeOf(Burst), 0); + // TACT: AT=1, slot=1 (TS2), LCSS=2. Slot Type: CC=9, data header=6. + PutCACH(Burst, $0E); + PutSlotType(Burst, $96); + DMRParseBurst(Burst, Info); + Check(Info.CACH.Valid, 'valid CACH rejected'); + Check(Info.CACH.AccessType and (Info.CACH.Slot = 1) and + (Info.CACH.LCSS = 2), 'TACT fields decoded incorrectly'); + Check(Info.SlotType.Valid, 'valid Slot Type rejected'); + Check((Info.SlotType.ColorCode = 9) and (Info.SlotType.DataType = 6), + 'Slot Type fields decoded incorrectly'); + + // Hamming(7,4) corrects one bit and Golay(20,8) corrects two. + Burst[0] := Burst[0] xor 2; + Burst[61] := Burst[61] xor 2; + Burst[92] := Burst[92] xor 1; + DMRParseBurst(Burst, Info); + Check(Info.CACH.Valid and (Info.CACH.CorrectedBits = 1), + 'CACH one-bit correction failed'); + Check(Info.CACH.AccessType and (Info.CACH.Slot = 1) and + (Info.CACH.LCSS = 2), 'corrected TACT fields are wrong'); + Check(Info.SlotType.Valid and (Info.SlotType.CorrectedBits = 2), + 'Slot Type two-bit correction failed'); + Check((Info.SlotType.ColorCode = 9) and (Info.SlotType.DataType = 6), + 'corrected Slot Type fields are wrong'); + + // A third Slot Type error exceeds the guaranteed correction radius. + Burst[94] := Burst[94] xor 1; + DMRParseBurst(Burst, Info); + Check(not Info.SlotType.Valid, 'uncorrectable Slot Type accepted'); + + WriteLn('DMR CACH and Slot Type tests passed'); +end.