Pull to refresh

Первые шаги с STM32 и компилятором mikroC для ARM архитектуры — Часть 3 — UART и GSM модуль

Reading time 11 min
Views 26K
Сейчас немного научившись программировать под наш микроконтроллер, попробуем связать его с внешним миром. Аппаратные интерфейсные модули STM32 поддерживают много много различных внешних интерфейсов. Начнем с самого часто используемого интерфейса UART. Что это за интерфейс можно прочитать здесь и здесь.

Попробуем подключить наш МК к компьютеру посредством UART. Для этого я использую простой USB — UART TTL (3.3V) (не забываем что у нашего МК уровни — 3.3 V ) конвертер на базе микросхемы PL2303.
image
В STM32 имеется 3 UART интерфейса. Перед тем как его использовать необходимо сконфигурировать наш последовательный приемопередатчик в МК. Для этого используем команду:

UARTх_Init(baud_rate);

Эта команда запускает наш UART на скорости baud_rate (например 9600 или 19200 kbs) со стандартными параметрами (8 бит данных, без аппаратного контроля четности.)

Если нужно запустить UART в каком-то нестандартном режиме, то используем:

UARTx_Init_Advanced(baud_rate, data_bits, parity,  stop_bits);

здесь:

  • baud_rate — скорость порта;
  • data_bits — количество бит данных, возможно использовать структуры _UART_5_BIT_DATA, _UART_6_BIT_DATA, _UART_7_BIT_DATA, _UART_8_BIT_DATA;
  • parity — контроль четности (_UART_NOPARITY, _UART_EVENPARITY, _UART_ODDPARITY) соответственно без контроля четности, контроль по четности, контроль по нечетности;
  • stop_bits — количество стоп бит (_UART_ONE_STOPBIT, _UART_TWO_STOPBITS) — 1 или 2 стоп-бита соответственно;

Чтобы передать байт данных в наш UART используем:


UARTx_Write(unsigned int _data);
UARTx_Write(char _data);

или для массива данных (не важно текст или набор значений) передаем функции:

UARTx_Write_Text(char * UART_text); 

используя указатель на маcсив.

Напишем простую программу:


char ourtext[] = "Hellow world";
char *txt_pnt;
void main()
 {
    UART1_INIT (19200); 
    delay_ms (5); // ждем 5 мс
    txt_pnt = &ourtext;
    UART1_Write_Text(txt_pnt);
    while (1)
      {
      }
{

Подключаем RX STM32 к TX конвертера и ТX STM32 к RX конвертера и вставляем его в USB порт нашего компьютера. Открываем терминальную программу (Я использую PuTTY), выбираем режим SERIAL, скорость порта, например, 19200 kbs и COM порт который появился при подключении нашего USB-TTL конвертера (можно посмотреть в диспетчере устройств в ветке «Порты COM и LPT») и нажимаем «OPEN». Видим наш текст в окне терминала.

image

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

USART1_SR. За окончание передачи отвечает бит USART1_SRbits.TC Функция UARTx_Write_Text (char * UART_text) сама контролирует, перед передачей следующего, завершение передачи предыдущего байта.

Для чтения данных с UART микроконтроллера используется функция:

char UARTx_READ()

Чтобы знать что в буфере нашего UARTа есть принятый байт используем функцию:

UARTх_Data_Ready()
возвращающую 1 если принятый байт есть в буфере и 0 если там его нет. В данном случае нам необходимо постоянно контролировать принял ли что-либо UART нашего контроллера. Давайте попробуем при приеме, например, буквы «А» (ASCII код 65) зажечь светодиод на плате:


void main()
{
  GPIO_Digital_Output(&GPIOb_BASE, _GPIO_PINMASK_1);
  UART1_INIT (19200);
  delay_ms (5);
  UART1_Write_Text("WAIT CHAR A");
  
	while(1)
      {
        if (UART1_Data_Ready()) // если есть данные в буфере приема UART
          {
            if (UART1_READ()== 65) // если принят код 65
              {
               GPIOb_ODR.b1 = 1; // зажигаем светодиод
              }
           }
       }
}


Чтобы постоянно не занимать МК опросом состояния регистра UART можно (и нужно) использовать прерывание. При приеме очередного байта вызовется аппаратное прерывание от UART, в котором мы можем обработать данные, принятые поп последовательному интерфейсу. Для включения прерывания по событию «RX UART» нам нужно записать в бит RXNEIE регистра CR1 используемого модуля UART логическую 1.


 UART1_init (19200);
 NVIC_IntEnable(IVT_INT_USART1); // Иннициализируем вектор прерывания IVT_INT_USART1
 EnableInterrupts(); // Разрешаем прерывания
 USART1_CR1bits.RXNEIE = 1; // Разрешаем прерывания по приему байта
 delay_ms (5);

как и в случае с таймером, при возникновении прерывания вызывается подпрограмма:


void our_int() iv IVT_INT_USART1
  {
    if (UART1_READ()== 65) GPIOb_ODR.b1 = 1; // Если приняли A = зажигаем светодиод
    if (UART1_READ()== 66) GPIOb_ODR.b1 = 0; // Если приняли B = гасим светодиод
  }

При помощи UART можно связывать наш МК не только с компьютером, но и с многими периферийными устройствами. Для примера рассмотрим широкодоступный GSM модуль SIM800L. Мне достался данный модуль уже распаянный на небольшой плате. Такой недорого можно заказать в Китае. На штырьки этой платы выведены все необходимые для работы с модулем сигналы, к тому же плата имеет держатель под СИМ карту.



Единственное что не удобно, это необходимость для питания данной платы напряжения от 3,7 до 4,2 В. Так что придется или подключать внешнюю литиевую батарею, или городить преобразовать с выходным напряжением 4,1 В. Учитывая широкое распостранение регулируемых DC-DC это, думаю, не составит особой проблемы.

Подключим наш модуль у МК, не забывая что RX модуля идет на TX МК и наоборот. Для Maple mini модуль подключаем RX к 26 пину (PA9 / UART1TX), TX к 25 (PA10 / UART1RX).

Для общения с модулем используются, так званые, AT команды. Это набор терминальных команд, которые понимает модем и выполняет в ответ на них определенные действия. С командами нашего модуля можно ознакомится в даташите, к тому же есть неплохая статья где неплохо расписана работа с данным модулем.

Мы будем использовать 2 UART микроконтроллера, UART1 для связи МК -GSM и UART3 для связи МК — терминал компьютера.

Иннициализируем наши UARTы


void main() 
{
  UART1_init (19200); // Включаем UART для связи с компьютером
  UART3_init (19200); // Включаем UART для связи с GSM модулем
  NVIC_IntEnable(IVT_INT_USART1); // Настраиваем прерывание по приему байта от GSM
  USART1_CR1bits.RXNEIE = 1;
  EnableInterrupts();
  delay_ms(200);
  
  while (1)
     {

     }
}

Наша программа потребует относительно большого количества функций, поэтому вынесем их в отдельный с-файл. Для этого нажмем Ctrl+N в Microc, создадим в проекте еще один файл, например «GSM_SIM800L.с». Для того чтоб наш компилятор понял, что часть функций нужно искать в другом файле, допишем в начале основного с-файла строку (аналогично можно и подключать библиотеки.).


#include "GSM_SIM800L.с"

Для начала, в файле «GSM_SIM800L.с» напишем функцию передающую модулю АТ команду и принимающую от него ответ.


static char GSM_TEMP[256];  // Буфер приема от GSM модуля
unsigned short com_status;  // Статус выполнения команды 0 - пускай будет ошибка, 1 - ОК, 2 - нет ответа от модуля (виснет он иногда)
unsigned short com_mode = 0; 0 - ответ на запрос, 1 - асинхронный ответ 

void Clear_GSM_Bufer () // Функция очищает буфер GSM
 {
   unsigned int cnt;
   for (cnt=0; cnt<256; cnt++) GSM_TEMP[cnt] = 0;
   cntas = 0;
  }

short int gsm_send_at (char *at_cmd) // передаем нашу AT команду, в MicroC текстовая строка передается в функцию указателем 
 {
    unsigned int brk; // переменная времени  ожидания ответа от модуля
    Clear_GSM_Bufer (); // очищаем буфер
    com_status = 2; //начальное состояние "нет ответа"
    com_mode = 0; // ждем ответа на запрос
    UART1_Write_Text (at_cmd); // передаем нашу AT - команду
    while (com_status == 2 && brk < 50000) // ждем com_status станет равным 1 или 0
      {
        brk++; 
        delay_us(2);
      }
    com_mode = 1; // ответ на запрос получен, принимаем асинхронные ответы
    return com_status; // возвращаем статус
 }

void gsm_read() iv IVT_INT_USART1
 {
    GSM_TEMP[cntas] = UART1_READ(); // читаем принятый байт UART в массив GSM_TEMP 
    if (cntas> 255) cntas = 0; else cntas++; // Инкриментируем индекс элемента массива и следим чтобы индекс не вышел за размер массива
    if (com_mode == 0) // Если ждем ответ на запрос
      {
      if (strstr(GSM_TEMP, "OK\r\n") != 0) com_status = 1; // ОК -  com_status = 1
      if (strstr(GSM_TEMP, "ERROR\r\n") != 0) com_status = 0; //Error -  com_status = 0
      }
}

В MicroC строка это массив символов char который заканчивается значением 00 (NULL) означающем конец строки. Строка как аргумент функции задается указателем на тип char (например char *nasha_stroka). MicroC поддерживает функции стандартной Си — шной библиотеки С_String.h. Например выше использованна функция


char *strstr(char *s1, char *s2);

эта функция ищет подстроку, заданную указателем *s2 в строке*s1. Функция вернет указатель на найденную подстроку *s2. (фактически вернет строку от начала *s2 до конца (NULL символа) строки *s1). Если подстрока *s2 не найдена то функция вернет NULL . По умолчанию в С передача массива по его имени это указатель на начало массива. Если нужно передать массив c определенного элемента, то используем (&array_name[ind]).

Для начала непроходимо дать модулю простую команду «AT\r» — Модуль на нее должен ответить «OK \r\n» и согласовать скорость своего UART. Любая АТ команда заканчивается символом перевода строки "\r". Ответ модуля на любую команду может состоять из нескольких строк и всегда заканчивается «OK\r\n» или «ERROR\r\n». Сообщение модуля, как то сообщение о входящем звонке или СМС всегда заканчивается "\r\n", причем жизнь упростило то, что информативна обычно только последняя строка. Прием таких строк, я называю асинхронным ответом (не совсем корректно, но пускай так).

Выполним несколько команд, необходимых для конфигурации модуля. Для простоты не будем контролировать ответы модуля, хотя несложно при выполнении каждой команды проверять статус на равенство 1;

  • «ATE0\r» — Отключим эхо (повтор на выход UARTa нашей команды)
  • «AT+GMM \r» — Вернет название модуля
  • «AT+CMGD=1,4\r» — Удаляем все СМС, сохраненные в нашем модуле
  • «AT+CMGF=1 \r» — Включаем для СМС режим ASCII, иначе нам сложнее будет их читать в символах Юникода
  • «AT+DDET=1» — Включаем распознание модулем DTMF команд


gsm_send_at ("AT\r");
gsm_send_at ("ATE0\r");
gsm_send_at ("AT+CMGD=1,4\r");
gsm_send_at ("AT+CMGF=1\r");
gsm_send_at ("AT+DDET=1\r");

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


char *gsm_at_parse (char *at_cmd, char *beg_str, char *end_str)
// *at_cmd = наша команда
// *beg_str = символы перед нужной нам частью строки
// *beg_str = символы после нужной нам части строки  
 {
   char *tempchar;
   if (gsm_send_at (at_cmd) == 1) // отправляем команду
     {
      tempchar = strstr (GSM_TEMP, beg_str); // ищем beg_str
      tempchar = tempchar + strlen(beg_str); 
      *(strstr (tempchar, end_str)) = 0; // ставим  NULL символ перед началом end_str
      return tempchar;
     }
 }

Например «AT+GMM \r\n» вернет название модуля в виде "\r\n SIM_800L \r\n\r\nOK\r\n". «Откусим» начальные "\r\n" и конец "\r\n\r\nOK\r\n"


UART3_Write_Text (gsm_at_parse("AT+GMM\r", "\r\n", "\r\n\r\nOK\r\n")); \\ Выведет в UART3 (в наш Терминал на ПК, имя модуля)
UART3_Write_Text ("\r\n");
UART3_Write_Text (gsm_at_parse("AT+CCLK?\r", ",", "+")); \\Выведет текущее время на часах модуля
UART3_Write_Text ("\r\n");
UART3_Write_Text (gsm_at_parse("AT+CSPN?\r", ": \"", "\""); \\Выведет название оператора сети GSM.
UART3_Write_Text ("\r\n");

Среди АТ команд модуля существует команда «AT+CREG?\r». Данная команда вернет состояние модуля о регистрации в сети. Если все ОК и модуль зарегистрирован ответ должен быть таким: "\r\n+CREG: 0,1\r\n\r\n\OK\r\n". 0,0 — модуль не зарегистрирован и нет поиска сети (например не вставлена SIM карта), 0,2 — модуль не зарегистрирован, идет поиск сети. Напишем функцию проверки состояния сети модуля, кроме этого вернем уровень сигнала GSM сети (для этого есть АТ команда «AT+CSQ\r»).


short int gsm_net_status ()
 {
    char *tempchar[7]; // временная переменная 
    int sgr; // уровень сигнала в сети в dBi
    if ( gsm_send_at("AT+CREG?\r") == 1) // выполним запрос состояния сети
       {
         if (strstr(GSM_TEMP, "0,1")) // если сеть есть  -  модуль вернул "+CREG: 0,1" 
           {
              tempchar = GSM_AT_PARSE ("AT+CSQ\r",": ","," ); //Запросим уровень сигнала, вернет строку типа +CSQ: 17,0, нужное нам значение между : и ,
              sgr = atoi(tempchar); // преобразуем строку в число
              sgr = -115 + (sgr * 2); // переведем число в  dBi 
              return sgr; // вернем значение
           }
       }
    return 0; // иначе вернем 0 - нет сети или модуль не зарегистрирован. 
  }

Теперь наш модуль работает. Если мы позвоним на него то он выдаст в свой UART строку типа
"\r\n RING\r\n +CLIP: "+380XXXXXXXX",145,"",,"",0\r\n" . При входящей СМС ответ будет "\r\n+CMTI: «SM»,10\r\n", где 10 означает что наша смс сохранена в памяти под номером 10. Это названые мной асинхронные ответы. мы их будем принимать и обрабатывать в обработчике нашего прерывания. Я написал распознавание наиболее нужных мне ответов. в принципе, как это делается, должно быть понятно из кода ниже.


char *eol // временная переменная, индекс символов "\r\n"
char callingnumber[14]; // номер входящего абонента, 13 символов  и NULL символ
char last_sms[4]; // Номер принятой СМС
char dtmf_in[2]; //Последняя приятая DTMF команда

unsigned short callnow; // входящий вызов
unsigned short active_call; // активный звонок
unsigned short sms; // принято смс сообщение

void gsm_read() iv IVT_INT_USART1
  {
   
    GSM_TEMP[cntas] = UART1_READ();
    if (cntas> 255) cntas = 0; else cntas++;
    if (com_mode == 0)
      {
      if (strstr(GSM_TEMP, "OK\r\n") != 0) com_status = 1;
      if (strstr(GSM_TEMP, "ERROR\r\n") != 0) com_status = 0;
      } // описаный ваше прием ответа на АТ команду

    if (com_mode == 1)
      {
        if (cntas> 2) eol = strstr(&GSM_TEMP[2], "\r\n"); else eol = 0; // ищем конец строки
        if (eol != 0) //если принята полная строка
          {
             if (strstr(&GSM_TEMP, "+CLIP") != 0) // если в ней нашли "+CLIP" то это сообщение о входящем звонке
              {
                callnow=1; //есть входящий звонок
                temp =  strstr(GSM_TEMP, "\"+"); // ищем в буфере знак + - с него начинается номер 
                strncpy (callingnumber, temp+1, 13); // копируем в массив callingnumber[] наш номер, длинной 13 сиволов 
              }

            if (strstr(&GSM_TEMP, "CARRIER") != 0) // если получили "NO CARRIER"
              {
                *callingnumber=0; // очистили строку с входящим номером (вписали NULL в начало строки)
                callnow=0; //  вызов и звонок окончен
                active_call=0; 
              }

            if (strstr(&GSM_TEMP, "DTMF") != 0)
              {
                temp =  strstr(GSM_TEMP, " "); //SIM800 умеет сам распознавать DTMF команды, в таком случае ищем в терминале сообщение о распознанной DTMF команде. 
                strncpy(dtmf_in,temp+1,1); //копируем в переменную dtmf_in[]
              }

            if (strstr(GSM_TEMP, "CMTI")!=0) // если приняли СМС
              {
                temp =  strstr(GSM_TEMP, ","); //ищем ",", она как раз перед номером СМС
                strncpy (last_sms, temp + 1, eol - (temp + 1)); //копируем в переменную last_sms[]
                sms=1; //принято СМС сообщение
              }
              Clear_GSM_Bufer (); //чистим буфер
          }
        }
    }

Контролируя во цикле программы значение переменной callnow можно узнать о входящем звонке. Поднять трубку мы можем при помощи команды «ATA\r», сбросить вызов - «ATH0\r».


сhar *nashomer[14] = "+380931234567"


     if (callnow == 1)
         {
             if (strcmp(&callingnumber, "nashomer")==0) // strcmp (*s1, *s2) - сравнивает 2 строки, вернет 0 если каждый символ s1 = s2
                {
                   gsm_send_at ("ATA\r"); // AT команда "Взять трубку"
                   active_call = 1; //ставим флаг означающий что сейчас идет звонок.
                }
            else
                {
                   gsm_send_at ("ATH0\r"); // AT команда "Повесить трубку"
                   callnow = 0; //сбросим флаг входящего звонка
                }
         }

Аналогично контролируя значение переменной sms узнаем о входящем смс сообщении. прочитать его мы можем командой «AT+CMGR=2\r» где N = номер СМС сообщения. В ответ модуль отдаст строку типа "\r\n+CMGR: «REC READ»,"+380XXXXXXXX" ,"" ,«17/02/1,11:57:46+2»\r\n hellow habrahabr \r\n\r\nOK\r\n". Для разбора этой строки создадим функцию:


char *gsm_read_sms (char *sms_num, unsigned short int sms_field)
// передаем номер смс (как строка) и поле которое нужно вернуть (целое число), 
//1 - номер отправителя, 
//2 - время получения смс, 
//3 - текст смс 
 {
    char sms_at[30];
    short int res_at;
    strcat (sms_at, "AT+CMGR=");
    strcat (sms_at, sms_num);
    strcat (sms_at, "\r");
    res_at = gsm_send_at (sms_at);
    if (res_at == 1 && sms_field == 1)
       {
         *(strstr (GSM_TEMP, "\",\"\",\"")) = 0;
         sms = 0;
         return strstr(GSM_TEMP, "\"+") + 1;
       }
    if (res_at == 1 && sms_field == 2)
       {
        *(strstr(strstr(GSM_TEMP, "\",\"\",\"") + 6, "+")) = 0;
         sms = 0;
         return strstr(GSM_TEMP, "\",\"\",\"") + 6;
       }
     if (res_at == 1 && sms_field == 3)
       {
        *(strstr (GSM_TEMP, "\r\n\r\nOK\r\n")) = 0;
         sms = 0;
         return strstr(GSM_TEMP, "\"\r\n") + 3;
       }
    return 0; //если ошибка вернем 0
}

Например:


UART3_Write_Text (gsm_read_sms("5",1)); // в терминал вернет номер отправителя смс с номером 5
UART3_Write_Text (gsm_read_sms("2",3)); // в терминал вернет текст смс с номером 2

Для отправки СМС используется АТ команда «AT+CMGS=»+380XXXXXXXX\r". Эта команда в ответ выдает символ "\r\n>" — приглашение к вводу. Можно ловить это приглашение в ответе модуля, я же просто даю задержку в 100 мс. и отправляю в UART текст смс который должен окончится символом с ASCII кодом 26


UART1_Write_Text ("AT+CMGS=\"");
UART1_Write_Text (nomer); // номер получателя смс
UART1_Write_Text ("\"\r");
Delay_ms (100);
UART1_Write_Text (text); //текст нашей смс
UART1_Write (26); Символ окончания ввода

В статье описана только малая часть возможностей как GSM модуля так и UART интерфейса МК STM32. Тем не менее, я надеюсь, что изложенная выше информация будет полезна для понимания работы с последовательным интерфейсом реализованном в микроконтроллере.

В следующей статье я расскажу что такое шина I2C, как работать с ней и как подключить LCD индикатор на основе контроллера HD44780 к STM32 по параллельной шине и через I2C расширитель линий ввода/вывода.
Tags:
Hubs:
+19
Comments 4
Comments Comments 4

Articles