8

I'm following root42's videos about DOS programming using Turbo C 2.01. I've written my own Soundblaster 1.xx driver following the Creative Labs documentation, and I'm confused about memory allocation. I'm working in real mode in the large model.

To play sound through the SB using DMA, the source memory block must be wholly contained in one memory segment (that is, the linear address of the first and last bytes of the block & 0xFFFF0000 must be equal).

In root42's video, they use the following code to allocate a 32768 byte block:

void assign_dma_buffer()
{
  unsigned char* temp_buf;
  long linear_address;
  short page1, page2;

  temp_buf = (char *) malloc(32768);
  linear_address = FP_SEG(temp_buf);
  linear_address = (linear_address << 4)+FP_OFF(temp_buf);
  page1 = linear_address >> 16;
  page2 = (linear_address + 32767) >> 16;
  if( page1 != page2 ) {
    dma_buffer = (char *)malloc(32768);
    free( temp_buf );
  } else {
    dma_buffer = temp_buf;
  }
  linear_address = FP_SEG(dma_buffer);
  linear_address = (linear_address << 4)+FP_OFF(dma_buffer);
  page = linear_address >> 16;
  offset = linear_address & 0xFFFF;
}

This looks like it allocates memory, examines the resulting absolute address to determine whether both end-points lie within the same segment, and if so places the address in page and offset global variables for use by DMA later, otherwise it has a second attempt and returns that unconditionally.

What would happen if the second attempt was also straddling a segment boundary? I think this code works on the assumption that malloc returns consecutive, contiguous blocks of memory.

Is there some Turbo C / MS-DOS specific way I can request a block of memory that's guaranteed to lie within just one segment? Or, even better, just request the whole segment?

1

2 Answers 2

15

You’re correct, the goal of this code is to ensure that the allocated buffer is entirely contained within the same DMA segment (DMA operates on 64KiB segments, not to be confused with the 16-byte-aligned real-mode segments of the x86 addressing model).

The assumption that the allocator returns successive blocks is safe, at least before the heap gets fragmented, since the program won’t be doing any other allocations while this is happening; but even when the allocations return successive blocks, they won’t be adjoining, because Turbo C’s allocator has some overhead between blocks. Some quick testing with Turbo C 2.01 suggests that two successive 32KiB allocations are separated by 16 bytes (the blocks always start at offset 8 of a 16-byte segment, which suggests an overhead of 8 bytes plus segment alignment), so there are edge cases where such a sequence of allocations will return two blocks which aren’t within 64KiB DMA segments; for example, one buffer at 0x4FFF8, the following at 0x58008...

The general way to avoid any issues with such allocations, for a DMA-compliant block of memory up to 32KiB, is to allocate a block of memory double the size (using farmalloc in Turbo C if necessary); at least one of the halves of the returned block will be entirely within the same 64KiB DMA segment:

void far * full_buf;

full_buf = farmalloc(65536);

At this point, one of the halves of the block pointed at by full_buf is within a 64KiB DMA segment. That’s not really DOS-like though since 32KiB is a lot of memory to waste; but you can always use it for something else.

9
  • Thank you - I plan to write a typical mixer with two DMA buffers so I think the best thing to do in this instance is to aim for a smaller buffer size (4KiB or 8KiB) and just accept that I'll be unable to use half of what I allocate.
    – knol
    Sep 15, 2020 at 16:26
  • I wonder if the history of DMA use could have been different if IBM had added a couple of NAND gates so that if the one of the DMA channels not used for hard drive or floppy drive would have its upper-address bits taken from its programmed slot when A15 is high, but from DMA channel 0's upper-bits register when A15 is low. That would have made it possible to accommodate DMA transfers of up to 32768 bytes at any range of addresses. Disk drive transfers would typically use pre-allocated 512-byte buffers, so page crossing wouldn't matter for those.
    – supercat
    Sep 15, 2020 at 16:40
  • 1
    @knol You can use the half of the buffer you're not using for DMA for something else.
    – user722
    Sep 15, 2020 at 17:00
  • 3
    farmalloc() must have been originally written for agricultural land management software Sep 15, 2020 at 18:47
  • 1
    If you need two blocks, less memory is wasted. Do a single allocation of three times the size and find the two complete blocks which are (necessarily) within them. The waste is the same size as the single allocation case (up to one block) but proportionately smaller. Jun 12, 2021 at 11:21
7

This is how I did it in an old program I wrote to exercise my knowledge of the DMA DSP of the Sound Blaster, back in 1996.

char *AllocDMABuffer (void)
{
  char *pTemp;
  unsigned int Segm;

  pTemp=farmalloc(131072);
  if (!pTemp)
    return NULL;

  Segm=FP_SEG (pTemp);
  while (Segm & 0x0FFF)
    Segm++;

  return MK_FP(Segm,0);
}

For this specific program, I didn't care about freeing the allocated memory, as there was just one DMA buffer to be used by the whole program, until its termination, so I didn't care that the original pTemp pointer was lost upon function exit.

As you see, it's all about allocating twice the desired amount of memory, and then normalize the pointer so that the memory region has a segment value which is an integral multiple of 4096.

The DMA chip uses 16 bits for memory addressing, and the remaining 4 bits are stored in a different off-chip register. This means that not every 64KB memory block is well suited for a DMA transfer. Its starting physical address must be in the form xxxx 0000 0000 0000 0000 in binary.

Translating this into the segment:offset scheme that the x86 uses in real mode, this means that the starting address must be in the form: X000h:0000h so that the linear address is X000<<4 + 0000 = X0000 = xxxx 0000 0000 0000 0000. That's why the segment has to be an integral multiple of 4096.

The while portion may probably be optimized, though. Something like:

if (Segm & 0x0FFF)
  Segm += (0x0FFF - Segm & 0x0FFF);

If you see the current code, you will notice why allocating twice the required size is needed. As you cannot control which segment the allocator gives to you, it might have any value so it must be adjusted to the next integral multiple of 4096 and that means advancing it up to 4095 steps. Each step (each Segm++ operation) walks through a parragrah (16 bytes), so in the worst case, finding a valid segment may mean wasting 4095*16 = 65520 bytes.

1
  • As per your correct statement that up to 65520 bytes may be wasted, you only need to allocate 65520 + 65536 = 131056 bytes, that is 16 bytes less than 128 KiB.
    – ecm
    Nov 16, 2020 at 9:55

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .