Table of Contents

SUT AVR Assembler Laboratory Node Hardware Reference

Introduction

Each laboratory node is equipped with an Arduino Uno R3 development board, based on the ATmega328P MCU. It also has two extension boards:

There are 10 laboratory nodes. They can be used independently, but for collaboration, nodes are interconnected symmetrically, with GPIOs described in the hardware reference section below.

Hardware reference

The table 1 lists all hardware components and details. Note that some elements are accessible, but their use is not supported via the remote lab, e.g., buttons and a buzzer.
The node is depicted in the figure 1 and its interface visual schematic is presented in the figure 2. The schematic presents only components used in scenarios and accessible via the VREL NextGen environment (controllable and observable via video stream), omitting unused components such as buttons, a buzzer, and a potentiometer.

Figure 1: AVR (Arduino Uno) SUT Node
Figure 2: SUT node's visual interface components schematic
Table 1: AVR (Arduino Uno) SUT Node Hardware Details
Component ID Component Hardware Details (controller) Control method GPIOs (as mapped to the Arduno Uno) Remarks
D1 LED (red) direct via GPIO binary (0→on, 1→off) GPIO13
D2 LED (red) direct via GPIO binary (0→on, 1→off) GPIO12
D3 LED (red) direct via GPIO binary (0→on, 1→off) GPIO11
D4 LED (red) direct via GPIO binary (0→on, 1→off) GPIO10 shared with interconnection with another module
LED4 4x 7-segment display(+DP) indirect, via two 74HC575 registers serial load to 2 registers, daisy-chained GPIO8 - serial input of the controller (SER_PORT)
GPIO7 - shift data internally (CLK_PIN), raising edge (write next bit and shift data in serial)
GPIO4 - store data to internal buffer, in 74H575, stores only one digit(LAT_PIN)

Handling of the buffered 4-digit, 7-segment display

To display a digit in the 4x7seg. display, there are two definitions needed: the shape of a digit (or other symbol), and its position (1,2,3,4: a binary mask).

The 7-segment display is a common-anode (you use zero to turn the segment on), and thus 0..9 digit definitions are declared below:

; Common Anode 7-segment masks (Active LOW)
; Segments:     DP,g,f,e,d,c,b,a (Bit 7 -> Bit 0)
; Indices:       0     1     2     3     4     5     6     7     8     9
segment_masks:
    .byte      0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90

In a common-anode configuration, the active signal to turn on a segment is LOW (0), and to turn it off, it is HIGH (1). The state of a single digit is represented by an 8-bit mask: 7 segments to build the symbol and a DP (decimal point). For example, a digit 7 is represented by bits corresponding to segments “a”, “b”, and “c” set to 0 (to turn segments “a”, “b”, and “c” on) and the remaining bits set to 1 (to turn them off), so the corresponding binary value looks as follows: 11111000b, hence the hexadecimal value is 0xF8 (as in the code above). The MSB bit represents DP, and the LSB segment “a”. This definition affects how one loads data into the shift register: starting from MSB towards LSB, because of the way the register is built and connected to the segments - refer to the function display_digit below.

Naturally, it is possible to expand those definitions to display other symbols, e.g., hexadecimal digits such as A,b,C,d,E,F.

The way the display works is similar to a typical matrix dot display: instead of having to control 32 independent digital lines to control each LED composing the display independently (8 per digit, 4 digits), we use a digit selector (lines 0,1,2,3) and common symbol lines (lines DP,g,f,e,d,c,b,a).
This way, the display “flashes” because, to display more than one digit, you need to iterate over the lines instantly and set the appropriate symbol definitions. However, the human eye is slow enough not to notice it, and thus we see all 4 digits in parallel, not being displayed one by one that in fact is a real scenario.
The schematic in Figure 1 shows an idea of how to control a single digit over a serial port pin (SER_PIN): you need to inject bit by bit, starting from the least significant bit of the symbol representing a digit, then 8 bits of the digit number - selected by lines 1,2,3,4, so only 0001b, 0010b, 0100b and 1000b combinations are used. A 0→1→0 pulse on the clock (SER_CLK) writes the data to the left registers and shifts the contents right (including passing from the left register to the right one). This way, after 16 cycles (8+8), the left register holds the line that selects the digit (1,2,3,4), and the right register holds the combination representing the symbol at this position.
When binary combinations in both registers (line and symbol) are ready to be represented, a LAT_PORT 0→1→0 pulse rewrites register counters to the internal buffer, and it instantly causes displays to light according to the symbol definition loaded into the right register (only current digit, others are off at this time).

Display single digit: function definition
To handle display, a sample function that displays a digit in a selected position is presented below. Note that it does not check parameters and thus assumes that the digit position is a number between 0 and 3, and that a digit to display is 0..9. Going beyond these limits causes unpredictable behaviour and usually an MCU program crash.

; Pin definitions using direct I/O addresses for ATmega328P
.equ SER_PORT, 0x05  ; PORTB I/O address
.equ SER_PIN,  0
.equ CLK_PORT, 0x0B  ; PORTD I/O address
.equ CLK_PIN,  7
.equ LAT_PORT, 0x0B  ; PORTD I/O address
.equ LAT_PIN,  4
 
.global display_digit
 
; void display_digit(uint8_t pos, uint8_t number);
; r24 = position (0 to 3)
; r22 = number (0 to 9)
display_digit:
    push r16
    push r17
    push r18
    push zl
    push zh
 
    ; 1. Load Segment Mask (for U3) from flash
    ldi zl, lo8(segment_masks)
    ldi zh, hi8(segment_masks)
    add zl, r22               ; Add number index to Z pointer
    adc zh, r1                ; r1 is assumed to be 0 (gcc standard)
    lpm r16, Z                ; r16 now holds segment data
 
    ; 2. Load Digit Select Mask (for U2) from flash
    ldi zl, lo8(digit_masks)
    ldi zh, hi8(digit_masks)
    add zl, r24               ; Add position index to Z pointer
    adc zh, r1
    lpm r17, Z                ; r17 now holds digit select data
 
    ; 3. Shift out Segment Data (r16) -> Ends up in U3
    ldi r18, 8                ; Loop counter for 8 bits
shift_segments:
    lsl r16                   ; Shift MSB into Carry flag
    brcs set_ser_seg          ; If Carry is 1, branch to set SER high
    cbi SER_PORT, SER_PIN     ; Clear SER low
    rjmp clock_seg
set_ser_seg:
    sbi SER_PORT, SER_PIN     ; Set SER high
clock_seg:
    ; Pulse SRCLK
    sbi CLK_PORT, CLK_PIN
    cbi CLK_PORT, CLK_PIN
    dec r18
    brne shift_segments
 
    ; 4. Shift out Digit Select Data (r17) -> Ends up in U2
    ldi r18, 8                ; Loop counter for 8 bits
shift_digits:
    lsl r17                   ; Shift MSB into Carry flag
    brcs set_ser_dig
    cbi SER_PORT, SER_PIN
    rjmp clock_dig
set_ser_dig:
    sbi SER_PORT, SER_PIN
clock_dig:
    ; Pulse SRCLK
    sbi CLK_PORT, CLK_PIN
    cbi CLK_PORT, CLK_PIN
    dec r18
    brne shift_digits
 
    ; 5. Pulse Latch (RCLK) to update the output displays
    sbi LAT_PORT, LAT_PIN
    cbi LAT_PORT, LAT_PIN
 
    pop zh
    pop zl
    pop r18
    pop r17
    pop r16
    ret
 
; ---------------------------------------------------------
; Data stored in Program Memory (Flash)
; ---------------------------------------------------------
.section .progmem.data, "a", @progbits
 
; Common Anode 7-segment masks (Active LOW)
; Segments:     DP,g,f,e,d,c,b,a (Bit 7 -> Bit 0)
; Indices:       0     1     2     3     4     5     6     7     8     9
segment_masks:
    .byte      0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90
 
; Digit select masks (Assuming active high on QA-QD for digits 1-4)
digit_masks:
    .byte 0x01, 0x02, 0x04, 0x08
