Все этапы создания робота для следования по линии, или как собрать все грабли с STM32

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.
Привет, Хабр!

В этой публикации речь пойдет про мой опыт создания робота для соревнований, а именно следование по линии. Постараюсь рассказать все этапы от проектирования схемотехники и заказа печатной платы до алгоритма и программы.

Гонка роботов по линии уже давно стала базовым испытанием для начинающих робототехников. Практически все региональный и международных соревнования включают это направление, поэтому сделав одного робота вы сможете участвовать практически везде. На первом курсе университета это было следующее задание после первого мигания светодиодом.

Постановка задачи


Любая разработка начинается с технического задания, в нашем случае в качестве ТЗ выступает регламент соревнований "Робофинист". В нем нас интересуют требования к габаритам (не более 25х25х25 см) и весу робота (не более 1 кг), а также форма трассы и ее ширина 1,5 см. В зависимости от сложности трассы устанавливается порог на минимальное время ее прохождения (60 сек).

Задача ясна, нужен робот способный обнаружить линию и проехать по ней от старта до финиша за минимальный отрезок времени.

Проектирование принципиальной схемы


Ядром робота является микроконтроллер STM32F103C8T6, он считывает информацию о трассе с помощью оптических датчиков QRE1113 и управляет двигателями через драйвер A3906. Для наглядности работы датчиков каждому присвоен собственный светодиод, как только датчик «видит» черную линию светодиод загорается, это очень упрощает процесс отладки. В качестве источника питания был выбран li-po аккумулятор типа 18650 с напряжением 3,7 В и микросхема заряда LTC4054ES5. Но для корректной работы двигателей нужно не менее 6 В, поэтому поставил повышающий DC-DC преобразователь напряжения LM2577, и линейный LM1117 на 3,3 В для питания логической части.

image

Пришло время рассказать про первые ошибки. По невнимательности перепутал одну пару пинов питания микроконтроллера и при запуске потратил несколько часов на поиски проблемы, в результате пришлось откусить их. Затем появилась проблема что не все датчики реагировали на линию, моя первая реакция была что я спалил микроконтроллер или перебитые ноги питания на прошлом этапе оказались совсем не лишними, или что я плохо пропаял контакты.

Воспользовавшись осциллографом, обнаружил что проблема крылась в самих датчиках, из 10 датчиков не работало 4, заменив их на новые появилась следующая проблема — не работало три индикаторных светодиода PB3, PB4 и PA15. Переписав и просверлив взглядом весь свой код я не мог найти отличий в инициализации не рабочих и рабочих портов везде был Push Pull, 10 MHz и включенное тактирование. Посмотрев осциллограмму заметил что один пин притянут к плюсу и даже в режиме отладки никак не сбрасывался, я понял что спалил микроконтроллер. Закрывая даташит заметил что по странному совпадению именно эти пины используются для JTAG, а я использую SWD, но решив что хуже не будет начал гуглить как отключить JTAG и о чудо это и вправду помогло (см. ниже).

Мини вывод, проверяйте правильность питание на всех этапах чтобы потом не думать спалили вы что то или оно не работает по другим причинам.

Что нужно доработать в схеме


Микросхему заряда догадался поставить, а вот про разряд забыл, хотя она не менее важна, плюс в 2020 г. поставил mini USB вместо type C. Хотелось бы еще микроконтроллером отслеживать заряд аккумулятора, но у него не осталось свободных пинов АЦП. Выбирая драйвер опирался на характеристики двигателя 6 В и 700 мА, но не учел что его максимальное напряжение 9 В, а хотелось бы 12 В.

Создание печатной платы


Первым делом было решено определиться с размерами и формой ПП и по совместительству платформой робота. Вспоминаем что по регламенту габариты не должны превышать 25х25 см, но также стоит учесть что при заказе ПП на заводе (PCBWay) в Китае, платы свыше 10х10 см значительно вырастают в цене. Думаю выбор габаритов очевиден.

Дальше следует распределить симметрично датчики (с нижней стороны платы) учитывая ширину линии, а затем и самые тяжелые элементы, такие как, двигатели и аккумулятор. Светодиоды устанавливаются на одной оси с соответствующими датчиками, с противоположно стороны платы. Для удобства устанавливаем тумблер включения и разъем заряда аккумулятора в задней части платформы. Остальные элементы устанавливаем ближе к центру.

Обрезав пустые края платы вместо квадрата получается вполне достойный образ платформы робота.

Плата получилось достаточно простой из-за малого количества уникальных элементов. Итого 2 слоя минимальная толщина дорожки 0,25 мм. Сохранив плату в формате Gerber отправил ее на завод выбрав белый цвет маски, тут у меня не возникло особых проблем.

image

И 3D вид платы с элементам. Живое фото будет в конце.

image

Что нужно доработать в плате


Некоторые элементы установил слишком близко к пластиковому корпусу аккумулятора и было сложно их паять, а еще сложнее перепаивать. Забыл подписать цоколевку разъема программирования SWD, UART и двигателей в итоге приходится заглядывать в схему.

