Page 1 of 1

Buffer Address for DMA

Posted: Mon Dec 18, 2023 10:13 am
by gray
To avoid busy-waiting at a serial output peripheral (USART), I am implementing buffered text output to a terminal. The intention is to use the DMA peripheral on the M0. Among other configuration, the DMA channel expects 1) a memory address of the buffer, and 2) the number of data items to transfer from this address upwards. The DMA device will then output the buffer contents on a pure hardware level, while the CPU can handle other tasks.

A common use case is

Code: Select all

Out.String("this is a test")
The string is stored in flash memory with the program code. So I would like to pass the string's address and length to the DMA channel, and avoid copying the flash-stored data to a RAM-based buffer with a known address. Since the DMA device nicely reads from a flash address, this would be unnecessary overhead, and also opens the question about the buffer size.

With a serial device driver API procedure like so... [2]

Code: Select all

PROCEDURE PrintString*(chan: INTEGER; s: ARRAY OF CHAR; nc: INTEGER);
... 'Out.String' can easily pass along the required data ('chan' is the DMA channel number, 'nc' the number of characters).[1]

But -- how can 'PrintString' determine the address of 's'?

I see two solutions, both work,[3] but neither "looks nice".

Code: Select all

(* 1 *)
PROCEDURE PrintString*(chan: INTEGER; s: ARRAY OF CHAR; nc: INTEGER);
BEGIN
  DMA.SetMemAddr(chan, SYSTEM.ADR(s[0])); (* set memory address for  DMA channel *)
  DMA.SetNumData(chan, nc); (* set number of data items for DMA channel *)
  (* ... *)
END PrintString;

(* 2 *)
PROCEDURE PrintString*(chan: INTEGER; s: ARRAY OF CHAR; nc: INTEGER);
BEGIN
  DMA.SetMemAddr(chan, SYSTEM.REG(1));
  DMA.SetNumData(chan, nc);
  (* ... *)
END PrintString;
Any thoughts about a "clean" Oberon solution for this problem? Thanks.

***

[1] The determination of the string length could also be delegated to 'PrintString', but output procedures such as 'Out.Hex' already have this data available due to how the corresponding output strings are constructed.

[2] I could also implement a more "raw" device driver procedure like this:

Code: Select all

PROCEDURE PrintStringBuffer(chan: INTEGER; bufAddr: INTEGER; nc: INTEGER);
But this would only shift the problem to another level, ie. into 'Out.String'.

[3] Evidenced by checking the generated code, as well as via test programs.

Variant (* 1 *)

Code: Select all

  PROCEDURE PutString*(chan: INTEGER; s: ARRAY OF CHAR; nc: INTEGER);
  BEGIN
.     4     04H  0B50FH          push     { r0, r1, r2, r3, lr }
    DMA.SetMemAddr(chan, SYSTEM.ADR(s[0]));
.     6     06H  09800H          ldr      r0,[sp]
.     8     08H  02100H          movs     r1,#0
.    10     0AH  09A02H          ldr      r2,[sp,#8]
.    12     0CH  04291H          cmp      r1,r2
.    14     0EH  0D301H          bcc.n    2 -> 20
.    16    010H  0DF01H          svc      1
.    18  <LineNo: 13>
.    20    014H  09A01H          ldr      r2,[sp,#4]
.    22    016H  01851H          adds     r1,r2,r1
.    24    018H  004050000H      bl.w     Ext Proc #5
Variant (* 2 *) is leaner, not least as no index bound checking needs to be done.

Code: Select all

  PROCEDURE PutString*(chan: INTEGER; s: ARRAY OF CHAR; nc: INTEGER);
  BEGIN
.     4     04H  0B50FH          push     { r0, r1, r2, r3, lr }
    DMA.SetMemAddr(chan, SYSTEM.REG(1));
.     6     06H  09800H          ldr      r0,[sp]
.     8     08H  004050000H      bl.w     Ext Proc #5

Re: Buffer Address for DMA

Posted: Mon Dec 18, 2023 11:00 am
by cfbsoftware
Try:

Code: Select all

DMA.SetMemAddr(chan, SYSTEM.ADR(s)); (* set memory address for  DMA channel *)

Re: Buffer Address for DMA

Posted: Mon Dec 18, 2023 1:37 pm
by gray
Interesting. Yes, this works, again a) checking the code and b) running a test program.

Surprising though. The Astrobe docs about module system say:
ADR returns the absolute address of the given variable.
Now, that SYSTEM.ADR of a VAR procedure parameter would return the actual address of the argument is not surprising, since the Oberon Report says:
A variable parameter corresponds to an actual parameter that is a variable, and it stands for that variable.
But non-VAR parameters? I would have expected to get a stack address, but for RECORDs and ARRAYs we get the actual address, VAR or not VAR.

Here's little test program:

Code: Select all

MODULE TestAddr;

  IMPORT SYSTEM, LinkOptions, Main, Out;

  CONST SP = 13;

  TYPE
    PROC = PROCEDURE;
    P = POINTER TO R;
    R = RECORD END;

  VAR
    x0, x1: INTEGER;
    p0, p1: P;
    r0, r1: R;
    pr1: PROC;
    s1: ARRAY 4 OF CHAR;

  PROCEDURE pr0;
  END pr0;

  PROCEDURE Check(addr, stackPtr: INTEGER);
    VAR stack, heap, code: INTEGER;
  BEGIN
    stack := LinkOptions.StackStart;
    heap := LinkOptions.HeapStart;
    code := LinkOptions.CodeStart;
    Out.Hex(addr, 10);
    IF addr > stack THEN
      Out.String(" module mem")
    ELSIF addr >= stackPtr THEN
      Out.String(" stack mem")
    ELSIF addr > heap THEN
      Out.String(" heap mem")
    ELSE
      Out.String(" code mem")
    END;
    Out.Ln
  END Check;

  PROCEDURE Proc0(x0: INTEGER; VAR x1: INTEGER; p0: P; VAR p1: P; r0: R; VAR r1: R);
  BEGIN
    Out.String("stack start "); Out.Hex(LinkOptions.StackStart, 0); Out.Ln;
    Out.String("heap start  "); Out.Hex(LinkOptions.HeapStart, 0); Out.Ln;
    Out.String("code start  "); Out.Hex(LinkOptions.CodeStart, 0); Out.Ln;
    Out.String("    x0  "); Check(SYSTEM.ADR(x0), SYSTEM.REG(SP));
    Out.String("VAR x1  "); Check(SYSTEM.ADR(x1), SYSTEM.REG(SP));
    Out.String("    p0  "); Check(SYSTEM.ADR(p0), SYSTEM.REG(SP));
    Out.String("VAR p1  "); Check(SYSTEM.ADR(p1), SYSTEM.REG(SP));
    Out.String("    r0  "); Check(SYSTEM.ADR(r0), SYSTEM.REG(SP));
    Out.String("VAR r1  "); Check(SYSTEM.ADR(r1), SYSTEM.REG(SP))
  END Proc0;

  PROCEDURE Proc1(s0: ARRAY OF CHAR; VAR s1: ARRAY OF CHAR; pr0: PROCEDURE; VAR pr1: PROC);
  BEGIN
    Out.String("    s0  "); Check(SYSTEM.ADR(s0), SYSTEM.REG(SP));
    Out.String("VAR s1  "); Check(SYSTEM.ADR(s1), SYSTEM.REG(SP));
    Out.String("    pr0 "); Check(SYSTEM.ADR(pr0), SYSTEM.REG(SP));
    Out.String("VAR pr1 "); Check(SYSTEM.ADR(pr1), SYSTEM.REG(SP))
  END Proc1;

BEGIN
  NEW(p0);
  x0 := 13;
  Proc0(x0, x1, p0, p1, r0, r1);
  Proc1("test", s1, pr0, pr1)
END TestAddr.
It prints:

Code: Select all

stack start 20007DFCH
heap start  20000200H
code start  08000000H
    x0   20007DD4H stack mem
VAR x1   20007E10H module mem
    p0   20007DDCH stack mem
VAR p1   20007E08H module mem
    r0   20007E08H module mem
VAR r1   20007E08H module mem
    s0   0800377CH code mem
VAR s1   20007E00H module mem
    pr0  20007DECH stack mem
VAR pr1  20007E04H module mem
Hence, regarding procedure parameters:

1) SYSTEM.ADR returns the address of the argument passed for VAR parameters;
2) SYSTEM.ADR returns the stack address of non-VAR arguments, but
3) SYSTEM.ADR returns the address of the argument for non-VAR RECORDs and ARRAYs.

1) and 2) are obvious, but 3) has surprised me. Learned something. Thanks. :)

Re: Buffer Address for DMA

Posted: Mon Dec 18, 2023 9:03 pm
by cfbsoftware
But non-VAR parameters? I would have expected to get a stack address, but for RECORDs and ARRAYs we get the actual address, VAR or not VAR.
This is a subtle consequence of Section 10.1 of the Oberon-07 Language Report:
A value parameter corresponds to an actual parameter that is an expression, and it stands for its
value, which cannot be changed by assignment. However, if a value parameter is of a basic type, it
represents a local variable to which the value of the actual expression is initially assigned.
The converse of this is that value parameters that are not basic types i.e. RECORDs and ARRAYs, do not have to be copied onto the stack. It is therefore possible in an Oberon-07 implementation to pass them much more efficiently in the same way as VAR parameters as long as they are flagged as read-only. This is how they are implemented in Astrobe for Cortex-M.