Pull to refresh

STM32 + linux

Reading time 15 min
Views 160K
Для разработки системы управления одной железякой после длительных поисков мною был выбран ARM-микроконтроллер семейства STM32 — STM32F103 (в «стоножечном» исполнении). А в качестве макетки для разработки и отладки — STM32P103 (там ножек хоть и меньше, но ядро то же самое). «Истории успеха» я понемногу выкладывал в своей ЖЖшке, но вот решил собрать все воедино и рассказать о том, каково же оно — программировать микроконтроллеры в линуксе. Сам проект лежит на sourceforge.



Прежде всего коснусь общего, а потом уже перейду к деталям.

Итак, помимо макетки (или же готового устройства — когда оно будет готово) потребуется JTAG-адаптер. В моем случае это — ST-LINK/V2. Одного железа, естественно, недостаточно: надо еще как-то код компилировать, а потом еще и заливать на контроллер. Для этого были установлены компилятор gcc для ARM (arm-none-eabi) и утилита для работы с ST-LINK (она так и называется — stlink).

В качестве образца я взял этот проект. Отсюда я скачал простенькие демонстрационные проекты и попробовал скомпилировать простейший. А самым первым оказался стандартный Helloworld для МК: мигание светодиодом.

Сразу скажу, на какие грабли я напоролся с самого начала: я забыл про objcopy, без которого получить работающий код будет сложновато. После компиляции проекта обязательно надо создать бинарник при помощи этой утилиты. И не стоит выкидывать из Makefile'ов непонятных (и вроде бы даже ненужных на первый взгляд) целей.

В качестве удобного IDE я использую Geany. Так как на работе у меня два монитора, довольно удобно работать: на одном мониторе у меня открыт Geany с кодом, а на втором — терминал, где я запускаю make и com (терминал из tinyserial).

Весь Makefile я рассматривать не буду, обращу лишь внимание на то, что в нем следует менять:
Код
BIN=testproject
…
STM32_LIBSRC+=stm32_lib/misc.c
STM32_LIBSRC+=stm32_lib/stm32f10x_adc.c
#~ STM32_LIBSRC+=stm32_lib/stm32f10x_bkp.c
#~ STM32_LIBSRC+=stm32_lib/stm32f10x_can.c
#~ STM32_LIBSRC+=stm32_lib/stm32f10x_cec.c
#~ STM32_LIBSRC+=stm32_lib/stm32f10x_crc.c
#~ STM32_LIBSRC+=stm32_lib/stm32f10x_dac.c
#~ STM32_LIBSRC+=stm32_lib/stm32f10x_dbgmcu.c
STM32_LIBSRC+=stm32_lib/stm32f10x_dma.c
STM32_LIBSRC+=stm32_lib/stm32f10x_exti.c
#~ STM32_LIBSRC+=stm32_lib/stm32f10x_flash.c
#~ STM32_LIBSRC+=stm32_lib/stm32f10x_fsmc.c
STM32_LIBSRC+=stm32_lib/stm32f10x_gpio.c
#~ STM32_LIBSRC+=stm32_lib/stm32f10x_i2c.c
#~ STM32_LIBSRC+=stm32_lib/stm32f10x_it.c
#~ STM32_LIBSRC+=stm32_lib/stm32f10x_iwdg.c
#~ STM32_LIBSRC+=stm32_lib/stm32f10x_pwr.c
STM32_LIBSRC+=stm32_lib/stm32f10x_rcc.c
#~ STM32_LIBSRC+=stm32_lib/stm32f10x_rtc.c
#~ STM32_LIBSRC+=stm32_lib/stm32f10x_sdio.c
#~ STM32_LIBSRC+=stm32_lib/stm32f10x_spi.c
#~ STM32_LIBSRC+=stm32_lib/stm32f10x_tim.c
STM32_LIBSRC+=stm32_lib/stm32f10x_usart.c
#~ STM32_LIBSRC+=stm32_lib/stm32f10x_wwdg.c
…
SRC=hw_config.c  main.c leds.c  interrupts.c  usb_desc.c usb_istr.c \
	usb_prop.c  usb_pwr.c onewire.c
…
#~ OBJ+=stm32f10x_bkp.o
#~ OBJ+=stm32f10x_can.o
#~ OBJ+=stm32f10x_cec.o
#~ OBJ+=stm32f10x_crc.o
#~ OBJ+=stm32f10x_dac.o
#~ OBJ+=stm32f10x_dbgmcu.o
OBJ+=stm32f10x_dma.o
OBJ+=stm32f10x_exti.o
#~ OBJ+=stm32f10x_flash.o
#~ OBJ+=stm32f10x_fsmc.o
OBJ+=stm32f10x_gpio.o
#~ OBJ+=stm32f10x_i2c.o
#~ OBJ+=stm32f10x_it.o
#~ OBJ+=stm32f10x_iwdg.o
#~ OBJ+=stm32f10x_pwr.o
OBJ+=stm32f10x_rcc.o
#~ OBJ+=stm32f10x_rtc.o
#~ OBJ+=stm32f10x_sdio.o
#~ OBJ+=stm32f10x_spi.o
#~ OBJ+=stm32f10x_tim.o
OBJ+=stm32f10x_usart.o
#~ OBJ+=stm32f10x_wwdg.o

  • BIN — имя получающегося после компиляции бинарника
  • STM32_LIBSRC и OBJ содержат подключаемые файлы библиотеки STDPeriphLib, неиспользуемые надо закомментировать
  • SRC сдоержит перечень пользовательских исходников


После того, как код написан, запускаем make. Если все в порядке, в текущей директории появится файл $(BIN).bin, который и нужно записать во флеш-память МКшки. Запись выполняется при помощи make load: эта цель сборки просто вызывает st-flash для прошивки микроконтроллера.

USB


Итак, прежде всего необходимо наладить связь компьютера и МКшки. Учитывая то, что в современных компьютерах RS-232 отсутствует, отладочную связь организую посредством USB. Однако, в «боевых условиях» команды МКшка будет получать по RS-232 от другого контроллера, поэтому я решил сразу же смотреть в сторону организации эмулятора переходника USB<->RS-232. Этот подход удобен еще тем, что не надо заморачиваться с лишним кодом для взаимодействия с устройством по USB (хоть это и элементарно, но лень же!). Да и отлаживать просто: открываем устройство /dev/ttyACM0 как последовательный порт при помощи любого эмулятора последовательного терминала и «общаемся». Да, в качестве эмулятора терминала на первых порах (пока нет никакого ПО со стороны компьютера) я использовал tinyserial.

Отсюда я скачал код эмулятора переходника USB<->RS-232. Так как проверить работоспособность второй стороны (RS-232) я сразу не мог (некуда подключить), неиспользуемый код работы с USART временно закомментировал.

Для работы с USB используется библиотека от STMicroelectronics. Если не вникать в коды самой библиотеки, все довольно-таки просто: нам нужно переопределить дескрипторы для своей железяки (файлы usb_desc.[ch]), чтобы компьютер опознал ее как переходник USB<->RS-232, а также изменить обработчики прерываний на события USB (как минимум — обработать принятые данные, а для прозрачной работы в качестве переходника, надо будет еще добавить обработку прерываний USART для передачи полученных оттуда данных по USB).

Для передачи сообщений используем что-то вроде кольцевого буфера, который постепенно будет наполняться, а по необходимости — передаваться по USB. Считывать данные будем в «обычный буфер». Так как пока что я использую только короткие команды, обработкой длинных посылок я не заморачивался. Если же они будут, нужно будет немного усложнить обработчик прерывания по приему данных с USB.

