본문 바로가기

[Harman] 반도체 설계/Avalon

SoC Design with Nios II Processor.

Nios II Processor

Programmable Logic Device (ASIC, FPGA, CLPD) 설계에 사용하기 위해 Intel이 설계하고 개발한 소프트 코어 마이크로 프로세서 (Soft Core Micro-processor) 로 Device에 임베디드 시스템을 구축하기 위한 커스텀 하드웨어와 소프트웨어를 함께 디자인 및 개발할 수 있는 환경을 제공한다. 이를 통해, 응용 프로그램의 Specific Needs에 따라 기능과 주변 장치를 사용자가 직접 정의할 수 있다. (e.g. Platform Designer)

Nios II Processor는 하버드 아키텍처를 사용한다. 또한 여러 Nios II 코어를 하나의 FPGA에 통합할 수 있는 대칭형 다중 처리 방식 (SMP, Symmetric Multi-Processing)을 지원하기 때문에 병렬처리 작업에 유용하다. 프로세서간 작업 분산(workload balance)을 시키기가 훨씬 쉽지만, 병목 현상이나 오버 헤드와 같은 문제점으로 인해 확장성(더 많은 프로세서나 코어를 추가하여 성능 향상)이 낮다는 단점이 있다. 

 

[프로그래머블 로직, Programmable Logic] 

특정 기능이나 작업을 수행하기 위해 디지털 논리 회로를 설계하고 재구성하는 기능을 말한다. VHDL이나 Verilog와 같은 HDL언어를 사용하며 Programmable Logic 구현을 위해 ASIC, FPGA 그리고 CPLD와 같은 Programmable Logic Device가 주로 사용된다. 

 

[소프트 코어 마이크로 프로세서, Soft Core Micro Processor]

소프트웨어로 구현된, 프로그래밍 가능한 CPU 또는 Micro-processor를 말한다. 하드웨어를 수정하지 않고도 프로그래밍을 통해 특정 기능이나 작업을 수행할 수 있다. 그렇기 때문에 개발 시간 및 비용을 절감할 수 있다. 주로, FPGA나 ASIC과 같은 집적 회로 설계에 사용한다. 

 

[하버드 아키텍처, Harvard Architecture]

명령용 버스와 데이터용 버스가 물리적으로 분리되어 있는 구조로, 명령을 메모리로부터 읽는 것과 데이터를 메모리로부터 읽는 것을 동시에 수행할 수 있다는 특징을 가진다. 폰 노이만 구조보다 병목 현상이 적어 명령어 처리가 끝나자마자 다음 명령을 읽어들이며 더 빠른 속도를 낼 수 있지만, 처리 속도를 높이려면 더 많은 전기 회로가 필요하다는 단점이 있다. 

Avalon Bus는 Nios II Processor가 사용하는 인터페이스 중 하나로, FPGA 기반 시스템에서 다양한 하드웨어 컴포넌트간 데이터 및 제어 신호의 통신을 위한 표준 인터페이스이며 Avalon Memory-Mapped (MM) Interface는 Avalon Bus의 일종으로서 메모리가 할당된 Input / Output과 상호 작용하기 위한 인터페이스를 말한다. Avalon MM Interface는 Avalon Bus를 통해 메모리 매핑된 주소 영역에 access하고 데이터를 읽거나 쓸 수 있게 한다. (Read / Write Transfer) 

 

address, byteenable, read, writedata는 Master(CPU, IP Core etc.)에서 Slave(Peripherals, Custom IP etc.)로 전달되는 신호로, Slave가 해당 신호를 받으면 Master로 waitrequest 1을 전달한다. 이후, waitrequest 0과 함께 readdata, reponse를 Master로 반환하고 Master는 read를 끝낸다. 

 

위 그림과 같은 Avalon MM Interface의 Read / Write Transfer는 Qsys에 Nios II Processor를 추가하면서 Interface type으로 미리 설정했기 때문에 따로 파형을 확인할 수 없었다. 따라서 task를 활용하여 Avalon Model을 설계하고 Modelsim으로 Avalon Bus의 Waveform을 확인했다. 

 

