News:

PROTON pic BASIC Compilers for PIC, PIC24, dsPIC33

Main Menu

Low cost SPI to Parallel I/O expander

Started by trastikata, Oct 09, 2023, 09:22 AM

Previous topic - Next topic

trastikata

Hello all,

looking for low cost Spi-Parallel I/O port expander but can't find anything below 2$ which is almost the price of the MCU, any suggestions?

Thanks

John Lawton

#1
74HC595 ?

Well that's okay for outputs.

Maybe the 74HC165 can be used for inputs?

JonW

WCH do the CH422 for $0.20 ish.  I2C to Parallel expander. CH422 Here

Buy from Here

Other way would be to code it in a low end MCU like Padauk for a few cents

tumbleweed

For SPI there's the MCP23S08 (8-bit) and MCP23S17 (16-bit), both of which can be found for < $2 depending on the pkg type and qty.

trastikata

Quote from: tumbleweed on Oct 09, 2023, 11:58 AMFor SPI there's the MCP23S08 (8-bit) and MCP23S17 (16-bit), both of which can be found for < $2 depending on the pkg type and qty.

Thank you tumbleweed, that will do the job, although I found it a bit pricey for such basic chip.

Interesting is that chips like that are are quite expensive although simple architecture thus easy to manufacture.

top204

#5
Why not get an inexpensive enhanced 14-bit core or 18F device with an on-board SPI peripheral, and make your own SPI slave device that operates as a port expander? With an enhanced 14-bit core's internal 32MHz oscillator or an 18F's 64MHz internal oscillator, they are fast enough for standard SPI speeds. Or use an inexpensive PIC24 or dsPIC33 device and then get even faster speeds for the slave if required.

SPI slave is relatively straightforward to do, and an interrupt is fired when SPI is received and then just see what the data is for. i.e. input, output, read, setting etc...


tumbleweed

I was just going to suggest that. You could get more features for less $, although it'll be a little slower than the pure hdw peripheral.

There's no buffering for the SPI input so you'll have to add a bit of delay between byte transfers for the slave to keep up.

trastikata

That's a good idea, but it will require flashing two PIC's instead of one, which will complicate things. Also I am afraid that glitches in the auxiliary MCU could cause malfunction. A hardware clocked SPI device looks less prone to random errors, at least I think so.

top204

#8
I don't think many Port expanders are just clocked shift registers, because they require the tristate functionality and the I/O mechanism inside them.

Below is an SPI slave program I wrote about 10 years ago for a PIC18F25K20 device on the Amicus18 board. If memory serves, the sample code for the SPI slave worked well, and I used the same slave mechanism on a board that was talking to 80 of them. Yes.... 80 microcontrollers operating as SPI slaves, for a memory reading project that could make any of its 80 pins into inputs, output, pullups, pulldowns and any voltage in or out, up to 24 volts. It never got sold because of an idiot in charge, who did not want the forensics to own the details of it, so I wasted my months of work on it, in my own time as well, and never got a penny for the months of work because I was a bloody idiot and did not get anything in writing.

Try the SPI slave code, you may like it:

'
'   /\\\\\\\\\
'  /\\\///////\\\
'  \/\\\     \/\\\                                                 /\\\          /\\\
'   \/\\\\\\\\\\\/        /\\\\\     /\\\\\\\\\\     /\\\\\\\\   /\\\\\\\\\\\  /\\\\\\\\\\\  /\\\\\\\\\
'    \/\\\//////\\\      /\\\///\\\  \/\\\//////    /\\\/////\\\ \////\\\////  \////\\\////  \////////\\\
'     \/\\\    \//\\\    /\\\  \//\\\ \/\\\\\\\\\\  /\\\\\\\\\\\     \/\\\         \/\\\        /\\\\\\\\\\
'      \/\\\     \//\\\  \//\\\  /\\\  \////////\\\ \//\\///////      \/\\\ /\\     \/\\\ /\\   /\\\/////\\\
'       \/\\\      \//\\\  \///\\\\\/    /\\\\\\\\\\  \//\\\\\\\\\\    \//\\\\\      \//\\\\\   \//\\\\\\\\/\\
'        \///        \///     \/////     \//////////    \//////////      \/////        \/////     \////////\//
'                                  Let's find out together what makes a PIC Tick!
'
' Demonstrate an SPI slave to read and write a block of RAM
'
' Written for the Positron8 BASIC compiler by Les Johnson
'
' Master                 Slave
' SDO(RC5) ------------> SDI(RC4)
' SDI(RC4) <------------ SDO(RC5)
' CLK(RC3) ------------> CLK(RC3)
' CS(RA5)  ------------> SS(RA5)
'
    Include "Amicus18.inc"                          ' Use an 18F25K20 device operating at 64MHz
    On_Hardware_Interrupt GoTo ISR_Handler          ' Point the interrupt to its handler
'
' Setup the SPI peripheral's pins
'
    Symbol SPI_CLK_Pin = PORTC.3
    Symbol SPI_SDI_Pin = PORTC.4
    Symbol SPI_SDO_Pin = PORTC.5
    Symbol SPI_SS_Pin = PORTA.5
'
' Variables
'
    Dim SPI_tDataReady As Bit                       ' Signals that the SPI has data
    Dim SPI_bByteCount As Byte Access               ' Counts the amount of bytes received
    Dim SPI_bValue As Byte Access                   ' Holds the value to write to RAM

    Dim SPI_bMnemonic As Byte                       ' Holds the mnemonic for read or write
    Dim SPI_bAddress As Byte                        ' Holds the address of the RAM to write/read
    Dim bRAM[255] As Byte Heap                      ' A block of RAM to read/write
