[index] [home]

Tweet

Simple junkbin quad-channel I/O box



This page presents a simple box for setting/reading 4 digital channels, using switches respectively LEDs. It can be used as a quick debugging-tool.

Since there's not really much to say about this, let's dive straight in.

Electronics

The following circuit is implemented 4 times - once for each channel. The node marked Vio is connected to the 4 mm banana socket for that channel.

The side driving the channel the strongest (either the electronics in the box or an applied external signal) determines the state of the channel, visible on the LED (on = high voltage). Resistors and power-ratings are chosen so that each channel is reasonably resistant against connecting to either a hard Gnd or Vcc at all times.

The logic (a MCU to debounce buttons and to implement a minimal intelligence) is powered by about 5 VDC, either coming from an internal regulator, or externally applied when the channel-voltage is too low - e.g. 3.3 V:

(Yes, there's a diode-drop to account for, and yes, I assume the user has the mental power to do so.)

No polarity- or overvoltage-protection, so this will probably blow up one day at an inconvenient moment.

So let's build this thing already - using stripboard instead of fancy PCB, since this is really only a one-off.

Junk:

A meditative soldering-session later:

The vertical board is basically an ATtiny48 to read switches and control corresponding Vctl-voltages - see schematic.

Mechanics

I had an enclosure lying around, so why not use it now. It is a plastic box with an aluminium panel at front and back. Marking positions of holes for sockets/buttons/LEDs:

An introspective drilling-/soldering-session later:

Now, I had made a mistake (LED series-resistor much too small), and some components were initially broken, causing the regulator to become a bit hot. Of course, the wrong thing to do then is to meh it, and create a makeshift heatsink from metal parts...

...instead of fixing the actual problem. Which I did, but left the heatsink as-is. It serves as a bit of weight in an otherwise light box.

PCB mounted inside box, on a single standoff:

Final result:

All switches are essencially pushbuttons. There are 2 Gnd-terminals, a Vcc-terminal (nominal channel-voltage) and a Vlogic-input to power the logic in case Vcc is too low to be regulated into 5 VDC.

Software

Each channel-switch is debounced, to prevent contact-bounce propagating to the actual I/O-channels.

Normally, each channel-switch either persuades the I/O channel down or up using a pulled-up open-collector output, as can be seen in the schematic.

The "func" switch enables an alternate function - pressing a channel-switch with the "func" switch held down toggles polarity for that channel.

Obviously, the "func" switch can be made to do a lot of things. But that can be implemented when it is actually needed. (Behold - eXtreme Programming paradigm applied to real life.)

Software is here:

    #include <avr/io.h>
    #include <avr/interrupt.h>
    #include <avr/eeprom.h>
    #include <stdint.h>
    #include <stdbool.h>
    #include <util/delay.h>

    #include "cr.h"
    #include "util.h"



    // I/O:
    //
    //      PD0 = gate 0
    //      PC7 =  "   1
    //      PC5 =  "   2
    //      PC4 =  "   3
    //
    //      PD6 = btn 'func'
    //      PD5 =  "  0
    //      PD4 =  "  1
    //      PD3 =  "  2
    //      PD2 =  "  3



    #define BTN_TABLE             \
        /*               Name */  \
        BTN_TABLE_ENTRY( FUNC )   \
        BTN_TABLE_ENTRY( 0    )   \
        BTN_TABLE_ENTRY( 1    )   \
        BTN_TABLE_ENTRY( 2    )   \
        BTN_TABLE_ENTRY( 3    )   \


    // Create enum "BTN" with entries like "BTN_FUNC", and end-marker "NUM_BTN".

    #define BTN_TABLE_ENTRY( Name )   BTN_ ## Name,

    typedef enum
    {
        BTN_TABLE
        NUM_BTN
    }
    BTN;

    #undef BTN_TABLE_ENTRY



    static uint8_t btn_hist[ NUM_BTN ];   // 0x00 = not pressed (i.e. inverted pin-level)

    static bool btn_pol_swapped[ NUM_BTN ];

    static uint8_t btn_pressed_mask;   // bitflag is set: button is down

    static volatile uint16_t my_100us;   // incremented by ISR; read atomically by userland wrapper

    ISR( TIMER0_COMPA_vect ) { my_100us++; }

    static uint16_t now_100us( void )
    {
        uint16_t x;

        do
        {
            x = my_100us;
        }
        while ( x != my_100us );

        return x;
    }



    #define US_SINCE( Then )   ( ( now_100us() - ( Then ) ) * 100 )



    #define CR_SLEEP_US( Duration )                              \
                                                                 \
        do                                                       \
        {                                                        \
            static uint16_t then_100us;                          \
            then_100us = now_100us();                            \
            CR_WAIT( US_SINCE( then_100us ) >= ( Duration ) );   \
        } while ( 0 )



    #define CR_SLEEP_MS( Duration )  do { static uint16_t N; N = ( Duration ); while ( N-- ) CR_SLEEP_US( 1000 ); } while ( 0 )



    #define SET_GATE_0( Val )   BITS_SET_OR_CLR( PORTD, _BV( 0 ), Val )
    #define SET_GATE_1( Val )   BITS_SET_OR_CLR( PORTC, _BV( 7 ), Val )
    #define SET_GATE_2( Val )   BITS_SET_OR_CLR( PORTC, _BV( 5 ), Val )
    #define SET_GATE_3( Val )   BITS_SET_OR_CLR( PORTC, _BV( 4 ), Val )

    #define SET_GATE( Name, Val )   SET_GATE_ ## Name( Val )



    #define BTN_FUNC_IS_PRESSED   BITS_ARE_ZERO( PIND, _BV( 6 ) )
    #define BTN_0_IS_PRESSED      BITS_ARE_ZERO( PIND, _BV( 5 ) )
    #define BTN_1_IS_PRESSED      BITS_ARE_ZERO( PIND, _BV( 4 ) )
    #define BTN_2_IS_PRESSED      BITS_ARE_ZERO( PIND, _BV( 3 ) )
    #define BTN_3_IS_PRESSED      BITS_ARE_ZERO( PIND, _BV( 2 ) )



    #define BTN_IS_PRESSED_FILTERED( Name )   ( !!( btn_pressed_mask & _BV( BTN_ ## Name ) ) )



    static void io_init( void )
    {

        DDRD  =  _BV( 0 );
        DDRC  =  _BV( 7 ) | _BV( 5 ) | _BV( 4 );

        // Enable pullups for all input-pins.

        PORTA = ~DDRA;
        PORTB = ~DDRB;
        PORTC = ~DDRC;
        PORTD = ~DDRD;
    }



    static void tmr_init( void )
    {
        TCCR0A = _BV( CTC0 ) | _BV( CS00 );   // CTC-mode: top = OCRA; no prescaler

        OCR0A   = 90;   // 1 MHz f_osc: 100 ticks = 100 us (hand-cal'd)

        TIMSK0 = _BV( OCIE0A );   // interrupt on timer reach TOP
    }



    static void my_task_debounce( void )
    {
        CR_BEGIN;

        while ( 1 )
        {

            // Filter each key, and update appropriate bits in "btn_pressed_mask".

    #define BTN_TABLE_ENTRY( Name )                                                    \
            {                                                                          \
                uint8_t *hist = &btn_hist[ BTN_ ## Name ];                             \
                                                                                       \
                *hist <<= 1;                                                           \
                *hist  |=  BTN_ ## Name ## _IS_PRESSED;                                \
                                                                                       \
                if      ( *hist == 0xff )  btn_pressed_mask |=  _BV( BTN_ ## Name );   \
                else if ( *hist == 0x00 )  btn_pressed_mask &= ~_BV( BTN_ ## Name );   \
            }

            BTN_TABLE

    #undef BTN_TABLE_ENTRY

            CR_SLEEP_MS( 2 );
        }
    }



    static void my_task_handle_keys( void )
    {
        CR_BEGIN;

        while ( 1 )
        {

    #define BTN_TABLE_ENTRY( Name )                                                              \
            {                                                                                    \
                if ( BTN_IS_PRESSED_FILTERED( FUNC ) )                                           \
                {                                                                                \
                    /* Alternate keypress-behaviour (FUNC held): swap polarity. */               \
                                                                                                 \
                    if ( BTN_IS_PRESSED_FILTERED( Name ) )                                       \
                    {                                                                            \
                        btn_pol_swapped[ BTN_ ## Name ]  =  ! btn_pol_swapped[ BTN_ ## Name ];   \
                                                                                                 \
                        CR_WAIT( ! BTN_IS_PRESSED_FILTERED( Name ) );                            \
                    }                                                                            \
                }                                                                                \
                else                                                                             \
                {                                                                                \
                    /* Normal keypress-behaviour: control FET-gate. */                           \
                                                                                                 \
                    bool val = BTN_IS_PRESSED_FILTERED( Name );                                  \
                                                                                                 \
                    if ( btn_pol_swapped[ BTN_ ## Name ] )  val = !val;                          \
                                                                                                 \
                    SET_GATE( Name, val );                                                       \
                }                                                                                \
            }

            // (can't use "BTN_TABLE" here because of duplicate labels in generated CR-stuff. Oh well.)
            BTN_TABLE_ENTRY( 0 )
            BTN_TABLE_ENTRY( 1 )
            BTN_TABLE_ENTRY( 2 )
            BTN_TABLE_ENTRY( 3 )

    #undef BTN_TABLE_ENTRY

            CR_YIELD;
        }
    }



    int main( void )
    {
        io_init();
        tmr_init();

        SET_GATE_0( false );
        SET_GATE_1( false );
        SET_GATE_2( false );
        SET_GATE_3( false );

        sei();

        while ( 1 )
        {
            my_task_debounce();
            my_task_handle_keys();
        }
    }

And that's all - happy hacking.


Delivered to you by Vim, GNU Make, MultiMarkdown, bozohttpd, NetBSD, and 1 human.