Go to file
2021-06-03 00:03:53 +12:00
.gitignore Merging issues with files and repo, restarting repo 2021-05-30 13:57:58 +12:00
I2C Definitions.asm Completely working read and writes in Fast-mode using only GPC 2021-06-03 00:03:23 +12:00
I2C Slave ASM.asm Completely working read and writes in Fast-mode using only GPC 2021-06-03 00:03:23 +12:00
README.md Rewrote and formatted README 2021-06-03 00:03:53 +12:00

I2C slave driver written in ASM

It is designed to run on a single FPPA device, specifically the PMS150C, however it will work on any IC of the range (may need adjustments and needs to have a general purpose comparator). It is blocking for the duration of any transfer and is reliant on getting into the ISR quickly to ensure things get done in time for Fast-mode communication. It is tested working for I2C Fast-mode at 400kHz with some caveats (explained below). Standard-mode comms will work with less possibility for issues however it will take more time away from the main process.

Reading and Writing

Data is read from and written directly to RAM, care must be taken to only write to the correct areas.

Writing process:

  • Single/ multiple bytes (1 to n) can be written by: addr -> memory index -> data0 -> data1 -> ... -> datan -> stop condition
  • The memory index will be reset after this write
  • There is no protection of other areas of memory, the user will have to be careful not to overwrite anything important
  • Repeated start conditions are not supported
  • If only an address is written, then this address will be retained and used for the next read, addr -> memory index -> stop condition
  • This is not retained if followed by another write, it will be lost and the other write will take control

Reading process:

  • Single or multiple bytes can be read.
  • A read will start from the hardcoded base data address unless the address is set as above by writing a single byte. In which case that address will be the start point.
  • After a read the memory address is reset to the start of the data block.
  • Data will keep being read out until NACK received at the end of a byte.

Limitations

  • There is no handling of a malformed transmission, the processor will hang. There simply isn't the cycles spare when operating at Fast-mode to catch any errors.

    • It would be possible to get something working for Standard-mode but then you might as well use the code-generation tool.
    • It could be mitigated by setting up a watchdog timer, however there is no non-volatile memory so any settings or data at the time of resetting will be lost.
    • This is a pretty major shortcoming and I hope to impelement a soft reset of some sort similar to a watchdog timer.
  • The chip needs to be setup with a 17MHz HSE and 8.5MHz sysclk.

    • It will generally work to send at receive at Fast-mode speeds with the standard 16MHz HSE and 8MHz clock but sometimes it will get slightly behind.
    • If you do not use the higher clock speed then there is a chance that SDA will rise/fall while SCL is high and possibly cause issues. It is not a problem on the device end as it will happily miss timings but the master or other devices may see this as a start/stop condition.
  • The I2C specifications are not quite met.

    • If SCL low time is <1.6µs then the same problems as above will occur with start/stop conditions been sent out onto the bus.
    • Again this is not a problem for the device but may cause issues for other devices or the master.
    • The I2C specs call for a minimum 1.3µs period so it is close but worth keeping in mind that it can cause issues.

Specific Process

The general purpose comparator (GPC) is used for catching edges throughout. Initially the comparator negative is attached to SCL (PA.3) and positive to an internal reference voltage which is set to roughly half Vdd. Only the falling edge is setup to trigger the interrupt request flag and the interrupt is enabled.

When a start condition is put onto the bus the falling edge of SCL triggers the interrupt and the ISR starts. It first clears the GPC interrupt and then checks if SCL is high. If SCL is low then it immediatly returns, this is implemented to reduce the amount of time the processor spends in the ISR while other devices are communicating on the bus. In this process takes around 6 cycles to execute, from leaving the main program to returning to it and this is confirmed in testing with a 700ns execution time @8.5MHz.

If SCL is high then it is a start condition, global interrupts are disabled and the accumulator and flags are pushed to the stack before the communication starts.

The GPC negative input is changed to SCL and it's output is inverted such that a rising edge on SCL is a falling edge on the GPC output and so sets the GPC interrupt request flag (GPC_REQ). This flag is immediately cleared to ensure no phantom edges are caught, from this point a rising edge on SCl will set the flag.

The bit counter is loaded with a value of 7, for the 7 bits in the address and then the program waits for GPC_REQ to be set (by a rising edge on SCL). This system has an advantage over waiting for a low and then high input on SCL in that if it arrives at the hold point for SCL to go high before the previous bit's clock pulse has gone low or after it's own clock pulse has gone high then it will not accidently read the previous bit again (the former case) or get stuck waiting for a low->high and miss a bit (the latter case) (I apologise for how confusing that is).

After the SCL has gone high the data is read off SDA into the carry flag, the carry flag is shifted into the lowest bit accumulator (which is shifted left once), GPC_REQ is cleared, the bit counter is decreased and the loop is started again to get the next bit.

This is done until all bits of the address have been read and the bit counter reaches zero. At this point the address has been read into the accumulator (with the highest bit set as zero) and this is compared to the hard coded device address. At this point there is no implementation of the general call but it would certainly be a simple thing to implement.

====== The process described above of using GPC to detect edges by inverting the output is used from here forward ======

If the address does not match then the ISR is exited, with the GPC settings reset to again trigger on a falling edge of SDA.

If the address does match then there is a wait for another rising edge (GPC_REQ set) and the read/write bit is read.

Writing: Master -> Slave

The SDA line is pulled low for the SCL cycle and released when SCL returns low. GPC is changed to again trigger on rising edges. i2c_first_byte_flag is set, this is used later.

8 bits of data are now read in and ACK sent identically to reading in the address.

If it is the first byte then it is moved into the memory pointer, this is used as the base RAM address to start writing any proceeding bytes of data, or in the case of an end followed by a read it is the base RAM address to send data from.

If it is not the first byte then it is moved into RAM at the address given by the memory pointer and the pointer is incremented.

After the ACK is sent and the data has been moved to the appropriate place the program waits to see if there is another byte coming or a stop condition. If SDA is high then it can only be another byte (or a repeated start but this is NOT implemented and will just be read as anothey byte). If SDA is low then it could either be a stop condition or a low bit for the next byte. The accumulator is primed with a value of 7 for the timing to work in the case of another byte (there aren't enough cycles ast Fast-mode otherwise). GPC_REQ is set by a rising edge and then while SCL is high the program monitors SDA, if SDA goes high then it is a stop condition, if it does not go high and SCL falls then it is another byte and the get_byte loop starts again, with the zero already being in the accumulator from clearing it earlier there is no need to shift it in and the bit counter has already been set to 7 ready for this situation. i2c_first_byte_flag is also cleared here.

If a stop condition is seen then i2c_first_byte_flag is checked, if it is set then the ISR exits as the memory pointer has been set to the received byte ready for a read. However if the flag is not set then the pointer is reset to the address of the start of the I2C data, given as I2C_FIRST_DATA_INDEX.

Reading: Slave -> Master

This requires a bit of extra mucking around to get the timing to work. SDA is set low for the ACK at the end of reading the address and then before the SCL pulse finishes the setup gets going for a read. First, the GPC output is inverted to trigger GPC_REQ on falling edges and GPC_REQ is cleared. Then, the bit counter is loaded and the memory at the pointer address is transferred into the accumulator.

At this point it waits for the falling edge of the SCL ACK pulse, if this has already passed then it still would have set GPC_REQ (as the GPC was setup first) and so GPC_REQ is checked to go forward and then cleared before jumping over the setup to be used in further writes, straight into outputing to SDA.

Data is shifted out of the accumulator into the carry flag and then this flag is used to set SDA. If SDA is set as an input and then written low nothing will happen and this is used to save a cycle.

The byte is sent over the bus, changing after falling edges detected by the GPC.

At the end of the byte ACK is checked, if ACK is received then the memory pointer is incremented, the data at that pointer is moved into the accumulator and it is sent out over the bus. If a NACK is received then the read finishes and the ISR is exited. Resetting the GPC settings identically to exiting a write.