0

I am reading force sensor data using NI DAQ card, I am able to read the data perfectly but I am confuse how to set the desired sampling rate.Right now when I am checking the sampling rate it is giving me random value like some time it is 500hz and some time 50000hz.

here is the code that I am using for reading force sensor data and calculating sampling rate.

main.h file
 int Run_main(void *pUserData)
    {
        /** to visual data **/
        CAdmittanceDlg* pDlg = (CAdmittanceDlg*)pUserData;
        /** timer for calculating sampling rate **/
        performanceTest timer;
        /*** Force Sensor **/
        ForceSensor sensor1("FT8682.cal","FT_Sensor1","dev3/ai0:6",2,1.0e4);
        std::vector<double> force;
        while(1)
        {
            /*** start time  ***/
            timer.setStartTimeNow();
            /*** reading force sensor data  ***/
            force=sensor1.readForce();
            /*** visualize data in GUI  ***/
            pDlg->setCustomData(0,"force_x ",force[0]);
            pDlg->setCustomData(1,"force_y ",force[1]);
            pDlg->setCustomData(2,"force_z ",force[2]);
            pDlg->setCustomData(3,"position ",currentPosition);
            timer.increaseFrequencyCount();
            /*** end time  ***/
            pDlg->setCustomData(4,"Sampling rate ",1/timer.getElapsedTime());
        }


        return 1;
    }

//here is ForceSensor.h file
class ForceSensor
{
public:

    DAQmx board;
    Calibration *cal;

    float bias[7];
    std::vector<double> f;

    ForceSensor(std::string calibrationFile, std::string task,
        std::string device, uInt64 numberOfSamples = 1000, float64 samplingRate = 1.0e4):
    board(task, device, numberOfSamples, samplingRate),
        f(6, 0)
    {
        board.initRead();
        cal = createCalibration(calibrationFile.c_str(), 1);
        if (!cal) throw std::runtime_error(calibrationFile + " couldn't be opened");
        board.readVoltage(2); // to get reed of zeros in the first batch of samples
        board.readVoltage(1000); // read big number of samples to determine the bias

        std::copy (board.da.cbegin(), board.da.cend(), std::ostream_iterator<double>(std::cout, " "));
        std::cout << std::endl;

        Bias(cal, board.da.data());
    }

    ~ForceSensor(void){}

    const std::vector<double> & readForce(){
        auto forces = std::vector<float>(6, 0);


        board.readVoltage(2);

        ConvertToFT(cal, board.da.data(), forces.data());

        std::transform(forces.cbegin(), forces.cend(),
            f.begin(),
            [](float a){ return static_cast<double>(a);});

        return f;
    }


};

//DAQmx.h file
class DAQmx {
    float64 MaxVoltage;
    TaskHandle taskHandle;
    TaskHandle counterHandle;
    std::string taskName;
    std::string device;
    float64 samplingRate;

public:
    std::vector <float> da;
    uInt64 numberOfSamples;


    DAQmx(std::string task, std::string device, uInt64 numberOfSamples = 1000, float64 samplingRate = 1.0e4):
        taskName(task), device(device), samplingRate(samplingRate), numberOfSamples(numberOfSamples),
        da(7, 0.0f)
    {
         MaxVoltage = 10.0;
    }

    ~DAQmx()
    {
        if( taskHandle == 0 )  return;
        DAQmxStopTask(taskHandle);
        DAQmxClearTask(taskHandle);
    }

    void CheckErr(int32 error, std::string functionName = "")
    {
        char        errBuff[2048]={'\0'};

        if( DAQmxFailed(error) ){
            DAQmxGetExtendedErrorInfo(errBuff, 2048);

            if( taskHandle!=0 )  {
                DAQmxStopTask(taskHandle);
                DAQmxClearTask(taskHandle);
            }
            std::cerr << functionName << std::endl;
            throw std::runtime_error(errBuff);
        }
    }


