diff --git a/magnifier.sln b/magnifier.sln new file mode 100644 index 0000000..1926568 --- /dev/null +++ b/magnifier.sln @@ -0,0 +1,23 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36705.20 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "magnifier", "magnifier\magnifier.pyproj", "{F619A247-964C-4302-AFCD-31ACD6D5E1E6}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F619A247-964C-4302-AFCD-31ACD6D5E1E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F619A247-964C-4302-AFCD-31ACD6D5E1E6}.Release|Any CPU.ActiveCfg = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {194D84A8-E21B-4F8D-8C26-7C946E91B65B} + EndGlobalSection +EndGlobal diff --git a/magnifier/demos.py b/magnifier/demos.py new file mode 100644 index 0000000..b7f89a5 --- /dev/null +++ b/magnifier/demos.py @@ -0,0 +1,184 @@ +class Demos: + def get_tasks(self): + return { + "Demo 1: Pojedyncza LED": self.get_demo1_code, + "Demo 2: Sekwencja 8 LED": self.get_demo2_code, + "Demo 3: Switche i LEDy": self.get_demo3_code, + "Demo 4: Sumuj liczby na switchach": self.get_demo4_code + } + + def get_demo1_code(self): + return """ +; Demo 1: Migająca dioda LED0 +; Rekomendowana prędkość: 20 + + udata 0x20 +CNT res 1 + +TIMEOUT EQU 0xFF ; maksymalnie 255 (8 bitów) + +START: + ; Zapal bit zerowy + BSF PORTB, 0 + CALL DELAY + ; Zgaś bit zerowy + BCF PORTB, 0 + CALL DELAY + + GOTO START + +DELAY: + ; Wczytaj opóźnienie + MOVLW TIMEOUT + MOVWF CNT +LOOP: + ; Dekrementuj CNT, + DECFSZ CNT, F + GOTO LOOP + ; tak długo jak nie jest zerem + RETURN + +""" + + def get_demo2_code(self): + return """ +; Demo 2: Sekwencja 8 LED +; Rekomendowana prędkość: 60 + + udata 0x20 +CNT res 1 +MASK res 1 + +TIMEOUT EQU 0xFF ; maksymalnie 255 (8 bitów) + +; Wczytaj 1 jako maskę. +; Ta jedynka będzie przesuwana +; instrukcją RLF. +RESET: + MOVLW 1 + MOVWF MASK + ; Wyczyść także bit Carry + ; specjalnego rejestru STATUS, + ; aby przesunięcie wykonało + ; się bez dodatkowego bitu. + BCF STATUS, C ; = BCF STATUS 0 + +START: + ; Wczytaj maskę do akumulatora. + MOVF MASK, W + ; Wypisz na PORTB + MOVWF PORTB + ; (Opóźnienie) + CALL DELAY + + ; Przesuń maskę + RLF MASK, F + + ; Tak długo, jak RLF + ; nie wywołało zapalenia bitu + ; Carry, wykonuj START. + BTFSS STATUS, C ; BTFSS STATUS, 0 + GOTO START + + ; W przeciwnym razie: + ; zacznij program od początku. + GOTO RESET + +DELAY: + ; Wczytaj opóźnienie + MOVLW TIMEOUT + MOVWF CNT +LOOP: + ; Dekrementuj CNT, + DECFSZ CNT, F + GOTO LOOP + ; tak długo jak nie jest zerem + RETURN + +""" + + def get_demo3_code(self): + return """ +; Demo 3: Switche i LEDy +; Rekomendowana prędkość: 1 + +; Demonstracja mająca na celu pokazać +; jak działa odczytywanie danych ze switchy (PORT A/C) +; oraz wypisywanie wartości do LEDów (PORT B/D). +START: + ; Wczytaj PORTA, PORTC (switche) + ; i wyprowadź na PORTB, PORTD (diody) + MOVF PORTA, W + MOVWF PORTB + + MOVF PORTC, W + MOVWF PORTD + + ; Opcjonalnie: + ; Użyj + ; @PRINTLN [@TIME] Akumulator -> @W + ; lub + ; @PRINTF W + ; aby wydrukować zawartość akumulatora + ; do standardowego wyjścia (linii poleceń). + + GOTO START + +""" + + def get_demo4_code(self): + return """ +; Demo 4: Sumuj liczby na switchach +; Rekomendowana prędkość: 1+ + +; Opcjonalnie: +; Zapisz wartości z PORTA, PORTC +; w zmiennych LICZBA1, LICZBA2. + udata 0x20 +LICZBA1 RES 1 +LICZBA2 RES 1 + + +START: + + ; Wczytaj pierwszą liczbę + ; (Switche 0-7) + MOVF PORTA, W + MOVWF LICZBA1 + @PRINTLN (@ @PC) LICZBA1 = @W + + ; Wczytaj drugą liczbę + ; (Switche 8-15) + MOVF PORTC, W + MOVWF LICZBA2 + @PRINTLN (@ @PC) LICZBA2 = @W + + ; Wyczyść akumulator + CLRW + + ; Dodaj liczby do siebie + ADDWF LICZBA1, W + ADDWF LICZBA2, W + + ; Sprawdź Carry + CLRF PORTD + BTFSC STATUS, C + CALL USTAW_CARRY + + ; Wypisz wynik na PORTB (LEDy) + MOVWF PORTB + ; Opcjonalnie też na STDOUT + @PRINTLN (@@PC) LICZBA1 + LICZBA2 = @W + @PRINTLN + + GOTO START + +USTAW_CARRY: + ; Jeśli Carry jest zapalone, + ; nastąpiło przepełnienie, + ; zatem zapalamy 8. LED (PORTD<0>) + @PRINTLN Wykryto przepełnienie, zapalam Carry! + BSF PORTD, 0 + RETURN + + """ \ No newline at end of file diff --git a/magnifier/gui.py b/magnifier/gui.py new file mode 100644 index 0000000..4012116 --- /dev/null +++ b/magnifier/gui.py @@ -0,0 +1,682 @@ +import tkinter as tk +from tkinter import ttk, scrolledtext, messagebox +import re + +# Im większa maksymalna prędkość, +# tym bardziej niestabilna symulacja. +MAX_SIMULATION_SPEED = 5_000 + +# Import własnych modułów: +# pic_core - zawiera logikę procesora (rejestry, wykonywanie instrukcji) +# tasks - biblioteka gotowych kodów (zadań) do wczytania +from pic_core import PIC16F87XA +from tasks import TaskLibrary + +class PICSimulatorGUI: + """ + Inicjalizacja głównego okna i stanu symulatora. + """ + def __init__(self, root): + self.root = root + self.root.title("magnifier-debug (PIC16F87XA)") + self.root.geometry("1600x950") + + self.pic = PIC16F87XA() + self.task_lib = TaskLibrary() + self.tasks = self.task_lib.tasks + self.switches = [0] * 16 # Tablica przechowująca stan fizycznych przełączników (0 lub 1) + + # Zmienne dla debuggera + self.breakpoints = set() # Przechowuje numery linii z breakpointami (1-based) + self.pc_to_line = {} # Mapowanie PC -> numer linii w edytorze + + self.btns = {} # Przechowalnia przycisków kontrolnych (Start, Stop itp.) + + self.create_widgets() + + """ + Tworzy i układa wszystkie elementy GUI (przyciski, ekrany, pamięć). + """ + def create_widgets(self): + hw_frame = ttk.LabelFrame(self.root, text="Hardware", padding="5") + hw_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5) + + # LEDy + led_f = ttk.Frame(hw_frame); led_f.pack(pady=5) + ttk.Label(led_f, text="LEDy (LD15-LD0)", font=("Arial",10,"bold")).pack() + c_f = ttk.Frame(led_f); c_f.pack() + self.led_canvas = [] + for i in range(16): + f=ttk.Frame(c_f); f.pack(side=tk.LEFT, padx=2) + c=tk.Canvas(f,width=30,height=30,bg="white",highlightthickness=1); c.pack() + o=c.create_oval(5,5,25,25,fill="gray",outline="black") + self.led_canvas.append((c,o)) + ttk.Label(f,text=f"{15-i}",font=("Arial",7)).pack() + + # Switche + sw_f = ttk.Frame(hw_frame); sw_f.pack(pady=5) + ttk.Label(sw_f, text="Switche (SW15-SW0)", font=("Arial",10,"bold")).pack() + sw_b_f = ttk.Frame(sw_f); sw_b_f.pack() + self.switch_buttons = [] + for i in range(16): + b = tk.Button(sw_b_f, text=f"SW{15-i}\nOFF", width=5, bg="lightgray", command=lambda x=i: self.toggle_switch(x)) + b.pack(side=tk.LEFT, padx=2) + self.switch_buttons.append(b) + + # Panel główny + pane = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL) + pane.pack(fill=tk.BOTH, expand=True, padx=5) + + # Panel kodu (linie + tekst) + prog_f = ttk.LabelFrame(pane, text="Program (ASM)", padding="0"); pane.add(prog_f, weight=2) + + # Kontener dla edytora + editor_frame = ttk.Frame(prog_f) + editor_frame.pack(fill=tk.BOTH, expand=True) + + # Scrollbar + self.text_scroll = ttk.Scrollbar(editor_frame) + self.text_scroll.pack(side=tk.RIGHT, fill=tk.Y) + + # Płótno (canvas) na numery linii i breakpointy + self.linenumbers = tk.Canvas(editor_frame, width=40, bg="#f0f0f0") + self.linenumbers.pack(side=tk.LEFT, fill=tk.Y) + + # Główny edytor tekstu + self.program_text = tk.Text(editor_frame, font=("Courier", 10), undo=True, yscrollcommand=self.text_scroll.set) + self.program_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + self.text_scroll.config(command=self.program_text.yview) + + # Konfiguracja tagów dla debuggera + self.program_text.tag_config("current_line", background="#ffee33") # Żółte tło + self.program_text.tag_config("error_line", background="#ffcccc") + + # Wydarzenia (eventy) dla edytora + self.program_text.bind("<>", self._on_content_changed) + self.program_text.bind("", self._on_content_changed) + self.program_text.bind("", self._on_scroll) + self.program_text.bind("", self._on_scroll) + self.program_text.bind("", self._on_scroll) + + # Kliknięcie w pasek numerów (obsługa breakpointów) + self.linenumbers.bind("", self.toggle_breakpoint) + + # --- ROZBUDOWANY PANEL PAMIĘCI --- + mem_container = ttk.Frame(pane); pane.add(mem_container, weight=2) + + # Rejestry statusowe + status_f = ttk.LabelFrame(mem_container, text="Rejestry kluczowe", padding="5") + status_f.pack(fill=tk.X, padx=2, pady=2) + + self.status_vars = {} + status_grid = ttk.Frame(status_f) + status_grid.pack(fill=tk.X) + + regs = [ + ("W", "Akumulator"), + ("STATUS", "Flagi"), + ("PC", "Program Counter"), + ("FSR", "File Select Reg"), + ("PORTA", "Port A (SW 0-7)"), + ("PORTB", "Port B (LED 0-7)"), + ("PORTC", "Port C (SW 8-15)"), + ("PORTD", "Port D (LED 8-15)"), + ("TMR1L", "Timer1 Low"), + ("TMR1H", "Timer1 High") + ] + + # Generowanie siatki z rejestrami + for idx, (name, desc) in enumerate(regs): + row = idx // 2 + col = (idx % 2) * 3 + + ttk.Label(status_grid, text=f"{name}:", font=("Arial", 9, "bold"), width=8).grid(row=row, column=col, sticky=tk.W, padx=2, pady=1) + v = tk.StringVar(value="00") + ttk.Label(status_grid, textvariable=v, font=("Courier", 10, "bold"), foreground="blue", width=6).grid(row=row, column=col+1, sticky=tk.W, padx=2) + ttk.Label(status_grid, text=desc, font=("Arial", 8), foreground="gray").grid(row=row, column=col+2, sticky=tk.W, padx=5) + self.status_vars[name] = v + + # Notebook z zakładkami dla różnych widoków pamięci + mem_notebook = ttk.Notebook(mem_container) + mem_notebook.pack(fill=tk.BOTH, expand=True, padx=2, pady=2) + + # Zakładka 1: Pełny RAM (0x00-0xFF) + ram_frame = ttk.Frame(mem_notebook) + mem_notebook.add(ram_frame, text="RAM (0x00-0xFF)") + + self.ram_text = scrolledtext.ScrolledText(ram_frame, font=("Courier", 9), width=60) + self.ram_text.pack(fill=tk.BOTH, expand=True) + + # Zakładka 2: Zmienne użytkownika (0x20-0x7F) + user_frame = ttk.Frame(mem_notebook) + mem_notebook.add(user_frame, text="Zmienne (0x20-0x7F)") + + self.user_text = scrolledtext.ScrolledText(user_frame, font=("Courier", 9), width=60) + self.user_text.pack(fill=tk.BOTH, expand=True) + + # Zakładka 3: Rejestry SFR (0x00-0x1F + wybrane) + sfr_frame = ttk.Frame(mem_notebook) + mem_notebook.add(sfr_frame, text="SFR (systemowe)") + + self.sfr_text = scrolledtext.ScrolledText(sfr_frame, font=("Courier", 9), width=60) + self.sfr_text.pack(fill=tk.BOTH, expand=True) + + # Dół ekranu + btm = ttk.Frame(self.root); btm.pack(side=tk.BOTTOM, fill=tk.X, padx=5, pady=5) + log_f = ttk.LabelFrame(btm, text="Log"); log_f.pack(side=tk.BOTTOM, fill=tk.X) + self.output_text = scrolledtext.ScrolledText(log_f, height=4); self.output_text.pack(fill=tk.X) + + # Zadania (dema) + task_f = ttk.LabelFrame(btm, text="Wybierz zadanie (kod)", padding="5") + task_f.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + self.task_var = tk.StringVar() + self.task_combo = ttk.Combobox(task_f, textvariable=self.task_var, state="readonly", width=40) + self.task_combo['values'] = list(self.tasks.keys()) + self.task_combo.current(0) + self.task_combo.pack(side=tk.LEFT, padx=5, pady=5) + + ttk.Button(task_f, text="Wczytaj zadanie", command=self.load_selected_task).pack(side=tk.LEFT, padx=5) + + # Panel kontrolny + ctrl_f = ttk.LabelFrame(btm, text="Panel kontrolny"); ctrl_f.pack(side=tk.RIGHT) + self.btns["reset_btn"] = ttk.Button(ctrl_f, text= "Reset", command=self.reset_sim) + self.btns[ "load_btn"] = ttk.Button(ctrl_f, text="Wczytaj", command=self.load_program) + self.btns[ "run_btn"] = ttk.Button(ctrl_f, text="Uruchom", command=self.run) + self.btns[ "stop_btn"] = ttk.Button(ctrl_f, text= "Stop", command=self.stop) + self.btns[ "step_btn"] = ttk.Button(ctrl_f, text= "Krok", command=self.step) + + for btn in self.btns: + self.btns[btn].pack(side=tk.LEFT, padx=2) + + sep = ttk.Separator(ctrl_f, orient=tk.VERTICAL) + sep.pack(side=tk.LEFT, fill=tk.Y, padx=5) + + # Licznik prędkości (spinbox) + ttk.Label(ctrl_f, text="Szybkość:").pack(side=tk.LEFT) + + # Zmienna przechowująca wartość + self.speed_var = tk.IntVar(value=100) + + # Spinbox: pozwala wpisać liczbę lub klikać strzałkami + # from_/to określa zakres, increment o ile skacze przy kliknięciu + self.speed_spin = tk.Spinbox(ctrl_f, from_=1, to=MAX_SIMULATION_SPEED, increment=10, + textvariable=self.speed_var, width=8) + self.speed_spin.pack(side=tk.LEFT, padx=5) + + self.running = False + + # Inicjalne rysowanie linii + self._redraw_lines() + self.load_selected_task() + + # --- OBSŁUGA GUI I LINE NUMBERS --- + """ + Synchronizuje przewijanie tekstu z paskiem numerów linii. + """ + def _on_scroll(self, event): + self.program_text.yview_scroll(int(-1*(event.delta/120)), "units") + self._redraw_lines() + return "break" + + """ + Odświeża numery linii, gdy użytkownik wpisuje kod. + """ + def _on_content_changed(self, event=None): + self._redraw_lines() + + """ + Rysuje numery linii i kropki breakpointów na lewym pasku Canvas. + """ + def _redraw_lines(self): + self.linenumbers.delete("all") + i = self.program_text.index("@0,0") + while True: + dline = self.program_text.dlineinfo(i) + if dline is None: break + y = dline[1] + linenum = str(i).split(".")[0] + + # Rysuj numer linii + self.linenumbers.create_text(25, y, anchor="ne", text=linenum, font=("Courier", 10), fill="#555") + + # Rysuj breakpoint jeśli istnieje + if int(linenum) in self.breakpoints: + self.linenumbers.create_oval(5, y+2, 17, y+14, fill="red", outline="darkred") + + i = self.program_text.index(f"{i}+1line") + + def toggle_breakpoint(self, event): + # Pobierz linię na podstawie kliknięcia myszką Y + y = event.y + # Znajdź linię tekstu najbliższą tej współrzędnej Y + idx = self.program_text.index(f"@0,{y}") + linenum = int(idx.split(".")[0]) + + if linenum in self.breakpoints: + self.breakpoints.remove(linenum) + else: + self.breakpoints.add(linenum) + + self._redraw_lines() + + def highlight_execution_line(self): + # Usuń stare podświetlenie + self.program_text.tag_remove("current_line", "1.0", tk.END) + + # Pobierz aktualny PC i znajdź odpowiadającą linię kodu źródłowego + current_pc = self.pic.PC + source_line = self.pc_to_line.get(current_pc) + + if source_line: + # Podświetl linię (source_line to int, format tagu to "line.0") + self.program_text.tag_add("current_line", f"{source_line}.0", f"{source_line}.end") + self.program_text.see(f"{source_line}.0") # Scrolluj do linii + + # --- LOGIKA SYMULATORA --- + """ + Obsługa kliknięcia przycisku Switch (Hardware). + """ + def toggle_switch(self, i): + + # 15-i ponieważ staramy się rysować przyciski/ledy + # w układzie jak na laboratorium: 15, 14, ... 1, 0 + self.switches[15-i] = 1 - self.switches[15-i] + + # Aktualizacja wyglądu przycisku + self.switch_buttons[i].config(text=f"SW{15-i}\n{'ON' if self.switches[15-i] else 'OFF'}", bg="lightgreen" if self.switches[15-i] else "lightgray") + + # Budowanie bajtów dla PORTA i PORTC na podstawie switchy + pa, pc = 0, 0 + for x in range(8): + if self.switches[x]: pa |= (1< Nazwa zmiennej + # Pobieramy to bezpośrednio z rdzenia PIC, który przeanalizował kod (udata/res) + addr_map = {} + for name, addr in self.pic.SYMBOLS.items(): + # Interesują nas tylko zmienne w obszarze RAM użytkownika (0x20 - 0x7F) + # Ignorujemy rejestry systemowe (np. STATUS, PORTA) i etykiety kodu + if 0x20 <= addr <= 0x7F: + # Jeśli pod jednym adresem jest kilka nazw, łączymy je + # + # Aktualizacja: + # To powinno być naprawione i było problemem, ponieważ + # etykiety były traktowane jako symbole. Przez to, symulator + # pokazywał etykiety w miejscu przeznaczonym dla zmiennych użytkownika. + if addr in addr_map: + addr_map[addr] += f" / {name}" + else: + addr_map[addr] = name + + # 2. Iterujemy przez pamięć RAM użytkownika (0x20 do 0x7F) + for i in range(0x20, 0x80): + val = self.pic.file_registers[i] + + # Sprawdzamy, czy pod tym adresem jest nazwa w naszej mapie + var_name = addr_map.get(i, "") + + # Znak ASCII (kropka dla znaków niedrukowalnych) + ascii_char = chr(val) if 32 <= val < 127 else "." + + # Formatowanie wiersza + # 0x20: | FF | 255 | 11111111 | . | <- CNT1 + line = f"0x{i:02X}: | {val:02X} | {val:3d} | {val:08b} | {ascii_char} " + + # Jeśli zmienna ma nazwę, dodajemy ją + if var_name: + line += f"| <- {var_name}" + + self.user_text.insert(tk.END, line + "\n") + + # --- Podświetlanie --- + # Obliczamy numer linii w polu tekstowym (2 linie nagłówka + index) + line_num = i - 0x20 + 3 + + if var_name: + # Jeśli to zmienna użytkownika (zadeklarowana w kodzie) -> Zielone tło + self.user_text.tag_add("defined_var", f"{line_num}.0", f"{line_num}.end") + elif val != 0: + # Jeśli to tylko śmieci w pamięci (wartość > 0, ale bez nazwy) -> Szare tło + self.user_text.tag_add("nonzero", f"{line_num}.0", f"{line_num}.end") + + # Konfiguracja kolorów podświetlenia + self.user_text.tag_config("defined_var", background="#ccffcc") # Jasna zieleń dla zmiennych użytkownika + self.user_text.tag_config("nonzero", background="#f0f0f0") # Szary dla niezerowych wartości bez nazwy + + # --- SFR --- + self.sfr_text.delete("1.0", tk.END) + self.sfr_text.insert(tk.END, "Rejestr | Adr | Hex | Bin | Opis\n") + self.sfr_text.insert(tk.END, "---------------+-----+-----+----------+-------------\n") + + sfr_list = [ + (0x00, "INDF", "Indirect addr"), + (0x01, "TMR0", "Timer0"), + (0x02, "PCL", "PC Low byte"), + (0x03, "STATUS", "Status flags"), + (0x04, "FSR", "File Select"), + (0x05, "PORTA", "Port A I/O"), + (0x06, "PORTB", "Port B I/O"), + (0x07, "PORTC", "Port C I/O"), + (0x08, "PORTD", "Port D I/O"), + # (0x09, "PORTE", "Port E I/O"), + (0x0A, "PCLATH", "PC High"), + (0x0B, "INTCON", "Interrupt Ctrl"), + (0x0C, "PIR1", "Periph Int 1"), + (0x0D, "PIR2", "Periph Int 2"), + (0x0E, "TMR1L", "Timer1 Low"), + (0x0F, "TMR1H", "Timer1 High"), + (0x10, "T1CON", "Timer1 Ctrl") + ] + + for addr, name, desc in sfr_list: + val = self.pic.file_registers[addr] + self.sfr_text.insert(tk.END, f"{name:14s} | {addr:02X} | {val:02X} | {val:08b} | {desc}\n") + + # Podświetlenie linii w kodzie + self.highlight_execution_line() + + """ + Funkcja: Pobiera kod, parsuje go i ładuje do procesora. + """ + def load_program(self): + # Czyścimy stare błędy + self.program_text.tag_remove("error_line", "1.0", tk.END) + self.output_text.insert(tk.END, "--- Kompilacja ---\n") + + try: + # 1. Pobieranie kodu + raw_lines = self.program_text.get("1.0", tk.END).split('\n') + + clean_code_for_core = [] + self.pc_to_line = {} + executable_pc_counter = 0 + + # 2. Parsowanie wstępne dla GUI + for idx, line in enumerate(raw_lines): + line_num = idx + 1 + clean_line = line.split(';')[0].strip() + + if not clean_line: continue + + parts = clean_line.split() + token = parts[0].upper() + + # Lista ignorowanych przez licznik PC (musi być zgodna z Core) + NON_EXECUTABLE = ['UDATA', 'RES', 'EQU', 'ORG', 'CONFIG', '__CONFIG', 'END'] + + is_preprocessor = token.startswith('#') + is_label_only = token.endswith(':') and len(parts) == 1 + is_directive = token in NON_EXECUTABLE + if len(parts) > 1 and parts[1].upper() in ['RES', 'EQU']: is_directive = True + + # Dodajemy do listy dla Core + clean_code_for_core.append((clean_line, line_num)) + + # Mapowanie PC -> Linia + if not is_directive and not is_label_only and not is_preprocessor: + if token.endswith(':') and len(parts) > 1: + self.pc_to_line[executable_pc_counter] = line_num + executable_pc_counter += 1 + elif not token.endswith(':'): + self.pc_to_line[executable_pc_counter] = line_num + executable_pc_counter += 1 + + # 3. Wysłanie do Core + self.pic.program_memory = clean_code_for_core + self.pic.PC = 0 + + # 4. "Kompilacja" w Core + self.pic.build_instruction_cache() + + # Sprawdzenie flagi sukcesu + if hasattr(self.pic, 'compilation_success') and not self.pic.compilation_success: + raise ValueError("Błąd kompilacji (szczegóły nieznane).") + + self.output_text.insert(tk.END, f"✓ Załadowano {executable_pc_counter} instrukcji.\n") + self.output_text.see(tk.END) + self.update_display() + self._redraw_lines() + + except Exception as e: + error_msg = str(e) + self.output_text.insert(tk.END, f"🗙 Wystąpił błąd: {error_msg}\n") + self.output_text.see(tk.END) + self.pic.instruction_cache = [] # Czyścimy cache, żeby nie uruchomić złego kodu + + + # Szukamy wzorca "linii X" w komunikacie błędu (który dodaliśmy w Kroku 1) + match = re.search(r"linii (\d+)", error_msg) + if match: + error_line_num = int(match.group(1)) + self._highlight_error(error_line_num) + + """ + Pomocnicza funkcja do zaznaczania błędnej linii + """ + def _highlight_error(self, line_num): + self.program_text.tag_add("error_line", f"{line_num}.0", f"{line_num}.end") + self.program_text.see(f"{line_num}.0") + + """ + Resetuje procesor i czyści GUI. + """ + def reset_sim(self): + self.pic.reset() + self.program_text.tag_remove("current_line", "1.0", tk.END) + self.output_text.insert(tk.END," Symulator zresetowany.\n") + self.output_text.see(tk.END) + self.update_display() + + """ + Wykonuje jeden krok (jedną instrukcję assemblera). + """ + def step(self): + if self.pic.input_overrides: # Najpierw wgraj stany switchy do pamięci + for addr, val in self.pic.input_overrides.items(): + self.pic.file_registers[addr] = val + + if self.pic.PC < len(self.pic.instruction_cache): # Sprawdź czy PC nie wykracza poza kod + self.pic.step_fast() # Wykonaj instrukcję w rdzeniu + self.pic.PC += 1 + self.update_display() + else: + self.running = False + self.output_text.insert(tk.END, "Koniec programu.\n") + + """ + Zatrzymuje ciągłe wykonywanie. + """ + def stop(self): + self.running = False + + self.btns[ "run_btn"]["state"] = "enabled" + self.btns["stop_btn"]["state"] = "disabled" + + self.output_text.insert(tk.END," Wykonanie zatrzymane.\n") + self.output_text.see(tk.END) + + """ + Rozpoczyna ciągłe wykonywanie programu. + """ + def run(self): + + # Blokuj przyciski + if self.running: + self.output_text.insert(tk.END, " OSTRZEŻENIE: Symulacja już działa!") + + # Zabezpieczenie przed uruchomieniem pustego lub błędnego kodu + if not self.pic.instruction_cache or (hasattr(self.pic, 'compilation_success') and not self.pic.compilation_success): + # Zamiast głośnego messagebox, wypisujemy ostrzeżenie do logu + self.output_text.insert(tk.END, " OSTRZEŻENIE: Najpierw załaduj poprawny program (przycisk \"Wczytaj\").\n") + self.output_text.see(tk.END) + return + + self.btns["stop_btn"]["state"] = "enabled" + self.btns[ "run_btn"]["state"] = "disabled" + + self.running = True + self.output_text.insert(tk.END," Uruchomiono...\n") + self.output_text.see(tk.END) + + # Zabezpieczenie pętli głównej + try: + self.run_loop() + except Exception as e: + self.running = False + # Zamiast głośnego messagebox, wypisujemy błąd do logu + self.output_text.insert(tk.END, f" BŁĄD WYKONANIA (Runtime error): {str(e)}\n") + self.output_text.insert(tk.END, " Symulacja zatrzymana awaryjnie.\n") + self.output_text.see(tk.END) + + """ + Pętla symulacji wykonywana cyklicznie przez Tkinter. + """ + def run_loop(self): + if not self.running: return + + # Pobieranie z limitem 1-5000 + try: + raw_val = int(self.speed_spin.get()) + + # 1. max(1, ...) -> jeśli liczba mniejsza niż 1, weź 1 + # 2. min(5000, ...) -> jeśli liczba większa niż 5000, weź 5000 + cycles_per_frame = max(1, min(5000, raw_val)) + + # Opcjonalnie: Jeśli użytkownik wpisał złą liczbę, popraw ją w okienku: + if raw_val != cycles_per_frame: + self.speed_var.set(cycles_per_frame) + + except (ValueError, tk.TclError): + cycles_per_frame = 1 + + + instr_len = len(self.pic.instruction_cache) + cache = self.pic.instruction_cache + + # Synchronizacja wejść + if self.pic.input_overrides: + for addr, val in self.pic.input_overrides.items(): + self.pic.file_registers[addr] = val + + # Wykonaj X instrukcji w jednej klatce + for _ in range(cycles_per_frame): + if self.pic.PC >= instr_len: + self.running = False + break + + # --- BREAKPOINT CHECK --- + src_line = self.pc_to_line.get(self.pic.PC) + if src_line in self.breakpoints: + self.running = False + self.btns[ "run_btn"]["state"] = "enabled" + self.btns["stop_btn"]["state"] = "disabled" + self.output_text.insert(tk.END, f" Breakpoint na linii {src_line} (PC: 0x{self.pic.PC:02X})\n") + self.output_text.see(tk.END) + break + # ------------------------ + + # Wywołaj Timer1 kilka razy na instrukcję + # for _ in range(1): # Symuluj 1 cykl zegara na instrukcję + # self.pic.tick_timer1() + + # Pobranie i wykonanie instrukcji z cache + func, args = cache[self.pic.PC] + if func.__name__ == "DBG_WAIT": + # DBG_WAIT wymaga zmiany w zachowaniu, + # aby nie zawiesić całego programu dla + # prędkości (cycles_per_frame) > 1. + func(args[0] / cycles_per_frame) + else: + func(*args) + self.pic.PC += 1 + + self.update_display() + if self.running: + self.root.after(10, self.run_loop) + + + """ + Ładuje przykładowy kod z listy rozwijanej. + """ + def load_selected_task(self): + self.stop() + task_name = self.task_var.get() + if task_name in self.tasks: + code = self.tasks[task_name]() + self.program_text.delete("1.0", tk.END) + self.program_text.insert("1.0", code.strip()) + self.load_program() diff --git a/magnifier/magnififer.pyproj b/magnifier/magnififer.pyproj new file mode 100644 index 0000000..bf458e0 --- /dev/null +++ b/magnifier/magnififer.pyproj @@ -0,0 +1,44 @@ + + + Debug + 2.0 + f619a247-964c-4302-afcd-31acd6d5e1e6 + . + main.py + + + . + . + magnifier + magnifier + Global|PythonCore|3.13 + False + + + true + false + + + true + false + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/magnifier/main.py b/magnifier/main.py new file mode 100644 index 0000000..6fafb7a --- /dev/null +++ b/magnifier/main.py @@ -0,0 +1,7 @@ +import tkinter as tk +from gui import PICSimulatorGUI + +if __name__ == "__main__": + root = tk.Tk() + app = PICSimulatorGUI(root) + root.mainloop() diff --git a/magnifier/pic_core.py b/magnifier/pic_core.py new file mode 100644 index 0000000..8bb0f82 --- /dev/null +++ b/magnifier/pic_core.py @@ -0,0 +1,1255 @@ +import time + +r""" + _ __ _ + _ __ ___ __ _ __ _ _ __ (_)/ _(_) ___ _ __ + | '_ ` _ \ / _` |/ _` | '_ \| | |_| |/ _ \ '__| + | | | | | | (_| | (_| | | | | | _| | __/ | + |_| |_| |_|\__,_|\__, |_| |_|_|_| |_|\___|_| + |___/ + + magnifier - symulator mikroprocesora PIC16F87XA. + + Proces "kompilowania" kodu składa się z dwóch + głównych etapów: + + 1. Preprocessing; poszukiwanie etykiet + + Wpierw poszukujemy etykiet, dyrektyw alokacji + pamięci i aliasów w kodzie, licząc przy okazji + linijki kodu (sumę PC). + + 2. Cache'owanie instrukcji + + Dla każdej linijki kodu sprawdzane są OPcode'y. + Jeśli udało się rozpoznać znajomy OPcode, + dodawany jest on do cache'u wraz ze sparsowaną + (zinterpretowaną) wartością (o ile OPcode został + z taką wywołany) oraz wskaźnikiem do metody. + W ten sposób GUI może sterować wykonywaniem + programu, korzystając jedynie z garstki metod + symulatora (m.in. build_instruction_cache(), + step_fast(), reset()). + + Uwagi: + + 1. Dla wygody użytkowania, do symulatora + dodano nieistniejące w rzeczywistości meta-instrukcje + (o OPcodach @PRINTLN, @PRINTF, @WAIT; dwie pierwsze + dają namiastkę wsparcia dla UART). + Pozwalają one w prosty sposób wpłynąć na symulację + w celu debugowania problemów. + + 2. Dla BTFSS/BTFSC zastosowano uproszczenie polegające na + uznaniu akumulator za zwyczajny rejestr (pozwalając na + odczyt jego wartości) + + 3. Symulator jest w bardzo wczesnej fazie rozwoju + i istnieje duża szansa, że jeśli będzie używany sprzecznie + z zaleceniami/uwagami, to może wprowadzić w błąd. + + Autorzy + 177045, 177188 +""" + + +class PIC16F87XA: + def __init__(self): + self.W = 0 # Akumulator + self.file_registers = [0] * (2 ** 9) + + # Adresy rejestrów specjalnych. + # Niestety, w tej chwili większość z nich to "atrapy". + self.INDF_ADDR = 0x00 + self.PCL_ADDR = 0x02 + self.STATUS_ADDR = 0x03 + self.FSR_ADDR = 0x04 + self.PORTA_ADDR = 0x05 + self.PORTB_ADDR = 0x06 + self.PORTC_ADDR = 0x07 + self.PORTD_ADDR = 0x08 + self.INTCON_ADDR = 0x0B + self.PIR1_ADDR = 0x0C + self.TMR1L_ADDR = 0x0E + self.TMR1H_ADDR = 0x0F + self.T1CON_ADDR = 0x10 + + # Słownik do ręcznego wymuszania wartości wejść (symulacja sygnałów z zewnątrz) + self.input_overrides = {} + + # Wartości początkowe + self.file_registers[self.STATUS_ADDR] = 0x18 + + # --- FLAGI ALU (cache do interakcji z GUI) --- + # Dla wygody trzymamy flagi w zmiennych, a nie tylko w rejestrze STATUS. + # Muszą być synchronizowane metodami update_status/read_status. + self.PC = 0 + self.stack = [] + self.program_memory = [] + self.instruction_cache = [] + self.compilation_success = False + + # Flagi (cache) + self.C = 0 + self.DC = 0 + self.Z = 0 + self.PD = 1 + self.TO = 1 + + # --- TABLICA SYMBOLI --- + # Mapowanie nazw rejestrów na ich adresy w pamięci. + self.BASE_SYMBOLS = { + 'INDF': 0, + 'TMR0': 1, + 'PCL': 2, + 'STATUS': 3, + 'FSR': 4, + 'PORTA': 5, + 'PORTB': 6, + 'PORTC': 7, + 'PORTD': 8, + # 'PORTE': 9, + 'PCLATH': 10, + 'INTCON': 11, + 'PIR1': 12, + 'PIR2': 13, + 'TMR1L': 14, + 'TMR1H': 15, + 'T1CON': 16, + # Mnemoniki: + 'F': 1, + 'W': 0, + 'C': 0, # + 'DC': 1, # + 'Z': 2, # } Status + 'PD': 3, # + 'TO': 4, # + 'T1L': 14, + 'T1H': 15, + 'TMR1IF': 0, + 'TMR1ON': 0 + } + + self.SYMBOLS = self.BASE_SYMBOLS.copy() + self.LABELS = {} + + # --- Lista prawidłowych instrukcji --- + # Spis dozwolonych mnemoników asemblera PIC + self.VALID_MNEMONICS = [ + 'ADDWF', 'ANDWF', 'CLRF', 'CLRW', 'COMF', 'DECF', 'DECFSZ', + 'INCF', 'INCFSZ', 'IORWF', 'MOVF', 'MOVWF', 'NOP', 'RLF', 'RRF', + 'SUBWF', 'SWAPF', 'XORWF', 'BCF', 'BSF', 'BTFSC', 'BTFSS', + 'ADDLW', 'ANDLW', 'CALL', 'CLRWDT', 'GOTO', 'IORLW', 'MOVLW', + 'RETFIE', 'RETLW', 'RETURN', 'SLEEP', 'SUBLW', 'XORLW', + '@PRINTF', '@PRINTLN', '@WAIT' # meta-instrukcje + ] + + # --- SYNCHRONIZACJA STATUSU --- + """ + Przepisuje wartości zmiennych self.C, self.Z itp. do bajtu w pamięci RAM pod adresem STATUS. + """ + def update_status_from_cache(self): + current = self.file_registers[self.STATUS_ADDR] + new_status = (current & 0xE0) | (self.TO << 4) | (self.PD << 3) | (self.Z << 2) | (self.DC << 1) | (self.C << 0) + self.file_registers[self.STATUS_ADDR] = new_status + + """ + Odczytuje bajt z pamięci RAM (STATUS) i aktualizuje zmienne lokalne flag. + """ + def read_status_to_cache(self): + val = self.file_registers[self.STATUS_ADDR] + self.C = (val >> 0) & 1 + self.DC = (val >> 1) & 1 + self.Z = (val >> 2) & 1 + self.PD = (val >> 3) & 1 + self.TO = (val >> 4) & 1 + + # --- PAMIĘĆ --- + """ + Odczyt z rejestru. Obsługuje adresowanie pośrednie. + """ + def read_f(self, addr): + target = addr + if addr == 0: + target = self.file_registers[self.FSR_ADDR] + return self.file_registers[target] if target < len(self.file_registers) else 0 + + """ + Zapis do rejestru. Obsługuje adresowanie pośrednie i maskowanie do 8 bitów. + """ + def write_f(self, addr, val): + target = addr + if addr == 0: + target = self.file_registers[self.FSR_ADDR] + if target < len(self.file_registers): + self.file_registers[target] = val & 0xFF # Zabezpieczenie przed liczbami > 255 + if target == self.STATUS_ADDR: self.read_status_to_cache() # Jeśli zapisujemy do STATUS, musimy dodatkowo zaktualizować cache flag + + # --- FLAGI I TIMER --- + """ + Ustawia flagę Z, jeśli wynik operacji (obcięty do 8 bitów) wynosi 0. + """ + def set_zero_flag(self, value): + self.Z = 1 if (value & 0xFF) == 0 else 0 + + """ + Ustawia flagę C, jeśli nastąpiło przepełnienie (wynik > 255). + """ + def set_carry_flag(self, value): + self.C = 1 if value > 0xFF else 0 + + """ + Ustawia flagę DC (Digit Carry) - przeniesienie z 3. bitu na 4. (używane w BCD). + """ + def set_digit_carry(self, a, b): + self.DC = 1 if ((a & 0x0F) + (b & 0x0F)) > 0x0F else 0 + + """ + Symulacja Timera 1 (16-bitowy licznik). + Obecnie nie jest wykorzystywany (GUI steruje prędkością). + """ + def tick_timer1(self): + if self.file_registers[self.T1CON_ADDR] & 0x01: + tmr1 = self.file_registers[self.TMR1L_ADDR] | (self.file_registers[self.TMR1H_ADDR] << 8) + tmr1 += 1 + if tmr1 > 0xFFFF: + tmr1 = 0 + self.file_registers[self.PIR1_ADDR] |= 0x01 + self.file_registers[self.TMR1L_ADDR] = tmr1 & 0xFF + self.file_registers[self.TMR1H_ADDR] = (tmr1 >> 8) & 0xFF + + # --- INSTRUKCJE --- + """ + Dodaj zawartość rejestru f (F) + do zawartości akumulatora w (W). + Wynik zapisz w d. + + Aktualizuje bit Zero, Carry, DC/Borrow + rejestru STATUS. + + Przykład: + CLRW + ADDWF LICZBA1, W + ADDWF LICZBA2, W + MOVWF PORTB + ... + """ + def ADDWF(self, f, d): + if f == self.PCL_ADDR: + self.PC = (self.PC + 1 + self.W) - 1 + self.file_registers[self.PCL_ADDR] = (self.PC + 1) & 0xFF + return + + val = self.read_f(f) + res = self.W + val + self.set_carry_flag(res) + self.set_digit_carry(self.W, val) + + res &= 0xFF + self.set_zero_flag(res) + + if d == 0: + self.W = res + else: + self.write_f(f, res) + + self.update_status_from_cache() + + """ + Koniunkcja akumulatora z rejestrem f. + Wynik zapisz w d. + + Wykonuje koniunkcję (AND) zawartości + rejestru f (F) z zawartością akumulatora (W). + Ekwiwalent: W/F <- F & W + + Aktualizuje bit Zero + rejestru STATUS. + + Użycie: `ANDWF MASKA, W` + """ + def ANDWF(self, f, d): + val = self.read_f(f) + res = self.W & val + self.set_zero_flag(res) + + if d == 0: + self.W = res + else: + self.write_f(f, res) + + self.update_status_from_cache() + + """ + Wyczyść (wyzeruj) rejestr f. + + Aktualizuje bit Zero + rejestru STATUS. + + Użycie: `CLRF CNT` + """ + def CLRF(self, f): + self.write_f(f, 0) + self.Z = 1 + self.update_status_from_cache() + + """ + Wyczyść (wyzeruj) akumulator. + + Aktualizuje bit Zero + rejestru STATUS. + + Użycie: `CLRW` + """ + def CLRW(self): + self.W = 0 + self.Z = 1 + self.update_status_from_cache() + + """ + Dopełnij (/zaneguj) f. + Wynik zapisz w d. + + Aktualizuje bit Zero + rejestru STATUS. + + Użycie: `COMF ZMIENNA, W` + """ + def COMF(self, f, d): + val = self.read_f(f) + res = (~val) & 0xFF + self.set_zero_flag(res) + + if d == 0: + self.W = res + else: + self.write_f(f, res) + + self.update_status_from_cache() + + """ + Dekrementuj f. + Wynik zapisz w d. + + Ekwiwalent: + F-- + W/F <- F + + Aktualizuje bit Zero rejestru STATUS, + w przeciwieństwie do DECFSZ. + + Przykład: + DECF LICZNIK, F + ... + """ + def DECF(self, f, d): + val = self.read_f(f) + res = (val - 1) & 0xFF + self.set_zero_flag(res) + + if d == 0: + self.W = res + else: + self.write_f(f, res) + + self.update_status_from_cache() + + """ + Dekrementuj f warunkowo. + Wynik zapisz w d. + + Ekwiwalent: + if (F--) ... + W/F <- F + + NIE AKTUALIZUJE bitu Zero + rejestru STATUS, w przeciwieństwie + do DECF. + + Przykład: + DECFSZ LICZNIK, F + CALL WYPISZ_DANE + ... + """ + def DECFSZ(self, f, d): + val = self.read_f(f) + res = (val - 1) & 0xFF + + if d == 0: + self.W = res + else: + self.write_f(f, res) + + if res == 0: + self.PC += 1 + + """ + Inkrementuj f. + Wynik zapisz w d. + + Ekwiwalent: + F++ + W/F <- F + + Aktualizuje bit Zero rejestru STATUS, + w przeciwieństwie do INCFSZ. + + Przykład: + INCF LICZNIK, F + ... + """ + def INCF(self, f, d): + val = self.read_f(f) + res = (val + 1) & 0xFF + self.set_zero_flag(res) + + if d == 0: + self.W = res + else: + self.write_f(f, res) + + self.update_status_from_cache() + + """ + Inkrementuj f warunkowo. + Wynik zapisz w d. + + Ekwiwalent: + if (F++) ... + W/F <- F + + NIE AKTUALIZUJE bitu Zero + rejestru STATUS, w przeciwieństwie + do INCF. + + Przykład: + INCFSZ LICZNIK, F + CALL WYPISZ_DANE + ... + """ + def INCFSZ(self, f, d): + val = self.read_f(f) + res = (val + 1) & 0xFF + + if d == 0: + self.W = res + else: + self.write_f(f, res) + + if res == 0: + self.PC += 1 + + """ + Alternatywa (OR) z rejestrem f. + Wynik zapisz w d. + + W/F <- W | F + + Instrukcja aktualizuje bit Zero rejestru STATUS. + + Przykład: `IORWF DIRTY_BIT, W`. + """ + def IORWF(self, f, d): + val = self.read_f(f) + res = self.W | val + self.set_zero_flag(res) + + if d == 0: + self.W = res + else: + self.write_f(f, res) + + self.update_status_from_cache() + + """ + Wczytuje zawartość rejestru + do akumulatora/rejestru. + W/F <- F + + Domyślnie wczytuje rejetr do samego siebie. + F <- F + + Gdy d = 0 (lub użyto mnemoniku W): + W <- F + """ + def MOVF(self, f, d): + val = self.read_f(f) + self.set_zero_flag(val) + + if d == 0: + self.W = val + else: + self.write_f(f, val) + + self.update_status_from_cache() + + """ + Zapisuje zawartość akumulatora + do rejestru f. + """ + def MOVWF(self, f): + self.write_f(f, self.W) + + """ + No operation. + Nie wykonuje żadnej operacji, + marnuje cykl procesora. + Może zostać użyta do tworzenia opóźnień. + """ + def NOP(self): + pass + + """ + Obrót rejestru f w lewą stronę. + Wynik zapisz w d. + + Aktualizuje bit Carry rejestru STATUS. + + Przykład: RLF (v Carry) + 1010 1011 -----> [1] 0101 0110 + """ + def RLF(self, f, d): + val = self.read_f(f) + res = (val << 1) | self.C + self.C = 1 if (val & 0x80) else 0 + res &= 0xFF + + if d == 0: + self.W = res + else: + self.write_f(f, res) + + self.update_status_from_cache() + + """ + Obrót rejestru f w prawą stronę. + Wynik zapisz w d. + + Aktualizuje bit Carry rejestru STATUS. + + Przykład: RRF + 0010 1011 -----> 0001 0101 + """ + def RRF(self, f, d): + val = self.read_f(f) + res = (val >> 1) | (self.C << 7) + self.C = val & 1 + + if d == 0: + self.W = res + else: + self.write_f(f, res) + + self.update_status_from_cache() + + """ + Odejmij akumulator od rejestru. + W/F <- F - W + Aktualizuje STATUS (Carry, DC/Borrow, Zero). + """ + def SUBWF(self, f, d): + val = self.read_f(f) + res = val - self.W + self.C = 1 if res >= 0 else 0 + self.DC = 1 if ((val & 0x0F) - (self.W & 0x0F)) >= 0 else 0 + res &= 0xFF + self.set_zero_flag(res) + + if d == 0: + self.W = res + else: + self.write_f(f, res) + + self.update_status_from_cache() + + """ + Zamień półbajty rejestru f. + Wynik zapisz w d. + + Przykład: SWAPF + 0010 1011 --------> 1011 0010 + """ + def SWAPF(self, f, d): + val = self.read_f(f) + res = ((val & 0x0F) << 4) | ((val & 0xF0) >> 4) + + if d == 0: + self.W = res + else: + self.write_f(f, res) + + """ + Wykonaj operację różnicy symetrycznej (XOR) + względem rejestru f. Wynik zapisz w d. + Instrukcja aktualizuje bit Zero rejestru STATUS. + + Przykład: + Jeśli W = 13, f[...] = 5, to W ^ f[...] = 8. + 1101 ( W=13) + ^ 101 (f[...]= 5) + ----- + 1000 (W^f=8) + """ + def XORWF(self, f, d): + val = self.read_f(f) + res = self.W ^ val + self.set_zero_flag(res) + + if d == 0: + self.W = res + else: + self.write_f(f, res) + + self.update_status_from_cache() + + """ + Zgaś b-ty bit rejestru f. + + Uwaga: Ta wersja symulatora przyjmuje + akumulator jako jeden z rejestrów, + jednak w rzeczywistości to MUSI być + jeden z rejestrów! + """ + def BCF(self, f, b): + val = self.read_f(f) + val &= ~(1 << b) + self.write_f(f, val) + + """ + Zapal b-ty bit rejestru f. + + Uwaga: Ta wersja symulatora przyjmuje + akumulator jako jeden z rejestrów, + jednak w rzeczywistości to MUSI być + jeden z rejestrów! + """ + def BSF(self, f, b): + val = self.read_f(f) + val |= (1 << b) + self.write_f(f, val) + + """ + Bit Test F, Skip if Clear. + + Sprawdzenie warunkowe, + wykonuje następną instrukcję, jeśli + bit b rejestru f jest zapalony. + Ekwiwalent if (f) ... + + Uwaga: Ta wersja symulatora przyjmuje + akumulator jako jeden z rejestrów, + jednak w rzeczywistości to MUSI być + jeden z rejestrów! + + Przykład: + BTFSC STATUS, 0 ; Sprawdza Carry + CALL RAISE_ASSERTION_ERROR + ... + """ + def BTFSC(self, f, b): + val = self.read_f(f) + if not (val & (1 << b)): + self.PC += 1 + + """ + Bit Test F, Skip if Set. + + Sprawdzenie warunkowe, + wykonuje następną instrukcję, jeśli + bit b rejestru f jest zgaszony. + Ekwiwalent if (!f) ... + + Uwaga: Ta wersja symulatora przyjmuje + akumulator jako jeden z rejestrów, + jednak w rzeczywistości to MUSI być + jeden z rejestrów! + + Przykład: + BTFSS FLAGI, 2; Flaga odpowiadająca za XYZ + CALL RAISE_ASSERTION_ERROR + ... + """ + def BTFSS(self, f, b): + val = self.read_f(f) + if (val & (1 << b)): + self.PC += 1 + + """ + Dodaj literał do akumulatora. + + Pozwala na dodanie literału ze wsparciem + dla flag przepełnienia (Carry) oraz zera (Zero). + + Przykład: `ADDLW 1`. + """ + def ADDLW(self, k): + res = self.W + k + self.set_carry_flag(res) + self.set_digit_carry(self.W, k) + + self.W = res & 0xFF + self.set_zero_flag(self.W) + self.update_status_from_cache() + + """ + Koniunkcja (AND) z literałem. + + Pozwala na wykonanie operacji + koniunkcji z literałem, lub aliasem. + + Przykład: `ANDLW 0x01`, `ANDLW DIRTY_BIT_MASK`. + """ + def ANDLW(self, k): + self.W &= k + self.set_zero_flag(self.W) + self.update_status_from_cache() + + """ + Uruchomienie podprocedury. + + Tak długo, jak stos zawiera + mniej niż 8 adresów w kodzie programu, + pozwala na wykonanie skoku + do podprocedury i powrotu za pomocą RETURN. + Jeśli stos zawiera już 8 adresów - nie robi nic. + + Przykład użycia: + MOVF ZMIENNA, W + CALL MOJA_PODPROCEDURA + ... + MOJA_PODPROCEDURA: ... + RETLW 42 + """ + def CALL(self, label): + if len(self.stack) < 8: + self.stack.append(self.PC) + self.PC = label - 1 + + def CLRWDT(self): + self.TO = 1 + self.PD = 1 + self.update_status_from_cache() + + """ + Przeskok do etykiety. + """ + def GOTO(self, label): + self.PC = label - 1 + + """ + Alternatywa (OR) z literałem. + Pozwala na wykonanie operacji + alternatywy z literałem, lub aliasem. + Przykład: `IORLW 0x01`, `IORLW DIRTY_BIT`. + """ + def IORLW(self, k): + self.W |= k + self.set_zero_flag(self.W) + self.update_status_from_cache() + + """ + Wczytuje literał do akumulatora. + k może być liczbą w systemie dziesiętnym, + lub heksadecymalnym. W obecnej wersji nie + ma wsparcia dla liczb w postaci binarnej. + Przykład: `MOVLW 0x01` + Wspiera wczytywanie aliasów (np. dla + `LIMIT EQU 0xff` można wywołać `MOVLW LIMIT`). + """ + def MOVLW(self, k): + self.W = k + + """ + Powrót z przerwania. + Ponieważ przerwania nie są zaimplementowane, + jest to instrukcja-atrapa. + Pełni tą samą rolę, co RETURN. + """ + def RETFIE(self): + self.PC = self.stack.pop() if self.stack else self.PC + + """ + Połączenie MOVLW + RETURN. + Wczytuje literał do akumulatora (w) + i wraca (o ile stos nie jest pusty) + do ostatniego zapisanego adresu w kodzie programu. + Uwaga: ze względu na to, że nie zaimplementowany został + skrót `dt 0x00, 0x01, 0x02, ...`, każde wczytanie + bajtu do akumulatora musi odbyć się w osobnej linijce, np.: + ADDWF PCL, F + RETLW 0x00 + RETLW 0x01 + RETLW 0x02 + ... + """ + def RETLW(self, k): + self.W = k + self.PC = self.stack.pop() if self.stack else self.PC + + """ + Pozwala na powrót z podprocedury. + Nie robi nic, jeśli na stosie nie znajduje się + żaden adres, do którego można byłoby wrócić. + """ + def RETURN(self): + self.PC = self.stack.pop() if self.stack else self.PC + + """ + Funkcja-atrapa, aktualizuje jedynie flagi + wewnątrz rejestru STATUS. + """ + def SLEEP(self): + self.TO = 1 + self.PD = 0 + self.update_status_from_cache() + + """ + Odejmuje literał od akumulatora. + W -= k + Wspiera aliasy; aktualizuje rejestr STATUS (Carry, DC/Borrow, Zero). + Użycie: `SUBLW 0x01` + """ + def SUBLW(self, k): + res = k - self.W + self.C = 1 if res >= 0 else 0 + self.DC = 1 if ((k & 0x0F) - (self.W & 0x0F)) >= 0 else 0 + self.W = res & 0xFF + self.set_zero_flag(self.W) + self.update_status_from_cache() + + """ + Wykonuje XOR na literale. + Przykład: + Jeśli W = 13, k = 5, to W ^ k = 8. + 1101 (W=13) + ^ 101 (k= 5) + ----- + 1000 (W^k=8) + Jeśli wynikiem jest 0, ustawia bit + zera w rejestrze STATUS. + """ + def XORLW(self, k): + self.W ^= k + self.set_zero_flag(self.W) + self.update_status_from_cache() + + """ + [META-INSTRUKCJA] - stworzona na potrzeby symulatora! + Pozwala wypisać wartość rejestru (w tym akumulatora). + Wypisywana wartość jest w postaci heksadecymalnej, + binarnej oraz dziesiętnej. + Przykład: `@PRINTF W` + Wypisuje: `[1768860228.5797740] f[W(0)]=01111111/0x7F/127` + """ + def DBG_PRINTF(self, f, special_name=None): + time_formatted = str(time.time()).ljust(18, '0') # dopisanie zer na końcu + name = str(f) + if special_name is not None: + name = special_name + "(" + name + ")" + if f == 0: + f_val = self.W + else: + f_val = self.read_f(f) + print(f"[{time_formatted}] f[{name}]={f_val:08b}/0x{f_val:02X}/{f_val}") + + """ + [META-INSTRUKCJA] - stworzona na potrzeby symulatora! + Pozwala wypisywać ciągi znaków (BEZ DWUKROPKA!) + na standardowe wyjście (linia poleceń). + + Szablony: + @PRINTLN wspiera wzorce: + - @TIME: UNIXowy timestamp (float), + - @PC: Program Counter, + - @W: zawartość akumulatora, + - @STATUS: zawartość rejestru statusowego. + + Przykład: `@PRINTLN [@TIME] Wartość wczytana z PORTA: @W` + Wypisuje: `[1768860228.5797740] Wartość wczytana z PORTA: 3` + """ + def DBG_PRINTLN(self, printstr): + time_formatted = str(time.time()).ljust(18, '0') # dopisanie zer na końcu + printstr = self._replace_many(printstr, { + '@TIME': time_formatted, + '@PC': self.PC, + '@W': self.W, + '@STATUS': self.file_registers[self.STATUS_ADDR] + }) + print(printstr) + + """ + [META-INSTRUKCJA] - stworzona na potrzeby symulatora! + Naiwne uśpienie symulatora. + + Usypia cały symulator, dlatego czas uśpienia powinien być + możliwie niewielki. Alternatywą jest skonstruowanie prostej + pętli marnującej cykle procesora na NOPach lub DECFSZ. + + Uwaga: Działa najlepiej dla prędkości = 1. + Dla prędkości > 1 czas uśpienia maleje + odwrotnie proporcjonalnie do prędkości (k ms / prędkość). + + Użycie: `@WAIT 1000` + """ + def DBG_WAIT(self, k): + time.sleep(k / 1_000) + + # --- KOMPILATOR (CORE) --- + + """ + Funkcja pomocnicza tłumacząca symbole, liczby w postaci binarnej/hex/dziesiętnej. + """ + def _resolve_val(self, op): + op = op.strip().upper() # Pomocnicza: zamienia string (np. '0xFF', 'STATUS') na liczbę + if not op: + return 0 + + try: # Obsługa formatów liczb Assemblera PIC + if op.startswith("D'") and op.endswith("'"): return int(op[2:-1]) + if op.startswith("B'") and op.endswith("'"): return int(op[2:-1], 2) + if op.startswith("0X"): return int(op, 16) + if op.isdigit(): return int(op) + except ValueError: + pass + + if op in self.SYMBOLS: # Jeśli `op` to nie liczba, szukamy w tabeli symboli (stałe, nazwy rejestrów) + return self.SYMBOLS[op] + + raise ValueError(f"Nieznany symbol: '{op}'") + + """ + Podmienia iteracyjnie łańcuch znaków, + zgodnie z kluczem i wartościami ze słownika. + Używana przez @PRINTLN. + """ + def _replace_many(self, obj: str, dictionary: dict) -> str: + for key in dictionary: + obj = obj.replace(key, str(dictionary[key])) + return obj + + """ + I Przebieg "kompilacji" (preprocessing): + 1. Znajduje etykiety (np. 'LOOP:') i przypisuje im numery instrukcji. + 2. Obsługuje dyrektywy pamięci (RES, EQU). + """ + def preprocess_code(self): + self.SYMBOLS = self.BASE_SYMBOLS.copy() + current_ram_addr = 0x20 # Pamięć użytkownika zaczyna się od 0x20 + instruction_counter = 0 # Licznik 'prawdziwych' instrukcji (pomija puste linie/etykiety) + + for line_data in self.program_memory: # Wyciągnij czysty tekst (obsługa krotek z numerami linii) + if isinstance(line_data, tuple): + clean_line = line_data[0].split(';')[0].strip() + else: + clean_line = line_data.split(';')[0].strip() + if not clean_line: + continue + + parts = clean_line.split() + first_token = parts[0].upper() + + if first_token.endswith(':'): # Wykrywanie etykiet (zakończonych dwukropkiem) + label_name = first_token[:-1] + self.LABELS[label_name] = instruction_counter # Jeśli za etykietą jest kod w tej samej linii, licznik rośnie + if len(parts) > 1: + instruction_counter += 1 + continue + # Rezerwacja pamięci (RES) + if len(parts) >= 3 and parts[1].upper() == 'RES': + var_name = first_token + try: size = int(parts[2]) + except: size = 1 + if var_name in self.SYMBOLS: raise KeyError(f"Redeklaracja '{var_name}'. Spróbuj użyć innej nazwy.") + self.SYMBOLS[var_name] = current_ram_addr + current_ram_addr += size + continue + # Ustawienie adresu RAM (UDATA) + if first_token == 'UDATA': + if len(parts) > 1: + try: + current_ram_addr = int(parts[1], 16) if '0X' in parts[1].upper() else int(parts[1]) + except: pass + continue + # Aliasy (EQU) + if len(parts) >= 3 and parts[1].upper() == 'EQU': + try: + val_str = parts[2] + val = int(val_str, 16) if '0X' in val_str.upper() else int(val_str) + if first_token in self.SYMBOLS: raise KeyError(f"Redeklaracja '{first_token}'. Spróbuj użyć innej nazwy.") + self.SYMBOLS[first_token] = val + except: pass + continue + instruction_counter += 1 # Zwykła instrukcja? Zwiększ licznik + + + # --- PARSOWANIE ARGUMENTÓW --- + # Funkcje pomocnicze rozdzielające argumenty po przecinku + + """ + Zwraca indeks adresu rejestru (f) w słowniku SYMBOLS + oraz lokalizację zapisu (d) ze wsparciem dla mnemoników (w, f). + Domyślna wartość d = 1 (zapis do rejestru f). + Używana przez np. MOVF. + """ + def parse_fd(self, ops): + p = ops.split(',') + return self._resolve_val(p[0]), (self._resolve_val(p[1]) if len(p)>1 else 1) + + """ + Zwraca indeks adresu rejestru (f) w słowniku SYMBOLS. + Używana przez np. CLRF. + """ + def parse_f(self, op): + return self._resolve_val(op) + + """ + Zwraca indeks adresu rejestru (f) w słowniku SYMBOLS + oraz jego bit (b). + Jeśli nie podano bitu, domyślną zwracaną wartością jest 0. + Używana przez np. BSF. + """ + def parse_fb(self, ops): + p = ops.split(',') + return self._resolve_val(p[0]), (self._resolve_val(p[1]) & 0x0f if len(p)>1 else 0) + + """ + Zwraca literał podany przez użytkownika modulo 255. + Używana przez np. MOVLW. + """ + def parse_k(self, op): + return self._resolve_val(op) & 0xff + + """ + Interpretuje etykietę jako numer instrukcji. + Niestety, na ten moment nie wspiera GOTO $. + """ + def parse_label(self, label_name): + try: + offset = self.LABELS[label_name] + except: + raise KeyError(f"Runtime error: wykryto odwołanie do nieistniejącej etykiety '{label_name}'") + + return offset + + """ + II Przebieg kompilacji (budowanie cache): + Parsuje każdą linię, rozpoznaje instrukcję i przypisuje jej funkcję Pythonową. + Dzięki temu pętla symulacji (step_fast) jest szybka (nie parsuje tekstu w kółko). + """ + def build_instruction_cache(self): + self.preprocess_code() # Najpierw znajdź etykiety + self.instruction_cache = [] + self.compilation_success = True + + for line_data in self.program_memory: + if isinstance(line_data, tuple): + line = line_data[0] + line_num = line_data[1] + else: + line = line_data + line_num = 0 + + clean_line = line.split(';')[0].strip() + if not clean_line: continue + + parts = clean_line.split() + if parts[0].endswith(':'): + parts = parts[1:] + if not parts: continue + + token = parts[0].upper() + if token in ['UDATA', 'RES', 'EQU', 'ORG', 'CONFIG', '__CONFIG']: continue + if len(parts) > 1 and parts[1].upper() in ['RES', 'EQU']: continue + + mnemonic = parts[0].upper() + operands = " ".join(parts[1:]) + + try: + # Sprawdzenie poprawności instrukcji + if mnemonic not in self.VALID_MNEMONICS: + raise ValueError(f"Nieznana instrukcja: '{mnemonic}'") + + # --- MAPOWANIE TEKSTU NA FUNKCJE --- + # Każdy blok if/elif przypisuje odpowiednią metodę klasy (np. self.ADDWF) + # oraz parsuje argumenty do formatu liczbowego. + + if mnemonic == "ADDWF": + args = self.parse_fd(operands) + self.instruction_cache.append((self.ADDWF, args)) + + elif mnemonic == "MOVF": + args = self.parse_fd(operands) + self.instruction_cache.append((self.MOVF, args)) + + elif mnemonic == "MOVWF": + args = (self.parse_f(operands),) + self.instruction_cache.append((self.MOVWF, args)) + + elif mnemonic == "DECFSZ": + args = self.parse_fd(operands) + self.instruction_cache.append((self.DECFSZ, args)) + + elif mnemonic == "GOTO": + args = (self.parse_label(operands),) + self.instruction_cache.append((self.GOTO, args)) + + elif mnemonic == "CALL": + args = (self.parse_label(operands),) + self.instruction_cache.append((self.CALL, args)) + + elif mnemonic == "CLRF": + args = (self.parse_f(operands),) + self.instruction_cache.append((self.CLRF, args)) + + elif mnemonic == "MOVLW": + args = (self.parse_k(operands),) + self.instruction_cache.append((self.MOVLW, args)) + + elif mnemonic == "BCF": + args = self.parse_fb(operands) + self.instruction_cache.append((self.BCF, args)) + + elif mnemonic == "BSF": + args = self.parse_fb(operands); + self.instruction_cache.append((self.BSF, args)) + + elif mnemonic == "RETURN": + self.instruction_cache.append((self.RETURN, ())) + + elif mnemonic == "ANDWF": + args = self.parse_fd(operands) + self.instruction_cache.append((self.ANDWF, args)) + + elif mnemonic == "CLRW": + self.instruction_cache.append((self.CLRW, ())) + + elif mnemonic == "COMF": + args = self.parse_fd(operands) + self.instruction_cache.append((self.COMF, args)) + + elif mnemonic == "DECF": + args = self.parse_fd(operands) + self.instruction_cache.append((self.DECF, args)) + + elif mnemonic == "INCF": + args = self.parse_fd(operands) + self.instruction_cache.append((self.INCF, args)) + + elif mnemonic == "INCFSZ": + args = self.parse_fd(operands) + self.instruction_cache.append((self.INCFSZ, args)) + + elif mnemonic == "IORWF": + args = self.parse_fd(operands) + self.instruction_cache.append((self.IORWF, args)) + + elif mnemonic == "NOP": + self.instruction_cache.append((self.NOP, ())) + + elif mnemonic == "RLF": + args = self.parse_fd(operands) + self.instruction_cache.append((self.RLF, args)) + + elif mnemonic == "RRF": + args = self.parse_fd(operands) + self.instruction_cache.append((self.RRF, args)) + + elif mnemonic == "SWAPF": + args = self.parse_fd(operands) + self.instruction_cache.append((self.SWAPF, args)) + + elif mnemonic == "SUBWF": + args = self.parse_fd(operands) + self.instruction_cache.append((self.SUBWF, args)) + + elif mnemonic == "XORWF": + args = self.parse_fd(operands) + self.instruction_cache.append((self.XORWF, args)) + + elif mnemonic == "ADDLW": + args = (self.parse_k(operands),) + self.instruction_cache.append((self.ADDLW, args)) + + elif mnemonic == "ANDLW": + args = (self.parse_k(operands),) + self.instruction_cache.append((self.ANDLW, args)) + + elif mnemonic == "CLRWDT": + self.instruction_cache.append((self.CLRWDT, ())) + + elif mnemonic == "IORLW": + args = (self.parse_k(operands),) + self.instruction_cache.append((self.IORLW, args)) + + elif mnemonic == "RETFIE": + self.instruction_cache.append((self.RETFIE, ())) + + elif mnemonic == "RETLW": + args = (self.parse_k(operands),) + self.instruction_cache.append((self.RETLW, args)) + + elif mnemonic == "SLEEP": + self.instruction_cache.append((self.SLEEP, ())) + + elif mnemonic == "SUBLW": + args = (self.parse_k(operands),) + self.instruction_cache.append((self.SUBLW, args)) + + elif mnemonic == "XORLW": + args = (self.parse_k(operands),) + self.instruction_cache.append((self.XORLW, args)) + + elif mnemonic == "BTFSS": + args = self.parse_fb(operands) + self.instruction_cache.append((self.BTFSS, args)) + + elif mnemonic == "BTFSC": + args = self.parse_fb(operands) + self.instruction_cache.append((self.BTFSC, args)) + + elif mnemonic == "@PRINTF": + args = (self.parse_f(operands), operands) + self.instruction_cache.append((self.DBG_PRINTF, args)) + + elif mnemonic == "@PRINTLN": + args = (operands,) + self.instruction_cache.append((self.DBG_PRINTLN, args)) + + elif mnemonic == "@WAIT": + args = (self.parse_f(operands),) + self.instruction_cache.append((self.DBG_WAIT, args)) + + else: + # To miejsce jest teoretycznie nieosiągalne po sprawdzeniu VALID_MNEMONICS + raise ValueError(f"Nieznana instrukcja (Internal error): '{mnemonic}'") + + # Przechwytujemy błąd i wyrzucamy nowy, który zawiera numer linii + except ValueError as e: + self.compilation_success = False + raise ValueError(f"Błąd w linii {line_num}: {str(e)}") + + """ + Wykonuje jeden cykl (krok) symulatora. + Uwaga: poniższy fragment wykorzystywany jest tylko + przez GUI do wykonania kroku. + """ + def step_fast(self): + if self.input_overrides: # 1. Obsługa wymuszeń wejść (dla celów testowych/symulacji zewnętrznych sygnałów) + for addr, val in self.input_overrides.items(): + self.file_registers[addr] = val + self.tick_timer1() # 2. Inkrementacja Timera (działa niezależnie od instrukcji) + if self.PC < len(self.instruction_cache): # 3. Wykonanie instrukcji + func, args = self.instruction_cache[self.PC] + func(*args) + + """ + Pełny reset symulatora do stanu początkowego. + """ + def reset(self): + self.W = 0 + self.file_registers = [0] * (2 ** 9) + self.file_registers[self.STATUS_ADDR] = 0x18 # domyślna wartość rejestru STATUS + self.PC = 0 + self.stack = [] + self.read_status_to_cache() + self.compilation_success = False diff --git a/magnifier/tasks.py b/magnifier/tasks.py new file mode 100644 index 0000000..bbbbc2b --- /dev/null +++ b/magnifier/tasks.py @@ -0,0 +1,19 @@ + +class TaskLibrary: + def __init__(self): + self.tasks = {} + + # --- Programy demonstracyjne --- + # Używamy try-except, żeby błąd w jednym pliku nie wywalił całego programu + try: + from demos import Demos + self.tasks["--- Demonstracyjne ---"] = self.get_empty + self.tasks.update(Demos().get_tasks()) + self.tasks["--- Koniec prog. demon. ---"] = self.get_empty + except: pass + + def get_tasks(self): + return self.tasks + + def get_empty(self): + return "; Wybierz zadanie."