Sending data over I2C using bit-banging

This post shows how to send data from an STM32 using the HAL. Hints on adapting it to other mcus is also given. So it is useful for actually understanding the mechanics of sending data.

CubeMX

In CubeMX, set PA9 and PA10 as GPIO_Output pins. In the configuration section, set:

  • GPIO output level: High
  • GPIO mode: Output Open Drain
  • GPIO pull-up/pull-down: Pull-up

This has the effect that the lines, by default, are high. This is the standard for I2C. So you won’t need to add pull-up resistors to your circuit. Your mcu will do this for you.

“Open drain” has the effect that a line can only pulled low, rather than forced high. The default will still be high, though. This is important, because it means that the slave can pull the line low itself to assert ACK. Left high, it is NACK.

We will use PA9 as the SCL pin, and PA10 as SDA.

Code

void sda(int v)
{
	HAL_GPIO_WritePin(GPIOA, GPIO_PIN_10, v ? GPIO_PIN_SET : GPIO_PIN_RESET);
}
void scl(int v)
{
	HAL_GPIO_WritePin(GPIOA, GPIO_PIN_9, v ? GPIO_PIN_SET : GPIO_PIN_RESET);
}

void gen_start(void) { 	sda(0); sda(0);}

void send_bit(int v)
{
	scl(0);
	sda(v);
	scl(1);
}

void ack(void)
{
	send_bit(1);
	scl(0);
}
void gen_stop(void) { 	sda(0); scl(1); sda(1);}

void send_byte_w_ack(uint8_t b)
{
	for(int i = 0; i < 8; i++) {
		scl(0);
		sda(b & 0b10000000);
		b <<= 1;
		scl(1);
	}
	ack();
}

void i2c_send(uint8_t sid, uint8_t *data, int n)
{
	gen_start(); // generate start condition
	send_byte_w_ack(sid<<1); // send the slave address
	while(n--) send_byte_w_ack(*data++); // send the data
	gen_stop(); // generate stop condition
}

That’s it, really. Not all that complicated when you’ve sort it out.

To send data, call the i2c_send() function. It generates a “start condition”, sends the address, then each byte in turn, then finally the stop condition.

sda(v) sends SDA low if v is 0; high otherwise. Function scl(v) manipulates the SCL line likewise.

As you can see, to generate a START condition, you pull SDA low whilst SCL is high.

The STOP condition is also fairly straightforward: make sure SDA is low, pull SCL high, then pull SDA high.

send_byte_w_ack(): bytes are sent one bit at a time, starting at the MSB. To send a bit, pull SCL low, set SDA to either 0 or 1, and release SCL. The slave will read the SDA value when SCL is high.

After sending 8 bits, you send an extra bit for ACK (acknowledgement), then set SCL low. I say “send”, but what I really mean is “receive”. The slave has an opportunity to either ACK or NACK the byte. For simplicity, examining the ACK value has not been implemented in this algorithm. You’ll have to do it yourself. I generally hope for the best. If the slave can’t acknowledge, then you’re in a bad place anyway. (See also: NACK and ACK responses on I2c bus)

Sending the address

You probably know that the sid (slave id) is a device number that is recognised by the particular slave that you want to address. It is 7 bits long (although 10-bit ones exist, I won’t cover them here). There are two ways of addressing a slave according to the standard: write mode (where you are sending data to a device) and read mode (where you are receiving it). This post doesn’t cover read.

To address a slave, you send its left-shifted address. This leaves the LSB to indicate whether the master wants to perform a read or a write. LSB=0 means write, LSB=1 means read. In our case, we are just considering devices that can only be written to. Things like sensors are probably read-only. There are likely to be some other devices that can be written or read to.

Bit-banging speed

The STM32 HAL is a bit of a plodder when it comes to changing pin states. It is slow enough for I2C communication, which is quite a slow protocol anyway. If you use something other than the HAL, like the Low-Level library or a different mcu, make sure that the transmission isn’t happening too fast for the slave to handle.

Clock stretching

According to the protocol, a slave is allowed to slow down communication by holding SCL low. So what the master should do is release the close into a high state (calling scl(1)), and actually check the line has actually gone high, waiting if necessary. This has not been implemented in this article.

Wrapping up

So that’s about it. The simplest thing that could possibly work. I have assumed at least some knowledge of I2C, so I have glossed over the basics, whilst hopefully introduced you to some details that you may not have been clear in your mind. Happy programming.

Ref: db07.78

About mcturra2000

Computer programmer living in Scotland.
This entry was posted in Uncategorized. Bookmark the permalink.

Leave a comment