DIY - TPMS(Tire Pressure Monitoring System) value display for RENAULT and NISSAN via CAN(Controller Area Network)

 

 

 

 

세번째 글 입니다.

 

첫번째 글에서 전체 설명과 모듈 구성의 실제 사진을 주로 보았고, 두번째 글에서는 실제 결선도와 주요 기능에 해당하는 code를 함수 단위로 살펴 보았습니다.

 

이번 글에서는 타이어 압력 수치 값을 표현하는 7 segment display 를 출력하는 루틴과 악셀과 엔진 RPM을 표현하는 bar-graph display 루틴을 살펴 봅니다.

 

앞서 언급 드렸지만, LED 7 segment 를 사용한 출력을 선택한 이유는 낮/밤 시인성이 좋아서 입니다. Graphic LCD를 사용하면 좀 나을 지 모르지만 Back light 를 통해 보이는 STN LCD 특유의 희미함은 어떻게 할 수 없는 아쉬움을 남게 하더군요.

 

3자리 7 segment 5개를 사용했기에 최종적으로 총 15자리를 디스플레이 해야 합니다.

7 segment 당 8개의 출력이 있어야 하기에 단순하게 모두 직결 한다면 8*15 = 120개의 신호선이 필요 합니다만, 이렇게 하지는 않습니다.

대신 8개의 출력은 15자리 모두에 공통으로 연결 됩니다(숫자표시+점). 다만 15개의 세그먼트 중 한번에 하나의 세그먼트만 선택이 됩니다.

즉 모두 15개 중 순간적으로 멈추어 보면 한개만 표시 하는 것이죠.

이렇게 하면 총 필요 신호선은 8 + 15 = 23 선만 연결하면 됩니다.

다만 충분히 빠른 속도로 15자리를 순차적으로 보여 주어야 합니다.  그래야 사람의 눈으로 보았을 때 전체 정보가 동시에 보이게 됩니다. 올려둔 위의 동영상/사진을 보면 동시에 15자리 세그먼트가 잘 보이고 있는 것을 확인 할 수 있습니다 - SLR 카메라 등의 셔터 속도로 충분히 짧게 (1/200 이하) 하여 촬영하면 1개만 출력 되는 것을 확인 할 수 있습니다.

 

이 부분에 대한 code를 살짝 살펴 보겠습니다.

Multiplexing segment refresh 루틴은 저도 처음 짜 보는 code 라 아마도 개선할 부분이 있을 듯 합니다! (제보 환영 합니다^^)

아두이노 처럼 C++ class로 묶어 보았습니다. 8bit AVR MICOM에서 C++ 사용 할 수 있게 된 것은 정말 큰 축복! 입니다. 다만 memory(AT90CAN도 겨우 8KB)가 워낙 작아 그냥 모듈에 대한 class화 정도 만으로 만족을...

 

Display routine header file - C++ class static method sets

MultiSegmentDisplay.h

#ifndef MULTI_SEGMENT_DISPLAY_H_
#define MULTI_SEGMENT_DISPLAY_H_

#include <stdint.h>

#define SEGMENT_OUT			PORTF
#define DIGIT_0_TO_7		PORTA
#define DIGIT_8_TO_15		PORTC

#define BAR_SEGMENT_OUT		PORTB
#define BAR_GRAPH_0_TO_7	PORTE


class MultiSegmentDisplay {
public:
	MultiSegmentDisplay();
	static void clearAll();
	static void setSegmentData(uint8_t bufferIndex, uint8_t rawValue);
	static uint8_t getRawValueFromOneDigitValue(uint8_t oneDigitValue);
	static uint8_t getRawValueFromOneChar(char aChar);
	static void set3DigitValueWithDotPos(uint8_t bufferIndex, uint16_t digit3Value, uint8_t dotPosition);
	static void setString(uint8_t bufferIndex, const char* string);
	static void setAttribute(uint8_t bufferIndex, uint8_t attribute);
	static void setAttribute(uint8_t bufferIndexStart, uint8_t bufferIndexEnd, uint8_t attribute);

	const static uint8_t MAX_SEGMENT_COUNT = 15;
								//   .gfedcba
	const static uint8_t RAW_0 = ~(0b00111111);
	const static uint8_t RAW_1 = ~(0b00000110);
	const static uint8_t RAW_2 = ~(0b01011011);
	const static uint8_t RAW_3 = ~(0b01001111);
	const static uint8_t RAW_4 = ~(0b01100110);
	const static uint8_t RAW_5 = ~(0b01101101);
	const static uint8_t RAW_6 = ~(0b01111101);
	const static uint8_t RAW_7 = ~(0b00000111);
	const static uint8_t RAW_8 = ~(0b01111111);
	const static uint8_t RAW_9 = ~(0b01101111);
	const static uint8_t RAW_DOT = (uint8_t)~(0b10000000);
	
	const static uint8_t RAW_A = ~(0b01110111);
	const static uint8_t RAW_B = ~(0b01111100);
	const static uint8_t RAW_C = ~(0b00111001);
	const static uint8_t RAW_D = ~(0b01011110);
	const static uint8_t RAW_E = ~(0b01111001);
	const static uint8_t RAW_F = ~(0b01110001);
	const static uint8_t RAW_G = ~(0b00111101);
	const static uint8_t RAW_H = ~(0b01110110);
	const static uint8_t RAW_I = ~(0b00110001);
	const static uint8_t RAW_J = ~(0b00001110);
	const static uint8_t RAW_K = ~(0b01110000);
	const static uint8_t RAW_L = ~(0b00111000);
	const static uint8_t RAW_M = ~(0b01111001);
	const static uint8_t RAW_N = ~(0b01010100);
	const static uint8_t RAW_O = RAW_0;
	const static uint8_t RAW_P = ~(0b01110011);
	const static uint8_t RAW_Q = ~(0b01100111);
	const static uint8_t RAW_R = ~(0b01010000);
	const static uint8_t RAW_S = ~(0b01101101);
	const static uint8_t RAW_T = ~(0b00000111);
	const static uint8_t RAW_U = ~(0b00111110);
	const static uint8_t RAW_V = ~(0b00111100);
	const static uint8_t RAW_W = ~(0b01001111);
	const static uint8_t RAW_X = ~(0b00110110);
	const static uint8_t RAW_Y = ~(0b01110010);
	const static uint8_t RAW_Z = ~(0b01011011);
	const static uint8_t RAW_BLANK = ~(0b00000000);
	
