Does everyone seriously write their own I2C library for PIC MCUs? - Page 1
Forum section: electronics / general

author: tinfever
author_rank: Regular Contributor
timestamp: February 20, 2022, 03:30:27 am
content: I'm working on a Dyson BMS which has a PIC16LF1847 that talks to a ISL94208 over I2C. Initially I thought it would be relatively easy to use MPLABX and MCC to get I2C communications working using the generated libraries. And it was...unless there are any glitches on the I2C bus, in which case the MCU will get stuck in a loop inside the library and effectively lock up until I reset it. The MCU is indicating the Bus Collision Interrupt Flag bit, but the generated libraries don't ever check for this and so it gets stuck in an infinite loop inside I2C1_MasterWaitForEvent().I've found many Microchip forum posts going back several years discussing how the generated libraries apparently have no way to handle errors, can be fragile, and are extremely complicated to say the least. Oh, and it's all undocumented.https://www.microchip.com/forums/m1192537.aspxhttps://www.microchip.com/forums/m1047846.aspxThe user Mysil in the above links discusses some modifications you can make to the generated libraries to prevent them from getting stuck like this. I've tested these and they do fix my issue, so at least it doesn't lock up, but instead I2C reads will just return 0 instead of an actual response since there is still no mechanism for error handling. Or if there is a mechanism for error handling, it has something to do with undocumented callback functions that are like four layers deep and are completely over my head.I'm sure I'm not the first person to encounter this type of issue. What do most people end up doing? I can't find an alternative PIC I2C library that acts as a defacto standard, like could exist. Does everyone have their own personally written PIC I2C library that they wrote once and now use in all their projects? Or are many people using the MCC libraries and this why all electronics require you to turn them off and on again to start working again: because the world is cursed with devices getting stuck in I2C library loops.

author: jpanhalt
author_rank: Super Contributor
timestamp: February 20, 2022, 10:59:15 am
content: In answer to the title, yes, I wrote my own "libraries" for I2C using that generation of PIC's with the hardware MSSP. I also include error detection and remediation. They are in Assembly.I suggest opening the library and adding error handling routine(s).

author: Siwastaja
author_rank: Super Contributor
timestamp: February 20, 2022, 11:26:32 am
content: Many of us just.... use the peripherals. Depending on their complexity, either just in-situ, maybe through simple helper functions, or maybe a separatecompilation unit(wow, its own .c file!)Very rarely you would go so far as making it an actual library. Practically, this would mean a stable, documented interface, and library object file to be linked in the project.The problem is, the more you abstract the parts away from each other, the more you need to standardize the interface, and as a result, give up on features, reduce flexibility, choose some preferred use case like "write this buffer to the device" not being able to "write one byte when it comes available". Then the actual application code might end up more complex as it now needs to work around the non-fitting library interface.Use libraries with truly complex things which can act like black boxes. I2C peripheral should not be complex.

author: woofy
author_rank: Frequent Contributor
timestamp: February 20, 2022, 11:37:03 am
content: With one exception, getting the USB stack up and running, I only ever use MCC to double check my understanding of various peripheral setups.I2C, SPI, UART Et Al. I write my own code. MCC code is always going to be simplistic and will very likely need modifying to suit your application anyway, so you may as well dive in the deep end to start with and learn the peripheral.

