This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
| en:multiasm:exercisesbook:arduinouno [2026/04/02 22:46] – [Examples] pczekalski | en:multiasm:exercisesbook:arduinouno [2026/04/02 23:03] (current) – [Examples] pczekalski | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| + | ====== Introduction to the Arduino Uno programming in Assembler ====== | ||
| + | |||
| + | The following chapter assumes that you are familiar with basic assembler operations for AVR microcontrollers. Below, we explain the most important construction elements and assembler instructions for manipulating the Arduino Uno's (figure {{ref> | ||
| + | |||
| + | <figure arduinouno> | ||
| + | {{: | ||
| + | < | ||
| + | </ | ||
| + | |||
| + | ===== GPIO and Ports ===== | ||
| + | |||
| + | The Arduino Uno exposes a number of GPIOs that can serve as binary inputs and outputs, analogue inputs, and many of them provide advanced, hardware-accelerated functions, such as UART, SPI, I2C, PWM, and ADC. In fact, not all of the pins on the development board are such " | ||
| + | |||
| + | On the programming level, GPIO ports are grouped into 3 " | ||
| + | * PortB, with GPIOs from D8 to D13, | ||
| + | * PortC, with GPIOs from port A0 to A5, | ||
| + | * PortD, with GPIOs from D0 to D7. | ||
| + | |||
| + | A bit in the port corresponds to a single GPIO pin, e.g. bit 5 (6th, zero-ordered) of PortB corresponds to GPIO D13 and is connected to the built-in LED. | ||
| + | |||
| + | <figure arduinoports> | ||
| + | {{: | ||
| + | < | ||
| + | </ | ||
| + | |||
| + | |||
| + | |||
| + | ===== IO Registers ===== | ||
| + | Each Port has assigned three 8-bit registers: | ||
| + | * DDRx (Data Direction Register): there are 3 of those registers, one per Port (B, C, D): DDRB, DDRC and DDRD. This registers configures GPIO as Input (0) or Output (1). Configuration is done "per bit", so it is equivalent to controlling each GPIO individually. | ||
| + | * PORTx (Port Data Register): there are also 3 of those registers: PORTB, PORTC and PORTD. The operation depends on the value of the specific bit in the corresponding DDR register; either pin is configured as input or output: | ||
| + | * If a specific GPIO pin (represented as a bit in the related DDRx register) is set as output, then PORTx bit directly affects the GPIO output: 1 is HIGH (+5V), while 0 is LOW (0V). | ||
| + | * If a specific GPIO pin is set to input, PORTx value controls the internal pull-up resistor: 1 enables pull-up, 0 disables it. | ||
| + | * PINx (Pin Value Register) represents the current input state of the GPIO. | ||
| + | |||
| + | ==== Instructions ==== | ||
| + | There is a set of assembler instructions that operate on Ports (I/O registers), as shown in table {{ref> | ||
| + | < | ||
| + | |||
| + | <table assemblergpioinstructions> | ||
| + | < | ||
| + | ^ Instruction | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | </ | ||
| + | |||
| + | A common scenario is to first set either the GPIO is input or output (using the correct DDRx register), then either set ('' | ||
| + | <note tip>'' | ||
| + | |||
| + | ==== Examples ==== | ||
| + | ** Template for the assembler code **\\ | ||
| + | |||
| + | Using plain assembler (not C++ + assembler) requires a specific construction of the application where the program is located (loaded) into memory exactly at 0x0000. | ||
| + | |||
| + | <code asm> | ||
| + | |||
| + | .org 0x0000 | ||
| + | rjmp start | ||
| + | |||
| + | start: | ||
| + | ... | ||
| + | </ | ||
| + | |||
| + | It is common practice to use '' | ||
| + | |||
| + | ** Core I/O registers and their IDs **\\ | ||
| + | To operate on I/O registers, the developer must either include a library with definitions or (when programming in pure assembler) declare them on their own.\\ | ||
| + | Below there is a table {{ref> | ||
| + | <table ioregisterids> | ||
| + | < | ||
| + | ^ Name ^ Address (I/O) ^ Description ^ | ||
| + | | PINB | 0x03 | Input pins register (Port B) | | ||
| + | | DDRB | 0x04 | Data direction register (Port B) | | ||
| + | | PORTB | 0x05 | Output register/ | ||
| + | | PINC | 0x06 | Input pins register (Port C) | | ||
| + | | DDRC | 0x07 | Data direction register (Port C) | | ||
| + | | PORTC | 0x08 | Output register/ | ||
| + | | PIND | 0x09 | Input pins register (Port D) | | ||
| + | | DDRD | 0x0A | Data direction register (Port D) | | ||
| + | | PORTD | 0x0B | Output register/ | ||
| + | </ | ||
| + | The easiest is to declare constants (converted to values at compile time) and insert them before the code starts (note that they do not exist in memory, so do not disturb code placement and proper execution): | ||
| + | <code asm> | ||
| + | ; I/O registers | ||
| + | .equ PINB, 0x03 | ||
| + | .equ DDRB, 0x04 | ||
| + | .equ PORTB, 0x05 | ||
| + | .equ PINC, 0x06 | ||
| + | .equ DDRC, 0x07 | ||
| + | .equ PORTC, 0x08 | ||
| + | .equ PIND, 0x09 | ||
| + | .equ DDRD, 0x0A | ||
| + | .equ PORTD, 0x0B | ||
| + | |||
| + | ; your code starts here | ||
| + | .org 0x0000 | ||
| + | rjmp start | ||
| + | |||
| + | start: | ||
| + | ... | ||
| + | </ | ||
| + | |||
| + | < | ||
| + | < | ||
| + | |||
| + | Below are sections representing common usage scenarios for GPIO management: | ||
| + | |||
| + | **USE GPIO as output**\\ | ||
| + | In this scenario, we use GPIO as an output. The simplest is to use the built-in LED to get instantly observable results.\\ | ||
| + | The built-in LED is connected to GPIO13 (D13) and is controlled via PortB (5th bit, zero-based indexing; see figure {{ref> | ||
| + | It is also convenient to declare a bit number representing the built-in LED position in PortB, so instead of using a number, we can use an identifier, such as '' | ||
| + | |||
| + | This code flashes the built-in LED. | ||
| + | <code asm> | ||
| + | .equ DDRB, 0x04 | ||
| + | .equ PORTB, 0x05 | ||
| + | .equ PB5, 5 ; PB5 is GPIO 13, and it is a built-in LED | ||
| + | .org 0x0000 | ||
| + | rjmp RESET | ||
| + | </ | ||
| + | Step 1 - configure GPIO13 (PortB, bit 5) as output, using DDRB register: | ||
| + | <code asm> | ||
| + | RESET: | ||
| + | ldi r16, 1 << PB5 ; Set bit 5 | ||
| + | out DDRB, r16 ; Set PB5 as output | ||
| + | </ | ||
| + | |||
| + | Execute in a loop on and off, setting directly PortB' | ||
| + | <code asm> | ||
| + | |||
| + | LOOP: | ||
| + | sbi PORTB, PB5 ; Turn LED off | ||
| + | rcall delay | ||
| + | cbi PORTB, PB5 ; Turn LED on | ||
| + | rcall delay | ||
| + | rjmp LOOP | ||
| + | </ | ||
| + | This implementation of the delay is based on calculating the CPU cycles used to execute the following algorithm: | ||
| + | <code asm> | ||
| + | delay: | ||
| + | ldi r20, 43 ; Outer loop | ||
| + | outer_loop: | ||
| + | ldi r18, 250 ; Mid loop | ||
| + | mid_loop: | ||
| + | ldi r19, 250 ; Inner loop | ||
| + | inner_loop: | ||
| + | dec r19 | ||
| + | brne inner_loop | ||
| + | dec r18 | ||
| + | brne mid_loop | ||
| + | dec r20 | ||
| + | brne outer_loop | ||
| + | ret | ||
| + | |||
| + | </ | ||
| + | |||
| + | Instructions used in those loops are listed in the table {{ref> | ||
| + | <table instloopticks> | ||
| + | < | ||
| + | ^ Instruction ^ Cycles ^ | ||
| + | | ldi | 1 | | ||
| + | | dec | 1 | | ||
| + | | brne | 2 (taken), 1 (not taken) | | ||
| + | | ret | 4 | | ||
| + | </ | ||
| + | Inner loop runs exactly 250 times. Thus, the exact number of cycles used is calculated as:\\ | ||
| + | * 1x1 (loop init, '' | ||
| + | * 250x1 (250 executions of '' | ||
| + | * 249x2 + 1+1 = 499 (249 executions of brne with jump + 1 when not jumping). | ||
| + | Total for this inner loop is then 750 clock cycles of the ATMEGA 328p MCU.\\ | ||
| + | |||
| + | Mid-loop runs also 250 times. Each of 250 mid-loop passes uses: | ||
| + | * 1x1 ('' | ||
| + | * 250x750 (inner loop execution cost, as counted above, because inner loop is nested inside mid-loop) | ||
| + | * 250x1 (250 executions of '' | ||
| + | * 249x2 + 1+1 = 499 (249 executions of brne with jump + 1 when not jumping) | ||
| + | |||
| + | Thus, at the level of mid-loop, the total cost of the algorithm consumes: 188250 cycles\\ | ||
| + | |||
| + | The outer loop runs 43 times. It calls mid-loop 43 times, and the exact number of cycles used is: | ||
| + | * 1x1 ('' | ||
| + | * 43x188250 (call mid-loop 43 times), | ||
| + | * 43x1 (cost of '' | ||
| + | * 42x2 + 1+1 = 85 (249 executions of brne with jump + 1 when not jumping). | ||
| + | The final cost of the loops is 8094879 cycles.\\ | ||
| + | An extra 4 cycles is for the final '' | ||
| + | |||
| + | Thus, the total cost of the '' | ||
| + | |||
| + | ATMEGA 328p runs at 16 MHz; thus, each cycle takes 1/16000000 of a second.\\ | ||
| + | Overall, the algorithm' | ||
| + | |||
| + | ** Use serial port for tracing **\\ | ||
| + | The Arduino Uno has no direct debugging capabilities, | ||
| + | |||
| + | UART uses two pins: | ||
| + | * TX (PortD, pin 1) - data from MCU to the external world, | ||
| + | * RX (PortD, pin 0) - data from the external world to the MCU. | ||
| + | |||
| + | While it is possible to implement a full serial port protocol using GPIOs alone (so-called soft-serial), | ||
| + | |||
| + | <table arduinouartports> | ||
| + | < | ||
| + | ^ Register ^ Address ^ Official Name ^ Common Name ^ Bits ^ Description ^ | ||
| + | | UDR0 | 0xC6 | USART I/O Data Register | Data register / TX-RX buffer | 7:0 | Write to transmit data, read to receive data | | ||
| + | | UCSR0A | 0xC0 | USART Control and Status Register A | Status register | RXC0, TXC0, UDRE0, FE0, DOR0, UPE0, U2X0, MPCM0 | Status flags (ready, complete, errors, speed mode) | | ||
| + | | UCSR0B | 0xC1 | USART Control and Status Register B | Control register | RXCIE0, TXCIE0, UDRIE0, RXEN0, TXEN0, UCSZ02, RXB80, TXB80 | Enable TX/RX, interrupts, 9-bit mode | | ||
| + | | UCSR0C | 0xC2 | USART Control and Status Register C | Configuration / Frame register | UMSEL01:0, UPM01:0, USBS0, UCSZ01:0, UCPOL0 | Frame format (mode, parity, stop bits, data size) | | ||
| + | | UBRR0L | 0xC4 | USART Baud Rate Register Low | Baud rate register (low) | 7:0 | Lower byte of baud rate divider | | ||
| + | | UBRR0H | 0xC5 | USART Baud Rate Register High | Baud rate register (high) | 3:0 | Upper byte of baud rate divider | | ||
| + | </ | ||
| + | In the example below, we will use TX only to send data from the MCU to the developer' | ||
| + | <code asm> | ||
| + | .equ UBRR0H, 0xC5 | ||
| + | .equ UBRR0L, 0xC4 | ||
| + | .equ UCSR0A, 0xC0 | ||
| + | .equ UCSR0B, 0xC1 | ||
| + | .equ UCSR0C, 0xC2 | ||
| + | .equ UDR0, 0xC6 | ||
| + | |||
| + | .equ TXEN0, 3 ; bit 3 controls if UART is enabled or disabled | ||
| + | .equ UDRE0, 5 ; bit 5 indicates the transmit buffer is empty | ||
| + | </ | ||
| + | Then let's define a message "Hello World" | ||
| + | <code asm> | ||
| + | .org 0x0000 | ||
| + | rjmp reset | ||
| + | message: | ||
| + | .byte ' | ||
| + | </ | ||
| + | The following section initialises the serial port for 9600bps: | ||
| + | <code asm> | ||
| + | ldi r16, hi8(103) | ||
| + | sts UBRR0H, r16 | ||
| + | ldi r16, lo8(103) | ||
| + | sts UBRR0L, r16 | ||
| + | </ | ||
| + | The 103 value is loaded into the '' | ||
| + | <figure usartprescaller> | ||
| + | {{: | ||
| + | < | ||
| + | </ | ||
| + | Where **Fcpu** is 16MHz. Note that this calculation does not exactly give 9600 bps but rather ~9615 bps. A tolerance of up to 2% is acceptable (here, 0.16%). | ||
| + | |||
| + | Next step is to enable UART: | ||
| + | <code asm> | ||
| + | ldi r16, (1 << TXEN0) | ||
| + | sts UCSR0B, r16 | ||
| + | </ | ||
| + | and configure frame format (8 bits, no parity, 1 stop bit, shortly 8N1 - the most common case): | ||
| + | <code asm> | ||
| + | ldi r16, (1 << TXEN0) | ||
| + | sts UCSR0B, r16 | ||
| + | </ | ||
| + | Now it is time to send the string to the transmitter, | ||
| + | <code asm> | ||
| + | main: | ||
| + | ldi ZH, hi8(message) | ||
| + | ldi ZL, lo8(message) | ||
| + | |||
| + | send_loop: | ||
| + | lpm r18, Z+ | ||
| + | cpi r18, 0 | ||
| + | breq main | ||
| + | </ | ||
| + | The next character can be sent only if the previous one is sent. The transmitter is ready for the next byte only when bit '' | ||
| + | <code asm> | ||
| + | wait_udre: | ||
| + | lds r19, UCSR0A | ||
| + | sbrs r19, UDRE0 | ||
| + | rjmp wait_udre | ||
| + | |||
| + | sts UDR0, r18 | ||
| + | rjmp send_loop | ||
| + | </ | ||
| + | |||
| + | |||
| + | **Use GPIO as input**\\ | ||
| + | |||
| + | **Use GPIO as input with pull-up**\\ | ||
| + | |||
| + | |||
| + | ==== Reading analogue values ==== | ||
| + | Reading of the analogue values is not so straightforward as in the case of binary ones. | ||
| + | Built-in ADC converter uses 10-bit resolution, has 6 channels (A0-A5, respectively). It also uses a reference voltage (configurable), | ||
| + | The low-level ADC register-based operations use the following formula to obtain an ADC value (figure {{ref> | ||
| + | |||
| + | <figure avreq1> | ||
| + | {{: | ||
| + | < | ||
| + | </ | ||
| + | |||
| + | Analogue reading uses a complex setup of ADC-related registers as presented in table {{ref> | ||
| + | |||
| + | <table tabadcregisters> | ||
| + | < | ||
| + | ^ Register | ||
| + | | '' | ||
| + | | | | | ||
| + | | | | | ||
| + | | | | | ||
| + | </ | ||