Skip to content

shashfrankenstien/ArchieTiny

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Archie Tiny OS (ATtiny85)

Portability (CR2477 at 1000 mAh??)

Debugging

Deps

sudo apt-get install avr-libc binutils-avr gcc-avr avrdude
sudo apt install simavr

Simulator

  • use simavr + gdb combo - see Makefile sim: and gdb: labels
  • simavr clock seems difficult to manage (need to verify) - buserror/simavr#201
  • see debug.gdb for hacky time estimation command

to compile simavr, we need glut

sudo apt install freeglut3-dev

Digispark

fuses

  • Low fuse: 0xe1 -- 16 MHz mode with no clock divide
  • High fuse: 0x5d -- EEPROM not preserved, brown-out detection enabled at 2.7v
  • Extended fuse: 0xfe -- Self-programming enabled

Raw attiny85 using ATTinyDebugTools

https://github.com/shashfrankenstien/ATTinyDebugTools

Function ATTinyDebugTools ATtiny85
SCK D13 pin 7
MISO D12 pin 6
MOSI D11 pin 5
VCC D10 pin 8
RESET D6 pin 1
GND GND pin 4

default fuses

  • Low fuse: 0x62 -- 8 MHz mode with clock divide by 8 (1 MHz)
  • High fuse: 0xdf -- SPI enabled, EEPROM not preserved
  • Extended fuse: 0xff -- Self-programming disabled

change to 16 MHz clock

  • Low fuse: 0xe1 -- 16 MHz mode with no clock divide

change to preserve eeprom + brownout detection

  • High fuse: 0xd7 -- EEPROM preserved
  • High fuse: 0xd6 -- EEPROM preserved + brown out detection at 1.8 volts

Power Management

  • disable Timer1 since we are not using that for anything at the moment (PRR bit 3)
  • disable brown-out detection through software while deep sleep
  • MCUCR – MCU Control Register - Bit 5 – SE: Sleep Enable
    • The SE bit must be written to logic one to make the MCU enter the sleep mode when the SLEEP instruction is executed

ArchieTiny implementation NOTES

  • Timer 0 in CTC mode - TCNT0,TCCR0A,TCCR0B,OCR0A
  • Timer compare A interrupt at addr 0x000A; enabled in TIMSK
  • Ugh, need to add -nostartfiles to avr-gcc so it doesn't include weird extra code that kills interrupts.
    • This also eliminates need to create and expose a global main routine

Time and Delays (lib/timer.asm)

  • Features
    • 24 bit software time counter - this requires that timer_tick_isr is attached to an interrupt that triggers every 1 millisecond
    • also includes a sort of accurate clock cycle counter delay. (see timer_delay_clock_cycles subroutine)
  • Ticks are stored in RAM addressed by TIME_SOFT_COUNTER config variable
    • HIGH_BYTE:MIDDLE_BYTE:LOW_BYTE
    • TIME_SOFT_COUNTER+2:TIME_SOFT_COUNTER+1:TIME_SOFT_COUNTER

Resource Status Registers

  • Each of the below resources are allocated 1 register of size 1 byte to store custom status flags
  • Each of these status registers are described within their corresponding modules
Resource Register config name Module
Oled SREG_OLED sh1106.asm
GPIO SREG_GPIO_PC gpio.asm
I2C I2C_BUS_RLOCK usi_i2c.asm

Task Manager (lib/tasks.asm)

Tasks Table is set up starting at RAM address TASK_RAM_START (Should be greater than 0x60 = 32 general registers + 64 I/O registers).

Task table

  • First byte will be the task counter (TASKCTS)
  • Second byte will be current task index (TASKPTR)
  • Next addresses will contain word size values of task stack pointers
         _________
        |_________| --> TASKCTS - task counter and status register (1)
        |_________| --> TASKPTR - current task index / pointer (1)
        |_________| --> task stack pointers vector (TASK_MAX_TASKS*2)
        |         | --> task stack 1 (TASK_STACK_SIZE)
        |_________|
        |         | --> task stack 2 (TASK_STACK_SIZE)
             .
             .

