This thread has been locked.

If you have a related question, please click the "Ask a related question" button in the top right corner. The newly created question will be automatically linked to this question.

TM4C123GH6PZ: BiSS-C Implementation Using GPIO/Timer

Part Number: TM4C123GH6PZ

Hello,

I'm looking to implement the BiSS-C protocol to interface with an absolute encoder. I have 2 pins of my micro connected to an RS-422 transceiver to produce the required differential output. I was thinking of using a timer in PWM mode to produce the clock signal on my MA line. I start producing the clock signal at 500 kHz every 1 second. Then using edge capture on that same pin,  generate a timer interrupt and read the state of the SLO line, reading in the bits this way. Based on the value of that bit, I run through a state machine. Once I have obtained my data and the timeout has passed, I stop generating the clock.

It seems as though I'm experiencing issues with my ISR. Adding debug statements in the ISR (simple count++) seems to completely change the data. Changing from positive edge capture to negative edge capture also completely changes the resulting data. I'm guessing I'm trying to accomplish too much in the ISR. 

Has anyone done anything similar? I've scavenged the web and these forums. There's quite a few old posts and an application note for a BiSS-C hardware interface. Most people suggest using the SPI interface. Unfortunately, this board is already designed and the SPI peripheral isn't connected to a transceiver to create differential signals. Are there any other better suggestions or examples? 

Regards,

