# I2C bit banging(手刻I2C)
github連結:https://github.com/lisyuanhao/I2C-bit-banging
## 🎉 Bit banging與I2C
### Bit banging
有一些MCU沒有內建通訊協定的硬體模組,例如MSP430F1232,因此,任何需要用I2C通訊的感測器都必須使用軟體模擬,master會負責同步與slave的通訊,資料部分則會由master或slave控制。具體作法是控制GPIO的高低電位來模擬I2C的通訊過程,那本次實驗則是以ESP32-S3為例。
> Sometimes, processors do not have built in hardware
support for the universal serial communication. In such
case, design your own code to implement serial
communication, which is known as bit banging. For
example, the MSP430F1232 MCU does not have any kind
of built in hardware serial communication support.
Therefore, to add any I2C serial device in a project based on
this MCU, you have to create code to handle the
communication. So, the challenge is to write bit banging
I2C slave driver using C programming language.The Slave
will be synchronized by the master clock, and the data
portion will be driven by either the master or the slave.
<cite>Ref:**Halder et al**.Implementation of MCU Invariant I2C Slave Driver Using Bit Banging.ICONS 2011 : The Sixth International Conference on Systems.January 2011
</cite>
### I2C
I²C (Inter Integrated Circuit)是由飛利浦(Philips)於西元1980年代所發表的二線式雙向串列匯流排(Serial Bus)標準,其技術主要目的是應用於IC之間的溝通,**優勢就是只需要兩條線就能實現多主從架構的通訊,一條為SCL,只由master負責控制,用來同步訊號,另一條為SDA,負責傳送地址或資料**。其通訊過程如下:

閒置時,兩條線為高電位,並且只有在SCL為低電位時,SDA上的狀態能夠被改變,SCL為高電位時,SDA上的狀態為有效。因此當master準備傳送地址時,會在SCL為高時,將SDA拉低,以發送start condition,因為正常是SCL為低時,SDA的狀態才能被改變,因此當這種狀況發生時,就能讓slave知道master要準備傳地址或是傳輸完畢(stop condition)。
常見的地址長度為7 bit加一位讀寫bit,也就是說master最多能接128個slave,並且不管是傳地址或資料,一次都是傳一個byte為單位。在傳完地址或資料後,會等待接收方在第9個clock時回傳一個ACK/NACK,確認是否正確收到資料,若將SDA拉低為ACK,若維持高電位則為NACK。直到傳完資料後回傳stop condiction。
### 寫入感測器資料
必須先寫0x00到此暫存器以喚醒感測器,否則讀不到回傳數值。

**Description:**
This register allows the user to configure the power mode and clock source. It also provides a bit for
resetting the entire device, and a bit for disabling the temperature sensor.
**By setting SLEEP to 1, the MPU-60X0 can be put into low power sleep mode.**
### 讀取感測器資料
然而,因為我們是要讀感測器中,某個暫存器內的資料,因此在傳地址時,實際上會如以下方式:

<cite>Ref:**Jonathan Valdez, Jared Becker**.Understanding the I2C Bus.<cite>
> Reading from a slave is very similar to writing, but with some extra steps. In order to read from a slave,
the master must first instruct the slave which register it wishes to read from. This is done by the master
starting off the transmission in a similar fashion as the write, by sending the address with the R/W bit
equal to 0 (signifying a write), followed by the register address it wishes to read from.Once the slave
acknowledges this register address, the master will send a START condition again, followed by the slave
address with the R/W bit set to 1 (signifying a read). This time, the slave will acknowledge the read
request, and the master releases the SDA bus, but will continue supplying the clock to the slave. During
this part of the transaction, the master will become the master-receiver, and the slave will become the
slave-transmitter
在傳完感測器地址後,會再傳一個Register Address(8 bits),後,會再傳送一次start condiction+slave address。
## 🎉 硬體架構
:::info
本次實驗以 ESP32-S3 與六軸感測器進行 XYZ 三軸加速度計的通訊,並搭配邏輯分析儀觀察其波形。
:::
### ESP32-S3:

### 六軸感測器(MPU6050):

以下是MPU6050中,存XYZ三軸加速度計的暫存器位置,每一軸由兩個8bit暫存器存,以X軸來說,`ACCEL_XOUT_H`為高位暫存器,`ACCEL_XOUT_L`則為低位暫存器。因此會需要先將高位暫存器左移8 bits後跟低位暫存器做or operation,因此一個軸的數值範圍可以用16個bit來表示,我們總共需要讀6個暫存器。

### 腳位對應:
| MPU6050 | ESP32 | LA |
|:-------:|:------:|:---:|
| VCC | 3V3 | 無 |
| GND | G | GND |
| SDA | GPIO17 | CH1 |
| SCL | GPIO18 | CH2 |
### 實體電路:

## 🎉 Code與其對應的邏輯分析儀波形
下圖為一次完整通訊的邏輯分析儀波形,包括寫入`Power Management 1`暫存器和讀取感測器資料,綠點為`start condition`,橘點為`stop condition`,接下來將針對各段程式碼及其對應的波形進行逐步說明。

### Init & Send start condition
起始訊號為SCL=1時SDA被改為低電位,從邏輯分析儀的波形可以看出將GPIO pins設成輸入輸出模式時,會被拉成低電位,因此必須要先設定SCL,再設定SDA。若先設定SDA導致其被拉低,SCL維持高電位,會被誤判為start condition。

```c
void i2c_init(uint8_t scl, uint8_t sda){
gpio_set_direction(scl, GPIO_MODE_INPUT_OUTPUT);
gpio_set_direction(sda, GPIO_MODE_INPUT_OUTPUT);
gpio_set_level(scl, 1);
gpio_set_level(sda, 1);
}
void i2c_start(uint8_t scl, uint8_t sda){
gpio_set_level(scl, 1);
gpio_set_level(sda, 1);
esp_rom_delay_us(5);
gpio_set_level(sda, 0);
esp_rom_delay_us(5);
gpio_set_level(scl, 0);
}
```
### Write byte
由MSB開始寫入,在第9個clock時接收SDA,若為低電位代表ACK,感測器有順利收到。
```c
int i2c_write_byte(uint8_t data, uint8_t scl, uint8_t sda){
uint8_t msb;
for (int i = 0; i<8; i++){
msb = (data >> (7-i)) & 0x01;
gpio_set_level(sda, msb);
esp_rom_delay_us(5);
gpio_set_level(scl, 1);
esp_rom_delay_us(5);
gpio_set_level(scl, 0);
}
gpio_set_level(scl, 1);
int ack = gpio_get_level(sda);
gpio_set_level(scl, 0);
return ack;
}
```
### Read byte
開始先將SCL拉高以讀取SDA,讀完後判斷是否需要繼續讀,若需要則回傳ACK,不需要則回傳NACK。
```c
void i2c_read_byte(uint8_t* buf, uint8_t idx, uint8_t num_reg, uint8_t scl, uint8_t sda){
uint8_t reg_data = 0;
for (int i = 0; i<8; i++){
reg_data = reg_data << 1;
gpio_set_level(scl, 1);
esp_rom_delay_us(5);
reg_data |= gpio_get_level(sda);
gpio_set_level(scl, 0);
esp_rom_delay_us(5);
}
buf[idx] = reg_data;
if (idx==num_reg)
gpio_set_level(sda, 1);//over:NACK
else
gpio_set_level(sda, 0);//continue:ACK
esp_rom_delay_us(5);
gpio_set_level(scl, 1);
esp_rom_delay_us(5);
gpio_set_level(scl, 0);
gpio_set_level(sda, 1);
}
```
### Send stop condition
先將SCL、SDA都改成低電位後,將SCL拉高再將SDA拉高,以符合停止訊號。
```c
void i2c_stop(uint8_t scl, uint8_t sda){
gpio_set_level(scl, 0);
esp_rom_delay_us(5);
gpio_set_level(sda, 0);
esp_rom_delay_us(5);
gpio_set_level(scl, 1);
esp_rom_delay_us(5);
gpio_set_level(sda, 1);
esp_rom_delay_us(5);
}
```
### 寫入PWR_MGMT_1以喚醒感測器
寫入感測器地址後接著寫入0x6B暫存器,再接著寫入0x00。

```c
void wakeup_mpu6050(uint8_t scl, uint8_t sda){
i2c_init(SCL, SDA);
i2c_start(scl, sda);
if (!i2c_write_byte(MPU6050_ADDR << 1 | W_BIT, scl, sda)){
if (!i2c_write_byte(MPU6050_PWR_MGMT_1, scl, sda)){
if (!i2c_write_byte(WAKEUP_VAL, scl, sda)){
i2c_stop(scl, sda);
}
}
else{
printf("I2C write reg failed!\n");
i2c_stop(scl, sda);
}
}
else{
printf("I2C write sensor addr failed!\n");
i2c_stop(scl, sda);
}
}
```
**若是沒有喚醒感測器,會發現波形呈現如下,前面寫地址(0x68)與暫存器(0x3B)都有回傳ACK,代表感測器其實是能正確接收到地址,但因為沒有喚醒造成後面讀到的數值皆為0。**

### 讀取加速度計
第一次寫地址時,因為接下來需寫入暫存器地址,因此後面會加上一個write bit(0),寫入暫存器後會需要再發送一次start condition跟0x68+read bit(1),後續就讀取回傳數值,再將高位暫存器左移8 bit與低位暫存器做or,得到在x軸上的加速度計。

```c
void read_mpu6050_accel(uint8_t scl, uint8_t sda, uint8_t *accel){
i2c_init(SCL, SDA);
i2c_start(scl, sda);
if (!i2c_write_byte(MPU6050_ADDR << 1 | W_BIT, scl, sda)) {
if (!i2c_write_byte(ACCEL_XOUT_H, scl, sda)) {
i2c_start(scl, sda); // Repeated START
if (!i2c_write_byte((MPU6050_ADDR << 1) | R_BIT, scl, sda)) {
for (uint8_t i = 0; i < NUM_REG; i++){
i2c_read_byte(accel, i, NUM_REG, scl, sda);
}
i2c_stop(scl, sda);
int16_t accel_x = (accel[0] << 8) | accel[1];
int16_t accel_y = (accel[2] << 8) | accel[3];
int16_t accel_z = (accel[4] << 8) | accel[5];
printf("Accel X: %d, Y: %d, Z: %d\n", accel_x, accel_y, accel_z);
}
else{
printf("I2C read failed!\n");
i2c_stop(scl, sda);
}
}
else{
printf("I2C write reg failed!\n");
i2c_stop(scl, sda);
}
}
else{
printf("I2C write sensor addr failed!\n");
i2c_stop(scl, sda);
}
}
```
## 🎉 DEMO
{%youtube 8Uby2KlBZDE %}