이번에는 Vitis를 통해 Interrupt를 구현해보자. 우선 Interrupt를 구현하기 위해서는 Interrupt Timer를 위한 Counting Module, 그리고 Block Design에서 몇가지가 추가되어야 한다.
1. Timer IP (Interrupt Tick 발생기) 설계
module Timer (
input wire clk,
input wire resetn,
input wire enable,
input wire clear,
input wire [31:0] PSC,
output wire [31:0] TCNT,
input wire [31:0] ARR,
output wire interrupt
);
reg [31:0] counter;
reg [31:0] timer;
reg timer_tick, intr_tick;
assign TCNT = timer;
assign interrupt = intr_tick;
always @(posedge clk) begin : PRESCALER
if (resetn == 1'b0) begin
counter <= 0;
timer_tick <= 1'b0;
end else begin
if (enable == 1'b1) begin
if (counter == PSC) begin
counter <= 0;
timer_tick <= 1'b1;
end else begin
counter <= counter + 1;
timer_tick <= 1'b0;
end
end
if (clear == 1'b1) begin
counter <= 0;
timer_tick <= 1'b0;
end
end
end
always @(posedge clk) begin
if (resetn == 1'b0) begin
timer <= 1'b0;
intr_tick <= 1'b0;
end else begin
if (timer_tick == 1'b1) begin
if (timer >= ARR) begin
timer <= 1'b0;
intr_tick <= 1'b1;
end else begin
timer <= timer + 1;
intr_tick <= 1'b0;
end
end
if (clear == 1'b1) begin
timer <= 1'b0;
intr_tick <= 1'b0;
end
end
end
endmodule
Vitis - 05. Common Code 작성
펌웨어에서 보통 여러 모듈이나 프로젝트에서 공통으로 사용되는 유틸리티 함수나 기본 기능을 common 폴더로 만들어 모아 놓는다. 그 중 시간 관리와 관련된 역할을 하는 delay와 Tick_timer를 구현
salmon1113.tistory.com
우선 이전에 설계한 Tick Generator와 유사하게 Interrupt 신호를 발생시키는 모듈을 설계한다. PSC, TCNT, ARR, Interrupt의 의미는 다음과 같다.
- PSC (Prescaler):
입력 클럭(clk)의 분주 비율을 결정한다. PSC는 32비트 값으로, 클럭을 얼마나 느리게 만들지 설정한다. - TCNT (Timer Counter):
실제 타이머의 카운터 값을 나타낸다. - ARR (Auto-Reload Register):
타이머 카운터(TCNT)가 도달해야 하는 최대 값을 정의한다. 이 값에 도달하면 타이머가 리셋되고 인터럽트가 발생하게 된다. - Interrupt:
타이머가 ARR에 도달했을 때 발생하는 신호로, 외부 시스템에 이벤트를 발생시킨다.
// Implement memory mapped register select and read logic generation
// Slave register read enable is asserted when valid address is available
// and the slave is ready to accept the read address.
assign slv_reg_rden = axi_arready & S_AXI_ARVALID & ~axi_rvalid;
always @(*)
begin
// Address decoding for reading registers
case ( axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
2'h0 : reg_data_out <= slv_reg0;
2'h1 : reg_data_out <= slv_reg1;
2'h2 : reg_data_out <= TCNT;
2'h3 : reg_data_out <= slv_reg3;
default : reg_data_out <= 0;
endcase
end
// Output register or memory read data
always @( posedge S_AXI_ACLK )
begin
if ( S_AXI_ARESETN == 1'b0 )
begin
axi_rdata <= 0;
end
else
begin
// When there is a valid read address (S_AXI_ARVALID) with
// acceptance of read address by the slave (axi_arready),
// output the read dada
if (slv_reg_rden)
begin
axi_rdata <= reg_data_out; // register read data
end
end
end
// Add user logic here
assign enable = slv_reg0[0];
assign clear = slv_reg0[1];
assign PSC = slv_reg1;
assign ARR = slv_reg3;
// User logic ends
이후 AXI4 Interface 레지스터에 각 설정값을 매핑해준다.
** 버그 수정
위의 intr_tick은 timer_tick == 1'b1일 때만 업데이트되는 문제가 있었다. 이 때문에 PSC 값, ARR 값의 비율에 따라 인터럽트 발생 주기가 달라지는 문제가 발생되었다. 이를 수정하기 위해 아래와 같이 수정하였다.
- 수정 전 : intr_tick은 timer_tick == 1'b1일 때만 업데이트됨
- 수정 후 : intr_tick은 매 클럭마다 timer >= ARR 조건에 따라 업데이트됩니다.
module Timer (
input wire clk,
input wire resetn,
input wire enable,
input wire clear,
input wire [31:0] PSC,
output wire [31:0] TCNT,
input wire [31:0] ARR,
output wire interrupt
);
reg [31:0] counter;
reg [31:0] timer;
reg timer_tick, intr_tick;
assign TCNT = timer;
assign interrupt = intr_tick;
always @(posedge clk) begin : PRESCALER
if (resetn == 1'b0) begin
counter <= 0;
timer_tick <= 1'b0;
end else begin
if (enable == 1'b1) begin
if (counter == PSC) begin
counter <= 0;
timer_tick <= 1'b1;
end else begin
counter <= counter + 1;
timer_tick <= 1'b0;
end
end
if (clear == 1'b1) begin
counter <= 0;
timer_tick <= 1'b0;
end
end
end
always @(posedge clk) begin
if (resetn == 1'b0) begin
timer <= 1'b0;
intr_tick <= 1'b0;
end else begin
if (timer >= ARR) begin
timer <= 1'b0;
intr_tick <= 1'b1;
end else begin
intr_tick <= 1'b0;
if (timer_tick == 1'b1) begin
timer <= timer + 1;
end
end
if (clear == 1'b1) begin
timer <= 1'b0;
intr_tick <= 1'b0;
end
end
end
endmodule
2. Interrupt Controller 설정 (Block Design)
MicroBlaze는 AXI Interrupt Controller(Interrupt Controller IP)를 활용하여 여러 인터럽트 신호를 관리한다. 이를 위해 우선 Run Block Automation 단계에서 Interrupt Controller를 활성화 시켜 줘야한다.
- AXI Interrupt Controller IP:
MicroBlaze에 전달되는 모든 인터럽트 신호를 수집하고 우선순위를 정해 프로세서에 전달한다. - xlconcat IP:
여러 개의 개별 인터럽트 신호를 하나의 벡터 신호로 결합(concatenate)한다. AXI Interrupt Controller는 단일 인터럽트 입력 포트(intr)를 가지므로, 여러 소스의 인터럽트를 처리하려면 xlconcat으로 묶어야 한다.
이후 Interrupt를 발생시키는 Timer IP의 Interrupt 신호, 그외의 Interrupt 신호를 Concat IP에 연결시켜준다. 그외의 IP들도 연결 후, Flatform 파일을 생성한다.
3. Interrupt 설정 (Vitis)
xparameters.h 헤더파일을 보면, concat에 연결된 Interrupt ID가 설정되어 있는 것을 확인할 수 있다. Interrupt Controller가 이를 통해 어떤 장치에서 인터럽트가 발생했는지 판단하게 된다.
- XPAR_INTC_0_UARTLITE_0_VEC_ID: UARTLite의 인터럽트 ID.
- XPAR_INTC_0_TIMER_0_VEC_ID: 타이머의 인터럽트 ID.
본 게시글에서는 Timer 인터럽트만 사용할 것이기에, Interrupt Timer IP의 값 설정을 위한 Driver Code를 작성한다.
1) Header File
/*
* tim.h
*
* Created on: 2025. 3. 12.
* Author: kccistc
*/
#ifndef DRIVER_TIM_TIM_H_
#define DRIVER_TIM_TIM_H_
#include <stdint.h>
#include "xparameters.h"
typedef struct {
volatile uint32_t TCR;
volatile uint32_t PSC;
volatile uint32_t TCNT;
volatile uint32_t ARR;
}TIM_TypeDef;
#define EN_BIT 0
#define CLEAR_BIT 1
#define TIM_BASE_ADDR XPAR_TIMER_0_S00_AXI_BASEADDR// 0x44A40000
#define TIM0 (TIM_TypeDef *)(TIM_BASE_ADDR)
#define TIM_TCR (TIM_BASE_ADDR + 0x00)
#define TIM_PSC (TIM_BASE_ADDR + 0x04)
#define TIM_TCNT (TIM_BASE_ADDR + 0x08)
#define TIM_ARR (TIM_BASE_ADDR + 0x0b)
void TIM_Start(TIM_TypeDef *tim);
void TIM_Stop(TIM_TypeDef *tim);
void TIM_SetPrescaler(TIM_TypeDef *tim, uint32_t value);
uint32_t TIM_GetPrescaler(TIM_TypeDef *tim);
void TIM_SetReload(TIM_TypeDef *tim, uint32_t value);
uint32_t TIM_GetReload(TIM_TypeDef *tim);
uint32_t TIM_GetTCNT(TIM_TypeDef *tim);
void TIM_Clear(TIM_TypeDef *tim);
#endif /* DRIVER_TIM_TIM_H_ */
2) Driver Code
/*
* tim.c
*
* Created on: 2025. 3. 12.
* Author: kccistc
*/
#include "TIM.h"
TIM_TypeDef* TIM;
void TIM_Start(TIM_TypeDef *tim)
{
tim -> TCR |= (1<<EN_BIT);
}
void TIM_Stop(TIM_TypeDef *tim)
{
tim -> TCR &= ~(1<< EN_BIT);
}
void TIM_SetPrescaler(TIM_TypeDef *tim, uint32_t value)
{
tim ->PSC = value;
}
uint32_t TIM_GetPrescaler(TIM_TypeDef *tim)
{
return tim->PSC;
}
void TIM_SetReload(TIM_TypeDef *tim, uint32_t value)
{
tim -> ARR = value;
}
uint32_t TIM_GetReload(TIM_TypeDef *tim)
{
return tim->ARR;
}
uint32_t TIM_GetTCNT(TIM_TypeDef *tim)
{
return tim->TCNT;
}
void TIM_Clear(TIM_TypeDef *tim)
{
tim -> TCR |= (1<<CLEAR_BIT);
}
3) AXI Interrupt Controller 초기화, Timer Interrupt 처리 (제일 중요!!)
#include "xintc.h"
#include "xil_exception.h"
XIntc intc;
void timerIntrHandler()
{
static int counter = 0;
xil_printf("%d timer interrupt routine !!\n", counter++);
}
void intc_init()
{
// Hardware info setting
XIntc_Initialize(&intc, XPAR_INTC_0_DEVICE_ID);
// Exception, Interrupt Setting
Xil_ExceptionInit();
Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT, (Xil_ExceptionHandler)XIntc_InterruptHandler, &intc);
Xil_ExceptionEnable();
// User Logic Starts
// Interrupt ISR Function
XIntc_Connect(&intc, XPAR_INTC_0_TIMER_0_VEC_ID, (XInterruptHandler)timerIntrHandler, 0);
// Interrupt Enable
XIntc_Enable(&intc, XPAR_INTC_0_TIMER_0_VEC_ID);
// User Logic End
// Interrupt Start
XIntc_Start(&intc, XIN_REAL_MODE);
}
void intc_init()
{
// Hardware info setting
XIntc_Initialize(&intc, XPAR_INTC_0_DEVICE_ID);
// Exception, Interrupt Setting
Xil_ExceptionInit();
Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT, (Xil_ExceptionHandler)XIntc_InterruptHandler, &intc);
Xil_ExceptionEnable();
// User Logic Starts
// Interrupt ISR Function
XIntc_Connect(&intc, XPAR_INTC_0_TIMER_0_VEC_ID, (XInterruptHandler)timerIntrHandler, 0);
// Interrupt Enable
XIntc_Enable(&intc, XPAR_INTC_0_TIMER_0_VEC_ID);
// User Logic End
// Interrupt Start
XIntc_Start(&intc, XIN_REAL_MODE);
}
우선, 인터럽트 컨트롤러 초기화 함수를 작성한다. 해당 부분은 User Logic으로 표기된 부분 이외에는 공통적으로 사용된다.
void timerIntrHandler()
{
static int counter = 0;
xil_printf("%d timer interrupt routine !!\n", counter++);
}
최종적으로 타이머 인터럽트가 발생할 때 호출되는 인터럽트 서비스 루틴(ISR, Interrupt Service Routine)을 정의한다.
void apInit()
{
ledState = ALL_ON;
Led_Init(GPIOA);
Button_Init(&hBtnAllOn, GPIOB, 0); // 0: up, 1: left, 2: right, 3: down
Button_Init(&hBtnLeft, GPIOB, 1);
Button_Init(&hBtnRight, GPIOB, 2);
Button_Init(&hBtnAllOff, GPIOB, 3);
TickTimer_Init();
TIM_SetPrescaler(TIM0, 100000 - 1); // 1 sec Interrupt
TIM_SetReload(TIM0, 1000 - 1);
TIM_Clear(TIM0);
TIM_Start(TIM0);
}
apInit() 함수에서 필자가 작성한 Timer IP의 초기값 설정까지 완료되면, 작성한 인터럽트 서비스 루틴 (ISR, Interrupt Service Routine)대로 프로그램이 작동된다.
4. Interrupt를 활용한 Stopwatch 구현
#include "apMain.h"
#include "xintc.h"
#include "xparameters.h"
extern XIntc intc;
extern volatile uint32_t TIM_ms_counter;
uint32_t State;
btnHandler hBtnUp;
btnHandler hBtnLeft;
btnHandler hBtnRight;
btnHandler hBtnDown;
void apMain()
{
static uint8_t isInterruptEnabled = 1;
uint32_t minute_counter = 0;
uint32_t sec_counter = 0;
uint32_t hour_counter = 0;
apInit();
Fnd_DisplayOn(FND);
Led_OnAll(GPIOA);
while(1)
{
if(TIM_ms_counter >= 100){
TIM_ms_counter = 0;
sec_counter ++;
}
if(sec_counter == 60){
sec_counter = 0;
minute_counter ++;
}
if(minute_counter == 60){
minute_counter = 0;
hour_counter++;
}
Fnd_WriteDot(FND, 0b0100);
FSM_ContextSwitch();
switch (State) {
case SEC_MS:
Fnd_WriteData(FND, (sec_counter * 100) + TIM_ms_counter);
break;
case MINUTE_SEC:
Fnd_WriteData(FND, (minute_counter * 100) + sec_counter);
break;
case HOUR_MINUTE:
Fnd_WriteData(FND, (hour_counter * 100) + minute_counter);
break;
}
if(Button_GetState(&hBtnDown) == ACT_PUSH)
{
if(isInterruptEnabled){
XIntc_Disable(&intc, XPAR_INTC_0_TIMER_0_VEC_ID); // Interrupt OFF
isInterruptEnabled = 0;
}else {
XIntc_Enable(&intc, XPAR_INTC_0_TIMER_0_VEC_ID); // Interrupt ON
isInterruptEnabled = 1;
}
}
}
}
'AMBA BUS > Vitis' 카테고리의 다른 글
Vitis - 08. Application Software(AP) 구성 (0) | 2025.03.13 |
---|---|
Vitis - 06. FND 제어 (0) | 2025.03.12 |
Vitis - 05. Common Code 작성 (0) | 2025.03.12 |
Vitis - 04. Driver Code 작성 (0) | 2025.03.10 |
Vitis - 03. 사용자 IP와 AXI4-Lite Interface 결합 (0) | 2025.03.10 |