CNK HUD

CNK HUD, SPI 프로그래밍 - 25F16-100HIP 제어

mcus 2014. 12. 24. 02:34

이글의 사진이나 소스프로그램 등은 저작자의 동의 없이는 상업적으로 사용할 수 없으며, 비상업적인 목적이라하더라도 출처를 밝히지 않고 게시하는 것은 금지합니다.

 

통합 개발 환경은 MPLAB IDE 8.92와 MPLAB X IDE 2.26을 번갈아 사용했습니다.

AVR은 처음부터 AVR Studio를 써와서 Atmel Studio보다 편한지라 가급적 AVR Studio 4.19 버전을 사용하는데, PIC는 처음이라 MPLAB IDE나 MPLAB X IDE 모두 기능도 제대로 파악하지 못하고 있습니다.

프로그래밍에 주로 사용한 SENS NT-R55/200에 램을 3GB로 늘려 놨지만, 아무래도 오래된 노트북이라 사양이 뒤떨어져 빠릿빠릿하지는 못합니다.

컴파일하는 속도는  MPLAB IDE나 MPLAB X IDE가 별로 차이나는 것 같지는 않지만, 로딩되는 시간은 MPLAB X IDE가 훨씬 많이 걸립니다.


컴파일러는 microchip의 홈페이지에서 http://www.microchip.com/pagehandler/en-us/devtools/mplabxc/home.html 에서 XC8 v1.33을 다운 받아 사용했습니다.


프로그래머로는 PICKIT3 clone을 사용했습니다.

잘 동작하지만 가끔 먹통이 되는 현상이 발생하기도 합니다.

이럴 때에는 전원을 분리했다가 다시 연결하면 잘됩니다.

PICKIT3와 CNK_HUD 연결을 확실히 마무리하지 않고 대충 선을 꼽아서 가끔 빠지기도 하는데, 그런 영향도 있을 듯합니다.

PICKIT2 복제품도 있는데 이상하게도 PICKIT2 복제품에서는 18F67J10을 인식하지 못합니다.

Device ID를 못 읽어 오는 증상을 보입니다


CNK HUD에 EN25F16-100HIP가 18F67J10의 MSSP1에 연결되어 있습니다.

 

테스터로 배선을 찍어 다음과 같이 배선되어 있음을 확인했습니다.

 

25F16               18F67J10

==========       ============

1 (/CS)            11 (RF7/SS1)

2 (DO)              35 (RC4/SDI1/SDA1)

3 (/WP)            13 (RF5/AN10/CVref)

4 (VSS)            GND

5 (DI)               36 (RC5/SDO1)

6 (CLK)            34 (RC3/SCK1/SCL1)

7 (/HOLD)         12 (RF6/AN11)

8 (VCC)            3.3V

 

전체적으로 보아 18F67J10의 SPI 1과 정상적으로 연결되어 있습니다.

25F16의 DataOut(DO), WriteProtect(/WP), DataIn(DI), Clock(CLK),  /HOLD와 연결된 18F67J10 핀의 데이터 입출력 방향을 지정하고, XC8 컴파일러 라이브러리의 SPI 함수를 사용하면 됩니다.

다만 한 가지 이상한 점은 /CS를 RF7/SS1과 연결했다는 점입니다.

25F16과 18F67J10의 연결은 당연히 18F67J10이 master가 되고 25F16이 slave가 되어야 합니다.

25F16자체가 master 기능이 없으므로 절대 master로 동작할 수 없기때문입니다.

나중에 다루겠지만 RF7/SS1 포트는 18F67J10이 slave로 동작할 때 slave select로 사용되는 핀입니다.

즉, CNK HUD에서 18F67J10은 slave로 SPI 통신할 일이 없으므로 멀리 있는 11번 RF7/SS1과 연결할 필요 없이, 바로 옆에 아무 것과도 연결하지 않은 33번 RC2로 /CS를 제어하는 것이 합리적이었을 것입니다.

왜 이랬을까? 추측컨데 이 시스템의 설계자가 /SS1의 기능을 잘못 알고 있지 않았을까 생각해봅니다. master로 동작할 때에 이 핀이 자동으로 동작하는 것으로 잘못 알고 있었던 것 같습니다.

 

취미로 마이크로컨트롤러를 가지고 노는 아마추어이면서 특히 PIC는 초보 수준이라 좀 조심스럽기는 하지만, 74HC42 배선한 것, /SS1 배선한 것, I2C 연결한 것 등을 종합해서 보면 이 시스템을 설계한 사람이 PIC에 대해서 잘 이해하고 있는 것 같아 보이지는 않습니다. 나중에 I2C 부분을 다루면서 또 더 심각한 오류에 관해서 이야기 하겠지만, 숙련된 설계자는 아닌 것 같습니다. 행여나 설계하신 분이 이글을 보시면 화가 나시겠지만, 초보가 보기에도 많이 부적절해 보입니다. 실력만큼 대접 받는 사회가 되기를 바라면서...

 

 

본론으로 돌아가 SPI에 관해 더 알아보기로 합니다.

책에서 여러번 보기는 했지만 실제로 SPI 프로그래밍하는 것은 이번이 처음입니다.

PIC를 다루는 것도 처음이라 공부했던 내용을 정리한다는 관점에서 글을 씁니다.

SPI 관련 내용을 한 꼭지로 쓰려니 내용이 많이 깁니다.

 

18F67J10에서는 SPI를 두 개까지 사용할 수 있습니다.

아래 설명에서 SSPx 또는 SSx 등과 같은 표현을 사용하는데 x는 SPI 번호로 1 또는 2를 쓸 수 있습니다.

x 없이 SSP나 SS라고 쓴 경우는 특별한 경우가 아니면 SSP1 또는 SS1입니다.

 

헤더 파일에 다음과 같이 매크로를 지정했습니다.

 

#define SPI_WP_OUTPUT           TRISFbits.RF5 = 0           // Write Protect Pin on Serial EEPROM
#define SPI_WP_SET                  LATFbits.LATF5 = 1
#define SPI_WP_CLEAR              LATFbits.LATF5 = 0
#define SPI_HOLD_OUTPUT        TRISFbits.RF6 = 0          // Hold Pin on Serial EEPROM
#define SPI_HOLD_SET               LATFbits.LATF6 = 1
#define SPI_HOLD_CLEAR           LATFbits.LATF6 = 0
#define SPI_CS_OUTPUT            TRISFbits.RF7 = 0         // Chip Select Pin on Serial EEPROM
#define SPI_CS_SET                   LATFbits.LATF7 = 1
#define SPI_CS_CLEAR               LATFbits.LATF7 = 0

끝에 OUTPUT이 붙은 매크로는 포트의 출력 방향을 output으로 설정하는 것이고

끝에 SET이 붙은 매크로는 포트의 값을 1로 설정하는 것이며

끝에 CLEAR가 붙은 매크로는 포트의 값을 0으로 설정하는 것입니다.

다음은 SPI에 사용하는 포트들을 설정하는 초기화 함수입니다.

void SPI_Init(void)
{
    SPI_WP_OUTPUT;                  // Set output RF5 = WP
    SPI_HOLD_OUTPUT;               // Set output RF6 = HOLD
    SPI_CS_OUTPUT;                  // Set output RF7 = CS
    SPI_WP_SET;                        // Set write protect
    SPI_HOLD_SET;                     // Set HOLD High
    SPI_CS_SET;                         // Disable CS
}

 

위 함수에서는 WP, HOLD, CS 핀을 output으로 정하고, 각각의 핀을 high로 설정합니다.

매크로 정의할 때와 SPI_Init() 함수에서는 CLK(clock)과 DO(DataOut), DI(DataIn)은 다루지 않았습니다.

이들을 처리하지 않는 이유는 SPI 통신을 준비하는 다음 단계에서 OpenSPI() 함수 안에서 적절히 설정하기 때문입니다.

SPI 통신의 전체적인 흐름은 다음과 같습니다.

 

1. 각 포트들을 적절한 상태로 설정

  이 기능은 위의 SPI_Init() 함수에서 수행합니다.

2. 필요한 모드로 SPI를 설정

 이 기능은 XC8 라이브러리의 OpenSPI() 함수를 호출하여 설정합니다.