Task workflow

  • init
    • set TASKCTS and TASKPTR to 0
  • add new task
    • increment TASKPTR till we find an empty slot in TASK_SP_VECTOR
    • calculate stack pointer address and store in TASK_SP_VECTOR at TASKPTR index
    • jump to task's alotted stack
    • store return address, function pointer + manager pushed registers on the stack
      • Note: Because of how the stack works, function pointer address should be divided by 2. cpu will then multiply it by 2 before executing (or we can use pm() apparently)
    • if TASK_SP_VECTOR is full, set FULL flag in TASKCTS
  • exec task
    • read TASKCTS counter, if eq 0 or 1, simply return because there is no task switching required
    • if RUNNING bit is set, there was previously a task that was running
    • push registers + SREG to stack, read TASKPTR, save stack pointer in TASK_SP_VECTOR at TASKPTR index
    • increment TASKPTR to go to next task
      • initially, TASKPTR will be 0
      • if TASKPTR overflows beyond TASK_MAX_TASKS, wrap around to 0
    • load stack pointer value from TASK_SP_VECTOR at TASKPTR index
    • set new stack pointer, pop all registers + SREG
    • reti

I2C (drivers/usi_i2c.asm)

  • Built-in USI I2C

    • outputs USIDR MSB on SDA line on falling edge of SCL
    • slave devices read on rising edge of SCL
    • slave addresses seem to be shifted left
      • for example, in SH1106, documentation says addresses are 0111100 and 0111101, but in reality, device only reponds to 01111000 and 01111001
  • I2C can only be used by one task at a time. Before using I2C, a task has to acquire a lock

  • I2C_BUS_RLOCK - i2c reentrant lock register (1)

   -------------------------------------------------------------------------------------
   | RLKCNT3 | RLKCNT2 | RLKCNT1 | RLKCNT0 | TASKPTR3 | TASKPTR2 | TASKPTR1 | TASKPTR0 |
   -------------------------------------------------------------------------------------
  • i2c lock is task specific - meaning, each task can acquire locks multiple times (reentrant) they only need to release it as many times to fully release the i2c lock
  • a lock can be acquired only if RLKCNT (I2C_BUS_RLOCK[7:4]) is 0
  • when a lock is acquired, I2C_BUS_RLOCK[3:0] is set to current TASKPTR value, and RLKCNT is incremented
  • when a lock is released, RLKCNT is decremented. When it reaches 0, the lock is fully released
  • tasks using i2c should use i2c_rlock_acquire and i2c_rlock_release these routines facilitate wait-aquire-release workflow
  • i2c_rlock_acquire will sleep till lock can be acquired it returns once it is able to acquire the lock

OLED display - I2C (drivers/sh1106.asm)

  • In SH1106 driver, 128x64 OLED is centered in most cases within the 132x64 ram, that means pixel (2,0) in ram is pixel (0,0) on the display
  • SH1106 Command Table is on page 30 of the datasheet
  • documentation and resources recommend sleeping for 100 ms before displaying anything on the screen
  • fonts - https://github.com/Tecate/bitmap-fonts - see vendor/bdf_fonts for more
    • bitocra7
    • spleen
    • unscii-fantasy
  • when including strings in program memory, we need to mind byte alignment. use .balign 2 after each string definition
  • SREG_OLED is used to track contrast, color inversion (highlight) and page scroll position
    --------------------------------------------------------------------------------------------------------------
    |  CNTRST3  |  CNTRST2  |  CNTRST1  |  CNTRST0  |  OLED_COLOR_INVERT  |  SCRL_PG2  |  SCRL_PG1  |  SCRL_PG0  |
    --------------------------------------------------------------------------------------------------------------

OLED text-mode (lib/textmode.asm)

  • this module wraps oled and provides helper routines to print continuous text
  • it handles control characters
    • '\n' as newline
    • '\b' as backspace
  • also manages page scrolling

Controls (drivers/gpio.asm)

Button press event manager (PCINT)

  • digital pin change interrupts (active low) - interrupt triggers for both falling and rising edges
    • on falling edge (button press), both GPIO_BTN_x_PRS and GPIO_BTN_x_HLD bits are set in SREG_GPIO_PC
    • Any program handling button press must clear GPIO_BTN_x_PRS bit after handing the press
    • on rising edge interrupt (button release), GPIO_BTN_x_HLD bits are automatically cleared

Button press (voltage divided ADC)

  • ADC ISR writes 8-bit precision byte from ADC_CHAN_x to ADC_CHAN_x_VAL register. We can use this byte to identify button press and release

  • We need to check expected voltage levels in ascending order

    • only 1 button can be pressed at a time per channel.
    • check lowest voltage threshold. If ADC reading is lower, set ADC_VD_CHx_BTN_y bit in r16 indicating press
    • continue checking as long as no press is identified
  • Reading button presses (Software stabilization)

    • ADC clock speed is clk / 128. for clk = 16 MHz, ADC clock speed will be 125 kHz
    • ADC generally takes about 13 - 15 ADC clocks to perform a conversion.
    • Let's approx to 14 which gives us a conversion frequency of ~9 kHz (i.e. each conversion takes ~110 micro seconds)
    • We're using a 680 pF capacitor against 50 k ohm internal pull-up (RESET pin) for smoothing. So, time to charge up to 63% is (50 * 10^3 * 681 * 10^-12) = 34 micro seconds (TAO). We might read a wrong value during this charge / discharge time. We can assume that the capacitor will be reasonably full at 5 * TAO
    • Given the ADC conversion period (110 micro seconds), we should make sure multiple readings are within threshold to confirm a button press
    • To be absolutely safe, we can take a bunch of readings waiting a few ms between them; report a press only if all the readings pass the same threshold
  • ADC voltage divider value calculation (internal pull up resistance R1)

    • equations (only care about 8 MSB precision)

      • VOUT = lambda VIN, R1, R2: VIN * R2/(R1+R2)
      • ADC_VAL = lambda VREF, VOUT: int((VOUT * 1024) / VREF) >> 2
    • below are approx measured / fudged values that worked out in tests

    • voltages are usually below these values. just to be sure, we set the threshold to be a few counts above these values (see config.inc)

    • channel 0: PIN 2 (Attiny85 ADC 3)

      • VREF = Vcc = 2.85 v
      • VIN = Vpin = 2.8 v
      • R1 = 35 kilo ohm aprox (guess??)
ADC button Resistance (R2) Voltage ADC threshold (8 MSB precision) V1 Mapping
ADC_VD_CH0_BTN_0 20 K 1.018 v 0x5b OK
ADC_VD_CH0_BTN_1 51 K 1.660 v 0x95 NAV_UP_BTN
ADC_VD_CH0_BTN_2 68 K 1.849 v 0xa6 NAV_DOWN_BTN
ADC_VD_CH0_BTN_3 100 K 2.074 v 0xba NAV_LEFT_BTN
ADC_VD_CH0_BTN_4 300 K 2.507 v 0xe1 NAV_RIGHT_BTN
  • similarly, chanel 1 - PIN 1 (reset pin - Attiny85 ADC 0)

    • VREF = Vcc = 2.8 v
    • VIN = Vpin = 2.45 v
    • R1 = RESET pin pull-up = 50 kilo ohm aprox (guess??)
  • when using the reset pin, input voltage cannot be below ~1.3 v (documentation says 0.9 v :/). to remedy this, reset pin resistors are all connected in series (each are 68 K)

ADC button Resistance (R2) Voltage ADC threshold (8 MSB precision) V1 Mapping
ADC_VD_CH1_BTN_0 68 K 1.412 v 0x81 OPTIONS_BTN
ADC_VD_CH1_BTN_1 136 K 1.791 v 0xa3 EXIT_BTN
ADC_VD_CH1_BTN_2 204 K 1.968 v 0xb3 ENTER_BTN

Keyboard / Typing (lib/kbd.asm)

  • text kbd input is just a single character printed with inverted colors
  • provides ability to
    • scrub through all characters (SCRUB_NEXT_BTN & SCRUB_PREV_BTN)
    • select the current character (SCRUB_OK_BTN)
    • remove previous character (SCRUB_BACKSP_BTN)
    • add space character (SCRUB_SPACE_BTN)
    • new line (ENTER_BTN)

