用 C 编写 AVR

Programming AVR in C

我使用的是 Atmega328。我有 "randomly" 沿引出线分布的输出引脚,即它们不属于相同的端口。例如,我的输出引脚可能是 PB0、PB4、PC1、PC3 和 PD1。

我一直用汇编来给微控制器编程,所以这是我第一次使用C。我想知道的是,是否有办法避免对每个引脚使用DDRx和Px来设置或清除它们.

例如,我想用这样的东西来设置端口 B 的第一位:

#define NAME_1 DDRB,0

sbi NAME_1;

这可能吗?

编辑:

可能是我没表达清楚。我想要的是能够使用一些有意义的名称来引用某些 I/O 端口引脚。例如命名为PD3"blue_LED",这样代码的可读性更好,如果以后改变了蓝色LED的位置,也可以方便的修改代码。换句话说,我希望能够打开和关闭某些名称未被硬编码的引脚。有办法吗?

我用 C++ 写了这样的东西:

//IO = I/O Subsystem
inline void Io_LEDred_Configure() {DDRB  |=   1<<0; } //Port B0
inline void Io_LEDred_On()        {PORTB |=   1<<0; }
inline void Io_LEDred_Off()       {PORTB &= ~(1<<0);}

inline void Io_LEDgreen_Configure() {DDRD  |=   1<<3; } //Port D3
inline void Io_LEDgreen_On()        {PORTD |=   1<<3; }
inline void Io_LEDgreen_Off()       {PORTD &= ~(1<<3);}

如果您必须将一个 IO 切换到另一个端口,您只需更改这三行即可。

在 C++ 中,编译器发出的代码与您作为汇编编码器编写的代码完全相同。在 C 中,您必须使用常规函数。但是这些电话会带来一些开销。如果你想避免这种情况,你必须使用宏:

#define IO_LEDRED_CONFIGURE DDRB  |= 1<<0
#define IO_LEDRED_ON        PORTB |= 1<<0
...

sbi指令的特殊之处在于它直接操作AVR平台上I/O端口中的一个位。使用 I/O 端口的正常过程是您必须使用其他指令(如 out)在 I/O 端口和寄存器之间复制整个单词。

也就是说,C 中没有 sbi。C 只是不知道某个特定平台的这些特殊功能。对于您举的程序集,您将在 C:

中编写
DDRB |= 1<<0;

我个人认为这看起来很简洁,但是你当然可以定义一个宏

#define sbi(x,b) (x) |= 1<<(b)

sbi(DDRB, 0);

(其中 b 是“位数”),也许反过来

#define cbi(x,b) (x) &= ~(1<<(b))

cbi(DDRB, 0)

这可行,但我推荐使用它。虽然第一个符号 DDRB |= 1<<0; 对任何 C 程序员来说都是显而易见的,但使用这样的宏可能不是。

最后一点,如果您担心性能:我还没有验证这一点,但我很确定 avr-gcc 足够聪明,可以发出 sbicbi 指令,当对 I/O 端口的位掩码操作实际上只改变一个位时。 edit:请参阅 JLH 的实验结果回答 gcc-avr indeed 足够聪明,可以发出这些 sbi/cbi 说明。

我使用 avg-gcc 对 Atmel AVR 进行编程,支持包绝对了解 cbi、sbi 以及与此相关的所有说明。因此,除非您愿意,否则无需求助于汇编。这里有一些反汇编的 C 来证明它。

        PORTD |= (1 << PD0);
d8: 58 9a           sbi 0x0b, 0 ; 11

现在我将向您展示如何操作。您可以很容易地设置每个端口的每个位来进行输入或输出。

DDRA |= (1<<PA0);

具有使端口 A 上的引脚 0 成为输出的效果,如图所示:

bit76543210
   00000001  // or-ing the 1 adds 1 leaving the other bits alone.

要同时将引脚 A3 设为输出,请执行以下操作:

DDRA |= (1<<PA3);
bit76543210
   00001001 //  now the DDRA register looks like this, pins 0 and 3 set as outputs

很乏味,对吧?好吧,这些右侧表达式的计算结果为常量,因此您可以将它们组合成一个语句:

DDRA |= (1<<PA0) | (1<<PA3);

编译器会将右侧折叠成一个常量,然后|=将其存入寄存器。因此,您实际上每个端口只需要一个这样的语句,编译器使其非常高效。

它负责方向——输入或输出。接下来是设置和清除输出。

要设置输出:

PORTD |= (1<<PD); // turns pin 0 on port D on (high) as an output.
PORTD &= ~(1<<PD); // turns pin 0 on port D off (low) as an output.
PORTD ^= (1<<PD); // toggles it (high-> low) or (low->high as an output.

编辑:

对于你命名的关于使用有意义的名字的额外要求,你当然可以做到。我经常这样做是为了避免记住什么与什么有关。例如,来自同一个项目:

#define LED_INDICATOR_PIN   PA0
#define LED_INDICATOR_PORT  PORTA
#define LED_INDICATOR_DDR   DDRA

我在代码中引用了 'friendly' 个名称:

void initialize(void)
{
    // Set up an output pin to blink an LED to show sign of life.
    LED_INDICATOR_DDR |= (1 << LED_INDICATOR_PIN);

    // Set up TTY port.
    init_uart();
}

void toggleLED(void)
{
    LED_INDICATOR_PORT ^= (1 << LED_INDICATOR_PIN);
}

更容易阅读。那是你想要的吗?我知道我每次都这样做。