The basic idea of a GSM engine is to make something that will control a modem's behavior. Like an engine for a car that can be used inside another car the parsing engine is for AT commands. For that kind of job, we need to focus on creating the basic core. The core provides the lower level workings which allow us to work at a higher level. There are many types of devices that use AT commands in order to control the modem and communication with other devices. This library was built in this manner, and can be used with other radios not only GSM modems.
GSM Engine
As we said at the introduction of this series about the GSM devices, AT commands are the most common method of controlling modems. Creating the parser for AT commands can be a good starting point but the parser alone can not be enough to control every aspect of communication.
Modems usually use serial communication and inside the embedded world it is known as UART protocol. The second thing we need is the HAL (hardware access layer) for UART access. This will make our library independent to the platform or compiler used. Hardware flow control is a part of serial communication, although it is not required, it can bring a lot of freedom to operations that can take large amounts of time - for example - string comparisons.
The third and most important part of communication is to ensure functionality in unusual circumstances. Sometimes, there might be an error in data transfer, so the modem does not recognize a command and will not respond to a request. For that kind of job, we need a timer that will count the time from our request. If the amount of time is larger than we have specified for the modem to respond, we want to stop excepting responses.
Finally, there are differences between modems, so we also must make some adaptations to our library that will ensure the functionality for those varying type of devices. We want place of of the adaptive operations in one separate file so something like an adapter is needed as well.
We will not go through the creation of HAL layer or timer but will pay attention to the at_parser.c as most vital and important part of our library. The gsm_adapter.c is the part that should be adapted to the specific GSM module will be explained in the next part of our tutorial.
https://github.com/MikroElektronika/GSM_Engine
AT Parser
The main job for the AT parser is to analyze the responses from the module. Depending on the returning response, it can be important to the user or intended to execute some type of task. That task can even be a callback function defined by the user. Actually that is the job for the engine, but we want our parser to return information as to what the engine should execute.
The idea is to store all AT commands that can be valuable for those module response by placing some of the responses in the MCU storage as well as the callback specified for what response will be executed. When we reviewed the chapter about AT commands, the conclusion was that there are 4 different types of commands that can be easy recognized by reading the string. So our type of data that will be stored could be type that have command string that will be used for comparison and 4 callbacks for each of the four different types commands.
typedef void ( *at_cmd_cb )( char *response );
typedef struct { uint32_t hash; uint32_t timeout; at_cmd_cb getter; at_cmd_cb setter; at_cmd_cb tester; at_cmd_cb executer; } at_cmd_t;
static at_cmd_t at_cmd_storage [ AT_STORAGE_SIZE ];
We have declared the function pointer of type callback - so every of our callbacks must have exact same definition. Our function pointer will have string arguments that will be used to provide the response to the callback.
We declared the type for storage with four callbacks in addition with hash and timeout, and have declared it in the storage area. Remember the previous section where we was discussing about the unusual occurrences where we might need a timeout after which we are not excepting response from the module anymore? So the member timeout is going to be used for storing that value.
You may be wondering where is the command string used for comparison - well, we replaced it with a hash. Every command will be converted to a uint32_t re-presenter. If you are asking why we are doing that there are at least two good reasons. First reason, in case of MCUs with a small amount of RAM space, we save it in this way because we need only 4 bytes for any command. The second, and more important reason, is the improvement of performance. It is much faster to compare two uint32_t numbers than two strings even if the string are shorter than 4 bytes.
static uint32_t _at_hash( char *cmd ) { uint16_t ch = 0; uint32_t hash = 4321; while( ( ch = *( cmd++ ) ) ) hash = ( ( hash << 2 ) + hash ) + ch; return hash; }
This function is used for hash calculation. Of course you can create your own formula but test it before real usage just to see does it work the way you want.
Next step is to create the function that will store the our types to the storage we created.
void at_parser_store( char *command, uint32_t timeout, at_cmd_cb getter, at_cmd_cb setter, at_cmd_cb tester, at_cmd_cb executer ) { at_cmd_t cmd; cmd.hash = _at_hash( command ); cmd.timeout = timeout; cmd.getter = getter; cmd.setter = setter; cmd.tester = tester; cmd.executer = executer; if( strlen( command ) >= AT_HEADER_SIZE ) return; if( at_cmd_storage_used == AT_STORAGE_SIZE ) return; if( _at_search( command ) ) return; at_cmd_storage[ at_cmd_storage_used ] = cmd; at_cmd_storage_used++; }
The parts of function which might be interesting is the function _at_search( command ) that is actually searching the same command on the storage, for example to avoid saving of the same command. Variable at_cmd_storage_used is a static variable used to provide the current number of saved commands. Constant AT_HEADER_SIZE represents the maximum size of the command including the AT which is part of almost every AT command.
static uint16_t _at_search( char* cmd ) { uint16_t i; uint32_t tmp_hash = _at_hash( cmd ); for( i = 0; i < at_cmd_storage_used; i++ ) if( at_cmd_storage[ i ].hash == tmp_hash ) return i; return 0; }
Next step is to make the function that is going to recognize the string and return the command type. For that kind of job we can involve a enum that will represent all types of command. Solutions might look like this:
typedef enum { AT_CMD_UNKNOWN = 0, AT_CMD_GET = 1, AT_CMD_SET = 2, AT_CMD_TEST = 3, AT_CMD_EXEC = 4, }at_type_t;
static at_type_t _at_sub_parse( char *raw_in, char *clean_out ) { uint8_t c = 0; uint8_t end_pos = 0; uint8_t set_pos = 0; uint8_t get_pos = 0; uint8_t start_pos = 0; char* tmp_ptr = raw_in; char tmp_cmd[ AT_HEADER_SIZE ] = { 0 }; if( strlen( tmp_ptr ) <= AT_HEAD_SIZE ) return AT_CMD_UNKNOWN; strncpy( tmp_cmd, tmp_ptr, AT_HEADER_SIZE ); for( c = 0; c < AT_HEADER_SIZE; c++ ) { if( tmp_cmd[ c ] == '' ) { if( !end_pos ) end_pos = c; break; } if( ( tmp_cmd[ c ] == '+') && !start_pos ) start_pos = c; if( ( tmp_cmd[ c ] == '=' ) && !set_pos ) set_pos = c; if( ( tmp_cmd[ c ] == '?' ) && !get_pos ) get_pos = c; if( ( ( tmp_cmd[ c ] == 'r' ) || ( tmp_cmd[ c ] == 'n' ) || ( tmp_cmd[ c ] == ':' ) ) && !end_pos ) end_pos = c; } if( !set_pos && !get_pos ) { strncpy( clean_out, &tmp_cmd[ start_pos ], end_pos - start_pos ); return AT_CMD_EXEC; } else if( !set_pos && get_pos ) { strncpy( clean_out, &tmp_cmd[ start_pos ], get_pos - start_pos ); return AT_CMD_TEST; } else if( set_pos && !get_pos ) { strncpy( clean_out, &tmp_cmd[ start_pos ], set_pos - start_pos ); return AT_CMD_SET; } else if( set_pos == get_pos - 1 ) { strncpy( clean_out, &tmp_cmd[ start_pos ], set_pos - start_pos ); return AT_CMD_GET; } return AT_CMD_UNKNOWN; }
The function accepts raw input that will be our response from the module. Second parameter will be a clean command string - without any characters that not belong to the command and of course our function will return the type of the command - that was the primary goal.
Finally we have the most important function, the parser. This function provides all responses and functions to parse. It also returns the exact function pointer and the proper timeout for the associated command.
void at_parse( char *input, at_cmd_cb *cb, uint32_t *timeout ) { at_type_t cmd_type = 0; uint16_t cmd_idx = 0; char cmd_temp[ AT_HEADER_SIZE ] = { 0 }; if( !( cmd_type = _at_sub_parse( input, cmd_temp ) ) ) { *cb = at_cmd_storage[ 0 ].executer; *timeout = at_cmd_storage[ 0 ].timeout; return; } if( !( cmd_idx = _at_search( cmd_temp ) ) ) { *cb = at_cmd_storage[ 0 ].executer; *timeout = at_cmd_storage[ 0 ].timeout; return; } switch ( cmd_type ) { case AT_CMD_SET : *cb = at_cmd_storage[ cmd_idx ].setter; *timeout = at_cmd_storage[ cmd_idx ].timeout; break; case AT_CMD_GET : *cb = at_cmd_storage[ cmd_idx ].getter; *timeout = at_cmd_storage[ cmd_idx ].timeout; break; case AT_CMD_TEST : *cb = at_cmd_storage[ cmd_idx ].tester; *timeout = at_cmd_storage[ cmd_idx ].timeout; break; case AT_CMD_EXEC : *cb = at_cmd_storage[ cmd_idx ].executer; *timeout = at_cmd_storage[ cmd_idx ].timeout; break; case AT_CMD_UNKNOWN : *cb = at_cmd_storage[ 0 ].executer; *timeout = at_cmd_storage[ 0 ].timeout; break; } return; }
This function assigns the callback and timeout from the first member in our storage. If the function is not recognized, or can not be parsed we have a default callback and default timeout for the functions that are not saved. This means that we have to assign something to the first member inside the our storage and that is the job for the initialization function. In this case, that will be done at a higher level or in at_engine.c because our parser doesn't need to know anything about default timeouts or callbacks.
Summary
We have passed through the AT command parser. Some improvements and changes can be made, for example if you want your parser to avoid trying to parse commands that doesn't start with AT string you can involve that check inside, but keep in mind that not all command starts with the AT string.
Part 3 >>