computers

Raspberry Pi Pico “cstdlib: no such file or directory”

This is another one of those “doh” moments, but one that the Internet didn’t seem to help me with, so I ended up spending several hours attempting to work out what was going on.

When building a Raspberry Pi Pico C/C++ SDK project, I’d been through the getting started guide (chapters 2 and 8 are the most relevant) to build the environment and it was all up and running, building the example files…

I’d even managed to get a simple test application up and running. But C is rusty (pun not intended) and my C++ is minimal and I’ve not really done anything with C/C++ on the Pico before…

So then I wanted to build some code that uses the standard C++ library that comes with the compilers, but I was having an issue that sent me on a right “wild goose chase”!

When compiling for an embedded system, there are two compilers involved (at least) – the host compiler that can be used to build the tools you’re going to need; and a cross-compiler that runs on the host but ends up building the code destined for your target board, in this case the Raspberry Pi Pico.

On my Ubuntu system, I have the following gcc executables in my /usr/bin area:

So there is the arm-none-eabi-gcc-* version which is the cross-compiler, and then pretty much everything else eventually links back to the x86_64-linux-gnu-gcc-11 which is the host compiler.

But all of this should be hidden away behind the Raspberry Pi Pico’s cmake based build system. But I was having the following build problem:

Yet my main.c file was pretty simple at this point:

#include <cstdlib>

int main(void)
{
 return 0;
}

So it had to be a build environment problem to be reporting “cstdlib: No such file or directory” – right? At least that is what everything I could find on the Internet (web and forums – it wasn’t much) was saying.

I checked the versions of the standard library installed, and that they were there for the arm-none-eabi architecture required to cross-compile for the Pico. In my case I had the following three installed:

I’m not clear why there are three – but -newlib appears to be just the basic libraries whilst -dev seems to include the header files required to build against them. libnewlib has a whole different set again. I’ll puzzle that out another day (feel free to tell me in the comments if you know!)

But regardless, in general terms these all appeared to match the version of the compilers.

Other things I checked:

  • Viewing the outputs of the build/CMakeFiles area: specifically CMakeError.log and CMakeOutput.log.
  • Checking the various produced Makefiles in both build and build/CMakeFiles.
  • Using cmake –trace to try to work out what some of the inputs to the process were.
  • Using make VERBOSE=1 for the same for make.
  • Went back to the SDK documentation: https://www.raspberrypi.com/documentation/microcontrollers/c_sdk.html
  • Browsing the Raspberry Pi cmake build system in their github: https://github.com/raspberrypi/pico-sdk – it starts off with pico_sdk_init.cmake and then rapidly spreads out from with the cmake subdirectory…

I did notice that as part of the cmake process I appeared to be getting one version of the compiler (10.3.1) but as part of the make process appeared to be logging a different one (11.4.0) which seems to imply one process was using the cross-compiler but the other was finding the native compiler:

Eventually I concluded that this must be because is building one of the tools on the host in order to proceed, but I was grasping at straws a little by now. Again something to figure out another day as it didn’t seem relevant to my issue.

Eventually it dawned on me. I was compiling main.c. A C file. Not a C++ file. Yet I was asking it to include the standard C++ library. Doh.

Sure enough renaming main.c to main.cpp, updating CMakeLists.txt, removing the contents of my build area and re-running cmake and make – yep. All was good. Everything resolved properly and checked out.

Sometimes, it really is something that simple. So I thought I’d write it down in case someone else has the same “simple” problem but all they find to help online are complex deep dives into the innards of the Raspberry Pi Pico’s build system!

Kevin

computers

Buildroot on the Orange Pi One

I’ve been wondering about the Orange Pi boards for some time now. There are quite a few! One thing I’d like to do is find another platform for embedded Linux type applications – possibly even some kind of additional “bare metal” kernel to run on them, so I thought I’d give the Orange Pi boards a go.

There is a huge range of boards, but I wanted to keep to the relatively cheap ones, so in the end I have the following to have a play with, with the following specifications:

Orange Pi OneAllwinner H3 (A7)32-bit1.2GHz1GBHDMI, USB 2, Eth
Orange Pi Zero 3Allwinner H618 (A53)64-bit1.5GHz1GBWi-Fi, uHDMI, USB 2, Eth
Orange Pi 3BRockchip RK3566 (A55)64-bit1.8GHz4GBWi-Fi, HDMI, USB 2/3, Eth

Typically they have official Orange Pi OS (based on Arch), Ubuntu and Android images. The official OS images for the Orange Pi One can be found here.

But I’m interested in options for building code to run with as little additional overheads as possible – I’m certainly not interested in a desktop interface, so to start with I’m looking at buildroot which is a system for scripting the buiding of embedded linux systems.

At the time of writing there is a configuration for the Orange Pi One, but not the others, so I’m starting with that.

Here are some key resources relevant to using buildroot and Orange Pi:

I’ve basically followed the first link, but I’m using the Orange Pi One rather than the Orange Pi Zero. I’d like to see if I can figure out how to do the same for the Orange Pi Zero 3 at some point.

I’ve done everything in an Ubuntu Virtual Machine that is already largely set up for cross-compilation of embedded systems, so things like git and an ARM and aarch64 elf cross-compilation toolchain are already installed.

The full list of pre-requisites can be found in the buildroot user manual’s requirements page. I was missing two things. First, ncurses is required to run the menuconfig system. Installing libncurses-dev solved that one. Then I found that automake was missing. It was complaining about aclocal-1.16 – I’m not entirely sure how these two things related to each other,but didn’t stop to worry about it. Installing automake solved that.

Here are some notes on how buildroot is structured and getting started.

The core board specific areas are as follows:

board/orangepi/orangepione - board specific scripts and notes.  In this case, just the readme.
board/orangepi/common - Orange Pi specific configuration/scripts common to Orange Pi boards.
configs/orangepi* - buildroot configuration files specific to Orange Pi boards
configs/orangepi-one-defconfig - the buildroot configuration for the Orange Pi One

To configure the buildroot system from scratch, requires running ‘make menuconfig’ but if the board is directly supported then we can get a head-start by using the board specific configuration already provided:

$ cd buildroot
$ make orangepi_one_defconfig

I went with the defaults, which configured the system to build the following:

  • ARM (little endian)
  • Cortex-A7
  • EABIhf
  • VFPv4
  • ARM
  • ELF

At this point, which the configuration saved and active, it was just a case of running make and then waiting for a couple of hours for everything to happen. Eventually the file outputs/images/sdcard.img was the result.

$ make
$ cd output/images
$ ls

The SD card image can be written directly to a micro SD card for use in the Orange Pi One. Note, this is a whole-card image, not a specific partition. In my case the SD card appeared as /dev/sdb. Actually, it was auto-mounted as /dev/sdb1…

$ cd buildroot/output/images
$ umount /dev/sdb1
$ sudo dd if=sdcard.img of=/dev/sdb bs=4M status=progress

Then I put the micro SD card into the Orange Pi One and fired it up. After a few seconds the onboard LED came on. The next step was to hook up a serial console using a cheap USB to serial converter. Details can be found on the Orange Pi One User Manual Wiki page here (“How to use the debug serial port”).

I used PuTTY on my Windows computer and a CH340 based USB to serial adaptor that in my case appeared as COM22. When configured for 115200 baud (no flow control) and connected RX to TX, TX to RX, GND, on rebooting the Orange Pi One I was able to login as root over a serial link.

I had a bit of a false start, as the guide I was following, using the Orange Pi Zero, had the GND-RX-TX pins the opposite way round to the Orange Pi One…. but I spotted the issue eventually!

U-Boot SPL 2023.01 (Aug 13 2023 - 01:18:33 -0700)
DRAM: 1024 MiB
Trying to boot from MMC1


U-Boot 2023.01 (Aug 13 2023 - 01:18:33 -0700) Allwinner Technology

CPU:   Allwinner H3 (SUN8I 1680)
Model: Xunlong Orange Pi One
DRAM:  1 GiB
Core:  62 devices, 19 uclasses, devicetree: separate
WDT:   Not starting watchdog@1c20ca0
MMC:   mmc@1c0f000: 0
Loading Environment from FAT... Unable to use mmc 0:1...
In:    serial
Out:   serial
Err:   serial
Net:   eth0: ethernet@1c30000
starting USB...
Bus usb@1c1a000: probe failed, error -2
Bus usb@1c1a400: probe failed, error -2
Bus usb@1c1b000: USB EHCI 1.00
Bus usb@1c1b400: USB OHCI 1.0
scanning bus usb@1c1b000 for devices... 1 USB Device(s) found
scanning bus usb@1c1b400 for devices... 1 USB Device(s) found
       scanning usb for storage devices... 0 Storage Device(s) found
Hit any key to stop autoboot:  0
switch to partitions #0, OK
mmc0 is current device
Scanning mmc 0:1...
Found /boot/extlinux/extlinux.conf
Retrieving file: /boot/extlinux/extlinux.conf
1:      default
Retrieving file: /boot/zImage
append: root=PARTUUID=529a397e-bd6f-4165-a530-6ca3e599877b rootwait console=ttyS0,115200 rootfstype=ext4 quiet panic=10
Retrieving file: /boot/sun8i-h3-orangepi-one.dtb
Kernel image @ 0x42000000 [ 0x000000 - 0x4daa20 ]
## Flattened Device Tree blob at 43000000
   Booting using the fdt blob at 0x43000000
Working FDT set to 43000000
   Loading Device Tree to 49ff7000, end 49fffb67 ... OK
Working FDT set to 49ff7000

Starting kernel ...

[    0.001602] /cpus/cpu@0 missing clock-frequency property
[    0.001633] /cpus/cpu@1 missing clock-frequency property
[    0.001651] /cpus/cpu@2 missing clock-frequency property
[    0.001669] /cpus/cpu@3 missing clock-frequency property
[    0.162419] lima 1c40000.gpu: error -ENODEV: _opp_set_regulators: no regulator (mali) found
Starting syslogd: OK
Starting klogd: OK
Running sysctl: OK
Seeding 256 bits without crediting
Saving 256 bits of non-creditable seed for next boot
Starting network: udhcpc: started, v1.36.1
udhcpc: broadcasting discover
udhcpc: no lease, forking to background
OK

Welcome to Buildroot for the Orange Pi One
OrangePi_One login: root
# cd /
# ls
bin         lib         media       root        tmp
boot        lib32       mnt         run         usr
dev         linuxrc     opt         sbin        var
etc         lost+found  proc        sys
#

Having got this far the next step is to work out how to build a custom application The following resources look like being really useful in this regard.

The other thing I’d like to do is see if I have enough of the bits and pieces now to create a buildroot environment for the Orange Pi Zero 3.

I’m also interested in getting as overheads down to an absolute minimum, even to the point of going “bare metal”. At the very least I’d like to investigate the RTAI extensions to Linux.

Kevin

computers

Summary of ATmega (AVR) Timers

I keep coming back to AVR timers, particularly when messing around with Arduino boards based on the ATmega328P.  But I’ve also looked at the ATmega2560, ATmega32U4 and more recently the ATmega4809, but keep forgetting what is what in terms of timers!

The main references are the datasheets (of course) for each MCU, but there are also a few other documents that are quite useful too, the main one being the AVR note 130 “Setup and use of AVR timers”.  But it still doesn’t actually list which timers are available on which devices – we have to dig into the datasheets for that.

As far as I can tell, there are essentially three different types of timer that will be encountered on the above devices:

  • TC0 – Timer 0 – “8-bit Timer/Counter0 with PWM”
  • TC1 – Timer 1 – “16-bit Timer/Counter1 with PWM”
  • TC2 – Timer 2 – “8-bit Timer/Counter2 with PWM and Asynchronous Operation”

When there are additional timers, they are usually extra instances of the 16-bit timer.

Here is a list of the timers available for the common AVR microcontrollers I tend to play with.

  TC0 TC1 TC2 Others
ATmega328P 1 (TC0) 1 (TC1) 1 (TC2)  
ATmega328PB 1 (TC0) 3 (TC1,3,4) 1 (TC2)  
ATmega2560 1 (TC0) 4 (TC1,3,4,5) 1 (TC2)  
ATmega32U4 1 (TC0) 2 (TC1,3)   1 (TC4*)

 

(*) The ATmega32u4 also has a Timer 4, but it is not a typical 16-bit timer in the same sense as the others described here, it is a “10-bit High Speed” timer/counter with quite different characteristics.  I talk more about that here: Getting CTC Mode on Timer 4 on an ATmega32U4.

The ATmega4809 is completely different again, having “Type A” and “Type B” timers.  More on that here: Arduino Nano Every Timers and PWM.

For completeness, I’ve also talked a little about timers on SAMD 32-bit MCUs too here: Comparing Timers on SAMD21 and SAMD51 Microcontrollers.

The Arduino Uno and Nano use the ATmega328P (not the 328PB) so have three timers available.

Kevin

 

computers · maker

Unbricking a Seeed Xiao Samd21

I’ve had a few Seeed Xiao Samd boards (previously called Seeeduino Xiao) kicking around so decided to finally have a bit of a play with them and the Arduino environment.

To get up and running requires the Xiao Samd core which can be added to the Arduino board manager using the following URL and then installed in the usual way: https://files.seeedstudio.com/arduino/package_seeeduino_boards_index.json

This is all detailed in the Seeed wiki here: https://wiki.seeedstudio.com/Seeeduino-XIAO/.

I have found uploading sketches to the Xiao as it comes “out of the box” to be somewhat “hit and miss”.  It would appear that getting the timing of the reset of the board right compared to the upload action seems quite critical.  Sometimes hitting reset twice to get the board into “bootloader” mode (i.e. it appears as a disk waiting for a UF2 file) and then resetting back again may work.  Yet triggering a reset at all is very fiddly, requiring the temporary connecting of two pads on the board itself.

However this is very common on any microcontroller that does not have a separate “USB co-processor” to handle the USB communications. It is the same issue one gets with the ATmega32U4 based devices – essentially the main processor has to handle the USB stack as well as the application code and if something goes wrong, then USB communications is lost too.  There is a brilliant description of the issue here in section 6: “Uploading/Bootloader/COM Port/Reset Problem”: https://sigmdel.ca/michel/ha/xiao/seeeduino_xiao_01_en.html#download_problem

Some of this might be related to the fact that the serial port presented in bootloader mode is a different serial port to that presented when running firmware.  This is common to other Samd21 devices too, so this might be worth trying: Adafruit Trinket M0 Moving COM Ports in Arduino.  If you are having problems, then it might be worth rebooting into bootloader mode, which you know is “listening” to the COM port, but the Arduino environment won’t “know the difference” as it were…

But one thing I have found is that it is also possible to “brick” the Xiao such that it no longer responds to reset, doesn’t illuminate the user LED and doesn’t even appear as a COM port any more at all.  Basically the bootloader won’t kick in at all.  The only way back from this seems to be to reprogramme the bootloader itself, which requires the use of the SWD pads on the underside of the board.

Prior to fearing the worst however and getting into the magic of SWD there are two things to double check.

Make sure the RESET switch is really working!

If you’re using the Xiao expansion board (which is really handy, giving access to a RESET button and SWD as well as the IO breakouts) just check the Xiao hasn’t come a little loose from the expansion board.  The RESET pin connects via “pogo pins” and if the Xiao pulls out even slightly, then it might not connect!

Seriously, this has caught me out several times as when plugged in via USB the tension in the lead can slowly lift the Xiao out of the expansion board!  Simply pushing the Xiao back in might get the RESET switch working again.

Merge the bootloader and user COM ports

Try the “moving COM port” trick mentioned above.  After doing this, then I’ve found uploading to be a lot more reliable by putting the Xiao into bootloader mode prior to an Arduino upload.  In fact this is now my preferred method to upload.

In summary – to get the most of the Xiao, I recommend both getting an expansion board (or wiring up a RESET button yourself) and merging the COM ports as described here: Adafruit Trinket M0 Moving COM Ports in Arduino.  Then I’ve found the following sequence pretty reliable:

  • Always “double click” the RESET button to put the Xiao into bootloader mode.
  • Then upload using the Arduino IDE.
  • RESET again to run the code if it hasn’t started automatically.

The Last Resort – “Unbricking” the Xiao

So based on experiences so far I can strongly recommend that if you want to get into using a Xiao with the Arduino you get hold of two things:

  • The Xiao expansion board.  This gives you a proper reset button and jumper header pin access to the SWD port.
  • A cheap STlink V2 debug connector.  This gives you a means to programme the board if all else fails and will be required to reload the bootloader if things really break.

STLinkV2-XIAO - Devices

Reprogramming the Bootloader with STLink V2

This is one of those things where all the information is “out there” somewhere, but nothing is quite all in the same place for this specific activity.  So here is a “notes to self” on how to get from nothing to being able to re-programme the Xiao bootloader using an STlink V2 from a Windows PC.

Prerequesites

Configuring OpenOCD For the XIAO SAMD21

The detailed instructions for this were obtained from the Seeed forum here: https://forum.seeedstudio.com/t/how-to-unbrick-a-dead-xiao-using-st-link-and-openocd/255562 using additional information from an Adafruit tutorial here: https://learn.adafruit.com/programming-microcontrollers-using-openocd-on-raspberry-pi/wiring-and-test.

As detailed in the forum post, there is a configuration that is already pretty well ready for the XIAO SAMD21.  This can be found in the directory:

<OpenOCD Install>/share/openocd/script/target/

Copy the file at91samdXX.cfg to XIAO_at91samdXX.cfg and change the following line this is line 35 in my file):

# set _CPUTAPID 0x4ba00477 <-- OLD
set _CPUTAPID 0x0bc11477

Preparing the Bootloader Configuration for the XIAO SAMD21

The key steps are as follows:

  • Create a convenient directory somewhere (e.g. c:\XIAO_bootloader as suggested in the forum post).
  • Obtain the bootloader binary from the Seeed GitHub here: https://github.com/Seeed-Studio/ArduinoCore-samd/tree/master/bootloaders/XIAOM0.
      • It recommends (and I used) the non-update file: bootloader-XIAO_m0-v3.7.0-33-g90ff611-dirty.bin which has to be downloaded “raw” and saved to the convenient directory.
  • Create a XIAO_openocd.cfg file in the convenient directory based on the information from the Adafruit article.
      • Note that there are two parts to the configuration file and each is listed separately in the article.
      • Then make the changes as suggested in the forum post.

My XIAO_openocd.cfg file ended up looking like this:

# File obtained from: https://learn.adafruit.com/programming-microcontrollers-using-openocd-on-raspberry-pi/wiring-and-test
# Modified according to: https://forum.seeedstudio.com/t/how-to-unbrick-a-dead-xiao-using-st-link-and-openocd/255562

source [find interface/stlink.cfg]
transport select hla_swd

set CHIPNAME at91samd21g18
source [find target/XIAO_at91samdXX.cfg]

# did not yet manage to make a working setup using srst
#reset_config srst_only
reset_config srst_nogate

adapter_nsrst_delay 100
adapter_nsrst_assert_width 100

init
targets
reset halt

# Extra code for burning (from later in the article)
init
targets
reset halt
at91samd bootloader 0
#program samd21_sam_ba verify
# Bootloader file obtained from: https://github.com/Seeed-Studio/ArduinoCore-samd/tree/master/bootloaders/XIAOM0
program bootloader-XIAO_m0-v3.7.0-33-g90ff611-dirty.bin verify
at91samd bootloader 8192
reset
shutdown

Connecting to the Xiao and Programming

This is where the expander module is really handy as the SWD pins are broken out to pin headers.  Without this they are just pads on the underside of the Xiao.

The following connections are required:

  • SWDIO
  • SWCLK
  • GND

I’ve powered my Xiao via USB whilst programming, so no 3V3 connection was required.  The SWD pins can be found on the far right hand side of the expander board (assuming the Xiao is on the far left) and need linking to the STLink as shown below.

STLinkV2-XIAO - Links

Once connected to each other, and assuming the STLink driver is installed, then the Xiao can be powered up (via USB) and then the ST Link connected to USB and powered up.

To programme the bootloader requires the following command to be run from the working directory containing the configuration and bootloader binary:

openocd -f XIAO_openocd.cfg

You will probably have to specify the path to the openocd executable.  Here is the complete command running for me.

