Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to create Bi-Directional IO in a JavaComponent #3

Closed
shawty opened this issue Feb 12, 2019 · 20 comments
Closed

How to create Bi-Directional IO in a JavaComponent #3

shawty opened this issue Feb 12, 2019 · 20 comments
Labels

Comments

@shawty
Copy link

shawty commented Feb 12, 2019

First off Great Project @hneemann I loved Logisim, and used to use it extensively for helping design arduino and PicMicro circuits.

I'm porting all of my Custom Logisim Components over to Digital, and the one I'm on with at the moment (A rockwell 6522 VIA) requires me to have a 2 way data bus, controlled from a r/w line on the device.

From what I can see so far, once an input is set as an input, and an output is set as an output, they can't be changed.

I understand the mathod of connecting two sets of pins together in a circuit based component, but I can't see a clean way to perform the same thing in a Java Component.

Is this by design, something your planning on addressing, or just not possible?

Do you have any advice on how to do this?

Shawty

PS: I'm really pleased with your simplification of the Java Component base class, it's wonderfull compared to the way Logisim did things.

@hneemann
Copy link
Owner

When you create the ObservableValue used for the output, you can call the method setBidirectional() to make the output bidirectional.
Once done so, you get an additional ObservableValue instance when setInputs(ObservableValues inputs) is called. Now use the output you have created to set the output pin and use the additional input you got to read back the value.
Remember to set the output to high Z by calling setToHighZ() on that output to allow using the pin as an input.

I'm really pleased with your simplification of the Java Component base class, it's wonderfull compared to the way Logisim did things.

Thank you very much! :-)

@shawty
Copy link
Author

shawty commented Feb 12, 2019

Fantastic. Thanks I'll give that a try tonight and let you know how I get on.

I'm considering donating my components to the project once I finish converting them, would you like to include them?

For reference (I may not convert them all) but to date I have:

6502 CPU

6522 Via

Simple Edge triggered counter/register

Switchable register (IE: DUal load, rapid switching between to preset values)

Real Time Clock (Pulse it's trigger and it reads and returns the PC's real time hour/minute/seconds value on it outputs.

Pixel based 320x256 24bit bitmapped display

4x4 Keypad, with high pulse on key press and hex nibble of key on 4 bit output

Binary file writer

Binary file reader

@shawty
Copy link
Author

shawty commented Feb 13, 2019

Well I now have a two way data bus, and it works great :-)

Now I understand how it works, I'm going to wire up all the other parts, port, control lines etc.

This should also allow me to simplify the 6502 CPU Too, doing bi-directional in Logisim was big pain, in Digital it's very easy.

I think though, the project might benefit fro having a wiki here on the custom component project, that documents these things, sort of like a mini SDK documentation.

@hneemann
Copy link
Owner

If you are implementing/porting many components the most important rules are:

  1. Never ever write to an output during the readInputs() call and
  2. never ever read an input during the writeOutputs() call.

If you do not follow these rules, all kinds of hard-to-find errors can occur. Unfortunately, compliance with these rules cannot be enforced. You have to remember that yourself.

@shawty
Copy link
Author

shawty commented Feb 13, 2019

Yep, picked that up quite quickly from your sample code :-)

Good first rule too, that caused all kinds of mess in Logisim.

Couple of things that did occur to me as I've been working however, just food for thought mainly.

  1. The Bi-directional pins on components are great, however, how difficult would it be to do at a bit level?

In the 6522 model I'm building, it has 2 8 bit ports on it. Iv'e currently implemented them as one pin with an 8-bit width, however, the I/O port on the device has a register that can set individual pins on the port as either an IN or an OUT.

I could easily just model it as 8 1 bit pins, but it would make for much more tidy connections if individual bit's could be set different ways at the same time on a multi bit connection.

  1. When readInputs is called, it would be nice if "Digital" passed in some kind of notification as to which input pin triggered the call.

In the 6522 for example, there are 3 input pins that are important, the clock, the R/W and the reset.

It would be good to be able to say (If call was trigger by "clock" do xx else if trigger was "rw" do yyy) instead of checking every applicable pin every time.

