如何解决嵌入式 C
我是一名初级嵌入式软件,大部分时间都在为 dspic33 微控制器编写 C 代码。
我的嵌入式代码基于定时器中断,带有几个全局变量。让我们考虑最简单的代码:
typedef struct data_t
{
uint16_t a;
uint16_t b;
} data;
data my_data = {0,0};
int16_t main(void)
{
my_data.a = 10;
while(1)
{
// can access and modify my_data here
}
}
extern my_data data;
void timer_interrupt()
{
if (my_data.a == 10)
{
// stuff here
}
}
此代码工作正常,但如果我通过将变量声明替换为以下内容来在 my_data
上添加范围控制:
static data my_data = {10,10};
my_data
不能在主文件之外访问了。太酷了,我可以创建一个全局指针并继续在任何地方访问 my_data
成员:
static data my_data = {10,10};
data * const my_data_ptr = &my_data;
// from another file
extern data * const my_data;
void timer_interrupt()
{
if (my_data_ptr->a == 10)
{
// do stuff here
}
}
我通过使指针保持不变来防止最终重新分配,并且我确信 my_data_ptr
将在整个程序生命周期中指向 my_data
的地址。到目前为止,我满足了所有要求 - 我个人的良好实践要求 - 我限制了全局变量的范围并保持了变量的完整性。
现在让我们想象两个函数,我定义一个函数需要只读 my_data
,函数 2 需要对 my_data
进行读写,例如:
// read-only access
void function_one()
{
const data * const local_my_data_copy = my_data_ptr;
if (local_my_data_copy->a == 10)
{
// do stuff here
}
}
// read and write access
void function_two()
{
data * local_my_data_copy = my_data_ptr;
if (local_my_data_copy->a == 0)
{
// do stuff here
local_my_data_copy->a = 20;
}
}
最后,我的问题是在需要访问/修改指向数据的每个函数中创建 my_data_ptr
的本地副本是否相关?在我看来,它与变量访问控制有关,可以防止代码编写过程中出现错误。由于需要额外的变量声明,并且微控制器的内存有限,因此该实现需要更多的堆栈和数据空间使用。我很难确定这个实现的 cpu 成本。请让我知道您对这段代码的看法。
解决方法
访问全局 data[]
值得特别考虑,因为它是由中断例程和非中断代码读/写。
不要通过 const
和辅助指针进行保护,只考虑访问函数 - 非中断代码没有变量访问。
uint16_t data_get_a(void);
uint16_t data_get_b(void);
void data_get_ab(uint16_t *a,uint16_t *b);
void data_set_ab(uint16_t a,uint16_t b);
在非中断代码中,对速度的需求减少了,但对访问完整性的需求非常大,例如设置成员 .a
和 .b
,中间不可能有中断。
这些辅助函数可以提供临时中断禁用或其他机制来确保原子访问。这可能非常重要。见atomic
。也许是:
foo() {
save timer interrupt state
disable timer interrupt
access data
restore timer interrupt state
}
,
您的 function_two()
规避了 const
的 my_data
“承诺”。仅此一项是不好的做法,但由于它允许,因此更安全的解决方案是 avoidance of global data。您使用指向常量的指针来强制执行只读语义只有在您真的只希望它为只读时才有效 - 显然您不是。创建一个指向 static 的全局指针只会创建一个无用的间接级别,其他的很少 - 您没有避免全局或使代码更加安全。只有在编码人员必须以您拥有的方式明确规避它的意义上才更安全,因此可以避免意外修改。作为此代码的作者,您可以施加更多控制,以避免未来的维护者(可能是您)自爆。
在这种情况下的解决方案是,对共享数据的任何访问都应该通过访问器函数来可见数据。在这种情况下,这可能意味着在与 function_one()
相同的文件中实现 function_two()
和 static my_data
,但这对于良好的内聚和松散的可能并不理想>耦合。通用的解决方案是编写访问函数来控制和强制执行您想要的语义。
示例(有缺陷 - 阅读):
static volatile data my_data = {10,10};
const data* getMyData(){ return &my_data ; }
void setMyData( const data* d ){ my_data = *d ; }
getter/setter 函数相对于全局访问(间接或其他方式)的优势包括:
- 您可以强制安全访问非原子数据对象 - 就像这个问题中的那个(通过各种方式 - 这可能是另一个问题)。
- 在调试中,get/set 中的断点将捕获所有访问,而无需在每次访问上放置断点,或使用数据观察断点。
- 您可以在 setter 中强制执行通用验证、范围限制或断言,以确保仅写入有效数据。
- 您可以执行“访问时”转换,例如在读取时将 ADC 单位转换为毫伏,反之亦然。因此,将数据表示与其内部表示解耦,在 get/set 中进行此类转换可避免在中断上下文中进行可能的昂贵处理。
您当然可以为结构的各个成员设置访问函数,例如允许通过不定义 setter 来强制某些成员只读,或者通过定义 only 甚至只写语义setter - 不能使用 const
在全局上强制执行的语义。总的来说,它比使用全局并尝试强制执行 const
无法支持的语义要灵活得多,也更安全。
真正阅读 Jack Ganssle 之前链接的文章 A pox on globals。作为未来的嵌入式系统开发人员,它会很有启发性,并使您处于有利地位。
其他几点:
您真的应该声明在上下文之间共享的数据 volatile
(参见 https://www.embedded.com/introduction-to-the-volatile-keyword/)。但请注意,单独使用 volatile
并不是上下文之间安全共享内存的完整解决方案。
例如,您必须注意,如果来自主线程(或任何其他线程上下文)的数据访问不是原子的,则可能会在读取数据的过程中被中断和修改数据。对于 16 位设备上的 32 位整数,这可能意味着例如您最终会得到旧值中的高位字和新值中的低位字 - 反之亦然。即使对于原子的数据类型,也不需要对 then 进行操作。例如,i += j
表示可能会中断的读-修改-写操作。对于数据结构,这可能意味着一个成员与另一个不一致。
我在这里提出这一点,因为您的代码清楚地展示了这些问题,但同样明显不是真正的代码,您可能已经意识到这些问题。这也不是您的问题的内容,如果您需要解决方案,您可能会发布一个新问题,但对于值得的,我早些时候为 getter 提供了一个可能的解决方案:
data* getMyData( data* d )
{
// read my_data until a consistent copy has been obtained.
do
{
*d = mydata ;
} while( *d != my_data ) ;
return d ;
}
最后一点,如果您接受上述建议,这在很大程度上无关紧要,但关于:
[...] 并且我确信 my_data_ptr 将在整个程序生命周期中指向 my_data 的地址。
无需相信这是偶然或维护错误 - 您可以强制执行该断言:
const data* const my_data_ptr = &my_data;
// ^^^^^
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。