Circuit Python adds Python to Microcontrollers

Add rapid prototyping to your rapid prototyping board with Circuit Python or MicroPython

CircuitPython Matrix

Back in 2013, a Kickstarter ran for a project to put a python interpreter on a microcontroller. At the time I could not see the benefit. Cool project, but I asked myself: “why?” On my last Adafruit order, I received a free Circuit Playground Express. The board comes with CircuitPython pre-installed. After playing with Circuit Python, or CP, I finally “get it.”

For Valentine’s Day, I made an animated LED heart for a new love in my life, Circuit Python. Well, love is a bit of a strong word. The past couple of weeks I have been learning Circuit Python, and I am excited by what it offers.

What is Circuit Python?

It is a Python implementation that runs on microcontrollers. The code exists on the microcontroller as text. The interpreter runs the code from that text file. Circuit Python is built on, or based on, MicroPython. Adafruit is designing it to teach programming. It is easy to get started, just open up the code.py file from the auto-mounted drive and start typing. When you hit save, the code runs. That’s it.

What is MicroPython, then?

MicroPython (MP) is the original Python 3 on a microcontroller project. It aims to be as compatible as possible with the “PC version” of Python. By the way, the “regular” version of Python gets called “CPython.”

Micro Python vs. Circuit Python

Okay, so what’s the difference? They are both Python, and they both run on microcontrollers, right?

Circuit Python is a fork of MicroPython, with a slightly different goal. At the core, MP is still there. From various Github and blog posts I found, it appears there is a slight difference in architecture as well as philosophy.

CP appears to have a better abstraction from hardware, allowing code to be more portable between (future) microcontrollers. MicroPython, on the other hand, seems focused on strong CPython compatibility.

The focus of Adafruit’s implementation seems to be the Atmel SAMD21 chips. That is the ARM Cortex-M0 found in the Arduino Zero, and the Adafruit M0 Feathers, which I wrote about previously.

What can you do with Circuit Python?

Circuit Python supports Adafruit’s popular LEDs, sensors, motors, and LCD screens. There are easy to use libraries, ready to go. Even with a minimal amount of Python knowledge, you can make NeoPixel blink patterns, display graphics on an LCD, or read temperature sensors.

Circuit Playground Express

Their Circuit Playground Express board is a fantastic example. It has a sound sensor, light sensor, accelerometer, gyroscope, NeoPixel ring, and touch-sensitive pads. CP supports all of the hardware on the playground express. (This $25 board makes for an excellent platform to programming electronics hardware.)

How can it really be used?

Even if you know how to program, or are comfortable with microcontrollers, Circuit Python, or MicroPython for that matter, is still a valuable tool.

First, do you know Python? If not, learning to blink some LEDs or read sensors is an amusing way to learn the language. While toggling an LED might not get used in a spreadsheet script, knowing Python’s structure is still transferable.

Next, consider Circuit Python, or again MicroPython, a rapid prototyping tool for rapid prototyping tools. The proof-of-concept project I built was the LED matrix, shown below.

Why is Circuit Python better than MicroPython?

Well, it is not better. It is, slightly, different. Like all things in engineering, there are trade-offs. A perfect solution for one application may not be ideal for another. Instead, I can tell you why I like Circuit Python. It is easy. Literally.

If you have a board with Circuit Python already loaded, it will appear on your computer as a disk drive. To run Python code, just create a file called “code.py” on that virtual drive. Whenever you save, CPX will automatically run the file.

That is it! No compiling. No uploading. Just File -> Save.

If your ESP8266 or M0-based board does not have CP yet, follow the installing Circuit Python instructions. It only takes a few minutes to flash the new bootloader with BOSSA.

Code Editor and Serial Monitor

Adafruit recommends using the “mu” editor. If you are new to programming or working with microcontroller boards, mu is a fine editor. However if you, like me, you have other tools in your workflow, no problem. No special tools are necessary to interact with Circuit Python. For example, I use Sublime Text to edit and screen to connect to the serial interface.

Serial Console

In addition to being a virtual disk drive, CP devices also have a virtual serial port. So open up your preferred serial terminal and connect to the virtual COM port.

Drive and REPL

Remember, there is no uploading involved. So it is no problem to keep your serial connection open while modifying the code.

In Python just use print() statements to send information back to the Serial Console. But that is not all. You can interact with your microcontroller directly as well.

