USI модуль в MSP430x2xx описывается как простое устройство основанное на управляемом сдвиговом регистре. Однако простота этого модуля оборачивается сложностью в его использовании. То, что в полноценном I2C модуле будет "спрятано под капотом" в виде незримой автоматики, здесь придется делать вручную.
В официальной библиотеке Texas Instruments для использования USI модуля в I2C режиме: slaa368.zip и документации к ней: slaa368.pdf алгоритм работы с USI представлен как конечный автомат. Мне показалось это интересным и я решил разобрать его работу в этой статье. Сама библиотека написана на ассемблере для IAR компилятора, и в так виде лично для меня она была бесполезна. Поэтому в процессе изучения библиотеки я портировал ее на mspgcc, правда не всю, а только работу в режиме мастера.
Статью условно можно разделить на три части. Вначале идет программная реализация I2C для MSP430, которая в дальнейшем будет использоваться как эталонная, т.е. с ней будут сравниваться остальные варианты. Затем будет рассмотрена аппаратная реализация I2C для USI MSP430, и закончим мы конечным автоматом(Finite-State Machine). В итоге у нас будет три драйвера I2C шины: один программный и два аппаратных.
В качестве микроконтроллера я буду использовать MSP430G2452 который шел в комплекте c MSP-EXP430G2 Launchpad. Данный микроконтроллер имеет два порта GPIO, один "А"-таймер, один USI-модуль, 8Кб флеш-памяти и 128 байт оперативной памяти. Т.е. это что-то вроде ATtiny84 по возможностям. Для обкатки драйверов I2C шины, в качестве целевого устройства я буду использовать RTC DS3231, для которого я портировал на Си свою Arduino-библиотеку DS3231SQW.
Так как программного кода в статье много, полные исходники и скомпилированные прошивки я выложил на gitlab.com https://gitlab.com/flank1er/msp430_usi_i2c
Для простоты будем считать, что вы работаете в Linux или в CYGWIN под Windows, используете компилятор mspgcc из комплекта Energia IDE, а в качестве программатора используется MSP-EXP430G2 Launchpad.
Блок схема USI модуля в режиме I2C представлена ниже:
Модуль имеет следующие регистры:
Всего четыре регистра: 1) управляющие регистры USICTL0/USICTL1, 2) регистр установки скорости I2C интерфейса USICKCTL, 3) счетчик битов USICNT, 4) и собственно сам сдвиговый регистр USISRL/USISRH.
Регистры содержат следующие флаги:
Первым делом, для отладки нам нужен будет UART передатчик. USCI модуля который реализует функции UART в микроконтроллере MSP430G2452 нет, поэтому придется делать программный вариант. Тема эта уже пройденная, алгоритм я брал отсюда Proteus8.x + MSP430x2xx: программная реализация I2C интерфейса, подключение устройств: RTC DS1307/DS3231, EEPROM AT24C512/AT24C32 Но в данном случае нужно добиться работы программного через Lauchpad, а это значит, что TX пин должен быть назначен на P1.1. Кроме того, в отличии от оригинала, для формирования задержек используется переход в режим энергосбережения вместо пустых циклов.
Создаем папку проекта:
$ mkdir -p ./02_soft_uart/{inc,src}
$ cd ./02_soft_uart/
Для сборки проекта нам нужен будет Makefile:
MCU=msp430g2452 OBJCOPY=msp430-objcopy CC=msp430-gcc CFLAGS=-mmcu=$(MCU) -Os -fdata-sections -ffunction-sections -Wall -I ./inc LDFLAGS=-mmcu=$(MCU) -Wl,--gc-sections -Werror OBJ=main.o timer_a.o uart_sw.o TARGET=firmware .PHONY: all clean %.o: ./src/%.c $(CC) -c -o $@ $< $(CFLAGS) all: $(OBJ) $(CC) $(LDFLAGS) -o $(TARGET).elf $(OBJ) $(OBJCOPY) -O ihex $(TARGET).elf $(TARGET).hex install: @mspdebug -n rf2500 "prog $(TARGET).elf" clean: @rm -v *.elf *.hex $(OBJ)
Для формирования задержек потребуется таймер. В этом микроконтроллере он всего один.
Заголовочный файл для библиотеки таймера ./inc/timer_a.c:
#ifndef __TIMER_A_H__ #define __TIMER_A_H__ extern void wait_ms(uint16_t ms); #endif
Исходный файл библиотеки таймера ./src/timer_a.c будет таким:
#include "msp430g2452.h" #include "sys/types.h" #include "timer_a.h" volatile uint16_t count; // for software uart #pragma vector=TIMER0_A0_VECTOR __interrupt void Timer0(void) { __bic_SR_register_on_exit(LPM0_bits); // wakeup from sleep mode } // for wait_ms() #pragma vector=TIMER0_A1_VECTOR __interrupt void Timer0_OVF(void) { switch(TA0IV) { case TA0IV_TACCR1: break; // CCR1 not used case TA0IV_TACCR2: break; // CCR2 not used case TA0IV_TAIFG: if (count) count--; // overflow else __bic_SR_register_on_exit(LPM0_bits); // wakeup from sleep mode break; } } void wait_ms(uint16_t ms) { count=ms; TACCR0=1000; // delay TACTL |= TAIE + MC_1; // enable interrupt __bis_SR_register(LPM0_bits + GIE); // goto sleep TACTL &= ~(TAIE + MC_1); }
Теперь собственно сам программный UART. Заголовочный файл ./inc/uart_sw.h:
#ifndef __UART_SW_H__ #define __UART_SW_H__ #define TXD BIT1 #define RXD BIT2 #define uOUT P1OUT #define uDIR P1DIR extern uint8_t uart_send_char(uint8_t ch); extern void uart_send_uint8(uint8_t num); extern void uart_send_string(char* str); extern void uart_send_hex_uint8(uint8_t num); #endif
Исходный файл ./src/uart_sw.c:
#include "msp430g2452.h" #include "sys/types.h" #include "uart_sw.h" uint8_t uart_send_char(uint8_t ch){ int i; ch=~ch; TACCR0=98; //9600 TACCTL0=CCIE; // enable interrupt TACTL |=MC_1; // enable timer __bis_SR_register(LPM0_bits + GIE); // goto sleep // START bit uOUT &= ~TXD; for(i=0;i<8;i++) // DATA transmission { __bis_SR_register(LPM0_bits + GIE); // goto sleep uOUT=(ch&(1<<i)) ? uOUT & ~TXD: uOUT | TXD; } // STOP bit __bis_SR_register(LPM0_bits + GIE); // goto sleep P1OUT &= ~TXD; __bis_SR_register(LPM0_bits + GIE); // goto sleep P1OUT |=TXD; TACCTL0=0; // disable interrupt TACTL &= ~MC_1; // disable timer; return ch; } void uart_send_string(char* str) { while (*str) { uart_send_char(*str++); } } void uart_send_hex_uint8(uint8_t num) { static const uint8_t symbol[16] ="0123456789ABCDEF"; uart_send_char('0'); uart_send_char('x'); uart_send_char(symbol[(num >> 4)]); uart_send_char(symbol[(num & 0x0f)]); } void uart_send_uint8(uint8_t num){ uint8_t sym[3]; int8_t i=2; do { if (num == 0 && i<2) sym[i]=0x20; // space else sym[i]=0x30+num%10; num=num/10; i--; } while (i>=0); uint8_t j=0; for (i=0;i<3;i++) { if (!(i<2 && sym[i] == 0x20)) uart_send_char(sym[i]); j++; } }
Здесь значение задержки для передачи одного бита - TACCR0=98, была подобрана экспериментально для 1MHz F_CPU, хотя она должна была составлять 103 такта. В принципе, если не трогать настройку частоты DCO, то UART как раз работает с такой задержкой. Так что я не знаю как правильно, возможно мой компилятор прописывает в прошивку неправильные константы, и если у вас терминальная программа будет выводить не читаемый текст при скорости порта 9600, значит стоит поменять значение TACCR0 на 103.
Исходник программы main.c:
#include <msp430g2452.h> #include <sys/types.h> #include "timer_a.h" #include "uart_sw.h" #define LED BIT0 int main (void) { count=0; // watchdog setup WDTCTL=WDTPW | WDTHOLD; //turn off watchdog // GPIO setup P1DIR = LED; // set P1.0 as Push Pull mode(PP) P1OUT &=~LED; // F_CPU 1MHz BCSCTL1 = CALBC1_1MHZ; DCOCTL = CALDCO_1MHZ; BCSCTL2 &=~(DIVS_0); //SMCLK=DCO BCSCTL3 |= LFXT1S_2; //ACLK= VLO =~ 12KHz // TimerA init TACTL=TASSEL_2 + ID_0 + TACTL; /* TASSEL_2 =use SMCLK; MC_1 =continous to TACCR0; ID_0 =prescaler = 1; TACLR =clean counter TAR; TAIE; =enable timer interrupt; */ // software uart uDIR += TXD; //interrupt __enable_interrupt(); // main loop uint8_t i=0; for(;;){ wait_ms(1000); P1OUT |= LED; uart_send_string("i= "); uart_send_uint8(i++); uart_send_char('\n'); P1OUT &= ~LED; }; return 0; }
На этом этапе уже можно собирать проект и прошивать микроконтроллер, но мы еще можем файл проекта для редактора кода Sublime Text 3. Для этого добавим в каталог проекта файл 02_soft_uart.sublime-project с таким содержанием:
{ "build_systems": [ { "cmd": ["make","all"], "name": "Software UART", "working_dir": "${project_path}", "file_regex": "^(^\\S.*\\.\\w+):(\\d+):(\\d+): (\\w+ ?\\w+?): (.*)$", "variants": [ { "name": "Clean", "cmd": ["make", "clean"] }, { "name": "Install", "cmd": ["make", "install"] } ] } ], "folders": [ { "follow_symlinks": true, "path": "." } ], }
Теперь в редакторе SublimeText3 мы можем открыть наш проект через меню->Projet->Open Project и в диалоговом окне выбора файла нужно указать файл проекта. Выглядит все это как-то так:
Проект можно собрать по Ctrl+B и прошить микроконтроллер выбрав через меню->Tools-Build With-> Software Uart Install.
Для работы программного UART передатчика на MSP430 Lauchpad v1.5 нужно Tx и Rx джампики установить следующим образом:
Полные исходники можно скачать отсюда: https://gitlab.com/flank1er/msp430_usi_i2c/tree/master/02_soft_uart
Тестировать драйвера I2C будем на RTC DS3231. Алгоритм работы программы будет такой:
На мой взгляд программа средней сложности, здесь есть как запись в DS3231 так и чтение данных оттуда, кроме того используются разные режимы энергосбережения. Думаю что если драйвер будет справляться с выполнением этой программы, то можно будет считать, что он работает корректно.
Библиотека для работы с DS3231 является портом на mpsgcc моей Arduino библиотеки - DS3231SQW. API I2C библиотеки было написано так, чтобы было удобно копировать код из Arduino.
Например функция установки будильника в Arduino выглядит так:
void DS3231::set_alarm_a1(ALARM_A1_TYPE type, uint8_t sec, uint8_t min, uint8_t hour ,uint8_t day) { //// W R I T E ///////////////////////////////////// set_control(DS3231_A1IE|DS3231_INTCH); Wire.beginTransmission(DS1307_I2C_ADDRESS); Wire.write(DS3231_ALARM1_ADDRESS); Wire.write(dec_bcd(sec)|((type<<7) & 0x80)); Wire.write(dec_bcd(min)|((type<<6) & 0x80)); Wire.write(dec_bcd(hour)|((type<<5) & 0x80)); Wire.write(dec_bcd(day)|((type<<4) & 0x80)|((type<<2) & 0x40)); Wire.endTransmission(); // read for check /////////////////////////////////// update_status_control(); }
Также функция на Си получилось такой:
void ds3231_set_alarm_a1(ALARM_A1_TYPE type, uint8_t sec, uint8_t min, uint8_t hour ,uint8_t day) { //// W R I T E ///////////////////////////////////// ds3231_set_control(DS3231_A1IE|DS3231_INTCH); if(i2c_begin(DS1307_ADR<<1) && !send_i2c(DS3231_ALARM1_ADDRESS)) { send_i2c(dec_bcd(sec)|((type<<7) & 0x80)); send_i2c(dec_bcd(min)|((type<<6) & 0x80)); send_i2c(dec_bcd(hour)|((type<<5) & 0x80)); send_i2c(dec_bcd(day)|((type<<4) & 0x80)|((type<<2) & 0x40)); } stop_i2c(); // read for check /////////////////////////////////// ds3231_update_status_control(); }
Функция чтения установленных в DS3231 будильников в Arduino имеет такой вид:
void DS3231::update_status_control() { Wire.beginTransmission(DS1307_I2C_ADDRESS); Wire.write(DS1307_CONTROL_ADDRESS); Wire.endTransmission(); char Buffer[10]; char * ptr= (char*)Buffer; Wire.requestFrom(DS1307_I2C_ADDRESS,10); for(char i=0; i<10; i++) Buffer[i]=Wire.read(); alarm=*(ALARM*)ptr; }
Ее аналог на Си:
uint8_t ds3231_update_status_control() { uint8_t ret=FAILURE; if (ds3231_set_address(DS1307_CONTROL_ADDRESS) && i2c_request_from(DS1307_ADR,10)) { char Buffer[10]; char * ptr= (char*)Buffer; uint8_t i; for(i=0;i<10;i++) Buffer[i]=i2c_read(); alarm=*(ALARM*)ptr; ret=SUCCESS; } return ret; }
В общем, думаю что понятно. Итак, добавляем в проект заголовочный файл программного драйвера I2C шины ./inc/i2c_sw.h:
#ifndef __I2C_SW_H__ #define __I2C_SW_H__ int start_i2c(); int stop_i2c(); uint8_t send_i2c(uint8_t value); uint8_t read_i2c(int last); uint8_t i2c_begin(uint8_t adr); uint8_t i2c_request_from(uint8_t ard, uint8_t cnt); uint8_t i2c_read(); #endif
Теоретически, объявления функций: start_i2c(), read_i2c(int last) можно убрать, несколько байт на объявлении их как static сэкономить можно)
Исходный файл с программным драйвером I2C шины ./src/i2c_sw.c:
#include "msp430g2452.h" #include "sys/types.h" #include "i2c_sw.h" #define SCL BIT6 #define SDA BIT7 #define sclOUT P1OUT #define sdaOUT P1OUT #define sclDIR P1DIR #define sdaDIR P1DIR #define sclREN P1REN #define sdaREN P1REN #define sdaIN P1IN #define sclIN P1IN #define QDEL __delay_cycles(20); #define HDEL __delay_cycles(40); #define SDA_I2C_HI sdaDIR&=~SDA #define SCL_I2C_HI sclDIR&=~SCL #define SDA_I2C_LO sdaDIR|=SDA #define SCL_I2C_LO sclDIR|=SCL #define SCL_TOGGLE_I2C HDEL; SCL_I2C_HI;HDEL;SCL_I2C_LO; #define initLEN 2 #define rtcLEN 7 #define LAST 1 #define NOLAST 0 static uint8_t i2c_count; /////////////// I2C ////////////////////////////////// int start_i2c() { int ret=64; uint8_t valueSDA, valueSCL; // init I2C sdaOUT &=~SDA; sclOUT &=~SCL; do { SDA_I2C_HI; SCL_I2C_HI; QDEL; valueSDA=sdaIN & SDA; valueSCL=sclIN & SCL; } while((!valueSDA || !valueSCL) && --ret>0); if (!ret) return 0; SDA_I2C_LO;QDEL; SCL_I2C_LO;QDEL; return ret; } int stop_i2c(){ SDA_I2C_LO; SCL_I2C_LO;HDEL; SCL_I2C_HI;QDEL; SDA_I2C_HI;QDEL; if (!(sdaIN & SDA) || !(sclIN & SCL)) return 1; else return 0; } uint8_t send_i2c(uint8_t value) { int bits=8; while(bits>0) { bits--; SCL_I2C_LO; QDEL; while((sclIN & SCL)); if (value & (1<<bits)) SDA_I2C_HI; else SDA_I2C_LO; SCL_TOGGLE_I2C; } //get ACK QDEL; SDA_I2C_HI; QDEL; SCL_I2C_HI; while(!(sclIN & SCL)); QDEL; uint8_t ack; ack=(sdaIN & SDA); QDEL; SCL_I2C_LO; HDEL; return ack; } uint8_t read_i2c(int last) { uint8_t c, b; int i; b=0; SDA_I2C_HI; for(i=0;i<8;i++) { HDEL; SCL_I2C_HI; c=(sdaIN & SDA); b <<=1; if(c) b|=1; HDEL; SCL_I2C_LO; } if(last) SDA_I2C_HI; else SDA_I2C_LO; SCL_TOGGLE_I2C; SDA_I2C_HI; return b; } uint8_t i2c_begin(uint8_t adr) { //////////// WRITE ADDRESS ///////////////////// // START bit int attempts; uint8_t ack=0xff; attempts=start_i2c(); if (attempts) ack=send_i2c(adr); // 0 - Error, 1 - Success return (!attempts || ack ) ? 0: 1; } uint8_t i2c_request_from(uint8_t ard, uint8_t cnt) { i2c_count=cnt; return (i2c_begin((ard<<1)|1)) ? 1 : 0; } uint8_t i2c_read() { uint8_t ret=0; if (i2c_count) { i2c_count--; if (!i2c_count) { ret=read_i2c(LAST); stop_i2c(); } else ret=read_i2c(NOLAST); } return ret; }
Исходник практически без изменений скопирован с апрельской статьи Proteus8.x + MSP430x2xx: программная реализация I2C интерфейса, подключение устройств: RTC DS1307/DS3231, EEPROM AT24C512/AT24C32, были только добавлены функции для совместимости с Arduino кодом: i2c_begin(uint8_t adr), i2c_request_from(uint8_t ard, uint8_t cnt) и i2c_read(). Ничего нового здесь нет, первая версия драйвера была написана еще два года назад для atmega8: Bit-banging AVR: делаем сканер TWI шины, и была результатом изучения Procyon AVRlib и книги Юрия Ревича "Практическое программирование микроконтроллеров atmel avr на языке ассемблера".
Заголовочный файл для библиотеки DS3231 ./inc/ds3231.h будет выглядеть так:
#ifndef __DS3231_H__ #define __DS3231_H__ #define DS1307_ADR 0x68 #define DS1307_CALENDAR 0x00 #define DS1307_CONTROL_ADDRESS 0x07 #define DS3231_CORRECTION 0 #define DS3231_ALARM1_ADDRESS 0x07 #define DS3231_ALARM2_ADDRESS 0x0B #define DS3231_CONTROL_ADDRESS 0x0E #define DS3231_STATUS_ADDRESS 0x0F #define DS3231_AGING_ADDRESS 0x10 #define DS3231_TEMPERATURE_ADDRESS 0x11 // control register #define DS3231_A1IE 0x01 #define DS3231_A2IE 0x02 #define DS3231_INTCH 0x04 #define DS3231_CONV 0x20 #define DS3231_BBSQW 0x40 #define DS3231_EOSC 0x80 #define DS3231_1HZ 0x0 #define DS3231_1024HZ 0x08 #define DS3231_4096HZ 0x10 #define DS3231_8192HZ 0x18 #define DS3231_32768HZ 0x01 // status register #define DS3231_A1F 0x01 #define DS3231_A2F 0x02 #define DS3231_BSY 0x04 #define DS3231_32KHZ 0x08 #define DS3231_OSF 0x80 typedef enum FIELD { YEAR=6, MONTH=5, DATE=4, DAY=3, HOUR=2, MINUTE=1, SECOND=0 } FIELD; //Alarm masks typedef enum ALARM_A1_TYPE { A1_EVERY_SECOND = 0x0F, A1_MATCH_SECONDS = 0x0E, A1_MATCH_MINUTES = 0x0C, //match minutes *and* seconds A1_MATCH_HOURS = 0x08, //match hours *and* minutes, seconds A1_MATCH_DATE = 0x00, //match date *and* hours, minutes, seconds A1_MATCH_DAY = 0x10, //match day *and* hours, minutes, seconds } ALARM_A1_TYPE; typedef enum ALARM_A2_TYPE { A2_EVERY_MINUTE = 0x8E, A2_MATCH_MINUTES = 0x8C, //match minutes A2_MATCH_HOURS = 0x88, //match hours *and* minutes A2_MATCH_DATE = 0x80, //match date *and* hours, minutes A2_MATCH_DAY = 0x90, //match day *and* hours, minutes } ALARM_A2_TYPE; typedef enum ALARM_NUMBER { ALARM_A1=0x01, ALARM_A2=0x02 } ALARM_NUMBER ; extern void ds3231_init(); extern uint8_t ds3231_set_address(uint8_t adr); extern uint8_t ds3231_set_register(uint8_t adr, uint8_t value); extern uint8_t ds3231_update(); extern uint8_t ds3231_get(FIELD value); extern uint8_t ds3231_update_status_control(); extern char ds3231_get_temperature(); extern char ds3231_get_temperature_fraction(); // CONTROL REGISTER ////////////////////////// extern uint8_t ds3231_get_control(); extern void ds3231_preset_control(uint8_t value); extern void ds3231_set_control(uint8_t value); extern void ds3231_reset_control(uint8_t value); // STATUS REGISTER ///////////////////////// extern uint8_t ds3231_get_status(); extern void ds3231_reset_status(uint8_t status); extern void ds3231_set_status(uint8_t status); // ALARM /////////////////////// extern uint8_t ds3231_is_alarm(ALARM_NUMBER num); void ds3231_disable_alarm(ALARM_NUMBER num); extern void ds3231_set_alarm_a1(ALARM_A1_TYPE type, uint8_t sec, uint8_t min, uint8_t hour ,uint8_t day); extern void ds3231_set_alarm_a2(ALARM_A2_TYPE type, uint8_t min, uint8_t hour ,uint8_t day); /////////////////////////////////////////////// extern void ds3231_print_calendar(); extern void ds3231_print_alarm_1(); extern void ds3231_print_alarm_2(); #endif
Исходник ./src/ds3231.c:
/* Based on https://github.com/flank1er/DS3231SQW/ */ #include "msp430g2452.h" #include "sys/types.h" #include "i2c_sw.h" #include "ds3231.h" #include "uart_sw.h" #define FAILURE 0x0 #define SUCCESS 0x1 #define SQW BIT3 volatile uint32_t sec; static uint32_t prev_sec; static uint8_t cal[DS1307_CONTROL_ADDRESS]; static char tempMSB; static uint8_t tempLSB; static void ds3231_add_day(); // 10 bytes typedef struct ALARM_t { unsigned second_a1 :7; unsigned a1m1 :1; unsigned minute_a1 :7; unsigned a1m2 :1; unsigned hour_a1 :7; unsigned a1m3 :1; unsigned day_date_a1 :6; unsigned dydt_a1 :1; unsigned a1m4 :1; unsigned minute_a2 :7; unsigned a2m2 :1; unsigned hour_a2 :7; unsigned a2m3 :1; unsigned day_date_a2 :6; unsigned dydt_a2 :1; unsigned a2m4 :1; unsigned control :8; unsigned status :8; unsigned aging :8; } ALARM, *ALARM_t; ALARM alarm; #pragma vector=PORT2_VECTOR __interrupt void Port2(void) { if(P2IFG & SQW) { if (++sec >= 86400) ds3231_add_day(); if (ds3231_get_control() & DS3231_INTCH) { __bic_SR_register_on_exit(LPM4_bits); } P2IFG&=~SQW; } } uint8_t dec_bcd(uint8_t dec) { return (dec/10*16) + (dec%10);}; uint8_t bcd_dec(uint8_t bcd) { return (bcd-6*(bcd>>4));}; void ds3231_init(){ P2DIR &=~SQW; // HiZ mode P2IES |= SQW; // falling front P2IFG &=~SQW; // interrupt flag clearing P2IE |= SQW; // enable external interrupt sec=0; prev_sec=0; } static void ds3231_add_day() { cal[DAY]=cal[DAY]%7+1; uint8_t days; switch(cal[MONTH]) { case 4: case 6: case 9: case 11: days=30; break; case 2: days=(cal[YEAR] % 4 == 0) ? 29 :28; break; default: days=31; } if (cal[DATE] == days) { cal[DATE]=1; cal[MONTH]=cal[MONTH]+1; } else cal[DATE]=cal[DATE]+1; // new year if (cal[MONTH] > 12) { cal[MONTH] = 1; cal[YEAR] = cal[YEAR] + 1; } sec=0; } uint8_t ds3231_set_address(uint8_t adr) { uint8_t ack=0xff; if(i2c_begin(DS1307_ADR<<1)) ack=send_i2c(adr); stop_i2c(); // 0 - Error, 1 - Success return (!ack) ? SUCCESS : FAILURE; } uint8_t ds3231_set_register(uint8_t adr, uint8_t value) { uint8_t ack=0xff; if(i2c_begin(DS1307_ADR<<1) && !send_i2c(adr)) ack=send_i2c(value); stop_i2c(); // 0 - Error, 1 - Success return (!ack) ? SUCCESS : FAILURE; } uint8_t ds3231_update() { uint8_t ret,i; if (ds3231_set_address(0x00) && i2c_request_from(DS1307_ADR,DS1307_CONTROL_ADDRESS)) { for(i=0;i<DS1307_CONTROL_ADDRESS;i++) { if (i==0) cal[i]=bcd_dec(i2c_read() & 0x7f); else if (i==2) cal[i]=bcd_dec(i2c_read() & 0x3f); else cal[i]=bcd_dec(i2c_read()); } ret=SUCCESS; sec=cal[SECOND] + cal[MINUTE]*60; for(i=0;i<cal[HOUR];i++) sec+=3600; sec+=DS3231_CORRECTION; prev_sec=sec; } else ret=FAILURE; return ret; } uint8_t ds3231_get(FIELD value) { if (sec != prev_sec) { cal[SECOND]=sec%60; cal[MINUTE]=(sec%3600)/60; cal[HOUR]=sec/3600; prev_sec=sec; } return cal[value]; } uint8_t ds3231_update_status_control() { uint8_t ret=FAILURE; if (ds3231_set_address(DS1307_CONTROL_ADDRESS) && i2c_request_from(DS1307_ADR,10)) { char Buffer[10]; char * ptr= (char*)Buffer; uint8_t i; for(i=0;i<10;i++) Buffer[i]=i2c_read(); alarm=*(ALARM*)ptr; ret=SUCCESS; } return ret; } char ds3231_get_temperature() { uint8_t ret=0xff; if (ds3231_set_address(DS3231_TEMPERATURE_ADDRESS) && i2c_request_from(DS1307_ADR,2)) { tempMSB=i2c_read(); tempLSB=i2c_read()>>6; ret=tempMSB; } return ret; } char ds3231_get_temperature_fraction() { char ret='?'; switch (tempLSB) { case 0: ret='0'; break; case 1: ret='2'; break; case 2: ret='5'; break; case 3: ret='7'; } return ret; } // STATUS REGISTER /////////////////// uint8_t ds3231_get_status() { return (uint8_t)alarm.status; } void ds3231_set_status(uint8_t status) { status = alarm.status | status; ds3231_set_register(DS3231_STATUS_ADDRESS, status); ds3231_update_status_control(); } void ds3231_reset_status(uint8_t status) { status = alarm.status & ~status; ds3231_set_register(DS3231_STATUS_ADDRESS, status); ds3231_update_status_control(); } // CONTROL REGISTER ////////////////// uint8_t ds3231_get_control() { return (uint8_t)alarm.control; } void ds3231_preset_control(uint8_t value) { ds3231_set_register(DS3231_CONTROL_ADDRESS, value); ds3231_update_status_control(); } void ds3231_set_control(uint8_t value) { value = value | alarm.control; ds3231_set_register(DS3231_CONTROL_ADDRESS, value); ds3231_update_status_control(); } void ds3231_reset_control(uint8_t value) { value = alarm.control & ~value; ds3231_set_register(DS3231_CONTROL_ADDRESS, value); ds3231_update_status_control(); } uint8_t ds3231_is_alarm(ALARM_NUMBER num){ uint8_t ret=FAILURE; if (alarm.status & num) { ds3231_reset_status(num); ret=SUCCESS; } return ret; } void ds3231_disable_alarm(ALARM_NUMBER num) { if (num == ALARM_A1) ds3231_reset_control(DS3231_A1IE); else ds3231_reset_control(DS3231_A2IE); } void ds3231_set_alarm_a1(ALARM_A1_TYPE type, uint8_t sec, uint8_t min, uint8_t hour ,uint8_t day) { //// W R I T E ///////////////////////////////////// ds3231_set_control(DS3231_A1IE|DS3231_INTCH); if(i2c_begin(DS1307_ADR<<1) && !send_i2c(DS3231_ALARM1_ADDRESS)) { send_i2c(dec_bcd(sec)|((type<<7) & 0x80)); send_i2c(dec_bcd(min)|((type<<6) & 0x80)); send_i2c(dec_bcd(hour)|((type<<5) & 0x80)); send_i2c(dec_bcd(day)|((type<<4) & 0x80)|((type<<2) & 0x40)); } stop_i2c(); // read for check /////////////////////////////////// ds3231_update_status_control(); } void ds3231_set_alarm_a2(ALARM_A2_TYPE type, uint8_t min, uint8_t hour ,uint8_t day) { //// W R I T E ///////////////////////////////////// ds3231_set_control(DS3231_A2IE|DS3231_INTCH); if(i2c_begin(DS1307_ADR<<1) && !send_i2c(DS3231_ALARM2_ADDRESS)) { send_i2c(dec_bcd(min)|((type<<6) & 0x80)); send_i2c(dec_bcd(hour)|((type<<5) & 0x80)); send_i2c(dec_bcd(day)|((type<<4) & 0x80)|((type<<2) & 0x40)); } stop_i2c(); // read for check /////////////////////////////////// ds3231_update_status_control(); } void ds3231_print_alarm_1() { if (alarm.a1m1) uart_send_string("A1M1: ON"); else uart_send_string("A1M1: OFF"); uart_send_string(" A1 second: "); uart_send_hex_uint8(alarm.second_a1); uart_send_char('\n'); if (alarm.a1m2) uart_send_string("A1M2: ON"); else uart_send_string("A1M2: OFF"); uart_send_string(" A1 minute: "); uart_send_hex_uint8(alarm.minute_a1); uart_send_char('\n'); if (alarm.a1m3) uart_send_string("A1M3: ON"); else uart_send_string("A1M3: OFF"); uart_send_string(" A1 hour: "); uart_send_hex_uint8(alarm.hour_a1); uart_send_char('\n'); if (alarm.a1m4) uart_send_string("A1M4: ON"); else uart_send_string("A1M4: OFF"); if (alarm.dydt_a1) uart_send_string(" DY/DT: ON, day is: "); else uart_send_string(" DY/DT: OFF, date is: "); uart_send_hex_uint8(alarm.day_date_a1); uart_send_char('\n'); } void ds3231_print_alarm_2() { if (alarm.a2m2) uart_send_string("A2M2: ON"); else uart_send_string("A2M2: OFF"); uart_send_string(" A2 minute: "); uart_send_hex_uint8(alarm.minute_a2); uart_send_char('\n'); if (alarm.a2m3) uart_send_string("A2M3: ON"); else uart_send_string("A2M3: OFF"); uart_send_string(" A2 hour: "); uart_send_hex_uint8(alarm.hour_a2); uart_send_char('\n'); if (alarm.a2m4) uart_send_string("A2M4: ON"); else uart_send_string("A2M4: OFF"); if (alarm.dydt_a2) uart_send_string(" DY/DT: ON, day is: "); else uart_send_string(" DY/DT: OFF, date is: "); uart_send_hex_uint8(alarm.day_date_a2); uart_send_char('\n'); } void ds3231_print_calendar(){ uart_send_char('\n'); // print date /* uart_send_string("year: "); uart_send_uint8(ds3231_get(YEAR)); uart_send_string(" month: "); uart_send_uint8(ds3231_get(MONTH)); uart_send_string(" date: "); uart_send_uint8(ds3231_get(DATE)); uart_send_string(" day: "); uart_send_uint8(ds3231_get(DAY)); uart_send_char('\n'); */ // print time uart_send_uint8(ds3231_get(HOUR)); uart_send_char(':'); uart_send_uint8(ds3231_get(MINUTE)); uart_send_char(':'); uart_send_uint8(ds3231_get(SECOND)); uart_send_string(" temp= "); uart_send_uint8(ds3231_get_temperature()); uart_send_char('.'); uart_send_char(ds3231_get_temperature_fraction()); uart_send_string(" ctl= "); uart_send_hex_uint8(ds3231_get_control()); uart_send_string(" sts= "); uart_send_hex_uint8(ds3231_get_status()); uart_send_char('\n'); }
В дальнейшем мы почти ничего не будем трогать, кроме драйвера I2C шины.
Текст программы main.c будет таким:
#include <msp430g2452.h> #include <sys/types.h> #include "timer_a.h" #include "uart_sw.h" #include "i2c_sw.h" #include "ds3231.h" #define LED BIT0 int main (void) { //count=0; // watchdog setup WDTCTL=WDTPW | WDTHOLD; //turn off watchdog // GPIO setup P1DIR = LED; // set P1.0 as Push Pull mode(PP) P1OUT &=~LED; // F_CPU 1MHz BCSCTL1 = CALBC1_1MHZ; DCOCTL = CALDCO_1MHZ; BCSCTL2 &=~(DIVS_0); //SMCLK=DCO BCSCTL3 |= LFXT1S_2; //ACLK= VLO =~ 12KHz // TimerA init TACTL=TASSEL_2 + ID_0 + TACTL; /* TASSEL_2 =use SMCLK; MC_1 =continous to TACCR0; ID_0 =prescaler = 1; TACLR =clean counter TAR; TAIE; =enable timer interrupt; */ // software uart uDIR += TXD; // rtc init ds3231_init(); //interrupt __enable_interrupt(); // RTC get data if (ds3231_update() && ds3231_update_status_control()) { //ds3231_reset_control(DS3231_INTCH); ds3231_set_register(DS3231_CONTROL_ADDRESS, 0x0); ds3231_set_register(DS3231_STATUS_ADDRESS, 0x0); //ds3231_update_status_control(); } uint8_t i=0; for(;;){ wait_ms(10000); P1OUT |= LED; ds3231_print_calendar(); if (ds3231_is_alarm(ALARM_A2)) uart_send_string("Alarm 2 is ON "); else uart_send_string("Alarm 2 is OFF "); if (ds3231_is_alarm(ALARM_A1)) uart_send_string("Alarm 1 is ON\n"); else uart_send_string("Alarm 1 is OFF\n"); // set timer ALARM A1 on two minutes if (++i == 5) { uart_send_string("\nSet alarm A1...\n"); ds3231_set_alarm_a1(A1_MATCH_MINUTES,ds3231_get(SECOND)%60, (ds3231_get(MINUTE)+2)%60, 0,0); uart_send_string("ctl= "); uart_send_hex_uint8(ds3231_get_control()); uart_send_string(" sts= "); uart_send_hex_uint8(ds3231_get_status()); uart_send_char('\n'); ds3231_print_alarm_1(); __bis_SR_register(LPM4_bits + GIE); wait_ms(10); uart_send_string("\nWAKEUP!\n"); ds3231_reset_control(DS3231_A1IE|DS3231_INTCH); ds3231_update(); ds3231_print_calendar(); } P1OUT&=~LED; }; return 0; }
Последнее, что осталось - заменить в Makefile строку:
OBJ=main.o timer_a.o uart_sw.o
на:
OBJ=main.o timer_a.o uart_sw.o i2c_sw.o ds3231.o
На этом всё, можно собирать и прошивать. Прошивка весит 3870 байт, работает как-то так:
На скриншоте виден глюк: после пробуждения читаются статусные флаги обоих будильников, хотя прерывание устанавливается только только на первый(control register=0x05), в общем работает, но код видимо еще сыроват...
К слову говоря, программный драйвер I2C шины не самое плохое решение, у него есть огромное преимущество: он работает на всех микроконтроллерах, а его рабочие линии можно определять на любые свободные ножки микросхемы. Причем он не занимает много флеш-памяти, на аппаратном драйвере удастся сэкономить от силы пару сотен байт.
Полные исходники можно скачать отсюда: https://gitlab.com/flank1er/msp430_usi_i2c/tree/master/03_soft_i2c
Работа USI модуля в режиме USI описывается в официальном вики Texas Instruments I2C Communication with USI Module, а также в переводе руководства на микроконтроллеры MSP430x2xx СЕМЕЙСТВО МИКРОКОНТРОЛЛЕРОВ MSP430x2xx. Архитектура. Программирование. Разработка приложений.(глава 14, страница 348).
Я потратил день пытаясь написать аппаратный драйвер на основе этих источников, пока не обнаружил на github'e готовый код пятилетней давности: https://github.com/samerpav/MMA8452_MSP430/blob/master/i2c_usi_mst.c.
Я практически без изменений скопировал этот исходник к себе в проект, заменив лишь только функцию ожидания с пустым циклом на уход в спящий режим.
Итак, для замены программного драйвера на аппаратный нужно будет в заголовочный файл драйвера добавить объявление новой функции:
void init_i2c();
Также в main.c перед разрешением прерываний нужно будет добавить вызов этой функции. В остальном - без изменений.
Исходник драйвера у меня получился таким:
// based on https://github.com/samerpav/MMA8452_MSP430/blob/master/i2c_usi_mst.c #include "msp430g2452.h" #include "sys/types.h" #include "i2c_usi.h" // ack = !LAST #define LAST 1 #define NOLAST 0 //// status code //////////// #define SUCCESS 1 #define FAILURE 0 //////////////// static uint8_t i2c_count; #define SET_SDA_AS_OUTPUT() (USICTL0 |= USIOE) #define SET_SDA_AS_INPUT() (USICTL0 &= ~USIOE) #define FORCING_SDA_HIGH() \ { \ USISRL = 0xFF; \ USICTL0 |= USIGE; \ USICTL0 &= ~(USIGE+USIOE); \ } #define FORCING_SDA_LOW() \ { \ USISRL = 0x00; \ USICTL0 |= USIGE+USIOE; \ USICTL0 &= ~USIGE; \ } // USI I2C ISR function ////////////////////////////// #pragma vector = USI_VECTOR __interrupt void USI_ISR (void) { USICTL1 &= ~USIIFG; __bic_SR_register_on_exit(LPM0_bits + GIE); } /////////////// I2C ////////////////////////////////// void init_i2c () { P1DIR = 0xFF; sdaDIR &= ~SDA; sclDIR &= ~SCL; sdaOUT &= ~SDA; sclOUT &= ~SCL; sdaREN |= SDA; sclREN |= SCL; // это не обязательно, если на шине уже есть подтягивающие резисторы P1SEL = BIT6 + BIT7; USICTL0 = USIPE6 + USIPE7 + USIMST + USISWRST; // Port & USI mode setup USICTL1 = USII2C + USIIE; // Enable I2C mode & USI interrupt USICKCTL = USIDIV_7 + USISSEL_2 + USICKPL; // USI clks: SCL = SMCLK/128 //USICKCTL = USIDIV_6 + USISSEL_2 + USICKPL; // USI clks: SCL = SMCLK/64 USICNT |= USIIFGCC ; // Disable automatic clear control USICTL0 &= ~USISWRST; // Enable USI USICTL1 &= ~USIIFG; // Clear pending flag } uint8_t start_i2c() { USISRL = 0x00; USICTL0 |= USIGE+USIOE; USICTL0 &= ~USIGE; return SUCCESS; } uint8_t stop_i2c() { USICTL0 |= USIOE; USISRL = 0x00; USICNT = 1; // wait for USIIFG is set __bis_SR_register(LPM0_bits + GIE); FORCING_SDA_HIGH(); return 0; // always success } uint8_t send_i2c(uint8_t byte) { // send address and R/W bit SET_SDA_AS_OUTPUT(); USISRL = byte; USICNT = (USICNT & 0xE0) + 8; // wait until USIIFG is set __bis_SR_register(LPM0_bits + GIE); // check NACK/ACK SET_SDA_AS_INPUT(); USICNT = (USICNT & 0xE0) + 1; // wait for USIIFG is set __bis_SR_register(LPM0_bits + GIE); // NACK received returns FALSE return (USISRL & 0x01) ? SUCCESS : FAILURE; } uint8_t read_i2c(int last) { SET_SDA_AS_INPUT(); USICNT = (USICNT & 0xE0) + 8; // wait for USIIFG is set __bis_SR_register(LPM0_bits + GIE); uint8_t ret=USISRL; SET_SDA_AS_OUTPUT(); USISRL=(last) ? 0xff : 0x00; USICNT = (USICNT & 0xE0) + 1; // wait until USIIFG is set __bis_SR_register(LPM0_bits + GIE); // set SDA as input SET_SDA_AS_INPUT(); return ret; } uint8_t i2c_begin(uint8_t adr) { //////////// WRITE ADDRESS ///////////////////// // START bit int attempts; uint8_t ack=0xff; attempts=start_i2c(); if (attempts) ack=send_i2c(adr); // 0 - Error, 1 - Success return (!attempts || ack ) ? 0: 1; } uint8_t i2c_request_from(uint8_t ard, uint8_t cnt) { i2c_count=cnt; return (i2c_begin((ard<<1)|1)) ? 1 : 0; } uint8_t i2c_read() { uint8_t ret=0; if (i2c_count) { i2c_count--; if (!i2c_count) { ret=read_i2c(LAST); stop_i2c(); } else ret=read_i2c(NOLAST); } return ret; }
Результат работы немного отличается от предыдущего, в данном случае флаги будильников отрабатывают как надо:
В этот раз размер прошивки получился 3670 байт, т.е. на двести байт меньше. Кроме уменьшения веса прошивки, мы также сократили энергопотребление устройства, т.к. для формирования задержек используется переход в энергосберегающий режим LPM0.
Полные исходники можно скачать отсюда: https://gitlab.com/flank1er/msp430_usi_i2c/tree/master/04_usi_i2c
Кроме способа представленного выше, драйвер I2C на USI модуле можно оформить как конечный автомат(finite-state machine).
С точки зрения программирования, конечный автомат в данном случае будет представлять собой switch оператор с несколькими case значениями(состояниями). Switch оператор располагается целиком в обработчике прерывания. Для установки какого-либо начального состояния, прерывание вызывается принудительно установкой флага прерывания. Дальнейшие состояния автомата переключаются автоматически(на то он и автомат), пока не будет достигнуто финальное состояние.
Такой подход к программированию драйвера I2C шины позволяет избавиться почти от всех функций работы с I2C шиной и заменить их одной функцией установки автомата на то или иное начальное значение.
Исходный код драйвера I2C шины на основе конечного автомата у меня получился таким:
#include "msp430g2452.h" #include "sys/types.h" #include "i2c_usi.h" //// status code //////////// #define SUCCESS 1 #define FAILURE 0 ////////////////////////// static uint8_t i2c_count; volatile uint8_t state; volatile uint8_t AckResult; volatile uint8_t slv_adr; volatile uint8_t ls; /////////////// I2C ////////////////////////////////// void init_i2c () { sdaDIR &= ~SDA; sclDIR &= ~SCL; sdaOUT &= ~SDA; sclOUT &= ~SCL; sdaREN |= SDA; sclREN |= SCL; P1SEL = BIT6 + BIT7; USICTL0 = USIPE6 + USIPE7 + USIMST + USISWRST; // Port & USI mode setup USICTL1 = USII2C + USIIE; // Enable I2C mode & USI interrupt USICKCTL = USIDIV_7 + USISSEL_2 + USICKPL; // USI clks: SCL = SMCLK/128 //USICKCTL = USIDIV_6 + USISSEL_2 + USICKPL; // USI clks: SCL = SMCLK/64 USICNT |= USIIFGCC ; // Disable automatic clear control USICTL0 &= ~USISWRST; // Enable USI USICTL1 &= ~USIIFG; // Clear pending flag } uint8_t stop_i2c() { fsm_i2c(0x0, STOP, NOLAST); return 0; // always success } uint8_t send_i2c(uint8_t value){ return (fsm_i2c(value, SEND, LAST)) ? 0 : 1; } uint8_t read_i2c(int last){ return fsm_i2c(0x0, GET, last); } uint8_t i2c_begin(uint8_t adr) { return fsm_i2c(adr, START, NOLAST); } uint8_t i2c_request_from(uint8_t ard, uint8_t cnt) { i2c_count=cnt; return (i2c_begin((ard<<1)|1)) ? 1 : 0; } uint8_t i2c_read() { uint8_t ret=0; if (i2c_count) { i2c_count--; if (!i2c_count) { ret=read_i2c(LAST); stop_i2c(); } else ret=read_i2c(NOLAST); } return ret; } uint8_t fsm_i2c(uint8_t adr, uint8_t st, uint8_t last) { slv_adr=adr; ls=last; state=st; do { if (state == START || state == SEND || state == STOP || state == GET) { __disable_interrupt(); USICTL1|=USIIFG; } __bis_SR_register(LPM0_bits + GIE); } while (state != 10); return AckResult; } // USI I2C ISR function #pragma vector = USI_VECTOR __interrupt void USI_ISR (void) { switch (state) { case 0: USISRL = 0x00; // MSB=0 USICTL0 |= USIGE+USIOE; // SDA as OUTPUT, turn off LATCH USICTL0 &= ~USIGE; // turn on LATCH state=1; break; case 1: // start // send address USICTL0|=USIOE; //sda as OUTPUT USISRL = slv_adr; // send data USICNT=8; //bit counter state=2; break; case 2: USICTL0 &= ~USIOE; //SET_SDA_AS_INPUT(); USICNT = 1; // bit counter state=3; break; case 3: // get ACK if (USISRL & 0x01) { AckResult=FAILURE; state=4; // prestop } else { AckResult=SUCCESS; state=10; // exit } break; case 4: // prestop USICTL0 |= USIOE; // sda as OUTPUT USISRL = 0x00; USICNT = 1; state=5; // goto STOP break; case 5: USISRL = 0xFF; USICTL0 |= USIGE; // Transparent latch enabled USICTL0 &= ~(USIGE+USIOE); // Latch/SDA output disabled state=10; break; case 6: USICTL0 &= ~USIOE; //SET_SDA_AS_INPUT(); USICNT = 8; state=7; break; case 7: AckResult = USISRL; //get data USICTL0|=USIOE; // sda as OUTPUT USISRL=(ls) ? 0xff : 0x00; USICNT=1; state=8; break; case 8: USICTL0 &= ~USIOE; //sda as input state=(ls) ? 4 : 10; break; default: break; } USICTL1 &= ~USIIFG; __bic_SR_register_on_exit(LPM0_bits + GIE); }
Заголовочный файл к нему:
#ifndef __I2C_USI_H__ #define __I2C_USI_H__ #define SCL BIT6 #define SDA BIT7 #define sclOUT P1OUT #define sdaOUT P1OUT #define sclDIR P1DIR #define sdaDIR P1DIR #define sclREN P1REN #define sdaREN P1REN #define sdaIN P1IN #define sclIN P1IN #define NOLAST 0x0 #define LAST 0x1 #define START 0x0 #define SEND 0x1 #define STOP 0x4 #define GET 0x6 void init_i2c(); uint8_t stop_i2c(); uint8_t send_i2c(uint8_t value); uint8_t read_i2c(int last); uint8_t i2c_begin(uint8_t adr); uint8_t i2c_request_from(uint8_t ard, uint8_t cnt); uint8_t i2c_read(); uint8_t fsm_i2c(uint8_t adr, uint8_t st, uint8_t last); #endif
Конечный автомат имеет десять состояний. Состояния 0,1,4,6 являются стартовыми, а состояние 10 - финишным. Остальные состояния являются промежуточными.
Для совместимости с API программного драйвера пришлось ввести функции-обертки: stop_i2c(), send_i2c(uint8_t value), read_i2c(int last), i2c_begin(uint8_t adr). Из-за этого не удалось еще более сжать размер прошивки, она получилась 3818 байт. Я думаю, что если переписать код ds3231.c на прямое использование функции fsm_i2c() вместо функций-оберток к ней, то прошивка получилась бы меньше по размеру.
Так же замечу, что исчезла функция start_i2c(), т.к. за стартом всегда следует передача адреса, он была заменена функцией i2c_begin();
Итого, конечный автомат имеет четыре начальных состояния, а значит он реализует четыре функции.
Функция stop_i2c() реализуется переключением состояний 4->5->10.
Функция send_i2c() реализуется переключением состояний 1->2->3->10, если Ack был получен, и 1->2->3->4->5->10 если Ack не был получен. Т.е. последнем случае она автоматически заканчивается STOP'ом.
Функция read_i2c() реализуется переключением состояний 6->7->8->10 если это не последний принимаемый байт, или 6->7->8->4-5->10 если байт последний. Т.е. в последнем случае опять вызывается стоп.
Функция i2c_begin() начинается c нулевого состояния, после чего переключается на состояние 1, т.е. идет вызов функции send_i2c().
Как видно, многие функции являются комбинаций состояний друг-друга, что позволяет сократить код и является аналогом вызова одной функции из другой.
Полные исходники можно скачать отсюда: https://gitlab.com/flank1er/msp430_usi_i2c/tree/master/05_i2c_usi_fsm
Теперь рассмотрим за что отвечает каждое состояние.
Согласно документации состояние START на I2C шине формируется следующим образом:
В драйвере slaa368 это реализуется так:
clr.b &USISRL ; Generate start condition bis.b #USIGE+USIOE,&USICTL0 ; bic.b #USIGE,&USICTL0 ;
У меня на Си это выглядит так:
case 0: USISRL = 0x00; // MSB=0 USICTL0 |= USIGE+USIOE; // SDA as OUTPUT, turn off LATCH USICTL0 &= ~USIGE; // turn on LATCH
Состояние STOP формируется в два этапа:
В драйвере slaa368 сначала вызывается Prestop:
Prestop bis.b #USIOE,&USICTL0 ; SDA =output bis.b #1,&USICNT ; Bit counter = 1, SCL high, SDA low mov.w #14,&TI_I2CState ; goto next state, generate stop bic.b #USIIFG,&USICTL1 reti
После чего вызывается 14-е состояние, т.е. сам STOP:
STATE14 ;common stop condition for Tx/Rx mov.b #0xFF,&USISRL ; USISRL=1 to release SDA bis.b #USIGE,&USICTL0 ; Transparent latch enabled bic.b #USIGE+USIOE,&USICTL0 ; Latch/SDA output disabled mov.w #0,&TI_I2CState ; Reset state machine for next oprn. bic.b #USIIFG,&USICTL1
Я сопоставлял это еще с кодом предыдущего драйвера, и в итоге получил такой результат:
case 4: // prestop USICTL0 |= USIOE; // sda as OUTPUT USISRL = 0x00; USICNT = 1; state=5; // goto STOP break; case 5: USISRL = 0xFF; USICTL0 |= USIGE; // Transparent latch enabled USICTL0 &= ~(USIGE+USIOE); // Latch/SDA output disabled state=10; break;
Передача байта производится в три этапа. 1) загрузка байта в сдвиговый регистр и настройка USI модуля на его передачу; 2) после завершения передачи USI модуль перенастраивается на чтение ACK кода 3) после принятия ACK или NACK передача байта завершается.
В драйвере slaa368 это выглядит так:
Data_Tx push.w R6 bis.b #USIOE,&USICTL0 ; SDA = output mov.w &TI_TxPtr,R6 ; Pointer to tx data mov.b @R6,&USISRL bis.b #8,&USICNT ; bit counter = 8, Tx data mov.w #10,&TI_I2CState ; Go to next state: Receive (N)ACK bic.b #USIIFG,&USICTL1 pop.w R6 reti
STATE10 ; Data transmitted, get ready to rx ack/nack byte from slave bic.b #USIOE,&USICTL0 ; SDA = input bis.b #1,&USICNT ; Bit counter = 1, rx (N)Ack bit mov.w #12,&TI_I2CState ; Goto next state: check (N)Ack bic.b #USIIFG,&USICTL1 reti
;Pre-stop condition for Tx, generate stop condition if ctr = 0 else loop back bit.b #0x01,&USISRL ; Process data (N)Ack bit jz Data_Ack ; if ACK received mov.b #1,&TI_AckResult ; Nack, result of oprn. = 1 clr.b &USISRL ; jmp Prestop ; generate prestop
У меня все это вылилось в такой код:
case 1: // start // send address USICTL0|=USIOE; //sda as OUTPUT USISRL = slv_adr; // send data USICNT=8; //bit counter state=2; break; case 2: USICTL0 &= ~USIOE; //SET_SDA_AS_INPUT(); USICNT = 1; // bit counter state=3; break; case 3: // get ACK if (USISRL & 0x01) { AckResult=FAILURE; state=4; // prestop } else { AckResult=SUCCESS; state=10; // exit } break;
Процедура приема байта так же происходит в три этапа: 1) настраиваем USI модуль на прием восьми бит; 2) дожидаемся завершения приема всех восьми бит и перенастраиваем USI модуль на передачу одного бита ACK или NACK; 3) передаем ответку и завершаем работу.
В драйвере slaa368 это реализовано так:
Data_Rx bic.b #USIOE,&USICTL0 ; SDA = input bis.b #8,&USICNT ; bit counter = 8, Rx data mov.w #6,&TI_I2CState ; goto next state: Send (N)ACK bic.b #USIIFG,&USICTL1 reti STATE6 ;Data received, move to buffer and transmit ack (nack if last byte) push.w R6 mov.w &TI_RxPtr,R6 ; Pointer to received data mov.b &USISRL,0(R6) inc.w &TI_RxPtr ; Increment pointer for next rx pop.w R6 cmp.b #1,&TI_ByteCtr ; Last byte? jz data_NACK ; If yes send Nack to slave bis.b #USIOE,&USICTL0 ; SDA = output clr.b &USISRL ; If no send ACK jmp STATE6_Exit data_NACK mov.b #0xFF,&USISRL ; Send NACK STATE6_Exit bis.b #1,&USICNT ; Bit counter = 1, send NACK bit mov.w #8,&TI_I2CState ; goto next state, pre-stop bic.b #USIIFG,&USICTL1 reti
Т.е. здесь два этапа, но я сделал в три как аппаратном драйвере, потому что посчитал, что так более правильно:
case 6: USICTL0 &= ~USIOE; //SET_SDA_AS_INPUT(); USICNT = 8; state=7; break; case 7: AckResult = USISRL; //get data USICTL0|=USIOE; // sda as OUTPUT USISRL=(ls) ? 0xff : 0x00; USICNT=1; state=8; break; case 8: USICTL0 &= ~USIOE; //sda as input state=(ls) ? 4 : 10; break;
На этом вроде бы все. На мой взгляд, в целом, USI модуль - это довольно мутная штука, но я посчитал, что конечный автомат стоит того, чтобы о нем рассказать. Конечные автоматы широко применяются в программировании, в играх например на них делают искусственный интеллект ботов, в электронике по принципу конечных автоматов работают электронные часы, микрокалькуляторы, и пр.