Я продолжаю, а этой статьей наверное заканчиваю описание конструкции самодельного 3D сканера, который был описан в этой статье. Вообще почти два года назад, когда мы только начинали этот проект, подобных сканеров было не так много, как сегодня. Так что описывать механику не имеет смысла (она у всех подобных сканеров одинакова), а софтварную часть писал не я. А эта статья еще может помочь тем, кто все-же решит собирать сканер сам. Для них и написана эта статья.


О чем


Тут не будет картинок, схем или даже котиков. Хотя нет. Будут.
image

А все потому, что дальше будет только код. Здесь я приведу программу только для четвертого сканера, ибо прошивка третьего сканера утеряна (именно после того случая я и стал делать бэкапы). Как вы наверное помните на плате для четвертого сканера стоит микроконтроллер STM32F401RE, соответственно для семейства F4 и будет написан код.

Библиотеки


Сначала мы должны подключить все необходимые библиотеки:
Библиотеки
#include «stm32f4xx.h»
#include «stm32f4xx_exti.h»
#include «stm32f4xx_gpio.h»
#include «stm32f4xx_rcc.h»
#include «stm32f4xx_tim.h»
#include «stm32f4xx_usart.h»
#include «misc.h»

Из названия библиотек видно, что они одинаковые для всего семейства STM32F4 (а значит код без проблем переносится на любой другой микроконтроллер этого семейства. Например на STM32F407VB). Второе, что видно из названий — это название периферии, для которой эти библиотеки нужны. Особняком стоит библиотека misc.h. Эта библиотека нужна для обработки прерываний.

Инициализация периферии и переменных


Начнем с объявления глобальных переменных:
Переменные
int Step;
int DelayTime=100000;
char ConfigState;
uint8_t StepsPerComand=1;
uint8_t LaserPower = 0;
uint8_t LightPower = 0;

Переменная Step показывает, на каком шаге сейчас находится мотор. DelayTime задает время между шагами. Это нужно для того, чтобы мотор успевал прокрутить вал. Задается в количестве тактов процессора. Про ConfigState и StepsPerComand я расскажу попозже. LaserPower и LightPower задают мощность лазера и подсветки соответственно (подсветка реализована не была, но ШИМ все равно выводится).
Глобальные переменные на этом закончились. Перейдем к функциям. Сначала пропишем вот такую незамысловатую, но полезную функцию:
Delay
void Delay (uint32_t n)
{
uint32_t i;
for (i=0;i<n;i++) {}
}

Думаю комментарии излишни. Далее функция инициализации пинов, к которым подключен мотор. У меня это A5, A6, A7 и B6:
Пины мотора
void InitMotorGPIO (void)
{
GPIO_InitTypeDef MotorGPIO;

RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);

MotorGPIO.GPIO_Pin = GPIO_Pin_5|GPIO_Pin_6|GPIO_Pin_7;
MotorGPIO.GPIO_Mode = GPIO_Mode_OUT;
MotorGPIO.GPIO_OType = GPIO_OType_PP;
MotorGPIO.GPIO_PuPd = GPIO_PuPd_DOWN;
MotorGPIO.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOA, &MotorGPIO);

MotorGPIO.GPIO_Pin = GPIO_Pin_6;
GPIO_Init(GPIOB, &MotorGPIO);
}

Далее мотором необходимо управлять. Для этого есть вот такие функции:
Управление мотором
void MotorResetGPIO (void)
{
GPIO_ResetBits(GPIOA, GPIO_Pin_5|GPIO_Pin_6|GPIO_Pin_7);
GPIO_ResetBits(GPIOB, GPIO_Pin_6);
}

void MotorCoil (int coil)
{
MotorResetGPIO();
switch (coil)
{
case 1: GPIO_SetBits(GPIOA, GPIO_Pin_5); break;
case 2: GPIO_SetBits(GPIOA, GPIO_Pin_6); break;
case 3: GPIO_SetBits(GPIOA, GPIO_Pin_7); break;
case 4: GPIO_SetBits(GPIOB, GPIO_Pin_6); break;
}
}

void MotorStepUP (int n)
{
int i;
for (i=0;i<n;i++)
{
Step++;
if (Step>4) {Step=1;}
MotorCoil(Step);
Delay (DelayTime);
}
}

void MotorStepDOWN (int n)
{
int i;
for (i=0;i<n;i++)
{
Step--;
if (Step<1) {Step=4;}
MotorCoil(Step);
Delay(DelayTime);
}
}

Функция MotorResetGPIO сбрасывает в 0 все пины мотора. Эта функция используется в следующей по порядку функции MotorCoil. Эта функция выставляет лог. 1 на один из пинов мотора. Так как каждому пину соответствует катушка шагового двигателя, то соответственно мы включаем одну из катушек. Если делать это в правильном порядке (а именно в этой функции он и задается), то двигатель будет совершать шаги.
Последние две функции (MotorStepUP и MotorStepDOWN) принимают на вход число — количество шагов, которые надо совершить.
После функция для мотора мы инициализируем USART:
Заголовок спойлера
void InitUsartGPIO (void)
{
GPIO_InitTypeDef UsartGPIO;

RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);

