Quantcast
Channel: Articles on JeeLabs
Viewing all 296 articles
Browse latest View live

Forth in 7 easy steps

$
0
0

Forth is a simple language, with simple rules and a simple execution model. It’s also self-hosted and interactive - you can type this at the prompt:

1 2 + .<cr>

What you’ll get as response is this (details may differ, everything below is for Mecrisp Forth):

1 2 + . 3  ok.<lf>
       ^^^^^^^^^^^ added by Forth

All code is parsed into a sequence of “words”, which is anything between whitespace. The above code consist of 4 words: 1, 2, +, and . - these words operate on a data stack. Numbers “push themselves” onto the stack, i.e. first 1, then 2. The word + is predefined, it replaces the two stack entries by a single one, their sum. The word . pops the top value off the stack and prints it as number followed by a space.

Since there are no more words left, Mecrisp then prints ok. followed by a linefeed, and waits for a new line of text to parse, so it can process more words.

That’s it. The essence of Forth. A few more steps like this, and you’ll have the complete story.

To define this as a new demo word, type this (let’s omit the obvious closing <cr> from now on):

: demo  1 2 + . ;

The : word starts a definition and ; ends it. Nothing interesting happens, but now this works:

demo 3 ok.
    ^^^^^^ added by Forth

New words must be defined in terms of existing ones. Here’s another definition:

: JC's-triple-demo  demo demo demo ;

Word names can be anything other than whitespace (including UTF-8, and even 1 or 2 - but that could lead to major confusion).

That’s step 2, now you know how to extend the language.

The definition of words is an important mechanism. You’ve already seen the data stack, but there is also a stack for words and their compiled code, which is called the “dictionary” in Forth. New words are added to the end, and words are looked up in reverse order, so that the last one will be used when words are re-defined.

The : word is quite special: it will parse the next word in the input stream and add it as a fresh definition to the dictionary. It also sets a “state” flag to compile mode. And then it returns to the main loop in Forth to process all remaining words.

And here’s The Big Trick, part 1: the main loop will parse more words, but since the state flag is set, it will append calls to these words to the dictionary instead of executing them.

At some point, Forth will need to finish the definition and return to run mode.

The Big Trick, part 2: a word can be marked as “immediate”. When this is the case, it overrides the state logic in the main loop, and gets executed right away, even in compile mode. So there’s an immediate ; word in Forth, which does two things: add a “return” statement to the end of the dictionary, and reset the state flag to zero.

Immediate words enable magical behaviour in Forth, because they’ll switch back to run mode during compilation. They can do anything (or to be more precise: they are in fact the compiler).

That’s step 3. This is how Forth unifies “run mode” and “compile mode”.

Forth has conditionals and loops. Here’s a rewritten version of the above:

: looping-demo  3 0 do demo loop ;

This should be readable by now: 3 and 0 just get added to the stack, do pops them as loop limits (in funky reverse order), then comes the loop body, followed by loop which presumably knows how to count and repeat, and the closing semicolon to finish the definition of looping-demo.

It should come as no surprise that do and loop are immediate words. They append code to the dictionary to implement the do loop and use the data stack (while in compile mode) to track branch offsets. There’s also a word called i to push the current loop value on the stack. As you can see, common words in Forth tend to have very short names.

This example loops until a key is pressed (key? pushes a flag on the stack, which until uses):

: boring-demo  begin demo key? until ;

Since if, do, begin, etc generate code, they can only be used inside word definitions. You can’t use them interactively, i.e. in run mode, but you can enter a definition and call it, all on one line.

Immediate words are also used to implement if, else, then, and several other jump-based words. The funky order of else and then takes a little getting used to, but it’s trivial stuff:

: even-demo  123 2 mod 0 = if ." EVEN!" then ;
: even-odd-demo  123 2 mod 0 = if ." EVEN!" else ." ODD!" then ;

Several new words have been used here. The mod word is used to calculate modulo-two, = will compare two values, and ." ..." prints a string. Note the space after the opening quote: in Forth, everything is a word, so the print-string word is called ." and must be enclosed in spaces. Note also that the closing quote does not need a space in front, because the ." word plays special tricks with parsing.

Such unconventional syntax details come from the fact that Forth uses stacks and treats everything as words - the price of a simple uniform data + parsing model.

Congratulations, this was step 4, with a little peek inside the compiler!

Words can call other words, and do loops can be nested (it might help to view do loops as a special way to “call” their body repeatedly).

This nesting is actually what all other languages also have, using a “return” stack. In Forth, the return stack is separate from the data stack. This is what gives the language its concatenative properties (a term coined decades after Forth was invented).

When a defined word is executed, it pushes the current instruction pointer on the return stack, and starts executing its own code. When it returns, it pops the instruction pointer back off the return stack and resumes where it left off. Do loops also use the return stack to store some state.

The return stack is fully exposed in Forth, which allows some amazing tricks, such as a one-line coroutine implementation, as well as a simple collaborative multi-tasker implementation.

In day-to-day use, the data and return stacks are the only ones that matter. The dictionary (i.e. code stack) and a stack to allocate RAM variables from can safely be ignored most of the time.

What about other data? Here is a constant and a variable definition:

123456789 constant MY-CONST
987654321 variable my-var

Constants are just that, they can be used wherever a value is needed, and push their value onto the data stack when executed. The convention is to write them in uppercase, but Forth is case insensitive (for ASCII characters, not for UTF-8!), so it won’t matter during use.

A variable pushes its address. To fetch (and then print) the value, you need to use the @ word:

my-var @ . 987654321  ok.
          ^^^^^^^^^^^^^^^ added by Forth

To store a value, there’s the ! word, which expects a value and an address on the stack:

123 my-var !  my-var @ . 123  ok.
                        ^^^^^^^^^ added by Forth

For allocating larger memory areas in RAM, there’s the buffer: word:

200 buffer: my-buffer

This sets aside a 200-byte word-aligned area in RAM. It remains available as long as my-buffer is in the dictionary. Executing my-buffer will push its buffer address on the (data) stack.

You’ve made it through step 5, now you know all about stacks and memory.

As you can imagine, there are a large number of words in Forth, each with their own behaviour and stack effect. Inline comments between words called ( and ) are normally used to document a word, followed by a \ comment about what it does. If ! were defined in Forth (it isn’t, it’s a primitive), it could have been documented as follows:

: ! ( u|n a-addr -- ) \ Stores single number in memory
  ... ;

Where u|n means: an unsigned or signed integer, and a-addr means aligned address. Everything before the -- is what is expected as stack input, everything after is the stack result (nothing in this example). These are just comments and conventions, Forth will skip all that.

Likewise, + might have been defined as:

: + ( u1|n1 u2|n2 -- u3|n3 ) ... ; \ Addition

Many words affect only the data stack, but a few mess with the return stack. Like so:

: >r ( x -- ) ( R: -- x ) ... ;

What this says is: the value on the data stack before calling >r will end up on the return stack afterwards: so >r moves an item from the data stack to the return stack (better get it off again with r> or rdrop before the current word returns, else the code will probably crash!).

These stack effect comments are a critical part of the documentation of each word, since there are usually no local variables in Forth.

Here is the glossary of the pre-defined words in Mecrisp Forth. There are a few hundred of them, but no worries: you can explore and gradually expand your vocabulary - only a fraction of these are needed to start programming in Forth.

Yeay, step 6 - you’re all set to build up your Forth vocabulary!

The last step that remains, is to try things out and look for examples and more documentation on the web. See the “Dive into Forth” series, part one, two, and three for a recent exploration here at JeeLabs. The PWM module (documented here) shows one example of how to implement a hardware feature in Forth.

For old, but still useful material, see the Thinking Forth and Starting Forth books by Leo Brodie, Julian Noble’s introduction, documents on the forth.org site, and links mentioned on the forum.

A last point to make, is that Forth lives extremely“close to the metal”. Any suggestion of high-level coding is purely smoke-and-mirrors. It has just enough machinery to be reasonably useful, and to let you compile and extend it with more definitions to do whatever you need.

And there you have it: Forth in sixteen hundred, ehm… words. Hopefully this intro can help you wrap your mind around an intriguingly powerful and concise programming language.


Preparing for serial re-flashing

$
0
0

The STM32F103 µC chips all have a ROM-based boot loader on board. Most chips variants can only be re-flashed via USART1, using the PA9 and PA10 pins, so we will need to connect to that serial port initially - even if the board is intended to be used only over USB or RF later on!

This is the “Blue Pill” board which will be used in this example:

Boards like these are very common, low-cost, and easy to find (on eBay, for example).

Many different STM32F103xx chip variants can be used, as long as they have at least 64 KB flash and 20 KB RAM. Most of the low-cost boards contain a 48-pin C8 (64K flash) or CB (128K) chip. The R8 and RB chips have 64 pins. If the chip is an xC, it has 256K flash and 48K RAM, and if it’s an xE, it’s 512K flash with 64K RAM. The Vx chips are 100-pin and the Zx’s even have a 144 pins. And then there’s the small HyTiny board - marked TB, with 128K flash and 36 pins.

So much for picking a board - as you can see, there are many candidates!

The serial connection can be very simple, all we need is power, ground, and the transmit plus receive signals. The Arduino-style DTR/RESET and CTS/RTS signals are not used here:

Four wires need to be connected as follows (you could also use jumpers or a breadboard):

(ignore the wires on the left, these are for the radio - they are not needed at this point)

A note on “RX” and “TX”: these are named from the perspective of the FTDI board i.e. header pin RX receives data sent out from pin PA9, which is therefore called TX on the µC’s USART1 - and similarly, header pin “TX” connects to PA10, which is USART1’s RX. It’s all crossed over.

Power to the board should be 5V, and be fed to the on-board regulator, not directly to the STM32F103 chip - applying 5V directly to the µC chip will damage it. The RX/TX signal levels should be 3.3V, but this will work with 5V on PA9/PA10 (not all pins are “five volt tolerant”!).

The last thing to note about connections is that the STM32F103 µC needs to be placed in a special “boot mode” to erase and re-flash its memory. On the above board, there are two jumpers for this, one for BOOT0 and one for BOOT1 (which rarely needs to be changed, if ever).

Here is how to place this board in boot mode, by lowering BOOT0 on the right to logic “1”:

Now insert USB + board to power up again.

Perfect, we’re all set for the next step: uploading the firmware - stay tuned…

(don’t forget to put this jumper back and press reset again once the firmware upload is done)

A new serial tool: vive la Folie!

$
0
0

Working with embdded µC boards involves quite a few steps: apart from the hardware itself, you need to connect to it and figure out how to upload code, of course. But you also think about the development cycle and revision control. The list of tools needed to get going can grow quickly…

Not so with Forth. It’s all self-contained and self-hosted. The embedded µC does it all, it just needs to be sent the source code. The same applies to uploading the initial firmware image: all you need, is a way to send the proper bytes to the µC while it is in a special boot loader mode.

In the Arduino world, this is handled by an IDE, which combines an editing environment, a cross-compiler for the µC you’re using, the “avrdude” or “stlink” utilities to handle firmware uploads, and a built-in serial terminal for actually interacting with the code, once it’s running. The success of the Arduino is probably largely due to the fact that this all-in-one approach has been properly packaged as a single “app” which runs on Windows, Mac OSX, and Linux.

Could something similar be done in a Forth-based environment?

Yes, it sure can, but quite differently: meet the Forth Line Evaluator - Folie, in short!

Folie combines a number of functions into an installation-free (!), single-file (!) executable, and is available for Windows, Mac OSX, and Linux (both Intel and ARM):

  • it’s a serial terminal to let you talk to the attached µC board
  • there’s command history to re-use easily previous entries via up-arrow
  • there’s a file include mechanism to send sources as if you had typed them in
  • all lines sent are throttled to avoid over-running the Forth word parser

Here is a brief interactive session as example:

$ folie -p /dev/cu.usbserial-A8009L2N 
Connected to: /dev/cu.usbserial-A8009L2N
  ok.
7 8 * . 56  ok.
\       >>> include a
1 2 + . 3  ok.
3 a 0 1 2  ok.
\       <<<<<<<<<<< a (3 lines)
\ done.
8 9 * . 72  ok.
^D
$

To clarify this further, here is what was typed in, literally:

<CR>
7 8 * .<CR>
include a<CR>
8 9 * .<CR>
<CTRL-D>

The contents of the “a” file is:

1 2 + .
: a 0 do i . loop ;
3 a

As you can see, it’s more or less just a line-by-line serial terminal, whereby each line is sent when you hit return (it can be edited locally until then). That and (nestable) include file expansion.

The line starting with “include” was not sent - the file’s contents was sent instead. Lines which generate no output will not echo back, only lines which cause some other effect will be shown. Which is why the “: a ...;” text does not show on the screen, but everything else does.

Folie is clearly not an IDE - it’s not even trying to be one. It only handles the communication with an attached µC running Mecrisp Forth. Folie is in fact intended to be used alongside your own preferred editor. Once done editing, switch to Folie and enter “include somefile.fs” to apply the changes (or hit up-arrow plus return). With proper definitions at the beginning, this’ll replace or extend what was already loaded - you can even include commands to start things up.

That’s the whole Forth development cycle: edit, send changes, explore interactively, repeat…

One more thing: to get started, Mecrisp Forth needs to be uploaded and “flashed” onto the µC. Since Folie has support for the STM32F103 ROM boot loader, it can also be used for this step:

  • set up the µC board to have the BOOT0 pin tied high, with the jumper in position “1”
  • insert the USB interface and board to power it up
  • launch Folie as follows: folie -p <comport> -u <firmware-file>

If all is well, you should see something like this:

$ folie -p /dev/cu.usbserial-A8009L2N -u mf224.hex 
Connected to: /dev/cu.usbserial-A8009L2N
        File: mf224.hex
       Count: 15684 bytes (converted from Intel HEX)
    Checksum: 4e555979 hex
 Synchronise: .+ OK
 Boot loader: 22 hex
   Chip type: 0410 hex - STM32F1, performance, medium-density
   Unprotect: OK
      Resume: .+ OK
  Mass erase: OK
   Uploading: ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ OK
$

Now restore the BOOT0 jumper to its normal “0” setting, and press reset.

Note: as of this writing (end March 2016), uploading works on Mac OSX and Linux, but not on Windows (grrr!) - you will need to use another mechanism, see this article for some options.

The latest Folie binary releases can be found here. If you have Go installed and properly set up, you can also build from source (on GitHub) using this magic incantation:

go install github.com/jeelabs/embello/tools/folie

Folie is a fairly small open source application, written in Go - it is still evolving quite rapidly at the moment, but everything should work as expected w.r.t. what has been described so far. A lot of its magic comes from the excellent chzyer/readline and tarm/serial packages it is based on.

The one remaining issue is: what firmware do we upload to the STM32F103?

If you’re really impatient, you can extract the latest “mecrisp-stellaris-stm32f103.hex” image from the Mecrisp release and get your feet wet in the world of Forth on ARM chips - or … check out the next article for a more complete image with RFM69 wireless radio support!

Adding the RFM69 module

$
0
0

The last step to create a wireless node is to hook up the RFM radio. Here is what we’re after:

There are 4 signals and 2 power pins to connect (more can be added later, for pin interrupts):

RFM69CWSTM32F103BOTH
SSEL (NSS)PA4+3.3V
SCLKPA5GND
MISOPA6
MOSIPA7

And here is the other side, all hooked up with thin “Kynar” wirewrap wire in this case:

It doesn’t matter which of the two ground pins on the RFM69CW you connect, either one is fine. Also, don’t forget to attach an antenna wire of about 82 mm to the ANT pin (for 868 MHz).

So much for the hardware. If you have no other RFM69 nodes already in operation, you’ll need to build a pair of these to try out actual wireless communication, of course.

To get all the software needed for the RFM69CW into the µC, and assuming you’re starting from scratch, the easiest will be to install a single “combined” image, containing Mecrisp Forth for the STM32F103 plus a slew of Forth definitions from the embello/explore/1608-forth/flib/ area, in particular the RF69 driver (which in turn depends on SPI words, etc):

  • make sure you have Folie ready on your machine (see the previous article)
  • download the latest mf224-ztw.hex file (it’s a text file, but nevertheless gibberish)
  • the file contains Mecrisp Forth 2.2.4 plus the pre-built “Zero To Wireless” demo code
  • go through the steps described before to put the board in “boot mode”

Now launch Folie as follows: folie -p <comport> -u mf224-ztw.hex

If all went well, the output will be similar to this (add the -v flag to see more):

$ folie -p /dev/cu.usbserial-A8009L2N -u mf224-ztw.hex
Connected to: /dev/cu.usbserial-A8009L2N
        File: mf224-ztw.hex
       Count: 32768 bytes (converted from Intel HEX)
    Checksum: c0574666 hex
 Synchronise: .+ OK
 Boot loader: 22 hex
   Chip type: 0410 hex - STM32F1, performance, medium-density
   Unprotect: OK
      Resume: .+ OK
  Mass erase: OK
   Uploading: ++++++++++++++++++++++<etc>++++++++++++++++++++++ OK
$

Good, almost there! Just restore the BOOT0 jumper to “0” and press reset. If you now connect again with Folie in normal mode, and type “reset”, this is what should appear:

$ folie -p /dev/cu.usbserial-A8009L2N
Connected to: /dev/cu.usbserial-A8009L2N
  ok.
reset ?Mecrisp-Stellaris 2.2.4 for STM32F103 by Matthias Koch
64 KB <ztw> 32a92103 

Those last two lines are the key: Mecrisp reports its version, platform, and creator on one line, and then the auto-launched init word prints a second line with some more tidbits:

  • the chip has been identified as containing 64 KB of flash memory
  • the wordset currently in flash corresponds to the “Zero To Wireless” demo from here
  • a 32-bit hex “name” for the chip, derived by xor-ing this chip’s 96-bit unique hardware ID

If you type “list”, you will see a dump of all the words currently defined on your board.

At this point, the board is working, Mecrisp is working, and there’s a bunch of Forth code in flash, ready to serve you. The next step is to find out whether the RFM69 radio is also working.

Type “rf69-init” to initialise SPI and the radio (using net group 42 @ 868.6 MHz defaults):

rf69-init  ok.

Now type “rf69.” (with the period at the end) to get a listing of the radio’s internal registers:

rf69. 
     0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F 
00: -- 04 00 02 8A 02 E1 D9 26 40 41 60 02 92 F5 20
10: 24 9F 09 1A 40 B0 7B 9B 08 4A 42 40 80 06 1C 00
20: 00 00 00 02 FF 00 07 80 00 A0 00 00 00 05 88 2D
30: 2A 00 00 00 00 00 00 D0 42 00 00 00 8F 12 00 00
40: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00
50: 00 01 88 08 00 00 01 00 1B 09 55 80 70 33 CA 08 ok.

Excellent - we’re seeing all sorts of settings. You could look them up in the HopeRF RFM69 datasheet, if you’re so inclined…

Now the tricky bit: as we all know, wireless needs (at least) two to tango!

  • set up a second node, then launch Folie a second time using that other serial port
    • … or skip this step, if you have other nodes already sending data out
    • verify that the second node also works and produces a good rf69. register dump
  • now turn the first node into a listener, by typing: rf69-listen
    • this starts a loop which waits for incoming packets and reports each one
    • that loop will continue to run until any command (or a return) is entered
  • on the second node, type: 12345 rf69-txtest
    • this will send out a single packet, with “12345” converted to ASCII bytes

If all goes well, the first node should now report a new incoming packet:

RF69 21EE2A7C040022801805 3132333435
         ^^            ^^ ^^^^^^^^^^
          |            |  |
      group      5 bytes  payload

Bingo! Lift-off! Wireless! Communication! Magic! Drumroll!

To select a different net group or frequency, you can adjust these variables:

123 rf69.group !  ok.
8683 rf69.freq !  ok.

Well, that’s the ideal case anyway. If for any reason this is not immediately working for you as expected, please consider registering and posting on the JeeLabs Forum or submitting an issue on GitHub. This code is still very young - getting it working under all circumstances (especially from Windows), is bound to take some more effort and detective work.

Still… it’s worth pointing out that we only needed two files to get a complete RF69 development environment going: the Folie serial utility, and the latest firmware hex dump. Once that works, everything else can be done in Forth, including reloading updated versions of the code in flash.

The limitations of the ADC

$
0
0

The Analog-to-Digital-Converter (ADC) we all know from Arduinos and other µCs is a marvel of integration and electronics engineering. Able to measure at rates of over a million samples per second (depending on the µC), you can just select an I/O pin, start an ADC conversion, wait briefly, and then you get a reading back - usually either 0..1023 for a 10-bit ADC or 0..4095 for a 12-bit ADC, representing input signals from 0..3.3V or 0..5V. On a chip stuffed with mostly digital logic gates, that’s close to sheer magic.

Internally, an ADC is usually implemented as follows (image from EEWeb):

I.e. some electronics “in front” of the µC, then inside a multiplexer which connects a specified pin to the actual ADC, then a sampling capacitor, and then an ADC based on a Successive Approximation register. This diagram can explain many properties of an ADC, as you will see.

But first, the input: being part of a µC chip powered at 3.3V (or 5V), means that the input signal must stay within that 0..3.3V range at all times - anything beyond that can damage the entire chip. Everything below assumes a 3.3V supply, by the way - as is common for ARM-based chips. Note that even if some of a chip’s pins are “5V tolerant”, this doesn’t apply when using the ADC!

Limitation #1 - Whatever you do, never connect a raw voltage < 0V or > 3.3V to an ADC pin.

Next: the multiplexer. Most lower-end ARM chips have one, sometimes two, ADC converters. They can still read analog voltages on many pins, because of the multiplexer, but never at the same time. An ADC busy with one measurement can’t do another measurement at the same time. You can measure N voltages one after the other, but that’ll take N times as much time.

Limitation #2 - An ADC can only be used with one pin at a time. Voltages on multiple pins need to be measured one after the other (this is usually not such a big deal if the ADC is fast).

The third element in the circuit is the sampling capacitor, and the resistance of the input circuit and multiplexer feeding it. Together, these form a low-pass filter which limits the rate of change of the signal seen by the ADC. This can have surprising effects. for example when the input comes from a resistor divider with values in the MΩ range. When reading different channels, that sampling capacitor needs to charge or discharge again when the multiplexer changes - so that even a constant voltage may require some time to accurately show up on the ADC.

Limitation #3 - Switching channels can take time, especially when the switching needs to be very frequent or when the input source has a relatively high input resistance / impedance. Sometimes it’s better to switch to a channel, and simply throw away the first reading(s).

The final issue is a complex theoretical one, but nevertheless one you need to be aware of. The Nyquist–Shannon sampling theorem can be boiled down to a very simple guideline: you need to sample at least twice as fast as the highest frequency present in the input signal. If you don’t know what frequencies are present, you have to insert a low-pass filter to keep high ones out.

So for DC, i.e. more or less constant voltages, we can simply ignore the issue. And if the input source is a microphone for example, we just need to sample at twice the maximum frequency picked up by the mike, probably sampling somewhere in the range 10..50 KHz will be fine. If the input is derived from AC mains, with no extreme spikes, then sampling at 1000 Hz will be fine.

Just don’t expect to get anything meaningful out of an ADC by reading at say 50 or even 100 samples per second, when the input might have a certain amount of AC mains “hum” pickup.

Limitation #4 - ADC’s can’t report anything meaningful if the signal can change faster than half the sampling rate. If you measure a signal 60 times per second, and the signal is a pure 50 Hz sine wave, the ADC will “see” a (pure!) 10 Hz sine wave - this effect is called aliasing.

So how do we measure voltages which might be either positive or negative? - Coming up next!

Measuring negative voltage

$
0
0

The task to be tackled here is measuring voltage ranging from +N to -N Volt with an ADC input which needs to be kept in the range 0 to 3.3V.

There are numerous resources all over the web which solve this puzzle. The focus here is to help you get some intuition, and not just to come up with a solution and its “explanation”.

If this is new for you: hang in there. You’ll be surprised how logical electronics can be…

Negative voltages are everywhere - just flip the two leads of a power supply or battery, and you’re there. That’s because “positive” and “negative” are relative concepts, i.e. a voltage is always between two points. It has become common to call one of those points “ground” - even when it’s not actually connected to “earth” using a conductive rod into the real ground!

Many circuits we use are in fact floating, with no real earth connection whatsoever. Most power supplies and bricks / wall warts tend to be isolated from real ground, even when the AC mains receptacle includes a true ground pin. It’s much safer that way - when two circuits float, there won’t be a significant current when you happen to touch both of them (actually, there is a minute leakage current, which can create a slight tingle or make a metal surface feel “funny” - but that’s totally harmless, and several orders of magnitude below the levels which require attention).

This all changes the moment we connect equipment together via conducting wires: from then on, we’ve established a path for electrons to flow, and everything else starts to have a defined voltage relative to both connected devices.

When we use USB to power a small µC board, the USB’s power source defines a ground level as far as all the signals go and the power it supplies. We can’t plug in a second USB cable and touch its ground line to anything on the µC other than the same ground level, without causing a short, and possibly even damaging stuff.

Ground is a great convenience, it’s the basis for power transfers and for communication.

Once we do have a ground, it’s quite possible for some signals or power lines to be lower than that level, and these are then called “negative”. Note that this is still just relative to that ground level we used as reference. There’s nothing different about negative voltages, they’re just lower!

Back to the ADC. It doesn’t like voltages below ground, i.e. under 0V - relative to its ground.

But to measure negative voltages, we can use a trick. This requires two resistors in series:

Our unknown voltage is Vin, and the bottom is our common ground. If we measure the voltage at Vout (relative to that same ground), we get a smaller value. Welcome to the voltage divider. There is nothing magical about this essential building block in electronics - here’s how it works:

  • Vin causes a current to flow through Z1 and then Z2
  • that current is the same in both (where else could it go? see Kirchhoff’s current law)
  • now assume that Z1 and Z2 are two resistors with the same value
  • Ohm’s law then tells us that the voltage over Z1 and Z2 must be the same
  • we know Vin, so the voltage over Z1 must be 12 Vin, and over Z2 is must also be 12 Vin
  • Vout is the same as the voltage over Z2, hence Vout = 12 Vin

We have divided the voltage Vin by two … for any Vin and for any (identical) Z1 and Z2 values.

One more example: assume Z1 is 900 Ω and Z2 is 100 Ω. The total series resistance is therefore 1000Ω. As before, the current through Z1 also goes through Z2, since (again) it has nowhere else to go. If Vin is 1V, then the current will be 1 mA (Ohm’s law, better get used to it!). But 1 mA over 900Ω creates a voltage drop of 0.9V, while 1 mA over 100Ω creates a voltage drop of 0.1V.

Sure enough, the total voltage drop is 1V, same as we’re feeding to Vin. But Vout is now 0.1V, one tenth of Vin - this is a 10:1 divider! Not just for 1V in, anyVin will end up as 1/10th on Vout.

The general formula is: Vout = (Z2/(Z1+Z2)) * Vin - and if you’ve been following along, this should now be 100% obvious to you. This isn’t a dry formula, it needs to match your intution!

Voltage dividers are everywhere. You will now be able to recognise them with your eyes closed.

Note that we can measure voltages well over 3.3V with this: connect the input voltage to a suitably-dimensioned voltage divider, and connect the ADC’s input to Vout.

But what about negative voltages? Almost there. We just need to play a little trick with ground levels. Instead of connecting the voltage divider’s lower end to ground, we connect it to 3.3V:

This is still a voltage divider. It merely works the other way around:

  • with Vin at 3.3V, it’s the same as the top of Z2, so all the points along the path are 3.3V
  • when Vin is negative 3.3V, and Z1 and Z2 are the same value, then the top of Z2 is +3.3V, while the bottom of Z1 is –3.3V: you can probably intuit that Vout will be midway, i.e. 0V
  • and if Vin is 0V, i.e. ground, then Vout will be midway 0 and 3.3V, i.e. 1.65V

What happens, is that Vin can only pull Vout down from 3.3V, and it takes 6.6V down to pull Vout 3.3V under the top of this 2:1 voltage divider. Since a Vin of –3.3V is 6.6V down relative to +3.3V, and since Vout then drops 3.3V down, i.e. at half the rate it’ll end up being exactly 0V.

So the above circuit transforms a +3.3V .. –3.3V voltage swing into a +3.3V .. 0V voltage swing. We can now deal with negative voltages at Vin, even though the ADC’s input can’t!

There are some fairly serious drawbacks with this circuit, but that’s for the next article…

Op-amps and virtual ground

$
0
0

The voltage-divider-tied-to-3.3V trick allows shifting the level of an input voltage to another range. But there are a few issues with it.

The first one is that we still cannot measure anything above 3.3V this way. Regardless of the values of Z1 and Z2, higher voltage would cause Vout to rise above 3.3V: not good for the ADC.

We could consider combining two voltage dividers, i.e. the first to reduce the voltage swing, feeding the second one for level shifting, but generally that’s not a good idea:

One reason is that a voltage divider assumes that there is no current flowing in or out of Vout. With cascaded voltage dividers, we break that assumption and their behaviour will change.

There’s another way - instead of tying the divider to 3.3V, we tie it to a lower fixed voltage:

  • suppose we want to handle a voltage swing of ±5V
  • that’s a total range of 10V
  • it needs to be reduced and shifted to the range 0..3.3V
  • we can use a 3:1 voltage divider for this
  • for example with Z1 = 20 kΩ and Z2 = 10 kΩ
  • with a 3:1 divider, Vout will swing a third of the input

So far so good, but we need to reference this divider to some voltage. Let’s say we tie it to 2.4V:

  • when Vin is +5V, Vout will be (5-2.4)/3 = +0.87V relative to 2.4V = 3.27V
  • when Vin is +2.4V, Vout will be (2.4-2.4)/3 = 0V relative to 2.4V = 2.4V
  • when Vin is 0V, Vout will be (0-2.4)/3 = -0.8V relative to 2.4V = 1.6V
  • when Vin is -5V, Vout will be (-5-2.4)/3 = -2.47V relative to 2.4V = -0.07V

This is not quite inside the allowed range, but by increasing Z2 a bit for a slightly higher division ratio than 3:1 (perhaps 2.2 kΩ or 2.4 kΩ), the maths will work out and place Vout well in range.

Which brings up the next question: how do we create a 2.4V reference voltage?

Again, we could use a voltage divider, such as a 26 kΩ Z1 in series with a 24 kΩ Z2 between +5V and ground. But that that’s a bad idea for exactly the same reason as before: combining voltage dividers leads to currents flowing in unintended ways.

What we need is a stiff 2.4V supply: one which doesn’t change if we load it a bit and draw some current (or feed it some current, when Vin > 2.4V).

Meet another workhorse of the electronics words, the Operational Amplifier, and its notation:

The topic of op-amps is far too extensive to cover in full, but we only need a few properties here:

  • whenever V+ is above V-, Vout rises - whenever V+ is below V-, Vout drops
  • the gain of an op-amp is very high, a tiny input change leads to a large output swing while V+ and V- stay close together
  • the input current at both V+ and V- is (extremely close to) zero

Also important to know is that Vs+ and Vs- are the power supply connections, and that both inputs (V+ and V-) and the output (Vout) will be limited by those two supply levels.

The circuit we are going to need here is the voltage follower:

Considering the above properties, Vout will “follow” Vin: when Vin goes up, V+ becomes higher than V-, forcing up Vout until Vin and Vout match again. Similarly when Vin goes down.

Since the input draws almost no current, yet the output can supply or consume current, this is also called a buffer amplifier: it isolates or buffers the input from loading effects on the output. Which makes it ideal for creating a “stiff” voltage reference:

The output is 2.5V, same as the V+ input, since it’s tied to a 2:1 voltage divider across the 5V supply. But unlike the input, it can supply many milliamps without affecting its output voltage. This voltage level can be used as a “virtual ground”, i.e. as a reference for other voltages.

Perfect for our needs. By tweaking the resistors a bit, we can produce any voltage we want, and use that as reference for the second (right-hand side) part of that first diagram above:

So with 4 suitably-chosen resistors, this will map any ± Vin voltage to the 0..3.3V of the ADC. But there’s another important drawback, which requires another op-amp and 2 more voltages.

Fixing the offset problem

$
0
0

There is still a fairly serious flaw in our little voltmeter setup: its Vin input is not at ground level. It’s connected via Z1 and Z2 to that 2.4V or so virtual ground we just created, and when Vin is not connected to anything, it will read as 2.4V instead of 0V.

More importantly, this means that when we connect this to a circuit-under-test, it is likely to influence that circuit by injecting a bit of current. While it is impossible to completely avoid affecting a circuit when attaching a voltage probe, we really need to minimise that effect and it should definitely not be biased towards some non-zero voltage level.

The way to accomplish this is to measure against ground and to make the input of our voltmeter very high-resistance/-impedance so that it draws almost no current. Many commercial digital multimeters have an input circuit with 11 MΩ resistance. At 20V, this will draw under 2 µA.

This is another good task for an op-amp as voltage follower. A precision op-amp such as the dual LT1413 has an extremely small input current (well under 1 nA). If we use it as voltage follower in front of our circuit so far, we get a very good input stage - on par with commercial multimeters.

But then the input voltage range issue rears its ugly head again: if the LT1413 is powered from +5V and ground, then it too won’t be able to handle negative voltages! This time, the solution is different: unlike digital circuits, op-amps can easily be powered from both a positive and a negative supply voltage. If we power the LT1413 from say +8V and -8V supplies, then it can easily pass through any signal in the range of ±5V.

There are chips which can generate such voltages from a single supply. Such as the MAX232:

This chip is intended for use as RS232 converters, but we can ignore that functionality and re-purpose it as a cheap DC-to-DC converter. Unlike conventional switching power supplies, such a circuit only needs a few small capacitors. The flip side is that the chip can supply only a few milliamps of current, but since that’s more than enough to power an LT1413 - who cares?

Given such a “dual-rail” power supply, we can now sketch an improved input circuit:

Some notes about this simple op-amp based circuit:

  • as you can verify, the input resistance is indeed 11 MΩ
  • note also that the two input resistors act as 11:1 voltage divider
  • for a ±22V input, the voltage on the V+ pin will be in the ±2V range
  • since this is a voltage follower, the output will also be in the ±2V range
  • we are well within the ±8V supplies - even ±5V supplies would be ok here
  • as configured above, this input stage could in fact be used up to at least ±70V

We’ve now reached the point of this whole exercise: a circuit which can handle voltages between -20V and +20V and turns these into something suitable for the ADC’s limited 0..3.3V range.

A note about op-amps: not all types are created equal. Unlike “ideal” op-amps, real ones have various limitations and imperfections. One issue is the input bias current - which is why a “precision” LT1413 was picked for the input stage, although it costs more than the “household” quad op-amps of an LM324. Another aspect is the allowed voltage range for inputs and output: many op-amps don’t support voltages all the way up to the upper supply level, or down to the lower supply level. For those that do, the inputs and/or outputs are tagged “Rail-to-Rail”.

This is why the LM324 is powered from 5V and GND in this project, and why ±8V was used for the LT1413 - both are a bit beyond the signal ranges they need to deal with. See this PDF from Texas Instruments for all the key op-amp parameters and what they mean.

It’s time to wrap things up, draw the complete circuit, and perform final calculations and checks.


Let's measure ± 20V @ 11MΩ

$
0
0

So much for theory and design. Here’s a built-up version of the voltmeter:

It was built as add-on for the RF Node Watcher, here’s the side view of this sandwich:

Everything was built with through-hole parts, the LM324 and LT1413 op-amp chips (quad and dual, respectively) were mounted on the bottom, to provide easy access to all their pins from the top and allow simple experimentation & soldering. The ST232C chip was mounted in a free spot, with the 4x 0.1µF caps needed to make it generate about ±9V. Here’s the corresponding circuit:

Some resistors differ (as built, it’ll handle ±15V), but it shows the line of thought which led to it:

  • U2B was built first, as buffer for the ADC input (PA3)
  • R9 limits the current into the ADC pin when the op-amp’s output exceeds 3.3V
  • U2A was added to create the offset reference voltage
  • then the LT1413’s U1A op-amp was added with dual supplies as improved input stage

This also adds some amplification to the op-amp. It is now no longer a voltage follower, because we’re not feeding all the output back to the V- pin but a slightly reduced level derived from it. As a result, the op-amp needs to generate a somewhat higher output swing to get V- to match V+.

As with software, once you have a working setup, you can start to optimise - either by improving the specs, or by reducing the parts count. In this case, that U2B buffer can go with little effect on the ADC performance, so we can drop the LM324 and use the 2nd LT1413 for a 3.0V reference:

Several changes are worth pointing out (and a minor tweak, R6 should be 20KΩ, not 22KΩ):

  • we can switch to +5 and -5V volt as supplies, no need to generate +8V anymore
  • this means the ST232C could be replaced by a smaller and cheaper TC7660S
  • the input is now 10 MΩ + 1 MΩ, which are more common resistor values
  • with this 11:1 divider, the input range now includes 24V, which can be useful
  • the reference voltage using in this second iteration is 3.0V
  • all resistors have been selected to get very close to the ADC’s full range
  • no need for an LM324 anymore, leaving just the LT1413 and negative supply chips

For the -5V supply, we can use this very simple circuit, based on a TC7660S in an 8-pin DIP:

But hey, why stop there? That reference voltage in this second iteration is 3.0V - couldn’t we dimension everything so that 3.3V itself can be used as reference? Sure enough, we can:

The ±2.27V input variation that comes out of the 11:1 input voltage divider ends up as ±3.11V on the output pin of the op-amp. This happens to be exactly right for a 2:1 divider tied directly to the +3.3V supply. The result varies from 0.1V to 3.2V, a perfect match for the ADC’s input range!

So there you have it: this implements a voltmeter not unlike the portable multimeters we use to measure voltages and more with (although our circuit only has a single ±25V range), with a very decent high 11 MΩ input resistance. It’s also quite robust against over-voltage.

With 1% accurate resistor values and a little calibration check, this single op-amp circuit is all we need to make the ADC of a µC usable for a much wider voltage range. The unused op-amp in the LT1413 could then be used to build an identical second channel, for example.

These designs have only addressed “static” DC behaviour so far. There is a lot more to go into, such as how to measure A/C voltages, how to measure current, and exploring the behaviour of this front-end at various frequencies. But that will have to wait for another time…

Slowed-down by the slew rate

$
0
0

Let’s re-visit the voltage meter for a moment - the actual circuit used in the next test differs in a few resistor values, but it’s still essentially the same as the triple op-amp setup described earlier. Here are the two main op-amps involved (omitting the 3rd one, used for the reference voltage):

Let’s inject a 10 KHz square wave into this circuit, and see how it behaves:

The input signal is shown at the top, in purple. It swings from +5V to -5V, and since R1 is only 1 MΩ in the actual setup, the voltage divider is 2:1, not 10:1.

The blue trace at the bottom is the output of U1A. In the test setup, R5 was shorted out and R6 was absent, making U1A a follower, so the output is ±2.5V.

Lastly, the yellow trace is what is fed into the ADC, i.e. the output of U2B. It varies between around +1.3V and +2.3V, nicely within the ADC allowable 0..3.3V range.

As you can see, the LT1413 output is far from a square wave - did we go wrong somewhere?

Yes and no: if the input signals we’re going to measure only interests us in the “static” DC range and in the low audio range anyway, then this setup is actually fine. We can easily measure any fixed DC voltage with this, as well as mains A/C at 50 and 60 Hz.

But there clearly is something very unexpected going on, in terms of using an op-amp as simple amplifier or voltage follower (i.e. amplification set to 1). Where did the “distortion” come from?

Here are the main specs, as presented on the first page of the LT1413 datasheet:

All very respectable figures. Nothing unusual here. Let’s look a little further:

Aha! Check out the “Slew Rate” specifications: what the LT1413 datasheet is saying, is that this particular op-amp’s output level can typically change by no more than 0.3V per microsecond.

In this circuit, the input signal swings between +5V and -5V, i.e. a 10V change - which in the case of a square wave happens very quickly of course. At the junction of R1 (1 MΩ) and R2, i.e. V+ of U1A, and at the output, the signal swings between +2.5V and -2.5V, i.e. 5V in either direction.

Surprise! The LT1413 is behaving exactly as specified: 5V / 0.3V/µs = 16.7 µs rise & fall times.

That doesn’t make the LT1413 a “bad” op-amp in any way. In fact, it has pretty impressive specs for several of its parameters. But in this circuit, if we want to handle higher frequencies, we’re going to need an op-amp with better slew rate specs.

(The slew rate is in fact often intentionally limited by the manufacturer, to avoid oscillation in voltage-follower mode, but that’s a whole different story, involving phase shifts and “poles”…)

This is typical when working with op-amps: they all make lots of choices and trade-offs, so you really have to select and match the op-amp for every single use case. In the next article we’ll go through a brief summary of the main parameters of op-amps, what they mean, and then try to pick a better one for our voltmeter setup.

For the true path to insight and wisdom, check out The Art of Electronics book: over 1200 pages filled with facts and experience to help the Electronics Engineer, including several tables with op-amps, their key criteria, and how they compare.

Op-amp parameters explained

$
0
0

It can’t be shown often enough - the most elegant building block in electronics:

In an ideal op-amp …

  • the V+ and V- inputs have infinite resistance
  • there is zero current into and out of both V+ and V-
  • if V+ is above V-, however slightly, then Vout will be Vs+
  • if V- is above V+, however slightly, then Vout will be Vs-
  • when V- is V+, Vout will be halfway between Vs+ and Vs-
  • the gain of the amplifier is infinitely large
  • when V+ and V- change by the same amount, Vout stays the same
  • as long as in-/outputs are within Vs+ and Vs-, changes to Vs+ and Vs- have no effect
  • the behaviour is identical for any frequency, temperature, and time span

Back on earth, things are a bit less rosy. Here are the main types of imperfections in real-world op-amps (with page references to The Art of Electronics book, 3rd edition):

Input Bias Current - TAoE p302

There will be a small current flowing into or out of the V+ and V- pins, called the bias current. This matters with high resistance values, i.e. with a 10 MΩ + 1MΩ 11:1 input divider, we are dealing with currents in the order of 0.1 to 1 µA through these resistors (when measuring a few volts). A bias current of 1 µA would completely mess up our measurements.

For a simple run-of-the-mill LM324 unit, the bias current is 15 nA typ, and 100 nA max (0.1 µA).

The equally-common CMOS-based LMC6482 has a minute 0.02 pA typ (20 femto-A!) and 4 pA max bias current, although it’ll be an order of magnitude higher when the temperature is 75°C.

Somewhere in between, in terms of bias current, lies the OPA277: 0.5 nA typ, 1 nA max.

Input Offset Voltage - TAoE p304

Ideally, shorting both input pins should generate a 0V output. In reality, the input stage is never 100% perfectly matched (although some chips are laser-trimmed to get them really close).

This input offset gets amplified and ends up affecting the output. This becomes much more of an issue when the gain of the op-amp is high.

For the LM324, the input offset voltage is 2 mV typ and 5 mV max. A more high-end “precision” op-amp such as the OPA277 has 20 µV typ, 50 µV max as specs.

Input offset voltage can be “trimmed away” with a small trimmer potentiometer and some extra resistors, if needed. What remains is a small temperature-dependent “input offset voltage drift”.

Common Mode Rejection Ratio - TAoE p305

Ideally, all an op-amp cares about is the difference between V+ and V-. In reality, the behaviour changes depending on whether both are near Vs+ or near Vs-. This change is the common mode, i.e. the difference stays the same, but both inputs are changing their voltage levels in common.

The CMRR is the rejection ratio. If a 1V change in both inputs leads to an unwanted offset of 1 mV, then we have a 1000-fold relationship between absolute changes and differential error.

For relative values, especially large ones, it’s more convenient to use the decibel as unit: +20 dB is ten times as much voltage. A 1 mV error per 1 V change is equivalent to a CMMR of 60 dB.

For the LM324, the CMRR is 80 dB typ (i.e. 0.1 mV/V), and 65 dB min. The high-end OPA277 has a better CMRR of 140 dB up to 10 Hz, dropping to ≈ 95 dB at 1 KHz and ≈ 35 dB at 1 MHz.

Power Suppy Rejection Ratio - TAoE p305

Similarly, there is some effect from power supply voltage variations. This can matter when the power supplies are not highly regulated, as some of that ripple might show up on the output pin.

For the LM324, the PSRR is 100 dB typ, and 65 dB min. For the OPA277, these values are again more accurately specified in the datasheet with a graph, going from 130 dB up to 100 Hz, and then dropping off at that same 20 dB per frequency decade as the CMRR.

Something to watch out for is that the PSRR is not necessarily the same for Vs+ and Vs-.

Slew Rate - TAoE p307

The slew rate of op-amps is often intentionally limited. This is the rate at which the output pin voltage can change. A high slew rate means that the op-amp can quickly track changes, but can also cause problems with accuracy, unwanted feedback, “ringing” and even oscillation.

For the LM324, the slew rate is 0.5 V/µs. For the LMC6482 it is 1.3 V/µs, and for the OPA277 it is 0.8 V/µs - all specified as their typical values. Some op-amps have a slew rate over 1000 V/µs.

Bandwidth - TAoE 247

All the common internally compensated op-amps have a fairly low frequency where they start to reduce their gain at a fixed 6 dB per octave (or equivalently: 20 dB per frequency decade). This is essential for op-amp stability - it acts like an RC low-pass filter, built right into the op-amp.

With such a downward slope, at some point the gain plot will drop below the 0 dB line. This is the frequency where the gain of the op-amp becomes less than 1.

For the LM324, this is typically at around 1 MHz, for the LMC6482 it’s 1.5 MHz, and for the OPA277 it’s also 1 MHz. Some high-end op-amps can go well into the 100’s of MHz.

Output Current

Most op-amps can only drive a fairly small amount of current - although there are exceptions.

The LM324 can sink or source at least 20 mA, the LMC6482 at least 11 mA, and the OPA277 also some 20 mA. Although with these larger currents, the output voltage range will be reduced.

Large output currents can lead to internal heat-up, which in turn may affect some of the op-amp’s parameters, so generally it’s not a good idea to load an op-amp with less than say 1 kΩ. For higher current, there are numerous ways to add an output driver, transistor, MOSFET, etc.

Input / Output Voltage Range

Not all op-amps can handle inputs swinging all the way from Vs+ down to Vs-. When they do, they are called “rail-to-rail in” (RRI).