3. 읽기 쓰기 등의 작업 프로그래밍

 이 기능은 XC8 라이브러리의 readSPI(), writeSPI() 함수를 이용하여 구현합니다.

 

1. 단계의 SPI_Init() 함수와 관련된 내용은 이미 다루었으므로 더 언급할 내용이 없습니다.

 

2. 단계의 OpenSPI()함수에 관하여 살펴 봅니다.

OpenSPI() 함수의 원형은 아래와 같이 세 개의 인수를 받습니다.

void OpenSPI( unsigned char sync_mode, unsigned char bus_mode, unsigned char smp_phase)

 

첫째 매개 변수 sync_mode의 용도를 살펴봅니다.

XC8과 같이 설치된 소스 파일 중 spi_open.c 안에 OpenSPI() 함수의 소스가 있습니다.

(spi_open.c의 경로는 XC8 v1.33을  디폴트로 설치한 경우 C:\Program Files\Microchip\xc8\V1.33\sources\pic18\plib\SPI 폴더 안에 있습니다.)

 

OpenSPI() 함수의 내부를 보면

SSPCON = 0x00;                   // power on state
SSPCON |= sync_mode;       // select serial mode 
SSPCON |= SSPENB;           // enable synchronous serial port
등의 코드가 있습니다.

 

즉, sync_mode는 SSPCON의 값을 가져야 합니다.

18F67J10(18F87J10)의 데이터시트 p195에 SSPxCON1에 대한 설명이 있습니다.

sync_mode는 이 중 SSPM<3:0>의 내용을 담아야 합니다.

 

sync_mode

===========

0b0101 (5)    Slave Mode, clock = SCKx, /SSx 사용 안함.

0b0100 (4)    Slave Mode, clock = SCKx, /SSx 사용

0b0011 (3)    Master Mode, clock = TMR2 output/2

0b0010 (2)    Master Mode, clock = Fosc/64

0b0001 (1)    Master Mode, clock = Fosc/16

0b0000 (0)    Master Mode, clock = Fosc/4

 

CNK HUD의 18F67J10은 SPI를 master mode에서 사용해야 하므로 sync_mode는 0-3중의 한 값을 가져야 합니다.

SPI와 관련된 내용을 가지고 있는 헤더 파일 spi.h (C:\Progrma Files\Microchip\xc8\v1.33\include\plib)에 다음과 같은 매크로가 정의되어 있으므로 적절한 것을 사용하면 됩니다.

#define   SPI_FOSC_4         0b00000000              // SPI Master mode, clock = Fosc/4
#define   SPI_FOSC_16        0b00000001              // SPI Master mode, clock = Fosc/16
#define   SPI_FOSC_64        0b00000010              // SPI Master mode, clock = Fosc/64
#define   SPI_FOSC_TMR2   0b00000011              // SPI Master mode, clock = TMR2 output/2
#define   SLV_SSON           0b00000100              // SPI Slave mode, /SS pin control enabled
#define   SLV_SSOFF          0b00000101              // SPI Slave mode, /SS pin control disabled

 

계속하여 OepnSPI() 함수의 소스 코드를 살펴 보기로 합니다.

이 함수의 소스 코드 아랫 부분에 있는 switch 문에서 sync_mode를 다시 한 번 사용하고 있습니다.

이 내용을 분석해봅니다.

SPI를 slave mode로 사용하는 경우의 sync_mode 값은 4 또는 5입니다. switch 문 안에  case 4:와 case 5: 로 slave mode를 위한 값을 설정하고 있습니다. 위의 sync_mode 설명에서 보듯이 sync_mode 4에서는 CLK와 /SSx 신호를 모두 사용합니다. 따라서 case 4:에서는 /SSx와 CLK를 모두 입력으로 설정하고 있습니다.