    void initRead()
    {
        CheckErr(DAQmxCreateTask(taskName.c_str(), &taskHandle));
        CheckErr(DAQmxCreateAIVoltageChan(taskHandle, device.c_str(),"", DAQmx_Val_Diff ,-10.0,10.0,DAQmx_Val_Volts,NULL));
//        CheckErr(DAQmxCfgSampClkTiming(taskHandle, "" , samplingRate, DAQmx_Val_Rising, DAQmx_Val_FiniteSamps, numberOfSamples));
        CheckErr(DAQmxCfgSampClkTiming(taskHandle, "OnboardClock" , samplingRate, DAQmx_Val_Rising, DAQmx_Val_ContSamps, numberOfSamples*10));
        CheckErr(DAQmxSetReadRelativeTo(taskHandle, DAQmx_Val_MostRecentSamp ));
        CheckErr(DAQmxSetReadOffset(taskHandle,-1));
        CheckErr(DAQmxStartTask(taskHandle));
    }

    void initWrite()
    {
        CheckErr(DAQmxCreateTask(taskName.c_str(), &taskHandle));
        CheckErr(DAQmxCreateAOVoltageChan(taskHandle, device.c_str(),"",-10.0, 10.0, DAQmx_Val_Volts,""));
        CheckErr(DAQmxStartTask(taskHandle));
    }


    int32 readVoltage (uInt64 samples = 0, bool32 fillMode = DAQmx_Val_GroupByScanNumber) //the other option is DAQmx_Val_GroupByScanNumber
    {
        int32   read; // samples actually read
        const float64 timeOut = 10;
        if (samples == 0) samples = numberOfSamples;
        std::vector<float64> voltagesRaw(7*samples);

        CheckErr(DAQmxReadAnalogF64(taskHandle, samples, timeOut, fillMode,
                                    voltagesRaw.data(), 7*samples, &read, NULL));
//        DAQmxStopTask(taskHandle);
        if (read < samples)
            throw std::runtime_error ("DAQmx::readVoltage(): couldn't read all the samples,"
                                      "requested: "  + std::to_string(static_cast<long long>(samples)) +
                                      ", actually read: " + std::to_string(static_cast<long long>(read)));

        //we change it
        for(int axis=0;axis < 7; axis++)
        {
            double temp = 0.0;
            for(int i=0;i<read;i++)
            {
                temp += voltagesRaw[i*7+axis];
            }
            da[axis] = temp / read;
        }


        return read;
    }


    void writeVoltage(float64 value)
    {
        if (value > MaxVoltage) value = MaxVoltage;
        if (value < -MaxVoltage) value = -MaxVoltage;
        const float64 timeOut = 10;
            //A value of 0 indicates to try once to read the requested samples.
            //If all the requested samples are read, the function is successful.
            //Otherwise, the function returns a timeout error and returns the samples that were actually read.

        float64 data[1] = {value};
        int32 written;

        CheckErr(DAQmxWriteAnalogF64(taskHandle, 1, 1, timeOut, DAQmx_Val_GroupByChannel, data, &written, NULL));

        DAQmxStopTask(taskHandle);
    }

};

Edit: when I am changing the number of samples, the throughput of my application reduce drastically,in ForceSensor.h file if I change the number of sample in below function from 2 to 100, throughput of application decrease to 500Hz from 10kHz.Please guide me related to this.

const std::vector<double> & readForce(){
        auto forces = std::vector<float>(6, 0);

        //changing value from 2 to 100 decrease the throughput from 10Khz to          500Hz
        board.readVoltage(2);
        ConvertToFT(cal, board.da.data(), forces.data());

        std::transform(forces.cbegin(), forces.cend(),
            f.begin(),
            [](float a){ return static_cast<double>(a);});


        return f;
    }

I am also using NI Motion card to control a motor, below is the main.h file in which I have add the NI Motion code. When I add the NI Motion code in main.h file throughput of the application reduces to 200Hz whatever the value I have used for number of samples in force sensor. Is there any way to increase the throughput of application when using both NI Motion for controlling motor and DAQ for reading force sensor at same time?

