/* File: drvuScopeBiasBoard.h * Author: Hovanes Egiyan, Jefferson Lab * Date: 05-Jan-2014 * * Purpose: * This module provides the driver support for the asyn device support layer * for the uScopeBiasBoard boards. * */ #include "drvuScopeBias.hh" #include "drvuScopeBiasBoard.hh" //! Declare global mutex and its attributes pthread_mutex_t drvuScopeBiasBoard::dubbClassMutex; pthread_mutexattr_t drvuScopeBiasBoard::dubbClassMtxAttr; //! Initialize global mutex and its attributes static int dummyInt = drvuScopeBiasBoard::initGlobalMutex(); string drvuScopeBiasBoard::dubbDriverName = "asynuScopeBiasBoard"; // Driver name //string drvuScopeBiasBoard::dubbDirNameDB = "db/"; // Directory prefix (may be redundant if global port is introduced) string drvuScopeBiasBoard::dubbBoardFileNameDB = "uScopeBiasBoard.db" ; // DB file template for boards string drvuScopeBiasBoard::dubbChannelFileNameDB = "uScopeBiasChannel.db" ; // DB file template for channel map drvuScopeBiasBoard::dubbPortMap; // Map of asyn ports for uScope boards vector drvuScopeBiasBoard::dubbMbbPrefixVec = drvuScopeBiasBoard::initPrefixVector(); map drvuScopeBiasBoard::dubbMbbPrefixMap = drvuScopeBiasBoard::initMbbMap( drvuScopeBiasBoard::dubbMbbPrefixVec ); unsigned drvuScopeBiasBoard::dubbMaxBoards = 17; // Maximum number of control boards /*Constructor */ drvuScopeBiasBoard::drvuScopeBiasBoard( const char* ipAddress, const char* asynPortName, int maxNumberOfChans ) : asynPortDriver( asynPortName, maxNumberOfChans, NUM_uScopeBiasBoard_PARAMS, asynOctetMask | asynUInt32DigitalMask | asynFloat64Mask | asynDrvUserMask | asynCommonMask | asynOptionMask, asynOctetMask | asynUInt32DigitalMask | asynFloat64Mask | asynDrvUserMask | asynCommonMask | asynOptionMask, ASYN_CANBLOCK | ASYN_MULTIDEVICE, 1, 0, 0 ), // 0, 1, 0, 0 ), dubbIP(ipAddress), dubbExists(false), dubbBoard(0), dubbMbbPrefix("") { cout << "In function " << " drvuScopeBiasBoard::drvuScopeBiasBoard " << endl; initMutex(); { MtxLock classLock( dubbClassMutex ); // Check if a port under this name already exists if( dubbPortMap.count(portName) > 0 ) throw ( string("Port named ") + string(portName) + string( "already exists" ) ); dubbPortMap[portName] = this; } static const char* functionName="drvuScopeBiasBoard::drvuScopeBiasBoard"; cout << "Maximum number of boards is " << maxNumberOfChans << endl; { MtxLock scopeLock( dubbMutex ); this->dubbEventID = epicsEventCreate( epicsEventEmpty ); createParam( uScopeBiasBoardAddressString , asynParamOctet , &uScopeBiasBoardAddress_ ); /* string, RO */ createParam( uScopeBiasBoardPortString , asynParamOctet , &uScopeBiasBoardPort_ ); /* string, RO */ createParam( uScopeBiasBoardTempString , asynParamFloat64 , &uScopeBiasBoardTemp_ ); /* double, RO */ createParam( uScopeBiasBoardStatusString , asynParamUInt32Digital , &uScopeBiasBoardStatus_ ); /* unsigned, RO */ createParam( uScopeBiasBoardChanNumbString , asynParamUInt32Digital , &uScopeBiasBoardChanNumb_ ); /* unsigned, RO */ // Channel paramaters createParam( uScopeBiasBoardChanAddrString , asynParamOctet , &uScopeBiasBoardChanAddr_ ); /* string, RO */ createParam( uScopeBiasBoardChanMaxVoltString , asynParamFloat64 , &uScopeBiasBoardChanMaxVolt_ ); /* double, RO */ createParam( uScopeBiasBoardChanSetPointString , asynParamFloat64 , &uScopeBiasBoardChanSetPoint_ ); /* double, RO */ createParam( uScopeBiasBoardChanRampUpString , asynParamFloat64 , &uScopeBiasBoardChanRampUp_ ); /* double, RO */ createParam( uScopeBiasBoardChanRampDownString , asynParamFloat64 , &uScopeBiasBoardChanRampDown_ ); /* double, RO */ createParam( uScopeBiasBoardChanTempString , asynParamFloat64 , &uScopeBiasBoardChanTemp_ ); /* double, RO */ createParam( uScopeBiasBoardChanStatString , asynParamUInt32Digital , &uScopeBiasBoardChanStat_ ); /* unsigned, RO */ createParam( uScopeBiasBoardChanEnableString , asynParamUInt32Digital , &uScopeBiasBoardChanEnable_ ); /* unsigned, RO */ } // Uncomment this line to enable asynTraceFlow during the constructor // pasynTrace->setTraceMask( pasynUserSelf, 0x11 ); pasynTrace->setTraceMask( pasynUserSelf, 0x255 ); // Try to create an uScopeBiasBoard object with the ip address and port name try { MtxLock classLock( dubbClassMutex ); dubbBoard = new UConnCtrlBoard( dubbIP ); // Fill the channel vector from the map map channelMap = dubbBoard->GetChannelMap(); // By default the map is sorted by the key. This loop is here // to be able to use sorting different from sorting by key for( map::iterator it = channelMap.begin(); it != channelMap.end(); it++ ) { UConnBiasChannel* channel = it->second; dubbChannelsInBoard.push_back( channel ); } } catch ( string& errString ) { cout << errString << endl ; exit(0); } int status = 0; /* Create the thread that reads the values from the registers in the background */ status = (asynStatus)( epicsThreadCreate(portName, epicsThreadPriorityMedium, epicsThreadGetStackSize( epicsThreadStackMedium ), (EPICSTHREADFUNC) ::readBoardParameters, this) == NULL); if ( status ) { printf( "%s:%s: epicsThreadCreate failure\n", dubbDriverName.c_str(), functionName ); return; } MtxLock classLock( dubbClassMutex ); // Assign the string for the string of mbbi/mbbo record by looking // at the map of the 17 string to see which one is available for( unsigned iPref = 0; iPref < dubbMbbPrefixVec.size(); iPref++ ) { string mbbString = dubbMbbPrefixVec[iPref]; drvuScopeBiasBoard* portPtr = dubbMbbPrefixMap[mbbString]; if( portPtr == 0 ) { dubbMbbPrefix = mbbString; dubbMbbPrefixMap[mbbString] = this; cout << "Found available prefix " << mbbString << endl; break; } } // If no mbbi/mbbo string could be found throw an exception if( dubbMbbPrefix == "") { cout << "Problem in " << functionName << ": all 17 uScopeBiasBoard ports are used. No more uScopeBiasBoard boards " << endl; throw "No More Boards"; } // Assign the address to the portmap dubbPortMap[portName] = this; dubbExists = true; classLock.Unlock(); return; } // Destructor drvuScopeBiasBoard::~drvuScopeBiasBoard() { MtxLock classLock( dubbClassMutex ); // If the portname existed in the static map or portnames, remove it from the map if( dubbPortMap.count( string(portName) ) > 0 ) { dubbPortMap.erase( string( portName ) ); // Remove this address from the map of the ports dubbMbbPrefixMap[dubbMbbPrefix] = 0; // Free the mbbi/mbbo string for use } closeMutex(); return; } // Assign the first two letter for the mbbi/mbbo string field for // each of the sixteen chassis. This would limit the number of chassis to 16. // But artificially added 15 string since UConn is expecting 17 boards. vector drvuScopeBiasBoard::initPrefixVector() { MtxLock classLock( dubbClassMutex ); vector prefVec( dubbMaxBoards ); prefVec[0] = "ZR"; prefVec[1] = "ON"; prefVec[2] = "TW"; prefVec[3] = "TH"; prefVec[4] = "FR"; prefVec[5] = "FV"; prefVec[6] = "SX"; prefVec[7] = "SV"; prefVec[8] = "EI"; prefVec[9] = "NI"; prefVec[10] = "TE"; prefVec[11] = "EL"; prefVec[12] = "TV"; prefVec[13] = "TT"; prefVec[14] = "FT"; prefVec[15] = "FF"; // This is more than mbbi/mbbo can handle, but added for possibility of // using just the first 16 boards in a manner similar to other type of chassis // where only 16 or less instances is expected in Hall D. prefVec[16] = "ST"; return prefVec; } // Assign null pointers to the map of the mbbi/mbbo mapping. // Null pointer will be interpreted as this mbbi item is vacant map drvuScopeBiasBoard::initMbbMap( vector& prefVec ) { MtxLock classLock( dubbClassMutex ); map prefMap; cout << "Initializing MBBI string map" << endl; // Assign zero to all map entries by actually creating those entries for( unsigned iPref = 0; iPref < prefVec.size(); iPref++ ) { prefMap[prefVec[iPref]] = 0; } cout << "Done initializing mbbo map" << endl; return prefMap; } // Function to read the 32-bit unsigned integer parameters. // This is useful when scanning at fixed rate and at the initialization asynStatus drvuScopeBiasBoard::readUInt32Digital( asynUser* pasynUser, epicsUInt32* value, epicsUInt32 mask ) { string functionName = "drvuScopeBiasBoard::readUInt32Digital"; cout << "In function " << functionName << endl; int function = pasynUser->reason; int chanNum; getAddress( pasynUser, &chanNum ); asynStatus aStat = asynError; // Boardwise functions if( function == uScopeBiasBoardStatus_ ) { *value = dubbBoard->GetStatus(); aStat = setUIntDigitalParam( uScopeBiasBoardChanStat_, *value, mask ); if( aStat == asynSuccess) callParamCallbacks(); return aStat; } if( function == uScopeBiasBoardChanNumb_) { *value = dubbBoard->GetNumberOfChannels(); aStat = setUIntDigitalParam( uScopeBiasBoardChanNumb_, *value, mask ); if( aStat == asynSuccess) callParamCallbacks(); return aStat; } // Channel-wise functions if( function == uScopeBiasBoardChanStat_ ) { *value = dubbChannelsInBoard[chanNum]->GetStatus(); aStat = setUIntDigitalParam( chanNum, uScopeBiasBoardChanStat_, *value, mask ); if( aStat == asynSuccess) callParamCallbacks( chanNum ); return aStat; } if( function == uScopeBiasBoardChanEnable_ ) { *value = dubbChannelsInBoard[chanNum]->IsEnabled(); aStat = setUIntDigitalParam( chanNum, uScopeBiasBoardChanEnable_, *value, mask ); if( aStat == asynSuccess) callParamCallbacks( chanNum ); return aStat; } return aStat; } asynStatus drvuScopeBiasBoard::readFloat64( asynUser *pasynUser, epicsFloat64* value ) { string functionName = "drvuScopeBiasBoard::readFloat64"; cout << "In function " << functionName << endl; int function = pasynUser->reason; int chanNum; getAddress( pasynUser, &chanNum ); asynStatus aStat = asynError; // Boardwise functions if( function == uScopeBiasBoardTemp_ ) { *value = dubbBoard->GetTemperature(); aStat = setDoubleParam( uScopeBiasBoardTemp_, *value ); if( aStat == asynSuccess ) callParamCallbacks(); return aStat; } // Channel-wise functions if( function == uScopeBiasBoardChanMaxVolt_ ) { *value = dubbChannelsInBoard[chanNum]->GetMaxVoltage(); aStat = setDoubleParam( chanNum, uScopeBiasBoardChanMaxVolt_, *value ); if( aStat == asynSuccess) callParamCallbacks( chanNum ); return aStat; } if( function == uScopeBiasBoardChanSetPoint_ ) { *value = dubbChannelsInBoard[chanNum]->GetVoltageSetPoint(); aStat = setDoubleParam( chanNum, uScopeBiasBoardChanSetPoint_, *value ); if( aStat == asynSuccess) callParamCallbacks( chanNum ); return aStat; } if( function == uScopeBiasBoardChanRampUp_ ) { *value = dubbChannelsInBoard[chanNum]->GetRampUpRate(); aStat = setDoubleParam( chanNum, uScopeBiasBoardChanRampUp_, *value ); if( aStat == asynSuccess) callParamCallbacks( chanNum ); return aStat; } if( function == uScopeBiasBoardChanRampDown_ ) { *value = dubbChannelsInBoard[chanNum]->GetRampDownRate(); aStat = setDoubleParam( chanNum, uScopeBiasBoardChanRampDown_, *value ); if( aStat == asynSuccess) callParamCallbacks( chanNum ); return aStat; } if( function == uScopeBiasBoardChanTemp_ ) { *value = dubbChannelsInBoard[chanNum]->GetTemperature(); aStat = setDoubleParam( chanNum, uScopeBiasBoardChanTemp_, *value ); if( aStat == asynSuccess) callParamCallbacks( chanNum ); return aStat; } return aStat; } asynStatus drvuScopeBiasBoard::readOctet( asynUser *pasynUser, char *value, size_t maxChars, size_t *nActual, int *eomReason ) { string functionName = "drvuScopeBiasBoard::readOctet"; cout << "In function " << functionName << endl; int function = pasynUser->reason; int chanNum; getAddress( pasynUser, &chanNum ); asynStatus aStat = asynError; // Board-wise parameters if( function == uScopeBiasBoardAddress_ ) { aStat = setStringParam( uScopeBiasBoardAddress_, dubbBoard->GetAddress().c_str() ); if( aStat == asynSuccess ) callParamCallbacks(); return asynPortDriver::readOctet( pasynUser, value, maxChars, nActual, eomReason ); } if( function == uScopeBiasBoardPort_ ) { aStat = setStringParam( uScopeBiasBoardPort_, portName ); if( aStat == asynSuccess) callParamCallbacks(); return asynPortDriver::readOctet( pasynUser, value, maxChars, nActual, eomReason ); } // Channel-wise functions if( function == uScopeBiasBoardChanAddr_ ) { aStat = setStringParam( chanNum, uScopeBiasBoardChanAddr_, dubbChannelsInBoard[chanNum]->GetChannelAddress().c_str() ); if( aStat == asynSuccess) callParamCallbacks( chanNum ); return asynPortDriver::readOctet( pasynUser, value, maxChars, nActual, eomReason ); } return aStat; } asynStatus drvuScopeBiasBoard::writeUInt32Digital( asynUser* pasynUser, epicsUInt32 value, epicsUInt32 mask ) { int command = pasynUser->reason; int channel; getAddress( pasynUser, &channel ); asynStatus aStat = asynError; string functionName = "drvuScopeBiasBoard::writeUInt32Digital"; printf( "Now inside %s , command is %d, value is %d \n", functionName.c_str(), command, value ); // Enable or disable this particular channel if( command == uScopeBiasBoardChanEnable_ ) { if( value != 0 ) value = 1; if( value != 0 ) dubbChannelsInBoard[channel]->Enable(); else dubbChannelsInBoard[channel]->Disable(); } return asynPortDriver::writeUInt32Digital( pasynUser, value, mask ); } asynStatus drvuScopeBiasBoard::writeFloat64( asynUser *pasynUser, epicsFloat64 value ) { int command = pasynUser->reason; int channel; getAddress( pasynUser, &channel ); asynStatus aStat = asynError; string functionName = "drvuScopeBiasBoard::writeFloat"; printf( "Now inside %s , command is %d, value is %f \n", functionName.c_str(), command, value ); // Set maximum voltage for a channel if( command == uScopeBiasBoardChanMaxVolt_ ) { dubbChannelsInBoard[channel]->SetMaxVoltage( value ); } // Set voltage setpoint for a channel if( command == uScopeBiasBoardChanSetPoint_ ) { dubbChannelsInBoard[channel]->SetVoltageSetPoint( value ); } // Set rampup rate for a channel if( command == uScopeBiasBoardChanRampUp_ ) { dubbChannelsInBoard[channel]->SetRampUpRate( value ); } // Set rampdown rate for a channel if( command == uScopeBiasBoardChanRampDown_ ) { dubbChannelsInBoard[channel]->SetRampDownRate( value ); } return asynPortDriver::writeFloat64( pasynUser, value ); } /* Report parameters */ void drvuScopeBiasBoard::report( FILE *fp, int details ) { if ( details > 0 ) { // fprintf(fp, " Scratch Content = 0x%x\n", uScopeBiasBoardSratch_ ); } // Call the base class method asynPortDriver::report( fp, details ); return; } // This function reads the values from the UConn driver and calls the callbacks // It is supposed to run in a separate thread for each board // Because the registers are read here we do not need to read // in our new readUInt32Digital function, just call the base version in // case the SCAN field is set to some fixed rate scanning. // The values read here will be moved to th appropriate records. void drvuScopeBiasBoard::readBoardParameters( void ) { asynStatus aStat ; // Set updating time to value in seconds double updateTime = 2.0; // while( taskDelay(sysClkRateGet()) ) { while( 1 ) { epicsEventId eventID; { MtxLock objLock( dubbMutex ); eventID = this->dubbEventID; } epicsEventWaitWithTimeout( eventID, updateTime ); // usleep( 100000 ); // if (run) epicsEventWaitWithTimeout(this->eventId, updateTime); // else epicsEventWait(this->eventId); // /* run could have changed while we were waiting */ // getIntegerParam(P_Run, &run); // if (!run) continue; // Read number channels per board and their types MtxLock objLock( dubbMutex ); for( unsigned int iChan = 0; iChan < dubbChannelsInBoard.size(); iChan++ ) { UConnBiasChannel* channel = dubbChannelsInBoard[iChan]; aStat = setStringParam( iChan, uScopeBiasBoardChanAddr_, channel->GetChannelAddress().c_str() ); // cout << "Setting Address parameter for channel " << iChan << " to " << channel->GetChannelAddress() << endl; aStat = setUIntDigitalParam( iChan, uScopeBiasBoardChanStat_, channel->GetStatus(), 0xFFFFFFFF ); // cout << "Setting Status parameter to " << "0x" << hex << channel->GetStatus() << dec << endl; aStat = setUIntDigitalParam( iChan, uScopeBiasBoardChanStat_, channel->IsEnabled(), 0xFFFFFFFF ); // cout << "Setting Enabled parameter to " << "0x" << hex << channel->IsEnabled() << dec << endl; aStat = setDoubleParam( iChan, uScopeBiasBoardChanMaxVolt_, channel->GetMaxVoltage() ); // cout << "Setting Max Volt parameter to " << channel->GetMaxVoltage() << endl; aStat = setDoubleParam( iChan, uScopeBiasBoardChanSetPoint_, channel->GetVoltageSetPoint() ); // cout << "Setting Setpoint parameter to " << channel->GetVoltageSetPoint() << endl; aStat = setDoubleParam( iChan, uScopeBiasBoardChanRampUp_, channel->GetRampUpRate() ); // cout << "Setting RampUp parameter to " << channel->GetRampUpRate() << endl; aStat = setDoubleParam( iChan, uScopeBiasBoardChanRampDown_, channel->GetRampDownRate() ); // cout << "Setting RampUp parameter to " << channel->GetRampDownRate() << endl; aStat = setDoubleParam( iChan, uScopeBiasBoardTemp_, channel->GetTemperature() ); // cout << "Setting Temperature parameter to " << channel->GetTemperature() << endl; callParamCallbacks( iChan ); } // Set the board parameters aStat = setStringParam( uScopeBiasBoardAddress_, dubbBoard->GetAddress().c_str() ); // cout << "Setting Address parameter to " << dubbBoard->GetAddress() << endl; aStat = setStringParam( uScopeBiasBoardPort_, portName ); // cout << "Settin port parameter to " << portName << endl; aStat = setDoubleParam( uScopeBiasBoardTemp_, dubbBoard->GetTemperature() ); // cout << "Setting Temperature parameter to " << dubbBoard->GetTemperature() << endl; aStat = setUIntDigitalParam( uScopeBiasBoardStatus_, dubbBoard->GetStatus(), 0xFFFFFFFF ); // cout << "Setting Status parameter to " << "0x" << hex << dubbBoard->GetStatus() << dec << endl; aStat = setUIntDigitalParam( uScopeBiasBoardChanNumb_, dubbBoard->GetNumberOfChannels(), 0xFFFFFFFF ); // cout << "Setting Number of Channels parameter to " << dubbBoard->GetNumberOfChannels() << endl; callParamCallbacks(); } } int drvuScopeBiasBoard::loadRecords() { // First load global records { MtxLock classLock( dubbClassMutex ); // Loading the configuration records that use this driver string substitString = "PORT=" + string(portName) + string(",MBB=") + dubbMbbPrefix ; // string fullFileName = drvGlobaluScopeBiasBoard::getDirDB() + dubbBoardFileNameDB; string fullFileName = drvuScopeBias::getDirDB() + dubbBoardFileNameDB; cout << "Will load from " << fullFileName << " with arguments " << substitString << endl; dbLoadRecords( fullFileName.c_str(), substitString.c_str() ); cout << "Database loaded for board " << portName << endl; } // Now load records for each channel on this board // Here the address for the channel will be the index of the vector // since what we are specifying is the address for the asyn driver , // that is the index of the asyn parameter. the actual address of the // channel defined by UConn will be kept in the driver and assigned // to the EPICS record name. Note that the asyn address and UConn address // do not have to be the same. for( unsigned iChan = 0; iChan < dubbChannelsInBoard.size(); iChan++ ) { MtxLock classLock( dubbClassMutex ); string fullFileName = drvuScopeBias::getDirDB() + dubbChannelFileNameDB; stringstream ssChan; ssChan << iChan; // This looks backward, address is assigned to "CHAN", but the // channel is assigned to "ADDR", and this is because ADDR referres // to asyn address, to to the channel address in the UConn notation, // which is here the "address" of the channel, and it is assigned to CHAN. string substitString = "PORT=" + string(portName) + ",CHAN=" + dubbChannelsInBoard[iChan]->GetChannelAddress() + ",ADDR=" + ssChan.str() ; cout << "Will load from " << fullFileName << " with arguments " << substitString << endl; dbLoadRecords( fullFileName.c_str(), substitString.c_str() ); cout << "Board Database loaded for channel " << dubbChannelsInBoard[iChan]->GetChannelAddress() << " on board " << portName << endl; } cout << "All DBs loaded for board " << portName << endl; return 0; } // This function simply calls the member function of the driver whose pointer // is passed as the argument. This function is to be called from a separate // thread and is specified in the argument when launching that thread void readBoardParameters( void *drvPtr ) { drvuScopeBiasBoard* pPtr = (drvuScopeBiasBoard *)drvPtr; pPtr->readBoardParameters(); } // Mutex manipulation functions //! Initialize global mutex int drvuScopeBiasBoard::initGlobalMutex() { pthread_mutexattr_init( &dubbClassMtxAttr ); pthread_mutexattr_setpshared( &dubbClassMtxAttr, PTHREAD_PROCESS_PRIVATE ); pthread_mutexattr_settype(&dubbClassMtxAttr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init( &dubbClassMutex, &dubbClassMtxAttr ); return 0; } //! Initialize mutex for individual objects void drvuScopeBiasBoard::initMutex() { pthread_mutexattr_init( &dubbMtxAttr ); pthread_mutexattr_setpshared( &dubbMtxAttr, PTHREAD_PROCESS_PRIVATE ); pthread_mutexattr_settype(&dubbMtxAttr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init( &dubbMutex, &dubbMtxAttr ); return; } //! Destroy mutex for individual objects void drvuScopeBiasBoard::closeMutex() { pthread_mutex_destroy( &dubbMutex ); pthread_mutexattr_destroy( &dubbMtxAttr ); return; } // Here we define IOC shell function calls extern "C" { char* drvuScopeBiasBoardConfig( const char *ipAddress, const char *asynPortName, int maxNumOfSlots ) { // Now create the asyn driver drvuScopeBiasBoard* puScopeBiasBoard = new drvuScopeBiasBoard( ipAddress, asynPortName, maxNumOfSlots ); return reinterpret_cast( puScopeBiasBoard ); } /* iocsh config function */ static const iocshArg drvuScopeBiasBoardConfigArg0 = { "Board IP Address" , iocshArgString }; static const iocshArg drvuScopeBiasBoardConfigArg1 = { "Asyn port name" , iocshArgString }; static const iocshArg drvuScopeBiasBoardConfigArg2 = { "Max N of Channels", iocshArgInt }; static const iocshArg * const drvuScopeBiasBoardConfigArgs[] = { &drvuScopeBiasBoardConfigArg0, &drvuScopeBiasBoardConfigArg1, &drvuScopeBiasBoardConfigArg2 }; static const iocshFuncDef drvuScopeBiasBoardConfigFuncDef = { "drvuScopeBiasBoardConfig", 3, drvuScopeBiasBoardConfigArgs }; static void drvuScopeBiasBoardConfigCallFunc(const iocshArgBuf *args) { drvuScopeBiasBoardConfig( args[0].sval, args[1].sval, args[2].ival ); } void drvuScopeBiasBoardRegister(void) { iocshRegister( &drvuScopeBiasBoardConfigFuncDef, drvuScopeBiasBoardConfigCallFunc ); } epicsExportRegistrar( drvuScopeBiasBoardRegister ); } // extern "C"