Having succeeded at getting a basic I2C master working on the STM8S it is not time to start to look at I2C slave devices on the STM8S.
The project will be a simple one, the STM8S will take a stream of bytes and perform simple addition. When requested, the STM8S will return the total as a 16-bit integer. Each new write request will clear the current total and start the whole process again.
Simple enough so let’s get started.
This post is a fairly long as it contains a lot of code so it might be a good time to grab a beer and settle down.
Simple Slave Adder
The slave adder is a simple device running over I2C. The devices has the following properties:
- Acts as an I2C slave device with address 0x50
- I2C bus speed is 50 KHz
- Upon receiving a valid start condition and address for a write operation the device will clear the current total.
- Bytes written to the device will be summed and a running total kept.
- A start condition with a valid address for a read operation will return the current total as a 16-bit integer, MSB first.
A Netduino 3 will be used as the I2C bus master device. This runs the .NET Microframework and has an implementation of the I2C protocol built into the framework. This gives a good reference point for the master device i.e. someone else has debugged that so we can assume that the device is working as per the protocol specification.
As mentioned in the previous article on I2C Master devices, there is a really good article about the I2C protocol on Wikipedia. If you want more information about the protocol then I suggest you head over there.
Netduino Code
The initial version of the I2C master device will simply send two bytes of data for the I2C slave device to sum. The code is simple enough:
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 | using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using System.Threading;
namespace I2CMaster
{
public class Program
{
public static void Main()
{
I2CDevice i2cBus = new I2CDevice( new I2CDevice.Configuration(0x50, 50));
byte [] buffer = { 1, 2 };
I2CDevice.I2CTransaction[] transactions = new I2CDevice.I2CTransaction[1];
transactions[0] = I2CDevice.CreateWriteTransaction(buffer);
while ( true )
{
int bytesRead = i2cBus.Execute(transactions, 100);
Thread.Sleep(1000);
}
}
}
}
|
The above application creates a new instance of the I2CDevice with a device address of )x50 and a clock frequency of 50 KHz. A single transaction is created and the master writes the same two bytes to the I2C bus every second.
STM8S Code
As with the I2C Master article, much of the action happens in two places:
- I2C device initialisation method
- I2C Interrupt Service Routine (ISR)
The initialisation method sets up the I2C peripheral on the STM8S and enters the waiting state, waiting for the master to put data onto the I2C bus.
The ISR will deal with the actual data processing and in a full application it will also deal with any error conditions that may arise.
I2C Initialisation
The initialisation method sets up the I2C bus by performing the following tasks:
- Setting the expected clock frequency
- Setting the device address
- Turning the interrupts on
The I2C peripheral must be disabled before configuring certain aspects of the peripheral, namely clock speeds:
The peripheral needs to know the current master clock frequency, the I2C mode (Standard or Fast) and the clock divider values. These must all be configured whilst the peripheral is disabled.
1 2 3 4 | I2C_FREQR = 16;
I2C_CCRH_F_S = 0;
I2C_CCRL = 0xa0;
I2C_CCRH_CCR = 0x00;
|
The device assumes that we will be using the standard 16 MHz clock speed which has been used in the other tutorials in this series. This is indicated by the I2C_FREQR register.
The values for the clock control register (I2C_CCRL and I2C_CCRH_CCR were simply taken from the STM8S programming reference manual. There is no coincidence that a bus speed of 50 KHz was chose, it is simply one of the reference values in the table. No point in creating work if you do not have to. If you want different speeds then you can either use one of the defined values in the remainder of the table or use the formulae provided.
The next step is to define the device address and addressing mode. I2C allows both 7 and 10 bit addressing modes. For simplicity this device will use a 7-bit device address:
1 2 3 4 | I2C_OARH_ADDMODE = 0;
I2C_OARH_ADD = 0;
I2C_OARL_ADD = 0x50;
I2C_OARH_ADDCONF = 1;
|
The device also allows for the maximum rise time to be configured. This example uses 17uS as the maximum time:
The I2C peripheral allows for three different interrupt conditions to be defined, buffer interrupts, event interrupts (start condition etc.) and error interrupts. For convenience we will turn all of these on:
1 2 3 | I2C_ITR_ITBUFEN = 1;
I2C_ITR_ITEVTEN = 1;
I2C_ITR_ITERREN = 1;
|
Now that the peripheral is configured we need to re-enable it:
The last bit of initialisation is to configure the device to return an ACK after each byte:
At this point the I2C peripheral should be listening to the I2C bus for a start condition and the address 0x50.
I2C Interrupt Service Routine (ISR)
The ISR contains the code which processes the data and error conditions for the I2C peripheral. All of the I2C events share the same ISR and the ISR will need to interrogate the status registers in order to determine the exact reason for the interrupt.
Starting with an empty ISR we have the following code:
1 2 3 4 | #pragma vector = I2C_RXNE_vector
__interrupt void I2C_IRQHandler()
{
}
|
There are several other vector names we could have chosen, they all contain the same value and the value I2C_RXNE_vector was chosen arbitrarily.
Assuming that there are no errors on the bus, the I2C master code above will cause the following events to be generated:
- Address detection
- Receive buffer not empty
This initial simple implementation can use these two events to clear the total when a new Start condition is received and add the current byte to the total when data is received.
1 2 3 4 5 6 7 8 9 10 11 | if (I2C_SR1_ADDR)
{
reg = I2C_SR1;
reg = I2C_SR3;
_total = 0;
return ;
}
|
I2C_SR1_ADDR should be set when an address is detected on the bus which matches the address currently in the address registers. As a starter application the code can simply assume that any address is going to be a write condition. The code can clear the totals and get ready to receive data. Note that this will be expanded later to take into consideration the fact that the master can perform both read and write operations.
The next event we need to consider is the receipt of data from the master. This will trigger the Receive Buffer Not Empty interrupt. This simple application should just read the buffer and add the current byte to the running total:
1 2 3 4 5 6 7 8 | if (I2C_SR1_RXNE)
{
_total += I2C_DR;
return ;
}
|
As indicated earlier, the I2C ISR is a generic ISRT and is triggered for all I2C events. The initialisation code above has turned on the error interrupts as well as the data capture interrupts. Whilst none of the code in this article will handle error conditions, the ISR will output the status registers for diagnostic purposes:
1 2 3 4 5 6 7 | PIN_ERROR = 1;
__no_operation();
PIN_ERROR = 0;
reg = I2C_SR1;
BitBang(reg);
reg = I2C_SR3;
BitBang(reg);
|
This will of course require suitable definitions and support methods.
Putting this all together gives an initial implementation of a slave device as:
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 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | #if defined DISCOVERY
#include <iostm8S105c6.h>
#else
#include <iostm8s103f3.h>
#endif
#include <intrinsics.h>
#define PIN_BIT_BANG_DATA PD_ODR_ODR4
#define PIN_BIT_BANG_CLOCK PD_ODR_ODR5
#define PIN_ERROR PD_ODR_ODR6
int _total;
void BitBang(unsigned char byte)
{
for ( short bit = 7; bit >= 0; bit--)
{
if (byte & (1 << bit))
{
PIN_BIT_BANG_DATA = 1;
}
else
{
PIN_BIT_BANG_DATA = 0;
}
PIN_BIT_BANG_CLOCK = 1;
__no_operation();
PIN_BIT_BANG_CLOCK = 0;
}
PIN_BIT_BANG_DATA = 0;
}
void InitialiseSystemClock()
{
CLK_ICKR = 0;
CLK_ICKR_HSIEN = 1;
CLK_ECKR = 0;
while (CLK_ICKR_HSIRDY == 0);
CLK_CKDIVR = 0;
CLK_PCKENR1 = 0xff;
CLK_PCKENR2 = 0xff;
CLK_CCOR = 0;
CLK_HSITRIMR = 0;
CLK_SWIMCCR = 0;
CLK_SWR = 0xe1;
CLK_SWCR = 0;
CLK_SWCR_SWEN = 1;
while (CLK_SWCR_SWBSY != 0);
}
void InitialiseI2C()
{
I2C_CR1_PE = 0;
I2C_FREQR = 16;
I2C_CCRH_F_S = 0;
I2C_CCRL = 0xa0;
I2C_CCRH_CCR = 0x00;
I2C_OARH_ADDMODE = 0;
I2C_OARH_ADD = 0;
I2C_OARL_ADD = 0x50;
I2C_OARH_ADDCONF = 1;
I2C_TRISER = 17;
I2C_ITR_ITBUFEN = 1;
I2C_ITR_ITEVTEN = 1;
I2C_ITR_ITERREN = 1;
I2C_CR1_PE = 1;
I2C_CR2_ACK = 1;
}
#pragma vector = I2C_RXNE_vector
__interrupt void I2C_IRQHandler()
{
unsigned char reg;
if (I2C_SR1_ADDR)
{
reg = I2C_SR1;
reg = I2C_SR3;
_total = 0;
return ;
}
if (I2C_SR1_RXNE)
{
_total += I2C_DR;
return ;
}
PIN_ERROR = 1;
__no_operation();
PIN_ERROR = 0;
reg = I2C_SR1;
BitBang(reg);
reg = I2C_SR3;
BitBang(reg);
}
int main()
{
_total = 0;
__disable_interrupt();
PD_ODR = 0;
PD_DDR_DDR4 = 1;
PD_CR1_C14 = 1;
PD_CR2_C24 = 1;
PD_DDR_DDR5 = 1;
PD_CR1_C15 = 1;
PD_CR2_C25 = 1;
PD_DDR_DDR6 = 1;
PD_CR1_C16 = 1;
PD_CR2_C26 = 1;
InitialiseSystemClock();
InitialiseI2C();
__enable_interrupt();
while (1)
{
__wait_for_interrupt();
}
}
|
Executing the Applications
At this stage a quick test with the two devices connected and the logic analyser will show the master device outputting data to the I2C bus. The correct operation of the I2C salve device can be verified by setting break points with in the two if statements in the ISR. Note that this will generate some errors but it is good enough to verify that the ISRs are being triggered correctly.

STM8 I2C Write Condition
The above shows the output from the Saleae Logic Analyser when the write transaction is sent to the bus by the Netduino.
Reading and Writing with I2C Slaves
At this point the above code should be accepting data from the Netduino 3. It is now time to expand the code to take into account the requirement to read back data from the slave device.
Netduino Code
The Netduino code will need to be modified to generate both a write transaction to send data to the I2C slave device and a read transaction to retrieve the results from the calculation.
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 | using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using System.Threading;
namespace I2CMaster
{
public class Program
{
public static void Main()
{
I2CDevice i2cBus = new I2CDevice( new I2CDevice.Configuration(0x50, 50));
byte [] buffer = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
byte [] resultBuffer = new byte [2];
I2CDevice.I2CTransaction[] transactions = new I2CDevice.I2CTransaction[2];
transactions[0] = I2CDevice.CreateWriteTransaction(buffer);
transactions[1] = I2CDevice.CreateReadTransaction(resultBuffer);
while ( true )
{
int bytesRead = i2cBus.Execute(transactions, 100);
Thread.Sleep(1000);
}
}
}
}
|
The transaction array has been expanded to contain a second transaction to read the data from the device. The data to be summed has also been expanded to include a further eight elements.
STM8S Code
A state machine will be used to allow the STM8S to work out what action should be taken within the ISR. This is required as the i”c peripheral will generate an event for the address detection for both the write transaction and the read transaction. The application will need to be able to differentiate between the two conditions as one requires the total to be cleared, the other requires that the total is sent back to the master device.
The state machine contains the following states:
- Waiting for a write condition to start
- Adding data to the running total
- Sending the MSB of the total
- Sending the LSB of the total
This is represented in code by an enum and a global variable:
1 2 3 4 5 6 7 8 | typedef enum
{
ISWaiting,
ISAdding,
ISSendingMSB,
ISSendingLSB
} I2CStateType;
I2CStateType _i2cState;
|
The ISR will need to be changed in order to deal with the state machine and also handle the slave transmitting data to the master device. The first change is to the address detection. This assumes that the main program loop initialises the state to ISWaiting:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | if (I2C_SR1_ADDR)
{
if (_i2cState == ISWaiting)
{
_i2cState = ISAdding;
_total = 0;
}
reg = I2C_SR1;
reg = I2C_SR3;
return ;
}
|
The code above works out if the application is waiting for the first write condition (IsWaiting) or if the address detection has been triggered by a read condition (any other state).
Next, any other data reception is assumed to be data to be added to the total. The state is changed to IsAdding just in case:
1 2 3 4 5 6 7 8 9 | if (I2C_SR1_RXNE)
{
_total += I2C_DR;
_i2cState = ISAdding;
return ;
}
|
Next we have two new conditions we have not considered so far, the first is the fact that we need to transmit the result to the master. The request from the client will trigger a Transmit Buffer Empty event:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | if (I2C_SR1_TXE)
{
if (_i2cState == ISAdding)
{
I2C_DR = (_total >> 8) & 0xff;
_i2cState = ISSendingMSB;
}
else
{
I2C_DR = _total & 0xff;
_i2cState = ISSendingLSB;
}
return ;
}
|
The master is asking for two bytes and the application needs to track if we are transmitting the most significant byte (MSB) or least significant byte (LSB).
The next new condition is the acknowledge event. This is generated at the end of the master read operation. This will set the system back into a waiting condition:
1 2 3 4 5 6 | if (I2C_SR2_AF)
{
I2C_SR2_AF = 0;
_i2cState = ISWaiting;
return ;
}
|
The full application becomes:
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 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 | #if defined DISCOVERY
#include <iostm8S105c6.h>
#else
#include <iostm8s103f3.h>
#endif
#include <intrinsics.h>
#define PIN_BIT_BANG_DATA PD_ODR_ODR4
#define PIN_BIT_BANG_CLOCK PD_ODR_ODR5
#define PIN_ERROR PD_ODR_ODR6
typedef enum
{
ISWaiting,
ISAdding,
ISSendingMSB,
ISSendingLSB
} I2CStateType;
I2CStateType _i2cState;
int _total;
void BitBang(unsigned char byte)
{
for ( short bit = 7; bit >= 0; bit--)
{
if (byte & (1 << bit))
{
PIN_BIT_BANG_DATA = 1;
}
else
{
PIN_BIT_BANG_DATA = 0;
}
PIN_BIT_BANG_CLOCK = 1;
__no_operation();
PIN_BIT_BANG_CLOCK = 0;
}
PIN_BIT_BANG_DATA = 0;
}
void InitialiseSystemClock()
{
CLK_ICKR = 0;
CLK_ICKR_HSIEN = 1;
CLK_ECKR = 0;
while (CLK_ICKR_HSIRDY == 0);
CLK_CKDIVR = 0;
CLK_PCKENR1 = 0xff;
CLK_PCKENR2 = 0xff;
CLK_CCOR = 0;
CLK_HSITRIMR = 0;
CLK_SWIMCCR = 0;
CLK_SWR = 0xe1;
CLK_SWCR = 0;
CLK_SWCR_SWEN = 1;
while (CLK_SWCR_SWBSY != 0);
}
void InitialisePortD()
{
PD_ODR = 0;
PD_DDR_DDR4 = 1;
PD_CR1_C14 = 1;
PD_CR2_C24 = 1;
PD_DDR_DDR5 = 1;
PD_CR1_C15 = 1;
PD_CR2_C25 = 1;
PD_DDR_DDR6 = 1;
PD_CR1_C16 = 1;
PD_CR2_C26 = 1;
}
void InitialiseI2C()
{
I2C_CR1_PE = 0;
I2C_FREQR = 16;
I2C_CCRH_F_S = 0;
I2C_CCRL = 0xa0;
I2C_CCRH_CCR = 0x00;
I2C_OARH_ADDMODE = 0;
I2C_OARH_ADD = 0;
I2C_OARL_ADD = 0x50;
I2C_OARH_ADDCONF = 1;
I2C_TRISER = 17;
I2C_ITR_ITBUFEN = 1;
I2C_ITR_ITEVTEN = 1;
I2C_ITR_ITERREN = 1;
I2C_CR1_PE = 1;
I2C_CR2_ACK = 1;
}
#pragma vector = I2C_RXNE_vector
__interrupt void I2C_IRQHandler()
{
unsigned char reg;
if (I2C_SR1_ADDR)
{
if (_i2cState == ISWaiting)
{
_i2cState = ISAdding;
_total = 0;
}
reg = I2C_SR1;
reg = I2C_SR3;
return ;
}
if (I2C_SR1_RXNE)
{
_total += I2C_DR;
_i2cState = ISAdding;
return ;
}
if (I2C_SR1_TXE)
{
if (_i2cState == ISAdding)
{
I2C_DR = (_total >> 8) & 0xff;
_i2cState = ISSendingMSB;
}
else
{
I2C_DR = _total & 0xff;
_i2cState = ISSendingLSB;
}
return ;
}
if (I2C_SR2_AF)
{
I2C_SR2_AF = 0;
_i2cState = ISWaiting;
return ;
}
PIN_ERROR = 1;
__no_operation();
PIN_ERROR = 0;
reg = I2C_SR1;
BitBang(reg);
BitBang(I2C_SR2);
reg = I2C_SR3;
BitBang(reg);
}
int main()
{
_total = 0;
_i2cState = ISWaiting;
__disable_interrupt();
InitialisePortD();
InitialiseSystemClock();
InitialiseI2C();
__enable_interrupt();
while (1)
{
__wait_for_interrupt();
}
}
|
The above two pieces of code are fuller applications and if we execute these and hook up the logic anayser to the I2C bus we see the following output:

Adding 1 To 10
Reading From Multiple Devices
The I2C protocol allows for one or more devices with different slave addresses to be connected to the same I2C bus. The previous article used the TMP102 temperature sensor with slave address 0x48 and this article has created a slave device with address 0x50. It should therefore be possible to connect the two devices to the same bus and talk to each selectively.
Netduino Application
As with the previous examples, the Netduino 3 will be used as the I2C bus master. The applicaiton above will need to be merged with the code in the previous article.
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 | using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using System.Threading;
namespace I2CMaster
{
public class Program
{
public static void Main()
{
I2CDevice.Configuration stm8s = new I2CDevice.Configuration(0x50, 50);
I2CDevice.Configuration tmp102 = new I2CDevice.Configuration(0x48, 50);
I2CDevice i2cBus = new I2CDevice(stm8s);
byte [] buffer = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
byte [] resultBuffer = new byte [2];
I2CDevice.I2CTransaction[] transactions = new I2CDevice.I2CTransaction[2];
transactions[0] = I2CDevice.CreateWriteTransaction(buffer);
transactions[1] = I2CDevice.CreateReadTransaction(resultBuffer);
byte [] temperatureBuffer = new byte [2];
I2CDevice.I2CTransaction[] reading = new I2CDevice.I2CTransaction[1];
reading[0] = I2CDevice.CreateReadTransaction(temperatureBuffer);
while ( true )
{
i2cBus.Config = stm8s;
int bytesRead = i2cBus.Execute(transactions, 100);
i2cBus.Config = tmp102;
bytesRead = i2cBus.Execute(reading, 100);
int sensorReading = ((temperatureBuffer[0] << 8) | temperatureBuffer[1]) >> 4;
double centigrade = sensorReading * 0.0625;
double fahrenheit = centigrade * 1.8 + 32;
Debug.Print(centigrade.ToString() + " C / " + fahrenheit.ToString() + " F" );
string message = "" ;
for ( int index = 0; index < buffer.Length; index++)
{
message += buffer[index].ToString();
if (index == (buffer.Length - 1))
{
message += " = " + ((resultBuffer[0] * 256) + resultBuffer[1]).ToString();
}
else
{
message += " + " ;
}
}
Debug.Print(message);
Thread.Sleep(1000);
}
}
}
}
|
The above code creates two I2C configuration object, one for the TMP102 and one for the STM8S device. The I2C bus object has the configuration changed depending upon which device is required.
Wiring up the Devices
Firstly, wire up the TMP102 sensor as described in the article on I2C master devices. The SDA and SCK lines should be connected to 3.3V via 4K7 resistors. Next connect the SDA and SCK lines from the STM8S device to the same point as SDA and SCK lines from the TMP102 sensor. Throw in the logic analyser connections and you get something like this:

TMP102 and STM8S I2C slave devices
Running the Application
Running the above two applications and starting the logic analyser generated the following output:

Reading From Two I2C Slaves
The write operation to the far left (W0x50) sends the 10 bytes to the STM8S device. This is followed by a read (R0x50) of two bytes from the same device, this can be seen about two thirds of the way from the left-hand side of the trace. The final operation is the read of the current temperature, R0x48, to the right of the trace.
Conclusion
The STM8S slave device above is a simple device. There are still plenty of modification which need to be made:
- Add error handling
- Allow for trapping conditions such as clock stretching
to name but a few. The code above should allow for simple devices to be put together is a short space of time.