Best way to read from a sensors that doesn't have interrupt pin and require some time before the measure is ready

跟風遠走 提交于 2019-12-01 11:37:05

This isn't a "how to read a sensor" problem, this is a "how to do non-blocking cooperative multi-tasking" problem. Assuming you are running bare-metal (no operating system, such as FreeRTOS), you have two good options.

First, the datasheet shows you need to wait up to 9.04 ms, or 9040 us.

Now, here are your cooperative multi-tasking options:

  1. Send a command to tell the device to do an ADC conversion (ie: to take an analog measurement), then configure a hardware timer to interrupt you exactly 9040 us later. In your ISR you then can either set a flag to tell your main loop to send a read command to read the result, OR you can just send the read command right inside the ISR.

  2. Use non-blocking time-stamp-based cooperative multi-tasking in your main loop. This will likely require a basic state machine. Send the conversion command, then move on, doing other things. When your time stamp indicates it's been long enough, send the read command to read the converted result from the sensor.

Number 1 above is my preferred approach for time-critical tasks. This isn't time-critical, however, and a little jitter won't make any difference, so Number 2 above is my preferred approach for general, bare-metal cooperative multi-tasking, so let's do that.

Here's a sample program to demonstrate the principle of time-stamp-based bare-metal cooperative multi-tasking for your specific case where you need to:

  1. request a data sample (start ADC conversion in your external sensor)
  2. wait 9040 us for the conversion to complete
  3. read in the data sample from your external sensor (now that the ADC conversion is complete)

Code:

enum sensorState_t 
{
    SENSOR_START_CONVERSION,
    SENSOR_WAIT,
    SENSOR_GET_CONVERSION
}

int main(void)
{
    doSetupStuff();
    configureHardwareTimer(); // required for getMicros() to work

    while (1)
    {
        //
        // COOPERATIVE TASK #1
        // Read the under-water pressure sensor as fast as permitted by the datasheet
        //
        static sensorState_t sensorState = SENSOR_START_CONVERSION; // initialize state machine
        static uint32_t task1_tStart; // us; start time
        static uint32_t sensorVal; // the sensor value you are trying to obtain 
        static bool newSensorVal = false; // set to true whenever a new value arrives
        switch (sensorState)
        {
            case SENSOR_START_CONVERSION:
            {
                startConversion(); // send command to sensor to start ADC conversion
                task1_tStart = getMicros(); // get a microsecond time stamp
                sensorState = SENSOR_WAIT; // next state 
                break;
            }
            case SENSOR_WAIT:
            {
                const uint32_t DESIRED_WAIT_TIME = 9040; // us
                uint32_t tNow = getMicros();
                if (tNow - task1_tStart >= DESIRED_WAIT_TIME)
                {
                    sensorState = SENSOR_GET_CONVERSION; // next state
                }
                break;
            }
            case SENSOR_GET_CONVERSION:
            {
                sensorVal = readConvertedResult(); // send command to read value from the sensor
                newSensorVal = true;
                sensorState = SENSOR_START_CONVERSION; // next state 
                break;
            }
        }

        //
        // COOPERATIVE TASK #2
        // use the under-water pressure sensor data right when it comes in (this will be an event-based task
        // whose running frequency depends on the rate of new data coming in, for example)
        //
        if (newSensorVal == true)
        {
            newSensorVal = false; // reset this flag 

            // use the sensorVal data here now for whatever you need it for
        }


        //
        // COOPERATIVE TASK #3
        //


        //
        // COOPERATIVE TASK #4
        //


        // etc etc

    } // end of while (1)
} // end of main

For another really simple timestamp-based multi-tasking example see Arduino's "Blink Without Delay" example here.

Here's an example of how to configure a timer for use as a timestamp-generator on an STM32F2 microcontroller.

This shows functions for configureHardwareTimer() and getMicros(), used above:

// Timer handle to be used for Timer 2 below
TIM_HandleTypeDef TimHandle;

// Configure Timer 2 to be used as a free-running 32-bit hardware timer for general-purpose use as a 1-us-resolution
// timestamp source
void configureHardwareTimer()
{
    // Timer clock must be enabled before you can configure it
    __HAL_RCC_TIM2_CLK_ENABLE();

    // Calculate prescaler
    // Here are some references to show how this is done:
    // 1) "STM32Cube_FW_F2_V1.7.0/Projects/STM32F207ZG-Nucleo/Examples/TIM/TIM_OnePulse/Src/main.c" shows the
    //    following (slightly modified) equation on line 95: `Prescaler = (TIMxCLK/TIMx_counter_clock) - 1`
    // 2) "STM32F20x and STM32F21x Reference Manual" states the following on pg 419: "14.4.11 TIMx prescaler (TIMx_PSC)"
    //    "The counter clock frequency CK_CNT is equal to fCK_PSC / (PSC[15:0] + 1)"
    //    This means that TIMx_counter_clock_freq = TIMxCLK/(prescaler + 1). Now, solve for prescaler and you
    //    get the exact same equation as above: `prescaler = TIMxCLK/TIMx_counter_clock_freq - 1`
    // Calculating TIMxCLK:
    // - We must divide SystemCoreClock (returned by HAL_RCC_GetHCLKFreq()) by 2 because TIM2 uses clock APB1
    // as its clock source, and on my board this is configured to be 1/2 of the SystemCoreClock.
    // - Note: To know which clock source each peripheral and timer uses, you can look at
    //  "Table 25. Peripheral current consumption" in the datasheet, p86-88.
    const uint32_t DESIRED_TIMER_FREQ = 1e6; // 1 MHz clock freq --> 1 us pd per tick, which is what I want
    uint32_t Tim2Clk = HAL_RCC_GetHCLKFreq() / 2;
    uint32_t prescaler = Tim2Clk / DESIRED_TIMER_FREQ - 1; // Don't forget the minus 1!

    // Configure timer
    // TIM2 is a 32-bit timer; See datasheet "Table 4. Timer feature comparison", p30-31
    TimHandle.Instance               = TIM2;
    TimHandle.Init.Period            = 0xFFFFFFFF; // Set pd to max possible for a 32-bit timer
    TimHandle.Init.Prescaler         = prescaler;
    TimHandle.Init.ClockDivision     = TIM_CLOCKDIVISION_DIV1;
    TimHandle.Init.CounterMode       = TIM_COUNTERMODE_UP;
    TimHandle.Init.RepetitionCounter = 0; // NA (has no significance) for this timer

    // Initialize the timer
    if (HAL_TIM_Base_Init(&TimHandle) != HAL_OK)
    {
        // handle error condition
    }

    // Start the timer
    if (HAL_TIM_Base_Start(&TimHandle) != HAL_OK)
    {
        // handle error condition
    }
}