	//: ; < = > ? @						   .gfedcba
	const static uint8_t RAW_COLON =	~0b01000001;
	const static uint8_t RAW_SEMICOLON =~0b01000101;
	const static uint8_t RAW_SMALL =	~0b01011010;
	const static uint8_t RAW_EQUAL =	~0b00001001;
	const static uint8_t RAW_GREAT =	~0b01101100;
	const static uint8_t RAW_QUESTION =	~0b01010011;
	const static uint8_t RAW_AT =		~0b01011111;

	//[ \ ] ^ _ '
	const static uint8_t RAW_SQUARE_BRACKET_LEFT = ~0b00111001;
	const static uint8_t RAW_BACK_SLASH = ~0b01100100;
	const static uint8_t RAW_SQUARE_BRACKET_RIGHT = ~0b00001111;
	const static uint8_t RAW_HAT = ~0b00000001;
	const static uint8_t RAW_UNDER_LINE = ~0b00001000;
	const static uint8_t RAW_SINGLE_QUOTATION = ~0b00100000;
	
	// '|'
	const static uint8_t RAW_BAR = ~0b00110000;
	// + , -
	const static uint8_t RAW_PLUS = ~0b01000110;
	const static uint8_t RAW_MINUS = ~0b01000000;
	
	const static uint8_t ATTR_BLINK = 0b10000000;			// Blink
	const static uint8_t ATTR_BLINK_FAST = 0b11000000;		// Fast blink
	const static uint8_t ATTR_BLINK_FOREVER = 0b00111111;	// Not 63 secs but forever blink
	
	
	//----- Bar graph
	const static uint8_t MAX_BAR_SEGMENT_COUNT = 5;			// 8x5 = 40 LED
	static void setAccelerator(uint16_t accelerator);				// max 10bit = 1024 (Not linear)
	static void setRpmData(uint16_t down, uint16_t current, uint16_t up);	// Set rpm values
	
public:
	static uint16_t tick_timer;
	static uint8_t segmentOutBuffer[MAX_SEGMENT_COUNT];			// For fast operation it declared as public.
	static uint8_t segmentAttributeBuffer[MAX_SEGMENT_COUNT];	// Attribute for display
	static uint8_t blinkDelayCount;								// Internal blinking timing count
	
	static uint8_t barGraphSegmentOutBuffer[MAX_BAR_SEGMENT_COUNT];
	static uint8_t barGraphSegmentAttributeBuffer[MAX_BAR_SEGMENT_COUNT];
	
	static uint8_t maxAcceleratorIndex;
	static uint16_t maxAcceleratorIndexUpdateTickTime;
	static bool startMoveDown;
	
	static uint8_t downShiftPointSegmentIndex;
	static uint8_t downShiftPointOffsetIndex;
	static bool	 downShiftPointDimming;

private:
	static uint8_t charMappingRawData['}' - '0'+ 1];
	static void initTimer0ForSegmentRefresh();
	static void initTimer2ForAttributeUpdate();
	static void initPort();
};

#endif /* MULTI_SEGMENT_DISPLAY_H_ */

상단에 LED 제어용에  할당한 Atmega 주요 port를 선언 하고, .cpp에서 해당 이름으로 사용 하게 됩니다.

이 class는 실제 singleton 으로 처리해서 사용 해야 하겠지만, MICOM에서 getInstance 함수 만들고 중복 생성 막고 하는 code를 넣는 것은 너무 과한 것이라 한번만 만들고 static method 직접 사용 하는 것을 기본으로 했습니다. (PC가 아닙니다!)

 

상단 부분에 7 segment 15자리 출력 제어용 함수들이 모여 있고 (clearAll() ~ setAttribute()) 그 다음에 7 segment 출력 선에 맞는 문자 data를 선언 했습니다. 알파벳 문자도 7 segment 한계에 맞추어 출력 하게 됩니다.

 

그리고 Bar graph 용 함수가 위치 합니다(setAccelerator(), setRpmData)

아래 부분에 public 으로 공개된 static member variable이 위치 합니다. 여기서도 당연히 일반 C++라면 member variable은 private 영역이어야 하겠지만 MICOM + 1ms 도 아까운 multiplexing display routine이라 그냥 public 처리하고 각종 interrupt 함수를 비롯한 주요 함수에서 바로 접근 하도록 설정 한 것입니다. (public/private 개념을 몰라 이렇게 둔 것은 아니니 감안 해서 봐 주세요)

 

이후 private method로 내부에서만 불리는 함수를 두었습니다.

 

이제 MultiSegmentDisplay.cpp 파일을 보겠습니다.

Constructor 

MultiSegmentDisplay::MultiSegmentDisplay()
{
	clearAll();
	initTimer0ForSegmentRefresh();
	initTimer2ForAttributeUpdate();
	initPort();
}

clearAll() 에서는 segment 출력 buffer 를 표시가 되지 않게 기본 값으로 둡니다.

initTimer0ForSegmentRefresh(), initTimer2ForAttributeUpdate() 는 주기적인 갱신을 위한 timer를 초기화 하게 됩니다.

마지막 initPort() 는 LED 출력에 할당된 port를 초기화 - 첫 출력값을 설정 합니다.

 

 

타이머 설정 함수 두개만 먼저 보면,

/*******************************************************************************
 7 Segment update timer initialization
*******************************************************************************/
void MultiSegmentDisplay::initTimer0ForSegmentRefresh()
{
	cli();
	TCCR0A = _BV(WGM01) | _BV(CS01) | _BV(CS00);	// 0b00001011WGM01 for CTC and Clock bit 011 (/64) 
	OCR0A = 64;	//125;		// 16,000,000 / 64 / x = 2,000 | x = 125
	TIMSK0 = _BV(OCIE0A);	// 0b10 – OCIE0A: Timer/Counter0 Output Compare Match A Interrupt Enable
	sei();
}

