diff options
-rw-r--r-- | .gitignore | 4 | ||||
-rw-r--r-- | LICENSE | 14 | ||||
-rw-r--r-- | Makefile | 36 | ||||
-rw-r--r-- | README.md | 107 | ||||
-rw-r--r-- | guides/debugging.md | 81 | ||||
-rw-r--r-- | timer.s | 110 |
6 files changed, 352 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..26734b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.hex +*.o +*.elf +timer @@ -0,0 +1,14 @@ +BSD Zero Clause License + +Copyright (c) 2025 Sotov Konstantin + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6b8f6b4 --- /dev/null +++ b/Makefile @@ -0,0 +1,36 @@ +# adjust according to your needs +AS = avr-as +LD = avr-ld +OBJ = avr-obj +MCU ?= atmega328p +MCUPATH ?= /dev/ttyACM0 +DUDE ?= avrdude +PROGRAMMER ?= arduino +LDFLAGS += -m avr5 +DUDEFLAGS += -c $(PROGRAMMER) -p $(MCU) -P $(MCUPATH) + +# timer.s file +TARGET ?= timer + +.PHONY: flash dump-timer clean all + +all: $(TARGET).hex + +flash: $(TARGET).hex + test $(shell id -u) = 0 + $(DUDE) $(DUDEFLAGS) -U flash:w:$< + +%.hex: % + $(OBJ)copy -O ihex $< $@ + +%: %.o + $(LD) $(LDFLAGS) -o $@ $< + +%.o: %.s + $(AS) -mmcu=$(MCU) -o $@ $< + +dump-%: % + $(OBJ)dump -d timer + +clean: + rm *.o *.hex *.elf *.bin timer diff --git a/README.md b/README.md new file mode 100644 index 0000000..575493b --- /dev/null +++ b/README.md @@ -0,0 +1,107 @@ +# AVR assembly programming +This is a repo containing my attempts at programming in AVR assembly + +I want the every byte of macihne code executed by my processor to be written by +myself, so no libraries, no definitions, no nothing. + +### DISCLAIMER +Please have a sense of humor and don't take the jokes in this +README seriously. + +### WARNING +ChatGPT and any other genAI, as of 2025-09-05, can NOT write proper +AVR assembly. And I doubt they'll learn any time soon. Try it for yourself, +maybe you'll get something working if they write it completely from scratch, +but as for debugging, you are on your own. Use debugging tools mentioned below. + +Examples of generative AI being completely useless include: them trying to +persuade me that I've used the wrong interrupt vector address, mixing up +`out`/`sts` and `in`/`lds` instructions, mixing up addresses of IO registers, +writing/reading data from wrong registers (like mixing up `TIMSK`/`TIFR`, +`PINx/PORTx`), not knowing how to actually compile the code and so much more, +not unserstanding how timers work and so on. Save yourself some time, don't use +genAI. + +## How can I use it? +Firstly, as an example of how to program in assembly for +AVR. I am not responsible for the quality though :silly:. My only source is +datasheets. + +Secondly, there is a really useful [Makefile](Makefile) that you can copy to use +with your own projects. Most variables do not need to be changed there, maybe +just the `MCUPATH`. Name a file `something.s`, and then run `make +TARGET=something` to build, or `make TARGET=something flash` to upload your +code to the arduino. You can set the `TARGET` as an environment variable to +aviod typing it in each invokation of `make`. + +Thirdly, for debugging. Run `make TARGET=something sim`, then run `avr-gdb +something`, when inside of it, type `target remote :1234` and you're good to go. +`make sim` automatically detects wheher the simulation is already running and +forks to background, so to rerun the simulation, just type `make sim` again: it +will rebuild the file, and you won't even have to restart gdb (it will preserve +breakpoints), just retype the `target remote :1234` command. I find this +workflow satisfying, and it does not exhaust my arduino flash memory by +flashing the program every 30 seconds there. I think I will add a debugging +guide as well... + +## Dependencies +### Software +* `make` +* avr toolchain (`avr-as`, `avr-ld`, `avr-objcopy`, `avr-objdump`) +* `avrdude` (flash tool) +* For debugging: `avr-gdb` + `simavr` (or `qemu-system-avr`, but I had problems + with it) + +Not that you don't actually need `avr-libc` or `avr-gcc` to build assembly. +### Hardware +* an ARDUINO UNO board, or any other AVR-based microcontroller if you change + the variables in Makefile. Note that most of the variables can be changed + from the environment. +* wirez +* rizz + +### Dependency installation +* Arch linux: `# pacman -S avr-binutils avrdude`. For C development, you also + need to install `avr-gcc` and `avr-libc`. For debugging, `avr-gdb` and + `simavr` [\[AUR\]](https://aur.archlinux.org/packages/simavr) + [\[CLONE\]](https://aur.archlinux.org/simavr.git) +* Any other non-superior distro: read the fucking manual + +## Why? +- to have fun. + +## Useful resources +* [ATMega328p datasheet](https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-7810-Automotive-Microcontrollers-ATmega328P_Datasheet.pdf) +* [AVR instruction manual](https://ww1.microchip.com/downloads/en/DeviceDoc/AVR-Instruction-Set-Manual-DS40002198A.pdf) +* [AVR130: Setup and use of AVR Timers](https://ww1.microchip.com/downloads/en/Appnotes/Atmel-2505-Setup-and-Use-of-AVR-Timers_ApplicationNote_AVR130.pdf) +* [Arduino UNO R3 info](https://docs.arduino.cc/hardware/uno-rev3/), [PINOUT](https://docs.arduino.cc/resources/pinouts/A000066-full-pinout.pdf) + +## Read this if you are going into it blind +This section contains some of the pain that I've gone through while doing this +programming journey. If you don't want to make same mistakes, please take your +time to read it. + +0. AVR interrupt addresses from datasheet are not the same ones you specify in + assembly! <br> + For example, TIMER1 COMPA (timer 1 compare A match) has address + `0x0016` in the datasheet, but in assembly you have to align it like this: + ```asm + .org 0x002C: jmp timer1_int + ``` + Why? Because they probably refer to "instruction address", not to "memory + address". Is it said anywhere? No. Can you figure it out by compiling + corresponding C code and looking at disassembly? Yes, but you will spend 5 + hours trying to understand why your interrupt is not working. +0. Don't forget to link your file after compiling! <br> + This would have not been a problem on a processor that has an OS, because + the file just wouldn't run unless you link it. But here you're using + `objcopy`. That means that ALL jumps and calls will be broken in your + assembly. I thought that this was a bug for so long, dont be like me. +0. I've said this before, but don't ever try to use AI, or even worse, vibe + code the microcontroller. It's just not going to work, you will loose time + instead of saving it. You can vibecode a crappy website in react only because + there is so much training data for it. But almost entirety of AVR programming + is done in C++, thanks to fucking nobody. But I mean, it is a good thing that + AI can't comprehend assembly. It means you'll have more fun exploring the old + fashined way ;) +0. I will add entries to this list as I go... diff --git a/guides/debugging.md b/guides/debugging.md new file mode 100644 index 0000000..1e13191 --- /dev/null +++ b/guides/debugging.md @@ -0,0 +1,81 @@ +# This is a simple guide to using AVR-GDB with simavr +First of all, you can use the [Makefile](../Makefile) in the directory above. +But for the competeness sake, I'll list commands + +## Running simulation +QEMU: I don't personally prefer it, but here is a breakdown: +```sh +qemu-system-avr -M arduino-uno -bios <image> -nographic -s -S +``` +* `-M`: machine to use, shorthand for `--machine`. Self-explanatory. +* `-bios`: the thing that you would flash onto your arduino. Note that this is + not a hex file, this must be an elf file, the one you get out of `ld`. +* `-nographic`: this should be default to be honest +* `-s`: shorthand for `-gdb tcp::1234`. Enables GDB debugging +* `-S`: suspend execution and wait for debugger to connect. +This should be everything you need. +[More documentation] (https://www.qemu.org/docs/master/system/target-avr.html) + +SIMAVR: This is the one I prefer + +First of all, the `--help` is actually not as big so it's easy to guess +everything yourself, but fine, here is the command: +```sh +simavr -m atmega328p -g <image> +``` +I think you can already see why I like it more. Breakdown of flags: +* `-m, --mcu` stands for "MicroController Unit". atmega328p is the processor arduino + uno uses +* `-g, --gdb`: wait for gdb to connect on `:1234` +* `-f, --freq`: optionally specify the clock frequency, arduino uno has 16MHz +You can also interrupt the process to kill it! Qemu does not let you do that. + +## gdb +```sh +$ avr-gdb <image> +(gdb) target remote :1234 +(gdb) c +``` +First of all, load the image. Then connect to the simulation. Since this is not +a program in a traditional sense, it's already running and you have to +`continue` instead of `run`. +* to break at an address, use `b *0x69`. also, 0x69 is odd, so you will not land + there. Instructions are either 16 bit or 32 bit on AVR, so almost any even + address is a valid instruction. +* to print instructions at some address, use `x/16i 0x420`, replace 16 with + amount of instructions and 0x420 with an actual address. you can also use + `$pc`, which stands for program counter (current instruction) as an address. + Also, unlike on intel/arm64, you can safely write `x/4i $pc-4` and get a valid + disassembly. +* To print register values, use `print $r16`, replace `r16` with any register + you want. For formatted printing, see `help x` in gdb shell, it is often + useful to `print/x` in hexadecimal or `print/t` in binary. +* use `displ` command. You will probably want to track current instruction, 1 or + 2 registers and some IO memory addresses. For me, a "must have" is + `displ/i $pc`, which displays an instruction to be executed each time the + execution avdances. really useful for stepping +* `si` command steps 1 assembly insstruction. you will find yourself using it a + lot +* To print IO registers, use `print *(unsigned char*)0xADD12E55`. NOTE that, if + you can use `in/out` instruction to access a memory location, gdb can't, it + will always use `sts/lds`. So don't forget `0x20` to all IO locations before + `0x3F`. For example, if you want to track `PORTB`, you'd often write + `displ/t *(unsigned char)0x25`, even though documentation says the `PORTB` is + at `0x05`. +* To restart the program without restarting the simulation, you can write + `set $pc = 0`. This will trick a processor into thinking that next instruction + is at `0x00`, which is a reset vector. Note that state of your registers does + not reset. The set command it useful in many places, for example you want a + loop to execute for a little longer - you can set the counter register to 0. +* you can safely put breakpoints at interrupts, at any labels you defined, + anywhere. +* I coudn't get `compile code` gdb instruction to work (to execute assembly + on-the-fly), because it can't find the avr C compiler. Maybe this is a + distribution misconfiguration. Didn't bother setting up stuff in a debian VM. + +This is it! Hope this was helpful. + +P.S.: Internet has a lof of `avr-gdb` guides, and even more "just `gdb`" guides, +which are as relevant. Search them up and have fun debugging! + +JAC, 2025 @@ -0,0 +1,110 @@ +.equ PINB, 0x03 +.equ DDRB, 0x04 +.equ PORTB, 0x05 +.equ PIND, 0x09 +.equ DDRD, 0x0A +.equ PORTD, 0x0B +.equ TIFR0, 0x15 +.equ ADDR, 0x256 +.equ SPL, 0x3D +.equ SPH, 0x3E +.equ SREG, 0x3F +.equ TCCR0A, 0x0024 +.equ TCCR0B, 0x0025 +.equ TCNT0, 0x0026 +.equ OCR0A, 0x0027 +.equ TIMSK0, 0x006E +.equ STOFL, 0xFF +.equ STOFH, 0x08 +.equ TOIE0, 1 + +.equ LOC, 0x128 +.equ FREQ, 0x80 + +.org 0x0000 ; reset interrupt +jmp init +.org 0x0040 ; timer 0 overflow interrupt +jmp tmrint + + +.org 0x0068 +init: + ; IO + sbi DDRB, 5 + sbi DDRD, 2 + cbi PORTB, 5 + cbi PORTD, 2 + ; stack + ldi r16, STOFL + ldi r17, STOFH + out SPL, r16 + out SPH, r17 + ; initial + ldi r16, 69 + sts ADDR, r16 + ; timer + ldi r18, 0b00000001 + out TCCR0B, r18 + ldi r18, TOIE0 + sts TIMSK0, r18 + sei + rcall main + +tmrint: + push r18 + in r18, SREG + push r18 + push r19 + push r20 + push r21 + + lds r20, LOC + ldi r21, FREQ + inc r20 + cpse r20, r21 + rjmp .stt + + in r18, PORTD + ldi r19, (1 << 2) + eor r18, r19 + out PORTD, r18 + eor r20, r20 +.stt: + sts LOC, r20 +.exit: + pop r21 + pop r20 + pop r19 + pop r18 + out SREG, r18 + pop r18 + sei + reti + + +main: + sei + ldi r17, 128 +.loop: + lds r1, ADDR + in r6,TCNT0 + cp r1, r17 + breq .finloop + inc r1 + sts ADDR, r1 + rjmp .loop +.finloop: + + in r13, PINB + in r14, PORTB + + lds r16, ADDR + cpse r16, r17 + cbi PORTB, 5 + + in r13, PINB + in r14, PORTB + + rjmp main + + |