GPIO_PinAFConfig(GPIOA, GPIO_PinSource2, GPIO_AF_USART2);
GPIO_PinAFConfig(GPIOA, GPIO_PinSource3, GPIO_AF_USART2);

UsartGPIO.GPIO_OType = GPIO_OType_PP;
UsartGPIO.GPIO_PuPd = GPIO_PuPd_UP;
UsartGPIO.GPIO_Mode = GPIO_Mode_AF;
UsartGPIO.GPIO_Pin = GPIO_Pin_2|GPIO_Pin_3;
UsartGPIO.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &UsartGPIO);
}

void InitUsart (void)
{
InitUsartGPIO();

USART_InitTypeDef USART_InitStructure;

RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);

USART_InitStructure.USART_BaudRate = 9600;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART2, &USART_InitStructure);
USART_Cmd(USART2, ENABLE);

NVIC_InitTypeDef UsartNVIC;

UsartNVIC.NVIC_IRQChannel = USART2_IRQn;
UsartNVIC.NVIC_IRQChannelPreemptionPriority = 2;
UsartNVIC.NVIC_IRQChannelSubPriority = 2;
UsartNVIC.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&UsartNVIC);

USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);
}

Комментировать тут тоже особо нечего. A2 — Rx, A3 — Tx. Скорость — 9600 бод. Разве что можно обратить внимание на последние 9 строчек. Там мы инициализируем прерывание. Оно будет возникать при приеме данных по USARTу.
Последний блок функций — это функции для управления лазером и подсветкой. Для этого необходимо настроить таймер и вывести ШИМ на интересующие нас ноги (микроконтроллера):
Инициализация таймера
void InitLaserGPIO (void)
{
GPIO_InitTypeDef LaserGPIO;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC, ENABLE);
GPIO_PinAFConfig(GPIOC, GPIO_PinSource7, GPIO_AF_TIM3);

LaserGPIO.GPIO_Mode = GPIO_Mode_AF;
LaserGPIO.GPIO_OType = GPIO_OType_PP;
LaserGPIO.GPIO_Pin = GPIO_Pin_7;
LaserGPIO.GPIO_PuPd = GPIO_PuPd_UP;
LaserGPIO.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_Init (GPIOC, &LaserGPIO);
}

void InitLightGPIO (void)
{
GPIO_InitTypeDef LightGPIO;
GPIO_PinAFConfig(GPIOB, GPIO_PinSource4, GPIO_AF_TIM3);

RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
LightGPIO.GPIO_Mode = GPIO_Mode_AF;
LightGPIO.GPIO_OType = GPIO_OType_PP;
LightGPIO.GPIO_Pin = GPIO_Pin_4;
LightGPIO.GPIO_PuPd = GPIO_PuPd_NOPULL;
LightGPIO.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_Init(GPIOB, &LightGPIO);
}

void InitLaserAndLight(void)
{
InitLaserGPIO();
InitLightGPIO();

TIM_TimeBaseInitTypeDef BaseTIM;

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);

BaseTIM.TIM_Period=0xFF;
BaseTIM.TIM_Prescaler=3;
BaseTIM.TIM_CounterMode=TIM_CounterMode_Up;
BaseTIM.TIM_ClockDivision=0;
TIM_TimeBaseInit(TIM3,&BaseTIM);

TIM_OCInitTypeDef TimOC;

TimOC.TIM_OCMode=TIM_OCMode_PWM1;
TimOC.TIM_OutputState=TIM_OutputState_Enable;
TimOC.TIM_Pulse=0;
TimOC.TIM_OCPolarity=TIM_OCPolarity_High;

TIM_OC1Init(TIM3,&TimOC);
TIM_OC1PreloadConfig(TIM3,TIM_OCPreload_Enable);

TimOC.TIM_OutputState = TIM_OutputState_Enable;
TimOC.TIM_Pulse=0;

TIM_OC2Init(TIM3, &TimOC);
TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable);

TIM_ARRPreloadConfig(TIM3,ENABLE);
TIM_Cmd(TIM3, ENABLE);
}

void SetLaserPower (uint8_t p)
{
TIM3->CCR2 = p;
}

void SetLightPower (uint8_t p)
{
TIM3->CCR1 = p;
}

Сначала инициализируем GPIO (C7 для лазера и B4 для подсветки). Затем настраиваем таймер. А последние две функции весьма понятно называются.
На этом периферия заканчивается. Осталось сделать функцию тотального выключения всего (чтоб не грелось когда не надо):
Тотальное выключение
void FullReset (void)
{
MotorResetGPIO();
SetLaserPower(0);
SetLightPower(0);
}

Ну и конечно же main:
main
int main(void)
{
InitMotorGPIO();
InitLaserAndLight();
InitUsart();

while(1)
{
}
}

