汇编操作 INS8250 进行串口全双工通信及 Emu8086 用 INS8250 模拟器

简介: 汇编操作 INS8250 进行串口全双工通信及 Emu8086 用 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 时正常工作的值。

目录
相关文章
Win10 汇编工具 EMU8086安装教程
EMU8086是一种学习汇编工具,它结合了一个原始编辑器、组译器、反组译器、具除错功能的软件模拟工具(虚拟PC),还有一个循序渐进的指导工具。下面的这一教程是 bs.aiesst.cn 专门为初学者入门而准备的一个安装教程,以及下载地址。
7435 1
进阶C语言 第七章-------《程序的编译(预处理操作)+链接》 (预编译、编译、汇编、#define、条件编译,#include的包含)知识点+完整思维导图+基本练习题+深入细节+通俗易懂建议收藏(三)
进阶C语言 第七章-------《程序的编译(预处理操作)+链接》 (预编译、编译、汇编、#define、条件编译,#include的包含)知识点+完整思维导图+基本练习题+深入细节+通俗易懂建议收藏(三)
|
编译器 C语言
进阶C语言 第七章-------《程序的编译(预处理操作)+链接》 (预编译、编译、汇编、#define、条件编译,#include的包含)知识点+完整思维导图+基本练习题+深入细节+通俗易懂建议收藏(二)
进阶C语言 第七章-------《程序的编译(预处理操作)+链接》 (预编译、编译、汇编、#define、条件编译,#include的包含)知识点+完整思维导图+基本练习题+深入细节+通俗易懂建议收藏(二)
|
存储 自然语言处理 程序员
进阶C语言 第七章-------《程序的编译(预处理操作)+链接》 (预编译、编译、汇编、#define、条件编译,#include的包含)知识点+完整思维导图+基本练习题+深入细节+通俗易懂建议收藏(一)
进阶C语言 第七章-------《程序的编译(预处理操作)+链接》 (预编译、编译、汇编、#define、条件编译,#include的包含)知识点+完整思维导图+基本练习题+深入细节+通俗易懂建议收藏(一)
|
Shell 测试技术 C语言
基于Qt实现的带图形界面的MIPS汇编指令的编辑器、汇编器、反汇编器、模拟器
MIPS-sc 为 MIPS simulator&compiler 的简称,是一个基于Qt实现的带图形界面的MIPS汇编指令的编辑器、汇编器、反汇编器、模拟器。是为浙江大学《计算机组成课程》编写的的课程项目之一。
675 0
|
iOS开发 算法
IDA反汇编/反编译静态分析iOS模拟器程序(一)话说IDA
上个月写了一系列文章《xcode反汇编调试iOS模拟器程序》,是使用xcode来动态反汇编分析iOS模拟器程序的。这个系列则是静态分析,用到IDA来做反汇编/反编译。
2227 0
|
数据库 iOS开发 Windows
IDA反汇编/反编译静态分析iOS模拟器程序(二)加载文件与保存数据库
启动windows版的IDA,在Quickstart界面点击New,弹出一个对话框选择文件。也可以按取消后再把文件拖进IDA。由于Mac版的IDA没注册,没有save功能,所以只好先把Mac上的东西拷贝到windows再打开了。
1495 0
|
iOS开发 编译器
xcode反汇编调试iOS模拟器程序(六)函数出入口处的处理与局部变量
引用第二节的例子: 函数的入口处,通常都是把esp的值传给ebp保存,然后下面的操作以ebp为基准做偏移量引用。因为esp作为栈指针,push和pop都会自动修改其值,所以用ebp可以不受影响。
814 0