Так как некоторые команды (например, чтение температуры с 1-wire датчиков) выполняются довольно-таки долго, обработчик пришедших по USB команд только модифицирует флаги для подобных операций, а уж основной цикл в main() эти флаги обрабатывает. Операции, выполняющиеся быстро (работа со светодиодом), вызываются непосредственно из этой функции. В отладочных целях я добавил «эхо» на команды в виде краткой ее расшифровки:
Код
void usb_handle_command(uint16_t cnt){
	uint8_t command, *answer;
	uint16_t i;
	for(i = 0; i < cnt; i++){
		command = USB_Rx_Buffer[i];
		switch(command){
			case CMD_LED_ON:
				LED_On();
				answer = (uint8_t*)"On";
			break;
			case CMD_LED_OFF:
				LED_Off();
				answer = (uint8_t*)"Off";
			break;
			case CMD_LED_BLINK:
				LED_OnBlink();
				answer = (uint8_t*)"Blk";
			break;
			case CMD_LED_DUTY_PLUS:
				LED_DutyPlus();
				answer = (uint8_t*)"Shn";
			break;
			case CMD_LED_DUTY_MINUS:
				LED_DutyMinus();
				answer = (uint8_t*)"Fad";
			break;
			case CMD_1W_GET_TEMP:
				FLAGS |= FLAG_READ_T;
				answer = (uint8_t*)"Read T";
			break;
			case CMD_1W_GET_DEV:
				FLAGS |= FLAG_GETDEV;
				answer = (uint8_t*)"find devices";
			break;
			case CMD_1W_PRNT_DEV:
				FLAGS |= FLAG_PRINTDEV;
				answer = (uint8_t*)"Print devices";
			break;
			case CMD_HALL_GET:
				FLAGS |= FLAG_PRINTHALL;
				answer = (uint8_t*)"Print Hall";
			break;
			case CMD_ADC_GET:
				FLAGS |= FLAG_PRINTADC;
				answer = (uint8_t*)"Print ADC val";
			break;
			default:
				answer = (uint8_t*)"Unk";
		}
		newline();
		prnt(answer);
		newline();
	}
}


Все, теперь при подключении макетки к компьютеру по USB (а она, вообще-то, у меня всегда подключена, т.к. питается через USB) появляется устройство /dev/ttyACM0, с которым можно работать, как с обычным последовательным портом. Например, открыть его при помощи последовательного терминала (как я уже выше сказал, на первых порах пользуюсь tinyserial).

Светодиод, кнопка


Наверное, традиционным является «помигать диодом» в начале изучения какой-нибудь новой железяки, поэтому и я сделаю так же. А заодно повешу на «user button» прерывание, которое будет менять режимы работы светодиода.

Просто мигать не интересно: интересно менять яркость. Для этого достаточно простого «софтового» ШИМа. Настроим таймер SysTick на период в 10мкс. Заведем два счетчика: один для количества «тиков», в течение которых светодиод горит, а второй — для количества «тиков», в течение которых светодиод не горит. Для изменения яркости свечения светодиода я сделал простейшую восьмиуровневую схему изменения скважности ШИМа.
Получилось вот что:
Код
uint8_t LED_GetState(){
	return led_state;
}
void LED_Duty(uint8_t duty){
	duty_cycle = duty;
	if(led_state == LEDSTATE_BLINK)
		LED_OnBlink();
}
void LED_DutyPlus(){
	if(duty_cycle < 7) duty_cycle++;
	if(led_state == LEDSTATE_BLINK)
		LED_OnBlink();
}
void LED_DutyMinus(){
	if(duty_cycle > 0) duty_cycle--;
	if(led_state == LEDSTATE_BLINK)
		LED_OnBlink();
}
uint8_t LED_GetBlinkState(uint16_t *blink_on, uint16_t *blink_off){
	*blink_on = led_blink_on;
	*blink_off = led_blink_off;
	return led_state;
}
void LED_On(){
	led_state = LEDSTATE_ON;
	led_blink_on = 0;
	led_blink_off = 0;
	GPIO_ResetBits(GPIOC, GPIO_Pin_12);
}
void LED_Off(){
	led_state = LEDSTATE_OFF;
	GPIO_SetBits(GPIOC, GPIO_Pin_12);
}
void LED_OnBlink(){
	led_blink_off = 1 << duty_cycle;
	led_blink_on = 0xff - led_blink_off;
	led_ticks_on = 0;
	led_ticks_off = 0;

	if(led_blink_off == 0){
		LED_On();
		return;
	}
	if(led_blink_on == 0)
	{
		LED_Off();
		return;
	}
	led_state = LEDSTATE_BLINK;
}
void LED_SysTick_Handler(){
	if(led_state != LEDSTATE_BLINK) return;
	if(led_ticks_on == 0)
		GPIO_SetBits(GPIOC, GPIO_Pin_12);
	if(led_ticks_on <= led_blink_on)	{
		led_ticks_on++;
		return;
	}
	if (led_ticks_off == 0){
		GPIO_ResetBits(GPIOC, GPIO_Pin_12);
	}
	if(led_ticks_off <= led_blink_off){
		led_ticks_off++;
		return;
	}
	led_ticks_on = 0;
	led_ticks_off = 0;
}