readdata를 Master(avalon model)로 반환할 Slave가 없기 때문에 My_reg.v(Slave)를 추가하였다. Slave를 추가하여 파형 확인 뿐만 아니라 De1-SoC Board 업로드 후, Eclipse를 통한 Read / Write Transfer 검증이 가능해졌다.

 

임의의 주소를 할당하고 임의의 데이터 값을 입력으로 주었다. 

writedata는 write 신호가 1일 때, rMy_reg에 저장이 되며 readdata는 read 신호가 1일 때, rMy_reg에 저장된 값을 읽는다. 파형을 확인했고 De1-SoC Board 업로드 후, Eclipse를 통한 검증을 진행했다.

 

Quartus의 Platform Designer에서 My_reg.v 로 component(Custom IP)를 추가해준다. De1-SoC Board의 reset 신호는 Active-low 신호이므로 Signal Type 설정을 reset_n으로 해준다. My_reg.v File을 작성할 때 Port Name을 address, read, writedata, reset_n 등 Avalon interface의 Signal Name과 같게 해주면 Component Signal이 자동으로 연결된다.

 

Custom component(My_reg)와 함께 IP(Nios II Processor, JTAG, Memory)를 추가하여 Interface를 연결한다. (Memory와 data-master 연결, signal type, pin map 설정 확인 필수, eclipse에서 elf file download error.)

 

Qsys 파일을 Generate하면 qip와 sopcinfo 파일이 생성된다. eclipse를 통한 보드 검증을 위해 qip파일을 포함한 Mark_1.v를 생성했다. niosII_system instance가 선언된 Mark_1.v를 Top module로 선언하고 컴파일을 진행한다.

// Mark_1.v

module Mark_1 ( //Primitive signals
    input    clk    ,
    input    reset_n
);
    niosII_system uNiosII_system_u0 ( // Declare an instance of niosII_system
        .clk_clk       (clk    ),
        .reset_reset_n (reset_n)
    );
endmodule

 

컴파일이 끝나면 De1-SoC Board에 생성된 .sof 파일을 업로드하고 eclipse에서 .sopcinfo 파일로 프로젝트를 생성한다. 

 

생성된 프로젝트에 Source File을 추가하고 데이터의 Read / Write Transfer을 검증하기 위해 다음 코드를 작성한다. 

#include <stdio.h>
#include <unistd.h>
#include "system.h"

int main(void)
{
    *(volatile unsigned int*)0x00009000 = 00001234; // write
    unsigned int readdata = *(volatile unsigned int*)0x00009000;
    printf("0x9000 readdata = %d, 0x9000 readdata = 0x%x\n", readdata, readdata);
	
    printf("Set 0xbeefbeef at 0x9007\n");
    *(volatile unsigned int*)0x00009007 = 0xbeefbeef; // write
    readdata = *(volatile unsigned int*)0x00009007;
    printf("0x9007 readdata = 0x%x\n", readdata);
	
    readdata = *(volatile unsigned int*)0x00009000;
    printf("0x9000 readdata = 0x%x\n", readdata);
	
    while(1) {
		
    }
}

 

 

 

Pulse Width Modulation.

펄스 폭 변조, 신호의 크기에 따라 펄스의 폭을 조절하는 방식이다. 펄스 파형의 HIGH 상태와 LOW 상태의 비율을 듀티 사이클 (Duty Cycle, Duty Ratio)이라 부르는데, PWM은 이 듀티 사이클을 조절하여 펄스의 폭을 변조한다. 전압, 전류 제어용으로 많이 사용되며 LED 밝기, 모터의 속도 제어 및 오디오 신호 생성 등에 응용된다. 

 

PWM 또한 My_reg와 마찬가지로 Avalon Model을 통해 시뮬레이션 파형을 확인하도록 하겠다. Avalon Bus Signal에 맞춰 입출력 포트와 펄스 폭 제어를 확인할 pwm_out을 선언한다. Test Bench의 Block Diagram의 구성 과정은 다음과 같다. 

