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/24 00:06] (current) – [Memory Map] 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> | ||
| + | {{: | ||
| + | < | ||
| + | </ | ||
| + | |||
| + | ===== 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 '' | ||
| + | |||
| + | ===== Memory Map ===== | ||
| + | Location of the code (Flash) and data (SRAM) is strictly assigned to the addressing space. The following image presents the ATMega328P memory map. When using fully manual memory control, e.g., when the source code does not use '' | ||
| + | |||
| + | <figure arduinomemorymap> | ||
| + | {{ : | ||
| + | < | ||
| + | </ | ||
| + | To summarise briefly, the code is intended to land in Flash, and variables in SRAM. Appropriate '' | ||
| + | |||
| + | The sample code below declares a 16-bit value named ' | ||
| + | * '' | ||
| + | * '' | ||
| + | |||
| + | <code asm> | ||
| + | .section .data | ||
| + | .org 0x100 ; Set SRAM start address manually | ||
| + | analogue_value: | ||
| + | .skip 2 ; 16-bit variable | ||
| + | |||
| + | .section .text | ||
| + | .org 0x0000 | ||
| + | rjmp main | ||
| + | |||
| + | main: | ||
| + | ; sample values to store | ||
| + | ldi r24, 0xFF | ||
| + | ldi r25, 0x03 | ||
| + | |||
| + | ; store it to SRAM | ||
| + | sts analogue_value, | ||
| + | sts analogue_value + 1, r25 | ||
| + | |||
| + | loop: | ||
| + | rjmp loop ; Dummy loop | ||
| + | </ | ||
| + | ===== 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 hardware level, GPIO pins 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 also connected to the built-in LED. | ||
| + | |||
| + | <figure arduinoports> | ||
| + | {{: | ||
| + | < | ||
| + | </ | ||
| + | |||
| + | <note tip>Some GPIOs have extra features (as presented on figure {{ref> | ||
| + | |||
| + | **IO Registers **\\ | ||
| + | Each Port has assigned three 8-bit registers (there are 9 in total then): | ||
| + | * 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. | ||
| + | |||
| + | |||
| + | ** 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 control. | ||
| + | ===== GPIO Control Assembler 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 for manual control of the GPIO pin is to first set either the GPIO is input or output (using the correct DDRx register), then either set ('' | ||
| + | <note tip>'' | ||
| + | |||
| + | |||
| + | |||
| + | ===== Code Examples ===== | ||
| + | Below are common scenarios implemented in assembler that will help you to understand the code and start programming. | ||
| + | |||
| + | |||
| + | |||
| + | ==== Use GPIO As Digital Output==== | ||
| + | In this scenario, we use GPIO as a digital 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' | ||
| + | |||
| + | <note tip>This kind of implementation of a '' | ||
| + | |||
| + | ==== Use GPIO as Digital Input ==== | ||
| + | |||
| + | GPIOs may be used as inputs, e.g., to check whether a button was pressed. A common scenario is that a button shorts to GND, which requires a pull-up resistor (either external or internal). For the internal pull-up, it is necessary to explicitly enable it in assembler code. | ||
| + | |||
| + | Configuring GPIO as input with pull-up is pretty simple: | ||
| + | * configure GPIO as input using the DDRx register (clear the bit that represents a particular GPIO so it becomes an input), | ||
| + | * enable pull-up: set a bit representing a particular GPIO in the PORTx register. | ||
| + | |||
| + | Reading the value of a GPIO is as simple as reading the corresponding bit in the PINx register: when the GPIO is HIGH, the bit is 1; when the GPIO is LOW, the bit is 0. When the GPIO value controls the algorithm flow, it is more convenient (and faster and more memory-efficient) to use conditional jumps based on the PINx bit value, such as the '' | ||
| + | |||
| + | The example below shows an Arduino Uno with a button connected to GPIO 8, controlling the built-in LED connected to GPIO 13. On button press, the LED turns on; on release, it turns off. Contextual circuit schematic is presented in figure {{ref> | ||
| + | <figure arduinogpioinput> | ||
| + | {{: | ||
| + | < | ||
| + | </ | ||
| + | |||
| + | Declare ports: both GPIOs are on PortB, bits 0 (GPIO 8) and 5 (GPIO 13) as presented in figure {{ref> | ||
| + | |||
| + | <code asm> | ||
| + | .equ DDRB, | ||
| + | .equ PORTB, | ||
| + | .equ PINB, | ||
| + | |||
| + | ; Bit Positions | ||
| + | .equ PB0, 0 ; Pin 8 is Port B, Bit 0 | ||
| + | .equ PB5, 5 ; Pin 13 is Port B, Bit 5 | ||
| + | |||
| + | .section .text | ||
| + | .org 0x0000 | ||
| + | rjmp main | ||
| + | </ | ||
| + | |||
| + | Configure GPIO 13 as output, GPIO 8 as input, and enable the internal pull-up resistor for GPIO 8. | ||
| + | <code asm> | ||
| + | main: | ||
| + | sbi DDRB, PB5 ; Set PB5 (GPIO 13) as Output | ||
| + | cbi DDRB, PB0 ; Set PB0 (GPIO 8) as Input | ||
| + | sbi | ||
| + | </ | ||
| + | |||
| + | This section is a simple push-switch implementation. Instead of reading PB0 (GPIO 8), we use the '' | ||
| + | <code asm> | ||
| + | loop: | ||
| + | |||
| + | sbic PINB, PB0 ; Skip next instruction if PB0 (GPIO 8) is LOW (Button Pressed) | ||
| + | rjmp led_off | ||
| + | | ||
| + | led_on: | ||
| + | sbi | ||
| + | rjmp loop ; Jump back to start of loop | ||
| + | |||
| + | led_off: | ||
| + | cbi | ||
| + | rjmp loop ; Jump back to start of loop | ||
| + | </ | ||
| + | |||
| + | ==== 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 for regular Arduino Uno (AtMega 328P). Note that this calculation yields ~9615 bps, not exactly 9600 bps. A tolerance of up to 2% is acceptable (here, it is 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+ ; Load next byte from program memory (message) into r18, then increment pointer | ||
| + | cpi r18, 0 ; Check end of string | ||
| + | breq main ; If the end of the string is reached, start sending the whole "Hello World" again | ||
| + | |||
| + | </ | ||
| + | The next character can be loaded to the sending buffer only if the previous one is already been sent. The transmitter is ready for the next byte only when bit '' | ||
| + | <code asm> | ||
| + | wait_udre: | ||
| + | lds r19, UCSR0A | ||
| + | sbrs r19, UDRE0 ; Check if buffer is ready to accept next byte | ||
| + | rjmp wait_udre | ||
| + | |||
| + | sts UDR0, r18 ; Write character from r18 to UART data register (start transmission) | ||
| + | rjmp send_loop | ||
| + | </ | ||
| + | |||
| + | |||
| + | |||
| + | ====Use of Timers to Generate PWM==== | ||
| + | Timers are handy for measuring time, waiting for a delay, or executing delayed tasks either once or periodically. The last one is very helpful for generating a PWM signal (a square wave with a controllable duty cycle) and thus controlling the amount of energy delivered to the externally connected device via the GPIO, e.g., to control an LED's brightness. It is somehow equivalent to an analogue output control. | ||
| + | |||
| + | The ATMega328P has 3 timers: one is high-precision (16-bit), and two are low-precision (8-bit). Details are presented in the table {{ref> | ||
| + | The timer counts " | ||
| + | |||
| + | <figure arduinotimerformula> | ||
| + | {{: | ||
| + | < | ||
| + | </ | ||
| + | |||
| + | Additionally, | ||
| + | |||
| + | <table arduinotimerstab> | ||
| + | < | ||
| + | ^ Timer ^ Size ^ Channels & Pins (PWM) ^ Valid prescallers | ||
| + | | Timer 0 | 8-bit | Ch A: Pin 6, Ch B: Pin 5 | 1, 8, 64, 256, 1024 | Used by Arduino for millis() and delay(). | ||
| + | | Timer 1 | 16-bit | ||
| + | | Timer 2 | 8-bit | Ch A: Pin 11, Ch B: Pin 3 | 1, 8, 32, 64, 128, 256, 1024 | Audio (tone) generation, Real-Time Clocks. | ||
| + | </ | ||
| + | |||
| + | <table arduinotimerprescalers> | ||
| + | < | ||
| + | ^ Prescaler ^ Frequency ^ Period (Tick Speed) ^ | ||
| + | | 1 | 16 MHz | 0.0625 µs | | ||
| + | | 8 | 2 MHz | 0.5 µs | | ||
| + | | 32 (Timer 2 only) | 500 kHz | 2.0 µs | | ||
| + | | 64 | 250 kHz | 4.0 µs | | ||
| + | | 128 (Timer 2 only) | 125 kHz | 8.0 µs | | ||
| + | | 256 | 62.5 kHz | 16.0 µs | | ||
| + | | 1024 | 15.625 kHz | 64.0 µs | | ||
| + | </ | ||
| + | |||
| + | The frequency is commonly represented as the number of ticks the timer counts per cycle and is referred to as the TOP value. | ||
| + | |||
| + | Each timer in the ATmega328P has 2 channels: A and B. Channels are hardwired to GPIO pins, and you cannot change their assignments. Channels share the same base frequency, but the duty cycle can be controlled separately for each channel. | ||
| + | |||
| + | <note tip> | ||
| + | '' | ||
| + | |||
| + | Each timer has a number of registers, named "The Big Five". Timer applications go far beyond generating a PWM signal and thus have complex configuration settings, but here we focus only on the PWM application and the use of Timer1. Note, however, that other timers (Timer0 and Timer2) have similar functions, composition and control, differ, e.g. in a number of registers, because in Timer1 you need to use two 8-bit registers (High part and Low part of the value) for each related setting, while in Timer0 and Timer2, you use just one 8-bit register. In the table {{ref> | ||
| + | |||
| + | <table arduinotimer1registers> | ||
| + | < | ||
| + | ^ Register Name ^ Size ^ Full Name ^ Role ^ Meaning / Purpose ^ | ||
| + | | TCCR1A / TCCR1B | 8-bit (each) | Timer/ | ||
| + | | TCNT1 (H/L) | 16-bit | Timer/ | ||
| + | | OCR1A (H/L) | 16-bit | Output Compare Register A | The Trigger | Defines the Duty Cycle (when the pin toggles). | | ||
| + | | ICR1 (H/L) | 16-bit | Input Capture Register 1 | The Ceiling | Defines the Frequency (the TOP value). | | ||
| + | | TIMSK1 / TIFR1 | 8-bit (each) | Timer Interrupt Mask & Flag Register | Notification | Handles Interrupts and status flags. | | ||
| + | </ | ||
| + | |||
| + | ** The Manager **\\ | ||
| + | Those registers control timer behaviour and functions (refer to table {{ref> | ||
| + | * Mode Selection (WGM): Decides if the timer is a simple counter or a PWM generator. | ||
| + | * Pin Behaviour (COM): Decides if the physical pin (Pin 9/10) should turn ON or OFF when the timer hits a certain number. | ||
| + | * Prescaler (CS): Sets the " | ||
| + | |||
| + | ** The Stopwatch **\\ | ||
| + | This is for reading; it represents the timer' | ||
| + | |||
| + | ** The Trigger **\\ | ||
| + | Those registers store comparator values (values to compare against a Stopwatch). Again, for Timer0 and Timer2, there is one per timer, per channel (so 2 per timer: one for channel A and one for channel B); for Timer1, there are two per channel. E.g. for Timer1, channel A, register names are '' | ||
| + | |||
| + | ** The Ceiling (TOP) **\\ | ||
| + | The TOP registers (also referred to as the Input Capture Register or Ceiling) define the maximum " | ||
| + | <note important> | ||
| + | Again, there are two registers for Timer1 ('' | ||
| + | |||
| + | ** The Notification **\\ | ||
| + | These registers are to control the timer-based interrupt notification system. We do not use interrupts for PWM; therefore, this description is omitted. | ||
| + | |||
| + | <table arduinothemanagertimer> | ||
| + | < | ||
| + | ^ Register | ||
| + | | TCCR1A | ||
| + | | ::: | 6 | COM1A0 | ||
| + | | ::: | 5 | COM1B1 | ||
| + | | ::: | 4 | COM1B0 | ||
| + | | ::: | 3 | - | 0 | Reserved: Always write to 0. | | ||
| + | | ::: | 2 | - | 0 | Reserved: Always write to 0. | | ||
| + | | ::: | 1 | WGM11 | 1 | Waveform Generation Mode bit 1: Part of Mode 14 selection. | ||
| + | | ::: | 0 | WGM10 | 0 | Waveform Generation Mode bit 0: Part of Mode 14 selection. | ||
| + | | TCCR1B | ||
| + | | ::: | 6 | ICES1 | 0 | Input Capture Edge Select: Selects trigger edge for capture (rising/ | ||
| + | | ::: | 5 | - | 0 | Reserved: Always write to 0. | | ||
| + | | ::: | 4 | WGM13 | 1 | Waveform Generation Mode bit 3: Part of Mode 14 selection. | ||
| + | | ::: | 3 | WGM12 | 1 | Waveform Generation Mode bit 2: Part of Mode 14 selection. | ||
| + | | ::: | 2 | CS12 | 0 | Clock Select bit 2: High bit of the Prescaler (gearbox). | ||
| + | | ::: | 1 | CS11 | 1 | Clock Select bit 1: Middle bit of the Prescaler. | ||
| + | | ::: | 0 | CS10 | 1 | Clock Select bit 0: Low bit of the Prescaler. | ||
| + | </ | ||
| + | Bits WGM13, WGM12, WGM11 and WGM10 are to be analysed together: they form a 4-bit value representing a mode. Mode 14 is Fast PWM, so binary representation is 1,1,1,0 (WGM13, WGM12, WGM11, WGM10 respectively).\\ | ||
| + | Bits CS define prescaler value as presented in table {{ref> | ||
| + | |||
| + | <table arduinocsprescaler> | ||
| + | < | ||
| + | ^ CS12 ^ CS11 ^ CS10 ^ Prescaler (Gear) ^ Ticks per second (at 16MHz) ^ Description ^ | ||
| + | | 0 | 0 | 0 | No Clock | 0 | Timer is stopped (Off). | | ||
| + | | 0 | 0 | 1 | clk/1 | 16,000,000 | No division. 1 tick = 1 CPU cycle. | | ||
| + | | 0 | 1 | 0 | clk/8 | 2,000,000 | Timer ticks once every 8 CPU cycles. | | ||
| + | | 0 | 1 | 1 | clk/64 | 250,000 | Our choice for 50Hz. | | ||
| + | | 1 | 0 | 0 | clk/256 | 62,500 | Used for medium-speed pulses. | | ||
| + | | 1 | 0 | 1 | clk/1024 | 15,625 | Used for very slow events or long delays. | | ||
| + | | 1 | 1 | 0 | External T1 | N/A | Timer ticks on a falling edge of Pin D5. | | ||
| + | | 1 | 1 | 1 | External T1 | N/A | Timer ticks on a rising edge of Pin D5. | | ||
| + | </ | ||
| + | |||
| + | <note important> | ||
| + | |||
| + | To refer to the registers from the assembler code level, it is necessary to use their numbers. It is, however, more convenient to use register literals. A full list of timer-related registers is presented in the table {{ref> | ||
| + | |||
| + | <table arduinotimerregistersnumbered> | ||
| + | < | ||
| + | ^ Timer ^ Register | ||
| + | | Timer 0 (8-bit) | ||
| + | | ::: | TCCR0B | ||
| + | | ::: | TCNT0 | 0x46 | Stopwatch: The actual 8-bit live count. | ||
| + | | ::: | OCR0A | 0x47 | Trigger A: Duty Cycle for Pin 6. | | ||
| + | | ::: | OCR0B | 0x48 | Trigger B: Duty Cycle for Pin 5. | | ||
| + | | ::: | TIMSK0 | ||
| + | | ::: | TIFR0 | 0x35 | Interrupt Flag: Shows if a timer event occurred. | ||
| + | | Timer 1 (16-bit) | ||
| + | | ::: | TCCR1B | ||
| + | | ::: | TCCR1C | ||
| + | | ::: | TCNT1H | ||
| + | | ::: | TCNT1L | ||
| + | | ::: | ICR1H | 0x87 | Ceiling High: Bits 8-15 of the frequency TOP. | | ||
| + | | ::: | ICR1L | 0x86 | Ceiling Low: Bits 0-7 of the frequency TOP. | | ||
| + | | ::: | OCR1AH | ||
| + | | ::: | OCR1AL | ||
| + | | ::: | OCR1BH | ||
| + | | ::: | OCR1BL | ||
| + | | ::: | TIMSK1 | ||
| + | | ::: | TIFR1 | 0x36 | Interrupt Flag: Shows Timer 1 status/ | ||
| + | | Timer 2 (8-bit) | ||
| + | | ::: | TCCR2B | ||
| + | | ::: | TCNT2 | 0xB2 | Stopwatch: The actual 8-bit live count. | ||
| + | | ::: | OCR2A | 0xB3 | Trigger A: Duty Cycle for Pin 11. | | ||
| + | | ::: | OCR2B | 0xB4 | Trigger B: Duty Cycle for Pin 3. | | ||
| + | | ::: | ASSR | 0xB6 | Asynchronous Status: Used for 32kHz watch crystals. | ||
| + | | ::: | TIMSK2 | ||
| + | | ::: | TIFR2 | 0x37 | Interrupt Flag: Shows Timer 2 status/ | ||
| + | | System | ||
| + | </ | ||
| + | |||
| + | To use timers for PWM generation, one must configure the following (in order): | ||
| + | * Decide which timer to use. | ||
| + | * Calculate all timings. | ||
| + | * Set pin for PWM as output | ||
| + | * Configure frequency (TOP registers). | ||
| + | * Configure duty cycle (Trigger registers). | ||
| + | * Set mode for the timer (Waveform = Fast PWM, Mode 14). | ||
| + | |||
| + | ** Example for the use of timers**\\ | ||
| + | |||
| + | The example below implements a standard servo PWM signal (50Hz) with a 10% duty cycle: | ||
| + | * Use Timer1: it is 16-bit -> required for high precision here. | ||
| + | * Timings: | ||
| + | * Prescaler 64 that is 16MHz/ | ||
| + | * 50Hz gives 250000/ | ||
| + | * Duty cycle 10% of 5000 is 500 (0...499)-> | ||
| + | * Use channel A of Timer1, so output pin is 9. | ||
| + | * Fast PWM mode is 14 -> WGM 14dec=1110bin. | ||
| + | |||
| + | The code contains only a minimal set of register declarations used to control Timer1 for PWM. Note that in the code below, the timer, once configured, generates a PWM signal independently of CPU work. In the final loop, the CPU is doing nothing, just the dummy loop. All logic is controlled solely by a timer, asynchronously and externally to the code. The configuration process is presented in the figure {{ref> | ||
| + | |||
| + | <figure arduinotimersconfiguratiosteps> | ||
| + | {{ : | ||
| + | < | ||
| + | </ | ||
| + | |||
| + | <code asm> | ||
| + | /* | ||
| + | * ATmega328P 50Hz PWM via Timer 1 | ||
| + | * No includes - Manual Address Mapping | ||
| + | */ | ||
| + | |||
| + | /* Register Addresses */ | ||
| + | .equ DDRB, 0x24 /* Port B Direction Register */ | ||
| + | .equ TCCR1A, | ||
| + | .equ TCCR1B, | ||
| + | .equ ICR1H, | ||
| + | .equ ICR1L, | ||
| + | .equ OCR1AH, | ||
| + | .equ OCR1AL, | ||
| + | |||
| + | .org 0x0000 | ||
| + | rjmp reset | ||
| + | |||
| + | reset: | ||
| + | </ | ||
| + | Configure PIN9 (Timer1, channel A) as output. | ||
| + | <code asm> | ||
| + | ; Configure PIN 9 as output (Timer1, channel A) | ||
| + | ldi r16, (1 << 1) | ||
| + | sts DDRB, r16 | ||
| + | </ | ||
| + | Preconfigure the TOP (register) of Timer1 to count from 0 to 4999 (0x1387), so it provides 5000 ticks per 20ms (50Hz) with a prescaler of 64. | ||
| + | <code asm> | ||
| + | ; Set frequency to 50Hz | ||
| + | ; Prescaler is 64, ICR1 (TOP) is set to 4999d=0x1387 | ||
| + | ldi r16, 0x13 ; High byte of 4999 | ||
| + | sts ICR1H, r16 | ||
| + | ldi r16, 0x87 ; Low byte of 4999 | ||
| + | sts ICR1L, r16 | ||
| + | </ | ||
| + | Preconfigure the trigger (comparator) so it flips the output on GPIO 9 when only the TOP reaches 500 (0x01F4), which is equivalent to 2ms (500 is 10% of 5000). The Timer1 instantly compares the TOP register with this trigger, and when the level of 500 is reached, it switches the output from 1 to 0. The other switch is handled automatically by Timer1 on TOP overflow. | ||
| + | <code asm> | ||
| + | ; Set triggers (comparators) to 10% of TOP | ||
| + | ; 500d=0x01F4 to OCR1A | ||
| + | ldi r16, 0x01 ; High byte of 500 | ||
| + | sts OCR1AH, r16 | ||
| + | ldi r16, 0xF4 ; Low byte of 500 | ||
| + | sts OCR1AL, r16 | ||
| + | </ | ||
| + | Configure Timer1 to work in Mode 14 (Fast PWM, cyclical square wave with controllable duty cycle via triggers/ | ||
| + | <code asm> | ||
| + | ; Set timer to operate as Fast PWM (Mode 14): | ||
| + | ; Mode 14 -> WGM = 1110b=14d | ||
| + | ; COM1A1 = 1 (Clear Pin on Match - Non-Inverting) | ||
| + | ldi r16, (1 << 7) | (1 << 1) | ||
| + | sts TCCR1A, r16 | ||
| + | </ | ||
| + | Set prescaler to 64 - it automatically starts Timer1 | ||
| + | <code asm> | ||
| + | ; Start timer with prescaler=64 | ||
| + | ; WGM13=1, WGM12=1, CS11=1, CS10=1 | ||
| + | ldi r16, (1 << 4) | (1 << 3) | (1 << 1) | (1 << 0) | ||
| + | sts TCCR1B, r16 | ||
| + | </ | ||
| + | And then do nothing: this loop is a dummy; all work is handled by Timer1. CPU is ready to handle something else. | ||
| + | <code asm> | ||
| + | loop: | ||
| + | rjmp loop ; The CPU does nothing! | ||
| + | ; The Timer1 hardware toggles the pin forever. | ||
| + | ; It is done asynchronously to the main code! | ||
| + | </ | ||
| + | When connecting an oscilloscope to GPIO pin 9, the result is as presented in figure {{ref> | ||
| + | |||
| + | <figure arduinooscilloscope1> | ||
| + | {{: | ||
| + | < | ||
| + | </ | ||
| + | |||
| + | <note important> | ||
| + | ==== Reading analogue values ==== | ||
| + | Reading from the analogue input is not as straightforward as with digital inputs. | ||
| + | Built-in ADC converter uses 10-bit resolution, has 6 channels (A0-A5, respectively). It also uses a reference voltage (configurable) as 5V (power source), internal 1.1V source or external reference voltage, connected to Aref input pin.\\ | ||
| + | Inputs are connected to the ADC through the multiplexer, | ||
| + | The low-level ADC register-based operations use the following formula to obtain an ADC value (figure {{ref> | ||
| + | |||
| + | <figure avreq1> | ||
| + | {{: | ||
| + | < | ||
| + | </ | ||
| + | |||
| + | Technically, | ||
| + | |||
| + | From the assembler developer' | ||
| + | |||
| + | <figure avreq2> | ||
| + | {{: | ||
| + | < | ||
| + | </ | ||
| + | |||
| + | <note important> | ||
| + | |||
| + | Analogue reading uses a complex setup of ADC-related registers as presented in table {{ref> | ||
| + | |||
| + | |||
| + | <table tabadcregisters> | ||
| + | < | ||
| + | ^ Register (Address) | ||
| + | | **ADMUX** (0x7C) | ||
| + | | ::: | 6 | REFS0 | Reference Selection Bit 0 (01 = AVcc) | | ||
| + | | ::: | 5 | ADLAR | Left Adjust Result (1 = Left, 0 = Right) | ||
| + | | ::: | 4 | - | Reserved | ||
| + | | ::: | 3 | MUX3 | Analog Channel Selection Bit 3 | | ||
| + | | ::: | 2 | MUX2 | Analog Channel Selection Bit 2 | | ||
| + | | ::: | 1 | MUX1 | Analog Channel Selection Bit 1 | | ||
| + | | ::: | 0 | MUX0 | Analog Channel Selection Bit 0 (0000 = A0) | | ||
| + | | **ADCSRA** (0x7A) | ||
| + | | ::: | 6 | ADSC | ADC Start Conversion (Write 1 to start) | ||
| + | | ::: | 5 | ADATE | ADC Auto Trigger Enable | ||
| + | | ::: | 4 | ADIF | ADC Interrupt Flag | | ||
| + | | ::: | 3 | ADIE | ADC Interrupt Enable | ||
| + | | ::: | 2 | ADPS2 | ADC Prescaler Select Bit 2 | | ||
| + | | ::: | 1 | ADPS1 | ADC Prescaler Select Bit 1 | | ||
| + | | ::: | 0 | ADPS0 | ADC Prescaler Select Bit 0 (111 = /128) | | ||
| + | | **ADCSRB** (0x7B) | ||
| + | | ::: | 6 | ACME | Analog Comparator Multiplexer Enable | ||
| + | | ::: | 5 | - | Reserved | ||
| + | | ::: | 4 | - | Reserved | ||
| + | | ::: | 3 | - | Reserved | ||
| + | | ::: | 2 | ADTS2 | ADC Auto Trigger Source Bit 2 | | ||
| + | | ::: | 1 | ADTS1 | ADC Auto Trigger Source Bit 1 | | ||
| + | | ::: | 0 | ADTS0 | ADC Auto Trigger Source Bit 0 | | ||
| + | | **ADCH ** (0x78) | ||
| + | | **ADCL ** (0x79) | ||
| + | | **DIDR0** (0x7E) | ||
| + | </ | ||
| + | |||
| + | An algorithm for reading an analogue value from a selected input is implemented as follows: | ||
| + | * configure reference voltage and select channel, using the ADMUX register, | ||
| + | * set prescaler for sampling frequency and enable ADC, | ||
| + | * disable GPIO being an input: this disables only the " | ||
| + | |||
| + | <note tip>Last step is optional but highly recommended: | ||
| + | |||
| + | |||
| + | |||
| + | <code asm> | ||
| + | ; --- Register Definitions (ATmega328P) --- | ||
| + | .equ ADCL, 0x78 | ||
| + | .equ ADCH, 0x79 | ||
| + | .equ ADCSRA, 0x7A | ||
| + | .equ ADCSRB, 0x7B | ||
| + | .equ ADMUX, | ||
| + | .equ DIDR0, | ||
| + | |||
| + | ; --- Bit Definitions --- | ||
| + | .equ REFS0, | ||
| + | .equ ADEN, | ||
| + | .equ ADSC, | ||
| + | .equ ADPS2, | ||
| + | .equ ADPS1, | ||
| + | .equ ADPS0, | ||
| + | |||
| + | ; --- Data Segment --- | ||
| + | .section .data | ||
| + | .org 0x0100 | ||
| + | adc_storage: | ||
| + | |||
| + | ; --- Code Segment --- | ||
| + | .section .text | ||
| + | .global main | ||
| + | |||
| + | main: | ||
| + | ; 1. Setup ADMUX | ||
| + | ; Use AVcc (5V) as reference and select Channel A0 (MUX 0000) | ||
| + | ldi r24, (1 << REFS0) | ||
| + | sts ADMUX, r24 | ||
| + | |||
| + | ; 2. Setup ADCSRA | ||
| + | ; Enable ADC and set prescaler to 128 (16MHz/128 = 125kHz) | ||
| + | ldi r24, (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0) | ||
| + | sts ADCSRA, r24 | ||
| + | |||
| + | ; 3. Disable Digital Input on A0 (Power Saving) | ||
| + | ldi r24, 0x01 | ||
| + | sts DIDR0, r24 | ||
| + | |||
| + | loop: | ||
| + | ; 4. Start Conversion | ||
| + | lds r24, ADCSRA | ||
| + | ori r24, (1 << ADSC) | ||
| + | sts ADCSRA, r24 | ||
| + | |||
| + | wait_adc: | ||
| + | ; 5. Poll the ADSC bit | ||
| + | ; When conversion is done, the hardware clears this bit to 0 | ||
| + | lds r24, ADCSRA | ||
| + | sbrc r24, ADSC | ||
| + | rjmp wait_adc | ||
| + | |||
| + | ; 6. Read Result | ||
| + | ; IMPORTANT: Read Low Byte first to lock the values | ||
| + | lds r18, ADCL ; r18 = Low Byte | ||
| + | lds r19, ADCH ; r19 = High Byte | ||
| + | |||
| + | ; 7. Store to a 16-bit variable in SRAM | ||
| + | ldi r26, lo8(adc_storage) | ||
| + | ldi r27, hi8(adc_storage) | ||
| + | st X+, r18 ; Store low byte at 0x0100 | ||
| + | st X, r19 ; Store high byte at 0x0101 | ||
| + | |||
| + | rjmp loop ; Repeat indefinitely | ||
| + | </ | ||