Mirror with some correction
Dependencies: mbed FastIO FastPWM USBDevice
BitBangI2C/BitBangI2C.h
- Committer:
- arnoz
- Date:
- 2021-10-01
- Revision:
- 116:7a67265d7c19
- Parent:
- 87:8d35c74403af
File content as of revision 116:7a67265d7c19:
// Bit-bang I2C for KL25Z // // This implements an I2C interface that can operate on any KL25Z GPIO // ports, whether or not they're connected to I2C hardware on the MCU. // We simply send and receive bits using direct port manipulation (often // called "bit banging") instead of using the MCU I2C hardware. This // is more flexible than the mbed I2C class, since that only works with // a small number of pins, and there are only two I2C modules in the // system. This GPIO version can be to gain additional I2C ports if // the hardware I2C modules are committed to other purposes, or all of // the I2C-capable pins are being used for other purposes. // // The tradeoff for the added flexibility is that the hardware I2C is // faster. This implementation can take advantage of bus speeds up to // about 500kHz, which produces data rates of about 272 kbps. Higher // clock speeds are allowed, but the actual bit rate will plateau at // this level due to the performance constraints of the CPU (and of // this code itself; some additional performance could probably be // gained by optimizing it further). The KL25Z I2C hardware can double // our speed: it can achieve bus speeds of 1MHz and data rates of about // 540kbps. Of course, such high speeds can only be used with compatible // devices; many devices are limited to the "standard mode" at 100kHz or // "fast mode" at 400kHz, both of which we can fully saturate. However, // even at the slower bus speeds, the hardware I2C has another advantage: // it's capable of DMA operation. That's vastly superior for large // transactions since it lets the CPU do other work in parallel with // I2C bit movement. // // This class isn't meant to be directly compatible with the mbed I2C // class, but we try to adhere to the mbed conventions and method names // to make it a mostly drop-in replacement. In particular, we use the // mbed library's "2X" device address convention. Most device data sheets // list the device I2C address in 7-bit format, so you'll have to shift // the nominal address from the data sheet left one bit in each call // to a routine here. // // Electrically, the I2C bus consists of two lines, SDA (data) and SCL // (clock). Multiple devices can connect to the bus by connecting to // these two lines; the lines are shared among all of the devices. Each // line has a pull-up resistor that pulls it to logic '1' voltage. Each // device connects with an open-collector circuit that can short the line // to ground (logic '0'). This means that any device can assert a 'low' // but no one can actually assert a 'high'; the pull-up makes it so that // a 'high' occurs when no one is asserting a 'low'. On an MCU, we release // a line by putting the GPIO pin in high-Z state, which we can do on the // KL25Z by setting its direction to INPUT mode. So our GPIO write strategy // is like this: // // - take a pin low (0): // pin.input(); // pin.write(0); // // - take a pin high (1): // pin.output(); // // Note that we don't actually have to write the '0' on each pull low, // since we just leave the port output register set with '0'. Changing // the direction to output is enough to assert the low level, since the // hardware asserts the level that was previously stored in the output // register whenever the direction is changed from input to output. // // The KL25Z by default provides a built-in pull-up resistor on each GPIO // set to input mode. This can optionally be used as the bus-wide pull-up // for each line. Standard practice is to use external pull-up resistors // rather than MCU pull-ups, but the internal pull-ups are fine for ad hoc // setups where there's only one external device connected to a GPIO pair. #ifndef _BITBANGI2C_H_ #define _BITBANGI2C_H_ #include "mbed.h" #include "gpio_api.h" #include "pinmap.h" // For testing purposes: a cover class for the mbed library I2C bridging // the minor differences in our interface. This allows switching between // BitBangI2C and the mbed library I2C via a macro of the like. class MbedI2C: public I2C { public: MbedI2C(PinName sda, PinName scl, bool internalPullups) : I2C(sda, scl) { } int write(int addr, const uint8_t *data, size_t len, bool repeated = false) { return I2C::write(addr, (const char *)data, len, repeated); } int read(int addr, uint8_t *data, size_t len, bool repeated = false) { return I2C::read(addr, (char *)data, len, repeated); } void reset() { } }; // DigitalInOut replacmement class for I2C use. I2C uses pins a little // differently from other use cases. I2C is a bus, where many devices can // be attached to each line. To allow this shared access, devices can // only drive the line low. No device can drive the line high; instead, // the line is *pulled* high, by the attached pull-up resistors, when no // one is driving it low. As a result, we can't use the normal DigitalOut // write(), since that would try to actively drive the pin high on write(1). // Instead, write(1) needs to change the pin to high-impedance (high-Z) // state instead of driving it, which on the KL25Z is accomplished by // changing the port direction mode to INPUT. So: // // write(0) = direction->OUTPUT (pin->0) // write(1) = direction->INPUT // class I2CInOut { public: I2CInOut(PinName pin, bool internalPullup) { // initialize the pin gpio_t g; gpio_init(&g, pin); // get the registers unsigned int portno = (unsigned int)pin >> PORT_SHIFT; uint32_t pinno = (uint32_t)(pin & 0x7C) >> 2; FGPIO_Type *r = (FGPIO_Type *)(FPTA_BASE + portno*0x40); __IO uint32_t *pin_pcr = &(((PORT_Type *)(PORTA_BASE + 0x1000*portno)))->PCR[pinno]; // set the desired internal pull-up mode if (internalPullup) *pin_pcr |= 0x02; else *pin_pcr &= ~0x02; // save the register information we'll need later this->mask = g.mask; this->PDDR = &r->PDDR; this->PDIR = &r->PDIR; // initially set as input to release the line r->PDDR &= ~mask; // Set the output value to 0. It will always be zero, since // this is the only value we ever drive. When we want the port // to go high, we release it by changing the direction to input. r->PCOR = mask; } // write a 1 (high) or 0 (low) value to the pin inline void write(int b) { if (b) hi(); else lo(); } // Take the line high: set as input to put it in high-Z state so that // the pull-up resistor takes over. inline void hi() { *PDDR &= ~mask; } // Take the line low: set as output to assert our '0' on the line and // pull it low. Note that we don't have to explicitly write the port // output register, since we initialized it with a '0' on our port and // never change it. The hardware will assert the level stored in the // register each time we change the direction to output, so there's no // need to write the port output register again each time. inline void lo() { *PDDR |= mask; } // read the line inline int read() { *PDDR &= ~mask; // set as input return *PDIR & mask; // read the port } // direction register volatile uint32_t *PDDR; // input register volatile uint32_t *PDIR; // pin mask uint32_t mask; }; // bit-bang I2C class BitBangI2C { public: // create the interface BitBangI2C(PinName sda, PinName scl, bool internalPullups); // set the bus frequency in Hz void frequency(uint32_t freq); // set START condition on the bus void start(); // set STOP condition on the bus void stop(); // Write a series of bytes. Returns 0 on success, non-zero on failure. // Important: 'addr' is 2X the nominal address - shift left by one bit. int write(uint8_t addr, const uint8_t *data, size_t len, bool repeated = false); // write a byte; returns true if ACK was received int write(uint8_t data); // Read a series of bytes. Returns 0 on success, non-zero on failure. // Important: 'addr' is 2X the nominal address - shift left by one bit. int read(uint8_t addr, uint8_t *data, size_t len, bool repeated = false); // read a byte, optionally sending an ACK on receipt int read(bool ack); // wait for ACK; returns true if ACK was received bool wait(uint32_t timeout_us); // reset the bus void reset(); protected: // read/write a bit int readBit(); // write a bit inline void writeBit(int bit) { // put the bit on the SDA line sdaPin.write(bit); hiResWait(tSuDat); // clock it sclPin.hi(); hiResWait(tHigh); // drop the clock sclPin.lo(); hiResWait(tLow); } // set SCL/SDA lines to high (1) or low(0) inline void scl(int level) { sclPin.write(level); } inline void sda(int level) { sdaPin.write(level); } inline void sclHi() { sclPin.hi(); } inline void sclLo() { sclPin.lo(); } inline void sdaHi() { sdaPin.hi(); } inline void sdaLo() { sdaPin.lo(); } // SDA and SCL pins I2CInOut sdaPin; I2CInOut sclPin; // inverse of frequency = clock period in microseconds uint32_t clkPeriod_us; // High-resolution wait. This provides sub-microsecond wait // times, to get minimum times for I2C events. With the ARM // compiler, this produces measured wait times as follows: // // n=0 104ns // n=1 167ns // n=2 271ns // n=3 375ns // n=4 479ns // // For n > 1, the wait time is 167ns + (n-1)*104ns. // These times take into account caller overhead to load the // wait time from a member variable. Callers getting the wait // time from a constant or stack variable will have different // results. inline void hiResWait(volatile int n) { while (n != 0) --n; } // Figure the hiResWait() time for a given nanosecond time. // We use this during setup to precompute the wait times required // for various events at a given clock speed. int calcHiResWaitTime(int nanoseconds) { // the shortest wait time is 104ns if (nanoseconds <= 104) return 0; // Above that, we work in 104ns increments with a base // of 167ns. We round at the halfway point, because we // assume there's always a little extra overhead in the // caller itself that will pad by at least one instruction // of 60ns, which is more than half our interval. return (nanoseconds - 167 + 52)/104 + 1; } // Time delays for I2C events. I2C has minimum timing requirements // based on the clock speed. Some of these are as short as 50ns. // The mbed wait timer has microsecond resolution, which is much // too coarse for fast I2C clock speeds, so we implement our own // finer-grained wait. // // These are in hiResWait() units - see above. // int tLow; // SCL low period int tHigh; // SCL high period int tHdSta; // hold time for start condition int tSuSta; // setup time for repeated start condition int tSuSto; // setup time for stop condition int tSuDat; // data setup time int tAck; // ACK time int tBuf; // bus free time between start and stop conditions // are we in a Stop condition? bool inStop; }; #endif /* _BITBANGI2C_H_ */