Только что-то маленький какой-то код в main, не находите? Это все от того, что обработка вся идет в прерываниях. Вернее всего в одном, которое мы инициализировали, когда настраивали USSART. Рассмотрим обработчик этого прерывания:
Нужно больше case!!!
void USART2_IRQHandler (void)
{
char data;
data=USART_ReceiveData(USART2);
if (data<10) {MotorStepUP(data);}
if (ConfigState){
switch (ConfigState){
case 'd': DelayTime = data; break;
case 'p': LaserPower = data; SetLaserPower(LaserPower); break;
case 'i': StepsPerComand = data; break;
case 'l': LightPower = data; break;
}
ConfigState = 0;
USART_SendData(USART2, 'R');
}
else {
switch (data){
case 's': MotorStepUP(StepsPerComand); break;
case 'n': SetLaserPower(LaserPower); break;
case 'f': SetLaserPower(0); break;
case 'r': FullReset(); break;
case 'b': MotorStepDOWN(StepsPerComand); break;
case 'h': SetLightPower(LightPower); break;
case 'u': SetLightPower(0); break;
default: ConfigState=data;
}
if (ConfigState) {USART_SendData(USART2, '#');}
else {USART_SendData(USART2, 'R');}
}
USART_ReceiveData(USART2);
data = 0;
USART_ClearITPendingBit(USART2, USART_IT_RXNE);
}

Разбираемся построчно. Сначала завели переменную data. Затем записали в нее данные с USART. Эм. А это что за магическое число 10? А тут небольшая хитрость. Если число (код символа) меньше 10, то мотор совершит количество шагов, равное коду символа. Далее идет проверка ConfigState. И второй раз пропустим эту переменную, а рассмотрим то, что записано в else. Здесь набор команд:
s — сделать StepsPerComand шагов вперед (это та переменная, которую мы пропустили в начале статьи).
n — включить лазер (мощность указана в LaserPower).
f — выключить лазер
r — ТОТАЛЬНОЕ ВЫКЛЮЧЕНИЕ (эх, хорошо звучит).
b — сделать StepsPerComand шагов назад.
h — включить подсветку (мощность указана в LightPower).
u — выключить подсветку.
Если же команда не подошла ни под одно из условий выше, то символ записывается в переменную ConfigState. Вернемся теперь снова к условию в 4й строчке функции. Если ConfigState не 0, то мы настраиваем параметр. Задаем новое значение. Если пришло d, то мы задаем новое DelayTime, если p, то новую мощность лазера (причем эта мощность сразу устанавливается на лазере, т.е. он включается), если пришло i, то меняем переменную StepsPerComand, ну а если l, то меняем мощность подсветки. Причем когда что-то записывается в ConfigState, микроконтроллер посылает в терминал '#', чтобы показать, что он ждет второй символ. Если же он просто выполнил команду, то приходит ответ 'R'.
В конце мы просто очищаем буфер приема и сбрасываем флаг прерывания.
На этом код заканчивается.

Финал


Вот собственно и все. Надеюсь этот материал поможет тем, кто решит повторить мой опыт (или кого заставят преподы). Я не претендую на идеальное решение, или на то, что все сделано правильно. Конечно на каждом этапе есть то, что можно сделать лучше. Так что лучше не просто повторять, но и самим дописывать/допаивать.
Удачи всем в их разработках и проектах! Надеюсь это хоть немного кому-нибудь поможет.

Комментарии (6)


  1. KonstantinSoloviov
    20.04.2015 10:57

    Вы крутите мотором в обработчике прерывания — это абсолютно недопустимо.
    Такой код не имеет развития. Готов спорить, что даже на 9600 теряются команды.

    И отдельное спасибо за

    ConfigState = 1;
    — отличная попытка вынести мозг тому, кто будет читать ваш код )


    1. Hellitron Автор
      20.04.2015 12:01

      Извините, с ответом промахнулся.


  1. Hellitron Автор
    20.04.2015 12:00

    Программа на компьютере ждёт ответа от микроконтроллера, а он приходит только после выполнения команды. А с ConfigState я как-то пропустил, ткните пожалуйста где, а то я найти не могу.


    1. KonstantinSoloviov
      20.04.2015 12:48
      +1

      Здесь,

      if (data<10) {ConfigState = 1; MotorStepUP(data);}

      выставляется ConfigState = 1, дальше проваливаемся в if затем в switch которые со свистом проскакиваем, что бы сбросить ConfigState в 0 и отправить 'R' — оно работает конечно, но все это лишняя сложность для читающего код.

      Программа на компьютере ждёт ответа от микроконтроллера, а он приходит только после выполнения команды.

      Понятно, можно и так — синхронный режим. Два моторчика одновременно так не покрутить.


      1. Hellitron Автор
        20.04.2015 21:20

        Да, забавная ошибка:) Сейчас исправлю. Спасибо.


        1. KonstantinSoloviov
          21.04.2015 13:49

          Стало еще хуже.