Skip to content

Commit

Permalink
Improve I2C slave library, handle stop condition properly, add second…
Browse files Browse the repository at this point in the history
…ary address listening, remove GPIO configuration
  • Loading branch information
renzenicolai committed Mar 7, 2024
1 parent dcaf798 commit e894edb
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 48 deletions.
48 changes: 47 additions & 1 deletion examples/i2c_slave/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,50 @@ The library uses a one-byte address, allowing for up to 256 registers to be defi

The first byte written to the device within a transaction determines the offset for following reads and writes, emulating a simple EEPROM.

The example will turn on a LED connected to PD0 when the LSB of register 0 is set to 1 and off when it's set to 0.
The example will turn on a LED connected to PA2 when the LSB of register 0 is set to 1 and off when it's set to 0.

## Usage

Initialize the I2C1 SDA and SCL pins you want to use as open-drain outputs:
```
funPinMode(PC1, GPIO_CFGLR_OUT_10Mhz_AF_OD); // SDA
funPinMode(PC2, GPIO_CFGLR_OUT_10Mhz_AF_OD); // SCL
```

For chips other than the CH32V003 you will need to change the pin numbers to the pins corresponding with the I2C1 peripheral. If you want to use the alternative pins for the I2C periperal in addition to configuring the pins you have to configure the chip to use the the alternative pins using the `I2C1_RM` and `I2C1REMAP1` fields of the `AFIO_PCFR1` register.

Then initialize the I2C1 peripheral in slave mode using:

```
SetupI2CSlave(0x09, i2c_registers, sizeof(i2c_registers), onWrite, onRead, false);
```

In which `0x09` is the I2C address to listen on and i2c_registers is a pointer to a volitile uint8_t array.

The `onWrite` and `onRead` functions are optional callbacks used to react to the registers being written to or read from.

```
void onWrite(uint8_t reg, uint8_t length) {}
void onRead(uint8_t reg) {}
```

The last boolean argument is for making the registers read only via I2C.

You can also enable and disable writing using the functions

```
void SetI2CSlaveReadOnly(bool read_only);
void SetSecondaryI2CSlaveReadOnly(bool read_only);
```

The I2C1 peripheral can also listen on a secondary address. To enable this feature call the following function:

```
SetupSecondaryI2CSlave(0x42, i2c_registers2, sizeof(i2c_registers2), onWrite2, onRead2, false);
```

The function arguments are identical to the normal `SetupI2CSlave` function. The secondary I2C address acts like a completely separate I2C device with it's own registers.

Calling `SetupSecondaryI2CSlave` with the I2C address set to 0 disables listening on the secondary address.

It is recommended to react to register writes using the `onWrite` callback and not by reading the registers array from main(). There is a chance the compiler will optimize away your code if you do that.
24 changes: 12 additions & 12 deletions examples/i2c_slave/i2c_slave.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,21 @@

volatile uint8_t i2c_registers[32] = {0x00};

void onWrite(uint8_t reg, uint8_t length) {
funDigitalWrite(PA2, i2c_registers[0] & 1);
}

int main() {
SystemInit();
funGpioInitAll();

SetupI2CSlave(0x9, i2c_registers, sizeof(i2c_registers));
// Initialize I2C slave
funPinMode(PC1, GPIO_CFGLR_OUT_10Mhz_AF_OD); // SDA
funPinMode(PC2, GPIO_CFGLR_OUT_10Mhz_AF_OD); // SCL
SetupI2CSlave(0x9, i2c_registers, sizeof(i2c_registers), onWrite, NULL, false);

// Enable GPIOD and set pin 0 to output
RCC->APB2PCENR |= RCC_APB2Periph_GPIOD;
GPIOD->CFGLR &= ~(0xf<<(4*0));
GPIOD->CFGLR |= (GPIO_Speed_10MHz | GPIO_CNF_OUT_PP)<<(4*0);
// Initialize LED
funPinMode(PA2, GPIO_CFGLR_OUT_10Mhz_PP); // LED

while (1) {
if (i2c_registers[0] & 1) { // Turn on LED (PD0) if bit 1 of register 0 is set
GPIOD-> BSHR |= 1 << 16;
} else {
GPIOD-> BSHR |= 1;
}
}
while (1) {} // Do not let main exit, you can do other things here
}
141 changes: 106 additions & 35 deletions examples/i2c_slave/i2c_slave.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* MIT License
*
* Copyright (c) 2023 Renze Nicolai
* Copyright (c) 2024 Renze Nicolai
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
Expand All @@ -27,37 +27,50 @@
#ifndef __I2C_SLAVE_H
#define __I2C_SLAVE_H

#include "ch32v003fun.h"
#include <stdint.h>
#include <stdio.h>
#include <stdbool.h>

#define APB_CLOCK FUNCONF_SYSTEM_CORE_CLOCK
typedef void (*i2c_write_callback_t)(uint8_t reg, uint8_t length);
typedef void (*i2c_read_callback_t)(uint8_t reg);

struct _i2c_slave_state {
uint8_t first_write;
uint8_t offset;
uint8_t position;
volatile uint8_t* volatile registers;
uint8_t size;
volatile uint8_t* volatile registers1;
uint8_t size1;
volatile uint8_t* volatile registers2;
uint8_t size2;
i2c_write_callback_t write_callback1;
i2c_read_callback_t read_callback1;
bool read_only1;
i2c_write_callback_t write_callback2;
i2c_read_callback_t read_callback2;
bool read_only2;
bool writing;
bool address2matched;
} i2c_slave_state;

void SetupI2CSlave(uint8_t address, volatile uint8_t* registers, uint8_t size) {
void SetupI2CSlave(uint8_t address, volatile uint8_t* registers, uint8_t size, i2c_write_callback_t write_callback, i2c_read_callback_t read_callback, bool read_only) {
i2c_slave_state.first_write = 1;
i2c_slave_state.offset = 0;
i2c_slave_state.position = 0;
i2c_slave_state.registers = registers;
i2c_slave_state.size = size;

// Enable GPIOC and I2C
RCC->APB2PCENR |= RCC_APB2Periph_GPIOC;
i2c_slave_state.registers1 = registers;
i2c_slave_state.size1 = size;
i2c_slave_state.registers2 = NULL;
i2c_slave_state.size2 = 0;
i2c_slave_state.write_callback1 = write_callback;
i2c_slave_state.read_callback1 = read_callback;
i2c_slave_state.read_only1 = read_only;
i2c_slave_state.write_callback2 = NULL;
i2c_slave_state.read_callback2 = NULL;
i2c_slave_state.read_only2 = false;

// Enable I2C1
RCC->APB1PCENR |= RCC_APB1Periph_I2C1;

// PC1 is SDA, 10MHz Output, alt func, open-drain
GPIOC->CFGLR &= ~(0xf<<(4*1));
GPIOC->CFGLR |= (GPIO_Speed_10MHz | GPIO_CNF_OUT_OD_AF)<<(4*1);

// PC2 is SCL, 10MHz Output, alt func, open-drain
GPIOC->CFGLR &= ~(0xf<<(4*2));
GPIOC->CFGLR |= (GPIO_Speed_10MHz | GPIO_CNF_OUT_OD_AF)<<(4*2);

// Reset I2C1 to init all regs
RCC->APB1PRSTR |= RCC_APB1Periph_I2C1;
RCC->APB1PRSTR &= ~RCC_APB1Periph_I2C1;
Expand All @@ -67,65 +80,123 @@ void SetupI2CSlave(uint8_t address, volatile uint8_t* registers, uint8_t size) {

// Set module clock frequency
uint32_t prerate = 2000000; // I2C Logic clock rate, must be higher than the bus clock rate
I2C1->CTLR2 |= (APB_CLOCK/prerate) & I2C_CTLR2_FREQ;
I2C1->CTLR2 |= (FUNCONF_SYSTEM_CORE_CLOCK/prerate) & I2C_CTLR2_FREQ;

// Enable interrupts
I2C1->CTLR2 |= I2C_CTLR2_ITBUFEN;
I2C1->CTLR2 |= I2C_CTLR2_ITEVTEN; // Event interrupt
I2C1->CTLR2 |= I2C_CTLR2_ITERREN; // Error interrupt
I2C1->CTLR2 |= I2C_CTLR2_ITBUFEN | I2C_CTLR2_ITEVTEN | I2C_CTLR2_ITERREN;

NVIC_EnableIRQ(I2C1_EV_IRQn); // Event interrupt
NVIC_SetPriority(I2C1_EV_IRQn, 2 << 4);
NVIC_EnableIRQ(I2C1_ER_IRQn); // Error interrupt
NVIC_SetPriority(I2C1_ER_IRQn, 2 << 4);

// Set clock configuration
uint32_t clockrate = 1000000; // I2C Bus clock rate, must be lower than the logic clock rate
I2C1->CKCFGR = ((APB_CLOCK/(3*clockrate))&I2C_CKCFGR_CCR) | I2C_CKCFGR_FS; // Fast mode 33% duty cycle
//I2C1->CKCFGR = ((APB_CLOCK/(25*clockrate))&I2C_CKCFGR_CCR) | I2C_CKCFGR_DUTY | I2C_CKCFGR_FS; // Fast mode 36% duty cycle
//I2C1->CKCFGR = (APB_CLOCK/(2*clockrate))&I2C_CKCFGR_CCR; // Standard mode good to 100kHz
I2C1->CKCFGR = ((FUNCONF_SYSTEM_CORE_CLOCK/(3*clockrate))&I2C_CKCFGR_CCR) | I2C_CKCFGR_FS; // Fast mode 33% duty cycle
//I2C1->CKCFGR = ((FUNCONF_SYSTEM_CORE_CLOCK/(25*clockrate))&I2C_CKCFGR_CCR) | I2C_CKCFGR_DUTY | I2C_CKCFGR_FS; // Fast mode 36% duty cycle
//I2C1->CKCFGR = (FUNCONF_SYSTEM_CORE_CLOCK/(2*clockrate))&I2C_CKCFGR_CCR; // Standard mode good to 100kHz

// Set I2C address
I2C1->OADDR1 = address << 1;
I2C1->OADDR2 = 0;

// Enable I2C
I2C1->CTLR1 |= I2C_CTLR1_PE;

// Acknowledge the first address match event when it happens
// Acknowledge bytes when they are received
I2C1->CTLR1 |= I2C_CTLR1_ACK;
}

void SetupSecondaryI2CSlave(uint8_t address, volatile uint8_t* registers, uint8_t size, i2c_write_callback_t write_callback, i2c_read_callback_t read_callback, bool read_only) {
if (address > 0) {
I2C1->OADDR2 = (address << 1) | 1;
i2c_slave_state.registers2 = registers;
i2c_slave_state.size2 = size;
i2c_slave_state.write_callback2 = write_callback;
i2c_slave_state.read_callback2 = read_callback;
i2c_slave_state.read_only2 = read_only;
} else {
I2C1->OADDR2 = 0;
}
}

void SetI2CSlaveReadOnly(bool read_only) {
i2c_slave_state.read_only1 = read_only;
}

void SetSecondaryI2CSlaveReadOnly(bool read_only) {
i2c_slave_state.read_only2 = read_only;
}

void I2C1_EV_IRQHandler(void) __attribute__((interrupt));
void I2C1_EV_IRQHandler(void) {
uint16_t STAR1, STAR2 __attribute__((unused));
STAR1 = I2C1->STAR1;
STAR2 = I2C1->STAR2;

I2C1->CTLR1 |= I2C_CTLR1_ACK;

if (STAR1 & I2C_STAR1_ADDR) { // Start event
i2c_slave_state.first_write = 1; // Next write will be the offset
i2c_slave_state.position = i2c_slave_state.offset; // Reset position
i2c_slave_state.address2matched = !!(STAR2 & I2C_STAR2_DUALF);
}

if (STAR1 & I2C_STAR1_RXNE) { // Write event
if (i2c_slave_state.first_write) { // First byte written, set the offset
i2c_slave_state.offset = I2C1->DATAR;
i2c_slave_state.position = i2c_slave_state.offset;
i2c_slave_state.first_write = 0;
i2c_slave_state.writing = false;
} else { // Normal register write
if (i2c_slave_state.position < i2c_slave_state.size) {
i2c_slave_state.registers[i2c_slave_state.position] = I2C1->DATAR;
i2c_slave_state.position++;
i2c_slave_state.writing = true;
if (i2c_slave_state.address2matched) {
if (i2c_slave_state.position < i2c_slave_state.size2 && !i2c_slave_state.read_only2) {
i2c_slave_state.registers2[i2c_slave_state.position] = I2C1->DATAR;
i2c_slave_state.position++;
}
} else {
if (i2c_slave_state.position < i2c_slave_state.size1 && !i2c_slave_state.read_only1) {
i2c_slave_state.registers1[i2c_slave_state.position] = I2C1->DATAR;
i2c_slave_state.position++;
}
}
}
}

if (STAR1 & I2C_STAR1_TXE) { // Read event
if (i2c_slave_state.position < i2c_slave_state.size) {
I2C1->DATAR = i2c_slave_state.registers[i2c_slave_state.position];
i2c_slave_state.position++;
i2c_slave_state.writing = false;
if (i2c_slave_state.address2matched) {
if (i2c_slave_state.position < i2c_slave_state.size2) {
I2C1->DATAR = i2c_slave_state.registers2[i2c_slave_state.position];
if (i2c_slave_state.read_callback2 != NULL) {
i2c_slave_state.read_callback2(i2c_slave_state.position);
}
i2c_slave_state.position++;
} else {
I2C1->DATAR = 0x00;
}
} else {
I2C1->DATAR = 0x00;
if (i2c_slave_state.position < i2c_slave_state.size1) {
I2C1->DATAR = i2c_slave_state.registers1[i2c_slave_state.position];
if (i2c_slave_state.read_callback1 != NULL) {
i2c_slave_state.read_callback1(i2c_slave_state.position);
}
i2c_slave_state.position++;
} else {
I2C1->DATAR = 0x00;
}
}
}

if (STAR1 & I2C_STAR1_STOPF) { // Stop event
I2C1->CTLR1 &= ~(I2C_CTLR1_STOP); // Clear stop
if (i2c_slave_state.address2matched) {
if (i2c_slave_state.write_callback2 != NULL) {
i2c_slave_state.write_callback2(i2c_slave_state.offset, i2c_slave_state.position - i2c_slave_state.offset);
}
} else {
if (i2c_slave_state.write_callback1 != NULL) {
i2c_slave_state.write_callback1(i2c_slave_state.offset, i2c_slave_state.position - i2c_slave_state.offset);
}
}
}
}
Expand Down

0 comments on commit e894edb

Please sign in to comment.