만면에 sync_mode 5에서는 CLK만 사용하고 /SSx 신호느 사용하지 않습니다. 따라서 case 5:에서는 CLK만 입력으로 설정하는 것을 볼 수 있습니다. case4:와 case 5: 이외에는 master mode이므로 CLK를 출력으로 설정합니다.

  switch 문 밖에서는 SDI는 입력으로 SDO는 출력으로 설정하고 있습니다. SDI는 slave 장치로부터 데이터를 받는 것이니까 입력으로 설정해야 합니다. 18F67J10의 경우 MSSP1의 SDI는 35번 핀(RC4/SDI1/SDA1)입니다. 이 핀이 slave인 25F16의 2번 핀(DO : DataOutput)과 연결되어 있습니다.

OpenSPI() 함수의 마지막 부분에서 SSPxCON1 레지스터의 SSPEN 비트를 설정하여 SPI 통신을 시작합니다.(SSPCON |= SSPENB;)

 

 

둘째 매개 변수 bus_mode는 SPI의 모드를 설정합니다.

  SPI모드는 0,1,2,3으로 4 가지가 있습니다.

인터넷 상에서 가져온 그림으로 간단히 언급하고자 합니다.

 

 

SPI mode 0과 1에서는 clock이 평상시에는 0이다가 신호 전송시에 1로 올라가고,

mode 2와 3에서는 반대로 clock이 평상시에는 1이다가 신호 전송시에 0으로 내려갑니다.

SPI mode 0과 2에서는 clock이 시작될 때에 sampling 하고,

mode 1과 3에서는 clock이 끝날 때에 sampling 합니다.

 

18F67J10의 SPI는 mode 0, 2, 3을 지원합니다.

SPI 모드는 SSPxCON 레지스터의 CKP 비트와 SSPxSTAT 레지스터의 CKE 비트로 설정합니다.

CKP 비트는 위 그림의 Clock Polarity(CPOL)이고, CKE 비트는 Clock Phase(CPHA)입니다.

CKP = 0, CKE = 0이면 mode 0

CKP = 0, CKE = 1이면 mode 1(18F67J10에서는 미지원)

CKP = 1, CKE = 0이면 mode 2

CKP = 1, CKE = 1이면 mode 3

25F16은 데이터시트 7 페이지에 mode 0와 mode 3만을 지원한다고 되어 있습니다.

spi.h를 살펴보면 다음과 같이 매크로가 정의되어 있습니다.

 

#define   MODE_00       0b00000000              // Setting for SPI bus Mode 0,0
#define   MODE_01       0b00000001              // Setting for SPI bus Mode 0,1
#define   MODE_10       0b00000010              // Setting for SPI bus Mode 1,0
#define   MODE_11       0b00000011              // Setting for SPI bus Mode 1,1

 

세째 매개 변수 smp_phase는 샘플링 시기를 지정하는 역할을 합니다.

OpenSPI() 함수에서 SSPxSTAT 레지스터에 값을 넣는 부분만 가져와 봅니다.

SSPSTAT &= 0x3F;                     // power on state 
SSPSTAT |= smp_phase;           // select data input sample phase

 

18F67J10의 데이터시트 p190에 SSPxSTAT 레지스터에 관한 설명을 가져와 봅니다.

 

bit 7 SMP: Sample bit
                    SPI Master mode:
                        1 = Input data sampled at end of data output time
                        0 = Input data sampled at middle of data output time
                    SPI Slave mode:
                        SMP must be cleared when SPI is used in Slave mode.

bit 6 CKE: SPI Clock Select bit  (SPI mode 설명할 때 다루었습니다.)

                        1 = Transmit occurs on transition from active to Idle clock state
                        0 = Transmit occurs on transition from Idle to active clock state
                        Note: Polarity of clock state is set by the CKP bit (SSPxCON1<4>).
bit 5 D/A: Data/Address bit
                     Used in I2C mode only.(SPI에서는 안 쓴다는 내용입니다.)
bit 4 P: Stop bit
                    Used in I2C mode only.(역시 SPI에서는 안 씁니다.)

                    This bit is cleared when the MSSP module is disabled, SSPEN is cleared.
bit 3 S: Start bit
                    Used in I2C mode only.(역시 SPI에서는 안 씁니다.)
bit 2 R/W: Read/Write Information bit
                    Used in I2C mode only.(역시 SPI에서는 안 씁니다.)