Закупка комплектующих


Разработка всего нового требует самых больших относительных затрат на единицу товара, попытаться сэкономить можно и нужно, но в разумных пределах, так заказав на Алиэкспресс датчики линии в 4,5 раза дешевле чем в магазине электроники, я поплатился тем что примерно 40% из них оказались не рабочими, а это время на поиск и устранения ошибок. С другой стороны мне не хотелось покупать микросхему LM2577 за 460 руб, в то время когда модуль со всей обвязкой стоит 100 руб. Часть деталей заказал на Али, другую часть в местном магазине.

Вот мои примерные затраты на комплектующие:

  1. Двигатели и колеса — 800 руб;
  2. Аккумулятор и кейс — 400 руб;
  3. Зарядное устройство — 300 руб;
  4. Микроконтроллер и драйвер — 200 руб;
  5. Стабилизаторы — 400 руб;
  6. Платы 10 ед с доставкой 2400/10 = 240 руб за ед;
  7. Остальная обвязка — 150 руб.

Итого: 4650 руб общие затраты на компоненты, или 2490 если вычесть цену остальных девяти не используемых плат.

Думаю при правильной закупке компонентов и их монтаже прямо на заводе печатных плат, можно рассчитывать на стоимость в районе 1500-2000 руб за единицу уже при партии в 20-30 единиц.

Алгоритм


Алгоритм реализовал самый простой, если линия оказалась, например, с правой стороны, то правый двигатель начнет замедляться и чем ближе линия к краю, тем медленнее будет работать двигатель, вплоть до его полной остановки, в то время как левый двигатель будет работать на максимальной скорости пытаясь развернуться в сторону линии. Аналогичная ситуация если линия окажется с левой стороны. Тем самым робот пытается стабилизироваться, возвращая линию к центральным датчикам.

image

Алгоритм в картинках
image

image

image

image

image

Так же необходимо предусмотреть и другие ситуации, например, если все датчики засекли линию, то это скорее всего означает что мы пересекаем перекресток и вероятнее всего нам нужно ехать дальше вперед. Или если только крайние датчики засекли линию, то мы приехали на финиш и нужно остановится.

Программирование


На этом этапе нужно доставать бубен и бить в него.

Писал в Atollic True Studio использовал только CMSIS и FreeRTOS. На запуск FreeRTOS потратил половину недели, делал все по инструкциям, но в процессе компиляции ловил ошибки, исправлял 1 появлялось 63, или не было ошибок, но контроллер зависал. В какой то момент я понял что уровень моего руко… достиг небывалых высот и пора бы обдумать чем я вообще занимаюсь и что делаю. Благодаря товарищам из сообщества начал копать в нужную сторону и из множества перерытых источников нашел несколько, которые мне помогли (см. ниже). В итоге удалось победить FreeRTOS и пока я его использовал на 0%, потому что все запихал в одну задачу.

Тут мне больше всего помогло видео и статья, и эта статья.

Система тактирования, тут я до сих пор не уверен что оно работает так как должно. Настроив системную частоту через PLL на 48 Мгц, я увидел на ноге MCO 24 Мгц (/2), нет я увидел 22-25 МГц сигнала отдаленно напоминающий синус. При этом я выставил в FreeRTOS config 48Мгц и поставил задержку в 500 мс и запустив увидел что мои 500 мс превратились в 300, перебрав все возможные настройки, заметил что целевые 500 мс я получаю только при тактирование от внутреннего генератора на 8 Мгц, а значение частоты FreeRTOS config вообще никак не влияло, ставил и 1 Гц и 48 ГГц. Решил вернуть 48 МГц и работать с 300 мс, после чего случайно перезагрузил True Studio и о чудо оно работает так как задумывалось, при этом с самого начала я уже указал все пути к файлам.

Проблему JTAG и светодиодов, описал выше, решение нашел тут и нужно просто его отключить освободив тем самым доступ к пинам PB3, PB4 и PA15.

AFIO->MAPR |= AFIO_MAPR_SWJ_CFG_JTAGDISABLE;

Структура проекта примерно следующая:

Много кода
1. Запускаем систему тактирования;

void RCC_Init(void) {						// Quartz 16 MHz

	RCC->CFGR |= RCC_CFGR_PLLXTPRE;	// PLLXTPRE set divider (/2) = 8 MHz

	RCC->CR |= RCC_CR_HSEON;			// On HSE
	while (!(RCC->CR & RCC_CR_HSERDY)) {
	};

	RCC->CFGR |= (RCC_CFGR_PLLMULL6);     // Setup PLLMULL set multiplier (x6) = 48 MHz

	RCC->CFGR |= RCC_CFGR_PLLSRC;		// PLLSRC set HSE

	RCC->CR |= RCC_CR_PLLON;			// ON PLL
	while (!(RCC->CR & RCC_CR_PLLRDY)) {
	};

	FLASH->ACR &= ~FLASH_ACR_LATENCY;	// Setup FLASH
	FLASH->ACR |= FLASH_ACR_LATENCY_1;

	RCC->CFGR |= RCC_CFGR_HPRE_DIV1; 	// Set not divider (AHB) = 48 MHz
	RCC->CFGR |= RCC_CFGR_PPRE1_DIV2; 	// Set divider (/2) (APB1) = 24 MHz
	RCC->CFGR |= RCC_CFGR_PPRE2_DIV1; 	// Set not divider (APB2) = 48 MHz

	RCC->CFGR &= ~RCC_CFGR_SW; 		// Setup SW, select PLL
	RCC->CFGR |= RCC_CFGR_SW_PLL;

//	RCC->CFGR |= RCC_CFGR_MCO_SYSCLK;

}