// Get the 1 us count value on Timer 2.
// This timer will be used for general purpose hardware timing that does NOT rely on interrupts.
// Therefore, the counter will continue to increment even with interrupts disabled.
// The count value increments every 1 microsecond.
// Since it is a 32-bit counter it overflows every 2^32 counts, which means the highest value it can
// store is 2^32 - 1 = 4294967295. Overflows occur every 2^32 counts / 1 count/us / 1e6us/sec
// = ~4294.97 sec = ~71.6 min.
uint32_t getMicros()
{
    return __HAL_TIM_GET_COUNTER(&TimHandle);
}

First of all thank you for your suggestions. I tried to analyze every single possible solution you proposed.

The solution proposed by Peter seemed very interesting but I have to say that, after having gone through the datasheet several times, I don't believe that is feasible. My consideration is based on the following facts.

Using a scope I see that the acknowledge is received right after sending the command for doing conversion. See following image concerning the temperature conversion:

It seems quite clear to me the acknowledge bit right after the command. After that the SDA line (yellow) goes high, therefore I don't see how it is possible that I can exploit that for detecting when the conversion is ready.

Concerning the solution when using SPI, yes, the SDO remains low during the conversion, but I cannot use it: I need to stick with I2C. Furthermore, I have other sensors attached to that SPI bus and I agree with what Gabriel Staples says.

After my consideration I went for the solution proposed by Gabriel Staples (considering that, in order to read pressure value, I also need to read and convert temperature).

My current solution is based on a state machine with 6 states. In my solution, I distinguish between the wait time for the pressure conversion and the wait time for the temperature conversion with the idea the I could try to see how much the pressure reading degrades if I use a less precise temperature reading.

Here is my current solution. The following function is called inside the main while:

void MS5803_update()
{
  static uint32_t tStart; // us; start time

  switch (sensor_state)
  {
    case MS5803_REQUEST_TEMPERATURE:
    {
        MS5803_send_command(MS5803_CMD_ADC_CONV + TEMPERATURE + baro.resolution);
        tStart = HAL_GetTick();
        sensor_state = MS5803_WAIT_RAW_TEMPERATURE;
        break;
    }

    case MS5803_WAIT_RAW_TEMPERATURE:
    {
        uint32_t tNow = HAL_GetTick();
        if (tNow - tStart >= conversion_time)
        {
            sensor_state = MS5803_CONVERTING_TEMPERATURE;
        }
        break;
    }

    case MS5803_CONVERTING_TEMPERATURE:
    {
        MS5803_send_command(MS5803_CMD_ADC_READ);
        uint8_t raw_value[3]; // Read 24 bit
        MS5803_read_value(raw_value,3);
        temperature_raw = ((uint32_t)raw_value[0] << 16) + ((uint32_t)raw_value[1] << 8) + raw_value[2];
        sensor_state = MS5803_REQUEST_PRESSURE;
        break;
    }

    case MS5803_REQUEST_PRESSURE:
    {
        MS5803_send_command(MS5803_CMD_ADC_CONV + PRESSURE + baro.resolution);
        tStart = HAL_GetTick();
        sensor_state = MS5803_WAIT_RAW_PRESSURE;
        break;
    }

    case MS5803_WAIT_RAW_PRESSURE:
    {
        uint32_t tNow = HAL_GetTick();
        if (tNow - tStart >= conversion_time)
        {
            sensor_state = MS5803_CONVERTING_PRESSURE;
        }
        break;
    }

    case MS5803_CONVERTING_PRESSURE:
    {
        MS5803_send_command(MS5803_CMD_ADC_READ);
        uint8_t raw_value[3]; // Read 24 bit
        MS5803_read_value(raw_value,3);
        pressure_raw = ((uint32_t)raw_value[0] << 16) + ((uint32_t)raw_value[1] << 8) + raw_value[2];

        // Now I have both temperature and pressure raw and I can convert them
        MS5803_updateMeasurements();

        // Reset the state machine to perform a new measurement
        sensor_state = MS5803_REQUEST_TEMPERATURE;
        break;
    }
  }
}

I don't pretend that my solution is better. I just post it in order to have an opinion from you guys. Note: I'm still working on it. Therefore I cannot guarantee is bug-free!

For PeterJ_01: I could agree that this is not strictly a teaching portal, but I believe that everybody around here asks questions to learn something new or to improve theirselves. Therefore, if you believe that the solution using the ack is better, it would be great if you could show us a draft of your idea. For me it would be something new to learn.

Any further comment is appreciated.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!