我无法在具有直接内存访问的 8086 程序集中使用中点算法绘制圆

I am unable to draw a circle using midpoint algorithm in 8086 Assembly with Direct Memory Access

我有一个作业,我应该让形状移动并用颜色改变形状。首先,我没有成功地画出一个圆的八分圆。假设在 DMA 模式下使用 TASM 使用 Intel 8086 汇编语言。 (模式 19) 我在想,如果我能完成一个圆圈,我可以为它制作动画并改变形状。搞不懂是算法错了还是代码错了

.model small
.stack 256

.code 
startaddr   dw  0a000h  ;start of video memory   
color   db  3 
xc dw  160
yc dw 100
r dw  50 ; radius
x dw 0 
y dw 50 ;radius
pk dw 1
temp dw 1

plot macro r1, r2, r3 ;x,y,color
    mov ax, r2
    mov bx, 320
    mul bx
    add ax, r1
    mov di, ax
    mov al, r3
    mov es:[di], al
endm

start:    
  mov ax, yc
  add y, ax
  mov ah,00    
  mov al, 13h    
  int 10h           ;switch to 320x200 mode  
  mov es, startaddr
  mov dx, y
  mov ax, xc
  add x, ax
  plot x, dx, color
  mov bx, r
  mov pk, bx
  sub pk, 1
  neg pk
  cmp pk, 0
  jge pk1

drawc:
    mov bx, x
    sub bx, xc
    mov ax, y
    sub ax, yc
    mov temp, ax
    cmp bx, temp
    jge keypress

    mov dx, y
    plot x, dx, color

peekay:
    cmp pk, 0
    jg pk1
    mov ax, x
    mov bx, 2
    mul bx
    add ax, pk
    mov pk, ax
    inc x ;x+1
    jmp drawc

pk1:
    dec y
    mov ax, x
    sub ax, y
    mov bx, 2
    mul bx
    add ax, pk
    mov pk, ax
    inc x
    jmp drawc

keypress:    
  mov ah,00    
  int 16h           ;await keypress  

  mov ah,00    
  mov al,03    
  int 10h   

  mov ah,4ch    
  mov al,00         ;terminate program    
  int 21h 
end start

Output

你目前的代码很难理解,因为它的注释太少了。当您编写汇编时,重要的是自由地注释,解释代码应该做什么,因为语言本身不是很有表现力。当然,我知道每条指令的含义,但作为一个人,我很难跟踪所有已注册的值及其在整个过程中的流动。我也不知道您的代码在 高级 .

上应该做什么

更糟糕的是,当我尝试阅读代码时,您的标签名称对我来说也毫无意义。什么是 peekaypk1?我会 猜测 drawcDrawCircle,但为什么不这样称呼它呢?那么你甚至不需要在那里发表评论,因为从名字上就很明显了。

至于你的实际问题,从输出来看你已经成功画了一条线。但这并不是你真正想要画的。你想使用 midpoint algorithm to draw a circle. And you're in luck, because that Wikipedia article has sample code implementing this algorithm in C。如果您不熟悉汇编,并且正在为这个问题而苦苦挣扎,我的建议是先用 C 语言编写代码并确保您的算法有效。 然后,你可以将工作的C代码翻译成汇编。一旦您对汇编更加熟悉,就可以开始跳过步骤并直接用汇编编写,将 C 风格的算法翻译成您头脑中的汇编指令。至少,这就是我所做的,而且我仍然每当我遇到困难时都会回到 C。

所以让我们从维基百科窃取一些 C 代码:

void DrawCircle(int x0, int y0, int radius)
{
    int x   = radius;
    int y   = 0;
    int err = 0;

    while (x >= y)
    {
        PlotPixel(x0 + x, y0 + y);
        PlotPixel(x0 + y, y0 + x);
        PlotPixel(x0 - y, y0 + x);
        PlotPixel(x0 - x, y0 + y);
        PlotPixel(x0 - x, y0 - y);
        PlotPixel(x0 - y, y0 - x);
        PlotPixel(x0 + y, y0 - x);
        PlotPixel(x0 + x, y0 - y);

        if (err <= 0)
        {
            y   += 1;
            err += 2*y + 1;
        }
        if (err > 0)
        {
            x   -= 1;
            err -= 2*x + 1;
        }
    }
}

通常情况下,发明、编写和测试算法是最困难的部分,但我们只是通过窃取 建立在他人工作的基础上来绕过这一步。现在,我们只需要 PlotPixel 函数。不过这很简单——您的汇编代码已经包含了该部分,因为您已成功绘制一条线!