For the 6522 for example, if reset is pressed, then once that's done it's thing, it's not worth checking anything else, so we return, but in some cases order can be important, and reset might have to be checked last.

Both of these are just ideas however, things that would make building new components for Digital even easier :-)

I have a lot of experience building JavaSimulations for Logisim, and have built a lot of additions to it over the years, these are all lessons I learned the hard way :-)

@hneemann
Copy link
Owner

The Bi-directional pins on components are great, however, how difficult would it be to do at a bit level?

You don't need to call setToHighZ() which sets all bits to high Z. You can also call set(long value, long highZ) which allows you to set each individual bit to high Z. This way you are able to configure single bits to be an input or output.

When readInputs is called, it would be nice if "Digital" passed in some kind of notification as to which input pin triggered the call.

It's possible to register your own listener to an input value by calling addObserver(Observer observer). If your observer is called, the value has changed. But i think it's less expensive to check the values in the readInputs() method.

@hneemann
Copy link
Owner

One important thing I forgot to mention: There are no real bidirectional splitters. And that is unfortunately a point that is very difficult to fix. Therefore it may not be useful to configure single bits as output or input.

@shawty
Copy link
Author

shawty commented Feb 13, 2019

The Bi-directional pins on components are great, however, how difficult would it be to do at a bit level?

You don't need to call setToHighZ() which sets all bits to high Z. You can also call set(long value, long highZ) which allows you to set each individual bit to high Z. This way you are able to configure single bits to be an input or output.

How on earth did I miss that one :-) I've been digging into the sources of Digital like crazy, and it was right under my nose.

When readInputs is called, it would be nice if "Digital" passed in some kind of notification as to which input pin triggered the call.

It's possible to register your own listener to an input value by calling addObserver(Observer observer). If your observer is called, the value has changed. But i think it's less expensive to check the values in the readInputs() method.

Yes, I agree, for most components one would write, checking state in readInputs() is the best way to do it, in the case of the IC I'm trying to model (A rockwell 6522 Via) it works in a very "event driven" way, where the I/O ports can (and often are) driven seperately from the the rest of the operations.

The port I/O and control on the device, are often used to connect other devices to a secondary data or I/O bus, for example the BBC Model B Micro uses one to pass data to/from it's sound hardware and keyboard controller. The Port Data and Control lines can change at any time, independent of the CPU interface, so in the case of this specific case, being able to seperate and handle the pins differently from the main input routine makes coding of the device simpler.

Once again, fantastic... :-) If I was to volunteer to write these notes up for you, to go into a github Wiki how would you want me to do that?

@shawty
Copy link
Author

shawty commented Feb 13, 2019

One important thing I forgot to mention: There are no real bidirectional splitters. And that is unfortunately a point that is very difficult to fix. Therefore it may not be useful to configure single bits as output or input.

SO it's probably better for me to model the I/O pins as individual 1 bit connections then. I'll do some experimenting :-)

@shawty
Copy link
Author

shawty commented Feb 13, 2019

I'll close this issue if it's ok with you. You answered things very fast and efficiently in the first couple of answers :-)

@hneemann
Copy link
Owner

Yes, I agree, for most components one would write, checking state in readInputs() is the best way to do it, in the case of the IC I'm trying to model (A rockwell 6522 Via) it works in a very "event driven" way, where the I/O ports can (and often are) driven seperately from the the rest of the operations.

In this case there is a more elegant solution: The base classes which are created if a model is generated is a Element which is a interface. This interface has a method registerNodes(Model model). And a Node is the piece of code which implements the behavior of the simulation. In almost all cases a component inherits from Node and implements the Element. But that is not required.
It is also possible to implement the Element in a way that it creates several Node classes and registers them to the model. In this case it is possible to implement different nodes which operate independent from each other but belonging to the same component. Maybe I should write an example which shows how to implement such a chip.

@hneemann
Copy link
Owner

hneemann commented Feb 13, 2019

Here is an example of a chip that contains three NOT gates. Each NOT gate is implemented as an isolated node that operates independently of the other nodes:

package de.neemann.digital.plugin;

import de.neemann.digital.core.*;
import de.neemann.digital.core.element.Element;
import de.neemann.digital.core.element.ElementAttributes;
import de.neemann.digital.core.element.ElementTypeDescription;
import de.neemann.digital.core.element.Keys;

