Winner Micro W806 - I2C.


Продолжаем трогать за регистры китайский микроконтроллер W806. В этой заметке найдем еще одну неточность в RM и разберемся с работой модуля I2C.

Для полного контроля над периферией I2C разработчики чипа выделили 5 регистров:
I2C_PRESCALE_L
I2C_PRESCALE_H
I2C_EN
I2C_DATA
I2C_CR_SR

В первых двух содержится значение предделителя, для деления тактовой частоты шины APB до необходимой частоты шины I2C. В RM дана формула для расчета значения этих регистров:
I2C_PRESCALE_L должен содержать младшие 8 бит, а I2C_PRESCALE_H — старшие 8 бит результата (APB_clk(MHz)*1000)/(5*I2C_clk(KHz)) – 1. При максимальном значении частоты шины APB — 40 МГц, значение предделителя должно быть 79 для частоты I2C 100 КГц, и 19 для частоты 400 КГц.
Нетрудно догадаться, что максимальное возможное значение предделителя, для стандартной частоты I2C 100 КГц не превысит 79. Зачем для этого выделять целых 16 бит, да еще в двух разных 32-битных(!) регистрах — остается загадкой. Значения по умолчанию 0xFF (оба регистра) соответственно дают итоговую частоту около 111 Гц.
Следующий регистр — I2C_EN, в нем доступно для записи-чтения всего два бита I2C_EN_ENABLE, установка которого в «1» включает периферийный модуль (по умолчанию «0»), и I2C_EN_IEMASK, сброс которого в «0» разрешает прерывания от I2С (по умолчанию — «1», прерывания запрещены). Источников прерывания от I2C три — завершение передачи или приема байта, и (предположительно) Arbitration Lost.
В регистре I2C_DATA помещается отправляемый или принятый байт данных.
И последний необходимый регистр — I2C_CR_SR. Из 32-х бит в работе участвуют только 8 младших, и то не все. Когда в этот регистр производится запись, то он работает как управляющий, а когда из него происходит чтение — он является статусным регистром.
Значения битов сведены в таблицу:

Не вздумайте в этот регистр писать через read-modify-write!!! Огребете кучу глюков! Запись только через "="!

Для включения альтернативных функций пинов PORTA1 и PORTA4 (I2C_SCL и I2C_SDA) достаточно вписать в регистр GPIOA->AF_S0 единицы. Настраивать пины порта на вход или выход не обязательно, так же как и трогать регистр GPIOA->AF_SEL. А вот если записать в него «0» напротив этих ножек порта — I2С работать не будет. Вероятно, есть возможность замапить I2C на PORTB20 и PORTB19, но там висит UART0, через который работает бутлоадер, и понапрасну его трогать не рекомендуется.
Вышесказанного уже достаточно, чтобы попробовать передать байт по шине. Напишем простенький сканер адресов, который будет отправлять в шину адреса в диапазоне от 0x08 до 0xF0, и отправлять в консоль адрес, который ответил ACK:
#include "wm_hal.h"
#include <stdio.h>
char char_buff[30];
	
uint8_t i2c_send_addr(uint8_t addr)
{
    I2C->DATA = addr;		// кладем адрес слейва в регистр данных I2C
    I2C->CR_SR = I2C_CR_START | I2C_CR_WR | I2C_CR_STOP;	// выдаем на шину START, запуcкаем передачу, по окончании передачи байта выдаем STOP 
    while(I2C->CR_SR & I2C_SR_TIP){};	// ждем окончания отправки
    return (I2C->CR_SR &  I2C_SR_RXACK);	// если обнаружили NACK, возвращаем "1", если ACK - то "0"
}

