从没有中断引脚并需要一些时间才能准备好测量的传感器读取的最佳方法
Best way to read from a sensor that doesn't have interrupt pin and requires some time before the measurement is ready
我正在尝试将压力传感器 (MS5803-14BA) 连接到我的电路板 (NUCLEO-STM32L073RZ)。
根据 datasheet(第 3 页),压力传感器需要几毫秒才能准备好读取测量值。对于我的项目,我会对原始数据转换需要大约 10 毫秒的最高分辨率感兴趣。
不幸的是,这个压力传感器没有任何中断引脚可以用来查看何时准备好测量,因此我暂时解决了在请求新数据后延迟的问题。
我不喜欢我目前的解决方案,因为在这 10 毫秒内我可以让 MCU 处理其他事情(我的板上连接了几个其他传感器),但没有任何中断引脚,我不是确定解决此问题的最佳方法是什么。
我想到了另一个解决方案:使用一个定时器,每隔 20 毫秒触发一次并执行以下操作:
1.a Read the current value stored in the registers (discarding the first value)
1.b Ask for a new value
这样,在下一次迭代时,我只需要读取上一次迭代结束时请求的值。
我不喜欢的是我的测量值总是 20 毫秒。直到延迟保持 20 毫秒,它应该仍然可以,但是如果我需要降低速率,我的解决方案的读数 "age" 会增加。
对于如何处理这个问题,您还有其他想法吗?
谢谢。
注意:如果您需要查看我当前的实现,请告诉我。
如何实现高分辨率、基于时间戳、非阻塞、单线程协同多任务
这不是“如何读取传感器”问题,而是“如何进行非阻塞协作多任务处理”问题。假设您是运行ning 裸机(无操作系统,如FreeRTOS),你有两个不错的选择
首先,数据表显示您最多需要等待 9.04 毫秒,或 9040 微秒。
现在,这是您的合作多任务选项:
发送命令告诉设备进行 ADC 转换(即:进行模拟测量),然后配置硬件定时器以在 9040 us 后准确中断您。在你的 ISR 中,你可以设置一个标志来告诉你的主循环发送一个读取命令来读取结果,或者你可以直接在 ISR 中发送读取命令。
在主循环中使用基于时间戳的非阻塞协作多任务处理。这可能需要一个基本的状态机。发送转换命令,然后继续,做其他事情。当您的时间戳表明它已经足够长时,发送读取命令以从传感器读取转换后的结果。
上面的第 1 条是我处理时间紧迫任务的首选方法。然而,这不是时间关键的,一点点抖动也不会有任何影响,所以上面的数字 2 是我首选的一般裸机协作多任务处理方法,所以让我们这样做吧。
这是一个示例程序,用于针对您需要的特定情况演示基于时间戳的裸机协作多任务处理的原理:
- 请求数据样本(在您的外部传感器中启动 ADC 转换)
- 等待 9040 us 转换完成
- 从外部传感器读取数据样本(现在 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
有关另一个非常简单的基于时间戳的多任务示例,请参阅 Arduino's "Blink Without Delay" example here。
一般基于时间戳的裸机协作多任务处理架构说明:
取决于你如何做,最后,你基本上会得到这种类型的代码布局,它只是 运行 以固定的时间间隔执行每个任务。每个任务都应该 非阻塞 以确保它不会与其他任务的 运行 间隔冲突。裸机上的非阻塞意味着“不要使用时钟浪费延迟、繁忙循环或其他类型的轮询、重复、计数或繁忙延迟!”。 (这与基于操作系统(OS 的)系统上的“阻塞”相反,这意味着“将时钟返回给调度程序,让它 运行 另一个线程,而此任务 'sleeps'。”请记住:裸机 表示无操作系统!)。相反,如果某些东西还没有完全准备好 运行,只需通过状态机保存您的状态,退出此任务的代码(这是“合作”部分,因为您的任务必须通过返回来自愿放弃处理器), 让另一个任务 运行!
这是基本架构,显示了一种简单的基于时间戳的方法,可以让 3 个任务以独立的固定频率 运行 不依赖任何中断,并且 最小抖动,由于我采用彻底和有条不紊的方法来检查时间戳并在每个 运行 时间更新开始时间。
1、main()
函数和主循环的定义:
int main(void)
{
doSetupStuff();
configureHardwareTimer();
while (1)
{
doTask1();
doTask2();
doTask3();
}
}
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!
}
}
上面的代码工作得很好,但如您所见,它非常多余,而且设置新任务时有点烦人。通过简单地定义一个宏 CREATE_TASK_TIMER()
,这项工作可以更加自动化并且更容易完成,如下所示,为我们完成所有冗余计时工作和时间戳变量创建:
/// @brief A function-like macro to get a certain set of events to run at a desired, fixed
/// interval period or frequency.
/// @details This is a timestamp-based time polling technique frequently used in bare-metal
/// programming as a basic means of achieving cooperative multi-tasking. Note
/// that getting the timing details right is difficult, hence one reason this macro
/// is so useful. The other reason is that this maro significantly reduces the number of
/// lines of code you need to write to introduce a new timestamp-based cooperative
/// task. The technique used herein achieves a perfect desired period (or freq)
/// on average, as it centers the jitter inherent in any polling technique around
/// the desired time delta set-point, rather than always lagging as many other
/// approaches do.
///
/// USAGE EX:
/// ```
/// // Create a task timer to run at 500 Hz (every 2000 us, or 2 ms; 1/0.002 sec = 500 Hz)
/// const uint32_t PERIOD_US = 2000; // 2000 us pd --> 500 Hz freq
/// bool time_to_run;
/// CREATE_TASK_TIMER(PERIOD_US, time_to_run);
/// if (time_to_run)
/// {
/// run_task_2();
/// }
/// ```
///
/// Source: Gabriel Staples
///
/// @param[in] dt_desired_us The desired delta time period, in microseconds; note: pd = 1/freq;
/// the type must be `uint32_t`
/// @param[out] time_to_run A `bool` whose scope will enter *into* the brace-based scope block
/// below; used as an *output* flag to the caller: this variable will
/// be set to true if it is time to run your code, according to the
/// timestamps, and will be set to false otherwise
/// @return NA--this is not a true function
#define CREATE_TASK_TIMER(dt_desired_us, time_to_run) \
{ /* Use scoping braces to allow multiple calls of this macro all in one outer scope while */ \
/* allowing each variable created below to be treated as unique to its own scope */ \
time_to_run = false; \
\
/* set the desired run pd / freq */ \
const uint32_t DT_DESIRED_US = dt_desired_us; \
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; \
} \
\
time_to_run = true; \
} \
}
现在,有多种使用方法,但为了演示,为了保持真正干净的 main()
循环代码,如下所示:
int main(void)
{
doSetupStuff();
configureHardwareTimer();
while (1)
{
doTask1();
doTask2();
doTask3();
}
}
让我们像这样使用 CREATE_TASK_TIMER()
宏。如您所见,代码现在更清晰,更容易设置新任务。 这是我的首选方法,因为它创建了上面显示的真正干净的主循环,仅包含各种 doTask()
调用,这些调用也易于编写和维护:
// Task 1: Let's run this one at 100 Hz (every 10ms, or 10000us)
void doTask1(void)
{
bool time_to_run;
const uint32_t DT_DESIRED_US = 10000; // 10000us = 10ms, or 100Hz run freq
CREATE_TASK_TIMER(DT_DESIRED_US, time_to_run);
if (time_to_run)
{
// PERFORM THIS TASK'S OPERATIONS HERE!
}
}
// Task 2: Let's run this one at 1000 Hz (every 1ms)
void doTask2(void)
{
bool time_to_run;
const uint32_t DT_DESIRED_US = 1000; // 1000us = 1ms, or 1000Hz run freq
CREATE_TASK_TIMER(DT_DESIRED_US, time_to_run);
if (time_to_run)
{
// PERFORM THIS TASK'S OPERATIONS HERE!
}
}
// Task 3: Let's run this one at 10 Hz (every 100ms)
void doTask3(void)
{
bool time_to_run;
const uint32_t DT_DESIRED_US = 100000; // 100000us = 100ms, or 10Hz run freq
CREATE_TASK_TIMER(DT_DESIRED_US, time_to_run);
if (time_to_run)
{
// PERFORM THIS TASK'S OPERATIONS HERE!
}
}
但是,您也可以像这样构建代码,它同样有效并产生相同的效果,只是方式略有不同:
#include <stdbool.h>
#include <stdint.h>
#define TASK1_PD_US (10000) // 10ms pd, or 100 Hz run freq
#define TASK2_PD_US (1000) // 1ms pd, or 1000 Hz run freq
#define TASK3_PD_US (100000) // 100ms pd, or 10 Hz run freq
// Task 1: Let's run this one at 100 Hz (every 10ms, or 10000us)
void doTask1(void)
{
// PERFORM THIS TASK'S OPERATIONS HERE!
}
// Task 2: Let's run this one at 1000 Hz (every 1ms)
void doTask2(void)
{
// PERFORM THIS TASK'S OPERATIONS HERE!
}
// Task 3: Let's run this one at 10 Hz (every 100ms)
void doTask3(void)
{
// PERFORM THIS TASK'S OPERATIONS HERE!
}
int main(void)
{
doSetupStuff();
configureHardwareTimer();
while (1)
{
bool time_to_run;
CREATE_TASK_TIMER(TASK1_PD_US, time_to_run);
if (time_to_run)
{
doTask1();
}
CREATE_TASK_TIMER(TASK2_PD_US, time_to_run);
if (time_to_run)
{
doTask2();
}
CREATE_TASK_TIMER(TASK3_PD_US, time_to_run);
if (time_to_run)
{
doTask3();
}
}
}
嵌入式裸机微控制器编程的部分艺术(和乐趣!)在于决定您希望如何交错每个任务并将它们 运行 放在一起的技巧和独创性,所有作为尽管它们是 运行ning 并行的。使用上述格式之一作为起点,并适应您的特定情况。消息传递可以根据需要在任务之间或任务与中断、任务与用户等之间添加,并根据您的特定应用程序的需要添加。
下面是一个示例,说明如何配置定时器以用作 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);
}
参考文献:
- https://www.arduino.cc/en/tutorial/BlinkWithoutDelay
- 氧气:What's the right way to reference a parameter in Doxygen?
- 用于错误处理的基于枚举的错误代码:Error handling in C code
- C 中的其他架构风格,例如通过不透明指针的“基于对象的”C:Opaque C structs: various ways to declare them
另请参阅:
- 一个完整的、运行可用的 Arduino 示例,其中包含我上面的
CREATE_TASK_TIMER()
宏的更好版本:
- 我对 C 和 C++ 的回答,包括微控制器和 Arduino(或任何其他系统):Full coulomb counter example demonstrating the above concept with timestamp-based, single-threaded, cooperative multi-tasking
首先感谢您的建议。我试图分析您提出的每一个可能的解决方案。
Peter 提出的解决方案看起来很有趣,但我不得不说,在多次查看数据表后,我认为这是不可行的。我的考虑是基于以下事实。
使用示波器,我看到在发送进行转换的命令后立即收到了确认。关于温度转换见下图:
命令之后的确认位对我来说似乎很清楚。之后 SDA 线(黄色)变高,因此我看不出如何利用它来检测转换何时准备就绪。
关于使用SPI时的解决方案,是的,SDO在转换过程中保持低电平,但我不能使用它:我需要坚持使用I2C。此外,我还有其他传感器连接到该 SPI 总线,我同意 Gabriel Staples 所说的。
经过深思熟虑,我选择了Gabriel Staples提出的解决方案(考虑到为了读取压力值,我还需要读取并转换温度)。
我当前的解决方案基于具有 6 个状态的状态机。在我的解决方案中,我区分了压力转换的等待时间和温度转换的等待时间,我的想法是如果我使用不太精确的温度读数,我可以尝试查看压力读数降低了多少。
这是我目前的解决方案。下面的函数在 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;
}
}
}
我不假装我的解决方案更好。我只是 post 它是为了征求你们的意见。注意:我仍在努力。因此我不能保证没有错误!
对于 PeterJ_01:我同意这不是严格意义上的教学门户,但我相信这里的每个人都会提出问题来学习新知识或提高自己。因此,如果您认为使用 ack 的解决方案更好,那么如果您能向我们展示您的想法草稿就太好了。对我来说,这将是一个新的学习点。
如有任何进一步的评论,我们将不胜感激。
我正在尝试将压力传感器 (MS5803-14BA) 连接到我的电路板 (NUCLEO-STM32L073RZ)。
根据 datasheet(第 3 页),压力传感器需要几毫秒才能准备好读取测量值。对于我的项目,我会对原始数据转换需要大约 10 毫秒的最高分辨率感兴趣。
不幸的是,这个压力传感器没有任何中断引脚可以用来查看何时准备好测量,因此我暂时解决了在请求新数据后延迟的问题。
我不喜欢我目前的解决方案,因为在这 10 毫秒内我可以让 MCU 处理其他事情(我的板上连接了几个其他传感器),但没有任何中断引脚,我不是确定解决此问题的最佳方法是什么。
我想到了另一个解决方案:使用一个定时器,每隔 20 毫秒触发一次并执行以下操作:
1.a Read the current value stored in the registers (discarding the first value)
1.b Ask for a new value
这样,在下一次迭代时,我只需要读取上一次迭代结束时请求的值。
我不喜欢的是我的测量值总是 20 毫秒。直到延迟保持 20 毫秒,它应该仍然可以,但是如果我需要降低速率,我的解决方案的读数 "age" 会增加。
对于如何处理这个问题,您还有其他想法吗?
谢谢。
注意:如果您需要查看我当前的实现,请告诉我。
如何实现高分辨率、基于时间戳、非阻塞、单线程协同多任务
这不是“如何读取传感器”问题,而是“如何进行非阻塞协作多任务处理”问题。假设您是运行ning 裸机(无操作系统,如FreeRTOS),你有两个不错的选择
首先,数据表显示您最多需要等待 9.04 毫秒,或 9040 微秒。
现在,这是您的合作多任务选项:
发送命令告诉设备进行 ADC 转换(即:进行模拟测量),然后配置硬件定时器以在 9040 us 后准确中断您。在你的 ISR 中,你可以设置一个标志来告诉你的主循环发送一个读取命令来读取结果,或者你可以直接在 ISR 中发送读取命令。
在主循环中使用基于时间戳的非阻塞协作多任务处理。这可能需要一个基本的状态机。发送转换命令,然后继续,做其他事情。当您的时间戳表明它已经足够长时,发送读取命令以从传感器读取转换后的结果。
上面的第 1 条是我处理时间紧迫任务的首选方法。然而,这不是时间关键的,一点点抖动也不会有任何影响,所以上面的数字 2 是我首选的一般裸机协作多任务处理方法,所以让我们这样做吧。
这是一个示例程序,用于针对您需要的特定情况演示基于时间戳的裸机协作多任务处理的原理:
- 请求数据样本(在您的外部传感器中启动 ADC 转换)
- 等待 9040 us 转换完成
- 从外部传感器读取数据样本(现在 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
有关另一个非常简单的基于时间戳的多任务示例,请参阅 Arduino's "Blink Without Delay" example here。
一般基于时间戳的裸机协作多任务处理架构说明:
取决于你如何做,最后,你基本上会得到这种类型的代码布局,它只是 运行 以固定的时间间隔执行每个任务。每个任务都应该 非阻塞 以确保它不会与其他任务的 运行 间隔冲突。裸机上的非阻塞意味着“不要使用时钟浪费延迟、繁忙循环或其他类型的轮询、重复、计数或繁忙延迟!”。 (这与基于操作系统(OS 的)系统上的“阻塞”相反,这意味着“将时钟返回给调度程序,让它 运行 另一个线程,而此任务 'sleeps'。”请记住:裸机 表示无操作系统!)。相反,如果某些东西还没有完全准备好 运行,只需通过状态机保存您的状态,退出此任务的代码(这是“合作”部分,因为您的任务必须通过返回来自愿放弃处理器), 让另一个任务 运行!
这是基本架构,显示了一种简单的基于时间戳的方法,可以让 3 个任务以独立的固定频率 运行 不依赖任何中断,并且 最小抖动,由于我采用彻底和有条不紊的方法来检查时间戳并在每个 运行 时间更新开始时间。
1、main()
函数和主循环的定义:
int main(void)
{
doSetupStuff();
configureHardwareTimer();
while (1)
{
doTask1();
doTask2();
doTask3();
}
}
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!
}
}
上面的代码工作得很好,但如您所见,它非常多余,而且设置新任务时有点烦人。通过简单地定义一个宏 CREATE_TASK_TIMER()
,这项工作可以更加自动化并且更容易完成,如下所示,为我们完成所有冗余计时工作和时间戳变量创建:
/// @brief A function-like macro to get a certain set of events to run at a desired, fixed
/// interval period or frequency.
/// @details This is a timestamp-based time polling technique frequently used in bare-metal
/// programming as a basic means of achieving cooperative multi-tasking. Note
/// that getting the timing details right is difficult, hence one reason this macro
/// is so useful. The other reason is that this maro significantly reduces the number of
/// lines of code you need to write to introduce a new timestamp-based cooperative
/// task. The technique used herein achieves a perfect desired period (or freq)
/// on average, as it centers the jitter inherent in any polling technique around
/// the desired time delta set-point, rather than always lagging as many other
/// approaches do.
///
/// USAGE EX:
/// ```
/// // Create a task timer to run at 500 Hz (every 2000 us, or 2 ms; 1/0.002 sec = 500 Hz)
/// const uint32_t PERIOD_US = 2000; // 2000 us pd --> 500 Hz freq
/// bool time_to_run;
/// CREATE_TASK_TIMER(PERIOD_US, time_to_run);
/// if (time_to_run)
/// {
/// run_task_2();
/// }
/// ```
///
/// Source: Gabriel Staples
///
/// @param[in] dt_desired_us The desired delta time period, in microseconds; note: pd = 1/freq;
/// the type must be `uint32_t`
/// @param[out] time_to_run A `bool` whose scope will enter *into* the brace-based scope block
/// below; used as an *output* flag to the caller: this variable will
/// be set to true if it is time to run your code, according to the
/// timestamps, and will be set to false otherwise
/// @return NA--this is not a true function
#define CREATE_TASK_TIMER(dt_desired_us, time_to_run) \
{ /* Use scoping braces to allow multiple calls of this macro all in one outer scope while */ \
/* allowing each variable created below to be treated as unique to its own scope */ \
time_to_run = false; \
\
/* set the desired run pd / freq */ \
const uint32_t DT_DESIRED_US = dt_desired_us; \
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; \
} \
\
time_to_run = true; \
} \
}
现在,有多种使用方法,但为了演示,为了保持真正干净的 main()
循环代码,如下所示:
int main(void)
{
doSetupStuff();
configureHardwareTimer();
while (1)
{
doTask1();
doTask2();
doTask3();
}
}
让我们像这样使用 CREATE_TASK_TIMER()
宏。如您所见,代码现在更清晰,更容易设置新任务。 这是我的首选方法,因为它创建了上面显示的真正干净的主循环,仅包含各种 doTask()
调用,这些调用也易于编写和维护:
// Task 1: Let's run this one at 100 Hz (every 10ms, or 10000us)
void doTask1(void)
{
bool time_to_run;
const uint32_t DT_DESIRED_US = 10000; // 10000us = 10ms, or 100Hz run freq
CREATE_TASK_TIMER(DT_DESIRED_US, time_to_run);
if (time_to_run)
{
// PERFORM THIS TASK'S OPERATIONS HERE!
}
}
// Task 2: Let's run this one at 1000 Hz (every 1ms)
void doTask2(void)
{
bool time_to_run;
const uint32_t DT_DESIRED_US = 1000; // 1000us = 1ms, or 1000Hz run freq
CREATE_TASK_TIMER(DT_DESIRED_US, time_to_run);
if (time_to_run)
{
// PERFORM THIS TASK'S OPERATIONS HERE!
}
}
// Task 3: Let's run this one at 10 Hz (every 100ms)
void doTask3(void)
{
bool time_to_run;
const uint32_t DT_DESIRED_US = 100000; // 100000us = 100ms, or 10Hz run freq
CREATE_TASK_TIMER(DT_DESIRED_US, time_to_run);
if (time_to_run)
{
// PERFORM THIS TASK'S OPERATIONS HERE!
}
}
但是,您也可以像这样构建代码,它同样有效并产生相同的效果,只是方式略有不同:
#include <stdbool.h>
#include <stdint.h>
#define TASK1_PD_US (10000) // 10ms pd, or 100 Hz run freq
#define TASK2_PD_US (1000) // 1ms pd, or 1000 Hz run freq
#define TASK3_PD_US (100000) // 100ms pd, or 10 Hz run freq
// Task 1: Let's run this one at 100 Hz (every 10ms, or 10000us)
void doTask1(void)
{
// PERFORM THIS TASK'S OPERATIONS HERE!
}
// Task 2: Let's run this one at 1000 Hz (every 1ms)
void doTask2(void)
{
// PERFORM THIS TASK'S OPERATIONS HERE!
}
// Task 3: Let's run this one at 10 Hz (every 100ms)
void doTask3(void)
{
// PERFORM THIS TASK'S OPERATIONS HERE!
}
int main(void)
{
doSetupStuff();
configureHardwareTimer();
while (1)
{
bool time_to_run;
CREATE_TASK_TIMER(TASK1_PD_US, time_to_run);
if (time_to_run)
{
doTask1();
}
CREATE_TASK_TIMER(TASK2_PD_US, time_to_run);
if (time_to_run)
{
doTask2();
}
CREATE_TASK_TIMER(TASK3_PD_US, time_to_run);
if (time_to_run)
{
doTask3();
}
}
}
嵌入式裸机微控制器编程的部分艺术(和乐趣!)在于决定您希望如何交错每个任务并将它们 运行 放在一起的技巧和独创性,所有作为尽管它们是 运行ning 并行的。使用上述格式之一作为起点,并适应您的特定情况。消息传递可以根据需要在任务之间或任务与中断、任务与用户等之间添加,并根据您的特定应用程序的需要添加。
下面是一个示例,说明如何配置定时器以用作 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);
}
参考文献:
- https://www.arduino.cc/en/tutorial/BlinkWithoutDelay
- 氧气:What's the right way to reference a parameter in Doxygen?
- 用于错误处理的基于枚举的错误代码:Error handling in C code
- C 中的其他架构风格,例如通过不透明指针的“基于对象的”C:Opaque C structs: various ways to declare them
另请参阅:
- 一个完整的、运行可用的 Arduino 示例,其中包含我上面的
CREATE_TASK_TIMER()
宏的更好版本:- 我对 C 和 C++ 的回答,包括微控制器和 Arduino(或任何其他系统):Full coulomb counter example demonstrating the above concept with timestamp-based, single-threaded, cooperative multi-tasking
首先感谢您的建议。我试图分析您提出的每一个可能的解决方案。
Peter 提出的解决方案看起来很有趣,但我不得不说,在多次查看数据表后,我认为这是不可行的。我的考虑是基于以下事实。
使用示波器,我看到在发送进行转换的命令后立即收到了确认。关于温度转换见下图:
命令之后的确认位对我来说似乎很清楚。之后 SDA 线(黄色)变高,因此我看不出如何利用它来检测转换何时准备就绪。
关于使用SPI时的解决方案,是的,SDO在转换过程中保持低电平,但我不能使用它:我需要坚持使用I2C。此外,我还有其他传感器连接到该 SPI 总线,我同意 Gabriel Staples 所说的。
经过深思熟虑,我选择了Gabriel Staples提出的解决方案(考虑到为了读取压力值,我还需要读取并转换温度)。
我当前的解决方案基于具有 6 个状态的状态机。在我的解决方案中,我区分了压力转换的等待时间和温度转换的等待时间,我的想法是如果我使用不太精确的温度读数,我可以尝试查看压力读数降低了多少。
这是我目前的解决方案。下面的函数在 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;
}
}
}
我不假装我的解决方案更好。我只是 post 它是为了征求你们的意见。注意:我仍在努力。因此我不能保证没有错误!
对于 PeterJ_01:我同意这不是严格意义上的教学门户,但我相信这里的每个人都会提出问题来学习新知识或提高自己。因此,如果您认为使用 ack 的解决方案更好,那么如果您能向我们展示您的想法草稿就太好了。对我来说,这将是一个新的学习点。
如有任何进一步的评论,我们将不胜感激。