从宏名称中获取字符以优化宏功能

时间:2018-05-26 21:34:57

标签: c arduino macros c-preprocessor avr

我正在使用以下宏功能

#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[] = {&DDRA, &DDRA};行实际上是多余的,因为A中的DDRA始终在FOO值中重复,即PA1和{{1} }。

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

PA2

但似乎无法获取#define setAsOutput(bit, i) { DDR ## <second char of bit[i]> |= (1 << bit[i]); } 名称的第二个字符。

有没有办法重写宏函数,因此bit[i]不需要明确定义,而是可以隐含FOO_DDR

3 个答案:

答案 0 :(得分:1)

如果您能提供MCVE通常会有帮助,以便其他人可以轻松编译您的代码,查看其工作原理,并尝试调整它。

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

您问题的一个主要前提是您使用数组和宏发布的代码等同于DDRA |= (1 << PA1);。不幸的是,这个前提是不正确的。当GCC看到DDRA |= (1 << 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核心代码中挖掘,你会发现这样的数组。 (但Arduino人错误地在pinModedigitalWrite函数中以浪费的方式访问这些数组。)

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

答案 1 :(得分:1)

如果希望代码等效于DDRA |= (1 << PA1); - 即在编译时没有读/写数组和指向IO寄存器的指针的最简单指令。你可以这样做。

1)我们假设我们已经在某处定义(例如通过<avr/io.h>

#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&lt;&lt;&lt;&lt;&lt;&lt; PA1);

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

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

答案 2 :(得分:0)

最后,我使用了_MMIO_BYTE中的avr/sfr_defs.h宏函数,将新的位操作函数基于:

#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编写的,但应易于适应其他主板。