By now we all get it, cows say moo. Sure. But what about subway car doors and jackhammers? What do they say? Build your own city-themed See N Say to teach the children!

This project replaces the brains of a classic talking toy with a modern, CircuitPython-powered KB2040 microcontroller, with a collection of typical urban sounds and custom illustrations.

illustrations by Brian Kesinger. Explore more of his work at Briankesinger.com

Parts

Angled shot of short black microcontroller.
A wild Kee Boar appears! It’s a shiny KB2040! An Arduino Pro Micro-shaped board for Keebs with RP2040. (#keeblife 4 evah) A lot of folks like using Adafruit...
$8.95
In Stock
Angled shot of a Adafruit I2S 3W Class D Amplifier Breakout.
Listen to this good news - we now have an all in one digital audio amp breakout board that works incredibly well with the 
Out of Stock
Angled shot of boost converter PCB.
This adorable little board will come in very handy whenever you need a good amount of 5V power. It's the size of a linear regulator, but it's actually a mini-booster! Input...
$3.95
In Stock
Top view of Adafruit Perma-Proto Quarter-sized Breadboard PCB.
Customers have asked us to carry basic perf-board, but we never liked the look of most basic perf: it's always crummy quality, with pads that flake off and no labeling. Then we...
$2.95
In Stock
Mini Panel Mount SPDT Toggle Switch
This or that, one or the other, perhaps or perhaps not! So hard to make decisions these days without feeling like you're just going back and forth constantly. Deciding whether or...
$0.95
In Stock
USB Type A to Type C Cable - approx 1 meter / 3 ft long
As technology changes and adapts, so does Adafruit. This  USB Type A to Type C cable will help you with the transition to USB C, even if you're still...
$4.95
In Stock

Fisher Price Classic Farmer Says See 'N Say

Available at toy stores and online, such as here. Note, this project uses the modern electronic toy, not a vintage mechanical one.

Shottky Diode

You can use a regular power diode such as the one below, but a proper Shottky diode is even better, it only has a ~0.3V drop vs. closer to ~0.7V.

 10 pack of 1N4001 Diodes
This here is a 10 pack of the classic 1N4001 power blocking diode. These are good for reverse polarity protection (put it between your DC power jack and circuitry to avoid a...
$1.50
In Stock
Large spool of Wire Wrap Thin Prototyping & Repair Wire
This stuff is called "wire-wrap wire" because it used to be used for wire-wrapping high-speed digital circuits on a special kind of contact board. It's pretty rare to see wire-wrapping in...
$7.50
In Stock
1 x High Temperature Polyimide Tape
1cm wide x 33 meter roll

Optional

You can replace the toy's graphics with your own, or the handy downloadable graphics included in the guide. Vinyl sticker paper for laser printers works well, such as this.

CircuitPython is a derivative of MicroPython designed to simplify experimentation and education on low-cost microcontrollers. It makes it easier than ever to get prototyping by requiring no upfront desktop software downloads. Simply copy and edit files on the CIRCUITPY drive to iterate.

CircuitPython Quickstart

Follow this step-by-step to quickly get CircuitPython running on your board.

Click the link above to download the latest CircuitPython UF2 file.

Save it wherever is convenient for you.

To enter the bootloader, hold down the BOOT/BOOTSEL button (highlighted in red above), and while continuing to hold it (don't let go!), press and release the reset button (highlighted in blue above). Continue to hold the BOOT/BOOTSEL button until the RPI-RP2 drive appears!

If the drive does not appear, release all the buttons, and then repeat the process above.

You can also start with your board unplugged from USB, press and hold the BOOTSEL button (highlighted in red above), continue to hold it while plugging it into USB, and wait for the drive to appear before releasing the button.

A lot of people end up using charge-only USB cables and it is very frustrating! Make sure you have a USB cable you know is good for data sync.

You will see a new disk drive appear called RPI-RP2.

 

Drag the adafruit_circuitpython_etc.uf2 file to RPI-RP2.

The RPI-RP2 drive will disappear and a new disk drive called CIRCUITPY will appear.

That's it, you're done! :)

Safe Mode

You want to edit your code.py or modify the files on your CIRCUITPY drive, but find that you can't. Perhaps your board has gotten into a state where CIRCUITPY is read-only. You may have turned off the CIRCUITPY drive altogether. Whatever the reason, safe mode can help.

Safe mode in CircuitPython does not run any user code on startup, and disables auto-reload. This means a few things. First, safe mode bypasses any code in boot.py (where you can set CIRCUITPY read-only or turn it off completely). Second, it does not run the code in code.py. And finally, it does not automatically soft-reload when data is written to the CIRCUITPY drive.

Therefore, whatever you may have done to put your board in a non-interactive state, safe mode gives you the opportunity to correct it without losing all of the data on the CIRCUITPY drive.

Entering Safe Mode

To enter safe mode when using CircuitPython, plug in your board or hit reset (highlighted in red above). Immediately after the board starts up or resets, it waits 1000ms. On some boards, the onboard status LED (highlighted in green above) will blink yellow during that time. If you press reset during that 1000ms, the board will start up in safe mode. It can be difficult to react to the yellow LED, so you may want to think of it simply as a slow double click of the reset button. (Remember, a fast double click of reset enters the bootloader.)

In Safe Mode

If you successfully enter safe mode on CircuitPython, the LED will intermittently blink yellow three times.

If you connect to the serial console, you'll find the following message.

Auto-reload is off.
Running in safe mode! Not running saved code.

CircuitPython is in safe mode because you pressed the reset button during boot. Press again to exit safe mode.

Press any key to enter the REPL. Use CTRL-D to reload.

You can now edit the contents of the CIRCUITPY drive. Remember, your code will not run until you press the reset button, or unplug and plug in your board, to get out of safe mode.

Flash Resetting UF2

If your board ever gets into a really weird state and CIRCUITPY doesn't show up as a disk drive after installing CircuitPython, try loading this 'nuke' UF2 to RPI-RP2. which will do a 'deep clean' on your Flash Memory. You will lose all the files on the board, but at least you'll be able to revive it! After loading this UF2, follow the steps above to re-install CircuitPython.

Text Editor

Adafruit recommends using the Mu editor for editing your CircuitPython code. You can get more info in this guide.

Alternatively, you can use any text editor that saves simple text files

Download the Project Bundle

Your project will use a specific set of CircuitPython libraries, and the code.py file. To get everything you need, click on the Download Project Bundle link below, and uncompress the .zip file.

Drag the contents of the uncompressed bundle directory onto your KeeBoar board's CIRCUITPY drive, replacing any existing files or directories with the same names, and adding any new ones that are necessary.

Upload the Code and Libraries to the KB RP2040

After downloading the Project Bundle, plug your KB2040 into the computer USB port. You should see a new flash drive appear in the computer's File Explorer or Finder (depending on your operating system) called CIRCUITPY. Unzip the folder and copy the following items to the KB2040's CIRCUITPY drive. 

  • lib folder
  • wavs folder
  • code.py
# SPDX-FileCopyrightText: 2023 John Park for Adafruit Industries
#
# SPDX-License-Identifier: MIT

import os
import audiocore
import board
import audiobusio
import audiomixer
from digitalio import DigitalInOut, Pull
from adafruit_debouncer import Button

button_pins = (
                board.D2, board.D3, board.D4, board.D5,
                board.D6, board.D7, board.D8, board.D9,
                board.D10, board.MOSI, board.MISO, board.SCK,
)
buttons = []   # will hold list of Debouncer button objects
for pin in button_pins:   # set up each pin
    tmp_pin = DigitalInOut(pin)  # defaults to input
    tmp_pin.pull = Pull.UP      # turn on internal pull-down resistor
    buttons.append(Button(tmp_pin, value_when_pressed=False))

# get the filenames in aplha order from files in the 'wavs' directory
sounds = []
for filename in sorted(os.listdir("/wavs")):
    filename = filename.lower()
    if filename.endswith(".wav") and not filename.startswith("."):
        sounds.append(filename)

audio = audiobusio.I2SOut(bit_clock=board.A1, word_select=board.A2, data=board.A3)
mixer = audiomixer.Mixer(voice_count=1, sample_rate=11025, channel_count=1,
                         bits_per_sample=16, samples_signed=True)

audio.play(mixer)
mixer.voice[0].level = 0.5

def play_sound(sound_number):
    wave_file = open(("wavs/" + sounds[sound_number]), "rb")
    wave = audiocore.WaveFile(wave_file)
    mixer.voice[0].play(wave, loop=False)


while True:
    for i in range(len(buttons)):
        buttons[i].update()
        if buttons[i].pressed:
            play_sound(i)

Make Your Own

Here's a great guide on prepping your own audio files for use on microcontrollers.

To convert your own .wav files, use audio software such as Audacity to save them with these settings:

  • bits per sample: 16
  • sample rate: 11kHz 
  • channels: mono

A great place to start is freesound.org. The sounds used in this project are:

https://freesound.org/s/86938/
https://freesound.org/s/125557/
https://freesound.org/s/200338/
https://freesound.org/s/339918/
https://freesound.org/s/353180/
https://freesound.org/s/405628/
https://freesound.org/s/417281/
https://freesound.org/s/434085/
https://freesound.org/s/523952/
https://freesound.org/s/612857/
https://freesound.org/s/619087/
https://freesound.org/s/633976/

How It Works

The code does two key things -- wait for button presses and play audio. Here's how it does it.

First, we import libraries, including os for reading the file system, audiocore, audiobusio, and audiomixer for audio playback/mixing, and digitalio and adafruit_debouncer for button input.

import os
import audiocore
import board
import audiobusio
import audiomixer
from digitalio import DigitalInOut, Pull
from adafruit_debouncer import Button

Button Setup

Next, we set up the button input pins and button objects using the debouncer library.

button_pins = (
                board.D2, board.D3, board.D4, board.D5,
                board.D6, board.D7, board.D8, board.D9,
                board.D10, board.MOSI, board.MISO, board.SCK,
)
buttons = []   # will hold list of Debouncer button objects
for pin in button_pins:   # set up each pin
    tmp_pin = DigitalInOut(pin)  # defaults to input
    tmp_pin.pull = Pull.UP      # turn on internal resistor
    buttons.append(Button(tmp_pin, value_when_pressed=False))

Sound Setup

The sounds list is created by alphabetically sorting the .wav files found in the wavs/ directory. This means you can place track numbers in front of your file names to assign them to the desired spot on the wheel.

Then, the audio object is created to send digital audio over I2S to the amp. The mixer object is created to play the chosen wav, and the audio mixer is set to play with a half-volume voice level of 0.5.

sounds = []
for filename in sorted(os.listdir("/wavs")):
    filename = filename.lower()
    if filename.endswith(".wav") and not filename.startswith("."):
        sounds.append(filename)

audio = audiobusio.I2SOut(bit_clock=board.A1, word_select=board.A2, data=board.A3)
mixer = audiomixer.Mixer(voice_count=1, sample_rate=11025, channel_count=1,
                         bits_per_sample=16, samples_signed=True)

audio.play(mixer)
mixer.voice[0].level = 0.5

play_sound() Function

The play_sound() function is defined to receive an integer number which it then uses to open the corresponding wav file and play it once.

def play_sound(sound_number):
    wave_file = open(("wavs/" + sounds[sound_number]), "rb")
    wave = audiocore.WaveFile(wave_file)
    mixer.voice[0].play(wave, loop=False)

Main Loop

The main loop of the program checks for button presses and when one is pressed it calls the play_sound() function to play the sound.

while True:
    for i in range(len(buttons)):
        buttons[i].update()
        if buttons[i].pressed:
            play_sound(i)

Open the Case

Flip the See N Say over and unscrew the battery door, then remove the batteries.

Unscrew the seven screws and set them aside, then carefully pull open the case.

Sound Button Press

Most of the pull string mechanism is there to spin the selector arrow, the only part in the front case section that causes sounds to play is the small plastic nub on the central gear -- when the string is fully pulled out that nub raises up slightly and presses the associated rubber dome switch.

Button Pad

The electronics are all on a single PCB. The center of the PCB is a set of 12 open contact buttons that are closed by pressing rubber domes with conductive pills in them. This is the same as what you'll find in a video game controller or rubber dome switch keyboard.

Carefully remove the rubber ring, then unscrew the two screws holding the PCB in place.

Glue Removal

You'll desolder the battery wires and speaker wires from the board, since we only need the button contacts and common contact test points, the rest of our electronics will be transplanted.

Flip over the PCB and you'll see the battery wires and speaker wires have hot glue blobs on them for strain relief.

Dab on some isopropyl alcohol to loosen and then remove the hot glue.

When desoldering wires be careful to avoid flinging molten solder. Please wear eye protection and avoid burns to extremities.

Wire Desoldering

Heat the two speaker wire solder points and the two battery wire solder points with a soldering iron while gently pulling down on the wires with tweezers or pliers.

KB2040 Prep

Jumper the USB+ > RAW pads with some solder. This will allow us to use 5V power from the MiniBoost (which takes the ~3V AA battery input and converts it). The I2S works better with 5V as well.

KB2040 on PermaProto

Solder the KB2040 to the PermaProto board as shown.

You can use the castellated pads instead of adding pins if you like, this keeps it a bit more svelte.

Amp

Solder the MAX98357A I2S amplifier board to the PermaProto as shown.

You can use bare header pins as shown to keep things low profile.

MiniBoost Prep

Add some Kapton tape or electrical tape to insulate the MiniBoost mounting hole from the PermaProto.

This isn't normally necessary, but since we're keeping it super low profile, the mounting hole plating could short some pads otherwise.

Solder the MiniBoost to the PermaProto board.

The PCB's center hole will need to be clear enough of the wires for the plastic shaft to clear it, so make sure you have enough slack for this. It may be helpful to look at the full set of build photos before going further.

Button Pad Wiring

Conveniently, there is a test pad next to each pair of switch contacts! Solder a thin wire, approximately 5" long, to each test pad as well as the GND pad.

Wire wrap wire, enamel coated magnet wire, and 28-30 gauge silicone covered wire all work well for this. Make an effort to run the wires inward so they won't disrupt the elastomer pad.

Button Connections

Place the button board and PermaProto into the back case temporarily to get a sense of the wire lengths and trim/strip as needed.

Then, one-by-one, feed a wire through the PCB's center hole and solder the twelve button wires to their respective GPIO pins, as well as the GND from the button board to the PermaProto.

KeeBoar Ground

Since the KB2040 is hanging off the edge of the board for space considerations, you'll need to free-wire its GND to the PermaProto. Run a wire from each side to the ground rails, so both sides of the PermaProto have common ground.

Battery Box Connections

Place some insulating tape over the battery box contacts so nothing is shorted to the PermaProto accidentally.

Solder the battery box ground (black) wire to PermaProto ground rail, and the battery box +V (red) wire to the Vin pad on the MiniBoost.

To prevent any issues in case the KB2040 is plugged into USB when the batteries are also supplying power, solder a Shottky diode from MiniBoost 5V to PermaProto power rail as shown.

Run a wire from one PermaProto power rail to the other, then wire the KB2040 RAW pin to the PermaProto power rail.

Power Enable Switch

You'll turn the toy on and off with a DPST toggle switch that shorts the MiniBoost enable pin to ground. 

Solder one wire to the center common pin and another to either outer pin of the switch.

Ream out the packaging key hole a bit and then insert the switch through it. Tighten the nut from the other side to secure.

Solder the center wire to ground and the other wire to the MiniBoost En pin.

I2S Audio Amp Wiring

Wire the MAX 98357A I2S Amp as follows:

  • KB2040 A1 to BCLK (bit clock)
  • KB2040 A2 to LRC (word select)
  • KB2040 A3 to DIN (data)
  • Ground to GND
  • 5V power rail (from MiniBoost output post-diode) to Vin
  • Speaker wires to speaker output

Place the elastomer button pad ring back on the PCB, pulling the rubber nub legs through their respective holes.

Test It

This is a great time to test it out! Put the two AA batteries back in the case, flip on the toggle switch, and press each rubber dome button to hear the audio.

Secure the Brain

Use double-stick foam tape to secure the PermaProto and elecronics to the inside of the case on the back of the battery box.

Screw the original PCB back in place.

Wire Dressing

Use Kapton tape to secure wiring in any places where it may be a bit loose in order to avoid any snags.

Close the Case

Pull and hold the pull-string to get the arced gear out of the way (it will slide under the transplanted PermaProto board during operation), then close the case.

Release the string slowly to make sure there are no obstructions.

Screw the Case in Place

Refasten the seven case screws, then insert two AA batteries and close the battery door, securing it with the captive screw.

You can print out this wonderful set of illustrations by the incredible Brian Kesinger and use them to customize your city-themed See N Say!

Matte finish vinyl sticker paper for laser printers worked very well for me -- I used the "heavy paper" setting in the print dialog to make sure the toner bonded well with the paper.

Print at 100% size, cut carefully with a hobby knife, and peel and stick, being sure to smooth out bubbles as you go.

This guide was first published on Feb 01, 2023. It was last updated on Jun 13, 2024.