I2C on PIC - Page 1
Forum section: electronics / beginners

author: Vodonik
author_rank: Newbie
timestamp: November 25, 2015, 11:32:02 pm
content: Hello, Im a beginner in electronics. I have experience with physics and programming. My mission, currently, is to design and implement software I2C protocol for general use. Im experimenting with GY-68 barometer with BMP180 sensor. My short term goal is to read a 'check' register from the device, which should always return 0x55. I have tried two different sets of functions i have found online, but none of them worked. I realised I could not get them to work because I had no real understanding of how I2C protocol worked at the lowest level. So I started reading and got somewhere.This is all of the code Im using. I expect to get 0x55, but instead I always get 0xFF.Im not asking for code that works, I want full understanding of the protocol, because Im planning to use it alot in the future. Its late where I am now, so maybe Im not making much sence.Also Im coding in MPLabX, using XC8 compiler and PIC16f628A chip.I will answer any questions, Im really looking forward to help in solving this.Code:#include <xc.h>// #pragma config statements should precede project file includes.// Use project enums instead of #define for ON and OFF.// CONFIG#pragma config FOSC = INTOSCCLK // Oscillator Selection bits (INTOSC oscillator: CLKOUT function on RA6/OSC2/CLKOUT pin, I/O function on RA7/OSC1/CLKIN)#pragma config WDTE = OFF // Watchdog Timer Enable bit (WDT disabled)#pragma config PWRTE = ON // Power-up Timer Enable bit (PWRT enabled)#pragma config MCLRE = OFF // RA5/MCLR/VPP Pin Function Select bit (RA5/MCLR/VPP pin function is digital input, MCLR internally tied to VDD)#pragma config BOREN = OFF // Brown-out Detect Enable bit (BOD disabled)#pragma config LVP = OFF // Low-Voltage Programming Enable bit (RB4/PGM pin has PGM function, low-voltage programming enabled)#pragma config CPD = OFF // Data EE Memory Code Protection bit (Data memory code protection off)#pragma config CP = OFF // Flash Program Memory Code Protection bit (Code protection off)#define LED1def TRISB1#define LED1 RB1#define LED2def TRISB2#define LED2 RB2#define SDA RB4#define SDA_IN TRISB4#define SCL RB5#define SCL_IN TRISB5#define _XTAL_FREQ 4000000void delay_ms(int milliseconds){while(milliseconds > 0){__delay_ms(1);milliseconds--;}}void Start(void){SDA = 1;delay_ms(1);SCL = 1;delay_ms(1);SDA = 0;delay_ms(1);SCL = 0;delay_ms(1);}void SendWriteAddress(void){unsigned char writeAddress = 0xEE;for (char i = 0; i < 8; i++){if (writeAddress & 0x80)SDA = 1;elseSDA = 0;delay_ms(1);SCL = 1;delay_ms(1);SCL = 0;delay_ms(1);writeAddress <<= 1;}}void SendRegisterAddress(void){unsigned char registerAddress = 0xD0;for (char i = 0; i < 8; i++){if (registerAddress & 0x80)SDA = 1;elseSDA = 0;delay_ms(1);SCL = 1;delay_ms(1);SCL = 0;delay_ms(1);registerAddress <<= 1;}}void SendReadAddress(void){unsigned char readAddress = 0xD0;for (char i = 0; i < 8; i++){if (readAddress & 0x80)SDA = 1;elseSDA = 0;delay_ms(1);SCL = 1;delay_ms(1);SCL = 0;delay_ms(1);readAddress <<= 1;}}unsigned char RecieveByte(void){unsigned char recievedData = 0x00;char binaryData = {0};SDA = 0;SDA_IN = 1;delay_ms(10); //Just in case.for (char i = 0; i < 8; i++){SCL = 1;while (!SCL){delay_ms(1);}binaryData = SDA;delay_ms(1);SCL = 0;}delay_ms(10);SDA_IN = 0;//Convert binary to hex:for (int i = 0; i < 8; i++){recievedData <<= 1;if (binaryData == 1)recievedData |= 1;}return recievedData;}void Stop(void){SCL = 1;delay_ms(1);SDA = 1;}void main(void){LED1def = 0;LED1 = 0;LED2def = 0;LED2 = 0;SDA_IN = 0;SCL_IN = 0;SDA = 1;SCL = 1;delay_ms(10);unsigned char testVariable = 0xCC;Start();SendWriteAddress();SendRegisterAddress();Start();SendReadAddress();testVariable = RecieveByte();Stop();while(1){for (char i = 0; i < 8; i++){if (testVariable & 0x80){LED1 = 1;delay_ms(100);LED1 = 0;delay_ms(900);}else{LED2 = 1;delay_ms(100);LED2 = 0;delay_ms(900);}testVariable <<= 1;}delay_ms(2000);}}

