我正在使用ARM微控制器在我的大学进行实时系统课程。在我正在研究的项目中,我正在实现矢量场直方图(VFH)算法。
问题是:我需要在线程之间进行通信;更具体地说,我希望有一个线程从测距仪获取传感器数据,对其进行必要的转换并将它们存放在队列中。他们,另一个线程必须得到这些数据并处理它等等。
目前,我正在使用更简单的版本 - 一个线程从ADC获取数据(SensorAcquisitionHandler),另一个线程将前5个项目(最多)的平均值输出到显示器(ControlSignalHandler)。
/* Queue used to store data from the rangefinder sensors. */
static unsigned int Sensors[100];
static int SensorsHead = 0;
static int SensorsTail = 0;
void SensorAcquisitionHandler(void) {
/* Clear the interrupt. */
ADCIntClear(ADC0_BASE, 1);
int i; /* Index for the measurements buffer. */
/* There are only 3 rangefinders used. */
if (ADCSequenceDataGet(ADC0_BASE, 1, rangeBuffer) == 3) {
/* Put rangeBuffer's data into SensorDataQueue. */
/* Also, when using SensorDataQueue, must put what's the direction of
the corresponding range measurement. */
/* Critical section ahead!!! Turn off interrupts!!! */
IntMasterDisable();
/* Temporarily using the simple FIFO... */
for (i = 0; i < 3; ++i) {
if (SensorsHead < 100) {
Sensors[SensorsHead] = rangeBuffer[i];
SensorsHead++;
}
}
/* All is fine, turn on interrupts. */
IntMasterEnable();
}
}
void ControlSignalHandler(void) {
/* Clear the timer interrupt. */
TimerIntClear(TIMER0_BASE, TIMER_TIMA_TIMEOUT);
unsigned char i; /* Index for the measurements buffer. */
unsigned long average = 0;
char buffer[20];
/* Average first n (n <= 5) elements from Sensors queue. */
for (i = 0; i < 5 && SensorsTail < SensorsHead; ++i) {
average += Sensors[SensorsTail];
SensorsTail++;
}
IntMasterDisable();
average /= i;
sprintf(buffer, "%d ", average);
average = 0;
if (SensorsTail >= SensorsHead) {
SensorsTail = 0;
SensorsHead = 0;
}
Display96x16x1StringDraw(buffer, 0, 0);
IntMasterEnable();
}
结果相对稳定一段时间,但随机间隔变得非常高(结果几乎一直都是~330)。此外,当我在“非常高的值”时刻使用符号调试器时,索引SensorTail和SensorHead可以达到300+(队列是100个元素的数组)。
这听起来像某种溢出,但我无法想象它是如何发生的。有人能帮我找到吗?
我知道问题的答案是“使用线程安全队列”,但我想了解竞争条件是如何发生的,索引如何搞砸了等等。谢谢!
答案 0 :(得分:3)
通过清除中断,您可以再次发生这种情况。想象一下如果(例如)ControlSignalHandler
在第一个循环的中间重新进入会发生什么,因为计时器设法超越你的代码......
在IntMasterDisable
+ IntMasterEnable
中包装整个功能(两者),并在禁用后和启用前清除中断。 (我会在启用之前立即执行此操作。)
答案 1 :(得分:3)
通过使用无锁单读取器单写入器FIFO,可以避免头部和尾部指针的竞争条件 - 其中头指针只能写入一个线程(或者在您的情况下是ISR)和尾巴写在另一个。这意味着您在每个ISR中执行缓冲区换行测试。
如果您这样做并在每个ISR结束时重置您的中断源,您根本不需要任何锁定 - 全局禁用中断正如您所做的那样非常糟糕。目前你拿着锁很长时间了。
您需要重写FIFO实现的另一个原因是:
for (i = 0; i < 3; ++i) {
if (SensorsHead < 100) {
由于您一次添加3个读数,因此您最终会使用SensorAcquisitionHandler()
输入SensorsHead==99
- 这样可以保证您将读取2个读数。
类似地:
/* Average first n (n <= 5) elements from Sensors queue. */
for (i = 0; i < 5 && SensorsTail < SensorsHead; ++i) {
average += Sensors[SensorsTail];
SensorsTail++;
}
在某些情况下,将执行相当少于5个值的计算。
根据您使用的ARM部分,没有硬件鸿沟。计算两次幂的平均值要便宜得多,因为这是一个单周期的逻辑转换。
最后,我认为Display96x16x1StringDraw(buffer, 0, 0);
是一项特别昂贵的操作,也可能是线程安全的。 IO在ISR中始终严格禁止。
您可能需要在计时器线程和非中断上下文之间建立另一个队列 - 它处理输出。
答案 2 :(得分:2)
您使用的是哪种特定处理器/微控制器?什么RTOS(如果有的话)?
ARM微控制器上的中断堆栈通常非常非常小。像snprintf()
这样的运行时例程很容易需要数百个字节,并且可能会超出小堆栈。除了堆栈空间考虑之外,C运行时函数在中断上下文中通常也不安全 - 您通常非常约束可以从中断调用哪些函数。具体细节取决于您正在使用的实际RTOS和编译器工具链。
如果您违反了这些限制,很容易导致数据损坏。
答案 3 :(得分:0)
查看程序集转储。可以针对易失性代码重新排序非易失性代码。
想象一下你有:
assignmentA_with_volatile_operands;
assignmentB_with_non_volatile_operands;
assignmentC_with_volatile_operands;
编译可以自由地重新订购:
assignmentA_with_volatile_operands;
assignmentC_with_volatile_operands;
assignmentB_with_non_volatile_operands;
例如,第一个处理程序for
中的SensorAcquisitionHandler
循环可以在IntMasterEnable
之后实际执行,因为for
循环中出现的任何对象都不是{{1}合格。
修改强>
有些人认为这种代码重新排序是不允许的。事实是它们是,并且它们是用现实生活中的编译器执行的。
volatile不会成为程序中的内存屏障。您不应该假设易失性访问在非易失性访问方面会成为内存障碍。
标准在C11,5.1.2.3中定义了关于程序的可观察行为的对符合实现的最低要求。
volatile
例如说:非易失性对象的访问不是针对易失性访问进行排序的。您不能使用易失性对象作为内存屏障来对非易失性存储器进行一系列写入。
http://gcc.gnu.org/onlinedocs/gcc/Volatiles.html
许多编译器都谨慎不要在volatile存在的情况下执行这些代码重新排序优化,但我已经看到编译器(例如gcc
)执行它们。