WRITING A LSE/OS DRIVER

An example: the 8237 DMA Controller driver
What is better than examining an existing driver and understanding how it works? Let's go in the lseos-srv/hw/dma directory.

$ cd lseos-srv/hw/dma
$ ls
CVS  Makefile  dmareg.h  dmavar.h  libdma.c  libdma.h  main.c

The driver is composed of three files: main.c, dmareg.h and dmavar.h. The other files: libdma.c and libdma.h are libraries for client tasks to use the DMA service offered by the driver. As in the BSD convention, the *reg.h files are for defining registers used by the driver. The *var.h files contains the softc (a softc is basically just miscellaneous software state for a device driver) The main.c file contains the source code of the driver itself. Let's have a look in these files:

dmareg.h

#ifndef __DMAREG_H__
#define __DMAREG_H__    1

#define DMAMODE_VRFY            (0<<2)
#define DMAMODE_WRITE           (1<<2)
#define DMAMODE_READ            (1<<3)
#define DMAMODE_AUTOINIT        (0<<4)
#define DMAMODE_NONAUTOINIT     (1<<4)
#define DMAMODE_AINCR           (1<<5)
#define DMAMODE_DEMAND          (0<<6)
#define DMAMODE_SINGLE          (1<<6)
#define DMAMODE_BLOCK           (1<<7)
#define DMAMODE_CASCADE         (DMAMODE_BLOCK|DMAMODE_SINGLE)

#endif
This file contains the hardware defines of the controller. These defines come from this documentation http://www.nondot.org/sabre/os/files/MiscHW/DMA_RTI.txt. Additional hardware documentation on this controller could be found here:http://www.nondot.org/sabre/os/articles/MiscellaneousDevices/.

dmavar.h

#ifndef __DMAVAR_H__
#define __DMAVAR_H__    1

typedef struct          s_dma
{
  pid_t                 holder;
  char                  *dmabuf;
  paddr_t               pa;             /* physical address of buffer */
  int                   dmapage;
  int                   dmalen;
  int                   dmaoff;
  u_int32_t             dmaopt;
  u_int32_t             avail:1;
}                       t_dma;

The t_dma structure contains the information stored by the driver for each dma channel.

main.c

#include <libc.h>
#include <core.h>
#include "libdma.h"
#include "dmavar.h"
#include "dmareg.h"
Note that drivers are tasks just as other programs and use libc.h.

/*
 * most of this is taken from nstalker@iag.net documentation
 */

/*
 * number of dma channels
 */
#define MAXDMA	8

_cpu_simple_lock_t   lock;
t_dma		dmas[MAXDMA];

The two above lines are very important. The first one define a spinlock that will be used in SMP (symetric multiprocessing) environment for avoiding two tasks beeing serviced at the same time. The second one defines an array of softcs representing the state of the 8 dma channels.

/*
 * quick-access registers and ports for each DMA channel. 
 */
u_char mask_reg[MAXDMA]   = {0x0A, 0x0A, 0x0A, 0x0A,
				 0xD4, 0xD4, 0xD4, 0xD4};
u_char mode_reg[MAXDMA]   = {0x0B, 0x0B, 0x0B, 0x0B,
				 0xD6, 0xD6, 0xD6, 0xD6};
u_char clear_reg[MAXDMA]  = {0x0C, 0x0C, 0x0C, 0x0C,
				 0xD8, 0xD8, 0xD8, 0xD8};

u_char page_port[MAXDMA]  = {0x87, 0x83, 0x81, 0x82,
				 0x8F, 0x8B, 0x89, 0x8A};
u_char addr_port[MAXDMA]  = {0x00, 0x02, 0x04, 0x06,
				 0xC0, 0xC4, 0xC8, 0xCC};
u_char count_port[MAXDMA] = {0x01, 0x03, 0x05, 0x07, 
				0xC2, 0xC6, 0xCA, 0xCE};

#define DMA_SIZE	(64*1024)

/*
 * defines for accessing the upper and lower byte of an integer. 
 */
#define LOW_BYTE(x)	(x & 0x00FF)
#define HI_BYTE(x)	((x & 0xFF00) >> 8)

void	dma_start(t_dma *dmas, u_char channel, u_char mode)
{
  if (channel >= 4)
    mode |= (channel & 0x0004);
  else
    mode |= channel;

#if 0
  printf("mode 0x%x\n", mode);
#endif

  /* 
   * set up the DMA channel so we can use it.  This tells the DMA that
   * we're going to be using this channel.  (It's masked)
   */
  outb(mask_reg[channel], 0x04 | channel);

  /* 
   * clear any data transfers that are currently executing.
   */
  outb(clear_reg[channel], 0x00);

  /* 
   * send the specified mode to the DMA. 
   */
  outb(mode_reg[channel], mode);

  /*
   * send the offset address.  the first byte is the low base offset,
   * the second byte is the high offset.
   */
  outb(addr_port[channel], LOW_BYTE(dmas[channel].dmaoff));
  outb(addr_port[channel], HI_BYTE(dmas[channel].dmaoff));

  /*
   * send the physical page that the data lies on. 
   */
  outb(page_port[channel], dmas[channel].dmapage);

  /*
   * send the length of the data.  Again, low byte first. 
   */
  outb(count_port[channel], LOW_BYTE(dmas[channel].dmalen));
  outb(count_port[channel], HI_BYTE(dmas[channel].dmalen));

  /*
   * ok, we're done.  enable the DMA channel (clear the mask). 
   */
  outb(mask_reg[channel], channel);
}

void	dma_stop(u_char channel)
{
  /* 
   * we need to set the mask bit for this channel, and then clear the
   * selected channel.  then we can clear the mask.
   */
  outb(mask_reg[channel], 0x04 | channel);

  /* 
   * send the clear command. 
   */
  outb(clear_reg[channel], 0x00);

  /*
   * and clear the mask. 
   */
  outb(mask_reg[channel], channel);
}

u_int	dma_remain(u_char channel)
{
  u_int8_t	low, high;
  u_int16_t	data;

  outb(0x0c, 0xff);

  low = inb(count_port[channel]);
  high = inb(count_port[channel]);

  data = high<<8 | low;

  return (data);
}
The above functions are the raw functions used to program the controller. The next functions are more for defining the functional aspect of the driver:
void	sys_dma_register(t_tcb *caller)
{
  int		channel, dmaoff, dmalen, dmapage;
  char		*dmabuf;
  paddr_t	pa;
  u_int32_t	dmaopt;
  u_int32_t	pa_mode;

  channel = SYS_ARG1(caller);
  dmabuf = (char *)SYS_ARG2(caller);
  dmaoff = SYS_ARG3(caller);
  dmalen = SYS_ARG4(caller);
  dmaopt = SYS_ARG5(caller);
The sys_dma_register() function is a syscall with 5 parameters: channel, dmabuf, dmaoff, dmalen and dmaopt. All of these parameters are 32bit sized pointers/integers, note the special way of retrieving them in the task context of the caller with SYS_ARG*() macros. On LSE/OS, syscall parameters are passed using registers. If these registers are not sufficient then arguments are directly copied from the caller address space (there is an example of such a copy in the sys_prop_store() syscall in lseos-srv/basic/prop/main.c).

  if (channel < 0 || channel > 7)
    {
      SYS_RETURN(caller, -1, EINVAL, DMA_REGISTER_BAD_CHANNEL);
    }

The above test is a sanity check of the channel number. Note the special usage of the SYS_RETURN() macro that returns an error to the caller.


#if 1
  dprintf("registering dma channel %d\n", channel);
#endif
  

  if (dmas[channel].avail == 0)
    {
      SYS_RETURN(caller, -1, EINVAL, DMA_REGISTER_CHANNEL_NOT_AVAIL);
    }

  if (dmaopt & DMAOPT_COPY)
    {
      vaddr_t	freshaddr;
      char	*freshbuf;
      /*
       * we copy buffer into our space
       */

      if ((dmaoff + dmalen) >= DMA_SIZE)
	{
	  SYS_RETURN(caller, -1, EINVAL, DMA_REGISTER_BAD_LEN);
	}

      /*
       * -2 option to prsv doesn't exist anymore, dma
       * must prereserve fitable physical memory or a program
       * should do so if really needed
       */
      SYS_RETURN(caller, -1, ENODEV, 0);

      if ((freshaddr = pgalloc2(-1, 
				(paddr_t)-2, /* dma */
				16,
				&pa)) == (vaddr_t)-1)
	{
	  SYS_RETURN(caller, -1, errno, suberrno);
	}
      
      freshbuf = (char *)freshaddr;

      if (copy(caller->pid, 
		   dmabuf, 
		   -1,
		   freshbuf,
		   dmaoff + dmalen) == -1)
	{
	  (void)pgfree(-1, freshaddr, 16);

	  SYS_RETURN(caller, -1, errno, 0);
	}

      dmapage = pa / DMA_SIZE;

      assert(dmapage < 16*16);
      
      dmas[channel].holder = caller->pid;
      dmas[channel].avail = 0;
      dmas[channel].dmabuf = freshbuf;
      dmas[channel].pa = pa;
      dmas[channel].dmapage = dmapage;
      dmas[channel].dmaoff = dmaoff;
      dmas[channel].dmalen = dmalen;
      dmas[channel].dmaopt = dmaopt;
    }
  else
    {
      vaddr_t	va;

      /*
       * we directly use user buffer
       */
      
      if (vdef(caller->pid,
	       (vaddr_t)dmabuf,
	       16,
	       &pa,
	       NULL,
	       &pa_mode,
	       NULL) == -1)
	{
	  SYS_RETURN(caller, -1, errno, suberrno);
	}

      if (!(pa_mode & PMODE_SHARED_RW))
	{
	  SYS_RETURN(caller, -1, EINVAL, DMA_REGISTER_NOT_SHARED_RW);
	}
      
      if (pa % DMA_SIZE != 0)
	{
	  SYS_RETURN(caller, -1, EINVAL, DMA_REGISTER_BAD_DMA_BLOCK);
	}
      
      if ((dmaoff + dmalen) >= DMA_SIZE)
	{
	  SYS_RETURN(caller, -1, EINVAL, DMA_REGISTER_BAD_LEN);
	}
      
      dmapage = pa / DMA_SIZE;
      
      /*
       * must be <16Mb
       */
      if (dmapage >= 16*16)
	{
	  SYS_RETURN(caller, -1, EINVAL, DMA_REGISTER_NOT_DMA_MEM);
	}

      if ((va = vrsv(-1, (vaddr_t)-1, 16, 0u)) == (vaddr_t)-1)
	{
	  SYS_RETURN(caller, -1, errno, suberrno);
	}
      
      /*
       * we dont *really* need to rsv and map pages but using this
       * cause page refcnt to be incremented and so, phys page to be
       * unremovable until unregister
       */
      if (vmap(-1, va, 16, pa, 16) == -1)
	{
	  (void)vrele(-1, va, 16);

	  SYS_RETURN(caller, -1, errno, suberrno);
	}
      
      dmas[channel].holder = caller->pid;
      dmas[channel].avail = 0;
      dmas[channel].dmabuf = (char *)va;
      dmas[channel].pa = pa;
      dmas[channel].dmapage = dmapage;
      dmas[channel].dmaoff = dmaoff;
      dmas[channel].dmalen = dmalen;
      dmas[channel].dmaopt = dmaopt;
    }
Note that the driver can manipulates user buffers (above) in using two ways:

It copies the user buffer in the driver address space (DMAOPT_COPY)

It maps directly the user physical buffer in its address space

  dma_start(dmas, channel,
	    (dmaopt & DMAOPT_READ ? DMAMODE_READ :
	     dmaopt & DMAOPT_WRITE ? DMAMODE_WRITE : 0u)|
	    DMAMODE_SINGLE);
  
  SYS_RETURN(caller, 0, EZERO, 0);
}

