最近上的一门选修课理论部分结课了,课程名是「接口与通信技术」,现在进入了实验阶段,要求用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 处获得。
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
谢谢~
(^_^)
挺好玩的样子,代码好长好长
码盲,读不懂,占个位置。喜欢就自学,就折腾吧,免得以后后悔。
确实,现在发现越大留给自己自由使用的时间就越少了
越大越魂不守舍,追RMB去了哈!小时候,更单纯吧,心眼也小,随便一点糖果就能心满意足。