Python REPL

Just like with CPython, you can interact directly with the interpreter. Opening the serial connection connects to Circuit Python’s console. If a script is running, just hit CTRL-C to cause a break. Type some commands. CTRL-D resumes the program.

Circuit Python Limitations

Running Python, or any interpreted language, on a microcontroller with tiny bits of RAM, does come with some limitations. RAM limitations and a friendly interpreter prompt reminds me of running BASIC programs on a C64, come to think of it.

Code Size

100 lines of code are only a few K of bytes. The libraries you pull in to support your code also take up space. Adafruit’s “Express” boards have 2 Mbytes of Flash memory. They can easily store your code plus the 350 Kbytes of Python libraries.

Boards like the Trinket or Feather M0 only have 256 K. Much of that is eaten up by the base Circuit Python. It appears you have about 30-40 Kbytes of space available for your code. This Flash limit means you need to manage the libraries a bit. Fortunately, it is as simple as copying libs to and from the “drive.”

RAM

The amount of RAM available becomes a trickier issue. Your code has to fit into RAM along with whatever variables you are using. Until now, I have not found a way to monitor RAM usage. From the documentation, it does appear you will get a memory error when you run out. Keep reading though, because I do not think this is an issue.

Limited Serial Interaction

One of the downsides to being so related to Python is that Circuit Python inherited the same keyboard input behavior. Which means, there is no way to read characters over USB without your code blocking.

There are two workarounds:

  1. You could push buttons for interaction. Or,
  2. Attach a separate USB to Serial adapter. There is an additional UART available, provided you are not using it.

I suspect this may get resolved over time. The pushback today is how to implement such a functionality correctly.

Speed

Remember, you are running interpreted code. It is not going to run nearly as fast as C code. That said, for a microcontroller platform, the performance is surprisingly high. Code execution speed or performance is not a reason to be using Python.

Circuit Python Matrix Animation

My Circuit Python LED Matrix

The first bit of code I wrote, didn’t work. Multiple LEDs were turning on at the same time. I stared at the code for what seemed like hours. (It was probably 30 seconds.) Then I started changing bits of code to see how the matrix’s behavior would change. With each change, I just hit save and immediately saw the result.

At one point, the REPL interface enabled me to drive individual pins manually. That is when I realized, the behavior only made sense if the transistors on the bank select were PNP. Guess what. They were.

Could I have done the same thing in C? Of course. However, it would take several minutes longer. The re-compile and upload time adds up. I am not ashamed to admit maybe even 10 minutes longer with how many changes I made to the Python code.

The Hardware

Here’s the circuit I used to build my heart shaped LED matrix. I wired the LEDs into three banks with a common cathode. The anodes were wired together into six “columns.” The matrix is driven by the M0 Basic Proto Feather. (These do not come with CP pre-installed, you’ll need to re-flash it.)

Python Heart Matrix Schematic

If I had to do the circuit over again, I would have wired the anodes as common. I forgot I was using the SAMD21 as the controller. Its I/O, as configured in CircuitPython, only provides 7 mA source and 10 mA sink. So to drive them, I needed a transistor. USB’s 5 V drives the LEDs, which means a PNP with NPN driver. This decision complicated the design a bit.

Matrix Photo

The Script

Here’s the Circuit Python code for the matrix. It is about 100 lines, with comments. The top section has some declarations (constants), a few global variables, and then some helper functions. Near the bottom, I provide some one-time run instructions, like an Arduino setup(). Then there is a “while True:” loop that runs until you hit CTRL-C or save new code. The expand() function loads a list of lists, which is the LED sequence.

from digitalio import DigitalInOut, Direction, Pull
import board
import time
import sys

######################################################
# Globals / Constants
# M0 Pins connected to the common cathode banks
bankPins = [board.A3,board.A1,board.A2]
bankState = []

# M0 Pins connected to the "columns"
colPins = [board.D13,board.D12,board.D11,board.D10,board.D9,board.D6]
colState = []

# Array that maps LED to bank and col (LED number is index)
# first is bank, second is column
ledMap = [[0,0],[0,1],[0,2],[0,3],[0,4],[0,5],[1,0],[1,1],[1,2],[1,3],[1,4],[1,5],[2,0],[2,1],[2,2],[2,3],[2,4],[2,5]]

######################################################
# Functions 
def fill(sequence):
	global colState
	global bankState

	for seq in sequence:
		# turn on LEDs in this array
		cycleCount = len(sequence)
		while(cycleCount > 0):
			for led in seq:
				bank = ledMap[led][0]
				col = ledMap[led][1]
				
				bankState[bank].value = True
				colState[col].value = True
				# no flicker, but kind of dim
				time.sleep(0.001)
				bankState[bank].value = False
				colState[col].value = False
			cycleCount = cycleCount - 1
	
	# hold last pattern
	cycleCount = 100
	while(cycleCount > 0):
		for led in sequence[len(sequence)-1]:
				bank = ledMap[led][0]
				col = ledMap[led][1]
				
				bankState[bank].value = True
				colState[col].value = True
				time.sleep(0.001)
				bankState[bank].value = False
				colState[col].value = False
		cycleCount = cycleCount - 1
	alloff()
	return

def alloff():
	global colState
	global bankState

	for col in colState:
		col.value = False

	for bank in bankState:
		bank.value = False
	return


def expand(sequence):
	global colState
	global bankState

	for seq in sequence:
		# turn on LEDs in this array
		cycleCount = 10
		while(cycleCount > 1):
			for led in seq:
				bank = ledMap[led][0]
				col = ledMap[led][1]
				
				bankState[bank].value = True
				colState[col].value = True
				time.sleep(0.01)
				bankState[bank].value = False
				colState[col].value = False
			cycleCount = cycleCount - 1
	return

######################################################
# This stuff is like setup() in Arduino
# setup bank pins for OUTPUT and off
for x in range(len(bankPins)):
	bankState.append(DigitalInOut(bankPins[x]))
	bankState[x].direction = Direction.OUTPUT
	bankState[x].value = False

# setup columns for OUTPUT
for x in range(len(colPins)):
	colState.append(DigitalInOut(colPins[x]))
	colState[x].direction = Direction.OUTPUT
	colState[x].value = False

print("And we're off...")

######################################################
# Patterns (I guess this could go in the globals section)
# but I wanted it close to the while() loop, so I could tweak them=
trickle = [[0,0],[1,17],[2,16],[3,15],[4,14],[5,13],[6,12],[7,11],[8,10],[9,9]]
chase = [[0],[1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[11],[12],[13],[14],[15],[16],[17]]
filling = [[0],[0,1,17],[0,1,17,2,16],[0,1,17,2,16,3,15],[0,1,17,2,16,3,15,4,14],[0,1,17,2,16,3,15,4,14,5,13],[0,1,17,2,16,3,15,4,14,5,13,12,6],[0,1,17,2,16,3,15,4,14,5,13,12,6,7,11],[0,1,17,2,16,3,15,4,14,5,13,12,6,7,11,8,10],[0,1,17,2,16,3,15,4,14,5,13,12,6,7,11,8,10,9]]

######################################################
# A while True is like loop() in Arduino
while True:
	print ("Down Trickle")
	expand(trickle)
	
	print ("Chase")
	expand(chase)
	
	print ("Filling")
	fill(filling)

This graphic matches the LEDs on the matrix to the code.

heart matrix numbered

Resources

With CircuitPython being relatively new, it can be a bit tough to find answers to some basic questions. The bits and pieces are all out there, just scattered about a bit. I imagine a few months after this post, that will not be the case. In the meantime, here’s a collection of links I found helpful.

Conclusion

I have had a blast playing with Circuit Python. There is a magic feeling when you hit CTRL-S and your hardware’s behavior changes. The feeling is even more magic when it does what you expect.

If you have an ESP8266 or M0, I highly recommend giving Circuit Python a try. If you do not have an Adafruit board, then go for MicroPython. I think playing with that would be as equally fun.

Long comments, URLs, and code tend to get flagged for spam moderation. No need to resubmit.

Leave a comment

4 thoughts on “Circuit Python adds Python to Microcontrollers

    • The Trinket is an impressive board for $9. I was using one to communicate with my FONA module. (The original post was going to be using CircuitPython to send data to Adafruit IO. But I thought the heart would be better for the 14th.)

    • Yes. The M0 Proto Basic Feather. (I updated the post to include that bit.) The M0 is my new favorite for non-WiFi stuff. (Although to be honest, I think my favorite changes every week.)