2. Настраиваем пины, для датчиков в режим Аналогового входа, для двигателей режим выход таймера, для всех остальных простой режим Push Pull;


void GPIO_Init(void) {

	RCC->APB2ENR |=	(RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN); // Enable clock portA, portB and Alternative function

	AFIO->MAPR |= AFIO_MAPR_SWJ_CFG_JTAGDISABLE;	// Disable JTAG for pins B3,B4 and A15

	//---------------LED: (B3 to B9; A11, A12, A15); Output mode: Push Pull, max 10 MHz---------------//

	GPIOB->CRL &= ~(GPIO_CRL_CNF3 | GPIO_CRL_MODE3);	// Setup B3 pins PP, 10MHz
	GPIOB->CRL |= GPIO_CRL_MODE3_0;

	GPIOB->CRL &= ~(GPIO_CRL_CNF4 | GPIO_CRL_MODE4);	// Setup B3 pins PP, 10MHz
	GPIOB->CRL |= GPIO_CRL_MODE4_0;

	GPIOB->CRL &= ~(GPIO_CRL_CNF5 | GPIO_CRL_MODE5);	// Setup B4 pins PP, 10MHz
	GPIOB->CRL |= GPIO_CRL_MODE5_0;

	GPIOB->CRL &= ~(GPIO_CRL_CNF6 | GPIO_CRL_MODE6);	// Setup B5 pins PP, 10MHz
	GPIOB->CRL |= GPIO_CRL_MODE6_0;

	GPIOB->CRL &= ~(GPIO_CRL_CNF7 | GPIO_CRL_MODE7);	// Setup B6 pins PP, 10MHz
	GPIOB->CRL |= GPIO_CRL_MODE7_0;

	GPIOB->CRH &= ~(GPIO_CRH_CNF8 | GPIO_CRH_MODE8);	// Setup B7 pins PP, 10MHz
	GPIOB->CRH |= GPIO_CRH_MODE8_0;

	GPIOB->CRH &= ~(GPIO_CRH_CNF9 | GPIO_CRH_MODE9);	// Setup B8 pins PP, 10MHz
	GPIOB->CRH |= GPIO_CRH_MODE9_0;

	GPIOA->CRH &= ~(GPIO_CRH_CNF11 | GPIO_CRH_MODE11);	// Setup A11 pins PP, 10MHz
	GPIOA->CRH |= GPIO_CRH_MODE11_0;

	GPIOA->CRH &= ~(GPIO_CRH_CNF12 | GPIO_CRH_MODE12);	// Setup A12 pins PP, 10MHz
	GPIOA->CRH |= GPIO_CRH_MODE12_0;

	GPIOA->CRH &= ~(GPIO_CRH_CNF15 | GPIO_CRH_MODE15);	// Setup A15 pins PP, 10MHz
	GPIOA->CRH |= GPIO_CRH_MODE15_0;

	//--Motors: (A8 to A10; B12 to B15); Output and input (B12, B13) mode: Alternative function, PWM - Push Pull, max 10 MHz--//

	GPIOB->CRH &= ~(GPIO_CRH_CNF12 | GPIO_CRH_MODE12);		// Setup B12 pins analog input

	GPIOB->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);		// Setup B13 pins analog input

	GPIOB->CRH &= ~(GPIO_CRH_CNF14   | GPIO_CRH_MODE14);	// Setup B14 pins PP, 10MHz
	GPIOB->CRH |=  GPIO_CRH_MODE14_0;

	GPIOB->CRH &= ~(GPIO_CRH_CNF15   | GPIO_CRH_MODE15);	// Setup B15 pins PP, AF, 10MHz
	GPIOB->CRH |=  (GPIO_CRH_CNF15_1 | GPIO_CRH_MODE15_0);

	GPIOA->CRH &= ~(GPIO_CRH_CNF8 | GPIO_CRH_MODE8);		// Setup A10 pins PP, 10MHz
	GPIOA->CRH |= GPIO_CRH_MODE8_0;

	GPIOA->CRH &= ~(GPIO_CRH_CNF9    | GPIO_CRH_MODE9);		// Setup A9 pins PP, AF, 10MHz
	GPIOA->CRH |=  (GPIO_CRH_CNF9_1  | GPIO_CRH_MODE9_0);

	GPIOA->CRH &= ~(GPIO_CRH_CNF10 | GPIO_CRH_MODE10);		// Setup A10 pins PP, 10MHz
	GPIOA->CRH |= GPIO_CRH_MODE10_0;

	//--UART3: (B10 - TX; B11 - RX); Output mode: Alternative function, UART - Push Pull, max 10 MHz--//

	GPIOB->CRH &= ~(GPIO_CRH_CNF10   | GPIO_CRH_MODE10);	// Setup B10 pins PP, AF, 10MHz
	GPIOB->CRH |=  (GPIO_CRH_CNF10_1 | GPIO_CRH_MODE10_0);

	GPIOB->CRH &= ~(GPIO_CRH_CNF11   | GPIO_CRH_MODE11);	// Setup B11 pins PP, AF, 10MHz
	GPIOB->CRH |=  (GPIO_CRH_CNF11_1 | GPIO_CRH_MODE11_0);

	//--Optical sensors: (B0, B1, A0 - A7); Input mode: Analog--//

	GPIOB->CRL &= ~(GPIO_CRL_CNF0 | GPIO_CRL_MODE0);	// Setup B0 pins analog input
	GPIOB->CRL &= ~(GPIO_CRL_CNF1 | GPIO_CRL_MODE1);	// Setup B1 pins analog input
	GPIOA->CRL &= ~(GPIO_CRL_CNF0 | GPIO_CRL_MODE0);	// Setup A0 pins analog input
	GPIOA->CRL &= ~(GPIO_CRL_CNF1 | GPIO_CRL_MODE1);	// Setup A1 pins analog input
	GPIOA->CRL &= ~(GPIO_CRL_CNF2 | GPIO_CRL_MODE2);	// Setup A2 pins analog input
	GPIOA->CRL &= ~(GPIO_CRL_CNF3 | GPIO_CRL_MODE3);	// Setup A3 pins analog input
	GPIOA->CRL &= ~(GPIO_CRL_CNF4 | GPIO_CRL_MODE4);	// Setup A4 pins analog input
	GPIOA->CRL &= ~(GPIO_CRL_CNF5 | GPIO_CRL_MODE5);	// Setup A5 pins analog input
	GPIOA->CRL &= ~(GPIO_CRL_CNF6 | GPIO_CRL_MODE6);	// Setup A6 pins analog input
	GPIOA->CRL &= ~(GPIO_CRL_CNF7 | GPIO_CRL_MODE7);	// Setup A7 pins analog input

}

