diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c6080b1..1e44895 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,6 @@ jobs: unit-tests: name: Unit Tests runs-on: ubuntu-latest - steps: - name: Checkout code uses: actions/checkout@v4 @@ -60,4 +59,4 @@ jobs: - name: End SonarQube Analysis env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: dotnet sonarscanner end /d:sonar.login="${SONAR_TOKEN}" \ No newline at end of file + run: dotnet sonarscanner end /d:sonar.login="${SONAR_TOKEN}" diff --git a/src/RP2040.Core/Cpu/InstructionDecoder.cs b/src/RP2040.Core/Cpu/InstructionDecoder.cs index ef99387..ad65893 100644 --- a/src/RP2040.Core/Cpu/InstructionDecoder.cs +++ b/src/RP2040.Core/Cpu/InstructionDecoder.cs @@ -74,10 +74,14 @@ public InstructionDecoder() new OpcodeRule(0xFFC0, 0x4340, &ArithmeticOps.Muls), // MVNS Rd, Rm new OpcodeRule(0xFFC0, 0x43C0, &BitOps.Mvns), - // LSLS Rd, Rm, #0 - new OpcodeRule(0xFFC0, 0x0000, &BitOps.LslsZero), + // LSRS (Register) - Encoding T2 + new OpcodeRule(0xFFC0, 0x40C0, &BitOps.LsrsRegister), // LSLS (Register) - Encoding T2 new OpcodeRule(0xFFC0, 0x4080, &BitOps.LslsRegister), + // LSLS Rd, Rm, #0 + new OpcodeRule(0xFFC0, 0x0000, &BitOps.LslsZero), + // LSRS Rd, Rm, #0 (Shift 32) + new OpcodeRule(0xFFC0, 0x0800, &BitOps.LsrsImm32), // Rev16 Rd, Rn new OpcodeRule(0xFFC0, 0xBA40, &BitOps.Rev16), // REVSH Rd, Rm @@ -178,6 +182,8 @@ public InstructionDecoder() new OpcodeRule(0xF800, 0xC800, &MemoryOps.Ldmia), // LSLS (Rd, Rm, imm5) new OpcodeRule(0xF800, 0x0000, &BitOps.LslsImm5), + // LSRS (Rd, Rm, imm5) + new OpcodeRule(0xF800, 0x0800, &BitOps.LsrsImm5), // ================================================================ // GROUP 9: Mask 0xBF00 // ================================================================ diff --git a/src/RP2040.Core/Cpu/Instructions/BitOps.cs b/src/RP2040.Core/Cpu/Instructions/BitOps.cs index 41df678..92407fd 100644 --- a/src/RP2040.Core/Cpu/Instructions/BitOps.cs +++ b/src/RP2040.Core/Cpu/Instructions/BitOps.cs @@ -140,17 +140,23 @@ public static void LslsRegister(ushort opcode, CortexM0Plus cpu) var valRdn = ptrRdn; var shift = (int)(cpu.Registers[rm] & 0xFF); + if (shift == 0) + { + cpu.Registers.N = (int)valRdn < 0; + cpu.Registers.Z = (valRdn == 0); + return; + } + var extended = (ulong)valRdn << shift; var result = shift >= 32 ? 0 : (uint)extended; var calcCarry = (extended & 0x1_0000_0000) != 0; - var finalCarry = (shift == 0) ? (cpu.Registers.GetC() != 0) : calcCarry; ptrRdn = result; cpu.Registers.N = (int)result < 0; cpu.Registers.Z = (result == 0); - cpu.Registers.C = finalCarry; + cpu.Registers.C = calcCarry; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -168,6 +174,75 @@ public static void LslsZero(ushort opcode, CortexM0Plus cpu) cpu.Registers.Z = (valRm == 0); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void LsrsImm5(ushort opcode, CortexM0Plus cpu) + { + // LSRS Rd, Rm, #imm5 + var imm5 = (opcode >> 6) & 0x1F; + var rm = (opcode >> 3) & 0x7; + var rd = opcode & 0x7; + + ref var ptrRd = ref cpu.Registers[rd]; + var valRm = cpu.Registers[rm]; + + var result = valRm >> imm5; + var carry = ((valRm >> (imm5 - 1)) & 1) != 0; + + ptrRd = result; + + cpu.Registers.N = false; // (result >> 31) will be always 0 if shift >= 1 + cpu.Registers.Z = (result == 0); + cpu.Registers.C = carry; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void LsrsImm32(ushort opcode, CortexM0Plus cpu) + { + // LSRS Rd, Rm, #0 + var rm = (opcode >> 3) & 0x7; + var rd = opcode & 0x7; + + ref var ptrRd = ref cpu.Registers[rd]; + var valRm = cpu.Registers[rm]; + + ptrRd = 0; + + cpu.Registers.N = false; + cpu.Registers.Z = true; + cpu.Registers.C = (int)valRm < 0; // Bit 31 + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void LsrsRegister(ushort opcode, CortexM0Plus cpu) + { + var rdn = opcode & 0x7; + var rm = (opcode >> 3) & 0x7; + + ref var ptrRdn = ref cpu.Registers[rdn]; + var valRdn = ptrRdn; + var shift = (int)(cpu.Registers[rm] & 0xFF); + + // Fast Guard + if (shift == 0) + { + cpu.Registers.N = (int)valRdn < 0; + cpu.Registers.Z = (valRdn == 0); + return; + } + + var result = shift >= 32 ? 0 : valRdn >> shift; + + var rawCarryBit = (valRdn >> ((shift - 1) & 0x1F)) & 1; + var limitMask = (32 - shift) >> 31; + var carry = (rawCarryBit & ~limitMask) != 0; + + ptrRdn = result; + + cpu.Registers.N = (int)result < 0; + cpu.Registers.Z = (result == 0); + cpu.Registers.C = carry; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void MovRegister(ushort opcode, CortexM0Plus cpu) { diff --git a/src/RP2040.Core/Helpers/InstructionEmiter.cs b/src/RP2040.Core/Helpers/InstructionEmiter.cs index 75cb1fb..32c66fd 100644 --- a/src/RP2040.Core/Helpers/InstructionEmiter.cs +++ b/src/RP2040.Core/Helpers/InstructionEmiter.cs @@ -307,6 +307,22 @@ public static ushort LslsRegister(uint rdn, uint rm) return (ushort)(0x4080 | ((rm & 7) << 3) | rdn); } + public static ushort LsrsImm5(uint rd, uint rm, uint imm5) + { + if (rd > 7 || rm > 7) + throw new ArgumentException(LowRegisterIndexOutOfRange); + if (imm5 > 31) + throw new ArgumentException("Immediate too large for LSLS"); + return (ushort)(0x0800 | (imm5 & 0x1F) << 6 | ((rm & 7) << 3) | (rd & 7)); + } + + public static ushort LsrsRegister(uint rdn, uint rm) + { + if (rdn > 7 || rm > 7) + throw new ArgumentException(LowRegisterIndexOutOfRange); + return (ushort)(0x40C0 | ((rm & 7) << 3) | rdn); + } + public static ushort Revsh(uint rd, uint rm) { if (rd > 7 || rm > 7) diff --git a/tests/RP2040.Core.Tests/Cpu/InstructionDecoderTests.cs b/tests/RP2040.Core.Tests/Cpu/InstructionDecoderTests.cs index 91cb326..e19d065 100644 --- a/tests/RP2040.Core.Tests/Cpu/InstructionDecoderTests.cs +++ b/tests/RP2040.Core.Tests/Cpu/InstructionDecoderTests.cs @@ -101,6 +101,9 @@ public static TheoryData GetInstructionTestCases() Add("LslsImm", InstructionEmiter.LslsImm5(R5, R5, 18), &BitOps.LslsImm5); Add("LslsImmZero", InstructionEmiter.LslsImm5(R5, R5, 0), &BitOps.LslsZero); Add("LslsRegister", InstructionEmiter.LslsRegister(R5, R0), &BitOps.LslsRegister); + Add("LsrsImm", InstructionEmiter.LsrsImm5(R5, R5, 18), &BitOps.LsrsImm5); + Add("LsrsImm32", InstructionEmiter.LsrsImm5(R1, R1, 0), &BitOps.LsrsImm32); + Add("LsrsRegister", InstructionEmiter.LsrsRegister(R5, R0), &BitOps.LsrsRegister); Add("Mvns", InstructionEmiter.Mvns(R0, R2), &BitOps.Mvns); Add("Orrs", InstructionEmiter.Orrs(R5, R0), &BitOps.Orrs); Add("Rev", InstructionEmiter.Rev(R0, R1), &BitOps.Rev); diff --git a/tests/RP2040.Core.Tests/Cpu/Instructions/BitOpsTests.cs b/tests/RP2040.Core.Tests/Cpu/Instructions/BitOpsTests.cs index 3cb2c8a..1d719fc 100644 --- a/tests/RP2040.Core.Tests/Cpu/Instructions/BitOpsTests.cs +++ b/tests/RP2040.Core.Tests/Cpu/Instructions/BitOpsTests.cs @@ -374,7 +374,7 @@ public void Should_BitwiseInvert_Value_And_UpdateNegativeFlag() } } - public class Lsls + public abstract class Lsls { public class Immediate : CpuTestBase { @@ -479,6 +479,234 @@ public void Should_ResultInZero_And_SetCarry_When_ShiftingBy32() Cpu.Registers.Z.Should().BeTrue(); // Result is 0 Cpu.Registers.C.Should().BeTrue(); // Bit 0 shifted out } + + [Fact] + public void Should_ResultInZero_And_ClearCarry_When_ShiftingLeftByMoreThan32() + { + // Arrange + var opcode = InstructionEmiter.LslsRegister(R0, R1); + Bus.WriteHalfWord(0x20000000, opcode); + + Cpu.Registers[R0] = 0xFFFFFFFF; + Cpu.Registers[R1] = 33; // Shift 33 + Cpu.Registers.C = true; + + // Act + Cpu.Step(); + + // Assert + Cpu.Registers[R0].Should().Be(0); + Cpu.Registers.Z.Should().BeTrue(); + Cpu.Registers.C.Should().BeFalse("LSL > 32 should set Carry to 0"); + } + + [Fact] + public void Should_InterpretShiftRegister_AsUnsignedByte() + { + // Arrange + var opcode = InstructionEmiter.LslsRegister(R0, R1); + Bus.WriteHalfWord(0x20000000, opcode); + + Cpu.Registers[R0] = 1; + Cpu.Registers[R1] = 0xFFFFFFFF; // Bottom byte is 0xFF (255) + + // Act + Cpu.Step(); + + // Assert + Cpu.Registers[R0].Should().Be(0); // 1 << 255 is 0 + Cpu.Registers.C.Should().BeFalse(); // Carry from huge shift is 0 + } + } + } + + public abstract class Lsrs + { + public class Immediate : CpuTestBase + { + [Fact] + public void Should_ShiftRight_ByImmediateValue() + { + // Port from JS: 'should execute a `lsrs r1, r1, #1` instruction' + // Arrange + var opcode = InstructionEmiter.LsrsImm5(R1, R1, 1); + Bus.WriteHalfWord(0x20000000, opcode); + + Cpu.Registers[R1] = 0b10; // 2 + Cpu.Registers.C = true; // Dirty Carry + + // Act + Cpu.Step(); + + // Assert + Cpu.Registers[R1].Should().Be(0b1); + Cpu.Registers.PC.Should().Be(0x20000002); + Cpu.Registers.C.Should().BeFalse(); // Bit 0 (0) shifted out + Cpu.Registers.N.Should().BeFalse(); + Cpu.Registers.Z.Should().BeFalse(); + } + + [Fact] + public void Should_PerformLogicalShiftRightBy32_When_ImmediateIsZero() + { + // Arrange + var opcode = InstructionEmiter.LsrsImm5(R1, R1, 0); + Bus.WriteHalfWord(0x20000000, opcode); + + Cpu.Registers[R1] = 0xFFFFFFFF; + + // Act + Cpu.Step(); + + // Assert + Cpu.Registers[R1].Should().Be(0u); + Cpu.Registers.C.Should().BeTrue(); + } + + [Fact] + public void Should_SetCarryFlag_When_OneIsShiftedOut() + { + // New Test: Explicitly checking carry behavior on standard shift + // Arrange + var opcode = InstructionEmiter.LsrsImm5(R0, R0, 1); + Bus.WriteHalfWord(0x20000000, opcode); + + Cpu.Registers[R0] = 0b11; // 3 -> Shift right 1 -> 1, Carry = 1 + Cpu.Registers.C = false; + + // Act + Cpu.Step(); + + // Assert + Cpu.Registers[R0].Should().Be(1); + Cpu.Registers.C.Should().BeTrue(); + } + + [Fact] + public void Should_ShiftRight_ByMaximumImmediate_31() + { + // Arrange + var opcode = InstructionEmiter.LsrsImm5(R0, R0, 31); + Bus.WriteHalfWord(0x20000000, opcode); + + Cpu.Registers[R0] = 0x80000000; // Bit 31 set + Cpu.Registers.C = true; + + // Act + Cpu.Step(); + + // Assert + Cpu.Registers[R0].Should().Be(1); // 0x80000000 >> 31 = 1 + Cpu.Registers.C.Should().BeFalse("Bit shifted out (bit 30) was 0"); + } + } + + public class Register : CpuTestBase + { + [Fact] + public void Should_Execute_LsrsRegister_StandardCase() + { + // Arrange + var opcode = InstructionEmiter.LsrsRegister(R5, R0); + Bus.WriteHalfWord(0x20000000, opcode); + + Cpu.Registers[R5] = 0xff00000f; + Cpu.Registers[R0] = 0xff003302; + + // Act + Cpu.Step(); + + // Assert + Cpu.Registers[R5].Should().Be(0x3fc00003); + Cpu.Registers.PC.Should().Be(0x20000002); + Cpu.Registers.C.Should().BeTrue(); + } + + [Fact] + public void Should_ResultInZero_When_ShiftingBy32() + { + // Port from JS: 'should return zero for `lsrs r2, r3` with 32 bit shift' + // Arrange + var opcode = InstructionEmiter.LsrsRegister(R2, R3); + Bus.WriteHalfWord(0x20000000, opcode); + + Cpu.Registers[R2] = 10; // ...00001010 + Cpu.Registers[R3] = 32; + Cpu.Registers.C = true; + + // Act + Cpu.Step(); + + // Assert + Cpu.Registers[R2].Should().Be(0); + Cpu.Registers.Z.Should().BeTrue(); + // Bit 31 of '10' is 0, so Carry should be 0 + Cpu.Registers.C.Should().BeFalse(); + } + + [Fact] + public void Should_ResultInZero_And_ClearCarry_When_ShiftingByMoreThan32() + { + // New Test: Edge case > 32 + // Arrange + var opcode = InstructionEmiter.LsrsRegister(R2, R3); + Bus.WriteHalfWord(0x20000000, opcode); + + Cpu.Registers[R2] = 0xFFFFFFFF; + Cpu.Registers[R3] = 33; // Shift > 32 + Cpu.Registers.C = true; + + // Act + Cpu.Step(); + + // Assert + Cpu.Registers[R2].Should().Be(0); + Cpu.Registers.Z.Should().BeTrue(); + Cpu.Registers.C.Should().BeFalse(); // For shift > 32, C becomes 0 + } + + [Fact] + public void Should_PreserveValue_And_PreserveCarry_When_ShiftIsZero() + { + // New Test: Shift by 0 (Register variant) + // Note: Unlike Immediate #0 (which means 32), Register 0 means 0. + // Arrange + var opcode = InstructionEmiter.LsrsRegister(R0, R1); + Bus.WriteHalfWord(0x20000000, opcode); + + Cpu.Registers[R0] = 0x12345678; + Cpu.Registers[R1] = 0; + Cpu.Registers.C = true; // Should persist + + // Act + Cpu.Step(); + + // Assert + Cpu.Registers[R0].Should().Be(0x12345678); + Cpu.Registers.C.Should() + .BeTrue("Shift by 0 in register mode should not affect Carry"); + Cpu.Registers.Z.Should().BeFalse(); + } + + [Fact] + public void Should_SetCarryToBit31_When_ShiftingBy32() + { + // Critical case: LSR by 32. Result is 0, Carry becomes original Bit 31. + // Arrange + var opcode = InstructionEmiter.LsrsRegister(R0, R1); + Bus.WriteHalfWord(0x20000000, opcode); + + Cpu.Registers[R0] = 0x80001234; // Bit 31 is 1 + Cpu.Registers[R1] = 32; + Cpu.Registers.C = false; + + // Act + Cpu.Step(); + + // Assert + Cpu.Registers[R0].Should().Be(0); + Cpu.Registers.C.Should().BeTrue("LSR by 32 should set Carry to original Bit 31"); + } } }