I2C slave device

17 January 2021
A significant issue I had with my redesigned USB-I2C master was testing and in particular the checking of error conditions, so I decided to make a test platform for this purpose. Originally I was going to make a dedicated board including DIP switches but I soon realised that test configuration could also be done via the I2C connection, so instead I built the hardware as a break-out board since the PIC16F1823 with RS232 and I2C is a common combination I use as a base for projects.

Finished adapter PCB

Circuit design

The circuit itself is the I2C master with a 14-pin PIC16F1823 rather than an 8-pin PIC12(L)F1840, and the circuit is powered via an on-board voltage regulator supplied via a barrel jack rather than the USB bus. A flash header is also included because it is by far the most convenient way to reprogram a microcontroller, and all the unused I/O pins have been fanned out. Note that between the PIC16F1823 and the FT230XS the RS232 pins are connected Tx-Rx & Rx-Tx and not Tx-Tx & Rx-Rx — getting this wrong is why the PCB went to a second revision.

Circuit schematic

An ulterior motive with this project was creating a KiCad footprint for the PIC16F1823 and this in turn is based on the foorprint for the PIC12F1822 from the same chip series. Several of my recent projects would have used PIC16F1823 if it was not for the absence of the footprint in the standard library, so it was time I got round to making my own. This footprint is included alongside the PCB itself within my Bitbucket repository.

List of components

I found it hard to see the cathode markings on the Osram LM R976 SMD LEDs and as a result had soldered some of them the wrong way round, and the CUI Devices PJ-102AH barrel jacks are also tricky to source, but they are components I had at hand so decided to use them up.

Pad Component Manufacturer Part number
U1 5v power regulator Texas Instruments UA78M05IDCYR
U2 Microcontroller Microchip PIC16F1823
U3 USB-RS232 converter FTDI FT230XS
R1 1kΩ protective resistor Multicomp MCWR08X1001FTL
R2,R3 27Ω USB resistors MCWR08X27R0FTL
D1 Indicator LED Osram LS R976
C1,C2 0.1μF bypass capacitor Samsung CL21B104KACNNNC
J1 Barrel Jack CUI Devices PJ-102AH
J4 Pin header Multicomp 2211S-22G
J5 6-way receptacle 2212S-06SG-85


Whereas the I2C routines that were used within my I2C master have long since been rewritten in assembly and used in many subsequent projects, the same is not the case with the I2C slave routines I have written as I have preferred to use RS232 instead. Therefore the firmware here is a reimplementation in assembly of the routines in the register-enabled slave from late-2018, which I decided to do from scratch. The procedures are summerised in the flow-chart below. Storing of incoming data can be done on either branch of the Acknowledge Time node but I found it more convenient to do it before dispatching the acknowledgment.

I2C flow-chart

Start and stop bits

I did not quite figure out start bit interrupts but I also saw no real need for them either. However getting interrupts for stop bits is useful because it helps in distinguishing between start and restart bits, which in turn can be used to allow a device to support a variable number of register addresses in read transactions. They can also be used as an end-of-transaction indicator rather than assuming fixed transaction lengths.

USB side-channel

I have found it very useful to have a serial side-channel as a way of getting debugging information to the outside world, which is particularly critical in cases like this where using simulation is not an option. When I wrote the first version of the slave firmware I was having to deduce internal state rather than being able to probe it directly, which makes things very difficult. Having used the side-channel for development I left in tidied-up versions of the debugging messages, and an example transaction log is shown below:

A:20 D:0a D:0b A:21 R:a R:a P

Here A is an incoming address byte value, D an incoming data byte, R is the ack/nack status of an outgoing byte, and P is a stop bit. Such use of RS232 in this way is also a good demonstration of the usefulness of clock stretching in I2C — without it I doubt that the UART transactions to print out the information would complete in time to avoid a buffer collision.

Address hold

Normally the microcontroller hardware automatically acknowledges incoming bytes, but by using address hold this decision on whether a matching I2C address should be acknowledged is instead passed on to the firmware. When I first tried implementing address holding back in late-2018 it seemed to be broken and looking back the symptoms of the fault it could plausibly have been an issue with whatever chip I was using. Having said that I did not consider the feature useful at the time so my investigation into it was limited. I still don't really see much use for it beyond testing some of the failure paths in the complimentary I2C master firmware, and for those tests I simply hard-coded the negative-acknowledge for the test-cases.

Data hold

Like address hold, data hold lets the firmware decide whether a data byte should be acknowledged or not. Although unlike address hold it seemed to work properly in the previous firmware in hindsight I am not entirely sure whether how things were done was entirety correct, although this is a moot point as I don't write PIC firmware in C any more. For testing purposes incoming data bytes of 0xff are not acknowledged which is sufficient to test most of the failure reporting cases.


Partly due to the design of the circuit being a bit rushed I got the internal RS232 connection between the USB converter and the microcontroller the wrong way round, which by any measure was a frustrating mistake to make. As a result the firmware development and testing of my I2C master was instead done using an alternative board equipped with a PIC16F1454 back in November, which thankfully needed very few changes to make it work with the PIC16F1823 once the revised PCB for the latter arrived. I did not have a 5-volt regulator to hand so I omitted the internal power supply and instead powered the slave via the ICSP header.

I tested the slave device with an off-the-shelf I2C adapter and then used this slave to test my own I2C master — in the process I found several faults with the latter's firmware which have now been fixed. While I have not noticed problems with my previous C-based I2C master and I2C slave routines I have more confidence in the correctness of my new assembly-based ones. The one thing the firmware does not guard against is spurious behaviour when pull-up resistors in place, but I think this is not possible to do and would explain why off-the-shelf adapters have built-in ones that cannot be disabled. I suspect that at some point I will reimplement my LED Matrix Tile firmware in pure assembly as it is the last bit of firmware written in C that I expect to have future interest in.