For a hardware evaluation project I'm working on, I want to create a device that can be controlled via LabView. LabView can talk to instruments using serial out of the box, and it knows how to talk Standard Commands for Programmable Instruments (SCPI).
In this blog I have a working SCPI conversation between LabVIEW and a Hercules safety microcontroller. |
SCPI Parser LIB Learns to Reply via USB
In the previous blog, I stopped when LabVIEW and the microcontroller were able to perform a mock conversation over USB.
LabVIEW sent us an *IDN? command, and the Hercules just echoed that command back to LabVIEW.
For a meaningful conversation, we have to do three things:
- send the command string we received from LabVIEW to the SCPI parser library
- inform the parser library how and what it should reply on an *IDN? command.
- learn the library how to reply via our USB port
Sending the string to the library is done for each character, as soon as we've received it. I'm using a freeRTOS interrupt notification mechanism.
I have an RTOS task that sits ready forever to consume characters from the USB port. It doesn't run. In the Read interrupt handler for my serial comms module, I have put a 'wake-up' call. It notifies the halted task that it can continue.
The halted task proceeds, moves the character to the parser input, and goes back to sleep until the next interrupt (the next character).
That's a step up from my initial design, where I was sleeping and polling for input. |
Initially I implemented a buffer to compose a string from the input characters, and send that concatenated string to the parser. But the parser has its own buffer mechanism.
I've changed my code to send each character to the lib directly. Whenever it determines that it received a complete command,it sends the reply via USB.
If this approach turns out to be too naive or too costly, I'll revert to buffering the data before sending it.
As explained in the side note above, the function normally halts at ulTaskNotifyTake(), waiting to be woken up by the USB Read interrupt.
void prvUARTCommandConsoleTask(void *pvParameters) { (void) pvParameters; _uRxChar = 0U; scpi_instrument_init(); sciReceive(scilinREG, 1, (uint8 *)&_uRxChar); for( ;; ) { /* Block indefinitely (without a timeout, so no need to check the function's return value) to wait for a notification. Here the RTOS task notification is being used as a binary semaphore, so the notification value is cleared to zero on exit. NOTE! Real applications should not block indefinitely, but instead time out occasionally in order to handle error conditions that may prevent the interrupt from sending any more notifications. */ ulTaskNotifyTake( pdTRUE, /* Clear the notification value before exiting. */ portMAX_DELAY ); /* Block indefinitely. */ /* The RTOS task notification is used as a binary (as opposed to a counting) semaphore, so only go back to wait for further notifications when all events pending in the peripheral have been processed. */ // in my case: I get an interrupt for a single character, no need to loop. scpi_instrument_input((const char *)&_uRxChar, 1); sciReceive(scilinREG, 1, (uint8 *)&_uRxChar); } }
The USB Read interrupt service routine just unlocks that task by sending it a notification vTaskNotifyGiveFromISR().
#pragma WEAK(sciNotification) void sciNotification(sciBASE_t *sci, uint32 flags) { /* enter user code between the USER CODE BEGIN and USER CODE END. */ /* USER CODE BEGIN (29) */ if ((flags == SCI_RX_INT) && (sci == scilinREG)) { BaseType_t xHigherPriorityTaskWoken; /* xHigherPriorityTaskWoken must be initialised to pdFALSE. If calling vTaskNotifyGiveFromISR() unblocks the handling task, and the priority of the handling task is higher than the priority of the currently running task, then xHigherPriorityTaskWoken will automatically get set to pdTRUE. */ xHigherPriorityTaskWoken = pdFALSE; /* Unblock the handling task so the task can perform any processing necessitated by the interrupt. xHandlingTask is the task's handle, which was obtained when the task was created. */ vTaskNotifyGiveFromISR( xUARTCommandInterpreterTaskHandle, &xHigherPriorityTaskWoken ); /* Force a context switch if xHigherPriorityTaskWoken is now set to pdTRUE. The macro used to do this is dependent on the port and may be called portEND_SWITCHING_ISR. */ portYIELD_FROM_ISR( xHigherPriorityTaskWoken ); // empty by design } /* USER CODE END */ }
The reply for the *IDN? command has to be placed in the application specific context that we pass to the library at initialisation time.
#define SCPI_IDN1 "TEXASINSTRUMENTS" #define SCPI_IDN2 "LAUNCHXL2-RM46" #define SCPI_IDN3 NULL #define SCPI_IDN4 "001000" // ... void scpi_instrument_init() { SCPI_Init(&scpi_context, scpi_commands, &scpi_interface, scpi_units_def, SCPI_IDN1, SCPI_IDN2, SCPI_IDN3, SCPI_IDN4, scpi_input_buffer, SCPI_INPUT_BUFFER_LENGTH, scpi_error_queue_data, SCPI_ERROR_QUEUE_SIZE);
You tell the SCPI library how to reply over USB by implementing the function SCPI_Write().
In the example that comes with the lib, the results are written to stdout. In our embedded app, it'll have to go to the serial communications API.
size_t SCPI_Write(scpi_t * context, const char * data, size_t len) { (void) context; sciSend(scilinREG, len, (uint8 *)data); return len; }
If you don't have LabVIEW, don't worry.
You can use PuTTY (or another terminal program) to run SCPI over USB.
Here's the result of running the *IDN? command, while connected at 9600, 8, 2, N:
You won't see the commands you type, because SCPI doesn't echo them back.
A simple trick is to temporarily turn on local echo in PuTTY, so you see your input while you type it.
Don't get confused by that 'local echo' edit mode: even though it seems you can correct your entry while typing, that's not true.
All characters are directly sent to the USB port as you type them. And there's no error handling (yet) on the controller side.
Further Steps
And that's it. By adding these three things to the puzzle, I can have a first meaningful conversation between LabVIEW and my own instrument.
Step by step, I can now add instrument functionality.
The first thing I want to do is control the PWM module. I'll provide functions to set and read PWM period, duty cycle and dead time.
That will be the first time that I can change behaviour of my instrument via LabVIEW.
The CCS project is attached to this blog. It includes the HALCoGen settings, the SCPI lib and all firmware. It's set up for a RM46 microcontroller with freeRTOS. With the info from the two previous blog posts, it isn't difficult to port it to another Hercules controller. |