6502.org Forum  Projects  Code  Documents  Tools  Forum
It is currently Fri Nov 22, 2024 7:50 pm

All times are UTC




Post new topic Reply to topic  [ 8 posts ] 
Author Message
PostPosted: Mon Sep 21, 2015 6:59 am 
Offline

Joined: Mon Sep 21, 2015 6:43 am
Posts: 1
I'm currently writing a C64 Simulator/Emulator in C++. My goal is to get a better understanding of how computers work internally and at the same time it's a project for school:

I aim to implement the 6502 CPU, the SID and the VIC. The CPU is far from being perfect and finished, but it seems to work. My problem is my missing understanding about the following two things:

- 1) Cycle Timing
- 2) How does the CPU work together with the VIC/SID.

In my CPU class, I do have one method doing the fetch-decode-execute phase:

Code:
int CPU::doCycles(int cyclesToExecute){
   int cycleCounter = cyclesToExecute;

   do{
      byte opcode = fetchPC();   // get OP-Code from program counter

      Instruction* inst = this->decode(opcode);      // decode instruction
      if (inst != nullptr){
         cycleCounter -= inst->getNumberOfCycles();

         inst->execute();

         this->Registers.PC++;   // increase PC
      }
   } while (cycleCounter > 0);
   return (cyclesToExecute - cycleCounter);
}


Code:
   
// example of an instruction: LDA Zeropage (Opcode, Mnemonic code, Num of cycles, Lambda expression
   instructionTable.insert(
      (new Instruction(0xA5, "LDA", 3, [](CPU* cpu) {
      // read the byte after opcode and load it into RegA
      cpu->Registers.A = cpu->ZeroPage_Read();
   }, this))->getPair());

These instruction objects are inserted into a hashmap. The instructions do not do any Timing calculations - I simply assign the number of cycles they require from a 6502 datasheet to the instruction object so I can get it with (inst->getNumberOfCycles();)

Code:
void C64::run(){
   int cycles = 10;        // Which number should I put here?
   int previousCycles = 0;

   do{   
      // VIC.doCycles();
      // SID.doCycles();
      // Interrupts...
      previousCycles = cpu.doCycles(cycles + previousCycles);

   } while (true);
   
}


This is what I do in the main loop of the Emulator. As you can see, I pass a number to the doCycles of the CPU, which executes instructions until the cyclescounter is 0 or beyond, and Returns the difference. If more than the specified number of cycles has been used, it will get less cycles during the next iteration.

My questions:
- Does this make any sense? I doubt it somehow, but I fail to see how I should do it :(
- If yes - is the precision enough to ever see some result with the SID or VIC? Or do I have to split the instructions into pieces so that I can make it more cycle-exact?
- Probably I have to execute this Loop 1mio times a second, to get the 0.98 MHz of the 6502, right?

About topic 2: If anybody has the time and nerves...I would be so thankful to get some "dummy-proof" Information about how the SID/VIC communicate with the CPU. Is it with Interrupts? All the Information I find in the Internet are way too complex for me to understand :/


Top
 Profile  
Reply with quote  
PostPosted: Mon Sep 21, 2015 10:28 am 
Offline
User avatar

Joined: Thu Dec 11, 2008 1:28 pm
Posts: 10985
Location: England
Hi decsis, and welcome!

That's quite the ambitious project - by all means have a go, but I hope you also considered tackling a simpler machine, like for example the PET.

Running each subsystem for some number of cycles should, I think, produce a reasonable approximation of the machine behaviour. However, you are right to think that a smaller number will produce a better approximation, and in fact for precise emulation you do have to deal with each cycle. As soon as the number of cycles is less than the length of an instruction, you need a different structure to the code, because the present code can't really do several distinct things during a multi-cycle instruction.

But I would say you don't need to start with such an extreme goal. Running for a video line's worth of cycles would make the point that you're not trying to be cycle-exact and pixel-perfect. Such an emulation should do a good job of emulating a Basic program which does sound and graphics. It won't do such a good job on a game or a demo, depending on how aggressive the machine code is.

I can't say much about the working of VIC-based machines. I think interaction between the subsystems can only be a combination of
- the CPU gets interrupts from the video and sound chips (and other chips: VIA, PIA, UART, whatever it has)
- chips' responses to changes in command registers
- and to changes in status registers
- chips read memory which the CPU has written
- possibly, the chips also write memory. Not sure if that happens.


Top
 Profile  
Reply with quote  
PostPosted: Mon Sep 21, 2015 2:24 pm 
Offline
User avatar

Joined: Fri Dec 11, 2009 3:50 pm
Posts: 3367
Location: Ontario, Canada
decsis wrote:
- 1) Cycle Timing
- 2) How does the CPU work together with the VIC/SID.