/*******************************************************************************
 Setup the timer 2 for update the attribute with timeout
*******************************************************************************/
void MultiSegmentDisplay::initTimer2ForAttributeUpdate()
{
	// Long term timer for attribute time out looping logic
	// 16MHz 0.0625us to /1024 then  --> 15,625 Hz
	// / 255(OCR2A) then 61.03 Hz
	// / 156(OCR2A) then 100.16 Hz
	cli();
	TCCR2A =0b00000111;				// /1024  [CS22,CS21,CS20] = 111
	OCR2A = 155;					// /156 then 100.16Hz --> + 10 count then 10Hz
	TIMSK2 = TIMSK2 | (1<< OCIE2A);	// Invoke interrupt on OCR2A matched
	sei();
}

initTimer0ForSegmentRefresh() 함수 - 15자리 7 segment 한개 문자 refresh를 위한 interrupt 설정

TCCR0A = _BV(WGM01) | _BV(CS01) | _BV(CS00); 

Atmega 의 8-bit Timer/Counter Register 를 설정 하는 부분입니다.

TCCR0A (Timer/Counter0 Control Register A) 값을 설정하여 순수 timer interrupt 를 발생 시키게끔 설정 하는게 목적 입니다. 

Timer mode 2 (Clear Timer on Compare match (CTC) mode) 선택을 위해 WGM01 = 1 / WGM00 = 0 으로 설정 합니다. [!]이 두 bit는 연속 하지 않습니다 

그리고 timer 증가 clock source 설정 하기 위해 나머지 하위 bit 3개인 CS02 / CS01 / CS00 를 설정합니다. code 상에서는 011 을 설정하여 현재 clock 기준으로(System Clock Prescaler - CLKPR 참조) 으로 /64 분기를 사용 합니다.

16MHz 를 사용 중이므로 16,000,000 / 64 = 250,000 Hz 값이 나오고 counter 는 250KHz 주기로 증가(+1) 하게 됩니다.

앞서 Timer mode 2 는 counter(TCNT0) 가 기본으로 매 주기(앞서 250KHz로 설정)마다 1 값이 증가 합니다. 이 증가 값이 OCR0A (Output Compare Register A)에 지정한 값이 되면 interrupt가 발생하게 사용할 수 있습니다.

** _BV() 는 해당 bit index 에 해당 하는 bit 값을 만들어 주게 됩니다. (AVR compiler only) 

 

OCR0A = 64;

다시 말에 앞서 250KHz 주기당 1씩 증가하는 값에 대해서 OCR0A 를 적절히 설정하면 250KHz / OCR0A 값의 주기 마다 interrupt가 발생하도록 세밀한 조절을 할 수 있게 됩니다.

code에서는 64 값을 사용 했으므로 250,000 / 64 = 3,906.25 Hz 가 나오게 됩니다.

나중에 보겠지만 총 15자리 수에 대해 이 주기 값을 사용 하게 되므로 15자리 모두 디스플레이(한 장면 모두 갱신)에는 3,906.25 / 15 = 260 Hz 라는 수치가 나오게 됩니다. 즉 15자리 display 모두 출력하는데 약 1/260 sec 가 걸리게 된다는 의미 입니다. 형광등의 1/60 보다 4배 정도의 속도 이므로 고정된 시점에서는 깜빡임을 느끼지 않을 만한 속도가 되겠습니다.

(디스플레이를 막 흔들어 보거나 눈동자를 주우로 움직여보면 약간의 떨림을 느낄 수는 있답니다)

 

TIMSK0 = _BV(OCIE0A); 

우리는 OCR0A 의 설정한 값이 match 값과 동일하면 interrupt를 발생 시키는 것을 원하기에

OCIE0A : Timer/Counter0 Output Compare Match A Interrupt Enable 를 설정 합니다.

 

initTimer2ForAttributeUpdate() 함수 - 7 segment 의 깜빡이 효과를 제어하기 위한 timer를 설정

특정 값이 바뀌었음을 알기 위한 가장 효과 적인 방법은 해당 부분을 강조 하면 되는데 깜빡이거나 색상을 달리 하면 효과 적입니다. 우리는 단색 7 segment 를 사용 중이므로 깜빡임 처리 만큼 효과 적인 선택은 없을 듯 합니다.

이 깜빡임을 위한 효과를 위하여 별도의 timer를 설정 하는 것으로 앞서 Timer0 설정 한 것과 매우 유사하게 설정 합니다.

초당 10번 까지의 깜빡임 혹은 타이밍 계산을 위해 최종적을 10Hz 호출 주기의 interrupt 소스를 만들게 됩니다.

상세 내용은 앞서 timer 0 와 유사하기에 추가 설명은 생략 합니다.

(음... 이 글 쓰면서 code를 확인하니 mode를 2 로 설정하지 않고 있네요 ^^ / 향후 다시(!) 만들 일 있으면 해당 부분 재 확인 하겠습니다)

 

 

타이머 인터럽터 핸들러를 보기 전에 15문자를 buffer에 기록 하는 부분을 먼저 보겠습니다.

최종 출력을 위해서 static으로 잡혀 있는 buffer에 문자를 기록 하게 되고, 인터럽트 루틴에서 buffer에 있는 raw한 value를 port에 적절히 출력 지정 하게 됩니다.

 

static uint8_t getRawValueFromOneChar(char aChar) : 문자 -> Port 출력 8bit 변환 루틴

/*******************************************************************************
 '0' ~ '9'		: 48 ~ 57
 : ; < = > ? @	: 58 ~ 64
 'A' ~ 'Z'		: 65 ~ 90
 [ \ ] ^ _ '	: 91 ~ 96
 'a' ~ 'z'		: 97 ~ 122
 { | }			: 123 ~ 125
*******************************************************************************/
uint8_t MultiSegmentDisplay::getRawValueFromOneChar(char aChar)
{
	uint8_t result = RAW_BLANK;
	if (aChar >= '0' && aChar <= '}') {
		result = charMappingRawData[aChar - '0'];
	} else if (aChar == '+') {
		result = RAW_PLUS;
	} else if (aChar == '-') {
		result = RAW_MINUS;
	}
	return result;
}

aChart는 ASCII code 한문자로 받아 7 segment common output bit값을 돌려 받습니다. code에서 보는 것 처럼 지정한 범위 내에서 준비한 bit 값으로 변환하는 것이 모두 입니다.

모르는 문자라면 그냥 빈 문자(아무 표시 없음)를 보여 주게 됩니다.

나머지 함수들은 크게 어려운 내용이 없어 추가 설명은 생략 하겠습니다.

 

 