void	sys_dma_unregister(t_tcb *caller)
{
  int		channel;

  channel = SYS_ARG1(caller);

#if 0
  dprintf("unregistering dma channel %d\n", channel);
#endif
  
  if (channel >= MAXDMA)
    {
      SYS_RETURN(caller, -1, EINVAL, 0);
    }

  if (dmas[channel].avail == 1)
    {
      SYS_RETURN(caller, -1, ENOENT, 0);
    }
  else
    {
      if (dmas[channel].holder != caller->pid)
	{
	  SYS_RETURN(caller, -1, EACCES, 0);	  
	}
      
      dma_stop(channel);

      if (dmas[channel].dmaopt & DMAOPT_COPY)
	{
	  (void)pgfree(-1, (vaddr_t)dmas[channel].dmabuf, 16);
	}
      else
	{
	  (void)vunmap(-1, (vaddr_t)dmas[channel].dmabuf, 16);
	  (void)vrele(-1, (vaddr_t)dmas[channel].dmabuf, 16);
	}

      dmas[channel].avail = 1;
    }

  SYS_RETURN(caller, 0, EZERO, 0);
}

void	sys_dma_reset(t_tcb *caller)
{
  int		channel;
  int		dmaoff, dmalen;

  channel = SYS_ARG1(caller);
  dmaoff = SYS_ARG2(caller);
  dmalen = SYS_ARG3(caller);

#if 0
  dprintf("unregistering dma channel %d\n", channel);
#endif
  
  if (channel >= MAXDMA)
    {
      SYS_RETURN(caller, -1, EINVAL, 0);
    }

  if (dmas[channel].avail == 1)
    {
      SYS_RETURN(caller, -1, ENOENT, 0);
    }
  else
    {
      if (dmas[channel].holder != caller->pid)
	{
	  SYS_RETURN(caller, -1, EACCES, 0);	  
	}

      if ((dmaoff + dmalen) >= DMA_SIZE)
	{
	  SYS_RETURN(caller, -1, EINVAL, 3);
	}
      
      dma_stop(channel);

      dmas[channel].dmaoff = dmaoff;
      dmas[channel].dmalen = dmalen;

      dma_start(dmas, channel,
		(dmas[channel].dmaopt & 
			DMAOPT_READ ? DMAMODE_READ :
		 dmas[channel].dmaopt & 
			DMAOPT_WRITE ? DMAMODE_WRITE : 0u)|
		DMAMODE_SINGLE);
    }

  SYS_RETURN(caller, 0, EZERO, 0);
}

void	sys_dma_dump(t_tcb *caller)
{
  int	i;

  for (i = 0;i < MAXDMA;i++)
    {
      if (!dmas[i].avail)
	{
  dprintf("%d: pid 0x%qx pa 0x%x off %d len %d opt 0x%x remain %d\n",
		 i, dmas[i].holder, dmas[i].pa, dmas[i].dmaoff,
		 dmas[i].dmalen, dmas[i].dmaopt, dma_remain(i));
	}
    }

  SYS_RETURN(caller, 0, EZERO, 0);
}

Above, we find other syscalls for unregistering, resetting, debugging the dma.

void    do_dmasrv_syscall()
{
  int	status;
  t_tcb caller;

  if (slink(&caller) == -1)
    {
      SET_SYS_RETURN(&caller, -1, errno, 0);

      status = llinkret(&caller);
      assert(status != -1);

      return ;
    }

  if (__cpu_simple_lock_try(&lock) == 0)
    {
      SET_SYS_RETURN(&caller, -1, EDEADLK, 0);

      status = llinkret(&caller);
      assert(status != -1);

      return ;
    }

  TRACE_IN("dmasrv", &caller);

  switch (caller.tss.eax)
    {
    case SYSDMA_REGISTER:
      {
	sys_dma_register(&caller);
	break ;
      }
    case SYSDMA_UNREGISTER:
      {
	sys_dma_unregister(&caller);
	break ;
      }
    case SYSDMA_RESET:
      {
	sys_dma_reset(&caller);
	break ;
      }
    case SYSDMA_DUMP:
      {
	sys_dma_dump(&caller);
	break ;
      }
    default:
      {
        SET_SYS_RETURN(&caller, -1, ENOSYS, 0);

        break ;
      }
    }

  TRACE_OUT("dmasrv", &caller);

  __cpu_simple_unlock(&lock);

  /*
   * update caller tcb
   */
  status = llinkret(&caller);
  assert(status != -1);

}

void    dmasrv_syscall()
{
  TASKGATE_ENTER;
  do_dmasrv_syscall();
  TASKGATE_LEAVE;
}

The two above functions are very important: they are the entry point of the syscall itself. We split them in two for convenience: the second one just use the mandatory macros TASKGATE_ENTER and TASKGATE_LEAVE as the first one fetch the caller context, lock the operations, call the wanted syscall routines and return the status to caller.

void	create_dmasrv()
{
  int			i;
  union descriptor	idt;
  t_tcb			dmasrv_tcb;
  t_thread_status	thread_status;

  /**
   ** allocate a task for dma server
   **/
  if ((thread_status =
       thread_create("dmasrv", -1, (u_int32_t)dmasrv_syscall,
		     1, CPU_ANY, RING3,
		     THREADOPT_SERVICE|
		     THREADOPT_IOMAP|THREADOPT_HEAPSTK,
		     NULL, 0,
		     &dmasrv_tcb)) != THREAD_EZERO)
    xdperror("thread_create");

The thread_create() routine is used for creating the syscall. Please note the THREADOPT_SERVICE option. Note a driver can create as many services it wants and also can create schedulable threads for deferred function calls.
  
  for (i = 0;i < MAXDMA;i+=4)
    {
      if (ioacquire(dmasrv_tcb.pid, mask_reg[i]) == -1)
	{
	  dprintf("ioacquire 0x%x: %s\n",
		 mask_reg[i], strerror(errno));
	  dexit(1);
	}
      if (ioacquire(dmasrv_tcb.pid, mode_reg[i]) == -1)
	{
	  dprintf("ioacquire 0x%x: %s\n",
		 mode_reg[i], strerror(errno));
	  dexit(1);
	}
      if (ioacquire(dmasrv_tcb.pid, clear_reg[i]) == -1)
	{
	  dprintf("ioacquire 0x%x: %s\n", 
		 clear_reg[i], strerror(errno));
	  dexit(1);
	}
    }
  
  for (i = 0;i < MAXDMA;i++)
    {
      if (ioacquire(dmasrv_tcb.pid, page_port[i]) == -1)
	{
	  dprintf("ioacquire 0x%x: %s\n",
		 page_port[i], strerror(errno));
	  dexit(1);
	}
      if (ioacquire(dmasrv_tcb.pid, addr_port[i]) == -1)
	{
	  dprintf("ioacquire 0x%x: %s\n",
		 addr_port[i], strerror(errno));
	  dexit(1);
	}
      if (ioacquire(dmasrv_tcb.pid, count_port[i]) == -1)
	{
	  dprintf("ioacquire 0x%x: %s\n",
		 count_port[i], strerror(errno));
	  dexit(1);
	}
    }

The ioacquire() routines allow the task to use the specific ioports. If this is not done, the processor will refuse the task to use the ioports.

  /*
   * install dmasrv syscall
   */
  settaskgate(&idt.gd, dmasrv_tcb.sel, 0, SDT_SYSTASKGT, RING3);
  
  if (srvreg(0, GATE_DMA, &idt) == -1)
    {
      dprintf("pb registering dmasrv syscall in idt %d\n", errno);
      dexit(1);
    }

The previous lines define and install the service.

}

int	main(int argc, char **argv)
{
  int	i;

  __cpu_simple_lock_init(&lock);
  
  for (i = 0;i < MAXDMA;i++)
    dmas[i].avail = 1;

  /*
   * create syscall
   */
  create_dmasrv();

  return (0); /* make compiler happy */
}
The main entry point of the driver. Note that the main thread dies but the service is still "alive" and waits for user requests. This mechanism is also described here.

libdma.h

#ifndef __LIBDMA_H__
#define __LIBDMA_H__	1
#include 

#define SYSDMA_REGISTER		1
#define DMAOPT_COPY	(1<<0u)		/* copy buffer		*/
#define DMAOPT_READ	(1<<1u)		/* dma read		*/
#define DMAOPT_WRITE	(1<<2u)		/* dma write		*/

#define SYSDMA_UNREGISTER	2
#define SYSDMA_RESET		3

#define SYSDMA_DUMP		5

/*
 * suberrno codes (advice)
 */
typedef enum
  {
    DMA_REGISTER_BAD_CHANNEL = 100,
    DMA_REGISTER_CHANNEL_NOT_AVAIL,
    DMA_REGISTER_BAD_LEN,
    DMA_REGISTER_NOT_SHARED_RW,
    DMA_REGISTER_BAD_DMA_BLOCK,
    DMA_REGISTER_NOT_DMA_MEM,
  } t_dma_register_suberrno_codes;

/* PROTO libdma.c */
/* libdma.c */
int dma_register(int channel, char *dmabuf, int dmaoff, 
	int dmalen, u_int32_t dmaopt);
int dma_write(int channel, char *buf, int len);
int dma_read(int channel, char *buf, int len);
int dma_unregister(int channel);
int dma_reset(int channel, int dmaoff, int dmalen);
int dma_dump(void);
#endif

This is the client side header that define the syscall number, the syscall options, types and the error codes.

libdma.c

#include "libdma.h"

int	dma_register(int	channel,
		     char	*dmabuf,
		     int	dmaoff,
		     int	dmalen,
		     u_int32_t	dmaopt)
{
  return (gate_syscall(GATE_DMA, 
			  SYSDMA_REGISTER,
			  channel, (int)dmabuf,
			  dmaoff, dmalen, dmaopt));
}

int	dma_write(int channel, char *buf, int len)
{
  return (dma_register(channel, buf, 0, len, DMAOPT_COPY|DMAOPT_WRITE));
}

int	dma_read(int channel, char *buf, int len)
{
  return (dma_register(channel, buf, 0, len, DMAOPT_COPY|DMAOPT_READ));
}	

int	dma_unregister(int channel)
{
  return (gate_syscall(GATE_DMA, SYSDMA_UNREGISTER, channel,
			  0, 0, 0, 0));
}

int	dma_reset(int channel, int dmaoff, int dmalen)
{
  return (gate_syscall(GATE_DMA, SYSDMA_RESET, channel, dmaoff, dmalen,
			  0, 0));
}

int	dma_dump()
{
  return (gate_syscall(GATE_DMA, SYSDMA_DUMP, 0, 0, 0, 0, 0));
}

Please note the particular usage of the gate_syscall() function.