3. Настраиваем АЦП на цикличную работу через DMA, этот код я полностью позаимствовал отсюда;


extern uint16_t adc_buf[10];

void ADC_Init(void) {

	 RCC->AHBENR |= RCC_AHBENR_DMA1EN;
	 DMA1_Channel1->CPAR = (uint32_t) &ADC1->DR;		// адрес периферийного устройства
	 DMA1_Channel1->CMAR = (unsigned int) adc_buf;   	// адрес буфера в памяти

	 DMA1_Channel1->CNDTR = 10;			  				// количество данных для обмена
	 DMA1_Channel1->CCR &= ~DMA_CCR_EN;			   		// Отключаем для настройки
	 DMA1_Channel1->CCR |= DMA_CCR_MSIZE_0;		  		// размер памяти 16 bit
	 DMA1_Channel1->CCR |= DMA_CCR_PSIZE_0;		 		// размер периферии 16 bit
	 DMA1_Channel1->CCR |= DMA_CCR_MINC;			  	// memory increment mode
	 DMA1_Channel1->CCR |= DMA_CCR_CIRC;
	 DMA1_Channel1->CCR |= DMA_CCR_TCIE;				// прерывание по окончанию передачи
	 DMA1_Channel1->CCR |= DMA_CCR_EN;				  	// разрешаем работу
	 NVIC_SetPriority(DMA1_Channel1_IRQn, 10);
	 NVIC_EnableIRQ(DMA1_Channel1_IRQn);

	 RCC->CFGR &= ~RCC_CFGR_ADCPRE;
	 RCC->CFGR |= RCC_CFGR_ADCPRE_DIV8;
	 RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;

	 ADC1->SQR3 = 0;			// 1
	 ADC1->SQR3 |= 1 << 5;		// 2 Слева номер канала справа сдвиг
	 ADC1->SQR3 |= 2 << 10;		// 3
	 ADC1->SQR3 |= 3 << 15;		// 4
	 ADC1->SQR3 |= 4 << 20;		// 5
	 ADC1->SQR3 |= 5 << 25;		// 6
	 ADC1->SQR2 = 6;			// 7
	 ADC1->SQR2 |= 7 << 5;		// 8
	 ADC1->SQR2 |= 8 << 10;		// 9
	 ADC1->SQR2 |= 9 << 15;		// 10

	 ADC1->CR2 = ADC_CR2_EXTSEL_0 | ADC_CR2_EXTSEL_1 | ADC_CR2_EXTSEL_2
	 | ADC_CR2_EXTTRIG;
	 ADC1->SMPR1 = 0;					 		//очистка регистров времени выборки
	 ADC1->SMPR2 = 0;					 		//
	 ADC1->SMPR2 |= (uint32_t) (6 << (0 * 3)); 	//канал 0, время преобразования 6 мкс
	 ADC1->SMPR2 |= (uint32_t) (6 << (1 * 3)); 	//канал 1, время преобразования 6 мкс
	 ADC1->SMPR2 |= (uint32_t) (6 << (2 * 3)); 	//канал 2, время преобразования 6 мкс
	 ADC1->SMPR2 |= (uint32_t) (6 << (3 * 3)); 	//канал 3, время преобразования 6 мкс
	 ADC1->SMPR2 |= (uint32_t) (6 << (4 * 3)); 	//канал 4, время преобразования 6 мкс
	 ADC1->SMPR2 |= (uint32_t) (6 << (5 * 3)); 	//канал 5, время преобразования 6 мкс
	 ADC1->SMPR2 |= (uint32_t) (6 << (6 * 3)); 	//канал 6, время преобразования 6 мкс
	 ADC1->SMPR2 |= (uint32_t) (6 << (7 * 3)); 	//канал 7, время преобразования 6 мкс
	 ADC1->SMPR2 |= (uint32_t) (6 << (8 * 3)); 	//канал 8, время преобразования 6 мкс
	 ADC1->SMPR2 |= (uint32_t) (6 << (9 * 3)); 	//канал 9, время преобразования 6 мкс
	 ADC1->SMPR1 |= (uint32_t) (6 << (0 * 3)); 	//канал 10, время преобразования 6 мкс

	 ADC1->CR2 |= ADC_CR2_ADON;

	 ADC1->CR2 |= ADC_CR2_RSTCAL;
	 while ((ADC1->CR2 & ADC_CR2_RSTCAL) == ADC_CR2_RSTCAL) {
	 }

	 ADC1->CR2 |= ADC_CR2_CAL;

	 while ((ADC1->CR2 & ADC_CR2_RSTCAL) == ADC_CR2_CAL) {
	 }

	 ADC1->SQR1 |= 9 << 20;						// Количество преобразования
	 ADC1->CR1 |= ADC_CR1_SCAN;					// Режим сканирования
	 ADC1->CR2 |= ADC_CR2_DMA;				  	// DMA on
	 // ADC1->CR2 |= ADC_CR2_CONT;

	 //  ADC1->CR2 |= ADC_CR2_SWSTART;
	 ADC1->CR2 |= ADC_CR2_ADON;

}

4. Настраиваем таймер в режим ШИМ для работы двигателей, это я тоже целиком позаимствовал отсюда;

void TIM1_Init(void) {

	/*************** Enable TIM1 (CH1) ***************/
	RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;
	TIM1->CCER = 0;										// обнуляем CCER (выключаем каналы)
	TIM1->ARR = 1000; 									// максимальное значение, до которого таймер ведет счет
	TIM1->PSC = 48 - 1;                					// предделитель
	TIM1->BDTR |= TIM_BDTR_MOE;     					// Разрешаем вывод сигнала на выводы

	TIM1->CCR2 = 0; 									// задаем скважность в регистр сравнения канала (значения от 0 до TIM1->ARR)
	TIM1->CCMR1 |= TIM_CCMR1_OC2M_1 | TIM_CCMR1_OC2M_2; // Включаем канал в режим ШИМ
	TIM1->CCER |= (TIM_CCER_CC2P |TIM_CCER_CC2E); 		// Разрешаем вывод не инвертированного сигнала на ногу МК
														// для второго ШИМ-сигнала используем канал 3
	TIM1->CCR3 = 0; 									// задаем скважность в регистр сравнения канала (значения от 0 до TIM1->ARR)
	TIM1->CCMR2 |= TIM_CCMR2_OC3M_1 | TIM_CCMR2_OC3M_2; // Включаем канал в режим ШИМ
	TIM1->CCER |= TIM_CCER_CC3NE; 						// Разрешаем вывод не инвертированного сигнала на ногу МК
	TIM1->CCER &= ~TIM_CCER_CC3NP;

	TIM1->CR1 |= TIM_CR1_CEN;
}

5. Настроим UART для отладки, тут брал информацию сразу из нескольких источников;

void UART3_Init(void) {

	RCC->APB1ENR |= RCC_APB1ENR_USART3EN;

	USART3->BRR = 0xD0;						// Speed = 115200; (24 000 000 + (115200 / 2)) / 115200 = 208 -> 0xD0

	USART3->CR1 |= USART_CR1_TE | USART_CR1_RE | USART_CR1_UE;

//	USART3->CR1 |= USART_CR1_RXNEIE;
//	NVIC_EnableIRQ(USART3_IRQn);
}

void UART3_Send(char chr) {
	while (!(USART3->SR & USART_SR_TC))
		;
	USART3->DR = chr;
}

void UART3_Send_String(char* str) {
	uint8_t i = 0;

	while (str[i])
		UART3_Send(str[i++]);
}