'
' Mnemonics
'
    Symbol cMnWrite = $F0                           ' Write command
    Symbol cMnRead = $0F                            ' Read command

$ifndef True
    $define True 1
$endif
$ifndef False
    $define False 0
$endif

'--------------------------------------------------------------------------
' The main program starts here
' Read the stream of SPI data and read or write accordingly
'
Main:
    SPI_SlaveInit()

    Do
        '
        ' Trap an unexpected activation of the SS pin
        '
        If SPI_SS_Pin = 1 Then                      ' Is the SS line high?
            PIR1bits_SSPIF = 0                      ' Yes. So clear the SPI flag
            WREG = SSPBUF                           ' Clear SSPBUF
            SPI_bByteCount = 0                      ' Reset the byte counter
            SPI_tDataReady = False                  ' Reset the ready flag
        EndIf
        '
        ' Read the data structure. i.e. Mnemonic, Address, Value
        '
        If SPI_tDataReady = True Then               ' Has a byte been read from the SPI interrupt?
            SPI_tDataReady = False                  ' Yes. So reset the ready flag
            If SPI_bByteCount = 1 Then              ' Have we read the first byte?
                SPI_bMnemonic = SPI_bValue          ' Yes. So this is the mnemonic

            ElseIf SPI_bByteCount = 2 Then          ' Have we read the second byte?
                SPI_bAddress = SPI_bValue           ' Yes. So this is the address
                If SPI_bMnemonic = cMnRead Then     ' Is the mnemonic a read command?
                    SSPBUF = bRAM[SPI_bAddress]     ' Yes. So setup for the return byte
                EndIf

            Else                                    ' Otherwise... It must be a third byte
                If SPI_bMnemonic = cMnWrite Then    ' Is the mnemonic a write command?
                    bRAM[SPI_bAddress] = SPI_bValue ' Yes. So place the byte read into the RAM array
                EndIf
                SPI_bByteCount = 0                  ' Reset the byte counter
            EndIf
        EndIf
    Loop

'--------------------------------------------------------------------------
' Initialise the SPI slave routines
'
Proc SPI_SlaveInit()
    INTCON = $00                                    ' Make sure interrupts are disabled
    WREG = SSPBUF                                   ' Clear the SSPBUF of any erroneous data
'
' Setup the SPI peripheral
'
    SSPCON1 = $04                                   ' \ Disable SPI and configure SCK, SDO, SDI and SS as SPI port pins
                                                    ' | Idle state for clock is a low level
                                                    ' / SPI Slave mode, clock = SCK pin, SS pin control enabled
    SSPSTAT = $00                                   ' \ Input data sampled at middle of data output time
                                                    ' / Transmit occurs on transition from Idle to active clock state
'
' SPI slave pin configurations
'
    PinInput SPI_CLK_Pin                            ' SCK as input
    PinInput SPI_SDI_Pin                            ' SDI as input
    PinOutput SPI_SDO_Pin                           ' SDO as output
    PinInput SPI_SS_Pin                             ' SS an input

    SPI_tDataReady = False                          ' Clear the data ready flag
    SPI_bByteCount = 0
    SPI_bValue = 0
    Clear bRAM                                      ' Clear the array
'
' Setup and enable the SPI interrupt mechanism
'
    PIR1bits_SSPIF = 0                              ' Clear the SPI interrupt flag
    SSPCON1bits_SSPEN = 1                           ' Enable the SPI peripheral
    PIE1bits_SSPIE = 1                              ' Enable the SPI peripheral interrupt
    INTCON = $C0                                    ' Enable peripheral and global interrupts
EndProc

'--------------------------------------------------------------------------
' Interrupt handler
' Operates as an SPI slave.
'
ISR_Handler:
    Context Save

    If PIR1bits_SSPIF = 1 Then                      ' Is it an SPI interrupt?
        Repeat: Until SSPSTATbits_BF = 1            ' Yes. So wait for the byte
        SPI_bValue = SSPBUF
        Inc SPI_bByteCount                          ' Increment the byte counter
        SSPCON1bits_WCOL = 0                        ' \
        SSPCON1bits_SSPOV = 0                       ' / Clear any possible errors
        SPI_tDataReady = True                       ' Signal that there's data to be worked upon
        PIR1bits_SSPIF = 0                          ' Reset the SPI interrupt flag
    EndIf

    Context Restore

Below is an SPI master program that should talk to the above SPI slave code. Remember, it is about 10 years since I wrote the code, but it should be OK, and I think I remember testing it as a demo I was going to place on the old forum:

'
'   /\\\\\\\\\
'  /\\\///////\\\
'  \/\\\     \/\\\                                                 /\\\          /\\\
'   \/\\\\\\\\\\\/        /\\\\\     /\\\\\\\\\\     /\\\\\\\\   /\\\\\\\\\\\  /\\\\\\\\\\\  /\\\\\\\\\
'    \/\\\//////\\\      /\\\///\\\  \/\\\//////    /\\\/////\\\ \////\\\////  \////\\\////  \////////\\\
'     \/\\\    \//\\\    /\\\  \//\\\ \/\\\\\\\\\\  /\\\\\\\\\\\     \/\\\         \/\\\        /\\\\\\\\\\
'      \/\\\     \//\\\  \//\\\  /\\\  \////////\\\ \//\\///////      \/\\\ /\\     \/\\\ /\\   /\\\/////\\\
'       \/\\\      \//\\\  \///\\\\\/    /\\\\\\\\\\  \//\\\\\\\\\\    \//\\\\\      \//\\\\\   \//\\\\\\\\/\\
'        \///        \///     \/////     \//////////    \//////////      \/////        \/////     \////////\//
'                                  Let's find out together what makes a PIC Tick!
'
' SPI Master demo to write and read RAM in the SPI slave
'
' Written for the Positron8 BASIC compiler by Les Johnson
'
' Master                 Slave
' SDO(RC5) ------------> SDI(RC4)
' SDI(RC4) <------------ SDO(RC5)
' CLK(RC3) ------------> CLK(RC3)
' CS(RA5)  ------------> SS(RA5)
'
    Include "Amicus18.inc"                              ' Use an 18F25K20 operating at 64MHz
'
' Setup the SPI peripheral's pins
'
    Symbol SPI_CLK_Pin = PORTC.3
    Symbol SPI_SDI_Pin = PORTC.4
    Symbol SPI_SDO_Pin = PORTC.5
    Symbol SPI_CS_Pin = PORTA.5
'
' Mnemonics
'
    Symbol cMnWrite = $F0                               ' Write command
    Symbol cMnRead = $0F                                ' Read command
'
' Variables
'
    Dim SPI_bAddress As Byte                            ' Holds the address of the RAM to write/read
    Dim SPI_ByteOut As Byte                             ' Holds the byte to write
    Dim SPI_ByteIn As Byte                              ' Holds the byte read

'-----------------------------------------------------------------------------------

$define SPI_Enable() PinClear SPI_CS_Pin
$define SPI_Disable() PinSet SPI_CS_Pin

'----------------------------------------------------------------------------
Main:
    DelayMS 100                                         ' Give time for the slave to power up

    SSPCON1 = $02                                       ' Setup SPI for master mode, Fosc / 64 (1MHz operation)
    SSPCON1bits_CKP = 0                                 ' Clock Polarity Select : Clock idle low
    SSPSTATbits_CKE = 0                                 ' Clock Edge Select Bit : Transmit on idle to active transition
    SSPSTATbits_SMP = 0                                 ' Data Input sample phase: Sample in middle of data
    PIR1bits_SSPIF = 0                                  ' Clear buffer full status

    PinOutput SPI_CLK_Pin                               ' Clock pin to output
    PinInput SPI_SDI_Pin                                ' Data in pin to input
    PinOutput SPI_SDO_Pin                               ' Data out pin to output
    PinHigh SPI_CS_Pin                                  ' Disable the SPI slave
    SSPCON1bits_SSPEN = 1                               ' Enable SPI

    SPI_bAddress = 0
    Do                                                  ' Create a loop
        HRSOutLn "Writing"
        For SPI_ByteOut = 255 DownTo 0                  ' Create a loop to write to the slave's RAM
            Command_Write(SPI_bAddress, SPI_ByteOut)    ' Write to the slave's RAM
            Inc SPI_bAddress                            ' Move up the Address
        Next                                            ' Close the loop

        HRSOutLn "\rReading"
        For SPI_bAddress = 0 To 255                     ' Create a loop to read from the slave's RAM
            If SPI_bAddress // 10 = 1 Then              ' Have we reached the 10th read?
                Command_Wrong(SPI_bAddress)             ' Yes. So send a malformed command for testing
                HRSOutLn "Incorrect Command Sent"       ' Signal that it has been sent
            Else                                        ' Otherwise...
                SPI_ByteIn = Command_Read(SPI_bAddress) ' Read from the slave's RAM
                HRSOutLn "Value = ", Dec SPI_ByteIn     ' Display what was read
            EndIf
        Next                                            ' Close the loop
    Loop                                                ' Do it forever

'-----------------------------------------------------------------------------------
' Write a byte to the slave
' Input     : pAddress holds the 8-bit address
'           : pValue holds the byte to write
' Output    : None
' Notes     : Has a small delay to give time for the SPI slave to write the value
'
Proc Command_Write(pAddress As Byte, pValue As Byte)
    SPI_Enable()
    SPI_Write(cMnWrite)
    SPI_Write(pAddress)
    SPI_Write(pValue)
    DelayCS 50
    SPI_Disable()
EndProc

'-----------------------------------------------------------------------------------
' Read a byte from the slave
' Input     : pAddress holds the 8-bit address
' Output    : pResult holds the byte read from the slave
' Notes     : Has a small delay to give time For the SPI slave to read the value
'
Proc Command_Read(pAddress As Byte), WREG
    SPI_Enable()
    SPI_Write(cMnRead)
    SPI_Write(pAddress)
    DelayCS 50
    SPI_Write($FF)
    Result = WREG
    SPI_Disable()
EndProc

'-----------------------------------------------------------------------------------
' Write a single byte (8-bits) to the SPI bus
' Input     : Single 8-bit variable for SPI bus
' Output    : Returns the value read from the SPI interface
' Notes     : None
'
Proc SPI_Write(pDataOut As WREG), WREG
    SSPSTATBits_BF = 0                                  ' Empty the SSPBUF buffer flags
    PIR1bits_SSPIF = 0                                  ' Clear the interrupt flag
    SSPBUF = WREG                                       ' Place the data to send into SSPBUF
    Clrwdt                                              ' Keep the watchdog happy
    Repeat: Until PIR1bits_SSPIF = 1                    ' Wait until cycle complete
    Result = SSPBUF                                     ' Read SSPBUF into WREG
