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

Han*_*iel 2 assembly tasm x86-16

我有一项任务,要求我使形状移动并用颜色改变形状。我一开始就没有成功地画出圆的八分圆。假设使用 Intel 8086 汇编语言,在 DMA 模式下使用 TASM。(模式 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
Run Code Online (Sandbox Code Playgroud)

输出

Cod*_*ray 5

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

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

至于您的实际问题,从输出看来您已经成功绘制了一条线。但这并不是您真正想要画的。您想使用中点算法来绘制圆。您很幸运,因为维基百科文章有用C 实现此算法的示例代码。如果您是汇编新手,并且正在努力解决这个问题,我的建议是首先用 C 编写代码,并确保您的算法可以正常工作。然后,您可以将工作的 C 代码翻译为汇编语言。一旦您对汇编更加熟悉,您就可以开始跳过步骤并直接用汇编编写,将 C 风格的算法翻译成您头脑中的汇编指令。至少,我就是这样做的,每当遇到困难时我仍然会回到 C。

那么让我们从 Wikipedia 窃取一些 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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

通常,发明、编写和测试算法是最困难的部分,但我们只是通过窃取他人的工作成果来绕过这一点。现在,我们所需要的只是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
Run Code Online (Sandbox Code Playgroud)

第二阶段是将 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
Run Code Online (Sandbox Code Playgroud)

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

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

请注意,我从宏中“内联”了代码PlotPixel。这使我可以在顶部设置AH和寄存器,并为所有调用重用它们。BH这会稍微缩短代码,并加快速度。并行结构使其具有足够的可读性(至少在我看来)。

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

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

一个黄色圆圈!

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

这段代码的主要瓶颈有两个:

  1. 使用堆栈。

    可能有一种更有创意的方法来编写代码,以更优化地利用寄存器,根据需要对值进行改组,以避免尽可能多地访问内存。但这对于我脆弱的头脑来说实在是太难以理解了。它不应该太慢——毕竟所有这些值都适合缓存。

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

    即使在现代机器上,这个速度也非常慢。它可以很好地在屏幕上绘制一个简单的圆圈,尤其是在虚拟化硬件上,但对于多个圆圈的复杂输出来说还不够快。要解决这个问题,您将不得不诉诸视频内存的原始位调整,这是不可移植的。我想这就是“DMA 模式”的意思。如果这样做,您将限制您的代码只能在具有符合标准规范的 VGA 硬件的系统上运行。您还会失去分辨率/模式独立性。

    进行更改非常简单。我刚刚添加了一个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
    
    Run Code Online (Sandbox Code Playgroud)

    我想你已经理解了这一部分,但它起作用的原因是因为在模式 19 (13h) 下,有 320×200 像素和 256 种颜色。因为 256 == 2 8,所以每个像素的颜色值恰好存储在 1 个字节(8 位)中。因此,如果从视频存储器的开头(地址A000h)开始,像素将被线性存储,并且可以直接写入它们的颜色值。写入像素 (x, y) 的公式为:A000h + (y * 320) + x

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

    您可以通过一次写入多个字节来进一步加快速度——在 8086 上,这将写入一个 WORD(两个字节)。这需要直接在函数中“内联”像素绘制代码DrawCircle,并进行一些创造性的寄存器分配,以确保您想要绘制的第一个像素位于,例如,,AL第二个像素位于AH。我将把这个作为练习。

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

说到这里,我用 NASM 编写了代码,并尝试将其即时转换为 TASM,这是一种我不太熟悉的语法。很抱歉,如果您在组装之前需要解决任何语法问题!