.gitignore | ||
I2C Definitions.asm | ||
I2C Slave ASM.asm | ||
README.md |
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 whileSCL
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.
- If
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.