Close

Just a Matter Of Software (JMOS technology)

A project log for 1978 "Heathkit" D&D Digital Dice Tower

Resto-Mod Dungeons and Dragons Digital Dice Tower based on a 1975 Bell & Howell IMD-202 Digital Multimeter

John AndersonJohn Anderson 01/22/2020 at 01:582 Comments

The last piece required to make all this old and new hardware useful is the software. The software for this application simply collects the input from the switches/buttons, determines the user's intent, and displays appropriate values on the nixie displays. The source for this application can be found in the main.c file attached to this project.

For code development, image loads, and debugging, I use Atmel Studio 7.0 running on an old Windows 7 laptop connected to an Atmel ICE via USB. I am very impressed with Atmel Studio's IDE, project management, editor, and JTAG interface. I haven't written code in 15-20 years and I learned how to use it to create projects, edit code, manage files in a project, load images to microcontroller flash, and debug (breakpoints, single step, set watch tables, etc) in no time flat without reading through loads of documentation. This is by far the best free software that I have ever used. My one ding on it is the lack of Linux support. But, I can live without that.

I'm also very impressed with the Atmel-ICE. I paid $90 for the basic kit and I got a full featured JTAG programmer/debugger that integrates with completely with Atmel Studio. It supports both AVR (ATtiny, ATmega, and ATxmega) and SAM ARM microcontrollers. And, it supports ISP, AVR JTAG, DebugWire, PDI, and SAM ARM JTAG. That's enough for years of projects I have in my queue. BTW, I have used a couple of the cheap AVR/Auduino USB ISP and JTAG programmers and they range from ok to non-functional. None of them provide the wealth of device/interface support the Atmel-ICE does. Or at least, I can't figure out if they do or not. Documentation is always sketchy at best.

The application program can be broken down into 3 parts; hardware initialization, hardware interface, and application state machine.

First is the hardware initialization. This is a single function that is called once in main() at start up. It sets up the I/O pins for the functions used by this application. In this case, port B pins 0-7 are connected to the 2 digit thumbwheel input, port C pins 0-2 are connected to the three outputs from the 74HC148 encoding the rotary switch setting, Port C pin 7 is connected to the momentary contact button, and Port D pins 0-7 are connected to the two 7441's driving the nixie displays. So the data direction registers and pull ups (enabled on all digital inputs) are configured as necessary.

// Setup the IO pins **********************************************************
void initIO()
{
    // Set Port B pins to input
    DDRB = 0b00000000;
    // Enable pull ups on port B
    PORTB = 0b11111111;
    
    // Set Port C pins to input
    DDRC = 0b00000000;
    // Enable pull ups on Port C
    PORTC = 0b11111111;

    // Set Port D pins to output
    DDRD = 0xff;
    PCICR = 0x00;
    PCMSK2 = 0x00;
    EIMSK = 0x00;
    
}

Note, there is some extra setup of the PCICR, PCMSK2, and EIMSK config registers. These represent the alternative functions for the port D pins. The code is setting them to their default cold boot values. So, they aren't necessary. They were added while debugging what turned out to be an unrelated problem. I never bothered to take them out.

The next part is the hardware interface functions. These functions read the status of the switches/button and output values to the nixie tubes.

The first function outputs the provided integer value to the two digit nixie display connected to Port D.

// Output a two digit decimal number on the Nixie Tube display ****************
void output(int out)
{
    int outputByte = out%10;
    out /= 10;
    outputByte |= (out%10)<<4;
    
    PORTD = outputByte;
}

The next function returns the current setting of the thumbwheel switch as an integer value.

// Get the dice count from the thumbwheel input *******************************
int getCount()
{
    uint8_t rawCount = ~PINB;
    
    // Bad wiring on both the thumbwheel and the board
    int count = (rawCount&0b00000011) | ((rawCount&0b00010000)>>2) | ((rawCount&0b00000100)<<1);
    count += (((rawCount&0b00100000)>>5) | ((rawCount&0b00001000)>>2) | ((rawCount&0b11000000)>>4)) * 10;
    
    return(count);
}

Note: There is a lot of bit twiddling going on here. I managed to accidentally flip two wires soldered to the thumbwheel switch and when I tried to "un-flip" them when I soldered the connector on the controller board I messed it up even more. So I fixed the hardware problem in code. Because that's yet another thing embedded software programmers do. They fix hardware problems :-)

Also, the value read from the port register is bit inverted. That's because the switch is wired with a common ground and each pin has a pull up enabled. The BCD encoding in the switch shorts the associated pins to common. So, for example, the values 1 and 2 on the thumbwheel switch will look like 0xec at the PINB register. Bit inverted that becomes 0x12.

The next function returns the selected dice type as an integer value representing the number of sides.

// Get the dice type **********************************************************
int getDice()
{
    uint8_t rawDice = (~PINC)&0x07;
    int     dice;
    
    switch(rawDice)
    {
        case 1:
            dice = 20;
            break;
        case 2:
            dice = 12;
            break;
        case 3:
            dice = 10;
            break;
        case 4:
            dice = 8;
            break;
        case 5:
            dice = 6;
            break;
        case 6:
            dice = 4;
            break;
        case 7:
            dice = 2;
            break;
        case 8:
            dice = 99;
            break;
        default:
            dice = 100;
    }
    return(dice);
}

The last hardware interface function returns the status of the push button. The status is returned as an integer that is 1 if the button is pressed and 0 if the button is not currently pressed.

// Get the status of the roll button ******************************************
uint8_t getButton()
{
    uint8_t ret = ((~PINC)&0b10000000)>>7;
    return(ret);
}

 With these hardware functions in place, it's just a matter of creating a simple state machine that updates the nixie displays relative to the inputs from the user. That code resides completely in the main() function. The state machine implemented as in a switch statement with each case representing a state. It's simple and relatively easy to read for the a state machine this small.

// Program entry point ********************************************************
int main(void)
{
    unsigned int seed = 0, seeded = 0;
    int          i, count = 0, dice = 0, result = 0, timer = 0;
    State        state = State_Init;
    
    initIO();
    
    // Forever
    while (1) 
    {
    // Increment the seend counter
    ++seed;
        
    // Implement the states of the application
    switch(state)
    {
        // Initialization
        case State_Init:
                count = getCount();
            dice = getDice();
            result = 0;
            timer = 0;
            state = State_Idle;
            break;
        // Idle
        case State_Idle:
            output(result);
            if(getDice() != dice)
            {
                timer = DISPLAY_BLANK_TIME;
            state = State_Update_Dice;
            }
            else if(getCount() != count)
            {
            timer = DISPLAY_BLANK_TIME;
            state = State_Update_Count;
            }
            else if(getButton())
            {
            timer = 10;
            state = State_Roll;
            }
            break;
        // Set the dice type to be rolled
        case State_Update_Dice:
            output(BLANK_OUTPUT);
            if(!--timer)
            {
            dice = getDice();
            timer = DISPLAY_CONFIG_TIME;
            result = 0;
            state = State_Display_Dice;
            }
            break;
        // Momentarily display the dice type
        case State_Display_Dice:
            if(dice == 100)
            output(99);
            else
            output(dice);
            if(getDice() != dice)
            {
            timer = DISPLAY_BLANK_TIME;
            state = State_Update_Dice;
            }
            else if(!--timer)
            state = State_Idle;
            break;
        // Set the number of dice to be rolled
        case State_Update_Count:
            output(BLANK_OUTPUT);
            if(!--timer)
            {
            count = getCount();
            timer = DISPLAY_CONFIG_TIME;
            result = 0;
            state = State_Display_Count;
            }
            break;
        // Momentarily display the dice count
        case State_Display_Count:
            output(count);
            if(getCount() != count)
            {
            timer = DISPLAY_BLANK_TIME;
            state = State_Update_Count;
            }
            else if(!--timer)
            state = State_Idle;
            break;
        // Simulate a roll by flashing random values of the dice type on the display
        case State_Roll:
            output((rand()%dice)+1);
            if(!--timer)
            state = State_Roll_Result;
            break;
        // Calculate and display the roll result
        case State_Roll_Result:
            // If not seeded...
            if(!seeded)
            {
            // Seed the rand() function with the seed counter
            // that has been incrementing since startup
            srand(seed);
            seeded = 1;
            }
            result = 0;
            for(i=0;i<count;++i)
            result+=(rand()%dice)+1;
            state = State_Idle;
            break;
        }
        // Insert delay based on the current state
        // Note: if init or idle state, no delay inserted and 
        // The seed counter increments as fast as this loop spins.
        if(state==State_Roll)
            delay_ms(100);
        else if(state>State_Idle)
            delay_ms(1);
    }
}

I don't have a state diagram to share. But it's simple enough to read through in source code.

Basically this state machine sits in the idle state displaying the last result value on the nixie tubes. If it detects a new value on the thumbwheel or rotary switch it sets a timer count and goes to the appropriate "update" state for a 100 milliseconds, displays 00, updates the value, and then goes to the "display" state. In the "display" state it displays the recently selected value (count or type) on the nixies for 500 milliseconds, and returns to idle. Back in idle, if it detects the roll button pressed it will set the timer counter and go to the roll state. In the roll state, it flashes 10 values calculated using the rand() function and the type of dice selected. Each of these values are displayed for 100 milliseconds. After 10 values are flashed, it goes to the roll result state. In the result state it checks if the rand() function has been seeded. If not, it seeds the rand() function using the seed counter that has been incrementing since start up. The rand() function is only seeded once. After that, the result state calculates the die roll result using the dice type and count to calculate the result. The result is stored and the system returns to the idle state where the result value is displayed until the next user input.

Discussions

Ken Yap wrote 01/22/2020 at 02:49 point

Your getDice() function could simply be a lookup of an 8-element array. Since you AND the returned bits with 0x7, the value is guaranteed to be between 0 and 7 inclusive, and will never be 8. The default would be 0 because 1-7 are already accounted for.

BTW I like delicious AMOS (Always a Matter of Software) cookie technology. Caffeine and sugar, yes!

  Are you sure? yes | no

John Anderson wrote 01/22/2020 at 15:33 point

That's a good point. It would be much more concise. I'll fix that in the next version of the code. 

I've already started the next iteration of this dice tower. This version will get rid of the riveted faceplate, wire up the control board and original PCB connection differently, use a different thumbwheel switch with stops, different microcontroller, and add sound/speech. Given all the differences, I'll create it as a new project.

  Are you sure? yes | no