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 typically has 8 pins assigned to it.
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 (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:
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 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 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.
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 less bytes (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 was 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:
#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.
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) );
Arduino ATmega168/328 Pin Mapping
Arduino Digital Pins
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!