Browse Source

Add 08-i2c.

master
flabbergast 5 months ago
parent
commit
413e1db8cc
  1. 31
      08-i2c/Makefile
  2. 14
      08-i2c/README.md
  3. 50
      08-i2c/macros.s
  4. 158
      08-i2c/main.s
  5. 1
      08-i2c/memmap
  6. 69
      08-i2c/p_clock.s
  7. 92
      08-i2c/p_delay.s
  8. 163
      08-i2c/p_i2c.s
  9. 114
      08-i2c/p_interrupts.s
  10. 76
      08-i2c/p_uart0.s
  11. 1
      08-i2c/platform_regs.inc
  12. 289
      08-i2c/write.s

31
08-i2c/Makefile

@ -0,0 +1,31 @@
PROG ?= main
JLINKEXE ?= ~/.platformio/packages/tool-jlink/JLinkExe
PREFIX ?= riscv32-elf-
ARCH ?= rv32imac
AFLAGS = --warn --fatal-warnings -g -L
LDFLAGS = -m elf32lriscv -b elf32-littleriscv --discard-none
all: $(PROG).hex
$(PROG).o: $(PROG).s p_i2c.s
$(PREFIX)as -march=$(ARCH) $(AFLAGS) $(PROG).s -o $(PROG).o
$(PROG).hex: memmap $(PROG).o
$(PREFIX)ld $(LDFLAGS) -o $(PROG).elf -T memmap $(PROG).o
$(PREFIX)objdump -Mnumeric -D $(PROG).elf > $(PROG).list
$(PREFIX)objcopy $(PROG).elf $(PROG).bin -O binary
$(PREFIX)objcopy $(PROG).elf $(PROG).hex -O ihex
$(PREFIX)size $(PROG).elf
flash: $(PROG).hex
echo -e "loadfile $(PROG).hex\nr\ng\nexit\n" | $(JLINKEXE) -device FE310 -if JTAG -speed 4000 -si JTAG -jtagconf -1,-1 -autoconnect 1
clean:
rm -f *.bin
rm -f *.hex
rm -f *.o
rm -f *.elf
rm -f *.list

14
08-i2c/README.md

@ -0,0 +1,14 @@
# 08-i2c
Add a basic i2c driver. Really only 1 function, `i2c_rw` which writes some bytes to the i2c bus, and then reads some bytes from it (either/both phases can be skipped).
The "demo" code in `main.s` is for messing around with a 24C02 eeprom (256 bytes, so just 1 byte addressing).
Note: the i2c peripheral remains in the "transfer-in-progress" state if there are no pull-ups on the I2C lines -- which there are none on Red-V Thing Plus, unless you use that solder jumper on the bottom of the board. I suppose it is because the peripheral supports clock stretching, and if there's no pullup on the SCL line, then it may remain low indefinitely, and the i2c peripheral is waiting until it comes up.
### Lessons learned
- Having a debugger is a godsend; I should not ever try to start learning on a bare chip.
- When I write new registers into `platform_regs.inc` by hand, re-check at least 17 times.
- Re-check push/pop patterns, especially when returning early, e.g. from a macro.

50
08-i2c/macros.s

@ -0,0 +1,50 @@
#
# macros.s
#
# 2021 flabbergast <flabbergast@drak.xyz>
# Unlicense: https://unlicense.org/
#
# designate some RAM memory to a symbol
.macro ramallot Name, Size
.equ \Name, rampointer
.set rampointer, rampointer + \Size
.endm
# stack: push, pop
.macro push reg
addi sp, sp, -4
sw \reg, 0(sp)
.endm
.macro pop reg
lw \reg, 0(sp)
addi sp, sp, 4
.endm
# insert a sized string
.macro sized_string Text
.byte 8f - 7f # Compute length of string.
7: .ascii "\Text"
.if compressed_isa
8: .balign 2, 0 # Realign
.else
8: .balign 4, 0 # Realign
.endif
.endm
# data stack
# _s11_ used as the data stack pointer!!
.macro pushda reg
addi s11, s11, -4
sw \reg, 0(s11)
.endm
.macro popda reg
lw \reg, 0(s11)
addi s11, s11, 4
.endm
.macro pushdaconst const
push t0 # yep ... should rethink this
li t0, \const # but it should being used very little
pushda t0
pop t0
.endm

158
08-i2c/main.s

@ -0,0 +1,158 @@
#
# main.s
#
# 2021 flabbergast <flabbergast@drak.xyz>
# Unlicense: https://unlicense.org/
#
.option norelax
.equ compressed_isa, 1
# -----------------------------------------------------------------------------
# macros and RAM variables
# -----------------------------------------------------------------------------
.include "macros.s"
.set rampointer, 0x80000000 # start allocating at the beginning of RAM
# end of RAM is +0x4000
ramallot data_stack_begin, 128 # 32 values
ramallot data_stack_end, 0
ramallot return_stack_begin, 512
ramallot return_stack_end, 0
# -----------------------------------------------------------------------------
# Execution begins here
# -----------------------------------------------------------------------------
.text
j Reset
# -----------------------------------------------------------------------------
# Static data
# -----------------------------------------------------------------------------
# -----------------------------------------------------------------------------
# Include any supporting code
# -----------------------------------------------------------------------------
.include "platform_regs.inc"
.include "p_interrupts.s"
.include "p_clock.s"
.include "p_delay.s"
.include "p_uart0.s"
.include "write.s"
.include "p_i2c.s"
# -----------------------------------------------------------------------------
Reset: # Execution begins here
# -----------------------------------------------------------------------------
# set up stacks
li sp, return_stack_end
li s11, data_stack_end
# set up interrupt and exception handler (ours needs sp)
csrci mstatus, 8 # disable interrupts
la t0, irq_handler
csrw mtvec, t0
csrsi mstatus, 8 # enable interrupts
# the board (usually) runs on 13.8MHz HFROSC on boot?
jal clock_extcryst # switch to external crystal, 16MHz
jal uart_init # initialise UART0 (115200baud)
# if you change clock, you'll need to adjust the
# baud divisor in p_uart0.s
jal write_init # set up default "emit" over uart
jal delay_init # enable the timer interrupt
jal i2c_init # init i2c: adjust prescaler if you change clock!
puts "Welcome!\n"
## messing with a 24c02b epprom (256bytes, 1byte addressing)
# allocate some space (hope it's 4-aligned...)
ramallot eep_readbuf, 8
ramallot eep_writebuf, 8
.equ eep_addr, 0x50
# data into writebuf: 1byte addr
li t0, eep_writebuf
li t1, 0x01
sb t1, 0(t0)
# comm with eep
li a0, eep_addr # i2c address
li a1, eep_writebuf
li a2, 1 # write 1 byte (eep address)
li a3, eep_readbuf
li a4, 8 # read 8 bytes from eeprom
jal i2c_rw
pushda a0
puts "i2c comm returned %B\n"
# print the result
puts "Recv: "
li a0, 8
li a1, eep_readbuf
jal write_bytes_hex
puts "\n"
li a0, 200 # 200ms delay
jal delay
# data into writebuf: 1byte addr, 3bytes data (little endian)
li t0, eep_writebuf
li t1, 0xFECAAA01
sw t1, 0(t0)
# comm with eep
li a0, eep_addr # i2c address
li a1, eep_writebuf
li a2, 4 # write 1 byte (eep address; 3 data)
li a4, 0 # read nothing
jal i2c_rw
pushda a0
puts "i2c comm returned %B\n"
li a0, 200 # 200ms delay
jal delay
# data into writebuf: 1byte addr
li t0, eep_writebuf
li t1, 0x00
sb t1, 0(t0)
# comm with eep
li a0, eep_addr # i2c address
li a1, eep_writebuf
li a2, 1 # write 1 byte (eep address)
li a3, eep_readbuf
li a4, 8 # read 8 bytes from eeprom
jal i2c_rw
pushda a0
puts "i2c comm returned %B\n"
# print the result
puts "Recv: "
li a0, 8
li a1, eep_readbuf
jal write_bytes_hex
puts "\n"
## chase again
li s1, GPIO_BASE
li s2, 0b111111<<18 # pins 18-23
sw s2, GPIO_OUTPUT_EN(s1) # set as outputs
li a0, 200 # delay in ms
li s2, 0b1<<18 # start with pin 18
li s3, 0b1<<23 # end with pin 23
1:
sw s2, GPIO_OUTPUT_VAL(s1) # make the pin high
li a0, 200
jal delay
slli s2, s2, 1 # move one to the left
bleu s2, s3, 1b # did we move past last pin?
li s2, 0b1<<18 # back to the first pin
j 1b
end:
j end

1
08-i2c/memmap

@ -0,0 +1 @@
../memmap

69
08-i2c/p_clock.s

