Like most engineers, when I want to build a project, I have an impulse to recreate everything from scratch. When I was working on my inverted pendulum, I wanted to break free from the comfortable Arduino IDE and I wrote my own low-level, embedded C. This weekend, for my quadcopter project, I thought I would attempt something similar, and hopped into Keil to write bare-metal code for the flight controller. While this is great for learning about some of the software and hardware that underpins our high-tech society, it takes a great deal of time and effort as the layers-of-abstraction are all out the window.
On Saturday morning I popped Keil open. Excited and full of energy to get this quadcopter to first-hover, I started to code the flight control software. I knew that writing low-level firmware is difficult (I have written some low-level code for AVR microcontrollers in the past), but I dramatically underestimated the complexity of this STM32F4 chip. The STM32CubeIDE allowed me to configure all of the system clocks I needed with a GUI interface - with Keil I had to consult the datasheet, write registers, check the CMSIS header definitions, and debug. Below is a nice pic of the STM32CubeIDE GUI being used to configure the system clock, followed by 30 lines of C code that performs the same function:
void BSP_Clock_Configuration(void){
//turning the HSI on, wait for ready
RCC->CR |= ((uint32_t) RCC_CR_HSION);
while((RCC->CR & RCC_CR_HSIRDY) == 0);
RCC->CFGR = RCC_CFGR_SW_HSI; /* HSI is system clock */
while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_HSI); /* Wait for HSI used as system clock */
//configure the PLL
RCC->PLLCFGR |= RCC_PLLCFGR_PLLSRC_HSI; //sets the PLL input to the HSI
RCC->PLLCFGR |= RCC_PLLCFGR_PLLM_4; //divides by 16
RCC->PLLCFGR |= (0xD0UL << RCC_PLLCFGR_PLLN_Pos); //logic for this in a spreadsheet
RCC->PLLCFGR |= RCC_PLLCFGR_PLLP_0; //divides by 4
//turn off some of the default PLL configuration
RCC->PLLCFGR &= ~RCC_PLLCFGR_PLLN_6;
//enable PLL, wait for ready
RCC->CR |= RCC_CR_PLLON;
while((RCC->CR & RCC_CR_PLLRDY) == 0);
//set the sysclk mux to take from the PLL
RCC->CFGR &= ~RCC_CFGR_SW;
RCC->CFGR |= RCC_CFGR_SW_PLL;
//wait for PLL to become sysclk source
while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
}
void timer_setup(){
PRR &= _BV(PRTIM2);
TCCR0B |= _BV(CS02) | _BV(CS00); //1024bit prescaler
}
void rotary_encoders_setup(){
//Set up PORTD to accept the rotary encoder data
PCICR |= _BV(PCIE2); //turn on interrupt
PCMSK2 |= _BV(PCINT21) | _BV(PCINT20) | _BV(PCINT19) | _BV(PCINT18); // enable pins on interrupt
DDRD = 0b11000000; //all pins on Port B are inputs except for PD7/6
PORTD = 0b00111100; //pullups for portD0, portD1, portD2, portD3
PIND = 0b11000000; //release the brakes
}
void BSP_GPIO_Configuration(void){
//configure GPIOA
//enable AHB clock for GPIOA
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
__DSB(); //delay after peripheral enables per product errata datasheet
//GPIO MODER register sets the GPIOA to
//"alternate function" mode. The PWM
//that is coming off the timer is considered an
//"alternate mode"
GPIOA->MODER |= GPIO_MODER_MODER11_1;
GPIOA->MODER |= GPIO_MODER_MODER10_1;
GPIOA->MODER |= GPIO_MODER_MODER9_1;
GPIOA->MODER |= GPIO_MODER_MODER8_1;
//select the pull-up/pull-down type, output speed,
//etc for the periphral
//push pull is default
//set pull to pull-up
GPIOA->PUPDR |= GPIO_PUPDR_PUPDR11_0;
GPIOA->PUPDR |= GPIO_PUPDR_PUPDR10_0;
GPIOA->PUPDR |= GPIO_PUPDR_PUPDR9_0;
GPIOA->PUPDR |= GPIO_PUPDR_PUPDR8_0;
//set speed to high
//3U for high mode
GPIOA->OSPEEDR |= GPIO_OSPEEDR_OSPEED11; //by default this is 3U bit shifted to pos
GPIOA->OSPEEDR |= GPIO_OSPEEDR_OSPEED10;
GPIOA->OSPEEDR |= GPIO_OSPEEDR_OSPEED9;
GPIOA->OSPEEDR |= GPIO_OSPEEDR_OSPEED8;
//attach the GPIO pin to the timer peripheral
//by altering the "GPIOA->AFRH" register,
//this will alter a MUX that goes to the GPIO.
//0x0001 => AF1 => TIM1, and the "AFRH" works on pins
//A8 to A15
//AFR[1] here is the AFRH
GPIOA->AFR[1] |= 0x0001 << GPIO_AFRH_AFSEL11_Pos;
GPIOA->AFR[1] |= 0x0001 << GPIO_AFRH_AFSEL10_Pos;
GPIOA->AFR[1] |= 0x0001 << GPIO_AFRH_AFSEL9_Pos;
GPIOA->AFR[1] |= 0x0001 << GPIO_AFRH_AFSEL8_Pos;
//configure GPIOB
}
void motor_setup(){
//Set up PWM
TCCR1A |= _BV(COM1A1) | _BV(WGM11) | _BV(WGM10);
TCCR1B |= _BV(CS10);
OCR1A = 0x03ff; //10-bit compare register, initially set at maximum
DDRB = 0b0010010; //pin 9 (PB1) is PWM, output, PB4 is status LED (red) control
//Motor direction control on PORTC
DDRC = 0b00000011;
}
void BSP_Timer_PWM_Configuration(void){
/*Quadcopter ESCs have their own uC's that have
*multiple different comm protocols to control them
*oneshot, etc. ESCs typically also accept a 50 Hz PWM
*signal. Currently this function will configure the
*timer counter to generate a 50 Hz PWM signal
*/
//enable SYSCLK for timer 1 on APB2
RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;
__DSB(); //delay after peripheral enables per product errata datasheet
//use prescaler to divide incoming 84 Mhz clock
//freq_tim1 = freq_clk / (psc + 1)
TIM1->PSC = 12UL;
//start at 0, the auto-reload is set s.t.
//the counter will overflow at 62677
//this is so the PWM hits the target rate of 50 Hz
//might implement the calculation here eventually,
//currently have a spreadsheet running these timer
//calcs
TIM1->CNT = 0UL;
TIM1->ARR = 56602UL;
TIM1->RCR = 0UL; //0 for RCR should force register reloads on every eupdate event
//set some default values here for signals
TIM1->CCR1 = 2000UL;
TIM1->CCR2 = 2000UL;
TIM1->CCR3 = 2000UL;
TIM1->CCR4 = 2000UL;
//enable preloading
TIM1->CR1 |= TIM_CR1_ARPE;
TIM1->CCMR1 |= TIM_CCMR1_OC1PE;
TIM1->CCMR1 |= TIM_CCMR1_OC2PE;
TIM1->CCMR2 |= TIM_CCMR2_OC3PE;
TIM1->CCMR2 |= TIM_CCMR2_OC4PE;
//ensure channels are configured as outputs
TIM1->CCMR1 &= ~TIM_CCMR1_CC1S; //turns off these bits
TIM1->CCMR1 &= ~TIM_CCMR1_CC2S;
TIM1->CCMR2 &= ~TIM_CCMR2_CC3S;
TIM1->CCMR2 &= ~TIM_CCMR2_CC4S;
//configure the PWM mode
TIM1->CCMR1 |= (6UL << TIM_CCMR1_OC1M_Pos); //0x110 triggers PWM mode 1
TIM1->CCMR1 |= (6UL << TIM_CCMR1_OC2M_Pos);
TIM1->CCMR2 |= (6UL << TIM_CCMR2_OC3M_Pos);
TIM1->CCMR2 |= (6UL << TIM_CCMR2_OC4M_Pos);
//enable outputs on the physical pins
TIM1->CCER |= TIM_CCER_CC1E;
TIM1->CCER |= TIM_CCER_CC2E;
TIM1->CCER |= TIM_CCER_CC3E;
TIM1->CCER |= TIM_CCER_CC4E;
//preloads are transferred to shadow registers only when
//an update event occurs according to docs, so
//need to initialize all the regs we just set
//by "setting the UG bit in the TIMx_EGR" reg
TIM1->EGR |= TIM_EGR_UG;
//set the main output enable bit
TIM1->BDTR |= TIM_BDTR_MOE;
//this will select the internal clock as the counter
//source and start the output compare mode
TIM1->CR1 |= TIM_CR1_CEN;
}