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 asare 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
