"Bare metal" serial communication over USB

Hi all,

I'm using an Arduino Uno as a devel platform for something that I will eventually burn directly onto a non-Arduino AVR. Currently I'm trying to make the Arduino send some data back to the Linux host via USB. I'm not using the Arduino IDE (I know about the Serial() library). I'm doing this in Assembler (because I want to), using avra and avrdude.

My code is below. It has a timer interrupt that fires the "ticker" routine at 50 Hz. Every 50 pulses it turns the Arduino built-in LED on or off, and it sends a string over the USART into my computer. I use "screen /dev/ttyACM0 9600" to monitor the outout.

Here's what I observe:

  • The LED goes on and off as it should
  • Every second the terminal receives in strings like '@y2@Hello, World: _'
  • The digit counts from 0 to 7 as it should, but it is at pos. 2 of the string, not 13

So two things seem to be going wrong. One, the additional garbage around my output string. Probably this is not my program's fault but has to do with the USART's data getting mangled through two USB interfaces on the Arduino and my host. I'm kind of OK with that.

What I don't understand, though, is why the digit doesn't show up where it should in the string. It should replace the underscore but it doesn't. Also this can't have to do with the terminal's cursor being kicked around by stray control chars because the underscore is still visibile.

Bear with me, this is my very first assembler program in 30+ years of C and Python. I always wanted to learn assembler and now I'm doing it. I know there are easier ways to go about this.

.include "m328Pdef.inc"
.list

.def r = r16                                       ; scratch register
.def s = r18                                       ; scratch register
.def r_ticks = r17                                 ; decremented in ISR
.equ MAX_TICKS = 50                                ; LED status toggle after this many
                                                   ; ticks
.equ LED_BIT = 5                                   ; Pin5 on PORT B = Arduino Pin 13
.equ COUNT_LIMIT = 1249                            ; results in 50Hz ticker freq

; A couple of helper macros to write constants into registers or I/O
.macro sts_i                                       ; "store immediate"
    ldi  r, @1
    sts  @0, r
.endmacro
.macro out_i                                       ; "out immediate"
    ldi  r, @1
    out  @0, r
.endmacro

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; MAIN PROGRAM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

.cseg

.org 0x00
    rjmp main
.org OC1Aaddr
    rjmp ticker
.org URXCaddr
    reti                                           ; USART receiving not (yet) implemented
.org UTXCaddr
    rjmp usart_isr_send

hello: .db "Hello World: _", 0x0d, 0x0a, 0         ; note the underscore @ pos 13

main:
    out_i  SPL, low(RAMEND)
    out_i  SPL, high(RAMEND)
    cli                                            ; disable Interrupts
; set timer control registers TCCR1A, TCCR1B
    clr  r
    sts  TCCR1A, r                                 ; clear TTCR1A
    sts_i  TCCR1B, (1 << WGM12) | (1 << CS12)      ; CTC mode, x256 prescaler
; set counter limit
    sts_i  OCR1AH, high(COUNT_LIMIT)               ; set high and...
    sts_i  OCR1AL, low(COUNT_LIMIT)                ; low byte of counter limite
; update interrupt mask
    lds  r, TIMSK1                                 ; load int mask
    ori  r, 1 << OCIE1A                            ; set int bit
    sts  TIMSK1, r                                 ; store int mask
; init tick counter
    ldi r_ticks, MAX_TICKS                         ; start ticks
; set LED bit in Port B to out
    in   r, DDRB
    ori  r, 1 << LED_BIT
    out  DDRB, r
    sbi  PORTB, LED_BIT
    sei                                            ; enable interrupts
    rcall usart_init                               ; set up USART

; fill USART buffer
    ldi   ZL, low(hello)
    ldi   ZH, high(hello)
    rcall usart_strcpy

loop:
    rjmp loop

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; TICKER ISR
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

.dseg
    counter: .byte 1

.cseg