byteenable 신호에 맞춰 div와 duty를 8-Bit로 지정했다. 펄스의 반복을 위해 counter를 추가했고 출력을 한 Bit씩 받는 off를 선언했다. 또한 데이터의 읽고 쓰기를 활성화하는 Divide, Duty Enable 신호도 추가하였다. Avalon_Model과 연결하여 파형을 확인하도록 하겠다. 

wr_n, addr 신호가 모두 0이면 div_ena이 1이 된다. div_ena가 활성화되면 div 레지스터에 writedata, 7이 저장된다. reset이 끝나고 다음 posedge에서 아직 duty가 0이기 때문에 카운터 클리어 조건으로 off가 1이 된다. wr_n이 0이고 addr이 1이면 duty_ena가 활성화되고 duty에 두 번째 writedata, 4를 저장한다. write가 끝난 시점부터 write는 1을 유지하기 때문에 off는 카운터가 클리어되기 전까지 그 값을 유지한다. 카운터가 클리어되면서 off는 0이 되고 카운트가 4가 되면 다시 1이 된다. 카운트가 4 또는 클리어 될 때마다 off는 1과 0을 반복한다. write 신호가 계속 1을 유지하므로 해당 상태는 반복되면서 펄스의 형태를 띠게 된다. 

 

이클립스를 활용한 De1-SoC 보드 검증은 다음과 같은 흐름으로 진행하도록 한다. 

 

My_reg와 마찬가지로 My_pwm.v 파일로 custom component를 추가하고 qsys 파일을 Generate한다. 

// Mark_2.v

module Mark_2 (
   input          clk,
   input  [3:0]   key,
   input          reset_n,
   output [7:0]   pwm_out  
);
   niosII_system niosII_system_u0 (
      .clk_clk                        (clk    ),      
      .key_external_connection_export (key    ), 
      .reset_reset_n                  (reset_n),               
      .my_pwm_0_conduit_end_export    (pwm_out)
   );
endmodule

컴파일까지 끝나면 보드에 sof를 업로드하고 eclipse 검증을 시작한다. Qsys를 Generate하면서 생긴 sopcinfo로 프로젝트를 생성하고 키 버튼을 누를 때마다 LED의 밝기가 달라지도록 아래와 같은 C code를 작성한다. 

#include <stdio.h>
#include "altera_avalon_pio_regs.h"
#include "altera_avalon_pwm_regs.h"
#include "system.h"

#define NONE_PRESSED 0xF // Value read from button PIO when no buttons pressed
#define DEBOUNCE 30000   // Time in microseconds to wait for switch debounce


int main()
{ 
   int buttons, button_val;
   int print_cnt;
   
   // Initialize LEDs
   IOWR_ALTERA_AVALON_PWM_DIVIDER(MY_PWM_0_BASE,0xFF);
   IOWR_ALTERA_AVALON_PWM_DUTY(MY_PWM_0_BASE,0xFF);

   printf("\n\tNios II PWM Lab\n\n");
   printf("\tPlease press button 1 to 4 on kit to adjust LED intensity:\n");

   button_val = 0;
   while (1)
   {
      // Sample SW Button from Development Board
     print_cnt = 0;
      buttons = IORD_ALTERA_AVALON_PIO_DATA(KEY_BASE);

      // Debouncing Algorithm
      if (buttons != NONE_PRESSED)  { // if button pressed
         usleep (DEBOUNCE);
         while (buttons != NONE_PRESSED)  { // wait for button release
            buttons = IORD_ALTERA_AVALON_PIO_DATA(KEY_BASE);
        }
         usleep (DEBOUNCE);
      
		button_val++;
      
		if(button_val > 3) {
         button_val = 0;
      }
        print_cnt = 1;
      }

      switch (button_val)
      {
       case 0:
            IOWR_ALTERA_AVALON_PWM_DUTY(MY_PWM_0_BASE,0xcc);
            if (print_cnt == 1)
               printf("Level 4 intensity\n");
            break;
       case 3:
            IOWR_ALTERA_AVALON_PWM_DUTY(MY_PWM_0_BASE,0x99);
            if (print_cnt == 1)
               printf("Level 3 intensity\n");
            break;
       case 2:
            IOWR_ALTERA_AVALON_PWM_DUTY(MY_PWM_0_BASE,0x66);
            if (print_cnt == 1)
               printf("Level 2 intensity\n");
            break;
       case 1:
            IOWR_ALTERA_AVALON_PWM_DUTY(MY_PWM_0_BASE,0x33);
            if (print_cnt == 1)
               printf("Level 1 intensity\n");
            break;
         default:
            IOWR_ALTERA_AVALON_PWM_DUTY(MY_PWM_0_BASE,0xFF);
            break;
      }
   }
   return 0;
}

