ff-ad9833 Repo

codeberg repository for ad9833-related FlashForth code

For my on-going exploration of ad9833 audio generation using FlashForth 5, a code repository is now available at codeberg:

https://codeberg.org/infrared/ff-ad9833

So far, I have a demo-pitches word that plays c4, e4, g4, and c5 frequencies, but I can set other frequencies ranging from a4 to g#6, using the words I have so far. There is no system yet for setting duration, so I can’t play notes proper. But it was fun setting up a system for setting the equal temperament pitches.

In microcontroller programming, it is much easier to justify efforts to save memory. I needed to store the precalculated frequency register data values for each pitch. Originally I had a table like so:

$5274 , $4000 ,
$538d , $4000 , 
$54b7 , $4000 ,
$55f2 , $4000 ,
...
$4b5a , $4002 ,

With each line (two 16 bits words) representing one pitch. However, the first two bits in each 16-bit word are actually not frequency data, but register addressing bits, which can easily be added in later. Also, the second 16-bit word in each pair has (in my application) only two bits of actual frequency data, which are the two right most bits. So, I cut the table memory usage in half by packing those two bits into the left-most two bits of the first word in each pair:

\ packaged freq register data for equal temp
\ pitches from A4, A#4, B4 , ... , G#6
\ format numbered from lsb to msb:
\ 0-13: the 14 LSBs of the lsb register load
\ 14-15: the 2 LSBs of the msb register load

create packed-pitches
$1274 , $138d ,
$14b7 , $15f2 ,
$1740 , $18a2 ,
$1a19 , $1ba7 ,
...

Now each line in the table is actually two separate pitches. Then I just need a few words to separate the bits back out into separate words, and recombine them with the frequency register addressing bits:

0  constant na
1  constant na#
2  constant nb
3  constant nc
4  constant nc#
5  constant nd
6  constant nd#
7  constant ne
8  constant nf
9  constant nf#
10 constant ng
11 constant ng#

0 constant o4
1 constant o5
2 constant o6

: pull-pitch ( note octave -- u ) 12 * + cells packed-pitches + @ ;

: pull-14lsb %0011111111111111 and ;

: pull-2msb %1100000000000000 and 14 rshift ;

%0100000000000000 constant FREG0

: tx-pitch ( note octave -- )
    pull-pitch cp>r pull-14lsb FREG0 or 2tx-spi
    r> pull-2msb FREG0 or 2tx-spi ;

In testing, I have some trouble with some pitches getting distorted when I set the volume to higher levels. I suspect the problem is that I am still using the USB power supply and need to feed in my +5V from an external PS. I plan to try that next week, God willing.

SPI Driven Audio Circuit w/ Frequency Selection

Assembled Audio Module based on GY-9833 Frequency Generator.

The PCBs I had ordered from OSH Park arrived, and my co-worker Mike was nice enough to solder the components on for me. The module works, and I was able to produce 1000Hz and 2000Hz audio using FlashForth to drive the SPI signals. The sound output sounds pure and pleasant, although when I twist the volume above halfway, my ears detect what sounds like a very slight overtone or distortion, but it still is pleasant-sounding enough.

The module is a step above the usual cheap piezo buzzer circuit, the latter having the grating, distorted sound which is usually undesirable. But it is a few steps below having an actual programmable sound generator chip or a full blown sound card.

The actual PCB design here would not be used in a commercial product as it is very large for its functionality (about 6 cm wide). But it could be constructed easily by any hobbyist as the components are all through-hole and spaced far apart.

Link to design on OSH Park:

https://oshpark.com/shared_projects/2lO7K5tc

Parts list:

Schematic:

KiCAD files:

These are the trimpots that fit the board:

https://www.amazon.com/gp/product/B071WW6VN8/

The GY-9833 module is also available from several manufacturers through Amazon and other Web stores.

Audio Circuit: Single Frequency Output Selection

Audio Circuit Driven by Op Amp, GY-9833 frequency synthesizer, and Arduino

Above is the circuit I mentioned earlier had been intending to design with a small 3W speaker driven by an LM386 op amp driven by an AD9833 frequency synthesizer chip, driven by SPI (sourced in the schematic from an Arduino Uno).

I thought designing and testing this simple circuit would only take an hour or two. But I got hung up for a lot of hours because, in the breadboard, I was getting weird spurious signals in the output when I had the the circuit set to certain volume levels, and I couldn’t figure out what the problem was. In the end, the problem was fixed by putting a large capacitor across +5V and AGND. After that, this output sounded pure and pleasant, testing with 1000Hz, 1500Hz, and 2000Hz signals. I do not know if it is really necessary to use a capacitor quite that big, but I didn’t feel like doing additional testing.

I had thoughts to turn the circuit into a PCB, though it would be through-hole (easier to assemble) and not a miniature surface-mount design like would be seen in a commercial application. This would be around 6cm x 5cm, with the speaker plugging in on the right pins and the Arduino plugging in on the left header. The GY-9833 module would slide onto the center pins and could be soldered on to them.

I can provide design files later after I finish laying out the traces. I can also post some FlashForth code later.

