JZ2440裸板開發練習#7 中斷與異常(2)

中斷

中斷控制器

本篇講一講中斷,上篇中已經講述了異常和異常的處理方式,中斷是異常的一種,因此處理方式也大同小異,與其他異常的主要差別是,存在中斷控制器這一角色。

中斷的流程是中斷源->中斷控制器->處理函數,中斷源有很多種類,可以是串口中斷、時鐘中斷或者是外部中斷。而控制器則控制着中斷是否屏蔽、中斷的優先級以及中斷的狀態。處理函數則與異常的處理函數無異,只要根據自己需要進行編寫即可。

 上圖爲中斷控制器框圖,最左爲中斷源,分成了兩種中斷源,區別僅在於sub source有多兩個寄存器SUBSRCPND和SUBMASK來控制中斷。中斷源歸於哪一類在S3C2440中有表列出:

 這裏主要討論的是source中斷源,可以看到描述一列中有具體的中斷源描述,可以根據自己需要一一對應,但是最重要理解的還是Arbiter一列中的分組,source中斷源被分成了5組,每組最多不超過6箇中斷源,最終分配結果如下圖,這種分配方式主要是爲了中斷優先級的實現,每一組的ARBITER存在兩個控制信號:1bitARB_mode,2bitsARB_SEL,其中ARB_SEL用來選擇中斷優先級排列的方式。可以看到如下圖,ARB_SEL兩位的四種組合所表示的四種優先級順序,其中REQ0爲固定的最高優先級,相對應的REQ5爲固定的最低優先級,而REQ1-4則隨着ARB_SEL的增大循環左移,我們可以直接設置ARB_SEL來確定優先級順序,但是優先級順序還受到ARB_mode影響,當ARB_mode爲0時,我們通過ARBS_SEL確定的優先級順序則永久固定,除非我們再去修改。當ARB_mode爲1時,每一次響應中斷,都會讓ARB_SEL增大1,即循環左移一個,從而導致最高優先級的中斷變爲最低優先級,從而保證較低優先級的中斷能夠有機會得到響應,防止飢餓,這種也叫輪轉調度,跟os的調度類似,因此這裏ARB_mode也可以說是輪轉調度的開關。

 中斷控制器針對source中斷源來說有5中控制寄存器:source pending register, interrupt mode register, mask
register, priority register和interrupt pending register
。可以和文首的框圖一一對應上。

其中source pending register和interrupt pending register類似,用來表示哪種中斷觸發了,當中斷觸發,對應中斷位就會置1,差別在於SRCPND會將所有觸發的中斷置位,而INTPND只會對當前需要處理的最高優先級的中斷置位,SRCPND通過MASK屏蔽和優先級寄存器篩選出最高優先級中斷給到INTPND,INTPND接收到之後進入中斷處理。需要注意的是,這兩個寄存器需要在處理完成之後清除,否則會一直響應已經處理過重複的中斷。且需要從源頭開始SRCPND->INTPND,這樣纔不會清除INTPND後SRCPND再次置位。置位的方式是在對應的位上置1即可。

 MASK register用來屏蔽中斷,當中斷源對應位上置1,則表示系統不響應中斷,即使SRCPND有中斷進來且置位,也不做響應,但是如果INTPND直接置位了,還是需要響應處理的(INTPND在MASK之後,不受MASK控制)。

 Mode register用來控制中斷是普通中斷還是快中斷,在上一節中已經說明FIQ因爲有較多自己的專屬寄存器,因此保存現場和恢復現場的開銷較小,響應速度也比較快,因此被稱爲快中斷。這裏默認值爲0,即默認情況下位普通中斷。

 priority register上文中已經講過了,只需要根據需要開啓輪轉開關,設置中斷優先級順序即可。

 

 除了上述控制器外,如果需要系統響應中斷,還需要把上節中提到的CPSR中的I-bit 或者F-bit開啓,這是系統的中斷總開關,方法爲將I-bit或者F-bit清0。

補充一點,S3C2440爲了方便中斷處理函數中分辨中斷源,設置了INTOFFSET寄存器,不同中斷源有不同的offset value,直接用標準值比較該寄存器即可。

 

外部中斷

除了中斷控制器需要配置,中斷源也需要進行配置。這裏以外部中斷爲例,外部中斷觸發開關爲按鍵,期望爲按鍵按下,觸發中斷,LED點亮。

https://blog.csdn.net/G_METHOD/article/details/104271762中也編寫過按鍵的內容,那次是使用輪詢的方式來進行按鍵狀態檢測,比較消耗CPU資源,而使用外部中斷的方式可以讓無事件發生時CPU處理其他事情,而有事件觸發時,又能夠及時處理。copy一下按鍵的原理圖和鍵腳對應的表格。

EINT0 EINT2 EINT11 EINT19
GPF0 GPF2 GPG3 GPG11

 上次我們將其設置爲輸入引腳從而可以讀引腳獲取按鍵狀態,這次需要用到外部中斷,則需要將其設置爲對應的外部中斷模式。從下圖可知,只要將GPxCON對應的引腳設置爲外部中斷引腳即可。

 除了GPIO的寄存器需要設置,還需要設置外部中斷的寄存器。如下,設置EXTINT可以指定外部中斷的觸發方式爲低電平、高電平、上升沿、下降沿或者雙邊沿觸發。這裏我們選擇使用雙邊沿觸發,根據自己需要或者喜好進行選擇即可。

 需要注意的是有些外部中斷引腳存在濾波,玩過32或者51的外部中斷可以知道,按鍵按下電平是會存在抖動的,如果不對抖動處理,則一個觸發中斷的按鍵按下動作可能會觸發多箇中斷事件,比如按鍵用來點燈,但是抖動觸發了多一次,讓你每次按下按鍵燈都亮滅一次。防抖動的方式可以是濾波,濾波這裏粗暴的方式就是一段延時等待電平穩定,這裏芯片幫我們做了濾波,當然可以寫寄存器來關閉濾波功能,也有其他寄存器能夠配置濾波的時鐘和時間。

 

 與中斷控制器比較相似的是,外部中斷寄存器也存在mask寄存器和PEND寄存器,同樣,mask用來屏蔽中斷,而PEND指示那一路外部中斷觸發。同樣地,處理完中斷之後需要清除PEND相應位。

 上述中斷寄存器的控制作用於中斷源,但是中斷處理函數中,在判斷中斷源時,如果使用中斷控制器的PND不能判斷出具體的中斷源時(如EINT存在幾個外部中斷共用一個標誌位),此時就需要藉助外部中斷寄存器PND來分辨具體是哪一個中斷源了。

根據上述內容輸出代碼如下:

.text
.global _start

_start:
	B RESET	
	LDR pc,UNDEFIE
	LDR pc,SW_INTERRUPT
	B ABORT_PREFETCH
	B ABORT_DATA
	B halt				//reserve
	LDR pc,INTERRUPT
	B FIQ_HANDLE

UNDEFIE:
	.word DO_UND	
	
SW_INTERRUPT:
	.word DO_SWI

INTERRUPT:
	.word DO_INTERRUPT
	

ABORT_PREFETCH:
ABORT_DATA:	
IRQ_HANDLE:
FIQ_HANDLE:
	B halt

DO_UND:
	//1.設置棧
	LDR SP,=0x34000000
	//2.保存現場
	STMDB SP!,{R0-R12,LR}

	//處理函數
	MRS R0,CPSR
	LDR R1,=UND_TEST_STRING
    BL ExecptionHandle
	
	//3.恢復現場,跳轉回原來的位置
	LDMIA SP!,{R0-R12,PC}^  //^ 表示將SPSR恢復到CPSR中
	
DO_SWI:
	//1.設置棧
	LDR SP,=0x33e00000
	//2.保存現場
	STMDB SP!,{R0-R12,LR}
	
	//處理函數
	MRS R0,CPSR
	LDR R1,=SWI_TEST_STRING
	SUB R2,LR,#4
    BL SWIHandler
	
	//3.恢復現場,跳轉回原來的位置
	LDMIA SP!,{R0-R12,PC}^  //^ 表示將SPSR恢復到CPSR中

DO_INTERRUPT:
	//1.設置棧
	LDR SP,=0x33d00000
	//2.保存現場
	SUBS LR, LR, #4
	STMDB SP!,{R0-R12,LR}
	
	//處理函數
    BL InterruptHandler
    bl testPrint1
	
	//3.恢復現場,跳轉回原來的位置
	LDMIA SP!,{R0-R12,PC}^  //^ 表示將SPSR恢復到CPSR中

UND_TEST_STRING:
	.string "enter undefin mode!\n"
	
SWI_TEST_STRING:
	.string "enter swi mode!\n"

.align 4
RESET:
	MOV R0,#0
	LDR R1,[R0]
	
	STR R0,[R0]
	LDR R2,[R0]
	
	CMP R2,R0
	LDR SP,=0x40000000+4096
	MOVEQ SP,#4096
	STREQ R1,[R0]
	
	BL HardwareInitAll
	BL UartInit
	
TEST_UND:
	bl testPrint
	.word 0xdeadc0de
	//bl testPrint
TEST_SWI:
	MRS R0,CPSR
	BIC R0,R0,#0x0F
	MSR CPSR,R0
	SWI 0x123
	
	LDR pc,=main
halt:
    B halt
interrupt.h
----------------------
#ifndef __INTERRUPT_H
#define __INTERRUPT_H


void AllInterruptInit(void);
void InterruptHandler(void);


#endif


 

interrupt.c
-------------------------------

#include "interrupt.h"
#include <stdint.h>

#include "led.h"
#include "s3c2440.h"


static void InterruptControllerInit(void)
{
	//開啓全局中斷開關
	asm(
		"MRS R0,CPSR \n\t"
		"BIC R0,R0,#0x80 \n\t"
		"MSR CPSR,R0 \n\t"
	);

	//關閉相關中斷的屏蔽
	INTMSK &= ~((1<<0) | (1<<2) | (1<<5)); 

}

static void ExternInterruptInit(void)
{
	//設置爲外部中斷引腳
	GPFCON &= ~((3<<0)|(3<<4));
	GPFCON |= (2<<0)|(2<<4);
	
	GPGCON &= ~((3<<6)|(3<<22));
	GPGCON |= (2<<6)|(2<<22);

	EXTINT0 |= (7<<0) | (7<<8);     /* S2,S3 */
	EXTINT1 |= (7<<12);             /* S4 */
	EXTINT2 |= (7<<12);
	
	/* 設置EINTMASK使能eint11,19 */
	EINTMASK &= ~((1<<11) | (1<<19));
}


void AllInterruptInit(void)
{
	InterruptControllerInit();
	ExternInterruptInit();
}

void ExtInterruptHandler(uint32_t irq)
{
	uint32_t ext_interrupt_bit = EINTPEND;
	
	switch(irq)
	{
		case 0:
		{
			ToggleLed(kLed1);
			break;
		}

		case 2:
		{		
			ToggleLed(kLed2);
			break;
		}
		
		case 5:
		{
			if(ext_interrupt_bit &( 1 << 9 ))
			{		
				ToggleLed(kLed3);
			}
			else if(ext_interrupt_bit &( 1 << 11 ))
			{			
				ToggleLed(kLed1);
				ToggleLed(kLed2);
				ToggleLed(kLed3);
			}
			
			break;
		}
		default:
			break;
	}
	
	EINTPEND = ext_interrupt_bit;
}

void InterruptHandler(void)
{
	uint32_t interrupt_bit = INTOFFSET;

	if(interrupt_bit>=0 && interrupt_bit <=5)
	{
		ExtInterruptHandler(interrupt_bit);
	}

	SRCPND = 1<< interrupt_bit;
	INTPND = 1<< interrupt_bit;
}


編寫這段代碼實際上工作量不大,有些寄存器直接使用默認值而不加修改,但是仍然卡了我很久,主要是按下按鍵執行中斷函數後總是會莫名其妙跑飛或者莫名重啓,最後看dis才發現是上一節中講到的不同異常的返回值需要不同處理的這點,中斷的返回值需要減去4才能返回到程序的正常流程。

 

定時器

定時器從名字可以知道是用來計時的,把它放在中斷這裏是因爲他存在中斷的功能,而非定時器只有中斷的功能。定時器通過設定一定時間後產生中斷可以用來週期性或者時間性地執行任務,另外一方面,通過定時器的比較寄存器能夠讓定時器引腳輸出PWM波,供電機或者其他需要控制能量的外設使用,呼吸燈就可以通過該功能實現,同時也能夠將其輸出作爲外設的時鐘源。

S3C2440有5個16位時鐘,不同時鐘估計因爲成本或者實現難度的原因,功能存在閹割或者共用資源的情況。TIMER0是功能最完整的一個定時器。

 從左往右看,可以看到定時器使用PCLK爲驅動時鐘,之前的內容中已經將PCLK設置爲50MHz。PCLK之後存在一個8位的預分頻器,可以根據需要分頻使用,即降低使用的時鐘頻率,其後還有divider可以在次降低頻率,這裏的分頻通過設置MUX寄存器選擇一個分頻數。定時器存在TCMPB、TCNTB寄存器,其中TCMPB爲比較寄存器,用於與當前計數值比較從而輸出PWM波,TCNTB則爲定時器設置的初值,當定時器計數值爲0時,且我們設置了自動加載,此時TCNTB則會自動賦值到計數寄存器中。還要說明的是這兩個寄存器可以隨時修改,但是不會隨時生效,需要在下一個計時週期開始的時候才能生效,這樣能夠保證當前正在進行的計數能夠正常運行。隨後出現的是數字電路中的取反,後面跟隨的梯形是選擇器,由一個invert bit來控制輸出的波形是否取反。Dead Zone Generator死區產生器,是用於PWM帶大功率電機時的一種保障措施,經典的應用在H橋電路控制電機。大致示意圖如下(手頭沒有AD先用畫圖簡單畫一下···),中間M爲電機,使用三極管作爲放大電路器件,當A給導通電壓時,B給0電勢,此時A的電路導通,假設此時電機正轉對應下圖的第二種情況。當B給導通電壓,而A給0時,B側電路導通,電機此時反轉,對應下圖第三種情況。當電機需要從正轉轉爲反轉或者相反轉換時,勢必會經過一個A和B的電壓交換時間,而真實電壓都是模擬量,不可能突變。此時如果沒有一段死區時間,則可能A和B同時處於導通電壓範圍,造成下圖第四中情況,直接將電源和地短路,特別危險。死區可以幫助我們解決這個問題,具體的就不再拓展了。

S3C2440的計數器比較簡單,之前使用32還能設置計數器增加或者減少,這裏只能減少,也算省心,當計數器減爲0時,即爲定時器觸發中斷的時機。想要使用定時器中斷,除了上節中設置的定時器控制器,還需要把定時器先設置好,並且啓動。相關寄存器如下:

第一個是預分頻的8位計數,我們使用的是定時器1,只用於中斷,因此只設置預分頻器0即可。這裏還需要配合分頻係數來使用,通過這兩個數來讓時鐘降低到合適的值,這裏的值其實夠用就行,比如這裏直接設置爲99預分頻,同時使用16分頻,最終根據公式計算時鐘爲50MHz/((99+1)*16)=31250Hz。

 確定了時鐘後,需要確定計數值,從而得到定時器週期,這裏因爲不需要輸出PWM因此不需要設置比較寄存器,只需要設置計數TCMPB0初值即可。假設我們期望0.5s中斷一次,根據上面得到的時鐘,可以知道我們需要設置初值爲15625‬,即可得到0.5s中斷一次的預期效果。

 最後一個需要設置的是時鐘控制寄存器,同樣的只需要關注定時器0相關即可,設置定時器0自動重載初始值,由於不需要輸出時鐘到引腳,這裏invert可以不用管,使用默認值,根據芯片手冊所說,第一次啓動需要手動把manual update手動設置,但是在下次寫之前需要清除該位。最後啓動定時器。

 其他的內容就和外部中斷類似,只需要在外部中斷程序的基礎上,加上定時器初始化、定時器中斷處理函數,同時關閉定時器中斷屏蔽相關即可,由此,輸出代碼如下:

interrupt.c
------------------------------------

#include "interrupt.h"
#include <stdint.h>

#include "led.h"
#include "s3c2440.h"

typedef void (*irq_func)(int);
irq_func irq_array[32];

static void register_irq(int irq, irq_func fp)
{
	irq_array[irq] = fp;
	INTMSK &= ~(1<<irq);
}

void Timer0InterruptHandler(int irq)
{
	ToggleLed(kLed1);
}

void ExtInterruptHandler(int irq)
{
	uint32_t ext_interrupt_bit = EINTPEND;
	
	switch(irq)
	{
		case 0:
		{
			ToggleLed(kLed1);
			break;
		}

		case 2:
		{		
			ToggleLed(kLed2);
			break;
		}
		
		case 5:
		{
			if(ext_interrupt_bit &( 1 << 9 ))
			{		
				ToggleLed(kLed3);
			}
			else if(ext_interrupt_bit &( 1 << 11 ))
			{			
				ToggleLed(kLed1);
				ToggleLed(kLed2);
				ToggleLed(kLed3);
			}
			
			break;
		}
		default:
			break;
	}
	
	EINTPEND = ext_interrupt_bit;
}


static void InterruptControllerInit(void)
{
	//開啓全局中斷開關
	asm(
		"MRS R0,CPSR \n\t"
		"BIC R0,R0,#0x80 \n\t"
		"MSR CPSR,R0 \n\t"
	);
}

static void ExternInterruptInit(void)
{
	//設置爲外部中斷引腳
	GPFCON &= ~((3<<0)|(3<<4));
	GPFCON |= (2<<0)|(2<<4);
	
	GPGCON &= ~((3<<6)|(3<<22));
	GPGCON |= (2<<6)|(2<<22);

	EXTINT0 |= (7<<0) | (7<<8);     /* S2,S3 */
	EXTINT1 |= (7<<12);             /* S4 */
	EXTINT2 |= (7<<12);
	
	/* 設置EINTMASK使能eint11,19 */
	EINTMASK &= ~((1<<11) | (1<<19));
	
	register_irq(0,ExtInterruptHandler);
	register_irq(2,ExtInterruptHandler);
	register_irq(5,ExtInterruptHandler);
}

static void Timer0InterruptInit(void)
{
	TCFG0 = 99;  
	TCFG1 &= ~0xf;
	TCFG1 |= 3;

	TCNTB0 = 15625;

	TCON |= (1<<1);

	TCON &= ~(1<<1);
	TCON |= (1<<0) | (1<<3);

	/* 設置中斷 */
	register_irq(10, Timer0InterruptHandler);
}


void AllInterruptInit(void)
{
	InterruptControllerInit();
	ExternInterruptInit();
	Timer0InterruptInit();
}

void InterruptHandler(void)
{
	uint32_t interrupt_bit = INTOFFSET;

	irq_array[interrupt_bit](interrupt_bit);

	SRCPND = 1<< interrupt_bit;
	INTPND = 1<< interrupt_bit;
}


 有了前期的工作,定時器中斷這裏需要做的就很少了,這裏除去正常功能外,使用函數指針數組優化了代碼結構,在中斷初始化時,要求註冊中斷號和中斷處理函數,從而總中斷處理函數在後續增加功能過程中不需要改動,代碼耦合性降低。代碼效果爲led亮滅,目測0.5s哈哈。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章