import static de.neemann.digital.core.element.PinInfo.input;

public class MultiNot implements Element {

    public static final ElementTypeDescription DESCRIPTION =
            new ElementTypeDescription(MultiNot.class,
                    input("I_0"),
                    input("I_1"),
                    input("I_2"))
                    .addAttribute(Keys.BITS);

    private final int bits;
    private final ObservableValue out0;
    private final ObservableValue out1;
    private final ObservableValue out2;
    private ObservableValues inputs;

    public MultiNot(ElementAttributes attr) {
        bits = attr.getBits();
        out0 = new ObservableValue("O_0", bits);
        out1 = new ObservableValue("O_1", bits);
        out2 = new ObservableValue("O_2", bits);
    }

    @Override
    public void setInputs(ObservableValues inputs) throws NodeException {
        this.inputs = inputs;
        for (ObservableValue i : inputs)
            i.checkBits(bits, null);
    }

    @Override
    public ObservableValues getOutputs() {
        return new ObservableValues(out0, out1, out2);
    }

    @Override
    public void registerNodes(Model model) {
        model.add(new MyNotNode(inputs.get(0), out0));
        model.add(new MyNotNode(inputs.get(1), out1));
        model.add(new MyNotNode(inputs.get(2), out2));
    }

    private static class MyNotNode extends Node {
        private final ObservableValue in;
        private final ObservableValue out;
        private long value;

        private MyNotNode(ObservableValue in, ObservableValue out) {
            this.in = in.addObserverToValue(this);
            this.out = out;
        }

        @Override
        public void readInputs() {
            value = in.getValue();
        }

        @Override
        public void writeOutputs() {
            out.setValue(~value);
        }

        @Override
        public ObservableValues getOutputs() {
            return out.asList();
        }
    }
}

@shawty
Copy link
Author

shawty commented Feb 13, 2019

oh cool. More experimenting for me to do :-)

I've actually come up with a method based on your previous notes, that I'm quite pleased with and which works quite efficiently.

First I created a "PinNameObserver"

package shawty.digital.plugin;

import de.neemann.digital.core.Observer;

public class PinNameObserver implements Observer {

  private PinNameNotification pinNotificationObserver;
  private String notificationPinName = "";
  
  public PinNameObserver(PinNameNotification notificationObserver, String pinName){
    pinNotificationObserver = notificationObserver;
    notificationPinName = pinName;
  }
  
  @Override
  public void hasChanged() {
    pinNotificationObserver.triggerPinName(notificationPinName);
  }
  
}

And an interface to implement it:

package shawty.digital.plugin;

public interface PinNameNotification {
  public void triggerPinName(String name);
}

Then I implmented my component as follows:

package shawty.digital.plugin;

import de.neemann.digital.core.Node;
import de.neemann.digital.core.NodeException;
import de.neemann.digital.core.ObservableValue;
import de.neemann.digital.core.ObservableValues;
import de.neemann.digital.core.element.Element;
import de.neemann.digital.core.element.ElementAttributes;
import de.neemann.digital.core.element.ElementTypeDescription;
import static de.neemann.digital.core.element.PinInfo.input;

public class R6522Via extends Node implements Element, PinNameNotification {

  public static final ElementTypeDescription DESCRIPTION
          = new ElementTypeDescription(R6522Via.class,
                  input("clock", "1 bit - System clock, transfers from CPU to VIA take place and timers tick on positive edge."),
                  input("cs1", "1 bit - Chip select 1. IC is active when CS1 is High and CS2 is Low."),
                  input("cs2", "1 bit - Chip select 2. IC is active when CS1 is High and CS2 is Low."),
                  input("rs", "4 bit - Register select. 0 to 15 to select internal register address."),
                  input("rw", "1 bit - Read/Write control. When 0 Databus is being written to, When 1 Databus is being read from."),
                  input("reset", "1 bit - Reset VIA to power on state when a transition to 0 is detected."));
  //.addAttribute(Keys.ROTATE);