Note: registers in this schema store data for only ONE digit. Iterating over digits and displaying them allows it to represent a full, multi-digit number. To display, e.g., 1023, it is necessary to handle each digit separately: “1”, “0”, “2”, and “3”, and to repeat this process continuously. If you stop, only the last digit will be visible.
Changing the definitions of the symbols stored in segment_masks enables you to easily present characters other than numbers. Think about segment_masks as a font definition that defines how a symbol looks.

Display single digit: how to use it to display a number?
Sample code that uses the function declared above and displays 1975 is presented below. Note, the MCU runs here at full speed, constantly updating the display. While it is not necessary to (a minimum, comfortable LED display refresh rate should be around 10Hz), we do not present such a solution here for the sake of simplicity. It is common to address timers for this job to periodically refresh the screen.

.equ SREG,     0x3F     ; Status Register
.equ SPH,      0x3E     ; Stack Pointer High
.equ SPL,      0x3D     ; Stack Pointer Low
.equ SER_PORT, 0x05     ; PORTB I/O address
.equ PINB,     0x03     ; Input Pins Port B (Toggle Shortcut)
.equ SER_PIN,  0        ; GPIO8
.equ DDRD,     0x0A     ; Data Direction Port D
.equ DDRB,     0x04     ; Data Direction Port B
.equ CLK_PORT, 0x0B     ; PORTD I/O address
.equ CLK_PIN,  7        ; GPIO7
.equ LAT_PORT, 0x0B     ; PORTD I/O address
.equ LAT_PIN,  4        ; GPIO4
 
.equ RAMEND,   0x08FF
.global display_digit
; ---------------------------------------------------------
; Data stored in Program Memory (Flash)
; ---------------------------------------------------------
.section .text
.org 0x0000
    rjmp RESET          
 
 
RESET:
 
    ; Prepare stack
    ldi r16, hi8(RAMEND)
    out SPH, r16
    ldi r16, lo8(RAMEND)
    out SPL, r16
    ; Initialise display control outputs
    sbi DDRB, SER_PIN  ; Set PB0 as output
    sbi DDRD, CLK_PIN  ; Set PD7 as output
    sbi DDRD, LAT_PIN  ; Set PD4 as output
 
    clr r25
    clr r23
    ; --- Main Loop, displays in sequence 1->9->7->5 ---
LOOP:
    ldi r24,0
    ldi r22,1
    call display_digit ; Display 1
    ldi r24,1
    ldi r22,9
    call display_digit ; Display 9
    ldi r24,2
    ldi r22,7
    call display_digit ; Display 7
    ldi r24,3
    ldi r22,5
    call display_digit ; Display 5
    rjmp LOOP
 
; void display_digit(uint8_t pos, uint8_t number);
; r24 = position (0 to 3)
; r22 = number (0 to 9)
 
.... here comes the body of the display_digit function

In the function above, we used fixed (constant) digits to display. A common scenario, however, is when the number is stored in some register or in a memory variable.

Convert number to digits: function definition
To display a number on this kind of display, you need to convert it into an array of bytes, each representing a digit. A function below does the trick.

; void convert_to_digits(uint16_t value, uint8_t* array);
; Inputs:
; r25:r24 = Value to convert (up to 9999)
; r23:r22 = Pointer to SRAM array (4 bytes long)
convert_to_digits:
    ; Save registers we are about to use
    push r26
    push r27
    push r18
    push r19
    push r20
 
    ; Move the SRAM pointer from r23:r22 into the X pointer (r27:r26)
    movw r26, r22
 
    ; ---------------------------------------------------
    ; 1. Thousands Digit (Subtract 1000 = 0x03E8)
    ; ---------------------------------------------------
    clr r18             ; Clear digit counter
    ldi r19, 0x03       ; High byte of 1000
    ldi r20, 0xE8       ; Low byte of 1000
loop_1000:
    cp r24, r20         ; Compare value low byte with 1000 low byte
    cpc r25, r19        ; Compare value high byte with 1000 high byte
    brlo done_1000      ; If value < 1000, branch out
    sub r24, r20        ; Subtract 1000 low byte
    sbc r25, r19        ; Subtract 1000 high byte (with carry)
    inc r18             ; Increment thousands digit
    rjmp loop_1000
