ATmega32 Bare Metal JTAG - Part 1: External Interrupt Handling and TCK

This example expands on the code from previous examples running code on the ATmega32. Note that this code is building towards interrogating other devices using JTAG, not demonstrating the internal JTAG features of the ATmega32 specifically.

I started by cleaning up the driver for the status LED on PORTB0. This is important in order to share the system safely with other code, particularly the new code to handle an external interrupt on the INT0 pin. I'm driving that pin with a 555 timer generating a ~1Hz square wave. Depending on how external interrupts (INT0, INT1, and INT2) are configured in the MCUCR, they can be set to trigger on one of four conditions: logic level low, logic level change, falling edge, or rising edge. Any typical function generator feeding a 0-Vcc logic signal into PORTD2 (INT0) should work.

External interrupt INT0 driven by clock signal from 555 timer

If a given external interrupt is enabled in the GICR, or Global Interrupt Control Register, then the appropriate condition on the corresponding pin will cause a per-interrupt (i.e., INT0, INT1, and INT2) bit to be set in the GIFR, or Global Interrupt Flags Register. On completion of the current instruction, if the I-bit in the SREG is set, enabling interrupts globally, then the processor will jump to the appropriate external interrupt vector: 0x02 for INT0, 0x04 for INT1, and 0x06 for INT2.

This means that external interrupts can continue to set flags in the GIFR, even if interrupts are globally disabled by the I-bit in the SREG, unless they are also disabled by unsetting the corresponding bit in the GICR. This enables the ATmega32 to queue up a single future interrupt per pin by setting the appropriate bit in the GIFR while an ISR (Interrupt Service Routine) is executing. However, further interrupts can only set the same flag value, resulting in that information being lost.

Upon entry to an ISR, the I-bit in the SREG is automatically disabled to prevent the nesting of interrupt handlers from overflowing the stack. (It is possible to re-enable interrupts inside an ISR, but the consequences are beyond the scope of this post.) When reti is executed, the ATmega32 will restore the PC from the stack and jmp to that location to continue execution.

In this example, the ISR jtag_bb_TCK will save SREG, disable INT0, clear the INTF0 bit in the GIFR (to ignore any interrupts that might have happened since the ISR began), pulse PORTB1, call toggle_led to change the state of the status LED on PORTB0, call delay for approximately 3 seconds, re-enable interrupts on INT0, restore SREG, and return control to the main thread. You normally want your ISRs to return as quickly as possible, but this is done for demonstration purposes.

Viewing the initial boot and signals in Saleae Logic

The project has been broken into out individual files:

$ tree
├── boot.s
├── defines.s
├── drivers
│   ├── jtag_bb.s
│   └── led.s
├── isr_table.s
├── main.s
├── utility.s
└── vendor

    2 directories, 8 files

vendor/ is a set of defines (which are, practically speaking, optional) provided by the manufacturer with their assembler, but you can find a copy on GitHub. These are solely to improve the readability of the code. You could do without them, or provide your own definitions (e.g., for different register locations in the memory map), but as the values are machine-specific, you'll wind up writing much the same code. There are a couple things missing that would be nice to have, so I put them in defines.s.

There are also more highly optimized ways to do these operations, to save cycles here and there, but the goals of the code are clarity and predictability, not raw performance or code size.

To assemble the code and flash the chip from the project root, do:

$ avra main.s && avrdude -c usbasp -p m32 -P usb -U flash:w:main.s.hex:i


;   Platform constant definitions
.include "vendor/"

;   Constant definitions
.include "defines.s"

;   SRAM segment

;   Code segment

;   Initialization code.
.include "isr_table.s"                  ;   ISR table must be located at 0x0000 in memory.
.include "boot.s"

;   General system utility functions
.include "utility.s"

;   Hardware drivers
.include "drivers/led.s"
.include "drivers/jtag_bb.s"

;   Main application
    ;   Enable PORTB1 to use as a status light for the JTAG bitbanging interface
    call jtag_bb_status_enable

    ;   Configure JTAG bitbanging interrupt on INT0
    push r24
    ldi r24, INT0_TRIGGER_RISE
    call jtag_bb_int0_trigger_on
    pop r24
    call jtag_bb_int0_enable

    ;   Enable interrupts

    ;   Infinite sleep loop to let interrupts drive program
        rjmp forever


.equ CODE_START = 0x00

.equ INT0_TRIGGER_LOW       = 0
.equ INT0_TRIGGER_FALL      = 2
.equ INT0_TRIGGER_RISE      = 3

    push r16
    in r16, SREG
    push r16

    pop r16
    out SREG, r16
    pop r16


