LM386 Audio Amplifier

Learning how to use the LM386 Audio Amplifier IC — a “minimum parts” circuit.

I was pondering how I was going to amplify the signal coming out of my AD9833 signal generator module, to drive my MakerHawk 3 watt, 8 ohm speaker. Then I found an LM386 IC in my box of assorted op-amps. I found this amplifier to be easy to use, especial with the example “minimum parts” circuit in the data sheet:

Minimum parts circuit from LM386 data sheet. Pins 1, 7, and 8 can be left open if you are okay with the default gain setting of 20.
The LM386 pinout

In the picture at the top of this post, you can see the AD9833 module, but I’m actually trying out the amplifier chip by driving it with a desktop signal generator, which is connected to the leads on the left. With the signal generator set to as little as 100mV p-p, I had no trouble hearing the sound on the speaker with 1khz, 1.5 khz, and 2 khz tones. The sound was quiet and distorted at 500 Hz and lower, but that would be expected of such a small speaker.

The Uno in the picture is not providing the LM386 input signal in this test, but is simply providing the 5V supply voltage.

Some caveats; the LM386 requires at least 4V supply voltage, which is a problem for folks who want to use their microcontrollers with a lower 3.3 V supply only. Also, I think the AD9833 generated a 700 mV p-p signal when I tried it last week, which I believe fits just inside the LM386’s range of minimum -4V and maximum +4V input signal — probably you will want a voltage divider or volume control to attenuate the signal some.

AD9833 Signal Driven by Flash Forth

400 Hz signal from an AD9833 module, initialized by 328P MC running FlashForth

Having figured out how to configure the parameters of the SPI bus, and transmit bytes, I wanted then drive my AD9833 module, which is a frequency generator.

Arduino Uno running FlashForth connected to a GY-9833 module, which is a dev module for the AD9833 signal generator chip

I determined from the datasheet that I would need the SCLK signal to be high on idle, and to sample on the leading edge. Also, fsync needs to go low before the data is transmitted.

Timing Diagrams for AD9833 chip

Here are the FSYNC, SCK, and data signals on a scope:

FSYNC signal is yellow, SCK signal is green, and data signal is purple

I actually had to study these signals for a few minutes because at first the initialization code was not working. I released that there was a bug in my code such that the fsync signal was inverted. That was easy to fix.

The above data translates to this example initialization data from the programmer’s guide:

Zooming in, you can see the first byte, 0x21:

First byte’s worth of signals

With sampling on leading edge of SCK (green signal) we have 00100001, i.e., 0x21.

Here is the code so far:

\ ad9833.fs

\ Copyright 2021 Christopher Howard

\ Licensed under the Apache License, Version 2.0 (the "License");
\ you may not use this file except in compliance with the License.
\ You may obtain a copy of the License at

\     http://www.apache.org/licenses/LICENSE-2.0

\ Unless required by applicable law or agreed to in writing, software
\ distributed under the License is distributed on an "AS IS" BASIS,
\ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
\ See the License for the specific language governing permissions and
\ limitations under the License.

ad9833
marker ad9833

: init-spi ( -- )
    \ set data direction bits
    DD_OUT DD_MOSI lshift
    DD_OUT DD_SCK  lshift or
    DD_OUT DD_SS   lshift or DDR_SPI mset
    \ Setup control register
    1           SPR0 lshift
    0           SPR1 lshift or \ fck/16
    CPHA_SMPLED CPHA lshift or
    CPOL_HIDLE  CPOL lshift or
    MSTR_MSTR   MSTR lshift or 
    DORD_MSB    DORD lshift or
    SPE_ENAB    SPE  lshift or
    SPIE_DISAB  SPIE lshift or SPCR c!
;