int main(void)
{
    uint32_t div = (40*1000) / (5*100) - 1; // 40 (MHz) APB bus, calculate prescaler for I2C
	
    GPIOA->AF_S0 |= GPIO_PIN_4 | GPIO_PIN_1;      // select Alternate Function for PORTA_1 - I2C_SCL, PORTA_4 - I2C_SDA
   
    I2C->PRESCALE_L = div & 0xff;               // clock divider
    I2C->PRESCALE_H = (div >> 8) & 0xff;        // clock divider
    I2C->EN |= I2C_EN_ENABLE;                  	// periph enable

    printf("Start I2C Address scan...\n\r");
    for(uint8_t addr = 0x08; addr<0xF0; addr+=2)
    {
        if(!i2c_send_addr(addr))
        {
            sprintf(char_buff, "0x%.2X addr ACK found!\n\r", addr);
            printf(char_buff);
        }
    }
    printf("I2C Address scan finished \n\r");
    while (1){};
}




Но где-же расчетные 100 КГц? И здесь китайцы обсчитали! :) Опытным путем выяснил, что для 100 КГц делитель должен быть 72, а для 400 КГц — 16, а также, что игры с делителем могут довести до цугундера тактовую частоту I2C до 1,5 МГц! Не каждый слейв это потянет, но то что такой запас есть — это плюс.
Теперь давайте поплотнее пообщаемся со слейвом. Раз на макетке уже присутствует DS1307, то прочитаем ее контрольный регистр по внутреннему адресу 0x07.
#include "wm_hal.h"

int main(void)
{
	uint32_t div = 72; // prescaler for 100 Khz i2c clock

	GPIOA->AF_S0 |= GPIO_PIN_4 | GPIO_PIN_1;      // select Alternate Function for PORTA_1 - I2C_SCL, PORTA_4 - I2C_SDA
   
	I2C->PRESCALE_L = div & 0xff;               // clock divider
	I2C->PRESCALE_H = (div >> 8) & 0xff;        // clock divider
	I2C->EN |= I2C_EN_ENABLE;                  	// periph enable

	I2C->DATA = 0xD0;		// заносим в регистр данных адрес DS1307
	I2C->CR_SR = I2C_CR_WR | I2C_CR_START;	// включаем модуль на передачу и выдаем START
	while(I2C->CR_SR & I2C_SR_TIP){};// ожидаем окончания передачи
	I2C->DATA = 0x07;		// заносим в регистр данных адрес регистра
	I2C->CR_SR = I2C_CR_WR ;	// передаем байт
	while(I2C->CR_SR & I2C_SR_TIP){}; // ожидаем окончания передачи
	I2C->DATA = 0xD0 + 1 ;	// передаем адрес DS1307 с признаком чтения
	I2C->CR_SR = I2C_CR_WR | I2C_CR_START ;	// передаем байт и повторный старт, для начала чтения
	while(I2C->CR_SR & I2C_SR_TIP){}; // ожидаем окончания передачи
	I2C->CR_SR = I2C_CR_RD |  I2C_CR_ACK | I2C_CR_STOP ;	// принимаем байт и сразу же после него - NACK и STOP
        while (I2C->CR_SR & I2C_SR_BUSY);	// ожидаем освобождения шины
        // здесь можно присвоить принятый байт какой-либо переменной путем чтения I2C->DATA
        // some_variable = I2C->DATA;
	while (1){};
}

При приеме последнего байта можно одной операцией выставить сразу три бита — чтение из шины, отправка слейву NACK и последующий STOP.
Теперь выдернем DS1307 из макетки и повторим попытку чтения байта:
Как видим, обмен не виснет, как это было бы на STM8, но как же узнать, был ли правильно передан или прочитан байт?
Напишем функцию отправки байта, с проверкой ответа ACK:
#include "wm_hal.h"

