MENU

【歪门邪道】8086汇编操作INS8250进行串口全双工通信及Emu8086用INS8250模拟器

May 10, 2020 • 瞎折腾

最近上的一门选修课理论部分结课了,课程名是「接口与通信技术」,现在进入了实验阶段,要求用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时正常工作的值。

-全文完-


知识共享许可协议
【歪门邪道】8086汇编操作INS8250进行串口全双工通信及Emu8086用INS8250模拟器天空 Blond 采用 知识共享 署名 - 非商业性使用 - 相同方式共享 4.0 国际 许可协议进行许可。
本许可协议授权之外的使用权限可以从 https://skyblond.info/about.html 处获得。

Archives QR Code
QR Code for this page
Tipping QR Code
Leave a Comment

7 Comments
  1. Congratulations! Today is the 1000th day since the establishment of this blog. You've recorded so many fragments of your life here. Looking back at the past three years, will you can't help but ask yourself: what I have gained from the flying time, or have I grown up from the experiences no matter it's suffering or precious since. Fortunately, we all persisted. Thank you for choosing Typecho, we will be always by your side.
    Always remember, there will be reverberations.
    ——from a friend of you

    1. @Typecho Official谢谢~

    2. @天空Blond(^_^)

  2. tsz tsz

    挺好玩的样子,代码好长好长

  3. wys wys

    码盲,读不懂,占个位置。喜欢就自学,就折腾吧,免得以后后悔。

    1. @wys确实,现在发现越大留给自己自由使用的时间就越少了

    2. wys wys

      @天空Blond越大越魂不守舍,追RMB去了哈!小时候,更单纯吧,心眼也小,随便一点糖果就能心满意足。