Browse Source

Add bitbanged I2C example.

master
flabbergast 1 year ago
parent
commit
02e7034d9e
  1. 19
      06-i2c-bitbang/ass
  2. 185
      06-i2c-bitbang/bitbang.asm
  3. 55
      06-i2c-bitbang/core.asm
  4. 183
      06-i2c-bitbang/i2c.asm
  5. 19
      06-i2c-bitbang/timers.asm
  6. 84
      06-i2c-bitbang/uart_lp.asm
  7. 199
      06-i2c-bitbang/write.asm
  8. 13
      README.md
  9. 2
      common/regconvention.asm

19
06-i2c-bitbang/ass

@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
#!/bin/bash
set -euo pipefail
ARG1=${1:-}
TARGET=bitbang
function clean { rm -f $TARGET.hex $TARGET.p $TARGET.lst ; }
clean
if test "$ARG1" = "clean" ; then
exit 0
fi
asl -r 2 -L $TARGET.asm && p2hex $TARGET.p -r 0x0000-0xffff
if test $? -eq 0 -a "$ARG1" == "flash" ; then
mspdebug rf2500 "prog $TARGET.hex"
fi

185
06-i2c-bitbang/bitbang.asm

@ -0,0 +1,185 @@ @@ -0,0 +1,185 @@
;
; for MSP430G2553
;
; Based off of Matthias Koch's mecrisp code
;
;------------------------------------------------------------------------------
; Base Definitions
;------------------------------------------------------------------------------
cpu msp430
include "../common/registers.asm"
include "../common/regconvention.asm"
;------------------------------------------------------------------------------
; Memory map
;------------------------------------------------------------------------------
RamBegin equ 200h ; Start of RAM
RamEnd equ 400h ; End of RAM, 512 Bytes
FlashBegin equ 0C000h ; Start of Flash, 16 kb, Flash end always is $FFFF.
ProgBegin equ 0F000h ; Start of code
org ProgBegin ; ... let the assembler know where to place the code
;------------------------------------------------------------------------------
; Load up functions
;------------------------------------------------------------------------------
include "core.asm"
include "timers.asm"
include "uart_lp.asm"
include "write.asm"
include "i2c.asm"
; include "../common/analog.asm"
; include "../common/flash.asm"
;------------------------------------------------------------------------------
; Static data
;------------------------------------------------------------------------------
s_hello: static_string "Hello! %d %%\n\r"
;------------------------------------------------------------------------------
Reset: ; Main entry point
;------------------------------------------------------------------------------
mov #returnstack_begin, sp ; Initialise return stack pointer
mov.w #WDTPW+WDTHOLD, &WDTCTL ; Watchdog off
mov #datastack_begin, ds ; Initialise return stack pointer
; Initialize hardware
; 8 MHz ! Take care: If you change clock, you have to change Flash clock divider, too !
; Current divider 19+1 --> 400kHz @ 8 MHz. Has to be in range 257 kHz - 476 kHz.
mov.b &CALBC1_8MHZ, &BCSCTL1 ; Set DCO
mov.b &CALDCO_8MHZ, &DCOCTL ; to 8 MHz.
clr.b &IE1 ; Clear interrupt flags of oscillator fault, NMR and flash violation.
mov.w #FWKEY, &FCTL1 ; Lock flash against writing
mov.w #FWKEY|FSSEL_1|19, &FCTL2 ; MCLK/20 for Flash Timing Generator
;------------------------------------------------------------------------------
; init subsystems
;------------------------------------------------------------------------------
call #uart_init
;------------------------------------------------------------------------------
eint
;------------------------------------------------------------------------------
; this is where we dance
;------------------------------------------------------------------------------
uart_puts "Welcome!\n\r"
; i2c test code
; assume we have a standard I2C EEPROM on P2.4(scl) and P2.5(sda)
; address #50h
call #i2c_init
i2c_delay
mov.b #(50h << 1), a0 ; address (write)
call #i2c_start ; start condition on I2C bus
call #i2c_write_byte ; send address
jc + ; got NACK (no response), finish
mov.b #01, a0 ; byte we're sending (eep address)
call #i2c_write_byte
; mov.b #0BEh, a0 ; byte we're sending (data for eep)
; call #i2c_write_byte
; mov.b #0EFh, a0 ; byte we're sending (data for eep)
; call #i2c_write_byte
; call #i2c_stop ; full stop after writing to eep
i2c_delay
mov.b #(50h << 1)|1, a0 ; address (read)
call #i2c_start ; re-start
call #i2c_write_byte ; send address
jc + ; got NACK (no response), finish
mov.b #0, a0 ; send ACK after reading a byte
call #i2c_read_byte ; read into a0
mov.b #1, a0 ; send NACK after reading last byte
call #i2c_read_byte
+ call #i2c_stop
; the second read byte is now in a0
pushda a0
mov #s_hello, a0
call #uart_write_str ; print the byte
; end of i2c test code
bis.b #01000001b, &P1DIR ; make P1.0 and P1.6 output
- bic.b #00000001b, &P1OUT ; clear P1.0 (red off)
bis.b #01000000b, &P1OUT ; set P1.6 (green on)
call #uart_recv ; wait for a byte, comes in a0
call #uart_emit ; ... and send it back
bis.b #00000001b, &P1OUT ; set P1.0 (red on)
bic.b #01000000b, &P1OUT ; clear P1.6 (green off)
call #uart_recv ; wait for a byte, comes in a0
call #uart_emit ; ... and send it back
pushda #32769
mov #s_hello, a0
call #uart_write_str
jmp - ; loop
;------------------------------------------------------------------------------
;------------------------------------------------------------------------------
coldreset:
clr &WDTCTL ; Reset with cold start
;------------------------------------------------------------------------------
; Interrupt vectors
;------------------------------------------------------------------------------
;------------------------------------------------------------------------------
null_handler: ; Catches unwired interrupts
reti
;------------------------------------------------------------------------------
; choose the isr functions
irq_vector_port1 equ null_handler
irq_vector_port2 equ null_handler
irq_vector_adc equ null_handler
irq_vector_tx equ wake_tx ; used for uart wait in lpm
irq_vector_rx equ wake_rx ; used for uart wait in lpm
irq_vector_timera1 equ null_handler
irq_vector_timera0 equ null_handler
irq_vector_watchdog equ null_handler
irq_vector_comp equ null_handler
irq_vector_timerb1 equ null_handler
irq_vector_timerb0 equ wake_timer ; used for timed sleep
;------------------------------------------------------------------------------
org 0FFE0h ; Interrupt table
.word null_handler ; 01: 0FFE0 Unused
.word null_handler ; 02: 0FFE2 Unused
.word irq_vector_port1 ; 03: 0FFE4 Port 1
.word irq_vector_port2 ; 04: 0FFE6 Port 2
.word null_handler ; 05: 0FFE8 Unused
.word irq_vector_adc ; 06: 0FFEA ADC10
.word irq_vector_tx ; 07: 0FFEC USCI Transmit - Terminal
.word irq_vector_rx ; 08: 0FFEE USCI Receive - interrupts
.word irq_vector_timera1 ; 09: 0FFF0 Timer A
.word irq_vector_timera0 ; 10: 0FFF2 Timer A
.word irq_vector_watchdog ; 11: 0FFF4 Watchdog
.word irq_vector_comp ; 12: 0FFF6 Comparator
.word irq_vector_timerb1 ; 13: 0FFF8 Timer B
.word irq_vector_timerb0 ; 14: 0FFFA Timer B
.word null_handler ; 15: 0FFFC NMI. Unused.
.word Reset ; 16: 0FFFE Main entry point
end

55
06-i2c-bitbang/core.asm

@ -0,0 +1,55 @@ @@ -0,0 +1,55 @@
;
; based off of Matthias Koch's mecrisp code (GPL3)
;
; (assumes 'ds' register alias)
; ----------------------------------------------------------
; RAM allocations
; ----------------------------------------------------------
rampointer set RamBegin ; Beginning of Ram
; Creating labels for variables and buffers in RAM
ramallot macro Name, Size
Name equ rampointer
rampointer set rampointer + Size
endm
; allocate datastack (ideally with uncritical data around it...)
datastack_length equ 64 ; Size of the datastack, has to be even
ramallot datastack_end, datastack_length
datastack_begin equ datastack_end + datastack_length
; allocate return stack
returnstack_length equ 64 ; Size of the return stack, has to be even
ramallot returnstack_end, returnstack_length
returnstack_begin equ returnstack_end + returnstack_length
; ----------------------------------------------------------
; Data stack
; ----------------------------------------------------------
popda macro dst ; Fetch from datastack - 2 Bytes
mov @ds+, dst ; Increment stackpointer by two after fetching content.
endm
pushda macro src ; Put something on datastack - 8 Bytes
decd ds ; Adjust stack pointer
mov src, @ds ; and put value in.
endm
; ----------------------------------------------------------
; Macros
; ----------------------------------------------------------
; Sized strings
static_string macro Text ; Text should not have parentheses
.word STRLEN(Text) ; word b/c alignment - asl freaks when .byte and escape in Text
.byte Text
align 2
endm

