I'm selected for the STM32H7B3I-DK - DISCOVERY KIT road test.
I'm working on a touch screen GUI for my electronic load.
In this part of the review, I explore the application layer:
- request data from the business layer
- display that data
- use the Model-View-Presentation style paradigm, where the display logic only deals with display matters
- use a freeRTOS message queue to send business layer data to the TouchGFX application
image source: ST TouchGFX documentation
The business layer in my example is a SCPI client.
The TouchGFX app has no knowledge of the fact that the SCPI client uses UART to talk to the my electronic load.
Ah, and it's in C++.
I made compromises in my example:
- I use a message queue to inform the model that new data arrived. But the other way around, I just do plain method calls.
This could (should) also be implemented by a queue. But by using it in one direction is proof that it works. - Although I use a message queue to inform the presentation layer that SCPI data arrived, I then directly talk to the business layer to retrieve the data buffer.
In theory this 2nd part should either be done in the Model layer, or the buffer (or a pointer to it) should be part of the message.
Since there is a single buffer that's always used, I went for a simpler way of directly fetching the buffer data. Discuss. - I used the compatibility layer for message queues freeRTOS V1 style. Because I used the logic of ST example: http://sw-center.st.com/touchgfx/TouchGFX/knowledgebase/c_task_condenser_example.4.9.3.zip.
This is a very clean Model - View - Presentation example. If you want to see an implementation that doesn't make compromises, check that one.
If you want to read up on the exchange with the business system, check this breakout of ST on backend communication.
Graphic Design
A simple design: a button and a textbox. When I click the button, I want to ask the load for its SCPI identification.
When that ID arrives, I want that the textbox displays it.
If I click the button again, I want the textbox cleared.
The button needs to have an event handler. I am not explaining that here because done in the previous posts.
For later reference, the function called when the button is clicked is fnClickID(). The implementation is discussed in the code part of this article.
And I'll attach the code.
The text area is a bit more involved. To be able to change the text, we need to define a wildcard.
This will create a 40 character buffer that we can write into. We'll do that in our View code, then ask the screen to redraw that part.
The Model Layer
Our model class can send a command to the business layer, and inform the Presentation when it receives info from that business layer.
extern "C" { xQueueHandle gui_msg_q; } Model::Model() : modelListener(0) { gui_msg_q = xQueueGenericCreate(1, 1, 0); } void Model::tick() { // Check for messages from backend, with zero timeout uint8_t msg = 0; if (xQueueReceive(gui_msg_q, &msg, 0) == pdTRUE) { if (msg == SCPI_IDN) { // Notify current presenter that IDN is received,and hand over payload modelListener->scpi_idn((const char*)ScpiMgr::getReply()); } } } void Model::request_scpi_idn() { ScpiMgr::idn(); }
The tick() is a standard feature. The TouchGFX loop keeps the model alive by making it tick every so often.
In that method, we chack if data is received from the business layer (SCPI).
If yes, we check what type of message, and then inform the presentation layer of that.
At this moment, the only thing the model (and other classes) understand is the reply on a *IDN? command.
The other way around, asking the business layer to send a SCPI command in request_scpi_idn(), is - as I discussed in the compromises section - done by directly calling that layer. Discuss.
The Presentation Layer
When the LCD asks for info, or when the Model receives info, this one will take care of the interaction.
void scrHomePresenter::scpi_idn(const char* data) { view.scpi_idn(data); } void scrHomePresenter::request_scpi_idn() { model-> request_scpi_idn(); }
Not hard to follow. The first one is called by the Model when a *IDN? reply arrives in the queue.
It calls the view, with a reference to the SCPI payload.
The second one is called by the view when it wants *IDN? info (read: when the button is clicked).
The View Layer
This one can detect the button click, and can refresh the text area when new data arrives.
void scrHomeView::scpi_idn(const char* data) { Unicode::strncpy(txtIDNBuffer, data, TXTIDN_SIZE); txtIDN.invalidate(); } void scrHomeView::fnClickID() { static bool bClear = false; if (bClear) { Unicode::strncpy(txtIDNBuffer, (const char*)" ", TXTIDN_SIZE); txtIDN.invalidate(); } else { presenter->request_scpi_idn(); } bClear = !bClear; }
The first function is the one that knows how to display the *IDN? payload.
It copies the payload in the text buffer (+ translate from ascii to Unicode), and requests a redraw of the text area only.
The second function is the handler for the button click.
In essence, it just asks the Presentation layer to go fetch *IDN? info from the business layer.
To make the application a bit more interesting, it has an additional functionality: on a second click, it wipes out the text area content.
The Business Layer
This is the SCPI engine. I'll only briefly discuss it here, because it's not TouchGFX relevant. But you'll recognise that it sends messages to the queue when an answer arrives from the instrument.
class ScpiMgr { public: static void commsFinished(uint16_t lenght); static void idn(); static uint8_t * getReply(); private: static uint8_t _scpi_message; };
extern UART_HandleTypeDef *huart; extern uint8_t uBuffer[64]; extern "C" { extern xQueueHandle gui_msg_q; } extern "C" void commsFinished(uint16_t lenght) { ScpiMgr::commsFinished(lenght); } uint8_t ScpiMgr::_scpi_message; void ScpiMgr::commsFinished(uint16_t lenght) { xQueueSend(gui_msg_q, &_scpi_message, 0); } void ScpiMgr::idn() { _scpi_message = SCPI_IDN; uint8_t str[] = "*IDN?\n"; HAL_UART_Transmit(huart, str, sizeof(str), 100); } uint8_t * ScpiMgr::getReply() { return uBuffer; }
The extern "C" function is there as a bridge to the C and C++ world. The Mx Generator creates a plain C main.c.
I use this function to call the C++ SCPI manager from that C code.
Because this class is full static (balearicdynamics, I use this instead of a Singleton), I'll show the header and code:
commsFinished() is called by the UART handler (interrupt driven, see previous articles) when it received a full SCPI message.
This then calls ScpiMgr::commsFinished(). And that posts our message to the TouchGFX queue.
ScpiMgr::idn() is the api function that allows to send a SCPI message. It just uses the ST HAL layer to send the payload over UART.
As mentioned a few times before, this should better be implemented with a queue, similar to how we talk from TouchGFX to the SCPI manager. Discuss.
ScpiMgr::getReply() is the api function to get at the SCPI payload buffer. Used by the Model to retrieve that data.
To make the picture completer, here's how I detect the end of a UART payload arriving (in the TaskUARTListen() freeRTOS task that deals with serial comms):
/* Infinite loop */ for(;;) { HAL_UART_Receive_IT(huart, in, 1); uint32_t ulNotificationValue = ulTaskNotifyTake( pdTRUE, portMAX_DELAY ); if( ulNotificationValue == 1 ) { /* The transmission ended as expected. */ uBuffer[uCnt++] = in[0]; if (in[0] == '\n') { // end of communication commsFinished(uCnt); uCnt = 0; } } else { /* The call to ulTaskNotifyTake() timed out. */ }
I know that the last character the electronic load sends on a data request, is a '\n'.
When I detect that, I call the SCPI manager to notify it that the payload arrived.
Here's a capture of an exchange.
SCPI doesn't always reply on a message. Only on queries. For this design it doesn't matter because request and response are separate mechanisms.
What would be unexpected is a response without sending a request first. But that should not happen.
Top Comments