0

I want to make a simple digital voltage monitor. So I have a sketch:

void setup() {
  Serial.begin(115200);
}

void loop() {
  int val = analogRead(A4);     // read the input pin
  val = 1;
  byte buf[4];
  buf[0] = 0;
  buf[1] = 0;
  buf[2] = val & 255;
  buf[3] = (val >> 8) & 255;
  Serial.write(buf, 4);             // report voltage value
}

When I tried to read serial buffer on PC host, I noticed that sometimes 1 byte can be lost, which lead to shifts and headache. How should the streaming be done?


PS.

  • host os: win7 x64;
  • language: c/c++
  • com port library: CSerial

Loop is as follows:

typedef int32_t sample_t;
while (true) {
    int nBytesRead = serial.ReadData(lpBuffer, bufSize * sizeof(sample_t));
    size_t numRead = nBytesRead / sizeof(sample_t);
    if (numRead > 0)
        std::cout << "read: " << numRead << " samples\n"; // Calculate average here!
}
  • Either by printing it as simple ASCII, or by wrapping the values in a proper packet format with DLE. – Majenko Jan 8 at 14:42
  • @Majenko What is a "proper DLE packet"? I cannot find any reference on what DLE could stand for. – xakepp35 Jan 8 at 14:45
  • 1
    That would give you \r\n as the ending - wasting a byte. Serial.print(val, HEX); Serial.print("\n"); saves that one byte (if you care). Then, yes, parse it on the host (which is simple enough). – Majenko Jan 8 at 15:00
  • 1
    @xakepp35 For higher baudrates, you can try a arduino leonardo or micro. They have usb connected to the microcontroller itself and the communication is with usb speed because there is no real serial port. What about calculating the average in the arduino? – Jot Jan 8 at 15:40
  • 1
    The arduino mega has a 10-bits adc. Everything (linearity, noise, and so on) matches those 10 bits. I think a good start is an external 16-bits adc: adafruit.com/product/1085 – Jot Jan 8 at 16:12
0

With help in a comments I've managed to deal with the task myself.

First of all, as @Majenko pointed out, I was dealing with some multi-byte data. So I need some control character, that separates packets. I choose hex output with '\n' delimeter as control character - that served good. I was able to read 3.5 to 5k packets per second (with voltage reading) from serial on the host.

Second improvement, credited to @Jot, is to stack some sequential values right in arduino, before sending.

Alongside with increasing sampler frequency that could be very good.

#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))

void setup() {
  sbi(ADCSRA, ADPS2);
  cbi(ADCSRA, ADPS1);
  cbi(ADCSRA, ADPS0);
  Serial.begin(115200);
}

void loop() {
  int val = 0;
  for( int i = 0; i < 32; ++i )
    val += analogRead(A0); 
  Serial.print(val, HEX);
  Serial.print("\n");
}

at the host recieve side, some simple conversion could be done:

#include "Serial.h"
#include <iostream>
#include <fstream>
#include <string>
#include <ctime>

// sample to voltage
double s2v(int sample) {
    return 5.0 * (static_cast<double>(sample) / 1024);
}

int main() {
    std::ofstream ofs("test.csv", std::ios::out | std::ios::ate);

    size_t comPort = 3;
    size_t baudRate = 115200; // 57600;

    CSerial serial;
    if (serial.Open(comPort, baudRate))
    {
        std::cout << "COM" << comPort << " opened @" << baudRate << "bit/s\n";

        // serial recv buffer 
        size_t bufSize = 1048576;
        auto lpBuffer = new char[bufSize];

        // accumulator
        size_t nVals = 0;
        int nAcc = 0;

        // timer
        __time64_t destTime;
        __time64_t destTime2;
        _time64(&destTime);

        while (true) {
            SleepEx(10, false); // care of 100% cpu core usage, poll serial port lazily
            size_t nBytesRead = serial.ReadData(lpBuffer, bufSize * sizeof(lpBuffer[0]));

            //parse buffer:
            size_t cvtPtr = 0;
            for (size_t i = 0; i < nBytesRead; ++i) {
                if (lpBuffer[i] == '\n') {
                    try {
                        lpBuffer[i] = '\0';
                        auto val = std::stoi(lpBuffer + cvtPtr, nullptr, 16);
                        nAcc += val;
                        nVals++;
                    }
                    catch (...) {
                        // ignore, or warn that serial recieved junk/empty packet
                    }
                    cvtPtr = i + 1;
                }
            }

            //check if doom had come!
            _time64(&destTime2);
            if ((destTime2 - destTime) > 0) {
                auto fVoltage = ( s2v(nAcc) / 32 ) / nVals;
                std::cout << fVoltage << "\t(" << nVals << " ksps)\n";
                ofs << destTime << "," << fVoltage << "," << nVals << "\n";
                if((destTime % 30 ) == 0) // care of disk, write only once in 30 seconds
                    ofs.flush();
                destTime = destTime2;
                nVals = 0;
                nAcc = 0;
            }
        } // while
    } // if opened
    return 0;
}

That gives voltmeter with high precision, about ±0.5mV, that is updated every second.

Also! It was stated about 10bit adc, 5.0V/1024 = 4.88mV, but i got some subthreshold oscillations in vivo, adding samples up. So you are wrong, noise can be attenuated and resolution can be artificially pumbed up, in a trade for increased sampling time. Here is 14bit ADC built on top of 10bit ADC: enter image description here. Its photovoltaic cell in a dark street, oscillations are caused by cars' front lights. Timescale is 1000 seconds per grid div

  • Don't you mean +-5mV? Because 5V / 1024 (10 bit) is about 4.9mV – chrisl Jan 8 at 20:01
  • @chrisl I mean i got 3-4 bits more resolution that was specified for arduino 10-bit ADC. – xakepp35 Jan 8 at 20:17

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.