The project aim is to prove the feasibility of a high configurable and versatile logic signal acquisition unit using the new Crossover MCU i.MX RT1010. The initial assessment shows endless possibilities and high performance using this very low cost device. The FlexIO interface is the core of the acquisition unit, providing exceptional high rates and leveraging the MCU to other possible tasks. A simple communication protocol is implemented and open source software project is used for the signal analysis interface.
Hardware Overview.The project is build around NXP evaluation board MIMXRT1010-EVK, the board has everything to get started. Since using FlexIO is the core of the firmware, we need some Flexio signals to be exposed on the board, and so this was one of the first challenges as the board does not expose many of the FlexIO lines. The best way to look at this is to open the board schematics (attached) and the reference manual chapter 10, where available GPIO pins associated to the FlexIO peripheral among others are listed in table 10-1.
This is because scatter pins are on different interfaces of the board, in this project not all the pins used are on the arduino interface, because as we will see a special feature of the FlexIO to act as a parallel interface is used, for this is necessary that FlexIO pins reside in continuous adjacent order which make the task more difficult. Before jumping into describing pins let quickly describe how FlexIO is used.
The EVK offers a USB connector for direct connection to the crossover MCU, which is used to emulate a serial VCOM interface and communicate with PC software.
FlexIO details.The interface best use for this application is as a parallel interface, this allows more than one signal to be acquire at the same clock, certain conditions should be met, for instance only 4, 8, 16 or 32 bits can be shifted into flexio shift registers.
Since the number of adjacent flexio pins, that is the flexio numbering, not to be confused with adjacent device pins, are scarce on the EVK, one possibility is to use less pins, in our case to prove the design only two pins suffice.
In this case since the numbers of pins to be sample are less than the actual pins shifted into the flexio shift registers, a padding will incur on the remaining bit. In our case the number of pins shifted should be 4. The configuration is governed by the value of SHIFTCFGx register, the PWIDTH field in this case will be setup for a value of 2, which automatically selects 4-bit shift operations as seen in RM page 1596. The padding is explained in section named 41.4.4.1 Parallel Interface.
In order to acquire synchronously from the FlexIo interface, the best way is again to relay on the hardware, and thus a clock signal has to be provided.
The clock signal can be provided externally or externally but as we want to be able to change the sampling frequency during operation, it's obviously better to generate it on the MCU. The solution can be to use a timer to toggle a GPIO, but since we have more than enough FlexIO Timers for the application, we can generate the clock from same peripheral at no processing cost. The FlexTimer will have it's output assigned to a physical pin. This clock signal will be used by other timer to generate the shift operations. And beautiful of the FlexIO peripheral is that we don't even has to care about connecting this clock signal externally to the next timer input, the connection will be available internally.
For this project since we want to see the clock signal for verification purposes we have assigned it to an available pin. In this case we have used the green LED on board, thou accessing the LED is difficult as there isn't a test point, after careful review of the EVK schematics (because of the scarce flexio pins), there is a connection from the GPIO_11 (FlexIO1_I26) to a header which is used for a non populated audio option, the header number is J54 and pin2 of the header has the LED signal connection.
As normally for an any digital signal acquisition device we must have a trigger to capture as to be able to look where we are interesting in. In this application, the trigger will be used by FlexTimers to ensure the beginning of the sampling. The duration will be controlled by the application based on the number of samples required and of course limited by available memory on the device.
The following table summarize the signals and MCU pins used.
The following image shows how the connection were made during testing. The testing signals such as the trigger and parallel signals are generated using external instrument fro Digilent. Thou it can also be used, during testing it was useful to connect a Saleae Logic analyzer as well, for instance, when trying to confirm the output clock frequency and the trigger and data signals.
Input parallel number 0 cannot be hook-up easily but as for quickly testing purposes the Input 1 was used, this is because the signal is not easily exposed, thou iit can be done by removing a 0 ohm resistor R792 and placing a new 0 ohm resistor on R800, then access will be given by signal name FLEXPWM1_PWMA3 on arduino connector J56-10.
The figure 1 shows the connections that appear in Table 1. It's worth notice that the JTAG connector is used since during the FlexIO interface firmware debugging something went completely wrong and the CMSIS-DAP interface stop working.
After trying the segger J-Link it was noticed that the problem persists and the issue was not clearly a solution, but luckily I noticed that if boot jumpers were to be change from original position of FlexSPI boot [0010] to Boot from fuses [0000] and new firmware is loaded, then after reconnecting the device it can be debugged again. Not exactly sure the reason but perhaps some malfunctioning code on the last debug session was producing the effect on the booting sequence. The question and detailed log was asked in NXP forum here.
Also notice that jumpers for the serial connection were removed as well as we are using some Flexio Pins on those Pads.
Firmware Overview.The firmware might be divided in two sections, the one pertaining to the signal acquisition using FlexIO and the data transfer and communication to external software for signal analysis. Before moving into FlexIO detailed firmware description let's comment the solution to analyze the acquired capture.
An open source software called sigrok was chosen as the application to analyse the captured data, the benefits besides the fact that is open source is multi platform as well as easy to use, but most important, we can use a simple driver to start playing around with the tool.
It was not possible to make Sigrok work under one of my windows 10 machines, it's perhaps some Java compatibility. I have tested under my debian buster virtual machine and works perfectly. Here is a screenshot of an actual simulated capture send from the crossover MCU over the VCOM interface.
Figure 2 shows 512 samples of an 8 bit number count from 0 to 512. At the beginning there is a trail of "zeros" created, which in this example corresponds to 12 samples. It's worth to mention that for this example, the samples and frequency were not adjusted, that is, sigrok will scale the data accordingly, in other words, the software will receive and interpret the data according to this configuration, but actually this configuration can also be send to the device so it's setup correctly.
Sigrok implements a vast number of communication protocols, the serial communication protocol used for this project is based on Open Bench Logic Sniffer, a simple protocol to configure and receive the captured samples. It was the choice due to simplicity but might not be the most efficiency way to transmit data specially when working with data streams. Thou it was not implemented in this project, a continuous stream of data using sigrok requires the serial data transmission to not be delayed by certain number of characters, otherwise transmission will be considered terminated and capture data presented in the GUI, discarding additional samples.
During the initial test, the dummy buffer was used to confirm communication, but it was valuable to noticed an existing issue with sigrok implementation. The very first buffer transmitted didn't look exactly as the one on figure 2, the data was actually presented in wrong order such as the last sample were the first is supposed to be. This is confirmed in this raised issue.
To correct the issue or perhaps better as a workaround suggested here, the data is placed in the USB buffer in different order, such as last values would then appear first. Here is the snippet used to create the data of figure 2.
memset(s_currSendBuf, 0, sizeof(s_currSendBuf));
int i = 0;
for(int j=500; j>0; j--){
s_currSendBuf[j] = i++;
}
Before posting here the snippet that places data from FlexIO buffer to the VCOM USB buffer let's examine in more detail the FlexIO implementation.
The firmware of this project can be found in github here.
FlexIO firmware details.The flexio code is contained in a library driver files similar to other flexio or device drivers. This will help maintain similar usage facilitating the importing process as well. The files are called fsl_flexio_logic.c and fsl_flexio_logic.h.
The code works by configuring the flexio peripheral and enabling it to run from the beginning, the trigger event will start a new capture. For the example code the capture length is fixed thou it can be easily modified on the fly as we will see.
The capture is done by a blocking read on the flexio shifter buffer event. This can be easily converted to an interrupt event or perhaps even better to a EDMA transfer, an initial effort (not fully tested) to provide EDMA was done.
The example code imports the aforementioned flexio drivers into the VCOM example which as the communication protocol as well. The flexio is initialized from virtual_com.c as shown below.
/* Clock setting for Flexio */
CLOCK_SetMux(kCLOCK_Flexio1Mux, FLEXIO_CLOCK_SELECT);
CLOCK_SetDiv(kCLOCK_Flexio1PreDiv, FLEXIO_CLOCK_PRE_DIVIDER);
CLOCK_SetDiv(kCLOCK_Flexio1Div, FLEXIO_CLOCK_DIVIDER);
/* Init flexio, use default configure
* Disable doze and fast access mode
* Enable in debug mode
*/
FLEXIO_GetDefaultConfig(&fxioUserConfig);
FLEXIO_Init(DEMO_FLEXIO_BASEADDR, &fxioUserConfig);
FLEXIO_LOGIC_Init(&s_FlexioLogicDevice, &s_FlexioLogicConfig);
/* Clear all the flag. */
FLEXIO_LOGIC_ClearStatusFlags(&s_FlexioLogicDevice, kFLEXIO_LOGIC_RxDataRegFullFlag | kFLEXIO_LOGIC_RxErrorFlag);
/* Enable FlexIO. */
FLEXIO_LOGIC_Enable(&s_FlexioLogicDevice, true);
The probably most important function above is FLEXIO_LOGIC_Init, it does all the work in configuring the shift buffers and timers. To illustrate the architecture a diagram was constructed based on the parallel diagram from application note AN12686.
Here we are using the same number of shift buffers, if needed we can use less. So the configuration of shift registers is pretty much identical.
Timer 0 is used to generate the clock, in this case a PWM of 50% duty cycle. As we are using similar code to the PWM example by NXP, is easy to change the frequency using below snippet.
/* Calculate timer lower and upper values of TIMCMP */
/* Calculate the nearest integer value for sum, using formula round(x) = (2 * floor(x) + 1) / 2 */
/* sum = DEMO_FLEXIO_CLOCK_FREQUENCY / freq_H */
sum = (DEMO_FLEXIO_CLOCK_FREQUENCY * 2 / freq_Hz + 1) / 2;
/* Calculate the nearest integer value for lowerValue, the high period of the pwm output */
/* lowerValue = sum * duty / 100 */
lowerValue = (sum * duty / 50 + 1) / 2;
/* Calculate upper value, the low period of the pwm output */
upperValue = sum - lowerValue;
timerConfig.timerCompare = ((upperValue - 1) << 8U) | (lowerValue - 1);
FLEXIO_SetTimerConfig(base->flexioBase, base->timerIdx[0], &timerConfig);
Timer 1 in this case is used to count the number of clock cycles required to acquire the number of samples specified. Using the config snippet below
timerConfig.triggerSelect = FLEXIO_TIMER_TRIGGER_SEL_PININPUT(base->trgPinIdx);
timerConfig.triggerPolarity = kFLEXIO_TimerTriggerPolarityActiveHigh;
timerConfig.triggerSource = kFLEXIO_TimerTriggerSourceInternal;
timerConfig.pinConfig = kFLEXIO_PinConfigOutputDisabled;
timerConfig.pinSelect = base->sclkPinIdx;
timerConfig.pinPolarity = kFLEXIO_PinActiveHigh;
timerConfig.timerMode = kFLEXIO_TimerModeSingle16Bit;
timerConfig.timerOutput = kFLEXIO_TimerOutputZeroNotAffectedByReset;
timerConfig.timerDecrement = kFLEXIO_TimerDecSrcOnPinInputShiftPinInput;
timerConfig.timerReset = kFLEXIO_TimerResetNever;
timerConfig.timerDisable = kFLEXIO_TimerDisableOnTimerCompare; // disable on count compare
timerConfig.timerEnable = kFLEXIO_TimerEnableOnTriggerRisingEdge;
timerConfig.timerStop = kFLEXIO_TimerStopBitDisabled;
timerConfig.timerStart = kFLEXIO_TimerStartBitDisabled;
timerConfig.timerCompare = ((uint32_t)(config->samples * 2 - 1U));
// block size in bits or clock transitions
FLEXIO_SetTimerConfig(base->flexioBase, base->timerIdx[1], &timerConfig);
As can be seen in the comment of code above, timerCompare utakes the value of samples multiplied by 2 less one, this is because the timer will take both clock transitions to decrease the counter. Ans for this case the timer is configured as 16 bit mode and disable on timer compare. The Timer trigger is the signal we want to measure that produces an event, from that event the timer 1 will start.
The Timer 2 handle the shift buffer load operation, it will indicate when the shift registers have to load from input pin or adjacent pins and when the shift registers has to load its contents into the shift buffers.
/* FLEXIO_LOGIC timer config to drive shift register
* the clk source of timer to drive the shifter
* is the CLK generated using FlexIO
*/
timerConfig.triggerSelect = FLEXIO_TIMER_TRIGGER_SEL_PININPUT(base->trgPinIdx);
timerConfig.triggerPolarity = kFLEXIO_TimerTriggerPolarityActiveHigh;
timerConfig.triggerSource = kFLEXIO_TimerTriggerSourceInternal;
timerConfig.pinConfig = kFLEXIO_PinConfigOutputDisabled;
timerConfig.pinSelect = base->sclkPinIdx;;
timerConfig.pinPolarity = kFLEXIO_PinActiveHigh;
timerConfig.timerMode = kFLEXIO_TimerModeSingle16Bit;
timerConfig.timerOutput = kFLEXIO_TimerOutputZeroNotAffectedByReset;
timerConfig.timerDecrement = kFLEXIO_TimerDecSrcOnPinInputShiftPinInput;
timerConfig.timerReset = kFLEXIO_TimerResetOnTimerTriggerRisingEdge;
timerConfig.timerDisable = kFLEXIO_TimerDisableOnPreTimerDisable; // disable on Timer N-1 disable
timerConfig.timerEnable = kFLEXIO_TimerEnableOnTriggerRisingEdge;
timerConfig.timerStop = kFLEXIO_TimerStopBitDisabled;
timerConfig.timerStart = kFLEXIO_TimerStartBitDisabled;
timerConfig.timerCompare = 16U * base->shifterCount - 1U;
FLEXIO_SetTimerConfig(base->flexioBase, base->timerIdx[2], &timerConfig);
For this Timer 2 we are using same trigger as Timer 1, the timer will as long as Timer 1 runs by having the timer disable value equals to disable of previous timer.
Once the configuration is done, we basically have the logic analyzer in armed state as it's waiting for the trigger signal. One more thing would be to configure the frequency and number of samples as shown below. Notice sampleData is a buffer that has to be of size 4096.
s_FlexioLogicConfig.samples = 4096;
s_FlexioLogicConfig.frequency = DEMO_FLEXIO_FREQUENCY;
s_FlexioLogicConfig.pwidth = 4; // parallel width of 4 bits
s_FlexioLogicConfig.sDataPtr = sampleData;
One more thing to remember is to setup the pins as below
IOMUXC_SetPinMux(
IOMUXC_GPIO_AD_14_FLEXIO1_IO26,
0U);
IOMUXC_SetPinMux(
IOMUXC_GPIO_11_FLEXIO1_IO03,
0U);
IOMUXC_SetPinMux(
IOMUXC_GPIO_08_FLEXIO1_IO00,
0U);
IOMUXC_SetPinMux(
IOMUXC_GPIO_09_FLEXIO1_IO01,
0U);
IOMUXC_SetPinConfig(
IOMUXC_GPIO_08_FLEXIO1_IO00,
0x10A0U);
IOMUXC_SetPinConfig(
IOMUXC_GPIO_09_FLEXIO1_IO01,
0x10A0U);
IOMUXC_SetPinConfig(
IOMUXC_GPIO_AD_14_FLEXIO1_IO26,
0x10A0U);
IOMUXC_SetPinConfig(
IOMUXC_GPIO_11_FLEXIO1_IO03,
0x10A0U);
We have removed the initialization of debug console pins as well as removed from code because we have a conflicting FlexIO GPIO_09 with UART_RX.
Finally we can call our blocking function to wait for samples.
void FLEXIO_LOGIC_ReadBlocking(FLEXIO_LOGIC_Type *base, const flexio_logic_config_t *config)
{
assert(config);
uint32_t sampleSize = config->samples; /// convert samples to actual bytes
uint32_t sEvtNumber = ((base->shifterCount*4) *(8/config->pwidth)); // the actual samples in a Flexio Event
uint8_t *sampleData = config->sDataPtr;
while (sampleSize)
{
/* Wait until data transfer complete. */
while (!(FLEXIO_LOGIC_GetStatusFlags(base) & kFLEXIO_LOGIC_RxDataRegFullFlag))
{
__NOP();
}
for(int j = base->shifterStartIdx; j < base->shifterCount; j++){
*sampleData++ = base->flexioBase->SHIFTBUFBYS[j];
}
sampleSize = sampleSize - sEvtNumber;
}
}
The point now is when to call this function. That leads us to more details of the OLS driver.
The driver implementation covers the basic commands. Here is the snippet.
usb_status_t error = kStatus_USB_Error;
if ((1 == s_cdcVcom.attach) && (1 == s_cdcVcom.startTransactions))
{
/* User Code */
if ((0 != s_recvSize) && (0xFFFFFFFFU != s_recvSize))
{
int32_t i = 0;
OutByte = s_currRecvBuf[i++];
switch (OutByte) {//switch on the current byte
case SUMP_RESET://reset
//PRINTF("RESET\r\n");
// do some reset stuff.
break;
case SUMP_ID://SLA0 or 1 backwards: 1ALS
//PRINTF("SUMP ID\r\n");
s_currSendBuf[s_sendSize++] = '1';
s_currSendBuf[s_sendSize++] = 'A';
s_currSendBuf[s_sendSize++] = 'L';
s_currSendBuf[s_sendSize++] = 'S';
// This will Flush after exiting the switch case
break;
case SUMP_RUN://arm the trigger
//PRINTF("SUMP RUN\r\n");
// SysTick_DelayTicks(2000U);
TxSamples = true;
break;
case SUMP_META://meta data
//PRINTF("SUMP META\r\n");
s_currSendBuf[s_sendSize++] = 0x40;
s_currSendBuf[s_sendSize++] = 0x08;
s_currSendBuf[s_sendSize++] = 0x41;
s_currSendBuf[s_sendSize++] = 0x02;
s_currSendBuf[s_sendSize++] = 0x00;
// This will Flush after exiting the switch case
break;
case SUMP_XON://resume send data
//PRINTF("SUMP XON\r\n");
// xflow=1;
break;
case SUMP_XOFF://pause send data
//PRINTF("SUMP XOFF\r\n");
// xflow=0;
break;
default://long command
//PRINTF("Default case\r\n");
sumpRX.command[0] = OutByte; //store first command byte
sumpRX.parameters = 4; //all long commands are 5 bytes, get 4 parameters
sumpRX.parCnt = 0; //reset the parameter counter
sumpRX.command[sumpRX.parCnt] = OutByte;
while (sumpRX.parCnt < sumpRX.parameters) {
sumpRX.parCnt++;
sumpRX.command[sumpRX.parCnt] = s_currRecvBuf[i++];
//PRINTF("%2x ", sumpRX.command[sumpRX.parCnt]);
// Process sump parameters here. For now do nothing.
}
break;
}
if(s_sendSize == 0){
// finish processing received data by sending ack.
error = USB_DeviceCdcAcmSend(s_cdcVcom.cdcAcmHandle, USB_CDC_VCOM_BULK_IN_ENDPOINT, NULL, 0);
if (error != kStatus_USB_Success)
{
/* Failure to send Data Handling code here */
}
}
s_recvSize = 0;
}
if(TxSamples){
FLEXIO_LOGIC_ReadBlocking(&s_FlexioLogicDevice, &s_FlexioLogicConfig);
s_sendSize = 512;
uint8_t *tmp = s_FlexioLogicConfig.sDataPtr;
for(int j=512; j>0; j--){ // hardwired at the moment
s_currSendBuf[j--] = (*tmp & 0x0f);
s_currSendBuf[j] = (*tmp & 0xf0)>>4;
tmp++;
}
TxSamples = false;
}
if (s_sendSize)
{
uint32_t size = s_sendSize;
s_sendSize = 0;
error = USB_DeviceCdcAcmSend(s_cdcVcom.cdcAcmHandle, USB_CDC_VCOM_BULK_IN_ENDPOINT, s_currSendBuf, size);
if (error != kStatus_USB_Success)
{
/* Failure to send Data Handling code here */
}
}
}
Some commands are for establishing communication, after that when you hit the Run button, Sigrok using OLS driver will send the SUMP_RUN command. Here a variable is set and later on checked.
if(TxSamples){
FLEXIO_LOGIC_ReadBlocking(&s_FlexioLogicDevice, &s_FlexioLogicConfig);
s_sendSize = 512;
uint8_t *tmp = s_FlexioLogicConfig.sDataPtr;
for(int j=512; j>0; j--){ // hardwired at the moment
s_currSendBuf[j--] = (*tmp & 0x0f);
s_currSendBuf[j] = (*tmp & 0xf0)>>4;
tmp++;
}
Notice in this case how the order of the capture 4-bit nibbles are handle and transmitted.
The captured data will look like the following figure 4.
The device connectivity is first established, parameters are shown in Fig 5.
In order to confirm our setup, a signal is generated using Digilent Discovery 2 kit, the signal, clock and trigger are captured using Saleae Pro Logic analyzer, and of course our capture using Sigrok is recorded.
First configure Waveforms software from Digilent as shown in Figure 5 below.
The signal was acquired simultaneously on our crossover Logic device and using Saleae Logic protocol analyzer.
The following Figure 7 shows the Saleae result.
Our result using Crossover Logic device in Figure 8.
Thou the timeline does not match exactly, it was perhaps a different value. Our clock is 19.95 kHz.
Conclusion.It was shown how powerful the crossover MCU is and how versatile when handling digital signals acquisition, something very difficult by just reading from GPIO and using interrupts, difficult in the sense that precise timing and parallel reception might not be always possible, this project can be easily extended to handle 16 or even 32 digital signals that will be sample at the same clock time. The clock can have a high speed such as 5MHz easily, for this project a low frequency was selected as the serial transfer from device to PC was not fully implemented, that is, some modifications are needed to send larger buffers or better to send a continuous stream.
This is possible by implementing a ping pong buffer on the FlexIO interface, thou probably the device sampling rate will be dominated by the speed of the serial connection.
The possibilities are big considering the speed of the MCU and how leveraging is a peripheral like the FlexIO.
Happy crossovering !
Comments