Skip to main content

My journey with ATtiny4313 (part 2)

Part 2: Programming in assembler

Why?

I need to get rid of that library and gain control of the entire CPU. So my idea is to rewrite the all code ... in assembly (or assembler).
A quick note here: I did program in assembly over 30 years ago, at University (8085, 68000) as well as for personnal projects (6502, 6809, 8086, a little bit of Z80). But since then, I turned to other languages, mostly to C/C++. So my assembly is a little bit rusty.

Pros and cons

  • Assembly is simple: programming in assembly is like playing with Lego (which I did a lot!). It consists of assembling small block (operational codes, or opcodes) together. This is why it's called ... assembly! Each opcode does a very limited operation, well described in the datasheet.
  • Nothing is hidden: no library, no help, you'll have to write everything from scratch. Not totally true, since we can find some headers files online, which I used.
  • The datasheet is the reference: it contains everything, even some examples in assembly.
  • The lack of documentation problem: mainly because it is used by a minority, and several toolsets exist, you cannot count on finding a quick answer when an problem arises. Actually, you are mostly on your own.
    Also, most of the doc online is outdated. However, the ATTiny4313/2313 datasheet is the reference source of information, as well as the Avr Instruction Set manual. Finally, I found some valuable information in avr-libc documentation.
  • Syntax is not consistent: since there are several assembler (the compilator), each one has its own implementation, hence slight differences on the syntax. I use avr-gcc.
  • Assembly is not portable: true, but in my case, not an issue. Also, the opcodes are coherent between all avr-based chips.

Some notes

  • Contrary to all CPU I used in the past, ATTiny is based on Harvard architecture where code and data are separated.
  • The interrupt vectors are located between 0x000 and 0x0014. If a vector is not used, the memory can be re-used for code.

The tools

I have installed:

  • avr-gcc (documentation)
    $ avr-gcc --version
    avr-gcc (GCC) 5.4.0
  • avr-as AKA gas (GNU Assembler)
    $ avr-as --version
    GNU assembler (GNU Binutils) 2.26.20160125
  • simulavr
    $ simulavr --version
    SimulAVR 1.2dev
Finding the device. You can set USBDEVICE manually, but the Makefile will do it automatically anyway.

$ dmesg | grep tty
[320448.786051] cdc_acm 5-2:1.0: ttyACM0: USB ACM device
$ export USBDEVICE=/dev/ttyACM0

The Makefile
.PHONY: \
	vars \
	help \
	clean \
	compile \
	dump \
	upload \
	simul \
	fuses \
	check-fuses

# Hack to get the directory this makefile is in:
MKFILE_PATH	:= $(lastword $(MAKEFILE_LIST))
MKFILE_DIR	:= $(notdir $(patsubst %/,%,$(dir $(MKFILE_PATH))))
MKFILE_ABSDIR	:= $(abspath $(MKFILE_DIR))
AVR_PATH	:= /snap/arduino/85/hardware/tools/avr
AVR_BIN_PATH	:= $(AVR_PATH)/bin

# Hack to get all *.h files into compile dependencies:
HEADERS		= $(shell find $(MKFILE_DIR) -name "*.h")

BUILDTMP	?= $(MKFILE_DIR)/build-tmp
OPTIMIZATION	?= -Os
AVRAS		?= $(AVR_BIN_PATH)/avr-as -Wall -al -v --listing-rhs-width=80 -mmcu=$(DEVICE) -I$(MKFILE_DIR)
AVRGCC		?= $(AVR_BIN_PATH)/avr-gcc -Wall -nostartfiles -mmcu=$(DEVICE) -I$(MKFILE_DIR)
AVRCC		?= $(AVRGCC)
AVRDUDE		?= $(AVR_BIN_PATH)/avrdude
LD		?= ld
#---------------------------------------------------------
# AVRDUDE_FLASHARG:
# This preserves the chip memory when updating the fuses.
# To erase the chip when setting fuses, do:
#
#     make AVRDUDE_FLASHARG=-e fuses
#
AVRDUDE_FLASHARG ?= -D
#---------------------------------------------------------
AVR_SIZE	?= $(AVR_BIN_PATH)/avr-size
AVR_OBJCOPY	?= $(AVR_BIN_PATH)/avr-objcopy
AVR_OBJDUMP	?= $(AVR_BIN_PATH)/avr-objdump
DEVICE		:= attiny4313
ARCHITECTURE	:= "avr:25"
CLOCK		:= 8000000L
PROGRAMMER	:= stk500v1
BAUD		:= 19200
SRC		:= blink.S
OBJ		:= $(BUILDTMP)/$(SRC:S=o)
ELF		:= $(BUILDTMP)/$(SRC:S=elf)
HEX		:= $(BUILDTMP)/$(SRC:S=hex)
EEP		:= $(BUILDTMP)/$(SRC:S=eep)
FUSE_EXT	:= 0xff
FUSE_HIGH	:= 0x9f
FUSE_LOW	:= 0xcf
AVRDUDE_CONF    := $(AVR_PATH)/etc/avrdude.conf
AVRDUDE_OPTS    := -C $(AVRDUDE_CONF) -p$(DEVICE) -c$(PROGRAMMER) -P$(USBDEVICE) -b$(BAUD)
USBDEVICE	:= $(shell dmesg | awk '/tty/ {gsub(/:/,"",$$4);A=$$4} END{print "/dev/"A}')

# Misc target info:
help_spacing  := 12

.DEFAULT_GOAL := compile

#---------------------------------------------------------
# Ensure temp directories.
#
# In order to ensure temp dirs exit, we include a file
# that doesn't exist, with a target declared as PHONY
# (above), and then have the target create our tmp dirs.
#---------------------------------------
-include ensure-tmp
ensure-tmp:
	@mkdir -p $(BUILDTMP)

vars: ## Print relevant environment vars
	@printf  "%-20.20s%s\n"  "MKFILE_PATH:" "$(MKFILE_PATH)"
	@printf  "%-20.20s%s\n"  "MKFILE_DIR:" "$(MKFILE_DIR)"
	@printf  "%-20.20s%s\n"  "MKFILE_ABSDIR:" "$(MKFILE_ABSDIR)"
	@printf  "%-20.20s%s\n"  "BUILDTMP:" "$(BUILDTMP)"
	@printf  "%-20.20s%s\n"  "OPTIMIZATION:" "$(OPTIMIZATION)"
	@printf  "%-20.20s%s\n"  "AVRAS:" "$(AVRAS)"
	@printf  "%-20.20s%s\n"  "AVRGCC:" "$(AVRGCC)"
	@printf  "%-20.20s%s\n"  "AVRCC:" "$(AVRCC)"
	@printf  "%-20.20s%s\n"  "AVRDUDE:" "$(AVRDUDE)"
	@printf  "%-20.20s%s\n"  "AVRDUDE_OPTS:" "$(AVRDUDE_OPTS)"
	@printf  "%-20.20s%s\n"  "AVR_SIZE:" "$(AVR_SIZE)"
	@printf  "%-20.20s%s\n"  "AVR_OBJCOPY:" "$(AVR_OBJCOPY)"
	@printf  "%-20.20s%s\n"  "AVR_OBJDUMP:" "$(AVR_OBJDUMP)"
	@printf  "%-20.20s%s\n"  "DEVICE:" "$(DEVICE)"
	@printf  "%-20.20s%s\n"  "CLOCK:" "$(CLOCK)"
	@printf  "%-20.20s%s\n"  "PROGRAMMER:" "$(PROGRAMMER)"
	@printf  "%-20.20s%s\n"  "USBDEVICE:" "$(USBDEVICE)"
	@printf  "%-20.20s%s\n"  "BAUD:" "$(BAUD)"
	@printf  "%-20.20s%s\n"  "SRC:" "$(SRC)"
	@printf  "%-20.20s%s\n"  "ELF:" "$(ELF)"
	@printf  "%-20.20s%s\n"  "EEP:" "$(EEP)"
	@printf  "%-20.20s%s\n"  "HEX:" "$(HEX)"

help: ## Print this makefile help menu
	@echo "TARGETS:"
	@grep '^[a-z_\-]\{1,\}:.*##' $(MAKEFILE_LIST) \
		| sed 's/^\([a-z_\-]\{1,\}\): *\(.*[^ ]\) *## *\(.*\)/\1:\t\3 (\2)/g' \
		| sed 's/^\([a-z_\-]\{1,\}\): *## *\(.*\)/\1:\t\2/g' \
		| awk '{$$1 = sprintf("%-$(help_spacing)s", $$1)} 1' \
		| sed 's/^/  /'
	@printf "\nUsage:\n make \\\n %s \\\n %s \\\n %s \\\n %s\n" \
		"USBDEVICE=/dev/cu.usbserial-1234" \
		"SRC=my_source.c" \
		"DEVICE=%lt;mcu>" \
		"%lt;make target>"

vpath %.o $(BUILDTMP)
vpath %.eep $(BUILDTMP)
vpath %.elf $(BUILDTMP)
vpath %.hex $(BUILDTMP)

$(OBJ): $(SRC)
	$(AVRAS) -mmcu=$(DEVICE) -o $@ $%lt;
$(ELF): $(OBJ)
	$(AVRGCC) -mmcu=$(DEVICE) -L$(BUILDTMP) $(LDFLAGS) -o $@ $%lt;
$(HEX): $(ELF)
	$(AVR_OBJCOPY) -O ihex -R .eeprom --preserve-dates $%lt; $@
	$(AVR_OBJDUMP) --architecture=$(ARCHITECTURE) -D $@
$(EEP): $(ELF)
	$(AVR_OBJCOPY) -O ihex -j .eeprom --set-section-flags=.eeprom=alloc,load --no-change-warnings --change-section-lma=.eeprom=0 --preserve-dates $%lt; $@

clean: ## Clean build artifacts
	rm -rf $(BUILDTMP)/*
	rm -vf *.s

compile: $(EEP) $(HEX)
	$(AVR_SIZE) -A $(ELF)

link: compile ## Link compilation artifacts and package for upload

upload: $(HEX)	 ## Upload (NOTE: USBDEVICE must be set)
ifndef USBDEVICE
	$(error 'USBDEVICE not defined! Please set USBDEVICE env var!')
endif # USBDEVICE
	$(AVRDUDE) -v $(AVRDUDE_OPTS) -Uflash:w:$(HEX):i

fuses: ## Flash the fuses
ifndef USBDEVICE
	$(error 'USBDEVICE not defined! Please set USBDEVICE env var!')
endif # USBDEVICE
	$(AVRDUDE) -v $(AVRDUDE_OPTS) -D \
	    -Uefuse:w:$(FUSE_EXT):m \
	    -Uhfuse:w:$(FUSE_HIGH):m \
	    -Ulfuse:w:$(FUSE_LOW):m

check-fuses: ## Verify device signature and check fuse values
	$(AVRDUDE) $(AVRDUDE_OPTS)

simul: $(ELF)
	simulavr -d attiny2313 -t attiny2313 -f $(ELF)

mo4: mo4.S  tn4313def.h attiny4313_registers.h
	avr-gcc -mmcu=attiny4313 $^

dump: $(HEX)
	@echo "Dump of Flash"
	$(AVRDUDE) $(AVRDUDE_OPTS) -U flash:r:$(MKFILE_DIR)/flash.bin:r
	@$(AVR_OBJDUMP) --architecture=$(ARCHITECTURE) --demangle --disassemble --source --wide $(MKFILE_DIR)/flash.bin

Blink in assembly

#include "tn4313def.h"	; I found this header file on Internet; it is mostly correct.

; *******************************************************************************************
; Macros
; *******************************************************************************************
; These macros fixes the address shift between I/O and registers

; When using the I/O specific commands IN and OUT, the I/O addresses 0x00 - 0x3F must be used. 
; When addressing I/O Registers as data space using LD and ST instructions, 0x20 must be added to these addresses.

.macro  STORE addr,reg
.if     \addr < 0x60
 out    \addr,\reg
.else
 sts    \addr + 0x20,\reg
.endif
.endm

.macro  LOAD reg,addr
.if     \addr < 0x60
 in     \reg,\addr
.else
 lds    \reg + 0x20,\addr
.endif
.endm  

;	Calculate the delay value
.equ	CLOCK_FREQ,	8000000		; 8 MHz
.equ	PRESCALER,	1024		; / 1024
.equ	DELAY_VALUE, (CLOCK_FREQ / (2 * PRESCALER)) / 16

; We use a 10 LED bargraph. MIDI_LED must flash every time LOOP_LED changes states. CTRL_LED is the control LED (always ON)
.equ	MIDI_LED, PIND6		; Timer 1 (compare with OCR1A)
.equ	LOOP_LED, PIND4		; Software loop
.equ	CTRL_LED, PIND0

.equ	V17,	0x01

.text
Reset:          rjmp main
INT0addr:       rjmp Reset
INT1addr:       rjmp Reset
ICP1addr:       rjmp Reset
OC1Aaddr:       rjmp OC1Aaddr_routine
OVF1addr:       rjmp Reset
;
; Starting from this address, it's not necessary to declare the interrupt vectors
; since I don't use them. I can start the program here. However, since I have enough
; program memory (4K - 40 bytes - stack), I prefer to declare the vectors. They all go
; to Reset, hence to main.
;
OVF0addr:       rjmp Reset
URXCaddr:       rjmp Reset
UDREaddr:       rjmp Reset
UTXCaddr:       rjmp Reset
ACIaddr:        rjmp Reset
PCIBaddr:       rjmp Reset
OC1Baddr:       rjmp Reset
OC0Aaddr:       rjmp Reset
OC0Baddr:       rjmp Reset
USI_STARTaddr:  rjmp Reset
USI_OVFaddr:    rjmp Reset
ERDYaddr:       rjmp Reset
WDTaddr:        rjmp Reset
PCIAaddr:       rjmp Reset
PCIDaddr:       rjmp Reset

; =========================================================
; Main routine
; =========================================================
main:   cli                      ; disable interrupts
        ldi     r16, lo8(RAMEND) ; Set stack pointer to end of ram
        STORE   SPL, r16
        ;ldi    r16, hi8(RAMEND) ; SPH doesn't exist on this CPU
        ;STORE  SPH, r16

        ldi     r16, (1<<PUD) ; Disable Pull-Up resistors globally
        STORE   MCUCR,r16         ; Update the MCU Control Register

        ; We select the pins of Port D that must be set as outputs
        ldi     r16, (1<<MIDI_LED)|(1<<LOOP_LED)|(1<<CTRL_LED)
        STORE   DDRD, r16         ; ... and set the data direction of port B to "out"

; ---------------------------------
; Configure Timer 1
; ---------------------------------
; Configure Timer/Counter 1 to mode 4 (CTC) and prescaler / 1024
;       TOP value = OCR1A
;TOV1 Flag Set on = MAX
;          |   7  |   6  |   5  |   4  |   3  |   2  |   1  |   0  |
;          +------+------+------+------+------+------+------+------+
; TCCR1A = |COM1A1|COM1A0|COM1B1|COM1B0|   -  |   -  | WGM11| WGM10|
;          |   0  |   0  |   0  |   0  |   0  |   0  |   0  |   0  |
; TCCR1B = |ICNC1 |ICES1 |   -  | WGM13| WGM12| CS12 | CS11 | CS10 |
;          |   0  |   0  |   0  |   0  |   1  |   1  |   0  |   1  | = mode 4, prescaler /1024
; TCCR1C = |FOC1A |FOC1B |   -  |  -   |   -  |   -  |   -  |   -  |
;          |   0  |   0  |   0  |   0  |   0  |   0  |   0  |   0  |

		ldi	r16, hi8(DELAY_VALUE)	; Prepare the values for OCR1A
        ldi	r17, lo8(DELAY_VALUE)
Very important here: 16 bits registers must be loaded high byte first. Otherwise, only the lowest byte is loaded, leading to unexpected results
; WRONG, NOT WHAT YOU'D EXPECT
;       STORE OCR1AL, r16		<--- lowest byte will be loaded
;       STORE OCR1AH, r17		<--- but not the highest byte; OCR1A contains 0x00FF not 0xFFFF.

; This is the good way 
        STORE OCR1AH, r16 ; High byte must be written first ...
        STORE OCR1AL, r17 ; .. when setting a 16 bits register.

; Configure the Timer/Counter 1 Control Registers
        clr   r16
        STORE TCCR1A, r16
        STORE TCCR1C, r16
        ldi   r16,(1<<WGM12)|(1<<CS10)|(1<<CS12)      ; Set CS10 and CS12 for prescaler of 1024
        STORE TCCR1B, r16
; Configure the OCF1A Interrupt Mask Register
        LOAD  r16, TIFR
        ldi   r16, (1<<OCIF1A)
        STORE TIFR, r16
; Configure the Timer/Counter Interrupt Mask Register
        ldi   r16, (1<<OCIE1A)
        STORE TIMSK, r16

        sei            ; Set Global Interrupt Enable
; ---------------------------------
; Starting the show
; ---------------------------------
        ; Light on the Control LED
        ldi     r16, (1<<CTRL_LED)|(1<<LOOP_LED)
        STORE   PORTD,  r16 

oloop:	; outer loop
    	ldi  r17, V17  ; Initialize our software counter
    	ldi  r18, 0xff ; Initialize our software counter
        ldi  r19, 0xff ; Initialize our software counter
iloop:	; inner loop
    	dec  r17       ; decrement r20
    	brne iloop
    	ldi  r17, V17  ; reset r20
    	dec  r18       ; decrement r21
    	brne iloop
    	ldi  r17, V17  ; reset r20
    	ldi  r18, 0xff ; reset r21
    	dec  r19       ; decrement r22
    	brne iloop
        
        ldi   r23, (1<<LOOP_LED)
        rcall toggle   ; Toggle the LOOP LED

        rcall start_timer
        rjmp  oloop    ; Restart the loop

; =========================================================
; Sub-routines
; =========================================================
toggle:		; Toggle the LED in register r23
        LOAD r16, PORTD  ; Load content of Port D
        eor  r16, r23    ; Exclusive OR to toggle the bit
        STORE PORTD, r16 ; Write Port D
        ret
start_timer:
; Disable interrupts
        cli
; Reset the TCNT1 counter
        clr   r16
        clr   r17
        STORE TCNT1H, r16
        STORE TCNT1L, r17
; Set prescaler = 1024
        LOAD  r16, TCCR1B
        ldi   r16, (1<<WGM12)|(1<<CS10)|(1<<CS12)
        STORE TCCR1B, r16
; Light the MIDI LED on
        LOAD  r16, PORTD
        sbr   r16, (1<<MIDI_LED)
        STORE PORTD, r16
; Enable interrupts
        sei
        ret

stop_timer:
; Set prescaler = 0 -> stop the timer
        LOAD  r16,  TCCR1B
        ldi   r16,  (1<<WGM12)
        STORE TCCR1B, r16
; Light the MIDI LED off
        LOAD  r16,   PORTD
        cbr   r16,   (1<<MIDI_LED)
        STORE PORTD, r16
        ret

; =========================================================
; Interrupt Service Routine (ISR)
; =========================================================
OC1Aaddr_routine: ; Timer 1: interrupt when TCNT1 = OCR1A
        cli              ; Disable interrupt
        rcall stop_timer
        sei              ; Enable interrupt
        reti             ; Return from ISR
  

Compiling and uploading

 make && make upload 
And yes, it blinks!

Follow up:  Part 1  Part 2  Part 3  Part 4  Part 5  Part 6 .

Comments

Popular posts from this blog

Drive replacement for Fostex DMT8-vl

The IDE hard drive on my Fostex DMT8-vl multitrack recorder shows signs of its imminent death; when getting hot, I could not record anymore. Must be said this drive comes from an old Sun Station, and has been replaced because I/O failures were detected by Solaris. It worked at least 5 years in my recorder: not so bad. However, time is now to replace it. The DMT8-vl is not able to handle drives bigger than 8.4 GB. Well, it is able to (the current drive is 15 GB), but only 8.4 GB will be usable. My tought was to use a 8 GB CompactFlash; having no moving parts means no noise, which is quite temptating for a music recording device. I purchased a CompactFlash-IDE adapter on the internet (8$) and I had to build a male-male IDE cable adapter (4$). Unfortunately, this doesn't work. The drive is correctly discovered by the operating system, which proposes to format it ("format IDE?"). After answering "yes", the formating runs pretty fast (faster than on a real drive), ...

Samba: Clients get "system error 1223" (or 123) after a server reboot

Facts: a Linux+Samba server shares anonymously a folder. After a reboot, Win clients could not attach the share drive anymore. C:\>net use \\mylinux\folder Enter the user name for 'mylinux': System error 1223 has occurred. The operation was canceled by the user. C:\>net view \\mylinux\ System error 123 has occurred. The filename, directory name, or volume label syntax is incorrect. The process are present, and tcpdump doesn't provide much information. What's going on? After hours of headscratching, the light came: the firewall was on and no rules for the Samba protocol! Grrr!

Issue with Soundpool MO4

I have a Atari STe with a Soundpool MO4 MIDI extension. It used to work very well, but unfortunatelly doesn't anymore: Cubase still detects it, and I can output MIDI to it but nothing is coming out from any MIDI Out. It took me a while to tackle it (lack of time, lack of tool, other items to play with), but I gave a glance last week-end. The parallel port on the Atari uses only the following signals: Pin 1 : Strobe (Atari -> MO4) Pin 2 : Data 0 (Atari -> MO4) Pin 3 : Data 1 (Atari -> MO4) Pin 4 : Data 2 (Atari -> MO4) Pin 5 : Data 3 (Atari -> MO4) Pin 6 : Data 4 (Atari -> MO4) Pin 7 : Data 5 (Atari -> MO4) Pin 8 : Data 6 (Atari -> MO4) Pin 9 : Data 7 (Atari -> MO4) Pin 11: Busy (MO4 -> Atari) The MO4 also decodes few other pins, but since the Atari doesn't, my guess is the MO4 was also targeted for PC. Inside the box, the MO4 is architectured around a CPLD (IspLSI1016 from Lattice) which contains the logi...