Over the past six months I have spent a lot of time porting different libraries to the Robo HAT MM1 board developed by Robotics Masters. This has lead to discovering a lot about these libraries, how they work behind the scenes and most importantly - what to do to add new boards in the future.
This is the second in a series of write-ups I will be doing to help others who wish to port libraries for their boards. Many of the sources of information can be vague or difficult for outsiders to understand. I hope to ‘demystify’ and explain how to achieve a successful port for everyone.
Today, we will be looking at the CircuitPython (and MicroPython) platform. It is a growing community started by Adafruit and is very active. There are currently only a few different board variants - mostly produced by Adafruit - but has gotten a lot of traction lately thanks to new Crowd Supply projects (like the Robo HAT MM1).
What Is CircuitPython?CircuitPython is a special programming language developed by Adafruit that is based on Python but focused purely on writing code for hardware. It has many thousands of libraries for different sensors. It makes programming motors, servos, sensors and other bits easy. CircuitPython doesn’t require any software on your computer - it runs natively on supported boards and appears as a USB drive.
Before You BeginBefore you begin with porting a software library or firmware to your board, you must know a few key points about the technology you are using and be able to answer the questions below.
- What processor are you using?
- What architecture does it use?
- Do I have access to the datasheet for this microprocessor?
- Is there a similar board on the market that uses the same microprocessor?
These are very important. It will impact on many aspects of how you approach the development process.
It is very clear from the CircuitPython documentation that only a few processors are supported or maintained by Adafruit. These include the SAMD21 and SAMD51. CircuitPython is derived from the more commonly used MicroPython which supports many more processors - ESP8266, ESP32, Teensy, PIC and others. You can use the same process for porting to MicroPython as they have the same repository set up.
The datasheet for a microprocessor is absolutely vital to ensure that the board responds as expected when you compile the firmware. Without it, you will not be able to set the correct pin output functions or configure serial ports.
Once you have all the information you need about the processor you are using, you can start to look at the software and modify it to work for your custom board.
OverviewThe hardest part of any project is finding a good starting point. This is no different. When I started looking at CircuitPython and how to port to different boards - there was only a single script in the repository or looking at the existing boards. No formal, written or detailed documentation existed for adding more boards to CircuitPython.
Here is a short summary of what you will have to do:
- Clone repository and initialize sub-repositories
- Copy existing board
- Update pin names
- Update board configuration
- Compile
Each step will be explained in detail below.
For this tutorial, I will be showing you how to create a custom board for SAMD processors. More specifically, the SAMD21G18A - which is the microprocessor used on the Robo HAT MM1 board which I was porting.
I also assume you already have some kind of text editor (nano, TextEdit) and using some UNIX distribution (I was using Ubuntu 16.04.5).
Step 1 - Clone Repository and Set Up EnvironmentThe first step is to grab the CircuitPython repository on GitHub and then create a copy of one of the boards. Choose the correct repository for the language you are porting (I will mention both MicroPython and CircuitPython throughout - just make sure your processor is supported by which one you choose).
Adafruit CircuitPython: https://github.com/adafruit/circuitpython
MicroPython: https://github.com/micropython/micropython
git clone https://github.com/adafruit/circuitpython
cd circuitpython
The second part of this step is to set up your environment for compiling and download all the additional repositories that sit inside of CircuitPython.
Download Sub-modules
git submodule update --init --recursive
Install ARM-Cross Compilers
sudo add-apt-repository ppa:team-gcc-arm-embedded/ppa
sudo apt-get update
sudo apt-get install gcc-arm-embedded
Compile mpy-cross
make -C mpy-cross
ARM-Cross Compiler: Since we are going to be using Ubuntu to compile CircuitPython firmware for the board, we need to install the correct compilers for ARM boards (SAMD boards are ARM based). I am assuming you already have or this command will download all the required compilers for your device.
mpy-cross: This is the MicroPython cross compiler that compiles .py scripts into.mpy scripts that can run on the SAMD boards.
You are now set to build a test board to check your environment is set up correctly. Building boards is a straightforward process once they exist. Let’s assume you are in the circuitpython repository already (that is where we left off above).
cd ports/atmel-samd/
make BOARD=boardname
Replace ‘boardname’ above with the board you want to build. For testing purposes I would highly recommend using the Feather M0 Board. It is a stable, maintained board.
make BOARD=feather_m0_basic
If you can successfully build this board you should see output like this:
This means you are now ready to compile any of the boards in the boards/ folder.
Step 2 - Create new boardThe fastest way to create a new board is to copy and existing one. All existing boards exist in the folder:
circuitpython/ports/atmel-samd/boards/
The best way to determine which board you want to copy is two things:
- Does your new CircuitPython board have SPI Flash Storage?
- What is it closest related to (processor, outputs, functionality)?
The SPI Flash allows for more libraries to be installed. It also changes the configuration quite a bit by having to set up a serial communication interface. The processor and outputs are just to save time when you finally get to modifying the configuration.
For the Robo HAT MM1, it was the closest related to the Feather M0 Express (with Flash) and the Crickit HAT by Adafruit. For the purposes of this tutorial, we are going to copy the Feather M0 Express to start.
Copy Feather_M0_Express and rename the folder to your new board name (robohatmm1).
The easiest way to start modifying the existing CircuitPython board to work with your board is to deal with the files in alphabetical order.
You will see that the directory contains four files:
robohatmm1/
boards.c
mpconfigboard.h
mpconfigboard.mk
pins.c
boards.c: We will not be modifying the file. This is for advanced usage. It defines special functions that are run during board startup, resets and safe mode. Most boards do not use the special functions in this file.
mpconfigboard.h: This file defines some pin configuration settings. In this file the SPI Flash is configured along with the other standard communication pins (I2C, SPI, UART).
mpconfigboard.mk: This file defines the USB settings (what appears to your computer when you connect the board to it), what hardware your board is using (processor, SPI Flash model) and what CircuitPython files to compile into the firmware - more on this later.
pins.c: Contains a large array that defines the names of the pins that appear to the user. The names defined in this file match up with a pin number from the datasheet. This will become more obvious when we talk about this file.
The boards.c file does not need to be touched. This one is complete. So let’s start by modifying the mpconfigboard.h file.
#define MICROPY_HW_BOARD_NAME "Adafruit Feather M0 Express"
#define MICROPY_HW_MCU_NAME "samd21g18"
Board Name: What you want to call your custom board (e.g. Robo HAT MM1)
MCU Name: The microprocessor model that you are using on your custom board. If you are using a SAMD 21G, this is: “samd21g18”.
#define MICROPY_HW_LED_STATUS (&pin_PA17)
#define MICROPY_HW_LED_TX &pin_PA27
#define MICROPY_HW_LED_RX &pin_PB03
#define MICROPY_HW_NEOPIXEL (&pin_PA06)
Status LED: This is the LED that will tell you when the board resets, if there are any errors or any other information you ask it to tell you. You need to give it a pin number from the datasheet that matches where your LED is connected.
LED TX / LED RX: These are the LEDs that are used to tell you when there is a serial data transfer happening. You need to give it a pin number from the datasheet that matches where your LED is connected. These are not required.
NeoPixel: The pin that the NEOPIXEL is connected to on your custom board (if present). CircuitPython is able to send advanced information feedback using a single Neopixel. This is not required unless you are using a NeoPixel for information feedback.
#define SPI_FLASH_BAUDRATE (8000000)
#define SPI_FLASH_MOSI_PIN &pin_PA08
#define SPI_FLASH_MISO_PIN &pin_PA14
#define SPI_FLASH_SCK_PIN &pin_PA09
#define SPI_FLASH_CS_PIN &pin_PA13
Flash: There are a number of flash pins to be configured if you have an SPI Flash module on your board for extra storage. The first is the speed of the flash chip, measured in baud. Some CircuitPython boards limit the speed of the flash to 8 MHz but this optional and dependant on the flash chip you use. The other flash pins are related to the SPI Interface of the chip. You must ensure two things: (1) On the microprocessor you are using a valid SERIAL COMMUNICATION interface (check the datasheet); (2) You match the pin out of your flash chip to the correct pins. On the Robo HAT MM1 and Feather M0 the flash chip is using SERCOM2.
#define CALIBRATE_CRYSTALLESS 1
#define BOARD_HAS_CRYSTAL 0
Crystal: Some boards use only the internal crystal for timing operations. Others have an external crystal. Whether to use the internal or external crystal is dependant on the applications of your board. Most people can manage without it. Check that your board either includes or excludes the external crystal and set accordingly.
// These are pins not to reset.
#define MICROPY_PORT_A (PORT_PA06)
#define MICROPY_PORT_B ( 0 )
#define MICROPY_PORT_C ( 0 )
This section is for pins that you do not want to reset on a reboot. In most cases there will be no pins but in rare cases such as when you have a Neopixel attached for information, you will want to add it to this list.
// If you change this, then make sure to update the linker scripts as well to
// make sure you don't overwrite code.
#define CIRCUITPY_INTERNAL_NVM_SIZE 256
#define BOARD_FLASH_SIZE (0x00040000 - 0x2000 - CIRCUITPY_INTERNAL_NVM_SIZE)
For more advanced configuration. Leave these alone. They set the size of the flash available for CircuitPython to use.
#define DEFAULT_I2C_BUS_SCL (&pin_PA23)
#define DEFAULT_I2C_BUS_SDA (&pin_PA22)
The default I2C bus pins. This will configure the pins specifically to use the hardware I2C bus and CircuitPython will also use these pins when you ask for the I2C bus to be used.
#define DEFAULT_SPI_BUS_SCK (&pin_PB11)
#define DEFAULT_SPI_BUS_MOSI (&pin_PB10)
#define DEFAULT_SPI_BUS_MISO (&pin_PA12)
The default SPI bus pins. This will configure the pins specifically to use the hardware SPI bus and CircuitPython will also use these pins when you ask for the SPI bus to be used.
#define DEFAULT_UART_BUS_RX (&pin_PA11)
#define DEFAULT_UART_BUS_TX (&pin_PA10)
The default Serial bus pins. This will configure the pins specifically to use the hardware Serial bus and CircuitPython will also use these pins when you ask for the Serial bus to be used.
// USB is always used internally so skip the pin objects for it.
#define IGNORE_PIN_PA24 1
#define IGNORE_PIN_PA25 1
Since the SAMD21 has a hardware USB interface, CircuitPython needs to leave the two data pins for USB alone. This will force CircuitPython to ignore them for any other purpose than USB.
There are other parameters that can be used in this file, however, they are optional and you can explore them in your own time. They are for more advanced configuration.
mpconfigboard.mkLD_FILE = boards/samd21x18-bootloader.ld # Arduino Zero
LD_FILE = boards/samd21x18-bootloader-external-flash.ld # Feather M0 Express
LD_FILE = boards/samd21x18-bootloader-external-flash-crystalless.ld # Robo HAT MM1
The Linker file is used for compiling the firmware. It must match your board microprocessor and flash configuration. You can check for what linker files are available inside the boards directory. Only include one of the lines above. I have put a few as examples.
USB_VID = 0x1209
USB_PID = 0x4D43
USB_PRODUCT = "Robo HAT MM1"
USB_MANUFACTURER = "Robotics Masters"
This is where all the USB information about your board is set. Every board should have a unique Vendor ID (VID) and Product ID (PID) combination. You are able to set the product and manufacturer to anything.
# Non-Flash Edition
#INTERNAL_FLASH_FILESYSTEM = 1
#LONGINT_IMPL = NONE
# SPI-Flash Edition
SPI_FLASH_FILESYSTEM = 1
EXTERNAL_FLASH_DEVICE_COUNT = 1
EXTERNAL_FLASH_DEVICES = "W25Q64JV_IQ"
LONGINT_IMPL = MPZ
The SPI Flash hardware is configured here (if present) or if your board does not contain external flash, you can set CircuitPython to use the internal flash of the microprocessor instead. I've had mixed success with these settings - for this reason - I recommend only setting the external count to one and listing the exact flash chip model you are using here. For a full list of supported flash chips you can look at the devices.h file.
CHIP_VARIANT = SAMD21G18A
CHIP_FAMILY = samd21
Set the microprocessor details here again (you should remember by now what you are using).
# Include these Python libraries in firmware.
FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_BusDevice
FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_INA219
FROZEN_MPY_DIRS += $(TOP)/frozen/RoboticsMasters_CircuitPython_MPU9250
FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_NeoPixel
FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_Crickit
FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_Motor
FROZEN_MPY_DIRS += $(TOP)/frozen/Adafruit_CircuitPython_seesaw
Frozen Libraries are very interesting in my experience. They are compiled python libraries that are run from the Flash storage. They do not occupy any program memory when they are loaded - which is good for boards with low memory like the SAMD21. You are able to compile as many (up to how much storage you have) libraries into the CircuitPython firmware. They are optional to include. You must make sure that they are present in the circuitpython/frozen folder before you build the firmware if you include them here.
# Make room for frozen Libraries
CIRCUITPY_DISPLAYIO = 0
CIRCUITPY_FREQUENCYIO = 0
CFLAGS_INLINE_LIMIT = 55
These may be outdated in the new CircuitPython 4.0.0. They used to reduce the amount of space that was taken up by the core CircuitPython library. This was only useful if you did not have an external flash chip.
pins.cThis is the last file we have to modify before going ahead and testing your configuration and building CircuitPython for your custom board. It is the simplest of all the files to customise and follows a set format for all boards.
There is an array that is used to look up the pin names in CircuitPython when you import the board object. See below:
CircuitPython 4.0.0
>>> import board
>>> board.SERVO1
It is highly important that you match the pin number in the pins.c file with that of the name printed on your board. A mismatch here could be problematic. All pin declarations look like this:
{ MP_ROM_QSTR(MP_QSTR_SERVO1), MP_ROM_PTR(&pin_PA16) },
Everything after MP_QSTR_ will be your pin name in CircuitPython. The pin_PA16 in this example is saying that "servo 1 is connected to pin 16 on port A of the microprocessor". Make sure you get this right for all your pins.
Down the bottom you will notice three different objects for I2C, SPI and UART.
{ MP_ROM_QSTR(MP_QSTR_I2C), MP_ROM_PTR(&board_i2c_obj) },
{ MP_ROM_QSTR(MP_QSTR_SPI), MP_ROM_PTR(&board_spi_obj) },
{ MP_ROM_QSTR(MP_QSTR_UART), MP_ROM_PTR(&board_uart_obj) },
Leave these here if you plan on using these interfaces on your custom board.
That is the end of modifying files for the custom board. You should now be able to run a build and see if you can make the CircuitPython firmware.
Step 4 - Compile BoardAdafruit has already written a great explanation on compiling boards on Adafruit Learn if you are looking for more details. Otherwise make sure you are in the circuitpython/ports/atmel-samd folder and run:
make BOARD=robohatmm1
...changing robohatmm1 to the folder you changed all your files in.
If it successfully builds you will get output like so:
The very last step is to upload your new firmware to the board.
Connect the Robo HAT MM1 to your computer's USB port.
Double Press the "Reset" button quickly, the LED should start to pulse and a ROBOBOOT drive will appear on your computer.
Copy the robohatmm1 uf2 firmware file onto the USB.
The board will reset and CircuitPython should be successfully installed. A new USB drive called CIRCUITPY will appear along with a serial port.
Make sure to check out the Adafruit CircuitPython Essentials Guide for using your board.
ConclusionThis has been fun experimenting with Adafruit CircuitPython and building a custom board. It has been a great learning experience!
Be sure to check out the Crowd Supply campaign as well. It ends on June 11 2019.
https://www.crowdsupply.com/robotics-masters/robo-hat-mm1
I hope this helped you or you had fun reading along!
Thanks!
Comments