ITEEDU

掌握了控制频率和时间的方法,就可以编制出演奏音乐的程序了。请看下面的乐谱:

识谱的朋友马上就能唱出这段旋律,将程序MUSIC.ASM编译并运行,计算机也会为你奏出这首动听的歌。

MUSIC.ASM
DATA	SEGMENT	
	ASSUME DS:DATA	
FREQ	DW 0,4552,4029,3617,3415	
	DW 3042,2710,2415,2279			;定义各频率对应的N值
NOTE	DB 3,2,1,2,3,3,3,2,2,2	
	DB 3,5,5,3,2,1,2,3,3,3	
	DB 3,2,2,3,2,1,-1				;定义乐谱,-1表示结束
DL_TIME	DB 4,4,4,4,4,4,8,4,4,8	
	DB 4,4,8,4,4,4,4,4,4,4	
	DB 4,4,4,4,4,8					;定义各音符的节拍
DATA	ENDS	
		
CODE	SEGMENT	
	ASSUME CS:CODE	
MAIN	PROC FAR	
	MOV AX,DATA						;取数据段地址
	MOV DS,AX						;设定DS
		
	MOV AL,10110110B	
	OUT 43H,AL 						;初始化8253 TIMER 2
;-------------------------------------	
PLAY:	MOV BX,0					;设定音符数组下标初值
	MOV AL,NOTE[BX]					;取一个音符
	MOV AH,DL_TIME[BX]				;取对应节拍
	INC BX							;数组指针加1
	PUSH BX							;保存音符指针
	PUSH AX							;保存节拍值
	CBW								;将音符转为16位
	SHL AX,1						;音符值×2,计算频率表指针
	MOV BX,AX						;频率表指针置入BX
	MOV AX,FREQ[BX]					;取出对应频率值
;-------------------------------------	
	OUT 42H,AL						;输出频率值
	MOV AL,AH	
	OUT 42H,AL	
		
	IN AL,61H						;开启定时器
	OR AL,3	
	OUT 61H,AL	
		
	MOV AH,0						;取时钟计数值
	INT 1AH	
	POP AX							;取回节拍值
	MOV AL,AH	
	CBW								;节拍值转为16位
	ADD AX,DX						;加上当前时钟计数值,得到计数终值
	MOV BX,AX						;计数终值置入BX
		
DELAY:	MOV AH,0	
	INT 1AH							;取时钟计数值
	CMP BX,DX						;到终值吗?
	JNZ DELAY						;未到,继续延时
		
	IN AL,61H						;延时结束,关闭定时器
	AND AL,0FCH	
	OUT 61H,AL	
		
	POP BX							;取回音符数组指针
	CMP NOTE[BX],-1 				;演奏结束吗?
		
		
		
	JNZ PLAY	;未结束,继续演奏下一个音符
	MOV AH,4CH	;选择DOS API的4CH功能
	INT 21H	;终止进程,返回DOS
MAIN	ENDP	
CODE	ENDS	
	END MAIN 	

程序中多了两个新的指令--CBW和SHL,同时还有一个新的伪指令──DW(Define Word)。这个伪指令的作用同DB类似,只不过它用来定义十六位数据,也就是一个"字"。

助记符:CBW(Convert Byte to Word)
用 途:将AL寄存器中的一字节数据转换成一个字,高八位扩展到AH中
格 式:CBW
执 行:AL的低7位状态不变,最高位被扩展成八位并存入AH寄存器中

此指令用于将八位数据转换成十六位,而且数的大小不变。转换过程并不复杂:如果AL寄存器的最高位(bit7)为0,则AH寄存器就被置成00H;如果AL寄存器的最高位为1,那么AH寄存器就被置成0FFH。例如若AL为0FH,AH为01H,则执行CBW指令后AX寄存器将是000FH;若AL为0F0H,AH为01H,则执行CBW后AX寄存器将是0FFF0H。

"有没有搞错啊!0F0H和0FFF0H可能相等吗?"
先不要惊奇,粗看上去这两者并不一样,但它们的确相等。现在我们先记住这个结论,详细的情况将在下面讨论。

助记符:SHL(Shift Left)
用 途:将寄存器或存储器中的数据向左移位
格 式:SHL 寄存器,1
SHL 寄存器,CL
SHL 存储单元,1
SHL 存储单元,CL
执 行:寄存器或存储器中的数据所有位都向左移动一位或CL寄存器中指定位数,最高位移入CF标志

SHL指令和前面所讲的SHR指令功能是相对的,这个指令在程序中的应用很特别,实际上程序是应用了移位的性质,即将一个数向左移一位相当于将这个数乘2,左移两位相当于乘4,而左移三位相当于乘8。同样向右移一位则相当于除以2,右移两位相当于除以4。

这个规律很容易验证:将1(0001b)左移两位可得0100b,恰好是4,而把8(1000b)右移两位即可得2(0010b)。不过必须说明一点,这个结论只对正数成立。

这是一个EXE程序,但是它和PROG7-A又有不同:主过程一开始并未初始化堆栈建立返回地址,而且结束进程用了21H中断的新功能--4CH。4CH功能是DOS提供而且提倡使用的进程结束功能,它比INT 20H要优越,而且这个功能允许向DOS返回一个错误码。它的应用方法如下:

功能号:4CH
用 途:结束进程,返回操作系统,同时返回错误码。
参 数:AH=4CH
AL=错误码
调 用:INT 21H
返 回:无

批处理文件(.BAT)中常见象"IF ERRORLEVEL 4 GOTO END"这样的语句。其中的"ERRORLEVEL"就是刚运行过的程序通过4CH功能返回给DOS的错误码。这个错误码不仅可用于批处理,而且也可用于其它程序。程序可以通过DOS提供的"4DH"功能取到上一个程序留下的错误码来判断上一个程序执行的情况。关于这方面更多的知识这里不作详细介绍,可查阅有关书籍。现在只需知道这个功能可以十分轻松地结束进程就可以了。

MUSIC程序并未返回什么有实际意义的错误码,因为没有必要。图4-3是这个程序对应的流程图,在数据段中我们定义了三个表:FREQ表中定义了休止符、中音C-B各音符对应的N值;NOTE表中定义了乐谱,我们用数字0表示休止符,用1-7表示C-B,这样做的目的是便于用这些数字作为指针在FREQ表中找到N值。

DL_TIME表中定义了每个音符的节拍,我们用220Ms作为一个四分音符的发音时间,对应时钟计数值为4,这使得乐曲听起来比正常的要快一些。那么程序是如何以"音符"作为指针寻找对应N值的呢?

请看用方框框住的程序段:程序先将BX寄存器置成0,以BX为指针分别在NOTE表和DL_TIME表内取得音符值和时间值。NOTE[BX]表示NOTE表内的第"BX"个数据,此写法和C语言中数组的表示法类似,其实NOTE本身就是一个"CHAR"类型的数组。

取到音符和时间后程序把BX寄存器加1,也就是把数组下标加1,而后将BX和AX保存到堆栈中,因为这两个寄存器下面还要使用。程序用CBW指令将音符值由"字节"转换成"字"并将其拷贝到BX中,然后再以BX为下标在FREQ数组中找到音符对应的N值。将此N值送入定时器,并将定时器和电子开关打开,此时就能听到声音。取得时钟计数值,从堆栈中弹出AX,将AH寄存器中的节拍值转成字,利用前面讲的方法完成延时后关闭定时器通道3,至此一个音符发音结束。