从没有中断引脚的传感器读取数据的最佳方法,该措施需要一段时间才能准备好

Ale*_*lek 2 c interrupt stm32 multitasking pressure

我正在尝试将压力传感器(MS5803-14BA)与我的电路板(NUCLEO-STM32L073RZ)连接。

根据数据表(第3页),压力传感器需要几毫秒的时间才能准备好读取测量值。对于我的项目,我对转换原始数据大约需要10 ms的最高分辨率感兴趣。

不幸的是,该压力传感器没有任何可用于查看测量准备就绪时间的中断引脚,因此,我暂时解决了在请求新数据后延迟的问题。

我不喜欢当前的解决方案,因为在这10毫秒内,我可以让单片机工作在其他地方(我的板上还连接了其他几个传感器),但是没有任何中断引脚,我不确定这是什么。解决此问题的最佳方法。

我想到了另一个解决方案:使用计时器,该计时器每20秒触发一次,并执行以下操作:

1.a Read the current value stored in the registers (discarding the first value)
1.b Ask for a new value
Run Code Online (Sandbox Code Playgroud)

这样,在下一次迭代中,我只需要读取上一次迭代结束时请求的值即可。

我不喜欢我的测量结果总是20毫秒。直到延迟保持20毫秒,它应该还是可以的,但是如果我需要降低速率,则解决方案的读数“老化”会增加。

您对如何处理还有其他想法吗?

谢谢。

注意:如果您需要查看我当前的实现,请告诉我。

Gab*_*les 5

这不是一个“如何读取传感器”的问题,这是一个“如何进行无阻塞协作式多任务处理”的问题。假设您正在运行裸机(没有操作系统,例如FreeRTOS),则有两个不错的选择。

首先,数据表显示您需要等待最多9.04 ms或9040 us。 在此处输入图片说明

现在,这是您的协作式多任务选项:

  1. 发送命令告诉设备进行ADC转换(即进行模拟测量),然后配置硬件定时器以在稍后9040 us完全中断您。然后,在ISR中,您可以设置一个标志来告诉您的主循环发送读取命令以读取结果,或者您可以只在ISR内部发送读取命令。

  2. 在主循环中使用非阻塞的基于时间戳的协作式多任务处理。这可能需要基本状态机。发送转换命令,然后继续执行其他操作。当您的时间戳指示已经足够长时,请发送读取命令以从传感器读取转换后的结果。

上面的1号是我对时间紧迫的任务的首选方法。但是,这不是时间紧迫的,并且一点抖动不会有任何区别,因此,上面的数字2是我通常用于裸机协作多任务处理的首选方法,所以让我们这样做。

这是一个示例程序,用于演示您需要在特定情况下基于时间戳的裸机协作多任务处理的原理:

  1. 请求数据样本(在外部传感器中启动ADC转换)
  2. 等待9040我们完成转换
  3. 从外部传感器读取数据样本(现在ADC转换已完成)

码:

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
Run Code Online (Sandbox Code Playgroud)

有关另一个非常简单的基于时间戳的多任务示例,请参见Arduino的“无延迟闪烁”示例

基于时间戳的常规裸机协作多任务体系结构注释:

最终,根据您的操作方式,基本上可以得到这种类型的代码布局,该代码布局仅以固定的时间间隔运行每个任务。每个任务都应该是非阻塞的,以确保它与其他任务的运行间隔不冲突。对裸机的非阻塞意味着“不要使用浪费时钟的延迟,繁忙循环或其他类型的轮询,重复,计数或繁忙延迟!”。(这与基于操作系统(基于OS)的系统上的“阻塞”相对,这意味着“将时钟发回给调度程序,以使其在此任务“休眠”时运行另一个线程。”记住:裸机手段没有操作系统!)。相反,如果尚未准备好运行某些东西,只需通过状态机保存状态,退出此任务的代码(这是“合作”部分,因为您的任务必须通过返回自动放弃处理器),然后让另一个任务运行!

这是基本的体系结构,显示了一种基于时间戳的简单方法,该方法使3个任务以独立,固定的频率运行,而无需依赖任何中断,并且具有最小的抖动,这是由于我采用了彻底而有条理的方法来检查时间戳和更新时间戳每个运行时间的开始时间。

1,main()函数和主循环的定义:

