从宏名称中获取字符以优化宏功能
Get characters from macro name to optimise macro function
我正在使用以下宏函数
#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};
行实际上是多余的,因为 DDRA
中的 A
总是在 FOO
值中重复,即 PA1
和 PA2
。
理想情况下,它可以完全删除,宏更改为类似
的内容
#define setAsOutput(bit, i) { DDR ## <second char of bit[i]> |= (1 << bit[i]); }
但是获取 bit[i]
的 name 的第二个字符似乎不可能。
有没有办法重写宏函数,使 FOO_DDR
不需要显式定义,而是可以从 {PA1, PA2}
隐含?
如果您能提供一个 MCVE 以便其他人可以轻松地编译您的代码、查看它的工作原理并尝试调整它,通常会有所帮助。
您不需要在代码中定义 DDRA
和 PA1
等内容。只需将适当的选项传递给编译器以指定您正在使用的 AVR(例如 -mmcu=atmega1284p
),然后在程序顶部添加 #include <avr/io.h>
以获得这些定义。将这些定义从 io.h
复制到 Whosebug 上的问题中通常没有多大意义,因为它们非常标准。这些定义来自 avr-libc,所以如果你真的想提供这些细节,你可以只说你使用的是什么版本的 avr-libc。
你的问题的一个主要前提是你用数组和宏发布的代码等同于 DDRA |= (1 << PA1);
。不幸的是,这个前提是不正确的。当 GCC 看到 DDRA |= (1 << PA1);
时,它实际上可以将其编译为单个原子 AVR 指令,该指令设置 DDRA 寄存器的位 1。当 GCC 看到你的代码时,它会做一些更复杂的事情,最终会读取、写入和修改寄存器。因此,数组代码会浪费 CPU 个周期,如果中断可能会修改 DDRA 寄存器,则使用起来不安全。
如果你不相信我,你可以看看这个 godbolt.org link 它比较了两种方法的程序集:
看起来您实际上可以通过向数组添加 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 的人犯了在他们的 pinMode
和 digitalWrite
函数中以浪费的方式访问这些数组的错误。)
请注意,使用我上面提供的代码,存在很大的风险,即您会不小心传递一个不是编译时常量的 pin 号,因此编译器将无法优化它,并产生wasteful/unsafe 代码。这就是为什么最好像 FastGPIO library 那样使用内联汇编和 C++ 模板的原因之一。
如果您希望代码等同于 DDRA |= (1 << PA1);
- 即在编译时生成的最简单的指令,没有 reading/writing 数组和指向 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 << PA1);
但是
如果你想将它们存储在数组中并按数组索引访问,就像你的示例中那样,那么除了定义两个数组或结构数组之外别无他法,其中两个元素都将包含位号或位掩码,以及指向 IO/register 的指针。
因为,尽管名称 PA1
PA2
等中有 A
字母,但在运行时它将被编译成它的值。 IE。 'PA1' 将是 1,而且 PB1
也将是 1。因此编译器无法知道访问了哪个寄存器,仅考虑该数组内的索引。
但在这里我可以给你几个生活小窍门:
1) 由于寄存器 PINx、DDRx、PORTx 总是按该顺序连续进行(请参阅数据表中的寄存器集摘要),因此您不需要将它们全部存储,仅存储对 PINx 寄存器的引用就足够了,并计算 DDRx 和 PORTx 的位置,只需将地址加 1 或 2,因为 AVR 有指令通过位移来间接访问内存,代码将足够有效。
2) 这些寄存器位于较低的内存地址,因此您可以将它们转换为 byte
并在访问时将它们转换回指针,而不是存储 2/4 字节的指针。它不仅会节省space,而且会加快速度。此外,将这种表存储在闪存中始终是一个好习惯,而不是浪费 RAM。
3) AVR 体系结构只有一个位置位移指令,因此 (1 << x) 其中 x 在编译时未知 - 被编译为循环,这可能是此类代码中需要最多时间的部分.因此,您可能想要存储 uint8_t FOO[] = {(1 << PA1), (1 << PA2)};
而不是存储 uint8_t FOO[] = {PA1, PA2};
- 即预先计算的掩码值。
最后我利用 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 编写的,但应该很容易适应其他板。
我正在使用以下宏函数
#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};
行实际上是多余的,因为 DDRA
中的 A
总是在 FOO
值中重复,即 PA1
和 PA2
。
理想情况下,它可以完全删除,宏更改为类似
的内容#define setAsOutput(bit, i) { DDR ## <second char of bit[i]> |= (1 << bit[i]); }
但是获取 bit[i]
的 name 的第二个字符似乎不可能。
有没有办法重写宏函数,使 FOO_DDR
不需要显式定义,而是可以从 {PA1, PA2}
隐含?
如果您能提供一个 MCVE 以便其他人可以轻松地编译您的代码、查看它的工作原理并尝试调整它,通常会有所帮助。
您不需要在代码中定义 DDRA
和 PA1
等内容。只需将适当的选项传递给编译器以指定您正在使用的 AVR(例如 -mmcu=atmega1284p
),然后在程序顶部添加 #include <avr/io.h>
以获得这些定义。将这些定义从 io.h
复制到 Whosebug 上的问题中通常没有多大意义,因为它们非常标准。这些定义来自 avr-libc,所以如果你真的想提供这些细节,你可以只说你使用的是什么版本的 avr-libc。
你的问题的一个主要前提是你用数组和宏发布的代码等同于 DDRA |= (1 << PA1);
。不幸的是,这个前提是不正确的。当 GCC 看到 DDRA |= (1 << PA1);
时,它实际上可以将其编译为单个原子 AVR 指令,该指令设置 DDRA 寄存器的位 1。当 GCC 看到你的代码时,它会做一些更复杂的事情,最终会读取、写入和修改寄存器。因此,数组代码会浪费 CPU 个周期,如果中断可能会修改 DDRA 寄存器,则使用起来不安全。
如果你不相信我,你可以看看这个 godbolt.org link 它比较了两种方法的程序集:
看起来您实际上可以通过向数组添加 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 的人犯了在他们的 pinMode
和 digitalWrite
函数中以浪费的方式访问这些数组的错误。)
请注意,使用我上面提供的代码,存在很大的风险,即您会不小心传递一个不是编译时常量的 pin 号,因此编译器将无法优化它,并产生wasteful/unsafe 代码。这就是为什么最好像 FastGPIO library 那样使用内联汇编和 C++ 模板的原因之一。
如果您希望代码等同于 DDRA |= (1 << PA1);
- 即在编译时生成的最简单的指令,没有 reading/writing 数组和指向 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 << PA1);
但是
如果你想将它们存储在数组中并按数组索引访问,就像你的示例中那样,那么除了定义两个数组或结构数组之外别无他法,其中两个元素都将包含位号或位掩码,以及指向 IO/register 的指针。
因为,尽管名称 PA1
PA2
等中有 A
字母,但在运行时它将被编译成它的值。 IE。 'PA1' 将是 1,而且 PB1
也将是 1。因此编译器无法知道访问了哪个寄存器,仅考虑该数组内的索引。
但在这里我可以给你几个生活小窍门:
1) 由于寄存器 PINx、DDRx、PORTx 总是按该顺序连续进行(请参阅数据表中的寄存器集摘要),因此您不需要将它们全部存储,仅存储对 PINx 寄存器的引用就足够了,并计算 DDRx 和 PORTx 的位置,只需将地址加 1 或 2,因为 AVR 有指令通过位移来间接访问内存,代码将足够有效。
2) 这些寄存器位于较低的内存地址,因此您可以将它们转换为 byte
并在访问时将它们转换回指针,而不是存储 2/4 字节的指针。它不仅会节省space,而且会加快速度。此外,将这种表存储在闪存中始终是一个好习惯,而不是浪费 RAM。
3) AVR 体系结构只有一个位置位移指令,因此 (1 << x) 其中 x 在编译时未知 - 被编译为循环,这可能是此类代码中需要最多时间的部分.因此,您可能想要存储 uint8_t FOO[] = {(1 << PA1), (1 << PA2)};
而不是存储 uint8_t FOO[] = {PA1, PA2};
- 即预先计算的掩码值。
最后我利用 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 编写的,但应该很容易适应其他板。