void UART3_Send_Number_Float(float data){

	char str[100];

	char *tmpSign = (data < 0) ? "-" : "";
	float tmpVal = (data < 0) ? -data : data;

	int tmpInt1 = tmpVal;                  // Get the integer (678).
	float tmpFrac = tmpVal - tmpInt1;      // Get fraction (0.0123).
	int tmpInt2 = trunc(tmpFrac * 10);  // Turn into integer (123).	int tmpInt2 = trunc(tmpFrac * 10000)

	// Print as parts, note that you need 0-padding for fractional bit.

	sprintf (str, "%s%d.%01d", tmpSign, tmpInt1, tmpInt2);

	UART3_Send_String(str);
}

6. На всякий приведу пример FreeRtos config

/*
 * FreeRTOS Kernel V10.3.1
 * Copyright (C) 2020 Amazon.com, Inc. or its affiliates.  All Rights Reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * http://www.FreeRTOS.org
 * http://aws.amazon.com/freertos
 *
 * 1 tab == 4 spaces!
 */

#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H

/* Library includes. */
//#include "stm32f10x_lib.h"
/*-----------------------------------------------------------
 * Application specific definitions.
 *
 * These definitions should be adjusted for your particular hardware and
 * application requirements.
 *
 * THESE PARAMETERS ARE DESCRIBED WITHIN THE 'CONFIGURATION' SECTION OF THE
 * FreeRTOS API DOCUMENTATION AVAILABLE ON THE FreeRTOS.org WEB SITE. 
 *
 * See http://www.freertos.org/a00110.html
 *----------------------------------------------------------*/

#define vPortSVCHandler SVC_Handler		// fix problem
#define xPortPendSVHandler PendSV_Handler
#define vPortSVCHandler SVC_Handler
#define xPortSysTickHandler SysTick_Handler


#define configUSE_PREEMPTION		1
#define configUSE_IDLE_HOOK			0
#define configUSE_TICK_HOOK			1
#define configCPU_CLOCK_HZ			( ( unsigned long ) 48000000 )
#define configTICK_RATE_HZ			( ( TickType_t ) 1000 )
#define configMAX_PRIORITIES		( 5 )
#define configMINIMAL_STACK_SIZE	( ( unsigned short ) 32 )
#define configTOTAL_HEAP_SIZE		( ( size_t ) ( 17 * 1024 ) )
#define configMAX_TASK_NAME_LEN		( 16 )
#define configUSE_TRACE_FACILITY	0
#define configUSE_16_BIT_TICKS		0
#define configIDLE_SHOULD_YIELD		1
#define configUSE_MUTEXES			1

/* Co-routine definitions. */
#define configUSE_CO_ROUTINES 		0
#define configMAX_CO_ROUTINE_PRIORITIES ( 2 )

/* Set the following definitions to 1 to include the API function, or zero
 to exclude the API function. */

#define INCLUDE_vTaskPrioritySet		1
#define INCLUDE_uxTaskPriorityGet		1
#define INCLUDE_vTaskDelete				1
#define INCLUDE_vTaskCleanUpResources	0
#define INCLUDE_vTaskSuspend			1
#define INCLUDE_vTaskDelayUntil			1
#define INCLUDE_vTaskDelay				1

/* This is the raw value as per the Cortex-M3 NVIC.  Values can be 255
 (lowest) to 0 (1?) (highest). */
#define configKERNEL_INTERRUPT_PRIORITY 		255
/* !!!! configMAX_SYSCALL_INTERRUPT_PRIORITY must not be set to zero !!!!
 See http://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html. */
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 	191 /* equivalent to 0xb0, or priority 11. */

/* This is the value being used as per the ST library which permits 16
 priority values, 0 to 15.  This must correspond to the
 configKERNEL_INTERRUPT_PRIORITY setting.  Here 15 corresponds to the lowest
 NVIC value of 255. */
#define configLIBRARY_KERNEL_INTERRUPT_PRIORITY	15

#endif /* FREERTOS_CONFIG_H */


7. И в конце main;

#include "main.h"

void vTaskUART2(void *argument);
void vTaskConvADC(void *argument);

uint32_t Motor(int32_t which, int32_t speed, int32_t turn);

void IndicatorLED(uint32_t number, uint32_t status);


//define Временная мера потом исправлюсь на typedef enum :)
#define MotorLeft		1
#define MotorRight		0
#define MoveBack		0
#define MoveForward		1
#define MaxSpeedMotor	1000

uint16_t adc_buf[10];				// Buffer DMA ADC sensors
uint16_t SensWhiteOrBlack[10];		// Buffer sensors white or black

int main(void) {

	RCC_Init();
	GPIO_Init();
	TIM1_Init();
	UART3_Init();
	ADC_Init();

	xTaskCreate(vTaskUART2, "UART", 512, NULL, 1, NULL);
	xTaskCreate(vTaskConvADC, "ADC", 128, NULL, 1, NULL);

	UART3_Send_String("Start program\r\n");

	GPIOA->BSRR |= GPIO_BSRR_BS10;	// Motor enable

	vTaskStartScheduler();

	while (1) {

	}
}
/*************************************Tasks***************************************/

