Добрый день, уважаемый Хабровчанин. Хочу рассказать тебе о своей работе, которой обычно занимаются студенты последних курсов технических ВУЗов (да-да, именно то нехорошее слово на букву «Д»).
Целью работы была разработка системы очувствления и управления мобильным роботом. За сими громкими словами стоит не очень большая, но для меня интересная задача.
Ближе к сути. Имеем микропроцессор, пачку датчиков, шаговый движок и необходимо, чтобы микропроцессор считывал данные с датчиков (акселерометры и гироскопы), отсылал данную информацию на ПК, принимал с компьютера команду управления движком, вращал движок.
Закупка:
Свой выбор остановил на следующих комплектующих:
• STM32F3DISCOVERY, так как имеет на борту уже установленный акселерометр и гироскоп. Да и под STM32 имеется уже много готовых примеров, что должно было облегчить задачу (отчасти облегчило).
• Цифровые акселерометры LIS331DH, 3ех осевые, высокоточные (от 2g до 8g). Вообще почти вся серия LIS* очень хороша и подходит под требования.
• Шаговый движок FL42STH25-0404A, ну тут что на любимой кафедре завалялось, то и пошло в дело.
Интересный момент, что в процессе работы искал статьи и информацию именно по STM32F3, и удивился, что ее не так много, как ожидалось (к примеру, по STM32F4 в разы больше примеров и информации). Да вы скажите, что там почти никакой разницы, и будете отчасти правы, но работа с периферией у них оказывается в некоторых местах разная. Поэтому я и решил внести свои 5 копеек по работе с этим микропроцессором.
Потихонечку разбираемся:
Достаем STM32F3DISCOVERY из коробочки, подключаем к ПК и запускаем. Демопрограмма показывает, что при отклонениях лампочки мигают, то есть датчики работают. Кричим «Ура!» и лезем в код разбираться и собственно реализовывать необходимое.
А необходимого много, но сначала решил остановиться на том, чтобы достучаться до внешних датчиков (не бортовых). Распаяли акселя, подключаем. У акселей есть 2 интерфейса для подключения: SPI и I2C. Решил остановиться на SPI, т.к. с ним уже приходилось иметь дело на ATTINY2313 (реализовывал его программно) и думал, что уж с аппаратным SPI вообще проблем не должно быть.
Хотел как проще, оказалось как всегда
Подключение: MISO – MISO, MOSI – MOSI, SCK – SCK, CS можно вешать на любую ногу, так как будем дергать его программно.
Сначала нам надо проинициализировать SPI. В данном примере работа идет с SPI2, так как через первый SPI работает встроенный гироскоп (или аксель, точно не помню):
void SPI2_Configuration(void)
{
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOB, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_PinAFConfig(GPIOB, GPIO_PinSource13, GPIO_AF_5); // SCK
GPIO_PinAFConfig(GPIOB, GPIO_PinSource14, GPIO_AF_5); // MISO
GPIO_PinAFConfig(GPIOB, GPIO_PinSource15, GPIO_AF_5); // MOSI
SPI_InitTypeDef SPI_InitStructure;
SPI_StructInit(&SPI_InitStructure);
SPI_I2S_DeInit(SPI2);
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_16b;
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI2, &SPI_InitStructure);
/* Configure the Priority Group to 1 bit */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
/* Configure the SPI interrupt priority */
NVIC_InitStructure.NVIC_IRQChannel = SPI2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
/* Initialize the FIFO threshold */
SPI_RxFIFOThresholdConfig(SPI2, SPI_RxFIFOThreshold_QF);
SPI_Cmd(SPI2, ENABLE);
}
Пытаемся прочитать данные с регистра WHO_AM_I:
getValue = getRegisterValue(&AXELx, 0x0F);
где
char getRegisterValue(AXEL_TypeDef* AXELx, char address)
{
AXELx->CS_Port->BRR = AXELx->CS_Pin;
SPI_I2S_SendData16(SPI2, 0x8000|(address<<8));
while(!SPI_I2S_GetFlagStatus(SPI2,SPI_I2S_FLAG_RXNE));
AXELx->CS_Port->BSRR = AXELx->CS_Pin;
return SPI_I2S_ReceiveData16(SPI2);
}
Тут необходимо отметить важный нюанс, что надо вовремя дергать CS акселерометра, к которому обращаемся, так как прижимание CS к земле инициализирует начало передачи данных (именно из-за этого момента у меня возникли жестокие затыки и проблемы, плюс не все акселя удачно запаялись и часть оказалась нерабочими, что застопорило мою работу примерно недели на две. О_о ). Потом отправляем адрес регистра, с которым будем работать (читать/писать), вторым байтом читаем или пишем.
Ну а писать будем так:
void setRegisterValue(AXEL_TypeDef* AXELx, char address, char data)
{
AXELx->CS_Port->BRR = AXELx->CS_Pin;
SPI_I2S_SendData16(SPI2,((short)address<<8)|(short)data);
while(!SPI_I2S_GetFlagStatus(SPI2,SPI_I2S_FLAG_RXNE));
AXELx->CS_Port->BSRR = AXELx->CS_Pin;
SPI_I2S_ReceiveData16(SPI2);
}
Для корректной работы датчики тоже надо проинициализировать, а именно указать, что будем читать по всем трем осям и указать рабочую частоту (значение управляющего слова и его формирование смотрим в даташите на аксель).
void Axel_Init(AXEL_TypeDef* AXELx)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* Enable Axel CS Pin */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_Pin = AXELx->CS_Pin;
GPIO_Init(AXELx->CS_Port, &GPIO_InitStructure);
setRegisterValue(AXELx, 0x20, 0x27);
}
С датчиками закончили, ура! Теперь давайте перейдем к управлению шаговыми двигателями.
Тише едешь – дальше будешь
Для управления ШД использовался драйвер VNH3SP30. Правда он позволяет управлять только одной из двух обмоток шагового двигателя, поэтому нам понадобится 2 таких платки.
Таким образом, для управления одной обмоткой нам понадобится 3 выхода с микроконтроллера (один несущей частоты и 2 направления), на весь двигатель – 6.
Дефайним порты для удобства:
#define A_PULSE_PORT GPIOB
#define A_PULSE_PIN GPIO_Pin_2
#define A_DIR1_PORT GPIOB
#define A_DIR1_PIN GPIO_Pin_0
#define A_DIR2_PORT GPIOE
#define A_DIR2_PIN GPIO_Pin_8
#define B_PULSE_PORT GPIOE
#define B_PULSE_PIN GPIO_Pin_12
#define B_DIR1_PORT GPIOE
#define B_DIR1_PIN GPIO_Pin_10
#define B_DIR2_PORT GPIOE
#define B_DIR2_PIN GPIO_Pin_14
И инициализируем их:
void StepMotorSetup()
{
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOB |
RCC_AHBPeriph_GPIOE, ENABLE);
/*
Инициализация выводов МК для управления ШД
*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitStructure.GPIO_Pin = A_PULSE_PIN;
GPIO_Init(A_PULSE_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = B_PULSE_PIN;
GPIO_Init(B_PULSE_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = A_DIR1_PIN;
GPIO_Init(A_DIR1_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = B_DIR1_PIN;
GPIO_Init(B_DIR1_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = A_DIR2_PIN;
GPIO_Init(A_DIR2_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = B_DIR2_PIN;
GPIO_Init(B_DIR2_PORT, &GPIO_InitStructure);
}
Для того чтобы сделать 1 шаг двигателем необходимо в нужном порядке включать обмотки двигателя, то есть подавать управляющие сигналы на драйвера
Маска управляющих сигналов следующая:
u8 WireConFullStep[4][4] = {{1, 0, 0, 0},
{0, 0, 1, 0},
{0, 1, 0, 0},
{0, 0, 0, 1}};
А теперь делаем шаг в нужном направлении. Направление в данном случае определяется направлением обхода по маске управляющих сигналов:
void DoStep(int index)
{
GPIO_ResetBits(A_DIR1_PORT, A_DIR1_PIN);
GPIO_ResetBits(A_DIR2_PORT, A_DIR2_PIN);
GPIO_ResetBits(B_DIR1_PORT, B_DIR1_PIN);
GPIO_ResetBits(B_DIR2_PORT, B_DIR2_PIN);
GPIO_ResetBits(A_PULSE_PORT, A_PULSE_PIN);
GPIO_ResetBits(B_PULSE_PORT, B_PULSE_PIN);
if(WireConFullStep[index][0] == 1)
{
GPIO_SetBits(A_DIR1_PORT, A_DIR1_PIN);
GPIO_SetBits(A_PULSE_PORT, A_PULSE_PIN);
}
if(WireConFullStep[index][1] == 1)
{
GPIO_SetBits(A_DIR2_PORT, A_DIR2_PIN);
GPIO_SetBits(A_PULSE_PORT, A_PULSE_PIN);
}
if(WireConFullStep[index][2] == 1)
{
GPIO_SetBits(B_DIR1_PORT, B_DIR1_PIN);
GPIO_SetBits(B_PULSE_PORT, B_PULSE_PIN);
}
if(WireConFullStep[index][3] == 1)
{
GPIO_SetBits(B_DIR2_PORT, B_DIR2_PIN);
GPIO_SetBits(B_PULSE_PORT, B_PULSE_PIN);
}
}
Определим еще для удобства шаг против часовой и шаг по часовой стрелке:
void CWStep()
{
DoStep(CurIndex);
CurIndex+=1;
if(CurIndex > 3)
CurIndex = 0;
}
void CCWStep()
{
DoStep(CurIndex);
CurIndex-=1;
if(CurIndex < 0)
CurIndex = 3;
}
А теперь напишем функцию, с помощью которой будем вращать двигатель на нужное количество шагов в нужном направлении:
void Steps(u8 dir, s16 n)
{
s16 i = 0;
for(i = 0; i < n; i++)
{
if(dir)
{
CWStep();
udelay(15000);
}
else
{
CCWStep();
udelay(15000);
}
}
}
В данной функции вставлены временные задержки, чтобы шаговый двигатель успевал сделать шаг, прежде чем нами будет послана команда следующего шага.
Власти в наших руках становится все больше и больше и мы переходим к следующему этапу – отправка данных на ПК и управление ШД с ПК.
Общение по USB
Для работы с USB использовал один из примеров работы с USB, а именно VirtualComport_Loopback (искать на просторах интернета в комплекте STM32 USB-FS-Device development kit). В данном демо подключённый stm32 к ПК определялся как виртуальный ком-порт, и отправлял в обратную все получаемые данные. Ну что же, это нам отлично подходит! Берем данный пример, разрываем петлю обмена и вуаля – пользуемся.
Единственная проблема, которая возникла – приложение на .Net не хотело подключаться к виртуальному ком-порту, если микропроцессор постоянно опрашивал датчик и слал данные на ПК (интересно, что сторонняя программа Hercules, которой я пользовался для отладки отлично открывала порт). Поэтому я решил добавить ожидание нажатия User Button, после которого уже начинался постоянный опрос датчиков и обмен информацией с ПК.
Собственно получился примерно следующий код:
Инициализация USB:
Set_System();
Set_USBClock();
USB_Interrupts_Config();
USB_Init();
Ждем пока не нажмем User Button:
while (1)
{
/* Data exhange via USB */
if (bDeviceState == CONFIGURED && UserButtonPressed != 0)
{
…
}
}
Обработчик на нажатие UserButton:
__IO uint32_t i =0;
extern __IO uint32_t UserButtonPressed;
void EXTI0_IRQHandler(void)
{
if ((EXTI_GetITStatus(USER_BUTTON_EXTI_LINE) == SET)&&(STM_EVAL_PBGetState(BUTTON_USER) != RESET))
{
/* Delay */
for(i=0; i<0x7FFFF; i++);
/* Wait for SEL button to be pressed */
while(STM_EVAL_PBGetState(BUTTON_USER) != RESET);
/* Delay */
for(i=0; i<0x7FFFF; i++);
UserButtonPressed++;
if (UserButtonPressed > 0x2)
{
UserButtonPressed = 0x0;
}
/* Clear the EXTI line pending bit */
EXTI_ClearITPendingBit(USER_BUTTON_EXTI_LINE);
}
}
Заключение
В данной статье я опустил многие моменты по распиновке и подключению устройств друг к другу, схемы плат и некоторые другие детали (работа с АЦП) и постарался сделать акцент на работу с периферией. К сожалению, собранный рабочий макет был сдан в ВУЗ (будем надеяться, что последующие поколения заинтересуются данной работой и продолжат ее), в результате чего я не могу продемонстрировать его работу, но у меня сохранилось несколько фото. Вот к примеру фото, когда мы проводили эксперимент по определению амплитуды ускорения при перемещении физической модели транспортного средства по синусоидальной поверхности с разной жесткостью пневмоподвески.
Также приложу проект для IAR под STM32F3. Там присутствует много «дурного» кода, так как писалось в большей части по принципу «лишь бы заработало, да поскорее». За любые комментарии по коду, и не только, буду благодарен.