The libraries for manipulating image formats are too complex and bulky to run on the SAMD21 (M0) boards like the CPX. The data for the DAC is best prepared on a host computer as a (mono) wav file.
Download the code below and the dacanim.wav (use Save link as... in browser) example wav file. Plug your CPX or other M0-based board into your computer via a known-good USB data cable. A flash drive named CIRCUITPY should appear in your file explorer/finder program. Copy the dacanim.wav and code below to the CIRCUITPY drive, renaming the latter to code.py.
Connect the CPX A0 pad output to the an oscilloscope input and GND to ground to see the image. The trigger value may need to be set and the timebase adjusted to set the width. The bright top line is best placed off screen by adjusting the volts/div and y position.
Scroll past the code below for a video showing the image output.
# Output prepared samples from a wav file to (CPX) DAC import board, audioio, audiocore dac = audioio.AudioOut(board.A0) wav_file = open("dacanim.wav", "rb") output_wave = audiocore.WaveFile(wav_file) dac.play(output_wave, loop=True) while True: pass
The image at the top of screen shows the full voltage range of the DAC on an oscilloscope running with a manual negative trigger value. The bitmap image is encoded to appear between 0.3V and 3.0V. The 3.3V level (bright line at top) is used for when there's no pixel to display, the 0V level is used for the synchronisation pulse signifying the beginning of each line.
Oscilloscope Output Video
The video below shows the spinning logo output from a CPX connected to a Hameg HM203-6 oscilloscope. The timebase and volts/div are set so the 40x40 resolution, 50fps animation fills the screen. This is a rare occasion where a slightly unfocussed oscilloscope beam can look better as it enlarges and softens the edges of the "pixels".
Python 3 Code
The example command line and code below for pngtowav can be used on a computer to generate the wav file. This code uses the imageio library.
pngtowav -r -f 50 -o dacanim.wav logo.frame.{00..49}.png
# SPDX-FileCopyrightText: 2019 Kevin J. Walters for Adafruit Industries # # SPDX-License-Identifier: MIT #!/usr/bin/python3 ### pngtowav v1.0 """Convert a list of png images to pseudo composite video in wav file form. This is Python code not intended for running on a microcontroller board. """ ### MIT License ### Copyright (c) 2019 Kevin J. Walters ### Permission is hereby granted, free of charge, to any person obtaining a copy ### of this software and associated documentation files (the "Software"), to deal ### in the Software without restriction, including without limitation the rights ### to use, copy, modify, merge, publish, distribute, sublicense, and/or sell ### copies of the Software, and to permit persons to whom the Software is ### furnished to do so, subject to the following conditions: ### The above copyright notice and this permission notice shall be included in all ### copies or substantial portions of the Software. ### THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ### IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ### FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ### AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ### LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ### OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE ### SOFTWARE. import getopt import sys import array import wave import imageio ### globals ### pylint: disable=invalid-name ### start_offset of 1 can help if triggering on oscilloscope ### is missing alternate lines debug = 0 verbose = False movie_file = False output_filename = "dacanim.wav" fps = 50 threshold = 128 ### pixel level replaceforsync = False start_offset = 1 max_dac_v = 3.3 ### 16 bit wav files always use signed representation for data dac_offtop = 2**15-1 ### 3.30V dac_sync = -2**15 ### 0.00V ### image from 3.00V to 0.30V dac_top = round(3.00 / max_dac_v * (2**16-1)) - 2**15 dac_bottom = round(0.30 / max_dac_v * (2**16-1)) - 2**15 def usage(exit_code): ### pylint: disable=missing-docstring print("pngtowav: " + "[-d] [-f fps] [-h] [-m] [-o outputfilename] [-r] [-s lineoffset] [-t threshold] [-v]", file=sys.stderr) if exit_code is not None: sys.exit(exit_code) def image_to_dac(img, row_offset, first_pix, dac_y_range): """Convert a single image to DAC output.""" dac_out = array.array("h", []) img_height, img_width = img.shape if verbose: print("W,H", img_width, img_height) for row_o in range(img_height): row = (row_o + row_offset) % img_height ### Currently using 0 to (n-1)/n range y_pos = round(dac_top - row / (img_height - 1) * dac_y_range) if verbose: print("Adding row", row, "at y_pos", y_pos) dac_out.extend(array.array("h", [dac_sync] + [y_pos if x >= threshold else dac_offtop for x in img[row, first_pix:]])) return dac_out, img_width, img_height def write_wav(filename, data, framerate): """Create one channel 16bit wav file.""" wav_file = wave.open(filename, "w") nchannels = 1 sampwidth = 2 nframes = len(data) comptype = "NONE" compname = "not compressed" if verbose: print("Writing wav file", filename, "at rate", framerate, "with", nframes, "samples") wav_file.setparams((nchannels, sampwidth, framerate, nframes, comptype, compname)) wav_file.writeframes(data) wav_file.close() def main(cmdlineargs): ### pylint: disable=too-many-branches """main(args)""" global debug, fps, movie_file, output_filename, replaceforsync ### pylint: disable=global-statement global threshold, start_offset, verbose ### pylint: disable=global-statement try: opts, args = getopt.getopt(cmdlineargs, "f:hmo:rs:t:v", ["help", "output="]) except getopt.GetoptError as err: print(err, file=sys.stderr) usage(2) for opt, arg in opts: if opt == "-d": ### pylint counts these towards too-many-branches :( debug = 1 elif opt == "-f": fps = int(arg) elif opt in ("-h", "--help"): usage(0) elif opt == "-m": movie_file = True elif opt in ("-o", "--output"): output_filename = arg elif opt == "-r": replaceforsync = True elif opt == "-s": start_offset = int(arg) elif opt == "-t": threshold = int(arg) elif opt == "-v": verbose = True else: print("Internal error: unhandled option", file=sys.stderr) sys.exit(3) dac_samples = array.array("h", []) ### Decide whether to replace first column with sync pulse ### or add it as an additional column first_pix = 1 if replaceforsync else 0 ### Read each frame, either ### many single image filenames in args or ### one or more video (animated gifs) (needs -m on command line) dac_y_range = dac_top - dac_bottom row_offset = 0 for arg in args: if verbose: print("PROCESSING", arg) if movie_file: images = imageio.mimread(arg) else: images = [imageio.imread(arg)] for img in images: img_output, width, height = image_to_dac(img, row_offset, first_pix, dac_y_range) dac_samples.extend(img_output) row_offset += start_offset write_wav(output_filename, dac_samples, (width + (1 - first_pix)) * height * fps) if __name__ == "__main__": main(sys.argv[1:])
Code Discussion
The code is fairly straightforward:
- Command line argument parsing.
- Iterate over images converting each one to the DAC representation.
- Write single channel wav file.
The sample rate set in the wav file is based on the resolution of the images and frame rate. A 40x40 pixel image at 50 frames per second (with the sync pulse replacing the first column) needs to be output at 40*40*50 = 80000 Hz.
Text editor powered by tinymce.