The second part of this tutorial introduced our AT parser and the idea of a GSM engine. For our final chapter in exploring GSM we will go through the adaptation of the engine for a specific GSM module. All adaptation should reside inside a single file. In this case, that single file is named gsm_adapter.c. The implementation requires some deep analysis and depends heavily on the device that we want to use. AT commands are a standard, but they vary from device to device. Usually, all products from same manufacturer follow the same convention for AT commands. Differences do arise between models of GSM modems and have to be adapted for our use.
Configuration
Before we start the adaptation of our GSM engine for the GSM2 click board, we are going examine the configuration file. This configuration is found in the at_config.h header file included with our source files. This file allows you to modify settings for GSM devices and there are also parameters important used to adapt our engine for different types of MCUs.
#define AT_HEAD "AT" #define AT_HEAD_SIZE 2 #define AT_CMS_ERROR "+CMS ERROR:" #define AT_CMS_ERROR_SIZE 11 #define AT_HEADER_SIZE 15 #define AT_STORAGE_SIZE 50 #define AT_TRANSFER_SIZE 1024 #define DEFAULT_TIMEOUT 500
First of all, let's focus on parameters that are important for our MCU. There are two parameters of that kind :
- AT_TRANSFER_SIZE - represents the amount of the data that will be transferred per one sequence of communication between MCU and GSM.
- AT_STORAGE_SIZE - represents size of storage for our commands that we want to register and assign a callback function to them.
Transfer size also represent the size of our RX / TX Buffers. Both of these parameters have an impact to RAM usage. Transfer size of 1024 bytes will take 2 kB ( RX + TX ) of your RAM space for buffers and every member of storage will take about 15 bytes of space. If you are limited with memory, pay attention to this variable and assign the proper size appropriate to your MCU. Inside the compiler there is a message that could very helpfully in this situation. A message is generated and displayed by the compiler after each successful build. It will summarize the amount of memory used by application. Trying different values and re-compiling will show how that change will affect the memory consumed.
It is also important for GSM modems to use a DEFAULT_TIMEOUT which represents the time after when the application can stop waiting for response. This information can be found inside the datasheet for your particular model.
AT_HEADER_SIZE represents the maximum size of the AT command without parameters provided. This variable can improve the speed of your library because parsing of the responses will not go beyond this length. If you make this variable smaller, then some of your registered commands will be never recognized.
AT_CMS_ERROR represent the general error string. Response with this kind of error message does not have a standard structure and usually has no summary sent after it.
Adaptation
GSM 2 click caries Quectel M95 FA GSM/GPRS module which supports GSM850MHz, GSM900MHz, DCS1800MHz or PCS1900MHz quad-band frequencies with 85.6 kbps GPRS data transfer. The module also contains a quadrupole audio/microphone jack, SMA antenna connector, and SIM card socket. GSM 2 Click communicates with the target microcontroller via seven lines and can use either 3.3V or 5V power supplies.
The main goal of adaptation inside the gsm_adapter.c is to create an algorithm inside the at_adapter_rx that will recognize the end of a response and set the response flag ( response_f ). The Response flag is checked inside the engine process which should be in infinite while loop. When the response flag is set, it starts the parsing process and execution of any callbacks registered to it.
The at_adapter_rx will be placed inside the UART receive interrupt. Implementation of any additional functions should avoided due to it could can take large amounts of time and miss any incoming characters.
The at_adapter_tx function will be used for sending data and inside it we can place the things like termination
After a deep and detailed analysis of the device documentation it can be concluded that the GSM2 click responses can be divided to 4 different classes. Some of responses are finished with a summary which can be ERROR or OK string, but some of them can only consist of response data that are indications received by the GSM, such as a new SMS indication. Also there are responses like RING, NO CARRIER that are also types of GSM modem indications received without request.
AT+CMGF=1<// Header ( Echo ) OK // Response summary
AT+CSCA// Header +CMS ERROR: 604 // Response data
+CMTI: "SM",6// Response data
AT+CMGR=6// Header +CMGR: "REC UNREAD","+381xxxxxxx", "24/04/16" // Response data Message Text OK // Response summary
There are several solutions how to implement this. Generally your at_adapter_rx function should have a simple algorithm, without complex operations, like copying large strings. The goal of this function is to recognize the end of a response and set our response_f after the last character is received.
void at_adapter_rx( char rx_input ) { if( rx_input == 'r' ) term_f = true; if( rx_input == 'n' && term_f ) { term_f = false; frag_f = true; } if( !frag_f ) { if( !head_f && !data_f && !summ_f ) { if( rx_input == 'A' ) { head_f = true; } else if( rx_input == 'O' || rx_input == 'E' || rx_input == 'B' || rx_input == 'C' || rx_input == 'R' || rx_input == 'N' || rx_input == 'P' ) { summ_f = true; } else if ( rx_input == '+' || data_t ) { data_f = true; } } if( rx_input == '>' ) { exception_f = true; } } if( head_f ) { if( !head_t ) head_t = true; rx_buffer[ rx_idx++ ] = rx_input; } if( data_f ) { if( !data_t ) { data_t = true; data_ptr = &rx_buffer[ rx_idx ]; } rx_buffer[ rx_idx++ ] = rx_input; } if( summ_f ) { if( !summ_t ) summ_t = true; rx_buffer[ rx_idx++ ] = rx_input; } if( frag_f ) { if( head_f ) head_f = false; if( data_f ) { while ( *error ) { if( error[ err_c ] == *( data_ptr + err_c++ ) ) { err_f = true; } else { err_f = false; break; } } if( err_f ) { rx_buffer[ rx_idx ] = ''; response_f = true; } if( !head_t ) { rx_buffer[ rx_idx ] = ''; response_f = true; } err_c = 0; data_f = false; } if( summ_f ) { rx_buffer[ rx_idx ] = ''; response_f = true; } frag_f = false; } if( rx_idx == AT_TRANSFER_SIZE ) { rx_buffer[ rx_idx ] = ''; response_f = true; } }
For our at_adapter_tx implementation, things are more simple. We have functions that will loop through the request string until it arrives. At the end of the string, and after that, adds terminators at the end of the request depending on whether we are writing a request or transmitting an SMS.
int at_adapter_tx( char tx_input ) { if( tx_input != '' ) { tx_buffer[ tx_idx++ ] = tx_input; } else { if( !exception_f ) { tx_buffer[ tx_idx++ ] = 'r'; tx_buffer[ tx_idx++ ] = 'n'; tx_buffer[ tx_idx ] = ''; } else { tx_buffer[ tx_idx++ ] = 0x1A; tx_buffer[ tx_idx++ ] = 'r'; tx_buffer[ tx_idx ] = ''; } while( !gsm_tx_ctl() ); gsm_hal_write( tx_buffer ); tx_idx = 0; exception_f = false; response_f = false; cue_f = true; return 1; } if ( tx_idx == AT_TRANSFER_SIZE ) { tx_buffer[ tx_idx ] = ''; while( !gsm_tx_ctl() ); gsm_hal_write( tx_buffer ); tx_idx = 0; return 0; } return 0; }
After we finishing adaptation of the gsm_adapter.c we can perform the very first test of the implementation. Interrupts are not part of standard program execution. Usage of a debugger in this case becomes difficult so it is best to avoid debugging and use "old school" style with LEDs or UART transmissions.
Before the test, datasheets need to be checked again for information pertaining to hardware expectations of pin input / output operation:
- Pulling a pin high or low
- Delay before switching to operation mode.
- Delay between power on and operation mode allowing for the firmware to initialize.
The best place for this kind of procedure is the gsm_adapter_init function which is called by gsm_engine_init .
void at_adapter_init( void ) { gsm_hal_init(); gsm_pwr_ctl( true ); Delay_ms( 100 ); gsm_pwr_ctl( false ); Delay_ms( 2500 ); gsm_pwr_ctl( true ); Delay_ms( 12500 ); at_adapter_reset(); err_c = 0; err_f = false; strcpy( error, ( char* )AT_CMS_ERROR ); memset( ( void* )tx_buffer, 0, AT_TRANSFER_SIZE ); memset( ( void* )rx_buffer, 0, AT_TRANSFER_SIZE ); }
The test program should have an infinite while loop with gsm_process inside it, and gsme_engine_init must be the first function called after hardware initialization. Engine initialization expects a function as an argument so we have to declare the function used for default callback that will be provided to the engine init function. Inside the default callback, we will implement printing with the some other UART. And we can do the same thing inside the UART interrupt routine for the communication between the MCU and GSM so we can easy monitor what exactly we are receiving from the GSM.
#include gsm_engine.h sbit GSM_PWR at GPIOC_ODR.B2; sbit GSM_CTS at GPIOD_ODR.B13; sbit GSM_RTS at GPIOD_IDR.B10; voit dbg_cb( char *response ); void system_init( void ); void dbg_cb( char *response ) { UART1_Write_Text( " < CALLBACK >rn" ); UART1_Write_Text( response ); } void system_init() { GPIO_Digital_Output( &GPIOC_ODR, _GPIO_PINMASK_2 ); GPIO_Digital_Output( &GPIOD_ODR, _GPIO_PINMASK_13 ); GPIO_Digital_Input( &GPIOD_IDR, _GPIO_PINMASK_10 ); Delay_ms( 100 ); // UART bus for monitoring UART1_Init( 57600 ); Delay_ms( 200 ); /// UART bus for GSM UART3_Init_Advanced( 9600, _UART_8_BIT_DATA, _UART_NOPARITY, _UART_ONE_STOPBIT, &_GPIO_MODULE_USART3_PD89 ); Delay_ms( 200 ); RXNEIE_USART3_CR1_bit = 1; NVIC_IntEnable( IVT_INT_USART3 ); EnableInterrupts(); } void main() { system_init(); gsm_engine_init( dbg_cb ); at_cmd( "AT" ); while( 1 ) { gsm_process(); } } // UART interrupt for GSM void LO_RX_ISR() iv IVT_INT_USART3 ics ICS_AUTO { if( RXNE_USART3_SR_bit ) { char tmp = USART3_DR; at_adapter_rx( tmp ); UART1_Write( tmp ); } }
If everything is implemented properly, we should have printed every response twice on UART for monitoring. Once when it is received, and once when callback is executed.
API Implementation
With the engine prepared, we know that each of our response headers( echo ) or indications will be parsed and compared to the strings saved in the parser storage. This string should be recognized by the parser and the callback saved. The callback will be executed with an argument that is the actual response.
We can now easily implement the API, or even our application with the functions that will be triggered by events. For example, if we want to have a blinking led we can create a function that toggles the LED on the board and assign that function as a callback for a "RING" response from the module.
sbit LED0 at GPIOB_ODR.B0; void gsm2_api_init() { GPIO_Digital_Output( &GPIOB_BASE, _GPIO_PINMASK_0 ); gsm_engine_init( gsm2_callback ); gsm2_audio_enable(); at_cmd_save( "RING", 1000, NULL, NULL, NULL, gsm2_cb_ring ); } void gsm2_callback( char *response ) { UART1_Write_Text( response ); } void gsm2_cb_ring( char *response ) { LED1 = ~LED1; } void gsm2_audio_enable() { at_cmd( "AT+QAUDCH=2" ); } void gsm2_call_answer() { at_cmd( "ATA" ); } void gsm2_call_hangup() { at_cmd( "ATH" ); }
As you can see we have moved the engine initialization to the API because this module is the proper place for callback functions. We have defined a default callback function that will be executed any time the parser does not recognize a response. Also we have defined the callback that will be executed when a modem response is the "RING" string. Because of that, we have saved that string with a function provided as a execute callback.
We have also three additional functions. Two of them are going to be executed on some user interaction and one of them is placed inside the init and will be executed at the library initialization. Our test program should be slightly modified and we will have our first application.
void main() { system_init(); gsm2_api_init(); while( 1 ) { gsm_process(); if( Button( &GPIOC_IDR, 9, 80, 1 ) ) answer_call = true; if( Button( &GPIOC_IDR, 6, 80, 1 ) ) hangup_call = true; if( answer_call ) { gsm2_call_answer(); answer_call = false; } if( hangup_call ) { gsm2_call_hangup(); hangup_call = false; } } }
That's it. We have blinking LED that report us a phone call - there will be also and sound on headphones or speaker connected to the GSM2 click's audio output. We have a button that is used to answer the call and a button for disconnecting the call.
Summary
This is a simple example of GSM engine usage with a GSM2 click. You will probably want to implement your own functionality such as printing the message on a TFT or creating a log of the GSM events. In that case, there are situations in which you should be careful. Callbacks that can use more time for execution, such as, writing the text on the screen, should be done by copying the response into a new variable to avoid data corruption from a new incoming response.
Also, usage of the MikroC Button library could be a better choice than implementation of an interrupt based user interactions. Errors can occur if interrupt based events are called multiple times.
The library packed for our compilers and examples of usage explained here can be downloaded from Libstock. Enjoy your favorite GSM click.
References
AT Command Reference for GSM2 click 2016
Hardware Design Reference for GSM2 click 2016
Libstock GSM2 Library 2016
GITHub Source for GMS2 Library 2016