【问题标题】:Loose-coupling patterns for embedded systems programming [closed]嵌入式系统编程的松散耦合模式
【发布时间】:2011-01-01 07:26:14
【问题描述】:

我在哪里可以找到一些关于用 C 编写可扩展、模块化、松散耦合的代码(如果可能的话)的好的、经过验证的指南或示例?

我们问题的背景是,我们正在为计算和内存资源有限的低成本微控制器维护大型纯 C 遗留代码项目。由于系统必须非常可靠且内存相当有限,因此第一个限制条件之一是根本不使用动态内存分配。所有结构都是静态映射的。

因此,我们正在寻找使此代码更易于维护和更模块化的方法。 我们对编码标准不感兴趣,而是对设计建议感兴趣。我们有很好的编码约定(命名、组织代码、SVN),所以这不是问题。

从我在网上看到的情况来看(我可能错了),似乎大多数只使用纯 C 或汇编程序编程的程序员,至少在 uC/Embedded 社区中,都限制使用任何更简单的东西程序化编程。

例如,我们可以使用回调函数、包含函数指针的结构和类似的东西(它不需要动态分配,只需传递指向结构的指针),在普通 C 中获得大部分 OOP 优势和解耦,但我们会想看看是否已经有一些行之有效的方法。

你知道这些资源吗,或者有类似的建议除了“你为什么不切换到C++或其他编程语言”?

[编辑]

非常感谢所有答案,我还没有时间检查它们。 平台是 16 位(XC166 或类似)uC,裸硬件(无 RTOS)。

【问题讨论】:

  • 出于兴趣,你在写什么架构?
  • 这也是我一直在思考的问题。
  • 如果我们对目标有更多了解,也有助于回答问题。你在为 Linux 编码吗?然后会想到某些事情。你有 MMU 吗?某些事情是可能的。你在为裸硬件编码吗?很多事情都成为可能。

标签: c embedded coding-style


【解决方案1】:

我们处于类似的情况。为了解决这些问题,我们实现了一个构建系统,该系统支持所需接口的多种实现(使用的实现是编译目标的函数),并避免使用可移植包装器中未包含的 API 功能。包装器定义位于一个 .h 文件中,#include 是特定于实现的头文件。以下模型演示了我们如何处理信号量接口:

#ifndef __SCHEDULER_H
#define __SCHEDULER_H

/*! \addtogroup semaphore Routines for working with semaphores.
 * @{
 */

/* impl/impl_scheduler.h gets copied into place before any file using
 * this interface gets compiled. */
#include "impl/impl_scheduler.h"

/* semaphore operation return values */
typedef enum _semaphoreErr_e
{
    SEMAPHORE_OK = impl_SEMAPHORE_OK,
    SEMAPHORE_TIMEOUT = impl_SEMAPHORE_TIMEOUT
} semaphoreErr_e;

/*! public data type - clients always use the semaphore_t type. */
typedef impl_semaphore_t semaphore_t;

/*! create a semaphore. */
inline semaphore_t *semaphoreCreate(int InitialValue) {
  return impl_semaphoreCreate(InitialValue);
}
/*! block on a semaphore. */
inline semaphoreErr_e semaphorePend(semaphore_t *Sem, int Timeout) {
  return impl_semaphorePend(Sem, Timeout);
}
/*! Allow another client to take a semaphore. */
inline void semaphorePost(semaphore_t *Sem) {
  impl_semaphorePost(Sem);
}

/*! @} */

#endif

公共 API 已记录在案以供使用,并且在编译之前隐藏实现。使用这些包装器也不应该带来任何开销(尽管它可能会,取决于您的编译器)。不过,这涉及到很多纯粹的机械打字。

