1

The maximum write speed I can achieve is 2.4 KB/s. Is there a way to increase this?

Using LUA on a NodeMCU ESP8266 and the SPI module in User_Modules.h. #define BUILD_FATFS is also enabled in user_config.h.

I have a datalogger that is sampling 920SPS or ~1.1ms/Sample for 10 hours at a time. 1.1 ms should be lots of time to write two Bytes to a SD card or a buffer of xxx Bytes in between samples, however the max write speed I see is 498 ms to write 1200 Bytes or 7ms to write 3 Bytes. This is a long way from SD class 0 standard of 12.5MB/s. The logger ends up missing ~450 Samples when I dump 1200 B to the card.


local adc1 = nil
local t_tbl={}
local n=1

function adcReady(_,_,c)
    
    _,_, adctbl[n], _ = adc1:read()
    n=n+1
    if n>400 then
    
        t_tbl[1]=tmr.now()
        
        file.open("/SD0/sddata.txt","a")
        for k,v in ipairs(adctbl) do 
            file.write(v..",")
            adctbl[k]=nil
        end
        file.close()
        
        t_tbl[2]=tmr.now()
        
        print(t_tbl[2] - t_tbl[1])
        n=1
        
    end
end

do
    local adc = {
        ADC1_ID             =   0,
        ADC1_ADDRESS        =   ads1115.ADDR_GND, 
        GAIN                =   ads1115.GAIN_4_096V, 
        SAMPLES             =   ads1115.DR_920SPS, 
        CHANNEL             =   ads1115.SINGLE_0,       
        MODE                =   ads1115.CONTINUOUS, 
        CONV_READY          =   ads1115.CONV_RDY_1, 
    }
    i2c.setup(i2c0.id, i2c0.sda, i2c0.scl, i2c0.speed)
    ads1115.reset()
    adc1 = ads1115.ads1015(adc.ADC1_ID, adc.ADC1_ADDRESS)   
    adc1:setting(adc.GAIN, adc.SAMPLES, adc.CHANNEL, adc.MODE, adc.CONV_READY)  
    
    spi.setup(1, spi.MASTER, spi.CPOL_LOW, spi.CPHA_LOW, 8, 2, spi.HALFDUPLEX)
    vol = file.mount("/SD0", 8)   -- 2nd parameter is optional for non-standard SS/CS pin
    file.open("/SD0/sddata.txt","w+")
    file.close()
    
    tmr.create():alarm(1000,tmr.ALARM_SINGLE,function()
        gpio.mode(i2c0.conv_rdy,gpio.INT) 
        gpio.trig(i2c0.conv_rdy,'up', adcReady) --enable interrupt, active low rising edge==conv ready  
    end)
end
Micke
  • 33
  • 3
  • It’s funny to choose cheapest ever solution and try to achieve hi-end quality out of it. Change your tools to be C written for the starter. What you are using is for kids, no serious project can may ever consider Lua on ESP8266 for RT closer tasks. – 0andriy May 05 '21 at 23:01
  • Oh, I did not know LUA was for kids. I just started using esp8266 and I dislike arduino's mash of c and c++. Thought LUA would give me a new challenge of learning a new language in my spare time. – Micke May 05 '21 at 23:05
  • Lua is fun language, the problem that you are running it on the platform that is overloaded by other tasks. – 0andriy May 05 '21 at 23:06
  • Arduino is C++, which is based on C. It's not a "mash of C and C++". There are still plenty of other reasons to dislike it, though. – romkey May 06 '21 at 04:47
  • 3
    @0andriy I doubt this will convince you but Lua is certainly not (just) for kids. https://en.wikipedia.org/wiki/List_of_applications_using_Lua. If NodeMCU Lua is good enough to run commercial ESP8266/ESP32 home security systems (not home automation) it's possibly good enough for a lot of other use cases. That being said it would be interesting to see a speed/throughput comparison for this specific task between NodeMCU Lua and a native C/C++ solution using e.g. Arduino core. – Marcel Stör May 06 '21 at 05:56
  • @MarcelStör hm... _If NodeMCU Lua is good enough to run commercial ESP8266/ESP32 home security systems (not home automation) it's possibly good enough for a lot of other use cases._ Logic 101: if A is true, it doesn’t mean B is also true. – 0andriy May 06 '21 at 07:42
  • 1
    And @MarcelStör, you seem missed my point completely, I would suggest to re-read my comments above. – 0andriy May 06 '21 at 07:48

1 Answers1

3

You can speedup file write by preparing 2Kbyte-aligned chunks of text.
Replace your adcReady with:

local log_text = ""
local chunk_size = 2*1024

function adcReady(_,_,c)
   _, _, adctbl[n], _ = adc1:read()
   n = n + 1
   if n > 400 then
   
      t_tbl[1] = tmr.now()
      
      log_text = log_text..table.concat(adctbl, ",", 1, n-1)..","
      local size = #log_text - #log_text % chunk_size
      local log_text_to_save = log_text:sub(1, size)
      log_text = log_text:sub(size + 1)
      
      t_tbl[2] = tmr.now()
      
      if size ~= 0 then 
         file.open("/SD0/sddata.txt","a")
         file.write(log_text_to_save)
         file.close()
      end
      
      t_tbl[3] = tmr.now()
      
      print(t_tbl[2] - t_tbl[1], t_tbl[3] - t_tbl[2])  -- for strings and GC, for File operations
      n = 1
      
   end
end

Is it faster than 498 ms?


UPDATE:

New version with cached tostring()

local num2str = {}

function adcReady(_,_,c)
   _, _, adctbl[n], _ = adc1:read()
   n = n + 1
   if n > 400 then

      t_tbl[1] = tmr.now()

      for i = 1, n - 1 do
         local v = adctbl[i]
         local s = num2str[v]
         if not s then
            s = v..","
            num2str[v] = s
         end
         adctbl[i] = s
      end
      local log_text_to_save = table.concat(adctbl, "", 1, n-1)

      t_tbl[2] = tmr.now()

      file.open("/SD0/sddata.txt","a")
      file.write(log_text_to_save)
      file.close()

      t_tbl[3] = tmr.now()

      print(t_tbl[2] - t_tbl[1], t_tbl[3] - t_tbl[2])  -- for strings and GC, for File operations
      n = 1

   end
end

Is it faster than previous version?


UPDATE2:

local chr = string.char

function adcReady(_,_,c)
   _, _, adctbl[n], _ = adc1:read()
   n = n + 1
   if n > 400 then

      t_tbl[1] = tmr.now()

      for i = 1, n - 1 do
         local v = adctbl[i]
         -- 0<=v<=4095
         local s
         if v < 10 then
            s = chr(v + 48, 44)
         else
            local m10 = v % 10
            if v < 100 then
               s = chr((v - m10)/10 + 48, m10 + 48, 44)
            else
               local m100 = v % 100
               if v < 1000 then
                  s = chr((v - m100)/10 + 48, (m100 - m10)/10 + 48, m10 + 48, 44)
               else
                  local m1000 = v % 1000
                  s = chr((v - m1000)/1000 + 48, (m1000 - m100)/100 + 48, (m100 - m10)/10 + 48, m10 + 48, 44)
               end
            end
         end
         adctbl[i] = s
      end
      local log_text_to_save = table.concat(adctbl, "", 1, n-1)

      t_tbl[2] = tmr.now()

      file.open("/SD0/sddata.txt","a")
      file.write(log_text_to_save)
      file.close()

      t_tbl[3] = tmr.now()

      print(t_tbl[2] - t_tbl[1], t_tbl[3] - t_tbl[2])  -- for strings and GC, for File operations
      n = 1

   end
end

UPDATE3:

For Lua 5.3 and hex digits in log:

-- log output is in hex
local high = {} -- [1] = "", [2] = "1", ..., [256] = "FF"
local low = {}  -- [1] = "0,", [2] = "1,", ..., [16] = "F,"
for x = 0, 255 do  -- replace 255 with 127 (to save memory) if ADC generates only positive values 0x0000-0x7FF0
   high[x+1] = string.format("%X", x*16):sub(1, -2)
   if x < 16 then
      low[x+1] = string.format("%X,", x)
   end
end

-- in case of out-of-memory error reduce measures count (400) to 256
local measures = 400   -- recommended values are powers of 2
local measures_2 = measures*2

-- adctbl[] is not used anymore, text_buffer[] is used instead
local text_buffer = {}  -- array of (2*measures) elements
for x = 1, measures_2 do
   text_buffer[x] = ""
end

