You can manipulate the I/O ports directly from C++ without having to resort to assembly code.
The AVR micro in your Arduino (ATmega328P) has three ports (PORTB, PORTC and PORTD) with eight pins each (Px0, Px1, ... Px7). You control the ports using three 8-bit registers. Each bit in a register represents one physical pin (LSB is pin 0, MSB pin 7):
- DDRx: data direction register. A bit set to "1" corresponds to output mode, "0" to input.
- PORTx: data register. In output mode, "1" sets the port HIGH, "0" sets it LOW. In input mode, controls whether pull-ups are enabled ("1") or not ("0").
- PINx: input and toggle register. Read it to get the status of all eight pins of the port at once. Writing to it toggles (LOW -> HIGH or HIGH -> LOW) the selected pins (e.g.
PINC = 0b00100001;
toggles pins PC0 and PC5).
With an Arduino, you don't have access to all the pins of all the ports. To get the mapping between Arduino pin names and ports, look at any good Arduino pinout reference (e.g. on the Arduino Uno page in the "Documentation" tab). You can see for example that all pins of port D are available as Arduino digital pins D0 to D7 on an Uno for example. The analog pins are from PORTC.
Macros are provided (by the AVR toolchain) for port and pin names to make things easier.
For example, PORTB
is an alias for the PORTB register, you can assign to it as if it was a regular C++ variable. PB5
is the index of pin 5 in port B. So setting port B pin 5 to output without affecting the others can be written as:
DDRB |= (1 << PB5);
If you wanted to set the 4 lower pins of port D to output, and the 4 others to input, you could:
DDRD = 0b00001111; // or 0x0f
Here's an example of a minimal blink sketch:
void setup() {
// Set PB5 (D13 - builtin led) LOW
PORTB &= ~(1 << PB5);
// Set PB5 mode to OUTPUT
DDRB |= (1 << PB5);
}
void loop() {
delay(1000);
// toggle PB5
PINB |= (1 << PB5);
}
The assembly generated is optimal, single instructions to set or clear a bit in I/O memory:
(See See compiled assembly output (Visual Micro) for how to get it)
// setup
PORTB &= ~(1 << PB5);
1f4: 2d 98 cbi 0x05, 5 ; 5
DDRB |= (1 << PB5);
1f6: 25 9a sbi 0x04, 5 ; 4
// loop
PINB |= (1 << PB5);
246: 1d 9a sbi 0x03, 5 ; 3
Another example, that blinks a led connected to A3 only when A2 is high:
void setup() {
// Set PC2 and PC3 LOW (A2 and A3)
PORTC &= ~((1 << PC2) | (1 << PC3));
// Set PC2 to input, PC3 mode to output
DDRC = (DDRC & ~(1 << PC2)) | (1 << PC3);
}
void loop() {
delay(1000);
// toggle PC3 only if PC2 is high
if (PINC & (1 << PC2)) {
PINC = (1 << PC3);
}
}
Again, assembly generated is quite terse:
if (PINC & (1 << PC2)) {
PINC = (1 << PC3);
202: 18 e0 ldi r17, 0x08 ; 8
252: 32 99 sbic 0x06, 2 ; 6
254: 16 b9 out 0x06, r17 ; 6
(The SBIC instruction is fun: skip the next instruction if a bit in I/O memory is clear.)
You can even save the use of that register r17
by using PINC |= ...
in this case:
// toggle PC3 only if PC2 is high
if (PINC & (1 << PC2)) {
PINC |= (1 << PC3);
250: 32 99 sbic 0x06, 2 ; 6
252: 33 9a sbi 0x06, 3 ; 6
Of course you can also resort to inline assembly, but that is an art in and of itself, and the syntax a bit peculiar. But for simple things, it is straightforward:
asm("sbi 0x03, 5;"); // toggle PB5
Some references: