Как установить частоту дискретизации при считывании данных датчика силы с помощью карты NI DAQ

Я читаю данные датчика силы с помощью карты NI DAQ, я могу прочитать данные отлично, но я не совсем понимаю, как установить желаемую частоту дискретизации. Прямо сейчас, когда я проверяю частоту дискретизации, она дает мне случайное значение, как когда-то 500 Гц, а иногда 50000 Гц.

Вот код, который я использую для чтения данных датчика силы и вычисления частоты дискретизации.

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);
    }

};

Изменить: когда я меняю количество выборок, пропускная способность моего приложения резко уменьшается, в файле ForceSensor.h, если я изменяю количество выборок в функции ниже с 2 на 100, пропускная способность приложения уменьшается до 500 Гц с 10 кГц. я связан с этим.

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;
    }

Я также использую карту NI Motion для управления двигателем, ниже приведен файл main.h, в который я добавил код NI Motion. Когда я добавляю код NI Motion в файл main.h, пропускная способность приложения уменьшается до 200 Гц, независимо от того, какое значение я использовал для количества выборок в датчике силы. Есть ли способ увеличить производительность приложения при одновременном использовании NI Motion для управления двигателем и DAQ для считывания датчика силы?

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());
        }
    }
};

1 ответ

Ваш вопрос состоит из двух частей:

  1. "Я удивлен частотой дискретизации, которую я наблюдаю".
  2. "Я хочу ускорить цикл управления".

1. Частота дискретизации и пропускная способность приложения

В вашем Run_main, у тебя есть

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

Похоже, что вы "проверяете частоту дискретизации". Это не отчет о частоте дискретизации, а о пропускной способности приложения.

Запрос частоты дискретизации

Если вы хотите узнать у драйвера частоту дискретизации устройства, используйте

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

Понимание производительности приложения

Несмотря на то, что аппаратное обеспечение всегда выполняет выборку данных с постоянной скоростью, оно все равно должно передавать их в память приложения. Эта скорость не всегда совпадает - иногда она медленнее, а иногда быстрее, в зависимости от других задач ОС и того, сколько ЦП получает ваше приложение.

Важно, чтобы средняя пропускная способность приложения соответствовала частоте дискретизации оборудования. Если приложение не может работать, то в конечном итоге аппаратное обеспечение заполнит свой встроенный буфер и буфер в драйвере и вернет ошибку.

Настройка пропускной способности приложения

NI-DAQmx имеет несколько различных способов управления тем, как и когда оборудование передает данные в приложение. Два, которые я рекомендую исследовать,

  • Используя обратный вызов, который срабатывает при получении N выборок:

    int32 __CFUNC  DAQmxRegisterEveryNSamplesEvent(TaskHandle task, int32 everyNsamplesEventType, uInt32 nSamples, uInt32 options, DAQmxEveryNSamplesEventCallbackPtr callbackFunction, void *callbackData);
    
  • Настройка степени заполнения встроенной памяти устройства перед его передачей на хост

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

Смотрите помощь для более подробной информации.

2. Реализация эффективного цикла управления

Как только у вас есть выход, который зависит от входа, у вас есть цикл управления, и вы внедряете систему управления. Чем быстрее может быть получен входной сигнал, вычислена новая уставка и чем больше новая уставка отправлена ​​на выход, тем лучше можно управлять системой.

Базовая линия

Операционная система является основой для любой системы управления. На высокопроизводительном конце спектра находятся системы без ОС, или системы "с нуля", такие как CPLD, микропроцессоры и цифровые конечные автоматы. На другом конце находятся приоритетные ОС, такие как Mac, Windows и настольный Linux. И в середине вы найдете операционные системы реального времени, такие как QNX, VxWorks и RTLinux.

Каждая из них обладает различными возможностями, и для приоритетных ОС, таких как Windows, можно ожидать максимальную частоту цикла около 1 кГц (один вход, вычисления, цикл вывода в 1 мс). Чем сложнее ввод, расчет и вывод, тем ниже максимальная скорость.

После ОС транспорт ввода-вывода (например, USB против PCIe против Ethernet), драйвер ввода-вывода и само оборудование ввода-вывода начинают снижать максимальную частоту зацикливания. Наконец, программа завершения добавляет собственную задержку.

Ваш вопрос спрашивает о программе. Вы уже выбрали свою ОС и I/O, поэтому вы ограничены их ограничениями.

Настройка циклов управления в приоритетных ОС

Со стороны ввода единственный способ ускорить чтение - увеличить частоту дискретизации оборудования и уменьшить количество полученных выборок. Для данной частоты выборки время, необходимое для сбора 100 образцов, меньше времени, необходимого для сбора 200 образцов. Чем меньше выборок требуется для расчета нового заданного значения, тем выше скорость цикла.

У DAQmx есть два подхода для быстрого чтения:

  1. Аппаратно-синхронизированная единая точка. Есть несколько разных способов использовать этот режим. Хотя Windows поддерживает HWTSP, драйвер и оборудование DAQ работают еще быстрее с LabVIEW RT (ОС NI Real-Time).

  2. Конечные приобретения. Драйвер DAQmx был оптимизирован для быстрого перезапуска конечных задач. Поскольку конечная задача переходит в состояние останова после получения всех ее выборок, программе нужно только перезапустить задачу. В сочетании с обратным вызовом every-N-samples программа может рассчитывать на драйвер, сообщающий, что данные готовы к обработке, и немедленно перезапустить задачу для следующей итерации цикла.

На стороне выхода драйвер NI-Motion использует пакеты для отправки и получения данных от встроенного контроллера двигателя. API уже настроен на высокую производительность, но у NI есть технический документ, описывающий лучшие практики. Возможно, некоторые из них применимы к вашей ситуации.

Другие вопросы по тегам