Arduino Inline Assembly Tutorial (Ports & Pins)

port

Mapping Memory

The 3 basic types of memory in the arduino were discussed in tutorial #1. Again, here we are interested in SRAM data memory, which the arduino uses as memory-mapped IO. MMIO means that a part of the SRAM space is reserved for registers that control peripherals (i.e., ports, timers, SPI, USART, etc.). Changing the content of the memory in this area changes the value controlling a peripheral.

A pin is a single addressable input/output line on a port. A port has a MMIO address. A port typically has 8 pins assigned to it, which means a port maps nicely to a byte memory location.

We have seen that the bottom of the SRAM address space is reserved for direct access to the general purpose registers (r0-r31). The IO register space is mapped into the SRAM just above these 32 GPRs and is divided into two sections. The first section is for Standard IO Registers (primarily the ports, but includes various other peripherals as well). The second section is for the Extended IO Registers, which includes a mishmash of peripherals. The actual locations can be found towards the rear of the data sheet for the arduino’s ATmega microcontroller. Actual SRAM is available behind the IO register area, starting at address 0x100 (otherwise known as RAMSTART).

Bit by Bit

The lower IO register portion of the SRAM is 32 bytes long (addresses 0 through 31, or 0x1f hexadecimal) and is further distinguished by the fact that it is bit-accessible using the SBI, CBI, SBIS and SBIC instructions. Above this area, IO registers are addressable only by byte value. The IN and OUT instructions can only access the Standard IO registers below 64 (0x3f and below). The 160 bytes of Extended IO registers are located at 96-255 (0x60 – 0xff). To further confuse matters, when using the LD and ST instructions, 0x20 must be added to the IO Register number. Not all of these registers are utilized. Hopefully, we can clear up some of this confusion.

Standard IO Registers (0-64):

Addr    Name     Bit7   Bit6   Bit5   Bit4   Bit3   Bit2   Bit1   Bit0
...
0x03   PINB     PINB7  PINB6  PINB5  PINB4  PINB3  PINB2  PINB1  PINB0
0x04   DDRB     DDB7   DDB6   DDB5   DDB4   DDB3   DDB2   DDB1   DDB0
0x05   PORTB    PORTB7 PORTB6 PORTB5 PORTB4 PORTB3 PORTB2 PORTB1 PORTB0
0x06   PINC     x      PINC6  PINC5  PINC4  PINC3  PINC2  PINC1  PINC0
0x07   DDRC     x      DDC6   DDC5   DDC4   DDC3   DDC2   DDC1   DDC0
0x08   PORTC    x      PORTC6 PORTC5 PORTC4 PORTC3 PORTC2 PORTC1 PORTC0
0x09   PIND     PIND7  PIND6  PIND5  PIND4  PIND3  PIND2  PIND1  PIND0
0x0A   DDRD     DDD7   DDD6   DDD5   DDD4   DDD3   DDD2   DDD1   DDD0
0x0B   PORTD    PORTD7 PORTD6 PORTD5 PORTD4 PORTD3 PORTD2 PORTD1 PORTD0
...
0x15   TIFR0    x      x      x      x      x      OCF0B  OCF0A  TOV0
0x16   TIFR1    x      x      ICF1   x      x      OCF1B  OCF1A  TOV1
0x17   TIFR2    x      x      x      x      x      OCF2B  OCF2A  TOV2
...
0x1B   PCIFR    x      x      x      x      x      PCIF2  PCIF1  PCIF0
0x1C   EIFR     x      x      x      x      x      x      INTF1  INTF0
0x1D   EIMSK    x      x      x      x      x      x      INT1   INT0
0x1E   GPIOR0             General Purpose IO Register 0
0x1F   EECR     x      x      EEPM1  EEPM0  EERIE  EEMPE  EEPE   EERE
0x20   EEDR                   EEPROM Data Register
0x21   EEARL             EEPROM Address Register Low Byte
0x22   EEARH             EEPROM Address Register High Byte
0x23   GTCCR    TSM    x      x      x      x      x      PSRASY PSRSYNC
0x24   TCCR0A   COM0A1 COM0A0 COM0B1 COM0B0 x      x      WGM01  WGM00
0x25   TCCR0B   FOC0A  FOC0B  x      x      WGM02  CS02   CS01   CS00
0x26   TCNT0                 Timer/Counter 0 (8-bit)
0x27   OCR0A         Timer/Counter 0 Output Compare Register A
0x28   OCR0B         Timer/Counter 0 Output Compare Register B
...
0x2A   GPIOR1             General Purpose IO Register 1
0x2B   GPIOR2             General Purpose IO Register 2
0x2C   SPCR     SPIE   SPE    DORD   MSTR   CPOL   CPHA   SPR1   SPR0
0x2D   SPSR     SPIF   WCOL   x      x      x      x      x      SPI2X
0x2E   SPDR                     SPI Data Register
...
0x30   ACSR     ACD    ACBG   ACO    ACI    ACIE   ACIC   ACIS1  ACIS0
...
0x33   SMCR     x      x      x      x      SM2    SM1    SM0    SE
0x34   MCUSR    x      x      x      x      WDRF   BORF   EXTRF  PORF
0x35   MCUCR    x      BODS   BODSE  PUD    x      x      IVSEL  IVCE
...
0x37   SPMCSR   SPMIE  RWWSB  SIGRD  RWWSRE BLBSET PGWRT  PGERS  SPMEN
...
0x3D   SPL      SP7    SP6    SP5    SP4    SP3    SP2    SP1    SP0
0x3E   SPH      x      x      x      x      x      SP10   SP9    SP8
0x3F   SREG     I      T      H      S      V      N      Z      C

Bits of Ports

Notice how the bits of the port registers compose all of the individual pins:

port registers

The fact that the Standard IO Registers are addressable by bit is a noteworthy feature of our Arduino. The typical method for addressing MMIO is through the use of a Read-Modify-Write strategy. Since port registers contain bits controlling several pins (typically 0 through 7), simply writing to the port without regard to the other bits would overwrite the status of the other pins too. A Read-Modify-Write strategy properly preserves the unaffected bits of the register. The Read-Write-Modify procedure is covered in our next tutorial which discusses bit manipulation.

Here are two key bit instructions: SBI is the mnemonic for Set Bit in I/o register. SBI sets a specified bit in the register. This instruction operates on the lower 32 MMIO addresses. This is not to be confused with the registers r0-r31. And keep in mind, the CBI and SBI instructions work with registers 0x00 to 0x1F only.

CBI is the mnemonic for Clear Bit in I/o register, and is the antipode to SBI. CBI clears a specified bit in an I/O register. Again, this instruction operates on the lower 32 I/O registers.

IN and its antipode, OUT are also used to load and store I/O locations to and from registers. Remember, the IN and OUT instructions can only access the IO Registers between 0x00-0x3f.

IN performs virtually the same function as LDS (we covered LDS earlier here, if you would like to refresh your memory, pun intended). This may lead you to ask, “why use IN vs. LDS?” IN has the following advantages over LDS:

  • IN executes twice as fast as LDS (one machine cycle vs. two).
  • IN is a 2-byte instruction and LDS is 4-bytes long.
  • LDS works on all SRAM, IN only on the Standard IO Registers.

Digital Right?

Here is an example of how to set the Arduino digital pin #13 HIGH (that’s Pin #5 of Port B). This is equivalent to the Arduino command, digitalWrite(13, HIGH). However, our inline version uses approximately 170 bytes less (see this post about “Yak Shaving” for further information).

asm (
  "sbi %0, %1 \n"
  : : "I" (_SFR_IO_ADDR(PORTB)), "I" (PORTB5)
);

One curious thing you should have noticed aboved, is the use of the C MACRO, _SFR_IO_ADDR. Since the port names are defined with their “memory” address in mind, in order to use them as parameters of the instructions SBI/CBI, (including IN, OUT, SBIS/SBIC), 32 (0x20 hexadecimal) must be subtracted first. Otherwise we will be addressing the wrong location.

For example, examine the following definitions used to address PORTB:

//sfr_defs.h
#define PORTB _SFR_IO8(0x05)
#define __SFR_OFFSET 0x20
#define _SFR_IO8(io_addr) _MMIO_BYTE ((io_addr) + __SFR_OFFSET)
#define _MMIO_BYTE(mem_addr) (*(volatile uint8_t *)(mem_addr))

#define _SFR_IO_ADDR(sfr) ((sfr) - __SFR_OFFSET)

Did you enjoy weaving your way through that convolution? The result is that PORTB = 0x25, however we want to address location 0x05. For simplicity sake, we’ll just use the _SFR_IO_ADDR macro. Note that all of the peripherals use this scheme, not just the IO pins. I suggest reading the iom328p.h header-file if you’re interested in the locations for other peripherals.

Also, keep in mind, on the Arduino, when using a pin that is also connected to a PWM timer (pins 3, 5, 6, 9, 10 or 11), PWM should be disabled first. However, that’s a topic of a later tutorial.

Go LOW

SBI is not enough to completely control the ports by itself. It only allows setting the pin to a high state. To conversely set a pin LOW, we need to use the CBI instruction. The following is the equivalent of the Arduino command, digitalWrite(13, LOW).

