Зеркало сайта: vivacious-stockings-frog.cyclic.app

MSP430G2452: USI модуль в режиме I2C как конечный автомат

разделы: MSP430 , I2C , дата: 10 декабря 2017г.

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.

    Содержание статьи:
  1. Основные характеристики USI модуля в режиме I2C;
  2. Создание базового проекта для Sublime Text 3: "Программный UART передатчик для MSP430G2452";
  3. Добавление в проект программной реализации I2C и библиотеки DS3231;
  4. Аппаратный драйвер I2C шины для USI модуля;
  5. USI модуль в I2C режиме как конечный автомат;
  6. Работа USI модуля в режиме I2C.

1) Основные характеристики USI модуля в режиме I2C

Блок схема USI модуля в режиме I2C представлена ниже:

Модуль имеет следующие регистры:

Всего четыре регистра: 1) управляющие регистры USICTL0/USICTL1, 2) регистр установки скорости I2C интерфейса USICKCTL, 3) счетчик битов USICNT, 4) и собственно сам сдвиговый регистр USISRL/USISRH.

Регистры содержат следующие флаги:

2) Создание базового проекта для Sublime Text 3: "Программный UART передатчик для MSP430G2452"

Первым делом, для отладки нам нужен будет 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

3) Добавление в проект программной реализации I2C и библиотеки DS3231

Тестировать драйвера I2C будем на RTC DS3231. Алгоритм работы программы будет такой:

  1. При включении микроконтроллера он запрашивает текущее время у DS3231, после чего у RTC конфигурируется SQW пин на работу с частотой 1Гц, и в дальнейшем микроконтроллер подсчитывает время через внешнее прерывание по этому пину.
  2. После пяти итераций главного цикла, в RTC устанавливается будильник A1 на две минуты, после чего микроконтроллер уходит в режим энергосбережения LPM4
  3. После пробуждения микроконтроллер снова запрашивает текущее время у 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

4) Аппаратный драйвер I2C шины для USI модуля

Работа 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

5) 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

6) Работа USI модуля в режиме I2C

Теперь рассмотрим за что отвечает каждое состояние.

a) Состояние START

Согласно документации состояние 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

Состояние 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 модуль - это довольно мутная штука, но я посчитал, что конечный автомат стоит того, чтобы о нем рассказать. Конечные автоматы широко применяются в программировании, в играх например на них делают искусственный интеллект ботов, в электронике по принципу конечных автоматов работают электронные часы, микрокалькуляторы, и пр.