ISIX-RTOS – przykład 3 – obsługa przerwań na przykładzie zegara RTC z magistralą I2C
Zadeklarowanie przyjaźni funkcji umożliwia wywoływanie dowolnych metod chronionych klasy z funkcji zaprzyjaźnionej, co zostało wykorzystane do wywołania metody isr() stanowiącej wektor obsługi przerwania. Do synchronizacji wątku z procedurą obsługi przerwania wykorzystano semafor sem_irq, natomiast semafor sem_lock jest wykorzystywany do blokowania sterownika w przypadku, gdy jest on zajęty jest obsługą magistrali I2C.
Konstruktor klasy obiektu i2c_host przyjmuje dwa argumenty: adres wybranego kontrolera I2C (np. I2C1, I2C2) oraz szybkość taktowania magistrali I2C wyrażoną w hercach, której domyślna wartość wynosi 100000. Zadaniem konstruktora jest inicjalizacja kontrolera sprzętowego I2C, uruchomienie przerwań oraz przypisanie wskaźnika this utworzonego obiektu do wskaźnika wykorzystywanego przez procedurę obsługi przerwania.
List. 3.
// TODO Add configuration for i2c2 device support if(_i2c==I2C1) { //GPIO configuration RCC->APB2ENR |= I2C1_GPIO_ENR; io_config( I2C1_PORT, I2C1_SDA_PIN, GPIO_MODE_50MHZ, GPIO_CNF_ALT_OD); io_config( I2C1_PORT, I2C1_SCL_PIN, GPIO_MODE_50MHZ, GPIO_CNF_ALT_OD); io_set( I2C1_PORT, I2C1_SCL_PIN); io_set( I2C1_PORT, I2C1_SDA_PIN); //I2C module configuration RCC->APB1ENR |= I2C1_ENR; } /* Enable I2C module*/ i2c->CR1 |= CR1_PE_SET; /* Reset the i2c device */ i2c->CR1 |= CR1_SWRST; nop(); i2c->CR1 &= ~CR1_SWRST; uint16_t tmpreg = i2c->CR2; /* Clear frequency FREQ[5:0] bits */ tmpreg &= CR2_FREQ_RESET; tmpreg |= static_cast( config::PCLK1_HZ/1000000); i2c->CR2 = tmpreg; //Set speed set_speed(clk_speed); /* CR1 configuration */ /* Get the I2Cx CR1 value */ tmpreg = i2c->CR1; /* Clear ACK, SMBTYPE and SMBUS bits */ tmpreg &= CR1_CLEAR_MASK; /* Configure I2Cx: mode and acknowledgement */ /* Set SMBTYPE and SMBUS bits according to I2C_Mode value */ /* Set ACK bit according to I2C_Ack value */ tmpreg |= I2C_MODE_I2C | I2C_ACK_ENABLE; /* Write to I2Cx CR1 */ i2c->CR1 = tmpreg; /* Set I2Cx Own Address1 and acknowledged address */ i2c->OAR1 = I2C_AcknowledgedAddress_7bit; i2c->SR1 = 0; i2c->SR2 = 0; /* Enable I2C interrupt */ devirq_on(); //Assign as the global object if(_i2c==I2C1) { i2c1_obj = this; } /* Enable interrupt controller */ nvic_set_priority( I2C1_EV_IRQn, IRQ_PRIO, IRQ_SUB); nvic_irq_enable( I2C1_EV_IRQn, true); }
Na początku sprawdzany jest numer kontrolera I2C (obecnie zaimplementowana jest tylko obsługa kontrolera I2C1), następnie konfigurowane są porty GPIO, tak aby pełniły funkcję wyjścia układu peryferyjnego. Następnie konfigurowany jest sprzętowy kontroler I2C, tak aby pełnił rolę sterownika master z 7-bitowym adresowaniem. Do wskaźnika na obiekt wykorzystanego przez procedurę obsługi przerwania przypisywany jest wskaźnik this aktualnie tworzonego obiektu. Przyjęto założenie że istnieje tylko jedna instancja klasy i2c_host, ponieważ do danego kontrolera magistrali może być przypisany tylko jeden sterownik. Klasę można rozbudować w taki sposób, aby w przypadku próby utworzenia kolejnej instancji klasy rzucany był wyjątek. Na zakończenie włączane są przerwania w kontrolerze I2C oraz konfigurowane są przerwania w kontrolerze NVIC.
Jedyną metodą interfejsu użytkownika klasy sterownika I2C jest metoda i2c_transfer_7bit umożliwiająca przeprowadzenie transakcji I2C na magistrali.
List. 4.
int i2c_host::i2c_transfer_7bit(uint8_t addr, const void* wbuffer, short wsize, void* rbuffer, short rsize) { int ret; if( (ret=sem_lock.wait( isix::ISIX_TIME_INFINITE))<0 ) { return ret; } //Disable I2C irq devirq_on(false); if(wbuffer) { bus_addr = addr & ~I2C_BUS_RW_BIT; } else if(rbuffer) { bus_addr = addr | I2C_BUS_RW_BIT; } tx_buf = static_cast(wbuffer); rx_buf = static_cast (rbuffer); tx_bytes = wsize; rx_bytes = rsize; buf_pos = 0; //ACK config ack_on(true); //Enable I2C irq devirq_on(); //Send the start generate_start(); //Sem read lock if( (ret=sem_irq.wait( TRANSFER_TIMEOUT)) <0 ) { if(ret==isix::ISIX_ETIMEOUT) { sem_irq.signal(); sem_lock.signal(); return ERR_TIMEOUT; } else { sem_irq.signal(); sem_lock.signal(); return ret; } } if( (ret=get_hwerror()) ) { err_flag = 0; sem_lock.signal(); return ret; } sem_lock.signal(); return ERR_OK; }
Jako parametry przyjmuje ona kolejno adres sprzętowy układu I2C, z którym chcemy przeprowadzić transakcję, wskaźnik do bufora z danymi oraz długość bufora, z którego dane chcemy przesłać magistralą I2C. Następnie przekazujemy wskaźnik do bufora gdzie będą przesłane dane odebrane z magistrali I2C oraz liczba danych jaką chcemy odczytać. Jeśli chcemy wykonać tylko pojedynczy zapis danych lub odczyt, do nieużywanego wskaźnika na bufor należy przypisać wartość NULL. Metoda blokuje wykonanie bieżącego wątku do chwili zakończenia transakcji I2C oraz w przypadku powodzenia zwraca wartość ERR_OK. Na początku funkcji wywoływana jest metoda wait() na głównym semaforze blokującym, w wyniku czego proces może zostać zablokowany, jeżeli sterownik jest zajęty przez inny proces. Do zmiennych klasy przypisywane są wskaźniki do buforów oraz wielkości tych buforów oraz jest generowana sekwencja start. Następnie oczekujemy na semaforze sygnalizującym zakończenie pracy przez procedurę obsługi przerwania. Cała obsługa cyklu magistrali wykonywana jest w przerwaniu. Oczekiwanie na semafor kończy się w momencie sygnalizacji przez procedurę obsługi przerwania lub w wyniku przekroczenia czasu oczekiwania, co informuje nas o wystąpieniu błędu. W przypadku powodzenia zwracany jest status ERR_OK oraz podnoszony jest semafor blokujący sem_lock za pomocą wywołania metody sem_lock.signal(). Generacja bitu startu powoduje rozpoczęcie działania kontrolera I2C. W wyniku wystąpienia poszczególnych zdarzeń zgłaszane jest przerwanie, którego obsługa realizowana jest przez procedurę i2c1_ev_isr_vector (list. 5).
List. 5. Procedura obsługi przerwania
//Call to the global c function extern "C" { void i2c1_ev_isr_vector(void) __attribute__ ((interrupt)); void i2c1_ev_isr_vector(void) { if(i2c1_obj) i2c1_obj->isr(); }
Funkcja ta jest zaprzyjaźniona z klasą i2c_host. Na rzecz obiektu klasyi2c_host jest wywoływana funkcja isr(), która jest właściwą procedurą obsługi przerwania (list. 6).
List. 6. Funkcja isr() jest procedurą obsługi przerwania
//I2c interrupt handler void i2c_host::isr() { uint32_t event = get_last_event(); switch( event ) { //Send address case I2C_EVENT_MASTER_MODE_SELECT: //EV5 send_7bit_addr(bus_addr); break; //Send bytes in tx mode case I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED: //EV6 case I2C_EVENT_MASTER_BYTE_TRANSMITTED: //EV8 if(tx_bytes>0) { send_data(tx_buf[buf_pos++]); tx_bytes--; } if(tx_bytes==0) { if(rx_buf) { //Change address to read only bus_addr |= I2C_BUS_RW_BIT; ack_on(true); generate_start(); buf_pos = 0; } else { generate_stop(); sem_irq.signal_isr(); } } break; //Master mode selected case I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED: //EV7 if(rx_bytes==1) { ack_on(false); } break; //Master byte rcv case I2C_EVENT_MASTER_BYTE_RECEIVED: if(rx_bytes>0) { rx_buf[buf_pos++] = receive_data(); rx_bytes--; } if(rx_bytes==1) { ack_on(false); generate_stop(); } else if(rx_bytes==0) { generate_stop(); sem_irq.signal_isr(); } break; //Stop generated event default: if(event & EVENT_ERROR_MASK) { err_flag = event >> 8; i2c->SR1 &= ~EVENT_ERROR_MASK; sem_irq.signal_isr(); } else { clear_flags(); } break; } }