@ -0,0 +1,69 @@
#
# p_clock.s: functions for setting up the main clock
#
# 2021 flabbergast@drak.xyz
# Unlicense: https://unlicense.org/
#
.if compressed_isa
.balign 2, 0
.else
.balign 4, 0
.endif
.include "platform_regs.inc"
# switch to external crystal (16MHz on red-v thing)
clock_extcryst:
li t0, PRCI_BASE
1: # Wait for crystal to oscillate
lw t1, PRCI_HFXOSCCFG(t0) # hfxoscen(bit30) is on by default on reset
bgtz t1, 1b # ... hfxoscrdy(bit31): makes value negative
li t1, 0x00070df1 # 0x00060df1 | (1<<16) | (1<<17) | (1<<18)
# = Reset value | PLLSEL | PLLREFSEL | PLLBYPASS
sw t1, PRCI_PLLCFG(t0) # Select crystal as the main clock source
ret
# switch to 64MHz PLL driven by 16MHz external crystal
clock_pll_64mhz:
li t0, PRCI_BASE
1: # Wait for crystal to oscillate
lw t1, PRCI_HFXOSCCFG(t0) # hfxoscen(bit30) is on by default on reset
bgtz t1, 1b # ... hfxoscrdy(bit31): makes value negative
# Set PLL to 64 MHz (don't activate); 64 = 16 /(2^1) *(2*(31+1)) /(2^3)
li t1, 0x20df1 # pllr=1, pllf=0x1f, pllq=0x3 (reset vals)
sw t1, PRCI_PLLCFG(t0) # pllsel=0, pllrefsel=1, pllbypass=0(!)
2: # Wait for PLL to lock: plllock(bit31): sign bit
lw t1, PRCI_PLLCFG(t0)
bgtz t1, 2b
li t1, 0x30df1 # pllsel=1 (on top of the above)
sw t1, PRCI_PLLCFG(t0)
ret
# switch to 320MHz PLL driven by 16MHz external crystal
# (the board gets quite warm!)
clock_pll_320mhz:
li t0, PRCI_BASE
1: # Wait for crystal to oscillate
lw t1, PRCI_HFXOSCCFG(t0) # hfxoscen(bit30) is on by default on reset
bgtz t1, 1b # ... hfxoscrdy(bit31): makes value negative
# Set PLL to 320 MHz (don't activate); 320 = 16 /(2^1) *(2*(39+1)) /(2^1)
li t1, 0x20671 # pllr=1, pllf=0x27(!), pllq=1(!)
sw t1, PRCI_PLLCFG(t0) # pllsel=0, pllrefsel=1, pllbypass=0(!)
2: # Wait for PLL to lock: plllock(bit31): sign bit
lw t1, PRCI_PLLCFG(t0)
bgtz t1, 2b
li t1, 0x30671 # pllsel=1 (on top of the above)
sw t1, PRCI_PLLCFG(t0)
ret

92
08-i2c/p_delay.s