위와 같이 while문 안에서 반복적으로 버튼의 입출력을 확인하며 switch문을 실행하는 방식을 pollong방식이라고 한다. 프로그램의 작성과 이해가 쉽지만 인터럽트가 언제 발생했는지 정확하게 알 수 없고, 반복적으로 입출력을 감시하는 방식으로 CPU의 점유율이 높아 성능에 부담을 주기도 한다. 폴링 방식과 반대로 인터럽트 신호가 발생했을 때만 특정 작업을 진행하는 interrupt방식이 있다. 

#include <stdio.h>
#include "altera_avalon_pio_regs.h"
#include "altera_avalon_pwm_regs.h"
#include "system.h"
#include "unistd.h"

#include "sys/alt_irq.h"
#include "priv/alt_iic_isr_register.h"

int print_flag = 0;

void key_isr (void * context)
{
    static int  button_v = 0;

    volatile int* button_val_ptr = (volatile int *)context ;
    volatile int* button = IORD_ALTERA_AVALON_PIO_EDGE_CAP(KEY_BASE);

    IOWR_ALTERA_AVALON_PIO_EDGE_CAP(KEY_BASE, (0x4 | 0x2 | 0x1));

    button_v++;   // strobe state of SW switch
    printf("BV %d\n", button_v);
    if(button_v > 3) {
        button_v = 0;
    }

    *button_val_ptr = button_v;
    print_flag = 1;
}

int main()
{
    int button_val;

    IOWR_ALTERA_AVALON_PWM_DIVIDER(MY_PWM_BASE,0xFF);
    IOWR_ALTERA_AVALON_PWM_DUTY(MY_PWM_BASE,0xFF);

    printf("Nios II Interrupt Lab\n\n");

    IOWR_ALTERA_AVALON_PIO_EDGE_CAP(KEY_BASE, (0x4 | 0x2 | 0x1));

    alt_iic_isr_register( 
        KEY_IRQ_INTERRUPT_CONTROLLER_ID      ,
        KEY_IRQ                              ,
        (void *)key_isr                      ,
        (void *)&button_val                  ,
        0x0);

    IOWR_ALTERA_AVALON_PIO_IRQ_MASK(KEY_BASE, 0x7); // 0x4|0x2|0x1

    while(1) {
        if (print_flag) {
            print_flag = 0;
            switch (button_val)   {
                case 3:
                    IOWR_ALTERA_AVALON_PWM_DUTY(MY_PWM_BASE,0xF0);
                    printf("Level 4 intensity\n");
                    break;
                case 2:
                    IOWR_ALTERA_AVALON_PWM_DUTY(MY_PWM_BASE,0xC0);
                    printf("Level 3 intensity\n");
                    break;
                case 1:
                    IOWR_ALTERA_AVALON_PWM_DUTY(MY_PWM_BASE,0x80);
                    printf("Level 2 intensity\n");
                    break;
                case 0:
                    IOWR_ALTERA_AVALON_PWM_DUTY(MY_PWM_BASE,0x10);
                    printf("Level 1 intensity\n");
                    break;
                default:
                    IOWR_ALTERA_AVALON_PWM_DUTY(MY_PWM_BASE,0xFF);
                    printf("Level default\n");
                    break;
            }
        }
    }
}

 

 

 