15자리 7 segment + Bar graph 출력을 위한 인터럽터 핸들러 

/*******************************************************************************
 Update 1 digit 7 segment display.
 It need 15 times for update all digit.
*******************************************************************************/
SIGNAL( SIG_OUTPUT_COMPARE0 )
{
	//static uint16_t tick_timer = 0;

	//---------------------------------------- 7 segment section
	static uint8_t digitIndex = 0;
	uint16_t activeSegment = 1 << digitIndex;
	SEGMENT_OUT = 0xFF;	// Turn off all LED before change digit scanning (prevent crosstalk)
	DIGIT_0_TO_7 = activeSegment & 0x00FF;
	DIGIT_8_TO_15 = (activeSegment & 0xFF00) >> 8;

	uint8_t& attr = MultiSegmentDisplay::segmentAttributeBuffer[digitIndex];
	bool showOrNot = true;
	if (attr & MultiSegmentDisplay::ATTR_BLINK) {
		if ((attr & MultiSegmentDisplay::ATTR_BLINK_FAST) == MultiSegmentDisplay::ATTR_BLINK_FAST) {
			showOrNot = (MultiSegmentDisplay::tick_timer % 300 > 150);
		} else {
			showOrNot = (MultiSegmentDisplay::tick_timer % 2000 > 1000);
		}
	}
	if (showOrNot) {
		SEGMENT_OUT = MultiSegmentDisplay::segmentOutBuffer[digitIndex];
	}

	// Shift to next LED segment	
	if (++digitIndex >= MultiSegmentDisplay::MAX_SEGMENT_COUNT) {
		digitIndex = 0;
	}
  

앞서 타이머는 3,906.25 Hz 주기로 호출 되게 됩니다. 약 4KHz로 호출이 됩니다만 이 빈도로는 1개의 7 segment 문자를 표시 하는 시간입니다. 15자리를 모두 출력해야 하기에 앞서 설명처럼 3,906.25 / 15 = 260Hz 가 되게 됩니다.

 

static uint8_t digitIndex = 0;

최초 0번 위치부터 출력을 시작 하고 14까지 증가 하다가 다시 0으로 돌아 가서 15자리 문자를 계속 적으로 돌아가면서 표시 하는 index값으로 사용 합니다.

 

uint16_t activeSegment = 1 << digitIndex;

15자리 중 하나만을 active하게 선택 되어야 하기에 16bit 범위의 port출력용 binary 값을 설정 합니다.

이 값은 8bit port 두개로 나누어 출력 하게 됩니다. 아래 두 code.

DIGIT_0_TO_7 = activeSegment & 0x00FF;

 

DIGIT_8_TO_15 = (activeSegment & 0xFF00) >> 8;

 

if (attr & MultiSegmentDisplay::ATTR_BLINK) {

Attribute에 깜빡임 속성을 가지고 있다면 깜빡임 처리를 하는 부분으로서 timer 에 의해서 증가하는 tick counter 를 이용하여 주기별로 깜빡임 처리를 하게 됩니다.

 

위의 code에 연속하여 아래 code가 Bar graph 용으로 위치 합니다.

	//---------------------------------------- Bar-graph section
	static uint8_t barIndex = 0;
	uint8_t activeBartSegment = 1 << barIndex;
	BAR_SEGMENT_OUT = 0xFF;	// Turn off before shift the segment
	BAR_GRAPH_0_TO_7 = activeBartSegment;
	
	BAR_SEGMENT_OUT = MultiSegmentDisplay::barGraphSegmentOutBuffer[barIndex];
	// Additional effect for specific bit
	if (MultiSegmentDisplay::downShiftPointSegmentIndex) {
		if (barIndex == MultiSegmentDisplay::downShiftPointSegmentIndex) {
			bool bitShowOrNot = MultiSegmentDisplay::downShiftPointDimming ?
							(MultiSegmentDisplay::tick_timer % 50 > 40) : (MultiSegmentDisplay::tick_timer % 500 > 250);
			if (!bitShowOrNot) {
				BAR_SEGMENT_OUT |= (uint8_t)1 << MultiSegmentDisplay::downShiftPointOffsetIndex;
			}
		}
	}
	
	
	if ((!MultiSegmentDisplay::startMoveDown) &&
		(MultiSegmentDisplay::tick_timer == MultiSegmentDisplay::maxAcceleratorIndexUpdateTickTime)) {
		MultiSegmentDisplay::startMoveDown = true;	
	}
	if (MultiSegmentDisplay::startMoveDown) {
		static bool tickTock = false;
		if (tickTock != (MultiSegmentDisplay::tick_timer % 1000 > 500)) {
			tickTock = !tickTock;
			if (MultiSegmentDisplay::maxAcceleratorIndex > 0) {
				--MultiSegmentDisplay::maxAcceleratorIndex;
			}
		}
	}
	
	if (MultiSegmentDisplay::maxAcceleratorIndex) {
		static const int16_t aceelToBarLevel[11] = {	// LSB bit is left most position in real LED bar.
			~0b0000000001,	// Full accelerator
			~0b0000000010,
			~0b0000000100,
			~0b0000001000,
			~0b0000010000,
			~0b0000100000,
			~0b0001000000,
			~0b0010000000,
			~0b0100000000,
			~0b1000000000,
			~0b0000000000,	// No accelerator
		};
		
		if (barIndex == 0) {
			BAR_SEGMENT_OUT &= aceelToBarLevel[10-MultiSegmentDisplay::maxAcceleratorIndex] & 0xff;
		} else if (barIndex == 1) {
			BAR_SEGMENT_OUT &= (uint8_t)(aceelToBarLevel[10-MultiSegmentDisplay::maxAcceleratorIndex] >> 8);
		}
	}
	
	if (++barIndex >= MultiSegmentDisplay::MAX_BAR_SEGMENT_COUNT) {
		barIndex = 0;
	}
	
	//---------------------------------------- Common section
	if (++MultiSegmentDisplay::tick_timer >= 60000) {
		MultiSegmentDisplay::tick_timer = 0;
	}
}

기본적인 동작은 7 segment 15 자리 출력과 같습니다 - 8bit을 공용으로 5번을 나누어 총 40개의 LED를 제어 하게 하는 것이죠.

다만 추가적인 effect 처리를 위한 기술이 약간 가미가 됩니다.

 

RPM 표시에서 down shift RPM 부분을 현재 RPM 에서 떨어진 위치에 어둡거나 깜빡이게 표시하기 위해 아래 code가 사용 됩니다. (실 동작은 동영상 잘 보아 주세요^^)

어둡게(클러치가 놓여 진 경우): (MultiSegmentDisplay::tick_timer % 50 > 40 

  -> PWM제어 처럼 약 20% duty cycle을 사용하여 이론상 1/5 가도로 LED 밝기가 조절 됩니다.

깜빡이게(클러치를 밟은 경우): (MultiSegmentDisplay::tick_timer % 500 > 250) 

  -> 좀더 길게 시간을 띄워서 PWM제어보다는 빠른 깜빡임으로 50% 비율로 보이게 합니다.

 

나머지 code부분은 Accelerator 에서 발을 떼었을 경우 직전 최고 위치에서 아래 단계로 LED 한개를 스스륵 아래로 이동 시키는 모습을 보여 주는 부분입니다. Audio graphics equalizer 처럼 최대 높이에 대해서 아래로 밀려 오는 그 모습을 code로서 만든 것입니다.

총 10bit를 차례로 움직이게 하는 노력이 포함 되어 있습니다. 말로 설명하기에 code보다 길어 질 듯 하여 이 부분은 직접 보시고 이해 해 주셨으면 합니다. 아마도 추가 optimize가 필요 할 수 있을 듯 합니다.

 

 

 

이상 주요 code 부분을 확인 했습니다. bar graph 부분 등은 이미 공유 드린(두번째 글에서 다운로드 가능 합니다) 소스 code에서 직접 확인 하시기 바랍니다.

더 많은 것을 설명 드렸으면 하지만 이것으로 마무리 하고자 합니다. 향후 변동 사항 혹은 개선 내용이 있다면 update 하도록 하겠습니다.

 

2년 전 시작한 DIY project를 결국 마무리를 했습니다. 시원섭섭? - 좀 더 작게 만들고 싶은 욕심은 있지만 당분간은 제 차에서 최초의 목적을 잘 수행 하리라 생각 됩니다.

덕분에 타이어 공기압을 조절 해야만 한다는 압박을 받고 있습니다. 날이 조금 시원해 지니 절로 타이어 압력이 10% 가량 줄어 들었음을 바로 바로 확인 할 수 있는 덕이죠.

 

감사합니다.

 

DIY - TPMS(Tire Pressure Monitoring System) value display for RENAULT and NISSAN via CAN(Controller Area Network)





두번째 글 입니다.

이 글에서는 전체적인 모듈 구성도, Display 처리 logic 을 제외한 CAN data 처리 부분 code까지 살펴 봅니다.


먼저 모듈 구성도를 보겠습니다. code 구성 전에 회로를 구성하게 되고 회로에 맞추어 code를 구성해야 합니다. 물론 구성 전에 개별 port를 할당과 연결은 미리 고민 후 할당 해야겠습니다.

저의 경우 AT90CAN128 모듈의 포트 위치에 따라서 TPMS 표시 모듈을 먼저 구성하여 포트가 연속이지는 않지만 해당 모듈의 기판에서는 물리적으로 연속하여 배치 되어 실제 pin header 로 연결 용이성에 최우선 순위로 결정 했습니다.

추가로 처리한 Accelerator / RPM bar-graph는 남는 반대편 port에 구성 하였습니다.

실제 모습은 첫번째 글을 참고 하시면 되겠습니다.


[모듈 구성도] 


OBDII Socket 

4선을 연결 하면 됩니다. 저의 경우 고장난 충전용 5pin USB 케이블의 선재만 이용 했습니다.

+ / - / D+/ D- 를 그대로 활용하여 VCC(8) / GND(4,5) / CANH(6) / CANL(14) 와 연결 합니다.

여기서 주목 할 사항은 Renault / Nissan 차량에서 OBDII Socket의 8번 pin을 사용하면 항시 전원이 아닌 시동 전원(IGN2)에 연결하게 되는 것입니다.

혹시나 ELM327 호환 OBDII 장치에 대해 상시전원을 막으려면 16pin 연결된 전원 선을 8pin으로 옮기면 방전 걱정을 덜게 됩니다.

아! OBDII 소켓은 별로로 구입해서 선을 연결해야 됩니다. 저는 이 소켓에 regulator까지 함께 장착 했습니다.


Switching regulator 

차량 전원은 +12 ~ 14.8 V 의 고전압(TTL회로에 적합하지 않은) 입니다. 직접 AT90CAN128 Module에 연결할 수 없고, 5V 로 낮추어 공급 해야 합니다.

요즘은 OBDII Socket 내부에 넣을 정도로 작은 switching regulator 모듈을 판매하고 있습니다. 이러한 부품을 사용 합니다.

7805 같은 linear regulator 는 여러 이유로 추천 드리지 않습니다. (12V 에서는 고열로 인한 방열 문제 등등)


AT90CAN128 module 

전원 +5V 는 앞서 switching regulator와 연결하고, CAN 라인은 Socket 으로부터 직접 연결 하면 됩니다. (6, 14 번 핀)

Tire 공기압 표시를 위한 7 segment 모듈과 연결은 Port F / A / C 를 통합니다. 총 24pin 연결하도록 2열 pin header , socket 으로 기판을 직접 결합 하게 됩니다.

Port B / E 는 accelerator, RPM gauge bar graph 표현 모듈과 연결 합니다. 8bit는 공통이고 총 4개 모듈을 제어 하게 합니다. 마찬가지로 4번 각각 선택 해야 총 40개의 LED를 표현하게 됩니다.


7 segment display module 

Port F 와여 연결선은 모든 7 segment 모듈에 공통으로 연결합니다. (한 시점에 한개의 자리만 표시)

Port A, C 를 개별 7 segment 하나의 단위와 차례로 연결하여 총 15자리 수를 순차적으로 선택 하게 합니다.


HW 준비 설명은 이렇게 서둘러(!) 마무리 하겠습니다.

100% 똑같이 만들기는 곤란하기에, 핵심만 전달 드립니다. (포트 역할, 전체 결선 방법)



AT90CAN128 을 활용한 CAN data수신 

AT90CAN128 은 칩 이름만 보면 아시겠지만 CAN 통신 기능이 이미 chip 수준에 내장된 MICOM입니다. 다만 물리적인 CAN 라인과 연결한 트랜시버는 추가로 부착 하는 정도의 수고가 필요 합니다. 제가 사용한 모듈에는 이미 부착 되어 단지 차량 CANH, CANL 라인과 연결만 하면 됩니다.

하지만 CAN 통신 처리가 그냥 되지는 않고, Atmel 에서 제공하는 규약에 맞추어 register 설정, interrupt 설정 등을 통해 동작이 되어야 하는데 - Atmel 에서 제공하는 library가 - 시원찮습니다. 특히나 최신 Atmel studio 에서는 제대로 import 하는 것만 해도 일이더군요.

그래서 별도의 library를 독일의 한 community(https://www.mikrocontroller.net/topic/98697) 에서 구하여 약간의 수정으로 사용하게 되었습니다.


**별도 압축파일로 제공하는 Atmel Studio용 project파일의 can.h / can.c 파일을 참고 하시기 바랍니다.

AT90CAN_TPMS_2016-09-06_003533.7z




Main 함수

인트로 보여주기, 안내 메시지, can 초기화(InitCAN()), CAN data 받기용 interrupt 설정(set_interrupt_handler) 과정을 차례로 수행 합니다.

그리고 실제 동작은 interrupt 루틴인 canInterruptHander 함수에서 하고, main은 while (true)로 대기 상태(무한 loop) 처리가 됩니다. [MICOM은 무한 루프라고 열내거나 처리하다 죽지 않습니다^^]



CAN 초기화 - QM5/KOLEOS는 일반적인 속도인 500kbps를 사용 합니다. 

초기화를 완료 하면 segment display로 잠깐 결과를 보여 주게 됩니다(이건 나중에 별도 설명)



CAN data 수신 interrupt handler  - 모든 data를 수신 처리 합니다.

CAN data가 수신 될 경우 - 사실 mask 를 0으로 처리 해서 CAN으로 뿌려지는 모든 data가 수신 되게 됩니다. AT90CAN128은 충분한 속도와 반응으로 수신 처리를 하게 됩니다.

can_get_id() 함수를 통해서 CAN id를 받고, data는 can_get_data() 함수로 8 byte 배열에 수신된 data를 받게 됩니다.

이후는 잘 '요리'하는 일이죠!


우리의 관심 data인 TPMS 수치 값은 CAN id 0x385 를 가지며 길이는 8 입니다.

차량 속도: CAN id 0x284 

스티어링 앵글(운전석 핸들의 각도): CAN id 0x002 

Throttle(Accelerator), RPM : CAN id 0x180 

기어 중립 상태 여부 : CAN id 0x358 


** QM5/KOLEOS CAN data 별도 페이지로 안내 드릴 예정 입니다.

우선 TPMS CAN data 수신에 대한 처리 부분을 좀 더 보도록 하겠습니다.



TPMS data 수신 후 처리 - checkTPMSAndDisplay() 함수

8byte data 중 3번째~6번째 byte data가 각각 FR, FL, RR, RL 의 타이어 압력 값이며 code에서 보는 것과 같이 raw 수치에 0.25 를 곱한 값이 최종 압력 단위 psi(pound per square inch / 제곱 인치 당 파운드) 값이 됩니다. 즉 resolution이 0.25 psi가 됩니다.


* 10.0f 한 것은 7 segment display에서 3자리만 출력 되는 관계로 소수점 1자리만 유효하고 그에 대한 scale 처리를 한 것입니다. 예로서 32.25 라면 32.2 만 표현 할 수 있는 상태고, 실제 display 루틴은 정수 값만 받아 처리 하도록 두어서 *10 하여 최종 "322"를 만들어 전달 합니다.

물론 출력할 때 "." 위치를 마지막 자리 수 앞에 두어 최종 결과는 "32.2" 로 보이게 만듭니다.


이 최종 출력은 showATPMSValue 함수에서 처리를 하게 됩니다.

약간의 Magic(마법?!)이 이 함수에 포함 되어 있습니다. 

먼저 이전 값을 기억 했다가(static 변수) 수치가 변동 되는 시점에 값을 갱신함과 동시에 깜빡임 처리를 하게 됩니다. 이 깜빡임은 bit flag MultiSegmentDisplay::ATTR_BLINK 으로 표현되고, "4" 값에 의해 지속 시간이 전달 됩니다. 실제 처리는 MultiSegmentDisplay class의 timer interrupt 에서 약 4초간 깜빡이게 사전 code가 구성 되어 있습니다. (다음 글에서 이 부분을 더 자세히 볼 예정 입니다)



속도 표시 - displaySpeed() 함수

차량의 현재 속도는 CAN id 0x284 에 담겨져 오고 5번째, 6번째 byte data의 조합으로 계산 됩니다.

계산 수식은 Speed = (D5*2)+(D6/128) 로 처리 됩니다. 실제 code상에서는 소수점 2자리를 표현하기 위해 100배의 값이 두 byte로 표현 하는 것으로 아래 처럼 계산처리가 가능 한 것입니다.

  speedx100 = (data[4] << 8 | data[5]);


이곳에서도 Magic(?!) 으로 소수점 단위의 절반 값에 대해서만 값 변동을 처리하여 빈번한 속도 표시 변동을 막는 기법을 사용 하고 있습니다.

표시는 7 segment 중간에 있는 3자리 수에 표시를 하게 됩니다.



스티어링 앵글(운전석 핸들 각도) - displaySteeringAngle() 함수

이 부분은 매우 큰 관심을 받을 만한 값으로 생각 됩니다. 

이미 제 블로그에 공유 드렸던 "[DIY] 차량용 코너링 램프 컨트롤러 제작" 에서 휠각을 판단하기 위해 별도의 센서를 설치하는 고난도(?)의 DIY를 소개 드렸는데, CAN 통신을 통해서 한번에 휠각을 실시간으로 0.1 도의 정확도로 받아 올 수 있는 길이 있는 것입니다.

이번 TPMS diy를 완성 하기 까지 여전히 제 차에는 앞서 직접 DIY한 코너링 램프 컨트롤러가 동작 중입니다. 언젠가는 CAN 연결을 통한 제어로 바꿀 수도 있을 것 같습니다 (귀찮아서 언제 쯤....)



수동 기어 단수 표시, Accelerator, RPM 상태 표시 - checkEngineParameter() 함수

처음 계획은 TPMS로 부터의 개별 tire psi 값 출력이었습니다. 항상 그렇지만 하나가 되니 추가로 무언가 하고 싶더군요(실은 예전 부터 계획했고, 부품도 함께 구입 했다는).

특히 AT90CAN128의 엄청난 수의 port를 남겨 둘 수 없는 마음에 - 가속 패달(Accelerator)과 엔진 회전수(RPM)을 그럴 듯 한 Bar LED 모듈를 사용한 bar graph 로 표현하고자 하여 만든 함수 입니다.

RPM, 가속 페달 값은 아래와 같이 계산이 되는 것입니다.

    uint16_t RPM = (data[0] << 7 | data[1]) >> 2; // Engin RPM = (D1*128+D2)/4

    uint16_t throttle = (data[5] << 2) | ((data[6] & 0xC0) >> 6);


특이점으로 수동 기어 단수를 현재 RPM과 속도로 부터 유추하여 디스플레이어 가운데 부분에 보여 주는 code도 담겨져 있습니다.

자동이던 수동이던 기어비는 이미 정해 져 있습니다(무단 변속기 제외). 이 기어비 값을 현재 RPM과 속도로 부터 역으로 유추하여 보여 주는 것이죠! - 단 클러치를 밝고 있는 경우 속도와 RPM은 비례 관계가 아니므로 정확한 실제 기어 위치를 알 수는 없습니다. 하지만 충분히 실용적인 표시가 가능 하고, clutch 동작 시점에만 표시 하기에 엉뚱한 값 표시를 최대한 억제 하게 됩니다.

해당 함수는 getTransmissionInformation() 를 살펴 보면 되겠습니다.



이상 주요 함수를 돌아 보았습니다.

다음 글에서는 7 segment 15개와 bar LED 10 x 5 에 대한 driver code구성을 살펴볼 예정 입니다.


AtmelStudio 7로 작성한 project와 source 코드는 앞서 언급 드린 첨부 파일을 다운로드 받으셔서 컴파일, 실행, 확인을 하실 수 있습니다.

AtmelStudio 7 은 Atmel에서 배포하는 무료 AVR 통합 환경 입니다. Visual studio IDE를 사용하여 만들어 졌고 AVR 개발에는 매우 유용한 도구 입니다.


AT90CAN_TPMS_2016-09-06_003533.7z


**개인 사용자라면 누구나 자유롭게 사용 가능 하며 출처 표기 정도는 해 주시면 감사 하겠습니다.

  단, 상업적인 목적으로는 허용하지 않습니다.



DIY - TPMS(Tire Pressure Monitoring System) value display for RENAULT and NISSAN via CAN(Controller Area Network)


직접 만드는 TPMS 값 표시기 입니다 - QM5/KOLEOS 용 이기는 한데 RENAULT, NISSAN 차량에 동일 CAN ID를 사용하는 차량이라면 호환 가능 할 것으로 예상 합니다.


*TPMS 장착 차량에서 개별 타이어 공기압 수치를 보기 위한 장치를 만드는 것이며 TPMS 미 장착 차량에 대한 추가 장착(개조)은 아닙니다. CAN ID 0x385 를 사용한다면 다른 차량에서도 정상 동작 할 듯 합니다.


[사진1] DIY TPMS module (케이스 만들기 전 마지막 확인 과정 중 촬영 / 클릭하면 원본 크기)
  오른쪽 Bar LED는 최초 계획에서 추가한 Accelerator, RPM 표시기 입니다(Shift down indicator 포함)

DIY - TPMS value display module


[동영상]


QM5(KOLEOS)에는 TPMS(Tire Pressure Monitoring System)가 선택 혹은 기본 사양으로 장착 되는 차량 입니다. 현 시점 기준으로 9년을 넘기고 있는 모델임에 불구하고 국내 차량 기준으로는 좀 빠른 시점에 장착이 되었다고 볼 수 있습니다 (BMW 같은 차량은 이미 오래오래 전에 기본 장착을 하고 있었죠)


그런데 QM5 TPMS display는 계기판에서 전담하고 있는데, 여러 글자를 표시할 수 있음에도 불구하고 개별 타이어의 공기압은 제공 해 주지 않습니다. 다만 공기압이 기준 수치 보다 낮아 졌을 때 경고 메시지 혹은 파열등에 의한 공기압 미확인 메시지 등만 제공을 하고 있죠.

아래 사진 처럼 말이죠.


[사진2] 뒷쪽왼편(RL) 타이어 공기압 부족 상황(이때 나사못(피스)에 의해 공기가 조금씩 빠지던 상황)

QM5/KOELOS TPMS warning messageTire pressure adjust needed


기억은 정확하지 않은데, 25psi 정도로 떨어져야 저 메시지를 보여 주게 됩니다.

이미 너무 많은 공기압이 빠진 상태에서 저 메시지를 보게 됩니다. 사전에 TPMS 에서 보고하는 수치값을 보여 준다면 미리 대비할 수 있을텐데 말이죠.

특히 비교적 최근 차량인 SM5의 경우 MMI 선택에 의해서 현재 개별 타이어 공기압 수치 확인이 가능 합니다.


그래서 TPMS 역시 별도의 MICOM으로 구성 된 것이 확인 되고(당연히) 이 또한 CAN network상에 연결 되어 있습니다. CANCLIP 으로도 확인이 가능한 내용이랍니다.


[사진3] Renault 진단 장비인 CANCLIP 에서 확인한 현재 공기압

  (RL만 1.57 bar == 23 psi 정도로 너무 낮은 상태)


위의 사진2 / 사진3과 같은 상황에서와 같이 실 사용에서 현재 공기압이 무지 궁금하고 그렇다고 매번 CANCLIP 장비(노트북 + 별도 연결용 CANCLIP 기기)를 연결해서 확인 해야 하기에 별도 TPMS display module 을 만들고자 한 꿈으로 이렇게 사진1과 같은 최종(케이스 뺀) 물건을 만들기에 이르렀습니다.


LCD로 보기에는 시인성이 떨어지기에 소형 LED 7 segment 를 여러개 조합하여 바퀴 4개에 대한 TPMS 값을 확실히 보이게 만드는 것이 목적 입니다.


사용한 모듈과 부품 등등은 아래와 같습니다.

* AT90CAN128 Module - Interboard 에서 만든 AT90CAN128BK2-ECO 모듈(CAN 트랜시버 내장)

* 3 자리 7 segment - Aliexpress 에서 찾은 매우 작은 크기의 3자리 표시 모듈

  (RED 3 Bit Digital Tube 0.25 inch LED Display 15*8*4mm)

* 만능 기판 - 3자리 7 segment 총 5개를 적절히 배치 할 크기

* 모듈과 만능기판을 연결할 2pin 핀과 핀헤더 소켓

* 배선용 배선

* 추가: 가속패달 표시와 RPM 표시(+Shift down 인디케이션)를 위한 10bit bar-graph LED 모듈



AT90CAN128 Module 은 아래 처럼 생겼습니다 - COM port 기판 표시와 실제 번호가 다른 것으로 기억.

[사진4] AT90CAN128 module


원래는 회로도를 그리고 하려고 했으나...

특정 port를 통으로 7 Segment LED에 연결 하는 것이 핵심이라... 넘어가고(2년 동안 해서 지쳤습니다)

작업 사진 몇 개와 실제 중요 code 구성을 안내 드리고자 합니다.

동일하게 작업하기에는 쉽지 않기에, 기본을 알려 드리고 참고 자료로서 개인별 응용이 더 맞을 듯 합니다.


위 보드만 있으면 남은 일은 전원, CAN 연결, LED 배선이 남은 일입니다. 물론 프로그래밍이 남았죠. 이 부분은 아래 설명과 함께 첨부 파일을 보신다면 절반은 해결 한 것이라고 해도 과언이 아닐 듯 합니다.


LED 배치와 배선 작업은 아래처럼 시작과 끝을 내었습니다.

무려 3x5 = 15 자리 segment를 제어 해야 하는데, 밝기를 위해 별도 트랜지스터 사용하기에는 보드 크기와 배선 난위도, 전력 고려 등등 너무 과하기에, 트랜지스터를 사용 하지 않고 8bit 출력 port에 AT90CAN128 개별 출력(40mA 한계) 전력을 넘지 않을 저항값을 선택 하게 되었습니다.

다시 말해 8개의 LED를 동시에 켜되 총 40mA 를 넘지 않게 즉 개별 LED 당 4.4 mA 를 넘지 않게 해야 합니다.

이를 위해 680옴 저항을 사용 하면 4.2mA 를 소모하게 되고 8개 모두 사용 하더라도 35mA 미만으로 소모 하기에 15자리 segment를 순차적으로 표시(15자리 중 순간적으로 하나만 표시)하게 되면 별도 TR 없이도 구동이 가능 하게 됩니다.

대신 duty cycle이 무려 1/15 라서 밝기는 줄어 들 것이라는 .... (최종 완성시에 문제 없었습니다)


말보다는 사진 나갑니다~ (클릭하면 아름다운 대형 크기로!)

[사진5] LCD 배치 - 느끼기에 균형미가 넘치게 자~알


[사진6] 뒷면에는 AT90CAN128 모듈과 직접 연결할 핀헤드를 자알~ 위치
          (고도의 도킹 시나리오 준비 필요?!)


[사진7] 공통 출력(8선)을 모두 이어 나갑니다 (거짓말 조금 하면 하루 작업 입니다 ㅠ)


[사진8] 전류 제한을 위한 공통라인 8선에 저항 부착(680옴- 검갈빨주노초)


[사진9] 개별 segment 선택 용 배선 15개 까지 추가 하면 - 인간의 한계를 시험 하는 느낌이 옵니다.


[사진10, 11] CAN 없이 Segment display 루틴 부터 만들고 배선을 제대로 했나 확인 합니다.


문자열 data도 넣어서, 영문 표현을 시도 해 봅니다^^


[사진12,13,14] AT90CAN128 보드와 디스플레이 기판 결합

핀 헤더는 디스플레이 기판에서, 소켓은 AT90128 모듈에 장착하여 서로 연결합니다.


뒷쪽은 아래와 같습니다. - 왼쪽이 CAN 라인, 우측이 전원, 헤더는 AVR ISP 프로그래머와 연결


약간 다른 각도~


위의 사진과 같이 핀헤더로 기성품인 AT90CAN128 모듈과 디스플레이 모듈을 결합 하게 됩니다.

7 Segment 표시를 위해 아래의 AVR 포트를 할당 했습니다. (code 참조 가능)
  PORTF : Segment out (수 표시용 7 세그먼트 7bit + 점(dot) 1bit)

  PORTA : Digit 0 ~ 7 (처음 8자리 표시)

  PORTC : Digit 8 ~ 15 (나머지 7자리 표시)

  ** [사진 4]에서 보시면 아시겠지만 기판 포트 위치에 맞추어 A,C,F 포트를 할당 하게 된 것입니다.


그리고 위 사진에 없지만 나중에 추가 연결한 악셀레이터(Accelerator) 와 RPM 표시를 위해 아래의 포트를 할당 했습니다.

  PORTB : 8 개 LED 개별 표시

  PORTE : 8 개 묶음 표시


LCD 모듈을 사용하면 포트 2개 정도(8bit + 8bit) 으로 해결 될 수 있겠지만 낮/밤 시인성을 위해 7 segment를 사용한 이상 엄청난 수의 출력 포트를 사용 하게 됩니다. AT90CAN128의 풍부한 포트를 잘 활용 했다고 볼 수 있습니다.


[사진15] 처음 디버깅용 MCP2515 + AVR 조합의 CAN data 전송, 확인(to BT), PC to CAN 전송 용 모듈들 - 한쌍(두줄)의 CAN 네트웍으로 연결 된 상태 입니다.

위 등장 모듈은 별도 글에서 설명 드릴 예정 입니다.


실제 모듈 구성과 기본 code 구성은 다음 글로 이어집니다.



01234567891011121314


작성은 민교아빠 님 입니다.

[항상 좋은 자료 감사합니다]

원글은 "민교아빠의 공간
"에 있습니다.

참고: 2013년 1월 국내 자동차 판매량





.


작성은 민교아빠 님 입니다.

[항상 좋은 자료 감사합니다]

원글은 "민교아빠의 공간
"에 있습니다.

참고: 2012년 10월 국내 자동차 판매량











_



+ Recent posts