From b40752eca707f9fd63f2be711f9f68e4aeb2e25d Mon Sep 17 00:00:00 2001 From: Manfred Steiner Date: Mon, 28 Oct 2024 14:27:35 +0100 Subject: [PATCH] test-software --- software/test-software/.gdb_history | 9 + software/test-software/.gdbinit | 2 + software/test-software/.gitignore | 4 + .../.vscode/c_cpp_properties.json | 18 + software/test-software/.vscode/launch.json | 37 + software/test-software/.vscode/settings.json | 29 + software/test-software/.vscode/tasks.json | 23 + software/test-software/LICENSE | 21 + software/test-software/Makefile | 22 + software/test-software/README.md | 22 + software/test-software/create-release | 86 ++ software/test-software/nano-1284/Makefile | 128 +++ .../test-software_nano-1284p_12mhz.elf | Bin 0 -> 75868 bytes software/test-software/nano-1284/src | 1 + software/test-software/nano-328/Makefile | 129 +++ .../test-software_nano-328p_16mhz.elf | Bin 0 -> 56356 bytes software/test-software/nano-328/src | 1 + software/test-software/nano-644/Makefile | 127 +++ .../test-software_nano-644_12mhz.elf | Bin 0 -> 74504 bytes software/test-software/nano-644/src | 1 + .../test_2024-07-23_nano-644.elf | Bin 0 -> 71992 bytes .../test-software/src/adafruit/bme280.cpp | 512 ++++++++++++ software/test-software/src/adafruit/bme280.h | 373 +++++++++ .../test-software/src/adafruit/ens160.cpp | 374 +++++++++ software/test-software/src/adafruit/ens160.h | 188 +++++ software/test-software/src/adafruit/sensor.h | 224 ++++++ software/test-software/src/i2cmaster.cpp | 155 ++++ software/test-software/src/i2cmaster.hpp | 30 + software/test-software/src/i2cslave.cpp | 92 +++ software/test-software/src/i2cslave.hpp | 32 + software/test-software/src/main.cpp | 412 ++++++++++ software/test-software/src/main.hpp | 38 + software/test-software/src/units/cc1101.cpp | 753 ++++++++++++++++++ software/test-software/src/units/cc1101.hpp | 281 +++++++ software/test-software/src/units/encoder.cpp | 138 ++++ software/test-software/src/units/encoder.hpp | 28 + software/test-software/src/units/i2c.cpp | 216 +++++ software/test-software/src/units/i2c.hpp | 43 + software/test-software/src/units/ieee485.cpp | 111 +++ software/test-software/src/units/ieee485.hpp | 22 + software/test-software/src/units/lcd.cpp | 443 +++++++++++ software/test-software/src/units/lcd.hpp | 48 ++ software/test-software/src/units/led.cpp | 139 ++++ software/test-software/src/units/led.hpp | 25 + software/test-software/src/units/modbus.cpp | 160 ++++ software/test-software/src/units/modbus.hpp | 24 + software/test-software/src/units/motor.cpp | 221 +++++ software/test-software/src/units/motor.hpp | 30 + software/test-software/src/units/portexp.cpp | 195 +++++ software/test-software/src/units/portexp.hpp | 24 + software/test-software/src/units/poti.cpp | 34 + software/test-software/src/units/poti.hpp | 17 + software/test-software/src/units/r2r.cpp | 48 ++ software/test-software/src/units/r2r.hpp | 17 + software/test-software/src/units/rgb.cpp | 152 ++++ software/test-software/src/units/rgb.hpp | 25 + software/test-software/src/units/rtc8563.cpp | 253 ++++++ software/test-software/src/units/rtc8563.hpp | 70 ++ software/test-software/src/units/seg7.cpp | 283 +++++++ software/test-software/src/units/seg7.hpp | 25 + software/test-software/src/units/switch.cpp | 100 +++ software/test-software/src/units/switch.hpp | 20 + software/test-software/src/units/uart1.cpp | 65 ++ software/test-software/src/units/uart1.hpp | 21 + 64 files changed, 7121 insertions(+) create mode 100644 software/test-software/.gdb_history create mode 100644 software/test-software/.gdbinit create mode 100644 software/test-software/.gitignore create mode 100644 software/test-software/.vscode/c_cpp_properties.json create mode 100644 software/test-software/.vscode/launch.json create mode 100644 software/test-software/.vscode/settings.json create mode 100644 software/test-software/.vscode/tasks.json create mode 100644 software/test-software/LICENSE create mode 100644 software/test-software/Makefile create mode 100644 software/test-software/README.md create mode 100755 software/test-software/create-release create mode 100644 software/test-software/nano-1284/Makefile create mode 100755 software/test-software/nano-1284/release/v2024-10-28_141638/test-software_nano-1284p_12mhz.elf create mode 120000 software/test-software/nano-1284/src create mode 100644 software/test-software/nano-328/Makefile create mode 100755 software/test-software/nano-328/release/v2024-10-28_141640/test-software_nano-328p_16mhz.elf create mode 120000 software/test-software/nano-328/src create mode 100644 software/test-software/nano-644/Makefile create mode 100755 software/test-software/nano-644/release/v2024-10-28_141636/test-software_nano-644_12mhz.elf create mode 120000 software/test-software/nano-644/src create mode 100755 software/test-software/release/v2024-08-18_1103/test_2024-07-23_nano-644.elf create mode 100644 software/test-software/src/adafruit/bme280.cpp create mode 100644 software/test-software/src/adafruit/bme280.h create mode 100644 software/test-software/src/adafruit/ens160.cpp create mode 100644 software/test-software/src/adafruit/ens160.h create mode 100644 software/test-software/src/adafruit/sensor.h create mode 100644 software/test-software/src/i2cmaster.cpp create mode 100644 software/test-software/src/i2cmaster.hpp create mode 100644 software/test-software/src/i2cslave.cpp create mode 100644 software/test-software/src/i2cslave.hpp create mode 100644 software/test-software/src/main.cpp create mode 100644 software/test-software/src/main.hpp create mode 100644 software/test-software/src/units/cc1101.cpp create mode 100644 software/test-software/src/units/cc1101.hpp create mode 100644 software/test-software/src/units/encoder.cpp create mode 100644 software/test-software/src/units/encoder.hpp create mode 100644 software/test-software/src/units/i2c.cpp create mode 100644 software/test-software/src/units/i2c.hpp create mode 100644 software/test-software/src/units/ieee485.cpp create mode 100644 software/test-software/src/units/ieee485.hpp create mode 100644 software/test-software/src/units/lcd.cpp create mode 100644 software/test-software/src/units/lcd.hpp create mode 100644 software/test-software/src/units/led.cpp create mode 100644 software/test-software/src/units/led.hpp create mode 100644 software/test-software/src/units/modbus.cpp create mode 100644 software/test-software/src/units/modbus.hpp create mode 100644 software/test-software/src/units/motor.cpp create mode 100644 software/test-software/src/units/motor.hpp create mode 100644 software/test-software/src/units/portexp.cpp create mode 100644 software/test-software/src/units/portexp.hpp create mode 100644 software/test-software/src/units/poti.cpp create mode 100644 software/test-software/src/units/poti.hpp create mode 100644 software/test-software/src/units/r2r.cpp create mode 100644 software/test-software/src/units/r2r.hpp create mode 100644 software/test-software/src/units/rgb.cpp create mode 100644 software/test-software/src/units/rgb.hpp create mode 100644 software/test-software/src/units/rtc8563.cpp create mode 100644 software/test-software/src/units/rtc8563.hpp create mode 100644 software/test-software/src/units/seg7.cpp create mode 100644 software/test-software/src/units/seg7.hpp create mode 100644 software/test-software/src/units/switch.cpp create mode 100644 software/test-software/src/units/switch.hpp create mode 100644 software/test-software/src/units/uart1.cpp create mode 100644 software/test-software/src/units/uart1.hpp diff --git a/software/test-software/.gdb_history b/software/test-software/.gdb_history new file mode 100644 index 0000000..3339046 --- /dev/null +++ b/software/test-software/.gdb_history @@ -0,0 +1,9 @@ +target remote :1234 +layout split +stepi +quit +target remote :1234 +layout split +stepi +b *main+9 +quit diff --git a/software/test-software/.gdbinit b/software/test-software/.gdbinit new file mode 100644 index 0000000..139597f --- /dev/null +++ b/software/test-software/.gdbinit @@ -0,0 +1,2 @@ + + diff --git a/software/test-software/.gitignore b/software/test-software/.gitignore new file mode 100644 index 0000000..a959910 --- /dev/null +++ b/software/test-software/.gitignore @@ -0,0 +1,4 @@ +.depend +**/build +**/dist +**/sim diff --git a/software/test-software/.vscode/c_cpp_properties.json b/software/test-software/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..3a57c79 --- /dev/null +++ b/software/test-software/.vscode/c_cpp_properties.json @@ -0,0 +1,18 @@ +{ + "configurations": [ + { + "name": "Linux AVR", + "includePath": [ + "/usr/lib/avr/include/**", + "/usr/lib/gcc/avr/**" + ], + "defines": [], + "compilerPath": "/usr/bin/avr-gcc", + "compilerArgs": [ "-mmcu=atmega644p", "-DF_CPU=12000000", "-Os" ], + "cStandard": "gnu11", + "cppStandard": "gnu++11", + "intelliSenseMode": "linux-gcc-x64" + } + ], + "version": 4 +} diff --git a/software/test-software/.vscode/launch.json b/software/test-software/.vscode/launch.json new file mode 100644 index 0000000..f29cf2e --- /dev/null +++ b/software/test-software/.vscode/launch.json @@ -0,0 +1,37 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Build", + // "request": "launch", + "type": "node-terminal", + "preLaunchTask": "build" + },{ + "name": "Flash", + // "request": "launch", + "type": "node-terminal", + "preLaunchTask": "flash" + },{ + "name": "Clean", + // "request": "launch", + "type": "node-terminal", + "preLaunchTask": "clean" + },{ + // es muss mit simuc --board arduino dist/programm.elf der Simulator + // gestartet werden. Dessen gdb-stub öffnet auf localhost:1234 einen Port + "name": "Debug (simuc)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/sim/atmega328p.elf", + "cwd": "${workspaceFolder}", + "externalConsole": false, + "MIMode": "gdb", + "miDebuggerPath": "/usr/bin/avr-gdb", + "miDebuggerServerAddress": ":1234", + "preLaunchTask": "build" + } + ] +} diff --git a/software/test-software/.vscode/settings.json b/software/test-software/.vscode/settings.json new file mode 100644 index 0000000..58539af --- /dev/null +++ b/software/test-software/.vscode/settings.json @@ -0,0 +1,29 @@ +{ + "[c]": { + "editor.insertSpaces": true, + "editor.tabSize": 3, + "editor.detectIndentation": false + }, + "[cpp]": { + "editor.insertSpaces": true, + "editor.tabSize": 3, + "editor.detectIndentation": false + }, + "[h]": { + "editor.insertSpaces": true, + "editor.tabSize": 3, + "editor.detectIndentation": false + }, + "[hpp]": { + "editor.insertSpaces": true, + "editor.tabSize": 3, + "editor.detectIndentation": false + }, + "cSpell.words": [], + "cSpell.ignorePaths": [ + "**/*.json", "**/*.c", "**/*.h", "**/*.cpp", "**/*.hpp", "**/Makefile" + ], + "java.project.sourcePaths": [ + "src/units" + ] +} diff --git a/software/test-software/.vscode/tasks.json b/software/test-software/.vscode/tasks.json new file mode 100644 index 0000000..74fb1c7 --- /dev/null +++ b/software/test-software/.vscode/tasks.json @@ -0,0 +1,23 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [{ + "label": "build", + "type": "shell", + "command": "make", + "problemMatcher":[ + "$gcc" + ] + },{ + "label": "clean", + "type": "shell", + "command": "make", + "args": [ "clean" ], + },{ + "label": "flash", + "type": "shell", + "command": "make", + "args": [ "flash" ], + }] +} \ No newline at end of file diff --git a/software/test-software/LICENSE b/software/test-software/LICENSE new file mode 100644 index 0000000..c9565b9 --- /dev/null +++ b/software/test-software/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Manfred Steiner (Manfred.Steiner@gmx.at) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/software/test-software/Makefile b/software/test-software/Makefile new file mode 100644 index 0000000..f5eaedd --- /dev/null +++ b/software/test-software/Makefile @@ -0,0 +1,22 @@ +.PHONY: all release clean + +SEP = -------------------------------------------------------- + +all: + @printf "\n\e[1;36m$(SEP)\nNano-644\n$(SEP)\e[m\n" + -@make -C nano-644 + @printf "\n\e[1;36m$(SEP)\n Nano-1284\n$(SEP)\e[m\n" + -@make -C nano-1284 + @printf "\n\e[1;36m$(SEP)\n Arduino Nano\n$(SEP)\e[m\n" + -@make -C nano-328 + +release: + -@make -C nano-644 release + -@make -C nano-1284 release + -@make -C nano-328 release + +clean: + @make -C nano-644 clean + @make -C nano-1284 clean + @make -C nano-328 clean + diff --git a/software/test-software/README.md b/software/test-software/README.md new file mode 100644 index 0000000..c8ef394 --- /dev/null +++ b/software/test-software/README.md @@ -0,0 +1,22 @@ +# Test-Software for Nano-X-Base + +This software supports: +* Arduino Nano (ATmega328P @16MHz) +* Nano-644 (ATmega644P @12MHz) +* Nano-1284 (ATmega1284P @12MHz) + +For the Nano-X-Base hardware version: +* V1a +* V2a + +## System requirements + +Makefiles and scripts are designed for running in a Linux system. +It is tested on a Debian system. + +## Build + +* To build all files use `make` +* To create release folder use `make release` +* To clean this folder use `make clean` + (release files are not removed) diff --git a/software/test-software/create-release b/software/test-software/create-release new file mode 100755 index 0000000..225277f --- /dev/null +++ b/software/test-software/create-release @@ -0,0 +1,86 @@ +#!/bin/sh +#set -x + +while [ ! -z "$1" ]; do + DIR="$1" + FILE="$2" + + #echo " creating release folder for file $FILE in directory $DIR ..." + + if [ ! -d "$DIR" ]; then + printf " [1;31mERROR: missing target directory $DIR\e[m\n" + exit 1 + fi + + if [ -z "$FILE" ] || [ ! -f "$FILE" ]; then + printf " [1;31mERROR: missing file $FILE\e[m\n" + exit 2 + fi + + shift + shift + + DATE_OBJDUMP=$(avr-objdump -tT "$FILE" 2>&1 | grep MAIN_CPP_DATE) + if [ -z "$DATE_OBJDUMP" ]; then + printf " [1;31mERROR ($FILE -> %DIR): symbol MAIN_CPP_DATE not found\e[m\n" + exit 3 + fi + + TIME_OBJDUMP=$(avr-objdump -tT "$FILE" 2>&1 | grep MAIN_CPP_TIME) + if [ -z "$DATE_OBJDUMP" ]; then + printf " [1;31mERROR ($FILE -> %DIR): symbol MAIN_CPP_TIME not found\e[m\n" + exit 4 + fi + + DATE_OFFSET_HEX="0x$(echo "$DATE_OBJDUMP" | cut -d " " -f 1)" + TIME_OFFSET_HEX="0x$(echo "$TIME_OBJDUMP" | cut -d " " -f 1)" + DATE_OFFSET=$(( $(printf "%d" $DATE_OFFSET_HEX) + 148 )) + TIME_OFFSET=$(( $(printf "%d" $TIME_OFFSET_HEX) + 148 )) + + DATE_STRINGS=$(strings -a -t d "$FILE" | grep "$(printf "%7s" "$DATE_OFFSET")") + TIME_STRINGS=$(strings -a -t d "$FILE" | grep "$(printf "%7s" "$TIME_OFFSET")") + + DATE_MONTH_STRING=$(echo $DATE_STRINGS | cut -d' ' -f2) + case "$DATE_MONTH_STRING" in + Jan) DATE_MONTH="01";; + Feb) DATE_MONTH="02";; + Mar) DATE_MONTH="03";; + Apr) DATE_MONTH="04";; + May) DATE_MONTH="05";; + Jun) DATE_MONTH="06";; + Jul) DATE_MONTH="07";; + Aug) DATE_MONTH="08";; + Sep) DATE_MONTH="09";; + Oct) DATE_MONTH="10";; + Nov) DATE_MONTH="11";; + Dec) DATE_MONTH="12";; + *) printf " [1;31mERROR ($FILE -> %DIR): invalidate date in file $FILE\e[m\n"; exit 5;; + esac + + DATE_DAY=$(echo $DATE_STRINGS | cut -d' ' -f3) + DATE_YEAR=$(echo $DATE_STRINGS | cut -d' ' -f4) + TIME_VALUE=$(echo $TIME_STRINGS | cut -d' ' -f2-) + TIME_HOUR=$(echo $TIME_VALUE | cut -d ':' -f1) + TIME_MINUTE=$(echo $TIME_VALUE | cut -d ':' -f2) + TIME_SECOND=$(echo $TIME_VALUE | cut -d ':' -f3) + + NAME=$(printf "v%4d-%02d-%02d_%02d%02d%02d" $DATE_YEAR $DATE_MONTH $DATE_DAY $TIME_HOUR $TIME_MINUTE $TIME_SECOND | egrep ^v[0-9]{4}-[0-9]{2}-[0-9]{1,2}_[0-9]{2}[0-9]{2}[0-9]{2}$) + if [ -z "$NAME" ]; then + printf " [1;31mERROR ($FILE -> %DIR): cannot create release name\e[m\n" + exit 6 + fi + FILENAME=$(echo "$FILE" | rev | cut -d"/" -f1 | rev) + if [ -d "$DIR/$NAME" ] && [ -f "$DIR/$NAME/$FILENAME" ]; then + echo " OK: release already done ($FILE -> $DIR/$NAME)" + else + test -d "$DIR/$NAME" || mkdir "$DIR/$NAME" + cp -a "$FILE" "$DIR/$NAME/" + if [ $? = 0 ]; then + echo " OK ($FILE -> $DIR/$NAME)" + else + printf " [1;31mERROR ($FILE -> %DIR)\e[m\n" + fi + fi + +done + diff --git a/software/test-software/nano-1284/Makefile b/software/test-software/nano-1284/Makefile new file mode 100644 index 0000000..6a29271 --- /dev/null +++ b/software/test-software/nano-1284/Makefile @@ -0,0 +1,128 @@ +.PHONY: all info flash flash0 flash1 flash2 picocom picocom0 picocom1 picocom2 release clean +$(shell mkdir -p dist >/dev/null) +$(shell mkdir -p build >/dev/null) +$(shell mkdir -p sim >/dev/null) +$(shell mkdir -p sim/build >/dev/null) +$(shell mkdir -p release/sim >/dev/null) + +NAME=test-software_nano-1284p_12mhz +SRC= $(wildcard src/*.c src/*.cpp src/*/*.c src/*/*.cpp) +HDR= $(wildcard src/*.h src/*.hpp src/*/*.h src/*/*.hpp) +OBJ_CPP = $(SRC:src/%.cpp=build/%.o) +OBJ = $(OBJ_CPP:src/%.c=build/%.o) +OBJ_SIM_CPP = $(SRC:src/%.cpp=sim/build/%.o) +OBJ_SIM = $(OBJ_SIM_CPP:src/%.c=sim/build/%.o) + +DEVICE=atmega1284p +AVRDUDE_DEVICE=m1284p + +CC= avr-g++ +CFLAGS= -Wall -mmcu=$(DEVICE) -Os -DF_CPU=12000000 -c +LFLAGS= -Wall -mmcu=$(DEVICE) -Os -DF_CPU=12000000 -Wl,-u,vfprintf -lprintf_flt -lm + +CFLAGS_SIM= -Wall -mmcu=$(DEVICE) -Og -DF_CPU=12000000 -g -c -c +LFLAGS_SIM= -Wall -mmcu=$(DEVICE) -Og -DF_CPU=12000000 -g -Wl,-u,vfprintf -lprintf_flt -lm + +# ------------------------------------------------ + +all: dist/$(NAME).elf dist/$(NAME).s dist/$(NAME).hex sim/$(NAME).elf sim/$(NAME).s info + +isp-fuse-$(AVRDUDE_DEVICE): + avrdude -c usbasp -p $(AVRDUDE_DEVICE) -U lfuse:w:0xFF:m -U hfuse:w:0xD9:m -U efuse:w:0xFF:m -U lock:w:0xFF:m + +info: + @avr-size --mcu=$(DEVICE) --format=avr dist/$(NAME).elf + +.depend: $(SRC) $(HDR) + $(CC) -mmcu=$(DEVICE) -MM $(SRC) | sed --regexp-extended 's/^(.*\.o)\: src\/(.*)(\.cpp|\.c) (.*)/build\/\2\.o\: src\/\2\3 \4/g' > .depend + +-include .depend + +dist/$(NAME).elf: .depend $(OBJ) + $(CC) $(LFLAGS) -o $@ $(OBJ) + +dist/%.s: dist/%.elf + avr-objdump -d $< > $@ + +dist/%.hex: dist/%.elf + avr-objcopy -O ihex $(HEX_FLASH_FLAGS) $< $@ + +sim/$(NAME).elf: .depend $(OBJ_SIM) + $(CC) $(LFLAGS_SIM) -o $@ $(OBJ_SIM) + +# ensure that __DATE__ and __TIME__ macros are up to date +build/main.o: src/main.cpp $(SRC) $(HDR) + @mkdir -p $(dir $@) + $(CC) $(CFLAGS) -o $@ $< + +build/%.o: src/%.c + @mkdir -p $(dir $@) + $(CC) $(CFLAGS) -o $@ $< + +build/%.o: src/%.cpp + @mkdir -p $(dir $@) + $(CC) $(CFLAGS) -o $@ $< + +sim/build/%.o: src/%.c + @mkdir -p $(dir $@) + $(CC) $(CFLAGS_SIM) -o $@ $< + +sim/build/%.o: src/%.cpp + @mkdir -p $(dir $@) + $(CC) $(CFLAGS_SIM) -o $@ $< + +sim/%.s: sim/%.elf + avr-objdump -d $< > $@ + +simuc: sim/$(NAME).elf + simuc --board nano-1284 $< + +gdb: sim/$(NAME).elf + avr-gdb $< + + +flash: dist/$(NAME).elf all + avrdude -c arduino -P /dev/ttyUSB0 -p $(AVRDUDE_DEVICE) -e -U flash:w:$< + +flash0: dist/$(NAME).elf all + avrdude -c arduino -P /dev/ttyUSB0 -p $(AVRDUDE_DEVICE) -e -U flash:w:$< + +flash1: dist/$(NAME).elf all + avrdude -c arduino -P /dev/ttyUSB1 -p $(AVRDUDE_DEVICE) -e -U flash:w:$< + +flash2: dist/$(NAME).elf all + avrdude -c arduino -P /dev/ttyUSB2 -p $(AVRDUDE_DEVICE) -e -U flash:w:$< + + +picocom: + # picocom sends CR for ENTER -> convert cr (\r) to lf (\n) + picocom -b 115200 --omap crlf --raise-dtr /dev/ttyUSB0 + +picocom0: + picocom -b 115200 --omap crlf --raise-dtr /dev/ttyUSB0 + +picocom1: + picocom -b 115200 --omap crlf --raise-dtr /dev/ttyUSB1 + +picocom2: + picocom -b 115200 --omap crlf --raise-dtr /dev/ttyUSB2 + + +isp-1284p: + avrdude -c usbasp -p $(AVRDUDE_DEVICE) + +isp-flash-1284p: dist/$(NAME).elf all + avrdude -c usbasp -p $(AVRDUDE_DEVICE) -e -U flash:w:$< + +flash-1284p: dist/$(NAME).elf all + avrdude -c arduino -p $(AVRDUDE_DEVICE) -P /dev/ttyUSB0 -b 115200 -e -U flash:w:$< + +release: dist/$(NAME).elf sim/$(NAME).elf + ../create-release release $(word 1, $^) release/sim $(word 2, $^) + +clean: + @rm -r dist + @rm -r build + @rm -r sim + @find . -type f -name ".depend" -exec rm {} \; + @echo "clean done" diff --git a/software/test-software/nano-1284/release/v2024-10-28_141638/test-software_nano-1284p_12mhz.elf b/software/test-software/nano-1284/release/v2024-10-28_141638/test-software_nano-1284p_12mhz.elf new file mode 100755 index 0000000000000000000000000000000000000000..03b31ccf9315d39ea4add6f4e90bb24db7778a73 GIT binary patch literal 75868 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[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/dev/null) +$(shell mkdir -p build >/dev/null) +$(shell mkdir -p sim >/dev/null) +$(shell mkdir -p sim/build >/dev/null) +$(shell mkdir -p release/sim >/dev/null) + +NAME=test-software_nano-328p_16mhz +SRC= $(wildcard src/*.c src/*.cpp src/*/*.c src/*/*.cpp) +HDR= $(wildcard src/*.h src/*.hpp src/*/*.h src/*/*.hpp) +OBJ_CPP = $(SRC:src/%.cpp=build/%.o) +OBJ = $(OBJ_CPP:src/%.c=build/%.o) +OBJ_SIM_CPP = $(SRC:src/%.cpp=sim/build/%.o) +OBJ_SIM = $(OBJ_SIM_CPP:src/%.c=sim/build/%.o) + +DEVICE=atmega328p +AVRDUDE_DEVICE=m328p + +CC= avr-g++ +CFLAGS= -Wall -mmcu=$(DEVICE) -Os -DF_CPU=16000000 -c +LFLAGS= -Wall -mmcu=$(DEVICE) -Os -DF_CPU=16000000 -Wl,-u,vfprintf -lprintf_flt -lm + +CFLAGS_SIM= -Wall -mmcu=$(DEVICE) -Og -DF_CPU=16000000 -g -c -c +LFLAGS_SIM= -Wall -mmcu=$(DEVICE) -Og -DF_CPU=16000000 -g -Wl,-u,vfprintf -lprintf_flt -lm + + +all: dist/$(NAME).elf dist/$(NAME).s dist/$(NAME).hex sim/$(NAME).elf sim/$(NAME).s info + +isp-fuse-$(AVRDUDE_DEVICE): + avrdude -c usbasp -p $(AVRDUDE_DEVICE) -U lfuse:w:0xFF:m -U hfuse:w:0xD9:m -U efuse:w:0xFF:m -U lock:w:0xFF:m + +# ------------------------------------------------ + +info: + @avr-size --mcu=$(DEVICE) --format=avr dist/$(NAME).elf + +.depend: $(SRC) $(HDR) + $(CC) -mmcu=$(DEVICE) -MM $(SRC) | sed --regexp-extended 's/^(.*\.o)\: src\/(.*)(\.cpp|\.c) (.*)/build\/\2\.o\: src\/\2\3 \4/g' > .depend + +-include .depend + +dist/$(NAME).elf: .depend $(OBJ) + $(CC) $(LFLAGS) -o $@ $(OBJ) + +dist/%.s: dist/%.elf + avr-objdump -d $< > $@ + +dist/%.hex: dist/%.elf + avr-objcopy -O ihex $(HEX_FLASH_FLAGS) $< $@ + +sim/$(NAME).elf: .depend $(OBJ_SIM) + $(CC) $(LFLAGS_SIM) -o $@ $(OBJ_SIM) + +# ensure that __DATE__ and __TIME__ macros are up to date +build/main.o: src/main.cpp $(SRC) $(HDR) + @mkdir -p $(dir $@) + $(CC) $(CFLAGS) -o $@ $< + +build/%.o: src/%.c + @mkdir -p $(dir $@) + $(CC) $(CFLAGS) -o $@ $< + +build/%.o: src/%.cpp + @mkdir -p $(dir $@) + $(CC) $(CFLAGS) -o $@ $< + +sim/build/%.o: src/%.c + @mkdir -p $(dir $@) + $(CC) $(CFLAGS_SIM) -o $@ $< + +sim/build/%.o: src/%.cpp + @mkdir -p $(dir $@) + $(CC) $(CFLAGS_SIM) -o $@ $< + +sim/%.s: sim/%.elf + avr-objdump -d $< > $@ + +simuc: sim/$(NAME).elf + simuc --board nano-1284 $< + +gdb: sim/$(NAME).elf + avr-gdb $< + + +flash: dist/$(NAME).elf all + avrdude -c arduino -P /dev/ttyUSB0 -p $(AVRDUDE_DEVICE) -e -U flash:w:$< + +flash0: dist/$(NAME).elf all + avrdude -c arduino -P /dev/ttyUSB0 -p $(AVRDUDE_DEVICE) -e -U flash:w:$< + +flash1: dist/$(NAME).elf all + avrdude -c arduino -P /dev/ttyUSB1 -p $(AVRDUDE_DEVICE) -e -U flash:w:$< + +flash2: dist/$(NAME).elf all + avrdude -c arduino -P /dev/ttyUSB2 -p $(AVRDUDE_DEVICE) -e -U flash:w:$< + + +picocom: + # picocom sends CR for ENTER -> convert cr (\r) to lf (\n) + picocom -b 115200 --omap crlf --raise-dtr /dev/ttyUSB0 + +picocom0: + picocom -b 115200 --omap crlf --raise-dtr /dev/ttyUSB0 + +picocom1: + picocom -b 115200 --omap crlf --raise-dtr /dev/ttyUSB1 + +picocom2: + picocom -b 115200 --omap crlf --raise-dtr /dev/ttyUSB2 + + +isp-1284p: + avrdude -c usbasp -p $(AVRDUDE_DEVICE) + +isp-flash-1284p: dist/$(NAME).elf all + avrdude -c usbasp -p $(AVRDUDE_DEVICE) -e -U flash:w:$< + +flash-1284p: dist/$(NAME).elf all + avrdude -c arduino -p $(AVRDUDE_DEVICE) -P /dev/ttyUSB0 -b 115200 -e -U flash:w:$< + +release: dist/$(NAME).elf sim/$(NAME).elf + ../create-release release $(word 1, $^) release/sim $(word 2, $^) + +clean: + @rm -r dist + @rm -r build + @rm -r sim + @find . -type f -name ".depend" -exec rm {} \; + @echo "clean done" diff --git a/software/test-software/nano-328/release/v2024-10-28_141640/test-software_nano-328p_16mhz.elf b/software/test-software/nano-328/release/v2024-10-28_141640/test-software_nano-328p_16mhz.elf new file mode 100755 index 0000000000000000000000000000000000000000..480cd668d429fc250e10f264c8ce9fda5530648e GIT binary patch literal 56356 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 C1!XS) literal 0 HcmV?d00001 diff --git a/software/test-software/nano-328/src b/software/test-software/nano-328/src new file mode 120000 index 0000000..5cd551c --- /dev/null +++ b/software/test-software/nano-328/src @@ -0,0 +1 @@ +../src \ No newline at end of file diff --git a/software/test-software/nano-644/Makefile b/software/test-software/nano-644/Makefile new file mode 100644 index 0000000..72f0f07 --- /dev/null +++ b/software/test-software/nano-644/Makefile @@ -0,0 +1,127 @@ +.PHONY: all info flash flash0 flash1 flash2 picocom picocom0 picocom1 picocom2 release clean +$(shell mkdir -p dist >/dev/null) +$(shell mkdir -p build >/dev/null) +$(shell mkdir -p sim >/dev/null) +$(shell mkdir -p sim/build >/dev/null) +$(shell mkdir -p release/sim >/dev/null) + +NAME=test-software_nano-644_12mhz +SRC= $(wildcard src/*.c src/*.cpp src/*/*.c src/*/*.cpp) +HDR= $(wildcard src/*.h src/*.hpp src/*/*.h src/*/*.hpp) +OBJ_CPP = $(SRC:src/%.cpp=build/%.o) +OBJ = $(OBJ_CPP:src/%.c=build/%.o) +OBJ_SIM_CPP = $(SRC:src/%.cpp=sim/build/%.o) +OBJ_SIM = $(OBJ_SIM_CPP:src/%.c=sim/build/%.o) + +DEVICE=atmega644p +AVRDUDE_DEVICE=m644p + +CC= avr-g++ +CFLAGS= -Wall -mmcu=$(DEVICE) -Os -DF_CPU=12000000 -c +LFLAGS= -Wall -mmcu=$(DEVICE) -Os -DF_CPU=12000000 -Wl,-u,vfprintf -lprintf_flt -lm + +CFLAGS_SIM= -Wall -mmcu=$(DEVICE) -Og -DF_CPU=12000000 -g -c -c +LFLAGS_SIM= -Wall -mmcu=$(DEVICE) -Og -DF_CPU=12000000 -g -Wl,-u,vfprintf -lprintf_flt -lm + +# ------------------------------------------------ + +all: dist/$(NAME).elf dist/$(NAME).s dist/$(NAME).hex sim/$(NAME).elf sim/$(NAME).s info + +isp-fuse-$(AVRDUDE_DEVICE): + avrdude -c usbasp -p $(AVRDUDE_DEVICE) -U lfuse:w:0xFF:m -U hfuse:w:0xD9:m -U efuse:w:0xFF:m -U lock:w:0xFF:m +info: + @avr-size --mcu=$(DEVICE) --format=avr dist/$(NAME).elf + +.depend: $(SRC) $(HDR) + $(CC) -mmcu=$(DEVICE) -MM $(SRC) | sed --regexp-extended 's/^(.*\.o)\: src\/(.*)(\.cpp|\.c) (.*)/build\/\2\.o\: src\/\2\3 \4/g' > .depend + +-include .depend + +dist/$(NAME).elf: .depend $(OBJ) + $(CC) $(LFLAGS) -o $@ $(OBJ) + +dist/%.s: dist/%.elf + avr-objdump -d $< > $@ + +dist/%.hex: dist/%.elf + avr-objcopy -O ihex $(HEX_FLASH_FLAGS) $< $@ + +sim/$(NAME).elf: .depend $(OBJ_SIM) + $(CC) $(LFLAGS_SIM) -o $@ $(OBJ_SIM) + +# ensure that __DATE__ and __TIME__ macros are up to date +build/main.o: src/main.cpp $(SRC) $(HDR) + @mkdir -p $(dir $@) + $(CC) $(CFLAGS) -o $@ $< + +build/%.o: src/%.c + @mkdir -p $(dir $@) + $(CC) $(CFLAGS) -o $@ $< + +build/%.o: src/%.cpp + @mkdir -p $(dir $@) + $(CC) $(CFLAGS) -o $@ $< + +sim/build/%.o: src/%.c + @mkdir -p $(dir $@) + $(CC) $(CFLAGS_SIM) -o $@ $< + +sim/build/%.o: src/%.cpp + @mkdir -p $(dir $@) + $(CC) $(CFLAGS_SIM) -o $@ $< + +sim/%.s: sim/%.elf + avr-objdump -d $< > $@ + +simuc: sim/$(NAME).elf + simuc --board nano-1284 $< + +gdb: sim/$(NAME).elf + avr-gdb $< + + +flash: dist/$(NAME).elf all + avrdude -c arduino -P /dev/ttyUSB0 -p $(AVRDUDE_DEVICE) -e -U flash:w:$< + +flash0: dist/$(NAME).elf all + avrdude -c arduino -P /dev/ttyUSB0 -p $(AVRDUDE_DEVICE) -e -U flash:w:$< + +flash1: dist/$(NAME).elf all + avrdude -c arduino -P /dev/ttyUSB1 -p $(AVRDUDE_DEVICE) -e -U flash:w:$< + +flash2: dist/$(NAME).elf all + avrdude -c arduino -P /dev/ttyUSB2 -p $(AVRDUDE_DEVICE) -e -U flash:w:$< + + +picocom: + # picocom sends CR for ENTER -> convert cr (\r) to lf (\n) + picocom -b 115200 --omap crlf --raise-dtr /dev/ttyUSB0 + +picocom0: + picocom -b 115200 --omap crlf --raise-dtr /dev/ttyUSB0 + +picocom1: + picocom -b 115200 --omap crlf --raise-dtr /dev/ttyUSB1 + +picocom2: + picocom -b 115200 --omap crlf --raise-dtr /dev/ttyUSB2 + + +isp-1284p: + avrdude -c usbasp -p $(AVRDUDE_DEVICE) + +isp-flash-1284p: dist/$(NAME).elf all + avrdude -c usbasp -p $(AVRDUDE_DEVICE) -e -U flash:w:$< + +flash-1284p: dist/$(NAME).elf all + avrdude -c arduino -p $(AVRDUDE_DEVICE) -P /dev/ttyUSB0 -b 115200 -e -U flash:w:$< + +release: dist/$(NAME).elf sim/$(NAME).elf + ../create-release release $(word 1, $^) release/sim $(word 2, $^) + +clean: + @rm -r dist + @rm -r build + @rm -r sim + @find . -type f -name ".depend" -exec rm {} \; + @echo "clean done" diff --git a/software/test-software/nano-644/release/v2024-10-28_141636/test-software_nano-644_12mhz.elf b/software/test-software/nano-644/release/v2024-10-28_141636/test-software_nano-644_12mhz.elf new file mode 100755 index 0000000000000000000000000000000000000000..0aa1e2f700270b068b3e43a75835550d655952e7 GIT binary patch literal 74504 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 literal 0 HcmV?d00001 diff --git a/software/test-software/src/adafruit/bme280.cpp b/software/test-software/src/adafruit/bme280.cpp new file mode 100644 index 0000000..1836c56 --- /dev/null +++ b/software/test-software/src/adafruit/bme280.cpp @@ -0,0 +1,512 @@ +#include "bme280.h" +#include +#include + +Adafruit_BME280 theBME280; +Adafruit_BME280_Temp bm280TempSensor; +Adafruit_BME280_Pressure bm280PressureSensor; +Adafruit_BME280_Humidity bm280HumiditySensor; + +Adafruit_BME280::Adafruit_BME280() { + static I2cMaster i2cDevice; + t_fine_adjust = 0; + temp_sensor = &bm280TempSensor; + pressure_sensor = &bm280PressureSensor; + humidity_sensor = &bm280HumiditySensor; + i2c_dev = &i2cDevice; +} + +bool Adafruit_BME280::begin (uint8_t addr) { + if (!i2c_dev->begin(addr)) { + return false; + } + return init(); +} + +bool Adafruit_BME280::init() { + _sensorID = read8(BME280_REGISTER_CHIPID); + if (_sensorID != 0x60) { + return false; + } + write8(BME280_REGISTER_SOFTRESET, 0xB6); + _delay_ms(10); // wait for chip to wake up. + + // if chip is still reading calibration, delay + while (isReadingCalibration()) { + _delay_ms(10); + } + + readCoefficients(); // read trimming parameters, see DS 4.2.2 + setSampling(); // use defaults + _delay_ms(100); + + return true; +} + +/*! + * @brief setup sensor with given parameters / settings + * + * This is simply a overload to the normal begin()-function, so SPI users + * don't get confused about the library requiring an address. + * @param mode the power mode to use for the sensor + * @param tempSampling the temp samping rate to use + * @param pressSampling the pressure sampling rate to use + * @param humSampling the humidity sampling rate to use + * @param filter the filter mode to use + * @param duration the standby duration to use + */ +void Adafruit_BME280::setSampling(sensor_mode mode, + sensor_sampling tempSampling, + sensor_sampling pressSampling, + sensor_sampling humSampling, + sensor_filter filter, + standby_duration duration) { + _measReg.mode = mode; + _measReg.osrs_t = tempSampling; + _measReg.osrs_p = pressSampling; + + _humReg.osrs_h = humSampling; + _configReg.filter = filter; + _configReg.t_sb = duration; + _configReg.spi3w_en = 0; + + // making sure sensor is in sleep mode before setting configuration + // as it otherwise may be ignored + write8(BME280_REGISTER_CONTROL, MODE_SLEEP); + + // you must make sure to also set REGISTER_CONTROL after setting the + // CONTROLHUMID register, otherwise the values won't be applied (see + // DS 5.4.3) + write8(BME280_REGISTER_CONTROLHUMID, _humReg.get()); + write8(BME280_REGISTER_CONFIG, _configReg.get()); + write8(BME280_REGISTER_CONTROL, _measReg.get()); +} + +/*! + * @brief Writes an 8 bit value over I2C or SPI + * @param reg the register address to write to + * @param value the value to write to the register + */ +void Adafruit_BME280::write8(uint8_t reg, uint8_t value) { + uint8_t buffer[2]; + buffer[1] = value; + if (i2c_dev) { + buffer[0] = reg; + i2c_dev->write(buffer, 2); + } +} + +/*! + * @brief Reads an 8 bit value over I2C or SPI + * @param reg the register address to read from + * @returns the data byte read from the device + */ +uint8_t Adafruit_BME280::read8(uint8_t reg) { + uint8_t buffer[1]; + if (i2c_dev) { + buffer[0] = uint8_t(reg); + i2c_dev->write_then_read(buffer, 1, buffer, 1); + } + return buffer[0]; +} + +/*! + * @brief Reads a 16 bit value over I2C or SPI + * @param reg the register address to read from + * @returns the 16 bit data value read from the device + */ +uint16_t Adafruit_BME280::read16(uint8_t reg) { + uint8_t buffer[2]; + + if (i2c_dev) { + buffer[0] = uint8_t(reg); + i2c_dev->write_then_read(buffer, 1, buffer, 2); + } + return uint16_t(buffer[0]) << 8 | uint16_t(buffer[1]); +} + +/*! + * @brief Reads a signed 16 bit little endian value over I2C or SPI + * @param reg the register address to read from + * @returns the 16 bit data value read from the device + */ +uint16_t Adafruit_BME280::read16_LE(uint8_t reg) { + uint16_t temp = read16(reg); + return (temp >> 8) | (temp << 8); +} + +/*! + * @brief Reads a signed 16 bit value over I2C or SPI + * @param reg the register address to read from + * @returns the 16 bit data value read from the device + */ +int16_t Adafruit_BME280::readS16(uint8_t reg) { return (int16_t)read16(reg); } + +/*! + * @brief Reads a signed little endian 16 bit value over I2C or SPI + * @param reg the register address to read from + * @returns the 16 bit data value read from the device + */ +int16_t Adafruit_BME280::readS16_LE(uint8_t reg) { + return (int16_t)read16_LE(reg); +} + +/*! + * @brief Reads a 24 bit value over I2C + * @param reg the register address to read from + * @returns the 24 bit data value read from the device + */ +uint32_t Adafruit_BME280::read24(uint8_t reg) { + uint8_t buffer[3]; + + if (i2c_dev) { + buffer[0] = uint8_t(reg); + i2c_dev->write_then_read(buffer, 1, buffer, 3); + } + return uint32_t(buffer[0]) << 16 | uint32_t(buffer[1]) << 8 | + uint32_t(buffer[2]); +} + +/*! + * @brief Take a new measurement (only possible in forced mode) + @returns true in case of success else false + */ +bool Adafruit_BME280::takeForcedMeasurement(void) { + bool return_value = false; + // If we are in forced mode, the BME sensor goes back to sleep after each + // measurement and we need to set it to forced mode once at this point, so + // it will take the next measurement and then return to sleep again. + // In normal mode simply does new measurements periodically. + if (_measReg.mode == MODE_FORCED) { + return_value = true; + // set to forced mode, i.e. "take next measurement" + write8(BME280_REGISTER_CONTROL, _measReg.get()); + // Store current time to measure the timeout + uint32_t timeout_start = millis(); + // wait until measurement has been completed, otherwise we would read the + // the values from the last measurement or the timeout occurred after 2 sec. + while (read8(BME280_REGISTER_STATUS) & 0x08) { + // In case of a timeout, stop the while loop + if ((millis() - timeout_start) > 2000) { + return_value = false; + break; + } + _delay_ms(1); + } + } + return return_value; +} + +/*! + * @brief Reads the factory-set coefficients + */ +void Adafruit_BME280::readCoefficients(void) { + _bme280_calib.dig_T1 = read16_LE(BME280_REGISTER_DIG_T1); + _bme280_calib.dig_T2 = readS16_LE(BME280_REGISTER_DIG_T2); + _bme280_calib.dig_T3 = readS16_LE(BME280_REGISTER_DIG_T3); + + _bme280_calib.dig_P1 = read16_LE(BME280_REGISTER_DIG_P1); + _bme280_calib.dig_P2 = readS16_LE(BME280_REGISTER_DIG_P2); + _bme280_calib.dig_P3 = readS16_LE(BME280_REGISTER_DIG_P3); + _bme280_calib.dig_P4 = readS16_LE(BME280_REGISTER_DIG_P4); + _bme280_calib.dig_P5 = readS16_LE(BME280_REGISTER_DIG_P5); + _bme280_calib.dig_P6 = readS16_LE(BME280_REGISTER_DIG_P6); + _bme280_calib.dig_P7 = readS16_LE(BME280_REGISTER_DIG_P7); + _bme280_calib.dig_P8 = readS16_LE(BME280_REGISTER_DIG_P8); + _bme280_calib.dig_P9 = readS16_LE(BME280_REGISTER_DIG_P9); + + _bme280_calib.dig_H1 = read8(BME280_REGISTER_DIG_H1); + _bme280_calib.dig_H2 = readS16_LE(BME280_REGISTER_DIG_H2); + _bme280_calib.dig_H3 = read8(BME280_REGISTER_DIG_H3); + _bme280_calib.dig_H4 = ((int8_t)read8(BME280_REGISTER_DIG_H4) << 4) | + (read8(BME280_REGISTER_DIG_H4 + 1) & 0xF); + _bme280_calib.dig_H5 = ((int8_t)read8(BME280_REGISTER_DIG_H5 + 1) << 4) | + (read8(BME280_REGISTER_DIG_H5) >> 4); + _bme280_calib.dig_H6 = (int8_t)read8(BME280_REGISTER_DIG_H6); +} + +/*! + * @brief return true if chip is busy reading cal data + * @returns true if reading calibration, false otherwise + */ +bool Adafruit_BME280::isReadingCalibration(void) { + uint8_t const rStatus = read8(BME280_REGISTER_STATUS); + + return (rStatus & (1 << 0)) != 0; +} + +/*! + * @brief Returns the temperature from the sensor + * @returns the temperature read from the device + */ +float Adafruit_BME280::readTemperature(void) { + int32_t var1, var2; + + int32_t adc_T = read24(BME280_REGISTER_TEMPDATA); + if (adc_T == 0x800000) // value in case temp measurement was disabled + return NAN; + adc_T >>= 4; + + var1 = (int32_t)((adc_T / 8) - ((int32_t)_bme280_calib.dig_T1 * 2)); + var1 = (var1 * ((int32_t)_bme280_calib.dig_T2)) / 2048; + var2 = (int32_t)((adc_T / 16) - ((int32_t)_bme280_calib.dig_T1)); + var2 = (((var2 * var2) / 4096) * ((int32_t)_bme280_calib.dig_T3)) / 16384; + + t_fine = var1 + var2 + t_fine_adjust; + + int32_t T = (t_fine * 5 + 128) / 256; + + return (float)T / 100; +} + +/*! + * @brief Returns the pressure from the sensor + * @returns the pressure value (in Pascal) read from the device + */ +float Adafruit_BME280::readPressure(void) { + int64_t var1, var2, var3, var4; + + readTemperature(); // must be done first to get t_fine + + int32_t adc_P = read24(BME280_REGISTER_PRESSUREDATA); + if (adc_P == 0x800000) // value in case pressure measurement was disabled + return NAN; + adc_P >>= 4; + + var1 = ((int64_t)t_fine) - 128000; + var2 = var1 * var1 * (int64_t)_bme280_calib.dig_P6; + var2 = var2 + ((var1 * (int64_t)_bme280_calib.dig_P5) * 131072); + var2 = var2 + (((int64_t)_bme280_calib.dig_P4) * 34359738368); + var1 = ((var1 * var1 * (int64_t)_bme280_calib.dig_P3) / 256) + + ((var1 * ((int64_t)_bme280_calib.dig_P2) * 4096)); + var3 = ((int64_t)1) * 140737488355328; + var1 = (var3 + var1) * ((int64_t)_bme280_calib.dig_P1) / 8589934592; + + if (var1 == 0) { + return 0; // avoid exception caused by division by zero + } + + var4 = 1048576 - adc_P; + var4 = (((var4 * 2147483648UL) - var2) * 3125) / var1; + var1 = (((int64_t)_bme280_calib.dig_P9) * (var4 / 8192) * (var4 / 8192)) / + 33554432; + var2 = (((int64_t)_bme280_calib.dig_P8) * var4) / 524288; + var4 = ((var4 + var1 + var2) / 256) + (((int64_t)_bme280_calib.dig_P7) * 16); + + float P = var4 / 256.0; + + return P; +} + +/*! + * @brief Returns the humidity from the sensor + * @returns the humidity value read from the device + */ +float Adafruit_BME280::readHumidity(void) { + int32_t var1, var2, var3, var4, var5; + + readTemperature(); // must be done first to get t_fine + + int32_t adc_H = read16(BME280_REGISTER_HUMIDDATA); + if (adc_H == 0x8000) // value in case humidity measurement was disabled + return NAN; + + var1 = t_fine - ((int32_t)76800); + var2 = (int32_t)(adc_H * 16384); + var3 = (int32_t)(((int32_t)_bme280_calib.dig_H4) * 1048576); + var4 = ((int32_t)_bme280_calib.dig_H5) * var1; + var5 = (((var2 - var3) - var4) + (int32_t)16384) / 32768; + var2 = (var1 * ((int32_t)_bme280_calib.dig_H6)) / 1024; + var3 = (var1 * ((int32_t)_bme280_calib.dig_H3)) / 2048; + var4 = ((var2 * (var3 + (int32_t)32768)) / 1024) + (int32_t)2097152; + var2 = ((var4 * ((int32_t)_bme280_calib.dig_H2)) + 8192) / 16384; + var3 = var5 * var2; + var4 = ((var3 / 32768) * (var3 / 32768)) / 128; + var5 = var3 - ((var4 * ((int32_t)_bme280_calib.dig_H1)) / 16); + var5 = (var5 < 0 ? 0 : var5); + var5 = (var5 > 419430400 ? 419430400 : var5); + uint32_t H = (uint32_t)(var5 / 4096); + + return (float)H / 1024.0; +} + +/*! + * Calculates the altitude (in meters) from the specified atmospheric + * pressure (in hPa), and sea-level pressure (in hPa). + * @param seaLevel Sea-level pressure in hPa + * @returns the altitude value read from the device + */ +float Adafruit_BME280::readAltitude(float seaLevel) { + // Equation taken from BMP180 datasheet (page 16): + // http://www.adafruit.com/datasheets/BST-BMP180-DS000-09.pdf + + // Note that using the equation from wikipedia can give bad results + // at high altitude. See this thread for more information: + // http://forums.adafruit.com/viewtopic.php?f=22&t=58064 + + float atmospheric = readPressure() / 100.0F; + return 44330.0 * (1.0 - pow(atmospheric / seaLevel, 0.1903)); +} + +/*! + * Calculates the pressure at sea level (in hPa) from the specified + * altitude (in meters), and atmospheric pressure (in hPa). + * @param altitude Altitude in meters + * @param atmospheric Atmospheric pressure in hPa + * @returns the pressure at sea level (in hPa) from the specified altitude + */ +float Adafruit_BME280::seaLevelForAltitude(float altitude, float atmospheric) { + // Equation taken from BMP180 datasheet (page 17): + // http://www.adafruit.com/datasheets/BST-BMP180-DS000-09.pdf + + // Note that using the equation from wikipedia can give bad results + // at high altitude. See this thread for more information: + // http://forums.adafruit.com/viewtopic.php?f=22&t=58064 + + return atmospheric / pow(1.0 - (altitude / 44330.0), 5.255); +} + +/*! + * Returns Sensor ID found by init() for diagnostics + * @returns Sensor ID 0x60 for BME280, 0x56, 0x57, 0x58 BMP280 + */ +uint32_t Adafruit_BME280::sensorID(void) { return _sensorID; } + +/*! + * Returns the current temperature compensation value in degrees Celsius + * @returns the current temperature compensation value in degrees Celsius + */ +float Adafruit_BME280::getTemperatureCompensation(void) { + return float((t_fine_adjust * 5) >> 8) / 100.0; +}; + +/*! + * Sets a value to be added to each temperature reading. This adjusted + * temperature is used in pressure and humidity readings. + * @param adjustment Value to be added to each temperature reading in Celsius + */ +void Adafruit_BME280::setTemperatureCompensation(float adjustment) { + // convert the value in C into and adjustment to t_fine + t_fine_adjust = ((int32_t(adjustment * 100) << 8)) / 5; +}; + + +/**************************************************************************/ +/*! + @brief Gets the sensor_t data for the BME280's temperature sensor +*/ +/**************************************************************************/ +void Adafruit_BME280_Temp::getSensor(sensor_t *sensor) { + /* Clear the sensor_t object */ + memset(sensor, 0, sizeof(sensor_t)); + + /* Insert the sensor name in the fixed length char array */ + strncpy(sensor->name, "BME280", sizeof(sensor->name) - 1); + sensor->name[sizeof(sensor->name) - 1] = 0; + sensor->version = 1; + sensor->sensor_id = _sensorID; + sensor->type = SENSOR_TYPE_AMBIENT_TEMPERATURE; + sensor->min_delay = 0; + sensor->min_value = -40.0; /* Temperature range -40 ~ +85 C */ + sensor->max_value = +85.0; + sensor->resolution = 0.01; /* 0.01 C */ +} + +/**************************************************************************/ +/*! + @brief Gets the temperature as a standard sensor event + @param event Sensor event object that will be populated + @returns True +*/ +/**************************************************************************/ +bool Adafruit_BME280_Temp::getEvent(sensors_event_t *event) { + /* Clear the event */ + memset(event, 0, sizeof(sensors_event_t)); + + event->version = sizeof(sensors_event_t); + event->sensor_id = _sensorID; + event->type = SENSOR_TYPE_AMBIENT_TEMPERATURE; + event->timestamp = millis(); + event->temperature = theBME280.readTemperature(); + return true; +} + +/**************************************************************************/ +/*! + @brief Gets the sensor_t data for the BME280's pressure sensor +*/ +/**************************************************************************/ +void Adafruit_BME280_Pressure::getSensor(sensor_t *sensor) { + /* Clear the sensor_t object */ + memset(sensor, 0, sizeof(sensor_t)); + + /* Insert the sensor name in the fixed length char array */ + strncpy(sensor->name, "BME280", sizeof(sensor->name) - 1); + sensor->name[sizeof(sensor->name) - 1] = 0; + sensor->version = 1; + sensor->sensor_id = _sensorID; + sensor->type = SENSOR_TYPE_PRESSURE; + sensor->min_delay = 0; + sensor->min_value = 300.0; /* 300 ~ 1100 hPa */ + sensor->max_value = 1100.0; + sensor->resolution = 0.012; /* 0.12 hPa relative */ +} + +/**************************************************************************/ +/*! + @brief Gets the pressure as a standard sensor event + @param event Sensor event object that will be populated + @returns True +*/ +/**************************************************************************/ +bool Adafruit_BME280_Pressure::getEvent(sensors_event_t *event) { + /* Clear the event */ + memset(event, 0, sizeof(sensors_event_t)); + + event->version = sizeof(sensors_event_t); + event->sensor_id = _sensorID; + event->type = SENSOR_TYPE_PRESSURE; + event->timestamp = millis(); + event->pressure = theBME280.readPressure() / 100; // convert Pa to hPa + return true; +} + +/**************************************************************************/ +/*! + @brief Gets the sensor_t data for the BME280's humidity sensor +*/ +/**************************************************************************/ +void Adafruit_BME280_Humidity::getSensor(sensor_t *sensor) { + /* Clear the sensor_t object */ + memset(sensor, 0, sizeof(sensor_t)); + + /* Insert the sensor name in the fixed length char array */ + strncpy(sensor->name, "BME280", sizeof(sensor->name) - 1); + sensor->name[sizeof(sensor->name) - 1] = 0; + sensor->version = 1; + sensor->sensor_id = _sensorID; + sensor->type = SENSOR_TYPE_RELATIVE_HUMIDITY; + sensor->min_delay = 0; + sensor->min_value = 0; + sensor->max_value = 100; /* 0 - 100 % */ + sensor->resolution = 3; /* 3% accuracy */ +} + +/**************************************************************************/ +/*! + @brief Gets the humidity as a standard sensor event + @param event Sensor event object that will be populated + @returns True +*/ +/**************************************************************************/ +bool Adafruit_BME280_Humidity::getEvent(sensors_event_t *event) { + /* Clear the event */ + memset(event, 0, sizeof(sensors_event_t)); + + event->version = sizeof(sensors_event_t); + event->sensor_id = _sensorID; + event->type = SENSOR_TYPE_RELATIVE_HUMIDITY; + event->timestamp = millis(); + event->relative_humidity = theBME280.readHumidity(); + return true; +} diff --git a/software/test-software/src/adafruit/bme280.h b/software/test-software/src/adafruit/bme280.h new file mode 100644 index 0000000..aa5aa72 --- /dev/null +++ b/software/test-software/src/adafruit/bme280.h @@ -0,0 +1,373 @@ +// https://github.com/adafruit/Adafruit_BME280_Library + +/*! + * @file Adafruit_BME280.h + * + * Designed specifically to work with the Adafruit BME280 Breakout + * ----> http://www.adafruit.com/products/2650 + * + * These sensors use I2C or SPI to communicate, 2 or 4 pins are required + * to interface. + * + * Adafruit invests time and resources providing this open source code, + * please support Adafruit and open-source hardware by purchasing + * products from Adafruit! + * + * Written by Kevin "KTOWN" Townsend for Adafruit Industries. + * + * BSD license, all text here must be included in any redistribution. + * See the LICENSE file for details. + * + */ + +#ifndef __BME280_H__ +#define __BME280_H__ + +// #include "Arduino.h" + +// #include +// #include +// #include + + +#include "../i2cmaster.hpp" +#include "../main.hpp" +// #define byte uint8_t + + + +#include +#include +#include "sensor.h" + +/*! + * @brief default I2C address + */ +#define BME280_ADDRESS (0x77) // Primary I2C Address + /*! + * @brief alternate I2C address + */ +#define BME280_ADDRESS_ALTERNATE (0x76) // Alternate Address + +/*! + * @brief Register addresses + */ +enum { + BME280_REGISTER_DIG_T1 = 0x88, + BME280_REGISTER_DIG_T2 = 0x8A, + BME280_REGISTER_DIG_T3 = 0x8C, + + BME280_REGISTER_DIG_P1 = 0x8E, + BME280_REGISTER_DIG_P2 = 0x90, + BME280_REGISTER_DIG_P3 = 0x92, + BME280_REGISTER_DIG_P4 = 0x94, + BME280_REGISTER_DIG_P5 = 0x96, + BME280_REGISTER_DIG_P6 = 0x98, + BME280_REGISTER_DIG_P7 = 0x9A, + BME280_REGISTER_DIG_P8 = 0x9C, + BME280_REGISTER_DIG_P9 = 0x9E, + + BME280_REGISTER_DIG_H1 = 0xA1, + BME280_REGISTER_DIG_H2 = 0xE1, + BME280_REGISTER_DIG_H3 = 0xE3, + BME280_REGISTER_DIG_H4 = 0xE4, + BME280_REGISTER_DIG_H5 = 0xE5, + BME280_REGISTER_DIG_H6 = 0xE7, + + BME280_REGISTER_CHIPID = 0xD0, + BME280_REGISTER_VERSION = 0xD1, + BME280_REGISTER_SOFTRESET = 0xE0, + + BME280_REGISTER_CAL26 = 0xE1, // R calibration stored in 0xE1-0xF0 + + BME280_REGISTER_CONTROLHUMID = 0xF2, + BME280_REGISTER_STATUS = 0XF3, + BME280_REGISTER_CONTROL = 0xF4, + BME280_REGISTER_CONFIG = 0xF5, + BME280_REGISTER_PRESSUREDATA = 0xF7, + BME280_REGISTER_TEMPDATA = 0xFA, + BME280_REGISTER_HUMIDDATA = 0xFD +}; + +/**************************************************************************/ +/*! + @brief calibration data +*/ +/**************************************************************************/ +typedef struct { + uint16_t dig_T1; ///< temperature compensation value + int16_t dig_T2; ///< temperature compensation value + int16_t dig_T3; ///< temperature compensation value + + uint16_t dig_P1; ///< pressure compensation value + int16_t dig_P2; ///< pressure compensation value + int16_t dig_P3; ///< pressure compensation value + int16_t dig_P4; ///< pressure compensation value + int16_t dig_P5; ///< pressure compensation value + int16_t dig_P6; ///< pressure compensation value + int16_t dig_P7; ///< pressure compensation value + int16_t dig_P8; ///< pressure compensation value + int16_t dig_P9; ///< pressure compensation value + + uint8_t dig_H1; ///< humidity compensation value + int16_t dig_H2; ///< humidity compensation value + uint8_t dig_H3; ///< humidity compensation value + int16_t dig_H4; ///< humidity compensation value + int16_t dig_H5; ///< humidity compensation value + int8_t dig_H6; ///< humidity compensation value +} bme280_calib_data; +/*=========================================================================*/ + +class Adafruit_BME280; + +/** Adafruit Unified Sensor interface for temperature component of BME280 */ +class Adafruit_BME280_Temp : public Adafruit_Sensor { +public: + /** @brief Create an Adafruit_Sensor compatible object for the temp sensor + @param parent A pointer to the BME280 class */ + Adafruit_BME280_Temp() { _sensorID = 280; } + bool getEvent(sensors_event_t *); + void getSensor(sensor_t *); + +private: + int _sensorID; +}; + +/** Adafruit Unified Sensor interface for pressure component of BME280 */ +class Adafruit_BME280_Pressure : public Adafruit_Sensor { +public: + /** @brief Create an Adafruit_Sensor compatible object for the pressure sensor + @param parent A pointer to the BME280 class */ + Adafruit_BME280_Pressure() { _sensorID = 280; } + bool getEvent(sensors_event_t *); + void getSensor(sensor_t *); + +private: + int _sensorID; +}; + +/** Adafruit Unified Sensor interface for humidity component of BME280 */ +class Adafruit_BME280_Humidity : public Adafruit_Sensor { +public: + /** @brief Create an Adafruit_Sensor compatible object for the humidity sensor + @param parent A pointer to the BME280 class */ + Adafruit_BME280_Humidity() { _sensorID = 280;} + bool getEvent(sensors_event_t *); + void getSensor(sensor_t *); + +private: + int _sensorID; +}; + +/**************************************************************************/ +/*! + @brief Class that stores state and functions for interacting with BME280 IC +*/ +/**************************************************************************/ +class Adafruit_BME280 { +public: + /**************************************************************************/ + /*! + @brief sampling rates + */ + /**************************************************************************/ + enum sensor_sampling { + SAMPLING_NONE = 0b000, + SAMPLING_X1 = 0b001, + SAMPLING_X2 = 0b010, + SAMPLING_X4 = 0b011, + SAMPLING_X8 = 0b100, + SAMPLING_X16 = 0b101 + }; + + /**************************************************************************/ + /*! + @brief power modes + */ + /**************************************************************************/ + enum sensor_mode { + MODE_SLEEP = 0b00, + MODE_FORCED = 0b01, + MODE_NORMAL = 0b11 + }; + + /**************************************************************************/ + /*! + @brief filter values + */ + /**************************************************************************/ + enum sensor_filter { + FILTER_OFF = 0b000, + FILTER_X2 = 0b001, + FILTER_X4 = 0b010, + FILTER_X8 = 0b011, + FILTER_X16 = 0b100 + }; + + /**************************************************************************/ + /*! + @brief standby duration in ms + */ + /**************************************************************************/ + enum standby_duration { + STANDBY_MS_0_5 = 0b000, + STANDBY_MS_10 = 0b110, + STANDBY_MS_20 = 0b111, + STANDBY_MS_62_5 = 0b001, + STANDBY_MS_125 = 0b010, + STANDBY_MS_250 = 0b011, + STANDBY_MS_500 = 0b100, + STANDBY_MS_1000 = 0b101 + }; + + // constructors + Adafruit_BME280(); + + bool begin(uint8_t addr = BME280_ADDRESS); + bool init(); + + void setSampling(sensor_mode mode = MODE_NORMAL, + sensor_sampling tempSampling = SAMPLING_X16, + sensor_sampling pressSampling = SAMPLING_X16, + sensor_sampling humSampling = SAMPLING_X16, + sensor_filter filter = FILTER_OFF, + standby_duration duration = STANDBY_MS_0_5); + + bool takeForcedMeasurement(void); + float readTemperature(void); + float readPressure(void); + float readHumidity(void); + + float readAltitude(float seaLevel); + float seaLevelForAltitude(float altitude, float pressure); + uint32_t sensorID(void); + + float getTemperatureCompensation(void); + void setTemperatureCompensation(float); + +protected: + I2cMaster *i2c_dev = NULL; ///< Pointer to I2C bus interface + // Adafruit_SPIDevice *spi_dev = NULL; ///< Pointer to SPI bus interface + + Adafruit_BME280_Temp *temp_sensor; + Adafruit_BME280_Pressure *pressure_sensor; + Adafruit_BME280_Humidity *humidity_sensor; + + void readCoefficients(void); + bool isReadingCalibration(void); + + void write8(uint8_t reg, uint8_t value); + uint8_t read8(uint8_t reg); + uint16_t read16(uint8_t reg); + uint32_t read24(uint8_t reg); + int16_t readS16(uint8_t reg); + uint16_t read16_LE(uint8_t reg); // little endian + int16_t readS16_LE(uint8_t reg); // little endian + + uint8_t _i2caddr; //!< I2C addr for the TwoWire interface + int32_t _sensorID; //!< ID of the BME Sensor + int32_t t_fine; //!< temperature with high resolution, stored as an attribute + //!< as this is used for temperature compensation reading + //!< humidity and pressure + + int32_t t_fine_adjust; //!< add to compensate temp readings and in turn + //!< to pressure and humidity readings + + bme280_calib_data _bme280_calib; //!< here calibration data is stored + + /**************************************************************************/ + /*! + @brief config register + */ + /**************************************************************************/ + struct config { + // inactive duration (standby time) in normal mode + // 000 = 0.5 ms + // 001 = 62.5 ms + // 010 = 125 ms + // 011 = 250 ms + // 100 = 500 ms + // 101 = 1000 ms + // 110 = 10 ms + // 111 = 20 ms + unsigned int t_sb : 3; ///< inactive duration (standby time) in normal mode + + // filter settings + // 000 = filter off + // 001 = 2x filter + // 010 = 4x filter + // 011 = 8x filter + // 100 and above = 16x filter + unsigned int filter : 3; ///< filter settings + + // unused - don't set + unsigned int none : 1; ///< unused - don't set + unsigned int spi3w_en : 1; ///< unused - don't set + + /// @return combined config register + unsigned int get() { return (t_sb << 5) | (filter << 2) | spi3w_en; } + }; + config _configReg; //!< config register object + + /**************************************************************************/ + /*! + @brief ctrl_meas register + */ + /**************************************************************************/ + struct ctrl_meas { + // temperature oversampling + // 000 = skipped + // 001 = x1 + // 010 = x2 + // 011 = x4 + // 100 = x8 + // 101 and above = x16 + unsigned int osrs_t : 3; ///< temperature oversampling + + // pressure oversampling + // 000 = skipped + // 001 = x1 + // 010 = x2 + // 011 = x4 + // 100 = x8 + // 101 and above = x16 + unsigned int osrs_p : 3; ///< pressure oversampling + + // device mode + // 00 = sleep + // 01 or 10 = forced + // 11 = normal + unsigned int mode : 2; ///< device mode + + /// @return combined ctrl register + unsigned int get() { return (osrs_t << 5) | (osrs_p << 2) | mode; } + }; + ctrl_meas _measReg; //!< measurement register object + + /**************************************************************************/ + /*! + @brief ctrl_hum register + */ + /**************************************************************************/ + struct ctrl_hum { + /// unused - don't set + unsigned int none : 5; + + // pressure oversampling + // 000 = skipped + // 001 = x1 + // 010 = x2 + // 011 = x4 + // 100 = x8 + // 101 and above = x16 + unsigned int osrs_h : 3; ///< pressure oversampling + + /// @return combined ctrl hum register + unsigned int get() { return (osrs_h); } + }; + ctrl_hum _humReg; //!< hum register object +}; + +extern Adafruit_BME280 theBME280; + +#endif diff --git a/software/test-software/src/adafruit/ens160.cpp b/software/test-software/src/adafruit/ens160.cpp new file mode 100644 index 0000000..29e3704 --- /dev/null +++ b/software/test-software/src/adafruit/ens160.cpp @@ -0,0 +1,374 @@ +/* + ScioSense_ENS160.h - Library for the ENS160 sensor with I2C interface from ScioSense + 2023 Mar 23 v6 Christoph Friese Bugfix measurement routine, prepare next release + 2021 Nov 25 v5 Martin Herold Custom mode timing fixed + 2021 Feb 04 v4 Giuseppe de Pinto Custom mode fixed + 2020 Apr 06 v3 Christoph Friese Changed nomenclature to ScioSense as product shifted from ams + 2020 Feb 15 v2 Giuseppe Pasetti Corrected firmware flash option + 2019 May 05 v1 Christoph Friese Created + based on application note "ENS160 Software Integration.pdf" rev 0.01 +*/ + +#include "ens160.h" +#include "math.h" +#include +#include +#include + +ScioSense_ENS160::ScioSense_ENS160 () { + _revENS16x = 0; + + //Isotherm, HP0 252°C / HP1 350°C / HP2 250°C / HP3 324°C / measure every 1008ms + _seq_steps[0][0] = 0x7c; + _seq_steps[0][1] = 0x0a; + _seq_steps[0][2] = 0x7e; + _seq_steps[0][3] = 0xaf; + _seq_steps[0][4] = 0xaf; + _seq_steps[0][5] = 0xa2; + _seq_steps[0][6] = 0x00; + _seq_steps[0][7] = 0x80; +} + +bool ScioSense_ENS160::begin () { + i2cDevice.begin(ENS160_I2CADDR_1); + _delay_ms(ENS160_BOOTING); + if (reset()) { + if (checkPartID()) { + if (setMode(ENS160_OPMODE_IDLE)) { + if (clearCommand()) { + if (getFirmware()) { + return true; + } + } + } + } + } + return false; + +} + +bool ScioSense_ENS160::write8 (uint8_t reg, uint8_t value) { + uint8_t buffer[2]; + buffer[1] = value; + buffer[0] = reg; + return i2cDevice.write(buffer, 2); +} + +bool ScioSense_ENS160::read8 (uint8_t reg, uint8_t *value) { + uint8_t buffer[1]; + buffer[0] = uint8_t(reg); + return i2cDevice.write_then_read(buffer, 1, value, 1); +} + +bool ScioSense_ENS160::read16 (uint8_t reg, uint16_t *value) { + uint8_t buffer[1]; + buffer[0] = uint8_t(reg); + return i2cDevice.write_then_read(buffer, 1, (uint8_t *)value, 2); +} + +bool ScioSense_ENS160::read16LE (uint8_t reg, uint16_t *value) { + uint16_t tmp; + if (read16(reg, &tmp)) { + *value = ((tmp & 0xff) << 8) | (tmp >> 8); + return true; + } + return false; +} + +bool ScioSense_ENS160::readBytes (uint8_t reg, uint8_t *bytes, uint8_t len) { + uint8_t buffer[1]; + buffer[0] = uint8_t(reg); + return i2cDevice.write_then_read(buffer, 1, buffer, len); +} + + +// Sends a reset to the ENS160. Returns false on I2C problems. +bool ScioSense_ENS160::reset () { + if (write8(ENS160_REG_OPMODE, ENS160_OPMODE_RESET)) { + _delay_ms(ENS160_BOOTING); + return true; + } + _delay_ms(ENS160_BOOTING); + return false; +} + +// Reads the part ID and confirms valid sensor +bool ScioSense_ENS160::checkPartID () { + uint16_t part_id; + + read16(ENS160_REG_PART_ID, &part_id); + _delay_ms(ENS160_BOOTING); + + if (part_id == ENS160_PARTID) { + _revENS16x = 0; + return true; + + } else if (part_id == ENS161_PARTID) { + _revENS16x = 1; + return true; + } + + return false; +} + +// Initialize idle mode and confirms +bool ScioSense_ENS160::clearCommand () { + uint8_t status; + + if (write8(ENS160_REG_COMMAND, ENS160_COMMAND_NOP)) { + if (write8(ENS160_REG_COMMAND, ENS160_COMMAND_CLRGPR)) { + _delay_ms(ENS160_BOOTING); + if (read8(ENS160_REG_DATA_STATUS, &status)) { + return true; + } + } + } + _delay_ms(ENS160_BOOTING); + return false; +} + +// Read firmware revisions +bool ScioSense_ENS160::getFirmware () { + uint8_t i2cbuf[3]; + + if (clearCommand()) { + _delay_ms(ENS160_BOOTING); + if (write8(ENS160_REG_COMMAND, ENS160_COMMAND_GET_APPVER)) { + if (readBytes(ENS160_REG_GPR_READ_4, i2cbuf, 3)) { + _fw_ver_major = i2cbuf[0]; + _fw_ver_minor = i2cbuf[1]; + _fw_ver_build = i2cbuf[2]; + _revENS16x = this->_fw_ver_major > 6 ? 1 : 0; + _delay_ms(ENS160_BOOTING); + return true; + } + } + } + _delay_ms(ENS160_BOOTING); + return false; +} + +// Set operation mode of sensor +bool ScioSense_ENS160::setMode (uint8_t mode) { + //LP only valid for rev>0 + if ((mode == ENS160_OPMODE_LP) and (_revENS16x == 0)) { + return false; + } + if (write8(ENS160_REG_OPMODE, mode)) { + _delay_ms(ENS160_BOOTING); + return true; + } + _delay_ms(ENS160_BOOTING); + return false; +} + +// Initialize definition of custom mode with steps +bool ScioSense_ENS160::initCustomMode (uint16_t stepNum) { + if (stepNum > 0) { + _stepCount = stepNum; + if (setMode(ENS160_OPMODE_IDLE)) { + if (clearCommand()) { + if (write8(ENS160_REG_COMMAND, ENS160_COMMAND_SETSEQ)) { + _delay_ms(ENS160_BOOTING); + return true; + } + } + } + } + _delay_ms(ENS160_BOOTING); + return false; +} + +// Add a step to custom measurement profile with definition of duration, enabled data acquisition and temperature for each hotplate +bool ScioSense_ENS160::addCustomStep (uint16_t time, bool measureHP0, bool measureHP1, bool measureHP2, bool measureHP3, uint16_t tempHP0, uint16_t tempHP1, uint16_t tempHP2, uint16_t tempHP3) { + uint8_t seq_ack; + uint8_t temp; + + _delay_ms(ENS160_BOOTING); + + temp = (uint8_t)(((time / 24) - 1) << 6); + if (measureHP0) { + temp = temp | 0x20; + } + if (measureHP1) { + temp = temp | 0x10; + } + if (measureHP2) { + temp = temp | 0x08; + } + if (measureHP3) { + temp = temp | 0x04; + } + if (!write8(ENS160_REG_GPR_WRITE_0, temp)) { + return false; + } + temp = (uint8_t)(((time / 24) - 1) >> 2); + if (!write8(ENS160_REG_GPR_WRITE_1, temp)) { + return false; + } + if (!write8(ENS160_REG_GPR_WRITE_2, (uint8_t)(tempHP0 / 2))) { + return false; + } + if (!write8(ENS160_REG_GPR_WRITE_3, (uint8_t)(tempHP1 / 2))) { + return false; + } + if (write8(ENS160_REG_GPR_WRITE_4, (uint8_t)(tempHP2 / 2))) { + return false; + } + if (write8(ENS160_REG_GPR_WRITE_5, (uint8_t)(tempHP3 / 2))) { + return false; + } + + if (write8(ENS160_REG_GPR_WRITE_6, (uint8_t)(_stepCount - 1))) { + return false; + } + + if (_stepCount == 1) { + if (!write8(ENS160_REG_GPR_WRITE_7, 128)) { + return false; + } + } else { + if (!write8(ENS160_REG_GPR_WRITE_7, 0)) { + return false; + } + } + _delay_ms(ENS160_BOOTING); + + if (!read8(ENS160_REG_GPR_READ_7, &seq_ack)) { + return false; + } + _delay_ms(ENS160_BOOTING); + + if ((ENS160_SEQ_ACK_COMPLETE | _stepCount) != seq_ack) { + _stepCount++; + return false; + } + + return true; +} + +bool ScioSense_ENS160::readStatus (uint8_t *status) { + return read8(ENS160_REG_DATA_STATUS, status); +} + +bool ScioSense_ENS160::readData (ENS160_DATA *data) { + uint8_t buffer[1] = { 0x21 }; + return i2cDevice.write_then_read(buffer, 1, (uint8_t *)data, sizeof(ENS160_DATA)); +} + +// Perform prediction measurement and stores result in internal variables +bool ScioSense_ENS160::measure (bool waitForNew) { + uint8_t i2cbuf[8]; + uint8_t status; + + // Set default status for early bail out + if (waitForNew) { + do { + if (!read8(ENS160_REG_DATA_STATUS, &status)) { + return false; + } + _delay_ms(1); + } while (!IS_NEWDAT(status)); + } else { + if (!read8(ENS160_REG_DATA_STATUS, &status)) { + return false; + } + } + + + // Read predictions + if (IS_NEWDAT(status)) { + if (!readBytes(ENS160_REG_DATA_AQI, i2cbuf, 7)) { + return false; + } + return false; + _data_aqi = i2cbuf[0]; + _data_tvoc = i2cbuf[1] | ((uint16_t)i2cbuf[2] << 8); + _data_eco2 = i2cbuf[3] | ((uint16_t)i2cbuf[4] << 8); + if (_revENS16x > 0) { + _data_aqi500 = ((uint16_t)i2cbuf[5]) | ((uint16_t)i2cbuf[6] << 8); + } else { + _data_aqi500 = 0; + } + return true; + } + + return false; +} + +// Perfrom raw measurement and stores result in internal variables +bool ScioSense_ENS160::measureRaw (bool waitForNew) { + uint8_t i2cbuf[8]; + uint8_t status; + + // Set default status for early bail out + if (waitForNew) { + do { + _delay_ms(1); + if (!read8(ENS160_REG_DATA_STATUS, &status)) { + return false; + } + } while (!IS_NEWGPR(status)); + } else { + if (!read8(ENS160_REG_DATA_STATUS, &status)) { + return false; + } + } + + if (IS_NEWGPR(status)) { + + // Read raw resistance values + if (!readBytes(ENS160_REG_GPR_READ_0, i2cbuf, 8)) { + return false; + } + _hp0_rs = CONVERT_RS_RAW2OHMS_F((uint32_t)(i2cbuf[0] | ((uint16_t)i2cbuf[1] << 8))); + _hp1_rs = CONVERT_RS_RAW2OHMS_F((uint32_t)(i2cbuf[2] | ((uint16_t)i2cbuf[3] << 8))); + _hp2_rs = CONVERT_RS_RAW2OHMS_F((uint32_t)(i2cbuf[4] | ((uint16_t)i2cbuf[5] << 8))); + _hp3_rs = CONVERT_RS_RAW2OHMS_F((uint32_t)(i2cbuf[6] | ((uint16_t)i2cbuf[7] << 8))); + + // Read baselines + if (!readBytes(ENS160_REG_DATA_BL, i2cbuf, 8)) { + return false; + } + _hp0_bl = CONVERT_RS_RAW2OHMS_F((uint32_t)(i2cbuf[0] | ((uint16_t)i2cbuf[1] << 8))); + _hp1_bl = CONVERT_RS_RAW2OHMS_F((uint32_t)(i2cbuf[2] | ((uint16_t)i2cbuf[3] << 8))); + _hp2_bl = CONVERT_RS_RAW2OHMS_F((uint32_t)(i2cbuf[4] | ((uint16_t)i2cbuf[5] << 8))); + _hp3_bl = CONVERT_RS_RAW2OHMS_F((uint32_t)(i2cbuf[6] | ((uint16_t)i2cbuf[7] << 8))); + + if (!read8(ENS160_REG_DATA_MISR, i2cbuf)) { + return false; + } + _misr = i2cbuf[0]; + return true; + } + + return false; +} + + +// Writes t (degC) and h (%rh) to ENV_DATA. Returns false on I2C problems. +bool ScioSense_ENS160::set_envdata (float t, float h) { + uint16_t t_data = (uint16_t)((t + 273.15f) * 64.0f); + uint16_t rh_data = (uint16_t)(h * 512.0f); + return this->set_envdata210(t_data, rh_data); +} + +// Writes t and h (in ENS210 format) to ENV_DATA. Returns false on I2C problems. +bool ScioSense_ENS160::set_envdata210 (uint16_t t, uint16_t h) { + //uint16_t temp; + uint8_t trh_in[4]; + + //temp = (uint16_t)((t + 273.15f) * 64.0f); + trh_in[0] = t & 0xff; + trh_in[1] = (t >> 8) & 0xff; + + //temp = (uint16_t)(h * 512.0f); + trh_in[2] = h & 0xff; + trh_in[3] = (h >> 8) & 0xff; + + if (!i2cDevice.writeByteAndBuffer(ENS160_REG_TEMP_IN, trh_in, 4)) { + return false; + } + + return true; +} diff --git a/software/test-software/src/adafruit/ens160.h b/software/test-software/src/adafruit/ens160.h new file mode 100644 index 0000000..7f26ba1 --- /dev/null +++ b/software/test-software/src/adafruit/ens160.h @@ -0,0 +1,188 @@ +/* + ScioSense_ENS160.h - Library for the ENS160 sensor with I2C interface from ScioSense + 2023 Mar 23 v6 Christoph Friese Bugfix measurement routine, prepare next release + 2021 July 29 v4 Christoph Friese Changed nomenclature to ScioSense as product shifted from ams + 2020 Apr 06 v3 Christoph Friese Changed nomenclature to ScioSense as product shifted from ams + 2020 Feb 15 v2 Giuseppe Pasetti Corrected firmware flash option + 2019 May 05 v1 Christoph Friese Created + based on application note "ENS160 Software Integration.pdf" rev 0.01 +*/ + +#ifndef __SCIOSENSE_ENS160_H_ +#define __SCIOSENSE_ENS160_H_ + +#include "../i2cmaster.hpp" +#include +// #define byte uint8_t + +// #if (ARDUINO >= 100) +// #include "Arduino.h" +// #else +// #include "WProgram.h" +// #endif + +// #include + +// Chip constants +#define ENS160_PARTID 0x0160 +#define ENS161_PARTID 0x0161 +#define ENS160_BOOTING 10 + +// 7-bit I2C slave address of the ENS160 +#define ENS160_I2CADDR_0 0x52 //ADDR low +#define ENS160_I2CADDR_1 0x53 //ADDR high + +// ENS160 registers for version V0 +#define ENS160_REG_PART_ID 0x00 // 2 byte register +#define ENS160_REG_OPMODE 0x10 +#define ENS160_REG_CONFIG 0x11 +#define ENS160_REG_COMMAND 0x12 +#define ENS160_REG_TEMP_IN 0x13 +#define ENS160_REG_RH_IN 0x15 +#define ENS160_REG_DATA_STATUS 0x20 +#define ENS160_REG_DATA_AQI 0x21 +#define ENS160_REG_DATA_TVOC 0x22 +#define ENS160_REG_DATA_ECO2 0x24 +#define ENS160_REG_DATA_BL 0x28 +#define ENS160_REG_DATA_T 0x30 +#define ENS160_REG_DATA_RH 0x32 +#define ENS160_REG_DATA_MISR 0x38 +#define ENS160_REG_GPR_WRITE_0 0x40 +#define ENS160_REG_GPR_WRITE_1 ENS160_REG_GPR_WRITE_0 + 1 +#define ENS160_REG_GPR_WRITE_2 ENS160_REG_GPR_WRITE_0 + 2 +#define ENS160_REG_GPR_WRITE_3 ENS160_REG_GPR_WRITE_0 + 3 +#define ENS160_REG_GPR_WRITE_4 ENS160_REG_GPR_WRITE_0 + 4 +#define ENS160_REG_GPR_WRITE_5 ENS160_REG_GPR_WRITE_0 + 5 +#define ENS160_REG_GPR_WRITE_6 ENS160_REG_GPR_WRITE_0 + 6 +#define ENS160_REG_GPR_WRITE_7 ENS160_REG_GPR_WRITE_0 + 7 +#define ENS160_REG_GPR_READ_0 0x48 +#define ENS160_REG_GPR_READ_4 ENS160_REG_GPR_READ_0 + 4 +#define ENS160_REG_GPR_READ_6 ENS160_REG_GPR_READ_0 + 6 +#define ENS160_REG_GPR_READ_7 ENS160_REG_GPR_READ_0 + 7 + +//ENS160 data register fields +#define ENS160_COMMAND_NOP 0x00 +#define ENS160_COMMAND_CLRGPR 0xCC +#define ENS160_COMMAND_GET_APPVER 0x0E +#define ENS160_COMMAND_SETTH 0x02 +#define ENS160_COMMAND_SETSEQ 0xC2 + +#define ENS160_OPMODE_RESET 0xF0 +#define ENS160_OPMODE_DEP_SLEEP 0x00 +#define ENS160_OPMODE_IDLE 0x01 +#define ENS160_OPMODE_STD 0x02 +#define ENS160_OPMODE_LP 0x03 +#define ENS160_OPMODE_CUSTOM 0xC0 + +#define ENS160_BL_CMD_START 0x02 +#define ENS160_BL_CMD_ERASE_APP 0x04 +#define ENS160_BL_CMD_ERASE_BLINE 0x06 +#define ENS160_BL_CMD_WRITE 0x08 +#define ENS160_BL_CMD_VERIFY 0x0A +#define ENS160_BL_CMD_GET_BLVER 0x0C +#define ENS160_BL_CMD_GET_APPVER 0x0E +#define ENS160_BL_CMD_EXITBL 0x12 + +#define ENS160_SEQ_ACK_NOTCOMPLETE 0x80 +#define ENS160_SEQ_ACK_COMPLETE 0xC0 + +#define IS_ENS160_SEQ_ACK_NOT_COMPLETE(x) (ENS160_SEQ_ACK_NOTCOMPLETE == (ENS160_SEQ_ACK_NOTCOMPLETE & (x))) +#define IS_ENS160_SEQ_ACK_COMPLETE(x) (ENS160_SEQ_ACK_COMPLETE == (ENS160_SEQ_ACK_COMPLETE & (x))) + +#define ENS160_DATA_STATUS_NEWDAT 0x02 +#define ENS160_DATA_STATUS_NEWGPR 0x01 + +#define IS_NEWDAT(x) (ENS160_DATA_STATUS_NEWDAT == (ENS160_DATA_STATUS_NEWDAT & (x))) +#define IS_NEWGPR(x) (ENS160_DATA_STATUS_NEWGPR == (ENS160_DATA_STATUS_NEWGPR & (x))) +#define IS_NEW_DATA_AVAILABLE(x) (0 != ((ENS160_DATA_STATUS_NEWDAT | ENS160_DATA_STATUS_NEWGPR ) & (x))) + +#define CONVERT_RS_RAW2OHMS_I(x) (1 << ((x) >> 11)) +#define CONVERT_RS_RAW2OHMS_F(x) (pow (2, (float)(x) / 2048)) + +typedef struct { + uint8_t aqi; + uint16_t tvoc; + uint16_t eco2; +} ENS160_DATA; + +class ScioSense_ENS160 { + + public: + ScioSense_ENS160(); + + void setI2C(uint8_t sda, uint8_t scl); // Function to redefine I2C pins + + bool begin(); // Init I2C communication, resets ENS160 and checks its PART_ID. Returns false on I2C problems or wrong PART_ID. + uint8_t revENS16x() { return this->_revENS16x; } // Report version of sensor (0: ENS160, 1: ENS161) + bool setMode(uint8_t mode); // Set operation mode of sensor + + bool initCustomMode(uint16_t stepNum); // Initialize definition of custom mode with steps + bool addCustomStep(uint16_t time, bool measureHP0, bool measureHP1, bool measureHP2, bool measureHP3, uint16_t tempHP0, uint16_t tempHP1, uint16_t tempHP2, uint16_t tempHP3); + // Add a step to custom measurement profile with definition of duration, enabled data acquisition and temperature for each hotplate + + bool readData (ENS160_DATA *data); + bool readStatus(uint8_t *status); + bool measure(bool waitForNew); // Perform measurement and stores result in internal variables + bool measureRaw(bool waitForNew); // Perform raw measurement and stores result in internal variables + bool set_envdata(float t, float h); // Writes t (degC) and h (%rh) to ENV_DATA. Returns "0" if I2C transmission is successful + bool set_envdata210(uint16_t t, uint16_t h); // Writes t and h (in ENS210 format) to ENV_DATA. Returns "0" if I2C transmission is successful + uint8_t getMajorRev() { return this->_fw_ver_major; } // Get major revision number of used firmware + uint8_t getMinorRev() { return this->_fw_ver_minor; } // Get minor revision number of used firmware + uint8_t getBuild() { return this->_fw_ver_build; } // Get build revision number of used firmware + + uint8_t getAQI() { return this->_data_aqi; } // Get AQI value of last measurement + uint16_t getTVOC() { return this->_data_tvoc; } // Get TVOC value of last measurement + uint16_t geteCO2() { return this->_data_eco2; } // Get eCO2 value of last measurement + uint16_t getAQI500() { return this->_data_aqi500; } // Get AQI500 value of last measurement + uint32_t getHP0() { return this->_hp0_rs; } // Get resistance of HP0 of last measurement + uint32_t getHP1() { return this->_hp1_rs; } // Get resistance of HP1 of last measurement + uint32_t getHP2() { return this->_hp2_rs; } // Get resistance of HP2 of last measurement + uint32_t getHP3() { return this->_hp3_rs; } // Get resistance of HP3 of last measurement + uint32_t getHP0BL() { return this->_hp0_bl; } // Get baseline resistance of HP0 of last measurement + uint32_t getHP1BL() { return this->_hp1_bl; } // Get baseline resistance of HP1 of last measurement + uint32_t getHP2BL() { return this->_hp2_bl; } // Get baseline resistance of HP2 of last measurement + uint32_t getHP3BL() { return this->_hp3_bl; } // Get baseline resistance of HP3 of last measurement + uint8_t getMISR() { return this->_misr; } // Return status code of sensor + + private: + I2cMaster i2cDevice; + bool reset(); // Sends a reset to the ENS160. Returns false on I2C problems. + bool checkPartID(); // Reads the part ID and confirms valid sensor + bool clearCommand(); // Initialize idle mode and confirms + bool getFirmware(); // Read firmware revisions + + uint8_t _revENS16x; // ENS160 or ENS161 connected? (FW >7) + + uint8_t _fw_ver_major; + uint8_t _fw_ver_minor; + uint8_t _fw_ver_build; + + uint16_t _stepCount; // Counter for custom sequence + + uint8_t _data_aqi; + uint16_t _data_tvoc; + uint16_t _data_eco2; + uint16_t _data_aqi500; + uint32_t _hp0_rs; + uint32_t _hp0_bl; + uint32_t _hp1_rs; + uint32_t _hp1_bl; + uint32_t _hp2_rs; + uint32_t _hp2_bl; + uint32_t _hp3_rs; + uint32_t _hp3_bl; + uint16_t _temp; + int _slaveaddr; // Slave address of the ENS160 + uint8_t _misr; + + uint8_t _seq_steps[1][8]; + + bool write8(uint8_t reg, uint8_t value); + bool read8 (uint8_t reg, uint8_t *value); + bool read16 (uint8_t reg, uint16_t *value); + bool read16LE (uint8_t reg, uint16_t *value); + bool readBytes (uint8_t reg, uint8_t *bytes, uint8_t len); +}; + + +#endif diff --git a/software/test-software/src/adafruit/sensor.h b/software/test-software/src/adafruit/sensor.h new file mode 100644 index 0000000..ac7e454 --- /dev/null +++ b/software/test-software/src/adafruit/sensor.h @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software< /span> + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Update by K. Townsend (Adafruit Industries) for lighter typedefs, and + * extended sensor support to include color, voltage and current */ + +#ifndef _ADAFRUIT_SENSOR_H +#define _ADAFRUIT_SENSOR_H + +#ifndef ARDUINO +#include +#elif ARDUINO >= 100 +#include "Arduino.h" +#include "Print.h" +#else +#include "WProgram.h" +#endif + +/* Constants */ +#define SENSORS_GRAVITY_EARTH (9.80665F) /**< Earth's gravity in m/s^2 */ +#define SENSORS_GRAVITY_MOON (1.6F) /**< The moon's gravity in m/s^2 */ +#define SENSORS_GRAVITY_SUN (275.0F) /**< The sun's gravity in m/s^2 */ +#define SENSORS_GRAVITY_STANDARD (SENSORS_GRAVITY_EARTH) +#define SENSORS_MAGFIELD_EARTH_MAX \ + (60.0F) /**< Maximum magnetic field on Earth's surface */ +#define SENSORS_MAGFIELD_EARTH_MIN \ + (30.0F) /**< Minimum magnetic field on Earth's surface */ +#define SENSORS_PRESSURE_SEALEVELHPA \ + (1013.25F) /**< Average sea level pressure is 1013.25 hPa */ +#define SENSORS_DPS_TO_RADS \ + (0.017453293F) /**< Degrees/s to rad/s multiplier \ + */ +#define SENSORS_RADS_TO_DPS \ + (57.29577793F) /**< Rad/s to degrees/s multiplier */ +#define SENSORS_GAUSS_TO_MICROTESLA \ + (100) /**< Gauss to micro-Tesla multiplier */ + +/** Sensor types */ +typedef enum { + SENSOR_TYPE_ACCELEROMETER = (1), /**< Gravity + linear acceleration */ + SENSOR_TYPE_MAGNETIC_FIELD = (2), + SENSOR_TYPE_ORIENTATION = (3), + SENSOR_TYPE_GYROSCOPE = (4), + SENSOR_TYPE_LIGHT = (5), + SENSOR_TYPE_PRESSURE = (6), + SENSOR_TYPE_PROXIMITY = (8), + SENSOR_TYPE_GRAVITY = (9), + SENSOR_TYPE_LINEAR_ACCELERATION = + (10), /**< Acceleration not including gravity */ + SENSOR_TYPE_ROTATION_VECTOR = (11), + SENSOR_TYPE_RELATIVE_HUMIDITY = (12), + SENSOR_TYPE_AMBIENT_TEMPERATURE = (13), + SENSOR_TYPE_OBJECT_TEMPERATURE = (14), + SENSOR_TYPE_VOLTAGE = (15), + SENSOR_TYPE_CURRENT = (16), + SENSOR_TYPE_COLOR = (17), + SENSOR_TYPE_TVOC = (18), + SENSOR_TYPE_VOC_INDEX = (19), + SENSOR_TYPE_NOX_INDEX = (20), + SENSOR_TYPE_CO2 = (21), + SENSOR_TYPE_ECO2 = (22), + SENSOR_TYPE_PM10_STD = (23), + SENSOR_TYPE_PM25_STD = (24), + SENSOR_TYPE_PM100_STD = (25), + SENSOR_TYPE_PM10_ENV = (26), + SENSOR_TYPE_PM25_ENV = (27), + SENSOR_TYPE_PM100_ENV = (28), + SENSOR_TYPE_GAS_RESISTANCE = (29), + SENSOR_TYPE_UNITLESS_PERCENT = (30), + SENSOR_TYPE_ALTITUDE = (31) +} sensors_type_t; + +/** struct sensors_vec_s is used to return a vector in a common format. */ +typedef struct { + union { + float v[3]; ///< 3D vector elements + struct { + float x; ///< X component of vector + float y; ///< Y component of vector + float z; ///< Z component of vector + }; ///< Struct for holding XYZ component + /* Orientation sensors */ + struct { + float roll; /**< Rotation around the longitudinal axis (the plane body, 'X + axis'). Roll is positive and increasing when moving + downward. -90 degrees <= roll <= 90 degrees */ + float pitch; /**< Rotation around the lateral axis (the wing span, 'Y + axis'). Pitch is positive and increasing when moving + upwards. -180 degrees <= pitch <= 180 degrees) */ + float heading; /**< Angle between the longitudinal axis (the plane body) + and magnetic north, measured clockwise when viewing from + the top of the device. 0-359 degrees */ + }; ///< Struct for holding roll/pitch/heading + }; ///< Union that can hold 3D vector array, XYZ components or + ///< roll/pitch/heading + int8_t status; ///< Status byte + uint8_t reserved[3]; ///< Reserved +} sensors_vec_t; + +/** struct sensors_color_s is used to return color data in a common format. */ +typedef struct { + union { + float c[3]; ///< Raw 3-element data + /* RGB color space */ + struct { + float r; /**< Red component */ + float g; /**< Green component */ + float b; /**< Blue component */ + }; ///< RGB data in floating point notation + }; ///< Union of various ways to describe RGB colorspace + uint32_t rgba; /**< 24-bit RGBA value */ +} sensors_color_t; + +/* Sensor event (36 bytes) */ +/** struct sensor_event_s is used to provide a single sensor event in a common + * format. */ +typedef struct { + int32_t version; /**< must be sizeof(struct sensors_event_t) */ + int32_t sensor_id; /**< unique sensor identifier */ + int32_t type; /**< sensor type */ + int32_t reserved0; /**< reserved */ + int32_t timestamp; /**< time is in milliseconds */ + union { + float data[4]; ///< Raw data */ + sensors_vec_t acceleration; /**< acceleration values are in meter per second + per second (m/s^2) */ + sensors_vec_t + magnetic; /**< magnetic vector values are in micro-Tesla (uT) */ + sensors_vec_t orientation; /**< orientation values are in degrees */ + sensors_vec_t gyro; /**< gyroscope values are in rad/s */ + float temperature; /**< temperature is in degrees centigrade (Celsius) */ + float distance; /**< distance in centimeters */ + float light; /**< light in SI lux units */ + float pressure; /**< pressure in hectopascal (hPa) */ + float relative_humidity; /**< relative humidity in percent */ + float current; /**< current in milliamps (mA) */ + float voltage; /**< voltage in volts (V) */ + float tvoc; /**< Total Volatile Organic Compounds, in ppb */ + float voc_index; /**< VOC (Volatile Organic Compound) index where 100 is + normal (unitless) */ + float nox_index; /**< NOx (Nitrogen Oxides) index where 100 is normal + (unitless) */ + float CO2; /**< Measured CO2 in parts per million (ppm) */ + float eCO2; /**< equivalent/estimated CO2 in parts per million (ppm + estimated from some other measurement) */ + float pm10_std; /**< Standard Particulate Matter <=1.0 in parts per million + (ppm) */ + float pm25_std; /**< Standard Particulate Matter <=2.5 in parts per million + (ppm) */ + float pm100_std; /**< Standard Particulate Matter <=10.0 in parts per + million (ppm) */ + float pm10_env; /**< Environmental Particulate Matter <=1.0 in parts per + million (ppm) */ + float pm25_env; /**< Environmental Particulate Matter <=2.5 in parts per + million (ppm) */ + float pm100_env; /**< Environmental Particulate Matter <=10.0 in parts per + million (ppm) */ + float gas_resistance; /**< Proportional to the amount of VOC particles in + the air (Ohms) */ + float unitless_percent; /** +#include +#include + +#include "i2cmaster.hpp" + +I2cMaster::I2cMaster () { + address = 0; + timer = 0; +} + +void I2cMaster::tick1ms () { + if (timer > 0) { + timer--; + } +} + +bool I2cMaster::begin (uint8_t addr) { + this->address = addr; + // TWBR = 13; // 100kHz (TWPS1:0 = 00), TWBR = (F_CPU - 16 * 100000) / (2 * 100000 * 4); + TWBR = 100; // 50kHz (TWPS1:0 = 00), TWBR = (F_CPU - 16 * 50000) / (2 * 50000 * 4); + TWCR = (1 << TWEN); + return true; +} + +void I2cMaster::end () { + TWCR = (1 << TWEN); + TWBR = 0; +} + +bool I2cMaster::read (uint8_t *buffer, uint8_t len) { + if (start(true)) { + if (readBytes(buffer, len)) { + if (stop()) { + return true; + } + } + } + return false; +} + + +bool I2cMaster::write (const uint8_t *buffer, uint8_t len) { + if (start(false)) { + if (writeBytes(buffer, len, false)) { + if (stop()) { + return true; + } + } + } + return false; +} + +bool I2cMaster::write_P (const uint8_t *buffer, uint8_t len) { + if (start(false)) { + if (writeBytes(buffer, len, true)) { + if (stop()) { + return true; + } + } + } + return false; +} + + +bool I2cMaster::writeByteAndBuffer (uint8_t byte, const uint8_t *buffer, uint8_t len) { + if (start(false)) { + do { + TWDR = byte; + TWCR = (1 << TWINT) | (1 << TWEN); // send byte + while (!(TWCR & (1 << TWINT))) {}; // wait until last action done + if ((TWSR & 0xf8) != 0x28) { + return false; + } + byte = *buffer++; + } while (len-- > 0); + return true; + } + return false; +} + + +bool I2cMaster::write_then_read (const uint8_t *write_buffer, uint8_t write_len, uint8_t *read_buffer, uint8_t read_len) { + if (start(false)) { + if (writeBytes(write_buffer, write_len, false)) { + if (start(true)) { + if (readBytes(read_buffer, read_len)) { + if (stop()) { + return true; + } + } + } + } + } + return false; +} + +// ------------------------------------------------------------- + +bool I2cMaster::readBytes (uint8_t *buffer, uint8_t len) { + while (len-- > 0) { + if (len > 0) { + TWCR = (1 << TWEA) | (1 << TWINT) | (1 << TWEN); // read data byte with ACK enabled + } else { + TWCR = (1 << TWINT) | (1 << TWEN); // read data byte with ACK disabled + } + while (!(TWCR & (1 << TWINT))) {}; // wait until last action done + uint8_t sr = TWSR & 0xf8; + if ((len > 0 && sr != 0x50) || (len == 0 && sr != 0x58)) { + return false; + } + *buffer++ = TWDR; + } + return true; +} + +bool I2cMaster::writeBytes (const uint8_t *buffer, uint8_t len, bool fromFlash) { + while (len-- > 0) { + // printf_P(PSTR("[wB:len=%d, byte=%02x]"), len + 1, *buffer); + TWDR = fromFlash ? pgm_read_byte(buffer++) : *buffer++; + TWCR = (1 << TWINT) | (1 << TWEN); // send data byte + timer = 5; + while (timer > 0 && !(TWCR & (1 << TWINT))) {}; // wait until last action done + if (!timer || (TWSR & 0xf8) != 0x28) { + return false; + } + } + return true; +} + +bool I2cMaster::start (bool read) { + TWCR = (1 << TWINT) | (1 << TWSTA) | (1 << TWEN); // send START condition + timer = 5; + while (timer > 0 && !(TWCR & (1 << TWINT))) {}; // wait until last action done + uint8_t sr = TWSR & 0xf8; + if (!timer || (sr != 0x08 && sr != 0x10)) { + return false; + } + TWDR = (address << 1) | (read ? 1 : 0); // address + R/nW + TWCR = (1 << TWINT) | (1 << TWEN); // send address/RW + timer = 5; + while (timer > 0 && !(TWCR & (1 << TWINT))) {}; // wait until last action done + sr = TWSR & 0xf8; + if (!timer || (!read && sr != 0x18) || (read && sr != 0x40)) { + return false; + } + return true; +} + +bool I2cMaster::stop () { + TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWSTO); + while (TWCR & ( 1 << TWSTO)); + return true; +} + diff --git a/software/test-software/src/i2cmaster.hpp b/software/test-software/src/i2cmaster.hpp new file mode 100644 index 0000000..89b1e46 --- /dev/null +++ b/software/test-software/src/i2cmaster.hpp @@ -0,0 +1,30 @@ +#ifndef I2C_MASTER +#define I2C_MASTER + +#include + +class I2cMaster { + public: + static void end (); + + public: + I2cMaster (); + void tick1ms (); + bool begin (uint8_t addr); + bool read (uint8_t *buffer, uint8_t len); + bool write (const uint8_t *buffer, uint8_t len); + bool write_P (const uint8_t *buffer, uint8_t len); + bool write_then_read (const uint8_t *write_buffer, uint8_t write_len, uint8_t *read_buffer, uint8_t read_len); + bool writeByteAndBuffer (uint8_t byte, const uint8_t *buffer, uint8_t len); + + private: + uint8_t address; + uint8_t timer; + bool start (bool read); + bool stop (); + bool writeBytes (const uint8_t *buffer, uint8_t len, bool fromFlash); + bool writeBytes_P (const uint8_t *buffer); + bool readBytes (uint8_t *buffer, uint8_t len); +}; + +#endif \ No newline at end of file diff --git a/software/test-software/src/i2cslave.cpp b/software/test-software/src/i2cslave.cpp new file mode 100644 index 0000000..2bf6ac2 --- /dev/null +++ b/software/test-software/src/i2cslave.cpp @@ -0,0 +1,92 @@ +#include +#include +#include + +#include "i2cslave.hpp" + +I2cSlave::I2cSlave () { + timer = 0; + fromMaster.rIndex = 0; + fromMaster.wIndex = 0; + toMaster.rIndex = 0; + toMaster.wIndex = 0; +} + +void I2cSlave::tick1ms () { + if (timer > 0) { + timer--; + } +} + +bool I2cSlave::begin (uint8_t addr, bool acceptGeneralCalls) { + if (addr > 127) { + return false; + } + TWAR = addr << 1 | (acceptGeneralCalls ? 1 : 0); + TWBR = 100; // 50kHz (TWPS1:0 = 00), TWBR = (F_CPU - 16 * 50000) / (2 * 50000 * 4); + TWCR = (1 << TWEA) | (1 << TWEN) | (1 << TWIE); + return true; +} + +void I2cSlave::end () { + TWCR = (1 << TWEN); + TWBR = 0; +} + +int I2cSlave::read () { + return getByte(fromMaster); +} + +void I2cSlave::write (uint8_t byte) { + putByte(toMaster, byte); +} + + +void I2cSlave::putByte (RingBuffer& buffer, uint8_t byte) { + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { + buffer.data[buffer.wIndex++] = byte; + if (buffer.wIndex >= sizeof(buffer.data)) { + buffer.wIndex = 0; + } + if (buffer.wIndex == buffer.rIndex) { + buffer.rIndex++; + if (buffer.rIndex >= sizeof(buffer.data)) { + buffer.rIndex = 0; + } + } + } +} + +int I2cSlave::getByte (RingBuffer& buffer) { + uint8_t b; + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { + if (buffer.rIndex == buffer.wIndex) { + return EOF; + } + b = buffer.data[buffer.rIndex++]; + if (buffer.rIndex >= sizeof(buffer.data)) { + buffer.rIndex = 0; + } + } + return b; +} + +void I2cSlave::handleTWIIsr () { + uint8_t sr = TWSR & 0xf8; + switch (sr) { + case 0x80: { // Previously addressed with own SLA+W; data has been received; ACK has been returned + putByte(fromMaster, TWDR); + TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWIE); // no TWEA -> only one byte accepted + break; + } + case 0xa8: { // Own SLA+R has been received; ACK has been returned + int response = getByte(toMaster);; + TWDR = response < 0 ? 0x00 : (uint8_t)response; + TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWIE); // no TWEA -> only one byte accepted + break; + } + default: TWCR = (1 << TWINT) | (1 << TWEA) | (1 << TWEN) | (1 << TWIE); break; + } + +} + diff --git a/software/test-software/src/i2cslave.hpp b/software/test-software/src/i2cslave.hpp new file mode 100644 index 0000000..2fe2dc7 --- /dev/null +++ b/software/test-software/src/i2cslave.hpp @@ -0,0 +1,32 @@ +#ifndef I2C_SLAVE +#define I2C_SLAVE + +#include + +class I2cSlave { + private: + typedef struct { + uint8_t rIndex; + uint8_t wIndex; + uint8_t data[8]; + } RingBuffer; + + public: + I2cSlave (); + void tick1ms (); + bool begin (uint8_t addr, bool acceptGeneralCalls); + void end (); + void handleTWIIsr (); + int read (); + void write (uint8_t byte); + + private: + uint8_t timer; + RingBuffer fromMaster; + RingBuffer toMaster; + void putByte (RingBuffer& buffer, uint8_t byte); + int getByte (RingBuffer& buffer); + +}; + +#endif \ No newline at end of file diff --git a/software/test-software/src/main.cpp b/software/test-software/src/main.cpp new file mode 100644 index 0000000..35f309f --- /dev/null +++ b/software/test-software/src/main.cpp @@ -0,0 +1,412 @@ +#include +#include +#include +#include + +#include +#include + +#include "main.hpp" +#include "units/encoder.hpp" +#include "units/i2c.hpp" +#include "units/led.hpp" +#include "units/ieee485.hpp" +#include "units/led.hpp" +#include "units/lcd.hpp" +#include "units/switch.hpp" +#include "units/rgb.hpp" +#include "units/seg7.hpp" +#include "units/poti.hpp" +#include "units/r2r.hpp" +#include "units/motor.hpp" +#include "units/portexp.hpp" +#include "units/uart1.hpp" +#include "units/modbus.hpp" +#include "units/rtc8563.hpp" +#include "units/cc1101.hpp" + +const char MAIN_CPP_DATE[] PROGMEM = __DATE__; +const char MAIN_CPP_TIME[] PROGMEM = __TIME__; +#ifdef __AVR_ATmega328P__ + const char MAIN_CPP_PART_NAME[] PROGMEM = "ATmega328P"; +#endif +#ifdef __AVR_ATmega644P__ + const char MAIN_CPP_PART_NAME[] PROGMEM = "ATmega644P"; +#endif +#ifdef __AVR_ATmega1284P__ + const char MAIN_CPP_PART_NAME[] PROGMEM = "ATmega1284P"; +#endif + +const char PSTR_DIVIDER[] PROGMEM = "\n====================================\n "; +const char PSTR_LINEFEED[] PROGMEM = "\n"; +const char PSTR_ERROR[] PROGMEM = "ERROR"; +const char PSTR_Done[] PROGMEM = "Done"; + +uint8_t hardwareVersion = 0; + +extern "C" { + void __cxa_pure_virtual () {} + int __cxa_guard_acquire(uint8_t *g) { return 0; } + void __cxa_guard_release(uint8_t *g) {} + void __cxa_guard_abort(uint8_t *g) {} + void __gxx_personality_sj0 () {} + void __cxa_rethrow () {} + void __cxa_begin_catch () {} + void __cxa_end_catch () {} + + int uart_putchar(char c, FILE *stream) { + if (c == '\n') { + uart_putchar('\r', stream); + } + if (stream == stdout) { + loop_until_bit_is_set(UCSR0A, UDRE0); + UDR0 = c; + } + return 0; + } + + + uint64_t volatile systemMillis = 0; + uint8_t volatile uartBuffer[32]; + uint8_t volatile rIndex = 0; + uint8_t volatile wIndex = 0; + + int uart_getchar (FILE *stream) { + // if (rIndex == wIndex) { + // // nothing in buffer + // return EOF; + // } + // printf_P(PSTR(" r%d"), rIndex); + while (rIndex == wIndex) { + // wait for character + } + + // don't use "char c" because german special characters would lead to negative return -> stream error + // char c = uartBuffer[rIndex++]; + + uint8_t c = uartBuffer[rIndex++]; + // printf_P(PSTR("(%02x) "), c); + if (c == '\r') { + c = '\n'; + } + putchar(c); // echo on terminal + return c; + } + + static FILE mystdout = { 0, 0, _FDEV_SETUP_WRITE , 0, 0, uart_putchar, NULL, 0 }; + static FILE mystdin = { 0, 0, _FDEV_SETUP_READ , 0, 0, NULL, uart_getchar, 0 }; + + #if defined(__AVR_ATmega644P__) || defined(__AVR_ATmega1284P__) + static volatile uint16_t timerFlashGreenLed = 0; + static volatile uint16_t timerFlashRedLed = 0; + void flashRedLed (uint16_t ms) { + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { + if (timerFlashRedLed == 0) { + PORTC |= (1 << PC2); + if (ms < 0xff80) { + ms += 0x80; // at least 128ms OFF after + } + timerFlashRedLed = ms; + } + } + } + void flashGreenLed (uint16_t ms) { + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { + if (timerFlashGreenLed == 0) { + PORTC |= (1 << PC4); + if (ms < 0xff80) { + ms += 0x80; // at least 128ms OFF after + } + timerFlashGreenLed = ms; + } + } + } + #endif + + static volatile uint32_t timer1ms = 0; + static volatile int keyUart0 = EOF; + + Led led; + Switch sw; + Rgb rgb; + Seg7 seg7; + Poti poti; + Encoder encoder; + R2r r2r; + Motor motor; + PortExp portExp; + Lcd lcd; + Uart1 uart1; + Modbus modbus; + Ieee485 ieee485; + I2c i2cSparkfun(I2c::SparkFunEnvCombo); + I2c i2cMaster(I2c::Master); + I2c i2cSlave(I2c::Slave); + Rtc8563 rtc8563(Rtc8563::NORMAL); + Cc1101 cc1101Send(Cc1101::Send); + Cc1101 cc1101Receive(Cc1101::Receive); +} + +uint8_t detectHardwareVersion () { + ADMUX = (1 << ADLAR) | (1 << REFS0) | 7; // ADC7, VREF=AVCC=3.3V + ADCSRA = (1 << ADEN) | 7; // ADC Enable, Prescaler 128 + ADCSRA |= (1 << ADSC); // start ADC + while (ADCSRA & (1 << ADSC)) {} // wait for result + hardwareVersion = 0; // unknown board version + // printf("Hardware-Version: ADC7H = %d\n", ADCH); + #if defined(__AVR_ATmega644P__) || defined(__AVR_ATmega1284P__) + + if (ADCH >= 0xf4) { + hardwareVersion = 1; + } else if (ADCH >= 0xe0) { + hardwareVersion = 2; + } + #endif + #if defined(__AVR_ATmega328P__) + if (ADCH < 0xd8 && ADCH >= 0xc0) { + hardwareVersion = 1; + } else if (ADCH < 0xd8 && ADCH >= 0xb0) { + hardwareVersion = 2; + } + #endif + + if (hardwareVersion < 1 || hardwareVersion > 2) { + #ifdef __AVR_ATmega644P__ + printf_P(PSTR("\nInvalid Hardware-Version: ADC7H = %d (ATmega644P, 3.3V)\n"), ADCH); + #elif __AVR_ATmega1284P__ + printf_P(PSTR("\nInvalid Hardware-Version: ADC7H = %d (ATmega1284P, 3.3V)\n"), ADCH); + #elif __AVR_ATmega328P__ + printf_P(PSTR("\nInvalid Hardware-Version: ADC7H = %d (ATmega328P, 5V)\n"), ADCH); + #endif + } else { + printf_P(PSTR("\n\nHardware %d detected (ADC7H=0x%02X)"), hardwareVersion, ADCH); + } + + ADMUX = 0; + ADCSRA = 0; + return hardwareVersion; +} + +void setTimer (uint32_t ms) { + ATOMIC_BLOCK(ATOMIC_FORCEON) { + timer1ms = ms; + } +} + +int wait (uint32_t ms) { + setTimer(ms); + do { + ATOMIC_BLOCK(ATOMIC_FORCEON) { + ms = timer1ms; + } + } while (ms > 0 && keyUart0 == EOF); + return keyUart0; +} + +int waitAndReadKey (uint32_t ms) { + keyUart0 = EOF; + int key = wait(ms); + keyUart0 = EOF; + return key; +} + +int main () { + + #if defined(__AVR_ATmega644P__) || defined(__AVR_ATmega1284P__) + // Nano-644 LEDs (Green, Orange, Red) + DDRC |= (1 << PC7) | (1 << PC4) | (1 << PC3) | (1 << PC2); + PORTC &= ~((1 << PC7) | (1 << PC4) | (1 << PC3) | (1 << PC2)); + + // Nano-644 push button SW2 + DDRC &= ~(1 << PC5); + PORTC |= (1 << PC5); // enable internal pullup resistor + #endif + + #ifdef __AVR_ATmega328P__ + DDRB |= (1 << PB5); + PORTB &= ~(1 << PB5); + #endif + + // UART0 interface on Nano-644 + UCSR0A = (1 << U2X0); + UCSR0B = (1 << RXCIE0) | (1 << RXEN0) | (1 <= 1 && hardwareVersion <= 2) { + printf_P(PSTR("Hardware: %d"), hardwareVersion); + } else { + printf_P(PSTR("ERROR: Invalid Hardware (%d)"), hardwareVersion); + for(;;); + } + printf_P(PSTR_DIVIDER); + printf_P(PSTR_LINEFEED); + printf_P(PSTR("Available units:\n\n")); + for (i = 0; i < sizeof(unit) / sizeof(unit[0]); i++) { + TestUnit *pu = unit[i]; + printf_P(PSTR("%3x ... "), i); + printf_P(pu->getName()); + printf_P(PSTR_LINEFEED); + } + printf_P(PSTR("\nSelect unit: ")); + rIndex = 0; wIndex = 0; + fgets(s, sizeof(s), stdin); + } while (sscanf(s, "%x", &i) != 1 || i < 0 || i >= sizeof(unit) / sizeof(unit[0]) ); + + TestUnit *pu = unit[i]; + printf_P(PSTR("\n\n[")); printf_P(pu->getName()); printf_P(PSTR("]: ")); + keyUart0 = EOF; + + pu->init(); + for (uint8_t subtest = 0; subtest < 0xff; subtest++) { + printf_P(PSTR("\n%4d: "), subtest); + if (pu->run(subtest) < 0) { + break; + } + if (keyUart0 == 27) { + keyUart0 = EOF; + break; + } + keyUart0 = EOF; + } + pu->cleanup(); + } +} + +uint64_t millis () { + volatile uint64_t millis = systemMillis; + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { + millis = systemMillis; + } + return millis; +} + +#ifndef USART0_RX_vect + #define USART0_RX_vect USART_RX_vect +#endif +ISR (USART0_RX_vect) { + uint8_t b = UDR0; + keyUart0 = b; + uartBuffer[wIndex++] = b; + // printf_P(PSTR(" w%d(%02x)"), wIndex, b); + if (wIndex == rIndex) { + // buffer overflow, kick out oldest byte + rIndex++; + } +} + +#ifdef USART1_RX_vect + ISR (USART1_RX_vect) { + uint8_t b = UDR1; + if (modbus.enabled) { + modbus.handleRxByte(b); + } + if (uart1.enabled) { + uart1.handleRxByte(b); + } + if (ieee485.enabled) { + ieee485.handleRxByte(b); + } + } +#endif + +ISR (TWI_vect) { + if (i2cMaster.enabled) { + i2cMaster.handleTwiIrq(); + } else if (i2cSlave.enabled) { + i2cSlave.handleTwiIrq(); + } else if (i2cSparkfun.enabled) { + i2cSparkfun.handleTwiIrq(); + } else { + TWCR = (1 << TWINT); // clear interrupt request bit and disable TWI + } +} + +ISR (TIMER2_COMPA_vect) { // every 100us + static uint16_t timer500ms = 0; + static uint8_t timer100us = 0; + + if (encoder.enabled) { + encoder.tick100us(); + } + if (motor.enabled) { + motor.tick100us(); + } + + timer100us++; + if (timer100us >= 10) { + timer100us = 0; + if (timer1ms > 0) { + timer1ms--; + } + systemMillis++; + i2cMaster.tick1ms(); + i2cSlave.tick1ms(); + i2cSparkfun.tick1ms(); + cc1101Send.tick1ms(); + cc1101Receive.tick1ms(); + #if defined(__AVR_ATmega644P__) || defined(__AVR_ATmega1284P__) + if (timerFlashRedLed > 0) { + if (--timerFlashRedLed < 128) { + PORTC &= ~(1 << PC2); // red LED + } + } + if (timerFlashGreenLed > 0) { + if (--timerFlashGreenLed < 128 ) { + PORTC &= ~(1 << PC4); // green LED + } + } + #endif + } + + timer500ms++; + if (timer500ms >= 5000) { + #if defined(__AVR_ATmega644P__) || defined(__AVR_ATmega1284P__) + PORTC ^= (1 << PC3); // orange LED blinking + #endif + #ifdef __AVR_ATmega328P__ + if (!seg7.enabled) { + PORTB ^= (1 << PB5); // LED L + } + #endif + timer500ms = 0; + } +} + diff --git a/software/test-software/src/main.hpp b/software/test-software/src/main.hpp new file mode 100644 index 0000000..7eeff16 --- /dev/null +++ b/software/test-software/src/main.hpp @@ -0,0 +1,38 @@ +#ifndef MAIN_HPP +#define MAIN_HPP + +#include +#include + +#define ENTER '\r' +#define CTRLC '\003' +#define ESCAPE 0x1b + +extern int wait (uint32_t ms); +extern int waitAndReadKey (uint32_t ms); +extern uint64_t millis (); + +extern uint8_t hardwareVersion; + +extern const char PSTR_DIVIDER[]; +extern const char PSTR_LINEFEED[]; +extern const char PSTR_ERROR[]; +extern const char PSTR_Done[]; + +#if defined(__AVR_ATmega644P__) || defined(__AVR_ATmega1284P__) + extern "C" { + extern void flashRedLed (uint16_t ms); + extern void flashGreenLed (uint16_t ms); + } +#endif + + +class TestUnit { + public: + virtual int8_t run (uint8_t subtest) = 0; + virtual void init () = 0; + virtual void cleanup () = 0; + virtual PGM_P getName () = 0; +}; + +#endif \ No newline at end of file diff --git a/software/test-software/src/units/cc1101.cpp b/software/test-software/src/units/cc1101.cpp new file mode 100644 index 0000000..aebe713 --- /dev/null +++ b/software/test-software/src/units/cc1101.cpp @@ -0,0 +1,753 @@ +#include +#include +#include +#include + +#include "cc1101.hpp" +#include "../main.hpp" + +// 868MHz Modem E07-900MM1DS (chipset CC1101) +// https://jlcpcb.com/partdetail/Chengdu_Ebyte_ElecTech-E07900MM10S/C5844212 + +#ifdef __AVR_ATmega328P__ + + // Arduino-Nano-5V + // ------------------------------------ + // not available + + void Cc1101::init () {} + void Cc1101::cleanup () {} + void Cc1101::setChipEnableLow () {} + void Cc1101::setChipEnableHigh () {} + int8_t Cc1101::run (uint8_t subtest) { return -1; } + PGM_P Cc1101::getName () { return PSTR("?"); } + +#endif + + +#if defined(__AVR_ATmega644P__) || defined(__AVR_ATmega1284P__) + + // Nano-644 + // -------------------------------------------------------- + // PA4 ... nCS + // PB4 ... nSS (not used, but must be high, otherwise AVR SPI master not working) + // PB5 ... MOSI + // PB6 ... MISO + // PB7 ... SCK + + // -------------------------------------------------------- + + // Output power table from CC1101 datasheet page 59 + const Cc1101::PATABLE_INIT_ITEM_t PMEM_CC1101_PATABLE_INIT[] PROGMEM = { + { 0xc6, "9.6" }, // 9.6 dBm / 29.4mA (default value) + { 0xc0, "11" }, // 11 dBm / 34.2mA + { 0xc5, "10" }, // 10 dBm / 30.0mA + { 0xcd, "7" }, // 7 dBm / 25.8mA + { 0x86, "5" }, // 5 dBm / 19.9mA + { 0x50, "0" }, // 0 dBm / 16.8mA + { 0x37, "-6" }, // -6 dBm / 16.4mA + { 0x26, "-10" }, // 10 dBm / 14.5mA + { 0x1d, "-15" }, // 15 dBm / 13.3mA + { 0x17, "-20" }, // 20 dBm / 12.6mA + { 0x03, "-30" }, // 30 dBm / 12.0mA + }; + const Cc1101::Register_t PMEM_CC1101_REGISTER_INIT PROGMEM = { + /* 0x00: iocfg2 0x0b (----) */ { Cc1101::CHIP_NOT_READY, 0, 0 }, // GDO2 + /* 0x01: iocfg1 0x2e (----) */ { Cc1101::HIGH_IMPEDANCE_WHEN_CHIP_SELECT_HIGH, 0, 0 }, // GDO1/MISO + /* 0x02: iocfg0 0x06 (----) */ { Cc1101::SYNC_SENT_OR_RECEIVED_UNTIL_END_OF_PACKET, 0, 0 }, // GDO0 -> PA5 + /* 0x03: fifothr 0x47 (0x07) */ { 7, 0, 1, 0 }, // fifo_thr = 0b111, close_in_rx=0, adc_retention = 1, (bit7=0) + /* 0x04: sync1 0x7a (0xd3) */ 0x7a, + /* 0x05: sync0 0x0e (0x91) */ 0x0e, + /* 0x06: pktlen 0x14 (0x3d) */ PACKET_LENGTH, + /* 0x07: pktctrl1 0x04 (----) */ { Cc1101::NO_ADDR_CHECK, 1, 0, 0, 0 }, // adr_chk=0b00, append_status=1, crc_autoflush=0, (bit4=0), pqt=0b000 + /* 0x08: pktctrl0 0x05 (----) */ { Cc1101::FIXED, 1, 0, Cc1101::NORMAL_USE_FIFO, 0, 0 }, // length_config=0b01, crc_en=1, (bit3=0), pkt_format=0b00, white_data=0, (bit7=0) + /* 0x09: addr 0x00 (----) */ 0x00, + /* 0x0a: channr 0x00 (----) */ 0x00, + /* 0x0b: fsctrl1 0x06 (0x08) */ { 6, 0, 0 }, // frqu_if=6, (bit5=0), (bit76=0b00) + /* 0x0c: fsctrl0 0x00 (----) */ { 0 }, // freqoff = 0 + /* 0x0d: frequ2 0x21 (----) */ 0x21, + /* 0x0e: frequ1 0x62 (----) */ 0x62, + /* 0x0f: frequ0 0x76 (----) */ 0x76, + /* 0x10: mdmcfg4 0xca (0x5b) */ { 0x0a, 0, 3 }, // drate_e=0x0a, chanbw_m=0, chanbw_e=3 + /* 0x11: mdmcfg3 0xf8 (----) */ { 0xf8 }, // drate_m=0xf8 + /* 0x12: mdmcfg2 0x16 (0x03) */ { Cc1101::SYNC_16_16_CARRIER_SENSE, 0, Cc1101::GFSK, 0}, // sync_mode=0b110, manchester_en=0, mod_format=0b001, dem_dcfilt_off=0 + /* 0x13: mdmcfg1 0x22 (----) */ { 2, 0, Cc1101::FOUR, 0 }, // chanspc_e=0b10, bit32=0, num_preamble=0b010, fec_en=0 + /* 0x14: mdmcfg0 0xf8 (----) */ { 0xf8 }, // chanspc_m = 0x08 + /* 0x15: deviatn 0x40 (0x47) */ { 0, 0, 4, 0 }, // deviation_m=0, (bit3=0), deviation_e=4, (bit7=0) + /* 0x16: mcsm2 0x07 (----) */ { 7, 0, 0, 0 }, // rx_time=7 (NA), rx_time_qual=0, rx_time_rssi=0, (bit76=0b00) + /* 0x17: mcsm1 0x30 (----) */ { Cc1101::TXOFF_IDLE, Cc1101::RXOFF_IDLE, Cc1101::RSSI_BELOW_THRESHOLD__UNLESS_RECEIVE_PACKET, 0 }, // mcsm1 (txoff_mode=0b00, rxoff_mode=0b00, cca_mode=0b11, (bit76=0b00) ) + /* 0x18: mcsm0 0x18 (----) */ { 0, 0, 2, Cc1101::IDLE_TO_RX_OR_TX, 0 }, // xosc_force_on=0, pin_ctrl_en=0, po_timeout=2 (149-155us), fs_autocal=0b01, (bit76=0b00) + /* 0x19: foccfg 0x16 (0x1d) */ 0x16, + /* 0x1a: bscfg 0x6c (0x1c) */ 0x6c, + /* 0x1b: agcctrl2 0x43 (0xc7) */ 0x43, + /* 0x1c: agcctrl1 0x49 (0x00) */ 0x49, + /* 0x1d: agcctrl0 0x91 (0xb2) */ 0x91, + /* 0x1e: worevt1 0x87 (----) */ 0x87, + /* 0x1f: worevt0 0x6b (----) */ 0x6b, + /* 0x20: worctrl 0xfb (0xf8) */ 0xfb, + /* 0x21: frend1 0x56 (0xb6) */ 0x56, + /* 0x22: frend0 0x10 (----) */ { 0, 0, 1, 0 }, // pa_power = 0, (bit3 = 0), lodiv_buf_current = 1, (bit76=0b00) + /* 0x23: fscal3 0xe9 (0xea) */ 0xe9, + /* 0x24: fscal2 0x2a (----) */ 0x2a, + /* 0x25: fscal1 0x00 (----) */ 0x00, + /* 0x26: fscal0 0x1f (0x11) */ 0x1f, + /* 0x27: rcctrl1 0x41 (----) */ 0x41, + /* 0x28: rcctrl0 0x00 (----) */ 0x00, + /* 0x29: fstest 0x59 (----) */ 0x59, + /* 0x2a: ptest 0x7f (----) */ 0x7f, + /* 0x2b: agctest 0x3f (----) */ 0x3f, + /* 0x2c: test2 0x81 (0x88) */ 0x81, + /* 0x2d: test1 0x35 (0x31) */ 0x35, + /* 0x2e: test0 0x09 (0x0b) */ 0x09 + }; + + // -------------------------------------------------------------------------------------- + + int8_t Cc1101::runSend (uint8_t subtest) { + if (subtest == 0) { + bool ok = true; + GDOx_CFG_t cfg0 = SYNC_SENT_OR_RECEIVED_UNTIL_END_OF_PACKET; + ok &= writeRegister(0x02, cfg0); + return ok; + } else if (subtest > 1) { + return -1; + } + + printf_P(PSTR(" use + and - for power change (other key -> back to default)")); + + uint16_t cnt = 0; + uint8_t paIndex = 0; + uint8_t pa_power; + int key = EOF; + do { + // if ((cnt & 0x0f) == 0) { + // paIndex = (cnt >> 4) & 0x07; + // union { FREND0_t value; uint8_t byte; } frend0 = { paIndex, 0, 1, 0 }; + // printf_P(PSTR("\n switch power ")); + // if (writeRegister(FREND0, frend0.byte )) { + // printf_P(PSTR("to %d (FREND0 = 0x%02X)"), frend0.value.pa_power, frend0.byte); + // } else { + // printf_P(PSTR("fails")); + // } + // } + if (key != EOF) { + uint8_t max = (sizeof(PMEM_CC1101_PATABLE_INIT) / sizeof(PMEM_CC1101_PATABLE_INIT[0])) - 1; + switch (key) { + case '+': if (paIndex == 0) paIndex = 1; else if (paIndex > 1) paIndex--; break; + case '-': if (paIndex == 0) paIndex = max; else if (paIndex < max) paIndex++; break; + default: paIndex = 0; break; // default value + } + + const PATABLE_INIT_ITEM_t *values = PMEM_CC1101_PATABLE_INIT; + + memcpy_P(&pa_power, &((values[paIndex]).pa_power), 1); + PGM_P info = (values[paIndex]).dbm; + printf_P(PSTR("\n switch power to ")); printf_P(info); + printf_P(PSTR("dBm ")); + if (writeRegister(0x3e, pa_power )) { + printf_P(PSTR("(PATABLE = 0x%02X)"), pa_power); + } else { + printf_P(PSTR("fails")); + } + } + + MainRadioControlState state = UNKNOWN; + uint8_t data[PACKET_LENGTH]; + printf_P(PSTR("\n [%04x]: state="), cnt++); + if (!readStatusRegister(MARCSTATE, (uint8_t *)&state)) { + printf_P(PSTR("E1")); + } else { + printf_P(PSTR("0x%02x"), state); + } + data[0] = pa_power; + printf_P(PSTR(" --> send %d bytes (HEX: %02X"), sizeof(data), paIndex); + for (uint8_t i = 1; i < sizeof(data); i++) { + data[i] = uint8_t((cnt + i - 1) & 0xff); + printf_P(PSTR(" %02X"), data[i]); + } + printf_P(PSTR("] -> ")); + + if (!sendData(data, sizeof(data))) { + flashRedLed(100); + printf_P(PSTR("E1")); + continue; + } + flashGreenLed(100); + key = waitAndReadKey(2000); + printf_P(PSTR("OK")); + + } while (key == EOF || (key != ESCAPE)); + + return 0; + } + + int8_t Cc1101::runReceive (uint8_t subtest) { + if (subtest == 0) { + bool ok = true; + GDOx_CFG_t cfg0 = SYNC_SENT_OR_RECEIVED_UNTIL_END_OF_PACKET; + ok &= writeRegister(0x02, cfg0); + return ok; + } else if (subtest > 1) { + return -1; + } + + int key = EOF; + uint16_t cnt = 0; + MainRadioControlState state; + while (key == EOF) { + printf_P(PSTR("\n [%04x] => start ... "), cnt++); + strobe(SRX); + do { + state = UNKNOWN; + if (!readStatusRegister(MARCSTATE, (uint8_t *)&state)) { + printf_P(PSTR("E1")); + _delay_ms(500); + break; + } + if (wait(0) != EOF) { + printf_P(PSTR("cancelled")); + _delay_ms(500); + break; + } + if (state == IDLE) { + printf_P(PSTR("? (IDLE)")); + _delay_ms(500); + break; + } + + } while (state != RX && state != RXFIFO_OVERFLOW); + + if (state != RX && state != RXFIFO_OVERFLOW) { + continue; + } + printf_P(PSTR("OK, receive ... ")); + + uint8_t length; + uint8_t lastLength = 0xff; + do { + uint8_t data[PACKET_LENGTH]; + if (!readStatusRegister(MARCSTATE, (uint8_t *)&state)) { + printf_P(PSTR("E2")); + _delay_ms(500); + continue; + } + if (wait(0) != EOF) { + printf_P(PSTR("cancelled")); + _delay_ms(500); + return -1; + } + if (!readStatusRegister(RXBYTES, (uint8_t *)&length)) { + printf_P(PSTR("E2")); + _delay_ms(500); + continue; + } + if (lastLength != length) { + lastLength = length; + } + if (length >= PACKET_LENGTH) { + if (receiveData(data, &length, sizeof(data))) { + if (length > 0) { + flashGreenLed(100); + printf_P(PSTR(" -> ")); + if (status.receivePacketStatusValid) { + printf_P(PSTR(" RSSI=%d, LQI=%d, CRC "), status.receivedPacketStatus.value.rssi, status.receivedPacketStatus.value.lqi); + if (status.receivedPacketStatus.value.crcOk) { + printf_P(PSTR("OK, ")); + } else { + printf_P(PSTR("ERROR, ")); + } + } + printf_P(PSTR("%d data bytes (HEX): "), length); + for (uint8_t i = 0; i < length; i++) { + printf_P(PSTR(" %02X"), data[i]); + } + printf_P(PSTR(" ... ")); + } + } + } + + } while (state == RX || state == RXFIFO_OVERFLOW); + + printf_P(PSTR("done (state=0x%02x)"), state); + } + return -1; + } + + int8_t Cc1101::runTest (uint8_t subtest) { + if (subtest > 0) { + return -1; + } + while (wait(0) == EOF) {} + return -1; + } + + + int8_t Cc1101::run (uint8_t subtest) { + switch (mode) { + case Send: return runSend(subtest); + case Receive: return runReceive(subtest); + case Test: return runTest(subtest); + default: return -1; + } + } + + uint8_t Cc1101::sendSpiByte (uint8_t b) { + SPDR = b; + while (!(SPSR & (1< "), addr, value); + if (ok && status.receivePacketStatusValid) { + printf_P(PSTR("status=0x%02x]"), status.spiResponse.byte); + } else { + printf_P(PSTR("ERR]")); + } + } + + return ok; + } + + bool Cc1101::writeRegisters (uint8_t addr, uint8_t *buffer, uint8_t length) { + bool ok = true; + uint8_t l = length; + uint8_t *p = buffer; + + addr = (addr & 0x3f) | 0x40; // write burst + setChipEnableLow(); + ok &= waitForMisoLow(); + sendSpiByte(addr); + while (length-- > 0) { + sendSpiByte(*buffer++); + } + setChipEnableHigh(); + + if (debugPrint.print.writeRegisters) { + printf_P(PSTR("\n [writeRegisters(0x%02x): "), addr & 0x3f); + while (l-- > 0) { + printf_P(PSTR(" 0x%02x"), *p++); + } + printf_P(PSTR(" -> ")); + if (ok && status.receivePacketStatusValid) { + printf_P(PSTR("status=0x%02x]"), status.spiResponse.byte); + } else { + printf_P(PSTR("ERR]")); + } + } + + return ok; + } + + + bool Cc1101::readRegister (uint8_t addr, uint8_t *value) { + bool ok = true; + addr = (addr & 0x3f) | 0x80; + setChipEnableLow(); + ok &= waitForMisoLow(); + status.spiResponse.byte = sendSpiByte(addr); + status.spiResponseValid = 1; + *value = sendSpiByte(0); + setChipEnableHigh(); + + if (debugPrint.print.readRegister) { + printf_P(PSTR("\n [readRegister(0x%02x) -> "), addr & 0x3f); + if (ok && status.spiResponseValid) { + printf_P(PSTR("0x%02x,status=0x%02x]"), *value, status.spiResponse.byte); + } else { + printf_P(PSTR("ERR]")); + } + } + return ok; + } + + bool Cc1101::readRegisters (uint8_t addr, uint8_t *buffer, uint8_t length) { + bool ok = true; + + uint8_t l = length; + uint8_t *p = buffer; + + addr |= 0xc0; // read burst + setChipEnableLow(); + ok &= waitForMisoLow(); + sendSpiByte(addr); + while (length-- > 0) { + *buffer++ = sendSpiByte(0); + } + setChipEnableHigh(); + + if (debugPrint.print.readRegisters) { + printf_P(PSTR("\n [readRegisters(0x%02x, ..., %d)] -> "), addr & 0x3f, l); + if (ok) { + while (l-- > 0) { + printf_P(PSTR(" 0x%02x"), *p++); + } + printf_P(PSTR("]")); + } else { + printf_P(PSTR("ERR]")); + } + } + + return ok; + } + + bool Cc1101::readStatusRegister (StatusAddress_t addr, uint8_t *value) { + if (addr < 0x30 || addr > 0x3d) { + return false; + } else { + return readRegisters(addr, value, 1); + } + } + + bool Cc1101::sendData (uint8_t *buffer, uint8_t length) { + timer = 10; + uint8_t ok = true; + uint8_t txbytes; + uint8_t states[16]; uint8_t statesIndex = 0; + // ok &= writeRegister(0x3f, length); + ok &= writeRegisters(0x3f, buffer, length); + ok &= readStatusRegister(TXBYTES, &txbytes); + ok &= (txbytes == 20); + if (ok) { + ok &= strobe(STX); // start sending bytes + // ok &= waitForGD0High(); // wait for start of SYNC + if (ok) { + uint8_t lastState = 0; + uint8_t state; + do { + ok &= readStatusRegister(MARCSTATE, &state); + if (state != lastState && statesIndex < 16) { + states[statesIndex++] = state; + } + lastState = state; + } while (ok && state != 0x13 && timer > 0); + ok &= state == 0x13; // check if in state TX + } + if (ok) { + uint8_t lastTxbytes = 20; + do { + ok &= readStatusRegister(TXBYTES, &txbytes); + if (lastTxbytes != txbytes) { + lastTxbytes = txbytes; + } + } while (ok && txbytes > 0 && timer > 0); + ok &= txbytes == 0; + } + } + if (!ok) { + strobe(SFTX); // flush TXfifo + } else { + uint8_t state; + uint8_t lastState = 0; + do { + ok &= readStatusRegister(MARCSTATE, &state); + if (lastState != state) { + lastState = state; + } + } while (ok && state != 0x01 && timer > 0); + ok &= state == 0x01; // check if in state IDLE + } + // ok &= waitForGD0Low(); // wait for end of package + + if (ok) { + printf_P(PSTR(" States[")); + for (uint8_t i = 0; i < statesIndex; i++) { + printf_P(PSTR(" 0x%02X"), states[i]); + } + printf_P(PSTR("] ")); + } + + return ok; + } + + bool Cc1101::receiveData (uint8_t *buffer, uint8_t *receivedLength, uint8_t maxBufferSize) { + bool ok = true; + STATUS_RXBYTES_t status; + uint8_t lastLength = 0; + uint8_t length = 0; + *receivedLength = 0; + timer = 50; + + this->status.receivePacketStatusValid = 0; + ok &= readStatusRegister(RXBYTES, &status.byte); + if (ok && status.rxbytes.num_rxbytes > 0) { + length = status.rxbytes.num_rxbytes; + + do { + _delay_us(400); // 20 Bytes in 4ms -> 200us/Byte -> CC1101 datasheet page 56: "twice that of which RF bytes are recived" + lastLength = length; + ok &= readStatusRegister(RXBYTES, &status.byte); + if (ok) { + // printf_P(PSTR(" [rxbytes=%d] "), status.byte); + length = status.byte & 0x7f; + } + ok &= timer > 0; + } while (ok && lastLength != length); + + if (ok) { + uint8_t extraBytesCount = this->status.receivePacketStatusEnabled ? 2 : 0; + if ((PACKET_LENGTH + extraBytesCount) != length) { + printf_P(PSTR(" ERROR[receive %d bytes, expect %d bytes] "), *receivedLength, PACKET_LENGTH + extraBytesCount); + *receivedLength = 0; + ok = false; + } else { + printf_P(PSTR(" OK[receive %d bytes] "), length); + *receivedLength = PACKET_LENGTH < maxBufferSize ? length - extraBytesCount : maxBufferSize; + ok &= readRegisters(0xff, buffer, *receivedLength); + if (!ok) { + *receivedLength = 0; + } else { + length -= *receivedLength; + if (length > extraBytesCount) { + printf_P(PSTR(" [WARN: buffer to small] ")); + while (length > extraBytesCount) { + uint8_t byte; + ok &= readRegister(0xff, &byte); + ok = false; + length--; + } + } + if (length > 0) { + ok &= readRegisters(0xff, this->status.receivedPacketStatus.byte, 2); + if (ok) { + this->status.receivePacketStatusValid = 1; + } + } + } + } + } + } + ok &= strobe(SFRX); + + return ok; + } + + + void Cc1101::printRegisters () { + const Register_t *regValues = &PMEM_CC1101_REGISTER_INIT; + printf_P(PSTR("\n")); + for (uint8_t addr = 0; addr < sizeof(Register_t); addr++) { + bool ok = true; + uint8_t regValue, value; + memcpy_P(®Value, ((const uint8_t *)regValues) + addr, 1); + ok &= readRegister(addr, &value); + if (value != regValue) { printf_P(PSTR(" != 0x%02x"), regValue); } + } + uint8_t data[8]; + for (uint8_t addr = 0x30; addr <= 0x3d; addr++) { + readStatusRegister((StatusAddress_t)addr, data); + } + readRegisters(0x3e, data, 8); // PATABLE + } + + bool Cc1101::resetCC1101 () { + bool ok = true; + setChipEnableLow(); + _delay_us(10); + setChipEnableHigh(); + _delay_us(50); + setChipEnableLow(); + ok &= waitForMisoLow(); + ok &= strobe(SRES); + ok &= waitForMisoLow(); + + + return ok; + } + + bool Cc1101::initRegister () { + const Register_t *regValues = &PMEM_CC1101_REGISTER_INIT; + bool ok = true; + // DebugPrint_t tmp = debugPrint.print; + // debugPrint.byte = 0xff; // print all + // printRegisters(); + + for (uint8_t addr = 0; ok && addr < sizeof(Register_t); addr++) { + uint8_t regValue; + memcpy_P(®Value, ((const uint8_t *)regValues) + addr, 1); + ok &= writeRegister(addr, regValue); + if ((addr == 0x07) && (regValue & 0x04)) { + status.receivePacketStatusEnabled = true; + } + } + + // debugPrint.print = tmp; + return ok; + } + + bool Cc1101::initPaTable () { + const PATABLE_INIT_ITEM_t *values = PMEM_CC1101_PATABLE_INIT; + uint8_t patable[8]; + uint8_t initValuesIndex = 0; + for (uint8_t i = 0; i < 8; i++) { + memcpy_P(&patable[i], &(values[initValuesIndex++].pa_power), 1); + if (initValuesIndex >= (sizeof(PMEM_CC1101_PATABLE_INIT) / sizeof(PATABLE_INIT_ITEM_t))) { + initValuesIndex = 0; + } + if (debugPrint.print.writePaTable) { + printf_P(PSTR("\nPTABLE %d: 0x%02x"), i, patable[i]); + } + } + return writeRegisters(0x3e, patable, 8); + } + +#endif + + diff --git a/software/test-software/src/units/cc1101.hpp b/software/test-software/src/units/cc1101.hpp new file mode 100644 index 0000000..a861f09 --- /dev/null +++ b/software/test-software/src/units/cc1101.hpp @@ -0,0 +1,281 @@ +#ifndef CC1101_HPP +#define CC1101_HPP + +#include +#include "../main.hpp" +#include + +class Cc1101 : public TestUnit { + public: + #define PACKET_LENGTH 20 + + public: + typedef enum { Test, Send, Receive } Cc1101Mode; + typedef enum { _IDLE = 0, _RX = 1, _TX = 2, _FSTXON = 3, _CALIBRATE = 4, _SETTLING = 5, _RXFIFO_OVFL = 6, _TXFIFO_UNFL = 7 } StatusState_t; + typedef struct { uint8_t fifoBytes:4; StatusState_t state:3; uint8_t chipNotReady:1; } Status_t; + typedef struct { int8_t rssi:8; uint8_t lqi:7; uint8_t crcOk:1; } ReceivedPacketStatus_t; // CC1101 datasheet page 37 + union { + struct { + union { + Status_t value; + uint8_t byte; + } spiResponse; + union { + ReceivedPacketStatus_t value; + uint8_t byte [2]; + } receivedPacketStatus; + uint8_t spiResponseValid: 1; + uint8_t receivePacketStatusEnabled:1; + uint8_t receivePacketStatusValid:1; + }; + uint32_t dword; + } status; + + public: + Cc1101 (Cc1101Mode mode) { timer = 0; this->mode = mode; status.dword = 0; } + virtual void init (); + virtual void cleanup (); + virtual int8_t run (uint8_t subtest); + virtual PGM_P getName (); + + public: + void tick1ms () { if (timer > 0) timer--; }; + + private: + typedef enum { SRES = 0x30, SCAL = 0x33, SRX = 0x34, STX = 0x35, SFRX = 0x3a, SFTX = 0x3b, SIDLE = 0x36 } StrobeCommand_t; + typedef enum { MARCSTATE = 0x35, TXBYTES = 0x3A, RXBYTES = 0x3B } StatusAddress_t; + typedef struct { uint8_t writeRegister:1; uint8_t writeRegisters:1; uint8_t readRegister:1; uint8_t readRegisters:1; uint8_t writePaTable:1; } DebugPrint_t; + + private: + int8_t runSend (uint8_t subtest); + int8_t runReceive (uint8_t subtest); + int8_t runTest (uint8_t subtest); + uint8_t sendSpiByte (uint8_t b); + void triggerOn(); + void triggerOff(); + void triggerToggle (); + void setChipEnableLow (); + void setChipEnableHigh (); + bool isMisoHigh (); + bool isGd0High (); + bool waitForMisoLow (); + bool waitForGD0High (); + bool waitForGD0Low (); + bool strobe (StrobeCommand_t strobe); + bool writeRegister (uint8_t addr, uint8_t value); + bool writeRegisters (uint8_t addr, uint8_t *buffer, uint8_t length); + bool readRegister (uint8_t addr, uint8_t *value); + bool readRegisters (uint8_t addr, uint8_t *buffer, uint8_t length); + bool readStatusRegister (StatusAddress_t addr, uint8_t *value); + bool sendData (uint8_t *buffer, uint8_t length); + bool receiveData (uint8_t *buffer, uint8_t *receivedLength, uint8_t maxBufferSize); + void printRegisters (); + + bool resetCC1101 (); + bool initRegister (); + bool initPaTable (); + + + private: + Cc1101Mode mode; + uint8_t timer; + union { + DebugPrint_t print; + uint8_t byte; + } debugPrint; + + + public: + typedef enum { + IOCFG2 = 0x00, IOCFG1 = 0x01, IOCFG0 = 0x02, + FIFOTHR = 0x03, + SYNC1 = 0x04, SYNC0 = 0x05, + PKTLEN = 0x06, + PKTCTRL1 = 0x07, PKTCTRL0 = 0x08, + ADDR = 0x00, CHANNR = 0x0a, + FSCTRL1 = 0x0b, FSCTRL0 = 0x0c, + FREQU2 = 0x0d, FREQU1 = 0x0e, FREQU0 = 0x0f, + MDMCFG4 = 0x10, MDMCFG3 = 0x11, MDMCFG2 = 0x12, MDMCFG1 = 0x13, MDMCFG0 = 0x14, + DEVIATN = 0x15, + MCSM2 = 0x16, MCSM1 = 0x17, MCSM0 = 0x18, + FOCCFG = 0x19, BSCFG = 0x1a, + AGCCTRL2 = 0x1b, AGCCTRL1 = 0x1c, AGCCTRL0 = 0x1d, + WOREVT1 = 0x1e, WOREVT0 = 0x1f, WORCTRL = 0x20, + FREND1 = 0x21, FREND0 = 0x22, + FSCAL3 = 0x23, FSCAL2 = 0x24, FSCAL1 = 0x25, FSCAL0 = 0x26, + RCCTRL1 = 0x27, RCCTRL0 = 0x28, + FSTEST = 0x29, PTEST = 0x2a, AGCTEST = 0x2b, + TEST2 = 0x2c, TEST1 = 0x2d, TEST0 = 0x2e + } RegisterAddress_t; + + typedef enum { + RX_FIFO_FILLED_OR_ABOVE_THRESHHOLD = 0x00, + RX_FIFO_FILLED_OR_ABOVE_THRESHHOLD_OR_END_OF_PACKAGE_UNTIL_FIFO_EMPTY = 0x01, + TX_FIFO_FILLED_OR_ABOVE_THRESHHOLD = 0x02, + TX_FIFO_FULL_UNTIL_BELOW_THRESHHOLD = 0x03, + RX_FIFO_OVERFLOW_UNTIL_FIFO_FLUSHED = 0x04, + TX_FIFO_UNDERFLOW_UNTIL_FIFO_FLUSHED = 0x05, + SYNC_SENT_OR_RECEIVED_UNTIL_END_OF_PACKET = 0x06, + PACKET_RECEIVED_WITH_CRC_OK_UNTIL_FIRST_BYTE_READ_FROM_RXFIFO = 0x07, + PREAMBLE_QUALITY_REACHED_UNTIL_REENTER_RX = 0x08, + RSSI_LEVEL_BELOW_THRESHOLD = 0x09, + LOCK_DETECTOR_OUTPUT = 0x0a, + SERIAL_CLOCK = 0x0b, + SERIAL_SYNCHRONOUS_DATA_OUTPUT = 0x0c, + SERIAL_ASYNC_DATA_OUTPUT = 0x0d, + CARRIER_DETECTED_UNTIL_ENTER_IDLE = 0x0e, + CRC_OK_UNTIL_REENTER_RX = 0x0f, + RX_HARD_DATA_1 = 0x16, + RX_HARD_DATA_0 = 0x17, + PA_PD = 0x1b, + LNA_PD = 0x1c, + RX_SYMBOL_TICK = 0x1d, + WAKEUP_ON_RECEIVE_EVENT0 = 0x24, + WAKEUP_ON_RECEIVE_EVENT1 = 0x25, + CLK_256 = 0x26, + CLK_32K = 0x27, + CHIP_NOT_READY = 0x29, + XOSC_STABLE = 0x2b, + HIGH_IMPEDANCE_WHEN_CHIP_SELECT_HIGH = 0x2e, + HW_TO_0 = 0x2f, + CLK_XOSC = 0x30, + CLK_XOSC_DIV_1P5 = 0x31, + CLK_XOSC_DIV_2 = 0x32, + CLK_XOSC_DIV_3 = 0x33, + CLK_XOSC_DIV_4 = 0x34, + CLK_XOSC_DIV_6 = 0x35, + CLK_XOSC_DIV_8 = 0x36, + CLK_XOSC_DIV_12 = 0x37, + CLK_XOSC_DIV_16 = 0x38, + CLK_XOSC_DIV_24 = 0x39, + CLK_XOSC_DIV_32 = 0x3a, + CLK_XOSC_DIV_48 = 0x3b, + CLK_XOSC_DIV_64 = 0x3c, + CLK_XOSC_DIV_96 = 0x3d, + CLK_XOSC_DIV_128 = 0x3e, + CLK_XOSC_DIV_192 = 0x3f + } GDOx_CFG_t; + + typedef enum { + SLEEP = 0, + IDLE = 1, + XOFF = 2, + MANCAL_VCOON = 3, + MANCAL_REGON = 4, + MANCAL = 5, + FS_WAKEUP_VCOON = 6, + FS_WAKEUP_REGON = 7, + CALIBRATE_START = 8, + SETTLING_BWBOOST = 9, + SETTLING_FS_LOCK = 10, + SETTLIN_IFADCON = 11, + CALIBRATE_END = 12, + RX = 13, + RX_END = 14, + RX_RST = 15, + TXRX_SETTLING = 16, + RXFIFO_OVERFLOW = 17, + FXTXON = 18, + TX = 19, + TX_END = 20, + RXTX_SETTLING = 21, + TXFIFO_UNDERFLOW = 22, + UNKNOWN = 255 + } MainRadioControlState; + + typedef struct { uint8_t pa_power; const char dbm[4]; } PATABLE_INIT_ITEM_t; + + typedef enum { NO_ADDR_CHECK = 0, CHECK_NO_BROADCAST = 1, CHECK_WITH_BROADCAST_0 = 2, CHECK_WITH_BROADCAST_0_AND_255 = 3 } ADR_CHK_t; + typedef enum { FIXED = 0, VARIABLE = 1, INFINITE = 2 } LENGTH_CONFIG_t; + typedef enum { NORMAL_USE_FIFO = 0, SYNC_SERIAL = 1, RANDOM_TX = 2, ASYNC_SERIAL = 3 } PKT_FORMAT_t; + typedef enum { FSK2 = 0, GFSK = 1, ASK_OOK = 3, FSK4 = 4, MSK = 7 } MOD_FORMAT_t; + typedef enum { NO_SYNC = 0, SYNC_15_16 = 1, SYNC_16_16 = 2, SYNC_30_32 = 3, CARRIER_SENSE = 4, SYNC_15_16_CARRIER_SENSE = 5, SYNC_16_16_CARRIER_SENSE = 6 , SYNC_30_326_CARRIER_SENSE = 7} SYNC_MODE_t; + typedef enum { TWO = 0, THREE = 1, FOUR = 2, SIX = 3, EIGHT = 4, TWELVE = 5, SIXTEEN = 6, TWENTYFOUR = 7 } NUM_PREAMBLE_t; + typedef enum { TXOFF_IDLE = 0, TXOFF_FSTXON = 1, STAY_IN_TX = 2, TXOFF_RX = 3 } TXOFF_MODE_t; + typedef enum { RXOFF_IDLE = 0, RXOFF_FSTXON = 1, RXOFF_TX = 2, STAY_IN_RX = 3 } RXOFF_MODE_t; + typedef enum { ALWAYS = 0, RSSI_BELOW_THRESHOLD = 1, UNLESS_RECEIVE_PACKET = 2, RSSI_BELOW_THRESHOLD__UNLESS_RECEIVE_PACKET = 3 } CCA_MODE_t; + typedef enum { NEVER = 0, IDLE_TO_RX_OR_TX = 1, RX_OR_TX_TO_IDLE = 2, RX_OR_TX_TO_IDLE_EVERY_4_TIME = 3 } FS_AUTOCAL_t; + + typedef struct { GDOx_CFG_t gdo0_cfg:6; uint8_t gdo0_inv:1; uint8_t bit7:1; } IOCFG0_t; + typedef struct { GDOx_CFG_t gdo1_cfg:6; uint8_t gdo1_inv:1; uint8_t bit7:1; } IOCFG1_t; + typedef struct { GDOx_CFG_t gdo2_cfg:6; uint8_t gdo2_inv:1; uint8_t bit7:1; } IOCFG2_t; + typedef struct { uint8_t fifo_thr:4; uint8_t close_in_rx:2; uint8_t adc_retention:1; uint8_t bit7:1; } FIFOTHR_t; + typedef struct { ADR_CHK_t adr_chk:2; uint8_t append_status:1; uint8_t crc_autoflush:1; uint8_t bit4:1; uint8_t pqt:3; } PKTCTRL1_t; + typedef struct { LENGTH_CONFIG_t length_config:2; uint8_t crc_en:1; uint8_t bit3:1; PKT_FORMAT_t pkt_format:2; uint8_t white_data:1; uint8_t bit7:1; } PKTCTRL0_t; + typedef struct { uint8_t frequ_if:5; uint8_t bit5:1; uint8_t bit76:2; } FSCTRL1_t; + typedef struct { uint8_t frequoff:8; } FSCTRL0_t; + typedef struct { uint8_t drate_e:4; uint8_t chanbw_m:2; uint8_t chanbw_e:2; } MDMCFG4_t; + typedef struct { uint8_t drate_m:8; } MDMCFG3_t; + typedef struct { SYNC_MODE_t sync_mode:3; uint8_t manchester_en:1; MOD_FORMAT_t mod_format:3; uint8_t dem_dcfilt_off:1; } MDMCFG2_t; + typedef struct { uint8_t chanspc_e:2; uint8_t bit32:2; NUM_PREAMBLE_t num_preamble:3; uint8_t fec_en:1; } MDMCFG1_t; + typedef struct { uint8_t chanspc_m:8; } MDMCFG0_t; + typedef struct { uint8_t deviation_m:3; uint8_t bit3:1; uint8_t deviation_e:3; uint8_t bit7:1; } DEVIATN_t; + typedef struct { uint8_t rx_time:3; uint8_t rx_time_qual:1; uint8_t rx_time_rssi:1; uint8_t bit765:3; } MCSM2_t; + typedef struct { TXOFF_MODE_t txoff_mode:2; RXOFF_MODE_t rxoff_mode:2; CCA_MODE_t cca_mode:2; uint8_t bit76:2; } MCSM1_t; + typedef struct { uint8_t xosc_force_on:1; uint8_t pin_ctrl_en:1; uint8_t po_timeout:2; FS_AUTOCAL_t fs_autocal:2; uint8_t bit76:2; } MCSM0_t; + typedef struct { uint8_t pa_power:3; uint8_t bit3:1; uint8_t lodiv_buf_current:2; uint8_t bit76:2; } FREND0_t; + + typedef union { + uint8_t byte; + struct { + uint8_t num_rxbytes:7; + uint8_t rxfifo_overflow:1; + } rxbytes; + } STATUS_RXBYTES_t; + + typedef struct { + IOCFG2_t iocfg2; + IOCFG1_t iocfg1; + IOCFG0_t iocfg0; + FIFOTHR_t fifothr; + uint8_t sync1; + uint8_t sync0; + uint8_t pktlen; + PKTCTRL1_t pktctrl1; + PKTCTRL0_t pktctrl0; + uint8_t addr; + uint8_t channr; + FSCTRL1_t fsctrl1; + FSCTRL0_t fsctrl0; + uint8_t frequ2; + uint8_t frequ1; + uint8_t frequ0; + MDMCFG4_t mdmcfg4; + MDMCFG3_t mdmcfg3; + MDMCFG2_t mdmcfg2; + MDMCFG1_t mdmcfg1; + MDMCFG0_t mdmcfg0; + DEVIATN_t deviatn; + MCSM2_t mcsm2; + MCSM1_t mcsm1; + MCSM0_t mcsm0; + uint8_t foccfg; + uint8_t bscfg; + uint8_t agcctrl2; + uint8_t agcctrl1; + uint8_t agcctrl0; + uint8_t worevt1; + uint8_t worevt0; + uint8_t worctrl; + uint8_t frend1; + FREND0_t frend0; + uint8_t fscal3; + uint8_t fscal2; + uint8_t fscal1; + uint8_t fscal0; + uint8_t rcctrl1; + uint8_t rcctrl0; + uint8_t fstest; + uint8_t ptest; + uint8_t agctest; + uint8_t test2; + uint8_t test1; + uint8_t test0; + + } Register_t; + + + +}; + +#endif \ No newline at end of file diff --git a/software/test-software/src/units/encoder.cpp b/software/test-software/src/units/encoder.cpp new file mode 100644 index 0000000..7b33b76 --- /dev/null +++ b/software/test-software/src/units/encoder.cpp @@ -0,0 +1,138 @@ +#include +#include + +#include "encoder.hpp" +#include "../main.hpp" + +// Encoder signals on rotation clockwise 1 step: +// A -----____------ one char app. 1ms..2ms (rotation speed) +// B -------___----- +// one step when: A = 0, B= 1->0 + +// Encoder signals on rotation counterclockwise 1 step: +// A -----____------ one char app. 1ms..2ms (rotation speed) +// B --______----- +// one step when: A = 0, B= 0->1 + +#if defined(__AVR_ATmega644P__) || defined(__AVR_ATmega1284P__) + + // Nano-644 + // --------------------------------------------------------------- + // PB0/T0 ... Encoder A + // PB1/T1 ... Encoder B + // PB2/INT2 ... push switch of encoder (pushed = 0) + + void Encoder::init () { + DDRB &= ~((1 << PB2) | (1 << PB1) | (1 << PB0)); + PORTB |= (1 << PORTB2) | (1 << PORTB1) | (1 << PORTB0); // enable pullup + enabled = 1; + } + + void Encoder::cleanup () { + enabled = 0; + DDRB &= ~((1 << PB2) | (1 << PB1) | (1 << PB0)); + PORTB &= ~((1 << PORTB2) | (1 << PORTB1) | (1 << PORTB0)); + } + + bool Encoder::isPressed () { + return (PINB & (1 << PB2)) == 0; + } + + bool Encoder::getA () { + return (PINB & (1 << PB0)) == 0; + } + + bool Encoder::getB () { + return (PINB & (1 << PB1)) == 0; + } + + +#endif + +#ifdef __AVR_ATmega328P__ + + // Arduino-Nano-5V + // --------------------------------------------------------------- + // PD4/T0 ... Encoder A + // PB0 ... Encoder B + // PD7 ... push switch of encoder (pushed = 0) + + void Encoder::init () { + DDRB &= ~(1 << PB0); + DDRD &= ~((1 << PD7) | (1 << PD4)); + PORTB |= (1 << PB0); // enable pullup + PORTD |= (1 << PD7) | (1 << PD4); // enable pullup + enabled = 1; + } + + void Encoder::cleanup () { + enabled = 0; + PORTB &= ~(1 << PB0); + PORTD &= ~((1 << PD7) | (1 << PD4)); + DDRB &= ~(1 << PB0); + DDRD &= ~((1 << PD7) | (1 << PD4)); + } + + bool Encoder::isPressed () { + return (PIND & (1 << PD7)) == 0; + } + + bool Encoder::getA () { + return (PIND & (1 << PD4)) == 0; + } + + bool Encoder::getB () { + return (PINB & (1 << PB0)) == 0; + } + +#endif + + +int8_t Encoder::run (uint8_t subtest) { + switch (subtest) { + case 0: { + while (wait(10) == EOF) { + printf_P(PSTR("\r => Encoder (push to clear): ")); + printf_P(PSTR("%5d (0x%02x) "), count, (uint8_t)count); + if (isPressed()) { + reset(); + } + } + return 0; + } + } + + return -1; +} + +struct EncoderState { + int8_t a:1; // signal A + int8_t b:1; // signal B +}; + +void Encoder::tick100us () { + static EncoderState lastState = { 1, 1 }; + static EncoderState lastStableState = { 1, 1 }; + + if (!enabled) { + count = 0; + return; + } + EncoderState nextState; + nextState.a = getA() ? 1 : 0; + nextState.b = getB() ? 1 : 0; + if (nextState.a == lastState.a && nextState.b == lastState.b) { + if (lastStableState.a == 0 && nextState.b != lastStableState.b) { + if (nextState.b == 0) { + count = count < 127 ? count + 1 : 127; + } else { + count = count > -128 ? count - 1 : -128; + } + } + lastStableState.a = nextState.a; + lastStableState.b = nextState.b; + } + lastState.a = nextState.a; + lastState.b = nextState.b; +} + diff --git a/software/test-software/src/units/encoder.hpp b/software/test-software/src/units/encoder.hpp new file mode 100644 index 0000000..9b0861b --- /dev/null +++ b/software/test-software/src/units/encoder.hpp @@ -0,0 +1,28 @@ +#ifndef ENCODER_HPP +#define ENCODER_PP + +#include +#include "../main.hpp" +#include + +class Encoder : public TestUnit { + public: + uint8_t enabled; + int8_t count; + + public: + Encoder () { reset(); enabled = 0; }; + virtual void init (); + virtual void cleanup (); + virtual int8_t run (uint8_t subtest); + virtual PGM_P getName () { return PSTR("Encoder"); } + void reset () { count = 0; } + void tick100us (); + bool isPressed (); + + private: + bool getA (); + bool getB (); +}; + +#endif \ No newline at end of file diff --git a/software/test-software/src/units/i2c.cpp b/software/test-software/src/units/i2c.cpp new file mode 100644 index 0000000..60dd22d --- /dev/null +++ b/software/test-software/src/units/i2c.cpp @@ -0,0 +1,216 @@ +#include +#include +#include +#include +#include + +#include "i2c.hpp" +#include "../adafruit/bme280.h" +#include "../main.hpp" + +// Sparkfun https://www.sparkfun.com/products/22858 +// ENS160 address 0x53 (air quality sensor) +// BME280 address 0x77 /humidity/temperature sensor) +// register 0xfe: humidity_7:0 +// register 0xfd: humidity_15:8 + +PGM_P I2c::getName () { + switch (mode) { + case SparkFunEnvCombo: return PSTR("I2C-Sparkfun Env-Combo"); + case Master: return PSTR("I2C-Master"); + case Slave: return PSTR("I2C-Slave"); + } + return "?"; +} + +void I2c::init () { + TWBR = 13; // 100kHz (TWPS1:0 = 00), TWBR = (F_CPU - 16 * 100000) / (2 * 100000 * 4); + TWBR = 28; // 50kHz (TWPS1:0 = 00), TWBR = (F_CPU - 16 * 50000) / (2 * 50000 * 4); + TWBR = 100; // 50kHz (TWPS1:0 = 00), TWBR = (F_CPU - 16 * 50000) / (2 * 50000 * 4); + TWCR = (1 << TWEN); + ADMUX = (1 << ADLAR) | (1 << REFS0); // ADC0, VREF=AVCC=3.3V + ADCSRA = (1 << ADEN) | 7; // ADC Enable, Prescaler 128 + enabled = true; +} + +void I2c::cleanup () { + enabled = false; + TWCR = (1 << TWEN); + TWBR = 0; + ADMUX = 0; + ADCSRA = 0; +} + +int8_t I2c::run (uint8_t subtest) { + if (subtest == 0 && mode == I2c::SparkFunEnvCombo) { + printf_P(PSTR(" BM280 ... ")); + if (!bm280.begin()) { + printf_P(PSTR("E1")); + return -1; + } + printf_P(PSTR("OK, ENS160 ... ")); + if (!ens160.begin()) { + printf_P(PSTR("E2")); + return -1; + } + if (!ens160.setMode(ENS160_OPMODE_STD)) { + printf_P(PSTR("E3")); + return -1; + } + if (!ens160.set_envdata(25.0, 65)) { + printf_P(PSTR("E4")); + return -1; + } + + printf_P(PSTR("OK")); + float accTemp = 0, accHumidity = 0; + int8_t accCount = -1; + + do { + // BME280 + float p = bm280.readPressure(); + printf_P(PSTR("\n => BM280: P= %.3fbar"), (double)p / 100000.0); + float t = bm280.readTemperature(); + printf_P(PSTR(", T= %.2f°C"), (double)t); + float h = bm280.readHumidity(); + printf_P(PSTR(", H= %.2f%%"), (double)h); + + if (accCount >= 0 && !isnanf(h) && !isnan(t)) { + accTemp += t; + accHumidity += h; + accCount++; + } + + bm280.setSampling( + Adafruit_BME280::MODE_NORMAL, + Adafruit_BME280::SAMPLING_X16, + Adafruit_BME280::SAMPLING_X16, + Adafruit_BME280::SAMPLING_X16, + Adafruit_BME280::FILTER_OFF, + Adafruit_BME280::STANDBY_MS_1000 + ); + + // ENS160 only activated every 32s to avoid wrong temperature measuerment + // if ES160 would be continously active, the temperature would be 4°C higher + // -> ES160 has not enough distance to BM280 + // This solution causes only a +0.3°C higher temperatur value + if (accCount < 0 || accCount >= 32) { + printf_P(PSTR(" | ENS160 (")); + if (accCount > 0) { + h = accHumidity / accCount; + t = accTemp / accCount; + accTemp = 0; + accHumidity = 0; + } + accCount = 0; + if (!ens160.set_envdata(t, h)) { + printf_P(PSTR("E1)")); + } else { + printf_P(PSTR("%.1f°C/%.1f%%): "), (double)t, (double)h); + if (!ens160.setMode(ENS160_OPMODE_STD)) { + printf_P(PSTR("E2")); + } else { + for (uint8_t i = 0; i < 100; i++) { + _delay_ms(15); + uint8_t status; + if (ens160.readStatus(&status)) { + if (status & ENS160_DATA_STATUS_NEWDAT) { + ENS160_DATA data; + if (ens160.readData(&data)) { + printf_P(PSTR(" aqi=%d("), data.aqi); + switch(data.aqi) { + case 1: printf_P(PSTR("excellent")); break; + case 2: printf_P(PSTR("good")); break; + case 3: printf_P(PSTR("moderate")); break; + case 4: printf_P(PSTR("poor")); break; + case 5: printf_P(PSTR("unhealthy")); break; + default: printf_P(PSTR("?")); break; + } + printf_P(PSTR("), tvoc=%dppb"), data.tvoc); + printf_P(PSTR(", eco2=%d("), data.eco2); + if (data.eco2 < 400) { + printf_P(PSTR("?")); + } else if (data.eco2 < 600) { + printf_P(PSTR("excellent")); + } else if (data.eco2 < 800) { + printf_P(PSTR("good")); + } else if (data.eco2 < 1000) { + printf_P(PSTR("fair")); + } else if (data.eco2 < 1500) { + printf_P(PSTR("poor")); + } else { + printf_P(PSTR("bad")); + } + printf_P(PSTR(")")); + } + break; + } + } + } + } + if (!ens160.setMode(ENS160_OPMODE_IDLE)) { + printf_P(PSTR("E3")); + } + } + } + + } while (wait(1000) == EOF); + + } else if (subtest == 0 && mode == I2c::Master) { + if (!master.begin(0x01)) { + printf_P(PSTR("E1")); + return -1; + } + do { + uint8_t buffer[1]; + // read poti + ADCSRA |= (1 << ADSC); // start ADC + while (ADCSRA & (1 << ADSC)) {} // wait for result + buffer[0] = ADCH; + printf_P(PSTR("\n I2C-MASTER: to slave: 0x%02x"), buffer[0]); + if (!master.write(buffer, 1)) { + printf_P(PSTR(" -> ERROR")); + } + printf_P(PSTR(", from slave: ")); + if (master.read(buffer, 1)) { + printf_P(PSTR("0x%02x"), buffer[0]); + } else { + printf_P(PSTR(" -> ERROR")); + } + } while (wait(1000) == EOF); + master.end(); + + } else if (subtest == 0 && mode == I2c::Slave) { + if (!slave.begin(0x01, false)) { + printf_P(PSTR("E1")); + return -1; + } + do { + int fromMaster = slave.read(); + if (fromMaster != EOF) { + ADCSRA |= (1 << ADSC); // start ADC + while (ADCSRA & (1 << ADSC)) {} // wait for result + slave.write(ADCH); + printf_P(PSTR("\n I2C SLAVE: from master: 0x%02x -> to master: 0x%02x"), fromMaster, ADCH); + } + } while (wait(0) == EOF); + slave.end(); + + } else { + printf_P(PSTR("end")); + return -1; + } + wait(500); + return 0; +} + +void I2c::handleTwiIrq () { + if (mode == I2c::Slave) { + DDRD |= (1 << PD7); + PORTD |= (1 << PD7); + slave.handleTWIIsr(); + PORTD &= ~(1 << PD7); + } else { + TWCR |= (1 << TWINT); // clear Interrupt Request + } +} diff --git a/software/test-software/src/units/i2c.hpp b/software/test-software/src/units/i2c.hpp new file mode 100644 index 0000000..2148cc0 --- /dev/null +++ b/software/test-software/src/units/i2c.hpp @@ -0,0 +1,43 @@ +#ifndef I2C_HPP +#define I2C_HPP + +#include +#include "../main.hpp" +#include "../adafruit/bme280.h" +#include "../adafruit/ens160.h" +#include "../i2cmaster.hpp" +#include "../i2cslave.hpp" + + +class I2c : public TestUnit { + public: + typedef enum I2cMode { SparkFunEnvCombo, Master, Slave } I2cMode; + + private: + I2cMode mode; + Adafruit_BME280 bm280; + ScioSense_ENS160 ens160; + I2cMaster master; + I2cSlave slave; + + public: + bool enabled; + + public: + I2c (I2cMode mode) { enabled = false; this->mode = mode; } + void tick1ms () { master.tick1ms(); slave.tick1ms(); } + virtual void init (); + virtual void cleanup (); + virtual int8_t run (uint8_t subtest); + virtual PGM_P getName (); + void handleTwiIrq (); + // uint16_t startRead (uint8_t address); + // uint16_t startWrite (uint8_t address); + // void stop (); + // uint16_t writeByte (uint8_t data); + // uint16_t writeData (uint8_t size, const uint8_t *data); + // uint16_t readData (uint8_t size, uint8_t *data); + // int32_t compensateBm280T (int32_t adcT); +}; + +#endif \ No newline at end of file diff --git a/software/test-software/src/units/ieee485.cpp b/software/test-software/src/units/ieee485.cpp new file mode 100644 index 0000000..8fcb67a --- /dev/null +++ b/software/test-software/src/units/ieee485.cpp @@ -0,0 +1,111 @@ +#include +#include +#include +#include + +#include "ieee485.hpp" +#include "../main.hpp" + +#ifdef __AVR_ATmega328P__ + +// Nano-328P +// ------------------------------------ +// IEE485 not supported (no UART1) + +void Ieee485::init () {} +void Ieee485::cleanup () {} +int8_t Ieee485::run (uint8_t subtest) { + return -1; +} + +#endif + + +#if defined(__AVR_ATmega644P__) || defined(__AVR_ATmega1284P__) + +// Nano-644 +// ------------------------------------ +// PB0 ... nRE .. Read enable +// PB1 ... DE .. Data enable + +#define SET_nRE (PORTB |= (1 << PB0)) +#define CLR_nRE (PORTB &= ~(1 << PB0)) +#define SET_DE (PORTB |= (1 << PB1)) +#define CLR_DE (PORTB &= ~(1 << PB1)) + +void Ieee485::init () { + // Poti + ADMUX = (1 << ADLAR) | (1 << REFS0); // ADC0, VREF=AVCC=3.3V + ADCSRA = (1 << ADEN) | 7; // ADC Enable, Prescaler 128 + + // Modbus + SET_nRE; + CLR_DE; + DDRB |= (1 << PB1) | (1 << PB0); + + // UART1 interface on Nano-644 + PORTD |= (1 << PD2); // enable RxD1 pullup + UCSR1A = (1 << U2X1); + UCSR1B = (1 << RXCIE1) | (1 << RXEN1) | (1 < send Byte 0x%02x"), ADCH); + int b; + ATOMIC_BLOCK(ATOMIC_FORCEON) { + b = receivedByte; + receivedByte = -1; + } + if (b >= 0) { + printf_P(PSTR("\n => receive Byte: 0x%02x"), b); + } + } + + } else { + printf_P(PSTR("end")); + return -1; + } + + return 0; +} + +void Ieee485::handleRxByte (uint8_t b) { + receivedByte = b; +} + +#endif \ No newline at end of file diff --git a/software/test-software/src/units/ieee485.hpp b/software/test-software/src/units/ieee485.hpp new file mode 100644 index 0000000..ffbb15c --- /dev/null +++ b/software/test-software/src/units/ieee485.hpp @@ -0,0 +1,22 @@ +#ifndef IEEE485_HPP +#define IEEE485_HPP + +#include +#include "../main.hpp" +#include + +class Ieee485 : public TestUnit { + public: + uint8_t enabled; + int16_t receivedByte; + + public: + Ieee485 () { enabled = 0; receivedByte = -1; } + virtual void init (); + virtual void cleanup (); + virtual int8_t run (uint8_t subtest); + virtual const char *getName () { return PSTR("IEEE485"); } + void handleRxByte (uint8_t); +}; + +#endif \ No newline at end of file diff --git a/software/test-software/src/units/lcd.cpp b/software/test-software/src/units/lcd.cpp new file mode 100644 index 0000000..e779c16 --- /dev/null +++ b/software/test-software/src/units/lcd.cpp @@ -0,0 +1,443 @@ +#include +#include +#include + +#include "lcd.hpp" +#include "../main.hpp" + +#if defined(__AVR_ATmega644P__) || defined(__AVR_ATmega1284P__) + + // Nano-644 / Nano-1284 + // --------------------------------------------------------------- + + // Nano-X-Base V1a V2a Beschreibung + // --------------------------------- ------- + // BL ........... PC0 PC0 Backlight ON(=1) / OFF(=0) + // E ........... PA3 PA3 LCD Enable (Verbindung via J25 erforderlich) + // R/W .......... PD6 PD6 Read/Write: Read=1, Write=0 + // RS ........... PD7 PD7 Register Select: Command=0, Data=1 + // Data ......... PB7:0 PB7:0 Achtung von 5V LCD nicht lesen! + + void Lcd::init () { + DDRA |= (1 << PA3) | (1 << PA0); + DDRB = 0xff; + DDRD |= (1 << PD7) | (1 << PD6); + initLcd(); + printf_P(PSTR("init LCD (")); + if (status == 1) { + printf_P(PSTR("OK)")); + } else { + printf_P(PSTR("ERROR %d)"), status); + } + setBacklightOn(); + } + + void Lcd::cleanup () { + clear(); + PORTA &= ~((1 << PA3) | (1 << PA0)); + DDRA &= ~((1 << PA3) | (1 << PA0)); + PORTB = 0; + DDRB = 0; + PORTD &= ~((1 << PD7) | (1 << PD6)); + DDRD &= ~((1 << PD7) | (1 << PD6)); + } + + void Lcd::setRS () { PORTD |= (1 << PD7); } + void Lcd::clrRS () { PORTD &= ~(1 << PD7); } + void Lcd::setRW () { PORTD |= (1 << PD6); } + void Lcd::clrRW () { PORTD &= ~(1 << PD6); } + void Lcd::setE () { PORTA |= (1 << PA3); } + void Lcd::clrE () { PORTA &= ~(1 << PA3); } + void Lcd::dataDirectionOut () { DDRB = 0xff; } + + void Lcd::dataDirectionIn () { + if (hardwareVersion == 1) { + // read back not allowed (missing level shifter 5V -> 3.3V) + DDRB = 0xff; + } else { + DDRB = 0x00; + } + } + + uint8_t Lcd::getData () { + if (hardwareVersion == 1) { + // read back not allowed (missing level shifter 5V -> 3.3V) + _delay_ms(1); + return 0x00; // bit 8 (busy) = 0 + } else { + return PINB; + } + } + + void Lcd::setData (uint8_t data) { + PORTB = data; + } + + void Lcd::setBacklightOn () { + PORTA |= (1 << PA0); + } + + void Lcd::setBacklightOff () { + PORTA &= ~(1 << PA0); + } + +#endif + +#ifdef __AVR_ATmega328P__ + + // Arduino Nano (5V) + // --------------------------------------------------------------- + + // Nano-X-Base V1a V2a Beschreibung + // --------------------------------- ------- + // BL ........... PC0 PC0 Backlight ON(=1) / OFF(=0) + // E ........... PC3 PC3 LCD Enable (Verbindung via J25 erforderlich) + // R/W .......... PD3 PD3 Read/Write: Read=1, Write=0 + // RS ........... PD2 PD2 Register Select: Command=0, Data=1 + // Data0 ........ PD4 PD4 + // Data1 .........PB0 PB0 + // Data2 .........PD7 PD7 + // Data3 .........PD6 PD6 + // Data4 .........PB2 PB2 + // Data5 .........PB3 PB3 + // Data6 .........PB4 PB4 + // Data7 .........PB5 PB5 + + // PC3 ..... E --> LCD Enable (Verbindung via J25 erforderlich) + // PD3 ..... R/W --> Read/Write: Read=1, Write=0 + // PD2 ..... RS --> Register Select: Command=0, Data=1 + + + void Lcd::init () { + clrRW(); + clrRS(); + clrE(); + setData(0); + DDRB |= (1 << PB5) | (1 << PB4) | (1 << PB3) | (1 << PB2) | (1 << PB0); + DDRC |= (1 << PC3) | (1 << PC0); + DDRD |= (1 << PD7) | (1 << PD6) | (1 << PD4) | (1 << PD3) | (1 << PD2); + initLcd(); + printf_P(PSTR("init LCD (")); + if (status == 1) { + printf_P(PSTR("OK)")); + } else { + printf_P(PSTR("ERROR %d)"), status); + } + setBacklightOn(); + } + + void Lcd::cleanup () { + clear(); + PORTB &= ~((1 << PB5) | (1 << PB4) | (1 << PB3) | (1 << PB2) | (1 << PB0)); + PORTC &= ~((1 << PC3) | (1 << PC0)); + PORTD &= ~((1 << PD7) | (1 << PD6) | (1 << PD4) | (1 << PD3) | (1 << PD2)); + DDRB &= ~((1 << PB5) | (1 << PB4) | (1 << PB3) | (1 << PB2) | (1 << PB0)); + DDRC &= ~((1 << PC3) | (1 << PC0)); + DDRD &= ~((1 << PD7) | (1 << PD6) | (1 << PD4) | (1 << PD3) | (1 << PD2)); + } + + void Lcd::setRS () { PORTD |= (1 << PD2); } + void Lcd::clrRS () { PORTD &= ~(1 << PD2); } + void Lcd::setRW () { PORTD |= (1 << PD3); } + void Lcd::clrRW () { PORTD &= ~(1 << PD3); } + void Lcd::setE () { PORTC |= (1 << PC3); } + void Lcd::clrE () { PORTC &= ~(1 << PC3); } + + void Lcd::setData (uint8_t data) { + if (data & 0x01) PORTD |= (1 << PD4); else PORTD &= ~((1 << PD4)); + if (data & 0x02) PORTB |= (1 << PB0); else PORTB &= ~((1 << PB0)); + if (data & 0x04) PORTD |= (1 << PD7); else PORTD &= ~((1 << PD7)); + if (data & 0x08) PORTD |= (1 << PD6); else PORTD &= ~((1 << PD6)); + if (data & 0x10) PORTB |= (1 << PB2); else PORTB &= ~((1 << PB2)); + if (data & 0x20) PORTB |= (1 << PB3); else PORTB &= ~((1 << PB3)); + if (data & 0x40) PORTB |= (1 << PB4); else PORTB &= ~((1 << PB4)); + if (data & 0x80) PORTB |= (1 << PB5); else PORTB &= ~((1 << PB5)); + } + + void Lcd::dataDirectionOut () { + if (!mode4Bit) { + DDRB |= (1 << PB0); + DDRD |= ((1 << PD7) | (1 << PD6) | (1 << PD4)); + } + DDRB |= ((1 << PB5) | (1 << PB4) | (1 << PB3) | (1 << PB2)); + } + + void Lcd::dataDirectionIn () { + if (hardwareVersion == 1) { + // read back not allowed (missing level shifter 5V -> 3.3V) + dataDirectionOut(); + } else { + if (!mode4Bit) { + DDRB &= ~(1 << PB0); + DDRD &= ~((1 << PD7) | (1 << PD6) | (1 << PD4)); + } + DDRB &= ~((1 << PB5) | (1 << PB4) | (1 << PB3) | (1 << PB2)); + } + } + + uint8_t Lcd::getData () { + if (hardwareVersion == 1) { + // read back not allowed (missing level shifter 5V -> 3.3V) + _delay_ms(1); + return 0x00; // bit 8 (busy) = 0 + } else { + uint8_t b = 0; + b |= ((PIND & (1 << PD4)) != 0) << 0; + b |= ((PINB & (1 << PB0)) != 0) << 1; + b |= ((PIND & (1 << PD7)) != 0) << 2; + b |= ((PIND & (1 << PD6)) != 0) << 3; + b |= ((PINB & (1 << PB2)) != 0) << 4; + b |= ((PINB & (1 << PB3)) != 0) << 5; + b |= ((PINB & (1 << PB4)) != 0) << 6; + b |= ((PINB & (1 << PB5)) != 0) << 7; + return b; + } + } + + void Lcd::setBacklightOn () { + PORTC |= (1 << PC0); + } + + void Lcd::setBacklightOff () { + PORTC &= ~(1 << PC0); + } + +#endif + +// Befehle für das Display + +#define DISP_CLEAR 0b00000001 // Display clear +#define DISP_ON 0b00001111 // Display on +#define DISP_OFF 0b00001011 // Display off +#define CURSOR_ON 0b00001111 // Cursor on +#define CURSOR_OFF 0b00001101 // Cursor off +#define BLINK_ON 0b00001111 // Cursor Blink +#define BLINK_OFF 0b00001110 // Cursor No Blink + +#define LCD_PULSE_LENGTH 15 +#define LCD_CMD_DISPLAY_CLEAR 0x01 // Display clear +#define LCD_CMD_CURSOR_HOME 0x02 // Move cursor digit 1 +#define LCD_CMD_SET_ENTRY_MODE 0x04 // Entry Mode Set +#define LCD_CMD_DISPLAY_ON_OFF 0x08 // Display on/off +#define LCD_CMD_SHIFT 0x10 // Display shift +#define LCD_CMD_SET_MODE4BIT 0x20 // 4/8 Bits... +#define LCD_CMD_SET_CGRAM_ADDR 0x40 // Character Generator ROM +#define LCD_CMD_SET_DDRAM_ADDR 0x80 // Display Data RAM +#define LCD_BUSY_FLAG 0x80 + + + +int8_t Lcd::run (uint8_t subtest) { + if (subtest == 0) { + for (uint8_t i = 0; i < 20 * 4; i++) { + char c = (char)(i + 32); + if (i % 20 == 0) { + setCursor(i / 20, 0); + } + putChar(c); + waitOnReady(); + } + printf_P(PSTR("LCD ")); + if (status == 1) { + printf_P(PSTR("OK")); + } else { + printf_P(PSTR("ERROR(%d)"), status); + } + while (wait(1) == EOF) { + } + + } else { + printf_P(PSTR("end")); + return -1; + } + wait(500); + return 0; +} + + +void Lcd::initLcd () { + + setData(0x00); + dataDirectionOut(); + + _delay_ms(16); // min 15ms warten für Reset des Displays + status = 0; + for (uint8_t i = 0; i < 4; i++) { + if (mode4Bit) { + setRegister(LCD_CMD_SET_MODE4BIT | 0x08); // 4 Bit, 2/4 Zeilen, 5x7 + } else { + setRegister(0x08); // 8 Bit, 2 Zeilen, 5x7 + } + if (i == 0) { + _delay_ms(5); + } else { + _delay_us(100); + } + } + + setRegister(LCD_CMD_DISPLAY_ON_OFF | 0x04); // display on, cursor off + if (!isReady(50)) { + status = -1; + return; + } + + setRegister(LCD_CMD_DISPLAY_ON_OFF | 0x04); // display on, cursor off + if (!isReady(50)) { + status = -3; + return; + } + + setRegister(LCD_CMD_DISPLAY_CLEAR); + if (!isReady(1200)) { + status = -4; + return; + } + + status = 1; +} + +void Lcd::setRegister (uint8_t cmd) { + clrRW(); + clrRS(); + writeData(cmd); +} + +void Lcd::writeData (uint8_t data) { + clrE(); + dataDirectionOut(); + if (mode4Bit) { + setData(data & 0xf0); // send High-Nibble + setE(); + _delay_us(LCD_PULSE_LENGTH); + clrE(); + _delay_us(1); + setData(data << 4); // send Low-Nibble + } else { + setData(data); // send data byte + } + setE(); + _delay_us(LCD_PULSE_LENGTH); + clrE(); + _delay_us(1); +} + +void Lcd::setDRAddr (uint8_t address) { + waitOnReady(); + setRegister(LCD_CMD_SET_DDRAM_ADDR | address); + waitOnReady(); +} + +void Lcd::waitOnReady () { + if (isReady(50) == 0) { + status = -6; + } +} + +bool Lcd::isReady (uint16_t us) { + if (status < 0) { + return false; + } + if (hardwareVersion == 1) { + // read back not allowed (missing level shifter) + _delay_ms(1); + return true; + } + + uint8_t busy; + dataDirectionIn(); + setData(0xff); // enable internal pull up + + do { + uint8_t data = 0; + setRW(); + clrRS(); + + _delay_us(1); + setE(); + _delay_us(LCD_PULSE_LENGTH); + data = getData() & 0xf0; // High Nibble + clrE(); + + _delay_us(1); + setE(); + _delay_us(LCD_PULSE_LENGTH); + data |= getData() >> 4; // Low Nibble + + clrE(); + _delay_us(1); + clrRW(); + + busy = data & LCD_BUSY_FLAG; + us = (us >= 11) ? us - 11 : 0; + } while (us > 0 && busy); + + if (status == 1 && busy) { + status = -5; + } + + setData(0x00); + dataDirectionOut(); + + return busy == 0; +} + + +void Lcd::setDisplayOn () { + if (status != 1) { + return; + } + waitOnReady(); + setRegister(LCD_CMD_DISPLAY_ON_OFF | 0x04); // display on + waitOnReady(); +} + +void Lcd::setDisplayOff () { + if (status != 1) { + return; + } + waitOnReady(); + setRegister(LCD_CMD_DISPLAY_ON_OFF); // display off + waitOnReady(); +} + +void Lcd::clear () { + if (status != 1) { + return; + } + waitOnReady(); + setRegister(LCD_CMD_DISPLAY_CLEAR); + waitOnReady(); +} + +void Lcd::setCursor (uint8_t rowIndex, uint8_t columnIndex) { + if (status != 1 || columnIndex >= 20) { + return; + } + uint8_t b; + switch (rowIndex) { + case 0: b = 0x00 + columnIndex; break; + case 1: b = 0x40 + columnIndex; break; + case 2: b = 0x14 + columnIndex; break; + case 3: b = 0x54 + columnIndex; break; + default: return; + } + setDRAddr(b); + waitOnReady(); +} + +void Lcd::putChar (char c) { + if (status != 1) { + return; + } + clrRW(); + setRS(); + writeData(c); + clrRS(); + waitOnReady(); +} + +void Lcd::puts (const char * str) { + while (*str && status == 1) { + putChar(*str++); + } +} \ No newline at end of file diff --git a/software/test-software/src/units/lcd.hpp b/software/test-software/src/units/lcd.hpp new file mode 100644 index 0000000..47a30cc --- /dev/null +++ b/software/test-software/src/units/lcd.hpp @@ -0,0 +1,48 @@ +#ifndef LCD_HPP +#define LCD_HPP + +#include +#include "../main.hpp" +#include + +class Lcd : public TestUnit { + public: + Lcd () { mode4Bit = hardwareVersion == 1 ? false : true; status = 0; }; + virtual void init (); + virtual void cleanup (); + virtual int8_t run (uint8_t subtest); + virtual PGM_P getName () { return PSTR("Lcd"); } + + private: + bool mode4Bit; + int8_t status; + + void initLcd (); + void setRegister (uint8_t cmd); + void setDRAddr (uint8_t address); + void writeData (uint8_t data); + void waitOnReady (); + bool isReady (uint16_t us); + void setDisplayOn (); + void setDisplayOff (); + void clear (); + void setCursor (uint8_t rowIndex, uint8_t columnIndex); + void putChar (char c); + void puts (const char * str); + + void setRS (); + void clrRS (); + void setRW (); + void clrRW (); + void setE (); + void clrE (); + uint8_t getData (); + void setData (uint8_t data); + void dataDirectionIn (); + void dataDirectionOut (); + void setBacklightOn (); + void setBacklightOff (); + +}; + +#endif \ No newline at end of file diff --git a/software/test-software/src/units/led.cpp b/software/test-software/src/units/led.cpp new file mode 100644 index 0000000..e908c21 --- /dev/null +++ b/software/test-software/src/units/led.cpp @@ -0,0 +1,139 @@ +#include +#include +#include + +#include "led.hpp" +#include "../main.hpp" + +#if defined(__AVR_ATmega644P__) || defined(__AVR_ATmega1284P__) + + // Nano-644 / Nano 1284 + // --------------------------------------------------------------- + + // Nano-X-Base V1a V2a + // ----------------------- + // Red PD4 PD7 + // Orange/Yellow PD5 PD6 + // Green PD6 PD5 + // Blue PD7 PD4 + + void Led::init () { + PORTD &= ~((1 << PD7) | (1 << PD6) | (1 << PD5) | (1 << PD4)); + DDRD |= (1 << PD7) | (1 << PD6) | (1 << PD5) | (1 << PD4); + } + + void Led::cleanup () { + DDRD &= ~((1 << PD7) | (1 << PD6) | (1 << PD5) | (1 << PD4)); + PORTD &= ~((1 << PD7) | (1 << PD6) | (1 << PD5) | (1 << PD4)); + } + + void Led::setLed (LED led, bool on) { + if (on) { + switch(led) { + case RED: PORTD |= (1 << PD4); break; + case ORANGE: PORTD |= (1 << PD5); break; + case GREEN: PORTD |= (1 << PD6); break; + case BLUE: PORTD |= (1 << PD7); break; + } + } else { + switch(led) { + case RED: PORTD &= ~(1 << PD4); break; + case ORANGE: PORTD &= ~(1 << PD5); break; + case GREEN: PORTD &= ~(1 << PD6); break; + case BLUE: PORTD &= ~(1 << PD7); break; + } + } + } + + void Led::ledToggle (LED led) { + switch(led) { + case RED: PORTD ^= (1 << PD4); break; + case ORANGE: PORTD ^= (1 << PD5); break; + case GREEN: PORTD ^= (1 << PD6); break; + case BLUE: PORTD ^= (1 << PD7); break; + } + } + +#endif + +#ifdef __AVR_ATmega328P__ + + // Arduino-Nano-5V + // --------------------------------------------------------------- + + // Nano-X-Base V1a V2a + // ----------------------- + // Red PD5 PD2 + // Orange/Yellow PB1 PD3 + // Green PD3 PB1 + // Blue PD2 PD5 + + void Led::init () { + PORTD &= ~((1 << PD5) | (1 << PD3) | (1 << PD2)); + PORTB &= ~(1 << PB1); + DDRD |= (1 << PD5) | (1 << PD3) | (1 << PD2); + DDRB |= (1 << PB1); + } + + void Led::cleanup () { + DDRD &= ~((1 << PD5) | (1 << PD3) | (1 << PD2)); + DDRB &= ~(1 << PB1); + PORTD &= ~((1 << PD5) | (1 << PD3) | (1 << PD2)); + PORTB &= ~(1 << PB1); + } + + void Led::setLed (LED led, bool on) { + if (on) { + switch(led) { + case RED: PORTD |= (1 << PD5); break; + case ORANGE: PORTB |= (1 << PB1); break; + case GREEN: PORTD |= (1 << PD3); break; + case BLUE: PORTD |= (1 << PD2); break; + } + } else { + switch(led) { + case RED: PORTD &= ~(1 << PD5); break; + case ORANGE: PORTB &= ~(1 << PB1); break; + case GREEN: PORTD &= ~(1 << PD3); break; + case BLUE: PORTD &= ~(1 << PD2); break; + } + } + } + + void Led::ledToggle (LED led) { + switch(led) { + case RED: PORTD ^= (1 << PD5); break; + case ORANGE: PORTB ^= (1 << PB1); break; + case GREEN: PORTD ^= (1 << PD3); break; + case BLUE: PORTD ^= (1 << PD2); break; + } + } + +#endif + +void Led::ledOn (LED led) { + setLed(led, true); +} + +void Led::ledOff (LED led) { + setLed(led, false); +} + + +int8_t Led::run (uint8_t subtest) { + if (subtest <= 15) { + subtest = (subtest) % 4; + switch (subtest) { + case 0: ledOff(BLUE); ledOn(RED); break; + case 1: ledOff(RED); ledOn(ORANGE); break; + case 2: ledOff(ORANGE); ledOn(GREEN); break; + case 3: ledOff(GREEN); ledOn(BLUE); break; + } + printf_P(PSTR("Test LED D%d"), subtest + 1); + wait(500); + return 0; + } + + return -1; +} + diff --git a/software/test-software/src/units/led.hpp b/software/test-software/src/units/led.hpp new file mode 100644 index 0000000..780827f --- /dev/null +++ b/software/test-software/src/units/led.hpp @@ -0,0 +1,25 @@ +#ifndef LED_HPP +#define LED_HPP + +#include +#include "../main.hpp" +#include + +class Led : public TestUnit { + public: + enum LED { RED, ORANGE, GREEN, BLUE }; + + public: + Led () {}; + virtual void init (); + virtual void cleanup (); + virtual int8_t run (uint8_t subtest); + virtual PGM_P getName () { return PSTR("Led"); } + + void setLed (LED led, bool on); + void ledOn (LED led); + void ledOff (LED led); + void ledToggle (LED led); +}; + +#endif \ No newline at end of file diff --git a/software/test-software/src/units/modbus.cpp b/software/test-software/src/units/modbus.cpp new file mode 100644 index 0000000..abbe36d --- /dev/null +++ b/software/test-software/src/units/modbus.cpp @@ -0,0 +1,160 @@ +#include +#include +#include + +#include "modbus.hpp" +#include "../main.hpp" + + +#ifdef __AVR_ATmega328P__ +void Modbus::init () {} +void Modbus::cleanup () {} +int8_t Modbus::run (uint8_t subtest) { return -1; } +void Modbus::handleRxByte (uint8_t b) {} +#endif + +#if defined(__AVR_ATmega644P__) || defined(__AVR_ATmega1284P__) + +// PB0 ... nRE .. Read enable +// PB1 ... DE .. Data enable + +#define SET_nRE (PORTB |= (1 << PB0)) +#define CLR_nRE (PORTB &= ~(1 << PB0)) +#define SET_DE (PORTB |= (1 << PB1)) +#define CLR_DE (PORTB &= ~(1 << PB1)) + + +void Modbus::init () { +} + +void Modbus::cleanup () { + enabled = 0; + UCSR1A = 0; + UCSR1B = 0; + UCSR1C = 0; + UBRR1H = 0; + UBRR1L = 0; + PORTD &= ~(1 << PD2); + DDRB &= ~((1 << PB1) | (1 << PB0)); + PORTB &= ~((1 << PB1) | (1 << PB0)); +} + +int8_t Modbus::run (uint8_t subtest) { + if (subtest == 0) { + SET_nRE; + CLR_DE; + DDRB |= (1 << PB1) | (1 << PB0); + + // UART1 interface on Nano-644 + PORTD |= (1 << PD2); // enable RxD1 pullup + UCSR1A = (1 << U2X1); + UCSR1B = (1 << RXCIE1) | (1 << RXEN1) | (1 <= 1 && subtest <= 4) { + uint8_t nre, de, b; + switch (subtest) { + case 1: nre = 1; de = 0; b = 0x01; break; + case 2: nre = 1; de = 1; b = 0x8e; break; + case 3: nre = 0; de = 0; b = 0x55; break; + case 4: nre = 0; de = 1; b = 0xaa; break; + default: return -1; + } + printf_P(PSTR(" DE=%u, nRE=%u send 0x%02x... "), de, nre, b); + if (nre) { + SET_nRE; + } else { + CLR_nRE; + } + if (de) { + SET_DE; + } else { + CLR_DE; + } + _delay_us(100); + receivedBytes = 0; + UDR1 = b; + _delay_ms(1); + if (receivedBytes > 0) { + printf_P(PSTR("0x%02x received"), received[0]); + receivedBytes = 0; + } else { + printf_P(PSTR("no byte received")); + } + printf_P(PSTR(" ... press key to proceed")); + while (wait(0xffffffff) == EOF) {} + CLR_DE; + SET_nRE; + + } else if (subtest == 5) { + static uint8_t frame[] = { 0x01, 0x04, 0x00, 0x00, 0x00, 0x02, 0x71, 0xcb }; + printf_P(PSTR("Modbus: lese Spannung von Eastron SDM-230 (Einphasenzähler)")); + SET_DE; + CLR_nRE; + _delay_us(100); + do { + SET_DE; + receivedBytes = 0; + for (uint8_t i = 0; i < sizeof(frame); i++) { + UCSR1A |= (1 << TXC1); + UDR1 = frame[i]; + while ((UCSR1A & (1 < Sending:")); + for (uint8_t i = 0; i < sizeof(frame); i++) { + printf_P(PSTR(" 0x%02x"), frame[i]); + } + int k = wait(100); + + printf_P(PSTR("\n RxD1:")); + if (receivedBytes == 0) { + printf_P(PSTR("?")); + } else { + for (uint8_t i = 0; i < receivedBytes; i++) { + if (i == sizeof(frame)) { + printf_P(PSTR(" ")); + } + printf_P(PSTR(" 0x%02x"), received[i]); + } + } + if (receivedBytes >= 16) { + union { + uint8_t b[4]; + float value; + } f; + f.b[0] = received[14]; + f.b[1] = received[13]; + f.b[2] = received[12]; + f.b[3] = received[11]; + printf_P(PSTR(" -> %4.8fV\n"), (double)f.value); + } + if (k != EOF) { + break; + } + + } while (wait(1000) == EOF); + + } else { + printf_P(PSTR("end")); + return -1; + } + wait(500); + return 0; +} + +void Modbus::handleRxByte (uint8_t b) { + if (receivedBytes < sizeof(received)) { + received[receivedBytes++] = b; + } +} + +#endif \ No newline at end of file diff --git a/software/test-software/src/units/modbus.hpp b/software/test-software/src/units/modbus.hpp new file mode 100644 index 0000000..44b6a9d --- /dev/null +++ b/software/test-software/src/units/modbus.hpp @@ -0,0 +1,24 @@ +#ifndef MODBUS_HPP +#define MODBUS_HPP + +#include +#include "../main.hpp" +#include + +class Modbus : public TestUnit { + public: + uint8_t enabled; + uint8_t receivedBytes; + uint8_t received[16]; + + + public: + Modbus () { enabled = 0; receivedBytes = 0; } + virtual void init (); + virtual void cleanup (); + virtual int8_t run (uint8_t subtest); + virtual PGM_P getName () { return PSTR("Modbus"); } + void handleRxByte (uint8_t); +}; + +#endif \ No newline at end of file diff --git a/software/test-software/src/units/motor.cpp b/software/test-software/src/units/motor.cpp new file mode 100644 index 0000000..7f8fbf9 --- /dev/null +++ b/software/test-software/src/units/motor.cpp @@ -0,0 +1,221 @@ +#include +#include +#include + +#include "motor.hpp" +#include "../main.hpp" + +#if defined(__AVR_ATmega644P__) || defined(__AVR_ATmega1284P__) + + // Nano-644 + // --------------------------------------------------------------- + // PB0 ..... rotation-sensor + // PB2 ..... nFault + // PB3 ..... PWM (OC0A) + // PB4 ..... EN + // PA3 ..... SW3 -> push button for Motor enable control + + #define ADC0K 64 + + void Motor::init () { + DDRD |= (1 << PD7); // sensor signal toggle on PD7 (LED D4 red) + ADMUX = (1 << ADLAR) | (1 << REFS0); // ADC0, VREF=AVCC=3.3V + ADCSRA = (1 << ADEN) | 7; // ADC Enable, Prescaler 128 + TCCR0A = (1 << COM0A1) | (1 << WGM01) | (1 << WGM00); // Fast PWM on OC0A + // TCCR0B = (1 << CS02) | ( 1 << CS00); // f = 12 MHz / 1024 = 11,71875 kHz -> fPWM=45Hz + TCCR0B = (1 << CS02); // f = 12 MHz / 256 = 46,875 kHz -> fPWM=183,1Hz + DDRB |= (1 << PB4) | (1 << PB3); // Motor enable + PORTA |= (1 << PORTA3); // push button for Motor enable control + setEnable(); + enabled = 1; + } + + void Motor::cleanup () { + DDRD &= ~(1 << PD7); + ADMUX = 0; + ADCSRA = 0; + TCCR0A = 0; + TCCR0B = 0; + DDRB &= ~((1 << PB4) | (1 << PB3)); + PORTA &= ~(1 << PORTA3); + enabled = 0; + } + + bool Motor::isSW3Pressed () { + return (PINA & (1 << PC3)) == 0; + } + + void Motor::clearEnable () { + PORTB &= ~(1 << PB4); + } + + void Motor::setEnable () { + PORTB |= (1 << PB4); + } + + bool Motor::isFaultLow () { + return (PINB & (1 << PB2)) == 0; + } + + bool Motor::isSensorHigh () { + return (PINB & (1 << PB0)) != 0; + } + + void Motor::toggleD4 () { + PORTD ^= (1 << PD7); + } + +#endif + +#ifdef __AVR_ATmega328P__ + + // Arduino-Nano-5V + // --------------------------------------------------------------- + // PD4 ..... rotation-sensor + // PD7 ..... nFault + // PD6/OC0A ..... PWM + // PB2 ..... EN + // PC3 ..... SW3 -> push button for Motor enable control + + #define ADC0K 91 + + void Motor::init () { + DDRD |= (1 << PD2); // sensor signal toggle on PD2 (LED D4 red) + ADMUX = (1 << ADLAR) | (1 << REFS0); // ADC0, VREF=AVCC=5V + ADCSRA = (1 << ADEN) | 7; // ADC Enable, Prescaler 128 + TCCR0A = (1 << COM0A1) | (1 << WGM01) | (1 << WGM00); // Fast PWM on OC0A + // TCCR0B = (1 << CS02) | ( 1 << CS00); // f = 16 MHz / 1024 = 15,625 kHz -> fPWM=61.04Hz + TCCR0B = (1 << CS02); // f = 16 MHz / 256 = 62.5 kHz -> fPWM=244.14Hz + DDRB |= (1 << PB2); + DDRC &= ~(1 << PC3); + DDRD |= (1 << PD6); + DDRD &= ~((1 << PD4) | (1 << PD7)); + PORTC |= ( 1 << PC3); + PORTD |= (1 << PD7) | (1 << PD5); + setEnable(); + enabled = 1; + } + + void Motor::cleanup () { + DDRD &= ~(1 << PD2); + enabled = 0; + ADMUX = 0; + ADCSRA = 0; + TCCR0A = 0; + TCCR0B = 0; + clearEnable(); + DDRB &= ~((1 << PB2)); + DDRC &= ~(1 << PC3); + DDRD &= ~((1 << PD7) | (1 << PD6) | (1 << PD4)); + PORTC &= ~( 1 << PC3); + PORTD &= ~((1 << PD7) | (1 << PD6)); + } + + bool Motor::isSW3Pressed () { + return (PINC & (1 << PC3)) == 0; + } + + void Motor::clearEnable () { + PORTB &= ~(1 << PB2); + } + + void Motor::setEnable () { + PORTB |= (1 << PB2); + } + + bool Motor::isFaultLow () { + return (PIND & (1 << PD7)) == 0; + } + + bool Motor::isSensorHigh () { + return (PIND & (1 << PD4)) != 0; + } + + void Motor::toggleD4 () { + PORTD ^= (1 << PD2); + } + +#endif + +int8_t Motor::run (uint8_t subtest) { + switch (subtest) { + case 0: { + printf_P(PSTR("\n")); + while (wait(10) == EOF) { + + printf_P(PSTR("\r SW3=%d->"), isSW3Pressed() ? 0 : 1); + if (isSW3Pressed()) { + clearEnable(); + printf_P(PSTR("EN=0")); + } else { + setEnable(); + printf_P(PSTR("EN=1")); + } + + ADCSRA |= (1 << ADSC); // start ADC + while (ADCSRA & (1 << ADSC)) {} // wait for result + printf_P(PSTR("\r => ADC0=%3d"), ADCH); + + ADMUX = (1 << ADLAR) | (1 << REFS1) | (1 << REFS0) | 2; // ADC2, VREF=2.5V + + int16_t x = ((int16_t)(ADCH) - 5) * ADC0K / 64; + if (x < 0) x = 0; else if (x > 255) x = 255; + uint8_t dutyCycle = 0xff - (uint8_t)x; + if (dutyCycle <= 1) { + dutyCycle = 0; + } else if (dutyCycle > 254) { + dutyCycle = 255; + } + OCR0A = dutyCycle; + printf_P(PSTR(" PWM/OC0A=%3d"), dutyCycle); + + ADCSRA |= (1 << ADSC); // start ADC + while (ADCSRA & (1 << ADSC)) {} // wait for result + printf_P(PSTR(" ADC2=%3d"), ADCH); + ADMUX = (1 << ADLAR) | (1 << REFS0); // ADC0, VREF=AVCC=3.3V + + printf_P(PSTR(" nFAULT=%d"), isFaultLow() ? 0 : 1); + printf_P(PSTR(" SENSOR=%d "), isSensorHigh()); + uint16_t timer; + ATOMIC_BLOCK(ATOMIC_FORCEON) { + timer = rpmTimer; + } + float rpm = 60.0 / (float)timer / 0.0001; + if (timer > 0) { + printf_P(PSTR(" n= %5d U/min (T=%04x)"), (int)rpm, timer); + } else { + printf_P(PSTR(" no rotation (T=%04x) "), timer); + } + + } + return 0; + } + } + + return -1; +} + +void Motor::tick100us () { + static uint16_t timerH = 0; + static uint16_t timerL = 0; + static bool lastSensorHigh = false; + + bool sensorHigh = isSensorHigh(); + if (!sensorHigh && sensorHigh != lastSensorHigh && timerL > 2) { + rpmTimer = timerL + timerH; + timerL = 0; + timerH = 0; + toggleD4(); + } + if (sensorHigh) { + timerH = timerH < 0x4000 ? timerH + 1 : 0x4000; + } else { + timerL = timerL < 0x4000 ? timerL + 1 : 0x4000; + } + if (timerH >= 0x4000 || timerL >= 0x4000) { + rpmTimer = 0; // no ratation detected + } + lastSensorHigh = sensorHigh; +} + + diff --git a/software/test-software/src/units/motor.hpp b/software/test-software/src/units/motor.hpp new file mode 100644 index 0000000..2cbd15a --- /dev/null +++ b/software/test-software/src/units/motor.hpp @@ -0,0 +1,30 @@ +#ifndef MOTOR_HPP +#define MOTOR_HPP + +#include +#include "../main.hpp" +#include + +class Motor : public TestUnit { + public: + uint8_t enabled; + uint16_t rpmTimer; + + public: + Motor () { enabled = 0; rpmTimer = 0; }; + virtual void init (); + virtual void cleanup (); + virtual int8_t run (uint8_t subtest); + virtual PGM_P getName () { return PSTR("Motor"); } + void tick100us (); + + private: + bool isSW3Pressed (); + void clearEnable (); + void setEnable (); + bool isFaultLow (); + bool isSensorHigh (); + void toggleD4 (); +}; + +#endif \ No newline at end of file diff --git a/software/test-software/src/units/portexp.cpp b/software/test-software/src/units/portexp.cpp new file mode 100644 index 0000000..a153589 --- /dev/null +++ b/software/test-software/src/units/portexp.cpp @@ -0,0 +1,195 @@ +#include +#include +#include + +#include "portexp.hpp" +#include "../main.hpp" + +// Port-Expander MCP23S17 + +// SN-Print Stecker IO16 +// MCP23S17 | IO16 (MEGA2560) | Ampel-Print | | MCP23S17 | IO16 (MEGA2560) | Ampel-Print | +// ------------------------------------------ -------------------------------------------- +// GPA0 | IO16O7 (PA7) | Taster RU | | GPB0 | IO16U7 (PC7) | Taster LU | +// GPA1 | IO16O6 (PA6) | Taster RO | | GPB1 | IO16U6 (PC6) | Taster LO | +// GPA2 | IO16O5 (PA5) | U-Gruen | | GPB2 | IO16U5 (PC5) | R-Gruen | +// GPA3 | IO16O4 (PA4) | U-Gelb | | GPB3 | IO16U4 (PC4) | R-Gelb | +// GPA4 | IO16O3 (PA3) | U-Rot | | GPB4 | IO16U3 (PC3) | R-Rot | +// GPA5 | IO16O2 (PA2) | L-Gruen | | GPB5 | IO16U2 (PC2) | O-Gruen | +// GPA6 | IO16O1 (PA1) | L-Gelb | | GPB6 | IO16U1 (PC1) | O-Gelb | +// GPA7 | IO16O0 (PA0) | L-Rot | | GPB7 | IO16U0 (PC0) | O-Rot | + +#if defined(__AVR_ATmega644P__) || defined(__AVR_ATmega1284P__) + + // Nano-644 / Nano1284 + // -------------------------------------------------------- + // PA7 ... nCS + // PB5 ... MOSI + // PB6 ... MISO + // PB7 ... SCK + + void PortExp::init () { + PRR0 &= (1 << PRSPI); + PORTA |= (1 << PA7); + DDRA |= (1 << PA7); // SPI nCS + // PORTB/DDRB must be configured before SPCR !! + PORTB |= (1 << PB4); // nSS must be HIGH, otherwise SPI master will not become active!! + DDRB |= (1 << PB7) | (1 << PB5) | (1 << PB4); // SPI SCK (=PB7) and SPI MOSI (=PB5) + DDRB &= ~(1 << PB6); + + SPCR |= (1 << SPE) | (1 << MSTR) | (1 << SPR1) | (1 << SPR0); // SPI enable , Master, f=12MHz/128=93,75kHz + // SPCR = (1 << SPE) | (1 << MSTR); // SPI enable , Master, f=12MHz/4 = 3MHz + } + + void PortExp::cleanup () { + DDRB &= ~(1 << PB6); // // SPI MISO (=PB6) + DDRB &= ~((1 << PB7) | (1 << PB5)); // SPI SCK (=PB7) and SPI MOSI (=PB5) + DDRA &= ~(1 << PA7); + PORTA &= ~(1 << PA7); // SPI nCS + SPCR = 0; + } + + void PortExp::setChipEnable () { + PORTA &= ~(1 << PA7); + } + + void PortExp::clearChipEnable () { + PORTA |= (1 << PA7); + } + + +#endif + +#ifdef __AVR_ATmega328P__ + + // Arduino-Nano-5V + // ------------------------------------ + // PC1 ... nCS (MANUAL (!) connection PA1 - PA7 required) + // PB3 ... MOSI + // PB4 ... MISO + // PB5 ... SCK + + void PortExp::init () { + PRR &= (1 << PRSPI); + PORTC |= (1 << PC1); + DDRC |= (1 << PC1); // SPI nCS + // PORTB/DDRB must be configured before SPCR !! + PORTB |= (1 << PB2); // nSS must be HIGH, otherwise SPI master will not become active!! + DDRB |= (1 << PB5) | (1 << PB3) | (1 << PB2); // SPI SCK (=PB5), SPI MOSI (=PB3), SPI nSS (=PB2) + + SPCR |= (1 << SPE) | (1 << MSTR) | (1 << SPR1) | (1 << SPR0); // SPI enable , Master, f=16MHz/128=125kHz + // SPCR = (1 << SPE) | (1 << MSTR); // SPI enable , Master, f=12MHz/4 = 3MHz + + if (hardwareVersion == 2) { + PORTD |= (1 << PD7); // nCS when V2a/JP39.2-3 jumpered + DDRD |= (1 << PD7); + } + } + + void PortExp::cleanup () { + PORTC &= ~(1 << PC1); + DDRC &= ~(1 << PC1); + PORTB &= ~(1 << PB2); + DDRB &= ~((1 << PB5) | (1 << PB3) | (1 << PB2)); + SPCR = 0; + } + + void PortExp::setChipEnable () { + if (hardwareVersion == 2) { + PORTD &= ~(1 << PD7); + } + } + + void PortExp::clearChipEnable () { + if (hardwareVersion == 2) { + PORTD |= (1 << PD7); + } + } + +#endif + + + +int8_t PortExp::writeByte (uint8_t addr, uint8_t b) { + // no response via SPI MISO because SO stays tristate on write cycle + setChipEnable(); + SPDR = 0x40; // WRITE BYTE + while ((SPSR & (1 << SPIF)) == 0) {} + SPDR = addr; // register address + while ((SPSR & (1 << SPIF)) == 0) {} + SPDR = b; // value + while ((SPSR & (1 << SPIF)) == 0) {} + clearChipEnable(); + + _delay_us(5); + return 0; +} + +uint8_t PortExp::readByte (uint8_t addr) { + // response via SPI MISO only on third byte + setChipEnable(); + SPDR = 0x41; // write "READ BYTE" + while ((SPSR & (1 << SPIF)) == 0) {} + SPDR = addr; // write "register address" + while ((SPSR & (1 << SPIF)) == 0) {} + SPDR = 0; // additional 8 clocks to get response from port expander + while ((SPSR & (1 << SPIF)) == 0) {} + clearChipEnable(); + return SPDR; +} + +void PortExp::checkResponse (uint8_t address, uint8_t response, uint8_t desired) { + printf_P(PSTR(" (read 0x%02x -> 0x%02x"), address, response); + if (response != desired) { + printf_P(PSTR(" ERROR")); + if (response == 0xff) { + printf_P(PSTR(" JP39.2/3 jumpered (left)?")); + } + } else { + printf_P(PSTR(" OK")); + } + printf_P(PSTR(")")); +} + + +int8_t PortExp::run (uint8_t subtest) { + #ifdef __AVR_ATmega328P__ + if (hardwareVersion == 1) { + printf_P(PSTR("ERROR - nCS not controlable\n")); + return -1; + } + #endif + if (subtest == 0) { + while (wait(500) == EOF) { + printf_P(PSTR("\n => start ...")); + for (uint8_t i = 0; i < 8; i++) { + writeByte(0, ~(1 << i)); // IODIRA (Bank = 0) + writeByte(0x12, (1 << i)); // GPIOA (Bank = 0) + printf_P(PSTR("\n Bank0 - GPA%d = 1"), i); + checkResponse (0x12, readByte(0x12), (1 << i)); + wait(200); + writeByte(0x12, 0); // GPIOA (Bank = 0) + printf_P(PSTR("\n Bank0 - GPA%d = 0"), i); + checkResponse (0x12, readByte(0x12), 0); + writeByte(0, 0xff); // IODIRA (Bank = 0) + wait(200); + } + for (uint8_t i = 0; i < 8; i++) { + writeByte(1, ~(1 << i)); // IODIRB (Bank = 0) + writeByte(0x13, (1 << i)); // GPIOB (Bank = 0) + printf_P(PSTR("\n Bank0 - GPB%d = 1"), i); + checkResponse (0x13, readByte(0x13), (1 << i)); + wait(200); + writeByte(0x13, 0); // GPIOB (Bank = 0) + printf_P(PSTR("\n Bank0 - GPB%d = 0"), i); + checkResponse (0x13, readByte(0x13), 0); + writeByte(1, 0xff); // IODIRB (Bank = 0) + wait(200); + } + } + return 0; + } + + return -1; +} + diff --git a/software/test-software/src/units/portexp.hpp b/software/test-software/src/units/portexp.hpp new file mode 100644 index 0000000..8705662 --- /dev/null +++ b/software/test-software/src/units/portexp.hpp @@ -0,0 +1,24 @@ +#ifndef PORTEXP_HPP +#define PORTEXP_HPP + +#include +#include "../main.hpp" +#include + +class PortExp : public TestUnit { + public: + PortExp () {}; + virtual void init (); + virtual void cleanup (); + virtual int8_t run (uint8_t subtest); + virtual PGM_P getName () { return PSTR("PortExp"); } + + private: + void setChipEnable (); + void clearChipEnable (); + int8_t writeByte (uint8_t addr, uint8_t b); + uint8_t readByte (uint8_t addr); + void checkResponse (uint8_t address, uint8_t response, uint8_t desired); +}; + +#endif \ No newline at end of file diff --git a/software/test-software/src/units/poti.cpp b/software/test-software/src/units/poti.cpp new file mode 100644 index 0000000..94fc5a4 --- /dev/null +++ b/software/test-software/src/units/poti.cpp @@ -0,0 +1,34 @@ +#include +#include + +#include "poti.hpp" +#include "../main.hpp" + +void Poti::init () { + ADMUX = (1 << REFS0); // ADC0, VREF=AVCC=3.3V + ADCSRA = (1 << ADEN) | 7; // ADC Enable, Prescaler 128 +} + +void Poti::cleanup () { + ADMUX = 0; + ADCSRA = 0; +} + +int8_t Poti::run (uint8_t subtest) { + switch (subtest) { + case 0: { + printf_P(PSTR("\n")); + while (wait(10) == EOF) { + printf_P(PSTR("\r => Measure ADC0: ")); + ADCSRA |= (1 << ADSC); // start ADC + while (ADCSRA & (1 << ADSC)) {} // wait for result + printf_P(PSTR("%4d (0x%03x)"), ADC, ADC); + } + return 0; + } + } + + return -1; +} + + diff --git a/software/test-software/src/units/poti.hpp b/software/test-software/src/units/poti.hpp new file mode 100644 index 0000000..b13dd29 --- /dev/null +++ b/software/test-software/src/units/poti.hpp @@ -0,0 +1,17 @@ +#ifndef POTI_HPP +#define POTI_PP + +#include +#include "../main.hpp" +#include + +class Poti : public TestUnit { + public: + Poti () {}; + virtual void init (); + virtual void cleanup (); + virtual int8_t run (uint8_t subtest); + virtual PGM_P getName () { return PSTR("Poti"); } +}; + +#endif \ No newline at end of file diff --git a/software/test-software/src/units/r2r.cpp b/software/test-software/src/units/r2r.cpp new file mode 100644 index 0000000..54664b1 --- /dev/null +++ b/software/test-software/src/units/r2r.cpp @@ -0,0 +1,48 @@ +#include +#include + +#include "r2r.hpp" +#include "../main.hpp" + +#if defined(__AVR_ATmega644P__) || defined(__AVR_ATmega1284P__) + // AVCC=3.3, POTI Vmax=3.3V + #define K 1.0 +#endif + +#ifdef __AVR_ATmega328P__ + // AVCC=4.7V, POTI Vmax=3.3V + #define K 1.32 +#endif + +void R2r::init () { + ADMUX = (1 << REFS0) | 2; // ADC2, VREF=AVCC (=3.3V for Nano-644/1284, =5V for Arduino Nano) + ADCSRA = (1 << ADEN) | 7; // ADC Enable, Prescaler 128 +} + +void R2r::cleanup () { + ADMUX = 0; + ADCSRA = 0; +} + +int8_t R2r::run (uint8_t subtest) { + switch (subtest) { + case 0: { + printf_P(PSTR("\n")); + while (wait(10) == EOF) { + printf_P(PSTR("\r => Measure ADC2: ")); + ADCSRA |= (1 << ADSC); // start ADC + while (ADCSRA & (1 << ADSC)) {} // wait for result + printf_P(PSTR("%4d (0x%03x)"), ADC, ADC); + // uint8_t sw = (uint8_t)( ((float)(ADC) + 4.0) / 64.0 * K ); + float swf = ((float)(ADC) * K) / 64.8 + 0.55; + uint8_t sw = (uint8_t)swf ; + printf_P(PSTR(" %3.1f => SW9:6 = %d %d% d %d "), swf, sw >> 3, (sw >> 2) & 0x01, (sw >> 1) & 0x01, sw & 0x01 ); + } + return 0; + } + } + + return -1; +} + + diff --git a/software/test-software/src/units/r2r.hpp b/software/test-software/src/units/r2r.hpp new file mode 100644 index 0000000..84e97e6 --- /dev/null +++ b/software/test-software/src/units/r2r.hpp @@ -0,0 +1,17 @@ +#ifndef R2R_HPP +#define R2R_PP + +#include +#include "../main.hpp" +#include + +class R2r : public TestUnit { + public: + R2r () {}; + virtual void init (); + virtual void cleanup (); + virtual int8_t run (uint8_t subtest); + virtual PGM_P getName () { return PSTR("R2R"); } +}; + +#endif \ No newline at end of file diff --git a/software/test-software/src/units/rgb.cpp b/software/test-software/src/units/rgb.cpp new file mode 100644 index 0000000..3f69cbd --- /dev/null +++ b/software/test-software/src/units/rgb.cpp @@ -0,0 +1,152 @@ +#include +#include + +#include "rgb.hpp" +#include "../main.hpp" + +#if defined(__AVR_ATmega644P__) || defined(__AVR_ATmega1284P__) + + // Nano-644 + // --------------------------------------------------------------- + // PB0 ..... Red (inverse logic -> 0 = ON) + // PB1 ..... Green (inverse logic -> 0 = ON) + // PB2 ..... Blue (inverse logic -> 0 = ON) + + void Rgb::init () { + ledOff(RED); + ledOff(GREEN); + ledOff(BLUE); + DDRB |= (1 << PB2) | (1 << PB1) | (1 << PB0); + } + + void Rgb::cleanup () { + ledOff(RED); + ledOff(GREEN); + ledOff(BLUE); + DDRB &= ~((1 << PB2) | (1 << PB1) | (1 << PB0)); + } + + void Rgb::setLed (LED led, bool on) { + if (on) { + switch(led) { + case RED: PORTB &= ~(1 << PB0); break; + case GREEN: PORTB &= ~(1 << PB1); break; + case BLUE: PORTB &= ~(1 << PB2); break; + } + } else { + switch(led) { + case RED: PORTB |= (1 << PB0); break; + case GREEN: PORTB |= (1 << PB1); break; + case BLUE: PORTB |= (1 << PB2); break; + } + } + } + + void Rgb::ledToggle (LED led) { + switch(led) { + case RED: PORTB ^= (1 << PB0); break; + case GREEN: PORTB ^= (1 << PB1); break; + case BLUE: PORTB ^= (1 << PB2); break; + } + } + + +#endif + +#ifdef __AVR_ATmega328P__ + + // Arduino-Nano-5V + // --------------------------------------------------------------- + // PD4 ..... Red (inverse logic -> 0 = ON) + // PB0 ..... Green (inverse logic -> 0 = ON) + // PD7 ..... Blue (inverse logic -> 0 = ON) + + void Rgb::init () { + ledOff(RED); + ledOff(GREEN); + ledOff(BLUE); + DDRB |= (1 << PB0); + DDRD |= (1 << PD7) | (1 << PD4); + } + + void Rgb::cleanup () { + ledOff(RED); + ledOff(GREEN); + ledOff(BLUE); + DDRB &= ~(1 << PB0); + DDRD &= ~((1 << PD7) | (1 << PD4)); + } + + void Rgb::setLed (LED led, bool on) { + if (on) { + switch (led) { + case RED: PORTD &= ~(1 << PD4); break; + case GREEN: PORTB &= ~(1 << PB0); break; + case BLUE: PORTD &= ~(1 << PD7); break; + } + } else { + switch (led) { + case RED: PORTD |= (1 << PD4); break; + case GREEN: PORTB |= (1 << PB0); break; + case BLUE: PORTD |= (1 << PD7); break; + } + } + } + + void Rgb::ledToggle (LED led) { + switch (led) { + case RED: PORTD ^= (1 << PD4); break; + case GREEN: PORTB ^= ~(1 << PB0); break; + case BLUE: PORTD ^= (1 << PD7); break; + } + } + +#endif + +void Rgb::ledOn (LED led) { + setLed(led, true); +} + +void Rgb::ledOff (LED led) { + setLed(led, false); +} + +int8_t Rgb::run (uint8_t subtest) { + switch (subtest) { + case 0: { + ledOn(RED); + printf_P(PSTR("Red")); + wait(3000); + ledOff(RED); + return 0; + } + + case 1: { + ledOn(GREEN); + printf_P(PSTR("Green")); + wait(3000); + ledOff(GREEN); + return 0; + } + + case 2: { + ledOn(BLUE); + printf_P(PSTR("Blue")); + wait(3000); + ledOff(BLUE); + return 0; + } + + case 3: { + ledOn(RED); ledOn(GREEN); ledOn(BLUE); + printf_P(PSTR("All")); + wait(3000); + ledOff(RED); ledOff(GREEN); ledOff(BLUE); + return 0; + } + } + + return -1; +} + + diff --git a/software/test-software/src/units/rgb.hpp b/software/test-software/src/units/rgb.hpp new file mode 100644 index 0000000..12e9da4 --- /dev/null +++ b/software/test-software/src/units/rgb.hpp @@ -0,0 +1,25 @@ +#ifndef RGB_HPP +#define RGB_PP + +#include +#include "../main.hpp" +#include + +class Rgb : public TestUnit { + public: + enum LED { RED, GREEN, BLUE }; + + public: + Rgb () {}; + virtual void init (); + virtual void cleanup (); + virtual int8_t run (uint8_t subtest); + virtual PGM_P getName () { return PSTR("Rgb"); } + + void setLed (LED led, bool on); + void ledOn (LED led); + void ledOff (LED led); + void ledToggle (LED led); +}; + +#endif \ No newline at end of file diff --git a/software/test-software/src/units/rtc8563.cpp b/software/test-software/src/units/rtc8563.cpp new file mode 100644 index 0000000..6ae6497 --- /dev/null +++ b/software/test-software/src/units/rtc8563.cpp @@ -0,0 +1,253 @@ +#include +#include +#include +#include +#include + +#include "rtc8563.hpp" +#include "../adafruit/bme280.h" +#include "../main.hpp" + +// RTC BME653EMA on Nano-644 + +const char PSTR_WEEKDAYS[] PROGMEM = "So\0Mo\0Di\0Mi\0Do\0Fr\0Sa\0"; + +// const uint8_t CONFIG[] PROGMEM = { +// /* config -> */ 0x00, 0x00, +// /* enable nINT -> */ 0x01, 0x01, // TIE = 1 +// /* set clock -> */ 0x02, 0x00, 0x00, 0x08, 0x16, 0x02, 0x08, 0x24 +// }; + +void Rtc8563::handleTwiIrq () { + TWCR |= (1 << TWINT); // clear Interrupt Request +} + +#ifdef __AVR_ATmega328P__ +void Rtc8563::init () {} +void Rtc8563::cleanup () {} +int8_t Rtc8563::run (uint8_t subtest) { return -1; } +PGM_P Rtc8563::getName () { return PSTR("?"); } +#endif + +#if defined(__AVR_ATmega644P__) || defined(__AVR_ATmega1284P__) + +void Rtc8563::init () { + PORTC &= ~(1 << PC7); // nInt and nPowerOn (Q7 -> BATTERY) + DDRC |= (1 << PC7); + TWBR = 13; // 100kHz (TWPS1:0 = 00), TWBR = (F_CPU - 16 * 100000) / (2 * 100000 * 4); + TWBR = 28; // 50kHz (TWPS1:0 = 00), TWBR = (F_CPU - 16 * 50000) / (2 * 50000 * 4); + TWBR = 100; // 50kHz (TWPS1:0 = 00), TWBR = (F_CPU - 16 * 50000) / (2 * 50000 * 4); + TWCR = (1 << TWEN); + enabled = true; +} + +void Rtc8563::cleanup () { + enabled = false; + TWCR = (1 << TWEN); + TWBR = 0; + // PORTC &= ~(1 << PC7); + // DDRC &= ~(1 << PC7); +} + +PGM_P Rtc8563::getName () { + return PSTR("RTC-8563"); +} + +uint8_t Rtc8563::bcd2bin (uint8_t value) { + return (value >> 4) * 10 + (value & 0x0f); +} + +int8_t Rtc8563::run (uint8_t subtest) { + int key = EOF; + if (subtest == 0) { + // printf_P(PSTR(" BM280 ... ")); + rtc8563.begin(0x51); + Clock_t clock; + // uint8_t bufferConfig[] = { 0x00, 0x00, 0x01, 0x01 }; + // uint8_t bufferSetClock[] = { 0x02, 0x00, 0x00, 0x08, 0x16, 0x02, 0x08, 0x24 }; + + Rtc8563Reg_t reg; + uint8_t *pReg = (uint8_t *)(void *)® + memset(®, 0, sizeof(reg)); + + printf_P(PSTR("\n => config 8563 ... ")); + reg.reg1.field.tie = 1; + if (!rtc8563.write(pReg, 2)) { + printf_P(PSTR_ERROR); + } else { + printf_P(PSTR_Done); + } + printf_P(PSTR("\n press:")); + printf_P(PSTR("\n t .... timer on/off")); + printf_P(PSTR("\n p .... power on/off (PC7->Q1)")); + printf_P(PSTR("\n c .... init clock")); + printf_P(PSTR("\n w/W .. weekday (+/-)\n")); + printf_P(PSTR("\n y/Y .. year (+/-)")); + printf_P(PSTR("\n m/M .. month (+/-)")); + printf_P(PSTR("\n d/D .. day (+/-)")); + printf_P(PSTR("\n h/H .. hour (+/-)")); + printf_P(PSTR("\n n/N .. minute (+/-)")); + printf_P(PSTR("\n s/S .. second (+/-)\n")); + + do { + uint8_t addr = 0x00; + printf_P(PSTR("\n => read register 0-15 (hex):")); + if (!rtc8563.write_then_read(&addr, 1, pReg, sizeof(reg))) { + printf_P(PSTR_ERROR); + key = waitAndReadKey(1000); + continue; + } + memccpy(&clock, ®.clock, sizeof(clock), sizeof(clock)); + for (uint8_t i = 0; i < 16; i++) { + if (i % 4 == 0) { + printf_P(PSTR(" ")); + } + printf_P(PSTR(" %02X"), pReg[i]); + } + uint16_t year = (clock.month.field.century ? 2100 : 2000) + bcd2bin(clock.year.byte); + int8_t month = bcd2bin(clock.month.byte); + int8_t day = bcd2bin(clock.day.byte); + int8_t hrs = bcd2bin(clock.hour.byte); + int8_t min = bcd2bin(clock.min.byte); + int8_t sec = bcd2bin(clock.sec.byte); + int8_t weekday = clock.weekday.byte; + + PGM_P d = weekday >= 0 && weekday < 7 ? &PSTR_WEEKDAYS[weekday * 3] : PSTR("??"); + printf_P(PSTR(" --> ")); + printf_P(d); + printf_P(PSTR(", %d %04d-%02d-%02d %02d:%02d:%02d"), weekday, year, month, day, hrs, min, sec); + printf_P(PSTR(" - Timer=0x%02x"), reg.timer); + + key = waitAndReadKey(1000); + bool ok = true; + bool change = false; + switch (key) { + case 't': ok &= setTimer(reg.timerControl.field.enable ? 0 : 10, reg); break; + case 'c': ok &= setClock(5, 2024,8,16, 17,12,10 ); break; // ok &= rtc8563.write(bufferSetClock, sizeof(bufferSetClock)); break; + case 'p': powerOnOff(10, reg); break; + case 'y': year++; change = true; break; + case 'Y': year--; change = true; break; + case 'm': month++; change = true; break; + case 'M': month--; change = true; break; + case 'd': day++; change = true; break; + case 'D': day--; change = true; break; + case 'h': hrs++; change = true; break; + case 'H': hrs--; change = true; break; + case 'n': min++; change = true; break; + case 'N': min--; change = true; break; + case 's': sec++; change = true; break; + case 'S': sec--; change = true; break; + case 'w': weekday++; change = true; break; + case 'W': weekday--; change = true; break; + } + if (change) { + printf_P(PSTR("\n set: %04d-%02d-%02d %02d:%02d:%02d"), year, month, day, hrs, min, sec); + setClock(weekday, year, month, day, hrs, min, sec); + } + + } while (key != ESCAPE); + + return 0; + } + + return -1; +} + +bool Rtc8563::setTimer (uint8_t seconds, Rtc8563Reg_t ®) { + + reg.timerControl.field.fd = FDTIMER_1HZ; + reg.timerControl.field.enable = seconds > 0; + reg.timer = seconds; + reg.reg1.field.tie = seconds > 0; + // clear and alarm flag behavior on I2C write different to datasheet + // datasheet: tf cleared to 0, af remains unchanged + // realchip: tf remains 1 and af is set to 1 (no negative result because tie=0 and aie=0) + reg.reg1.field.tf = 0; // clear timer flag + reg.reg1.field.af = 1; // alarm flag remains unchanged + if (seconds > 0) { + printf_P(PSTR("\n Timer set to %ds (1:%02X) ... "), seconds, reg.reg1.byte); + } else { + printf_P(PSTR("\n Timer off ... ")); + } + if (rtc8563.writeByteAndBuffer(0x01, ®.reg1.byte, 1) && rtc8563.writeByteAndBuffer(14, ®.timerControl.byte, 2)) { + printf_P(PSTR("OK")); + return true; + } else { + printf_P(PSTR("fails")); + return false; + } +} + +bool Rtc8563::powerOnOff (uint8_t delayOffSeconds, Rtc8563Reg_t ®) { + int key = EOF; + if (PORTC & (1 << PC7)) { + printf_P(PSTR("\n power on ...")); + DDRC |= ( 1<< PC7); + PORTC &= ~(1 << PC7); + setTimer(0, reg); + } else { + printf_P(PSTR("\n")); + key = EOF; + for (int8_t i = 9; i > 0 && key == EOF; i--) { + printf_P(PSTR("\r press ESC to abort, power off in %ds (press key to skip timer) "), i); + key = waitAndReadKey(1000); + } + if (key == ESCAPE) { + return true; + } + setTimer(10, reg); + reg.reg1.field.af = 1; // alarm flag remains unchanged + reg.reg1.field.tf = 0; // timer flag clear + reg.reg1.field.tie = 1; // enable timer interrupt + rtc8563.writeByteAndBuffer(0x01, ®.reg1.byte, 1); + printf_P(PSTR("\n power off now ...")); + DDRC |= ( 1<< PC7); + PORTC |= (1 << PC7); + _delay_ms(5); + DDRC &= ~( 1<< PC7); + PORTC &= ~(1 << PC7); + waitAndReadKey(5000); + printf_P(PSTR("power off fails, I am still alive :-) ... proceed")); + } + return true; +} + + +bool Rtc8563::setClock (int8_t weekday, uint16_t year, int8_t month, int8_t day, int8_t hour, int8_t min, int8_t sec) { + Clock_t clock; + clock.month.field.century = (year < 2000 || year > 2100) ? 1: 0; + uint8_t y = year % 100; clock.year.field.bcdL = y % 10; clock.year.field.bcdH = y / 10; + + while (weekday < 0) { weekday += 7; } + while (weekday > 6) { weekday -= 7; } + clock.weekday.field.bcdL = weekday; + + while (month < 1) { month += 12; } + while (month > 12) { month -= 12; } + clock.month.field.bcdL = month % 10; clock.month.field.bcdH = month / 10; + + while (day < 1) { day += 31; } + while (day > 31) { day -= 31; } + clock.day.field.bcdL = day % 10; clock.day.field.bcdH = day / 10; + + while (hour < 0) { hour += 24; } + while (hour > 23) { hour -= 24; } + clock.hour.field.bcdL = hour % 10; clock.hour.field.bcdH = hour / 10; + + while (min < 0) { min += 60; } + while (min > 59) { min -= 60; } + clock.min.field.bcdL = min % 10; clock.min.field.bcdH = min / 10; + + while (sec < 0) { sec += 60; } + while (sec > 59) { sec -= 60; } + clock.sec.field.bcdL = sec % 10; clock.sec.field.bcdH = sec / 10; + + printf_P(PSTR("\n %p %p %p %p %p %p %p -> write: "), &clock.sec.byte, &clock.min.byte, &clock.hour.byte, &clock.day.byte, &clock.weekday.byte, &clock.month.byte, &clock.year.byte ); + for (uint8_t i = 0; i < sizeof(clock); i++) { + printf_P(PSTR(" %02x"), ((uint8_t *)(void *)&clock)[i]); + } + return rtc8563.writeByteAndBuffer(0x02, (uint8_t *)(void *)&clock, sizeof(clock)); +} + +#endif + diff --git a/software/test-software/src/units/rtc8563.hpp b/software/test-software/src/units/rtc8563.hpp new file mode 100644 index 0000000..ca79713 --- /dev/null +++ b/software/test-software/src/units/rtc8563.hpp @@ -0,0 +1,70 @@ +#ifndef RTC8563_HPP +#define RTC8563_HPP + +#include +#include "../main.hpp" +#include "../adafruit/bme280.h" +#include "../adafruit/ens160.h" +#include "../i2cmaster.hpp" +#include "../i2cslave.hpp" + + +class Rtc8563 : public TestUnit { + public: + typedef enum { NORMAL } Rtc8563Mode_t; + + private: + I2cMaster rtc8563; + typedef enum { FDCLOCKOUT_32768HZ = 0, FDCLOCKOUT_1024HZ = 1, FDCLOCKOUT_32HZ = 2, FDCLOCKOUT_1HZ = 3 } FDCLOCKOUT_t; + typedef enum { FDTIMER_4096HZ = 0, FDTIMER_64HZ = 1, FDTIMER_1HZ = 2, FDTIMER_1D60HZ = 3 } FDTIMER_t; + typedef struct { + union { uint8_t byte; struct { uint8_t bcdL:4; uint8_t bcdH:3; uint8_t voltageLow:1; } field; } sec; + union { uint8_t byte; struct { uint8_t bcdL:4; uint8_t bcdH:3; uint8_t notUsed:1; } field; } min; + union { uint8_t byte; struct { uint8_t bcdL:4; uint8_t bcdH:2; uint8_t notUsed:2; } field; } hour; + union { uint8_t byte; struct { uint8_t bcdL:4; uint8_t bcdH:2; uint8_t notUsed:2; } field; } day; + union { uint8_t byte; struct { uint8_t bcdL:3; } field; uint8_t notUsed:5; } weekday; + union { uint8_t byte; struct { uint8_t bcdL:4; uint8_t bcdH:1; uint8_t notUsed:2; uint8_t century:1; } field; } month; + union { uint8_t byte; struct { uint8_t bcdL:4; uint8_t bcdH:4; } field; } year; + } Clock_t; // identical to 8563 register 2..8 + + typedef struct { + union { uint8_t byte; struct { uint8_t bcdL:4; uint8_t bcdH:3; uint8_t enable:1; } field; } min; + union { uint8_t byte; struct { uint8_t bcdL:4; uint8_t bcdH:2; uint8_t notUsed:1; uint8_t enable:1; } field; } hour; + union { uint8_t byte; struct { uint8_t bcdL:4; uint8_t bcdH:2; uint8_t notUsed:1; uint8_t enable:1; } field; } day; + union { uint8_t byte; struct { uint8_t bcdL:3; } field; uint8_t notUsed:4; uint8_t enable_W:1; } weekday; + } Alarm_t; + + typedef struct { + union { uint8_t byte; struct { uint8_t notUsed210:3; uint8_t testC:1; uint8_t notUsed4:1; uint8_t stop:1; uint8_t notUsed6:1; uint8_t test1:1; } field; } reg0; + union { uint8_t byte; struct { uint8_t tie:1; uint8_t aie:1; uint8_t tf:1; uint8_t af:1; uint8_t ti_tp:1; uint8_t notUsed:3; } field; } reg1; + Clock_t clock; + Alarm_t alarm; + union { uint8_t byte; struct { FDCLOCKOUT_t fd:2; uint8_t notUsed65432:5; uint8_t enable:1; } field; } clockoutControl; + union { uint8_t byte; struct { FDTIMER_t fd:2; uint8_t notUsed65432:5; uint8_t enable:1; } field; } timerControl; + uint8_t timer; + } Rtc8563Reg_t; + + + public: + bool enabled; + + public: + Rtc8563 (Rtc8563Mode_t mode) { enabled = false; this->mode = mode; } + void tick1ms () { rtc8563.tick1ms(); } + virtual void init (); + virtual void cleanup (); + virtual int8_t run (uint8_t subtest); + virtual PGM_P getName (); + void handleTwiIrq (); + + private: + Rtc8563Mode_t mode; + uint8_t bcd2bin (uint8_t value); + int8_t runModeNormal (uint8_t subtest); + int8_t runModeBattery (uint8_t subtest); + bool setTimer (uint8_t seconds, Rtc8563Reg_t ®); + bool powerOnOff (uint8_t delayOffSeconds, Rtc8563Reg_t ®); + bool setClock (int8_t weekday, uint16_t year, int8_t month, int8_t day, int8_t hour, int8_t min, int8_t sec); +}; + +#endif \ No newline at end of file diff --git a/software/test-software/src/units/seg7.cpp b/software/test-software/src/units/seg7.cpp new file mode 100644 index 0000000..f522456 --- /dev/null +++ b/software/test-software/src/units/seg7.cpp @@ -0,0 +1,283 @@ +#include +#include + +#include "seg7.hpp" +#include "../main.hpp" + +#if defined(__AVR_ATmega644P__) || defined(__AVR_ATmega1284P__) + + // Nano-644 / Nano 1284 + // --------------------------------------------------------------- + + // Nano-X-Base V1a V2a + // --------------------------------- + // Anode Segment A ....... PB0 PB0 + // Anode Segment B ....... PB1 PB1 + // Anode Segment C ....... PB2 PB2 + // Anode Segment D ....... PB3 PB3 + // Anode Segment E ....... PB4 PB4 + // Anode Segment F ....... PB5 PB5 + // Anode Segment G ....... PB6 PB6 + // Anode DP .............. PB7 PB7 + // --------------------------------- + // Cathode Char 1 ........ PA0 PD4 + // Cathode Char 2 ........ PA1 PD5 + // Cathode Char 3 ........ PA2 PD6 + // Cathode Char 4 ........ PA3 PD7 + // --------------------------------- + // nOE (Output Enable) ... PD5 PA1 + // Anode L1:2 ............ PD6 PA2 + // Anode L3 .............. PD7 PA3 + +void Seg7::init () { + setAnodes(0); + setCathodes(0); + DDRB = 0xff; + switch (hardwareVersion) { + case 1: { + DDRA |= (1 << PA3) | (1 << PA2) | (1 << PA1) | (1 << PA0); + DDRD |= (1 << PD7) | (1 << PD6) | (1 << PD5); + break; + } + case 2: { + DDRA |= (1 << PA3) | (1 << PA2) | (1 << PA1); + DDRD |= (1 << PD7) | (1 << PD6) | (1 << PD5) | (1 << PD4); + } + break; + } + } + + void Seg7::cleanup () { + setAnodes(0); + setCathodes(0); + DDRB = 0x00; + switch (hardwareVersion) { + case 1: { + DDRA &= ~((1 << PA3) | (1 << PA2) | (1 << PA1) | (1 << PA0)); + DDRD &= ~((1 << PD7) | (1 << PD6) | (1 << PD5)); + break; + } + case 2: { + DDRA &= ~((1 << PA3) | (1 << PA2) | (1 << PA1)); + DDRD &= ~((1 << PD7) | (1 << PD6) | (1 << PD5) | (1 << PD4)); + } + break; + } + } + + void Seg7::setAnodes (uint16_t a) { + if (a & 0x0001) PORTB |= (1 << PB0); else PORTB &= ~(1 << PB0); // Anode Char A + if (a & 0x0002) PORTB |= (1 << PB1); else PORTB &= ~(1 << PB1); // Anode Char B + if (a & 0x0004) PORTB |= (1 << PB2); else PORTB &= ~(1 << PB2); // Anode Char C + if (a & 0x0008) PORTB |= (1 << PB3); else PORTB &= ~(1 << PB3); // Anode Char D + if (a & 0x0010) PORTB |= (1 << PB4); else PORTB &= ~(1 << PB4); // Anode Char E + if (a & 0x0020) PORTB |= (1 << PB5); else PORTB &= ~(1 << PB5); // Anode Char F + if (a & 0x0040) PORTB |= (1 << PB6); else PORTB &= ~(1 << PB6); // Anode Char G + if (a & 0x0080) PORTB |= (1 << PB7); else PORTB &= ~(1 << PB7); // Anode Char DP + switch (hardwareVersion) { + case 1: { + if (a & 0x0100) PORTD |= (1 << PD6); else PORTD &= ~(1 << PD6); // Anode L1/L2 + if (a & 0x0200) PORTD |= (1 << PD7); else PORTD &= ~(1 << PD7); // Anode L3 + break; + } + case 2: { + if (a & 0x0100) PORTA |= (1 << PA2); else PORTA &= ~(1 << PA2); // Anode L1/L2 + if (a & 0x0200) PORTA |= (1 << PA3); else PORTA &= ~(1 << PA3); // Anode L3 + break; + } + } + } + + void Seg7::setCathodes (uint8_t c) { + switch (hardwareVersion) { + case 1: { + if (c & 0x01) PORTA |= (1 << PA0); else PORTA &= ~(1 << PA0); // Chathode Char 1 (most left) + if (c & 0x02) PORTA |= (1 << PA1); else PORTA &= ~(1 << PA1); // Chathode Char 2 + if (c & 0x04) PORTA |= (1 << PA2); else PORTA &= ~(1 << PA2); // Chathode Char 3 + if (c & 0x08) PORTA |= (1 << PA3); else PORTA &= ~(1 << PA3); // Chathode Char 4 (most right) + break; + } + case 2: { + if (c & 0x01) PORTD |= (1 << PD4); else PORTD &= ~(1 << PD4); // Chathode Char 1 (most left) + if (c & 0x02) PORTD |= (1 << PD5); else PORTD &= ~(1 << PD5); // Chathode Char 2 + if (c & 0x04) PORTD |= (1 << PD6); else PORTD &= ~(1 << PD6); // Chathode Char 3 + if (c & 0x08) PORTD |= (1 << PD7); else PORTD &= ~(1 << PD7); // Chathode Char 4 (most right) + break; + } + } + } + + void Seg7::setOE (bool enabled) { + switch (hardwareVersion) { + case 1: if (enabled) PORTD &= ~(1 << PD5); else PORTD |= (1 << PD5); break; + case 2: if (enabled) PORTA &= ~(1 << PA1); else PORTA |= (1 << PA1); break; + } + } + +#endif + +#ifdef __AVR_ATmega328P__ + + // Arduino-Nano-5V + // --------------------------------------------------------------- + + // Nano-X-Base V1a V2a + // --------------------------------- + // Anode Segment A ....... PD4 PD4 + // Anode Segment B ....... PB0 PB0 + // Anode Segment C ....... PD7 PD7 + // Anode Segment D ....... PD6 PD6 + // Anode Segment E ....... PB2 PB2 + // Anode Segment F ....... PB3 PB3 + // Anode Segment G ....... PB4 PB4 + // Anode DP .............. PB5 PB5 + // --------------------------------- + // Cathode Char 1 ........ PC0 PD5 + // Cathode Char 2 ........ PC1 PB1 + // Cathode Char 3 ........ PC2 PD3 + // Cathode Char 4 ........ PC3 PD2 + // --------------------------------- + // nOE (Output Enable) ... PB1 PC1 + // Anode L1:2 ............ PD3 PC2 + // Anode L3 .............. PD2 PC3 + + + void Seg7::init () { + enabled = 1; + setAnodes(0); + setCathodes(0); + switch (hardwareVersion) { + case 1: { + DDRB |= (1 << PB5) | (1 << PB4) | (1 << PB3) | (1 << PB2) | (1 << PB1) | (1 << PB0); + DDRC |= (1 << PC3) | (1 << PC2) | (1 << PC1) | (1 << PC0); + DDRD |= (1 << PD7) | (1 << PD6) | (1 << PD4) | (1 << PD3) | (1 << PD2); + break; + } + case 2: { + DDRB |= (1 << PB5) | (1 << PB4) | (1 << PB3) | (1 << PB2) | (1 << PB1) | (1 << PB0); + DDRC |= (1 << PC3) | (1 << PC2) | (1 << PC1); + DDRD |= (1 << PD7) | (1 << PD6) | (1 << PD5) | (1 << PD4) | (1 << PD3) | (1 << PD2); + break; + } + } + + } + + void Seg7::cleanup () { + enabled = 0; + setAnodes(0); + setCathodes(0); + switch (hardwareVersion) { + case 1: { + DDRB &= ~((1 << PB5) | (1 << PB4) | (1 << PB3) | (1 << PB2) | (1 << PB1) | (1 << PB0)); + DDRC &= ~((1 << PC3) | (1 << PC2) | (1 << PC1) | (1 << PC0)); + DDRD &= ~((1 << PD7) | (1 << PD6) | (1 << PD4) | (1 << PD3) | (1 << PD2)); + break; + } + case 2: { + DDRB &= ~((1 << PB5) | (1 << PB4) | (1 << PB3) | (1 << PB2) | (1 << PB1) | (1 << PB0) ); + DDRC &= ~((1 << PC3) | (1 << PC2) | (1 << PC1)); + DDRD &= ~((1 << PD7) | (1 << PD6) | (1 << PD5) | (1 << PD4) | (1 << PD3) | (1 << PD2)); + break; + } + } + + } + + void Seg7::setAnodes (uint16_t a) { + if (a & 0x0001) PORTD |= (1 << PD4); else PORTD &= ~(1 << PD4); // Anode Char A + if (a & 0x0002) PORTB |= (1 << PB0); else PORTB &= ~(1 << PB0); // Anode Char B + if (a & 0x0004) PORTD |= (1 << PD7); else PORTD &= ~(1 << PD7); // Anode Char C + if (a & 0x0008) PORTD |= (1 << PD6); else PORTD &= ~(1 << PD6); // Anode Char D + if (a & 0x0010) PORTB |= (1 << PB2); else PORTB &= ~(1 << PB2); // Anode Char E + if (a & 0x0020) PORTB |= (1 << PB3); else PORTB &= ~(1 << PB3); // Anode Char F + if (a & 0x0040) PORTB |= (1 << PB4); else PORTB &= ~(1 << PB4); // Anode Char G + if (a & 0x0080) PORTB |= (1 << PB5); else PORTB &= ~(1 << PB5); // Anode Char DP + switch (hardwareVersion) { + case 1: { + if (a & 0x0100) PORTD |= (1 << PD3); else PORTD &= ~(1 << PD3); // Anode L1/L2 + if (a & 0x0200) PORTD |= (1 << PD2); else PORTD &= ~(1 << PD2); // Anode L3 + break; + } + case 2: { + if (a & 0x0100) PORTC |= (1 << PC2); else PORTC &= ~(1 << PC2); // Anode L1/L2 + if (a & 0x0200) PORTC |= (1 << PC3); else PORTC &= ~(1 << PC3); // Anode L3 + break; + } + } + } + + void Seg7::setCathodes (uint8_t c) { + switch (hardwareVersion) { + case 1: { + if (c & 0x01) PORTC |= (1 << PC0); else PORTC &= ~(1 << PC0); // Chathode Char 1 (most left) + if (c & 0x02) PORTC |= (1 << PC1); else PORTC &= ~(1 << PC1); // Chathode Char 2 + if (c & 0x04) PORTC |= (1 << PC2); else PORTC &= ~(1 << PC2); // Chathode Char 3 + if (c & 0x08) PORTC |= (1 << PC3); else PORTC &= ~(1 << PC3); // Chathode Char 4 (most right) + break; + } + case 2: { + if (c & 0x01) PORTD |= (1 << PD5); else PORTD &= ~(1 << PD5); // Chathode Char 1 (most left) + if (c & 0x02) PORTB |= (1 << PB1); else PORTB &= ~(1 << PB1); // Chathode Char 2 + if (c & 0x04) PORTD |= (1 << PD3); else PORTD &= ~(1 << PD3); // Chathode Char 3 + if (c & 0x08) PORTD |= (1 << PD2); else PORTD &= ~(1 << PD2); // Chathode Char 4 (most right) + break; + } + } + } + + void Seg7::setOE (bool enabled) { + switch (hardwareVersion) { + case 1: if (enabled) PORTB &= ~(1 << PB1); else PORTB |= (1 << PB1); break; + case 2: if (enabled) PORTC &= ~(1 << PC1); else PORTC |= (1 << PC1); break; + } + } + +#endif + +const char *segName[] = { "A", "B", "C", "D", "E", "F", "G", "DP" }; + + +int8_t Seg7::run (uint8_t subtest) { + if (subtest == 0) { + setCathodes(0x0f); // all segment cathodes conected to GND + setAnodes(0x3ff); // all segments ON + setOE(true); + printf_P(PSTR("ON")); + wait(2000); + setAnodes(0); + return 0; + + } else if (subtest == 1) { + printf_P(PSTR("OFF")); + wait(1000); + return 0; + + } else if (subtest == 2) { + setAnodes(0x100); // L1/L2 ON + printf_P(PSTR("L1/L2 ON")); + wait(1000); + setAnodes(0); + return 0; + + } else if (subtest == 3) { + setAnodes(0x200); // L3 ON + printf_P(PSTR("L1/L2 ON")); + wait(1000); + setAnodes(0); + return 0; + + } else if (subtest < (4 + 4 * 8)) { + uint8_t chIndex = (subtest - 4) / 8; + uint8_t segIndex = (subtest - 4) % 8; + setCathodes(1 << chIndex); + setAnodes(1 << segIndex); + printf_P(PSTR("Char %d - %s -> %02x"), chIndex, segName[segIndex], (1 << segIndex)); + wait(400); + return 0; + } + + return -1; +} + + diff --git a/software/test-software/src/units/seg7.hpp b/software/test-software/src/units/seg7.hpp new file mode 100644 index 0000000..0e71fde --- /dev/null +++ b/software/test-software/src/units/seg7.hpp @@ -0,0 +1,25 @@ +#ifndef SEG7_HPP +#define SEG7_HPP + +#include +#include "../main.hpp" +#include + +class Seg7 : public TestUnit { + public: + bool enabled; + + public: + Seg7 () { enabled = false; } + virtual void init (); + virtual void cleanup (); + virtual int8_t run (uint8_t subtest); + virtual PGM_P getName () { return PSTR("Seg7"); } + + private: + void setAnodes (uint16_t); + void setCathodes (uint8_t mask); + void setOE (bool enabled); +}; + +#endif \ No newline at end of file diff --git a/software/test-software/src/units/switch.cpp b/software/test-software/src/units/switch.cpp new file mode 100644 index 0000000..4ce9456 --- /dev/null +++ b/software/test-software/src/units/switch.cpp @@ -0,0 +1,100 @@ +#include +#include +#include + +#include "switch.hpp" +#include "../main.hpp" + +#if defined(__AVR_ATmega644P__) || defined(__AVR_ATmega1284P__) + + // Nano-644 + // --------------------------------------------------------------- + // PA0 ..... SW1 + // PA1 ..... SW2 + // PA2 ..... SW3 + // PA3 ..... SW4 + + void Switch::init () { + DDRA &= ~((1 << PORTA3) | (1 << PORTA2) | (1 << PORTA1) | (1 << PORTA0)); + PORTA |= (1 << PORTA3) | (1 << PORTA2) | (1 << PORTA1) | (1 << PORTA0); + } + + void Switch::cleanup () { + PORTA &= ~((1 << PORTA3) | (1 << PORTA2) | (1 << PORTA1) | (1 << PORTA0)); + DDRA &= ~((1 << PORTA3) | (1 << PORTA2) | (1 << PORTA1) | (1 << PORTA0)); + } + + bool Switch::isPressed (SWITCH sw) { + switch (sw) { + case SW1: return (PINA & ( 1 << PA0)) == 0; + case SW2: return (PINA & ( 1 << PA1)) == 0; + case SW3: return (PINA & ( 1 << PA2)) == 0; + case SW4: return (PINA & ( 1 << PA3)) == 0; + default: return false; + } + } + +#endif + +#ifdef __AVR_ATmega328P__ + + // Arduino-Nano-5V + // --------------------------------------------------------------- + // PC0 ..... SW1 + // PC1 ..... SW2 + // PC2 ..... SW3 + // PC3 ..... SW4 + + void Switch::init () { + DDRC &= ~((1 << PORTC3) | (1 << PORTC2) | (1 << PORTC1) | (1 << PORTC0)); + PORTC |= (1 << PORTC3) | (1 << PORTC2) | (1 << PORTC1) | (1 << PORTC0); + } + + void Switch::cleanup () { + PORTC &= ~((1 << PORTC3) | (1 << PORTC2) | (1 << PORTC1) | (1 << PORTC0)); + DDRC &= ~((1 << PORTC3) | (1 << PORTC2) | (1 << PORTC1) | (1 << PORTC0)); + } + + bool Switch::isPressed (SWITCH sw) { + switch (sw) { + case SW1: return (PINC & ( 1 << PC0)) == 0; + case SW2: return (PINC & ( 1 << PC1)) == 0; + case SW3: return (PINC & ( 1 << PC2)) == 0; + case SW4: return (PINC & ( 1 << PC3)) == 0; + default: return false; + } + } + +#endif + +int8_t Switch::run (uint8_t subtest) { + if (subtest < 16) { + SWITCH sw = (SWITCH)(subtest / 4); + switch (subtest % 4) { + case 1: { + if (!isPressed(sw)) { + printf_P(PSTR("Press SW%d"), sw + 1); + while (!isPressed(sw) && wait(0) == EOF) {} + wait(10); + } + return 0; + } + + case 0: case 2: { + if (isPressed(sw)) { + printf_P(PSTR("Release SW%d "), sw + 1); + while (isPressed(sw) && wait(0) == EOF) {} + wait(10); + } + return 0; + } + + case 3: { + return 0; + } + } + + } + + return -1; +} diff --git a/software/test-software/src/units/switch.hpp b/software/test-software/src/units/switch.hpp new file mode 100644 index 0000000..03bf0b2 --- /dev/null +++ b/software/test-software/src/units/switch.hpp @@ -0,0 +1,20 @@ +#ifndef SWITCH_HPP +#define SWITCH_HPP + +#include +#include "../main.hpp" +#include + +class Switch : public TestUnit { + typedef enum { SW1 = 0, SW2 = 1, SW3 = 2, SW4 = 3 } SWITCH; + + public: + Switch () {}; + virtual void init (); + virtual void cleanup (); + virtual int8_t run (uint8_t subtest); + virtual PGM_P getName () { return PSTR("Switch"); } + bool isPressed (SWITCH sw); +}; + +#endif \ No newline at end of file diff --git a/software/test-software/src/units/uart1.cpp b/software/test-software/src/units/uart1.cpp new file mode 100644 index 0000000..57e0bfb --- /dev/null +++ b/software/test-software/src/units/uart1.cpp @@ -0,0 +1,65 @@ +#include +#include +#include + +#include "uart1.hpp" +#include "../main.hpp" + +#ifdef __AVR_ATmega328P__ + void Uart1::init () {} + void Uart1::cleanup () {} + int8_t Uart1::run (uint8_t subtest) { return -1; } + void Uart1::handleRxByte (uint8_t b) {} +#endif + +#if defined(__AVR_ATmega644P__) || defined(__AVR_ATmega1284P__) + +int uart1_putchar(char c, FILE *stream) { + if (c == '\n') { + uart1_putchar('\r', stream); + } + loop_until_bit_is_set(UCSR1A, UDRE1); + UDR1 = c; + return 0; +} + +static FILE mystderr = { 0, 0, _FDEV_SETUP_WRITE , 0, 0, uart1_putchar, NULL, 0 }; + +void Uart1::init () { + PORTD |= (1 << PD2); // enable RxD1 pullup + UCSR1A = (1 << U2X1); + UCSR1B = (1 << RXCIE1) | (1 << RXEN1) | (1 < send text via UART1 now...")); + fprintf_P(stderr, PSTR("Hello UART1, ECHO-Modus active\n")); + } while (wait(5000) == EOF); + return 0; + } + + return -1; +} + +void Uart1::handleRxByte (uint8_t b) { + uart1_putchar(b, stderr); +} + +#endif \ No newline at end of file diff --git a/software/test-software/src/units/uart1.hpp b/software/test-software/src/units/uart1.hpp new file mode 100644 index 0000000..40437e1 --- /dev/null +++ b/software/test-software/src/units/uart1.hpp @@ -0,0 +1,21 @@ +#ifndef UART1_HPP +#define UART1_HPP + +#include +#include "../main.hpp" +#include + +class Uart1 : public TestUnit { + public: + uint8_t enabled; + + public: + Uart1 () { enabled = 0; } + virtual void init (); + virtual void cleanup (); + virtual int8_t run (uint8_t subtest); + virtual PGM_P getName () { return PSTR("Uart1"); } + void handleRxByte (uint8_t); +}; + +#endif \ No newline at end of file -- 2.39.5