最近上的一门选修课理论部分结课了，课程名是「接口与通信技术」，现在进入了实验阶段，要求用8086汇编实现利用INS8250的串口全双工通信。我的具体实现如下：因为我也没学过汇编，选这门课全靠头铁，遂花了几个小时自学了一下8086汇编，然后知道了个叫Emu8086的模拟器，这个东西我觉得就算是目前比较全面的模拟器了，虽然也可以安装DOSBox和MASM进行编译调试，但是问题在于INS8250不好找，也不好模拟，而且选修课还是8086汇编，如果大费周折配置这种一次性的环境，感觉不太划算，遂这里使用Emu8086，这个模拟器将输入输出映射到文件中，因此可以使用任意语言来拓展它的功能，这一点对于模拟INS8250芯片还是非常好的。关于INS8250，这里只进行简单的介绍，具体更详细的内容请各位移步它的数据手册。实际上要写这个程序其实都不用看我这篇文章，照着数据手册就能把程序给写了。因此这篇文章仅作为备忘。INS8250是以前IBM PC上负责串口通信的芯片，现在说8250基本上就泛指兼容8250的一套芯片了，在之后还有INS16550，它完全兼容8250芯片的代码，而且还比8250多了一些功能。但归根结底，这个实验只用8250就够了。8250用起来非常方便，初始化时按照数据手册设置到分频寄存器/除数寄存器就完成了波特率的设置，然后填写控制字告诉8250你需要几位停止字、校验位、停止位和数据位，以及校验方式，然后你只要监听它的状态字并根据你的需要取数据放数据，之后串口收发的工作就全都由8250完成，程序员就可以放心的去做别的事情了。8086汇编这个实验要做要给完整的程序：计算机A和B使用串口相联，然后在其中任意一台计算机上键入一个字符，在另一台计算机上就能显示出来，当按下ESC的时候程序便退出。解决的思路便是程序一开始先初始化8250，进行一些必要的设置之后就开始无限循环，每次循环做下列三件事之一：检查接收有无错误接收新字符发送字符检查错误的目的是出错时取走寄存器中的脏数据并在屏幕上打印一个问号来表示出错。接收新字符就是看看8250有没有接收到新字符，若有就打印在屏幕上。发送则是看键盘有没有输入，有就发送。这三件事按顺序分别检查，谁先满足条件就执行谁，执行完毕后就开始新一次循环，这样能够有效避免很多复杂性。代码如下：; Full Duplex Communication Program using INS8250 ; 3FBh - Line Control Register ; 3FDh - Line Status Register ; When D7 = 1: ; 3F8h - Division Latcher low 8bits ; 3F9h - Division Latcher high 8bits ; When D7 = 0: ; 3F8h - Recieve buffer / Transmit hold Register ; 3F9h - Interrupt Enable Register data segment ends stack segment dw 64 dup(0) ends code segment start: ; set segment registers: mov ax, data mov ds, ax mov es, ax ; initialize 8250 ; set DLAB(Division Latcher Access bit) to 1, to set division latcher mov dx, 3FBh mov al, 10000000b ; 1 0 000 0 00 b out dx, al ; set baud rate to 4800bps mov dx, 3F8h mov AX, 24 ; When using 1.8432Mhz, according to datasheet out dx, ax ; When writting 2byte, low 8bits goes to 3F8h and high 8bits goes to 3F9h ; set uart parameter: 7 bits data with no fancy things mov dx, 3FBh mov al, 00000010b ; 0 0 000 0 10 b out dx, al ; disable interrupt, here we using query looping mov dx, 3F9h mov al, 0 ; 0000 0000 means disable all interrupts out dx, al ; the main query loop mainLoop: ; to ack simulator only, no need for real hardwares mov dx, 3FFh mov al, 0FFh out dx, al ; query and wait line ; read line status register mov dx, 3FDh in al, dx ; bit 1~4 corresponds to different errors test al, 00011110b ; if any of those bit is 1, go error handler jnz errorHandler ; then lookup bit0, corresponds to receiver data ready test al, 00000001b ; if is 1, means data is ready to read jnz receiveHandler ; last thing is to check if ready to transmit ; bit5 corresponds to transmitter holding register empty test al, 00100000b ; if 1, means ready to put new data jnz sendData ; if none of them satisfied, keep waiting jmp mainLoop exit: ; return 0 mov ax, 4C00h int 21h ends ; if error occur, put a '?' on screen errorHandler: ; read broken data mov dx, 3F8h in al, dx ; print question mark on screen mov ah, 02h mov dl, '?' int 21h ; continue jmp mainLoop ; read data and print it on screen receiveHandler: ; read data mov dx, 3F8h in al, dx and al, 01111111b ; select only low 7bits, since we only send 7bits ; now al is the data we received ; print it to screen ; save al to prevent dos function change it push ax ; cannot perform push al, since stack operate 2 bytes at once mov ah, 02h mov dl, al int 21h ; recover data pop ax ; check if is '\CR' cmp al, 0Dh ; if not, continue jnz mainLoop ; if is, then "\CR\LF" means a new line ; print '\LF' mov ah, 02h mov dl, 0Ah int 21h ; continue jmp mainLoop ; send data, send one char at once ; press ESC to terminate program sendData: ; check if keyboard has input mov ah, 0Bh int 21h ; now al = 0 means no input, al = FF means has input ; if no input, continue cmp al, 0 jz mainLoop ; else read input mov ah, 0 ; function 0 of 16h has no echo int 16h ; using keyboard IO here, al is the character ;now al is the char we want to send ; if is ESC cmp al, 1Bh jz exit ; if not exit mov dx, 3F8h out dx, al ; done, continue jmp mainLoop end start 代码这里就不多说了，因为我不熟汇编，因此基本上每一句都写注释了，写程序的时候中英文切换很麻烦，所以就用的英文。以我这散装英文的水平大概大家都能看懂。有两点需要说明的是这里只用了一片8250，因此实际上是假的全双工。只是默认程序运行非常快，因此两个人同时按下按键导致同时发送的情况几乎不太可能，因为很微小的时间差就能决定其中一台计算机一定先进入发送模式，而另一台一定会进入接收而推迟当此发送。实际上这种资源抢争的情况还是有概率发生的，这里忽略了而已。如果需要实现真正的全双工，则需要使用两块8250。例如3F8是发，2F8是接收，这样的话要求接线时需要RX和TX两根线。由于实验内容是使用一根线数据线+一根地线，因此这里就没有实现。第二点需要说明之处在于主循环伊始大约46行处的三行代码，这个是专门给模拟器看的，实际硬件的话不需要这三行，这三行的存在反而还会带来意料之外的副作用。原因请看下面INS8250模拟器的说明。INS8250模拟器这里使用Kotlin编写模拟器，只要循环监视Emu8086的IO文件就是了。代码如下：import java.io.File import kotlin.random.Random // to show binary properly fun Int.formatBinary(): String = Integer.toBinaryString(this).takeLast(8).let { if (it.length < 8) "0".repeat(8 - it.length) + it else it } // easy to set line status register fun generateStatus(vararg status: Int): Byte = status.fold(0) { acc, i -> acc or i }.toByte() // write portFile and reload by return new value fun refresh(file: File, data: ByteArray = byteArrayOf()): ByteArray { if (data.isNotEmpty()) file.writeBytes(data) return file.readBytes() } fun main() { // emu8086 io file val portFile = File("C:\\Users\\<username>\\AppData\\Local\\VirtualStore\\emu8086.io") // Address of registers val lineControlRegisterAddr = 0x3FB val lineStatusRegisterAddr = 0x3FD val divisionLatcherRegisterLowAddr = 0x3F8 val divisionLatcherRegisterHighAddr = 0x3F9 val dataRegisterAddr = 0x3F8 val interruptEnableRegisterAddr = 0x3F9 // watch dog, let emulator know a new loop start val watchDog = 0x3FF // status val transmitError = 0b00011110 val readyToTakeData = 0b00000001 val readyToPutData = 0b00100000 // buffer var lineControlRegister = 0 var divisionLatcherLowRegister = 0 var divisionLatcherHighRegister = 0 var dataRegister = 0 var interruptEnableRegister = 0 // main loop while (true) { //at first if emu8086 don't output something, then this file is missing. var bytes = try { portFile.readBytes() } catch (e: Exception) { continue } // keep 8250 ready to send new data if (bytes[lineStatusRegisterAddr].toInt() and readyToPutData != readyToPutData) { //ready to accept data from 8086 bytes[lineStatusRegisterAddr] = generateStatus(readyToPutData) bytes = refresh(portFile, bytes) } // line control register change listener if (lineControlRegister != bytes[lineControlRegisterAddr].toInt()) { lineControlRegister = bytes[lineControlRegisterAddr].toInt() println("Line Control Register set to: " + lineControlRegister.formatBinary()) if (lineControlRegister and 0b10000000 == 0b10000000) { //change 0x3F8 and 0x3F9 to division latcher bytes[divisionLatcherRegisterLowAddr] = divisionLatcherLowRegister.toByte() bytes[divisionLatcherRegisterHighAddr] = divisionLatcherHighRegister.toByte() } else { // change 0x3F8 and 0x3F9 to data and ier bytes[dataRegisterAddr] = dataRegister.toByte() bytes[interruptEnableRegisterAddr] = interruptEnableRegister.toByte() } //update bytes bytes = refresh(portFile, bytes) } // can access division latcher if (lineControlRegister and 0b10000000 == 0b10000000) { // 0x3F8 and 0x3F9 is division latcher // division latcher low if (divisionLatcherLowRegister != bytes[divisionLatcherRegisterLowAddr].toInt()) { divisionLatcherLowRegister = bytes[divisionLatcherRegisterLowAddr].toInt() println("Division Latcher Low Register set to: 0x" + Integer.toHexString(divisionLatcherLowRegister)) } // division latcher high if (divisionLatcherHighRegister != bytes[divisionLatcherRegisterHighAddr].toInt()) { divisionLatcherHighRegister = bytes[divisionLatcherRegisterHighAddr].toInt() println("Division Latcher High Register set to: 0x" + Integer.toHexString(divisionLatcherHighRegister)) } } else { // can access receiver and transmitter register // 0x3F8 and 0x3F9 is data if (interruptEnableRegister != bytes[interruptEnableRegisterAddr].toInt()) { interruptEnableRegister = bytes[interruptEnableRegisterAddr].toInt() println("Interrupt Enable Register set to: " + interruptEnableRegister.formatBinary()) } // if new data is arrive if (bytes[dataRegisterAddr].toInt() != 0) { dataRegister = bytes[dataRegisterAddr].toInt() print("Transmit request: $dataRegister, ") //begin to transmit, clear ready to accept date bytes[lineStatusRegisterAddr] = generateStatus() refresh(portFile, bytes) // delay for transmit, wait for next loop // add delay here to make sure 8086 wait transmition finished Thread.sleep(5000) do { bytes = refresh(portFile) } while (bytes[watchDog].toInt() == 0xFF) bytes[watchDog] = 0 if (Random.nextDouble() <= 0.05) { // got an error bytes[lineStatusRegisterAddr] = generateStatus(transmitError) println("Transmit error") } else { // repeat char sent bytes[lineStatusRegisterAddr] = generateStatus(readyToTakeData) println("Loop char read") } bytes = refresh(portFile, bytes) //wait next loop, during this 8086 should take data // TODO change this to make sure only read data once. 400 is ok to delay 1ms per instruction Thread.sleep(400) bytes[dataRegisterAddr] = 0 refresh(portFile, bytes) } } } }程序的注释我认为非常详细，因此这里只着重说明两点。其一是关于Emu8086的IO文件，一开始是没有这个文件的，只有第一次使用out执行写入之后才会创建，而且是用多少写多少。比如说我写3F8h，那么文件最大长度也就是3F8，然后就没了。因此你需要手动拓展IO文件让它长度足够长，否则后面程序会报索引超长。因此这里一个取巧的办法就是让8086写一个很大的地址，这样能够保证涵盖需要的字节。其二是在硬件中如果你访问了3F8取走了数据，那么地址产生片选信号给接收数据寄存器的同时，还会产生一个复位信号给线路状态寄存器来清除接收寄存器已满的状态。但是这里我们没法知道Emu8086是否读取了接收寄存器，因此我这里手动的选了地址0x3FF作为看门狗，每次循环伊始便写一次这个地址，既能保证IO文件足够长，又能让8250模拟器直到8086的运行状态。当然了，一个更理想的办法自然是初始化时便写入0x3FF，然后每次读取的时候写一次0x3FF，这样更方便实现，但是考虑现实的话，如果过几周开学了我肯定要去机房调程序，那个时候我肯定已经忘了这些细节了，如果因为这些写入3FF而出现负作用，肯定会让我很烦恼。因为在循环一开始，因此我很快能够发现，配合注释删掉就是了。如果放在开头初始化和后面接收子程序里，我可能不会很快发现，到时候把时间耽误在这上面，有点得不偿失。如果各位不需要上机调试的话，完全可以修改我的程序，让他在每次读的时候写入看门狗地址，这样后面就不需要调成132行的等待时间来匹配Emu8086的运行速度了。代码中的400是调节Emu8086运行时step delay为1ms时正常工作的值。-全文完-