From cc8fad0168bf68e77c2855cbf0b5d5779d4399c6 Mon Sep 17 00:00:00 2001 From: zombieleet Date: Fri, 25 Sep 2026 20:16:38 +0200 Subject: [PATCH 1/3] builder: add the esp32h2 firmware image format Add the ESP32-H2 chip ID (0x0010) and the flash speed/size byte to the ESP image header. For the ESP32-H2, the flash clock code 0xF is 48 MHz, which is the maximum. The flasher changes the flash size to the real value. See esptool targets/esp32h2.py (IMAGE_CHIP_ID, FLASH_FREQUENCY). --- builder/build.go | 2 +- builder/esp.go | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/builder/build.go b/builder/build.go index 1fffbca073..1e282aac3e 100644 --- a/builder/build.go +++ b/builder/build.go @@ -1094,7 +1094,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe if err != nil { return result, err } - case "esp32", "esp32-img", "esp32c3", "esp32s3", "esp32c6", "esp8266": + case "esp32", "esp32-img", "esp32c3", "esp32s3", "esp32c6", "esp32h2", "esp8266": // Special format for the ESP family of chips (parsed by the ROM // bootloader). result.Binary = filepath.Join(tmpdir, "main"+outext) diff --git a/builder/esp.go b/builder/esp.go index 9b59047a24..9f6545623e 100644 --- a/builder/esp.go +++ b/builder/esp.go @@ -169,6 +169,7 @@ func makeESPFirmwareImage(infile, outfile, format string) error { "esp32": 0x0000, "esp32c3": 0x0005, "esp32c6": 0x000d, + "esp32h2": 0x0010, "esp32s3": 0x0009, }[chip] @@ -180,12 +181,13 @@ func makeESPFirmwareImage(infile, outfile, format string) error { "esp32": 0x1f, // 80MHz=0x0F, 2MB=0x10 "esp32c3": 0x1f, // 80MHz=0x0F, 2MB=0x10 "esp32c6": 0x10, // 80MHz=0x00, 2MB=0x10 (C6 uses different freq encoding) + "esp32h2": 0x1f, // 48MHz=0x0F, 2MB=0x10 "esp32s3": 0x1f, // 80MHz=0x0F, 2MB=0x10 }[chip] // Image header. switch chip { - case "esp32", "esp32c3", "esp32s3", "esp32c6": + case "esp32", "esp32c3", "esp32s3", "esp32c6", "esp32h2": // Header format: // https://github.com/espressif/esp-idf/blob/v4.3/components/bootloader_support/include/esp_app_format.h#L71 // Note: not adding a SHA256 hash as the binary is modified by From a89675be8153c7ab5848b6f06d1805fad7b8eea1 Mon Sep 17 00:00:00 2001 From: zombieleet Date: Fri, 25 Sep 2026 20:17:29 +0200 Subject: [PATCH 2/3] runtime,machine,targets: add ESP32-H2 support Add the esp32h2 chip target. The ESP32-H2 uses the same RISC-V core, PLIC and USB Serial/JTAG block as the ESP32-C6, so it uses the ESP32-C6 startup assembly and the shared PLIC, interrupt and USB files with a build tag. Differences from the ESP32-C6: - 320 KB SRAM and different ROM function addresses in the linker script. - The CPU runs at 32 MHz from the XTAL. TIMG0 uses the XTAL divided by 4, which gives 125 ns for each tick. - The UART uses the 32 MHz XTAL (PCR SCLK_SEL 3). The default UART0 pins are GPIO24 (TX) and GPIO23 (RX). - The UART interrupt uses CPU interrupt 13. Interrupts 3, 4 and 7 are bound to the CLINT and cannot be used. The linker script also pads the end of .text so that .rodata is at a 16-byte aligned flash offset, and it checks with ASSERT that .text and .rodata have the same address as their flash offset. References: ESP-IDF components/soc/esp32h2, components/esp_rom/esp32h2, components/esp_hal_uart/esp32h2, components/esp_hal_timg/esp32h2 and components/esp_hw_support/port/esp32h2/esp_cpu_intr.c. --- builder/builder_test.go | 1 + src/device/esp/esp32c6.S | 15 +- src/machine/machine_esp32c6_usb.go | 28 +- src/machine/machine_esp32h2.go | 544 +++++++++++++++++++++ src/runtime/interrupt/interrupt_esp32c6.go | 19 +- src/runtime/runtime_esp32h2.go | 74 +++ src/runtime/runtime_esp32xx.go | 2 +- src/runtime/runtime_esp32xx_exit.go | 2 +- src/runtime/runtime_esp32xx_plic.go | 2 +- targets/esp32h2.json | 32 ++ targets/esp32h2.ld | 172 +++++++ 11 files changed, 857 insertions(+), 34 deletions(-) create mode 100644 src/machine/machine_esp32h2.go create mode 100644 src/runtime/runtime_esp32h2.go create mode 100644 targets/esp32h2.json create mode 100644 targets/esp32h2.ld diff --git a/builder/builder_test.go b/builder/builder_test.go index f10e66e636..194994b8a8 100644 --- a/builder/builder_test.go +++ b/builder/builder_test.go @@ -30,6 +30,7 @@ func TestClangAttributes(t *testing.T) { "cortex-m7", "esp32c3", "esp32c6", + "esp32h2", "esp32s3", "fe310", "gameboy-advance", diff --git a/src/device/esp/esp32c6.S b/src/device/esp/esp32c6.S index 7afd39f681..eb1007bbbc 100644 --- a/src/device/esp/esp32c6.S +++ b/src/device/esp/esp32c6.S @@ -1,12 +1,13 @@ -// This is a very minimal bootloader for the ESP32-C6. It only initializes the -// flash and then continues with the generic RISC-V initialization code, which -// in turn will call runtime.main. +// This is a very minimal bootloader for the ESP32-C6 and ESP32-H2. It only +// initializes the flash and then continues with the generic RISC-V +// initialization code, which in turn will call runtime.main. // It is written in assembly (and not in a higher level language) to make sure // it is entirely loaded into IRAM and doesn't accidentally call functions // stored in IROM. // -// The ESP32-C6 has a unified IRAM/DRAM address space at 0x40800000, and -// separate DROM (0x42800000) / IROM (0x42000000) flash-mapped regions. +// The ESP32-C6 and ESP32-H2 have a unified IRAM/DRAM address space at +// 0x40800000, and separate DROM (0x42800000) / IROM (0x42000000) flash-mapped +// regions. .section .init .global call_start_cpu0 @@ -26,8 +27,8 @@ call_start_cpu0: call Cache_MMU_Init // Set up flash mapping (both IROM and DROM). - // On ESP32-C6, Cache_Dbus_MMU_Set is replaced by Cache_MSPI_MMU_Set - // which has an extra "sensitive" parameter. + // On ESP32-C6 and ESP32-H2, Cache_Dbus_MMU_Set is replaced by + // Cache_MSPI_MMU_Set which has an extra "sensitive" parameter. // C equivalent: // Cache_MSPI_MMU_Set(0, 0, 0x42000000, 0, 64, 256, 0) // Maps 16MB starting at 0x42000000, covering both IROM and DROM. diff --git a/src/machine/machine_esp32c6_usb.go b/src/machine/machine_esp32c6_usb.go index f05d54c5ae..500bad0112 100644 --- a/src/machine/machine_esp32c6_usb.go +++ b/src/machine/machine_esp32c6_usb.go @@ -1,4 +1,4 @@ -//go:build esp32c6 +//go:build esp32c6 || esp32h2 package machine @@ -11,10 +11,10 @@ import ( ) // USB Serial/JTAG Controller -// The ESP32-C6 has the same built-in USB Serial/JTAG hardware IP as the -// ESP32-C3. The only difference at the machine level is how the peripheral -// clock is enabled: C3 uses SYSTEM.PERIP_CLK_EN0, while C6 uses the PCR -// (Peripheral Clock Reset) block. +// The ESP32-C6 and ESP32-H2 have the same built-in USB Serial/JTAG hardware IP +// as the ESP32-C3. The only difference at the machine level is how the +// peripheral clock is enabled: C3 uses SYSTEM.PERIP_CLK_EN0, while C6 and H2 +// use the PCR (Peripheral Clock Reset) block. const cpuInterruptFromUSB = 10 @@ -51,9 +51,9 @@ type Serialer interface { var usbConfigured bool -// USBDevice provides a stub USB device for the ESP32-C6. The hardware -// only supports a fixed-function CDC-ACM serial port, so the programmable -// USB device features are no-ops. +// USBDevice provides a stub USB device for the ESP32-C6 and ESP32-H2. The +// hardware only supports a fixed-function CDC-ACM serial port, so the +// programmable USB device features are no-ops. type USBDevice struct { initcomplete bool InitEndpointComplete bool @@ -84,8 +84,8 @@ func (usbdev *USB_DEVICE) Configure(config UARTConfig) error { } usbConfigured = true - // Enable the USB_DEVICE peripheral clock via PCR (C6 uses PCR instead - // of SYSTEM.PERIP_CLK_EN0 that C3 used). + // Enable the USB_DEVICE peripheral clock via PCR (C6 and H2 use PCR + // instead of SYSTEM.PERIP_CLK_EN0 that C3 used). esp.PCR.SetUSB_DEVICE_CONF_USB_DEVICE_CLK_EN(1) esp.PCR.SetUSB_DEVICE_CONF_USB_DEVICE_RST_EN(0) @@ -240,16 +240,16 @@ func FlushSerial() { } } -// ConfigureUSBEndpoint is a no-op on ESP32-C6 — the hardware does not -// support programmable USB endpoints. +// ConfigureUSBEndpoint is a no-op on ESP32-C6 and ESP32-H2. The hardware does +// not support programmable USB endpoints. func ConfigureUSBEndpoint(desc descriptor.Descriptor, epSettings []usb.EndpointConfig, setup []usb.SetupConfig) { } -// SendZlp is a no-op on ESP32-C6. +// SendZlp is a no-op on ESP32-C6 and ESP32-H2. func SendZlp() { } -// SendUSBInPacket is a no-op on ESP32-C6. +// SendUSBInPacket is a no-op on ESP32-C6 and ESP32-H2. func SendUSBInPacket(ep uint32, data []byte) bool { return false } diff --git a/src/machine/machine_esp32h2.go b/src/machine/machine_esp32h2.go new file mode 100644 index 0000000000..e3362a7482 --- /dev/null +++ b/src/machine/machine_esp32h2.go @@ -0,0 +1,544 @@ +//go:build esp32h2 + +package machine + +import ( + "device/esp" + "device/riscv" + "errors" + "runtime/interrupt" + "runtime/volatile" + "sync" + "unsafe" +) + +const deviceName = esp.Device +const maxPin = 28 +const cpuInterruptFromPin = 6 + +// CPU interrupts 3, 4 and 7 are bound to the CLINT and cannot be used. +// See ESP-IDF esp_hw_support/port/esp32h2/esp_cpu_intr.c. +const cpuInterruptFromUART = 13 + +// CPUFrequency returns the current CPU frequency of the chip. +// Currently it is a fixed frequency but it may allow changing in the future. +func CPUFrequency() uint32 { + return 32e6 // 32MHz +} + +const ( + PinOutput PinMode = iota + PinInput + PinInputPullup + PinInputPulldown + PinAnalog +) + +const ( + GPIO0 Pin = 0 + GPIO1 Pin = 1 + GPIO2 Pin = 2 + GPIO3 Pin = 3 + GPIO4 Pin = 4 + GPIO5 Pin = 5 + GPIO6 Pin = 6 + GPIO7 Pin = 7 + GPIO8 Pin = 8 + GPIO9 Pin = 9 + GPIO10 Pin = 10 + GPIO11 Pin = 11 + GPIO12 Pin = 12 + GPIO13 Pin = 13 + GPIO14 Pin = 14 + GPIO15 Pin = 15 + GPIO16 Pin = 16 + GPIO17 Pin = 17 + GPIO18 Pin = 18 + GPIO19 Pin = 19 + GPIO20 Pin = 20 + GPIO21 Pin = 21 + GPIO22 Pin = 22 + GPIO23 Pin = 23 + GPIO24 Pin = 24 + GPIO25 Pin = 25 + GPIO26 Pin = 26 + GPIO27 Pin = 27 +) + +type PinChange uint8 + +// Pin change interrupt constants for SetInterrupt. +const ( + PinRising PinChange = iota + 1 + PinFalling + PinToggle +) + +// Configure this pin with the given configuration. +func (p Pin) Configure(config PinConfig) { + if p == NoPin { + // This simplifies pin configuration in peripherals such as SPI. + return + } + + var muxConfig uint32 + + // Configure this pin as a GPIO pin. + const function = 1 // function 1 is GPIO for every pin + muxConfig |= function << esp.IO_MUX_GPIO_MCU_SEL_Pos + + // FUN_IE: disable for PinAnalog (high-Z for ADC) + if config.Mode != PinAnalog { + muxConfig |= esp.IO_MUX_GPIO_FUN_IE + } + + // Set drive strength: 0 is lowest, 3 is highest. + muxConfig |= 2 << esp.IO_MUX_GPIO_FUN_DRV_Pos + + // Select pull mode (no pulls for PinAnalog). + if config.Mode == PinInputPullup { + muxConfig |= esp.IO_MUX_GPIO_FUN_WPU + } else if config.Mode == PinInputPulldown { + muxConfig |= esp.IO_MUX_GPIO_FUN_WPD + } + + // Configure the pad with the given IO mux configuration. + p.mux().Set(muxConfig) + + // Set the output signal to the simple GPIO output. + p.outFunc().Set(0x80) + + switch config.Mode { + case PinOutput: + // Set the 'output enable' bit. + esp.GPIO.ENABLE_W1TS.Set(1 << p) + case PinInput, PinInputPullup, PinInputPulldown, PinAnalog: + // Clear the 'output enable' bit. + esp.GPIO.ENABLE_W1TC.Set(1 << p) + } +} + +// configure is the same as Configure, but allows setting a specific GPIO matrix signal. +func (p Pin) configure(config PinConfig, signal uint32) { + p.Configure(config) + if signal == 256 { + return + } + if config.Mode == PinOutput { + p.outFunc().Set(signal) + } else { + inFunc(signal).Set(esp.GPIO_FUNC_IN_SEL_CFG_SEL | uint32(p)) + } +} + +// outFunc returns the FUNCx_OUT_SEL_CFG register used for configuring the +// output function selection. +func (p Pin) outFunc() *volatile.Register32 { + return (*volatile.Register32)(unsafe.Add(unsafe.Pointer(&esp.GPIO.FUNC0_OUT_SEL_CFG), uintptr(p)*4)) +} + +func (p Pin) pinReg() *volatile.Register32 { + return (*volatile.Register32)(unsafe.Pointer((uintptr(unsafe.Pointer(&esp.GPIO.PIN0)) + uintptr(p)*4))) +} + +// inFunc returns the FUNCy_IN_SEL_CFG register used for configuring the input +// function selection. +func inFunc(signal uint32) *volatile.Register32 { + return (*volatile.Register32)(unsafe.Add(unsafe.Pointer(&esp.GPIO.FUNC0_IN_SEL_CFG), uintptr(signal)*4)) +} + +// mux returns the I/O mux configuration register corresponding to the given +// GPIO pin. +func (p Pin) mux() *volatile.Register32 { + return (*volatile.Register32)(unsafe.Add(unsafe.Pointer(&esp.IO_MUX.GPIO0), uintptr(p)*4)) +} + +// pin returns the PIN register corresponding to the given GPIO pin. +func (p Pin) pin() *volatile.Register32 { + return (*volatile.Register32)(unsafe.Add(unsafe.Pointer(&esp.GPIO.PIN0), uintptr(p)*4)) +} + +// Set the pin to high or low. +// Warning: only use this on an output pin! +func (p Pin) Set(value bool) { + if value { + reg, mask := p.portMaskSet() + reg.Set(mask) + } else { + reg, mask := p.portMaskClear() + reg.Set(mask) + } +} + +// Get returns the current value of a GPIO pin when configured as an input or as +// an output. +func (p Pin) Get() bool { + reg := &esp.GPIO.IN + return (reg.Get()>>p)&1 > 0 +} + +// Return the register and mask to enable a given GPIO pin. This can be used to +// implement bit-banged drivers. +// +// Warning: only use this on an output pin! +func (p Pin) PortMaskSet() (*uint32, uint32) { + reg, mask := p.portMaskSet() + return ®.Reg, mask +} + +// Return the register and mask to disable a given GPIO pin. This can be used to +// implement bit-banged drivers. +// +// Warning: only use this on an output pin! +func (p Pin) PortMaskClear() (*uint32, uint32) { + reg, mask := p.portMaskClear() + return ®.Reg, mask +} + +func (p Pin) portMaskSet() (*volatile.Register32, uint32) { + return &esp.GPIO.OUT_W1TS, 1 << p +} + +func (p Pin) portMaskClear() (*volatile.Register32, uint32) { + return &esp.GPIO.OUT_W1TC, 1 << p +} + +// SetInterrupt sets an interrupt to be executed when a particular pin changes +// state. The pin should already be configured as an input, including a pull up +// or down if no external pull is provided. +// +// You can pass a nil func to unset the pin change interrupt. If you do so, +// the change parameter is ignored and can be set to any value (such as 0). +// If the pin is already configured with a callback, you must first unset +// this pins interrupt before you can set a new callback. +func (p Pin) SetInterrupt(change PinChange, callback func(Pin)) (err error) { + if p >= maxPin { + return ErrInvalidInputPin + } + + if callback == nil { + // Disable this pin interrupt + p.pin().ClearBits(esp.GPIO_PIN_INT_TYPE_Msk | esp.GPIO_PIN_INT_ENA_Msk) + + if pinCallbacks[p] != nil { + pinCallbacks[p] = nil + } + return nil + } + + if pinCallbacks[p] != nil { + // The pin was already configured. + // To properly re-configure a pin, unset it first and set a new + // configuration. + return ErrNoPinChangeChannel + } + pinCallbacks[p] = callback + + onceSetupPinInterrupt.Do(func() { + err = setupPinInterrupt() + }) + if err != nil { + return err + } + + p.pin().Set( + (p.pin().Get() & ^uint32(esp.GPIO_PIN_INT_TYPE_Msk|esp.GPIO_PIN_INT_ENA_Msk)) | + uint32(change)< 1 { + return errWrongStopBitSize + } + // - data length + uart.Bus.SetCONF0_BIT_NUM(uint32(dataBits - 5)) + // - stop bit + uart.Bus.SetCONF0_STOP_BIT_NUM(uint32(stopBits)) + // - parity check + switch parity { + case ParityNone: + uart.Bus.SetCONF0_PARITY_EN(0) + case ParityEven: + uart.Bus.SetCONF0_PARITY_EN(1) + uart.Bus.SetCONF0_PARITY(0) + case ParityOdd: + uart.Bus.SetCONF0_PARITY_EN(1) + uart.Bus.SetCONF0_PARITY(1) + } + return nil +} + +func initUARTClock(bus *esp.UART_Type) { + // The UART clock is in PCR. SCLK_SEL 3 is the 32MHz XTAL. + // See ESP-IDF esp_hal_uart/esp32h2/include/hal/uart_ll.h uart_ll_set_sclk. + switch bus { + case esp.UART0: + esp.PCR.SetUART0_CONF_UART0_CLK_EN(1) + esp.PCR.SetUART0_CONF_UART0_RST_EN(0) // ensure not in reset + bus.SetCLK_CONF_RST_CORE(1) + esp.PCR.SetUART0_CONF_UART0_RST_EN(1) + esp.PCR.SetUART0_CONF_UART0_RST_EN(0) + bus.SetCLK_CONF_RST_CORE(0) + esp.PCR.SetUART0_SCLK_CONF_UART0_SCLK_SEL(3) + esp.PCR.SetUART0_SCLK_CONF_UART0_SCLK_EN(1) + esp.PCR.SetUART0_SCLK_CONF_UART0_SCLK_DIV_NUM(0) + esp.PCR.SetUART0_SCLK_CONF_UART0_SCLK_DIV_A(0) + esp.PCR.SetUART0_SCLK_CONF_UART0_SCLK_DIV_B(0) + case esp.UART1: + esp.PCR.SetUART1_CONF_UART1_CLK_EN(1) + esp.PCR.SetUART1_CONF_UART1_RST_EN(0) + bus.SetCLK_CONF_RST_CORE(1) + esp.PCR.SetUART1_CONF_UART1_RST_EN(1) + esp.PCR.SetUART1_CONF_UART1_RST_EN(0) + bus.SetCLK_CONF_RST_CORE(0) + esp.PCR.SetUART1_SCLK_CONF_UART1_SCLK_SEL(3) + esp.PCR.SetUART1_SCLK_CONF_UART1_SCLK_EN(1) + esp.PCR.SetUART1_SCLK_CONF_UART1_SCLK_DIV_NUM(0) + esp.PCR.SetUART1_SCLK_CONF_UART1_SCLK_DIV_A(0) + esp.PCR.SetUART1_SCLK_CONF_UART1_SCLK_DIV_B(0) + } + // wait for Core Clock to ready for configuration + for bus.GetREG_UPDATE() > 0 { + riscv.Asm("nop") + } +} + +func (uart *UART) SetBaudRate(baudRate uint32) { + // based on esp-idf + max_div := uint64((1 << 12) - 1) + baud := uint64(baudRate) + sclk_div := (uartClockFreq + (max_div * baud) - 1) / (max_div * baud) + clk_div := (uartClockFreq << 4) / (baud * sclk_div) + uart.Bus.SetCLKDIV(uint32(clk_div >> 4)) + uart.Bus.SetCLKDIV_FRAG(uint32(clk_div & 0xf)) + // The SCLK divisor is in the PCR register. + switch uart.Bus { + case esp.UART0: + esp.PCR.SetUART0_SCLK_CONF_UART0_SCLK_DIV_NUM(uint32(sclk_div - 1)) + case esp.UART1: + esp.PCR.SetUART1_SCLK_CONF_UART1_SCLK_DIV_NUM(uint32(sclk_div - 1)) + } +} + +func (uart *UART) setupPins(config UARTConfig, regs registerSet) { + config.RX.Configure(PinConfig{Mode: PinInputPullup}) + config.TX.Configure(PinConfig{Mode: PinInputPullup}) + + // link TX with GPIO signal X (technical reference manual, GPIO matrix) + if config.InvertTX { + config.TX.outFunc().Set(regs.gpioMatrixSignal | esp.GPIO_FUNC_OUT_SEL_CFG_INV_SEL) + } else { + config.TX.outFunc().Set(regs.gpioMatrixSignal) + } + // link RX with GPIO signal X and route signals via GPIO matrix + if config.InvertRX { + inFunc(regs.gpioMatrixSignal).Set(esp.GPIO_FUNC_IN_SEL_CFG_SEL | uint32(config.RX) | esp.GPIO_FUNC_IN_SEL_CFG_IN_INV_SEL) + } else { + inFunc(regs.gpioMatrixSignal).Set(esp.GPIO_FUNC_IN_SEL_CFG_SEL | uint32(config.RX)) + } +} + +func (uart *UART) configureInterrupt(intrMapReg *volatile.Register32) { + // Disable all UART interrupts + uart.Bus.INT_ENA.ClearBits(0x0ffff) + + intrMapReg.Set(cpuInterruptFromUART) + onceUart.Do(func() { + _ = interrupt.New(cpuInterruptFromUART, func(i interrupt.Interrupt) { + UART0.serveInterrupt(0) + UART1.serveInterrupt(1) + }).Enable() + }) +} + +func (uart *UART) serveInterrupt(num int) { + // get interrupt status + interrutFlag := uart.Bus.INT_ST.Get() + if (interrutFlag & uartInterrupts) == 0 { + return + } + + // block UART interrupts while processing + uart.Bus.INT_ENA.ClearBits(uartInterrupts) + + if interrutFlag&esp.UART_INT_ENA_RXFIFO_FULL_INT_ENA > 0 { + for uart.Bus.GetSTATUS_RXFIFO_CNT() > 0 { + b := uart.Bus.GetFIFO_RXFIFO_RD_BYTE() + if !uart.Buffer.Put(byte(b & 0xff)) { + uart.DataOverflowDetected = true + } + } + } + if interrutFlag&esp.UART_INT_ENA_PARITY_ERR_INT_ENA > 0 { + uart.ParityErrorDetected = true + } + if 0 != interrutFlag&esp.UART_INT_ENA_FRM_ERR_INT_ENA { + uart.DataErrorDetected = true + } + if 0 != interrutFlag&esp.UART_INT_ENA_RXFIFO_OVF_INT_ENA { + uart.DataOverflowDetected = true + } + if 0 != interrutFlag&esp.UART_INT_ENA_GLITCH_DET_INT_ENA { + uart.DataErrorDetected = true + } + + // Clear the UART interrupt status + uart.Bus.INT_CLR.SetBits(interrutFlag) + uart.Bus.INT_CLR.ClearBits(interrutFlag) + // Enable interrupts + uart.Bus.INT_ENA.Set(uartInterrupts) +} + +const uart_empty_thresh_default = 10 + +func (uart *UART) enableTransmitter() { + uart.Bus.SetCONF0_TXFIFO_RST(1) + uart.Bus.SetCONF0_TXFIFO_RST(0) + uart.Bus.SetCONF1_TXFIFO_EMPTY_THRHD(uart_empty_thresh_default) +} + +func (uart *UART) enableReceiver() { + uart.Bus.SetCONF0_RXFIFO_RST(1) + uart.Bus.SetCONF0_RXFIFO_RST(0) + uart.Bus.SetCONF1_RXFIFO_FULL_THRHD(1) + uart.Bus.SetINT_ENA_RXFIFO_FULL_INT_ENA(1) + uart.Bus.SetINT_ENA_FRM_ERR_INT_ENA(1) + uart.Bus.SetINT_ENA_PARITY_ERR_INT_ENA(1) + uart.Bus.SetINT_ENA_GLITCH_DET_INT_ENA(1) + uart.Bus.SetINT_ENA_RXFIFO_OVF_INT_ENA(1) +} + +func (uart *UART) writeByte(b byte) error { + for (uart.Bus.STATUS.Get()&esp.UART_STATUS_TXFIFO_CNT_Msk)>>esp.UART_STATUS_TXFIFO_CNT_Pos >= 128 { + // Wait until there is space in the transmit buffer. + } + uart.Bus.FIFO.Set(uint32(b)) + return nil +} + +func (uart *UART) flush() {} diff --git a/src/runtime/interrupt/interrupt_esp32c6.go b/src/runtime/interrupt/interrupt_esp32c6.go index 83cc5e54df..590f956aa3 100644 --- a/src/runtime/interrupt/interrupt_esp32c6.go +++ b/src/runtime/interrupt/interrupt_esp32c6.go @@ -1,4 +1,4 @@ -//go:build esp32c6 +//go:build esp32c6 || esp32h2 package interrupt @@ -12,12 +12,11 @@ import ( //go:extern tinygo_saved_ra var tinygo_saved_ra uintptr -// plicType maps the ESP32-C6 PLIC (Platform-Level Interrupt Controller) -// machine-mode registers. The C6 uses the PLIC — not the INTPRI/INTC block -// that the ESP32-C3 uses — as its CPU interrupt controller -// (SOC_INT_PLIC_SUPPORTED). The INTPRI registers at 0x600c5000 are vestigial -// backward-compatibility aliases on the C6 and are not wired to the CPU, so -// writing them never delivers an interrupt. +// plicType maps the PLIC (Platform-Level Interrupt Controller) machine-mode +// registers. The ESP32-C6 and ESP32-H2 use the PLIC, not the INTPRI/INTC +// block of the ESP32-C3, as the CPU interrupt controller +// (SOC_INT_PLIC_SUPPORTED). +// The INTPRI registers at 0x600c5000 are not wired to the CPU. type plicType struct { MXINT_ENABLE volatile.Register32 // 0x00 bit N enables CPU interrupt line N MXINT_TYPE volatile.Register32 // 0x04 bit N: 1=edge, 0=level @@ -31,10 +30,10 @@ type plicType struct { var plic = (*plicType)(unsafe.Pointer(uintptr(0x20001000))) // Enable registers a CPU interrupt. -// The ESP32-C6 has 31 CPU independent interrupts (1..31). +// The ESP32-C6 and ESP32-H2 have 31 CPU independent interrupts (1..31). func (i Interrupt) Enable() error { if i.num < 1 || i.num > 31 { - return errors.New("interrupt for ESP32-C6 must be in range of 1 through 31") + return errors.New("interrupt must be in range of 1 through 31") } mask := riscv.DisableInterrupts() defer riscv.EnableInterrupts(mask) @@ -101,7 +100,7 @@ const ( const ( defaultThreshold = 5 - // Priority 0 disables an interrupt on ESP32-C6. + // Priority 0 disables an interrupt on ESP32-C6 and ESP32-H2. disableThreshold = 0 ) diff --git a/src/runtime/runtime_esp32h2.go b/src/runtime/runtime_esp32h2.go new file mode 100644 index 0000000000..f8439d81ce --- /dev/null +++ b/src/runtime/runtime_esp32h2.go @@ -0,0 +1,74 @@ +//go:build esp32h2 + +package runtime + +import ( + "device/esp" +) + +// TIMG0 counts at 8MHz. The 32MHz XTAL is divided by 4. +const ( + timerDivider = 4 + nsPerTick = 125 +) + +// This is the function called on startup after the flash (IROM/DROM) is +// initialized and the stack pointer has been set. +// +//export main +func main() { + // This initialization configures the following things: + // * It disables all watchdog timers. They might be useful at some point in + // the future, but will need integration into the scheduler. For now, + // they're all disabled. + // * It sets the CPU frequency to 32MHz from the XTAL. + + // Disable Timer Group 0 watchdog (unlock first). + esp.TIMG0.WDTWPROTECT.Set(0x50D83AA1) + esp.TIMG0.WDTCONFIG0.Set(0) + + // Disable Timer Group 1 watchdog (unlock first). + esp.TIMG1.WDTWPROTECT.Set(0x50D83AA1) + esp.TIMG1.WDTCONFIG0.Set(0) + + // Disable LP watchdog (write-protect key first). + esp.LP_WDT.WDTWPROTECT.Set(0x50D83AA1) + esp.LP_WDT.WDTCONFIG0.Set(0) + + // Disable super watchdog. + esp.LP_WDT.SWD_WPROTECT.Set(0x50D83AA1) + esp.LP_WDT.SWD_CONF.SetBits(1 << 30) // SWD_DISABLE bit + + // Run CPU, AHB and APB at 32MHz from the XTAL. The field values are divider-1. + // See ESP-IDF esp_hw_support/port/esp32h2/rtc_clk.c rtc_clk_cpu_freq_to_xtal. + esp.PCR.SetCPU_FREQ_CONF_CPU_DIV_NUM(0) + esp.PCR.SetAHB_FREQ_CONF_AHB_DIV_NUM(0) + esp.PCR.SetAPB_FREQ_CONF_APB_DIV_NUM(0) + esp.PCR.SetSYSCLK_CONF_SOC_CLK_SEL(0) + esp.PCR.SetBUS_CLK_UPDATE_BUS_CLOCK_UPDATE(1) + for esp.PCR.GetBUS_CLK_UPDATE_BUS_CLOCK_UPDATE() != 0 { + } + + // Use the 32MHz XTAL as the TIMG0 clock. 0 = XTAL, 1 = RC_FAST, 2 = PLL_F48M. + // See ESP-IDF esp_hal_timg/esp32h2/include/hal/timer_ll.h. + esp.PCR.SetTIMERGROUP0_CONF_TG0_CLK_EN(1) + esp.PCR.SetTIMERGROUP0_TIMER_CLK_CONF_TG0_TIMER_CLK_SEL(0) + esp.PCR.SetTIMERGROUP0_TIMER_CLK_CONF_TG0_TIMER_CLK_EN(1) + + clearbss() + + // Configure interrupt handler + interruptInit() + + // Initialize main system timer used for time.Now. + initTimer() + + // Initialize timer alarm interrupt for the scheduler. + initTimerInterrupt() + + // Initialize the heap, call main.main, etc. + run() + + // Fallback: if main ever returns, hang the CPU. + exit(0) +} diff --git a/src/runtime/runtime_esp32xx.go b/src/runtime/runtime_esp32xx.go index 6737317ded..8d94ec78c0 100644 --- a/src/runtime/runtime_esp32xx.go +++ b/src/runtime/runtime_esp32xx.go @@ -1,4 +1,4 @@ -//go:build esp32 || esp32c3 || esp32c6 +//go:build esp32 || esp32c3 || esp32c6 || esp32h2 package runtime diff --git a/src/runtime/runtime_esp32xx_exit.go b/src/runtime/runtime_esp32xx_exit.go index 25a2010ca5..004d84ab4a 100644 --- a/src/runtime/runtime_esp32xx_exit.go +++ b/src/runtime/runtime_esp32xx_exit.go @@ -1,4 +1,4 @@ -//go:build (esp32 && !qemu) || esp32c3 || esp32c6 +//go:build (esp32 && !qemu) || esp32c3 || esp32c6 || esp32h2 package runtime diff --git a/src/runtime/runtime_esp32xx_plic.go b/src/runtime/runtime_esp32xx_plic.go index a3c3093f61..9b307bbbf6 100644 --- a/src/runtime/runtime_esp32xx_plic.go +++ b/src/runtime/runtime_esp32xx_plic.go @@ -1,4 +1,4 @@ -//go:build esp32c6 +//go:build esp32c6 || esp32h2 package runtime diff --git a/targets/esp32h2.json b/targets/esp32h2.json new file mode 100644 index 0000000000..465ad319a2 --- /dev/null +++ b/targets/esp32h2.json @@ -0,0 +1,32 @@ +{ + "inherits": [ + "riscv32" + ], + "inheritable-only": true, + "features": "+32bit,+a,+c,+m,+zaamo,+zalrsc,+zmmul,-relax", + "build-tags": [ + "esp32h2", + "esp" + ], + "serial": "usb", + "rtlib": "compiler-rt", + "libc": "picolibc", + "default-stack-size": 8192, + "linkerscript": "targets/esp32h2.ld", + "extra-files": [ + "src/device/esp/esp32c6.S" + ], + "binary-format": "esp32h2", + "flash-method": "esp32jtag", + "serial-port": [ + "303a:1001" + ], + "openocd-interface": "esp_usb_jtag", + "openocd-target": "esp32h2", + "openocd-commands": [ + "gdb_memory_map disable" + ], + "gdb": [ + "riscv32-esp-elf-gdb" + ] +} diff --git a/targets/esp32h2.ld b/targets/esp32h2.ld new file mode 100644 index 0000000000..871fc43478 --- /dev/null +++ b/targets/esp32h2.ld @@ -0,0 +1,172 @@ +/* Linker script for the ESP32-H2. + * The memory layout is the same as the ESP32-C6. Only the SRAM size (320kB) + * and the ROM function addresses are different. + * See ESP-IDF components/soc/esp32h2/include/soc/soc.h. + * + * Keep .data and .iram before the dummy sections. lld can calculate a wrong + * value for a forward SIZEOF() reference, and then the image is corrupt. + */ +MEMORY +{ + /* HP-SRAM. IRAM and DRAM use the same address space. */ + SRAM (rwx) : ORIGIN = 0x40800000, LENGTH = 320K + + /* DROM and IROM are in separate, non-overlapping address ranges. */ + DROM (r) : ORIGIN = 0x42800000, LENGTH = 8M /* Data bus (read-only, flash-mapped) */ + IROM (rx) : ORIGIN = 0x42000000, LENGTH = 8M /* Instruction bus (flash-mapped) */ +} + +/* The entry point. It is set in the image flashed to the chip, so must be + * defined. + */ +ENTRY(call_start_cpu0) + +SECTIONS +{ + /* === SRAM sections (loaded by ROM bootloader) === */ + + /* Put the stack at the bottom of SRAM, so that the application will + * crash on stack overflow instead of silently corrupting memory. + * See: http://blog.japaric.io/stack-overflow-protection/ + */ + .stack (NOLOAD) : + { + . = ALIGN(16); + . += _stack_size; + _stack_top = .; + } >SRAM + + /* Global variables that are mutable and zero-initialized. + * These must be zeroed at startup (unlike data, which is loaded by the + * bootloader). + */ + .bss (NOLOAD) : ALIGN(4) + { + . = ALIGN (4); + _sbss = ABSOLUTE(.); + *(.sbss) + *(.bss .bss.*) + . = ALIGN (4); + _ebss = ABSOLUTE(.); + } >SRAM + + /* Mutable global variables. This data (in the SRAM segment) is initialized + * by the ROM bootloader. + */ + .data : ALIGN(4) + { + . = ALIGN (4); + _sdata = ABSOLUTE(.); + *(.sdata) + *(.data .data.*) + *(.dram*) + . = ALIGN (4); + _edata = ABSOLUTE(.); + } >SRAM + + /* Code that must run from IRAM (not flash-mapped), for example interrupt + * vectors and code that runs before the flash has been mapped. + * Since IRAM and DRAM share the same address space, this is placed + * sequentially after .data in the same SRAM region. + */ + .iram : ALIGN(4) + { + *(.init) /* call_start_cpu0 (before flash is mapped) */ + *(.iram*) /* code that must run from IRAM */ + *(.text.handleInterruptASM) /* must be in SRAM, within JAL range of _vector_table */ + . = ALIGN(256); + *(.text.exception_vectors) + . = ALIGN(4); + } > SRAM + + /* Heap starts after IRAM. */ + . = ALIGN(4); + _heap_start = ABSOLUTE(.); + _heap_end = ORIGIN(SRAM) + LENGTH(SRAM); + + /* === IROM sections (flash-mapped code) === */ + + /* Dummy section to align .text virtual address with its flash offset. + * In the firmware image, .data and .iram (SRAM) come before .text (IROM). + * SIZEOF(.data) and SIZEOF(.iram) are backward references (defined above). + * .data may be empty (esp.go skips empty sections), so use a ternary. + */ + .irom_dummy (NOLOAD) : ALIGN(0x10000) + { + . += 0x18; /* image header */ + . += (SIZEOF(.data) > 0) ? (SIZEOF(.data) + 0x8) : 0; /* data segment + header (if non-empty) */ + . += SIZEOF(.iram) + 0x8; /* iram segment + header */ + . += 0x8; /* text segment header */ + } > IROM + + /* Code stored in IROM (flash-mapped). + * This is the main program code. + */ + .text : ALIGN(4) + { + *(.text .text.*) + /* Put the .rodata data at a 16-byte aligned flash offset, the same as its address. */ + . = ALIGN(. + 0x8, 16) - 0x8; + } > IROM + + /* === DROM sections (flash-mapped read-only data) === */ + + /* Dummy section to align .rodata virtual address with its flash offset. + * Segments in the image are sorted by address: SRAM (.data, .iram), + * IROM (.text), then DROM (.rodata). So .rodata comes last, and we must + * account for all preceding segments. + * All SIZEOF() references are backward (sections defined above). + */ + .rodata_dummy (NOLOAD): ALIGN(4) + { + . += 0x18; /* image header (24 bytes) */ + . += (SIZEOF(.data) > 0) ? (SIZEOF(.data) + 0x8) : 0; /* data segment + header (if non-empty) */ + . += SIZEOF(.iram) + 0x8; /* iram segment + header */ + . += SIZEOF(.text) + 0x8; /* text segment + header */ + . += 0x8; /* rodata segment header */ + } > DROM + + /* Constant global variables, stored in DROM. */ + .rodata : ALIGN(4) + { + *(.rodata*) + . = ALIGN (4); + } >DROM + + ASSERT(ADDR(.text) == ADDR(.irom_dummy) + SIZEOF(.irom_dummy), ".text address does not match its flash offset") + ASSERT(ADDR(.rodata) == ADDR(.rodata_dummy) + SIZEOF(.rodata_dummy), ".rodata address does not match its flash offset") + + /DISCARD/ : + { + *(.eh_frame) /* we don't do exception handling in C */ + } + + /* For the garbage collector. */ + .tinygo_stacksizes (INFO) : + { + *(.tinygo_stacksizes) + } + .tinygo_arm_entries (INFO) : + { + *(.tinygo_arm_entries) + } +} + +/* For the GC and scheduler. */ +_globals_start = _sbss; +_globals_end = _edata; + +/* Default stack size. */ +_stack_size = 4K; + +/* ROM function addresses for ESP32-H2. + * See ESP-IDF components/esp_rom/esp32h2/ld/esp32h2.rom.ld. + */ +PROVIDE( ets_delay_us = 0x40000040 ); + +/* Cache ROM functions */ +Cache_Invalidate_ICache_All = 0x40000620; +Cache_Suspend_ICache = 0x4000066c; +Cache_Resume_ICache = 0x40000670; +Cache_MMU_Init = 0x40000688; +Cache_MSPI_MMU_Set = 0x4000068c; From 1fa4da97d8b21d1956dee01b2c6bf23ab223ecf9 Mon Sep 17 00:00:00 2001 From: zombieleet Date: Fri, 25 Sep 2026 20:17:55 +0200 Subject: [PATCH 3/3] machine,targets: add the ESP32-H2-DevKitM-1 board Add the Espressif ESP32-H2-DevKitM-1 board. The board target uses the port printed UART, which is a CP2102N USB to UART bridge on UART0. So it uses esp32flash with the classic reset, and machine.Serial uses the UART. The onboard LED is a WS2812 on GPIO8, not a plain LED. The board file defines WS2812 and BUTTON (the BOOT button on GPIO9), but not LED, the same as other boards that have only an addressable LED. The smoke test builds examples/machinetest and examples/serial. See the user guide: https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32h2/esp32-h2-devkitm-1/user_guide.html --- make/smoketest.mk | 5 +++++ src/machine/board_esp32h2-devkitm-1.go | 23 +++++++++++++++++++++++ targets/esp32h2-devkitm-1.json | 7 +++++++ 3 files changed, 35 insertions(+) create mode 100644 src/machine/board_esp32h2-devkitm-1.go create mode 100644 targets/esp32h2-devkitm-1.json diff --git a/make/smoketest.mk b/make/smoketest.mk index 3860dc1771..411c7be43a 100644 --- a/make/smoketest.mk +++ b/make/smoketest.mk @@ -510,6 +510,11 @@ ifneq ($(XTENSA), 0) @$(MD5SUM) $(SMOKE_OUT).bin $(TINYGO) build -size short -o $(SMOKE_OUT).bin -target=xiao-esp32c6 examples/blinkm @$(MD5SUM) $(SMOKE_OUT).bin + # esp32h2-devkitm-1 + $(TINYGO) build -size short -o $(SMOKE_OUT).bin -target=esp32h2-devkitm-1 examples/machinetest + @$(MD5SUM) $(SMOKE_OUT).bin + $(TINYGO) build -size short -o $(SMOKE_OUT).bin -target=esp32h2-devkitm-1 examples/serial + @$(MD5SUM) $(SMOKE_OUT).bin # xiao-esp32s3 $(TINYGO) build -size short -o $(SMOKE_OUT).bin -target=xiao-esp32s3 examples/blinky1 @$(MD5SUM) $(SMOKE_OUT).bin diff --git a/src/machine/board_esp32h2-devkitm-1.go b/src/machine/board_esp32h2-devkitm-1.go new file mode 100644 index 0000000000..c1f36f94c9 --- /dev/null +++ b/src/machine/board_esp32h2-devkitm-1.go @@ -0,0 +1,23 @@ +//go:build esp32h2_devkitm_1 + +// This file contains the pin mappings for the Espressif ESP32-H2-DevKitM-1 board. +// +// - https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32h2/esp32-h2-devkitm-1/user_guide.html + +package machine + +// Data pin of the onboard addressable RGB LED. The board has no plain LED. +const ( + WS2812 = GPIO8 +) + +// BOOT button +const ( + BUTTON = GPIO9 +) + +// UART pins +const ( + UART_TX_PIN = GPIO24 + UART_RX_PIN = GPIO23 +) diff --git a/targets/esp32h2-devkitm-1.json b/targets/esp32h2-devkitm-1.json new file mode 100644 index 0000000000..c6eed37472 --- /dev/null +++ b/targets/esp32h2-devkitm-1.json @@ -0,0 +1,7 @@ +{ + "inherits": ["esp32h2"], + "build-tags": ["esp32h2_devkitm_1"], + "serial": "uart", + "flash-method": "esp32flash", + "serial-port": ["10c4:ea60"] +}