Inbuilt SPI in PIC16F877A

JohnInTX

Joined Jun 26, 2012
4,787
Think about it in parts.

1) You always have to initialize the SPI (and everything else) so you need a function to do that.
C:
void SPI_Init(void)
{
   // set up the SSPCON1 etc. to do what you want
}
2) SPI always sends one byte and receives one byte at a time, regardless of what kind of slave. That means that at a low level you need a function that does that.
C:
// sends one byte, returns byte received
unsigned char SPI_TXRX(unsigned char TXchar)
{
  unsigned char RXchar;
  while (!BF);  // wait for open buffer
  RXchar = SSPBUF;  // read returned char if interested
  SSPBUF = TXchar; // send new character
  return RXchar;
}
Note that this function is very general, it just sends and receives ONE byte over SPI. That's all SPI actually does.

To complete the low level SPI functions, we need a way to select and deselect a particular slave using CS/. That's just an output bit but it should be part of the early coding.
C:
// macros are handy for bit-oriented IO
#define SPI_SelectDAC (PORTB.0 = 0)
#define SPI_deSelectDAC (PORTB.0 = 1)

#define SPI_SelectBAROMETER (PORTB.1 = 0)
#define SPI_SelectBAROMETER (PORTB.1 = 1)

// and so on..
3) That's all you need to implement a basic SPI link. It inits, it selects a slave, it reads, it writes. That's it. You use these primitives to make bigger messages:
C:
// send 2 bytes to the DAC
SPI_SelectDAC;
SPI_TXRX(0x12);  // send 1234h to DAC
SPI_TXRX(0x34);
SPI_deSelectDAC;
That's not as useful as it could be. See next post.
 
Last edited:

JohnInTX

Joined Jun 26, 2012
4,787
Once you have the low level SPI stuff written and working, you raise your thinking to the device level. What data does the device need and what is the best way to represent that data?
Consider the DAC as a device that outputs a voltage proportional to the value you write to it. At this level, it would be nice just to specify the value and have a routine take care of the rest. The DAC has two channels so you have to specify that as well.
C:
// MCP4922 driver.  Writes the value to the selected DAC channel: 0=channel A, else=channel B
//Value is 0-4095 for the 12 bit DAC.

void setDAC(unsigned char channel, unsigned int value)
{
   value &= 0x0FFF;  // clear the DAC command bits (upper 4 bits of value)
   value |=  0x7000; // append control bits for channel A, 1x gain, output ON
   if(channel)
   value |= 0x8000; // if channel B, set that bit

   // value now has 12bit DAC value plus command bits
  SPI_SelectDAC;
  SPI_TXRX((unsigned char) value >> 8);  // send upper value byte and command bits
  SPI_TXRX(unsigned char)  value & 0x00ff); send lower byte
  SPI_deSelectDAC;
}
With this, you can write the DAC with one statement:
C:
setDAC(1, 0x800);  // set channel B to 800h
setDAC(0, 0x40);  //  set channel A to 040h
You can make it easier yet. If you are interested in the voltage output, write a routine that converts voltage to DAC counts and send that:
C:
void DACvolts(unsigned char channel, float volts)
{
  unsigned int value;
  value = convert_volts_to_DAC_value(volts);  // do the math
  setDAC(channel, value);  // done!
}
As you can see, the way to do this stuff is to break the task into levels. Start at the low level hardware then work away from the low level device and more towards what you are actually doing. DACvolts() shows how to convert a friendly voltage specification to low level DAC value. That value is calculated from Vref and the selected gain so the math will vary. Figure that out once and implement the conversion.

The low level SPI routines are the same for all devices - they just move bytes.

You can describe the levels even higher. If your DAC controls RPM of a motor for example, write a routine called setRPM(unsigned char motor_number, unsigned int RPM), do the math and use those lower level routines.

For other devices, write the interface routine to again take the data in a useful format. For example, the barometer has a set of registers. Consider a struct that describes the registers. Read and write the struct by converting the SPI bytes to something useful. Write top-level routines that get specific information i.e. getBARO_altitude, getBARO_temperature etc.

To summarize: Always represent the data in a format useful to the program. Write intermediate-level routines (like DACvolts() above) that convert the useful format to raw SPI then use the low level SPI routines to move the bytes.

That's how it's done.


Good luck!
 
Last edited:

Thread Starter

Gajyamadake

Joined Oct 9, 2019
310
To summarize: Always represent the data in a format useful to the program. Write intermediate-level routines (like DACvolts() above) that convert the useful format to raw SPI then use the low level SPI routines to move the bytes.

Good luck!
your description is detailed and very accurate, It will be useful for many people

I am sorry but I am going to ask some different questions, that is not related to thread titles I hope you will be able to explain t

We always need to store the data before transferring it from micro-controller to slave. I have seen that we always store the data in the main function, and after that we pass that data to the other function

Remember that I am not talking about any protocol

single write byte
Code:
  void main ()
{
        char buffer = 0x12;
        Sendslave (char buffer)
 }
This is the function that first stores the value 0x12 and then passes this value.

1. If I have eeprom ( work on spi/i2c) and I want to send 0x12 to eeprom, then I use universal main function

2. If I have sensor (work spi/i2c) and I want to send 0x12 to sensor, then I can use universal main function

3. If I have ADC (work spi/i2c) and I want to send 0x12 to ADC, then I can use universal main function

My doubt is that we can send data to the eeprom from micro-controller but in point 2 and 3 How can we send data from microcontroler to sensor and adc , both device are not writable?

If you do not understand my question, please feel free to ask me anything
 

JohnInTX

Joined Jun 26, 2012
4,787
If you use the SPI MSSP peripheral, you will ALWAYS send and receive bytes at the same time because that is how SPI works. But what you do with the data DOES depend on the slave. There are 3 possibilities:
1) the slave supports full duplex communication i.e. it sends and receives at the same time. In that case you use a function like this. You call it with the byte to send and you store the byte received.
C:
// sends one byte, returns byte received
unsigned char SPI_TXRX(unsigned char TXchar)
{
  unsigned char RXchar;
  while (!BF);  // wait for open buffer
  RXchar = SSPBUF;  // read returned char if interested
  SSPBUF = TXchar; // send new character
  return RXchar;
}
2) If you are just sending data, you STILL get a byte back, even if MISO is not connected (like the DAC). In that case you can STILL use SPI_TXRX. You call it with the byte to send and ignore the byte received.

3) If you are just reading data, you STILL have to write something, even if MOSI is not connected. In that case, you just call SPI_TXRX with a dummy byte (any value) to send and store the byte returned.

When you bit bang to a specific type of peripheral you can omit what you don't need. For example, the DAC only requires a clock and input data (from MOSI) so you don't have to worry about a dummy byte to send. But for standard SPI hardware, you always will send AND receive on a byte by byte basis. Whether you send dummy bytes or throw away the bytes received depends on the specific peripheral you are talking to.

Keep in mind that many peripherals require sending and receiving at different points in their operation. For example, to read an SPI EEPROM, you still have to write the address you want to access then read the data. When you write the address you will ignore the returned data and when reading bytes, you'll send dummy bytes.

SO.. I always use a routine like SPI_TXRX() and use the data in and out as appropriate.

Good luck!
 
Last edited:
Top