I was not afraid of writing driver code for the ADC module, and I did. When I later built the NodeMCU firmware, I found that this driver was already written and available for inclusion. Nevertheless using such a driver would be difficult without reviwing the ADC datasheet, and I did not worry too much for reinvenbting the wheel. Hence the title of this log.
Note 1 : why NodeMCU firmware (Lua interpreting language) was selected for this development
The ADS1x15 driver was developed on top of the i2c module available from the NodeMCU firmware build.The code checks the presence of the i2c module in the build first
-- check presence of the i2c driver if require('i2c') then print("i2c driver present; execution continue") -- otherwise the LUA interpreter flags an error end
After that some variables are initialised with particular values to refer to these values by name (interpreting languages do not support C-like #define)
-- DEFINE section - constants to use -- NodeMCU specific id_i2c = 0 -- not to get confused with compulsory 0 in the code -- WEMOS mini specific sda = 2 -- SDA line on the i2c WEMOS mini shields scl = 1 -- SCL line on the i2c WEMOS mini shields -- ADS breakout board default ads1x15=0x48 -- connection on the breakout board -- ADS operating ranges (datasheet table 8 p. 26) ranges = {6.144,4.096,2.048,1.024,0.512,0.256}
Three high level functions operate the ADC as specified in its datasheet; these must be presented to the Lua interpreter before the first call
-- THREE ADS DRIVE FUNCTIONS -- ADS driver function - start a singe conversion -- ch 0..3 (MUX), ran 0..5 (PGA) -- datasheet ADS1x15 p.26, table 8 function ads1x15_single_start(id_i2c,dev_addr,ch,ran) i2c.start(id_i2c) tmp=i2c.address(id_i2c, dev_addr, i2c.TRANSMITTER) -- i2c.write(id_i2c, 0x1) -- select the CONFIG reg then write to it -- -- 1xxx xxxx start a single conversion = 8 (0 for continuous) -- x1xx xxxx single ended mode = 4 -- xx?? xxxx channel from the user -- xxxx ???x measurement range -- xxxx xxx1 single shot mode i2c.write(id_i2c, (8+4+ch)*16+(ran*2+1) ) -- MSB -- -- 000x xxxx lowest data rate (applicable for continuous mode) -- xxx0 xxxx comparator mode - traditional (default) -- xxxx 0xxx comparator polarity - active low (default) -- xxxx x0xx non-latching comparator action (default) -- xxxx xx11 keep RDY pin in high-impedance state i2c.write(id_i2c, 0x03 ) -- LSB -- i2c.stop(id_i2c) -- return tmp -- the device should acknowledge its address end -- ADS driver function - check that singe conversion is completed function ads1x15_not_ready(id,dev_addr) i2c.start(id_i2c) tmp=i2c.address(id_i2c, dev_addr, i2c.TRANSMITTER) i2c.write(id_i2c, 0x1) -- select the CONFIG reg i2c.stop(id_i2c) -- read it i2c.start(id_i2c) tmp = i2c.address(id_i2c, dev_addr, i2c.RECEIVER) tmp = i2c.read(id_i2c,2) -- i2c.stop(id_i2c) if (string.byte(tmp,1)<128) then return true else return false end end -- ADS driver function - read the last conversion's result function ads1x15_read(id,dev_addr) i2c.start(id_i2c) -- select the CONVERSION register tmp=i2c.address(id_i2c, dev_addr, i2c.TRANSMITTER) i2c.write(id_i2c, 0x0) -- pointer to the CONFIG reg i2c.stop(id_i2c) -- read it i2c.start(id_i2c) tmp = i2c.address(id_i2c, dev_addr, i2c.RECEIVER) tmp = i2c.read(id_i2c,2) -- i2c.stop(id_i2c) return tmp -- string of two bytes is returned end -- ADS driver function - init the device function ads1x15_reset(id,dev_addr) i2c.start(id_i2c) -- select the CONVERSION register tmp=i2c.address(id_i2c, 0, i2c.TRANSMITTER) i2c.write(id_i2c, 0x06) -- code for power down mode p.21 i2c.stop(id_i2c) -- return tmp -- string of two bytes is returned end
Next comes the code to initialise the driver and the ADC ( what is called setup() in Arduino programming )
-- setup and check the soft i2c peripheral i2c.setup(id_i2c, sda, scl, i2c.SLOW) print("SDA = ",gpio.read(sda)) -- check it is 1 (line pulled up whilst idle) print("SCL = ",gpio.read(scl)) -- check it is 1 (line pulled up whilst idle) print("ADS reset = ",ads1x15_reset(id_i2c,ads1x15)) -- test case ran=1 ch=1 -- when the code executes, change at will in the terminal
Finally, the following code, when presented to the interpreter, causes voltage measurement on a single defined CH pin using set RAN every 3 seconds with proper ADC readiness and presence checks. In addition, it measures the conversion time, prints raw bytes received from the ADC, and handles the issue of negative measured voltages (MSB is set in the returned data; NodeMCU/Lua seems not to support signed 16-bit integers).
-- measure each 3 seconds tmr.alarm(0,1000,tmr.ALARM_AUTO, function () if ads1x15_not_ready(id_i2c,ads1x15) then print("ADS1x15 has not finished a conversion") return end tmp=ads1x15_single_start(id_i2c,ads1x15,ch,ran) if tmp then print("") else print("ADS1x15 does not respond") return end t1=tmr.now() while ads1x15_not_ready(id_i2c,ads1x15) do tmr.delay(1000) end print("Convertion took > ",tmr.now()-t1," us") rez=ads1x15_read(id_i2c,ads1x15) print("Raw bytes = ",string.byte(rez,1,2)) if string.byte(rez,1)<128 then print("Reading, V = ",(string.byte(rez,1)*256+string.byte(tmp,2))*ranges[ran+1]/32768) else print("Reading, V = ",((string.byte(rez,1)-255)*256+string.byte(tmp,2)-255)*ranges[ran+1]/32768) end end )
When the code is running, one can change CH and RAN from the console, getting instant measuremnent results without the need to recompile the whole code. The code starts ADC measurement and checks whether it is completed every 1 ms; the typical value was found around 140 ms which was commensurate with the slowest sampling rate of the ADC of 8 samples per second.
Using the built in ADS1015 module makes the code more compact (but some of the constants may still look cryptic)
-- check presence of the ads1115 driver if require('ads1115') then print("ads1115 driver present; execution continue") -- otherwise the LUA interpreter flags an error end -- DEFINE section - constants to use -- NodeMCU specific id_i2c = 0 -- not to get confused with compulsory 0 in the code -- WEMOS mini specific sda = 2 -- SDA line on the i2c WEMOS mini shields scl = 1 -- SCL line on the i2c WEMOS mini shields -- ALERT pin of the ADC is not used at present -- module's constants were put into arrays for ease of alterations ranges={ ads1115.GAIN_6_144V, -- 2/3x Gain ads1115.GAIN_4_096V, -- 1x Gain ads1115.GAIN_2_048V, -- 2x Gain ads1115.GAIN_1_024V, -- 4x Gain ads1115.GAIN_0_512V, -- 8x Gain ads1115.GAIN_0_256V -- 16x Gain } channels={ ads1115.SINGLE_0, -- channel 0 to GND ads1115.SINGLE_1, -- channel 1 to GND ads1115.SINGLE_2, -- channel 2 to GND ads1115.SINGLE_3, -- channel 3 to GND ads1115.DIFF_0_1, -- channel 0 to 1 ads1115.DIFF_0_3, -- channel 0 to 3 ads1115.DIFF_1_3, -- channel 1 to 3 ads1115.DIFF_2_3 -- channel 2 to 3 } -- setup section i2c.setup(id_i2c, sda, scl, i2c.SLOW) -- set up the i2c controller ads1115.setup(ads1115.ADDR_GND) -- ADR to GND connection for the i2c address 0x48 ran=0 ch=0 rez=0. cnt=0 tmr_adc = tmr.create() -- measure each 200 ms tmr_adc:register(200, tmr.ALARM_AUTO, function() -- single shot setup -- data rate stated for correct timer callback setting ads1115.setting(ranges[ran+1], ads1115.DR_8SPS, channels[ch+1], ads1115.SINGLE_SHOT) -- start adc conversion and get result in callback after conversion is ready ads1115.startread( function(volt, volt_dec, adc) rez = math.floor(volt)/1000 cnt = cnt + 1 if cnt>10 then cnt = 0 print('Voltage = ' .. rez .. ' V') end end ) end ) tmr_adc:start() --tmr_adc:stop()This code was used for the complete design - it starts mesuring voltage from the single ended channel A0 of the ADC every 200 ms then reads the ADC converted voltage using callback mechanism after a particular delay. It outputs to the console one in ten measurements only in order to give a chance to other pieces of firmware outputting their diagnostic messages. (Note 2: how do the drivers handle delays for ADC conversions.)
The latter code was used for the first prototype.
Note 1: why NodeMCU firmware (Lua interpreting language) was selected for this development
A few words regarding the choice for firmware development. At the moment one can develop for ESP using Arduino ecosystem, AT commands, Basic, native C (C++), Espruino (JavaScript), microPython, NodeMCU (Lua) - this alphabetical list may not be exhaustive. All of these options were developed, supported and used for projects by a number of smart and inspirational people. My background is with embedded development and not computer networking. For this reason I personally found it easier to use NodeMCU as this was the first open and comprehensive interpreting system with zero cost software toolset. (Espruino and microPython came later, I supported development of both through Kickstarter as I do believe people with other background would benefit from these.) The advantage of interpreting environment is that you do not need to wait a couple of minutes to compile the code then some time to program the flash code memory; instead you can type your instructions and get instant feedback. I did require this option when I tried to figure out how to operate, for example, a UDP listener. The drawback of interpreters is the need to store a substantial code that taxes resources of the computer system, and slower execution because the interpreter needs to process any repeated instructions every time they need to be executed. A perfect combination would be to use an interpreter for debugging then compiler for deployment but this combination does not seem to exist for the ESPs. For the above reason this development was conducted using NodeMCU firmware (Lua interpreter built on top of the Espressif SDK + optional custom modules).
An added complication for using NodeMCU/Lua is its RTOS-like behavior. Howevre this feature may became a serious advantage when developing a real application.
Of course there are other compliucations, four of which are discussed here.
Note 2: how do the drivers handle delays for ADC conversions
My driver checks whether the last conversion was completed then reads the data. ADS1115 driver simply reads the ADC's output register after a set delay. For this reason it is important to state the data rate that sets a delay which is long enough to complete the conversion, i.e. ADS115.DR_8SPS .
TODO: the ADC can report completion of the present conversion via its ALERT pin. By connecting this pin to an unused Wemos mini pin one can trigger an interrupt then the callback function will simply need to read the converted value.
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.