GPIO

General Purpose Input / Output, 마이크로프로세서와 주변 장치들 간의 통신에 사용되는 입출력 포트를 의미한다. LED나 스위치 등 단일 비트로 동작하는 소자들을 제어할 때 주로 쓰인다. 특정한 입력에 대해 IRQ를 발생시킬 수 있다는 특징이 있으며 Avalon Interface의 Slave(PIO)에서 IRQ를 발생시키면 Master(Core)가 받아들인다. 

내부의 레지스터 (data_in, interruptmask, edgecapture)들은 IP나 모듈에서 특정한 기능들을 수행하도록 설계되었다. PIO Regsiter Map을 확인하면 각각 0,2,3의 offset을 가지며 Data value / PIO input, Control IRQ for each input, Edge detection for each input의 기능을 한다. Avalon Model을 활용한 Test-Bench Module의 Block Diagram은 다음과 같다. 

offset에 따른 시뮬레이션 파형은 다음과 같다. offset 2에서 int mask strobe가 발생하고 int mask는 1을 유지한다. offset 3에서 edge cap strobe가 발생하지만 in_port의 edge det이 발생하지 않았기 때문에 edge cap은 0을 유지하게 된다. 

 

이후, data_in이 edge det되면 edge cap이 1이 되고 이 때부터는 int mask와 edge cap 모두 1이기 때문에 IRQ 신호가 발생함을 시뮬레이션 파형을 통해 확인할 수 있다. 다음으로 eclipse를 통한 검증을 진행하도록 하겠다. De1-SoC 보드를 활용한 검증 흐름은 아래와 같다. 

 

De1-SoC의 버튼 키는 Active-low이기 때문에 PIO(key)의 Edge는 Falling Edge로 설정한다. 

// Mark_3.v

module Mark_3 (
    input clk,
    input sw,
    input [3:0] key,
    input reset_n,
    output [7:0] led
);
    niosII_system uNiosII_system_0 (
        .clk_clk(clk),
        .key_external_connection_export(key),
        .led_external_connection_export(led),
        .my_gpio_0_conduit_end_export(sw),
        .reset_reset_n(reset_n)
    );
endmodule

컴파일이 끝나면 보드에 업로드하고 이클립스로 검증을 진행한다. 레지스터와 PWM과 마찬가지로 프로젝트를 생성하고 아래와 같은 C Code를 작성한다. 


#include "sys/alt_stdio.h"
#include "altera_avalon_pio_regs.h"

#include "unistd.h"
#include "system.h"

#include "sys/alt_irq.h"
#include "priv/alt_iic_isr_register.h"

int print_flag = 0;

   void key_isr (void)
   {
      unsigned int k;
      k = IORD_ALTERA_AVALON_PIO_EDGE_CAP(AVALON_GPIO_IN_0_BASE); //input
      alt_printf("key_isr [0x%x] \n",k);
      IOWR_ALTERA_AVALON_PIO_EDGE_CAP(AVALON_GPIO_IN_0_BASE, 0x1); //clear
   }

int main()
{ 
   unsigned int switch_v;
   alt_putstr("Hello from Nios II!\n");

  *(volatile unsigned int *)0x9020 = 0xff;

  alt_iic_isr_register(
        AVALON_GPIO_IN_0_IRQ_INTERRUPT_CONTROLLER_ID,
        AVALON_GPIO_IN_0_IRQ,
          key_isr, // move to key_isr function
          NULL,
          NULL);

  IOWR_ALTERA_AVALON_PIO_EDGE_CAP(AVALON_GPIO_IN_0_BASE, 0x1); //clear
  IOWR_ALTERA_AVALON_PIO_IRQ_MASK(AVALON_GPIO_IN_0_BASE, 0x1); //mask

  while (1){
     switch_v = *(volatile unsigned int *)0x0;
     printf("switch values = 0x%x \n", switch_v);
     usleep(1*500*1000);
  }
  while(1);

  return 0;
}