author: MarkF
author_rank: Super Contributor
timestamp: November 26, 2015, 01:12:29 am
content: May I ask why you want to do this is software? The PIC has hardware I2C and it would perform faster and better.I have only used the SPI protocol and don't have experience with the I2C protocol. Although I'm interesting in trying I2C. May I suggest you spend a lot of time looking over the timing diagrams and the I2C description in the specification for the PIC you're using. From a few problems selecting the correct SPI mode for some devices, your scope and the timing diagrams will be your best friend.I'm a EE with 35 years of software development. Don't worry about size. Just code it as simple and straight forward as possible. Test with your scope as you go (especially since you're trying to do the timing in software). Also double check your data is packed/unpacked correctly and that the bit order to/from the hardware is correct (MSB or LSB is first).Good luck

author: Maxlor
author_rank: Frequent Contributor
timestamp: November 26, 2015, 01:20:01 am
content: In SendReadAddress, you use the wrong address: you wrote the register address there (0xD0), probably because you copy/pasted, but you should be using the device address with read bit set (0xEF).I have to agree with MarkF here, the PICs have I2C hardware, which is easier to use and more robust than bit-banging, even though you seem to be doing a good job, taking into account clock stretching and so on. And using the peripheral, it runs much faster. All the delays can cause problems, because there are I2C slaves that reset if no valid data is seen on the bus for a while (one device I've worked with had a reset after 20ms.) The BMP180 isn't one of them though, I think.

author: Maxlor
author_rank: Frequent Contributor
timestamp: November 26, 2015, 01:39:04 am
content: Here's an example of how to use the I2C peripheral on a PIC. This was written for an PIC16F1822, some details (like available status bits) might be different with the chip you use, but the general concept should be the same. The function supports write-restart-read, which is what you seem to need. The crucial thing to note is that the PIC notices when you write SSP1BUF, and starts sending automatically. It tells you when it's done by setting status bits, so in the code I wait for those bits to be set using the various while loops. If busy-looping isn't your thing, you could also configure interrupts, so you can do other stuff while the chip is communicating. You should never use fixed delays in code like this (with a few exception, like when a datasheet tells you that you need to wait 10ms after startup, or 750ms after sending a "measure" command before reading the result), because that isn't fast, reliable nor maintainable.Code:void i2c_init() {// Configure RA1/SCL and RA2/SDA as digital inputsTRISAbits.TRISA1 = 1;TRISAbits.TRISA2 = 1;ANSELAbits.ANSA1 = 0;ANSELAbits.ANSA2 = 0;// configure MSSP module for I2CSSP1STAT = 0b10000000; // disable slew rate control and SMBus compatibilitySSP1CON1 = 0b00111000; // enable and configure module for I2CSSP1ADD = 0; // should result in 100kHz I2C clock with 500kHz oscillator}uint8_t i2c_writeRead(uint8_t address, uint8_t *wData, uint8_t wLen, uint8_t *rData, uint8_t rLen) {if ((wLen | rLen) == 0) { return 1; }uint8_t success = 0;address <<= 1;if (wLen) {SSP1CON2bits.SEN = 1; // send startwhile (SSP1STATbits.S);SSP1BUF = address; // address and write bitwhile (SSP1STATbits.BF);if (SSP1CON2bits.ACKSTAT) { goto end; }do {SSP1BUF = *wData;++wData; --wLen;while (SSP1STATbits.BF);if (SSP1CON2bits.ACKSTAT) { goto end; }} while (wLen);}if (rLen) {if (wData) {SSP1CON2bits.RSEN = 1; // send restartwhile (SSP1CON2bits.RSEN);} else {SSP1CON2bits.SEN = 1; // send startwhile (SSP1STATbits.S);}SSP1BUF = address | 1; // address and read bitwhile (SSP1STATbits.BF);if (SSP1CON2bits.ACKSTAT) { goto end; }do {SSP1CON2bits.RCEN = 1; // receivewhile (SSP1CON2bits.RCEN);*rData++ = SSP1BUF;--rLen;// send ACK (or NACK for last byte)if (rLen) { SSP1CON2bits.ACKDT = 0; }else { SSP1CON2bits.ACKDT = 1; }SSP1CON2bits.ACKEN = 1;while (SSP1CON2bits.ACKEN);} while (rLen);}success = 1;end:SSP1CON2bits.PEN = 1;while (SSP1CON2bits.PEN);return success;}

author: Vodonik
author_rank: Newbie
timestamp: November 26, 2015, 05:47:11 pm
content: Hi guys!The main reason im using bit-banging, my favorite phrase from now on, is my PIC does not support hardware I2C.Do you have any idea why my code is not working? The address in SendReadAddress function address is only wrong on forum, in my project is ok, and still I only get all ones. I guess its because slave is not responding (not pulling SDA to ground). But I have no idea why or an oscilloscope, so I cant even guess.Or some reading material for this kind of situation?

author: kwass
author_rank: Frequent Contributor
timestamp: November 26, 2015, 06:58:43 pm
content: QuoteMicrochip has an application note, with flowcharts and C source code for exactly what you're trying to do:http://www.microchip.com/wwwAppNotes/AppNotes.aspx?appnote=en560799

author: Maxlor
author_rank: Frequent Contributor
timestamp: November 26, 2015, 09:11:20 pm
content: Quote from: Vodonik on November 26, 2015, 05:47:11 pmOh wow, didn't realize that. That must be a really old one...So I found the oldest PIC I had lying around, a PIC16F88, and tried your code. Upon a second look, and with some help from the scope, it's pretty obvious what's wrong: you're not receiving the Ack/Nack bit from your slave after writing a byte. What you need to do is immediately (no waiting!) after driving SCL low, switch SDA no read mode, drive SCL high (clock stretching can happen here, pretty common actually), read SDA, drive SCL low, switch SDA back to output.Btw: I assume you're driving SDA and SCL using a mosfet, right? Those lines should be configured as open drain outputs with external pull-up. If you actively drive them high, you'll get shorts in clock stretching situations. If you do not use a mosfet, the right thing to do is to configure them as output and drive them low for 0, but switch them to input (high-Z!) and let the pull-up resistor pull them high for 1.Oh, and one more thing that me scratching my head for a while: these old chips lack an internal pull-up on !MCLR. You need to add an external one, otherwise you'll get random resets, or the chip doesn't come out of reset after programming.

author: Vodonik
author_rank: Newbie
timestamp: November 26, 2015, 11:07:42 pm
content: Thanks kwass. Just skimed over for now, looks good I like flowcharts, charts in general.Will read over the weekend.Maxlor...Thanks man, its working now. Its really a core basic of the protocol what I was missing. Makes me feel kinda bummed for not figuring it out myself.Now the really fun part begins. My next short term goal is to get a temperature reading.

author: FrankBuss
author_rank: Unknown
timestamp: November 26, 2015, 11:38:55 pm
content: Quote from: Maxlor on November 26, 2015, 09:11:20 pmDo you have an example which devices use clock stretching? I've used a number of different I2C chips, like clock expanders, accelerometers, and temperature sensors, and never saw it on the scope.@Vodonik: How did you connect it? You should use two external pullup resistors if you want to use it with 100 kHz (something which provides about 1 mA for your supply voltage). But with less than 1 kHz speed with your current code the internal pullups of the PIC might be sufficient. But note that theI2C specificationrequires a max fall time (page 47, 250 ns) and some devices might not work properly, if it is longer. Sometimes devices even hang and can block the I2C bus by pulling low the SDA or SCL line permanently. I fixed this in a product by detecting such hangs and then driving hard a few SCL pulses, which restored the hanging device. I avoid I2C if possible, I had never such problems with SPI devices. BTW: in normal mode for your bitbanging code, you shouldn't drive SCL or SDA to 1. Instead set the pin direction to input, if you want 1 and to output (with 0 for the output value), if you want 0.

author: FrankBuss
author_rank: Unknown
timestamp: November 26, 2015, 11:41:34 pm
content: PS: max allowed rise time is important for the pullups, which is 1000 ns (page 48), not the fall time.

author: Maxlor
author_rank: Frequent Contributor
timestamp: November 27, 2015, 01:25:31 am
content: Quote from: FrankBuss on November 26, 2015, 11:38:55 pmYup. The pics below show the communication with an NXP PCF8566 LCD controller, you can see that it needs to slow down after a couple of bytes. Then there's the HTU21D temperature sensor which has a blocking mode, i.e. it stretches the clock while taking the measurement (which takes up to 16ms). You can turn it off there though. I encountered another device this year where the ACK took slightly longer than a regular clock cycle, like 12us vs the normal 10us, but can't remember which one it was anymore. I tested a couple of different environment sensors this year, it'll have been one of them.Edit: Btw, the one place on this damn scope where a tiny font would acutally come in handy (decoder display), they don't use it. Rigol really needs to study usability.

author: Vodonik
author_rank: Newbie
timestamp: November 27, 2015, 10:00:18 pm
content: Quote from: FrankBuss on November 26, 2015, 11:38:55 pmWhy is this? I see that can work, but what is wrong with driving SCL and SDA to 1?Btw. It is currently 22.0 degrees Celsius in my room.Managed to get the temperature reading out of the sensor.

author: FrankBuss
author_rank: Unknown
timestamp: November 28, 2015, 01:28:34 am
content: It is just safe programming, because this is how the bus is designed. For example looks like in your initial code you didn't check for the acknowledge bit, where the I2C slave pulls down the SDA line. If you drive it high at the same time, it could damage your chips (not likely, but unecessary high current). Or there might be a glitch in the slave and it pulls down one or both lines low sometimes (as I wrote, this happens in a project where I wrote the firmware for, would be best in this case to reset or power-cycle the slave). And might reduce EMI a bit and needs a little bit less power.

author: void_error
author_rank: Frequent Contributor
timestamp: November 29, 2015, 12:39:14 am
content: Quote from: MarkF on November 26, 2015, 01:12:29 amThe 16F628A doesn't have a MSSP module so no SPI/I2C.Anyway, why use a 16F628A for I2C in the first place? It's an obsolete piece of junk. I suggest using a 16F886 or similar or one of the newer 16F1xxx chips if you have the patience to go through the datasheet although most basic functions are similar to the '628A.Seriously, bit-banging the I2C bus with a PIC's general purpose I/O's is completely pointless when the are are PICs which have hardware I2C.As an alternative to going through several pages of datasheet you could just use Microchip's Code Configurator plugin if you're using Mplab X.

author: Vodonik
author_rank: Newbie
timestamp: November 29, 2015, 11:47:17 am
content: Im using 16F628A because thats the only one I have.And Im not able to get another one for a week or so, and I didnt want to sit araound waiting doing nothing, plus this way I get to really get into how I2C really works.I looked at 16F886, and that one will probably be my next one, and I will use hardware I2C (on my current pic, I dont even have enough program memory to get all the data from the sensor).

author: trash
author_rank: Contributor
timestamp: August 01, 2019, 07:18:24 am
content: Time to revive this old thread because I notice this problem appears to be a common one and nobody has a satisfactory reason or solution for it.The BMP180 returns 0xFF.So yes, I am bit banging the I2C and no I do not wish to use hardware to do it.This does not solve the issue, it ignores it. As does using libraries.So to describe the problem in more detail. My I2C code works fine. I use it with other devices.I can poll the BMP180. Naturally I have my own bit banging scanner which returns a valid I2C address of 0x77 (which translates to 0xEE/EF)No suprises there. The BMP responds to being addressed correctly with an ACK.If we write EE and then a sequential bytes, the BMP180 responds to each byte with an ACK. So it is receiving data.IF we read EF the BMP180 does not respond. It doesn't pull the SDA line low.So, if I send the following sequence to the BMP180.S EE D0 S EF .. I expect to see 0x55 from the chip IDThe problem is with the BMP180 datasheet.If we look at the READ operation['S] [WRITE 0xEE] {ACKS} [Register address 0xF6] {ACKS}  [READ 0xEF] {ACKS} {ADC result 0x5C}  {ADC result 0x96}  ['P]And we notice that this appears to be correct as per the IC2 standards.http://e2e.ti.com/cfs-file/__key/communityserver-discussions-components-files/166/82111.Capture.PNGThis doesn't work.The restart bit is not a restart, but an ordinary start bit.If we look closely at the level diagram for the BMP180 we can see there is a ['P] stop bit in the streamThe sequence which works is...['S] [WRITE 0xEE] {ACKS} [Register address 0xF6] {ACKS}['P]['S] [READ 0xEF] {ACKS} {ADC result 0x5C}  {ADC result 0x96}  ['P]So if you're get an 0xFF response from a BMP180, then check the I2C is a START bit and not a RESTART.