为了让我们达成一致,最简单的方法是调用 BIOS 中断 10h,函数 0Ch,它在图形模式下绘制一个像素。 DX包含点的x坐标,CX包含y坐标,AL包含颜色属性,BH包含视频页面。为简单起见,我们假设视频页面为 0(默认值)。这可以包含在一个简单的宏中,如下所示:

PlotPixel MACRO x, y, color, videoPage
   mov  cx, x
   mov  dx, y
   mov  bh, videoPage
   mov  ax, (color | (0Ch << 4))   ; AL == color, AH == function 0Ch
   int  10h
ENDM

第二阶段是将 C 函数翻译成汇编。由于对 PlotPixel 函数的重复调用以及循环结构,此翻译不会是一个简单的练习。我们将以 代码清单结束。我们还有另一个问题:没有足够的寄存器来保存我们所有的临时值!当然,这在通用寄存器数量非常有限的 x86 上很常见,所以我们将做我们总是必须做的事情:使用堆栈。它速度较慢,但​​它有效。 (这段代码无论如何都不会很快。)这是我想出的:

; Draws a circle of the specified radius at the specified location
; using the midpoint algorithm.
; 
; Parameters:    DX == center, x
;                CX == center, y
;                BX == radius
;                AL == color
; Clobbers:      <none>
; Returns:       <none>
DrawCircle:
   push bp
   mov  bp, sp
   push dx                       ; xCenter [bp -  2]
   push cx                       ; yCenter [bp -  4]
   push bx                       ; x       [bp -  6]
   push 0                        ; y       [bp -  8]
   push 0                        ; err     [bp - 10]

   ; Prepare to plot pixels:
   mov  ah, 0Ch                  ; AH == function 0Ch (plot pixel in graphics mode)
   xor  bx, bx                   ; BH == video page 0       

DrawLoop:
   mov  dx, WORD [bp - 6]
   cmp  dx, WORD [bp - 8]
   jl   Finished                 ; (x < y) ? we're finished drawing : keep drawing

   ; Plot pixels:       
   mov  cx, WORD [bp - 2]
   mov  dx, WORD [bp - 4]
   add  cx, WORD [bp - 6]        ; CX = xCenter + x
   add  dx, WORD [bp - 8]        ; DX = yCenter + y
   int  10h

   mov  cx, WORD [bp - 2]
   sub  cx, WORD [bp - 6]        ; CX = xCenter - x
   int  10h

   mov  dx, WORD [bp - 4]
   sub  dx, WORD [bp - 8]        ; DX = yCenter - y
   int  10h

   mov  cx, WORD [bp - 2]
   add  cx, WORD [bp - 6]        ; CX = xCenter + x
   int  10h

   mov  cx, WORD [bp - 2]   
   mov  dx, WORD [bp - 4]
   add  cx, WORD [bp - 8]        ; CX = xCenter + y
   add  dx, WORD [bp - 6]        ; DX = yCenter + x
   int  10h

   mov  cx, WORD [bp - 2]
   sub  cx, WORD [bp - 8]        ; CX = xCenter - y
   int  10h

   mov  dx, WORD [bp - 4]
   sub  dx, WORD [bp - 6]        ; DX = yCenter - x
   int  10h

   mov  cx, WORD [bp - 2]
   add  cx, WORD [bp - 8]        ; CX = xCenter + y
   int  10h

   ; Update state values and check error:
   mov  dx, WORD [bp - 8]
   inc  dx
   mov  WORD [bp - 8], dx
   add  dx, dx                ; DX *= 2
   inc  dx
   add  dx, WORD [bp - 10]
   mov  WORD [bp - 10], dx
   sub  dx, WORD [bp - 6]
   add  dx, dx                ; DX *= 2
   js   DrawLoop              ; DX < 0 ? keep looping : fall through and check error
   inc  dx

   dec  cx
   mov  WORD [bp - 6], cx
   add  cx, cx                ; CX *= 2
   neg  cx
   inc  cx                    ; CX = (1 - CX)
   add  WORD [bp - 10], cx
   jmp  DrawLoop              ; keep looping

Finished:
   pop  bx                    ; (clean up the stack; no need to save this value)
   pop  bx                    ; (clean up the stack; no need to save this value)
   pop  bx
   pop  cx
   pop  dx
   pop  bp
   ret
END DrawCircle

您会看到,在函数的顶部,我在堆栈上分配了 space 用于我们的临时值。如内联注释中所述,我们将使用 BP 寄存器的偏移量访问它们中的每一个。*

函数的大部分由主绘图循环组成,DrawLoop。在顶部,我们进行比较以查看是否应该继续循环。然后我们开始认真绘制像素,进行必要的操作,就像维基百科的 C 代码中显示的那样。绘图后,我们进行更多操作,将结果存储回堆栈中的临时值,然后 运行 再进行几次比较,看看我们是否应该继续循环(同样,大致类似于 if原始 C 代码中的块)。最后,一旦我们完成,我会在返回前清理堆栈。

请注意,我 "inlined" 来自 PlotPixel 宏的代码。这使我可以在顶部设置 AHBH 寄存器,并将它们重新用于所有调用。这稍微缩短了代码,也加快了速度。并行结构使其具有足够的可读性(至少在我看来是这样)。

这里没有什么特别棘手的事情,除了我的一些算术操作。显而易见的是,将一个寄存器加到它本身与将它乘以 2 是一样的。我通过取反原始值然后将其递增 1 从 1 中减去一个寄存器。这些在代码中有注释。我认为唯一值得指出的另一件事是我使用 test reg, reg 进行简单比较,而不是 cmp reg, 0。前者更短更快。

只需设置您的视频模式,将参数放入适当的寄存器,然后调用它!

当然有加速此功能的方法,但它们是以严重牺牲可读性和可理解性为代价的。在继续阅读之前,请确保您首先了解这里发生的事情!

此代码中的大瓶颈有两个:

  1. 使用堆栈。

    可能有一种更有创意的方法来编写代码,以更优化地使用寄存器,根据需要调整值以避免尽可能多地占用内存。但这对我虚弱的头脑来说太多了,无法跟踪。它不应该非常慢——毕竟所有这些值都将适合缓存。

  2. 使用 BIOS 像素绘制功能。

    这个 非常 慢,即使在现代机器上也是如此。它可以很好地在屏幕上绘制一个简单的圆圈,尤其是在虚拟化硬件上,但对于多个圆圈的复杂输出来说速度还不够快。要解决这个问题,您将不得不求助于视频内存的原始位旋转,这是不可移植的。我想这就是您所说的 "DMA mode" 的意思。如果这样做,您将限制您的代码只在具有符合标准规范的 VGA 硬件的系统上使用 运行ning。你也输了 resolution/mode-independence.

    进行更改非常简单。我刚刚添加了一个 PlotPixel 函数来完成繁重的工作,并更改了 DrawCircle 中的代码以调用此函数而不是 BIOS 中断(并删除了前导 mov ah, 0Chxor bx, bx 行):

    ; Plots a pixel by writing a BYTE-sized color value directly to memory,
    ; based on the formula:    0A000h + (Y * 320) + X
    ; 
    ; Parameters: DX == x-coordinate
    ;             CX == y-coordinate
    ;             AL == color
    ; Clobbers:   CX
    ; Returns:    <none>
    PlotPixel:
       push di
    
       mov  di, 0A000h
       mov  es, di            ; ES == video memory offset
    
       mov  di, cx
       add  cx, cx
       add  cx, cx
       add  di, cx
       shl  di, 6             ; Y *= 320
    
       add  di, dx            ; Y += X
    
       mov  BYTE es:[di], al  ; write the color byte to memory at (X, Y)
    
       pop  di
       ret
    END PlotPixel
    

    我想你已经理解这部分了,但之所以能这样工作是因为在模式 19 (13h) 中,有 320×200 像素和 256 种颜色。因为 256 == 28,每个像素的颜色值正好存储在 1 个字节(8 位)中。因此,如果从显存的开头(地址A000h)开始,像素是线性存储的,可以直接写入它们的颜色值。写入像素 (x, y) 的公式为:A000h + (y * 320) + x.

    正如您在原始代码中所做的那样,您可以通过将 ES 的设置提升到调用方并将 ES == 0A000h 作为 PlotPixel 函数的前提条件来进一步改进.但是我确实对您的原始代码进行了重大更改,用左移和一些加法替换了缓慢的乘法 (MUL)。您原来的基于乘法的代码也有一个溢出错误,我的重写修复了这个错误。

    您可以通过一次写入多个字节来进一步加快速度——在 8086 上,这将是写入一个 WORD(两个字节)。这将需要 "inlining" 直接在 DrawCircle 函数中使用像素绘图代码,并进行一些创造性的寄存器分配以确保您要绘制的第一个像素位于 AL 中,并且第二个像素在 AH 中。我会把它留作练习。

* 我喜欢使用宏将这些偏移量转换为常量,然后在整个函数中使用该符号名称,而不是对数值进行硬编码。这不仅使代码更具可读性,而且如果您决定更改推送参数的顺序或推送的参数数量,还可以更轻松地进行更改。我没有在这里这样做,因为我不知道 TASM 的正确语法是什么,并且在调试这段代码时它咬了我。

说起来,我写了代码在 NASM 中并尝试即时翻译成 TASM,这是我不太熟悉的一种语法。对不起,如果有任何小的语法问题你必须先解决才能 assemble 它!