【讨论】:

  • 谢谢,这似乎是一个简单而干净的解决方案,无需引入自定义框架。实际实现是在编译时解决的,这可以防止错误,尽管它不会留下在运行时使用自定义策略的选项(如果我做对了)。开发人员习惯该策略应该不会太难。还有一个问题:您实际上在这方面做了多远?您是否将系统的所有部分都分离到这样的接口中?当您说“构建系统”时,您指的是复制实现文件的构建脚本?再次感谢!
  • 我认为你说得对……如果我们想在运行时引入自定义策略,我们会使用其他机制(函数指针等)。我们没有以这种方式隔离整个系统——只是我们知道我们需要不同实现的部分。 HAL 具有一个或多个针对接口将支持的不同硬件的实现(如不同的中断控制器),并且(希望)至少一个可以针对开发 PC 的实现,以便在该环境中更好地控制/测试。单元测试将使用不同的框架...
  • “构建系统”实际上是构建运行的方式。它是一个支持 DSL 的自定义 ruby​​ 脚本(就像我们定义内存地址的脚本一样,它生成可供 .c 或汇编文件使用的输出,构建系统本身可以使用其输出来构建 MMU 表,以及ARM 链接器使用的 scatter.txt 文件),将 .h 文件复制到位,编译/链接 .c、.a、.s 文件等。我通常对结果很满意,但如果我有的话再做一遍,我会非常仔细地查看 scons。
【解决方案2】:

您可能想查看 xDAIS 算法标准。它是为 DSP 应用而设计的,但也可以针对低资源嵌入式设计进行调整。

http://en.wikipedia.org/wiki/XDAIS_algorithms

简而言之:xDAIS 是一种 OOP 风格的接口约定,与 C 语言的 COM 不同。您有一组固定的接口,模块可以通过函数指针结构实现这些接口。

接口是严格定义的,因此很容易交换组件,将它们堆叠在一起以构建更高级别的功能等等。接口(和代码检查器)还确保所有组件保持分离。如果使用代码检查器,则不可能编写直接调用其他组件私有函数的组件。

内存分配通常在初始化时完成并在系统设计者的控制下完成(它是所有组件必须实现的主界面的一部分)。

静态和动态分配策略是可能的。您甚至可以在没有内存碎片风险的情况下实现动态,因为所有组件都必须能够将自己重新定位到不同的内存地址。

xDAIS 标准定义了一种非常精简的 OOP 风格的继承机制。这对于调试和记录目的非常方便。有一个做有趣事情的算法吗?只需围绕现有算法添加一个简单的单文件包装器,并将所有调用记录到 UART 左右。由于严格定义的接口,模块的工作方式和参数的传递方式无需猜测。

我过去使用过 xDAIS,它运行良好。习惯它需要一段时间,但即插即用架构和易于调试的好处超过了最初的努力。

【讨论】:

  • xDAIS 还有其他指针吗?它看起来很有趣,我一直在寻找类似的东西。
  • 当然。它在 ti.com 网站上。从这里开始:focus.ti.com/lit/ug/spru424c/spru424c.pdf IALG 接口是最重要的(尤其是内存分配部分)。您可能希望忽略所有与 DMA 相关的事情并为您的应用程序发明自己的接口(例如文件或网络访问或任何您想做的事情)。
  • @Nils,谢谢!有一种标准的做事方式总是很好的。尤其是当它似乎是一个很好的方法时。 :-)
  • 谢谢,这看起来很有希望。所有用于数据和实现的内存都可以静态分配,这正是我所需要的。还有一件事,您发现所提供框架的哪些部分在实践中有用(向导、代码编写器和其他提到的工具)?您是使用整套,还是只遵循指南并拥有自己的框架?再次感谢!
  • 不要使用任何工具,除非您的目标平台恰好是该标准所针对的 DSP 之一。那将毫无用处。相反,看看 TI 是如何定义 ALG 接口的,并以此为灵感来开发自己的东西。您肯定会有与典型 DSP 算法不同的要求 :-)
【解决方案3】:

我将尝试从这里开始回答。如果还有什么想起来的,我会回到这里,因为这个问题很有趣。我也会关注这个问题以寻找其他答案。

  • 独立的逻辑和执行:

    嵌入式系统可以从与大型业务应用程序相同的逻辑和 I/O 分离中受益。

    例如,如果您正在为某些嵌入式设备编写代码,该设备读取值、解释这些值并根据这些读数更改某些内容, 您可能希望将“逻辑”部分与您实际与网络、硬件、用户或任何外部实体进行通信的部分完全分开。

    当您可以在某种内存结构或 C 代码中完全描述“规则”时,除了消息传递例程或类似程序之外不链接到任何东西,您就有了我试图描述的内容。简而言之,减少副作用使代码更加模块化。

  • 我不知道你是否使用线程,但无论哪种方式proto threads 都提供了类似的抽象,不如线程强大,但也不太可能让程序员感到困惑。

  • 在 Amiga 上长大,我很难忘记它。支持 ROM 的操作系统,但可通过可加载库在 RAM 中轻松扩展。大量使用 pointer passing 用于紧凑的代码和快速的消息。

阅读 Nils Pipenbrinck 的另一个答案,他建议使用 xDAIS 看起来是一种很好的(但远不是唯一的)实现方式。如果您的大部分代码都使用这样的消息约定,那么您的代码很可能是模块化且可维护的。

我还会在为目标编译之前启动运行的预处理器或预编译器传递代码,但随后我们就会陷入灰色地带……这几乎就像切换语言一样,而且要求是 C。

【讨论】:

    【解决方案4】:

    您提到的模仿 OOP 是一种很好的做法。除了了解标准之外,我还可以让您了解我是如何做到的。我实际上是在使用它来处理一些细节:

    • 隐藏内部模块结构。
    • 向应用程序公开结构大小以允许静态分配。
    • 公开回调接口以借用其他模块的功能。
    • 保持每个模块可自行编译。
    • 最后但同样重要的是:保持代码易于阅读、易于使用和易于修改。

    @my_module.c

    typedef struct _s_class
    {
        uint32_t an_attribute;
        void (*required_behavior)(uint32_t);
    } class_t;
    
    void obj_init(void * obj, void(*req_beh_callback)(uint32_t))
    {
        ((class_t*)obj)->an_attribute = 0;
        ((class_t*)obj)->required_behavior = req_beh_callback;
    }
    
    void obj_method1(void* obj)
    {
        ((class_t*)obj)->an_attribute++;
        required_behavior(((class_t*)obj)->an_attribute);
    }
    
    size_t get_object_size()
    {
        return sizeof(class_t);
    }
    

    @my_module.h

    void obj_init(void * obj, void(*req_beh_callback)(uint32_t));
    void obj_method1(void* obj);
    size_t get_object_size();
    
    // run get_object_size() once to get the number
    // that goes in this macro. may differ between CPU
    // architectures.
    #define OBJECT_SIZE 4
    

    @my_application.c

    #include "my_module.h"
    
    uint8_t my_object[OBJECT_SIZE];    // static allocation :)
    
    void callback_for_obj(uint32_t i)
    {
        ... do stuff ...
    }
    
    int main()
    {
        obj_init(my_object, callback_for_obj);
        obj_method1(my_object);
        return 0;
    }
    

    如果您有任何建议或问题,请告诉我,因为它们也有助于我了解更多信息!

    【讨论】:

    • 这很有帮助,有两点说明:1)使用包含结构布局的#define" for the object size is not the best idea in terms of maintainability. Perhaps a better approach would be to have a my_module_internal.h`头,然后将其单独包含在需要静态分配空间的编译单元中(使用编译时间sizeof)。
    • 2) 根据 C 标准,您只能使用 char* 给对象起别名,反之则不行。优化编译器可能会将my_object 视为与其别名的任何实际结构完全不同的实体,因此它通常会在不同的优化级别导致奇怪的运行时错误。在实践中,您还必须使用编译器指令来确保正确对齐my_object(例如__attribute__((aligned(8))),如果您的任何字段是int64_tdouble),因为没有任何东西可以保证该数组将正确对齐否则。
    【解决方案5】:

    我们没有使用很多小型设备,但我们确实有一些存在内存限制。我们正在分配静态缓冲区,但我们发现有时动态内存分配实际上有助于减少内存使用量。我们严格控制堆大小和分配策略,并且必须检查和处理内存不足的情况,而不是错误,而是正常操作。例如。我们内存不足,所以我们发送我们拥有的数据,清除缓冲区并从我们离开的地方恢复操作。

    为什么我们不切换到 C++?我喜欢。我们不切换主要有以下原因:

    1. 我们的代码猴子不会摸索,也不愿意学习。
    2. C++ 库要大得多(尽管我们可以解决这个问题。)
    3. 对于那些非常小的 RTOS 设备,通常没有必要。对于运行嵌入式 Linux 的大型设备,这将是很好的选择。

    【讨论】:

    • 不完全是我们的想法,但感谢您分享这个想法。虽然,如果你说“我们发送数据,清理缓冲区”,看起来你可以对 FIFO 缓冲区(生产者/消费者队列)做同样的事情?在执行过程中一直出现“内存不足”错误似乎相当危险(但如果它被证明并且工作可靠,那么我猜你做对了)。但是我们的应用程序中的数据处理必须真正实时完成,没有延迟,所以我们避免动态分配和不确定的行为(而且我们不需要动态分配任何东西,数据大小是固定的)。
    【解决方案6】:

    您最好确定一个固定的布局就是您想要的!拆掉它并添加一个动态的可能会非常棘手!

    我认为任何嵌入式框架都试图解决的问题是:

    计算数据偏移量

    应该可以为所有内存创建一个单独的结构,但是这个 *so* 感觉不是正确的方法。 C 编译器通常不会被要求处理数兆字节的结构,我觉得这样做在编译器之间不太可移植。

    如果不使用结构,则需要根据本质上是数据模式的五组定义:

    • 简单类型的大小
    • 组类型中的字段偏移量
    • 组类型的大小
    • 组运行中的组偏移量
    • 组运行的大小
    • (如果性能要求,也可能是组运行的绝对地址)

    这些定义有一个树状的依赖树,在原始 C 中它很快变得非常复杂,因为类型通常必须打包/对齐,例如 4 字节块以获得最佳性能。完全扩展的定义很快就会变得比某些编译器乐于处理的复杂。

    在原始 C 项目中管理这些问题的最简单方法是使用作为项目的“构建工具”可执行文件的 C 程序 计算偏移量,并将它们作为 . h 文件,其中包含显式 opffset 编号。使用这种方法,在主编译期间应该可以使用带有基地址和相关索引的单个宏来访问数据结构的每个叶子。

    避免函数指针损坏并最大限度地提高调试效率

    如果函数指针存储在对象中,它们更容易受到数据损坏而导致神秘错误的影响。更好的方法(对于那些不时包含不同对象类型的内存范围)是在对象中存储一个 vtable 代码,它是一组函数指针集的查找索引。

    vtables 可以再次通过作为可执行“构建工具”的生成器 C 程序计算并生成为带有#defines 的 .h 文件。

    需要一个特殊的构造函数宏来写入适当的vtable id来初始化对象的使用。

    这两个问题已经通过例如 Objective-C 预处理器(它将输出原始 C)有效地解决了,但是如果您想使用非常少的工具集,您可以从头开始。

    为静态内存结构中的资源/任务分配内存块

    如果您需要支持多线程,将短期动态任务与树结构中的特定索引相关联(最接近于在等效的过程/OO 程序中分配对象)可能最好通过尝试锁定任意索引,例如使用原子增量(使用 ==1 检查从零开始)或互斥体,然后检查该块是否可用,如果可用,将其标记为已使用,然后解锁该块。

    如果不需要多线程支持,那么这是不必要的;我建议编写一个自定义框架来管理可以在多线程或单线程模式下运行的此类资源分配过程,以允许其余代码库不关心这个主题,并在单线程系统上实现更快的性能。

    【讨论】:

    • "没有动态内存分配" != "一个描述所有变量的巨大结构"。在这种应用程序中,目标定义全局变量,其偏移量由链接器确定,而不是由您确定。
    • 哦——真的!我抓错了棍子的一端。我错了
    猜你喜欢
    • 2013-05-06
    • 1970-01-01
    • 2012-09-01
    • 2020-02-17
    • 2017-06-05
    • 2011-01-20
    • 2010-09-22
    • 1970-01-01
    • 2014-03-15
    相关资源
    最近更新 更多