Dynamic heap memory allocation - malloc (lib/mem.asm)

  • MALLOCFREECTR (1 byte)

    • this counter tracks the number of free blocks available
    • intially, this is set to MALLOC_MAX_BLOCKS
  • malloc table (MALLOC_TABLE_SIZE bytes)

    |_________|
    |_________| --> MALLOCFREECTR
    |         | --> malloc table index 0 (MALLOC_TABLE_START)
    |         |     .
    |         |     .
    |         |     .
    |_________|     malloc table index MALLOC_MAX_BLOCKS (MALLOC_TABLE_END - 1)
    |         | --> start of malloc blocks (MALLOC_TABLE_END)
  • bytes in the malloc table are indexed starting with 0 and counting up to MALLOC_MAX_BLOCKS

  • each byte corresponds to a block of memory of the same index

  • if the value of the byte is 0xff, the block at the corresponding index is free

  • if the value of the byte is 0xfe, it means that one block of data is allocated at the index

  • if the value of the byte is any other number, that number is the index of the next block of the allocated memory

    • a chain of blocks terminate when the value is 0xfe
  • during allocation of multiple blocks, the final block is allocated first, all the way up to the first block

    • this is just because it works with simpler code. should make no other difference
  • mallock blocks (MALLOC_BLOCK_SIZE * MALLOC_MAX_BLOCKS bytes)

    • free RAM is allocated in blocks of MALLOC_BLOCK_SIZE

    • block chaining is handled by the malloc table

    • min(MALLOC_FREE_RAM, 256) is divided into blocks of MALLOC_BLOCK_SIZE bytes

      • capped at 256 so that we can use 8 bit pointers and 8 bit MALLOCFREECTR
  • MALLOC_MAX_BLOCKS can't be greater than 250 (never gonna happen on this device, but whatever)

    • MALLOC_MAX_BLOCKS is capped at 250 because the last few address values are used as control bytes in the malloc table (0xff, 0xfe, ..)

FAT-8 File System (lib/fs.asm)

  • https://www.youtube.com/watch?v=HjVktRd35G8

  • file system (fat-8) - structure is basically very similar to mem.asm

  • uses driver/eeprom.asm (or optionally drivers/mb85rc64.asm) to read and write

  • FATFREECTR (1 byte)

    • this counter tracks the number of free clusters available
    • intially, this is set to FS_MAX_CLUSTERS
  • file allocation table / FAT (FAT_BYTE_SIZE bytes)

    |_________|
    |_________| --> FATFREECTR
    |         | --> FAT index 0 (FAT_START) - 2 byte entries
    |         |     .
    |         |     .
    |         |     .
    |_________|     FAT index FAT_BYTE_SIZE (FAT_END - 1)
    |         | --> start of fs clusters (FAT_END)
  • this implementation of FAT is set of doubly linked lists. this makes it easier/faster to scroll (thinking i2c in the future)
  • a pair of bytes in the FAT are indexed starting with 0 and counting up to FS_MAX_CLUSTERS
  • each pair corresponds to a cluster of memory of the same index
  • first byte is the index of previous cluster (PREV_IDX), and second byte is the index of next cluster (NEXT_IDX)
    -------------------------------------------
    | PREV_IDX(1) | NEXT_IDX(1) | .......
    -------------------------------------------
  • the cluster at the corresponding index is free if the value of both bytes (PREV_IDX and NEXT_IDX) are 0xff
  • if the value of both bytes are 0xfe, it means that one cluster of data is allocated at the index
  • if the value of either byte is any other number, that number is the index of the prev/next cluster of the allocated disk space
    • the first cluster in a chain of clusters has PREV_IDX value of 0xfe
    • a chain of clusters terminate when the NEXT_IDX value is 0xfe

dirent

  • directories make up a tree structure across the file system
  • when formatted, the fs will have the first cluster allocated to be the root directory
  • the root directory may contain entries to more directories
  • directory entry cluster - 10 bytes per entry -> 2 entries per cluster of 20 bytes
    --------------------------------------------------
    | SIGNATURE(1) |     NAME(8)     | START_ADDR(1) |
    --------------------------------------------------
  • description

    • NAME (8 bytes)
    • SIGNATURE (1 byte) -> information about the item (is_directory, read_only flags, external mount info)
    • START_ADDR (1 byte) -> cluster address of the first cluster. use FAT to find additional clusters
  • unlike typical FAT file systems, directory clusters do not have a '.' or '..' entry (to save space)

    • the caller should instead save parent directory pointers before opening a file or subdirectory