ticker:
    dec   r_ticks                                  ; decrement ticks
    brne  ticker_end                               ; go to end if > 0
    ldi   r_ticks, MAX_TICKS                       ; reset ticks
    in    r, PORTB                                 ; read port
    ldi   s, 1 << LED_BIT                          ; "1" at LED bit
    eor   r, s                                     ; toggle LED bit in r
    out   PORTB, r                                 ; write LED port

    lds   r, counter
    inc   r
    sts   counter, r
    andi  r, 0x07                                  ; limit to 0..7
    subi  r, -48                                   ; '0' == 48
    sts   usart_buffer + 13, r                     ; copy ASCII code for digit to pos 13

    rcall usart_send_buffer
ticker_end:
    reti

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; USART stuff
; sends constant strings from program space, interrupt driven
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

.equ F_CPU = 16000000
.equ BAUD = 9600

.dseg
    usart_p: .byte 2                               ; pointer to next char to be sent
    usart_buffer: .byte 20

.cseg

; copy string in program memory @ Z into buffer
usart_strcpy:
    cli
    ldi  YL, low(usart_buffer)                     ; set Y to beginning of buffer
    ldi  YH, high(usart_buffer)
loop_strcpy:
    lpm  r, Z+                                     ; fetch char pointed to by Z into r, increment Z
    st   Y+, r                                     ; store into buffer @ Y++
    ori  r, 0x00                                   ; NOP to set zero flag if r == 0
    brne loop_strcpy                               ; r == 0 -> end of string
    sei
    ret

usart_init:
    .equ prescaler = F_CPU / (16 * BAUD) - 1
    cli
    ldi  r, high(prescaler)
    sts  UBRR0H, r
    ldi  r, low(prescaler)
    sts  UBRR0L, r
    ldi  r,  (1 << RXEN0) | (1 << TXEN0)           ; TX and RX enable
    sts  UCSR0B, r
; 8 data bits, no parity
    ldi  r,  (1 << UCSZ01) | (1 << UCSZ00)\
           | (0 << UPM00)  | (0 << UPM01)
    sts  UCSR0C, r
    sei
    ret

; Send out current contents of usart_buffer
usart_send_buffer:
    cli
    ldi  r, low(usart_buffer)                      ; set pointer to buffer start
    sts  usart_p, r
    ldi  r, high(usart_buffer)
    sts  usart_p + 1, r
    lds  r, UCSR0B
    ori  r, 1 << TXCIE0                            ; enable transmit complete interrupt
    sts  UCSR0B, r
    rcall usart_send                               ; send first char.
    sei
    ret

usart_send:
    cli
    lds  ZL, usart_p                               ; load pointer to current char into Z
    lds  ZH, usart_p + 1
    ld   r, Z+                                     ; fetch char pointed to by Z into r, increment Z
    ori  r, 0x00                                   ; NOP to set zero flag if r == 0
    breq finished                                  ; r == 0 -> end of string
    sts  UDR0, r                                   ; send char.
    sts  usart_p, ZL                               ; save Z into pointer
    sts  usart_p + 1, ZH
    rjmp end
finished:
    lds  r, UCSR0B
    andi r, ~(1 << TXCIE0)                         ; disable interrupt
    sts  UCSR0B, r
end:
    sei
    ret

usart_isr_send:
    rcall usart_send
    reti

Then it's not much of an Arduino project. If you can make it work in C/C++ and the Arduino framework, do you still want to write it in assembly code?

If the code will be written in assembly, I recommend to start with Microchip Studio. It is a mature development platform.

The first thing that pops out is that your ISRs are not saving any of the register or flag state that they modify.

It might be that you've carefully constructed your program not to need the state saved, but that's risky...