EndProc

'-----------------------------------------------------------------------------------
' Deliberately send a malformed SPI command
' This will test the reset mechanism of the slave SPI
'
Proc Command_Wrong(pAddress As Byte)
    SPI_Enable()
    SPI_Write(pAddress)
    SPI_Disable()
EndProc

top204

#9
As a guide, below is the actual code I used for the SPI slave controllers, adapted for the Positron8 compiler. It looks confusing, but it should give you an idea how the SPI slave interrupt mechanism works without buffering it. The slave device controlled some peripherals for resistor pull-ups/pull-downs and voltages etc, but the way of capturing the bytes within the interrupt via SPI remain the same.

The slave waits for its address, and if its address is valid, it reads the next mnemonics from the SPI master, so quite a few slaves can share a CS line.

'
'   /\\\\\\\\\
'  /\\\///////\\\
'  \/\\\     \/\\\                                                 /\\\          /\\\
'   \/\\\\\\\\\\\/        /\\\\\     /\\\\\\\\\\     /\\\\\\\\   /\\\\\\\\\\\  /\\\\\\\\\\\  /\\\\\\\\\
'    \/\\\//////\\\      /\\\///\\\  \/\\\//////    /\\\/////\\\ \////\\\////  \////\\\////  \////////\\\
'     \/\\\    \//\\\    /\\\  \//\\\ \/\\\\\\\\\\  /\\\\\\\\\\\     \/\\\         \/\\\        /\\\\\\\\\\
'      \/\\\     \//\\\  \//\\\  /\\\  \////////\\\ \//\\///////      \/\\\ /\\     \/\\\ /\\   /\\\/////\\\
'       \/\\\      \//\\\  \///\\\\\/    /\\\\\\\\\\  \//\\\\\\\\\\    \//\\\\\      \//\\\\\   \//\\\\\\\\/\\
'        \///        \///     \/////     \//////////    \//////////      \/////        \/////     \////////\//
'                                  Let's find out together what makes a PIC Tick!
'
' Memory Reader Header SPI Slave
'
' Written for the Positron8 BASIC compiler by Les Johnson
'
    Device = 18F13K22
    Declare Xtal = 64
    Declare Watchdog = On

    On_Hardware_Interrupt GoTo ISR_Handler
'
' SPI Slave pins
'
    Symbol SPI_CLK_Pin = PORTB.6
    Symbol SPI_SDI_Pin = PORTB.4
    Symbol SPI_SDO_Pin = PORTC.7
    Symbol SPI_SS_Pin = PORTC.6
'
' Pullup/Pulldown ADG1419 switch pins
'
    Symbol PullUpDown_IO_Enable_Pin = LATB.7    ' To the EN pin of U7 (ADG1419) (used for pullup/pulldown resistor)
    Symbol PullUpDown_IO_In_Pin = LATB.5        ' To the IN pin of U7 (ADG1419) (used for pullup/pulldown resistor)
'
' Digital Resistor pin declarations
'
    Symbol Res_CS_Pin = LATC.2                  ' SPI CS to Digital Resistor CS
    Symbol Res_CLK_Pin = LATC.1                 ' SPI CLK to Digital Resistor CLK
    Symbol Res_SDO_Pin = LATC.0                 ' SPI SDO to Digital Resistor SI
    Symbol Res_SDI_Pin = LATC.4                 ' SPI SDI from Digital Resistor SO (Not Used)
'
' DUT I/O Enable pin
'
    Symbol Main_IO_Enable_Pin = LATC.5          ' To the I/O Enable pin of the ADG1413
'
' A debug pin connects to an LED
'
    Symbol Debug_Pin = PORTA.4
'
' A physical reset pin used to reset the microcontroller when it receives the reset mnemonic
'
    Symbol Reset_Pin = PORTA.5

$ifndef True
    $define True 1
$endif
$ifndef False
    $define False 0
$endif
'
' Mnemonics (in the form of bit numbers)
'
    Symbol cPinInput =       0      ' Set the pin to DUT as an input (1)
    Symbol cPinOutput =      1      ' Set the pin to DUT as an output (2)
    Symbol cSetOutVolts =    2      ' Alter the voltage output to DUT (4)
    Symbol cSetThreshVolts = 3      ' Alter the threshold voltage to the input comparator (8)
    Symbol cPullUp_On =      4      ' Enable the pull up resistor on the main I/O line (16)
    Symbol cPullDown_On =    5      ' Enable the pull down resistor on the main I/O line (32)
    Symbol cPullUpDown_Off = 6      ' Disable the pullup and down resistors on the main I/O line (64)
    Symbol cReadADC =        7      ' Take a reading from the internal ADC (8-bit) (128)
'
' Address of this header
'
    Symbol cMyAddress = 1
'
' Variables
'
    Dim tSPI_DataReady As Bit

    Dim bAddress As Byte Access
    Dim bMnemonic As Byte Access
    Dim bVoltageValue As Byte Access

'--------------------------------------------------------------------------
' Enable the pullup resistor on the main I/O line
'
' Notes : Set the IN Line High, Enable the EN line (high)
'
$define PullUp_On()             '
    PinSet PullUpDown_IO_In_Pin '
    PinSet PullUpDown_IO_Enable_Pin

