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 时正常工作的值。