The digital compensation posts covered all the control aspects of building a switch mode power supply, but did not cover external communication and control. This post will demonstrate how to write dsPIC I2C code, and how to talk to the dsPIC with C#.
I am assuming:
Therefore, there is just enough information for you to understand the intent of each piece of code, and you can use some grey matter to implement your own. But if you are totally lost on something, please don’t hesitate to ask. My assumptions are not always accurate.
If you follow the examples, you should be able to get an I2C working in an afternoon with minimal effort, and it should be reliable.
Before you layout your board, there are a couple of things you need to know. The dsPIC can be programmed from two different sets of pins:
PGEC1/PGED1 or PGEC2/PGED2
The I2C uses pins SDA and SCL, which are shared with PGEC1/PGED2.
If you program the dsPIC with PGEC1/PGED2, you will have to remove your I2C dongle or drive circuit when you program the dsPIC. The I2C can talk when the programmers is connected, but not the other way around.
The I2C has to be enabled in your C Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | void InitI2C(void) { PMD1bits.I2C1MD = 0; // Enable I2C module // Setup as slave I2C1CONbits.I2CEN = 1; // Enable I2C I2C1CONbits.DISSLW = 0; // Slew rate control enabled I2C1CONbits.SMEN = 0; // No SMB levels I2C1CONbits.GCEN = 0; // Allow general call interrupt I2C1CONbits.ACKDT = 0; // ACK/ I2C1CONbits.ACKEN = 1; // Enable ACK I2C1CONbits.STREN = 1; // Stretch Enabled I2C1ADD = 0x08; // Set the slave address I2C1STAT = 0x0000; // Clear stat register // Setup the baud rate I2C1BRG = 0x18B; // 100Khz baud rate // Setup the interrupts IPC4bits.SI2C1IP = 5; // Interrupt priority _SI2C1IF = 0; // I2C Interrupt Flag Status bit is cleared _SI2C1IE = 1; // I2C Interrupt Enable } |
The key things to note is you need to set the slave address, enable interrupts, and set the interrupt priority. The priority must be lower than the control loop interrupt so as not to disrupt the PWM. I initially set the control loop as the highest priority (1), followed by timer/ramping interrupts (3), and put I2C (5) at the lowest. However, this was causing the I2C to lockup randomly. The fix was to set the timer/ramp interrupt priority at 5. This means the I2C can disrupt a ramp.
A second key thing you must pay attention to is how much time is available from the dsPIC to process the I2C calls. The control loop typically uses at least 50% of the processor. Because it is higher priority, the I2C only gets leftovers for dinner. You can get timeouts if there is too much I2C activity or not enough processor time.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | void __attribute__((__interrupt__, no_auto_psv)) _SI2C1Interrupt() { if (I2CSTATbits.I2COV) { I2CSTATbits.I2COV = 0; I2CSTATbits.RBF = 0; I2CDataPos = 0; // Reset the recieve process due to overflow. I2CCONbits.SCLREL = 1; // Release clock. } else if (I2CSTATbits.IWCOL) { I2CSTATbits.IWCOL = 0; I2CCONbits.SCLREL = 1; // Release clock. } // State 1: Write Address ready else if (I2CSTATbits.S == 1 && I2CSTATbits.R_W == 0 && I2CSTATbits.D_A == 0 && I2CSTATbits.RBF == 1) { I2CState = 1; I2CAddress = I2CRCV; I2CDataPos = 0; I2CCONbits.SCLREL = 1; // Release clock. } // State 2: Write Data ready else if (I2CSTATbits.S == 1 && I2CSTATbits.R_W == 0 && I2CSTATbits.D_A == 1 && I2CSTATbits.RBF == 1) { I2CState = 2; if (I2CDataPos == 0) I2CAddress = I2CRCV; else if (I2CDataPos == 1) I2CData0 = I2CRCV; else if (I2CDataPos == 2) I2CData1 = I2CRCV; else if (I2CDataPos == 3) I2CData2 = I2CRCV; else if (I2CDataPos == 4) I2CData3 = I2CRCV; else if (I2CDataPos == 5) I2CData4 = I2CRCV; else if (I2CDataPos == 6) I2CData5 = I2CRCV; else if (I2CDataPos == 7) I2CData6 = I2CRCV; else if (I2CDataPos == 8) I2CData7 = I2CRCV; else I2CJunkData = I2CRCV; // Guarentee all data is read even with bugs. if (I2CAddress == SET_K1_ADR && I2CDataPos == 2) ConstTable[0] = I2CData0 << 8 | I2CData1; else if (I2CAddress == SET_K2_ADR && I2CDataPos == 2) ConstTable[1] = I2CData0 << 8 | I2CData1; else if (I2CAddress == SET_K3_ADR && I2CDataPos == 2) ConstTable[2] = I2CData0 << 8 | I2CData1; else if (I2CAddress == SET_GAIN_ADR && I2CDataPos == 2) Gain = I2CData0 << 8 | I2CData1; else if (I2CAddress == SET_COLLECT_DATA && I2CDataPos == 1) { // Works as transaction with collection, but there is no // protection against reading data too soon. DataPosition = 0; DataReady = FALSE; CollectData = TRUE; } else if (I2CAddress == SET_DATA_POSITION && I2CDataPos == 2) DataPosition = (I2CData0 << 8 | I2CData1) << 1; else if (I2CAddress == SET_DATA_SAMPLE_RATE && I2CDataPos == 2) SampleRate = (I2CData0 << 8 | I2CData1) << 1; else if (I2CAddress == SET_LOWER_NON_LINEAR && I2CDataPos == 2) CMPDAC2 = (I2CData0 << 8 | I2CData1); else if (I2CAddress == SET_NON_LINEAR_SIZE && I2CDataPos == 2) NonLinearSize = (I2CData0 << 8 | I2CData1); else if (I2CAddress == SET_NON_LINEAR_TIME && I2CDataPos == 2) NonLinearMaxTime = (I2CData0 << 8 | I2CData1); else if (I2CAddress == GET_LOWER_NON_LINEAR && I2CDataPos == 0) // Only gets one byte, needs to be fixed, used for debug only I2CReturn = CMPDAC2 & 0xFF; else if (I2CAddress == GET_OVP_ADR && I2CDataPos == 0) I2CReturn = OVP; else if (I2CAddress == GET_OIP_ADR && I2CDataPos == 0) I2CReturn = OIP; else if (I2CAddress == GET_UVLO_ADR && I2CDataPos == 0) I2CReturn = 0; I2CDataPos++; I2CCONbits.SCLREL = 1; // Release clock. } // State 3: Read Address ready else if (I2CSTATbits.S == 1 && I2CSTATbits.R_W == 1 && ((I2CSTATbits.D_A == 0 && I2CSTATbits.RBF == 1) || (I2CSTATbits.D_A == 1 && I2CSTATbits.RBF == 0))) { if (I2CSTATbits.D_A == 0 && I2CSTATbits.RBF == 1) { I2CState = 3; I2CAddress = I2CRCV; if (DataReady == 0) I2CTRN = (I2CReturn >> 8) & 0xFF; } else if (I2CSTATbits.D_A == 1 && I2CSTATbits.RBF == 0) { I2CState = 4; I2CAddress = I2CRCV; if (DataReady == 0) I2CTRN = I2CReturn & 0xFF; } if (DataReady == 1) { int right = FALSE; if ((DataPosition & 0x0001) == 0) right = TRUE; else right = FALSE; int position = 0; if (DataPosition < 2 * DATA_COLLECTION_SIZE) { position = DataPosition/2; I2CTRN = right ? ErrorData[position] & 0xFF : ErrorData[position] >> 8; } else if (DataPosition < 4 * DATA_COLLECTION_SIZE) { position = DataPosition/2 - 1 * DATA_COLLECTION_SIZE; I2CTRN = right ? X1Data[position] & 0xFF : X1Data[position] >> 8; } else if (DataPosition < 6 * DATA_COLLECTION_SIZE) { position = DataPosition/2 - 2 * DATA_COLLECTION_SIZE; I2CTRN = right ? X2Data[position] & 0xFF : X2Data[position] >> 8; } else if (DataPosition < 8 * DATA_COLLECTION_SIZE) { position = DataPosition/2 - 3 * DATA_COLLECTION_SIZE; I2CTRN = right ? X3Data[position] & 0xFF : X3Data[position] >> 8; } else if (DataPosition < 10 * DATA_COLLECTION_SIZE) { position = DataPosition/2 - 4 * DATA_COLLECTION_SIZE; I2CTRN = right ? ControlData[position] & 0xFF : ControlData[position] >> 8; } DataPosition++; } I2C1CONbits.SCLREL = 1; I2CCONbits.SCLREL = 1; // Release clock. } // State 5: NACK else if (I2CSTATbits.S == 1 && I2CSTATbits.R_W == 1 && I2CSTATbits.D_A == 0 && I2CSTATbits.ACKSTAT == 1) { I2CState = 5; I2CCONbits.SCLREL = 1; // Release clock. } // State 6: Undefined else { I2CUndefined++; } _SI2C1IF = 0; // I2C Interrupt Flag Status bit is cleared } |
The I2C code is written as an interrupt driven state machine. The state variable is I2CState and is assigned an integer, but you could use #defines, enums, and such to make the code easier to read. If we strip most of the code we can see the structure:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | void __attribute__((__interrupt__, no_auto_psv)) _SI2C1Interrupt() { if (I2CSTATbits.I2COV) { I2CSTATbits.I2COV = 0; I2CSTATbits.RBF = 0; I2CDataPos = 0; // Reset the recieve process due to overflow. I2CCONbits.SCLREL = 1; // Release clock. } else if (I2CSTATbits.IWCOL) { I2CSTATbits.IWCOL = 0; I2CCONbits.SCLREL = 1; // Release clock. } // State 1: Write Address ready else if (I2CSTATbits.S == 1 && I2CSTATbits.R_W == 0 && I2CSTATbits.D_A == 0 && I2CSTATbits.RBF == 1) { I2CState = 1; I2CAddress = I2CRCV; I2CDataPos = 0; I2CCONbits.SCLREL = 1; // Release clock. } // State 2: Write Data ready else if (I2CSTATbits.S == 1 && I2CSTATbits.R_W == 0 && I2CSTATbits.D_A == 1 && I2CSTATbits.RBF == 1) { I2CState = 2; } // State 3: Read Address ready else if (I2CSTATbits.S == 1 && I2CSTATbits.R_W == 1 && ((I2CSTATbits.D_A == 0 && I2CSTATbits.RBF == 1) || (I2CSTATbits.D_A == 1 && I2CSTATbits.RBF == 0))) { if (I2CSTATbits.D_A == 0 && I2CSTATbits.RBF == 1) { I2CState = 3; } else if (I2CSTATbits.D_A == 1 && I2CSTATbits.RBF == 0) { I2CState = 4; } I2C1CONbits.SCLREL = 1; I2CCONbits.SCLREL = 1; // Release clock. } // State 5: NACK else if (I2CSTATbits.S == 1 && I2CSTATbits.R_W == 1 && I2CSTATbits.D_A == 0 && I2CSTATbits.ACKSTAT == 1) { I2CState = 5; I2CCONbits.SCLREL = 1; // Release clock. } // State 6: Undefined else { I2CUndefined++; } _SI2C1IF = 0; // I2C Interrupt Flag Status bit is cleared } |
Now we can review each of the conditionals and see what they do.
1 2 3 4 5 6 7 8 | void __attribute__((__interrupt__, no_auto_psv)) _SI2C1Interrupt() { if (I2CSTATbits.I2COV) { I2CSTATbits.I2COV = 0; I2CSTATbits.RBF = 0; I2CCONbits.SCLREL = 1; // Release clock. } |
This handles an overflow condition. This happens if data arrives before the previous value is transferred out of the receive register. The flag is cleared. The buffer full status is cleared. And we release the clock so transactions can continue.
1 2 3 4 5 | else if (I2CSTATbits.IWCOL) { I2CSTATbits.IWCOL = 0; I2CCONbits.SCLREL = 1; // Release clock. } |
This handles a write collision. The flag is cleared, and the clock is cleared, so transactions can continue.
In both the above cases, the response is to clear the problem and let things continue. Your application may need to do more. For example, if there are lots of errors, you could shut down the control loop if it is not safe to let it run when communication is lost.
1 2 3 4 5 6 7 8 9 | // State 1: Write Address ready else if (I2CSTATbits.S == 1 && I2CSTATbits.R_W == 0 && I2CSTATbits.D_A == 0 && I2CSTATbits.RBF == 1) { I2CState = 1; I2CAddress = I2CRCV; I2CDataPos = 0; I2CCONbits.SCLREL = 1; // Release clock. } |
At this point the conditionals are looking for combinations of bits to know what is going on, so we need to understand the bits.
So this case is looking for an address write.
1 2 3 4 5 6 | // State 2: Write Data ready else if (I2CSTATbits.S == 1 && I2CSTATbits.R_W == 0 && I2CSTATbits.D_A == 1 && I2CSTATbits.RBF == 1) { I2CState = 2; } |
This one is looking for data.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // State 3: Read Address ready else if (I2CSTATbits.S == 1 && I2CSTATbits.R_W == 1 && ((I2CSTATbits.D_A == 0 && I2CSTATbits.RBF == 1) || (I2CSTATbits.D_A == 1 && I2CSTATbits.RBF == 0))) { if (I2CSTATbits.D_A == 0 && I2CSTATbits.RBF == 1) { I2CState = 3; } else if (I2CSTATbits.D_A == 1 && I2CSTATbits.RBF == 0) { I2CState = 4; } I2C1CONbits.SCLREL = 1; I2CCONbits.SCLREL = 1; // Release clock. } |
This is looking for a read address, and also when to send data. It also has to release the clock because data will be sent.
More on this when we discuss the implementation of this.
1 2 3 4 5 6 7 | // State 5: NACK else if (I2CSTATbits.S == 1 && I2CSTATbits.R_W == 1 && I2CSTATbits.D_A == 0 && I2CSTATbits.ACKSTAT == 1) { I2CState = 5; I2CCONbits.SCLREL = 1; // Release clock. } |
This checks if the slave NACKed and if so, the clock must be released or the bus will lock up.
1 2 3 4 5 6 7 | // State 6: Undefined else { I2CUndefined++; } _SI2C1IF = 0; // I2C Interrupt Flag Status bit is cleared } |
Finally we increment a variable used to debug any undefined states and clear the interrupt.
Now lets look at the application code to see how registers are handled in the state machine. I’ll leave out the code we just looked at so you only have to think about the application code.
1 2 3 4 5 6 7 8 9 | void __attribute__((__interrupt__, no_auto_psv)) _SI2C1Interrupt() { // State 1: Write Address ready else if (I2CSTATbits.S == 1 && I2CSTATbits.R_W == 0 && I2CSTATbits.D_A == 0 && I2CSTATbits.RBF == 1) { I2CAddress = I2CRCV; I2CDataPos = 0; } |
The first part of the write consists of receiving the address. We store it in the variable I2CAddress then we set the counter I2CDataPos to 0. This counter will keep track of each byte of data recieved.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // State 2: Write Data ready else if (I2CSTATbits.S == 1 && I2CSTATbits.R_W == 0 && I2CSTATbits.D_A == 1 && I2CSTATbits.RBF == 1) { if (I2CDataPos == 0) I2CAddress = I2CRCV; else if (I2CDataPos == 1) I2CData0 = I2CRCV; else if (I2CDataPos == 2) I2CData1 = I2CRCV; ... else I2CJunkData = I2CRCV; // Guarentee all data is read even with bugs. if (I2CAddress == SET_K1_ADR && I2CDataPos == 2) ConstTable[0] = I2CData0 << 8 | I2CData1; else if (I2CAddress == SET_K2_ADR && I2CDataPos == 2) ConstTable[1] = I2CData0 << 8 | I2CData1; I2CDataPos++; } |
This code is receiving the bytes and processing the commands. If the data position is 0, the address is received. If the position is 1, we save the first data byte in I2CData0. Then when the position is 2, we put the second byte in I2CData1, etc.
The big question is why are we getting the address twice? I’ll confess that this might be coded better, but let’s take it as I originally wrote the code. In State 1, the I2CRCV contains the I2C address of the dsPIC, which is acting as a slave device. In State 2 the I2CRCV contains a data byte that is being used as an address in the command structure of our protocol. We can think of this as a register address.
The data from State 1 is essentially thrown away, using the I2CAddress as a convenient place to put the data. It can be used during debug, and does not use more memory than required, and avoids using a “debug” variable that can get stomped on by other code.
At the bottom of the routine we pattern match on addresses. We compare the I2CAddress with a #defined address and ensure that the number of data items captured by I2CDataPos are correct, and if there is a match, we take two bytes and combine them into a 16 bit register variable. In the present case, a ConstTable is being loaded. These constants happen to be the compensation table.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // State 3: Read Address ready else if (I2CSTATbits.S == 1 && I2CSTATbits.R_W == 1 && ((I2CSTATbits.D_A == 0 && I2CSTATbits.RBF == 1) || (I2CSTATbits.D_A == 1 && I2CSTATbits.RBF == 0))) { if (I2CSTATbits.D_A == 0 && I2CSTATbits.RBF == 1) { I2CState = 3; I2CAddress = I2CRCV; if (DataReady == 0) I2CTRN = (I2CReturn >> 8) & 0xFF; } else if (I2CSTATbits.D_A == 1 && I2CSTATbits.RBF == 0) { I2CState = 4; I2CAddress = I2CRCV; if (DataReady == 0) I2CTRN = I2CReturn & 0xFF; } ... } |
Finally, we read data. This is designed for a two byte read. When the bits are Address and Read Buffer Full, we store the address in I2CAddress. We then put the high byte in the transmit register. When the bits are Data and Read Buffer Empty, we read the address and put the low byte in the transmit register. The address is not used.
Note: The read of the address is probably not required, but I have not removed and tested it.
The way the protocol works is the host, meaning the PC running a C# application, is doing a write that leads to preparing the data for read. This sets DataReady to 0. Then the read fetches the result. Given that we are also passing an address during write, this means that from an I2C point of view, there is one write and one read address. But from the protocol point of view, there are multiple write addresses, and one read address. And what is read is set by a write instruction. Thus, there are multiple internal registers.
In my opinion, if you want to communicate with I2C from a USB, and you want to develop I2C code, go to TotalPhase and buy a Beagle/Aardvark package. Beagle is a protocol analyzer that will allow you to spy on the I2C transactions, and Aardvark is a USB/I2C host adapter. They are both very reliable and the software is easy to use.
The Aardvark comes with a C# programming API which was used for this example, so we can walk through the code.
To get started, Aardvark comes with a C# file that wrappers a dll. The beginning of the class looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class AardvarkApi { /*========================================================================= | HELPER FUNCTIONS / CLASSES ========================================================================*/ static long tp_min(long x, long y) { return x < y ? x : y; } ... /*========================================================================= | VERSION ========================================================================*/ [DllImport ("aardvark")] private static extern int c_version (); |
This means the application must be able to load the “aardvark.dll” file, so be sure it is on the PATH or in the directory with the executable. This class is little more than a thin wrapper around the dll.
Now we will put a read/write wrapper around this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | public class AardvarkIO { private readonly int _handle = -1; private bool _disposed; public AardvarkIO() { _handle = AardvarkApi.aa_open(0); AardvarkApi.aa_configure(_handle, AardvarkConfig.AA_CONFIG_SPI_I2C); AardvarkApi.aa_i2c_pullup(_handle, AardvarkApi.AA_I2C_PULLUP_BOTH); //AardvarkApi.aa_target_power(_handle, AardvarkApi.AA_TARGET_POWER_BOTH); int bitrate = AardvarkApi.aa_i2c_bitrate(_handle, 100); } ~AardvarkIO() { AardvarkApi.aa_close(_handle); } public void Write(byte address) { if (_handle < 0) throw new ApplicationException("No handle"); var count = AardvarkApi.aa_i2c_write(_handle, 8, AardvarkI2cFlags.AA_I2C_NO_FLAGS, 1, new byte[] { address }); if (count != 1) throw new ApplicationException("Failed to send 3 bytes " + GetI2CError(count)); } public void WriteUShort(byte address, ushort data) { if (_handle < 0) throw new ApplicationException("No handle"); var count = AardvarkApi.aa_i2c_write(_handle, 8, AardvarkI2cFlags.AA_I2C_NO_FLAGS, 3, new byte[] { address, (byte)(data >> 8), (byte)(data & 0xFF) }); if (count != 3) throw new ApplicationException("Failed to send 3 bytes " + GetI2CError(count)); } public void WriteByte(byte address, byte data) { if (_handle < 0) throw new ApplicationException("No handle"); var count = AardvarkApi.aa_i2c_write(_handle, 8, AardvarkI2cFlags.AA_I2C_NO_FLAGS, 2, new byte[] { address, data }); if (count != 2) throw new ApplicationException("Failed to send 1 byte " + GetI2CError(count)); } public void Write(byte address, ushort[] data) { if (_handle < 0) throw new ApplicationException("No handle"); int pos = 0; byte[] sendData = new byte[2*data.Length+1]; sendData[0] = address; foreach (short item in data) { sendData[2*pos+1] = (byte)(item >> 8); sendData[2*pos + 2] = (byte)(item & 0xFF); pos++; } var count = AardvarkApi.aa_i2c_write(_handle, 8, AardvarkI2cFlags.AA_I2C_NO_FLAGS, (ushort)sendData.Length, sendData); if (count != 2 * data.Length + 1) throw new ApplicationException("Failed to send 2 bytes " + GetI2CError(count)); } public ushort Read() { if (_handle < 0) throw new ApplicationException("No handle"); byte[] data = new byte[2]; var count = AardvarkApi.aa_i2c_read(_handle, 8, AardvarkI2cFlags.AA_I2C_NO_FLAGS, 2, data); if (count != 2) throw new ApplicationException("Failed to recieve 2 bytes " + GetI2CError(count)); return (ushort) (data[0] << 8 | data[1]); } public byte[] Read(byte address, ushort length) { if (_handle < 0) throw new ApplicationException("No handle"); byte[] data = new byte[length]; var count = AardvarkApi.aa_i2c_read(_handle, 8, AardvarkI2cFlags.AA_I2C_NO_FLAGS, length, data); if (count != length) throw new ApplicationException("Failed to recieve " + length + " bytes " + GetI2CError(count)); return data; } private string GetI2CError(int status) { return AardvarkApi.aa_status_string(status); } } |
This gives a higher level interface and uses C# naming conventions. It sets up the bus speed, pull ups, power, and has read write commands with exceptions. This code also comes from Total Phase
Around this is an IO class that talks to the dsPIC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | public static class BuckIO { private static readonly AardvarkIO io = new AardvarkIO(); private const byte GetOvpAdr = 0; ... private const byte SetK1Adr = 3; ... public static bool OverVoltage { get { io.Write(GetOvpAdr); Thread.Sleep(10); return (io.Read() == 1); } } ... public static ushort K1 { set { io.WriteUShort(SetK1Adr, value); } } ... } |
First it defines the “io” variable to be the AardvarkIO class, the one supplied by Total Phase. Then there are contents for each of the addresses as discussed in the dsPIC code. Finally, we have two attributes, one to get, and one to set.
OverVoltage gets an over voltage boolean. It first writes the address to the I2C, then reads a byte, which is compared with a 1, and returns a bool. K1 simply writes the passed in unsigned short to I2C. The dsPIC code will receive it in two bytes as discussed in the dsPIC code.
Finally, we have a GUI application that uses the IO class to run things for us. There is no need to go through that, there should be enough here to build your own protocol, dsPIC, and C# code.
Beagle is the tool for that. It is pretty easy to use, but let’s look at a quick example.
Lets look at index 33-35. 33 is a write transaction with data 07 01. Byte 07 is the address of our protocol, and 01 is the data sent. So this is a one byte instruction. Index 34 shows a two byte instruction to address 9. This instruction returns data, so at index 35 it is getting 64 bytes of data. Notice that this pattern repeats. This example reads a lot of data using 64 byte chunks.
In the dsPIC code (not shown), the dsPIC is sampling data from the internal states of the state space controller, storing them in an array, and then this example is fetching the data. Then the GUI plots it. This makes it easy to debug limit cycles and other problems that occur from coding errors. The final application also allows one to change the compensator constants, reference level, ramp rates, etc.
In this example the protocol is not PM Bus. It is a custom protocol. One of the considerations was simplicity of the dsPIC code. All code that converts floating point to unsigned integers or applies calibration is in C#. The less floating point math in the dsPIC the faster it can process instructions.
If you look at PM Bus controllers like the LTC3880, you will see that calibration and floating point math is handled in the device itself. This makes it easier for the C# programmer. However, it is more typical to control the device with an FPGA or embedded system. Having most of this functionality in the device simplifies the software task for the embedded system.
If you are rolling your own, you have to make some fundamental choices where to put the complexity, consider the calculation power required, and the time it takes to execute. If you choose to push lots of this into the device, my suggestion is to start simple with most of the work done in C#. Then migrate the C# features into the dsPIC gradually. Then move to the embedded system last.
It is very difficult to debug the dsPIC I2C code. If you put a break point, the transaction fails and you can’t single step. So I can’t emphasize enough, start very simple, and add complexity slowly, using this example to get started.
This shows and example GUI. It has some gain controls, status lights that are updated by polling via I2C, but also the data from a transient response. The error is the difference between the reference and feedback values. X1, X2, X3 are the state registers. Control is the value fed to the PWM. The data is sampled because there is not enough memory in the dsPIC to capture a complete transient while sampling every PWM clock cycle.
During the early phases of the design, there were strange behaviors. This tool allowed me to see that registers were wrapping around from rounding effects, observe limit cycling, and other problems.
When I first started debugging, I copied the data from the PickKit3 debugger to Excel and plotted it. But in the end, this was a much better way to spy on things, because I could change the gain, capture the data, and compare it with the external waveforms taken with a scope.
Writing I2C code for the dsPIC is simple, but starting from square one is tough. The goal of this post was to provide example code to get you started, and give you a couple of ideas what to use it for. Feel free to e-mail me if you get stuck, and please share with the community if you discover ways to improve it.
Best of luck!
Wire-to-board interconnection options from Sullins feature a wide range of sizes and applications
MCC’s TVS series high-power suppressors protect sensitive components from voltage spikes and transients
Evaluation boards that streamline evaluating circuit protection on RS-485 serial device ports
There are currently no comments.