'--------------------------------------------------------------------------
' Enable the pulldown resistor on the main I/O line
'
' Notes : Pull the IN Line Low, Enable the EN line (high)
'
$define PullDown_On()              '
    PinClear PullUpDown_IO_In_Pin  '
    PinSet PullUpDown_IO_Enable_Pin

'--------------------------------------------------------------------------
' Disable the pull up and down resistors on the main I/O line
'
' Notes : Disable the EN line
'
$define PullUpDown_Off() PinClear PullUpDown_IO_Enable_Pin

'--------------------------------------------------------------------------
' Make the main I/O of the Header an input
'
$define MakeMainPin_Input() PinSet Main_IO_Enable_Pin

'--------------------------------------------------------------------------
' Make the Main I/O of the Header an output
'
$define MakeMainPin_Output() PinClear Main_IO_Enable_Pin

'--------------------------------------------------------------------------
$define ClockOut() PinSet Res_CLK_Pin : Clrwdt : PinClear Res_CLK_Pin                     ' Clock the bit out
$define PutBit(pBitValue) PinSet Res_SDO_Pin : Btfss pBitValue : PinClear Res_SDO_Pin     ' Put current outgoing bit on Res_SDO_Pin

'-----------------------------------------------------------------------------
' Implementation of a fast Software Mode 0-0 Master SPI (out only) interface.
' Transmits 16-bits using the SPI protocol mode 0 (MSB first)
' SCK is idle-low, and bits are latched on SCK rising.
'
' Input     : bVoltageValue holds the 8-bit Voltage to send (0 is highest voltage, 255 is the lowest)
'           : Res_CLK_Pin is the LAT.BIT of the SPI Clock pin
'           : Res_SDO_Pin is the LAT.BIT of the SPI Data IN Pin
'           : Res_CS_Pin is the LAT.BIT of the SPI CS Pin
' Output    : None
' Notes     : Clocks the bits out individually for extra speed
'           : Some common voltage values are 225 = 1.8V, 207 = 2.8V, 199 = 3.3V, 169 = 5.0V
'
$define SetVoltageOut()         '
    PinClear Res_CS_Pin         '
    PinClear Res_SDO_Pin        '
    ClockOut()                  '
    ClockOut()                  '
    ClockOut()                  '
    PinSet Res_SDO_Pin          '
    ClockOut()                  '
    PinClear Res_SDO_Pin        '
    ClockOut()                  '
    ClockOut()                  '
    PinSet Res_SDO_Pin          '
    ClockOut()                  '
    PinClear Res_SDO_Pin        '
    ClockOut()                  '
    PutBit(bVoltageValue.7)     '
    ClockOut()                  '
    PutBit(bVoltageValue.6)     '
    ClockOut()                  '
    PutBit(bVoltageValue.5)     '
    ClockOut()                  '
    PutBit(bVoltageValue.4)     '
    ClockOut()                  '
    PutBit(bVoltageValue.3)     '
    ClockOut()                  '
    PutBit(bVoltageValue.2)     '
    ClockOut()                  '
    PutBit(bVoltageValue.1)     '
    ClockOut()                  '
    PutBit(bVoltageValue.0)     '
    ClockOut()                  '
    PinSet Res_CS_Pin

'-----------------------------------------------------------------------------
' Implementation of a fast Software Mode 0-0 Master SPI (out only) interface.
' Transmits 16-bits using the SPI protocol mode 0 (MSB first)
' SCK is idle-low, and bits are latched on SCK rising.
'
' Input     : bVoltageValue holds the 8-bit Voltage to send
'           : Res_CLK_Pin is the LAT.BIT of the SPI Clock pin
'           : Res_SDO_Pin is the LAT.BIT of the SPI Data IN Pin
'           : Res_CS_Pin is the LAT.BIT of the SPI CS Pin
' Output    : None
' Notes     : Clocks the bits out individually for extra speed
'
$define SetVoltageIn()          '
    PinClear  Res_CS_Pin        '
    PinClear Res_SDO_Pin        '
    ClockOut()                  '
    ClockOut()                  '
    ClockOut()                  '
    PinSet Res_SDO_Pin          '
    ClockOut()                  '
    PinClear Res_SDO_Pin        '
    ClockOut()                  '
    ClockOut()                  '
    ClockOut()                  '
    PinSet Res_SDO_Pin          '
    ClockOut()                  '
    PutBit(bVoltageValue.7)     '
    ClockOut()                  '
    PutBit(bVoltageValue.6)     '
    ClockOut()                  '
    PutBit(bVoltageValue.5)     '
    ClockOut()                  '
    PutBit(bVoltageValue.4)     '
    ClockOut()                  '
    PutBit(bVoltageValue.3)     '
    ClockOut()                  '
    PutBit(bVoltageValue.2)     '
    ClockOut()                  '
    PutBit(bVoltageValue.1)     '
    ClockOut()                  '
    PutBit(bVoltageValue.0)     '
    ClockOut()                  '
    PinSet Res_CS_Pin

