Developing on Windows#

Every chapter builds and uploads on Windows with the same commands used on Linux and macOS. What differs is the port name (COM3 rather than /dev/ttyACM0) and how the tools are installed.

A handful of errors, however, only appear on Windows, and they are hard to read if you have not seen them before. This page shows each one as it actually prints. It also covers talking to the board from Python, which the book does not describe anywhere.

Note

Every output on this page was captured from a real Arduino UNO R4 WiFi attached to Windows 11, driven from PowerShell 7. The Windows error messages are localized by the OS, so on an English install the wording differs while the error class and numeric code stay the same.

1. Set up the environment#

Open PowerShell#

The quickest way is right-click the Start button → “Terminal” (Win + X opens the same menu). Typing PowerShell into the Start menu works as well. No administrator rights are needed.

Windows 11 ships with Windows PowerShell 5.1, and every command on this page runs there as written. To see which one you opened:

$PSVersionTable.PSVersion

The newer PowerShell 7 (invoked as pwsh, not powershell) is fine too — the outputs on this page were captured with PowerShell 7.6.2.

Command Prompt (cmd.exe) can build and upload as well, but the examples here follow PowerShell syntax.

Install uv#

uv manages the Python environment and its packages in one tool; it is all the sample code needs.

Open PowerShell and run:

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

winget install --id=astral-sh.uv -e works too. Other options are in uv’s installation guide.

The installer drops uv in %USERPROFILE%\.local\bin and adds that to PATH (the list of folders the OS searches so that you can run a command by its name alone), so uv is not found until you reopen PowerShell. Reopen it, then check:

uv --version
uv 0.9.26

Get the sample code#

Windows does not come with git, so install it first and reopen PowerShell (Git for Windows).

winget install --id Git.Git -e --source winget
git clone https://github.com/iory/learning-os-from-arduino.git
cd learning-os-from-arduino\docs\os-on-arduino\code
uv sync

uv sync installs PlatformIO and pyserial. Every command below is run through uv run.

2. Find the board#

Plug the board in over USB, then:

uv run pio device list
COM3
----
Hardware ID: USB VID:PID=2341:1002 SER=B43A45B9F1CC LOCATION=-4:x.1
Description: USB シリアル デバイス (COM3)

The number varies per machine (COM4, COM5, …). The same entry appears in Device Manager under Ports (COM & LPT).

../_images/windows_device_manager.png

How the board appears in Device Manager (Japanese Windows shown; an English install reads “USB Serial Device (COM3)”)#

The name is the confusing part: it is not shown as Arduino UNO R4 WiFi. The board runs on the stock Windows USB CDC driver, so it appears as “USB Serial Device (COMn)”. Installing the Arduino IDE does not change this.

What identifies it is VID:PID=2341:1002 in the Hardware ID. 2341 is Arduino’s vendor ID; use it to tell the board apart from other USB serial devices (section 6 does exactly that).

3. Build and upload#

Pass the chapter directory to -d:

uv run pio run -d 05_shell -t upload
Configuring upload protocol...
AVAILABLE: cmsis-dap, jlink, sam-ba
CURRENT: upload_protocol = sam-ba
Looking for upload port...
Auto-detected: COM3
Forcing reset using 1200bps open/close on port COM3
Uploading .pio\build\uno_r4_wifi\firmware.bin
Erase flash

Done in 0.000 seconds
Write 41200 bytes to flash (11 pages)

[==============================] 100% (11/11 pages)
Done in 2.803 seconds
========================= [SUCCESS] Took 7.23 seconds =========================

As Auto-detected: COM3 shows, the port is normally found for you. Add --upload-port COM3 only when another USB serial device is picked instead.

../_images/windows_upload.gif

Checking the port and uploading, recorded from a real PowerShell session#

4. Watch the serial output#

uv run pio device monitor -b 115200
--- Terminal on COM3 | 115200 8-N-1
--- Available filters and text transformations: debug, default, direct, hexlify, log2file, nocontrol, printable, send_on_enter, time
--- More details at https://bit.ly/pio-monitor-filters
--- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H

Ctrl-C leaves the monitor.

Important

Nothing after those four lines is normal, not a fault.

Opening the serial port does not reset the UNO R4 WiFi. USB serial is handled by the on-board ESP32-S3, which runs independently of the RA4M1 your program is on. Unlike most Arduino-compatible boards, the sketch does not restart the moment a monitor attaches.

So for chapters that only print inside setup(), the startup banner has already gone by the time the monitor opens. To see it, open the monitor right after uploading, or press RESET once. In the Chapter 5 shell, typing reboot does the same thing.

Why chapter 1 waits for an S

Chapter 1’s sketch prints nothing until you send a single S from the monitor. Type S once the monitor is open.

The board starts running the instant it is powered, while you need a few seconds to open the monitor after flashing. In a chapter that prints once from setup() and stops, written naively it would finish printing before your monitor is even open, and you would see nothing.

The S is how the PC says “I am ready”:

while (Serial.read() != 'S') { delay(1); }

01_boot, 01_boot_vector_dump, adv3_heap and adv4_fs work this way. Chapters that keep printing (chapter 2, for example) do not need it.

5. Where Windows trips you up#

You cannot upload while the monitor is open#

On Windows one process holds a serial port exclusively. Uploading with the monitor still open gives:

Looking for upload port...
Auto-detected: COM3
*** [upload] could not open port 'COM3': PermissionError(13, 'アクセスが拒否されました。', None, 5)
========================== [FAILED] Took 0.94 seconds ==========================

The trailing 5 is the Windows error code ERROR_ACCESS_DENIED (“Access is denied.”); the message text follows the OS language. It is the equivalent of Device or resource busy on Linux. Close the monitor with Ctrl-C and upload again.

The same happens when the Arduino IDE’s serial monitor, or a Python script of your own, holds the port. Only one program at a time may open a COM port.

Wrong COM number#

Pointing at a port that does not exist gives a different error:

Looking for upload port...
Using manually specified: COM9
*** [upload] could not open port 'COM9': FileNotFoundError(2, '指定されたファイルが見つかりません。', None, 2)
========================== [FAILED] Took 0.94 seconds ==========================

FileNotFoundError means the port number is wrong; PermissionError means it is in use. Knowing which is which makes this a five-second diagnosis. Run uv run pio device list to confirm the number.

The number is not fixed. Unplug and replug the board, or attach another USB serial device first, and the same board can move from COM3 to COM5. If FileNotFoundError appears out of nowhere, check this first — Device Manager’s “Ports (COM & LPT)” shows it too.

The same happens on Linux (/dev/ttyACM0 becomes /dev/ttyACM1). Section 6 shows how to write code that does not depend on the number.

It stops with No device found on COMn#

The board is not in bootloader mode (the bootloader is a small program that stays on the microcontroller to accept new firmware). Press RESET twice quickly to drop into the bootloader — the L LED fading slowly in and out means it worked — then upload again.

This is also what you see right after halting the CPU with a debugger (CMSIS-DAP, or VS Code debugging): the 1200 bps reset the uploader relies on no longer takes effect.

Other things#

  • Plug into the PC directly rather than through a USB hub; uploads through hubs sometimes fail

  • Check the cable carries data — a charge-only cable produces no port at all

6. Talking to the board from Python#

Driving the board from Python instead of a serial monitor lets you save the output, or read a reply and decide what to send next. The Chapter 13 quadruped is brought up exactly this way.

The library is pyserial, already installed by uv sync.

The smallest example#

Run this with Chapter 5 (05_shell) on the board:

import sys
import time

import serial

ser = serial.Serial("COM3", 115200, timeout=0.2)
time.sleep(1.0)
ser.reset_input_buffer()

for cmd in (b"help\n", b"info\n"):
    ser.write(cmd)
    time.sleep(0.6)
    text = ser.read(8192).decode("utf-8", "replace")
    sys.stdout.write(text.replace("\r\n", "\n"))

ser.close()

First save the code above as hello_serial.py in the docs\os-on-arduino\code folder, then run it:

uv run python hello_serial.py
help
Available commands:
  ps      - List all tasks
  kill N  - Suspend task N
  exec N  - Resume task N
  info    - System information
  reboot  - Reboot system
  help    - Show this help
> info

=== System Information ===
System Ticks:  67770
Uptime:        67 s
Task Count:    5
Time Slice:    10 ms

>
../_images/windows_pyserial.gif

Running hello_serial.py and getting the board’s shell to answer#

Send ps and the task table comes back the same way:

ID  NAME           STATE      CPU%
--  ----           -----      ----
0   idle           READY     100%
1   LED1           BLOCKED   0%
2   LED2           BLOCKED   0%
3   Heartbeat      BLOCKED   0%
4   Shell          RUNNING   0%

Stop hard-coding the port number#

COM3 breaks as soon as you move to another machine or the port shifts. Searching by Arduino’s vendor ID (0x2341) makes one piece of code work on Windows, Linux and macOS alike.

import serial.tools.list_ports

ARDUINO_VID = 0x2341


def find_board():
    """Return the board's port: COM3 on Windows, /dev/ttyACM0 on Linux."""
    for port in serial.tools.list_ports.comports():
        if port.vid == ARDUINO_VID:
            return port.device
    raise RuntimeError("board not found")


print(find_board())
COM3

comports() reads the registry on Windows and /sys on Linux. Only the returned name differs; the call is identical.

Three things worth knowing up front#

Line breaks come out doubled. The board terminates lines with CRLF. Printing that straight back doubles the spacing on Windows, because Python translates the \n into another \r\n. Pass it through text.replace("\r\n", "\n") as above.

Only one program can hold the port. The serial monitor, the Arduino IDE and your own script are mutually exclusive. If your script raises PermissionError, close the monitor first.

Opening the port does not restart the board. As in section 4. Waiting for setup() output right after Serial() waits forever. To catch the startup output, open the port immediately after uploading — the COM port does not disappear across an upload on the UNO R4 WiFi, so opening it straight away catches the boot messages:

=== Interactive Shell Demo ===

Tasks created. Starting OS...


Mini OS Shell
Type 'help' for commands.
>

7. Driving the quadruped from the PC#

You can skip this until Chapter 13.

The fastest way to bring the Chapter 13 quadruped up is to drive the servos from the PC before flashing anything. Those tools live in docs/os-on-arduino/code/13_quadruped/host/ and run on Windows as they are.

cd 13_quadruped\host                   # from docs\os-on-arduino\code
uv run python quad_host.py --port COM3 scan        # how many servos answer
uv run python quad_host.py --port COM3 calibrate   # measure sign and zero -> calib.json
uv run python quad_host.py --port COM3 stand       # hold the home stance
uv run python quad_host.py --port COM3 teleop      # walk it with w/s/a/d

--port takes the COM number of the servo bus USB adapter. With the Arduino used as a bridge (--bus bridge) it is the Arduino’s COM number. Omit it and the tool uses the port recorded in calib.json, falling back to probing the attached USB serial devices.

teleop reads single keypresses, so run it directly in a PowerShell window. Ctrl-C releases servo torque.

Warning

Power the eight servos from the 12 V AC adapter or a battery. Never from the PC’s USB (2.7 A stall current each). Assembly and safety notes are on the Japanese site.

四脚ロボットを歩かせる — on the Japanese site ↗

8. Verify every chapter at once#

A bundled script builds, uploads and checks the serial output of every chapter in one run.

# run in docs\os-on-arduino\code (after step 7, go back with cd ..\..)
uv run python scripts\verify_chapters.py --port COM3
OS: Windows 11  Python: 3.12.12
ボード: COM3

=== 01_boot =====================================================
  -> PASS  (15.5s)
     OK  === Boot Sequence Check ===
     OK  BSS がゼロクリアされている
     OK  DATA が Flash からコピーされている
     OK  ベクタテーブルが RAM 上にある
     OK  If you see this, boot sequence completed!

=== 05_shell ====================================================
  -> PASS  (19.7s)
     OK  Available commands:
     OK  ID\s+NAME\s+STATE\s+CPU%
     OK  idle タスクが ID 0 で並ぶ
     OK  CPU% の合計 = 99%(5 タスク)

--only narrows it to specific chapters (the run above used --only 01_boot 05_shell), and --build-only checks the builds without a board attached. The script’s messages are in Japanese.

全章の動作確認 — full options and interactive mode, on the Japanese site ↗