uint8_t i2c_send(uint8_t addr, uint8_t sl_reg, uint8_t data)
{
	I2C->DATA = addr;		// заносим в регистр данных адрес слейва
	I2C->CR_SR = I2C_CR_WR | I2C_CR_START;	// включаем модуль на передачу и выдаем START
	while(I2C->CR_SR & I2C_SR_TIP){};	// ожидаем окончания передачи
	if(I2C->CR_SR & I2C_SR_RXACK)	// если по окончанию передачи байта слейв не ответил
	{	
		I2C->CR_SR = I2C_CR_STOP;	// останавливаем обмен, 
		while (I2C->CR_SR & I2C_SR_BUSY);	// ожидаем освобождения шины
		return 1;		// возвращаем код ошибки "1"
	}						// если есть ответ от слейва
	I2C->DATA = sl_reg;		// заносим в регистр данных адрес регистра слейва
	I2C->CR_SR = I2C_CR_WR ;	// передаем байт
	while(I2C->CR_SR & I2C_SR_TIP){}; // ожидаем окончания передачи
	I2C->DATA = data;	// заносим в регистр данных байт на отправку
	I2C->CR_SR = I2C_CR_WR | I2C_CR_STOP ;	// передаем байт и по окончании передачи - STOP
	while (I2C->CR_SR & I2C_SR_BUSY);	// ожидаем освобождения шины
	return 0;	// возвращаем "0" - передача успешна
}

int main(void)
{
    uint32_t div = 72; // prescaler for 100 Khz i2c clock
    GPIOB->DIR |= GPIO_PIN_0 | GPIO_PIN_1;
    GPIOA->PULLUP_EN &= ~(GPIO_PIN_2);			// включим подтяжку к питанию на PORTA2
    GPIOA->AF_S0 |= GPIO_PIN_4 | GPIO_PIN_1;	// select Alternate Function for PORTA_1 - I2C_SCL, PORTA_4 - I2C_SDA
    I2C->PRESCALE_L = div & 0xff;				// clock divider
    I2C->PRESCALE_H = (div >> 8) & 0xff;        // clock divider
    I2C->EN |= I2C_EN_ENABLE;                  	// periph enable

    if(i2c_send(0xD0, 0x07, 1<<4))		// включим вывод меандра 1Гц  на ножке SQW/OUT
    {
        GPIOB->DATA &= ~(GPIO_PIN_0);	// светодиод на ножке PB0 загорится, если слейв не ответил ACK
    }
    else
    {
        GPIOB->DATA |= GPIO_PIN_0;	// и погаснет, если слейв присутствует на шине
        i2c_send(0xD0, 0x00, ~(1<<7));	// тогда заодно запустим осциллятор на DS1307
    }
    while (1)
    {
	if(GPIOA->DATA & GPIO_PIN_2)	// светодиод на PB1 должен замигать, если обмен прошел успешно
	{
	    GPIOB->DATA &= ~(GPIO_PIN_1);
	}
	else
	{
	    GPIOB->DATA |= GPIO_PIN_1;
	}
    };
}

Сетап на макетке такой: DS1307, ее вывод SQW подключен на PORTA2, чуть не забыл, что ей тоже нужно тактирование :)
Прошиваем, и видим как светодиод на PORTB1 замигал с частотой 1 Гц. Если же отключить микросхему часов, то загорятся оба светодиода на плате W806 — PORTB0 и PORTB1.
Работа по прерываниям будет описана немного позже, что-то я туплю, как в прерывании определять, передается байт или принимается. А кучу лишних переменных вводить не хочется.
Разобрался с прерываниями от I2C, рассказываю.
Как ни уворачивался, а пришлось ввести структуру, которая и будет контролировать весь обмен по шине. Код достаточно комментирован, так что разобраться можно без особого труда. Состояние конечного автомата STEP_RD1ST введено для чтения первого байта от слейва, костыль, навенрное, но лучшего не придумал :(
В приведенном примере сначала записываются 32 байта в оперативную память DS1307, а потом считываются 8 байт оттуда же. Записанные и считанные байты выводятся в консоль прошивальщика.
#include "wm_hal.h"
#include <stdio.h>
#define ISR __attribute__((isr)) void

char buff[16];

#define ARRAY_SIZE 32
uint8_t i2c_array[ARRAY_SIZE];

#define DIR_READ 	1
#define DIR_WRITE	0
#define STEP_ADDR	0
#define STEP_REPSTART	1
#define STEP_TRANSF	2
#define STEP_DONE	3
#define STEP_RD1ST	4

volatile struct 
{
    uint8_t slave_addr;		// address of slave device 
    uint8_t slave_reg;		// internal offset address 
    uint8_t* dat_pointer;	// pointer to data storage
    uint8_t byte_count;		// amount of bytes to be transfered
    uint8_t dir;		// transfer direction (1 - read, 0 - write)
    uint8_t step;		// current state of transfer
}i2c_ctrl;

// start reading "num_bytes" from offset of "slave_reg" from device on "slave_addr" to "dest" data array
void i2c_read(uint8_t slave_addr, uint8_t slave_reg, uint8_t* dest, uint8_t num_bytes)
{
    i2c_ctrl.step = STEP_ADDR;		// set state - "sending slave address"
    i2c_ctrl.dir = DIR_READ;		// set direction of transfer
    i2c_ctrl.slave_addr = slave_addr;	// set slave address
    i2c_ctrl.slave_reg = slave_reg;		// set internal slave offset addr
    i2c_ctrl.dat_pointer = dest;		// set address of data array
    i2c_ctrl.byte_count = num_bytes;	// set amount of bytes
    I2C->DATA = i2c_ctrl.slave_addr;	// 
    I2C->CR_SR =  I2C_CR_WR | I2C_CR_START;    // HW I2C "start" & direction "write"
}

// start writing "num_bytes" at offset of "slave_reg" to device on "slave_addr" from "src" data array
void i2c_write(uint8_t slave_addr, uint8_t slave_reg, uint8_t* src,uint8_t num_bytes)
{
    i2c_ctrl.step = STEP_ADDR; // set state - "sending slave address"
    i2c_ctrl.dir = DIR_WRITE; // set direction of transfer
    i2c_ctrl.dat_pointer = src;	// set addres of data array
    i2c_ctrl.slave_addr = slave_addr;	// set slave address
    i2c_ctrl.slave_reg = slave_reg;	// set internal slave offset addr 
    i2c_ctrl.byte_count = num_bytes;	// set amount of bytes
    I2C->DATA = i2c_ctrl.slave_addr;
    I2C->CR_SR =  I2C_CR_WR | I2C_CR_START;    // start & direction "write"
}
 
ISR I2C_IRQHandler(void)
{ 
    I2C->CR_SR = I2C_SR_IF;            // clear interrupt flag
    switch (i2c_ctrl.step)
    {
        case STEP_ADDR:
	    I2C->DATA = i2c_ctrl.slave_reg;	// send slave register offset
	    I2C->CR_SR = I2C_CR_WR;	// i2c write
	    if(i2c_ctrl.dir)	// if now reading transfer
	    {
	        i2c_ctrl.step = STEP_REPSTART;	// next step - Repeated Start
	    }
            else
	    {
	        i2c_ctrl.step = STEP_TRANSF;	// start transfering bytes
	    }
	break;
		
	case STEP_REPSTART:	// if reading transfer and slave offset has been sent
	    i2c_ctrl.step = STEP_RD1ST;	// 
	    I2C->DATA = i2c_ctrl.slave_addr + i2c_ctrl.dir; // send slave addr with "read" bit
	    I2C->CR_SR = I2C_CR_WR | I2C_CR_START ;	// rep start & write slave address+'read'
	break;
			
        case STEP_RD1ST:		// start reading 1st byte from slave
	    I2C->CR_SR = I2C_CR_RD;
            i2c_ctrl.byte_count--;
	    i2c_ctrl.step = STEP_TRANSF;
        break;
			
	case STEP_TRANSF:
	    if(i2c_ctrl.byte_count)	// if not all bytes sent
	    {
	        if(i2c_ctrl.dir)	// if reading
                {
	            *i2c_ctrl.dat_pointer++ = I2C->DATA; // read byte from RX data register
		    if(i2c_ctrl.byte_count == 1)	// if it is a last byte in a transfer
		    {
			I2C->CR_SR = I2C_CR_RD | I2C_CR_ACK;	// send NACK to slave
		    }
                    else
		    {
			I2C->CR_SR = I2C_CR_RD;	// read and ACK
		    }
		}
		else	// if writing
		{
		    I2C->DATA = *i2c_ctrl.dat_pointer++;	// load next byte to TX data register
		    I2C->CR_SR = I2C_CR_WR;	// 'write' byte to bus
		}
		i2c_ctrl.byte_count--;	// decrement remainig bytes
	    }
	    else		// if all bytes was sent
	    {
		i2c_ctrl.step = STEP_DONE;	// finish transfer flag set
		I2C->CR_SR = I2C_CR_STOP;	// send 'stop' on bus
		while(I2C->CR_SR & I2C_SR_BUSY);	// wait bus free
	    }
	break;
    }
}

int main(void)
{
   uint32_t div = 72;  // 40 (MHz) APB bus, calculate prescaler for I2C
	
    GPIOB->DIR |= GPIO_PIN_0;
    GPIOA->DIR |= GPIO_PIN_4 | GPIO_PIN_1;  // set I2C hardware pins to out
    GPIOA->AF_S0 |= GPIO_PIN_4 | GPIO_PIN_1;// select Alternate Function for PORTA_1 - I2C_SCL, PORTA_4 - I2C_SDA
    I2C->PRESCALE_L = div & 0xff;           // dec 79 to prescale clock
    I2C->PRESCALE_H = (div >> 8) & 0xff;    //   clock divider
    I2C->EN |= I2C_EN_ENABLE;               // periph enable
    I2C->EN &= ~(I2C_EN_IEMASK);	// enable interrupts I2C
    NVIC_EnableIRQ(I2C_IRQn);		// enable global interrupts I2C
   
    for(uint8_t i = 0; i<ARRAY_SIZE; i++)	// fill data to send, just sequence of numbers
    {
	i2c_array[i] = i;
    }
    printf("Start writing...\n\r");
    i2c_write(0xD0, 0x08, i2c_array, ARRAY_SIZE);
    while(i2c_ctrl.step != STEP_DONE){};
    for(uint8_t i = 0; i<ARRAY_SIZE; i++)
    {
	sprintf(buff, "0x%.2X \n\r", i2c_array[i]);
	printf(buff);
    }
    printf("Writing done!\n\r");
	
    printf("Start reading...\n\r");
    i2c_read(0xD0, 0x08, i2c_array, 8);
    while(i2c_ctrl.step != STEP_DONE);
    for(uint8_t i = 0; i<8; i++)
    {
	sprintf(buff, "0x%.2X \n\r", i2c_array[i]);
	printf(buff);
    }
    printf("Reading done!\n\r");
   while (1){}
}

Запись:

Чтение:

Результат:


Еще обнаружил кое-что интересное про регистр I2C_EN:

Младшие 6 бит доступны для записи и чтения! Хоть и указаны в RM как reserved. На свой страх и риск можно использовать их как статусные биты при работе по прерываниям, а можно отдать их под счетчик переданных байт, от 0 до 63х включительно. Причем, при достижении счетчиком значения 64 бит I2C_EN_IEMASK установится в «1», что автоматически отключит прерывания! Удобно же!
К сожалению, больше ни к каким регистрам I2C такой грязный хак применить не удалось, старшие 24 бита регистра I2C_EN тоже доступны только для чтения. Возможно в следующих ревизиях чипа и эти «неучтенные» 6 младших бит отдадут под что-нибуь полезное.
На этом с I2C вроде бы все!
Поправки и дополнения в комментариях — категорически приветствуются!
  • +4
  • 28 января 2022, 13:10
  • finskiy
  • 3

Комментарии (0)

RSS свернуть / развернуть
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.