Skip to content
Snippets Groups Projects

MK Stepper Embedded Doc

My goal is to

  • verify that I can communicate over both UART ports to the board
  • now, wrap gpio and uart in tiny packages
  • verify that I can speak SPI to the TMC2660, maybe the AS5147D.
  • then, wrap spi into a package
  • fiddle through tmc2660 config and send some steps
  • send a key:value to the board that is a 'step command' - a # of steps to take over a timespan, and setup a queue of these

Ring Testing the ATSAMD51

OK, first I setup the systick timer to fire every 500 cycles. This is handy - I use the interrupt to toggle a pin, then I can get a rough estimate of where the main clock is running.

int main(void)
{
    /* Initialize the SAM system */
    SystemInit();
	
	// setup blinking
	
	PORT->Group[1].DIRSET.reg |= (uint32_t)(1 << 13);
	PORT->Group[1].DIRSET.reg |= (uint32_t)(1 << 14);
	PORT->Group[1].OUTSET.reg |= (uint32_t)(1 << 13);
	PORT->Group[1].OUTSET.reg |= (uint32_t)(1 << 14);
	
	// setup ring
	
	PORT->Group[1].DIRSET.reg |= (uint32_t)(1 << 2);
	PORT->Group[1].OUTSET.reg |= (uint32_t)(1 << 2);
	
	PORT->Group[1].DIRCLR.reg = (uint32_t)(1 << 3);
	PORT->Group[1].PINCFG[3].reg |= PORT_PINCFG_INEN;
	PORT->Group[1].PINCFG[3].reg &= ~PORT_PINCFG_PULLEN;
	
	SysTick_Config(500);

    while (1) 
    {

    }
}

void SysTick_Handler(void){
	PORT->Group[1].OUTTGL.reg = (uint32_t)(1 << 2);
	PORT->Group[1].OUTTGL.reg = (uint32_t)(1 << 14); // blink STLB
}

I see 48kHz on the scope, and I know I've got a 1000x multiplier on that wave (as the scope is counting positive clock edges only, the toggle does postive-to-negative etc). So these chips set up to run at 48MHz unless we tell them otherwise. Let's try that. I'm following adafruit's bootloader code, mostly.

Also, this was nice GPIO reference for the structures used in the ATSAMD series (21 and 51).

OK, this clock is baffling me. I'm trapped at 48MHz ... going to move on for now. Somehow I want to set up the DFLL (digital frequency locked loop .. ?) to run on the 32.768kHz xtal (as a reference) with a big ol' multiplier to bring it up to 120MHz. To be honest I'm not really sure if even this is correct.

In any case, here's some code for a ring test, for when I get the clock sorted. On a 48MHz clock it runs at 1.6MHz. Uses the setup above...

    while (1) 
    {
		//PORT->Group[1].OUTTGL.reg = (uint32_t)(1 << 2);
		if(PORT->Group[1].IN.reg & (1 << 3)){
			PORT->Group[1].OUTCLR.reg = (uint32_t)(1 << 2);
		} else {
			PORT->Group[1].OUTSET.reg = (uint32_t)(1 << 2);
		}
    }

USART on the ATSAMD51

Altogether more registers than I'd like to manage, but here we are.


