2

我正在寻求智者的帮助。:-) 这不是关于测试本身的技术帮助,而是更多的代码组织问题。我正在使用 libopencm3 HAL 和 ceedling 作为测试套件的 STM32 项目。

我将问题保持简短并仅引用一小部分来证明这一点,显然代码中还有其他此类模块。

深入研究这个问题,主函数调用了一个函数“Init”,该函数存在于它自己的模块“Init”中。Init 从 IO 模块调用几个函数——IOInit 和 IOBlink,它们初始化 GPIO 并以特定模式闪烁 GPIO。

IOInit和IOBlink的形式如下

IOInit(GPIO)
IOBlink(PATTERN)

其中 GPIO 和 PATTERN 分别是 STATUS_LEDS、POWERUP 之类的东西,可以是枚举或 #define,具体取决于我选择构建它们的方式。

所以第一个问题就在这里,在这里定义IOInit 和IOBlink 的参数有什么意义?如果我想要一些在这里冗长但稍后在较低级别的函数调用中得到解决的参数?我将如何定义论点?作为 uint8_t,枚举,#define?

所以他们可以看起来像

IOInit(GPIO)
{
    //Something like this 
    if (GPIO == STATUS_LED)
        {
            InitIO(STATUS_LED_PORT, OUTPUT, STATUS_LED);
        }
    else (GPIO == MODE_IO)
        {
            //Do something else..
        }
    ...
}

IOBlink(PATTERN)
{
    if (PATTERN == POWERUP)
        {
            //GPIOToggle(Pin, delay, repetitions)
            GPIOToggle(STATUS_LED, 500, 4);
        }
         else if (PATTERN == ERROR)
          // Do something else
}

现在,IOInit 和 IOBlink 在位于 HAL 顶部的“DriverIO”模块中调用它们的较低级别的函数。DriverIO 具有相应的功能 - InitIO 和 GPIOToggle,它们可能看起来像这样

InitIO(PORT, Mode, Pins)
{
    rcc_periph_clock_enable(PORT);
    gpio_set_mode(PORT, GPIO_MODE_OUTPUT_2_MHZ, Mode, Pins);
    //Need to find a way to resolve PORT, Mode and Pins into a HAL compatible format
}

GPIOToggle(Pin, delay, repetitions)
{
    for (uint8_t i = 0; i<= repetitions, i++)
        {
            gpio_toggle(STATUS_LED_PORT, Pin);
            delay_ms(delay);
        }
}

现在有一些明显的明显的事情我认为可以而且应该改进。

所有三个层,Init、IO 和 DriverIO 都需要一个公共包含,其中包含 STATUS_LED_PORT 和 STATUS_LED 的定义 - 这闻起来很糟糕。这个想法是让每一层更加模块化和自包含,如果我必须在所有三层中包含 BSP.h 或类似的东西,这将失败。

2a. DriverIO 无法模拟和测试 - 因为它位于 HAL 之上。- 解决这个问题的一种方法,从我在这里读到并且可以理解的是添加一个“垫片层”或一个“包装器”,它基本上位于 DriverIO 和 HAL 之间,我可以简单地将标题包含在“包装器”中DriverIO,模拟包装器并测试 DriverIO。

2b。垫片层还有另一个问题。低级 HAL 使用 typedef 枚举作为参数,垫片层和上述层将无法访问这些参数。那么 shim 层中的翻译函数会是什么样子?

例如 - libopenCM3 HAL 中的时钟启用功能如下所示

void rcc_periph_clock_enable(enum rcc_periph_clken clken);

在垫片层中它会是什么样子?考虑到 DriverIO 不会知道枚举 rcc_periph_clken 的样子?

整体架构在设计时感觉还不错。但在实施过程中它真的崩溃了。原则上,main 只调用状态函数,每个状态函数只调用存在大量控制逻辑的中间层函数,中间层调用位于 HAL 上的驱动层函数。我为这篇冗长的帖子道歉,但我在设计和架构阶段花了很多时间来做这件事,我无法直截了当地思考。我患有一些分析麻痹。这可能是一个非常简单的解决方案,但我无法直接看到它。

再次,如果我没有多大意义,我深表歉意。我已经盯着这个看了好几天了,我认为它对其他人没有多大意义(如果有的话)。我试图让它可以理解,但这可能只是我缺乏梦话。请让我知道是否有特定部分我可以以更好的方式解释并说清楚。

真诚感谢任何帮助。

4

1 回答 1

1

所以第一个问题就在这里,在这里定义IOInit 和IOBlink 的参数有什么意义?

对于IOInit,它将需要端口、引脚和掩码。如果您允许在同一端口上使用其他不相关的功能,或者驱动程序对其具有独占控制权,则必须小心。从程序中多个不相关的地方更新 GPIO 寄存器是个坏主意,因为这会导致竞争条件和其他类似的异常情况。

根据我的经验,在简单的 GPIO 上编写抽象层往往弊大于利。

在您的情况下,GPIO 驱动程序实际上应该是一个“LED 闪烁应用程序模块”,它比 GPIO 稍高一些,因为它还必须使用定时器和/或 PWM 硬件外设。因此,请考虑将其命名为其他名称。

对不同的闪烁模式使用枚举是个好主意。然后,驱动程序可以在内部将这个枚举用作查找表等的索引,其中指定了时序和模式。除非您出于某种原因希望计时器周期可变,否则这些也需要作为参数传递。

现在,IOInit 和 IOBlink 在位于 HAL 顶部的“DriverIO”模块中调用它们的较低级别的函数。

为什么!?然后你有一些 3-4 抽象层,用于完全简单的东西,不需要抽象。这是非常糟糕的设计!

1 层有意义,处理 LED 图案的一层。其余无用的膨胀软件中间层必须离开:它们只占用资源并充当错误的来源。您应该直接从 LED 模式模块访问寄存器。ST所谓的“HAL库”一般都是有害的,应该避免。这意味着您必须重新设计其中的大部分内容。

DriverIO 无法模拟和测试

以在功能、可读性、可维护性和速度方面最有意义的方式设计您的程序。不要设计您的程序以适应某些 TDD 流行语测试套件模板。在设计代码时,您可以始终牢记测试,但您不应该让它支配设计。

例如,您可以设计特定数量的测试函数,而不是模拟,这些测试函数随驱动程序一起提供,并且可以直接访问驱动程序的私有成员,但仅在调试构建中链接。这允许比模拟更深入的测试,模拟实际上只是一个“黑盒”测试。

值得注意的是,在某种模式后闪烁 LED 非常容易测试,您实际上不需要功能模拟或其他任何东西,只需要强制示波器。这也是对所有嵌入式固件进行基准测试的正确工具。


关于 TDD 和一般测试,它应该是这样的:

规范 -> 程序设计 -> 包括测试的程序实施 -> 测试。

因此,对于每个需求,程序中都有一个模块,并且对于每个这样的模块,您都有一个测试。测试是为了查看代码是否符合其编写的要求,以便产品符合指定的功能。

这意味着您必须有一个规范开始。程序设计不应该是凭空发明的。你不应该实现你没有用的功能。测试应该测试产品的需求,而不仅仅是测试一般随机的东西或适应某种“测试套件”。

但请注意,程序设计很难,需要大量经验,只能学习到一定程度,其余的你必须边做边学。你已经完成了设计,编写了程序,然后最终意识到你可以以更好的方式完成设计,这是很正常的。

于 2021-03-10T11:41:15.367 回答