На «пользовательскую кнопку» я повесил внешнее прерывание:
Код
	// Enable the BUTTON Clock
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	// Configure Button pin as input
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	// Connect Button EXTI Line to Button GPIO Pin
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
	// Configure Button EXTI line
	EXTI_InitStructure.EXTI_Line = EXTI_Line0;
	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
	EXTI_InitStructure.EXTI_LineCmd = ENABLE;
	EXTI_Init(&EXTI_InitStructure);
	// Enable and set Button EXTI Interrupt to the lowest priority
	NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x0F;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x0F;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);

А его обработчик занимается тем, что переводит светодиод в режим непрерывного свечения, если тот «мигал», или наоборот — в режим «мигания», если тот светился:
Код
void EXTI0_IRQHandler(void){
	if(EXTI_GetITStatus(EXTI_Line0) != RESET){
		if(LED_GetState() != LEDSTATE_BLINK)
			LED_OnBlink();
		else
			LED_On();
		EXTI_ClearITPendingBit(EXTI_Line0);
	}
}


1-wire


Код для работы с 1-wire я стащил откуда-то с сайта easyelectronics.ru. Изменил я его совсем немного. Прежде всего, изменил функцию поиска устройств, висящих на шине (она в оригинале почему-то не работала, хотя логика вроде-бы вполне четкая и правильная была).

В сворованном мною примере 1-wire работала через USART, а для чтения/записи использовался DMA. Мне эта идея очень понравилась, поэтому воспользовался именно этим способом (хотя можно было организовать и программный протокол 1-wire).

Стандартная схема подключения 1-wire шины к последовательному порту подразумевает наличие диода Шоттки:

Однако, у меня такого диода не было. Но я углядел, что помимо push-pull режима можно отвечающую за USART_TX ногу перевести в режим с открытым стоком — в этом случае короткого замыкания не будет. Для работы с 1-wire я использовал USART3 (пока я балуюсь, ног мне хватает — поэтому remap делать не надо). На схеме я увидел, что ноги USART3 (PB10 и PB11) уже подтянуты к земле через резисторы по 10кОм, так что мне даже резистор припаивать не пришлось: только подпаял на макетку небольшую платку с гнездами, чтобы удобно было подключать термодатчики.

Подробно расписывать содержимое файла onewire.c не буду: это сделано уже до меня неоднократно, а коснусь лишь непосредственно работы с термометрами.

Для мониторинга температуры теплых (выше -50°C) частей устройства я решил воспользоваться простыми датчиками DS18S20 (заявленная точность измерения — не хуже 0.5°C). Подпаянную на макетку панельку я подключил к нужным выводам, чтобы можно было одновременно подключить к МКшке пару термометров.

Вот, например, что я получаю при работе с термометрами:
Код
com /dev/ttyACM0
C-a exit, C-x modem lines status
[STATUS]: RTS CTS DTR
 // жму 'c' :
find devices
Found 2 devices
 // жму 'p' :
Print devices
device 0:
0x10 0x7c 0xee 0x8f 0x02 0x08 0x00 0x1c
device 1:
0x10 0xad 0xbc 0x8f 0x02 0x08 0x00 0xf9
 // жму 't' :
Read T
Device 0: 0x3b 0x00 0x4b 0x46 0xff 0xff 0x08 0x10 0x39
Device 1: 0x3a 0x00 0x4b 0x46 0xff 0xff 0x0c 0x10 0x41


Для начала я промаркировал все термометры, чтобы знать, какой идентификатор у кого. А после я решил посмотреть, сильно ли различаются их показания. Температура — первые два байта ответа датчиков. В седьмом байте хранится «остаток» от преобразования температуры встроенным в термометр АЦП. По документации на датчики, этот остаток помогает уточнить значение температуры. Однако, выяснилось, что толку от него — как от козла молока.

В процессе работы датчик сам нагревается, что сказывается на результатах измерения. Поэтому не стоит слишком часто их опрашивать. Кроме того, показания датчиков отличались друг от друга вплоть до полутора градусов! Это нужно иметь в виду: если планируется использовать несколько датчиков так, чтобы мониторить разницу температур между участками чего-то с точностью не хуже 0.5°C, предварительно надо все датчики откалибровать. И показания брать по калибровочным формулам, а не ответу датчиков.

Реальная погрешность датчика иной раз превышает 0.5°C, поэтому все-таки лушче считать, что датчик имеет точность в 1°C.

Датчик Холла


Датчики Холла у меня аналоговые — SS495A. Спецификации на датчик можно найти в интернете. Скажу лишь, что в нормальном состоянии на его выходной ноге напряжение составляет около 2.5В (логическая единица STM32), в зависимости от полярности и величины внешнего магнитного поля он будет изменять свои показания в пределах 0..5В. Учитывая то, что напряжение на выходе может достигать пяти вольт, надо использовать не обычные, а «пятивольтовые» (обозначены как FR в спецификации) входы контроллера.

Имеющиеся у меня магниты (специально для этого датчика) при помещении их рабочей поверхности в пределах 1мм от «морды» датчика (маркированная сторона) приводили к появлению на его выходе нулевого напряжения. Причем уровень логического нуля появляется в довольно-таки небольшой зоне по координатам в параллельной маркированной стороне датчика плоскости, т.е. точность позиционирования получается довольно приличной.

Для опытов я распаял на макетке один датчик. Питание его подключил к 5В, а сигнальный выход вывел на порт PC10, который не сгорит, если на него подать 5В. Для того, чтобы не дергать постоянно порт, я повесил на него прерывание (по аналогии с кнопкой). Обработчик прерывания просто выставляет соответствующий флаг, а уж в основном цикле, если этот флаг выставлен (т.е. магнит либо появился, либо покинул «поле зрения» датчика) проверяем, что у нас на PC10. Если там ноль (есть МП), пишем в терминал «Magnet», иначе пишем «clear». Еще можно принудительно проверить, есть датчик или нет его, нажав «h» в терминале.

АЦП


Помимо «теплых зон» мне еще надо будет измерять температуру в холодных (вплоть до 75К сверху). Для этого будут использоваться платиновые термосопротивления, подключенные к аналоговому коммутатору ADG506A. Ну и, естественно, мне стало интересно, насколько плох «родной» АЦП МКшки: нельзя ли его использовать для измерения температуры?

Примеров работы STM32 с АЦП полным-полно, я взял пример из STDPeriphLib. Будем запускать АЦП в режиме непрерывного преобразования, а результат заносить в память при помощи DMA. Время преобразования устанавливаю в самое большое (чтобы поточнее было), а сам вход АЦП пока что повешу на ногу PB0 (ADC8):
Код
	// 0. Configure ADC8 (PB0) as analog input (clocking GPIOB sat on in onewire.c)
	RCC_ADCCLKConfig(RCC_PCLK2_Div4);
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOB, ENABLE);
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	// 1. DMA for converted value (DMA1 clocking sat on at onewire.c)
	//RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
	DMA_DeInit(DMA1_Channel1);
	DMA_InitStructure.DMA_PeripheralBaseAddr = ADC1_DR_Address;
	DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)&ADC_value;
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
	DMA_InitStructure.DMA_BufferSize = 1;
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Disable;
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
	DMA_InitStructure.DMA_Priority = DMA_Priority_High;
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);
	DMA_Cmd(DMA1_Channel1, ENABLE);
	// 2. ADC1 config
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
	ADC_InitStructure.ADC_ScanConvMode = ENABLE;
	ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
	ADC_InitStructure.ADC_NbrOfChannel = 1;
	ADC_Init(ADC1, &ADC_InitStructure);
	// Connect ADC to ADC8 (PB0),
	ADC_RegularChannelConfig(ADC1, ADC_Channel_8, 1, ADC_SampleTime_239Cycles5);
	// Enable ADC1 DMA
	ADC_DMACmd(ADC1, ENABLE);
	ADC_Cmd(ADC1, ENABLE);
	// Calibration of ADC1
	ADC_ResetCalibration(ADC1);
	while(ADC_GetResetCalibrationStatus(ADC1));
	ADC_StartCalibration(ADC1);
	while(ADC_GetCalibrationStatus(ADC1));
	ADC_SoftwareStartConvCmd(ADC1, ENABLE); // turn conversion on


Для работы с коммутатором нужно сконфигурировать пять бит управляющего порта. Чтобы не париться с преобразованием бит, я просто взял первые четыре бита порта C в качестве адреса, а пятый бит — в качестве ключа, включающего коммутатор:
Код
	GPIO_InitStructure.GPIO_Pin = 0x1f; // first 5 bits of PC0
	// PC0..PC3 - analog channel address, PC4 - analog enable switch
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_Init(GPIOC, &GPIO_InitStructure);


Прерываний здесь никаких не надо, а в файле interrupts.c надо дописать установку нового флага при поступлении команды (скажем, команды 'a') отображения напряжения на датчиках. В main() добавим обработку этого флага:
Код
inline void prntADC(){
	uint32_t address; // addr = 0, EN = 1
	uint8_t *_2b = (uint8_t *) &ADC_value;
	for(address = 0x10; address < 0x20; address++){
		// changhe channel address & turn on switch
		GPIOC->BSRR = address;
		Delay(2); // wait for AD conversion
		prnt((uint8_t*)"Temperature ");
		printInt(address&0x0f); prnt((uint8_t*)" = ");
		printInt(_2b[1]);
		printInt(_2b[0]);
		newline();
		// turn off switch & reset bits
		GPIOC->BRR = (uint32_t)0x1f;
		Delay(2);
	}
}

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

На отдельной макетке я собрал простой резистивный делитель напряжения, соединив все аналоговые входы коммутатора мелкоомными (200..900 Ом) резисторами. К S1 подключил «землю», а к S16 — +3.3В с макетки STM32. Запитал микросхему я старым БП от внешнего HDD (12В).

В макетке STM32P103 эталонное напряжение для АЦП берется от общего питания, поэтому точность получилась низкой: значения плавают иной раз аж на 20 единиц!

Вот, например, что получилось при двух опросах:
Код
// опрос 1
Temperature 0x00  = 0x00 0x00
Temperature 0x01  = 0x00 0x84
Temperature 0x02  = 0x00 0xaf
Temperature 0x03  = 0x01 0xdb
Temperature 0x04  = 0x03 0x10
Temperature 0x05  = 0x03 0xe4
Temperature 0x06  = 0x05 0xca
Temperature 0x07  = 0x06 0x9b
Temperature 0x08  = 0x07 0x4e
Temperature 0x09  = 0x08 0xd6
Temperature 0x0a  = 0x0a 0x04
Temperature 0x0b  = 0x0a 0xb4
Temperature 0x0c  = 0x0b 0xfc
Temperature 0x0d  = 0x0d 0xe0
Temperature 0x0e  = 0x0e 0xb7
Temperature 0x0f  = 0x0f 0xff


// опрос 2
Temperature 0x00  = 0x00 0x00
Temperature 0x01  = 0x00 0x7f
Temperature 0x02  = 0x00 0xaf
Temperature 0x03  = 0x01 0xdf
Temperature 0x04  = 0x03 0x0f
Temperature 0x05  = 0x03 0xe4
Temperature 0x06  = 0x05 0xcc
Temperature 0x07  = 0x06 0x9d
Temperature 0x08  = 0x07 0x5a
Temperature 0x09  = 0x08 0xd6
Temperature 0x0a  = 0x0a 0x01
Temperature 0x0b  = 0x0a 0xb5
Temperature 0x0c  = 0x0b 0xfc
Temperature 0x0d  = 0x0e 0x09
Temperature 0x0e  = 0x0e 0xb0
Temperature 0x0f  = 0x0f 0xec


В общем, надо будет либо попробовать добавить стабильный источник опорного напряжения (да и измеряемую цепь питать оттуда же), либо вообще использовать внешний АЦП. Учитывая низкое сопротивление датчиков, которые будут использоваться, все равно придется паять еще и усилитель.

Шаговый двигатель


С шаговиком я еще не закончил возиться, т.к. подозреваю, что при монтаже элементов на макетке у меня ничего «не взлетит». Надо паять. А пайкой займусь, скорее всего, только в следующем году (надо еще радиодеталей прикупить). Пока лишь вкратце расскажу, как планирую управлять шаговыми двигателями.

Шаговики у меня будут — VSS42 на 1.2 ампера. Управлять такими удобней всего при помощи драйвера ШД — L6208. При работе на эту микросхему надо подавать лишь сигналы управления направлением движения, сигнал разрешения работы да тактовые импульсы. Контроллер сам регулирует ШИМ и устанавливает нужные напряжения на обмотках двигателя.

Укажу пока основное, на что следует обратить внимание:
  • Регулировка ШИМа выполняется посредством сравнения падения напряжения на Sense-резисторах с опорным напряжением Vref. Поэтому для тока Imax и сопротивления резисторов RSense это падение можно рассчитать довольно просто:
    Uref = Imax · RSense
    Т.е. для выставления предельного тока в 1.2А при RSense=0.33Ом нужно задать Uref=0.4 В. Ни в коем случае нельзя оставлять ноги Vref висящими в воздухе или прижатыми к земле!
  • Режим Slow/Fast decay имеет значение для обычных коллекторных двигателей, шаговикам же Fast decay нужен лишь в режиме microstepping. В общем, если не извращаться, достаточно просто подать +5В на ножку CONTROL. На HALF/FULL тоже просто подаем +5В и работаем в полушаговом режиме. Аналогично поступаем с ногой RESET, если не хотим сбрасывать счетчик фаз (а его сбрасывать и не нужно, если честно отдавать на каждый шаг по 8 синхроимпульсов).
  • Вход ENABLE является еще и выходом: если с драйвером L6208 случается неприятность (перегрев, скачок тока), он самостоятельно отключает напряжение на нагрузке, а ENABLE подтягивает к земле. Это значит, что можно проверять, не случилась ли аварийная ситуация, если ногу контроллера, управляющую портом ENABLE, активировать в режиме выхода с открытым коллектором.
    По спецификации STM32, в режиме открытого коллектора при подаче единицы на выход порта просто запирается транзистор, подтягивающий ногу к земле. Если же на выход подать нуль, то нога опять подтягивается.
    Таким образом, подтянув ногу контроллера к +5В (ногу нужно выбирать FT) через, скажем, пятикилоомный резистор, а между ней и ENABLE воткнув, скажем, килоомный резистор (и обязательно не забыть шунтировать ногу ENABLE конденсатором на землю, иначе можно сжечь управляющий контроллер), можно и включать/выключать нужный двигатель, и проверять, не было ли аварий (а для этого можно повесить на соответствующие ноги контроллера прерывание периферии по спадающему фронту).
  • Проводники, помеченные в спецификации жирным, должны быть как можно короче и шире. Но при этом надо следить и за тем, чтобы уменьшить паразитные емкости и индуктивности.
    RSense должны располагаться как можно ближе к драйверу. Недалеко от них должны быть и конденсаторы C₁ и C₂ (по спецификации): через эту цепь течет не только ток сравнения, но и обратный ток индукции (поэтому, кстати, диоды в эту цепочку включать нельзя).
    Сигнальную землю соединять с силовой землей только далее точки подключения C₁ к земле, иначе ток индукции может попортить электронику просто падением напряжения на проводниках печатки (до меня, правда, не дошло, как это возможно, но лучше прислушаться к требованиям безопасности).
  • Еще я прочел интересную штуку: силовое питание нельзя подключать без сигнального! В документации рекомендуется даже брать напряжение 5 В от силового источника при помощи стабилизирующего блока.
    Я, честно говоря, ничего не понял: собственно +5 В подается лишь на управляющие ноги драйвера. Если мы не пользуемся двигателем, то, по идее, у него на всех сигнальных входах может быть 0. Но включать силовое питание таки буду после включения питания контроллера. И выключать в обратной последовательности. И следить, чтобы USB-шнурок, питающий контроллер, не выскочил...
Tags:
Hubs:
+21
Comments 21
Comments Comments 21

Articles