bit 1 UA: Update Address bit
                    Used in I2C mode only.(역시 SPI에서는 안 씁니다.)
bit 0 BF: Buffer Full Status bit (Receive mode only)

 

SSPSTAT &= 0x3F;에서 상위 비트인 bit 7과 bit 6을 clear하고 나머지 비트들은 그대로 두고 있습니다. bit 6 CKE는 둘째 매개 변수 bus_mode에서 설정한 것을 건드리지 말아야 하므로

SSPSTAT |= smp_phase; 에서 설정하는 것은 bit 7 밖에 없습니다.

spi.h를 찾아보면
#define   SMPEND        0b10000000          // Input data sample at end of data out            
#define   SMPMID        0b00000000           // Input data sample at middle of data out

이렇게 두 개의 매크로가 있는데 결국은 bit 7(SMP)를 1로 설정할 것인지 0으로 할 것인지를 결정하는 것입니다. SMP를 1로 하면 dataout time의 끝 부분에서 data를 가져오도록 설정하는 것이고, 0으로 하면 dataout time의 중간 부분에서 data를 가져 오도록 설정하는 것입니다. 이 둘의 차이점은 18F67J10의 데이터 시트 p194의 그림에서 아래 부분의 SDIx 부분을 보면 이해가 쉽게 될 것입니다. 25F16과 SPI 통신하는 데는 어느 값을 주어도 잘 됩니다.

 

장황한 설명을 했지만 18F67J10과 EN25F16이 SPI 통신하기 위해서는 OpenSPI() 함수를 다음과 같이 호출하면 됩니다.
OpenSPI(SPI_FOSC_4,MODE_00,SMPMID);

 

1. 단계에서 각 핀들을 적절한 상태로 설정하기 위해서 SPI_Init() 함수를 만들었고

2. 단계에서 필요한 모드로 SPI를 설정하기 위해서 OpenSPI() 함수를 이용했습니다.

3. 단계 읽기 쓰기 등의 작업 프로그래밍을 할 차례입니다.

 

EN25F16의 데이터 시트에 따라 몇 가지 기능 구현만 해보겠습니다.

다음은 25F16의 매뉴얼을 보고 status register bit를 정의한 매크로입니다.

 

#define SPI_SR_WIP                  0
#define SPI_SR_WEL                  1
#define SPI_SR_BP0                  2
#define SPI_SR_BP1                  3
#define SPI_SR_BP2                  4
#define SPI_SR_RES1                 5
#define SPI_SR_RES2                 6
#define SPI_SR_SRP                  7
#define SPI_SR_S0                    8
#define SPI_SR_S1                    9
#define SPI_SR_S2                    10
#define SPI_SR_S3                    11
#define SPI_SR_S4                    12
#define SPI_SR_S5                    13
#define SPI_SR_S6                    14
#define SPI_SR_S7                    15

다음은 역시 25F16의 매뉴얼에 나오는 명령어들을 정의한 매크로들입니다.

 

#define COMMAND_EN25F16_WREN        0x06        // Write enable
#define COMMAND_EN25F16_WRDI         0x04        // Write disable
#define COMMAND_EN25F16_OTPOUT    0x04        // OTP mode
#define COMMAND_EN25F16_RDSR         0x05        // Read status register (16bit)
#define COMMAND_EN25F16_WRSR         0x01        // Write status register
#define COMMAND_EN25F16_READ         0x03        // Read data
#define COMMAND_EN25F16_FAST         0x0B        // Read data fast
#define COMMAND_EN25F16_PP             0x02        // Page programming
#define COMMAND_EN25F16_WRITE        0x02        // Write data
#define COMMAND_EN25F16_SE             0x20        // Sector Erase
#define COMMAND_EN25F16_BE             0xD8        // Block Erase
#define COMMAND_EN25F16_CE             0xC7        // Chip Erase
#define COMMAND_EN25F16_DPDN         0xB9        // Deep Power Down
#define COMMAND_EN25F16_DEVID         0xAB        // Read Device ID
#define COMMAND_EN25F16_MANUID      0x90        // Read Manufacturer ID
#define COMMAND_EN25F16_RDID           0x9F        // Read Manufacturer and device ID
#define COMMAND_EN25F16_OTP           0x3A        // Read Manufacturer and device ID

#define PAGE_SIZE_EN25F16                  256

 

SPI에 직접 입력과 출력을 하는 것은 XC8의 함수 WriteSPI()와 ReadSPI() 함수를 사용합니다.

 

데이터 시트 p12에 있는 Status Register 읽기입니다.

 

 

En25F16의 status register를 읽고 이 값을 unsigned int로 리턴하는 함수입니다.
구체적인 기능은 주석으로 대신합니다.

 

unsigned int SPI_ReadStatusReg(void)
{
    unsigned int data1,data2;


    SPI_CS_CLEAR;                                   // set cs low
    WriteSPI(COMMAND_EN25F16_RDSR);   // send command read status register (0x05)
    data1 = ReadSPI();                              // read status reegister high byte
    data2 = ReadSPI();                              // read status register low byte
    SPI_CS_SET;                                     // set cs high
    return((data1 << 8) | data2);
}

 

데이터 시트 p20에 있는 Device ID 읽기입니다.

 

 

unsigned char Read_EN25F16_DeviceID(void)
{
    unsigned char ID;

 

    SPI_CS_CLEAR;                                         // set cs low
    WriteSPI(COMMAND_EN25F16_DEVID);         // send command for device id (0xAB)
    WriteSPI(0);                                               // Write three dummy bytes
    WriteSPI(0);                                               // Write three dummy bytes
    WriteSPI(0);                                               // Write three dummy bytes
    ID = ReadSPI();                                         // Read Device ID
    SPI_CS_SET;                                            // set cs high
    return(ID);
}

 

데이터 시트 p21에 있는 Manufacturer / Device ID 읽기입니다.

 

 

unsigned int Read_EN25F16_ManufacturerID(void)
{
    unsigned int ID;


    SPI_CS_CLEAR;                                          // set cs low
    WriteSPI(COMMAND_EN25F16_MANUID);
    WriteSPI(0);                                                // Write Adress 0 in three bytes.
    WriteSPI(0);                                                // Write Adress 0 in three bytes.
    WriteSPI(0);                                                // Write Adress 0 in three bytes.
    ID = (ReadSPI() << 8);                                 // Manufacturer ID
    ID += ReadSPI();                                         // Device ID
    SPI_CS_SET;                                            // set cs high
    return(ID);
}

 

Page Program (PP), Sector Erase (SE), Block Erase (BE), Chip Erase (CE) and Write Status Register (WRSR) 명령을 수행하기 전에는 반드시 Status Register의 WEL(Write Enable Latch)를 1로 설정해야 합니다.  Status Register의 WEL 비트를 1로 설정하는 명령이 WREN(Write Enable) 명령입니다.

PP의 구현을 예로 듭니다.

 

 

void SPI_PageProgram(unsigned long address,int bytes,unsigned char *arr)
{
    int i;

 

    // Send WREN(Write Enable).
    SPI_CS_CLEAR;
    WriteSPI(COMMAND_EN25F16_WREN);
    SPI_CS_SET;
    while(!(SPI_ReadStatusReg() & (1 << SPI_SR_WEL)));     // status register의 WEL 비트 기다림
    SPI_CS_CLEAR;
    WriteSPI(COMMAND_EN25F16_WRITE);
    WriteSPI((address & 0xFF0000) >> 16);                          // send address
    WriteSPI((address & 0x00FF00) >> 8);
    WriteSPI(address & 0x0000FF);
    for(i = 0;i < bytes;i++) WriteSPI(*(arr + i));
    SPI_CS_SET;
    while(SPI_ReadStatusReg() & (1 << SPI_SR_WIP));       // write 완료까지 기다림.
}

 

이 함수는 주소와 기록할 바이트 수와 기록할 내용이 있는 주소를 전달 받아서 기록하는 기능을 합니다. 25F16은 용량이 2mega byte이므로 주소룰 unsigned long 으로 받아 그 중 24비트(3바이트)를 사용합니다.

 

다른 기능들은 위의 내용을 참고하여 만드시면 됩니다.