done_1000:
    st X+, r18          ; Store thousands digit in array[0] and increment X
 
    ; ---------------------------------------------------
    ; 2. Hundreds Digit (Subtract 100 = 0x0064)
    ; ---------------------------------------------------
    clr r18             ; Reset digit counter
    ldi r19, 0x00       ; High byte of 100
    ldi r20, 0x64       ; Low byte of 100
loop_100:
    cp r24, r20
    cpc r25, r19
    brlo done_100
    sub r24, r20
    sbc r25, r19
    inc r18
    rjmp loop_100
done_100:
    st X+, r18          ; Store hundreds digit in array[1] and increment X
 
    ; ---------------------------------------------------
    ; 3. Tens Digit (Subtract 10 = 0x000A)
    ; ---------------------------------------------------
    clr r18             ; Reset digit counter
    ldi r19, 0x00       ; High byte of 10
    ldi r20, 0x0A       ; Low byte of 10
loop_10:
    cp r24, r20
    cpc r25, r19
    brlo done_10
    sub r24, r20
    sbc r25, r19
    inc r18
    rjmp loop_10
done_10:
    st X+, r18          ; Store tens digit in array[2] and increment X
 
    ; ---------------------------------------------------
    ; 4. Ones Digit (The Remainder)
    ; ---------------------------------------------------
    ; Whatever is left in r24 is the ones digit (0-9)
    st X, r24           ; Store ones digit in array[3] (no need to increment X)
 
    ; Restore registers and return
    pop r20
    pop r19
    pop r18
    pop r27
    pop r26
    ret

Note, this function operates on a buffer located in the memory, which can be declared, e.g. as follows:

.section .bss  ; .bss is for uninitialized variables in SRAM
; Reserve 4 bytes in SRAM to hold the 4 converted digits
display_array:
    .space 4

Communication

Devices (laboratory nodes) are interconnected in pairs, so it is possible to work in groups and implement scenarios involving more than one device:

Interconnections are symmetrical, so that device 1 can send data to device 2 and vice versa (similar to serial communication). Note that analogue inputs are also involved in the interconnection interface. See image 3 for details.

Figure 3: SUT AVR nodes interconnection diagram

The in-series resistors protect the Arduino boards' outputs from excessive current when both pins are configured as outputs with opposite logic states.

The capacitors on the analogue lines filter the PWM signal, providing a stable voltage for the analogue-to-digital converter to measure.

Table 2: AVR (Arduino Uno) SUT Node Interconnections
Arduino Uno pin name AVR pin name Alternate function Comment
D2 PD2 INT0 Interrupt input
D5 PD5 T1 Timer/counter input
D6 PD6 OC0A PWM output to generate analogue voltage
D9 PB1 OC1A Digital output / Timer output
D10 PB2 OC1B Digital output / Timer output
A5 PC5 ADC5 Analogue input

Such a connection makes it possible to implement a variety of scenarios:

Nodes are interconnected in pairs: 1-2, 3-4, 5-6, 7-8, 9-10. Scenarios for data transmission between MCUs require booking and the use of correct nodes for sending and receiving messages.

Visualising Instruction Execution Time Using an Oscilloscope

Let's try to visualise how code operates the GPIO. Naturally, in the remote lab, it is not possible to do it remotely, so here we present some desk-based experiments.
The LAT_PIN is GPIO4, and an oscilloscope is connected to it. In the function that displays a single digit, there is a section that loads a binary mask into the internal registers, enabling the LED segments that constitute the digit to be turned on and off. It is:

...
    sbi LAT_PORT, LAT_PIN
    cbi LAT_PORT, LAT_PIN
...

The figures 4 and 5 present the LAT_PIN signal, called periodically during the display of the consecutive digits (they represent the same signal but differ by the oscilloscope time base for better observation).
SBI causes the signal to rise, while CBI to fall. Thus, the HIGH time is the exact time during which the CBI instruction executes. It takes about 120-130ns.
The Arduino Uno operates at 16 MHz, so each cycle is 1/16000000 s, which is about 63 ns. According to the documentation, CBI takes 2 cycles, which is ~126ns.

Figure 4: LAT_PIN signal (50ns time base)
Figure 5: LAT_PIN signal (25ns time base)