asm (
  "cbi %0, %1 \n"
  : : "I" (_SFR_IO_ADDR(PORTB)), "I" (PORTB5)
);

Again, this saves many (many) bytes.

Don’t Forget to Mode the Pin

Prior to using a pin, it needs to be configured to behave either as an input or an output. The Arduino function for setting the pin mode is pinMode(13, OUTPUT). Remember, pins default as input, so they don’t need to be explicitly declared for input. Here is a simple method to set the pin direction:

asm (
  "sbi %0, %1 \n"
    : : "I" (_SFR_IO_ADDR(DDRB)), "I" (DDB5)
);

Notice we are not referring to the PORT definition of the pin, but rather the Data Direction Register for Port B (DDRB and DDB5). However, it’s still necessary to use the SFR_IO_ADDR MACRO with the DDR.

If you know the status for all the pins on a particular port, it is far easier to set the entire port at once, rather than setting individual pins. This is easily accomplished using a bit definition which helps visualize the pins, like so:

#define PB_PIN_DIRS 0b00101000 //PIN 3 & 5 output,
//same as #define PB_PIN_DIRS (1<<DDB3) | (1<<DDB5)

asm (
  "out %0, %1 \n"
  : : "I" (_SFR_IO_ADDR(DDRB)), "r" (PB_PIN_DIRS)
);

In the event you haven’t taken notice, all of the above digital port writing examples result in the inclusion of just one assembly instruction into your program. That’s a pretty phenomenal thing if you ask me.

Do You Read Digital?

The Arduino command to read a digital pin is digitalRead(pin). Again, we can emulate the Arduino command and consume far less memory by using the SBIS instruction. SBIS stands for Skip if Bit in I/o register is Set. SBIS tests a single bit in an I/O register and skips the next instruction if the bit is set. This instruction operates on the lower 32 I/O registers – addresses 0-31. Suppose we want to read digital pin #13 (Port B bit 5), and place the result (low/high, true/false) into the variable status:

volatile uint8_t status;

asm (
  "in __tmp_reg__, __SREG__  \n"
  "cli                       \n" //disable interrupts
  "ldi %0, 1                 \n"
  "sbis %1, %2               \n" //skip next if pin high
  "clr %0                    \n"
  "out __SREG__, __tmp_reg__ \n"
  : "=r" (status) : "I" (_SFR_IO_ADDR(PINB)), "I" (PINB5)
);

As you may have guessed, the SBIS instruction has an opposite relative called SBIC. SBIC, or Skip if Bit in I/o register is Cleared tests a single bit in an I/O register and skips the next instruction if the bit is cleared. Likewise, this instruction operates on the lower 32 I/O registers – addresses 0-31.

You’re Both Write

Previous examples demonstrated separately setting a port bit and clearing a port bit. Fine, but how about demonstrating a routine that operates like the Arduino’s digitalWrite, allowing either HIGH or LOW (set/clear together)? Unfortunately, this requires a comparison and a conditional and unconditional branch, which are subjects of future tutorials. However, for completeness and without explanation, here it is (it’s really quite simple):

//digitalWrite(output)
volatile uint8_t output = HIGH; //LOW or HIGH

asm (
  "cpi %2, 0     \n"
  "breq 1f       \n"
  "sbi %0, %1    \n"
  "rjmp 2f       \n"
  "1: cbi %0, %1 \n"
  "2:            \n"
  : : "I" (_SFR_IO_ADDR(PORTB)), "I" (PORTB5), "r" (output)
);

References

Arduino ATmega168/328 Pin Mapping
Arduino Digital Pins
Arduino pinMode
Arduino digitalWrite
Arduino digitalRead
ATmega 168/328 Datasheet
AVR 8-bit Instruction Set
AVR-GCC Inline Assembler Cookbook
Extended Asm – Assembler Instructions with C Expression Operands

P.S. How Does One Turn Off PWM?

Here is an example of turning off the PWM for Arduino digital pin #11. This is as simple as disconnecting OC2A output compare function from pin #3 on Port B (digital pin #11). But, you need to follow our next tutorial about the nitty-gritty of port manipulation to understand what is happening in this example:

asm (
  "ld  r16, Z \n"
  "ldi r17, 0xff \n"
  "eor r17, %1 \n"
  "and r16, r17 \n"
  "st  Z, r16 \n"
  : : "z" (_SFR_MEM_ADDR(TCCR2A)), "d" (COM2A1) : "r16", "r17"
);

Also available as a book, with greatly expanded coverage!

BookCover
[click on the image]

About Jim Eli

µC experimenter
This entry was posted in Uncategorized and tagged , , , , , . Bookmark the permalink.

Leave a comment