I2C (Устаревшая статья)

Материал из MIK32 микроконтроллер

Статья находится в разработке.

Пример использования I2C MIK32

Рисунок 1 - Схема подключения

В данном примере используется две отладочные платы с микроконтроллером MIK32, которые соединены как показано на рисунке 1. Первая плата используется как ведущее устройство (Master), вторая используется как ведомое (Slave).

Ведущий передает ведомому число размером 2 байта. Ведомый принимает это число и увеличивает на единицу. Затем ведущий принимает число от ведомого. Так продолжается до тех пор, пока число, после увеличения ведомым, не станет больше 65530.

Ведущий

Переменные ведущего:

- uint8_t slave_address - адрес ведомого, с которым ведущий обменивается данными;

- uint16_t to_send - число из двух байт;

- uint8_t data[2] - массив с байтами для отправки / приема ведущим.

Массив data заполняется байтами числа to_send.

Для передачи данных используется I2C0, который имеет следующие выводы:

- PORT_0_9 - I2C0_sda;

- PORT_0_10 - I2C0_scl.

Инициализация

Сначала требуется провести инициализацию. Это делается с помощью функции i2c_master_init. Эта функция включает тактирование необходимых блоков для работы GPIO и I2C. Затем следует настройка регистра I2C_TIMINGR, где выставляется делитель частоты и задержки. В конце для включения интерфейса в регистре I2C_CR1 выставляется бит PE. После этого можно отправлять и принимать данные.

Функция принимает аргумент типа I2C_TypeDef* i2c. Это может быть I2C_0 или I2C_1.

Функция i2c_master_init

void i2c_master_init(I2C_TypeDef* i2c)
{
    //Включаем тактирование необходимых блоков и модуля выбора режима GPIO 
    PM->CLK_APB_P_SET |= PM_CLOCK_GPIO_0_M | PM_CLOCK_I2C_0_M;
    PM->CLK_APB_M_SET |= PM_CLOCK_PAD_CONFIG_M | PM_CLOCK_WU_M | PM_CLOCK_PM_M ;
    for (volatile int i = 0; i < 10; i++);


    // обнуление регистра управления
    i2c->CR1 = 0;

    /*
    * Инициализация i2c
    * TIMING - регистр таймингов
    * 
    * SCLDEL - Задержка между изменением SDA и фронтом SCL в режиме ведущего и ведомого при NOSTRETCH = 0
    * 
    * SDADEL - Задержка между спадом SCL и изменением SDA в режиме ведущего и ведомого при NOSTRETCH = 0
    * 
    * SCLL - Время удержания SCL в состоянии логического «0» в режиме веедущего
    * 
    * SCLH - Время удержания SCL в состоянии логической «1» в режиме веедущего
    * 
    * PRESC - Делитель частоты I2CCLK. Используется для вычесления периода сигнала TPRESC для счетчиков предустановки, 
    * удержания, уровня «0»и «1»
    * 
    */
    i2c->TIMINGR = I2C_TIMINGR_SCLDEL(1) | I2C_TIMINGR_SDADEL(1) |
        I2C_TIMINGR_SCLL(20) | I2C_TIMINGR_SCLH(20) | I2C_TIMINGR_PRESC(3); //частота 164,7 кГц tsync1 + tsync2 = 10^(-6)

    /*
    *
    * CR1 - Регистр управления
    * 
    * PE - Управление интерфейсом: 0 – интерфейс выключен; 1 – интерфейс включен
    *
    */
    i2c->CR1 = I2C_CR1_PE_M;
    xprintf("\nМастер. Старт\n");
}

Передача ведущим

Для передачи данных ведомому, которые были записаны в массив data, используется функция i2c_master_write.

i2c_master_write(I2C_0, slave_address, data, sizeof(data), false);

Функция принимает следующие аргументы:

- I2C_TypeDef* i2c - выбранный интерфейс. I2C_0 или I2C_1;

- uint8_t slave_adr - адрес ведомого, которому отправляются данные;

- uint8_t data[] - массив с байтами для передачи;

- uint8_t byte_count - количество отправляемых байтов. В данном примере можно отправлять не более 255 байт;

- bool shift - сдвиг адреса на 1. false - если сдвиг не требуется, true - если требуется сдвиг на 1 вправо.


Чтобы интерфейс работал в режиме ведущего, в регистре I2C_CR2 нужно настроить:

- режим адреса (7 или 10 бит) ADD10;

- адрес ведомого SADD;

- направление передачи RD_WRN;

- в случае чтения из ведомого с 10-битным адресом HEAD10R. Обуславливает передачу всего адреса или только заголовка в случае смены направления;

- количество байтов на передачу NBYTES[7:0]. Если количество байтов равно или более 255, в NBYTES[7:0] должно быть сперва записано значение 0xFF.


В примере функция i2c_master_write настраивает регистр I2C_CR2 следующим образом:

- 7 битный режим адреса;

- адрес ведомого SADD записывается в соответствии с переданным в функцию аргументом slave_adr;

- режим записи;

- количество отправляемых байт записывается в соответствии со значением byte_count (в примере 2 байта);

- выставляется бит AUTOEND, который нужен для автоматической отправки сигнала STOP.

После этого в регистре I2C_CR2 выставляется бит START и начинается отправка стартового сигнала и адреса.

Фрагмент кода функции i2c_master_write с настройкой регистра I2C_CR2

// shift - true когда адрес ведомого должен быть сдвинут на 1 бит
if(!shift)
{
    slave_adr = slave_adr << 1;
}

/*
*
* CR2 - регистр управления 2
* 
* SADD - адрес ведомого
* 
* RD_WRN - Направление передачи: 0 – ведущий в режиме записи; 1 – ведущий в режиме чтения
* 
* Количество байт для приема / передачи
* 
* AUTOEND - Управление режимом автоматического окончания: 0 – автоматическое окончание выкл; 1 – автоматическе окончание вкл
* 
*/
i2c->CR2 = I2C_CR2_SADD(slave_adr) | I2C_CR2_WR_M | I2C_CR2_NBYTES(byte_count) | I2C_CR2_AUTOEND_M; 

i2c->CR2 |= I2C_CR2_START_M; // старт отправки адреса, а затем данных

Флаг TXIS взводится после отправки каждого байта, если получено подтверждение (ACK). При отправке адреса и получения подтверждения устанавливается флаг TXIS, после чего в регистр I2C_TXDR записывается байт, который должен быть отправлен.

Флаг TXIS взводится после отправки каждого байта, если получено подтверждение (ACK). При отправке адреса и получения подтверждения устанавливается флаг TXIS, после чего в регистр I2C_TXDR записывается байт, который должен быть отправлен.

В примере передача данных может прекратиться если:

- за 1000000 итераций флаг TXIS не выставился. Возможен разрыв линии связи между ведущим и ведомым;

- во время передачи ведомый прислал нет подтверждения (NACK).

В случае превышения ожидания флага TXIS производится программный сброс с помощью функции i2c_master_restart. В регистре I2C_CR1 бит PE устанавливает в 0, а затем вызывается функция i2c_master_init для повторной инициализации.

Если во время передачи ведомый прислал NACK, сбрасывается флаг NACKF.

Функция i2c_master_restart

void i2c_master_restart(I2C_TypeDef* i2c)
{
    // Программный сброс модуля i2c
    i2c->CR1 &= ~I2C_CR1_PE_M;
    for (volatile int i = 0; i < 1000000; i++); 

    // Повторная инициализация
    i2c_master_init(i2c);
}

Фрагмент кода функции i2c_master_write с отправкой данных

for (uint8_t i = 0; i < byte_count; i++)
{
    int counter = 0; // Счетчик для ожидания
    while(!(i2c->ISR & I2C_ISR_TXIS_M))// TXIS = 1 - предыдущий байт доставлен
    {
        counter++;
        if(counter == 1000000)
        {
            xprintf("Разрыв связи\n");
            // Ожидание превышено. Возможно механическое повреждение линии связи
            // Программный сброс модуля i2c и его повторная инициалиизация 
            i2c_master_restart(i2c);
            return;
        }

        // Ошибка. При записи байта слейв прислал NACK
        if(i2c->ISR & I2C_ISR_NACKF_M) 
        {
            xprintf("Запись. NACKF = %d\n", (i2c->ISR & I2C_ISR_NACKF_M) >> I2C_ISR_NACKF_S);
            break;
        }
    }
    
    // Ошибка. При записи байта слейв прислал NACK
    if(i2c->ISR & I2C_ISR_NACKF_M)
    {
        i2c->ICR |=  I2C_ICR_NACKCF_M; // сброс флага NACKF 
        xprintf("Ошибка при передаче\n");
        break;
    }

    xprintf("Отправка по адресу 0x%02x байта  0x%02x\n", slave_adr_print, data[i]);
    i2c->TXDR = data[i];
}

Приём ведущим

Для приёма данных от ведомого используется функция i2c_master_read.

i2c_master_read(I2C_0, slave_address, data, sizeof(data), false);

Функция принимает следующие аргументы:

- I2C_TypeDef* i2c - выбранный интерфейс. I2C_0 или I2C_1;

- uint8_t slave_adr - адрес ведомого, от которого принимаются данные;

- uint8_t data[] - массив для приема байтов;

- uint8_t byte_count - количество принимаемых байтов. В данном примере можно отправлять не более 255 байт;

- bool shift - сдвиг адреса на 1. false - если сдвиг не требуется, true - если требуется сдвиг на 1 вправо.


Как и в функции i2c_master_write, в начале нужно настроить регистр I2C_CR2. В примере функция i2c_master_read настраивает регистр следующим образом:

- 7 битный режим адреса;

- адрес ведомого SADD записывается в соответствии с переданным в функцию аргументом slave_adr;

- режим чтения;

- количество отправляемых байт записывается в соответствии со значением byte_count (в примере 2 байта);

- выставляется бит AUTOEND, который нужен для автоматической отправки сигнала STOP.

После этого в регистре I2C_CR2 выставляется бит START и начинается отправка стартового сигнала с адресом.

Фрагмент кода функции i2c_master_read с настройкой регистра I2C_CR2

// shift - true когда адрес ведомого должен быть сдвинут на 1 бит
if(!shift)
{
    slave_adr = slave_adr << 1;
}

xprintf("\nЧтение\n");

/*
*
* CR2 - регистр управления 2
* 
* SADD - адрес ведомого
* 
* RD_WRN - Направление передачи: 0 – ведущий в режиме записи; 1 – ведущий в режиме чтения
* 
* Количество байт для приема / передачи
* 
* AUTOEND - Управление режимом автоматического окончания: 0 – автоматическое окончание выкл; 1 – автоматическе окончание вкл
* 
*/

i2c->CR2 = I2C_CR2_SADD(slave_adr) | I2C_CR2_RD_M | I2C_CR2_NBYTES(byte_count) | I2C_CR2_AUTOEND_M; 
i2c->CR2 |= I2C_CR2_START_M;

Флаг RXNE взводится после приема каждого байта. Когда устанавливается флаг RXNE, в соответствующий номеру байта элемент массива data записывается значение регистра I2C_RXDR, в котором хранится принятый байт.

В примере приём данных может прекратиться если:

- за 1000000 итераций флаг RXNE не выставился. Возможен разрыв линии связи между ведущим и ведомым;

- во время приёма ведущий прислал NACK или STOP.

В случае превышения ожидания флага RXNE производится программный сброс с помощью функции i2c_master_restart.

Если во время приёма ведущий прислал NACK или STOP, сбрасывается флаг NACKF и STOPF.

Фрагмент кода функции i2c_master_read с приёмом данных

for(uint8_t i = 0; i < byte_count; i++)
{
    // Счетчик для ожидания
    int counter = 0;

    while(!(i2c->ISR & I2C_ISR_RXNE_M)) // байт принят когда RXNE = 1
    {
        counter++;
        if(counter == 1000000)
        {
            xprintf("Разрыв связи\n");
            // Ожидание превышено. Возможно механическое повреждение линии связи
            // Программный сброс модуля i2c и его повторная инициалиизация 
            i2c_master_restart(i2c);
            return;
        }

        // Ошибка. Во время чтения мастер прислал NACK или STOP
        if((i2c->ISR & I2C_ISR_STOPF_M) && (i2c->ISR & I2C_ISR_NACKF_M))
        {
            xprintf("Чтение. STOPF = %d, NACKF = %d\n", (i2c->ISR & I2C_ISR_STOPF_M) >> I2C_ISR_STOPF_S, (i2c->ISR & I2C_ISR_NACKF_M) >> I2C_ISR_NACKF_S);
            break;
        }
        
    }

    // Ошибка. Во время чтения мастер прислал NACK или STOP
    if((i2c->ISR & I2C_ISR_STOPF_M) && (i2c->ISR & I2C_ISR_NACKF_M))
    {
        i2c->ICR |= I2C_ICR_STOPCF_M | I2C_ICR_NACKCF_M; // сброс флага STOPF и сброс флага NACKF
        break;
    }


    data[i] = i2c->RXDR; // чтение байта и сброс RXNE
    xprintf("Чтение по адресу 0x%02x байта  0x%02x\n", slave_adr_print, data[i]);
}

Полный код для ведущего

main.c для ведущего

#include "common.h"
#include "i2c.h"
#include "stdbool.h"


void i2c_master_init(I2C_TypeDef* i2c)
{
    //Включаем тактирование необходимых блоков и модуля выбора режима GPIO 
    PM->CLK_APB_P_SET |= PM_CLOCK_GPIO_0_M | PM_CLOCK_I2C_0_M;
    PM->CLK_APB_M_SET |= PM_CLOCK_PAD_CONFIG_M | PM_CLOCK_WU_M | PM_CLOCK_PM_M ;
    for (volatile int i = 0; i < 10; i++);


    // обнуление регистра управления
    i2c->CR1 = 0;

    /*
    * Инициализация i2c
    * TIMING - регистр таймингов
    * 
    * SCLDEL - Задержка между изменением SDA и фронтом SCL в режиме ведущего и ведомого при NOSTRETCH = 0
    * 
    * SDADEL - Задержка между спадом SCL и изменением SDA в режиме ведущего и ведомого при NOSTRETCH = 0
    * 
    * SCLL - Время удержания SCL в состоянии логического «0» в режиме веедущего
    * 
    * SCLH - Время удержания SCL в состоянии логической «1» в режиме веедущего
    * 
    * PRESC - Делитель частоты I2CCLK. Используется для вычесления периода сигнала TPRESC для счетчиков предустановки, 
    * удержания, уровня «0»и «1»
    * 
    */
    i2c->TIMINGR = I2C_TIMINGR_SCLDEL(1) | I2C_TIMINGR_SDADEL(1) |
        I2C_TIMINGR_SCLL(20) | I2C_TIMINGR_SCLH(20) | I2C_TIMINGR_PRESC(3); //частота 164,7 кГц tsync1 + tsync2 = 10^(-6)

    /*
    *
    * CR1 - Регистр управления
    * 
    * PE - Управление интерфейсом: 0 – интерфейс выключен; 1 – интерфейс включен
    *
    */
    i2c->CR1 = I2C_CR1_PE_M;
    xprintf("\nМастер. Старт\n");
}

void i2c_master_restart(I2C_TypeDef* i2c)
{
    // Программный сброс модуля i2c
    i2c->CR1 &= ~I2C_CR1_PE_M;
    for (volatile int i = 0; i < 1000000; i++); 

    // Повторная инициализация
    i2c_master_init(i2c);
}

void i2c_master_write(I2C_TypeDef* i2c, uint8_t slave_adr, uint8_t data[], uint8_t byte_count, bool shift)
{
    xprintf("\nОтправка %d\n", data[0] << 8 | data[1]);
    uint8_t slave_adr_print = slave_adr; // переменная используется для вывода адреса

    // shift - true когда адрес ведомого должен быть сдвинут на 1 бит
    if(!shift)
    {
        slave_adr = slave_adr << 1;
    }

    /*
    *
    * CR2 - регистр управления 2
    * 
    * SADD - адрес ведомого
    * 
    * RD_WRN - Направление передачи: 0 – ведущий в режиме записи; 1 – ведущий в режиме чтения
    * 
    * Количество байт для приема / передачи
    * 
    * AUTOEND - Управление режимом автоматического окончания: 0 – автоматическое окончание выкл; 1 – автоматическе окончание вкл
    * 
    */
    i2c->CR2 = I2C_CR2_SADD(slave_adr) | I2C_CR2_WR_M | I2C_CR2_NBYTES(byte_count) | I2C_CR2_AUTOEND_M; 

    i2c->CR2 |= I2C_CR2_START_M; // старт отправки адреса, а затем данных 
    
    for (uint8_t i = 0; i < byte_count; i++)
    {
        int counter = 0; // Счетчик для ожидания
        while(!(i2c->ISR & I2C_ISR_TXIS_M))// TXIS = 1 - предыдущий байт доставлен
        {
            counter++;
            if(counter == 1000000)
            {
                xprintf("Разрыв связи\n");
                // Ожидание превышено. Возможно механическое повреждение линии связи
                // Программный сброс модуля i2c и его повторная инициалиизация 
                i2c_master_restart(i2c);
                return;
            }

            // Ошибка. При записи байта слейв прислал NACK
            if(i2c->ISR & I2C_ISR_NACKF_M) 
            {
                xprintf("Запись. NACKF = %d\n", (i2c->ISR & I2C_ISR_NACKF_M) >> I2C_ISR_NACKF_S);
                break;
            }
        }
        
        // Ошибка. При записи байта слейв прислал NACK
        if(i2c->ISR & I2C_ISR_NACKF_M)
        {
            i2c->ICR |=  I2C_ICR_NACKCF_M; // сброс флага STOPF и сброс флага NACKF 
            xprintf("Ошибка при передаче\n");
            break;
        }
 
        xprintf("Отправка по адресу 0x%02x байта  0x%02x\n", slave_adr_print, data[i]);
        i2c->TXDR = data[i];
    }

}

void i2c_master_read(I2C_TypeDef* i2c, uint8_t slave_adr, uint8_t data[], uint8_t byte_count, bool shift)
{
    uint8_t slave_adr_print = slave_adr; // переменная используется для вывода адреса

    // shift - true когда адрес ведомого должен быть сдвинут на 1 бит
    if(!shift)
    {
        slave_adr = slave_adr << 1;
    }

    xprintf("\nЧтение\n");

    /*
    *
    * CR2 - регистр управления 2
    * 
    * SADD - адрес ведомого
    * 
    * RD_WRN - Направление передачи: 0 – ведущий в режиме записи; 1 – ведущий в режиме чтения
    * 
    * Количество байт для приема / передачи
    * 
    * AUTOEND - Управление режимом автоматического окончания: 0 – автоматическое окончание выкл; 1 – автоматическе окончание вкл
    * 
    */

    i2c->CR2 = I2C_CR2_SADD(slave_adr) | I2C_CR2_RD_M | I2C_CR2_NBYTES(byte_count) | I2C_CR2_AUTOEND_M; 
    i2c->CR2 |= I2C_CR2_START_M;

    for(uint8_t i = 0; i < byte_count; i++)
    {
        // Счетчик для ожидания
        int counter = 0;

        while(!(i2c->ISR & I2C_ISR_RXNE_M)) // байт принят когда RXNE = 1
        {
            counter++;
            if(counter == 1000000)
            {
                xprintf("Разрыв связи\n");
                // Ожидание превышено. Возможно механическое повреждение линии связи
                // Программный сброс модуля i2c и его повторная инициалиизация 
                i2c_master_restart(i2c);
                return;
            }

            // Ошибка. Во время чтения мастер прислал NACK или STOP
            if((i2c->ISR & I2C_ISR_STOPF_M) && (i2c->ISR & I2C_ISR_NACKF_M))
            {
                xprintf("Чтение. STOPF = %d, NACKF = %d\n", (i2c->ISR & I2C_ISR_STOPF_M) >> I2C_ISR_STOPF_S, (i2c->ISR & I2C_ISR_NACKF_M) >> I2C_ISR_NACKF_S);
                break;
            }
            
        }

        // Ошибка. Во время чтения мастер прислал NACK или STOP
        if((i2c->ISR & I2C_ISR_STOPF_M) && (i2c->ISR & I2C_ISR_NACKF_M))
        {
            i2c->ICR |= I2C_ICR_STOPCF_M | I2C_ICR_NACKCF_M; // сброс флага STOPF и сброс флага NACKF
            break;
        }


        data[i] = i2c->RXDR; // чтение байта и сброс RXNE
        xprintf("Чтение по адресу 0x%02x байта  0x%02x\n", slave_adr_print, data[i]);
    }

    xprintf("Получено %d\n" , data[0] << 8 | data[1]);

}


int main()
{    
    // Адрес ведомого
    uint8_t slave_address = 0x36; 

    // Число для оптавки
    uint16_t to_send = 0; 
    
    // Массив с байтами для отправки / приема
    uint8_t data[2] = {to_send >> 8, to_send & 0b0000000011111111}; // массив заполняется байтами числа to_send

    // Инициализация блока i2c в режиме мастер (ведущий)
    i2c_master_init(I2C_0); 
    

    while (1)
    {
        
        // Запись данных по адресу slave_address = 0x36 без сдвига адреса
        i2c_master_write(I2C_0, slave_address, data, sizeof(data), false); 
        for (volatile int i = 0; i < 1000000; i++); 

        // Чтение данных по адресу slave_address = 0x36 без сдвига адреса
        i2c_master_read(I2C_0, slave_address, data, sizeof(data), false); 
        for (volatile int i = 0; i < 1000000; i++); 

    }
    
}

Ведомый

Переменные ведомого:

- uint8_t slave_address - собственный адрес ведомого;

- uint16_t to_send - число из двух байт;

- uint8_t data[2] - массив с байтами для отправки / приема ведомым.

При отправке данных ведомым массив data заполняется байтами числа to_send. При получении данных ведомым число to_send формируется из байтов массива data.

Для передачи данных используется I2C0, который имеет следующие выводы:

- PORT_0_9 - I2C0_sda;

- PORT_0_10 - I2C0_scl.

Инициализация

Чтобы интерфейс работал в режиме ведомого, нужно задействовать хотя бы один адрес ведомого. Адреса OA1 и OA2 настраиваются в регистрах I2C_OAR1 и I2C_OAR2. Адрес OA1 может быть настроен либо в 7-битном режиме, либо в 10-битном, для чего должен быть установлен бит OA1MODE в регистре I2C_OAR1. Использование адреса OA1 разрешается битом OA1EN. При необходимости можно настроить дополнительный адрес OA2. Маскирование битов OA2 настраивается битами OA2MSK. Использование OA2EN разрешается битом OA2EN. Адреса общего вызова разрешаются установкой бита GCEN.

В примере инициализация ведомого проводится с помощью функции i2c_slave_init. Эта функция включает тактирование необходимых блоков для работы GPIO и I2C. Регистр I2C_OAR1 настраивается следующим образом:

- OA1 соответствует переменной slave_address;

- 7-битный режим;

- использование адреса OA1 разрешено.


Затем следует настройка регистра I2C_TIMINGR, где выставляется делитель частоты и задержки. В конце для включения интерфейса в регистре I2C_CR1 выставляется бит PE. После этого можно отправлять и принимать данные.

Функция принимает следующие аргументы:

- I2C_TypeDef* i2c - выбранный интерфейс. I2C_0 или I2C_1;

- uint8_t slave_address - собственный адрес ведомого.

Функция i2c_slave_init

void i2c_slave_init(I2C_TypeDef *i2c, uint8_t slave_address)
{
    slave_address = slave_address << 1;

    // Включаем тактирование необходимых блоков
    PM->CLK_APB_P_SET |= PM_CLOCK_GPIO_0_M | PM_CLOCK_I2C_0_M;
    PM->CLK_APB_M_SET |= PM_CLOCK_PAD_CONFIG_M | PM_CLOCK_WU_M | PM_CLOCK_PM_M ;
    
    // обнуление регистра управления
    i2c->CR1 = 0;

    /*
    *
    * I2C_OAR1 - регистр собственного адреса
    * 
    * OA1 - собственный адрес 1
    * 
    * OA1MODE - режим 7/10 бит адреса OA1. 0 - 7 бит
    * 
    * OA1EN - использование собствевнного адреса OA1. 1 - при получении адреса OA1 - ACK 
    * 
    */
    i2c->OAR1 = (slave_address << I2C_OAR1_OA1_S) | (1 << I2C_OAR1_OA1EN_S); // собственный адрес 
    

    /*
    * Инициализация i2c
    * TIMING - регистр таймингов
    * 
    * SCLDEL - Задержка между изменением SDA и фронтом SCL в режиме ведущего и ведомого при NOSTRETCH = 0
    * 
    * SDADEL - Задержка между спадом SCL и изменением SDA в режиме ведущего и ведомого при NOSTRETCH = 0
    * 
    * SCLL - Время удержания SCL в состоянии логического «0» в режиме веедущего
    * 
    * SCLH - Время удержания SCL в состоянии логической «1» в режиме веедущего
    * 
    * PRESC - Делитель частоты I2CCLK. Используется для вычесления периода сигнала TPRESC для счетчиков предустановки, 
    * удержания, уровня «0»и «1»
    * 
    */
    i2c->TIMINGR = I2C_TIMINGR_SCLDEL(1) | I2C_TIMINGR_SDADEL(1) |
        I2C_TIMINGR_SCLL(20) | I2C_TIMINGR_SCLH(20) | I2C_TIMINGR_PRESC(3); //частота 164,7 кГц tsync1 + tsync2 = 10^(-6)

    /*
    *
    * CR1 - Регистр управления
    * 
    * PE - Управление интерфейсом: 0 – интерфейс выключен; 1 – интерфейс включен
    *
    */
    i2c->CR1 |= I2C_CR1_PE_M;

    xprintf("\nВедомый. Старт\n");

}

