代码之家  ›  专栏  ›  技术社区  ›  101

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

  •  0
  • 101  · 技术社区  · 6 年前

    我正在使用以下宏函数

    #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 PA2 .

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

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

    但是得到第二个角色 名称 属于 bit[i] 似乎不可能。

    有没有办法重写宏函数以便 FOO_DDR 不需要明确定义,而是可以从 {PA1, PA2} ?

    3 回复  |  直到 6 年前
        1
  •  1
  •   101    6 年前

    如果你能提供一个 MCVE 所以其他人可以很容易地编译你的代码,看看它是如何工作的,并尝试调整它。

    你不需要定义这样的事情 DDRA PA1 在你的代码中。只需将适当的选项传递给编译器,以指定正在使用的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周期,使用起来不安全。

    如果你不相信我,你可以看看这个神箭。org链接,用于比较两种方法的程序集:

    https://godbolt.org/g/jddzpK

    看起来你可以通过添加 const 数组的限定符。然后,编译器将知道数组在编译时保存的值,并可以生成好的代码。

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

    现在转到您的主要问题,即如何消除冗余阵列。我不认为有一种简单的方法可以做到这一点,在你的程序中有两个常量数组并不是什么大问题,它们可能会在编译时被优化掉。你可以做的是扩展这些阵列,使它们包含芯片上每个引脚的条目。然后,当你想写入一个管脚时,你只需要使用一个管脚号,它是数组的索引(而不需要定义新的数组)。然后,您的大部分代码都会处理这些pin码,而不必担心阵列。下面是我的写作方式:

    #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码,因此编译器将无法对其进行优化,并生成浪费/不安全的代码。这就是为什么最好使用内联汇编和C++模板,比如 FastGPIO library

        2
  •  1
  •   AterLux    6 年前

    如果希望代码与 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寄存器中的一位的简单写入。

    为了实现这一点,你可以定义成吨的

    #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/寄存器的指针。 虽然名字 PA1 PA2 等等 A 在运行时,它将被编译成它的值。也就是说,“PA1”将是1,但是 PB1 也会是1。因此,编译器无法知道访问了哪个寄存器,只考虑数组中的索引。

    但在这里,我可以给你一些生活小技巧: 1) 由于寄存器PINx、DDRx、PORTx总是按顺序依次运行(请参阅数据表中的寄存器集摘要),您不需要全部存储它们,只存储对PINx寄存器的引用就足够了,只需在地址中添加1或2即可计算DDRx和PORTx的位置,因为AVR具有指令,可以通过移位来阻止内存访问,代码将足够有效。 2) 这些寄存器位于较低的内存地址中,因此不必存储2/4字节的指针,您可以将它们转换为 byte 并在访问时将其转换回指针。它不仅可以节省空间,还可以加快速度。此外,将此类表存储在闪存中,而不是浪费RAM,这也是一种很好的做法。 3) AVR体系结构只有一个位置位移位指令,因此(1<<x),其中x在编译时是未知的,被编译为循环,这可能是此类代码中最需要时间的部分。所以,与其储存 uint8_t FOO[] = {PA1, PA2}; 你可能想储存 uint8_t FOO[] = {(1 << PA1), (1 << PA2)}; -即预先计算的遮罩值。

        3
  •  0
  •   101    6 年前

    最后我利用了 _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编写的,但应该很容易适应其他板。