'--------------------------------------------------------------------------
' Main program loop
' Waits for SPI reception.
' If the address matches this head, read the mnemonic and act upon it.
' If the address is 255 then this signifies an "all header" broadcast so, read the Mnemonic and act upon it.
' It is possible to set the pin as an output or input, while simultaneously adjusting the voltage output or voltage threshold
'
Main:
    Setup()                                             ' Setup the program and initialise the SPI peripheral

    bVoltageValue = 199                                 ' A default voltage of 3.3 Volts
    SetVoltageIn()                                      ' Set the initial input threshold (based upon bVoltageValue)
    SetVoltageOut()                                     ' Set the initial output voltage (based upon bVoltageValue)

    MakeMainPin_Input()                                 ' Default to the main I/O pin as an input

    Do                                                  ' Create an inifinite loop
        'Repeat: Clrwdt: Until SPI_SS_Pin = 0           ' Wait for the SS line to go low
        Repeat: Clrwdt: Until tSPI_DataReady = True     ' Wait for an SPI reception to occur
        tSPI_DataReady = False                          ' Reset the ready flag
        If bAddress = cMyAddress Then                   ' Is the data meant for just us?
            PinSet Debug_Pin                            ' Indicate that we have received a command request
            If bMnemonic.cPinInput = 1 Then             ' Set the header as an input?
                MakeMainPin_Input()
            ElseIf bMnemonic.cPinOutput = 1 Then        ' Set the header as an output?
                MakeMainPin_Output()
            EndIf

            If bMnemonic.cSetOutVolts = 1 Then          ' Set the output voltage of the header?
                SetVoltageOut()                         ' Yes. So alter the digital resistor accordingly
            ElseIf bMnemonic.cSetThreshVolts = 1 Then   ' Yes. So set the input voltage threshold of the header?
                SetVoltageIn()                          ' Alter the digital resistor accordingly
            EndIf

            If bMnemonic.cPullUp_On = 1 Then            ' Enable the pull up resistor on the main I/O line?
                PullUp_On()                             ' Yes. So enable the pull-up resistor
            ElseIf bMnemonic.cPullDown_On = 1 Then      ' Enable the pull down resistor on the main I/O line?
                PullDown_On()                           ' Yes. So enable the pull-down resistor
            ElseIf bMnemonic.cPullUpDown_Off = 1 Then   ' Disable the pull up and down resistors on the main I/O line?
                PullUpDown_Off()                        ' Yes. So disable the pull-up and pull-down resistors
            EndIf

        ElseIf bAddress = $FF Then                      ' Otherwise.. Is the data for all headers?
            PinSet Debug_Pin                            ' Indicate that we have received a command request
            If bMnemonic.cPinInput = 1 Then             ' Set the header as an input?
                MakeMainPin_Input()
            ElseIf bMnemonic.cPinOutput = 1 Then        ' Set the header as an output?
                MakeMainPin_Output()
            EndIf

            If bMnemonic.cSetOutVolts = 1 Then          ' Set the output voltage of the header?
                SetVoltageOut()                         ' Alter the digital resistor accordingly
            ElseIf bMnemonic.cSetThreshVolts = 1 Then   ' Set the input voltage threshold of the header?
                SetVoltageIn()                          ' Alter the digital resistor accordingly
            EndIf

            If bMnemonic.cPullUp_On = 1 Then            ' Enable the pull up resistor on the main I/O line?
                PullUp_On()                             ' Yes. So enable the pull-up resistor
            ElseIf bMnemonic.cPullDown_On = 1 Then      ' Enable the pull down resistor on the main I/O line?
                PullDown_On()                           ' Yes. So enable the pull-down resistor
            ElseIf bMnemonic.cPullUpDown_Off = 1 Then   ' Disable the pull up and down resistors on the main I/O line?
                PullUpDown_Off()                        ' Yes. So disable the pull-up and pull-down resistors
            EndIf

        EndIf
        PinClear Debug_Pin                              ' Clear the debug pin
    Loop

'--------------------------------------------------------------------------
' Setup the program
'
Proc Setup()
    OSCCON = %01111000                                  ' Device enters Idle mode on Sleep instruction
                                                        ' Internal oscillator frequency is 16 MHz
                                                        ' Device is running from the clock defined by FOSC<2:0> of the CONFIG1 register
                                                        ' Primary clock (determined by CONFIG1H[FOSC<3:0>])
    INTCON = 0
'
' Set all the ports to digital mode except AN2 (RA2)
'
    CM1CON0 = 0
    CM2CON0 = 0
    ANSEL = %00000100                                   ' AN2 as analogue
    ANSELH = 0
    ADCON0 = %00001001                                  ' Select AN2 as the channel and enable the ADC
    ADCON1 = %00000000                                  ' Vref+ = Vdd, Vref- = Vss
    ADCON2 = %00001011                                  ' Left justified (8-bit), 2 Tad, FRC oscillator
'
' Initialise the SPI slave
'
    WREG = SSPBUF                                       ' Clear the SSPBUF of any erroneous data
    Clrwdt
    PIR1bits_SSPIF = 0                                  ' Clear the SPI interrupt flag
'
' Setup the SPI peripheral
'
    SSPCON1 = %00000100                                 ' Disable SPI and configure SCK, SDO, SDI and SS as SPI port pins
                                                        ' Idle state for clock is a low level
                                                        ' SPI Slave mode, clock = SCK pin, SS pin control enabled

    SSPSTAT = %00000000                                 ' Input data sampled at middle of data output time
                                                        ' Transmit occurs on transition from Idle to active clock state
'
' SPI slave pin configurations
'
    PinInput SPI_CLK_Pin                                ' SCK as input
    PinInput SPI_SDI_Pin                                ' SDI as input
    PinInput SPI_SDO_Pin                                ' SDO as input (So it does not clash with other headers)
    PinInput SPI_SS_Pin                                 ' SS an input