После инициализации ведомый ожидает запроса от мастера. При получении ведомым от ведущего собственного адреса взводится флаг ADDR в регистре I2C_ISR. Флаг ADDR сбрасывается установкой бита ADDRCF в регистре I2C_ICR. После сброса флага ADDR ведомый шлет ACK ведущему.

xprintf("\nОжидание\n");
int counter = 0;

// Ожидание запроса мастером адреса слейва
while(!(I2C_0->ISR & I2C_ISR_ADDR_M))
{
    counter++;
    if(counter==10000000)
    {
        xprintf("\nОжидание\n");
        counter = 0;
    }
    
} 

// срос флага ADDR для подверждения принятия адреса
I2C_0->ICR |= I2C_ICR_ADDRCF_M;

После этого должен быть проанализирован флаг DIR в регистре I2C_ISR, чтобы узнать направление передачи. DIR = 1 - режим ведомый - передатчик, DIR = 0 - режим ведомый приемник.

/*
* I2C_ISR - Регистр прерываний и статуса
*   
* DIR - Тип передачи. Обновляется в режиме ведомого при получении адреса:
*       0 – передача типа запись, ведомый переходит в режим приемника;
*       1 – передача типа чтения, ведомый переходит в режим передатчика
*/

if(I2C_0->ISR & I2C_ISR_DIR_M) // DIR = 1. Режим ведомого - передатчик
{
    xprintf("\nЗапрос на запись\n");

    // Отправляется число to_send, состоящее из двух байт
    // Заполняется массив data байтами, которые нужно отправить
    data[0] = to_send >> 8;
    data[1] = to_send & 0b0000000011111111;

    // Отправка
    i2c_slave_write(I2C_0, data, sizeof(data));
} 
else // DIR = 0. Режим ведомого - приемник
{
    xprintf("\nЗапрос на чтение\n");
    
    // Чтение двух байт от мастера и запись их в массив data 
    i2c_slave_read(I2C_0, data, sizeof(data));

    // Формирование принятого числа
    to_send = data[0] << 8 | data[1];
    to_send++; 
    if(to_send > 65530)
    {
        to_send = 0;
    }
}

Передача ведомым

Для передачи данных ведущему используется функция i2c_slave_write.

i2c_slave_write(I2C_0, data, sizeof(data));

Функция принимает следующие аргументы:

- I2C_TypeDef* i2c - выбранный интерфейс. I2C_0 или I2C_1;

- uint8_t data[] - массив с байтами для передачи

- uint8_t byte_count - количество отправляемых байтов. В данном примере можно отправлять не более 255 байт.


Флаг TXIS взводится после отправки каждого байта, если получено подтверждение (ACK) от ведущего. При получении ведомым собственного адреса от ведущего отправляется подтверждение и устанавливается флаг TXIS. Каждый раз при установлении флага TXIS в регистр I2C_TXDR записывается байт, который должен быть отправлен ведущему.

В примере передача данных может прекратиться если:

- за 1000000 итераций флаг TXIS не выставился. Возможен разрыв линии связи между ведущим и ведомым;

- во время передачи ведущий прислал нет подтверждения (NACK).

В случае превышения ожидания флага TXIS производится программный сброс с помощью функции i2c_slave_restart. В регистре I2C_CR1 бит PE устанавливает в 0, а затем вызывается функция i2c_master_init для повторной инициализации.

Если ведущий прислал NACK, сбрасываются флаги STOPF и NACKF.

Функция i2c_slave_restart

void i2c_slave_restart(I2C_TypeDef* i2c)
{
    // Получение адреса ведомого
    uint8_t slave_address = (uint8_t)(i2c->OAR1 & (0b1111111111));
    slave_address >>= 1;
    xprintf("Рестарт. adres = 0x%02x\n", slave_address);
    
    // Программный сброс модуля i2c
    i2c->CR1 &= ~I2C_CR1_PE_M;
    for (volatile int i = 0; i < 1000000; i++); 

    // Повторная инициализация
    i2c_slave_init(i2c, slave_address);
}

Функция i2c_slave_write

void i2c_slave_write(I2C_TypeDef* i2c, uint8_t data[], uint8_t byte_count)
{

    xprintf("\nОтправка %d\n", data[0]<<8 | data[1]);

    for (uint8_t i = 0; i < byte_count; i++)
    {
        int counter = 0; // Счетчик для ожидания
        while(!(i2c->ISR & I2C_ISR_TXIS_M)) // TXIS = 1 - регистр TXDR свободен
        {
            counter++;
            if(counter == 1000000)
            {
                // Ожидание превышено. Возможно механическое повреждение линии связи
                break;
            }

            // При записи байта мастер прислал NACK. Возникла ошибка при передаче
            if(i2c->ISR & I2C_ISR_NACKF_M)
            {
                xprintf("Запись. NACKF = %d\n", (i2c->ISR & I2C_ISR_NACKF_M) >> I2C_ISR_NACKF_S);
                break;
            }
        }

        if(counter == 1000000)
        {
            xprintf("Разрыв связи\n");

            // Программный сброс модуля i2c и его повторная инициалиизация 
            i2c_slave_restart(i2c);
            break;
        } 

        // При записи байта мастер прислал NACK. Возникла ошибка при передаче
        if(i2c->ISR & I2C_ISR_NACKF_M)
        {
            i2c->ICR |= I2C_ICR_STOPCF_M | I2C_ICR_NACKCF_M; // сброс флага STOPF и NACKF
            xprintf("Ошибка при передаче\n");
            break;
        }

        i2c->TXDR = data[i]; // Загрузка передаваемого байта в регистр TXDR

        xprintf("Отправлен байт 0x%02x\n", data[i]);
        i2c->ICR |= I2C_ICR_STOPCF_M | I2C_ICR_NACKCF_M; // сброс флага STOPF и NACKF
    }

}

Приём ведомым

Для приёма данных от ведомого используется функция i2c_slave_read.

i2c_slave_read(I2C_0, data, sizeof(data));

Функция принимает следующие аргументы:

- I2C_TypeDef* i2c - выбранный интерфейс. I2C_0 или I2C_1;

- uint8_t data[] - массив для приема байтов;

- uint8_t byte_count - количество принимаемых байтов. В данном примере можно отправлять не более 255 байт.


Флаг RXNE взводится после приема каждого байта. Когда устанавливается флаг RXNE, в соответствующий номеру байта элемент массива data записывается значение регистра I2C_RXDR, в котором хранится принятый байт.

В примере приём данных может прекратиться если:

- за 1000000 итераций флаг RXNE не выставился. Возможен разрыв линии связи между ведущим и ведомым;

- во время приёма ведущий прислал NACK.

В случае превышения ожидания флага RXNE производится программный сброс с помощью функции i2c_slave_restart.


Функция i2c_slave_read

void i2c_slave_read(I2C_TypeDef* i2c, uint8_t data[], uint8_t byte_count)
{
    xprintf("\nЧтение\n");

    for (uint8_t i = 0; i < byte_count; i++)
    {
        // Счетчик для ожидания
        int counter = 0;

        // Байт нужно прочитать когда RXNE = 1
        while(!(i2c->ISR & I2C_ISR_RXNE_M))
        {
            
            counter++;
            if(counter == 1000000)
            {
                // Ожидание превышено. Возможно механическое повреждение линии связи
                break;
            }

            if((i2c->ISR & I2C_ISR_NACKF_M))
            {
                // Мастер прислал NACK. Ошибка чтения
                break;
            }

        } 

        // Ожидание превышено. Возможно механическое повреждение линии связи
        if(counter == 1000000)
        {
            xprintf("Разрыв связи\n");

            // Программный сброс модуля i2c и его повторная инициалиизация 
            i2c_slave_restart(i2c);
            break;
        }

        if((i2c->ISR & I2C_ISR_NACKF_M))
        {
            xprintf("Ошибка чтения\n");
            break;
        }

        data[i] = i2c->RXDR; // Чтение байта и сброс флага RXNE
        xprintf("Чтение байта  0x%02x\n", data[i]);
    }

    xprintf("Прочитано  %d\n", data[0] << 8 | data[1]);
}

Полный код для ведомого

main.c для ведомого

#include "common.h"
#include "i2c.h"
#include "stdbool.h"