@ -0,0 +1,92 @@
#
# p_delay.s: functions for waiting
#
# 2021 flabbergast@drak.xyz
# Unlicense: https://unlicense.org/
#
.if compressed_isa
.balign 2, 0
.else
.balign 4, 0
.endif
.include "platform_regs.inc"
.equ MTIME_FREQUENCY, 33 # 32_768 Hz clock
# enable timer interrupt and put a 'safe' value into MTIMECMP
delay_init:
# clear the interrupt by writing a high value to CLINT_MTIMECMP.h
li t0, CLINT_BASE+CLINT_MTIMECMP
li t1, 0xFFFFF000
sw t1, 4(t0)
li t0, 1<<7
csrs mie, t0 # set MTIE bit
ret
# a0: delay value in milliseconds
# (should be less than 0xFFFF_FFFF/33/4)
# this version uses interrupts if enabled,
# i.e. it waits with wfi, rather than a busy loop
delay:
li t0, CLINT_BASE+CLINT_MTIME+8 # load addresses
li t4, CLINT_BASE+CLINT_MTIMECMP
lw t1, -8(t0) # load the current value of the timer.l
lw t3, -4(t0) # load current timer.h
li t2, MTIME_FREQUENCY # let our clock frequency
mul t2, t2, a0 # multiply milliseconds with frequency
add t2, t1, t2 # target mtime is now in t2
bgeu t2, t1, 4f # _not_ overflown back to zero?
# if overflown, we assume that (signed)t1<0 and t2>0
addi t3, t3, 1 # overflown => increase target time.h by one
4:
sw t2, 0(t4) # set MTIMECMP.l (assuming .h is still very big)
sw t3, 4(t4) # set MTIMECMP.h
sltu t3, t1, t2 # keep the "overflown" flag in t3
1: # main waiting loop
csrr t1, mstatus # check if interrupts are enabled
andi t1, t1, 8
beqz t1, 3f
wfi # if they are, wait for one
3:
lw t1, -8(t0) # read mtime value again
bnez t3, 2f # check overflow
bltu t1, t2, 1b # no overflow: unsigned comparison
2:
blt t1, t2, 1b # overflow: (signed)t1 can be <0
li t1, 0xFFFFF000 # clear timer interrupt
sw t1, 4(t4) # ... by writing big into MTIMECMP.h
ret
# a0: delay value in milliseconds
# (should be less than 0xFFFF_FFFF/33/4)
# delay:
# li t0, CLINT_BASE+CLINT_MTIME+8 # load the timer registers base
# lw t1, -8(t0) # load the current value of the timer (lsw)
#
# li t2, MTIME_FREQUENCY # let our clock frequency
# mul t2, t2, a0 # multiply milliseconds with frequency
# add t2, t1, t2 # target mtime is now in t2
# bltu t2, t1, 2f # overflown back to zero?
# # if yes, assume that (signed)t1<0 and t2>0
#
# 1:
# lw t1, -8(t0) # read mtime value again
# bltu t1, t2, 1b # keep looping until timout
# ret
#
# 2: # overflown branch
# lw t1, -8(t0) # read mtime; t1 can be "<0"
# blt t1, t2, 2b # do signed compare
# ret
# a0: (delay in cycles)/2
busy_delay_cycles:
1:
addi a0, a0, -1
bnez a0, 1b
ret

163
08-i2c/p_i2c.s

