Thursday, May 7, 2020

USB Data Acquisition on a $3 Board


It is possible to build a simple data acquisition system using the STM32F103 processor. A few analog ports are sampled at the frequency defined by the user and the result is sent over USB configured as a virtual com port, in human readable format to a computer. The code is available at my GitHub repo. This project demonstrates a few things:

  • Sample signals with a fairly precise sampling time,
  • Use ADC, DMA and a timer for analog to digiral conversion in hardware, completely independent from software,
  • How to use the CubeMX graphical configuration and code generation tool,
  • Create a USB CDC device (virtual com port, actually a virtual modem device),
  • Show what can be done using only a $3 microprocessor board.

 
Pots used to generate the voltage data.


The system uses a timer to trigger the ADC, which automatically samples one sequence of the channels you specify, stores them using DMA into your array. It finally sets a flag in an interrupt service routine (ISR).


At the same time, a USB CDC device is also set-up, so this appears to the computer as a virtual serial comm port (actually a modem device). At the firmware side, all that the user needs to do is to check a flag, called the ADC sequence complete flag, and if it is true, get the ADC results, format them into a string, and send the string to the host computer over the comm port. Since a standard USB device class is used, it should work on most modern OS'es without a driver.


The project also demonstrates how to use STM32 CubeMX to create a fairly complex system using the graphical user interface (GUI). The USB device setup can be simply created, as well as the timer, ADC, DMA, the links between them and the interrupts that they generate. After this, we only need to fill up a few locations, typically in main.c. Since STM32 CubeMX auto-generates the project sources, you must not interfere with its code. It must not interfere with what you write either. To deal with this, ST has come up with a method, where they seperated the generated code with this type of comments:

/* USER CODE BEGIN Includes */
  -> your code goes between these comments and survives CubeMX source re-generation.
/* USER CODE END Includes */
 -> whatever you write outside gets erased the next time CubeMX
re-generates your project source.

If you write between them, STM32 CubeMX does not touch your parts of the code even if you re-generate it using the GUI. Well, the method does make for bloated code. But at the end, if you are sure that you will not go through another re-generation iteration, you can delete all those markers and you will be fine and your code senile and readable (ask the OCD in me!).


The code is built as a GCC project using GNU make. The Makefile is modified a bit from the original, because the autogenerated one has too much repetition in file paths etc. I also added a "make jflash" rule so that you can use Texane's excellent st-util for ST-Link V2 programmer. To compile, you should have a GNU-ARM-Embedded toolchain installed (such as from the Launchpad site). Edit Makefile to point to your toolchain path. (BTW, I intentionally obfuscated the Makefile after it was first generated by CubeMX. In later re-generations of the source, CubeMX fails to recognize your Makefile for updating, does not modify it, and gives an error message. This is intentional. Just ignore that error).


After you program the processor, power it down and plug it back in through the USB port. It should appear as a CDC device (such as /dev/ttyACM0). Connect to it using a terminal emulator (I like kermit). After you set the port (no need to set connection speed in ACM devices) and connect, you should see the readings coming from ADC1, 2, 3 as:

...
200: ADC1=0, ADC2=4029, ADC3=4030
201: ADC1=1039, ADC2=4029, ADC3=4029
202: ADC1=2353, ADC2=4030, ADC3=4030
203: ADC1=3503, ADC2=4030, ADC3=4030
...
 
The first number is sample sequence. How you format the data is up to you. The above format is just for clarity; in a normal application a comma separated list is probably better. You can send data in binary form also. You can even build a simple data graph of sorts by using the excellent SerialPlot program. Give it a try!

There are a few things that can be changed:

  • Add or remove ADC channels -> See the ADC section in CubeMX
  • Change the sample and hold duration -> See the ADC section in CubeMX
  • Change sampling time -> See TIM3 section in CubeMX. The sampling time is calculated as follows: The processor runs at 72MHz. This is first divided in the prescaler (it is currently set to 7200), and further in the ARR (max count limit of timer/counter) which is set to 1000. The overall sampling time (Ts) is therefore 72,000,000  /7200 /1000  = 10 samples/s => One sample at every 0.1s, or, Ts=100ms. Or course, it can be much faster than this; this rate is only to make it easy fo visually follow the output.
You can modify them either in the source code or in CubeMX. If you modify directly in the source code, your changes will disappear the next time you re-generate the source from CubeMX. So it is better to do it from CubeMX.


Going through the source:


Here are some main points about the source code. The important thing to remember is that CubeMXis a tool for initializing the hardware. It does not generate a program that is useful for anything. We need to write that. Let's go over the significant parts. (Some of the parts mentioned below are already filled in by CubeMX):

It is important to start with the includes for the USB CDC functions. We add:

 #include "usbd_cdc_if.h"

This is the array for the string sent over the virtual comm port (we add):

   char msgbuf[128];

From this line onwards is the meat of the matter:


   /* USER CODE BEGIN 2 */
 
The following shows how to send a string over the virtual comm port:
 
  sprintf(msgbuf, "Starting device.\r\n");
  CDC_Transmit_FS((uint8_t *) msgbuf, strlen(msgbuf));


Starting the timer and the DMA are user's responsibility:

  HAL_TIM_Base_Start(&htim3); //AO!: Start the timer.

The ADC and DMA are initialized with the following HAL function. It specifies which ADC to use, which array the results of the conversion are stored and the length of the transfer. The actual link between the ADC and DMA was set in stm32f1xx_hal_msp.c:
 

  HAL_ADC_Start_DMA(&hadc1, adcBuf, ADC_BUFLEN); //Link DMA to ADC1

The infinite loop follows. It simply waits for the ADC_FLAG to become TRUE, which is set by the DMA transfer complete callback function. When this happens, the ADC results have already been stored in adcBuf[ ] array. We format  them as a string and send over the virtual comm port:

      sprintf(msgbuf, "%d,%d,%d,%d\r\n", i,
          (int)adcBuf[0], (int)adcBuf[1], (int)adcBuf[2]);
      CDC_Transmit_FS((uint8_t *) msgbuf, strlen(msgbuf));


CubeMX links ADC and TIM3 as:

static void MX_ADC1_Init(void){


The next significant part is the modification of the DMA transfer complete callback function, which sets ADC_FLAG. We add:


/* USER CODE BEGIN 4 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc1){
  ADC_FLAG=TRUE;
}


This is essentially all the important changes to main.c. There is one extra. I modified the error handler so that it sends the file and line number of where the error function was called, and then blocks there while flashing the LED at 20Hz. If the virtual comm port ever came up correctly by the time an error occurred in your program, you should see where the error was generated. This is done in _Error_Handler function.

The next significant file is stm32f1xx_hal_msp.c where the link between TIM3, ADC and DMA are set around the line /* ADC1 DMA Init */ (already done by CubeMX):

 __HAL_LINKDMA(hadc,DMA_Handle,hdma_adc1);


So that's it. Please modify the code to suit your needs. "Share and Enjoy!"


PS: If you are new to STM32 programming in GCC, please see my blog on how to set up a development environment for it.

Wednesday, May 6, 2020

Flysky i6 - Suddenly broke down!


My previous blog was about building a flight sim controller using a regular Flysky i6 transmitter. It worked nicely for some time, until one day, the screen suddenly went blank, and it was completely dead...

I checked the regular suspects. The voltage regulators for the processor and the RF module worked fine and everything looked OK. It just would not turn on at all. I was thinking that probably I shorted the 5V coming from the USB port to the 3V3 line on the board for an instant and burned some of the main chips. I gave up on the device and started to build my custom joystic interface electronics and new firmware that read i6's potentiometers as the input to the joystick interface. I was planning to bypass all the electronics of the i6, and use only its sticks (analog voltages of the pots are nicely broken out at the top, just below "CON8").

The day to start wiring it up to the joystick interface. I switched the i6 on out of usual habit. It turned on normally and worked! WTF! Alright, it means intermittent connections. However, it worked only for a short time and went bad again. Such intermittent operation continued from then on. Usual suspect in this case, is bad connections (solder joints, connectors, wires etc.). Looking at the board under a makeshift microscope, the EEPROM chip seemed to have suspicious soldering. I resoldered it. No difference. Then I noticed the top cover of the crystal was bent inwards, as if struck by something. I did not have a 4 pin 5032 SMD package 8MHz crystal laying around, so I unsoldered it and replaced it with a through hole regular crystal. It did work a bit better but still went bad from time to time. When it did work, pressing on the processor package would kill it. But looking at the processor solering, all looked fine.

Eventually, I laid my eyes on the large RF module. I had not given it much thought until then, because in regular RC transmitters, the RF module simply receives input signals from the rest of the circuit. Even if it is broken, the rest of the system would still work. But this one is different; it has bi-directional communication with the processor for setup and running. If it does not cooperate, the processor cannot go further in the program. It had one of the worst soldering job that I have seen on Chinese products (too large for the pick and place machine, it was probably hand soldered). Fairly wide pin spacing, so it was easy to re-solder it. Well, that was the problem. After the re-soldering, it has worked flawlessly since.

The bad solder joints on the RF module...
In post-mortem, I understood why it broke. I had removed the PCB from the transmitter to check some of the soldering side traces to the trainer port. However, the PCB is held very tightly in plastic standoffs and requires much  force to get it out. The RF module is fairly large and there is no strain relief betwen it and the PCB. So all three conditions must have come together and probably caused some solder connections or their PCB traces to crack. The processor then received no reply from the RF module at startup, and the whole system simply hang.

Now, some rant. There is obviously a reason why these units are so cheap compared to brand name products. It is inevitable that factory reject parts or shady components find their way into them. The plastic or wiring is not the best quality. In this case, the crystal was obviously of dubious quality. I still think that Flysky i6 is exceptionally well made, and if you don't fiddle with it, it will give years of good service. I also think that it is a great platform to tinker with, especially if you do  not use it for a critical system, such as a flight simulator.

So there it is. If your Flysky i6 breaks down and goes completely blank at some point, the RF module re-solder is worthy of a repair candidate.