【问题标题】:Get characters from macro name to optimise macro function从宏名中获取字符以优化宏功能
【发布时间】:2018-05-26 21:34:56
【问题描述】:

我正在使用以下宏函数

#define setAsOutput(bit, i)   { *bit ## _DDR[i] |= (1 << bit[i]); }

简化定义和设置一些寄存器值

// registers
volatile uint8_t *FOO_DDR[] = {&DDRA, &DDRA};
uint8_t FOO[] = {PA1, PA2};

setAsOutput(FOO, 0);

// these are defined somewhere else
#define PA1     1
#define PA2     2
#define DDRA    _SFR_IO8(0X01)

这给出了相当于

的代码
DDRA |= (1 << PA1);

但是volatile uint8_t *FOO_DDR[] = {&amp;DDRA, &amp;DDRA}; 行实际上是多余的,因为DDRA 中的A 总是在FOO 值中重复,即PA1PA2

理想情况下,它可以完全删除,宏更改为类似

#define setAsOutput(bit, i)   { DDR ## <second char of bit[i]> |= (1 << bit[i]); }

但似乎无法获得bit[i]名称 的第二个字符。

有没有办法重写宏函数,使FOO_DDR 不需要显式定义,而是可以从{PA1, PA2} 中隐含?

【问题讨论】:

  • 宏不能访问数组元素,宏在处理C代码之前被展开。
  • @Barmar 所以至少我总是需要定义值A1 来生成DDRAPA1?请注意,它始终是 DDRxPxy

标签: c arduino macros c-preprocessor avr


【解决方案1】:

如果您能提供一个MCVE 通常会有所帮助,这样其他人就可以轻松编译您的代码,了解它是如何工作的,并尝试对其进行调整。

您不需要在代码中定义 DDRAPA1 之类的内容。只需将适当的选项传递给编译器以指定您正在使用的 AVR(例如 -mmcu=atmega1284p),然后在程序顶部添加 #include &lt;avr/io.h&gt; 以获取这些定义。将io.h 中的这些定义复制到 StackOverflow 上的问题中通常没有多大意义,因为它们非常标准。这些定义来自 avr-libc,所以如果你真的想提供这些细节,你可以直接说出你使用的是什么版本的 avr-libc。

您问题的一个主要前提是您发布的带有数组和宏的代码等同于DDRA |= (1 &lt;&lt; PA1);。不幸的是,这个前提是不正确的。当 GCC 看到 DDRA |= (1 &lt;&lt; PA1); 时,它实际上可以将其编译为单个原子 AVR 指令,该指令设置 DDRA 寄存器的第 1 位。当 GCC 看到你的代码时,它会做一些更复杂的事情,最终会读取、写入和修改寄存器。因此,如果中断可能会修改 DDRA 寄存器,则数组代码会浪费 CPU 周期并且使用起来不安全。

如果你不相信我,你可以看看这个 godbolt.org 链接,它比较了两种方法的程序集:

https://godbolt.org/g/jddzpK

看起来您实际上可以通过将const 限定符添加到您的数组来解决此问题。然后编译器会知道你的数组在编译时持有什么值,并可以生成好的代码。

volatile uint8_t * const FOO_DDR[] = {&DDRA, &DDRA};
uint8_t const FOO[] = {PA1, PA2};

现在谈谈您的主要问题,即如何摆脱冗余数组。我不认为有一种简单的方法可以做到这一点,并且在您的程序中有两个 const 数组并不是什么大不了的事,无论如何它们可能会在编译时被优化掉。您可以做的是扩展这些数组,以便它们包含芯片上每个引脚的条目。然后,当您想写入引脚时,您只需使用作为数组索引的引脚号(而不需要定义新数组)。然后,您的绝大多数代码将处理这些引脚编号,而不必担心数组。所以我会这样写:

#include <avr/io.h>

// Here is a general GPIO library for your chip.
// TODO: expand these arrays to cover every pin on the chip

#define setAsOutput(i)   { *pin_dir[i] |= (1 << pin_bit[i]); }
#define setHigh(i)   { *pin_value[i] |= (1 << pin_bit[i]); }

static volatile uint8_t * const pin_dir[] = {
  &DDRA,  // Pin 0
  &DDRA,  // Pin 1
};

static volatile uint8_t * const pin_value[] = {
  &PORTA,  // Pin 0
  &PORTA,  // Pin 1
};

static const uint8_t pin_bit[] = {
  PA1,    // Pin 0
  PA2,    // Pin 1
};

// Pin definitions for your particular project.
// (e.g. pin 0 is connected to a green LED)

#define GREEN_LED_PIN 0

void nice()
{
  setAsOutput(GREEN_LED_PIN);
  setHigh(GREEN_LED_PIN);
}

上面的每个 GPIO 函数调用最终都会编译成一条汇编指令。

如果您深入研究 Arduino Core 代码,您会发现像这样的数组。 (但是 Arduino 人错误地在他们的 pinModedigitalWrite 函数中以浪费的方式访问这些数组。)