Quick and Dirty Complex Functions of Time with Guile + Gnuplot

f(t) = e^(st) where s = 0+i

Inspired by a great video on the Laplace transform, I was looking for an easy way to visualize complex functions of time, i.e., functions of time where the output is a complex number. This is fairly easy to do with Guile Scheme and Gnuplot, though the method could be polished more into a proper application, which I didn’t do.

This is the core function for plotting the data:

(define (complex-plot t0 tmax step f)
  (unless (> t0 tmax)
    (let ((z (f t0)))
      (display t0)
      (display " ")
      (display (real-part z))
      (display " ")
      (display (imag-part z))
      (display "\r\n")
      (complex-plot (+ t0 step) tmax step f))))

We just pass in the starting value (t0), the maximum time value, a step value (usually some fraction of Pi), and a lambda with our complex function of time. Writing complex functions of time is pretty easy in Guile Scheme because all the numbers are represented internally as complex numbers anyway. E.g.:

scheme@(guile-user)> (complex-plot 0 25 (/ 3.1415 32) (lambda (t) (exp (* 0+i t))))
0 1.0 0.0
0.098171875 0.9951850104692727 0.09801425884672994
0.19634375 0.9807864101254524 0.19508464243304188
0.294515625 0.9569428571883648 0.2902763649975122
0.3926875 0.9238839645735444 0.3826727322450213
...etc...

We have to output this to a file:

(with-output-to-file "out.txt" (lambda () (complex-plot 0 25 (/ 3.1415 32) (lambda (t) (exp (* 0+i t))))))

Then we start gnuplot and use a 3d splot with lines. (Credit goes to this tutorial.)

gnuplot> set xrange [0:25]
gnuplot> set yrange [-2:2]
gnuplot> set ticslevel 0
gnuplot> splot "out.txt" u 1:2:3 with lines

Another interesting complex function of time is the addition of two complex functions of time which leave only a signal on the real axis:

(lambda (t) (+ (exp (* 0+i t)) (exp (* 0-i t))))
e^(s_0 * t) + e^(s_1 * t) where s_0 = 0+i and s_1 = 0-i

And here is my hacker emacs screen, where the action is happening!

hacker emacs screenshot

Moiré Patterns: Square and Hexagon

Overlaid hexagon grids with moderate rotational offset

A square grid with a slight rotational offset gives a square or diamond zig-zag grid:

With smaller squares, the checkerboard pattern is more interesting:

Hexagonal grids give an interesting variety of overlap patterns in one image:

The new code is below:

;; 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.

(define* (draw-square-block r orig-x orig-y base-l
                         #:key (rotr 0))
  (let* ((lrot (if (zero? rotr) (lambda (x) x)
                   (lambda (c) (rot rotr c))))
         (ccomp (lambda (c) (cex (lrot c)))))
    (render-draw-lines
     r
     (map ccomp (list (list orig-x orig-y)
                      (list (+ orig-x base-l) orig-y)
                      (list (+ orig-x base-l) (+ orig-y base-l)))))))

(define* (draw-square-pattern r offset-x offset-y #:key (rotr 0))
  (let* ((base-l 10))
    (do ((j 0 (1+ j)))
        ((> j 80))
        (do ((i 0 (1+ i)))
            ((> i 80))
          (draw-square-block
           r
           (+ (* i base-l) offset-x)
           (+ (* j base-l) offset-y)
           base-l
           #:rotr rotr)))))

(define* (draw-hexagon-block r orig-x orig-y base-l
                         #:key (rotr 0))
  (let* ((base-hl (* 0.5 base-l))
         (base-h (* (* base-l (sqrt 3)) 0.5))
         (lrot (if (zero? rotr) (lambda (x) x)
                   (lambda (c) (rot rotr c))))
         (ccomp (lambda (c) (cex (lrot c)))))
    (render-draw-lines
     r
     (map ccomp (list (list orig-x orig-y)
                      (list (- orig-x base-hl)
                            (+ orig-y base-h))
                      (list orig-x
                            (+ orig-y (* 2 base-h)))
                      (list (+ orig-x base-l)
                            (+ orig-y (* 2 base-h)))
                      (list (+ orig-x base-l base-hl)
                            (+ orig-y base-h))
                      (list (+ orig-x base-l)
                            orig-y))))
    (render-draw-lines
     r
     (map ccomp (list (list (- orig-x base-hl)
                            (+ orig-y base-h))
                      (list (- orig-x base-hl base-l)
                            (+ orig-y base-h)))))))

(define* (draw-hexagon-pattern r offset-x offset-y #:key (rotr 0))
  (let* ((base-l 20)
         (base-h (* (* base-l (sqrt 3)) 0.5))
         (base-hl (* 0.5 base-l)))
    (do ((j 0 (1+ j)))
        ((> j 40))
        (do ((i 0 (1+ i)))
            ((> i 40))
          (draw-hexagon-block
           r
           (+ offset-x
              (* i 3 base-l))
           (+ offset-y (* j 2 base-h))
           base-l
           #:rotr rotr)))))