diff --git a/memory/MEMORY.md b/memory/MEMORY.md new file mode 100644 index 0000000..3d645bb --- /dev/null +++ b/memory/MEMORY.md @@ -0,0 +1,58 @@ +# AVR8Sharp Project Memory + +## Project Structure +- Solution: `AVR8Sharp.sln` at repo root +- Core lib: `src/AVR8Sharp.Core/` (multi-target: net6, net8, netstandard1.2, netstandard2.0, net10) +- TestKit: `src/Avr8Sharp.TestKit/` (net10, FluentAssertions 6.12.0) +- Tests: `tests/Avr8Sharp.Tests/` (NUnit, net10) +- Demo: `src/Avr8Sharp.Demo/` (net10) + +## TestKit Architecture (created 2026-02-28) +See `memory/testkit.md` for details. + +Key entry point: +```csharp +var sim = AvrTestSimulation.Create() + .WithFrequency(16_000_000) + .WithHex(hexString) // or .WithAsm() / .WithProgram() + .AddGpio(AvrIoPort.PortBConfig, out var portB) + .AddUsart(AvrUsart.Usart0Config, out var serial) + .AddTimer(AvrTimer.Timer0Config); + +sim.RunMilliseconds(500); + +portB.Should().HavePinHigh(5); +serial.Should().Contain("Hello"); +sim.Cpu.Should().HaveRegister(24, 42); +sim.Memory.Should().HaveByteAt(0x200, 0xFF); +``` + +Pre-configured boards (src/Avr8Sharp.TestKit/Boards/): +- ArduinoUnoSimulation — ATmega328P, 16 MHz, PortB/C/D, Timer0/1/2, Serial +- ArduinoMegaSimulation — ATmega2560, 16 MHz, PortA-L, Timer0-4, Serial0/1/2 +- ATtiny85Simulation — ATtiny85, 8 MHz, PortB, Timer0 + +CI/CD workflows (.github/workflows/): +- ci.yml — build + test on every push/PR (GitHub + Gitea compatible) +- publish.yml — pack + push NuGet on tag/main/dispatch; uses secrets: + NUGET_PRIVATE_SOURCE_URL, NUGET_PRIVATE_API_KEY, NUGET_PUBLIC_API_KEY +- build-nugets.yml — deprecated stub (replaced by publish.yml) + +NuGet.Config: XML comments must NOT contain '--' (invalid XML). + +## Key Namespaces +- `Avr8Sharp.TestKit` — AvrTestSimulation, AvrMemoryView, AssertionExtensions +- `Avr8Sharp.TestKit.Probes` — SerialProbe +- `Avr8Sharp.TestKit.Assertions` — AvrCpuAssertions, AvrGpioAssertions, SerialProbeAssertions, AvrMemoryAssertions + +## Core CPU Facts +- `Cpu.Data[0..31]` = general registers R0–R31 +- `Cpu.Data[93..94]` = SP (little-endian via DataView) +- `Cpu.Data[95]` = SREG +- `Cpu.PC` = word address (byte address = PC × 2) +- `Cpu.Cycles` = int (wraps ~134 s at 16 MHz) +- BREAK opcode = 0x9598 + +## Existing Test Utilities +`tests/Avr8Sharp.Tests/Utils.cs` has `TestProgramRunner` and `AsmProgram()` helpers +(these are superseded by TestKit for new tests). diff --git a/memory/testkit.md b/memory/testkit.md new file mode 100644 index 0000000..7da4c68 --- /dev/null +++ b/memory/testkit.md @@ -0,0 +1,71 @@ +# Avr8Sharp.TestKit — Design Notes + +## Files Created +``` +src/Avr8Sharp.TestKit/ +├── Avr8Sharp.TestKit.csproj # FluentAssertions 6.12.0 + Core ref +├── AvrTestSimulation.cs # Fluent builder + execution engine +├── AvrMemoryView.cs # Read-only SRAM wrapper for assertions +├── Probes/ +│ └── SerialProbe.cs # Captures USART TX bytes +├── Assertions/ +│ ├── AvrCpuAssertions.cs # HaveRegister, HavePC, HaveSP, SREG flags +│ ├── SerialProbeAssertions.cs # Contain, StartWith, ContainLine, etc. +│ ├── AvrGpioAssertions.cs # HavePinHigh, HavePinLow, HaveOutputValue +│ └── AvrMemoryAssertions.cs # HaveByteAt, HaveWordAt, HaveBytesAt +└── Extensions/ + └── AssertionExtensions.cs # .Should() for AvrCpu, SerialProbe, AvrIoPort, AvrMemoryView +``` + +## AvrTestSimulation API + +### Configuration (builder methods, all return `this`) +- `.Create(flashSize, sramBytes)` — static factory (default: 0x8000, 8192) +- `.WithFrequency(uint hz)` — default 16 MHz +- `.LoadHex(string)` — Intel HEX (gcc/avra output) +- `.LoadAsm(string)` — inline assembly via AvrAssembler +- `.LoadBytes(byte[])` — raw binary +- `.AddGpio(AvrPortConfig, out AvrIoPort)` +- `.AddUsart(AvrUsartConfig, out SerialProbe)` — auto-captures TX +- `.AddUsart(AvrUsartConfig)` — no capture +- `.AddTimer(AvrTimerConfig)` / `.AddTimer(config, out AvrTimer)` +- `.AddSpi(AvrSpiConfig, out AvrSpi)` +- `.AddTwi(AvrTwiConfig, out AvrTwi)` +- `.AddAdc(AvrAdcConfig, out AvrAdc)` + +### Execution (all return `this`) +- `.RunCycles(long)` — precise cycle count +- `.RunMilliseconds(double)` — simulated time +- `.RunInstructions(int)` — instruction count +- `.RunUntil(Func, maxInstructions=100_000)` +- `.RunToBreak(maxInstructions=100_000)` — stops AT BREAK (0x9598), not executing it +- `.RunToAddress(int byteAddress, maxInstructions=100_000)` + +## Assertion Classes + +### AvrCpuAssertions (`cpu.Should()`) +- `.HaveRegister(index, byte)` — R0–R31 +- `.HavePC(uint)` — word address +- `.HaveSP(ushort)` +- `.HaveCycles(int)` +- `.HaveSreg(byte)` — raw SREG byte +- `.HaveCarryFlag(bool=true)` / `.HaveZeroFlag` / `.HaveNegativeFlag` / `.HaveOverflowFlag` / `.HaveHalfCarryFlag` / `.HaveInterruptsEnabled` + +### SerialProbeAssertions (`serial.Should()`) +- `.Contain(string)` / `.NotContain` / `.StartWith` / `.EndWith` / `.Be` +- `.BeEmpty()` +- `.HaveLineCount(int)` / `.ContainLine(string)` + +### AvrGpioAssertions (`portB.Should()`) +- `.HavePinHigh(int)` / `.HavePinLow(int)` / `.HavePinInput(int)` / `.HavePinInputPullup(int)` +- `.HavePinState(int, PinState)` +- `.HaveOutputValue(byte)` — bitmask of all 8 output pins + +### AvrMemoryAssertions (`sim.Memory.Should()`) +- `.HaveByteAt(address, byte)` / `.HaveWordAt(address, ushort)` / `.HaveWordBEAt` / `.HaveBytesAt(address, byte[])` + +## SerialProbe +- `probe.Text` — all captured output as string +- `probe.Lines` — split on '\n', '\r' trimmed +- `probe.Clear()` — reset buffer +- `probe.InjectByte(byte)` — simulate incoming RX byte (calls `AvrUsart.WriteByte`) diff --git a/src/Avr8Sharp/Core/Utils/Assembler.cs b/src/Avr8Sharp/Core/Utils/Assembler.cs index 502ada4..99b15b5 100644 --- a/src/Avr8Sharp/Core/Utils/Assembler.cs +++ b/src/Avr8Sharp/Core/Utils/Assembler.cs @@ -318,8 +318,12 @@ public partial class AvrAssembler r |= ((k & 0xf0) << 4) | (k & 0xf); return ZeroPad (r); }}, - { "LDS", (a, b, _, _) => { - var k = ConstValue(b, 0, 65535); + { "LDS", (a, b, byteLoc, labels) => { + var k = ConstOrLabel(b, labels); + if (k == int.MinValue) { + return new Func, object> ((l) => OpTable?["LDS"](a, b, byteLoc, l) ?? new KeyValuePair(string.Empty, string.Empty)); + } + k = ConstValue(k, 0, 65535); var r = 0x9000 | DestRIndex(a); return new KeyValuePair(ZeroPad(r), ZeroPad(k)); }}, @@ -523,8 +527,12 @@ public partial class AvrAssembler var r = 0x0200 | DestRIndex(b) | StldYzQ(a); return ZeroPad (r); }}, - { "STS", (a, b, _, _) => { - var k = ConstValue(a, 0, 65535); + { "STS", (a, b, byteLoc, labels) => { + var k = ConstOrLabel(a, labels); + if (k == int.MinValue) { + return new Func, object> ((l) => OpTable?["STS"](a, b, byteLoc, l) ?? new KeyValuePair(string.Empty, string.Empty)); + } + k = ConstValue(k, 0, 65535); var r = 0x9200 | DestRIndex(b); return new KeyValuePair(ZeroPad(r), ZeroPad(k)); }}, @@ -600,6 +608,30 @@ public byte[] Assemble (string input) return _errors.Count > 0 ? [] : PassTwo(); } + /// + /// Assemble and report success explicitly. Returns false when any error was + /// produced (in which case is empty) and exposes the + /// collected messages, so callers cannot mistake an error for empty output. + /// + public bool TryAssemble (string input, out byte[] bytes, out IReadOnlyList errors) + { + bytes = Assemble(input); + errors = _errors.AsReadOnly(); + return _errors.Count == 0; + } + + /// + /// Assemble, throwing if any error was produced + /// instead of silently returning an empty array. + /// + public byte[] AssembleOrThrow (string input) + { + var bytes = Assemble(input); + if (_errors.Count > 0) + throw new AssemblerException(_errors.ToList()); + return bytes; + } + /// /// Assemble multiple source files together. /// Pass 1: Scan all files for .global exports → build combined symbol table. @@ -740,6 +772,15 @@ private void PassOne (string inputData) _currentSymbolTable = _symbolTable; int byteOffset = 0; + + // Data segment (.data/.bss/.dseg) state. Symbols defined here are assigned + // sequential SRAM addresses by a simple allocator (no linker), starting at + // 0x100 — the SRAM base of the ATmega family. This lets lds/sts reference + // data symbols; the addresses are self-consistent but are NOT avr-gcc's + // linker-assigned addresses. + bool inDataSegment = false; + int dataOffset = 0x100; + _labels.Clear(); _errors.Clear(); _lines.Clear(); @@ -753,6 +794,10 @@ private void PassOne (string inputData) List? macroParams = null; List? macroBody = null; + // .rept block recording state + List? reptBody = null; + int reptCount = 0; + // Use a list + index so we can inject macro-expanded lines var lineList = allLines; int lineCount = lineList.Count; @@ -789,6 +834,28 @@ private void PassOne (string inputData) continue; } + // ---------------------------------------------------------------- + // .rept block recording: collect the body, then inject it reptCount + // times after .endr (GNU repeat blocks). + // ---------------------------------------------------------------- + if (reptBody != null) + { + if (keyword is "ENDR" or "ENDREPT") + { + var injected = new List(reptBody.Count * Math.Max(reptCount, 0)); + for (int r = 0; r < reptCount; r++) injected.AddRange(reptBody); + lineList = lineList.Take(idx + 1).Concat(injected).Concat(lineList.Skip(idx + 1)).ToList(); + lineCount = lineList.Count; + reptBody = null; + reptCount = 0; + } + else + { + reptBody.Add(rawLine); + } + continue; + } + // ---------------------------------------------------------------- // Conditional assembly directives (must process even in false branch) // ---------------------------------------------------------------- @@ -808,8 +875,9 @@ private void PassOne (string inputData) // ---------------------------------------------------------------- if (parsed.Label != null) { - _labels[parsed.Label] = byteOffset; - _symbolTable.Set(parsed.Label, byteOffset); + int labelAddr = inDataSegment ? dataOffset : byteOffset; + _labels[parsed.Label] = labelAddr; + _symbolTable.Set(parsed.Label, labelAddr); } // ---------------------------------------------------------------- @@ -846,7 +914,22 @@ private void PassOne (string inputData) } if (dirName == "ENDMACRO" || dirName == "ENDM") { - _errors.Add($"Line {idx}: .endm without .macro"); + _errors.Add($"Line {idx + 1}: .endm without .macro"); + continue; + } + + // Repeat block start: .rept ... .endr + if (dirName == "REPT") + { + int? n = ExpressionEvaluator.TryEvaluate(dirArgs, _symbolTable, byteOffset); + if (n == null) { _errors.Add($"Line {idx + 1}: Cannot evaluate .rept count"); continue; } + reptCount = Math.Max(n.Value, 0); + reptBody = new List(); + continue; + } + if (dirName == "ENDR" || dirName == "ENDREPT") + { + _errors.Add($"Line {idx + 1}: .endr without .rept"); continue; } @@ -854,13 +937,50 @@ private void PassOne (string inputData) if (dirName == "INCLUDE") { if (_fileResolver == null) - _errors.Add($"Line {idx}: .include requires a file resolver"); + _errors.Add($"Line {idx + 1}: .include requires a file resolver"); continue; } - // Segment directives (simple) - if (dirName == "CSEG" || dirName == "DSEG" || dirName == "ESEG") - continue; // segment switching not fully implemented; cseg is default + // Segment switching. .dseg/.data/.bss select the data segment (SRAM + // allocator); .cseg/.text return to code. .section picks by its argument. + if (dirName == "DSEG" || dirName == "DATA" || dirName == "BSS") + { + inDataSegment = true; + continue; + } + if (dirName == "CSEG" || dirName == "TEXT" || dirName == "ESEG") + { + inDataSegment = false; // eseg (EEPROM) is not modelled; treat as code-ish no-op + continue; + } + if (dirName == "SECTION") + { + var sec = dirArgs.TrimStart().Split(new[] { ' ', '\t', ',' }, 2)[0]; + inDataSegment = sec.StartsWith(".data", StringComparison.OrdinalIgnoreCase) + || sec.StartsWith(".bss", StringComparison.OrdinalIgnoreCase); + continue; + } + + // Reserve space: .comm/.lcomm NAME,SIZE and .skip/.space/.zero SIZE. + if (dirName == "COMM" || dirName == "LCOMM") + { + var parts = dirArgs.Split(','); + if (parts.Length < 2) { _errors.Add($"Line {idx + 1}: .{dirName.ToLowerInvariant()} requires NAME, SIZE"); continue; } + var symName = parts[0].Trim(); + int? size = ExpressionEvaluator.TryEvaluate(parts[1], _symbolTable, dataOffset); + if (size == null) { _errors.Add($"Line {idx + 1}: Cannot evaluate .{dirName.ToLowerInvariant()} size"); continue; } + _symbolTable.Set(symName, dataOffset); + _labels[symName] = dataOffset; + dataOffset += Math.Max(size.Value, 0); + continue; + } + if (inDataSegment && (dirName == "SKIP" || dirName == "SPACE" || dirName == "ZERO")) + { + int? size = ExpressionEvaluator.TryEvaluate(dirArgs.Split(',')[0], _symbolTable, dataOffset); + if (size == null) { _errors.Add($"Line {idx + 1}: Cannot evaluate .{dirName.ToLowerInvariant()} size"); continue; } + dataOffset += Math.Max(size.Value, 0); + continue; + } // Symbol definitions if (dirName == "EQU") @@ -883,12 +1003,21 @@ private void PassOne (string inputData) if (dirName == "ORG") { int? orgVal = ExpressionEvaluator.TryEvaluate(dirArgs, _symbolTable, byteOffset); - if (orgVal == null) { _errors.Add($"Line {idx}: Cannot evaluate .org expression"); continue; } - if ((orgVal.Value & 1) != 0) { _errors.Add($"Line {idx}: .org value must be even"); continue; } + if (orgVal == null) { _errors.Add($"Line {idx + 1}: Cannot evaluate .org expression"); continue; } + if ((orgVal.Value & 1) != 0) { _errors.Add($"Line {idx + 1}: .org value must be even"); continue; } byteOffset = orgVal.Value; continue; } + // In the data segment, data directives only reserve address space + // (advance the SRAM allocator) — no bytes are emitted into the code image. + if (inDataSegment && dirName is "BYTE" or "DB" or "WORD" or "DW" or "DWORD" + or "ASCII" or "ASCIZ" or "STRING") + { + dataOffset += DataReserveSize(dirName, dirArgs); + continue; + } + // Data directives — emit raw bytes into line table if (dirName == "BYTE" || dirName == "DB") { @@ -930,14 +1059,14 @@ private void PassOne (string inputData) // GNU/avr-gcc metadata directives: accepted as no-ops. AVR8Sharp emits a // flat code image, so section/symbol/debug metadata carries no payload here. // (Section switching is intentionally ignored — single-section output.) - if (dirName is "FILE" or "SECTION" or "TEXT" or "DATA" or "TYPE" or "SIZE" + if (dirName is "FILE" or "TYPE" or "SIZE" or "IDENT" or "WEAK" or "GLOBL" or "LOCAL" or "ALIGN" or "P2ALIGN" or "BALIGN" or "LOC" or "FUNC" or "ENDFUNC" or "CFI_STARTPROC" or "CFI_ENDPROC") continue; // Unknown dot-directive: fall through to error - _errors.Add($"Line {idx}: Unknown directive: .{dirName}"); + _errors.Add($"Line {idx + 1}: Unknown directive: .{dirName}"); continue; } @@ -1029,7 +1158,7 @@ private void PassOne (string inputData) _lines.Add(lt); } catch (Exception e) { - _errors.Add ($"Line {idx}: {e.Message}"); + _errors.Add ($"Line {idx + 1}: {e.Message}"); } } @@ -1045,7 +1174,7 @@ private void LoadDeviceSymbols(string deviceName, int lineIdx = -1) if (def == null) { if (lineIdx >= 0) - _errors.Add($"Line {lineIdx}: Unknown device: {deviceName}"); + _errors.Add($"Line {lineIdx + 1}: Unknown device: {deviceName}"); return; } @@ -1113,29 +1242,48 @@ private void ProcessConditional(string directive, string args, int byteOffset, r private void ProcessSymbolDef(string args, int lineIdx, int byteOffset, bool isImmutable) { var parts = args.Split('=', 2); - if (parts.Length != 2) { _errors.Add($"Line {lineIdx}: .equ/.set requires NAME = VALUE"); return; } + if (parts.Length != 2) { _errors.Add($"Line {lineIdx + 1}: .equ/.set requires NAME = VALUE"); return; } var name = parts[0].Trim(); var valStr = parts[1].Trim(); var val = ExpressionEvaluator.TryEvaluate(valStr, _symbolTable, byteOffset); - if (val == null) { _errors.Add($"Line {lineIdx}: Cannot evaluate expression for '{name}'"); return; } + if (val == null) { _errors.Add($"Line {lineIdx + 1}: Cannot evaluate expression for '{name}'"); return; } try { if (isImmutable) _symbolTable.DefineConst(name, val.Value); else _symbolTable.DefineVar(name, val.Value); } - catch (Exception ex) { _errors.Add($"Line {lineIdx}: {ex.Message}"); } + catch (Exception ex) { _errors.Add($"Line {lineIdx + 1}: {ex.Message}"); } } private void ProcessDefDirective(string args, int lineIdx) { var parts = args.Split('=', 2); - if (parts.Length != 2) { _errors.Add($"Line {lineIdx}: .def requires ALIAS = rN"); return; } + if (parts.Length != 2) { _errors.Add($"Line {lineIdx + 1}: .def requires ALIAS = rN"); return; } var alias = parts[0].Trim(); var regStr = parts[1].Trim(); int n = Encoders.EncoderHelpers.TryParseRegister(regStr.AsSpan()); - if (n < 0) { _errors.Add($"Line {lineIdx}: .def: right side must be a register, got '{regStr}'"); return; } + if (n < 0) { _errors.Add($"Line {lineIdx + 1}: .def: right side must be a register, got '{regStr}'"); return; } try { _symbolTable.DefineRegisterAlias(alias, n); } - catch (Exception ex) { _errors.Add($"Line {lineIdx}: {ex.Message}"); } + catch (Exception ex) { _errors.Add($"Line {lineIdx + 1}: {ex.Message}"); } + } + + // ----------------------------------------------------------------------- + // Number of bytes a data directive reserves (used in the data segment, where + // the directive advances the SRAM allocator without emitting code bytes). + // String length is approximate for escape sequences — adequate for reservation. + // ----------------------------------------------------------------------- + private int DataReserveSize(string dirName, string args) + { + var parts = SplitDirectiveArgs(args); + if (dirName is "ASCII" or "ASCIZ" or "STRING") + { + int n = 0; + foreach (var p in parts) + n += p.Trim().Trim('"', '\'').Length; + return dirName == "ASCII" ? n : n + parts.Count; // one NUL per string for asciz/string + } + int unit = dirName is "WORD" or "DW" ? 2 : dirName == "DWORD" ? 4 : 1; + return parts.Count * unit; } // ----------------------------------------------------------------------- @@ -1155,7 +1303,7 @@ private void EmitDataBytes(string args, int lineIdx, ref int byteOffset, string continue; } var val = ExpressionEvaluator.TryEvaluate(p, _symbolTable, byteOffset); - if (val == null) { _errors.Add($"Line {lineIdx}: Cannot evaluate .byte expression: {p}"); return; } + if (val == null) { _errors.Add($"Line {lineIdx + 1}: Cannot evaluate .byte expression: {p}"); return; } bytes.Add((byte)(val.Value & 0xFF)); } if (bytes.Count > 0) @@ -1173,7 +1321,7 @@ private void EmitDataWords(string args, int lineIdx, ref int byteOffset, string foreach (var part in parts) { var val = ExpressionEvaluator.TryEvaluate(part.Trim(), _symbolTable, byteOffset); - if (val == null) { _errors.Add($"Line {lineIdx}: Cannot evaluate .word expression: {part.Trim()}"); return; } + if (val == null) { _errors.Add($"Line {lineIdx + 1}: Cannot evaluate .word expression: {part.Trim()}"); return; } wordBytes.Add((byte)(val.Value & 0xFF)); wordBytes.Add((byte)((val.Value >> 8) & 0xFF)); } @@ -1192,7 +1340,7 @@ private void EmitDataDwords(string args, int lineIdx, ref int byteOffset, string foreach (var part in parts) { var val = ExpressionEvaluator.TryEvaluate(part.Trim(), _symbolTable, byteOffset); - if (val == null) { _errors.Add($"Line {lineIdx}: Cannot evaluate .dword expression: {part.Trim()}"); return; } + if (val == null) { _errors.Add($"Line {lineIdx + 1}: Cannot evaluate .dword expression: {part.Trim()}"); return; } dwordBytes.Add((byte)(val.Value & 0xFF)); dwordBytes.Add((byte)((val.Value >> 8) & 0xFF)); dwordBytes.Add((byte)((val.Value >> 16) & 0xFF)); @@ -1286,8 +1434,13 @@ private static List SplitDirectiveArgs(string args) private byte [] PassTwo () { _errors.Clear(); - - if (_lines.Count == 0) + + // Restore the symbol table for deferred closures: forward-referenced + // expressions (e.g. `lds rN, sym+1`) resolve through ConstOrLabel's + // expression evaluator, which reads _currentSymbolTable here in pass two. + _currentSymbolTable = _symbolTable; + + if (_lines.Count == 0) return []; var lastElement = _lines[_lines.Count - 1]; @@ -1347,7 +1500,7 @@ private int ElementSize (ref LineTablePassOne lt) return s2.Length / 2; } if (res is KeyValuePair p) { - lt.Bytes = p.Key + p.Value; + lt.Bytes = p; // keep the 4-byte pair; concatenating would be misread as a 2-byte string return 4; } } @@ -1629,3 +1782,18 @@ public class LineTable : LineTablePassOne { public new string Bytes { get; set; } } + +/// +/// Thrown by when assembly produced errors. +/// +public class AssemblerException : Exception +{ + /// The individual assembler error messages. + public IReadOnlyList Errors { get; } + + public AssemblerException(IReadOnlyList errors) + : base($"Assembly failed with {errors.Count} error(s):\n " + string.Join("\n ", errors)) + { + Errors = errors; + } +} diff --git a/src/Avr8Sharp/Core/Utils/AssemblerContext.cs b/src/Avr8Sharp/Core/Utils/AssemblerContext.cs deleted file mode 100644 index 9cbba26..0000000 --- a/src/Avr8Sharp/Core/Utils/AssemblerContext.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace AVR8Sharp.Core.Utils; - -public enum DiagnosticSeverity { Error, Warning } - -/// -/// Structured assembler diagnostic, compatible with avr-as output format. -/// -public record AssemblerDiagnostic( - DiagnosticSeverity Severity, - string Message, - string? SourceFile, - int Line, - int Column = 0) -{ - public override string ToString() - { - string loc = SourceFile != null - ? $"{SourceFile}:{Line}:{Column}" - : $":{Line}:{Column}"; - string sev = Severity == DiagnosticSeverity.Error ? "error" : "warning"; - return $"{loc}: {sev}: {Message}"; - } -} - -/// -/// Assembler context holding all state that persists through both passes. -/// -internal class AssemblerContext -{ - public SymbolTable Symbols { get; } = new SymbolTable(); - public List Lines { get; } = new List(); - public List Diagnostics { get; } = new List(); - public List Fixups { get; } = new List(); - - public bool HasErrors => Diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error); - - public void Error(string message, int line, string? file = null, int column = 0) => - Diagnostics.Add(new AssemblerDiagnostic(DiagnosticSeverity.Error, message, file, line, column)); - - public void Warning(string message, int line, string? file = null, int column = 0) => - Diagnostics.Add(new AssemblerDiagnostic(DiagnosticSeverity.Warning, message, file, line, column)); - - public void Reset() - { - Lines.Clear(); - Diagnostics.Clear(); - Fixups.Clear(); - } -} diff --git a/src/Avr8Sharp/Core/Utils/DeviceDefinitions.cs b/src/Avr8Sharp/Core/Utils/DeviceDefinitions.cs index 8615063..5ff5e08 100644 --- a/src/Avr8Sharp/Core/Utils/DeviceDefinitions.cs +++ b/src/Avr8Sharp/Core/Utils/DeviceDefinitions.cs @@ -27,6 +27,7 @@ public static class DeviceDefinitions static DeviceDefinitions() { Register(ATmega328P()); + Register(ATmega2560()); Register(ATtiny85()); } @@ -179,6 +180,226 @@ static DeviceDefinitions() } }; + // ----------------------------------------------------------------------- + // ATmega2560 (Arduino Mega 2560) + // Datasheet: DS40002211A — Microchip ATmega640/1280/1281/2560/2561 + // Register addresses extracted from avr-libc (iomxx0_1.h). + // Low I/O (data < 0x60) stored as I/O address; extended I/O (>= 0x60) as + // data-space address — same convention as the other device definitions. + // ----------------------------------------------------------------------- + private static DeviceDefinition ATmega2560() => new() + { + Name = "ATmega2560", + FlashSize = 0x40000, // 256 KB + SramSize = 8192, + EepromSize = 4096, + Symbols = new(StringComparer.OrdinalIgnoreCase) + { + // ── Memory limits ── + ["RAMEND"] = 0x21FF, + ["FLASHEND"] = 0x3FFFF, + ["E2END"] = 0x0FFF, + // ── Port A-G (low I/O) ── + ["PINA"] = 0x00, + ["DDRA"] = 0x01, + ["PORTA"] = 0x02, + ["PINB"] = 0x03, + ["DDRB"] = 0x04, + ["PORTB"] = 0x05, + ["PINC"] = 0x06, + ["DDRC"] = 0x07, + ["PORTC"] = 0x08, + ["PIND"] = 0x09, + ["DDRD"] = 0x0A, + ["PORTD"] = 0x0B, + ["PINE"] = 0x0C, + ["DDRE"] = 0x0D, + ["PORTE"] = 0x0E, + ["PINF"] = 0x0F, + ["DDRF"] = 0x10, + ["PORTF"] = 0x11, + ["PING"] = 0x12, + ["DDRG"] = 0x13, + ["PORTG"] = 0x14, + // ── Port H-L (extended I/O) ── + ["PINH"] = 0x100, + ["DDRH"] = 0x101, + ["PORTH"] = 0x102, + ["PINJ"] = 0x103, + ["DDRJ"] = 0x104, + ["PORTJ"] = 0x105, + ["PINK"] = 0x106, + ["DDRK"] = 0x107, + ["PORTK"] = 0x108, + ["PINL"] = 0x109, + ["DDRL"] = 0x10A, + ["PORTL"] = 0x10B, + // ── Status / stack / RAMPZ / EIND ── + ["SREG"] = 0x3F, + ["SPL"] = 0x3D, + ["SPH"] = 0x3E, + ["RAMPZ"] = 0x3B, + ["EIND"] = 0x3C, + // ── EEPROM ── + ["EECR"] = 0x1F, + ["EEDR"] = 0x20, + ["EEARL"] = 0x21, + ["EEARH"] = 0x22, + // ── General-purpose I/O regs ── + ["GPIOR0"] = 0x1E, + ["GPIOR1"] = 0x2A, + ["GPIOR2"] = 0x2B, + // ── SPI ── + ["SPCR"] = 0x2C, + ["SPSR"] = 0x2D, + ["SPDR"] = 0x2E, + // ── TWI ── + ["TWBR"] = 0xB8, + ["TWSR"] = 0xB9, + ["TWAR"] = 0xBA, + ["TWDR"] = 0xBB, + ["TWCR"] = 0xBC, + ["TWAMR"] = 0xBD, + // ── ADC ── + ["ADCL"] = 0x78, + ["ADCH"] = 0x79, + ["ADCSRA"] = 0x7A, + ["ADCSRB"] = 0x7B, + ["ADMUX"] = 0x7C, + ["DIDR0"] = 0x7E, + ["DIDR1"] = 0x7F, + ["DIDR2"] = 0x7D, + // ── Timer/Counter 0 (8-bit) ── + ["TIFR0"] = 0x15, + ["TCCR0A"] = 0x24, + ["TCCR0B"] = 0x25, + ["TCNT0"] = 0x26, + ["OCR0A"] = 0x27, + ["OCR0B"] = 0x28, + ["TIMSK0"] = 0x6E, + // ── Timer/Counter 1 (16-bit) ── + ["TIFR1"] = 0x16, + ["TCCR1A"] = 0x80, + ["TCCR1B"] = 0x81, + ["TCCR1C"] = 0x82, + ["TCNT1L"] = 0x84, + ["TCNT1H"] = 0x85, + ["ICR1L"] = 0x86, + ["ICR1H"] = 0x87, + ["OCR1AL"] = 0x88, + ["OCR1AH"] = 0x89, + ["OCR1BL"] = 0x8A, + ["OCR1BH"] = 0x8B, + ["OCR1CL"] = 0x8C, + ["OCR1CH"] = 0x8D, + ["TIMSK1"] = 0x6F, + // ── Timer/Counter 2 (8-bit async) ── + ["TIFR2"] = 0x17, + ["TCCR2A"] = 0xB0, + ["TCCR2B"] = 0xB1, + ["TCNT2"] = 0xB2, + ["OCR2A"] = 0xB3, + ["OCR2B"] = 0xB4, + ["ASSR"] = 0xB6, + ["TIMSK2"] = 0x70, + // ── Timer/Counter 3 (16-bit) ── + ["TIFR3"] = 0x18, + ["TCCR3A"] = 0x90, + ["TCCR3B"] = 0x91, + ["TCCR3C"] = 0x92, + ["TCNT3L"] = 0x94, + ["TCNT3H"] = 0x95, + ["ICR3L"] = 0x96, + ["ICR3H"] = 0x97, + ["OCR3AL"] = 0x98, + ["OCR3AH"] = 0x99, + ["OCR3BL"] = 0x9A, + ["OCR3BH"] = 0x9B, + ["OCR3CL"] = 0x9C, + ["OCR3CH"] = 0x9D, + ["TIMSK3"] = 0x71, + // ── Timer/Counter 4 (16-bit) ── + ["TIFR4"] = 0x19, + ["TCCR4A"] = 0xA0, + ["TCCR4B"] = 0xA1, + ["TCCR4C"] = 0xA2, + ["TCNT4L"] = 0xA4, + ["TCNT4H"] = 0xA5, + ["ICR4L"] = 0xA6, + ["ICR4H"] = 0xA7, + ["OCR4AL"] = 0xA8, + ["OCR4AH"] = 0xA9, + ["OCR4BL"] = 0xAA, + ["OCR4BH"] = 0xAB, + ["OCR4CL"] = 0xAC, + ["OCR4CH"] = 0xAD, + ["TIMSK4"] = 0x72, + // ── Timer/Counter 5 (16-bit) ── + ["TIFR5"] = 0x1A, + ["TCCR5A"] = 0x120, + ["TCCR5B"] = 0x121, + ["TCCR5C"] = 0x122, + ["TCNT5L"] = 0x124, + ["TCNT5H"] = 0x125, + ["ICR5L"] = 0x126, + ["ICR5H"] = 0x127, + ["OCR5AL"] = 0x128, + ["OCR5AH"] = 0x129, + ["OCR5BL"] = 0x12A, + ["OCR5BH"] = 0x12B, + ["OCR5CL"] = 0x12C, + ["OCR5CH"] = 0x12D, + ["TIMSK5"] = 0x73, + // ── USART0 ── + ["UCSR0A"] = 0xC0, + ["UCSR0B"] = 0xC1, + ["UCSR0C"] = 0xC2, + ["UBRR0L"] = 0xC4, + ["UBRR0H"] = 0xC5, + ["UDR0"] = 0xC6, + // ── USART1 ── + ["UCSR1A"] = 0xC8, + ["UCSR1B"] = 0xC9, + ["UCSR1C"] = 0xCA, + ["UBRR1L"] = 0xCC, + ["UBRR1H"] = 0xCD, + ["UDR1"] = 0xCE, + // ── USART2 ── + ["UCSR2A"] = 0xD0, + ["UCSR2B"] = 0xD1, + ["UCSR2C"] = 0xD2, + ["UBRR2L"] = 0xD4, + ["UBRR2H"] = 0xD5, + ["UDR2"] = 0xD6, + // ── USART3 ── + ["UCSR3A"] = 0x130, + ["UCSR3B"] = 0x131, + ["UCSR3C"] = 0x132, + ["UBRR3L"] = 0x134, + ["UBRR3H"] = 0x135, + ["UDR3"] = 0x136, + // ── External / pin-change interrupts ── + ["EIMSK"] = 0x1D, + ["EIFR"] = 0x1C, + ["EICRA"] = 0x69, + ["EICRB"] = 0x6A, + ["PCICR"] = 0x68, + ["PCIFR"] = 0x1B, + ["PCMSK0"] = 0x6B, + ["PCMSK1"] = 0x6C, + ["PCMSK2"] = 0x6D, + // ── Misc control ── + ["MCUSR"] = 0x34, + ["MCUCR"] = 0x35, + ["SMCR"] = 0x33, + ["PRR0"] = 0x64, + ["PRR1"] = 0x65, + ["CLKPR"] = 0x61, + ["WDTCSR"] = 0x60, + ["OSCCAL"] = 0x66, + } + }; + // ----------------------------------------------------------------------- // ATtiny85 (Digispark / Trinket) // Datasheet: DS40002311A — Microchip ATtiny25/45/85 diff --git a/src/Avr8Sharp/Core/Utils/DirectiveProcessor.cs b/src/Avr8Sharp/Core/Utils/DirectiveProcessor.cs deleted file mode 100644 index f5689bb..0000000 --- a/src/Avr8Sharp/Core/Utils/DirectiveProcessor.cs +++ /dev/null @@ -1,460 +0,0 @@ -namespace AVR8Sharp.Core.Utils; - -/// -/// Segment type for multi-segment support. -/// -public enum SegmentType { Code, Data, Eeprom } - -/// -/// Processes assembler directives during pass 1. -/// -internal class DirectiveProcessor -{ - private readonly SymbolTable _symbols; - private readonly Dictionary Params, List Body)> _macros = new(StringComparer.OrdinalIgnoreCase); - - // Segment location counters (byte addresses) - private int _codeOffset; - private int _dataOffset; - private int _eepromOffset; - private SegmentType _currentSegment = SegmentType.Code; - - // Conditional assembly stack: true = currently assembling - private readonly Stack _condStack = new(); - private bool _assembling = true; // top-level: always assemble - - // Macro recording state - private string? _recordingMacro; - private List? _recordingParams; - private List? _recordingBody; - - // Include file resolver - private readonly Func? _fileResolver; - - public int CurrentOffset => _currentSegment switch - { - SegmentType.Code => _codeOffset, - SegmentType.Data => _dataOffset, - SegmentType.Eeprom => _eepromOffset, - _ => _codeOffset - }; - - public SegmentType CurrentSegment => _currentSegment; - public bool Assembling => _assembling; - - public DirectiveProcessor(SymbolTable symbols, Func? fileResolver = null) - { - _symbols = symbols; - _fileResolver = fileResolver; - } - - public void SetCodeOffset(int offset) => _codeOffset = offset; - public void AdvanceCodeOffset(int bytes) => _codeOffset += bytes; - - /// - /// Process a directive line. Returns true if the line was a directive (and thus consumed), - /// false if it is not a directive. Also handles macro expansion by returning lines to insert. - /// - public bool ProcessDirective(string name, string args, out List? extraLines, out List emittedLines, out string? error) - { - extraLines = null; - emittedLines = new List(); - error = null; - - // Handle macro recording - if (_recordingMacro != null) - { - string upper = name.ToUpperInvariant(); - if (upper == ".ENDMACRO" || upper == ".ENDM" || upper == "ENDMACRO" || upper == "ENDM") - { - _macros[_recordingMacro] = (_recordingParams!, _recordingBody!); - _recordingMacro = null; - _recordingParams = null; - _recordingBody = null; - } - else - { - _recordingBody!.Add(string.IsNullOrEmpty(args) ? name : $"{name} {args}"); - } - return true; - } - - string directive = name.TrimStart('.').ToUpperInvariant(); - - switch (directive) - { - case "CSEG": - _currentSegment = SegmentType.Code; - return true; - case "DSEG": - _currentSegment = SegmentType.Data; - return true; - case "ESEG": - _currentSegment = SegmentType.Eeprom; - return true; - - case "ORG": - case "LOC": - { - string expr = args.Trim(); - int? val = ExpressionEvaluator.TryEvaluate(expr, _symbols, _codeOffset); - if (val == null) { error = $"Cannot evaluate expression: {expr}"; return true; } - if ((val.Value & 1) != 0 && _currentSegment == SegmentType.Code) { error = "Code origin must be even"; return true; } - switch (_currentSegment) - { - case SegmentType.Code: _codeOffset = val.Value; break; - case SegmentType.Data: _dataOffset = val.Value; break; - case SegmentType.Eeprom: _eepromOffset = val.Value; break; - } - return true; - } - - case "EQU": - { - // .equ NAME = VALUE - var parts = args.Split('=', 2); - if (parts.Length != 2) { error = ".equ requires NAME = VALUE"; return true; } - string symName = parts[0].Trim(); - int? val = ExpressionEvaluator.TryEvaluate(parts[1].Trim(), _symbols, _codeOffset); - if (val == null) { error = $"Cannot evaluate .equ expression for {symName}"; return true; } - try { _symbols.DefineConst(symName, val.Value); } - catch (Exception ex) { error = ex.Message; } - return true; - } - - case "SET": - { - var parts = args.Split('=', 2); - if (parts.Length != 2) { error = ".set requires NAME = VALUE"; return true; } - string symName = parts[0].Trim(); - int? val = ExpressionEvaluator.TryEvaluate(parts[1].Trim(), _symbols, _codeOffset); - if (val == null) { error = $"Cannot evaluate .set expression for {symName}"; return true; } - try { _symbols.DefineVar(symName, val.Value); } - catch (Exception ex) { error = ex.Message; } - return true; - } - - case "DEF": - { - // .def ALIAS = rN - var parts = args.Split('=', 2); - if (parts.Length != 2) { error = ".def requires ALIAS = rN"; return true; } - string alias = parts[0].Trim(); - string regStr = parts[1].Trim(); - int n = Encoders.EncoderHelpers.TryParseRegister(regStr.AsSpan()); - if (n < 0) { error = $".def: right side must be a register, got '{regStr}'"; return true; } - try { _symbols.DefineConst(alias, n); } - catch (Exception ex) { error = ex.Message; } - return true; - } - - case "REPLACE": - { - // backward-compat: _REPLACE NAME, VALUE (no '=') - var parts = args.Split(',', 2); - if (parts.Length != 2) { error = "_REPLACE requires NAME, VALUE"; return true; } - string symName = parts[0].Trim(); - string valStr = parts[1].Trim(); - int? val = ExpressionEvaluator.TryEvaluate(valStr, _symbols, _codeOffset); - if (val == null) - { - // store as raw text for later text-substitution (backward-compat mode) - _symbols.Set(symName, 0); // placeholder - return true; - } - try { _symbols.Set(symName, val.Value); } - catch { /* ignore redefinition in backward-compat mode */ } - return true; - } - - case "BYTE": - case "DB": - { - if (_currentSegment != SegmentType.Code) return true; // DSEG .byte just reserves space - var bytes = EmitCommaSeparatedBytes(args, out error); - if (bytes != null && bytes.Length > 0) - { - emittedLines.Add(new AsmLine - { - BytesOffset = _codeOffset, - Encoding = new InstructionWord.DataEncoding(bytes), - Text = $".{directive.ToLowerInvariant()} {args}", - Line = 0 - }); - _codeOffset += bytes.Length; - } - return true; - } - - case "WORD": - case "DW": - case "IW": // backward-compat _IW - { - var words = EmitCommaSeparatedWords(args, out error); - if (words != null && words.Length > 0) - { - var wordBytes = new byte[words.Length * 2]; - for (int i = 0; i < words.Length; i++) - { - wordBytes[i * 2] = (byte)(words[i] & 0xFF); - wordBytes[i * 2 + 1] = (byte)((words[i] >> 8) & 0xFF); - } - emittedLines.Add(new AsmLine - { - BytesOffset = _codeOffset, - Encoding = new InstructionWord.DataEncoding(wordBytes), - Text = $".{directive.ToLowerInvariant()} {args}", - Line = 0 - }); - _codeOffset += wordBytes.Length; - } - return true; - } - - case "DWORD": - { - var words = EmitCommaSeparatedWords(args, out error); - if (words != null && words.Length > 0) - { - var dwordBytes = new byte[words.Length * 4]; - for (int i = 0; i < words.Length; i++) - { - dwordBytes[i * 4] = (byte)(words[i] & 0xFF); - dwordBytes[i * 4 + 1] = (byte)((words[i] >> 8) & 0xFF); - dwordBytes[i * 4 + 2] = (byte)((words[i] >> 16) & 0xFF); - dwordBytes[i * 4 + 3] = (byte)((words[i] >> 24) & 0xFF); - } - emittedLines.Add(new AsmLine - { - BytesOffset = _codeOffset, - Encoding = new InstructionWord.DataEncoding(dwordBytes), - Text = ".dword " + args, - Line = 0 - }); - _codeOffset += dwordBytes.Length; - } - return true; - } - - case "ASCII": - { - string str = UnquoteString(args.Trim()); - var bytes = System.Text.Encoding.ASCII.GetBytes(str); - emittedLines.Add(new AsmLine - { - BytesOffset = _codeOffset, - Encoding = new InstructionWord.DataEncoding(bytes), - Text = ".ascii " + args, - Line = 0 - }); - _codeOffset += bytes.Length; - return true; - } - - case "ASCIZ": - case "STRING": - { - string str = UnquoteString(args.Trim()); - var rawBytes = System.Text.Encoding.ASCII.GetBytes(str); - var bytes = new byte[rawBytes.Length + 1]; - rawBytes.CopyTo(bytes, 0); - bytes[^1] = 0; - emittedLines.Add(new AsmLine - { - BytesOffset = _codeOffset, - Encoding = new InstructionWord.DataEncoding(bytes), - Text = ".asciz " + args, - Line = 0 - }); - _codeOffset += bytes.Length; - return true; - } - - case "IF": - { - int? val = ExpressionEvaluator.TryEvaluate(args.Trim(), _symbols, _codeOffset); - bool result = val.HasValue && val.Value != 0; - _condStack.Push(_assembling); - _assembling = _assembling && result; - return true; - } - - case "IFDEF": - { - bool defined = _symbols.ContainsKey(args.Trim()); - _condStack.Push(_assembling); - _assembling = _assembling && defined; - return true; - } - - case "IFNDEF": - { - bool notDefined = !_symbols.ContainsKey(args.Trim()); - _condStack.Push(_assembling); - _assembling = _assembling && notDefined; - return true; - } - - case "ELSEIF": - { - if (_condStack.Count == 0) { error = ".elseif without .if"; return true; } - bool wasActive = _condStack.Peek(); - if (_assembling) - { - // We were assembling the if-branch; now skip - _assembling = false; - } - else if (wasActive) - { - // Parent was active; evaluate this branch - int? val = ExpressionEvaluator.TryEvaluate(args.Trim(), _symbols, _codeOffset); - _assembling = val.HasValue && val.Value != 0; - } - return true; - } - - case "ELSE": - { - if (_condStack.Count == 0) { error = ".else without .if"; return true; } - bool parentActive = _condStack.Peek(); - _assembling = parentActive && !_assembling; - return true; - } - - case "ENDIF": - { - if (_condStack.Count == 0) { error = ".endif without .if"; return true; } - _assembling = _condStack.Pop(); - return true; - } - - case "MACRO": - { - var parts = args.Split(new[] { ' ', '\t', ',' }, StringSplitOptions.RemoveEmptyEntries); - if (parts.Length == 0) { error = ".macro requires a name"; return true; } - _recordingMacro = parts[0].Trim(); - _recordingParams = parts.Skip(1).Select(p => p.Trim()).ToList(); - _recordingBody = new List(); - return true; - } - - case "ENDMACRO": - case "ENDM": - { - error = ".endm without .macro"; - return true; - } - - case "INCLUDE": - { - string filename = UnquoteString(args.Trim()); - if (_fileResolver == null) { error = ".include requires a file resolver"; return true; } - try - { - string content = _fileResolver(filename); - extraLines = content.Split('\n').ToList(); - } - catch (Exception ex) - { - error = $".include error: {ex.Message}"; - } - return true; - } - - default: - return false; // Not a directive we handle - } - } - - /// - /// Try to expand a macro by name. Returns the expanded lines or null if not a macro. - /// - public List? TryExpandMacro(string name, string args, int depth = 0) - { - if (depth > 8) throw new Exception($"Macro recursion limit exceeded in '{name}'"); - if (!_macros.TryGetValue(name, out var macro)) return null; - - var argValues = args.Split(',').Select(a => a.Trim()).ToArray(); - var expanded = new List(); - - foreach (var line in macro.Body) - { - string expandedLine = line; - for (int i = 0; i < macro.Params.Count; i++) - { - if (i < argValues.Length) - { - expandedLine = expandedLine.Replace(@"\" + macro.Params[i], argValues[i]); - expandedLine = expandedLine.Replace("@" + i, argValues[i]); - } - } - expanded.Add(expandedLine); - } - return expanded; - } - - private byte[]? EmitCommaSeparatedBytes(string args, out string? error) - { - error = null; - var parts = SplitArgs(args); - var bytes = new List(); - foreach (var p in parts) - { - string trimmed = p.Trim(); - // String literals - if (trimmed.StartsWith('"')) - { - string str = UnquoteString(trimmed); - bytes.AddRange(System.Text.Encoding.ASCII.GetBytes(str)); - continue; - } - int? val = ExpressionEvaluator.TryEvaluate(trimmed, _symbols, _codeOffset); - if (val == null) { error = $"Cannot evaluate: {trimmed}"; return null; } - bytes.Add((byte)(val.Value & 0xFF)); - } - return bytes.ToArray(); - } - - private int[]? EmitCommaSeparatedWords(string args, out string? error) - { - error = null; - var parts = SplitArgs(args); - var words = new List(); - foreach (var p in parts) - { - int? val = ExpressionEvaluator.TryEvaluate(p.Trim(), _symbols, _codeOffset); - if (val == null) { error = $"Cannot evaluate: {p.Trim()}"; return null; } - words.Add(val.Value); - } - return words.ToArray(); - } - - private static List SplitArgs(string args) - { - // Split by comma, but not inside quotes - var parts = new List(); - int depth = 0; - bool inString = false; - var cur = new System.Text.StringBuilder(); - foreach (char c in args) - { - if (c == '"') inString = !inString; - if (!inString) - { - if (c == '(') depth++; - else if (c == ')') depth--; - else if (c == ',' && depth == 0) { parts.Add(cur.ToString()); cur.Clear(); continue; } - } - cur.Append(c); - } - if (cur.Length > 0) parts.Add(cur.ToString()); - return parts; - } - - private static string UnquoteString(string s) - { - if (s.StartsWith('"') && s.EndsWith('"') && s.Length >= 2) - return s[1..^1]; - return s; - } -} diff --git a/src/Avr8Sharp/Core/Utils/Encoders/ArithmeticEncoders.cs b/src/Avr8Sharp/Core/Utils/Encoders/ArithmeticEncoders.cs deleted file mode 100644 index 9992f20..0000000 --- a/src/Avr8Sharp/Core/Utils/Encoders/ArithmeticEncoders.cs +++ /dev/null @@ -1,153 +0,0 @@ -using static AVR8Sharp.Core.Utils.Encoders.EncoderHelpers; - -namespace AVR8Sharp.Core.Utils.Encoders; - -internal static class ArithmeticEncoders -{ - public static ushort ADD(string rd, string rr) => - (ushort)(0x0C00 | DestRd(ParseRegister(rd)) | SrcRr(ParseRegister(rr))); - - public static ushort ADC(string rd, string rr) => - (ushort)(0x1C00 | DestRd(ParseRegister(rd)) | SrcRr(ParseRegister(rr))); - - public static ushort ADIW(string rd, string k) - { - int d = ParseWordRegPair(rd); - int imm = ParseImm(k, 0, 63); - return (ushort)(0x9600 | (d & 0x3) << 4 | ((imm & 0x30) << 2) | (imm & 0x0F)); - } - - public static ushort AND(string rd, string rr) => - (ushort)(0x2000 | DestRd(ParseRegister(rd)) | SrcRr(ParseRegister(rr))); - - public static ushort ANDI(string rd, string k) - { - int d = ParseRegister(rd, 16, 31); - int imm = ParseImm(k, 0, 255); - return (ushort)(0x7000 | (DestRd(d) & 0xF0) | ((imm & 0xF0) << 4) | (imm & 0x0F)); - } - - public static ushort COM(string rd) => - (ushort)(0x9400 | DestRd(ParseRegister(rd))); - - public static ushort CP(string rd, string rr) => - (ushort)(0x1400 | DestRd(ParseRegister(rd)) | SrcRr(ParseRegister(rr))); - - public static ushort CPC(string rd, string rr) => - (ushort)(0x0400 | DestRd(ParseRegister(rd)) | SrcRr(ParseRegister(rr))); - - public static ushort CPI(string rd, string k) - { - int d = ParseRegister(rd, 16, 31); - int imm = ParseImm(k, 0, 255); - return (ushort)(0x3000 | (DestRd(d) & 0xF0) | ((imm & 0xF0) << 4) | (imm & 0x0F)); - } - - public static ushort CPSE(string rd, string rr) => - (ushort)(0x1000 | DestRd(ParseRegister(rd)) | SrcRr(ParseRegister(rr))); - - public static ushort DEC(string rd) => - (ushort)(0x940A | DestRd(ParseRegister(rd))); - - public static ushort EOR(string rd, string rr) => - (ushort)(0x2400 | DestRd(ParseRegister(rd)) | SrcRr(ParseRegister(rr))); - - public static ushort FMUL(string rd, string rr) => - (ushort)(0x0308 | DestRd(ParseRegister(rd, 16, 23)) | (SrcRr(ParseRegister(rr, 16, 23)) & 0x7)); - - public static ushort FMULS(string rd, string rr) - { - int d = ParseRegister(rd, 16, 23); - int r = ParseRegister(rr, 16, 23); - return (ushort)(0x0380 | ((d & 0x7) << 4) | (r & 0x7)); - } - - public static ushort FMULSU(string rd, string rr) - { - int d = ParseRegister(rd, 16, 23); - int r = ParseRegister(rr, 16, 23); - return (ushort)(0x0388 | ((d & 0x7) << 4) | (r & 0x7)); - } - - public static ushort INC(string rd) => - (ushort)(0x9403 | DestRd(ParseRegister(rd))); - - public static ushort MOV(string rd, string rr) => - (ushort)(0x2C00 | DestRd(ParseRegister(rd)) | SrcRr(ParseRegister(rr))); - - public static ushort MOVW(string rd, string rr) - { - int d = ParseRegister(rd); - int r = ParseRegister(rr); - return (ushort)(0x0100 | ((d >> 1) & 0xF) << 4 | ((r >> 1) & 0xF)); - } - - public static ushort MUL(string rd, string rr) => - (ushort)(0x9C00 | DestRd(ParseRegister(rd)) | SrcRr(ParseRegister(rr))); - - public static ushort MULS(string rd, string rr) - { - int d = ParseRegister(rd, 16, 31); - int r = ParseRegister(rr, 16, 31); - return (ushort)(0x0200 | ((d & 0xF) << 4) | (r & 0xF)); - } - - public static ushort MULSU(string rd, string rr) - { - int d = ParseRegister(rd, 16, 23); - int r = ParseRegister(rr, 16, 23); - return (ushort)(0x0300 | ((d & 0x7) << 4) | (r & 0x7)); - } - - public static ushort NEG(string rd) => - (ushort)(0x9401 | DestRd(ParseRegister(rd))); - - public static ushort OR(string rd, string rr) => - (ushort)(0x2800 | DestRd(ParseRegister(rd)) | SrcRr(ParseRegister(rr))); - - public static ushort ORI(string rd, string k) - { - int d = ParseRegister(rd, 16, 31); - int imm = ParseImm(k, 0, 255); - return (ushort)(0x6000 | (DestRd(d) & 0xF0) | ((imm & 0xF0) << 4) | (imm & 0x0F)); - } - - public static ushort SBC(string rd, string rr) => - (ushort)(0x0800 | DestRd(ParseRegister(rd)) | SrcRr(ParseRegister(rr))); - - public static ushort SBCI(string rd, string k) - { - int d = ParseRegister(rd, 16, 31); - int imm = ParseImm(k, 0, 255); - return (ushort)(0x4000 | (DestRd(d) & 0xF0) | ((imm & 0xF0) << 4) | (imm & 0x0F)); - } - - public static ushort SBIW(string rd, string k) - { - int d = ParseWordRegPair(rd); - int imm = ParseImm(k, 0, 63); - return (ushort)(0x9700 | (d & 0x3) << 4 | ((imm & 0x30) << 2) | (imm & 0x0F)); - } - - public static ushort SUB(string rd, string rr) => - (ushort)(0x1800 | DestRd(ParseRegister(rd)) | SrcRr(ParseRegister(rr))); - - public static ushort SUBI(string rd, string k) - { - int d = ParseRegister(rd, 16, 31); - int imm = ParseImm(k, 0, 255); - return (ushort)(0x5000 | (DestRd(d) & 0xF0) | ((imm & 0xF0) << 4) | (imm & 0x0F)); - } - - // Pseudo-instructions - public static ushort CLR(string rd) => EOR(rd, rd); - public static ushort LSL(string rd) => ADD(rd, rd); - public static ushort ROL(string rd) => ADC(rd, rd); - public static ushort TST(string rd) => AND(rd, rd); - public static ushort SBR(string rd, string k) => ORI(rd, k); - public static ushort CRB(string rd, string k) - { - int kv = ParseValue(k); - return ANDI(rd, (~kv & 0xFF).ToString()); - } -} diff --git a/src/Avr8Sharp/Core/Utils/Encoders/BitManipEncoders.cs b/src/Avr8Sharp/Core/Utils/Encoders/BitManipEncoders.cs deleted file mode 100644 index 88b5193..0000000 --- a/src/Avr8Sharp/Core/Utils/Encoders/BitManipEncoders.cs +++ /dev/null @@ -1,92 +0,0 @@ -using static AVR8Sharp.Core.Utils.Encoders.EncoderHelpers; - -namespace AVR8Sharp.Core.Utils.Encoders; - -internal static class BitManipEncoders -{ - public static ushort BCLR(string s) - { - int bit = ParseImm(s, 0, 7); - return (ushort)(0x9488 | (bit << 4)); - } - - public static ushort BSET(string s) - { - int bit = ParseImm(s, 0, 7); - return (ushort)(0x9408 | (bit << 4)); - } - - public static ushort BLD(string rd, string b) - { - int d = ParseRegister(rd); - int bit = ParseImm(b, 0, 7); - return (ushort)(0xF800 | DestRd(d) | bit); - } - - public static ushort BST(string rd, string b) - { - int d = ParseRegister(rd); - int bit = ParseImm(b, 0, 7); - return (ushort)(0xFA00 | DestRd(d) | bit); - } - - public static ushort CBI(string A, string b) - { - int a = ParseImm(A, 0, 31); - int bit = ParseImm(b, 0, 7); - return (ushort)(0x9800 | (a << 3) | bit); - } - - public static ushort SBI(string A, string b) - { - int a = ParseImm(A, 0, 31); - int bit = ParseImm(b, 0, 7); - return (ushort)(0x9A00 | (a << 3) | bit); - } - - public static ushort SBIC(string A, string b) - { - int a = ParseImm(A, 0, 31); - int bit = ParseImm(b, 0, 7); - return (ushort)(0x9900 | (a << 3) | bit); - } - - public static ushort SBIS(string A, string b) - { - int a = ParseImm(A, 0, 31); - int bit = ParseImm(b, 0, 7); - return (ushort)(0x9B00 | (a << 3) | bit); - } - - public static ushort SBRC(string rd, string b) - { - int d = ParseRegister(rd); - int bit = ParseImm(b, 0, 7); - return (ushort)(0xFC00 | DestRd(d) | bit); - } - - public static ushort SBRS(string rd, string b) - { - int d = ParseRegister(rd); - int bit = ParseImm(b, 0, 7); - return (ushort)(0xFE00 | DestRd(d) | bit); - } - - // Flag set/clear - public static ushort CLC() => BCLR("0"); - public static ushort CLH() => BCLR("5"); - public static ushort CLI() => BCLR("7"); - public static ushort CLN() => BCLR("2"); - public static ushort CLS() => BCLR("4"); - public static ushort CLT() => BCLR("6"); - public static ushort CLV() => BCLR("3"); - public static ushort CLZ() => BCLR("1"); - public static ushort SEC() => BSET("0"); - public static ushort SEH() => BSET("5"); - public static ushort SEI() => BSET("7"); - public static ushort SEN() => BSET("2"); - public static ushort SES() => BSET("4"); - public static ushort SET() => BSET("6"); - public static ushort SEV() => BSET("3"); - public static ushort SEZ() => BSET("1"); -} diff --git a/src/Avr8Sharp/Core/Utils/Encoders/BranchEncoders.cs b/src/Avr8Sharp/Core/Utils/Encoders/BranchEncoders.cs deleted file mode 100644 index fbdb5a0..0000000 --- a/src/Avr8Sharp/Core/Utils/Encoders/BranchEncoders.cs +++ /dev/null @@ -1,70 +0,0 @@ -using static AVR8Sharp.Core.Utils.Encoders.EncoderHelpers; - -namespace AVR8Sharp.Core.Utils.Encoders; - -internal static class BranchEncoders -{ - public static (ushort, ushort) CALL(int k) - { - k = k >> 1; - int hk = (k >> 16) & 0x3F; - int lk = k & 0xFFFF; - ushort high = (ushort)(0x940E | ((hk & 0x3E) << 3) | (hk & 1)); - return (high, (ushort)lk); - } - - public static (ushort, ushort) JMP(int k) - { - k = k >> 1; - int hk = (k >> 16) & 0x3F; - int lk = k & 0xFFFF; - ushort high = (ushort)(0x940C | ((hk & 0x3E) << 3) | (hk & 1)); - return (high, (ushort)lk); - } - - public static ushort RCALL(int k) - { - int offset = k >> 1; // k is byte offset from NEXT instruction - return (ushort)(0xD000 | FitTwoC(offset, 12)); - } - - public static ushort RJMP(int k) - { - int offset = k >> 1; - return (ushort)(0xC000 | FitTwoC(offset, 12)); - } - - public static ushort BRBC(string s, int k) - { - int bit = EncoderHelpers.ParseImm(s, 0, 7); - int offset = FitTwoC(k >> 1, 7); - return (ushort)(0xF400 | bit | (offset << 3)); - } - - public static ushort BRBS(string s, int k) - { - int bit = EncoderHelpers.ParseImm(s, 0, 7); - int offset = FitTwoC(k >> 1, 7); - return (ushort)(0xF000 | bit | (offset << 3)); - } - - // Flag-specific branches (delegate to BRBC/BRBS) - public static ushort BRCC(int k) => BRBC("0", k); - public static ushort BRCS(int k) => BRBS("0", k); - public static ushort BREQ(int k) => BRBS("1", k); - public static ushort BRGE(int k) => BRBC("4", k); - public static ushort BRHC(int k) => BRBC("5", k); - public static ushort BRHS(int k) => BRBS("5", k); - public static ushort BRID(int k) => BRBC("7", k); - public static ushort BRIE(int k) => BRBS("7", k); - public static ushort BRLO(int k) => BRBS("0", k); - public static ushort BRLT(int k) => BRBS("4", k); - public static ushort BRMI(int k) => BRBS("2", k); - public static ushort BRNE(int k) => BRBC("1", k); - public static ushort BRPL(int k) => BRBC("2", k); - public static ushort BRSH(int k) => BRBC("0", k); - public static ushort BRTC(int k) => BRBC("6", k); - public static ushort BRTS(int k) => BRBS("6", k); - public static ushort BRVC(int k) => BRBC("3", k); - public static ushort BRVS(int k) => BRBS("3", k); -} diff --git a/src/Avr8Sharp/Core/Utils/Encoders/LoadStoreEncoders.cs b/src/Avr8Sharp/Core/Utils/Encoders/LoadStoreEncoders.cs deleted file mode 100644 index 3ca937c..0000000 --- a/src/Avr8Sharp/Core/Utils/Encoders/LoadStoreEncoders.cs +++ /dev/null @@ -1,158 +0,0 @@ -using static AVR8Sharp.Core.Utils.Encoders.EncoderHelpers; - -namespace AVR8Sharp.Core.Utils.Encoders; - -internal static class LoadStoreEncoders -{ - - public static ushort LD(string rd, string src) - { - int d = ParseRegister(rd); - int mode = StldXyz(src); - return (ushort)(DestRd(d) | mode); - } - - public static ushort LDD(string rd, string src) - { - int d = ParseRegister(rd); - int mode = StldYzQ(src); - return (ushort)(DestRd(d) | mode); - } - - public static ushort LDI(string rd, string k) - { - int d = ParseRegister(rd, 16, 31); - int imm = ParseImm(k, 0, 255); - return (ushort)(0xE000 | (DestRd(d) & 0xF0) | ((imm & 0xF0) << 4) | (imm & 0x0F)); - } - - public static (ushort, ushort) LDS(string rd, string k) - { - int d = ParseRegister(rd); - int addr = ParseImm(k, 0, 65535); - return ((ushort)(0x9000 | DestRd(d)), (ushort)addr); - } - - public static ushort LPM(string rd, string src) - { - if (string.IsNullOrEmpty(rd)) return 0x95C8; - int d = ParseRegister(rd); - int op = src switch { "Z" => 4, "Z+" => 5, _ => throw new Exception("LPM: second operand must be Z or Z+") }; - return (ushort)(0x9000 | DestRd(d) | op); - } - - public static ushort ELPM(string rd, string src) - { - if (string.IsNullOrEmpty(rd)) return 0x95D8; - int d = ParseRegister(rd); - int op = src switch { "Z" => 6, "Z+" => 7, _ => throw new Exception("ELPM: second operand must be Z or Z+") }; - return (ushort)(0x9000 | DestRd(d) | op); - } - - public static ushort LSR(string rd) => - (ushort)(0x9406 | DestRd(ParseRegister(rd))); - - public static ushort ASR(string rd) => - (ushort)(0x9405 | DestRd(ParseRegister(rd))); - - public static ushort ROR(string rd) => - (ushort)(0x9407 | DestRd(ParseRegister(rd))); - - public static ushort SWAP(string rd) => - (ushort)(0x9402 | DestRd(ParseRegister(rd))); - - public static ushort POP(string rd) => - (ushort)(0x900F | DestRd(ParseRegister(rd))); - - public static ushort PUSH(string rd) => - (ushort)(0x920F | DestRd(ParseRegister(rd))); - - public static ushort ST(string dst, string rr) - { - int r = ParseRegister(rr); - int mode = StldXyz(dst); - return (ushort)(0x0200 | DestRd(r) | mode); - } - - public static ushort STD(string dst, string rr) - { - int r = ParseRegister(rr); - int mode = StldYzQ(dst); - return (ushort)(0x0200 | DestRd(r) | mode); - } - - public static (ushort, ushort) STS(string addr, string rr) - { - int r = ParseRegister(rr); - int k = ParseImm(addr, 0, 65535); - return ((ushort)(0x9200 | DestRd(r)), (ushort)k); - } - - public static ushort IN(string rd, string A) - { - int d = ParseRegister(rd); - int a = ParseImm(A, 0, 63); - return (ushort)(0xB000 | DestRd(d) | ((a & 0x30) << 5) | (a & 0x0F)); - } - - public static ushort OUT(string A, string rr) - { - int r = ParseRegister(rr); - int a = ParseImm(A, 0, 63); - return (ushort)(0xB800 | DestRd(r) | ((a & 0x30) << 5) | (a & 0x0F)); - } - - public static ushort LAC(string z, string rd) - { - if (!z.Equals("Z", StringComparison.OrdinalIgnoreCase)) throw new Exception("First operand must be Z"); - return (ushort)(0x9206 | DestRd(ParseRegister(rd))); - } - - public static ushort LAS(string z, string rd) - { - if (!z.Equals("Z", StringComparison.OrdinalIgnoreCase)) throw new Exception("First operand must be Z"); - return (ushort)(0x9205 | DestRd(ParseRegister(rd))); - } - - public static ushort LAT(string z, string rd) - { - if (!z.Equals("Z", StringComparison.OrdinalIgnoreCase)) throw new Exception("First operand must be Z"); - return (ushort)(0x9207 | DestRd(ParseRegister(rd))); - } - - public static ushort XCH(string z, string rd) - { - if (!z.Equals("Z", StringComparison.OrdinalIgnoreCase)) throw new Exception("First operand must be Z"); - return (ushort)(0x9204 | DestRd(ParseRegister(rd))); - } - - public static ushort SER(string rd) - { - int d = ParseRegister(rd, 16, 31); - return (ushort)(0xEF0F | (DestRd(d) & 0xF0)); - } - - private static int StldXyz(string xyz) => xyz.ToUpperInvariant() switch - { - "X" => 0x900C, - "X+" => 0x900D, - "-X" => 0x900E, - "Y" => 0x8008, - "Y+" => 0x9009, - "-Y" => 0x900A, - "Z" => 0x8000, - "Z+" => 0x9001, - "-Z" => 0x9002, - _ => throw new Exception($"Not a valid indirect address mode: {xyz}") - }; - - private static int StldYzQ(string yzq) - { - var (baseReg, q) = LineParser.ParseYzDisplacement(yzq); - if (q < 0 || q > 63) throw new Exception("Displacement q out of range 0..63"); - int r = 0x8000; - if (baseReg == 'Y') r |= 0x8; - r |= ((q & 0x20) << 8) | ((q & 0x18) << 7) | (q & 0x7); - return r; - } -} diff --git a/src/Avr8Sharp/Core/Utils/Encoders/MiscEncoders.cs b/src/Avr8Sharp/Core/Utils/Encoders/MiscEncoders.cs deleted file mode 100644 index 7e8cae3..0000000 --- a/src/Avr8Sharp/Core/Utils/Encoders/MiscEncoders.cs +++ /dev/null @@ -1,30 +0,0 @@ -using static AVR8Sharp.Core.Utils.Encoders.EncoderHelpers; - -namespace AVR8Sharp.Core.Utils.Encoders; - -internal static class MiscEncoders -{ - public static ushort NOP() => 0x0000; - public static ushort RET() => 0x9508; - public static ushort RETI() => 0x9518; - public static ushort SLEEP() => 0x9588; - public static ushort WDR() => 0x95A8; - public static ushort BREAK() => 0x9598; - public static ushort ICALL() => 0x9509; - public static ushort IJMP() => 0x9409; - public static ushort EICALL() => 0x9519; - public static ushort EIJMP() => 0x9419; - - public static ushort DES(string k) - { - int imm = ParseImm(k, 0, 15); - return (ushort)(0x940B | (imm << 4)); - } - - public static ushort SPM(string? operand = null) - { - if (string.IsNullOrEmpty(operand)) return 0x95E8; - if (operand.Trim().ToUpperInvariant() == "Z+") return 0x95F8; - throw new Exception("SPM operand must be empty or Z+"); - } -} diff --git a/src/Avr8Sharp/Core/Utils/Tokenizer.cs b/src/Avr8Sharp/Core/Utils/Tokenizer.cs index ee18c1b..24278e2 100644 --- a/src/Avr8Sharp/Core/Utils/Tokenizer.cs +++ b/src/Avr8Sharp/Core/Utils/Tokenizer.cs @@ -152,6 +152,15 @@ public static List Tokenize(string line) continue; } + // Bare '.' = current location counter (used in expressions like ".+4"). + // Reuses the Dollar token kind, which the evaluator resolves to the current PC. + if (c == '.') + { + tokens.Add(new Token(TokenKind.Dollar, ".", col)); + i++; + continue; + } + // Identifiers, mnemonics, registers if (char.IsLetter(c) || c == '_') { diff --git a/tests/Avr8Sharp.Tests/AssemblerNewFeaturesTests.cs b/tests/Avr8Sharp.Tests/AssemblerNewFeaturesTests.cs index 0f969cd..e2acf54 100644 --- a/tests/Avr8Sharp.Tests/AssemblerNewFeaturesTests.cs +++ b/tests/Avr8Sharp.Tests/AssemblerNewFeaturesTests.cs @@ -422,6 +422,46 @@ public void Device_CaseInsensitive() Assert.That(result, Is.EqualTo(Bytes("05b9"))); } +[Test] +public void Device_ATmega2560_ExtendedIO_Usart3() +{ +// UDR3 = data-space 0x136 (extended I/O) — verified against avr-as -mmcu=atmega2560 +var asm = new AvrAssembler(deviceName: "ATmega2560"); +var result = asm.Assemble("sts UDR3, r16"); +Assert.That(asm.Errors, Is.Empty); +Assert.That(result, Is.EqualTo(Bytes("00933601"))); // STS 0x136, r16 +} + +[Test] +public void Device_ATmega2560_PortL() +{ +// PORTL = data-space 0x10B (port H-L block) — verified against avr-as +var asm = new AvrAssembler(deviceName: "ATmega2560"); +var result = asm.Assemble("sts PORTL, r16"); +Assert.That(asm.Errors, Is.Empty); +Assert.That(result, Is.EqualTo(Bytes("00930b01"))); // STS 0x10B, r16 +} + +[Test] +public void Device_ATmega2560_Twi_LowIO() +{ +// TWBR = data-space 0xB8 (extended I/O) — verified against avr-as +var asm = new AvrAssembler(deviceName: "ATmega2560"); +var result = asm.Assemble("lds r17, TWBR"); +Assert.That(asm.Errors, Is.Empty); +Assert.That(result, Is.EqualTo(Bytes("1091b800"))); // LDS r17, 0xB8 +} + +[Test] +public void Device_ATmega2560_RAMEND() +{ +// RAMEND = 0x21FF (8 KB SRAM); lo8 = 0xFF +var asm = new AvrAssembler(deviceName: "ATmega2560"); +var result = asm.Assemble("ldi r16, lo8(RAMEND)"); +Assert.That(asm.Errors, Is.Empty); +Assert.That(result, Is.EqualTo(Bytes("0fef"))); // LDI r16, 0xFF +} + // --- Multi-file assembly (P2) --- [Test] public void Global_Extern_DirectivesAccepted() diff --git a/tests/Avr8Sharp.Tests/AssemblerTests.cs b/tests/Avr8Sharp.Tests/AssemblerTests.cs index 3e9846b..e69ba36 100644 --- a/tests/Avr8Sharp.Tests/AssemblerTests.cs +++ b/tests/Avr8Sharp.Tests/AssemblerTests.cs @@ -73,7 +73,7 @@ public void Empty_When_Error () Assert.That(result, Is.Empty); Assert.That(assembler.Errors, Has.Count.EqualTo(1)); - Assert.That(assembler.Errors[0], Is.EqualTo("Line 0: Rd out of range: 16<>31")); + Assert.That(assembler.Errors[0], Is.EqualTo("Line 1: Rd out of range: 16<>31")); Assert.That(assembler.Lines, Is.Empty); Assert.That(assembler.Labels, Is.Empty); @@ -839,6 +839,87 @@ public void RJMP_Dot () Assert.That (Bytes ("00c0"), Is.EqualTo (assembler.Assemble ("rjmp ."))); } + [Test(Description = "'.' location-counter arithmetic (.+N / .-N) matches avr-as")] + public void DotArithmetic () + { + Assert.That (Bytes ("0000" + "01c0"), Is.EqualTo (new AvrAssembler ().Assemble ("nop\nrjmp .+2"))); + Assert.That (Bytes ("0000" + "ffcf"), Is.EqualTo (new AvrAssembler ().Assemble ("nop\nrjmp .-2"))); + Assert.That (Bytes ("0000" + "02d0"), Is.EqualTo (new AvrAssembler ().Assemble ("nop\nrcall .+4"))); + } + + [Test(Description = ".rept N / .endr repeats the block N times (matches avr-as)")] + public void ReptBlock () + { + var assembler = new AvrAssembler (); + var result = assembler.Assemble (".rept 3\n nop\n.endr"); + + Assert.That (assembler.Errors, Is.Empty); + Assert.That (Bytes ("000000000000"), Is.EqualTo (result)); // 3 NOPs (2 bytes each) + } + + [Test(Description = ".rept composes with a \\name-parameterised macro")] + public void ReptWithMacro () + { + var assembler = new AvrAssembler (); + var result = assembler.Assemble (".macro twice reg\n inc \\reg\n inc \\reg\n.endm\n.rept 2\n twice r16\n.endr"); + + Assert.That (assembler.Errors, Is.Empty); + Assert.That (Bytes ("03950395" + "03950395"), Is.EqualTo (result)); // 4x INC r16 + } + + [Test(Description = ".endr without .rept is an error")] + public void OrphanEndr () + { + var assembler = new AvrAssembler (); + assembler.Assemble ("nop\n.endr"); + + Assert.That (assembler.Errors, Has.Count.EqualTo (1)); + Assert.That (assembler.Errors[0], Does.Contain (".endr without .rept")); + } + + [Test(Description = "Data-segment labels get sequential SRAM addresses (lds/sts resolve)")] + public void DataSegment_Labels () + { + var assembler = new AvrAssembler (); + // buf=0x100, flag=0x101; lds r16,buf / lds r17,flag + var result = assembler.Assemble (".data\nbuf: .byte 1\nflag: .byte 1\n.text\nlds r16, buf\nlds r17, flag"); + + Assert.That (assembler.Errors, Is.Empty); + Assert.That (Bytes ("00910001" + "10910101"), Is.EqualTo (result)); + } + + [Test(Description = ".lcomm reserves SRAM and forward-referenced data symbols resolve in lds/sts")] + public void DataSegment_LcommAndForwardRef () + { + var assembler = new AvrAssembler (); + // data segment placed after code (avr-gcc style); counter resolves to 0x100 + var result = assembler.Assemble ("lds r16, counter\nsts counter, r16\n.data\ncounter: .byte 1"); + + Assert.That (assembler.Errors, Is.Empty); + Assert.That (Bytes ("00910001" + "00930001"), Is.EqualTo (result)); + } + + [Test(Description = "Forward-referenced address expression (sym+1) resolves in lds/sts (pass two)")] + public void DataSegment_ForwardExpression () + { + var assembler = new AvrAssembler (); + // p = 0x100 via .comm placed after use; p+1 must resolve to 0x101 + var result = assembler.Assemble ("lds r17, p+1\n.comm p, 2"); + + Assert.That (assembler.Errors, Is.Empty); + Assert.That (Bytes ("10910101"), Is.EqualTo (result)); // LDS r17, 0x101 + } + + [Test(Description = "A deferred 4-byte instruction as the last line resolves correctly")] + public void Deferred4ByteAsLastLine () + { + // Regression: ElementSize concatenated the resolved pair into a 2-byte string. + var call = new AvrAssembler (); + Assert.That (Bytes ("0e940300" + "0000"), Is.EqualTo (call.Assemble ("call end\nnop\nend:"))); + var lds = new AvrAssembler (); + Assert.That (Bytes ("00910001"), Is.EqualTo (lds.Assemble ("lds r16, x\n.data\nx: .byte 1"))); + } + // ----------------------------------------------------------------------- // avr-gcc / avr-as front-end compatibility // ----------------------------------------------------------------------- @@ -920,6 +1001,47 @@ public void UnknownDirectiveStillErrors () Assert.That (assembler.Errors, Is.Not.Empty); } + [Test(Description = "Error messages report 1-based line numbers")] + public void ErrorLineNumbersAreOneBased () + { + var assembler = new AvrAssembler (); + assembler.Assemble ("nop\nnop\n.frobnicate 1"); + + Assert.That (assembler.Errors, Has.Count.EqualTo (1)); + Assert.That (assembler.Errors[0], Does.StartWith ("Line 3:")); + } + + [Test(Description = "TryAssemble returns false and surfaces errors on failure")] + public void TryAssemble_Failure () + { + var assembler = new AvrAssembler (); + var ok = assembler.TryAssemble (".frobnicate 1", out var bytes, out var errors); + + Assert.That (ok, Is.False); + Assert.That (bytes, Is.Empty); + Assert.That (errors, Is.Not.Empty); + } + + [Test(Description = "TryAssemble returns true with bytes on success")] + public void TryAssemble_Success () + { + var assembler = new AvrAssembler (); + var ok = assembler.TryAssemble ("ADD r16, r11", out var bytes, out var errors); + + Assert.That (ok, Is.True); + Assert.That (errors, Is.Empty); + Assert.That (Bytes ("0b0d"), Is.EqualTo (bytes)); + } + + [Test(Description = "AssembleOrThrow throws AssemblerException carrying the errors")] + public void AssembleOrThrow_Throws () + { + var assembler = new AvrAssembler (); + var ex = Assert.Throws (() => assembler.AssembleOrThrow (".frobnicate 1")); + + Assert.That (ex!.Errors, Is.Not.Empty); + } + private byte[] Bytes (string hex) { var result = new byte[hex.Length / 2];