●오류처리
LIN 통신에서 통신 오류가 발생하면 프로그래머가 기록해뒀다가 마스터에게 반드시 보고해야한다. 오류의 종류는 매우 다양하다.
먼저 헤더 필드에 발생할 수 있는 오류는 다음과 같다.
- BREAK_STEP에서 BREAK가 발생하지 않고 정상적인 바이트 필드가 수신될 때
- SYNC_STEP에서 계산한 baud rate가 규정(1~20kbps)을 벗어날 때
- PID_STEP에서 수신된 PID에 패러티 오류가 발생할 때
다음은 응답 필드에서 발생할 수 잇는 오류이다.
- 응답 필드(TX_STEP, RX_STEP, CRC_STEP)에서 바이트 프레임 오류가 발생할 때
- 응답 필드(TX_STEP, RX_STEP, CRC_STEP)에서 BREAK가 발생할 때
- RX_STEP에서 수신된 check sum이 계산한 값과 다를 때
- TX_STEP, CRC_STEP에서 전송한 값과 수신한 값이 다를 때
이외에 시간 관리에 관련된 오류가 있다.
- 응답 필드를 수신할 때, 제한시간 이내에 모든 응답 필드가 수신되지 않을 때
LIN Protocol Spec. 6.3 네트워크에 대한 보고에서는 "각 슬레이브는 자신의 전송 프레임 중에 하나의 상태 비트 신호 Response_Error를 마스터에게 전송해야한다. 노드에 의하여 전송되거나 수신되는 응답 필드에 오류가 있을 경우, 그 노드는 항상 Response_Error를 1로 설정해야한다. Response_Error는 전송 후 지워야한다."는 규정이 있다.
규정에 따르면 모든 오류에 대하여 보고할 필요는 없으며, 응답 필드에서 발생한 오류에 대하여만 보고하면 된다. 특히 헤더 필드에는 오류가 있더라도 오류보고를 하지 않는다는 점에 유의해야한다. 오류의 종류도 상세하게 할 필요가 없으며, 어떤 오류가 발생하더라도 Response_Error 비트 필드만 1로 설정하여 보고하면 된다. 또한 Response_Error 비트 필드가 포함된 메시지의 송신이 완료되면 Response _Error는 0으로 리셋해야한다.
LIN Application Program Interface Spec.의 2.5.10절에는 오류처리와 관련된 16-비트 상태 변수를 각 노드에서 아래 그림과 같이 정의하고 관리하도록 규정하고 있다. 이 변수의 이름을 linStatus라고 명명한다.
- Error in response : 프레임의 응답 필드에 첵섬이나 필드 오류 등이 발생할 경우 1로 설정
- Successful transfer : 오류 없이 응답 필드가 수행된 경우 1로 설정
- Overrun : 노드 내부에서 linStatus 변수를 읽어서 처리하기 전에 두 개 이상의 프레임이 수행된 경우 1로 설정(하나의 LIN 프레임을 송/수신하면 반드시 linStatus 변수를 읽고 처리한다.)
- Goto sleep : 휴면 진입 명령이 수신되면 1로 설정
- Last PID : 가장 최근에 노드에서 송/수신 처리한 프레임의 PID
linStatus는 노드 내부적으로만 이용되는 변수이므로 반드시 규정을 지켜야하는 것은 아니지만, 앞으로 규정이 어떻게 개정될 지 모르므로 가급적 규정을 준수하는 것이 좋겠다. linStatus 관리를 위하여 다음과 같이 변수를 공용체로 선언한다.
union{
unsigned int word;
struct{
unsigned char errorInResponse : 1;
unsigned char successfulTransfer : 1;
unsigned char overrun : 1;
unsigned char gotoSleep : 1;
unsigned char : 4;
unsigned char lastPID : 8;
}bits;
} linStatus;
오류가 발생하면 errorInResponse 비트를 1로 설정하는 함수 setErrorInResponse()를 미리 작성해보자. errorInResponse 비트 또는 successfulTransfer 비트가 이미 1로 설정되어 있는 상황에서 또다시 응답 필드 수신중에 오류(프레임, check sum 등)가 발생하면 이전 linState 변수가 처리되지 않은 상황이므로 overrun 비트도 1로 설정해야한다.
inline void setErrorInResponse()
{
if (linStatus.word & 0x0003) // successfulTransfer or errorInResponse bit is already set
linStatus.word |= 0x0005; // set overrun & errorInResponse bits
else
linStatus.bits.errorInResponse = 1; // set only errorInResponse bit
}
successfulTransfer 비트는 해당 노드가 처리해야할 프레임을 성공적으로 송/수신했을 때 설정해야하며, successfulTransfer 비트 또는 errorInResponse 비트가 이미 설정되어 있다면 linState 변수가 처리되지 않은 상태이므로 overrun 비트도 함께 1로 설정해야한다. 그리고 송/수신에 성공적으로 처리했으면 처리한 메시지의 PID를 lastPID 필드에 저장해야한다.
inline void setSuccessfulTransfer()
{
if (linStatus.word & 0x0003) // successfulTransfer or errorInResponse bit is already set
linStatus.word |= 0x0006; // set overrun & successfulTransfer bits
else
linStatus.bits.successfulTransfer = 1; // set only successfulTransfer bit
linStatus.bits.lastPID = linPID;
}
프로그래밍에 앞서 하나 더 짚고 넘어갈 것이 있다. LIN Protocol Spec. 6.상태 관리의 서문에 보다 상세한 오류 정보에 대한 언급이 있다. 이는 의무적인 규정이 아니지만 노드의 디버깅에 유용하게 사용할 수 있으므로 개발 과정에는 다음과 같이 상세한 오류 정보를 저장하는 변수를 추가적으로 만들어 관리하도록 한다.
union{
unsigned char byte;
struct {
unsigned char noResponse : 1;
unsigned char parityError : 1;
unsigned char checkSumError : 1;
unsigned char frameError : 1;
unsigned char incomplete : 1;
unsigned char baudRateError : 1; // baud rate = 1~20 kbps
unsigned char rxNotEqualTx : 1; // Rx != Tx
unsigned char timeOut : 1;
}bits;
}errorFlags;
마지막으로 오류가 발생할 경우 오류를 처리한 뒤, 대부분은 dormant 상태로 전환되어야한다(LIN 프로토콜 규정 그림4.2 참조). 따라서 doramnt로 전환하는 함수 resetToDormant()를 inline 함수로 미리 작성하여둔다. dormant 상태에서는 BREAK 신호를 수신할 준비만 하면 된다. 다만 마스터의 전송 속도를 모른다는 가정 하에 BREAK를 수신해야하므로 슬레이브 노드는 가장 짧은 BREAK 신호를 검출할 수 있는 전송속도(20kbps)로 초기화해야한다.
inline void resetToDormant()
{
baud_rate = 20000;
timer3.ENABLE= DISABLE; // timer3 interrupt disable
linStep = BREAK_STEP; // BREAK 수신 준비
}
위에서 timer3는 슬롯 시간 관리를 위하여 사용되는데 뒤에 다시 설명하겠다.
먼저 UART_RX() 서비스 루틴에서 처리해야할 오류가 있는 위치부터 확인해보자.
interrupt_service_routine UART_RX(void)
{
if (RX_FRAME_ERROR) {
if (RX_BUFFER == 0) { //BREAK 수신
if (linStep == RX_STEP || linStep == TX_STEP || linStep == CRC_STEP)
{// ① 미완성 프레임 오류 처리}
.......
linStep = SYNC_STEP; // 다음 순서
}
else{
if (linStep == RX_STEP || linStep == TX_STEP || linStep == CRC_STEP)
{// ① 미완성 프레임 오류 처리}
.......
resetToDormant(); // 다음 순서 = BREAK_STEP
}
}
else { // 프레임 오류 없음.
if (linStep == BREAK_STEP) {
//② BREAK 오류처리
// dormant 전환
}
else if (linStep == PID_STEP){
.......
if (RX_BUFFER == pidParity.PID)
{
//패러티 맞음.
.....
}
else{
//③ 패러티 오류 처리
// dormant 전환
.....
}
}
else if (linStep == RX_STEP)
{
if (rxCount < dataLength) // 응답 데이터 수신
{
....
}
else // check sum 수신
{
if (checkSum + RX_BUFFER == 255) // check sum 맞음.
{
.......
// 수신완료 처리
}
else // 계산한 것과 수신한 check sum이 틀림
{
// ④ check sum 오류처리
.....
}
// 응답 필드 데이터 수신 완료
// dormant 전환
.....
}
}
else if (linStep == TX_STEP){
if (RX_BUFFER != tx_data[txCount])
{
// ⑤ 버스 충돌 오류처리
// dormant 전환
.....
}
else
{
.........
}
}
else { //linStep == CRC_STEP
if (RX_BUFFER == checkSum)
{
// 응답 필드 데이터 전송 완료
....... // 전송 완료 처리
}
// 버스 충돌
else
{
// ⑤ 버스 충돌 오류처리
....
}
// dormant 전환
}
}}
위의 코드에서 dormant 전환해야할 부분에는 모두 resetToDormant() 함수를 호출해야할 자리이다. 번호가 붙은 각 오류는 다음과 같이 다뤄져야한다. 해당 코멘트 자리에 들어갈 코드도 제시한다.
① 미완성 바이트 프레임 오류 처리
정상적인 바이트 프레임 오류는 BREAK 필드에서만 발생해야한다. SYNC 필드나 PID 필드에서 바이트 프레임 오류가 발생할 경우 오류보고는 하지 않고 노드 자신의 디버깅 자료로 이용한다. RX_STEP, TX_STEP, CRC_STEP과 같이 응답 필드에서 프레임 오류가 발생할 경우, 이전에 수행되던 프레임을 중단해야하므로 setErrorInResponse()를 호출한다.
if (linStep==TX_STEP || linStep==RX_STEP || linStep==CRC_STEP)
setErrorInResponse();
errorFlags.bits.incomplete = 1;
BREAK 수신은 슬레이브가 어떤 상태에 있었던간에 무조건 하던 작업을 멈추고 새로운 프레임의 수신에 착수해야한다.
② BREAK 오류처리
BREAK가 수신되길 기다리고 있는데 BREAK가 아닌 필드가 수신되었을 경우이다. 이 경우에는 어쨌든 LIN 프레임의 시작(헤더)를 찾지 못한 경우이므로 노드 내부적인 오류처리는 하지만, 마스터에는 보고할 필요 없으므로 setErrorInResponse()는 호출하지 않는다.
errorFlags.bits.frameError = 1;
③ 패러티 오류 처리
패러티 오류는 PID 필드를 수신할 때 발생하므로 헤더 오류의 일종이다. 내부적인 오류로만 처리한다.
errorFlags.bits.parityError = 1;
④ check sum 오류처리
첵섬은 응답 필드에서 발생하므로 setErrorInResponse()를 호출한다.
errorFlags.bits.checkSumError = 1;
setErrorInResponse();
⑤ 버스 충돌 오류처리
버스 충돌은 전송한 바이트 필드가 궤환되어 수신되었을 때, 다른 값을 가질 경우에 버스에 연결된 다른 노드와 동시에 송신하고 있는 상황이다. 이 경우 전송할 데이터가 남았더라도 전송을 중지하고 dormant 상태로 전환해야한다. 버스 충돌 오류는 응답 필드에서 발생하므로 오류보고가 필요하다.
errorFlags.bits.rxNotEqualTx = 1;
setErrorInResponse();
수신완료 처리
다른 노드(마스터 또는 다른 슬레이브)가 송신한 응답 필드수신이 완료되면 successfulTansfer 비트를 1로 설정하고 수신한 메시지의 PID를 lastPID에 저장한다. 노드 내부의 응용계층 프로그램은 successfulTransfer 비트와 lastPID를 읽고 수신된 데이터를 처리해야한다.
setSuccessfulTransfer();
전송완료 처리
전송이 완료되면 successfulTransfer를 1로 설정하고 전송한 메시지의 PID를 lastPID에 저장해야한다. 이 때, 전송된 메시지에 errorInResponse 필드가 포함되어 있었다면 errorInResponse 비트는 0으로 리셋해야한다.
if (PID == STATE_MSG)
stateData.bits.Response_Error = 0;
setSuccessfulTransfer();
●시간관리
LIN에서 시간관리는 LIN Protocol Spec 2.2프레임 슬롯(식6~8 참조)과 5.2휴면진입(goto sleep)에 관련된 시간을 관리하는 것이다. 프레임 슬롯과 관련하여 슬레이브는 BREAK 신호 이후에 20*Tbit*1.4 이내에 SYNC와 PID 필드가 수신되어야 한다. 따라서 BREAK 수신 직후 타이머를 20*Tbit*1.4 이후에 인터럽트가 발생하도록 설정하고, 인터럽트가 발생하면 통신 오류로 판단한 후 dormant 상태로 전환해야한다. Tbit는 SYNC 수신 이전이므로 전송속도를 알 수 없는 상태라고 가정해야한다. 따라서 LIN의 가장 낮은 전송속도(1kbps)로 계산되어야한다. SYNC 신호를 수신하면 전송속도 즉, Tbit를 알 수 있으므로 타이머를 새로 계산된 Tbit를 이용하여 10*Tbit*1.4로 다시 설정해야한다. LIN 기능이 없는 프로세서에서는 SYNC 신호를 input capture로 SYNC 수신 직후의 타이머 재설정은 input capture에서 한다.
헤더 수신중에 타임아웃(타이머 인터럽트)이 발생하면 통신오류이지만 errorInResponse를 변경하지않고 단순히 dormant 상태로 전환하기만하면 된다.
PID 수신 이후에는 타임아웃을 다시 설정해야한다. 수신된 PID를 분석하여 만일 노드 자신이 수신 또는 송신해야하는 프레임이라면 송수신 데이터 수에 맞게 타이머를 재설정한다. 슬레이브 노드가 프레임의 응답 필드 송수신 중에 타임아웃이 발생하면 통신오류로 판단하고 errorInResponse를 1로 설정해야한다.
지금까지의 설명을 코딩하면 다음과 같다.
------------------------------------------------------------------------------------------------
interrupt_service_routine UART_RX(void)
{
if (RX_FRAME_ERROR) {
if (RX_BUFFER == 0) { //BREAK 수신
.....
timer3.TIMEOUT = 28ms; // 초기 타임아웃
}
}
else { // 프레임 오류 없음.
if (linStep == BREAK_STEP) {
.....
}
else if (linStep == PID_STEP){
.......
if (RX_BUFFER == pidParity.PID)
{
//패러티 맞음.
.....
//응답필드 타임아웃. data+checkSum
timer3.TIMEOUT = (Tbit*(dataLength+1)*8)*1.4;
}
.....
}
else if (linStep == RX_STEP)
{
if (rxCount < dataLength) // 응답 데이터 수신
{
....
}
else // check sum 수신
{
timer3.ENABLE = DISABLE; // 타임아웃 해제
.....
}
}
else if (linStep == TX_STEP){
.........
}
else { //linStep == CRC_STEP
// 전송완료
.......
timer3.ENABLE = DISABLE; // 타임아웃 해제
}
}}
interrupt_service_routine INPUT_CAPTURE(void)
{
edge_count++;
if (edge_count == 1)
t1 = input_capture_buffer;
else if (degd_count == 5)
{
...... // baud_rate 계산 완료
timer3.TIMEOUT = Tbit*14; // 헤더 타임아웃
}
else{
input_capture_buffer; // 버퍼 비움
}
}
------------------------------------------------------------------------------------------------
Timer3 인터럽트 서비스 루틴은 항상 타임아웃이 발생할 때에만 수행된다. 타임아웃 처리는 타임아웃이 발생할 때의 상태에 따라 다르다. 먼저 SYNC_STEP에서 타임아웃이 발생했다면 UART_RX_interrupt는 disable 상태이고, input capture가 작동하고 있는 상태이다. 따라서 이 경우에는 input capture 기증을 억제하고 UART_RX를 활성화해야한다. RX_STEP에서 타임아웃이 발생했다면 노드는 슬레이브로 작동하고 있을 경우이다. 이 경우에 수신 바이트 카운터 변수가 0이면 수신이 전혀 되지 않은 상태이고, 0이 아니면 일부만 수신된 경우이다. 두 경우로 나눈 이유는 디버깅에 도움이 되기 때문이다.
TX_STEP에서 타임아웃이 발생한 경우는 심각하게 고려해야한다. 이는 노드 자신이 전송을 제대로 못하고 있다는 것이기 때문이다. 프로그램이 제대로 된 경우라면 전송 중에 타임아웃이 발생되지말아야한다. 프로그램에는 전송 중 타임아웃을 처리하도록 코딩하지만, 이 부분은 절대 수행되지 않도록 코드를 작성해야한다.
타임아웃이 어떤 상태에서 발생하더라도 다음 상태는 초기상태로 전환한다. 데이터를 송/수신중이라 하더라도 이를 중단한다.
interrupt_service_routine TIMER3(void)
{
errorFlags.bits.timeOut = 1; // 디버깅용
if (linStep == SYNC_STEP)
{
input_capture_interrupt = DISABLE; // disable interrupt
UART_RX_interrupt = ENABLE;
}
else if (linStep == RX_STEP)
{
if (rxCount == 0)
errorFlags.bits.noResponse = 1;
else if (rxCount =< dataLength) // checkSum까지 수신
errorFlags.bits.incomplete = 1;
setErrorInResponse();
}
//--** 전송중에 타임아웃이 발행하지 않도록 프로그래밍해야한다.
else if (linStep == TX_STEP || linStep == CRC_STEP) // 이 경우는 절대 수행되지 않아야한다.
{
errorFlags.bits.incomplete = 1;
setErrorInResponse();
}
resetToDormant();
}
------------------------------------------------------------------------------------------------
한편 Dormant 상태에서는 4초 이상 버스가 비활성이면 휴면상태로 진입해야한다(goto sleep). 4초를 카운트하기 위해 별도의 타이머를 설정해도 되지만 다른 용도의 타이머 인터럽트에 카운터 변수를 증감시켜서 4초를 측정하기만하면 된다. 본 강좌에서는 편의상 timer1이 10ms 간격으로 일정하게 인터럽트를 발생시키도록 설정되어 있다고 가정한다. 카운터 변수 sleepCount는 timer1 인터럽트 서비스 루틴에서 증가된다. 클러스터에 통신 데이터가 발생하면 sleepCount는 0으로 리셋되어야하므로 UART_RX 인터럽트 서비스 루틴에서 리셋하도록 코딩한다. sleepCount가 400 이상이 되면 노드는 휴면 모드로 진입해야하지만 제품을 개발하다보면 노드가 슬립 모드로 진입하기 전에 처리애햐할 일들이 여러가지 있을 것이다. 따라서 휴면진입 실행 코드는 gotoSleep() 함수를 별도로 작성해두고, timer1에서는 linStatus.bits.gotoSleep 비트만 1로 설정한다. 그리고 메인에서 이 비트를 검사하여 gotoSleep() 함수를 호출하도록 코딩한다.
int sleepCount = 0;
interrupt_service_routine timer1() // 10ms 주기 인터럽트
{
if (sleepCount<400) sleepCount++;
else linStatus.bits.gotoSleep = 1;
}
interrupt_service_routine UART_RX()
{
.....
sleepCount = 0;
}
void gotoSleep(void)
{
//휴면진입 직전에 처리해야할 코드 - 다른 통신(CAN,SPI,UART..)완료, 전원절약모드 등..
.....
// 기상 시 LIN 상태를 초기값으로 설정
linRxStep = SLEEP_STEP;
sleepCount = 0;
baud_rate = 20000;
idle();//sleep();//halt()//powerSaveMode();... // 휴면상태 진입
linRxStep = BREAKSTEP;
// 기상 직후 처리해야할 코드 - 통신 초기화, 주변장치 작동 등..
......
}
void main(void)
{
.... // 노드 초기화
while(1){
....
if (linStatus.bits.gotoSleep)
gotoSleep();
}
}
휴면상태에서 기상은 input capture 또는 UART_RX 인터럽트로 기상하도록 설정할 수 있다. 본 강좌에서는 UART_RX 인터럽트로 기상하는 것을 가정한다. LIN Protocol Spec. 5.1 기상(wake up)절을 보면 "모든 슬레이브는 기상 요청(150us이상의 도미넌트 펄스)을 검출해야하고, ....."라고 명시되어 있다.
슬레이브 노드가 baud rate를 20kbps로 설정한 후 휴면상태로 진입하면 기상 신호는 슬레이브의 UART_RX에서 0xFC보다 작은 값으로 읽혀질 것이다. 위의 코드처럼 기상 직후 RX_BUFFER를 읽어서 그 값이 0xFC보다 크다면 기상 펄스가 아니라 노이즈로 판단하고 다시 휴면상태로 진입해야한다. 따라서 위 코드 중 휴면상태 진입 부분은 다음과 같이 수정되어야 한다.
do
{
idle();//sleep();//halt()//powerSaveMode();... // 휴면상태 진입
}while(RX_BUFFER > 0xFC);
또한 프로토콜 규정에는 "기상 요청은 버스를 250us에서 5ms 동안 도미넌트 상태로 만들어서 발생된다."라는 부분이 있는데, 엄밀히 따지지면 펄스가 5ms 이상 지속되더라도 기상하지말고 휴면상태로 다시 진입하도록 코딩해야한다. 이 부분의 구현은 독자에게 미루겠다.(미안합니다.)
------------------------------------------------------------------------------------------------
강좌 4 끝.
'LIN 2.0' 카테고리의 다른 글
LIN 2.0 통신 프로그래밍 강좌 6 - 진단과 설정 구현 2 (0) | 2013.03.07 |
---|---|
LIN 2.0 통신 프로그래밍 강좌 5 - 진단과 설정 구현 1 (0) | 2013.02.27 |
LIN2.0 통신 프로그래밍 강좌 3 - 응답 송/수신 (0) | 2013.02.20 |
LIN2.0 통신 프로그래밍 강좌 2 - PID 수신 처리 (0) | 2013.02.13 |
LIN2.0 통신 프로그래밍 강좌 1 - BREAK/SYNC 처리 (0) | 2013.02.12 |