【问题标题】:Can I turn this requirement into a macro?我可以把这个要求变成一个宏吗?
【发布时间】:2020-04-24 20:48:02
【问题描述】:

我有带有递减软件计数器的 C 程序。例如,如果我想每 2 秒闪烁一次 LED,我可以这样做:

if(!ledT) { 
    ledT = 200;
    // code
    // code
    // code 
}

因为我总是对每个计数器进行完全相同的组合,所以我倾向于输入一行。

if(!ledT) { ledT = 200;
    // code 
    // code 
    // code  
}

对于整个 if 行,我想改用宏。所以代码看起来像:

expired(ledT, 200) {
    // code 
    // code 
    // code 
}

我在进入状态的状态机代码中使用了类似的东西。

if(runOnce) { runOnce = false;
    // code 
    // code 
    // code 

所需的语法:

entryState {
    // code 
    // code 
    // code 

.

#define entryState if(runOnce) { runOnce = false; // this ofcourse cannot work But something like this is what I want.

我已经做了几次尝试,但都没有成功。问题是{ 位于宏的中间位置,我想在宏后面键入{,因为众所周知,没有代码编辑器可以处理不相等数量的{ 和@987654331 @。

expired(ledT, 200); // expired is macro, not function
    // code
    // code
    // code
}

所以这是不可能的。

在阅读宏时,我读到了一些关于使用的有趣内容:do ... while(0)。这种“技巧”滥用编译器的优化功能来创建某个宏,否则这是不可能的。

This site

阐明了这种方式。

有没有办法使用某种“宏观技巧”来实现我想要的?

再说一遍,这就是转变:

// this
if(runOnce) {
    runOnce = false;
    // code
    // code
    // code

// into this
entryState {
    // code
    // code
    // code

// and this:
if(!someTimer) {
    someTimer = someInterval;
    // code
    // code
    // code

// must be transformed into:
timeExpired(someTimer, someInterval) {
    // code
    // code
    // code

类似“不,根本做不到”的回答也将被接受(前提是您知道自己在说什么)

编辑: 我需要添加一个补充,因为似乎不是每个人都知道我想要什么,最后给出的答案甚至不是针对手头的具体问题。不知何故切换 IO 突然变得很重要?因此我修改了我的代码示例以更好地说明问题所在。

编辑2: 我同意 timeExpired 宏根本不会提高可读性

为了说明某些宏可以提高可读性,我将给出一个状态和状态机的 sn-p。这是我的代码中生成的状态的样子:

State(stateName) {
    entryState {
        // one time only stuff
    }
    onState {
        // continous stuff
        exitFlag = true; // setting this, exits the state
    }
    exitState {
        // one time only stuff upon exit
        return true;
    }
}

目前使用这些宏:

#define State(x) static bool x##F(void)
#define entryState if(runOnce) 
#define onState runOnce = false;
#define exitState if(!exitFlag) return false; else

我想我应该在美国用EXIT 或其他更漂亮的东西交换return true;。 调用这些状态的状态机看起来像:

#undef State

#define State(x) break; case x: if(x##F())
extern bit weatherStates(void) {
    if(enabled) switch(state){
        default: case weatherStatesIDLE: return true;

        State(morning) {
            if(random(0,1)) nextState(afternoon, 0);
            else            nextState(rain, 0); }

        State(afternoon) {
            nextState(evening, 0); }

        State(evening) {
            if(random(0,1)) nextState(night, 0);
            else            nextState(thunder, 0); }

        State(night) {
            nextState(morning, 0); }

        State(rain) {
            nextState(evening, 0); }

        State(thunder) {
            nextState(morning, 0); }

        break; }
    else if(!weatherStatesT) enabled = true; 
    return false; }
#undef State

唯一没有生成的是 'nextState()' 函数之前的 'if' 和 'else'。这些“流动条件”需要填写。

如果向用户提供了一个小例子或解释,他应该完全没有困难填写状态。他还应该能够手动添加状态。

我什至想用宏来交换这个:

extern bit weatherStates(void) {
        if(enabled) switch(state){
            default: case weatherStatesIDLE: return true;

break;} }
    else if(!weatherStatesT) enabled = true;
    return false;}

我为什么要这样做?将不相关的信息隐藏在您的显示器之外。删除状态机中的许多选项卡。通过使用简单的语法来提高整体可读性。与第 3 个库函数一样,您需要知道如何使用代码,而不是知道函数是如何发挥作用的。

您不需要知道状态是如何发出准备好的信号的。知道所讨论的函数被用作状态函数比知道它返回一个位变量更重要。

我还在使用前测试宏。所以我不会向别人提供可能表现出奇怪行为的状态机。

【问题讨论】:

  • 为什么不使用函数而不是宏?
  • 为什么要使用宏?第一个代码示例清晰明了。所有其他的只是模糊了代码(它们看起来不像是有效的 C 代码),你不会从需要维护它的任何其他人那里得到任何感谢。如果你真的想使用一个函数。
  • Because I always do the exact same combination with every counter, I tend to type it one line. 这只是普通丑陋的代码。
  • Swedgin -> 将代码放在一行中很难看,这就是我想要一个宏的原因。 Konrad -> 使用函数通常比较慢,尤其是在传递争论的情况下。 Kaylum -> 在这种特殊情况下可能是这样,但宏可以增加可读性。如果你下载一些随机库来让一些 I2C 传感器工作,你只需要寻找示例代码就可以“完成工作”。根本不需要在库本身内部进行挖掘。宏也是如此。代码可读性取决于使用的语法。
  • @basknippels Because I always do the exact same combination with every counter 这需要一个函数。就像在教科书示例中重构为子函数一样。

标签: c macros keil 8051


【解决方案1】:

这里没有必要使用宏,这样做会导致非常不习惯的 C 代码与正确的 C 代码相比没有任何优势。

改用函数:

int toggle_if_unset(int time, int pin, int interval) {
    if (time == 0) {
        time = 200;
        TOG(pin);
    }
    return time;
}
ledT = toggle_if_unset(ledT, ledPin, 200);

(我根据您的示例猜测适当的参数名称;酌情调整。)

更重要的是,看起来ledTledPin 总是成对的并且属于同一类,在这种情况下,您应该考虑将它们放入struct

struct led {
    pin_t pin;
    int interval;
};

void toggle_if_unset(struct led *led, int new_interval);

或者类似的东西。

【讨论】:

  • ledT 和 ledPin 未链接。我有一个纯 txt 文件,其中包含我所有的软件计数器及其时基。 python 脚本为我生成 timers.c 和 timers.h 文件。因此,如果需要更改计时器,我只需更改一个位置并运行脚本。 “......这样做会导致高度不惯用的 C 代码与正确的 C 代码相比没有任何优势”。不对。函数调用速度较慢,尤其是在您开始传递参数时。宏比函数更易读还是更难读完全取决于它们的编写方式。
  • @basknippels 这里有很多误解。例如,“函数调用速度较慢”——这通常是不正确的,最好立即忘记这个神话。例如,我在这里展示的函数将被任何值得其盐的优化器(= 每个当前编译器)简单地内联。事实上,我可能会在标题中将其定义为 inline 以使其更容易。作为一般规则,在现代 C 语言中,您从不想要使用宏来代替适当的函数。 仅在函数不可用时使用宏,即用于代码生成和包含守卫。
  • @basknippels 在您的问题中应该在前面和中心提到您被限制为不符合标准的 not-quite-C 编译器这一事实。 ? 目前你的问题被标记为c。我的答案是 C 的规范答案。你想要一些不同的东西。
  • @basknippels 出于好奇,您为什么要为 40 年前的硬件编写代码?这是一些复古的计算项目吗?
  • @basknippels 我更喜欢提供有用的、有教育意义的答案,而不是“技术上正确”但具有误导性的答案,我坚信这就是 Stack Overflow 的目的。这就是为什么在问题中提供所有相关背景如此重要的原因。
【解决方案2】:
#define LL(ledT) do {if(!ledT) { ledT = 200; TOG(ledPin); }}while(0)

在阅读宏时,我读到了一些有趣的东西 使用:做...而(0)。这个“技巧”滥用了编译器的 优化功能以创建某个宏,否则 不可能。

那里的大多数意见实际上是错误的。没有什么关于优化的。

主要原因是使用花括号来编译宏。

这个不会编译

#define A(x) {foo(x);bar(x);}

void foo1(int x)
{
    if (x) A(1);
       else B(0);
}

但是这个会编译

#define A(x) do{foo(x);bar(x);}while(0)

void foo1(int x)
{
    if (x) A(1);
       else B(0);
}

https://godbolt.org/z/4jH2jP

【讨论】:

  • 关于优化:据我所知。我们编写的 C 代码被转换为汇编代码和/或机器代码。 if(0) {body} 之类的东西将被完全删除,因为编译器知道它永远无法执行。 while(0) 也是如此。它不会被翻译成实际的机器代码 afaik。
  • @basknippels *Same goes for while(0)* 我不明白你的意思,但通常不是。在花括号中的语句之后检查}while(
  • 另外,不幸的是,您的第一个宏无法解决问题。问题是 TOG(ledPin) 可以是任何东西。那条特定的行被放在大括号中的宏后面,这样它就可以是任何东西。我什至不确定我想要的是否可能。至少感谢您迄今为止所做的努力。
  • @basknippels 我不明白你的评论。这没有任何意义
  • 很多人会说使用不带大括号的if ... else 总是不好的风格,不使用do-while(0) 技巧时出现的编译器错误是一件好事。
【解决方案3】:

鉴于这是针对一些旧的 8051 遗留项目,极不可能您需要为引脚 I/O 处理创建抽象层宏。你只会有这么多的别针。您的原始代码很可能是最好、最清晰的代码。

如果您出于某种原因担心代码重复,因为您有多种产品组合/支持具有不同路由等的多个 PCB,并且您被当前的代码库困住了......那么作为最后的手段 你可以使用宏来避免代码重复。这也假设您是一位经验丰富的 C 程序员 - 否则请停止阅读此处。

在这种罕见的情况下,您将看到的可能是所谓的“X 宏”,它是关于声明一个预处理器常量的完整列表。然后,每当您需要做一些重复的事情时,您就调用该列表并使用其中您对该特定调用感兴趣的常量。每个调用都是通过指定宏“X”在该特定调用中应该做什么来完成的,然后取消定义宏。

例如,如果您有端口 A、B、C,端口 A:0、B:1 和 C:2 上分别有 LED,并且希望每个引脚使用不同的延迟,您可以声明如下列表:

#define LED_LIST           \
/*  port   pin   delay */  \
  X(A,     0,    100)      \
  X(B,     1,    200)      \
  X(C,     2,    300)      \

然后,您可以在需要执行重复性任务时调用此列表。例如,如果这些端口有数据方向寄存器,您需要进行相应设置,这些寄存器称为 DDRA、DDRB、DDRC(以 Motorola/AVR 命名为例):

/* set data direction registers */
#define X(port, pin, delay) DDR##port |= 1u<<pin;
  LED_LIST
#undef X

这将扩展为:

DDRA |= 1u<<0;
DDRB |= 1u<<1;
DDRC |= 1u<<2;

同样,您可以将计数器初始化为:

/* declare counters */
#define X(port, delay) static uint16_t count##port = delay;
  LED_LIST
#undef X


...

/* check if counters elapsed */
#define X(port, delay) if(count##port == 0) { count##port = delay; PORT##port ^= 1u << pin; }
  LED_LIST
#undef X

(我用一个简单的按位异或替换了切换宏)

这将扩展为:

static uint16_t countA = 100;
static uint16_t countB = 200;
static uint16_t countC = 300;

...

if(countA == 0) 
{ 
  countA = 100; 
  PORTA ^= 1u << 0; 
}
if(countB == 0) 
{ 
  countB = 200; 
  PORTB ^= 1u << 1; 
}
if(countC == 0) 
{ 
  countC = 300; 
  PORTC ^= 1u << 2; 
}

当然,除非必须,否则请避免使用此处所做的 16 位计数器,因为您使用的是糟糕的 8 位计数器。

【讨论】:

  • 我已经更新了这个问题。我不明白为什么有人会认为这个问题是关于切换 IO 的。我不关心IO,我只是以一个为例。就是将 2 行代码与一个宏合并到一行代码中。
  • @basknippels 不管怎样,同样的宏方案是完全通用的。 “X 宏”主要用于在维护现有代码库的同时避免重复。
【解决方案4】:

免责声明:我不建议使用此解决方案。

我曾尝试将其变成宏。这确实是可能的,但如果它更快,那是另一个问题。每次调用宏时都会创建一个新变量。

#include <stdio.h>

#define entryState(runOnce) int temp_state = runOnce; if (runOnce) runOnce = 0; if (temp_state)
#define timeExpired(someTimer, someInterval) int temp_expired = someTimer; if (!someTimer) someTimer = someInterval; if (!temp_expired)

int main(int argc, const char* argv[]) {

    int runOnce = 1;
    int someTimer = 0;
    int someInterval = 200;
    timeExpired(someTimer, someInterval) {
        printf("someTimer is Expired\n");
    }

    printf("someTimer: %i\n\n", someTimer);

    entryState(runOnce) {
        printf("this is running once\n");
    }

    printf("runOnce: %i\n", runOnce);

}

编译运行:

c:/repo $ gcc test.c -o test
c:/repo $ ./test.exe 
someTimer is Expired
someTimer: 200

this is running once
runOnce: 0

我现在手头没有 C51 编译器,所以我将 8051 的测试交给你。

【讨论】:

  • 除了这些宏真的很丑之外,这在 C90 中是行不通的,因为那时你不能在块的中间声明变量。
  • @Lundin 拍摄,你是对的。我的环境 atm 是带有 xc32-gcc 的 C99,这是可能的。是的,我完全同意,这个解决方案真的很难看,但这就是 OP 所要求的。添加免责声明
  • @Lundin 我也认为这是一个 XY 问题。如果我们有程序的附带代码/目的,也许很明显这些宏根本不需要,它只是重写一个函数。
  • 出色的答案! @Lundin你根本没有明白这一点。宏定义在你眼中可能很难看。但关键是你不应该首先查看定义。你应该能够使用它。对于 Arduino 项目,我一直使用它们的功能。我从来没有看过实际的函数体。我不必。我的状态机的每个状态都有 3 个部分“entryState”、“onState”和“exitState”。状态函数也被宏化成State(aName) { // code } 不用知道它是如何工作的,只要你能填就行。
  • @basknippels 不,这正是重点。您可以将整个 1000 LoC 代码隐藏在一些类似秘密函数的宏 BLIP BLOP 之间,这是完全有效的 C。问题是 C 程序员 完全理解 if(!ledT) { ledT = 200; ... 的含义。他们理解秘密宏语言expired(ledT, 200) { ... 的含义。只有你,现在。若干年后,你自己也不会明白。不要试图重新发明极其标准化的语言。
猜你喜欢
  • 1970-01-01
  • 2017-03-03
  • 2018-09-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-07-03
  • 2014-03-01
  • 1970-01-01
相关资源
最近更新 更多