int main(void)
{
    doSetupStuff();
    configureHardwareTimer();

    while (1)
    {
        doTask1();
        doTask2();
        doTask3();
    }
}
Run Code Online (Sandbox Code Playgroud)

2,doTask()功能定义:

// Task 1: Let's run this one at 100 Hz (every 10ms)
void doTask1(void)
{
    const uint32_t DT_DESIRED_US = 10000; // 10000us = 10ms, or 100Hz run freq
    static uint32_t t_start_us = getMicros();
    uint32_t t_now_us = getMicros();
    uint32_t dt_us = t_now_us - t_start_us;

    // See if it's time to run this Task
    if (dt_us >= DT_DESIRED_US)
    {
        // 1. Add DT_DESIRED_US to t_start_us rather than setting t_start_us to t_now_us (which many 
        // people do) in order to ***avoid introducing artificial jitter into the timing!***
        t_start_us += DT_DESIRED_US;
        // 2. Handle edge case where it's already time to run again because just completing one of the main
        // "scheduler" loops in the main() function takes longer than DT_DESIRED_US; in other words, here
        // we are seeing that t_start_us is lagging too far behind (more than one DT_DESIRED_US time width
        // from t_now_us), so we are "fast-forwarding" t_start_us up to the point where it is exactly 
        // 1 DT_DESIRED_US time width back now, thereby causing this task to instantly run again the 
        // next time it is called (trying as hard as we can to run at the specified frequency) while 
        // at the same time protecting t_start_us from lagging farther and farther behind, as that would
        // eventually cause buggy and incorrect behavior when the (unsigned) timestamps start to roll over
        // back to zero.
        dt_us = t_now_us - t_start_us; // calculate new time delta with newly-updated t_start_us
        if (dt_us >= DT_DESIRED_US)
        {
            t_start_us = t_now_us - DT_DESIRED_US;
        }

        // PERFORM THIS TASK'S OPERATIONS HERE!

    }
}

// Task 2: Let's run this one at 1000 Hz (every 1ms)
void doTask2(void)
{
    const uint32_t DT_DESIRED_US = 1000; // 1000us = 1ms, or 1000Hz run freq
    static uint32_t t_start_us = getMicros();
    uint32_t t_now_us = getMicros();
    uint32_t dt_us = t_now_us - t_start_us;

    // See if it's time to run this Task
    if (dt_us >= DT_DESIRED_US)
    {
        t_start_us += DT_DESIRED_US;
        dt_us = t_now_us - t_start_us; // calculate new time delta with newly-updated t_start_us
        if (dt_us >= DT_DESIRED_US)
        {
            t_start_us = t_now_us - DT_DESIRED_US;
        }

        // PERFORM THIS TASK'S OPERATIONS HERE!

    }
}

// Task 3: Let's run this one at 10 Hz (every 100ms)
void doTask3(void)
{
    const uint32_t DT_DESIRED_US = 100000; // 100000us = 100ms, or 10Hz run freq
    static uint32_t t_start_us = getMicros();
    uint32_t t_now_us = getMicros();
    uint32_t dt_us = t_now_us - t_start_us;

    // See if it's time to run this Task
    if (dt_us >= DT_DESIRED_US)
    {
        t_start_us += DT_DESIRED_US;
        dt_us = t_now_us - t_start_us; // calculate new time delta with newly-updated t_start_us
        if (dt_us >= DT_DESIRED_US)
        {
            t_start_us = t_now_us - DT_DESIRED_US;
        }

        // PERFORM THIS TASK'S OPERATIONS HERE!

    }
}
Run Code Online (Sandbox Code Playgroud)

嵌入式裸机微控制器编程的一部分技术(而且很有趣!)是决定确切地希望如何交错每个任务并使它们一起运行时的技能和独创性,就像它们在并行运行一样。使用以上格式作为起点,并适应您的特定情况。可以根据需要以及特定应用程序的需要,在任务之间或任务与中断之间,任务与用户之间添加消息传递。

这是一个如何配置定时器以用作STM32F2微控制器上的时间戳生成器的示例。

这显示了上面用于configureHardwareTimer()和的功能getMicros()

// 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);
}
Run Code Online (Sandbox Code Playgroud)

参考文献:

  1. https://www.arduino.cc/en/tutorial/BlinkWithoutDelay