|
|
|
/* R E S O U R C E M A N A G E R - An FPGA driver */
|
|
|
|
/* Project Name: "fpga" */
|
|
|
|
/* What is a Resource Manager under QNX Neutrino?
|
|
* A resource manager is a superset of a device driver. The QNX
|
|
* resource manager framework is used to create the POSIX
|
|
* interface (open, read, write, etc.) for any resource you
|
|
* can think of. Imagine you are coding a device driver for
|
|
* a device that reads credit cards. Your application would then
|
|
* be able to just use open() to access the card, read() to retrieve
|
|
* data from the card, and write() to store data on the card.
|
|
* If later your hardware changes, and you don't need to read
|
|
* credit cards any more but are reading serial data from some
|
|
* field bus instead, you replace/update the resource manager
|
|
* while your application still just uses open(), read() and write()!
|
|
*/
|
|
|
|
/*
|
|
* This project is an fpga driver. It provides a means of programming
|
|
* the fpga and displaying the status of the fpga.
|
|
*
|
|
* Start the driver as:
|
|
* fpga-variant -b 0x01e26000 -f 0x66000000 &
|
|
*
|
|
* It will create the following devices:
|
|
* /dev/fpga/cmd Writes to this device perform different commands
|
|
* 1 Assert FPGA reset
|
|
* 2 Initiate programming cycle
|
|
* 3 Check if programming worked (enumerate cores?)
|
|
* /dev/fpga/image Writes to this device send data to the fpga when being programmed
|
|
* /dev/fpga/state Reads from this device give current state of fpga. Possible states
|
|
* UNKNOWN fpga might or might not be programmed
|
|
* RESET fpga is being reset
|
|
* PROGRAM_FAIL an attempt was made to program the fpga and it failed
|
|
* PROGRAMMING fpga is currently being programmed
|
|
* PROGRAMMED fpga has been successfully programmed
|
|
* /dev/fpga/version Reads from this device give version numbers when device is programmed
|
|
* /dev/fpga/int_status Reads from this device give interrupt information (not implemented now)
|
|
*/
|
|
|
|
#include <errno.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
struct fpga_attr; // This overrides the default structure so we can add our own fields
|
|
#define IOFUNC_ATTR_T struct fpga_attr
|
|
#include <sys/iofunc.h>
|
|
#include <sys/dispatch.h>
|
|
#include <sys/neutrino.h>
|
|
#include <sys/resmgr.h>
|
|
#include <sys/mman.h>
|
|
#include <hw/inout.h>
|
|
#define FPGA_CTRL_C
|
|
#include "core_ids.h"
|
|
|
|
/*
|
|
* Enumeration for different directions of pins
|
|
*/
|
|
enum gpio_direct { GPIO_IN, GPIO_OUT };
|
|
typedef enum gpio_direct gpio_direct_t;
|
|
|
|
/*
|
|
* Our attribute structure. This includes the default structure
|
|
* at the front and then specifies our own added fields.
|
|
*/
|
|
#define PAGE_SIZE 4096
|
|
typedef struct fpga_attr fpga_attr_t;
|
|
struct fpga_attr
|
|
{
|
|
iofunc_attr_t attr; /* must be first, this is the regular structure */
|
|
// unsigned bank; /* bank of the pin */
|
|
// unsigned pin; /* pin number within the bank (0-15) */
|
|
// gpio_direct_t direction; /* direction of pin */
|
|
// unsigned init_value; /* initial value if direction is GPIO_OUT */
|
|
char buffer[PAGE_SIZE]; /* buffer for values being read */
|
|
int id; /* resmgr id associated with the file, filled in after attach */
|
|
// gpio_attr_t* next; /* pointer to next gpio_attr structure */
|
|
// const char * name; /* pointer to name for the device */
|
|
};
|
|
|
|
/*
|
|
* defines to specify register offsets within each bank.
|
|
*/
|
|
#define GPIO_DIR (0x00)
|
|
#define GPIO_OUT_DATA (0x04)
|
|
#define GPIO_SET_DATA (0x08)
|
|
#define GPIO_CLR_DATA (0x0C)
|
|
#define GPIO_IN_DATA (0x10)
|
|
#define GPIO_SET_RIS_TRIG (0x14)
|
|
#define GPIO_CLR_RIS_TRIG (0x18)
|
|
#define GPIO_SET_FAL_TRIG (0x1C)
|
|
#define GPIO_CLR_FAL_TRIG (0x20)
|
|
#define GPIO_INTSTAT (0x24)
|
|
#define BANKSIZE (0x28)
|
|
#define NUM_BANKS (9)
|
|
|
|
/*
|
|
* Macros to help calculate the bank offset from the base address
|
|
* and to generate the pin-mask for each pin.
|
|
*
|
|
* Note that each bank of registers actually has 2 banks of pins.
|
|
* Each bank of pins has 16 pins. The even number banks are in the
|
|
* low 16 bits and the odd number banks are in the high 16 bits.
|
|
*/
|
|
#define BANKOFF(bank) (0x10 + ((bank)>>1) * BANKSIZE)
|
|
#define PINMASK(bank,pin) (1 << ((pin) + (((bank)&1)? 16 : 0 ) ) )
|
|
|
|
static uintptr_t gpio_base = 0x01e26000; /* base address of the gpio peripheral */
|
|
static uintptr_t gpio_vbase; /* mmap version of base */
|
|
|
|
#define FPGA_BASE_ADDR 0x66000000
|
|
#define FPGA_CORE_SIZE 0x80
|
|
#define FPGA_MAX_CORES 32
|
|
#define FPGA_BASEMODULE_OFFSET 0
|
|
|
|
static uintptr_t fpga_base = FPGA_BASE_ADDR; /* base address of the FPGA */
|
|
static uintptr_t fpga_vbase; /* mmap version of fpga base */
|
|
|
|
#define GPIO_TO_PIN( bank, pinno ) (((bank)<<16)|(pinno))
|
|
#define PIN_TO_BANK(gpiopin) (((gpiopin)>>16)&0xff)
|
|
#define PIN_TO_PIN(gpiopin) ((gpiopin)&0xff)
|
|
#define FPGA_PROGRAM GPIO_TO_PIN(6,15)
|
|
#define FPGA_INIT GPIO_TO_PIN(1,15)
|
|
#define FPGA_RDWR GPIO_TO_PIN(3,9)
|
|
#define FPGA_INT0 GPIO_TO_PIN(6,12)
|
|
#define FPGA_INT1 GPIO_TO_PIN(6,13)
|
|
|
|
#define FPGA_STATE_UNKNOWN 0 /* FPGA is not in a known state */
|
|
#define FPGA_STATE_RESET 1 /* FPGA has been reset, but we aren't programming */
|
|
#define FPGA_STATE_PROGRAMMING 2 /* FPGA is being programmed */
|
|
#define FPGA_STATE_PROGRAM_FAIL 3 /* FPGA failed programming */
|
|
#define FPGA_STATE_PROGRAMMED 4 /* FPGA has been programmed */
|
|
|
|
#define FPGA_CMD_RESET 1 /* Assert FPGA reset */
|
|
#define FPGA_CMD_PROGRAM 2 /* Initiate programming cycle */
|
|
#define FPGA_CMD_FINISHPROGRAM 3 /* Check if programming worked, then enumerate cores */
|
|
|
|
|
|
/**
|
|
* Core Version Register, FIFO_no = 0
|
|
*/
|
|
typedef union corever0 {
|
|
struct bits0 {
|
|
unsigned int core_id : 8; /* Core ID 0xF0-0xFF are reserved for customers */
|
|
unsigned int vector : 4; /* interrupt vector level */
|
|
unsigned int level : 2; /* interrupt level */
|
|
unsigned int FIFO_no : 2; /* = 00 */
|
|
} bits;
|
|
uint16_t word;
|
|
} corever0;
|
|
|
|
/**
|
|
* Core Version Register, FIFO_no = 1
|
|
*/
|
|
typedef union corever1 {
|
|
struct bits1 {
|
|
unsigned int minor : 4; /* minor revision */
|
|
unsigned int major : 4; /* major revision */
|
|
unsigned int year : 5; /* years since 2000 */
|
|
unsigned int reserved : 1; /* not used */
|
|
unsigned int FIFO_no : 2; /* = 01 */
|
|
} bits;
|
|
uint16_t word;
|
|
} corever1;
|
|
|
|
/**
|
|
* Core Version Register, FIFO_no = 2
|
|
*/
|
|
typedef union corever2 {
|
|
struct bits2 {
|
|
unsigned int day : 5; /* minor revision */
|
|
unsigned int reserved : 3; /* not used */
|
|
unsigned int month : 4; /* major revision */
|
|
unsigned int reserved1 : 2; /* not used */
|
|
unsigned int FIFO_no : 2; /* = 10 */
|
|
} bits;
|
|
uint16_t word;
|
|
} corever2;
|
|
/**
|
|
* Core Version Register, FIFO_no = 3
|
|
*/
|
|
typedef union corever3 {
|
|
struct bits3 {
|
|
unsigned int reserved1 : 14; /* not used */
|
|
unsigned int FIFO_no : 2; /* = 10 */
|
|
} bits;
|
|
uint16_t word;
|
|
} corever3;
|
|
|
|
/**
|
|
* This structure holds the FPGA core version information.
|
|
*/
|
|
struct coreversion {
|
|
corever0 ver0;
|
|
corever1 ver1;
|
|
corever2 ver2;
|
|
corever3 ver3;
|
|
};
|
|
|
|
struct fpga_ctrl
|
|
{
|
|
unsigned int state;
|
|
uintptr_t vbaseaddr;
|
|
uintptr_t baseaddr;
|
|
struct coreversion bm_version;
|
|
struct coreversion app_version;
|
|
} fpga_ctrl = {
|
|
.state = FPGA_STATE_UNKNOWN
|
|
};
|
|
static struct fpga_ctrl *fpgactrl = &fpga_ctrl;
|
|
|
|
|
|
/*
|
|
* options processing
|
|
*
|
|
* This routine handles the command-line options.
|
|
* -v verbose operation
|
|
* -b addr gpio base address (default is 0x01e26000)
|
|
* -f addr fpga base address (default is 0x66000000)
|
|
*/
|
|
static int optv; // -v for verbose operation
|
|
#if NEVER
|
|
enum gpio_pinoptions { IN, OUT, END };
|
|
static char * gpio_pinopts[] =
|
|
{
|
|
[IN] = "in",
|
|
[OUT] = "out",
|
|
[END] = NULL
|
|
};
|
|
#endif
|
|
static void
|
|
options (int argc, char **argv)
|
|
{
|
|
int opt;
|
|
optv = 0;
|
|
|
|
while (optind < argc) {
|
|
while ((opt = getopt (argc, argv, "vb:f:")) != -1) {
|
|
switch (opt) {
|
|
case 'v':
|
|
optv = 1;
|
|
break;
|
|
case 'b':
|
|
gpio_base = strtoul( optarg, 0, 0 );
|
|
break;
|
|
case 'f':
|
|
fpga_base = strtoul( optarg, 0, 0 );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* A resource manager mainly consists of callbacks for POSIX
|
|
* functions a client could call. In the example, we have
|
|
* callbacks for the open(), read() and write() calls. More are
|
|
* possible. If we don't supply own functions (e.g. for stat(),
|
|
* seek(), etc.), the resource manager framework will use default
|
|
* system functions, which in most cases return with an error
|
|
* code to indicate that this resource manager doesn't support
|
|
* this function.*/
|
|
|
|
/* These prototypes are needed since we are using their names
|
|
* in main(). */
|
|
|
|
//static int io_read (resmgr_context_t *ctp, io_read_t *msg, RESMGR_OCB_T *ocb);
|
|
static int io_cmd_write (resmgr_context_t *ctp, io_write_t *msg, RESMGR_OCB_T *ocb);
|
|
static int io_image_write (resmgr_context_t *ctp, io_write_t *msg, RESMGR_OCB_T *ocb);
|
|
//static int io_version_read (resmgr_context_t *ctp, io_read_t *msg, RESMGR_OCB_T *ocb);
|
|
static int io_state_read (resmgr_context_t *ctp, io_read_t *msg, RESMGR_OCB_T *ocb);
|
|
static int io_devices_read (resmgr_context_t *ctp, io_read_t *msg, RESMGR_OCB_T *ocb);
|
|
|
|
/*
|
|
* Attribute structures for each of the files to be created
|
|
*/
|
|
static IOFUNC_ATTR_T attr_cmd;
|
|
static IOFUNC_ATTR_T attr_image;
|
|
static IOFUNC_ATTR_T attr_version;
|
|
static IOFUNC_ATTR_T attr_state;
|
|
static IOFUNC_ATTR_T attr_devices;
|
|
|
|
/*
|
|
* Our connect and I/O functions - we supply two tables
|
|
* which will be filled with pointers to callback functions
|
|
* for each POSIX function. The connect functions are all
|
|
* functions that take a path, e.g. open(), while the I/O
|
|
* functions are those functions that are used with a file
|
|
* descriptor (fd), e.g. read().
|
|
*/
|
|
|
|
static resmgr_connect_funcs_t connect_funcs;
|
|
static resmgr_io_funcs_t io_cmd_funcs;
|
|
static resmgr_io_funcs_t io_image_funcs;
|
|
static resmgr_io_funcs_t io_version_funcs;
|
|
static resmgr_io_funcs_t io_state_funcs;
|
|
static resmgr_io_funcs_t io_devices_funcs;
|
|
|
|
/*
|
|
* Our dispatch, resource manager, and iofunc variables
|
|
* are declared here. These are some small administrative things
|
|
* for our resource manager.
|
|
*/
|
|
|
|
static dispatch_t *dpp;
|
|
static resmgr_attr_t rattr;
|
|
static dispatch_context_t *ctp;
|
|
|
|
|
|
static char *progname = "fpga";
|
|
|
|
void attachFile( IOFUNC_ATTR_T *pAttr, const char *name, mode_t mode, resmgr_io_funcs_t *pio_funcs )
|
|
{
|
|
iofunc_attr_init (&pAttr->attr, 0660, NULL, NULL);
|
|
pAttr->attr.nbytes=sizeof(pAttr->buffer); /* we have a buffer size of 128 byte */
|
|
|
|
pAttr -> id = resmgr_attach (dpp, &rattr, name,
|
|
_FTYPE_ANY, 0,
|
|
&connect_funcs,
|
|
pio_funcs,
|
|
pAttr);
|
|
if (pAttr -> id == -1) {
|
|
fprintf (stderr, "%s: couldn't attach pathname(%s): %s\n",
|
|
progname, name, strerror (errno));
|
|
exit (1);
|
|
}
|
|
}
|
|
void gpio_init( unsigned gpiopin, gpio_direct_t direction, unsigned init_value )
|
|
{
|
|
unsigned bankoff = BANKOFF(PIN_TO_BANK(gpiopin));
|
|
unsigned reg_direction = in32( gpio_vbase+bankoff+GPIO_DIR );
|
|
unsigned pinmask = PINMASK( PIN_TO_BANK(gpiopin), PIN_TO_PIN(gpiopin) );
|
|
if( direction == GPIO_OUT )
|
|
{
|
|
/*
|
|
* Turn off pin position for output
|
|
*/
|
|
reg_direction &= ~(pinmask) ;
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
* Turn on pin position for input
|
|
*/
|
|
reg_direction |= pinmask;
|
|
}
|
|
/*
|
|
* No interrupts for rising or falling
|
|
*/
|
|
out32( gpio_vbase + bankoff + GPIO_CLR_RIS_TRIG, pinmask );
|
|
out32( gpio_vbase + bankoff + GPIO_CLR_FAL_TRIG, pinmask );
|
|
/*
|
|
* Write the new direction register with this pin set properly
|
|
* Leave the other pin positions the same
|
|
*/
|
|
out32( gpio_vbase + bankoff + GPIO_DIR, reg_direction );
|
|
|
|
if( direction == GPIO_OUT )
|
|
{
|
|
/*
|
|
* Set the initial value. For the initial value,
|
|
* binary zero says to clear the value, non-zero
|
|
* says to set the value.
|
|
*/
|
|
if( init_value )
|
|
{
|
|
out32( gpio_vbase + bankoff + GPIO_SET_DATA, pinmask );
|
|
}
|
|
else
|
|
{
|
|
out32( gpio_vbase + bankoff + GPIO_CLR_DATA, pinmask );
|
|
}
|
|
}
|
|
}
|
|
void gpio_direction_output( unsigned int pin, unsigned value )
|
|
{
|
|
unsigned bankoff = BANKOFF( PIN_TO_BANK(pin) );
|
|
unsigned pinmask = PINMASK( PIN_TO_BANK(pin), PIN_TO_PIN(pin) );
|
|
out32( gpio_vbase + bankoff + (value == 1 ? GPIO_SET_DATA : GPIO_CLR_DATA), pinmask );
|
|
}
|
|
void setFPGAState( unsigned state )
|
|
{
|
|
char *state_name;
|
|
fpgactrl->state = state;
|
|
|
|
switch( fpgactrl->state )
|
|
{
|
|
default:
|
|
case FPGA_STATE_UNKNOWN: state_name = "UNKNOWN"; break;
|
|
case FPGA_STATE_RESET: state_name = "RESET"; break;
|
|
case FPGA_STATE_PROGRAMMING: state_name = "PROGRAMMING"; break;
|
|
case FPGA_STATE_PROGRAM_FAIL: state_name = "PROGRAM_FAIL"; break;
|
|
case FPGA_STATE_PROGRAMMED: state_name = "PROGRAMMED"; break;
|
|
}
|
|
attr_state.attr.nbytes = snprintf( attr_state.buffer, PAGE_SIZE, state_name );
|
|
}
|
|
void setVersion()
|
|
{
|
|
int rv = 0;
|
|
char *buf = &attr_version.buffer[0];
|
|
switch( fpgactrl->state )
|
|
{
|
|
default:
|
|
case FPGA_STATE_UNKNOWN:
|
|
case FPGA_STATE_RESET:
|
|
case FPGA_STATE_PROGRAMMING:
|
|
case FPGA_STATE_PROGRAM_FAIL:
|
|
rv += snprintf( &buf[rv], PAGE_SIZE-rv, "NOT_PROGRAMMED" );
|
|
break;
|
|
case FPGA_STATE_PROGRAMMED:
|
|
rv += snprintf(&buf[rv], PAGE_SIZE-rv, "PROGRAMMED\n");
|
|
rv += snprintf(&buf[rv], PAGE_SIZE-rv, "FPGA Version : %02d.%02d\n",
|
|
fpgactrl->app_version.ver1.bits.major, fpgactrl->app_version.ver1.bits.minor);
|
|
rv += snprintf(&buf[rv], PAGE_SIZE-rv, "FPGA Date : %04d-%02d-%02d\n",
|
|
fpgactrl->app_version.ver1.bits.year,
|
|
fpgactrl->app_version.ver2.bits.month,
|
|
fpgactrl->app_version.ver2.bits.day);
|
|
|
|
rv += snprintf(&buf[rv], PAGE_SIZE-rv, "Base Module Version : %02d.%02d\n",
|
|
fpgactrl->bm_version.ver1.bits.major,
|
|
fpgactrl->bm_version.ver1.bits.minor);
|
|
rv += snprintf(&buf[rv], PAGE_SIZE-rv, "Base Module Date : %04d-%02d-%02d\n",
|
|
fpgactrl->bm_version.ver1.bits.year,
|
|
fpgactrl->bm_version.ver2.bits.month,
|
|
fpgactrl->bm_version.ver2.bits.day);
|
|
break;
|
|
}
|
|
attr_version.attr.nbytes = rv;
|
|
}
|
|
/**
|
|
* Reads the core version information out of a spot in the
|
|
* FPGA.
|
|
*
|
|
* \param[in] baseaddr location of the core version register
|
|
* \param[in] pdata location to store the core version data
|
|
*
|
|
* \return non-zero if the core data is invalid
|
|
*/
|
|
int read_core_version(uintptr_t baseaddr, struct coreversion* pdata)
|
|
{
|
|
int i;
|
|
corever0 ver;
|
|
int found = 0;
|
|
int rv = -1;
|
|
|
|
for (i = 0; i < 4; i++)
|
|
{
|
|
ver.word = in16(baseaddr);
|
|
switch(ver.bits.FIFO_no)
|
|
{
|
|
case 0:
|
|
pdata->ver0.word = ver.word;
|
|
break;
|
|
case 1:
|
|
pdata->ver1.word = ver.word;
|
|
break;
|
|
case 2:
|
|
pdata->ver2.word = ver.word;
|
|
break;
|
|
case 3:
|
|
pdata->ver3.word = ver.word;
|
|
break;
|
|
}
|
|
found |= (1<<ver.bits.FIFO_no);
|
|
}
|
|
if (found == 0x0F)
|
|
rv = 0;
|
|
return rv;
|
|
}
|
|
int main (int argc, char **argv)
|
|
{
|
|
progname = argv[0];
|
|
|
|
/* Check for command line options (-v and pin specifications) */
|
|
options (argc, argv);
|
|
|
|
/* Allocate and initialize a dispatch structure for use
|
|
* by our main loop. This is for the resource manager
|
|
* framework to use. It will receive messages for us,
|
|
* analyze the message type integer and call the matching
|
|
* handler callback function (i.e. io_open, io_read, etc.) */
|
|
dpp = dispatch_create ();
|
|
if (dpp == NULL) {
|
|
fprintf (stderr, "%s: couldn't dispatch_create: %s\n",
|
|
argv[0], strerror (errno));
|
|
exit (1);
|
|
}
|
|
|
|
/* Set up the resource manager attributes structure. We'll
|
|
* use this as a way of passing information to
|
|
* resmgr_attach(). The attributes are used to specify
|
|
* the maximum message length to be received at once,
|
|
* and the number of message fragments (iov's) that
|
|
* are possible for the reply.
|
|
* For now, we'll just use defaults by setting the
|
|
* attribute structure to zeroes. */
|
|
memset (&rattr, 0, sizeof (rattr));
|
|
|
|
/* Now, let's initialize the tables of connect functions and
|
|
* I/O functions to their defaults (system fallback
|
|
* routines) and then override the defaults with the
|
|
* functions that we are providing. */
|
|
iofunc_func_init (_RESMGR_CONNECT_NFUNCS, &connect_funcs,
|
|
_RESMGR_IO_NFUNCS, &io_cmd_funcs);
|
|
iofunc_func_init (_RESMGR_CONNECT_NFUNCS, &connect_funcs,
|
|
_RESMGR_IO_NFUNCS, &io_image_funcs);
|
|
iofunc_func_init (_RESMGR_CONNECT_NFUNCS, &connect_funcs,
|
|
_RESMGR_IO_NFUNCS, &io_state_funcs);
|
|
iofunc_func_init (_RESMGR_CONNECT_NFUNCS, &connect_funcs,
|
|
_RESMGR_IO_NFUNCS, &io_version_funcs);
|
|
iofunc_func_init (_RESMGR_CONNECT_NFUNCS, &connect_funcs,
|
|
_RESMGR_IO_NFUNCS, &io_devices_funcs);
|
|
|
|
/* Now we override the default function pointers with
|
|
* some of our own coded functions: */
|
|
//connect_funcs.open = io_open;
|
|
//io_funcs.read = io_read;
|
|
//io_funcs.write = io_write;
|
|
|
|
/*
|
|
* Map the GPIO registers so we can access them.
|
|
*/
|
|
gpio_vbase = mmap_device_io( BANKSIZE*(NUM_BANKS+1)/2+0x10, gpio_base );
|
|
if( gpio_vbase == (uintptr_t)MAP_FAILED )
|
|
{
|
|
fprintf( stderr, "mmap_device_io gpio failed: errno = %d\n", errno );
|
|
exit(1);
|
|
}
|
|
|
|
/*
|
|
* Map the FPGA memory so we can access them.
|
|
*/
|
|
fpga_vbase = mmap_device_io( FPGA_CORE_SIZE, fpga_base );
|
|
if( fpga_vbase == (uintptr_t)MAP_FAILED )
|
|
{
|
|
fprintf( stderr, "mmap_device_io fpga failed: errno = %d\n", errno );
|
|
exit(1);
|
|
}
|
|
fpgactrl->vbaseaddr = fpga_vbase;
|
|
fpgactrl->baseaddr = fpga_base;
|
|
|
|
/*
|
|
* Create all the different resource files. (cmd, image, state, and version)
|
|
*/
|
|
io_cmd_funcs.write = io_cmd_write;
|
|
attachFile( &attr_cmd, "/dev/fpga/cmd", S_IFCHR | S_IWUSR | S_IWGRP | S_IWOTH, &io_cmd_funcs );
|
|
io_image_funcs.write = io_image_write;
|
|
attachFile( &attr_image, "/dev/fpga/image", S_IFCHR | S_IWUSR | S_IWGRP | S_IWOTH, &io_image_funcs );
|
|
io_state_funcs.read = io_state_read;
|
|
attachFile( &attr_state, "/dev/fpga/state", S_IFCHR | S_IRUSR | S_IRGRP | S_IROTH, &io_state_funcs );
|
|
io_version_funcs.read = io_state_read; // version and state share a read function
|
|
attachFile( &attr_version, "/dev/fpga/version", S_IFCHR | S_IRUSR | S_IRGRP | S_IROTH, &io_version_funcs );
|
|
io_devices_funcs.read = io_devices_read;
|
|
attachFile( &attr_devices, "/dev/fpga/devices", S_IFCHR | S_IRUSR | S_IRGRP | S_IROTH, &io_devices_funcs );
|
|
|
|
gpio_init( FPGA_PROGRAM, GPIO_OUT, 1 );
|
|
gpio_init( FPGA_INIT, GPIO_IN, 0 );
|
|
gpio_init( FPGA_RDWR, GPIO_OUT, 1 );
|
|
gpio_init( FPGA_INT0, GPIO_IN, 0 );
|
|
gpio_init( FPGA_INT1, GPIO_IN, 0 );
|
|
setFPGAState( FPGA_STATE_UNKNOWN );
|
|
setVersion();
|
|
|
|
#if NEVER
|
|
if( gpio_attr_head )
|
|
{
|
|
gpio_attr_t *pAttr;
|
|
|
|
for( pAttr = gpio_attr_head; pAttr; pAttr = pAttr -> next )
|
|
{
|
|
unsigned bankoff = BANKOFF(pAttr -> bank);
|
|
unsigned reg_direction = in32( gpio_vbase+bankoff+GPIO_DIR );
|
|
unsigned pinmask = PINMASK( pAttr -> bank, pAttr -> pin );
|
|
char devname[PATH_MAX+1];
|
|
if( pAttr -> direction == GPIO_OUT )
|
|
{
|
|
/*
|
|
* Turn off pin position for output
|
|
*/
|
|
reg_direction &= ~(pinmask) ;
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
* Turn on pin position for input
|
|
*/
|
|
reg_direction |= pinmask;
|
|
}
|
|
/*
|
|
* No interrupts for rising or falling
|
|
*/
|
|
out32( gpio_vbase + bankoff + GPIO_CLR_RIS_TRIG, pinmask );
|
|
out32( gpio_vbase + bankoff + GPIO_CLR_FAL_TRIG, pinmask );
|
|
/*
|
|
* Write the new direction register with this pin set properly
|
|
* Leave the other pin positions the same
|
|
*/
|
|
out32( gpio_vbase + bankoff + GPIO_DIR, reg_direction );
|
|
|
|
if( pAttr -> direction == GPIO_OUT )
|
|
{
|
|
/*
|
|
* Set the initial value. For the initial value,
|
|
* binary zero says to clear the value, non-zero
|
|
* says to set the value.
|
|
*/
|
|
if( pAttr ->init_value )
|
|
{
|
|
out32( gpio_vbase + bankoff + GPIO_SET_DATA, pinmask );
|
|
}
|
|
else
|
|
{
|
|
out32( gpio_vbase + bankoff + GPIO_CLR_DATA, pinmask );
|
|
}
|
|
}
|
|
|
|
iofunc_attr_init (&pAttr->attr, S_IFCHR | 0666, NULL, NULL);
|
|
pAttr->attr.nbytes=1; /* we have a buffer size of 1 byte */
|
|
|
|
/*
|
|
* Create the file name with 3 possibilities:
|
|
* No name provided: use /dev/gpBpP
|
|
* Name provided that starts with a /, use the provided name
|
|
* Name provided that is relative, use /dev/name
|
|
*/
|
|
if( pAttr->name == NULL )
|
|
{
|
|
snprintf(devname, PATH_MAX, "/dev/gp%dp%d", pAttr->bank, pAttr->pin);
|
|
}
|
|
else if( pAttr->name[0] == '/' )
|
|
{
|
|
snprintf(devname, PATH_MAX, "%s", pAttr->name );
|
|
}
|
|
else
|
|
{
|
|
snprintf(devname, PATH_MAX, "/dev/%s", pAttr->name );
|
|
}
|
|
|
|
pAttr -> id = resmgr_attach (dpp, &rattr, devname,
|
|
_FTYPE_ANY, 0,
|
|
&connect_funcs,
|
|
&io_funcs,
|
|
pAttr);
|
|
if (pAttr -> id == -1) {
|
|
fprintf (stderr, "%s: couldn't attach pathname: %s\n",
|
|
argv[0], strerror (errno));
|
|
exit (1);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
fprintf( stderr, "%s: no pins specified\n", argv[0] );
|
|
exit(1);
|
|
}
|
|
#endif
|
|
|
|
/* Now we allocate some memory for the dispatch context
|
|
* structure, which will later be used when we receive
|
|
* messages. */
|
|
ctp = dispatch_context_alloc (dpp);
|
|
|
|
/* Done! We can now go into our "receive loop" and wait
|
|
* for messages. The dispatch_block() function is calling
|
|
* MsgReceive() under the covers, and receives for us.
|
|
* The dispatch_handler() function analyzes the message
|
|
* for us and calls the appropriate callback function. */
|
|
while (1) {
|
|
if ((ctp = dispatch_block (ctp)) == NULL) {
|
|
fprintf (stderr, "%s: dispatch_block failed: %s\n",
|
|
argv[0], strerror (errno));
|
|
exit (1);
|
|
}
|
|
/* Call the correct callback function for the message
|
|
* received. This is a single-threaded resource manager,
|
|
* so the next request will be handled only when this
|
|
* call returns. Consult QNX documentation if you want
|
|
* to create a multi-threaded resource manager. */
|
|
dispatch_handler (ctp);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* io_read
|
|
*
|
|
* At this point, the client has called the library read()
|
|
* function, and expects zero or more bytes. If this is a read
|
|
* for exactly 1 byte, we will always return the current
|
|
* value for the pin. If this is a read for multiple bytes,
|
|
* then we use our "buffer" and return one byte and then
|
|
* on the next request for multiple bytes, we will return
|
|
* EOF. This allows repeated reads for polling and convenient
|
|
* access from the command line.
|
|
*/
|
|
|
|
|
|
/* The message that we received can be accessed via the
|
|
* pointer *msg. A pointer to the OCB that belongs to this
|
|
* read is the *ocb. The *ctp pointer points to a context
|
|
* structure that is used by the resource manager framework
|
|
* to determine whom to reply to, and more.
|
|
*/
|
|
|
|
static int
|
|
io_state_read (resmgr_context_t *ctp, io_read_t *msg, RESMGR_OCB_T *ocb)
|
|
{
|
|
int status;
|
|
int nparts;
|
|
int nbytes = 0;
|
|
int offset = ocb->offset;
|
|
|
|
if (optv) {
|
|
printf ("%s: in io_read, id=%d\n", progname, ctp->id);
|
|
}
|
|
|
|
/* Here we verify if the client has the access
|
|
* rights needed to read from our device */
|
|
if ((status = iofunc_read_verify(ctp, msg, ocb, NULL)) != EOK) {
|
|
return (status);
|
|
}
|
|
|
|
/* We check if our read callback was called because of
|
|
* a pread() or a normal read() call. If pread(), we return
|
|
* with an error code indicating that we don't support it.*/
|
|
if ((msg->i.xtype & _IO_XTYPE_MASK) != _IO_XTYPE_NONE) {
|
|
return (ENOSYS);
|
|
}
|
|
/*
|
|
* Get the number of bytes to read. If requested is >1, look
|
|
* at the current offset to see if we have a byte to return.
|
|
* If the requested number is exactly 1, then read 1 byte.
|
|
*/
|
|
if( msg -> i.nbytes > 0 )
|
|
{
|
|
int nleft;
|
|
nleft = ocb -> attr -> attr.nbytes - ocb -> offset;
|
|
nbytes = min( msg -> i.nbytes, nleft );
|
|
if( nleft )
|
|
ocb -> offset += nbytes;
|
|
}
|
|
|
|
if( nbytes > 0 )
|
|
{
|
|
/* Here we set the number of bytes we will return. */
|
|
_IO_SET_READ_NBYTES(ctp, nbytes);
|
|
|
|
/* The next line is used to tell the system how
|
|
* large your buffer is in which you want to return your
|
|
* data for the read() call.
|
|
*
|
|
*/
|
|
SETIOV( ctp->iov, &ocb -> attr -> buffer[offset], nbytes);
|
|
nparts = 1;
|
|
|
|
}
|
|
else
|
|
{
|
|
_IO_SET_READ_NBYTES(ctp, 0);
|
|
nparts = 0;
|
|
}
|
|
|
|
if (msg->i.nbytes > 0) {
|
|
ocb->attr->attr.flags |= IOFUNC_ATTR_ATIME;
|
|
}
|
|
|
|
/*
|
|
* Return the number of parts specified above (1 or 0).
|
|
*/
|
|
return (_RESMGR_NPARTS (nparts));
|
|
|
|
|
|
}
|
|
/**
|
|
* Retrieve human readable core description.
|
|
*
|
|
* \param[in] ID core number
|
|
* \return string description of core
|
|
*/
|
|
const char* CoreName(unsigned char ID)
|
|
{
|
|
int i = 0;
|
|
for (i = 0; i < ARRAY_SIZE(KnownCores); i++)
|
|
{
|
|
if (ID == KnownCores[i].ID)
|
|
{
|
|
return KnownCores[i].Name;
|
|
}
|
|
}
|
|
return "Unknown";
|
|
}
|
|
static int
|
|
io_devices_read (resmgr_context_t *ctp, io_read_t *msg, RESMGR_OCB_T *ocb)
|
|
{
|
|
int status;
|
|
int nparts;
|
|
int nbytes = 0;
|
|
int offset = ocb->offset;
|
|
int rv = 0;
|
|
char *buf = &ocb->attr->buffer[0];
|
|
|
|
if( offset == 0 )
|
|
{
|
|
rv += snprintf( &buf[rv], PAGE_SIZE-rv, "Enumerating Devices\n" );
|
|
int i;
|
|
struct coreversion cv;
|
|
|
|
for (i = 0; i < FPGA_MAX_CORES; i++)
|
|
{
|
|
uintptr_t vbaseaddr = (uintptr_t)((char*)(fpgactrl->vbaseaddr)+FPGA_CORE_SIZE*i);
|
|
uintptr_t baseaddr = (uintptr_t)((char*)(fpgactrl->baseaddr )+FPGA_CORE_SIZE*i);
|
|
if (0 == read_core_version(vbaseaddr,&cv))
|
|
{
|
|
//struct fpga_device* fpgadev;
|
|
//int ret;
|
|
|
|
rv += snprintf( &buf[rv], PAGE_SIZE-rv, "Found Device ID %02d-%s (%02d.%02d) at %08X\n",
|
|
cv.ver0.bits.core_id,
|
|
CoreName(cv.ver0.bits.core_id),
|
|
cv.ver1.bits.major, cv.ver1.bits.minor,baseaddr);
|
|
}
|
|
}
|
|
ocb->attr->attr.nbytes = rv;
|
|
}
|
|
if (optv) {
|
|
printf ("%s: in io_read, id=%d\n", progname, ctp->id);
|
|
}
|
|
|
|
/* Here we verify if the client has the access
|
|
* rights needed to read from our device */
|
|
if ((status = iofunc_read_verify(ctp, msg, ocb, NULL)) != EOK) {
|
|
return (status);
|
|
}
|
|
|
|
/* We check if our read callback was called because of
|
|
* a pread() or a normal read() call. If pread(), we return
|
|
* with an error code indicating that we don't support it.*/
|
|
if ((msg->i.xtype & _IO_XTYPE_MASK) != _IO_XTYPE_NONE) {
|
|
return (ENOSYS);
|
|
}
|
|
/*
|
|
* Get the number of bytes to read. If requested is >1, look
|
|
* at the current offset to see if we have a byte to return.
|
|
* If the requested number is exactly 1, then read 1 byte.
|
|
*/
|
|
if( msg -> i.nbytes > 0 )
|
|
{
|
|
int nleft;
|
|
nleft = ocb -> attr -> attr.nbytes - ocb -> offset;
|
|
nbytes = min( msg -> i.nbytes, nleft );
|
|
if( nleft )
|
|
ocb -> offset += nbytes;
|
|
}
|
|
|
|
if( nbytes > 0 )
|
|
{
|
|
/* Here we set the number of bytes we will return. */
|
|
_IO_SET_READ_NBYTES(ctp, nbytes);
|
|
|
|
/* The next line is used to tell the system how
|
|
* large your buffer is in which you want to return your
|
|
* data for the read() call.
|
|
*
|
|
*/
|
|
SETIOV( ctp->iov, &ocb -> attr -> buffer[offset], nbytes);
|
|
nparts = 1;
|
|
|
|
}
|
|
else
|
|
{
|
|
_IO_SET_READ_NBYTES(ctp, 0);
|
|
nparts = 0;
|
|
}
|
|
|
|
if (msg->i.nbytes > 0) {
|
|
ocb->attr->attr.flags |= IOFUNC_ATTR_ATIME;
|
|
}
|
|
|
|
/*
|
|
* Return the number of parts specified above (1 or 0).
|
|
*/
|
|
return (_RESMGR_NPARTS (nparts));
|
|
|
|
|
|
}
|
|
#if NEVER
|
|
static int
|
|
io_version_read (resmgr_context_t *ctp, io_read_t *msg, RESMGR_OCB_T *ocb)
|
|
{
|
|
int status;
|
|
int nparts;
|
|
int nbytes = 0;
|
|
int offset = ocb->offset;
|
|
|
|
if (optv) {
|
|
printf ("%s: in io_read, id=%d\n", progname, ctp->id);
|
|
}
|
|
|
|
/* Here we verify if the client has the access
|
|
* rights needed to read from our device */
|
|
if ((status = iofunc_read_verify(ctp, msg, ocb, NULL)) != EOK) {
|
|
return (status);
|
|
}
|
|
|
|
/* We check if our read callback was called because of
|
|
* a pread() or a normal read() call. If pread(), we return
|
|
* with an error code indicating that we don't support it.*/
|
|
if (msg->i.xtype & _IO_XTYPE_MASK != _IO_XTYPE_NONE) {
|
|
return (ENOSYS);
|
|
}
|
|
/*
|
|
* Get the number of bytes to read. If requested is >1, look
|
|
* at the current offset to see if we have a byte to return.
|
|
* If the requested number is exactly 1, then read 1 byte.
|
|
*/
|
|
if( msg -> i.nbytes > 0)
|
|
{
|
|
int nleft;
|
|
nleft = ocb -> attr -> attr.nbytes - ocb -> offset;
|
|
nbytes = min( msg -> i.nbytes, nleft );
|
|
if( nleft )
|
|
ocb -> offset += nbytes;
|
|
}
|
|
if( nbytes )
|
|
{
|
|
|
|
/* Here we set the number of bytes we will return. */
|
|
_IO_SET_READ_NBYTES(ctp, nbytes);
|
|
|
|
/* The next line is used to tell the system how
|
|
* large your buffer is in which you want to return your
|
|
* data for the read() call.
|
|
*
|
|
* We get the pin value by reading the IN_DATA register, masking
|
|
* with the pinmask and if the value is 0, returning an ascii '0'.
|
|
* If the value is not 0, then we return an ascii '1'.
|
|
*/
|
|
SETIOV( ctp->iov, &ocb -> attr -> buffer[ocb->offset], nbytes);
|
|
nparts = 1;
|
|
|
|
}
|
|
else
|
|
{
|
|
_IO_SET_READ_NBYTES(ctp, 0);
|
|
nparts = 0;
|
|
}
|
|
|
|
if (msg->i.nbytes > 0) {
|
|
ocb->attr->attr.flags |= IOFUNC_ATTR_ATIME;
|
|
}
|
|
|
|
/*
|
|
* Return the number of parts specified above (1 or 0).
|
|
*/
|
|
return (_RESMGR_NPARTS (nparts));
|
|
|
|
|
|
}
|
|
#endif
|
|
|
|
/*
|
|
* io_write
|
|
*
|
|
* At this point, the client has called the library write()
|
|
* function, and expects that our resource manager will write
|
|
* the number of bytes that have been specified to the device.
|
|
*
|
|
* Since this is /dev/Null, all of the clients' writes always
|
|
* work -- they just go into Deep Outer Space.
|
|
*/
|
|
|
|
static int
|
|
io_cmd_write (resmgr_context_t *ctp, io_write_t *msg, RESMGR_OCB_T *ocb)
|
|
{
|
|
int status;
|
|
char *buf;
|
|
char free_buf = 0;
|
|
//char value;
|
|
|
|
if (optv) {
|
|
printf ("%s: in io_write, id=%d\n", progname, ctp->id);
|
|
}
|
|
|
|
/* Check the access permissions of the client */
|
|
if ((status = iofunc_write_verify(ctp, msg, ocb, NULL)) != EOK) {
|
|
return (status);
|
|
}
|
|
|
|
/* Check if pwrite() or normal write() */
|
|
if ((msg->i.xtype & _IO_XTYPE_MASK) != _IO_XTYPE_NONE) {
|
|
return (ENOSYS);
|
|
}
|
|
|
|
/* Set the number of bytes successfully written for
|
|
* the client. This information will be passed to the
|
|
* client by the resource manager framework upon reply.
|
|
* In this example, we just take the number of bytes that
|
|
* were sent to us and we always write them. */
|
|
_IO_SET_WRITE_NBYTES (ctp, msg -> i.nbytes);
|
|
|
|
if( optv ) printf("got write of %d bytes, data:\n", msg->i.nbytes);
|
|
|
|
/* First check if our message buffer was large enough
|
|
* to receive the whole write at once. If yes, print data.*/
|
|
if( (msg->i.nbytes <= ctp->info.msglen - ctp->offset - sizeof(msg->i)) &&
|
|
(ctp->info.msglen < ctp->msg_max_size)) { // space for NUL byte
|
|
buf = (char *)(msg+1);
|
|
|
|
} else {
|
|
/* If we did not receive the whole message because the
|
|
* client wanted to send more than we could receive, we
|
|
* allocate memory for all the data and use resmgr_msgread()
|
|
* to read all the data at once. Although we did not receive
|
|
* the data completely first, because our buffer was not big
|
|
* enough, the data is still fully available on the client
|
|
* side, because its write() call blocks until we return
|
|
* from this callback! */
|
|
buf = malloc( msg->i.nbytes + 1);
|
|
free_buf = 1;
|
|
if( buf )
|
|
{
|
|
resmgr_msgread( ctp, buf, msg->i.nbytes, sizeof(msg->i));
|
|
}
|
|
}
|
|
/*
|
|
* Write all the values to the pin.
|
|
* The bytes passed in can be 3 types of values:
|
|
* '1' causes the pin to be set
|
|
* '0' causes the pin to be cleared
|
|
* other values have no effect
|
|
*/
|
|
if( buf )
|
|
{
|
|
|
|
int rv;
|
|
int tmp;
|
|
unsigned int cmd;
|
|
rv = sscanf( buf, "%d", &cmd );
|
|
switch( cmd )
|
|
{
|
|
case FPGA_CMD_RESET:
|
|
gpio_direction_output( FPGA_PROGRAM, 0 );
|
|
setFPGAState( FPGA_STATE_RESET );
|
|
break;
|
|
case FPGA_CMD_PROGRAM:
|
|
gpio_direction_output( FPGA_PROGRAM, 1 );
|
|
gpio_direction_output( FPGA_RDWR, 0 );
|
|
setFPGAState( FPGA_STATE_PROGRAMMING );
|
|
break;
|
|
case FPGA_CMD_FINISHPROGRAM:
|
|
gpio_direction_output( FPGA_RDWR, 1 );
|
|
tmp = read_core_version( fpgactrl->vbaseaddr, &fpgactrl->app_version );
|
|
if( !tmp )
|
|
{
|
|
tmp = read_core_version( fpgactrl->vbaseaddr, &fpgactrl->bm_version );
|
|
setFPGAState( FPGA_STATE_PROGRAMMED );
|
|
setVersion();
|
|
}
|
|
else
|
|
{
|
|
setFPGAState( FPGA_STATE_PROGRAM_FAIL );
|
|
}
|
|
break;
|
|
}
|
|
|
|
|
|
if( free_buf )
|
|
free( buf );
|
|
}
|
|
|
|
|
|
/* Finally, if we received more than 0 bytes, we mark the
|
|
* file information for the device to be updated:
|
|
* modification time and change of file status time. To
|
|
* avoid constant update of the real file status information
|
|
* (which would involve overhead getting the current time), we
|
|
* just set these flags. The actual update is done upon
|
|
* closing, which is valid according to POSIX. */
|
|
if (msg->i.nbytes > 0) {
|
|
ocb->attr->attr.flags |= IOFUNC_ATTR_MTIME | IOFUNC_ATTR_CTIME;
|
|
}
|
|
|
|
return (_RESMGR_NPARTS (0));
|
|
}
|
|
/*
|
|
* io_write
|
|
*
|
|
* At this point, the client has called the library write()
|
|
* function, and expects that our resource manager will write
|
|
* the number of bytes that have been specified to the device.
|
|
*
|
|
* Since this is /dev/Null, all of the clients' writes always
|
|
* work -- they just go into Deep Outer Space.
|
|
*/
|
|
|
|
static int
|
|
io_image_write (resmgr_context_t *ctp, io_write_t *msg, RESMGR_OCB_T *ocb)
|
|
{
|
|
int status;
|
|
char *buf;
|
|
char free_buf = 0;
|
|
//char value;
|
|
|
|
if (optv) {
|
|
printf ("%s: in io_write, id=%d\n", progname, ctp->id);
|
|
}
|
|
|
|
/* Check the access permissions of the client */
|
|
if ((status = iofunc_write_verify(ctp, msg, ocb, NULL)) != EOK) {
|
|
return (status);
|
|
}
|
|
|
|
/* Check if pwrite() or normal write() */
|
|
if ((msg->i.xtype & _IO_XTYPE_MASK) != _IO_XTYPE_NONE) {
|
|
return (ENOSYS);
|
|
}
|
|
|
|
/* Set the number of bytes successfully written for
|
|
* the client. This information will be passed to the
|
|
* client by the resource manager framework upon reply.
|
|
* In this example, we just take the number of bytes that
|
|
* were sent to us and we always write them. */
|
|
_IO_SET_WRITE_NBYTES (ctp, msg -> i.nbytes);
|
|
|
|
if( optv ) printf("got write of %d bytes, data:\n", msg->i.nbytes);
|
|
|
|
/* First check if our message buffer was large enough
|
|
* to receive the whole write at once. If yes, print data.*/
|
|
if( (msg->i.nbytes <= ctp->info.msglen - ctp->offset - sizeof(msg->i)) &&
|
|
(ctp->info.msglen < ctp->msg_max_size)) { // space for NUL byte
|
|
buf = (char *)(msg+1);
|
|
|
|
} else {
|
|
/* If we did not receive the whole message because the
|
|
* client wanted to send more than we could receive, we
|
|
* allocate memory for all the data and use resmgr_msgread()
|
|
* to read all the data at once. Although we did not receive
|
|
* the data completely first, because our buffer was not big
|
|
* enough, the data is still fully available on the client
|
|
* side, because its write() call blocks until we return
|
|
* from this callback! */
|
|
buf = malloc( msg->i.nbytes + 1);
|
|
free_buf = 1;
|
|
resmgr_msgread( ctp, buf, msg->i.nbytes, sizeof(msg->i));
|
|
}
|
|
/*
|
|
* Write all the values to the pin.
|
|
* The bytes passed in can be 3 types of values:
|
|
* '1' causes the pin to be set
|
|
* '0' causes the pin to be cleared
|
|
* other values have no effect
|
|
*/
|
|
if( buf )
|
|
{
|
|
int i;
|
|
char *p = buf;
|
|
for( i = 0; i < msg -> i.nbytes; i++, p++ )
|
|
{
|
|
out8( fpgactrl->vbaseaddr, *p );
|
|
}
|
|
if( free_buf )
|
|
free( buf );
|
|
}
|
|
|
|
|
|
/* Finally, if we received more than 0 bytes, we mark the
|
|
* file information for the device to be updated:
|
|
* modification time and change of file status time. To
|
|
* avoid constant update of the real file status information
|
|
* (which would involve overhead getting the current time), we
|
|
* just set these flags. The actual update is done upon
|
|
* closing, which is valid according to POSIX. */
|
|
if (msg->i.nbytes > 0) {
|
|
ocb->attr->attr.flags |= IOFUNC_ATTR_MTIME | IOFUNC_ATTR_CTIME;
|
|
}
|
|
|
|
return (_RESMGR_NPARTS (0));
|
|
}
|
|
/* Why we don't have any close callback? Because the default
|
|
* function, iofunc_close_ocb_default(), does all we need in this
|
|
* case: Free the ocb, update the time stamps etc. See the docs
|
|
* for more info.
|
|
*/
|
|
|
|
|