Kommunikation über I2C mit MCP32017
So schön die Prozessorfamilie von ST rund um die STM3-Bit Prozessorfamile auch ist, es gibt ein paar Dinge die sind wirklich schrecklich. Eins davon ist die I2C-Kommunikation. Sofern man nicht ganz(!) genau die diversen States und Flags beachtet geht eigentlich garnichts - nicht mal im Ansatz.
Hintergrund: Als Grundlage für meine geplante Alarmanlage benötige ich noch die Erweiterung von IOs mit sogenannten Port-Extendern.
An anderer Stelle habe ich geschrieben wie man die I2C-Kommunikation mit dem Beagle-Bone oder dem Raspberry Pi hinbekommt. Hier folgen jetzt die Beispiele für den STM32F103x Prozessor.
MCP23017 is ein Portextender von Microchip der meiner Meinung einige ganz gute Eigenschaften hat. Eine davon ist die Möglichkeit einen Interruptausgang anzusteuern wenn sich Port-Bits ändern. So muss man nicht ständig den Zustand pollen. Aber dazu später mehr.
Wie funktioniert die I2C Kommunikation ? Die Grundlagen findet man bei Wikipedia ausreichend, das muss man nicht näher erklären. Aber wie geht das mit einem echten I2C-Busteilnehmer ?
Anschluss des Microchip Expander MCP23017 an den STM32F103RB
Im obigen Schaltbild sind die I2C-Leitungen SCL und SDA direkt mit den passenden Eingängen des Portexpander verbunden. Die STM32-Chips besitzen interne Pullup-Widerstände, sodass die externen I2C-Widerstände (2*3.3k) entfallen können. Der STM32 agiert in den Beispielen als I2C-Master.
Die LEDs und die Taster sind nur zu Test- und Demozwecken vorgesehen. Ich habe den I2C2-Port verwendet, da ich den UART2 bereits verwendet habe und daher der I2C1 nicht gleichzeitig benutzt werden kann.
I2C-Kommunikation benötigt im Prinzip nur die Funktionen I2C-Write und I2C-Read, größere Datenmengen könnte man auch per DMA transferieren.
Hier ist jetzt die grundsätzliche Ansprache des MC23017 zum setzen von Registern:
1. Initialisierung des I2C2-Subsystems im STM32
InitI2C2();
2. I2C-Write Sequenz:
a) Sende Startsequenz
I2C_GenerateSTART
b) sende Adressbyte des MCP-Chips mit RW-Flag=Write
I2C_Send7bitAddres
c) sende Registeradresse des MCP23017 welche beschrieben werden soll
I2C_SendData(I2C2, reg);
d) sende Registerwert
I2C_SendData(I2C2, byte);
e) Sende I2C-Stopsequenz
I2C_GenerateSTOP
Nachfolgend sieht man wie dies im Logikdiagramm aussieht das Register (OLATA=14) wird mit dem Wert 0xFF beschrieben (alle Signale von PortA sind Ausgänge):
Man muss darauf achten, dass man auch ein Acknowledge-Signal vom Expanderchip bekommt. Wenn kein Acknowledge-Bit (im Bild das Symbol A) gesetzt wird stimmt die I2C-Adresse nicht. Dies kann durchaus sein, wenn man vergisst, dass die I2C-Adressen immer 7-Bit-Adressen sind, und diese ein Bit nach links geschoben werden um Platz für das R/W-Flag zu lassen. Die Basisadresse des MCP23017 ist 0x20, d.h. wenn alle Addressbits A0..A2 auf 0 gesetzt werden, analog zu folgendem Adressschema.
Folgende MCP23017-Register sind zu programmieren
Port-A als Ausgang:
IODIRA = 0x00 : Port A alles auf Ausgang
OLATA = 0x00 : Alle Ausgänge auf 0 entsprechend 0xFF alle Ausgänge auf 1
Port B als Eingang:
IODIRB = 0xFF : Port B alles auf Eingang
GPPUB = 0xFF : Port B enable internal Pullups
Um Taster einzulesen liest man einfach das entsprechende Register des jeweiligen Ports z.B. GPPUB.
1. Initialisierung des I2C2-Subsystems im STM32
InitI2C2();
2. I2C-Write Sequenz:
a) Sende Startsequenz
I2C_GenerateSTART
b) sende Adressbyte des MCP-Chips mit RW-Flag=Write
I2C_Send7bitAddres
c) sende Registeradresse des MCP23017 welche beschrieben werden soll
I2C_SendData(I2C2, reg);
d) sende Registerwert
I2C_GenerateSTART (start repeat)
I2C_Send7bitAddres
c) sende Registeradresse des MCP23017 welche gelesen werden soll
I2C_SendData(I2C2, byte);
I2C_ReceiveData(I2C2);
e) Sende I2C-Stopsequenz
I2C_GenerateSTOP
Um Änderungen abzufragen gibts drei Möglichkeiten:
1. Register GPPUB pollen und damit zyklisch abfragen und vergleichen ob sich was geändert hat.
2. Interrupt Ausgang des MCP23017 so programmieren, dass dieses Ausgangssignal gesetzt wird wenn sich einer der Porteingänge geändert hat. Dieser Interruptausgang kann man dann mit einem Interrupteingang des STM32 verbinden und darauf reagieren.
Nachfolgend ist der Code zum anschauen bzw. zum direkten herunterladen.
// Wolframs I2C-Lib for STM32 CPUs #include "stm32f10x_conf.h" #include "stm32_i2c.h" void InitI2C2() { /* Initialize I2C on I2C2 port of STM32F103RB Wolframs */ GPIO_InitTypeDef GPIO_InitStructure; I2C_InitTypeDef I2C_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE); //GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;not necessary for I2C2 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; GPIO_Init(GPIOB, &GPIO_InitStructure); RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C2, ENABLE); I2C_DeInit(I2C2); I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; I2C_InitStructure.I2C_OwnAddress1 = 0x12; //own address only in slave mode I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; I2C_InitStructure.I2C_Ack = I2C_Ack_Disable; //I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; // I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; //Adress mode 7 bits I2C_InitStructure.I2C_ClockSpeed = 100000; //clock rate scl 50kHz I2C_ITConfig(I2C2, I2C_IT_ERR, ENABLE); I2C_Init(I2C2, &I2C_InitStructure); // enable the various interrupt events NVIC_InitTypeDef NVIC_InitStructure; // Configure the I2C Interrupts for Errors on I2C Bus NVIC_InitStructure.NVIC_IRQChannel = I2C2_ER_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); I2C_ITConfig(I2C2, I2C_IT_ERR, ENABLE); // Enable the interrupt in I2C Subsystem I2C_ITConfig(I2C2, I2C_IT_EVT, ENABLE); I2C_ITConfig(I2C2, I2C_IT_BUF, ENABLE); I2C_Cmd(I2C2, ENABLE); // finally enable the I2C Port I2C_StretchClockCmd(I2C2, ENABLE); } void InitI2C1() { /* Initialize I2C on I2C1 port of STM32F103RB Wolframs */ GPIO_InitTypeDef GPIO_InitStructure; I2C_InitTypeDef I2C_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; GPIO_Init(GPIOB, &GPIO_InitStructure); RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1, ENABLE); I2C_DeInit(I2C1); // define i2c settings I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; I2C_InitStructure.I2C_OwnAddress1 = 0x12; //own address I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; //I2C_InitStructure.I2C_Ack = I2C_Ack_Disable; I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; //Adress mode 7 bits I2C_InitStructure.I2C_ClockSpeed = 30000; //clock rate I2C_Init(I2C1, &I2C_InitStructure); I2C_Cmd(I2C1, ENABLE); } // // Wolframs read_i2c routine // uint8_t read_i2c(uint8_t i2c_address,uint8_t reg){ uint8_t rec_byte; // shift address one bit left (7 bit addressing plus rw-flag) I2C_GenerateSTART (I2C2,ENABLE); while(!I2C_CheckEvent (I2C2, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C2, i2c_address << 1, I2C_Direction_Transmitter); while(!I2C_CheckEvent (I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); I2C_SendData(I2C2, reg); while(!I2C_GetFlagStatus(I2C2,I2C_FLAG_BTF)); I2C_GenerateSTART (I2C2,ENABLE); // Start repeat within active I2C communication while(!I2C_GetFlagStatus(I2C2, I2C_FLAG_SB)); I2C_Send7bitAddress(I2C2, i2c_address << 1, I2C_Direction_Receiver); while(!I2C_CheckEvent (I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); //I2C_AcknowledgeConfig(I2C2, DISABLE); while (!I2C_CheckEvent (I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED)); rec_byte = I2C_ReceiveData(I2C2); I2C_GenerateSTOP(I2C2, ENABLE); return rec_byte; } // Wolframs write I2C void write_i2c(uint8_t i2c_address,uint8_t reg,uint8_t byte){ I2C_GenerateSTART (I2C2,ENABLE); while(!I2C_GetFlagStatus(I2C2, I2C_FLAG_SB)); // shift address one bit left (7 bit addressing plus rw flag) I2C_Send7bitAddress(I2C2, i2c_address<< 1, I2C_Direction_Transmitter); while(!I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); I2C_SendData(I2C2, reg); // address target register while(!I2C_GetFlagStatus(I2C2,I2C_FLAG_TXE)); I2C_SendData(I2C2, byte); // write byte value into the addressed register while(!I2C_GetFlagStatus(I2C2,I2C_FLAG_TXE)); I2C_GenerateSTOP(I2C2, ENABLE); // stop I2C transission and release bus while(!I2C_GetFlagStatus(I2C2, I2C_FLAG_BTF)); }
Und hier noch das Beispielprogramm:
#include "stm32f10x_conf.h" // STM32 includes #include "stm32_i2c.h" // Wolframs i2c Lib for STM32 CPUs #include "mcp23017.h" // Wolfram MCP23017 Constants #include "stdio.h" #include "core_cm3.h" /* #include "stm32f10x_gpio.h" #include "stm32f10x_usart.h" #include "stm32f10x_i2c.h" */ /* Test program to communicate from STM32F103RB via I2C with MCP 23017 E/SP Port expander * Following connections to the MCP chip are necessary * The STM32 chip I2C interface 2 is used to avoid conflicts with CAN or UART IOs * Connections * MCP STM32 * Pin 1..8 and Pin 21..28 GPIOs * Pin 9 +Ub * Pin 10 GND * Pin 12 SCL PB10 D29 Pin5 on Ext green * Pin 13 SDA PB11 D30 Pin6 on Ext orange * Pin 18 Reset +Ub * Pin 15 A0 GND * Pin 16 A1 GND * Pin 17 A2 +UB * Address set to 24h (value is 2x ) * Program PCP to output: set DDRA 00 FF * to set all IOA to outputs * * (c) Wolfram Koerver 5.7.2015 * */ // Function prototypes void RCC_Init(void); void USART2_SendString(char* data); void delay_us(uint32_t nus); void delay_ms(uint32_t time_ms); void Int2Str(char *pStr, unsigned int value, int charCount); void InitUart2(void); void Init_Led(void); void blink_yellow(void); void blink_green(void); void init_extint1(void); int main(void) { uint8_t in; char out_text[50]; out_text[49] = '\0'; // termination SystemInit(); RCC_Init(); Init_Led(); InitUart2(); InitI2C2(); blink_yellow(); //init_extint1(); // in case you want to use the interrupt outputs of MCP while(USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET); USART2_SendString("Wolframs Test Program for I2C-Extender MCP23017 \n\r"); blink_green(); write_i2c(0x20,IODIRA,0x00); write_i2c(0x20,GPINTENB,0xFF); // enable pin change interrupt on port b write_i2c(0x20,IODIRB,0xFF); // set port b as input write_i2c(0x20,GPPUB,0xFF); // enable pull ups on port b in=read_i2c(0x20,MGPIOB); // read once the input bits while(1) { write_i2c(0x20,OLATA,0x00); // Set all PortA Bits to 0 delay_ms(200); write_i2c(0x20,OLATA,0xFF); // Set all PortA Bits to 1 delay_ms(200); in=read_i2c(0x20,MGPIOB); // Read Input PORT B delay_ms(50); sprintf(out_text,"In portb: %3d \r",in); USART2_SendString(out_text); } } void delay_us(uint32_t time_us) { // wks version SysTick->LOAD = 8 * time_us-1; SysTick->VAL = 0; /* Loadthe SysTick Counter Value */ SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; /* Enable SysTick Timer */ do { } while ((SysTick->CTRL & SysTick_CTRL_COUNTFLAG)==0); SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; /*Disable SysTick Timer */ SysTick->VAL = 0; /* Load the SysTick Counter Value */ } void delay_ms(uint32_t time_ms) //Delay in msec { while (time_ms>0) { delay_us(1000); time_ms--; } } void Int2Str(char *pStr, unsigned int value, int charCount) { // this implements sprintf(strVal, "%d", temp); faster // note that this is just for values >= 0, while sprintf covers negative values. // this also does not check if the pStr pointer points to a valid allocated space. // caller should make sure it is allocated. // point to the end of the array pStr = pStr + (charCount - 1); // convert the value to string starting from the ones, then tens, then hundreds etc... do { *pStr-- = (value % 10) + '0'; value /= 10; } while(charCount--); } void Init_Led(void) { // the following statements are for the LEDs of the Olimexino STM32 board /* Initialize GPIO Structure, configure clock on peripheral bus * Enable GPIO Pins for LED which are connected to PA1,5*/ /* Configure the GPIO_LED pin PC6 = Green LED PC7 = Yellow LED * Configure direction and clock speed*/ GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1|GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); } void blink_yellow() { GPIOA->BRR = GPIO_Pin_1; //reset bit GPIOA->BSRR = GPIO_Pin_1; //set bit delay_us(10000); GPIOA->BRR = GPIO_Pin_1; //reset bit } void blink_green() { GPIOA->BRR = GPIO_Pin_5; //reset bit GPIOA->BSRR = GPIO_Pin_5; //set bit delay_us(1000); GPIOA->BRR = GPIO_Pin_5; //reset bit } void RCC_Init(void) { // Route and enable clocks for low and high speed bus RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO | RCC_APB2Periph_GPIOD, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); } void InitUart2(void) { // Initialize Structs GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; USART_ClockInitTypeDef USART_ClockInitStructure; // USART1 RX-Pin initialize PA3 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; GPIO_Init(GPIOA, &GPIO_InitStructure); // USART1 TX-Pin initialize PA2 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; GPIO_Init(GPIOA, &GPIO_InitStructure); // USART initialize USART_ClockStructInit(&USART_ClockInitStructure); USART_ClockInit(USART2, &USART_ClockInitStructure); USART_InitStructure.USART_BaudRate = 38400; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_Init(USART2, &USART_InitStructure); USART_Cmd(USART2, ENABLE); } void USART2_SendString(char* data) { while (*data) { USART_SendData(USART2, (uint16_t) *data); while(USART_GetFlagStatus(USART2, USART_FLAG_TC)==RESET); data++; } } void init_extint1(void) { // configure ext interrupt function on pin PB0 // for MCP23017 interrupt outputs connected to. GPIO_InitTypeDef GPIO_InitStructure; EXTI_InitTypeDef EXTI_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0); EXTI_InitStructure.EXTI_Line = EXTI_Line0; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling; //EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x0F; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x0F; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); } void EXTI0_IRQHandler(void){ // Interrupt on PB0 USART2_SendString("Pin Change Interrupt \n\r"); EXTI_ClearITPendingBit(EXTI_Line0); } void i2c_handleErrorInterrupt(void){ I2C_GenerateSTOP(I2C2, ENABLE); I2C_ClearFlag(I2C2, I2C_FLAG_AF); I2C_ClearFlag(I2C2, I2C_FLAG_ARLO); I2C_ClearFlag(I2C2, I2C_FLAG_BERR); USART2_SendString("I2C Error Interrupt Handler \n\r"); } void I2C2_EV_IRQHandler(void){ while(USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET); //USART2_SendString("Event Interrupt Handler \n\r"); } void I2C2_ER_IRQHandler(void){ I2C_InitTypeDef I2C_InitStructure; // This handler is called when there is any error on the I2C Bus; Must be enabled by NVIC I2C_GenerateSTOP(I2C2, ENABLE); while(USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET); USART2_SendString("ER Interrupt Handler \n\r"); I2C_ClearFlag(I2C2, I2C_FLAG_BERR); I2C_ClearFlag(I2C2, I2C_FLAG_SMBALERT); I2C_ClearFlag(I2C2, I2C_FLAG_TIMEOUT); I2C_ClearFlag(I2C2, I2C_FLAG_PECERR); I2C_ClearFlag(I2C2, I2C_FLAG_OVR); I2C_ClearFlag(I2C2, I2C_FLAG_AF); delay_ms(500); I2C_DeInit(I2C2); //NVIC_SystemReset(); // This function works very well in case there is a I2C-Bus error. BUT it does also reset the complete CPU }