183
06-i2c-bitbang/i2c.asm

@ -0,0 +1,183 @@ @@ -0,0 +1,183 @@
;
; Software I2C driver
;
; based on https://calcium3000.wordpress.com/2016/08/19/i2c-bit-banging-tutorial-part-i/
;
; the driver assumes that there are pull-up resistors on both SDA and SCL
; (as clearing a line is done by making it an input)
; clock stretching is allowed; but possible to save some bytes (~50?) when disabled
; (you'll need to comment out *_stretch parts below)
; SCL:P2.4 SDA:P2.5
SCL equ 00010000b
SDA equ 00100000b
I2C_PSEL equ P2SEL
I2C_P2SEL equ P2SEL2
I2C_INPORT equ P2IN
I2C_PDIR equ P2DIR
I2C_POUT equ P2OUT
; "half i2c cycle" (configure i2c bus speed here)
; for 400kHz (fast i2c) on 8MHz MCU clock it should be 10 cycles
; so 100kHz (normal speed) should be 40
; use the nop instead of a loop to really get 10 cycles
i2c_delay_loop: ; call is 5 cycles
mov #8, t1 ; 2 cycles
- dec t1 ; 2 cycles
jnz - ; 2 cycles
; nop ; 1 cycle
; nop ; 1 cycle
ret ; 3 cycles
;------------------------------------------------------------------------------
; Sample usage
;------------------------------------------------------------------------------
; i2c test code
; assume we have a standard I2C EEPROM on P2.4(scl) and P2.5(sda)
; address #50h
; call #i2c_init
; i2c_delay
; mov.b #(50h << 1), a0 ; address (write)
; call #i2c_start ; start condition on I2C bus
; call #i2c_write_byte ; send address
; jc + ; got NACK (no response), finish
; mov.b #01, a0 ; byte we're sending (eep address)
; call #i2c_write_byte
; i2c_delay
; mov.b #(50h << 1)|1, a0 ; address (read)
; call #i2c_start ; re-start
; call #i2c_write_byte ; send address
; jc + ; got NACK (no response), finish
; mov.b #0, a0 ; send ACK after reading a byte
; call #i2c_read_byte ; read into a0
; mov.b #1, a0 ; send NACK after reading last byte
; call #i2c_read_byte
; + call #i2c_stop
; ; the second read byte is now in a0
;------------------------------------------------------------------------------
; Implementation
;------------------------------------------------------------------------------
; registers:
; t0 used for bit loops
; t1 used for delay loop
; a0 used for data
; a1 used for n/ack
scl_high macro
bic.b #SCL, &I2C_PDIR
endm
scl_low macro ; needs to preserve carry
bis.b #SCL, &I2C_PDIR
endm
scl_high_stretch:
scl_high ; needs to make SCL input
- bit.b #SCL, &I2C_INPORT
jz - ; wait for stretching to finish
ret
sda_high macro
bic.b #SDA, &I2C_PDIR
endm
sda_low macro
bis.b #SDA, &I2C_PDIR
endm
i2c_delay macro
call #i2c_delay_loop
endm
i2c_init:
bic.b #SDA|SCL, &I2C_PSEL
bic.b #SDA|SCL, &I2C_P2SEL
bic.b #SDA|SCL, &I2C_POUT
scl_high
sda_high
ret
i2c_start:
scl_high
sda_high
i2c_delay
sda_low
i2c_delay
scl_low
i2c_delay
ret
i2c_stop:
sda_low
i2c_delay
scl_high
i2c_delay
sda_high
i2c_delay
ret
; bit in carry
i2c_write_bit:
jc +
sda_low
jmp ++
+ sda_high
+ i2c_delay
scl_high
i2c_delay
scl_low
ret
; rotates I2C bit into LSB of a0
i2c_read_bit:
sda_high
i2c_delay
scl_high
i2c_delay
bit #SDA, &I2C_INPORT
rlc.b a0 ; (bit7 will end up in carry)
scl_low ; (this needs to preserve carry)
ret ; (b/c want to keep carry for i2c_read_byte)
; rotates I2C bit into LSB of a0 (stretching allowed)
i2c_read_bit_stretch:
sda_high
i2c_delay
call #scl_high_stretch
i2c_delay
bit #SDA, &I2C_INPORT
rlc.b a0
scl_low
ret
; a0: byte to send
; returns NACK in carry (C=NACK)
i2c_write_byte:
mov #8, t0
- rlc.b a0 ; shift MSbit to carry
call #i2c_write_bit
dec t0
jnz -
; call #i2c_read_bit
call #i2c_read_bit_stretch
rrc.b a0 ; move NACK to carry
ret
; a0: LSbit is NACK (1=NACK)
; returns byte in a0
i2c_read_byte:
mov #8, t0
- dec t0
jz +
call #i2c_read_bit
jmp -
;+ call #i2c_read_bit ; NACK is now in carry
+ call #i2c_read_bit_stretch ; NACK is now in carry
call #i2c_write_bit
ret

19
06-i2c-bitbang/timers.asm

@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
;
; timers
;
; assumes irq_vector_timerb0 is "wake_timer"
delayticks_lp: ; a0:ticks (on a 32768 Hz crystal)
push sr ; save GIE flag
mov.w #CCIE, &TBCCTL0 ; enable CC interrupt
clr &TBR ; clear the counter
mov.w a0, &TBCCR0 ; set the required delay
mov.w #TASSEL_1|MC_1, &TBCTL ; ACLK, up-mode
bis #LPM3|GIE, sr ; sleep in LPM3
clr &TBCTL ; stop timer
reti ; reti also restores SR from stack
;------------------------------------------------------------------------------
wake_timer: ; waking up from sleep through an interrupt
bic #LPM4, 0(sp) ; clears all lpm flags
reti

84
06-i2c-bitbang/uart_lp.asm

@ -0,0 +1,84 @@ @@ -0,0 +1,84 @@
;
; uart_lp: basic uart using USCI_A0 on P1.1/P1.2 with LPM wait
;
; based off of Matthias Koch's mecrisp code
;
; assumes "core.asm"
; (note that MSP430G2 FET can only do 9600 baud)
uart_init: ; Initialize 9600 baud on 8MHz clock
; Init pins
mov.b #06h, &P1SEL ; use P1.1/P1.2 for USCI_A0
mov.b #06h, &P1SEL2 ; use P1.1/P1.2 for USCI_A0
bis.b #UCSSEL_2,&UCA0CTL1 ; SMCLK as clock source
; UCBRx and UCBRSx from table 15-4 (p424) in SLAU144J
mov.b #65,&UCA0BR0 ; 8MHz 9600 baud --> 833 cycles/bit
mov.b #3,&UCA0BR1 ; 8MHz 9600 baud
mov.b #UCBRS_2,&UCA0MCTL ; modulation UCBRSx = 2
; mov.b #69,&UCA0BR0 ; 8MHz 115200 baud --> 69 cycles/bit
; mov.b #0,&UCA0BR1 ; 8MHz 115200 baud
; mov.b #UCBRS_4,&UCA0MCTL ; modulation UCBRSx = 4
bic.b #UCSWRST,&UCA0CTL1 ; **initialize USCI state machine**
ret
uart_emit: ; output the character in a0
mov #UCA0TXIFG, t0
call #_uart_sleeproutine ; sleep until available
mov.b a0, &UCA0TXBUF
ret
jq_uart_emit: ; branch to addr if ready to emit ( addr -> )
bit.b #UCA0TXIFG, &IFG2
jz +
mov a0, @sp ; change return address
+ ret
uart_recv: ; return incoming byte ( -> byte )
mov #UCA0RXIFG, t0
call #_uart_sleeproutine ; sleep until available
mov.b &UCA0RXBUF, a0
ret
jq_uart_recv: ; branch to add if incoming byte ( addr -> )
bit.b #UCA0RXIFG, &IFG2
jz +
mov a0, @sp ; change return address
+ ret
;------------------------------------------------------------------------------
; internals
;------------------------------------------------------------------------------
_uart_sleeproutine: ; sleep (or loop) until tx||rx (selected in t0) happens
push sr
dint
nop
bis.b t0, &IE2 ; enable interrupt source for tx||rx
- bit.b t0, &IFG2 ; check if available
jnz +
bit #GIE, @sp ; have interrupts been enabled before?
jnc - ; if not, simply poll for a character
bis #LPM3|GIE, sr ; if yes, sleep and wait for interrupt
_uart_here_we_dream:
jmp -
+ bic.b t0, &IE2 ; disable interrupt source for tx||rx
nop
reti
;------------------------------------------------------------------------------
; irq
;------------------------------------------------------------------------------
wake_tx:
wake_rx:
cmp #_uart_here_we_dream, 2(sp)
jne +
bic #LPM3|GIE, 0(sp)
+ reti

199
06-i2c-bitbang/write.asm

@ -0,0 +1,199 @@ @@ -0,0 +1,199 @@
;
; write: basic string writing and manipulation
;
; assumes "core.asm" (data stack and macros)
; assumes uart_emit symbol
; sized string: w_size=n b_c1 b_c2 ... b_cn
; print a single hexadecimal digit
; expects:
; - a0: value to print (0-15)
; - a1: "emit" routine to use
write_custom_hexdigit:
add.b #'0', a0
cmp #':', a0 ; ':' = '9'+1
jl +
add.b #39, a0 ; get to lowercase alpha
+ mov a1, pc ; jump to "emit" (call/ret shortcut)
; print a byte, hexadecimal, unsigned
; expects:
; - a0: value to print
; - a1: "emit" routine to call
; - a2: if non zero, always print two digits
uart_write_byte_hex:
mov #uart_emit, a1
write_custom_byte_hex:
push s3
mov.b a0, s3
rrc.b a0
rrc.b a0
rrc.b a0
rrc.b a0
and #0Fh, a0
jnz +
cmp #0, a2
jz ++
+ call #write_custom_hexdigit
+ mov.b s3, a0
pop s3
and #0Fh, a0
jmp write_custom_hexdigit ; (call/ret shortcut)
; print a word, hexadecimal, unsigned, always four digits
; expects:
; - a0: value to print
; - a1: "emit" routine to call
; clobbers: a2
uart_write_word_hex_p:
mov.w #uart_emit, a1
write_custom_word_hex_p:
push.b a0 ; save the lower byte
swpb a0
mov #1, a2 ; want leading zeroes
call #write_custom_byte_hex ; only prints LSB of a0
pop a0
jmp write_custom_byte_hex
; print a word, hexadecimal, unsigned, not padded
; expects:
; - a0: value to print
; - a1: "emit" routine to call
; clobbers: a2
uart_write_word_hex:
mov.w #uart_emit, a1
write_custom_word_hex:
push.b a0 ; save the lower byte
swpb a0
mov #0, a2 ; assume we won't want leading zeroes
and #0FFh, a0 ; restrict and test
jz + ; do not print zeroes
call #write_custom_byte_hex
mov #1, a2 ; want leading zeroes (as we printed the upper byte)
+ pop a0
jmp write_custom_byte_hex
; print a word, decimal, unsigned
; expects:
; - a0: value to print
; - a1: "emit" routine to call
; clobbers: t0
uart_write_word_dec:
mov #uart_emit, a1
write_custom_word_dec:
push a1 ; save "emit"
call #to_bcd ; a1|a0 is now BCD
mov a1, t0
pop a1 ; restore "emit"
and #0Fh, t0
jz + ; skip leading 0
push a0
mov t0, a0
call #write_custom_hexdigit ; print fifth digit
pop a0
jmp write_custom_word_hex_p
+ jmp write_custom_word_hex
; print a sized string
; expects:
; - a0: sized string address
; - data stack: data for variables printing (%..)
; - if called as write_custom: a1: address for "emit" routine
; clobbers: a0, potentially (flags): a2, t0, t1
uart_write_str:
mov #uart_emit, a1
write_custom_str:
push s0
push s1
mov @a0+, s1 ; set up loop: s0:current, s1:end
add a0, s1 ; s1 = a0.orig + @a0.orig + 2
mov a0, s0 ; a0 = a0.orig + 2
- mov.b @s0+, a0
cmp #'%', a0 ; check for "print value" char
jne .emit
; logic to deal with % flags
mov.b @s0+, a0
cmp #'B', a0 ; byte in hex
jne +
popda a0 ; get the value to be printed
mov #1, a2 ; want leading zeroes
call #write_custom_byte_hex
jmp -
+ cmp #'W', a0 ; word (16bit) in hex, padded 4 digits
jne +
popda a0 ; get the value to be printed
call #write_custom_word_hex_p
jmp -
+ cmp #'d', a0 ; unsigned decimal (16bit)
jne +
popda a0 ; get the value to be printed
call #write_custom_word_dec
jmp -
+ ; another flag comes here ...
.emit
call a1 ; "emit"
cmp s1, s0 ; repeat until s0 reaches s1
jlo -
pop s1
pop s0
ret
; convert 16 bit unsigned to BCD (binary coded decimal)
; from slaa024 (chapter5, BINDEC)
; expects:
; - a0: number to convert
; returns:
; - a1|a0 BCD
; clobbers:
; - t0, t1
to_bcd:
mov #16, t0 ; loop counter
clr a1 ; 0, result MSD
clr t1 ; 0, result LSD
- rla a0 ; MSbit to carry
dadd t1, t1 ; 2*result+carry to LSD
dadd a1, a1 ; to MSD
dec t0 ; loop
jnz -
mov t1, a0 ; fix return value register
ret
; expected usage: uart_puts "text\n"
; will send "text\n" to uart
uart_puts macro Text
call #_write_puts_pc
static_string Text
endm
;------------------------------------------------------------------------------
; internals
;------------------------------------------------------------------------------
_write_puts_pc: ; sized string on stack (as a return address)
mov @sp, a0
add @a0, @sp ; fix the return address
incd @sp
bit #1, @sp ; add 1 if odd
jz +
inc @sp
+ jmp uart_write_str

13
README.md

@ -1,4 +1,17 @@ @@ -1,4 +1,17 @@
## register conventions
See `common/regconvention.asm`.
I renamed the registers and am pretty much using only the aliases, according to the "convention" specified in that file.
## function calling conventions
I am mixing two approaches: I try to make function parameters to be passed in registers (`a0`,`a1`,...) to try to avoid memory read/writes. However some functions do use the data stack that's set up in `core.asm`. You need to look at the comments around the function definitions to find out which one is being used. (Starts at 04-.)
[mas]: http://john.ccac.rwth-aachen.de:8000/as/

2
common/regconvention.asm

@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
; Register conventions
;------------------------------------------------------------------------------
; For passing parameters to functions
; For passing parameters to functions (caller responsible for preserving)
a0 reg r15
a1 reg r14
a2 reg r13

Loading…
Cancel
Save