├── .gitignore ├── bootloader ├── pycorder_m4 │ ├── board.mk │ └── board_config.h └── bootloader-pycorder_m4-v3.7.0-152-g1833c02-dirty.bin ├── images └── board.png ├── circuitpython └── pycorder_m4 │ ├── mpconfigboard.mk │ ├── board.c │ ├── mpconfigboard.h │ └── pins.c └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.s#* 3 | *.b#* 4 | *.l#* 5 | -------------------------------------------------------------------------------- /bootloader/pycorder_m4/board.mk: -------------------------------------------------------------------------------- 1 | CHIP_FAMILY = samd51 2 | CHIP_VARIANT = SAMD51G19A 3 | -------------------------------------------------------------------------------- /images/board.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeycastillo/pycorder/HEAD/images/board.png -------------------------------------------------------------------------------- /bootloader/bootloader-pycorder_m4-v3.7.0-152-g1833c02-dirty.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeycastillo/pycorder/HEAD/bootloader/bootloader-pycorder_m4-v3.7.0-152-g1833c02-dirty.bin -------------------------------------------------------------------------------- /circuitpython/pycorder_m4/mpconfigboard.mk: -------------------------------------------------------------------------------- 1 | USB_VID = 0x239A 2 | USB_PID = 0x8021 3 | USB_PRODUCT = "The Pycorder" 4 | USB_MANUFACTURER = "Oddly Specific Objects" 5 | 6 | CHIP_VARIANT = SAMD51G19A 7 | CHIP_FAMILY = samd51 8 | 9 | QSPI_FLASH_FILESYSTEM = 1 10 | EXTERNAL_FLASH_DEVICE_COUNT = 1 11 | EXTERNAL_FLASH_DEVICES = GD25Q32C 12 | LONGINT_IMPL = MPZ 13 | 14 | CIRCUITPY_AUDIOBUSIO = 0 15 | -------------------------------------------------------------------------------- /circuitpython/pycorder_m4/board.c: -------------------------------------------------------------------------------- 1 | #include "supervisor/board.h" 2 | #include "mpconfigboard.h" 3 | #include "shared-bindings/pwmio/PWMOut.h" 4 | #include "hal/include/hal_gpio.h" 5 | 6 | void board_init(void) { 7 | // set up square wave on PA01 (display EXTCOMIN) 8 | pwmio_pwmout_obj_t pwm; 9 | common_hal_pwmio_pwmout_construct(&pwm, &pin_PA01, 32768, 2, false); 10 | common_hal_pwmio_pwmout_never_reset(&pwm); 11 | 12 | // keep display on at all times (display DISP is PA00) 13 | gpio_set_pin_function(PIN_PA00, GPIO_PIN_FUNCTION_OFF); 14 | gpio_set_pin_direction(PIN_PA00, GPIO_DIRECTION_OUT); 15 | gpio_set_pin_level(PIN_PA00, true); 16 | never_reset_pin_number(PIN_PA00); 17 | } 18 | 19 | bool board_requests_safe_mode(void) { 20 | return false; 21 | } 22 | 23 | void reset_board(void) { 24 | } 25 | -------------------------------------------------------------------------------- /circuitpython/pycorder_m4/mpconfigboard.h: -------------------------------------------------------------------------------- 1 | #define MICROPY_HW_BOARD_NAME "The Pycorder" 2 | #define MICROPY_HW_MCU_NAME "samd51g19" 3 | 4 | #define CIRCUITPY_MCU_FAMILY samd51 5 | 6 | 7 | #define MICROPY_HW_LED_STATUS (&pin_PA06) 8 | 9 | // These are pins not to reset. 10 | // QSPI Data pins 11 | #define MICROPY_PORT_A (PORT_PA08 | PORT_PA09 | PORT_PA10 | PORT_PA11 | PORT_PA31) 12 | // QSPI CS, QSPI SCK 13 | #define MICROPY_PORT_B (PORT_PB10 | PORT_PB11) 14 | #define MICROPY_PORT_C (0) 15 | #define MICROPY_PORT_D (0) 16 | 17 | #define DEFAULT_I2C_BUS_SCL (&pin_PA13) 18 | #define DEFAULT_I2C_BUS_SDA (&pin_PA12) 19 | 20 | #define DEFAULT_SPI_BUS_SCK (&pin_PB02) 21 | #define DEFAULT_SPI_BUS_MOSI (&pin_PB03) 22 | #define DEFAULT_SPI_BUS_MISO (&pin_PB22) 23 | 24 | // USB is always used internally so skip the pin objects for it. 25 | #define IGNORE_PIN_PA24 1 26 | #define IGNORE_PIN_PA25 1 27 | -------------------------------------------------------------------------------- /bootloader/pycorder_m4/board_config.h: -------------------------------------------------------------------------------- 1 | #ifndef BOARD_CONFIG_H 2 | #define BOARD_CONFIG_H 3 | 4 | #define CRYSTALLESS 1 5 | 6 | #define VENDOR_NAME "Oddly Specific Objects" 7 | #define PRODUCT_NAME "The Pycorder" 8 | #define VOLUME_LABEL "PYCOBOOT" 9 | #define INDEX_URL "https://github.com/joeycastillo/pycorder" 10 | #define BOARD_ID "OSO-PYCO-A1-02" 11 | 12 | #define USB_VID 0x239A 13 | #define USB_PID 0x007D 14 | 15 | #define LED_PIN PIN_PA06 16 | 17 | #define BOOT_USART_MODULE SERCOM3 18 | #define BOOT_USART_MASK APBBMASK 19 | #define BOOT_USART_BUS_CLOCK_INDEX MCLK_APBBMASK_SERCOM3 20 | #define BOOT_USART_PAD_SETTINGS UART_RX_PAD3_TX_PAD2 21 | #define BOOT_USART_PAD3 PINMUX_PA19D_SERCOM3_PAD3 22 | #define BOOT_USART_PAD2 PINMUX_PA18D_SERCOM3_PAD2 23 | #define BOOT_USART_PAD1 PINMUX_UNUSED 24 | #define BOOT_USART_PAD0 PINMUX_UNUSED 25 | #define BOOT_GCLK_ID_CORE SERCOM3_GCLK_ID_CORE 26 | #define BOOT_GCLK_ID_SLOW SERCOM3_GCLK_ID_SLOW 27 | 28 | #endif 29 | -------------------------------------------------------------------------------- /circuitpython/pycorder_m4/pins.c: -------------------------------------------------------------------------------- 1 | #include "shared-bindings/board/__init__.h" 2 | 3 | STATIC const mp_rom_map_elem_t board_global_dict_table[] = { 4 | { MP_OBJ_NEW_QSTR(MP_QSTR_A0), MP_ROM_PTR(&pin_PA02) }, 5 | { MP_OBJ_NEW_QSTR(MP_QSTR_A1), MP_ROM_PTR(&pin_PB03) }, 6 | { MP_OBJ_NEW_QSTR(MP_QSTR_SCK), MP_ROM_PTR(&pin_PB03) }, 7 | { MP_OBJ_NEW_QSTR(MP_QSTR_A2), MP_ROM_PTR(&pin_PB02) }, 8 | { MP_OBJ_NEW_QSTR(MP_QSTR_MOSI), MP_ROM_PTR(&pin_PB02) }, 9 | { MP_OBJ_NEW_QSTR(MP_QSTR_D0), MP_ROM_PTR(&pin_PB23) }, 10 | { MP_OBJ_NEW_QSTR(MP_QSTR_CS), MP_ROM_PTR(&pin_PB23) }, 11 | { MP_OBJ_NEW_QSTR(MP_QSTR_D1), MP_ROM_PTR(&pin_PB22) }, 12 | { MP_OBJ_NEW_QSTR(MP_QSTR_MISO), MP_ROM_PTR(&pin_PB22) }, 13 | 14 | { MP_OBJ_NEW_QSTR(MP_QSTR_STEMMA), MP_ROM_PTR(&pin_PA05) }, 15 | 16 | { MP_OBJ_NEW_QSTR(MP_QSTR_LED), MP_ROM_PTR(&pin_PA06) }, 17 | { MP_OBJ_NEW_QSTR(MP_QSTR_EN2), MP_ROM_PTR(&pin_PA04) }, 18 | 19 | { MP_OBJ_NEW_QSTR(MP_QSTR_SDA),MP_ROM_PTR(&pin_PA12) }, 20 | { MP_OBJ_NEW_QSTR(MP_QSTR_SCL),MP_ROM_PTR(&pin_PA13) }, 21 | 22 | { MP_OBJ_NEW_QSTR(MP_QSTR_DISPLAY_DATA),MP_ROM_PTR(&pin_PB08) }, 23 | { MP_OBJ_NEW_QSTR(MP_QSTR_DISPLAY_CLOCK),MP_ROM_PTR(&pin_PB09) }, 24 | { MP_OBJ_NEW_QSTR(MP_QSTR_DISPLAY_CS),MP_ROM_PTR(&pin_PA07) }, 25 | 26 | { MP_OBJ_NEW_QSTR(MP_QSTR_UP),MP_ROM_PTR(&pin_PA17) }, 27 | { MP_OBJ_NEW_QSTR(MP_QSTR_RIGHT),MP_ROM_PTR(&pin_PA16) }, 28 | { MP_OBJ_NEW_QSTR(MP_QSTR_DOWN),MP_ROM_PTR(&pin_PA20) }, 29 | { MP_OBJ_NEW_QSTR(MP_QSTR_LEFT),MP_ROM_PTR(&pin_PA19) }, 30 | { MP_OBJ_NEW_QSTR(MP_QSTR_CENTER),MP_ROM_PTR(&pin_PA18) }, 31 | { MP_OBJ_NEW_QSTR(MP_QSTR_NEXT),MP_ROM_PTR(&pin_PA15) }, 32 | { MP_OBJ_NEW_QSTR(MP_QSTR_PREVIOUS),MP_ROM_PTR(&pin_PA21) }, 33 | 34 | { MP_ROM_QSTR(MP_QSTR_I2C), MP_ROM_PTR(&board_i2c_obj) }, 35 | { MP_ROM_QSTR(MP_QSTR_SPI), MP_ROM_PTR(&board_spi_obj) }, 36 | { MP_ROM_QSTR(MP_QSTR_UART), MP_ROM_PTR(&board_uart_obj) }, 37 | }; 38 | 39 | MP_DEFINE_CONST_DICT(board_module_globals, board_global_dict_table); 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The PyCorder 2 | ============ 3 | 4 | A credit-card-sized, SAM D51-based gadget for sensing, storing and showing data. 5 | 6 | PCB (new link soon)) | [BOM](https://octopart.com/bom-tool/ggGZZjZ3) | [UPDATES](https://twitter.com/josecastillo) 7 | 8 | Features 9 | -------- 10 | 11 | * A distinctly crisp and impressively low power 240x400 Sharp Memory Display 12 | * A SAMD51G19 microcontroller, with plenty of room for [CircuitPython](https://circuitpython.org/)! 13 | * 4 megabyte QSPI Flash; plenty of space for code, fonts and images 14 | * Two buttons and a five-way joystick for user input 15 | * Built-in battery charging, and room for a [350 mAh battery](https://www.adafruit.com/product/4237) 16 | * One STEMMA port with current limiting and voltage protection 17 | * One STEMMA-QT port for plugging in [STEMMA-QT compatible boards](https://www.adafruit.com/category/1018) 18 | * One 9-pin connector, compatible with a veritable menagerie of [Sensor Cap](https://github.com/joeycastillo/Sensor-Cap/tree/main/PCB/Sensor%20Boards) sensor boards 19 | * Enable switch for cutting power to the device 20 | * One amber LED for charge state and one red LED for general purpose blinking 21 | 22 | ![image](/images/board.png) 23 | 24 | Building your own PyCorder 25 | -------------------------- 26 | 27 | You will need to order the PCB's from someplace like OSH Park (I recommend getting it in After Dark), and purchase all the parts on the [PyCorder Bill of Materials](https://octopart.com/bom-tool/ggGZZjZ3). 28 | 29 | The board is hand-solderable, but a word of warning: **this is a relatively advanced build**. The SAM D51 microcontroller is a QFN part, which is best soldered with a hot air reflow station. Aside from that, almost all of the passive components are 0603 in size, which is at the limit of hand-solderability for many folks. A [fine tip](https://www.adafruit.com/product/1249) for your soldering iron is a must, and a [syringe of flux](https://www.adafruit.com/product/2667) couldn't hurt. 30 | 31 | Pin definitions 32 | --------------- 33 | 34 | Unlike a Metro or even a Feather, the PyCorder doesn't have a whole heap of GPIO pins. Having said that, I made sure to choose the pins with the most diverse set of features, opening the door to a whole lot of possibilities. 35 | 36 | The **STEMMA port** is arguably one of the most powerful pins on the microcontroller. You can use it as a 10-bit true analog output, analog input, digital in/out or PWM. Access it in CircuitPython as `board.STEMMA`. 37 | 38 | The **STEMMA-QT port** gives access to the board's I²C pins. Get at the I²C bus with `board.I2C()`, or the pins with `board.SDA` / `board.SCL`. 39 | 40 | The **9-pin connector** can be used in a lot of different ways. From the top of the connector (which is pin 9): 41 | 42 | * Pin 9, or `board.A0`, is the SAMD51's other DAC channel. Use this as an analog output, analog input or digital in/out. 43 | * Pins 8 and 7 are the same I²C pins as the STEMMA-QT port; all I²C devices share the `board.I2C()` bus. 44 | * Pin 6 has two names: `board.A1` and `board.SCK`. It can act as an analog input, PWM or digital in/out. In addition, it can act as the SCK pin for an SPI device on the other end of the 9-pin cable. 45 | * Pin 5 (`board.A2` / `board.MOSI`) can act as an analog input, PWM, digital in/out, and the Main Out / Secondary In for an SPI device. 46 | * Pin 4 (`board.D0` / `board.CS`) cannot support analog input, but you do get PWM and digital in/out. Can act as chip select for an SPI device. 47 | * Pin 3 (`board.D1` / `board.MISO`) is PWM or digital in/out. Can act as the Main In / Secondary Out for an SPI device. 48 | * Pin 2 is ground. 49 | * Pin 1 is a regulated 3.3 volt output from the on-board regulator. 50 | 51 | The board has a **dual-output votage regulator** that can gate power to these three ports separately from the rest of the device. The enable pin for the main regulator output is, of course, tied to the on/off switch and pulled up by default. The second enable pin is tied to CircuitPython's `board.EN2`, and it is *pulled down by default*. This means that to power any device plugged in to these ports, you must do something like this first: 52 | 53 | ``` 54 | en2 = DigitalInOut(board.EN2) 55 | en2.switch_to_output() 56 | en2.value = True 57 | ``` 58 | 59 | The LED is accessible at `board.LED`; you can output a digital signal to turn it on and off, or fade it with PWM. 60 | 61 | Finally: the button inputs. These are easy: they're all active low and tied directly to pins on the SAMD51 (so you should enable internal pull-up resistors to use them). The rundown: 62 | 63 | * The button on the left side of the board is `board.PREVIOUS` 64 | * The button on the right side is `board.NEXT` 65 | * The four cardinal directions of the joystick map to `board.UP`, `board.DOWN`, `board.LEFT`, and `board.RIGHT`. 66 | * You can press in on the joystick as a 'select' type action; detect this action on the `board.CENTER` pin. 67 | 68 | Burning the bootloader 69 | ---------------------- 70 | 71 | You will burn the bootloader over the SWD interface. These pins are available as exposed test points on the right side of the back of the board. To connect to them, you can solder directly to the pads, or use pogo pins; they are 0.1 inch pitch, so you can make a compatible cable using [a 5x1 wire housing](https://www.adafruit.com/product/3145), [raw jumper wires](https://www.adafruit.com/product/3633) and [some pogo pins](https://www.adafruit.com/product/2429). 72 | 73 | For convenience, a prebuilt bootloader is available in this repository. You can burn the bootloader using any SWD debugging tool like a J-Link or Atmel-ICE, but you can also use an [Adafruit Trinket M0](https://www.adafruit.com/product/3500) or [PyRuler](https://www.adafruit.com/product/4319) and the [Adafruit_DAP](https://github.com/adafruit/Adafruit_DAP) library. 74 | 75 | You can also build the bootloader from scratch by copying the `bootloader/pycorder_m4` directory to the `boards` directory of [uf2-samdx1](https://github.com/adafruit/uf2-samdx1) and typing `make BOARD=pycorder_m4`. 76 | 77 | CircuitPython on the PyCorder 78 | ----------------------------- 79 | 80 | A CircuitPython UF2 is not included in this repository, because it's probably better to build your own with the latest greatest features. To get set up, follow the instructions in [Adafruit's guide to building CircuitPython](https://learn.adafruit.com/building-circuitpython?view=all); then copy the `circuitpython/pycorder_m4` directory to the CircuitPython repository's `ports/atmel-samd/boards` directory. 81 | 82 | Navigate to the `ports/atmel-samd` folder in your terminal and type `make BOARD=pycorder_m4`. This will generate a UF2 that you can drag to the PYCOBOOT drive. 83 | 84 | Note that the CircuitPython build does not currently drive the display by default. Whether you intend to display console output or your own UI, you will have to paste some code like this at the beginning of your `code.py` to get the display working: 85 | 86 | ``` 87 | import busio 88 | import framebufferio 89 | import sharpdisplay 90 | displayio.release_displays() 91 | bus = busio.SPI(board.DISPLAY_CLOCK, MOSI=board.DISPLAY_DATA) 92 | framebuffer = sharpdisplay.SharpMemoryFramebuffer(bus, board.DISPLAY_CS, 400, 240) 93 | display = framebufferio.FramebufferDisplay(framebuffer, auto_refresh = True, rotation=270) 94 | ``` 95 | 96 | At this point you can add `display.show(None)` to display the CircuitPython console, or if you wish to show a displayio Group, `display.show(your_group)`. 97 | 98 | Two final notes on the display: first, per the data sheet, the display requires a constant ~1Hz pulse on its EXTCOMIN pin. This reverses the polarity of the LCD, and is necessary for the display's long term stability. The CircuitPython board definition sets this pulse up as a PWM in its `board_init` function. If you wanted to do something like an Arduino core for it, you would be responsible for zatzing the display roughly once a second. EXTCOMIN is tied to the SAMD51's pin PA01, and the TC2 peripheral is ideal for generating this signal. 99 | 100 | Second: the display only turns on when its DISP pin is pulled high, but the PyCorder connects it via a pull-down resistor so that the display defaults to an off state. This is intentional: the goal is to prevent the screen from turning on in the absence of the EXTCOMIN signal and degrading. DISP is tied to the SAMD51's pin PA00, and the `board_init` function pulls it high right after setting up the 1Hz pulse. This means that as long as the board is running, the screen will be on and the EXTCOMIN signal will be going to the screen. 101 | 102 | Schematics and resources 103 | ------------------------ 104 | 105 | TODO 106 | --------------------------------------------------------------------------------