H-bridge controller firmware: i2c-pwm.asm

ian – Tue, 2006 – 07 – 04 21:36

This is the main source file for the PIC device in the I2C-based H-bridge controller with PWM.


; PIC16F84A H-Bridge controller
;
; Release 1.0, 27 June 2006
; Copyright (C) 2006 Ian Howson (ian@mouldy.org)
;
; Uses an I2C interface and PWM to provide variable speed and direction 
; control to two H-bridges. These are intended for Eugene Blanchard's H-bridge
; design (http://www.geocities.com/fet_h_bridge/index.html), but should work 
; with any H-bridge that can accept 5V logic-level inputs.
;
; This design includes braking functionality. If this is not needed, only the 
; A and B outputs for each output channel should be connected.
;
; Numbers shown in greater than/less than symbols such as  are the amount
; of time that the relevant function takes to execute, measured in 
; instructions (cpuclk/4).
;
; Maximum time between bus samples is <33> (address detect) + <33> (ISR)
; <66> instructions, and we want four bus samples per cycle (in case of mid-
; transmission START/STOP), so the absolute worst-case I2C frequency is 
; 9469Hz. In practice, you'll get away with more than that without any real 
; problems.
;
; The default PWM frequency is about 300Hz, but can be increased to about 8kHz
; without causing problems. Above this, you may encounter problems with the
; I2C bus.
;
; The truth table for the output is:
; Reverse Brake | A B C D
; --------------+--------
; 0       0     | P 0 P 0
; 0       1     | 0 0 0 0
; 1       0     | 0 P 0 P
; 1       1     | 0 0 0 0
; coast         | 0 0 1 1
; where P is the PWM signal, determined by the speed. Variable power braking is
; supported.
; 
; See http://www.mouldy.org/projects/I2C-PWM-H-Bridge-Controller for more 
; information.
;


	list      p=16F84A            ; list directive to define processor
	#include         ; processor specific variable definitions

	__CONFIG   _CP_OFF & _WDT_OFF & _PWRTE_ON & _HS_OSC

;***** VARIABLE DEFINITIONS
w_temp        EQU     0x0C        ; variable used for context saving 
status_temp   EQU     0x0D        ; variable used for context saving

twi	equ 0x15 ; last sampled value of twi port
twibits	equ 0x16 ; number of bits remaining to be sampled in byte
twibyte equ 0x17 ; destination for addr/data received on twi bus
twistrap equ 0x18 ; this device's i2c address. It's stored rotated one bit to the left so that it can be directly compared with the incoming byte.
twiold equ 0x19 ; previous sample of the twi port

; The PWM code must take as little time as possible to execute, or i2c 
; performance will suffer. To achieve this, we precompute the values to be 
; placed in the PWM port under various conditions. The 0/1 refers to the 
; channel number, and low/high refers to whether the PWM signal is low or high
; at that moment in time. The respective values for channels 0 and 1 are ORed 
; together to control both channels simultaneously.
;
; The pwm_1 values are in the high-order nibble so that they don't need to be rotated later.
;
; Each channel's three registers (ratio, low, high) must be contiguous and in that order.
;
pwm_0_ratio equ 0x20		  ; desired PWM ratio (channel 0)
pwm_0_low equ 0x21
pwm_0_high equ 0x22

pwm_1_ratio equ 0x23		  ; desired PWM ratio (channel 1)
pwm_1_low equ 0x24
pwm_1_high equ 0x25

pwmval equ 0x26		  ; current PWM value (shared between channels)
pwmshadow equ 0x27 ; temp register used to assemble the current PWM port status

; ***** CONSTANTS

PWMMAX EQU 0xf	; maximum PWM ratio that can be specified; increase for more accuracy but lower frequency

PWMPORT EQU	PORTB	; PWM output port
LEDBIT equ 4

TWIPORT equ PORTA
TWITRIS equ TRISA
TWISDA equ 2
TWISCL equ 3
TWIPORTMASK equ ((1 << TWISDA) | (1 << TWISCL))

TWI_ADDR_MASK equ 0xfe
TWI_RW_MASK equ 1

BITS_PER_BYTE equ 8

; errors on the i2c bus
TWI_ERR_NONE equ 0 ; no error
TWI_ERR_GOTSTART equ 1 ; got a START condition in the middle of a transfer
TWI_ERR_GOTSTOP equ 2 ; got a STOP condition in the middle of a transfer

TWISTRAPPORT equ PORTA
TWISTRAPMASK equ 0x3 ; pins 0 and 1 are address straps
TWISTRAPBASE equ b'01000000'

TWI_CMD_CHANNEL_BIT equ 6
TWI_CMD_DIRECTION_BIT equ 5
TWI_CMD_BRAKE_BIT equ 4

TWI_CMD_CHANNEL_MASK equ (1 << TWI_CMD_CHANNEL_BIT)
TWI_CMD_DIRECTION_MASK equ (1 << TWI_CMD_DIRECTION_BIT)
TWI_CMD_BRAKE_MASK equ (1 << TWI_CMD_BRAKE_BIT)
TWI_CMD_SPEED_MASK equ b'00001111'

; PWM output table
; outputs are ABCD
PWM_BRAKE equ b'0000'
PWM_COAST equ b'0011'
PWM_FORWARD equ b'1010'
PWM_REVERSE equ b'0101'

PIC16F84A_RAM_BASE equ 0x20

;**********************************************************************

	ORG     0x000             ; processor reset vector
	goto    main              ; go to beginning of program

;**********************************************************************

; **** library functions

; copy register from 'in' to 'out' <2>
copy macro in, out
	movf in, w
	movwf out
	endm

; ---

; move literal to file <2>
movlf macro l, f 
	movlw l
	movwf f
	endm

; ***** end library functions

	; spiport must be sampled at least twice while scl is high (sample 
	; rate > 2xI2C clock rate) in order to detect START and STOP 
	; transitions; this determines the worst-case performance with interrupts

	; Interrupt takes <33> instructions to execute, worst case. At 10MHz, this 
	; gives a maximum PWM frequency of 75kHz, assuming no time spent servicing
	; the i2c bus.

	ORG     0x004             ; interrupt vector location
	movwf   w_temp            ; save off current W register contents
	movf	STATUS,w          ; move status register into W register
	movwf	status_temp       ; save off contents of STATUS register

	bcf STATUS, RP0 ; make sure the right bank is selected

	; We do the increment here so that we're comparing values 1-15 instead of 
	; 0-14. This makes the compare logic simpler; subtracting n from n sets the
	; carry bit, which we don't want.
	incf pwmval, f

	; channel 0
	; if ratio > value, set the output, otherwise clear
	movf pwmval, w
	subwf pwm_0_ratio, w ; w = pwm_x_ratio - pwmval

	copy pwm_0_low, pwmshadow

	btfss STATUS, C
	goto interrupt_ch1_test
	copy pwm_0_high, pwmshadow ; <12>

interrupt_ch1_test:
	; channel 1
	movf pwmval, w
	subwf pwm_1_ratio, w ; w = pwm_x_ratio - pwmval

	btfsc STATUS, C ; <15>
	goto interrupt_ch1_high

interrupt_ch1_low:
	movf pwm_1_low, w
	iorwf pwmshadow, f
	goto interrupt_ch_done
	
interrupt_ch1_high:
	movf pwm_1_high, w
	iorwf pwmshadow, f
	
interrupt_ch_done:
	; <20> max at this point
	
	copy pwmshadow, PWMPORT

	; update pwmval; this is surprisingly fiddly
	movlw PWMMAX
	xorwf pwmval, w ; <23>
	btfsc STATUS, Z
	clrf pwmval

	; <26> worst case
interrupt_return:
	; clear interrupt bit
	bcf INTCON, T0IF
	
	movf    status_temp, w     ; retrieve copy of STATUS register
	movwf	STATUS            ; restore pre-isr STATUS register contents
	swapf   w_temp, f
	swapf   w_temp, w          ; restore pre-isr W register contents
	retfie                    ; return from interrupt

;**********************************************************************

main:

	; RAM-scrubbing code from the datasheet, tweaked to be less horrible
	movlw PIC16F84A_RAM_BASE
	movwf FSR
scrub:
	clrf INDF
	incf FSR, f
	btfss FSR, 4 ; all done?
	goto scrub

	clrf PORTA
	clrf PORTB
	
	; select bank 1
	bsf STATUS, RP0

	clrf TRISA ; set porta as outputs
	bsf TWITRIS, TWISDA ; TWI pins are inputs (emulating open-drain outputs)
	bsf TWITRIS, TWISCL
	clrf TRISB
	
	; configure timer
	movlw 0	; 1:2 prescale rate - tweak this and PWMSTEP to adjust the PWM rate
	movwf OPTION_REG
	
	; select bank 0
	bcf STATUS, RP0

	movlw b'10100000'
	movwf INTCON ; enable timer interrupt, disable others

	movlw 0
	movwf TMR0

	; load the TWI address from pin straps
	movf TWISTRAPPORT, w
	andlw TWISTRAPMASK
	iorlw TWISTRAPBASE
	movwf twistrap
	bcf STATUS, C
	rlf twistrap, f ; rotate left so we can just compare against the read byte

	; the output registers are cleared, but the ports are set as input; to set the bus to 0, we change the pin to be an output
	; more info at http://www.brouhaha.com/~eric/pic/open_drain.html
	bcf TWIPORT, TWISDA 
	bcf TWIPORT, TWISCL

	goto twiloop

; ***** start I2C interface code

; the TWI port needs to be sampled; this avoids race conditions where the port
; state changes while we're processing on it
; <2>
copytwi macro
	copy TWIPORT, twi
	endm

; set TWISDA to be low <4>
sda_low macro
	bsf STATUS, RP0
	bcf TWITRIS, TWISDA
	bcf STATUS, RP0
	bcf TWIPORT, TWISDA ; FIXME: shouldn't need this
	endm

; set TWISDA to be high <3>
sda_high macro
	bsf STATUS, RP0
	bsf TWITRIS, TWISDA
	bcf STATUS, RP0
	endm

; Send an ACK bit.
; Returns an error code in W
; <7> until first bus sample
; <16> from bus sample to exit, worst-case
twi_send_ack:
	sda_low ; send the ACK 
	call twi_wait_scl
	call twi_wait_scl_endbit ; wait for clock #9 to pass
	btfss STATUS, Z ; don't ACK if there was an error
	return

	sda_high
	return ; pass on W from the endbit call

; wait for the clock line to be asserted; twi will contain the latest bus sample
twi_wait_scl:
	copytwi
	
	btfss twi, TWISCL
	goto twi_wait_scl
	return

; ---

; Wait for TWISCL to be deasserted; twi will contain the latest bus sample. 
; Returns an error code in W in case we get a START or STOP event.
; <5> from sample to return (normal case)
; <13> from sample to return worst-case (STOP condition)
twi_wait_scl_endbit:
	copy twi, twiold

twi_wait_scl_endbit_loop:
	copytwi
	
	btfss twi, TWISCL
	retlw TWI_ERR_NONE

	; clock still high, make sure data hasn't changed state
	movf twiold, w
	xorwf twi, w
	andlw TWIPORTMASK ; don't compare bits that aren't related to I2C but happen to be on the same port
	btfsc STATUS, Z
	goto twi_wait_scl_endbit_loop ; bus unchanged

	; got a START or a STOP; figure out what it was
	btfss twi, TWISDA
	retlw TWI_ERR_GOTSTART ; downward transition: START condition
	retlw TWI_ERR_GOTSTOP ; upward transition: STOP condition
	
; ---

; Clock in a byte from the TWI bus, but don't ACK it. Returns an error code in W
; <16> between bus samples
; <4> before first sample
; <23> after last sample (error case)
; <14> after last sample (normal case)
twi_get_byte:
	movlf BITS_PER_BYTE, twibits

twi_get_bit:
	call twi_wait_scl

	; set C to TWISDA
 	movf twi, w
	andlw 1 << TWISDA
	addlw 0xff

	rlf twibyte, f

	call twi_wait_scl_endbit ; <7> from last sample

	xorlw TWI_ERR_NONE
	btfss STATUS, Z
	return ; pass on W value

	decfsz twibits, f
	goto twi_get_bit

	retlw TWI_ERR_NONE

; ---

; wait for start condition
; <7> from sample to exit (normal case)
twi_wait_for_start: 
	; waiting for scl high, sda transition low
	call twi_wait_scl

	btfss twi, TWISDA
	goto twi_wait_for_start

twi_wait_for_start_sdalow:
	copytwi

	btfss twi, TWISCL
	goto twi_wait_for_start

	btfsc twi, TWISDA
	goto twi_wait_for_start_sdalow

	call twi_wait_scl_endbit ; wait for SCL to go low again

	return

; ---

; if return val != 0, goto error handler (<3> normal case, <4> error case)
twi_check_error macro
	xorlw TWI_ERR_NONE
	btfss STATUS, Z
	goto twi_error
	endm

; ---

twi_error: ; handle the TWI error code in W
	movwf twibyte ; save the error code

	; TODO: there's probably a nicer way to do a jump table like this

	movf twibyte, w
	xorlw TWI_ERR_GOTSTART
	btfsc STATUS, Z
	goto twi_error_restart

	movf twibyte, w
	xorlw TWI_ERR_GOTSTOP
	btfsc STATUS, Z
	goto twiloop

	; something else happened; go back to the start
	goto twiloop
	
twi_error_restart: ; handle a mid-transfer START
	call twi_wait_scl_endbit ; wait for the START condition to end
	twi_check_error
	goto twi_got_start

; ---

; twibyte should contain a command. If the command applies to channel 1, swap the nibbles in INDF
swap_indf_ch1 macro
	btfsc twibyte, TWI_CMD_CHANNEL_BIT
	swapf INDF, f
	endm

; ---

; Executes the command stored in twibyte
; Uses 'twi' as a temporary register.
; We do as much of the processing here as possible so that we can spend less time in the interrupt handler. This improves overall I2C performance.
; Given the new input command, we need to update the PWM channel registers (pwm__{low|high})
; 
twi_command:
	; command format: 8 bits
	; ccdbssss
	; cc = channel number. Only the LSB is used on this device.
	; d = direction. 0 forward, 1 backwards
	; b = brake. 0 no brake, 1 brake
	; ssss = speed. higher values -> higher speed

	; TODO: tidy up this function. It's needlessly long. Having it like this is good for, uh, performance. And it's easier to write.
	
	; start critical section; disable interrupts
	bcf INTCON, GIE

	; make INDF contain the ratio for the selected channel
	movlf pwm_0_ratio, FSR
	btfsc twibyte, TWI_CMD_CHANNEL_BIT
	movlf pwm_1_ratio, FSR

	movf twibyte, w
	andlw TWI_CMD_SPEED_MASK
	movwf INDF

	incf FSR, f ; INDF is now pwm_x_low

	movlf PWM_COAST, INDF
	swap_indf_ch1

	incf FSR, f ; INDF is now pwm_x_high

	btfsc twibyte, TWI_CMD_BRAKE_BIT
	goto twi_command_brake

	btfsc twibyte, TWI_CMD_DIRECTION_BIT
	goto twi_command_reverse

twi_command_forward:
	movlf PWM_FORWARD, INDF
	goto twi_command_end

twi_command_reverse:
	movlf PWM_REVERSE, INDF
	goto twi_command_end

twi_command_brake:
	movlf PWM_BRAKE, INDF

twi_command_end:
	swap_indf_ch1 ; all three code paths require the swap, so do it here to save code space

	; end critical section; reenable interrupts
	bsf INTCON, GIE

	return

; ***** end I2C interface code

twiloop:
	; FIXME: error checks aren't free! make sure the timing analysis is right

	; geez, this would be so much easier in VHDL

	call twi_wait_for_start ; <7> from sample to exit
	bcf PORTA, LEDBIT ; transfer in progress

twi_got_start:
	call twi_get_byte ; max <14> between samples, <14> after
	twi_check_error ; <3> normal

	movf twibyte, w
	andlw TWI_ADDR_MASK

	; if the byte is our address, continue reception
	xorwf twistrap, w
	btfss STATUS, Z
	goto twiloop ; no match, wait for START

	; got the right address, check RW bit
	; NOTE: we don't handle reads. If it's a read (rw = 1), we just don't respond.
	movf twibyte, w

	andlw TWI_RW_MASK
	btfss STATUS, Z
	goto twiloop ; abort, wait for the next START condition

twi_addr_match:
	call twi_send_ack ; <33> between samples
	twi_check_error

	call twi_get_byte ; get the data byte
	twi_check_error

	call twi_send_ack
	twi_check_error

	call twi_command

	bsf PORTA, LEDBIT ; debug: signal transfer successful

	; we *should* wait for a STOP here, but it's easier to just wait for the next START or repeated START

	goto twiloop

	end


Post new comment

Please solve the math problem above and type in the result. e.g. for 1+1, type 2
The content of this field is kept private and will not be shown publicly.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.
More information about formatting options