Welcome, decsis. It's a fascinating topic! The mechanism that's at the very lowest level is simple, but worthy of close attention. As you're aware, one instruction -- a single statement in your 65xx assembly-language source -- takes multiple clocks (aka CPU cycles or bus cycles) to execute. In general, every one of those cycles is a transaction between the CPU and the outside world (ie, memory and IO). For example the first cycle of any instruction involves the CPU placing PC on the address bus and setting the R/W pin high (read). This causes the data bus to return a byte from memory to the CPU -- and that byte is the first byte of the instruction in question. The remaining cycles vary according to the instruction, but each cycle (except for the occasional "dead" or dummy cycle) is a complete and self-contained transaction -- a read or a write -- between the CPU and the outside world.

Aside from fetching the instructions themselves, some of the other transactions involve the data operated upon by the instructions -- and this includes the data read from and written to the VIC/SID. This is the underpinning for everything else, whether in the context of an interrupt or otherwise.

In 1976, MOS Technology published three excellent manuals documenting the 65xx family in general and the KIM-1 microcomputer. Links to all three are below, but in particular I suggest you study APPENDIX A -- SUMMARY OF SINGLE CYCLE EXECUTION from the Hardware Manual. Bear in mind that the CPU is unaware of any distinction between IO devices and memory -- it merely operates on the address provided (and user hardware determines whether memory or IO will respond to that address). IOW the CPU talks to the VIC/SID registers just as if they were bytes in memory.

Best of luck, and don't hesitate to ask for further info,

Jeff

MCS6500 MICROCOMPUTER FAMILY HARDWARE MANUAL
MCS6500 MICROCOMPUTER FAMILY PROGRAMMING MANUAL
KIM-1 MICROCOMPUTER MODULE USER MANUAL

_________________
In 1988 my 65C02 got six new registers and 44 new full-speed instructions!
https://laughtonelectronics.com/Arcana/ ... mmary.html


Top
 Profile  
Reply with quote  
PostPosted: Tue Sep 22, 2015 2:59 am 
Offline

Joined: Tue Jul 24, 2012 2:27 am
Posts: 679
64doc.txt is your friend for cycle-specific CPU instruction operation, and undocumented/invalid/illegal opcodes.

Likewise, there's a vic-ii.txt showing how it interacts with the bus.

When writing a C64 emulator, you do need to emulate the bus actions mid-instruction, at least if you want to support demos, fastloaders, and advanced games. Many of these trigger writes on the bus at specific cycles, sometimes using the read-modify-write instructions which actually perform multiple writes per instruction.

As a simple architectural overview, the C64's bus actually is accessed at 2MHz. The CPU alternates with the VIC-II's reading of graphics data, with each effectively running at 1MHz, transparently ignorant of what the other is doing.* However, the VIC-II does need to interrupt the CPU to use full 2MHz accesses when starting a new character line, as there isn't enough bandwidth at 1MHz to keep up. The CIAs and SID don't control the bus at all; they're accessed by the CPU on its cycles.


* = Actually, there is 1 esoteric interaction between the two: When reading from unconnected addresses, the data from the prior access still lingers on the bus. This has been used for detecting/defeating emulators (though modern emulators emulate that effect as well) and goofy things like trying to determine the temperature of the system programmatically.

_________________
WFDis Interactive 6502 Disassembler
AcheronVM: A Reconfigurable 16-bit Virtual CPU for the 6502 Microprocessor


Top
 Profile  
Reply with quote  
PostPosted: Tue Sep 22, 2015 7:33 am 
Offline
User avatar

Joined: Sun Dec 29, 2002 8:56 pm
Posts: 460
Location: Canada
Hi decsis,

You've picked quite a complex project for a school project. I hope it goes well for you. I've written a couple of simpler emulators / simulators in particular one for FISA64 (a processor project) which is under my github account. If you are looking for some poor code samples. It contains a keyboard / Screen emulation. There are also several projects on the web for C64 emulators. (VICE / MEM). One really needs to know a bit about both hardware and software in order to write an emulator.

As White Flame says
Quote:
When writing a C64 emulator, you do need to emulate the bus actions mid-instruction, at least if you want to support demos, fastloaders, and advanced games. Many of these trigger writes on the bus at specific cycles, sometimes using the read-modify-write instructions which actually perform multiple writes per instruction.

You may however be able to get a simulator/emulator working much more easily if it doesn't support all software. There is no need to worry about all the details of a emulator all at once.

In order to communicate with devices (VIC, SID) in the system a number of Read() and Write() methods are required in the different objects.
There could be a system wide Read() // Write() methods in the C64 object.
The C64.Read() or .Write() would have to resolve the memory addressing for the SID / VIC devices (And also the CIA's).
It would look something like:
Code:
int C64::Read(int ad) {
if (ad >= VIC_ADDRESS && ad < VIC_ADDRESS+VIC_NUMREGS)
    return VIC.Read(ad);
else if (ad >= SID_ADDRESS && ad < SID_ADDRESS+SID_NUMREGS)
   return SID.Read(ad);
else
  return memory.Read(ad);
}

Of course it much more complex than that due to memory management in the C64.

In the cpu emulator object, the cpu would then read / write bytes from the system object.
Code:
// somewhere in the cpu emulation
// get a data byte
    data = C64::Read(ad);


The hierarchy and connection of objects in the emulator needs to follow the real hardware. Looking at schematics might help.

You may want to run some devices as separate threads (which requires thread safe objects). In order to better control performance.

_________________
http://www.finitron.ca


Top
 Profile  
Reply with quote  
PostPosted: Tue Sep 22, 2015 9:01 am 
Offline

Joined: Mon Jan 07, 2013 2:42 pm
Posts: 576
Location: Just outside Berlin, Germany
Rob Finch wrote:
There is no need to worry about all the details of a emulator all at once.
Based on my experience with my crude 65816 emulator project, I would second that advice. In my case, I decided to completely ignore timing stuff. Timing is overrated, right? Just get the job done!

Some practical advice for day-to-day work:

The most important thing for me was "getting the loop going": A simple setup that defines memory (64K for the 6502, 16M for the 65816), starts at a certain address, and reads/evaluates the next byte. At the beginning, all it will do is print "Instruction $XX not coded yet" for everything except 00 (BRK, to get out of the loop), but that will get you going. Now add the most simple commands first (like NOP and CLC). This is more a motivational trick than sound engineering, but it works, and if you're working alone, you're going to need all the motivation you can get.

In that vein, print out a table with the opcodes and tape it to the back of your door. Every time you've coded an instruction, you get to go up and make a big X with a black felt marker. So you've got a few minutes of time before you go to bed? Code, say, the INY instruction, cross it out of your table, go to bed with a smile.

The biggest mistake I have made so far is waiting too long to create a comprehensive test suite, instead using targeted snippets. Now I've had to make a rather large change to the architecture, and I don't have jack to do serious regression testing with. Yes, test suites are not fun and not cool and pretty much double your work, but there is reason that the professionals write them parallel to the main code. I notice that new languages such as Clojure automatically set up the infrastructure for testing, which is a Good Thing.

Best of luck!


Top
 Profile  
Reply with quote  
PostPosted: Tue Sep 22, 2015 9:05 am 
Offline
User avatar

Joined: Tue Nov 16, 2010 8:00 am
Posts: 2353
Location: Gouda, The Netherlands
There's a good test suite here: https://github.com/Klaus2m5/6502_65C02_functional_tests which was discussed in this thread: viewtopic.php?f=2&t=2241


Top
 Profile  
Reply with quote  
PostPosted: Wed Sep 23, 2015 5:15 am 
Offline

Joined: Tue Jul 24, 2012 2:27 am
Posts: 679
Now that I have a bit more time, I can give a more comprehensive response.

First off, what are your goals? Speed, accuracy, and code presentation/maintainability are generally at odds with each other here. If you want a very simple system that just runs BASIC with the screen editor in text mode, that's a lot easier than trying to implement all the normal graphics modes, raster effects, run copy-protected games at full speed, etc. If you want to showcase your code readably, not just the runtime behavior, well... optimized code can be messy. ;)

Quote:
- Does this make any sense? I doubt it somehow, but I fail to see how I should do it :(
It certainly does make sense, and would work for many simple cases.

Quote:
- If yes - is the precision enough to ever see some result with the SID or VIC?
Yes, if you write $d021 to change the background color to leave it at that new color, it doesn't really matter at which cycle it changed. The screen will stay at the new color, and that's what you want. The same applies to the SID; a few cycles shifted here or there won't cause any audible problems in regular use. Only advanced trickery used in games and demos would be problematic, though some simpler tricks like raster interrupts might jitter a lot more than the canonical case.

Quote:
- Probably I have to execute this Loop 1mio times a second, to get the 0.98 MHz of the 6502, right?
Not exactly. If the loop executes one instruction per iteration, and the average cycles taken per instruction is around 3, then that's ~330k iterations per second to keep up with realtime.

Quote:
About topic 2: If anybody has the time and nerves...I would be so thankful to get some "dummy-proof" Information about how the SID/VIC communicate with the CPU. Is it with Interrupts? All the Information I find in the Internet are way too complex for me to understand :/
It is a hardware topic, which can get quite complex if it's your introduction to such. I would suggest reading the 64doc and vic-ii articles linked above over and over as you understand more small pieces.

The VIC-II and SID (and CIAs, and RAM chips, etc) are mapped into the CPU's address space, meaning normal CPU reads & writes to addresses command those chips. Writes from the CPU to address $0103 will write to the RAM chips, while writes to to $d021 don't hit the RAM chips at all (in the standard configuration), they write to the VIC-II chip. Each major chip on the bus has a "Chip Select" pin which is activated when the read/write targets it. The PLA (configured by memory locations $00/01) determines which address should activate which chip. That activated chip then sees the address, whether it's a read or write, and if it's a write it also sees the data that the CPU put on the data bus lines.



A detailed example of what the hardware is doing:

CPU writes the value $0e to address $d021, by doing the following:
- CPU drives the 16-bit address bus to hold the value $d021
- CPU drives the 8-bit data bus to hold the value $0e
- CPU sets the R/W line to indicate Write

On every cycle, the PLA is also doing its thing to manage the bus:
- PLA examines the high bits of the address, the R/W line, and the current memory configuration (bits from the 6510's value at address $01 are hardwired to inputs on the PLA).
- PLA determines that this address in this configuration is addressing the VIC-II chip.
- PLA activates the CS (Chip Select) pin of the VIC-II chip for this clock cycle (and not the CS of any other chip, including the RAM chips).

The VIC-II chip notices an incoming write, since it was selected. Being wired to the bus, it directly reads the values of the address lines (=$d021) and the data lines (=$0e)
- The VIC-II puts this value of $0e into an on-chip register as indicated by the low-bits of the address, $21.
- Separately, as the VIC-II is drawing the screen and needs to render the background color, it refers to the current value in this internal register.


Reads are similar, except that the CPU indicates "Read" on the R/W line, and does not assert anything on the data bus. When one of the chips is selected while the R/W line indicates a Read, it will be the one to assert the data bus lines to the value that the CPU will then pick up.


Now, inside an emulator, all reads & writes need to figure out if they go to RAM or to a special chip. I believe these are all of them:
  • SID
  • VIC-II
  • CIA1
  • CIA2
  • Character ROM
  • BASIC ROM
  • Kernal ROM
  • Color RAM
  • $00/01 data port inside the 6510

Reads from ROMs, and reads/writes against RAM are easy to do; just read/write a byte from an array representing the RAM or ROM.

Reading from special chips is often also a simple lookup, though some trigger behavior, like clearing an interrupt when a special address is read. When writing to many of the special chips, you can simply store a value in a register (like the VIC-II's background color). Then, when the VIC-II code runs, it can read that to draw colors; the act of writing that register doesn't have to perform any special graphics operations in the simple case.

As mentioned above, it's reasonable to have separate functions per chip to perform reads or writes, to contain any special behaviors.


I know the C64 pretty well, not only the 6502 side of things, so feel free to ask anything.

_________________
WFDis Interactive 6502 Disassembler
AcheronVM: A Reconfigurable 16-bit Virtual CPU for the 6502 Microprocessor


Top
 Profile  
Reply with quote  
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 8 posts ] 

All times are UTC


Who is online

Users browsing this forum: No registered users and 4 guests


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Jump to:  
cron