@ -0,0 +1,163 @@
#
# p_i2c0.s: basic i2c driver
#
# 2021 flabbergast@drak.xyz
# Unlicense: https://unlicense.org/
#
# no description in the manual, refers to https://opencores.org/projects/i2c
# (that requires registration which requires manual approval from someone)
# here's a copy of i2cspecs.pdf:
# https://pilvi.disasm.info/index.php/s/MqvdhL3lBqFxY0P/download
#
# prescaler:
# - max 0xFFFF
# - (clock_rate/(5*baud)) - 1
# - on 16MHz core, 400kHz I2C => 7
# - on 16MHz core, 100kHz I2C => 31
# - set when CONTROL_EN is off
# sample usage:
# jal i2c_init (somewhere at the beginning)
#
# ## i2c bus scanner
# li s1, 1 # start with address
# li s2, 127 # end with address
# 2:
# mv a0, s1
# li a2, 0
# li a4, 0
# jal i2c_rw # try address
# bltz a0, 3f # no response=>skip
# pushda s1 # print message
# puts "Found a device at %B\n"
# 3:
# li a0, 2 # wait a little
# jal delay # ... seems to be too fast otherwise
# addi s1, s1, 1 # inc counter
# bleu s1, s2, 2b # repeat
.if compressed_isa
.balign 2, 0
.else
.balign 4, 0
.endif
.include "platform_regs.inc"
i2c_init:
li t0, GPIO_BASE # UART0 RX/TX are selected in IOF_SEL (IOF0) on Reset.
li t2, (1<<12)|(1<<13) # which bits
lw t1, GPIO_IOF_EN(t0) # set IOF_EN bits ... read existing
or t1, t1, t2 # twiddle bits
sw t1, GPIO_IOF_EN(t0) # write back
not t2, t2 # invert bits
lw t1, GPIO_IOF_SEL(t0) # clear IOF_SEL bits (tho they should be fine on Reset)
and t1, t1, t2 # twiddle bits
sw t1, GPIO_IOF_SEL(t0) # write back
li t0, I2C0_BASE
sb zero, I2C_CTR(t0) # control: disable en, ie
sb zero, I2C_PRERhi(t0) # prescaler MSB
li t1, 31
sb t1, I2C_PRERlo(t0) # prescaler LSB
li t1, I2C_CTR_EN
sb t1, I2C_CTR(t0) # enable module; irq disabled
ret
.macro _wait_for_tip
1: # wait-for-transfer loop
lbu t1, I2C_SR(t0) # status register
andi t1, t1, I2C_SR_TIP # "transfer-in-progress" bit
bnez t1, 1b # wait for TIP to clear
.endm
.macro _check_ack
# check ack
lbu t1, I2C_SR(t0) # status again
andi t1, t1, I2C_SR_RXACK # ack received?
beqz t1, 1f # should be 0
li t1, I2C_CR_STOP # slave did not respond, so...
sb t1, I2C_CR(t0) # set stop condition on the bus
li a0, -1 # set 'fail' return value
ret
1:
.endm
# read/write bytes through I2C
# expects:
# - a0: I2C address
# - a1: pointer to data to write
# - a2: how many bytes to write
# - a3: pointer to data to read
# - a4: how many bytes to read
# returns: a0: 0 success, -1 problem
# a1 and a3 point to 1 byte after the last used
# note: both r/w can have 0 bytes to r/w
# in which case that part is skipped
# note: does not jal anything, so does not push/pop ra!
# if you want to e.g. puts, need to add this,
# including in the macros above!
i2c_rw:
li t0, I2C0_BASE
slli a0, a0, 1
# if zero write nonzero read, goto read
bnez a2, 1f # writing some bytes=>skip
bnez a4, 5f # reading=>goto reading part
1: # writing
sb a0, I2C_TXR(t0) # write the address to tx register
li t1, I2C_CR_START|I2C_CR_WRITE
sb t1, I2C_CR(t0) # start cond, write to bus
_wait_for_tip
_check_ack
add t2, a1, a2 # t2: end of loop pointer
bnez a2, 3f # writing some bytes=>skip
li t1, I2C_CR_STOP # here: 0 write, 0 read
sb t1, I2C_CR(t0) # just set stop
j 9f # and exit
3: # write loop
lbu t1, 0(a1) # read current byte to send
sb t1, I2C_TXR(t0) # write data to tx register
li t1, I2C_CR_WRITE
addi a1, a1, 1 # inc address
bltu a1, t2, 1f # if not the last byte, skip
bnez a4, 1f # subsequent read => do not set stop cond
ori t1, t1, I2C_CR_STOP # last byte => stop condition
1:
sb t1, I2C_CR(t0) # do the deed
_wait_for_tip
_check_ack
bltu a1, t2, 3b # repeat for the next byte
beqz a4, 9f # nothing to read=>goto end
5: # reading
ori t1, a0, 1 # address | read_bit
sb t1, I2C_TXR(t0) # write the address to tx register
li t1, I2C_CR_START|I2C_CR_WRITE
sb t1, I2C_CR(t0) # start cond, write to bus
_wait_for_tip
_check_ack
add t2, a3, a4 # t2: end of loop pointer
addi t2, t2, -1 # one less (to detect the last byte)
6: # read loop
li t1, I2C_CR_READ
bne a3, t2, 1f # if not the last byte=>skip
ori t1, t1, I2C_CR_STOP|I2C_CR_ACK # last byte=>stop and nack
1:
sb t1, I2C_CR(t0)
_wait_for_tip
lbu t1, I2C_RXR(t0) # read the received byte
sb t1, 0(a3) # store it
addi a3, a3, 1 # inc pointer
bleu a3, t2, 6b # repeat for the next byte
9: # end
li a0, 0
ret

114
08-i2c/p_interrupts.s