CamK

  • Hello CamK

    What is the system clock frequency being used? Also did you try performing a time profile of the function execution to see how much time w.r.t 500KHz is being utilized in code execution.
  • The system clock is 80 MHz. I'm not following the second question of your post. Are you asking if I'm giving the timer ISR enough time to run?

    Regards,
    CamK
  • Hello CamK

    Yes, in a way. What I am asking is that how long does the ISR take to process compared to the time window for every sample.

    The other thing I wanted to ask is that is there some filter on the signal path that takes care of glitches introduced by overshoots and undershoots of the signals? Did you check if the signal edges have a clean monotonic ramp?
  • I will double-check at the office tomorrow and measure the time inside the ISR and provide you with scope prints for both the outgoing clock (on MA+/-) and incoming data from the encoder (on SLO+/-). The incoming data does look very clean with definitive edges and not much noise. The transceiver is the LTC2852. It's rated for 20 Mbps, so I don't believe it should cause any issues.

    The timer ISR is getting generated based on the edges of the outgoing clock using the TimerControlEvent() Tivaware function. The timer ISR is definitely getting called and then calling the state machine. The state machine reads the SLO line. I will post a code snippet tomorrow as well.

    Thanks,
    CamK
  • Hello Amit,

    After spending some time going over the code and doing what you had recommended, I calculated the time required by the ISR to run the state machine and read in the SLO line.

    I set the clock frequency to 500kHz and was reading the encoder every 1ms (start comm. every 1ms). Unfortunately, with only 2us between each cycle, some cycles required 1.6us-2.35us to complete. This means that the ISR occasionally didn't complete before the next clock cycle.

    Here is a sample of my old code:

    volatile uint32 encoderPosition[64];
    static uint32 encoderIndex = 0;
    
    static uint32 readPosition;
    static uint32 readError;
    static uint32 readWarning;
    static uint32 readCRC;
    static volatile uint32 BiSSCState;
    static uint32 clkPeriodCnt;
    static uint32 clkDutyCycleCnt;
    
    static void BiSSC_StateMachine(void);
    
    void BiSSC_StartComm(void)
    {
    	TimerLoadSet(WTIMER4_BASE, TIMER_B, clkPeriodCnt - 1);
    	TimerMatchSet(WTIMER4_BASE, TIMER_B, clkDutyCycleCnt);
    
    	BiSSCState = BISSC_RECV_ACK;
    	readPosition = 0;
    
    	TimerEnable(WTIMER4_BASE, TIMER_B);
    }
    
    void BiSSC_StopComm(void)
    {
    	TimerDisable(WTIMER4_BASE, TIMER_B);
    	BiSSCState = BISSC_NO_COMM;
    }
    
    uint32 goodReading = 0;
    uint32 badReading = 0;
    
    static void BiSSC_ProcessMessage(void)
    {
    	if(readError == 1) // Check to ensure no error (no error = 1)
    	{
    		encoderPosition[encoderIndex++] = (readPosition & 0x3FFFFFF);
    		goodReading++;
    	}
    	else
    	{
    		//encoderPosition[encoderIndex++]  = 99999;
    		badReading++;
    	}
    
    	if(encoderIndex >= 64)
    		encoderIndex = 0;
    }
    
    static void BiSSC_StateMachine(void)
    {
    	static uint32 indexCnt = 0;
    
    	switch(BiSSCState)
    	{
    		case BISSC_NO_COMM:
    			break;
    
    		case BISSC_RECV_ACK: // wait for ACK LOW signal
    			if(IO_StatusRead(enc_slo_line) == SLO_LOW)
    			{
    				indexCnt = 0;
    				BiSSCState = BISSC_WAIT_ACK;
    			}
    			else
    			{
    				BiSSCState = BISSC_RECV_ACK;
    			}
    			break;
    
    		case BISSC_WAIT_ACK: // wait for ACK signal to finish (20uS max), START signal HIGH
    			if(IO_StatusRead(enc_slo_line) == SLO_HIGH)
    			{
    				indexCnt = 0;
    				BiSSCState = BISSC_RECV_STRT;
    				bitCount++;
    			}
    			else
    			{
    				if (indexCnt < BISSC_ACK_WAIT_COUNTS)
    				{
    					indexCnt++;
    					BiSSCState = BISSC_WAIT_ACK;
    				}
    				else
    				{
    					BiSSCState = BISSC_RECV_ACK;
    				}
    			}
    			break;
    
    		case BISSC_RECV_STRT:
    			if(IO_StatusRead(enc_slo_line) == SLO_LOW) // wait for "0"
    			{
    				BiSSCState = BISSC_RECV_POS;
    				indexCnt = 0;
    			}
    			else
    			{
    				BiSSCState = BISSC_RECV_ACK;
    			}
    			break;
    
    		case BISSC_RECV_POS:
    			if(indexCnt < BISSC_MSG_LENGTH)
    			{
    				indexCnt++;
    				readPosition <<= 1;
    				readPosition |= IO_StatusRead(enc_slo_line);
    				BiSSCState = BISSC_RECV_POS;
    			}
    			else
    			{
    				indexCnt = 0;
    				BiSSCState = BISSC_RECV_ERROR;
    			}
    			break;
    
    		case BISSC_RECV_ERROR:
    			readError = IO_StatusRead(enc_slo_line);
    			BiSSCState = BISSC_RECV_WARNING;
    			break;
    
    		case BISSC_RECV_WARNING:
    			readWarning = IO_StatusRead(enc_slo_line);
    			readCRC = 0;
    			BiSSCState = BISSC_RECV_CRC;
    			break;
    
    		case BISSC_RECV_CRC:
    			if(indexCnt < 6)
    			{
    				readCRC <<= 1;
    				readCRC |= IO_StatusRead(enc_slo_line);
    				indexCnt++;
    				BiSSCState = BISSC_RECV_CRC;
    			}
    			else
    			{
    				indexCnt = 0;
    				BiSSCState = BISSC_TIMEOUT;
    			}
    			break;
    
    		case BISSC_TIMEOUT:
    			if(IO_StatusRead(enc_slo_line) == SLO_HIGH)
    			{
    				BiSSC_StopComm();
    				BiSSC_ProcessMessage();
    				BiSSCState = BISSC_RECV_ACK;
    			}
    			else
    			{
    				BiSSCState = BISSC_TIMEOUT;
    			}
    			break;
    
    	}
    }
    
    void BiSSC_MALineIntHandler(void)
    {
    	uint32 status = MAP_TimerIntStatus(WTIMER4_BASE, true);
    
    	MAP_TimerIntClear(WTIMER4_BASE, status);
    
    	if(status & TIMER_CAPB_EVENT)
    	{
    		BiSSC_StateMachine();
    	}
    }
    
    static void ClockMALine_Init(uint32 clkFreq)
    {
    	clkPeriodCnt = MAP_SysCtlClockGet() / clkFreq;
    
    	clkDutyCycleCnt = (clkPeriodCnt * 2) / 4;
    
    	SysCtlPeripheralEnable(SYSCTL_PERIPH_WTIMER4);
    	TimerConfigure(WTIMER4_BASE, TIMER_CFG_SPLIT_PAIR | TIMER_CFG_B_PWM);
    
    	GPIOPinConfigure(BISSC_MA);
    	GPIOPinTypeTimer(BISSC_MA_PORT, BISSC_MA_PIN);
    	TimerLoadSet(WTIMER4_BASE, TIMER_B, 0);
    	TimerMatchSet(WTIMER4_BASE, TIMER_B, 0);
    	TimerUpdateMode(WTIMER4_BASE, TIMER_B, TIMER_UP_LOAD_IMMEDIATE | TIMER_UP_MATCH_IMMEDIATE);
    
    
    	MAP_IntDisable(INT_WTIMER4B);
    	MAP_IntPrioritySet(INT_WTIMER4B, 0x00);
    	HWREG(WTIMER4_BASE + TIMER_O_TBMR) |= TIMER_TBMR_TBPWMIE;
    
    	TimerControlEvent(WTIMER4_BASE, TIMER_B, TIMER_EVENT_POS_EDGE);
    	TimerIntEnable(WTIMER4_BASE, TIMER_CAPB_EVENT);
    
    	HWREG(WTIMER4_BASE + TIMER_O_TBMR) |= TIMER_TBMR_TBPLO;
    
        IntEnable(INT_WTIMER4B);
    }
    
    void BiSSC_Init(void)
    {
    	ClockMALine_Init(BISSC_MA_CLK_FREQ);
    
    	readPosition = 0;
    	readError = 0;
    	readWarning = 0;
    	readCRC = 0;
    	BiSSCState = 0;
    }

    I rewrote my code and removed any processing until the actual message was entirely received. I send a predefined number of clock pulses instead of "just enough." This doesn't seem to have any negative impact as sending extra pulses during the encoder timeout period doesn't hurt. 

    volatile uint32 encoderData[64];
    volatile uint32 encoderPosition[64];
    static uint32 encoderIndex = 0;
    
    static uint32 readPosition;
    static uint32 readError;
    static uint32 readWarning;
    static uint32 readCRC;
    static uint32 clkPeriodCnt;
    static uint32 clkDutyCycleCnt;
    
    void BiSSC_StartComm(void)
    {
    	TimerLoadSet(WTIMER4_BASE, TIMER_B, clkPeriodCnt - 1);
    	TimerMatchSet(WTIMER4_BASE, TIMER_B, clkDutyCycleCnt);
    
    	readPosition = 0;
    
    	TimerEnable(WTIMER4_BASE, TIMER_B);
    }
    
    void BiSSC_StopComm(void)
    {
    	TimerDisable(WTIMER4_BASE, TIMER_B);
    }
    
    uint32 goodReading = 0;
    uint32 badReading = 0;
    
    static void BiSSC_ProcessMessage(void)
    {
    	uint32 dataIndex = 0;
    	uint32 posIndex = 0;
    	uint32 ACKCmplte = 0;
    
    	/* ACK starts after 2 clock pulses on MA */
    	dataIndex += 2;
    
    	if(encoderData[dataIndex] == SLO_LOW)
    	{
    		/* ACK lasts a max of 20 uS - find START*/
    		while (ACKCmplte == 0)
    		{
    			if(encoderData[dataIndex] == SLO_HIGH)
    			{
    				ACKCmplte = 1;
    			}
    			dataIndex++;
    		}
    
    		/* Next bit should be 0 */
    		if(encoderData[dataIndex] == SLO_LOW)
    		{
    			dataIndex++;
    
    			/* 26 bit position */
    			for (posIndex = 0; posIndex < BISSC_MSG_LENGTH; posIndex++)
    			{
    				readPosition <<= 1;
    				readPosition |= encoderData[dataIndex++];
    			}
    
    			/* Error bit */
    			readError = encoderData[dataIndex++];
    
    			/* Warning bit */
    			readWarning= encoderData[dataIndex++];
    
    			/* 6 bit CRC */
    			for (posIndex = 0; posIndex < 6; posIndex++)
    			{
    				readCRC <<= 1;
    				readCRC |= encoderData[dataIndex++];
    			}
    
    			if(readError == 1) // Check to ensure no error (no error = 1)
    			{
    
    				encoderPosition[encoderIndex++] = (readPosition & 0x3FFFFFF);
    				goodReading++;
    
    			}
    			else
    			{
    				//encoderPosition[encoderIndex++]  = 99999;
    				badReading++;
    			}
    
    			if(encoderIndex >= 64)
    				encoderIndex = 0;
    		}
    	}
    }
    
    static volatile uint32 clkCnt = 0;
    void BiSSC_MALineIntHandler(void)
    {
    	uint32 status = MAP_TimerIntStatus(WTIMER4_BASE, true);
    	MAP_TimerIntClear(WTIMER4_BASE, status);
    
    	if(status & TIMER_CAPB_EVENT)
    	{
    		asm("nop");
    		encoderData[clkCnt] = IO_StatusRead(enc_slo_line);
    
    		clkCnt++;
    		if (clkCnt >= 50 )
    		{
    			BiSSC_StopComm();
    			BiSSC_ProcessMessage();
    
    			clkCnt = 0;
    		}
    	}
    }
    
    static void ClockMALine_Init(uint32 clkFreq)
    {
    	clkPeriodCnt = MAP_SysCtlClockGet() / clkFreq;
    
    	clkDutyCycleCnt = (clkPeriodCnt * 2) / 4;
    
    	SysCtlPeripheralEnable(SYSCTL_PERIPH_WTIMER4);
    	TimerConfigure(WTIMER4_BASE, TIMER_CFG_SPLIT_PAIR | TIMER_CFG_B_PWM);
    
    	GPIOPinConfigure(BISSC_MA);
    	GPIOPinTypeTimer(BISSC_MA_PORT, BISSC_MA_PIN);
    	TimerLoadSet(WTIMER4_BASE, TIMER_B, 0);
    	TimerMatchSet(WTIMER4_BASE, TIMER_B, 0);
    	TimerUpdateMode(WTIMER4_BASE, TIMER_B, TIMER_UP_LOAD_IMMEDIATE | TIMER_UP_MATCH_IMMEDIATE);
    
    
    	MAP_IntDisable(INT_WTIMER4B);
    	MAP_IntPrioritySet(INT_WTIMER4B, 0x00);
    	HWREG(WTIMER4_BASE + TIMER_O_TBMR) |= TIMER_TBMR_TBPWMIE;
    
    	TimerControlEvent(WTIMER4_BASE, TIMER_B, TIMER_EVENT_POS_EDGE);
    	TimerIntEnable(WTIMER4_BASE, TIMER_CAPB_EVENT);
    
    	HWREG(WTIMER4_BASE + TIMER_O_TBMR) |= TIMER_TBMR_TBPLO;
    
        IntEnable(INT_WTIMER4B);
    }
    
    void BiSSC_Init(void)
    {
    	ClockMALine_Init(BISSC_MA_CLK_FREQ);
    
    	readPosition = 0;
    	readError = 0;
    	readWarning = 0;
    	readCRC = 0;
    }
    
    

    I consistently get the correct result. I could go as far as just set a flag once the message has been received and process the message outside of any ISR. If there are any other suggestions for communicating with BiSS-C over a timer pin and GPIO pin, share away.

    I have one remaining issue. When my timer PWM pin isn't generating a PWM signal, I'd like that pin to stay high. It doesn't seem to stick high or low consistently.This seems to cause me issues.

    I want the MA signal to the encoder to be set high when not generating a PWM. Right now, sometimes it is and sometimes it isn't. I'm guessing this is because it's floating. I set the legacy TIMER_TBMR_TBPLO bit in the TIMER_O_TBMR register to drive the CCP pin high when the timer reaches 0. This doesn't seem to have any effect. I can set the TIMER_CTL_TBPWML bit in the TIMER_O_CTL register but that will just invert my signal, still leaving me with a pin that is high when it is not in use sometimes but not always.

    Am I attempting to do this portion correctly? Is there a better way to ensure that the timer PWM pin is high when not generating a PWM signal? If I understand correctly, 100% duty cycle isn't possible.

    Regards,

    CamK

  • In quick answer to your (easier) last question, " Is there a better way to ensure that the timer PWM pin is high when not generating a PWM signal" I'd suggest that you employ a simple, 2 input "Or Gate" w/the 2nd "Or" input controlled by a GPIO. During (normal) PWM that GPIO would be set low - and when PWM completes - you'd toggle it high.

    To your (harder) question - wouldn't it prove useful/insightful to reduce your Timer Clock to 100KHz - and see "how or even if" this relaxed timing impacts your issue?
  • Thanks for the reply cb1,

    Unfortunately, I can't modify the board. I'm stuck with the current configuration, at least until the next board revision (though your way sounds like a true and tried solution rather than a workaround). I have 2 pins (UART6TX/RX) tied to an LTC2852 to produce a differential output. I would have thought there'd be a way to pull the timer PWM pin high in software. It's interesting to me that when I disable the timer and stop the output, the pin goes high or low but isn't consistent. If it were consistent, I could use the TIMER_CTL_TBPWML bit in the TIMER_O_CTL register.

    I wonder if this has something to do with my setting of TIMER_UP_MATCH_IMMEDIATE using the TimerUpdateMode() function. I will have to try this in the lab tomorrow.

    The encoder I'm communicating with has a minimum clocking frequency of 250kHz. So far so good with testing at 250kHz and 500kHz. I've given this interrupt the highest priority. I'll have to test to ensure it doesn't put too much load on the micro.

    Regards,
    CamK
  • There's not (much) modification involved in creating a postage stamp size (add on) pcb - which holds a 5 pin, SOT23 logic gate. Indeed it is tried/true - enables the precise control you seek.

    It would seem that if you persist enough you'll hit on (some) method to have the Timer output match your desire. The add-on method I've suggested is superior (firm & I believe) as it enables "migration" from ARM MCU to ARM MCU. (we employ MCUs from multiple vendors)

    Developing reusable, near universal assemblies and/or techniques - enables our firm to respond quickly & with great assurance of functionality - independent of the "MCU du jour!"

  • Hello CamK,

    Since a board modification is not possible, one solution would be to write the pin in GPIO mode and clear the AFSEL bit when stopping the PWM. Then when re-enabling set the AFSEL bit. Can you try this approach and check?
  • cb1,

    Thank you for the recommendation. When we respin the board for a new revision, this will definitely be something to add. Easy modification for pennies that will save us hassle.

    Amit,

    Your suggested solution did the trick. Before I start communication, I setup the pin as a GPIO and set the pin high. I then initialize the timer for PWM. When not generating a PWM signal, I clear the pin in the GPIO_O_AFSEL register.

    	HWREG(GPIO_PORTD_BASE+ GPIO_O_AFSEL) &= ~(GPIO_PIN_5);
    

    Whenever I want to start communicating with the encoder, I set the pin's bit in the GPIO_O_AFSEL register. 

    HWREG(GPIO_PORTD_BASE+ GPIO_O_AFSEL) |= GPIO_PIN_5;

    Works great. Thank you!