Likewise for the output: it’s relatively difficult for an op-amp to drive its output pin all the way up to Vs+ or down to Vs-. Those that can are called - wait for it…“rail-to-rail out” (RRO).

Some op-amps are champions in this league and can do both - these are designated as “RRIO”.

The LM324 can go from Vs- to about Vs+ - 1.5V, which makes it very suitable for “single-rail” operation, i.e. running off a single power supply, tied to Vs+, with Vs- tied to ground. It also means that with a 5V supply, it can’t handle (or generate) signal levels above 3.5V.

The LMC6482 is RRIO, its output can swing to within 0.1V of both voltage rails. Interestingly, and this is often the case with RRI, it can in fact handle input signals levels slightly beyond its two supply voltage levels.

The OPA277 isn’t happy with input voltages less than 2V away from its supply rails, and its output also stays within 0.5V above and 1.2V below its power supply voltages, respectively.

Winding down…

This concludes our little tour of op-amp parameters for now. In many cases, you can get a lot of mileage by just picking a few op-amps to work with, with somewhat different trade-offs and properties, and sticking to them where possible. The LM324 (quad) and LMC6482 (dual) or LMC6484 (quad) units are low-cost and capable. And these are fairly arbitrary picks, really.

For more information than you’ll ever need, with all the background, detailed comparisons, and a huge variety of example circuits, see the Art of Electronics book by Horowitz and Hill. The 3rd edition came out in 2015, but earlier editions are also still an impressive and useful resource.

Better speed and sensitivity

$
0
0

First off, let’s find out how that last single op-amp circuit works in practice. The chip used is an LMC6484 quad CMOS op-amp with extremely high input impedance. Since it’s also rail-to-rail on both inputs and outputs, the Vs+ positive supply can be 5V and the negative Vs- supply can be -5V, as generated by the TC7660S negative voltage generator (it’s a small DIP-8 package which only needs two 10 µF capacitors to work). For reference, here is the schematic again:

Here is the above circuit on a 4x6 cm prototype board, again plugged into the RF Node Watcher:

All the resistors used are metal film and accurate to 1%. Using a lab supply, we can measure the actual voltages presented to the ADC over the full ± 25V design range - here are the results:

VinVadc
-250.065 V
-240.128 V
-210.316 V
-180.504 V
-150.692 V
-120.880 V
-91.068 V
-61.256 V
-31.444 V
01.633 V
31.821 V
62.009 V
92.197 V
122.386 V
152.574 V
182.762 V
212.950 V
243.138 V
253.201 V

As you can see, it’s all mapped inside the 0 .. 3.3V range needed for the ADC. There is a small offset: an ideal op-amp would have placed 0V at 1.650 V on the ADC, i.e. 17 mV higher than measured - this is probably due to the op-amp’s input offset plus bias current imperfections.

That same table, but now plotted to show (at least coarsely) the linearity of it all - which is not really surprising since the op-amp is being used well inside its datasheet specs:

The frequency response is flat from DC until at least 10 KHz, but there are some details to deal with w.r.t. higher frequencies - the slew rate is around 0.5 V/µs, somewhat lower than the specs in the datasheet indicate. Still, more than enough all the way into the audio frequency range.

Now, since the LMC6484 is a quad op-amp, that leaves us with 3 unused op-amps…

One option would be to add up to three more identical channels, so that this circuit could be used to measure and report/display up to 4 signals at the same time.

More interesting would be to measure lower voltage ranges with decent resolution. One use for this, is to read small voltages across a “shunt” resistor, for performing current measurements.

There are many ways to implement this sort of “auto-ranging” functionality - it comes down to where to adjust the amplification of the op-amps (or where to place switchable voltage dividers). It’s quite involved to do this on the input side, because of the dual voltage supplies.

A much simpler (albeit less precise) option, is to add an extra op-amp after the front end. Then, we don’t have to worry so much about input impedances, for example. In fact, let’s go ahead and add a cascade of 2 op-amps after the input stage:

Some notes about this circuit (which has not yet been tried, by the way):

  • the first stage (U1B) has a 1.1:1 divider, making its maximum input range ±3.5V
  • it “only” has 1.1 MΩ input impedance, i.e. it’ll draw up to 3.2 µA from the signal source
  • U1B is set up as voltage follower here, without any amplification
  • the R3 input resistor protects the op-amp against damage, even with ±25V inputs
  • the output uses the same level-shifting trick as before, feeding 0.1..3.2V to another ADC
  • U1C takes the “bipolar” signal of ±3.2V and amplifies it by a factor 11, due to R11 and R12
  • this works for inputs of up to around ±0.3V, higher voltages will hit the ADC’s extremes
  • likewise for U1D, which takes that ±0.3V signal, and amplifies it again 11 times
  • the output will “fit” into the ADC’s range only when Vin2 is within ±0.027V or so

This means that only as long as the signal stays within that small ±0.027V range, will all three ADC outputs be useful - though ADC2 sees a tiny value, ADC4 can report the 121x amplified value in nearly full 12-bit resolution, i.e. in steps which are only a fraction of a millivolt.

There is a drawback with this approach: for higher input voltages, the cascaded op-amp outputs will be “saturated” because they can’t reach high enough voltages. The output of an op-amp can never go above its V+ or below its V- supply rail, which in this case will be around+5V and -5V.

Another issue is the non-zero offset error, i.e. seeing 1.633V at ADC2, when it really ought to have been 1.650V. That offset error is going to be amplified first by U1C and then by U1D. We can compensate for it in software, but it will reduce the total range of these cascaded stages.

So what happens when the Vin2 input rises to +1V, for example?

