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, а также, что игры с делителем могут довести
Теперь давайте поплотнее пообщаемся со слейвом. Раз на макетке уже присутствует 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 свернуть / развернуть