C:\XIAO_bootloader>C:\OpenOCD-20230202-0.12.0\bin\openocd -f XIAO_openocd.cfg
Open On-Chip Debugger 0.12.0 (2023-02-02) [https://github.com/sysprogs/openocd]
Licensed under GNU GPL v2
libusb1 09e75e98b4d9ea7909e8837b7a3f00dda4589dc3
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
DEPRECATED! use 'adapter srst delay' not 'adapter_nsrst_delay'
DEPRECATED! use 'adapter srst pulse_width' not 'adapter_nsrst_assert_width'
Info : clock speed 400 kHz
Info : STLINK V2J29S7 (API v2) VID:PID 0483:3748
Info : Target voltage: 3.160938
Info : [at91samd21g18.cpu] Cortex-M0+ r0p1 processor detected
Info : [at91samd21g18.cpu] target has 4 breakpoints, 2 watchpoints
Info : starting gdb server for at91samd21g18.cpu on 3333
Info : Listening on port 3333 for gdb connections
[at91samd21g18.cpu] halted due to debug-request, current mode: Thread
xPSR: 0xb1000000 pc: 0xfffffffe msp: 0xfffffffc
[at91samd21g18.cpu] halted due to debug-request, current mode: Thread
xPSR: 0xb1000000 pc: 0xfffffffe msp: 0xfffffffc
[at91samd21g18.cpu] halted due to debug-request, current mode: Thread
xPSR: 0xb1000000 pc: 0xfffffffe msp: 0xfffffffc
** Programming Started **
Info : SAMD MCU: SAMD21G18A (256KB Flash, 32KB RAM)
** Programming Finished **
** Verify Started **
** Verified OK **
shutdown command invoked

C:\XIAO_bootloader>

After which the two devices can be powered off and the STLink SWD connection removed and the Xiao should be usable as before.

IMG_7037

Other Notes

There is an option within the Arduino IDE to select “J-Link over Openocd” as the programmable for the Xiao and then a “burn bootloader” option, but I don’t have a Segger J-link to try this.

References

Kevin

computers

Emulating a Sony Stereo Remote Control

I have a few Sony stereos about the place that largely (these days) act as amplification for something or other.  One I use with a TV, using it’s AUX setting to feed in an audio input.  The only problem is that there is no way to select the AUX input from the front panel – it has to be done with a remote control. But the remote that came with it doesn’t have an option for AUX either – it has CD, tape or tuner, but no AUX.  The only way to get to the AUX setting is using the “function” button which cycles between CD, tape, tuner and AUX!

But I’ve now lost the remote (and the spare remote I was saving “just in case” and the spare, spare turns out only works with power, CD and volume, not “function”) and someone changed the mode to CD and there is no way to get it back onto AUX!  I have a few other remote controls for Sony stereos and they have various combinations of CD, tape, tuner, MD, second tape deck, and so on.  But it seems that only the power, CD and volume controls are common, so no escape there, and second hand ones online seem to be going for upwards of £10!

So I turned to an Arduino, an infrared LED and the following websites to help:

And the Arduino IRemote library from Ken Shirrif: https://github.com/Arduino-IRremote/Arduino-IRremote.

The problem is that the usual way to use an Arduino and the IRemote library is to first get it to “dump” the IR parameters for your controller by using a LED receiver and your existing remote control.  But I’ve lost mine, so no joy there.

As I say, I did have some similar remotes and wondered if sampling the selection buttons from them might lead to an obvious omission that I could infer was the AUX setting.  So using the “ReceiveDump” sample application and the following circuit I was able to grab the magic numbers from some of the remotes I still have.

Arduino IR RX_bb

WARNING: Check the pinout for the IR receiver you have, there are several different varieties! I’ve found some with the pinouts reversed to that shown above.

This gives an output over the serial link like the following:

Protocol=Sony Address=0x10 Command=0x25 Raw-Data=0x825 12 bits LSB first

Send with: IrSender.sendSony(0x10, 0x25, <numberOfRepeats>);

Raw result in internal ticks (50 us) - with leading gap
rawData[26]: 
-65535
+47,-13
+22,-13 +11,-12 +23,-13 +11,-12
+11,-13 +23,-13 +10,-13 +11,-13
+11,-12 +11,-13 +11,-13 +23
Sum: 378
Raw result in microseconds - with leading gap
rawData[26]: 
-3276750
+2350,- 650
+1100,- 650 + 550,- 600 +1150,- 650 + 550,- 600
+ 550,- 650 +1150,- 650 + 500,- 650 + 550,- 650
+ 550,- 600 + 550,- 650 + 550,- 650 +1150
Sum: 18900

Result as internal ticks (50 us) array - compensated with MARK_EXCESS_MICROS=20
uint8_t rawTicks[25] = {47,13, 22,13, 11,12, 23,13, 11,12, 11,13, 23,13, 10,13, 11,13, 11,12, 11,13, 11,13, 23}; // Protocol=Sony Address=0x10 Command=0x25 Raw-Data=0x825 12 bits LSB first

Result as microseconds array - compensated with MARK_EXCESS_MICROS=20
uint16_t rawData[25] = {2330,670, 1080,670, 530,620, 1130,670, 530,620, 530,670, 1130,670, 480,670, 530,670, 530,620, 530,670, 530,670, 1130}; // Protocol=Sony Address=0x10 Command=0x25 Raw-Data=0x825 12 bits LSB first

uint16_t address = 0x10;
uint16_t command = 0x25;
uint32_t data = 0x825;

Pronto Hex as string
char prontoData[] = "0000 006D 000D 0000 005B 0018 002B 0018 0016 0016 002D 0018 0016 0016 0016 0018 002D 0018 0014 0018 0016 0018 0016 0016 0016 0018 0016 0018 002D 06C3 ";

The most useful bits are the address and command portions.  These relate to the two values required to be passed back into the Arduino IRemote library sendSony() call to send that command back out over the IR LED (also shown in the circuit above).

Unfortunately there was no simple sequence of commands being detected for which I could infer some missing values!  Here is a selection of the values I was receiving for the various “CD”, “Tape”, “Tuner”, “MD”, “Aux”, and similar buttons on a RM-AMU179 and RM-SR5 remote:

Address Command   Raw-Data
 0x10    0x25      0x825    - CD
0x410    0x17     0x20817   - USB
 0x90    0x29      0x4829   - DAB
 0x10    0x18      0x818    - FM
 0x10    0x6E      0x86E    - Audio IN
 0x11    0x32      0x8B2    - CD
 0xD     0xF       0x68F    - Tuner
 0xE     0X74      0x774    - Deck A
 0xE     0x34      0x734    - Deck B
 0xF     0x2A      0x7AA    - MD

I can’t spot a pattern!  And looking more closely at the following site: http://www.hifi-remote.com/sony/Sony_rcvr.htm we can see that the selection options are pretty much “all over the place”.  I did have one “brute force” attempt at sending a selection of potentially useful looking candidates over in a loop:

for (int i=29; i<38; i++)
{
  IrSender.sendSony(0x10, i, 3, 12);
  delay(1000);
}

for (int i=64; i<74; i++)
{
  IrSender.sendSony(0x10, i, 3, 12);
  delay(1000);
}

for (int i=104; i<108; i++)
{
  IrSender.sendSony(0x10, i, 3, 12);
  delay(1000);
}

But all I could make it recognise were Tape (0x10, 0x23 or 35) and CD (0x10, 0x25 or 37).  So, over to “plan B”.

The Linux IR Control (LIRC) project has a really extensive list of IR codes for controllers, and I was able to work out that my stereo uses the Sony RM-SC1 remote control. I wasn’t able to find the setting for the AUX input (although with time that still might be possible), but I was able to find the magic numbers that need to be sent to enable the “function” button to allow me to cycle between CD, tape, tuner and AUX.

From: https://lirc.sourceforge.net/remotes/sony/RM-SC1 I can see that the “function” button is described as follows:

begin remote

  name  Sony_RM-SC1_2
  bits            7
  flags SPACE_ENC|CONST_LENGTH
  eps            30
  aeps          100

  header       2450   500
  one          1260   541
  zero          650   541
  post_data_bits  8
  post_data      0x9
  gap          44783
  toggle_bit_mask 0x0

      begin codes
          function                 0x4B
          eq                       0x63
      end codes

end remote

So this is a 7-bit command with an 8-bit “post data” fixed field containing the value 0x9, which means the code required to activate it is constructed as follows:

<7-bit command> <8-bit post-data fixed value>

So this is one of the 15-bit Sony values (Sony also uses 12 and 20-bit values).

0x4B . 0x09 -> 100 1011 0000 1001

The trick is working out how to turn this into values to put into the Arduino IRemote library, which requires a “command” and an “address” for the Sony protocol.  You can read more about how to interpret these values in Ken Shirrif’s post here.

The bits as listed in the LIRC database should be read as values from right to left.  There are three formats of Sony protocol (see: https://www.sbprojects.net/knowledge/ir/sirc.php):

  • 12 bits = <7 bit command> <5 bit address>
  • 15 bits = <7 bit command> <8 bit address>
  • 20 bits = <7 bit command> <8 bits extended address> < 5 bits address>

So for our 15 bit value, we need to read the last 8 bits “back to front” as an 8-bit address:

0000 1001 -> 1001 0000 = 0x90

Then the final 7 bits are the command:

100 1011 -> 110 1001 = 0x69

This means that the call into the Arduino IRemote library we need is as follows:

 IrSender.sendSony(0x90, 0x69, 3, SIRCS_15_PROTOCOL);

Note that we need to specify we’re using the 15-bit protocol.  Cross-referencing with http://www.hifi-remote.com/sony/Sony_rcvr.htm we can look up 0x90 (144) and 0x69 (105) but that is listed as “Input +/Right” so I would never have guessed to try that from my “brute force” approach!

The easiest way to “hack” this in is to take the IRemote SimpleSender example program and place this line at the start of the main loop() function, followed by an immediate return. I included a delay and flashed the LED to show it was working.

void loop() {
  IrSender.sendSony(0x90, 0x69, 3, SIRCS_15_PROTOCOL);
  delay(200);
  digitalWrite(LED_BUILTIN, HIGH);
  delay(200);
  digitalWrite(LED_BUILTIN, LOW);
  delay(2000);

  return;

  // rest of original loop() function

When used with an InfraRed diode on D2 (with a suitable current limiting resistor), this should result in sending the “function” button press every two seconds.

Arduino IR TX_bb

My IR LED has the following properties:

  • 5mm IR LED (940nm)
  • Vf = 1.2-1.4V
  • If = 100mA

But the Arduino’s limit is 40mA (max) or 20mA (ideal) for an IO pin to drive an LED directly.

  • So @40mA/5V -> 95Ω
  • @20mA/5V -> 190Ω

I used a 220Ω resistor as I had some spare, which means I’m running with a current of I = V/R = (5 – 1.2) / 220 ~ 17mA.

Just for completeness, looking back at my original trace, we can see that some of the values I detected used a 12-bit command at address 0x10, for example:

Protocol=Sony Address=0x10 Command=0x25 Raw-Data=0x825 12 bits LSB first

Mapping this onto the values from http://www.hifi-remote.com/sony/Sony_rcvr.htm, we can see that address 0x10 is a “Sony 16” device (0x10 = 16 in decimal), and that command 0x25 (37 decimal) is “CD, CD/SACD, Aux1”.  So again for completeness this maps as follows:

  • It is a 12-bit command, so has a 5-bit address and 7-bit command.
  • The “raw” value listed is 0x825 which is 1000 0010 0101, giving an address of 1 0000 (0x10 or 16), and a command of 010 0101 (0x25 or 37).
  • Looking in the LIRC data dump for the “Select CD command” we can see the following:
begin remote

  name  Sony_RM-SC1_1
  bits           12
  flags SPACE_ENC|CONST_LENGTH
  eps            30
  aeps          100

  header       2476   491
  one          1270   532
  zero          659   532
  gap          44783
  toggle_bit_mask 0x0

      begin codes
          KEY_SLEEP                0x061                     #  Was: sleep
          KEY_POWER                0xA81                     #  Was: power
          display                  0xD21
          clock_timer_select       0x461
          clock_timer_set          0xA61
          tuner_memory             0x701
          KEY_TAPE                 0xC41                     #  Was: tape
          KEY_CD                   0xA41                     #  Was: cd
          tuner-band               0xF01
          KEY_VOLUMEUP             0x481                     #  Was: vol_plus
          KEY_VOLUMEDOWN           0xC81                     #  Was: vol_minus
      end codes

end remote

KEY_CD is 0xA41 or 1010 0100 0001.  Decoding this “backwards” using the 12-bit format <7-bit command> <5-bit address> gives:

  • [ 1010 010 ][ 0 0001 ] -> [ 1000 0 ][ 010 0101 ]
  • Address = 00001 -> 10000 = 0x10 (16)
  • Command = 101 0010 -> 010 0101 = 0x25 (37)

So the following call would trigger this command (and it does):

 IrSender.sendSony(0x10, 0x25, 3);

Note that a bit length of 12 is assumed if none is specified.

So back to the original problem. I need to use “function” several times to select through CD – tape – tuner – AUX, so I ended up just letting the above code run until the AUX setting could be seen on the display, then I turned off the Arduino!

Kevin

computers

Micropython on the CH32V307 RISC-V MPU

I’ve been interested in finding out a little more about the RISC-V architecture, so when I found out there was an experimental Micropython port for the CH32V307 running on WCH’s evaluation board, I thought I’d get one and have a go.

This is very early days, as I’m not really a python person (but I dabble), I don’t really know the Micropython code much, and I currently know very little about RISC-V, but this is how I got to the point of having Micropython running on the board.

I’m using Ubuntu 22.04 LTS in a virtual machine.

References:

The Evaluation Board

CH32V307V-EVT-R1

My board looks like the above.  The core evaluation board has the following features:

  • Uses the WCH CH32V107 microcontroller.
  • Ethernet.
  • USB 2.0 high speed (HS/480Mbps) connection (with USB-C physical port) for host, device and OTG.
  • USB 2.0 full speed (FS/12Mbps) connection (with USB-C physical port) for host, device and power.
  • 8 UARTs, 2 I2C, 3 SPI, 2 I2S.
  • ADC, DAC and TKey (touch key) modules. 
  • A reset and user button.
  • Arduino Uno style headers.
  • A huge number of GPIO pins!

The board also has two boot jumpers (BOOT1 and BOOT0) that are connected to ground.  According to the data sheet, these control the boot mode: boot from program flash; system memory; or internal SRAM.  When BOOT1 is pulled LOW, the state of BOOT0 doesn’t matter and the chip will boot from program flash memory.

You can also see that the above board has two sections.  The larger part is the main board with the above features.  The smaller board is a WCH-Link board that allows you to program and control the main board.  The WCH-Link has the following features:

  • USB-C link for power and communications (I’m not sure if this is another USB 2.0 in USB-C format, or “real” USB-C).
  • A slider ON/OFF switch which controls power to the main board.

I believe the set of jumpers (top right in the picture) controls the link between the WCH-Link and the main board.  Mine is configured as shown above so that:

  • RX0 <-> TX
  • TX0 <-> RX
  • CLK0 <-> CLK
  • DIO0 <-> DIO

Note that the following are not connected via jumpers: 5V, GND, VCC but it looks like they are connected via PCB traces regardless.

It looks like the four signal traces connect to the main board via the top (in the photo) join, along with VCC.  I believe 5V travels from the switch to the main board via the bottom (in the photo) join.  I think there are common GND planes straddling both joins top and bottom.

The CH32V307 Microcontroller

CH32V307VCT6

The CH32V307 microcontroller from WCH is based on the QingKe RISC-V 32-bit architecture using the V4F core which supports single precision floating point operations and can run at 144MHz.  The 307 has 256K flash and 64K SRAM and a wealth of connectivity.  The 307 is called the “interconnectivity device” of the 303/305/307 family.

For more details, see the CH32V303_305_307 datasheet.

Getting to the Point of Running Micropython

Installing the WCH Toolchain

The toolchain is provided by MounRiver and there are versions for Windows, Mac and Linux.

Ubuntu instructions

Download the MounRiver toolset .tar.xz from http://www.mounriver.com/ and unpack into a local directory (e.g. wch/).  Then there is an additional step to install some additional libraries and rules for the devices in the “beforeinstall” directory.

$ cd wch/MRS_Toolchain_Linux_x64...
$ mv 'RISC-V Embedded GCC' RISC-V_Embedded_GCC
$ cd beforeinstall
$ ./start.sh

Note that part of this involved renaming the toolchain directory so that it doesn’t have spaces in its name!  Also, running start.sh will involve several sudo commands so expect to enter your password if you’ve not already used sudo in this session.

Grabbing and Building Micropython

There is a fork of Micropython that contains a fork for the WCH series microcontrollers which can be found here: https://github.com/r4d10n/micropython-wch-ch32v307/.  There is a readme for the WCH port in the ports/wch directory which has most of the steps required to follow to build Micropython but for someone like me having not really build Micropython before I needed to spell things out a little more, so here is the complete sequence for me.

This is starting in an area where you wish to build the source.  For me, this is ~/src and will end up with all the code in ~src/micropython-wch-ch32v307.

$ git clone github.com:r4d10n/micropython-wch-ch32v307.git
$ cd micropython-wch-ch32v307
$ make -C mpy-cross

Now I need to edit ports/wch/Makefile to set the paths to the toolchain:

TOOLCHAIN_ROOT = ~/src/wch/MRS_Toolchain_Linux_x64_V1.60/RISC-V_Embedded_GCC/bin/
OPENOCD_ROOT = ~/src/wch/MRS_Toolchain_Linux_x64_V1.60/OpenOCD/bin/

And now, assuming that the “beforeinstall/start.sh” script was run successfully, I can now build Micropython for the WCH.

$ cd ports/wch
$ make -j`nproc`

Again, assuming start.sh has done its thing and set all the right device permissions, then it should just be a case of connecting the board (assuming the jumper configuration as described previously), ensuring the switch is such that the main board is powered on, and then make has all the rules required to program the board with a simple “make prog”.

This is what it looked like for me…

kevin@ubuntu:~/src/micropython-wch-ch32v307/ports/wch$ make prog
Use make V=1 or set BUILD_VERBOSE in your environment to increase build verbosity.
~/src/wch/MRS_Toolchain_Linux_x64_V1.60/OpenOCD/bin/openocd -f ~/src/wch/MRS_Toolchain_Linux_x64_V1.60/OpenOCD/bin/wch-riscv.cfg -c init -c halt -c "flash erase_sector wch_riscv 0 last " -c exit
Open On-Chip Debugger 0.11.0+dev-02215-gcc0ecfb6d-dirty (2022-10-10-10:35)
Licensed under GNU GPL v2
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
Info : only one transport option; autoselect 'jtag'
Ready for Remote Connections
Info : WCH-Link-CH549 mod:RV version 2.7
Info : wlink_init ok
Info : This adapter doesn't support configurable speed
Info : JTAG tap: riscv.cpu tap/device found: 0x00000001 (mfg: 0x000 (<invalid>), part: 0x0000, ver: 0x0)
Warn : Bypassing JTAG setup events due to errors
Info : [riscv.cpu.0] datacount=2 progbufsize=8
Info : Examined RISC-V core; found 1 harts
Info : hart 0: XLEN=32, misa=0x40901125
[riscv.cpu.0] Target successfully examined.
Info : starting gdb server for riscv.cpu.0 on 3333
Info : Listening on port 3333 for gdb connections
Info : device id = 0x26624f72
Info : flash size = 288kbytes
Info : ROM 256 kbytes RAM 64 kbytes
erased sectors 0 through 287 on flash bank 0 in 0.162391s

#~/src/wch/MRS_Toolchain_Linux_x64_V1.60/OpenOCD/bin/openocd -f ~/src/wch/MRS_Toolchain_Linux_x64_V1.60/OpenOCD/bin/wch-riscv.cfg -c init -c halt -c "program build/CH32V307EVT.elf 0x08000000" -c exit # OpenOCD-wch 0.10 (MRS Toolchain v1.30)
~/src/wch/MRS_Toolchain_Linux_x64_V1.60/OpenOCD/bin/openocd -f ~/src/wch/MRS_Toolchain_Linux_x64_V1.60/OpenOCD/bin/wch-riscv.cfg -c init -c halt -c "program build/CH32V307EVT.elf" -c exit # OpenOCD-wch 0.11 (MRS Toolchain v1.40)
Open On-Chip Debugger 0.11.0+dev-02215-gcc0ecfb6d-dirty (2022-10-10-10:35)
Licensed under GNU GPL v2
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
Info : only one transport option; autoselect 'jtag'
Ready for Remote Connections
Info : WCH-Link-CH549 mod:RV version 2.7
Info : wlink_init ok
Info : This adapter doesn't support configurable speed
Info : JTAG tap: riscv.cpu tap/device found: 0x00000001 (mfg: 0x000 (<invalid>), part: 0x0000, ver: 0x0)
Warn : Bypassing JTAG setup events due to errors
Info : [riscv.cpu.0] datacount=2 progbufsize=8
Info : Examined RISC-V core; found 1 harts
Info : hart 0: XLEN=32, misa=0x40901125
[riscv.cpu.0] Target successfully examined.
Info : starting gdb server for riscv.cpu.0 on 3333
Info : Listening on port 3333 for gdb connections
Info : JTAG tap: riscv.cpu tap/device found: 0x00000001 (mfg: 0x000 (<invalid>), part: 0x0000, ver: 0x0)
Warn : Bypassing JTAG setup events due to errors
** Programming Started **
Info : device id = 0x26624f72
Info : flash size = 288kbytes
** Programming Finished **
#~/src/wch/MRS_Toolchain_Linux_x64_V1.60/OpenOCD/bin/openocd -f ~/src/wch/MRS_Toolchain_Linux_x64_V1.60/OpenOCD/bin/wch-riscv.cfg -c init -c halt -c "verify_image build/CH32V307EVT.elf" -c exit
#~/src/wch/MRS_Toolchain_Linux_x64_V1.60/OpenOCD/bin/openocd -f ~/src/wch/MRS_Toolchain_Linux_x64_V1.60/OpenOCD/bin/wch-riscv.cfg -c init -c "reset" -c exit || exit 0 # OpenOCD-wch 0.10 (MRS Toolchain v1.30)
~/src/wch/MRS_Toolchain_Linux_x64_V1.60/OpenOCD/bin/openocd -f ~/src/wch/MRS_Toolchain_Linux_x64_V1.60/OpenOCD/bin/wch-riscv.cfg -c init -c wlink_reset_resume -c exit || exit 0 # OpenOCD-wch 0.11 (MRS Toolchain v1.40)
Open On-Chip Debugger 0.11.0+dev-02215-gcc0ecfb6d-dirty (2022-10-10-10:35)
Licensed under GNU GPL v2
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
Info : only one transport option; autoselect 'jtag'
Ready for Remote Connections
Info : WCH-Link-CH549 mod:RV version 2.7
Info : wlink_init ok
Info : This adapter doesn't support configurable speed
Info : JTAG tap: riscv.cpu tap/device found: 0x00000001 (mfg: 0x000 (<invalid>), part: 0x0000, ver: 0x0)
Warn : Bypassing JTAG setup events due to errors
Info : [riscv.cpu.0] datacount=2 progbufsize=8
Info : Examined RISC-V core; found 1 harts
Info : hart 0: XLEN=32, misa=0x40901125
[riscv.cpu.0] Target successfully examined.
Info : starting gdb server for riscv.cpu.0 on 3333
Info : Listening on port 3333 for gdb connections
#miniterm.py - 115200 --raw
kevin@ubuntu:~/src/micropython-wch-ch32v307/ports/wch$

Then fingers crossed, you should be able to connect a terminal to the board and get access to the Micropython REPL.  Notice at the end of the above script, there is a commented out miniterm.py.  If you want to use miniterm.py this is part of the pyserial package.

I just used minicom: minicom -D /dev/ttyACM0

Micropython-CH32V307-1

And there we are.  Micropython is now up and running (with a limited subset of supported functions at present) on the CH32V307.

Kevin

 

 

 

computers · maker

Arduino Nano Every Timers and PWM

Returning to my exploration of the Arduino Nano Every, this post examines the use of timers and how they are used for pulse width modulation (PWM) applications.

For a detailed breakdown of the use of Timers (and the source for much of the information in this post, alongside the official Microchip documentation for the ATmega4809), see:

As I mentioned in my introductory article, the Arduino Nano Every works hard to be “pin compatible” with the original Arduino Nano.  One of the reasons for the odd port mapping described is down to which pins need to support PWM.  The Nano Every supports PWM on the following pins:

  • D3 – PF5
  • D5 – PB2
  • D6 – PF4
  • D9 – PB9
  • D10 – PB1

Unlike the Nano, there is no PWM possible on D11.  Part of the reason for the odd mapping to ATmega4809 ports and IO pins is that PWM is implemented using hardware timers and there are limitations on which pins can be used with the timers.

For the ATmega328 as used on the Nano, there are three timers which can be configured each to provide two outputs (OC0A/B, OC1A/B, OC2A/B). These are mapped onto the six PWM pins for an Uno or Nano.

The ATmega4809 has five timers in total, one “type A” which has three compare channels, and four “type B” timers with one channel channel each.  The Arduino environment uses these as follows:

  • TCA: 3 compare channels:
    • 0 – PWM pin 9
    • 1 – PWM pin 10
    • 2 – PWM pin 5
  • TCB0: PWM pin 6
  • TCB1: PWM pin 3, also used for tone() function
  • TCB2: Used with the servo library
  • TCB3: Used for millis(), micros(), delay()

Note that there are many more options for the ATmega4809 itself but these are not broken out in the Arduino Nano Every.

Arduino Nano Every Timer Configuration

Which output pins are connected to which timer channel is controlled by the ATmega4809’s PORTMUX registers (see chapter 15 in the datasheet).  There are six options for the three channels of TCA: they can be mapped onto pins [5:0] for each of PORTA to PORTF depending on the setting of the TCAROUTEA register.  To use all six pins requires the use of “split mode” for TCA, but only pins [2:0] of PORTB are actually broken out for the Nano Every anyway, so “non-split mode” is used which only applies to bits [2:0].  There are two options for each of the single channels of TCB0, 1, 2 and 3, a default and alternative mapping, depending on the settings of the TCBROUTEA register.  The Arduino library has TCA (“non-split” mode) mapped onto PORTB (pins 5, 9, 10); TCB0 to PF4 (pin 6); and TCB1 to PF5 (pin 3). None of the other output pins options from the ATmega4809 are routed out to physical pins on the Arduino Every.

A major limitation with the 4809’s timer setup is that there is only one prescalar setting and it is shared across all timers.  The Arduino library configures the TCA CTRLA “CLKSEL” bits to /64 in the setup_timers() function from the relevant variant.c file:

// Use DIV64 prescaler (giving 250kHz clock), enable TCA timer
TCA0.SINGLE.CTRLA = (TCA_SINGLE_CLKSEL_DIV64_gc) | (TCA_SINGLE_ENABLE_bm);

This means that TCA is running at 250kHz, for the default system clock of 16MHz.  Then in Wiring.c it sets the system TCBn timer to use the same prescalar as TCA in the CTRLA register:

/* Clock selection -> same as TCA (F_CPU/64 -- 250kHz) */
_timer->CTRLA = TCB_CLKSEL_CLKTCA_gc;

The “_timer” pointer will be set to the TCB3 due to the Nano Every having MILLIS_USE_TIMERB3 defined in the board.txt file configuration, thus placing the system timer for millis(), micros() and delay() on TCB3.

The tone() function will set up TCB1 depending on the frequency of tone required to either the TCA prescalar value (250kHz) or the system clock value (16MHz):

if(prescaler_needed){
_timer->CTRLA = TCB_CLKSEL_CLKTCA_gc;
} else {
_timer->CTRLA = TCB_CLKSEL_CLKDIV1_gc;
}

The “prescalar_needed” flag is calculated based on the requested frequency for the tone to be played.

In terms of PWM, the counters are all initialised (again in the setup_timers() function in variant.c), alongside the prescalar values, to give a base PWM frequency of 977Hz.  The timers are configured to run in 8-bit mode, with a period of 255 (via the PWM_TIMER_PERIOD define in timers.h).  In fact this is done for all timers apart from TCB3.  Incidentally this is where the routing of timers to IO pins is setup too.

Why 977Hz?  Well, 16Mhz / 64 = 250kHz as already mentioned.  With a counter (period) of 255, the counters will count from 0 to 255 at 250kHz and then reset, so 250kHz / 256 = 976.5625, so this is the default “tick” for all timers apart from TCB3.  The tone() function will mess with this for TCB1, which is why PWM on pin 3 will not be available if using tone().

Aside: Nano Every Main Clock Configuration

For completeness, this is a brief diversion into how the main system clock is configured.  This all happens in Wiring.c which is responsible for the configuration of the timers for millis(), micros() and delay() and the implementation of these functions.

There are two main clock control registers (see chapter 10 in the ATmega4809 datasheet) that we need to worry about: MCLKCTRLA and MCLKCTRLB.  There are others, but they aren’t of interest at the moment.

MCLKCTRLA contains the main system CLKSEL configuration, which is set to 0 by default, meaning the internal 16/20 MHz oscillator, whose frequency is determined by a fuse setting (OSCCFG – see section 7.8.2.3 in the datasheet).  The assumed default of the Arduino environment is 16MHz to maintain compatibility with the original Arduino Nano (this is all configured in the board.txt file, via the .build.f_cpu and .bootloader.OSCCFG settings – Tom Almy’s book has details of how to run it at 20MHz).

MCLKCTRLB contains the configuration for the system prescalar.  In the init() function is Wiring.c it is set according to the processor speed (as specified by F_CPU).  As this is 16MHz for the Arduino environment for the Nano Every, the following code is used:

#elif (F_CPU == 16000000)
cpu_freq = 16000000;

/* No division on clock */
_PROTECTED_WRITE(CLKCTRL_MCLKCTRLB, 0x00);

This disables the pre-scalar at the system level.  The init() function eventually calls the setup_timers() function that is implemented in Variant.c (as mentioned above).

Using PWM for Audio

There are several common uses for PWM on an Arduino: controlling servos, controlling LEDs, or for audio output.  The default configuration as described above, and implemented through the analogWrite() Arduino function is fine for servos and LEDs, but not for audio.  To get anywhere near half-decent PWM audio output, there are usually two timer-related functions required:

  • Getting a hardware timer to output a voltage level using PWM in place of having an actual digital to analog converter (basically what happens with servos and LEDs, but tailored to audio frequencies).
  • Configuring a periodic timer to “play samples” by changing the PWM duty cycle value to be sent at the fixed “sample rate”.

For a detailed discussion of using PWM on an ATmega328 based Arduino, see “Secrets of Arduino PWM”.  For a detailed discussion of the use of PWM for audio, see “Open Music Labs: PWM DAC”.

Typically on an ATmega328 based system, a sample rate is chosen (e.g. 16kHz or 32kHz) that relates to the size and pitch of samples to be played (for example, from a “wavetable” at a specific frequency).  Also, for the actual PWM output itself, a core PWM frequency is required that is ideally above human hearing, so again something like 32kHz is ideal.

So, we need to configure a timer, with a PWM pin, able to run at the appropriate PWM base frequency, but with a PWM mode that allows a duty cycle to be configurable from a timer-driven interrupt routine itself running at a fixed playback rate.

If we choose the type A timer, that would give us a choice of three potential pins for output.  But it also means that other PWM applications on those pins are not possible as they are all driven from the same timer. However type B timers only have a single 8-bit PWM mode.

Timer B for Audio PWM

In terms of a fixed PWM base frequency, the default is too slow – 977Hz will just not do it.  So, the only option that doesn’t involve messing with the system clock is to override the CLKSEL mode for one of the TCB timers and set it to the system clock or the system clock / 2.  This means it will run at either 16MHz or 8MHz rather than 250kHz.  So then it is a question of setting up the compare registers to keep the PWM ticking over at a sensible speed.  The default mode (again) is as an 8-bit timer, so for it to count the full range of 0 to 255 means the PWM frequency will either be 16MHz/256 = 62.5kHz or 8MHz/256 = 31.25kHz.

Either is fine as the starting point, but exactly how many times the “PWM” triggers will normally depend on the counter’s mode.  Recall that an ATmega328 has several modes that support a PWM output.  However, whilst the ATmega4809 type A timer has several PWM modes, a type B timer, has just one PWM mode – “8-bit PWM Mode” (see section 21.3.3.1.8 in the datasheet).

In 8-bit PWM mode the 16-bit compare register is treated as two 8-bit compare registers – CCMPH and CCMPL.  The least-significant 8-bits (CCMPL) determines the period for the PWM cycle.  The most-significant 8-bits (CCMPH) determines the duty cycle.  From the datasheet:

“The period (T) is controlled by CCMPL, while CCMPH controls the duty cycle of the waveform.  The counter will conitnuously count from BOTTOM to CCMPL, and the output will be set at BOTTOM and cleared when the counter reaches CCMPH.”

So by way of example, to configure Timer B1 (the one “reserved” for the arduino tone() function) for a base frequency PWM on pin D3, we have to do the following.

PORTMUX.TCBROUTEA |= PORTMUX_TCB1_bm; // Enable "alternative" pin output (PF5=D3) for TCB1
TCB1.CTRLA = TCB_CLKSEL_CLKDIV1_gc | TCB_ENABLE_bm; // SysClk and enabled
TCB1.CTRLB = TCB_CNTMODE_PWM8_gc | TCB_CCMPEN_bm; // 8-bit PWM and output enabled
TCB1.CCMPL = 255; // Default value TOP=255, duty cycle = 25%
TCB1.CCMPH = 64;

This outputs a nice 25% PWM (64 is 25% of 256) signal with a base frequency of 62.5kHz which you can see if you hook up a scope or logic analyser to D3.

So what about a timer routine to feed the PWM hardware some actual sample values?  Well the simplest option would be to hang an interrupt off the same timer to trigger on each period.

On an ATmega328 we usually have several options: interrupt on overflow (i.e. counter wrap around), interrupt on compare match, and others.  Unfortunately there is only one interrupt on a type B timer on the ATmega4809, the “capture event interrupt”.  The datasheet says it will trigger “when the counter reaches CCML” in 8-bit PWM mode, which means it triggers on every wrap-around of the counter.  So that still gives us the possibility of a 62.5kHz “tick”, so that should do the trick.

TCB1.INTCTRL |= TCB_CAPT_bm;

ISR(TCB1_INT_vect) {
// Should run at the same period as the PWM base frequency
TCB1.INTFLAGS = TCB_CAPT_bm; // Clear the interrupt flag

// Process the sample...
TCB1.CCMPL = TOP;
TCB1.CCMPH = NEW_SAMPLE; // In range 0 to TOP
}

For some reason, if I only wrote to CCMPH then it seemed to mess with the CCMPL value too, so I’ve found it best to update both in the interrupt routine.

One final note.  I won’t go into the details here, but in general terms, it makes things a lot easier (when using an 8-bit microcontroller anyway) to have your sample rate a multiple that makes calculation easier.  So for a typical wave-table synthesis application, where you might have 256 samples in your table you need to output in each period for your note, then if you choose a sample rate that is a multiple of 256, some optimisations are possible that means that you don’t need to get into floating point arithmetic or weird fractions of updates for your index into your table.

That is certainly possible here too.  We know what with our default TOP = 255 value, we can get a sample rate of 62.5kHz.  So what would it take to make it the more calculating friendly 65536 Hz?  Well we know the PWM frequency is given by (21.3.3.1.8 in the data sheet: “CCMPL+1 is the period of the output pulse”):

PWM Frequency = System Clock / (TOP value + 1)

So we can also say that:

TOP value + 1 = System Clock / PWM Frequency

So substituting for 65536 Hz and a 16MHz system clock gives us:

TOP value + 1 = 16 MHz / 65536 = 244.14

So choosing a TOP value of 243 should give us our required 65536 Hz sample rate.  The only complication now is that our samples have to be scaled to fit in a 0 to 243 range rather than a more typical 0 to 255 range.  This can be done in two ways – either simply divide by 2 if performance is critical, giving each sample a 0 to 127 value; or multiply the same by 244/256 which may involve floating or fixed point arithmetic if you don’t want to lose accuracy (although divide by 256 is the same as shift right by 8 places).

So before I finish, how does using 65536Hz help?  Well in a typical direct digital synthesis application, using a 256 byte wavetable, we need to maintain an “accumulator” to work out which sample to play from the wavetable at any one time.  If the index moves on 1 step at a time, then we know that frequency of the sounding note will be:

Frequency = Sample Rate / 256 = 256 Hz

And we also know that skipping a number (so the step is 2) will double the frequency, and “playing” each sample twice will half the frequency.  In general, it can be shown that the index into the wavetable is determined using the following:

Increment = frequency * 256 / Sample Rate

But to keep accuracy, the increment is often calculated using “fixed point arithmetic”.  This basically means, keeping it 256 times bigger than it needs to be, then diving by 256 when it is time to add it on to the accumulator.  So the actual (256 times bigger) increment is now:

Incrementx256 = frequency * 256 * 256 / Sample Rate

You can perhaps now see why having a sample rate that is a power of two is advantageous!  To calculate the index to use, the simplest way to divide by 256 is to shift right by 8 bits as follows:

Increment = Increment256 >> 8

Timer A for Audio PWM

The ATmega4809 type A timer has more useful options for audio PWM but there is a caveat.  It is also used to define the Arduino’s system “tick” which drives the functions: millis(), delay(), micros(). Messing with this timer will change the functionality of these timer-related functions too as well as any libraries that use them.

So is this an issue?  Yes for audio PWM.

The Arduino system clock runs at 16MHz (it could go up to 20MHz with the ATmega4809, but the Arduino environment keeps it at 16MHz for backwards compatibility) and the system “tick” is configured to use TCA with a prescalar setting of /64 giving a PWM frequency of 977Hz (as described previously) which is nowhere near fast enough for an audio PWM output.

Options:

  • Change the Arduino core to give more clock resolution: Tom Almy has a chapter on “Better Timer Setup” in his book “Far Inside the Arduino: Nano Every Supplement”.
  • Use a different Arduino core: MegaCoreX has an option to set the PWM frequency for example.
  • Change the “master configuration” for Timer A to be more audio friendly.
  • There might be an alternative master clock configuration, maybe involving different external crystals or something… I’ve not looked into it…
  • Go back to using the type B timers!

I’m attempting to keep to solutions that don’t involve changing the Arduino core, but the price is that I end up changing the core’s functionality, so I’m going to describe how to reconfigure TCA for use with audio and talk through the implications.

Much of the previous discussion for type B timers applies here too.  However there are now several different PWM modes that we get to choose from:

  • “Single-slope” PWM, with overflow triggered at the BOTTOM of the timer count.
  • “Dual-slope” PWM, with overflow triggered at TOP.
  • “Dual-slope” PWM, with overflow triggered at TOP and BOTTOM.
  • “Dual-slope” PWM, with overflow triggered at BOTTOM.

All PWM modes are configured using the PER (period) register and there are three compare registers that can be used: CMP0, CMP1, CMP2. These map onto the three output pins for the port as specified by TCBROUTEA – which is PORTB by default for the Nano Every.

There is also a “split mode” operation which turns the timer into two 8-bit timers, each with their own three CMPn registers and each able to drive three output waveforms on output pins, so TCA0 could in principle drive 6 PWM pins.  Split mode is only possible with Single-slope PWM.  But the Arduino Nano Every only has three output pins broken out, so there are no advantages to split mode in this case.

Starting with dual-slope operation, the period calculation is as follows.

The frequency for dual-slope operation is given by (20.3.3.4.4):

freq = Sys Clock / (2 . Prescalar . PER)

So for a PWM frequency of 32768Hz and a prescalar of 1:

PER = Sys Clock / (2 x freq) = 16MHz / (2 x 32768) = 244

So for an 8-bit PWM value of 0..255 then the calculation referred to above is required to scale it to a value in the range 0..243.

Consequently, I initialise the TCA0 timer for use with audio as follows.

pinMode(9, OUTPUT);

PORTMUX.TCAROUTEA = PORTMUX_TCA0_PORTB_gc; // Enable "alternative" pin output PORTB[5:0] for TCA0
TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1_gc | TCA_SINGLE_ENABLE_bm; // SysClk and enabled
TCA0.SINGLE.CTRLB = TCA_SINGLE_CMP0EN_bm | TCA_SINGLE_WGMODE_DSTOP_gc;
TCA0.SINGLE.CTRLD = 0; // Normal (Single) mode (not split)
TCA0.SINGLE.PER = 244; // 32768 Hz "tick"
TCA0.SINGLE.CMP0 = 61; // 25% duty cycle

TCA0.SINGLE.INTCTRL |= TCA_SINGLE_OVF_bm; // Turn on interrupts

ISR(TCA0_OVF_vect) {
TCA0.SINGLE.INTFLAGS = TCA_SINGLE_OVF_bm; // Clear the interrupt flag
TCA0.SINGLE.CMP0BUF = pwmval; // Write out the PWM value (0 to 243)
}

This configures TCA0 for use with PORTB (which should already be the default anyway); sets the prescalar to 1, so TCA0 runs as system clock speed (16MHz); configures dual-slope PWM mode with overflow at TOP running at 32768Hz; and enables the CMP0 output for PB0 (D9).

Notes on the code:

  • TCA0 is now running 64 times faster than the default, so millis(), delay(), micros() will all be running 64 times faster than before!
  • The overflow interrupt routine has to clear the OVF interrupt in code.
  • The pwmval will need scaling to 0..243 as described above.
  • The PWM frequency (32768Hz) is also the sample update rate for the PWM output.
  • Only CMP0 is being used, given a PWM audio output on PB0 which is mapped to Arduino D9.
  • The PWM output is set using the CMP0 register buffer.  This means it will only actually update when the counter reaches BOTTOM.  This prevents out of phase updates.
  • Don’t forget to set D9 as an OUTPUT pin! I’ve forgotten to do this several times and it always takes a while to work out why I’m getting no PWM output!

Here is the corresponding configuration for single-slope operation:

The frequency for single-slope operation is given by (20.3.3.4.3):

freq = Sys Clock / (Prescalar . (PER + 1))

So for a PWM frequency of 32768Hz and a prescalar of 1:

PER + 1 = Sys Clock / freq = 16MHz / 32768 = 488

PER = 488 – 1 = 487

So for an 8-bit PWM value of 0..255 then the calculation referred to above is required to scale it to a value in the range 0..487.  But it could be used “as is” but it will just generate a slightly quieter signal if it isn’t using the full PWM resolution.

For this operation, I initialise the TCA0 timer for use with audio as follows.

pinMode(9, OUTPUT);

PORTMUX.TCAROUTEA = PORTMUX_TCA0_PORTB_gc; // Enable "alternative" pin output PORTB[5:0] for TCA0
TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1_gc | TCA_SINGLE_ENABLE_bm; // SysClk and enabled
TCA0.SINGLE.CTRLB = TCA_SINGLE_CMP0EN_bm | TCA_SINGLE_WGMODE_SINGLESLOPE_gc;
TCA0.SINGLE.CTRLD = 0; // Normal (Single) mode (not split)
TCA0.SINGLE.PER = 487; // 32768 Hz "tick"
TCA0.SINGLE.CMP0 = 122; // 25% duty cycle

TCA0.SINGLE.INTCTRL |= TCA_SINGLE_OVF_bm; // Turn on interrupts

ISR(TCA0_OVF_vect) {
TCA0.SINGLE.INTFLAGS = TCA_SINGLE_OVF_bm; // Clear the interrupt flag
TCA0.SINGLE.CMP0BUF = pwmval; // Write out the PWM value (0 to 487)
}

This configures TCA0 for use with PORTB; sets the prescalar to 1, so TCA0 runs as system clock speed (16MHz); configures single-slope PWM mode running at 32768Hz; and enables the CMP0 output for PB0 (D9).

All the same notes from above apply but naturally the PWM value now can be in the range 0..487 if we want to utilise the full range available.

Single vs Dual-slope Operation:

  • Single: allows full 8-bit (“almost 9-bit”) PWM resolution.  Dual: requires scaling down to 0-243.
  • Dual: gives a “phase correct” update. Single: may get anomalies when updating.

So single gives more PWM resolution but at the cost of loss of phase accuracy.  This probably isn’t a major issue for audio applications (is it? I don’t know!?  Let me know :)).

Conclusion

It turns out that once the basic details have been worked through, it isn’t so bad creating audio-friendly PWM on an Arduino Nano Every.  The choice of output pin is not as straight forward as for the original Nano.  The option that has the least number of side effects and limitations (in terms of limited how other pins can be used) is to use one of the type B timers.

But TCB3 is used for the system timer, and TCB2 is used for the servo library.  These also don’t have output pins available on the board, so I opted to use TCB1 as used with the tone() function.  The thinking being that if you are using PWM audio, you probably aren’t interested in the tone() function anymore!  But this means the audio output is limited to D3.  The principles described here should apply equally well to TCB0 which would also give audio output on D6.

Using TCA0 opens up the use of D5, D9 and D10 for PWM audio output, but will interfere with the system clock and require adjustments to any use of millis(), delay() or micros().

If you have an alternative ATmega4809 microcontroller board then there are almost certainly more options in which case it would be worth investigating the unofficial MegaCoreX support, but pretty much all the theory described in this post will still apply.

At some point I’d like to investigate it myself too.

Kevin

 

computers

Reading I2C Registers from Circuitpython

I had some code that used the Python SMBus library on a Raspberry Pi to drive an I2C comms board, and I wanted to use it from an RP2040 (as used on the Raspberry Pi Pico) board running Circuitpython.  It turns out it isn’t a straight forward swap of one set of I2C support for another!

If I was using Micropython, then I found a “translation layer” to provide the same API on top of the Micropython machine.I2C code here: https://github.com/gkluoe/micropython-smbus.

But Circuitpython appears to have a different approach again to I2C as far as I can see.  There are (as you might expect) some great tutorials on getting up and running with I2C on Circuitpython, but I’ve not been able to personally translate that over into what to do if you want to access specific registers on a device.  As far as I can see, the examples are for relatively simple “register-less” devices. I think the assumption is that if you want to talk to anything more complicated then you’d go off and dig out an appropriate library (which is a fair assumption in most cases!).

As far as I can make out there are two levels at which you could be accessing I2C from Circuitpython:

The second is meant to be a helper class to take away some of the complexities of managing the I2C bus yourself.  In fact the documentation for busio.I2C states:

“Using this class directly requires careful lock management. Instead, use I2CDevice to manage locks.”

But I was trying to replace code using write_i2c_block_data and read_i2c_block_data to access a specific register of a device, and there doesn’t seem to be an equivalent in either of these that I could find.

In the end I used the following code to read and write to I2C registers. I don’t know if this is the right way, but it works for me at present. I think the key thing I was missing is that to read a register, you first need to write the required register out to the device so it can respond (this is standard I2C stuff, but only once you know it!).  Hence, I’m using the “writeto_then_readfrom” function for reading.  But you only need the “writeto” when writing, but again you need to ensure the first value in your data being written is the register to write to.

i2cbus = board.I2C()

def i2c_read_reg(i2cbus, addr, reg, result):
  while not i2cbus.try_lock():
    pass
  try:
    i2cbus.writeto_then_readfrom(addr, bytes([reg]), result)
    return result
  finally:
    i2cbus.unlock()
    return None

def i2c_write_reg(i2cbus, addr, reg, data):
  while not i2cbus.try_lock():
    pass
  try:
    buf = bytearray(1)
    buf[0] = reg
    buf.extend(data)
    i2cbus.writeto(addr, buf)
  finally:
    i2cbus.unlock()

I believe it should be possible to use the adafruit_bus_device class to handle the bus locking and so on but I couldn’t get that working, so I used busio.i2c directly.

So this all seems to work for my situation, but do let me know if you know better or if you can tell me what I’m missing with the existing tutorials and libraries!  I still don’t know a lot about Circuitpython and I’m still working out I2C if I’m honest too!

Here are some tutorials and guides that talk a bit more about the I2C hardware and software protocols, including accessing registers on devices.

Kevin

computers · maker

Arduino Nano Every I2C and SPI Displays

In this part of my series on the Arduino Nano Every, I take a look at a couple of common displays to see how they work with the Nano Every.  In short – pretty well.  Read on for how I hooked up I2C OLED and an SPI TFT displays.

I’m using the standard Arduino environment as described in my introduction to the Nano Every.

I2C OLED Displays

I thought I’d try an OLED display first.  There are some very common ones using the SSD1306 as the interface chip that hook up to the I2C bus.  The Nano Every maintains the original Nano’s I2C pins:

  • A4 = SDA
  • A5 = SCL

You have to watch out when wiring up your OLED display as there are a number of different combinations of pinout!  In my own collection I have displays with different arrangements for VCC and GND for example.

Nano Every OLED_bb

The basic circuit is as shown on the left.  It needs just four connections: VCC (5V), GND, SCL (A5), SDA (A4).  Also above you can see how you could wire it up for two displays, but one of them will need to have the I2C address changed, before you can do this, but more on that in a moment.

I’m using the Adafruit graphics library for the SSD1306 range of displays.  The default is to use hardware I2C so all that is required is to set the I2C address to match the display you have.  My displays come with a default address of 0x3C (although it is marked as 0x78 on the actual display if it is marked at all – the this is due to the least significant bit not be part of the address itself, but it is often included – so 0x3C is the address, but “on the wire” it has to be shifted one bit to the left, which then “looks” like 0x78… but anyway…).

I’ve grabbed the example from: Examples -> SSD1306 -> ssd1306128x64_i2c.  The line that sets the address is the following one:

#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32

The datasheet referred to is for Adafruit’s own displays where the 128×64 is on 0x3D and the 128×32 is on 0x3C.  For most other displays the default is probably like mine – 0x3C.

I’ve attempted to run two displays before, but an Arduino Uno or Nano just doesn’t have enough memory to support two complete frame buffers for two 128×64 displays.  I’ve managed to configure them as 128×32 displays and got two of them running, but that is all.  So I was keen to try with the Nano Every as it has more memory!

Some displays have a jumper on the back to let you change the address, but on mine they are a fiddly surface mount resistor which needs desoldering from one link and moving to another.  Some displays aren’t configurable at all. I have some of both, as shown below.

IMG_5624

You can perhaps spot how my dodgy soldering has just about managed to move the jumper from the 0x78 position to the 0x7A position! Mind you, it took a couple of goes!

In terms of showing it working, I replicated a couple of lines of the Adafruit demo code to get as far as showing the logo on the second screen.  I didn’t change anything else.  At the top of the file, the following lines need to be added.

#define SCREEN2_ADDRESS 0x3D ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
Adafruit_SSD1306 display2(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

Then in the setup() code, I added the following.

if(!display2.begin(SSD1306_SWITCHCAPVCC, SCREEN2_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
} else {
display2.display();
}

This means that if a second display (on address 0x3D) is found then it will show the initial logo and then nothing else for that display, whilst the code goes on the run through the full demo on the other display (on address 0x3C).

IMG_5623

ST7735 SPI TFT Display

Next I turned to the cheap TFT displays I’ve looked at before.

Note the display in the Fritzing diagram below is just for illustration as I don’t have an exact match (the observant among you might notice I have one extra pin on mine!). Be sure to check and double check the required pins and expected voltage level for your display module.

Nano Every TFT_bb

One of the differences between the Nano Every and the original Nano is that the SPI SS pin is on pin 8, not pin 10. This means the required pin connections for my display are as follows:

 1-VCC - 5V
2-GND - GND
3-N/C
4-N/C
5-N/C
6-N/C
7-CLK - 13 (SCLK)
8-SDA - 11 (MOSI)
9-RS - 10 (D10)
10-RST - 9 (D9)
11-CS - 8 (D8)

I’m using the Adafruit ST7735 library this time.  Taking the code from:

Examples -> Adafruit ST7735 and ST7739 Library -> graphicstest

the pins must be configured at the top as follows:

#define TFT_CS   8
#define TFT_RST 9
#define TFT_DC 10

Then all the graphics primitives from the Adafruit GFX Library should be available.

IMG_5627

Conclusion

I was anticipating some issues with the displays, but actually they worked pretty well.  Most of this is down to the fact that the Wire (I2C) and SPI Libraries are part of the “built-in” library support provided by the Arduino environment and the low-level drivers for the displays provided by Adafruit are written well with minimal reliance on hardware specific features of any single microcontroller.

Kevin

 

computers · maker

Arduino Nano Every Analog Inputs

The next part in my series on the Arduino Nano Every, looks at how to access the additional 6 analog inputs that can be found scattered around the digital IO pins.

As I mentioned in my introductory article, the Arduino Nano Every works hard to be “pin compatible” with the original Arduino Nano.  This means that it supports 8 analog inputs, labelled A0 to A7, all along one side of the board.  But the ATmega4809 actually has 18 analog inputs, and the additional ones can be found as extra functions on some of the other pins.  Reading the data sheet, we can see that the 16 analog inputs are mapped as follows (I’ve included the Arduino pin numbers for reference):

  • AIN0 -> PD0 – A3
  • AIN1 -> PD1 – A2
  • AIN2 -> PD2 – A1
  • AIN3 -> PD3 – A0
  • AIN4 -> PD4 – A6
  • AIN5 -> PD5 – A7
  • AIN6 -> PD6
  • AIN7 -> PD7 – AREF
  • AIN8 -> PE0 – D11
  • AIN9 -> PE1 – D12
  • AIN10 -> PE2 – D13
  • AIN11 -> PE3 – D8
  • AIN12 -> PF2 – A4
  • AIN13 -> PF3 – A5
  • AIN14 -> PF4 – D6
  • AIN15 -> PF5 – D3

Note that AIN6 (PD6) is not connected and that AIN7 (PD7) is used for AREF.

To allow the Arduino environment to support the extra analog inputs, there are a few lists and structures in the Arduino “pins” definition file for the Nano Every (the same place that we tweaked to add serial ports last time):

...../arduino15/packages/arduino/hardware/megaavr/1.8.5/variants/nona4809/pins_arduino.h

There are several entries in this file that determine the mapping of Arduino pin labels to MCU ports and pins.  However there is a slight issue.  The pins for A0 to A7 are contiguous in the Arduino’s pin mappings – they map onto (internal) pins 14-21.  This means that you can do things like the following quite happily and it will all work:

for (int i=0; i<8; i++) {
aval[i] = analogRead(A0+i);
}

So to extend the range of analog inputs to include A8 to A13, ideally we’d extend the original range so that “A0+8 = A8” but these (internal) pin numbers are already taken for other functions (in this case the I2C and USB serial connections).  One option is to “shuffle” these other functions down to make room.  The other is to accept that “A7+1 != A8” and leave a gap. In what follows I’ve opted to leave a gap…

So here are the changes we need to make to pins_arduino.h.

There is a definition of NUM_ANALOG_INPUTS that isn’t really the number of analog inputs, but is used to determined the “highest numbered analog channel” used.  This is currently set to 14, but needs updating to 16.

#define NUM_ANALOG_INPUTS 16

We now need to set up some new pin definitions (e.g. PIN_A8 and A8 onwards) as follows. This is where the “gap” first becomes apparent as “internal” pin numbers 22 to 25 are already in use.

#define PIN_A0 (14) // AIN3
#define PIN_A1 (15) // AIN2
#define PIN_A2 (16) // AIN1
#define PIN_A3 (17) // AIN0
#define PIN_A4 (18) // PF2 / AIN12
#define PIN_A5 (19) // PF3 / AIN13
#define PIN_A6 (20) // AIN5
#define PIN_A7 (21) // AIN4

// Additional analog inputs
// Arduino pin numbering starts after OFFICIAL pin
// numbers end...
// Note: AIN6/AIN7 don't make it to Arduino pins
#define ANALOG_INPUT_EXTRA 26
#define PIN_A8 (ANALOG_INPUT_EXTRA) // AIN8
#define PIN_A9 (ANALOG_INPUT_EXTRA+1) // AIN9
#define PIN_A10 (ANALOG_INPUT_EXTRA+2) // AIN10
#define PIN_A11 (ANALOG_INPUT_EXTRA+3) // AIN11
#define PIN_A12 (ANALOG_INPUT_EXTRA+4) // AIN14
#define PIN_A13 (ANALOG_INPUT_EXTRA+5) // AIN15

static const uint8_t A0 = PIN_A0;
static const uint8_t A1 = PIN_A1;
static const uint8_t A2 = PIN_A2;
static const uint8_t A3 = PIN_A3;
static const uint8_t A4 = PIN_A4;
static const uint8_t A5 = PIN_A5;
static const uint8_t A6 = PIN_A6;
static const uint8_t A7 = PIN_A7;
static const uint8_t A8 = PIN_A8;
static const uint8_t A9 = PIN_A9;
static const uint8_t A10 = PIN_A10;
static const uint8_t A11 = PIN_A11;
static const uint8_t A12 = PIN_A12;
static const uint8_t A13 = PIN_A13;

Then there are four structures that map these internal pin numbers onto various properties of the hardware.  Each of these needs our additional six new entries adding in at the end.

const uint8_t digital_pin_to_port[] = {
... skip "internal" pins 0 through to 25 ...
PE, // 26 PE0/AI8
PE, // 27 PE1/AI9
PE, // 28 PE2/AI10
PE, // 29 PE3/AI11
PF, // 30 PF4/AI14
PF, // 31 PF5/AI15
};

const uint8_t digital_pin_to_bit_position[] = {
... skip "internal" pins 0 through to 25 ...
PIN0_bp, // 26 PE0/AI8
PIN1_bp, // 27 PE1/AI9
PIN2_bp, // 28 PE2/AI10
PIN3_bp, // 29 PE3/AI11
PIN4_bp, // 30 PF4/AI14
PIN5_bp, // 31 PF5/AI15
};

const uint8_t digital_pin_to_bit_mask[] = {
... skip "internal" pins 0 through to 25 ...
PIN0_bm, // 26 PE0/AI8
PIN1_bm, // 27 PE1/AI9
PIN2_bm, // 28 PE2/AI10
PIN3_bm, // 29 PE3/AI11
PIN4_bm, // 30 PF4/AI14
PIN5_bm, // 31 PF5/AI15
};

const uint8_t digital_pin_to_timer[] = {
... skip "internal" pins 0 through to 25 ...
NOT_ON_TIMER, // 26 PE0/AI8
NOT_ON_TIMER, // 27 PE1/AI9
NOT_ON_TIMER, // 28 PE2/AI10
NOT_ON_TIMER, // 29 PE3/AI11
NOT_ON_TIMER, // 30 PF4/AI14
NOT_ON_TIMER, // 31 PF5/AI15
};

Finally there is a macro that is used to spot if the analogRead function has been given a “An” label or a “Dn” number and maps that onto the appropriate analog input channel on the chip.  That needs to be updated in two ways:

  • The extra six analog inputs need the appropriate analog channels (AINxx) defining and adding.
  • The “translation” macro, digitalPinToAnalogInput(p), needs to take into account the gap in the analog input range.
const uint8_t analog_pin_to_channel[] = {
3,
2,
1,
0,
12,
13,
4,
5,
// Start of extra analog inputs
8,
9,
10,
11,
14,
15
};

#endif

#define digitalPinToAnalogInput(p) ((p < ANALOG_INPUT_OFFSET) ? analog_pin_to_channel[p] : \
(p < ANALOG_INPUT_EXTRA) ? (analog_pin_to_channel[p - ANALOG_INPUT_OFFSET] ) : (analog_pin_to_channel[p - ANALOG_INPUT_EXTRA + 8] ) )

The digitalPinToAnalogInput macro basically says:

  • IF provided pin is in digital range, just use the pin number directly as the index into the analog_pin_to_channel array.
  • ELSE IF provided pin is in the original A0 to A7 range, then subtract the number for A0, then use as an index into the analog_pin_to_channel array.
  • ELSE provided pin is in the new extended range, so subtract the number for A8 and add 8, and use that as the index into the analog_pin_to_channel array.

In all cases we should end up reading out which AINxx channel to use from the analog_pin_to_channel array and it will work for all the following input ranges:

  • 0 to 13 – analog inputs A0 to A13.
  • A0 (14) to A7 (21) – analog inputs A0 to A7.
  • A8 (26) to A13 (31) – analog inputs A8 to A13.

With those changes, A0 through to A13 should now all be usable, although remember that A8 does not directly follow (numerically) A7.

Here is some test code to read all 14 analog inputs.

void setup() {
Serial.begin(9600);
}

void loop() {
for (int i=0; i<8; i++) {
int aval = analogRead(A0+i);
Serial.print(aval);
Serial.print("\t");
}
for (int i=0; i<6; i++) {
int aval = analogRead(A8+i);
Serial.print(aval);
Serial.print("\t");
}
Serial.print("\n");
delay(100);
}

Conclusion

I’d really prefer to have made the range of “internal” pin values contiguous but I don’t know what else would break.  To be perfectly honest, the above may break other functions of the Arduino in quite spectacular ways – I just don’t know yet!  But it does seem to give access to all 14 analog inputs.

Once again, MegaCoreX seems to already include all this and a lot more beside.  In fact it appears to support all 16 analog inputs, even though two of them don’t actually appear as pins on the Nano Every.

Kevin