  // Input pins
  private ObservableValue clock;
  private ObservableValue cs1;   // Chip select lines
  private ObservableValue cs2;
  private ObservableValue rs;    // Register select lines
  private ObservableValue rw;    // Read/Write control
  private ObservableValue reset; // Chip reset signal

  // Output pins
  private final ObservableValue irq;

  // Input/Output pins
  private final ObservableValue dbOUT;    // System data bus
  private ObservableValue dbIN;
  private final ObservableValue portaOUT;
  private final ObservableValue portbOUT;
  private ObservableValue portbIN;
  private final ObservableValue ca1OUT;
  private final ObservableValue ca2OUT;
  private final ObservableValue cb1OUT;
  private final ObservableValue cb2OUT;

  // Other vars
  private boolean areWeBeingWrittenTo = false;
  private long[] internalRegisters = new long[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; // Initialise internal registers to all 0's
  private String triggerPinName = ""; // Set by the triggerPinName method when called from an external observable.

  // Databus output control
  private boolean writeToDatabus = false; // We set this to true when we want the dbOUT pin writing to
  private long databusOutputValue = 0;    // and this has the value to write in the bool above is true

  // I/O ports output control
  private boolean isPortAinput = false; // I/O Ports are set as all outputs by default
  private boolean isPortBinput = false;
  private boolean portAWritePending = false; // Nothing to sent to Ports output pins by default
  private boolean portBWritePending = false;
  private long portAOutputValue = 0;
  private long portBOutputValue = 0;

  public R6522Via(ElementAttributes attr) {

    irq = new ObservableValue("irq", 1).setDescription("1 bit - Interrupt signal from VIA, goes low when an interrupt needs servicing in the VIA");

    dbOUT = new ObservableValue("db", 8).setDescription("8 bits - System Data Bus I/O").setBidirectional();
    portaOUT = new ObservableValue("porta", 8).setDescription("8 bits - Input/Output for data port A");
    portbOUT = new ObservableValue("portb", 8).setDescription("8 bits - Input/Output for data port B").setBidirectional();
    ca1OUT = new ObservableValue("ca1", 1).setDescription("1 bit - Input/Output for port A control line 1");
    ca2OUT = new ObservableValue("ca2", 1).setDescription("1 bit - Input/Output for port A control line 2");
    cb1OUT = new ObservableValue("cb1", 1).setDescription("1 bit - Input/Output for port B control line 1");
    cb2OUT = new ObservableValue("cb2", 1).setDescription("1 bit - Input/Output for port B control line 2");
  }

  // Called ONCE at component startup to set up the inputs and trigger types
  @Override
  public void setInputs(ObservableValues inputs) throws NodeException {

    // Setup known static input pins
    clock = inputs.get(0);
    cs1 = inputs.get(1);
    cs2 = inputs.get(2);
    rs = inputs.get(3);
    rw = inputs.get(4);
    reset = inputs.get(5);

    // Setup input pins derived from bidirectional outputs
    dbIN = inputs.get(6); // Looking at input's in getInputs this seems to be 6????
    portbIN = inputs.get(7); // anyway we can be 100% sure of these pin numbers

    // Setup notifications for the pins we care about, for BARE digital only the addObserverToValue
    // is needed. The PinObserver is a custom class that informs THIS class of the name of the pin
    // just before the normal "readInputs()" method is called on this class.
    clock.addObserver(new PinNameObserver(this, "clock"));
    clock.addObserverToValue(this);

    rw.addObserver(new PinNameObserver(this, "rw"));
    rw.addObserverToValue(this);

    reset.addObserver(new PinNameObserver(this, "reset"));
    reset.addObserverToValue(this);

    portbIN.addObserver(new PinNameObserver(this, "portb"));
    portbIN.addObserverToValue(this);

  }

  // Called by Pin Notification Observer to inform us of the trigger pin name;
  @Override
  public void triggerPinName(String name) {
    triggerPinName = name;
  }

  // Called EVERYTIME there is a change on an observed input setup in "setInputs"
  @Override
  public void readInputs() {

    if ("".equals(triggerPinName)) {
      // This is a default start up, IE: No pin name is set, so we need to set things
      // to a known state (We will also end up here for pins that have only a default
      // "Digital" observer on and NOT a pin notification one).

      // By default R/W should be low, so we need to disable output on data bus
      if (!rw.getBool()) {
        dbOUT.setToHighZ();
      }

      return;
    }

    // If we get to here we have a trigger name, that means we where notified of
    // the pin name before this was called, so lets act on it, then reset it so
    // it's not called in error again afterwards.
    switch (triggerPinName) {
      case "clock":
        handleClock();
        break;

      case "rw":
        handleRw();
        break;

      case "reset":
        handleReset();
        break;

      case "portb":
        handlePortb();
        break;

    }

    // Reset the trigger name to prevet erronous calls
    triggerPinName = "";

  }

  private void handleClock() {
    if (clock.getBool()) {
      // Clock is high, we need to perform a clock step
      if (cs1.getBool() && !cs2.getBool()) {
        // Chip is selected, so register operations can take place
        long registerNumber = rs.getValue();
        long dbVal = 0;
        if (areWeBeingWrittenTo) {
          dbVal = dbIN.getValue();
          writeToInternalRegister((int) registerNumber, dbVal);
        } else {
          // WE DONT Write outputs in readInputs we flag them for later
          writeToDatabus = true;
          databusOutputValue = internalRegisters[(int) registerNumber];
        }
      }
    }
  }

  private void handleRw() {
    if (rw.getBool()) {
      // If rw pin is high, system is reading from us so we are writing
      areWeBeingWrittenTo = false;
      System.out.println("R/W has gone high, DB set to write.");
    } else {
      // otherwise rw is low, and system is writing to us, meaning we are reading
      areWeBeingWrittenTo = true;
      dbOUT.setToHighZ(); // db needs to be in a state we can read from
      System.out.println("R/W has gone low, DB set to read.");
    }
  }

  private void handleReset() {

  }

  private void handlePortb() {

  }


// Called EVERYTIME the circuit changes to set output pin states
  @Override
  public void writeOutputs() {

    // Just setting defaults for now
    // we'll set propper values once we start to build the internal registers
    irq.setBool(true);  // No IRQ being generated
    ca1OUT.setBool(false);     // Port A control 1 at rest.
    ca2OUT.setBool(false);     // Port A control 2 at rest.
    cb1OUT.setBool(false);     // Port B control 1 at rest.
    cb2OUT.setBool(false);     // Port B control 2 at rest.

    // if we signaled a register output above, read and output
    // the register, then reset the notification
    if (writeToDatabus) {
      writeToDatabus = false;
      dbOUT.setValue(databusOutputValue);
      databusOutputValue = 0;
    }

    if (portAWritePending) {
      portAWritePending = false;
      portaOUT.setValue(portAOutputValue);
      portAOutputValue = 0;
    }

    if (portBWritePending) {
      portBWritePending = false;
      portbOUT.setValue(portBOutputValue);
      portBOutputValue = 0;
    }

  }

  // Triggers at multiple times during app lifetime to give app a list of observable outputs
  @Override
  public ObservableValues getOutputs() {
    return new ObservableValues(dbOUT, irq, portaOUT, portbOUT, ca1OUT, ca2OUT, cb1OUT, cb2OUT);
  }

}

NOTE: This is still work in progress, so it's not complete.

You can see however, that I now do the following for each pin I care about:

clock.addObserver(new PinNameObserver(this, "clock"));
clock.addObserverToValue(this);

Order doesn't seem to matter, but essentially what happens is that, my new PinNameObserver fires first, and that calls back to the parent class, via this method (Implemented in accordance witht he interface)

@Override
public void triggerPinName(String name) {
  triggerPinName = name;
}

This passes back the name I passed in when I added the observer, and then calls the regular readInputs() handler, which now has the ability to just either act as normal as it did previously, or actually examine the triggerPinName variable to determine exactly which pin caused the update and act accordingly.

Cheers
Shawty

@hneemann
Copy link
Owner

It is possible that two pins have changed if readInputs() is called.
Then your callback function is called twice and the first pin name is lost.
Maybe you should do something like this:

rw.addObserver(() -> rwHasChanged = true);
clock.addObserver(() -> clockHasChanged = true);
reset.addObserver(() -> resetHasChanged = true);
portbIN.addObserver(() -> portbINHasChanged = true);

Using one boolean flag for each input you care about.

@shawty
Copy link
Author

shawty commented Feb 14, 2019

oh that looks neater. :-)