fs pointers

  • to avoid 16 bit math, fs subroutines work on a pair of values

  • when reading raw data (file / directory names or file contents):

    • r16 points to cluster index (possible values: 0 to FS_MAX_CLUSTERS - 1)
    • r17 points to byte index within cluster (possible values: 0 to FS_CLUSTER_SIZE - 1)
  • for directory related operations:

    • r16 will contain index to starting cluster
    • r17 will be used to index an item within a directory (may span across clusters. max is 256 items in a dir??)
      • also, r17 index needs to skip over deleted items??? Ugh
  • signature byte

    --------------------------------------------------------------------------------------------------
    |  IS_EXT_MOUNT  |  N/A  |  N/A  |  N/A  |  N/A  |  FS_IS_DIR  |  FS_IS_DELETED  |  FS_IS_ENTRY  |
    --------------------------------------------------------------------------------------------------

FRAM fs - I2C

  • fs_wrapper_* methods are wrappers around eeprom_* or fram_* routines
  • we use these accross the board, and have the ability to switch between internal eeprom and external fram

File Manager app (based on fs)

  • notes TODO

Buzzer - PWM (drivers/buzzer.asm)

  • only PB1 (pysical pin 6) is currently supported for buzzer as it uses Timer/Counter1 in PWM1A mode

  • PWM_CTRL -> PWM1A enabled; PB1 cleared on compare match and set when TCNT1=0; prescaler at CK/256 prescaled clock frequency is determined by bits [3:0] of PWM_CTRL byte

  • per PWM_CTRL, while running Timer1 in PWM1A mode:

    • a match on compare register A (OCR1A) triggers the PWM signal to go LOW
    • a match on compare register C (OCR1C) triggers the PWM signal to go back HIGH
  • PWM_COMPVAL_C controls the frequency / pitch

    • with a 16MHz clock (CK), PWM counter frequency is set to CK/256 in PWM_CTRL
    • PWM_COMPVAL_C acts as the PWM signal divider. so final frequency at the BUZZER_PIN (OC1A) will be CK/256/PWM_COMPVAL_C
    • middle-C musical note (262 Hz) can thus be obtained by setting PWM_COMPVAL_C = 238
      • 16000000 / 256 / 238 = 262.605
      • or more generally, PWM_COMPVAL_C = lambda NOTE_FREQ: int(16000000 / 256 / NOTE_FREQ)
      • ( value floored so as to make the notes slightly sharp )
  • PWM_COMPVAL_A controls volume

    • PWM signal at BUZZER_PIN will be HIGH till counter reaches PWM_COMPVAL_A, then goes low till PWM_COMPVAL_C
    • if signal stays HIGH for just as long as it stays LOW, volume is maximum (PWM_COMPVAL_A = PWM_COMPVAL_C / 2)
    • PWM_COMPVAL_A can be varied between 0 and PWM_COMPVAL_C/2 to change volume
  • buzzer_macro_play_melody and buzzer melody format

    • buzzer_macro_play_melody takes 2 arguments

      • tempo in milliseconds (1 unit of duration to play a note)
      • melody a sequence of pairs => a note, and duration units to play the note
    • example usage

      • buzzer_macro_play_melody 250 C4 1 D4 1 E4 2 D4 1 C4 1

Real Time Clock - I2C (based on fs)

  • notes TODO

The Shell!

  • shell_home_task is the entry point to user space. It is started through task manager
    • starts with a splash screen
    • on button press, shows main menu of applications
  • reusable ui components (lib/ui.asm)
    • splash screen (shell.asm)
    • confirm y/n popup
    • scrollable menu

EEPROM settings (file?) (TODO)

Text Editor! (TODO)

Simple Calculator (TODO)

Game (pong?) (TODO)


docs/Atmel-AT1886-Mix-C-and-Asm.pdf

Table 5-1. Summary of the register interfaces between C and assembly.

Register Description Assembly code called from C Assembly code that calls C code
r0 Temporary Save and restore if using Save and restore if using
r1 Always zero Must clear before returning Must clear before calling
r2-r17 “call-saved” Save and restore if using Can freely use
r28 “call-saved” Save and restore if using Can freely use
r29 “call-saved” Save and restore if using Can freely use
r18-r27 “call-used” Can freely use Save and restore if using
r30 “call-used” Can freely use Save and restore if using
r31 “call-used” Can freely use Save and restore if using

Related (and unrelated?)

Some STM8S stuff

Some PIC stuff

Some NRF53832 stuff (PineTime)


Similar projects

About

OS for ATtiny85 (WIP)

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages