「杂文」基于 MASM 的音乐盒程序

Luckyblock / 2024-04-23 / 原文

目录
  • 写在前面
  • 问题描述
  • 实验环境
  • 实验原理
    • 主界面
    • 播放过程中切歌/退出
    • 乐理
    • 控制扬声器
  • 代码
  • 吐槽
    • \(\text{533H} \times 896 = \text{123280H} \not= \text{1234DCH}\)
  • 参考

写在前面

计算机原理与汇编实验实验报告。

因为比较好玩于是先在这里随性地写一份然后再改成杀软实验报告。

问题描述

实现一个音乐盒程序,要求:

  1. 主界面显示点歌单,至少要有 3 首乐曲;
  2. 按相应的按键(1、2、3 等)选择对应的乐曲;
  3. 按 Q 键退出;
  4. 在乐曲演奏过程中按下相应按键可以播放另一首乐曲或退出。

实验环境

  • IDE:Visual Studio Code 1.88.1 with Extension MASM/TASM v1.1.1
  • 汇编工具:MASM-v6.11
  • DOS环境模拟器:jsdos

实验原理

主界面

使用 INT 21H 调用 DOS 的 9 号功能完成字符串输出,输出主界面。

输出主界面后再调用 DOS 的 1 号功能进行键盘读入单个字符,据此判断接下来要执行的操作。

代码节选如下:

DATA SEGMENT 
  MENU DB 0DH, 0AH, 'Choose the song you want and press the key:'
  DB 0DH, 0AH, '1: Senbon Zakura'
	DB 0DH, 0AH, '2: Merry Christmas Mr. Lawrence'
	DB 0DH, 0AH, '3: Bad Apple!'
	DB 0DH, 0AH, 'q: EXIT'             ;退出
	DB 0AH, 0AH, '$'
DATA ENDS

INPUT:                    ;控制音乐播放的主程序
  LEA DX, MENU            ;主界面字符串
	MOV AH, 9               ;调用9号中断,将菜单显示在屏幕上
	INT 21H                   
	MOV AH, 1
	INT 21H                 ;调用1号中断,输入播放哪首音乐或者退出播放

播放过程中切歌/退出

要求实现在播放过程中随时可以切换歌曲或退出,一个类似多线程输入的功能。

考虑在循环枚举音符并进行发声的同时,使用 INT 16H 的 1 号功能。该功能用于查询键盘缓冲区,对键盘扫描但是不等待(也就是不会中断程序),不会删除缓冲区中对应字符,并设置标志寄存器中的 ZF。若如果有键盘输入(即键盘缓冲区不空),则令 ZF=0,令 AL 存放当前输入的 ASCII 码,AH 存放输入字符的扩展码;若无键盘操作,则标志位 ZF=1

于是可通过如下代码检查是否读入了字符,若读入则中断当前过程并跳转到主界面,再在主界面中通过 DOS 的 9 号功能,从缓冲区读入字符并判断接下来的操作:

MOV AH, 1
INT 16H
JNZ INPUT

乐理

妈的为什么做汇编实验需要乐理啊幸好我学过点,已弃坑的:「学习笔记」乐理从入门到出门 - Luckyblock - 博客园。

乐谱中每个音符具有音高(频率)和音长(持续时长)两种属性。将音符转换为相应频率的脉冲方波,通到扬声器上即可发出该音符的声音。此时控制输出波形的频率即可控制音高,通过维持频率波形的时间即可控制音长。

转换过程参考如下音符与频率对照表:

控制扬声器

考虑使用计算机内部的 8253 芯片进行计数与定时并产生音符对应的脉冲方波信号,8255 芯片向扬声器进行数据传输。

对于 8253 芯片:

  • 其控制端口地址为 43H,定时器 2 端口地址为 42H
  • 初始化,将芯片设置为模式 3;此时输出线中 0 和 1 各占计数时间的一半,从而产生一系列间隔均匀的方波信号,从而通过输出波形的频率控制音调
  • 对定时器 2 编程,使其寄存器接收控制声音频率的计数值。石英震荡器每秒震荡 1193180 = 1234DCH 次,也即 8253 芯片的主频为 1.193180Mhz。因此对于频率为 \(f\) 的音符,其对应方波频率的计数值 \(F\)(即每秒内产生方波的数量)可由下式计算:

\[F = \left\lfloor\dfrac{\text{1234DCH}}{f}\right\rfloor \]

  • 计算出方波频率的计数值 \(F\) 后,将其送入定时器 2 的端口地址,产生方波作为 8255 芯片的控制信号。

对于 8255 芯片:

  • 其 PB 端口地址为 61H
  • 当端口 PB 的第 0 位 PB0 为 1 时,控制 8253 定时器来驱动扬声器,发声频率由 8253 芯片定时器 2 输出 OUT2 决定;当第 1 位 PB1 为 1 时,扬声器的门电路接通开始发声,并一直保持到位 1 变为 0 时关闭停止发声,即控制电路能以位触发和定时器控制两种不同的方式驱动扬声器发声。
  • 通过循环控制 PB0,PB1 均为 1 的时间间隔,从而通过改变扬声器的发声时间以控制音长

控制扬声器,输出一个音符代码节选如下:

;调用 sound 前,将音高传入寄存器 DI,音长传入寄存器 BX

sound proc near
    PUSH AX 
    PUSH BX 
    PUSH CX 
    PUSH DX 
    PUSH DI 

    MOV AL, 0B6H   ;8253 初始化
    OUT 43H, AL    ;43H 是 8253 芯片控制口的端口地址
    MOV DX, 12H    ;高 16 位
    MOV AX, 3280H  ;低 16 位                                        
    DIV DI         ;计算分频值, 赋给 AX, DI 中存放声音的频率值。
    OUT 42H, AL    ;先送低 8 位到计数器,42H 是 8253 定时器 2 的端口地址
    MOV AL, AH 
    OUT 42H, AL    ;后送高 8 位计数器
		
	  ;设置 8255 芯片, 控制扬声器的开/关
    IN AL, 61H    ;读取 8255 B 端口原值
    MOV AH, AL    ;保存原值
    OR AL, 3      ;使低两位置变为 1,打开开关
    OUT 61H, AL   ;开扬声器, 发声
		
WAIT1:    
    MOV CX, 28000 ;设置一拍的长度                                          
DELAY1:   
      NOP
	  LOOP DELAY1
    DEC BX        ;循环 BX 拍
    JNZ WAIT1 

    MOV AL, AH    ;恢复扬声器端口原值
    OUT 61H, AL 
    
    POP DI 
    POP DX 
    POP CX 
    POP BX                                                 
    POP AX 
    RET 
sound ENDP

代码

见 Github 项目:https://github.com/Luckyblock233/Musicbox-based-on-MASM。

吐槽

\(\text{533H} \times 896 = \text{123280H} \not= \text{1234DCH}\)

每个音符的频率值 \(f\) 经过转换后送入定时器的 42H 端口,以产生相应频率 \(F\) 的脉冲。当定时器的计数值为 533H 时能产生 896 Hz 的声音,则转换的公式为:

\[F = \frac{\text{533H} \times 896}{f}=\frac{\text{1234DCH}}{f} \]

中文互联网上有关该实验的相关内容中,在计算音符对应方波频率的计数值时无一例外地提到了上述结论。然而用计算器算了八十万遍也只能得到 \(\text{533H} \times 896 = \text{123280H} \not= \text{1234DCH}\),到底怎么转换成右侧形式的?为什么非要转换成右侧形式?

于是去查询了 1234DCH 这个常量的信息,发现该常量即石英震荡器每秒的震荡频率的近似值 1193180HZ,即芯片工作的主频,也即计时器的时钟输入。于是恍然大悟:\(\frac{\text{1234DCH}}{f}\) 即计算通过计数器的时钟输入每秒可以产生多少频率为 \(f\) 的方波信号,也即定时器的计数值。

既然等式右侧的转换是有实际意义的,那么问题来了,定时器的计数值为 533H 时能产生 896 Hz 的声音这个莫名其妙的用结果反推过程的结论,以及基于这个结论的莫名奇妙的式子 \(\text{533H} \times 896 = \text{1234DCH}\) 又是从何而来?为什么大家都要提一嘴这东西?

于是跑去 Google 了一下,发现该式比较早的出处为 2004 年机械工业出版社出版的《微机接口技术 500 问 - 李恩林 陈斌生》中例 4-74 举例说明用 8253 如何产生乐曲的代码注释中,节选如下:

mov al, 0b6h          ;置 8253/ 8254 方式寄存器的值
out 43h, al           ;43h 为 8253/ 8254 方式寄存器地址
mov dx, 12h
mov ax, 533 h * 896   ;当定时器的计数值为 533h 时, 能产生 896 Hz 的声音 533h * 896 = 1234dch
div di

据我个人猜测,此书大概在曾经计算机专业的授课中占有一定地位,于是该样例被选入了各大高校的实验中计算机原理实验,这个莫名奇妙的结论于是被原封不动地抄进了实验指导书。曾经学习计算机原理并被分配到做这个实验的先辈们也没有在意这个莫名奇妙的等式是否正确、从何而来,这个莫名奇妙的结论于是又被原封不动地抄进了实验报告里。

终于在 20 年后的今天——一个尝试搞清楚原理的大冤种用计算器验算了一下——发现这他妈有问题啊啊啊啊!!!

令人感慨!

参考

原理:

  • 主要参考:基于汇编语言的音乐盒设计_音乐盒程序汇编-CSDN博客
  • 微机原理课程设计乐曲演奏程序设计与实现 - 百度文库
  • 汇编课设报告 字符分类统计和音乐盒.doc
  • 基于汇编语言的音乐盒设计与实现_音乐盒汇编语言-CSDN博客
  • 汇编语言中OUT和IN的用法_汇编语言out-CSDN博客
  • x86汇编利用int 16h中断实现伪多线程输入 - scyq - 博客园
  • 键盘I_O中断调用(INT 16H)-CSDN博客
  • 键盘I_O中断调用(INT 16H)和常见的int 17H、int 1A H_int13中断的17h功能用法-CSDN博客
  • 微机原理 __ 8253 芯片 (详细讲解 + 经典例题)-CSDN博客
  • 8253练习题(8253端口地址怎么求?怎么求初值?怎么看出工作方式)-CSDN博客
  • 微机接口芯片(1)—— 可编程并行接口芯片8255_8255芯片-CSDN博客
  • 关于8253 芯片计数器初值的问题_8253用小时频率输出初值是多少-CSDN博客
  • EE314 - labs - labmanual - EXPERIMENT FIFTEEN INTERRUPT HOOKS
  • Dos系统功能调用表9号功能_dos9号-CSDN博客
  • INT 21H 指令说明及使用方法(汇编语言学习)_int 21h指令-CSDN博客
  • 汇编语言(第3版,王爽著):第 17 章 使用 BIOS 进行键盘输入的学习总结 - 夏夜星空晚风 - 博客园

简谱:

  • 千本樱 简谱-简谱口琴谱-学口琴音乐网
  • 圣诞快乐劳伦斯先生钢琴简谱-坂本龙一演唱-看谱啦
  • Bad Apple!!_简谱_搜谱网