function adcReady(_,_,c)
   local _, _, v = adc1:read()
   -- 0x0000<=v<=0xFFF0
   text_buffer[n] = high[(v>>8)+1]
   text_buffer[n+1] = low[((v>>4)&15)+1]
   n = n + 2
   if n > measures_2 then

      t_tbl[1] = tmr.now()

      local log_text_to_save = table.concat(text_buffer, "", 1, n-1)

      t_tbl[2] = tmr.now()

      file.open("/SD0/sddata.txt","a")
      file.write(log_text_to_save)
      file.close()

      t_tbl[3] = tmr.now()

      print(t_tbl[2] - t_tbl[1], t_tbl[3] - t_tbl[2])  -- for strings and GC, for File operations
      n = 1

   end
end
Egor Skriptunoff
  • 23,359
  • 2
  • 34
  • 64
  • That is much faster. About 250ms average. Still won't work as it misses 230 samples when it writes at 400 samples, or 57%. `238985 888, 243066 14247, 247259 6542, 246113 6399, 249848 6572, 254208 6358, 244630 6476, 243958 6331, 246767 7765, 247258 12558, 250222 15281, 242579 5383, 247921 5225, 249047 87, 244824 6217, 242633 6105, 245013 5339, 245346 5220, 247794 12641, 242356 6075, 244482 5397, 230294 5237, 235795 6283, 239691 6065, ` – Micke May 07 '21 at 20:29
  • Is there any way to have the file Open globally and stay open for the duration of the program? Open/Close == 2.5ms/2.7ms while writing 3 bytes (12 bit adc and ',') takes 0.188ms. It would work if the sample could be written every interrupt without having to open/close the file. – Micke May 07 '21 at 20:49
  • 3
    As you see from the measures, the script spends 250 ms on strings operations (convert number to string and concatenate) and 6 ms on saving to file. Your SD card is fast! 6 ms = 2Kbyte, 0.3 Mbyte/sec. Lua strings are slow. 250 ms is a price you have to pay for creating human-readable log file. – Egor Skriptunoff May 07 '21 at 22:34
  • 1
    Check new version (see update in the answer). It might be faster. – Egor Skriptunoff May 07 '21 at 22:55
  • 2
    This is very clever, using num2str as a dynamically created lookup table. You are right, I incorrectly assumed the SD spi write speed is slow when it is actually the data type conversion in LUA. The updated code is much faster <20ms for 400 samples, however num2str kills the stack with a 12bit adc and the mcu crashes causing a reset. These are really good examples of buffers I have not considered before and will use them in other projects. ``13194 3264, 13183 4359, 13192 3116, 13180 3228, 13172 4203, 13166 4214, 13155 3598, 13140 6142, 13203 3022, 13159 3366, 13197 3223`` – Micke May 08 '21 at 20:50
  • 1
    If values are 0..4095 then try code from update2. – Egor Skriptunoff May 08 '21 at 21:57
  • The [ads1015](https://www.ti.com/lit/ds/symlink/ads1015.pdf?ts=1619582676446&ref_url=https%253A%252F%252Fwww.ti.com%252Fproduct%252FADS1015) returns the value in the upper three words {0x0000, 0x0010,0x0020..0x7FF0} so I added `v=v/16` before `local s`. (0 to 7FF for single ended mode, 0-FFF for differential.) The times: `37856 2986` for 0x67A(Vcc); `11994 1697` for 0x000; and `161868 2897` for everything in between. – Micke May 09 '21 at 22:27
  • 1
    What Lua do you have: 5.1 or 5.3? – Egor Skriptunoff May 10 '21 at 00:27
  • Lua 5.3 on Nodemcu with LFS 64k – Micke May 10 '21 at 04:07
  • 1
    For Lua 5.3 you can use bitwise operators, see update3 – Egor Skriptunoff May 10 '21 at 13:42
  • Apologies for the delay. It is running lua5.1 and not 5.3 as I assumed. I tried to compile 5.3 but could not figure makefile in windows. So I change >> to bit.rshift(), & to bit.band(). Changed measures=>256 as you commented. Removed Open,Close() and left the file open globally. New times are really, really impressive: (random Vin) `9354 808; 9541 1605; 9786 1858; 9516 1645; 9523 1913; 9758 807; 9622 1884` – Micke May 13 '21 at 18:56
  • Yep. divisions are very slow (*update 3* is division free). It's good you got maximum out of this eventually (you may even go faster with writing binary stream). – 0andriy Jun 15 '21 at 08:52