More wood added to the fire, and yes totally get your point about being called twice, just finished debugging EXACTLY that scenario.

Here's the current state of play, with what I'm attempting.

Component Sources are in one Zip, and test Dig file in the other attached to excercise it.

(I'm working in Netbeans 8.x by the way)

6522test.zip
CustomComponents.zip

And for reference, the datasheet I'm working from can be found here: http://archive.6502.org/datasheets/mos_6522_preliminary_nov_1977.pdf and the rockwell version here: http://archive.6502.org/datasheets/rockwell_r6522_via.pdf

(I'm mostly working from the rockwell version of the chip)

To test:
setting RS=0, RW=1, And toggling the clock line positive, should read from PortB. Set the value, by changing PortB input, and RS=0, RW=1 should make it appear on the databus.

setting RS=2, Databus=255, RW=0 and toggling the clock line to 1, should write 255 into the datadirection register thus setting PortB as an output port (Make sure you change the dip switch first), once reg 2 is set, then change RS=0 and any value you then put on the databus, followed by a clock toggle to 1, should put that value on the PortB output.

Setting reg2 (RS=2) back to 0 will switch portb back to input mode (Remember to change this to input, before you change the dipswitch on portb's wires, otherwise you'll end up in an invalid state.)

You should be able to see now, from what I've done so far, why some of the inputs have to be independent, PortB when set as an input for example, has to latch it's data in register 0, ready for the processor interface to want to read it onto the databus at it's own lesiure.

I'll do some experimenting with your ideas tomorrow. :-)

@hneemann
Copy link
Owner

I have looked into your code:

This way you can use the build-in generic shape more flexible. In this case the width is set to 5:

public class ShawtysComponentSource implements ComponentSource {

    @Override
    public void registerComponents(ComponentManager manager) throws InvalidNodeException {
        manager.addComponent("shawty", RealTimeClock.DESCRIPTION,
                (attr, in, out) -> new GenericShape("RTC", in, out,
                        attr.getLabel(), true, 5));
        manager.addComponent("shawty", R6522Via.DESCRIPTION,
                (attr, in, out) -> new GenericShape("R6522Via", in, out,
                        attr.getLabel(), true, 5));
    }

    public static void main(String[] args) {
        new Main.MainBuilder()
                .setLibrary(new ElementLibrary().registerComponentSource(new ShawtysComponentSource()))
                .openLater();
    }
}

In contrast to my proposal yesterday, I think such a solution to detect a pin change is easier to understand and also faster in comparison to adding an additional observer to the value:

    final boolean thisRW = rw.getBool(); 
    if (thisRW != lastRW) {
        lastRW = thisRW;

        ...
  
    }

@shawty
Copy link
Author

shawty commented Feb 14, 2019

Great stuff, will look at implementing that today.

Yet another great addition :-) I was going to ask if there was a way just to increase the default rectangle size, but I was going to leave that until after the main dev was finished.

Once again, great work :-)

@shawty
Copy link
Author

shawty commented Feb 14, 2019

Latest version. Uses bools to detect pins now instead of extra Observable, and used the change to the component loader to increase the rectangle size.

PortA and PortB now work correctly, still all or nothing though, 0 in the DDRA/DDRB register makes all inputs, anything greater makes All Outputs, but again this is beacuse I've not really thought of a good way to handle this other than making single I/O lines.

R/W on the chip behaves as expected, and changes on the ports when they are set to inputs loads the internal registers etc.

Next on my list, is the reset pin, and the timer functionality. :-)

CustomComponents.zip
6522Test.zip

@shawty
Copy link
Author

shawty commented Feb 14, 2019

PS: Feel free to add the code for these to this Github if you wish, as a more complex example.

@shawty
Copy link
Author

shawty commented Feb 21, 2019

JUst a quck note. Sorry been no updates, Windows Update deaded my main PC, only just got the thing fixed and the OS reinstalled, now to re-inst all my software ...

@hneemann hneemann closed this as completed Jun 3, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants