diff --git a/ports/esp32/Makefile b/ports/esp32/Makefile
index f30a7cfadd05539461685bbf5ebac420db57fd1e..3802c2fc9c8d4167fb55bac3a0010995fdb2def6 100644
--- a/ports/esp32/Makefile
+++ b/ports/esp32/Makefile
@@ -1,7 +1,30 @@
+# Select the board to build for: if not given on the command line,
+# then default to GENERIC.
+BOARD ?= GENERIC
+
+# If the build directory is not given, make it reflect the board name.
+BUILD ?= build-$(BOARD)
+
+BOARD_DIR ?= boards/$(BOARD)
+ifeq ($(wildcard $(BOARD_DIR)/.),)
+$(error Invalid BOARD specified: $(BOARD_DIR))
+endif
+
 include ../../py/mkenv.mk
 
+# Optional (not currently used for ESP32)
+-include mpconfigport.mk
+
+ifneq ($(SDKCONFIG),)
+$(error Use the BOARD variable instead of SDKCONFIG)
+endif
+
+# Expected to set SDKCONFIG
+include $(BOARD_DIR)/mpconfigboard.mk
+
 # qstr definitions (must come before including py.mk)
 QSTR_DEFS = qstrdefsport.h
+QSTR_GLOBAL_DEPENDENCIES = $(BOARD_DIR)/mpconfigboard.h
 
 MICROPY_PY_USSL = 0
 MICROPY_SSL_AXTLS = 0
@@ -22,8 +45,7 @@ FLASH_SIZE ?= 4MB
 CROSS_COMPILE ?= xtensa-esp32-elf-
 OBJDUMP = $(CROSS_COMPILE)objdump
 
-# SDKCONFIG should be overridden to get a different configuration
-SDKCONFIG ?= boards/sdkconfig
+SDKCONFIG_COMBINED = $(BUILD)/sdkconfig.combined
 SDKCONFIG_H = $(BUILD)/sdkconfig.h
 
 # the git hash of the currently supported ESP IDF version
@@ -130,6 +152,7 @@ CFLAGS_BASE = -std=gnu99 $(CFLAGS_COMMON) -DMBEDTLS_CONFIG_FILE='"mbedtls/esp_co
 CFLAGS = $(CFLAGS_BASE) $(INC) $(INC_ESPCOMP)
 CFLAGS += -DIDF_VER=\"$(IDF_VER)\"
 CFLAGS += $(CFLAGS_MOD) $(CFLAGS_EXTRA)
+CFLAGS += -I$(BOARD_DIR)
 
 # this is what ESPIDF uses for c++ compilation
 CXXFLAGS = -std=gnu++11 $(CFLAGS_COMMON) $(INC) $(INC_ESPCOMP)
@@ -201,6 +224,7 @@ SRC_C = \
 	mpthreadport.c \
 	machine_rtc.c \
 	machine_sdcard.c \
+	$(wildcard $(BOARD_DIR)/*.c) \
 	$(SRC_MOD)
 
 EXTMOD_SRC_C = $(addprefix extmod/,\
@@ -242,7 +266,11 @@ SRC_QSTR_AUTO_DEPS +=
 ################################################################################
 # Generate sdkconfig.h from sdkconfig
 
-$(SDKCONFIG_H): $(SDKCONFIG)
+$(SDKCONFIG_COMBINED): $(SDKCONFIG)
+	$(Q)$(MKDIR) -p $(dir $@)
+	$(Q)$(CAT) $^ > $@
+
+$(SDKCONFIG_H): $(SDKCONFIG_COMBINED)
 	$(ECHO) "GEN $@"
 	$(Q)$(MKDIR) -p $(dir $@)
 	$(Q)$(PYTHON) $(ESPIDF)/tools/kconfig_new/confgen.py \
@@ -255,7 +283,7 @@ $(SDKCONFIG_H): $(SDKCONFIG)
 		--env "COMPONENT_KCONFIGS_PROJBUILD=$(ESPCOMP_KCONFIGS_PROJBUILD)"
 	$(Q)touch $@
 
-$(HEADER_BUILD)/qstrdefs.generated.h: $(SDKCONFIG_H)
+$(HEADER_BUILD)/qstrdefs.generated.h: $(SDKCONFIG_H) $(BOARD_DIR)/mpconfigboard.h
 
 ################################################################################
 # List of object files from the ESP32 IDF components
@@ -429,12 +457,12 @@ $(eval $(foreach lib,$(LIB_ESPIDF),$(eval $(call gen_sections_info_rule,$(BUILD_
 $(LDGEN_SECTION_INFOS): $(LDGEN_SECTIONS_INFO) $(ESPIDF)/make/ldgen.mk
 	$(Q)printf "$(foreach info,$(LDGEN_SECTIONS_INFO),$(info)\n)" > $@
 
-$(BUILD)/esp32.project.ld: $(ESPCOMP)/esp32/ld/esp32.project.ld.in $(LDGEN_FRAGMENTS) $(SDKCONFIG) $(LDGEN_SECTION_INFOS)
+$(BUILD)/esp32.project.ld: $(ESPCOMP)/esp32/ld/esp32.project.ld.in $(LDGEN_FRAGMENTS) $(SDKCONFIG_COMBINED) $(LDGEN_SECTION_INFOS)
 	$(ECHO) "GEN $@"
 	$(Q)$(PYTHON) $(ESPIDF)/tools/ldgen/ldgen.py \
 		--input $< \
 		--output $@ \
-		--config $(SDKCONFIG) \
+		--config $(SDKCONFIG_COMBINED) \
 		--kconfig $(ESPIDF)/Kconfig \
 		--fragments $(LDGEN_FRAGMENTS) \
 		--sections $(LDGEN_SECTION_INFOS) \
@@ -472,6 +500,7 @@ OBJ = $(OBJ_MP)
 
 APP_LD_ARGS =
 APP_LD_ARGS += $(LDFLAGS_MOD)
+APP_LD_ARGS += $(addprefix -T,$(LD_FILES))
 APP_LD_ARGS += --start-group
 APP_LD_ARGS += -L$(dir $(LIBGCC_FILE_NAME)) -lgcc
 APP_LD_ARGS += -L$(dir $(LIBSTDCXX_FILE_NAME)) -lstdc++
@@ -653,7 +682,9 @@ $(BUILD)/bootloader.elf: $(BOOTLOADER_OBJ) $(addprefix $(BOOTLOADER_LIB_DIR)/lib
 # Declarations to build the partitions
 
 PYTHON2 ?= python2
-PART_SRC = partitions.csv
+
+# Can be overriden by mkconfigboard.mk.
+PART_SRC ?= partitions.csv
 
 $(BUILD)/partitions.bin: $(PART_SRC)
 	$(ECHO) "Create $@"
diff --git a/ports/esp32/README.md b/ports/esp32/README.md
index 12144d822d9ad485a781af1e55437f60b87e5e22..31347b6a2a6a1f4cc5ab204f2ea67e5a45888dad 100644
--- a/ports/esp32/README.md
+++ b/ports/esp32/README.md
@@ -74,11 +74,11 @@ variables for the build.  In that case, create a new file in the esp32
 directory called `makefile` and add the following lines to that file:
 ```
 ESPIDF = <path to root of esp-idf repository>
+BOARD = GENERIC
 #PORT = /dev/ttyUSB0
 #FLASH_MODE = qio
 #FLASH_SIZE = 4MB
 #CROSS_COMPILE = xtensa-esp32-elf-
-#SDKCONFIG = boards/sdkconfig.spiram
 
 include Makefile
 ```
@@ -92,16 +92,18 @@ are `PORT` for the serial port of your esp32 module, and `FLASH_MODE`
 (which may need to be `dio` for some modules)
 and `FLASH_SIZE`.  See the Makefile for further information.
 
-The default ESP IDF configuration settings are provided in the file
-`boards/sdkconfig`, and this file is specified in the build by the make
-variable `SDKCONFIG`.  To use a custom configuration either set `SDKCONFIG`
-in your custom `makefile` (or `GNUmakefile`) or set this variable on the
-command line:
+The default ESP IDF configuration settings are provided by the `GENERIC`
+board definition in the directory `boards/GENERIC`. For a custom configuration
+you can define your own board directory.
+
+The `BOARD` variable can be set on the make command line:
 ```bash
-$ make SDKCONFIG=sdkconfig.myboard
+$ make BOARD=TINYPICO
 ```
-The file `boards/sdkconfig.spiram` is provided for ESP32 modules that have
-external SPIRAM.
+or added to your custom `makefile` (or `GNUmakefile`) described above. There
+is also a `GENERIC_SPIRAM` board for for ESP32 modules that have external
+SPIRAM, but prefer to use a specific board target (or define your own as
+necessary).
 
 Building the firmware
 ---------------------
diff --git a/ports/esp32/boards/GENERIC/mpconfigboard.h b/ports/esp32/boards/GENERIC/mpconfigboard.h
new file mode 100644
index 0000000000000000000000000000000000000000..644807f78e6011e3cdbbc5c784b1c5d40f49b25a
--- /dev/null
+++ b/ports/esp32/boards/GENERIC/mpconfigboard.h
@@ -0,0 +1,2 @@
+#define MICROPY_HW_BOARD_NAME "ESP32 module"
+#define MICROPY_HW_MCU_NAME "ESP32"
diff --git a/ports/esp32/boards/GENERIC/mpconfigboard.mk b/ports/esp32/boards/GENERIC/mpconfigboard.mk
new file mode 100644
index 0000000000000000000000000000000000000000..fc49d2a8c256cb8b6d96834cc9a2ba31472b2640
--- /dev/null
+++ b/ports/esp32/boards/GENERIC/mpconfigboard.mk
@@ -0,0 +1 @@
+SDKCONFIG += boards/sdkconfig.base
diff --git a/ports/esp32/boards/GENERIC_SPIRAM/mpconfigboard.h b/ports/esp32/boards/GENERIC_SPIRAM/mpconfigboard.h
new file mode 100644
index 0000000000000000000000000000000000000000..a5982e47e5c32c6bbef77df29c2433b205f8a546
--- /dev/null
+++ b/ports/esp32/boards/GENERIC_SPIRAM/mpconfigboard.h
@@ -0,0 +1,2 @@
+#define MICROPY_HW_BOARD_NAME "ESP32 module (spiram)"
+#define MICROPY_HW_MCU_NAME "ESP32"
diff --git a/ports/esp32/boards/GENERIC_SPIRAM/mpconfigboard.mk b/ports/esp32/boards/GENERIC_SPIRAM/mpconfigboard.mk
new file mode 100644
index 0000000000000000000000000000000000000000..59aa75f8571b964a2c94914d0597bc23be4e9bf6
--- /dev/null
+++ b/ports/esp32/boards/GENERIC_SPIRAM/mpconfigboard.mk
@@ -0,0 +1,2 @@
+SDKCONFIG += boards/sdkconfig.base
+SDKCONFIG += boards/sdkconfig.spiram
diff --git a/ports/esp32/boards/TINYPICO/mpconfigboard.h b/ports/esp32/boards/TINYPICO/mpconfigboard.h
new file mode 100644
index 0000000000000000000000000000000000000000..e63f43ed25a2fb80226858c4ac590abc1391165c
--- /dev/null
+++ b/ports/esp32/boards/TINYPICO/mpconfigboard.h
@@ -0,0 +1,2 @@
+#define MICROPY_HW_BOARD_NAME "TinyPICO"
+#define MICROPY_HW_MCU_NAME "ESP32-PICO-D4"
diff --git a/ports/esp32/boards/TINYPICO/mpconfigboard.mk b/ports/esp32/boards/TINYPICO/mpconfigboard.mk
new file mode 100644
index 0000000000000000000000000000000000000000..59aa75f8571b964a2c94914d0597bc23be4e9bf6
--- /dev/null
+++ b/ports/esp32/boards/TINYPICO/mpconfigboard.mk
@@ -0,0 +1,2 @@
+SDKCONFIG += boards/sdkconfig.base
+SDKCONFIG += boards/sdkconfig.spiram
diff --git a/ports/esp32/boards/sdkconfig b/ports/esp32/boards/sdkconfig.base
similarity index 100%
rename from ports/esp32/boards/sdkconfig
rename to ports/esp32/boards/sdkconfig.base
diff --git a/ports/esp32/boards/sdkconfig.spiram b/ports/esp32/boards/sdkconfig.spiram
index 1467f3171b23e111c38c46c224402c8b3316c623..53950e587c074f0e9db806bf686c31f2619ef55c 100644
--- a/ports/esp32/boards/sdkconfig.spiram
+++ b/ports/esp32/boards/sdkconfig.spiram
@@ -1,33 +1,5 @@
 # MicroPython on ESP32, ESP IDF configuration with SPIRAM support
-# The following options override the defaults
 
-CONFIG_IDF_TARGET="esp32"
-
-# Application manager
-CONFIG_APP_EXCLUDE_PROJECT_VER_VAR=y
-CONFIG_APP_EXCLUDE_PROJECT_NAME_VAR=y
-
-# Bootloader config
-CONFIG_LOG_BOOTLOADER_LEVEL_WARN=y
-
-# ESP32-specific
-CONFIG_ESP32_DEFAULT_CPU_FREQ_240=y
 CONFIG_SPIRAM_SUPPORT=y
 CONFIG_SPIRAM_IGNORE_NOTFOUND=y
 CONFIG_SPIRAM_USE_MEMMAP=y
-CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU0=n
-CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU1=n
-CONFIG_ESP32_XTAL_FREQ_AUTO=y
-
-# Power Management
-CONFIG_PM_ENABLE=y
-
-# FreeRTOS
-CONFIG_FREERTOS_THREAD_LOCAL_STORAGE_POINTERS=2
-CONFIG_SUPPORT_STATIC_ALLOCATION=y
-CONFIG_ENABLE_STATIC_TASK_CLEAN_UP_HOOK=y
-
-# UDP
-CONFIG_PPP_SUPPORT=y
-CONFIG_PPP_PAP_SUPPORT=y
-CONFIG_PPP_CHAP_SUPPORT=y
diff --git a/ports/esp32/mpconfigport.h b/ports/esp32/mpconfigport.h
index b5c7f50792ff58817e08a7187992f4953b0b950e..364b2de5c2e88d3fb7051d01f5cda67613e0f9d0 100644
--- a/ports/esp32/mpconfigport.h
+++ b/ports/esp32/mpconfigport.h
@@ -1,6 +1,9 @@
 // Options to control how MicroPython is built for this port,
 // overriding defaults in py/mpconfig.h.
 
+// Board-specific definitions
+#include "mpconfigboard.h"
+
 #include <stdint.h>
 #include <alloca.h>
 #include "rom/ets_sys.h"
@@ -268,9 +271,6 @@ typedef long mp_off_t;
 #include <sys/types.h>
 
 // board specifics
-
-#define MICROPY_HW_BOARD_NAME "ESP32 module"
-#define MICROPY_HW_MCU_NAME "ESP32"
 #define MICROPY_PY_SYS_PLATFORM "esp32"
 
 #ifndef MICROPY_HW_ENABLE_MDNS_QUERIES