gbadev.org forum archive

This is a read-only mirror of the content originally found on forum.gbadev.org (now offline), salvaged from Wayback machine copies. A new forum can be found here.

Coding > ECG on GBA

#53930 - Tomik - Tue Sep 13, 2005 3:35 pm

Hi, did someone a ECG on the GBA?
I have some problems with that.

I did:
- read in the ecg data
- plot an scroll them
- draw a line between all pixels

... and that is very slowly .... what can I do better for show like two or three heartbeats at a time on the display ??

Thomas

#54013 - Tomik - Wed Sep 14, 2005 4:02 pm

nobody has an idea?
Code:
   while(1)
   {   
      Raster();
      RighttoLeft(yArr);         
      DrawLine(yArr);
      WaitForVblank();
      Flip();      
   }


I draw:
- a Raster
- draw the current and old shifted pixels
- draw lines between the pixels
- Flip to back buffer (or frontbuffer)

how can i make that faster.
i think i dont need to draw my raster everytime new and dont need to draw all lines in every step new ? But I am a Noob and need help with that !!

Thanks, Thomas

#54016 - poslundc - Wed Sep 14, 2005 5:09 pm

Tomik wrote:
i think i dont need to draw my raster everytime new and dont need to draw all lines in every step new ?


So why don't you try that, then? Just draw whatever columns have changed between the last frame and the current frame. If most of your data doesn't change, then don't update it.

You have an idea, so try it. Don't just ask "what can I do better".

Dan.

#54045 - tepples - Thu Sep 15, 2005 4:29 am

poslundc wrote:
Just draw whatever columns have changed between the last frame and the current frame.

This technique does require your display to be arranged in a tiled (not linear) format, however, if you want a scrolling display and not a sweeping display like a radar or like Lumines.
_________________
-- Where is he?
-- Who?
-- You know, the human.
-- I think he moved to Tilwick.

#54071 - Cearn - Thu Sep 15, 2005 9:36 am

You probably don't need to draw the raster every time. You can, for example, use sprites for the raster, draw that at start-up and you'll never have to draw the raster again. It will always be in front of the background, though.

As for making things faster, there are a number of fairly simple ways of doing that. For example, a number of tutorials engage in some pretty serious pessimisation; if you can get rid of that you might be able to speed up the code by a few factors. For example:
  • Compiling options. Generally, it's better to compile as thumb code (-mthumb) rather than arm code (-marm, which is the default). This may speed things up by 50% or more. The exception to this is when code goes into IWRAM, but I'm guessing that's not happening yet. Putting code into IWRAM is also a good idea of speeding things up, btw, but it can be a little tricky at times.
    If you don't have optimisations turned on yet (-O options), maybe it's a good time to start. By default, optimisations are off; adding -O2 or -O3 to your compiler flags can make a world of difference.
  • Inline functions and macros. For small functions, like plotting pixels, DO NOT use regular functions, use inline functions or macros instead.
Code:
// Normal function
void m3_plot_std(int x, int y, u16 clr)
{   vid_mem[y*240+x]= clr;   }

// inline function
static inline void m3_plot_inl(int x, int y, u16 clr)
{   vid_mem[y*240+x]= clr;   }

// macro
#define M3_PLOT_MACRO(x, y, clr) vid_mem[(y)*240+(x)]= clr
Using m3_plot_inl or M3_PLOT_MACRO instead of the normal function is about three times faster, and hardly more difficult to use.
  • If possible, work in word-sized chunks. The GBA is a 32bit machine, and prefers 32bits (words). Working in bytes (8bit) and halfwords (16bit) is often slower.
    For example, if you need to fill blocks of memory (like drawing horizontal grid lines), you can fill in word chunks instead of halfword chunks and speed things up by roughly a factor 2.
  • Code:
    // filling a scanline in mode 3
    int ii;

    // in halfwords
    u16 clr= 0x03E0;    // green
    u16 *dst= &vid_mem[10*240]; // scanline 10;
    for(ii=0; ii<240; ii++)
        dst[ii] = clr;

    // or in words (filling 2 pixels at once)
    u32 clr32= 0x03E003E0; // green times two
    u32 *dst32= (u32*)&vid_mem[10*240];  // scanline 10
    for(ii=0; ii<240/2; ii++)
        dst32[ii]= clr32;

    Because there are half the iterations in the second loop, it's nearly twice as fast. There are plenty of methods faster still, though: DMA and CpuFastSet, for example, but those require a little more effort to get working that a few casts. Well worth it, though.
    Also, note that the loop variable ii is an int (also 32bit). DON'T use non-words for local variables, especially not for loop variables. If I had used u16 as a type, it'd have been 20% slower, which can run up to 50% or more if you use the variable often. Use of non-words require extra effort in the CPU's part, use those only if you have to.
    These things are still pretty easy to deal with and may speed things up by as much as 800%. More is certainly possible, but that depends on the current implementation, so it's difficult to see what we can do about that without seeing more of the code.

    #54184 - Tomik - Fri Sep 16, 2005 8:10 am

    I think the fastest way is tp plot current ecg black and the last one white so its erased. then shift it. and that works nice an fast !
    but how can I draw the line betwenn the 120 pixels to get a real ecg?
    or better how can I shift it ?
    I have already the drawing function made and it works, but now i need to shift the y=mx+b function ! i do not have an idea.
    the problem is that i draw a line between to pixels with this function.
    i dont have an array where this value are in ! (would be tooooo big)

    have an idea (!!!thats what i want poslundc!!!)

    Thomas

    #54185 - Cearn - Fri Sep 16, 2005 8:37 am

    How exactly is the data coming in? Some thing like: every frame you get a new y-value to add to the left or right of the screen? If so, do you store those values in an array somewhere? An array of 240 values is definitely not too big. Then you can use that array to erase the previous lines, and redraw it one pixel further along.

    Also, if you do have a horizontal resolution of 1 (successive x-values are 1 unit apart), then you don't really need a full line drawer. If you have, say coordinates (x,y0) and (x+1, y1), the line between them would just be two vertical lines at most. Something like
    Code:

     #
     #
     #
     #
    #
    #
    #
    #

    You can take the vertical average ya= (y0+y1)/2, and draw two vertical lines between (x,y0)-(x,ya) and (x+1,ya), (x+1, y1).

    #54186 - Tomik - Fri Sep 16, 2005 9:01 am

    It is a 120 Array because I use Mode 4 so I can only plot 2 Pixel at a time !

    Here I readin the new value of the ecg :
    Code:
    void RighttoLeft(int *yArr)                  // Plot current value and scroll right to left
    {
       int i;
       int xAXIS=0;

       for(i=21;i<140;i++)
       {
          PlotPixel(1,i,0xFFFF);
       }

       for(i=0; i<120 ; i++)                  
       {
          PlotPixel(i,yArr[i],0x0000);      // draw current Pixels      
          PlotPixel(i+2,yArr[i],0xFFFF);      // erase last pixel   

          yArr[i]=yArr[i+1];               // shift all Pixel one step to right (no drawing)   
       }
       
       ADC_CNT(yArr);                     // if loop down to 0 then load new ADC value   
    }


    ..and this is my drawing function:
    Code:
    void DrawLine(int *yArr)
    {
       float m,b;
       int x,y,i;
       
       for(i=0;i<119;i++)
       {
          if((abs(yArr[i]-yArr[i+1]))>2)
          {
             int x0=i;
             int y0=yArr[i];
             int x1=i+1;
             int y1=yArr[i+1];
             
             int dx=x1-x0;               
             int dy=y1-y0;

             m=((float)abs(dy))/(float)dx;            // explains the function
             b = y0-m*x0;

             if(y0<=y1)                        // if falling line
             {
                for(y=y0;y<y1;y++)
                {
                   x=(int)(-(b-y)/m+0.5);
                   PlotPixel(x,y,0x0000);
                   PlotPixel(x,y,0xFFFF);
                }
             }
             else                           // if rising line
             {
                for(y=y1;y<y0;y++)
                {
                   x=(int)(-(b-y)/m+0.5);
                   PlotPixel(x,y,0x0000);
                   PlotPixel(x,y,0xFFFF);
                }
             }
          }
       }
    }


    ..and if I use this drawing function every step and draw all lines new .. very slowly ;)

    Thomas

    #54187 - Cearn - Fri Sep 16, 2005 9:03 am

    Quote:
    Code:
    m=((float)abs(dy))/(float)dx;            // explains the function

    *cough*
    Well there's your slowdown.

    No floats on GBA please, and preferably no division too. I'll see about the rest later.

    ETA:

    The GBA has no floating point unit, so any floats or doubles have to be emulated (read: is slow). Just did a tiny little check and found that floating point division is roughly 7 times slower than the 'standard' integer division. The GBA also doesn't have a hardware integer division, so that's slow as well 17 times as slow as multiplication for example in this particular test (can vary a little bit, but it's a fair number). So right there you've lost yourself a factor 100.

    Instead of floating point, read up on fixed point arithmetic.
    Instead of division, use a reciprocal look up table.

    In fact, for drawing lines, you don't need any division at all, look up Bresenham.

    #54190 - Tomik - Fri Sep 16, 2005 10:35 am

    the problem is the draw line function:
    I will change the float and division things!
    What about that idea:
    make the timesteps on the xAxis fixed and so I can plot more pixel (new ADC value) for every xAxis value ?
    i think it sounds good, but dont know NOW how to realize it !!

    #54192 - Cearn - Fri Sep 16, 2005 10:56 am

    Quote:
    Code:
    ...
             int x0=i;
             int y0=yArr[i];
             int x1=i+1;     // <--
             int y1=yArr[i+1];
             
             int dx=x1-x0;               
             int dy=y1-y0;

    Hadn't noticed this yet. So yes, x0 and x1 are always 1 apart, and dx=1, by definition. That simplifies things enormously. Try this (untested) code:

    Code:
    void DrawLine(int *yArr)
    {
        int ix, iy;
        u32 y0, y1, ya;
        for(ix=0; ix<119; ix++)     // draw each line segment
        {
            y0= yArr[ix];
            y1= yArr[ix+1];
            if(y0>y1)       // swap so that y1 > y0
            {   int tmp= y0; y0= y1; y1= tmp; }

            ya= (y0+y1)/2;  // div by 2^n are optimised by GCC. This is fine.

            for(iy=y0; iy<ya; iy++)
                PlotPixel(ix, iy, 0xFF);

            for(iy=ya; iy<y1; iy++)
                PlotPixel(ix+1, iy, 0xFF);
        }
    }


    It's really not to difficult to use all 240 pixels in mode 4 btw, I gave you a function for that in another thread.

    #54193 - Tomik - Fri Sep 16, 2005 11:12 am

    really nice, works fine ! i have to change some little things ....
    thanks cearn !! it fast ;)

    thomas / germany

    #54199 - Cearn - Fri Sep 16, 2005 2:39 pm

    Messed up in the line drawer. For falling lines, the x values need to be swapped as well. Sorry :-]

    Also added a color parameter for the function, so that you can change line colors more easily.
    Code:

    void DrawLine(int *yArr, u32 clr)
    {
        int ix, iy;
        int x0, x1, y0, y1, ya;
        for(ix=0; ix<119; ix++)     // draw each line segment
        {
            y0= yArr[x0= ix];
            y1= yArr[x1= ix+1];
            if(y0>y1)       // swap so that y1 > y0
            {
                int tmp= y0; y0= y1; y1= tmp;
                x0= ix+1; x1= ix;
            }

            ya= (y0+y1)>>1;

            for(iy=y0; iy<ya; iy++)
                PlotPixel(x0, iy, clr);

            for(iy=ya; iy<y1; iy++)
                PlotPixel(x1, iy, clr);
        }
    }


    Also tested a full scrolling thing in no$gba. Basic procedure works, but may be just a little too slow for one VBlank. At least when you use the all 240 columns. With page flipping it worked fine though.
    Of course, there are still ways of speeding up the drawing code, like putting it in IWRAM.

    #54204 - Tomik - Fri Sep 16, 2005 4:43 pm

    ..and I have still the problem with draw only one pixel.
    In your function there between the steps one pixel missing if i plot in single pixel mode.

    It works now fine, but I still plot two pixels in a row, they are still together and i dont know how to split them !

    the problem wasnt the falling line, it was the rising one. because zero ist on the top ;) doesnt matter, i know what you ment.

    Thomas

    #54437 - Tomik - Mon Sep 19, 2005 1:42 pm

    It works very nice right now.
    I want now a constant time for readin the adc ! therefore i think i have to use a interrupt.
    can someone give me information how to use and handle that?

    Thanks, Thomas

    #54490 - tepples - Mon Sep 19, 2005 11:38 pm

    (In order to decide whether to use one of the LCD controller's timers or one of the programmable timers)
    How often do you need to read the ADC?
    _________________
    -- Where is he?
    -- Who?
    -- You know, the human.
    -- I think he moved to Tilwick.

    #54542 - Tomik - Tue Sep 20, 2005 7:52 am

    I would like to make 200, 400 or 800Hz readout !!

    #54543 - Cearn - Tue Sep 20, 2005 8:47 am

    You mean 200, 400, 800 new pixels/second? That's a whole lotta plotting.
    Then again, maybe not: the screen itself refreshes every 60 Hz; why you could redraw the whole thing 200 times a second, you'd only be able to show it in 200/60 ~ 3 pixel increments anyway. And that'd go up to 13 for 800 Hz. It could be done with timer interrupts. There may be trouble with the fact that drawing the whole line takes a lot longer though (best case: around 450 Hz for a 120 horizontal resolution, all code in IWRAM), but it could be done.

    But I have to wonder whether you'd want to go through all that, though. 200 Hz would mean 200 scrolls/second, and for 120 pixels that'd mean you'd have a completely new screen ever 0.6 seconds. While it won't exactly be a blur, it may still be a little too much for comfortable viewing. For 800, it probably would be a blur.

    #54545 - Tomik - Tue Sep 20, 2005 8:58 am

    i dont want to plot 800hz pixel after pixel (xAxis). the value might be at the "same" xaxis-value. means maybe more pixels on the y Axis, so i would get a better resolution and can forget the linedrawing !
    i want to get 3 sec on the display (40pxl/beat @ 60hz).
    the problem is to get 3 seconds on display ... doesnt matter if hearbeat is 60 or 250BPM.

    #54552 - Tomik - Tue Sep 20, 2005 11:00 am

    i think i will make xAxis steps in 50Hz, that means every 0.02sek a new x axis value!! that DONT mean 50Hz new ADC values ! if i readout the adc data with 400-800Hz i could draw 8-16 yvalue on on x step !!!

    so how can i realize the 50Hz xAxis Steps??

    Like this http://user.chem.tue.nl/jakvijn/tonc/timers.htm#REG_TMxD:
    Code:
    REG_TM2D= 0x147;                      // 0x147 (50Hz) ticks till overflow
    REG_TM2CNT= TM_ON | TM_FREQ_1024; // we're using the 1024 cycle timer   
    REG_TM3CNT= TM_ON | TM_CASCADE;   // cascade into tm3

    main
    {
      u16 count=REG_TM3D;
      while(1)
      {
        if(REG_TM3D != count)
        {
           // [plotting code]
           count= REG_TM3D;
        }
      }
    }

    #54654 - Tomik - Wed Sep 21, 2005 7:59 am

    what do you think about this:

    Code:

    void main(void)
    {
       REG_TM2D   = 0xFFFF-0x147;         // 327 ticks is 20ms = 50 Hz for xValue shifting
       REG_TM2CNT = 0x83;               // 1024 cycle timer - 61.04us / 16.384kHz
       REG_TM3CNT = 0x87;

      u16 xAxisShifter = REG_TM2D;
    .
    .
    .
      u16 xAxisShifter=REG_TM3D;
      while(1)
      {
        if(REG_TM3D != xAxisShifter)
        {
           // [plotting code]
           xAxisShifter= REG_TM3D;
        }
    }

    #54756 - Tomik - Thu Sep 22, 2005 10:45 am

    it runs very nice right now ... but the solution is still not so good.
    so how can i get this "two pixel in a row" drawing to a one pixel drawing.

    Code is:
    Code:

    void PlotPixel(int x,int y, u16 c)   
    {
       videoBuffer[(y) *120 + (x)] = (c);
    }

    void RighttoLeft(int *yArr)                  // Plot current value and scroll right to left
    {
       int i;
       int xAXIS=0;

       for(i=0; i<120 ; i++)                  
       {
          PlotPixel(i,yArr[i],0x0000);      // draw current Pixels   
          PlotPixel(i+2,yArr[i],0xFFFF);      // erase last pixel
          yArr[i]=yArr[i+1];               // shift all Pixel one step to right (no drawing)   
       }
       
       ADC_CNT(yArr);                     // if loop is 119 then load new ADC value   
    }


    have an idea ??

    #54778 - tepples - Thu Sep 22, 2005 3:29 pm

    Untested, but it should give you the idea of how to use a read-modify-write sequence to plot one pixel at a time:
    Code:

    void PlotPixel(unsigned int x, unsigned int y, unsigned int c)
    {
      /* Find the address of a pair of pixels. */
      unsigned short *pair = &videoBuffer[y * 120 + (x >> 1)];

      /* Bounds checking is useful for debugging and optimizing. */
      assert(x < 240 && y < 160 && c < 256);

      /* 1: pixel on the right; 0: pixel on the left */
      if(x & 1)
        /* High byte of pixels is on the right. Erase the high byte and replace it. */
        *pair = (*pair & 0x00ff) | (c << 8);
      else
        /* Low byte of pixels is on the left. Erase the low byte and replace it. */
        *pair = (*pair & 0xff00) | c;
    }


    EDIT: comments added
    _________________
    -- Where is he?
    -- Who?
    -- You know, the human.
    -- I think he moved to Tilwick.


    Last edited by tepples on Tue Sep 27, 2005 4:20 am; edited 1 time in total

    #55215 - Tomik - Mon Sep 26, 2005 1:52 pm

    What is the difference between assert() and if() ?
    assert: if zero then .... else stop programm !?!?
    if: could be the same !?!?!?!

    can you comment that code a bit please .... thanks

    Thomas

    #55221 - poslundc - Mon Sep 26, 2005 5:00 pm

    A thread about asserts: http://forum.gbadev.org/viewtopic.php?t=2999

    One thing not mentioned in that thread is that you can easily define asserts to be automatically optimized out for a non-debug build, such as:

    Code:

    #if OPTION_ASSERT
    #define ASSERT(x) \
        if (!(x)) \
            AssertPrint("Assertion failed:\n\tFile: %s\n\tLine: %d\n", __FILE__, __LINE__);
    #else
    #define ASSERT(x) ;
    #endif


    (Where AssertPrint is a custom-function that outputs a message to the debugger, then drops into an infinite loop, or perhaps waits for a button combination before proceeding.)

    The advantages speak for themselves: they give you a simple way to put bounds-checking on the input to your various modules and test that the results of your algorithms behave as expected, and it can all automatically vanish when you want to create an optimized build.

    Dan.

    #57055 - Tomik - Thu Oct 13, 2005 11:04 am

    Hey, its me again.

    I changed a lot. Specially I have 800Hz Sample Rate, with 40Hz scrolling.
    That means 20 ECG value in one of the 240 columns ....
    It doesnt work fine right now, because I have an Array which is 240*20 Pixel. I thought about changing the mode. I use Mode 4 right now.
    I think I need a Tile-Mode for that.
    What do you think? TILE or BITMAP-MODE ?
    Can I make all that with Tile-Mode 2?

    I need help with that...

    tahnks, Thomas /Germany

    #57061 - Cearn - Thu Oct 13, 2005 12:31 pm

    Define "It doesnt work fine right now". It's too slow?

    Anyway, in order to use tilemodes you'd have to edit individual tiles to get the right pixels. That could be a bit hairy. But as in your case scrolling is limited to constant speed, horizontally, and there wouldn't be too many other things on screen (right?), I have a feeling you can simplify a lot.
    First of all, as scrolling in tile-moes wraps around, you only have to update the new column and the rest can be kept as was, meaning you don't need any array at all. In its stead, just use a single scroll counter, indicating the column you have to fill in. But please, do not use something like REG_BGxHOFS++, because the scrolling registers are write only.

    Here's what might work (depends on the complexity of your case, and basically pulled out of my a^Hhat as I'm writing this, so bare with me).

    For the tile modes, you have a charblock (CBB) and a screenblock (SBB). A full screenblock contains 32x32=1024 screen entries (SE), which is also the amount of tiles you can access in regular backgrounds (i.e., modes 0 and 1; mode 2 would only make things harder). Set up the map so that it's column-based to start with. That is, something like this:
    Code:
    // tile-indices in the screenblock (in hex):
    000 020 040 060 ... 3C0
    001 021 041 061 ... 3C1
    ...
    01F 03F 05F 07F ... 3FF     

    This means that each stretch of 32 tiles in the charblock signifies a column on the background. That right, each row in the CBB is a column in the screenblock. This is a win situation because if you use 4bpp tiles, there are 8 pixels/tile-row, == one u32 /tile-row, and so each column on screen is basically represented by a continuous array of words. Get the tile-column from the current offset (ofs>>3), and the nybble-offset in the word (which corresponds with the pixels ofset in the tile-column) by ofs&7. plotting a vertical line then becomes something like
    Code:
    int tx= ofs>>3, ix= ofs&7, iy;

    // nasty casting here because I don't know how you're defining your
    // charblocks, if you're using my tile_mem stuff, it'd be much easier.
    // u32 *col= &tile_mem[cbb_id][tx*32];
    u32 *col= (u32*) &( ((u8*)cbb_ptr)[tx*32*32] );

    // clr_id: palette index in [0,15), shifted to the right nybble
    u32 pxl l= clrid<<(4*ix);

    // plotting new pixels as (ix,iy), for iy in [y0, y1) ; 0<=y0<=y1<32
    for(iy=y0; iy<y1; iy++)
        col[iy] |= pxl;

    You will have to make sure that you erase tiles that you've scrolled past, but that shouldn't be too hard. And, again, because of the column-based map, it shouldn't take long either, as you can use a single DMA_FILL or CpuFastSet for it.

    Hmm, hope that make any kind of sense to you :P.

    Just curious, how variable is an ECG? Do you have a representative screenshot somewhere?

    #57211 - Tomik - Fri Oct 14, 2005 10:24 am

    the ECG I use is not very hard. but forget the ECG, use a sinus ... its simple too !
    I have to trie a lot ...

    i would like to use a 800Hz samplerate ... 40Hz scrolling !! that means
    20 value in column ..big array ..but now drawing line (interpolation)
    in mode 4 i have to have an array ... or how cxan i scroll the whole matrix ? that would make everything faster and easier ....

    Thomas

    #57212 - Cearn - Fri Oct 14, 2005 10:35 am

    20 values/column = 480 entries. That's not all that big actually. You could house a lot more in IWRAM.
    For scrolling the entire screen you can use the scrolling registers, see tutorial code to see how you can work with them. Technically they work for the bitmap modes too, but as those modes don't wrap around at the edges, you can't really use them there.

    #57232 - Tomik - Fri Oct 14, 2005 1:38 pm

    20values/column ... 240columns ..=4800values ! thats big ! ever step (40Hz=25ms) shifting 4800 values and draw 4799 lines between them ... make a detection algorithmus or something else ... thats much, too much !!
    I will check out the scrolling register !!
    do you have an example code for this ??

    #57235 - Tomik - Fri Oct 14, 2005 2:03 pm

    My question is: how can I copy (in Mode 4) the current screen to the inactive on ... an refresh the old on with only one new value ? that means not calculating every step every value, every line ...aso !!
    know what i mean ??

    Thomas

    #57236 - Cearn - Fri Oct 14, 2005 2:17 pm

    Tomik wrote:
    20values/column ... 240columns ..=4800values

    Heh. Oops :P
    Would still fit though.

    I thought the point of having so many samples was that you wouldn't have to draw lines between them anymore? You probably wouldn't be able to see 20 pixels / column, so you wouldn't need to keep track of them all; just min/max or average might work just as well.

    And you don't need to shift each and every one of the array entries either, you could use it as a ring buffer: keep an index ix for the 'current' column and work with offsets. Still, drawing 4800 lines would be too much, yeah.

    Using tilemodes would probably be best if you need scrolling, just browse around in the gbadev demo section and tutorial text and code. Don't rush into applying it to the ECG graph, just muck around with charblocks and screenblock and registers to get a feel of what you're doing. It's not hard, it just takes some getting used to.

    Tomik wrote:
    do you have an example code for this ??

    tonc code does have a few examples for working with tilemap, nothing fancy though.

    #57244 - Tomik - Fri Oct 14, 2005 2:48 pm

    does the shifting of the whole screen also work in Mode 4 ?

    #57603 - Cearn - Mon Oct 17, 2005 8:56 am

    Tomik wrote:
    does the shifting of the whole screen also work in Mode 4 ?
    Yes. And no! Mostly no.
    You can move the screen around (with REG_BG2X and REG_BG2Y, as bg2 is an affine background in the bitmap modes), but because it doesn't wrap around at the edges, the whole background disappears from view after you've scrolled 240 pixels.
    In tile-bgs do wrap around: if you move it by, say, 60 pixels to the left, the pixels that have disappeared on the left side wrap around the screen and are shown on the right side, so you have a repeating background. Note that the wrap is around 256 pixels, not 240, this gives you a 16 pixel window in which you can change either the screen entries or the tiles themselves in order to get a continuously scrolling background. That's pretty much how all the scrolling games work; you can see this in action in emulators with map/tile viewers.

    Below is an example of what I was talking about.
    m0_ecg_init() sets up a map of increasingtile indices in column format.
    m0_ecg_scroll() adds a new column on the right of the screen; ecg_hofs is a global counter, indicating which (moved) column we're working on. Because the screenblock was column based, each consecutive block of 32 tiles represents an 8x256 block of pixels on screen: a tile-column. We have to find the column which is on the right side of the screen, (ecg_hofs&255)>>3, and then the pixel in that tile-column,ecg_hofs&7. Simply OR the pixel of choice in there. Because we re-use columns, we need to make sure old ones are erased before re-use, that's done in the hofs&7 if-block. I'm using memset there because it's easy, but if you really need to, there are faster methods.
    m0_ecg() is basically the main function here, with the main loop and a retrieval for the y values using a sine function because I don't have an actual ECG using to hook it up to.

    You will probably need to rename a few things for it to compile, but I hope you can manage.

    Code:

    #include <string.h>     // for memset

    // --- some extra types ---
    typedef struct { u32 data[8];  } TILE, TILE4;
    typedef TILE     CHARBLOCK[512];
    typedef u16      SCREENBLOCK[1024];

    // --- memory areas ---
    // tile_mem[y] = TILE[]   (char block y)
    // tile_mem[y][x] = TILE (char block y, tile x)
    #define tile_mem      ( (CHARBLOCK*)0x06000000)

    // se_mem[y] = SB_ENTRY[] (screen block y)
    // se_mem[y][x] = screen entry (screen block y, screen entry x)
    #define se_mem        ((SCREENBLOCK*)0x06000000)

    // === the real stuff ===

    #define ECG_CBB      0
    #define ECG_SBB     31
    #define ECG_CLRID    1
    #define ECG_HOFS0  240
    #define ECG_POINTS   2
    #define ECG_Y0      80

    int ecg_hofs=0;
    int ecg_ys[ECG_POINTS];

    void m0_ecg_init()
    {
        int ix, iy;
        u16 *se= (u16*)&se_mem[ECG_SBB];

        for(ix=0; ix<ECG_POINTS; ix++)
            ecg_ys[ix]= ECG_Y0;

        for(ix=0; ix<32; ix++)
            for(iy=0; iy<1024; iy += 32)
                *se++= iy+ix;

        pal_bg_mem[ECG_CLRID]= CLR_CYAN;
        REG_BG0CNT= (ECG_SBB<<BG_SBB_SHIFT) | ECG_CBB;

        REG_DISPCNT= _DCNT_MODE0 | DCNT_BG0_ON;
    }

    void m0_ecg_scroll()
    {
        // current pixel ofs; with wrapping
        u32 hofs= ecg_hofs&255;

        ecg_hofs++;

        u32 tx= hofs>>3;    // tile-column id

        // get pointer to the tile-column
        // 4bpp: 8pixels/word
        u32 *col= (u32*)&tile_mem[ECG_CBB][32*tx];

        // new tile-column:
        //   erase the one that'll be used next
        if((hofs&7)==0)
        {
            u32 otx= (tx+1)&31;
            u32 *oldcol= (u32*)&tile_mem[ECG_CBB][32*otx];
            // clears 32*8*4 bytes (32 tiles)
            // (use memset32 or CpuFastSet if you know how)
            memset(oldcol, 0, 32*8*4);
        }

        u32 sh= (hofs&7)*4; // pixel shift in tile-column

        // draw line at right side of screen
        int iy, y0, y1;

        y0= ecg_ys[0];
        y1= ecg_ys[1];
        col[y1] |= ECG_CLRID<<sh;

        // swap if necessary
        if(y0 > y1)
        {   int t= y0; y0= y1; y1= t;   }

        for(iy=y0; iy<y1; iy++)
            col[iy] |= ECG_CLRID<<sh;

        REG_BG0HOFS= hofs-ECG_HOFS0;
    }

    void m0_ecg()
    {
        m0_ecg_init();

        int x=0;
        while(1)
        {
            // Get new pixels from a sine LUT (change for your own needs)
            ecg_ys[0]= ecg_ys[1];
            ecg_ys[1]= ECG_Y0-(60*SIN(x>>4)>>8);
            x += 0x022;

            vid_vsync();    // vsync (60Hz updates)
            m0_ecg_scroll();
        }
    }