;   Interrupt Handlers
jmp boot            ;  RESET    = External Reset Vector  
jmp jtag_bb_TCK     ;  INT0     = External Interrupt Request 0
reti                ;  INT1     = External Interrupt Request 1
reti                ;  INT2     = External Interrupt Request 2
reti                ;  OC2      = Timer/Counter2 Compare Match
reti                ;  OVF2     = Timer/Counter2 Overflow
reti                ;  ICP1     = Timer/Counter1 Capture Event
reti                ;  OC1A     = Timer/Counter1 Compare Match A
reti                ;  OC1B     = Timer/Counter1 Compare Match B
reti                ;  OVF1     = Timer/Counter1 Overflow
reti                ;  OC0      = Timer/Counter0 Compare Match
reti                ;  OVF0     = Timer/Counter0 Overflow
reti                ;  SPI      = Serial Transfer Complete
reti                ;  URXC     = USART, Rx Complete
reti                ;  UDRE     = USART Data Register Empty
reti                ;  UTXC     = USART, Tx Complete
reti                ;  ADCC     = ADC Conversion Complete
reti                ;  ERDY     = EEPROM Ready
reti                ;  ACI      = Analog Comparator
reti                ;  TWI      = 2-wire Serial Interface
reti                ;  SPMR     = Store Program Memory Ready


;   On RESET
    enable_stack:                   ;   Setup stack so we can use call
        ldi r16, high(RAMEND)
        out SPH, r16
        ldi r16, low(RAMEND)
        out SPL, r16
    call enable_led                 ;   Enable the PORTA0 status LED (reserves r26)
    call flash_led                  ;   Flash the PORTA0 status LED to signal handover to user code
    jmp start


;   Delay is for r24 * r25 * 250 microseconds
;   r24 and r25 must be > 0 
    push r16
    push r17
    push r24
    push r25

    mov r17, r24
        ldi r16, 0x3E
            dec r16
            brne PC - 1
        dec r17
        brne PC - 4
    dec r25
    brne PC - 7

    pop r25
    pop r24
    pop r17
    pop r16


;  LED driver for PORTB, PINB0
    sbi DDRB, PINB0

    sbic PORTB, PINB0
    rjmp led_off
    rjmp led_on

    sbi PORTB, PINB0

    cbi PORTB, PINB0

;   Library functions
    push r16
    push r24
    push r25
    ldi r16, 0x06
        ldi r24, 0x7D
        ldi r25, 0x10
        call delay
        call toggle_led
        dec r16
        brne PC - 5
    pop r25
    pop r24
    pop r16


;   Enable PINB1 and PINB2
    push r16
    in r16, DDRB
    ori r16, (1 << PINB1) | (1 << PINB2)
    out DDRB, r16
    pop r16

;   Set the triggering mode for INT0. Pass mode in r24:
    push r16
    in r16, MCUCR
    or r16, r24 
    out MCUCR, r16
    pop r16

;   Enable external interrupt INT0 to listen for a JTAG TCK signal
    push r16
    in r16, GICR
    ori r16, (1 << INT0) 
    out GICR, r16
    pop r16

;   Disable external interrupt INT0
    push r16                        
    in r16, GICR
    andi r16, ~(1 << INT0) 
    out GICR, r16
    pop r16

;   Clear the INTF0 interrupt flag in the GIFR
    push r16                        
    in r16, GIFR
    andi r16, ~(1 << INTF0)
    out GIFR, r16
    pop r16

;   Pass pin mask to pulse on PORTB in r24
    push r16

    in r16, PORTB                   ;   Get current state
    push r16                        ;   Save for later
    or r16, r24                     ;   Enable bitmask and write to PORTB
    out PORTB, r16                  
    pop r16                         ;   Restore state and write to PORTB
    out PORTB, r16

    pop r16

;   ISR to handle JTAG clocking
    INTERRUPT_START                 ;   Pushes r16, then pushes SREG to the stack
    call jtag_bb_int0_disable       ;   Disable INT0 so GIFR won't be set (even though GIE I-bit is cleared during interrupt handling).
    call jtag_bb_int0_clear         ;   Clear the INTF0 interrupt flag in the GIFR

    push r24                        ;   Pulse PORTB, PINB1
    ldi r24, (1 << PINB1)
    call jtag_bb_pulse
    pop r24

    call toggle_led                 ;   Toggle the LED so we have a visual cue

    push r24                        ;   Delay inside the ISR to skip over a small number of potential interrupts
    push r25                        ;   This is only for demonstration purposes. Normally, you want your ISR to
        ldi r24, 0xFF               ;   process as fast as possible and return control to the main thread.
        ldi r25, 0xFF
        call delay
    pop r25
    pop r24

    call jtag_bb_int0_enable        ;   Re-enable INT0 so we don't miss future events
    INTERRUPT_END                   ;   Pop and restore SREG, pop r16, and return