: init-fsync [ DD_OUT #4 lshift ] literal DDRD mset ;

: fsync-low [ 1 #4 lshift ] literal PORTD mclr ;

: fsync-high [ 1 #4 lshift ] literal PORTD mset ;

: demo-400hz
    init-spi
    init-fsync
    fsync-low
    $21 tx-spi $00 tx-spi
    $50 tx-spi $c7 tx-spi 
    $40 tx-spi $00 tx-spi 
    $c0 tx-spi $00 tx-spi 
    $20 tx-spi $00 tx-spi
    fsync-high
;

\ Only for viewing the SPI signals
: test-demo begin demo-400hz 1 ms again ;

First you have to load this ad9833 code:

\ ff-328p.fs

\ Copyright 2021 Christopher Howard

\ Licensed under the Apache License, Version 2.0 (the "License");
\ you may not use this file except in compliance with the License.
\ You may obtain a copy of the License at

\     http://www.apache.org/licenses/LICENSE-2.0

\ Unless required by applicable law or agreed to in writing, software
\ distributed under the License is distributed on an "AS IS" BASIS,
\ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
\ See the License for the specific language governing permissions and
\ limitations under the License.

ff-328p
marker ff-328p

\ General I/O port addresses

$25 constant PORTB
$28 constant PORTC
$2b constant PORTD

\ Data Direction Registers

$24 constant DDRB
  1 constant DD_OUT
  0 constant DD_IN
$2a constant DDRD

\ DDR for SPI comms

$24 constant DDR_SPI
$2 constant DD_SS
$3 constant DD_MOSI
$5 constant DD_SCK

\ SPI control register

$4c constant SPCR
$0 constant SPR0 \ SPI Clock Rate Selector bits
$1 constant SPR1 \ (see table 18-5 in 328P datasheet)
$2 constant CPHA \ Clock Phase bit
  1 constant CPHA_SMPTRL \ sample on trailing edge of SCK
  0 constant CPHA_SMPLED \ sample on leading edge of SCK
$3 constant CPOL \ Clock Polarity bit
  1 constant CPOL_HIDLE \ SCK high when idle
  0 constant CPOL_LIDLE \ SCK low when idle
$4 constant MSTR \ Master/Slave Select bit
  1 constant MSTR_MSTR \ master mode
  0 constant MSTR_SLAVE \ slave mode
$5 constant DORD \ Data Order bit
  1 constant DORD_LSB \ LSB transmitter first
  0 constant DORD_MSB \ MSB transmitted first
$6 constant SPE \ SPI Enable bit
  1 constant SPE_ENAB \ SPI enabled
  0 constant SPE_DISAB \ SPI disabled
$7 constant SPIE \ SPI Interrupt Enable bit
  1 constant SPIE_ENAB \ SPI Interrupt Enabled
  0 constant SPIE_DISAB \ SPI Interrupt Disabled

\ SPI status register

$4d constant SPSR 
$0 constant SPI2X \ Double SPI Speed Bit
$6 constant WCOL \ Write COLlision Flag
$7 constant SPIF \ SPI Interrupt Flag

\ SPI Data Register (i/o port)

$4e constant SPDR

: tx-spi ( c -- )
    SPDR c! begin SPSR c@ 1 SPIF lshift and until
;

2d Array Problem in Gforth

Standard Forth includes the ability to define classes of memory objects, along with an instance behavior for that class. It’s a little like OOP, but without inheritance and with only one class method. Basically, it is a way to add a forth word which can be used to initialize memory and map it to a name, and then use that new name as an interface to the memory. The forth words constant and variable are such words, which define in memory a constant or a variable. Then the name of said constant or variable can be called on to return the value of the constant or the address of the variable’s storage memory.

One problem from Forth Application Techniques is to create a 2D array class. Here is a (non-optimized) solution:

: 2< ( n0 n1 n2 n3 -- b ) rot > -rot < and ;

: 4dup ( n0 n1 n2 n3 -- n0 n1 n2 n3 n0 n1 n2 n3 )
    3 pick 3 pick 3 pick 3 pick ;

: 2array ( n0 n1 -- ) create 2dup , , * cells allot
  does> ( n0 n1 -- a )
    dup >r 2@ 4dup 2< 0= if -24 throw then
    swap 2 pick * nip nip + cells r> + 2 cells + ;

The first two words, 2< and 4dup are some helper words.

The first line of 2array is the defining action, which takes two numbers of the stack and stores them in the memory space to serve as dimensions for the array. Further memory is allotted according to the dimensions given. create associates the memory with the instance word.

The code for 2array after does> is the instance behavior, which takes two index numbers off the stack, and returns the correct memory address. The first line of the instance behavior code serves to confirm that the indices are not larger than the array dimensions. The second line then calculates the appropriate memory address and drops it on the stack.

Here is defining a 3×2 array:

3 2 2array data  ok

In Gforth our cell size is 8 bytes, so our memory address increments by 8 as we move through the array:

0 0 data . 140051427947736  ok
1 0 data . 140051427947744  ok
2 0 data . 140051427947752  ok
0 1 data . 140051427947760  ok
1 1 data . 140051427947768  ok
2 1 data . 140051427947776  ok

If we go too large, we throw an error:

0 2 data . 
:19: Invalid numeric argument
0 2 >>>data<<< .
Backtrace:
$7F60439CF420 throw 
$7F60439CF4C8

Of course, we can store things in our array:

0 0 0 data !  ok
1 1 0 data !  ok
2 2 0 data !  ok
3 0 1 data !  ok
4 1 1 data !  ok
5 2 1 data !  ok
0 0 data 3 2 * cells dump 
7F60439CF4D8: 00 00 00 00  00 00 00 00 - 01 00 00 00  00 00 00 00  ................
7F60439CF4E8: 02 00 00 00  00 00 00 00 - 03 00 00 00  00 00 00 00  ................
7F60439CF4F8: 04 00 00 00  00 00 00 00 - 05 00 00 00  00 00 00 00  ................
 ok

FlashForth SPI (328P)

example 0: byte 0xDE sent using SPI (green) and SCK driver signal (yellow). Common SPI configuration.

I wrote some some FlashForth code to be able to initialize the SPI system on the 328P, and transmit bytes – a more thorough version of what I had done earlier with Arduino-FVM. It basically amounted to a list of SPI-related register and bit constants.

\ ff-328p.fs

\ Copyright 2020 Christopher Howard

\ Licensed under the Apache License, Version 2.0 (the "License");
\ you may not use this file except in compliance with the License.
\ You may obtain a copy of the License at

\     http://www.apache.org/licenses/LICENSE-2.0

\ Unless required by applicable law or agreed to in writing, software
\ distributed under the License is distributed on an "AS IS" BASIS,
\ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
\ See the License for the specific language governing permissions and
\ limitations under the License.

ff-328p
marker ff-328p

\ General I/O port addresses

$25 constant PORTB
$28 constant PORTC
$2b constant PORTD

\ Data Direction Registers

$24 constant DDRB
  1 constant DD_OUT
  0 constant DD_IN

\ DDR for SPI comms

$24 constant DDR_SPI
$2 constant DD_SS
$3 constant DD_MOSI
$5 constant DD_SCK

\ SPI control register

$4c constant SPCR
$0 constant SPR0 \ SPI Clock Rate Selector bits
$1 constant SPR1 \ (see table 18-5 in 328P datasheet)
$2 constant CPHA \ Clock Phase bit
  1 constant CPHA_SMPTRL \ sample on trailing edge of SCK
  0 constant CPHA_SMPLED \ sample on leading edge of SCK
$3 constant CPOL \ Clock Polarity bit
  1 constant CPOL_HIDLE \ SCK high when idle
  0 constant CPOL_LIDLE \ SCK low when idle
$4 constant MSTR \ Master/Slave Select bit
  1 constant MSTR_MSTR \ master mode
  0 constant MSTR_SLAVE \ slave mode
$5 constant DORD \ Data Order bit
  1 constant DORD_LSB \ LSB transmitter first
  0 constant DORD_MSB \ MSB transmitted first
$6 constant SPE \ SPI Enable bit
  1 constant SPE_ENAB \ SPI enabled
  0 constant SPE_DISAB \ SPI disabled
$7 constant SPIE \ SPI Interrupt Enable bit
  1 constant SPIE_ENAB \ SPI Interrupt Enabled
  0 constant SPIE_DISAB \ SPI Interrupt Disabled

\ SPI status register

$4d constant SPSR 
$0 constant SPI2X \ Double SPI Speed Bit
$6 constant WCOL \ Write COLlision Flag
$7 constant SPIF \ SPI Interrupt Flag

\ SPI Data Register (i/o port)

$4e constant SPDR

: tx-spi ( c -- )
    SPDR c! begin SPSR c@ 1 SPIF lshift and until
;

Here are some examples:

\ a common configuration
: init-spi-ex0 ( -- )
    \ set data direction bits
    DD_OUT DD_MOSI lshift
    DD_OUT DD_SCK  lshift or
    DD_OUT DD_SS   lshift or DDR_SPI c!
    \ Setup control register
    1           SPR0 lshift
    0           SPR1 lshift or \ fck/16
    CPHA_SMPLED CPHA lshift or
    CPOL_LIDLE  CPOL lshift or
    MSTR_MSTR   MSTR lshift or 
    DORD_MSB    DORD lshift or
    SPE_ENAB    SPE  lshift or
    SPIE_DISAB  SPIE lshift or SPCR c!
;

\ send byte repeatedly with 1 ms delay in between
: rpt-spi-char ( c -- )
    begin dup tx-spi 1 ms again ;

init-spi-ex0  ok<#,ram> 
$de rpt-spi-char
example 0: a common configuration

Here we are transmitting with a 1Mhz clock (fck/16). We have the SCK set to idle on low voltage, with the sample on the lead edge. This means when you see the yellow signal jump up, look down at the green line to see the bit – high for 1 and low for 0. Here we have 11011110, i.e., value 0xDE in Most Significant bit (MSB) order, as expected.

In this slight variation, we sample on the trailing edge of the SCK signal instead:

\ with clock phase sampled on trailing
: init-spi-ex1 ( -- )
    \ set data direction bits
    DD_OUT DD_MOSI lshift
    DD_OUT DD_SCK  lshift or
    DD_OUT DD_SS   lshift or DDR_SPI c!
    \ Setup control register
    1          SPR0 lshift
    0          SPR1 lshift or \ fck/16
    CPHA_SMPTRL CPHA lshift or
    CPOL_LIDLE CPOL lshift or
    MSTR_MSTR  MSTR lshift or 
    DORD_MSB   DORD lshift or
    SPE_ENAB   SPE  lshift or
    SPIE_DISAB SPIE lshift or SPCR c!
;
example 1: sample on trailing edge of SCK

So to determine the bit value, you must look at the value below where the yellow SCK signal falls down to low.

Another variation is to go back to sampling on the leading edge, but have SCK idle on high, so again we must look for the SCK signal to fall down to low.

\ clock polarity high on idle
: init-spi-ex2 ( -- )
    \ set data direction bits
    DD_OUT DD_MOSI lshift
    DD_OUT DD_SCK  lshift or
    DD_OUT DD_SS   lshift or DDR_SPI c!
    \ Setup control register
    1           SPR0 lshift
    0           SPR1 lshift or \ fck/16
    CPHA_SMPLED CPHA lshift or
    CPOL_HIDLE  CPOL lshift or
    MSTR_MSTR   MSTR lshift or 
    DORD_MSB    DORD lshift or
    SPE_ENAB    SPE  lshift or
    SPIE_DISAB  SPIE lshift or SPCR c!
;
example 2: SCK idle on high

In another variation, we can go back to the common configuration, with SCK idle on low, and looking for the leading edge (rise in SCK signal), but instead transmit the bits in Least Significant Bit (LSB) order:

\ LSB data order
: init-spi-ex3 ( -- )
    \ set data direction bits
    DD_OUT DD_MOSI lshift
    DD_OUT DD_SCK  lshift or
    DD_OUT DD_SS   lshift or DDR_SPI c!
    \ Setup control register
    1           SPR0 lshift
    0           SPR1 lshift or \ fck/16
    CPHA_SMPLED CPHA lshift or
    CPOL_LIDLE  CPOL lshift or
    MSTR_MSTR   MSTR lshift or 
    DORD_LSB    DORD lshift or
    SPE_ENAB    SPE  lshift or
    SPIE_DISAB  SPIE lshift or SPCR c!
;
example 3: transmit bits in LSB order

Now you see the bits are 01111011, which reverses to 11011110, i.e., 0xDE.

Our final demonstration variation is the common configuration but at a much slower speed, fck/128, or 125,000 bits/sec in our case.

\ much slower speed
: init-spi-ex4 ( -- )
    \ set data direction bits
    DD_OUT DD_MOSI lshift
    DD_OUT DD_SCK  lshift or
    DD_OUT DD_SS   lshift or DDR_SPI c!
    \ Setup control register
    1           SPR0 lshift
    1           SPR1 lshift or \ fck/128
    CPHA_SMPLED CPHA lshift or
    CPOL_LIDLE  CPOL lshift or
    MSTR_MSTR   MSTR lshift or 
    DORD_MSB    DORD lshift or
    SPE_ENAB    SPE  lshift or
    SPIE_DISAB  SPIE lshift or SPCR c!
;
example 4: slower speed

You see the waveform is the same except one SCK cycle is taking a full 8 microseconds.

FlashForth Question: Sending a File

A question recently received from a new FlashForth user:

> The (theoretically) simple thing I'm struggling with is: How do I
> transmit a file for compilation. I google the !/&§" out of it, but
> can't find a solution.
> Working with a linux system, I've tried putty, moserial, minicom and
> more, but none allows me to tranfer a file w/o errors.
> Just working through the terminal works great, jut uploading words is
> not possible. Handshake is on (hard- and soft).
> How do you do that?

My response:

As best as I can figure it, it seems that for some reason FlashForth cannot receive characters while it is processing the previous line of input from the terminal, so some characters are lost if you simply paste a file to the terminal. However, the shell that comes with FlashForth is evidently designed to work around this limitation, waiting for the correct time to send each line. In the FlashForth files there is a directory called shell which contains a python script called ff-shell.py. After you start it, you can use the #send <filename> command to send a file. There are a number of other handy commands in that shell for interacting with FlashForth.

A few caveats about the FlashForth shell:

(1) The script is a python2 script, and you must have the pyserial python library install (the python2 version of it).

(2) #send will try to add the extension “.txt” to whatever filename you give it, which is annoying. I fixed this by commenting out lines 175-176 in the source code.

          if len(args) > 3:
            stopString = args[3]
          # if pathfile.endswith(".txt") == False:
          #   pathfile = pathfile + ".txt"
          line = ""