author: DavidAlfa
author_rank: Super Contributor
timestamp: February 20, 2022, 01:21:43 pm
content: The documentation is basically in "Header files / MCC Generated Files / i2c_master.h"But the good part is in the example generated by MCC at "Source files / MCC Generated Files / examples / i2c1_master_example.c".It explains very well how to use it and has the functions you're really interested in.After 5 minutes of investigation, the basics are dead easy, here're some examples interfacing the ISL94208:Code:/* ISL94208 I2C address (50h) */#define ISL_ADDR 0x50/* ISL94208 register addresses (00h-08h) */enum{ISL_CONFIG_OP_STATUS = 0,ISL_OPERATION_STATUS,ISL_CELL_BALANCE,ISL_ANALOG_OUT,ISL_FET_CONTROL,ISL_DISCHARGE_SET,ISL_CHARGE_SET,ISL_FEATURE_SET,ISL_WRITE_ENABLE,}isl94208_regs;/*Main application*/void main(void){// initialize the deviceSYSTEM_Initialize();// When using interrupts, you need to set the Global and Peripheral Interrupt Enable bits// Use the following macros to:// Enable the Global Interrupts//INTERRUPT_GlobalInterruptEnable();// Enable the Peripheral Interrupts//INTERRUPT_PeripheralInterruptEnable();// Disable the Global Interrupts//INTERRUPT_GlobalInterruptDisable();// Disable the Peripheral Interrupts//INTERRUPT_PeripheralInterruptDisable();/* Working variable */uint8_t data;/* Read STATUS register */data = I2C1_Read1ByteRegister(ISL_ADDR, ISL_OPERATION_STATUS);/* Read ANALOG OUT CONTROL register */data = I2C1_Read1ByteRegister(ISL_ADDR, ISL_ANALOG_OUT);/*Write to ANALOG OUT CONTROL register, select internal temp voltage at analog pin A0Mask (Preseve) UFLG1 and UFLG0 general purpose flag registers (backed up by the ISL), also the reserved bits.*/data = (data & 0xF0) | 0b1001;I2C1_Write1ByteRegister(ISL_ADDR, ISL_ANALOG_OUT, data);/* Now temperature internal signal can be measured at A0 pin. *//* Write to ANALOG OUT CONTROL register, select CELL1 voltage at analog pin A0 */data = (data & 0xF0) | 0b0001;I2C1_Write1ByteRegister(ISL_ADDR, ISL_ANALOG_OUT, data);/* Now CELL1 signal can be measured at A0 pin. *//* Write to ANALOG OUT CONTROL register, disable analog pin A0 to save power */data = (data & 0xF0);I2C1_Write1ByteRegister(ISL_ADDR, ISL_ANALOG_OUT, data);/* Write to CELL BALANCE CONTROL register, turn off all FETs except for CELL5 */data = 0b01000000;I2C1_Write1ByteRegister(ISL_ADDR, ISL_FET_CONTROL, data);/* Write to CELL BALANCE CONTROL register, turn off all FETs */data = 0;I2C1_Write1ByteRegister(ISL_ADDR, ISL_FET_CONTROL, data);while (1){// Add your application code}}Attached the sample project.Be very careful! You're interfacing a fully working system, connected to the cells, etc.Writing wrong values could damage the controller, ex. enabling the fets in the V7(?) which has no limiting resistors.Or you could use any existing i2c library like this one:https://github.com/Apress/programming-pic-microcontrollers-w-xc8

author: BradC
author_rank: Super Contributor
timestamp: February 20, 2022, 02:33:07 pm
content: I find it faster and easier to write my own “libraries” where I’m likely to use a peripheral across many projects than to try and make someone else’s opaque spaghetti actually meet all my requirements (reliability first, all else second). I just don’t like other peoples code, and I suspect if I were ever stupid enough to publish mine others would fill barf bags.

author: DavidAlfa
author_rank: Super Contributor
timestamp: February 20, 2022, 03:33:38 pm
content: That's usually true when using simple mcus like PICs, where all you have is 3 registers and 5 flags.However some are a lot more complex (ex. ARM), requiring hours of research in the RM, debugging, bug fixing....Easy to interface libraries are timesavers, can greatly boost the development, at least in early stages where you're testing small parts or blocks.Polishing and optimizing is usually the most expensive part, so if your design works with the provided libraries, you're done!No need to optimize unless you're running out of resources.

author: JPortici
author_rank: Super Contributor
timestamp: February 20, 2022, 05:55:34 pm
content: "Does everyone seriously write their own I2C library"Yes. Different flavors of them, depending of what i have to interface to.I write my library for every peripheral, except for USBMCC in partigular create a godawful mess but generic libraries are never good at anything

author: hans
author_rank: Super Contributor
timestamp: February 20, 2022, 06:27:46 pm
content: Yes. These kind of serial peripherals only need to have transaction start/stop, transmit, receive and error handling. It's really not much work to set it up, although some parts of an implementation can be a bit more fiddly (for example, if you need/want to use a peripherals FIFO, interrupts or DMA).Peripherals like USB and Ethernet are fairly interleaved with mandatory protocols, for example IPv4+UDP/TCP, or USB device enumeration and HID/CDC class. Implementing that is a large amount of work, not only because of the many different kind of packets that need to be handled, but also statemachines which creates an enormous amount of conditions to test (if you were to roll your own).Also reading your start post, you were fortunately able to figure out the peripheral is signalling collision errors, but the library doesn't handle them properly. You could copy the library code to your project and modify it accordingly. Alternatively write your own from scratch, since you have already spent considerable time debugging I suppose. I wouldn't stick to vendor libraries forever, especially in cases where they start to slow you down.

author: DavidAlfa
author_rank: Super Contributor
timestamp: February 20, 2022, 07:53:13 pm
content: Had the 3rd covid shot yesterday, I got it in the very upper part of shoulder, any minimal movement hurts like hell, can't even scratch my head while looking at the codeSo, I had a lot of free time today.I ported and improved the i2c code from that github repo.Tested it reading a 24C04, worked perfectly.Edit: v4- Automatically disable the code belonging to SSP, SSP1 or SSP2 modules if not present in the PIC device- Allow disabling sections manually when the modules aren't going to be used for i2c, saving space.- Ensure the user isn't selecting SSP when it should be SSP1, which is possible due legacy reasons and might cause issues.- Error reporting and handling.- Common code optimizations- Enhanced functions, use pointers and sizes for flexible data read/write operations.

author: tinfever
author_rank: Regular Contributor
timestamp: February 21, 2022, 05:41:05 pm
content: Thank you all for the valuable feedback! And thank you to DavidAlfa for even going so far as to do all the work for me!In the interest of learning and practicing what I'd need to do for any future MCU projects, I'm going to have a go at implementing it myself. Then I'll compare what I've created with DavidAlfa's implementation and see where I have room for improvement.Thanks again!

author: DavidAlfa
author_rank: Super Contributor
timestamp: February 21, 2022, 05:56:35 pm
content: You don't really have to break your head doing it all if there's a nice library already hanging around.You can learn the same or better by checking how it works!After spending hours or even days working in the library, reading the datasheet whole, then you'll start using the high-level functions, quickly forgetting the deepest technical details like registers, bits, flags...Keep the good work, don't hesitate to ask when struggling.What are your plans with the firmware?I guess the original fw monitors the voltage at ISL 's A0 pin, switching between the different mux options.And it probably fails because some cell is unbalanced.BTW, I had few mistake in the examples:- Cell balance fets are controlled in CELL_BALANCE register, not in FET_CONTROL.- Reserved bits must be always written as 0.- Add FET_CONTROL example.Code:/* Read STATUS register */debug_i2c_result( I2C1_ReadMemory(ISL_ADDR, ISL_OPERATION_STATUS, &data, 1) );/* Read ANALOG OUT CONTROL register */debug_i2c_result( I2C1_ReadMemory(ISL_ADDR, ISL_ANALOG_OUT, &data, 1) );/*Write to ANALOG OUT CONTROL register, select internal temp voltage at analog pin A0Preserve UFLG1 and UFLG0 general purpose flag registers (backed up by the ISL)*/data = (data & 0xC0) | 0b1001;debug_i2c_result( I2C1_WriteMemory(ISL_ADDR, ISL_ANALOG_OUT, &data, 1) );/* Now temperature internal signal can be measured at A0 pin. *//* Write to ANALOG OUT CONTROL register, select CELL1 voltage at analog pin A0 */data = (data & 0xC0) | 0b0001;debug_i2c_result( I2C1_WriteMemory(ISL_ADDR, ISL_ANALOG_OUT, &data, 1) );/* Now CELL1 signal can be measured at A0 pin. *//* Write to ANALOG OUT CONTROL register, disable analog pin A0 to save power */data = (data & 0xC0);debug_i2c_result( I2C1_WriteMemory(ISL_ADDR, ISL_ANALOG_OUT, &data, 1) );/* Write to CELL BALANCE CONTROL register, turn off all FETs except for CELL5 */data = 0b01000000;debug_i2c_result( I2C1_WriteMemory(ISL_ADDR, ISL_CELL_BALANCE, &data, 1) );/* Write to CELL BALANCE CONTROL register, turn off all FETs */data = 0;debug_i2c_result( I2C1_WriteMemory(ISL_ADDR, ISL_CELL_BALANCE, &data, 1) );/* Read FET CONTROL register */debug_i2c_result( I2C1_ReadMemory(ISL_ADDR, ISL_FET_CONTROL, &data, 1) );/* Write FET CONTROL register, turn on CHARGE & DISCHARGE FETS */data = data | 0b11;debug_i2c_result( I2C1_WriteMemory(ISL_ADDR, ISL_FET_CONTROL, &data, 1) );

author: SiliconWizard
author_rank: Super Contributor
timestamp: February 21, 2022, 06:25:42 pm
content: I've actually used PIC MCUs quite a bit in the past (from 16F to 18F to the 16-bit dsPIC and PIC24, to PIC32) and have never ever used vendor libraries with them, only the base headers for register definitions.And frankly, in particular for PIC 8-bitters, those are so simple and (usually) well documented that programming them "baremetal" is pretty straightforward. (And never found Microchip's "peripheral libraries" neither really good nor really relevant.)Now for more modern and complex MCUs, this can be a slightly different matter (even though that's still doable.)But for a PIC16F/16LF? It's a no-brainer IMHO.

author: tinfever
author_rank: Regular Contributor
timestamp: February 21, 2022, 09:10:45 pm
content: Quote from: DavidAlfa on February 21, 2022, 05:56:35 pmOkay. You're right haha. I'll stop being stubborn and use the nice library you wrote.This project has already taken longer than I'd wanted so there's no reason to just read through and understand what you've created, and then just run with that. I can do the hard learning and writing from scratch some other time when it's actually required.As far as plans with the firmware, umm... I started making a flow chart that has one item on it so far haha. I currently have various ideas of what functions will be needed but have yet to coalesce them in to something concrete. It's a work in progress but I'll definitely keep posting with my progress in the Dyson battery pack thread you've been in.

author: tinfever
author_rank: Regular Contributor
timestamp: February 25, 2022, 03:27:09 am
content: Quote from: DavidAlfa on February 21, 2022, 05:56:35 pmI've been working with your I2C library (thanks again!) and wanted to suggest some additional error handling mechanisms for inclusion. Also, have you considered uploading this to GitHub so others can find it?I added two more error status modes in i2c.hCode:typedef enum{I2C_OK = 0,I2C_NO_ACK,I2C_COLLISION,I2C_OVERFLOW,I2C_BUS_COLLISION_IF,I2C_TIMEOUT} i2c_result_t;Then I added the following function in i2c.c. This is primarily to check if BCL1IF has been set because I've found that if this is set, SSP1IF will never be set and the program can get stuck in a loop. I've also managed to induce an error where none of the flags will ever be set so I added a timeout routine.:Code:/* Initialize timeout variable */uint8_t timeout_counter = 0;/***************************************************************************************Check for error conditions that preclude SSPIF from being set***************************************************************************************/bool I2C1_CheckErrorCondition(){if(SSP1CON2bits.ACKSTAT){return true;}if(SSP1CON1bits.WCOL){return true;}if(SSP1CON1bits.SSPOV){return true;}if (PIR2bits.BCL1IF){return true;}if (timeout_counter > 250){return true;}timeout_counter++; //Approximately 117 cycles between counter increments = ~914uS timeout period at 32MHz clockreturn false;}Then on all of the functions that wait for SSP1IF, I modified like the following, where I added the line to reset timeout_counter and then added the "&& !I2C1_CheckErrorCondition" condition to the while loop. I'm only showing _I2C1_Write below but any function that had "while(!PIR1bits.SSP1IF); "was modified.Code:static void _I2C1_Write(unsigned char data){PIR1bits.SSP1IF=0; // clear SSP interrupt bitSSP1BUF = data; // send datatimeout_counter = 0;while(!PIR1bits.SSP1IF && !I2C1_CheckErrorCondition()); // Wait for interrupt flag to go high indicating transmission is complete or for an error condition to become present}Finally, I added options to return the two newly created error modes:Code:static i2c_result_t _I2C1_GetError(void){if(SSP1CON2bits.ACKSTAT){_I2C1_StopBit();return I2C_NO_ACK;}if(SSP1CON1bits.WCOL){SSP1CON1bits.WCOL = 0;_I2C1_StopBit();return I2C_COLLISION;}if(SSP1CON1bits.SSPOV){SSP1CON1bits.SSPOV = 0;_I2C1_StopBit();return I2C_OVERFLOW;}if (PIR2bits.BCL1IF){PIR2bits.BCL1IF = 0;_I2C1_StopBit();return I2C_BUS_COLLISION_IF;}if (timeout_counter > 250){_I2C1_StopBit();return I2C_TIMEOUT;}return I2C_OK;}One thing you could also consider adding is a function to manually clear the I2C bus. I've encountered the slave holding SDA low after the PIC is reset, and then the PIC throws an error because it doesn't look like the I2C bus is idle while the slave is just waiting for a clock. Here's what I've done inside my main.c but I'm sure there is room for improvement.Code:#define SDA RB1#define SCL RB4#define TRIS_SDA TRISB1#define TRIS_SCL TRISB4#define ANS_SDA ANSB1#define ANS_SCL ANSB4void ClearI2CBus(){uint8_t initialState[] = {TRIS_SDA, TRIS_SCL, ANS_SDA, ANS_SCL, SDA, SCL, SSP1CON1bits.SSPEN}; //Backup initial pin setup stateSSP1CON1bits.SSPEN = 0; //Disable MSSP if enabledTRIS_SDA = 1; //SDA - Set Data as inputTRIS_SCL = 1; //SCL - Set Clock as input = high-impedance outputANS_SDA = 0; //Set both pins as digitalANS_SCL = 0;SCL = 0; //Set SCL PORT to be low, then we'll toggle TRIS to switch between low and high-impedanceuint8_t validOnes = 0;//Clock the SCL pin until we get 10 valid ones in a row like we'd expect from an idle buswhile (validOnes < 10){TRIS_SCL = 0; //Clock low__delay_us(5); //Wait out clock low periodTRIS_SCL = 1; //Clock high__delay_us(2.5); //Wait until we are in the mid point of clock high periodif (SDA == 1){ //Read data and check if SDA is high (idle)validOnes++;}else{validOnes = 0; //if the data isn't a one, reset the counter so we get 10 in a row}__delay_us(2.5); //Wait remainder of clock high period}TRIS_SDA = (__bit) initialState; //Restore initial pin I/O stateTRIS_SCL = (__bit) initialState;ANS_SDA = (__bit) initialState;ANS_SCL = (__bit) initialState;SDA = (__bit) initialState;SCL = (__bit) initialState;SSP1CON1bits.SSPEN = (__bit) initialState;}}In case you are wondering, the way I'm inducing all these errors is to randomly ground the SCL or SDA lines on the PCB while the program is repeatedly reading over I2C. Realistic? Probably not!

author: DavidAlfa
author_rank: Super Contributor
timestamp: February 25, 2022, 08:15:43 am
content: I won't because I'm not using neither I want to maintain it.Consider doing that yourself, you're testing and enhancing the library

author: tinfever
author_rank: Regular Contributor
timestamp: February 25, 2022, 07:00:13 pm
content: Quote from: DavidAlfa on February 25, 2022, 08:15:43 amTotally understand. No problem.I might do this and if I do, I'll post a link in this thread.

author: DavidAlfa
author_rank: Super Contributor
timestamp: February 25, 2022, 10:22:44 pm
content: Use it as you like, I had some entertaiment while coding/testing it during the weekend, I hope it's useful to you and others, but that's all, yeah, it would be great that you share the library once done.