STM32F4-Discovery + ChibiOS = Data Acquisition System

Although I am mostly a hardware guy, I do enjoy mucking around in the embedded software world when I have the time. In fact, I think it is fundamental for every hardware engineer to have at least some basic notions of how the embedded software / firmware will interact with the hardware. Lately I’ve been playing with a STM32F4-Discovery board as I am interested in learning how to use ARM microcontrollers as they are taking over the scene at a frightening pace. I know C fairly well, and I’m familiar with the AVR microcontrollers as I’ve been using them for years, but ARM is a more complex beast and it comes with a steep learning curve, but worth the climb.

STM32F4-Discovery

STM32F4-Discovery Dev Board

I’ve played around with the STM Standard Peripheral Library and writing small programs on “Bare Silicon” for the STM32F4, but with so much processing power and features, it just made sense to try out a Real Time Operating System (RTOS) of some description. In addition, using an RTOS makes developing multitasking software a bit more manageable, and since I have absolutely no experience with RTOS-es, I thought this would be a good opportunity to learn this stuff.

After looking around for a few weeks, I settled on trying out a small RTOS called ChibiOS. It provides an excellent development environment called ChibiStudio (well, it is really Eclipse but configured for use with ChibiOS), and I was able to compile and upload firmware to the discovery board in a matter of minutes. I played around with ChibiOS for a few weeks on and off and posted a few questions on the forums. I must say the author of ChibiOS, Giovanni Di Sirio, provides excellent support and he answered each one of my questions within a few hours.

ChibiOS Logo

ChibiOS Logo

In addition to providing a real time kernel, ChibiOS also provides a Hardware Abstraction Layer (HAL) which makes configuring the microcontroller and using the various hardware resources much easier. It also makes it easier to port software between chips – for instance changing from a STM2F4 to a supported TI microcontoller. See the ChibiOS website for a list of supported platforms. ChibiOS comes with many examples and excellent documentation. The problem is that the documentation is in rigid and concise format, and I found the best way to get to grips with ChibiOS was to study the examples, understand them and modify them, occasionally referring to documentation and the reference manual of the chosen platform. This way of learning is very time consuming, and I often wished there were some more user-friendly (or newbie friendly) tutorials and examples. In this post I am hoping to provide a little more user friendly introduction to ChibiOS.

After studying and modifying a number of provided examples in ChibiStudio for the STM32F4, I finally took the plunge to write something of my own. My first attempt was a hack and paste mess in which I implemented a simple shell over USB. The program allows a user to access the STM32F4 hardware through a shell interface. For instance, typing “ADC0” at the shell prompt would return the value sampled by ADC channel IN0, or issuing a “PWM 0 43” would set one of the PWM pins (the one I decided to call PWM0), to output a 43% duty cycle PWM signal. I am not going to share this code right now as it is a hack and I want to clean it up and expand it, but getting this working nicely gave me the confidence to try something a little more challenging.

I was recently working on a project for a client who required a data acquisition system. The client already settled on which microcontroller will be used and wrote much of the firmware himself. I needed to design the hardware around his firmware. I thought I would give the basic firmware a go as well using the STM32F4. I aimed to implement ADC sampling of six analog channels at a precise sampling rate and to output the data in real time over a USB-Serial link to the computer. The host computer can then be used to capture the data and plot it (either in real time with Matlab or something of the sort, or at a later date).

I decided to sample each channel at 10kHz and output the data to the PC in ASCII format so it will be easier to work with it on the PC side. The alternative would be to send raw binary data from the ADC, and it would be much faster, but then the data needs to be interpreted correctly on the PC side.

To see the note click here.To hide the note click here.
Just a little note about this – ADC data ranges from 0 to 4095 (12 bit ADC). Sending this data as ASCII, means I am sending four ASCII characters (say ‘4’, ‘0’, ‘9’ and ‘5’) for each sample. Each character is 8 bits (one byte), for a total of 4 bytes or 32 bits. If I were to send this data in raw format, I would be sending a total of 12 bits (well 16 bits actually, as the 12 bit ADC data is padded out to 16 bits) for each sample. Hence, for 6 channels in ASCII format, not counting delimiters and serial control flow characters, I need to send out 6 x 32 = 192 bits or 24 bytes of data. If sending raw binary data, this reduces to 6 x 16 = 96 bits or 12 bytes. This is important as the transmission time needs to be minimized and has to be faster than the time between samples. As aforementioned, I am using ASCII data which takes longer to transmit as it is easier to work with ASCII on the PC side and with 10kHz sampling rate I have determined that I have plenty of time to send the data out in ASCII format before new data arrives. I am mentioning all of this in case someone will want to modify the program to achieve much higher data rates. Off the top of my head, making a few changes I think 30 kHz sampling of six channels should be possible. Also note that the ADC can sample much much faster than this, it is a limitation of getting data out through the USB-Serial link. If we were saving data to some kind of memory for instance, we could be running the sampling much faster.

 

Basically, I am aiming to write some firmware for the STM32F4 using ChibiOS which will allow me to use the STM32F4-Discovery board as a data acquisition system of sorts. It will constantly sample 10 ADC channels while outputting the results over a USB-Serial link to a computer. I chose to output all the channel data at once, separated by semicolons. This format is easy to work with and many data processing programs will happily take this format and split it accordingly.

My program does nothing until the user presses the blue “User” button on the Discovery board. At that point, a General Purpose Timer (GPT) is activated to trigger every 0.1 milliseconds and issue a callback function upon triggering. Inside the GPT callback function, I collect the ADC samples at that point in time (the ADC is continuously running in the background sampling values at much faster rate), and send those values as mailbox messages to the listening thread. Basically, there are three critical parts to the code:

  1. The GPT Callback Function
  2. The USB Communication Thread
  3. The Button Press Detection Thread

Let’s have a look these parts in turn. Firstly the GPT callback – as shown in the code snippet below:

/* GPT2 callback. */
static void gpt2cb(GPTDriver *gptp) {
     (void)gptp;
     palSetPad(GPIOD, GPIOD_LED6);
     chSysLockFromIsr();
     chMBPostI(&mb[0], (msg_t)samples[0]);
     chMBPostI(&mb[0], (msg_t)samples[1]);
     chMBPostI(&mb[0], (msg_t)samples[2]);
     chMBPostI(&mb[0], (msg_t)samples[3]);
     chMBPostI(&mb[0], (msg_t)samples[4]);
     chMBPostI(&mb[0], (msg_t)samples[5]);
     chSysUnlockFromIsr();
     palClearPad(GPIOD, GPIOD_LED6);
}

The callback for the timer is executed every time the timer triggers. The triggering of the timer is set by the timer configuration and the trigger value in the timer initialization. I’ve set mine to run at 100kHz and trigger on the value of 10, meaning it will trigger at 10kHz rate. So, every 0.1ms the callback is executed. It just turns on the blue LED (so I can do some timing profiling – I will elaborate on this later), then locks the kernel and sends six mailbox messages, each containing the ADC value of one of the six channels. The mailbox concept is one of those things which is new to me, but seems to be a standard method for inter-thread communication in the RTOS world. The mailbox is basically a FIFO queue between threads. Hence, the callback function dumps the six ADC values down the FIFO, unlocks the kernel and turns the blue LED off, every 0.1 millisecond. Thats it!

Let’s have a look at the USB communication thread next:


static msg_t USBCommsThread(void *arg) {
     msg_t msg;
     adcsample_t txsamples[6];
     static int i = 0;
     (void)arg;

     chRegSetThreadName("USBComms");

     while (TRUE) {
          for (i = 0; i < 6; i++) {
              chSysLock();
              chMBFetch(&mb[0], &msg, TIME_INFINITE);
              chSysUnlock();
              txsamples[i] = (adcsample_t)msg;
              if (i == 5) {
                   palSetPad(GPIOD, GPIOD_LED4);
                   chprintf(((BaseSequentialStream *)&SDU1),
                         "%d;%d;%d;%d;%d;%d\r\n",
                           txsamples[0], txsamples[1], txsamples[2],
                           txsamples[3], txsamples[4], txsamples[5] );
                   palClearPad(GPIOD, GPIOD_LED4);
                   i = -1;
               }
          }
     }
     return 0;
}

The thread is constantly in a loop. It waits for mailbox messages and when a message is received, it adds it to a local array of ADC samples. When a sixth message in a row is received, the transmission string is created with all the six values separated by semicolons. The string is transmitted over the USB CDC interface using the chprintf() function available in ChibiOS. The values are quietly converted to ASCII characters in the background. While the transmission process is underway, I set LED4 to high so I can see what is going on (on the oscilloscope).

One problem I had with this thread was that I was sending the samples as they arrive in the mailbox, instead of collecting them in an array as done in the code above. This resulted in calls to chprintf() happening six times more often, and if the blue user button is pressed while the computer is receiving data, the transmission can be cut off in the middle of the six samples somewhere. Next time the button is pressed, serial terminal would continue displaying data from where it left off, but the firmware would start by sending the first channel data and thus causing misalignment in the displayed data. Although if one is careful and aware of this issue it is not a big problem, I did not like it and opted for the approach shown above where the transmission happens “at once” for all channels. This way the process can not be interrupted mid way, and I also think it is more efficient as less calls to chprintf() are made.

Finally, we have the thread that listens to the button state.

static msg_t ButtonThread(void *arg) {
     (void) arg;

     chRegSetThreadName("Button");

     while (TRUE) {
          if (palReadPad(GPIOA, GPIOA_BUTTON)) {
               if (running) {
                    /* STOP */
                    gptStopTimer(&GPTD2);
                    running = 0;
               } else {
                    /* START */
                    gptStartContinuous(&GPTD2, 10); /* 10kHz trigger rate .*/
                    running = 1;
               }
           }
     chThdSleepMilliseconds(100);
     }
     return 0;
}

The thread just reads the state of the user button at intervals of 100 milliseconds. If a button press is detected and the system is not running, then the GPT is started. If the button is pressed and the system is running then the GPT is stopped. As the GPT callback basically controls the rest of the system, it is sufficient to control the GPT here. The ADC still keeps running in the background, so if power is a concern one might consider turning off the ADC in this thread to reduce power consumption.

The rest of the code contains the setup and initialization code for the peripherals used. This is well documented in the ChibiOS documentation and examples, so I will not go into details about it here. The full code for the main file is here.

Remember that we put the statements to turn on and off the LEDs in the GPT callback function and during the transmission of the ADC values over USB? This was so that I could make sure the timing is not marginal – that no messages will be lost in inter-thread communication. Basically, we need to make sure that once all six messages are passed to the USB Communication thread, the thread has enough time to transmit those messages before a new set of samples (messages) starts arriving. If this condition is not met, the mailbox will slowly get filled up and we will start losing messages or other weird things will happen.

Connecting the o-scope channels to LED6 and LED4, we can monitor the timing of getting samples from the ADC, sending them as mailbox mail, and how long it takes to transmit them. I connected my Analog Discovery to the relevant pins of the STM32F4-Discovery board (PD12 and PD15) to have a look at the timing. The image below illustrates the waveforms:

TX and Trigger Timing

Timing of the Sampling Trigger and Transmission Process

The orange trace is the LED6 signal triggered in the GPT callback function. It can be seen the callback triggers every 100uS = 0.1mS as expected. In addition, the pulse width of the orange trace tells us how much time we are spending in the callback function. The blue trace shows the signal driving the green LED4. It tells us how long the chprintf() function is taking to convert the values to ASCII (format them), and transmit them over USB CDC interface. It jitters a bit due to variable length of the ADC data, but at maximum it is around 35uS. This means that we still have quite a lot of time to transmit data before the next sampling trigger occurs. It also means we could bring the two orange pulses closer together – as long as they don’t interfere with the blue transmission pulse. To achieve that, we would trigger the timer at a higher frequency. Looking at the waveforms, transmission takes 35uS, there is a little bit of time after the GPT callback finishes and transmission starts (this is the time the USB Communications thread takes to receive the messages and stuff them into the array), and then a whole lot of downtime before the next trigger pulse at 100uS. This means we could make the trigger pulses around 40uS and still be safe for a sampling rate of 25kHz. I haven’t tried this, but it should be perfectly acceptable according to the timing (and my interpretation!).

Finally, I used a function generator to feed a 1 kHz signal into one of the channels, and then captured the data to a file using RealTerm. I then used kst2 to plot the values of the captured channel. As I am feeding in a 1 kHz waveform, and I am sampling at 10kHz, I am expecting to see 10 data points per period of the input sinewave.

Sine Wave Samples

Reconstruction of the 1 kHz sinewave after sampling

In fact, the plot of the reconstructed sinewave using the ADC samples shows there are exactly ten data points per sinewave period as expected. This confirms that we are indeed sampling at 10 kHz successfully. Keep in mind that the ADC is converting values at a much quicker rate in the background, but we are only taking values out of the circular ADC buffer every 100uS.

Well, that is my excursion into RTOS territory with ChibiOS for now. I quite enjoy using ChibiOS after the initial learning curve. I am still a beginner when it comes to ChibiOS but I finally managed to do something useful with it! I am sure there are better ways to achieve what I did, and if you know how feel free to leave a comment.

Finally, attached below is my ChibiStudio Project with all the required files and MCU configuration. You should be able to unzip the file, place it under the demos in the ChibiStudio distribution, and then select File->Import in Eclipse and choose “Import existing project into Workspace”. Then it should compile fine if everything is setup correctly on your end. In any case, I am providing a .hex file below as well, which you can download to the STM32F4 Discovery through STLink utility. Note that I had to rename the .hex file to .txt to be able to upload it. Please change the extension back to .hex once downloaded.

  • Project source files (zip archive) 
  • Project HEX file 

 

SHARE IT:

Commenting area

  1. Murali, P. March 14, 2015 at 4:12 am · · Reply

    Hi Igor,

    Thanks for this information. I am interested to use this code to sample 8 ADC channels each at 250 KHz with STM32F Discovery. May be sending the bits without converting to ASCII may be faster. I will try to use your code to make modifications and see if it works.

    Thanks,

    • Hi Murali,

      I think you can do this, but streaming the data out over USB might be a problem. Off the top of my head, you have 8 channels at 250kHz = 8 x 250k = 2 000 000 * 16 bits (lets assume each sample is 16 bits) = 32 000 000 = 32Mbit. USB low speed (1.5Mbit) and USB full speed (12 Mbit) would be too slow, but USB high speed (480 Mbit) should work. You just need to check if that mode is supported by the STM32 and how to set it up.

  2. Murali, P. March 16, 2015 at 8:57 pm · · Reply

    Hi Igor,

    How did you program the board ? Did you use the RS232 adapter ?

    Thanks,

    • I just used STM provided ST-Link software and the USB cable straight to the STM32F4Discovery board to download the HEX (or was it BIN?) file generated by ChibiStudio. STM32F4Discovery already has a built-in ST-LINK/V2 adapter on board (the smaller processor on there). There is no true RS232 on the discovery board as far as I can recall off the top of my head.

  3. Hi Igor,

    I was implementing the similar analog acquisition method as the one you show here. However, I was using STM32F0 Discovery instead. I wanted to sample analog input from 4 channels at a constant rate of 1 kHz, and I checked my GPT callback via oscilloscope and LED on/off, and it showed 1.0 kHz.

    Then, I start posting the data via Mailbox to a thread. However, when the thread fetching the data and I printed the fetched data to a console (using chprintf), it resulted in a dropped rate to about 250 Hz.

    I was able to prove this by feeding my channel #0 with a 50 Hz sinusoid signal from a function generator, printed the data to the console and saved the log, and then plot the data. Instead of getting 20 points per period (1 kHz sampling / 50 Hz signal), I got 5 points per period instead = 250 Hz sampling.

    Do you know what might cause this drop in sampling / Mailbox transmission?

    • Hi Sam,

      Might be a bit late know but a few things come to mind. First are you sure the serial port / USB is set to high enough baud rate to support sending the data at 1KHz? Second, as you are aiming at 1KHz sampling, and are getting 250Hz and 4 channels are involved, are you sure you are sending / sampling all 4 channels on each ADC pass? Since 1Khz/4=250Hz it seems as if you are either sampling each channel on every 4th pass or you are not sending all off the channel data on each pass?

      Hope that helps!

  4. HI Igor !

    I’m new in programming and was searching for a long while an economical way of serial communication (not one like with aMG USB Converter), finally got one 🙂

    I’ve downloaded ChibiStudio but I don’t know who to work in it and run your code. I want to sample one ADC channel and plot the data in Simulink or even in worst case in kst. Will you please help me with this ?

    • Hi,

      To get started with ChibiOS I suggest you read the documentation and the reference manual on the ChibiOS website, as you can ask any ChibiOS specific questions on their forum. If you are completely new to embedded platforms and programming, you might want to have a look at Arduino as an easier way to achieve what you are after.

  5. Hi Igor,

    Very good job done by you. I have done everything according to your tutorial. I am not understanding how to interface with REALTERM with discovery board. Can you share the realterm terminal configuration? have you used extra ttl logic coverter for usb interface? waiting for your reply.. Thanks

  6. Sanket Sarkar March 10, 2017 at 2:20 am · · Reply

    Hello Mr IGOR.

    I have confgiured the STM32F board according to your instructions but am not being able to interface and get the recordings with REALTERM software. I would be greatly oblighed of you tell me how to proceed in this area. Also how to activate the virtual usb and use it with REALTERM and STM32F is not clear.

    Waiting for your valuable reply,

    Thanking in advance.

  7. Nikola Georgijevic June 19, 2017 at 10:41 am · · Reply

    Hi Igor,

    This is a great article. Thank you for sharing it with us (Хвала ти ко брату).
    Which version of chibios have you used for this project?

    regards,
    Nikola

    • Hi Nikola,

      Glad you found the article useful. The ChibiOS version is an older one (now), I believe the ChibiOS 2 branch but can not recall exactly which version.
      Pozdrav,
      -Igor

  8. Hi Igor,
    I wanted to sample 32 channel 10khz analog signal using on-board ADC, is it possible or should i need external ADC? In addition, what storage should I use to store the data? Would SD Card sufficient, or do I need direct usb connection to a PC? I’m kinda new with this board and I used your awesome article as my main reference.

    -Michael

Leave a Reply

You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>