main.h
inline int Run_main(void *pUserData)
    {
        /** to visual data **/
        CAdmittanceDlg* pDlg = (CAdmittanceDlg*)pUserData;
        /** timer for calculating sampling rate **/
        performanceTest timer;
        /*** Force Sensor **/
        ForceSensor sensor1("FT8682.cal","FT_Sensor1","dev3/ai0:6",2,4.0e4);
        /*** NI Motion Card 2=board ID, 0x01=axis number**/
        NiMotion Drive(2,0x01);
        int count=0;
        sensor1.Sampling_rate_device(&sampling_rate);
        std::vector<double> force;
        timer.setStartTimeNow();
        while(x)
        {
            /*** start time  ***/

            /*** reading force sensor data  ***/
            force=sensor1.readForce();
            /*** reading current position of drive  ***/
            currentPosition = Drive.readPosition();
           /*** Actuate Drive ***/
            Drive.actuate(2047);

            enalble_motor=Drive.isEnabled();

            /*** visualize data in GUI  ***/
            pDlg->setCustomData(0,"force_x ",force[0]);
            pDlg->setCustomData(1,"force_y ",force[1]);
            pDlg->setCustomData(2,"force_z ",force[2]);
            pDlg->setCustomData(3,"position ",currentPosition);
            pDlg->setCustomData(5,"sampling rate of device ",sampling_rate);
            timer.increaseFrequencyCount();
            count++;
            if(count==1000)
            {
                pDlg->setCustomData(4,"time elapsed ",count/timer.getElapsedTime());
                count=0;
                timer.setStartTimeNow();
            }
            /*** end time  ***/
        }


        return 1;
    }

//here is NiMotion.file 
class NiMotion {

    u8  boardID;    // Board identification number
    u8  axis;       // Axis number
    u16 csr;        // Communication status register
    u16 axisStatus; // Axis status
    u16 moveComplete;
    int32 encoderCounts; //current position [counts]
    int32 encoderCountsStartPosition;
    double position; //Position in meters
    bool read_first;

public:
    NiMotion(u8 boardID = 1, u8 axis = NIMC_AXIS1): boardID(boardID), axis(axis), csr(0)
    {
        init();
    }

    ~NiMotion(){enableMotor(false);}

    void init() {
        CheckErr(flex_initialize_controller(boardID, nullptr)); // use default settings
        CheckErr(flex_read_pos_rtn(boardID, axis, &encoderCounts));
        encoderCountsStartPosition = encoderCounts;
        enableMotor(false);
        read_first=true;
        loadConfiguration();
    }

    double toCm(i32 counts){ return counts*countsToCm; }

    i32 toCounts(double Cm){ return (Cm/countsToCm); }

    double readPosition(){
//        CheckErr(flex_read_pos_rtn(boardID, axis, &encoderCounts));

        CheckErr(flex_read_encoder_rtn(boardID, axis, &encoderCounts));
        if(read_first)
        {
            encoderCountsStartPosition = encoderCounts;
            read_first=false;
        }

        encoderCounts -= encoderCountsStartPosition;
        position = encoderCounts*countsToCm;
        return position;
    }

    void enableMotor(bool state)
    {
        if (state)
            CheckErr(flex_set_inhibit_output_momo(boardID, 1 << axis, 0));
        else
            CheckErr(flex_set_inhibit_output_momo(boardID, 0, 1 << axis));
    }

    bool isEnabled(){
        u16 home = 0;
        CheckErr(flex_read_home_input_status_rtn(boardID, &home));
        return (home & (1 << axis));
    }

//    void resetPosition()
//    {
////        CheckErr(flex_reset_encoder(boardID, NIMC_ENCODER1, 0, 0xFF));
//        CheckErr(flex_reset_pos(boardID, NIMC_AXIS1, 0, 0, 0xFF));
//    }

    void actuate(double positionCommand)
    {
        int32 positionCommandCounts = toCounts(positionCommand);
//        CheckErr(flex_load_dac(boardID, NIMC_DAC1, std::lround(positionCommand), 0xFF));
//        CheckErr(flex_load_dac(boardID, NIMC_DAC2, std::lround(positionCommand), 0xFF));
//        CheckErr(flex_load_dac(boardID, NIMC_DAC3, std::lround(positionCommand), 0xFF));
        // CheckErr(flex_load_dac(boardID, NIMC_DAC1, (positionCommand), 0xFF));
         CheckErr(flex_load_target_pos(boardID, axis, (positionCommand), 0xFF));
         CheckErr(flex_start(2, axis, 0));
         //CheckErr(flex_load_target_pos (2, 0x01, 2047, 0xFF));
//        CheckErr(flex_load_dac(boardID, NIMC_DAC3, 10000, 0xFF));

//        std::cout << PositionCotroller(desiredPositionCounts, encoderCounts) << std::endl;
//        std::this_thread::sleep_for(cycle);
    }

    void moveToPosition(double desiredPosition, double P, double I, double D)
    {
       /* int32 desiredPositionCounts = toCounts(desiredPosition);
        std::chrono::milliseconds cycle(100);
        PIDController PositionCotroller = PIDController(P, I, D, 0.1);
        while((encoderCounts - desiredPositionCounts)*(encoderCounts - desiredPositionCounts) > 100){
            CheckErr(flex_load_dac(boardID, NIMC_DAC1, PositionCotroller(desiredPositionCounts, encoderCounts), 0xFF));
            std::cout << PositionCotroller(desiredPositionCounts, encoderCounts) << std::endl;
            std::this_thread::sleep_for(cycle);*/
       // }
    }

     void loadConfiguration()
    {
       // Set the velocity for the move (in counts/sec)
    CheckErr(flex_load_velocity(boardID, axis, 10000, 0xFF));
    // Set the acceleration for the move (in counts/sec^2)
    CheckErr(flex_load_acceleration(boardID, axis, NIMC_ACCELERATION, 100000, 0xFF));


    // Set the deceleration for the move (in counts/sec^2)
    CheckErr(flex_load_acceleration(boardID, axis, NIMC_DECELERATION, 100000, 0xFF));

    // Set the jerk (s-curve value) for the move (in sample periods)
    CheckErr(flex_load_scurve_time(boardID, axis, 100, 0xFF));


    // Set the operation mode to velocity
    CheckErr(flex_set_op_mode(boardID, axis, NIMC_RELATIVE_POSITION));

    }
    void CheckErr(int32 error){
        if(error !=0 ) {
            u32 sizeOfArray;                        //Size of error description
            u16 descriptionType = NIMC_ERROR_ONLY;  //The type of description to be printed
            u16 commandID = 0;
            u16 resourceID = 0;
            sizeOfArray = 0;
            flex_get_error_description(descriptionType, error, commandID, resourceID, NULL, &sizeOfArray );
            // NULL means that we want to get the size of the message, not message itself

            sizeOfArray++;
            std::unique_ptr<i8[]> errorDescription(new i8[sizeOfArray]);

            flex_get_error_description(descriptionType, error, commandID, resourceID, errorDescription.get(), &sizeOfArray);
            throw std::runtime_error(errorDescription.get());
        }
    }
};
nabeel
  • 159
  • 2
  • 3
  • 13

1 Answers1

0

Your question has two parts:

  1. "I'm surprised by the sample rate I'm observing."
  2. "I want to make my control loop rate faster."

1. Sample Rate vs Application Throughput

In your Run_main, you have

pDlg->setCustomData(4,"Sampling rate ",1/timer.getElapsedTime());

which appears to be how you are "checking the sampling rate". That is not reporting the sample rate, but the application throughput.

Querying the sample rate

If you want to ask the driver for the device's sample rate, use

int32 DllExport __CFUNC DAQmxGetSampClkRate(TaskHandle taskHandle, float64 *data);

Understanding application throughput

While the hardware is always sampling data at a constant rate, it still needs to transfer it to the application's memory. That rate does not always match -- sometimes it is slower and sometimes it is faster, depending on other OS tasks and how much CPU your application receives.

What's important is that the average application throughput match the hardware's sample rate. If the application cannot keep up, then eventually the hardware will fill its onboard and in-driver buffer and return an error.

Tuning application throughput

NI-DAQmx has a few different ways to control how and when the hardware transfers data to the application. The two I recommend exploring are

  • Using a callback that fires whenever N samples have been acquired:

    int32 __CFUNC  DAQmxRegisterEveryNSamplesEvent(TaskHandle task, int32 everyNsamplesEventType, uInt32 nSamples, uInt32 options, DAQmxEveryNSamplesEventCallbackPtr callbackFunction, void *callbackData);
    
  • Adjusting how full the device's on-board memory must be before it transfers it to the host

    int32 DllExport __CFUNC DAQmxSetAIDataXferMech(TaskHandle taskHandle, const char channel[], int32 data);
    

See the help for more details.

2. Implementing an Efficient Control Loop

Once you have output that depends on input, you have a control loop and you're implementing a control system. The faster input can be acquired, a new set point calculated, and that new set point sent to the output, the better a system can be controlled.

Base line

The operating system is the foundation for any control system. On the high-performance end of the spectrum are no-OS systems, or "bare metal" systems, like CPLDs, microprocessors, and digital state machines. On the other end are pre-emptive OSes, like Mac, Windows and desktop Linux. And in the middle you'll find real-time operating systems, like QNX, VxWorks, and RTLinux.

Each has different capabilities, and for pre-emptive OSes like Windows, you can expect a maximum loop rate of about 1 kHz (one input, computation, output cycle in 1 ms). The more complex the input, calculation, and output are, the lower the maximum rate.

After the OS, the I/O transport (eg USB vs PCIe vs Ethernet), the I/O driver, and the I/O hardware itself begin to reduce the maximum loop rate. Finally, the end program adds its own delay.

Your question is asking about the program. You have already chosen your OS and I/O, so you are constrained by their limitations.

Tuning Control Loops on Pre-Emptive OSes

On the input side, the only way to make reads faster is to increase the hardware sample rate and decrease the number of samples acquired. For a given sample rate, the time required to collect 100 samples is less than the time required to collect 200 samples. The fewer samples required to calculate the new set point, the faster the loop rate can run.

DAQmx has two approaches for fast reads:

  1. Hardware-timed single point. There are several different ways to use this mode. While Windows supports HWTSP, the DAQ driver and hardware perform even faster with LabVIEW RT (the NI Real-Time OS).

  2. Finite acquisitions. The DAQmx driver has been optimized for quickly restarting finite tasks. Since a finite task enters the stopped state once all of its samples have been acquired, the program only needs to restart the task. Combined with an every-N-samples-callback, the program can rely on the driver telling it data is ready to process and immediately restart the task for the next loop iteration.

On the output side, the NI-Motion driver uses packets to send and receive data from the motor's embedded controller. The API is already tuned for high performance, but NI does have a white paper describing best practices. Perhaps some of those apply to your situation.

Joe Friedrichsen
  • 1,976
  • 14
  • 14
  • thank you for your reply, I have edit the question could you please guide me about that? – nabeel Jan 29 '15 at 02:21
  • I'm worried you'll keep extending your question to cover more and more. Your first revision was "I'm surprised by the sample rate I'm observing." Your second revision is "I want to make my control loop rate faster." While it's the same real-world problem, it's not the same question. – Joe Friedrichsen Jan 29 '15 at 20:13
  • sorry for adding more, right now when I am using only force sensor the throughput of my application is around 10kHz but to get 10KHz I have to reduce no. of samples. so can you just guide me how can I maintain same throughput of 10KHz independent of no. of samples?? – nabeel Jan 30 '15 at 00:43