void i2c_slave_init(I2C_TypeDef *i2c, uint8_t slave_address)
{
    slave_address = slave_address << 1;

    // Включаем тактирование необходимых блоков
    PM->CLK_APB_P_SET |= PM_CLOCK_GPIO_0_M | PM_CLOCK_I2C_0_M;
    PM->CLK_APB_M_SET |= PM_CLOCK_PAD_CONFIG_M | PM_CLOCK_WU_M | PM_CLOCK_PM_M ;
    
    // обнуление регистра управления
    i2c->CR1 = 0;

    /*
    *
    * I2C_OAR1 - регистр собственного адреса
    * 
    * OA1 - собственный адрес 1
    * 
    * OA1MODE - режим 7/10 бит адреса OA1. 0 - 7 бит
    * 
    * OA1EN - использование собствевнного адреса OA1. 1 - при получении адреса OA1 - ACK 
    * 
    */
    i2c->OAR1 = (slave_address << I2C_OAR1_OA1_S) | (1 << I2C_OAR1_OA1EN_S); // собственный адрес 
    

    /*
    * Инициализация i2c
    * TIMING - регистр таймингов
    * 
    * SCLDEL - Задержка между изменением SDA и фронтом SCL в режиме ведущего и ведомого при NOSTRETCH = 0
    * 
    * SDADEL - Задержка между спадом SCL и изменением SDA в режиме ведущего и ведомого при NOSTRETCH = 0
    * 
    * SCLL - Время удержания SCL в состоянии логического «0» в режиме веедущего
    * 
    * SCLH - Время удержания SCL в состоянии логической «1» в режиме веедущего
    * 
    * PRESC - Делитель частоты I2CCLK. Используется для вычесления периода сигнала TPRESC для счетчиков предустановки, 
    * удержания, уровня «0»и «1»
    * 
    */
    i2c->TIMINGR = I2C_TIMINGR_SCLDEL(1) | I2C_TIMINGR_SDADEL(1) |
        I2C_TIMINGR_SCLL(20) | I2C_TIMINGR_SCLH(20) | I2C_TIMINGR_PRESC(3); //частота 164,7 кГц tsync1 + tsync2 = 10^(-6)



    /*
    *
    * CR1 - Регистр управления
    * 
    * PE - Управление интерфейсом: 0 – интерфейс выключен; 1 – интерфейс включен
    *
    */
    i2c->CR1 |= I2C_CR1_PE_M;

    xprintf("\nВедомый. Старт\n");

}

void i2c_slave_restart(I2C_TypeDef* i2c)
{
    // Получение адреса ведомого
    uint8_t slave_address = (uint8_t)(i2c->OAR1 & (0b1111111111));
    slave_address >>= 1;
    xprintf("Рестарт. adres = 0x%02x\n", slave_address);
    
    // Программный сброс модуля i2c
    i2c->CR1 &= ~I2C_CR1_PE_M;
    for (volatile int i = 0; i < 1000000; i++); 

    // Повторная инициализация
    i2c_slave_init(i2c, slave_address);
}

void i2c_slave_write(I2C_TypeDef* i2c, uint8_t data[], uint8_t byte_count)
{

    xprintf("\nОтправка %d\n", data[0]<<8 | data[1]);

    for (uint8_t i = 0; i < byte_count; i++)
    {
        int counter = 0; // Счетчик для ожидания
        while(!(i2c->ISR & I2C_ISR_TXIS_M)) // TXIS = 1 - регистр TXDR свободен
        {
            counter++;
            if(counter == 1000000)
            {
                // Ожидание превышено. Возможно механическое повреждение линии связи
                break;
            }

            // При записи байта мастер прислал NACK. Возникла ошибка при передаче
            if(i2c->ISR & I2C_ISR_NACKF_M)
            {
                xprintf("Запись. NACKF = %d\n", (i2c->ISR & I2C_ISR_NACKF_M) >> I2C_ISR_NACKF_S);
                break;
            }
        }

        if(counter == 1000000)
        {
            xprintf("Разрыв связи\n");

            // Программный сброс модуля i2c и его повторная инициалиизация 
            i2c_slave_restart(i2c);
            break;
        } 

        // При записи байта мастер прислал NACK. Возникла ошибка при передаче
        if(i2c->ISR & I2C_ISR_NACKF_M)
        {
            i2c->ICR |= I2C_ICR_STOPCF_M | I2C_ICR_NACKCF_M; // сброс флага STOPF и NACKF
            xprintf("Ошибка при передаче\n");
            break;
        }

        i2c->TXDR = data[i]; // Загрузка передаваемого байта в регистр TXDR

        xprintf("Отправлен байт 0x%02x\n", data[i]);
        i2c->ICR |= I2C_ICR_STOPCF_M | I2C_ICR_NACKCF_M; // сброс флага STOPF и NACKF
    }


}

void i2c_slave_read(I2C_TypeDef* i2c, uint8_t data[], uint8_t byte_count)
{

    xprintf("\nЧтение\n");

    for (uint8_t i = 0; i < byte_count; i++)
    {
        // Счетчик для ожидания
        int counter = 0;

        // Байт нужно прочитать когда RXNE = 1
        while(!(i2c->ISR & I2C_ISR_RXNE_M))
        {
            
            counter++;
            if(counter == 1000000)
            {
                // Ожидание превышено. Возможно механическое повреждение линии связи
                break;
            }

            if((i2c->ISR & I2C_ISR_NACKF_M))
            {
                // Мастер прислал NACK. Ошибка чтения
                break;
            }

        } 

        // Ожидание превышено. Возможно механическое повреждение линии связи
        if(counter == 1000000)
        {
            xprintf("Разрыв связи\n");

            // Программный сброс модуля i2c и его повторная инициалиизация 
            i2c_slave_restart(i2c);
            break;
        }

        if((i2c->ISR & I2C_ISR_NACKF_M))
        {
            xprintf("Ошибка чтения\n");
            break;
        }

        data[i] = i2c->RXDR; // Чтение байта и сброс флага RXNE
        xprintf("Чтение байта  0x%02x\n", data[i]);
    }

    xprintf("Прочитано  %d\n", data[0] << 8 | data[1]);
}

int main()
{

    // Адрес ведомого
    uint8_t slave_address = 0x36;
    // Число для оптавки
    uint16_t to_send = 0; 
    // Массив с байтами для отправки / приема
    uint8_t data[2];

    // инициализация блока i2c в режиме слейв (ведомый)
    i2c_slave_init(I2C_0, slave_address); 
    
    while (1)
    {
        xprintf("\nОжидание\n");
        int counter = 0;

        // Ожидание запроса мастером адреса слейва
        while(!(I2C_0->ISR & I2C_ISR_ADDR_M))
        {
            counter++;
            if(counter==10000000)
            {
                xprintf("\nОжидание\n");
                counter = 0;
            }
            
        } 
        
        //I2C_0 -> ISR |=  I2C_ISR_TXE_M;

        // срос флага ADDR для подверждения принятия адреса
        I2C_0->ICR |= I2C_ICR_ADDRCF_M; 

        /*
        * I2C_ISR - Регистр прерываний и статуса
        *   
        * ADDR - Флаг соответствия адреса в ре-жиме ведомого
        *   
        * DIR - Тип передачи. Обновляется в режиме ведомого при получении адреса:
        *       0 – передача типа запись, ведомый переходит в режим приемника;
        *       1 – передача типа чтения, ведомый переходит в режим передатчика
        * 
        */

        if(I2C_0->ISR & I2C_ISR_DIR_M) // DIR = 1. Режим ведомого - передатчик
        {
            xprintf("\nЗапрос на запись\n");

            // Отправляется число to_send, состоящее из двух байт
            // Заполняется массив data байтами, которые нужно отправить
            data[0] = to_send >> 8;
            data[1] = to_send & 0b0000000011111111;

            // Отправка
            i2c_slave_write(I2C_0, data, sizeof(data));
        } 
        else // DIR = 0. Режим ведомого - приемник
        {
            xprintf("\nЗапрос на чтение\n");
            
            // Чтение двух байт от мастера и запись их в массив data 
            i2c_slave_read(I2C_0, data, sizeof(data));

            // Формирование принятого числа
            to_send = data[0] << 8 | data[1];
            to_send++; 
            if(to_send > 65530)
            {
                to_send = 0;
            }
        }

    }
    
}

Результат

Рисунок 2 - Вывод

Результат выполнения программы можно видеть в COM-порте. В примере для передачи данных используется UART_0, который имеет следующие выводы:

- PORT_0_5 - UART0_rxd;

- PORT_0_6 - UART0_txd.

Установлена скорость 9600 бод. На рисунке 2 изображен вывод программ ведущего и ведомого.

Пример для ведущего - ссылка

Пример для ведомого - ссылка

Пример был написан в Visual Studio Code.

DMA запросы

Для использования DMA нужно настроить биты в регистре CR1. Для отправки данных устанавливается бит TXDMAEN, для приёма данных бит RXDMAEN.

Ведущий

Полный код для ведущего

Пример для ведущего - ссылка

main.c для ведущего

#include "common.h"
#include "i2c.h"
#include "stdbool.h"
#include "dma_config.h"


#define no_shift false
#define DMA_CHANNEL_0     0
#define DMA_CHANNEL_1     1
#define DMA_CHANNEL_2     2
#define DMA_CHANNEL_3     3


typedef enum
{
    i2c_dma_tx,
    i2c_dma_rx,
} i2c_dma; 


void i2c_master_init(I2C_TypeDef* i2c)
{
    //Включаем тактирование необходимых блоков и модуля выбора режима GPIO 
    PM->CLK_APB_P_SET |= PM_CLOCK_GPIO_0_M | PM_CLOCK_I2C_0_M;
    PM->CLK_APB_M_SET |= PM_CLOCK_PAD_CONFIG_M | PM_CLOCK_WU_M | PM_CLOCK_PM_M ;
    for (volatile int i = 0; i < 10; i++);


    // обнуление регистра управления
    i2c->CR1 = 0;

    /*
    * Инициализация i2c
    * TIMING - регистр таймингов
    * 
    * SCLDEL - Задержка между изменением SDA и фронтом SCL в режиме ведущего и ведомого при NOSTRETCH = 0
    * 
    * SDADEL - Задержка между спадом SCL и изменением SDA в режиме ведущего и ведомого при NOSTRETCH = 0
    * 
    * SCLL - Время удержания SCL в состоянии логического «0» в режиме веедущего
    * 
    * SCLH - Время удержания SCL в состоянии логической «1» в режиме веедущего
    * 
    * PRESC - Делитель частоты I2CCLK. Используется для вычесления периода сигнала TPRESC для счетчиков предустановки, 
    * удержания, уровня «0»и «1»
    * 
    */
    i2c->TIMINGR = I2C_TIMINGR_SCLDEL(1) | I2C_TIMINGR_SDADEL(1) |
        I2C_TIMINGR_SCLL(20) | I2C_TIMINGR_SCLH(20) | I2C_TIMINGR_PRESC(3); //частота 164,7 кГц tsync1 + tsync2 = 10^(-6)

    /*
    *
    * CR1 - Регистр управления
    * 
    * PE - Управление интерфейсом: 0 – интерфейс выключен; 1 – интерфейс включен
    *
    */
    i2c->CR1 = I2C_CR1_PE_M;
    xprintf("\nМастер. Старт\n");
}

void i2c_master_DMA_mode(I2C_TypeDef* i2c, i2c_dma dma_mode)
{
    switch (dma_mode)
    {
        case i2c_dma_tx:
        I2C_0->CR1 |= I2C_CR1_TXDMAEN_M;
        break;
        case i2c_dma_rx:
        I2C_0->CR1 |= I2C_CR1_RXDMAEN_M;
        break;
        default:
        xprintf("Неизвестный режим поддержки DMA");
    }
}

void DMA_Channels_init(
    DMA_CONFIG_TypeDef* dma, 
    void* tx_address, void* rx_address, 
    int dma_request_index, uint8_t DMA_Channel, uint8_t data[],uint32_t count,
    uint32_t common_config, i2c_dma i2c_dma_mode
)
{
    // ConfigStatus - регистр DMA_CNTR
    dma->ConfigStatus = 0xFFFFFFFF;

    /*
    *
    * common_config - регистр CHx_CFG
    * 
    * DMA_CFG_CH_ENABLE_M - Разрешение работы канала:
    * 
    * DMA_CFG_CH_WRITE_REQ - Write requets. Выбор периферийной линии назначения. 5 для I2C_0
    * 
    * DMA_CFG_CH_READ_REQ - Read requets. Выбор периферийной линии источника.
    * 
    */
    common_config |= DMA_CFG_CH_ENABLE_M | 
        DMA_CFG_CH_WRITE_REQ(dma_request_index) |
        DMA_CFG_CH_READ_REQ(dma_request_index);

    /*
    * Выбор канала DMA
    * Создаем указатель типа DMA_CHANNEL_TypeDef и присваиваем ему 
    * адрес нулевого канала dma с наивысшим приоритетом 
    * 
    */
    DMA_CHANNEL_TypeDef* dma_ch;
    switch (DMA_Channel)
    {
    case DMA_CHANNEL_0:
        dma_ch = &(dma->CHANNELS[DMA_CHANNEL_0]);
        break;
    case DMA_CHANNEL_1:
        dma_ch = &(dma->CHANNELS[DMA_CHANNEL_1]);
        break;
    case DMA_CHANNEL_2:
        dma_ch = &(dma->CHANNELS[DMA_CHANNEL_2]);
        break;
    case DMA_CHANNEL_3:
        dma_ch = &(dma->CHANNELS[DMA_CHANNEL_3]);
        break;
    }
    
    /*
    *
    * DESTINATIONS - CHx_DST. Регистр адреса назначения канала
    * 
    * SOURCE - CHx_SRC. Регистр адреса источника канала
    * 
    * LEN - CHx_LEN. Регистр размера передаваемых данных канала. 
    * Количество байт пересылки расчитывается как Data_Len +1
    * *******************
    * CONFIG - CHx_CFG. Регистр управления и конфигурации канала
    * 
    * DMA_CFG_CH_READ_MODE_memory_M - Read mode. Режим адреса источника:
    * 
    * DMA_CFG_CH_READ_INCREMENT_M - Read incr. Инкремент адреса источника:
    * 
    * DMA_CFG_CH_WRITE_MODE_periphery_M - Write mode. Режим адреса назначения:
    * 
    * DMA_CFG_CH_WRITE_no_INCREMENT_M - Write incr. Инкремент адреса назначения:
    * 
    */
    switch(i2c_dma_mode)
    {
        case i2c_dma_tx:
            dma_ch->SOURCE = (uint32_t)data;
            dma_ch->DESTINATIONS = (uint32_t)(tx_address);
            dma_ch->LEN = count-1;
            dma_ch->CONFIG = common_config | 
            DMA_CFG_CH_READ_MODE_memory_M | DMA_CFG_CH_READ_INCREMENT_M |
            DMA_CFG_CH_WRITE_MODE_periphery_M | DMA_CFG_CH_WRITE_no_INCREMENT_M;
        break;
        case i2c_dma_rx:
            dma_ch->SOURCE = (uint32_t)(rx_address);
            dma_ch->DESTINATIONS = (uint32_t)data;
            dma_ch->LEN = count-1;
            dma_ch->CONFIG = common_config | 
                DMA_CFG_CH_READ_MODE_periphery_M | DMA_CFG_CH_READ_no_INCREMENT_M |
                DMA_CFG_CH_WRITE_MODE_memory_M | DMA_CFG_CH_WRITE_INCREMENT_M;
        break;
    }

}

void DMA_Master_Wait(DMA_CONFIG_TypeDef* dma, i2c_dma i2c_dma_mode)
{
    uint32_t timeout = 10000000; 

    /*
    *
    * Последние 4 бита ConfigStatus - Статус состояния каналов:
    * «1» - готов к работе;
    * «0» - занят
    * 
    */
    switch(i2c_dma_mode)
    {
        case i2c_dma_tx:
            while (((dma->ConfigStatus & DMA_STATUS_READY(DMA_CHANNEL_0)) == 0) && (--timeout < 0));
        break;
        case i2c_dma_rx:
            while (((dma->ConfigStatus & DMA_STATUS_READY(DMA_CHANNEL_0)) == 0) && (--timeout < 0)); 
        break;
    }

    if (timeout < 0)
    {
        xprintf("DMA: ожидание готовности канала превышено\n");
    }
}

void DMA_check_data(uint8_t data[], int count, i2c_dma i2c_dma_mode)
{
    for(int i = 0; i < count; i++)
    {
        switch(i2c_dma_mode)
        {
            case i2c_dma_tx:
            xprintf("Отправка байта[%d]  0x%02x\n", i, data[i]);
            break;
            case i2c_dma_rx:
            xprintf("Чтение байта[%d]  0x%02x\n", i, data[i]);
            break;
        }
    }

    switch(i2c_dma_mode)
    {
        case i2c_dma_tx:
        xprintf("Отправлено %d\n\n", data[0] << 8 | data[1]);
        break;
        case i2c_dma_rx:
        xprintf("Получено %d\n\n", data[0] << 8 | data[1]);
        break;
    }
}

void i2c_master_DMA(I2C_TypeDef* i2c, int dma_request_index, uint8_t data[], 
                    uint32_t count, uint8_t slave_address, i2c_dma i2c_dma_mode, 
                    uint8_t DMA_Channel, bool shift)
{
    uint8_t slave_adr = slave_address;
    // shift - true когда адрес ведомого должен быть сдвинут на 1 бит
    if(!shift)
    {
        slave_adr = slave_adr << 1;
    }
    

    DMA_CONFIG_TypeDef* dma = DMA_CONFIG;
    /*
    * DMA_CFG_CH_READ_SIZE_byte_M - Read size - Разрядность адреса источника:
    * 
    * DMA_CFG_CH_READ_BURST_SIZE_S - Read block size
    * Количество байт в пакете. Определяется как 2^Read block size
    * 
    * DMA_CFG_CH_WRITE_SIZE_byte_M - Write size. Разрядность адреса назначения
    */
    uint32_t config =   DMA_CFG_CH_READ_SIZE_byte_M |(0 << DMA_CFG_CH_READ_BURST_SIZE_S) | 
                        DMA_CFG_CH_WRITE_SIZE_byte_M |(0 << DMA_CFG_CH_WRITE_BURST_SIZE_S);

    DMA_Channels_init(dma, (void*)&i2c->TXDR, (void*)&i2c->RXDR, dma_request_index, DMA_Channel, data, count, config, i2c_dma_mode);

    switch (i2c_dma_mode)
    {
    case i2c_dma_tx:
        i2c->CR2 = I2C_CR2_SADD(slave_adr) | I2C_CR2_WR_M | I2C_CR2_NBYTES(count) | I2C_CR2_AUTOEND_M;
        break;
    case i2c_dma_rx:
        i2c->CR2 = I2C_CR2_SADD(slave_adr) | I2C_CR2_RD_M | I2C_CR2_NBYTES(count) | I2C_CR2_AUTOEND_M;
        break;
    }

    i2c->CR2 |= I2C_CR2_START_M; // старт отправки адреса, а затем данных 
    
    DMA_Master_Wait(dma, i2c_dma_mode);
    DMA_check_data(data, count, i2c_dma_mode);
}

int main ()
{
    // Адрес ведомого
    uint8_t slave_address = 0x36; 

    // Число для оптавки
    uint16_t to_send = 65000; 
    
    // Массив с байтами для отправки / приема
    uint8_t data[2];
    data[0] = to_send >> 8; // массив заполняется байтами числа to_send
    data[1] = to_send & 0b0000000011111111; // массив заполняется байтами числа to_send

    // Инициализация блока i2c в режиме мастер (ведущий)
    i2c_master_init(I2C_0);

    i2c_dma i2c_dma_mode;

    while (1)
    {
        i2c_dma_mode = i2c_dma_tx;
        i2c_master_DMA_mode(I2C_0, i2c_dma_mode);
        i2c_master_DMA(I2C_0, DMA_I2C_0_INDEX, data, sizeof(data), slave_address, i2c_dma_mode, DMA_CHANNEL_0, no_shift); 
        for (volatile int i = 0; i < 1000000; i++); 

        i2c_dma_mode = i2c_dma_rx;
        i2c_master_DMA_mode(I2C_0, i2c_dma_mode);
        i2c_master_DMA(I2C_0, DMA_I2C_0_INDEX, data, sizeof(data), slave_address, i2c_dma_mode, DMA_CHANNEL_0, no_shift); 
        for (volatile int i = 0; i < 1000000; i++);

    }
    
}

Ведомый

Полный код для ведомого

Пример для ведомого - ссылка

main.c для ведомого

#include "common.h"
#include "i2c.h"
#include "stdbool.h"
#include "dma_config.h"


#define no_shift false
#define DMA_CHANNEL_0     0
#define DMA_CHANNEL_1     1
#define DMA_CHANNEL_2     2
#define DMA_CHANNEL_3     3


typedef enum
{
    i2c_dma_tx,
    i2c_dma_rx,
} i2c_dma; 

void i2c_slave_init(I2C_TypeDef *i2c, uint8_t slave_address)
{
    slave_address = slave_address << 1;

    // Включаем тактирование необходимых блоков
    PM->CLK_APB_P_SET |= PM_CLOCK_GPIO_0_M | PM_CLOCK_I2C_0_M;
    PM->CLK_APB_M_SET |= PM_CLOCK_PAD_CONFIG_M | PM_CLOCK_WU_M | PM_CLOCK_PM_M ;
    
    // обнуление регистра управления
    i2c->CR1 = 0;

    /*
    *
    * I2C_OAR1 - регистр собственного адреса
    * 
    * OA1 - собственный адрес 1
    * 
    * OA1MODE - режим 7/10 бит адреса OA1. 0 - 7 бит
    * 
    * OA1EN - использование собствевнного адреса OA1. 1 - при получении адреса OA1 - ACK 
    * 
    */
    i2c->OAR1 = (slave_address << I2C_OAR1_OA1_S) | (1 << I2C_OAR1_OA1EN_S); // собственный адрес 
    

    /*
    * Инициализация i2c
    * TIMING - регистр таймингов
    * 
    * SCLDEL - Задержка между изменением SDA и фронтом SCL в режиме ведущего и ведомого при NOSTRETCH = 0
    * 
    * SDADEL - Задержка между спадом SCL и изменением SDA в режиме ведущего и ведомого при NOSTRETCH = 0
    * 
    * SCLL - Время удержания SCL в состоянии логического «0» в режиме веедущего
    * 
    * SCLH - Время удержания SCL в состоянии логической «1» в режиме веедущего
    * 
    * PRESC - Делитель частоты I2CCLK. Используется для вычесления периода сигнала TPRESC для счетчиков предустановки, 
    * удержания, уровня «0»и «1»
    * 
    */
    i2c->TIMINGR = I2C_TIMINGR_SCLDEL(1) | I2C_TIMINGR_SDADEL(1) |
        I2C_TIMINGR_SCLL(20) | I2C_TIMINGR_SCLH(20) | I2C_TIMINGR_PRESC(3); //частота 164,7 кГц tsync1 + tsync2 = 10^(-6)

    /*
    *
    * CR1 - Регистр управления
    * 
    * PE - Управление интерфейсом: 0 – интерфейс выключен; 1 – интерфейс включен
    *
    */
    i2c->CR1 |= I2C_CR1_PE_M;

    xprintf("\nВедомый. Старт\n");

}

void i2c_slave_DMA_mode(I2C_TypeDef* i2c, i2c_dma dma_mode)
{
    switch (dma_mode)
    {
        case i2c_dma_tx:
        I2C_0->CR1 |= I2C_CR1_TXDMAEN_M;
        break;
        case i2c_dma_rx:
        I2C_0->CR1 |= I2C_CR1_RXDMAEN_M;
        break;
        default:
        xprintf("Неизвестный режим поддержки DMA");
    }
}
void DMA_Channels_init(
    DMA_CONFIG_TypeDef* dma, 
    void* tx_address, void* rx_address, 
    int dma_request_index, uint8_t DMA_Channel, uint8_t data[],uint32_t count,
    uint32_t common_config, i2c_dma i2c_dma_mode
)
{
    // ConfigStatus - регистр DMA_CNTR
    dma->ConfigStatus = 0xFFFFFFFF;

    /*
    *
    * common_config - регистр CHx_CFG
    * 
    * DMA_CFG_CH_ENABLE_M - Разрешение работы канала:
    * 
    * DMA_CFG_CH_WRITE_REQ - Write requets. Выбор периферийной линии назначения. 5 для I2C_0
    * 
    * DMA_CFG_CH_READ_REQ - Read requets. Выбор периферийной линии источника.
    * 
    */
    common_config |= DMA_CFG_CH_ENABLE_M | 
        DMA_CFG_CH_WRITE_REQ(dma_request_index) |
        DMA_CFG_CH_READ_REQ(dma_request_index);
    /*
    * Выбор канала DMA
    * Создаем указатель типа DMA_CHANNEL_TypeDef и присваиваем ему 
    * адрес нулевого канала dma с наивысшим приоритетом 
    * 
    */

    DMA_CHANNEL_TypeDef* dma_ch;
    switch (DMA_Channel)
    {
    case DMA_CHANNEL_0:
        dma_ch = &(dma->CHANNELS[DMA_CHANNEL_0]);
        break;
    case DMA_CHANNEL_1:
        dma_ch = &(dma->CHANNELS[DMA_CHANNEL_1]);
        break;
    case DMA_CHANNEL_2:
        dma_ch = &(dma->CHANNELS[DMA_CHANNEL_2]);
        break;
    case DMA_CHANNEL_3:
        dma_ch = &(dma->CHANNELS[DMA_CHANNEL_3]);
        break;
    }
    

    /*
    *
    * DESTINATIONS - CHx_DST. Регистр адреса назначения канала
    * 
    * SOURCE - CHx_SRC. Регистр адреса источника канала
    * 
    * LEN - CHx_LEN. Регистр размера передаваемых данных канала. 
    * Количество байт пересылки расчитывается как Data_Len +1
    * *******************
    * CONFIG - CHx_CFG. Регистр управления и конфигурации канала
    * 
    * DMA_CFG_CH_READ_MODE_memory_M - Read mode. Режим адреса источника:
    * 
    * DMA_CFG_CH_READ_INCREMENT_M - Read incr. Инкремент адреса источника:
    * 
    * DMA_CFG_CH_WRITE_MODE_periphery_M - Write mode. Режим адреса назначения:
    * 
    * DMA_CFG_CH_WRITE_no_INCREMENT_M - Write incr. Инкремент адреса назначения:
    * 
    */

    switch(i2c_dma_mode)
    {
        case i2c_dma_tx:
            dma_ch->SOURCE = (uint32_t)data;
            dma_ch->DESTINATIONS = (uint32_t)(tx_address);
            dma_ch->LEN = count-1;
            dma_ch->CONFIG = common_config | 
            DMA_CFG_CH_READ_MODE_memory_M | DMA_CFG_CH_READ_INCREMENT_M |
            DMA_CFG_CH_WRITE_MODE_periphery_M | DMA_CFG_CH_WRITE_no_INCREMENT_M;
        break;
        case i2c_dma_rx:
            dma_ch->SOURCE = (uint32_t)(rx_address);
            dma_ch->DESTINATIONS = (uint32_t)data;
            dma_ch->LEN = count-1;
            dma_ch->CONFIG = common_config | 
                DMA_CFG_CH_READ_MODE_periphery_M | DMA_CFG_CH_READ_no_INCREMENT_M |
                DMA_CFG_CH_WRITE_MODE_memory_M | DMA_CFG_CH_WRITE_INCREMENT_M;
        break;
    }

}

void DMA_Slave_Wait(DMA_CONFIG_TypeDef* dma, i2c_dma i2c_dma_mode)
{
    uint32_t timeout = 10000000; 

    /*
    *
    * Последние 4 бита ConfigStatus - Статус состояния каналов:
    * «1» - готов к работе;
    * «0» - занят
    * 
    */
    switch(i2c_dma_mode)
    {
        case i2c_dma_tx:
            while (((dma->ConfigStatus & DMA_STATUS_READY(DMA_CHANNEL_0)) == 0) && (--timeout < 0));
        break;
        case i2c_dma_rx:
            while (((dma->ConfigStatus & DMA_STATUS_READY(DMA_CHANNEL_0)) == 0) && (--timeout < 0)); 
        break;
    }

    if (timeout < 0)
    {
        xprintf("DMA: ожидание готовности канала превышено\n");
    }
}

void DMA_check_data(uint8_t data[], int count, i2c_dma i2c_dma_mode)
{
    for(int i = 0; i < count; i++)
    {
        switch(i2c_dma_mode)
        {
            case i2c_dma_tx:
            xprintf("Отправка байта[%d]  0x%02x\n", i, data[i]);
            break;
            case i2c_dma_rx:
            xprintf("Чтение байта[%d]  0x%02x\n", i, data[i]);
            break;
        }
    }

    switch(i2c_dma_mode)
    {
        case i2c_dma_tx:
        xprintf("Отправлено %d\n\n", data[0] << 8 | data[1]);
        break;
        case i2c_dma_rx:
        xprintf("Получено %d\n\n", data[0] << 8 | data[1]);
        break;
    }
}

void i2c_slave_DMA(I2C_TypeDef* i2c, int dma_request_index, uint8_t data[], 
                    uint32_t count, uint8_t slave_address, i2c_dma i2c_dma_mode, 
                    uint8_t DMA_Channel, bool shift)
{
    uint8_t slave_adr = slave_address;
    // shift - true когда адрес ведомого должен быть сдвинут на 1 бит
    if(!shift)
    {
        slave_adr = slave_adr << 1;
    }
    

    DMA_CONFIG_TypeDef* dma = DMA_CONFIG;
    /*
    * DMA_CFG_CH_READ_SIZE_byte_M - Read size - Разрядность адреса источника:
    * 
    * DMA_CFG_CH_READ_BURST_SIZE_S - Read block size
    * Количество байт в пакете. Определяется как 2^Read block size
    * 
    * DMA_CFG_CH_WRITE_SIZE_byte_M - Write size. Разрядность адреса назначения
    */
    uint32_t config =   DMA_CFG_CH_READ_SIZE_byte_M |(0 << DMA_CFG_CH_READ_BURST_SIZE_S) | 
                        DMA_CFG_CH_WRITE_SIZE_byte_M |(0 << DMA_CFG_CH_WRITE_BURST_SIZE_S);

    DMA_Channels_init(dma, (void*)&i2c->TXDR, (void*)&i2c->RXDR, dma_request_index, DMA_Channel, data, count, config, i2c_dma_mode);

    switch (i2c_dma_mode)
    {
    case i2c_dma_tx:
        i2c->CR2 = I2C_CR2_SADD(slave_adr) | I2C_CR2_WR_M | I2C_CR2_NBYTES(count) | I2C_CR2_AUTOEND_M;
        break;
    case i2c_dma_rx:
        i2c->CR2 = I2C_CR2_SADD(slave_adr) | I2C_CR2_RD_M | I2C_CR2_NBYTES(count) | I2C_CR2_AUTOEND_M;
        break;
    }

    i2c->CR2 |= I2C_CR2_START_M; // старт отправки адреса, а затем данных 
    
    i2c->ICR |= I2C_ICR_ADDRCF_M; 
    DMA_Slave_Wait(dma, i2c_dma_mode);
    DMA_check_data(data, count, i2c_dma_mode);
}

int main()
{
    // Адрес ведомого
    uint8_t slave_address = 0x36;
    // Число для оптавки
    uint16_t to_send = 0; 
    // Массив с байтами для отправки / приема
    uint8_t data[2];

    // инициализация блока i2c в режиме слейв (ведомый)
    i2c_slave_init(I2C_0, slave_address); 

    i2c_dma i2c_dma_mode;

    while (1)
    {
        xprintf("\nОжидание\n");
        int counter = 0;

        // Ожидание запроса мастером адреса слейва
        while(!(I2C_0->ISR & I2C_ISR_ADDR_M))
        {
            counter++;
            if(counter==10000000)
            {
                xprintf("\nОжидание\n");
                counter = 0;
            }
            
        } 

        /*
        * I2C_ISR - Регистр прерываний и статуса
        *   
        * ADDR - Флаг соответствия адреса в ре-жиме ведомого
        *   
        * DIR - Тип передачи. Обновляется в режиме ведомого при получении адреса:
        *       0 – передача типа запись, ведомый переходит в режим приемника;
        *       1 – передача типа чтения, ведомый переходит в режим передатчика
        * 
        */
        if(I2C_0->ISR & I2C_ISR_DIR_M) // DIR = 1. Режим ведомого - передатчик
        {
            xprintf("\nЗапрос на запись\n");

            // Отправляется число to_send, состоящее из двух байт
            // Заполняется массив data байтами, которые нужно отправить
            data[0] = to_send >> 8;
            data[1] = to_send & 0b0000000011111111;

            i2c_dma_mode = i2c_dma_tx;
            i2c_slave_DMA_mode(I2C_0, i2c_dma_mode);
            i2c_slave_DMA(I2C_0, DMA_I2C_0_INDEX, data, sizeof(data), slave_address, i2c_dma_mode, DMA_CHANNEL_0, no_shift); 
        } 
        else // DIR = 0. Режим ведомого - приемник
        {
            xprintf("\nЗапрос на чтение\n");
            
            // Чтение двух байт от мастера и запись их в массив data 
            i2c_dma_mode = i2c_dma_rx;
            i2c_slave_DMA_mode(I2C_0, i2c_dma_mode);
            i2c_slave_DMA(I2C_0, DMA_I2C_0_INDEX, data, sizeof(data), slave_address, i2c_dma_mode, DMA_CHANNEL_0, no_shift); 

            // Формирование принятого числа
            to_send = data[0] << 8 | data[1];
            to_send++; 
            if(to_send > 65530)
            {
                to_send = 0;
            }
        }

    }
    
}