Well, the output of U1B will be about 0.9V (the same as its input, since it’s a voltage follower.

The output of U1C will be “pegged” at approximately +5V, since it can’t go any higher (if it could, it would settle at 11 x 0.9V, i.e. ≈ 10V). This also means that ADC3 will see an input voltage of around 4.2V, which is outside its valid measurement range. The ADC will report this as “4095”, its maximum reading. Damage is not likely, since R14 will limit the current flowing into the ADC pin to well under a milliamp (the LMC6484 can handle ±5 mA).

Likewise, for U1D, which sees +5V at its input, and has no way to amplify that another 11 times. It too will have its output pegged at +5V, with the same effect on ADC4 as with U1C and ADC3.

In summary: for “large” input voltages, U1C and U1D will be pushed to their limit, and feed either +5V or -5V to the ADC’s 2:1 input voltage dividers, making their readings useless. But for smaller values, where U1B reports small values around zero (i.e. an ADC reading close to its 2000 middle range), the values from ADC3, and possibly also ADC4 will become meaningful.

We can now simply read out ADC2, see if it’s close to what represents 0V, and if so, read out ADC3, see again if it’s close to 0V, to finally check ADC4 for values really close to 0V.

Due to the input error and their substantial effect on the readings of ADC3 and ADC4, this circuit will need to be calibrated: once by shorting the input so it is exactly 0V, and once near both ends of the scale, i.e. ±25V on the input (or ±24V, if that’s more convenient). The 2nd calibration can be skipped if you’re ok with the 1% accuracy of the resistors used in the circuit.

If the input offset turns out to be too large to make U1D perform in a useful range, a manual offset compensation trimmer will have to be added to this circuit, to be calibrated once.

Well, that’s the theory anyway… we’ll have to try this out to see how it behaves!

Water and electricity don't mix

$
0
0

The hot water tap in the kitchen at JeeLabs is provided by what’s called a “close-in boiler” in the Netherlands: a small 7L electrical boiler, sitting under the kitchen sink and hooked up with two reinforced flexible hoses, similar to the ones often used in showers.

The rubber in these hoses is probably high-grade, but their normal lifetime is rated at 10 years. At JeeLabs, they were 19 years old and decided to start spraying, as you can see in this close up:

This caused a slow drip in the area below, due to some horribly drilled holes for electrical wiring:

Now this also happens to be the meter cupboard, where all the electricity and gas is distributed. And the drip happens to have been right on top of a USB cable, plugged into this little adapter:

Scary stuff! - if you look closely, you can see that the USB adapter was cracked open, probably by the pressure released as it finally gave up and let out the magic smoke:

Here’s the full extent of this little disaster, as seen from the inside:

It has obviously been going on for a long, long time - that amount of corrosion couldn’t possibly have been caused by just a few hours of moisture!

Both leaky hoses have been replaced now. All is well again, and there is no moisture anymore in that electrical area. It might in fact be a good idea to place a moisture sensor node in there…

And then the JeeNode stopped

$
0
0

Unfortunately, the USB power supply is not all that got damaged by the water leak:

There’s a blueish sludge on the RJ12 jack. Presumably some copper salt caused by humidity and corrosion. This is a little JeePlug board on top of a JeeNode, which is used to detect pulses from three different 2000 pulse/kWh counters. This is what it looked like three and a half years ago:

The Arduino IDE based code it was running is on GitHub.

Unfortunately, when plugged into a replacement adapter, the USB connector and adapter both got very hot, and the node didn’t send out any packets. Even if this is merely some residual moisture, this node really needs to be replaced with all the corrosion going on.

This node has done excellent service: over 16,000 packets on a typical day. Over a period of over 42 months this means it has measured and reported over 20 million packets here at JeeLabs!

Now it’s time to put that node to rest and replace it with something new. The reason is really not just that it can be replaced, but that there are several additional signal sources in that meter cupboard, some of which haven’t even been measured yet.

Here’s the list of input sources, all nearby:

  • three pulse counters: one for solar, one for the stove, and one for the rest of the house
  • three CT’s, not yet connected: again, one for solar and two for the rest of the house
  • the smart meter with a P1 output: this is read out by a separate JeeNode (still operational)
  • a gas meter, reported by the smart meter, but only once an hour
  • a water meter under the floor, in a separate area, also not attached yet

The water meter is interesting - holding an iPhone nearby, running the Teslameter 11th app, shows that there is a distinct magnetic fluctuation when the water is running:

Which means it could be read out without adding a separate water meter into the water pipe!

Perhaps something similar is possible for the gas meter, this hasn’t been investigated yet.

In other words: there are a lot more things worth measuring in the meter cupboard now, and this looks like a good opportunity to implement a much more powerful sensor node in there…

A new design, moving to 32-bit

$
0
0

With one of the two main energy metering nodes out of order, it’s becoming more urgent now to implement a replacement. It would be a shame to break the cycle, after some 8 years of data collection, all logged and waiting for future visualisation.

There are quite some signal sources, more than a simple JeeNode w/ ATmega328 can easily handle. Given the recent focus on STM32F103-based boards, it should come as no surprise that this has also been picked as base for the new node. The Olimexino-STM32, to be exact:

The Olimexino-STM32 has an STM32F103 µC with 128 KB flash and 20 KB RAM, as well as a very nice complement of features on-board:

  • plenty of I/O, since this is based on a 64-pin µC
  • 12-bit ADCs, which can read up to 1 million samples per second
  • a LiPo battery connector for backup, including charger
  • very low power 3.3V regulator (the same as on the JeeNode)
  • on-board switching regulator, which can run off 9..30 V (DC, but see below)
  • µSD card slot (on the bottom), for long-term data storage
  • 8 MHz and 32 KHz crystals, allowing the µC to keep track of real time
  • USB mini jack, for power and as USB device
  • CAN bus terminal header (note that it’s either USB or CAN on this µC)
  • Arduino-shield compatible headers, including the offset fix
  • mini 2x5 header for JTAG (also compatible with a Black Magic Probe)
  • 10-pin UEXT header, for off-board extensibility
  • and a 16-pin extension header with all the otherwise-unused pins

In short - lots to like, and many convenient features for an “energy reporting node”.

Here is a first build, with RFM69, OLED (optional), P1 port with NPN transistor to invert the signal, and a “pulse port” for the 3 pulse counters - the most urgent task for this whole setup:

There’s a LiPo on the back, attached with dual-sided tape, and there’s a small DC jack adapter (newer models of the Olimexino-STM32 have it on-board, instead of the screws on this one):

The board is well-documented, even though in this case it’s all obscured by the battery:

One nice property of the Olimexino-STM32, is that it includes an efficient switching regulator with a wide voltage range. Since the board also includes a diode in series, it can in fact be operated from an AC power source. Which is perfect here for two reasons:

  • feeding AC into the unit means we now have a way to detect phase and measure AC mains variations, as well as fluctuations in 50 Hz for estimating the load on the power grid
  • there’s already a bell transformer nearby, used for … the door bell!

Here’s the meter cupboard - nice and dry now - and the new 8 VAC power feed, ready for use:

From the logo in the first picture, you can see that Mecrisp Forth is already running, and so are the OLED & RF69 drivers. The next task is to read the pulse counters and report them over RF.

Note that this RFM69 is being operated in native mode, i.e. it’s not sending out packets in the original RF12 format. Since this is the first such node added to the JeeLabs Home Monitoring Network, the central hub also has to be extended with a receiver node to pick up these packets.

Lots of software tasks ahead. And also some electrical puzzles, apparently - it looks like the pulse counters are not yet properly being sensed by this new board.


Energy monitor requirements

$
0
0

The meter cupboard is - as one would expect - the place where lots of energy-related matters come together. Here is the situation as of early 2016 at JeeLabs:

Note: that “JeeLabs Energy Monitor” is wishful thinking and vapourware at the moment.

There is also an FTTH internet modem, feeding an ethernet cable going upstairs to the FritzBox wireless router. And lots of wiring, no longer used: a splitter feeding TV and radio signals into four different cables, with sockets at various places in the house (replaced by a single DVB-T receiver in the living room), as well as a maze of old telephone wires.

At one point in time - in the era of modems and faxes - there were fourPOTS phone lines coming into the house (the last ones in the neighbourhood, as it turned out), later replaced by ISDN, and now all routed over the internet link. And at a fraction of the cost…

Listed in the above diagram are all the potential sources of information. Several of these haven’t been implemented yet, such as the magnetometer-based readouts of the water and gas meters, and the sensing of temperature and humidity. Also not yet hooked up, but in-place and ready to go, are the three Current Transformers.

The advantage of pulse counters is that their signal is very easy to connect, and fairly accurate - especially at low power levels, when pulses are far apart. The benefit of current transformers is that they are more effective for high current loads and inductive loads, where true and apparent power can differ substantially. With the above setup, we can have the best of both worlds - even though the different sensors are not currently hooked up to the exact same circuits.

This is a relatively small setup. The house is from the 1970’s, when current requirements as well as regulations were fairly basic. It’s all 1-phase 230 VAC mains (many houses in the Netherlands now have 3-phase distribution). There is not even a DIN rail in the meter cabinet at the moment!

Powering from an AC source

$
0
0

Ok, we have a name: “JeeLabs Energy Monitor” - and we have a board to use: the Olimexino-STM32. As mentioned before, that board has a number of convenient features for this project.

Then again, any other board can be used - most differences are minor, and there is a huge variety to choose from. Even the “F103” µC family is not really important. Since the F103 is mature, and this is a low-end 128K flash / 20K RAM unit, most other µC’s should be fairly easy to port to.

The Olimexino can be powered from a 9-30 V supply, from the USB port, or from a LiPo battery. But there are a couple of advantages to powering from an AC power source, i.e. directly off a step-down transformer of 9..12 VAC:

  • the waveform can be used as approximation of the 230V AC mains voltage
  • its zero-crossings will let us calculate reactive power for the Current Transformers
  • the mains 50 Hz frequency fluctuations can be measured to estimate power grid load
  • there’s already an always-on bell transformer in the meter cupboard - might as well use it!
  • the Olimexino has a diode in series with the power jack, it can be powered directly by AC

That last point requires some clarification: the Olimexino specs mention a 9..30V DC power source. Powering it with AC means the on-board switching power supply will have to work a bit harder, since it will run with a widely varying input voltage: zero 50% of the time, and a half sinewave the other 50%.

The input capacitor is 100 µF, and will end up seeing fairly large currents, as it gets topped up every 20 ms. As will be shown below, the resulting voltage level always stays above 9V, allowing the regulator to do its work.

The voltage on an unloaded 9 VAC transformer used for testing looks like this:

It’s not such a great sine wave, probably because it’s not such a great transformer. But it’ll just have to do - a high-quality bell transformer should produce a more accurate representation of the AC mains waveform.

When we hook this up to power the Olimexino, something unfortunate happens:

The asymmetric loading of one half of the sine wave to power the board, plus the fact that the on-board 100 µF capacitor only draws current while being topped up, causes this distortion.

That’s not great if we also want to use the waveform later on for reactive power calculations. Fortunately, the original waveform is symmetric. So the solution is to use one half of this 50 Hz wave for drawing power and the other half for measurements. Here’s the circuit we will use:

As described in a recent article, a voltage divider is used to reduce the signal to acceptable levels for ADC readout and this divider is then connected to +3.3V to handle this - negative! - voltage.

By matching up the scales of the two signals we can see the result on the oscilloscope:

The blue trace is the transformer’s AC voltage (about 34 Vpp). The top half is used to power the board, and the bottom half gets converted to a “3.3V signal with downward blips”. There is enough information to reconstruct an accurate representation of the full signal. Thanks to the symmetry, we can place the zero crossings at the level where the upward and downward slopes have 50% duty cycle - halfway on the above screen shot, i.e. roughly 3.0V on the ADC input.

One last screenshot, using a bit of scope trickery to bring it all together:

There is a lot of useful information in here:

  • persistence has been turned on, and the scope is triggering on both slopes
  • because of this, the signal is shown twice, once shifted 180° in phase
  • the yellow and blue traces are the same signal, but yellow is shown inverted
  • this creates a nice overlap and shows the symmetry, which is nearly 100%
  • the top half shows the blue line “capacitor top-up” voltage dips
  • the magenta line is the voltage after the diode, i.e. the switcher’s input voltage
  • as you can see, that switcher input voltage fluctuates between 12V and 17V
  • what is also very clear is how the capacitor pulls the AC feed as it charges
  • there are small spikes on the supply voltage - this is normal for switching regulators

When running at 72 MHz, the Olimexino draws about 45 mA @ 5V - with the switcher this translates to around 20..30 mA drawn from the AC power supply. This should not be a problem, even if it were to double later on, when a bit more circuitry gets added.

So there you have it: we’ve re-used an existing power source, and we have a 0..3.3V signal which can be fed to an ADC pin, to let us determine AC mains voltage and frequency fluctuations as well as obtain a decent representation of the wave shape of AC mains power, all in real time.

Reading out the pulse counters

$
0
0

Unfortunately, progress on pulse input recognition has been a bit slow - partly due to unrelated “time sinks” - so this will be an overview of some other details of the JeeLabs Energy Monitor.

The mounting bracket was 3D-printed from a design on Thingiverse, with these parameters:

pcb_width = 53.5;
pcb_length = 53.5;
pcb_height = 2;
slisse = 1;

pcb_bottom_margin = 10;
wall_width_thick = 2.5;
wall_width = wall_width_thick-slisse;
box_height = 16;

This custom-sized “clip” holds the Arduino-shaped board nicely, with room for the LiPo battery. It was shortened a bit to stay clear of the reset button. A quick click-and-slide now does the trick:

A DCF77 receiver module from Pollin hangs just below the unit, fixed to a nail with a tie-wrap.

There’s an RFM69 on the shield, and three CT inputs have been created, with a 1 KΩ + 1 KΩ voltage divider to create a 1.65V reference. There are some capacitors to decouple and “stiffen” this reference voltage (further details to follow later, when the current transformers are added).

The shield is a hodge-podge of experimental circuits and connections, as can be expected in this very early prototyping phase:

And here’s the back side, all wired up with prototyping-friendly isolated Kynar wire:

No Arduinos were harmed in the construction of this board, despite their 0.06” header offsets, because the Olimexino has room for extra headers with a normal tinkerer-friendly 0.1” pin grid.

If you look very closely though, you will see that at position (R,18) an extra pin was added to the board - this mates with a soldered-on pin on the Olimexino to tie into the 5V power connection, so that the board can be powered from just this shield via an FTDI header during development.

A remote console w/ ESP-Link

$
0
0

The ESP-Link is a clever project, which turns an ESP8266 WiFi module into a transparent serial link - sort of a wireless FTDI interface. Thorsten not only created a really powerful software package for this, he also built a few prototypes to turn this into a small “BUB-like” package:

Here’s the schematic of what’s on this board:

An ESP8266 and a switching regulator to bring the 5V down to 3.3V, basically. And some very finicky choices of pins and jumpers to be able to use this in various ways:

  • ESP-Link software has to be uploaded to the ESP8266 using another serial FTDI board
  • note that this only needs to be done once: after that, updates can be installed over the air
  • when done, a few jumpers and pins are changed to make it match the normal FTDI pinout

This is all documented on the ESP-Link project site. It can be a slightly tricky business to get going initially, but after that it’s a delight to use.

The ESP-Link presents an impressive home page for configuration and use from a browser:

If you want, you can even use its web-based console page for everything:

Note the reset button, which makes recovery of runaway code in Mecrisp a breeze.

But that’s all icing on the cake. The main use of ESP-Link is as a socket-based telnet connection. For this reason, the Forth Line evaluator tool has now been extended to also support telnet:

$ folie -p jemesp:23
Connected to: jemesp:23
ok.
11 22 + . 33  ok.
^D
$

A telnet connection will be used when the path to the “serial port” has the format shown above, i.e. ending in a colon and network port number - else folie falls back to normal serial port mode.

Best of all, (remote!) uploading via include <filename> in folie still works as expected:

$ folie -p jemesp:23
Connected to: jemesp:23
ok.
Mecrisp-Stellaris 2.2.4 for STM32F103 by Matthias Koch
\       >>> include h
\       >>> include ../mlib/hexdump.fs
\       <<<<<<<<<<< ../mlib/hexdump.fs (73 lines)
\       >>> include ../flib/io-stm32f1.fs
\       <<<<<<<<<<< ../flib/io-stm32f1.fs (69 lines)
\       >>> include ../flib/hal-stm32f1.fs
\       <<<<<<<<<<< ../flib/hal-stm32f1.fs (134 lines)
\       >>> include ../flib/timer-stm32f1.fs
\       <<<<<<<<<<< ../flib/timer-stm32f1.fs (47 lines)
\       >>> include ../flib/pwm-stm32f1.fs
\       <<<<<<<<<<< ../flib/pwm-stm32f1.fs (52 lines)
\       >>> include ../flib/adc-stm32f1.fs
\       <<<<<<<<<<< ../flib/adc-stm32f1.fs (54 lines)
\       >>> include ../flib/rtc-stm32f1.fs
\       <<<<<<<<<<< ../flib/rtc-stm32f1.fs (43 lines)
\       >>> include ../flib/ring.fs
\       <<<<<<<<<<< ../flib/ring.fs (32 lines)
\       >>> include ../flib/uart2-stm32f1.fs
\       <<<<<<<<<<< ../flib/uart2-stm32f1.fs (31 lines)
\       >>> include ../flib/uart2-irq-stm32f1.fs
\       <<<<<<<<<<< ../flib/uart2-irq-stm32f1.fs (24 lines)
\       >>> include ../flib/spi-stm32f1.fs
\       <<<<<<<<<<< ../flib/spi-stm32f1.fs (68 lines)
\       >>> include ../flib/i2c-bb.fs
\       <<<<<<<<<<< ../flib/i2c-bb.fs (49 lines)
\       <<<<<<<<<<< h (57 lines)
\ done.

There’s still a buglet in this setup: the “reset” word leads to a runaway loop of “Unhandled Interrupt 00000003” - but this can be recovered through the reset button on the web page. The same happens with eraseflash and any other word indirectly calling reset.

Perhaps it’s related to timing or the junk character generated by Mecrisp after such a s/w reset?

Anyway… thanks to ESP-Link, it’s now possible to tinker with the JeeLabs Energy Monitor prototype from anywhere in the house. A huge convenience!

Measuring AC supply voltage

$
0
0

With the JeeLabs Energy Monitor prototype hooked up in the meter cabinet, it’s now very easy to explore things a bit. One interesting test is to look at the analog signal from the AC power supply + divider, which is connected to pin PA0.

Here is some code which samples that signal and prints it out until a key is pressed:

: a1 +adc begin pa0 adc . key? until ;
a1 4029 4031 4030 4030 [...] 1268 1343 1763 2330  ok.

The ADC is initialised, and then we acquire a value, print it, and loop until some key is pressed. The readings are easily copied to a spreadsheet and graphed:

Some observations:

  • as expected, we’re only reading the negative excursions of the supply voltage
  • the scale is working out nicely, over 70% of the ADC’s 12-bit resolution is used
  • it looks like we’re capturing about 25 samples per 50 Hz cycle
  • this loop timing is partly limited by the 115,200 baud rate of the (polled!) serial output
  • i.e. 10 bits per char, 5 chars per ADC (4 digits and a space) @ 115,200 baud takes 434 µs
  • plus some time for the ADC - hmmm… not sure where the remaining 300+ µs went!

The current polled ADC acquisition code itself is very fast. It’s easy to measure this:

: a2 micros pa0 adc micros nip swap - . ;  ok.
a2 41  ok.

So it takes 41 µs to acquire one ADC sample (less actually, the micros code has some overhead). To improve on this, we can first acquire samples in a quick loop, and then print them out:

4000 buffer: adata  ok.
: a3 1000 0 do pa0 adc i cells adata + ! loop ;  ok.
: a4 +adc micros a3 micros swap - . ;  ok.
: a5 1000 0 do i cells adata + @ . loop ;  ok.
a4 32915  ok.
a5 1333 1334 1334 [...] 4026 4028 4027  ok.

Which means that we’ve captured 33 ms of data, i.e. more than one full 50 Hz cycle:

Both half-wave captures consist of 338 readings, with 269 readings in between, when the ADC returns 4030 or 4031. Which means that the zero crossings are indeed included in our data, and that there is enough information to reconstruct a full sine wave for CT power calculations later.

Note that the sine wave has somewhat flattened peaks and there also appears to be some noise.

These readings were taken with a µC running at 8 MHz, which gives enough resolution for now. At 72 MHz, a2 reports 6 µs and a4 reports 4949 µs - this translates to a polled ADC acquisition rate of over 200,000 samples per second. The flip side is that at this higher rate we can only capture some 5 ms with a 1000-entry buffer, considerably less than a full 50 Hz AC cycle:

For tests, the polled approach is fine - and very convenient, given the interactive command-line access provided by the ESP-Link - but the acquisition rate will not be constant due to occasional interrupts, and the µC would be tied up most of the time, which is clearly not very practical.

The solution is to use DMA-based ADC acquisition. Then, we only need to deal with acquired data in large chunks. The STM32F103 µC’s DMA hardware has two nice interrupts for this: when the buffer is half full, and when it reaches the end and acquisition starts over at the beginning.

Let’s plan ahead a bit and consider the rates and time + memory resources needed:

  • there are 4 ADC channels: 1 for supply voltage and 3 for the current transformers
  • we could collect 1600 samples per channel, that’s 800 samples per interrupt
  • this requires 4 x 1600 x 2 bytes = 12.8 KB, which is ok - the Olimexino has 20 KB RAM
  • if we sample at 25,000 Hz, each 800-sample block will cover 32 ms of time
  • this is enough to always capture more than one full wave, i.e. 3 .. 4 zero crossings
  • we’ll get one interrupt every 32 ms, with a 800-sample block ready for each channel
  • so during this interrupt, the code will need to process 4 x 800 = 3200 data points

Even if our calculations take more than 32 ms, this would be ok: we could simply ignore some acquisition cycles. The only constraint is that the buffered data needs to be processed (or moved out of the way) before the DMA engine overwrites it with new data, i.e. within 32 ms.

The beauty of DMA is that it completely frees the CPU. It “only” has to deal with ≈ 30 interrupts per second, with all ADC values acquired exactly 40 µs apart and stored in the DMA buffer.

There is still plenty of work to do… such as dealing with phase and finding those zero crossings.

Viewing all 296 articles
Browse latest View live