int main(void)
{
    /* Initialize the SAM system */
    SystemInit();
	
	//clock_init();
	
	/*setup blinking
	STLR		PB13
	STLB		PB14
	*/
	PORT->Group[1].DIRSET.reg |= (uint32_t)(1 << 13);
	PORT->Group[1].DIRSET.reg |= (uint32_t)(1 << 14);
	PORT->Group[1].OUTSET.reg |= (uint32_t)(1 << 13);
	PORT->Group[1].OUTSET.reg |= (uint32_t)(1 << 14);
	
	SysTick_Config(5000000);
	
	/* setup UARTs
	NP1RX		PA12 / SER4-1
	NP1TX		PA13 / SER4-0

	NP2RX		PB03 / SER5-1 / Peripheral D
	NP2TX		PB02 / SER5-0 / Peripheral D
	*/
	
	// setup pins for peripheral
	PORT->Group[1].DIRCLR.reg = (uint32_t)(1 << 3); // rx is input
	PORT->Group[1].DIRSET.reg = (uint32_t)(1 << 2); // tx output
	PORT->Group[1].PINCFG[3].bit.PMUXEN = 1;
	PORT->Group[1].PMUX[3>>1].reg |= PORT_PMUX_PMUXE(0x3); 
	PORT->Group[1].PMUX[3>>3].reg |= PORT_PMUX_PMUXO(0x3);
	PORT->Group[1].PINCFG[2].bit.PMUXEN = 1;
	PORT->Group[1].PMUX[2>>1].reg |= PORT_PMUX_PMUXE(0x3);
	PORT->Group[1].PMUX[2>>3].reg |= PORT_PMUX_PMUXO(0x3);
	// unmask clocks
	MCLK->APBDMASK.reg |= MCLK_APBDMASK_SERCOM5;
	// generate clocks to, starting with clock 6 (arbitrary choice, lower # held for system things)
	// datasheet says normally one gclk per peripheral shrugman knows why
	GCLK->GENCTRL[6].reg = GCLK_GENCTRL_SRC(GCLK_GENCTRL_SRC_DFLL) | GCLK_GENCTRL_GENEN;
	while(GCLK->SYNCBUSY.reg & GCLK_SYNCBUSY_GENCTRL6);
	GCLK->PCHCTRL[SERCOM5_GCLK_ID_CORE].reg = GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN_GCLK6;
	// now the sercom
	while(SERCOM5->USART.SYNCBUSY.bit.ENABLE);
	SERCOM5->USART.CTRLA.bit.ENABLE = 0;
	while(SERCOM5->USART.SYNCBUSY.bit.SWRST);
	SERCOM5->USART.CTRLA.bit.SWRST = 1;
	while(SERCOM5->USART.SYNCBUSY.bit.SWRST);
	while(SERCOM5->USART.SYNCBUSY.bit.SWRST || SERCOM5->USART.SYNCBUSY.bit.ENABLE);
	// now reset and ready, do config
	SERCOM5->USART.CTRLA.reg = SERCOM_USART_CTRLA_MODE(1) | SERCOM_USART_CTRLA_DORD | SERCOM_USART_CTRLA_RXPO(1) | SERCOM_USART_CTRLA_TXPO(0);
	while(SERCOM5->USART.SYNCBUSY.bit.CTRLB);
	SERCOM5->USART.CTRLB.reg = SERCOM_USART_CTRLB_RXEN | SERCOM_USART_CTRLB_TXEN | SERCOM_USART_CTRLB_CHSIZE(0);
	/*
	BAUD = 65536*(1-S*(fBAUD/fref))
	where S is samples per bit, 16 for async uart
	where fBAUD is the rate that you want
	where fref is the peripheral clock from GCLK, in this case (and most) 48MHz
	*/
	SERCOM5->USART.BAUD.reg = 45402;
	while(SERCOM5->USART.SYNCBUSY.bit.ENABLE);
	SERCOM5->USART.CTRLA.bit.ENABLE = 1;
	
	
    while (1) 
    {
		while(!SERCOM5->USART.INTFLAG.bit.DRE);
		SERCOM5->USART.DATA.reg = (uint8_t)170;
    }
}

void SysTick_Handler(void){
	PORT->Group[1].OUTTGL.reg = (uint32_t)(1 << 14); // blink STLB
}

Interrupts on the ATSAMD51

To configure an interrupt on the ATSAMD51, roughly, we do this:


int main(void){

	__enable_irq(); // globally enables the NVIC

	// turns on a particular 'irq line' or 'irq number'
	// for example, in the SERCOM->UART section of the datasheet, 
	// see 34.8.8 - the interrupt 'flag' sections. these are analagous to 'line numbers'
	// and are linked to 'interrupt numbers' per 10.2.2
	NVIC_EnableIRQ(SERCOM4_2_IRQn); 
}

// we can then handle the interrupt:

void SERCOM4_2_Handler(void){
	// we must clear the flag, often this is done by reading the flag register, like this:
	// SERCOM4->USART->INTFLAG.bit.RXC = 1; // odd, writing '1' is normally the way to clear it
	// however, for this particular register, we read the data from the peripheral to clear the interrupt.
	uint8_t data = SERCOM4->USART.DATA.reg;
	pin_clear(&stlr); // indicate
	// then we would presumably do something
}

TODO: make ATSAMD51 doc page for all of this bringup !

Async-happy USART

To handle USART on these systems, I implement a (hopefully) bulletproof and interrupt-friendly architecture.

TX and RX both have interrupt handlers - the RX dumps bytes into a ringbuffer for the application to poll later on (polling the ringbuffer, not the UART hardware) and the TX buffer is set up so that the application can dump a block of bytes into the transmitter at once, without waiting for anything to transmit.


int main(void)
{
    /* Initialize the SAM system */
    SystemInit();
	SysTick_Config(5000000);
	
	//clock_init();
	
	// lights
	
	stlb = pin_new(&PORT->Group[1], 14);
	pin_output(&stlb);
	pin_set(&stlb);
	stlr = pin_new(&PORT->Group[1], 13);
	pin_output(&stlr);
	pin_set(&stlr);
	
	// ready interrupt system
	__enable_irq();
	NVIC_EnableIRQ(SERCOM4_0_IRQn); //up1tx
	NVIC_EnableIRQ(SERCOM4_2_IRQn); //up1rx
	NVIC_EnableIRQ(SERCOM5_0_IRQn);
	NVIC_EnableIRQ(SERCOM5_2_IRQn);
	
	// ringbuffers (for uart ports)
	rb_init(&up1_rbrx);
	rb_init(&up1_rbtx);
	rb_init(&up2_rbrx);
	rb_init(&up2_rbtx);
	
	// uarts (ports)
	// TODO: have used PMUXO and PMUXE incorrectly: only set one of these, based on whether / not pin is even / odd !
	
	up1 = uart_new(SERCOM4, &PORT->Group[0], &up1_rbrx, &up1_rbtx, 12, 13, HARDWARE_IS_APBD, HARDWARE_ON_PERIPHERAL_D); 
	MCLK->APBDMASK.reg |= MCLK_APBDMASK_SERCOM4;
	uart_init(&up1, 6, SERCOM4_GCLK_ID_CORE, 63018); // baud: 45402 for 921600, 63018 for 115200
	up2 = uart_new(SERCOM5, &PORT->Group[1], &up2_rbrx, &up2_rbtx, 3, 2, HARDWARE_IS_APBD, HARDWARE_ON_PERIPHERAL_D);
	MCLK->APBDMASK.reg |= MCLK_APBDMASK_SERCOM5;
	uart_init(&up2, 7, SERCOM5_GCLK_ID_CORE, 63018);
	
	
	// SPI
	// TMC_MOSI		PA07 / SER0-3
	// TMC_MISO		PA04 / SER0-0
	// TMC_SCK		PA05 / SER0-1
	// TMC_CSN		PA06 / SER0-2
	
	// spi
	spi_tmc = spi_new(SERCOM0, &PORT->Group[0], 4, 7, 5, 6, HARDWARE_IS_APBA, HARDWARE_ON_PERIPHERAL_D);
	MCLK->APBAMASK.reg |= MCLK_APBAMASK_SERCOM0;
	spi_init(&spi_tmc, 8, SERCOM0_GCLK_ID_CORE, 126, 0, 2);
	
	// -> DO SPI, talk to the TMC !
		
    while (1) 
    {
		// spi_txchar_polled(&spitmc, 'x');
		// find TMC registers now, try to read!
		
    }
}

void SysTick_Handler(void){
	pin_toggle(&stlb);
	while(!rb_empty(up1.rbrx)){
		uart_sendchar_buffered(&up1, rb_get(up1.rbrx));
	}
}

void SERCOM4_0_Handler(void){
	uart_txhandler(&up1);
}

void SERCOM4_2_Handler(void){
	uart_rxhandler(&up1);
}

void SERCOM5_0_Handler(void){
	uart_txhandler(&up2);
}

void SERCOM5_2_Handler(void){
	uart_rxhandler(&up2);
}

and


void uart_sendchar_polled(uartport_t *uart, uint8_t data){
	while(!uart->com->USART.INTFLAG.bit.DRE);
	uart->com->USART.DATA.reg = data;
}

void uart_sendchar_buffered(uartport_t *uart, uint8_t data){
	rb_putchar(uart->rbtx, data); // dump it in there
	uart->com->USART.INTENSET.bit.DRE = 1; // set up the volley
}

void uart_rxhandler(uartport_t *uart){
	uint8_t data = uart->com->USART.DATA.reg;
	rb_putchar(uart->rbrx, data);
}

void uart_txhandler(uartport_t *uart){
	if(!rb_empty(uart->rbtx)){
		uart->com->USART.DATA.reg = rb_get(uart->rbtx);
	} else {
		uart->com->USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE;
	}
}

Trinamic Bit-Fiddling

Next I'm going to try to get some steps to happen. I guess we're ready for this!

I found this library, which will save me from re-writing all of the registers and addresses. Bless.

SPI commands and responses are all 20-bits long. Each 20-bit command is met with a 20-bit status word. Those 20-bit 'commands' are registers - the first three bits of each word denote the address of which register we are re-writing. This means that we have to keep track of each entire register-state in the microcontroller, or should.

Most likely, operation will consist of starting up with sending all 5 registers sequentially in the setup config, and then occasionally polling one to get the response (which, it looks like, contains useful things like relative loads!).

Late on, when I try to make a closed loop stepper with the AS5147, I'll be writing coil currents directly into these registers (I think?) to commutate the motor based on encoder input and a tiny PID loop. That, later - but for now I see that in the DRVCTRL register has two 8-bit words for Current in A and Current in B. Noice.

Another thing I like is that I can set both step edges to signify a step. Normally, step drivers have a 'minimum on time' and only count the positive edge, so the microcode has to do a tiny wait (normally not long enough to warrant an interrupt-to-turn-off). This way I'll just toggle the step pin whenever I want to step, and I can forget about it.

So, first I'll try to figure out how to send a 20-bit word with the SPI peripheral, and verify that I get any kind of response from the TMC.

The ATSAMD51 only allows an 8, 9, or 32 bit word, so 32 it is - I'll just keep the last 12 set to 0 and hope that suffices.

OK, here's a classic debugging moment: I take ~45 minutes (or was it two hours? would rather not count) trying to get my SPI setup correctly. Nothing returns. Welp. I pull my hair out. I take a break, decide to plug in some motor power, because why not? Of course, now, data returns. Heck.

To solve that 20-bit word problem, I'm taking over manual control of the chip select pin, issuing 3 8-bit words (which I'll have to bit-splice myself) and then sending those on one CSN low cycle.

Great, now we unfux the hacked together code, and try to send some data, then some steps, I guess?

I think I'm actually going to try writing this library from the datasheet. Let's see how that goes.

Er, how about we just write some values down manually and send those? Sounds good.