티스토리 뷰
이번에는 주로 디바이스간의 정보를 주고 받기 위한 방법중 하나인 TWI에 대해서 설명하도록 하겠다.
TWI는 Two-Wire Serial Interface의 약자로써 TWSI라고도 부른다. 원래는 필립스사에서 만든 IIC(Inter Integrated-Circuit)라는 규격을 일반적인 용어로 다르게 부르는 것이다. 그러나 아직까지 많은 개발자들이 TWI 대신 I2C라고 부르는 것을 더 편하게 생각하고 있다.
TWI는 이미 이름에서 알수 있듯이 두개의 선을 이용하여 데이터를 주고 받는다. 하나는 SDA라는 데이터를 주고 받는 선이고, 또 다른 하나는 SCL이라는 클럭 정보를 제공하는 선이다. 데이터 선이 하나 밖에 없으므로 반이중(half-duplex) 통신을 할 수 밖에 없고 SPI와 마찬가지로 마스터-슬레이브 구조를 따른다.
위의 그림이 TWI 통신을 위한 연결도를 보여주는 것이다. 모든 디바이스가 SDA, SCL 선을 공유하고 pull-up 저항으로 평소에는 high 상태로 유지 되도록 한다.
TWI에서 SDA 데이터는 SCL이 high 상태에서는 동일한 상태를 유지하고 있어야 한다. SCL이 low 상태에서만 SDA의 신호가 바뀔수 있다.
그러나 START, REPEATED START, STOP 신호에 대해서는 SCL이 high인 상태에서 SDA의 상태를 변경하여 통신의 시작과 끝을 알려주도록 되어 있다.
SCL이 high상태에서 SDA가 high에서 low로 상태가 바뀌게 되면 TWI 통신이 개시 된다는 것을 슬레이브로 알려주게 된다. 이와 반대로 SCL이 high 상태에서 SDA가 low에서 high로 변경되면 TWI 통신이 끝났음을 슬레이브로 알려 주는 것이다.
TWI 통신은 항상 START와 STOP 신호가 쌍으로 제공되어야 하는데 때로는 STOP을 생략하고 다시 START 신호를 줄 필요가 있다. 이런 기능을 제공하기 위하여 REPEATED START 신호를 사용하면 된다.
SPI 통신에서 마스터가 특정 슬레이브와 통신하기 위하여 SS 신호를 이용하여 슬레이브를 선택하였다면, TWI에서는 슬레이브마다 가지고 있는 고유의 주소를 이용하여 특정 슬레이브를 선택할 수 있다. 그러므로 동일한 TWI 버스에 중복된 주소를 가진 슬레이브가 있으면 안된다. TWI 주소는 7비트값을 가진다.
TWI 통신을 시작하기 위해서 START 신호를 보내고, 그 다음에는 어떤 슬레이브와 통신할 것인지 알려주기 위하여 슬레이브 주소를 이어서 보낸다. TWI 통신은 항상 MSB 비트부터 내 보내도록 되어 있다. 마스터에서 슬레이브 주소를 내보낼때 읽기 동작을 할 것인지, 쓰기 동작을 할것인지 미리 알려 주도록 되어 있다. 읽기 동작은 '1'을 내보내고, 쓰기 동작을 하기 위해서는 '0'을 내 보내도록 되어 있다.
예를 들어 0x52라는 주소값을 가지는 슬레이브로 읽기 동작을 하기 위해서는 0xA5의 데이터가 SDA 선으로 나가게 되고 쓰기 동작을 하려면 0xA4를 내보내여야 한다.
선택된 슬레이브에서 주소를 제대로 인식 하였으면 다음 SCL 클럭에서 Ack 신호를 보내 주어야 한다. 제대로 수신하였다는 것을 알려주기 위하여 SDA 신호를 low로 만들어 준다. 만약 마스터에서 주소값을 보낸후 Ack 신호를 검출하였을 때 그 값이 0이 아니면 슬레이브에서 제대로 주소를 받지 못했다고 판단하게 된다.
위의 그림은 데이터 통신에 있어서 신호의 변화를 설명한 그림이다. 위 그림의 두번째 줄에 있는 'SDA from Transmitter' 그림은 데이터를 내보내는 쪽에서의 상태 변화를 보여준다. 8비트 데이터를 모두 보낸 다음에는 반드시 SDA 신호를 high 상태로 만든다. 세번째 줄에 있는 'SDA from Receiver'는 수신쪽에서의 상태 변화를 보여주는 것으로, 8비트 데이터를 모두 받으면 SDA 신호를 low로 만들어 Ack 신호를 알려준다. 만약 수신측에서 9번째 비트 위치에서 SDA 신호를 'high'로 만들면 더 이상의 수신을 하지 않겠다는 의미로 사용된다.
위의 그림이 일반적인 TWI 통신에 있어서 슬레이브 주소와 데이터 통신을 하는 순서를 보여주는 것이다.
위의 그림은 AVR에 있는 TWI 블럭의 구성도이다. 굵은 선의 박스로 표시되어 있는것이 CPU에서 제어 가능한 TWI 레지스터 들이다. 읽기/쓰기를 위한 데이터는 TWDR 레지스터를 이용하고, TWI의 제어를 위하여 TWCR과 TWSR이 사용된다. TWBR은 TWI의 통신속도를 조절하기 위하여 사용된다. TWAR은 슬레이브로 동작될 때 사용하게 되는 주소를 써놓은 레지스터이다. 마스터로 동작될때는 사용하지 않아도 된다.
TWBR값에 따른 TWI의 통신 주파수를 계산하는 계산식이다. CPU가 1MHz로 동작되고 있고, TWBR이 0으로 설정하였다면 SCL의 클럭주파수는 62.5KHz가 된다. TWI에 연결된 디바이스가 이보다 더 낮은 주파수에서 동작된다면 TWBR값이나 prescaler값을 조절하여 최적의 속도로 통신이 이루어질수 있도록 하여야 한다.
위의 그림은 TWI의 레지스터를 이용하여 TWI 통신을 수행할 때 소프트웨어와 하드웨어가 서로 어떻게 동작되는지 설명하고 있다. 이 그림의 자세한 설명은 datasheet 문서에 잘 나와 있으니 문서를 참고하기 바란다.
간단한 설명을 하자면,
1. 프로그램으로 TWCR레지스터에 START 신호를 내보내는 동작을 수행한다.
2. START 신호가 나가면 하드웨어에서는 TWINT 비트를 set 시킨다.
3. 프로그램에서 TWINT값이 set 된것을 확인한 다음 TWSR 값에서 상태 정보를 읽어 START신호가 제대로 나갔는지 확인한다. 문제 없으면 슬레이브주소와 읽기/쓰기 비트값을 합쳐 TWDR레지스터 쓴후 TWINT 비트값을 set 한다. TWINT 비트는 W1C 이므로 프로그램에서 1을 쓰면 1로 되어 있는 비트값이 clear가 된다. 그러면 하드웨어서는 TWDR값을 SDA를 통해서 내보낸다. 슬레이브에서는 이에 대한 응답으로 Ack 신호를 내 보낸다.
4. 마스터에서 슬레이브 주소를 내보내고 나면 TWINT가 set된다.
5. 프로그램에서는 TWSR값을 확인하여 정상적으로 통신이 이루어 졌는지 확인하고 그렇지 못한 경우 에러 처리를 한다. 정상 완료 되었다면 슬레이브로 데이터를 내 보낸다.
6. 데이터를 모두 내 보내면 하드웨어에서 TWINT값을 set 한다.
7. 프로그램에서는 TWSR값을 보고 정상적으로 데이터가 나갔는지 확인한다. 그리고 마지막으로 STOP 신호를 내보낸다.
위의 그림에서 표현된 절차를 프로그램으로 구현하는 방법은 아래 표를 참조하면 된다.
위의 샘플 코드를 참조하여 실제 TWI 통신이 어떻게 이루어 지는지 확인해 보도록 하겠다.
이번 TWI 프로젝트에서 사용하는 부품은 PCF8563이다. PCF8563은 Real-Time Clock/Calendar 용 칩이다. 1초마다 현재 시간을 업데이트 할 수 있는 부품이다. 이 칩을 모듈화 한 것과 AVR을 TWI를 통해서 연결하고, PCF8563안에 있는 레지스터를 읽어 오는것을 구현해 보도록 하겠다.
위 사진에서 보이는 것과 같이 왼쪽에 SDA, SCL 신호를 연결하기 위한 핀이 있다.
AVR 블럭도에는 TWI 블럭이 PORT C와 연결되어 있는 것을 볼 수 있다. 그렇다면 SDA, SCL은 PORT C에서 찾아보면 될 것 같다.
SDA 핀은 PC4에, SCL 핀은 PC5에 연결되어 있다.
프로젝트를 위한 회로는 위의 그림처럼 구성하면 된다.
회로를 구성하였으면 본격적으로 코드를 작성해 보도록 하겠다.
가장 먼저 할 일은 TWI 블럭의 제어를 위한 레지스터 구조체를 만드는 것이다.
위의 표가 TWI 블럭에서 사용하는 레지스터들이다. 따라서 다음과 같은 구조체를 작성하면 된다.
#define TWI_REG 0xB8
struct twi
{
uint8_t twbr;
uint8_t twsr;
uint8_t twar;
uint8_t twdr;
uint8_t twcr;
};
volatile struct twi *const twi = (void*)TWI_REG;
위의 구조체를 이용하여 실제 레지스터의 상태를 확인하거나 데이터를 내보내는 드라이브 코드를 inline 함수로 작성한다.
static inline void twiStart(void)
{
twi->twcr = TWINT | TWSTA | TWEN;
}
static inline void twiStop(void)
{
twi->twcr = TWINT | TWSTO | TWEN;
}
static inline void twiWaitAck(void)
{
while (!(twi->twcr & TWINT));
}
static inline uint8_t twiChkAck(void)
{
return twi->twsr & TW_STS;
}
static inline void twiSendAck(void)
{
twi->twcr |= TWEA;
}
static inline void twiSendNack(void)
{
twi->twcr &= ~TWEA;
}
static inline void twiSendByte(uint8_t data)
{
twi->twdr = data;
twi->twcr = TWINT | TWEN;
}
static inline void twiRcvNackByte(void)
{
twi->twcr = TWINT | TWEN;
}
static inline void twiRcvAckByte(void)
{
twi->twcr = TWINT | TWEN | TWEA;
}
그리고, TWI의 각 단계별 동작을 수행하는 코드를 다음과 같이 작성한다.
static int twi_start(void)
{
twiStart();
twiWaitAck();
if (twiChkAck() != TWI_START)
return -1;
return 0;
}
static void twi_stop(void)
{
twiStop();
}
static int twi_restart(void)
{
twiStart();
twiWaitAck();
if (twiChkAck() != TWI_RESTART)
return -1;
return 0;
}
static int twi_send_dev_addr(uint8_t addr)
{
uint8_t flag = addr & 1 ? TWI_MR_SLA_ACK : TWI_MT_SLA_ACK;
twiSendByte(addr);
twiWaitAck();
if (twiChkAck() != flag)
return -1;
return 0;
}
static int twi_send_data(uint8_t data)
{
twiSendByte(data);
twiWaitAck();
if (twiChkAck() != TWI_MT_DATA_ACK)
return -1;
return 0;
}
static int twi_read_ack_data(uint8_t *data)
{
twiRcvAckByte();
twiWaitAck();
if (twiChkAck() != TWI_MR_DATA_ACK)
return -1;
*data = twi->twdr;
return 0;
}
static int twi_read_nack_data(uint8_t *data)
{
twiRcvNackByte();
twiWaitAck();
if (twiChkAck() != TWI_MR_DATA_NACK)
return -1;
*data = twi->twdr;
return 0;
}
위의 함수를 이용하여 실제 TWI 통신을 이용하여 데이터를 송수신하는 코드는 다음과 같다.
int twi_write(uint8_t dev_addr, uint8_t reg_addr, uint8_t data)
{
if (twi_start())
return -1;
if (twi_send_dev_addr((dev_addr << 1) | TWI_WR))
return -1;
if (twi_send_data(reg_addr))
return -1;
if (twi_send_data(data))
return -1;
twi_stop();
return 0;
}
int twi_read(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint8_t len)
{
int i;
if (twi_start())
return -1;
if (twi_send_dev_addr((dev_addr << 1) | TWI_WR))
return -1;
if (twi_send_data(reg_addr))
return -1;
if (twi_restart())
return -1;
if (twi_send_dev_addr((dev_addr << 1) | TWI_RD))
return -1;
for (i=0; i<len-1; i++)
{
if (twi_read_ack_data(data + i))
return -1;
}
if (twi_read_nack_data(data + i))
return -1;
twi_stop();
return 0;
}
TWI 통신을 위한 코드가 모두 완성 되었으면 setup() 함수를 다음과 같이 작성하여 PCF8563으로부터 어떤 값들이 읽혀 오는지 확인해 보자.
void
twi_init(void)
{
twi->twbr = 0;
twi->twsr = 0;
}
void read_pcf8563_reg(void)
{
uint8_t data[16];
int i;
if (!twi_read(RTC_ADDR, 0, data, 16))
{
printf("RTC Data: ");
for (i=0; i<16; i++)
{
printf("%02X ", data[i]);
}
}
}
void setup(void)
{
uart_init();
printf("\nTWI Test Program. [%s %s]\n",__DATE__,__TIME__);
twi_init();
read_pcf8563_reg();
}
PCF8563 datasheet 문서에 설명된 레지스터 맵이다. 모두 16개의 레지스터가 있으므로 read_pcf8563_reg() 함수에서 16개의 레지스터 모두를 읽어 오는 코드를 작성하였다.
수행결과 위의 값이 나왔다. 몇개의 레지스터는 문서에서 설명한 reset 값과 같은것도 있고, 아닌것도 있는것처럼 보인다. 세번째 값이 매초마다 변경된다고 문서에 나와 있으므로 loop() 함수에서 read_pcf8563_reg()를 반복해서 수행하여 초를 표시해주는 레지스터 값만 1초마다 증가되는지 확인해 보기 바란다.
아마 회로에 문제가 없다면 세번째 레지스터 값이 1초마다 증가되는 것을 볼 수 있을 것이다.
코드를 약간 수정하여 초를 나타내는 레지스터가 이전 값과 다를때, 즉, 1초 지났을 때 한번만 출력되도록 수정해 보도록 하겠다.
void read_pcf8563_reg(void)
{
uint8_t data[16];
static uint8_t last_sec = 0;
int i;
if (!twi_read(RTC_ADDR, 0, data, 16))
{
if (last_sec != data[2])
{
last_sec = data[2];
printf("\x1B[60DRTC Data: ");
for (i=0; i<16; i++)
{
printf("%02X ", data[i]);
}
}
}
}
1초에 한번만 레지스터 값들이 출력될 것이다.
PCF8563 datasheet를 참조하여 사람의 눈으로 보기 쉽게 가공하는 작업은 각자 해보기 바란다.
'입문' 카테고리의 다른 글
시스템 클럭 (8) | 2016.04.18 |
---|---|
SPI (6) | 2016.04.16 |
ADC(Analog to Digital Converter) (0) | 2016.04.14 |
타이머/카운터 - PWM(Phase Correct PWM Mode) (0) | 2016.04.12 |
타이머/카운터 - PWM(Fast PWM Mode) (2) | 2016.04.12 |