@ -0,0 +1,114 @@
#
# interrupts.s: basic irq handling setup
#
# 2021 flabbergast@drak.xyz
# Unlicense: https://unlicense.org/
#
# ! when you edit the irq handler, don't forget to push/pop
# any extra registers that you use !
# - we don't use the vectored interrupt handling, so
# there is a single function that's executed on any
# interrupt or exception (which needs to make decisions
# based on mcause)
# - the address of this fn should be loaded into mtvec
#
# (SiFive FE310 only has 3 interrupt sources (sw, timer, plic),
# so any vector table would only have 3 "used" entries.)
#
.include "platform_regs.inc"
# irq handler needs to be always 4-aligned
.balign 4, 0
irq_handler:
addi sp, sp, 9*(-4)
sw ra, 8*4(sp) # x1
sw s1, 7*4(sp) # x9
sw t6, 6*4(sp) # x31
sw t5, 5*4(sp) # x30
sw t4, 4*4(sp) # x29
sw t3, 3*4(sp) # x28
sw t2, 2*4(sp) # x7
sw t1, 1*4(sp) # x6
sw t0, 0*4(sp) # x5
# check if it's an interrupt or an exception
csrr t0, mcause
blt t0, zero, .interrupts
## exceptions
.exceptions:
# read CSR and stall
csrr t1, mepc # tells us where it occured
# t0 still contains mcause
jal zero, .exceptions
## interrupts
.interrupts:
andi t0, t0, 0xF # FE310 only has irqs 3,7,11
srli t0, t0, 2
bne t0, zero, 1f
## 3, software interrupt
jal zero, irq_handler_exit
1:
srli t0, t0, 1
bne t0, zero, 1f
## 7, timer interrupt
# clear the interrupt by writing a high value to CLINT_MTIMECMP.h
li t0, CLINT_BASE+CLINT_MTIMECMP
li t1, 0xFFFFF000
sw t1, 4(t0)
jal zero, irq_handler_exit
1:
## 11, external interrupt
# # use PLIC claim/complete mechanism
# li t0, PLIC_BASE+PLIC_CLAIM-4 # easier load
# lw s1, 4(t0) # claim: highest pending irq id
#
# # UART0
# li t1, 3 # UART0 is source 3
# bne t1, s1, 1f # if not UART0, skip
# jal ra, irq_handler_uart0
#
# 1:
# # any other PLIC sources should go here
#
# li t0, PLIC_BASE+PLIC_CLAIM-4
# sw s1, 4(t0) # complete the claimed irq
# exit
irq_handler_exit:
lw t0, 0*4(sp) # x5
lw t1, 1*4(sp) # x6
lw t2, 2*4(sp) # x7
lw t3, 3*4(sp) # x28
lw t4, 4*4(sp) # x29
lw t5, 5*4(sp) # x30
lw t6, 6*4(sp) # x31
lw s1, 7*4(sp) # x9
lw ra, 8*4(sp) # x1
addi sp, sp, 9*4
mret
# enable enternal interrupts (PLIC)
plic_init:
li t0, PLIC_BASE+PLIC_THRESHOLD
sw zero, 0(t0) # set PLIC threshold 0 (all irqs unmasked)
li t0, PLIC_BASE+PLIC_ENABLE1
sw zero, 0(t0) # disable all PLIC irq sources
sw zero, 4(t0) # (a lot seem enabled after reset?)
li t0, 1<<11
csrs mie, t0 # set MEIE(bit11)
ret

76
08-i2c/p_uart0.s

@ -0,0 +1,76 @@
#
# p_uart0.s: basic uart0
#
# 2021 flabbergast@drak.xyz
# Unlicense: https://unlicense.org/
#
.ifdef compressed_isa
.balign 2, 0
.else
.balign 4, 0
.endif
.include "platform_regs.inc"
# UART0: RX:pin16 TX:pin17
# init
# !! need to change baud setting if the main clock changes
# clobbers: t0, t1, t2
uart_init:
li t0, GPIO_BASE # UART0 RX/TX are selected in IOF_SEL (IOF0) on Reset.
li t2, (1<<17)|(1<<16) # which bits
lw t1, GPIO_IOF_EN(t0) # set IOF_EN bits ... read existing
or t1, t1, t2 # twiddle bits
sw t1, GPIO_IOF_EN(t0) # write back
not t2, t2 # invert bits
lw t1, GPIO_IOF_SEL(t0) # clear IOF_SEL bits (tho they should be fine on Reset)
and t1, t1, t2 # twiddle bits
sw t1, GPIO_IOF_SEL(t0) # write back
li t0, UART0_BASE # setting baud rate
li t1, 139-1 # 16 MHz / 115200 Baud = 138.89
sw t1, UART0_DIV(t0)
li t1, 1
sw t1, UART0_TXCTRL(t0) # enable transmit
sw t1, UART0_RXCTRL(t0) # enable receive
ret
# emit one byte
# a0: (unsigned!) byte to send
# blocking until byte sent
# clobbers: t0, t1
uart_emit:
li t0, UART0_BASE+UART0_TXDATA
1: # repeat until accepted
amoor.w t1, a0, (t0) # (t0)->t1, then write (t0)|a0 into (t0)
bnez t1, 1b # non-zero means tx fifo full
ret
# check if ready to transmit
# returns: a0 non-zero if tx fifo full
# clobbers: t0
q_uart_emit:
li t0, UART0_BASE
lw a0, UART0_TXDATA(t0) # bit31 indicates "full"
ret
# receive a byte
# returns: a0: received byte
# blocking until available
# clobbers: t0
uart_recv:
li t0, UART0_BASE
1: # wait until we get a byte from rx fifo
lw a0, UART0_RXDATA(t0) # bit31="empty", bit7-bit0 data
bltz a0, 1b
ret
# check if there is an incoming byte waiting
# returns: a0 non-zero if rx fifo empty
# clobbers: t0
q_uart_recv:
li t0, UART0_BASE
lw a0, UART0_RXDATA(t0) # bit31 indicates "empty"...
ret # (there can be data in b7-b0 even if "empty")