请注意,使用我上面提供的代码,您可能会意外传递一个不是编译时常量的引脚号,因此编译器将无法对其进行优化,并产生浪费/不安全的代码。这就是为什么最好使用像 FastGPIO library 这样的内联汇编和 C++ 模板的原因之一。

【讨论】:

  • 感谢您提供有用的答案和const 提示。
【解决方案2】:

如果您希望代码等效于DDRA |= (1 &lt;&lt; PA1); - 即在编译时生成的最简单指令,无需读取/写入数组和指向 IO 寄存器的指针。你可以这样做。

1) 假设我们已经定义了某个地方(例如,通过 &lt;avr/io.h&gt;

#define PA1     1
#define PA2     2
...
#define DDRA    _SFR_IO8(0X01)
#define PB1     1
#define PB2     2
...
#define DDRB    _SFR_IO8(0X01)

2) 你想要这样的声明:

#define BIG_RED_LED PA1
#define SMALL_GREEN_LED PB2

然后只是像使用它们一样

setAsOutput(BIG_RED_LED);
setAsOutput(SMALL_GREEN_LED);
setLow(BIG_RED_LED);
setHigh(SMALL_GREEN_LED);

等等,其中每一行都是对相应 IO 寄存器中的一个 BIT 的简单写入。

要实现这一点,您可以定义大量

#define DDR_PA0 DDRA
#define PORT_PA0 PORTA
#define PIN_PA0 PINA
#define DDR_PA1 DDRA
#define PORT_PA1 PORTA
#define PIN_PA1 PINA
...
#define DDR_PB0 DDRB
#define PORT_PB0 PORTB
#define PIN_PB0 PINB
...

然后

#define setAsOutput(px)   { DDR_ ## px |= (1 << px); }
#define setHigh(px)   { PORT_ ## px |= (1 << px); }
#define setLow(px)   { PORT_ ## px &= ~(1 << px); }
etc.

然后,每次在您的代码中发生setAsOutput(PA1) 之类的事情时,它都会被编译为完全与 DDRA |= (1

但是 如果您想将它们存储在数组中并通过数组索引访问,就像在您的示例中一样,那么除了定义两个数组或结构数组之外,您别无他法,其中两个元素都将包含位数或位掩码,以及指向 IO/寄存器的指针。 因为,虽然名称 PA1 PA2 等中有 A 字母,但在运行时它将被编译成它的值。 IE。 'PA1' 将为 1,但 PB1 也将为 1。因此编译器无法知道访问了哪个寄存器,只考虑该数组中的索引。

但是在这里我可以给你一些生活小窍门: 1) 由于寄存器 PINx, DDRx, PORTx 总是按这个顺序连续运行(请参阅数据表中的寄存器集摘要),您不需要全部存储它们,只存储对 PINx 寄存器的引用就足够了,并计算 DDRx 和 PORTx 的位置,只需在地址上加 1 或 2,因为 AVR 有指令可以用位移来取消内存访问,因此代码将足够有效。 2) 这些寄存器位于较低的内存地址中,因此您可以将它们转换为 byte 并在访问时将它们转换回指针,而不是存储 2/4 字节的指针。它不仅可以节省空间,还可以加快速度。此外,将这种表存储在闪存中始终是一个好习惯,而不是浪费 RAM。 3) AVR 架构只有一个位置位移指令,因此 (1 uint8_t FOO[] = {(1 << PA1), (1 << PA2)};,而不是存储uint8_t FOO[] = {PA1, PA2}; - 即预先计算的掩码值。

【讨论】:

  • 谢谢,我最终利用了连续的内存地址,但增加了数组功能。
【解决方案3】:

最后我利用avr/sfr_defs.h 中的_MMIO_BYTE 宏函数来建立新的位操作函数:

#define SET_OUTPUT(pin)     (_MMIO_BYTE(OFFSET_ADDR((pin)[0] + 0x1)) |=  _BV((pin)[1]))
#define SET_INPUT(pin)      (_MMIO_BYTE(OFFSET_ADDR((pin)[0] + 0x1)) &= ~_BV((pin)[1]))
// etc

这可以将引脚定义为引脚数组或单个引脚:

#define NUM_LEDS 3

const uint16_t LEDS[NUM_LEDS][2] = {
    {PB, 4},
    {PB, 5},
    {PB, 6}
};
const uint16_t BUTTON[2] = {PB, 7};

然后可以像这样操作引脚:

SET_INPUT(BUTTON);
ENABLE_PULLUP(BUTTON);

for (int i = 0; i < NUM_LEDS; ++i) {
    SET_OUTPUT(LEDS[i]);
    SET_HIGH(LEDS[i]);
}

源代码在这里:https://github.com/morefigs/avr-bit-funcs

这只是为 Mega 2560 编写的,但应该很容易适应其他板。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-04-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-05-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多