(also your topic title is wrong. This has nothing to do with USB (which is all handled outside of your program and hardware. It's just UART stuff.)

I don't think it is causing the problem but I see that 'usart_send' contains an 'sei' instruction and is called from an ISR. That will re-enable interrupts before the ISR returns. Bad practice.

Yeah. Since the whole program is interrupt driven and the actual program is just idling in a loop, there's no danger of accidentally trampling over registers. Originally I didn't have that in, didn't make a difference.

Yes I do because I'm learning assembler. Just for the fun of it. If I wanted to see "Hello World" on my screen I could just type it :wink:

Yes you're right about the USB thing. About registers and flags: Since an ISR cannot interrupt another ISR, and everything that happens in the program happens inside ISRs I think I'm safe. The cli/sei guards make no sense (but also no difference).

I'm sure avra is fine. There isn't much to an assembler, mature or not.

OK. I ran your code, displaying the output on an Arduino Serial monitor.
Since the serial monitor is "dumber" than the average terminal emulator, it shows:

⸮⸮⸮<⸮⸮⸮⸮⸮⸮⸮⸮⸮1⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮y⸮Hello World: _
⸮⸮⸮<⸮⸮⸮⸮⸮⸮⸮⸮⸮2⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮y⸮Hello World: _
⸮⸮⸮<⸮⸮⸮⸮⸮⸮⸮⸮⸮3⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮⸮y⸮Hello World: _

There are a couple of interesting things about this:

  1. There are considerably more "garbage" characters before the legible "Hello World" than seen in your original post.
  2. the properly incrementing number is in fact 13 positions from the beginning of each line.

This lead me to believe that somehow your copy of the flash string into RAM was starting in the wrong place. Looking at the code:

         ; fill USART buffer
C:00004f e2e9          ldi   ZL, low(hello)
C:000050 e0f0          ldi   ZH, high(hello)
C:000051 d013          rcall usart_strcpy

I had a flashback to a common problem - in Atmel parlance, code addresses are WORD addresses. But LPM wants a BYTE address. Looking at a better disassembly of the .hex file (with byte addresses, as produced by the gcc tools), I see:

  58:   57 6f           ori     r21, 0xF7       ;    The "Hello World" string....
  5a:   72 6c           ori     r23, 0xC2       ; 194
  5c:   64 3a           cpi     r22, 0xA4       ; 164
  5e:   20 5f           subi    r18, 0xF0       ; 240
  60:   0d 0a           sbc     r0, r29
  62:   00 00           nop

  64:   0f ef           ldi     r16, 0xFF       ; 255   main:
      :
  9e:   e9 e2           ldi     r30, 0x29       ; 41  Oh look!  29 != 59.  In fact, 59 == 29*2
  a0:   f0 e0           ldi     r31, 0x00       ; 0
  a2:   13 d0           rcall   .+38            ;  0xca  This is the call to usart_strcopy
  a4:   ff cf           rjmp    .-2             ;  0xa4  The infinite loop.

So the problem is that you need to convert hello to a byte address:

    ldi   ZL, low(hello*2)   ; get byte address of hello
    ldi   ZH, high(hello*2)
    rcall usart_strcpy

With that change, the program operates correctly.

Edit: BTW, I checked and Atmel's AVRASM2 treats things just the same, so this is not an AVRA "bug."

This would have shown up pretty quickly in a debugger or simulator, since it's in the initialization before interrupt events start getting really confusing.

You should still clean up the ISR code to properly save context and not do CLI/SEI inappropriately. It's just a really bad habit not to do so!

1 Like

Great, thanks!

Yup, I had just found that out myself and logged in here to post it, and your answer was already there.

Yes you're right. The cli/sei bits weren't in there originally, they are just remnants of all my attempts to try to get this running. Of course I'm aware that generally an ISR should make sure that it doesn't mess up the interrupted code's registers. However, since this program is going to be entirely interrupt-driven (everything it does it will do inside ISRs), and since interrupts are disabled during an ISR's execution, this shouldn't matter (unless I stupidly re-enable interrupts by using sei). Am I correct?

I know that if these ISRs end up in some library that I want to re-use in other code that maybe does have a main program, they will break things.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.