Первые шаги с STM32 и компилятором mikroC для ARM архитектуры - Часть 4 - I2C, pcf8574 и подключение LCD на базе HD4478
Следующую статью я хочу посвятить работе с распространенным интерфейсом i2c, достаточно часто используемом в разнообразных микросхемах, подключаемых к микроконтроллеру.
I2C представляет собой шину работающую по двум физическим соединениям (помимо общего провода). Достаточно много о ней расписано в Интернете, неплохие статьи есть в Википедии . Кроме того алгоритм работы шины очень понятно описан . В вкратце, шина представят собой двухпроводную синхронную шину. На шине может одновременно находится до 127 устройств (адрес устройства 7-битный, к этому вернемся далее). Ниже приведена типичная схема подключения устройств к i2c шине, с МК в качестве ведущего устройства.
Для i2c все устройства (как мастер так и слейвы) используют open-drain выходы. Проще говоря они могут притягивать шину ТОЛЬКО К ЗЕМЛЕ. Высокий уровень ша шине обеспечивается подтягивающими резисторами. Номинал этих резисторов обычно выбирается в диапазоне от 4,7 до 10 кОм. i2c достаточна чувствительна к физическим линиям, соединяющим устройства, поэто если используется соединение с большой емкостью (например длинный тонкий или экранированный кабель), влияние этой емкости может «размыть» фронты сигналов и помешать нормальной работе шины. Чем меньше подтягивающий резистор, тем меньше влияет эта емкость на характеристику фронтов сигнала, но ТЕМ БОЛЬШЕ НАГРУЗКА на выходные транзисторы на интерфейсах i2c. Значение этих резисторов подбирается для каждой конкретной реализации, но они не должны быть меньше 2,2 кОмов, иначе можно просто спалить выходные транзисторы в устройствах, работающих с шиной.
Шина состоит из двух линий: SDA (линии данных) и SCL (тактирующего сигнала). Тактирует шину Мастер устройство , обычно наш МК. Когда на SCL высокий уровень информация считывается с шины данных. Изменять состояние SDA можно только при низком уровне тактирующего сигнала . При высоком уровне SCL сигнал на SDAизменяется при формировании сигналов START (при высоком уровне SCL сигнал на SDA изменяется высокого на низкий) и STOP - при высоком уровне SCL сигнал на SDA изменяется с низкого на высокий).
Отдельно следует сказать, что в i2c адрес задается 7-битным числом. 8 - младший бит указывает направление передачи данных 0 - означает что слейв будет передавать данные, 1 - принимать. . Вкратце алгоритм работы с i2c такой:
- Высокий уроень на SDA и SCL - шина свободна, можно начинать работу
- Мастер поднимает SCL в 1, и изменяет состояние SDA c 1 на 0 - притягивает его к земле - формируется сигнал START
- Мастер передает 7-битный адрес слейва с битом направления (данные на SDA выставляются когда SCL притянут к земле, и читаются слейвом когда он отпущен). Если слейв не успевает «схавать» предыдущий бит, он притягивает SCL к земле, давая понять мастеру что состаяние шинны данных не нужно менять: «еще читаю предыдущий». После того как мастер отпустил шину он проверяет, отпустил ли ее слейв .
- После передачи 8 бит адреса мастер генерирует 9-й такт и отпускает шину данных. Если слейв услышал и свой адрес и принял его то он прижмет SDA к земле . Так формируется сигнал ASK - принял, все ОК. Если слейв ничего не понял, или его просто там нет то некому будет прижать шину. мастер подождет таймаут и поймет что его не поняли.
- После передачи адреса, если у нас выставлено направление от мастера к слейву (8 бит адреса равен 1), то мастер передает данные в слейв, не забывая после передачи каждого байта проверять наличие ASK от слейва, ожидая обработки поступившей информации ведомым устройством.
- При приеме мастером данных от слейва, мастер сам формирует сигнал ASK после приема каждого байта, а слейв контролирует его наличие. Мастер может специально не послать ASK перед отправкой команды STOP , обычно, так давая понять ведомому, что больше предавать данные не нужно.
- Если после отправки данных мастером (режим записи) необходимо прочитать данные со слейва, то мастер формирует снова сигнал START , отправляя адрес слейва с флагом чтения. (еcли перед командой START не был передан STOP то формируется команда RESTART ). Это используется для смены направления общения мастре-слейв. Например мы передаем слейву адрес регистра, а потом читаем из него данные.)
- По окончанию работы со слейвом мастер формирует сигнал STOP - при высоком уровне тактирующего сигнала формирует переход шины данных с 0 в 1.
В MicroC перед использованием i2c (как впрочем и любой периферии) ее необходимо должным образом проинициализировать. Для этого используем такую функцию (Иннициализация в качестве мастера):
I2Cn_Init_Advanced(unsigned long: I2C_ClockSpeed, const Module_Struct *module);
- n - номер используемого модуля, например I2C1 или I2C2 .
- I2C_ClockSpeed - скорость работы шины, 100000 (100 kbs, стандартный режим) или 400000 (400 kbs, быстрый режим). Второй в 4 раза быстрее, но его поддерживают не все устройства
- *module - указатель на периферийный модуль, например &_GPIO_MODULE_I2C1_PB67 , здесь не забываем что Code Assistant (ctrl-пробел ) очень помогает.
I2Cn_Start();
где n
- номер используемого модуля i2c нашего микроконтроллера. Функция вернет 0 если на шине возникла ошибка и 1 если все ОК.
Для того чтоб передать данные слейву используем функцию:
I2Cn_Write(unsigned char slave_address, unsigned char *buf, unsigned long count, unsigned long END_mode);
- n - номер используемого модуля
- slave_address - 7-битный адрес слейва.
- *buf - указатель на наши данные - байт или массив байтов.
- count - количество передаваемых байт данных.
- END_mode - что делать после передачи данных слейву, END_MODE_STOP - передать сигнал STOP , либо END_MODE_RESTART снова отправить START , сформировав сигнал RESTART и дав понять ведомству, что сеанс работы с ним не окончен и с него сейчас будут читать данные.
I2Cn_Read(char slave_address, char *ptrdata, unsigned long count, unsigned long END_mode);
- n - номер используемого модуля
- slave_address - 7-битный адрес слейва.
- *buf - указатель на переменную или массив в который мы принимаем данные, тип char или short int
- count - количество принимаемых байт данных.
- END_mode - что делать после приема данных от слейва - END_MODE_STOP - передать сигнал STOP , либо END_MODE_RESTART отправить сигнал RESTART .
Адрес микросхемы формируется из состояния выводов A0, А1, А2 . Для микросхемы PCF8574 адрес будет: 0100A0A1A2 . (Например у нас A0, А1, А2 имеют высокий уровень, соответственно адрес нашей микросхемы будет 0b0100111 = 0x27). Для PCF8574A - 0111A0A1A2 , что с нашей схемой подключения даст адрес 0b0111111 = 0x3F . Если, допустим A2 соединить с землей, то адрес для PCF8574A будет 0x3B . Итого на одну шину i2c можно одновременно повесить 16 микросхем, по 8 PCF8574A и PCF8574.
Давайте попробуем что-то передать иннициализировать i2c шину и что-то передать нашей PCF8574.
#define PCF8574A_ADDR 0x3F //Адреc нашей PCF8574
void I2C_PCF8574_WriteReg(unsigned char wData)
{
I2C1_Start(); // Формируем сигнал START
I2C1_Write(PCF8574A_ADDR,&wData, 1, END_MODE_STOP); // Передаем 1 байт данных и формируем сигнал STOP
}
char PCF8574A_reg; // переменная которую мы пишем в PCF8574
void main ()
{
I2C1_Init_Advanced(400000, &_GPIO_MODULE_I2C1_PB67); // Запускаем I2C
delay_ms(25); // Немного подождем
PCF8574A_reg.b0 = 0; //зажжем первый светодиод
PCF8574A_reg.b1 = 1; // погасим второй светодиод
while (1)
{
delay_ms(500);
PCF8574A_reg.b0 = ~PCF8574A_reg.b0;
PCF8574A_reg.b1 = ~PCF8574A_reg.b1; //инвертируем состояние светодиодов
I2C_PCF8574_WriteReg (PCF8574A_reg); //передадим нашей PCF8574 данные
}
}
Компилируем и запускаем нашу программу и видим что наши светодиоды попеременно моргают.
Я не просто так подключил светодиоды катодом к нашей PCF8574. Все дело в том, что микросхема при подачи на выход логического 0 честно притягивает свой вывод к земле, а вот при подаче логической 1 подключает его к + питания через источник тока в 100 мкА. Тоесть «честной» логической 1 на выходе не получить. И светодиод от 100 мкА не зажечь. Сделано это для того, чтобы без дополнительных регистров настраивать вывод PCF8574 на вход. Мы просто пишем в выходной регистр 1 (фактически устанавливаем состояния ножки в Vdd) и можем просто коротить его на землю. Источник тока не даст «сгореть» выходному каскаду нашего расширителя ввода/вывода. Если ножка притянута к земле, то на ней потенциал земли, и читается логический 0. Если ножка притянута к +, то читается логическая 1. С одной стороны просто, но с другой, про это всегда нужно помнить, работая с данными микросхемами.
Давайте попробуем прочитать состояние выводов нашей микросхемы-расширителя.
#define PCF8574A_ADDR 0x3F //Адреc нашей PCF8574
void I2C_PCF8574_WriteReg(unsigned char wData)
{
I2C1_Start(); // Формируем сигнал START
I2C1_Write(PCF8574A_ADDR, &wData, 1, END_MODE_STOP); // Передаем 1 байт данных и формируем сигнал STOP
}
void I2C_PCF8574_ReadReg(unsigned char rData)
{
I2C1_Start(); // Формируем сигнал START
I2C1_Read(PCF8574A_ADDR, &rData, 1, END_MODE_STOP); // Читаем 1 байт данных и формируем сигнал STOP
}
char PCF8574A_reg; //переменная которую мы пишем в PCF8574
char PCF8574A_out; // переменная в которую мы читаем и PCF8574
char lad_state; //включен либо выключен наш светодиод
void main ()
{
I2C1_Init_Advanced(400000, &_GPIO_MODULE_I2C1_PB67); // Запускаем I2C
delay_ms(25); // Немного подождем
PCF8574A_reg.b0 = 0; // зажжем первый светодиод
PCF8574A_reg.b1 = 1; // погасим второй светодиод
PCF8574A_reg.b6 = 1; // Притяним выводы 6 и 7 к питанию.
PCF8574A_reg.b7 = 1;
while (1)
{
delay_ms(100);
I2C_PCF8574_WriteReg (PCF8574A_reg); // пишем данные в РCF8574
I2C_PCF8574_ReadReg (PCF8574A_out); // читаем из РCF8574
if (~PCF8574A_out.b6) PCF8574A_reg.b0 = ~PCF8574A_reg.b0; // Если нажата 1 кнопка (6 бит прочитанного байта из РCF8574 равен 0, то включим/выключим наш светодиод)
if (~PCF8574A_out.b7) PCF8574A_reg.b1 = ~PCF8574A_reg.b1; // аналогично для 2 кнопки и 2 светодиода
}
}
Теперь нажимая на кнопочки мы включаем или отключаем наш светодиод. У микросхемы есть еще вывод INT
. На нем формируется импульс каждый раз, когда меняется состояние выводов нашего расширителя ввода/вывода. Подключив его в входу внешнего прерывания нашего МК (как настроить внешние прерывания и как с ними работать я расскажу в одной из следующих статей).
Давайте используя наш расширитель портов подключим через него символьный дисплей. Таких существует великое множество, но практически все они построены на базе чипа-контроллера HD44780 и его клонов. Например я использовал дисплей LCD2004.
Даташит на него и контроллер HD44780 можно с легкостью найти в Интернете. Подключим наш дисплей к РCF8574, а ее, соответственно к нашему STM32.
HD44780
использует параллельный стробируемый интерфейс. Данные передаются по 8 (за один такт) либо 4 (за 2 такта) стробирующего импульса на выводе E
. (читаются контроллером дисплея по нисходящему фронту, переходу с 1 в 0). Вывод RS
указывает передаем ли мы нашему дисплею данные (RS = 1
) (символы которые он должен отобразить, фактически из ASCII коды) либо команды (RS = 0
). RW
указывает направление передачи данных, запись либо чтение. Обычно мы пишем данные в дисплей, поэтому (RW = 0
). Резистор R6 управляет контрастностью дисплея. Просто подключать вход регулировке контрастности к земле или питанию нельзя, иначе ничего не увидите.
. VT1 служит для включения и выключения подсветки дисплея по командам МК. В MicroC есть библиотека для работе с такими дисплеями по параллельному интерфейсу, но обычно, тратить на дисплей 8 ног накладно, поэтому я практически всегда использую РCF8574 для работы с такими экранчиками. (Если кому-то будет интересно, то напишу статью про работу с дисплеями на базе HD44780 встроенными в MicroC по параллельному интерфейсу.) Протокол обмена не особо сложный (мы будем использовать 4 линии данных и передавать информацию за 2 такта), его наглядно показывает следующая временная диаграмма:
Перед передачей данных на наш дисплей его надо проинициаллизировать, передав служебные команды. (описаны в даташите, здесь приведем только самые используемые)
- 0x28 - связь с индикатором по 4 линиям
- 0x0C - включаем вывод изображения, отключаем отображение курсора
- 0x0E - включаем вывод изображения, включаем отображение курсора
- 0x01 - очищаем индикатор
- 0x08 - отключаем вывод изображения
- 0x06 - после вывода символа курсор сдвигается на 1 знакоместо
#define PCF8574A_ADDR 0x3F //Адреc нашей PCF8574
#define DB4 b4 // Соответствие выводов PCF8574 и индикатора
#define DB5 b5
#define DB6 b6
#define DB7 b7
#define EN b3
#define RW b2
#define RS b1
#define BL b0 //управление подсветкой
#define displenth 20 // количество символов в строке нашего дисплея
static unsigned char BL_status; // переменная хранящая состояние подсветки (вкл/выкл)
void lcd_I2C_Init(void);
// Функция иннициализации дисплея и PCF8574
void lcd_I2C_txt(char *pnt);
// Выводит на экран строку текста, параметр - указатель на эту строку
void lcd_I2C_int(int pnt);
// Выводит на экран значение целочисленной переменной, параметр - выводимое значение
void lcd_I2C_Goto(unsigned short row, unsigned short col);
// перемещает курсор на указанную позицию, параметры row - строка (от 1 до 2 или 4 в зависимости от дисплея) и col - (от 1 до displenth))
void lcd_I2C_cls();
// Очищает экран
void lcd_I2C_backlight (unsigned short int state);
// Включает (при передаче 1 и отключает - при передаче 0 подсветку дисплея)
Теперь опишем наши фунции, снова идем в Project Maneger
клацнем правой кнопкой по папке Sources
и выберем Add New File
. Создаем файл «i2c_lcd.с»
.
#include "i2c_lcd.h" //инклудим наш хедер-файл
char lcd_reg; //регистр временного хранения данных отправляемых в PCF8574
void I2C_PCF8574_WriteReg(unsigned char wData) //функция отпарвки данных по i2c в чип PCF8574
{
I2C1_Start();
I2C1_Write(PCF8574A_ADDR,&wData, 1, END_MODE_STOP);
}
void LCD_COMMAND (char com) //функция отправки команды нашему дисплею
{
lcd_reg = 0; //пишем 0 во временный регистр
lcd_reg.BL = BL_status.b0; //пин подсветки выставляем в соответстви со значением переменной, хранящей состояние подсветки
lcd_reg.DB4 = com.b4; //выставляем на шину данных индикатора 4 старших бита нащей команды
lcd_reg.DB5 = com.b5;
lcd_reg.DB6 = com.b6;
lcd_reg.DB7 = com.b7;
lcd_reg.EN = 1; //ставим строб. вывод в 1
I2C_PCF8574_WriteReg (lcd_reg); //пишем в регистр PCF8574, фактически отправив данные на индикатор
delay_us (300); //ждем тайммаут
lcd_reg.EN = 0; //сбрасываем строб импульс в 0, индикатор читает данные
I2C_PCF8574_WriteReg (lcd_reg);
delay_us (300);
lcd_reg = 0;
lcd_reg.BL = BL_status.b0;
lcd_reg.DB4 = com.b0; //то же самое для 4 младших бит
lcd_reg.DB5 = com.b1;
lcd_reg.DB6 = com.b2;
lcd_reg.DB7 = com.b3;
lcd_reg.EN = 1;
I2C_PCF8574_WriteReg (lcd_reg);
delay_us (300);
lcd_reg.EN = 0;
I2C_PCF8574_WriteReg (lcd_reg);
delay_us (300);
}
void LCD_CHAR (unsigned char com) //отправка индикатору данных (ASCII кода символа)
{
lcd_reg = 0;
lcd_reg.BL = BL_status.b0;
lcd_reg.EN = 1;
lcd_reg.RS = 1; //отправка символа отличается от отправки команды установкой в 1 бита RS
lcd_reg.DB4 = com.b4; //выставляем на входах 4 старших бита
lcd_reg.DB5 = com.b5;
lcd_reg.DB6 = com.b6;
lcd_reg.DB7 = com.b7;
I2C_PCF8574_WriteReg (lcd_reg);
delay_us (300);
lcd_reg.EN = 0; //сбрасываем строб. импульс в 0, индикатор читает данные
I2C_PCF8574_WriteReg (lcd_reg);
delay_us (300);
lcd_reg = 0;
lcd_reg.BL = BL_status.b0;
lcd_reg.EN = 1;
lcd_reg.RS = 1;
lcd_reg.DB4 = com.b0; //выставляем на входах 4 младших бита
lcd_reg.DB5 = com.b1;
lcd_reg.DB6 = com.b2;
lcd_reg.DB7 = com.b3;
I2C_PCF8574_WriteReg (lcd_reg);
delay_us (300);
lcd_reg.EN = 0;
I2C_PCF8574_WriteReg (lcd_reg);
delay_us (300);
}
void lcd_I2C_Init(void)
{
I2C1_Init_Advanced(400000, &_GPIO_MODULE_I2C1_PB67); //иннициализируем наш I2c модуль у МК
delay_ms(200);
lcd_Command(0x28); // Дисплей в режиме 4 бита за такт
delay_ms (5);
lcd_Command(0x08); //Отключаем вывод данных на дисплей
delay_ms (5);
lcd_Command(0x01); //Очищаем дисплей
delay_ms (5);
lcd_Command(0x06); //Включаем автоматический сдвиг курсора после вывода символа
delay_ms (5);
lcd_Command(0x0C); //Включаем отображение информации без отображения курсора
delay_ms (25);
}
void lcd_I2C_txt(char *pnt) //Вывод строки символов на дисплей
{
unsigned short int i; //временная переменная индекса масисва символов
char tmp_str; //временный массив символов, длиной на 1 больше длинны строки дисплея, так как строку нужно закончить сиv символом NULL ASCII 0x00
strncpy(tmp_str, pnt, displenth); //копируем в нашу временную строку не более displenth символов исходной строки
for (i=0; i
#include "i2c_lcd.h" //инклудим наш хедер-файл unsigned int i; //временная переменная счетчик void main() { lcd_I2C_Init(); //иннициализируем дисплей lcd_I2C_backlight (1); //включим подсветку lcd_I2C_txt ("Hellow habrahabr"); //выведем на дисплей стрроку while (1) { delay_ms(1000); lcd_I2C_Goto (2,1); //перейдем к 1 символу 2 строки lcd_i2c_int (i); //выведем значение на дисплей i++; // инкриментируем счетчик } }
Если все правильно собрано то мы должны увидеть на индикаторе текст и инкриметирующийся каждую секунду счетчик. В общем, ничего сложного:)
В следующей статье мы продолжем разбиратся с i2c протоколом и устройствами работающем с ним. Рассмотрим работу с EEPROM 24XX памятью и акселерометром/гироскопом MPU6050.
В последнее время все чаще натыкаюсь на негативные отзывы о шине I2C
у STM32
, мол работа с ней это танцы с бубном и тд.
За последний месяц мне удалось запустить две микросхемы, работающие по I2C
и ни каких танцев, только вдумчивое чтение даташита.
Модуль I2C у STM32 обладает следующими особенностями:
- может работать в двух режимах Fm (fast mode) и Sm (standart mode), первый работает на частотах до 400KHz, второй до 100KHz
- буфер размером 1 байт с поддержкой DMA
- поддерживает аппаратный подсчет контрольной суммы
- на его основе возможна реализуется SMBus (System Management Bus) и PMBus (Power Management Bus)
- два вектора прерывания, генерируются при успешной передаче и при возникновении ошибки
- фильтр для борьбы с шумами
- может работать в режиме Master или Slave
- генерирует тактирующий сигнал
- генерирует START и STOP
- можно программировать основной и альтернативный адрес на который он будет отзываться
- определяет STOP
По умолчанию модуль находится в режиме Slave
, но он автоматически переключается в режим Master
после генерации состояния START
.
Принципиальное отличие между Master и Slave, в том, что Master
генерирует тактовый сигнал и всегда инициирует передачу данных и заканчивает её
. Slave
же, откликается на свой адрес и широковещательный
, при чем отклик на широковещательный адрес можно отключить. Также Slave
генерирует состояние
ACK
, но его тоже можно отключить.
Такое подробное разъяснение необходимо потому, что в обоих режимах устройство может выступать как передатчиком, так и приемником.
- Slave transmitter
- Slave receiver
- Master transmitter
- Master receiver
Ниже показана структура модуля I2C.
Регистр управления I2C_CR1:
SWRST (Software reset) - единица в этом бите сбрасывает значение всех регистров модуля в дефолтное стояние, может использоваться для сброса при возникновении ошибки.
ALERT (SMBus alert) - установка единицы в этот бит разрешает генерировать сигнал alert в режиме SMBus .
PEC (Packet error checking) - управление этим битом производится программно, но он может быть сброшен аппаратно когда передается PEC, START, STOP или PE=0. Единица в этом бите разрешает передачу CRC .
POS (Acknowledge/PEC Position (for data reception)) - состояние этого бита определяет положение ACK /PEC в двух байтовой конфигурации в режиме Master.
ACK (Acknowledge enable) - единица в этом бите разрешает отправлять ACK /NACK после приема байта адреса или данных.
STOP (Stop generation) - установка единицы в этот бит генерирует сигнал STOP в режиме Master.
START (Start generation) - установка единицы в этот бит генерирует состояние START в режиме Master,
NOSTRETCH (Clock stretching disable (Slave mode)) - если на обработку данных требуется время Slave может остановить передачу мастера, прижав линию SCL к земле, Master будет ждать и не будет ни чего слать, пока линия не будет отпущена. Ноль в этом бите прижимает SCL к земле.
ENGC (General call enable) - если в этом бите установлена единица, модуль отвечает ACK ом на широковещательный адрес 0х00.
ENPEC (PEC enable) - установка единицы в этот бит включает аппаратный подсчет CRC .
ENARP (ARP enable) - установка единицы в этот бит включает ARP .
SMBTYPE (SMBus type) - если в этом бите установлен ноль модуль работает в режиме Slave, если единица в режиме Master.
SMBUS (SMBus mode) - если в этом бите установлен ноль модуль работает в режиме I2C , если единица SMBus .
PE (Peripheral enable) - единица в этом бите включает модуль.
Регистр управления I2C_CR2:
LAST (DMA last transfer) - единица в этом бите разрешает DMA генерировать сигнал окончания передачи EOT (End of Transfer).
DMAEN (DMA requests enable) - единица в этом бите разрешает делать запрос к DMA при установке флагов TxE или RxNE .
ITBUFEN (Buffer interrupt enable) - если этот бит сброшен, разрешены все прерывания, кроме прерываний по приему и передаче.
ITEVTEN (Event interrupt enable) - единица в этом бите разрешает прерывания по событию.
ITERREN (Error interrupt enable) - единица в этом бите разрешает прерывания при возникновении ошибок.
FREQ (Peripheral clock frequency) - в это битовое битовое поле необходимо записать частоту тактирования модуля, она может принимать значение от 2 до 50.
Регистр I2C_OAR1:
ADDMODE (Addressing mode) - этот бит определяет размер адреса Slave, ноль соответствует размеру адреса 7 бит, единица - 10 бит.
ADD (Interface address) - старшие биты адреса, в случае если адрес 10-битный.
ADD (Interface address) - адрес устройства.
ADD0 (Interface address) - младший бит адреса, в случае если адрес 10-битный..
Регистр I2C_OAR2:
ADD2 - альтернативный адрес на который будет отзываться Slave.
ENDUAL (Dual addressing mode enable) - единица в этом бите разрешает Slave отзываться на альтернативный адрес в 7-битном режиме.
I2C_DR - регистр данных, для отправки данных пишем в регистр DR , для приёма читаем его же.
Регистр статуса I2C_SR1:
SMBALERT (SMBus alert) - возникает в случае alert в шине SMBus .
TIMEOUT (Timeout or Tlow error) - возникает если линия SCL прижата к земле. Для master 10mS, для slave 25mS.
PECERR (PEC Error in reception) - возникает при ошибке PEC при приеме.
OVR (Overrun/Underrun) - возникает при переполнении данных.
AF (Acknowledge failure) - устанавливается при получении сигнала NACK . Для сброса нужно записать 0.
ARLO (Arbitration lost (master mode)) - устанавливается при потере арбитража. Для сброса нужно записать 0.
BERR (Bus error) - ошибка шины. Устанавливается в случае возникновения сигнала START или STOP в неправильный момент.
TxE (Data register empty (transmitters)) - устанавливается при опустошении регистра DR, а точнее когда данные из него были перемещены в сдвиговый регистр.
RxNE (Data register not empty (receivers)) - устанавливается при приеме байта данных, кроме адреса.
STOPF (Stop detection (slave mode)) - при работе в режиме slave устанавливается при обнаружении сигнала STOP , если перед этим был сигнал ACK. Для сброса необходимо прочитать SR1 и произвести запись в CR1 .
ADD10 (10-bit header sent (Master mode)) - устанавливается при отправке первого байта 10-битного адреса.
BTF (Byte transfer finished) - флаг устанавливается по окончании приема/передачи байта, работает только при NOSTRETCH равном нулю.
ADDR (Address sent (master mode)/matched (slave mode)) - в режиме master устанавливается после передачи адреса, в режиме slave устанавливается при совпадении адреса. Для сброса нужно прочитать регистр SR1, а затем SR2.
SB (Start bit (Master mode)) - устанавливается при возникновении сигнала START. Для сброса флага необходимо прочитать SR1 и записать данные в регистр DR .
Регистр статуса I2C_SR2:
PEC (Packet error checking register) - в это битовое поле записывается контрольная сумма кадра.
DUALF (Dual flag (Slave mode)) - ноль в этом бите говорит о том, что адрес который принял Slave соответствует OAR1 , иначе OAR2 .
SMBHOST (SMBus host header (Slave mode)) - устанавливается, когда принят заголовок SMBus Host .
SMBDEFAULT
(SMBus device default address (Slave mode)) - устанавливается, если принят адрес по умолчанию
для SMBus
-устройства.
GENCALL (General call address (Slave mode)) - устанавливается, если принят широковещательный адрес в режиме ведомого.
TRA (Transmitter/receiver) - единица в этом бите говорит о том, что модуль работает как передатчик, иначе приемник.
BUSY (Bus busy) - флаг занятости.
MSL (Master/slave) - единица в этом бите говорит о том, что модуль работает в режиме Master, иначе Slave.
Регистр управления частотой I2C_CCR:
F/S (I2C master mode selection) - при установке единицы в этот бит модуль работает в режиме FAST , иначе STANDART .
DUTY (Fm mode duty cycle) - этот бит задает скважность сигнала SCL в режиме FAST . Если установлен ноль tlow/thigh = 2, иначе tlow/thigh = 16/9.
CCR (Clock control register in Fm/Sm mode (Master mode)) - при работе в режиме Master задает тактовую частоту линии SCL.
Sm mode or SMBus
:
Thigh = CCR * TPCLK1
Tlow = CCR * TPCLK1
Fm mode
:
If DUTY = 0:
Thigh = CCR * TPCLK1
Tlow = 2 * CCR * TPCLK1
If DUTY = 1: (to reach 400 kHz)
Thigh = 9 * CCR * TPCLK1
Tlow = 16 * CCR * TPCLK1
Получаем для режима SM
следующее:
CCR * TPCLK1 + CCR * TPCLK1 = 10 000ns
CCR = 10 000/(2* TPCLK1)
Регистр I2C_TRISE:
TRISE
- определяет время нарастания фронта. Рассчитывается по формуле (Tr max/TPCLK1)+1
,
где Tr max
для SM
составляет 1000nS
, а для FM 300nS
,
а TPCLK1
- период который рассчитывается как 1/F
(APB1).
Регистр управления фильтрами I2C_FLTR:
ANOFF (Analog noise filter OFF) - ноль в этом бите включает аналоговый фильтр.
DNF (Digital noise filter) - битовое поле для настройки цифрового фильтра. За подробностями нужно обратиться к документации.
Инициализация модуля из рабочего проекта.
void I2C2_Init(void)
{
/*
SDL -> PB10
SDA -> PB11
RST -> PE15
*/
//включаем тактирование портов и модуля I2C
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN | RCC_AHB1ENR_GPIOEEN;
RCC->APB1ENR |= RCC_APB1ENR_I2C2EN;
//альтернативная ф-ция, выход с открытым стоком, 2 MHz
GPIOB->AFR |= (0x04<<2*4);
GPIOB->AFR |= (0x04<<3*4);
GPIOB->MODER |= GPIO_MODER_MODER10_1;
GPIOB->OTYPER |= GPIO_OTYPER_OT_10;
GPIOB->OSPEEDR &= ~GPIO_OSPEEDER_OSPEEDR10;
GPIOB->MODER |= GPIO_MODER_MODER11_1;
GPIOB->OTYPER |= GPIO_OTYPER_OT_11;
GPIOB->OSPEEDR &= ~GPIO_OSPEEDER_OSPEEDR11;
//PE15 двухтактный выход 50MHz
GPIOE->MODER |= GPIO_MODER_MODER15_0;
GPIOE->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR15;
AU_RST_HIGH
//настраиваем модуль в режим I2C
I2C2->CR1 &= ~2C_CR1_SMBUS;
//указываем частоту тактирования модуля
I2C2->CR2 &= ~I2C_CR2_FREQ;
I2C2->CR2 |= 42; // Fclk1=168/4=42MHz
//конфигурируем I2C, standart mode, 100 KHz duty cycle 1/2
I2C2->CCR &= ~(I2C_CCR_FS | I2C_CCR_DUTY);
//задаем частоту работы модуля SCL по формуле 10 000nS/(2* TPCLK1)
I2C2->CCR |= 208; //10 000ns/48ns = 208
//Standart_Mode = 1000nS, Fast_Mode = 300nS, 1/42MHz = 24nS
I2C2->TRISE = 42; //(1000nS/24nS)+1
//включаем модуль
I2C2->CR1 |= I2C_CR1_PE;
}
void I2C_Write(uint8_t reg_addr, uint8_t data)
{
//стартуем
I2C2->CR1 |= I2C_CR1_START;
while(!(I2C2->SR1 & I2C_SR1_SB)){};
(void) I2C2->SR1;
//передаем адрес устройства
I2C2->DR = I2C_ADDRESS(ADDR,I2C_MODE_WRITE);
while(!(I2C2->SR1 & I2C_SR1_ADDR)){};
(void) I2C2->SR1;
(void) I2C2->SR2;
//передаем адрес регистра
I2C2->DR = reg_addr;
while(!(I2C2->SR1 & I2C_SR1_TXE)){};
//пишем данные
I2C2->DR = data;
while(!(I2C2->SR1 & I2C_SR1_BTF)){};
I2C2->CR1 |= I2C_CR1_STOP;
}
uint8_t I2C_Read(uint8_t reg_addr)
{
uint8_t data;
//стартуем
I2C2->CR1 |= I2C_CR1_START;
while(!(I2C2->SR1 & I2C_SR1_SB)){};
(void) I2C2->SR1;
//передаем адрес устройства
I2C2->DR = I2C_ADDRESS(ADR,I2C_MODE_WRITE);
while(!(I2C2->SR1 & I2C_SR1_ADDR)){};
(void) I2C2->SR1;
(void) I2C2->SR2;
//передаем адрес регистра
I2C2->DR = reg_addr;
while(!(I2C2->SR1 & I2C_SR1_TXE)){};
I2C2->CR1 |= I2C_CR1_STOP;
//рестарт!!!
I2C2->CR1 |= I2C_CR1_START;
while(!(I2C2->SR1 & I2C_SR1_SB)){};
(void) I2C2->SR1;
//передаем адрес устройства, но теперь для чтения
I2C2->DR = I2C_ADDRESS(ADR,I2C_MODE_READ);
while(!(I2C2->SR1 & I2C_SR1_ADDR)){};
(void) I2C2->SR1;
(void) I2C2->SR2;
//читаем
I2C2->CR1 &= ~I2C_CR1_ACK;
while(!(I2C2->SR1 & I2C_SR1_RXNE)){};
data = I2C2->DR;
I2C2->CR1 |= I2C_CR1_STOP;
return data;
}
Сегодня на нашем операционном столе новый гость, это продукт компании Microchip расширитель портов MCP23008-E. Предназначена эта штуковина (как понятно из названия) для увеличения числа I/O ног микроконтроллера, если их вдруг стало не хватать. Конечно если нам нужные ноги-выходы то можно взять и не париться. Если нужны ноги-входы то и тут есть решение на жесткой логике. Если же нам нужны одновременно входы и выходы да еще и управляемая подтяжка для входов, то расширитель портов это пожалуй самое нормальное решение. Что касаемо цены девайса то она весьма скромная — примерно бакс. В данной статье я попробую детально описать как рулить данной микросхемой при помощи микроконтроллера AVR.
Для начала немного о характеристиках:
- 8 независимых пинов порта
- Интерфейс для связи с внешним миром — I2C (частота до 1.7 МГц)
- Настраиваемая подтяжка для входов
- Может дёрнуть ногой когда состояние определённых входов изменится
- Три входа для задания адреса микросхемы (можно повесить 8 устройств на одну шину)
- Рабочее напряжение от 1.8 до 5.5 вольт
- Малый ток потребления
- Большой выбор корпусов (PDIP/SOIC/SSOP/QFN)
Интерфейс я предпочитаю использовать I2C, он привлекает меня малым количеством проводков:-) однако если нужна очень быстрая скорость (до 10 МГц), то использовать нужно SPI интерфейс который присутствует в MCP23S08. Различие между MCP23S08 и MCP23008 как я понял только в интерфейсе и в количестве ног для задания адреса микросхемы. Кому что по душе короче. Распиновка микрухи есть в даташите, она ни чем не интересна и рассматриваться тут не будет. Поэтому сразу перейдем к тому как начать работать с данным девайсом. Все работа сводится к тому чтоб записывать и считывать определенные данные из регистров микросхемы. Регистров на радость мне оказалось совсем не много — всего лишь одиннадцать штук. Писать и читать данные из регистров очень просто, пост про это . Речь там правда не о регистрах, но принцип тот же. Теперь осталось выяснить из каких регистров что считывать и в какие регистры что записывать. В этом нам разумеется поможет даташит. Из даташита мы узнаем, что у микросхемы есть следующие регистры:
Регистр IODIR
Задаёт направление в котором идут данные. Если бит соответствующий определённой ножке установлен в единицу, то ножка вход. Если сброшен в ноль, то выход. Короче этот регистр аналог DDRx в AVR (только в AVR 1- это выход а 0 — вход).
Регистр IPOL
Если бит регистра установлен,то для соответствующей ноги включена инверсия входа. Это означает, что если на ножку подать лог. ноль то из регистра GPIO считается единица и наоборот. Полезность данной фичи весьма сомнительна.
Регистр GPINTEN
Каждый бит этого регистра соответствует определённому пину порта. Если бит установлен, то соответствующий пин порта настроенный на вход может вызывать прерывание. Если бит сброшен, то что бы не делали с ногой порта - прерывания не будет. Условия возникновения прерывания задаются двумя следующими регистрами.
Регистр DEFVAL
Текущее значение пинов настроенных на вход постоянно сравнивается с этим регистром. Если вдруг текущее значение стало отличаться от того что в этом регистре, то возникает прерывание. Проще говоря - если бит установлен то прерывание будет возникать когда на соответствующей ножке произойдет смена уровня с высокого на низкий. В случае если бит сброшен, то прерывание возникает по нарастающему фронту.
Регистр INTCON
Каждый бит этого регистра соответствует определённому пину порта. Если бит сброшен, то любая смена логического уровня на противоположный вызывает прерывание. В случае если бит установлен, то на возникновение прерывания оказывает влияние регистр DEFVAL (в противоположном случае он вообще игнорируется).
Регистр IOCON
Это регистр настроек. Состоит он из четырёх бит:
SEQOP
— бит управляет автоувеличением адреса. Если он установлен то автоувеличение отключено, в противном случае включено. Если мы выключим его, то можем очень быстро считывать значение одного и того же регистра за счёт того, что нам не придётся передавать его адрес каждый раз. Если же нужно быстро прочитать все 11 регистров по очереди то автоувеличение нужно включить. При каждом считывании байта из регистра адрес будет сам увеличиваться на единицу и передавать его не придётся.
DISSLW
— фиг знает чё за бит. Как его не крутил всё равно работает всё. Буду рад если кто объяснит.
HAEN
— Бит настройки вывода INT. Если он установлен то вывод сконфигурирован как открытый сток, если бит сброшен, то активный уровень на ноге INT определяет бит INTPOL
INTPOL
— определяет активный уровень на ноге INT. Если он установлен то активный уровень единица, в противном случае ноль.
Есть еще бит HAEN но он не используется в данной микросхеме (включает/выключает пины аппаратной адресации в MCP23S08)
Регистр GPPU
Управляет подтяжкой входов. Если бит установлен то на соответствующем ему пине появится подтяжка к питанию через резистор 100 кОм.
Регистр INTF
Регистр флагов прерываний. Если бит установлен то это означает, что соответствующая ему нога порта вызывала прерывание. Разумеется прерывания для нужных ног должны быть включены в регистре GPINTEN
Регистр INTCAP
В момент возникновения прерывания в этот регистр считывается весь порт. Если потом после этого будут еще прерывания, то содержимое этого регистра не затрется новым значением до тех пор пока мы не считаем его или GPIO.
Регистр GPIO
Считывая данные из регистра — считываем логические уровни на ножках порта. Записывая данные — устанавливаем логические уровни. При записи в этот регистр автоматически происходит запись этих же самых данных в регистр OLAT
Регистр OLAT
Записывая данные в этот регистр - выводим данные в порт. Если прочитать оттуда данные, то прочитается то что записали, а не то что что фактически есть на входах порта. Для чтения входов используем только GPIO.
Описание регистров на мой взгляд вполне нормальное и мозг ни кому взорвать не должно. Теперь ради интереса напишем небольшой демонстрационный пример который будет опрашивать 4 кнопки подключенные к расширителю порта, и в зависимости от их состояния зажигать или гасить 4 соответствующих светодиода подключенных к тому же расширителю. Пример будет максимально простым, без всяких прерываний. Просто постоянно считываем состояние кнопок и выводим это состояние на светодиоды. Схема нашего устройства будет выглядеть так:
Вывод прерывания можно не подключать, он тут нам не нужен, а вот подтяжка для ноги сброса обязательна! Я потратил много времени пока понял, что расширитель портов у меня периодически сбрасывался из-за отсутствия подтяжки. Теперь возьмёмся за код. Писать будем конечно же на . Код простецкий:
Program rashiritel; const AdrR=%01000001; //Адрес микросхемы с битом чтение const AdrW=%01000000; //Адрес микросхемы с битом запись var r:byte; ///Процедура записывает данные из переменной Dat в регистр по адресу Adr Procedure WriteReg(Dat,Adr:byte); Begin TWI_Start(); TWI_Write(AdrW); TWI_Write(Adr); TWI_Write(Dat); TWI_Stop(); End; ///Функция возвращает значение регистра по адресу Adr Function ReadReg(Adr:byte):byte; var a:byte; Begin TWI_Start(); TWI_Write(AdrW); TWI_Write(Adr); TWI_Start(); TWI_Write(AdrR); a:=TWI_Read(0); TWI_Stop(); result:=a; End; begin TWI_INIT(200000); ///Инициализация i2c WriteReg(%00001111,0x00); //Младшие 4 бита входы а остальные 4 выходы WriteReg(%00001111,0x06); //Вкл. подтяжку для 4-х входов While TRUE do Begin r:=ReadReg(0x09); //Считали состояние входов r:= NOT r; //Надо инвертировать биты иначе при отпущеной кнопке светодиоды будут гореть r:= r shl 4; //Сдвигаем на 4 бита влево... WriteReg(r,0x0A); //Выводим состояние кнопок end; end.
Кто-то любит пирожки, а кто-то - нет.
Интерфейс i2c широко распространён и используется. В stm32f4 модулей, реализующих данный протокол, аж целых три штуки.
Естественно, с полной поддержкой всего этого дела.
Работа с модулем, в целом, такая же, как и в других контроллерах: даёшь ему команды, он их выполняет и отчитывается о результате:
Я> Шли START.
S> Ок, послал.
Я> Круто, шли адрес теперь. Вот такой: 0xXX.
S> Ок, послал. Мне сказали, что ACK. Давай дальше.
Я> Жив ещё, хорошо. Вот тебе номер регистра: 0xYY, - шли.
S> Послал, получил ACK.
Я> Шли ему теперь данные, вот тебе байт: 0xZZ.
S> Послал, он согласен на большее: ACK.
Я> Фиг ему, а не ещё. Шли STOP.
S> Okay.
И всё примерно в таком духе.
В данном контроллере выводы i2c раскиданы по портам таким образом:
PB6: I2C1_SCL
PB7: I2C1_SDA
PB8: I2C1_SCL
PB9: I2C1_SDA
PB10: I2C2_SCL
PB11: I2C2_SDA
PA8: I2C3_SCL
PC9: I2C3_SDA
Вообще, распиновку периферии удобно смотреть в на 59 странице.
Что удивительно, но для работы с i2c нужны все его регистры, благо их немного:
I2C_CR1
- команды модулю для отправки команд/состояний и выбор режимов работы;
I2C_CR2
- настройка DMA и указание рабочей частоты модуля (2-42 МГц);
I2C_OAR1
- настройка адреса устройства (для slave), размер адреса (7 или 10 бит);
I2C_OAR2
- настройка адреса устройства (если адресов два);
I2C_DR
- регистр данных;
I2C_SR1
- регистр состояния модуля;
I2C_SR2
- регистр статуса (slave, должен читаться, если установлен флаги ADDR или STOPF в SR1);
I2C_CCR
- настройка скорости интерфейса;
I2C_TRISE
- настройка таймингов фронтов.
Впрочем, половина из них типа «записать и забыть».
На плате STM32F4-Discovery уже есть I2C устройство, с коим можно попрактиковаться: CS43L22 , аудиоЦАП. Он подключён к выводам PB6/PB9. Главное, не забыть подать высокий уровень на вывод PD4 (там сидит ~RESET), иначе ЦАП не станет отвечать.
Порядок настройки примерно таков:
1
. Разрешить тактирование портов и самого модуля.
Нам нужны выводы PB6/PB9, потому надо установить бит 1 (GPIOBEN) в регистре RCC_AHB1ENR, чтоб порт завёлся.
И установить бит 21 (I2C1EN) в регистре RCC_APB1ENR, чтоб включить модуль I2C. Для второго и третьего модуля номера битов 22 и 23 соответственно.
2
. Дальше настраиваются выводы: выход Oped Drain (GPIO->OTYPER), режим альтернативной функции (GPIO->MODER), и номер альтренативной функции (GPIO->AFR).
По желанию можно настроить подтяжку (GPIO->PUPDR), если её нет на плате (а подтяжка к питанию обеих линий необходима в любом виде). Номер для I2C всегда один и тот же: 4. Приятно, что для каждого типа периферии заведён отдельный номер.
3
. Указывается текущая частота тактирования периферии Fpclk1 (выраженная в МГц) в регистре CR2. Я так понял, это нужно для расчёта разных таймингов протокола.
Кстати, она должна быть не менее двух для обычного режима и не менее четырёх для быстрого. А если нужна полная скорость в 400 кГц, то она ещё и должна делиться на 10 (10, 20, 30, 40 МГц).
Максимально разрешённая частота тактирования: 42 МГц.
4
. Настраивается скорость интерфейса в регистре CCR, выбирается режим (обычный/быстрый).
Cмысл таков: Tsck = CCR * 2 * Tpckl1, т.е. период SCK пропорционален CCR (для быстрого режима всё несколько хитрее, но в RM расписано).
5
. Настраивается максимальное время нарастания фронта в регистре TRISE. Для стандартного режима это время 1 мкс. В регистр надо записать количество тактов шины, укладывающихся в это время, плюс один:
если такт Tpclk1 длится 125 нс, то записываем (1000 нс / 125 нс) + 1 = 8 + 1 = 9.
6
. По желанию разрешается генерация сигналов прерывания (ошибки, состояние и данных);
7
. Модуль включается: флаг PE в регистре CR1 переводится в 1.
Дальше модуль работает уже как надо. Надо только реализовать правильный порядок команд и проверки результатов. Например, запись регистра:
1
. Сначала нужно отправить START, установив флаг с таким именем в регистре CR1. Если всё ок, то спустя некоторое время выставится флаг SB в регистре SR1.
Хочу заметить один момент, - если нет подтяжки на линии (и они в 0), то этот флаг можно не дождаться вовсе.
2
. Если флаг-таки дождались, то отправляем адрес. Для семибитного адреса просто записываем его в DR прям в таком виде, как он будет на линии (7 бит адреса + бит направления). Для десятибитного более сложный алгоритм.
Если устройство ответит на адрес ACK"ом, то в регистре SR1 появится флаг ADDR. Если нет, то флаг AF (Acknowledge failure).
Если ADDR появился, надо прочитать регистр SR2. Можно ничего там и не смотреть, просто последовательное чтение SR1 и SR2 сбрасывает этот флаг. А пока флаг установлен, SCL удерживается мастером в низком состоянии, что полезно, если надо попросить удалённое устройство подождать с отправкой данных.
Если всё ок, то дальше модуль перейдёт в режим приёма или передачи данных в зависимости от младшего бита отправленного адреса. Для записи он должен быть нулём, для чтения - единицей.
но мы рассматриваем запись, потому примем, что там был ноль.
3
. Дальше отправляем адрес регистра, который нас интересует. Точно так же, записав его в DR. После передачи выставится флаг TXE (буфер передачи пуст) и BTF (передача завершена).
4
. Дальше идут данные, которые можно отправлять, пока устройство отвечает ACK. Если ответом будет NACK, то эти флаги не установятся.
5
. По завершении передачи (или в случае непредвиденного состояния) отправляем STOP: устанавливается одноимённый флаг в регистре CR1.
При чтении всё то же самое. Меняется только после записи адреса регистра.
Вместо записи данных идёт повторная отправка START (повторный старт) и отправка адреса с установленным младшим битом (признак чтения).
Модуль будет ждать данных от устройства. Чтобы поощрать его к отправке следующих байт, надо перед приёмом установить флаг ACK в CR1 (чтобы после приёма модуль посылал этот самый ACK).
Как надоест, флаг снимаем, устройство увидит NACK и замолчит. После чего шлём STOP обычным порядком и радуемся принятым данным.
Вот то же самое в виде кода:
// Инициализация модуля
void i2c_Init(void)
{
uint32_t Clock = 16000000UL; // Частота тактирования модуля (system_stm32f4xx.c не используется)
uint32_t Speed = 100000UL; // 100 кГц
// Включить тактирование порта GPIOB
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN;
// Настроим выводы PB6, PB9
// Open drain!
GPIOB->OTYPER |= GPIO_OTYPER_OT_6 | GPIO_OTYPER_OT_9;
// Подтяжка внешняя, потому тут не настраивается!
// если надо, см. регистр GPIOB->PUPDR
// Номер альтернативной функции
GPIOB->AFR &= ~(0x0FUL << (6 * 4)); // 6 очистим
GPIOB->AFR |= (0x04UL << (6 * 4)); // В 6 запишем 4
GPIOB->AFR &= ~(0x0FUL << ((9 - 8) * 4)); // 9 очистим
GPIOB->AFR |= (0x04UL << ((9 - 8) * 4)); // В 9 запишем 4
// Режим: альтернативная функция
GPIOB->MODER &= ~((0x03UL << (6 * 2)) | (0x03UL << (9 * 2))); // 6, 9 очистим
GPIOB->MODER |= ((0x02UL << (6 * 2)) | (0x02UL << (9 * 2))); // В 6, 9 запишем 2
// Включить тактирование модуля I2C1
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
// На данный момент I2C должен быть выключен
// Сбросим всё (SWRST == 1, сброс)
I2C1->CR1 = I2C_CR1_SWRST;
// PE == 0, это главное
I2C1->CR1 = 0;
// Считаем, что запущены от RC (16 МГц)
// Предделителей в системе тактирования нет (все 1)
// По-хорошему, надо бы вычислять это вс из
// реальной частоты тактирования модуля
I2C1->CR2 = Clock / 1000000UL; // 16 МГц
// Настраиваем частоту
{
// Tclk = (1 / Fperiph);
// Thigh = Tclk * CCR;
// Tlow = Thigh;
// Fi2c = 1 / CCR * 2;
// CCR = Fperiph / (Fi2c * 2);
uint16_t Value = (uint16_t)(Clock / (Speed * 2));
// Минимальное значение: 4
if(Value < 4) Value = 4;
I2C1->CCR = Value;
}
// Задаём предельное время фронта
// В стандартном режиме это время 1000 нс
// Просто прибавляем к частоте, выраженной в МГц единицу (см. RM стр. 604).
I2C1->TRISE = (Clock / 1000000UL) + 1;
// Включим модуль
I2C1->CR1 |= (I2C_CR1_PE);
// Теперь можно что-нибудь делать
}
// Отправить байт
bool i2c_SendByte(uint8_t Address, uint8_t Register, uint8_t Data)
{
if(!i2c_SendStart()) return false;
// Адрес микросхемы
if(!i2c_SendAddress(Address)) return i2c_SendStop();
// Адрес регистра
if(!i2c_SendData(Register)) return i2c_SendStop();
// Данные
if(!i2c_SendData(Data)) return i2c_SendStop();
// Стоп!
i2c_SendStop();
return true;
}
// Получить байт
bool i2c_ReceiveByte(uint8_t Address, uint8_t Register, uint8_t * Data)
{
if(!i2c_SendStart()) return false;
// Адрес микросхемы
if(!i2c_SendAddress(Address)) return i2c_SendStop();
// Адрес регистра
if(!i2c_SendData(Register)) return i2c_SendStop();
// Повторный старт
if(!i2c_SendStart()) return false;
// Адрес микросхемы (чтение)
if(!i2c_SendAddress(Address | 1)) return i2c_SendStop();
// Получим байт
if(!i2c_ReceiveData(Data)) return i2c_SendStop();
// Стоп!
i2c_SendStop();
return true;
}
Использование:
{
uint8_t ID = 0;
i2c_Init();
// Считаем, что PD4 выставлен в высокий уровень и ЦАП работает (это надо сделать как-нибудь)
// Отправка байта в устройство с адресом 0x94, в регистр 0x00 со значением 0x00.
i2c_SendByte(0x94, 0x00, 0x00);
// Приём байта из устройства с адресом 0x94 из регистра 0x01 (ID) в переменную buffer
i2c_ReceiveByte(0x94, 0x01, &ID);
}
Конечно, кроме как в учебном примере так делать нельзя. Ожидание окончания действия слишком уж долгое для такого быстрого контроллера.
(Руководство разработчика по микроконтроллерам семейства HCS08)
Для управления модулем I2C используются 6 регистров специальных функций:
- IICC — первый регистр управления модуля I2C;
- IICC2 — второй регистр управления модуля I2C;
- IICS — регистр состояния модуля I2C;
- IICF — регистр скорости обмена модуля I2C;
- IICA — регистр адреса модуля I2C;
- IICD — регистр данных модуля I2C.
В МК серии QE присутствуют 2 модуля I2C и, соответственно, по два регистра управления каждого типа. Например, первый регистр состояния IIC1S и второй регистр состояния IIC2S.
11.2.8.1. Регистр управления IICC
Для МК серий AC. AW, Dx, EL, GB, GT, JM, LC, QE. QG. SG, SH, SL
Регистр | Режим | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
IICC | Чтение | IICEN | IICIE | MST | TX | TXAK | 0 | 0 | 0 |
Запись | RSTA | — | — | ||||||
Сброс | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Описание битов:
Имя бита | Описание | Символ в языке С |
IICEN | Бит разрешения работы модуля I2C: 0 — контроллер I2C выключен; 1 — контроллер I2C включен. |
bIICEN |
IICIE | Бит разрешения прерывания модуля от I2C: 0 — прерывания по запросу I2C запрещены; 1 — прерывания по запросу I2C разрешены. |
bHCIE |
MST | Бит выбора режима работы контроллера I2C: 0 — контроллер I2C работает в режиме ведомого (Slave); 1 — контроллер I2C работает в режиме ведущего (Master). Когда этот бит изменяется с 0 на 1, генерируется состояние Старт. Наоборот, когда бит изменяется с 1 на 0, генерируется состояние Стоп. |
bMST |
TX | Бит выбора направления передачи по линии данных SDA: 0 — линия работает на ввод; 1 — линия работает на вывод. |
bTX |
TXAK | Бит подтверждения в режиме приема: 0 — генерируется бит подтверждения после приема байта; 1 — генерируется бит неподтверждения приема байта. Этот бит управляет генерацией бита подтверждения после приема байта данных, независимо от того, ведомый это или ведущий. |
bTXAK |
RSTA | Если модуль I2C работает в режиме ведущего, то запись 1 в этот бит вызывает повторную генерацию состояния Старт — «Повторный старт». | bRSTA |
11.2.8.2. Регистр состояния IICS
Для МК серий AC, AW, Dx, EL, GB, GT, JM, LC, QE, QG, SG, SH, SL
Регистр | Режим | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
IICS | Чтение | TCF | IAAS | BUSY | ARBL | 0 | SRW | IICIF | RXAK |
Запись | — | — | — | — | — | ||||
Сброс | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Описание битов:
Имя бита | Описание | Символ в языке С |
TCF | Бит завершения обмена. Устанавливается после завершения обмена одним байтом: 0 — обмен не завершен; 1 — обмен завершен. Флаг TCF сбрасывается в 0, когда читают регистр данных IICD (в режиме приема) или когда записывают в регистр данных IICD (в режиме передачи). |
bTCF |
IAAS | Флаг адреса ведомого. Устанавливается, если устройство работает в режиме ведомого и переданный в сообщении ведущего адрес равен адресу ведомого, который хранится в регистре адреса IICA. Флаг сбрасывается при записи в регистр IICC. |
bIAAS |
BUSY | Флаг занятой линии. Этот флаг устанавливается, если модуль I2C распознал старт-бит на линии. Флаг сбрасывается, когда модуль распознает стоп-бит на линии. 0 — шина I2C свободна; 1 — шина I2C занята. |
bBUSY |
ARBL | Флаг потери арбитража: 0 — нет нарушений в работе шины I2C; 1 — есть потеря арбитража. Модуль I2C должен некоторое время подождать, а потом начать операцию передачи снова. |
bARBL |
SRW | Бит направления передачи ведомого. Этот бит показывает состояние бита R/W в поле адреса: 0 — ведомый принимает. Ведущий передает ведомому; 1 — ведомый передает. Ведущий принимает от ведомого. |
bSRW |
IICIF | Флаг необслуженных запросов на прерывание модуля I2C.Устанавливается в 1, если установлен один из флагов: TCF, IAAS или ARBL. 0 — нет необслуженных прерываний; 1 — есть необслуженные прерывания. Флаг сбрасывается посредством записи в него 1. |
bIICIF |
RXAK | Бит подтверждения приема ведущего: 0 — ведомый подтвердил прием данных; 1 — ведомый не подтвердил прием данных. Этот бит отражает состояние поля ASK на временной диаграмме обмена. |
bRXAK |
11.2.8.3. Регистр адреса IICA
Регистр | Режим | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
IICA | Чтение | ADDR | |||||||
Запись | |||||||||
Сброс | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
В этом регистре хранится 7-битный адрес ведомого устройства, который разработчик присвоил данному устройству при разработке системы. Этот адрес автоматически сравнивается с кодом адреса, который ведомый получил в поле адреса по шине I2C. Если адреса совпали, то устанавливается бит IAAS в регистре состояния IICS.
11.2.8.4. Регистр скорости обмена IICF
Для МК серий AC, AW, Dx, EL,GB, GT, JM, LC, QE, QG, SG, SH, SL
Регистр | Режим | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
IICF | Чтение | MULT | ICR | ||||||
Запись | |||||||||
Сброс | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Описание битов:
Значения коэффициентов SCL_DIV и SDA_HV
ISR | SCL_DIV | SDA_HV | ISR | SCL_DIV | SDA_HV | |
0x00 | 20 | 7 | 0x20 | 160 | 17 | |
0x01 | 22 | 7 | 0x21 | 192 | 17 | |
0x02 | 24 | 8 | 0x22 | 224 | 33 | |
0x03 | 26 | 8 | 0x23 | 256 | 33 | |
0x04 | 28 | 9 | 0x24 | 288 | 49 | |
0x05 | 30 | 9 | 0x25 | 320 | 49 | |
0x06 | 34 | 10 | 0x26 | 384 | 65 | |
0x07 | 40 | 10 | 0x27 | 480 | 65 | |
0x08 | 28 | 7 | 0x28 | 320 | 33 | |
0x09 | 32 | 7 | 0x29 | 384 | 33 | |
0x0A | 36 | 9 | 0x2A | 448 | 65 | |
0x0B | 40 | 9 | 0x2B | 512 | 65 | |
0x0C | 44 | 11 | 0x2C | 576 | 97 | |
0x0D | 48 | 11 | 0x2D | 640 | 97 | |
0x0E | 56 | 13 | 0x2E | 768 | 129 | |
0x0F | 68 | 13 | 0x2F | 960 | 129 | |
0x10 | 48 | 9 | 0x30 | 640 | 65 | |
0x11 | 56 | 9 | 0x31 | 768 | 65 | |
0x12 | 64 | 13 | 0x32 | 896 | 129 | |
0x13 | 72 | 13 | 0x33 | 1024 | 129 | |
0x14 | 80 | 17 | 0x34 | 1152 | 193 | |
0x15 | 88 | 17 | 0x35 | 1280 | 193 | |
0x16 | 104 | 21 | 0x36 | 1536 | 257 | |
0x17 | 128 | 21 | 0x37 | 1920 | 257 | |
0x18 | 80 | 9 | 0x38 | 1280 | 129 | |
0x19 | 96 | 9 | 0x39 | 1536 | 129 | |
0x1A | 112 | 17 | 0x3A | 1792 | 257 | |
0x1B | 128 | 17 | 0x3B | 2048 | 257 | |
0x1C | 144 | 25 | 0x3C | 2304 | 385 | |
0x1D | 160 | 25 | 0x3D | 2560 | 385 | |
0x1E | 192 | 33 | 0x3E | 3072 | 513 | |
0x1F | 240 | 33 | 0x3F | 3840 | 513 |
В этом регистре хранятся два битовых поля, которые определяют скорость и параметры временной диаграммы обмена по I2C. Частота сигналов синхронизации определяется по формуле:
Время установления данных SDA_hold_time на шине I2C — это временной интервал между моментом установления в 0 сигнала SCL и изменением данных на линии SDA. Назначается параметром SDA_HV (SDA_Hold_Value) из таблицы для фактора ICR регистра скорости обмена:
.
11.2.8.5. Регистр данных IICD
Для МК серий AC, AW, Dx, EL,GB, GT, JM, LC, QE, QG, SG, SH, SL
Регистр | Режим | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
IICD | Чтение | I2C DATA | |||||||
Запись | |||||||||
Сброс | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Если модуль I2C работает в режиме ведущего, то операция записи в этот регистр инициирует обмен по I2C (но только если бит направления обмена в регистре управления IICC выставлен правильно, т.е. TX = 1). Первый байт после состояния Старт, который программа записывает в регистр данных, интерпретируется ведомыми как адрес устройства. Поэтому программа должна правильно сформировать содержимое первого байта. Операция чтения регистра возвращает последний принятый байт по I2C. Операция чтения регистра также инициирует начало приема следующего байта, но только если бит направления обмена в регистре управления IICC выставлен правильно, т.е. при TX = 0! При TX = 1 операция чтения регистра не вызовет прием нового байта по I2C от ведомого.
Если модуль I2C работает в режиме ведомого, то записанные в этот регистр данные будут переданы на линию SDA шины I2C тогда, когда ведущее устройство будет выполнять цикл приема от этого ведомого. Операция чтения регистра возвращает последний принятый байт от ведущего.
11.2.8.6. Регистр управления IICC2
Для МК серий AC, Dx, EL, JM, QE, SG, SH, SL
Регистр | Режим | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
IICC | Чтение | GCAEN | ADEXT | 0 | 0 | 0 | AD10 | AD9 | AD8 |
Запись | — | — | — | ||||||
Сброс | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |