nano improvements

This commit is contained in:
Bane Stojkovic
2026-09-07 12:26:07 +02:00
parent e2cddf5954
commit 37a5acce32
5 changed files with 166 additions and 36 deletions
@@ -79,6 +79,8 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
public static Color ANTENNA_COLOR = Color.decode("#DAA520");
public static Color ANTENNA_BG_COLOR = Color.decode("#1E1E1E");
public static Color PAD_COLOR = Color.decode("#DAA520");
public static Color CONNECTOR_PLASTIC_COLOR = Color.decode("#F5F5DC");
public static Color CONNECTOR_PLASTIC_BORDER = Color.decode("#333333");
public static Color BUTTON_BODY_COLOR = Color.decode("#383838");
public static Color BUTTON_BORDER_COLOR = Color.decode("#666666");
public static Color BUTTON_ACTUATOR_COLOR = Color.decode("#A0A0A0");
@@ -53,7 +53,7 @@ import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Arduino Nano", category = "Controllers",
author = "Branislav Stojkovic",
description = "Arduino Nano Breadboard-Friendly Microcontroller Board (classic, Every, 33 IoT, "
+ "33 BLE, RP2040 Connect, Nano ESP32)",
+ "33 BLE, RP2040 Connect, Nano ESP32, Nano R4)",
instanceNamePrefix = "MCU", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
@@ -66,16 +66,29 @@ public class ArduinoNano extends AbstractMakerBoard {
public static Size BOARD_WIDTH = new Size(0.73d, SizeUnit.in);
public static Size BOARD_LENGTH = new Size(1.70d, SizeUnit.in);
public static Size ROW_SPACING = new Size(0.60d, SizeUnit.in);
public static Size BOARD_MARGIN_Y = new Size(0.15d, SizeUnit.in);
// Where the part at the end of the board opposite the USB jack starts, and the footprint of the
// Nano R4's Qwiic socket. The R4's extra control point is derived from these, so the point and
// the drawn connector stay together.
public static Size END_PART_OFFSET_Y = new Size(1.5d, SizeUnit.mm);
public static Size QWIIC_WIDTH = new Size(6.5d, SizeUnit.mm);
public static Size QWIIC_LENGTH = new Size(4.5d, SizeUnit.mm);
public static final int PINS_PER_ROW = 15;
// Ordered by label so the Version drop-down reads alphabetically. XStream serializes enum
// constants by name, so the order here is free to change without affecting existing .diy files.
public enum NanoVersion {
CLASSIC("Nano (ATmega328)", "NANO", "m328P", null),
EVERY("Nano Every", "EVERY", "m4809", "SAMD11"),
NANO_33_IOT("Nano 33 IoT", "33 IOT", "SAMD21", "NINA-W102"),
NANO_33_BLE("Nano 33 BLE", "33 BLE", null, "NINA-B306"),
NANO_33_BLE_SENSE("Nano 33 BLE Sense", "33 BLE SENSE", null, "NINA-B306"),
NANO_RP2040_CONNECT("Nano RP2040 Connect", "RP2040 CONNECT", "RP2040", "NINA-W102"),
NANO_ESP32("Nano ESP32", "NANO ESP32", null, "NORA-W106");
NANO_33_IOT("Nano 33 IoT", "33 IOT", "SAMD21", "NINA-W102"),
NANO_ESP32("Nano ESP32", "NANO ESP32", null, "NORA-W106"),
EVERY("Nano Every", "EVERY", "m4809", "SAMD11"),
NANO_R4("Nano R4", "NANO R4", "RA4M1", "QWIIC"),
NANO_RP2040_CONNECT("Nano RP2040 Connect", "RP2040 CONNECT", "RP2040", "NINA-W102");
private final String label;
private final String silkLabel;
@@ -158,11 +171,24 @@ public class ArduinoNano extends AbstractMakerBoard {
// the other boards repeat RESET.
public static final String[] PIN_NAMES_RP2040 = new String[] {
// Left row (0..14, top to bottom)
"D1 (TX)", "D0 (RX)", "RST1", "GND1", "D2 (~)", "D3 (~)", "D4 (~)", "D5 (~)", "D6 (~)", "D7 (~)", "D8 (~)", "D9 (~)", "D10 (~)", "D11 (~, MOSI)", "D12 (~, MISO)",
"D1 (TX)", "D0 (RX)", "RST1", "GND_1", "D2 (~)", "D3 (~)", "D4 (~)", "D5 (~)", "D6 (~)", "D7 (~)", "D8 (~)", "D9 (~)", "D10 (~)", "D11 (~, MOSI)", "D12 (~, MISO)",
// Right row (15..29, top to bottom)
"VIN", "GND2", "REC", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3", "A2", "A1", "A0", "AREF", "3.3V", "D13 (~, SCK)"
};
// Nano R4 (ABX00142): the Renesas RA4M1 board. Same PWM set as the classic, but D4 / D5 double as
// the CAN pins, A1-A3 reach the on-chip OPAMP, A0 is the DAC, and BOOT replaces the second RESET.
public static final String[] PIN_NAMES_R4 = new String[] {
// Left row (0..14, top to bottom)
"D1 (TX)", "D0 (RX)", "RST1", "GND1", "D2", "D3 (~)", "D4 (CAN TX)", "D5 (~, CAN RX)", "D6 (~)",
"D7", "D8", "D9 (~)", "D10 (~, CS)", "D11 (~, MOSI)", "D12 (MISO)",
// Right row (15..29, top to bottom)
"VIN", "GND2", "BOOT", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3 (OPAMP OUT)",
"A2 (OPAMP -)", "A1 (OPAMP +)", "A0 (DAC)", "AREF", "3.3V", "D13 (SCK)",
// Qwiic socket (30), a single point at the centre of the connector so a wire can land on it
"QWIIC"
};
// Nano ESP32 (ABX00083): PWM on every digital and analog pin, and the pins the other boards use
// for RESET and AREF are the B1 / B0 boot-mode pads. Dual-labelled with the Espressif GPIO
// number, which is what the ESP-IDF documentation refers to.
@@ -213,6 +239,8 @@ public class ArduinoNano extends AbstractMakerBoard {
return PIN_NAMES_RP2040;
case NANO_ESP32:
return PIN_NAMES_ESP32;
case NANO_R4:
return PIN_NAMES_R4;
default:
return PIN_NAMES;
}
@@ -223,6 +251,7 @@ public class ArduinoNano extends AbstractMakerBoard {
case CLASSIC:
return UsbPortType.MINI;
case NANO_ESP32:
case NANO_R4:
return UsbPortType.TYPE_C;
default:
return UsbPortType.MICRO;
@@ -242,11 +271,19 @@ public class ArduinoNano extends AbstractMakerBoard {
}
/**
* True for the boards whose end module is a shielded can. The Every's part there is a bare QFN
* USB bridge, so it is drawn as a chip instead.
* Number of control points that are actual header pins. The Nano R4's last point is the centre
* of its Qwiic socket, which takes a wire but is not a pin to be populated with a header.
*/
private boolean hasShieldedModule() {
return getVersion() != NanoVersion.CLASSIC && getVersion() != NanoVersion.EVERY;
private int getHeaderPinCount() {
return getVersion() == NanoVersion.NANO_R4 ? controlPoints.length - 1 : controlPoints.length;
}
/**
* True for the boards carrying their MCU as a package rotated 45 degrees, the way the classic
* Nano mounts its ATmega328P and the Every its ATmega4809.
*/
private boolean isChipRotated() {
return getVersion() == NanoVersion.CLASSIC || getVersion() == NanoVersion.EVERY;
}
@EditableProperty(name = "Headers")
@@ -272,7 +309,7 @@ public class ArduinoNano extends AbstractMakerBoard {
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double spacing = PIN_SPACING.convertToPixels(); // 20px (0.10")
double rowSpacing = new Size(0.60d, SizeUnit.in).convertToPixels(); // 120px (0.60")
double rowSpacing = ROW_SPACING.convertToPixels(); // 120px (0.60")
double[][] relativeOffsets = new double[getPinNames().length][2];
@@ -287,8 +324,14 @@ public class ArduinoNano extends AbstractMakerBoard {
relativeOffsets[15 + i][1] = i * spacing;
}
// Only the classic Nano populates the 2x3 ICSP block; on every later board that end of the
// PCB carries the radio module or USB bridge instead.
// PCB carries the radio module, USB bridge or Qwiic socket instead.
if (getVersion() != NanoVersion.CLASSIC) {
if (getVersion() == NanoVersion.NANO_R4) {
// Centre of the Qwiic socket, matching where draw() places it
relativeOffsets[30][0] = rowSpacing / 2.0;
relativeOffsets[30][1] = -BOARD_MARGIN_Y.convertToPixels() + END_PART_OFFSET_Y.convertToPixels()
+ QWIIC_LENGTH.convertToPixels() / 2.0;
}
rotatePoints(firstPoint, relativeOffsets);
return;
}
@@ -413,8 +456,9 @@ public class ArduinoNano extends AbstractMakerBoard {
double chipCenterX = boardX + boardW / 2.0;
double chipCenterY = boardY + new Size(1.06d, SizeUnit.in).convertToPixels();
if (getVersion() == NanoVersion.CLASSIC) {
// The ATmega328P TQFP is mounted at 45 degrees on the classic Nano
if (isChipRotated()) {
// The ATmega328P on the classic Nano and the ATmega4809 on the Every are both mounted at
// 45 degrees
AffineTransform oldChipTx = g2d.getTransform();
g2d.translate(chipCenterX, chipCenterY);
g2d.rotate(Math.PI / 4.0);
@@ -445,16 +489,37 @@ public class ArduinoNano extends AbstractMakerBoard {
// The end of the board opposite the USB jack: the classic Nano's ICSP header, and on every
// later board the part that identifies it at a glance
if (getVersion().getModuleLabel() != null) {
double moduleY = boardY + new Size(1.5d, SizeUnit.mm).convertToPixels();
if (hasShieldedModule()) {
double moduleW = new Size(10.0d, SizeUnit.mm).convertToPixels();
double moduleH = new Size(11.0d, SizeUnit.mm).convertToPixels();
drawMetalConnector(g2d, boardX + (boardW - moduleW) / 2.0, moduleY, moduleW, moduleH,
getVersion().getModuleLabel());
} else {
double bridgeSize = new Size(5.0d, SizeUnit.mm).convertToPixels();
drawChip(g2d, boardX + (boardW - bridgeSize) / 2.0, moduleY, bridgeSize, bridgeSize,
getVersion().getModuleLabel());
double moduleY = boardY + END_PART_OFFSET_Y.convertToPixels();
switch (getVersion()) {
case EVERY: {
// A bare QFN USB bridge rather than a shielded module
double bridgeSize = new Size(5.0d, SizeUnit.mm).convertToPixels();
drawChip(g2d, boardX + (boardW - bridgeSize) / 2.0, moduleY, bridgeSize, bridgeSize,
getVersion().getModuleLabel());
break;
}
case NANO_R4: {
// The Qwiic I2C socket is a 4-way JST-SH in beige plastic, not a shielded module
double qwiicW = QWIIC_WIDTH.convertToPixels();
double qwiicH = QWIIC_LENGTH.convertToPixels();
double qwiicX = boardX + (boardW - qwiicW) / 2.0;
g2d.setColor(CONNECTOR_PLASTIC_COLOR);
g2d.fill(new Rectangle2D.Double(qwiicX, moduleY, qwiicW, qwiicH));
g2d.setColor(CONNECTOR_PLASTIC_BORDER);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(new Rectangle2D.Double(qwiicX, moduleY, qwiicW, qwiicH));
g2d.setFont(SILK_FONT_SMALL);
StringUtils.drawCenteredText(g2d, getVersion().getModuleLabel(), qwiicX + qwiicW / 2.0,
moduleY + qwiicH / 2.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
break;
}
default: {
double moduleW = new Size(10.0d, SizeUnit.mm).convertToPixels();
double moduleH = new Size(11.0d, SizeUnit.mm).convertToPixels();
drawMetalConnector(g2d, boardX + (boardW - moduleW) / 2.0, moduleY, moduleW, moduleH,
getVersion().getModuleLabel());
}
}
}
@@ -489,9 +554,9 @@ public class ArduinoNano extends AbstractMakerBoard {
// Draw pins or solder pads
if (headers) {
drawPinHeader(g2d, 0, controlPoints.length, false, outlineMode, drawingObserver);
drawPinHeader(g2d, 0, getHeaderPinCount(), false, outlineMode, drawingObserver);
} else if (getVersion() == NanoVersion.CLASSIC) {
drawPcbSolderPads(g2d, 0, controlPoints.length, true, outlineMode, drawingObserver);
drawPcbSolderPads(g2d, 0, getHeaderPinCount(), true, outlineMode, drawingObserver);
}
g2d.setComposite(oldComposite);
@@ -383,9 +383,9 @@ public class RaspberryPi extends AbstractMakerBoard {
double uartX = boardX + new Size(32.5d, SizeUnit.mm).convertToPixels() - uartW / 2.0;
double uartY = boardY + new Size(52.5d, SizeUnit.mm).convertToPixels() - uartH / 2.0;
g2d.setColor(Color.decode("#F5F5DC")); // cream/beige
g2d.setColor(CONNECTOR_PLASTIC_COLOR);
g2d.fill(new Rectangle2D.Double(uartX, uartY, uartW, uartH));
g2d.setColor(Color.decode("#333333"));
g2d.setColor(CONNECTOR_PLASTIC_BORDER);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(new Rectangle2D.Double(uartX, uartY, uartW, uartH));
@@ -552,7 +552,7 @@ public class RaspberryPi extends AbstractMakerBoard {
g2d.fillRect(2, 13, 1, 5);
g2d.fillRect(18, height - 10, 1, 6);
g2d.fillRect(20, height - 10, 1, 6);
g2d.setColor(Color.decode("#F5F5DC")); // UART
g2d.setColor(CONNECTOR_PLASTIC_COLOR); // UART
g2d.fillRect(13, height - 5, 2, 1);
} else {
g2d.fillRect(3, 11, 1, 7); // DISP
@@ -157,11 +157,11 @@ public class ArduinoNanoTest {
ArduinoNano nano = new ArduinoNano();
nano.setVersion(version);
int expected = version == ArduinoNano.NanoVersion.CLASSIC ? 36 : 30;
Assert.assertEquals(version + " pin count", expected, nano.getControlPointCount());
Assert.assertEquals(version + " pin count", expectedPinCount(version), nano.getControlPointCount());
Assert.assertEquals(version + " outline should be unchanged",
classicBody.getBounds2D(), nano.getBodyShape().getBounds2D());
for (int i = 0; i < nano.getControlPointCount(); i++) {
// The 2x15 pin field is identical on every version; anything past it is board-specific
for (int i = 0; i < 30; i++) {
Assert.assertEquals(version + " pin " + i, classic.getControlPoint(i), nano.getControlPoint(i));
}
}
@@ -180,7 +180,8 @@ public class ArduinoNanoTest {
Assert.assertEquals("MISO", nano.getControlPointNodeName(30));
Assert.assertNull("The classic Nano has the header, not a module", version.getModuleLabel());
} else {
Assert.assertEquals(version + " should stop at the pin rows", 30, nano.getControlPointCount());
Assert.assertEquals(version + " should have no ICSP block",
expectedPinCount(version), nano.getControlPointCount());
Assert.assertNotNull(version + " should name the part in place of the ICSP block",
version.getModuleLabel());
}
@@ -236,6 +237,46 @@ public class ArduinoNanoTest {
"D2", "D3", "D4", "D5", "D6", "D7", "D8", "D9", "D10", "D11", "D12", "D13");
assertPwm(ArduinoNano.NanoVersion.NANO_RP2040_CONNECT,
"D2", "D3", "D4", "D5", "D6", "D7", "D8", "D9", "D10", "D11", "D12", "D13");
assertPwm(ArduinoNano.NanoVersion.NANO_R4, "D3", "D5", "D6", "D9", "D10", "D11");
}
@Test
public void testNanoR4() {
ArduinoNano r4 = new ArduinoNano();
r4.setVersion(ArduinoNano.NanoVersion.NANO_R4);
Assert.assertEquals("Nano R4", ArduinoNano.NanoVersion.NANO_R4.toString());
// 2x15 header pins plus a point at the centre of the Qwiic socket
Assert.assertEquals(31, r4.getControlPointCount());
// JP2, top to bottom: D4 and D5 double as the CAN pins
Assert.assertEquals("D4 (CAN TX)", r4.getControlPointNodeName(6));
Assert.assertEquals("D5 (~, CAN RX)", r4.getControlPointNodeName(7));
Assert.assertEquals("D12 (MISO)", r4.getControlPointNodeName(14));
// JP1 reversed: BOOT replaces the classic's second RESET, and A0-A3 reach the DAC and OPAMP
Assert.assertEquals("BOOT", r4.getControlPointNodeName(17));
Assert.assertEquals("5V", r4.getControlPointNodeName(18));
Assert.assertEquals("A3 (OPAMP OUT)", r4.getControlPointNodeName(23));
Assert.assertEquals("A0 (DAC)", r4.getControlPointNodeName(26));
Assert.assertEquals("D13 (SCK)", r4.getControlPointNodeName(29));
Assert.assertEquals("RA4M1", ArduinoNano.NanoVersion.NANO_R4.getMcuLabel());
Assert.assertEquals("QWIIC", ArduinoNano.NanoVersion.NANO_R4.getModuleLabel());
// The Qwiic point sits at the centre of the drawn socket: horizontally between the pin rows,
// and vertically inside the connector body at the top of the board
Assert.assertEquals("QWIIC", r4.getControlPointNodeName(30));
Point2D qwiic = r4.getControlPoint(30);
Point2D leftPin = r4.getControlPoint(0);
Point2D rightPin = r4.getControlPoint(15);
Assert.assertEquals((leftPin.getX() + rightPin.getX()) / 2.0, qwiic.getX(), 0.1);
Rectangle2D bounds = r4.getBodyShape().getBounds2D();
double connectorTop = bounds.getY() + ArduinoNano.END_PART_OFFSET_Y.convertToPixels();
Assert.assertTrue("Qwiic point should sit inside the connector body",
qwiic.getY() > connectorTop
&& qwiic.getY() < connectorTop + ArduinoNano.QWIIC_LENGTH.convertToPixels());
}
@Test
@@ -312,6 +353,18 @@ public class ArduinoNanoTest {
private static final double PIN_SPACING_PX = new Size(0.1d, SizeUnit.in).convertToPixels();
/** 36 for the classic with its ICSP block, 31 for the R4 with its Qwiic point, 30 otherwise. */
private static int expectedPinCount(ArduinoNano.NanoVersion version) {
switch (version) {
case CLASSIC:
return 36;
case NANO_R4:
return 31;
default:
return 30;
}
}
@Test
public void testAllVersionsDrawing() {
java.awt.image.BufferedImage img =
+13 -3
View File
@@ -86,9 +86,13 @@ public void setVersion(XxxVersion version) {
}
```
New variants must be **appended** to the enum (XStream serializes enum constants by name;
appending is backward compatible, reordering or removing is not). Keep the default constant and
the null-guard so existing `.diy` files deserialize unchanged.
XStream serializes enum constants **by name** — `.diy` files contain `<orientation>_90</orientation>`
and the like, and no custom enum converter is registered — so adding constants and reordering them
are both backward compatible. **Removing or renaming one is not.** Ordinals are used only by the
rotation transformers (`Orientation`, `OrientationHV`, `Orientation45`), never by a `Version` enum,
so a version enum may be sorted for a tidy drop-down; `ArduinoNano.NanoVersion` is ordered by label
for exactly that reason. Keep the default constant and the null-guard so existing files deserialize
unchanged.
### 4.3 Serialization safety checklist (per changed class)
@@ -267,6 +271,12 @@ wrong: the PWM sets and the power pins genuinely differ.
| NANO_33_BLE / _SENSE | D2–D13 (all) | RST2 | 5V | AREF |
| NANO_RP2040_CONNECT | D2–D13 (all) | **REC** (BOOTSEL) | 5V | AREF |
| NANO_ESP32 | D0–D13 and A0–A7 | **B1** | **VUSB** | **B0** |
| NANO_R4 | D3 D5 D6 D9 D10 D11 | **BOOT** | 5V | AREF |
The Nano R4 (ABX00142, Renesas RA4M1) additionally annotates D4/D5 as the CAN pins, A0 as the DAC
and A1–A3 as the on-chip OPAMP inputs and output, and its end-of-board part is the **Qwiic**
JST-SH socket rather than a radio module — drawn with `drawFpcConnector`, since it is a connector
and not a shielded can.
`PIN_NAMES_ESP32` additionally dual-labels with the Espressif GPIO number, e.g. `D2 (~, GPIO5)`;
those numbers are taken from the ABX00083 pinout diagram.