diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 27795bf78..9d0004718 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -163,7 +163,7 @@ jobs: run: tar zxf sources.tar.gz - name: Docker Login - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -251,7 +251,7 @@ jobs: run: tar zxf sources.tar.gz - name: Docker Login - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/interpreters/smallvm/Kconfig b/interpreters/smallvm/Kconfig new file mode 100644 index 000000000..b8e9683d2 --- /dev/null +++ b/interpreters/smallvm/Kconfig @@ -0,0 +1,42 @@ +config INTERPRETERS_SMALLVM + tristate "smallVM" + default n + +if INTERPRETERS_SMALLVM + +config INTERPRETERS_SMALLVM_PRIORITY + int "smallVM priority" + default 100 + +config INTERPRETERS_SMALLVM_STACKSIZE + int "smallVM stack size" + default 8192 + +choice + prompt "Communication with Microblocks IDE" + default INTERPRETERS_SMALLVM_SERIAL + +config INTERPRETERS_SMALLVM_SERIAL + bool "Use serial line for communication" + +config INTERPRETERS_SMALLVM_TCP + bool "Use TCP for communication" + +endchoice + +if INTERPRETERS_SMALLVM_SERIAL + +config INTERPRETERS_SMALLVM_SERIAL_DEVICE + string "Serial device used for communication" + default "/dev/ttyACM0" + +endif + +if INTERPRETERS_SMALLVM_TCP + +config INTERPRETERS_SMALLVM_TCP_PORT + int "Port number to listen on" + default 9876 +endif + +endif diff --git a/interpreters/smallvm/Make.defs b/interpreters/smallvm/Make.defs new file mode 100644 index 000000000..e3b0edf2c --- /dev/null +++ b/interpreters/smallvm/Make.defs @@ -0,0 +1,3 @@ +ifneq ($(CONFIG_INTERPRETERS_SMALLVM),) +CONFIGURED_APPS += $(APPDIR)/interpreters/smallvm +endif diff --git a/interpreters/smallvm/Makefile b/interpreters/smallvm/Makefile new file mode 100644 index 000000000..1ade0c660 --- /dev/null +++ b/interpreters/smallvm/Makefile @@ -0,0 +1,22 @@ +include $(APPDIR)/Make.defs + +CFLAGS += -DGNUBLOCKS -DNUTTX -Wno-strict-prototypes -Wno-shadow + +CSRCS = dataPrims.c \ + interp.c \ + mem.c \ + miscPrims.c \ + persist.c \ + runtime.c \ + tinyJSON.c \ + varPrims.c \ + tftPrims.c + +MAINSRC = nuttx.c + +PROGNAME = smallvm +PRIORITY = $(CONFIG_INTERPRETERS_SMALLVM_PRIORITY) +STACKSIZE = $(CONFIG_INTERPRETERS_SMALLVM_STACKSIZE) +MODULE = $(CONFIG_INTERPRETERS_SMALLVM) + +include $(APPDIR)/Application.mk diff --git a/interpreters/smallvm/blinking_LED.ubp b/interpreters/smallvm/blinking_LED.ubp new file mode 100644 index 000000000..dc58ec070 --- /dev/null +++ b/interpreters/smallvm/blinking_LED.ubp @@ -0,0 +1,19 @@ +module main +author unknown +version 1 0 +description '' + +script 75 65 { +whenStarted +forever { + setUserLED true + waitMillis 500 + setUserLED false + waitMillis 500 +} +} + +script 55 295 (boardType) + +script 55 336 ('[misc:version]') + diff --git a/interpreters/smallvm/dataPrims.c b/interpreters/smallvm/dataPrims.c new file mode 100644 index 000000000..8608784a4 --- /dev/null +++ b/interpreters/smallvm/dataPrims.c @@ -0,0 +1,1197 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Copyright 2018 John Maloney, Bernat Romagosa, and Jens Mönig + +// dataPrims.cpp - Microblocks list and string primitives +// John Maloney, September 2017 + +#include +#include +#include + +#include "mem.h" +#include "interp.h" +#include "persist.h" + +// Helper Functions + +static int stringSize(OBJ obj) { + int wordCount = objWords(obj); + if (!wordCount) return 0; // empty string + char *s = (char *) &FIELD(obj, 0); + int byteCount = 4 * (wordCount - 1); + for (int i = 0; i < 4; i++) { + // scan the last word for the null terminator byte + if (s[byteCount] == 0) break; // found terminator + byteCount++; + } + return byteCount; +} + +static void printIntegerOrBooleanInto(OBJ obj, char *buf) { + // Helper for primJoin. Write a representation of obj into the given string. + // Assume buf has space for at least 20 characters. + + *buf = 0; // null terminator + if (isInt(obj)) sprintf(buf, "%d", obj2int(obj)); + else if (obj == falseObj) strcat(buf, "0"); + else if (obj == trueObj) strcat(buf, "1"); + else if (IS_TYPE(obj, StringType)) sprintf(buf, "%.15s...", obj2str(obj)); +} + +static inline int matches(const char *s, OBJ obj) { + // Return true if the given object is a string that matches s. + + return IS_TYPE(obj, StringType) && (0 == strcmp(s, obj2str(obj))); +} + +static inline char * nextUTF8(char *s) { + // Return a pointer to the start of the UTF8 character following the given one. + // If s points to a null byte (i.e. end of the string) return it unchanged. + + if (!*s) return s; // end of string + if ((uint8) *s < 128) return s + 1; // single-byte character + if (0xC0 == (*s & 0xC0)) s++; // start of multi-byte character + while (0x80 == (*s & 0xC0)) s++; // skip continuation bytes + return s; +} + +static int countUTF8(char *s) { + int count = 0; + while (*s) { + s = nextUTF8(s); + count++; + } + return count; +} + +static int unicodeCodePoint(char *s) { + // Return the Unicode code point starting at the given start byte. + + int result = -1; // bad unicode character; should not happen + uint8 *byte = (uint8 *) s; + int firstByte = byte[0]; + if (firstByte < 128) { + result = firstByte; // 7-bit ASCII + } else if (firstByte < 0xE0) { + result = ((firstByte & 0x1F) << 6) | (byte[1] & 0x3F); + } else if (firstByte < 0xF0) { + result = ((firstByte & 0xF) << 12) | ((byte[1] & 0x3F) << 6) | (byte[2] & 0x3F); + } else if (firstByte < 0xF8) { + result = ((firstByte & 0x7) << 18) | + ((byte[1] & 0x3F) << 12) | + ((byte[2] & 0x3F) << 6) | + (byte[3] & 0x3F); + } + return result; +} + +static int bytesForUnicode(int unicode) { + if (unicode < 0x80) return 1; // 7 bits, one byte + if (unicode < 0x800) return 2; // 11 bits, two bytes + if (unicode < 0x10000) return 3; // 16 bits, three bytes + if (unicode < 0x11000) return 4; // 21 bits, four bytes + return 0; // invalid unicode value; skip +} + +static uint8 * appendUTF8(uint8 *s, int unicode) { + // Append the UTF-8 bytes for the given Unicode character to the given string and + // return a pointer to the following byte. + + if (unicode < 0x80) { // 7 bits, one byte + *s++ = unicode; + } else if (unicode < 0x800) { // 11 bits, two bytes + *s++ = 0xC0 | ((unicode >> 6) & 0x1F); + *s++ = 0x80 | (unicode & 0x3F); + } else if (unicode < 0x10000) { // 16 bits, three bytes + *s++ = 0xE0 | ((unicode >> 12) & 0x0F); + *s++ = 0x80 | ((unicode >> 6) & 0x3F); + *s++ = 0x80 | (unicode & 0x3F); + } else if (unicode < 0x11000) { // 21 bits, four bytes + *s++ = 0xF0 | ((unicode >> 18) & 0x07); + *s++ = 0x80 | ((unicode >> 12) & 0x3F); + *s++ = 0x80 | ((unicode >> 6) & 0x3F); + *s++ = 0x80 | (unicode & 0x3F); + } + return s; +} + +static int substringIsInteger(char *start, char *end) { + // Return true if the substring from start up to end represents an integer. + + if ((start < end) && ('-' == *start)) start++; // skip leading minus sign, if any + if (start >= end) return false; // no digits; not an integer + while (start < end) { + int ch = *start++; + if ((ch < '0') || (ch > '9')) return false; // non-digit found + } + return true; +} + +static int substringToInteger(char *start, char *end) { + // Return the integer represented by the substring from start up to end. + + char s[20]; + int count = end - start; + if (count > 19) count = 19; + strncpy(s, start, count); + s[count] = 0; + return strtol(s, NULL, 10); +} + +// Growable Lists: +// First field is the item count (N). Items are stored in fields 2..N. +// Fields N+1..end are available for adding additional items without growing. + +OBJ primNewList(int argCount, OBJ *args) { + // Return a new List filled with zeros. Optional argument specifies size. + + int count = ((argCount > 0) && isInt(args[0])) ? obj2int(args[0]) : 0; + if (count < 0) count = 0; + OBJ fillValue = (argCount > 1) ? args[1] : zeroObj; + + OBJ result = newObj(ListType, count + 1, fillValue); + if (result) FIELD(result, 0) = int2obj(count); + return result; +} + +OBJ primFillList(int argCount, OBJ *args) { + OBJ obj = args[0]; + OBJ value = args[1]; + + if (IS_TYPE(obj, ListType)) { + int count = obj2int(FIELD(obj, 0)); + if (count >= WORDS(obj))count = WORDS(obj) - 1; + for (int i = 0; i < count; i++) FIELD(obj, i + 1) = value; + } else if (IS_TYPE(obj, ByteArrayType)) { + if (!isInt(value)) return fail(byteArrayStoreError); + int byteValue = obj2int(value); + if ((byteValue < 0) || (byteValue > 255)) return fail(byteArrayStoreError); + uint8 *dst = (uint8 *) &FIELD(obj, 0); + uint8 *end = dst + (4 * WORDS(obj)); + while (dst < end) *dst++ = byteValue; + } else { + fail(needsListError); + } + return falseObj; +} + +OBJ primAt(int argCount, OBJ *args) { + OBJ obj = args[1]; + int i, count = 0; + + if (IS_TYPE(obj, ListType)) { + count = obj2int(FIELD(obj, 0)); + if (count >= WORDS(obj)) count = WORDS(obj) - 1; + } else if (IS_TYPE(obj, StringType)) { + count = stringSize(obj); + } else if (IS_TYPE(obj, ByteArrayType)) { + count = BYTES(obj); + } + + OBJ arg0 = args[0]; + if (isInt(arg0)) { + i = obj2int(arg0); + if ((i < 1) || (i > count)) return fail(indexOutOfRangeError); + } else if (matches("random", arg0)) { + if (count == 0) return fail(indexOutOfRangeError); + i = (rand() % count) + 1; + } else if (matches("last", arg0)) { + i = count; + } else if (IS_TYPE(arg0, StringType)) { + i = evalInt(arg0); + if ((0 == i) && !matches("0", arg0)) return fail(needsIntegerIndexError); + if ((i < 1) || (i > count)) return fail(indexOutOfRangeError); + } else { + return fail(needsIntegerIndexError); + } + + if (IS_TYPE(obj, ListType)) { + return FIELD(obj, i); + } else if (IS_TYPE(obj, StringType)) { + char *start = obj2str(obj); + while (i-- > 1) { // find start of the ith Unicode character + if (!*start) return fail(indexOutOfRangeError); // end of string + start = nextUTF8(start); + } + int byteCount = nextUTF8(start) - start; + OBJ result = newString(byteCount); + if (result) { + memcpy(obj2str(result), start, byteCount); + } + return result; + } else if (IS_TYPE(obj, ByteArrayType)) { + uint8 *bytes = (uint8 *) &FIELD(obj, 0); + return int2obj(bytes[i - 1]); + } + return fail(needsListError); +} + +OBJ primAtPut(int argCount, OBJ *args) { + OBJ obj = args[1]; + OBJ value = args[2]; + int count, i; + uint32 byteValue = 0; + + if (IS_TYPE(obj, ListType)) { + count = obj2int(FIELD(obj, 0)); + if (count >= WORDS(obj)) count = WORDS(obj) - 1; + } else if (IS_TYPE(obj, ByteArrayType)) { + count = BYTES(obj); + if (!isInt(value)) return fail(byteArrayStoreError); + byteValue = obj2int(value); + if (byteValue > 255) return fail(byteArrayStoreError); + } else { + return fail(needsListError); + } + + if (matches("all", args[0])) { + if (IS_TYPE(obj, ListType)) { + for (i = 1; i <= count; i++) { + FIELD(obj, i) = value; + } + } else if (IS_TYPE(obj, ByteArrayType)) { + for (i = 1; i <= count; i++) { + ((uint8 *) &FIELD(obj, 0))[i - 1] = byteValue; + } + } + return falseObj; + } + + OBJ arg0 = args[0]; + if (isInt(arg0)) { + i = obj2int(arg0); + if ((i < 1) || (i > count)) return fail(indexOutOfRangeError); + } else if (matches("last", arg0)) { + i = count; + } else if (IS_TYPE(arg0, StringType)) { + i = evalInt(arg0); + if ((0 == i) && !matches("0", arg0)) return fail(needsIntegerIndexError); + if ((i < 1) || (i > count)) return fail(indexOutOfRangeError); + } else { + return fail(needsIntegerIndexError); + } + + if (IS_TYPE(obj, ListType)) { + FIELD(obj, i) = value; + } else if (IS_TYPE(obj, ByteArrayType)) { + ((uint8 *) &FIELD(obj, 0))[i - 1] = byteValue; + } + return falseObj; +} + +OBJ primLength(int argCount, OBJ *args) { + OBJ obj = args[0]; + + if (IS_TYPE(obj, ListType)) { + return FIELD(obj, 0); // actual count stored in first field + } else if (IS_TYPE(obj, ByteArrayType)) { + return int2obj(BYTES(obj)); + } else if (IS_TYPE(obj, StringType)) { + return int2obj(countUTF8(obj2str(obj))); + } + return zeroObj; +} + +// Named primitives + +OBJ primMakeList(int argCount, OBJ *args) { + OBJ result = newObj(ListType, argCount + 1, falseObj); + if (!result) return result; // allocation failed + + FIELD(result, 0) = int2obj(argCount); + for (int i = 0; i < argCount; i++) FIELD(result, i + 1) = args[i]; + return result; +} + +OBJ primRange(int argCount, OBJ *args) { + if (argCount < 2) return fail(notEnoughArguments); + int start = evalInt(args[0]); + int end = evalInt(args[1]); + int incr = (argCount > 2) ? evalInt(args[2]) : 1; + if (incr < 1) return fail(needsPositiveIncrement); // increment must be >= 1 + + int count; + if (end >= start) { + count = ((end - start) / incr) + 1; + } else { + count = ((start - end) / incr) + 1; + incr = -incr; // make the increment negative + } + + OBJ result = newObj(ListType, count + 1, falseObj); + if (!result) return result; // allocation failed + + FIELD(result, 0) = int2obj(count); + int n = start; + for (int i = 0; i < count; i++) { + FIELD(result, i + 1) = int2obj(n); + n += incr; + } + return result; +} + +OBJ primListAddLast(int argCount, OBJ *args) { + // Add the given item to the end of the List. Grow if necessary. + + OBJ list = args[1]; + if (!IS_TYPE(list, ListType)) return fail(needsListError); + + int count = obj2int(FIELD(list, 0)); + if (count >= (WORDS(list) - 1)) { // no more capacity; try to grow + int growBy = count / 3; + if (growBy < 4) growBy = 3; + if (growBy > 100) growBy = 100; + + list = resizeObj(list, WORDS(list) + growBy); + } + if (count < (WORDS(list) - 1)) { // append item if there's room + count++; + FIELD(list, count) = args[0]; + FIELD(list, 0) = int2obj(count); + } + return falseObj; +} + +OBJ primListDelete(int argCount, OBJ *args) { + // Delete item(s) from the given List. + + if (argCount < 2) return fail(notEnoughArguments); + if (!IS_TYPE(args[1], ListType)) return fail(needsListError); + OBJ list = args[1]; + int count = obj2int(FIELD(list, 0)); + if (count >= WORDS(list)) count = WORDS(list) - 1; + + int i; + if (isInt(args[0])) { + i = obj2int(args[0]); + if ((i < 1) || (i > count)) return fail(indexOutOfRangeError); + } else if (matches("all", args[0])) { + for (int i = 0; i <= count; i++) FIELD(list, i) = zeroObj; + return falseObj; + } else if (matches("last", args[0])) { + if (count) { + FIELD(list, count) = zeroObj; + FIELD(list, 0) = int2obj(count - 1); + } + return falseObj; + } else if (IS_TYPE(args[0], StringType)) { + i = evalInt(args[0]); + if ((0 == i) && !matches("0", args[0])) return fail(needsIntegerIndexError); + if ((i < 1) || (i > count)) return fail(indexOutOfRangeError); + } else { + return fail(needsIntegerIndexError); + } + + while (i < count) { + FIELD(list, i) = FIELD(list, i + 1); + i++; + } + FIELD(list, count) = zeroObj; // clear final field + FIELD(list, 0) = int2obj(count - 1); + return falseObj; +} + +OBJ primCopyFromTo(int argCount, OBJ *args) { + // Return a copy of the first argument (a string or list) between the indices give by the + // second and third arguments. If the optional third argument is not supplied it is taken + // to be the last index. + + if (argCount < 2) return fail(notEnoughArguments); + if (!isInt(args[1])) return fail(needsIntegerError); + int startIndex = obj2int(args[1]); + if (startIndex < 1) startIndex = 1; + if ((argCount > 2) && !isInt(args[2])) return fail(needsIntegerError); + + OBJ src = args[0]; + if (IS_TYPE(src, ListType)) { + int srcLen = obj2int(FIELD(src, 0)); + int endIndex = (argCount > 2) ? obj2int(args[2]) : srcLen; + if (endIndex > srcLen) endIndex = srcLen; + int resultLen = (endIndex - startIndex) + 1; + if (resultLen < 0) resultLen = 0; + OBJ result = newObj(ListType, resultLen + 1, int2obj(0)); + if (result) { + src = args[0]; // update src after possible GC + FIELD(result, 0) = int2obj(resultLen); + OBJ *dst = &FIELD(result, 1); + for (int i = startIndex; i <= endIndex; i++) *dst++ = FIELD(src, i); + } + return result; + } else if (IS_TYPE(src, StringType)) { + int srcLen = countUTF8(obj2str(src)); + int endIndex = (argCount > 2) ? obj2int(args[2]) : srcLen; + if (endIndex > srcLen) endIndex = srcLen; + if (startIndex > endIndex) return newString(0); + + char *start = obj2str(src); + for (int i = 1; i < startIndex; i++) start = nextUTF8(start); + int startOffset = start - obj2str(src); + char *end = start; + for (int i = startIndex; i <= endIndex; i++) end = nextUTF8(end); + int byteCount = end - start; + + OBJ result = newString(byteCount); + if (result) { + memcpy(obj2str(result), obj2str(args[0]) + startOffset, byteCount); + } + return result; + } else if (IS_TYPE(src, ByteArrayType)) { + int srcLen = BYTES(src); + int endIndex = (argCount > 2) ? obj2int(args[2]) : srcLen; + if (endIndex > srcLen) endIndex = srcLen; + if (startIndex > endIndex) return newObj(ByteArrayType, 0, falseObj); + + char *base = (char *) (&FIELD(src, 0)); + int byteCount = (endIndex - startIndex) + 1; + int wordCount = (byteCount + 3) / 4; + OBJ result = newObj(ByteArrayType, wordCount, falseObj); + if (result) { + setByteCountAdjust(result, byteCount); + memcpy(&FIELD(result, 0), base + startIndex - 1, byteCount); + } + return result; + } + return fail(needsIndexable); +} + +OBJ primJoin(int argCount, OBJ *args) { + if (argCount < 2) return fail(notEnoughArguments); + char buf[50]; + int count, resultCount = 0; + OBJ arg, arg1 = args[0]; + OBJ result = falseObj; + + if (IS_TYPE(arg1, ListType)) { + for (int i = 0; i < argCount; i++) { + arg = args[i]; + if (!IS_TYPE(arg, ListType)) return fail(joinArgsNotSameType); + resultCount += obj2int(FIELD(arg, 0)); + } + result = newObj(ListType, resultCount + 1, int2obj(0)); + if (!result) return result; // allocation failed + FIELD(result, 0) = int2obj(resultCount); + OBJ *dst = &FIELD(result, 1); + for (int i = 0; i < argCount; i++) { + arg = args[i]; + count = obj2int(FIELD(arg, 0)); + if (count >= WORDS(arg)) count = WORDS(arg) - 1; + for (int j = 0; j < count; j++) *dst++ = FIELD(arg, j + 1); + } + } else if (IS_TYPE(arg1, ByteArrayType)) { + for (int i = 0; i < argCount; i++) { + arg = args[i]; + if (IS_TYPE(arg, ByteArrayType)) { + resultCount += BYTES(arg); + } else if (IS_TYPE(arg, StringType)) { + resultCount += stringSize(arg); + } else { + return fail(joinArgsNotSameType); + } + } + int wordCount = (resultCount + 3) / 4; + result = newObj(ByteArrayType, wordCount, falseObj); + if (!result) return result; // allocation failed + setByteCountAdjust(result, resultCount); + + char *dst = (char *) &FIELD(result, 0); + for (int i = 0; i < argCount; i++) { + arg = args[i]; + int byteCount = IS_TYPE(arg, ByteArrayType) ? BYTES(arg) : stringSize(arg); + char *src = (char *) &FIELD(arg, 0); + for (int j = 0; j < byteCount; j++) *dst++ = src[j]; + } + } else { + for (int i = 0; i < argCount; i++) { + arg = args[i]; + if (IS_TYPE(arg, StringType)) { + resultCount += stringSize(arg); + } else if (isInt(arg) || isBoolean(arg)) { + printIntegerOrBooleanInto(arg, buf); + resultCount += strlen(buf); + } else if (IS_TYPE(arg, ByteArrayType)) { + resultCount += BYTES(arg); + } else { + return fail(joinArgsNotSameType); + } + } + result = newString(resultCount); + if (!result) return result; // allocation failed + char *dst = (char *) &FIELD(result, 0); + for (int i = 0; i < argCount; i++) { + arg = args[i]; + if (IS_TYPE(arg, StringType)) { + count = stringSize(arg); + memcpy(dst, obj2str(arg), count); + dst += count; + } else if (isInt(arg) || isBoolean(arg)) { + printIntegerOrBooleanInto(arg, buf); + count = strlen(buf); + memcpy(dst, buf, count); + dst += count; + } else if (IS_TYPE(arg, ByteArrayType)) { + count = BYTES(arg); + memcpy(dst, (char *) &FIELD(arg, 0), count); + dst += count; + } + } + *dst = 0; // null terminator + } + return result; +} + +OBJ primSplit(int argCount, OBJ *args) { + if (argCount < 2) return fail(notEnoughArguments); + if (!IS_TYPE(args[0], StringType)) return fail(needsStringError); + if (!IS_TYPE(args[1], StringType)) return fail(needsStringError); + char *s = obj2str(args[0]); + char *delim = obj2str(args[1]); + int delimLen = strlen(delim); + int convertNums = ((argCount > 2) && (trueObj == args[2])); + + // count substrings for result list + int resultCount = 0; + if (delimLen == 0) { + resultCount = countUTF8(s); + } else { + if (strstr(s, delim) == s) resultCount++; // s begins with a delimiter + char *match = s; + while (match) { + resultCount++; + match = strstr(match + delimLen, delim); + } + } + + // allocate result list (stored in tempGCRoot so it will be processed by garbage collector + // if a GC happens during a later allocation) + tempGCRoot = newObj(ListType, resultCount + 1, zeroObj); + if (!tempGCRoot) return tempGCRoot; // allocation failed + FIELD(tempGCRoot, 0) = int2obj(resultCount); + + // add substrings to the result list + if (delimLen == 0) { + // no delimiter provided; return a list containing the characters of s + char *last = s; + char *next = nextUTF8(last); + for (int i = 0; i < resultCount; i++) { + // allocate string and save in list + int byteCount = next - last; + OBJ item = newStringFromBytes(last, byteCount); + if (!item) return falseObj; // allocation failed + FIELD(tempGCRoot, i + 1) = item; + last = next; + next = nextUTF8(last); + } + } else if (1 == resultCount) { // no delimiter found; return list with unsplit source string + FIELD(tempGCRoot, 1) = args[0]; + } else { + int i = 1; + char *last = s; + char *end = s + strlen(s); + char *next = strstr(last, delim); + while (next && (i <= resultCount)) { + OBJ item; + int byteCount = next - last; + if (convertNums && (substringIsInteger(last, next))) { + item = int2obj(substringToInteger(last, next)); + } else { + item = newStringFromBytes(last, byteCount); + if (!item) return falseObj; // allocation failed + } + FIELD(tempGCRoot, i++) = item; + last = next + delimLen; + next = strstr(last, delim); + if (!next) next = end; // handle string after final delimiter + } + } + return tempGCRoot; +} + +OBJ primJoinStrings(int argCount, OBJ *args) { + if (argCount < 1) return fail(notEnoughArguments); + if (!IS_TYPE(args[0], ListType)) return fail(needsListError); + + OBJ stringList = args[0]; + int count = obj2int(FIELD(stringList, 0)); + if (count <= 0) return newString(0); + + char *separator = ((argCount > 1) && IS_TYPE(args[1], StringType)) ? obj2str(args[1]) : (char *) ""; + int separatorLen = strlen(separator); + char buf[50]; + + int resultBytes = (count - 1) * separatorLen; + for (int i = 0; i < count; i++) { + OBJ item = FIELD(stringList, i + 1); + if (IS_TYPE(item, StringType)) { + resultBytes += stringSize(item); + } else if (IS_TYPE(item, ByteArrayType)) { + resultBytes += BYTES(item); + } else if (isInt(item) || isBoolean(item)) { + printIntegerOrBooleanInto(item, buf); + resultBytes += strlen(buf); + } else { + resultBytes += strlen(obj2str(item)); + } + } + OBJ result = newString(resultBytes); + if (!result) return result; // allocation failed + + // update temps after possible GC triggered by newString() + stringList = args[0]; + if (separatorLen) separator = obj2str(args[1]); + + char *dst = obj2str(result); + for (int i = 0; i < count; i++) { + OBJ item = FIELD(stringList, i + 1); + char *s = obj2str(item); + if (isInt(item) || isBoolean(item)) { + printIntegerOrBooleanInto(item, buf); + s = buf; + } + int n = strlen(s); + if (IS_TYPE(item, ByteArrayType)) { + s = (char *) &FIELD(item, 0); + n = BYTES(item); + } + memcpy(dst, s, n); + dst += n; + if (separatorLen && (i < (count - 1))) { + memcpy(dst, separator, separatorLen); + dst += separatorLen; + } + } + return result; +} + +OBJ primFind(int argCount, OBJ *args) { + // If both arguments are strings, return the index of next instance the second string + // in the first or -1 if not found. If the second argument is a list, return the index + // of the first argument in the list or -1 if not found. + // An optional third argument can be used to specify the starting point for the search. + + if (argCount < 2) return fail(notEnoughArguments); + OBJ arg0 = args[0]; + OBJ arg1 = args[1]; + int startOffset = ((argCount > 2) && isInt(args[2])) ? obj2int(args[2]) : 1; + if (startOffset < 1) startOffset = 1; + + if (IS_TYPE(arg1, StringType)) { // search for substring in a string + if (!(IS_TYPE(arg0, StringType))) return fail(needsStringError); + if (startOffset > stringSize(arg1)) return int2obj(-1); // not found + char *s = obj2str(arg1); + char *sought = obj2str(arg0); + if (0 == sought[0]) return int2obj(-1); // empty string + char *match = strstr(s + startOffset - 1, sought); + if (!match) return int2obj(-1); + // count the Unicode characters up to match + int charIndex = 1; + while (*s && (s < match)) { + s = nextUTF8(s); + charIndex++; + } + return int2obj(charIndex); + } else if (IS_TYPE(arg1, ListType)) { // search in a list + int listCount = obj2int(FIELD(arg1, 0)); + if (startOffset > listCount) return int2obj(-1); // not found + if (IS_TYPE(arg0, StringType)) { // search for a string in a list + char *sought = obj2str(arg0); + for (int i = startOffset; i <= listCount; i++) { + OBJ item = FIELD(arg1, i); + if (item == arg0) return int2obj(i); // identical + if (IS_TYPE(item, StringType) && (0 == strcmp(sought, obj2str(item)))) { + return int2obj(i); // string match + } + } + } else { // search for an integer, boolean, or other object in a list + for (int i = startOffset; i <= listCount; i++) { + if (FIELD(arg1, i) == arg0) return int2obj(i); // identical + } + } + return int2obj(-1); + } else if (IS_TYPE(arg1, ByteArrayType)) { // search in a ByteArray + uint8 *target = (uint8 *) &FIELD(arg1, 0); + int targetSize = BYTES(arg1); + if (startOffset > targetSize) return int2obj(-1); // not found + uint8 *sought; + int soughtSize = 0; + if (IS_TYPE(arg0, ByteArrayType)) { + sought = (uint8 *) &FIELD(arg0, 0); + soughtSize = BYTES(arg0); + } else if (IS_TYPE(arg0, StringType)) { + sought = (uint8 *) obj2str(arg0); + soughtSize = stringSize(arg0); + } else if (isInt(arg0)) { + // search for a byte in a ByteArray + int soughtByte = obj2int(arg0); + if ((soughtByte < 0) || (soughtByte > 255)) return fail(byteOutOfRange); + for (int i = startOffset - 1; i <= targetSize; i++) { + if (target[i] == soughtByte) return int2obj(i + 1); + } + return int2obj(-1); + } else { + // a ByteArray can be searched for a String or ByteArray + return fail(nonComparableError); + } + int lastPotenialMatch = targetSize - soughtSize; + uint8 *soughtEnd = sought + soughtSize; + for (int i = startOffset - 1; i <= lastPotenialMatch; i++) { + uint8 *p1 = target + i; + uint8 *p2 = sought; + while (p2 < soughtEnd) { + if (*p1 != *p2) break; + p1++; + p2++; + } + if (p2 == soughtEnd) return int2obj(i + 1); // found a match! + } + return int2obj(-1); + } + return int2obj(-1); +} + +OBJ primUnicodeAt(int argCount, OBJ *args) { + // Return the Unicode value (an integer) for the given character of a string. + // Return -1 if the given character is not a valid UTF-8 Unicode character. + + if (argCount < 2) return fail(notEnoughArguments); + + if (!isInt(args[0])) return fail(needsIntegerIndexError); + if (!IS_TYPE(args[1], StringType)) return fail(needsStringError); + int i = obj2int(args[0]); + char *s = obj2str(args[1]); + if ((i < 1) || (i > countUTF8(s))) return fail(indexOutOfRangeError); + + for (; i > 1; i--) s = nextUTF8(s); // find first byte of desired Unicode character + int result = unicodeCodePoint(s); + return int2obj(result); +} + +OBJ primUnicodeString(int argCount, OBJ *args) { + // Return a string containing the given Unicode character(s). + + if (argCount < 1) return fail(notEnoughArguments); + OBJ arg = args[0]; + + if (isInt(arg) || IS_TYPE(arg, StringType)) { // convert a single integer to a Unicode character + uint8 buf[8]; // buffer for one UTF-8 character + uint8 *s = appendUTF8(buf, evalInt(arg)); + int byteCount = s - buf; + return newStringFromBytes((char *) buf, byteCount); + } else if (IS_TYPE(arg, ListType)) { // convert list of integers to a Unicode string + int listCount = obj2int(FIELD(arg, 0)); + int utfByteCount = 0; + for (int i = 1; i <= listCount; i++) { + OBJ item = FIELD(arg, i); + utfByteCount += bytesForUnicode(evalInt(item)); + } + if (failure()) return fail(needsIntOrListOfInts); // evalInt failed on some list item + + OBJ result = newString(utfByteCount); + if (!result) return result; // allocation failed + arg = args[0]; // update arg after possible GC + uint8 *s = (uint8 *) obj2str(result); + for (int i = 1; i <= listCount; i++) { + OBJ item = FIELD(arg, i); + s = appendUTF8(s, evalInt(item)); + } + return result; + } + return fail(needsIntOrListOfInts); +} + +OBJ primNewByteArray(int argCount, OBJ *args) { + if (argCount < 1) return fail(notEnoughArguments); + if (!isInt(args[0])) return fail(needsIntegerError); + + int byteCount = obj2int(args[0]); + if (byteCount < 0) byteCount = 0; + + int fillWord = 0; + if (argCount > 1) { + if (!isInt(args[1])) return fail(needsIntegerError); + int fillByte = obj2int(args[1]); + if ((fillByte < 0) || (fillByte > 255)) return fail(byteArrayStoreError); + fillWord = (fillByte << 24) | (fillByte << 16) | (fillByte << 8) | fillByte; + } + + OBJ result = newObj(ByteArrayType, (byteCount + 3) / 4, (OBJ) fillWord); + if (result) setByteCountAdjust(result, byteCount); + return result; +} + +OBJ primAsByteArray(int argCount, OBJ *args) { + if (argCount < 1) return fail(notEnoughArguments); + OBJ arg = args[0]; + OBJ result = falseObj; + int byteCount; + + if (isInt(arg)) { + int byteValue = obj2int(arg); + if ((byteValue < 0) || (byteValue > 255)) return fail(byteArrayStoreError); + result = newObj(ByteArrayType, 1, falseObj); + if (result) { + setByteCountAdjust(result, 1); + *((uint8 *) &FIELD(result, 0)) = byteValue; + } + } else if (IS_TYPE(arg, StringType)) { + byteCount = stringSize(arg); + result = newObj(ByteArrayType, (byteCount + 3) / 4, falseObj); + if (result) { + setByteCountAdjust(result, byteCount); + memcpy(&FIELD(result, 0), obj2str(arg), byteCount); + } + } else if (IS_TYPE(arg, ListType)) { + byteCount = obj2int(FIELD(arg, 0)); + result = newObj(ByteArrayType, (byteCount + 3) / 4, falseObj); + OBJ arg = args[0]; // update arg in case allocation caused a GC + if (result) { + setByteCountAdjust(result, byteCount); + uint8 *bytes = (uint8 *) &FIELD(result, 0); + for (int i = 0; i < byteCount; i++) { + OBJ item = FIELD(arg, i + 1); + if (isInt(item)) { + int byteValue = obj2int(item); + if ((byteValue < 0) || (byteValue > 255)) return fail(byteArrayStoreError); + bytes[i] = byteValue; + } + } + } + } else if (IS_TYPE(arg, ByteArrayType)) { + result = arg; + } else { + return fail(byteArrayStoreError); + } + return result; +} + +OBJ primFreeMemory(int argCount, OBJ *args) { + return int2obj(wordsFree()); +} + +// Helper functions for convert primitive + +static OBJ stringToList(OBJ strObj) { + // Return a list containing the Unicode character values (codepoints) of the given string. + + int itemCount = countUTF8(obj2str(strObj)); + + tempGCRoot = strObj; // record strObj in case allocation triggers GC that moves it + OBJ result = newObj(ListType, itemCount + 1, falseObj); + strObj = tempGCRoot; // restore strObj + if (!result) return fail(insufficientMemoryError); // allocation failed + FIELD(result, 0) = int2obj(itemCount); + + char *codepointPtr = obj2str(strObj); + for (int i = 0; i < itemCount; i++) { + FIELD(result, i + 1) = int2obj(unicodeCodePoint(codepointPtr)); + codepointPtr = nextUTF8(codepointPtr); + } + return result; +} + +static OBJ stringToByteArray(OBJ strObj) { + // Return a byte array containing the bytes of the given string (i.e. its utf8 encoding). + + int byteCount = strlen(obj2str(strObj)); + int wordCount = (byteCount + 3) / 4; + + tempGCRoot = strObj; // record strObj in case allocation triggers GC that moves it + OBJ result = newObj(ByteArrayType, wordCount, falseObj); + strObj = tempGCRoot; // restore strObj + if (!result) return fail(insufficientMemoryError); // allocation failed + setByteCountAdjust(result, byteCount); + + char *src = obj2str(strObj); + char *dst = (char *) &FIELD(result, 0); + for (int i = 0; i < byteCount; i++) { + *dst++ = src[i]; + } + return result; +} + +static OBJ listToString(OBJ listObj) { + // Return a string the given list of Unicode characters (code points). + // Assume the list contains only integers that represent valid Unicode characters. + + int itemCount = obj2int(FIELD(listObj, 0)); + int utfByteCount = 0; + for (int i = 1; i <= itemCount; i++) { + OBJ item = FIELD(listObj, i); + utfByteCount += bytesForUnicode(evalInt(item)); + } + if (failure()) return fail(needsIntOrListOfInts); // evalInt failed on some list item + + tempGCRoot = listObj; // record listObj in case allocation triggers GC that moves it + OBJ result = newString(utfByteCount); + listObj = tempGCRoot; // restore listObj + if (!result) return result; // allocation failed + + uint8 *s = (uint8 *) obj2str(result); + for (int i = 1; i <= itemCount; i++) { + OBJ item = FIELD(listObj, i); + if (!isInt(item)) return fail(needsListOfIntegers); + int codepoint = obj2int(item); + if ((codepoint < 0) || (codepoint > 1114111)) return fail(invalidUnicodeValue); + s = appendUTF8(s, codepoint); + } + return result; +} + +static OBJ listToByteArray(OBJ listObj) { + // Return a byte array containing the contents of the given list. + // Assume the list elements are integers between 0 and 255. + + int byteCount = obj2int(FIELD(listObj, 0)); + int wordCount = (byteCount + 3) / 4; + + tempGCRoot = listObj; // record listObj in case allocation triggers GC that moves it + OBJ result = newObj(ByteArrayType, wordCount, falseObj); + listObj = tempGCRoot; // restore listObj + if (!result) return fail(insufficientMemoryError); // allocation failed + setByteCountAdjust(result, byteCount); + + OBJ *src = &FIELD(listObj, 1); + char *dst = (char *) &FIELD(result, 0); + for (int i = 0; i < byteCount; i++) { + OBJ item = *src++; + if (!isInt(item)) return fail(needsListOfIntegers); + int byte = obj2int(item); + if ((byte < 0) || (byte > 255)) return fail(byteArrayStoreError); + *dst++ = byte; + } + return result; +} + +static OBJ byteArrayToString(OBJ byteArrayObj) { + // Return a string containing the given bytes. + // Assume the byte array is a valid UTF8 string encoding. + + int byteCount = BYTES(byteArrayObj); + tempGCRoot = byteArrayObj; // record byteArrayObj in case allocation triggers GC that moves it + OBJ result = newString(byteCount); + byteArrayObj = tempGCRoot; // restore byteArrayObj + if (!result) return fail(insufficientMemoryError); // allocation failed + + char *src = (char *) &FIELD(byteArrayObj, 0); + char *dst = (char *) &FIELD(result, 0); + for (int i = 0; i < byteCount; i++) { + *dst++ = *src++; + } + + return result; +} + +static OBJ byteArrayToList(OBJ byteArrayObj) { + // Return a list containing the byte values of the given byte array. + + int itemCount = BYTES(byteArrayObj); + tempGCRoot = byteArrayObj; // record byteArrayObj in case allocation triggers GC that moves it + OBJ result = newObj(ListType, itemCount + 1, falseObj); + byteArrayObj = tempGCRoot; // restore byteArrayObj + if (!result) return fail(insufficientMemoryError); // allocation failed + FIELD(result, 0) = int2obj(itemCount); + + uint8 *bytes = (uint8 *) &FIELD(byteArrayObj, 0); + for (int i = 0; i < itemCount; i++) { + FIELD(result, i + 1) = int2obj(bytes[i]); + } + return result; +} + +static OBJ singletonList(OBJ anObj) { + // Return a singleton list containing the given object. + + tempGCRoot = anObj; // record anObj in case allocation triggers GC that moves it + OBJ result = newObj(ListType, 2, falseObj); + anObj = tempGCRoot; // restore anObj + if (!result) return fail(insufficientMemoryError); // allocation failed + FIELD(result, 0) = int2obj(1); + FIELD(result, 1) = anObj; + return result; +} + +static OBJ singletonByteArray(OBJ anObj) { + // Return a singleton list containing the given number or boolean object. + + int byteValue = 0; + if (isInt(anObj)) { + byteValue = obj2int(anObj); + if ((byteValue < 0) || (byteValue > 255)) return fail(byteArrayStoreError); + } else { + // Convert boolean to byte value. + byteValue = (anObj == falseObj) ? 0 : 1; + } + + OBJ result = newObj(ByteArrayType, 1, falseObj); + if (!result) return fail(insufficientMemoryError); // allocation failed + setByteCountAdjust(result, 1); + *((uint8 *) &FIELD(result, 0)) = byteValue; + return result; +} + +OBJ primConvertType(int argCount, OBJ *args) { + if (argCount < 2) return fail(notEnoughArguments); + OBJ srcObj = args[0]; + int srcType = objType(srcObj); + char *dstTypeName = obj2str(args[1]); + + int dstType = -1; + if (strcmp(dstTypeName, "boolean") == 0) dstType = BooleanType; + if (strcmp(dstTypeName, "number") == 0) dstType = IntegerType; + if (strcmp(dstTypeName, "string") == 0) dstType = StringType; + if (strcmp(dstTypeName, "list") == 0) dstType = ListType; + if (strcmp(dstTypeName, "byte array") == 0) dstType = ByteArrayType; + if (dstType < 0) return fail(unknownDatatype); + + char s[32]; + char *srcStr; + OBJ result = srcObj; // default used when converting object to its current type + int srcItemCount = 0; + + switch (srcType) { + case BooleanType: + switch (dstType) { + case IntegerType: + result = int2obj((srcObj == trueObj) ? 1 : 0); + break; + case StringType: + result = newStringFromBytes(((srcObj == trueObj) ? "1" : "0"), 1); + break; + case ListType: + return singletonList(srcObj); + break; + case ByteArrayType: + return singletonByteArray(srcObj); + break; + } + break; + case IntegerType: + switch (dstType) { + case BooleanType: + // 0 is false; all other numbers are true + result = (obj2int(srcObj) == 0) ? falseObj : trueObj; + break; + case StringType: + sprintf(s, "%d", obj2int(srcObj)); + result = newStringFromBytes(s, strlen(s)); + break; + case ListType: + return singletonList(srcObj); + break; + case ByteArrayType: + return singletonByteArray(srcObj); + break; + } + break; + case StringType: + switch (dstType) { + case BooleanType: + srcStr = obj2str(srcObj); + // "0" is false; all other strings are true + result = (strcmp(srcStr, "0") == 0) ? falseObj : trueObj; + break; + case IntegerType: + result = int2obj(evalInt(srcObj)); + break; + case ListType: + result = stringToList(srcObj); + break; + case ByteArrayType: + result = stringToByteArray(srcObj); + break; + } + break; + case ListType: + srcItemCount = obj2int(FIELD(srcObj, 0)); + switch (dstType) { + case BooleanType: + if ((srcItemCount != 1) || (objType(FIELD(srcObj, 1)) != BooleanType)) { + return fail(cannotConvertToBoolean); + } + return FIELD(srcObj, 1); + break; + case IntegerType: + if ((srcItemCount != 1) || !isInt(FIELD(srcObj, 1))) { + return fail(cannotConvertToInteger); + } + return FIELD(srcObj, 1); + break; + case StringType: + result = listToString(srcObj); + break; + case ByteArrayType: + result = listToByteArray(srcObj); + break; + } + break; + case ByteArrayType: + srcItemCount = BYTES(srcObj); + switch (dstType) { + case BooleanType: + if (srcItemCount != 1) return fail(cannotConvertToBoolean); + return (*((uint8 *) &FIELD(result, 0)) ? trueObj : falseObj); + break; + case IntegerType: + if (srcItemCount != 1) return fail(cannotConvertToInteger); + return int2obj(*((uint8 *) &FIELD(result, 0))); + break; + case StringType: + result = byteArrayToString(srcObj); + break; + case ListType: + result = byteArrayToList(srcObj); + break; + } + break; + } + return result; +} + +OBJ primToString(int argCount, OBJ *args) { + if (argCount < 1) return fail(notEnoughArguments); + OBJ srcObj = args[0]; + char s[32]; // buffer for number-to-string conversion + const char *s2 = NULL; + + switch (objType(srcObj)) { + case BooleanType: + return newStringFromBytes(((srcObj == trueObj) ? "1" : "0"), 1); + case IntegerType: + sprintf(s, "%d", obj2int(srcObj)); + return newStringFromBytes(s, strlen(s)); + case StringType: + return srcObj; + case ListType: + case ByteArrayType: + s2 = obj2str(srcObj); + return newStringFromBytes(s2, strlen(s2)); + } + return newString(0); +} + +// Primitives + +static PrimEntry entries[] = { + {"makeList", primMakeList}, + {"range", primRange}, + {"addLast", primListAddLast}, + {"delete", primListDelete}, + {"join", primJoin}, + {"split", primSplit}, + {"copyFromTo", primCopyFromTo}, + {"find", primFind}, + {"joinStrings", primJoinStrings}, + {"unicodeAt", primUnicodeAt}, + {"unicodeString", primUnicodeString}, + {"newByteArray", primNewByteArray}, + {"asByteArray", primAsByteArray}, + {"freeMemory", primFreeMemory}, + {"convertType", primConvertType}, + {"toString", primToString}, +}; + +void addDataPrims() { + addPrimitiveSet(DataPrims, "data", sizeof(entries) / sizeof(PrimEntry), entries); +} diff --git a/interpreters/smallvm/drawing_TFT.ubp b/interpreters/smallvm/drawing_TFT.ubp new file mode 100644 index 000000000..8234071c7 --- /dev/null +++ b/interpreters/smallvm/drawing_TFT.ubp @@ -0,0 +1,142 @@ +module main +author unknown +version 1 0 +description '' + +script 609 50 { +whenButtonPressed 'A' +forever { + setUserLED true + waitMillis 500 + setUserLED false + waitMillis 500 +} +} + +script 54 82 { +whenStarted +forever { + '[tft:clear]' + waitMillis 1000 + '[tft:rect]' 100 10 100 100 (colorSwatch 0 0 255 255) + waitMillis 1000 + '[tft:circle]' 350 100 70 (colorSwatch 255 63 235 255) true + waitMillis 1000 + '[tft:line]' 50 150 300 170 (colorSwatch 0 255 0 255) + waitMillis 20000 +} +} + +script 651 262 { +whenStarted +forever { + if ('[tft:tftTouched]') {'[tft:rect]' ('[tft:tftTouchX]') ('[tft:tftTouchY]') 30 30 (colorSwatch 35 190 30 255)} + waitMillis 500 +} +} + + +module TFT Output +author MicroBlocks +version 1 12 +description 'Draw graphics and write text on boards with a TFT display, such as the M5Stack, M5Stick, Citilab ED1 or (discontinued) IoT-Bus.' + + spec ' ' '[tft:clear]' 'clear TFT display' + space + spec ' ' '[tft:rect]' 'draw rectangle on TFT at x _ y _ width _ height _ color _ : filled _' 'num num num num color bool' 10 10 40 30 nil true + spec ' ' '[tft:roundedRect]' 'draw rounded rectangle on TFT at x _ y _ width _ height _ radius _ color _ : filled _' 'num num num num num color bool' 10 10 40 30 8 nil true + spec ' ' '[tft:circle]' 'draw circle on TFT at x _ y _ radius _ color _ : filled _' 'num num num color bool' 40 40 30 nil true + spec ' ' '[tft:triangle]' 'draw triangle on TFT at x _ y _ , x _ y _ , x _ y _ color _ : filled _' 'num num num num num num color bool' 20 20 30 80 60 5 nil true + spec ' ' '[tft:line]' 'draw line on TFT from x _ y _ to x _ y _ color _' 'num num num num color' 12 8 25 15 + spec ' ' 'tft_drawVector' 'draw vector x _ y _ angle _ length _ color _' 'num num num num color' 40 40 45 40 + space + spec ' ' '[tft:text]' 'write _ on TFT at x _ y _ color _ : scale _ wrap _ : bg color _' 'str num num color num bool color' 'Hello World!' 5 5 nil 2 true + spec ' ' 'tft_drawText' 'draw text _ on TFT at x _ y _ color _ : scale _ : bg color _' 'str num num color num color' 'Line 1 +Line 2' 50 20 nil 2 + space + spec ' ' '[tft:setPixel]' 'set TFT pixel x _ y _ to _' 'num num color' 10 10 + spec ' ' '[tft:pixelRow]' 'draw pixel row _ x _ y _ : bytesPerPixel _ : palette _' 'auto num num num str' 'aList' 0 0 4 + spec ' ' '[tft:drawBitmap]' 'draw bitmap _ palette _ on TFT at x _ y _' 'str str num num' 'aBitmap' 'a list of colors' 10 10 + space + spec 'r' 'tft_colorSwatch' '_' 'color' + spec 'r' 'makeColor' 'color r _ g _ b _ (0-255)' 'num num num' 0 100 100 + spec 'r' 'makeGray' 'gray _ %' 'num' 50 + spec 'r' 'randomColor' 'random color' + space + spec 'r' '[tft:getWidth]' 'TFT width' + spec 'r' '[tft:getHeight]' 'TFT height' + space + spec ' ' '[tft:setBacklight]' 'set TFT backlight _ (0-10)' 'num' 10 + space + spec ' ' '_deferMonochromeDisplayUpdates' '_defer monochrome display updates' + spec ' ' '_resumeMonochromeDisplayUpdates' '_resume monochrome display updates' + +to '_deferMonochromeDisplayUpdates' { + '[tft:deferUpdates]' +} + +to '_resumeMonochromeDisplayUpdates' { + '[tft:resumeUpdates]' +} + +to makeColor r g b { + r = (maximum 0 (minimum r 255)) + g = (maximum 0 (minimum g 255)) + b = (maximum 0 (minimum b 255)) + return ((r << 16) | ((g << 8) | b)) +} + +to makeGray percent { + gray = ((percent * 255) / 100) + gray = (maximum 0 (minimum gray 255)) + return ((gray << 16) | ((gray << 8) | gray)) +} + +to randomColor { + local 'n1' (random 100 200) + local 'n2' (random 0 100) + if (1 == (random 1 3)) { + return ((n1 << 16) | (n2 << 8)) + } (1 == (random 1 2)) { + return ((n2 << 16) | n1) + } else { + return ((n1 << 8) | n2) + } +} + +to tft_colorSwatch color { + return color +} + +to tft_drawText s x y color optionalScale optionalBGColor { + s = ('[data:convertType]' s 'string') + local 'scale' (argOrDefault 5 2) + local 'bgColor' (argOrDefault 6 '') + local 'lines' ('[data:split]' s ('[data:unicodeString]' 10)) + for line ('[data:split]' s ('[data:unicodeString]' 10)) { + if (isType bgColor 'number') { + '[tft:text]' line x y color scale false bgColor + } else { + '[tft:text]' line x y color scale false + } + y += (8 * scale) + } +} + +to tft_drawVector x y angle length color { + local 'endX' (x + ((length * ('[misc:sin]' (100 * (angle + 90)))) >> 14)) + local 'endY' (y + ((length * ('[misc:sin]' (100 * angle))) >> 14)) + '[tft:line]' x y endX endY color +} + + +module 'Touch Screen' Input +author MicroBlocks +version 1 0 +tags qvga touch tft +description 'Mouse pointer support for Boardie and the Linux VM, plus support for QVGA touch screen display on the (discontinued) IoT-Bus Io system.' + + spec 'r' '[tft:tftTouched]' 'TFT touched' + spec 'r' '[tft:tftTouchX]' 'TFT touch X position' + spec 'r' '[tft:tftTouchY]' 'TFT touch Y position' + spec 'r' '[tft:tftTouchPressure]' 'TFT touch pressure' diff --git a/interpreters/smallvm/fileSys.h b/interpreters/smallvm/fileSys.h new file mode 100644 index 000000000..9eeafc030 --- /dev/null +++ b/interpreters/smallvm/fileSys.h @@ -0,0 +1,17 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Copyright 2019 John Maloney, Bernat Romagosa, and Jens Mönig + +// fileSys.c - File system selection. +// John Maloney, December 2021 + +#include + +// always use LittleFS +#include +#define myFS LittleFS + +void closeIfOpen(char *fileName); +void closeAndDeleteFile(char *fileName); diff --git a/interpreters/smallvm/interp.c b/interpreters/smallvm/interp.c new file mode 100644 index 000000000..e59de5923 --- /dev/null +++ b/interpreters/smallvm/interp.c @@ -0,0 +1,1548 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Copyright 2018 John Maloney, Bernat Romagosa, and Jens Mönig + +// interp.c - Simple interpreter based on 32-bit opcodes +// John Maloney, April 2017 + +#define _DEFAULT_SOURCE // enable usleep() declaration from unistd.h + +#include +#include +#include +#include + +#include "mem.h" +#include "interp.h" +#include "persist.h" + +// Tasks - Set USE_TASKS to false to test interpreter performance without task switching + +#define USE_TASKS 1 + +// RECENT is a threshold for waking up tasks waiting on timers +// The timer can be up to this many usecs past the wakeup time. + +#define RECENT 10000000 + +// Interpreter State + +CodeChunkRecord chunks[MAX_CHUNKS]; + +Task tasks[MAX_TASKS]; +int taskCount = 0; + +OBJ vars[MAX_VARS]; + +// Error Reporting + +// When a primitive encounters an error, it calls fail() with an error code. +// The VM stops the task and records the error code and IP where the error occurred. + +static uint8 errorCode = noError; +static int taskSleepUSecs = 0; + +OBJ fail(uint8 errCode) { + errorCode = errCode; + return falseObj; +} + +int failure() { + return errorCode != noError; +} + +#ifndef EMSCRIPTEN + void taskSleep(int msecs) { + // Make the current task sleep for the given number of milliseconds to free up cycles. + taskSleepUSecs = msecs * 1000; + if (noError == errorCode) errorCode = sleepSignal; + } + + void taskSleepMicros(int usecs) { + // Make the current task sleep for the given number of microseconds to free up cycles. + taskSleepUSecs = usecs; + if (noError == errorCode) errorCode = sleepSignal; + } +#endif + +// Printing + +#define PRINT_BUF_SIZE 1000 +static char printBuffer[PRINT_BUF_SIZE]; +static int printBufferByteCount = 0; + +int extraByteDelay = 1000; // default of 1000 usecs assumes serial throughput of ~1000 bytes/sec + +static void printObj(OBJ obj) { + // Append a printed representation of the given object to printBuffer. + + char *dst = &printBuffer[printBufferByteCount]; + int n = PRINT_BUF_SIZE - printBufferByteCount; + + if (isInt(obj)) snprintf(dst, n, "%d", obj2int(obj)); + else if (obj == falseObj) snprintf(dst, n, "false"); + else if (obj == trueObj) snprintf(dst, n, "true"); + else if (objType(obj) == StringType) { + snprintf(dst, n, "%s", obj2str(obj)); + } else if (objType(obj) == ListType) { + snprintf(dst, n, "[%d item list]", obj2int(FIELD(obj, 0))); + } else if (objType(obj) == ByteArrayType) { + snprintf(dst, n, "(%d bytes)", BYTES(obj)); + } else { + snprintf(dst, n, "(object type: %d)", objType(obj)); + } + printBufferByteCount = strlen(printBuffer); +} + +static void printArgs(int argCount, OBJ *args, int forSay, int insertSpaces) { + // Print all args into printBuffer and return the size of the resulting string. + + if (forSay) { + printBuffer[0] = 2; // type is string (printBuffer is used as outputValue message body) + printBufferByteCount = 1; + } else { + printBufferByteCount = 0; + } + printBuffer[printBufferByteCount] = 0; // null terminate + + for (int i = 0; i < argCount; i++) { + printObj(args[i]); + if (insertSpaces && (i < (argCount - 1)) && (printBufferByteCount < PRINT_BUF_SIZE)) { + printBuffer[printBufferByteCount++] = ' '; // add a space + printBuffer[printBufferByteCount] = 0; // null terminate + } + } +} + +static int bytesForObject(OBJ value) { + // Return the number of bytes needed to transmit the given value. + + int headerBytes = 6; // message header (5 bytes) + type byte + if (isInt(value)) { // 32-bit integer + return headerBytes + 4; + } else if (IS_TYPE(value, StringType)) { // string + int len = strlen(obj2str(value)); + if (len > 800) len = 800; + return headerBytes + len; + } else if ((value == trueObj) || (value == falseObj)) { // boolean + return headerBytes + 1; + } + return 512; // maximum that might be needed, based on size of buffer in sendValueMessage +} + +// Broadcast + +OBJ lastBroadcast = zeroObj; // Note: This variable must be processed by the garbage collector! + +static void primSendBroadcast(int argCount, OBJ *args) { + // Variadic broadcast; all args are concatenated into printBuffer. + printArgs(argCount, args, false, false); + // save the last broadcasted message + lastBroadcast = newStringFromBytes(printBuffer, printBufferByteCount); + startReceiversOfBroadcast(printBuffer, printBufferByteCount); + sendBroadcastToIDE(printBuffer, printBufferByteCount); +} + +OBJ primBroadcastToIDEOnly(int argCount, OBJ *args) { + // Broadcast a string to the IDE only, not locally. + + printArgs(argCount, args, false, false); + sendBroadcastToIDE(printBuffer, printBufferByteCount); + return falseObj; +} + +// Timing Support + +static uint32 timerStart = 0; + +void resetTimer() { timerStart = millisecs(); } + +static int timer() { + // Return the number of milliseconds since the timer was last reset. + // Note: The millisecond clock is the 32-bit microsecond clock divided by 1000, + // so it wraps around to zero when the microsecond clock wraps, which occurs + // about every 72 minutes and 35 seconds. That's the maximum duration that can + // be measured with this simple timer implementation. + + const uint32 msecWrap = 4294967; // 2^32 / 1000, value at which the millisecond clock wraps + + uint32 now = millisecs(); + if (now < timerStart) { // clock wrapped + return (msecWrap - timerStart) + now; // time to wrap + time since wrap + } + return now - timerStart; +} + +static OBJ primMSecsSince(int argCount, OBJ *args) { + int startTime = obj2int(args[0]); + int endTime = ((argCount > 1) && isInt(args[1])) ? + obj2int(args[1]) : + ((uint32) ((totalMicrosecs() / 1000))) & 0x3FFFFFFF; + + int deltaTime = endTime - startTime; + if (deltaTime < 0) deltaTime += 0x40000000; + return int2obj(deltaTime); +} + +static OBJ primUSecsSince(int argCount, OBJ *args) { + int startTime = obj2int(args[0]); + int endTime = ((argCount > 1) && isInt(args[1])) ? + obj2int(args[1]) : + microsecs() & 0x3FFFFFFF; + + int deltaTime = endTime - startTime; + if (deltaTime < 0) deltaTime += 0x40000000; + return int2obj(deltaTime); +} + +// String Access + +static inline char * nextUTF8(char *s) { + // Return a pointer to the start of the UTF8 character following the given one. + // If s points to a null byte (i.e. end of the string) return it unchanged. + + if (!*s) return s; // end of string + if ((uint8) *s < 128) return s + 1; // single-byte character + if (0xC0 == (*s & 0xC0)) s++; // start of multi-byte character + while (0x80 == (*s & 0xC0)) s++; // skip continuation bytes + return s; +} + +static int countUTF8(char *s) { + int count = 0; + while (*s) { + s = nextUTF8(s); + count++; + } + return count; +} + +static OBJ charAt(OBJ stringObj, int i) { + char *start = obj2str(stringObj); + while (i-- > 1) { // find start of the ith Unicode character + if (!*start) return fail(indexOutOfRangeError); // end of string + start = nextUTF8(start); + } + int byteCount = nextUTF8(start) - start; + OBJ result = newString(byteCount); + if (result) { + memcpy(obj2str(result), start, byteCount); + } + return result; +} + +// Board Type + +#define BOARD_TYPE_SIZE 32 + +// statically allocated object for the boardType primitive result +static struct { + uint32 header; + char body[BOARD_TYPE_SIZE]; +} boardTypeObj; + +OBJ primBoardType() { + strncpy(boardTypeObj.body, boardType(), BOARD_TYPE_SIZE - 1); + int wordCount = (strlen(boardTypeObj.body) + 4) / 4; + boardTypeObj.header = HEADER(StringType, wordCount); + return (OBJ) &boardTypeObj; +} + +// Misc primitives + +static OBJ primModulo(int argCount, OBJ *args) { + int n = evalInt(args[0]); + int modulus = evalInt(args[1]); + if (0 == modulus) return fail(zeroDivide); + if (modulus < 0) modulus = -modulus; + int result = n % modulus; + if (result < 0) result += modulus; + return int2obj(result); +} + +static OBJ primRandom(int argCount, OBJ *args) { + int first = 1, last = 100; // defaults for zero arguments + if (argCount == 1) { // use range [1..arg] + first = 1; + last = evalInt(args[0]); + if (last < 0) first = -1; + } else if (argCount == 2) { // use range [first..last] + first = evalInt(args[0]); + last = evalInt(args[1]); + } + if (first > last) { // ensure first <= last + int tmp = first; + first = last; + last = tmp; + } + int range = (last + 1) - first; // if first == last range is 1 and first is returned + return int2obj(first + (rand() % range)); // result range is [first..last], inclusive +} + +static OBJ primMinimum(int argCount, OBJ *args) { + if (argCount < 1) return fail(notEnoughArguments); + int result = obj2int(args[0]); + for (int i = 0; i < argCount; i++) { + OBJ arg = args[i]; + if (!isInt(arg)) return fail(needsIntegerError); + int n = obj2int(arg); + if (n < result) result = n; + } + return int2obj(result); +} + +static OBJ primMaximum(int argCount, OBJ *args) { + if (argCount < 1) return fail(notEnoughArguments); + int result = obj2int(args[0]); + for (int i = 0; i < argCount; i++) { + OBJ arg = args[i]; + if (!isInt(arg)) return fail(needsIntegerError); + int n = obj2int(arg); + if (n > result) result = n; + } + return int2obj(result); +} + +static inline OBJ primSum(int argCount, OBJ *args) { + int result = 0; + if ((1 == argCount) && IS_TYPE(args[0], ListType)) { + OBJ *src = &FIELD(args[0], 0); + int count = obj2int(*src++); // list item count + for (int i = 0; i < count; i++) { + OBJ arg = *src++; + if (!isInt(arg)) return fail(needsListOfIntegers); + result += obj2int(arg); + } + } else { + for (int i = 0; i < argCount; i++) { + OBJ arg = args[i]; + if (!isInt(arg)) return fail(needsIntegerError); + result += obj2int(arg); + } + } + return int2obj(result); +} + +static inline int compareObjects(OBJ obj1, OBJ obj2) { + // Compare two objects with the given operator and return one of: + // -1 (<), 0 (==), 1 (>) + // For mixed string-int comparison, try to convert the string to an integer. + // Set nonComparableError flag if the objects are not comparable. + + int n1 = 0, n2 = 0; + if (IS_TYPE(obj1, StringType) && IS_TYPE(obj2, StringType)) { + return strcmp(obj2str(obj1), obj2str(obj2)); + } else if (IS_TYPE(obj1, StringType) && isInt(obj2)) { + n1 = strtol(obj2str(obj1), NULL, 10); + n2 = obj2int(obj2); + } else if (isInt(obj1) && IS_TYPE(obj2, StringType)) { + n1 = obj2int(obj1); + n2 = strtol(obj2str(obj2), NULL, 10); + } else if (isInt(obj1) && isInt(obj2)) { + // Note: For efficiency, caller should handle this special case + n1 = obj2int(obj1); + n2 = obj2int(obj2); + } else { + fail(nonComparableError); + } + if (n1 < n2) return -1; + if (n1 > n2) return 1; + return 0; +} + +static OBJ primCompare(int op, OBJ obj1, OBJ obj2) { + // Compare objects with the given operator: + // -2 (<), -1 (<=), 0 (==), 1 (>=), 2 (>) + // Return a boolean. Set nonComparableError error if objects are not comparable. + + int result = compareObjects(obj1, obj2); + if (result < 0) return (op < 0) ? trueObj : falseObj; + if (result > 0) return (op > 0) ? trueObj : falseObj; + return ((-1 <= op) && (op <= 1)) ? trueObj : falseObj; +} + +static int stringsEqual(OBJ obj1, OBJ obj2) { + // Return true if the given strings have the same length and contents. + // Assume s1 and s2 are of Strings. + + int byteCount = 4 * objWords(obj1); + if (byteCount != (4 * objWords(obj2))) return false; // different lengths + char *s1 = (char *) &FIELD(obj1, 0); + char *s2 = (char *) &FIELD(obj2, 0); + char *end = s1 + byteCount; + while (s1 < end) { + if (!*s1 && !*s2) return true; // null terminator in both strings + if (*s1++ != *s2++) return false; // not equal + } + return true; +} + +static OBJ argOrDefault(OBJ *fp, int argNum, OBJ defaultValue) { + // Useful for working with optional arguments. + // Return the given argument or defaultValue if the argument was not supplied by the caller. + + if (argNum < 1) return defaultValue; // argNum index is 1-based + int actualArgCount = obj2int(*(fp - 3)); + if (argNum > actualArgCount) return defaultValue; // argument not supplied, return default + return *(fp - (4 + actualArgCount) + argNum); // return the desired argument +} + +static int functionNameMatches(int chunkIndex, char *functionName, int nameLength) { + // Return true if given chunk is the function with the given function name. + // Use the function name from the function's metadata (the last metadata field). + + uint32 *code = (uint32 *) chunks[chunkIndex].code; + uint8 *chunkStart = (uint8 *) (code + PERSISTENT_HEADER_WORDS); + uint8 *src = chunkStart + (4 * code[1]) - 1; // last byte of chunk + + // skip any trailing zeros in chunk data + while (src > chunkStart) { + if (*src) break; // found a non-zero byte + src--; + } + + src -= nameLength; + if (src < chunkStart) return false; + if (*src != 0) return false; // *src is not the end of previous metatdata field + + char *name = (char *) (src + 1); + for (int i = 0; i < nameLength; i++) { + if (*name++ != *functionName++) return false; // mismatch + } + return true; +} + +int chunkIndexForFunction(char *functionName) { + // Return the chunk index for the function with the given name or -1 if not found. + + int nameLength = strlen(functionName); + for (int i = 0; i < MAX_CHUNKS; i++) { + if ((functionHat == chunks[i].chunkType) && + functionNameMatches(i, functionName, nameLength)) { + return i; + } + } + return -1; +} + +PrimitiveFunction findPrimitive(char *namedPrimitive); + +static int findCallee(char *functionOrPrimitiveName) { + // Look for a primitive match first since that is fast + PrimitiveFunction f = findPrimitive(functionOrPrimitiveName); + if (f) return (int) f; + + // Look for a user-defined function match (slow if no match found!) + int result = chunkIndexForFunction(functionOrPrimitiveName); + if (result >= 0) return (0xFFFFFF00 | result); // set top 24 bits to show callee is a chunk + // assume: result < 256 (MAX_CHUNKS) so it fits in low 8 bits + + fail(primitiveNotImplemented); + return -1; +} + +// Interpreter + +// Macros to pop arguments for commands and reporters (pops args, leaves result on stack) +#define POP_ARGS_COMMAND() { sp -= arg; } +#define POP_ARGS_REPORTER() { sp -= arg - 1; } + +// Macro to check for stack overflow +#define STACK_CHECK(n) { \ + if (((sp + (n)) - task->stack) > STACK_LIMIT) { \ + errorCode = stackOverflow; \ + goto error; \ + } \ +} + +// Macros to support function calls +#define IN_CALL() (fp > task->stack) + +static void interpDebug(int ip, int cmd, int arg, int sp) { + // Show interpreter state for debugging. + + char tmpStr[100]; + sprintf(tmpStr, "ip: %d cmd: %d arg: %d sp: %d", ip - 4, cmd, arg, sp); + outputString(tmpStr); \ +} + +// Macro to inline dispatch in the end of each opcode (avoiding a jump back to the top) +#define DISPATCH() { \ + if (errorCode) goto error; \ + op = *ip++; \ + arg = ARG(op); \ + task->sp = sp - task->stack; /* record stack pointer for garbage collector */ \ + /* interpDebug((ip - (int16 *) task->code), CMD(op), arg, task->sp); */ \ + goto *jumpTable[CMD(op)]; \ +} + +// Macro for debugging stack errors +#define SHOW_SP(s) { \ + outputString(s); \ + reportNum("sp", sp - task->stack); \ + reportNum("fp", fp - task->stack); \ +} + +#define OP_POP 19 +#define OP_DECREMENT_AND_JUMP 26 + +static void runTask(Task *task) { + register int op; + register int16 *ip; + register OBJ *sp; + register OBJ *fp; + int arg, tmp; + OBJ tmpObj; + + // initialize jump table + static void *jumpTable[] = { + &&halt_op, // stop this task + &&stopAll_op, // stop all tasks except this one + &&pushImmediate_op, // true, false, and ints that fit in 8 bits [-64..63] + &&pushLargeInteger_op, // ints that fit in 24 bits + &&pushHugeInteger_op, // ints that need > 24 bits + &&pushLiteral_op, // string constant from literals frame + &&pushGlobal_op, + &&storeGlobal_op, + &&incrementGlobal_op, + &&initLocals_op, + &&pushLocal_op, // 10 + &&storeLocal_op, + &&incrementLocal_op, + &&pushArg_op, + &&storeArg_op, + &&incrementArg_op, // 15 + &&pushArgCount_op, + &&getArg_op, + &&argOrDefault_op, + &&pop_op, + &&ignoreArgs_op, // 20 (alias for pop_op) + &&noop_op, + &&jmp_op, + &&longJmp_op, // (alias for jmp_op) + &&jmpTrue_op, + &&jmpFalse_op, // 25 + &&decrementAndJmp_op, + &&forLoop_op, + &&jmpOr_op, + &&jmpAnd_op, + &&waitUntil_op, // 30 (alias for jmpFalse_op) + &&exitLoop_op, + &&waitMicros_op, + &&waitMillis_op, + &&callFunction_op, + &&returnResult_op, // 35 + &&commandPrimitive_op, + &&reporterPrimitive_op, + &&callCustomCommand_op, + &&callCustomReporter_op, + &&sendBroadcast_op, // 40 + &&recvBroadcast_op, + &&getLastBroadcast_op, + &&millis_op, + &µs_op, + &&secs_op, // 45 + &&millisSince_op, + &µsSince_op, + &&timer_op, + &&resetTimer_op, + &&add_op, // 50 + &&subtract_op, + &&multiply_op, + &÷_op, + &&modulo_op, + &&bitAnd_op, // 55 + &&bitOr_op, + &&bitXor_op, + &&bitInvert_op, + &&bitShiftLeft_op, + &&bitShiftRight_op, // 60 + &&lessThan_op, + &&lessOrEq_op, + &&equal_op, + &¬Equal_op, + &&greaterOrEq_op, // 65 + &&greaterThan_op, + &¬_op, + &&RESERVED_op, + &&sum_op, + &&longMultiply_op, // 70 + &&absoluteValue_op, + &&minimum_op, + &&maximum_op, + &&random_op, + &&hexToInt_op, // 75 + &&isType_op, + &&sayIt_op, + &&graphIt_op, + &&boardType_op, + &&newList_op, // 80 + &&at_op, + &&atPut_op, + &&size_op, + &&analogPins_op, + &&digitalPins_op, // 85 + &&analogRead_op, + &&analogWrite_op, + &&digitalRead_op, + &&digitalWrite_op, + &&digitalSet_op, // 90 + &&digitalClear_op, + &&buttonA_op, + &&buttonB_op, + &&setUserLED_op, + &&i2cSet_op, // 95 + &&i2cGet_op, + &&spiSend_op, + &&spiRecv_op, + &&RESERVED_op, + &&RESERVED_op, // 100 + &&RESERVED_op, + &&RESERVED_op, + &&RESERVED_op, + &&RESERVED_op, + &&RESERVED_op, + &&RESERVED_op, + &&RESERVED_op, + &&RESERVED_op, + &&RESERVED_op, + &&RESERVED_op, + &&RESERVED_op, + &&RESERVED_op, + &&RESERVED_op, + &&RESERVED_op, + &&RESERVED_op, + &&RESERVED_op, + &&RESERVED_op, + &&RESERVED_op, + &&RESERVED_op, + &&RESERVED_op, + &&RESERVED_op, + &&RESERVED_op, + &&RESERVED_op, + &&RESERVED_op, + &&RESERVED_op, + &&comment_op, // (alias for noop_op) + &&codeEnd_op, // 127 (alias for halt_op) + }; + + // Restore task state + ip = (int16 *) task->code + task->ip; + sp = task->stack + task->sp; + fp = task->stack + task->fp; + + DISPATCH(); + + error: + // sleepSignal is not a actual error; it just suspends the current task + if (sleepSignal == errorCode) { + errorCode = noError; // clear the error + if (taskSleepUSecs > 0) { + task->status = waiting_micros; + task->wakeTime = microsecs() + (taskSleepUSecs); + } + goto suspend; + } + // tmp encodes the error location: <16 bit ip><8 bit chunkIndex> + tmp = ((ip - (int16 *) task->code) << 8) | (task->currentChunkIndex & 0xFF); + sendTaskError(task->taskChunkIndex, errorCode, tmp); + task->status = unusedTask; + if (unusedTask == tasks[taskCount - 1].status) taskCount--; + errorCode = noError; // clear the error + goto suspend; + suspend: + // save task state + task->ip = ip - (int16 *) task->code; + task->sp = sp - task->stack; + task->fp = fp - task->stack; + return; + RESERVED_op: + halt_op: + codeEnd_op: + sendTaskDone(task->taskChunkIndex); + task->status = unusedTask; + if (unusedTask == tasks[taskCount - 1].status) taskCount--; + goto suspend; + noop_op: + comment_op: + POP_ARGS_COMMAND(); + DISPATCH(); + pushImmediate_op: + STACK_CHECK(1); + *sp++ = (OBJ) arg; + DISPATCH(); + pushLargeInteger_op: + // push an integer object that fits into 24 bits + STACK_CHECK(1); + tmp = (*ip++ << 8) | (arg & 0xFF); // most significant bits are in the following 16-bit word + *sp++ = (OBJ) tmp; + DISPATCH(); + pushLiteral_op: + STACK_CHECK(1); + tmp = *ip; // offset to the literal is in the following 16-bit word + *sp++ = (OBJ) (ip++ + tmp); + DISPATCH(); + pushGlobal_op: + STACK_CHECK(1); + *sp++ = vars[arg]; + DISPATCH(); + storeGlobal_op: + vars[arg] = *--sp; + DISPATCH(); + incrementGlobal_op: + tmp = evalInt(vars[arg]); + if (!errorCode) { + vars[arg] = int2obj(tmp + evalInt(*--sp)); + } + DISPATCH(); + pushArgCount_op: + STACK_CHECK(1); + *sp++ = IN_CALL() ? *(fp - 3) : zeroObj; + DISPATCH(); + pushArg_op: + STACK_CHECK(1); + if (IN_CALL()) { + *sp++ = *(fp - obj2int(*(fp - 3)) - 3 + arg); + } else { + *sp++ = fail(notInFunction); + } + DISPATCH(); + storeArg_op: + if (IN_CALL()) { + *(fp - obj2int(*(fp - 3)) - 3 + arg) = *--sp; + } else { + fail(notInFunction); + } + DISPATCH(); + incrementArg_op: + if (IN_CALL()) { + tmp = evalInt(*(fp - obj2int(*(fp - 3)) - 3 + arg)) + evalInt(*--sp); + *(fp - obj2int(*(fp - 3)) - 3 + arg) = int2obj(tmp); + } else { + fail(notInFunction); + } + DISPATCH(); + pushLocal_op: + STACK_CHECK(1); + *sp++ = *(fp + arg); + DISPATCH(); + storeLocal_op: + *(fp + arg) = *--sp; + DISPATCH(); + incrementLocal_op: + *(fp + arg) = int2obj(obj2int(*(fp + arg)) + evalInt(*--sp)); + DISPATCH(); + pop_op: + ignoreArgs_op: + sp -= arg; + if (sp >= task->stack) { + DISPATCH(); + } else { + vmPanic("Stack underflow"); + } + DISPATCH(); + jmp_op: + longJmp_op: + if (!arg) arg = *ip++; // zero arg means offset is in the next word + ip += arg; +#if USE_TASKS + if (arg < 0) goto suspend; +#endif + DISPATCH(); + exitLoop_op: + if (!arg) arg = *ip++; // zero arg means offset is in the next word + ip += arg; + tmp = CMD(*(ip - 1)); + if (tmp == OP_POP) { // pop 'for' loop state + sp -= 3; + } else if (tmp == OP_DECREMENT_AND_JUMP) { // pop 'repeat' loop counter + sp -= 1; + } + DISPATCH(); + jmpTrue_op: + if (!arg) arg = *ip++; // zero arg means offset is in the next word + if (trueObj == (*--sp)) ip += arg; +#if USE_TASKS + if ((arg < 0) && (trueObj == *sp)) goto suspend; +#endif + DISPATCH(); + jmpFalse_op: + waitUntil_op: + if (!arg) arg = *ip++; // zero arg means offset is in the next word + if (trueObj != (*--sp)) ip += arg; // treat any value but true as false +#if USE_TASKS + if ((arg < 0) && (trueObj != *sp)) goto suspend; +#endif + DISPATCH(); + decrementAndJmp_op: + if (!arg) arg = *ip++; // zero arg means offset is in the next word + if (isInt(*(sp - 1))) { + tmp = obj2int(*(sp - 1)) - 1; // decrement loop counter (normal case) + } else { + tmp = evalInt(*(sp - 1)) - 1; // decrement loop counter (first time: convert string to int if needed) + } + if (tmp >= 0) { + ip += arg; // loop counter >= 0, so branch + *(sp - 1) = int2obj(tmp); // update loop counter +#if USE_TASKS + goto suspend; +#else + DISPATCH(); +#endif + } else { + sp--; // loop done, pop loop counter + } + DISPATCH(); + callFunction_op: + // function call stack layout for N function arguments and M local variables: + // local M-1 + // ... + // local 0 <- fp points here during call, so the value of local m is *(fp + m) + // *(fp - 1), the old fp + // *(fp - 2), return address, <22 bit ip><8 bit chunkIndex> encoded as an integer object + // *(fp - 3), # of function arguments + // arg N-1 + // ... + // arg 0 + arg = *ip++; + callFunctionByName: + tmp = (arg >> 8) & 0xFF; // callee's chunk index (middle byte of arg) + if (chunks[tmp].chunkType != functionHat) { + fail(badChunkIndexError); + goto error; + } + STACK_CHECK(3); + *sp++ = int2obj(arg & 0xFF); // # of arguments (low byte of arg) + *sp++ = int2obj(((ip - (int16 *) task->code) << 8) | (task->currentChunkIndex & 0xFF)); // return address + *sp++ = int2obj(fp - task->stack); // old fp + fp = sp; + task->currentChunkIndex = tmp; // callee's chunk index (middle byte of arg) + task->code = chunks[task->currentChunkIndex].code; + ip = (int16 *) (task->code + PERSISTENT_HEADER_WORDS); // first instruction in callee + DISPATCH(); + returnResult_op: + tmpObj = *(sp - 1); // return value + if (fp == task->stack) { // not in a function call + if (!hasOutputSpace(bytesForObject(tmpObj) + 100)) { // leave room for other messages + ip--; // retry when task is resumed + goto suspend; + } + sendTaskReturnValue(task->taskChunkIndex, tmpObj); + task->status = unusedTask; + goto suspend; + } + sp = fp - obj2int(*(fp - 3)) - 3; // restore stack pointer; *(fp - 3) is the arg count + *sp++ = tmpObj; // push return value (no need for a stack check; just recovered at least 3 words from the old call frame) + tmp = obj2int(*(fp - 2)); // return address + task->currentChunkIndex = tmp & 0xFF; + task->code = chunks[task->currentChunkIndex].code; + ip = ((int16 *) task->code) + ((tmp >> 8) & 0x3FFFFF); // restore old ip + fp = task->stack + obj2int(*(fp - 1)); // restore the old fp + DISPATCH(); + waitMicros_op: + tmp = evalInt(*(sp - 1)); // wait time in usecs + POP_ARGS_COMMAND(); + if (tmp <= 30) { + if (tmp <= 0) { DISPATCH(); } // don't wait at all + // busy-wait for wait times up to 30 usecs to avoid a context switch + tmp = microsecs() + tmp - 3; // wake time + while ((microsecs() - tmp) >= RECENT) { } // busy wait + DISPATCH(); + } + task->status = waiting_micros; + task->wakeTime = (microsecs() + tmp) - 7; // adjusted for approximate scheduler overhead + goto suspend; + waitMillis_op: + tmp = evalInt(*(sp - 1)); // wait time in usecs + POP_ARGS_COMMAND(); + if (tmp <= 0) { DISPATCH(); } // don't wait at all + if (tmp > 3600000) { + fail(waitTooLong); + goto error; + } + task->status = waiting_micros; + task->wakeTime = microsecs() + ((1000 * tmp) - 7); + goto suspend; + sendBroadcast_op: + primSendBroadcast(arg, sp - arg); + POP_ARGS_COMMAND(); + DISPATCH(); + recvBroadcast_op: + POP_ARGS_COMMAND(); // pop the broadcast name (a literal string) + DISPATCH(); + stopAll_op: + stopAllTasksButThis(task); // clears all tasks except the current one + DISPATCH(); + forLoop_op: + // stack layout: + // *(sp - 1) the loop counter (decreases from N to 1); falseObj the very first time + // *(sp - 2) N, the total loop count or item count of a list, string or byte array + // *(sp - 3) the object being iterated over: an integer, list, string, or byte array + + tmpObj = *(sp - 1); // loop counter, or falseObj the very first time + if (falseObj == tmpObj) { // first time: compute N, the total iterations (in tmp) + tmpObj = *(sp - 3); + if (isInt(tmpObj)) { + tmp = obj2int(tmpObj); + } else if (IS_TYPE(tmpObj, ListType)) { + tmp = obj2int(FIELD(tmpObj, 0)); + } else if (IS_TYPE(tmpObj, StringType)) { + tmp = countUTF8(obj2str(tmpObj)); + } else if (IS_TYPE(tmpObj, ByteArrayType)) { + tmp = BYTES(tmpObj); + } else { + fail(badForLoopArg); + goto error; + } + *(sp - 2) = int2obj(tmp); // save N, the total iterations; tmp is initial loop counter + } else { // not the first time + tmp = obj2int(tmpObj) - 1; // decrement the loop counter (in tmp) + } + if (tmp > 0) { // loop counter > 0 + *(sp - 1) = int2obj(tmp); // store the loop counter + tmp = obj2int(*(sp - 2)) - tmp; // set tmp to the loop index (increasing from 0 to N-1) + tmpObj = *(sp - 3); // set tmpObj to thing being iterated over + if (isInt(tmpObj)) { + // set the index variable to the loop index + *(fp + arg) = int2obj(tmp + 1); // add 1 to get range 1..N + } else if (IS_TYPE(tmpObj, ListType)) { + // set the index variable to the next list item + *(fp + arg) = FIELD(tmpObj, tmp + 1); // skip count field + } else if (IS_TYPE(tmpObj, StringType)) { + // set the index variable to the next character of a string + *(fp + arg) = charAt(tmpObj, tmp + 1); + } else if (IS_TYPE(tmpObj, ByteArrayType)) { + // set the index variable to the next byte of a byte array + *(fp + arg) = int2obj(((uint8 *) &FIELD(tmpObj, 0))[tmp]); + } else { + fail(badForLoopArg); + goto error; + } + } else { // loop counter <= 0 + ip += 2; // skip the following longJmp instruction (two words) thus ending the loop + } + DISPATCH(); + initLocals_op: + // Reserve stack space for 'arg' locals initialized to zero + STACK_CHECK(arg); + while (arg-- > 0) *sp++ = zeroObj; + DISPATCH(); + getArg_op: + // For variadic functions. Unlike pushVar, the argument index is passed on the stack. + STACK_CHECK(1); + if (IN_CALL()) { + tmp = evalInt(*(sp - 1)); + if ((1 <= tmp) && (tmp <= obj2int(*(fp - 3)))) { // if arg index in range: + *(sp - arg) = *(fp - obj2int(*(fp - 3)) - 4 + tmp); + } else { + fail(argIndexOutOfRange); + } + } else { + fail(notInFunction); + } + POP_ARGS_REPORTER(); + DISPATCH(); + getLastBroadcast_op: + *(sp - arg) = lastBroadcast; + POP_ARGS_REPORTER(); + DISPATCH(); + jmpOr_op: + if (!arg) arg = *ip++; // zero arg means offset is in the next word + // if true, jump leaving true (result of "or" expression) on stack, otherwise pop + if (trueObj == *(sp - 1)) { ip += arg; } else { sp--; } + DISPATCH(); + jmpAnd_op: + if (!arg) arg = *ip++; // zero arg means offset is in the next word + // if not true, push false (result of "and" expression) on stack and jump + if (trueObj != (*--sp)) { // treat any value but true as false + *sp++ = falseObj; + ip += arg; + } + DISPATCH(); + + // For the primitive ops below, arg is the number of arguments (any primitive can be variadic). + // Commands pop all their arguments. + // Reporters pop all their arguments and leave a result on the top of the stack. + minimum_op: + *(sp - arg) = primMinimum(arg, sp - arg); + POP_ARGS_REPORTER(); + DISPATCH(); + maximum_op: + *(sp - arg) = primMaximum(arg, sp - arg); + POP_ARGS_REPORTER(); + DISPATCH(); + lessThan_op: + tmpObj = *(sp - 2); + if (isInt(tmpObj) && isInt(*(sp - 1))) { // special case for integers: + *(sp - arg) = (obj2int(tmpObj) < obj2int(*(sp - 1))) ? trueObj : falseObj; + } else { + *(sp - arg) = primCompare(-2, tmpObj, *(sp - 1)); + } + POP_ARGS_REPORTER(); + DISPATCH(); + lessOrEq_op: + tmpObj = *(sp - 2); + if (isInt(tmpObj) && isInt(*(sp - 1))) { // special case for integers: + *(sp - arg) = (obj2int(tmpObj) <= obj2int(*(sp - 1))) ? trueObj : falseObj; + } else { + *(sp - arg) = primCompare(-1, tmpObj, *(sp - 1)); + } + POP_ARGS_REPORTER(); + DISPATCH(); + equal_op: + tmpObj = *(sp - 2); + if (tmpObj == *(sp - 1)) { // identical objects + *(sp - arg) = trueObj; + } else if (tmpObj <= trueObj) { + *(sp - arg) = falseObj; // boolean, not equal + } else if (isInt(tmpObj) && isInt(*(sp - 1))) { + *(sp - arg) = falseObj; // integer, not equal + } else if (IS_TYPE(tmpObj, StringType) && IS_TYPE(*(sp - 1), StringType)) { + *(sp - arg) = (stringsEqual(tmpObj, *(sp - 1)) ? trueObj : falseObj); + } else { + *(sp - arg) = falseObj; // not comparable, so not equal + } + POP_ARGS_REPORTER(); + DISPATCH(); + notEqual_op: + tmpObj = *(sp - 2); + if (tmpObj == *(sp - 1)) { // identical objects + *(sp - arg) = falseObj; + } else if (tmpObj <= trueObj) { + *(sp - arg) = trueObj; // boolean, not equal + } else if (isInt(tmpObj) && isInt(*(sp - 1))) { + *(sp - arg) = trueObj; // integer, not equal + } else if (IS_TYPE(tmpObj, StringType) && IS_TYPE(*(sp - 1), StringType)) { + *(sp - arg) = (stringsEqual(tmpObj, *(sp - 1)) ? falseObj : trueObj); + } else { + *(sp - arg) = trueObj; // not comparable, so not equal + } + POP_ARGS_REPORTER(); + DISPATCH(); + greaterOrEq_op: + tmpObj = *(sp - 2); + if (isInt(tmpObj) && isInt(*(sp - 1))) { // special case for integers: + *(sp - arg) = (obj2int(tmpObj) >= obj2int(*(sp - 1))) ? trueObj : falseObj; + } else { + *(sp - arg) = primCompare(1, tmpObj, *(sp - 1)); + } + POP_ARGS_REPORTER(); + DISPATCH(); + greaterThan_op: + tmpObj = *(sp - 2); + if (isInt(tmpObj) && isInt(*(sp - 1))) { // special case for integers: + *(sp - arg) = (obj2int(tmpObj) > obj2int(*(sp - 1))) ? trueObj : falseObj; + } else { + *(sp - arg) = primCompare(2, tmpObj, *(sp - 1)); + } + POP_ARGS_REPORTER(); + DISPATCH(); + not_op: + *(sp - arg) = (trueObj == *(sp - 1)) ? falseObj : trueObj; + POP_ARGS_REPORTER(); + DISPATCH(); + add_op: + *(sp - arg) = int2obj(evalInt(*(sp - 2)) + evalInt(*(sp - 1))); + POP_ARGS_REPORTER(); + DISPATCH(); + subtract_op: + *(sp - arg) = int2obj(evalInt(*(sp - 2)) - evalInt(*(sp - 1))); + POP_ARGS_REPORTER(); + DISPATCH(); + multiply_op: + *(sp - arg) = int2obj(evalInt(*(sp - 2)) * evalInt(*(sp - 1))); + POP_ARGS_REPORTER(); + DISPATCH(); + divide_op: + tmp = evalInt(*(sp - 1)); + *(sp - arg) = ((0 == tmp) ? fail(zeroDivide) : int2obj(evalInt(*(sp - 2)) / tmp)); + POP_ARGS_REPORTER(); + DISPATCH(); + modulo_op: + *(sp - arg) = primModulo(arg, sp - arg); + POP_ARGS_REPORTER(); + DISPATCH(); + absoluteValue_op: + *(sp - arg) = int2obj(abs(evalInt(*(sp - 1)))); + POP_ARGS_REPORTER(); + DISPATCH(); + random_op: + *(sp - arg) = primRandom(arg, sp - arg); + POP_ARGS_REPORTER(); + DISPATCH(); + hexToInt_op: + *(sp - arg) = primHexToInt(arg, sp - arg); + POP_ARGS_REPORTER(); + DISPATCH(); + + // bit operations: + bitAnd_op: + *(sp - arg) = int2obj(evalInt(*(sp - 2)) & evalInt(*(sp - 1))); + POP_ARGS_REPORTER(); + DISPATCH(); + bitOr_op: + *(sp - arg) = int2obj(evalInt(*(sp - 2)) | evalInt(*(sp - 1))); + POP_ARGS_REPORTER(); + DISPATCH(); + bitXor_op: + *(sp - arg) = int2obj(evalInt(*(sp - 2)) ^ evalInt(*(sp - 1))); + POP_ARGS_REPORTER(); + DISPATCH(); + bitInvert_op: + *(sp - arg) = int2obj(~evalInt(*(sp - 1)));; + POP_ARGS_REPORTER(); + DISPATCH(); + bitShiftLeft_op: + *(sp - arg) = int2obj(evalInt(*(sp - 2)) << evalInt(*(sp - 1))); + POP_ARGS_REPORTER(); + DISPATCH(); + bitShiftRight_op: + *(sp - arg) = int2obj(evalInt(*(sp - 2)) >> evalInt(*(sp - 1))); + POP_ARGS_REPORTER(); + DISPATCH(); + + sum_op: + *(sp - arg) = primSum(arg, sp - arg); + POP_ARGS_REPORTER(); + DISPATCH(); + longMultiply_op: + { + long long product = (long long) (evalInt(*(sp - 3))) * (long long) (evalInt(*(sp - 2))); + tmp = (int) ((product >> (evalInt(*(sp - 1)))) & 0xFFFFFFFF); + *(sp - arg) = int2obj(tmp); + } + POP_ARGS_REPORTER(); + DISPATCH(); + + // list operations: + newList_op: + *(sp - arg) = primNewList(arg, sp - arg); + POP_ARGS_REPORTER(); + DISPATCH(); + at_op: + *(sp - arg) = primAt(arg, sp - arg); + POP_ARGS_REPORTER(); + DISPATCH(); + atPut_op: + primAtPut(arg, sp - arg); + POP_ARGS_COMMAND(); + DISPATCH(); + size_op: + *(sp - arg) = primLength(arg, sp - arg); + POP_ARGS_REPORTER(); + DISPATCH(); + // miscellaneous operations: + isType_op: + { + char *type = obj2str(*(sp - 1)); + switch (objType(*(sp - 2))) { + case BooleanType: + *(sp - arg) = strcmp(type, "boolean") == 0 ? trueObj : falseObj; + break; + case IntegerType: + *(sp - arg) = strcmp(type, "number") == 0 ? trueObj : falseObj; + break; + case StringType: + *(sp - arg) = strcmp(type, "string") == 0 ? trueObj : falseObj; + break; + case ListType: + *(sp - arg) = strcmp(type, "list") == 0 ? trueObj : falseObj; + break; + case ByteArrayType: + *(sp - arg) = strcmp(type, "byte array") == 0 ? trueObj : falseObj; + break; + } + } + POP_ARGS_REPORTER(); + DISPATCH(); + millis_op: + STACK_CHECK(1); + *sp++ = int2obj((uint32) ((totalMicrosecs() / 1000) & 0x3FFFFFFF)); // result range is 0 - 1073741823 + DISPATCH(); + micros_op: + STACK_CHECK(1); + *sp++ = int2obj(microsecs() & 0x3FFFFFFF); // low 30-bits so result is positive + DISPATCH(); + timer_op: + STACK_CHECK(1); + *sp++ = int2obj(timer()); + DISPATCH(); + resetTimer_op: + resetTimer(); + POP_ARGS_COMMAND(); + DISPATCH(); + sayIt_op: + if (!ideConnected()) { + POP_ARGS_COMMAND(); // serial port not open; do nothing + DISPATCH(); + } + printArgs(arg, sp - arg, true, true); + if (!hasOutputSpace(printBufferByteCount + 100)) { // leave room for other messages + ip--; // retry when task is resumed + goto suspend; + } + sendSayForChunk(printBuffer, printBufferByteCount, task->taskChunkIndex); + POP_ARGS_COMMAND(); + // wait for data to be sent; prevents use in tight loop from clogging serial line + task->status = waiting_micros; + task->wakeTime = microsecs() + (extraByteDelay * (printBufferByteCount + 6)); + goto suspend; + graphIt_op: + if (!ideConnected()) { + POP_ARGS_COMMAND(); // serial port not open; do nothing + DISPATCH(); + } + printArgs(arg, sp - arg, false, true); + if (!hasOutputSpace(printBufferByteCount + 100)) { // leave room for other messages + ip--; // retry when task is resumed + goto suspend; + } + #if USE_TASKS + logData(printBuffer); + #else + printf("(NO TASKS) %s\r\n", printBuffer); + #endif + POP_ARGS_COMMAND(); + // wait for data to be sent; prevents use in tight loop from clogging serial line + task->status = waiting_micros; + task->wakeTime = microsecs() + (extraByteDelay * (printBufferByteCount + 6)); + goto suspend; + boardType_op: + *(sp - arg) = primBoardType(); + POP_ARGS_REPORTER(); + DISPATCH(); + argOrDefault_op: + if (arg < 2) { + *(sp - arg) = fail(notEnoughArguments); // not enough arguments to primitive + } else if (fp <= task->stack) { + *(sp - arg) = *(sp - 1); // not in a function call; return default value + } else { + *(sp - arg) = argOrDefault(fp, obj2int(*(sp - 2)), *(sp - 1)); + } + POP_ARGS_REPORTER(); + DISPATCH(); + + // I/O operations: + analogPins_op: + *(sp - arg) = primAnalogPins(sp - arg); + POP_ARGS_REPORTER(); + DISPATCH(); + digitalPins_op: + *(sp - arg) = primDigitalPins(sp - arg); + POP_ARGS_REPORTER(); + DISPATCH(); + analogRead_op: + *(sp - arg) = primAnalogRead(arg, sp - arg); + POP_ARGS_REPORTER(); + DISPATCH(); + analogWrite_op: + primAnalogWrite(sp - arg); + POP_ARGS_COMMAND(); + DISPATCH(); + digitalRead_op: + *(sp - arg) = primDigitalRead(arg, sp - arg); + POP_ARGS_REPORTER(); + DISPATCH(); + digitalWrite_op: + primDigitalWrite(sp - arg); + POP_ARGS_COMMAND(); + DISPATCH(); + digitalSet_op: + // no args to pop; pin number is encoded in arg field of instruction + primDigitalSet(arg, true); + DISPATCH(); + digitalClear_op: + // no args to pop; pin number is encoded in arg field of instruction + primDigitalSet(arg, false); + DISPATCH(); + buttonA_op: + *(sp - arg) = primButtonA(sp - arg); + POP_ARGS_REPORTER(); + DISPATCH(); + buttonB_op: + *(sp - arg) = primButtonB(sp - arg); + POP_ARGS_REPORTER(); + DISPATCH(); + setUserLED_op: + primSetUserLED(sp - arg); + POP_ARGS_COMMAND(); + DISPATCH(); + i2cSet_op: + primI2cSet(sp - arg); + POP_ARGS_COMMAND(); + DISPATCH(); + i2cGet_op: + *(sp - arg) = primI2cGet(sp - arg); + POP_ARGS_REPORTER(); + DISPATCH(); + spiSend_op: + primSPISend(sp - arg); + POP_ARGS_COMMAND(); + DISPATCH(); + spiRecv_op: + *(sp - arg) = primSPIRecv(sp - arg); + POP_ARGS_REPORTER(); + DISPATCH(); + + // more time operations + secs_op: + STACK_CHECK(1); + *sp++ = int2obj((uint32) ((totalMicrosecs() / 1000000)) & 0x3FFFFFFF); // result range is 0 - 1073741823 + DISPATCH(); + millisSince_op: + *(sp - arg) = primMSecsSince(arg, sp - arg); + POP_ARGS_REPORTER(); + DISPATCH(); + microsSince_op: + *(sp - arg) = primUSecsSince(arg, sp - arg); + POP_ARGS_REPORTER(); + DISPATCH(); + + pushHugeInteger_op: + // push integer object that requires 32 bits + STACK_CHECK(1); + tmp = (uint16) *ip++; // least significant bits + tmp |= (uint16) *ip++ << 16; // most significant bits + *sp++ = (OBJ) tmp; + DISPATCH(); + + // new primitive call ops: + commandPrimitive_op: + tmp = (*ip >> 10) & 0x3F; // primitive set index + tmpObj = (OBJ) (ip + (*ip & 0x3FF)); // primitive name object + ip++; // skip second instruction word + arg = arg & 0xFF; // argument count + doPrimitiveCall(tmp, obj2str(tmpObj), arg, sp - arg); + POP_ARGS_COMMAND(); + DISPATCH(); + reporterPrimitive_op: + tmp = (*ip >> 10) & 0x3F; // primitive set index + tmpObj = (OBJ) (ip + (*ip & 0x3FF)); // primitive name object + ip++; // skip second instruction word + arg = arg & 0xFF; // argument count + *(sp - arg) = doPrimitiveCall(tmp, obj2str(tmpObj), arg, sp - arg); + POP_ARGS_REPORTER(); + DISPATCH(); + + // call a function using the function name and parameter list: + callCustomCommand_op: + callCustomReporter_op: + if (arg > 0) { + taskSleep(-1); // do background VM tasks sooner + uint32 callee = -1; + OBJ params = *(sp - 1); // save the parameters array, if any + // look up the function or primitive name + if ((arg == 1) && (IS_TYPE(*(sp - 1), StringType))) { + callee = findCallee(obj2str(*(sp - 1))); + } else if ((arg == 2) && (IS_TYPE(*(sp - 2), StringType))) { + callee = findCallee(obj2str(*(sp - 2))); + } + POP_ARGS_COMMAND(); + if (callee != -1) { // found a callee + int paramCount = 0; + if (arg == 2) { // has an optional parameters list (the second argument) + if (IS_TYPE(params, ListType)) { // push the parameters onto the stack + paramCount = (obj2int(FIELD(params, 0)) & 0xFF); + for (int i = 1; i <= paramCount; i++) { + *sp++ = FIELD(params, i); + } + } else { // fail: parameters must be a list + *sp++ = fail(needsListError); + DISPATCH(); + } + } + + // invoke the callee + task->sp = sp - task->stack; // record the stack pointer in case callee does a GC + if ((callee & 0xFFFFFF00) == 0xFFFFFF00) { // callee is a MicroBlocks function (i.e. a chunk index) + arg = ((callee & 0xFF) << 8) | paramCount; + goto callFunctionByName; + } else { // callee is a named primitive (i.e. a pointer to a C function) + tmpObj = ((PrimitiveFunction) callee)(paramCount, sp - paramCount); // call the primitive + tempGCRoot = NULL; // clear tempGCRoot in case it was used + sp -= paramCount; + *sp++ = tmpObj; // push primitive return value + DISPATCH(); + } + } + } + // failed: bad arguments + *sp++ = falseObj; // push a dummy return value + DISPATCH(); +} + +// Interpreter Entry Point + +#if !defined(EMSCRIPTEN) + +void vmLoop() { + // Run the next runnable task. Wake up any waiting tasks whose wakeup time has arrived. + + int currentTaskIndex = 0; + int count = 0; + while (true) { + if (count-- < 0) { + // do background VM tasks once every N VM loop cycles + processMessage(); + checkButtons(); + #if defined(HAS_LED_MATRIX) + updateMicrobitDisplay(); + #endif + #if defined(COCUBE) + cocubeSensorUpdate(); + #endif + handleMicosecondClockWrap(); + count = 95; // must be under 30 when building on mbed to avoid serial errors + } else if ((count & 0xF) == 0) { + captureIncomingBytes(); + } + int runCount = 0; + uint32 usecs = 0; // compute times only the first time they are needed + for (int t = 0; t < taskCount; t++) { + currentTaskIndex++; + if (currentTaskIndex >= taskCount) currentTaskIndex = 0; + Task *task = &tasks[currentTaskIndex]; + if (unusedTask == task->status) { + continue; + } else if (running == task->status) { + runTask(task); + runCount++; + break; + } else if (waiting_micros == task->status) { + if (!usecs) usecs = microsecs(); // get usecs + if ((usecs - task->wakeTime) < RECENT) { + task->status = running; + runTask(task); + runCount++; + break; + } + } + } + if (taskSleepUSecs) { + // if any task called taskSleep(), do VM background tasks sooner + taskSleepUSecs = 0; + count = (count < 5000) ? count : 5000; + } + +#ifdef GNUBLOCKS + if (!runCount) { // no active tasks; consider taking a nap + if (!usecs) usecs = microsecs(); // get usecs + int sleepUSecs = 2000000; + for (int i = 0; i < taskCount; i++) { + Task *task = &tasks[i]; + if (waiting_micros == task->status) { + int usecsUntilWake = (task->wakeTime - usecs) - 5; // leave 5 extra usecs + if ((usecsUntilWake > 0) && (usecsUntilWake < sleepUSecs)) { + sleepUSecs = usecsUntilWake; + } + } + } + if (sleepUSecs > 5) { + if(waitUSecsOrEvent(sleepUSecs) > 0) { // nap a while to relinquish the CPU + count = -1; + } + } + } +#endif + } +} + +#endif // not EMSCRIPTEN + +// Boardie support + +#ifdef EMSCRIPTEN + +#include + +#define CLOCK_MASK 0xFFFFFFFF + +int shouldYield = false; +void EMSCRIPTEN_KEEPALIVE taskSleep(int msecs) { shouldYield = true; } + +static int currentTaskIndex = 0; // remember this across calls to interpretStep() + +void interpretStep() { + uint32 endTime = millisecs() + 15; + processMessage(); + checkButtons(); + updateMicrobitDisplay(); + shouldYield = false; + while ((millisecs() < endTime) && !shouldYield) { + // Run the next runnable task. Wake up any waiting tasks whose wakeup time has arrived. + int runCount = 0; + uint32 usecs = microsecs(); // get usecs + for (int t = 0; t < taskCount; t++) { + currentTaskIndex++; + if (currentTaskIndex >= taskCount) currentTaskIndex = 0; + Task *task = &tasks[currentTaskIndex]; + if (unusedTask == task->status) { + continue; + } else if (running == task->status) { + runTask(task); + runCount++; + break; + } else if (waiting_micros == task->status) { + if (((usecs - task->wakeTime) & CLOCK_MASK) < RECENT) task->status = running; + } + if (running == task->status) { + runTask(task); + runCount++; + break; + } + } + if (!runCount) { // no active tasks; consider taking a nap + usecs = microsecs(); // get usecs + int sleepUSecs = 100000; + for (int i = 0; i < taskCount; i++) { + Task *task = &tasks[i]; + if (waiting_micros == task->status) { + int usecsUntilWake = (task->wakeTime - usecs) & CLOCK_MASK; + if ((usecsUntilWake > 0) && (usecsUntilWake < sleepUSecs)) { + sleepUSecs = usecsUntilWake; + } + } + } + if (sleepUSecs > 2000) { + shouldYield = true; + break; + } // relinquish control + } + } +} + +#endif + +// Testing + +void runTasksUntilDone() { + // Used for testing/benchmarking the interpreter. Run all tasks to completion. + + int count = 0; + int hasActiveTasks = true; + while (hasActiveTasks) { + if (count-- <= 0) { + processMessage(); + count = 100; // reduce to 30 when building on mbed to avoid serial errors + } + hasActiveTasks = false; + uint32 usecs = 0; // compute times only the first time they are needed + for (int t = 0; t < taskCount; t++) { + Task *task = &tasks[t]; + if (running == task->status) { + runTask(task); + hasActiveTasks = true; + continue; + } else if (unusedTask == task->status) { + continue; + } else if (waiting_micros == task->status) { + if (!usecs) usecs = microsecs(); // get usecs + if ((usecs - task->wakeTime) < RECENT) task->status = running; + } + if (running == task->status) runTask(task); + hasActiveTasks = true; + } + } +} diff --git a/interpreters/smallvm/interp.h b/interpreters/smallvm/interp.h new file mode 100644 index 000000000..4b67eb436 --- /dev/null +++ b/interpreters/smallvm/interp.h @@ -0,0 +1,468 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Copyright 2018 John Maloney, Bernat Romagosa, and Jens Mönig + +// interp.h - Simple interpreter based on 32-bit opcodes +// John Maloney, April 2017 + +#include "mem.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Instruction Format + +#define OP(opcode, arg) (((unsigned) arg << 8) | (opcode & 0xFF)) +#define CMD(n) (n & 0x7F) // use only low 7 bits for now +#define ARG(n) (n >> 8) + +// Global Variables + +#define MAX_VARS 128 +extern OBJ vars[MAX_VARS]; + +// Code Chunks + +// The code chunk table is an array of CodeChunkRecords. A code chunk is referenced by its +// index in this table. This "chunk index" is used for several purposes: +// +// 1. to specify the called function in function calls +// 2. to specify the code to run when starting a task +// 3. to communicate with the client IDE about code chunks and tasks +// +// Referencing code chunks via their index in the code chunk table allows the actual code +// chunk objects, which may be located in either Flash or RAM memory, to be updated by user +// or edits. When code chunks are stored in Flash (which is write-once until it is erased), +// newer versions of a chunk are appended to the end of Flash. At startup time, Flash memory +// is scanned, and the chunks[] table is reconstructed with references to the latest version +// of each chunk. + +typedef enum { + unusedChunk = 0, + command = 1, + reporter = 2, + functionHat = 3, + startHat = 4, + whenConditionHat = 5, + broadcastHat = 6, + buttonAHat = 7, + buttonBHat = 8, + buttonsAandBHat = 9, +} ChunkType_t; + +typedef struct { + OBJ code; + uint8 chunkType; +} CodeChunkRecord; + +#define MAX_CHUNKS 255 +extern CodeChunkRecord chunks[MAX_CHUNKS]; + +// Task List + +// The task list is an array of taskCount Tasks. Each Task has a chunkIndex for +// the top-level block of the task, as well as for the current function chunk when +// inside a call to user-defined function. It also holds the task status, processor +// state (instruction pointer (ip), stack pointer (sp), and frame pointer (fp)), +// and the wakeTime (used when a task is waiting on the microsecond clock). +// In the current design, Tasks have a fixed-size stack built in. In the future, +// this will become a reference to a growable stack object in memory. +// +// "When " hats have their condition test compiled into them. They +// loop back and suspend themselves when the condition is false. When the condition +// becomes true, execution proceeds to the blocks under the hat. + +typedef enum { + unusedTask = 0, // task entry is available + waiting_micros = 1, // waiting for microseconds to reach wakeTime + running = 2, +} MicroBlocksTaskStatus_t; + +#ifdef GNUBLOCKS + #define STACK_LIMIT 1000 // Task size is 6 + STACK_LIMIT words +#elif (defined(NRF51) || defined(ESP8266) || defined(DUELink)) + #define STACK_LIMIT 54 // Task size is 6 + STACK_LIMIT words +#else + #define STACK_LIMIT 125 // Task size is 6 + STACK_LIMIT words +#endif + +typedef struct { + uint8 status; // MicroBlocksTaskStatus_t, stored as a byte + uint8 taskChunkIndex; // chunk index of the top-level stack for this task + uint8 currentChunkIndex; // chunk index when inside a function + uint32 wakeTime; + OBJ code; + int ip; // ip offset in code + int sp; + int fp; + OBJ stack[STACK_LIMIT]; +} Task; + +// Task list shared by interp.c and runtime.c + +#define MAX_TASKS 5 +extern Task tasks[MAX_TASKS]; +extern int taskCount; + +// Extra delay used to limit serial transmission speed + +extern int extraByteDelay; + +// Serial Protocol Messages: IDE -> Board + +#define chunkCodeMsg 1 // bidirectional +#define deleteChunkMsg 2 +#define startChunkMsg 3 +#define stopChunkMsg 4 +#define startAllMsg 5 +#define stopAllMsg 6 +#define getVarMsg 7 // value returned to IDE via varValueMsg +#define setVarMsg 8 +#define getVarNamesMsg 9 +#define clearVarsMsg 10 +#define getChunkCRCMsg 11 +#define getVersionMsg 12 +#define getAllCodeMsg 13 +#define deleteAllCodeMsg 14 +#define systemResetMsg 15 + +// Serial Protocol Messages: Board -> IDE + +#define taskStartedMsg 16 +#define taskDoneMsg 17 +#define taskReturnedValueMsg 18 +#define taskErrorMsg 19 +#define outputValueMsg 20 +#define varValueMsg 21 +#define versionMsg 22 +#define chunkCRCMsg 23 +#define clearGraphMsg 24 +#define codeStoreFullMsg 25 + +// Serial Protocol Messages: Bidirectional + +#define pingMsg 26 +#define broadcastMsg 27 +#define chunkAttributeMsg 28 +#define varNameMsg 29 +#define extendedMsg 30 +#define enableBLEMsg 31 +#define chunkCode16Msg 32 +#define codeStoreUsedMsg 33 + +// Serial Protocol Messages: CRC Exchange + +#define getAllCRCsMsg 38 +#define allCRCsMsg 39 +#define LAST_MSG 39 + +// Error Codes (codes 1-9 are reserved for protocol errors; 10 and up are runtime errors) + +#define noError 0 // No error +#define unspecifiedError 1 // Unknown error +#define badChunkIndexError 2 // Unknown chunk index + +#define insufficientMemoryError 10 // Insufficient memory to allocate object +#define needsListError 11 // Needs a list +#define needsBooleanError 12 // Needs a boolean +#define needsIntegerError 13 // Needs an integer +#define needsStringError 14 // Needs a string +#define nonComparableError 15 // Those objects cannot be compared for equality +#define arraySizeError 16 // List size must be a non-negative integer +#define needsIntegerIndexError 17 // List or string index must be an integer +#define indexOutOfRangeError 18 // List or string index out of range +#define byteArrayStoreError 19 // A ByteArray can only store integer values between 0 and 255 +#define hexRangeError 20 // Hexadecimal input must between between -40000000 and 3FFFFFFF +#define i2cDeviceIDOutOfRange 21 // I2C device ID must be between 0 and 127 +#define i2cRegisterIDOutOfRange 22 // I2C register must be between 0 and 255 +#define i2cValueOutOfRange 23 // I2C value must be between 0 and 255 +#define notInFunction 24 // Attempt to access an argument outside of a function +#define badForLoopArg 25 // for-loop argument must be a positive integer or list +#define stackOverflow 26 // Insufficient stack space +#define primitiveNotImplemented 27 // Primitive not implemented in this virtual machine +#define notEnoughArguments 28 // Not enough arguments passed to primitive +#define waitTooLong 29 // The maximum wait time is 3600000 milliseconds (one hour) +#define noWiFi 30 // This board does not support WiFi +#define zeroDivide 31 // Division (or modulo) by zero is not defined +#define argIndexOutOfRange 32 // Argument index out of range +#define needsIndexable 33 // Needs an indexable type such as a string or list +#define joinArgsNotSameType 34 // All arguments to join must be the same type (e.g. lists) +#define i2cTransferFailed 35 // I2C transfer failed +#define needsByteArray 36 // Needs a byte array +#define serialPortNotOpen 37 // Serial port not open +#define serialWriteTooBig 38 // Serial port write is limited to 128 bytes +#define needsListOfIntegers 39 // Needs a list of integers +#define byteOutOfRange 40 // Needs a value between 0 and 255 +#define needsPositiveIncrement 41 // Range increment must be a positive integer +#define needsIntOrListOfInts 42 // Needs an integer or a list of integers +#define wifiNotConnected 43 // Not connected to a WiFi network +#define cannotConvertToInteger 44 // Cannot convert that to an integer +#define cannotConvertToBoolean 45 // Cannot convert that to a boolean +#define cannotConvertToList 46 // Cannot convert that to a list +#define cannotConvertToByteArray 47 // Cannot convert that to a byte array +#define unknownDatatype 48 // Unknown datatype +#define invalidUnicodeValue 49 // Unicode values must be between 0 and 1114111 (0x10FFFF) +#define cannotUseWithBLE 50 // Cannot use this feature when board is connected to IDE via Bluetooth +#define bad8BitBitmap 51 // Needs an 8-bit bitmap: a list containing the bitmap width and contents (a byte array) +#define badColorPalette 52 // Needs a color palette: a list of positive 24-bit integers representing RGB values +#define encoderNotStarted 53 // Encoder not started; pin may not support interrupts +#define scriptTooLarge 54 // Script too large +#define udpPortNotOpen 55 // UDP port not open +#define sleepSignal 255 // Not a real error; used to make current task sleep + +// Runtime Operations + +OBJ fail(uint8 errCode); +int failure(); +void initTasks(void); +void startAll(); +void stopAllTasksButThis(Task *task); +void startReceiversOfBroadcast(char *msg, int byteCount); +void processMessage(void); +int hasOutputSpace(int byteCount); +int bytesToOutput(void); +void logData(char *s); +void outputString(const char *s); +void sendTaskDone(uint8 chunkIndex); +void sendTaskError(uint8 chunkIndex, uint8 errorCode, int where); +void sendTaskReturnValue(uint8 chunkIndex, OBJ returnValue); +void sendBroadcastToIDE(char *s, int len); +void sendCodeStoreFull(); +int broadcastMatches(uint8 chunkIndex, char *msg, int byteCount); +void sendSayForChunk(char *s, int len, uint8 chunkIndex); +void vmLoop(void); +void interpretStep(); +void taskSleep(int msecs); +void taskSleepMicros(int usecs); +int waitUSecsOrEvent(int usecs); +void vmPanic(const char *s); +int indexOfVarNamed(const char *varName); +void processFileMessage(int msgType, int dataSize, char *data); +void waitAndSendMessage(int msgType, int chunkIndex, int dataSize, char *data); +void deferIDEDisconnect(); +void suspendCodeFileUpdates(); +void resumeCodeFileUpdates(); + +// Debugging + +void consolePrint(const char *s); +void consoleReportNum(const char *label, int n); + +// Integer Evaluation + +static inline int evalInt(OBJ obj) { + if (isInt(obj)) { + return obj2int(obj); + } else if (IS_TYPE(obj, StringType)) { + // try to parse an int out of a string + return strtol(obj2str(obj), NULL, 10); // returns 0 if string is not a number + } else if (IS_TYPE(obj, ByteArrayType)) { + // try to parse an int out of a byte array (treating it as a string) + return strtol((char *) &FIELD(obj, 0), NULL, 10); // returns 0 if string is not a number + } else { + fail(needsIntegerError); + return 0; + } +} + +// Testing Support + +void startTaskForChunk(uint8 chunkIndex); +void runTasksUntilDone(void); + +void interpTests1(void); +void taskTest(void); + +// Platform Specific Operations + +uint64 totalMicrosecs(); +uint32 microsecs(void); +uint32 millisecs(void); +uint32 seconds(); +void handleMicosecondClockWrap(); + +int chunkIndexForFunction(char *functionName); + +int ideConnected(); +int recvBytes(uint8 *buf, int count); +int sendBytes(uint8 *buf, int start, int end); +void captureIncomingBytes(); +void restartSerial(); + +const char *boardType(); +int hasPSRAM(); +void hardwareInit(void); + +int readI2CReg(int deviceID, int reg); +void writeI2CReg(int deviceID, int reg, int value); + +// I/O Support + +extern int mbDisplayColor; + +int pinCount(); +int mapDigitalPinNum(int userPinNum); +void setPinMode(int pin, int newMode); +void turnOffPins(); +void resetTimer(); +int hasI2CPullups(); +void updateMicrobitDisplay(); +void checkButtons(); +void resetRadio(); +void stopPWM(); +void stopServos(); +void stopTone(); +int readAnalogMicrophone(); +void setPicoEdSpeakerPin(int pin); +void showMicroBitPixels(int microBitDisplayBits, int xPos, int yPos); +void setAllNeoPixels(int pin, int ledCount, int color); + +// Primitives + +OBJ primNewList(int argCount, OBJ *args); +OBJ primFillList(int argCount, OBJ *args); +OBJ primAt(int argCount, OBJ *args); +OBJ primAtPut(int argCount, OBJ *args); +OBJ primLength(int argCount, OBJ *args); + +OBJ primHexToInt(int argCount, OBJ *args); + +OBJ primBroadcastToIDEOnly(int argCount, OBJ *args); + +OBJ primAnalogPins(OBJ *args); +OBJ primDigitalPins(OBJ *args); +OBJ primAnalogRead(int argCount, OBJ *args); +void primAnalogWrite(OBJ *args); +OBJ primDigitalRead(int argCount, OBJ *args); +void primDigitalWrite(OBJ *args); +void primDigitalSet(int pinNum, int flag); +OBJ primButtonA(OBJ *args); +OBJ primButtonB(OBJ *args); +void primSetUserLED(OBJ *args); + +OBJ primI2cGet(OBJ *args); +OBJ primI2cSet(OBJ *args); +OBJ primSPISend(OBJ *args); +OBJ primSPIRecv(OBJ *args); + +OBJ primMBDisplay(int argCount, OBJ *args); +OBJ primMBDisplayOff(int argCount, OBJ *args); +OBJ primMBEnableDisplay(int argCount, OBJ *args); +OBJ primMBPlot(int argCount, OBJ *args); +OBJ primMBUnplot(int argCount, OBJ *args); + +OBJ primMBDrawShape(int argCount, OBJ *args); +OBJ primMBShapeForLetter(int argCount, OBJ *args); + +OBJ primMBTiltX(int argCount, OBJ *args); +OBJ primMBTiltY(int argCount, OBJ *args); +OBJ primMBTiltZ(int argCount, OBJ *args); +OBJ primMBTemp(int argCount, OBJ *args); + +OBJ primNeoPixelSend(int argCount, OBJ *args); +OBJ primNeoPixelSetPin(int argCount, OBJ *args); +void turnOffInternalNeoPixels(); + +OBJ primDeferUpdates(int argCount, OBJ *args); +OBJ primResumeUpdates(int argCount, OBJ *args); + +// TFT Support + +extern int useTFT; +extern int isOLED1106; + +void tftInit(); +void tftClear(); +void tftSetHugePixel(int x, int y, int state); +void tftSetHugePixelBits(int bits); + +// CoCube Sensor Support + +void cocubeSensorInit(); +void cocubeSensorUpdate(); + +// BLE Support + +extern int BLE_connected_to_IDE; +extern char BLE_ThreeLetterID[4]; +extern uint32 lastRcvTime; + +void BLE_initThreeLetterID(); +void BLE_start(); +void BLE_stop(); + +void BLE_pauseAdvertising(); +void BLE_resumeAdvertising(); +void BLE_setPicoAdvertisingData(char *name, const char *uuidString); + +void BLE_setEnabled(int enableFlag); +int BLE_isEnabled(); + +void BLE_UART_ReceiveCallback(uint8 *data, int byteCount); +void BLE_UART_Send(uint8 *data, int byteCount); + +void getMACAddress(uint8 *sixBytes); + +// Primitive Sets + +// These primitive set indices are compiled into primitive calls, so their order cannot change. +// New primitive sets must be added at the end, just before PrimitiveSetCount. +typedef enum { + VarPrims, + DataPrims, + MiscPrims, + IOPrims, + SensorPrims, + SerialPrims, + DisplayPrims, + FilePrims, + NetPrims, + BLEPrims, + RadioPrims, + TFTPrims, + HIDPrims, + CameraPrims, + OneWirePrims, + EncoderPrims, + SDCardPrims, + PrimitiveSetCount +} PrimitiveSetIndex; + +void addVarPrims(); +void addDataPrims(); +void addMiscPrims(); +void addIOPrims(); +void addSensorPrims(); +void addSerialPrims(); +void addDisplayPrims(); +void addFilePrims(); +void addNetPrims(); +void addBLEPrims(); +void addRadioPrims(); +void addTFTPrims(); +void addHIDPrims(); +void addCameraPrims(); +void addOneWirePrims(); +void addEncoderPrims(); +void addSDCardPrims(); + +// Named Primitive Support + +typedef OBJ (*PrimitiveFunction)(int argCount, OBJ *args); + +typedef const struct { + const char *primName; + PrimitiveFunction primFunc; +} PrimEntry; + +void addPrimitiveSet(PrimitiveSetIndex primSetIndex, const char *setName, int entryCount, PrimEntry *entries); +OBJ doPrimitiveCall(PrimitiveSetIndex setIndex, const char *primName, int argCount, OBJ *args); +void primsInit(); + +#ifdef __cplusplus +} +#endif diff --git a/interpreters/smallvm/mem.c b/interpreters/smallvm/mem.c new file mode 100644 index 000000000..a8037b114 --- /dev/null +++ b/interpreters/smallvm/mem.c @@ -0,0 +1,516 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Copyright 2018 John Maloney, Bernat Romagosa, and Jens Mönig + +// mem.c - object memory +// Just an allocator for now; no garbage collector. +// John Maloney, April 2017 + +#include +#include +#include +#include + +#include "mem.h" +#include "interp.h" + +// Object Store +// +// The object store is a contiguous portion of RAM used to store objects. The object store +// contains both object and free memory "chunks". Whether it is an object or free, every +// memory chunk starts with a header word that specifies its length and type. Free chunks +// have a zero type field. +// +// Chunks start on 32-bit word boundaries and are multiples of 32-bit words. Chunks are packed +// contiguously, so the entire object store can be scanned by jumping from one chunk header to +// the next. The final chunk is always a free chunk. The object allocator carves objects off the +// free chunk until it is no longer large enough for a desired allocation. At that point, the +// garbage collector/compactor is run, consolidating all free space into the final free chunk. +// +// Every chunk starts with a header word with its size and type: +// +// +// +// An extra header word, called the "forwarding field" is reserved immediately before the header +// word of each chunk. That field is used by the marking phase of the garbage collector and to +// update (forward) references of objects that move during compaction and object resizing. + +#if defined(NRF51) + #define OBJSTORE_BYTES 1120 +#elif defined(ARDUINO_BBC_MICROBIT_V2) || defined(ARDUINO_CALLIOPE_MINI_V3) + #define OBJSTORE_BYTES 45000 +#elif defined(ARDUINO_NRF52_PRIMO) + #define OBJSTORE_BYTES 13000 +#elif defined(NRF52) + #define OBJSTORE_BYTES 150000 // max is 219000 +#elif defined(ARDUINO_ARCH_SAMD) + #define OBJSTORE_BYTES 11000 +#elif defined(HAS_CAMERA) + #define OBJSTORE_BYTES 230000 // will be allocated from PSRAM +#elif defined(ESP32_S3) || defined(ESP32_C3) + #define OBJSTORE_BYTES 77000 +#elif defined(ARDUINO_ARCH_ESP32) + // object store is allocated from heap on ESP32 + #if defined(USE_NIMBLE) + #define OBJSTORE_BYTES 37000 // max that allows both BLE and WiFi is 59000; drops to 38000 w/ ESP Now + #else + #define OBJSTORE_BYTES 69000 + #endif +#elif defined(GNUBLOCKS) + #define OBJSTORE_BYTES 7200 // max number of bytes that we can allocate for now +#elif defined(ARDUINO_ARCH_RP2040) + #define OBJSTORE_BYTES 57000 +#elif defined(ARDUINO_SAM_DUE) + #define OBJSTORE_BYTES 75000 +#elif defined(CONFIG_BOARD_BEAGLECONNECT_FREEDOM) + #define OBJSTORE_BYTES 37000 +#elif defined(DUELink) + #define OBJSTORE_BYTES 7200 +#else + #define OBJSTORE_BYTES 3600 + // max that works on Wemos D1 mini (ESP8266) is 11000 + // however, WiFi is unreliable for 4 concurrent requestions even down to 7200 + // 5000 seems stable for up to 10 concurrent requests + // max that compiles for all boards is 16886 (17624 NodeMCU) +#endif + +#define OBJSTORE_WORDS ((OBJSTORE_BYTES / 4) + 4) + +#if defined(ARDUINO_ARCH_ESP32) + static OBJ *objstore = NULL; // allocated from heap on ESP32 + + #include + int hasPSRAM() { + return heap_caps_get_total_size(MALLOC_CAP_SPIRAM) > 0; + } +#else + static OBJ objstore[OBJSTORE_WORDS]; + + int hasPSRAM() { return false; } +#endif + +static OBJ memStart = NULL; +static OBJ memEnd = NULL; +static OBJ freeChunk = NULL; + +OBJ tempGCRoot = NULL; // used during resizeObj() and primitives that allocate multiple objects + +extern OBJ lastBroadcast; // an additional GC root + +// Initialization + +void memInit() { + // verify 32-bit architecture + if (!(sizeof(int) == 4 && sizeof(int*) == 4 && sizeof(float) == 4)) { + vmPanic("MicroBlocks expects int, int*, and float to all be 32-bits"); + } + + #if defined(ARDUINO_ARCH_ESP32) + objstore = (OBJ *) malloc(4 * OBJSTORE_WORDS); + if (!objstore) vmPanic("ESP32 could not allocate objectstore"); + #endif + + // initialize object heap memory + memStart = (OBJ) objstore; + memEnd = (OBJ) (objstore + OBJSTORE_WORDS); + memClear(); +} + +void memClear() { + // Clear object memory and set all global variables to zero. + + // clear global variables + for (int i = 0; i < MAX_VARS; i++) vars[i] = zeroObj; + lastBroadcast = zeroObj; + + // zero objectstore memory (not essential) + memset(objstore, 0, sizeof(objstore)); + + // create the free chunk (prefixed by a forwarding word) + objstore[0] = (OBJ) 0; // forwarding word + objstore[1] = (OBJ) HEADER(FREE_CHUNK, OBJSTORE_WORDS - 2); // free chunk + freeChunk = (OBJ) &objstore[1]; +} + +int wordsFree() { + int result = WORDS(freeChunk) - 2; + return (result < 0) ? 0 : result; +} + +void vmPanic(const char *errorMessage) { + // Called when VM encounters a fatal error. Output the given message and loop forever. + // NOTE: This call never returns! + + char s[100]; + sprintf(s, "\r\nVM Panic: %s\r\n", errorMessage); + outputString(s); + while (true) processMessage(); // there's no way to recover; loop forever! +} + +// Forward References + +void applyForwarding(); +void clearForwardingFields(); +void gc(); + +// Object Allocation + +OBJ newObj(int type, int wordCount, OBJ fill) { + // Allocate a new object of the given size. + + // check available space + int available = WORDS(freeChunk); + if (available < (wordCount + 2)) { + gc(); + available = WORDS(freeChunk); // retry after garbage collection + if (available < (wordCount + 2)) return fail(insufficientMemoryError); + } + + // allocate result and update freeChunk + OBJ result = (OBJ) freeChunk; + freeChunk += wordCount + 2; + *freeChunk = HEADER(FREE_CHUNK, available - (wordCount + 2)); + + // initialize and return the new object + *(result - 1) = 0; // clear its forwarding word + *result = HEADER(type, wordCount); // set header word + OBJ *ptr = (OBJ *) result + 1; + OBJ *end = ptr + wordCount; + while (ptr < end) { *ptr++ = fill; } + return result; +} + +OBJ resizeObj(OBJ oldObj, int wordCount) { + // Change the size of the given object to wordCount and return the new object. + + if (isInt(oldObj)) return oldObj; + if ((oldObj < memStart) || (oldObj >= memEnd)) return oldObj; // object must be in object store + + tempGCRoot = oldObj; // record oldObj in case newObj() triggers GC that moves it + OBJ result = newObj(TYPE(oldObj), wordCount, zeroObj); + oldObj = tempGCRoot; // restore oldObj + tempGCRoot = NULL; + if (!result) return oldObj; + + int copyCount = WORDS(oldObj); + if (wordCount < copyCount) copyCount = wordCount; // new size is smaller + memcpy(result + 1, oldObj + 1, 4 * copyCount); // copy from the old to the new body + + clearForwardingFields(); + *(oldObj - 1) = (uint32) result; // point forwarding field of oldObj to result + applyForwarding(); + *(oldObj - 1) = 0; // clear forwarding field + *oldObj = HEADER(FREE_CHUNK, WORDS(oldObj)); // mark oldObj free + + return result; +} + +// String Primitives + +OBJ newString(int byteCount) { + // Allocate a string that can hold byteCount bytes. + + int wordCount = ((byteCount + 1) + 3) / 4; // leave room for terminator byte + return newObj(StringType, wordCount, 0); +} + +OBJ newStringFromBytes(const char *bytes, int byteCount) { + // Create a new string object with the given bytes. + // Round up to an even number of words and pad with nulls. + + OBJ result = newString(byteCount); + if (!result) return result; // insufficient room to allocate string (newObj reported failure) + + char *dst = (char *) &result[HEADER_WORDS]; + for (int i = 0; i < byteCount; i++) *dst++ = *bytes++; + *dst = 0; // null terminator byte + return result; +} + +char* obj2str(OBJ obj) { + if (isInt(obj)) return (char *) ""; + if (isBoolean(obj)) return (char *) ((trueObj == obj) ? "true" : "false"); + if (IS_TYPE(obj, StringType)) return (char *) &obj[HEADER_WORDS]; + if (IS_TYPE(obj, ListType)) return (char *) ""; + if (IS_TYPE(obj, ByteArrayType)) return (char *) ""; + return (char *) ""; +} + +// Debugging Utilities + +void reportNum(const char *msg, int n) { + char s[100]; + sprintf(s, "%s: %d", msg, n); + outputString(s); +} + +void reportHex(const char *msg, int n) { + char s[100]; + sprintf(s, "%s: 0x%x", msg, n); + outputString(s); +} + +void reportObj(const char *msg, OBJ obj) { + char s[100]; + int type = objType(obj); + switch (type) { + case IntegerType: + sprintf(s, "%s: %d", msg, obj2int(obj)); + break; + case StringType: + sprintf(s, "%s: %s", msg, obj2str(obj)); + break; + case BooleanType: + sprintf(s, "%s: %s", msg, ((trueObj == obj) ? "true" : "false")); + break; + case ListType: + sprintf(s, "%s: List (%d fields)", msg, obj2int(FIELD(obj, 0))); + break; + default: + sprintf(s, "%s: (%d fields)", msg, type, WORDS(obj)); + } + outputString(s); +} + +void dumpObjectStore() { + char s[100]; + + outputString("Object store:"); + uint32 *end = (uint32 *) &objstore[OBJSTORE_WORDS]; + uint32 *next = (uint32 *) objstore + 1; + uint32 *base = (uint32 *) objstore; + while (next < end) { + int wordCount = WORDS(next); + int type = TYPE(next); + if (type) { + uint32 *fwd = (uint32 *) *(next - 1); + if (fwd) { + if (fwd > base) fwd = (uint32 *) (fwd - base); // word offset in objstore + sprintf(s, "%d type: %d words: %d fwd: %d", (next - base), type, wordCount, (int) fwd); + } else { + sprintf(s, "%d type: %d words: %d", (next - base), type, wordCount); + } + } else { + sprintf(s, "%d FREE %d", (next - base), wordCount); + } + outputString(s); + next = next + wordCount + 2; + } + outputString("----------"); +} + +void memDumpObj(OBJ obj) { + char s[100]; + + if ((obj < memStart) || (obj >= memEnd)) { + sprintf(s, "bad object at %x", (int) obj); + outputString(s); + return; + } + int typeID = TYPE(obj); + int wordCount = WORDS(obj); + sprintf(s, "%x: %d words, typeID %d", (int) obj, wordCount, typeID); + outputString(s); + + sprintf(s, "Header: %x", obj[0]); + outputString(s); + + for (int i = 0; i < wordCount; i++) { + sprintf(s, " 0x%x,", obj[HEADER_WORDS + i]); + outputString(s); + } +} + +// Object Forwarding + +void clearForwardingFields() { + // Set all forwarding fields to zero. This may not be needed if we maintain the invariant + // that forward fields are zero except during garbage collection or forwarding operations. + + uint32 *end = (uint32 *) &objstore[OBJSTORE_WORDS]; + uint32 *next = (uint32 *) objstore + 1; + while (next < end) { + *(next - 1) = 0; // clear forwarding field + next += WORDS(next) + 2; + } +} + +static inline OBJ forward(OBJ obj) { + if (isInt(obj)) return obj; + if ((obj < memStart) || (obj > memEnd)) return obj; // outside the object store + OBJ fwd = (OBJ) *(obj - 1); + return fwd ? fwd : obj; // forward if the forwarding field is not zero +} + +static void forwardRoots(void) { + // forward global variables + for (int i = 0; i < MAX_VARS; i++) vars[i] = forward(vars[i]); + lastBroadcast = forward(lastBroadcast); + + if (tempGCRoot) tempGCRoot = forward(tempGCRoot); + + // forward objects on Task stacks + for (int i = 0; i < taskCount; i++) { + Task *task = &tasks[i]; + if (task->status != unusedTask) { + for (int j = tasks[i].sp - 1; j >= 0; j--) { + task->stack[j] = forward(task->stack[j]); + } + } + } +} + +void applyForwarding() { + // Update all forwarded references. + + uint32 *end = (uint32 *) &objstore[OBJSTORE_WORDS]; + uint32 *next = (uint32 *) objstore + 1; + while (next < end) { + if (TYPE(next) > BinaryObjectTypes) { // non-free chunk with OBJ fields (not a string) + for (int i = WORDS(next); i > 0; i--) { + OBJ child = (OBJ) next[i]; + if (!isInt(child) && // child is not an integer + ((memStart < child) && (child <= memEnd)) && // child is in the object store + *(child - 1)) { // child has a non-zero forwarding field + next[i] = *(child - 1); // update the forwarded OBJ + } + } + } + next += WORDS(next) + 2; + } + forwardRoots(); +} + +// Mark-Sweep-Compact Garbage Collector + +#define SET_MARK(obj) ((*(((uint32 *) (obj)) - 1)) = 1) +#define IS_MARKED(obj) (*(((uint32 *) (obj)) - 1)) + +void mark(OBJ root) { + // Mark all objects reachable from the given root. + + if (isInt(root)) return; + if ((root < memStart) || (root > memEnd)) return; // ignore objects outside the object store + if (IS_MARKED(root)) return; // already marked + + OBJ current = root; + int i = WORDS(current); // scan backwards from last field + + while (1) { + if (i == 0) { // done processing fields of the current object + SET_MARK(current); + if (current == root) return; // we're done! + OBJ parent = (OBJ) *current; // backpointer to parent was stored in header + i = *(parent - 1); // restore field index in parent + *current = parent[i]; // restore header of child + parent[i] = (int) current; // restore pointer to child in parent[i] + current = parent; + i--; // process the next field of parent + continue; + } + + // process next child + OBJ child = (OBJ) current[i]; + if (!isInt(child) && (memStart <= child) && (child <= memEnd) && !IS_MARKED(child)) { + // child an unmarked, non-integer object in the object store + if (TYPE(child) > BinaryObjectTypes) { // child has pointer fields to process + // reverse pointers before processing child + current[i] = *child; // store child's header it ith field of current + *(current - 1) = i; // store i in forwarding field of current + i = WORDS(child); // scan backwards from last field of child + *child = (int) current; // backpointer to current + current = child; // process child + } else { + SET_MARK(child); + } + } else { + i--; + } + } +} + +static void markRoots(void) { + // mark global variables + for (int i = 0; i < MAX_VARS; i++) mark(vars[i]); + mark(lastBroadcast); + + // mark temporary object used during object resizing + if (tempGCRoot) mark(tempGCRoot); + + // mark objects on Task stacks + for (int i = 0; i < taskCount; i++) { + Task *task = &tasks[i]; + if (task->status != unusedTask) { + for (int j = tasks[i].sp - 1; j >= 0; j--) { + mark(task->stack[j]); + } + } + } +} + +void sweep() { + // Scan object memory and set the forwarding fields of surviving objects that will move. + + uint32 *end = (uint32 *) &objstore[OBJSTORE_WORDS]; + uint32 *next = (uint32 *) objstore + 1; + uint32 *dst = next; + while (next < end) { + uint32 wordCount = WORDS(next); + if (*(next - 1)) { // surviving object + // set the forwarding field to dst if the object will move, zero if not + *(next - 1) = (dst != next) ? (uint32) dst : 0; + dst += wordCount + 2; + } else { // inaccessible object or free chunk + // mark chunk as free by clearing its type field + *next = HEADER(FREE_CHUNK, wordCount); + } + next += wordCount + 2; + } +} + +void compact() { + // Consolidate free space into a single free chunk. + + uint32 *next = (uint32 *) objstore + 1; + uint32 *end = (uint32 *) &objstore[OBJSTORE_WORDS]; + uint32 *dst = next; + while (next < end) { + uint32 wordCount = WORDS(next); + if (TYPE(next)) { // live object chunk + if (dst != next) memmove(dst, next, 4 * (wordCount + 1)); // move object, if necessary + dst += wordCount + 1; + *dst++ = 0; // forwarding word + } + next += wordCount + 2; + } + uint32 freeWords = (end - dst) - 1; + *dst = HEADER(FREE_CHUNK, freeWords); + freeChunk = (OBJ) dst; +} + +void gc() { + // Perform a garbage collection to reclaim unused objects and compact memory. + // Call captureIncomingBytes() to avoid serial buffer overruns during garbage collection. + + captureIncomingBytes(); + updateMicrobitDisplay(); + + uint32 usecs = microsecs(); + + // assume: forwarding pointers cleared at end of compaction so no need to clear them here + markRoots(); + sweep(); + applyForwarding(); + compact(); + + usecs = microsecs() - usecs; + + char s[100]; + sprintf(s, "GC took %d usecs; free %d words", usecs, WORDS(freeChunk) - 2); + outputString(s); + + captureIncomingBytes(); + updateMicrobitDisplay(); +} diff --git a/interpreters/smallvm/mem.h b/interpreters/smallvm/mem.h new file mode 100644 index 000000000..a06160d66 --- /dev/null +++ b/interpreters/smallvm/mem.h @@ -0,0 +1,199 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Copyright 2018 John Maloney, Bernat Romagosa, and Jens Mönig + +// mem.h - Object memory definitions using 32-bit object references +// John Maloney, April 2017 + +#ifndef _MEM_H_ +#define _MEM_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +// Unify Arduino IDE and PlatformIO +#if defined(NRF52_SERIES) && !defined(NRF52) + #define NRF52 1 +#endif + +// Define short symbols ESP32 variants +#if defined(CONFIG_IDF_TARGET_ESP32S2) + #define ESP32_S2 1 +#elif defined(CONFIG_IDF_TARGET_ESP32S3) + #define ESP32_S3 1 +#elif defined(CONFIG_IDF_TARGET_ESP32C3) + #define ESP32_C3 1 +#elif defined(CONFIG_IDF_TARGET_ESP32C6) + #define ESP32_C6 1 +#endif + +#if defined(ARDUINO_NUCLEO_C071RB) + #define DUELink 1 + + // HAS_LED_MATRIX is defined for all boards but display operations ignored if not CincoBit + #define HAS_LED_MATRIX 1 + + // Return the 24-bit product ID from the OTP area + #define DUE_PID (*((uint32 *) 0x1FFF7004) & 0xFFFFFF) + + // These macros use the 24-bit product ID from the OTP area. + // CincoBit = 1, PixoBit = 2; ID's 3-15 are reserved for future boards with edge connectors + #define DUE_HAS_EDGE_CONNECTOR (DUE_PID < 16) + #define IS_DUE_CINCO (DUE_PID == 1) + #define IS_DUE_STEM (DUE_PID == 16) + #define IS_DUE_CLIPIT (DUE_PID == 17) + #define IS_DUE_CHRONO (DUE_PID == 0x0C0004) +#endif + +#if defined(ESP32) && !(defined(ESP32_S2) || defined(ESP32_S3) || defined(ESP32_C3) || defined(ESP32_C6)) + #define ESP32_ORIGINAL 1 +#endif + +#if defined(ARDUINO_ARCH_RP2040) && !defined(__MBED__) + #define RP2040_PHILHOWER 1 +#endif + +#if defined(XRP) && defined(PICO_RP2350) && (PICO_RP2350A == 0) + #define XRP_2350 1 +#endif + +#if defined(BLE_IDE) || defined(BLE_KEYBOARD) || defined(BLE_UART) || defined(BLE_OCTO) + #define USE_NIMBLE 1 +#endif + +#if defined(ARDUINO_BBC_MICROBIT) || defined(ARDUINO_CALLIOPE_MINI) || defined(CALLIOPE_V3) || \ + defined(ARDUINO_BBC_MICROBIT_V2) || defined(M5Atom_Matrix) || \ + defined(GNUBLOCKS) || defined(ARDUINO_Mbits) || defined(STEAMaker) || defined(FOXBIT) + #define HAS_LED_MATRIX 1 +#endif + +// Integer types + +typedef unsigned char uint8; +typedef unsigned short uint16; +typedef unsigned int uint32; +typedef unsigned long long uint64; + +typedef signed short int int16; + +// Boolean constants for readability (if not already defined) + +#if !defined(__cplusplus) && (!defined(true) || !defined(false)) + #define true 1 + #define false 0 +#endif + +// Object reference type (32-bits) + +typedef int * OBJ; + +// Type IDs + +#define FREE_CHUNK 0 +#define BooleanType 1 +#define IntegerType 2 +#define ByteArrayType 3 +#define StringType 4 +// types 5-7 reserved for future non-pointer objects +#define BinaryObjectTypes 7 // objects with type ID's <= 7 do not contain pointers +#define ArrayType 8 +#define ListType 9 + +// Booleans +// Note: These are constants, not pointers to objects in memory. + +#define falseObj ((OBJ) 0) +#define trueObj ((OBJ) 4) + +#define isBoolean(obj) ((obj) <= trueObj) + +// Integers + +// Integers are encoded in object references; they have no memory object. +// They have a 1 in their lowest bit and a signed value in their top 31 bits. + +#define isInt(obj) (((int) (obj)) & 1) +#define int2obj(n) ((OBJ) (((n) << 1) | 1)) +#define obj2int(obj) ((int)(obj) >> 1) +#define zeroObj ((OBJ) 1) + +// Memory Objects +// +// Even-valued object references (except true and false) point to an object in memory. +// Memory objects start with one or more header words. + +#define HEADER_WORDS 1 +#define HEADER(typeID, wordCount) (((wordCount) << 4) | ((typeID) & 0xF)) +#define WORDS(obj) ((*((uint32*) (obj)) >> 4) & 0xFFFF) +#define TYPE(obj) (*((uint32*) (obj)) & 0xF) + +static inline int objWords(OBJ obj) { + if (isInt(obj) || isBoolean(obj)) return 0; + return WORDS(obj); +} + +// ByteArray Objects +// +// ByteArray objects use two bits in the object header to adjust their size in bytes so that +// they are not limited to multiples of four bytes. To get the size in bytes, this field +// is subtracted from 4 * WORDS(obj). + +#define BYTECOUNT_ADJUST(obj) ((*((uint32*) (obj)) >> 29) & 0x3) +#define BYTES(obj) (4 * WORDS(obj) - BYTECOUNT_ADJUST(obj)) + +static inline void setByteCountAdjust(OBJ obj, int byteCount) { + if (isInt(obj) || isBoolean(obj) || (ByteArrayType != TYPE(obj))) return; + int delta = 4 - (byteCount & 3); // # of bytes to subtract from 4 * WORDS(obj) + *obj = ((delta & 3) << 29) | ((*obj) & 0x9FFFFFFF); +} + +// Types + +static inline int objType(OBJ obj) { + if (isInt(obj)) return IntegerType; + if (isBoolean(obj)) return BooleanType; + return TYPE(obj); +} + +// Type check for non-integer/boolean objects +// (Note: Use isInt() and isBoolean() to test for integers and booleans) + +#define IS_TYPE(obj, typeID) (((((int) obj) & 1) == 0) && ((obj) > trueObj) && (TYPE(obj) == typeID)) + +// FIELD() macro can be used either to get or set an object field (zero-based) + +#define FIELD(obj, i) (((OBJ *) obj)[HEADER_WORDS + (i)]) + +// Global temporary GC root for use by primitives that do multiple allocations. + +extern OBJ tempGCRoot; + +// Object Memory Operations + +void memInit(); +void memClear(); +int wordsFree(); +void gc(); + +OBJ newObj(int typeID, int wordCount, OBJ fill); +OBJ resizeObj(OBJ obj, int wordCount); +OBJ newString(int byteCount); +OBJ newStringFromBytes(const char *bytes, int byteCount); +char* obj2str(OBJ obj); + +// Debugging Support + +void reportNum(const char *msg, int n); +void reportHex(const char *msg, int n); +void reportObj(const char *msg, OBJ obj); +void dumpObjectStore(void); +void memDumpObj(OBJ obj); + +#ifdef __cplusplus +} +#endif + +#endif // _MEM_H_ diff --git a/interpreters/smallvm/miscPrims.c b/interpreters/smallvm/miscPrims.c new file mode 100644 index 000000000..e989728b7 --- /dev/null +++ b/interpreters/smallvm/miscPrims.c @@ -0,0 +1,838 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Copyright 2019 John Maloney, Bernat Romagosa, and Jens Mönig + +// miscPrims.c - Miscellaneous primitives +// John Maloney, May 2019 + +#include +#include +#include +#include + +#include "mem.h" +#include "interp.h" +#include "tinyJSON.h" +#include "version.h" + +OBJ primVersion(int argCount, OBJ *args) { + int result = atoi(&VM_VERSION[1]); // skip initial "v" + return int2obj(result); +} +#if 0 +OBJ primBLE_ID(int argCount, OBJ *args) { + OBJ result; + if (strlen(BLE_ThreeLetterID) == 3) { + result = newStringFromBytes(BLE_ThreeLetterID, 3); + } else { + const char bleNotSupported[] = "BLE not supported"; + result = newStringFromBytes(bleNotSupported, strlen(bleNotSupported)); + } + if (!result) return fail(insufficientMemoryError); + return result; +} +#endif +OBJ primHexToInt(int argCount, OBJ *args) { + if (!IS_TYPE(args[0], StringType)) return fail(needsStringError); + + char *s = obj2str(args[0]); + if ('#' == *s) s++; // skip leading # if there is one + if (('0' == *s) && (('x' == s[1]) || ('X' == s[1]))) s += 2; // skip leading '0x' or '0X' + long result = strtol(s, NULL, 16); + result = (result << 1) >> 1; // extend sign bit if bit 31 is set + if ((result < -0x40000000) || (result > 0x3FFFFFFF)) return fail(hexRangeError); + return int2obj(result); +} + +OBJ primBinaryToInt(int argCount, OBJ *args) { + if (!IS_TYPE(args[0], StringType)) return fail(needsStringError); + + char *s = obj2str(args[0]); + long result = strtol(s, NULL, 2); + result = (result << 1) >> 1; // extend sign bit if bit 31 is set + if ((result < -0x40000000) || (result > 0x3FFFFFFF)) return fail(hexRangeError); + return int2obj(result); +} + +OBJ primRescale(int argCount, OBJ *args) { + if (argCount < 5) return fail(notEnoughArguments); + int inVal = evalInt(args[0]); + int inMin = evalInt(args[1]); + int inMax = evalInt(args[2]); + int outMin = evalInt(args[3]); + int outMax = evalInt(args[4]); + + if (inMax == inMin) return fail(zeroDivide); + + int result = outMin + (((inVal - inMin) * (outMax - outMin)) / (inMax - inMin)); + return int2obj(result); +} + +// HSV Colors + +static float clampedPercent(int percent) { + // Return a float between 0.0 and 1.0 for the given percent. + + if (percent < 0) percent = 0; + if (percent > 100) percent = 100; + return (float) percent / 100.0; +} + +static void extractHSV(int rgb, float *hue, float *sat, float *bri) { + int r = (rgb >> 16) & 255; + int g = (rgb >> 8) & 255; + int b = rgb & 255; + + int min = (r < g) ? ((r < b) ? r : b) : ((g < b) ? g : b); + int max = (r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b); + + if (max == min) { + // gray; hue is arbitrarily chosen to be zero + *hue = 0.0; + *sat = 0.0; + *bri = max / 255.0; + } + + int f = 0; + int i = 0; + if (r == min) { + f = g - b; + i = 3; + } else if(g == min) { + f = b - r; + i = 5; + } else if (b == min) { + f = r - g; + i = 1; + } + + *hue = fmod(60.0 * (i - (((float) f) / (max - min))), 360.0); + *sat = 0.0; + if (max > 0) *sat = ((float) (max - min)) / max; + *bri = max / 255.0; +} + +OBJ primHSVColor(int argCount, OBJ *args) { + if (argCount < 3) return fail(notEnoughArguments); + + int h = evalInt(args[0]) % 360; + if (h < 0) h += 360; + float s = clampedPercent(evalInt(args[1])); + float v = clampedPercent(evalInt(args[2])); + + int i = h / 60; + float f = (h / 60.0) - i; + float p = v * (1.0 - s); + float q = v * (1.0 - (s * f)); + float t = v * (1.0 - (s * (1.0 - f))); + float r, g, b; + + switch (i) { + case 0: + r = v; g = t; b = p; + break; + case 1: + r = q; g = v; b = p; + break; + case 2: + r = p; g = v; b = t; + break; + case 3: + r = p; g = q; b = v; + break; + case 4: + r = t; g = p; b = v; + break; + case 5: + r = v; g = p; b = q; + break; + } + + int rgb = (((int) (255 * r)) << 16) | (((int) (255 * g)) << 8) | ((int) (255 * b)); + return int2obj(rgb); +} + +OBJ primColorHue(int argCount, OBJ *args) { + if (argCount < 1) return fail(notEnoughArguments); + float h, s, v; + extractHSV(evalInt(args[0]), &h, &s, &v); + return int2obj((int) h); +} + +OBJ primColorSaturation(int argCount, OBJ *args) { + if (argCount < 1) return fail(notEnoughArguments); + float h, s, v; + extractHSV(evalInt(args[0]), &h, &s, &v); + return int2obj((int) (100.0 * s)); +} + +OBJ primColorBrightness(int argCount, OBJ *args) { + if (argCount < 1) return fail(notEnoughArguments); + float h, s, v; + extractHSV(evalInt(args[0]), &h, &s, &v); + return int2obj((int) (100.0 * v)); +} + +static int16 sineTable[91] = { + 0, 286, 572, 857, 1143, 1428, 1713, 1997, 2280, 2563, 2845, + 3126, 3406, 3686, 3964, 4240, 4516, 4790, 5063, 5334, 5604, + 5872, 6138, 6402, 6664, 6924, 7182, 7438, 7692, 7943, 8192, + 8438, 8682, 8923, 9162, 9397, 9630, 9860, 10087, 10311, 10531, + 10749, 10963, 11174, 11381, 11585, 11786, 11982, 12176, 12365, 12551, + 12733, 12911, 13085, 13255, 13421, 13583, 13741, 13894, 14044, 14189, + 14330, 14466, 14598, 14726, 14849, 14968, 15082, 15191, 15296, 15396, + 15491, 15582, 15668, 15749, 15826, 15897, 15964, 16026, 16083, 16135, + 16182, 16225, 16262, 16294, 16322, 16344, 16362, 16374, 16382, 16384}; + +static OBJ primIntSine(int argCount, OBJ *args) { + // Returns the sine of the given angle * 2^14 (i.e. a fixed point integer with 13 bits of + // fraction). The input is the angle in hundreths of a degree (e.g. 4500 means 45 degrees). + // This version uses table lookup with interpolation and uses int integer math, no floats. + + // This version using floats is 3x to 4x slower: + // const float hundrethsToRadians = 6.2831853071795864769 / 36000.0; + // return int2obj((int) round(16384.0 * sin(evalInt(args[0]) * hundrethsToRadians))); + + int angle = evalInt(args[0]) % 36000; + if (angle < 0) angle += 36000; // positive angle in hundreds of a degree [0..35999] + + int sign = 1; + if (angle < 9000) { + // first quarter; use angle directly + } else if (angle < 18000) { + angle = 18000 - angle; // second quarter: reverse of first quarter + } else if (angle < 27000) { + sign = -1; + angle = angle - 18000; // third quarter; like first quarter but invert sign of output + } else { + sign = -1; + angle = 36000 - angle; // fourth quarter; like second quarter but invert sign of output + } + + int i = angle / 100; // sineTable index + int frac = angle % 100; // fraction (0-99) + + int result = sineTable[i]; + if (frac) { + result = (((100 - frac) * result) + (frac * sineTable[i + 1])) / 100; + } + + return int2obj(sign * result); +} + +static OBJ primIntSqrt(int argCount, OBJ *args) { + // Returns the integer square root of a given number rounded to the nearest integer. + // For example, sqrt(9) = 3. To get more precision, you can pre-multiply by a scaling + // factor squared. For example, to get two digits of precision you can multiple by + // 100 * 100 = 10000. The square root of two with two digits: sqrt(20000) = 141 + + int n = evalInt(args[0]); + if (n < 0) n = -n; // xxx should we give an error here? + + // The following code generates same values as: + // round(sqrt(n))) + // without using floats. It uses Heron's method, a special case of Newton's method. + // https://en.wikipedia.org/wiki/Integer_square_root + + if (n < 2) return int2obj(n); // 0 and 1 return themselves + + int x0 = n / 2; // initial estimate (must be greater than the square root + int x1 = (x0 + (n / x0)) / 2; + while (x0 > x1) { + x0 = x1; + x1 = (x0 + (n / x0)) / 2; + } + + // choose the closer square root between x0 and x0 + 1 + int next = x0 + 1; + if (((next * next) - n) < (n - (x0 * x0))) { + x0 = next; + } + return int2obj(x0); +} + +static OBJ primArctan(int argCount, OBJ *args) { + // Returns angle (in hundredths of a degree) of vector dx, dy. + + if (argCount < 2) return fail(notEnoughArguments); + if (!isInt(args[0]) || !isInt(args[1])) return fail(needsIntegerError); + + double x = obj2int(args[0]); + double y = obj2int(args[1]); + double degreeHundredths = (18000.0 * atan2(y, x)) / 3.141592653589793238463; + + return int2obj((int) round(degreeHundredths)); +} + +static OBJ primPressureToAltitude(int argCount, OBJ *args) { + // Computes the altitude difference (in millimeters) for a given pressure difference. + // dH = 44330 * [ 1 - ( p / p0 ) ^ ( 1 / 5.255) ] + + if (argCount < 2) return fail(notEnoughArguments); + int p0 = obj2int(args[0]); + int p = obj2int(args[1]); + double result = 44330.0 * (1.0 - pow((double) p / p0, (1.0 / 5.255))); // meters + return int2obj((int) (1000.0 * result)); // return result in millimeters +} + +static OBJ primConnectedToIDE(int argCount, OBJ *args) { + return ideConnected() ? trueObj : falseObj; +} + +static OBJ primScriptTooLarge(int argCount, OBJ *args) { + // Used by IDE to report scriptTooLarge errors. + + return fail(scriptTooLarge); +} + +static OBJ jsonValue(char *item) { + char buf[1024]; + char *end; + if (!item) return newString(0); // path not found + + switch (tjr_type(item)) { + case tjr_Array: + case tjr_Object: + end = tjr_endOfItem(item); + return newStringFromBytes(item, (end - item)); + case tjr_Number: + return int2obj(tjr_readInteger(item)); + case tjr_String: + tjr_readStringInto(item, buf, sizeof(buf)); + return newStringFromBytes(buf, strlen(buf)); + case tjr_True: + return trueObj; + case tjr_False: + return falseObj; + case tjr_Null: + return newStringFromBytes("null", 4); + } + return newString(0); // json parse error or end +} + +static OBJ primJSONGet(int argCount, OBJ *args) { + // Return the value at the given path in a JSON string or the empty string + // if the path doesn't refer to anything. The optional third argument returns + // the value of the Nth element of an array or object. + + if (argCount < 2) return fail(notEnoughArguments); + if (!IS_TYPE(args[0], StringType)) return fail(needsStringError); + if (!IS_TYPE(args[1], StringType)) return fail(needsStringError); + char *json = obj2str(args[0]); + char *path = obj2str(args[1]); + int i = ((argCount > 2) && isInt(args[2])) ? obj2int(args[2]) : -1; + + char *item = tjr_atPath(json, path); + int itemType = tjr_type(item); + if ((tjr_Array == itemType) && (i > 0)) { + item++; // skip '[' + for (; i > 1; i--) item = tjr_nextElement(item); + } + if ((tjr_Object == itemType) && (i > 0)) { + item++; // skip '{' + for (; i > 1; i--) { + item = tjr_nextProperty(item, NULL, 0); + item = tjr_nextElement(item); // skip value + } + } + return jsonValue(item); +} + +static OBJ primJSONCount(int argCount, OBJ *args) { + // Return the number of entries in the array or entry at the given path of a JSON string. + + if (argCount < 2) return fail(notEnoughArguments); + if (!IS_TYPE(args[0], StringType)) return fail(needsStringError); + if (!IS_TYPE(args[1], StringType)) return fail(needsStringError); + char *json = obj2str(args[0]); + char *path = obj2str(args[1]); + + char *item = tjr_atPath(json, path); + return int2obj(tjr_count(item)); +} + +static OBJ primJSONValueAt(int argCount, OBJ *args) { + // Return the value for the Nth object or array entry at the given path of a JSON string. + + if (argCount < 3) return fail(notEnoughArguments); + if (!IS_TYPE(args[0], StringType)) return fail(needsStringError); + if (!IS_TYPE(args[1], StringType)) return fail(needsStringError); + if (!isInt(args[2])) return fail(needsIntegerError); + char *json = obj2str(args[0]); + char *path = obj2str(args[1]); + int i = obj2int(args[2]); + + char *item = tjr_atPath(json, path); + return jsonValue(tjr_valueAt(item, i)); +} + +static OBJ primJSONKeyAt(int argCount, OBJ *args) { + // Return the key for the Nth object entry at the given path of a JSON string. + + if (argCount < 3) return fail(notEnoughArguments); + if (!IS_TYPE(args[0], StringType)) return fail(needsStringError); + if (!IS_TYPE(args[1], StringType)) return fail(needsStringError); + if (!isInt(args[2])) return fail(needsIntegerError); + char *json = obj2str(args[0]); + char *path = obj2str(args[1]); + int i = obj2int(args[2]); + + char key[100]; + key[0] = '\0'; + char *item = tjr_atPath(json, path); + tjr_keyAt(item, i, key, sizeof(key)); + return newStringFromBytes(key, strlen(key)); +} + +static OBJ primBMP680GasResistance(int argCount, OBJ *args) { + if (argCount < 3) return fail(notEnoughArguments); + int gas_res_adc = evalInt(args[0]); + int gas_range = evalInt(args[1]); + int range_sw_err = evalInt(args[2]); + + // ensure that gas_range is [0..15] + if (gas_range < 0) gas_range = 0; + if (gas_range > 15) gas_range = 15; + + /* Look up table 1 for the possible gas range values */ + uint32_t lookupTable1[16] = { + UINT32_C(2147483647), UINT32_C(2147483647), UINT32_C(2147483647), UINT32_C(2147483647), + UINT32_C(2147483647), UINT32_C(2126008810), UINT32_C(2147483647), UINT32_C(2130303777), + UINT32_C(2147483647), UINT32_C(2147483647), UINT32_C(2143188679), UINT32_C(2136746228), + UINT32_C(2147483647), UINT32_C(2126008810), UINT32_C(2147483647), UINT32_C(2147483647) }; + + /* Look up table 2 for the possible gas range values */ + uint32_t lookupTable2[16] = { + UINT32_C(4096000000), UINT32_C(2048000000), UINT32_C(1024000000), UINT32_C(512000000), + UINT32_C(255744255), UINT32_C(127110228), UINT32_C(64000000), UINT32_C(32258064), UINT32_C(16016016), + UINT32_C(8000000), UINT32_C(4000000), UINT32_C(2000000), UINT32_C(1000000), UINT32_C(500000), + UINT32_C(250000), UINT32_C(125000) }; + + int64_t var1 = (int64_t) ((1340 + (5 * (int64_t) range_sw_err)) * + ((int64_t) lookupTable1[gas_range])) >> 16; + uint64_t var2 = (((int64_t) ((int64_t) gas_res_adc << 15) - (int64_t) (16777216)) + var1); + int64_t var3 = (((int64_t) lookupTable2[gas_range] * (int64_t) var1) >> 9); + uint32_t calc_gas_res = (uint32_t) ((var3 + ((int64_t) var2 >> 1)) / (int64_t) var2); + + return int2obj(calc_gas_res); +} + +const uint8 font5x7[95 * 5] = { + 0x00, 0x00, 0x00, 0x00, 0x00, // space + 0x00, 0x00, 0x4f, 0x00, 0x00, // ! + 0x00, 0x07, 0x00, 0x07, 0x00, // " + 0x14, 0x7f, 0x14, 0x7f, 0x14, // # + 0x24, 0x2a, 0x7f, 0x2a, 0x12, // $ + 0x23, 0x13, 0x08, 0x64, 0x62, // % + 0x36, 0x49, 0x55, 0x22, 0x20, // & + 0x00, 0x05, 0x03, 0x00, 0x00, // ' + 0x00, 0x1c, 0x22, 0x41, 0x00, // ( + 0x00, 0x41, 0x22, 0x1c, 0x00, // ) + 0x14, 0x08, 0x3e, 0x08, 0x14, // // + 0x08, 0x08, 0x3e, 0x08, 0x08, // + + 0x50, 0x30, 0x00, 0x00, 0x00, // , + 0x08, 0x08, 0x08, 0x08, 0x08, // - + 0x00, 0x60, 0x60, 0x00, 0x00, // . + 0x20, 0x10, 0x08, 0x04, 0x02, // / + 0x3e, 0x51, 0x49, 0x45, 0x3e, // 0 + 0x00, 0x42, 0x7f, 0x40, 0x00, // 1 + 0x42, 0x61, 0x51, 0x49, 0x46, // 2 + 0x21, 0x41, 0x45, 0x4b, 0x31, // 3 + 0x18, 0x14, 0x12, 0x7f, 0x10, // 4 + 0x27, 0x45, 0x45, 0x45, 0x39, // 5 + 0x3c, 0x4a, 0x49, 0x49, 0x30, // 6 + 0x01, 0x71, 0x09, 0x05, 0x03, // 7 + 0x36, 0x49, 0x49, 0x49, 0x36, // 8 + 0x06, 0x49, 0x49, 0x29, 0x1e, // 9 + 0x00, 0x36, 0x36, 0x00, 0x00, // : + 0x00, 0x56, 0x36, 0x00, 0x00, // ; + 0x08, 0x14, 0x22, 0x41, 0x00, // < + 0x14, 0x14, 0x14, 0x14, 0x14, // = + 0x00, 0x41, 0x22, 0x14, 0x08, // > + 0x02, 0x01, 0x51, 0x09, 0x06, // ? + 0x3e, 0x41, 0x5d, 0x55, 0x1e, // @ + 0x7e, 0x11, 0x11, 0x11, 0x7e, // A + 0x7f, 0x49, 0x49, 0x49, 0x36, // B + 0x3e, 0x41, 0x41, 0x41, 0x22, // C + 0x7f, 0x41, 0x41, 0x22, 0x1c, // D + 0x7f, 0x49, 0x49, 0x49, 0x41, // E + 0x7f, 0x09, 0x09, 0x09, 0x01, // F + 0x3e, 0x41, 0x49, 0x49, 0x7a, // G + 0x7f, 0x08, 0x08, 0x08, 0x7f, // H + 0x00, 0x41, 0x7f, 0x41, 0x00, // I + 0x20, 0x40, 0x41, 0x3f, 0x01, // J + 0x7f, 0x08, 0x14, 0x22, 0x41, // K + 0x7f, 0x40, 0x40, 0x40, 0x40, // L + 0x7f, 0x02, 0x0c, 0x02, 0x7f, // M + 0x7f, 0x04, 0x08, 0x10, 0x7f, // N + 0x3e, 0x41, 0x41, 0x41, 0x3e, // O + 0x7f, 0x09, 0x09, 0x09, 0x06, // P + 0x3e, 0x41, 0x51, 0x21, 0x5e, // Q + 0x7f, 0x09, 0x19, 0x29, 0x46, // R + 0x26, 0x49, 0x49, 0x49, 0x32, // S + 0x01, 0x01, 0x7f, 0x01, 0x01, // T + 0x3f, 0x40, 0x40, 0x40, 0x3f, // U + 0x1f, 0x20, 0x40, 0x20, 0x1f, // V + 0x3f, 0x40, 0x38, 0x40, 0x3f, // W + 0x63, 0x14, 0x08, 0x14, 0x63, // X + 0x07, 0x08, 0x70, 0x08, 0x07, // Y + 0x61, 0x51, 0x49, 0x45, 0x43, // Z + 0x00, 0x7f, 0x41, 0x41, 0x00, // [ + 0x02, 0x04, 0x08, 0x10, 0x20, // (backslash) + 0x00, 0x41, 0x41, 0x7f, 0x00, // ] + 0x04, 0x02, 0x01, 0x02, 0x04, // ^ + 0x40, 0x40, 0x40, 0x40, 0x40, // _ + 0x00, 0x00, 0x03, 0x05, 0x00, // ` + 0x20, 0x54, 0x54, 0x54, 0x78, // a + 0x7F, 0x44, 0x44, 0x44, 0x38, // b + 0x38, 0x44, 0x44, 0x44, 0x44, // c + 0x38, 0x44, 0x44, 0x44, 0x7f, // d + 0x38, 0x54, 0x54, 0x54, 0x18, // e + 0x04, 0x04, 0x7e, 0x05, 0x05, // f + 0x08, 0x54, 0x54, 0x54, 0x3c, // g + 0x7f, 0x08, 0x04, 0x04, 0x78, // h + 0x00, 0x44, 0x7d, 0x40, 0x00, // i + 0x20, 0x40, 0x44, 0x3d, 0x00, // j + 0x7f, 0x10, 0x28, 0x44, 0x00, // k + 0x00, 0x41, 0x7f, 0x40, 0x00, // l + 0x7c, 0x04, 0x7c, 0x04, 0x78, // m + 0x7c, 0x08, 0x04, 0x04, 0x78, // n + 0x38, 0x44, 0x44, 0x44, 0x38, // o + 0x7c, 0x14, 0x14, 0x14, 0x08, // p + 0x08, 0x14, 0x14, 0x14, 0x7c, // q + 0x7c, 0x08, 0x04, 0x04, 0x08, // r + 0x48, 0x54, 0x54, 0x54, 0x24, // s + 0x04, 0x04, 0x3f, 0x44, 0x44, // t + 0x3c, 0x40, 0x40, 0x20, 0x7c, // u + 0x1c, 0x20, 0x40, 0x20, 0x1c, // v + 0x3c, 0x40, 0x30, 0x40, 0x3c, // w + 0x44, 0x28, 0x10, 0x28, 0x44, // x + 0x0c, 0x50, 0x50, 0x50, 0x3c, // y + 0x44, 0x64, 0x54, 0x4c, 0x44, // z + 0x08, 0x36, 0x41, 0x41, 0x00, // { + 0x00, 0x00, 0x77, 0x00, 0x00, // | + 0x00, 0x41, 0x41, 0x36, 0x08, // } + 0x02, 0x01, 0x02, 0x04, 0x02, // ~ +}; + +static OBJ primShapeforChar(int argCount, OBJ *args) { + // Return a byte array with the columns (left to right) of a character from + // the built-in the font (max 8 pixels tall). Character set is 0 to 255. + + const int fontWidth = 5; + int ascii = -1; + OBJ arg = args[0]; + if (isInt(arg)) { + // argument is an integer + ascii = evalInt(arg); + } else if (IS_TYPE(arg, StringType) && (objWords(arg) > 0)) { + // argument is a non-empty string; use its first (and usually only) byte + ascii = *((uint8 *) &FIELD(arg, 0)); + } + if ((ascii < 32) || (ascii > 126)) return zeroObj; // out of range + + // create byte array + OBJ result = newObj(ByteArrayType, 2, falseObj); // two words, up to 8 bytes + if (result) setByteCountAdjust(result, fontWidth); // font width + + // copy fontWidth bytes, one byte per column + uint8 *dst = (uint8 *) &FIELD(result, 0); + const uint8 *src = &font5x7[fontWidth * (ascii - 32)]; + memcpy(dst, src, fontWidth); + return result; +} + +static OBJ primClearGraph(int argCount, OBJ *args) { + if (!ideConnected()) return falseObj; // do nothing if not connected to IDE + waitAndSendMessage(clearGraphMsg, 0, 0, NULL); + return falseObj; +} + +static OBJ primFunctionExists(int argCount, OBJ *args) { + if ((argCount < 1) || !IS_TYPE(args[0], StringType)) return fail(needsStringError); + + return (chunkIndexForFunction(obj2str(args[0])) < 0) ? falseObj : trueObj; +} + +static OBJ primDUELinkPID(int argCount, OBJ *args) { + return int2obj(*((uint32 *) 0x1FFF7004) & 0xFFFFFF); +} + +#if defined(ARDUINO_ARCH_ESP32) || defined(ESP8266) + +#if defined(ARDUINO_ARCH_ESP32) + #include +#else + // Defined in ioPrims.cpp because it needs to use the ESP C++ class. + void esp8266DeepSleep(uint64_t usecs); +#endif + +static OBJ primESPSleep(int argCount, OBJ *args) { + // Deep sleep for N seconds. When that time elapses, the ESP32 will reset/boot. + // Note: on ESP8266, you must connect GPIO16 ("Wake" pin) to the RST to use deep sleep: + // https://randomnerdtutorials.com/esp8266-deep-sleep-with-arduino-ide/ + + if ((argCount < 1) || !isInt(args[0])) return fail(needsIntegerError); + + uint64_t usecs = obj2int(args[0]) * 1000000; + #if defined(ARDUINO_ARCH_ESP32) + esp_sleep_enable_timer_wakeup(usecs); + esp_deep_sleep_start(); + #else + esp8266DeepSleep(usecs); + #endif + return falseObj; // this is never executed +} + +#endif + +#if defined(DUELink) + +#include +#include +#include +#include // for CDC_deInit() + +void delay(unsigned long); // Arduino delay function + +// These are the only possible wakeup pins on C071: +// WKUP1 - PA0 - Due P1 +// WKUP2 - PC13 (nc) or PA4 - Due P3 +// WKUP3 - PB6 (SCL) - Due P15 +// WKUP4 - PA2 (UART2 TX) - off limits +// WKUP5 - PC5 (nc) +// WKUP6 - PB5 (SPI) - Due P14 +// MicroBlocks currently supports only PA0, PA4 and PB5 (see below) + +static OBJ primDUESleep(int argCount, OBJ *args) { + // Some measurments: + // HAL_PWR_EnterSTOPMode(0, 0); // 500-750 uA (Snowy) + // HAL_PWR_EnterSTANDBYMode(); // 53 uA (can't recall which board; on Snowy it is < 1 uA) + // HAL_PWREx_EnterSHUTDOWNMode(); // < 1 uA (too low to measure) + // Note: Boards with voltage regulators consume 1-3 mA even in shutdown mode. + + // The following allows a user to recover if they create a script like "when started, sleep" + // It gives them ten seconds to connect the IDE to the board so they can change their code. + if (totalMicrosecs() < (5 * 1000000)) return falseObj; // do nothing for N secs after startup + + HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1_HIGH); + HAL_PWREx_EnablePullUpPullDownConfig(); + HAL_PWREx_EnableGPIOPullDown(PWR_GPIO_A, GPIO_PIN_0); + __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WUF1); + + HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN2_HIGH); + HAL_PWREx_EnablePullUpPullDownConfig(); + HAL_PWREx_EnableGPIOPullDown(PWR_GPIO_A, GPIO_PIN_4); + __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WUF2); + +// Commented out to save space (308 bytes) +// if ((argCount > 0) && (args[0] == trueObj)) { +// // STOP mode; wakes up on alarm but uses about 1 mA +// CDC_deInit(); +// HAL_SuspendTick(); // suspend tick interrupts so we don't spontaneously wake up +// HAL_PWR_EnterSTOPMode(0, PWR_STOPENTRY_WFI); // stop; continue from here on wakeup +// SystemClock_Config(); // necessary; restarts the USB clock, I think +// HAL_ResumeTick(); +// CDC_init(); +// HAL_PWREx_DisablePullUpPullDownConfig(); +// } else { + // default: SHUTDOWN mode; wake up on wakeup pin and uses less than 0.001 mA + // on boards without voltage regulators (e.g. Snowy or Chrono) + HAL_PWREx_EnterSHUTDOWNMode(); + __WFI(); // shuts down here; restarts on wakeup +// } + + return falseObj; +} + +#include + +static RTC_HandleTypeDef hrtc; +static bool rtc_initialized = false; + +static void Rtc_Initialize() { + if (rtc_initialized) return; + + LL_RCC_LSI_Enable(); // enable LSI clock + while (LL_RCC_LSI_IsReady() != 1) { /* wait until ready */ } + + __HAL_RCC_SYSCFG_CLK_ENABLE(); + __HAL_RCC_PWR_CLK_ENABLE(); + + RCC_PeriphCLKInitTypeDef PeriphClkInit = {0}; + PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC; + PeriphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSI; + int status = HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit); + if (status != HAL_OK) { + reportNum("HAL_RCCEx_PeriphCLKConfig error", status); + return; + } + + // enable peripheral clock + __HAL_RCC_RTC_ENABLE(); + __HAL_RCC_RTCAPB_CLK_ENABLE(); + + // initialize RTC + hrtc.Instance = RTC; + hrtc.Lock = HAL_UNLOCKED; + hrtc.Init.HourFormat = RTC_HOURFORMAT_24; + hrtc.Init.AsynchPrediv = 0x7F; + hrtc.Init.SynchPrediv = 0xFF; + hrtc.Init.OutPut = RTC_OUTPUT_DISABLE; + hrtc.Init.OutPutRemap = RTC_OUTPUT_REMAP_NONE; + hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH; + hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN; + hrtc.Init.OutPutPullUp = RTC_OUTPUT_PULLUP_NONE; + status = HAL_RTC_Init(&hrtc); + if (status != HAL_OK) { + reportNum("HAL_RTC_Init error", status); + } + + rtc_initialized = true; +} + +static OBJ primDUEGetDateAndTime(int argCount, OBJ *args) { + // Get current time and date. + // Result is list: year month day dayOfWeek hour minute second + + if (!rtc_initialized) Rtc_Initialize(); + + RTC_TimeTypeDef sTime; + RTC_DateTypeDef sDate; + HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN); + HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN); + + OBJ result = newObj(ListType, 8, falseObj); + if (!result) return fail(insufficientMemoryError); // allocation failed + FIELD(result, 0) = int2obj(7); // list size + FIELD(result, 1) = int2obj(2000 + sDate.Year); + FIELD(result, 2) = int2obj(sDate.Month); + FIELD(result, 3) = int2obj(sDate.Date); + FIELD(result, 4) = int2obj(sDate.WeekDay); + FIELD(result, 5) = int2obj(sTime.Hours); + FIELD(result, 6) = int2obj(sTime.Minutes); + FIELD(result, 7) = int2obj(sTime.Seconds); + return result; +} + +static OBJ primDUESetTime(int argCount, OBJ *args) { + // Set time: hours minutes seconds + + if (argCount < 3) return falseObj; // not enough arguments + if (!rtc_initialized) Rtc_Initialize(); + + RTC_TimeTypeDef sTime; + sTime.Hours = obj2int(args[0]); + sTime.Minutes = obj2int(args[1]); + sTime.Seconds = obj2int(args[2]); + sTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE; + sTime.StoreOperation = RTC_STOREOPERATION_RESET; + HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN); + + return falseObj; +} + +static OBJ primDUESetDate(int argCount, OBJ *args) { + // Set date: year month day [optional: weekeday (1-7)] + + if (argCount < 3) return falseObj; // not enough arguments + if (!rtc_initialized) Rtc_Initialize(); + + int year = obj2int(args[0]); + if (year > 2000) year -= 2000; + + RTC_DateTypeDef sDate; + sDate.Year = year; + sDate.Month = obj2int(args[1]); + sDate.Date = obj2int(args[2]); + if (argCount > 3) sDate.WeekDay = obj2int(args[3]); + HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN); + + return falseObj; +} + +// Commented out to save space (~1000 bytes!): +// static OBJ primDUESetAlarm(int argCount, OBJ *args) { +// // Set the alarm: hours minutes seconds (date and weekday are ignored) +// // Call without arguments to disable the alarm +// +// if (argCount < 3) return falseObj; // not enough arguments +// if (!rtc_initialized) Rtc_Initialize(); +// +// if (argCount < 3) { // disable alarm +// HAL_NVIC_DisableIRQ(RTC_IRQn); +// return falseObj; +// } +// +// RTC_AlarmTypeDef sAlarm = {0}; +// sAlarm.Alarm = RTC_ALARM_A; +// sAlarm.AlarmTime.Hours = obj2int(args[0]); +// sAlarm.AlarmTime.Minutes = obj2int(args[1]); +// sAlarm.AlarmTime.Seconds = obj2int(args[2]); +// sAlarm.AlarmMask = RTC_ALARMMASK_DATEWEEKDAY; // ignore date and weekday +// sAlarm.AlarmTime.SubSeconds = 0; +// sAlarm.AlarmSubSecondMask = 0; // ignore subseconds +// +// HAL_NVIC_SetPriority(RTC_IRQn, 0, 0); +// HAL_NVIC_EnableIRQ(RTC_IRQn); +// +// int status = HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, RTC_FORMAT_BIN); +// if (status != HAL_OK) { +// reportNum("HAL_RTC_SetAlarm_IT error", status); +// } +// +// return falseObj; +// } +// +// void RTC_IRQHandler(void) { +// HAL_RTC_AlarmIRQHandler(&hrtc); +// } + +#endif + +// Primitives + +static PrimEntry entries[] = { + {"sqrt", primIntSqrt}, + {"sin", primIntSine}, + {"version", primVersion}, +// {"bleID", primBLE_ID}, + {"hexToInt", primHexToInt}, + {"binToInt", primBinaryToInt}, + {"rescale", primRescale}, + {"connectedToIDE", primConnectedToIDE}, + {"broadcastToIDE", primBroadcastToIDEOnly}, + {"shapeforChar", primShapeforChar}, + {"clearGraph", primClearGraph}, + {"functionExists", primFunctionExists}, +#if defined(ARDUINO_ARCH_ESP32) || defined(ESP8266) + {"espSleep", primESPSleep}, +#endif +#if defined(DUELink) + {"dueLinkPID", primDUELinkPID}, + {"dueSleep", primDUESleep}, + {"dueGetTime", primDUEGetDateAndTime}, + {"dueSetTime", primDUESetTime}, + {"dueSetDate", primDUESetDate}, +// {"dueSetAlarm", primDUESetAlarm}, // commented out to save space +#else + {"hsvColor", primHSVColor}, + {"hue", primColorHue}, + {"saturation", primColorSaturation}, + {"brightness", primColorBrightness}, + {"atan2", primArctan}, + {"pressureToAltitude", primPressureToAltitude}, + {"bme680GasResistance", primBMP680GasResistance}, +#endif + {"jsonGet", primJSONGet}, + {"jsonCount", primJSONCount}, + {"jsonValueAt", primJSONValueAt}, + {"jsonKeyAt", primJSONKeyAt}, + {"scriptTooLarge", primScriptTooLarge}, +}; + +void addMiscPrims() { + addPrimitiveSet(MiscPrims, "misc", sizeof(entries) / sizeof(PrimEntry), entries); +} diff --git a/interpreters/smallvm/nuttx.c b/interpreters/smallvm/nuttx.c new file mode 100644 index 000000000..ad29e2730 --- /dev/null +++ b/interpreters/smallvm/nuttx.c @@ -0,0 +1,473 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Copyright 2018 John Maloney, Bernat Romagosa, and Jens Mönig + +// nuttx.c - Microblocks for NuttX + +// John Maloney, December 2017 +// Bernat Romagosa, February 2018 +// Martin Vajnar, November 2025 + +#define _XOPEN_SOURCE 600 +#define _DEFAULT_SOURCE + +#include +#include +#include // still needed? +#include +#include +#include // still needed? +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#include "mem.h" +#include "interp.h" +#include "persist.h" + +// Timing Functions + +static int startSecs = 0; + +static void initTimers() { + struct timeval now; + gettimeofday(&now, NULL); + startSecs = now.tv_sec; +} + +uint32 microsecs() { + struct timeval now; + gettimeofday(&now, NULL); + + return (1000000 * (now.tv_sec - startSecs)) + now.tv_usec; +} + +uint32 millisecs() { + struct timeval now; + gettimeofday(&now, NULL); + + return (1000 * (now.tv_sec - startSecs)) + (now.tv_usec / 1000); +} + +uint64 totalMicrosecs() { + // Returns a 64-bit integer containing microseconds since start. + struct timeval now; + gettimeofday(&now, NULL); + return (1000000 * (now.tv_sec - startSecs)) + now.tv_usec; +} + +void delay(int ms) { + clock_t start = millisecs(); + while (millisecs() < start + ms); +} + + +// Communication/System Functions + +#ifdef DEBUG +static void print_hex_dump(const unsigned char *buffer, size_t length) { + for (size_t i = 0; i < length; i += 16) { + // Print the hex values + for (size_t j = 0; j < 16; j++) { + if (i + j < length) { + printf("%02X ", buffer[i + j]); + } else { + printf(" "); // Print spaces for missing bytes + } + } + + // Print the ASCII representation + printf(" |"); + for (size_t j = 0; j < 16; j++) { + if (i + j < length) { + printf("%c", isprint(buffer[i + j]) ? buffer[i + j] : '.'); + } + } + printf("|\n"); + } +} +#endif + +static int listen_fd = -1; +static int fd = -1; // pseudo terminal used for communication with the IDE + +int serialConnected() { + return fd > -1; +} + +int waitUSecsOrEvent(int usecs) { + int ret; + int nfds = 2; + struct pollfd fds[2]; + struct timespec timeout = { .tv_sec = usecs / 1000000, .tv_nsec = (usecs % 1000000) * 1000}; + + memset(&fds, 0, sizeof(fds)); + fds[0].fd = fd; + fds[0].events = POLLIN; + fds[1].fd = listen_fd; + fds[1].events = POLLIN; + if (bytesToOutput()) { fds[0].events |= POLLOUT; } + printf("Timeout = %lld.%09ld\n", timeout.tv_sec, timeout.tv_nsec); + ret = ppoll(fds, nfds, &timeout, NULL); + printf("ppoll() = %d\n", ret); + + return ret; +} + +int recvBytes(uint8 *buf, int count) { + int readCount = 0; + + int nfds = 1; + struct pollfd fds[1]; + struct timespec timeout = {0,0}; + + if (fd < 0) { + int ret = accept(listen_fd, NULL, NULL); + if (ret < 0) { + if (errno != EAGAIN && errno != EWOULDBLOCK) { + perror("Error on accept(), will retry: "); + return 0; + } + } else if (ret > 0) { + fd = ret; + } + } + fds[0].fd = fd; + fds[0].events = POLLIN; + int ret = ppoll(fds, nfds, &timeout, NULL); + if (ret == -1) { + perror("ppoll()"); + } else if (ret) { + if ((fds[0].revents & POLLHUP) || (fds[0].revents & POLLERR)) { + close(fd); + fd = -1; + } else if (fds[0].revents & POLLIN) { + readCount = read(fd, buf, count); + if (readCount < 0) { + if (errno == EPIPE) { + close(fd); + fd = -1; + } + readCount = 0; + perror("Error recvBytes: "); + } +#ifdef CONFIG_INTERPRETERS_SMALLVM_TCP + else if (readCount == 0) { + close(fd); + fd = -1; + } +#endif +#ifdef DEBUG + else if (readCount > 0) { + printf("recvBytes: buf = %p, readCount = %d, count = %d\n", buf, readCount, count); + print_hex_dump(buf, readCount); + } +#endif + } + } + return readCount; +} + +int sendBytes(uint8 *buf, int start, int end) { + int writtenBytes = 0; + + int nfds = 1; + struct pollfd fds[1]; + struct timespec timeout = {0,0}; + + if (fd < 0) { + int ret = accept(listen_fd, NULL, NULL); + if (ret < 0) { + if (errno != EAGAIN && errno != EWOULDBLOCK) { + perror("Error on accept(), will retry: "); + return 0; + } + } else if (ret > 0) { + fd = ret; + } + } + fds[0].fd = fd; + fds[0].events = POLLOUT; + int ret = ppoll(fds, nfds, &timeout, NULL); + if (ret == -1) { + perror("ppoll()"); + } else if (ret) { + if ((fds[0].revents & POLLHUP) || (fds[0].revents & POLLERR)) { + close(fd); + fd = -1; + } else if (fds[0].revents & POLLOUT) { + writtenBytes = write(fd, &buf[start], end - start); + if (writtenBytes < 0) { + if (errno == EPIPE) { + close(fd); + fd = -1; + } + writtenBytes = 0; + perror("Error sendBytes no: "); + } + #ifdef CONFIG_INTERPRETERS_SMALLVM_TCP + else if (writtenBytes == 0) { + close(fd); + fd = -1; + } + #endif + #ifdef DEBUG + else if (writtenBytes > 0) { + printf("sendBytes: &buf[start] = %p, start = %d, end = %d, writtenBytes = %d, to write() = %d\n", &buf[start], start, end, writtenBytes, end - start); + print_hex_dump(&buf[start], writtenBytes); + } + #endif + } + } + return writtenBytes; +} + +int ideConnected() { + return serialConnected(); +} + +// System Functions + +const char * boardType() { + return "NuttX"; +} + +static bool ledEnabled = false; +static int led_fd; + +void primSetUserLED(OBJ *args) { + int ret; + if (!ledEnabled) { + ret = open("/dev/led0", O_WRONLY); + if (ret < 0) { + perror("Failed to open /dev/led0: "); + abort(); + } + led_fd = ret; + ledEnabled = true; + } + struct userled_s led; + led.ul_led = 0; + if (trueObj == args[0]) { + led.ul_on = true; + } else { + led.ul_on = false; + } + ret = ioctl(led_fd, ULEDIOC_SETLED, &led); + if (ret < 0) { + perror("Failed to set LED state: "); + } +} + +static bool buttonEnable = false; +static int button_fd; + +OBJ primButtonA(OBJ *args) { + if (!buttonEnable) { + int ret = open("/dev/buttons", O_RDONLY | O_NONBLOCK); + if (ret < 0) { + perror("ERROR: Failed to open /dev/buttons: "); + abort(); + } + button_fd = ret; + buttonEnable = true; + } + btn_buttonset_t sample; + int nbytes = read(button_fd, (void *)&sample, sizeof(btn_buttonset_t)); + if (nbytes > 0) { + return (sample & 1) ? trueObj : falseObj; + } + return falseObj; +} + +OBJ primButtonB(OBJ *args) { + if (!buttonEnable) { + int ret = open("/dev/buttons", O_RDONLY | O_NONBLOCK); + if (ret < 0) { + perror("ERROR: Failed to open /dev/buttons: "); + abort(); + } + button_fd = ret; + buttonEnable = true; + } + btn_buttonset_t sample; + int nbytes = read(button_fd, (void *)&sample, sizeof(btn_buttonset_t)); + if (nbytes > 0) { + return (sample & 2) ? trueObj : falseObj; + } + return falseObj; +} + +// Stubs + +int useTFT = 0; + +void turnOffInternalNeoPixels() { } +OBJ primMBDisplayOff(int argCount, OBJ *args) { return falseObj; } +void stopTone() { } +OBJ primI2cGet(OBJ *args) { return int2obj(0); } +OBJ primI2cSet(OBJ *args) { return int2obj(0); } +OBJ primSPISend(OBJ *args) { return int2obj(0); } +OBJ primSPIRecv(OBJ *args) { return int2obj(0); } +void updateMicrobitDisplay() { } +void resetRadio() { } +void BLE_setEnabled(int enableFlag) { } +void handleMicosecondClockWrap() { } + +// Stubs for IO primitives + +OBJ primAnalogPins(OBJ *args) { return int2obj(0); } +OBJ primDigitalPins(OBJ *args) { return int2obj(0); } +OBJ primAnalogRead(int argCount, OBJ *args) { return int2obj(0); } +void primAnalogWrite(OBJ *args) { } +OBJ primDigitalRead(int argCount, OBJ *args) { return int2obj(0); } +void primDigitalWrite(OBJ *args) { } +void primDigitalSet(int pinNum, int flag) { }; + +// Stubs for other functions not used on Linux + +void processFileMessage(int msgType, int dataSize, char *data) {} +void resetServos() {} +void stopPWM() {} +void systemReset() {} +void turnOffPins() {} +void stopServos() {} + +// Persistence support + +char *codeFileName = "/w25/ublockscode"; +FILE *codeFile; + +int initCodeFile(uint8 *flash, int flashByteCount) { + codeFile = fopen(codeFileName, "ab+"); + fseek(codeFile, 0 , SEEK_END); + long fileSize = ftell(codeFile); + + // read code file into simulated Flash: + fseek(codeFile, 0L, SEEK_SET); + long bytesRead = fread((char*) flash, 1, flashByteCount, codeFile); + if (bytesRead != fileSize) { + outputString("initCodeFile did not read entire file"); + } + return bytesRead; +} + +void writeCodeFile(uint8 *code, int byteCount) { + fwrite(code, 1, byteCount, codeFile); + fflush(codeFile); + sync(); + printf("Written %d bytes to persistent storage.\n", byteCount); +} + +void writeCodeFileWord(int word) { + fwrite(&word, 1, 4, codeFile); + fflush(codeFile); + sync(); + printf("Written %d bytes to persistent storage.\n", 4); +} + +void clearCodeFile(int ignore) { + fclose(codeFile); + remove(codeFileName); + codeFile = fopen(codeFileName, "ab+"); + uint32 cycleCount = ('S' << 24) | 1; // Header record, version 1 + fwrite((uint8 *) &cycleCount, 1, 4, codeFile); + fflush(codeFile); + sync(); + printf("Written %d bytes to persistent storage.\n", 4); +} +// Debug + +static void exitGracefully() { + close(fd); +} + +void segfault() { + printf("-- VM crashed --\n"); + exitGracefully(); +} + +#ifdef CONFIG_INTERPRETERS_SMALLVM_SERIAL +void setupConnection(void) { + fd = open(CONFIG_INTERPRETERS_SMALLVM_SERIAL_DEVICE, + O_NOCTTY | O_NONBLOCK | O_RDWR | O_SYNC); + if (fd < 0) { + perror("setupConnection: open()"); + exit(-1); + } + + struct termios settings; + memset(&settings, 0, sizeof(settings)); + + tcgetattr(fd, &settings); + cfmakeraw(&settings); + cfsetispeed(&settings, B115200); + cfsetospeed(&settings, B115200); + settings.c_cc[VMIN] = 0; + settings.c_cc[VTIME] = 0; + tcsetattr(fd, TCSANOW, &settings); + tcflush(fd, TCIOFLUSH); +} +#endif + +#ifdef CONFIG_INTERPRETERS_SMALLVM_TCP +void setupConnection(void) { + listen_fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0); + const int bool_true = 1; + setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &bool_true, sizeof(bool_true)); + + struct sockaddr_in saddr; + memset(&saddr, 0, sizeof(struct sockaddr_in)); + saddr.sin_family = AF_INET; + saddr.sin_addr.s_addr = htonl(INADDR_ANY); + saddr.sin_port = htons(CONFIG_INTERPRETERS_SMALLVM_TCP_PORT); + + bind(listen_fd, (struct sockaddr *)&saddr, sizeof(struct sockaddr_in)); + listen(listen_fd, 1); + int ret = accept(listen_fd, NULL, NULL); + if (ret < 0) { + if (errno != EAGAIN && errno != EWOULDBLOCK) { + perror("Error on accept(), will retry: "); + } + } else if (ret > 0) { + fd = ret; + } +// setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &bool_true, sizeof(bool_true)); +// setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &bool_true, sizeof(bool_true)); +// int flags = fcntl(fd, F_GETFL, 0); +// fcntl(fd, F_SETFL, flags | O_NONBLOCK); +} +#endif + +// NuttX Main + +int main(int argc, char *argv[]) { + signal(SIGSEGV, segfault); + signal(SIGINT, exit); + signal(SIGPIPE, SIG_IGN); + atexit(exitGracefully); + setupConnection(); + printf("Starting NuttX MicroBlocks...\n"); + initTimers(); + memInit(); + primsInit(); + outputString("Welcome to uBlocks for NuttX!"); + restoreScripts(); + startAll(); + vmLoop(); + return 0; +} diff --git a/interpreters/smallvm/nuttx.h b/interpreters/smallvm/nuttx.h new file mode 100644 index 000000000..c1665afd6 --- /dev/null +++ b/interpreters/smallvm/nuttx.h @@ -0,0 +1 @@ +void delay(int ms); diff --git a/interpreters/smallvm/persist.c b/interpreters/smallvm/persist.c new file mode 100644 index 000000000..07b08c143 --- /dev/null +++ b/interpreters/smallvm/persist.c @@ -0,0 +1,1273 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Copyright 2018 John Maloney, Bernat Romagosa, and Jens Mönig + +// persist.c - Persistent memory for code and variables +// John Maloney, December 2017 + +// Porting: +// Different boards have different Flash sizes and memory layouts. In addition, +// different processors require different code to modify their Flash memory. +// +// To add a new board, add a case to the #ifdef for that board and define the constants: +// +// START - starting address of persistent memory +// HALF_SPACE - size (in bytes) of each half-space; must be a multiple of Flash page size +// +// and implement the platform-specific Flash functions: +// +// static void flashErase(int *startAddr, int *endAddr) +// static void flashWriteData(int *dst, int wordCount, uint8 *src) +// static void flashWriteWord(int *addr, int value) + +#include +#include +#include + +#include "mem.h" +#include "interp.h" +#include "persist.h" + +void delay(unsigned long); // Arduino delay function + +#if defined(ARDUINO_ARCH_ESP32) + // use Flash codestore on all ESP32 variants + #define ESP32_FLASH_CODESTORE true +#endif + +// Half-space header + +#if defined(DUELink) + #define CYCLE_COUNT_WORDS 2 +#else + #define CYCLE_COUNT_WORDS 1 +#endif + +// flash operations for supported platforms + +#if defined(NRF51) || defined(NRF52) || defined(ARDUINO_NRF52_PRIMO) + #include "nrf.h" // nRF51 and nRF52 + + // to check available FLASH, build with -Wl,-Map,output.map and make sure __etext < START + #if defined(NRF51) + // BBC micro:bit and Calliope: App: 0-96k; Persistent Mem: 96k-256k + #define START (96 * 1024) + #define HALF_SPACE (80 * 1024) + #elif defined(ARDUINO_NRF52_PRIMO) + // Primo: SoftDevice: 0-112k; App: 112k-210k; Persistent Mem: 210k-488k; Boot: 488k-512k + #define START (210 * 1024) + #define HALF_SPACE (100 * 1024) + #elif defined(NRF52833_XXAA) + // nrf52833: SoftDevice + app: 0-316k; Persistent Mem: 316-436k; User data: 436k-464k; Boot: 464k-512k + #define START (316 * 1024) + #define HALF_SPACE (60 * 1024) + #elif defined(NRF52840_XXAA) + // nrf52840: SoftDevice + app: 0-450k; Persistent Mem: 450k-570k; User data: 948k-976k; Boot: 976k-1024k + #define START (450 * 1024) + #define HALF_SPACE (60 * 1024) + #elif defined(NRF52) + // nrf52832: SoftDevice + app: 0-316k; Persistent Mem: 316-436k; User data: 436k-464k; Boot: 464k-512k + #define START (316 * 1024) + #define HALF_SPACE (60 * 1024) + #endif + + static void flashErase(int *startAddr, int *endAddr) { + uint32 pageSize = NRF_FICR->CODEPAGESIZE / 4; // page size in words + NRF_NVMC->CONFIG = NVMC_CONFIG_WEN_Een; // enable Flash erase + while (startAddr < endAddr) { + while (NRF_NVMC->READY == NVMC_READY_READY_Busy){} + NRF_NVMC->ERASEPAGE = (int) startAddr; + startAddr += pageSize; + captureIncomingBytes(); + #if defined(BLE_IDE) + delay(30); + #endif + } + NRF_NVMC->CONFIG = 0; // disable Flash erase + } + + static void flashWriteWord(int *addr, int value) { + NRF_NVMC->CONFIG = NVMC_CONFIG_WEN_Wen; // enable Flash write + while (NRF_NVMC->READY == NVMC_READY_READY_Busy){} + *addr = value; + NRF_NVMC->CONFIG = 0; // disable Flash write + } + + static void flashWriteData(int *dst, int wordCount, uint8 *src) { + NRF_NVMC->CONFIG = NVMC_CONFIG_WEN_Wen; // enable Flash write + for ( ; wordCount > 0; wordCount--) { + int n = *src++; + n |= *src++ << 8; + n |= *src++ << 16; + n |= *src++ << 24; + while (NRF_NVMC->READY == NVMC_READY_READY_Busy){} + *dst++ = n; + } + NRF_NVMC->CONFIG = 0; // disable Flash write + } + +#elif defined(ARDUINO_ARCH_SAMD) || \ + defined(ARDUINO_SAMD_MKRZERO) || defined(ARDUINO_SAMD_ZERO) || defined(ARDUINO_SAM_ZERO) || \ + defined(ARDUINO_SAMD_CIRCUITPLAYGROUND_EXPRESS) || defined(ADAFRUIT_ITSYBITSY_M0) + + #include "samr.h" // SAM21D + + // SAMD: App: 0-96k; Persistent Mem: 96k-256k + #define START (136 * 1024) + #define HALF_SPACE (60 * 1024) + + // SAM21 Non-Volatile Memory Controller Registers + #define NVMC_CTRLA ((volatile int *) 0x41004000) + #define NVMC_CTRLB ((volatile int *) 0x41004004) + #define NVMC_INTFLAG ((volatile int *) 0x41004014) + #define NVMC_ADDR ((volatile int *) 0x4100401C) + + // SAM21 Non-Volatile Memory Controller Constants and Commands + #define MANW 128 // manual write bit in NVMC_CTRLB + #define READY_BIT 1 // ready bit in NVMC_INTFLAG + #define CMD_ERASE_PAGE 0xA502 + #define CMD_WRITE_PAGE 0xA504 + + static void flashErase(int *startAddr, int *endAddr) { + while (!(*NVMC_INTFLAG & READY_BIT)){} // wait for previous operation to complete + + while (startAddr < endAddr) { + *NVMC_ADDR = ((int) startAddr) >> 1; // must shift address right by 1-bit (see processor data sheet) + *NVMC_CTRLA = CMD_ERASE_PAGE; + startAddr += 64; // erasure unit (a "row") is 4 * 64 bytes = 64 words + } + } + + static void flashWriteWord(int *addr, int value) { + while (!(*NVMC_INTFLAG & READY_BIT)){} // wait for previous operation to complete + *addr = value; + *NVMC_CTRLA = CMD_WRITE_PAGE; + } + + static void flashWriteData(int *dst, int wordCount, uint8 *src) { + while (!(*NVMC_INTFLAG & READY_BIT)){} // wait for previous operation to complete + + *NVMC_CTRLB = *NVMC_CTRLB & ~MANW; // automatically write pages at page boundaries + for ( ; wordCount > 0; wordCount--) { + int n = *src++; + n |= *src++ << 8; + n |= *src++ << 16; + n |= *src++ << 24; + *dst++ = n; + } + *NVMC_CTRLA = CMD_WRITE_PAGE; // write final partial page + *NVMC_CTRLB = *NVMC_CTRLB | MANW; // stop page auto-write + } + +#elif defined(ARDUINO_SAM_DUE) + #include "sam.h" // AT91SAM3X8E + + // NOTE: Sam3 does not allow writing into the same Flash bank as the executing program. + // Since the uBlocks VM runs in the lower bank of Flash, the persistent memory area + // must be in the upper bank of Flash which starts at 256K. + + #define START (256 * 1024) + #define HALF_SPACE (128 * 1024) + + // SAM3 Flash Memory Controller Registers + #define EFC1_CMD ((volatile int *) 0x400E0C04) // Command register for second 256k bank + #define EFC1_STATUS ((volatile int *) 0x400E0C08) // IntFlag register for second 256k bank + + // SAM3 Flash Memory Controller Constants and Commands + #define READY_BIT 1 + #define KEY 0x5A000000 // Flash command key + #define WRITE_PAGE 1 + #define ERASE_PAGE 3 + + static void flashErase(int *startAddr, int *endAddr) { + while (startAddr < endAddr) { + *EFC1_CMD = KEY | ((int) startAddr & 0xFFFF00) | ERASE_PAGE; + while (!(*EFC1_STATUS & READY_BIT)){} // wait for operation to complete + startAddr += 64; // erasure unit (a "row") is 4 * 64 bytes = 64 words + } + } + + static void flashWriteWord(int *dst, int value) { + *dst = value; + *EFC1_CMD = KEY | ((int) dst & 0xFFFF00) | WRITE_PAGE; + while (!(*EFC1_STATUS & READY_BIT)){} // wait for operation to complete + } + + static void flashWriteData(int *dst, int wordCount, uint8 *src) { + // Copy wordCount words into Flash memory starting at dst. + // The destination address must be word-aligned, but the source need not be. + + dst = (int *) ((int) dst & 0xFFFFFFFC); // ensure that dst is word-aligned + for ( ; wordCount > 0; wordCount--) { + *dst++ = *((int *) src); + src += 4; // increment by 4 bytes + if (0 == ((int) dst & 0x3F)) { // page boundary + *EFC1_CMD = KEY | (((int) dst - 4) & 0xFFFF00) | WRITE_PAGE; // write the previous page + while (!(*EFC1_STATUS & READY_BIT)){} // wait for operation to complete + } + } + *EFC1_CMD = KEY | (((int) dst - 4) & 0xFFFF00) | WRITE_PAGE; // write the final page + while (!(*EFC1_STATUS & READY_BIT)){} // wait for operation to complete + } + +#elif defined(__MK20DX256__) + +#include + +// Teensy 3.2 - 32K Data Flash, 2K FlexRAM, 1K sectors, 32 bit word size +#define START (0x10000000) +#define HALF_SPACE (16 * 1024) + +static void flashErase(int *startAddr, int *endAddr) { + uint32_t addr = (uint32_t)startAddr & 0xFFFFFC00; + uint16_t cmd[] = {0x2380, 0x7003, 0x7803, 0xb25b, 0x2b00, 0xdafb, 0x4770}; + if (!(FTFL_FCNFG & FTFL_FCNFG_RAMRDY)) return; + while (addr < (uint32_t)endAddr) { + __disable_irq(); + *(uint32_t *)&FTFL_FCCOB3 = 0x09800000 | (addr & 0x007FFFFC); + FTFL_FSTAT = 0x70; + (*((void (*)(volatile uint8_t *))((uint32_t)cmd | 1)))(&FTFL_FSTAT); + __enable_irq(); + addr = addr + 1024; + } +} + +static void flashWriteData(int *dst, int wordCount, uint8_t *src) { + uint32_t addr = (uint32_t)dst & 0xFFFFFFFC; + uint16_t cmd[] = {0x2380, 0x7003, 0x7803, 0xb25b, 0x2b00, 0xdafb, 0x4770}; + if (!(FTFL_FCNFG & FTFL_FCNFG_RAMRDY)) return; + while (wordCount > 0) { + uint32_t n = wordCount; + if (n > 256) n = 256; + memcpy((void *)0x14000000, src, n * 4); + __disable_irq(); + *(uint32_t *)&FTFL_FCCOB3 = 0x0B800000 | (addr & 0x007FFFFC); + *(uint32_t *)&FTFL_FCCOB7 = n << 16; + FTFL_FSTAT = 0x70; + (*((void (*)(volatile uint8_t *))((uint32_t)cmd | 1)))(&FTFL_FSTAT); + __enable_irq(); + addr = addr + n * 4; + wordCount -= n; + } +} + +static void flashWriteWord(int *addr, int value) { + flashWriteData(addr, 1, (uint8_t *)&value); +} + +#elif defined(__MK64DX512__) +// Teensy 3.5 - 128K Data Flash, 4K FlexRAM, 4K sectors, 64 bit word size +#define START (0x10000000) +#define HALF_SPACE (64 * 1024) + +#elif defined(__MK66FX1M0__) +// Teensy 3.6 - 256K Data Flash, 4K FlexRAM, 4K sectors, 64 bit word size, can't write in HSRUN mode +#define START (0x10000000) +#define HALF_SPACE (128 * 1024) + +#elif defined(__IMXRT1062__) + +#include + +#if defined(ARDUINO_TEENSY40) +// Teensy 4.0 - 60K Data Flash, 4K sectors, 256 byte pages +#define START (0x601F0000) +#define HALF_SPACE (30 * 1024) + +#elif defined(ARDUINO_TEENSY41) +// Teensy 4.1 - 252K Data Flash, 4K sectors, 256 byte pages +#define START (0x607F0000) +#define HALF_SPACE (124 * 1024) +#endif // Teensy 4.x + +#define LUT0(opcode, pads, operand) (FLEXSPI_LUT_INSTRUCTION((opcode), (pads), (operand))) +#define LUT1(opcode, pads, operand) (FLEXSPI_LUT_INSTRUCTION((opcode), (pads), (operand)) << 16) +#define CMD_SDR FLEXSPI_LUT_OPCODE_CMD_SDR +#define ADDR_SDR FLEXSPI_LUT_OPCODE_RADDR_SDR +#define READ_SDR FLEXSPI_LUT_OPCODE_READ_SDR +#define WRITE_SDR FLEXSPI_LUT_OPCODE_WRITE_SDR +#define PINS1 FLEXSPI_LUT_NUM_PADS_1 +#define PINS4 FLEXSPI_LUT_NUM_PADS_4 + +static void teensy4_flash_wait() { + FLEXSPI_LUT60 = LUT0(CMD_SDR, PINS1, 0x05) | LUT1(READ_SDR, PINS1, 1); // 05 = read status + FLEXSPI_LUT61 = 0; + uint8_t status; + do { + FLEXSPI_IPRXFCR = FLEXSPI_IPRXFCR_CLRIPRXF; // clear rx fifo + FLEXSPI_IPCR0 = 0; + FLEXSPI_IPCR1 = FLEXSPI_IPCR1_ISEQID(15) | FLEXSPI_IPCR1_IDATSZ(1); + FLEXSPI_IPCMD = FLEXSPI_IPCMD_TRG; + while (!(FLEXSPI_INTR & FLEXSPI_INTR_IPCMDDONE)) { + asm("nop"); + } + FLEXSPI_INTR = FLEXSPI_INTR_IPCMDDONE; + status = *(uint8_t *)&FLEXSPI_RFDR0; + } while (status & 1); + FLEXSPI_MCR0 |= FLEXSPI_MCR0_SWRESET; // purge stale data from FlexSPI's AHB FIFO + while (FLEXSPI_MCR0 & FLEXSPI_MCR0_SWRESET) ; // wait + __enable_irq(); +} + +// write bytes into flash memory (which is already erased to 0xFF) +static void teensy4_flash_write(void *addr, const void *data, uint32_t len) { + __disable_irq(); + FLEXSPI_LUTKEY = FLEXSPI_LUTKEY_VALUE; + FLEXSPI_LUTCR = FLEXSPI_LUTCR_UNLOCK; + FLEXSPI_IPCR0 = 0; + FLEXSPI_LUT60 = LUT0(CMD_SDR, PINS1, 0x06); // 06 = write enable + FLEXSPI_LUT61 = 0; + FLEXSPI_LUT62 = 0; + FLEXSPI_LUT63 = 0; + FLEXSPI_IPCR1 = FLEXSPI_IPCR1_ISEQID(15); + FLEXSPI_IPCMD = FLEXSPI_IPCMD_TRG; + arm_dcache_delete(addr, len); // purge old data from ARM's cache + while (!(FLEXSPI_INTR & FLEXSPI_INTR_IPCMDDONE)) ; // wait + FLEXSPI_INTR = FLEXSPI_INTR_IPCMDDONE; + FLEXSPI_LUT60 = LUT0(CMD_SDR, PINS1, 0x32) | LUT1(ADDR_SDR, PINS1, 24); // 32 = quad write + FLEXSPI_LUT61 = LUT0(WRITE_SDR, PINS4, 1); + FLEXSPI_IPTXFCR = FLEXSPI_IPTXFCR_CLRIPTXF; // clear tx fifo + FLEXSPI_IPCR0 = (uint32_t)addr & 0x007FFFFF; + FLEXSPI_IPCR1 = FLEXSPI_IPCR1_ISEQID(15) | FLEXSPI_IPCR1_IDATSZ(len); + FLEXSPI_IPCMD = FLEXSPI_IPCMD_TRG; + const uint8_t *src = (const uint8_t *)data; + uint32_t n; + while (!((n = FLEXSPI_INTR) & FLEXSPI_INTR_IPCMDDONE)) { + if (n & FLEXSPI_INTR_IPTXWE) { + uint32_t wrlen = len; + if (wrlen > 8) wrlen = 8; + if (wrlen > 0) { + memcpy((void *)&FLEXSPI_TFDR0, src, wrlen); + src += wrlen; + len -= wrlen; + } + FLEXSPI_INTR = FLEXSPI_INTR_IPTXWE; + } + } + FLEXSPI_INTR = FLEXSPI_INTR_IPCMDDONE | FLEXSPI_INTR_IPTXWE; + teensy4_flash_wait(); +} + +// erase a 4K sector +static void teensy4_flash_erase_sector(void *addr) { + __disable_irq(); + FLEXSPI_LUTKEY = FLEXSPI_LUTKEY_VALUE; + FLEXSPI_LUTCR = FLEXSPI_LUTCR_UNLOCK; + FLEXSPI_LUT60 = LUT0(CMD_SDR, PINS1, 0x06); // 06 = write enable + FLEXSPI_LUT61 = 0; + FLEXSPI_LUT62 = 0; + FLEXSPI_LUT63 = 0; + FLEXSPI_IPCR0 = 0; + FLEXSPI_IPCR1 = FLEXSPI_IPCR1_ISEQID(15); + FLEXSPI_IPCMD = FLEXSPI_IPCMD_TRG; + arm_dcache_delete((void *)((uint32_t)addr & 0xFFFFF000), 4096); // purge data from cache + while (!(FLEXSPI_INTR & FLEXSPI_INTR_IPCMDDONE)) ; // wait + FLEXSPI_INTR = FLEXSPI_INTR_IPCMDDONE; + FLEXSPI_LUT60 = LUT0(CMD_SDR, PINS1, 0x20) | LUT1(ADDR_SDR, PINS1, 24); // 20 = sector erase + FLEXSPI_IPCR0 = (uint32_t)addr & 0x007FF000; + FLEXSPI_IPCR1 = FLEXSPI_IPCR1_ISEQID(15); + FLEXSPI_IPCMD = FLEXSPI_IPCMD_TRG; + while (!(FLEXSPI_INTR & FLEXSPI_INTR_IPCMDDONE)) ; // wait + FLEXSPI_INTR = FLEXSPI_INTR_IPCMDDONE; + teensy4_flash_wait(); +} + +static void flashErase(int *startAddr, int *endAddr) { + uint32_t addr = (uint32_t)startAddr & 0xFFFFF000; + while (addr < (uint32_t)endAddr) { + teensy4_flash_erase_sector((void *)addr); + addr = addr + 4096; + } +} + +static void flashWriteData(int *dst, int wordCount, uint8_t *src) { + uint32_t n, count, addr = (uint32_t)dst; + + if (wordCount < 1) return; + count = wordCount * 4; + if ((addr & 0xFF) > 0) { + n = 256 - (addr & 0xFF); + if (n > count) n = count; + teensy4_flash_write(addr, src, n); + addr += n; + count -= n; + } + while (count > 0) { + n = count; + if (n > 256) n = 256; + teensy4_flash_write(addr, src, n); + addr += n; + count -= n; + } +} + +static void flashWriteWord(int *addr, int value) { + flashWriteData(addr, 1, (uint8_t *)&value); +} + +#elif defined(ESP32_FLASH_CODESTORE) + #include "esp_partition.h" + + #define START 0 + #define HALF_SPACE (80 * 1024) + + static uint32 flashBaseAddr = 0; // address of mbcode partition in Flash + static uint32 ramBaseAddr = 0; // virutal memory address to which mbcode partition is mapped + + static size_t flashAddr(int *ramAddr) { + // Convert the given RAM address to a Flash address + return flashBaseAddr + ((uint32) ramAddr - ramBaseAddr); + } + + static void flashErase(int *startAddr, int *endAddr) { + uint32 byteCount = 4 * (endAddr - startAddr); + spi_flash_erase_range(flashAddr(startAddr), byteCount); + } + + static void flashWriteWord(int *addr, int value) { + spi_flash_write(flashAddr(addr), &value, 4); + } + + static void flashWriteData(int *dst, int wordCount, uint8 *src) { + spi_flash_write(flashAddr(dst), src, 4 * wordCount); + } + +#elif defined(__ZEPHYR__) + +#include +#include + +#define PERSIST_PARTITION_LABEL persist_partition +#define START FIXED_PARTITION_OFFSET(PERSIST_PARTITION_LABEL) +#define HALF_SPACE (FIXED_PARTITION_SIZE(PERSIST_PARTITION_LABEL) / 2) + +const struct device *flash_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_flash_controller)); + +static void flashErase(int *startAddr, int *endAddr) { + size_t bytes = (endAddr - startAddr) * sizeof(int); + flash_erase(flash_dev, (uintptr_t)startAddr, bytes); +} + +static void flashWriteWord(int *addr, int value) { + flash_write(flash_dev, (uintptr_t)addr, &value, sizeof(value)); +} + +static void flashWriteData(int *dst, int wordCount, uint8 *src) { + flash_write(flash_dev, (uintptr_t)dst, src, wordCount * sizeof(int)); +} + +#elif defined(DUELink) + +#include +#include + +#define STM32_FLASH_START 0x08000000 +#define STM32_PAGE_SIZE 0x800 // 2k bytes + +#define START (STM32_FLASH_START + (96 * 1024)) +#define HALF_SPACE (16 * 1024) + +static void flashErase(int *startAddr, int *endAddr) { + int startPage = ((int) startAddr - STM32_FLASH_START) / STM32_PAGE_SIZE; + int pageCount = ((endAddr - startAddr) * 4) / STM32_PAGE_SIZE; + FLASH_EraseInitTypeDef eraseParams = {FLASH_TYPEERASE_PAGES, startPage, pageCount}; + uint32_t err = 0; + + HAL_FLASH_Unlock(); + HAL_FLASHEx_Erase(&eraseParams, &err); + HAL_FLASH_Lock(); +} + +static void flashWriteTwoWords(int *addr, uint32_t word1, uint32_t word2) { + // STM32 only supports writing 64-bit double words. Address must be double word aligned. + + uint32_t err = 0; + uint32_t words[2] = {word1, word2}; + HAL_FLASH_Unlock(); + err = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, (uint32_t) addr, *((uint64_t *) &words[0])); + HAL_FLASH_Lock(); +} + +static void flashWriteData(int *dst, int wordCount, uint8 *src) { + // Write the given number of 32-bit words of data from src to flash starting at dst. + // If wordCount is odd, pad the final 64-bit write with a zero word. + // Note: The STM32 flash system can only write 64-bit double words, double-word aligned. + + uint32_t dstAddr = (uint32_t) dst; + uint8 buf[8]; + + HAL_FLASH_Unlock(); + for (int i = 0; i < wordCount / 2; i++) { + memcpy(buf, src + (8 * i), sizeof(buf)); + int err = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, dstAddr, *((uint64_t *) &buf)); + dstAddr += 8; + } + if (wordCount & 1) { // wordCount is odd + // write the final 64-bit double word padded with a zero word + memset(buf, 0, sizeof(buf)); + memcpy(buf, src + (4 * (wordCount - 1)), 4); + int err = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, dstAddr, *((uint64_t *) &buf)); + dstAddr += 8; + } + HAL_FLASH_Lock(); +} + +#else + // Simulate Flash operations using a RAM code store; allows MicroBlocks to run in RAM + // on platforms that do not support Flash-based persistent memory. On systems with + // a file system, the RAM code store is stored in a file to provide persistence. + + #define RAM_CODE_STORE true + + #if defined(ESP8266) + #define USE_CODE_FILE true + #define HALF_SPACE (18 * 1024) // ESP8266 is unreliable at 24 + #elif defined(ESP32_S3) || defined(ESP32_C3) + #define USE_CODE_FILE true + #define HALF_SPACE (60 * 1024) + #elif defined(ARDUINO_ARCH_ESP32) || defined(GNUBLOCKS) + #define USE_CODE_FILE 1 + #define HALF_SPACE (8 * 1024) + #elif defined(ARDUINO_ARCH_RP2040) + #define USE_CODE_FILE RP2040_PHILHOWER + #define HALF_SPACE (40 * 1024) + #else + #define HALF_SPACE (40 * 1024) + #endif + + #define START (&flash[0]) + static uint8 flash[HALF_SPACE] __attribute__ ((aligned (32))); // simulated Flash memory + + static void flashErase(int *startAddr, int *endAddr) { + int *dst = (int *) startAddr; + while (dst < endAddr) { *dst++ = -1; } + } + + static void flashWriteWord(int *addr, int value) { + *addr = value; + } + + static void flashWriteData(int *dst, int wordCount, uint8 *src) { + for ( ; wordCount > 0; wordCount--) { + int n = *src++; + n |= *src++ << 8; + n |= *src++ << 16; + n |= *src++ << 24; + *dst++ = n; + } + } + +#endif + +// variables + +// persistent memory half-space ranges: +static int *start0 = (int *) START; +static int *end0 = (int *) (START + HALF_SPACE); +static int *start1 = (int *) (START + HALF_SPACE); +static int *end1 = (int *) (START + (2 * HALF_SPACE));; + +static int current; // current half-space (0 or 1) +static int *freeStart; // first free word + +#ifdef USE_CODE_FILE + static int suspendFileUpdates = false; // suspend slow file updates when loading a project/library +#endif + +// helper functions + +#if defined(ESP32_FLASH_CODESTORE) + +static void initESP32Flash() { + esp_partition_iterator_t partitionIterator = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, "mbcode"); + if (!partitionIterator) vmPanic("'mbcode' partition not found in initESP32Flash()"); + + const esp_partition_t* partition = esp_partition_get(partitionIterator); + flashBaseAddr = partition->address; + + // Map the partition to data memory + spi_flash_mmap_handle_t map_handle; // not used + int err = esp_partition_mmap(partition, 0, partition->size, SPI_FLASH_MMAP_DATA, (const void**) &ramBaseAddr, &map_handle); + if (err) vmPanic("mmap failure in initESP32Flash()"); + + start0 = (int *) ramBaseAddr; + end0 = (int *) (ramBaseAddr + HALF_SPACE); + start1 = (int *) (ramBaseAddr + HALF_SPACE); + end1 = (int *) (ramBaseAddr + (2 * HALF_SPACE)); +} + +#endif + +static void clearHalfSpace(int halfSpace) { + int *startAddr = (0 == halfSpace) ? start0 : start1; + int *endAddr = (0 == halfSpace) ? end0 : end1; + flashErase(startAddr, endAddr); +} + +static int cycleCount(int halfSpace) { + // Return the cycle count for the given half-space or zero if not initialized. + // Details: Each half-space begins with a word of the form <'S'>. + // The cycle count is incremented each time persistent memory is compacted. + + int *p = (0 == halfSpace) ? start0 : start1; + return ('S' == ((*p >> 24) & 0xFF)) ? (*p & 0xFFFFFF) : 0; +} + +static void setCycleCount(int halfSpace, int cycleCount) { + // Store the given cycle count at the given address. + + int *p = (0 == halfSpace) ? start0 : start1; + #if defined(DUELink) + flashWriteTwoWords(p, ('S' << 24) | (cycleCount & 0xFFFFFF), 0); + #else + flashWriteWord(p, ('S' << 24) | (cycleCount & 0xFFFFFF)); + #endif +} + +static void initPersistentMemory() { + // Figure out which is the current half-space and find freeStart. + // If neither half-space has a valid cycle counter, initialize persistent memory. + + #ifdef RAM_CODE_STORE + // Use a single persistent memory; HALF_SPACE is the total amount of RAM to use + // Make starts and ends the same to allow the same code to work for either RAM or Flash + start0 = start1 = (int *) START; + end0 = end1 = (int *) (START + HALF_SPACE); + #elif defined(ESP32_FLASH_CODESTORE) + // init ESP32 for direct Flash codestore + initESP32Flash(); + #endif + + int c0 = cycleCount(0); + int c1 = cycleCount(1); + + if (!c0 && !c1) { // neither half-space has a valid counter + // Flash hasn't been used for uBlocks yet; erase it all. + flashErase(start0, end1); + setCycleCount(0, 1); + current = 0; + freeStart = start0 + CYCLE_COUNT_WORDS; + return; + } + + int *end; + if (c0 > c1) { + current = 0; + freeStart = start0 + CYCLE_COUNT_WORDS; + end = end0; + } else { + current = 1; + freeStart = start1 + CYCLE_COUNT_WORDS; + end = end1; + } + + while ((freeStart < end) && (-1 != *freeStart)) { + int header = *freeStart; + if ('R' != ((header >> 24) & 0xFF)) { + outputString("Bad record found during initialization"); + clearPersistentMemory(); + return; + } + int wordCount = *(freeStart + 1); // size word of header + freeStart += wordCount + 2; // increment by the record length plus 2-word header + #if defined(DUELink) + if (wordCount & 1) freeStart++; // wordCount is odd; round up to double-word boundary + #endif + } + if (freeStart >= end) freeStart = end; +} + +int * recordAfter(int *lastRecord) { + // Return a pointer to the record following the given record, or NULL if there are + // no more records. Pass NULL to get the first record. + + int *start, *end; + if (0 == current) { + start = start0; + end = end0; + } else { + start = start1; + end = end1; + } + int *p = lastRecord; + if (NULL == lastRecord) { // return the first record + p = start + CYCLE_COUNT_WORDS; + return ('R' == ((*p >> 24) & 0xFF)) ? p : NULL; + } + if ((p >= end) || ('R' != ((*p >> 24) & 0xFF))) return NULL; // should not happen + int wordCount = *(p + 1); + p += wordCount + 2; // increment by wordCount plus 2-word header + #if defined(DUELink) + if (wordCount & 1) p++; // wordCount is odd; round up to double-word boundary + #endif + if ((p >= end) || 'R' != ((*p >> 24) & 0xFF)) return NULL; // bad header; probably start of free space + return p; +} + +void outputRecordHeaders() { + // For debugging. Output all the record headers of the current half-space. + + int recordCount = 0; + int wordCount = 0; + int maxID = 0; + + char s[200]; + int *p = recordAfter(NULL); + while (p) { + recordCount++; + wordCount += 2 + *(p + 1); + int id = (*p >> 8) & 0xFF; + if (id > maxID) maxID = id; + sprintf(s, "%d %d %d (%d words)", + (*p >> 16) & 0xFF, (*p >> 8) & 0xFF, *p & 0xFF, *(p + 1)); + outputString(s); + +// xxx debug: dump contents +// int chunkWords = *(p + 1); +// sprintf(s, "\t%x (%d words)", *p, chunkWords); +// outputString(s); +// int *w = p + 2; +// for (int i = 0; i < chunkWords; i++) { +// int word = *w; +// sprintf(s, "\t\t%d: %d %d %d %d ", i, +// (word & 255), ((word >> 8) & 255), ((word >> 16) & 255), ((word >> 24) & 255)); +// outputString(s); +// w++; +// } + + p = recordAfter(p); + } + sprintf(s, "%d records, %d words, maxID %d, compaction cycles %d", + recordCount, wordCount, maxID, cycleCount(current)); + outputString(s); + + int bytesUsed = 4 * (freeStart - ((0 == current) ? start0 : start1)); + sprintf(s, "%d bytes used (%d%%) of %d", + bytesUsed, (100 * bytesUsed) / HALF_SPACE, HALF_SPACE); + outputString(s); +} + +void eraseCheck() { + int badCount = 0; + int *start = (0 == current) ? start1 : start0; // inactive half space + int *end = (0 == current) ? end1 : end0; + for (int *p = start; p < end; p++) { + if (*p != 0xFFFFFFFF) badCount++; + if (*p != 0xFFFFFFFF) { + char s[200]; + sprintf(s, "bad %d: %x", (p - start), *p); + outputString(s); + } + } + reportNum("Non-erased words:", badCount); +} + +void dumpHex() { + int *start = (0 == current) ? start0 : start1; + int *end = freeStart + 5000; + for (int *p = start; p <= end; ) { + char s[200]; + sprintf(s, "%d: %x %x %x %x %x %x %x %x %x %x", + (p - start), p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8], p[9]); + outputString(s); + p += 10; + } +} + +static int * compactionStartRecord() { + // Return a pointer to the first record at which to start compaction or script restoration + // at startup. + + int *ptr = recordAfter(NULL); + int *result = ptr; // first record in half-space; default if no 'deleteAll' records found + while (ptr) { + int type = (*ptr >> 16) & 0xFF; + ptr = recordAfter(ptr); + if (deleteAll == type) result = ptr; + } + return result; +} + +static int * copyChunk(int *dst, int *src) { + // Copy the chunk record at src to dst and return the new value of dst. + + int wordCount = *(src + 1) + 2; + flashWriteData(dst, wordCount, (uint8 *) src); + #if defined(DUELink) + if (wordCount & 1) wordCount++; // wordCount is odd; round up to double-word boundary + #endif + return dst + wordCount; +} + +static void updateChunkTable() { + memset(chunks, 0, sizeof(chunks)); // clear chunk table + + int *p = compactionStartRecord(); + while (p) { + int recType = (*p >> 16) & 0xFF; + if (chunkCode == recType) { + int chunkIndex = (*p >> 8) & 0xFF; + if (chunkIndex < MAX_CHUNKS) { + chunks[chunkIndex].chunkType = *p & 0xFF; + chunks[chunkIndex].code = p; + } + } + if (chunkDeleted == recType) { + int chunkIndex = (*p >> 8) & 0xFF; + if (chunkIndex < MAX_CHUNKS) { + chunks[chunkIndex].chunkType = unusedChunk; + chunks[chunkIndex].code = NULL; + } + } + p = recordAfter(p); + } + + // update code pointers for tasks + for (int i = 0; i < MAX_TASKS; i++) { + if (tasks[i].status) { // task entry is in use + tasks[i].code = chunks[tasks[i].taskChunkIndex].code; + } + } +} + +// Flash Compaction + +#ifndef RAM_CODE_STORE + +static char chunkProcessed[256]; +static char varProcessed[256]; + +static int * copyChunkInfo(int id, int *src, int *dst) { + // Copy the most recent data about the chunk with the given ID to dst and return + // the new dst pointer. If the given chunk id has been processed do nothing. + + if (chunkProcessed[id]) return dst; + + int *chunkSrc = 0; + + // scan rest of the records to get the most recent info about this chunk + while (src) { + if (id == ((*src >> 8) & 0xFF)) { // id field matches + int type = (*src >> 16) & 0xFF; + switch (type) { + case chunkCode: + chunkSrc = src; + break; + case chunkDeleted: + chunkSrc = 0; + break; + } + } + src = recordAfter(src); + } + if (chunkSrc) { + dst = copyChunk(dst, chunkSrc); + } + chunkProcessed[id] = true; + return dst; +} + +static int * copyVarInfo(int id, int *src, int *dst) { + if (varProcessed[id]) return dst; + + // record info from first reference to this variable + int type = (*src >> 16) & 0xFF; + int *nameRec = (varName == type) ? src : NULL; + + // scan rest of the records to get the most recent info about this variable + while (src) { + int type = (*src >> 16) & 0xFF; + switch (type) { + case varName: + if (id == ((*src >> 8) & 0xFF)) nameRec = src; + break; + case varsClearAll: + nameRec = NULL; + break; + } + src = recordAfter(src); + } + if (nameRec) dst = copyChunk(dst, nameRec); + varProcessed[id] = true; + return dst; +} + +static void compactFlash() { + // Copy only the most recent chunk and variable records to the other half-space. + // Details: + // 1. erase the other half-space + // 2. clear the chunk and variable processed flags + // 3. find the start point for the scan (half space start or after latest 'deleteAll' record) + // 4. for each chunk and variable record in the current half-space + // a. if the chunk or variable has been processed, skip it + // b. gather the most recent information about that chunk or variable + // c. copy that information into the other half-space + // d. mark the chunk or variable as processed + // 5. switch to the other half-space + // 6. remember the free pointer for the new half-space + + uint32_t startT = millisecs(); + + // clear the processed flags + memset(chunkProcessed, 0, sizeof(chunkProcessed)); + memset(varProcessed, 0, sizeof(varProcessed)); + + // clear the destination half-space and init dst pointer + clearHalfSpace(!current); + int *dst = ((0 == !current) ? start0 : start1) + CYCLE_COUNT_WORDS; + + int *src = compactionStartRecord(); + while (src) { + captureIncomingBytes(); + int header = *src; + int type = (header >> 16) & 0xFF; + int id = (header >> 8) & 0xFF; + if ((chunkCode <= type) && (type <= chunkDeleted)) { + dst = copyChunkInfo(id, src, dst); + } else if ((varName <= type) && (type <= varsClearAll)) { + dst = copyVarInfo(id, src, dst); + } + src = recordAfter(src); + } + + // increment the cycle counter and switch to the other half-space + setCycleCount(!current, cycleCount(current) + 1); // this commits the compaction + current = !current; + freeStart = dst; + + updateChunkTable(); + + #if defined(NRF51) || defined(ARDUINO_BBC_MICROBIT_V2) || defined(CALLIOPE_V3) + // Compaction messes up the serial port on the micro:bit v1 and v2 and Calliope + restartSerial(); + #endif + + char s[100]; + int bytesUsed = 4 * (freeStart - ((0 == current) ? start0 : start1)); + sprintf(s, "Compacted Flash code store (%lu msecs)\n%d bytes used (%d%%) of %d", + millisecs() - startT, + bytesUsed, (100 * bytesUsed) / HALF_SPACE, HALF_SPACE); + outputString(s); +} + +#endif // compactFlash + +// RAM compaction + +#ifdef RAM_CODE_STORE + +static int keepCodeChunk(int id, int header, int *start) { + // Return true if this code chunk should be kept when compacting RAM. + + if (unusedChunk == chunks[id].chunkType) return false; // code chunk was deleted + + int *rec = start; + while (rec) { + if (*rec == header) return false; // superceded + rec = recordAfter(rec); + } + return true; +} + +static void compactRAM(int printStats) { + // Compact a RAM-based code store in place. In-place compaction is possible in RAM since, + // unlike Flash memory, RAM can be re-written without first erasing it. This approach + // allows twice as much space for code as the half-space design. + // + // In-place compaction differs from half-space compaction because the destination of the + // copying operations cannot overlap with the unprocessed portion of the code store. This + // constraint is ensured by maintaining record order and "sliding down" all the surviving + // records to so that all unused space is left at the end of the code store. where it is + // available for storing new records. + // + // Details: + // 1. find the start point for the scan (half space start or after latest 'deleteAll' record) + // 2. find the most recent "varsClearAll" record + // 3. for each chunk and variable record in the current half-space + // a. deterimine if the record should be kept or skipped + // b. if kept, copy the record down to the destination pointer + // 4. update the free pointer + // 5. clear the rest of the code store + // 6. update the compaction count + // 7. re-write the code file + + uint32 startT = millisecs(); + + int *dst = ((0 == !current) ? start0 : start1) + 1; + int *src = compactionStartRecord(); + + if (!src) { // nothing to compact + if (printStats) outputString("RAM code store is empty"); + return; + } + + // find the most recent varsClearAll record + int *varsStart = src; + int *rec = src; + while (rec) { + if (varsClearAll == ((*rec >> 16) & 0xFF)) varsStart = rec; + rec = recordAfter(rec); + } + + while (src) { + int *next = recordAfter(src); + int header = *src; + int type = (header >> 16) & 0xFF; + int id = (header >> 8) & 0xFF; + if ((type == chunkCode) && keepCodeChunk(id, header, next)) { + dst = copyChunk(dst, src); + } else if ((varName == type) && (src >= varsStart)) { + dst = copyChunk(dst, src); + } else { + } + src = next; + } + + freeStart = dst; + memset(freeStart, 0, (4 * (end0 - freeStart))); // clear everything following freeStart + + updateChunkTable(); + + // re-write the code file + #if USE_CODE_FILE + setCycleCount(current, cycleCount(current) + 1); + clearCodeFile(cycleCount(current)); + int *codeStart = ((0 == current) ? start0 : start1) + 1; // skip half-space header + writeCodeFile((uint8 *) codeStart, 4 * (freeStart - codeStart)); + #endif + + if (printStats) { + char s[100]; + int bytesUsed = 4 * (freeStart - ((0 == current) ? start0 : start1)); + + sprintf(s, "Compacted RAM code store (%lu msecs)\n%d bytes used (%d%%) of %d", + millisecs() - startT, + bytesUsed, (100 * bytesUsed) / HALF_SPACE, HALF_SPACE); + outputString(s); + } +} +#endif + +#ifdef EMSCRIPTEN +int *ramStart() { return start0; } +int ramSize() { return 4 * (freeStart - start0); } +#endif + + +// entry points + +void clearPersistentMemory() { + int c0 = cycleCount(0); + int c1 = cycleCount(1); + int count = (c0 > c1) ? c0 : c1; + current = !current; + clearHalfSpace(current); + freeStart = ((0 == current) ? start0 : start1) + CYCLE_COUNT_WORDS; + setCycleCount(current, count + 1); +} + +int * appendPersistentRecord(int recordType, int id, int extra, int byteCount, uint8 *data) { + // Append the given record at the end of the current half-space and return its address. + // Header word: (8-bits each) + // Perform a compaction if necessary. + int wordCount = (byteCount + 3) / 4; + int needed = wordCount + 2 + 4; // add 4 extra words for DUELink rounding + int *end = (0 == current) ? end0 : end1; + if ((freeStart + needed) > end) { + compactCodeStore(NULL, NULL); + end = (0 == current) ? end0 : end1; + if ((freeStart + needed) > end) { + sendCodeStoreFull(); + return NULL; + } + } + // write the record + int header = ('R' << 24) | ((recordType & 0xFF) << 16) | ((id & 0xFF) << 8) | (extra & 0xFF); + +// xxx debug: dump contents +// char s[500]; +// sprintf(s, "Appending type %d id %d (%d words)", recordType, id, wordCount); +// outputString(s); +// char *p = data; +// for (int i = 0; i < wordCount; i++) { +// sprintf(s, "\t%d: %d %d %d %d ", i, *p, *(p + 1), *(p + 2), *(p + 3)); +// outputString(s); +// p += 4; +// } + + #if USE_CODE_FILE + if (!suspendFileUpdates) { + writeCodeFileWord(header); + writeCodeFileWord(wordCount); + writeCodeFile(data, 4 * wordCount); + } + #endif + + int *result = freeStart; + #if defined(DUELink) + flashWriteTwoWords(freeStart, header, wordCount); + freeStart += 2; + #else + flashWriteWord(freeStart++, header); + flashWriteWord(freeStart++, wordCount); + #endif + if (wordCount) flashWriteData(freeStart, wordCount, data); + #if defined(DUELink) + if (wordCount & 1) wordCount++; // wordCount is odd; round up to double-word boundary + #endif + freeStart += wordCount; + return result; +} + +void compactCodeStore(int *codeStoreUsed, int *codeStoreTotal) { + // Compact the code store. If arguments are not NULL, use them to report the code stats. + + #ifdef RAM_CODE_STORE + compactRAM(true); + #else + compactFlash(); + #endif + if (codeStoreUsed && codeStoreTotal) { // report code store stats + *codeStoreUsed = 4 * (freeStart - ((0 == current) ? start0 : start1)); + *codeStoreTotal = HALF_SPACE; + } +} + +void restoreScripts() { + initPersistentMemory(); + + #if USE_CODE_FILE + int codeFileBytes = initCodeFile(flash, HALF_SPACE); + int *start = current ? start1 : start0; + freeStart = start + (codeFileBytes / 4); + #elif defined(ARDUINO_ARCH_ESP32) + initFileSystem(); + #endif + + updateChunkTable(); + + // Give feedback: + int chunkCount = 0; + for (int i = 0; i < MAX_CHUNKS; i++) { + if (chunks[i].code) chunkCount++; + } + char s[100]; + sprintf(s, "Restored %d scripts", chunkCount); + outputString(s); + outputString("Started"); +} + +int *scanStart() { + // Return a pointer to the first record at which to start scanning the current code. + + int *ptr = recordAfter(NULL); + int *result = ptr; // default if no 'deleteAll' records found + while (ptr) { + int type = (*ptr >> 16) & 0xFF; + ptr = recordAfter(ptr); + if (deleteAll == type) result = ptr; + } + return result; +} + +void suspendCodeFileUpdates() { + #ifdef USE_CODE_FILE + suspendFileUpdates = true; + #endif +} + +void resumeCodeFileUpdates() { + #ifdef USE_CODE_FILE + if (suspendFileUpdates) { + compactRAM(false); // also updates code file + } + suspendFileUpdates = false; + #endif +} + +// testing + +static void dumpWords(int halfSpace, int count) { + // Dump the first count words of the given half-space. + + char s[100]; + int *p = (current == 0) ? start0 : start1; + for (int i = 0; i < count; i++) { + sprintf(s, "%d %d %d %d", + (*p >> 24) & 0xFF, + (*p >> 16) & 0xFF, + (*p >> 8) & 0xFF, + *p & 0xFF); + outputString(s); + p++; + } +} + +static void showRecordHeaders() { + // Dump the record headers of the current half-space. + + char s[100]; + int *p = recordAfter(NULL); + while (p) { + sprintf(s, "Record at offset %d: %d %d %d %d (%d words)", + (p - start0), + (*p >> 24) & 0xFF, (*p >> 16) & 0xFF, (*p >> 8) & 0xFF, *p & 0xFF, *(p + 1)); + outputString(s); + p = recordAfter(p); + } +} + +void basicTest() { + int testData[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; + uint8 charData[] = { + 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, + 5, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 9, 0, 0, 0}; + + #define PAGE ((int *) START) + + flashErase(PAGE, PAGE + 100); + dumpWords(0, 35); + outputString("-----"); +#if !defined(DUELink) + flashWriteData(PAGE, 10, (uint8 *) testData); + flashWriteWord(PAGE + 13, 13); + flashWriteWord(PAGE + 15, 42); + flashWriteWord(PAGE + 17, 17); + flashWriteData(PAGE + 19, 3, charData); + flashWriteData(PAGE + 23, 3, &charData[1]); + flashWriteData(PAGE + 27, 3, &charData[2]); +#endif + dumpWords(0, 35); + flashErase(PAGE, PAGE + 100); + dumpWords(0, 20); +} + +void persistTest() { + int dummyData[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; + + outputString("Persistent Memory Test\n"); + basicTest(); + + outputString("\nInitializing Memory"); + initPersistentMemory(); + initPersistentMemory(); + clearPersistentMemory(); + outputString("Memory intitialized; writing records..."); + + for (int i = 0; i < 3000; i++) { + appendPersistentRecord(chunkCode, i % 100, 0, (i % 5) * 4, (uint8 *) dummyData); + } + compactCodeStore(NULL, NULL); + + dumpWords(current, 150); + showRecordHeaders(); + + char s[100]; + sprintf(s, "Final: current %d used %d c0 %d c1 %d", + current, + freeStart - ((0 == current) ? start0 : start1), + cycleCount(0), cycleCount(1)); + outputString(s); +} diff --git a/interpreters/smallvm/persist.h b/interpreters/smallvm/persist.h new file mode 100644 index 000000000..13af44fd9 --- /dev/null +++ b/interpreters/smallvm/persist.h @@ -0,0 +1,66 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Copyright 2018 John Maloney, Bernat Romagosa, and Jens Mönig + +// persist.h - Persistent memory +// John Maloney, December 2017 + +#ifdef __cplusplus +extern "C" { +#endif + +// Persistent Memory Records + +// Records in persistent memory start with two header words. They have the form: +// <'R'> (8-bits for each field) +// word count (32-bits) +// ... word count data words ... +// +// Not all record types use the header field. + +#define PERSISTENT_HEADER_WORDS 2 + +typedef enum { + chunkCode32bit = 10, // deprecated + chunkAttribute = 11, // deprecated + chunkCode = 12, // 16-bit code chunk + chunkDeleted = 19, + varName = 21, + varsClearAll = 29, + deleteAll = 218, // 218 in hex is 0xDA, short for "delete all" +} RecordType_t; + +// Persistent Memory Operations + +int * appendPersistentRecord(int recordType, int id, int extra, int byteCount, uint8 *data); +void clearPersistentMemory(); +int * recordAfter(int *lastRecord); +void restoreScripts(); +int *scanStart(); +void compactCodeStore(int *codeStoreUsed, int *codeStoreTotal); +void outputRecordHeaders(); + +#ifdef EMSCRIPTEN +int *ramStart(); +int ramSize(); +#endif + +// File-Based Persistent Memory Operations + +int initCodeFile(uint8 *flash, int flashByteCount); +void initFileSystem(); +void writeCodeFile(uint8 *code, int byteCount); +void writeCodeFileWord(int word); +void clearCodeFile(int cycleCount); + +// File operations for storing system state + +void createFile(const char *fileName); +void deleteFile(const char *fileName); +int fileExists(const char *fileName); + +#ifdef __cplusplus +} +#endif diff --git a/interpreters/smallvm/pty2tcp/buildVMLinux-stripped-tcp.sh b/interpreters/smallvm/pty2tcp/buildVMLinux-stripped-tcp.sh new file mode 100755 index 000000000..b01b36c76 --- /dev/null +++ b/interpreters/smallvm/pty2tcp/buildVMLinux-stripped-tcp.sh @@ -0,0 +1,6 @@ +#!/bin/sh +# Build TCP to pseudo-terminal communication with IDE MicroBlocks + +gcc -std=c99 -Wall -O3 \ + pty2tcp.c \ + -o pty2tcp diff --git a/interpreters/smallvm/pty2tcp/pty2tcp.c b/interpreters/smallvm/pty2tcp/pty2tcp.c new file mode 100644 index 000000000..4994c864d --- /dev/null +++ b/interpreters/smallvm/pty2tcp/pty2tcp.c @@ -0,0 +1,180 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Copyright 2018 John Maloney, Bernat Romagosa, and Jens Mönig + +// Code for transferring data between IDE MicroBlocks (using +// pseudo-terminal) and smallVM (connected via TCP socket) + +#define _XOPEN_SOURCE 600 +#define _DEFAULT_SOURCE +#define _GNU_SOURCE + +#include +#include +#include // still needed? +#include +#include +#include // still needed? +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +static int pty; +static int tcp_socket; + +static void makePtyFile() { + FILE *file = fopen("/tmp/ublocksptyname", "w"); + if (file) { + fprintf(file, "%s", (char*) ptsname(pty)); + fclose(file); + } +} + +static void exitGracefully() { + remove("/tmp/ublocksptyname"); + close(tcp_socket); + close(pty); + exit(0); +} + +static void openPseudoTerminal() { + pty = posix_openpt(O_RDWR | O_NOCTTY | O_NONBLOCK); + if (-1 == pty) { + perror("Error opening pseudo terminal\n"); + exit(-1); + } + + struct termios settings; + tcgetattr(pty, &settings); + cfmakeraw(&settings); + tcsetattr(pty, TCSANOW, &settings); + + grantpt(pty); + unlockpt(pty); + + makePtyFile(); +} + +void transferData() { + const int nfds = 2; + char buf[2][1024]; + int recv[2] = {0,0}; + int sent[2] = {0,0}; + struct pollfd fds[2]; + struct timespec timeout; + timeout.tv_sec = 30; + timeout.tv_nsec = 0; + + memset(fds, 0, sizeof(fds)); + for (int i = 0; i < 2; i++) { + fds[i].fd = (i == 0) ? pty : tcp_socket; + fds[i].events = POLLIN; + } + + int ret = ppoll(fds, nfds, &timeout, NULL); + if (ret > 0) { + for (int i = 0; i < 2; i++) { + if (fds[i].revents & POLLIN) { + int readd = read(fds[i].fd, buf[i], sizeof(buf[0])); + if (readd < 0) { + if (errno == EPIPE) { + exit(EXIT_FAILURE); + } + perror("read()"); + } else if (readd) { + recv[i] = readd; + printf("%d = read(%d, %p, %ld)\n", readd, fds[i].fd, buf[i], sizeof(buf[0])); + } + } else if ((fds[i].revents & POLLHUP) || (fds[i].revents & POLLERR)) { + printf("Terminating connection\n"); + exit(EXIT_FAILURE); + } + } + } + + while (recv[0] > 0 || recv[1] > 0) { + timeout.tv_sec = 30; + timeout.tv_nsec = 0; + memset(fds, 0, sizeof(fds)); + for (int i = 0; i < 2; i++) { + if (recv[i] > 0) { + fds[i].fd = (i == 0) ? tcp_socket : pty; + } else { + fds[i].fd = -1; + } + fds[i].events = POLLOUT; + } + + ret = ppoll(fds, nfds, &timeout, NULL); + if (ret > 0) { + for (int i = 0; i < 2; i++) { + if (fds[i].revents & POLLOUT) { + int written = write(fds[i].fd, &buf[i][sent[i]], recv[i]); + printf("%d = write(%d, %p, %d)\n\n", written, fds[i].fd, &buf[i][sent[i]], recv[i]); + if (written < 0) { + if (errno == EPIPE) { + exit(EXIT_FAILURE); + } + written = 0; + perror("written(): "); + } + sent[i] += written; + recv[i] -= written; + } else if ((fds[i].revents & POLLHUP) || (fds[i].revents & POLLERR)) { + printf("Terminating connection\n"); + exit(EXIT_FAILURE); + } + } + } + } +} + +void dataLoop() { + printf("TCP socket: %d\r\nPTY fd: %d\r\n", tcp_socket, pty); + while(1) { + transferData(); + } +} + +void setupTcpConnection(void) { + tcp_socket = socket(AF_INET, SOCK_STREAM, 0); +// const int bool_true = 1; + + struct sockaddr_in saddr; + memset(&saddr, 0, sizeof(struct sockaddr_in)); + saddr.sin_family = AF_INET; + saddr.sin_addr.s_addr = inet_addr("10.0.0.1"); + saddr.sin_port = htons(9876); + + int ret = connect(tcp_socket, &saddr, sizeof(saddr)); + if (ret < 0) { + perror(NULL); + exit(-1); + } +// ret = setsockopt(tcp_socket, SOL_SOCKET, SO_KEEPALIVE, &bool_true, sizeof(bool_true)); +// ret = setsockopt(tcp_socket, IPPROTO_TCP, TCP_NODELAY, &bool_true, sizeof(bool_true)); +// int flags = fcntl(tcp_socket, F_GETFL, 0); +// ret = fcntl(tcp_socket, F_SETFL, flags | O_NONBLOCK); +} + +int main(void) { + signal(SIGINT, exit); + signal(SIGPIPE, SIG_IGN); + atexit(exitGracefully); + setupTcpConnection(); + openPseudoTerminal(); + dataLoop(); + + return 0; +} diff --git a/interpreters/smallvm/runtime.c b/interpreters/smallvm/runtime.c new file mode 100644 index 000000000..3763a433e --- /dev/null +++ b/interpreters/smallvm/runtime.c @@ -0,0 +1,1388 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Copyright 2018 John Maloney, Bernat Romagosa, and Jens Mönig + +// runtime.c - Runtime for uBlocks, including code chunk storage and task management +// John Maloney, April 2017 + +#include +#include +#include +#include +#include + +#if defined(GNUBLOCKS) && !defined(EMSCRIPTEN) && !defined(NUTTX) +#include "../linux+pi/linux.h" +#endif + +#if defined(NUTTX) +#include "nuttx.h" +#endif + +#include "mem.h" +#include "interp.h" +#include "persist.h" +#include "version.h" + +// Forward Reference Declarations + +#if !defined(GNUBLOCKS) || defined(EMSCRIPTEN) +void delay(unsigned long); // Arduino delay function +#endif + +static void softReset(int clearMemoryFlag); +static void sendMessage(int msgType, int chunkIndex, int dataSize, char *data); +static void sendChunkCRC(int chunkID); +static void sendData(); + +// debugging + +#ifdef DEBUG_BEEP + +static void debugBeep(int count) { + // Useful for audio debugging communication issues. + + const int speakerPin = 27; + pinMode(speakerPin, 1); // output pin + for (int i = 0; i < 10; i++) { + digitalWrite(speakerPin, true); + delay(count); + digitalWrite(speakerPin, false); + delay(count); + } + delay(20); +} + +#endif + +// DUELink support + +#if defined(DUELink) + +#include +#include +#include // for CDC_deInit() + +__RAM_FUNC __NOINLINE static void dueLinkEraseFlashAndReset() { + // Danger! This function erases all of Flash memory then reboots the board in DFU mode. + + CDC_deInit(); // stop USB serial + + // disable interrupts + __disable_irq(); + + while (FLASH->SR & FLASH_SR_BSY1_Msk); // wait for any previous operation to complete + + if (__HAL_FLASH_GET_FLAG(FLASH_FLAG_CFGBSY) != 0) { + __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS); + } + + // mass erase all of Flash + HAL_FLASH_Unlock(); + FLASH->CR |= (FLASH_CR_STRT | FLASH_CR_MER1); + while (FLASH->SR & FLASH_SR_BSY1_Msk); // wait for erase to complete + + // NOTE: Flash has been erased! Do not call any library functions after this point! + + // set the Flash empty flag + SET_BIT(FLASH->ACR, 1 << 16); + + // reset + SCB->AIRCR = ( + (0x5FA << SCB_AIRCR_VECTKEY_Pos) | // unlock key + (1 << SCB_AIRCR_SYSRESETREQ_Pos)); // reset request + + // wait for reset + while (1); + // This function never returns because the board resets. +} + +#else + +static void dueLinkEraseFlashAndReset() { } // noop on non-DUELink boards + +#endif + +// Named Primitive Support + +typedef struct { + const char *setName; + int entryCount; + PrimEntry *entries; +} PrimitiveSet; + +PrimitiveSet primSets[PrimitiveSetCount]; + +void addPrimitiveSet(PrimitiveSetIndex primSetIndex, const char *setName, int entryCount, PrimEntry *entries) { + primSets[primSetIndex].setName = setName; + primSets[primSetIndex].entryCount = entryCount; + primSets[primSetIndex].entries = entries; +} + +PrimitiveFunction findPrimitive(char *primName) { + // Return the address of the named primitive with the given name or NULL if not found. + // The primitive name is a string of the form: [primSet:primName]. + + int len = strlen(primName); + if (len < 2) return NULL; + if (('[' != primName[0]) || (']' != primName[len - 1])) return NULL; + char *colon = strchr(primName + 1, ':'); + if (!colon) return NULL; + + char setName[100]; + char opName[100]; + + // extract primitive set name + int count = colon - (primName + 1); + if (count < 1) return NULL; + strncpy(setName, primName + 1, count); + setName[count] = 0; + + // extract primitive name + count = (primName + len - 1) - (colon + 1); + if (count < 1) return NULL; + strncpy(opName, colon + 1, count); + opName[count] = 0; + + for (int i = 0; i < PrimitiveSetCount; i++) { + if ((primSets[i].setName != NULL) && (0 == strcmp(primSets[i].setName, setName))) { + PrimEntry *entries = primSets[i].entries; + if (!entries) continue; // uninitialized primitive set + int entryCount = primSets[i].entryCount; + for (int j = 0; j < entryCount; j++) { + if (0 == strcmp(entries[j].primName, opName)) { + return entries[j].primFunc; + } + } + } + } + return NULL; +} + +OBJ doPrimitiveCall(PrimitiveSetIndex setIndex, const char *primName, int argCount, OBJ *args) { + // Call a named primitive with the given primitive set index and name. + + if ((setIndex < 0) || (setIndex >= PrimitiveSetCount)) { + return fail(primitiveNotImplemented); + } + + PrimEntry *entries = primSets[setIndex].entries; + int entryCount = primSets[setIndex].entryCount; + for (int i = 0; i < entryCount; i++) { + if ((entries[i].primName != NULL) && (0 == strcmp(entries[i].primName, primName))) { + OBJ result = (entries[i].primFunc)(argCount, args); // call the primitive + tempGCRoot = NULL; // clear tempGCRoot in case it was used + return result; + } + } + char s[200]; + snprintf(s, sizeof(s), "Unknown primitive %d:%s", setIndex, primName); + outputString(s); + return fail(primitiveNotImplemented); +} + +void primsInit() { + // Called at startup to call functions to add named primitive sets. + + memset(primSets, 0, sizeof(primSets)); + +#if defined(DUELink) + addDataPrims(); // ~5600 bytes + addDisplayPrims(); // ~1500 bytes +// addFilePrims(); + addIOPrims(); // ~2900 bytes + addMiscPrims(); // ~6000 bytes (could be reduced?) +// addNetPrims(); +// addBLEPrims(); +// addRadioPrims(); + addSensorPrims(); // ~3000 bytes + addSerialPrims(); // ~3500 bytes +// addTFTPrims(); + addVarPrims(); // ~300 bytes +// addHIDPrims(); +// addOneWirePrims(); // ~200 bytes +// addCameraPrims(); + addEncoderPrims(); // ~1650 bytes +// addSDCardPrims(); +#elif defined(GNUBLOCKS) && !defined(EMSCRIPTEN) && !defined(NUTTX) + addDataPrims(); + addDisplayPrims(); + addFilePrims(); + addIOPrims(); + addMiscPrims(); + addNetPrims(); +// addBLEPrims(); +// addRadioPrims(); + addSensorPrims(); + addSerialPrims(); + addTFTPrims(); + addVarPrims(); + addHIDPrims(); + addOneWirePrims(); +// addCameraPrims(); +// addEncoderPrims(); +// addSDCardPrims(); +#elif defined(NUTTX) + addDataPrims(); +// addDisplayPrims(); +// addFilePrims(); +// addIOPrims(); + addMiscPrims(); +// addNetPrims(); +// addBLEPrims(); +// addRadioPrims(); +// addSensorPrims(); +// addSerialPrims(); + addTFTPrims(); + addVarPrims(); +// addHIDPrims(); +// addOneWirePrims(); +// addCameraPrims(); +// addEncoderPrims(); +// addSDCardPrims(); +#else + addDataPrims(); + addDisplayPrims(); + addFilePrims(); + addIOPrims(); + addMiscPrims(); + addNetPrims(); + addBLEPrims(); + addRadioPrims(); + addSensorPrims(); + addSerialPrims(); + addTFTPrims(); + addVarPrims(); + addHIDPrims(); + addOneWirePrims(); + addCameraPrims(); + addEncoderPrims(); + addSDCardPrims(); +#endif +} + +// Task Ops + +void initTasks() { + memset(tasks, 0, sizeof(tasks)); + taskCount = 0; +} + +void startTaskForChunk(uint8 chunkIndex) { + // Start a task for the given chunk, if there is not one already. + + int i; + for (i = 0; i < taskCount; i++) { + if ((chunkIndex == tasks[i].taskChunkIndex) && tasks[i].status) { + return; // already running + } + } + for (i = 0; i < MAX_TASKS; i++) { + if (unusedTask == tasks[i].status) break; + } + if (i >= MAX_TASKS) { + outputString("No free task entries"); + return; + } + + memset(&tasks[i], 0, sizeof(Task)); + tasks[i].status = running; + tasks[i].taskChunkIndex = chunkIndex; + tasks[i].currentChunkIndex = chunkIndex; + tasks[i].code = chunks[chunkIndex].code; + tasks[i].ip = 4; // offset is 4 short words (8 bytes) relative to start of the code chunk + tasks[i].sp = 0; // relative to start of stack + tasks[i].fp = 0; // 0 means "not in a function call" + if (i >= taskCount) taskCount = i + 1; + sendMessage(taskStartedMsg, chunkIndex, 0, NULL); +} + +static void stopTaskForChunk(uint8 chunkIndex) { + // Stop the task for the given chunk, if any. + + int i; + for (i = 0; i < MAX_TASKS; i++) { + if (chunkIndex == tasks[i].taskChunkIndex) break; + } + if (i >= MAX_TASKS) return; // no task for chunkIndex + memset(&tasks[i], 0, sizeof(Task)); // clear task + if (i == (taskCount - 1)) taskCount--; + sendMessage(taskDoneMsg, chunkIndex, 0, NULL); +} + +static void stopAllTasks() { + // Stop all tasks. + + for (int t = 0; t < taskCount; t++) { + if (tasks[t].status) { + sendMessage(taskDoneMsg, tasks[t].taskChunkIndex, 0, NULL); + } + } + initTasks(); + fail(0); // clear error flag +} + +void startAll() { + // Start tasks for all start and 'when' hat blocks. + + // stop running tasks, reset, and clear memory + stopAllTasks(); + softReset(true); + resetTimer(); + + for (int i = 0; i < MAX_CHUNKS; i++) { + uint8 chunkType = chunks[i].chunkType; + if ((startHat == chunkType) || (whenConditionHat == chunkType)) { + startTaskForChunk(i); + } + } +} + +void stopAllTasksButThis(Task *thisTask) { + // Stop all tasks except the given one. + + for (int i = 0; i < MAX_TASKS; i++) { + Task *task = &tasks[i]; + if ((task != thisTask) && task->status) { + sendMessage(taskDoneMsg, task->taskChunkIndex, 0, NULL); + memset(task, 0, sizeof(Task)); // clear task + } + if (task == thisTask) { taskCount = i + 1; } + } +} + +// Selected Opcodes (see MicroBlocksCompiler.gp for complete set) + +#define pushLiteral 5 +#define initLocals 9 +#define recvBroadcast 41 + +int broadcastMatches(uint8 chunkIndex, char *msg, int byteCount) { + int16 *code = (int16 *) (chunks[chunkIndex].code + PERSISTENT_HEADER_WORDS); + // First three instructions of a broadcast hat should be: + // initLocals + // pushLiteral + // (data: literal offset) + // recvBroadcast + // A function with zero arguments can be also launched via a broadcast. + + if ((pushLiteral != CMD(code[1])) || + (recvBroadcast != CMD(code[3]))) { + return false; + } + code++; // skip initLocals + char *s = obj2str((OBJ) (code + *(code + 1) + 1)); + if (strlen(s) == 0) return true; // empty parameter in the receiver means "any message" + if (strlen(s) != byteCount) return false; + for (int i = 0; i < byteCount; i++) { + if (s[i] != msg[i]) return false; + } + return true; +} + +extern OBJ lastBroadcast; + +void startReceiversOfBroadcast(char *msg, int byteCount) { + // Start tasks for chunks with hat blocks matching the given broadcast if not already running. + + lastBroadcast = newStringFromBytes(msg, byteCount); + for (int i = 0; i < MAX_CHUNKS; i++) { + int chunkType = chunks[i].chunkType; + if (((broadcastHat == chunkType) || (functionHat == chunkType)) && (broadcastMatches(i, msg, byteCount))) { + startTaskForChunk(i); // only starts a new task if if chunk is not already running + } + } +} + +// Button Hat Support + +#define BUTTON_CHECK_INTERVAL 10000 // microseconds +#define BUTTON_CLICK_TIME 50 // milliseconds + +static uint32 lastCheck = 0; +static uint32 buttonADownTime = 0; +static uint32 buttonBDownTime = 0; +static char buttonAHandled = false; +static char buttonBHandled = false; + +static void startButtonHats(int hatType) { + for (int i = 0; i < MAX_CHUNKS; i++) { + if (hatType == chunks[i].chunkType) { + startTaskForChunk(i); // only starts a new task if if chunk is not already running + } + } +} + +static int mustPollButtons() { + // Return true if there is at least one "when button _ pressed" script. + // Always return true on ED1 because we need to poll the touch sensor buttons. + + #ifdef ARDUINO_CITILAB_ED1 + return true; + #endif + + for (int i = 0; i < MAX_CHUNKS; i++) { + int hatType = chunks[i].chunkType; + if ((buttonAHat <= hatType) && (hatType <= buttonsAandBHat)) { + return true; + } + } + return false; +} + +void checkButtons() { + // If button A, button B, or both are pressed, start tasks for all of the relevant + // hat blocks (if they are not already running). This check is done at most once + // every BUTTON_CHECK_INTERVAL microseconds. + + uint32 now = microsecs(); + if (now < lastCheck) lastCheck = 0; // clock wrap + if ((now - lastCheck) < BUTTON_CHECK_INTERVAL) return; // not time yet + lastCheck = now; + + if (!mustPollButtons()) return; // no need to poll buttons (allows button pins to be used for output) + + now = millisecs(); // use milliseconds for button timeouts + if (!now) now = 1; // the value is reserved to mean button is not down + + int buttonAIsDown = (int) primButtonA(NULL); + int buttonBIsDown = (int) primButtonB(NULL); + + if (buttonAIsDown && !buttonADownTime) { // button A up -> down + buttonADownTime = now; + if (buttonBDownTime) { + if (!buttonBHandled) { + startButtonHats(buttonsAandBHat); + buttonAHandled = true; + buttonBHandled = true; + } else { + startButtonHats(buttonAHat); + buttonAHandled = true; + } + } + } + if (buttonBIsDown && !buttonBDownTime) { // button B up -> down + buttonBDownTime = now; + if (buttonADownTime) { + if (!buttonAHandled) { + startButtonHats(buttonsAandBHat); + buttonAHandled = true; + buttonBHandled = true; + } else { + startButtonHats(buttonBHat); + buttonBHandled = true; + } + } + } + + if (buttonADownTime && !buttonAHandled) { + if (now < buttonADownTime) buttonADownTime = now; // clock wrap + if ((now - buttonADownTime) >= BUTTON_CLICK_TIME) { // button A held for click time + startButtonHats(buttonAHat); + buttonAHandled = true; + } + } + if (buttonBDownTime && !buttonBHandled) { + if (now < buttonBDownTime) buttonBDownTime = now; // clock wrap + if ((now - buttonBDownTime) >= BUTTON_CLICK_TIME) { // button B held for click time + startButtonHats(buttonBHat); + buttonBHandled = true; + } + } + + if (buttonADownTime && !buttonAIsDown) { // button A down -> up + if (!buttonAHandled) startButtonHats(buttonAHat); // fast click (< BUTTON_CLICK_TIME) + buttonADownTime = 0; + buttonAHandled = false; + } + if (buttonBDownTime && !buttonBIsDown) { // button B down -> up + if (!buttonBHandled) startButtonHats(buttonBHat); // fast click (< BUTTON_CLICK_TIME) + buttonBDownTime = 0; + buttonBHandled = false; + } +} + +// Store Ops + +static void storeCodeChunk(uint8 chunkIndex, int byteCount, uint8 *data) { + if (chunkIndex >= MAX_CHUNKS) return; + stopTaskForChunk(chunkIndex); + int chunkType = data[0]; // first byte is the chunk type + int *persistentChunk = appendPersistentRecord(chunkCode, chunkIndex, chunkType, byteCount - 1, &data[1]); + chunks[chunkIndex].code = persistentChunk; + chunks[chunkIndex].chunkType = persistentChunk ? chunkType : unusedChunk; +} + +static void storeVarName(uint8 varIndex, int byteCount, uint8 *data) { + uint8 buf[100]; + if (byteCount > 99) byteCount = 99; + uint8 *dst = buf; + for (int i = 0; i < byteCount; i++) *dst++ = data[i]; + *dst = 0; // null terminate + appendPersistentRecord(varName, varIndex, 0, (byteCount + 1), buf); +} + +// Delete Ops + +static void deleteCodeChunk(uint8 chunkIndex) { + if (chunkIndex >= MAX_CHUNKS) return; + stopTaskForChunk(chunkIndex); + chunks[chunkIndex].code = NULL; + chunks[chunkIndex].chunkType = unusedChunk; + appendPersistentRecord(chunkDeleted, chunkIndex, 0, 0, NULL); +} + +static void deleteAllChunks() { + stopAllTasks(); + #if defined(ESP8266) || defined(ARDUINO_ARCH_ESP32) || defined(GNUBLOCKS) || defined(RP2040_PHILHOWER) + clearPersistentMemory(); + clearCodeFile(0); + #else + appendPersistentRecord(deleteAll, 0, 0, 0, NULL); + #endif + memset(chunks, 0, sizeof(chunks)); +} + +static void clearAllVariables() { + // Clear variable name records (but don't clear the variable values). + appendPersistentRecord(varsClearAll, 0, 0, 0, NULL); +} + +// Extended Messages + +static void processExtendedMessage(uint8 msgID, int byteCount, uint8 *data) { + switch (msgID) { + case 1: // set extraByteDelay + if (byteCount < 1) break; + int arg = *data; + if (arg < 1) arg = 1; + if (arg > 50) arg = 50; + extraByteDelay = arg * 100; // 100 to 5000 microseconds per character + break; + case 2: // suspend saving to the code file on file-based boards while loading a project or library + suspendCodeFileUpdates(); + break; + case 3: // save the entire RAM code store to the code file and resume incremental saving + resumeCodeFileUpdates(); + break; + } +} + +// Soft Reset + +static void softReset(int clearMemoryFlag) { + // Reset the hardware and, optionally, clear memory. + // Do not reload scripts from persistent memory. + // This is not a full hardware reset/reboot, but close. + + stopAllTasks(); + resumeCodeFileUpdates(); + + OBJ off = falseObj; + if (!useTFT) primSetUserLED(&off); + #if defined(OLED_128_64) + if (!useTFT) tftInit(); + #endif + +#if defined(ARDUINO_BBC_MICROBIT) || defined(ARDUINO_BBC_MICROBIT_V2) || \ + defined(ARDUINO_CALLIOPE_MINI) || defined(CALLIOPE_V3) + OBJ enable = trueObj; + primMBEnableDisplay(1, &enable); + primMBDisplayOff(0, NULL); + updateMicrobitDisplay(); +#endif + + resetRadio(); + stopPWM(); + stopServos(); + stopTone(); + #if !defined(DATABOT) + turnOffInternalNeoPixels(); + #endif + turnOffPins(); + if (clearMemoryFlag) { + memClear(); + outputString("Memory cleared"); + } +} + +// Sending Messages to IDE + +// Circular output buffer +#define OUTBUF_SIZE 1024 // must be a power of 2! +#define OUTBUF_MASK (OUTBUF_SIZE - 1) +static uint8 outBuf[OUTBUF_SIZE]; +static int outBufStart = 0; +static int outBufEnd = 0; + +#define OUTBUF_BYTES() ((outBufEnd - outBufStart) & OUTBUF_MASK) + +static void sendData() { +#ifdef EMSCRIPTEN + // xxx can this special case for EMSCRIPTEN be removed? try it and test w/ boardie. + if (outBufStart > outBufEnd) { + if (sendBytes(outBuf, outBufStart, OUTBUF_SIZE)) { + outBufStart = 0; + } + } + if (outBufStart != outBufEnd) { + if (sendBytes(outBuf, outBufStart, outBufEnd)) { + outBufStart = outBufEnd & OUTBUF_MASK; + } + } +#else + int byteCount = 0; + + if (outBufStart > outBufEnd) { + byteCount = sendBytes(outBuf, outBufStart, OUTBUF_SIZE); + outBufStart = (outBufStart + byteCount) & OUTBUF_MASK; + } + if (outBufStart < outBufEnd) { + byteCount = sendBytes(outBuf, outBufStart, outBufEnd); + outBufStart = (outBufStart + byteCount) & OUTBUF_MASK; + } +#endif +} + +static inline void queueByte(uint8 aByte) { + outBuf[outBufEnd] = aByte; + outBufEnd = (outBufEnd + 1) & OUTBUF_MASK; +} + +static void sendMessage(int msgType, int chunkIndex, int dataSize, char *data) { + if (!data) { // short message + if (!hasOutputSpace(3)) return; // no space; drop message + queueByte(250); + queueByte(msgType); + queueByte(chunkIndex); + } else { + int totalBytes = 5 + dataSize; + if (!hasOutputSpace(totalBytes)) return; // no space; drop message + queueByte(251); + queueByte(msgType); + queueByte(chunkIndex); + queueByte(dataSize & 0xFF); // low byte of size + queueByte((dataSize >> 8) & 0xFF); // high byte of size + for (int i = 0; i < dataSize; i++) { + queueByte(data[i]); + } + } +} + +int hasOutputSpace(int byteCount) { return ((OUTBUF_MASK - OUTBUF_BYTES()) > byteCount); } + +int bytesToOutput(void) { return OUTBUF_BYTES(); } + +static void waitForOutbufBytes(int bytesNeeded) { + // Wait until there is room for the given number of bytes in the output buffer. + + while (bytesNeeded > (OUTBUF_MASK - OUTBUF_BYTES())) { + sendData(); // should eventually create enough room for bytesNeeded + } +} + +void waitAndSendMessage(int msgType, int chunkIndex, int dataSize, char *data) { + // Wait for space, then send the given message. + + waitForOutbufBytes(dataSize + 5); + sendMessage(msgType, chunkIndex, dataSize, data); +} + +static void sendValueMessage(uint8 msgType, uint8 chunkOrVarIndex, OBJ value) { + // Send a value message of the given type for the given chunkOrVarIndex. + // Data is: <...data...> + // Types: 1 - integer, 2 - string, 3 - boolean, 4 - list, 5 - bytearray + + char data[801]; + + if (isInt(value)) { // 32-bit integer, little endian + data[0] = 1; // data type (1 is integer) + int n = obj2int(value); + data[1] = (n & 0xFF); + data[2] = ((n >> 8) & 0xFF); + data[3] = ((n >> 16) & 0xFF); + data[4] = ((n >> 24) & 0xFF); + sendMessage(msgType, chunkOrVarIndex, 5, data); + } else if (IS_TYPE(value, StringType)) { + data[0] = 2; // data type (2 is string) + char *s = obj2str(value); + int len = strlen(s); + int sendCount = (len > 800) ? 800 : len; + for (int i = 0; i < sendCount; i++) { + data[i + 1] = s[i]; + } + if (len > 800) { + memcpy(&data[798], "...", 3); // string was truncated; add ellipses + } + sendMessage(msgType, chunkOrVarIndex, (sendCount + 1), data); + } else if ((value == trueObj) || (value == falseObj)) { + data[0] = 3; // data type (3 is boolean) + data[1] = (trueObj == value) ? 1 : 0; + sendMessage(msgType, chunkOrVarIndex, 2, data); + } else if (IS_TYPE(value, ListType)) { + data[0] = 4; // data type (4 is list) + // Note: xxx Does not handle sublists. + char *dst = &data[1]; + // total items in list (16-bit, little endian) + int itemCount = obj2int(FIELD(value, 0)); + *dst++ = itemCount & 0xFF; + *dst++ = (itemCount >> 8) & 0xFF; + int sendCount = 32; // send up to this many items + if (itemCount < sendCount) sendCount = itemCount; + *dst++ = sendCount; + for (int i = 0; i < sendCount; i++) { + OBJ item = FIELD(value, i + 1); + int type = objType(item); + if (IntegerType == type) { // integer (32-bit signed, little-endian) + *dst++ = 1; // item type (1 is integer) + int n = obj2int(item); + *dst++ = (n & 0xFF); + *dst++ = ((n >> 8) & 0xFF); + *dst++ = ((n >> 16) & 0xFF); + *dst++ = ((n >> 24) & 0xFF); + } else if (StringType == type) { + *dst++ = 2; // item type (2 is string) + int maxStringItem = 20; + char *s = obj2str(item); + int len = strlen(s); + if (len <= maxStringItem) { + *dst++ = len; + for (int i = 0; i < len; i++) *dst++ = s[i]; + } else { + *dst++ = maxStringItem; // send (maxStringItem - 3) bytes, then '...' + for (int i = 0; i < (maxStringItem - 3); i++) *dst++ = s[i]; + for (int i = 0; i < 3; i++) *dst++ = '.'; + } + } else if (BooleanType == type) { + *dst++ = 3; // item type (3 is boolean) + *dst++ = (trueObj == item) ? 1 : 0; + } else if (ListType == type) { // sublist within a list; send item count only + *dst++ = 4; // item type (4 is list) + int n = obj2int(FIELD(item, 0)); // item count of sublist + *dst++ = n & 0xFF; + *dst++ = (n >> 8) & 0xFF; + *dst++ = 0; // send zero items of sublists + } else if (ByteArrayType == type) { // bytearray within a list; send bytecount only + *dst++ = 5; // item type (5 is bytearray) + int n = BYTES(item); // bytecount item + *dst++ = n & 0xFF; + *dst++ = (n >> 8) & 0xFF; + *dst++ = 0; // send zero bytes + } else { + *dst++ = 0; // item type (0 is unknown) + } + } + sendMessage(msgType, chunkOrVarIndex, (dst - data), data); + } else if (IS_TYPE(value, ByteArrayType)) { + data[0] = 5; // data type (5 is bytearray) + char *dst = &data[1]; + // total bytecount + int byteCount = BYTES(value); + *dst++ = byteCount & 0xFF; + *dst++ = (byteCount >> 8) & 0xFF; + uint8 *bytes = (uint8 *) &FIELD(value, 0); + int sendCount = (byteCount < 100) ? byteCount : 100; // send up to 100 bytes + *dst++ = sendCount; + for (int i = 0; i < sendCount; i++) { + *dst++ = bytes[i]; + } + sendMessage(msgType, chunkOrVarIndex, (sendCount + 4), data); + } +} + +void logData(char *s) { + // Log data (the former 'print' command). Use chunkID 254. + + char data[200]; + data[0] = 2; // data type (2 is string) + int byteCount = strlen(s); + if (byteCount > (int) (sizeof(data) - 1)) byteCount = sizeof(data) - 1; + for (int i = 0; i < byteCount; i++) { + data[i + 1] = s[i]; + } + sendMessage(outputValueMsg, 254, (byteCount + 1), data); +} + +void outputString(const char *s) { + // Sending a debug string. Use chunkID 255. + + if (!ideConnected()) return; // serial port not open; do nothing + + char data[200]; + data[0] = 2; // data type (2 is string) + int byteCount = strlen(s); + if (byteCount > (int) (sizeof(data) - 1)) byteCount = sizeof(data) - 1; + for (int i = 0; i < byteCount; i++) { + data[i + 1] = s[i]; + } + + waitForOutbufBytes(byteCount + 50); + sendMessage(outputValueMsg, 255, (byteCount + 1), data); + + // wait for string to be sent: + while (ideConnected() && (OUTBUF_BYTES() > 0)) { + sendData(); // should eventually create enough room for bytesNeeded + } +} + +void sendTaskDone(uint8 chunkIndex) { + sendMessage(taskDoneMsg, chunkIndex, 0, NULL); +} + +void sendTaskError(uint8 chunkIndex, uint8 errorCode, int where) { + // Send a task error message: one-byte error code + 4-byte location. + // Location is + + char data[5]; + data[0] = (errorCode & 0xFF); // one byte error code + data[1] = (where & 0xFF); + data[2] = ((where >> 8) & 0xFF); + data[3] = ((where >> 16) & 0xFF); + data[4] = ((where >> 24) & 0xFF); + sendMessage(taskErrorMsg, chunkIndex, sizeof(data), data); +} + +void sendTaskReturnValue(uint8 chunkIndex, OBJ returnValue) { + // Send the value returned by the task for the given chunk. + + sendValueMessage(taskReturnedValueMsg, chunkIndex, returnValue); +} + +static void sendVariableValue(int varID) { + if ((varID >= 0) && (varID < MAX_VARS)) { + sendValueMessage(varValueMsg, varID, vars[varID]); + } +} + +static void sendValueOfVariableNamed(uint8 chunkIndex, int byteCount, uint8 *data) { + char varName[100]; + if (byteCount > 99) return; // variable name too long; ignore request + memcpy(varName, &data[0], byteCount); + varName[byteCount] = 0; // null terminate + int varID = indexOfVarNamed(varName); + if (varID >= 0) sendValueMessage(varValueMsg, chunkIndex, vars[varID]); +} + +static void setVariableValue(int varID, int byteCount, uint8 *data) { + if ((varID >= 0) && (varID < MAX_VARS)) { + int type = data[0]; + switch (type) { + case 1: // integer + vars[varID] = int2obj((data[4] << 24) | (data[3] << 16) | (data[2] << 8) | data[1]); + break; + case 2: // string + if (byteCount >= 1) { + vars[varID] = newStringFromBytes((char *) &data[1], byteCount - 1); + } + break; + case 3: // boolean + vars[varID] = data[1] ? trueObj : falseObj; + break; + } + } +} + +static void sendVersionString(int chunkIndex) { + char s[100]; + #if defined(DUELink) + if (1 == chunkIndex) { // return the PID as a hex string + snprintf(s, sizeof(s), "0x%06X", DUE_PID); + sendMessage(versionMsg, 1, strlen(s), s); + return; + } + #endif + snprintf(s, sizeof(s), " %s %s", VM_VERSION, boardType()); + s[0] = 2; // data type (2 is string) + sendMessage(versionMsg, 0, strlen(s), s); +} + +void sendBroadcastToIDE(char *s, int len) { + int spaceNeeded = len + 50; // leave room for header and a few other messages + if (!hasOutputSpace(spaceNeeded)) { + if (!ideConnected()) { + return; // apparently not connected to IDE + } else { + waitForOutbufBytes(spaceNeeded); + } + } + sendMessage(broadcastMsg, 0, len, s); + taskSleep(1); // avoid Boardie lockup +} + +void sendSayForChunk(char *s, int len, uint8 chunkIndex) { + // Used by the "say" primitive. The buffer s includes the string value type byte. + sendMessage(outputValueMsg, chunkIndex, len, s); +} + +void sendCodeStoreFull() { + sendMessage(codeStoreFullMsg, 0, 0, NULL); +} + +// Code chunk error checking (CRC-32) + +const uint32_t crcTable[] = { + 0x0, 0x77073096, 0xEE0E612C, 0x990951BA, 0x76DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, + 0xEDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x9B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, +0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, +0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, +0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, +0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, +0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, +0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, +0x76DC4190, 0x1DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x6B6B51F, 0x9FBFE4A5, 0xE8B8D433, +0x7807C9A2, 0xF00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x86D3D2D, 0x91646C97, 0xE6635C01, +0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, +0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, +0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, +0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, +0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, +0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, +0xEDB88320, 0x9ABFB3B6, 0x3B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x4DB2615, 0x73DC1683, +0xE3630B12, 0x94643B84, 0xD6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0xA00AE27, 0x7D079EB1, +0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, +0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, +0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, +0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, +0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, +0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, +0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x26D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x5005713, +0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0xCB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0xBDBDF21, +0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, +0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, +0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, +0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, +0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, +0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D}; + +uint32_t crc32(uint8_t *buf, int byteCount) { + uint32_t crc = ~0; + uint8_t *end = buf + byteCount; + for (uint8_t *p = buf; p < end; p++) { + crc = (crc >> 8) ^ crcTable[(crc & 0xff) ^ *p]; + } + return ~crc; +} + +static void sendChunkCRC(int chunkID) { + // Send the 4-byte CRC-32 for the given chunk. Do nothing if the chunk is not in use. + + if ((chunkID < 0) || (chunkID >= MAX_CHUNKS)) return; + OBJ code = chunks[chunkID].code; + if (code) { + int wordCount = *(code + 1); // size is the second word in the persistent store record + uint8_t *chunkData = (uint8_t *) (code + PERSISTENT_HEADER_WORDS); + uint32_t crc = crc32(chunkData, (4 * wordCount)); + waitForOutbufBytes(9); + sendMessage(chunkCRCMsg, chunkID, 4, (char *) &crc); + sendData(); + } +} + +void sendAllCRCs() { + // count chunks + int chunkCount = 0; + for (int i = 0; i < MAX_CHUNKS; i++) { + if (chunks[i].code) chunkCount++; + } + + // send message header + int dataSize = 5 * chunkCount; + waitForOutbufBytes(10); + queueByte(251); + queueByte(allCRCsMsg); + queueByte(0); + queueByte(dataSize & 0xFF); // low byte of size + queueByte((dataSize >> 8) & 0xFF); // high byte of size + + // send CRC records for chunks in use + // each record is 5 bytes: chunkID (one byte) + the CRC for that chunk (four bytes) + int delayPerCRC = extraByteDelay / 250; // msec delay for 4 bytes (extraByteDelay is in usecs) + for (int i = 0; i < MAX_CHUNKS; i++) { + if (chunks[i].code) { + OBJ code = chunks[i].code; + int wordCount = *(code + 1); // size is the second word in the persistent store record + uint8_t *chunkData = (uint8_t *) (code + PERSISTENT_HEADER_WORDS); + uint32_t crc = crc32(chunkData, (4 * wordCount)); + char *crcBytes = (char *) &crc; + waitForOutbufBytes(5); + queueByte(i); + queueByte(crcBytes[0]); + queueByte(crcBytes[1]); + queueByte(crcBytes[2]); + queueByte(crcBytes[3]); + delay(delayPerCRC); + } + } + deferIDEDisconnect(); +} + +// Retrieving source code + +static void sendCodeChunk(int chunkID, int chunkType, int chunkBytes, char *chunkData) { + int msgSize = 1 + chunkBytes; + waitForOutbufBytes(5 + msgSize); + queueByte(251); + queueByte(chunkCode16Msg); + queueByte(chunkID); + queueByte(msgSize & 0xFF); // low byte of size + queueByte((msgSize >> 8) & 0xFF); // high byte of size + queueByte(chunkType); // first byte of msg body is the chunk type + char *end = chunkData + chunkBytes; + for (char *p = chunkData; p < end; p++) { + queueByte(*p); + } +} + +static void sendAllCode() { + // Send the code for all chunks to the IDE. + + int delayPerWord = extraByteDelay / 250; // derive from extraByteDelay + for (int chunkID = 0; chunkID < MAX_CHUNKS; chunkID++) { + OBJ code = chunks[chunkID].code; + if (NULL == code) continue; // skip unused chunk entry + + int chunkType = chunks[chunkID].chunkType; + int chunkWords = *(code + 1); // chunk word count is second word of persistent store record + char *chunkData = (char *) (code + PERSISTENT_HEADER_WORDS); + sendCodeChunk(chunkID, chunkType, (4 * chunkWords), chunkData); + sendData(); + delay(delayPerWord * chunkWords); // 2 fails on Johns Chromebook; 3 works; 5 is conservative + sendData(); + } + deferIDEDisconnect(); +} + +// Variable support + +static void sendVarNameMessage(int varID, int *persistentRecord) { + if (!persistentRecord) return; // NULL persistentRecord; do nothing + + char *varName = (char *) (persistentRecord + 2); + int bodyBytes = strlen(varName); + waitForOutbufBytes(5 + bodyBytes); + + queueByte(251); + queueByte(varNameMsg); + queueByte(varID); + queueByte(bodyBytes & 0xFF); // low byte of size + queueByte((bodyBytes >> 8) & 0xFF); // high byte of size + char *src = varName; + for (int i = 0; i < bodyBytes; i++) { + queueByte(*src++); + } +} + +static int* varsStart() { + int *p = scanStart(); + + // find the last varsClearAll record + int *result = p; + while (p) { + if (varsClearAll == ((*p >> 16) & 0xFF)) result = p; + p = recordAfter(p); + } + return result; +} + +static void sendVarNames() { + // Send the names of all variables. + + int *p = varsStart(); + while (p) { + int recType = (*p >> 16) & 0xFF; + int varID = (*p >> 8) & 0xFF; + if (recType == varName) sendVarNameMessage(varID, p); + p = recordAfter(p); + } + deferIDEDisconnect(); +} + +int indexOfVarNamed(const char *s) { + // Return the index of the given variable or -1 if not found. + + int result = -1; // default is not found + int *p = varsStart(); + while (p) { + int recType = (*p >> 16) & 0xFF; + int id = (*p >> 8) & 0xFF; + if (recType == varName) { + if (0 == strcmp(s, (char *) (p + 2))) result = id; + } else if (recType == varsClearAll) { + result = -1; + } + p = recordAfter(p); + } + return result; +} + +// Receiving Messages from IDE + +#define RCVBUF_SIZE 1024 +#define MAX_MSG_SIZE (RCVBUF_SIZE - 10) // 5 header + 1 terminator bytes plus a few extra +static uint8 rcvBuf[RCVBUF_SIZE]; +static int rcvByteCount = 0; +uint32 lastRcvTime = 0; + +static void skipToStartByteAfter(int startIndex) { + int i, nextStart = -1; + for (i = startIndex; i < rcvByteCount; i++) { + int b = rcvBuf[i]; + if ((0xFA == b) || (0xFB == b)) { + if ((i + 1) < rcvByteCount) { + b = rcvBuf[i + 1]; + if ((b == 0) || ((b > LAST_MSG) && (b < 200))) continue; // illegal msg type; keep scanning + } + nextStart = i; + break; + } + } + if (-1 == nextStart) { // no start byte found; clear the entire buffer + rcvByteCount = 0; + return; + } + uint8 *dst = &rcvBuf[0]; + for (i = nextStart; i < rcvByteCount; i++) { + *dst++ = rcvBuf[i]; + } + rcvByteCount -= nextStart; +} + +static int receiveTimeout() { + // Check for receive timeout. This allows recovery from bad length or incomplete message. + + uint32 usecs = microsecs(); + if (usecs < lastRcvTime) lastRcvTime = 0; // clock wrap + return (usecs - lastRcvTime) > 20000; +} + +#if !defined(GNUBLOCKS) || defined(EMSCRIPTEN) + +int ideConnected() { + // Return true if the board is connected to the MicroBlocks IDE + // (i.e. if it has received a message from the IDE in the past few seconds). + + if (0 == lastRcvTime) return false; // startup - no IDE messages yet + + uint32 now = microsecs(); + uint32 elapsed = (lastRcvTime > now) ? now : (now - lastRcvTime); + return elapsed < (5 * 1000000); // an ide msg was received in the past few seconds +} + +#endif + +void deferIDEDisconnect() { + lastRcvTime = microsecs(); +} + +static void sendPingNow(int chunkIndex) { + // Used to acknowledge receipt of a command that may take time, such as sending all CRC's. + sendMessage(pingMsg, chunkIndex, 0, NULL); // send a ping to acknowledge receipt + sendData(); +} + +static int processShortMessage() { + if (rcvByteCount < 3) { // message is not complete + if (receiveTimeout()) { + skipToStartByteAfter(1); + return 1; + } + return 0; // message incomplete + } + int cmd = rcvBuf[1]; + int chunkIndex = rcvBuf[2]; + switch (cmd) { + case deleteChunkMsg: + deleteCodeChunk(chunkIndex); + break; + case startChunkMsg: + startTaskForChunk(chunkIndex); + sendPingNow(chunkIndex); // send a ping to acknowledge + break; + case stopChunkMsg: + stopTaskForChunk(chunkIndex); + sendPingNow(chunkIndex); // send a ping to acknowledge + break; + case startAllMsg: + if (1 != chunkIndex) break; // ignore msg from 32-bit IDE + startAll(); + break; + case stopAllMsg: + stopAllTasks(); + softReset(false); + outputString("All tasks stopped"); + break; + case getVarMsg: + sendVariableValue(chunkIndex); + break; + case getVarNamesMsg: + sendVarNames(); + break; + case clearVarsMsg: + if (1 != chunkIndex) break; // ignore msg from 32-bit IDE + clearAllVariables(); + memClear(); + break; + case getChunkCRCMsg: + sendChunkCRC(chunkIndex); + break; + case getAllCRCsMsg: + if (1 != chunkIndex) break; // ignore msg from 32-bit IDE + sendPingNow(chunkIndex); // send a ping to acknowledge receipt + sendAllCRCs(); + break; + case getVersionMsg: + sendVersionString(chunkIndex); + break; + case getAllCodeMsg: + if (1 != chunkIndex) break; // ignore msg from 32-bit IDE + sendPingNow(chunkIndex); // send a ping to acknowledge receipt + if (chunkIndex == 1) { // requested by 16-bit IDE + sendAllCode(); + } + break; + case deleteAllCodeMsg: + if (1 != chunkIndex) break; // ignore msg from 32-bit IDE + deleteAllChunks(); + memClear(); + primMBDisplayOff(0, NULL); + break; + case systemResetMsg: + // non-zero chunkIndex is used for debugging operations + if (1 == chunkIndex) { outputRecordHeaders(); break; } + if (2 == chunkIndex) { + // compact the code store and return the code usage stats + char msgBody[8]; + compactCodeStore((int *) &msgBody[0], (int *) &msgBody[4]); + sendMessage(codeStoreUsedMsg, 0, 8, msgBody); + sendData(); + break; + } + if (3 == chunkIndex) { primMBDisplayOff(0, NULL); } // used by Boardie reset + if (199 == chunkIndex) { + clearAllVariables(); // do a Flash write operation to enable DFU after reset + dueLinkEraseFlashAndReset(); + } + softReset(true); + break; + case pingMsg: + sendPingNow(chunkIndex); + break; + case enableBLEMsg: + BLE_setEnabled(chunkIndex); + break; + default: + if ((200 <= cmd) && (cmd <= 205)) { + processFileMessage(cmd, 0, NULL); + sendData(); + } + } + skipToStartByteAfter(3); + return 1; +} + +static int processLongMessage() { + int msgLength = (rcvBuf[4] << 8) | rcvBuf[3]; + if ((rcvByteCount >= 5) && (msgLength > MAX_MSG_SIZE)) { // message too large for buffer + skipToStartByteAfter(1); + return 1; + } + if ((rcvByteCount < 5) || (rcvByteCount < (5 + msgLength))) { // message is not complete + if (receiveTimeout()) { + skipToStartByteAfter(1); + return 1; + } + return 0; // message incomplete + } + if (0xFE != rcvBuf[5 + msgLength - 1]) { // chunk does not end with a terminator byte + skipToStartByteAfter(1); + return 1; + } + int cmd = rcvBuf[1]; + int chunkIndex = rcvBuf[2]; + int bodyBytes = msgLength - 1; // subtract terminator byte + switch (cmd) { + case chunkCode16Msg: // code chunk from 16-bit IDE + sendPingNow(chunkIndex); // send a ping to acknowledge receipt + #if defined(ESP32_S3) + delay(10); // avoid chunk save glitches on m5atom-lite; less than 5 msecs fails + #endif + storeCodeChunk(chunkIndex, bodyBytes, &rcvBuf[5]); + sendChunkCRC(chunkIndex); + break; + case setVarMsg: + setVariableValue(rcvBuf[2], bodyBytes, &rcvBuf[5]); + break; + case getVarMsg: + sendValueOfVariableNamed(chunkIndex, bodyBytes, &rcvBuf[5]); + break; + case broadcastMsg: + startReceiversOfBroadcast((char *) &rcvBuf[5], bodyBytes); + break; + case varNameMsg: + storeVarName(chunkIndex, bodyBytes, &rcvBuf[5]); + sendPingNow(chunkIndex); // send a ping to acknowledge save + break; + case extendedMsg: + processExtendedMessage(chunkIndex, bodyBytes, &rcvBuf[5]); + break; + default: + if ((200 <= cmd) && (cmd <= 205)) { + processFileMessage(cmd, bodyBytes, (char *) &rcvBuf[5]); + sendData(); + } + } + skipToStartByteAfter(5 + msgLength); + return 1; +} + +// Uncomment when building on mbed: +// static void busyWaitMicrosecs(int usecs) { +// uint32 start = microsecs(); +// while ((microsecs() - start) < (uint32) usecs) /* wait */; +// } + +void captureIncomingBytes() { + int bytesRead = recvBytes(&rcvBuf[rcvByteCount], RCVBUF_SIZE - rcvByteCount); + rcvByteCount += bytesRead; + // uncomment to check for serial buffer overruns: + // if (bytesRead > 49) reportNum("bytesRead", bytesRead); +} + +void processMessage() { + // Process a message from the client. + sendData(); + + int bytesRead = recvBytes(&rcvBuf[rcvByteCount], RCVBUF_SIZE - rcvByteCount); + // uncomment to check for serial buffer overruns: + // if (bytesRead > 49) reportNum("bytesRead", bytesRead); + rcvByteCount += bytesRead; + if (!rcvByteCount) return; + + // the following is needed when built on mbed to avoid dropped bytes +// while (bytesRead > 0) { +// // on Arduino Primo, 100 sometimes fails; use 150 to be safe (character time is ~90 usecs) +// busyWaitMicrosecs(150); +// bytesRead = recvBytes(&rcvBuf[rcvByteCount], RCVBUF_SIZE - rcvByteCount); +// rcvByteCount += bytesRead; +// } + + // Drain receive buffer. + // It can happen on TCP that data is buffered due to re-xmit attempts. When successful the data + // is sent all at once overloading receive buffer with messages. + lastRcvTime = microsecs(); + int processMessage = 1; + while(processMessage) { + int firstByte = rcvBuf[0]; + if (0xFA == firstByte) { + processMessage = processShortMessage(); + } else if (0xFB == firstByte) { + processMessage = processLongMessage(); + } else { + skipToStartByteAfter(1); // bad message, probably due to dropped bytes + } + if (!rcvByteCount) processMessage = 0; + sendData(); + } +} diff --git a/interpreters/smallvm/tftPrims.c b/interpreters/smallvm/tftPrims.c new file mode 100644 index 000000000..fcc054968 --- /dev/null +++ b/interpreters/smallvm/tftPrims.c @@ -0,0 +1,357 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Copyright 2020 John Maloney, Bernat Romagosa, and Jens Mönig + +// linuxTftPrims.cpp - Microblocks TFT screen primitives simulated on an SDL window +// Bernat Romagosa, February 2021 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mem.h" +#include "interp.h" + +static int fb_fd; +static struct fb_planeinfo_s pinfo; +static struct fb_videoinfo_s vinfo; +static uint8_t *fb_mem; +static uint16_t color = 0; + +static bool tftEnabled = false; + +// Helper Functions + +#define COLOR_888_TO_565(color) (((((color) >> 19) & 0x1f) << 11) \ + |((((color) >> 10) & 0x3f) << 5) \ + |(((color) >> 3) & 0x1f)) + +static void setRenderColor(uint32_t colorA) { + color = COLOR_888_TO_565(colorA); +} + +void tftClear() { + tftInit(); + setRenderColor(0); + // TODO: clear screen +} + +void tftInit() { + if (!tftEnabled) { + int ret = open("/dev/fb0", O_RDWR); + if (ret < 0) { + perror("Cannot open /dev/fb0: "); + abort(); + } + fb_fd = ret; + ret = ioctl(fb_fd, FBIOGET_PLANEINFO, &pinfo); + if (ret < 0) { + perror("Cannot get plane info: "); + close(fb_fd); + abort(); + } + if (pinfo.bpp != 16) { + printf("Bits per pixel != 16\n"); + close(fb_fd); + abort(); + } + ret = ioctl(fb_fd, FBIOGET_VIDEOINFO, &vinfo); + if (ret < 0) { + perror("Cannot get video info: "); + close(fb_fd); + abort(); + } + fb_mem = mmap(NULL, + pinfo.fblen, + PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_FILE, fb_fd, 0); + if (fb_mem == MAP_FAILED) { + perror("Cannot map framebuffer: "); + close(fb_fd); + abort(); + } + tftEnabled = true; + } +} + +// TFT Primitives + +static OBJ primEnableDisplay(int argCount, OBJ *args) { + if (trueObj == args[0]) { + tftInit(); + } else { + //TODO: paint screen black to clear it + //tftEnabled = false; + } + return falseObj; +} + +static OBJ primGetWidth(int argCount, OBJ *args) { + int w; + tftInit(); + w = vinfo.xres; + return int2obj(w); +} + +static OBJ primGetHeight(int argCount, OBJ *args) { + int h; + tftInit(); + h = vinfo.yres; + return int2obj(h); +} + +static OBJ primSetPixel(int argCount, OBJ *args) { + tftInit(); + int x = obj2int(args[0]); + int y = obj2int(args[1]); + setRenderColor(obj2int(args[2])); + + uint16_t *dst = (uint16_t*) fb_mem; + dst[y * vinfo.xres + x] = color; + +#ifdef CONFIG_FB_UPDATE + struct fb_area_s area; + area.x = x; + area.y = y; + area.w = 1; + area.h = 1; + + ioctl(fb_fd, FBIO_UPDATE, &area); +#endif + return falseObj; +} + +static OBJ primLine(int argCount, OBJ *args) { + tftInit(); + int x0 = obj2int(args[0]); + int y0 = obj2int(args[1]); + int x1 = obj2int(args[2]); + int y1 = obj2int(args[3]); + setRenderColor(obj2int(args[4])); + + uint16_t *dst = (uint16_t *) fb_mem; + + double dx = x1 - x0; + double dy = y1 - y0; + double m = dy/dx; + if (x0 != x1) { + int x0_s = x0; + int x1_s = x1; + if (x1_s < x0_s) + { + int tmp = x0_s; + x0_s = x1_s; + x1_s = tmp; + } + for (int x = x0_s; x <= x1_s; x++) + { + int y = (int)(m * ((double)x - (double)x0)) + y0; + dst[y * vinfo.xres + x] = color; + } + } else { + int y0_s = y0; + int y1_s = y1; + if (y1_s < y0_s) + { + int tmp = y0_s; + y0_s = y1_s; + y1_s = tmp; + } + for (int y = y0_s; y <= y1_s; y++) + { + dst[y * vinfo.xres + x0] = color; + } + } +#ifdef CONFIG_FB_UPDATE + struct fb_area_s area; + area.x = x0_s; + area.y = y1 > y0 ? y0 : y1; + area.w = x1_s - x0_s; + area.h = abs(y1 - y0); + + ioctl(fb_fd, FBIO_UPDATE, &area); +#endif + return falseObj; +} + +static OBJ primRect(int argCount, OBJ *args) { + tftInit(); + int X = obj2int(args[0]); + int Y = obj2int(args[1]); + int width = obj2int(args[2]); + int height = obj2int(args[3]); + int fill = (argCount > 5) ? (trueObj == args[5]) : true; + setRenderColor(obj2int(args[4])); + uint16_t *dst = (uint16_t *) fb_mem; + + for (int x = X; x < X + width; x++) + { + if ((x == X) || (x == X + width - 1) || fill) + { + for (int y = Y; y < Y + height; y++) + { + dst[y * vinfo.xres + x] = color; + } + } + else + { + int y = Y; + dst[y * vinfo.xres + x] = color; + y = Y + height - 1; + dst[y * vinfo.xres + x] = color; + } + } +#ifdef CONFIG_FB_UPDATE + struct fb_area_s area; + area.x = X; + area.y = Y; + area.w = width; + area.h = height; + + ioctl(fb_fd, FBIO_UPDATE, &area); +#endif + return falseObj; +} + +static OBJ primCircle(int argCount, OBJ *args) { + tftInit(); + int originX = obj2int(args[0]); + int originY = obj2int(args[1]); + int radius = obj2int(args[2]); + setRenderColor(obj2int(args[3])); + int fill = (argCount > 4) ? (trueObj == args[4]) : true; + uint16_t *dst = (uint16_t *) fb_mem; + + for (int x = originX - radius; x <= originX + radius; x++) + { + int y = originY + sqrt(radius * radius - (x - originX) * (x - originX)); + dst[y * vinfo.xres + x] = color; + y = originY - sqrt(radius * radius - (x - originX) * (x - originX)); + dst[y * vinfo.xres + x] = color; + if (fill) + { + for (int y_tmp = originY - sqrt(radius * radius - (x - originX) * (x - originX)); \ + y_tmp < originY + sqrt(radius * radius - (x - originX) * (x - originX)); \ + y_tmp++) + { + dst[y_tmp * vinfo.xres + x] = color; + } + } + } +#ifdef CONFIG_FB_UPDATE + struct fb_area_s area; + area.x = originX - radius; + area.y = originY - radius; + area.w = 2 * radius + 1; + area.h = 2 * radius + 1; + + ioctl(fb_fd, FBIO_UPDATE, &area); +#endif + return falseObj; +} + +static OBJ primClear(int argCount, OBJ *args) { + tftInit(); + uint16_t *dst = (uint16_t *) fb_mem; + + for (int x = 0; x < vinfo.xres; x++) { + for (int y = 0; y < vinfo.yres; y++) { + dst[y * vinfo.xres + x] = 0; + } + } + +#ifdef CONFIG_FB_UPDATE + struct fb_area_s area; + area.x = 0; + area.y = 0; + area.w = vinfo.xres; + area.h = vinfo.yres; + + ioctl(fb_fd, FBIO_UPDATE, &area); +#endif + return falseObj; +} + +static bool touchEnabled = false; +static int touch_fd; +static int touchX; +static int touchY; +static bool touchDown = false; + +static void initTouch(void) { + if (!touchEnabled) { + int ret = open("/dev/input0", O_RDONLY | O_NONBLOCK); + if (ret < 0) { + perror("Failed to open /dev/input0: "); + abort(); + } + touch_fd = ret; + touchEnabled = true; + } +} + +static void getTouchSample(void) { + struct touch_sample_s sample; + + int ret = read(touch_fd, &sample, sizeof(struct touch_sample_s)); + if (ret < 0 && errno != EWOULDBLOCK && errno != EAGAIN) { + perror("Failed to read() touch sample: "); + abort(); + } else if (ret > 0) { + touchX = sample.point[0].x; + touchY = sample.point[0].y; + if ((sample.point[0].flags & TOUCH_DOWN) || (sample.point[0].flags & TOUCH_MOVE)) { + touchDown = true; + } else { + touchDown = false; + } + } +} + +static OBJ primTftTouched(int argCount, OBJ *args) { + initTouch(); + getTouchSample(); + return touchDown ? trueObj : falseObj; +} + +static OBJ primTftTouchX(int argCount, OBJ *args) { +// initTouch(); +// getTouchSample(); + return int2obj(touchDown ? touchX : -1); +} + +static OBJ primTftTouchY(int argCount, OBJ *args) { +// initTouch(); +// getTouchSample(); + return int2obj(touchDown ? touchY : -1);} + + +// Primitives + +static PrimEntry entries[] = { + {"enableDisplay", primEnableDisplay}, + {"getWidth", primGetWidth}, + {"getHeight", primGetHeight}, + {"setPixel", primSetPixel}, + {"line", primLine}, + {"rect", primRect}, + {"circle", primCircle}, + {"clear", primClear}, + {"tftTouched", primTftTouched}, + {"tftTouchX", primTftTouchX}, + {"tftTouchY", primTftTouchY}, +}; + +void addTFTPrims() { + addPrimitiveSet(TFTPrims, "tft", sizeof(entries) / sizeof(PrimEntry), entries); +} diff --git a/interpreters/smallvm/tinyJSON.c b/interpreters/smallvm/tinyJSON.c new file mode 100644 index 000000000..3d5c3728b --- /dev/null +++ b/interpreters/smallvm/tinyJSON.c @@ -0,0 +1,326 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Copyright 2018 John Maloney, Bernat Romagosa, and Jens Mönig + +// tinyJSON.c - A tiny JSON reader for embedded systems +// John Maloney, September 2018 + +/* +Tiny JSON Reader + +Tiny JSON Reader (TJR) is a lightweight JSON reader written in C that allows clients to +extract information from a JSON string. The JSON string is scanned in place, so TJR +doesn't need to build data structures or allocate memory, and the JSON string is not +mutated, so it can be stored in read-only or write-once Flash memory. These features make +TJR ideal for embedded applications that run on microcontrollers with very limited RAM. + +The simplest way to use TJR is to use the tjr_atPath() function to extract data from the +JSON structure via a dot-delimited path. For example, if jsonData is: + + { "points": + [ + { "x": 1, "y": 2 }, + { "x": 3, "y": 4 } + ] + } + +then tjr_atPath(jsonData "points.1.x") would get the "x" property of the second element of +the array in the "points" property of the top-level object. What tjr_atPath() returns is a +pointer to the character "3" in the original JSON string. That pointer can be passed to +tjr_type() to discover that the value is a number, then to tjr_readInteger() to extract +its value. In general JSON numbers can be floating point numbers with optional fractions +and exponents, but since TJR is intended for embedded applications where floating point +numbers are expensive, it currently only supports extracting integer values. + +TJR also supports enumeration of object properties and array elements, so one can do a +complete traversal of the entire JSON structure if needed. However, using paths to access +parts of the structure is often sufficient. + +Limitations: + * assumes input is legal JSON + * each property name component of a path must be under 100 characters long + * floating point numbers are not supported, only integers + * the \uHHHH hex escape sequence in strings is not supported; it is passed through verbatim +*/ + +#include +#include +#include "tinyJSON.h" + +// helper functions + +static inline int isDigit(char ch) { + return (('0' <= ch) && (ch <= '9')); +} + +static inline char * tjr_skipWhitespace(char *p) { + while ((*p <= ' ') && (*p != 0)) p++; + return p; +} + +static char * tjr_skip(char *p) { + // Return a pointer to the token or value after the current one. + // Skip whitespace both before and after the skipped token or value. + + p = tjr_skipWhitespace(p); + int ch = *p; + if ('\0' == ch) return p; // end of JSON string + p++; + if ('"' == ch) { + while (*p) { + ch = *p++; + if ('"' == ch) return tjr_skipWhitespace(p); // closing quote + if (('\\' == ch) && *p) p++; // skip escaped character + } + } else if ((':' == ch) || (',' == ch)) { + return tjr_skipWhitespace(p); + } else if ('{' == ch) { + p = tjr_skipWhitespace(p); + while (*p) { + if ('}' == *p) return tjr_skipWhitespace(p + 1); + p = tjr_skip(p); + } + } else if ('[' == ch) { + p = tjr_skipWhitespace(p); + while (*p) { + if (']' == *p) return tjr_skipWhitespace(p + 1); + p = tjr_skip(p); + } + } else { // number, true, false, or null + while (*p) { + ch = *p; + if (ch <= ' ') return tjr_skipWhitespace(p); + if ((',' == ch) || ('}' == ch) || (']' == ch) || ('"' == ch)) return p; + p++; + } + } + return tjr_skipWhitespace(p); +} + +static char * tjr_atPropName(char *p, char *propName, int propNameLen) { + // Return a pointer to the value of the property with the given name or NULL if not found. + + char s[100]; + if ('{' != *p) return NULL; + p++; // skip '{' + while (1) { + p = tjr_nextProperty(p, s, sizeof(s)); + if (!p) return NULL; // no more properties + if (0 == strncmp(s, propName, propNameLen)) return p; + p = tjr_skip(p); // skip value + } +} + +// accessing the JSON structure by path or index + +char * tjr_atIndex(char *p, int index) { + // Return a pointer to the index-th element of the (one-based) array at p. + // Return NULL if p is not the start of an array or if index is out of range. + + if ((index <= 0) || (*p != '[')) return NULL; + p = tjr_skipWhitespace(p + 1); // skip '[' + int i = 1; + while (1) { + if (i == index) return p; + p = tjr_nextElement(p); + if (!p) return NULL; // no more elements + i++; + } +} + +char * tjr_atPath(char *p, char *pathString) { + // Return a pointer to the value at the given dot-delimited path or NULL if not found. + // The path string consists of a sequence of property names and/or array indices + // separated by dots (periods), such as "shape.points.1.x". + + char *propName = pathString; + while (*propName) { + char *nextDot = strchr(propName, '.'); + int propNameLen = nextDot ? (nextDot - propName) : strlen(propName); + if (isDigit(*propName)) { + int index = tjr_readInteger(propName); + p = tjr_atIndex(p, index); + } else { + p = tjr_atPropName(p, propName, propNameLen); + } + if (!p || !nextDot) return p; + propName += propNameLen + 1; // advance to start of next path component + } + return p; +} + +char * tjr_valueAt(char *p, int index) { + // Return the value at the given (one-based) index in the array or object p. + // Return NULL p is not an array or object or the index is out of range. + + if (index < 1) return NULL; + int itemType = tjr_type(p); + if (tjr_Array == itemType) { + p++; // skip '[' + for (; index > 1; index--) p = tjr_nextElement(p); + return p; + } + if (tjr_Object == itemType) { + p++; // skip '{' + p = tjr_nextProperty(p, NULL, 0); + for (; index > 1; index--) { + p = tjr_nextElement(p); // skip value + p = tjr_nextProperty(p, NULL, 0); + } + return p; + } + return NULL; // not an object or array +} + +char * tjr_keyAt(char *p, int index, char *key, int keySize) { + // Return set the key the return the value at the given (one-based) index in the object p. + // Return NULL if p is not an object or if the index is out of range. + + if (index < 1) return NULL; + if (tjr_Object != tjr_type(p)) return NULL; + p++; // skip '{' + p = tjr_nextProperty(p, key, keySize); + for (; index > 1; index--) { + p = tjr_nextElement(p); // skip value + p = tjr_nextProperty(p, key, keySize); + } + return p; +} + +// types and values + +int tjr_type(char *p) { + // Return the type of the value at p (after any leading whitespace). + + if (!p) return tjr_End; // null pointer + + p = tjr_skipWhitespace(p); + int ch = *p; + if ('"' == ch) return tjr_String; + if ('{' == ch) return tjr_Object; + if ('[' == ch) return tjr_Array; + if (isDigit(ch) || (('-' == ch) && isDigit(*(p + 1)))) return tjr_Number; + if ('t' == ch) return tjr_True; + if ('f' == ch) return tjr_False; + if ('n' == ch) return tjr_Null; + if ('\0' == ch) return tjr_End; + return tjr_Error; +} + +int tjr_readInteger(char *p) { + // Read the number at p as an integer. If the JSON contains a floating point number, + // the mantissa is read as an integer and the fraction and exponent are ignored. + + p = tjr_skipWhitespace(p); + int result = 0; + int sign = 1; + if ('-' == *p) { sign = -1; p++; } + while (isDigit(*p)) { + result = (10 * result) + (*p++ - '0'); + } + return sign * result; +} + +void tjr_readStringInto(char *p, char *dstString, int dstSize) { + // Read the string at p into dstString. + // dstString must be a writeable string with size > 0 (i.e. not NULL). + + p = tjr_skipWhitespace(p); + if ('"' != *p) { // not a string + *dstString = '\0'; + return; + } + int spaceAvailable = dstSize - 1; // reserve space for null terminator + p++; // skip opening quote + while (1) { + int ch = *p++; + if (('"' == ch) || ('\0' == ch)) { + *dstString = '\0'; + return; + } + if ('\\' == ch) { + // Note: the \uHHHH escape is not handled; it is passed through unchanged + ch = *p++; + if ('b' == ch) ch = '\b'; + if ('f' == ch) ch = '\f'; + if ('n' == ch) ch = '\n'; + if ('r' == ch) ch = '\r'; + if ('t' == ch) ch = '\t'; + } + if (spaceAvailable <= 0) { + *dstString = '\0'; + return; + } + *dstString++ = ch; + spaceAvailable--; + } +} + +char * tjr_endOfItem(char *p) { + // Return a pointer to the first character following the JSON item pointed to by p. + + char *result = tjr_skip(p); + while ((result > p) && (*result <= 32) && (*result != 0)) result--; // back up over whitespace, if any + return result; +} + +// enumeration + +int tjr_count(char *p) { + // If p is the start of an object, returh the number of properties it has. + // If p is the start of an array, returh the number of elements it has. + // If p is not an object or array, return 0. + + if (!p) return 0; // null pointer + + int count = 0; + p = tjr_skipWhitespace(p); + if ('{' == *p) { + p++; // skip '{' + while (1) { + p = tjr_nextProperty(p, NULL, 0); + if (!p) return count; + p = tjr_skip(p); // skip value + count++; + } + } else if ('[' == *p) { + p++; // skip '[' + while (1) { + p = tjr_nextElement(p); + if (!p) return count; + count++; + } + } + return count; +} + +char * tjr_nextElement(char *p) { + // Skip to the next array or object field. Return NULL when there are no more elements. + + if (!p || !*p) return NULL; + p = tjr_skipWhitespace(p); + if ((']' == *p) || ('}' == *p)) return NULL; + p = tjr_skip(p); // skip current element + if (',' == *p) p++; // skip comma + p = tjr_skipWhitespace(p); + return p; +} + +char * tjr_nextProperty(char *p, char *propertyName, int propertyNameSize) { + // Skip to the next object property value and copy the property name into + // the optional propertyName string. At most propertyNameSize characters will + // be written to the propertyName string (including the null terminator). + // Return NULL when there are no more properties. + + if (propertyName) propertyName[0] = '\0'; // clear property name + if (!p || !*p) return NULL; + p = tjr_skipWhitespace(p); + if (',' == *p) p = tjr_skipWhitespace(p + 1); // skip comma after last value + if ('}' == *p) return NULL; + if (propertyName) tjr_readStringInto(p, propertyName, propertyNameSize); // record property name + p = tjr_skip(p); // skip property name + if (':' == *p) p = tjr_skipWhitespace(p + 1); // skip colon + return p; +} diff --git a/interpreters/smallvm/tinyJSON.h b/interpreters/smallvm/tinyJSON.h new file mode 100644 index 000000000..a3a993143 --- /dev/null +++ b/interpreters/smallvm/tinyJSON.h @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Copyright 2018 John Maloney, Bernat Romagosa, and Jens Mönig + +// tinyJSON.h - A tiny JSON reader for embedded systems +// John Maloney, September 2018 + +#ifdef __cplusplus +extern "C" { +#endif + +// JSON Types returned by tjr_type() + +enum { + tjr_Error = -1, + tjr_End = 0, // end of JSON string + tjr_Array = 1, + tjr_Object = 2, + tjr_Number = 3, + tjr_String = 4, + tjr_True = 5, + tjr_False = 6, + tjr_Null = 7 +}; + +// JSON structure access and value extraction by path + +char * tjr_atPath(char *p, char *pathString); +char * tjr_valueAt(char *p, int index); +char * tjr_keyAt(char *p, int index, char *key, int keySize); + +int tjr_type(char *p); +int tjr_readInteger(char *p); +void tjr_readStringInto(char *p, char *dstString, int dstSize); +char * tjr_endOfItem(char *p); + +// Object/Array enumeration + +int tjr_count(char *p); +char * tjr_atIndex(char *p, int index); + +char * tjr_nextElement(char *p); +char * tjr_nextProperty(char *p, char *propertyName, int propertyNameSize); + +#ifdef __cplusplus +} +#endif diff --git a/interpreters/smallvm/varPrims.c b/interpreters/smallvm/varPrims.c new file mode 100644 index 000000000..f057a9262 --- /dev/null +++ b/interpreters/smallvm/varPrims.c @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Copyright 2020 John Maloney, Bernat Romagosa, and Jens Mönig + +// varPrims.c - Basic introspection primitives for variables +// Bernat Romagosa, March 2020 + +#include +#include +#include + +#include "mem.h" +#include "interp.h" +#include "persist.h" + +static OBJ primVarExists (int argCount, OBJ *args) { + return indexOfVarNamed(obj2str(args[0])) > - 1 ? trueObj : falseObj; +} + +static OBJ primVarNamed (int argCount, OBJ *args) { + int index = indexOfVarNamed(obj2str(args[0])); + if (index > -1) { + return vars[index]; + } + return int2obj(0); +} + +static OBJ primSetVarNamed (int argCount, OBJ *args) { + int index = indexOfVarNamed(obj2str(args[0])); + if (index > -1) { + vars[index] = args[1]; + } + return falseObj; +} + +OBJ primVarNameForIndex(int argCount, OBJ *args) { + // Returns the variable name for the given (one-based) index. + // If a variable with that index is not found, return the highest index. + // Pass -1 as the index to get the number of global variables. + + int varIndex = ((argCount > 0) && isInt(args[0])) ? obj2int(args[0]) - 1 : -1; + + int maxVarIndex = -1; + char *varEntry = NULL; + int *p = scanStart(); + while (p) { + int recType = (*p >> 16) & 0xFF; + int id = (*p >> 8) & 0xFF; + if (recType == varName) { + if (id > maxVarIndex) maxVarIndex = id; + if (varIndex == id) varEntry = (char *) (p + 2); + } else if (recType == varsClearAll) { + maxVarIndex = -1; + varEntry = NULL; + } + p = recordAfter(p); + } + if (varEntry) return newStringFromBytes(varEntry, strlen(varEntry)); + return int2obj(maxVarIndex + 1); +} + +// Primitives + +static PrimEntry entries[] = { + {"varExists", primVarExists}, + {"varNamed", primVarNamed}, + {"setVarNamed", primSetVarNamed}, + {"varNameForIndex", primVarNameForIndex}, +}; + +void addVarPrims() { + addPrimitiveSet(VarPrims, "vars", sizeof(entries) / sizeof(PrimEntry), entries); +} diff --git a/interpreters/smallvm/version.h b/interpreters/smallvm/version.h new file mode 100644 index 000000000..3c1cc821b --- /dev/null +++ b/interpreters/smallvm/version.h @@ -0,0 +1 @@ +#define VM_VERSION "v377" diff --git a/testing/drivers/drivertest/drivertest_framebuffer.c b/testing/drivers/drivertest/drivertest_framebuffer.c index cf57d3f8a..2aecabd7e 100644 --- a/testing/drivers/drivertest/drivertest_framebuffer.c +++ b/testing/drivers/drivertest/drivertest_framebuffer.c @@ -367,10 +367,10 @@ static void draw_rect(FAR struct fb_state_s *fb_state, int x, int y, #ifdef CONFIG_FB_UPDATE int ret; struct fb_area_s area; - area.x = 0; - area.y = 0; - area.w = xres; - area.h = yres; + area.x = x; + area.y = y; + area.w = w; + area.h = h; #endif for (j = y; j <= (y + h - 1) && j < yres; j++)