diff --git a/Makefile b/Makefile index 661e3d0..29d5da0 100644 --- a/Makefile +++ b/Makefile @@ -1,30 +1,39 @@ -TARGET := libevicsdk -TARGET_CRT0 := $(TARGET)_crt0 - -# We make the following assumptions on Windows: -# arm-none-eabi gcc and binutils are compiled for Windows, -# so if you are using Cygwin, we will need path translations -# NUVOSDK must be lazily evaluated, so that we can later -# change EVICSDK when building include paths. - -# Small fix to bug where cygpath -w mistranslates paths with mixed slashes (/, \) -EVICSDK := $(subst \,/,$(EVICSDK)) -NUVOSDK = $(EVICSDK)/nuvoton-sdk/Library - -OBJS := $(NUVOSDK)/Device/Nuvoton/M451Series/Source/system_M451Series.o \ - $(NUVOSDK)/StdDriver/src/clk.o \ - $(NUVOSDK)/StdDriver/src/fmc.o \ - $(NUVOSDK)/StdDriver/src/gpio.o \ - $(NUVOSDK)/StdDriver/src/spi.o \ - $(NUVOSDK)/StdDriver/src/sys.o \ - $(NUVOSDK)/StdDriver/src/timer.o \ - $(NUVOSDK)/StdDriver/src/rtc.o \ - $(NUVOSDK)/StdDriver/src/usbd.o \ - $(NUVOSDK)/StdDriver/src/eadc.o \ - $(NUVOSDK)/StdDriver/src/pwm.o \ +# This file is part of eVic SDK. +# +# eVic SDK is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# eVic SDK is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with eVic SDK. If not, see . +# +# Copyright (C) 2016 ReservedField + +include $(EVICSDK)/make/Helper.mk +include $(EVICSDK)/make/Common.mk + +# Output targets. +TARGET_SDK := libevicsdk +TARGET_NUVO := libnuvosdk +TARGET_CRT0 := evicsdk-crt0 + +# SDK objects that are always compiled as no-FPU. +OBJS_SDK_NOFPU := \ + src/thread/Thread.o \ + src/thread/Queue.o + +# SDK objects. +OBJS_SDK := \ src/startup/initfini.o \ src/startup/sbrk.o \ src/startup/init.o \ + src/startup/mainthread.o \ src/startup/sleep.o \ src/sysinfo/SysInfo.o \ src/dataflash/Dataflash.o \ @@ -39,150 +48,196 @@ OBJS := $(NUVOSDK)/Device/Nuvoton/M451Series/Source/system_M451Series.o \ src/usb/USB_VirtualCOM.o \ src/adc/ADC.o \ src/battery/Battery.o \ - src/atomizer/Atomizer.o + src/atomizer/Atomizer.o \ + $(OBJS_SDK_NOFPU) -TAGNAME := src/startup/evicsdk_tag -OBJS_CRT0 := src/startup/startup.o \ - $(TAGNAME).o - -AEABI_OBJS := src/aeabi/aeabi_memset-thumb2.o \ +# SDK objects to build in case of missing __aeabi functions. +OBJS_SDK_AEABI := \ + src/aeabi/aeabi_memset-thumb2.o \ src/aeabi/aeabi_memclr.o -OUTDIR := lib -DOCDIR := doc - -CPU := cortex-m4 - -# We need to find out if on cygwin or not -ifeq ($(OS),Windows_NT) - ifeq (, $(findstring cygwin, $(shell gcc -dumpmachine))) - WIN_CYG := 0 - else - WIN_CYG := 1 - endif - -endif - -ifeq ($(shell $(CC) -v 2>&1 | grep -c "clang version"), 1) - CC_IS_CLANG := 1 +# Nuvoton SDK roots (relative). +NUVOSDK_LOCAL := nuvoton-sdk/Library +NUVOSDK_DEVSRC := $(NUVOSDK_LOCAL)/Device/Nuvoton/M451Series/Source +NUVOSDK_STDSRC := $(NUVOSDK_LOCAL)/StdDriver/src + +# Nuvoton SDK objects. +OBJS_NUVO := \ + $(NUVOSDK_DEVSRC)/system_M451Series.o \ + $(NUVOSDK_STDSRC)/clk.o \ + $(NUVOSDK_STDSRC)/fmc.o \ + $(NUVOSDK_STDSRC)/gpio.o \ + $(NUVOSDK_STDSRC)/spi.o \ + $(NUVOSDK_STDSRC)/sys.o \ + $(NUVOSDK_STDSRC)/timer.o \ + $(NUVOSDK_STDSRC)/rtc.o \ + $(NUVOSDK_STDSRC)/usbd.o \ + $(NUVOSDK_STDSRC)/eadc.o \ + $(NUVOSDK_STDSRC)/pwm.o + +# SDK tag object file. +SDKTAG_OBJ := src/startup/sdktag.o + +# Crt0 objects. +OBJS_CRT0 := \ + src/startup/startup.o \ + src/thread/ContextSwitch.o \ + $(SDKTAG_OBJ) \ + $(if $(EVICSDK_FPU_DISABLE),,src/thread/UsageFault_fpu.o) + +# Crt0 objects only compiled in debug builds. +OBJS_CRT0_DBG := src/startup/fault.o + +# Extra C/C++ flags for SDK/crt0 compilation. +# TODO: fixup warnings for GCC and Clang before enabling this. +# Also need -Wno-bitwise-op-parentheses for Clang + Nuvo SDK. +#CCFLAGS_EXTRA := -Wall + +# Find path to libc. Using find would be better, but on Cygwin we'd have to +# convert the native style library paths to Cygwin style. This hack avoids it +# by (ab)using binutils to do the search. +LIBC_PATH_FIXBU := $(shell \ + arm-none-eabi-ld --verbose $(foreach d,$(ARM_LIBDIRS_FIXBU),-L$d) -lc \ + $(NULLDEV_TC) 2>&1 | \ + sed -n 's/attempt to open \(.*libc\.a\) succeeded/\1/p' | head -1) +ifndef LIBC_PATH_FIXBU +$(error Could not detect libc path) endif - -ifeq ($(ARMGCC),) - ARMGCC := $(shell cd $(shell arm-none-eabi-gcc --print-search-dir | grep 'libraries' | \ - tr '=$(if $(filter Windows_NT,$(OS)),;,:)' '\n' | \ - grep -E '/arm-none-eabi/lib/?$$' | head -1)/../.. && pwd) -endif - -ifeq ($(OS),Windows_NT) - # Always fix binutils path - ifneq ($(ARMGCC),) - # If using cygwin, use cygpath - ifeq ($(WIN_CYG),1) - ARMGCC := $(shell cygpath -w $(ARMGCC)) - endif - - endif - - ifndef CC_IS_CLANG - NEED_FIXPATH := 1 - endif -endif - -ifneq ($(ARMGCC),) - ifdef CC_IS_CLANG - CFLAGS += -target armv7em-none-eabi -fshort-enums - - AEABI_COUNT := $(shell arm-none-eabi-nm -g $(ARMGCC)/arm-none-eabi/lib/armv7e-m/libc.a | grep -Ec 'T __aeabi_mem(set|clr)[48]?$$') - ifeq ($(AEABI_COUNT), 0) - # __aeabi_memset* and __aeabi_memclr* are not exported by libc - # We provide our own implementations - OBJS += $(AEABI_OBJS) - else ifneq ($(AEABI_COUNT), 6) - # Only part of __aeabi_memset* and __aeabi_memclr* are exported by libc - # This should never happen, bail out in env_check - AEABI_ERROR := 1 - endif - else - CC := arm-none-eabi-gcc - endif - - ifdef NEED_FIXPATH - ifeq ($(WIN_CYG), 0) - OBJS_FIXPATH := $(OBJS) - OBJS_CRT0_FIXPATH := $(OBJS_CRT0) - else - OBJS_FIXPATH := $(shell cygpath -w $(OBJS)) - OBJS_CRT0_FIXPATH := $(shell cygpath -w $(OBJS_CRT0)) - EVICSDK := $(shell cygpath -w $(EVICSDK)) - endif - else - OBJS_FIXPATH := $(OBJS) - OBJS_CRT0_FIXPATH := $(OBJS_CRT0) - endif +ifdef EVICSDK_MAKE_DEBUG +$(info Libc path (raw): $(LIBC_PATH_FIXBU)) +$(info Libc path (canonical): \ + $(call path-canon,$(call unfixpath-tc,$(LIBC_PATH_FIXBU)))) endif -ifeq ($(WIN_CYG),0) - SDKTAG := $(shell git describe --abbrev --dirty --always --tags 2> NUL ) # Fix for Windows w/o cygwin (NUL instead of /dev/null) -else - SDKTAG := $(shell git describe --abbrev --dirty --always --tags 2> /dev/null ) +# Old newlib versions don't have __aeabi_memset* and __aeabi_memclr* (added in +# commit 24e054c). If this is the case, we bake our __aeabi objects into the +# SDK to avoid undefined references (especially with Clang). +AEABI_COUNT := $(shell arm-none-eabi-nm -g $(LIBC_PATH_FIXBU) | \ + grep -Ec 'T __aeabi_mem(set|clr)[48]?$$') +ifndef AEABI_COUNT +$(error Could not detect state of __aeabi symbols) endif -ifeq ($(SDKTAG),) - SDKTAG := unknown +$(if $(EVICSDK_MAKE_DEBUG),$(info __aeabi count: $(AEABI_COUNT))) +ifeq ($(AEABI_COUNT),0) + OBJS_SDK += $(OBJS_SDK_AEABI) +else ifneq ($(AEABI_COUNT),6) +$(error Libc is exporting only part of __aeabi symbols) endif -AS := arm-none-eabi-as -LD := arm-none-eabi-ld -AR := arm-none-eabi-ar -OBJCOPY := arm-none-eabi-objcopy +# Generates the SDK tag from git. Since this is an expensive operation, the +# result is cached after the first invocation. +get-sdktag = $(or $(__SDKTAG),$(eval __SDKTAG := $(__get-sdktag))$(__SDKTAG)) +__get-sdktag = evic-sdk-$(or $(strip $(shell \ + git describe --abbrev --dirty --always --tags 2> $(NULLDEV))),unknown) +$(if $(EVICSDK_MAKE_DEBUG),$(info SDK tag: $(get-sdktag))) -INCDIRS := $(foreach d,$(shell arm-none-eabi-gcc -x c -v -E /dev/null 2>&1 | sed -n -e '/<\.\.\.>/,/End/ p' | tail -n +2 | head -n -1 | sed 's/^\s*//'),-I$d) \ - -I$(NUVOSDK)/CMSIS/Include \ - -I$(NUVOSDK)/Device/Nuvoton/M451Series/Include \ - -I$(NUVOSDK)/StdDriver/inc \ - -Iinclude +# All objects, excluding debug-only objects. +OBJS_ALL := $(OBJS_SDK) $(OBJS_NUVO) $(OBJS_CRT0) +# All debug-only objects. +OBJS_ALL_DBG := $(OBJS_CRT0_DBG) -CFLAGS += -Wall -mcpu=$(CPU) -mthumb -Os -fdata-sections -ffunction-sections -CFLAGS += $(INCDIRS) - -ASFLAGS := -mcpu=$(CPU) - -all: env_check gen_tag $(TARGET_CRT0).o $(TARGET).a - -%.o: %.c - $(CC) $(CFLAGS) -c $< -o $@ - -%.o: %.s - $(AS) $(ASFLAGS) -o $@ $< - -$(TARGET).a: $(OBJS_FIXPATH) - test -d $(OUTDIR) || mkdir $(OUTDIR) - $(AR) -rv $(OUTDIR)/$(TARGET).a $(OBJS_FIXPATH) - -$(TARGET_CRT0).o: $(OBJS_CRT0_FIXPATH) - test -d $(OUTDIR) || mkdir $(OUTDIR) - $(LD) -r $(OBJS_CRT0_FIXPATH) -o $(OUTDIR)/$(TARGET_CRT0).o +# Documentation output directory. +DOCDIR := doc +# Output directory clean template. +clean-sdkdir-tmpl = $(call clean-dir-tmpl,$1,$2,$(SDKDIR)) +# Library output path template. Extra argument: library name. +lib-tmpl = $(call sdkdir-tmpl,$1,$2)/$3.a +# SDK library output path template. +sdk-tmpl = $(call lib-tmpl,$1,$2,$(TARGET_SDK)) +# Nuvoton SDK library output path template. +nuvo-tmpl = $(call lib-tmpl,$1,$2,$(TARGET_NUVO)) +# Crt0 object output path template +crt0-tmpl = $(call sdkdir-tmpl,$1,$2)/$(TARGET_CRT0).o + +# Object targets for all devices and flavors. +objs-all := $(call tmpl-all,objs-tmpl,$(OBJS_ALL)) \ + $(call tmpl-flavor,objs-tmpl,$(BUILD_FLAVOR_DBG),$(OBJS_ALL_DBG)) +# SDK library output paths for all devices and flavors. +sdk-all := $(call tmpl-all,sdk-tmpl) +# Nuvoton SDK library output paths for all device and flavors. +nuvo-all := $(call tmpl-all,nuvo-tmpl) +# All library output paths for all devices and flavors. +lib-all := $(sdk-all) $(nuvo-all) +# Crt0 object output paths for all devices and flavors. +crt0-all := $(call tmpl-all,crt0-tmpl) +# Crt0 object output paths for all devices, debug flavor. +crt0-dbg := $(call tmpl-flavor,crt0-tmpl,$(BUILD_FLAVOR_DBG)) +# No-FPU objects for all devices and flavors. +nofpu-objs-all := $(call tmpl-all,objs-tmpl,$(OBJS_SDK_NOFPU)) +# SDK tag object for all devices and flavors. +sdktag-all := $(call tmpl-all,objs-tmpl,$(SDKTAG_OBJ)) + +# Cache all needed paths for fixpath. +$(call fixpath-cache, \ + $(call objs-fixpath-cache,$(OBJS_ALL)) \ + $(call objs-fixpath-cache,$(OBJS_ALL_DBG),$(BUILD_FLAVOR_DBG)) \ + $(lib-all) $(crt0-all)) + +# Add outputs to clean templates. +CLEAN_PATH_TMPL += clean-sdkdir-tmpl + +# Enable secondary expansion. +.SECONDEXPANSION: + +# Rule to archive prerequisite objects into a library. +$(lib-all): | $$(@D) + $(call info-cmd,LIB) + @$(call trace, \ + $(AR) -rc $(call fixpath-bu,$@) $(call fixpath-bu,$^)) + +# Build SDK objects for SDK library. +$(sdk-all): $$(call tmpl-build,objs-tmpl,$$(OBJS_SDK)) + +# Build Nuvoton SDK objects for Nuvoton SDK library. +$(nuvo-all): $$(call tmpl-build,objs-tmpl,$$(OBJS_NUVO)) + +# Rule to link crt0 objects into a partially linked object. +$(crt0-all): $$(call tmpl-build,objs-tmpl,$$(OBJS_CRT0)) | $$(@D) + $(call info-cmd,LNK) + @$(call trace, \ + $(LD) -r $(call fixpath-bu,$^) -o $(call fixpath-bu,$@)) +# Build debug-only crt0 objects for debug builds. +$(crt0-dbg): $$(call tmpl-build,objs-tmpl,$$(OBJS_CRT0_DBG)) + +# Define EVICSDK_SDKTAG for the SDK tag target (asm). +$(sdktag-all): ASFLAGS += -DEVICSDK_SDKTAG=\"$(get-sdktag)\" +# Always rebuild SDK tag (.PHONY doesn't play well with pattern rules). +$(sdktag-all): .FORCE + +# Build no-FPU objects with no-FPU CPU flags. +$(nofpu-objs-all): CPUFLAGS := $(CPUFLAGS_NOFPU) + +# Set extra C/C++ flags for SDK/crt0 objects. +# TODO: see CCFLAGS_EXTRA. +#$(sdk-all) $(crt0-all): CFLAGS += $(CCFLAGS_EXTRA) +#$(sdk-all) $(crt0-all): CXXFLAGS += $(CCFLAGS_EXTRA) + +# Device-flavor target: build all outputs. +$(devfla-all): \ + $$(call tmpl-build,crt0-tmpl) \ + $$(call tmpl-build,nuvo-tmpl) \ + $$(call tmpl-build,sdk-tmpl) + +# Rule to build documentation via doxygen. docs: doxygen -clean: - rm -rf $(OBJS) $(OBJS_CRT0) $(AEABI_OBJS) $(OUTDIR)/$(TARGET).a $(OUTDIR)/$(TARGET_CRT0).o $(OUTDIR) $(DOCDIR) +# Rule to clean documentation. +clean-docs: + rm -rf $(DOCDIR) -env_check: -ifeq ($(ARMGCC),) - $(error You must set the ARMGCC environment variable) -endif -ifneq ($(AEABI_ERROR),) - $(error Your libc is exporting only part of __aeabi symbols) -endif +# Set BUILD_* for output targets. +$(call build-vars-rules,sdk-tmpl) +$(call build-vars-rules,nuvo-tmpl) +$(call build-vars-rules,crt0-tmpl) -gen_tag: - @rm -f $(TAGNAME).s $(TAGNAME).o -ifeq ($(WIN_CYG),0) - @printf ".section .evicsdk_tag\n.asciz \"evic-sdk-$(SDKTAG)\"\n" > $(TAGNAME).s -else - @printf '.section .evicsdk_tag\n.asciz "evic-sdk-$(SDKTAG)"\n' > $(TAGNAME).s -endif +# Generate directory targets. +$(call mkdir-rules,objs-dirs-tmpl,$(OBJS_ALL) $(OBJS_ALL_DBG)) +$(call mkdir-rules,sdkdir-tmpl) +# Set object directories as order-only prerequisites for object targets. +$(objs-all): | $$(@D) -.PHONY: all clean docs env_check gen_tag +.FORCE: +.PHONY: .FORCE docs diff --git a/README.md b/README.md index 2f05a33..d67d65c 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,9 @@ brew install gcc-arm-none-eabi On Windows, first install the [precompiled ARM toolchain](https://launchpad.net/gcc-arm-embedded). Choose an installation path without spaces to avoid problems with the build process. -If you already have a working Windows `make`, along with utilities such as `awk`, `grep`, `sed`, -`tr`, `git`, etc. you can go ahead, but make sure those binaries are in your `PATH`. Otherwise, -install [Cygwin](https://www.cygwin.com/) and add the following packages on top of the base install: +If you already have a working Windows `make` and `git` along with standard GNU utilities you can go +ahead, but make sure those binaries are in your `PATH`. Otherwise, install [Cygwin](https://www.cygwin.com/) +and add the following packages on top of the base install: ``` make git @@ -101,8 +101,8 @@ Installation from Nuvoton and copy the `Library` folder inside `evic-sdk/nuvoton-sdk`, as to have `evic-sdk/nuvoton-sdk/Library`. -3. Point the `EVICSDK` environment variable to the `evic-sdk` folder. This should do (assuming your - current directory is evic-sdk): +3. Point the `EVICSDK` environment variable to the `evic-sdk` folder. For example, if you're using + bash and are in the SDK directory: ``` echo "export EVICSDK=$(pwd)" >> $HOME/.bashrc ``` @@ -113,45 +113,25 @@ Installation make ``` -At this point, the SDK should be fully set up. You can also generate Doxygen documentation with: -``` -make docs -``` -To clean up the build (for example if you want to do a full rebuild), use the standard: -``` -make clean -``` - Building your first APROM -------------------------- The `helloworld` example should be the first thing you try compiling and flashing, -to check that everything is working correctly. -Building is as easy as: +to check that everything is working correctly. Build it like: ``` cd example/helloworld make ``` -To clean you can use `make clean`, as usual. -If the build succeeds, you should now have a `bin/helloworld.bin` file ready to flash. +If the build succeeds, you should now have a `bin/rel/DEVICE/helloworld.bin` file ready to flash. This file is encrypted and compatible with the official updater. -You can also generate a unencrypted binary: -``` -make helloworld_unencrypted.bin -``` Flashing -------- You can flash the output binary using the official updater. For development, -using `python-evic` is quicker and simpler. -I suggest to backup your dataflash before flashing, in case anything goes south: -``` -evic-usb dump-dataflash -o data.bin +using `python-evic` is quicker and simpler: ``` -Now, flash: -``` -evic-usb upload bin/helloworld.bin +evic-usb upload bin/rel/DEVICE/helloworld.bin ``` If everything went well you should see the "Hello, World." message. @@ -163,14 +143,224 @@ over USB is more convenient (as long as the APROM doesn't require significant po it doesn't fire the atomizer). Similiarly, holding the left button during powerup will force the system to boot from APROM. -If `python-evic` fails and the eVic won't flash back to a functioning state, don't panic. -Find a Windows/Mac machine (or virtualize one), boot the eVic to LDROM and flash an original -firmware using the official Joyetech updater. It has always worked for me. +Unless you're messing with the LDROM, this is practically unbrickable - you can always boot to +LDROM and restore. Actually, APROM update is always done from LDROM: the official APROM reboots +to LDROM and flashing happens from there. + +The build system +---------------- + +The build system, used both by the SDK and APROMs, works around *specifiers*. A specifier is a +string in the form `device-flavor`, where `device` is a supported device and `flavor` is a build +flavor. + +**Supported devices:** + +- `evic`: Joyetech eVic VTC Mini +- `all`: all of the above + +**Build flavors:** + +- `dbg`: debug build +- `rel`: release build +- `all`: all of the above + +See below for the differences between debug and release builds. The `all-all` specifier is also +aliased to `all`. There are two classes of make targets: + +- `specifier`: performs a build for the specified device and flavor +- `clean-specifier`: cleans the build for the specified device and flavor + +Some examples (if more than one command is given, they are alternatives): +``` +# Build everything +make all +make all-all +# Clean everything +make clean-all +make clean-all-all +# Build for eVic VTC Mini, debug flavor +make evic-dbg +# Build for eVic VTC Mini, all flavors +make evic-all +# Clean for eVic VTC Mini, release flavor +make clean-evic-rel +# Build for all devices, release flavor +make all-rel +# Clean for all devices, debug flavor +make clean-all-dbg +# Mix & match +make all-rel evic-dbg +``` + +#### Default targets + +While the specifier system lets you specify exactly what you want, it can feel too verbose for +day-to-day development. For this reason, the build system accepts two environment variables: + +- `EVICSDK_MAKE_DEFAULT_DEVICE`: default device(s) +- `EVICSDK_MAKE_DEFAULT_FLAVOR`: default flavor(s) + +Those can be set to a single device/flavor or to a list of them (for example, default flavors +`dbg rel` and `all` are the same). You can omit the device, the flavor or both from any specifier +and they will be filled in from those variables. Unset or empty variables default to `all`. The +empty target is also aliased to `def`. Some examples: +``` +# Build for default device(s) and flavor(s) +make +make def +# Clean for default device(s) and flavor(s) +make clean +# Build for default device(s), debug flavor +make dbg +# Build for eVic VTC Mini, default flavor(s) +make evic +# Clean for default devices(s), release flavor +make clean-rel +``` +If you're writing scripts around the build system don't assume the user's defaults and use the +full specifiers (unless you actually want to use the user's defaults). + +#### Object files + +Objects files for a specific device and flavor live in the `obj/FLAVOR/DEVICE` directory. The +source tree is replicated in there, to avoid conflicts between files with the same name. If +you wanted to compile the file `src/foo/bar.c` for an eVic VTC Mini, debug flavor, you could +issue `make obj/dbg/evic/src/foo/bar.o`. + +#### SDK builds + +SDK output files live in the `lib/FLAVOR/DEVICE` directory. Debug builds enable debug info +generation from the compiler and a fault handler that will display crash and register info, which +can be decoded using the script in `tools/fault-decode`. -Unless you're messing with the LDROM, this is practically unbrickable - you can always boot -to LDROM and restore. Actually, APROM update is always done from LDROM - the official firmware -doesn't even contain flash writing routines, it only provides access to the dataflash and the -actual APROM upload happens in LDROM after a reset. +You can generate Doxygen documentation (`doc` directory) for the SDK using `make docs`. To clean it +use `make clean-docs`. + +#### APROM builds + +APROM output files live in the `bin/FLAVOR/DEVICE` directory. The standard Makefile for APROMs +follows this scheme: +``` +TARGET = helloworld +OBJS = main.o +include $(EVICSDK)/make/Base.mk +``` +`TARGET` is the base name for outputs. `OBJS` is a list of objects to be built. Notice that, even +though objects live in `obj/FLAVOR/DEVICE`, the object list is written as if they were in the same +directory as the sources. For example, `src/foo/bar.c` would appear as `src/foo/bar.o` in the list. +This is done so that you can let the build system worry about devices and flavors (and to preserve +compatibility with Makefiles written for the old build system). Since the source tree is replicated +for the objects, **always use relative paths without `..` components** (you can't have sources at +a higher level than the Makefile). Finally, `make/Base.mk` has to be included (**after** setting +those variables). See the top of `make/Base.mk` for more variables you can use. + +The main output is `bin/FLAVOR/DEVICE/TARGET.bin`, which is an encrypted binary compatible with the +official updater. Debug builds enable debug info generation from the compiler and generate some +additional outputs: + +- `bin/dbg/DEVICE/TARGET.elf`: ELF output from compilation +- `bin/dbg/DEVICE/TARGET_dec.bin`: unencrypted binary +- `bin/dbg/DEVICE/TARGET.map`: linker map + +Also, they are built against the debug SDK, so the fault handler is enabled. All the outputs except +the linker map are targets (so you could `make bin/dbg/DEVICE/TARGET.elf`, for example). + +Note that APROMs for a certain device and flavor combination are built against the SDK for that +same device and flavor, so you'll need to have that SDK built for it to succeed. If you see linker +errors about missing `evicsdk-crt0.o`, `-levicsdk` or `-lnuvosdk`, or you get +`No SDK found for DEVICE-FLAVOR`, chances are you don't have the needed SDK built. + +#### Parallel make + +Parallel make (`-j` option) is supported and works as you would expect. However, problems can arise +when you mix build and clean targets. For example, a default rebuild can be performed with +`make clean && make`. For single-threaded make this could be shortened to `make clean def`, because +targets will be made one at a time from left to right. With parallel make you will need to use the +full form because the two targets would likely be made concurrently, breaking the build. Of course, +mixing multiple build targets or multiple clean targets poses no issues. + +#### Clang support + +Clang is supported: just build with `CC=clang make`. At the moment, the support still depends on +arm-none-eabi GCC for some standard headers and libraries. It will also generate a fair amount of +warnings for SDK builds. **On Cygwin Clang is assumed to be compiled for Cygwin** and not for +Windows (e.g. downloaded via the Cygwin package manager). + +#### C++ support + +C++ is supported, just name your sources with `.cpp` extensions. The C++ standard library is not +supported. Exception handling and RTTI are disabled. It's more like C with classes than C++. You'll +need to have `arm-none-eabi-g++` installed to compile C++ code (even if you use Clang - we still +need GCC's headers). + +#### Legacy clean + +If you're migrating from the old build system to the new one you'll probably want to get rid of +the old cruft. Aside from running `make clean` with an old SDK, you can do it manually: + +- For the SDK, remove the `lib` directory and all `.o` files (recursively). +- For APROMs, remove the `bin` directory and all `.o` files (recursively). + +#### Misc + +The following environment/make variables are available: + +- `EVICSDK_MAKE_DEBUG`: set to non-empty to enable extra debug output from the build system. + Useful when investigating or reporting bugs. +- `EVICSDK_FPU_DISABLE`: set to non-empty to disable FPU support and lazy stacking. Needs a full + rebuild to avoid mixing FPU and no-FPU objects. Can be useful when debugging very rare, tricky + FP bugs. Do *not* use this unless you fully understand what it means (no, it won't make you + binaries smaller or faster, even if you don't use the FPU). + +Thread/ISR safety +----------------- + +Unless otherwise specified by the documentation, SDK functions are thread-safe and ISR-safe. +A function is thread-safe when it can be used concurrently by multiple threads. ISR-safety, on +the other hand, is not strictly about interrupt concurrency: it means that the function can be +called from ISR/callback contexts. Don't call non ISR-safe functions from these contexts. + +Reporting bugs +-------------- + +I invite you to report bugs using the Issues page. Please confirm the bug against the latest SDK +commit and include all information needed to reproduce the bug, like: + +- OS, make/compiler/binutils/newlib versions; +- Head commit and branch of the SDK you're using; +- Device and flavor (does it happen in release builds? Debug? Both?); +- Code (if applicable), preferably reduced to a Minimal, Complete and Verifiable (MCV) example; +- Things you tried and didn't work; +- For crashes, the crash dump generated by a debug build; +- For build system bugs, the debug output (i.e. `EVICSDK_MAKE_DEBUG=1 make ...`). Try minimizing + it: if `make evic-dbg` is enough to make it happen, don't send output for `make all`. + +Of course if you've already identified the bug in the SDK code you don't need to include as much +information. Be sensible and everyone involved will be happy. + +Pull requests +------------- + +Pull requests are welcome. A few rules: + +- Respect the coding and documentation style of the SDK and refer to the tips & tricks. +- PRs must merge cleanly against the branch they target (preferably rebased on top of the latest + head for that branch). +- No useless commits. Multiple related commits in the same PR are okay, but no merge/revert commits + and no "Add foo" "Fix foo" "Fix foo again" stuff. Familiarize yourself with `git rebase [-i]` and + use it. +- Respect the commit message style (look at the history). The title should tell what the *effect* + of a commit is, not how it does it. Titles should be imperative: "Add foo", not "Adds foo". For + non-trivial commits you're welcome to add further technical details in the commit body. Stick to + 80 columns per row. +- If you're fixing a bug [reference it](https://help.github.com/articles/closing-issues-via-commit-messages/) + in the title. If you're fixing a bug that hasn't been reported yet, create an issue first and + reference it in your commit title (you don't need to wait for an answer on the issue - go ahead + with the PR). + +It is understood that big or complex changes often won't be perfect, no need to worry about it. +Again, just use common sense and everything will work out. USB debugging ------------- @@ -185,22 +375,25 @@ An example on how to use the port is given in `example/usbdebug`. You can commun using your favorite serial port terminal. All the line coding parameters (baud rate, parity, stop bits, data bits) are ignored, so you don't need to worry about them. -Coding guidelines ------------------ +Tips & tricks +------------- While the SDK does a fairly good job of abstracting the low-level details, you still need to -remember that you're coding on an embedded platform. A few tips that might be useful: - -- You should declare all variables shared between your main code and callbacks/interrupt - handlers as `volatile`. -- Try to minimize dynamic memory allocation: all memory not used by data or stack is assigned - to heap, but RAM is only 32kB. -- Declare constant data (such as lookup tables) as `const`: the compiler will place it in - the ROM, reducing RAM usage. +remember that you're coding on an embedded platform. A few tips that might be helpful: + +- You should declare variables shared between threads or with callbacks/interrupts as `volatile`. +- Resources are limited: be frugal. Write efficient code. +- Minimize dynamic memory allocation: all memory not used by data or stack is assigned to heap, + but RAM is only 32kB and nobody likes a failed `malloc`. +- Declare constant data (such as lookup tables) as `const`: the compiler will place it in ROM, + reducing RAM usage. - Prefer `siprintf` over `sprintf`, as it produces much smaller binaries by stripping out the floating point printing routines. Of course `siprintf` doesn't support floating point numbers, so if you need to print them and cannot use a fixed-point representation you'll have to live with the increased binary size. -- C++ is supported, just name your C++ files with `.cpp` extensions. The C++ standard library is - NOT (yet?) supported. It's really bulky and it hardly fits in the ROM/RAM space we have. - Exception handling and RTTI are disabled. +- When using floating point variables, prefer `float` over `double`, because `float` has hardware + support. Using `double`s will pull in some floating point emulation code, making your binaries + larger. +- When using standard floating point functions, prefer the ones with the `f` suffix: hardware + support for `float`s means faster and smaller binaries. If you use `double` functions, they'll + pull in tons of floating point emulation code. diff --git a/example/usbdebug/main.c b/example/usbdebug/main.c index f8fcd41..e84c349 100644 --- a/example/usbdebug/main.c +++ b/example/usbdebug/main.c @@ -18,6 +18,7 @@ */ #include +#include #include #include #include @@ -34,7 +35,7 @@ int main() { char tmpBuf[9]; - uint8_t recvBuf[64], bufPos, i; + uint8_t recvBuf[64], bufPos, i, tmp; uint16_t readSize; // The virtual COM port is not initialized by default. @@ -67,8 +68,12 @@ int main() { // Data is available // We read 1 byte at a time because I'm too lazy // to write a real ring buffer for this example - readSize = USB_VirtualCOM_Read(recvBuf + bufPos, 1); - bufPos = (bufPos + readSize) % 64; + readSize = USB_VirtualCOM_Read(&tmp, 1); + // Filter out control characters for display + if(readSize && isprint(tmp)) { + recvBuf[bufPos] = tmp; + bufPos = (bufPos + readSize) % 64; + } } // Display the received data diff --git a/include/ADC.h b/include/ADC.h index 4d8e625..bf0fa26 100644 --- a/include/ADC.h +++ b/include/ADC.h @@ -66,6 +66,17 @@ extern "C" { */ #define ADC_MODULE_VBAT 0x12 +/** + * Function pointer type for ADC filters. + * Invoked from an interrupt handler, keep it as fast as possible. + * + * @param value Read ADC value. + * @param filterData Optional filter data passed to ADC_SetFilter(). + * + * @return Filtered ADC value. + */ +typedef uint16_t (*ADC_Filter_t)(uint16_t value, uint32_t filterData); + /** * Initializes the ADC. * System control registers must be unlocked. @@ -99,6 +110,16 @@ uint16_t ADC_GetCachedResult(uint8_t moduleNum); */ uint16_t ADC_Read(uint8_t moduleNum); +/** + * Sets a filter for an ADC module. + * + * @param moduleNum One of ADC_MODULE_*. + * @param filter ADC filter function, or NULL to disable. + * @param filterData Optional data to pass to the filter function. + * Ignored when disabling. + */ +void ADC_SetFilter(uint8_t moduleNum, ADC_Filter_t filter, uint32_t filterData); + #ifdef __cplusplus } #endif diff --git a/include/AtomicOps.h b/include/AtomicOps.h new file mode 100644 index 0000000..3d8ef09 --- /dev/null +++ b/include/AtomicOps.h @@ -0,0 +1,122 @@ +/* + * This file is part of eVic SDK. + * + * eVic SDK is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * eVic SDK is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with eVic SDK. If not, see . + * + * Copyright (C) 2016 ReservedField + */ + +#ifndef EVICSDK_ATOMICOPS_H +#define EVICSDK_ATOMICOPS_H + +#include + +// extern "C" is omitted because none +// of this is going to be exported. + +/* Always inline, no extern version. */ +#define ATOMICOPS_INLINE __attribute__((always_inline)) static inline + +/** + * Atomically stores a 32-bit value to memory, giving + * back the old value. + * + * @param ptr Memory to store the value to. + * @param newVal Value to store. + * + * @return Old value. + */ +ATOMICOPS_INLINE uint32_t AtomicOps_Swap(volatile uint32_t *ptr, uint32_t newVal) { + uint32_t oldVal, strexRet; + + // TODO: too many memory barriers? + asm volatile("@ AtomicOps_Swap\n\t" + "dmb\n" + "1:\n\t" + "ldrex %0, [%2]\n\t" + "strex %1, %3, [%2]\n\t" + "teq %1, #0\n\t" + "bne 1b\n\t" + "dmb" + : "=&r" (oldVal), "=&r" (strexRet) + : "r" (ptr), "r" (newVal) + : "memory", "cc"); + + return oldVal; +} + +/** + * Atomically stores a 32-bit value to memory, only if + * the value currently in memory is equal to the expected + * value, giving back the old value. + * + * @param ptr Memory to store the value to. + * @param expVal Expected value. + * @param newVal Value to store. + * + * @return Old value. + */ +ATOMICOPS_INLINE uint32_t AtomicOps_CmpSwap(volatile uint32_t *ptr, uint32_t expVal, uint32_t newVal) { + uint32_t loadVal, strexRet; + + // TODO: too many memory barriers? + asm volatile("@ AtomicOps_CmpSwap\n\t" + "dmb\n" + "1:\n\t" + "ldrex %0, [%2]\n\t" + "teq %0, %3\n\t" + "itt eq\n\t" + "strexeq %1, %4, [%2]\n\t" + "teqeq %1, #1\n\t" + "beq 1b\n\t" + "dmb" + : "=&r" (loadVal), "=&r" (strexRet) + : "r" (ptr), "r" (expVal), "r" (newVal) + : "memory", "cc"); + + return loadVal; +} + +/** + * Atomically adds to a 32-bit value in memory. + * + * @param ptr Memory where the first addend is and + * where the result will be stored. + * @param n Second addend. + * + * @return New value after addition. + */ +ATOMICOPS_INLINE uint32_t AtomicOps_Add(volatile uint32_t *ptr, uint32_t n) { + uint32_t result, strexRet; + + // TODO: too many memory barriers? + asm volatile("@ AtomicOps_Add:\n\t" + "dmb\n" + "1:\n\t" + "ldrex %0, [%2]\n\t" + "add %0, %0, %3\n\t" + "strex %1, %0, [%2]\n\t" + "teq %1, #0\n\t" + "bne 1b\n\t" + "dmb" + : "=&r" (result), "=&r" (strexRet) + : "r" (ptr), "r" (n) + : "memory", "cc"); + + return result; +} + +#undef ATOMICOPS_INLINE + +#endif diff --git a/include/Atomizer.h b/include/Atomizer.h index 7b6ffd3..02ce2f5 100755 --- a/include/Atomizer.h +++ b/include/Atomizer.h @@ -27,10 +27,40 @@ extern "C" { #endif +/** + * Minimum output voltage, in mV. + */ +#define ATOMIZER_VOLTAGE_MIN 500 +/** + * Maximum output voltage, in mV. + */ +#define ATOMIZER_VOLTAGE_MAX 9000 +/** + * Maximum output current, in mA. + */ +#define ATOMIZER_CURRENT_MAX 25000 +/** + * Minimum output power, in mW. + */ +#define ATOMIZER_POWER_MIN 1000 +/** + * Maximum output power, in mW. + */ +#define ATOMIZER_POWER_MAX 75000 +/** + * Minimum resistance, in mOhm. + */ +#define ATOMIZER_RESISTANCE_MIN 50 +/** + * Maximum resistance, in mOhm. + */ +#define ATOMIZER_RESISTANCE_MAX 3500 + /** * Maximum output voltage, in millivolts. + * Deprecated: use ATOMIZER_VOLTAGE_MAX. */ -#define ATOMIZER_MAX_VOLTS 9000 +#define ATOMIZER_MAX_VOLTS ATOMIZER_VOLTAGE_MAX /** * Structure to hold atomizer info. @@ -163,7 +193,7 @@ uint8_t Atomizer_IsOn(); Atomizer_Error_t Atomizer_GetError(); /** - * Reads the atomizer info. + * Reads the atomizer info (not ISR-safe). * This may power up the atomizer for resistance measuring, * depending on the situation. Refresh rate is internally * limited, so you can call this as often as you like. diff --git a/include/Dataflash.h b/include/Dataflash.h index 4697d12..416e397 100644 --- a/include/Dataflash.h +++ b/include/Dataflash.h @@ -57,7 +57,7 @@ void Dataflash_Init(); /** * Gets a list of magic numbers for structures present - * int the dataflash. + * in the dataflash (not ISR-safe). * * @param magicList An array at least DATAFLASH_STRUCT_MAX_COUNT * elements big to receive the magic numbers. @@ -67,7 +67,7 @@ void Dataflash_Init(); uint8_t Dataflash_GetMagicList(uint32_t *magicList); /** - * Reads a structure from the dataflash. + * Reads a structure from the dataflash (not ISR-safe). * * @param structInfo Structure info. * @param dst Pointer to structure to be filled. @@ -77,7 +77,7 @@ uint8_t Dataflash_GetMagicList(uint32_t *magicList); uint8_t Dataflash_ReadStruct(const Dataflash_StructInfo_t *structInfo, void *dst); /** - * Selects the set of dataflash structures to be used from now on. + * Selects the set of dataflash structures to be used from now on (not ISR-safe). * This can only be called once (further calls will be ignored) and must * have been called prior to performing updates (they'll fail otherwise). * NOTE: GCC and Clang went big-hammer on multiple-indirection const casts, @@ -92,7 +92,7 @@ uint8_t Dataflash_ReadStruct(const Dataflash_StructInfo_t *structInfo, void *dst uint8_t Dataflash_SelectStructSet(Dataflash_StructInfo_t **structInfo, uint8_t count); /** - * Updates/writes a structure in the dataflash. + * Updates/writes a structure in the dataflash (not ISR-safe). * The dataflash structure set must have already been selected, or * this will fail. * The update operation is a read-compare-write, i.e. it won't waste flash @@ -107,7 +107,7 @@ uint8_t Dataflash_SelectStructSet(Dataflash_StructInfo_t **structInfo, uint8_t c uint8_t Dataflash_UpdateStruct(const Dataflash_StructInfo_t *structInfo, void *src); /** - * Invalidates a structure in the dataflash. + * Invalidates a structure in the dataflash (not ISR-safe). * This doesn't actually erase the structure, but marks it in a * way such that it won't be read by the dataflash routines. * NOTE: This is provided for development/debugging only. You should @@ -123,7 +123,7 @@ uint8_t Dataflash_UpdateStruct(const Dataflash_StructInfo_t *structInfo, void *s uint8_t Dataflash_InvalidateStruct(const Dataflash_StructInfo_t *structInfo); /** - * Erases the whole dataflash. + * Erases the whole dataflash (not ISR-safe). * This actually erases all the flash memory belonging to dataflash. * NOTE: This is provided for development/debugging only. Do NOT use this * in production code. The right way to restore defaults is to push an diff --git a/include/Display.h b/include/Display.h index 37a9281..697543a 100644 --- a/include/Display.h +++ b/include/Display.h @@ -91,7 +91,7 @@ void Display_Init(); Display_Type_t Display_GetType(); /** - * Turns the display on or off. + * Turns the display on or off (not ISR-safe). * This only acts on the pixels, the display will * still be powered. If you want to control the supply * rails, use Display_SSD_SetPowerOn(). @@ -101,7 +101,7 @@ Display_Type_t Display_GetType(); void Display_SetOn(uint8_t isOn); /** - * Powers the display on or off. + * Powers the display on or off (not ISR-safe). * This turns the actual supply rails on/off, cutting * off all current draw from the display when off. It is * slower than Display_SSD_SetOn(). @@ -118,29 +118,30 @@ void Display_SetPowerOn(uint8_t isPowerOn); bool Display_IsFlipped(); /** - * Flips the display. + * Flips the display (not ISR-safe). */ void Display_Flip(); /** - * Sets whether the display colors are inverted. + * Sets whether the display colors are inverted (not ISR-safe). * * @param invert True for inverted display, false for normal display. */ void Display_SetInverted(bool invert); /** - * Sends the framebuffer to the controller and updates the display. + * Sends the framebuffer to the controller and updates the display + * (not ISR-safe). */ void Display_Update(); /** - * Clears the framebuffer. + * Clears the framebuffer (not ISR-safe). */ void Display_Clear(); /** - * Copies a bitmap into the framebuffer. + * Copies a bitmap into the framebuffer (not ISR-safe). * * @param x X coordinate to place the bitmap at. * @param y Y coordinate to place the bitmap at. @@ -151,7 +152,7 @@ void Display_Clear(); void Display_PutPixels(int x, int y, const uint8_t *bitmap, int w, int h); /** - * Draws a line into the framebuffer. + * Draws a line into the framebuffer (not ISR-safe). * * @param startX X coordinate to start the line at. * @param startY Y coordinate to start the line at. @@ -161,7 +162,7 @@ void Display_PutPixels(int x, int y, const uint8_t *bitmap, int w, int h); void Display_PutLine(int startX, int startY, int endX, int endY); /** - * Blits text into the framebuffer. + * Blits text into the framebuffer (not ISR-safe). * * @param x X coordinate to place the text at. * @param y Y coordinato to place the text at. @@ -181,7 +182,7 @@ void Display_PutText(int x, int y, const char *txt, const Font_Info_t *font); uint8_t *Display_GetFramebuffer(); /* - * Sets the display contrast. + * Sets the display contrast (not ISR-safe). * * @param contrast Contrast (0 - 255). */ diff --git a/include/Display_SSD.h b/include/Display_SSD.h index 9dd4395..a040c49 100644 --- a/include/Display_SSD.h +++ b/include/Display_SSD.h @@ -18,6 +18,10 @@ * Copyright (C) 2015-2016 Jussi Timperi */ +/** + * NOTE: this low-level interface is not guaranteed to be thread/ISR-safe. + */ + #ifndef EVICSDK_DISPLAY_SSD_H #define EVICSDK_DISPLAY_SSD_H diff --git a/include/Display_SSD1306.h b/include/Display_SSD1306.h index 934a40b..041c965 100644 --- a/include/Display_SSD1306.h +++ b/include/Display_SSD1306.h @@ -18,6 +18,10 @@ * Copyright (C) 2015-2016 Jussi Timperi */ +/** + * NOTE: this low-level interface is not guaranteed to be thread/ISR-safe. + */ + #ifndef EVICSDK_DISPLAY_SSD1306_H #define EVICSDK_DISPLAY_SSD1306_H diff --git a/include/Display_SSD1327.h b/include/Display_SSD1327.h index d48333f..93620d6 100644 --- a/include/Display_SSD1327.h +++ b/include/Display_SSD1327.h @@ -18,6 +18,10 @@ * Copyright (C) 2015-2016 Jussi Timperi */ +/** + * NOTE: this low-level interface is not guaranteed to be thread/ISR-safe. + */ + #ifndef EVICSDK_DISPLAY_SSD1327_H #define EVICSDK_DISPLAY_SSD1327_H diff --git a/include/Queue.h b/include/Queue.h new file mode 100644 index 0000000..110eebe --- /dev/null +++ b/include/Queue.h @@ -0,0 +1,94 @@ +/* + * This file is part of eVic SDK. + * + * eVic SDK is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * eVic SDK is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with eVic SDK. If not, see . + * + * Copyright (C) 2016 ReservedField + */ + +#ifndef EVICSDK_QUEUE_H +#define EVICSDK_QUEUE_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Queue. + */ +typedef struct { + /**< First item in queue. NULL when queue is empty. */ + void *head; + /**< Last item in queue. */ + void *tail; +} Queue_t; + +/** + * Queue item header. + * This MUST be the first field in every item. + */ +typedef struct { + /** Next item in queue. */ + void *next; +} Queue_Header_t; + +/** + * Initializes an user-allocated queue. + * + * @param queue Queue. + */ +void Queue_Init(Queue_t *queue); + +/** + * Pushes an item to the front of the queue. + * + * @param queue Queue. + * @param item Item to push. + */ +void Queue_PushFront(Queue_t *queue, void *item); + +/** + * Pushes an item to the back of the queue. + * + * @param queue Queue. + * @param item Item to push. + */ +void Queue_PushBack(Queue_t *queue, void *item); + +/** + * Pops the first item off the queue. + * + * @param queue Queue. + * + * @return First item, or NULL if the queue is empty. + */ +void *Queue_PopFront(Queue_t *queue); + +/** + * Removes an item from the queue. + * + * @param queue Queue. + * @param prev Item preceding the one to be removed, + * or NULL if removing the first item. + * @param item Item to remove. + */ +void Queue_Remove(Queue_t *queue, void *prev, void *item); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/Thread.h b/include/Thread.h new file mode 100644 index 0000000..2332a8b --- /dev/null +++ b/include/Thread.h @@ -0,0 +1,319 @@ +/* + * This file is part of eVic SDK. + * + * eVic SDK is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * eVic SDK is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with eVic SDK. If not, see . + * + * Copyright (C) 2016 ReservedField + */ + +#ifndef EVICSDK_THREAD_H +#define EVICSDK_THREAD_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Number of system ticks in one millisecond. + */ +#define THREAD_SYSTICK_MS 1 + +/** + * Default stack size for the main thread. + */ +#define THREAD_DEFAULT_STACKSIZE 1024 + +/** + * Defines the stack size for the main thread. + * This must be used outside of any function. If this + * macro isn't used, the stack size will default to + * THREAD_DEFAULT_STACKSIZE. + */ +#define THREAD_MAIN_STACKSIZE(size) uint16_t Startup_mainThreadStackSize = (size) + +/** + * Type for thread handles. + */ +typedef uint32_t Thread_t; + +/** + * Type for semaphore handles. + */ +typedef uint32_t Thread_Semaphore_t; + +/** + * Type for mutex handles. + */ +typedef uint32_t Thread_Mutex_t; + +/** + * Return values for thread API. + * Zero means success, a negative value means error. + */ +typedef enum { + /**< No error. */ + TD_SUCCESS = 0, + /**< Couldn't allocate memory. */ + TD_NO_MEMORY = -1, + /**< A try operation failed. */ + TD_TRY_FAIL = -2, + /**< An invalid value was passed. */ + TD_INVALID_VALUE = -3, + /**< An invalid thread handle was passed. */ + TD_INVALID_THREAD = -100, + /**< Attempted to join an already joined thread. */ + TD_ALREADY_JOINED = -101, + /**< An invalid semaphore handle was passed. */ + TD_INVALID_SEMA = -200, + /**< An invalid mutex handle was passed. */ + TD_INVALID_MUTEX = -300, + /**< Bad mutex unlock: already unlocked, or locked by a different thread. */ + TD_MUTEX_BAD_UNLOCK = -301 +} Thread_Error_t; + +/** + * Thread entry function pointer type. + * + * @param args Arguments parameter from Thread_Create(). + */ +typedef void *(*Thread_EntryPtr_t)(void *args); + +/** + * Initializes the thread manager. + */ +void Thread_Init(); + +/** + * Creates a new thread. + * + * @param thread Pointer to receive the thread handle. + * @param entry Thread entry function. + * @param args Arguments parameter to be passed to entry. + * @param stackSize Stack size, in bytes. + * + * @return TD_SUCCESS or TD_NO_MEMORY. In case of error + * the value of thread is undefined. + */ +Thread_Error_t Thread_Create(Thread_t *thread, Thread_EntryPtr_t entry, void *args, uint16_t stackSize); + +/** + * Yields this thread back to the scheduler, letting + * other threads run (not ISR-safe). + */ +void Thread_Yield(); + +/** + * Waits for another thread to complete (not ISR-safe). + * Each thread can only have one other thread waiting on it. + * Attempting to join a thread that already has another thread + * waiting on it will fail with TD_ALREADY_JOINED. + * + * @param thread Handle of the thread to wait for. + * @param ret Pointer to receive the return value of the + * joined thread. + * + * @return TD_SUCCESS, TD_INVALID_THREAD or TD_ALREADY_JOINED. + */ +Thread_Error_t Thread_Join(Thread_t thread, void **ret); + +/** + * Delays the current thread for the specified amount of time + * (not ISR-safe). + * It is guaranteed that the actual delay time will never be less + * than the specified time, but it may be longer. + * + * @param delay Delay time, in milliseconds. + */ +void Thread_DelayMs(uint32_t delay); + +/** + * Enters a global critical section, disabling scheduling + * for other threads. + * Critical sections can be nested. No-op from ISRs. + */ +void Thread_CriticalEnter(); + +/** + * Exits a global critical section, re-enabling scheduling + * for other threads once the last nested critical section + * is exited. No-op from ISRs. + */ +void Thread_CriticalExit(); + +/** + * Creates a semaphore. + * + * @param sema Pointer to receive semaphore handle. + * @param count Initial count (must not be negative). + * + * @return TD_SUCCESS, TD_INVALID_VALUE (negative count) or TD_NO_MEMORY. + * In case of error the value of sema is undefined. + */ +Thread_Error_t Thread_SemaphoreCreate(Thread_Semaphore_t *sema, int32_t count); + +/** + * Destroys a semaphore. + * + * @param sema Semaphore handle. + * + * @return TD_SUCCESS or TD_INVALID_SEMA. + */ +Thread_Error_t Thread_SemaphoreDestroy(Thread_Semaphore_t sema); + +/** + * Decrements the semaphore count (not ISR-safe). + * If count is zero, waits until another thread increments it. + * + * @param sema Semaphore handle. + * + * @return TD_SUCCESS or TD_INVALID_SEMA. + */ +Thread_Error_t Thread_SemaphoreDown(Thread_Semaphore_t sema); + +/** + * Decrements the semaphore count. + * If count is zero, it fails without waiting. + * + * @param sema Semaphore handle. + * + * @return TD_SUCCESS, TD_INVALID_SEMA or TD_TRY_FAIL. + */ +Thread_Error_t Thread_SemaphoreTryDown(Thread_Semaphore_t sema); + +/** + * Increments the semaphore count. + * + * @param sema Semaphore handle. + * + * @return TD_SUCCESS or TD_INVALID_SEMA. + */ +Thread_Error_t Thread_SemaphoreUp(Thread_Semaphore_t sema); + +/** + * Gets the semaphore count. + * + * @param sema Semaphore handle. + * @param count Pointer to receive the count. + * + * @return TD_SUCCESS or TD_INVALID_SEMA. In case of + * error the value of count is undefined. + */ +Thread_Error_t Thread_SemaphoreGetCount(Thread_Semaphore_t sema, int32_t *count); + +/** + * Creates a mutex. + * The mutex is initially unlocked. + * + * @param mutex Pointer to receive mutex handle. + * + * @return TD_SUCCESS or TD_NO_MEMORY. In case of + * error the value of mutex is undefined. + */ +Thread_Error_t Thread_MutexCreate(Thread_Mutex_t *mutex); + +/** + * Destroys a mutex. + * + * @param mutex Mutex handle. + * + * @return TD_SUCCESS or TD_INVALID_MUTEX. + */ +Thread_Error_t Thread_MutexDestroy(Thread_Mutex_t mutex); + +/** + * Locks a mutex (not ISR-safe). + * If the mutex is locked, waits until it's unlocked. + * + * @param mutex Mutex handle. + * + * @return TD_SUCCESS or TD_INVALID_MUTEX. + */ +Thread_Error_t Thread_MutexLock(Thread_Mutex_t mutex); + +/** + * Locks a mutex (not ISR-safe). + * If the mutex is locked, fails without waiting. + * + * @param mutex Mutex handle. + * + * @return TD_SUCCESS, TD_INVALID_MUTEX or TD_TRY_FAIL. + */ +Thread_Error_t Thread_MutexTryLock(Thread_Mutex_t mutex); + +/** + * Unlocks a mutex (not ISR-safe). + * Only the thread that locked a mutex can unlock it. + * + * @param mutex Mutex handle. + * + * @return TD_SUCCESS or TD_MUTEX_BAD_UNLOCK. + */ +Thread_Error_t Thread_MutexUnlock(Thread_Mutex_t mutex); + +/** + * Checks the state of a mutex. + * + * @param mutex Mutex handle. + * @param isLocked Pointer to receive state. Will be set to true + * if the mutex is locked, false if unlocked. + * + * @return TD_SUCCESS or TD_INVALID_MUTEX. In case of error the value + * of isLocked is undefined. + */ +Thread_Error_t Thread_MutexGetState(Thread_Mutex_t mutex, uint8_t *isLocked); + +/* Always inline, no extern version. */ +#define THREAD_INLINE __attribute__((always_inline)) static inline + +/** + * Gets the current system uptime. + * Wraps around at 49 days, 17:02:47.296. + * + * @return Current system uptime, in ticks. + */ +THREAD_INLINE uint32_t Thread_GetSysTicks() { + extern volatile uint32_t Thread_sysTick; + return Thread_sysTick; +} + +/** + * Saves and disables interrupts. + * + * @return Interrupt mask for Thread_IrqRestore. + */ +THREAD_INLINE uint32_t Thread_IrqDisable() { + uint32_t primask = __get_PRIMASK(); + __set_PRIMASK(1); + return primask; +} + +/** + * Restores interrupts. + * + * @param primask Interrupt mask from Thread_IrqDisable. + */ +THREAD_INLINE void Thread_IrqRestore(uint32_t primask) { + __set_PRIMASK(primask); +} + +#undef THREAD_INLINE + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/TimerUtils.h b/include/TimerUtils.h index 35cd7f3..76a6a21 100644 --- a/include/TimerUtils.h +++ b/include/TimerUtils.h @@ -80,18 +80,50 @@ int8_t Timer_CreateTimeout(uint16_t timeout, uint8_t isPeriodic, Timer_Callback_ void Timer_DeleteTimer(int8_t index); /** - * Delays for the specified time. + * Delays for the specified time (not ISR-safe). + * Do not call from interrupt/callback context. * - * @param delay Delay in microseconds. Valid range is 0 - 233016. + * @param delay Delay in milliseconds. */ -void Timer_DelayUs(uint32_t delay); +void Timer_DelayMs(uint32_t delay); + +/* Always inline, no extern version. */ +#define TIMERUTILS_INLINE __attribute__((always_inline)) static inline /** * Delays for the specified time. + * Busy loops wasting CPU cycles, so it's only + * appropriate for small delays (e.g. < 1ms). * - * @param delay Delay in milliseconds. + * @param delay Delay in microseconds. */ -void Timer_DelayMs(uint32_t delay); +TIMERUTILS_INLINE void Timer_DelayUs(uint16_t delay) { + // Clock is 72MHz and every subs + bne iteration takes 3 cycles. + // The last iteration would take only 2 cycles due to the bne not + // being taken, so a nop is inserted to bring it up to 3. + // We multiply delay by 72 / 3 = 24 to get the number of iterations. + // Multiplication is broken into: + // - delay += delay << 1 (i.e. delay = delay * 3) + // - delay = delay << 3 (i.e. delay = delay * 8) + // This takes the same cycles and space as mov + mul, but it doesn't + // use an extra register to store the constant multiplier. + // We subtract 4 cycles to the total for the checks and calculations + // (non-taken cbz, movs, muls, subs). + asm volatile("@ Timer_DelayUs\n\t" + "cbz %0, 2f\n\t" // 2 cycles taken, 1 cycle not taken + "add %0, %0, %0, lsl #1\n\t" // 1 cycle + "lsl %0, %0, #3\n\t" // 1 cycle + "subs %0, %0, #4\n" // 1 cycle + "1:\n\t" + "subs %0, %0, #1\n\t" // 1 cycle + "bne 1b\n\t" // 2 cycles taken, 1 cycle not taken + "nop\n" // 1 cycle + "2:" + : "+&r" (delay) + : : "cc"); +} + +#undef TIMERUTILS_INLINE #ifdef __cplusplus } diff --git a/include/USB_VirtualCOM.h b/include/USB_VirtualCOM.h index 4c8ecde..0a87f99 100644 --- a/include/USB_VirtualCOM.h +++ b/include/USB_VirtualCOM.h @@ -54,7 +54,7 @@ typedef void (*USB_VirtualCOM_RxCallback_t)(); void USB_VirtualCOM_Init(); /** - * Sends data over the USB virtual COM port. + * Sends data over the USB virtual COM port (not ISR-safe). * * @param buf Data buffer. * @param size Number of bytes to send. @@ -62,7 +62,7 @@ void USB_VirtualCOM_Init(); void USB_VirtualCOM_Send(const uint8_t *buf, uint32_t size); /** - * Sends a string over the USB virtual COM port. + * Sends a string over the USB virtual COM port (not ISR-safe). * * @param str String to send. */ diff --git a/linker/linker.ld b/linker/linker.ld index cc09417..30df1d4 100644 --- a/linker/linker.ld +++ b/linker/linker.ld @@ -1,18 +1,32 @@ +/* + * This file is part of eVic SDK. + * + * eVic SDK is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * eVic SDK is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with eVic SDK. If not, see . + * + * Copyright (C) 2015-2016 ReservedField + */ + +/* Library configurations */ +INPUT(evicsdk-crt0.o) +GROUP(-lgcc -lc -lm -lnuvosdk -levicsdk) + /* Memory regions */ MEMORY { ROM (rx) : ORIGIN = 0x00000000, LENGTH = 128K RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 32K } -/* Library configurations */ -INPUT(libevicsdk_crt0.o) -GROUP( - armv7e-m/libgcc.a - armv7e-m/libc.a - armv7e-m/libm.a - libevicsdk.a -) - ENTRY(Reset_Handler) SECTIONS { diff --git a/make/Base.mk b/make/Base.mk index 17e26c7..8f68452 100644 --- a/make/Base.mk +++ b/make/Base.mk @@ -1,139 +1,158 @@ -# Supported vars: -# TARGET -# OBJS -# CFLAGS -# CPPFLAGS -# ASFLAGS -# LDFLAGS - -# We make the following assumptions on Windows: -# arm-none-eabi gcc and binutils are compiled for Windows, -# so if you are using Cygwin, we will need path translations -# NUVOSDK must be lazily evaluated, so that we can later -# change EVICSDK when building include paths. - -NUVOSDK = $(EVICSDK)/nuvoton-sdk/Library - -# Force OBJS immediate expansion, since we'll be -# changing EVICSDK later. -OBJS := $(OBJS) - -CPU := cortex-m4 - -# We need to find out if on cygwin or not -ifeq ($(OS),Windows_NT) - ifeq (, $(findstring cygwin, $(shell gcc -dumpmachine))) - WIN_CYG := 0 - else - WIN_CYG := 1 - endif - -endif - -ifeq ($(shell $(CC) -v 2>&1 | grep -c "clang version"), 1) - CC_IS_CLANG := 1 -endif - -ifeq ($(ARMGCC),) - ARMGCC := $(shell cd $(shell arm-none-eabi-gcc --print-search-dir | grep 'libraries' | \ - tr '=$(if $(filter Windows_NT,$(OS)),;,:)' '\n' | \ - grep -E '/arm-none-eabi/lib/?$$' | head -1)/../.. && pwd) -endif - -ifeq ($(OS),Windows_NT) - # Always fix binutils path - ifneq ($(ARMGCC),) - # If using cygwin, use cygpath - ifeq ($(WIN_CYG),1) - ARMGCC := $(shell cygpath -w $(ARMGCC)) - endif - - endif - - ifndef CC_IS_CLANG - NEED_FIXPATH := 1 - endif -endif - -ifdef CC_IS_CLANG - CFLAGS += -target armv7em-none-eabi -fshort-enums -else - CC := arm-none-eabi-gcc -endif - -ifdef NEED_FIXPATH - ifeq ($(WIN_CYG), 0) - OBJS_FIXPATH := $(OBJS) - else - OBJS_FIXPATH := $(shell cygpath -w $(OBJS)) - EVICSDK := $(shell cygpath -w $(EVICSDK)) - endif - else - OBJS_FIXPATH := $(OBJS) -endif - -GCC_VERSION := $(shell arm-none-eabi-gcc -dumpversion) - -AS := arm-none-eabi-as -LD := arm-none-eabi-ld -OBJCOPY := arm-none-eabi-objcopy - +# This file is part of eVic SDK. +# +# eVic SDK is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# eVic SDK is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with eVic SDK. If not, see . +# +# Copyright (C) 2016 ReservedField + +# Supported variables: +# TARGET: output target name. +# OBJS: objects to compile. +# INCDIRS: include directories. +# INCDIRS_C: include directories (C only). +# INCDIRS_CXX: include directories (C++ only). +# LIBDIRS: library directories. +# CFLAGS: C compiler flags. +# CXXFLAGS: C++ compiler flags. +# ASFLAGS: assembler flags. +# LDFLAGS: linker flags. + +ifndef __evicsdk_make_base_inc +__evicsdk_make_base_inc := 1 + +include $(EVICSDK)/make/Helper.mk +include $(EVICSDK)/make/Common.mk + +# Binary output directory. BINDIR := bin -INCDIRS := $(foreach d,$(shell arm-none-eabi-gcc -x c -v -E /dev/null 2>&1 | sed -n -e '/<\.\.\.>/,/End/ p' | tail -n +2 | head -n -1 | sed 's/^\s*//'),-I$d) \ - -I$(NUVOSDK)/CMSIS/Include \ - -I$(NUVOSDK)/Device/Nuvoton/M451Series/Include \ - -I$(NUVOSDK)/StdDriver/inc \ - -I$(EVICSDK)/include - +# Linker script. LDSCRIPT := $(EVICSDK)/linker/linker.ld -LIBDIRS := -L$(ARMGCC)/arm-none-eabi/lib \ - -L$(ARMGCC)/arm-none-eabi/newlib \ - -L$(ARMGCC)/lib/arm-none-eabi/newlib \ - -L$(ARMGCC)/gcc/arm-none-eabi/$(GCC_VERSION) \ - -L$(ARMGCC)/lib/gcc/arm-none-eabi/$(GCC_VERSION) \ - -L$(EVICSDK)/lib - -CFLAGS += -Wall -mcpu=$(CPU) -mthumb -Os -fdata-sections -ffunction-sections -CFLAGS += $(INCDIRS) - -CPPFLAGS += -fno-exceptions -fno-rtti - -ASFLAGS += -mcpu=$(CPU) - -LDFLAGS += $(LIBDIRS) -LDFLAGS += -nostdlib -nostartfiles -T$(LDSCRIPT) --gc-sections - -all: env_check $(TARGET).bin - -%.o: %.c - $(CC) $(CFLAGS) -c $< -o $@ - -%.o: %.cpp - $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@ - -%.o: %.s - $(AS) $(ASFLAGS) -o $@ $< - -$(TARGET).elf: $(OBJS_FIXPATH) - test -d $(BINDIR) || mkdir $(BINDIR) - $(LD) $(OBJS_FIXPATH) $(LDFLAGS) -o $(BINDIR)/$(TARGET).elf - -$(TARGET)_unencrypted.bin: $(TARGET).elf - $(OBJCOPY) -O binary -j .text -j .data $(BINDIR)/$(TARGET).elf $(BINDIR)/$(TARGET)_unencrypted.bin - rm -f $(BINDIR)/$(TARGET).elf - -$(TARGET).bin: $(TARGET)_unencrypted.bin - evic convert $(BINDIR)/$(TARGET)_unencrypted.bin -o $(BINDIR)/$(TARGET).bin - rm -f $(BINDIR)/$(TARGET)_unencrypted.bin - -clean: - rm -rf $(OBJS) $(BINDIR) - -env_check: -ifeq ($(ARMGCC),) - $(error You must set the ARMGCC environment variable) -endif - -.PHONY: all clean env_check +# Binary output directory template. +bindir-tmpl = $(call dir-tmpl,$1,$2,$(BINDIR)) +# Binary output directory clean template. +clean-bindir-tmpl = $(call clean-dir-tmpl,$1,$2,$(BINDIR)) +# ELF output path template. +elf-tmpl = $(call bindir-tmpl,$1,$2)/$(TARGET).elf +# Binary output path template. Extra argument: binary name. +bin-tmpl = $(call bindir-tmpl,$1,$2)/$3.bin +# Unecrypted binary output path template. +bin-dec-tmpl = $(call bin-tmpl,$1,$2,$(TARGET)_dec) +# Encrypted binary output path template. +bin-enc-tmpl = $(call bin-tmpl,$1,$2,$(TARGET)) +# Linker map output path template. +ldmap-tmpl = $(call bindir-tmpl,$1,$2)/$(TARGET).map + +# Object targets for all devices and flavors. +objs-all := $(call tmpl-all,objs-tmpl,$(OBJS)) +# ELF targets for all devices and flavors. +elf-all := $(call tmpl-all,elf-tmpl) +# ELF targets for all devices, debug flavor. +elf-dbg := $(call tmpl-flavor,elf-tmpl,$(BUILD_FLAVOR_DBG)) +# ELF targets for all devices, release flavor. +elf-rel := $(call tmpl-flavor,elf-tmpl,$(BUILD_FLAVOR_REL)) +# Unencrypted binary targets for all devices and flavors. +bin-dec-all := $(call tmpl-all,bin-dec-tmpl) +# Unencrypted binary targets for all devices, release flavor. +bin-dec-rel := $(call tmpl-flavor,bin-dec-tmpl,$(BUILD_FLAVOR_REL)) +# Encrypted binary targets for all devices and flavors. +bin-enc-all := $(call tmpl-all,bin-enc-tmpl) +# Linker map output paths for all device, debug flavor. +ldmap-dbg := $(call tmpl-flavor,ldmap-tmpl,$(BUILD_FLAVOR_DBG)) +# SDK output directories for all devices and flavors. +sdk-all := $(call tmpl-all,sdkdir-tmpl,$(EVICSDK)) + +# Cache all needed paths for fixpath. +$(call fixpath-cache, \ + $(call objs-fixpath-cache,$(OBJS)) \ + $(elf-all) $(bin-dec-all) $(bin-enc-all) $(ldmap-dbg) $(sdk-all) \ + $(LIBDIRS) $(LDSCRIPT)) + +# Set up linker flags. +# We're linking with --no-warn-mismatch because the thread library is compiled +# without FPU support to avoid issues with FPU context switching, which would +# normally result in a linker error due to different FP ABIs (soft/hard). Since +# no function in the thread library accepts FP arguments or calls FP library +# functions, they will work fine together. This may trainwreck if SDK and APROM +# are compiled with different FP ABIs, but no-FPU builds are just a rarely used +# debugging tool, so it's good enough. +LDFLAGS += \ + -T$(call fixpath-bu,$(LDSCRIPT)) \ + -nostdlib -nostartfiles --gc-sections --no-warn-mismatch +# We keep -L flags separated because we want to specify them before LDFLAGS so +# that -l flags work correctly. Also, we're going to add the SDK library path +# to this on a device/flavor basis. +LDFLAGS_LIBDIRS := \ + $(foreach d,$(ARM_LIBDIRS_FIXBU) $(call fixpath-bu,$(LIBDIRS)),-L$d) + +# Objcopy flags for converting ELF to unencrypted binary. +OBJCOPYFLAGS += -O binary -j .text -j .data + +# Add binaries to clean templates. +CLEAN_PATH_TMPL += clean-bindir-tmpl + +# Enable secondary expansion. +.SECONDEXPANSION: + +# Set BUILD_* for all our targets. +$(call build-vars-rules,elf-tmpl) +$(call build-vars-rules,bin-dec-tmpl) +$(call build-vars-rules,bin-enc-tmpl) + +# Rule to link objects into an ELF. +$(elf-all): $$(call tmpl-build,sdkdir-tmpl,$(EVICSDK)) \ + $$(call tmpl-build,objs-tmpl,$$(OBJS)) | $$(@D) + $(call info-cmd,LD) + @$(call trace, \ + $(LD) $(call fixpath-bu,$(wordlist 2,$(words $^),$^)) \ + $(LDFLAGS_LIBDIRS) $(LDFLAGS) -o $(call fixpath-bu,$@)) + +# Add the SDK to LDFLAGS_LIBDIRS for all ELF targets. +$(elf-all): LDFLAGS_LIBDIRS += \ + -L$(call fixpath-bu,$(call tmpl-build,sdkdir-tmpl,$(EVICSDK))) + +# Generate a linker map for debug ELF targets. +$(elf-dbg): LDFLAGS += -Map=$(call fixpath-bu,$(call tmpl-build,ldmap-tmpl)) + +# Rule to generate an unencrypted binary from the ELF. +$(bin-dec-all): $$(call tmpl-build,elf-tmpl) | $$(@D) + $(call info-cmd,BIN) + @$(call trace, \ + $(OBJCOPY) $(OBJCOPYFLAGS) $(call fixpath-bu,$<) $(call fixpath-bu,$@)) + +# Rule to generate an encrypted binary from the unencrypted one. +$(bin-enc-all): $$(call tmpl-build,bin-dec-tmpl) | $$(@D) + $(call info-cmd,ENC) +# Silence evic-convert, errors will still get through. + @$(call trace, \ + evic-convert $< -o $@ > $(NULLDEV)) + +# Device-flavor target rule: build encrypted binary. +$(devfla-all): $$(call tmpl-build,bin-enc-tmpl) + +# Set object directories as order-only prerequisites for object targets. +$(objs-all): | $$(@D) + +# Generate directory targets. +$(call mkdir-rules,objs-dirs-tmpl,$(OBJS)) +$(call mkdir-rules,bindir-tmpl) + +# If this is remade the SDK output directory doesn't exist. +$(sdk-all): + $(error No SDK found for $(BUILD_DEVICE)-$(BUILD_FLAVOR)) + +# Mark all release ELFs and unencrypted binaries as intermediate files. +.INTERMEDIATE: $(elf-rel) $(bin-dec-rel) + +endif # __evicsdk_make_base_inc diff --git a/make/Common.mk b/make/Common.mk new file mode 100644 index 0000000..41167b1 --- /dev/null +++ b/make/Common.mk @@ -0,0 +1,337 @@ +# This file is part of eVic SDK. +# +# eVic SDK is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# eVic SDK is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with eVic SDK. If not, see . +# +# Copyright (C) 2016 ReservedField + +# If you need to, set INCDIRS(_*) *before* including this file. + +ifndef __evicsdk_make_common_inc +__evicsdk_make_common_inc := 1 + +include $(EVICSDK)/make/gmsl/gmsl +include $(EVICSDK)/make/Helper.mk +include $(EVICSDK)/make/Device.mk + +# Check make version. The sort/firstword trick fails if MAKE_VERSION is empty +# (make < 3.69), so we $(and) it with MAKE_VERSION. +$(if $(EVICSDK_MAKE_DEBUG),$(info Make version: $(MAKE_VERSION))) +ifneq ($(and $(MAKE_VERSION),$(firstword $(sort $(MAKE_VERSION) 3.81))),3.81) +$(error Make >= 3.81 required) +endif + +# Disable all builtin rules (they only slow us down). +MAKEFLAGS += --no-builtin-rules +.SUFFIXES: + +# Detect the OS, setting the following variables: +# IS_WIN_OS: 1 on Windows, empty otherwise. +# IS_CYGWIN: 1 on Windows + Cygwin, empty otherwise. +# On Windows + Cygwin both will be 1. +# On Windows + MinGW only IS_WIN_OS will be 1. +IS_WIN_OS := $(if $(findstring windows,$(call lc,$(OS))),1) +IS_CYGWIN := $(if $(findstring cygwin,$(call lc,$(shell uname -s))),1) +$(if $(EVICSDK_MAKE_DEBUG),$(info Detected OS: \ + $(if $(IS_WIN_OS),Windows ($(if $(IS_CYGWIN),Cygwin,native)),Unix-like))) + +# OS null device (e.g. /dev/null on Linux and Cygwin, NUL on Windows). +NULLDEV := $(if $(and $(IS_WIN_OS),$(call not,$(IS_CYGWIN))),NUL,/dev/null) +# GCC/binutils null device (e.g. /dev/null on Linux, NUL on Cygwin, Windows). +NULLDEV_TC := $(if $(IS_WIN_OS),NUL,/dev/null) + +# Detect the compiler based on CC, setting the following variables: +# CC_IS_CLANG: 1 if Clang, empty if GCC. +# If the compiler is not Clang, CC is set to arm-none-eabi-gcc. +CC_IS_CLANG := $(if $(findstring clang version,$(shell $(CC) -v 2>&1)),1) +ifndef CC_IS_CLANG + CC := arm-none-eabi-gcc +endif +$(if $(EVICSDK_MAKE_DEBUG),$(info Detected compiler: \ + $(if $(CC_IS_CLANG),Clang,GCC))) + +# Set up CPU flags for the compiler. Use EVICSDK_FPU_DISABLE to disable FPU. +# Sets the following variables: +# CPUFLAGS_GCC_NOFPU: flags for no-FPU build, GCC compiler. +# CPUFLAGS_GCC_FPU: flags for FPU build, GCC compiler. +# CPUFLAGS_GCC: flags for current build, GCC compiler. +# CPUFLAGS_NOFPU: flags for no-FPU build, detected compiler. +# CPUFLAGS_FPU: flags for FPU build, detected compiler. +# CPUFLAGS: flags for current build, detected compiler. +CPUFLAGS_GCC_NOFPU := -mcpu=cortex-m4 -mthumb +CPUFLAGS_GCC_FPU := $(CPUFLAGS_GCC_NOFPU) -mfloat-abi=hard -mfpu=fpv4-sp-d16 +CPUFLAGS_GCC := \ + $(if $(EVICSDK_FPU_DISABLE),$(CPUFLAGS_GCC_NOFPU),$(CPUFLAGS_GCC_FPU)) +ifndef CC_IS_CLANG + CPUFLAGS_NOFPU := $(CPUFLAGS_GCC_NOFPU) + CPUFLAGS_FPU := $(CPUFLAGS_GCC_FPU) +else + CPUFLAGS_FPU := -target armv7em-none-eabi + CPUFLAGS_NOFPU := $(CPUFLAGS_FPU) -mfpu=none +endif +CPUFLAGS := $(if $(EVICSDK_FPU_DISABLE),$(CPUFLAGS_NOFPU),$(CPUFLAGS_FPU)) + +# Detect library paths for the selected CPU flags, setting ARM_LIBDIRS_FIXBU. +# This is already fixed for binutils (see the fixpath-* docs in Helper.mk). +# The linker search path must follow the exact order of ARM_LIBDIRS_FIXBU. +ARM_LIBDIRS_FIXBU := $(subst $(if $(IS_WIN_OS),;,:), ,$(shell \ + arm-none-eabi-gcc $(CPUFLAGS_GCC) --print-search-dirs | \ + sed -n 's/^libraries:\s*=\?\(.*\)/\1/p')) +ifndef ARM_LIBDIRS_FIXBU +$(error Could not detect arm-none-eabi library paths) +endif +ifdef EVICSDK_MAKE_DEBUG +$(info ARM library paths (raw): $(ARM_LIBDIRS_FIXBU)) +$(info ARM library paths (canonical): \ + $(call path-canon,$(call unfixpath-tc,$(ARM_LIBDIRS_FIXBU)))) +endif + +# Nuvoton SDK root path. +NUVOSDK := $(EVICSDK)/nuvoton-sdk/Library + +# Gets GCC include directories for a language. +# Argument 1: language. +get-gcc-incdirs = $(shell \ + arm-none-eabi-gcc $(CPUFLAGS_GCC) -x $1 -v -E $(NULLDEV_TC) 2>&1 | \ + sed -n '/<\.\.\.>/,/End/p' | tail -n +2 | head -n -1 | sed 's/^\s*//') + +# Clang doesn't know about standard headers for arm-none-eabi. +# Fix it by using GCC's headers. We'll add -nostdinc later. +# TODO: this works, but generates warnings. Find a better way. +ifdef CC_IS_CLANG + __INCDIRS_GCC_C := $(call get-gcc-incdirs,c) + __INCDIRS_GCC_CXX := $(call get-gcc-incdirs,c++) + __INCDIRS_GCC_C_UNFIX := $(call unfixpath-tc,$(__INCDIRS_GCC_C)) + __INCDIRS_GCC_CXX_UNFIX := $(call unfixpath-tc,$(__INCDIRS_GCC_CXX)) +ifdef EVICSDK_MAKE_DEBUG +$(info GCC C include paths (raw): $(__INCDIRS_GCC_C)) +$(info GCC C include paths (canonical): \ + $(call path-canon,$(__INCDIRS_GCC_C_UNFIX))) +$(info GCC C++ include paths (raw): $(__INCDIRS_GCC_CXX)) +$(info GCC C++ include paths (canonical): \ + $(call path-canon,$(__INCDIRS_GCC_CXX_UNFIX))) +endif + INCDIRS_C += $(__INCDIRS_GCC_C_UNFIX) + INCDIRS_CXX += $(__INCDIRS_GCC_CXX_UNFIX) +endif + +# Add include directories for SDKs to INCDIRS. +INCDIRS += \ + $(NUVOSDK)/CMSIS/Include \ + $(NUVOSDK)/Device/Nuvoton/M451Series/Include \ + $(NUVOSDK)/StdDriver/inc \ + $(EVICSDK)/include + +# Cache all needed paths for fixpath. +$(call fixpath-cache,$(INCDIRS) $(INCDIRS_C) $(INCDIRS_CXX)) + +# Gets the compiler flags for the specified include paths. +# Argument 1: list of include paths. +get-incflags = $(foreach d,$(call fixpath-cc,$1),-I$d) + +# Set up toolchain flags. Appends to the following variables: +# CFLAGS: C compiler flags. +# CXXFLAGS: C++ compiler flags. +# ASFLAGS: assembler flags. +# Note that those are NOT inclusive of CPU flags. +__CC_FLAGS := \ + $(call get-incflags,$(INCDIRS)) \ + -Os -fdata-sections -ffunction-sections \ + $(if $(CC_IS_CLANG),-nostdinc) +ifndef EVICSDK_FPU_DISABLE + __CC_FLAGS += -DEVICSDK_FPU_SUPPORT + ASFLAGS += -DEVICSDK_FPU_SUPPORT +endif +__CFLAGS_INCDIRS := $(call get-incflags,$(INCDIRS_C)) +__CXXFLAGS_INCDIRS := $(call get-incflags,$(INCDIRS_CXX)) +CFLAGS += $(__CFLAGS_INCDIRS) $(__CC_FLAGS) +CXXFLAGS += $(__CXXFLAGS_INCDIRS) $(__CC_FLAGS) -fno-exceptions -fno-rtti +__EXTRA_FLAGS_DBG := -g + +# Set up toolchain tool names. CC is already set. +# We use CC as the assembler to take advantage of preprocessing. +CXX := $(CC) +AS := $(CC) +LD := arm-none-eabi-ld +AR := arm-none-eabi-ar +OBJCOPY := arm-none-eabi-objcopy + +# Default device: all devices. +EVICSDK_MAKE_DEFAULT_DEVICE ?= all +# Default flavor: all flavors. +EVICSDK_MAKE_DEFAULT_FLAVOR ?= all + +# Template that wraps the template specified by the extra argument, converting +# empty arguments to "all". Doesn't support extra arguments to the template. +ea-wrap = $(call $3,$(or $1,all),$(or $2,all)) +# Template that wraps the template specified by the extra argument, ignoring +# flavor and always passing in an empty one. Doesn't support extra arguments to +# the template. +dev-wrap = $(call $3,$1) +# Template that wraps the template specified by the extra argument, ignoring +# device and always passing in an empty one. Doesn't support extra arguments to +# the template. +fla-wrap = $(call $3,,$2) + +# Generates phony rules for a template that default: +# - device-all targets to all device-flavor targets for the specified device; +# - all-flavor targets to all device-flavor targets for the specified flavor; +# - all-all and all targets to all device-flavor targets. +# Argument 1: template name. Must produce the correct target names when called +# with one empty argument. +# Argument 2: extra template argument (optional). +alias-devfla-rules = $(eval $(call __alias-devfla-rules,$1,$2)) +define __alias-devfla-rules +$(call tmpl-rule-all-device,prereq-rule,$1,ea-wrap $1,$2) +$(call tmpl-rule-all-flavor,prereq-rule,$1,ea-wrap $1,$2) +$(call $1,all,all,$2) $(call $1,all,,$2): $(call tmpl-all,$1,$2) +.PHONY: $(call tmpl-flavor,$1,all,$2) $(call tmpl-device,$1,all,$2) \ + $(call $1,all,all,$2) $(call $1,all,,$2) +endef + +# Generates phony rules for a template that default: +# - device targets to device-flavor targets for the default flavor(s); +# - flavor targets to device-flavor targets for the default device(s); +# - the empty target to device-flavor targets for the default flavor(s) and +# device(s). +# Argument 1: template name. Must produce the correct target names when called +# with one or both empty arguments. +# Argument 2: extra template argument (optional). +alias-default-rules = $(eval $(call __alias-default-rules,$1,$2)) +define __alias-default-rules +$(foreach f,$(EVICSDK_MAKE_DEFAULT_FLAVOR),$(call \ + tmpl-rule-flavor,prereq-rule,$f,$1,dev-wrap $1,$2)) +$(foreach d,$(EVICSDK_MAKE_DEFAULT_DEVICE),$(call \ + tmpl-rule-device,prereq-rule,$d,$1,fla-wrap $1,$2)) +$(call $1,,,$2): $(foreach f,$(EVICSDK_MAKE_DEFAULT_FLAVOR),$(foreach \ + d,$(EVICSDK_MAKE_DEFAULT_DEVICE),$(call $1,$d,$f,$2))) +.PHONY: $(call tmpl-flavor,$1,,$2) $(call tmpl-device,$1,,$2) +endef + +# Helper that calls both alias-devfla-rules and alias-default-rules. +# Argument 1: template name. Must produce the correct target names when called +# with one or both empty arguments. +# Argument 2: extra template argument (optional). +alias-rules = $(call alias-devfla-rules,$1,$2) \ + $(call alias-default-rules,$1,$2) + +# SDK output directory. +SDKDIR = lib +# Base object output directory. +OBJDIR = obj + +# SDK output directory template. +# Extra argument: SDK root (empty for a relative path). +sdkdir-tmpl = $(if $3,$3/)$(call dir-tmpl,$1,$2,$(SDKDIR)) +# Object output directory template. +objdir-tmpl = $(call dir-tmpl,$1,$2,$(OBJDIR)) +# Object output directory template for clean. Accepts empty arguments. +clean-objdir-tmpl = $(call clean-dir-tmpl,$1,$2,$(OBJDIR)) +# Objects output path template. Extra argument: object list. +objs-tmpl = $(addprefix $(call objdir-tmpl,$1,$2)/,$3) +# Objects output directory template (no trailing slash, duplicates removed). +# Extra argument: object list. +objs-dirs-tmpl = $(patsubst %/,%,$(sort $(dir $(call objs-tmpl,$1,$2,$3)))) +# Object pattern template. +objpat-tmpl = $(call objdir-tmpl,$1,$2)/%.o +# Device-flavor template. Accepts empty arguments. +# If both device and flavor are empty, "def" is returned. +devfla-tmpl = $(or $1$(and $1,$2,-)$2,def) +# Clean-device-flavor template. Accepts empty arguments. +clean-devfla-tmpl = clean$(if $1,-$1)$(if $2,-$2) + +# Gets the paths to all possible sources for the given objects. +# Argument 1: object list. +get-srcs = $(foreach e,c cpp s,$(patsubst %.o,%.$e,$1)) + +# Gets object and source paths to cache for fixpath. +# Argument 1: object list. +# Argument 2: flavor (optional). +objs-fixpath-cache = $(if $2,$(call tmpl-flavor,objs-tmpl,$2,$1),$(call \ + tmpl-all,objs-tmpl,$1)) $(call get-srcs,$1) + +# Object patterns for all devices, debug flavor. +objpat-dbg := $(call tmpl-flavor,objpat-tmpl,$(BUILD_FLAVOR_DBG)) +# Device-flavor targets for all devices and flavors. +devfla-all := $(call tmpl-all,devfla-tmpl) +# Clean-device-flavor targets for all devices and flavors. +clean-devfla-all := $(call tmpl-all,clean-devfla-tmpl) +# Clean-device-all targets for all devices. +clean-devall-all := $(call tmpl-flavor,clean-devfla-tmpl,all) +# Clean-all-flavor targets for all flavors. +clean-allfla-all := $(call tmpl-device,clean-devfla-tmpl,all) +# Clean-all-all and clean-all targets. +clean-all := $(call clean-devfla-tmpl,all,all) $(call clean-devfla-tmpl,all) + +# Templates for paths to be removed on clean. Each template must support being +# called with one or both empty arguments and can return multiple paths. +# Add objects to clean templates. +CLEAN_PATH_TMPL += clean-objdir-tmpl + +# Object compilation rules macro. +# Since those are pattern rules, make assumes all targets are built within a +# single invocation when multiple targets are present. Because of this we need +# to redefine the rules for each device and flavor instead of writing them once +# with $(call tmpl-all,objpat-tmpl) as target. +define compile-rules +$3: %.c + $$(call info-cmd,CC) + @$$(call trace, \ + $$(CC) $$(CPUFLAGS) $$(CFLAGS) \ + -c $$(call fixpath-cc,$$<) -o $$(call fixpath-cc,$$@)) +$3: %.cpp + $$(call info-cmd,CXX) + @$$(call trace, \ + $$(CXX) $$(CPUFLAGS) $$(CXXFLAGS) \ + -c $$(call fixpath-cc,$$<) -o $$(call fixpath-cc,$$@)) +$3: %.s + $$(call info-cmd,AS) + @$$(call trace, \ + $$(AS) $$(CPUFLAGS) $$(ASFLAGS) -c -x assembler-with-cpp \ + $$(call fixpath-cc,$$<) -o $$(call fixpath-cc,$$@)) +endef + +# Compilation rules for all object targets. +$(call tmpl-rule-all,compile-rules,objpat-tmpl) + +# Set target-specific variables for debug targets. +$(objpat-dbg): CFLAGS += $(__EXTRA_FLAGS_DBG) +$(objpat-dbg): CXXFLAGS += $(__EXTRA_FLAGS_DBG) +$(objpat-dbg): ASFLAGS += $(__EXTRA_FLAGS_DBG) + +# Common rule for all clean targets. +$(clean-devfla-all) $(clean-devall-all) $(clean-allfla-all) $(clean-all): + rm -rf $(call tmpl-cat,$(CLEAN_PATH_TMPL),$(BUILD_DEVICE),$(BUILD_FLAVOR)) + +# All aliases for device-flavor targets. +$(call alias-rules,devfla-tmpl) +# Default aliases for clean targets. +$(call alias-default-rules,clean-devfla-tmpl) + +# Set BUILD_* for object, device-flavor and clean-device-flavor targets. +# We don't have to split up object patterns for target-specific variables. +$(call build-vars-rules,objpat-tmpl) +$(call build-vars-rules,devfla-tmpl) +$(call build-vars-rules,clean-devfla-tmpl) +# Set BUILD_* for clean-device-all targets (empty flavor). +$(call tmpl-rule-flavor,build_device_flavor-rules,,ea-wrap,,clean-devfla-tmpl) +# Set BUILD_* for clean-all-flavor targets (empty device). +$(call tmpl-rule-device,build_device_flavor-rules,,ea-wrap,,clean-devfla-tmpl) + +# Default target: empty device-flavor target. +.DEFAULT_GOAL := $(call devfla-tmpl) + +.PHONY: $(devfla-all) \ + $(clean-devfla-all) $(clean-devall-all) $(clean-allfla-all) $(clean-all) + +endif # __evicsdk_make_common_inc diff --git a/make/Device.mk b/make/Device.mk new file mode 100644 index 0000000..24b8835 --- /dev/null +++ b/make/Device.mk @@ -0,0 +1,24 @@ +# This file is part of eVic SDK. +# +# eVic SDK is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# eVic SDK is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with eVic SDK. If not, see . +# +# Copyright (C) 2016 ReservedField + +ifndef __evicsdk_make_devices_inc +__evicsdk_make_devices_inc := 1 + +# Supported devices list. +DEVICES := evic + +endif # __evicsdk_make_devices_inc diff --git a/make/Helper.mk b/make/Helper.mk new file mode 100644 index 0000000..6d905c7 --- /dev/null +++ b/make/Helper.mk @@ -0,0 +1,239 @@ +# This file is part of eVic SDK. +# +# eVic SDK is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# eVic SDK is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with eVic SDK. If not, see . +# +# Copyright (C) 2016 ReservedField + +ifndef __evicsdk_make_helper_inc +__evicsdk_make_helper_inc := 1 + +include $(EVICSDK)/make/gmsl/gmsl +include $(EVICSDK)/make/Device.mk + +# Terms used in documentation: +# OPTIONAL: an optional argument can be empty. Make syntax allows you to omit +# empty arguments if they are the last ones. +# TEMPLATE: an user-defined function that generates some kind of string from +# the following arguments: +# Argument 1: device name. +# Argument 2: flavor. +# Argument 3: extra argument (if specified for tmpl-*). +# RULE MACRO: an user-defined function that will be eval'ed (escape your $s!). +# Called a rule macro because it's usually used to define rules. +# It takes the following arguments: +# Argument 1: device name. +# Argument 2: flavor. +# Argument 3: template output (if a template was specified for +# tmpl-rule-*). +# Argument 4: extra argument (if specified for tmpl-rule-*). + +# Build flavors. +BUILD_FLAVOR_DBG := dbg +BUILD_FLAVOR_REL := rel +BUILD_FLAVORS := $(BUILD_FLAVOR_DBG) $(BUILD_FLAVOR_REL) + +# Converts a list of Cygwin style paths to native Windows style. +# Argument 1: list of paths to convert. +cygpath-w = $(if $(strip $1),$(shell cygpath -w $(strip $1))) + +# Converts a list of native Windows style paths to Cygwin style. +# Argument 1: list of paths to convert. +cygpath-u = $(if $(strip $1),$(shell cygpath -u $(strip $1))) + +# Converts a list of paths to the needed system style for binutils (BU) or the +# C compiler (CC). A few assumptions need to be made for Windows + Cygwin, due +# to path style differences between native and Cygwin binaries. We assume +# arm-none-eabi-gcc and binutils are native binaries, while Clang is a Cygwin +# binary. This is consistent with the available packages. This means we'll +# fixup paths through cygpath-w when passing them to GCC/binutils on Cygwin, +# but not when passing them to Clang. +# IS_CYGWIN and CC_IS_CLANG must be set prior to calling those. Since the +# conversion is expensive, paths are internally cached. They can be manually +# cached through fixpath-cache, which expands to empty. +# Argument 1: list of paths to fix. +fixpath-bu = $(if $(IS_CYGWIN),$(call memoize-list,cygpath-w,$1),$1) +fixpath-cc = $(if $(and $(IS_CYGWIN),$(call not,$(CC_IS_CLANG))),$(call \ + memoize-list,cygpath-w,$1),$1) +fixpath-cache = $(if $(IS_CYGWIN),$(call memoize-list-update,cygpath-w,$1)) + +# Converts a list of paths from GCC/binutils to shell style. Same assumptions +# are made as for fixpath-*. The paths are not internally cached (Windows uses +# colons in paths, which are not supported by memoize-list). +# Argument 1: list of paths to unfix. +unfixpath-tc = $(if $(IS_CYGWIN),$(call cygpath-u,$1),$1) + +# Canonicalizes a list of paths. +# If a path doesn't exist it's retuned unchanged and surrounded by (). +# Note: $(realpath) is broken for absolute paths on Windows make 3.81. Cygwin +# make is fine (Unix-style paths). This is only used for debugging anyway. +# Argument 1: list of paths to canonicalize. +path-canon = $(foreach p,$1,$(or $(realpath $p),($p))) + +# Prints a message formatted like: +# [device-flavor] command target +# This function is to be used only inside rules with BUILD_* variables set. +# Argument 1: command name (at most 3 characters). +info-cmd = $(info [$(BUILD_DEVICE)-$(BUILD_FLAVOR)] $(call \ + substr,$1 ,1,3) $@) + +# Prints a string if EVICSDK_MAKE_DEBUG is defined. +# Always evaluates to the provided string. +# This is meant to be used for tracing recipe commands. Using the --trace +# option of make >= 4.0 is simpler, but we support make >= 3.81. +# Argument 1: string to trace and return. +trace = $(if $(EVICSDK_MAKE_DEBUG),$(info $(strip $1)))$(strip $1) + +# Generates template output for all flavors for the specified device. +# Argument 1: template name. +# Argument 2: device name (passed as-is to template, can be anything). +# Argument 3: extra template argument (optional). +tmpl-device = $(foreach f,$(BUILD_FLAVORS),$(call $1,$2,$f,$3)) + +# Generates template output for all devices for the specified flavor. +# Argument 1: template name. +# Argument 2: flavor (passed as-is to template, can be anything). +# Argument 3: extra template argument (optional). +tmpl-flavor = $(foreach d,$(DEVICES),$(call $1,$d,$2,$3)) + +# Generates template output for all devices and flavors. +# Argument 1: template name. +# Argument 2: extra template argument (optional). +tmpl-all = $(foreach f,$(BUILD_FLAVORS),$(call tmpl-flavor,$1,$f,$2)) + +# Generates template output based on BUILD_* variables. +# Argument 1: template name. +# Argument 2: extra template argument (optional). +tmpl-build = $(call $1,$(BUILD_DEVICE),$(BUILD_FLAVOR),$2) + +# Generates rules for all flavors for the specified device. +# Argument 1: rule macro name. +# Argument 2: device name (passed as-is to template and rule, can be anything). +# Argument 3: template name (optional). +# Argument 4: extra rule argument (optional). +# Argument 5: extra template argument (optional). +tmpl-rule-device = $(foreach f,$(BUILD_FLAVORS),$(eval $(call $1,$2,$f,$(call \ + $3,$2,$f,$5),$4))) + +# Generates rules for all devices for the specified flavor. +# Argument 1: rule macro name. +# Argument 2: flavor (passed as-is to template and rule, can be anything). +# Argument 3: template name (optional). +# Argument 4: extra rule argument (optional). +# Argument 5: extra template argument (optional). +tmpl-rule-flavor = $(foreach d,$(DEVICES),$(eval $(call $1,$d,$2,$(call \ + $3,$d,$2,$5),$4))) + +# Generates rules for all devices and flavors. +# Argument 1: rule macro name. +# Argument 2: template name (optional). +# ARgument 3: extra rule argument (optional). +# Argument 4: extra template argument (optional). +tmpl-rule-all = $(foreach f,$(BUILD_FLAVORS),$(call \ + tmpl-rule-flavor,$1,$f,$2,$3,$4)) + +# Generates rules for all devices and flavors, grouping them by device. This +# means that if a template is specified, the template output passed to the rule +# macro will be the output of tmpl-device. Since multiple flavors will be in +# the template output, the rule will be passed an empty flavor argument. +# Argument 1: rule macro name. +# Argument 2: template name (optional). +# Argument 3: extra rule argument (optional). +# Argument 4: extra template argument (optional). +tmpl-rule-all-device = $(foreach d,$(DEVICES),$(eval $(call $1,$d,,$(call \ + tmpl-device,$2,$d,$4),$3))) + +# Generates rules for all devices and flavors, grouping them by flavor. This +# means that if a template is specified, the template output passed to the rule +# macro will be the output of tmpl-flavor. Since multiple devices will be in +# the template output, the rule will be passed an empty device argument. +# Argument 1: rule macro name. +# Argument 2: template name (optional). +# Argument 3: extra rule argument (optional). +# Argument 4: extra template argument (optional). +tmpl-rule-all-flavor = $(foreach f,$(BUILD_FLAVORS),$(eval $(call \ + $1,,$f,$(call tmpl-flavor,$2,$f,$4),$3))) + +# Makes a list out of the outputs of a list of templates. +# Argument 1: list of template names. +# Argument 2: device name (passed as-is to template, can be anything). +# Argument 3: flavor (passed as-is to template, can be anything). +tmpl-cat = $(foreach t,$1,$(call $t,$2,$3)) + +# Memoizes a function that accepts and returns lists. The order of the list +# returned by the function is assumed to match the input list. There will be +# a single function call, but each input-output value pair will be cached +# separately. Already seen input values will be satisfied by the cache and +# filtered out from the function input. Returns the output list. +# Argument 1: function name. +# Argument 2: input list (colons are not allowed). +memoize-list = $(call memoize-list-update,$1,$(filter-out $(call \ + keys,__memoize-map-$1),$2))$(foreach k,$2,$(call get,__memoize-map-$1,$k)) + +# Caches values for memoize-list. The function semantics are the same, but the +# input-output pairs will always be forcefully cached. Expands to empty. +# Argument 1: function name. +# Argument 2: input list (colons are not allowed). +memoize-list-update = $(if $(strip $2),$(eval __memoize-$1 = $$(call \ + set,__memoize-map-$1,$$1,$$2))$(call \ + pairmap,__memoize-$1,$2,$(call $1,$2))) + +# Rule macro to set BUILD_DEVICE and BUILD_FLAVOR for the targets specified by +# the template output. +# Note: this could be split into two rules and used with tmpl-rule-all-*. While +# it looks cleaner, it takes two calls (i.e. two times the same device-flavor +# loop) and make will split them up into a single target per rule internally, +# so there's no performance gain. It also becomes tricky for clean targets. +define build_device_flavor-rules +$3: BUILD_DEVICE := $1 +$3: BUILD_FLAVOR := $2 +endef + +# Generates rules to set BUILD_DEVICE and BUILD_FLAVOR +# for the targets generated by the specified template. +# Argument 1: template name. +# Argument 2: extra template argument (optional). +build-vars-rules = $(call tmpl-rule-all,build_device_flavor-rules,$1,,$2) + +# Rule macro to create the target directories specified +# by the template output. +define mkdir-target-rule +$3: + @mkdir -p $$@ +endef + +# Generates rules to create the directories generated +# by the specified template. +# Argument 1: template name. +# Argument 2: extra template argument (optional). +mkdir-rules = $(call tmpl-rule-all,mkdir-target-rule,$1,,$2) + +# Rule macro to set the template output as prerequisite for the target +# generated by the template specified by the extra rule argument. You can +# specify an extra argument to the template by separating it with a space +# from the template name in the extra rule argument. +define prereq-rule +$(call $(word 1,$4),$1,$2,$(word 2,$4)): $3 +endef + +# Output directory template (no trailing slash). +# Extra argument: base directory. +dir-tmpl = $3/$2/$1 + +# Output directory clean template (no trailing slash). +# Extra argument: base directory. +# Accepts one or both empty arguments. May return a list. +clean-dir-tmpl = $(if $(and $1,$(call not,$2)),$(call \ + tmpl-device,clean-dir-tmpl,$1,$3),$3$(if $2,/$2$(if $1,/$1))) + +endif # __evicsdk_make_helper_inc diff --git a/make/gmsl/__gmsl b/make/gmsl/__gmsl new file mode 100644 index 0000000..3db20ad --- /dev/null +++ b/make/gmsl/__gmsl @@ -0,0 +1,940 @@ +# ---------------------------------------------------------------------------- +# +# GNU Make Standard Library (GMSL) +# +# A library of functions to be used with GNU Make's $(call) that +# provides functionality not available in standard GNU Make. +# +# Copyright (c) 2005-2014 John Graham-Cumming +# +# This file is part of GMSL +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# Neither the name of the John Graham-Cumming nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# ---------------------------------------------------------------------------- + +# This is the GNU Make Standard Library version number as a list with +# three items: major, minor, revision + +gmsl_version := 1 1 7 + +__gmsl_name := GNU Make Standard Library + +# Used to output warnings and error from the library, it's possible to +# disable any warnings or errors by overriding these definitions +# manually or by setting GMSL_NO_WARNINGS or GMSL_NO_ERRORS + +ifdef GMSL_NO_WARNINGS +__gmsl_warning := +else +__gmsl_warning = $(if $1,$(warning $(__gmsl_name): $1)) +endif + +ifdef GMSL_NO_ERRORS +__gmsl_error := +else + __gmsl_error = $(if $1,$(error $(__gmsl_name): $1)) +endif + +# If GMSL_TRACE is enabled then calls to the library functions are +# traced to stdout using warning messages with their arguments + +ifdef GMSL_TRACE +__gmsl_tr1 = $(warning $0('$1')) +__gmsl_tr2 = $(warning $0('$1','$2')) +__gmsl_tr3 = $(warning $0('$1','$2','$3')) +else +__gmsl_tr1 := +__gmsl_tr2 := +__gmsl_tr3 := +endif + +# See if spaces are valid in variable names (this was the case until +# GNU Make 3.82) +ifeq ($(MAKE_VERSION),3.82) +__gmsl_spaced_vars := $(false) +else +__gmsl_spaced_vars := $(true) +endif + +# Figure out whether we have $(eval) or not (GNU Make 3.80 and above) +# if we do not then output a warning message, if we do then some +# functions will be enabled. + +__gmsl_have_eval := $(false) +__gmsl_ignore := $(eval __gmsl_have_eval := $(true)) + +# If this is being run with Electric Cloud's emake then warn that +# their $(eval) support is incomplete in 1.x, 2.x, 3.x, 4.x and 5.0, +# 5.1, 5.2 and 5.3 + +ifdef ECLOUD_BUILD_ID +__gmsl_emake_major := $(word 1,$(subst ., ,$(EMAKE_VERSION))) +__gmsl_emake_minor := $(word 2,$(subst ., ,$(EMAKE_VERSION))) +ifneq ("$(findstring $(__gmsl_emake_major),1 2 3 4)$(findstring $(__gmsl_emake_major)$(__gmsl_emake_minor),50 51 52 53)","") +$(warning You are using a version of Electric Cloud's emake which has incomplete $$(eval) support) +__gmsl_have_eval := $(false) +endif +endif + +# See if we have $(lastword) (GNU Make 3.81 and above) + +__gmsl_have_lastword := $(lastword $(false) $(true)) + +# See if we have native or and and (GNU Make 3.81 and above) + +__gmsl_have_or := $(if $(filter-out undefined, \ + $(origin or)),$(call or,$(true),$(false))) +__gmsl_have_and := $(if $(filter-out undefined, \ + $(origin and)),$(call and,$(true),$(true))) + +ifneq ($(__gmsl_have_eval),$(true)) +$(call __gmsl_warning,Your make version $(MAKE_VERSION) does not support $$$$(eval): some functions disabled) +endif + +__gmsl_dollar := $$ +__gmsl_hash := \# + +# ---------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- +# Function: gmsl_compatible +# Arguments: List containing the desired library version number (maj min rev) +# Returns: $(true) if this version of the library is compatible +# with the requested version number, otherwise $(false) +# ---------------------------------------------------------------------------- +gmsl_compatible = $(strip \ + $(if $(call gt,$(word 1,$1),$(word 1,$(gmsl_version))), \ + $(false), \ + $(if $(call lt,$(word 1,$1),$(word 1,$(gmsl_version))), \ + $(true), \ + $(if $(call gt,$(word 2,$1),$(word 2,$(gmsl_version))), \ + $(false), \ + $(if $(call lt,$(word 2,$1),$(word 2,$(gmsl_version))), \ + $(true), \ + $(call lte,$(word 3,$1),$(word 3,$(gmsl_version)))))))) + +# ########################################################################### +# LOGICAL OPERATORS +# ########################################################################### + +# not is defined in gmsl + +# ---------------------------------------------------------------------------- +# Function: and +# Arguments: Two boolean values +# Returns: Returns $(true) if both of the booleans are true +# ---------------------------------------------------------------------------- +ifneq ($(__gmsl_have_and),$(true)) +and = $(__gmsl_tr2)$(if $1,$(if $2,$(true),$(false)),$(false)) +endif + +# ---------------------------------------------------------------------------- +# Function: or +# Arguments: Two boolean values +# Returns: Returns $(true) if either of the booleans is true +# ---------------------------------------------------------------------------- +ifneq ($(__gmsl_have_or),$(true)) +or = $(__gmsl_tr2)$(if $1$2,$(true),$(false)) +endif + +# ---------------------------------------------------------------------------- +# Function: xor +# Arguments: Two boolean values +# Returns: Returns $(true) if exactly one of the booleans is true +# ---------------------------------------------------------------------------- +xor = $(__gmsl_tr2)$(if $1,$(if $2,$(false),$(true)),$(if $2,$(true),$(false))) + +# ---------------------------------------------------------------------------- +# Function: nand +# Arguments: Two boolean values +# Returns: Returns value of 'not and' +# ---------------------------------------------------------------------------- +nand = $(__gmsl_tr2)$(if $1,$(if $2,$(false),$(true)),$(true)) + +# ---------------------------------------------------------------------------- +# Function: nor +# Arguments: Two boolean values +# Returns: Returns value of 'not or' +# ---------------------------------------------------------------------------- +nor = $(__gmsl_tr2)$(if $1$2,$(false),$(true)) + +# ---------------------------------------------------------------------------- +# Function: xnor +# Arguments: Two boolean values +# Returns: Returns value of 'not xor' +# ---------------------------------------------------------------------------- +xnor =$(__gmsl_tr2)$(if $1,$(if $2,$(true),$(false)),$(if $2,$(false),$(true))) + +# ########################################################################### +# LIST MANIPULATION FUNCTIONS +# ########################################################################### + +# ---------------------------------------------------------------------------- +# Function: first (same as LISP's car, or head) +# Arguments: 1: A list +# Returns: Returns the first element of a list +# ---------------------------------------------------------------------------- +first = $(__gmsl_tr1)$(firstword $1) + +# ---------------------------------------------------------------------------- +# Function: last +# Arguments: 1: A list +# Returns: Returns the last element of a list +# ---------------------------------------------------------------------------- +ifeq ($(__gmsl_have_lastword),$(true)) +last = $(__gmsl_tr1)$(lastword $1) +else +last = $(__gmsl_tr1)$(if $1,$(word $(words $1),$1)) +endif + +# ---------------------------------------------------------------------------- +# Function: rest (same as LISP's cdr, or tail) +# Arguments: 1: A list +# Returns: Returns the list with the first element removed +# ---------------------------------------------------------------------------- +rest = $(__gmsl_tr1)$(wordlist 2,$(words $1),$1) + +# ---------------------------------------------------------------------------- +# Function: chop +# Arguments: 1: A list +# Returns: Returns the list with the last element removed +# ---------------------------------------------------------------------------- +chop = $(__gmsl_tr1)$(wordlist 2,$(words $1),x $1) + +# ---------------------------------------------------------------------------- +# Function: map +# Arguments: 1: Name of function to $(call) for each element of list +# 2: List to iterate over calling the function in 1 +# Returns: The list after calling the function on each element +# ---------------------------------------------------------------------------- +map = $(__gmsl_tr2)$(strip $(foreach a,$2,$(call $1,$a))) + +# ---------------------------------------------------------------------------- +# Function: pairmap +# Arguments: 1: Name of function to $(call) for each pair of elements +# 2: List to iterate over calling the function in 1 +# 3: Second list to iterate over calling the function in 1 +# Returns: The list after calling the function on each pair of elements +# ---------------------------------------------------------------------------- +pairmap = $(strip $(__gmsl_tr3)\ + $(if $2$3,$(call $1,$(call first,$2),$(call first,$3)) \ + $(call pairmap,$1,$(call rest,$2),$(call rest,$3)))) + +# ---------------------------------------------------------------------------- +# Function: leq +# Arguments: 1: A list to compare against... +# 2: ...this list +# Returns: Returns $(true) if the two lists are identical +# ---------------------------------------------------------------------------- +leq = $(__gmsl_tr2)$(strip $(if $(call seq,$(words $1),$(words $2)), \ + $(call __gmsl_list_equal,$1,$2),$(false))) + +__gmsl_list_equal = $(if $(strip $1), \ + $(if $(call seq,$(call first,$1),$(call first,$2)), \ + $(call __gmsl_list_equal, \ + $(call rest,$1), \ + $(call rest,$2)), \ + $(false)), \ + $(true)) + +# ---------------------------------------------------------------------------- +# Function: lne +# Arguments: 1: A list to compare against... +# 2: ...this list +# Returns: Returns $(true) if the two lists are different +# ---------------------------------------------------------------------------- +lne = $(__gmsl_tr2)$(call not,$(call leq,$1,$2)) + +# ---------------------------------------------------------------------------- +# Function: reverse +# Arguments: 1: A list to reverse +# Returns: The list with its elements in reverse order +# ---------------------------------------------------------------------------- +reverse =$(__gmsl_tr1)$(strip $(if $1,$(call reverse,$(call rest,$1)) \ + $(call first,$1))) + +# ---------------------------------------------------------------------------- +# Function: uniq +# Arguments: 1: A list from which to remove repeated elements +# Returns: The list with duplicate elements removed without reordering +# ---------------------------------------------------------------------------- +uniq = $(strip $(__gmsl_tr1) $(if $1,$(firstword $1) \ + $(call uniq,$(filter-out $(firstword $1),$1)))) + +# ---------------------------------------------------------------------------- +# Function: length +# Arguments: 1: A list +# Returns: The number of elements in the list +# ---------------------------------------------------------------------------- +length = $(__gmsl_tr1)$(words $1) + +# ########################################################################### +# STRING MANIPULATION FUNCTIONS +# ########################################################################### + +# Helper function that translates any GNU Make 'true' value (i.e. a +# non-empty string) to our $(true) + +__gmsl_make_bool = $(if $(strip $1),$(true),$(false)) + +# ---------------------------------------------------------------------------- +# Function: seq +# Arguments: 1: A string to compare against... +# 2: ...this string +# Returns: Returns $(true) if the two strings are identical +# ---------------------------------------------------------------------------- +seq = $(__gmsl_tr2)$(if $(subst x$1,,x$2)$(subst x$2,,x$1),$(false),$(true)) + +# ---------------------------------------------------------------------------- +# Function: sne +# Arguments: 1: A string to compare against... +# 2: ...this string +# Returns: Returns $(true) if the two strings are not the same +# ---------------------------------------------------------------------------- +sne = $(__gmsl_tr2)$(call not,$(call seq,$1,$2)) + +# ---------------------------------------------------------------------------- +# Function: split +# Arguments: 1: The character to split on +# 2: A string to split +# Returns: Splits a string into a list separated by spaces at the split +# character in the first argument +# ---------------------------------------------------------------------------- +split = $(__gmsl_tr2)$(strip $(subst $1, ,$2)) + +# ---------------------------------------------------------------------------- +# Function: merge +# Arguments: 1: The character to put between fields +# 2: A list to merge into a string +# Returns: Merges a list into a single string, list elements are separated +# by the character in the first argument +# ---------------------------------------------------------------------------- +merge = $(__gmsl_tr2)$(strip $(if $2, \ + $(if $(call seq,1,$(words $2)), \ + $2,$(call first,$2)$1$(call merge,$1,$(call rest,$2))))) + +ifdef __gmsl_have_eval +# ---------------------------------------------------------------------------- +# Function: tr +# Arguments: 1: The list of characters to translate from +# 2: The list of characters to translate to +# 3: The text to translate +# Returns: Returns the text after translating characters +# ---------------------------------------------------------------------------- +tr = $(strip $(__gmsl_tr3)$(call assert_no_dollar,$0,$1$2$3) \ + $(eval __gmsl_t := $3) \ + $(foreach c, \ + $(join $(addsuffix :,$1),$2), \ + $(eval __gmsl_t := \ + $(subst $(word 1,$(subst :, ,$c)),$(word 2,$(subst :, ,$c)), \ + $(__gmsl_t))))$(__gmsl_t)) + +# Common character classes for use with the tr function. Each of +# these is actually a variable declaration and must be wrapped with +# $() or ${} to be used. + +[A-Z] := A B C D E F G H I J K L M N O P Q R S T U V W X Y Z # +[a-z] := a b c d e f g h i j k l m n o p q r s t u v w x y z # +[0-9] := 0 1 2 3 4 5 6 7 8 9 # +[A-F] := A B C D E F # + +# ---------------------------------------------------------------------------- +# Function: uc +# Arguments: 1: Text to upper case +# Returns: Returns the text in upper case +# ---------------------------------------------------------------------------- +uc = $(__gmsl_tr1)$(call assert_no_dollar,$0,$1)$(call tr,$([a-z]),$([A-Z]),$1) + +# ---------------------------------------------------------------------------- +# Function: lc +# Arguments: 1: Text to lower case +# Returns: Returns the text in lower case +# ---------------------------------------------------------------------------- +lc = $(__gmsl_tr1)$(call assert_no_dollar,$0,$1)$(call tr,$([A-Z]),$([a-z]),$1) + +# ---------------------------------------------------------------------------- +# Function: strlen +# Arguments: 1: A string +# Returns: Returns the length of the string +# ---------------------------------------------------------------------------- + +# This results in __gmsl_tab containing a tab + +__gmsl_tab := # + +__gmsl_characters := A B C D E F G H I J K L M N O P Q R S T U V W X Y Z +__gmsl_characters += a b c d e f g h i j k l m n o p q r s t u v w x y z +__gmsl_characters += 0 1 2 3 4 5 6 7 8 9 +__gmsl_characters += ` ~ ! @ \# $$ % ^ & * ( ) - _ = + +__gmsl_characters += { } [ ] \ : ; ' " < > , . / ? | + +# This results in __gmsl_space containing just a space + +__gmsl_space := +__gmsl_space += + +strlen = $(__gmsl_tr1)$(call assert_no_dollar,$0,$1)$(strip $(eval __temp := $(subst $(__gmsl_space),x,$1))$(foreach a,$(__gmsl_characters),$(eval __temp := $$(subst $$a,x,$(__temp))))$(eval __temp := $(subst x,x ,$(__temp)))$(words $(__temp))) + +# This results in __gmsl_newline containing just a newline + +define __gmsl_newline + + +endef + +# ---------------------------------------------------------------------------- +# Function: substr +# Arguments: 1: A string +# 2: Start position (first character is 1) +# 3: End position (inclusive) +# Returns: A substring. +# Note: The string in $1 must not contain a § +# ---------------------------------------------------------------------------- + +substr = $(if $2,$(__gmsl_tr3)$(call assert_no_dollar,$0,$1$2$3)$(strip $(eval __temp := $$(subst $$(__gmsl_space),§ ,$$1))$(foreach a,$(__gmsl_characters),$(eval __temp := $$(subst $$a,$$a$$(__gmsl_space),$(__temp))))$(eval __temp := $(wordlist $2,$3,$(__temp))))$(subst §,$(__gmsl_space),$(subst $(__gmsl_space),,$(__temp)))) + +endif # __gmsl_have_eval + +# ########################################################################### +# SET MANIPULATION FUNCTIONS +# ########################################################################### + +# Sets are represented by sorted, deduplicated lists. To create a set +# from a list use set_create, or start with the empty_set and +# set_insert individual elements + +# This is the empty set +empty_set := + +# ---------------------------------------------------------------------------- +# Function: set_create +# Arguments: 1: A list of set elements +# Returns: Returns the newly created set +# ---------------------------------------------------------------------------- +set_create = $(__gmsl_tr1)$(sort $1) + +# ---------------------------------------------------------------------------- +# Function: set_insert +# Arguments: 1: A single element to add to a set +# 2: A set +# Returns: Returns the set with the element added +# ---------------------------------------------------------------------------- +set_insert = $(__gmsl_tr2)$(sort $1 $2) + +# ---------------------------------------------------------------------------- +# Function: set_remove +# Arguments: 1: A single element to remove from a set +# 2: A set +# Returns: Returns the set with the element removed +# ---------------------------------------------------------------------------- +set_remove = $(__gmsl_tr2)$(filter-out $1,$2) + +# ---------------------------------------------------------------------------- +# Function: set_is_member, set_is_not_member +# Arguments: 1: A single element +# 2: A set +# Returns: (set_is_member) Returns $(true) if the element is in the set +# (set_is_not_member) Returns $(false) if the element is in the set +# ---------------------------------------------------------------------------- +set_is_member = $(__gmsl_tr2)$(if $(filter $1,$2),$(true),$(false)) +set_is_not_member = $(__gmsl_tr2)$(if $(filter $1,$2),$(false),$(true)) + +# ---------------------------------------------------------------------------- +# Function: set_union +# Arguments: 1: A set +# 2: Another set +# Returns: Returns the union of the two sets +# ---------------------------------------------------------------------------- +set_union = $(__gmsl_tr2)$(sort $1 $2) + +# ---------------------------------------------------------------------------- +# Function: set_intersection +# Arguments: 1: A set +# 2: Another set +# Returns: Returns the intersection of the two sets +# ---------------------------------------------------------------------------- +set_intersection = $(__gmsl_tr2)$(filter $1,$2) + +# ---------------------------------------------------------------------------- +# Function: set_is_subset +# Arguments: 1: A set +# 2: Another set +# Returns: Returns $(true) if the first set is a subset of the second +# ---------------------------------------------------------------------------- +set_is_subset = $(__gmsl_tr2)$(call set_equal,$(call set_intersection,$1,$2),$1) + +# ---------------------------------------------------------------------------- +# Function: set_equal +# Arguments: 1: A set +# 2: Another set +# Returns: Returns $(true) if the two sets are identical +# ---------------------------------------------------------------------------- +set_equal = $(__gmsl_tr2)$(call seq,$1,$2) + +# ########################################################################### +# ARITHMETIC LIBRARY +# ########################################################################### + +# Integers a represented by lists with the equivalent number of x's. +# For example the number 4 is x x x x. + +# ---------------------------------------------------------------------------- +# Function: int_decode +# Arguments: 1: A number of x's representation +# Returns: Returns the integer for human consumption that is represented +# by the string of x's +# ---------------------------------------------------------------------------- +int_decode = $(__gmsl_tr1)$(words $1) + +# ---------------------------------------------------------------------------- +# Function: int_encode +# Arguments: 1: A number in human-readable integer form +# Returns: Returns the integer encoded as a string of x's +# ---------------------------------------------------------------------------- +__int_encode = $(if $1,$(if $(call seq,$(words $(wordlist 1,$1,$2)),$1),$(wordlist 1,$1,$2),$(call __int_encode,$1,$(if $2,$2 $2,x)))) +__strip_leading_zero = $(if $1,$(if $(call seq,$(patsubst 0%,%,$1),$1),$1,$(call __strip_leading_zero,$(patsubst 0%,%,$1))),0) +int_encode = $(__gmsl_tr1)$(call __int_encode,$(call __strip_leading_zero,$1)) + +# The arithmetic library functions come in two forms: one form of each +# function takes integers as arguments and the other form takes the +# encoded form (x's created by a call to int_encode). For example, +# there are two plus functions: +# +# plus Called with integer arguments and returns an integer +# int_plus Called with encoded arguments and returns an encoded result +# +# plus will be slower than int_plus because its arguments and result +# have to be translated between the x's format and integers. If doing +# a complex calculation use the int_* forms with a single encoding of +# inputs and single decoding of the output. For simple calculations +# the direct forms can be used. + +# Helper function used to wrap an int_* function into a function that +# takes a pair of integers, perhaps a function and returns an integer +# result +__gmsl_int_wrap = $(call int_decode,$(call $1,$(call int_encode,$2),$(call int_encode,$3))) +__gmsl_int_wrap1 = $(call int_decode,$(call $1,$(call int_encode,$2))) +__gmsl_int_wrap2 = $(call $1,$(call int_encode,$2),$(call int_encode,$3)) + +# ---------------------------------------------------------------------------- +# Function: int_plus +# Arguments: 1: A number in x's representation +# 2: Another number in x's represntation +# Returns: Returns the sum of the two numbers in x's representation +# ---------------------------------------------------------------------------- +int_plus = $(strip $(__gmsl_tr2)$1 $2) + +# ---------------------------------------------------------------------------- +# Function: plus (wrapped version of int_plus) +# Arguments: 1: An integer +# 2: Another integer +# Returns: Returns the sum of the two integers +# ---------------------------------------------------------------------------- +plus = $(__gmsl_tr2)$(call __gmsl_int_wrap,int_plus,$1,$2) + +# ---------------------------------------------------------------------------- +# Function: int_subtract +# Arguments: 1: A number in x's representation +# 2: Another number in x's represntation +# Returns: Returns the difference of the two numbers in x's representation, +# or outputs an error on a numeric underflow +# ---------------------------------------------------------------------------- +int_subtract = $(strip $(__gmsl_tr2)$(if $(call int_gte,$1,$2), \ + $(filter-out xx,$(join $1,$2)), \ + $(call __gmsl_warning,Subtraction underflow))) + +# ---------------------------------------------------------------------------- +# Function: subtract (wrapped version of int_subtract) +# Arguments: 1: An integer +# 2: Another integer +# Returns: Returns the difference of the two integers, +# or outputs an error on a numeric underflow +# ---------------------------------------------------------------------------- +subtract = $(__gmsl_tr2)$(call __gmsl_int_wrap,int_subtract,$1,$2) + +# ---------------------------------------------------------------------------- +# Function: int_multiply +# Arguments: 1: A number in x's representation +# 2: Another number in x's represntation +# Returns: Returns the product of the two numbers in x's representation +# ---------------------------------------------------------------------------- +int_multiply = $(strip $(__gmsl_tr2)$(foreach a,$1,$2)) + +# ---------------------------------------------------------------------------- +# Function: multiply (wrapped version of int_multiply) +# Arguments: 1: An integer +# 2: Another integer +# Returns: Returns the product of the two integers +# ---------------------------------------------------------------------------- +multiply = $(__gmsl_tr2)$(call __gmsl_int_wrap,int_multiply,$1,$2) + +# ---------------------------------------------------------------------------- +# Function: int_divide +# Arguments: 1: A number in x's representation +# 2: Another number in x's represntation +# Returns: Returns the result of integer division of argument 1 divided +# by argument 2 in x's representation +# ---------------------------------------------------------------------------- +int_divide = $(__gmsl_tr2)$(strip $(if $1,$(if $2, \ + $(if $(call int_gte,$1,$2), \ + x $(call int_divide,$(call int_subtract,$1,$2),$2),), \ + $(call __gmsl_error,Division by zero)))) + +# ---------------------------------------------------------------------------- +# Function: divide (wrapped version of int_divide) +# Arguments: 1: An integer +# 2: Another integer +# Returns: Returns the integer division of the first argument by the second +# ---------------------------------------------------------------------------- +divide = $(__gmsl_tr2)$(call __gmsl_int_wrap,int_divide,$1,$2) + +# ---------------------------------------------------------------------------- +# Function: int_max, int_min +# Arguments: 1: A number in x's representation +# 2: Another number in x's represntation +# Returns: Returns the maximum or minimum of its arguments in x's +# representation +# ---------------------------------------------------------------------------- +int_max = $(__gmsl_tr2)$(subst xx,x,$(join $1,$2)) +int_min = $(__gmsl_tr2)$(subst xx,x,$(filter xx,$(join $1,$2))) + +# ---------------------------------------------------------------------------- +# Function: max, min +# Arguments: 1: An integer +# 2: Another integer +# Returns: Returns the maximum or minimum of its integer arguments +# ---------------------------------------------------------------------------- +max = $(__gmsl_tr2)$(call __gmsl_int_wrap,int_max,$1,$2) +min = $(__gmsl_tr2)$(call __gmsl_int_wrap,int_min,$1,$2) + +# ---------------------------------------------------------------------------- +# Function: int_gt, int_gte, int_lt, int_lte, int_eq, int_ne +# Arguments: Two x's representation numbers to be compared +# Returns: $(true) or $(false) +# +# int_gt First argument greater than second argument +# int_gte First argument greater than or equal to second argument +# int_lt First argument less than second argument +# int_lte First argument less than or equal to second argument +# int_eq First argument is numerically equal to the second argument +# int_ne First argument is not numerically equal to the second argument +# ---------------------------------------------------------------------------- +int_gt = $(__gmsl_tr2)$(call __gmsl_make_bool, \ + $(filter-out $(words $2), \ + $(words $(call int_max,$1,$2)))) +int_gte = $(__gmsl_tr2)$(call __gmsl_make_bool, \ + $(call int_gt,$1,$2)$(call int_eq,$1,$2)) +int_lt = $(__gmsl_tr2)$(call __gmsl_make_bool, \ + $(filter-out $(words $1), \ + $(words $(call int_max,$1,$2)))) +int_lte = $(__gmsl_tr2)$(call __gmsl_make_bool, \ + $(call int_lt,$1,$2)$(call int_eq,$1,$2)) +int_eq = $(__gmsl_tr2)$(call __gmsl_make_bool, \ + $(filter $(words $1),$(words $2))) +int_ne = $(__gmsl_tr2)$(call __gmsl_make_bool, \ + $(filter-out $(words $1),$(words $2))) + +# ---------------------------------------------------------------------------- +# Function: gt, gte, lt, lte, eq, ne +# Arguments: Two integers to be compared +# Returns: $(true) or $(false) +# +# gt First argument greater than second argument +# gte First argument greater than or equal to second argument +# lt First argument less than second argument +# lte First argument less than or equal to second argument +# eq First argument is numerically equal to the second argument +# ne First argument is not numerically equal to the second argument +# ---------------------------------------------------------------------------- +gt = $(__gmsl_tr2)$(call __gmsl_int_wrap2,int_gt,$1,$2) +gte = $(__gmsl_tr2)$(call __gmsl_int_wrap2,int_gte,$1,$2) +lt = $(__gmsl_tr2)$(call __gmsl_int_wrap2,int_lt,$1,$2) +lte = $(__gmsl_tr2)$(call __gmsl_int_wrap2,int_lte,$1,$2) +eq = $(__gmsl_tr2)$(call __gmsl_int_wrap2,int_eq,$1,$2) +ne = $(__gmsl_tr2)$(call __gmsl_int_wrap2,int_ne,$1,$2) + +# increment adds 1 to its argument, decrement subtracts 1. Note that +# decrement does not range check and hence will not underflow, but +# will incorrectly say that 0 - 1 = 0 + +# ---------------------------------------------------------------------------- +# Function: int_inc +# Arguments: 1: A number in x's representation +# Returns: The number incremented by 1 in x's representation +# ---------------------------------------------------------------------------- +int_inc = $(strip $(__gmsl_tr1)$1 x) + +# ---------------------------------------------------------------------------- +# Function: inc +# Arguments: 1: An integer +# Returns: The argument incremented by 1 +# ---------------------------------------------------------------------------- +inc = $(__gmsl_tr1)$(call __gmsl_int_wrap1,int_inc,$1) + +# ---------------------------------------------------------------------------- +# Function: int_dec +# Arguments: 1: A number in x's representation +# Returns: The number decremented by 1 in x's representation +# ---------------------------------------------------------------------------- +int_dec = $(__gmsl_tr1)$(strip \ + $(if $(call sne,0,$(words $1)), \ + $(wordlist 2,$(words $1),$1))) + +# ---------------------------------------------------------------------------- +# Function: dec +# Arguments: 1: An integer +# Returns: The argument decremented by 1 +# ---------------------------------------------------------------------------- +dec = $(__gmsl_tr1)$(call __gmsl_int_wrap1,int_dec,$1) + +# double doubles its argument, and halve halves it + +# ---------------------------------------------------------------------------- +# Function: int_double +# Arguments: 1: A number in x's representation +# Returns: The number doubled (i.e. * 2) and returned in x's representation +# ---------------------------------------------------------------------------- +int_double = $(strip $(__gmsl_tr1)$1 $1) + +# ---------------------------------------------------------------------------- +# Function: double +# Arguments: 1: An integer +# Returns: The integer times 2 +# ---------------------------------------------------------------------------- +double = $(__gmsl_tr1)$(call __gmsl_int_wrap1,int_double,$1) + +# ---------------------------------------------------------------------------- +# Function: int_halve +# Arguments: 1: A number in x's representation +# Returns: The number halved (i.e. / 2) and returned in x's representation +# ---------------------------------------------------------------------------- +int_halve = $(__gmsl_tr1)$(strip $(subst xx,x,$(filter-out xy x y, \ + $(join $1,$(foreach a,$1,y x))))) + +# ---------------------------------------------------------------------------- +# Function: halve +# Arguments: 1: An integer +# Returns: The integer divided by 2 +# ---------------------------------------------------------------------------- +halve = $(__gmsl_tr1)$(call __gmsl_int_wrap1,int_halve,$1) + +# ---------------------------------------------------------------------------- +# Function: sequence +# Arguments: 1: An integer +# 2: An integer +# Returns: The sequence [arg1, arg2] of integers if arg1 < arg2 or +# [arg2, arg1] if arg2 > arg1. If arg1 == arg1 return [arg1] +# ---------------------------------------------------------------------------- +sequence = $(__gmsl_tr2)$(strip $(if $(call lte,$1,$2), \ + $(call __gmsl_sequence_up,$1,$2), \ + $(call __gmsl_sequence_dn,$2,$1))) + +__gmsl_sequence_up = $(if $(call seq,$1,$2),$1,$1 $(call __gmsl_sequence_up,$(call inc,$1),$2)) +__gmsl_sequence_dn = $(if $(call seq,$1,$2),$1,$2 $(call __gmsl_sequence_dn,$1,$(call dec,$2))) + +# ---------------------------------------------------------------------------- +# Function: dec2hex, dec2bin, dec2oct +# Arguments: 1: An integer +# Returns: The decimal argument converted to hexadecimal, binary or +# octal +# ---------------------------------------------------------------------------- + +__gmsl_digit = $(subst 15,f,$(subst 14,e,$(subst 13,d,$(subst 12,c,$(subst 11,b,$(subst 10,a,$1)))))) + +dec2hex = $(call __gmsl_dec2base,$(call int_encode,$1),$(call int_encode,16)) +dec2bin = $(call __gmsl_dec2base,$(call int_encode,$1),$(call int_encode,2)) +dec2oct = $(call __gmsl_dec2base,$(call int_encode,$1),$(call int_encode,8)) + +__gmsl_base_divide = $(subst $2,X ,$1) +__gmsl_q = $(strip $(filter X,$1)) +__gmsl_r = $(words $(filter x,$1)) + +__gmsl_dec2base = $(eval __gmsl_temp := $(call __gmsl_base_divide,$1,$2))$(call __gmsl_dec2base_,$(call __gmsl_q,$(__gmsl_temp)),$(call __gmsl_r,$(__gmsl_temp)),$2) +__gmsl_dec2base_ = $(if $1,$(call __gmsl_dec2base,$(subst X,x,$1),$3))$(call __gmsl_digit,$2) + +ifdef __gmsl_have_eval +# ########################################################################### +# ASSOCIATIVE ARRAYS +# ########################################################################### + +# Magic string that is very unlikely to appear in a key or value + +__gmsl_aa_magic := faf192c8efbc25c27992c5bc5add390393d583c6 + +# ---------------------------------------------------------------------------- +# Function: set +# Arguments: 1: Name of associative array +# 2: The key value to associate +# 3: The value associated with the key +# Returns: Nothing +# ---------------------------------------------------------------------------- +set = $(__gmsl_tr3)$(call assert_no_space,$0,$1$2)$(call assert_no_dollar,$0,$1$2$3)$(eval __gmsl_aa_$1_$(__gmsl_aa_magic)_$2_gmsl_aa_$1 := $3) + +# Only used internally by memoize function + +__gmsl_set = $(call set,$1,$2,$3)$3 + +# ---------------------------------------------------------------------------- +# Function: get +# Arguments: 1: Name of associative array +# 2: The key to retrieve +# Returns: The value stored in the array for that key +# ---------------------------------------------------------------------------- +get = $(strip $(__gmsl_tr2)$(call assert_no_space,$0,$1$2)$(call assert_no_dollar,$0,$1$2)$(__gmsl_aa_$1_$(__gmsl_aa_magic)_$2_gmsl_aa_$1)) + +# ---------------------------------------------------------------------------- +# Function: keys +# Arguments: 1: Name of associative array +# Returns: Returns a list of all defined keys in the array +# ---------------------------------------------------------------------------- +keys = $(__gmsl_tr1)$(call assert_no_space,$0,$1)$(call assert_no_dollar,$0,$1)$(sort $(patsubst __gmsl_aa_$1_$(__gmsl_aa_magic)_%_gmsl_aa_$1,%, \ + $(filter __gmsl_aa_$1_$(__gmsl_aa_magic)_%_gmsl_aa_$1,$(.VARIABLES)))) + +# ---------------------------------------------------------------------------- +# Function: defined +# Arguments: 1: Name of associative array +# 2: The key to test +# Returns: Returns true if the key is defined (i.e. not empty) +# ---------------------------------------------------------------------------- +defined = $(__gmsl_tr2)$(call assert_no_space,$0,$1$2)$(call assert_no_dollar,$0,$1$2)$(call sne,$(call get,$1,$2),) + +endif # __gmsl_have_eval + +ifdef __gmsl_have_eval +# ########################################################################### +# NAMED STACKS +# ########################################################################### + +# ---------------------------------------------------------------------------- +# Function: push +# Arguments: 1: Name of stack +# 2: Value to push onto the top of the stack (must not contain +# a space) +# Returns: None +# ---------------------------------------------------------------------------- +push = $(__gmsl_tr2)$(call assert_no_space,$0,$1$2)$(call assert_no_dollar,$0,$1$2)$(eval __gmsl_stack_$1 := $2 $(if $(filter-out undefined,\ + $(origin __gmsl_stack_$1)),$(__gmsl_stack_$1))) + +# ---------------------------------------------------------------------------- +# Function: pop +# Arguments: 1: Name of stack +# Returns: Top element from the stack after removing it +# ---------------------------------------------------------------------------- +pop = $(__gmsl_tr1)$(call assert_no_space,$0,$1)$(call assert_no_dollar,$0,$1)$(strip $(if $(filter-out undefined,$(origin __gmsl_stack_$1)), \ + $(call first,$(__gmsl_stack_$1)) \ + $(eval __gmsl_stack_$1 := $(call rest,$(__gmsl_stack_$1))))) + +# ---------------------------------------------------------------------------- +# Function: peek +# Arguments: 1: Name of stack +# Returns: Top element from the stack without removing it +# ---------------------------------------------------------------------------- +peek = $(__gmsl_tr1)$(call assert_no_space,$0,$1)$(call assert_no_dollar,$0,$1)$(call first,$(__gmsl_stack_$1)) + +# ---------------------------------------------------------------------------- +# Function: depth +# Arguments: 1: Name of stack +# Returns: Number of items on the stack +# ---------------------------------------------------------------------------- +depth = $(__gmsl_tr1)$(call assert_no_space,$0,$1)$(call assert_no_dollar,$0,$1)$(words $(__gmsl_stack_$1)) + +endif # __gmsl_have_eval + +ifdef __gmsl_have_eval +# ########################################################################### +# STRING CACHE +# ########################################################################### + +# ---------------------------------------------------------------------------- +# Function: memoize +# Arguments: 1. Name of the function to be called if the string +# has not been previously seen +# 2. A string +# Returns: Returns the result of a memo function (which the user must +# define) on the passed in string and remembers the result. +# +# Example: Set memo = $(shell echo "$1" | md5sum) to make a cache +# of MD5 hashes of strings. $(call memoize,memo,foo bar baz) +# ---------------------------------------------------------------------------- +__gmsl_memoize = $(subst $(__gmsl_space),§,$1)cc2af1bb7c4482f2ba75e338b963d3e7$(subst $(__gmsl_space),§,$2) +memoize = $(__gmsl_tr2)$(strip $(if $(call defined,__gmsl_m,$(__gmsl_memoize)),\ + $(call get,__gmsl_m,$(__gmsl_memoize)), \ + $(call __gmsl_set,__gmsl_m,$(__gmsl_memoize),$(call $1,$2)))) + +endif # __gmsl_have_eval + +# ########################################################################### +# DEBUGGING FACILITIES +# ########################################################################### + +# ---------------------------------------------------------------------------- +# Target: gmsl-print-% +# Arguments: The % should be replaced by the name of a variable that you +# wish to print out. +# Action: Echos the name of the variable that matches the % and its value. +# For example, 'make gmsl-print-SHELL' will output the value of +# the SHELL variable +# ---------------------------------------------------------------------------- +gmsl-print-%: ; @echo $* = $($*) + +# ---------------------------------------------------------------------------- +# Function: assert +# Arguments: 1: A boolean that must be true or the assertion will fail +# 2: The message to print with the assertion +# Returns: None +# ---------------------------------------------------------------------------- +assert = $(if $2,$(if $1,,$(call __gmsl_error,Assertion failure: $2))) + +# ---------------------------------------------------------------------------- +# Function: assert_exists +# Arguments: 1: Name of file that must exist, if it is missing an assertion +# will be generated +# Returns: None +# ---------------------------------------------------------------------------- +assert_exists = $(if $0,$(call assert,$(wildcard $1),file '$1' missing)) + +# ---------------------------------------------------------------------------- +# Function: assert_no_dollar +# Arguments: 1: Name of a function being executd +# 2: Arguments to check +# Returns: None +# ---------------------------------------------------------------------------- +assert_no_dollar = $(call __gmsl_tr2)$(call assert,$(call not,$(findstring $(__gmsl_dollar),$2)),$1 called with a dollar sign in argument) + +# ---------------------------------------------------------------------------- +# Function: assert_no_space +# Arguments: 1: Name of a function being executd +# 2: Arguments to check +# Returns: None +# ---------------------------------------------------------------------------- +ifeq ($(__gmsl_spaced_vars),$(false)) +assert_no_space = $(call assert,$(call not,$(findstring $(__gmsl_aa_magic),$(subst $(__gmsl_space),$(__gmsl_aa_magic),$2))),$1 called with a space in argument) +else +assert_no_space = +endif diff --git a/make/gmsl/gmsl b/make/gmsl/gmsl new file mode 100644 index 0000000..17891f1 --- /dev/null +++ b/make/gmsl/gmsl @@ -0,0 +1,85 @@ +# ---------------------------------------------------------------------------- +# +# GNU Make Standard Library (GMSL) +# +# A library of functions to be used with GNU Make's $(call) that +# provides functionality not available in standard GNU Make. +# +# Copyright (c) 2005-2014 John Graham-Cumming +# +# This file is part of GMSL +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# Neither the name of the John Graham-Cumming nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# ---------------------------------------------------------------------------- + +# Determine if the library has already been included and if so don't +# bother including it again + +ifndef __gmsl_included + +# Standard definitions for true and false. true is any non-empty +# string, false is an empty string. These are intended for use with +# $(if). + +true := T +false := + +# ---------------------------------------------------------------------------- +# Function: not +# Arguments: 1: A boolean value +# Returns: Returns the opposite of the arg. (true -> false, false -> true) +# ---------------------------------------------------------------------------- +not = $(if $1,$(false),$(true)) + +# Prevent reinclusion of the library + +__gmsl_included := $(true) + +# Try to determine where this file is located. If the caller did +# include /foo/gmsl then extract the /foo/ so that __gmsl gets +# included transparently + +__gmsl_root := + +ifneq ($(MAKEFILE_LIST),) +__gmsl_root := $(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST)) + +# If there are any spaces in the path in __gmsl_root then give up + +ifeq (1,$(words $(__gmsl_root))) +__gmsl_root := $(patsubst %gmsl,%,$(__gmsl_root)) +endif + +endif + +include $(__gmsl_root)__gmsl + +endif # __gmsl_included + diff --git a/src/adc/ADC.c b/src/adc/ADC.c index f9f22f9..ffa710c 100644 --- a/src/adc/ADC.c +++ b/src/adc/ADC.c @@ -19,6 +19,7 @@ #include #include +#include /** * \file @@ -44,14 +45,25 @@ static const uint8_t ADC_moduleNum[4] = { /** * Cached ADC conversion results for interrupts 0-3. */ -static volatile uint16_t ADC_convResult[4] = {0}; +static volatile uint16_t ADC_convResult[4]; + +/** + * Filter function pointers for interrupts 0-3. + * NULL when the filter isn't set. + */ +static volatile ADC_Filter_t ADC_filterPtr[4]; + +/** + * Filter user-defined data for interrupts 0-3. + */ +static volatile uint32_t ADC_filterData[4]; /** * Convenience macro to define ADC IRQ handlers. */ #define ADC_DEFINE_IRQ_HANDLER(n) void ADC0 ## n ## _IRQHandler() { \ - ADC_convResult[n] = EADC_GET_CONV_DATA(EADC, ADC_moduleNum[n]); \ - EADC_DISABLE_SAMPLE_MODULE_INT(EADC, n, 1 << ADC_moduleNum[n]); \ + uint16_t value = EADC_GET_CONV_DATA(EADC, ADC_moduleNum[n]); \ + ADC_convResult[n] = ADC_filterPtr[n] ? ADC_filterPtr[n](value, ADC_filterData[n]) : value; \ EADC_CLR_INT_FLAG(EADC, 1 << n); \ } @@ -60,32 +72,44 @@ ADC_DEFINE_IRQ_HANDLER(1); ADC_DEFINE_IRQ_HANDLER(2); ADC_DEFINE_IRQ_HANDLER(3); -void ADC_UpdateCache(const uint8_t moduleNum[], uint8_t len, uint8_t isBlocking) { - uint8_t i, j, finishFlag; +/** + * Finds the interrupt number for a module number. + * This is an internal function. + * + * @param moduleNum Module number. + * + * @return Interrupt number, or a negative value if not found. + */ +static int8_t ADC_LookupIntNum(uint8_t moduleNum) { + int8_t i; + + // Reversed so that it goes to -1 on failure + for(i = 3; i >= 0 && moduleNum != ADC_moduleNum[i]; i--); - // Enter critical section - __set_PRIMASK(1); + return i; +} + +void ADC_UpdateCache(const uint8_t moduleNum[], uint8_t len, uint8_t isBlocking) { + int8_t intNum; + uint8_t i, finishFlag; + uint32_t primask; for(i = 0; i < len; i++) { - // Find interrupt number for module number - for(j = 0; j < 4 && moduleNum[i] != ADC_moduleNum[j]; j++); + if((intNum = ADC_LookupIntNum(moduleNum[i])) < 0) { + continue; + } + primask = Thread_IrqDisable(); if(!(EADC_GET_PENDING_CONV(EADC) & (1 << moduleNum[i]))) { - // Configure module + // Configure module to a sane state EADC_ConfigSampleModule(EADC, moduleNum[i], EADC_SOFTWARE_TRIGGER, moduleNum[i]); - - // Enable interrupt - EADC_CLR_INT_FLAG(EADC, 1 << j); - EADC_ENABLE_SAMPLE_MODULE_INT(EADC, j, 1 << moduleNum[i]); - + EADC_CLR_INT_FLAG(EADC, 1 << intNum); // Start conversion EADC_START_CONV(EADC, 1 << moduleNum[i]); } + Thread_IrqRestore(primask); } - // Exit critical section - __set_PRIMASK(0); - if(isBlocking) { // Wait for modules to finish // Keep in mind they could be restarted by another concurrent @@ -102,13 +126,8 @@ void ADC_UpdateCache(const uint8_t moduleNum[], uint8_t len, uint8_t isBlocking) } uint16_t ADC_GetCachedResult(uint8_t moduleNum) { - uint8_t i; - - // Find interrupt number for module number - for(i = 0; i < 4 && moduleNum != ADC_moduleNum[i]; i++); - - // Atomic - return ADC_convResult[i]; + int8_t intNum = ADC_LookupIntNum(moduleNum); + return intNum < 0 ? 0 : ADC_convResult[intNum]; } void ADC_Init() { @@ -141,6 +160,7 @@ void ADC_Init() { // Enable interrupts for(i = 0; i < 4; i++) { EADC_ENABLE_INT(EADC, 1 << i); + EADC_ENABLE_SAMPLE_MODULE_INT(EADC, i, 1 << ADC_moduleNum[i]); NVIC_EnableIRQ(irqNum[i]); } } @@ -148,4 +168,19 @@ void ADC_Init() { uint16_t ADC_Read(uint8_t moduleNum) { ADC_UpdateCache((uint8_t []) {moduleNum}, 1, 1); return ADC_GetCachedResult(moduleNum); -} \ No newline at end of file +} + +void ADC_SetFilter(uint8_t moduleNum, ADC_Filter_t filter, uint32_t filterData) { + int8_t intNum; + + if((intNum = ADC_LookupIntNum(moduleNum)) < 0) { + return; + } + + // To avoid races: disable old -> update data -> enable new + ADC_filterPtr[intNum] = NULL; + if(filter != NULL) { + ADC_filterData[intNum] = filterData; + ADC_filterPtr[intNum] = filter; + } +} diff --git a/src/atomizer/Atomizer.c b/src/atomizer/Atomizer.c index e2212bc..65d4899 100755 --- a/src/atomizer/Atomizer.c +++ b/src/atomizer/Atomizer.c @@ -18,12 +18,14 @@ * Copyright (C) 2016 kfazz */ +#include #include #include #include #include #include #include +#include /** * \file @@ -41,12 +43,23 @@ #define ATOMIZER_PWMCH_BUCK 0 #define ATOMIZER_PWMCH_BOOST 2 -/* Macros to convert ADC values */ +/* Feedback loop frequency (Hz) */ +#define ATOMIZER_LOOP_FREQ 10000 + +/* Warmup timer: 10 feedback iterations */ +#define ATOMIZER_TMRCNT_WARMUP 10 +/* Refresh timer: 200ms */ +#define ATOMIZER_TMRCNT_REFRESH (200 * ATOMIZER_LOOP_FREQ / 1000) + +/* Median filter window size (must be odd) */ +#define ATOMIZER_MEDIANFILTER_WINDOW 5 + +/* Macros to convert ADC readings to absolute values */ // Read voltage is x * ADC_VREF / ADC_DENOMINATOR. // This is 3/13 of actual voltage, so we multiply by 13/3. // Result is in 10mV units. // Maximum result size: 11 bits. -#define ATOMIZER_ADC_VOLTAGE(x) (13L * (x) * ADC_VREF / 30L / ADC_DENOMINATOR) +#define ATOMIZER_ADC_VOLTAGE(x) ((x) * (13L * ADC_VREF) / (30L * ADC_DENOMINATOR)) // Voltage drop on shunt (100x gain) is x * ADC_VREF / ADC_DENOMINATOR. // Current is Vdrop / R, units are in mV/mOhm, R has 100x gain too (100ths of mOhm). // So current will be in A. We multiply by 1000 to get mA. @@ -75,6 +88,14 @@ // Board temperature limit is 70°C. // Simplified from: ATOMIZER_ADC_THERMRES(x) <= Atomizer_boardTempTable[14] #define ATOMIZER_ADC_OVERTEMP(x) (41L * (x) <= 16480L) + +/* Macros to convert absolute values to ADC readings */ +// Simplified expression: x = I * Atomizer_shuntRes * ADC_DENOMINATOR / ADC_VREF / 1000 +// See ATOMIZER_ADC_CURRENT for more details on the expression. +// To avoid overflows in the nominator, ADC_VREF and ADC_DENOMINATOR are hardcoded. +// Maximum result size (for 16-bit input): 14 bits. +#define ATOMIZER_ADCINV_CURRENT(I) ((I) * 8L * Atomizer_shuntRes / 5000L) + // This assumes a battery internal resistance of 10mOhm (which is pretty low). // It takes the target output voltage in 10mV units, the atomizer resistance in mOhm and // the battery voltage in mV. The battery is weak if it's under 3.1V, or if it's expected @@ -85,23 +106,27 @@ // Timer flags #define ATOMIZER_TMRFLAG_WARMUP (1 << 0) #define ATOMIZER_TMRFLAG_REFRESH (1 << 1) -#define ATOMIZER_TIMER_WARMUP_RESET() do { __set_PRIMASK(1); \ - Atomizer_timerCountWarmup = 0; \ +#define ATOMIZER_TIMER_WARMUP_RESET() do { \ + uint32_t primask = Thread_IrqDisable(); \ + Atomizer_timerCountWarmup = ATOMIZER_TMRCNT_WARMUP + 1; \ Atomizer_timerFlag &= ~ATOMIZER_TMRFLAG_WARMUP; \ - __set_PRIMASK(0); } while(0) -#define ATOMIZER_TIMER_REFRESH_RESET() do { __set_PRIMASK(1); \ - Atomizer_timerCountRefresh = 0; \ + Thread_IrqRestore(primask); } while(0) +#define ATOMIZER_TIMER_REFRESH_RESET() do { \ + uint32_t primask = Thread_IrqDisable(); \ + Atomizer_timerCountRefresh = ATOMIZER_TMRCNT_REFRESH + 1; \ Atomizer_timerFlag &= ~ATOMIZER_TMRFLAG_REFRESH; \ - __set_PRIMASK(0); } while(0) -#define ATOMIZER_TIMER_RESET() do { __set_PRIMASK(1); \ - Atomizer_timerCountWarmup = 0; \ - Atomizer_timerCountRefresh = 0; \ - Atomizer_timerFlag = 0; \ - __set_PRIMASK(0); } while(0) + Thread_IrqRestore(primask); } while(0) // Busy wait for warmup or error #define ATOMIZER_WAIT_WARMUP() do {} while(!(Atomizer_timerFlag & ATOMIZER_TMRFLAG_WARMUP) && Atomizer_error == OK) +// Updates the ADC cache, blocking if block is true +#define ATOMIZER_ADC_UPDATECACHE(block) do { \ + ADC_UpdateCache((uint8_t []) { \ + ADC_MODULE_VATM, ADC_MODULE_CURS, \ + ADC_MODULE_VBAT, ADC_MODULE_TEMP \ + }, 4, block); } while(0) + // True when abs(a - b) > bound // Works for unsigned types #define ATOMIZER_DIFF_NOT_BOUND(a, b, bound) (((a) < (b) && (b) - (a) > (bound)) || ((a) > (b) && (a) - (b) > (bound))) @@ -124,15 +149,52 @@ typedef enum { POWERON_BOOST } Atomizer_ConverterState_t; +/** + * Struct to accumulate ADC data. + */ +typedef struct { + /** + * Voltage accumulator (ADC). + */ + uint32_t voltage; + /** + * Current accumulator (ADC). + */ + uint32_t current; + /** + * Resistance accumulator (mOhm). + */ + uint32_t resistance; + /** + * Accumulator counter. Counts down to zero. + */ + uint8_t count; +} Atomizer_ADCAccumulator_t; + +/** + * Struct to hold context for a median filter. + */ +typedef struct { + /** + * Sample buffer. + */ + uint16_t buf[ATOMIZER_MEDIANFILTER_WINDOW]; + /** + * Index of the oldest sample in the buffer. + */ + uint8_t idx; + +} Atomizer_MedianFilterCtx_t; + /** * Target voltage, in 10mV units. */ -static volatile uint16_t Atomizer_targetVolts = 0; +static volatile uint16_t Atomizer_targetVolts; /** * Current duty cycle. */ -static volatile uint16_t Atomizer_curCmr = 0; +static volatile uint16_t Atomizer_curCmr; /** * Current converters state. @@ -152,7 +214,6 @@ static volatile Atomizer_Error_t Atomizer_error; /** * Initial atomizer resistance, in mOhm. - * If an atomizer error occurs, this is set to zero. * Will be reset to zero on atomizer error. */ static volatile uint16_t Atomizer_baseRes; @@ -170,6 +231,11 @@ static volatile uint16_t Atomizer_tempRes; */ static volatile uint8_t Atomizer_baseTemp; +/** + * True if a measure is in progress. + */ +static volatile uint8_t Atomizer_isMeasuring; + /** * True if a measure has been forced. * Will be reset to false on atomizer error. @@ -182,14 +248,14 @@ static volatile uint8_t Atomizer_forceMeasure; static volatile uint16_t Atomizer_tempTargetVolts; /** - * Warmup timer counter. Each tick is 400us. - * Counts up to 6 (2ms) and stops. + * Warmup timer counter. One tick per feedback operation. + * Counts down to zero and sets ATOMIZER_TMRFLAG_WARMUP. */ static volatile uint8_t Atomizer_timerCountWarmup; /** - * Refresh timer counter. Each tick is 400us. - * Counts up to 500 (200ms) and stops. + * Refresh timer counter. One tick per feedback iteration. + * Counts down to zero and sets ATOMIZER_TMRFLAG_REFRESH. */ static volatile uint16_t Atomizer_timerCountRefresh; @@ -220,6 +286,29 @@ static volatile uint8_t Atomizer_isLocked; */ static volatile Atomizer_ErrorCallback_t Atomizer_errorCallbackPtr; +/** + * Overcurrent threshold (ADC value). + */ +static uint16_t Atomizer_adcOverCurrent; + +/** + * Atomizer mutex. + */ +static Thread_Mutex_t Atomizer_mutex; + +/** + * ADC data. + */ +static volatile Atomizer_ADCAccumulator_t Atomizer_adcAcc; + +/** + * Median filter contexts. + */ +static Atomizer_MedianFilterCtx_t Atomizer_medianFilterCtx[3]; +#define ATOMIZER_MEDIANFILTER_VOLTAGE Atomizer_medianFilterCtx[0] +#define ATOMIZER_MEDIANFILTER_CURRENT Atomizer_medianFilterCtx[1] +#define ATOMIZER_MEDIANFILTER_RESISTANCE Atomizer_medianFilterCtx[2] + /** * Thermistor resistance to board temperature lookup table. * boardTempTable[i] maps the 5°C range starting at 5*i °C. @@ -231,6 +320,42 @@ static const uint16_t Atomizer_boardTempTable[21] = { 1648, 1388, 1175, 999, 853, 732, 630 }; +/** + * Performs median filtering. + * Can be casted to ADC_Filter_t. + * + * @param value New sample. + * @param ctx Context to work on. + * + * @return New filtered sample. + */ +static uint16_t Atomizer_MedianFilter(uint16_t value, Atomizer_MedianFilterCtx_t *ctx) { + uint8_t i, j, minIdx; + uint16_t sortBuf[ATOMIZER_MEDIANFILTER_WINDOW], min; + + // Replace oldest sample with the new one + ctx->buf[ctx->idx] = value; + ctx->idx = (ctx->idx + 1) % ATOMIZER_MEDIANFILTER_WINDOW; + + // Selection sort. We only need to sort the first half. + memcpy(sortBuf, ctx->buf, sizeof(sortBuf)); + for(i = 0; i < (ATOMIZER_MEDIANFILTER_WINDOW + 1) / 2; i++) { + minIdx = i; + for(j = i + 1; j < ATOMIZER_MEDIANFILTER_WINDOW; j++) { + if(sortBuf[j] < sortBuf[minIdx]) { + minIdx = j; + } + } + if(i != minIdx) { + min = sortBuf[minIdx]; + sortBuf[minIdx] = sortBuf[i]; + sortBuf[i] = min; + } + } + + return sortBuf[ATOMIZER_MEDIANFILTER_WINDOW / 2]; +} + /** * Configures a PWM channel. * This is an internal function. @@ -301,6 +426,62 @@ static void Atomizer_ConfigureConverters(uint8_t enableBuck, uint8_t enableBoost } } +static void Atomizer_SetError(Atomizer_Error_t); + +/** + * Powers the atomizer on or off. + * This is an internal function. + * + * @param powerOn True to power the atomizer on, false to power it off. + */ +static void Atomizer_ControlUnlocked(uint8_t powerOn) { + uint8_t i; + uint16_t battVolts, resSeed; + + if(powerOn && (Atomizer_isLocked || Atomizer_error == SHORT)) { + // Lock atomizer after short or if locked by error + return; + } + + if((!powerOn && Atomizer_curState == POWEROFF) || (powerOn && Atomizer_curState != POWEROFF)) { + // Nothing to do + return; + } + + if(powerOn) { + // Don't even bother firing if the battery is weak + battVolts = Battery_GetVoltage(); + if(ATOMIZER_PREDICT_WEAKBATT(Atomizer_targetVolts, Atomizer_baseRes, battVolts)) { + Atomizer_SetError(WEAK_BATT); + return; + } + + // Reset filters used by the feedback loop + memset(Atomizer_medianFilterCtx, 0, sizeof(Atomizer_medianFilterCtx)); + // Seed resistance filter with ATOMIZER_RESISTANCE_MIN or base resistance + resSeed = Atomizer_baseRes == 0 ? ATOMIZER_RESISTANCE_MIN : Atomizer_baseRes; + for(i = 0; i < ATOMIZER_MEDIANFILTER_WINDOW; i++) { + ATOMIZER_MEDIANFILTER_RESISTANCE.buf[i] = resSeed; + } + + // Update ADC cache for the first feedback iteration, blocking + ATOMIZER_ADC_UPDATECACHE(1); + + // Start from buck with duty cycle 20 + Atomizer_error = OK; + Atomizer_curCmr = 20; + PWM_SET_CMR(PWM0, ATOMIZER_PWMCH_BUCK, Atomizer_curCmr); + Atomizer_ConfigureConverters(1, 0); + ATOMIZER_TIMER_WARMUP_RESET(); + Atomizer_curState = POWERON_BUCK; + } + else { + Atomizer_curState = POWEROFF; + Atomizer_ConfigureConverters(0, 0); + ATOMIZER_TIMER_REFRESH_RESET(); + } +} + /** * Sets the atomizer error, resetting the approriate * atomizer state if needed. If the error is not OK, @@ -311,9 +492,10 @@ static void Atomizer_ConfigureConverters(uint8_t enableBuck, uint8_t enableBoost static void Atomizer_SetError(Atomizer_Error_t error) { if(error != OK) { // Shutdown and reset measurement state - Atomizer_Control(0); + Atomizer_ControlUnlocked(0); Atomizer_tempRes = 0; Atomizer_forceMeasure = 0; + Atomizer_isMeasuring = 0; if(Atomizer_errorLock) { // Lock atomizer @@ -344,17 +526,8 @@ static void Atomizer_NegativeFeedback(uint32_t unused) { uint32_t resistance; Atomizer_ConverterState_t nextState; - // Update ADC cache without blocking. - // This loop always runs (until this point), even when - // the atomizer is not powered on. Let's exploit it to - // keep good values in the cache for when we power it up. - ADC_UpdateCache((uint8_t []) { - ADC_MODULE_VATM, ADC_MODULE_CURS, - ADC_MODULE_VBAT, ADC_MODULE_TEMP - }, 4, 0); - - if(Atomizer_timerCountRefresh != 500) { - Atomizer_timerCountRefresh++; + if(Atomizer_timerCountRefresh > 0) { + Atomizer_timerCountRefresh--; } else { Atomizer_timerFlag |= ATOMIZER_TMRFLAG_REFRESH; @@ -364,39 +537,66 @@ static void Atomizer_NegativeFeedback(uint32_t unused) { return; } - if(Atomizer_timerCountWarmup != 6) { - Atomizer_timerCountWarmup++; + if(Atomizer_timerCountWarmup > 0) { + Atomizer_timerCountWarmup--; } else { Atomizer_timerFlag |= ATOMIZER_TMRFLAG_WARMUP; } + // Update ADC cache for next iteration without blocking + ATOMIZER_ADC_UPDATECACHE(0); + // Get ADC readings adcVoltage = ADC_GetCachedResult(ADC_MODULE_VATM); adcCurrent = ADC_GetCachedResult(ADC_MODULE_CURS); adcBattery = ADC_GetCachedResult(ADC_MODULE_VBAT); adcBoardTemp = ADC_GetCachedResult(ADC_MODULE_TEMP); - Atomizer_error = OK; + // Critical checks + if(adcCurrent >= Atomizer_adcOverCurrent) { + Atomizer_SetError(SHORT); + return; + } + if(ATOMIZER_ADC_WEAKBATT(adcBattery)) { + Atomizer_SetError(WEAK_BATT); + return; + } if(ATOMIZER_ADC_OVERTEMP(adcBoardTemp)) { Atomizer_SetError(OVER_TEMP); + return; } - else if(ATOMIZER_ADC_WEAKBATT(adcBattery)) { - Atomizer_SetError(WEAK_BATT); + + // Calculate resistance (16-bit clamped) + resistance = ATOMIZER_ADC_RESISTANCE(adcVoltage, adcCurrent); + if(resistance > 0xFFFF) { + resistance = 0xFFFF; } - else if(Atomizer_timerCountWarmup > 3) { - // Start checking resistance after ~1ms - resistance = ATOMIZER_ADC_RESISTANCE(adcVoltage, adcCurrent); - if(resistance >= 5 && resistance < 40) { + + // Don't check resistance unless there's some precision + if(adcVoltage >= 5 && adcCurrent >= 5) { + // Filter resistance (filter is pre-seeded) + resistance = Atomizer_MedianFilter(resistance, &ATOMIZER_MEDIANFILTER_RESISTANCE); + + // Check resistance + if(resistance < ATOMIZER_RESISTANCE_MIN) { Atomizer_SetError(SHORT); + return; } - else if(adcVoltage != 0 && adcCurrent < 5) { + if(resistance > ATOMIZER_RESISTANCE_MAX) { Atomizer_SetError(OPEN); + return; } } - if(Atomizer_error != OK) { - return; + Atomizer_error = OK; + + // Accumulate ADC data after warmup + if((Atomizer_timerFlag & ATOMIZER_TMRFLAG_WARMUP) && Atomizer_adcAcc.count > 0) { + Atomizer_adcAcc.voltage += adcVoltage; + Atomizer_adcAcc.current += adcCurrent; + Atomizer_adcAcc.resistance += resistance; + Atomizer_adcAcc.count--; } curVolts = ATOMIZER_ADC_VOLTAGE(adcVoltage); @@ -409,7 +609,7 @@ static void Atomizer_NegativeFeedback(uint32_t unused) { if(curVolts < Atomizer_targetVolts) { if(Atomizer_curState == POWERON_BUCK) { - if(Atomizer_curCmr == 479) { + if(Atomizer_curCmr == 959) { // Reached maximum for buck, switch to boost nextState = POWERON_BOOST; } @@ -418,15 +618,15 @@ static void Atomizer_NegativeFeedback(uint32_t unused) { Atomizer_curCmr++; } } - else if(Atomizer_curCmr > 80) { - // Boost duty cycle must be greater than 80 + else if(Atomizer_curCmr > 160) { + // Boost duty cycle must be greater than 160 // In boost mode, decreased duty cycle = increased voltage Atomizer_curCmr--; } } else { if(Atomizer_curState == POWERON_BOOST) { - if(Atomizer_curCmr == 479) { + if(Atomizer_curCmr == 959) { // Reached minimum for boost, switch to buck nextState = POWERON_BUCK; } @@ -436,8 +636,8 @@ static void Atomizer_NegativeFeedback(uint32_t unused) { } } else { - if(Atomizer_curCmr <= 10) { - // Buck duty cycles below 10 are forced to zero + if(Atomizer_curCmr <= 20) { + // Buck duty cycles below 20 are forced to zero Atomizer_curCmr = 0; } else { @@ -488,6 +688,18 @@ void Atomizer_Init() { break; } + // Calculate overcurrent threshold + Atomizer_adcOverCurrent = ATOMIZER_ADCINV_CURRENT(ATOMIZER_CURRENT_MAX); + if(Atomizer_adcOverCurrent > ADC_DENOMINATOR - 1) { + Atomizer_adcOverCurrent = ADC_DENOMINATOR - 1; + } + + // Setup ADC median filtering + ADC_SetFilter(ADC_MODULE_VATM, (ADC_Filter_t) Atomizer_MedianFilter, + (uint32_t) &ATOMIZER_MEDIANFILTER_VOLTAGE); + ADC_SetFilter(ADC_MODULE_CURS, (ADC_Filter_t) Atomizer_MedianFilter, + (uint32_t) &ATOMIZER_MEDIANFILTER_CURRENT); + // Setup control pins PC1 = 0; GPIO_SetMode(PC, BIT1, GPIO_MODE_OUTPUT); @@ -511,68 +723,35 @@ void Atomizer_Init() { PWM_SET_CMR(PWM0, ATOMIZER_PWMCH_BUCK, 0); PWM_SET_CMR(PWM0, ATOMIZER_PWMCH_BOOST, 0); - Atomizer_targetVolts = 0; - Atomizer_curCmr = 0; - Atomizer_curState = POWEROFF; - Atomizer_error = OK; - Atomizer_baseRes = 0; - Atomizer_tempRes = 0; - Atomizer_baseTemp = 0; - Atomizer_forceMeasure = 0; - Atomizer_baseUpdateCallbackPtr = NULL; - Atomizer_errorLock = 0; - Atomizer_isLocked = 0; - Atomizer_errorCallbackPtr = NULL; - ATOMIZER_TIMER_RESET(); + // Create our Big Atomizer Lock + if(Thread_MutexCreate(&Atomizer_mutex) != TD_SUCCESS) { + // No user code has run yet, the heap is messed up + asm volatile ("udf"); + } - // Setup 2.5kHz timer for negative feedback cycle - // This function should run during system init, so + // Setup timer for the feedback loop. + // This function runs during system init, so // the user hasn't had time to create timers yet. - Timer_CreateTimer(2500, 1, Atomizer_NegativeFeedback, 0); + Timer_CreateTimer(ATOMIZER_LOOP_FREQ, 1, Atomizer_NegativeFeedback, 0); } void Atomizer_SetOutputVoltage(uint16_t volts) { - if(volts > ATOMIZER_MAX_VOLTS) { - volts = ATOMIZER_MAX_VOLTS; + if(volts < ATOMIZER_VOLTAGE_MIN) { + volts = ATOMIZER_VOLTAGE_MIN; + } + else if(volts > ATOMIZER_VOLTAGE_MAX) { + volts = ATOMIZER_VOLTAGE_MAX; } Atomizer_targetVolts = (volts + 5) / 10; } void Atomizer_Control(uint8_t powerOn) { - uint16_t battVolts; - - if(powerOn && (Atomizer_isLocked || Atomizer_error == SHORT)) { - // Lock atomizer after short or if locked by error - return; - } - - if((!powerOn && Atomizer_curState == POWEROFF) || (powerOn && Atomizer_curState != POWEROFF)) { - // Nothing to do - return; - } - - if(powerOn) { - // Don't even bother firing if the battery is weak - battVolts = Battery_GetVoltage(); - if(ATOMIZER_PREDICT_WEAKBATT(Atomizer_targetVolts, Atomizer_baseRes, battVolts)) { - Atomizer_SetError(WEAK_BATT); - return; - } - - // Start from buck with duty cycle 10 - Atomizer_error = OK; - Atomizer_curCmr = 10; - PWM_SET_CMR(PWM0, ATOMIZER_PWMCH_BUCK, Atomizer_curCmr); - Atomizer_ConfigureConverters(1, 0); - ATOMIZER_TIMER_WARMUP_RESET(); - Atomizer_curState = POWERON_BUCK; - } - else { - Atomizer_curState = POWEROFF; - Atomizer_ConfigureConverters(0, 0); - ATOMIZER_TIMER_REFRESH_RESET(); - } + // This is ISR safe. + // User ISRs won't preempt the feedback loop. + Thread_CriticalEnter(); + Atomizer_ControlUnlocked(powerOn); + Thread_CriticalExit(); } uint8_t Atomizer_IsOn() { @@ -581,7 +760,7 @@ uint8_t Atomizer_IsOn() { Atomizer_Error_t Atomizer_GetError() { // Mask OK code while resistance is stabilizing (on first measure) - return Atomizer_error == OK && !Atomizer_baseRes && Atomizer_tempRes ? OPEN : Atomizer_error; + return Atomizer_isMeasuring ? OPEN : Atomizer_error; } /** @@ -608,90 +787,102 @@ static void Atomizer_BaseUpdate(uint16_t newRes, uint8_t newTemp) { * Samples the atomizer info. * This is an internal function. * - * @param targetVolts Target volts for sampling (if not firing), in mV. - * @param voltage Pointer to store voltage (0 on error). - * @param current Pointer to store current (0 on error). - * @param resistance Pointer to store resistance (0 on error). + * @param targetVolts Target volts for sampling (if not firing), in 10mV units. + * If this is zero the atomizer will be assumed to be firing. + * @param voltage Pointer to store voltage in mV (optional, can be NULL). + * @param current Pointer to store current in mA (optional, can be NULL). + * @param resistance Pointer to store resistance in mOhm (optional, can be NULL). + * + * @return True on success, false if an atomizer error occurs. */ -static void Atomizer_Sample(uint16_t targetVolts, uint16_t *voltage, uint16_t *current, uint16_t *resistance) { - uint32_t vSum, iSum, adcRes; +static uint8_t Atomizer_Sample(uint16_t targetVolts, uint16_t *voltage, uint16_t *current, uint16_t *resistance) { + uint32_t vSum, iSum, res; uint16_t savedTargetVolts; - uint8_t i, newTemp; + uint8_t fromPowerOff, count, newTemp; - vSum = 0; - iSum = 0; + // OFF -> ON transistions are assumed to be locked. + // ON -> OFF transistions are assumed to be locked from userspace + // and will only happen if an atomizer error occurs. - if(Atomizer_curState == POWEROFF) { + fromPowerOff = (targetVolts && Atomizer_curState == POWEROFF); + count = fromPowerOff ? 50 : 1; + + // Reset accumulators + Atomizer_adcAcc.count = 0; + Atomizer_adcAcc.voltage = 0; + Atomizer_adcAcc.current = 0; + Atomizer_adcAcc.resistance = 0; + Atomizer_adcAcc.count = count; + + if(fromPowerOff) { // Power on atomizer for measurement savedTargetVolts = Atomizer_targetVolts; - Atomizer_SetOutputVoltage(targetVolts); - Atomizer_Control(1); - ATOMIZER_WAIT_WARMUP(); + Atomizer_targetVolts = targetVolts; + Atomizer_ControlUnlocked(1); + } - if(Atomizer_error == OK) { - // Sample and average V and I - for(i = 0; i < 50; i++) { - Timer_DelayUs(10); - vSum += ADC_Read(ADC_MODULE_VATM); - Timer_DelayUs(10); - iSum += ADC_Read(ADC_MODULE_CURS); - } - } + // Wait for accumulation to complete + while(Atomizer_adcAcc.count > 0 && Atomizer_error == OK); + if(fromPowerOff) { // Power off and restore target voltage - Atomizer_Control(0); - Atomizer_SetOutputVoltage(savedTargetVolts); - - *voltage = 0; - *current = 0; - } - else { - // Use cached V and I - vSum = ADC_GetCachedResult(ADC_MODULE_VATM); - iSum = ADC_GetCachedResult(ADC_MODULE_CURS); - - *voltage = ATOMIZER_ADC_VOLTAGE(vSum) * 10; - *current = ATOMIZER_ADC_CURRENT(iSum); + Atomizer_ControlUnlocked(0); + Atomizer_targetVolts = savedTargetVolts; } if(Atomizer_error != OK) { - *resistance = 0; - return; + goto error; } + // Avoid useless volatile accesses + vSum = Atomizer_adcAcc.voltage; + iSum = Atomizer_adcAcc.current; + + // If we take more than one sample, calculate resistance from + // accumulated voltage and current because it's more precise. + // If we take only one sample, use the accumulated resistance + // because it has been filtered. + res = count > 1 ? ATOMIZER_ADC_RESISTANCE(vSum, iSum) : Atomizer_adcAcc.resistance; + // The feedback cycle has more relaxed limits, // so we re-check the resistance. - adcRes = ATOMIZER_ADC_RESISTANCE(vSum, iSum); - if(adcRes >= 5 && adcRes < 50) { + if(res < ATOMIZER_RESISTANCE_MIN) { Atomizer_SetError(SHORT); + goto error; } - else if(adcRes > 3500) { + if(res > ATOMIZER_RESISTANCE_MAX) { Atomizer_SetError(OPEN); + goto error; + } + + if(voltage != NULL) { + *voltage = ATOMIZER_ADC_VOLTAGE(vSum / count) * 10; + } + if(current != NULL) { + *current = ATOMIZER_ADC_CURRENT(iSum / count); + } + if(resistance != NULL) { + // No need to clamp to 16-bit (see checks above) + *resistance = res; } - else { - *resistance = adcRes; - - // Since TCR is always positive, adcRes < baseRes - // implies a better resistance reading has been acquired. - if(adcRes >= 5 && adcRes < Atomizer_baseRes) { - // Also update base temperature if lower - newTemp = Atomizer_ReadBoardTemp(); - if(newTemp > Atomizer_baseTemp) { - newTemp = Atomizer_baseTemp; - } - Atomizer_BaseUpdate(adcRes, newTemp); + // Since TCR is always positive, res < baseRes + // implies a better resistance reading has been acquired. + if(res >= 5 && res < Atomizer_baseRes) { + // Also update base temperature if lower + newTemp = Atomizer_ReadBoardTemp(); + if(newTemp > Atomizer_baseTemp) { + newTemp = Atomizer_baseTemp; } - return; + Atomizer_BaseUpdate(res, newTemp); } - // Check failed - *voltage = 0; - *current = 0; - *resistance = 0; + return 1; - return; +error: + Atomizer_adcAcc.count = 0; + return 0; } /** @@ -700,34 +891,36 @@ static void Atomizer_Sample(uint16_t targetVolts, uint16_t *voltage, uint16_t *c * This is an internal function. */ static void Atomizer_Refresh() { - uint16_t voltage, current, resistance, targetVolts; + uint16_t resistance, targetVolts; if(Atomizer_tempRes == 0) { + Atomizer_isMeasuring = Atomizer_forceMeasure || !Atomizer_baseRes; + // Use a 300mV test voltage for refresh targetVolts = Atomizer_targetVolts; - Atomizer_SetOutputVoltage(300); - Atomizer_Control(1); + Atomizer_targetVolts = 30; + Atomizer_ControlUnlocked(1); ATOMIZER_WAIT_WARMUP(); - Atomizer_Control(0); - Atomizer_SetOutputVoltage(targetVolts); + Atomizer_ControlUnlocked(0); + Atomizer_targetVolts = targetVolts; - if((!Atomizer_forceMeasure && Atomizer_baseRes) || Atomizer_error != OK) { + if(!Atomizer_isMeasuring) { return; } + + // The atomizer has just been connected or a measure has just + // been forced. Start sampling at 1.00V. + targetVolts = 100; + } + else { + // Calculate test voltage for 1.5% target error. + targetVolts = (Atomizer_tempRes * 49L / 30L + 744L) / 10L; } - // If tempRes == 0, then the atomizer has just been connected or - // a measure has just been forced, so we start sampling at 1.00V. - // Otherwise, we use the previously calculated target voltage. - targetVolts = Atomizer_tempRes == 0 ? 100 : Atomizer_tempTargetVolts; - Atomizer_Sample(targetVolts, &voltage, ¤t, &resistance); - if(Atomizer_error != OK) { + if(!Atomizer_Sample(targetVolts, NULL, NULL, &resistance)) { return; } - // Calculate test voltage for 1.5% target error. - Atomizer_tempTargetVolts = (resistance * 49L / 30L + 744L) / 10L; - // Only update baseRes when resistance has stabilized to +/- 5mOhm. // This is needed because resistance fluctuates while screwing // in the 510 connector. @@ -744,10 +937,13 @@ static void Atomizer_Refresh() { if(Atomizer_baseRes == 0) { Atomizer_SetError(OPEN); } + Atomizer_isMeasuring = 0; } } void Atomizer_ReadInfo(Atomizer_Info_t *info) { + Thread_MutexLock(Atomizer_mutex); + if(Atomizer_curState == POWEROFF) { // Lock atomizer after short or if locked by error if(!Atomizer_isLocked && Atomizer_error != SHORT && @@ -767,15 +963,20 @@ void Atomizer_ReadInfo(Atomizer_Info_t *info) { Atomizer_forceMeasure = 0; } - Atomizer_Sample(0, &info->voltage, &info->current, &info->resistance); + if(!Atomizer_Sample(0, &info->voltage, &info->current, &info->resistance)) { + // Either an atomizer error happened while sampling + // or an error raced our ON/OFF if. + info->voltage = info->current = info->resistance = 0; + } } info->baseResistance = Atomizer_baseRes; info->baseTemperature = Atomizer_baseTemp; + + Thread_MutexUnlock(Atomizer_mutex); } void Atomizer_SetBaseUpdateCallback(Atomizer_BaseUpdateCallback_t callbackPtr) { - // Atomic Atomizer_baseUpdateCallbackPtr = callbackPtr; } diff --git a/src/button/Button.c b/src/button/Button.c index cda6dce..ba56ee0 100644 --- a/src/button/Button.c +++ b/src/button/Button.c @@ -30,16 +30,17 @@ #include #include +#include /** * Button callback function pointers. */ -static Button_Callback_t Button_callbackPtr[3] = {NULL}; +static volatile Button_Callback_t Button_callbackPtr[3] = {NULL, NULL, NULL}; /** * Button callback masks. */ -static uint8_t Button_callbackMask[3]; +static volatile uint8_t Button_callbackMask[3]; /** * Global button state. @@ -53,7 +54,8 @@ extern void GPD7_IRQHandler(); /** * Updates the global button state for the specified - * buttons. + * buttons. Access to the global button state must be + * externally synchronized. * This is an internal function. * * @param mask Button mask specifing which buttons to update. @@ -77,6 +79,9 @@ static void Button_IRQHandler() { int i; uint8_t mask; + // All button ISRs have the same priority, so global + // state is already synchronized. + // Dirty hack: invoke soft PD.7 interrupt handler if(GPIO_GET_INT_FLAG(PD, BIT7)) { GPD7_IRQHandler(); @@ -128,27 +133,32 @@ void Button_Init() { } uint8_t Button_GetState() { + // Atomic return Button_state; } int8_t Button_CreateCallback(Button_Callback_t callback, uint8_t buttonMask) { int i; + uint32_t primask; if(callback == NULL) { return -1; } + primask = Thread_IrqDisable(); + // Find an unused callback for(i = 0; i < 3 && Button_callbackPtr[i] != NULL; i++); if(i == 3) { + Thread_IrqRestore(primask); return -1; } - // Button_callbackPtr[i] must be set as last - // to avoid race conditions. + // Setup callback Button_callbackMask[i] = buttonMask; Button_callbackPtr[i] = callback; + Thread_IrqRestore(primask); return i; } @@ -158,5 +168,6 @@ void Button_DeleteCallback(int8_t index) { return; } + // Atomic Button_callbackPtr[index] = NULL; } diff --git a/src/dataflash/Dataflash.c b/src/dataflash/Dataflash.c index 99b5eda..84b6766 100644 --- a/src/dataflash/Dataflash.c +++ b/src/dataflash/Dataflash.c @@ -20,6 +20,7 @@ #include #include #include +#include /* Number of pages in dataflash */ #define DATAFLASH_PAGE_COUNT DATAFLASH_STRUCT_MAX_COUNT @@ -92,6 +93,11 @@ static uint8_t Dataflash_mruPage; */ static uint8_t Dataflash_structSetSelected; +/** + * Dataflash mutex. + */ +static Thread_Mutex_t Dataflash_mutex; + /** * Decodes a unary-coded word. The decoded value is equal * to the number of trailing (on the right) zero bits + 1. @@ -394,6 +400,11 @@ void Dataflash_Init() { uint8_t i; uint32_t baseAddr, pageAddr, word; + if(Thread_MutexCreate(&Dataflash_mutex) != TD_SUCCESS) { + // No user code has run yet, the heap is messed up + asm volatile ("udf"); + } + Dataflash_mruPage = DATAFLASH_PAGE_COUNT - 1; Dataflash_structSetSelected = 0; @@ -441,6 +452,8 @@ uint8_t Dataflash_GetMagicList(uint32_t *magicList) { uint8_t i, j, magicCount, flag; uint32_t magic; + Thread_MutexLock(Dataflash_mutex); + magicCount = 0; for(i = 0; i < DATAFLASH_PAGE_COUNT; i++) { flag = Dataflash_pageInfo[i].flag; @@ -461,6 +474,8 @@ uint8_t Dataflash_GetMagicList(uint32_t *magicList) { } } + Thread_MutexUnlock(Dataflash_mutex); + return magicCount; } @@ -476,9 +491,12 @@ uint8_t Dataflash_ReadStruct(const Dataflash_StructInfo_t *structInfo, void *dst return 0; } + Thread_MutexLock(Dataflash_mutex); + // Find most recent page for struct pageIndex = Dataflash_GetStructPageIndex(structInfo, 0); if(pageIndex < 0) { + Thread_MutexUnlock(Dataflash_mutex); return 0; } pageInfo = &Dataflash_pageInfo[pageIndex]; @@ -497,6 +515,7 @@ uint8_t Dataflash_ReadStruct(const Dataflash_StructInfo_t *structInfo, void *dst Dataflash_Read(dst, addr, structInfo->size); DATAFLASH_FMC_CLOSE(); + Thread_MutexUnlock(Dataflash_mutex); return 1; } @@ -506,7 +525,7 @@ uint8_t Dataflash_SelectStructSet(Dataflash_StructInfo_t **structInfo, uint8_t c uint32_t magic; if(Dataflash_structSetSelected || count > DATAFLASH_STRUCT_MAX_COUNT) { - // Already executed or invalid count + // Already executed (1) or invalid count return 0; } @@ -519,6 +538,14 @@ uint8_t Dataflash_SelectStructSet(Dataflash_StructInfo_t **structInfo, uint8_t c return 0; } + Thread_MutexLock(Dataflash_mutex); + + if(Dataflash_structSetSelected) { + // Already executed (2) + Thread_MutexUnlock(Dataflash_mutex); + return 0; + } + DATAFLASH_FMC_OPEN(); for(i = 0; i < DATAFLASH_PAGE_COUNT; i++) { @@ -550,19 +577,23 @@ uint8_t Dataflash_SelectStructSet(Dataflash_StructInfo_t **structInfo, uint8_t c DATAFLASH_FMC_CLOSE(); Dataflash_structSetSelected = 1; + Thread_MutexUnlock(Dataflash_mutex); return 1; } uint8_t Dataflash_UpdateStruct(const Dataflash_StructInfo_t *structInfo, void *src) { int8_t oldPage, newPage; - uint8_t allocPage, owFlag; + uint8_t allocPage, owFlag, ret = 1; uint16_t structSizeAlign; uint32_t baseAddr, endAddr, dstAddr; Dataflash_BlockInfo_t *blockInfo; + Thread_MutexLock(Dataflash_mutex); + if(!Dataflash_structSetSelected) { // Dataflash_SelectStructSet() hasn't been called yet + Thread_MutexUnlock(Dataflash_mutex); return 0; } @@ -581,8 +612,7 @@ uint8_t Dataflash_UpdateStruct(const Dataflash_StructInfo_t *structInfo, void *s dstAddr = blockInfo->addr + DATAFLASH_MRUPD_OFFSET(blockInfo->count, structSizeAlign); if(Dataflash_Compare(dstAddr, src, structInfo->size, &owFlag)) { // Data is identical, no need to update - DATAFLASH_FMC_CLOSE(); - return 1; + goto success; } if(owFlag) { @@ -621,8 +651,7 @@ uint8_t Dataflash_UpdateStruct(const Dataflash_StructInfo_t *structInfo, void *s newPage = Dataflash_AllocatePage(structInfo, oldPage); if(newPage < 0) { // Allocation failed - DATAFLASH_FMC_CLOSE(); - return 0; + goto error; } dstAddr = Dataflash_pageInfo[newPage].blockInfo.addr + 4; } @@ -630,16 +659,19 @@ uint8_t Dataflash_UpdateStruct(const Dataflash_StructInfo_t *structInfo, void *s // Write update to flash Dataflash_Write(dstAddr, src, structInfo->size); +error: + ret = 0; +success: DATAFLASH_FMC_CLOSE(); - - return 1; + Thread_MutexUnlock(Dataflash_mutex); + return ret; } uint8_t Dataflash_InvalidateStruct(const Dataflash_StructInfo_t *structInfo) { - uint8_t ret, i; + uint8_t i, ret = 0; uint32_t baseAddr, magicWord, magicMask; - ret = 0; + Thread_MutexLock(Dataflash_mutex); DATAFLASH_FMC_OPEN(); baseAddr = DATAFLASH_READ_BASEADDR(); @@ -662,15 +694,16 @@ uint8_t Dataflash_InvalidateStruct(const Dataflash_StructInfo_t *structInfo) { } DATAFLASH_FMC_CLOSE(); + Thread_MutexUnlock(Dataflash_mutex); return ret; } uint8_t Dataflash_Erase() { - uint8_t ret, i; + uint8_t i, ret = 1; uint32_t baseAddr; - ret = 1; + Thread_MutexLock(Dataflash_mutex); DATAFLASH_FMC_OPEN(); baseAddr = DATAFLASH_READ_BASEADDR(); @@ -689,6 +722,7 @@ uint8_t Dataflash_Erase() { } DATAFLASH_FMC_CLOSE(); + Thread_MutexUnlock(Dataflash_mutex); return ret; } diff --git a/src/display/Display.c b/src/display/Display.c index 22d92cd..efe70be 100644 --- a/src/display/Display.c +++ b/src/display/Display.c @@ -39,6 +39,7 @@ #include #include #include +#include /** * Global framebuffer. @@ -50,6 +51,21 @@ static uint8_t Display_framebuf[DISPLAY_FRAMEBUFFER_SIZE]; */ static Display_Type_t Display_type; +/** + * Display/framebuffer mutex. + * TODO: refactor display locking into the lower + * layers, only keep framebuffer locking here. + */ +static Thread_Mutex_t Display_mutex; + +/** + * Clears the framebuffer. + * This is an internal function. + */ +static void Display_ClearUnlocked() { + memset(Display_framebuf, 0x00, DISPLAY_FRAMEBUFFER_SIZE); +} + void Display_SetupSPI() { // Setup output pins PA0 = 0; @@ -64,7 +80,7 @@ void Display_SetupSPI() { GPIO_SetMode(PE, BIT12, GPIO_MODE_OUTPUT); // Setup MFP - SYS->GPB_MFPL &= ~(SYS_GPE_MFPH_PE11MFP_Msk | SYS_GPE_MFPH_PE12MFP_Msk | SYS_GPE_MFPH_PE13MFP_Msk); + SYS->GPE_MFPL &= ~(SYS_GPE_MFPH_PE11MFP_Msk | SYS_GPE_MFPH_PE12MFP_Msk | SYS_GPE_MFPH_PE13MFP_Msk); SYS->GPE_MFPH |= SYS_GPE_MFPH_PE11MFP_SPI0_MOSI0 | SYS_GPE_MFPH_PE12MFP_SPI0_SS | SYS_GPE_MFPH_PE13MFP_SPI0_CLK; // SPI0 master, MSB first, 8bit transaction, SPI Mode-0 timing, 4MHz clock @@ -78,6 +94,11 @@ void Display_SetupSPI() { } void Display_Init() { + if(Thread_MutexCreate(&Display_mutex) != TD_SUCCESS) { + // No user code has run yet, the heap is messed up + asm volatile ("udf"); + } + switch(gSysInfo.hwVersion) { case 102: case 103: @@ -91,16 +112,21 @@ void Display_Init() { Display_type = DISPLAY_SSD1306; break; } - Display_Clear(); + + Display_ClearUnlocked(); Display_SSD_Init(); } void Display_SetOn(uint8_t isOn) { + Thread_MutexLock(Display_mutex); Display_SSD_SetOn(isOn); + Thread_MutexUnlock(Display_mutex); } void Display_SetPowerOn(uint8_t isPowerOn) { + Thread_MutexLock(Display_mutex); Display_SSD_SetPowerOn(isPowerOn); + Thread_MutexUnlock(Display_mutex); } Display_Type_t Display_GetType() { @@ -112,23 +138,35 @@ bool Display_IsFlipped() { } void Display_Flip() { + Thread_MutexLock(Display_mutex); gSysInfo.displayFlip ^= 1; Display_SSD_SetOn(0); Display_SSD_Flip(); Display_SSD_Update(Display_framebuf); Display_SSD_SetOn(1); + Thread_MutexUnlock(Display_mutex); } void Display_SetInverted(bool invert) { + Thread_MutexLock(Display_mutex); Display_SSD_SetInverted(invert); + Thread_MutexUnlock(Display_mutex); } void Display_Update() { + // TODO: using critical sections as a ugly + // hack to make the fault handler work + Thread_CriticalEnter(); Display_SSD_Update(Display_framebuf); + Thread_CriticalExit(); } void Display_Clear() { - memset(Display_framebuf, 0x00, DISPLAY_FRAMEBUFFER_SIZE); + // TODO: using critical sections as a ugly + // hack to make the fault handler work + Thread_CriticalEnter(); + Display_ClearUnlocked(); + Thread_CriticalExit(); } /** @@ -210,7 +248,17 @@ static void Display_BitCopy(uint8_t *dst, const uint8_t *src, uint32_t dstOffset dst[0] |= (src[0] & bitMask1) << dstOffset; } -void Display_PutPixels(int x, int y, const uint8_t *bitmap, int w, int h) { +/** + * Copies a bitmap into the framebuffer. + * This is an internal function. + * + * @param x X coordinate to place the bitmap at. + * @param y Y coordinate to place the bitmap at. + * @param bitmap Bitmap buffer. + * @param w Width of the bitmap. + * @param h Height of the bitmap. + */ +static void Display_PutPixelsUnlocked(int x, int y, const uint8_t *bitmap, int w, int h) { int colSize, startRow, curX; // Sanity check @@ -233,20 +281,35 @@ void Display_PutPixels(int x, int y, const uint8_t *bitmap, int w, int h) { } } -void Display_PutLine(int x0, int y0, int x1, int y1) { - int dx = abs(x1-x0), sx = x0dy ? dx : -dy)/2, e2; - - uint8_t buff[] = { 0xFF }; +void Display_PutPixels(int x, int y, const uint8_t *bitmap, int w, int h) { + Thread_MutexLock(Display_mutex); + Display_PutPixelsUnlocked(x, y, bitmap, w, h); + Thread_MutexUnlock(Display_mutex); +} - for(;;){ - Display_PutPixels(x0, y0, buff, 1, 1); - if (x0==x1 && y0==y1) break; +void Display_PutLine(int x0, int y0, int x1, int y1) { + const uint8_t black[] = { 0xFF }; + int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1; + int dy = abs(y1 - y0), sy = y0 < y1 ? 1 : -1; + int err = (dx > dy ? dx : -dy) / 2, e2; + + Thread_MutexLock(Display_mutex); + while(1) { + Display_PutPixelsUnlocked(x0, y0, black, 1, 1); + if(x0 == x1 && y0 == y1) { + break; + } e2 = err; - if (e2 >-dx) { err -= dy; x0 += sx; } - if (e2 < dy) { err += dx; y0 += sy; } + if(e2 > -dx) { + err -= dy; + x0 += sx; + } + if(e2 < dy) { + err += dx; + y0 += sy; + } } + Thread_MutexUnlock(Display_mutex); } void Display_PutText(int x, int y, const char *txt, const Font_Info_t *font) { @@ -255,6 +318,9 @@ void Display_PutText(int x, int y, const char *txt, const Font_Info_t *font) { curX = x; + // TODO: using critical sections as a ugly + // hack to make the fault handler work + Thread_CriticalEnter(); for(i = 0; i < strlen(txt); i++) { // Handle newlines if(txt[i] == '\n') { @@ -284,9 +350,10 @@ void Display_PutText(int x, int y, const char *txt, const Font_Info_t *font) { charPtr = font->data + font->charInfo[charIdx].offset; // Blit character - Display_PutPixels(curX, y, charPtr, font->charInfo[charIdx].width, font->height); + Display_PutPixelsUnlocked(curX, y, charPtr, font->charInfo[charIdx].width, font->height); curX += font->charInfo[charIdx].width; } + Thread_CriticalExit(); } uint8_t *Display_GetFramebuffer() { @@ -294,5 +361,7 @@ uint8_t *Display_GetFramebuffer() { } void Display_SetContrast(uint8_t contrast) { + Thread_MutexLock(Display_mutex); Display_SSD_SetContrast(contrast); + Thread_MutexUnlock(Display_mutex); } diff --git a/src/display/Display_SSD.c b/src/display/Display_SSD.c index 186b35a..5f199cb 100644 --- a/src/display/Display_SSD.c +++ b/src/display/Display_SSD.c @@ -97,7 +97,7 @@ void Display_SSD_Init() { Display_SSD_SetOn(1); // Delay 20ms - Timer_DelayUs(20000); + Timer_DelayMs(20); } void Display_SSD_SetOn(uint8_t isOn) { diff --git a/src/display/Display_SSD1306.c b/src/display/Display_SSD1306.c index caf6b1e..36c247f 100644 --- a/src/display/Display_SSD1306.c +++ b/src/display/Display_SSD1306.c @@ -46,21 +46,21 @@ static const uint8_t Display_SSD1306_initCmds[] = { void Display_SSD1306_PowerOn() { DISPLAY_SSD_VDD = 1; DISPLAY_SSD_VCC = 1; - Timer_DelayUs(1000); + Timer_DelayMs(1); DISPLAY_SSD_RESET = 0; - Timer_DelayUs(1000); + Timer_DelayMs(1); DISPLAY_SSD_RESET = 1; - Timer_DelayUs(1000); + Timer_DelayMs(1); } void Display_SSD1306_PowerOff() { Display_SSD_SendCommand(SSD_DISPLAY_OFF); DISPLAY_SSD_VCC = 0; - Timer_DelayUs(100000); + Timer_DelayMs(100); DISPLAY_SSD_VDD = 0; - Timer_DelayUs(100000); + Timer_DelayMs(100); DISPLAY_SSD_RESET = 0; - Timer_DelayUs(100000); + Timer_DelayMs(100); } void Display_SSD1306_SendInitCmds() { diff --git a/src/display/Display_SSD1327.c b/src/display/Display_SSD1327.c index df01d5c..0e8510a 100644 --- a/src/display/Display_SSD1327.c +++ b/src/display/Display_SSD1327.c @@ -53,23 +53,23 @@ static const uint8_t Display_SSD1327_initCmds[] = { void Display_SSD1327_PowerOn() { DISPLAY_SSD_VDD = 1; - Timer_DelayUs(1000); + Timer_DelayMs(1); DISPLAY_SSD_RESET = 0; - Timer_DelayUs(10000); + Timer_DelayMs(10); DISPLAY_SSD_RESET = 1; - Timer_DelayUs(1000); + Timer_DelayMs(1); DISPLAY_SSD_VCC = 1; - Timer_DelayUs(1000); + Timer_DelayMs(1); } void Display_SSD1327_PowerOff() { Display_SSD_SendCommand(SSD_DISPLAY_OFF); DISPLAY_SSD_VCC = 0; - Timer_DelayUs(100000); + Timer_DelayMs(100); DISPLAY_SSD_VDD = 0; - Timer_DelayUs(100000); + Timer_DelayMs(100); DISPLAY_SSD_RESET = 0; - Timer_DelayUs(100000); + Timer_DelayMs(100); } void Display_SSD1327_SendInitCmds() { diff --git a/src/rtc/RTCUtils.c b/src/rtc/RTCUtils.c index fa4b430..1424ed4 100755 --- a/src/rtc/RTCUtils.c +++ b/src/rtc/RTCUtils.c @@ -19,14 +19,16 @@ #include #include +#include /** * True if RTC_Open has already been called. */ -static uint8_t RTCUtils_isRTCOpen = 0; +static volatile uint8_t RTCUtils_isRTCOpen = 0; void RTCUtils_SetDateTime(const RTCUtils_DateTime_t *dateTime) { S_RTC_TIME_DATA_T rtcData; + uint32_t primask; // Populate RTC data rtcData.u32Year = 2000 + dateTime->year; @@ -38,6 +40,9 @@ void RTCUtils_SetDateTime(const RTCUtils_DateTime_t *dateTime) { rtcData.u32Second = dateTime->second; rtcData.u32TimeScale = RTC_CLOCK_24; + // Protect against concurrent SetDateTime and GetDateTime + primask = Thread_IrqDisable(); + if(!RTCUtils_isRTCOpen) { // Enable LXT clock SYS_UnlockReg(); @@ -48,23 +53,30 @@ void RTCUtils_SetDateTime(const RTCUtils_DateTime_t *dateTime) { // Initialize RTC with supplied data RTC_Open(&rtcData); + // Atomic, set as last to minimize CS in GetDateTime RTCUtils_isRTCOpen = 1; } else { // Update RTC data RTC_SetDateAndTime(&rtcData); } + + Thread_IrqRestore(primask); } void RTCUtils_GetDateTime(RTCUtils_DateTime_t *dateTime) { S_RTC_TIME_DATA_T rtcData; + uint32_t primask; + // Atomic (RTC can't be closed) if(!RTCUtils_isRTCOpen) { return; } - // Grab RTC data + // Grab RTC data (protect against concurrent SetDateTime) + primask = Thread_IrqDisable(); RTC_GetDateAndTime(&rtcData); + Thread_IrqRestore(primask); // Populate dateTime dateTime->year = rtcData.u32Year - 2000; diff --git a/src/startup/fault.c b/src/startup/fault.c new file mode 100644 index 0000000..b76b16e --- /dev/null +++ b/src/startup/fault.c @@ -0,0 +1,234 @@ +/* + * This file is part of eVic SDK. + * + * eVic SDK is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * eVic SDK is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with eVic SDK. If not, see . + * + * Copyright (C) 2016 ReservedField + */ + +#include +#include +#include +#include +#include + +/** + * Hard fault: + * HARD + * + * Usage fault: + * USAGE + * + * Bus fault: + * BUS + * + * Memory management fault: + * MEM + * + * For all faults, register dump: + * + * + * + * + * + * + * + * + * Pressing left/right, high register dump: + * + * + * + * + * + * + * + * + * Pressing fire resumes execution (only guaranteed + * for UDF instruction). + */ + +// MemManage fault status +#define FAULT_GET_MMFSR() (SCB->CFSR & 0xFF) +// Bus fault status +#define FAULT_GET_BFSR() ((SCB->CFSR >> 8) & 0xFF) +// Usage fault status +#define FAULT_GET_UFSR() ((SCB->CFSR >> 16) & 0xFFFF) +// Reset fault status +#define FAULT_RESET_CFSR() do { SCB->CFSR = 0; } while(0) + +/** + * Gets the button state, like Button_GetState(). + * This is needed because the normal Button library + * is interrupt-driven, and we're blocking inside + * a higher priority interrupt. + * This is an internal function. + * + * @return Button state. + */ +static uint8_t Fault_GetButtonState() { + uint8_t state; + + state = PE0 ? 0 : BUTTON_MASK_FIRE; + state |= PD2 ? 0 : BUTTON_MASK_RIGHT; + state |= PD3 ? 0 : BUTTON_MASK_LEFT; + + return state; +} + +/** + * Dumps registers to display. + * This is an internal function. + * + * @param y Y coordinate to start putting text at. + * @param stack Pointer to the stack where the registers + * have been pushed. + * @param isHigh True to print R4-R11, false to print the + * low registers. + */ +static void Fault_DumpRegisters(uint8_t y, uint32_t *stack, uint8_t isHigh) { + char buf[9]; + int8_t i; + + for(i = 0; i < 8; i++) { + // XXX I have no idea why I need to start from -1 for high regs + siprintf(buf, "%08lx", stack[isHigh ? i - 1 : 8 + i]); + Display_PutText(0, y, buf, FONT_DEJAVU_8PT); + y += FONT_DEJAVU_8PT->height; + } +} + +/** + * Dump fault info to display, along with the low registers. + * This is an internal function. + * + * @param stack Pointer to the stack where the registers + * have been pushed. + */ +static void Fault_DumpFaultLow(uint32_t *stack) { + char buf[16]; + uint8_t mmfsr, bfsr; + uint16_t ufsr; + + mmfsr = FAULT_GET_MMFSR(); + bfsr = FAULT_GET_BFSR(); + ufsr = FAULT_GET_UFSR(); + + if(!(SCB->HFSR & SCB_HFSR_FORCED_Msk)) { + siprintf(buf, "HARD\n%08lx", SCB->HFSR); + } + else if(bfsr != 0) { + siprintf(buf, "BUS %02x\n%08lx", bfsr, SCB->BFAR); + } + else if(mmfsr != 0) { + siprintf(buf, "MEM %02x\n%08lx", mmfsr, SCB->MMFAR); + } + else if(ufsr != 0) { + siprintf(buf, "USAGE\n%04x", ufsr); + } + else { + siprintf(buf, "???"); + } + + Display_PutText(0, 0, buf, FONT_DEJAVU_8PT); + Fault_DumpRegisters(FONT_DEJAVU_8PT->height * 2, stack, 0); +} + +/** + * Generates a pure busy CPU delay, + * without using SysTick or timers. + * + * @param delay Delay (in milliseconds). + */ +static void Fault_Delay(uint16_t delay) { + uint32_t counter; + + // 1 iteration = 3 clock cycles (@ 72MHz) + // The non-loop overhead is negligible + counter = delay * 24000; + asm volatile("Fault_Delay_loop%=:\n\t" + "SUBS %0, #1\n\t" + "BNE Fault_Delay_loop%=" + : "+r" (counter)); +} + +/** + * Handles a hard fault. + * This is an internal function. + * + * @param stack Pointer to the stack where the registers + * have been pushed. + */ + void Fault_HandleHardFault(uint32_t *stack) { + uint8_t isHigh, btnState, oldBtnState, pressRelease; + +#ifdef EVICSDK_FPU_SUPPORT + // Re-enable UsageFault for the thread library + // in case this is an escalated usage fault + SCB->SHCSR |= SCB_SHCSR_USGFAULTENA_Msk; +#endif + + Display_Clear(); + Fault_DumpFaultLow(stack); + Display_Update(); + + isHigh = 0; + oldBtnState = BUTTON_MASK_NONE; + while(1) { + btnState = Fault_GetButtonState(); + pressRelease = (btnState ^ oldBtnState) & oldBtnState; + + if(pressRelease & BUTTON_MASK_FIRE) { + // Advance PC to next instruction + // This only works for 16-bit Thumb, but it's ok since + // we only guarantee it to work for UDF + stack[14] += 2; + break; + } + if(pressRelease & BUTTON_MASK_RIGHT || pressRelease & BUTTON_MASK_LEFT) { + isHigh ^= 1; + Display_Clear(); + if(isHigh) { + Fault_DumpRegisters(0, stack, 1); + } + else { + Fault_DumpFaultLow(stack); + } + Display_Update(); + } + + if(btnState != oldBtnState) { + // Debounce + Fault_Delay(30); + oldBtnState = btnState; + } + } + + // Some more debouncing to avoid FIRE continuing + // the next fault, too + Fault_Delay(100); + + FAULT_RESET_CFSR(); +} + +__attribute__((naked)) void HardFault_Handler() { + asm volatile("@ HardFault_Handler\n\t" + "TST LR, #4\n\t" + "ITE EQ\n\t" + "MRSEQ R0, MSP\n\t" + "MRSNE R0, PSP\n\t" + "STMFD R0!, {R4-R11}\n\t" + "LDR R1, =Fault_HandleHardFault\n\t" + "BX R1" + ); +} diff --git a/src/startup/init.c b/src/startup/init.c index d160742..0b92610 100644 --- a/src/startup/init.c +++ b/src/startup/init.c @@ -25,6 +25,7 @@ #include #include #include +#include /** * PLL clock: 72MHz. @@ -36,31 +37,32 @@ * System control registers must be unlocked. */ void Sys_Init() { - // TODO: why is SYS_UnlockReg() needed? Should be already unlocked. - SYS_UnlockReg(); - // HIRC clock (internal RC 22.1184MHz) CLK_EnableXtalRC(CLK_PWRCTL_HIRCEN_Msk); CLK_WaitClockReady(CLK_STATUS_HIRCSTB_Msk); - + + // LIRC clock (internal RC 10kHz) + CLK_EnableXtalRC(CLK_PWRCTL_LIRCEN_Msk); + CLK_WaitClockReady(CLK_STATUS_LIRCSTB_Msk); + // HCLK clock source: HIRC, HCLK source divider: 1 CLK_SetHCLK(CLK_CLKSEL0_HCLKSEL_HIRC, CLK_CLKDIV0_HCLK(1)); - + // HXT clock (external XTAL 12MHz) CLK_EnableXtalRC(CLK_PWRCTL_HXTEN_Msk); CLK_WaitClockReady(CLK_STATUS_HXTSTB_Msk); - + // Enable 72MHz optimization FMC_EnableFreqOptimizeMode(FMC_FTCTL_OPTIMIZE_72MHZ); - - // Core clock: PLL + + // Core clock: PLL @ 144MHz, HCLK @ 72MHz CLK_SetCoreClock(PLL_CLOCK); CLK_WaitClockReady(CLK_STATUS_PLLSTB_Msk); - + // SPI0 clock: PCLK0 CLK_SetModuleClock(SPI0_MODULE, CLK_CLKSEL2_SPI0SEL_PCLK0, 0); CLK_EnableModuleClock(SPI0_MODULE); - + // TMR0-3 clock: HXT CLK_SetModuleClock(TMR0_MODULE, CLK_CLKSEL1_TMR0SEL_HXT, 0); CLK_SetModuleClock(TMR1_MODULE, CLK_CLKSEL1_TMR1SEL_HXT, 0); @@ -71,24 +73,24 @@ void Sys_Init() { CLK_EnableModuleClock(TMR2_MODULE); CLK_EnableModuleClock(TMR3_MODULE); - // PWM clock: PCLK0 - CLK_SetModuleClock(PWM0_MODULE, CLK_CLKSEL2_PWM0SEL_PCLK0, 0); + // PWM clock: PLL + CLK_SetModuleClock(PWM0_MODULE, CLK_CLKSEL2_PWM0SEL_PLL, 0); CLK_EnableModuleClock(PWM0_MODULE); // USBD clock CLK_SetModuleClock(USBD_MODULE, 0, CLK_CLKDIV0_USB(3)); CLK_EnableModuleClock(USBD_MODULE); - + // Enable USB 3.3V LDO SYS->USBPHY = SYS_USBPHY_LDO33EN_Msk; // EADC clock: 72Mhz / 8 CLK_SetModuleClock(EADC_MODULE, 0, CLK_CLKDIV0_EADC(8)); CLK_EnableModuleClock(EADC_MODULE); - + // Enable BOD (reset, 2.2V) SYS_EnableBOD(SYS_BODCTL_BOD_RST_EN, SYS_BODCTL_BODVL_2_2V); - + // Update system core clock SystemCoreClockUpdate(); @@ -97,13 +99,12 @@ void Sys_Init() { Dataflash_Init(); // Setup debounce, used both by buttons and battery presence pin. - // Buttons only need 100us, while at least 200us is needed by + // Good buttons only need 100us, while at least 200us is needed by // battery presence. The original firmware uses 100ms for battery and // timer-based debounce for buttons, but that's excessive. - // 200us will work fine for both. Button debounce isn't really noticeable - // up to 128 LIRC clock cycles, so this can be upped if battery presence - // is unstable. - GPIO_SET_DEBOUNCE_TIME(GPIO_DBCTL_DBCLKSRC_LIRC, GPIO_DBCTL_DBCLKSEL_2); + // Older buttons need more debouncing (a few ms). Button debounce + // isn't really noticeable up to 128 LIRC clock cycles. + GPIO_SET_DEBOUNCE_TIME(GPIO_DBCTL_DBCLKSRC_LIRC, GPIO_DBCTL_DBCLKSEL_32); // Initialize I/O Display_SetupSPI(); @@ -114,4 +115,7 @@ void Sys_Init() { // Initialize display Display_Init(); -} \ No newline at end of file + + // Initialize thread manager + Thread_Init(); +} diff --git a/src/startup/mainthread.c b/src/startup/mainthread.c new file mode 100644 index 0000000..f1aacc3 --- /dev/null +++ b/src/startup/mainthread.c @@ -0,0 +1,67 @@ +/* + * This file is part of eVic SDK. + * + * eVic SDK is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * eVic SDK is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with eVic SDK. If not, see . + * + * Copyright (C) 2016 ReservedField + */ + +#include +#include +#include + +extern void __libc_init_array(); +extern void __libc_fini_array(); +extern int main(); + +// Weak: address will be NULL if not defined by application code +extern uint16_t Startup_mainThreadStackSize __attribute__((weak)); + +/** + * Main thread. + * + * @param args Unused. + * + * @return NULL. + */ +void *Startup_MainThread(void *args) { + // Invoke init functions (e.g. static C++ constructors) + __libc_init_array(); + + // Run user main + main(); + + // Invoke fini functions (e.g. static C++ destructors) + __libc_fini_array(); + + return NULL; +} + +/** + * Creates the main thread. + * The caller must follow up with an infinite loop while + * waiting for the scheduler to start up. + * Once the scheduler executes, the caller won't run anymore. + */ +void Startup_CreateMainThread() { + Thread_t mainThread; + uint16_t stackSize; + + stackSize = THREAD_DEFAULT_STACKSIZE; + if(&Startup_mainThreadStackSize != NULL) { + stackSize = Startup_mainThreadStackSize; + } + + Thread_Create(&mainThread, Startup_MainThread, NULL, stackSize); +} diff --git a/src/startup/sdktag.s b/src/startup/sdktag.s new file mode 100644 index 0000000..f409ec1 --- /dev/null +++ b/src/startup/sdktag.s @@ -0,0 +1,19 @@ +@ This file is part of eVic SDK. +@ +@ eVic SDK is free software: you can redistribute it and/or modify +@ it under the terms of the GNU General Public License as published by +@ the Free Software Foundation, either version 3 of the License, or +@ (at your option) any later version. +@ +@ eVic SDK is distributed in the hope that it will be useful, +@ but WITHOUT ANY WARRANTY; without even the implied warranty of +@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +@ GNU General Public License for more details. +@ +@ You should have received a copy of the GNU General Public License +@ along with eVic SDK. If not, see . +@ +@ Copyright (C) 2016 ReservedField + +.section .evicsdk_tag +.asciz EVICSDK_SDKTAG diff --git a/src/startup/startup.s b/src/startup/startup.s index 7afdcb4..8a07d0a 100755 --- a/src/startup/startup.s +++ b/src/startup/startup.s @@ -1,3 +1,20 @@ +@ This file is part of eVic SDK. +@ +@ eVic SDK is free software: you can redistribute it and/or modify +@ it under the terms of the GNU General Public License as published by +@ the Free Software Foundation, either version 3 of the License, or +@ (at your option) any later version. +@ +@ eVic SDK is distributed in the hope that it will be useful, +@ but WITHOUT ANY WARRANTY; without even the implied warranty of +@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +@ GNU General Public License for more details. +@ +@ You should have received a copy of the GNU General Public License +@ along with eVic SDK. If not, see . +@ +@ Copyright (C) 2015-2016 ReservedField + .syntax unified @ Verification strings @@ -121,13 +138,13 @@ ISR_Vector_Base: .text .align 2 .thumb + .global Startup_FpSetup .global Reset_Handler .weak Reset_Handler -Reset_Handler: .thumb_func - +Reset_Handler: + @ Unlock write-protected registers LDR R0, =0x40000100 - @ Unlock registers LDR R1, =0x59 STR R1, [R0] LDR R1, =0x16 @@ -135,73 +152,84 @@ Reset_Handler: LDR R1, =0x88 STR R1, [R0] - @ Init POR - LDR R2, =0x40000024 + @ Disable POR (SYS_PORCTL = 0x55A5) + LDR R0, =0x40000024 LDR R1, =0x00005AA5 - STR R1, [R2] + STR R1, [R0] - @ Select INV type - LDR R2, =0x40000200 - LDR R1, [R2] - BIC R1, R1, #0x1000 - STR R1, [R2] + @ Select INV type for HXT (CLK_PWRCTL[12] = 0) + LDR R0, =0x40000200 + LDR R1, [R0] + BIC R1, #0x1000 + STR R1, [R0] + + LDR R0, =0xE000EF34 + LDR R1, [R0] +#ifdef EVICSDK_FPU_SUPPORT + @ Enable lazy stacking + @ (ASPEN = 1, LSPEN = 1, i.e. FPCCR[31:30] = 11) + ORR R1, #(0x3 << 30) +#else + @ Disable lazy stacking, disable FPU state saving + @ (ASPEN = 0, LSPEN = 0, i.e. FPCCR[31:30] = 00) + BIC R1, #(0x3 << 30) +#endif + STR R1, [R0] + +#ifdef EVICSDK_FPU_SUPPORT + @ Enable FPU (enable CP10/CP11, i.e. CPACR[23:20] = 1111) + LDR R0, =0xE000ED88 + LDR R1, [R0] + ORR R1, #(0xF << 20) + STR R1, [R0] + + @ FPU enabled: sync barrier, flush pipeline + DSB + ISB +#endif @ Copy .data to RAM. Symbols defined by linker script: - @ Data_Start_ROM: start of .data section in ROM - @ Data_Start_RAM: start of .data section in RAM - @ Data_Size : size of .data section - LDR R0, =Data_Start_ROM - LDR R1, =Data_Start_RAM - LDR R2, =Data_Size - B Data_Copy_Check + @ Data_Size : size of .data section + @ Data_Start_ROM : start of .data section in ROM + @ Data_Start_RAM : start of .data section in RAM + LDR R0, =Data_Size + CBZ R0, Data_Copy_End + LDR R1, =Data_Start_ROM + LDR R2, =Data_Start_RAM Data_Copy_Loop: - LDMIA R0!, {R3} - STMIA R1!, {R3} - SUBS R2, R2, #4 -Data_Copy_Check: - CMP R2, #0 + LDR R3, [R1], #4 + STR R3, [R2], #4 + SUBS R0, #4 BNE Data_Copy_Loop +Data_Copy_End: @ Zero out BSS. Symbols defined by linker script: - @ BSS_Start: start of .bss section in RAM - @ BSS_Size : size of .bss section - LDR R0, =BSS_Start + @ BSS_Size : size of .bss section + @ BSS_Start : start of .bss section in RAM + @ R0 is already zeroed from the .data copy. LDR R1, =BSS_Size - MOVS R2, #0 - B BSS_Zero_Check + CBZ R1, BSS_Zero_End + LDR R2, =BSS_Start BSS_Zero_Loop: - STMIA R0!, {R2} - SUBS R1, R1, #4 -BSS_Zero_Check: - CMP R1, #0 + STR R0, [R2], #4 + SUBS R1, #4 BNE BSS_Zero_Loop - - @ Call SystemInit - LDR R0, =SystemInit - BLX R0 +BSS_Zero_End: @ Call Sys_Init LDR R0, =Sys_Init BLX R0 - @ Lock registers + @ Lock write-protected registers LDR R0, =0x40000100 MOVS R1, #0 STR R1, [R0] - @ Call __libc_init_array - LDR R0, =__libc_init_array - BLX R0 - - @ Call main - LDR R0, =main - BLX R0 - - @ Call __libc_fini_array - LDR R0, =__libc_fini_array + @ Create main thread + LDR R0, =Startup_CreateMainThread BLX R0 - @ Trap the CPU in a infinite loop + @ Wait for scheduler B . .pool diff --git a/src/thread/ContextSwitch.s b/src/thread/ContextSwitch.s new file mode 100644 index 0000000..7c28902 --- /dev/null +++ b/src/thread/ContextSwitch.s @@ -0,0 +1,60 @@ +@ This file is part of eVic SDK. +@ +@ eVic SDK is free software: you can redistribute it and/or modify +@ it under the terms of the GNU General Public License as published by +@ the Free Software Foundation, either version 3 of the License, or +@ (at your option) any later version. +@ +@ eVic SDK is distributed in the hope that it will be useful, +@ but WITHOUT ANY WARRANTY; without even the implied warranty of +@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +@ GNU General Public License for more details. +@ +@ You should have received a copy of the GNU General Public License +@ along with eVic SDK. If not, see . +@ +@ Copyright (C) 2016 ReservedField + +.syntax unified + +.global PendSV_Handler +PendSV_Handler: + @ This has the lowest possible priority, so it will not + @ preempt other interrupt handlers. This means it will only + @ preempt thread code, or will tailchain to an interrupt + @ that preempted thread code. In other words, when we get here + @ we can easily context switch between threads. Since we never + @ preempt ISRs, we always work with the PSP. + .thumb_func + + PUSH {LR} + + @ Call Thread_Schedule(EXC_RETURN) + @ We can delay the context push because the ABI enforces + @ routines to save and restore R4-R11 and S16-S31. + @ EXC_RETURN is ignored if FPU support is disabled, save + @ a cycle there for the no-FPU build. + @ Return: R0 = new ctx (or NULL), R1 = old ctx (or NULL) +#ifdef EVICSDK_FPU_SUPPORT + MOV R0, LR +#endif + LDR R1, =Thread_Schedule + BLX R1 + + POP {LR} + + @ If needed, save old thread context + TEQ R1, #0 + ITT NE + MRSNE R2, PSP + STMNE R1, {R2, R4-R11, LR} + + @ If needed, switch context + @ Also clear any exclusive lock held by the old thread + TEQ R0, #0 + ITTT NE + LDMNE R0, {R2, R4-R11, LR} + MSRNE PSP, R2 + CLREXNE + + BX LR diff --git a/src/thread/Queue.c b/src/thread/Queue.c new file mode 100644 index 0000000..b14e976 --- /dev/null +++ b/src/thread/Queue.c @@ -0,0 +1,92 @@ +/* + * This file is part of eVic SDK. + * + * eVic SDK is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * eVic SDK is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with eVic SDK. If not, see . + * + * Copyright (C) 2016 ReservedField + */ + +/* + * NOTE: this file is always compiled without hardware FPU support, to avoid + * issues with FPU context switching (Thread.c uses this queue implementation). + */ + +#include +#include + +#define QUEUE_HDR(queue) ((Queue_Header_t *) (queue)) + +void Queue_Init(Queue_t *queue) { + queue->head = NULL; + queue->tail = NULL; +} + +void Queue_PushFront(Queue_t *queue, void *item) { + if(queue->head == NULL) { + // Empty queue, set as tail + queue->tail = item; + } + + // Link to head and set as head + QUEUE_HDR(item)->next = queue->head; + queue->head = item; +} + +void Queue_PushBack(Queue_t *queue, void *item) { + if(queue->head == NULL) { + // Empty queue, set as head + queue->head = item; + } + else { + // Link to old tail + QUEUE_HDR(queue->tail)->next = item; + } + + // Make item terminal and set as tail + QUEUE_HDR(item)->next = NULL; + queue->tail = item; +} + +void *Queue_PopFront(Queue_t *queue) { + void *front; + + front = queue->head; + if(front == NULL) { + // Empty queue + return NULL; + } + + // Pop front off head + queue->head = QUEUE_HDR(front)->next; + + return front; +} + +void Queue_Remove(Queue_t *queue, void *prev, void *item) { + if(prev == NULL) { + // Remove from front, emptying list + // if this is the only item in queue + queue->head = QUEUE_HDR(item)->next; + } + else if(QUEUE_HDR(item)->next == NULL) { + // Remove from back (this isn't the + // only item in queue) + QUEUE_HDR(prev)->next = NULL; + queue->tail = prev; + } + else { + // Middle removal + QUEUE_HDR(prev)->next = QUEUE_HDR(item)->next; + } +} diff --git a/src/thread/Thread.c b/src/thread/Thread.c new file mode 100644 index 0000000..649b31e --- /dev/null +++ b/src/thread/Thread.c @@ -0,0 +1,1059 @@ +/* + * This file is part of eVic SDK. + * + * eVic SDK is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * eVic SDK is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with eVic SDK. If not, see . + * + * Copyright (C) 2016 ReservedField + */ + +/* + * NOTE: this file is always compiled without hardware FPU support, to avoid + * issues with FPU context switching. + */ + +#include +#include +#include +#include +#include +#include + +/* Load value for Systick. */ +#define THREAD_SYSTICK_LOAD (SystemCoreClock / 1000 / THREAD_SYSTICK_MS) + +/* Thread quantum, in system ticks. */ +#define THREAD_QUANTUM 20 + +/* Number of remaining ticks before preemption after a delay to justify busy looping it. */ +#define THREAD_DELAY_MARGIN (THREAD_QUANTUM / 5) + +/* Set when thread is ready for execution, not set when suspended. */ +#define THREAD_STATE_MSK_READY (1 << 0) + +/* Invalid magic, must be different from all magics. */ +#define THREAD_MAGIC_INVALID 0x00000000 +/* TCB magic: 'THRD'. */ +#define THREAD_MAGIC_TCB 0x44524854 +/* Semaphore magic: 'SEMA'. */ +#define THREAD_MAGIC_SEMA 0x414D4553 + +/* True if the size bytes that ptr points to fully reside in RAM. */ +#define THREAD_CHECK_RAM(ptr, size) (((uint32_t) (ptr)) >= 0x20000000 && \ + ((uint32_t) (ptr)) + (size) <= 0x20008000) +/* True if tcb points to a valid TCB. Needs critical section to protect from exit. */ +#define THREAD_CHECK_TCB(tcb) (THREAD_CHECK_RAM((tcb), sizeof(Thread_TCB_t)) && \ + ((Thread_TCB_t *) (tcb))->magic == THREAD_MAGIC_TCB) +/* True if sema points to a valid semaphore. Needs critical section to protect from destruction. */ +#define THREAD_CHECK_SEMA(sema) (THREAD_CHECK_RAM((sema), sizeof(Thread_SemaphoreInternal_t)) && \ + ((Thread_SemaphoreInternal_t *) (sema))->magic == THREAD_MAGIC_SEMA) + +/* Number of the current exception, or 0 in thread mode. */ +#define THREAD_GET_IRQN() (__get_IPSR() & 0xFF) + +/* Marks a thread ready and pushes it to back of ready queue, taking care of IRQ masking. */ +#define THREAD_READY(tcb) do { \ + tcb->state |= THREAD_STATE_MSK_READY; \ + uint32_t primask = Thread_IrqDisable(); \ + Queue_PushBack(&Thread_readyQueue, tcb); \ + Thread_IrqRestore(primask); } while(0) + +/* Thread_StackedContext_t size, aligned to 8-byte boundary. */ +#define THREAD_HWCTX_SIZE_ALIGN ((sizeof(Thread_StackedContext_t) + 7) & ~7) + +/* Default PSR for new threads, only thumb flag set. */ +#define THREAD_DEFAULT_PSR 0x01000000 +/* Default EXC_RETURN for new threads: thread mode, PSP, no FP state. */ +#define THREAD_DEFAULT_ER 0xFFFFFFFD + +#ifdef EVICSDK_FPU_SUPPORT +/* NOT set in EXC_RETURN if the thread used FPU at least once. */ +#define THREAD_ER_MSK_FPCTX (1 << 4) +#endif + +/* Creates the return value for Thread_Schedule(). */ +#define THREAD_MAKE_SCHEDRET(newCtx, oldCtx) \ + ((((uint64_t)(uint32_t) (oldCtx)) << 32) | ((uint32_t) (newCtx))) + +/* Marks the scheduler as pending by flagging PendSV. */ +#define THREAD_PEND_SCHED() do { SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk; } while(0) + +/* Busy waits for the current thread to become ready again. */ +#define THREAD_WAIT_READY() do {} while( \ + !(*((volatile uint8_t *) &Thread_curTcb->state) & THREAD_STATE_MSK_READY)) + +/* Base 2 exponent for stack guard size. Must be 5 <= EXP <= 32. */ +#define THREAD_STACKGUARD_SIZE_EXP 5 +/* Stack guard size, in bytes. Guaranteed to be 8-byte aligned. + * The stack guard address must be aligned to its size. */ +#define THREAD_STACKGUARD_SIZE (1 << THREAD_STACKGUARD_SIZE_EXP) +/* Stack guard region number for MPU. */ +#define THREAD_STACKGUARD_REGNUM 0 + +/** + * Hardware-pushed thread context. + */ +typedef struct { + /* Hardware-pushed registers: R0-R3, R12, LR, PC, PSR. */ + uint32_t r0; + uint32_t r1; + uint32_t r2; + uint32_t r3; + uint32_t r12; + uint32_t lr; + uint32_t pc; + uint32_t psr; +#ifdef EVICSDK_FPU_SUPPORT + /* Hardware-pushed FPU registers: S0-S15, FPSCR. */ + uint32_t s[16]; + uint32_t fpscr; +#endif +} Thread_StackedContext_t; + +/** + * Software-saved thread context. + * Keep in sync with context switcher. + */ +typedef struct { + /**< Saved stack pointer. */ + uint32_t sp; + /**< Software-saved registers: R4-R11. */ + uint32_t r[8]; + /**< EXC_RETURN to be used when resuming. */ + uint32_t er; +#ifdef EVICSDK_FPU_SUPPORT + /**< Software-saved FPU registers: S0-S31. */ + uint32_t s[32]; +#endif +} Thread_SoftwareContext_t; + +#ifdef EVICSDK_FPU_SUPPORT +/** + * FPU context state. + * Keep in sync with UsageFault handler. + */ +typedef struct { + /** + * Pointer to software-saved FPU state (Thread_SoftwareContex_t.s) + * of the thread that held FPU last. Will be saved when another + * thread uses the FPU. NULL if no thread holds the FPU state. + */ + uint32_t *holderCtx; + /** + * Pointer to software-saved FPU state (Thread_SoftwareContext_t.s) + * for the current thread. Will be restored when the current thread + * uses the FPU. NULL if the current thread has no FPU state. + * Shared with the UsageFault handler. + */ + uint32_t *curCtx; +} Thread_FpuState_t; +#endif + +/** + * Thread control block. + * Field order is arranged first to minimize + * memory usage, and then by logic grouping. + * Be mindful when changing it. + */ +typedef struct Thread_TCB { + /**< Next TCB in queue. */ + struct Thread_TCB *next; + /**< TCB magic. */ + uint32_t magic; + /**< Pointer to the allocated memory block. */ + void *blockPtr; + /**< Software-saved thread context. */ + Thread_SoftwareContext_t ctx; + /**< State-specific info field. */ + union { + /**< Executing: system time for preemption. */ + uint32_t preemptTime; + /**< In chrono list: system time for wakeup. */ + uint32_t chronoTime; + } info; + struct { + /**< TCB of thread that joined this one, or NULL. */ + struct Thread_TCB *tcb; + /**< Pointer to store return value (if tcb != NULL). */ + void **retPtr; + } join; + /**< Thread state. */ + uint8_t state; +} Thread_TCB_t; + +/** + * Semaphore. + * Synchronization: global critical section. + */ +typedef struct { + /**< Semaphore magic. */ + uint32_t magic; + /**< Counter. Negative values count waiting threads. */ + volatile int32_t count; + /**< Queue of threads waiting on an up. Synchronization: interrupt masking. */ + Queue_t waitQueue; +} Thread_SemaphoreInternal_t; + +/** + * Mutex. + */ +typedef struct { + /**< Binary semaphore for the mutex. */ + Thread_SemaphoreInternal_t sema; + /**< Thread that locked the mutex. NULL when unlocked. */ + Thread_TCB_t *owner; +} Thread_MutexInternal_t; + +/** + * Ready threads queue. + * Accessed by threads, scheduler and other ISRs. + * Synchronization: interrupt masking. + */ +static Queue_t Thread_readyQueue; + +/** + * Chronologically ordered queue of suspended threads + * waiting for a timed event. + * Accessed by threads and scheduler. + * Synchronization: global critical section. + */ +static Queue_t Thread_chronoQueue; + +/** + * Current thread TCB. + * This must be NULL even before Thread_Init(). + * Synchronization: global critical section. + */ +static Thread_TCB_t *Thread_curTcb = NULL; + +/** + * Critical section counter. Zero when not in a critical section. + */ +static volatile uint32_t Thread_criticalCount; + +/** + * System time. + * Wraps around at 49 days, 17:02:47.296. + */ +volatile uint32_t Thread_sysTick; + +#ifdef EVICSDK_FPU_SUPPORT +/** + * FPU context state. + * Shared with UsageFault handler. Non-atomic operations + * must be carried out with IRQs masked, to protect from + * faults generated by higher priority FPU-using ISRs. + */ +Thread_FpuState_t Thread_fpuState; +#endif + +/** + * Inserts a thread into a queue ordered by info.chronoTime. + * This is an internal function. + * + * @param queue Queue. + * @param tcb TCB to insert. + */ +static void Thread_ChronoQueueInsert(Queue_t *queue, Thread_TCB_t *tcb) { + Thread_TCB_t *prev, *next; + + // Look up insertion place + // If a TCB has the same chronoTime as this one we + // insert before it to save iterations, since TCBs + // with the same chronoTime are handled together. + prev = NULL; + next = queue->head; + while(next != NULL && next->info.chronoTime < tcb->info.chronoTime) { + prev = next; + next = next->next; + } + + if(prev == NULL) { + // Insert as first + Queue_PushFront(queue, tcb); + } + else if(next == NULL) { + // Insert as last + Queue_PushBack(queue, tcb); + } + else { + // Middle insertion + tcb->next = next; + prev->next = tcb; + } +} + +/** + * Updates the ready queue, moving suspended threads + * in the chronological list to it when they become ready. + * Assumes a critical section has been acquired. Disables + * and re-enables interrupts for ready queue push only when + * a new ready thread is found. + * This is an internal function. + * + * @return True if at least one thread was added to the ready + * queue, false otherwise. + */ +static uint8_t Thread_UpdateReadyQueueFromChrono() { + Thread_TCB_t *tcb; + uint8_t found = 0; + + while((tcb = Thread_chronoQueue.head) != NULL && tcb->info.chronoTime <= Thread_sysTick) { + // Wake up this thread (always removing from front) + Queue_Remove(&Thread_chronoQueue, NULL, tcb); + THREAD_READY(tcb); + found = 1; + } + + return found; +} + +/** + * Sets up memory protection for the stack guard. + * Must be called from ISR context with IRQs disabled. + * This is an internal function. + * + * @param guardPtr Pointer to beginning of stack guard. + * Must be aligned to stack guard size. + */ +static void Thread_SetupStackGuard(void *guardPtr) { + uint32_t attrAndSize; + + // No need for pre-update memory barrier because the + // stack guard is strongly-ordered. + + attrAndSize = + (1 << MPU_RASR_XN_Pos ) | // Instruction fetch disabled + (0 << MPU_RASR_AP_Pos ) | // No access permissions + (0 << MPU_RASR_TEX_Pos ) | // Non-cacheable + (0 << MPU_RASR_S_Pos ) | // Shareable (ignored) + (0 << MPU_RASR_C_Pos ) | // Non-normal + (0 << MPU_RASR_B_Pos ) | // Strongly-ordered + (0 << MPU_RASR_SRD_Pos ) | // No subregions + (1 << MPU_RASR_ENABLE_Pos) | // Enabled + ((THREAD_STACKGUARD_SIZE_EXP - 1) << MPU_RASR_SIZE_Pos); // Size = 2^(SIZE+1) + + // Since our attributes and size are constant, we don't + // need to disable the region before updating the base + // address: it'll be invalid only at first setup, and the + // region isn't yet enabled at that point. + + // Switch to stack guard region + MPU->RNR = THREAD_STACKGUARD_REGNUM << MPU_RNR_REGION_Pos; + // Configure base address + MPU->RBAR = (uint32_t) guardPtr; + // Configure attributes and size + MPU->RASR = attrAndSize; + + // No need for post-update memory barrier and pipeline flush + // because we're always returning from an exception when the + // thread gets control back (this must be called from ISR). +} + +#ifdef EVICSDK_FPU_SUPPORT +/** + * Enables/disables the FPU. + * + * @param enable True to enable, false to disable. + */ +static void Thread_FpuControl(uint8_t enable) { + if(enable) { + // Enable CP10/CP11, i.e. CPACR[23:20] = 1111 + SCB->CPACR |= (0xFUL << 20); + } + else { + // Disable CP10/CP11, i.e. CPACR[23:20] = 0000 + SCB->CPACR &= ~(0xFUL << 20); + } + + // Ensure write completed, flush pipeline + __DMB(); + __ISB(); +} +#endif + +/** + * Schedules the next thread. Called from PendSV. + * This is an internal function. + * + * @param er EXC_RETURN from the PendSV handler. + * Ignored if FPU support is disabled. + * + * @return The lower 32 bits are the address of the software-saved + * context for the new thread. If NULL, no context switch + * is performed. The higher 32 bits are the address of the + * software-saved context for the old thread. If NULL, the + * old context isn't saved. + */ +uint64_t Thread_Schedule(uint32_t er) { + Thread_TCB_t *nextTcb; + Thread_SoftwareContext_t *newCtx = NULL, *oldCtx = NULL; + uint8_t isCurReady; + uint32_t primask; + + if(Thread_criticalCount > 0) { + // Current thread is in a critical section, resume it + // Scheduler will be invoked again on section exit + return THREAD_MAKE_SCHEDRET(NULL, NULL); + } + + // Since no thread is in a critical section we have free + // access to all internal data structures that are + // synchronized by a global critical section. When working + // with the ready queue we need to disable interrupts. + + // Update ready queue before checking if there's something + // to do to amortize time complexity. + Thread_UpdateReadyQueueFromChrono(); + + if(Thread_curTcb != NULL && Thread_curTcb->info.preemptTime > Thread_sysTick) { + // Nothing to do + return THREAD_MAKE_SCHEDRET(NULL, NULL); + } + + primask = Thread_IrqDisable(); + isCurReady = (Thread_curTcb != NULL && Thread_curTcb->state & THREAD_STATE_MSK_READY); + // Short-circuit order is *very* important here + while((nextTcb = Queue_PopFront(&Thread_readyQueue)) == NULL && !isCurReady) { + // No ready threads to schedule. + // Instead of having an idle thread, we make use of + // the fact that we're running in a low priority + // PendSV interrupt, so we can busy wait for a thread + // to become ready. We're here because either: + // - all threads are delayed, or suspended threads are + // waiting on delayed threads or on semaphores that + // will be released by delayed threads: we keep + // updating the ready queue from the chrono list; + // - all threads are waiting on each other or on semas + // that will be released by other waiting threads, + // resulting in a deadlock: scheduler death; + // - all threads have terminated: scheduler death. + // If the scheduler dies, only ISRs will run from now on. + // To touch the ready queue we need to disable IRQs, + // but we can't keep them disabled, otherwise SysTick + // won't run. We re-enable interrupts and keep calling + // UpdateReadyQueueFromChrono() to find new ready threads + // from the chronological list. Since it disables IRQs + // only when it actually finds a new ready thread, we're + // good. We then disable IRQs for the Queue_PopFront() call. + Thread_IrqRestore(primask); + while(!Thread_UpdateReadyQueueFromChrono()); + primask = Thread_IrqDisable(); + } + Thread_IrqRestore(primask); + + // nextTcb can be NULL only if the current thread is ready and + // no other thread is. In that case, we'll just resume it. + if(nextTcb != NULL) { + // Thread_curTcb will be NULL only if this is the first + // run (from startup code) or if the current thread has + // been deleted. In both cases we don't want to save state. + if(Thread_curTcb != NULL) { + // Save old context + oldCtx = &Thread_curTcb->ctx; + + if(isCurReady) { + // Current thread hasn't been suspended + // Push it to back of ready queue (round-robin) + THREAD_READY(Thread_curTcb); + } + } + +#ifdef EVICSDK_FPU_SUPPORT + uint32_t *prevFpuCtx = (Thread_curTcb != NULL ? Thread_curTcb->ctx.s : NULL); +#endif + + // Switch to next thread + Thread_curTcb = nextTcb; + newCtx = &Thread_curTcb->ctx; + + primask = Thread_IrqDisable(); + +#ifdef EVICSDK_FPU_SUPPORT + if(Thread_fpuState.curCtx == NULL && !(er & THREAD_ER_MSK_FPCTX)) { + // The previous thread used FPU for the first time + // The previous holder already had its context saved + Thread_fpuState.holderCtx = prevFpuCtx; + } + // Switch current FPU context. If a thread has never used FPU + // NULL its context to avoid useless saves. If it ends up using + // it, the holder will be updated (see above). + Thread_fpuState.curCtx = (Thread_curTcb->ctx.er & THREAD_ER_MSK_FPCTX ? + NULL : Thread_curTcb->ctx.s); + // If we're resuming the holder thread, enable FPU since + // registers are good. Otherwise, disable FPU and let lazy + // stacking do its job. Also disable FPU when curCtx is NULL, + // since we don't have FP context for that thread yet. + Thread_FpuControl(Thread_fpuState.curCtx != NULL && + Thread_fpuState.curCtx == Thread_fpuState.holderCtx); +#endif + + // Configure stack guard: stack is at the beginning + // of the allocated block. + Thread_SetupStackGuard(Thread_curTcb->blockPtr); + + Thread_IrqRestore(primask); + } + + // Reset quantum + Thread_curTcb->info.preemptTime = Thread_sysTick + THREAD_QUANTUM; + + return THREAD_MAKE_SCHEDRET(newCtx, oldCtx); +} + +/** + * System tick handler. + * This is an internal function. + */ +void SysTick_Handler() { + Thread_sysTick++; + + // TODO: make this smarter + THREAD_PEND_SCHED(); +} + +/** + * Function to which a dying thread returns. + * This is an internal function. + * + * @param ret Return value of the main thread procedure. + * This is taken as an argument because, on ARM, + * the R0 register is used both for first argument + * and return value. + */ +static void Thread_ExitProc(void *ret) { + Thread_CriticalEnter(); + +#ifdef EVICSDK_FPU_SUPPORT + // We don't hold FPU anymore + uint32_t primask = Thread_IrqDisable(); + if(Thread_fpuState.holderCtx == Thread_fpuState.curCtx) { + Thread_fpuState.holderCtx = NULL; + } + Thread_fpuState.curCtx = NULL; + Thread_IrqRestore(primask); +#endif + + // If a thread has joined us, wake him up + if(Thread_curTcb->join.tcb != NULL) { + *Thread_curTcb->join.retPtr = ret; + THREAD_READY(Thread_curTcb->join.tcb); + } + + // Delete ourselves + // We won't be using stack or TCB after free + Thread_curTcb->magic = THREAD_MAGIC_INVALID; + free(Thread_curTcb->blockPtr); + Thread_curTcb = NULL; + + // Release all critical sections + Thread_criticalCount = 0; + + // Pend scheduler and wait for death. + // Since Thread_curTcb is NULL, the context + // switcher won't use the stack we just freed. + // If scheduler runs between critical release + // and the THREAD_PEND_SCHED() call we won't + // get back here. + THREAD_PEND_SCHED(); + while(1); +} + +void Thread_Init() { + Queue_Init(&Thread_readyQueue); + Queue_Init(&Thread_chronoQueue); + + // Configure MPU for stack guard + MPU->CTRL = + (1 << MPU_CTRL_PRIVDEFENA_Pos) | // Use default map as background + (0 << MPU_CTRL_HFNMIENA_Pos ) | // Disable MPU for fault handlers + (1 << MPU_CTRL_ENABLE_Pos ) ; // Enable MPU + + // Set lowest priority for PendSV + // While NVIC_SetPriority works with system interrupts, + // it considers priority to always be 4 bits like NVIC + // interrupts, while system interrupts only have 2 high bits, + // so we shift to make up for it. + NVIC_SetPriority(PendSV_IRQn, 3 << 2); + + // Configure systick (highest priority) but leave it disabled + SysTick->LOAD = THREAD_SYSTICK_LOAD - 1; + SysTick->VAL = 0; + SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk; + NVIC_SetPriority(SysTick_IRQn, 0 << 2); + +#ifdef EVICSDK_FPU_SUPPORT + // Enable UsageFault for lazy stacking + SCB->SHCSR |= SCB_SHCSR_USGFAULTENA_Msk; +#endif +} + +Thread_Error_t Thread_Create(Thread_t *thread, Thread_EntryPtr_t entry, void *args, uint16_t stackSize) { + uint8_t *block; + Thread_TCB_t *tcb; + Thread_StackedContext_t *ctx; + + // Align stack size to 8-byte boundary, reserving extra + // space for hardware-pushed context and stack guard + stackSize = (stackSize + 7) & ~7; + stackSize += THREAD_HWCTX_SIZE_ALIGN; + stackSize += THREAD_STACKGUARD_SIZE; + + // Allocate space for TCB and stack. TCB is below thread + // stack (i.e. at higher addresses). Stack must be 8-byte + // aligned and stack guard must be size-aligned, so align + // allocation to stack guard size (which is 8-byte aligned). + block = memalign(THREAD_STACKGUARD_SIZE, stackSize + sizeof(Thread_TCB_t)); + if(block == NULL) { + return TD_NO_MEMORY; + } + + // Setup TCB + tcb = (Thread_TCB_t *) (block + stackSize); + tcb->magic = THREAD_MAGIC_TCB; + tcb->blockPtr = block; + tcb->join.tcb = NULL; + tcb->state = 0; + *thread = (Thread_t) tcb; + + // Setup initial thread context and stack + ctx = (Thread_StackedContext_t *) (block + stackSize - THREAD_HWCTX_SIZE_ALIGN); + ctx->r0 = (uint32_t) args; + ctx->lr = (uint32_t) Thread_ExitProc; + ctx->pc = (uint32_t) entry; + ctx->psr = THREAD_DEFAULT_PSR; + tcb->ctx.er = THREAD_DEFAULT_ER; + tcb->ctx.sp = (uint32_t) ctx; + + // Push new thread to back of ready queue + THREAD_READY(tcb); + + if(!(SysTick->CTRL & SysTick_CTRL_ENABLE_Msk)) { + // SysTick is not enabled, i.e. this is the first + // run. Enable SysTick and pend scheduler. + // Once the scheduler runs we will never go back + // to the startup code. + SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; + THREAD_PEND_SCHED(); + } + + return TD_SUCCESS; +} + +void Thread_Yield() { + // Will waste 1 tick if sysTick == 0 when + // the scheduler runs, but that's *very* + // unlikely (and would only happen once). + Thread_curTcb->info.preemptTime = 0; + + // Schedule (even if we might have already + // been preempted) and wait for suspend/wakeup. + THREAD_PEND_SCHED(); + THREAD_WAIT_READY(); +} + +Thread_Error_t Thread_Join(Thread_t thread, void **ret) { + Thread_TCB_t *tcb; + + Thread_CriticalEnter(); + + if(!THREAD_CHECK_TCB(thread)) { + Thread_CriticalExit(); + return TD_INVALID_THREAD; + } + + tcb = (Thread_TCB_t *) thread; + if(tcb->join.tcb != NULL) { + return TD_ALREADY_JOINED; + } + + // Setup ourselves as the joined thread + tcb->join.tcb = Thread_curTcb; + tcb->join.retPtr = ret; + + // Mark current thread as suspended + Thread_curTcb->state &= ~THREAD_STATE_MSK_READY; + + Thread_CriticalExit(); + + // CriticalExit pended the scheduler, wait for suspend/wakeup + THREAD_WAIT_READY(); + + return TD_SUCCESS; +} + +void Thread_DelayMs(uint32_t delay) { + uint32_t delayEnd; + + Thread_CriticalEnter(); + + delayEnd = Thread_sysTick + delay * THREAD_SYSTICK_MS; + if(delayEnd < Thread_curTcb->info.preemptTime - THREAD_DELAY_MARGIN) { + // The delay will end before preemption: busy wait. + Thread_CriticalExit(); + while(Thread_sysTick < delayEnd); + } + else { + // Suspend thread to the chronological queue + Thread_curTcb->info.chronoTime = delayEnd; + Thread_curTcb->state &= ~THREAD_STATE_MSK_READY; + Thread_ChronoQueueInsert(&Thread_chronoQueue, Thread_curTcb); + Thread_CriticalExit(); + // CriticalExit pended the scheduler, wait for suspend/wakeup + THREAD_WAIT_READY(); + } +} + +void Thread_CriticalEnter() { + // No-op from ISRs or startup code + if(THREAD_GET_IRQN() != 0 || Thread_curTcb == NULL) { + return; + } + + // No atomic operations needed here. + // criticalCount = 0: even if preemption occurs + // during read-update-store, the atomic store to + // 1 will still be correct since criticalCount + // must be zero for this code to be resumed. + // criticalCount > 0: already in critical section, + // so read-update-store won't be disturbed. + Thread_criticalCount++; +} + +void Thread_CriticalExit() { + // No-op from ISRs or startup code + if(THREAD_GET_IRQN() != 0 || Thread_curTcb == NULL) { + return; + } + + // No atomic operations needed here. + // criticalCount = 0: even if preemption occurs + // during read-check, the atomic read of 0 will + // still be correct since criticalCount must be + // zero for this code to be resumed. + // criticalCount > 0: already in critical section, + // so read-update-store won't be disturbed. + if(Thread_criticalCount == 0) { + return; + } + Thread_criticalCount--; + + // This check is correct even in case of preemption + // for the same reasons shown above. + if(Thread_criticalCount == 0) { + // Just exited the last critical section, time + // to reschedule. Even if we've been preempted + // after exiting, an extra scheduler call won't + // cause issues. + THREAD_PEND_SCHED(); + } +} + +/** + * Initializes a semaphore. + * This is an internal function. + * + * @param sema Semaphore. + * @param count Initial semaphore count. + */ +static void Thread_SemaphoreInit(Thread_SemaphoreInternal_t *sema, int32_t count) { + sema->magic = THREAD_MAGIC_SEMA; + sema->count = count; + Queue_Init(&sema->waitQueue); +} + +/** + * Deletes and deallocates a semaphore. + * This is an internal function. + * + * @param sema Semaphore. + * @param doFree True to free the memory pointed by sema. + * + * @return TD_SUCCESS or TD_INVALID_SEMA. + */ +static Thread_Error_t Thread_SemaphoreDelete(Thread_SemaphoreInternal_t *sema, uint8_t doFree) { + Thread_CriticalEnter(); + + if(!THREAD_CHECK_SEMA(sema)) { + Thread_CriticalExit(); + return TD_INVALID_SEMA; + } + + // Delete semaphore + sema->magic = THREAD_MAGIC_INVALID; + if(doFree) { + free(sema); + } + + Thread_CriticalExit(); + + return TD_SUCCESS; +} + +Thread_Error_t Thread_SemaphoreCreate(Thread_Semaphore_t *sema, int32_t count) { + Thread_SemaphoreInternal_t *sm; + + if(count < 0) { + return TD_INVALID_VALUE; + } + + // Allocate semaphore + sm = malloc(sizeof(Thread_SemaphoreInternal_t)); + if(sm == NULL) { + return TD_NO_MEMORY; + } + + // Initialize semaphore + Thread_SemaphoreInit(sm, count); + *sema = (Thread_Semaphore_t) sm; + + return TD_SUCCESS; +} + +Thread_Error_t Thread_SemaphoreDestroy(Thread_Semaphore_t sema) { + return Thread_SemaphoreDelete((Thread_SemaphoreInternal_t *) sema, 1); +} + +Thread_Error_t Thread_SemaphoreDown(Thread_Semaphore_t sema) { + Thread_SemaphoreInternal_t *sm; + int32_t newSema; + uint32_t primask; + uint8_t waitWakeup = 0; + + Thread_CriticalEnter(); + + if(!THREAD_CHECK_SEMA(sema)) { + Thread_CriticalExit(); + return TD_INVALID_SEMA; + } + + // Decrement semaphore + sm = (Thread_SemaphoreInternal_t *) sema; + newSema = AtomicOps_Add((volatile uint32_t *) &sm->count, -1); + + if(newSema < 0) { + // This semaphore can't be downed without waiting. + // We might have been preempted after determining that + // newSema < 0, but before disabling interrupts, by a thread + // or ISR that upped the semaphore. + // If we don't re-check, we could wrongly suspend ourselves + // and we wouldn't be woken up until another up occurs. + // This double check avoids disabling interrupts when + // the semaphore can be simply downed without suspending and + // the race described above doesn't happen. + primask = Thread_IrqDisable(); + if(sm->count < 0) { + // Mark current thread as suspended + Thread_curTcb->state &= ~THREAD_STATE_MSK_READY; + // Push ourselves to back of wait queue + Queue_PushBack(&sm->waitQueue, Thread_curTcb); + waitWakeup = 1; + } + Thread_IrqRestore(primask); + } + + Thread_CriticalExit(); + + if(waitWakeup) { + // Thread_CriticalExit pended scheduler, wait for suspend/wakeup + THREAD_WAIT_READY(); + } + + return TD_SUCCESS; +} + +Thread_Error_t Thread_SemaphoreTryDown(Thread_Semaphore_t sema) { + Thread_SemaphoreInternal_t *sm; + int32_t oldVal; + + Thread_CriticalEnter(); + + if(!THREAD_CHECK_SEMA(sema)) { + Thread_CriticalExit(); + return TD_INVALID_SEMA; + } + + // Try to down by compare-and-swap + sm = (Thread_SemaphoreInternal_t *) sema; + do { + oldVal = sm->count; + if(oldVal <= 0) { + // Can't be downed without waiting + Thread_CriticalExit(); + return TD_TRY_FAIL; + } + } while(AtomicOps_CmpSwap((volatile uint32_t *) &sm->count, oldVal, oldVal - 1) != oldVal); + + Thread_CriticalExit(); + + return TD_SUCCESS; +} + +Thread_Error_t Thread_SemaphoreUp(Thread_Semaphore_t sema) { + Thread_SemaphoreInternal_t *sm; + Thread_TCB_t *tcb; + int32_t newSema; + uint32_t primask; + + Thread_CriticalEnter(); + + if(!THREAD_CHECK_SEMA(sema)) { + Thread_CriticalExit(); + return TD_INVALID_SEMA; + } + + // Increment semaphore + sm = (Thread_SemaphoreInternal_t *) sema; + newSema = AtomicOps_Add((volatile uint32_t *) &sm->count, 1); + if(newSema <= 0) { + // The old value was negative: there's at least a thread + // waiting on this sema. Each up handles a single wakeup + // only, so another thread or ISR (try)downing between + // the newSema check and disabling IRQs won't hurt us. + // Wake up the first thread in queue. + primask = Thread_IrqDisable(); + if((tcb = Queue_PopFront(&sm->waitQueue)) != NULL) { + tcb->state |= THREAD_STATE_MSK_READY; + Queue_PushBack(&Thread_readyQueue, tcb); + } + Thread_IrqRestore(primask); + } + + Thread_CriticalExit(); + + return TD_SUCCESS; +} + +Thread_Error_t Thread_SemaphoreGetCount(Thread_Semaphore_t sema, int32_t *count) { + Thread_SemaphoreInternal_t *sm; + int32_t val; + + Thread_CriticalEnter(); + + if(!THREAD_CHECK_SEMA(sema)) { + Thread_CriticalExit(); + return TD_INVALID_SEMA; + } + + // We internally use negative values to count + // waiting threads, mask them as zero + sm = (Thread_SemaphoreInternal_t *) sema; + val = sm->count; + *count = val < 0 ? 0 : val; + + Thread_CriticalExit(); + + return TD_SUCCESS; +} + +Thread_Error_t Thread_MutexCreate(Thread_Mutex_t *mutex) { + Thread_MutexInternal_t *mtx; + + // Allocate mutex and binary semaphore + mtx = malloc(sizeof(Thread_MutexInternal_t)); + if(mtx == NULL) { + return TD_NO_MEMORY; + } + + // Initialize mutex/semaphore to 1 (unlocked) + Thread_SemaphoreInit(&mtx->sema, 1); + mtx->owner = NULL; + *mutex = (Thread_Mutex_t) mtx; + + return TD_SUCCESS; +} + +Thread_Error_t Thread_MutexDestroy(Thread_Mutex_t mutex) { + Thread_MutexInternal_t *mtx; + + // Destroy semaphore, without freeing memory + mtx = (Thread_MutexInternal_t *) mutex; + if(Thread_SemaphoreDelete(&mtx->sema, 0) == TD_INVALID_SEMA) { + return TD_INVALID_MUTEX; + } + + // Destroy mutex + free(mtx); + + return TD_SUCCESS; +} + +Thread_Error_t Thread_MutexLock(Thread_Mutex_t mutex) { + Thread_MutexInternal_t *mtx; + + // Down semaphore (blocking) and take ownership + mtx = (Thread_MutexInternal_t *) mutex; + if(Thread_SemaphoreDown((Thread_Semaphore_t) &mtx->sema) == TD_INVALID_SEMA) { + return TD_INVALID_MUTEX; + } + mtx->owner = Thread_curTcb; + + return TD_SUCCESS; +} + +Thread_Error_t Thread_MutexTryLock(Thread_Mutex_t mutex) { + Thread_MutexInternal_t *mtx; + Thread_Error_t ret; + + // Try downing the semaphore + mtx = (Thread_MutexInternal_t *) mutex; + ret = Thread_SemaphoreTryDown((Thread_Semaphore_t) &mtx->sema); + switch(ret) { + case TD_SUCCESS: + // Lock succeeded, take ownership + mtx->owner = Thread_curTcb; + break; + case TD_INVALID_SEMA: + ret = TD_INVALID_MUTEX; + break; + default: + ret = TD_TRY_FAIL; + break; + } + + return ret; +} + +Thread_Error_t Thread_MutexUnlock(Thread_Mutex_t mutex) { + Thread_MutexInternal_t *mtx; + + mtx = (Thread_MutexInternal_t *) mutex; + + // Assumption: Thread_curTcb != NULL + if(mtx->owner != Thread_curTcb) { + // We didn't lock this mutex, it's + // already unlocked or we preempted + // a Thread_Mutex(Try)Lock, which can + // only happen if we don't own the mutex + return TD_MUTEX_BAD_UNLOCK; + } + + // Release ownership and up semaphore + mtx->owner = NULL; + Thread_SemaphoreUp((Thread_Semaphore_t) &mtx->sema); + + return TD_SUCCESS; +} + +Thread_Error_t Thread_MutexGetState(Thread_Mutex_t mutex, uint8_t *isLocked) { + Thread_MutexInternal_t *mtx; + int32_t count; + + mtx = (Thread_MutexInternal_t *) mutex; + if(Thread_SemaphoreGetCount((Thread_Semaphore_t) &mtx->sema, &count) == TD_INVALID_SEMA) { + return TD_INVALID_MUTEX; + } + *isLocked = (count == 0); + + return TD_SUCCESS; +} diff --git a/src/thread/UsageFault_fpu.s b/src/thread/UsageFault_fpu.s new file mode 100644 index 0000000..c76210f --- /dev/null +++ b/src/thread/UsageFault_fpu.s @@ -0,0 +1,111 @@ +@ This file is part of eVic SDK. +@ +@ eVic SDK is free software: you can redistribute it and/or modify +@ it under the terms of the GNU General Public License as published by +@ the Free Software Foundation, either version 3 of the License, or +@ (at your option) any later version. +@ +@ eVic SDK is distributed in the hope that it will be useful, +@ but WITHOUT ANY WARRANTY; without even the implied warranty of +@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +@ GNU General Public License for more details. +@ +@ You should have received a copy of the GNU General Public License +@ along with eVic SDK. If not, see . +@ +@ Copyright (C) 2016 ReservedField + +.syntax unified + +.global UsageFault_Handler +UsageFault_Handler: + .thumb_func + + @ Check if NOCP (CFSR[16+3]) is set + @ If not, this is not a CP denial + LDR R2, =0xE000ED28 + LDR R3, [R2] + TST R3, #(1 << 19) + BEQ UsageFault_Handler_escalate + + @ Check if FPU is enabled (i.e. CPACR[23:20] != 0000) + @ If it is, this is not an FPU CP denial + LDR R0, =0xE000ED88 + LDR R1, [R0] + TST R1, #(0xF << 20) + BNE UsageFault_Handler_escalate + + @ Clear NOCP flag + BIC R3, #(1 << 19) + STR R3, [R2] + + @ Enable FPU (enable CP10/CP11, i.e. CPACR[23:20] = 1111) + ORR R1, #(0xF << 20) + STR R1, [R0] + @ FPU enabled: sync barrier, flush pipeline + DSB + ISB + + @ If the current thread is the FPU holder, we are done + LDR R0, =Thread_fpuState + LDM R0, {R1-R2} + TEQ R1, R2 + IT EQ + BXEQ LR + + PUSH {R4, R5} + + @ Save FPCCR, clear LSPACT (FPCCR[0] = 0) + LDR R4, =0xE000EF34 + LDR R5, [R4] + BIC R12, R5, #(1 << 0) + STR R12, [R4] + + @ Note that we never check whether we are in thread mode + @ or in an interrupt. This means that an FPU-using ISR will + @ behave like the thread it preempted used FPU. + @ While this is unnecessary, it solves the race where an + @ ISR uses the FPU before the thread does in this quantum: + @ if we only enable FPU without switching context, there + @ would be no UsageFault when the thread uses it, resulting + @ in corrupted context. + @ This can be avoided by tailchaining PendSV and disabling + @ FPU again there, but the performance has to be evaluated. + + @ If needed, save FPU context for FPU holder + TEQ R1, #0 + IT NE + VSTMNE R1, {S0-S31} + + @ If needed, restore FPU context for current + @ thread and set it as FPU holder + TEQ R2, #0 + ITT NE + VLDMNE R2, {S0-S31} + STRNE R2, [R0] + + @ Restore FPCCR + STR R5, [R4] + + POP {R4, R5} + + @ If other fault flags were set, escalate + TEQ R3, #0 + IT EQ + BXEQ LR + +UsageFault_Handler_escalate: + @ If this is not an FPU CP denial, we want to escalate + @ it to a hard fault to hand it over to the SDK fault + @ handler (if enabled). We do this by disabling the + @ UsageFault exception (i.e. SHCSR[18] = 0) and returning. + @ The faulting instruction will be executed again, and + @ this time it will escalate to a hard fault because the + @ UsageFault handler is disabled. If the SDK fault handler + @ is enabled, it will re-enable the UsageFault handler for + @ us. If not, the system will halt. + LDR R0, =0xE000ED24 + LDR R1, [R0] + BIC R1, #(1 << 18) + STR R1, [R0] + BX LR diff --git a/src/timer/TimerUtils.c b/src/timer/TimerUtils.c index d7aab3f..5a0ac27 100644 --- a/src/timer/TimerUtils.c +++ b/src/timer/TimerUtils.c @@ -19,6 +19,7 @@ #include #include +#include /** * Structure for holding timeout status. @@ -100,25 +101,29 @@ TIMER_DEFINE_IRQ_HANDLER(2); TIMER_DEFINE_IRQ_HANDLER(3); /** - * Finds a slot for a new timer and sets it up. + * Assigns a slot for a new timer and sets the timer and callback info up. * The timer will not be started yet. * For best accuracy, the frequency should be a divisor of 12MHz. * This is an internal function. * * @param freq Timer frequency, in Hz. * @param isPeriodic True if the timer is periodic, false if one-shot. + * @param callback Timer callback function. + * @param callbackData Optional argument to pass to the callback function. * * @return A positive index for the newly created timer, or a negative * value if there are no timer slots available. */ -static int8_t Timer_AssignSlot(uint32_t freq, uint8_t isPeriodic) { +static int8_t Timer_AssignSlot(uint32_t freq, uint8_t isPeriodic, Timer_Callback_t callback, uint32_t callbackData) { uint8_t i; + uint32_t primask; + + primask = Thread_IrqDisable(); // Find an unused timer for(i = 0; i < 4 && Timer_callbackPtr[i] != NULL; i++); - if(i == 4) { - // All timers are in use + Thread_IrqRestore(primask); return -1; } @@ -127,6 +132,17 @@ static int8_t Timer_AssignSlot(uint32_t freq, uint8_t isPeriodic) { TIMER_EnableInt(Timer_TimerPtr[i]); NVIC_EnableIRQ(Timer_IrqNum[i]); + // The callback pointer is set here to avoid using a "poison" non-NULL + // pointer to synchronize concurrent AssignSlot calls (before the caller + // would have a chance to set it to a non-NULL value). + Timer_callbackPtr[i] = callback; + + Thread_IrqRestore(primask); + + // Callback data is lumped in here because it's common for both kinds of + // timers. There's no need to set it inside the critical section. + Timer_callbackData[i] = callbackData; + return i; } @@ -138,14 +154,12 @@ int8_t Timer_CreateTimer(uint32_t freq, uint8_t isPeriodic, Timer_Callback_t cal } // Get a timer slot - timerIndex = Timer_AssignSlot(freq, isPeriodic); + timerIndex = Timer_AssignSlot(freq, isPeriodic, callback, callbackData); if(timerIndex < 0) { return timerIndex; } - // Setup callback and info - Timer_callbackPtr[timerIndex] = callback; - Timer_callbackData[timerIndex] = callbackData; + // Mark info as 0 (not a timeout) Timer_info &= ~(1 << timerIndex); TIMER_Start(Timer_TimerPtr[timerIndex]); @@ -170,7 +184,7 @@ int8_t Timer_CreateTimeout(uint16_t timeout, uint8_t isPeriodic, Timer_Callback_ // We want to minimize the timer frequency. // Frequency must also evenly divide 12MHz for maximum precision. - // Since timeout is in ms, the minimum frequency is 1KHz. + // Since timeout is in ms, the maximum frequency is 1KHz. // If N divides both timeout and 1Khz, we can use 1KHz / N // as the frequency and timeout / N as the counter target. // If N divides 1KHz, it also divides 12MHz, so the precision @@ -190,14 +204,12 @@ int8_t Timer_CreateTimeout(uint16_t timeout, uint8_t isPeriodic, Timer_Callback_ } // Get a timer slot - timerIndex = Timer_AssignSlot(timerFreq, isPeriodic); + timerIndex = Timer_AssignSlot(timerFreq, isPeriodic, callback, callbackData); if(timerIndex < 0) { return timerIndex; } - // Setup callback, info and timeout status - Timer_callbackPtr[timerIndex] = callback; - Timer_callbackData[timerIndex] = callbackData; + // Mark info as 1 (timeout) and setup timeout status Timer_info |= 1 << timerIndex; Timer_timeoutData[timerIndex].tickCounter = 0; Timer_timeoutData[timerIndex].tickTarget = tickCount; @@ -216,24 +228,29 @@ void Timer_DeleteTimer(int8_t index) { TIMER_Close(Timer_TimerPtr[index]); NVIC_DisableIRQ(Timer_IrqNum[index]); TIMER_DisableInt(Timer_TimerPtr[index]); + // Atomic, must be last to avoid races with AssignSlot() Timer_callbackPtr[index] = NULL; } -void Timer_DelayUs(uint32_t delay) { - CLK_SysTickDelay(delay); -} - void Timer_DelayMs(uint32_t delay) { uint8_t delayRem; - // We do 233000us delays, which is nearly the maximum - // Maximizing single delay time increases precision - delayRem = delay % 233; - delay = delay / 233; + if(!(SysTick->CTRL & SysTick_CTRL_ENABLE_Msk)) { + // SysTick isn't enabled. This means the scheduler + // hasn't been started yet, so we use SysTick. + // We do 233000us delays, which is nearly the maximum. + // Maximizing single delay time increases precision. + delayRem = delay % 233; + delay = delay / 233; - while(delay--) { - CLK_SysTickDelay(233000); - } + while(delay--) { + CLK_SysTickDelay(233000); + } - CLK_SysTickDelay(delayRem * 1000); + CLK_SysTickDelay(delayRem * 1000); + } + else { + // Scheduler is working: hand delay to thread library + Thread_DelayMs(delay); + } } diff --git a/src/usb/USB_VirtualCOM.c b/src/usb/USB_VirtualCOM.c index da42718..50ecd51 100644 --- a/src/usb/USB_VirtualCOM.c +++ b/src/usb/USB_VirtualCOM.c @@ -27,6 +27,7 @@ #include #include #include +#include /* Endpoints */ #define USB_VCOM_CTRL_IN_EP EP0 @@ -350,6 +351,11 @@ static volatile USB_VirtualCOM_RxCallback_t USB_VirtualCOM_rxCallbackPtr; */ static uint8_t USB_VirtualCOM_isAsync; +/** + * Virtual COM mutex. + */ +static Thread_Mutex_t USB_VirtualCOM_mutex; + /** * Write data to the RX ring buffer. * If the data size exceeds current buffer capacity, excess @@ -679,15 +685,12 @@ static void USB_VirtualCOM_SendSync(const uint8_t *buf, uint32_t size) { */ static void USB_VirtualCOM_SendAsync(const uint8_t *buf, uint32_t size) { USB_VirtualCOM_TxTransfer_t *transfer; - uint32_t partialSize; + uint32_t primask, partialSize; if(size == 0) { return; } - // Enter critical section - __set_PRIMASK(1); - partialSize = 0; if(USB_VirtualCOM_bulkInWaiting) { // Transfer first packet right away @@ -700,7 +703,6 @@ static void USB_VirtualCOM_SendAsync(const uint8_t *buf, uint32_t size) { if(partialSize != 0 && partialSize < USB_VCOM_BULK_IN_MAX_PKT_SIZE) { // We already transferred the whole packet - __set_PRIMASK(0); return; } @@ -710,7 +712,6 @@ static void USB_VirtualCOM_SendAsync(const uint8_t *buf, uint32_t size) { // allocated for the bulk IN handler to send the zero packet. transfer = (USB_VirtualCOM_TxTransfer_t *) malloc(sizeof(USB_VirtualCOM_TxTransfer_t) + size); if(transfer == NULL) { - __set_PRIMASK(0); return; } @@ -723,6 +724,7 @@ static void USB_VirtualCOM_SendAsync(const uint8_t *buf, uint32_t size) { } // Append transfer to queue + primask = Thread_IrqDisable(); if(USB_VirtualCOM_txQueue.head == NULL) { USB_VirtualCOM_txQueue.head = USB_VirtualCOM_txQueue.tail = transfer; } @@ -730,20 +732,16 @@ static void USB_VirtualCOM_SendAsync(const uint8_t *buf, uint32_t size) { USB_VirtualCOM_txQueue.tail->next = transfer; USB_VirtualCOM_txQueue.tail = transfer; } - - // Exit critical section - __set_PRIMASK(0); + Thread_IrqRestore(primask); } void USB_VirtualCOM_Init() { - // Initialize state - USB_VirtualCOM_lineState = 0; - USB_VirtualCOM_txQueue.head = USB_VirtualCOM_txQueue.tail = NULL; USB_VirtualCOM_bulkInWaiting = 1; - USB_VirtualCOM_rxBuffer.readIndex = USB_VirtualCOM_rxBuffer.writeIndex = 0; - USB_VirtualCOM_rxBuffer.dataSize = 0; - USB_VirtualCOM_rxCallbackPtr = NULL; - USB_VirtualCOM_isAsync = 0; + + if(Thread_MutexCreate(&USB_VirtualCOM_mutex) != TD_SUCCESS) { + // No user code has run yet, the heap is messed up + asm volatile ("udf"); + } // Open USB USBD_Open(&USB_VirtualCOM_UsbdInfo, USB_VirtualCOM_HandleClassRequest, NULL); @@ -783,12 +781,14 @@ void USB_VirtualCOM_Send(const uint8_t *buf, uint32_t size) { return; } + Thread_MutexLock(USB_VirtualCOM_mutex); if(USB_VirtualCOM_isAsync) { USB_VirtualCOM_SendAsync(buf, size); } else { USB_VirtualCOM_SendSync(buf, size); } + Thread_MutexUnlock(USB_VirtualCOM_mutex); } void USB_VirtualCOM_SendString(const char *str) { @@ -801,17 +801,16 @@ uint16_t USB_VirtualCOM_GetAvailableSize() { uint16_t USB_VirtualCOM_Read(uint8_t *buf, uint16_t size) { uint16_t readSize; + uint32_t primask; - // Read in critical section - __set_PRIMASK(1); + primask = Thread_IrqDisable(); readSize = USB_VirtualCOM_RxBuffer_Read(buf, size); - __set_PRIMASK(0); + Thread_IrqRestore(primask); return readSize; } void USB_VirtualCOM_SetRxCallback(USB_VirtualCOM_RxCallback_t callbackPtr) { - // Atomic USB_VirtualCOM_rxCallbackPtr = callbackPtr; } @@ -839,4 +838,4 @@ void USBD_IRQHandler() { // This handler should be fast. // Be careful with display debugging in here, as it may throw off USB timing. USB_VirtualCOM_IRQHandler(); -} \ No newline at end of file +} diff --git a/tools/fault-decode b/tools/fault-decode new file mode 100755 index 0000000..942851c --- /dev/null +++ b/tools/fault-decode @@ -0,0 +1,153 @@ +#!/usr/bin/python + +# This file is part of eVic SDK. +# +# eVic SDK is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# eVic SDK is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with eVic SDK. If not, see . +# +# Copyright (C) 2016 ReservedField + +import sys + +ISR_NAMES = [ + 'thread mode', '', 'NMI', 'HardFault', 'MemManage', 'BusFault', 'UsageFault', + '', '', '', '', 'SVCall', 'reserved (debug)', '', 'PendSV', 'SysTick', 'BOD', + 'IRC', 'PWRWU', 'RAMPE', 'CLKFAIL', '', 'RTC', 'TAMPER', 'WDT', 'WWDT', 'EINT0', + 'EINT1', 'EINT2', 'EINT3', 'EINT4', 'EINT5', 'GPA', 'GPB', 'GPC', 'GPD', 'GPE', + 'GPF', 'SPI0', 'SPI1', 'BRAKE0', 'PWM0P0', 'PWM0P1', 'PWM0P2', 'BRAKE1', 'PWM1P0', + 'PWM1P1', 'PWM1P2', 'TMR0', 'TMR1', 'TMR2', 'TMR3', 'UART0', 'UART1', 'I2C0', + 'I2C1', 'PDMA', 'DAC', 'ADC00', 'ADC01', 'ACMP01', '', 'ADC02', 'ADC03', 'UART2', + 'UART3', '', 'SPI2', '', 'USBD', 'USBH', 'USBOTG', 'CAN0', '', 'SC0', '', '', '', + '', 'TK' +] + +def dump_hard(hfsr): + reason = '???' + if hfsr & (1 << 1): + reason = 'bus fault on vector table read' + + forced = hfsr & (1 << 30) + + print('Hard fault' + (' (forced)' if forced else '') + ': ' + reason) + +def dump_usage(ufsr): + # We assume the fault resets the system, i.e. no sticky bits + reason = '???' + if ufsr & (1 << 0): + reason = 'undefined instruction' + elif ufsr & (1 << 1): + reason = 'invalid state' + elif ufsr & (1 << 2): + reason = 'invalid PC load' + elif ufsr & (1 << 3): + reason = 'no coprocessor' + elif ufsr & (1 << 8): + reason = 'unaligned memory access' + elif ufsr & (1 << 9): + reason = 'division by zero' + + print('Usage fault: ' + reason) + +def dump_bus(bfsr, bfar): + reason = '???' + if bfsr & (1 << 0): + reason = 'instruction' + elif bfsr & (1 << 1): + reason = 'data (precise)' + elif bfsr & (1 << 2): + reason = 'data (imprecise)' + elif bfsr & (1 << 3): + reason = 'exception return unstacking' + elif bfsr & (1 << 4): + reason = 'exception entry stacking' + elif bfsr & (1 << 5): + reason = 'FP lazy state preservation' + + addr = '???' + if bfsr & (1 << 7): + addr = '0x{:08x}'.format(bfar) + + print('Bus fault @ ' + addr + ': ' + reason) + +def dump_mem(mmfsr, mmfar): + reason = '???' + if mmfsr & (1 << 0): + reason = 'instruction' + elif mmfsr & (1 << 1): + reason = 'data' + elif mmfsr & (1 << 3): + reason = 'exception return unstacking' + elif mmfsr & (1 << 4): + reason = 'exception entry stacking' + elif mmfsr & (1 << 5): + reason = 'FP lazy state preservation' + + addr = '???' + if mmfsr & (1 << 7): + addr = '0x{:08x}'.format(mmfar) + + print('Memory access violation @ ' + addr + ': ' + reason) + +def dump_regs(regs): + reg_names = ['R0', 'R1', 'R2', 'R3', 'R12', 'LR', 'PC', 'PSR'] + + for i in range(len(reg_names)): + print('{:3} = 0x{:08x}'.format(reg_names[i], regs[i])) + +def decode_psr(psr): + flags = [] + if psr & (1 << 31): + flags.append('N') + if psr & (1 << 30): + flags.append('Z') + if psr & (1 << 29): + flags.append('C') + if psr & (1 << 28): + flags.append('V') + if psr & (1 << 27): + flags.append('Q') + flags.append('GE={:04b}'.format((psr >> 16) & 0xF)) + + isrnum = psr & 0x1FF + isr = 'reserved (!)' + if isrnum < len(ISR_NAMES) and ISR_NAMES[isrnum] != '': + isr = ISR_NAMES[isrnum] + + iciit = ((psr >> 19) & 0xC0) | ((psr >> 10) & 0x3F) + iciit_label = 'ICI' if iciit & 0xC3 == 0 else 'IT' + + thumb = psr & (1 << 24) + + print('CPU flags : ' + ' '.join(flags)) + print('Current ISR : ' + isr) + print('{:3} state : {:08b}'.format(iciit_label, iciit)) + print('Thumb bit : ' + ('1' if thumb else '0 (!)')) + + +dump = [line.strip() for line in sys.stdin.readlines()] + +header = dump[0].split(' ') +if header[0].lower() == 'hard': + dump_hard(int(dump[1], 16)) +elif header[0].lower() == 'usage': + dump_usage(int(dump[1], 16)) +elif header[0].lower() == 'bus': + dump_bus(int(header[1], 16), int(dump[1], 16)) +elif header[0].lower() == 'mem': + dump_mem(int(header[1], 16), int(dump[1], 16)) +else: + print('???') + +regs = [int(reg, 16) for reg in dump[2:]] +dump_regs(regs) +decode_psr(regs[7])