uLisp – ARM Assembler in Lisp

栏目: IT技术 · 发布时间: 4年前

内容简介:This article describes an in-line assembler written in Lisp, as an add-on for uLisp. The project is currently a work in progress, but I thought it would be interesting to describe where I've got to, and I'd be interested to hear suggestions for extensions

This article describes an in-line assembler written in Lisp, as an add-on for uLisp. The project is currently a work in progress, but I thought it would be interesting to describe where I've got to, and I'd be interested to hear suggestions for extensions or improvements.

The assembler generates ARM Thumb code, and I've tested it on ARM M0 platforms, such as the Adafruit ItsyBitsy M0, and ARM M4 platforms, such as the Adafruit Metro M4. It is currently restricted to generating one assembler routine with up to three integer parameters, and returning an integer result.

uLisp extensions

The assembler uses a special version of uLisp to which I've added two functions:

The assemble function takes a series of 16-bit integer arguments comprising the machine code of the routine being assembled. These are written into RAM. Any argument can also return nil , in which case it is ignored.

The call function will then call the assembler routine in RAM, and return the result.

The actual instruction generation is handled by an assembler written in Lisp. This makes it easy to check the generated code, and extend the assembler to handle additional instructions and addressing modes.

Using the assembler

To use the assembler you need to run the version of uLisp ARM 3.0x from here: uLisp ARM Version 3.0x

Then load in the Lisp source of the assembler from here:ARM Assembler

Assembler syntax

Where possible the syntax is very similar to ARM assembler syntax, with the following differences:

  • The mnemonics are prefixed by '$' (because some mnemonics such as  push  and  pop  are already in use as Lisp functions).
  • Registers are represented as symbols, prefixed with a quote. Constants are just numbers.
  • Lists of registers, as used in the  $push  and  $pop  mnemonics, are represented as a Lisp list.

Assembler instructions are just Lisp functions, so you can see the code they generate:

> (x ($mov 'r1 13))
#x210d

The function x is a convenient addition that prints a 16-bit value in hexadecimal.

The following table shows typical ARM assembler formats, and the equivalent in this Lisp assembler:

Examples ARM assembler uLisp assembler
Push and pop push  {r4, r5, r6, lr} ($push '(r4 r5 r6 lr))
Registers mov  r1, r4 ($mov 'r1 'r4)
Immediate mov r2, #3 ($mov 'r2 3)
Load relative ldr  r0, [r3, #0] ($ldr 'r0 '(r3 0))
Branch bne label ($bne 'label)
Constant .short  0x0b0b #x0b0b

Simple example

Here's a trivial example consisting of four ARM Thumb instructions that multiplies its parameter by 13 and returns the result:

(assemble
 ($push '(lr))
 ($mov 'r1 13)
 ($mul 'r0 'r1)
 ($pop '(pc)))

After executing the assemble command we can then call the function as follows:

> (call 11)
143

The result is the number returned in the r0 register.

If you prefer you could wrap up the call in a Lisp function:

(defun times13 (x) (call x))

Labels

The previous example assembled a function with no branches, in which case you can simply give the mnemonics as arguments to the assemble function.

However, to assemble branches to labels you have to do a bit of extra work as described here:

Enclose the whole program in a let to declare the labels you are going to use as local variables.

Then enclose the assemble call in a

(dotimes (p 2 ass)
    (setq *pc* 0)
    ....
    )

to do a two-pass assembly which will resolve forward references.

Where you want to insert a label such as loop in the program use a statement such as:

($label 'loop)

This assigns the current program counter to the label variable, but returns nil so that no additional code is generated.

For example, here's a simple routine to calculate the Greatest Common Divisor, which uses three labels:

; Greatest Common Divisor
(let ((swap 0) (again 0) (minus 0))
  (dotimes (p 2)
    (setq *pc* 0)
    (assemble
     ($push '(r4 lr))
     ($label 'swap)
     ($mov 'r4 'r1)
     ($mov 'r1 'r0)
     ($label 'again)
     ($mov 'r0 'r4)
     ($label 'minus)
     ($sub 'r4 'r4 'r1)
     ($blt 'swap)
     ($bne 'again)
     ($pop '(r4 pc)))))

For example, to find the GCD of 3287 and 3460:

> (call 3287 3460)
173

For some more examples seebelow.

The assembler

Here's a description of some of the key assembler routines.

The program counter *pc*

To handle labels and branches the assembler uses a global variable *pc* that needs to be defined as follows:

(defvar *pc* 0)

The other routines will give errors if you don't do this.

Packing arguments into bit fields

The emit function takes a list of bit field widths, and a series of arguments, packs the arguments into a 16-bit number according to the bit widths, and returns the result:

(defun emit (bits &rest args)
  (let ((word 0))
    (unless (= (apply #'+ bits) 16) (error "Incorrect number of bits"))
    (mapc #'(lambda (width value)
              (when (> value (1- (expt 2 width))) (error "Won't fit"))
              (setq word (logior (ash word width) value)))
          bits args)
    (incf *pc* 2)
    word))

The bit field widths should add up to 16. For example:

> (x (emit '(4 4 4 4) 12 13 14 15))
#xcdef

As a side effect the emit function also increments the global variable *pc* .

Generating the assembler instructions

A set of helper routines are provided that handle groups of mnemonics with similar parameters; for example mov-cmp-2 handles the  $mov and $cmp mnemonics with an 8-bit immediate argument:

(defun mov-cmp-2 (op argd immed8)
  (emit '(4 1 3 8) 2 op (regno argd) immed8))

These are called in turn by the functions for each individual mnemonic such as $cmp :

(defun $cmp (argd argm)
  (cond
   ((numberp argm)
    (mov-cmp-2 1 argd argm))
   (t
    (and-mvn-4 10 argd argm))))

The number in the name, 2, refers to the value in the top four bits of the instruction to help classify the routines.

A separate helper routine, and-mvn-4 , handles the case of $cmp with two register arguments.

I found Appendix B2 from the ARM System Developer's Guide by Andrew Sloss, Dominic Symes, and Chris Wright useful.

Examples

Here are some examples.

Fibonacci sequence

The Fibonacci sequence is:

1, 1, 2, 3, 5, 8, 13, 21 ...

where the first two terms are 1, and each subsequent term is the sum of the two previous terms. The following recursive function finds the nth term, counting from 0:

(defun fib (n)
  (if (< n 3) 1
    (+ (fib (- n 1)) (fib (- n 2)))))

Running the Lisp version:

> (for-millis () (print (fib 27)))

196418
24609

The first number is the result and the second number is the time, in milliseconds.

Here's the assembler version:

; Fibonacci sequence
(let ((fib 0) (loop 0) (add 0))
  (dotimes (p 2)
    (setq *pc* 0)
    (assemble
     ($label 'fib)
     ($push '(r4 r5 r6 lr))
     ($mov 'r5 'r0)
     ($mov 'r4 0)
     ($label 'loop)
     ($cmp 'r5 2)
     ($ble 'add)
     ($sub 'r0 'r5 1)
     ($bl 'fib)
     ($sub 'r5 2)
     ($add 'r4 'r4 'r0)
     ($b 'loop)
     ($label 'add)
     ($add 'r0 'r4 1)
     ($pop '(r4 r5 r6 pc)))))

Running the assembler version:

> (for-millis () (print (call 27)))

196418
61

On a Adafruit Metro M4 the assembler version is approximately a factor of 400 faster.

Takeuchi function

The Takeuchi function is a classic benchmark for comparing implementations of Lisp, originally used by Ikuo Takeuchi of Japan. Here's the Lisp version:

(defun tak (x y z)
  (if (not (< y x))
      z
    (tak
     (tak (1- x) y z)
     (tak (1- y) z x)
     (tak (1- z) x y))))

For example:

> (for-millis () (print (tak 18 12 6)))

7 
5230

Here's the assembler version:

(let ((tak 0) (compare 0) (exit 0))
  (dotimes (p 2)
    (setq *pc* 0)
    (assemble
     ($label 'tak)
     ($push '(r0 r1 r2 r4 r5 r6 r7 lr))
     ($mov 'r5 'r0)
     ($mov 'r6 'r1)
     ($mov 'r4 'r2)
     ($label 'compare)
     ($cmp 'r6 'r5)
     ($bge 'exit)
     ($mov 'r2 'r4)
     ($mov 'r1 'r6)
     ($sub 'r0 'r5 1)
     ($bl 'tak)
     ($mov 'r2 'r5)
     ($mov 'r1 'r4)
     ($str 'r0 '(sp 4))
     ($sub 'r0 'r6 1)
     ($bl 'tak)
     ($mov 'r2 'r6)
     ($mov 'r7 'r0)
     ($mov 'r1 'r5)
     ($sub 'r0 'r4 1)
     ($bl 'tak)
     ($mov 'r6 'r7)
     ($mov 'r4 'r0)
     ($ldr 'r5 '(sp 4))
     ($b 'compare)
     ($label 'exit)
     ($mov 'r0 'r4)
     ($pop '(r1 r2 r3 r4 r5 r6 r7 pc)))))

Run it as follows:

> (for-millis () (print (call 18 12 6)))

7 
18

On a Adafruit Metro M4 the assembler version is a factor of 290 faster.

Hofstadter Q sequence

This is one of several recursive sequences described in Douglas Hofstadter's book "Gödel, Escher, Bach: an Eternal Golden Braid". It is related to the Fibonacci sequence, except that in this case the two preceding terms specify how far to go back in the sequence to find the two terms to be summed:

(defun q (n)
  (if (<= n 2) 1
    (+
     (q (- n (q (- n 1))))
     (q (- n (q (- n 2)))))))

Running the Lisp version:

> (for-millis () (print (q 21)))

12 
6798

Here's the assembler version:

; Hofstadter Q sequence
(let ((q 0) (compare 0) (add 0))
  (dotimes (p 2)
    (setq *pc* 0)
    (assemble
     ($label 'q)
     ($push '(r4 r5 r6 lr))
     ($mov 'r4 'r0)
     ($mov 'r5 0)
     ($label 'compare)
     ($cmp 'r4 2)
     ($ble 'add)
     ($sub 'r0 'r4 1)
     ($bl 'q)
     ($sub 'r0 'r4 'r0)
     ($bl 'q)
     ($mov 'r6 'r0)
     ($sub 'r0 'r4 2)
     ($bl 'q)
     ($add 'r5 'r5 'r6)
     ($sub 'r4 'r4 'r0)
     ($b 'compare)
     ($label 'add)
     ($add 'r0 'r5 1)
     ($pop '(r4 r5 r6 pc))))))

Running the assembler version:

> (for-millis () (print (call 21)))

12 
25

In other words, about 270 times faster.

  1. It's available as a PDF here:  http://engold.ui.ac.ir/~nikmehr/Appendix_B2.pdf .

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

你也能看得懂的Python算法书

你也能看得懂的Python算法书

王硕,董文馨,张舒行,张洁 著 / 电子工业出版社 / 2018-11-1 / 59.00

编程的核心是算法,学习算法不仅能教会你解决问题的方法,而且还能为你今后的发展提供一种可能。 《你也能看得懂的Python算法书》面向算法初学者,首先介绍当下流程的编程语言Python,详细讲解Python语言中的变量和循序、分支、循环三大结构,以及列表和函数的使用,为之后学习算法打好基础。然后以通俗易懂的语言讲解双指针、哈希、深度优先、广度优先、回溯、贪心、动态规划和至短路径等经典算法。 ......一起来看看 《你也能看得懂的Python算法书》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具