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

Зараз трохи навчившись програмувати під наш мікроконтролер, спробуємо пов'язати його з зовнішнім світом. Апаратні інтерфейсні модулі STM32 підтримують багато різних зовнішніх інтерфейсів. Почнемо з самого часто використовуваного інтерфейсу UART. Що це за інтерфейс можна прочитати здесь і тут.

Спробуємо підключити наш МК до комп'ютера за допомогою UART. Для цього я використовую простий USB — UART TTL (3.3 V) (не забуваємо що у нашого МК рівні — 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); 

використовуючи вказівник на масив.

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


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 конвертера і ТХ STM32 до RX конвертера і вставляємо його в USB порт нашого комп'ютера. Відкриваємо термінальну програму (Я використовую Pytty), вибираємо режим 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; // OK - 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.


сһаг *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 розширювач ліній введення/виводу.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.