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

MCP23017 Portexpander with STM32F103

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
}