Close
0%
0%

Reimplementing a 1 bit CPU

Learning the MC14500B Industrial Control Unit

Similar projects worth following
I've always been fascinated by the concept of a 1 bit CPU, in particular the Motorola MC14500B. I'm going to reimpment this in sofware and maybe hardware.

Background photo of MC14500B processor from Wikipedia. By JPL - Own work, CC BY-SA 4.0, https://commons.wikimedia.org/w/index.php?curid=48560865

This is a joke, right?

Not at all. 1 bit processors have existed for a long time.

But with 1 bit you get two instructions, what can you do with that?

The data path is 1 bit, but the instruction width is wider. With 4 bits you get 16 instructions. When combined with an address operand, the program store width can be 4, 8, or other numbers. As you will see the design is very flexible.

But then you can only operate on 1 bit of data, surely that's not very useful?

With addressing you can have multiple locations for the data, including multiple input and output ports, and scratchpad locations.

What is it good for?

It's typically used for control applications. It reads 1 bit inputs, uses boolean operators to make decisions based on those in conjunction with stored state, and writes 1 bit outputs. An example might be a traffic light controller for an intersection.

Why not use a microcontroller?

Indeed one would use one these days. However back in its day, this offered a design paradigm based on Ladder Logic. Another advantage was the wide range of supply voltage possible in the CMOS implementation which gave large noise immunity.

So you designed this?

I seldom do anything original. This is based on the Motorola MC14500B Industrial Controller Unit chip. The best reference for this controller is the 1977 handbook among several locations at archive.org

On Hackaday @agp.cooper has already done projects on 1 bit CPUs including an examination of the MC14500B, and there is a WDR 1 bit computer project based on this chip so I'm treading a well-worn path.

In the links section you will find pointers to resources. Naturally there are assemblers, simulators, and even replicas implemented in FPGAs. There was a reimplementation as recent as 2019.

If you find any more resources of interest do tell me and I'll add a link if it's worthy.

You can still buy MC14500B chips on eBay but I don't see the point of getting something you could emulate in a 10¢ MCU, unless one is a collector, or you must have an exact workalike for existing equipment.

At this point I revert to standard exposition mode.

Goal

To understand this unique architecture. Steps:

  1. Simulate a MC14500B system in software
  2. Design a discrete workalike in Logisim
  3. If feasible and I still feel like it, implement with TTL chips from my junk box

Note that there may be cycles of this for version 1, 2, etc.

Architecture

Unlike microcontrollers, the MC14500B is not a complete controller. It doesn't even have a program counter on-chip, that has to be implemented externally. Think of it as the nucleus of a controller. That's why it's labelled an ICU, which stands for Industrial Control Unit, not the ICU in a hospital which I hope you never have to visit. It runs relatively slowly, at up to 1MHz but this is sufficient for the uses it's put to.

Note that my goal is to simulate a system, not the chip. By not making a workalike for the MC14500B, but a typical system, the design can be less constrained because some design choices have been set. Some less used instructions such as JMP, RTN and SKPZ could be omitted in the design.

One design choice is the program store (PS). The handbook talks about different design choices for the PS width. You can have 4 bit interleaved instructions and operands (with the clever use of the 2 phase clock for the LSB). Since most memory widths are multiples of 8 bits these days, a 8 bit PS width would be more natural. However this limits the operands to 4 bits with which you can address only 16 input and 16 output ports. Some of those ports are given over to scratchpad locations so typically 8 ports input and output, and 8 scratchpad locations. An expansion would use another byte to expand the space to 4096 locations. That's not hard to do in software, but I'm not sure I want to build hardware implementations, we'll see.

Version 1 simulation results

The logs detail the process and results of simulating a candidate implementation...

Read more »

v1-circ.zip

Logisim .circ files for version 1 simulated hardware

Zip Archive - 12.61 kB - 08/23/2021 at 13:39

Download

  • Version 1 putting the modules together

    Ken Yap08/23/2021 at 09:54 0 comments

    First I had to correct errors in the modules. Then I needed a NOR gate to combine the reset button with the NOPF reset signal. In addition I have attached a clock generator, a hex instruction address display, input and output pins for the test program, and a LED on output 7.

    The program simulated was the one used in the Arduino emulator:

    LD 1; AND 2; STO 3; STOC 7; NOPF;

    and letting the simulation free-run showed it looping through the instructions, and lighting the LED if  I1 AND I2 is FALSE; STOC 7 effectively makes the LED show a NAND.

    I'll test it a bit more before posting the Logisim files, and decide what the next stage, if any, should be.

  • Version 1 preparing to put the modules together

    Ken Yap08/18/2021 at 04:06 0 comments

    I'm going to split this into two logs. At first I thought I could write one log with the modules connected together and the results of testing, but it turns out that I have to go back and fix various infelicities in the modules due to my learning to use Logisim. As I don't want to continually revise logs like some authors do, making it difficult for readers to see what's changed, I will describe the preparation now. I also have to go back and rewrite history, er module logs, to fix up the sub-circuits.

    I need to label all the external connections of each module, not with text but with pin labels, otherwise you cannot see from outside each pin's function. Also the pin label field has the hint HDL compatible on a yellow background. What this means is only certain characters are valid. I cannot use ~ or / to indicate inverted logic, so I have to write it like nRST.

    To combine modules, the Merge action is used. However this reads in the module once rather than linking to it. So if the module changes, the sub-circuit has to be deleted and reread. When merging, module names need to be unique, or existing modules will be overwritten. So instead of letting the module name default to main I gave each one a unique name.

    So for now here are the 4 modules read into one project:

  • Version 1 Logisim of Input Output

    Ken Yap08/12/2021 at 01:19 0 comments

    There are 16 locations addressed by the address nybble of the 8-bit code word. We allocate the bottom 8 locations to I/O, and the top 8 locations to scratchpad memory which is used for temporary booleans. Input 0 is special, it's the Result Register. Being able to read back the RR is required for most configurations. Here is the Logisim circuit:

    As expected, A3 is used to disambiguate between I/O and scratchpad locations.

    A couple of notes: The demultiplexer and output register could be combined in one chip so the 8-bit bus between the two is intrachip. The 2 to 1 selector between the multiplexer and the RAM could be dispensed with if both of them support tristate outputs switched by A3 of course. Both these points highlight that often Logisim doesn't have the parts that exist in the real world, although they can be synthesised by existing gates. Or one could write a Java implementation of a chip, but that is more work. We shall return to this point.

  • Version 1 Logisim of Program Store

    Ken Yap08/09/2021 at 00:22 0 comments

    Recall that the MC14500B does not have a program counter so it has to be implemented external to the chip. So the program store is straightforward, an 8-bit counter coupled to an 8-bit wide ROM.

    The counter in Logisim has many features but we use only some of them.

    The high nybble of the code byte is the operation and the low nybble is the address of the port or scratchpad memory.

  • Version 1 Logisim of Control Unit

    Ken Yap08/07/2021 at 12:31 0 comments

    Now we complete the instruction processing by dealing with the upper 8 instructions, the ones that have the high bit on, comprising STO, STOC, IEN, OEN, JMP, RTN, SKPZ, and NOPF. In this version we don't implement JMP, RTN and SKPZ. Here is the Logisim design for the rest.

    We use a 3 to 8 decoder/demultiplexer to enable one of 8 outputs when that particular instruction is selected. The D line is used to enable decoding for this group of 8. Unlike the TTL design I'm following, I intend to use an active high out decoder, the 74LS259, instead of the original active low out 74LS138. So the Write line is enabled using an OR gate whenever STO or STOC is executed. STO or STOC are also used in the Logic Unit to invert Do.

    IEN and OEN are inverted and used to clock in the input data. The inversion means that the Di is clocked in at the falling edge of the IEN or OEN lines depending, meaning at the beginning of the next instruction. I was uncomfortable with using IEN and OEN to clock the D flip-flops this way but this is in fact how it's represented in the block diagram of the innards of the MC14500B. Note that the flip-flops are reset to IEN and OEN high with nRST.

    Commentary on more use of Logisim: It's fairly easy to verify combinatorial circuit behaviour, but sequential circuits take more effort, because of the extra dimension of time obviously. It would be nice to have models for various TTL chips, but only a handful are supplied in the standard package. So the generic parts such as decoder, D flip-flop, etc. may differ from the actual part you hope to use. For example, in the diagram above the R and S lines of the flip-flops are active high but in the actual design, they will be active low. Hence the inverter for the nRST line.

  • Version 1 Logisim of Logical Unit

    Ken Yap08/05/2021 at 10:52 0 comments

    First thing is to see what has been done before. In the project #One Bit CPUs a previous design in TTL is mentioned and the diagram is here.

    It makes sense to elaborate the design modularly, so let's first look at instruction processing. You'll notice that the 16 instructions fall into two groups when you exclude the NOPs at 0 and F. The first 7 are load and logical operations. while the next 7 are store and control operations. The difference is that only the former affect the result register (RR). So I'll adopt the design and create a logic unit LU (no arithmetic, so not ALU). This is done using gates and an 8 input multiplexer feeding a D flip-flop which is the RR.

    In order from 0, the operations are NOPO, LD, LDC, AND, ANDC, OR, ORC and XNOR. The first is just the identity transform, so RR out is looped back to RR in. At this point I have to gripe about the ordering of the operations as designed. LD is 1 and LDC is 2. It would have been nice if XNOR had been moved to 1, then LD and LDC would be 2 and 3 respectively, thus differing in only one bit. Similarly for AND and OR. Fortunately this doesn't complicate the circuit because of the multiplexer, we just use A to invert the Di as necessary.

    The instruction line D is used to force the operation to NOPO using 3 AND gates so that the RR is unchanged by the top 8 instructions. In the original design this was done by futzing with the clock signal. I believe this is a mistake because it can lead to glitches.

    If you have been following the project, you may notice that the circuit has been updated to be modular, see this log.

  • Version 1 emulator

    Ken Yap08/01/2021 at 02:10 3 comments

    This is the block diagram of the system that is emulated. Not shown are the scratchpad locations for memory, and the FLGF reset line, see description below.

    It was relatively easy to write an emulator for the Arduino.

    /*
            MC14500B Industrial Control Unit system emulator Version 1
    */
    
    #define DEBUG
    #define HALFPERIOD  100 // sets speed of emulation
    
    // All 16 instructions
    #define NOPO  0x0
    #define LD    0x1
    #define LDC   0x2
    #define AND   0x3
    #define ANDC  0x4
    #define OR    0x5
    #define ORC   0x6
    #define XNOR  0x7
    #define STO   0x8
    #define STOC  0x9
    #define IEN   0xA
    #define OEN   0xB
    #define JMP   0xC
    #define RTN   0xD
    #define SKPZ  0xE
    #define NOPF  0xF
    
    struct icu {
      byte  counter;        // current instruction
      byte  clock;          // code executed on low phase
      byte  rr;                 // access to I/O address 0 goes here
      byte  flg0;
      byte  flgf;
      byte  ien;
      byte  oen;
      byte  jmp;
      byte  rtn;
      byte  skpz;
    } icu;
    
    const byte code[256] = {
    #include "code.h"
    };
    byte rwmem[8];
    const byte iportmap[8] = { 0, 2, 3, 4, 5, 6, 7, 8 };
    const byte oportmap[8] = { A0, A1, A2, A3, A4, A5, 9, 13 /*LED*/ };
    
    byte read(byte addr) {
      if (icu.ien == 0)
        return 0;
      byte port = addr & 0x7;
      return addr & 0x8 ? rwmem[port] : (port == 0 ? icu.rr : digitalRead(iportmap[port]));
    }
    
    void write(byte addr, byte data) {
      if (icu.oen == 0)
        return;
      byte port = addr & 0x7;
      if (addr & 0x8)
        rwmem[port] = data;
      else
        digitalWrite(oportmap[port], data);
    }
    
    void setup() {
      // put your setup code here, to run once:
    #ifdef DEBUG
      Serial.begin(115200);
    #endif
      memset(&icu, 0, sizeof(icu));
      icu.ien = icu.oen = 1;
      for (byte i = 1; i < sizeof(iportmap) / sizeof(iportmap[0]); i++)
        pinMode(iportmap[i], INPUT_PULLUP);   // means unconnected pins will be 1
      for (byte i = 1; i < sizeof(oportmap) / sizeof(oportmap[0]); i++)
        pinMode(oportmap[i], OUTPUT);
    }
    
    void loop() {
      // put your main code here, to run repeatedly:
      for (icu.counter = 0; ; icu.counter++) {
    
        icu.clock = 1;              // high phase, fetch
        byte c = code[icu.counter];
        byte instr = c >> 4;
        byte addr = c & 0xF;
    
    #ifdef DEBUG
        Serial.print(icu.counter, HEX);
        Serial.print(" ");
        Serial.print(c, HEX);
        Serial.print(" ");
        Serial.println(icu.rr, HEX);
    #endif
    
        delay(HALFPERIOD);
    
        icu.clock = 0;              // low phase, execute
    
        // on flgf jump to beginning of program
        if (icu.flgf) {
          icu.flgf = 0;
          icu.counter = 255;
          continue;
        }
        icu.flg0 = icu.flgf = 0;    // reset flags
    
        // todo: deal with jmp and rtn
        if (icu.skpz) {             // skip instruction
          icu.skpz = 0;
          continue;
        }
    
        icu.jmp = icu.rtn = icu.skpz = 0;   // reset control flags
    
        switch (instr) {
          case NOPO:
            icu.flg0 = 1;           // currently no-op
            break;
          case LD:
            icu.rr = read(addr);
            break;
          case LDC:
            icu.rr = !read(addr);
            break;
          case AND:
            icu.rr &= read(addr);
            break;
          case ANDC:
            icu.rr &= !read(addr);
            break;
          case OR:
            icu.rr |= read(addr);
            break;
          case ORC:
            icu.rr |= !read(addr);
            break;
          case XNOR:
            icu.rr ^= !read(addr);
            break;
          case STO:
            write(addr, icu.rr);
            break;
          case STOC:
            write(addr, !icu.rr);
            break;
          case IEN:
            icu.ien = icu.rr;
            break;
          case OEN:
            icu.oen = icu.rr;
            break;
          case JMP:
            icu.jmp = 1;
            break;
          case RTN:
            icu.rtn = 1;
            break;
          case SKPZ:
            icu.skpz = 1;
            break;
          case NOPF:
            icu.flgf = 1;
            break;
        }
        delay(HALFPERIOD);
      }
    }

    And the first test program is:

    // Test program: LD 1; AND 2; STO 3; STOC 7; NOPF;
    // Input pins are pulled high by default so
    // grounding either pin D2 or D3 should turn on LED
    0x11, 0x32, 0x83, 0x97, 0xF0 

    Relevant points about this design

    The program storage (PS) is 256 bytes so the program counter is 8 bits wide. Each instruction and operand fits in a byte, the 4 MSBs are the instruction and the 4 LSBs are the address. As mentioned before, the program counter is not on-chip, it has to be provided externally. It is incremented by the clock signal. In the high phase, the code nybble is read in and in the low phase, it is executed, making use of the address nybble if necessary. The clock variable is only written to and never read so it's redundant, but I find it useful...

    Read more »

View all 7 project logs

Enjoy this project?

Share

Discussions

agp.cooper wrote 09/11/2021 at 02:59 point

Hi Ken,

Which version of LogiSim are you using? I has just started using LogiSim for my CPUs.

I have looked at the LogiSim 2.7.1, LogiSim 2.7.10 and LogiSim Evolution.

Regards Alan X

  Are you sure? yes | no

Ken Yap wrote 09/11/2021 at 03:25 point

Evolution

  Are you sure? yes | no

jim.brakefield wrote 09/10/2021 at 16:28 point

For low speed, logic emulation or simulation takes less space on an FPGA than the discrete LUT implementation (assuming one has a spare block RAM available).  Thus motivated, did https://opencores.org/projects/lem1_9min.  The four-bit versions support binary or BCD.  Low speed means a 40KHz update rate.

  Are you sure? yes | no

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates