How to map device addresses into user space

Dr. Rolf Jansen rj at cyclaero.com
Mon Jan 7 23:40:19 UTC 2013


Am 07.01.2013 um 17:21 schrieb John Baldwin:

> On Monday, January 07, 2013 12:25:33 PM Dr. Rolf Jansen wrote:
>> Am 07.01.2013 um 13:11 schrieb John Baldwin:
>>> On Monday, January 07, 2013 08:52:01 AM Dr. Rolf Jansen wrote:
>>>> Am 03.01.2013 um 14:45 schrieb Dr. Rolf Jansen:
>>>>> ...
>>>>> 
>>>>> So, how can I map device addresses into user space on FreeBSD?
>>>> 
>>>> Many Thanks to everybody, who responded.
>>>> 
>>>> I got it working. I wrote a PCI device driver, which in addition to
>>>> open/close/read/write responds also to ioctl and mmap requests from user
>>>> space. In its pci_attach routine, it assembles a custom address_space
>>>> struct containing the addresses of the two BAR's of my NI PCI-DAQ board
>>>> and two huge allocated memory regions for DMA...
>>> 
>>> Note that if your BARs are not contiguous you can allow mapping of things
>>> in the middle.  This is a potentital security hole if your driver can be
>>> opened by non-root.
>> 
>> I can understand this. Actually, on the present model of the DAQ board, the
>> BARs are contiguous, and in the present incarnation, my driver can only be
>> addressed by root. Anyway, I want to do it correctly, that means, I would
>> like to avoid right now potential holes bubbling up in the future, when
>> using my driver with other DAQ boards, and in different usage scenarios.
> 
> Note that the BARs being contiguous is a property of your BIOS or OS, not your 
> board. :)  The firmware/OS are free to assign whatever ranges to your BARs 
> that they wish.
> 
>>> A more robust approach is to define a virtual address space for
>>> your device. This is also a lot easier to program against in userland.
>>> For example, you could define offset 0 as mapping to BAR 0 and the next
>>> chunk
>> 
>>> of virtual address space (the offset) map to BAR 1, etc.:
>> I did this, but id didn't work out. The 2 BARs are contiguous, and the 2
>> DMA regions are also, but there is huge gap between BAR and DMA. And
>> depending on which mapping is done first, either the DMAs or the BARs
>> receive invalid paddr. So, I guess, defining a virtual address space is a
>> little bit more involved than simply getting the offsets straight as you
>> lined out.
>> 
>> I am still learning things, so please bear with me, if it is a dumb
>> question. How do I define a virtual address space for the device?
> 
> Ah, I'll explain a bit.  When you mmap a section of a file, the OS is treating 
> the file as if it were its own section of virtual memory, where files contents 
> were addressed by the offset.  That is, virtual address 0 of the file 
> corresponds to the data at file offset 0, and virtual address N corresponds to 
> the data at file offset N.
> 
> Now, think of your /dev/foo device as a file.  When you mmap() your /dev/foo, 
> the OS has assumes it has a similar virtual memory that is addressed by the 
> offset into the file.  However, for a character device, the OS needs the 
> driver to describe this virtual address space.  That is what you are doing 
> with d_mmap() and the 'offset' parameter.  You are telling it what physical 
> page backs each "virtual page".  It is important to note that when using 
> d_mmap(), these mappings are immutable.  Once you provide a mapping for a 
> given virtual page, the OS will cache it forever (or until reboot).
> 
> In other words, you can write your d_mmap() to give the appearance that your 
> /dev/foo "file" contains BAR 0 followed by BAR 1, etc.
> 
>>> static int
>>> mydaq_mmap(...)
>>> {
>>> 
>>> 	struct mydaq_softc *sc;  // you should be storing things like the
>>> 	pointers
>>> 	
>>>                            // to your BARs in a softc
>>> 	
>>> 	sc = dev->si_drv1;
>>> 	if (offset <= rman_get_size(sc->bar0res))
>>> 	
>>> 		return (rman_get_start(sc->bar0res) + offset);
>>> 	
>>> 	offset -= rman_get_size(sc->bar0res);
>>> 	if (offset <= rman_get_size(sc->bar1res))
>>> 	
>>> 		return (rman_get_start(sc->bar1res) + offset);
>>> 	
>>> 	offset -= rman_get_size(sc->bar1res);
>>> 	if (offset <= dmaAIBufSize)
>>> 	
>>> 		return (vtophys(sc->dma[0]) + offset);
>>> 	
>>> 	offset -= dmaAIBufSize;
>>> 	if (offset <= dmaAOBufSize)
>>> 	
>>> 		return (vtophys(sc->dma[1]) + offset);
>>> 	
>>> 	/* Invalid offset */
>>> 	return (-1);
>>> 
>>> }
>> 
>> I guess, you hacked this quickly together, and I picked the concept, so
>> this is fine. In order to make this working, the addresses should be
>> assigned to the *paddr parameter, and a status code should be returned. In
>> order to check things, of course I applied these minor corrections, but it
>> doesn't work for either the BAR block or the DMA block, depending on which
>> is mapped first.
> 
> Yes, it needs the changes you made.  Can you post your updated d_mmap()
> routine along with how you are mapping it in userland?  With what I posted
> above you would do something like this in userland:
> 
> const char *bar0, *bar1;
> 
> int fd = open("/dev/foo");
> 
> bar0 = mmap(NULL, <size of BAR 0>, PROT_READ, MAP_SHARED, fd, 0);
> bar1 = mmap(NULL, <size of BAR 1>, PROT_READ, MAP_SHARED, fd,
>    <size of BAR 0>);
> 
> etc.


Yes, now I understood it. I realized, that mydaq_mmap() is not called only once
for each mapping, but once for each page in each mapping. And finally I got it
working. Besides the corrections already mentioned, the comparison operators
needed to be changed from "<=" to "<". The working d_mmap() is now:


static int mydaq_mmap(struct cdev *dev, vm_ooffset_t offset, vm_paddr_t *paddr,
                      int nprot, vm_memattr_t *memattr)
{
   mydaq_pci_softc *sc = dev->si_drv1;

   if (offset < rman_get_size(sc->bar0res))
   {
      *paddr = rman_get_start(sc->bar0res) + offset;
      return 0;
   }

   offset -= rman_get_size(sc->bar0res);
   if (offset < rman_get_size(sc->bar1res))
   {
      *paddr = rman_get_start(sc->bar1res) + offset;
      return 0;
   }

   offset -= rman_get_size(sc->bar1res);
   if (offset < dmaAIBufferSize)
   {
      *paddr = vtophys(sc->dma[0]) + offset;
      return 0;
   }

   offset -= dmaAIBufferSize;
   if (offset < dmaAOBufferSize)
   {
      *paddr = vtophys(sc->dma[1]) + offset;
      return 0;
   }

   return -1;
}


Thank you very much again, for the big help.

Best regards

Rolf


More information about the freebsd-drivers mailing list