1
08-i2c/platform_regs.inc

@ -0,0 +1 @@
../platform_regs.inc

289
08-i2c/write.s

@ -0,0 +1,289 @@
#
# write.s: printing strings
#
# 2021 flabbergast@drak.xyz
# Unlicense: https://unlicense.org/
#
# assumes: stack macros, ramallot macro, uart_emit symbol
# if 'fixed_emit'==0, assumes that an address for "emit" is
# at 'write_emit_routine_p'
# (and assumes that the routine expects the byte in a0)
#
# uses sized strings: b_len b_c1 .. b_cn
#
# we use the stack/memory here - the expectation is that
# "emit" is 'slow' anyway, so don't worry about speed
# can use up to 48 bytes on return stack (temporarily of course)
#
.if compressed_isa
.balign 2, 0
.else
.balign 4, 0
.endif
# either just always do uart_emit, or use a pointer to RAM to
# store an "emit" routine address
.equ fixed_emit, 0
.if fixed_emit
.equ emit_routine, uart_emit
.else
# reserve a location where the address of an "emit" routine is stored
ramallot write_emit_routine_p, 4
.endif
# record the default "emit" routine
# clobbers: t0, t1
write_init:
.if fixed_emit
.else
la t0, uart_emit # let write.s know what to use for printing
li t1, write_emit_routine_p
sw t0, (t1)
.endif
ret
# print a single hexadecimal digit (internal)
# expects:
# - a0: value to print (assumed 0-15)
# - s7: "emit" routine to use
_write_custom_hexdigit:
addi a0, a0, -10 # check if we're adding '0' or 'a'
bltz a0, 1f
addi a0, a0, 'a' # a-f
jr s7 # jump to "emit" routine
1:
addi a0, a0, 10+'0' # 0-9
jr s7 # jump to "emit" routine
# print a hexadecimal unsigned value, up to some #of digits
# expects:
# - a0: value to print
# - a1: shift to 1st nibble to print (e.g. 16bit->12)
# - a2: if non zero, print leading zero digits
write_value_hex:
push ra # push return addr onto stack; we are doing calls
push s0 # we'll use s0 (as temp)
push s7 # s7 as well (for "emit" address)
.if fixed_emit
la s7, emit_routine
.else
li s7, write_emit_routine_p
lw s7, 0(s7) # load "emit" address into s7
.endif
or s0, a0, a2 # for checking if a0==a2==0
bnez s0, 1f # if s0==0, then set a1=0 (only printing the last zero)
li a1, 0
1:
bnez a1, 2f # if a1==0, set a2=1 (always print the last digit)
li a2, 1
2:
mv s0, a0 # save the value that we're printing
3: # main digit printing loop
srl a0, s0, a1 # extract the nibble into a0
andi a0, a0, 0xF # ...
bnez a0, 4f # it's not zero, we're printing it
beqz a2, 5f # not requesting to print zero
4:
li a2, 1 # if we're printing a digit, we're printing the rest
jal _write_custom_hexdigit
5:
addi a1, a1, -4 # move to the next nibble if a1 didn't become negative yet
bgez a1, 3b
pop s7
pop s0
pop ra # restore the return address
ret
# print a byte, hexadecimal, unsigned
# expects:
# - a0: value to print (lowest byte printed)
# - a2: if non zero, always print two digits
# clobbers: a1
write_byte_hex:
li a1, 4
j write_value_hex
# print bytes from memory, hex, padded
# expects:
# - a0: number of bytes
# - a1: address to memory
write_bytes_hex:
push ra # push return addr onto stack; we are doing calls
push s0 # preserve s0
push s1 # preserve s1
mv s1, a1 # we'll use it for looping
li a2, 1 # want padding with zeroes
add s0, s1, a0 # compute final address
1: # byte printing loop
lbu a0, 0(s1) # get the byte
li a1, 4 # want to print two nibbles
jal write_value_hex
addi s1, s1, 1 # increment the address pointer
bgeu s0, s1, 1b # loop if we're not beyond s0 yet
pop s1 # restore s1
pop s0 # restore s0
pop ra # restore the return address
ret
# output a fixed string
.macro puts Text
jal _puts
sized_string "\Text"
.endm
# output a string, address in ra
# need to also fix ra
# clobbers: a0
_puts:
push t6 # let's not clobber
mv a0, ra
lbu t6, 0(ra) # get the string length
addi ra, ra, 1 # skip size byte
add ra, ra, t6 # skip string
# round up to the next half/word boundary
.if compressed_isa
andi t6, ra, 1
add ra, ra, t6
.else
andi t6, ra, 1
add ra, ra, t6
andi t6, ra, 2
add ra, ra, t6
.endif
pop t6
# ! will continue to write_str below !
# print a sized string
# expects:
# - a0: sized string address (length >=1)
# - data stack: data for variables printing (%..)
write_str:
push ra
push s0
push s1
push s7 # s7 as well (for "emit" address)
push a1 # we'll need those, and let's not clobber
push a2 # ... anything
.if fixed_emit
la s7, emit_routine
.else
li s7, write_emit_routine_p
lw s7, 0(s7) # load "emit" address into s7
.endif
lbu s1, 0(a0) # set up loop: s0:current, s1:end
addi s0, a0, 1
add s1, s1, s0
1: # main character printing loop
lbu a0, 0(s0) # read byte
addi s0, s0, 1 # inc pointer
li a1, '%' # check if it's a "print value" char (a1 as temp)
bne a0, a1, 3f
# logic to deal with % flags
lbu a0, 0(s0) # get next char
addi s0, s0, 1 # inc pointer
li a2, 1 # require padding for numbers by default
li a1, 'B' # (padded) unsigned hex byte (a1 as temp)
bne a0, a1, 2f
popda a0 # get the value to be printed (data stack)
li a1, 4 # two digits
jal write_value_hex
j 4f
2:
li a1, 'H' # (padded) unsigned hex half (a1 as temp)
bne a0, a1, 2f
popda a0 # get the value to be printed (data stack)
li a1, 12 # four digits
jal write_value_hex
j 4f
2:
li a1, 'W' # (padded) unsigned hex word (32bit) (a1 as temp)
bne a0, a1, 2f
popda a0 # get the value to be printed (data stack)
li a1, 28 # eight digits
jal write_value_hex
j 4f
2:
li a1, 'd' # unsigned decimal (up to 99999999) (a1 as temp)
bne a0, a1, 2f
popda a0 # get the value to be printed (data stack)
jal to_bcd # convert to BCD
li a2, 0 # do not pad
li a1, 28 # max eight digits
jal write_value_hex
j 4f
2: # more flags can be added here
# fallthrough just prints the character
3:
jalr s7 # call "emit"
4:
blt s0, s1, 1b # repeat until s0 reaches s1
pop a2
pop a1
pop s7
pop s1
pop s0
pop ra
ret
# convert unsigned to BCD (binary coded decimal)
# algo from: https://fraserinnovations.com/fpga-tutor/altera-risc-v-fpga-tutorial-hexadecimal-number-to-bcd-code-conversion-and-application-fii-pra040-fpga-board-experimental-7/
# expects:
# - a0: number to convert (should be < 99999999)
# returns:
# - a0: BCD
# clobbers: intentionally nothing...
to_bcd:
push t0
push t1
push t2
push t3
push t4
slli a0, a0, 5 # skip first few bits, should be <99999999
li t0, 0 # store the result here
li t1, 26 # 27 bits to process in the loop
1: # main loop
# nibble checking isn't done after the last shift
li t3, 28 # check the nibbles: start with the topmost
li t4, 5 # "if nibble>=5, add 3 to it"
2:
srl t2, t0, t3 # shift to the current nibble (reuse t2)
andi t2, t2, 0xF # ... and mask it out
bltu t2, t4, 3f # skip if nibble < 5
li t2, 3 # want to add 3 to the nibble (reuse t2)
sll t2, t2, t3 # shift to the right place
add t0, t0, t2 # add it to the nibble
3:
addi t3, t3, -4 # move to the next nibble
bge t3, zero, 2b # ... unless we've done all of them
srli t2, a0, 31 # extract top bit of a0 (reuse t2)
slli t0, t0, 1 # ... and rotate it
or t0, t0, t2 # ... into t0
slli a0, a0, 1 # also move it out of a0
addi t1, t1, -1 # loop counter
bge t1, zero, 1b
mv a0, t0 # move the result to a0
pop t4
pop t3
pop t2
pop t1
pop t0
ret
Loading…
Cancel
Save