void vTaskUART2(void *argument) {

	while (1) {

//-------------------Read sensors from buff DMA ADC, and print to UART3-------------//

		for (uint32_t i = 0; i <= 9; i++)
		{

			if (adc_buf[i] <= 300)		// value sens is white
			{
				IndicatorLED(i, 0);
				SensWhiteOrBlack[i] = 0;
			} else						// else value sens is black
			{
				IndicatorLED(i, 1);
				SensWhiteOrBlack[i] = 1;
			}

//			UART3_Send_Number_Float(SensWhiteOrBlack[i]);
//			UART3_Send_String("\t");
		}
///		UART3_Send_String("\r\n");

//-----------------------------------------Move-----------------------------------------//


		//----Normal mode----//
		float devMotorLeft  = 1;
		float devMotorRight = 1;
		uint32_t flagSizeBuf = 0;

		for (int32_t i = 6; i <= 9; i++)
		{
			if (SensWhiteOrBlack[i])
			{
				flagSizeBuf = 1;

				if(i == 6 && SensWhiteOrBlack[i] == 1)
				{
					devMotorRight = 0.75;
				}
				else if(i == 7 && SensWhiteOrBlack[i] == 1)
				{
					devMotorRight = 0.5;
				}
				else if(i == 8 && SensWhiteOrBlack[i] == 1)
				{
					devMotorRight = 0.25;
				}
				else if(i == 9 && SensWhiteOrBlack[i] == 1)
				{
					devMotorRight = 0.0;
				}
			}
		}

		for(int32_t i = 5; i >= 0; i--)
		{
			if(SensWhiteOrBlack[i])
			{
				flagSizeBuf = 1;

				if(i == 3 && SensWhiteOrBlack[i] == 1)
				{
					devMotorLeft = 0.75;
				}
				else if(i == 2 && SensWhiteOrBlack[i] == 1)
				{
					devMotorLeft = 0.5;
				}
				else if(i == 1 && SensWhiteOrBlack[i] == 1)
				{
					devMotorLeft = 0.25;
				}
				else if(i == 0 && SensWhiteOrBlack[i] == 1)
				{
					devMotorLeft = 0.0;
				}
			}
		}

		if(!flagSizeBuf)
		{
			devMotorLeft  = 0;
			devMotorRight = 0;
		}

		Motor(MotorRight, (int32_t)(MaxSpeedMotor * (float)devMotorRight), MoveForward);
		Motor(MotorLeft,  (int32_t)(MaxSpeedMotor * (float)devMotorLeft),  MoveForward);


/*
		if (adc_buf[0] > 1000)
		{
			Motor(MotorLeft, 500, MoveBack);
		}
		else if (adc_buf[0] <= 1000)
		{
			Motor(MotorLeft, 0, MoveForward);
		}

		if (adc_buf[9] > 1000)
		{
			Motor(MotorRight, 500, MoveBack);//Motor(0, 500, 1);	MotorRight	MoveBack
		}
		else if (adc_buf[9] <= 1000)
		{
			Motor(MotorRight, 0, MoveForward);
		}
*/

		vTaskDelay(50);
	}
}
void vTaskConvADC(void *argument) {

	while (1) {

		// just void :)

		vTaskDelay(5000);
	}

}
/**********************************Function*************************************/

void IndicatorLED(uint32_t number, uint32_t status) {

	if (number == 0 && status == 0) {
		GPIOB->BSRR |= GPIO_BSRR_BS9;
	}

	else if (number == 0 && status == 1) {
		GPIOB->BSRR |= GPIO_BSRR_BR9;
	}

	if (number == 1 && status == 0) {
		GPIOB->BSRR |= GPIO_BSRR_BS8;
	}

	else if (number == 1 && status == 1) {
		GPIOB->BSRR |= GPIO_BSRR_BR8;
	}

	if (number == 2 && status == 0) {
		GPIOB->BSRR |= GPIO_BSRR_BS7;
	}

	else if (number == 2 && status == 1) {
		GPIOB->BSRR |= GPIO_BSRR_BR7;
	}

	if (number == 3 && status == 0) {
		GPIOB->BSRR |= GPIO_BSRR_BS6;
	}

	else if (number == 3 && status == 1) {
		GPIOB->BSRR |= GPIO_BSRR_BR6;
	}

	if (number == 4 && status == 0) {
		GPIOB->BSRR |= GPIO_BSRR_BS5;
	}

	else if (number == 4 && status == 1) {
		GPIOB->BSRR |= GPIO_BSRR_BR5;
	}

	if (number == 5 && status == 0) {
		GPIOB->BSRR |= GPIO_BSRR_BS4;
	}

	else if (number == 5 && status == 1) {
		GPIOB->BSRR |= GPIO_BSRR_BR4;
	}

	if (number == 6 && status == 0) {
		GPIOB->BSRR |= GPIO_BSRR_BS3;
	}

	else if (number == 6 && status == 1) {
		GPIOB->BSRR |= GPIO_BSRR_BR3;
	}

	if (number == 7 && status == 0) {
		GPIOA->BSRR |= GPIO_BSRR_BS15;
	}

	else if (number == 7 && status == 1) {
		GPIOA->BSRR |= GPIO_BSRR_BR15;
	}

	if (number == 8 && status == 0) {
		GPIOA->BSRR |= GPIO_BSRR_BS12;
	}

	else if (number == 8 && status == 1) {
		GPIOA->BSRR |= GPIO_BSRR_BR12;
	}

	if (number == 9 && status == 0) {
		GPIOA->BSRR |= GPIO_BSRR_BS11;
	}

	else if (number == 9 && status == 1) {
		GPIOA->BSRR |= GPIO_BSRR_BR11;
	}

}

uint32_t Motor(int32_t which, int32_t speed, int32_t turn) {

	if (which == 0)	//Right motor
	{
		UART3_Send_Number_Float(speed);
		UART3_Send_String("\t");

		if (speed > 0 && speed <= 1000)
		{
			TIM1->CCR3 = speed;
			if (turn == 0) 	// back
			{
				GPIOB->BSRR |= GPIO_BSRR_BS14;
			}
			else			//forward
			{
				GPIOB->BSRR |= GPIO_BSRR_BR14;
			}
		}
		else		//disable motor
		{
			TIM1->CCR3 = 0;
			GPIOB->BSRR |= GPIO_BSRR_BR14;

		}

	}

	else if (which == 1)	//left motor
	{
		UART3_Send_Number_Float(speed);
		UART3_Send_String("\r\n");

		if (speed > 0 && speed <= 1000)
		{
			TIM1->CCR2 = speed;
			if (turn == 1) 	// back
			{
				GPIOA->BSRR |= GPIO_BSRR_BS8;
			}
			else			//forward
			{
				GPIOA->BSRR |= GPIO_BSRR_BR8;
			}
		}
		else		//disable motor
		{
			TIM1->CCR2 = 0;
			GPIOA->BSRR |= GPIO_BSRR_BS8;

		}

	}



	return 0;
}

/*************************************IRQ***************************************/
/*
 void USART2_IRQHandler(void) {

 if (USART2->SR & USART_CR1_RXNEIE) {

 USART2->SR &= ~USART_CR1_RXNEIE;

 if (USART2->DR == '0') {
 UART2_Send_String("OFF\r\n");
 GPIOC->BSRR = GPIO_BSRR_BS13;
 } else if (USART2->DR == '1') {
 UART2_Send_String("ON\r\n");
 GPIOC->BSRR = GPIO_BSRR_BR13;
 } else if (USART2->DR == '2') {
 uint16_t d0 = ADC1->DR;
 float Vdd = 1.2 * 4069 / d0;
 UART2_Send(Vdd);
 GPIOC->BSRR = GPIO_BSRR_BR13;
 }
 }
 }
 */
void DMA1_Channel1_IRQHandler(void) {

	DMA1->IFCR = DMA_IFCR_CGIF1 | DMA_IFCR_CTCIF1;	// clear DMA interrupt flags
	DMA1_Channel1->CCR &= ~DMA_CCR_EN;					// Запрещаем работу
	/*
	 * Code
	 */
	DMA1_Channel1->CCR |= DMA_CCR_EN;			   // разрешаем работу
	ADC1->CR2 |= ADC_CR2_ADON;
}



Немного поговорить


Этот проект висел у меня в планах уже несколько лет и все время откладывался, но вот сейчас он действительно приближается к логическому завершению чему я очень рад, закончив его хотел бы сделать его частично открытым и продавать в качестве очередной платформы/конструктора если получится, то и до робота сумоиста не далеко, а там и другие пойдут.
В планах не было писать статью, но участие в конкурсе ее требует.

Чуть не забыл про фото, некоторые компоненты еще не пришли, а дедлайн конкурса уже завтра поэтому пришлось использовать DIY не в лучшем его исполнении.

Крайнее фото
image

vk.com/timurkyun
Источник: https://habr.com/ru/post/497782/


Интересные статьи

Интересные статьи

Когда тот, кто работает в сфере Data Science, собирается показать результаты своей деятельности другим людям, оказывается, что таблиц и отчётов, полных текстов, недостаточно для того чтоб...
Как быстро определить, что на отдельно взятый сайт забили, и им никто не занимается? Если в подвале главной страницы в копирайте стоит не текущий год, а старый, то именно в этом году опека над са...
Все вокруг говорят про голосовых помощников, Алису, Google Assistant, что они умеют, чего не умеют… А мы взяли и написали фреймворк для создания мобильных голосовых ассистентов. Да еще и с открыт...
Андрей Мершин до сих пор зол на собак. «Ну, вообще-то я их люблю», – говорит греческо-русский ученый в своем уютном офисе в Mассачусетском институте (MIT). – «Но они меня просто уделывают». ...
Привет, народ! Представляю вам разработанный мной прототип кошачьих глаз. Конечно, проект еще не идеальный (находится на стадии доработки), но успешно работает. Задумка была – создать робота...