'
' Software SPI pin configurations for coms with the digital pot
'
    PinHigh Res_CS_Pin                                  ' Software SPI CS to Digital Resistor CS
    PinLow Res_CLK_Pin                                  ' Software SPI CLK to Digital Resistor CLK
    PinOutput Res_SDO_Pin                               ' Software SPI SDO to Digital Resistor SI
    PinInput Res_SDI_Pin                                ' Software SPI SDI from Digital Resistor SO (Not Used)

    PinOutput Main_IO_Enable_Pin                        ' Set the header I/O selector pin as an output
'
' Setup the Pullup/Pulldown ADG1419 switch pins
'
    PinLow PullUpDown_IO_Enable_Pin                     ' Disable U7 (ADG1419) (used for pullup/pulldown resistor)
    PinLow PullUpDown_IO_In_Pin                         ' To the IN pin of U7 (ADG1419) (used for pullup/pulldown resistor)

    PinInput Reset_Pin                                  ' Make the reset pin an input so it does not cause any loading
    PinLow Debug_Pin                                    ' Pull the PORTA.5 pin low (Sometimes used as an LED indicator)
    PinInput PORTA.2                                    ' Make the ADC pin an input

    tSPI_DataReady = False                              ' Clear the data ready flag
'
' Setup and enable the SPI interrupt mechanism
'
    SSPCON1bits_SSPEN = 1                               ' Enable SPI and configure SCK, SDO, SDI and SS As serial port pins
    PIR1bits_SSPIF = 0                                  ' Clear the SPI interrupt flag
    PIE1bits_SSPIE = 1                                  ' Enable SPI peripheral interrupts
    INTCONbits_PEIE = 1                                 ' Enable peripheral interrupts
    INTCONbits_GIE = 1                                  ' Enable global interrupts
EndProc

'--------------------------------------------------------------------------
' SPI peripheral interrupt handler
' Receive three bytes from the SPI interface. Address, Mnemonic, and Voltage Value
'
ISR_Handler:
    Context Save

    Repeat: Until SSPSTATbits_BF = 1
    bAddress = SSPBUF                                   ' Receive the first byte (Address)

    Repeat: Until SSPSTATbits_BF = 1
    bMnemonic = SSPBUF                                  ' Receive the second byte (Mnemonic)

    If bAddress = cMyAddress Then                       ' Is this information meant for us?
        PinOutput SPI_SDO_Pin                           ' Yes. So set the SDO pin an output
        SSPBUF = bAddress                               ' Send back the Address so that the master knows that we are receiving
        Repeat: Until SSPSTATbits_BF = 1
        bVoltageValue = SSPBUF                          ' Receive the third byte (Value)

        If bMnemonic.cReadADC = 1 Then                  ' Is the mnemonic to read the ADC?
            ADCON0bits_GO_DONE = 1                      ' Yes. So start an ADC conversion
            Repeat: Until ADCON0bits_GO_DONE = 0        ' Wait for the ADC conversion to finish
            SSPBUF = ADRESH                             ' Transfer the 8-bit ADC reading to SSPBUF for transmission
            Repeat: Until SSPSTATbits_BF = 1
            WREG = SSPBUF                               ' Receive the fourth byte (dummy)

        ElseIf bMnemonic = 255 Then                     ' Have we received a reset mnemonic?
            'PinLow Reset_Pin                           ' Yes. So reset the microcontroller
            Pop
            GoTo Main                                   ' Make sure it resets
        EndIf
        PinInput SPI_SDO_Pin                            ' Set the SDO pin back to an input so it does not clash with other headers

    ElseIf bAddress = 0 Then                            ' Is the address 0 usually casued by a miss-timing?
        If bMnemonic = 255 Then                         ' Yes. So have we received a reset mnemonic?
            'Low Reset_Pin                              ' Yes. So reset the microcontroller
            Pop
            GoTo Main                                   ' Make sure it resets
        EndIf
    Else                                                ' Otherwise...
        Repeat: Until SSPSTATbits_BF = 1
        bVoltageValue = SSPBUF                          ' Receive the third byte (Value)
    EndIf

    tSPI_DataReady = True                               ' Signal that there's data to be worked upon
    SSPCON1bits_WCOL = 0                                ' Clear the WCOL flag
    SSPCON1bits_SSPOV = 0                               ' Clear the SSPOV flag
    PIR1bits_SSPIF = 0                                  ' Reset the SPI interrupt flag

    Context Restore                                     ' Exit the interrupt

'--------------------------------------------------------------------------
'
Config_Start
    WDTEN = On          ' Watchdog Timer enabled
    FOSC = IRC          ' Internal RC oscillator
    PLLEN = On          ' Oscillator multiplied by 4
    MCLRE = On          ' MCLR pin enabled, RE3 input pin disabled
    PCLKEN = On         ' Primary clock enabled
    FCMEN = Off         ' Fail-Safe Clock Monitor disabled
    IESO = Off          ' Oscillator Switchover mode disabled
    PWRTEN = On         ' PWRT enabled
    BOREN = On          ' Brown-out Reset enabled and controlled by software (SBOREN is enabled)
    BORV = 19           ' VBOR set to 1.9 V nominal
    WDTPS = 512         ' Watchdog to 1:512
    HFOFST = Off        ' The system clock is held Off until the HFINTOSC is stable.
    STVREN = Off        ' Stack full/underflow will not cause Reset
    LVP = Off           ' Single-Supply ICSP disabled
    BBSIZ = Off         ' 1k Word boot block size
    XINST = Off         ' Instruction set extension and Indexed Addressing mode disabled (Legacy mode)
    CP0 = Off            ' Block 0 code-protected
    CP1 = Off            ' Block 1 code-protected
    CPB = Off            ' Boot block code-protected
    CPD = Off            ' Data EEPROM code-protected
    WRT0 = Off           ' Block 0 write-protected
    WRT1 = Off           ' Block 1 write-protected
    WRTB = Off           ' Boot block write-protected
    WRTC = Off           ' Configuration registers not write-protected
    WRTD = Off           ' Data EEPROM write-protected
    EBTR0 = Off          ' Block 0 protected from table reads executed in other blocks
    EBTR1 = Off          ' Block 1 protected from table reads executed in other blocks
    EBTRB = Off          ' Boot block protected from table reads executed in other blocks
Config_End

See_Mos

I like the MCP23S17, it's so easy to use, even if it is a little bit more expensive than some other options.

Beware of simulating it in Proteus.  I don't know if the model has been fixed but there were some errors up to version 8 Beta.

top204

#11
The microchip MCP devices actually look as though they are a re-packaged PIC microcontroller running firmware for a port expander using either I2C or SPI.

Creating one in Positron8 would be very straightforward on any device that has the capabilities of SPI slave. And if a small microcontroller is chosen, it would be less money. It could be created as a seperate item and programmed as an expander before it is placed on the board, but with the capabilities of a re-program on the PCB, just in case.

However, there are a plethora of port expanders out there, and it is just a matter of choosing which one, and from whom. For example, RS, Farnell and Digikey etc, are silly, silly prices for items anyway, so choose the least expensive from them and find it elsewhere. :-)

The XRA1404IL16TR-F device seems to be the least expensive, but it has a limitation for some circuits. It has a maximum voltage of 3.6 volts, which is the standard maximum voltage these days, but a lot of people still work with 5 volts instead of 3.3 volts.

It has an 8-pin I/O expansion, and it comes in the dreadful 16-pin QFN package, or the better 16-pin TSSOP package.

But for what it is, it is still expensive in my opinion, and it could be re-created with an inexpensive PIC microcontroller.

tumbleweed

QuoteThe microchip MCP devices actually look as though they are a re-packaged PIC microcontroller running firmware for a port expander using either I2C or SPI.
I don't think so. If you look at the timing the MCP parts can do back-to-back transfers.
A uC running code can't handle that... there needs to be time between bytes for the uC to read and act on the data.

The master needs to add delays between bytes when used with a slave, or the slave has to run a lot faster than the master.


JonW

#13
Tumbleweed
I've come across a truckload of MCU-emulated chips in the past that are based on processors or HW that are not seen in the open market but are still hybrid HW/MCU-based (Emulated). Les is right. Been like this for 30yrs in TW.CN space. 

For instance

The Padauk MCU are quite unique for this kind of custom high-speed process, costing 0.07. I've been using them for yrs, along with others that have unique features for $.  They have limited HW but have a unique architecture where they hand off the processor to a new process. withing a pre-defined clock structure or "BEAT",  It has to be managed but holy crap its fast and massively precise.  16M core 2 FPPA can run 2 processes at 4M!  Has a 12-bit ADC, Comparator, opamp. 8x8 Multipleier, PWM, timers and Band gap,..  Code is a weird pseudo-C and Asm hybrid.  But is $0.07...   flash-based not OTP.  Can produce a fast Spi - Parallel for << 0.1 usd.  this is an industrial MCU, too.
They can do 8FPPA fast... that's 8 processes using the same HW but at speeds Arm 7 can't do for 10 cents. Emulation of HW in SW.

 The architecture of the processor is built to emulate




JonW

Lots of open source tools too Les!!  ;) Not Nyquest complexity/ Variance on the processor architecture.
Look at he padauk FPPA. There is $$ in the new leap dragon

tumbleweed

#15
My point is that with an SPI PIC slave, if the master sends two bytes back to back rapidly you can get an overrun and the first byte will be lost.

If you look at the block diagram of the SPI peripheral you'll see that there's only a single buffer, the SSPBUF register. When the eighth clock comes in data in the SSPSR shift register is transferred to the SSPBUF. If the master SPI clock is fast, say 8MHz that only allows 1us for the slave to get the byte before transfers fail. The master has to add delays between bytes to allow time for the slave to react, and this makes the timing very fragile since it all depends on how fast the slave runs and how it's coded so the time is basically unknown.

For this discussion it doesn't matter how it's done in other hardware.

I'm not knocking Les's code... I'm just saying there are a lot of pitfalls with this approach and you have to be aware of them and account for it.



top204

You are correct tumbleweed, the standard MSSP peripheral on an 8-bit device is not as fast as a dedicated peripheral doing a single job. However, they can be improved upon and still use the PIC core if that is the device's only purpose. Just look at PIC24 devices! They are using the same hardware core mechanism as 8-bit devices, but are a lot faster, and their peripherals are very fast to operate.

It also depends on what the expansion peripheral is being used for. For example, people are actually using I2C to interface with graphic LCDs?? I know, I cannot believe it either! I2C is far too slow for talking to graphic LCDs with all of their pixel writes and reads and commands within them, but they are popular. Why, I do not know, but they are. For an alphanumeric LCD, I2C is just right because they are not fast peripherals with lots of RAM in them and commands.

So for a standard I/O expansion, a standard 8-bit PIC microcontroller running slave firmware will be just right, and it can be adapted to do any specialist work as well, and take some of the burden off the master controlling it. For example automatically setting its TRIS state if a command is sent to change a pin or the whole port, and the same if reading a pin or port.