esp32c3: add support for this chip
This change adds support for the ESP32-C3, a new chip from Espressif. It is a RISC-V core so porting was comparatively easy. Most peripherals are shared with the (original) ESP32 chip, but with subtle differences. Also, the SVD file I've used gives some peripherals/registers a different name which makes sharing code harder. Eventually, when an official SVD file for the ESP32 is released, I expect that a lot of code can be shared between the two chips. More information: https://www.espressif.com/en/products/socs/esp32-c3 TODO: - stack scheduler - interrupts - most peripherals (SPI, I2C, PWM, etc)
Этот коммит содержится в:
родитель
c830f878c6
коммит
cb147b9475
12 изменённых файлов: 660 добавлений и 70 удалений
3
Makefile
3
Makefile
|
@ -124,6 +124,7 @@ build/gen-device-svd: ./tools/gen-device-svd/*.go
|
||||||
|
|
||||||
gen-device-esp: build/gen-device-svd
|
gen-device-esp: build/gen-device-svd
|
||||||
./build/gen-device-svd -source=https://github.com/posborne/cmsis-svd/tree/master/data/Espressif-Community -interrupts=software lib/cmsis-svd/data/Espressif-Community/ src/device/esp/
|
./build/gen-device-svd -source=https://github.com/posborne/cmsis-svd/tree/master/data/Espressif-Community -interrupts=software lib/cmsis-svd/data/Espressif-Community/ src/device/esp/
|
||||||
|
./build/gen-device-svd -source=https://github.com/posborne/cmsis-svd/tree/master/data/Espressif -interrupts=software lib/cmsis-svd/data/Espressif/ src/device/esp/
|
||||||
GO111MODULE=off $(GO) fmt ./src/device/esp
|
GO111MODULE=off $(GO) fmt ./src/device/esp
|
||||||
|
|
||||||
gen-device-nrf: build/gen-device-svd
|
gen-device-nrf: build/gen-device-svd
|
||||||
|
@ -432,6 +433,8 @@ ifneq ($(XTENSA), 0)
|
||||||
$(TINYGO) build -size short -o test.bin -target=nodemcu examples/blinky1
|
$(TINYGO) build -size short -o test.bin -target=nodemcu examples/blinky1
|
||||||
@$(MD5SUM) test.bin
|
@$(MD5SUM) test.bin
|
||||||
endif
|
endif
|
||||||
|
$(TINYGO) build -size short -o test.bin -target=esp32c3 examples/serial
|
||||||
|
@$(MD5SUM) test.bin
|
||||||
$(TINYGO) build -size short -o test.hex -target=hifive1b examples/blinky1
|
$(TINYGO) build -size short -o test.hex -target=hifive1b examples/blinky1
|
||||||
@$(MD5SUM) test.hex
|
@$(MD5SUM) test.hex
|
||||||
$(TINYGO) build -size short -o test.hex -target=hifive1-qemu examples/serial
|
$(TINYGO) build -size short -o test.hex -target=hifive1-qemu examples/serial
|
||||||
|
|
|
@ -677,7 +677,7 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(Buil
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case "esp32", "esp8266":
|
case "esp32", "esp32c3", "esp8266":
|
||||||
// Special format for the ESP family of chips (parsed by the ROM
|
// Special format for the ESP family of chips (parsed by the ROM
|
||||||
// bootloader).
|
// bootloader).
|
||||||
tmppath = filepath.Join(dir, "main"+outext)
|
tmppath = filepath.Join(dir, "main"+outext)
|
||||||
|
|
|
@ -78,11 +78,21 @@ func makeESPFirmareImage(infile, outfile, format string) error {
|
||||||
// An added benefit is that we don't need to check for errors all the time.
|
// An added benefit is that we don't need to check for errors all the time.
|
||||||
outf := &bytes.Buffer{}
|
outf := &bytes.Buffer{}
|
||||||
|
|
||||||
|
// Chip IDs. Source:
|
||||||
|
// https://github.com/espressif/esp-idf/blob/v4.3/components/bootloader_support/include/esp_app_format.h#L22
|
||||||
|
chip_id := map[string]uint16{
|
||||||
|
"esp32": 0x0000,
|
||||||
|
"esp32c3": 0x0005,
|
||||||
|
}[format]
|
||||||
|
|
||||||
// Image header.
|
// Image header.
|
||||||
switch format {
|
switch format {
|
||||||
case "esp32":
|
case "esp32", "esp32c3":
|
||||||
// Header format:
|
// Header format:
|
||||||
// https://github.com/espressif/esp-idf/blob/8fbb63c2/components/bootloader_support/include/esp_image_format.h#L58
|
// 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
|
||||||
|
// esptool.py while flashing and therefore the hash won't be valid
|
||||||
|
// anymore.
|
||||||
binary.Write(outf, binary.LittleEndian, struct {
|
binary.Write(outf, binary.LittleEndian, struct {
|
||||||
magic uint8
|
magic uint8
|
||||||
segment_count uint8
|
segment_count uint8
|
||||||
|
@ -91,15 +101,18 @@ func makeESPFirmareImage(infile, outfile, format string) error {
|
||||||
entry_addr uint32
|
entry_addr uint32
|
||||||
wp_pin uint8
|
wp_pin uint8
|
||||||
spi_pin_drv [3]uint8
|
spi_pin_drv [3]uint8
|
||||||
reserved [11]uint8
|
chip_id uint16
|
||||||
|
min_chip_rev uint8
|
||||||
|
reserved [8]uint8
|
||||||
hash_appended bool
|
hash_appended bool
|
||||||
}{
|
}{
|
||||||
magic: 0xE9,
|
magic: 0xE9,
|
||||||
segment_count: byte(len(segments)),
|
segment_count: byte(len(segments)),
|
||||||
spi_mode: 0, // irrelevant, replaced by esptool when flashing
|
spi_mode: 2, // ESP_IMAGE_SPI_MODE_DIO
|
||||||
spi_speed_size: 0, // spi_speed, spi_size: replaced by esptool when flashing
|
spi_speed_size: 0x1f, // ESP_IMAGE_SPI_SPEED_80M, ESP_IMAGE_FLASH_SIZE_2MB
|
||||||
entry_addr: uint32(inf.Entry),
|
entry_addr: uint32(inf.Entry),
|
||||||
wp_pin: 0xEE, // disable WP pin
|
wp_pin: 0xEE, // disable WP pin
|
||||||
|
chip_id: chip_id,
|
||||||
hash_appended: true, // add a SHA256 hash
|
hash_appended: true, // add a SHA256 hash
|
||||||
})
|
})
|
||||||
case "esp8266":
|
case "esp8266":
|
||||||
|
@ -142,7 +155,7 @@ func makeESPFirmareImage(infile, outfile, format string) error {
|
||||||
outf.Write(make([]byte, 15-outf.Len()%16))
|
outf.Write(make([]byte, 15-outf.Len()%16))
|
||||||
outf.WriteByte(checksum)
|
outf.WriteByte(checksum)
|
||||||
|
|
||||||
if format == "esp32" {
|
if format != "esp8266" {
|
||||||
// SHA256 hash (to protect against image corruption, not for security).
|
// SHA256 hash (to protect against image corruption, not for security).
|
||||||
hash := sha256.Sum256(outf.Bytes())
|
hash := sha256.Sum256(outf.Bytes())
|
||||||
outf.Write(hash[:])
|
outf.Write(hash[:])
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 9c35b6d9df1f9eeecfcc33fc6f98719dbaaa30ce
|
Subproject commit df75ff974c76a911fc2815e29807f5ecaae06fc2
|
49
src/device/esp/esp32c3.S
Обычный файл
49
src/device/esp/esp32c3.S
Обычный файл
|
@ -0,0 +1,49 @@
|
||||||
|
// This is a very minimal bootloader for the ESP32-C3. 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.
|
||||||
|
//
|
||||||
|
// For reference, here is a nice introduction into RISC-V assembly:
|
||||||
|
// https://www.imperialviolet.org/2016/12/31/riscv.html
|
||||||
|
|
||||||
|
.section .init
|
||||||
|
.global call_start_cpu0
|
||||||
|
.type call_start_cpu0,@function
|
||||||
|
call_start_cpu0:
|
||||||
|
// At this point:
|
||||||
|
// - The ROM bootloader is finished and has jumped to here.
|
||||||
|
// - We're running from IRAM: both IRAM and DRAM segments have been loaded
|
||||||
|
// by the ROM bootloader.
|
||||||
|
// - We have a usable stack (but not the one we would like to use).
|
||||||
|
// - No flash mappings (MMU) are set up yet.
|
||||||
|
|
||||||
|
// Reset MMU, see bootloader_reset_mmu in the ESP-IDF.
|
||||||
|
call Cache_Suspend_ICache
|
||||||
|
mv s0, a0 // autoload value
|
||||||
|
call Cache_Invalidate_ICache_All
|
||||||
|
call Cache_MMU_Init
|
||||||
|
|
||||||
|
// Set up DROM from flash.
|
||||||
|
// Somehow, this also sets up IROM from flash. Not sure why, but it avoids
|
||||||
|
// the need for another such call.
|
||||||
|
// C equivalent:
|
||||||
|
// Cache_Dbus_MMU_Set(MMU_ACCESS_FLASH, 0x3C00_0000, 0, 64, 128, 0)
|
||||||
|
li a0, 0 // ext_ram: MMU_ACCESS_FLASH
|
||||||
|
li a1, 0x3C000000 // vaddr: address in the data bus
|
||||||
|
li a2, 0 // paddr: physical address in the flash chip
|
||||||
|
li a3, 64 // psize: always 64 (kilobytes)
|
||||||
|
li a4, 128 // num: pages to be set (8192K / 64K = 128)
|
||||||
|
li a5, 0 // fixed
|
||||||
|
call Cache_Dbus_MMU_Set
|
||||||
|
|
||||||
|
// Enable the flash cache.
|
||||||
|
mv a0, s0 // restore autoload value from Cache_Suspend_ICache call
|
||||||
|
call Cache_Resume_ICache
|
||||||
|
|
||||||
|
// Jump to generic RISC-V initialization, which initializes the stack
|
||||||
|
// pointer and globals register. It should not return.
|
||||||
|
// (It appears that the linker relaxes this jump and instead inserts the
|
||||||
|
// _start function right after here).
|
||||||
|
j _start
|
144
src/machine/machine_esp32c3.go
Обычный файл
144
src/machine/machine_esp32c3.go
Обычный файл
|
@ -0,0 +1,144 @@
|
||||||
|
// +build esp32c3
|
||||||
|
|
||||||
|
package machine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"device/esp"
|
||||||
|
"runtime/volatile"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 160e6 // 160MHz
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
PinOutput PinMode = iota
|
||||||
|
PinInput
|
||||||
|
PinInputPullup
|
||||||
|
PinInputPulldown
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// Make this pin an input pin (always).
|
||||||
|
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.
|
||||||
|
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:
|
||||||
|
// Clear the 'output enable' bit.
|
||||||
|
esp.GPIO.ENABLE_W1TC.Set(1 << 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.Pointer((uintptr(unsafe.Pointer(&esp.GPIO.FUNC0_OUT_SEL_CFG)) + 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.Pointer((uintptr(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.Pointer((uintptr(unsafe.Pointer(&esp.IO_MUX.GPIO0)) + 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
var DefaultUART = UART0
|
||||||
|
|
||||||
|
var (
|
||||||
|
UART0 = &_UART0
|
||||||
|
_UART0 = UART{Bus: esp.UART0, Buffer: NewRingBuffer()}
|
||||||
|
UART1 = &_UART1
|
||||||
|
_UART1 = UART{Bus: esp.UART1, Buffer: NewRingBuffer()}
|
||||||
|
)
|
||||||
|
|
||||||
|
type UART struct {
|
||||||
|
Bus *esp.UART_Type
|
||||||
|
Buffer *RingBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
// Read UART_TXFIFO_CNT from the status register, which indicates how
|
||||||
|
// many bytes there are in the transmit buffer. Wait until there are
|
||||||
|
// less than 128 bytes in this buffer (the default buffer size).
|
||||||
|
}
|
||||||
|
uart.Bus.FIFO.Set(uint32(b))
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -6,19 +6,8 @@ import (
|
||||||
"device"
|
"device"
|
||||||
"device/esp"
|
"device/esp"
|
||||||
"machine"
|
"machine"
|
||||||
"unsafe"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type timeUnit int64
|
|
||||||
|
|
||||||
var currentTime timeUnit
|
|
||||||
|
|
||||||
func putchar(c byte) {
|
|
||||||
machine.Serial.WriteByte(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func postinit() {}
|
|
||||||
|
|
||||||
// This is the function called on startup right after the stack pointer has been
|
// This is the function called on startup right after the stack pointer has been
|
||||||
// set.
|
// set.
|
||||||
//export main
|
//export main
|
||||||
|
@ -50,23 +39,15 @@ func main() {
|
||||||
// Clear .bss section. .data has already been loaded by the ROM bootloader.
|
// Clear .bss section. .data has already been loaded by the ROM bootloader.
|
||||||
// Do this after increasing the CPU clock to possibly make startup slightly
|
// Do this after increasing the CPU clock to possibly make startup slightly
|
||||||
// faster.
|
// faster.
|
||||||
preinit()
|
clearbss()
|
||||||
|
|
||||||
// Initialize UART.
|
// Initialize UART.
|
||||||
machine.Serial.Configure(machine.UARTConfig{})
|
machine.Serial.Configure(machine.UARTConfig{})
|
||||||
|
|
||||||
// Configure timer 0 in timer group 0, for timekeeping.
|
// Initialize main system timer used for time.Now.
|
||||||
// EN: Enable the timer.
|
initTimer()
|
||||||
// INCREASE: Count up every tick (as opposed to counting down).
|
|
||||||
// DIVIDER: 16-bit prescaler, set to 2 for dividing the APB clock by two
|
|
||||||
// (40MHz).
|
|
||||||
esp.TIMG0.T0CONFIG.Set(esp.TIMG_T0CONFIG_T0_EN | esp.TIMG_T0CONFIG_T0_INCREASE | 2<<esp.TIMG_T0CONFIG_T0_DIVIDER_Pos)
|
|
||||||
|
|
||||||
// Set the timer counter value to 0.
|
|
||||||
esp.TIMG0.T0LOADLO.Set(0)
|
|
||||||
esp.TIMG0.T0LOADHI.Set(0)
|
|
||||||
esp.TIMG0.T0LOAD.Set(0) // value doesn't matter.
|
|
||||||
|
|
||||||
|
// Initialize the heap, call main.main, etc.
|
||||||
run()
|
run()
|
||||||
|
|
||||||
// Fallback: if main ever returns, hang the CPU.
|
// Fallback: if main ever returns, hang the CPU.
|
||||||
|
@ -79,44 +60,6 @@ var _sbss [0]byte
|
||||||
//go:extern _ebss
|
//go:extern _ebss
|
||||||
var _ebss [0]byte
|
var _ebss [0]byte
|
||||||
|
|
||||||
func preinit() {
|
|
||||||
// Initialize .bss: zero-initialized global variables.
|
|
||||||
// The .data section has already been loaded by the ROM bootloader.
|
|
||||||
ptr := unsafe.Pointer(&_sbss)
|
|
||||||
for ptr != unsafe.Pointer(&_ebss) {
|
|
||||||
*(*uint32)(ptr) = 0
|
|
||||||
ptr = unsafe.Pointer(uintptr(ptr) + 4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ticks() timeUnit {
|
|
||||||
// First, update the LO and HI register pair by writing any value to the
|
|
||||||
// register. This allows reading the pair atomically.
|
|
||||||
esp.TIMG0.T0UPDATE.Set(0)
|
|
||||||
// Then read the two 32-bit parts of the timer.
|
|
||||||
return timeUnit(uint64(esp.TIMG0.T0LO.Get()) | uint64(esp.TIMG0.T0HI.Get())<<32)
|
|
||||||
}
|
|
||||||
|
|
||||||
func nanosecondsToTicks(ns int64) timeUnit {
|
|
||||||
// Calculate the number of ticks from the number of nanoseconds. At a 80MHz
|
|
||||||
// APB clock, that's 25 nanoseconds per tick with a timer prescaler of 2:
|
|
||||||
// 25 = 1e9 / (80MHz / 2)
|
|
||||||
return timeUnit(ns / 25)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ticksToNanoseconds(ticks timeUnit) int64 {
|
|
||||||
// See nanosecondsToTicks.
|
|
||||||
return int64(ticks) * 25
|
|
||||||
}
|
|
||||||
|
|
||||||
// sleepTicks busy-waits until the given number of ticks have passed.
|
|
||||||
func sleepTicks(d timeUnit) {
|
|
||||||
sleepUntil := ticks() + d
|
|
||||||
for ticks() < sleepUntil {
|
|
||||||
// TODO: suspend the CPU to not burn power here unnecessarily.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func abort() {
|
func abort() {
|
||||||
for {
|
for {
|
||||||
device.Asm("waiti 0")
|
device.Asm("waiti 0")
|
||||||
|
|
65
src/runtime/runtime_esp32c3.go
Обычный файл
65
src/runtime/runtime_esp32c3.go
Обычный файл
|
@ -0,0 +1,65 @@
|
||||||
|
// +build esp32c3
|
||||||
|
|
||||||
|
package runtime
|
||||||
|
|
||||||
|
import (
|
||||||
|
"device/esp"
|
||||||
|
"device/riscv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 160MHz, which is the maximum speed allowed
|
||||||
|
// for this CPU. Lower frequencies might be possible in the future, but
|
||||||
|
// running fast and sleeping quickly is often also a good strategy to save
|
||||||
|
// power.
|
||||||
|
// TODO: protect certain memory regions, especially the area below the stack
|
||||||
|
// to protect against stack overflows. See
|
||||||
|
// esp_cpu_configure_region_protection in ESP-IDF.
|
||||||
|
|
||||||
|
// Disable Timer 0 watchdog.
|
||||||
|
esp.TIMG0.WDTCONFIG0.Set(0)
|
||||||
|
|
||||||
|
// Disable RTC watchdog.
|
||||||
|
esp.RTC_CNTL.RTC_WDTWPROTECT.Set(0x50D83AA1)
|
||||||
|
esp.RTC_CNTL.RTC_WDTCONFIG0.Set(0)
|
||||||
|
|
||||||
|
// Disable super watchdog.
|
||||||
|
esp.RTC_CNTL.RTC_SWD_WPROTECT.Set(0x8F1D312A)
|
||||||
|
esp.RTC_CNTL.RTC_SWD_CONF.Set(esp.RTC_CNTL_RTC_SWD_CONF_SWD_DISABLE)
|
||||||
|
|
||||||
|
// Change CPU frequency from 20MHz to 80MHz, by switching from the XTAL to
|
||||||
|
// the PLL clock source (see table "CPU Clock Frequency" in the reference
|
||||||
|
// manual).
|
||||||
|
esp.SYSTEM.SYSCLK_CONF.Set(1 << esp.SYSTEM_SYSCLK_CONF_SOC_CLK_SEL_Pos)
|
||||||
|
|
||||||
|
// Change CPU frequency from 80MHz to 160MHz by setting SYSTEM_CPUPERIOD_SEL
|
||||||
|
// to 1 (see table "CPU Clock Frequency" in the reference manual).
|
||||||
|
// Note: we might not want to set SYSTEM_CPU_WAIT_MODE_FORCE_ON to save
|
||||||
|
// power. It is set here to keep the default on reset.
|
||||||
|
esp.SYSTEM.CPU_PER_CONF.Set(esp.SYSTEM_CPU_PER_CONF_CPU_WAIT_MODE_FORCE_ON | esp.SYSTEM_CPU_PER_CONF_PLL_FREQ_SEL | 1<<esp.SYSTEM_CPU_PER_CONF_CPUPERIOD_SEL_Pos)
|
||||||
|
|
||||||
|
clearbss()
|
||||||
|
|
||||||
|
// Initialize main system timer used for time.Now.
|
||||||
|
initTimer()
|
||||||
|
|
||||||
|
// Initialize the heap, call main.main, etc.
|
||||||
|
run()
|
||||||
|
|
||||||
|
// Fallback: if main ever returns, hang the CPU.
|
||||||
|
abort()
|
||||||
|
}
|
||||||
|
|
||||||
|
func abort() {
|
||||||
|
// lock up forever
|
||||||
|
for {
|
||||||
|
riscv.Asm("wfi")
|
||||||
|
}
|
||||||
|
}
|
69
src/runtime/runtime_esp32xx.go
Обычный файл
69
src/runtime/runtime_esp32xx.go
Обычный файл
|
@ -0,0 +1,69 @@
|
||||||
|
// +build esp32 esp32c3
|
||||||
|
|
||||||
|
package runtime
|
||||||
|
|
||||||
|
import (
|
||||||
|
"device/esp"
|
||||||
|
"machine"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type timeUnit int64
|
||||||
|
|
||||||
|
func putchar(c byte) {
|
||||||
|
machine.Serial.WriteByte(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func postinit() {}
|
||||||
|
|
||||||
|
// Initialize .bss: zero-initialized global variables.
|
||||||
|
// The .data section has already been loaded by the ROM bootloader.
|
||||||
|
func clearbss() {
|
||||||
|
ptr := unsafe.Pointer(&_sbss)
|
||||||
|
for ptr != unsafe.Pointer(&_ebss) {
|
||||||
|
*(*uint32)(ptr) = 0
|
||||||
|
ptr = unsafe.Pointer(uintptr(ptr) + 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initTimer() {
|
||||||
|
// Configure timer 0 in timer group 0, for timekeeping.
|
||||||
|
// EN: Enable the timer.
|
||||||
|
// INCREASE: Count up every tick (as opposed to counting down).
|
||||||
|
// DIVIDER: 16-bit prescaler, set to 2 for dividing the APB clock by two
|
||||||
|
// (40MHz).
|
||||||
|
esp.TIMG0.T0CONFIG.Set(esp.TIMG_T0CONFIG_T0_EN | esp.TIMG_T0CONFIG_T0_INCREASE | 2<<esp.TIMG_T0CONFIG_T0_DIVIDER_Pos)
|
||||||
|
|
||||||
|
// Set the timer counter value to 0.
|
||||||
|
esp.TIMG0.T0LOADLO.Set(0)
|
||||||
|
esp.TIMG0.T0LOADHI.Set(0)
|
||||||
|
esp.TIMG0.T0LOAD.Set(0) // value doesn't matter.
|
||||||
|
}
|
||||||
|
|
||||||
|
func ticks() timeUnit {
|
||||||
|
// First, update the LO and HI register pair by writing any value to the
|
||||||
|
// register. This allows reading the pair atomically.
|
||||||
|
esp.TIMG0.T0UPDATE.Set(0)
|
||||||
|
// Then read the two 32-bit parts of the timer.
|
||||||
|
return timeUnit(uint64(esp.TIMG0.T0LO.Get()) | uint64(esp.TIMG0.T0HI.Get())<<32)
|
||||||
|
}
|
||||||
|
|
||||||
|
func nanosecondsToTicks(ns int64) timeUnit {
|
||||||
|
// Calculate the number of ticks from the number of nanoseconds. At a 80MHz
|
||||||
|
// APB clock, that's 25 nanoseconds per tick with a timer prescaler of 2:
|
||||||
|
// 25 = 1e9 / (80MHz / 2)
|
||||||
|
return timeUnit(ns / 25)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ticksToNanoseconds(ticks timeUnit) int64 {
|
||||||
|
// See nanosecondsToTicks.
|
||||||
|
return int64(ticks) * 25
|
||||||
|
}
|
||||||
|
|
||||||
|
// sleepTicks busy-waits until the given number of ticks have passed.
|
||||||
|
func sleepTicks(d timeUnit) {
|
||||||
|
sleepUntil := ticks() + d
|
||||||
|
for ticks() < sleepUntil {
|
||||||
|
// TODO: suspend the CPU to not burn power here unnecessarily.
|
||||||
|
}
|
||||||
|
}
|
16
targets/esp32c3.json
Обычный файл
16
targets/esp32c3.json
Обычный файл
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"inherits": ["riscv32"],
|
||||||
|
"features": ["+c", "+m"],
|
||||||
|
"build-tags": ["esp32c3", "esp"],
|
||||||
|
"scheduler": "none",
|
||||||
|
"serial": "uart",
|
||||||
|
"rtlib": "compiler-rt",
|
||||||
|
"libc": "picolibc",
|
||||||
|
"linkerscript": "targets/esp32c3.ld",
|
||||||
|
"extra-files": [
|
||||||
|
"src/device/esp/esp32c3.S"
|
||||||
|
],
|
||||||
|
"binary-format": "esp32c3",
|
||||||
|
"flash-command": "esptool.py --chip=esp32c3 --port {port} write_flash 0x0 {bin}"
|
||||||
|
}
|
||||||
|
|
285
targets/esp32c3.ld
Обычный файл
285
targets/esp32c3.ld
Обычный файл
|
@ -0,0 +1,285 @@
|
||||||
|
/* Linker script for the ESP32-C3
|
||||||
|
*
|
||||||
|
* The ESP32-C3 has a rather funky memory layout, more like its Xtensa
|
||||||
|
* predecessors than like other RISC-V chips:
|
||||||
|
* - It has 384kB of regular RAM. This RAM can be used both as data RAM and
|
||||||
|
* instruction RAM, but needs to be accessed via a different address space
|
||||||
|
* (DRAM/IRAM).
|
||||||
|
* - It has another 16kB of RAM, that could be used as regular RAM but is
|
||||||
|
* normally used by the flash cache.
|
||||||
|
* - It has 8MB of address space for the DROM and IROM, but for some reason
|
||||||
|
* this address space is shared. So it isn't possible to map all DROM at
|
||||||
|
* 0x3C000000 and all DRAM at 0x42000000: they would overlap.
|
||||||
|
* - The MMU works in pages of 64kB, which means the bottom 16 bits of the
|
||||||
|
* address in flash and the address in DROM/IROM need to match.
|
||||||
|
* - Memory in DRAM and IRAM is loaded at reset by the ROM bootloader.
|
||||||
|
* Luckily, this doesn't have significant alignment requirements.
|
||||||
|
*
|
||||||
|
* This linker script has been written to carefully work around (or with) these
|
||||||
|
* limitations:
|
||||||
|
* - It adds dummy sections so that the bottom 16 bits of the virtual address
|
||||||
|
* and the physical address (in the generated firmware image) match.
|
||||||
|
* - It also offsets sections that share an address space using those same
|
||||||
|
* dummy sections.
|
||||||
|
* - It sorts the sections by load address, to avoid surprises as esptool.py
|
||||||
|
* also does it.
|
||||||
|
* This way, it's possible to create a very small firmware image that still
|
||||||
|
* conforms to the expectations of esptool.py and the ROM bootloader.
|
||||||
|
*/
|
||||||
|
|
||||||
|
MEMORY
|
||||||
|
{
|
||||||
|
/* Note: DRAM and IRAM below are actually in the same 384K address space. */
|
||||||
|
DRAM (rw) : ORIGIN = 0x3FC80000, LENGTH = 384K /* Internal SRAM 1 (data bus) */
|
||||||
|
IRAM (x) : ORIGIN = 0x40380000, LENGTH = 384K /* Internal SRAM 1 (instruction bus) */
|
||||||
|
|
||||||
|
/* Note: DROM and IROM below are actually in the same 8M address space. */
|
||||||
|
DROM (r) : ORIGIN = 0x3C000000, LENGTH = 8M /* Data bus (read-only) */
|
||||||
|
IROM (rx) : ORIGIN = 0x42000000, LENGTH = 8M /* Instruction bus */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The entry point. It is set in the image flashed to the chip, so must be
|
||||||
|
* defined.
|
||||||
|
*/
|
||||||
|
ENTRY(call_start_cpu0)
|
||||||
|
|
||||||
|
SECTIONS
|
||||||
|
{
|
||||||
|
/* Dummy section to make sure the .rodata section starts exactly behind the
|
||||||
|
* image header.
|
||||||
|
*/
|
||||||
|
.rodata_dummy (NOLOAD): ALIGN(4)
|
||||||
|
{
|
||||||
|
. += 0x18; /* image header at start of flash: esp_image_header_t */
|
||||||
|
. += 0x8; /* DROM segment header (8 bytes) */
|
||||||
|
} > DROM
|
||||||
|
|
||||||
|
/* Constant global variables, stored in DROM.
|
||||||
|
*/
|
||||||
|
.rodata : ALIGN(4)
|
||||||
|
{
|
||||||
|
*(.rodata .rodata.*)
|
||||||
|
. = ALIGN (4);
|
||||||
|
} >DROM
|
||||||
|
|
||||||
|
/* Put the stack at the bottom of DRAM, so that the application will
|
||||||
|
* crash on stack overflow instead of silently corrupting memory.
|
||||||
|
* See: http://blog.japaric.io/stack-overflow-protection/
|
||||||
|
* TODO: this might not actually work because memory protection hasn't been set up.
|
||||||
|
*/
|
||||||
|
.stack (NOLOAD) :
|
||||||
|
{
|
||||||
|
. = ALIGN(16);
|
||||||
|
. += _stack_size;
|
||||||
|
_stack_top = .;
|
||||||
|
} >DRAM
|
||||||
|
|
||||||
|
/* 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(.);
|
||||||
|
} >DRAM
|
||||||
|
|
||||||
|
/* Mutable global variables. This data (in the DRAM segment) is initialized
|
||||||
|
* by the ROM bootloader.
|
||||||
|
*/
|
||||||
|
.data : ALIGN(4)
|
||||||
|
{
|
||||||
|
. = ALIGN (4);
|
||||||
|
_sdata = ABSOLUTE(.);
|
||||||
|
*(.sdata)
|
||||||
|
*(.data .data.*)
|
||||||
|
. = ALIGN (4);
|
||||||
|
_edata = ABSOLUTE(.);
|
||||||
|
} >DRAM
|
||||||
|
|
||||||
|
/* Dummy section to make sure the .init section (in the IRAM segment) is just
|
||||||
|
* behind the DRAM segment. For IRAM and DRAM, we luckily don't have to
|
||||||
|
* worry about 64kB pages or image headers as they're loaded in RAM by the
|
||||||
|
* bootloader (not mapped from flash).
|
||||||
|
*/
|
||||||
|
.iram_dummy (NOLOAD): ALIGN(4)
|
||||||
|
{
|
||||||
|
. += SIZEOF(.stack);
|
||||||
|
. += SIZEOF(.bss);
|
||||||
|
. += SIZEOF(.data);
|
||||||
|
} > IRAM
|
||||||
|
|
||||||
|
/* Initialization code is loaded into IRAM. This memory area is also used by
|
||||||
|
* the heap, so no RAM is wasted.
|
||||||
|
*/
|
||||||
|
.init : ALIGN(4)
|
||||||
|
{
|
||||||
|
*(.init)
|
||||||
|
} >IRAM
|
||||||
|
|
||||||
|
/* Dummy section to put the IROM segment exactly behind the IRAM segment.
|
||||||
|
* This has to follow the app image format exactly.
|
||||||
|
*/
|
||||||
|
.text_dummy (NOLOAD): ALIGN(4)
|
||||||
|
{
|
||||||
|
/* Note: DRAM and DROM are not always present so the header should only
|
||||||
|
* be inserted if it actually exists.
|
||||||
|
*/
|
||||||
|
. += 0x18; /* esp_image_header_t */
|
||||||
|
. += SIZEOF(.rodata) + ((SIZEOF(.rodata) != 0) ? 0x8 : 0); /* DROM segment (optional) */
|
||||||
|
. += SIZEOF(.data) + ((SIZEOF(.data) != 0) ? 0x8 : 0); /* DRAM segment (optional) */
|
||||||
|
. += SIZEOF(.init) + 0x8; /* IRAM segment */
|
||||||
|
. += 0x8; /* IROM segment header */
|
||||||
|
} > IROM
|
||||||
|
|
||||||
|
/* IROM segment. This contains all the actual code and is placed right after
|
||||||
|
* the DROM segment.
|
||||||
|
*/
|
||||||
|
.text : ALIGN(4)
|
||||||
|
{
|
||||||
|
*(.text .text.*)
|
||||||
|
} >IROM
|
||||||
|
|
||||||
|
/DISCARD/ :
|
||||||
|
{
|
||||||
|
*(.eh_frame) /* causes 'no memory region specified' error in lld */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check that the boot ROM stack (for the APP CPU) does not overlap with the
|
||||||
|
* data that is loaded by the boot ROM. This is unlikely to happen in
|
||||||
|
* practice.
|
||||||
|
* The magic value comes from here:
|
||||||
|
* https://github.com/espressif/esp-idf/blob/61299f879e/components/bootloader/subproject/main/ld/esp32c3/bootloader.ld#L191
|
||||||
|
*/
|
||||||
|
ASSERT((_edata + SIZEOF(.init)) < 0x3FCDE710, "the .init section overlaps with the stack used by the boot ROM, possibly causing corruption at startup")
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For the garbage collector.
|
||||||
|
* Note that _heap_start starts after _edata (without caring for the .init
|
||||||
|
* section), because the .init section isn't necessary anymore after startup and
|
||||||
|
* can thus be overwritten by the heap.
|
||||||
|
*/
|
||||||
|
_globals_start = _sbss;
|
||||||
|
_globals_end = _edata;
|
||||||
|
_heap_start = _edata;
|
||||||
|
_heap_end = ORIGIN(DRAM) + LENGTH(DRAM);
|
||||||
|
|
||||||
|
_stack_size = 4K;
|
||||||
|
|
||||||
|
/* ROM functions used for setting up the flash mapping.
|
||||||
|
*/
|
||||||
|
Cache_Invalidate_ICache_All = 0x400004d8;
|
||||||
|
Cache_Suspend_ICache = 0x40000524;
|
||||||
|
Cache_Resume_ICache = 0x40000528;
|
||||||
|
Cache_MMU_Init = 0x4000055c;
|
||||||
|
Cache_Dbus_MMU_Set = 0x40000564;
|
||||||
|
|
||||||
|
/* From ESP-IDF:
|
||||||
|
* components/esp_rom/esp32c3/ld/esp32c3.rom.libgcc.ld
|
||||||
|
* These are called from LLVM during codegen. The original license is Apache
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
__absvdi2 = 0x40000764;
|
||||||
|
__absvsi2 = 0x40000768;
|
||||||
|
__adddf3 = 0x4000076c;
|
||||||
|
__addsf3 = 0x40000770;
|
||||||
|
__addvdi3 = 0x40000774;
|
||||||
|
__addvsi3 = 0x40000778;
|
||||||
|
__ashldi3 = 0x4000077c;
|
||||||
|
__ashrdi3 = 0x40000780;
|
||||||
|
__bswapdi2 = 0x40000784;
|
||||||
|
__bswapsi2 = 0x40000788;
|
||||||
|
__clear_cache = 0x4000078c;
|
||||||
|
__clrsbdi2 = 0x40000790;
|
||||||
|
__clrsbsi2 = 0x40000794;
|
||||||
|
__clzdi2 = 0x40000798;
|
||||||
|
__clzsi2 = 0x4000079c;
|
||||||
|
__cmpdi2 = 0x400007a0;
|
||||||
|
__ctzdi2 = 0x400007a4;
|
||||||
|
__ctzsi2 = 0x400007a8;
|
||||||
|
__divdc3 = 0x400007ac;
|
||||||
|
__divdf3 = 0x400007b0;
|
||||||
|
__divdi3 = 0x400007b4;
|
||||||
|
__divsc3 = 0x400007b8;
|
||||||
|
__divsf3 = 0x400007bc;
|
||||||
|
__divsi3 = 0x400007c0;
|
||||||
|
__eqdf2 = 0x400007c4;
|
||||||
|
__eqsf2 = 0x400007c8;
|
||||||
|
__extendsfdf2 = 0x400007cc;
|
||||||
|
__ffsdi2 = 0x400007d0;
|
||||||
|
__ffssi2 = 0x400007d4;
|
||||||
|
__fixdfdi = 0x400007d8;
|
||||||
|
__fixdfsi = 0x400007dc;
|
||||||
|
__fixsfdi = 0x400007e0;
|
||||||
|
__fixsfsi = 0x400007e4;
|
||||||
|
__fixunsdfsi = 0x400007e8;
|
||||||
|
__fixunssfdi = 0x400007ec;
|
||||||
|
__fixunssfsi = 0x400007f0;
|
||||||
|
__floatdidf = 0x400007f4;
|
||||||
|
__floatdisf = 0x400007f8;
|
||||||
|
__floatsidf = 0x400007fc;
|
||||||
|
__floatsisf = 0x40000800;
|
||||||
|
__floatundidf = 0x40000804;
|
||||||
|
__floatundisf = 0x40000808;
|
||||||
|
__floatunsidf = 0x4000080c;
|
||||||
|
__floatunsisf = 0x40000810;
|
||||||
|
__gcc_bcmp = 0x40000814;
|
||||||
|
__gedf2 = 0x40000818;
|
||||||
|
__gesf2 = 0x4000081c;
|
||||||
|
__gtdf2 = 0x40000820;
|
||||||
|
__gtsf2 = 0x40000824;
|
||||||
|
__ledf2 = 0x40000828;
|
||||||
|
__lesf2 = 0x4000082c;
|
||||||
|
__lshrdi3 = 0x40000830;
|
||||||
|
__ltdf2 = 0x40000834;
|
||||||
|
__ltsf2 = 0x40000838;
|
||||||
|
__moddi3 = 0x4000083c;
|
||||||
|
__modsi3 = 0x40000840;
|
||||||
|
__muldc3 = 0x40000844;
|
||||||
|
__muldf3 = 0x40000848;
|
||||||
|
__muldi3 = 0x4000084c;
|
||||||
|
__mulsc3 = 0x40000850;
|
||||||
|
__mulsf3 = 0x40000854;
|
||||||
|
__mulsi3 = 0x40000858;
|
||||||
|
__mulvdi3 = 0x4000085c;
|
||||||
|
__mulvsi3 = 0x40000860;
|
||||||
|
__nedf2 = 0x40000864;
|
||||||
|
__negdf2 = 0x40000868;
|
||||||
|
__negdi2 = 0x4000086c;
|
||||||
|
__negsf2 = 0x40000870;
|
||||||
|
__negvdi2 = 0x40000874;
|
||||||
|
__negvsi2 = 0x40000878;
|
||||||
|
__nesf2 = 0x4000087c;
|
||||||
|
__paritysi2 = 0x40000880;
|
||||||
|
__popcountdi2 = 0x40000884;
|
||||||
|
__popcountsi2 = 0x40000888;
|
||||||
|
__powidf2 = 0x4000088c;
|
||||||
|
__powisf2 = 0x40000890;
|
||||||
|
__subdf3 = 0x40000894;
|
||||||
|
__subsf3 = 0x40000898;
|
||||||
|
__subvdi3 = 0x4000089c;
|
||||||
|
__subvsi3 = 0x400008a0;
|
||||||
|
__truncdfsf2 = 0x400008a4;
|
||||||
|
__ucmpdi2 = 0x400008a8;
|
||||||
|
__udivdi3 = 0x400008ac;
|
||||||
|
__udivmoddi4 = 0x400008b0;
|
||||||
|
__udivsi3 = 0x400008b4;
|
||||||
|
__udiv_w_sdiv = 0x400008b8;
|
||||||
|
__umoddi3 = 0x400008bc;
|
||||||
|
__umodsi3 = 0x400008c0;
|
||||||
|
__unorddf2 = 0x400008c4;
|
||||||
|
__unordsf2 = 0x400008c8;
|
||||||
|
|
||||||
|
/* From ESP-IDF:
|
||||||
|
* components/esp_rom/esp32c3/ld/esp32c3.rom.newlib.ld
|
||||||
|
* These are called during codegen and thus it's a good idea to make them always
|
||||||
|
* available. ROM functions may also be faster than functions in IROM (that go
|
||||||
|
* through the flash cache) and are always available in interrupts.
|
||||||
|
*/
|
||||||
|
memset = 0x40000354;
|
||||||
|
memcpy = 0x40000358;
|
||||||
|
memmove = 0x4000035c;
|
|
@ -428,11 +428,14 @@ func readSVD(path, sourceURL string) (*Device, error) {
|
||||||
licenseBlock = regexp.MustCompile(`\s+\n`).ReplaceAllString(licenseBlock, "\n")
|
licenseBlock = regexp.MustCompile(`\s+\n`).ReplaceAllString(licenseBlock, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove "-" characters from the device name because such characters cannot
|
||||||
|
// be used in build tags. Necessary for the ESP32-C3 for example.
|
||||||
|
nameLower := strings.ReplaceAll(strings.ToLower(device.Name), "-", "")
|
||||||
metadata := &Metadata{
|
metadata := &Metadata{
|
||||||
File: filepath.Base(path),
|
File: filepath.Base(path),
|
||||||
DescriptorSource: sourceURL,
|
DescriptorSource: sourceURL,
|
||||||
Name: device.Name,
|
Name: device.Name,
|
||||||
NameLower: strings.ToLower(device.Name),
|
NameLower: nameLower,
|
||||||
Description: strings.TrimSpace(device.Description),
|
Description: strings.TrimSpace(device.Description),
|
||||||
LicenseBlock: licenseBlock,
|
LicenseBlock: licenseBlock,
|
||||||
}
|
}
|
||||||
|
|
Загрузка…
Создание таблицы
Сослаться в новой задаче