From 9b565bea31de84a0a7363a7d108b1f33551267e9 Mon Sep 17 00:00:00 2001 From: Colin Clark Date: Sun, 11 Sep 2022 23:25:50 -0400 Subject: [PATCH] Squashed 'hosts/daisy/vendor/libDaisy/' changes from 44151454..ef4cc6a0 ef4cc6a0 added changelog for v5.2.0 release. (#540) 899b01a5 Fixes issue with new uart class and midi (#539) 5b7d7f33 Adding support for Legio (#532) 63fcabd3 added scaling of colors via multiply operator (#533) d6f0f1f6 precomputing output level adjustment based on new config param (#531) 966d0650 Feature: audio output compensation (#530) 80ab34b3 Add TLV493D device (#447) dc30068d Create neotrellis and neopixel devices (#461) 58331896 Create apds9960 device (#446) 61ad4241 Create mpr121 device (#455) 12b49e5c Dps310 (#475) 7f373b02 ICM20948 Sensor (#469) 6ae066d4 updated changelog for v5.1.0 release (#529) 023481e6 Rename getting started spi to conform d7e83b1f Uart wrapup (#496) 2fc6bfcd Spi examples (#526) 3b0174e3 Add bugfix for single data byte running status, add unit test (#525) 54d2a6c0 Spi dma fix (#527) 363dffc9 Midi Junk Status byte bugfix (#515) e3e3380b TimerHandle callback fixes/improvements (#523) 04dd3361 sync'd changelog with recent changes. (#519) cad55ead Changes for issues with compiling with optimisation (#484) b74d584d USB Host callbacks (#511) f22b7512 User callbacks to timer peripheral (#518) 7e7e554a Bootloader v5 (#498) e12fb04e Fix PersistentStorage crash when calling RestoreDefaults (#517) 7950ba93 fixed #503 - added norrti flag to core/Makefile (#516) git-subtree-dir: hosts/daisy/vendor/libDaisy git-subtree-split: ef4cc6a0a934c5074204175be32663705c7189f4 --- CHANGELOG.md | 45 + CMakeLists.txt | 1 + .../Inc/stm32h7xx_ll_usb.h | 6 +- .../Src/stm32h7xx_ll_usb.c | 29 +- Makefile | 1 + .../Class/MSC/Src/usbh_msc.c | 2 +- Middlewares/notes.md | 3 +- core/Makefile | 60 +- core/STM32H750IB_sram.lds | 2 +- core/dsy_bootloader_v4.bin | Bin 96780 -> 0 bytes core/dsy_bootloader_v5_4.bin | Bin 0 -> 113716 bytes .../_a7_Getting-Started-Daisy-Bootloader.md | 90 ++ doc/md/_a8_Getting-Started-SPI.md | 176 ++++ doc/md/reference_home.md | 2 + examples/SPI/SpiBlockingTransmit/Makefile | 12 + .../SpiBlockingTransmit.cpp | 59 ++ examples/SPI/SpiDmaTransmit/Makefile | 12 + .../SPI/SpiDmaTransmit/SpiDmaTransmit.cpp | 62 ++ examples/TIM_SingleCallback/Makefile | 12 + .../TIM_SingleCallback/TIM_SingleCallback.cpp | 57 + .../Blocking_Receive/Blocking_Receive.cpp | 49 + examples/uart/Blocking_Receive/Makefile | 12 + .../Blocking_Transmit/Blocking_Transmit.cpp | 29 + examples/uart/Blocking_Transmit/Makefile | 12 + .../Blocking_Transmit_Dma_Receive.cpp | 67 ++ .../Blocking_Transmit_Dma_Receive/Makefile | 12 + .../Blocking_Transmit_Fifo_Receive.cpp | 57 + .../Blocking_Transmit_Fifo_Receive/Makefile | 12 + .../Dma_Fifo_Receive/Dma_Fifo_Receive.cpp | 57 + examples/uart/Dma_Fifo_Receive/Makefile | 12 + examples/uart/Dma_Receive/Dma_Receive.cpp | 63 ++ examples/uart/Dma_Receive/Makefile | 12 + examples/uart/Dma_Transmit/Dma_Transmit.cpp | 48 + examples/uart/Dma_Transmit/Makefile | 12 + .../Dma_Transmit_Blocking_Receive.cpp | 72 ++ .../Dma_Transmit_Blocking_Receive/Makefile | 12 + .../Dma_Transmit_Dma_Receive.cpp | 70 ++ .../uart/Dma_Transmit_Dma_Receive/Makefile | 12 + libdaisy.vcxproj | 2 + libdaisy.vcxproj.filters | 6 + src/daisy.h | 7 + src/daisy_legio.cpp | 201 ++++ src/daisy_legio.h | 142 +++ src/dev/apds9960.h | 857 +++++++++++++++ src/dev/dps310.h | 655 ++++++++++++ src/dev/icm20948.h | 997 ++++++++++++++++++ src/dev/mpr121.h | 360 +++++++ src/dev/neopixel.h | 632 +++++++++++ src/dev/neotrellis.h | 497 +++++++++ src/dev/tlv493d.h | 466 ++++++++ src/hid/audio.cpp | 59 +- src/hid/audio.h | 31 +- src/hid/midi.h | 28 +- src/hid/usb_host.cpp | 96 +- src/hid/usb_host.h | 61 +- src/per/spi.cpp | 20 + src/per/tim.cpp | 138 ++- src/per/tim.h | 95 +- src/per/uart.cpp | 842 +++++++++++---- src/per/uart.h | 137 ++- src/sys/dma.c | 7 +- src/sys/stm32h7xx_hal_conf.h | 2 +- src/sys/system.cpp | 2 + src/util/PersistentStorage.h | 4 +- src/util/color.h | 8 + tests/Midi_gtest.cpp | 164 ++- 66 files changed, 7395 insertions(+), 372 deletions(-) delete mode 100644 core/dsy_bootloader_v4.bin create mode 100644 core/dsy_bootloader_v5_4.bin create mode 100644 doc/md/_a7_Getting-Started-Daisy-Bootloader.md create mode 100644 doc/md/_a8_Getting-Started-SPI.md create mode 100644 examples/SPI/SpiBlockingTransmit/Makefile create mode 100644 examples/SPI/SpiBlockingTransmit/SpiBlockingTransmit.cpp create mode 100644 examples/SPI/SpiDmaTransmit/Makefile create mode 100644 examples/SPI/SpiDmaTransmit/SpiDmaTransmit.cpp create mode 100644 examples/TIM_SingleCallback/Makefile create mode 100644 examples/TIM_SingleCallback/TIM_SingleCallback.cpp create mode 100644 examples/uart/Blocking_Receive/Blocking_Receive.cpp create mode 100644 examples/uart/Blocking_Receive/Makefile create mode 100644 examples/uart/Blocking_Transmit/Blocking_Transmit.cpp create mode 100644 examples/uart/Blocking_Transmit/Makefile create mode 100644 examples/uart/Blocking_Transmit_Dma_Receive/Blocking_Transmit_Dma_Receive.cpp create mode 100644 examples/uart/Blocking_Transmit_Dma_Receive/Makefile create mode 100644 examples/uart/Blocking_Transmit_Fifo_Receive/Blocking_Transmit_Fifo_Receive.cpp create mode 100644 examples/uart/Blocking_Transmit_Fifo_Receive/Makefile create mode 100644 examples/uart/Dma_Fifo_Receive/Dma_Fifo_Receive.cpp create mode 100644 examples/uart/Dma_Fifo_Receive/Makefile create mode 100644 examples/uart/Dma_Receive/Dma_Receive.cpp create mode 100644 examples/uart/Dma_Receive/Makefile create mode 100644 examples/uart/Dma_Transmit/Dma_Transmit.cpp create mode 100644 examples/uart/Dma_Transmit/Makefile create mode 100644 examples/uart/Dma_Transmit_Blocking_Receive/Dma_Transmit_Blocking_Receive.cpp create mode 100644 examples/uart/Dma_Transmit_Blocking_Receive/Makefile create mode 100644 examples/uart/Dma_Transmit_Dma_Receive/Dma_Transmit_Dma_Receive.cpp create mode 100644 examples/uart/Dma_Transmit_Dma_Receive/Makefile create mode 100644 src/daisy_legio.cpp create mode 100644 src/daisy_legio.h create mode 100644 src/dev/apds9960.h create mode 100644 src/dev/dps310.h create mode 100644 src/dev/icm20948.h create mode 100644 src/dev/mpr121.h create mode 100644 src/dev/neopixel.h create mode 100644 src/dev/neotrellis.h create mode 100644 src/dev/tlv493d.h diff --git a/CHANGELOG.md b/CHANGELOG.md index da3acdc..728780e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,51 @@ ## Unreleased +## v5.2.0 + +### Features + +* board: added board support for Noise Engineering legio platform +* audio: added `output_compensation` value to config struct to allow for post-scaling of uneven audio passthru levels. +* util: added a multiply operator to the Color class for scaling a color by a single factor. +* device: Added ICM20948 sensor device driver +* device: Added DPS310 device driver +* device: Added MPR121 device driver +* device: Added APDS9960 device driver +* device: Added TLV493D device driver. +* device: Added neotrellis driver +* device: Added neopixel driver + +### Bug fixes + +* uart: fixed bug with fifo-dma-receive mode that would result in erratic reads over time. Fixes issues with UART (TRS/DIN) MIDI parsing + +## v5.1.0 + +### Features + +* tim: `TimerHandle` now has callbacks each time the Period has elapsed. These can be enabled with `TimerHandle::Config::enable_irq` at Init time. +* bootloader: Working with the bootloader has been simplified. See [the new guide for updates on usage](https://electro-smith.github.io/libDaisy/md_doc_md__a7__getting__started__daisy__bootloader.html) +* usb: `USBHost` class has added support for user callbacks on device connection, disconnection, and when the MSC class becomes active. +* uart: Adds DMA RX and TX modes, similar to how they work on the I2C and SPI. +* uart: Update function names to be more in line with the new DMA / Blocking scheme. + * The old methods are wrappers for the new ones to preserve backwards compatibility, but **will be removed in a future version**. + * Affected functions: `PollReceive`, `PollTx`, `StartRx`, `RxActive`, `FlushRx`, `PopRx`, `Readable` + +### Bug Fixes + +* util: PersistentStorage class had a bug where calling the `RestoreDefaults` function would cause a crash +* usb: LL HAL files for USB were updated to prevent timing issues when running with optimization +* spi: Add IRQ handlers for SPI2-5. These should work with DMA now. +* midi: bugs related to running status bytes for note off, and single data-byte messages have been resolved + +### Other + +* build: core/Makefile has had the `-fnortti` flag added to match libDaisy's Makefile +* bootloader: local version of daisy bootloader has been updated to improve stability +* spi: Added examples for blocking TX and DMA TX, added a writeup explaining how to use the SPI on the Daisy +* uart: Adds examples for common modes of communication, both DMA, blocking, FIFO, and mixed. + ## v5.0.0 ### Breaking Changes diff --git a/CMakeLists.txt b/CMakeLists.txt index eb1cb14..11b0201 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,6 +32,7 @@ add_library(${TARGET} STATIC ${MODULE_DIR}/daisy_petal.cpp ${MODULE_DIR}/daisy_field.cpp ${MODULE_DIR}/daisy_versio.cpp + ${MODULE_DIR}/daisy_legio.cpp ${MODULE_DIR}/daisy_patch_sm.cpp ${MODULE_DIR}/sys/system.cpp ${MODULE_DIR}/dev/sr_595.cpp diff --git a/Drivers/STM32H7xx_HAL_Driver/Inc/stm32h7xx_ll_usb.h b/Drivers/STM32H7xx_HAL_Driver/Inc/stm32h7xx_ll_usb.h index 335c83b..3adcf00 100644 --- a/Drivers/STM32H7xx_HAL_Driver/Inc/stm32h7xx_ll_usb.h +++ b/Drivers/STM32H7xx_HAL_Driver/Inc/stm32h7xx_ll_usb.h @@ -394,9 +394,9 @@ typedef struct #define USBx_PCGCCTL *(__IO uint32_t *)((uint32_t)USBx_BASE + USB_OTG_PCGCCTL_BASE) #define USBx_HPRT0 *(__IO uint32_t *)((uint32_t)USBx_BASE + USB_OTG_HOST_PORT_BASE) -#define USBx_DEVICE ((USB_OTG_DeviceTypeDef *)(USBx_BASE + USB_OTG_DEVICE_BASE)) -#define USBx_INEP(i) ((USB_OTG_INEndpointTypeDef *)(USBx_BASE + USB_OTG_IN_ENDPOINT_BASE + ((i) * USB_OTG_EP_REG_SIZE))) -#define USBx_OUTEP(i) ((USB_OTG_OUTEndpointTypeDef *)(USBx_BASE + USB_OTG_OUT_ENDPOINT_BASE + ((i) * USB_OTG_EP_REG_SIZE))) +#define USBx_DEVICE ((__IO USB_OTG_DeviceTypeDef *)(USBx_BASE + USB_OTG_DEVICE_BASE)) +#define USBx_INEP(i) ((__IO USB_OTG_INEndpointTypeDef *)(USBx_BASE + USB_OTG_IN_ENDPOINT_BASE + ((i) * USB_OTG_EP_REG_SIZE))) +#define USBx_OUTEP(i) ((__IO USB_OTG_OUTEndpointTypeDef *)(USBx_BASE + USB_OTG_OUT_ENDPOINT_BASE + ((i) * USB_OTG_EP_REG_SIZE))) #define USBx_DFIFO(i) *(__IO uint32_t *)(USBx_BASE + USB_OTG_FIFO_BASE + ((i) * USB_OTG_FIFO_SIZE)) #define USBx_HOST ((USB_OTG_HostTypeDef *)(USBx_BASE + USB_OTG_HOST_BASE)) diff --git a/Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_ll_usb.c b/Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_ll_usb.c index aeae776..00c4ca3 100644 --- a/Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_ll_usb.c +++ b/Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_ll_usb.c @@ -138,7 +138,7 @@ HAL_StatusTypeDef USB_CoreInit(USB_OTG_GlobalTypeDef *USBx, USB_OTG_CfgTypeDef c HAL_StatusTypeDef USB_SetTurnaroundTime(USB_OTG_GlobalTypeDef *USBx, uint32_t hclk, uint8_t speed) { - uint32_t UsbTrd; + volatile uint32_t UsbTrd; /* The USBTRD is configured according to the tables below, depending on AHB frequency used by application. In the low AHB frequency range it is used to stretch enough the USB response @@ -441,7 +441,7 @@ HAL_StatusTypeDef USB_DevInit(USB_OTG_GlobalTypeDef *USBx, USB_OTG_CfgTypeDef cf */ HAL_StatusTypeDef USB_FlushTxFifo(USB_OTG_GlobalTypeDef *USBx, uint32_t num) { - uint32_t count = 0U; + volatile uint32_t count = 0U; USBx->GRSTCTL = (USB_OTG_GRSTCTL_TXFFLSH | (num << 6)); @@ -464,7 +464,7 @@ HAL_StatusTypeDef USB_FlushTxFifo(USB_OTG_GlobalTypeDef *USBx, uint32_t num) */ HAL_StatusTypeDef USB_FlushRxFifo(USB_OTG_GlobalTypeDef *USBx) { - uint32_t count = 0; + volatile uint32_t count = 0; USBx->GRSTCTL = USB_OTG_GRSTCTL_RXFFLSH; @@ -511,7 +511,7 @@ uint8_t USB_GetDevSpeed(USB_OTG_GlobalTypeDef *USBx) { uint32_t USBx_BASE = (uint32_t)USBx; uint8_t speed; - uint32_t DevEnumSpeed = USBx_DEVICE->DSTS & USB_OTG_DSTS_ENUMSPD; + volatile uint32_t DevEnumSpeed = USBx_DEVICE->DSTS & USB_OTG_DSTS_ENUMSPD; if (DevEnumSpeed == DSTS_ENUMSPD_HS_PHY_30MHZ_OR_60MHZ) { @@ -1121,7 +1121,7 @@ HAL_StatusTypeDef USB_DevDisconnect(USB_OTG_GlobalTypeDef *USBx) */ uint32_t USB_ReadInterrupts(USB_OTG_GlobalTypeDef *USBx) { - uint32_t tmpreg; + volatile uint32_t tmpreg; tmpreg = USBx->GINTSTS; tmpreg &= USBx->GINTMSK; @@ -1137,7 +1137,7 @@ uint32_t USB_ReadInterrupts(USB_OTG_GlobalTypeDef *USBx) uint32_t USB_ReadDevAllOutEpInterrupt(USB_OTG_GlobalTypeDef *USBx) { uint32_t USBx_BASE = (uint32_t)USBx; - uint32_t tmpreg; + volatile uint32_t tmpreg; tmpreg = USBx_DEVICE->DAINT; tmpreg &= USBx_DEVICE->DAINTMSK; @@ -1153,7 +1153,7 @@ uint32_t USB_ReadDevAllOutEpInterrupt(USB_OTG_GlobalTypeDef *USBx) uint32_t USB_ReadDevAllInEpInterrupt(USB_OTG_GlobalTypeDef *USBx) { uint32_t USBx_BASE = (uint32_t)USBx; - uint32_t tmpreg; + volatile uint32_t tmpreg; tmpreg = USBx_DEVICE->DAINT; tmpreg &= USBx_DEVICE->DAINTMSK; @@ -1171,7 +1171,7 @@ uint32_t USB_ReadDevAllInEpInterrupt(USB_OTG_GlobalTypeDef *USBx) uint32_t USB_ReadDevOutEPInterrupt(USB_OTG_GlobalTypeDef *USBx, uint8_t epnum) { uint32_t USBx_BASE = (uint32_t)USBx; - uint32_t tmpreg; + volatile uint32_t tmpreg; tmpreg = USBx_OUTEP((uint32_t)epnum)->DOEPINT; tmpreg &= USBx_DEVICE->DOEPMSK; @@ -1189,7 +1189,7 @@ uint32_t USB_ReadDevOutEPInterrupt(USB_OTG_GlobalTypeDef *USBx, uint8_t epnum) uint32_t USB_ReadDevInEPInterrupt(USB_OTG_GlobalTypeDef *USBx, uint8_t epnum) { uint32_t USBx_BASE = (uint32_t)USBx; - uint32_t tmpreg, msk, emp; + volatile uint32_t tmpreg, msk, emp; msk = USBx_DEVICE->DIEPMSK; emp = USBx_DEVICE->DIEPEMPMSK; @@ -1289,7 +1289,7 @@ HAL_StatusTypeDef USB_EP0_OutStart(USB_OTG_GlobalTypeDef *USBx, uint8_t dma, uin */ static HAL_StatusTypeDef USB_CoreReset(USB_OTG_GlobalTypeDef *USBx) { - uint32_t count = 0U; + volatile uint32_t count = 0U; /* Wait for AHB master IDLE state. */ do @@ -1819,7 +1819,7 @@ HAL_StatusTypeDef USB_HC_Halt(USB_OTG_GlobalTypeDef *USBx, uint8_t hc_num) { uint32_t USBx_BASE = (uint32_t)USBx; uint32_t hcnum = (uint32_t)hc_num; - uint32_t count = 0U; + volatile uint32_t count = 0U; uint32_t HcEpType = (USBx_HC(hcnum)->HCCHAR & USB_OTG_HCCHAR_EPTYP) >> 18; /* Check for space in the request queue to issue the halt. */ @@ -1885,7 +1885,7 @@ HAL_StatusTypeDef USB_DoPing(USB_OTG_GlobalTypeDef *USBx, uint8_t ch_num) uint32_t USBx_BASE = (uint32_t)USBx; uint32_t chnum = (uint32_t)ch_num; uint32_t num_packets = 1U; - uint32_t tmpreg; + volatile uint32_t tmpreg; USBx_HC(chnum)->HCTSIZ = ((num_packets << 19) & USB_OTG_HCTSIZ_PKTCNT) | USB_OTG_HCTSIZ_DOPING; @@ -1907,8 +1907,8 @@ HAL_StatusTypeDef USB_DoPing(USB_OTG_GlobalTypeDef *USBx, uint8_t ch_num) HAL_StatusTypeDef USB_StopHost(USB_OTG_GlobalTypeDef *USBx) { uint32_t USBx_BASE = (uint32_t)USBx; - uint32_t count = 0U; - uint32_t value; + volatile uint32_t count = 0U; + volatile uint32_t value; uint32_t i; @@ -2005,3 +2005,4 @@ HAL_StatusTypeDef USB_DeActivateRemoteWakeup(USB_OTG_GlobalTypeDef *USBx) */ /************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/ + diff --git a/Makefile b/Makefile index 6d37a46..812d0bc 100644 --- a/Makefile +++ b/Makefile @@ -27,6 +27,7 @@ daisy_patch \ daisy_petal \ daisy_field \ daisy_versio \ +daisy_legio \ daisy_patch_sm \ sys/fatfs \ sys/system \ diff --git a/Middlewares/ST/STM32_USB_Host_Library/Class/MSC/Src/usbh_msc.c b/Middlewares/ST/STM32_USB_Host_Library/Class/MSC/Src/usbh_msc.c index 5daa689..58c58bb 100644 --- a/Middlewares/ST/STM32_USB_Host_Library/Class/MSC/Src/usbh_msc.c +++ b/Middlewares/ST/STM32_USB_Host_Library/Class/MSC/Src/usbh_msc.c @@ -253,7 +253,7 @@ static USBH_StatusTypeDef USBH_MSC_InterfaceDeInit(USBH_HandleTypeDef *phost) if (phost->pActiveClass->pData) { - USBH_free(phost->pActiveClass->pData); + // USBH_free(phost->pActiveClass->pData); phost->pActiveClass->pData = 0U; } diff --git a/Middlewares/notes.md b/Middlewares/notes.md index 45eebb3..711843f 100644 --- a/Middlewares/notes.md +++ b/Middlewares/notes.md @@ -3,4 +3,5 @@ This can be used to keep track of any manual changes to Middleware files to make migrating to new versions easier in the future. * Middlewares/ST/STM32_USB_Host_Library/Class/MSC/Src/usbh_msc.c - * The msc class was modified to prevent dynamic allocation of the `MSC_HandleTypeDef` struct. It was also placed in uncached D2 ram to allow DMA transfers with D cache enabled. \ No newline at end of file + * The msc class was modified to prevent dynamic allocation of the `MSC_HandleTypeDef` struct. It was also placed in uncached D2 ram to allow DMA transfers with D cache enabled. + * modified again on 18 April 2022 to temporarily remove USBH_Free from usbh class -- this should be done for device classes, and/or we should just rework the system to work with malloc/free as designed. That change may require moving the heap out of DTCMRAM (default location within daisy linker) if the DMA needs access to the class data \ No newline at end of file diff --git a/core/Makefile b/core/Makefile index 10c1c52..0bc22fe 100644 --- a/core/Makefile +++ b/core/Makefile @@ -4,9 +4,6 @@ CHIPSET ?= stm32h7x TARGET_BIN=$(TARGET).bin TARGET_ELF=$(TARGET).elf -BOOT_BIN ?= $(wildcard $(SYSTEM_FILES_DIR)/dsy_bootloader*) -FLASH_ADDRESS ?= 0x08000000 -QSPI_ADDRESS ?= 0x90040000 # If you have the arm-none-eabi- toolchain located in a particular place, but not installed for the entire system, add the path here: # GCC_PATH= @@ -188,19 +185,58 @@ CPPFLAGS += \ -fshort-enums \ -fno-move-loop-invariants \ -fno-unwind-tables \ +-fno-rtti \ -Wno-register C_STANDARD = -std=gnu11 CPP_STANDARD ?= -std=gnu++14 + ####################################### -# LDFLAGS +# Boot Management ####################################### -# link script + +INTERNAL_ADDRESS = 0x08000000 +QSPI_ADDRESS ?= 0x90040000 + +# For the time being, we'll keep the Daisy Bootloader's PID as df11 +# DAISY_PID = a360 +DAISY_PID = df11 +STM_PID = df11 + +BOOT_BIN ?= $(wildcard $(SYSTEM_FILES_DIR)/dsy_bootloader*) +APP_TYPE ?= BOOT_NONE + +ifeq ($(APP_TYPE), BOOT_NONE) + LDSCRIPT ?= $(SYSTEM_FILES_DIR)/STM32H750IB_flash.lds +USBPID = $(STM_PID) +FLASH_ADDRESS ?= $(INTERNAL_ADDRESS) -# libraries +else ifeq ($(APP_TYPE), BOOT_SRAM) + +LDSCRIPT ?= $(SYSTEM_FILES_DIR)/STM32H750IB_sram.lds +USBPID = $(DAISY_PID) +FLASH_ADDRESS ?= $(QSPI_ADDRESS) +C_DEFS += -DBOOT_APP + +else ifeq ($(APP_TYPE), BOOT_QSPI) + +LDSCRIPT ?= $(SYSTEM_FILES_DIR)/STM32H750IB_qspi.lds +USBPID = $(DAISY_PID) +FLASH_ADDRESS ?= $(QSPI_ADDRESS) +C_DEFS += -DBOOT_APP +else + +$(error Unkown app type "$(APP_TYPE)") + +endif + +####################################### +# LDFLAGS +####################################### +# libraries LIBS += -ldaisy -lc -lm -lnosys LIBDIR += -L$(LIBDAISY_DIR)/build #LIBDIR = -L./VisualGDB/Release @@ -277,22 +313,24 @@ debug: debug_client: ddd --eval-command="target remote localhost:3333" --debugger $(GDB) $(TARGET_ELF) +ifeq ($(APP_TYPE), BOOT_NONE) program: $(OCD) -s $(OCD_DIR) $(OCDFLAGS) \ -c "program ./build/$(TARGET).elf verify reset exit" +else +program: + $(error Cannot program via openocd with an app type of "$(APP_TYPE)". Try program-dfu instead) +endif ####################################### # dfu-util ####################################### program-dfu: - dfu-util -a 0 -s $(FLASH_ADDRESS):leave -D $(BUILD_DIR)/$(TARGET_BIN) -d ,0483:df11 - -program-app: - dfu-util -a 0 -s $(QSPI_ADDRESS):leave -D $(BUILD_DIR)/$(TARGET_BIN) -d ,0483:df11 + dfu-util -a 0 -s $(FLASH_ADDRESS):leave -D $(BUILD_DIR)/$(TARGET_BIN) -d ,0483:$(USBPID) program-boot: - dfu-util -a 0 -s $(FLASH_ADDRESS):leave -D $(BOOT_BIN) -d ,0483:df11 + dfu-util -a 0 -s $(INTERNAL_ADDRESS):leave -D $(BOOT_BIN) -d ,0483:$(STM_PID) ####################################### # dependencies diff --git a/core/STM32H750IB_sram.lds b/core/STM32H750IB_sram.lds index 58eb531..f84f05d 100644 --- a/core/STM32H750IB_sram.lds +++ b/core/STM32H750IB_sram.lds @@ -9,7 +9,7 @@ MEMORY { FLASH (RX) : ORIGIN = 0x08000000, LENGTH = 128K DTCMRAM (RWX) : ORIGIN = 0x20000000, LENGTH = 128K - SRAM (RWX) : ORIGIN = 0x24000000, LENGTH = 512K + SRAM (RWX) : ORIGIN = 0x24000000, LENGTH = 512K - 32K RAM_D2_DMA (RWX) : ORIGIN = 0x30000000, LENGTH = 32K RAM_D2 (RWX) : ORIGIN = 0x30008000, LENGTH = 256K RAM_D3 (RWX) : ORIGIN = 0x38000000, LENGTH = 64K diff --git a/core/dsy_bootloader_v4.bin b/core/dsy_bootloader_v4.bin deleted file mode 100644 index 3678b7d07858fbec1974e415775d1602280e60f3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 96780 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 b%Np_N% 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®^A1&tXmO5Z$otq?6Y0Nw5`t)^qDz*3_znny%g&;+>7N z!JYNC1=Xt)V)ft`d9~hFQeCcLR*AEcgf**A$>kXP{RDXvOOIW@j(Feis$RVxPb__P z`EX$j=ugbBEXaqxd9&o&E=ywd>b~{X5|+*)rH!p;We00M{w5**+TMT*tT2Tzle;?^ ztv9&mi`DjOYxU}QlU2>{s?ynC}u~r-Kx4PM7f@gH|s`_um4?V#Y1N}p*;tVa8l_s8 z)tCpp$nw(>ki6$k6P${A%g>dPp#b}$YlsDPfH+4kR=;t@YLszzS~lR1$xYP-%_Qs@|lGxiT% zJ#b=h*LM-E)pppETuSwTVmCtcFI-Tn)>xT^sOLUUE`6cYB88w!hu;bN#^9V!`!ndl{(|b{>#Qwo zOe-{vxGT5yZYjl>!I^w&+bO!4>nJmC?Cv*T}4$FzKJ_^0S^^dHag0 zS8J^noXcW7Z?P59E!x64HEd;JWA_e!Y7HChEC)r3s?~DKj`EK^Y)|mh{!aW!e_5K~ z9xY1Rr6vC$1@6*7fB$eF7yE-0>^%pYX7N_`rOPJ#@-vf`2$uI>rI$^V=R*G_sF(;L zHd|w5{7lcGo~oazD}TC$OrMdTF zMfaD?FR?9IbpJxTZ80I)!c48@{)Kbx^Mc{rg-c2n%`LPcIX?F2vA-JkhZT&5Go+8X zd(M4?T)43|e|mPo^btdiWO9Laa#sF$E&hb88K_#T{ZSlyr6rX8;c5r}oD`PhJm+WP zOv3!a#iDkYaoH$i@Nd|inL{%3<^^LiS=_KYjYID2iIQTzk0J`5kfBBPg{-|p&AF4= zFaCn(=b^j7{z{@60k;6|J{3_-4I?Vc%<)m``Yyz0a$-i zhV|A&_vsg#H~OyN=n zrW_`3lLqoKv87B+`Ay1wDci{oqDkqKl9`g8k_e3|V^hwOBV=~Uj1+sy+!Sldl$7}? ze}(9@i+o5vBJYzs=*Q&m zCfCX5WH;GQUL!T+HtpZY>*Ph^BX5#D=|=qD&26F2(XCX={e@QHe>?vFivJhri}-(;zCw4>SMk4#?#6!&-Ani3zZU;* z&^PgafF7i8;s0;cM;qv2`Zhg6kJ5MG-lOlseMmoo`xq_#oKo@>{eni*b_!*_v;+Ts znhHe)(R4HyMoQAhaAUZ;xw+}%I1`tJe}N09(>W6FDFKJLzFrZ~6&Ts5i)5vTRx?A0)$&Cx7fui7lNB_%p2h4byj+U%00c3X*7EL>PTpRJ=k@fIv^i*H5~0>z{3k4irCu8t?<_X2(e$wr;? z-~<+nwNu8NLa7QP9OWqCa3!e1O^+6fo0AprqX%;0=cN8+dMbo$xZ@ zeGaby-YIxR@ScIU3Eo(E7I6fbU#ck| zH}I1scU{RNuHzef6RG#)azp+b`8O1N3pQuK(wJ{q07BmWcBw z;T&_`cwquD-Z4BQbHtr@jU1I_wq%bNZiy%?vK8aB6MHFA?|)16*7Kk#ZVkV~5!Hi_ z5Y?!siK?WMsLnh?RDHiAs_T@ha@9Cvok3OGkcRcd)u&+@*SdJ-zJLCi!lZQngZQ3ta_e5|XR`mVqW%wHwP}I? diff --git a/core/dsy_bootloader_v5_4.bin b/core/dsy_bootloader_v5_4.bin new file mode 100644 index 0000000000000000000000000000000000000000..c29b4e762587f7317aded9a43e952a9eed5453bf GIT binary patch literal 113716 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 literal 0 HcmV?d00001 diff --git a/doc/md/_a7_Getting-Started-Daisy-Bootloader.md b/doc/md/_a7_Getting-Started-Daisy-Bootloader.md new file mode 100644 index 0000000..11dc74f --- /dev/null +++ b/doc/md/_a7_Getting-Started-Daisy-Bootloader.md @@ -0,0 +1,90 @@ +# Getting Started - Daisy Bootloader + +In the context of embedded applications, a bootloader is a small program that runs on bootup and manages loading and even updating target applications. The update routine of the STM32H7's system bootloader can be accessed on the Daisy by resetting the device with the BOOT button held down. +However, the built-in bootloader can only reprogram the chip's internal flash (128kB). With this setup, it's not so easy to store larger programs in non-volatile memory, and it's even harder to get them running. That's where the Daisy bootloader comes in. + +## Advantages of the Daisy bootloader + +With the Daisy bootloader, you can easily run significantly larger programs (up to 480kB on SRAM and almost 8MB on QSPI flash). Execution from SRAM has comparable speed to the internal flash, while the QSPI flash is naturally a fair bit slower. The update process for end-users can also be made extremely simple with SD cards or USB drives; simply starting up the Daisy with media plugged in will launch a search for valid executable binaries. + +## Flashing the bootloader + +With the latest version of libDaisy, you should be able to flash the bootloader using the `program-boot` rule within any project. The bootloader will reside in the internal flash like any normal application, so you'll need to go through the [normal DFU procedure](https://github.com/electro-smith/DaisyWiki/wiki/1.-Setting-Up-Your-Development-Environment#4a-flashing-the-daisy-via-usb) before running `make program-boot`. + +## Generating programs for the bootloader + +To generate programs that can run from the bootloader, you only need one additional line in your makefile: + +~~~makefile +APP_TYPE = BOOT_SRAM +~~~ + +The valid `APP_TYPE`s are `BOOT_SRAM` (runs program on the internal SRAM), `BOOT_QSPI` (runs programs on the QSPI flash chip), and `BOOT_NONE` (which just behaves like normal and is unable to run from the bootloader). Once you've added the desired app type, you'll need to recompile your project. It should be obvious from the memory usage output where your program will run. For example, when compiling for SRAM, you might see something like: + +~~~sh +Memory region Used Size Region Size %age Used + FLASH: 0 GB 128 KB 0.00% + DTCMRAM: 7440 B 128 KB 5.68% + SRAM: 46000 B 512 KB 8.77% + RAM_D2_DMA: 16 KB 32 KB 50.00% + RAM_D2: 0 GB 256 KB 0.00% + RAM_D3: 0 GB 64 KB 0.00% + ITCMRAM: 0 GB 64 KB 0.00% + SDRAM: 0 GB 64 MB 0.00% + QSPIFLASH: 0 GB 7936 KB 0.00% +~~~ + +Notice the zero bytes used in FLASH, which is the chip's internal flash storage. Since the bootloader lives there, any program that you intend to run with it can't use that region. + +Compiling for QSPI will move the program over into the QSPIFLASH region: + +~~~sh +Memory region Used Size Region Size %age Used + FLASH: 0 GB 128 KB 0.00% + DTCMRAM: 0 GB 128 KB 0.00% + SRAM: 7440 B 512 KB 1.42% + RAM_D2_DMA: 16 KB 32 KB 50.00% + RAM_D2: 0 GB 256 KB 0.00% + RAM_D3: 0 GB 64 KB 0.00% + ITCMRAM: 0 GB 64 KB 0.00% + SDRAM: 0 GB 64 MB 0.00% + QSPIFLASH: 46000 B 7936 KB 0.57% +~~~ + +## Flashing with the bootloader + +With your program compiled, you have a few options available for programming it using the bootloader: + +### DFU + +If the bootloader's LED is pulsing in the grace period, and the Daisy is connected to your computer via USB, you can run `make program-dfu`. The `APP_TYPE` will automatically adjust the DFU command to write to the correct address. Note that you'll have to change the app type back to `BOOT_NONE` or remove it to flash non-bootloader programs. + +### SD Card + +If your Daisy is connected to a micro SD card slot, you can just drag and drop the `.bin` file generated in your project's build folder onto the card. If you then plug it into the Daisy and restart it, the bootloader will automatically flash it to the QSPI chip. + +At this time, the SD card needs to be fully wired up with all of the 4-bit data lines connected. + +### USB Drive + +If your Daisy's external USB pins (D29 and D30) are connected to a USB port, you can follow the same process as SD cards above. + +## Custom linkers + +From the two examples above, you'll notice that the `BOOT_SRAM` type does not use any SRAM as RAM. Instead, it's all placed in DTCMRAM, which is only 128kB. The `BOOT_QSPI` does use the SRAM, but the program's execution can be a fair bit slower running from QSPI. Essentially, each configuration has its own trade-offs. If you want to customize those trade-offs, you can write your own linker to determine where each part of your program is placed. + +For a starting point, you can look through the linkers provided in libDaisy's `core` folder. Non-bootloader programs running on internal flash are linked with the `STM32H750IB_flash.lds` script, while the other two `.lds` scripts handle their respective configurations. Right now, the Daisy bootloader will only accept QSPI and SRAM as valid locations for programs, so custom linkers must place the `.isr_vector` and `.text` sections in these regions. + +## Detailed Behavior + +Once flashed, the bootloader has a grace period of 2.5 seconds on startup indicated by sinusoidal LED blinks. During this time, it will listen for DFU transactions over USB and search any connected media for valid binaries. Once this period elapses, the bootloader will attempt to load a program and jump to it. If no program is present, it will simply wait in the grace period until a DFU transaction occurs. You can extend the grace period indefinitely by pressing the BOOT button (the bootloader will acknowledge the extension with a few rapid blinks). + +Programs are stored on the QSPI flash chip that comes with every Daisy. The first four 64kB sectors are left untouched, though these may be used in the future for additional features. Sectors are erased in 64kB chunks, so if you plan to use the space beyond the program, ensure that the first address lies on a 64k boundary beyond the size of the program code. In the STM32H7's address space, QSPI flash lies at address `0x90000000`, so programs are stored at `0x90040000`. + +The bootloader needs a portion of the SRAM for its own processes. It uses 32kB located at the end of the region, so any programs run from SRAM cannot be greater than 480kB. You are free to use this space for any normal RAM sections, however. + +For SD cards and USB drives, the executable binary search is not very sophisticated -- it simply scans through the files in the root directory of the connected media looking for a file with an extension of `.bin`. Once a `.bin` file is found, the search will cease and the binary will be verified as executable. If that check passes, the bootloader will compare the file to what's already stored in QSPI and flash it if the file isn't the same. + +SD Cards, if present, are checked before USB drives. If a binary is found on the SD card, the USB drive will be skipped whether it was valid or not. + +If any error is encountered in the bootloading process, the Daisy will emit a single SOS pattern on the user LED and then continue as normal. The primary cause of errors is invalid programs, which can be encountered after a DFU transaction or in a media search. If you see an SOS, double check any connected media for stray `.bin` files and make sure you're not uploading a program that's supposed to run on the internal flash. diff --git a/doc/md/_a8_Getting-Started-SPI.md b/doc/md/_a8_Getting-Started-SPI.md new file mode 100644 index 0000000..33dbd5f --- /dev/null +++ b/doc/md/_a8_Getting-Started-SPI.md @@ -0,0 +1,176 @@ +# SPI + +SPI stands for Serial Peripheral Interface, and is a way for devices to talk to one another. +The protocol is used with one device as the main device or the leader. +All other devices are followers. + +There are four pins generally used: +- MOSI: Main Out Serial In +- MISO: Main In Serial Out +- SCLK: Serial Clock +- NSS: Serial Select (Chip Select) + +## How to Use SPI + +First create your SpiHandle object and a Config struct to configure it. +Set any configurations you want to change in the config object, then initialize the spi peripheral with it. + +A typical Daisy Seed configuration might look like this + +```cpp +// SpiHandle object and Spi Configuration object +SpiHandle spi_handle; +SpiHandle::Config spi_conf; + +// Set some configurations +spi_conf.periph = SpiHandle::Peripheral::SPI_1; +spi_conf.mode = SpiHandle::Mode::MASTER; +spi_conf.direction = SpiHandle::Direction::TWO_LINES; +spi_conf.nss = SpiHandle::NSS::NSS_HARD_OUTPUT; +spi_conf.pin_config.sclk = Pin(PORTG, 11); +spi_conf.pin_config.miso = Pin(PORTB, 4); +spi_conf.pin_config.mosi = Pin(PORTB, 5); +spi_conf.pin_config.nss = Pin(PORTG, 10); + +// Initialize the handle using our configuration +spi_handle.Init(spi_conf); +``` + +### Configuration + +The default configuration sets the common defaults for SPI peripherals. However some peripherals may require changing these. Some other configurations you'll have to set every time you use SPI. + +`spi_conf.peripheral` +The peripheral to be used. Must be specified by the user. For example `SpiHandle::Config::Peripheral::SPI_2`. +The Daisy has 6 different SPI peripherals available, each of which uses different pins. Refer to the pinout diagram for more details. + +`spi_conf.mode` +Master or slave. The main device is in charge of the bus (sets the clock, and NSS), the other devices follow along. Most of the time the Daisy will be in charge of the bus. +- `SpiHandle::Config::Mode::MASTER`. The Daisy runs the bus. +- `SpiHandle::Config::Mode::SLAVE`. The Daisy follows another device. + +`spi_conf.pin_config` +The pins to be used by this SPI peripheral. These will have to match the peripheral you choose. Must be specified by the user. + +- `spi_conf.pin_config.miso`: Main In Serial Out: The main device reads from this pin, and the others write to it. +- `spi_conf.pin_config.mosi`: Main Out Serial In: The main device writes to this pin, and the others read from it. +- `spi_conf.pin_config.sclk`: Serial Clock. The main device outputs a clock signal on this pin. +- `spi_conf.pin_config.nss`: Serial Select. The main device uses this to indicate data is being sent. Usually active low. + +If you're not using a pin (e.g. software NSS, or simplex communication) you can set it to `Pin()`. + +`spi_conf.direction` +Which direction data will travel. Must be specified by the user. + +- `SpiHandle::Config::Direction::TWO_LINES`. Data goes both ways. Each line only goes one direction. i.e. MOSI -> MISO and MISO <- MOSI. This is full duplex. +- `SpiHandle::Config::Direction::TWO_LINES_TX_ONLY`. The Daisy will only send data. If Daisy's the main device: MOSI->. If the Daisy's a follower MISO->. This is simplex TX. +- `SpiHandle::Config::Direction::TWO_LINES_RX_ONLY`. The Daisy will only read data. If Daisy;s the main device MISO<-. If Daisy's a follower MOSI<-. This is simplex RX. +- `SpiHandle::Config::Direction::ONE_LINE`. Data goes both ways over one line. i.e. MOSI <-> MISO. This is half duplex. + +`spi_conf.data_size` +How many bits in each transmission. Defaults to 8. +Must be in the range [4, 32] inclusive. + +`spi_conf.clock_polarity` +Is the clock active low or high? +(Determines if the "first" and "second" edges are rising or falling.) + +- `SpiHandle::Config::ClockPolarity::HIGH`. The clock is active high. +- `SpiHandle::Config::ClockPolarity::LOW`. The clock is active low. + +Defaults to `SpiHandle::Config::ClockPolarity::LOW`. + +`spi_conf.clock_phase` +When is the data ready to be read? +The "first edge" is when we transition __to__ the clock polarity. +The "second edge" is when we transition __away from__ the clock polarity. + +So if the polarity is low, the first edge is when the clock goes low. +Again, if the polarity is low, the second edge is when the clock goes high. + +- `SpiHandle::Config::ClockPhase::ONE_EDGE` Data is read on the first edge. +- `SpiHandle::Config::ClockPhase::TWO_EDGE`. Data is read on the second edge. + +Defaults to `SpiHandle::Config::ClockPhase::ONE_EDGE`. + +`spi_conf.nss` +Serial select mode. Must be set by the user. + +- `SpiHandle::Config::NSS::SOFT`. Serial select is handled in software. You can ignore the NSS pin. +- `SpiHandle::Config::NSS::HARD_INPUT`. The NSS pin is in use, and the Daisy is a follower. +- `SpiHandle::Config::NSS::HARD_OUTPUT`. The NSS pin is in use, and the Daisy is a leader. + +`spi_conf.baud_prescaler` +Division of the default clock rate. +The clock rate is always 25MHz. +So with a prescaler of 4 for example, the final clock rate is 25MHz / 4 = 6.25MHz. + +Defaults to `SpiHandle::Config::BaudPrescaler::PS_8` + +## Blocking Transmit and Receive + +Send / receive data in a blocking fashion. The code waits while the transmissions are taking place. + +```cpp +// send 4 bytes +uint8_t buffer[4] = {0, 1, 2, 3}; +spi_handle.BlockingTransmit(buffer, 4); +``` + +```cpp +// receive 4 bytes +uint8_t buffer[4]; +spi_handle.BlockingReceive(buffer, 4); +``` + +```cpp +// send and receive 4 bytes +uint8_t tx_buffer[4] = {0, 1, 2, 3}; +uint8_t rx_buffer[4]; +spi_handle.BlockingTransmitAndReceive(tx_buffer, rx_buffer, 4); +``` + +## DMA Transmit and Receive + +Send / receive data using DMA (Direct Memory Access). +This allows the hardware to handle the transmission in the background while the code is doing other things. +i.e. it is non blocking. + +You can also pass along a callback to be called when the transfer starts, another for when the transfer is over, and a pointer to some data to send those callbacks. + +**Note:** Your buffer has to be in the DMA section of memory, as well as in a global scope. + +```cpp +// buffer for sending data +uint8_t DMA_BUFFER_MEM_SECTION buffer[4]; + +// fill the buffer 0, 1, 2, 3 +for(uint8_t i = 0; i < 4; i++) +{ + buffer[i] = i; +} + +// transmit the data +spi_handle.DmaTransmit(buffer, 4, NULL, NULL, NULL); +``` + +```cpp +// receive 4 bytes. No callbacks or callback data. +uint8_t DMA_BUFFER_MEM_SECTION buffer[4]; +spi_handle.DmaReceive(buffer, 4, NULL, NULL, NULL); +``` + +```cpp +// send and receive 4 bytes. No callbacks or callback data. +uint8_t DMA_BUFFER_MEM_SECTION tx_buffer[4]; +uint8_t DMA_BUFFER_MEM_SECTION rx_buffer[4]; + +// fill the TX buffer 0, 1, 2, 3 +for(uint8_t i = 0; i < 4; i++) +{ + tx_buffer[i] = i; +} + +// transmit and receive +spi_handle.DmaTransmitAndReceive(tx_buffer, rx_buffer, 4, NULL, NULL, NULL); +``` \ No newline at end of file diff --git a/doc/md/reference_home.md b/doc/md/reference_home.md index f1d4e5f..2358f19 100644 --- a/doc/md/reference_home.md +++ b/doc/md/reference_home.md @@ -15,6 +15,8 @@ libDaisy is a C++ hardware support library for the Electrosmith Daisy platform. * [Getting Started - Audio](_a3_Getting-Started-Audio.md) * [Getting Started - the ADC Inputs](_a4_Getting-Started-ADCs.md) * [Getting Started - External SDRAM](_a6_Getting-Started-External-SDRAM.md) +* [Getting Started - Daisy Bootloader](_a7_Getting-Started-Daisy-Bootloader.md) +* [Getting Started - SPI](_a8_Getting-Started-SPI.md) ## Development diff --git a/examples/SPI/SpiBlockingTransmit/Makefile b/examples/SPI/SpiBlockingTransmit/Makefile new file mode 100644 index 0000000..ff4544b --- /dev/null +++ b/examples/SPI/SpiBlockingTransmit/Makefile @@ -0,0 +1,12 @@ +# Project Name +TARGET = SpiBlockingTransmit + +# Sources +CPP_SOURCES = SpiBlockingTransmit.cpp + +# Library Locations +LIBDAISY_DIR = ../../../ + +# Core location, and generic Makefile. +SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core +include $(SYSTEM_FILES_DIR)/Makefile diff --git a/examples/SPI/SpiBlockingTransmit/SpiBlockingTransmit.cpp b/examples/SPI/SpiBlockingTransmit/SpiBlockingTransmit.cpp new file mode 100644 index 0000000..157a994 --- /dev/null +++ b/examples/SPI/SpiBlockingTransmit/SpiBlockingTransmit.cpp @@ -0,0 +1,59 @@ +/** + * @author beserge + * @brief + * @date 2022-05-19 + * + * SPI Blocking Transmit Example + * Shows how to transmit some data over SPI on the Daisy Seed in a blocking manner. + * This means the code will wait while the transmission is occurring. + */ + +#include "daisy_seed.h" + +using namespace daisy; + +DaisySeed hw; + +int main(void) +{ + // Initialize the hardware + hw.Init(); + + // Handle we'll use to interact with SPI + SpiHandle spi_handle; + + // Structure to configure the SPI handle + SpiHandle::Config spi_conf; + + spi_conf.mode = SpiHandle::Config::Mode::MASTER; // we're in charge + + spi_conf.periph = SpiHandle::Config::Peripheral::SPI_1; // Use the SPI_1 Peripheral + + // Pins to use. These must be available on the selected peripheral + spi_conf.pin_config.sclk = seed::D8; // Use pin D8 as SCLK + spi_conf.pin_config.miso = Pin(); // We won't need this + spi_conf.pin_config.mosi = seed::D10; // Use D10 as MOSI + spi_conf.pin_config.nss = seed::D7; // use D7 as NSS + + // data will flow from master to slave over just the MOSI line + spi_conf.direction = SpiHandle::Config::Direction::TWO_LINES_TX_ONLY; + + // The master will output on the NSS line + spi_conf.nss = SpiHandle::Config::NSS::HARD_OUTPUT; + + // Initialize the SPI Handle + spi_handle.Init(spi_conf); + + // loop forever + while(1) + { + // put these four bytes in a buffer + uint8_t buffer[4] = {0, 1, 2, 3}; + + // transmit those 4 bytes + spi_handle.BlockingTransmit(buffer, 4); + + // wait 500 ms + System::Delay(500); + } +} diff --git a/examples/SPI/SpiDmaTransmit/Makefile b/examples/SPI/SpiDmaTransmit/Makefile new file mode 100644 index 0000000..d36a99e --- /dev/null +++ b/examples/SPI/SpiDmaTransmit/Makefile @@ -0,0 +1,12 @@ +# Project Name +TARGET = SpiDmaTransmit + +# Sources +CPP_SOURCES = SpiDmaTransmit.cpp + +# Library Locations +LIBDAISY_DIR = ../../../ + +# Core location, and generic Makefile. +SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core +include $(SYSTEM_FILES_DIR)/Makefile diff --git a/examples/SPI/SpiDmaTransmit/SpiDmaTransmit.cpp b/examples/SPI/SpiDmaTransmit/SpiDmaTransmit.cpp new file mode 100644 index 0000000..9bfe524 --- /dev/null +++ b/examples/SPI/SpiDmaTransmit/SpiDmaTransmit.cpp @@ -0,0 +1,62 @@ +/** + * @author beserge + * @brief + * @date 2022-05-19 + * + * SPI DMA Transmit + * Shows how to transmit some data over SPI on the Daisy Patch SM using DMA. + * (DMA = Direct Memory Access). This method is non-blocking, meaning the hardware will + * handle the transfer in the background, and the code is free to continue on. + */ + +#include "daisy_patch_sm.h" + +using namespace daisy; +using namespace patch_sm; + +DaisyPatchSM hw; + +// put these four bytes in a buffer. It has to be in a special memory section to be used with DMA +uint8_t DMA_BUFFER_MEM_SECTION buffer[4] = {0, 1, 2, 3}; + +int main(void) +{ + // Initialize the hardware + hw.Init(); + + // Handle we'll use to interact with SPI + SpiHandle spi_handle; + + // Structure to configure the SPI handle + SpiHandle::Config spi_conf; + + spi_conf.mode = SpiHandle::Config::Mode::MASTER; // we're in charge + + spi_conf.periph = SpiHandle::Config::Peripheral::SPI_2; // Use the SPI_2 Peripheral + + // Pins to use. These must be available on the selected peripheral + spi_conf.pin_config.sclk = DaisyPatchSM::D10; // Use pin D10 as SCLK + spi_conf.pin_config.miso = Pin(); // We won't need this + spi_conf.pin_config.mosi = DaisyPatchSM::D9; // Use D9 as MOSI + spi_conf.pin_config.nss = DaisyPatchSM::D1; // use D1 as NSS + + // data will flow from master to slave over just the MOSI line + spi_conf.direction = SpiHandle::Config::Direction::TWO_LINES_TX_ONLY; + + // The master will output on the NSS line + spi_conf.nss = SpiHandle::Config::NSS::HARD_OUTPUT; + + // Initialize the SPI Handle + spi_handle.Init(spi_conf); + + // loop forever + while(1) + { + // Use DMA to Transmit those 4 bytes. We don't have to wait for this to complete, we could go on and do other things here + // We won't use the callback or callback data features either, so we'll set them to NULL + spi_handle.DmaTransmit(buffer, 4, NULL, NULL, NULL); + + // wait 500 ms + System::Delay(500); + } +} diff --git a/examples/TIM_SingleCallback/Makefile b/examples/TIM_SingleCallback/Makefile new file mode 100644 index 0000000..6380b4a --- /dev/null +++ b/examples/TIM_SingleCallback/Makefile @@ -0,0 +1,12 @@ +# Project Name +TARGET = TIM_SingleCallback + +# Sources +CPP_SOURCES = TIM_SingleCallback.cpp + +# Library Locations +LIBDAISY_DIR = ../.. + +# Core location, and generic Makefile. +SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core +include $(SYSTEM_FILES_DIR)/Makefile diff --git a/examples/TIM_SingleCallback/TIM_SingleCallback.cpp b/examples/TIM_SingleCallback/TIM_SingleCallback.cpp new file mode 100644 index 0000000..6ffd03d --- /dev/null +++ b/examples/TIM_SingleCallback/TIM_SingleCallback.cpp @@ -0,0 +1,57 @@ +/** TIM Single Callback + * Example of setting up and using a TIM peripheral + * to generate periodic callbacks + * + * To demonstrate, the LED will be toggled from the callback + */ +#include "daisy_seed.h" + +using namespace daisy; + +/** Create Daisy Seed object */ +DaisySeed hw; + +/** This is our Timer-generated callback + * + * Once the timer has been configured, and started, this function will be called + * at the end of each timer period. + */ +void Callback(void* data) +{ + /** Use system time to blink LED once per second (1023ms) */ + bool led_state = (System::GetNow() & 1023) > 511; + /** Set LED */ + hw.SetLed(led_state); +} + +int main(void) +{ + /** Initialize Daisy Seed */ + hw.Init(); + + /** Create Handle and config + * We'll use TIM5 here, but TIM3, and TIM4 are also available + * At this time, TIM2 is used by the System class for sub-millisecond time/delay functions + */ + TimerHandle tim5; + TimerHandle::Config tim_cfg; + + /** TIM5 with IRQ enabled */ + tim_cfg.periph = TimerHandle::Config::Peripheral::TIM_5; + tim_cfg.enable_irq = true; + + /** Configure frequency (30Hz) */ + auto tim_target_freq = 30; + auto tim_base_freq = System::GetPClk2Freq(); + tim_cfg.period = tim_base_freq / tim_target_freq; + + /** Initialize timer */ + tim5.Init(tim_cfg); + tim5.SetCallback(Callback); + + /** Start the timer, and generate callbacks at the end of each period */ + tim5.Start(); + + /** Do nothing here, and rely on callback to toggle LED */ + while(1) {} +} diff --git a/examples/uart/Blocking_Receive/Blocking_Receive.cpp b/examples/uart/Blocking_Receive/Blocking_Receive.cpp new file mode 100644 index 0000000..44f84ab --- /dev/null +++ b/examples/uart/Blocking_Receive/Blocking_Receive.cpp @@ -0,0 +1,49 @@ +#include "daisy_patch.h" + +using namespace daisy; + +DaisyPatch hw; +UartHandler uart; + +int main(void) +{ + // start the Daisy Patch + hw.Init(); + + // set up our UART peripheral + UartHandler::Config uart_conf; + uart_conf.periph = UartHandler::Config::Peripheral::USART_1; + uart_conf.mode = UartHandler::Config::Mode::TX_RX; + uart_conf.pin_config.tx = Pin(PORTB, 6); + uart_conf.pin_config.rx = Pin(PORTB, 7); + + // initialize the UART peripheral, and start reading + uart.Init(uart_conf); + + uint8_t rx[4] = {0,0,0,0}; + while(1) + { + // blocking rx + uart.BlockingReceive(rx, 4, 1000); + + // clear the display + hw.display.Fill(false); + + // draw the title text + char cstr[22]; + sprintf(cstr, "Uart Blocking Rx"); + hw.display.SetCursor(0, 0); + hw.display.WriteString(cstr, Font_7x10, true); + + // draw the receive buffer contents + sprintf(cstr, "%d %d %d %d", rx[0], rx[1], rx[2], rx[3]); + hw.display.SetCursor(0, 12); + hw.display.WriteString(cstr, Font_7x10, true); + + // update the display + hw.display.Update(); + + // wait 100 ms + System::Delay(100); + } +} diff --git a/examples/uart/Blocking_Receive/Makefile b/examples/uart/Blocking_Receive/Makefile new file mode 100644 index 0000000..40afa3f --- /dev/null +++ b/examples/uart/Blocking_Receive/Makefile @@ -0,0 +1,12 @@ +# Project Name +TARGET = Blocking_Receive + +# Sources +CPP_SOURCES = Blocking_Receive.cpp + +# Library Locations +LIBDAISY_DIR = ../../.. + +# Core location, and generic Makefile. +SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core +include $(SYSTEM_FILES_DIR)/Makefile diff --git a/examples/uart/Blocking_Transmit/Blocking_Transmit.cpp b/examples/uart/Blocking_Transmit/Blocking_Transmit.cpp new file mode 100644 index 0000000..285cfc0 --- /dev/null +++ b/examples/uart/Blocking_Transmit/Blocking_Transmit.cpp @@ -0,0 +1,29 @@ +#include "daisy_seed.h" + +using namespace daisy; + +DaisySeed hw; +UartHandler uart; + +int main(void) +{ + // Initialize the daisy hardware + hw.Init(); + + // Configure the Uart Peripheral + UartHandler::Config uart_conf; + uart_conf.periph = UartHandler::Config::Peripheral::USART_1; + uart_conf.mode = UartHandler::Config::Mode::TX; + uart_conf.pin_config.tx = Pin(PORTB, 6); + uart_conf.pin_config.rx = Pin(PORTB, 7); + + // Initialize the uart peripheral and start the DMA transmit + uart.Init(uart_conf); + + uint8_t tx = 0; + while(1) { + uart.BlockingTransmit(&tx, 1, 1000); + tx++; + System::Delay(200); + } +} diff --git a/examples/uart/Blocking_Transmit/Makefile b/examples/uart/Blocking_Transmit/Makefile new file mode 100644 index 0000000..446854d --- /dev/null +++ b/examples/uart/Blocking_Transmit/Makefile @@ -0,0 +1,12 @@ +# Project Name +TARGET = Blocking_Transmit + +# Sources +CPP_SOURCES = Blocking_Transmit.cpp + +# Library Locations +LIBDAISY_DIR = ../../.. + +# Core location, and generic Makefile. +SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core +include $(SYSTEM_FILES_DIR)/Makefile diff --git a/examples/uart/Blocking_Transmit_Dma_Receive/Blocking_Transmit_Dma_Receive.cpp b/examples/uart/Blocking_Transmit_Dma_Receive/Blocking_Transmit_Dma_Receive.cpp new file mode 100644 index 0000000..b9db09a --- /dev/null +++ b/examples/uart/Blocking_Transmit_Dma_Receive/Blocking_Transmit_Dma_Receive.cpp @@ -0,0 +1,67 @@ +#include "daisy_patch.h" + +using namespace daisy; + +DaisyPatch hw; +UartHandler uart; + +// buffer to send from +uint8_t DMA_BUFFER_MEM_SECTION buff[4]; + +bool dma_ready = true; + +// dma end callback, will start a new DMA transfer +void RestartUart(void* state, UartHandler::Result res) +{ + dma_ready = true; +} + +int main(void) +{ + // start the Daisy Patch + hw.Init(); + + // set up our UART peripheral + UartHandler::Config uart_conf; + uart_conf.periph = UartHandler::Config::Peripheral::USART_1; + uart_conf.mode = UartHandler::Config::Mode::TX_RX; + uart_conf.pin_config.tx = Pin(PORTB, 6); + uart_conf.pin_config.rx = Pin(PORTB, 7); + + // initialize the UART peripheral + uart.Init(uart_conf); + for(int i = 0; i < 4; i++){ + buff[i] = i; + } + while(1) + { + // blocking tx + uart.BlockingTransmit(buff, 4, 100); + + if(dma_ready) + { + uart.DmaReceive(buff, 4, NULL, RestartUart, NULL); + dma_ready = false; + } + + // clear the display + hw.display.Fill(false); + + // draw the title text + char cstr[22]; + sprintf(cstr, "Uart DMA Test"); + hw.display.SetCursor(0, 0); + hw.display.WriteString(cstr, Font_7x10, true); + + // draw the receive buffer contents + sprintf(cstr, "%d %d %d %d", buff[0], buff[1], buff[2], buff[3]); + hw.display.SetCursor(0, 12); + hw.display.WriteString(cstr, Font_7x10, true); + + // update the display + hw.display.Update(); + + // wait 100 ms + System::Delay(100); + } +} diff --git a/examples/uart/Blocking_Transmit_Dma_Receive/Makefile b/examples/uart/Blocking_Transmit_Dma_Receive/Makefile new file mode 100644 index 0000000..459e42c --- /dev/null +++ b/examples/uart/Blocking_Transmit_Dma_Receive/Makefile @@ -0,0 +1,12 @@ +# Project Name +TARGET = Blocking_Transmit_Dma_Receive + +# Sources +CPP_SOURCES = Blocking_Transmit_Dma_Receive.cpp + +# Library Locations +LIBDAISY_DIR = ../../.. + +# Core location, and generic Makefile. +SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core +include $(SYSTEM_FILES_DIR)/Makefile diff --git a/examples/uart/Blocking_Transmit_Fifo_Receive/Blocking_Transmit_Fifo_Receive.cpp b/examples/uart/Blocking_Transmit_Fifo_Receive/Blocking_Transmit_Fifo_Receive.cpp new file mode 100644 index 0000000..c2b13e2 --- /dev/null +++ b/examples/uart/Blocking_Transmit_Fifo_Receive/Blocking_Transmit_Fifo_Receive.cpp @@ -0,0 +1,57 @@ +#include "daisy_patch.h" + +using namespace daisy; + +DaisyPatch hw; +UartHandler uart; + +int main(void) +{ + // start the Daisy Patch + hw.Init(); + + // set up our UART peripheral + UartHandler::Config uart_conf; + uart_conf.periph = UartHandler::Config::Peripheral::USART_1; + uart_conf.mode = UartHandler::Config::Mode::TX_RX; + uart_conf.pin_config.tx = Pin(PORTB, 6); + uart_conf.pin_config.rx = Pin(PORTB, 7); + + // initialize the UART peripheral, and start reading + uart.Init(uart_conf); + uart.DmaReceiveFifo(); + + uint8_t pop = 0; + uint8_t send = 0; + while(1) + { + // send the data in a blocking fashion + uart.BlockingTransmit(&send, 1); + send++; + + // if there's data, pop it from the FIFO + if(uart.ReadableFifo()){ + pop = uart.PopFifo(); + } + + // clear the display + hw.display.Fill(false); + + // draw the title text + char cstr[22]; + sprintf(cstr, "Uart DMA Test"); + hw.display.SetCursor(0, 0); + hw.display.WriteString(cstr, Font_7x10, true); + + // draw the receive buffer contents + sprintf(cstr, "%d", pop); + hw.display.SetCursor(0, 12); + hw.display.WriteString(cstr, Font_7x10, true); + + // update the display + hw.display.Update(); + + // wait 100 ms + System::Delay(100); + } +} diff --git a/examples/uart/Blocking_Transmit_Fifo_Receive/Makefile b/examples/uart/Blocking_Transmit_Fifo_Receive/Makefile new file mode 100644 index 0000000..e51a605 --- /dev/null +++ b/examples/uart/Blocking_Transmit_Fifo_Receive/Makefile @@ -0,0 +1,12 @@ +# Project Name +TARGET = Blocking_Transmit_Fifo_Receive + +# Sources +CPP_SOURCES = Blocking_Transmit_Fifo_Receive.cpp + +# Library Locations +LIBDAISY_DIR = ../../.. + +# Core location, and generic Makefile. +SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core +include $(SYSTEM_FILES_DIR)/Makefile diff --git a/examples/uart/Dma_Fifo_Receive/Dma_Fifo_Receive.cpp b/examples/uart/Dma_Fifo_Receive/Dma_Fifo_Receive.cpp new file mode 100644 index 0000000..c49be84 --- /dev/null +++ b/examples/uart/Dma_Fifo_Receive/Dma_Fifo_Receive.cpp @@ -0,0 +1,57 @@ +#include "daisy_patch.h" + +using namespace daisy; + +DaisyPatch hw; +UartHandler uart; + +int main(void) +{ + // Initialize the Daisy Patch + hw.Init(); + + // Configure the Uart Peripheral + UartHandler::Config uart_conf; + uart_conf.periph = UartHandler::Config::Peripheral::USART_1; + uart_conf.mode = UartHandler::Config::Mode::RX; + uart_conf.pin_config.tx = Pin(PORTB, 6); + uart_conf.pin_config.rx = Pin(PORTB, 7); + + // Initialize the Uart Peripheral + uart.Init(uart_conf); + + // Start the FIFO Receive + uart.DmaReceiveFifo(); + + uint8_t pop = 0; + while(1) { + // if there's data, pop it from the FIFO + if(uart.ReadableFifo()){ + pop = uart.PopFifo(); + hw.seed.SetLed(false); + } + else{ + hw.seed.SetLed(true); + } + + // clear the display + hw.display.Fill(false); + + // draw the title text + char cstr[26]; + sprintf(cstr, "Uart DMA Fifo Rx"); + hw.display.SetCursor(0, 0); + hw.display.WriteString(cstr, Font_7x10, true); + + // draw the last popped data + sprintf(cstr, "%d", pop); + hw.display.SetCursor(0, 12); + hw.display.WriteString(cstr, Font_7x10, true); + + // update the display + hw.display.Update(); + + // wait 100 ms + System::Delay(100); + } +} diff --git a/examples/uart/Dma_Fifo_Receive/Makefile b/examples/uart/Dma_Fifo_Receive/Makefile new file mode 100644 index 0000000..ae797e0 --- /dev/null +++ b/examples/uart/Dma_Fifo_Receive/Makefile @@ -0,0 +1,12 @@ +# Project Name +TARGET = Dma_Fifo_Receive + +# Sources +CPP_SOURCES = Dma_Fifo_Receive.cpp + +# Library Locations +LIBDAISY_DIR = ../../.. + +# Core location, and generic Makefile. +SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core +include $(SYSTEM_FILES_DIR)/Makefile diff --git a/examples/uart/Dma_Receive/Dma_Receive.cpp b/examples/uart/Dma_Receive/Dma_Receive.cpp new file mode 100644 index 0000000..cd6e784 --- /dev/null +++ b/examples/uart/Dma_Receive/Dma_Receive.cpp @@ -0,0 +1,63 @@ +#include "daisy_patch.h" + +using namespace daisy; + +DaisyPatch hw; +UartHandler uart; + +// buffer to read into +uint8_t DMA_BUFFER_MEM_SECTION buff[4]; + +void RestartUart(void* state, UartHandler::Result res); + +// dma end callback, will start a new DMA transfer +void RestartUart(void* state, UartHandler::Result res) +{ + uart.DmaReceive(buff, 4, NULL, RestartUart, NULL); +} + +int main(void) +{ + // start the Daisy Patch + hw.Init(); + + // reset our receive buffer + for(int i = 0; i < 4; i++) + { + buff[i] = 0; + } + + // set up our UART peripheral + UartHandler::Config uart_conf; + uart_conf.periph = UartHandler::Config::Peripheral::USART_1; + uart_conf.mode = UartHandler::Config::Mode::RX; + uart_conf.pin_config.tx = Pin(PORTB, 6); + uart_conf.pin_config.rx = Pin(PORTB, 7); + + // initialize the UART peripheral, and start reading + uart.Init(uart_conf); + uart.DmaReceive(buff, 4, NULL, RestartUart, NULL); + + while(1) + { + // clear the display + hw.display.Fill(false); + + // draw the title text + char cstr[22]; + sprintf(cstr, "Uart DMA Rx Test"); + hw.display.SetCursor(0, 0); + hw.display.WriteString(cstr, Font_7x10, true); + + // draw the receive buffer contents + sprintf(cstr, "%d %d %d %d", buff[0], buff[1], buff[2], buff[3]); + hw.display.SetCursor(0, 12); + hw.display.WriteString(cstr, Font_7x10, true); + + // update the display + hw.display.Update(); + + // wait 100 ms + System::Delay(100); + } +} diff --git a/examples/uart/Dma_Receive/Makefile b/examples/uart/Dma_Receive/Makefile new file mode 100644 index 0000000..552be64 --- /dev/null +++ b/examples/uart/Dma_Receive/Makefile @@ -0,0 +1,12 @@ +# Project Name +TARGET = Dma_Receive + +# Sources +CPP_SOURCES = Dma_Receive.cpp + +# Library Locations +LIBDAISY_DIR = ../../.. + +# Core location, and generic Makefile. +SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core +include $(SYSTEM_FILES_DIR)/Makefile diff --git a/examples/uart/Dma_Transmit/Dma_Transmit.cpp b/examples/uart/Dma_Transmit/Dma_Transmit.cpp new file mode 100644 index 0000000..61647df --- /dev/null +++ b/examples/uart/Dma_Transmit/Dma_Transmit.cpp @@ -0,0 +1,48 @@ +#include "daisy_seed.h" + +using namespace daisy; + +DaisySeed hw; +UartHandler uart; + +#define BUFF_SIZE 256 + +// buffer to be dma transmitted +static uint8_t DMA_BUFFER_MEM_SECTION buff[BUFF_SIZE]; + +void RestartUart(void* state, UartHandler::Result res); + +// dma end callback, will start a new DMA transfer +void RestartUart(void* state, UartHandler::Result res) +{ + if(res != UartHandler::Result::ERR) + { + uart.DmaTransmit(buff, BUFF_SIZE, NULL, RestartUart, NULL); + } +} + +int main(void) +{ + // Initialize the daisy hardware + hw.Init(); + + // initilize the buffer + for(int i = 0; i < BUFF_SIZE; i++) + { + buff[i] = i; + } + + // Configure the Uart Peripheral + UartHandler::Config uart_conf; + uart_conf.periph = UartHandler::Config::Peripheral::USART_1; + uart_conf.mode = UartHandler::Config::Mode::TX; + uart_conf.pin_config.tx = Pin(PORTB, 6); + uart_conf.pin_config.rx = Pin(PORTB, 7); + + // Initialize the uart peripheral and start the DMA transmit + uart.Init(uart_conf); + uart.DmaTransmit(buff, BUFF_SIZE, NULL, RestartUart, NULL); + + // loop forever + while(1) {} +} diff --git a/examples/uart/Dma_Transmit/Makefile b/examples/uart/Dma_Transmit/Makefile new file mode 100644 index 0000000..df25152 --- /dev/null +++ b/examples/uart/Dma_Transmit/Makefile @@ -0,0 +1,12 @@ +# Project Name +TARGET = Dma_Transmit + +# Sources +CPP_SOURCES = Dma_Transmit.cpp + +# Library Locations +LIBDAISY_DIR = ../../.. + +# Core location, and generic Makefile. +SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core +include $(SYSTEM_FILES_DIR)/Makefile diff --git a/examples/uart/Dma_Transmit_Blocking_Receive/Dma_Transmit_Blocking_Receive.cpp b/examples/uart/Dma_Transmit_Blocking_Receive/Dma_Transmit_Blocking_Receive.cpp new file mode 100644 index 0000000..7b23abc --- /dev/null +++ b/examples/uart/Dma_Transmit_Blocking_Receive/Dma_Transmit_Blocking_Receive.cpp @@ -0,0 +1,72 @@ +#include "daisy_patch.h" + +using namespace daisy; + +DaisyPatch hw; +UartHandler uart; + +// buffer to send from +uint8_t DMA_BUFFER_MEM_SECTION buff[256]; + +bool dma_ready = true; + +// dma end callback, will start a new DMA transfer +void RestartUart(void* state, UartHandler::Result res) +{ + dma_ready = true; +} + +int main(void) +{ + // start the Daisy Patch + hw.Init(); + + // init the tx buffer + for(int i = 0; i < 256; i++) + { + buff[i] = i; + } + + // set up our UART peripheral + UartHandler::Config uart_conf; + uart_conf.periph = UartHandler::Config::Peripheral::USART_1; + uart_conf.mode = UartHandler::Config::Mode::TX_RX; + uart_conf.pin_config.tx = Pin(PORTB, 6); + uart_conf.pin_config.rx = Pin(PORTB, 7); + + // initialize the UART peripheral, and start reading + uart.Init(uart_conf); + + uint8_t rx[4] = {0,0,0,0}; + while(1) + { + // blocking rx + uart.BlockingReceive(rx, 4, 1000); + + if(dma_ready) + { + uart.DmaTransmit(buff, 256, NULL, RestartUart, NULL); + dma_ready = false; + } + + // clear the display + hw.display.Fill(false); + + // draw the title text + char cstr[22]; + sprintf(cstr, "Uart DMA Test"); + hw.display.SetCursor(0, 0); + hw.display.WriteString(cstr, Font_7x10, true); + + // draw the receive buffer contents + sprintf(cstr, "%d %d %d %d", rx[0], rx[1], rx[2], rx[3]); + hw.display.SetCursor(0, 12); + hw.display.WriteString(cstr, Font_7x10, true); + + // update the display + hw.display.Update(); + + // wait 100 ms + System::Delay(100); + } +} diff --git a/examples/uart/Dma_Transmit_Blocking_Receive/Makefile b/examples/uart/Dma_Transmit_Blocking_Receive/Makefile new file mode 100644 index 0000000..44dc7da --- /dev/null +++ b/examples/uart/Dma_Transmit_Blocking_Receive/Makefile @@ -0,0 +1,12 @@ +# Project Name +TARGET = Dma_Transmit_Blocking_Receive + +# Sources +CPP_SOURCES = Dma_Transmit_Blocking_Receive.cpp + +# Library Locations +LIBDAISY_DIR = ../../.. + +# Core location, and generic Makefile. +SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core +include $(SYSTEM_FILES_DIR)/Makefile diff --git a/examples/uart/Dma_Transmit_Dma_Receive/Dma_Transmit_Dma_Receive.cpp b/examples/uart/Dma_Transmit_Dma_Receive/Dma_Transmit_Dma_Receive.cpp new file mode 100644 index 0000000..7dac0b5 --- /dev/null +++ b/examples/uart/Dma_Transmit_Dma_Receive/Dma_Transmit_Dma_Receive.cpp @@ -0,0 +1,70 @@ +#include "daisy_field.h" +#include "daisy_patch.h" + +using namespace daisy; + +// DaisyField hw; +DaisyPatch hw; +UartHandler uart; + +// rx / tx buffers +uint8_t DMA_BUFFER_MEM_SECTION rx_buff[4]; +uint8_t DMA_BUFFER_MEM_SECTION tx_buff[10]; + +void RestartUartTx(void* state, UartHandler::Result res); + +void RestartUartRx(void* state, UartHandler::Result res){ +} + +void RestartUartTx(void* state, UartHandler::Result res) +{ + uart.DmaReceive(rx_buff, 4, NULL, RestartUartRx, NULL); + uart.DmaTransmit(tx_buff, 10, NULL, RestartUartTx, NULL); +} + +int main(void) +{ + // start the Daisy Patch + hw.Init(); + + // reset our dma buffers + for(int i = 0; i < 10; i++) + { + tx_buff[i] = i; + } + + // set up our UART peripheral + UartHandler::Config uart_conf; + uart_conf.periph = UartHandler::Config::Peripheral::USART_1; + uart_conf.mode = UartHandler::Config::Mode::TX_RX; + uart_conf.pin_config.tx = Pin(PORTB, 6); + uart_conf.pin_config.rx = Pin(PORTB, 7); + + // initialize the UART peripheral, and start reading + uart.Init(uart_conf); + + uart.DmaReceive(rx_buff, 4, NULL, NULL, NULL); + uart.DmaTransmit(tx_buff, 10, NULL, RestartUartTx, NULL); + while(1) + { + // clear the display + hw.display.Fill(false); + + // draw the title text + char cstr[22]; + sprintf(cstr, "Uart DMA TxRx Test"); + hw.display.SetCursor(0, 0); + hw.display.WriteString(cstr, Font_7x10, true); + + // draw the receive buffer contents + sprintf(cstr, "%d %d %d %d", rx_buff[0], rx_buff[1], rx_buff[2], rx_buff[3]); + hw.display.SetCursor(0, 12); + hw.display.WriteString(cstr, Font_7x10, true); + + // update the display + hw.display.Update(); + + // wait 100 ms + System::Delay(100); + } +} diff --git a/examples/uart/Dma_Transmit_Dma_Receive/Makefile b/examples/uart/Dma_Transmit_Dma_Receive/Makefile new file mode 100644 index 0000000..5691dfc --- /dev/null +++ b/examples/uart/Dma_Transmit_Dma_Receive/Makefile @@ -0,0 +1,12 @@ +# Project Name +TARGET = Dma_Transmit_Dma_Receive + +# Sources +CPP_SOURCES = Dma_Transmit_Dma_Receive.cpp + +# Library Locations +LIBDAISY_DIR = ../../.. + +# Core location, and generic Makefile. +SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core +include $(SYSTEM_FILES_DIR)/Makefile diff --git a/libdaisy.vcxproj b/libdaisy.vcxproj index d172716..f106692 100644 --- a/libdaisy.vcxproj +++ b/libdaisy.vcxproj @@ -241,6 +241,7 @@ + @@ -436,6 +437,7 @@ + diff --git a/libdaisy.vcxproj.filters b/libdaisy.vcxproj.filters index bff3ff5..756ae9a 100644 --- a/libdaisy.vcxproj.filters +++ b/libdaisy.vcxproj.filters @@ -472,6 +472,9 @@ boards + + boards + dev @@ -1095,6 +1098,9 @@ boards + + boards + dev diff --git a/src/daisy.h b/src/daisy.h index 6eae7e6..38023cc 100644 --- a/src/daisy.h +++ b/src/daisy.h @@ -11,6 +11,7 @@ #include "per/gpio.h" #include "per/tim.h" #include "dev/leddriver.h" +#include "dev/mpr121.h" #include "dev/sdram.h" #include "dev/sr_4021.h" #include "hid/audio.h" @@ -41,11 +42,17 @@ #include "hid/led.h" #include "hid/rgb_led.h" #include "dev/sr_595.h" +#include "dev/apds9960.h" #include "dev/codec_pcm3060.h" #include "dev/codec_wm8731.h" +#include "dev/dps310.h" #include "dev/lcd_hd44780.h" #include "dev/mcp23x17.h" #include "dev/max11300.h" +#include "dev/tlv493d.h" +#include "dev/neopixel.h" +#include "dev/neotrellis.h" +#include "dev/icm20948.h" #include "ui/ButtonMonitor.h" #include "ui/PotMonitor.h" #include "ui/UI.h" diff --git a/src/daisy_legio.cpp b/src/daisy_legio.cpp new file mode 100644 index 0000000..9eb339e --- /dev/null +++ b/src/daisy_legio.cpp @@ -0,0 +1,201 @@ +#include "daisy_legio.h" + +using namespace daisy; + +#define PIN_SW_ENC 1 +#define PIN_TRIG_GATE 18 +#define PIN_TOGGLE3_RIGHT_A 30 +#define PIN_TOGGLE3_RIGHT_B 29 +#define PIN_TOGGLE3_LEFT_A 17 +#define PIN_TOGGLE3_LEFT_B 15 +#define PIN_ENC_A 2 +#define PIN_ENC_B 3 + +#define PIN_LED_LEFT_R 28 +#define PIN_LED_LEFT_G 6 +#define PIN_LED_LEFT_B 5 +#define PIN_LED_RIGHT_R 26 +#define PIN_LED_RIGHT_G 25 +#define PIN_LED_RIGHT_B 24 + +#define PIN_ADC_PITCH0 22 +#define PIN_ADC_PITCH1 23 +#define PIN_ADC_CV0 19 +#define PIN_ADC_CV1 21 + +void DaisyLegio::Init(bool boost) +{ + // seed init + seed.Configure(); + seed.Init(boost); + seed.SetAudioBlockSize(48); + float blockrate_ = seed.AudioSampleRate() / (float)seed.AudioBlockSize(); + + uint8_t toggle_pina[] = {PIN_TOGGLE3_LEFT_A, PIN_TOGGLE3_RIGHT_A}; + uint8_t toggle_pinb[] = {PIN_TOGGLE3_LEFT_B, PIN_TOGGLE3_RIGHT_B}; + uint8_t ledr_pin[] = {PIN_LED_LEFT_R, PIN_LED_RIGHT_R}; + uint8_t ledg_pin[] = {PIN_LED_LEFT_G, PIN_LED_RIGHT_G}; + uint8_t ledb_pin[] = {PIN_LED_LEFT_B, PIN_LED_RIGHT_B}; + uint8_t adc_pin[] = {PIN_ADC_PITCH0, PIN_ADC_CV0, PIN_ADC_CV1}; + + // push-button encoder + encoder.Init(seed.GetPin(PIN_ENC_A), + seed.GetPin(PIN_ENC_B), + seed.GetPin(PIN_SW_ENC)); + + // gate CV gate + dsy_gpio_pin gate_gpio = seed.GetPin(PIN_TRIG_GATE); + gate.Init(&gate_gpio, false); + + // 3-position switches + for(size_t i = 0; i < SW_LAST; i++) + { + sw[i].Init(seed.GetPin(toggle_pina[i]), seed.GetPin(toggle_pinb[i])); + } + + // ADC + AdcChannelConfig adc_cfg[CONTROL_LAST]; + for(size_t i = 0; i < CONTROL_LAST; i++) + { + adc_cfg[i].InitSingle(seed.GetPin(adc_pin[i])); + } + seed.adc.Init(adc_cfg, CONTROL_LAST); + + for(size_t i = 0; i < CONTROL_LAST; i++) + { + controls[i].Init(seed.adc.GetPtr(i), blockrate_, true); + } + + // RGB LEDs + for(size_t i = 0; i < LED_LAST; i++) + { + dsy_gpio_pin r = seed.GetPin(ledr_pin[i]); + dsy_gpio_pin g = seed.GetPin(ledg_pin[i]); + dsy_gpio_pin b = seed.GetPin(ledb_pin[i]); + leds[i].Init(r, g, b, true); + } +} + +void DaisyLegio::SetHidUpdateRates() +{ + for(size_t i = 0; i < CONTROL_LAST; i++) + { + controls[i].SetSampleRate(AudioCallbackRate()); + } +} + +void DaisyLegio::StartAudio(AudioHandle::InterleavingAudioCallback cb) +{ + seed.StartAudio(cb); +} + +void DaisyLegio::StartAudio(AudioHandle::AudioCallback cb) +{ + seed.StartAudio(cb); +} + +void DaisyLegio::ChangeAudioCallback(AudioHandle::InterleavingAudioCallback cb) +{ + seed.ChangeAudioCallback(cb); +} + +void DaisyLegio::ChangeAudioCallback(AudioHandle::AudioCallback cb) +{ + seed.ChangeAudioCallback(cb); +} + +void DaisyLegio::StopAudio() +{ + seed.StopAudio(); +} + +void DaisyLegio::SetAudioBlockSize(size_t size) +{ + seed.SetAudioBlockSize(size); + SetHidUpdateRates(); +} + +size_t DaisyLegio::AudioBlockSize() +{ + return seed.AudioBlockSize(); +} + +void DaisyLegio::SetAudioSampleRate(SaiHandle::Config::SampleRate samplerate) +{ + seed.SetAudioSampleRate(samplerate); + SetHidUpdateRates(); +} + +float DaisyLegio::AudioSampleRate() +{ + return seed.AudioSampleRate(); +} + +float DaisyLegio::AudioCallbackRate() +{ + return seed.AudioCallbackRate(); +} + +void DaisyLegio::StartAdc() +{ + seed.adc.Start(); +} + +void DaisyLegio::StopAdc() +{ + seed.adc.Stop(); +} + +void DaisyLegio::ProcessDigitalControls() +{ + encoder.Debounce(); +} + +void DaisyLegio::ProcessAnalogControls() +{ + for(size_t i = 0; i < CONTROL_LAST; i++) + { + controls[i].Process(); + } +} + +bool DaisyLegio::Gate() +{ + return !gate.State(); +} + +void DaisyLegio::SetLed(size_t idx, float red, float green, float blue) +{ + leds[idx].Set(red, green, blue); +} + +void DaisyLegio::DelayMs(size_t del) +{ + seed.DelayMs(del); +} + +void DaisyLegio::UpdateLeds() +{ + for(size_t i = 0; i < LED_LAST; i++) + { + leds[i].Update(); + } +} + +float DaisyLegio::GetKnobValue(int idx) +{ + return controls[idx].Value(); +} + +void DaisyLegio::UpdateExample() +{ + encoder.Debounce(); + SetLed(LED_LEFT, + sw[SW_LEFT].Read() / 2.0, + controls[CONTROL_PITCH].Value(), + controls[CONTROL_KNOB_TOP].Value()); + SetLed(LED_RIGHT, + sw[SW_RIGHT].Read() / 2.0, + controls[CONTROL_KNOB_BOTTOM].Value(), + gate.State() ? 1.0 : 0.0); +} \ No newline at end of file diff --git a/src/daisy_legio.h b/src/daisy_legio.h new file mode 100644 index 0000000..667ac0f --- /dev/null +++ b/src/daisy_legio.h @@ -0,0 +1,142 @@ +#pragma once +#include "daisy_seed.h" + +namespace daisy +{ +/** + @brief Class that handles initializing all of the hardware specific to the Virt Iter Legio hardware. \n + Helper funtions are also in place to provide easy access to built-in controls and peripherals. + @author Eris Fairbanks, Olivia Artz Modular + Noise Engineering + @date June 2022 + @ingroup boards +*/ +class DaisyLegio +{ + public: + // ENUMS to help index 2 LEDs, 2 controls/CV inputs, 2 3-position switches + enum LEGIO_LEDS + { + LED_LEFT, + LED_RIGHT, + LED_LAST + }; + + enum LEGIO_CONTROLS + { + CONTROL_PITCH, + CONTROL_KNOB_TOP, + CONTROL_KNOB_BOTTOM, + CONTROL_LAST + }; + + enum LEGIO_TOGGLE3 + { + SW_LEFT, + SW_RIGHT, + SW_LAST + }; + + DaisyLegio() {} + ~DaisyLegio() {} + + /**Initializes the Legio, and all of its hardware.*/ + void Init(bool boost = false); + + /** + Wait some ms before going on. + \param del Delay time in ms. + */ + void DelayMs(size_t del); + + /** Starts the callback + \param cb Interleaved callback function + */ + void StartAudio(AudioHandle::InterleavingAudioCallback cb); + + /** Starts the callback + \param cb Non-interleaved callback function + */ + void StartAudio(AudioHandle::AudioCallback cb); + + /** + Switch callback functions + \param cb New interleaved callback function. + */ + void ChangeAudioCallback(AudioHandle::InterleavingAudioCallback cb); + + /** + Switch callback functions + \param cb New non-interleaved callback function. + */ + void ChangeAudioCallback(AudioHandle::AudioCallback cb); + + /** Stops the audio if it is running. */ + void StopAudio(); + + /** Sets the number of samples processed per channel by the audio callback. + */ + void SetAudioBlockSize(size_t size); + + /** Returns the number of samples per channel in a block of audio. */ + size_t AudioBlockSize(); + + /** Updates the Audio Sample Rate, and reinitializes. + ** Audio must be stopped for this to work. + */ + void SetAudioSampleRate(SaiHandle::Config::SampleRate samplerate); + + /** Returns the audio sample rate in Hz as a floating point number. + */ + float AudioSampleRate(); + + /** Returns the rate in Hz that the Audio callback is called */ + float AudioCallbackRate(); + + /** Start analog to digital conversion.*/ + void StartAdc(); + + /** Stop converting ADCs */ + void StopAdc(); + + /** Process digital controls */ + void ProcessDigitalControls(); + + /** Normalize ADC CV input. Call this once per main loop update to normalize CV input to range (0.0f, 1.0f) */ + void ProcessAnalogControls(); + + /** Does what it says */ + inline void ProcessAllControls() + { + ProcessDigitalControls(); + ProcessAnalogControls(); + } + + /** Returns true if gate in is HIGH */ + bool Gate(); + + /** Set an LED (idx < 4) to a color */ + void SetLed(size_t idx, float red, float green, float blue); + + /** Get Knob Value, float from 0.0f to 1.0f */ + float GetKnobValue(int idx); + + /** Update LED PWM state. Call this once per main loop update to correctly display led colors */ + void UpdateLeds(); + + /* Call this once per main loop update to test the Legio Hardware + * Each input (1 gate in, 1 momentary in, 2 3-position switches, 3 controls) should change some or all of the output LEDs colors + **/ + void UpdateExample(); + + DaisySeed seed; + Encoder encoder; + GateIn gate; + RgbLed leds[LED_LAST]; + AnalogControl controls[CONTROL_LAST]; + Switch3 sw[SW_LAST]; + + private: + void SetHidUpdateRates(); +}; + +} // namespace daisy \ No newline at end of file diff --git a/src/dev/apds9960.h b/src/dev/apds9960.h new file mode 100644 index 0000000..e65c698 --- /dev/null +++ b/src/dev/apds9960.h @@ -0,0 +1,857 @@ +#pragma once +#ifndef DSY_APDS9960_H +#define DSY_APDS9960_H + +#define APDS9960_ADDRESS (0x39) /**< I2C Address */ + +#define APDS9960_UP 0x01 /**< Gesture Up */ +#define APDS9960_DOWN 0x02 /**< Gesture Down */ +#define APDS9960_LEFT 0x03 /**< Gesture Left */ +#define APDS9960_RIGHT 0x04 /**< Gesture Right */ + +/** I2C Registers */ +#define APDS9960_RAM 0x00 +#define APDS9960_ENABLE 0x80 +#define APDS9960_ATIME 0x81 +#define APDS9960_WTIME 0x83 +#define APDS9960_AILTIL 0x84 +#define APDS9960_AILTH 0x85 +#define APDS9960_AIHTL 0x86 +#define APDS9960_AIHTH 0x87 +#define APDS9960_PILT 0x89 +#define APDS9960_PIHT 0x8B +#define APDS9960_PERS 0x8C +#define APDS9960_CONFIG1 0x8D +#define APDS9960_PPULSE 0x8E +#define APDS9960_CONTROL 0x8F +#define APDS9960_CONFIG2 0x90 +#define APDS9960_ID 0x92 +#define APDS9960_STATUS 0x93 +#define APDS9960_CDATAL 0x94 +#define APDS9960_CDATAH 0x95 +#define APDS9960_RDATAL 0x96 +#define APDS9960_RDATAH 0x97 +#define APDS9960_GDATAL 0x98 +#define APDS9960_GDATAH 0x99 +#define APDS9960_BDATAL 0x9A +#define APDS9960_BDATAH 0x9B +#define APDS9960_PDATA 0x9C +#define APDS9960_POFFSET_UR 0x9D +#define APDS9960_POFFSET_DL 0x9E +#define APDS9960_CONFIG3 0x9F +#define APDS9960_GPENTH 0xA0 +#define APDS9960_GEXTH 0xA1 +#define APDS9960_GCONF1 0xA2 +#define APDS9960_GCONF2 0xA3 +#define APDS9960_GOFFSET_U 0xA4 +#define APDS9960_GOFFSET_D 0xA5 +#define APDS9960_GOFFSET_L 0xA7 +#define APDS9960_GOFFSET_R 0xA9 +#define APDS9960_GPULSE 0xA6 +#define APDS9960_GCONF3 0xAA +#define APDS9960_GCONF4 0xAB +#define APDS9960_GFLVL 0xAE +#define APDS9960_GSTATUS 0xAF +#define APDS9960_IFORCE 0xE4 +#define APDS9960_PICLEAR 0xE5 +#define APDS9960_CICLEAR 0xE6 +#define APDS9960_AICLEAR 0xE7 +#define APDS9960_GFIFO_U 0xFC +#define APDS9960_GFIFO_D 0xFD +#define APDS9960_GFIFO_L 0xFE +#define APDS9960_GFIFO_R 0xFF + +namespace daisy +{ +/** @addtogroup external + @{ +*/ + +/** I2C Transport for APDS9960 */ +class Apds9960I2CTransport +{ + public: + Apds9960I2CTransport() {} + ~Apds9960I2CTransport() {} + + struct Config + { + I2CHandle::Config::Peripheral periph; + I2CHandle::Config::Speed speed; + Pin scl; + Pin sda; + + Config() + { + periph = I2CHandle::Config::Peripheral::I2C_1; + speed = I2CHandle::Config::Speed::I2C_100KHZ; + + scl = Pin(PORTB, 8); + sda = Pin(PORTB, 9); + } + }; + + /** \return Did the transaction error? i.e. Return true if error, false if ok */ + inline bool Init(Config config) + { + I2CHandle::Config i2c_config; + i2c_config.mode = I2CHandle::Config::Mode::I2C_MASTER; + i2c_config.periph = config.periph; + i2c_config.speed = config.speed; + + i2c_config.pin_config.scl = config.scl; + i2c_config.pin_config.sda = config.sda; + + return I2CHandle::Result::OK != i2c_.Init(i2c_config); + } + + /** \return Did the transaction error? i.e. Return true if error, false if ok */ + bool Write(uint8_t *data, uint16_t size) + { + return I2CHandle::Result::OK + != i2c_.TransmitBlocking(APDS9960_ADDRESS, data, size, 10); + } + + /** \return Did the transaction error? i.e. Return true if error, false if ok */ + bool Read(uint8_t *data, uint16_t size) + { + return I2CHandle::Result::OK + != i2c_.ReceiveBlocking(APDS9960_ADDRESS, data, size, 10); + } + + private: + I2CHandle i2c_; +}; + +/** @brief Device support for APDS9960 + gesture / RGB / proximity sensor. + @author beserge + @date December 2021 +*/ +template +class Apds9960 +{ + public: + Apds9960() {} + ~Apds9960() {} + + struct Config + { + uint16_t integrationTimeMs; + uint8_t adcGain; // (0,3) -> {1x, 4x, 16x, 64x} + + uint8_t gestureDimensions; // (0,2) -> {all, up/down, left/right} + uint8_t + gestureFifoThresh; // (0,3) -> interrupt after 1 dataset in fifo, 2, 3, 4 + uint8_t gestureGain; // (0,3) -> {1x, 2x, 4x, 8x} + uint16_t gestureProximityThresh; + + bool color_mode; + bool prox_mode; + bool gesture_mode; + + typename Transport::Config transport_config; + + Config() + { + integrationTimeMs = 10; + adcGain = 1; // 4x + + gestureDimensions = 0; // gesture all + gestureFifoThresh = 1; // interrupt w/ 2 datasets in fifo + gestureGain = 2; // 4x gesture gain + gestureProximityThresh = 40; + + color_mode = true; + prox_mode = true; + gesture_mode = false; + } + }; + + enum Result + { + OK = 0, + ERR + }; + + // turn on/off elements + void enable(bool en = true); + /** Initialize the APDS9960 device + \param config Configuration settings + */ + Result Init(Config config) + { + config_ = config; + transport_error_ = false; + + SetTransportErr(transport_.Init(config_.transport_config)); + + /* Set default integration time and gain */ + SetADCIntegrationTime(config_.integrationTimeMs); + SetADCGain(config.adcGain); + + // disable everything to start + EnableGesture(false); + EnableProximity(false); + EnableColor(false); + + SetColorInterrupt(false); + SetProximityInterrupt(false); + ClearInterrupt(); + + /* Note: by default, the device is in power down mode on bootup */ + Enable(false); + System::Delay(10); + Enable(true); + System::Delay(10); + + // default to all gesture dimensions + SetGestureDimensions(config_.gestureDimensions); + SetGestureFIFOThreshold(config_.gestureFifoThresh); + SetGestureGain(config_.gestureGain); + SetGestureProximityThreshold(config_.gestureProximityThresh); + ResetCounts(); + + gpulse_.GPLEN = 0x03; // 32 us + gpulse_.GPULSE = 9; // 10 pulses + Write8(APDS9960_GPULSE, gpulse_.get()); + + // prox / color mode by default + // only one gesture or prox can be used at a time + // in gesture mode you should have prox mode on also, the data will just not be useful + EnableGesture(config_.gesture_mode); + EnableProximity(config_.prox_mode); + EnableColor(config_.color_mode); + + return GetTransportErr(); + } + + /** Sets the integration time for the ADC of the APDS9960, in millis + \param iTimeMS Integration time + */ + void SetADCIntegrationTime(uint16_t iTimeMS) + { + float temp; + + // convert ms into 2.78ms increments + temp = iTimeMS; + temp /= 2.78f; + temp = 256.f - temp; + if(temp > 255.f) + temp = 255.f; + if(temp < 0.f) + temp = 0.f; + + /* Update the timing register */ + Write8(APDS9960_ATIME, (uint8_t)temp); + } + + /** + * Returns the integration time for the ADC of the APDS9960, in millis + * \return Integration time + */ + float GetADCIntegrationTime() + { + float temp; + + temp = Read8(APDS9960_ATIME); + + // convert to units of 2.78 ms + temp = 256 - temp; + temp *= 2.78; + return temp; + } + + /** Adjusts the color/ALS gain on the APDS9960 (adjusts the sensitivity to light) + \param aGain Gain + */ + void SetADCGain(uint8_t aGain) + { + control_.AGAIN = aGain; + Write8(APDS9960_CONTROL, control_.get()); + } + + /** Sets gesture sensor offset + \param offset_up Up offset + \param offset_down Down offset + \param offset_left Left offset + \param offset_right Right offset + */ + void SetGestureOffset(uint8_t offset_up, + uint8_t offset_down, + uint8_t offset_left, + uint8_t offset_right) + { + Write8(APDS9960_GOFFSET_U, offset_up); + Write8(APDS9960_GOFFSET_D, offset_down); + Write8(APDS9960_GOFFSET_L, offset_left); + Write8(APDS9960_GOFFSET_R, offset_right); + } + + /** Sets gesture dimensions + \param dims Dimensions + */ + void SetGestureDimensions(uint8_t dims) + { + Write8(APDS9960_GCONF3, dims & 0x03); + } + + /** Sets gesture FIFO Threshold + \param thresh Threshold + */ + void SetGestureFIFOThreshold(uint8_t thresh) + { + gconf1_.GFIFOTH = thresh; + Write8(APDS9960_GCONF1, gconf1_.get()); + } + + /** Sets gesture sensor gain + \param gain Gain + */ + void SetGestureGain(uint8_t gain) + { + gconf2_.GGAIN = gain; + Write8(APDS9960_GCONF2, gconf2_.get()); + } + + /** Sets gesture sensor threshold + \param thresh Threshold + */ + void SetGestureProximityThreshold(uint8_t thresh) + { + Write8(APDS9960_GPENTH, thresh); + } + + /** Enables the device / Disables the device + (putting it in lower power sleep mode) + \param en Enable + */ + void Enable(bool en) + { + enable_.PON = en; + Write8(APDS9960_ENABLE, enable_.get()); + } + + /** Enable gesture readings + \param en Enable + */ + void EnableGesture(bool en) + { + if(!en) + { + gconf4_.GMODE = 0; + Write8(APDS9960_GCONF4, gconf4_.get()); + } + enable_.GEN = en; + Write8(APDS9960_ENABLE, enable_.get()); + ResetCounts(); + } + + /** Enable proximity readings + \param en Enable + */ + void EnableProximity(bool en) + { + enable_.PEN = en; + + Write8(APDS9960_ENABLE, enable_.get()); + } + + /** Enable color readings + \param en Enable + */ + void EnableColor(bool en) + { + enable_.AEN = en; + Write8(APDS9960_ENABLE, enable_.get()); + } + + /** Enables/disables color interrupt + \param en Enable / disable + */ + void SetColorInterrupt(bool en) + { + enable_.AIEN = en; + Write8(APDS9960_ENABLE, enable_.get()); + } + + /** Enables / Disables color interrupt + \param en Enable / disable + */ + void SetProximityInterrupt(bool en) + { + enable_.PIEN = en; + Write8(APDS9960_ENABLE, enable_.get()); + } + + /** Clears interrupt */ + void ClearInterrupt() + { + uint8_t val = APDS9960_AICLEAR; + SetTransportErr(transport_.Write(&val, 1)); + } + + /** Resets gesture counts */ + void ResetCounts() + { + gestCnt_ = 0; + UCount_ = 0; + DCount_ = 0; + LCount_ = 0; + RCount_ = 0; + } + + void Write8(uint8_t reg, uint8_t data) + { + uint8_t buff[2] = {reg, data}; + SetTransportErr(transport_.Write(buff, 2)); + } + + uint8_t Read8(uint8_t reg) + { + uint8_t buff[1] = {reg}; + SetTransportErr(transport_.Write(buff, 1)); + SetTransportErr(transport_.Read(buff, 1)); + return buff[0]; + } + + uint16_t Read16R(uint8_t reg) + { + uint8_t ret[2]; + SetTransportErr(transport_.Write(®, 1)); + SetTransportErr(transport_.Read(ret, 2)); + + return (ret[1] << 8) | ret[0]; + } + + /** Read proximity data + \return Proximity + */ + uint8_t ReadProximity() { return Read8(APDS9960_PDATA); } + + /** Adjusts the Proximity gain on the APDS9960 + \param pGain Gain + */ + void SetProxGain(uint8_t pGain) + { + control_.PGAIN = pGain; + + /* Update the timing register */ + Write8(APDS9960_CONTROL, control_.get()); + } + + /** Returns the Proximity gain on the APDS9960 + \return Proxmity gain + */ + uint8_t GetProxGain() { return ((Read8(APDS9960_CONTROL) & 0x0C) >> 2); } + + /** Sets number of proxmity pulses + \param pLen Pulse Length + \param pulses Number of pulses + */ + void SetProxPulse(uint8_t pLen, uint8_t pulses) + { + if(pulses < 1) + pulses = 1; + if(pulses > 64) + pulses = 64; + pulses--; + + ppulse_.PPLEN = pLen; + ppulse_.PPULSE = pulses; + + Write8(APDS9960_PPULSE, ppulse_.get()); + } + + /** Returns validity status of a gesture + \return Status (True/False) + */ + bool GestureValid() + { + gstatus_.set(Read8(APDS9960_GSTATUS)); + return gstatus_.GVALID; + } + + /** Reads gesture + \return Received gesture (1,4) -> {UP, DOWN, LEFT, RIGHT} + */ + uint8_t ReadGesture() + { + uint8_t toRead; + uint8_t buf[256]; + unsigned long t = 0; + uint8_t gestureReceived; + while(true) + { + int up_down_diff = 0; + int left_right_diff = 0; + gestureReceived = 0; + if(!GestureValid()) + return 0; + + System::Delay(30); + toRead = Read8(APDS9960_GFLVL); + + // produces sideffects needed for readGesture to work + uint8_t reg = APDS9960_GFIFO_U; + SetTransportErr(transport_.Write(®, 1)); + SetTransportErr(transport_.Read(buf, toRead)); + + if(abs((int)buf[0] - (int)buf[1]) > 13) + up_down_diff += (int)buf[0] - (int)buf[1]; + + if(abs((int)buf[2] - (int)buf[3]) > 13) + left_right_diff += (int)buf[2] - (int)buf[3]; + + if(up_down_diff != 0) + { + if(up_down_diff < 0) + { + if(DCount_ > 0) + { + gestureReceived = APDS9960_UP; + } + else + UCount_++; + } + else if(up_down_diff > 0) + { + if(UCount_ > 0) + { + gestureReceived = APDS9960_DOWN; + } + else + DCount_++; + } + } + + if(left_right_diff != 0) + { + if(left_right_diff < 0) + { + if(RCount_ > 0) + { + gestureReceived_ = APDS9960_LEFT; + } + else + LCount_++; + } + else if(left_right_diff > 0) + { + if(LCount_ > 0) + { + gestureReceived_ = APDS9960_RIGHT; + } + else + RCount_++; + } + } + + if(up_down_diff != 0 || left_right_diff != 0) + t = System::GetNow(); + + if(gestureReceived || System::GetNow() - t > 300) + { + ResetCounts(); + return gestureReceived; + } + } + } + + /** Set LED brightness for proximity/gesture + \param drive LED Drive (0,3) -> {100mA, 50mA, 25mA, 12.5mA} + \param boost LED Boost (0,3) -> {100%, 150%, 200%, 300%} + */ + void SetLED(uint8_t drive, uint8_t boost) + { + config2_.LED_BOOST = boost; + Write8(APDS9960_CONFIG2, config2_.get()); + + control_.LDRIVE = drive; + rite8(APDS9960_CONTROL, control_.get()); + } + + /** Converts the raw R/G/B values to color temperature in degrees Kelvin + \param r Red value + \param g Green value + \param b Blue value + + \return Color temperature + */ + uint16_t CalculateColorTemperature(uint16_t r, uint16_t g, uint16_t b) + { + float X, Y, Z; /* RGB to XYZ correlation */ + float xc, yc; /* Chromaticity co-ordinates */ + float n; /* McCamy's formula */ + float cct; + + /* 1. Map RGB values to their XYZ counterparts. */ + /* Based on 6500K fluorescent, 3000K fluorescent */ + /* and 60W incandescent values for a wide range. */ + /* Note: Y = Illuminance or lux */ + X = (-0.14282F * r) + (1.54924F * g) + (-0.95641F * b); + Y = (-0.32466F * r) + (1.57837F * g) + (-0.73191F * b); + Z = (-0.68202F * r) + (0.77073F * g) + (0.56332F * b); + + /* 2. Calculate the chromaticity co-ordinates */ + xc = (X) / (X + Y + Z); + yc = (Y) / (X + Y + Z); + + /* 3. Use McCamy's formula to determine the CCT */ + n = (xc - 0.3320F) / (0.1858F - yc); + + /* Calculate the final CCT */ + cct = (449.0F * powf(n, 3)) + (3525.0F * powf(n, 2)) + (6823.3F * n) + + 5520.33F; + + /* Return the results in degrees Kelvin */ + return (uint16_t)cct; + } + + /** Calculate ambient light values + \param r Red value + \param g Green value + \param b Blue value + \return LUX value + */ + uint16_t CalculateLux(uint16_t r, uint16_t g, uint16_t b) + { + float illuminance; + + /* This only uses RGB ... how can we integrate clear or calculate lux */ + /* based exclusively on clear since this might be more reliable? */ + illuminance = (-0.32466F * r) + (1.57837F * g) + (-0.73191F * b); + + return (uint16_t)illuminance; + } + + /** Sets interrupt limits + \param low Low limit + \param high High limit + */ + void SetIntLimits(uint16_t low, uint16_t high) + { + Write8(APDS9960_AILTIL, low & 0xFF); + Write8(APDS9960_AILTH, low >> 8); + Write8(APDS9960_AIHTL, high & 0xFF); + Write8(APDS9960_AIHTH, high >> 8); + } + + + /** Returns status of color data + \return True if color data ready, False otherwise + */ + bool ColorDataReady() + { + status_.set(Read8(APDS9960_STATUS)); + return status_.AVALID; + } + + + /** Reads the raw red channel value + \return Red channel value + */ + uint16_t GetColorDataRed() { return Read16R(APDS9960_RDATAL); } + + + /** Reads the raw green channel value + \return Green channel value + */ + uint16_t GetColorDataGreen() { return Read16R(APDS9960_GDATAL); } + + + /** Reads the raw blue channel value + \return Blue channel value + */ + uint16_t GetColorDataBlue() { return Read16R(APDS9960_BDATAL); } + + + /** Reads the raw clear channel value + \return Clear channel value + */ + uint16_t GetColorDataClear() { return Read16R(APDS9960_CDATAL); } + + + /** Reads the raw red, green, blue and clear channel values + \param *r Red value + \param *g Green value + \param *b Blue value + \param *c Clear channel value + */ + void GetColorData(uint16_t *r, uint16_t *g, uint16_t *b, uint16_t *c) + { + *c = GetColorDataClear(); + *r = GetColorDataRed(); + *g = GetColorDataGreen(); + *b = GetColorDataBlue(); + } + + private: + uint8_t gestCnt_, UCount_, DCount_, LCount_, RCount_; // counters + uint8_t gestureReceived_; + bool transport_error_; + + Config config_; + Transport transport_; + + /** Set the global transport_error_ bool */ + void SetTransportErr(bool err) { transport_error_ |= err; } + + /** Get the global transport_error_ bool (as a Result), then reset it */ + Result GetTransportErr() + { + Result ret = transport_error_ ? ERR : OK; + transport_error_ = false; + return ret; + } + + struct gconf1 + { + uint8_t GEXPERS : 2; // Gesture Exit Persistence + uint8_t GEXMSK : 4; // Gesture Exit Mask + uint8_t GFIFOTH : 2; // Gesture FIFO Threshold + + uint8_t get() { return (GFIFOTH << 6) | (GEXMSK << 2) | GEXPERS; } + }; + + gconf1 gconf1_; + + struct gconf2 + { + uint8_t GWTIME : 3; // Gesture Wait Time + uint8_t GLDRIVE : 2; // Gesture LED Drive Strength + uint8_t GGAIN : 2; // Gesture Gain Control + + uint8_t get() { return (GGAIN << 5) | (GLDRIVE << 3) | GWTIME; } + }; + gconf2 gconf2_; + + struct gconf4 + { + uint8_t GMODE : 1; // Gesture mode + uint8_t GIEN : 2; // Gesture Interrupt Enable + uint8_t get() { return (GIEN << 1) | GMODE; } + void set(uint8_t data) + { + GIEN = (data >> 1) & 0x01; + GMODE = data & 0x01; + } + }; + gconf4 gconf4_; + + struct control + { + uint8_t AGAIN : 2; // ALS and Color gain control + uint8_t PGAIN : 2; // proximity gain control + uint8_t LDRIVE : 2; // led drive strength + + uint8_t get() { return (LDRIVE << 6) | (PGAIN << 2) | AGAIN; } + }; + control control_; + + struct ppulse + { + uint8_t PPULSE : 6; //Proximity Pulse Count + uint8_t PPLEN : 2; // Proximity Pulse Length + + uint8_t get() { return (PPLEN << 6) | PPULSE; } + }; + ppulse ppulse_; + + struct enable + { + uint8_t PON : 1; // power on + uint8_t AEN : 1; // ALS enable + uint8_t PEN : 1; // Proximity detect enable + uint8_t WEN : 1; // wait timer enable + uint8_t AIEN : 1; // ALS interrupt enable + uint8_t PIEN : 1; // proximity interrupt enable + uint8_t GEN : 1; // gesture enable + + uint8_t get() + { + return (GEN << 6) | (PIEN << 5) | (AIEN << 4) | (WEN << 3) + | (PEN << 2) | (AEN << 1) | PON; + }; + }; + struct enable enable_; + + struct gpulse + { + uint8_t GPULSE : 6; // Number of gesture pulses = GPULSE + 1 + uint8_t GPLEN : 2; // Gesture Pulse Length + + uint8_t get() { return (GPLEN << 6) | GPULSE; } + }; + gpulse gpulse_; + + struct gstatus + { + uint8_t GVALID : 1; // Gesture FIFO Data Are we OK to read FIFO? + uint8_t GFOV : 1; // Gesture FIFO Overflow Flag + + void set(uint8_t data) + { + GFOV = (data >> 1) & 0x01; + GVALID = data & 0x01; + } + }; + gstatus gstatus_; + + struct config2 + { + /* Additional LDR current during proximity and gesture LED pulses. Current + value, set by LDRIVE, is increased by the percentage of LED_BOOST. + */ + uint8_t LED_BOOST : 2; + uint8_t CPSIEN : 1; // clear photodiode saturation int enable + uint8_t PSIEN : 1; // proximity saturation interrupt enable + + uint8_t get() + { + return (PSIEN << 7) | (CPSIEN << 6) | (LED_BOOST << 4) | 1; + } + }; + config2 config2_; + + struct status + { + uint8_t AVALID : 1; // ALS Valid + uint8_t PVALID : 1; // Proximity Valid + uint8_t GINT : 1; // Gesture Interrupt + uint8_t AINT : 1; // ALS Interrupt + uint8_t PINT : 1; // Proximity Interrupt + + /* Indicates that an analog saturation event occurred during a previous + proximity or gesture cycle. Once set, this bit remains set until cleared by + clear proximity interrupt special function command (0xE5 PICLEAR) or by + disabling Prox (PEN=0). This bit triggers an interrupt if PSIEN is set. + */ + uint8_t PGSAT : 1; + + /* Clear Photodiode Saturation. When asserted, the analog sensor was at the + upper end of its dynamic range. The bit can be de-asserted by sending a + Clear channel interrupt command (0xE6 CICLEAR) or by disabling the ADC + (AEN=0). This bit triggers an interrupt if CPSIEN is set. + */ + uint8_t CPSAT : 1; + + void set(uint8_t data) + { + AVALID = data & 0x01; + PVALID = (data >> 1) & 0x01; + GINT = (data >> 2) & 0x01; + AINT = (data >> 4) & 0x01; + PINT = (data >> 5) & 0x01; + PGSAT = (data >> 6) & 0x01; + CPSAT = (data >> 7) & 0x01; + } + }; + status status_; +}; + +/** @} */ + +using Apds9960I2C = Apds9960; +} // namespace daisy +#endif diff --git a/src/dev/dps310.h b/src/dev/dps310.h new file mode 100644 index 0000000..5457a4c --- /dev/null +++ b/src/dev/dps310.h @@ -0,0 +1,655 @@ +#pragma once +#ifndef DSY_DPS310_H +#define DSY_DPS310_H + +#define DPS310_I2CADDR_DEFAULT (0x77) ///< Default breakout addres + +#define DPS310_PRSB2 0x00 ///< Highest byte of pressure data +#define DPS310_TMPB2 0x03 ///< Highest byte of temperature data +#define DPS310_PRSCFG 0x06 ///< Pressure configuration +#define DPS310_TMPCFG 0x07 ///< Temperature configuration +#define DPS310_MEASCFG 0x08 ///< Sensor configuration +#define DPS310_CFGREG 0x09 ///< Interrupt/FIFO configuration +#define DPS310_RESET 0x0C ///< Soft reset +#define DPS310_PRODREVID 0x0D ///< Register that contains the part ID +#define DPS310_TMPCOEFSRCE 0x28 ///< Temperature calibration src + +namespace daisy +{ +/** @addtogroup external + @{ +*/ + +/** I2C Transport for Dps310 */ +class Dps310I2CTransport +{ + public: + Dps310I2CTransport() {} + ~Dps310I2CTransport() {} + + struct Config + { + I2CHandle::Config::Peripheral periph; + I2CHandle::Config::Speed speed; + Pin scl; + Pin sda; + + uint8_t address; + + Config() + { + address = DPS310_I2CADDR_DEFAULT; + + periph = I2CHandle::Config::Peripheral::I2C_1; + speed = I2CHandle::Config::Speed::I2C_400KHZ; + + scl = Pin(PORTB, 8); + sda = Pin(PORTB, 9); + } + }; + + inline void Init(Config config) + { + config_ = config; + + I2CHandle::Config i2c_config; + i2c_config.mode = I2CHandle::Config::Mode::I2C_MASTER; + i2c_config.periph = config.periph; + i2c_config.speed = config.speed; + + i2c_config.pin_config.scl = config.scl; + i2c_config.pin_config.sda = config.sda; + + i2c_.Init(i2c_config); + } + + void Write(uint8_t *data, uint16_t size) + { + error_ |= I2CHandle::Result::OK + != i2c_.TransmitBlocking(config_.address, data, size, 10); + } + + void Read(uint8_t *data, uint16_t size) + { + error_ |= I2CHandle::Result::OK + != i2c_.ReceiveBlocking(config_.address, data, size, 10); + } + + /** Writes an 8 bit value + \param reg the register address to write to + \param value the value to write to the register + */ + void Write8(uint8_t reg, uint8_t value) + { + uint8_t buffer[2]; + + buffer[0] = reg; + buffer[1] = value; + + Write(buffer, 2); + } + + /** Read from a reg address a defined number of bytes */ + void ReadReg(uint8_t reg, uint8_t *buff, uint8_t size) + { + Write(®, 1); + Read(buff, size); + } + + /** Reads an 8 bit value + \param reg the register address to read from + \returns the 16 bit data value read from the device + */ + uint8_t Read8(uint8_t reg) + { + uint8_t buffer; + ReadReg(reg, &buffer, 1); + return buffer; + } + + /** Reads a 16 bit value + \param reg the register address to read from + \returns the 16 bit data value read from the device + */ + uint16_t Read16(uint8_t reg) + { + uint8_t buffer[2]; + ReadReg(reg, buffer, 2); + + return uint16_t(buffer[0]) << 8 | uint16_t(buffer[1]); + } + + /** Reads a 24 bit value + \param reg the register address to read from + \returns the 24 bit data value read from the device + */ + uint32_t Read24(uint8_t reg) + { + uint8_t buffer[3]; + + ReadReg(reg, buffer, 3); + + return uint32_t(buffer[0]) << 16 | uint32_t(buffer[1]) << 8 + | uint32_t(buffer[2]); + } + + bool GetError() + { + bool tmp = error_; + error_ = false; + return tmp; + } + + private: + I2CHandle i2c_; + Config config_; + + // true if error has occured since last check + bool error_; +}; + +/** SPI Transport for Dps310 */ +class Dps310SpiTransport +{ + public: + Dps310SpiTransport() {} + ~Dps310SpiTransport() {} + + struct Config + { + SpiHandle::Config::Peripheral periph; + Pin sclk; + Pin miso; + Pin mosi; + Pin nss; + + Config() + { + periph = SpiHandle::Config::Peripheral::SPI_1; + sclk = Pin(PORTG, 11); + miso = Pin(PORTB, 4); + mosi = Pin(PORTB, 5); + nss = Pin(PORTG, 10); + } + }; + + inline void Init(Config config) + { + SpiHandle::Config spi_conf; + spi_conf.mode = SpiHandle::Config::Mode::MASTER; + spi_conf.direction = SpiHandle::Config::Direction::TWO_LINES; + spi_conf.clock_polarity = SpiHandle::Config::ClockPolarity::LOW; + spi_conf.clock_phase = SpiHandle::Config::ClockPhase::ONE_EDGE; + spi_conf.baud_prescaler = SpiHandle::Config::BaudPrescaler::PS_2; + spi_conf.nss = SpiHandle::Config::NSS::SOFT; + + spi_conf.periph = config.periph; + spi_conf.pin_config.sclk = config.sclk; + spi_conf.pin_config.miso = config.miso; + spi_conf.pin_config.mosi = config.mosi; + spi_conf.pin_config.nss = config.nss; + + spi_.Init(spi_conf); + } + + void Write(uint8_t *data, uint16_t size) + { + error_ |= SpiHandle::Result::OK != spi_.BlockingTransmit(data, size); + } + + void Read(uint8_t *data, uint16_t size) + { + error_ |= SpiHandle::Result::OK != spi_.BlockingReceive(data, size, 10); + } + + /** Writes an 8 bit value + \param reg the register address to write to + \param value the value to write to the register + */ + void Write8(uint8_t reg, uint8_t value) + { + uint8_t buffer[2]; + + buffer[0] = reg & ~0x80; + buffer[1] = value; + + Write(buffer, 2); + } + + /** Read from a reg address a defined number of bytes */ + void ReadReg(uint8_t reg, uint8_t *buff, uint8_t size) + { + reg = uint8_t(reg | 0x80); + Write(®, 1); + Read(buff, size); + } + + /** Reads an 8 bit value + \param reg the register address to read from + \returns the data uint8_t read from the device + */ + uint8_t Read8(uint8_t reg) + { + uint8_t buffer; + ReadReg(reg, &buffer, 1); + return buffer; + } + + /** 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 Read16(uint8_t reg) + { + uint8_t buffer[2]; + ReadReg(reg, buffer, 2); + + return uint16_t(buffer[0]) << 8 | uint16_t(buffer[1]); + } + + /** Reads a 24 bit value + \param reg the register address to read from + \returns the 24 bit data value read from the device + */ + uint32_t Read24(uint8_t reg) + { + uint8_t buffer[3]; + + ReadReg(reg, buffer, 3); + + return uint32_t(buffer[0]) << 16 | uint32_t(buffer[1]) << 8 + | uint32_t(buffer[2]); + } + + bool GetError() + { + bool tmp = error_; + error_ = false; + return tmp; + } + + private: + SpiHandle spi_; + bool error_; +}; + +/** \brief Device support for DPS310 Barometric Pressure and Altitude Sensor + @author beserge + @date January 2022 +*/ +template +class Dps310 +{ + public: + Dps310() {} + ~Dps310() {} + + int32_t oversample_scalefactor[8] + = {524288, 1572864, 3670016, 7864320, 253952, 516096, 1040384, 2088960}; + + /** The measurement rate ranges */ + enum dps310_rate_t + { + DPS310_1HZ, ///< 1 Hz + DPS310_2HZ, ///< 2 Hz + DPS310_4HZ, ///< 4 Hz + DPS310_8HZ, ///< 8 Hz + DPS310_16HZ, ///< 16 Hz + DPS310_32HZ, ///< 32 Hz + DPS310_64HZ, ///< 64 Hz + DPS310_128HZ, ///< 128 Hz + }; + + /** The oversample rate ranges */ + enum dps310_oversample_t + { + DPS310_1SAMPLE, ///< 1 Hz + DPS310_2SAMPLES, ///< 2 Hz + DPS310_4SAMPLES, ///< 4 Hz + DPS310_8SAMPLES, ///< 8 Hz + DPS310_16SAMPLES, ///< 16 Hz + DPS310_32SAMPLES, ///< 32 Hz + DPS310_64SAMPLES, ///< 64 Hz + DPS310_128SAMPLES, ///< 128 Hz + }; + + /** The oversample rate ranges */ + enum dps310_mode_t + { + DPS310_IDLE = 0b000, ///< Stopped/idle + DPS310_ONE_PRESSURE = 0b001, ///< Take single pressure measurement + DPS310_ONE_TEMPERATURE = 0b010, ///< Take single temperature measurement + DPS310_CONT_PRESSURE = 0b101, ///< Continuous pressure measurements + DPS310_CONT_TEMP = 0b110, ///< Continuous pressure measurements + DPS310_CONT_PRESTEMP = 0b111, ///< Continuous temp+pressure measurements + }; + + struct Config + { + typename Transport::Config transport_config; + + Config() {} + }; + + enum Result + { + OK = 0, + ERR + }; + + /** Initialize the Dps310 device + \param config Configuration settings + */ + Result Init(Config config) + { + config_ = config; + + transport_.Init(config_.transport_config); + + // make sure we're talking to the right chip + if(Read8(DPS310_PRODREVID) != 0x10) + { + // No DPS310 detected ... return false + return ERR; + } + + reset(); + _readCalibration(); + // default to high precision + configurePressure(DPS310_64HZ, DPS310_64SAMPLES); + configureTemperature(DPS310_64HZ, DPS310_64SAMPLES); + // continuous + setMode(DPS310_CONT_PRESTEMP); + // wait until we have at least one good measurement + while(!temperatureAvailable() || !pressureAvailable()) + { + System::Delay(10); + } + + return GetTransportError(); + } + + + /** Performs a software reset */ + void reset(void) + { + Write8(DPS310_RESET, 0x89); + + // Wait for a bit till its out of hardware reset + System::Delay(10); + + while(!ReadBits(DPS310_MEASCFG, 1, 6)) + { + System::Delay(1); + } + } + + static int32_t twosComplement(int32_t val, uint8_t bits) + { + if(val & ((uint32_t)1 << (bits - 1))) + { + val -= (uint32_t)1 << bits; + } + return val; + } + + void _readCalibration(void) + { + // Wait till we're ready to read calibration + while(!ReadBits(DPS310_MEASCFG, 1, 7)) + { + System::Delay(1); + } + + uint8_t coeffs[18]; + for(uint8_t addr = 0; addr < 18; addr++) + { + coeffs[addr] = Read8(0x10 + addr); + } + _c0 = ((uint16_t)coeffs[0] << 4) | (((uint16_t)coeffs[1] >> 4) & 0x0F); + _c0 = twosComplement(_c0, 12); + + _c1 = twosComplement((((uint16_t)coeffs[1] & 0x0F) << 8) | coeffs[2], + 12); + + _c00 = ((uint32_t)coeffs[3] << 12) | ((uint32_t)coeffs[4] << 4) + | (((uint32_t)coeffs[5] >> 4) & 0x0F); + _c00 = twosComplement(_c00, 20); + + _c10 = (((uint32_t)coeffs[5] & 0x0F) << 16) | ((uint32_t)coeffs[6] << 8) + | (uint32_t)coeffs[7]; + _c10 = twosComplement(_c10, 20); + + _c01 = twosComplement(((uint16_t)coeffs[8] << 8) | (uint16_t)coeffs[9], + 16); + _c11 = twosComplement( + ((uint16_t)coeffs[10] << 8) | (uint16_t)coeffs[11], 16); + _c20 = twosComplement( + ((uint16_t)coeffs[12] << 8) | (uint16_t)coeffs[13], 16); + _c21 = twosComplement( + ((uint16_t)coeffs[14] << 8) | (uint16_t)coeffs[15], 16); + _c30 = twosComplement( + ((uint16_t)coeffs[16] << 8) | (uint16_t)coeffs[17], 16); + } + + + /** Whether new temperature data is available + \return True if new data available to read + */ + bool temperatureAvailable(void) { return ReadBits(DPS310_MEASCFG, 1, 5); } + + + /** Whether new pressure data is available + \returns True if new data available to read + */ + bool pressureAvailable(void) { return ReadBits(DPS310_MEASCFG, 1, 4); } + + + /** Calculates the approximate altitude using barometric pressure and the supplied sea level hPa as a reference. + \param seaLevelhPa The current hPa at sea level. + \return The approximate altitude above sea level in meters. + */ + float GetAltitude(float seaLevelhPa) + { + float altitude; + + Process(); + + altitude = 44330 * (1.0 - pow((_pressure / 100) / seaLevelhPa, 0.1903)); + + return altitude; + } + + + /** Set the operational mode of the sensor (continuous or one-shot) + \param mode can be DPS310_IDLE, one shot: DPS310_ONE_PRESSURE or + DPS310_ONE_TEMPERATURE, continuous: DPS310_CONT_PRESSURE, DPS310_CONT_TEMP, + DPS310_CONT_PRESTEMP + */ + void setMode(dps310_mode_t mode) { WriteBits(DPS310_MEASCFG, mode, 3, 0); } + + + /** Set the sample rate and oversampling averaging for pressure + \param rate How many samples per second to take + \param os How many oversamples to average + */ + void configurePressure(dps310_rate_t rate, dps310_oversample_t os) + { + WriteBits(DPS310_PRSCFG, rate, 3, 4); + WriteBits(DPS310_PRSCFG, os, 4, 0); + + if(os > DPS310_8SAMPLES) + { + WriteBits(DPS310_CFGREG, 1, 1, 2); + } + else + { + WriteBits(DPS310_CFGREG, 0, 1, 2); + } + + pressure_scale = oversample_scalefactor[os]; + } + + + /** Set the sample rate and oversampling averaging for temperature + \param rate How many samples per second to take + \param os How many oversamples to average + */ + void configureTemperature(dps310_rate_t rate, dps310_oversample_t os) + { + WriteBits(DPS310_TMPCFG, rate, 3, 4); + WriteBits(DPS310_TMPCFG, os, 4, 0); + temp_scale = oversample_scalefactor[os]; + + // Set shift bit if necessary + if(os > DPS310_8SAMPLES) + { + WriteBits(DPS310_CFGREG, 1, 1, 3); + } + else + { + WriteBits(DPS310_CFGREG, 0, 1, 3); + } + + // Find out what our calibration source is + uint8_t read = ReadBits(DPS310_TMPCOEFSRCE, 1, 7); + WriteBits(DPS310_TMPCFG, read, 1, 7); + } + + + /** Read the XYZ data from the sensor and store in the internal + raw_pressure, raw_temperature, _pressure and _temperature variables. + */ + void Process(void) + { + raw_temperature = twosComplement(Read24(DPS310_TMPB2), 24); + raw_pressure = twosComplement(Read24(DPS310_PRSB2), 24); + + _scaled_rawtemp = (float)raw_temperature / temp_scale; + _temperature = _scaled_rawtemp * _c1 + _c0 / 2.0; + + _pressure = (float)raw_pressure / pressure_scale; + + _pressure + = (int32_t)_c00 + + _pressure + * ((int32_t)_c10 + + _pressure + * ((int32_t)_c20 + _pressure * (int32_t)_c30)) + + _scaled_rawtemp + * ((int32_t)_c01 + + _pressure + * ((int32_t)_c11 + _pressure * (int32_t)_c21)); + } + + /** Get last temperature reading + \return temp in degrees Centigrade + */ + float GetTemperature() { return _temperature; } + + /** Get the last pressure reading + \return Pressure in hPa + */ + float GetPressure() { return _pressure / 100; } + + /** Writes an 8 bit value + \param reg the register address to write to + \param value the value to write to the register + */ + void Write8(uint8_t reg, uint8_t value) + { + return transport_.Write8(reg, value); + } + + /** Read from a reg address a defined number of bytes */ + void ReadReg(uint8_t reg, uint8_t *buff, uint8_t size) + { + return transport_.ReadReg(reg, buff, size); + } + + /** Reads an 8 bit value + \param reg the register address to read from + \returns the data uint8_t read from the device + */ + uint8_t Read8(uint8_t reg) { return transport_.Read8(reg); } + + /** 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 Read16(uint8_t reg) { return transport_.Read16(reg); } + + /** Reads a 24 bit value + \param reg the register address to read from + \returns the 24 bit data value read from the device + */ + uint32_t Read24(uint8_t reg) { return transport_.Read24(reg); } + + /** 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 Read16_LE(uint8_t reg) + { + uint16_t temp = Read16(reg); + return (temp >> 8) | (temp << 8); + } + + /** 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 ReadS16(uint8_t reg) { return (int16_t)Read16(reg); } + + /** 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 ReadS16_LE(uint8_t reg) { return (int16_t)Read16_LE(reg); } + + + uint8_t ReadBits(uint8_t reg, uint8_t bits, uint8_t shift) + { + uint8_t val = Read8(reg); + val >>= shift; + return val & ((1 << (bits)) - 1); + } + + void WriteBits(uint8_t reg, uint8_t data, uint8_t bits, uint8_t shift) + { + uint8_t val = Read8(reg); + + // mask off the data before writing + uint8_t mask = (1 << (bits)) - 1; + data &= mask; + + mask <<= shift; + val &= ~mask; // remove the current data at that spot + val |= data << shift; // and add in the new data + + Write8(reg, val); + } + + /** Get and reset the transport error flag + \return Whether the transport has errored since the last check + */ + Result GetTransportError() { return transport_.GetError() ? ERR : OK; } + + private: + Config config_; + Transport transport_; + + int16_t _c0, _c1, _c01, _c11, _c20, _c21, _c30; + int32_t _c00, _c10; + + int32_t raw_pressure, raw_temperature; + float _temperature, _scaled_rawtemp, _pressure; + int32_t temp_scale, pressure_scale; +}; + +/** @} */ + +using Dps310I2C = Dps310; +using Dps310Spi = Dps310; +} // namespace daisy +#endif \ No newline at end of file diff --git a/src/dev/icm20948.h b/src/dev/icm20948.h new file mode 100644 index 0000000..c782e35 --- /dev/null +++ b/src/dev/icm20948.h @@ -0,0 +1,997 @@ +#pragma once +#ifndef DSY_ICM20948_H +#define DSY_ICM20948_H + +// Misc configuration macros +#define I2C_MASTER_RESETS_BEFORE_FAIL \ + 5 ///< The number of times to try resetting a stuck I2C master before giving up +#define NUM_FINISHED_CHECKS \ + 100 ///< How many times to poll I2C_SLV4_DONE before giving up and resetting + +// Bank 0 +#define ICM20X_B0_WHOAMI 0x00 ///< Chip ID register +#define ICM20X_B0_USER_CTRL 0x03 ///< User Control Reg. Includes I2C Master +#define ICM20X_B0_LP_CONFIG 0x05 ///< Low Power config +#define ICM20X_B0_REG_INT_PIN_CFG 0xF ///< Interrupt config register +#define ICM20X_B0_REG_INT_ENABLE 0x10 ///< Interrupt enable register 0 +#define ICM20X_B0_REG_INT_ENABLE_1 0x11 ///< Interrupt enable register 1 +#define ICM20X_B0_I2C_MST_STATUS \ + 0x17 ///< Records if I2C master bus data is finished +#define ICM20X_B0_REG_BANK_SEL 0x7F ///< register bank selection register +#define ICM20X_B0_PWR_MGMT_1 0x06 ///< primary power management register +#define ICM20X_B0_ACCEL_XOUT_H 0x2D ///< first byte of accel data +#define ICM20X_B0_GYRO_XOUT_H 0x33 ///< first byte of accel data + +// Bank 2 +#define ICM20X_B2_GYRO_SMPLRT_DIV 0x00 ///< Gyroscope data rate divisor +#define ICM20X_B2_GYRO_CONFIG_1 0x01 ///< Gyro config for range setting +#define ICM20X_B2_ACCEL_SMPLRT_DIV_1 0x10 ///< Accel data rate divisor MSByte +#define ICM20X_B2_ACCEL_SMPLRT_DIV_2 0x11 ///< Accel data rate divisor LSByte +#define ICM20X_B2_ACCEL_CONFIG_1 0x14 ///< Accel config for setting range + +// Bank 3 +#define ICM20X_B3_I2C_MST_ODR_CONFIG 0x0 ///< Sets ODR for I2C master bus +#define ICM20X_B3_I2C_MST_CTRL 0x1 ///< I2C master bus config +#define ICM20X_B3_I2C_MST_DELAY_CTRL 0x2 ///< I2C master bus config +#define ICM20X_B3_I2C_SLV0_ADDR \ + 0x3 ///< Sets I2C address for I2C master bus slave 0 +#define ICM20X_B3_I2C_SLV0_REG \ + 0x4 ///< Sets register address for I2C master bus slave 0 +#define ICM20X_B3_I2C_SLV0_CTRL 0x5 ///< Controls for I2C master bus slave 0 +#define ICM20X_B3_I2C_SLV0_DO 0x6 ///< Sets I2C master bus slave 0 data out + +#define ICM20X_B3_I2C_SLV4_ADDR \ + 0x13 ///< Sets I2C address for I2C master bus slave 4 +#define ICM20X_B3_I2C_SLV4_REG \ + 0x14 ///< Sets register address for I2C master bus slave 4 +#define ICM20X_B3_I2C_SLV4_CTRL 0x15 ///< Controls for I2C master bus slave 4 +#define ICM20X_B3_I2C_SLV4_DO 0x16 ///< Sets I2C master bus slave 4 data out +#define ICM20X_B3_I2C_SLV4_DI 0x17 ///< Sets I2C master bus slave 4 data in + +#define ICM20948_CHIP_ID 0xEA ///< ICM20948 default device id from WHOAMI + +#define ICM20948_I2CADDR_DEFAULT 0x69 ///< ICM20948 default i2c address +#define ICM20948_MAG_ID 0x09 ///< The chip ID for the magnetometer + +#define ICM20948_UT_PER_LSB 0.15 ///< mag data LSB value (fixed) + +#define AK09916_WIA2 0x01 ///< Magnetometer +#define AK09916_ST1 0x10 ///< Magnetometer +#define AK09916_HXL 0x11 ///< Magnetometer +#define AK09916_HXH 0x12 ///< Magnetometer +#define AK09916_HYL 0x13 ///< Magnetometer +#define AK09916_HYH 0x14 ///< Magnetometer +#define AK09916_HZL 0x15 ///< Magnetometer +#define AK09916_HZH 0x16 ///< Magnetometer +#define AK09916_ST2 0x18 ///< Magnetometer +#define AK09916_CNTL2 0x31 ///< Magnetometer +#define AK09916_CNTL3 0x32 ///< Magnetometer + +#define SENSORS_GRAVITY_EARTH (9.80665F) +#define SENSORS_DPS_TO_RADS (0.017453293F) + +namespace daisy +{ +/** @addtogroup external + @{ +*/ + +/** I2C Transport for Icm20948 */ +class Icm20948I2CTransport +{ + public: + Icm20948I2CTransport() {} + ~Icm20948I2CTransport() {} + + struct Config + { + I2CHandle::Config::Peripheral periph; + I2CHandle::Config::Speed speed; + Pin scl; + Pin sda; + + uint8_t address; + + Config() + { + address = ICM20948_I2CADDR_DEFAULT; + + periph = I2CHandle::Config::Peripheral::I2C_1; + speed = I2CHandle::Config::Speed::I2C_400KHZ; + + scl = Pin(PORTB, 8); + sda = Pin(PORTB, 9); + } + }; + + inline void Init(Config config) + { + config_ = config; + + I2CHandle::Config i2c_config; + i2c_config.mode = I2CHandle::Config::Mode::I2C_MASTER; + i2c_config.periph = config.periph; + i2c_config.speed = config.speed; + + i2c_config.pin_config.scl = config.scl; + i2c_config.pin_config.sda = config.sda; + + i2c_.Init(i2c_config); + } + + void Write(uint8_t *data, uint16_t size) + { + error_ |= I2CHandle::Result::OK + != i2c_.TransmitBlocking(config_.address, data, size, 10); + } + + void Read(uint8_t *data, uint16_t size) + { + error_ |= I2CHandle::Result::OK + != i2c_.ReceiveBlocking(config_.address, data, size, 10); + } + + /** Writes an 8 bit value + \param reg the register address to write to + \param value the value to write to the register + */ + void Write8(uint8_t reg, uint8_t value) + { + uint8_t buffer[2]; + + buffer[0] = reg; + buffer[1] = value; + + Write(buffer, 2); + } + + /** Writes a 16 bit value MSB first + \param reg the register address to write to + \param value the value to write to the register + */ + void Write16(uint8_t reg, uint16_t value) + { + uint8_t buffer[3]; + + buffer[0] = reg; + buffer[1] = value >> 8; + buffer[1] = value & 0xFF; + + Write(buffer, 2); + } + + /** Read from a reg address a defined number of bytes */ + void ReadReg(uint8_t reg, uint8_t *buff, uint8_t size) + { + Write(®, 1); + Read(buff, size); + } + + /** Reads an 8 bit value + \param reg the register address to read from + \return the 16 bit data value read from the device + */ + uint8_t Read8(uint8_t reg) + { + uint8_t buffer; + ReadReg(reg, &buffer, 1); + return buffer; + } + + bool GetError() + { + bool tmp = error_; + error_ = false; + return tmp; + } + + private: + I2CHandle i2c_; + Config config_; + + // true if error has occured since last check + bool error_; +}; + +/** SPI Transport for Icm20948 */ +class Icm20948SpiTransport +{ + public: + Icm20948SpiTransport() {} + ~Icm20948SpiTransport() {} + + struct Config + { + SpiHandle::Config::Peripheral periph; + Pin sclk; + Pin miso; + Pin mosi; + Pin nss; + + Config() + { + periph = SpiHandle::Config::Peripheral::SPI_1; + sclk = Pin(PORTG, 11); + miso = Pin(PORTB, 4); + mosi = Pin(PORTB, 5); + nss = Pin(PORTG, 10); + } + }; + + inline void Init(Config config) + { + SpiHandle::Config spi_conf; + spi_conf.mode = SpiHandle::Config::Mode::MASTER; + spi_conf.direction = SpiHandle::Config::Direction::TWO_LINES; + spi_conf.clock_polarity = SpiHandle::Config::ClockPolarity::LOW; + spi_conf.clock_phase = SpiHandle::Config::ClockPhase::ONE_EDGE; + spi_conf.baud_prescaler = SpiHandle::Config::BaudPrescaler::PS_2; + spi_conf.nss = SpiHandle::Config::NSS::SOFT; + + spi_conf.periph = config.periph; + spi_conf.pin_config.sclk = config.sclk; + spi_conf.pin_config.miso = config.miso; + spi_conf.pin_config.mosi = config.mosi; + spi_conf.pin_config.nss = config.nss; + + spi_.Init(spi_conf); + } + + void Write(uint8_t *data, uint16_t size) + { + error_ |= SpiHandle::Result::OK != spi_.BlockingTransmit(data, size); + } + + void Read(uint8_t *data, uint16_t size) + { + error_ |= SpiHandle::Result::OK != spi_.BlockingReceive(data, size, 10); + } + + /** Writes an 8 bit value + \param reg the register address to write to + \param value the value to write to the register + */ + void Write8(uint8_t reg, uint8_t value) + { + uint8_t buffer[2]; + + buffer[0] = reg & ~0x80; + buffer[1] = value; + + Write(buffer, 2); + } + + /** Writes a 16 bit value MSB first + \param reg the register address to write to + \param value the value to write to the register + */ + void Write16(uint8_t reg, uint16_t value) + { + uint8_t buffer[3]; + + buffer[0] = reg & ~0x80; + buffer[1] = value >> 8; + buffer[2] = value & 0xFF; + + Write(buffer, 3); + } + + /** Read from a reg address a defined number of bytes */ + void ReadReg(uint8_t reg, uint8_t *buff, uint8_t size) + { + reg = uint8_t(reg | 0x80); + Write(®, 1); + Read(buff, size); + } + + + /** Reads an 8 bit value + \param reg the register address to read from + \return the data uint8_t read from the device + */ + uint8_t Read8(uint8_t reg) + { + uint8_t buffer; + ReadReg(reg, &buffer, 1); + return buffer; + } + + bool GetError() + { + bool tmp = error_; + error_ = false; + return tmp; + } + + private: + SpiHandle spi_; + bool error_; +}; + +/** \brief Device support for ICM20948 IMU sensor + @author beserge + @date December 2021 +*/ +template +class Icm20948 +{ + public: + Icm20948() {} + ~Icm20948() {} + + struct Config + { + typename Transport::Config transport_config; + + Config() {} + }; + + struct Icm20948Vect + { + float x; + float y; + float z; + }; + + /** The accelerometer data range */ + enum icm20948_accel_range_t + { + ICM20948_ACCEL_RANGE_2_G, + ICM20948_ACCEL_RANGE_4_G, + ICM20948_ACCEL_RANGE_8_G, + ICM20948_ACCEL_RANGE_16_G, + }; + + /** The gyro data range */ + enum icm20948_gyro_range_t + { + ICM20948_GYRO_RANGE_250_DPS, + ICM20948_GYRO_RANGE_500_DPS, + ICM20948_GYRO_RANGE_1000_DPS, + ICM20948_GYRO_RANGE_2000_DPS, + }; + + /** Data rates/modes for the embedded AsahiKASEI AK09916 3-axis magnetometer */ + enum ak09916_data_rate_t + { + AK09916_MAG_DATARATE_SHUTDOWN = 0x0, ///< Stops measurement updates + AK09916_MAG_DATARATE_SINGLE + = 0x1, ///< Takes a single measurement then switches to + ///< AK09916_MAG_DATARATE_SHUTDOWN + AK09916_MAG_DATARATE_10_HZ = 0x2, ///< updates at 10Hz + AK09916_MAG_DATARATE_20_HZ = 0x4, ///< updates at 20Hz + AK09916_MAG_DATARATE_50_HZ = 0x6, ///< updates at 50Hz + AK09916_MAG_DATARATE_100_HZ = 0x8, ///< updates at 100Hz + }; + + enum Result + { + OK = 0, + ERR + }; + + /** Initialize the Icm20948 device + \param config Configuration settings + */ + Result Init(Config config) + { + config_ = config; + + transport_.Init(config_.transport_config); + + SetBank(0); + + uint8_t chip_id = Read8(ICM20X_B0_WHOAMI); + + if(chip_id != ICM20948_CHIP_ID) + { + return ERR; + } + + _sensorid_accel = 0; + _sensorid_gyro = 1; + _sensorid_mag = 2; + _sensorid_temp = 3; + + Reset(); + + // take out of default sleep state + WriteBits(ICM20X_B0_PWR_MGMT_1, 0, 1, 6); + + // 3 will be the largest range for either sensor + WriteGyroRange(3); + WriteAccelRange(3); + + // 1100Hz/(1+10) = 100Hz + SetGyroRateDivisor(10); + + // # 1125Hz/(1+20) = 53.57Hz + SetAccelRateDivisor(20); + + System::Delay(20); + + return GetTransportError(); + } + + /** Reset the internal registers and restores the default settings */ + void Reset() + { + SetBank(0); + + WriteBits(ICM20X_B0_PWR_MGMT_1, 1, 1, 7); + System::Delay(20); + + while(ReadBits(ICM20X_B0_PWR_MGMT_1, 1, 7)) + { + System::Delay(10); + }; + + System::Delay(50); + } + + uint8_t GetMagId() + { + // verify the magnetometer id + return ReadExternalRegister(0x8C, 0x01); + } + + Result SetupMag() + { + uint8_t buffer[2]; + + SetI2CBypass(false); + + ConfigureI2CMaster(); + + EnableI2CMaster(true); + + if(AuxI2CBusSetupFailed() == ERR) + { + return ERR; + } + + // set mag data rate + if(!SetMagDataRate(AK09916_MAG_DATARATE_100_HZ)) + { + // Serial.println("Error setting magnetometer data rate on external bus"); + return ERR; + } + + // TODO: extract method + // Set up Slave0 to proxy Mag readings + SetBank(3); + + // set up slave0 to proxy reads to mag + Write8(ICM20X_B3_I2C_SLV0_ADDR, 0x8C); + if(GetTransportError() != OK) + { + return ERR; + } + + Write8(ICM20X_B3_I2C_SLV0_REG, 0x10); + if(GetTransportError() != OK) + { + return ERR; + } + + // enable, read 9 bytes + Write8(ICM20X_B3_I2C_SLV0_CTRL, 0x89); + if(GetTransportError() != OK) + { + return ERR; + } + + return OK; + } + + /** + \param slv_addr + \param mag_reg_addr + \param num_finished_checks + \return uint8_t + */ + uint8_t ReadMagRegister(uint8_t mag_reg_addr) + { + return ReadExternalRegister(0x8C, mag_reg_addr); + } + + bool WriteMagRegister(uint8_t mag_reg_addr, uint8_t value) + { + return WriteExternalRegister(0x0C, mag_reg_addr, value); + } + + void ScaleValues() + { + icm20948_gyro_range_t gyro_range + = (icm20948_gyro_range_t)current_gyro_range_; + icm20948_accel_range_t accel_range + = (icm20948_accel_range_t)current_accel_range_; + + float accel_scale = 1.0; + float gyro_scale = 1.0; + + if(gyro_range == ICM20948_GYRO_RANGE_250_DPS) + gyro_scale = 131.0; + if(gyro_range == ICM20948_GYRO_RANGE_500_DPS) + gyro_scale = 65.5; + if(gyro_range == ICM20948_GYRO_RANGE_1000_DPS) + gyro_scale = 32.8; + if(gyro_range == ICM20948_GYRO_RANGE_2000_DPS) + gyro_scale = 16.4; + + if(accel_range == ICM20948_ACCEL_RANGE_2_G) + accel_scale = 16384.0; + if(accel_range == ICM20948_ACCEL_RANGE_4_G) + accel_scale = 8192.0; + if(accel_range == ICM20948_ACCEL_RANGE_8_G) + accel_scale = 4096.0; + if(accel_range == ICM20948_ACCEL_RANGE_16_G) + accel_scale = 2048.0; + + gyroX = rawGyroX / gyro_scale; + gyroY = rawGyroY / gyro_scale; + gyroZ = rawGyroZ / gyro_scale; + + accX = rawAccX / accel_scale; + accY = rawAccY / accel_scale; + accZ = rawAccZ / accel_scale; + + magX = rawMagX * ICM20948_UT_PER_LSB; + magY = rawMagY * ICM20948_UT_PER_LSB; + magZ = rawMagZ * ICM20948_UT_PER_LSB; + } + + /** Sets the accelerometer's data rate divisor. + \param new_accel_divisor The accelerometer's data rate divisor (`uint16_t`). This 12-bit value must be <= 4095 + */ + void SetAccelRateDivisor(uint16_t new_accel_divisor) + { + SetBank(2); + Write16(ICM20X_B2_ACCEL_SMPLRT_DIV_1, new_accel_divisor); + SetBank(0); + } + + /** Get the accelerometer's measurement range. + \return The accelerometer's measurement range (`icm20948_accel_range_t`). + */ + icm20948_accel_range_t GetAccelRange() + { + return (icm20948_accel_range_t)ReadAccelRange(); + } + + /** Get the accelerometer's measurement range. + \return The accelerometer's measurement range (`icm20x_accel_range_t`). + */ + uint8_t ReadAccelRange() + { + SetBank(2); + uint8_t range = ReadBits(ICM20X_B2_ACCEL_CONFIG_1, 2, 1); + SetBank(0); + return range; + } + + /** Sets the accelerometer's measurement range. + \param new_accel_range Measurement range to be set. Must be an `icm20x_accel_range_t`. + */ + void WriteAccelRange(uint8_t new_accel_range) + { + SetBank(2); + WriteBits(ICM20X_B2_ACCEL_CONFIG_1, new_accel_range, 2, 1); + current_accel_range_ = new_accel_range; + SetBank(0); + } + + /** Sets the accelerometer's measurement range. + \param new_accel_range Measurement range to be set. Must be an `icm20948_accel_range_t`. + */ + void SetAccelRange(icm20948_accel_range_t new_accel_range) + { + WriteAccelRange((uint8_t)new_accel_range); + } + + + /** Sets the gyro's data rate divisor. + @param new_gyro_divisor The gyro's data rate divisor (`uint8_t`). + */ + void SetGyroRateDivisor(uint8_t new_gyro_divisor) + { + SetBank(2); + Write8(ICM20X_B2_GYRO_SMPLRT_DIV, new_gyro_divisor); + SetBank(0); + } + + /** Get the gyro's measurement range. + \return The gyro's measurement range (`icm20948_gyro_range_t`). + */ + icm20948_gyro_range_t GetGyroRange() + { + return (icm20948_gyro_range_t)ReadGyroRange(); + } + + /** Sets the gyro's measurement range. + \param new_gyro_range Measurement range to be set. Must be an `icm20948_gyro_range_t`. + */ + void SetGyroRange(icm20948_gyro_range_t new_gyro_range) + { + WriteGyroRange((uint8_t)new_gyro_range); + } + + /** Sets the gyro's measurement range. + \param new_gyro_range Measurement range to be set. Must be an `icm20x_gyro_range_t`. + */ + void WriteGyroRange(uint8_t new_gyro_range) + { + SetBank(2); + WriteBits(ICM20X_B2_GYRO_CONFIG_1, new_gyro_range, 2, 1); + current_gyro_range_ = new_gyro_range; + SetBank(0); + } + + + /** Get the gyro's measurement range. + \return The gyro's measurement range (`icm20x_gyro_range_t`). + */ + uint8_t ReadGyroRange() + { + SetBank(2); + uint8_t range = ReadBits(ICM20X_B2_GYRO_CONFIG_1, 2, 1); + SetBank(0); + return range; + } + + + /** Get the current magnetometer measurement rate + \return ak09916_data_rate_t the current rate + */ + ak09916_data_rate_t GetMagDataRate() + { + uint8_t raw_mag_rate = ReadMagRegister(AK09916_CNTL2); + return (ak09916_data_rate_t)(raw_mag_rate); + } + + /** Set the magnetometer measurement rate + \param rate The rate to set. + \return true: success false: failure + */ + bool SetMagDataRate(ak09916_data_rate_t rate) + { + /* Following the datasheet, the sensor will be set to + * AK09916_MAG_DATARATE_SHUTDOWN followed by a 100ms delay, followed by + * setting the new data rate. + * + * See page 9 of https://www.y-ic.es/datasheet/78/SMDSW.020-2OZ.pdf */ + + // don't need to read/mask because there's nothing else in the register and + // it's right justified + bool success + = WriteMagRegister(AK09916_CNTL2, AK09916_MAG_DATARATE_SHUTDOWN); + System::Delay(1); + return WriteMagRegister(AK09916_CNTL2, rate) && success; + } + + /** Sets register bank. + \param bank_number The bank to set to active + */ + void SetBank(uint8_t bank_number) + { + Write8(ICM20X_B0_REG_BANK_SEL, (bank_number & 0b11) << 4); + } + + + /** Read a single byte from a given register address for an I2C slave device on the auxiliary I2C bus + \param slv_addr the 7-bit I2C address of the slave device + \param reg_addr the register address to read from + \return the requested register value + */ + uint8_t ReadExternalRegister(uint8_t slv_addr, uint8_t reg_addr) + { + return AuxillaryRegisterTransaction(true, slv_addr, reg_addr); + } + + /** Write a single byte to a given register address for an I2C slave device on the auxiliary I2C bus + \param slv_addr the 7-bit I2C address of the slave device + \param reg_addr the register address to write to + \param value the value to write + \return true + \return false + */ + bool + WriteExternalRegister(uint8_t slv_addr, uint8_t reg_addr, uint8_t value) + { + return (bool)AuxillaryRegisterTransaction( + false, slv_addr, reg_addr, value); + } + + /** Read / Write a single byte to a given register address for an I2C slave device on the auxiliary I2C bus + \param slv_addr the 7-bit I2C address of the slave device + \param reg_addr the register address to write to + \param value the value to write + \return Read value ( if it's a read operation ), else true or false + */ + uint8_t AuxillaryRegisterTransaction(bool read, + uint8_t slv_addr, + uint8_t reg_addr, + uint8_t value) + { + SetBank(3); + + if(read) + { + // set high bit for read, presumably for multi-byte reads + slv_addr |= 0x80; + } + else + { + Write8(ICM20X_B3_I2C_SLV4_DO, value); + if(GetTransportError() == ERR) + { + return (uint8_t) false; + } + } + + Write8(ICM20X_B3_I2C_SLV4_ADDR, slv_addr); + if(GetTransportError() == ERR) + { + return (uint8_t) false; + } + + Write8(ICM20X_B3_I2C_SLV4_REG, reg_addr); + if(GetTransportError() == ERR) + { + return (uint8_t) false; + } + + Write8(ICM20X_B3_I2C_SLV4_CTRL, 0x80); + if(GetTransportError() == ERR) + { + return (uint8_t) false; + } + + SetBank(0); + uint8_t tries = 0; + // wait until the operation is finished + while(ReadBits(ICM20X_B0_I2C_MST_STATUS, 1, 6) != true) + { + tries++; + if(tries >= NUM_FINISHED_CHECKS) + { + return (uint8_t) false; + } + } + + if(read) + { + SetBank(3); + return Read8(ICM20X_B3_I2C_SLV4_DI); + } + + return (uint8_t) true; + } + + /** Updates the measurement data for all sensors simultaneously */ + void Process() + { + SetBank(0); + + // reading 9 bytes of mag data to fetch the register that tells the mag we've + // read all the data + const uint8_t numbytes + = 14 + 9; // Read Accel, gyro, temp, and 9 bytes of mag + + uint8_t buffer[numbytes]; + transport_.ReadReg(ICM20X_B0_ACCEL_XOUT_H, buffer, numbytes); + + rawAccX = buffer[0] << 8 | buffer[1]; + rawAccY = buffer[2] << 8 | buffer[3]; + rawAccZ = buffer[4] << 8 | buffer[5]; + + rawGyroX = buffer[6] << 8 | buffer[7]; + rawGyroY = buffer[8] << 8 | buffer[9]; + rawGyroZ = buffer[10] << 8 | buffer[11]; + + temperature = buffer[12] << 8 | buffer[13]; + + rawMagX = ((buffer[16] << 8) + | (buffer[15] & 0xFF)); // Mag data is read little endian + rawMagY = ((buffer[18] << 8) | (buffer[17] & 0xFF)); + rawMagZ = ((buffer[20] << 8) | (buffer[19] & 0xFF)); + + ScaleValues(); + SetBank(0); + } + + Icm20948Vect GetAccelVect() + { + Icm20948Vect vect; + vect.x = accX * SENSORS_GRAVITY_EARTH; + vect.y = accY * SENSORS_GRAVITY_EARTH; + vect.z = accZ * SENSORS_GRAVITY_EARTH; + + return vect; + } + + Icm20948Vect GetGyroVect() + { + Icm20948Vect vect; + vect.x = gyroX * SENSORS_DPS_TO_RADS; + vect.y = gyroY * SENSORS_DPS_TO_RADS; + vect.z = gyroZ * SENSORS_DPS_TO_RADS; + + return vect; + } + + Icm20948Vect GetMagVect() + { + Icm20948Vect vect; + vect.x = magX; + vect.y = magY; + vect.z = magZ; + + return vect; + } + + float GetTemp() { return (temperature / 333.87) + 21.0; } + + /** Reads an 8 bit value + \param reg the register address to read from + \return the data uint8_t read from the device + */ + uint8_t Read8(uint8_t reg) { return transport_.Read8(reg); } + + /** Writes an 8 bit value + \param reg the register address to write to + \param value the value to write to the register + */ + void Write8(uint8_t reg, uint8_t value) + { + return transport_.Write8(reg, value); + } + + /** Writes a 16 bit value MSB first + \param reg the register address to write to + \param value the value to write to the register + */ + void Write16(uint8_t reg, uint16_t value) + { + return transport_.Write16(reg, value); + } + + /** Read from a reg address a defined number of bytes */ + void ReadReg(uint8_t reg, uint8_t *buff, uint8_t size) + { + return transport_.ReadReg(reg, buff, size); + } + + uint8_t ReadBits(uint8_t reg, uint8_t bits, uint8_t shift) + { + uint8_t val = Read8(reg); + val >>= shift; + return val & ((1 << (bits)) - 1); + } + + void WriteBits(uint8_t reg, uint8_t data, uint8_t bits, uint8_t shift) + { + uint8_t val = Read8(reg); + + // mask off the data before writing + uint8_t mask = (1 << (bits)) - 1; + data &= mask; + + mask <<= shift; + val &= ~mask; // remove the current data at that spot + val |= data << shift; // and add in the new data + + Write8(reg, val); + } + + /** Sets the bypass status of the I2C master bus support. + \param bypass_i2c Set to true to bypass the internal I2C master circuitry, + connecting the external I2C bus to the main I2C bus. Set to false to + re-connect + */ + void SetI2CBypass(bool bypass_i2c) + { + SetBank(0); + WriteBits(ICM20X_B0_REG_INT_PIN_CFG, bypass_i2c, 1, 1); + } + + /** Enable or disable the I2C mastercontroller + \param enable_i2c_master true: enable false: disable + \return true: success false: error + */ + Result EnableI2CMaster(bool enable_i2c_master) + { + SetBank(0); + WriteBits(ICM20X_B0_USER_CTRL, enable_i2c_master, 1, 5); + return GetTransportError(); + } + + /** Set the I2C clock rate for the auxillary I2C bus to 345.60kHz and disable repeated start + \return true: success false: failure + */ + Result ConfigureI2CMaster(void) + { + SetBank(3); + Write8(ICM20X_B3_I2C_MST_CTRL, 0x17); + return GetTransportError(); + } + + /** Reset the I2C master */ + void ResetI2CMaster(void) + { + SetBank(0); + + WriteBits(ICM20X_B0_USER_CTRL, true, 1, 1); + while(ReadBits(ICM20X_B0_USER_CTRL, 1, 1)) + { + System::Delay(10); + } + System::Delay(100); + } + + // A million thanks to the SparkFun folks for their library that I pillaged to + // write this method! See their Arduino library here: + // https://github.com/sparkfun/SparkFun_ICM-20948_ArduinoLibrary + Result AuxI2CBusSetupFailed(void) + { + // check aux I2C bus connection by reading the magnetometer chip ID + for(int i = 0; i < I2C_MASTER_RESETS_BEFORE_FAIL; i++) + { + if(GetMagId() != ICM20948_MAG_ID) + { + ResetI2CMaster(); + } + else + { + return ERR; + } + } + return OK; + } + + /** Get and reset the transport error flag + \return Whether the transport has errored since the last check + */ + Result GetTransportError() { return transport_.GetError() ? ERR : OK; } + + private: + Config config_; + Transport transport_; + + uint16_t _sensorid_accel, ///< ID number for accelerometer + _sensorid_gyro, ///< ID number for gyro + _sensorid_mag, ///< ID number for mag + _sensorid_temp; ///< ID number for temperature + + uint8_t current_accel_range_; ///< accelerometer range cache + uint8_t current_gyro_range_; ///< gyro range cache + + int16_t rawAccX, ///< temp variables + rawAccY, ///< temp variables + rawAccZ, ///< temp variables + rawTemp, ///< temp variables + rawGyroX, ///< temp variables + rawGyroY, ///< temp variables + rawGyroZ, ///< temp variables + rawMagX, ///< temp variables + rawMagY, ///< temp variables + rawMagZ; ///< temp variables + + float temperature, ///< Last reading's temperature (C) + accX, ///< Last reading's accelerometer X axis m/s^2 + accY, ///< Last reading's accelerometer Y axis m/s^2 + accZ, ///< Last reading's accelerometer Z axis m/s^2 + gyroX, ///< Last reading's gyro X axis in rad/s + gyroY, ///< Last reading's gyro Y axis in rad/s + gyroZ, ///< Last reading's gyro Z axis in rad/s + magX, ///< Last reading's mag X axis in rad/s + magY, ///< Last reading's mag Y axis in rad/s + magZ; ///< Last reading's mag Z axis in rad/s +}; + +/** @} */ + +using Icm20948I2C = Icm20948; +using Icm20948Spi = Icm20948; +} // namespace daisy +#endif \ No newline at end of file diff --git a/src/dev/mpr121.h b/src/dev/mpr121.h new file mode 100644 index 0000000..1a20868 --- /dev/null +++ b/src/dev/mpr121.h @@ -0,0 +1,360 @@ +#pragma once +#ifndef DSY_MPR121_H +#define DSY_MPR121_H + +// The default I2C address +#define MPR121_I2CADDR_DEFAULT 0x5A ///< default I2C address +#define MPR121_TOUCH_THRESHOLD_DEFAULT 12 ///< default touch threshold value +#define MPR121_RELEASE_THRESHOLD_DEFAULT 6 ///< default relese threshold value + +namespace daisy +{ +/** @addtogroup external + @{ +*/ + +/** I2C Transport for MPR121 */ +class Mpr121I2CTransport +{ + public: + Mpr121I2CTransport() {} + ~Mpr121I2CTransport() {} + + struct Config + { + I2CHandle::Config::Peripheral periph; + I2CHandle::Config::Speed speed; + Pin scl; + Pin sda; + + I2CHandle::Config::Mode mode; + + uint8_t dev_addr; + + Config() + { + periph = I2CHandle::Config::Peripheral::I2C_1; + speed = I2CHandle::Config::Speed::I2C_400KHZ; + + scl = Pin(PORTB, 8); + sda = Pin(PORTB, 9); + + dev_addr = MPR121_I2CADDR_DEFAULT; + } + }; + + /** \return Did the transaction error? i.e. Return true if error, false if ok */ + inline bool Init(Config config) + { + config_ = config; + + I2CHandle::Config i2c_conf; + i2c_conf.mode = I2CHandle::Config::Mode::I2C_MASTER; + i2c_conf.periph = config.periph; + i2c_conf.speed = config.speed; + + i2c_conf.pin_config.scl = config.scl; + i2c_conf.pin_config.sda = config.sda; + + return I2CHandle::Result::OK != i2c_.Init(i2c_conf); + } + + /** \return Did the transaction error? i.e. Return true if error, false if ok */ + bool Write(uint8_t *data, uint16_t size) + { + return I2CHandle::Result::OK + != i2c_.TransmitBlocking(config_.dev_addr, data, size, 10); + } + + /** \return Did the transaction error? i.e. Return true if error, false if ok */ + bool Read(uint8_t *data, uint16_t size) + { + return I2CHandle::Result::OK + != i2c_.ReceiveBlocking(config_.dev_addr, data, size, 10); + } + + private: + I2CHandle i2c_; + Config config_; +}; + + +/** @brief Device support for MPR121 12x Capacitive Touch Sensor + @author beserge + @date December 2021 +*/ +template +class Mpr121 +{ + public: + Mpr121() {} + ~Mpr121() {} + + struct Config + { + typename Transport::Config transport_config; + uint8_t touch_threshold; + uint8_t release_threshold; + + Config() + { + touch_threshold = MPR121_TOUCH_THRESHOLD_DEFAULT; + release_threshold = MPR121_RELEASE_THRESHOLD_DEFAULT; + } + }; + + enum Result + { + OK = 0, + ERR + }; + + /** Initialize the MPR121 device + \param config Configuration settings + */ + Result Init(Config config) + { + config_ = config; + + SetTransportErr(transport_.Init(config_.transport_config)); + + // soft reset + WriteRegister(MPR121_SOFTRESET, 0x63); + System::Delay(1); + + WriteRegister(MPR121_ECR, 0x0); + + // this doesn't work for some reason... + // uint8_t c = ReadRegister8(MPR121_CONFIG2); + + // if(c != 0x24) + // return ERR; + + SetThresholds(config_.touch_threshold, config_.release_threshold); + WriteRegister(MPR121_MHDR, 0x01); + WriteRegister(MPR121_NHDR, 0x01); + WriteRegister(MPR121_NCLR, 0x0E); + WriteRegister(MPR121_FDLR, 0x00); + + WriteRegister(MPR121_MHDF, 0x01); + WriteRegister(MPR121_NHDF, 0x05); + WriteRegister(MPR121_NCLF, 0x01); + WriteRegister(MPR121_FDLF, 0x00); + + WriteRegister(MPR121_NHDT, 0x00); + WriteRegister(MPR121_NCLT, 0x00); + WriteRegister(MPR121_FDLT, 0x00); + + WriteRegister(MPR121_DEBOUNCE, 0); + WriteRegister(MPR121_CONFIG1, 0x10); // default, 16uA charge current + WriteRegister(MPR121_CONFIG2, 0x20); // 0.5uS encoding, 1ms period + + // autoconfig + // WriteRegister(MPR121_AUTOCONFIG0, 0x0B); + + // correct values for Vdd = 3.3V + // WriteRegister(MPR121_UPLIMIT, 200); // ((Vdd - 0.7)/Vdd) * 256 + // WriteRegister(MPR121_TARGETLIMIT, 180); // UPLIMIT * 0.9 + // WriteRegister(MPR121_LOWLIMIT, 130); // UPLIMIT * 0.65 + // autoconfig + + // enable X electrodes and start MPR121 + uint8_t ECR_SETTING + = 0x80 + + 12; // 5 bits for baseline tracking & proximity disabled + X + // amount of electrodes running (12) + WriteRegister(MPR121_ECR, + ECR_SETTING); // start with above ECR setting + + return GetTransportErr(); + } + + /** Set the touch and release thresholds for all 13 channels on the + device to the passed values. + \param touch The touch threshold value from 0 to 255. + \param release The release threshold from 0 to 255. + */ + void SetThresholds(uint8_t touch, uint8_t release) + { + // set all thresholds (the same) + for(uint8_t i = 0; i < 12; i++) + { + WriteRegister(MPR121_TOUCHTH_0 + 2 * i, touch); + WriteRegister(MPR121_RELEASETH_0 + 2 * i, release); + } + } + + + /** Read the filtered data from channel t. The ADC raw data outputs + run through 3 levels of digital filtering to filter out the high + frequency and low frequency noise encountered. For detailed information on + this filtering see page 6 of the device datasheet. + \param t the channel to read + \returns the filtered reading as a 10 bit unsigned value + */ + uint16_t FilteredData(uint8_t t) + { + if(t > 12) + return 0; + return ReadRegister16(MPR121_FILTDATA_0L + t * 2); + } + + /** Read the baseline value for the channel. The 3rd level filtered + result is internally 10bit but only high 8 bits are readable + from registers 0x1E~0x2A as the baseline value output for each channel. + \param t the channel to read. + \returns the baseline data that was read + */ + uint16_t BaselineData(uint8_t t) + { + if(t > 12) + return 0; + uint16_t bl = ReadRegister8(MPR121_BASELINE_0 + t); + return (bl << 2); + } + + /** Read the touch status of all 13 channels as bit values in a 12 bit integer. + \returns a 12 bit integer with each bit corresponding to the touch status + of a sensor. For example, if bit 0 is set then channel 0 of the + device is currently deemed to be touched. + */ + uint16_t Touched() + { + uint16_t t = ReadRegister16(MPR121_TOUCHSTATUS_L); + return t & 0x0FFF; + } + + /** Read the contents of an 8 bit device register. + \param reg the register address to read from + \returns the 8 bit value that was read. + */ + uint8_t ReadRegister8(uint8_t reg) + { + uint8_t buff; + SetTransportErr(transport_.Write(®, 1)); + SetTransportErr(transport_.Read(&buff, 1)); + + return buff; + } + + /** Read the contents of a 16 bit device register. + \param reg the register address to read from + \returns the 16 bit value that was read. + */ + uint16_t ReadRegister16(uint8_t reg) + { + uint16_t buff; + SetTransportErr(transport_.Write(®, 1)); + SetTransportErr(transport_.Read((uint8_t *)&buff, 2)); + + return buff; + } + + /** Writes 8-bits to the specified destination register + \param reg the register address to write to + \param value the value to write + */ + void WriteRegister(uint8_t reg, uint8_t value) + { + // MPR121 must be put in Stop Mode to write to most registers + bool stop_required = true; + + // first get the current set value of the MPR121_ECR register + uint8_t ecr_reg = MPR121_ECR; + uint8_t buff[2] = {ecr_reg, 0x00}; + + SetTransportErr(transport_.Write(buff, 1)); + + uint8_t ecr_backup; + SetTransportErr(transport_.Read(&ecr_backup, 1)); + if((reg == MPR121_ECR) || ((0x73 <= reg) && (reg <= 0x7A))) + { + stop_required = false; + } + + if(stop_required) + { + // clear this register to set stop mode + SetTransportErr(transport_.Write(buff, 2)); + } + + buff[0] = reg; + buff[1] = value; + SetTransportErr(transport_.Write(buff, 2)); + + if(stop_required) + { + // write back the previous set ECR settings + buff[0] = ecr_reg; + buff[1] = ecr_backup; + SetTransportErr(transport_.Write(buff, 2)); + } + } + + /** Device register map */ + enum RegMap + { + MPR121_TOUCHSTATUS_L = 0x00, + MPR121_TOUCHSTATUS_H = 0x01, + MPR121_FILTDATA_0L = 0x04, + MPR121_FILTDATA_0H = 0x05, + MPR121_BASELINE_0 = 0x1E, + MPR121_MHDR = 0x2B, + MPR121_NHDR = 0x2C, + MPR121_NCLR = 0x2D, + MPR121_FDLR = 0x2E, + MPR121_MHDF = 0x2F, + MPR121_NHDF = 0x30, + MPR121_NCLF = 0x31, + MPR121_FDLF = 0x32, + MPR121_NHDT = 0x33, + MPR121_NCLT = 0x34, + MPR121_FDLT = 0x35, + + MPR121_TOUCHTH_0 = 0x41, + MPR121_RELEASETH_0 = 0x42, + MPR121_DEBOUNCE = 0x5B, + MPR121_CONFIG1 = 0x5C, + MPR121_CONFIG2 = 0x5D, + MPR121_CHARGECURR_0 = 0x5F, + MPR121_CHARGETIME_1 = 0x6C, + MPR121_ECR = 0x5E, + MPR121_AUTOCONFIG0 = 0x7B, + MPR121_AUTOCONFIG1 = 0x7C, + MPR121_UPLIMIT = 0x7D, + MPR121_LOWLIMIT = 0x7E, + MPR121_TARGETLIMIT = 0x7F, + + MPR121_GPIODIR = 0x76, + MPR121_GPIOEN = 0x77, + MPR121_GPIOSET = 0x78, + MPR121_GPIOCLR = 0x79, + MPR121_GPIOTOGGLE = 0x7A, + + MPR121_SOFTRESET = 0x80, + }; + + private: + Config config_; + Transport transport_; + bool transport_error_; + + /** Set the global transport_error_ bool */ + void SetTransportErr(bool err) { transport_error_ |= err; } + + /** Get the global transport_error_ bool (as a Result), then reset it */ + Result GetTransportErr() + { + Result ret = transport_error_ ? ERR : OK; + transport_error_ = false; + return ret; + } + +}; // class + +using Mpr121I2C = Mpr121; + +/** @} */ + +} // namespace daisy +#endif diff --git a/src/dev/neopixel.h b/src/dev/neopixel.h new file mode 100644 index 0000000..e6fad6c --- /dev/null +++ b/src/dev/neopixel.h @@ -0,0 +1,632 @@ +#pragma once +#ifndef DSY_NEO_PIXEL_H +#define DSY_NEO_PIXEL_H + +#define NEO_TRELLIS_ADDR_NEOPIXEL (0x2E) ///< Default Neotrellis I2C address + +// RGB NeoPixel permutations; white and red offsets are always same +// Offset: W R G B +#define NEO_RGB ((0 << 6) | (0 << 4) | (1 << 2) | (2)) +#define NEO_RBG ((0 << 6) | (0 << 4) | (2 << 2) | (1)) +#define NEO_GRB ((1 << 6) | (1 << 4) | (0 << 2) | (2)) +#define NEO_GBR ((2 << 6) | (2 << 4) | (0 << 2) | (1)) +#define NEO_BRG ((1 << 6) | (1 << 4) | (2 << 2) | (0)) +#define NEO_BGR ((2 << 6) | (2 << 4) | (1 << 2) | (0)) + +// RGBW NeoPixel permutations; all 4 offsets are distinct +// Offset: W R G B +#define NEO_WRGB ((0 << 6) | (1 << 4) | (2 << 2) | (3)) +#define NEO_WRBG ((0 << 6) | (1 << 4) | (3 << 2) | (2)) +#define NEO_WGRB ((0 << 6) | (2 << 4) | (1 << 2) | (3)) +#define NEO_WGBR ((0 << 6) | (3 << 4) | (1 << 2) | (2)) +#define NEO_WBRG ((0 << 6) | (2 << 4) | (3 << 2) | (1)) +#define NEO_WBGR ((0 << 6) | (3 << 4) | (2 << 2) | (1)) + +#define NEO_RWGB ((1 << 6) | (0 << 4) | (2 << 2) | (3)) +#define NEO_RWBG ((1 << 6) | (0 << 4) | (3 << 2) | (2)) +#define NEO_RGWB ((2 << 6) | (0 << 4) | (1 << 2) | (3)) +#define NEO_RGBW ((3 << 6) | (0 << 4) | (1 << 2) | (2)) +#define NEO_RBWG ((2 << 6) | (0 << 4) | (3 << 2) | (1)) +#define NEO_RBGW ((3 << 6) | (0 << 4) | (2 << 2) | (1)) + +#define NEO_GWRB ((1 << 6) | (2 << 4) | (0 << 2) | (3)) +#define NEO_GWBR ((1 << 6) | (3 << 4) | (0 << 2) | (2)) +#define NEO_GRWB ((2 << 6) | (1 << 4) | (0 << 2) | (3)) +#define NEO_GRBW ((3 << 6) | (1 << 4) | (0 << 2) | (2)) +#define NEO_GBWR ((2 << 6) | (3 << 4) | (0 << 2) | (1)) +#define NEO_GBRW ((3 << 6) | (2 << 4) | (0 << 2) | (1)) + +#define NEO_BWRG ((1 << 6) | (2 << 4) | (3 << 2) | (0)) +#define NEO_BWGR ((1 << 6) | (3 << 4) | (2 << 2) | (0)) +#define NEO_BRWG ((2 << 6) | (1 << 4) | (3 << 2) | (0)) +#define NEO_BRGW ((3 << 6) | (1 << 4) | (2 << 2) | (0)) +#define NEO_BGWR ((2 << 6) | (3 << 4) | (1 << 2) | (0)) +#define NEO_BGRW ((3 << 6) | (2 << 4) | (1 << 2) | (0)) + +// If 400 KHz support is enabled, the third parameter to the constructor +// requires a 16-bit value (in order to select 400 vs 800 KHz speed). +// If only 800 KHz is enabled (as is default on ATtiny), an 8-bit value +// is sufficient to encode pixel color order, saving some space. + +#define NEO_KHZ800 0x0000 // 800 KHz datastream +#define NEO_KHZ400 0x0100 // 400 KHz datastream + +namespace daisy +{ +/** @addtogroup external + @{ +*/ + +/** I2C Transport for NeoPixel */ +class NeoPixelI2CTransport +{ + public: + NeoPixelI2CTransport() {} + ~NeoPixelI2CTransport() {} + + struct Config + { + I2CHandle::Config::Peripheral periph; + I2CHandle::Config::Speed speed; + Pin scl; + Pin sda; + + uint8_t address; + + Config() + { + address = NEO_TRELLIS_ADDR_NEOPIXEL; + + periph = I2CHandle::Config::Peripheral::I2C_1; + speed = I2CHandle::Config::Speed::I2C_400KHZ; + + scl = Pin(PORTB, 8); + sda = Pin(PORTB, 9); + } + }; + + inline void Init(Config config) + { + config_ = config; + + I2CHandle::Config i2c_config; + i2c_config.mode = I2CHandle::Config::Mode::I2C_MASTER; + i2c_config.periph = config.periph; + i2c_config.speed = config.speed; + + i2c_config.pin_config.scl = config.scl; + i2c_config.pin_config.sda = config.sda; + + error_ |= I2CHandle::Result::OK != i2c_.Init(i2c_config); + } + + void Write(uint8_t *data, uint16_t size) + { + error_ |= I2CHandle::Result::OK + != i2c_.TransmitBlocking(config_.address, data, size, 10); + } + + void Read(uint8_t *data, uint16_t size) + { + error_ |= I2CHandle::Result::OK + != i2c_.ReceiveBlocking(config_.address, data, size, 10); + } + + void + ReadLen(uint8_t reg_high, uint8_t reg_low, uint8_t *buff, uint16_t size) + { + uint8_t reg[2] = {reg_high, reg_low}; + Write(reg, 2); + Read(buff, size); + } + + void + WriteLen(uint8_t reg_high, uint8_t reg_low, uint8_t *buff, uint16_t size) + { + // max write size of 126... + if(size >= 126) + { + return; + } + + uint8_t reg[128]; + + reg[0] = reg_high; + reg[1] = reg_low; + + for(int i = 0; i < size; i++) + { + reg[i + 2] = buff[i]; + } + + Write(reg, size + 2); + } + + /** Writes an 8 bit value + \param reg the register address to write to + \param value the value to write to the register + */ + void Write8(uint8_t reg_high, uint8_t reg_low, uint8_t value) + { + uint8_t buffer[3]; + + buffer[0] = reg_high; + buffer[1] = reg_low; + buffer[2] = value; + + Write(buffer, 3); + } + + /** Reads an 8 bit value + \param reg the register address to read from + \returns the 16 bit data value read from the device + */ + uint8_t Read8(uint8_t reg_high, uint8_t reg_low) + { + uint8_t buffer; + ReadLen(reg_high, reg_low, &buffer, 1); + return buffer; + } + + bool GetError() + { + bool tmp = error_; + error_ = false; + return tmp; + } + + private: + I2CHandle i2c_; + Config config_; + + // true if error has occured since last check + bool error_; +}; + +/** \brief Device support for Adafruit Neopixel Device + @author beserge + @date December 2021 +*/ +template +class NeoPixel +{ + public: + NeoPixel() {} + ~NeoPixel() {} + + struct Config + { + typename Transport::Config transport_config; + uint16_t type; + uint16_t numLEDs; + int8_t output_pin; + + Config() + { + type = NEO_GRB + NEO_KHZ800; + numLEDs = 16; + output_pin = 3; + } + }; + + enum Result + { + OK = 0, + ERR + }; + + typedef uint16_t neoPixelType; + + /** Module Base Addreses + The module base addresses for different seesaw modules. + */ + enum ModBaseAdd + { + SEESAW_STATUS_BASE = 0x00, + SEESAW_GPIO_BASE = 0x01, + SEESAW_SERCOM0_BASE = 0x02, + + SEESAW_TIMER_BASE = 0x08, + SEESAW_ADC_BASE = 0x09, + SEESAW_DAC_BASE = 0x0A, + SEESAW_INTERRUPT_BASE = 0x0B, + SEESAW_DAP_BASE = 0x0C, + SEESAW_EEPROM_BASE = 0x0D, + SEESAW_NEOPIXEL_BASE = 0x0E, + SEESAW_TOUCH_BASE = 0x0F, + SEESAW_KEYPAD_BASE = 0x10, + SEESAW_ENCODER_BASE = 0x11, + SEESAW_SPECTRUM_BASE = 0x12, + }; + + /** neopixel module function address registers */ + enum ModAddReg + { + SEESAW_NEOPIXEL_STATUS = 0x00, + SEESAW_NEOPIXEL_PIN = 0x01, + SEESAW_NEOPIXEL_SPEED = 0x02, + SEESAW_NEOPIXEL_BUF_LENGTH = 0x03, + SEESAW_NEOPIXEL_BUF = 0x04, + SEESAW_NEOPIXEL_SHOW = 0x05, + }; + + /** status module function address registers */ + enum StatAddReg + { + SEESAW_STATUS_HW_ID = 0x01, + SEESAW_STATUS_VERSION = 0x02, + SEESAW_STATUS_OPTIONS = 0x03, + SEESAW_STATUS_TEMP = 0x04, + SEESAW_STATUS_SWRST = 0x7F, + }; + + /** Initialize the NeoPixel device + \param config Configuration settings + */ + Result Init(Config config) + { + config_ = config; + + type = config_.type; + numLEDs = config_.numLEDs; + pin = config_.output_pin; + pixels = pixelsd; + + transport_.Init(config_.transport_config); + + SWReset(); + + // 10 ms delay + System::Delay(10); + + UpdateType(type); + UpdateLength(numLEDs); + SetPin(pin); + + return GetTransportError(); + } + + void Write(uint8_t reg_high, uint8_t reg_low, uint8_t *buff, uint8_t size) + { + transport_.WriteLen(reg_high, reg_low, buff, size); + } + + /** Writes an 8 bit value + \param reg the register address to write to + \param value the value to write to the register + */ + void Write8(uint8_t reg_high, uint8_t reg_low, uint8_t value) + { + return transport_.Write8(reg_high, reg_low, value); + } + + /** Reads an 8 bit value + \param reg the register address to read from + \returns the data uint8_t read from the device + */ + uint8_t Read8(uint8_t reg_high, uint8_t reg_low) + { + return transport_.Read8(reg_high, reg_low); + } + + void ReadLen(uint8_t reg_high, uint8_t reg_low, uint8_t *buff, uint8_t len) + { + transport_.ReadLen(reg_high, reg_low, buff, len); + } + + /** Get and reset the transport error flag + \return Whether the transport has errored since the last check + */ + Result GetTransportError() { return transport_.GetError() ? ERR : OK; } + + /** Perform a software reset. + This resets all seesaw registers to their default values. + This is called automatically from Init(). + */ + void SWReset() + { + return Write8(SEESAW_STATUS_BASE, SEESAW_STATUS_SWRST, 0xFF); + } + + void UpdateLength(uint16_t n) + { + // Allocate new data -- note: ALL PIXELS ARE CLEARED + numBytes = n * ((wOffset == rOffset) ? 3 : 4); + mymemset(pixels, 0, numBytes); + numLEDs = n; + + uint8_t buf[] = {(uint8_t)(numBytes >> 8), (uint8_t)(numBytes & 0xFF)}; + Write(SEESAW_NEOPIXEL_BASE, SEESAW_NEOPIXEL_BUF_LENGTH, buf, 2); + } + + void UpdateType(neoPixelType t) + { + bool oldThreeBytesPerPixel = (wOffset == rOffset); // false if RGBW + + wOffset = (t >> 6) & 0b11; // See notes in header file + rOffset = (t >> 4) & 0b11; // regarding R/G/B/W offsets + gOffset = (t >> 2) & 0b11; + bOffset = t & 0b11; + is800KHz = (t < 256); // 400 KHz flag is 1<<8 + + Write8(SEESAW_NEOPIXEL_BASE, SEESAW_NEOPIXEL_SPEED, is800KHz); + + // If bytes-per-pixel has changed (and pixel data was previously + // allocated), re-allocate to new size. Will clear any data. + bool newThreeBytesPerPixel = (wOffset == rOffset); + if(newThreeBytesPerPixel != oldThreeBytesPerPixel) + UpdateLength(numLEDs); + } + + inline bool CanShow(void) { return (System::GetUs() - endTime) >= 300L; } + + void Show(void) + { + // Data latch = 300+ microsecond pause in the output stream. Rather than + // put a delay at the end of the function, the ending time is noted and + // the function will simply hold off (if needed) on issuing the + // subsequent round of data until the latch time has elapsed. This + // allows the mainline code to start generating the next frame of data + // rather than stalling for the latch. + while(!CanShow()) + ; + + Write(SEESAW_NEOPIXEL_BASE, SEESAW_NEOPIXEL_SHOW, NULL, 0); + + endTime = System::GetUs(); // Save EOD time for latch on next call + } + + // Set the output pin number + void SetPin(uint8_t p) + { + Write8(SEESAW_NEOPIXEL_BASE, SEESAW_NEOPIXEL_PIN, p); + pin = p; + } + + // Set pixel color from separate R,G,B components: + void SetPixelColor(uint16_t n, uint8_t r, uint8_t g, uint8_t b) + { + if(n < numLEDs) + { + if(brightness) + { // See notes in setBrightness() + r = (r * brightness) >> 8; + g = (g * brightness) >> 8; + b = (b * brightness) >> 8; + } + uint8_t *p; + if(wOffset == rOffset) + { // Is an RGB-type strip + p = &pixels[n * 3]; // 3 bytes per pixel + } + else + { // Is a WRGB-type strip + p = &pixels[n * 4]; // 4 bytes per pixel + p[wOffset] = 0; // But only R,G,B passed -- set W to 0 + } + p[rOffset] = r; // R,G,B always stored + p[gOffset] = g; + p[bOffset] = b; + + uint8_t len = (wOffset == rOffset ? 3 : 4); + uint16_t offset = n * len; + + uint8_t writeBuf[6]; + writeBuf[0] = (offset >> 8); + writeBuf[1] = offset; + mymemcpy(&writeBuf[2], p, len); + + Write(SEESAW_NEOPIXEL_BASE, SEESAW_NEOPIXEL_BUF, writeBuf, len + 2); + } + } + + void SetPixelColor(uint16_t n, uint8_t r, uint8_t g, uint8_t b, uint8_t w) + { + if(n < numLEDs) + { + if(brightness) + { // See notes in setBrightness() + r = (r * brightness) >> 8; + g = (g * brightness) >> 8; + b = (b * brightness) >> 8; + w = (w * brightness) >> 8; + } + uint8_t *p; + if(wOffset == rOffset) + { // Is an RGB-type strip + p = &pixels[n * 3]; // 3 bytes per pixel (ignore W) + } + else + { // Is a WRGB-type strip + p = &pixels[n * 4]; // 4 bytes per pixel + p[wOffset] = w; // Store W + } + p[rOffset] = r; // Store R,G,B + p[gOffset] = g; + p[bOffset] = b; + + uint8_t len = (wOffset == rOffset ? 3 : 4); + uint16_t offset = n * len; + + uint8_t writeBuf[6]; + writeBuf[0] = (offset >> 8); + writeBuf[1] = offset; + mymemcpy(&writeBuf[2], p, len); + + Write(SEESAW_NEOPIXEL_BASE, SEESAW_NEOPIXEL_BUF, writeBuf, len + 2); + } + } + + // Set pixel color from 'packed' 32-bit RGB color: + void SetPixelColor(uint16_t n, uint32_t c) + { + if(n < numLEDs) + { + uint8_t *p, r = (uint8_t)(c >> 16), g = (uint8_t)(c >> 8), + b = (uint8_t)c; + if(brightness) + { // See notes in setBrightness() + r = (r * brightness) >> 8; + g = (g * brightness) >> 8; + b = (b * brightness) >> 8; + } + if(wOffset == rOffset) + { + p = &pixels[n * 3]; + } + else + { + p = &pixels[n * 4]; + uint8_t w = (uint8_t)(c >> 24); + p[wOffset] = brightness ? ((w * brightness) >> 8) : w; + } + p[rOffset] = r; + p[gOffset] = g; + p[bOffset] = b; + + uint8_t len = (wOffset == rOffset ? 3 : 4); + uint16_t offset = n * len; + + uint8_t writeBuf[6]; + writeBuf[0] = (offset >> 8); + writeBuf[1] = offset; + mymemcpy(&writeBuf[2], p, len); + + Write(SEESAW_NEOPIXEL_BASE, SEESAW_NEOPIXEL_BUF, writeBuf, len + 2); + } + } + + // Convert separate R,G,B into packed 32-bit RGB color. + // Packed format is always RGB, regardless of LED strand color order. + uint32_t Color(uint8_t r, uint8_t g, uint8_t b) + { + return ((uint32_t)r << 16) | ((uint32_t)g << 8) | b; + } + + // Convert separate R,G,B,W into packed 32-bit WRGB color. + // Packed format is always WRGB, regardless of LED strand color order. + uint32_t Color(uint8_t r, uint8_t g, uint8_t b, uint8_t w) + { + return ((uint32_t)w << 24) | ((uint32_t)r << 16) | ((uint32_t)g << 8) + | b; + } + + // Query color from previously-set pixel (returns packed 32-bit RGB value) + uint32_t GetPixelColor(uint16_t n) const + { + if(n >= numLEDs) + return 0; // Out of bounds, return no color. + + uint8_t *p; + + if(wOffset == rOffset) + { // Is RGB-type device + p = &pixels[n * 3]; + if(brightness) + { + // Stored color was decimated by setBrightness(). Returned value + // attempts to scale back to an approximation of the original 24-bit + // value used when setting the pixel color, but there will always be + // some error -- those bits are simply gone. Issue is most + // pronounced at low brightness levels. + return (((uint32_t)(p[rOffset] << 8) / brightness) << 16) + | (((uint32_t)(p[gOffset] << 8) / brightness) << 8) + | ((uint32_t)(p[bOffset] << 8) / brightness); + } + else + { + // No brightness adjustment has been made -- return 'raw' color + return ((uint32_t)p[rOffset] << 16) + | ((uint32_t)p[gOffset] << 8) | (uint32_t)p[bOffset]; + } + } + else + { // Is RGBW-type device + p = &pixels[n * 4]; + if(brightness) + { // Return scaled color + return (((uint32_t)(p[wOffset] << 8) / brightness) << 24) + | (((uint32_t)(p[rOffset] << 8) / brightness) << 16) + | (((uint32_t)(p[gOffset] << 8) / brightness) << 8) + | ((uint32_t)(p[bOffset] << 8) / brightness); + } + else + { // Return raw color + return ((uint32_t)p[wOffset] << 24) + | ((uint32_t)p[rOffset] << 16) + | ((uint32_t)p[gOffset] << 8) | (uint32_t)p[bOffset]; + } + } + } + + // Returns pointer to pixels[] array. Pixel data is stored in device- + // native format and is not translated here. Application will need to be + // aware of specific pixel data format and handle colors appropriately. + uint8_t *GetPixels(void) const { return pixels; } + + uint16_t NumPixels(void) const { return numLEDs; } + + void Clear() + { + // Clear local pixel buffer + mymemset(pixels, 0, numBytes); + + // Now clear the pixels on the seesaw + uint8_t writeBuf[32]; + mymemset(writeBuf, 0, 32); + for(uint8_t offset = 0; offset < numBytes; offset += 32 - 4) + { + writeBuf[0] = (offset >> 8); + writeBuf[1] = offset; + Write(SEESAW_NEOPIXEL_BASE, SEESAW_NEOPIXEL_BUF, writeBuf, 32); + } + } + + void SetBrightness(uint8_t b) { brightness = b; } + + private: + void mymemcpy(uint8_t *dest, uint8_t *src, uint8_t len) + { + for(uint8_t i = 0; i < len; i++) + { + dest[i] = src[i]; + } + } + + void mymemset(uint8_t *addr, uint8_t val, uint8_t len) + { + for(uint8_t i = 0; i < len; i++) + { + addr[i] = val; + } + } + + Config config_; + Transport transport_; + + protected: + bool is800KHz, // ...true if 800 KHz pixels + begun; // true if begin() previously called + uint16_t numLEDs, // Number of RGB LEDs in strip + numBytes; // Size of 'pixels' buffer below (3 or 4 bytes/pixel) + int8_t pin; + + uint8_t pixelsd[256]; // hopefully we won't need more than this... + + uint8_t brightness, + *pixels, // Holds LED color values (3 or 4 bytes each) + rOffset, // Index of red byte within each 3- or 4-byte pixel + gOffset, // Index of green byte + bOffset, // Index of blue byte + wOffset; // Index of white byte (same as rOffset if no white) + uint32_t endTime; // Latch timing reference + + uint16_t type; + +}; // namespace daisy + +/** @} */ + +using NeoPixelI2C = NeoPixel; +} // namespace daisy +#endif \ No newline at end of file diff --git a/src/dev/neotrellis.h b/src/dev/neotrellis.h new file mode 100644 index 0000000..503f1a2 --- /dev/null +++ b/src/dev/neotrellis.h @@ -0,0 +1,497 @@ +#pragma once +#ifndef DSY_NEO_TRELLIS_H +#define DSY_NEO_TRELLIS_H + +#include "dev/neopixel.h" + +#define NEO_TRELLIS_ADDR 0x2E + +#define NEO_TRELLIS_NEOPIX_PIN 3 + +#define NEO_TRELLIS_NUM_ROWS 4 +#define NEO_TRELLIS_NUM_COLS 4 +#define NEO_TRELLIS_NUM_KEYS (NEO_TRELLIS_NUM_ROWS * NEO_TRELLIS_NUM_COLS) + +#define NEO_TRELLIS_MAX_CALLBACKS 32 + +#define NEO_TRELLIS_KEY(x) (((x) / 4) * 8 + ((x) % 4)) +#define NEO_TRELLIS_SEESAW_KEY(x) (((x) / 8) * 4 + ((x) % 8)) + +#define NEO_TRELLIS_X(k) ((k) % 4) +#define NEO_TRELLIS_Y(k) ((k) / 4) + +#define NEO_TRELLIS_XY(x, y) ((y)*NEO_TRELLIS_NUM_ROWS + (x)) + +namespace daisy +{ +/** @addtogroup external + @{ +*/ + +/** I2C Transport for NeoTrellis */ +class NeoTrellisI2CTransport +{ + public: + NeoTrellisI2CTransport() {} + ~NeoTrellisI2CTransport() {} + + struct Config + { + I2CHandle::Config::Peripheral periph; + I2CHandle::Config::Speed speed; + Pin scl; + Pin sda; + + uint8_t address; + + Config() + { + address = NEO_TRELLIS_ADDR; + + periph = I2CHandle::Config::Peripheral::I2C_1; + speed = I2CHandle::Config::Speed::I2C_400KHZ; + + scl = Pin(PORTB, 8); + sda = Pin(PORTB, 9); + } + }; + + inline void Init(Config config) + { + config_ = config; + + I2CHandle::Config i2c_config; + i2c_config.mode = I2CHandle::Config::Mode::I2C_MASTER; + i2c_config.periph = config.periph; + i2c_config.speed = config.speed; + + i2c_config.pin_config.scl = config.scl; + i2c_config.pin_config.sda = config.sda; + + error_ |= I2CHandle::Result::OK != i2c_.Init(i2c_config); + } + + void Write(uint8_t *data, uint16_t size) + { + error_ |= I2CHandle::Result::OK + != i2c_.TransmitBlocking(config_.address, data, size, 10); + } + + void Read(uint8_t *data, uint16_t size) + { + error_ |= I2CHandle::Result::OK + != i2c_.ReceiveBlocking(config_.address, data, size, 10); + } + + void ReadLen(uint8_t reg_high, + uint8_t reg_low, + uint8_t *buff, + uint16_t size, + int delay) + { + uint8_t reg[2] = {reg_high, reg_low}; + Write(reg, 2); + + System::DelayUs(delay); + + Read(buff, size); + } + + /** Writes an 8 bit value + \param reg the register address to write to + \param value the value to write to the register + */ + void Write8(uint8_t reg_high, uint8_t reg_low, uint8_t value) + { + uint8_t buffer[3]; + + buffer[0] = reg_high; + buffer[1] = reg_low; + buffer[2] = value; + + Write(buffer, 3); + } + + /** Reads an 8 bit value + \param reg the register address to read from + \returns the 16 bit data value read from the device + */ + uint8_t Read8(uint8_t reg_high, uint8_t reg_low, int delay) + { + uint8_t buffer; + ReadLen(reg_high, reg_low, &buffer, 1, delay); + return buffer; + } + + bool GetError() + { + bool tmp = error_; + error_ = false; + return tmp; + } + + private: + I2CHandle i2c_; + Config config_; + + // true if error has occured since last check + bool error_; +}; + +/** \brief Device support for the Adafruit Neotrellis device + @author beserge + @date December 2021 +*/ +template +class NeoTrellis +{ + public: + NeoTrellis() {} + ~NeoTrellis() {} + + /** Module Base Addreses + The module base addresses for different seesaw modules. + */ + enum ModuleBaseAddress + { + SEESAW_STATUS_BASE = 0x00, + SEESAW_GPIO_BASE = 0x01, + SEESAW_SERCOM0_BASE = 0x02, + + SEESAW_TIMER_BASE = 0x08, + SEESAW_ADC_BASE = 0x09, + SEESAW_DAC_BASE = 0x0A, + SEESAW_INTERRUPT_BASE = 0x0B, + SEESAW_DAP_BASE = 0x0C, + SEESAW_EEPROM_BASE = 0x0D, + SEESAW_NEOPIXEL_BASE = 0x0E, + SEESAW_TOUCH_BASE = 0x0F, + SEESAW_KEYPAD_BASE = 0x10, + SEESAW_ENCODER_BASE = 0x11, + SEESAW_SPECTRUM_BASE = 0x12, + }; + + + /** keypad module function address registers */ + enum KeypadFuncAddRegs + { + SEESAW_KEYPAD_STATUS = 0x00, + SEESAW_KEYPAD_EVENT = 0x01, + SEESAW_KEYPAD_INTENSET = 0x02, + SEESAW_KEYPAD_INTENCLR = 0x03, + SEESAW_KEYPAD_COUNT = 0x04, + SEESAW_KEYPAD_FIFO = 0x10, + }; + + /** status module function address registers */ + enum StatusFuncAddRegs + { + SEESAW_STATUS_HW_ID = 0x01, + SEESAW_STATUS_VERSION = 0x02, + SEESAW_STATUS_OPTIONS = 0x03, + SEESAW_STATUS_TEMP = 0x04, + SEESAW_STATUS_SWRST = 0x7F, + }; + + union keyEventRaw + { + struct + { + uint8_t EDGE : 2; ///< the edge that was triggered + uint8_t NUM : 6; ///< the event number + } bit; ///< bitfield format + uint8_t reg; ///< register format + }; + + /** extended key event stucture for keypad module */ + union keyEvent + { + struct Bit + { + uint8_t EDGE : 2; ///< the edge that was triggered + uint16_t NUM : 14; ///< the event number + } bit; ///< bitfield format + uint16_t reg; ///< register format + }; + + /** key state struct that will be written to seesaw chip keypad module */ + union keyState + { + struct + { + uint8_t STATE : 1; ///< the current state of the key + uint8_t ACTIVE : 4; ///< the registered events for that key + } bit; ///< bitfield format + uint8_t reg; ///< register format + }; + + /** keypad module edge definitions */ + enum KeypadEdge + { + HIGH = 0, + LOW, + FALLING, + RISING, + }; + + struct Config + { + typename Transport::Config transport_config; + NeoPixelI2C::Config pixels_conf; + + Config() {} + }; + + enum Result + { + OK = 0, + ERR + }; + + typedef void (*TrellisCallback)(keyEvent evt); //< Trellis Callback typedef + + /** Initialize the NeoTrellis device + \param config Configuration settings + */ + Result Init(Config config) + { + config_ = config; + + // init neopixels + if(pixels.Init(config_.pixels_conf) == NeoPixelI2C::Result::ERR) + { + return ERR; + } + + transport_.Init(config_.transport_config); + + // 10 ms delay + System::Delay(10); + + EnableKeypadInterrupt(); + + return GetTransportError(); + } + + /** Writes an 8 bit value + \param reg the register address to write to + \param value the value to write to the register + */ + void Write8(uint8_t reg_high, uint8_t reg_low, uint8_t value) + { + return transport_.Write8(reg_high, reg_low, value); + } + + /** Reads an 8 bit value + \param reg the register address to read from + \returns the data uint8_t read from the device + */ + uint8_t Read8(uint8_t reg_high, uint8_t reg_low, int delay) + { + return transport_.Read8(reg_high, reg_low, delay); + } + + void ReadLen(uint8_t reg_high, + uint8_t reg_low, + uint8_t *buff, + uint8_t len, + int delay) + { + transport_.ReadLen(reg_high, reg_low, buff, len, delay); + } + + /** Get and reset the transport error flag + \return Whether the transport has errored since the last check + */ + Result GetTransportError() { return transport_.GetError() ? ERR : OK; } + + /** Perform a software reset. + This resets all seesaw registers to their default values. + This is called automatically from Init(). + */ + void SWReset() + { + return Write8(SEESAW_STATUS_BASE, SEESAW_STATUS_SWRST, 0xFF); + } + + /** activate or deactivate a given key event + \param key the key number to map the event to + \param edge the edge sensitivity of the event + \param enable pass true to enable the passed event, false to disable it. + */ + void ActivateKey(uint8_t x, uint8_t y, uint8_t edge, bool enable) + { + int xkey = NEO_TRELLIS_X(x); + int ykey + = NEO_TRELLIS_Y(y % NEO_TRELLIS_NUM_ROWS * NEO_TRELLIS_NUM_COLS); + + SetKeypadEvent( + NEO_TRELLIS_KEY(NEO_TRELLIS_XY(xkey, ykey)), edge, enable); + } + + /** read all events currently stored in the seesaw fifo and call any callbacks. + \param polling pass true if the interrupt pin is not being used, false if + it is. Defaults to true. + */ + void Process(bool polling = true) + { + uint8_t count = GetKeypadCount(); + System::DelayUs(500); + if(count > 0) + { + if(polling) + count = count + 2; + keyEventRaw e[count]; + ReadKeypad(e, count); + for(int i = 0; i < count; i++) + { + e[i].bit.NUM = NEO_TRELLIS_SEESAW_KEY(e[i].bit.NUM); + if(e[i].bit.NUM < NEO_TRELLIS_NUM_KEYS) + { + keyEvent evt = {e[i].bit.EDGE, e[i].bit.NUM}; + + state_[evt.bit.NUM] + = evt.bit.EDGE == HIGH || evt.bit.EDGE == RISING; + rising_[evt.bit.NUM] = evt.bit.EDGE == RISING; + falling_[evt.bit.NUM] = evt.bit.EDGE == FALLING; + + // call any callbacks associated with the key + if(_callbacks[e[i].bit.NUM] != NULL) + { + _callbacks[e[i].bit.NUM](evt); + } + } + } + } + } + + /** Is this key currently pressed or released? Updated via the Process() function. + \param idx Key to check + \return True if pressed, false otherwise + */ + bool GetState(uint8_t idx) + { + if(idx < NEO_TRELLIS_NUM_KEYS) + { + return state_[idx]; + } + + return false; + } + + /** Has this key just been pressed? (aka rising edge) Updated via the Process() function. + \param idx Key to check + \return True if just pressed, false otherwise + */ + bool GetRising(uint8_t idx) + { + if(idx < NEO_TRELLIS_NUM_KEYS) + { + bool tmp = rising_[idx]; + rising_[idx] = false; + return tmp; + } + + return false; + } + + /** Has this key just been released? (aka falling edge) Updated via the Process() function. + \param idx Key to check + \return True if just released, false otherwise + */ + bool GetFalling(uint8_t idx) + { + if(idx < NEO_TRELLIS_NUM_KEYS) + { + bool tmp = falling_[idx]; + falling_[idx] = false; + return tmp; + } + + return false; + } + + void ReadKeypad(keyEventRaw *buf, uint8_t count) + { + ReadLen(SEESAW_KEYPAD_BASE, + SEESAW_KEYPAD_FIFO, + (uint8_t *)buf, + count, + 1000); + } + + /** Get the number of events currently in the fifo + \return the number of events in the fifo + */ + uint8_t GetKeypadCount() + { + return Read8(SEESAW_KEYPAD_BASE, SEESAW_KEYPAD_COUNT, 500); + } + + /** activate or deactivate a key and edge on the keypad module + \param key the key number to activate + \param edge the edge to trigger on + \param enable passing true will enable the passed event, passing false will disable it. + */ + void SetKeypadEvent(uint8_t key, uint8_t edge, bool enable) + { + keyState ks; + ks.bit.STATE = enable; + ks.bit.ACTIVE = (1 << edge); + uint8_t cmd[] = {SEESAW_KEYPAD_BASE, SEESAW_KEYPAD_EVENT, key, ks.reg}; + transport_.Write(cmd, 4); + } + + /** Enable the keypad interrupt that fires when events are in the fifo. */ + void EnableKeypadInterrupt() + { + Write8(SEESAW_KEYPAD_BASE, SEESAW_KEYPAD_INTENSET, 0x01); + } + + /** register a callback for a key addressed by key index. + \param x the column index of the key. column 0 is on the lefthand side of the matix. + \param y the row index of the key. row 0 is at the top of the matrix and the numbers increase downwards. + \param cb the function to be called when an event from the specified key is detected. + */ + void RegisterCallback(uint8_t x, uint8_t y, TrellisCallback (*cb)(keyEvent)) + { + int xkey = NEO_TRELLIS_X(x); + int ykey + = NEO_TRELLIS_Y(y % NEO_TRELLIS_NUM_ROWS * NEO_TRELLIS_NUM_COLS); + + _callbacks[NEO_TRELLIS_XY(xkey, ykey)] = cb; + } + + /** Unregister a callback for a key addressed by key index. + \param x the column index of the key. column 0 is on the lefthand side of the matix. + \param y the row index of the key. row 0 is at the top of the matrix and the numbers increase downwards. + */ + void UnregisterCallback(uint8_t x, uint8_t y) + { + int xkey = NEO_TRELLIS_X(x); + int ykey + = NEO_TRELLIS_Y(y % NEO_TRELLIS_NUM_ROWS * NEO_TRELLIS_NUM_COLS); + + _callbacks[NEO_TRELLIS_XY(xkey, ykey)] = NULL; + } + + NeoPixelI2C pixels; + + private: + Config config_; + Transport transport_; + + bool state_[NEO_TRELLIS_NUM_KEYS]; + bool rising_[NEO_TRELLIS_NUM_KEYS]; + bool falling_[NEO_TRELLIS_NUM_KEYS]; + + TrellisCallback (*_callbacks[NEO_TRELLIS_NUM_KEYS])( + keyEvent); ///< the array of callback functions + +}; // namespace daisy + +/** @} */ + +using NeoTrellisI2C = NeoTrellis; +} // namespace daisy +#endif \ No newline at end of file diff --git a/src/dev/tlv493d.h b/src/dev/tlv493d.h new file mode 100644 index 0000000..d64d8e5 --- /dev/null +++ b/src/dev/tlv493d.h @@ -0,0 +1,466 @@ +#pragma once +#ifndef DSY_TLV493D_H +#define DSY_TLV493D_H + +#define TLV493D_DEFAULTMODE POWERDOWNMODE + +#define TLV493D_ADDRESS1 0x5E +#define TLV493D_ADDRESS2 0x1F +#define TLV493D_BUSIF_READSIZE 10 +#define TLV493D_BUSIF_WRITESIZE 4 +#define TLV493D_NUM_OF_REGMASKS 25 +#define TLV493D_DEFAULTMODE POWERDOWNMODE +#define TLV493D_MEASUREMENT_READOUT 7 + +#define TLV493D_B_MULT 0.098f +#define TLV493D_TEMP_MULT 1.1 +#define TLV493D_TEMP_OFFSET 315 + +#define REGMASK_READ 0 +#define REGMASK_WRITE 1 + +namespace daisy +{ +/** @addtogroup external + @{ +*/ + +/** I2C Transport for TLV493D */ +class Tlv493dI2CTransport +{ + public: + Tlv493dI2CTransport() {} + ~Tlv493dI2CTransport() {} + + struct Config + { + I2CHandle::Config::Peripheral periph; + I2CHandle::Config::Speed speed; + dsy_gpio_pin scl; + dsy_gpio_pin sda; + + uint8_t address; + + Config() + { + periph = I2CHandle::Config::Peripheral::I2C_1; + speed = I2CHandle::Config::Speed::I2C_400KHZ; + + scl = {DSY_GPIOB, 8}; + sda = {DSY_GPIOB, 9}; + + address = TLV493D_ADDRESS1; + } + }; + + void Init(Config config) + { + config_ = config; + + I2CHandle::Config i2c_config; + i2c_config.mode = I2CHandle::Config::Mode::I2C_MASTER; + i2c_config.periph = config_.periph; + i2c_config.speed = config_.speed; + + i2c_config.pin_config.scl = config_.scl; + i2c_config.pin_config.sda = config_.sda; + + err_ = false; + + err_ |= I2CHandle::Result::OK != i2c_.Init(i2c_config); + } + + // used for the weird reset sequence. This ends in an error but that's OK + void WriteAddress(uint8_t add, uint8_t *data, uint16_t size) + { + i2c_.TransmitBlocking(add, data, size, 10); + } + + void Write(uint8_t *data, uint16_t size) + { + err_ |= I2CHandle::Result::OK + != i2c_.TransmitBlocking(config_.address, data, size, 10); + } + + void Read(uint8_t *data, uint16_t size) + { + err_ |= I2CHandle::Result::OK + != i2c_.ReceiveBlocking(config_.address, data, size, 10); + } + + bool GetError() + { + bool tmp = err_; + err_ = false; + + return tmp; + } + + uint8_t GetAddress() { return config_.address; } + + private: + I2CHandle i2c_; + Config config_; + bool err_; +}; + + +/** @brief Device support for TLV493D Magnetic Sensor + @author beserge + @date December 2021 +*/ +template +class Tlv493d +{ + public: + Tlv493d() {} + ~Tlv493d() {} + + enum Registers_e + { + R_BX1 = 0, + R_BX2, + R_BY1, + R_BY2, + R_BZ1, + R_BZ2, + R_TEMP1, + R_TEMP2, + R_FRAMECOUNTER, + R_CHANNEL, + R_POWERDOWNFLAG, + R_RES1, + R_RES2, + R_RES3, + W_PARITY, + W_ADDR, + W_INT, + W_FAST, + W_LOWPOWER, + W_TEMP_NEN, + W_LP_PERIOD, + W_PARITY_EN, + W_RES1, + W_RES2, + W_RES3 + }; + + struct RegMask_t + { + uint8_t rw; + uint8_t byteAdress; + uint8_t bitMask; + uint8_t shift; + }; + + typedef struct + { + uint8_t fast; + uint8_t lp; + uint8_t lpPeriod; + uint16_t measurementTime; + } AccessMode_t; + + enum AccessMode_e + { + POWERDOWNMODE = 0, + FASTMODE, + LOWPOWERMODE, + ULTRALOWPOWERMODE, + MASTERCONTROLLEDMODE, + }; + + const AccessMode_t accModes[5] = { + {0, 0, 0, 1000}, // POWERDOWNMODE + {1, 0, 0, 0}, // FASTMODE + {0, 1, 1, 10}, // LOWPOWERMODE + {0, 1, 0, 100}, // ULTRALOWPOWERMODE + {1, 1, 1, 10} // MASTERCONTROLLEDMODE + }; + + const RegMask_t RegMasks[25] = { + {REGMASK_READ, 0, 0xFF, 0}, // R_BX1 + {REGMASK_READ, 4, 0xF0, 4}, // R_BX2 + {REGMASK_READ, 1, 0xFF, 0}, // R_BY1 + {REGMASK_READ, 4, 0x0F, 0}, // R_BY2 + {REGMASK_READ, 2, 0xFF, 0}, // R_BZ1 + {REGMASK_READ, 5, 0x0F, 0}, // R_BZ2 + {REGMASK_READ, 3, 0xF0, 4}, // R_TEMP1 + {REGMASK_READ, 6, 0xFF, 0}, // R_TEMP2 + {REGMASK_READ, 3, 0x0C, 2}, // R_FRAMECOUNTER + {REGMASK_READ, 3, 0x03, 0}, // R_CHANNEL + {REGMASK_READ, 5, 0x10, 4}, // R_POWERDOWNFLAG + {REGMASK_READ, 7, 0x18, 3}, // R_RES1 + {REGMASK_READ, 8, 0xFF, 0}, // R_RES2 + {REGMASK_READ, 9, 0x1F, 0}, // R_RES3 + {REGMASK_WRITE, 1, 0x80, 7}, // W_PARITY + {REGMASK_WRITE, 1, 0x60, 5}, // W_ADDR + {REGMASK_WRITE, 1, 0x04, 2}, // W_INT + {REGMASK_WRITE, 1, 0x02, 1}, // W_FAST + {REGMASK_WRITE, 1, 0x01, 0}, // W_LOWPOWER + {REGMASK_WRITE, 3, 0x80, 7}, // W_TEMP_EN + {REGMASK_WRITE, 3, 0x40, 6}, // W_LOWPOWER + {REGMASK_WRITE, 3, 0x20, 5}, // W_POWERDOWN + {REGMASK_WRITE, 1, 0x18, 3}, // W_RES1 + {REGMASK_WRITE, 2, 0xFF, 0}, // W_RES2 + {REGMASK_WRITE, 3, 0x1F, 0} // W_RES3 + }; + + struct Config + { + typename Transport::Config transport_config; + Config() {} + }; + + enum Result + { + OK = 0, + ERR + }; + + /** Initialize the TLV493D device + \param config Configuration settings + */ + Result Init(Config config) + { + config_ = config; + System::Delay(40); // 40ms startup delay + + transport_.Init(config_.transport_config); + + ResetSensor(); + + // get all register data from sensor + ReadOut(); + // copy factory settings to write registers + SetRegBits(W_RES1, GetRegBits(R_RES1)); + SetRegBits(W_RES2, GetRegBits(R_RES2)); + SetRegBits(W_RES3, GetRegBits(R_RES3)); + // enable parity detection + SetRegBits(W_PARITY_EN, 1); + // config sensor to lowpower mode + // also contains parity calculation and writeout to sensor + SetAccessMode(MASTERCONTROLLEDMODE); + + prev_sample_period_ = System::GetNow(); + + return GetTransportErr(); + } + + void ReadOut() { transport_.Read(regReadData, TLV493D_BUSIF_READSIZE); } + + void WriteOut() { transport_.Write(regWriteData, TLV493D_BUSIF_WRITESIZE); } + + void SetRegBits(uint8_t regMaskIndex, uint8_t data) + { + if(regMaskIndex < TLV493D_NUM_OF_REGMASKS) + { + SetToRegs(&RegMasks[regMaskIndex], regWriteData, data); + } + } + + uint8_t GetRegBits(uint8_t regMaskIndex) + { + if(regMaskIndex < TLV493D_NUM_OF_REGMASKS) + { + const RegMask_t *mask = &(RegMasks[regMaskIndex]); + if(mask->rw == REGMASK_READ) + { + return GetFromRegs(mask, regReadData); + } + else + { + return GetFromRegs(mask, regWriteData); + } + } + return 0; + } + + + void UpdateData() + { + uint32_t now = System::GetNow(); + if(now - prev_sample_period_ >= GetMeasurementDelay()) + { + prev_sample_period_ = now; + + ReadOut(); + + // construct results from registers + mXdata = ConcatResults(GetRegBits(R_BX1), GetRegBits(R_BX2), true); + mYdata = ConcatResults(GetRegBits(R_BY1), GetRegBits(R_BY2), true); + mZdata = ConcatResults(GetRegBits(R_BZ1), GetRegBits(R_BZ2), true); + mTempdata = ConcatResults( + GetRegBits(R_TEMP1), GetRegBits(R_TEMP2), false); + + // SetAccessMode(POWERDOWNMODE); + GetRegBits(R_CHANNEL); + + mExpectedFrameCount = GetRegBits(R_FRAMECOUNTER) + 1; + } + } + + void SetInterrupt(bool enable) + { + SetRegBits(W_INT, enable); + CalcParity(); + WriteOut(); + } + + void EnableTemp(bool enable) + { + SetRegBits(W_TEMP_NEN, enable); + CalcParity(); + WriteOut(); + } + + float GetX() { return static_cast(mXdata) * TLV493D_B_MULT; } + + + float GetY() { return static_cast(mYdata) * TLV493D_B_MULT; } + + + float GetZ() { return static_cast(mZdata) * TLV493D_B_MULT; } + + + float GetTemp() + { + return static_cast(mTempdata - TLV493D_TEMP_OFFSET) + * TLV493D_TEMP_MULT; + } + + + float GetAmount() + { + // sqrt(x^2 + y^2 + z^2) + return TLV493D_B_MULT + * sqrt(pow(static_cast(mXdata), 2) + + pow(static_cast(mYdata), 2) + + pow(static_cast(mZdata), 2)); + } + + + float GetAzimuth() + { + // arctan(y/x) + return atan2(static_cast(mYdata), static_cast(mXdata)); + } + + + float GetPolar() + { + // arctan(z/(sqrt(x^2+y^2))) + return atan2(static_cast(mZdata), + sqrt(pow(static_cast(mXdata), 2) + + pow(static_cast(mYdata), 2))); + } + + uint16_t GetMeasurementDelay() { return accModes[mMode].measurementTime; } + + void SetAccessMode(AccessMode_e mode) + { + const AccessMode_t *modeConfig = &(accModes[mode]); + SetRegBits(W_FAST, modeConfig->fast); + SetRegBits(W_LOWPOWER, modeConfig->lp); + SetRegBits(W_LP_PERIOD, modeConfig->lpPeriod); + CalcParity(); + WriteOut(); + mMode = mode; + } + + void CalcParity() + { + uint8_t i; + uint8_t y = 0x00; + // set parity bit to 1 + // algorithm will calculate an even parity and replace this bit, + // so parity becomes odd + SetRegBits(W_PARITY, 1); + // combine array to one byte first + for(i = 0; i < TLV493D_BUSIF_WRITESIZE; i++) + { + y ^= regWriteData[i]; + } + // combine all bits of this byte + y = y ^ (y >> 1); + y = y ^ (y >> 2); + y = y ^ (y >> 4); + // parity is in the LSB of y + SetRegBits(W_PARITY, y & 0x01); + } + + int16_t ConcatResults(uint8_t upperByte, uint8_t lowerByte, bool upperFull) + { + //16-bit signed integer for 12-bit values of sensor + int16_t value = 0x0000; + if(upperFull) + { + value = upperByte << 8; + value |= (lowerByte & 0x0F) << 4; + } + else + { + value = (upperByte & 0x0F) << 12; + value |= lowerByte << 4; + } + value >>= 4; //shift left so that value is a signed 12 bit integer + return value; + } + + private: + Config config_; + Transport transport_; + uint8_t regReadData[TLV493D_BUSIF_READSIZE]; + uint8_t regWriteData[TLV493D_BUSIF_WRITESIZE]; + int16_t mXdata, mYdata, mZdata, mTempdata, mExpectedFrameCount, mMode; + uint32_t prev_sample_period_; + + /** Get the global transport_error_ bool (as a Result), then reset it */ + Result GetTransportErr() + { + Result ret = transport_.GetError() ? ERR : OK; + return ret; + } + + // internal function called by begin() + void ResetSensor() + { + uint8_t data; + if(transport_.GetAddress() == TLV493D_ADDRESS1) + { + // if the sensor shall be initialized with i2c address 0x1F + data = 0xFF; + } + else + { + // if the sensor shall be initialized with address 0x5E + data = 0x00; + } + + // Write data to slave add 0x00 + transport_.WriteAddress(0x00, &data, 1); + } + + uint8_t GetFromRegs(const RegMask_t *mask, uint8_t *regData) + { + return (regData[mask->byteAdress] & mask->bitMask) >> mask->shift; + } + + + void SetToRegs(const RegMask_t *mask, uint8_t *regData, uint8_t toWrite) + { + if(mask->rw == REGMASK_WRITE) + { + uint8_t regValue = regData[mask->byteAdress]; + regValue &= ~(mask->bitMask); + regValue |= (toWrite << mask->shift) & mask->bitMask; + regData[mask->byteAdress] = regValue; + } + } +}; + +/** @} */ + +using Tlv493dI2C = Tlv493d; +} // namespace daisy +#endif diff --git a/src/hid/audio.cpp b/src/hid/audio.cpp index 7b06f2e..211e83c 100644 --- a/src/hid/audio.cpp +++ b/src/hid/audio.cpp @@ -65,7 +65,18 @@ class AudioHandle::Impl if(val <= 0.f) return AudioHandle::Result::ERR; config_.postgain = val; - postgain_recip_ = 1.f / config_.postgain; + // Precompute input adjust + postgain_recip_ = 1.f / config_.postgain; + // Precompute output adjust + output_adjust_ = config_.postgain * config_.output_compensation; + return AudioHandle::Result::OK; + } + + AudioHandle::Result SetOutputCompensation(float val) + { + config_.output_compensation = val; + // recompute output adjustment (no need to recompute input adjust here) + output_adjust_ = config_.output_compensation * config_.postgain; return AudioHandle::Result::OK; } @@ -82,6 +93,7 @@ class AudioHandle::Impl int32_t* buff_rx_[2]; int32_t* buff_tx_[2]; float postgain_recip_; + float output_adjust_; }; // ================================================================ @@ -99,11 +111,15 @@ AudioHandle::Result AudioHandle::Impl::Init(const AudioHandle::Config config, { config_ = config; + /** Precompute input level adjustment */ if(config_.postgain > 0.f) postgain_recip_ = 1.f / config_.postgain; else return Result::ERR; + /** Precompute output level adjustment */ + output_adjust_ = config_.postgain * config_.output_compensation; + if(sai.IsInitialized()) { sai1_ = sai; @@ -308,25 +324,25 @@ void AudioHandle::Impl::InternalCallback(int32_t* in, int32_t* out, size_t size) case SaiHandle::Config::BitDepth::SAI_16BIT: for(size_t i = 0; i < size; i += 2) { - out[i] = f2s16(fout[i] * audio_handle.config_.postgain); + out[i] = f2s16(fout[i] * audio_handle.output_adjust_); out[i + 1] - = f2s16(fout[i + 1] * audio_handle.config_.postgain); + = f2s16(fout[i + 1] * audio_handle.output_adjust_); } break; case SaiHandle::Config::BitDepth::SAI_24BIT: for(size_t i = 0; i < size; i += 2) { - out[i] = f2s24(fout[i] * audio_handle.config_.postgain); + out[i] = f2s24(fout[i] * audio_handle.output_adjust_); out[i + 1] - = f2s24(fout[i + 1] * audio_handle.config_.postgain); + = f2s24(fout[i + 1] * audio_handle.output_adjust_); } break; case SaiHandle::Config::BitDepth::SAI_32BIT: for(size_t i = 0; i < size; i += 2) { - out[i] = f2s32(fout[i] * audio_handle.config_.postgain); + out[i] = f2s32(fout[i] * audio_handle.output_adjust_); out[i + 1] - = f2s32(fout[i + 1] * audio_handle.config_.postgain); + = f2s32(fout[i + 1] * audio_handle.output_adjust_); } break; default: break; @@ -416,15 +432,15 @@ void AudioHandle::Impl::InternalCallback(int32_t* in, int32_t* out, size_t size) for(size_t i = 0; i < size; i += 2) { out[i] - = f2s16(fout[0][i / 2] * audio_handle.config_.postgain); + = f2s16(fout[0][i / 2] * audio_handle.output_adjust_); out[i + 1] - = f2s16(fout[1][i / 2] * audio_handle.config_.postgain); + = f2s16(fout[1][i / 2] * audio_handle.output_adjust_); if(chns > 2) { audio_handle.buff_tx_[1][offset + i] = f2s16( - fout[2][i / 2] * audio_handle.config_.postgain); + fout[2][i / 2] * audio_handle.output_adjust_); audio_handle.buff_tx_[1][offset + i + 1] = f2s16( - fout[3][i / 2] * audio_handle.config_.postgain); + fout[3][i / 2] * audio_handle.output_adjust_); } } break; @@ -432,15 +448,15 @@ void AudioHandle::Impl::InternalCallback(int32_t* in, int32_t* out, size_t size) for(size_t i = 0; i < size; i += 2) { out[i] - = f2s24(fout[0][i / 2] * audio_handle.config_.postgain); + = f2s24(fout[0][i / 2] * audio_handle.output_adjust_); out[i + 1] - = f2s24(fout[1][i / 2] * audio_handle.config_.postgain); + = f2s24(fout[1][i / 2] * audio_handle.output_adjust_); if(chns > 2) { audio_handle.buff_tx_[1][offset + i] = f2s24( - fout[2][i / 2] * audio_handle.config_.postgain); + fout[2][i / 2] * audio_handle.output_adjust_); audio_handle.buff_tx_[1][offset + i + 1] = f2s24( - fout[3][i / 2] * audio_handle.config_.postgain); + fout[3][i / 2] * audio_handle.output_adjust_); } } break; @@ -448,15 +464,15 @@ void AudioHandle::Impl::InternalCallback(int32_t* in, int32_t* out, size_t size) for(size_t i = 0; i < size; i += 2) { out[i] - = f2s32(fout[0][i / 2] * audio_handle.config_.postgain); + = f2s32(fout[0][i / 2] * audio_handle.output_adjust_); out[i + 1] - = f2s32(fout[1][i / 2] * audio_handle.config_.postgain); + = f2s32(fout[1][i / 2] * audio_handle.output_adjust_); if(chns > 2) { audio_handle.buff_tx_[1][offset + i] = f2s32( - fout[2][i / 2] * audio_handle.config_.postgain); + fout[2][i / 2] * audio_handle.output_adjust_); audio_handle.buff_tx_[1][offset + i + 1] = f2s32( - fout[3][i / 2] * audio_handle.config_.postgain); + fout[3][i / 2] * audio_handle.output_adjust_); } } break; @@ -546,4 +562,9 @@ AudioHandle::Result AudioHandle::SetPostGain(float val) return pimpl_->SetPostGain(val); } +AudioHandle::Result AudioHandle::SetOutputCompensation(float val) +{ + return pimpl_->SetOutputCompensation(val); +} + } // namespace daisy diff --git a/src/hid/audio.h b/src/hid/audio.h index c372cb9..cfbe1aa 100644 --- a/src/hid/audio.h +++ b/src/hid/audio.h @@ -26,9 +26,28 @@ class AudioHandle /** TODO: Figure out how to get samplerate in here. */ struct Config { - size_t blocksize; + /** number of samples to process per callback */ + size_t blocksize; + + /**< Sample rate of audio */ SaiHandle::Config::SampleRate samplerate; - float postgain; + + /** factor for adjustment before and after callback for hardware that may have extra headroom */ + float postgain; + + /** factor for additional one-sided compensation to audio path for hardware that may + * have unequal input/output ranges + */ + float output_compensation; + + /** Sets default values for config struct */ + Config() + : blocksize(48), + samplerate(SaiHandle::Config::SampleRate::SAI_48KHZ), + postgain(1.f), + output_compensation(1.f) + { + } }; enum class Result @@ -117,6 +136,14 @@ class AudioHandle ** \param val Gain adjustment amount. The hardware will clip at the reciprical of this value. */ Result SetPostGain(float val); + /** Sets an additional amount of gain compensation to perform at the end of the callback + ** Useful if the hardware input/output levels are not equal. + ** + ** \param val To calcuate the value, measure the input signal, then measure the output signal + ** (with this set to default value of 1.0). + ** Then calculate val as: val = 1 / (vout / vin); */ + Result SetOutputCompensation(float val); + /** Starts the Audio using the non-interleaving callback. */ Result Start(AudioCallback callback); diff --git a/src/hid/midi.h b/src/hid/midi.h index 0576e55..16dcd1d 100644 --- a/src/hid/midi.h +++ b/src/hid/midi.h @@ -155,6 +155,11 @@ class MidiHandler */ void Parse(uint8_t byte) { + // reset parser when status byte is received + if((byte & kStatusByteMask) && pstate_ != ParserSysEx) + { + pstate_ = ParserEmpty; + } switch(pstate_) { case ParserEmpty: @@ -217,7 +222,21 @@ class MidiHandler // Handle as running status incoming_message_.type = running_status_; incoming_message_.data[0] = byte & kDataByteMask; - pstate_ = ParserHasData0; + //check for single byte running status, really this only applies to channel pressure though + if(running_status_ == ChannelPressure + || running_status_ == ProgramChange + || incoming_message_.sc_type == MTCQuarterFrame + || incoming_message_.sc_type == SongSelect) + { + //Send the single byte update + pstate_ = ParserEmpty; + event_q_.Write(incoming_message_); + } + else + { + pstate_ + = ParserHasData0; //we need to get the 2nd byte yet. + } } break; case ParserHasStatus: @@ -264,12 +283,17 @@ class MidiHandler if(running_status_ == NoteOn && incoming_message_.data[1] == 0) { - incoming_message_.type = running_status_ = NoteOff; + incoming_message_.type = NoteOff; } // At this point the message is valid, and we can add this MidiEvent to the queue event_q_.Write(incoming_message_); } + else + { + // invalid message go back to start ;p + pstate_ = ParserEmpty; + } // Regardless, of whether the data was valid or not we go back to empty // because either the message is queued for handling or its not. pstate_ = ParserEmpty; diff --git a/src/hid/usb_host.cpp b/src/hid/usb_host.cpp index e94bcb5..f3c1ea6 100644 --- a/src/hid/usb_host.cpp +++ b/src/hid/usb_host.cpp @@ -22,12 +22,34 @@ class USBHostHandle::Impl Result Init(Config config); Result Deinit(); + Result Process(); + Result ReEnumerate(); - void Process(); bool GetReady(); + inline Config &GetConfig() { return config_; } + private: Config config_; + + /** @brief Maps ST Middleware USBH_StatusTypeDef to USBHostHandle::Result codes */ + Result ConvertStatus(USBH_StatusTypeDef sta) + { + if(sta != USBH_OK) + { + return Result::FAIL; + } + switch(sta) + { + case USBH_OK: return Result::OK; + case USBH_BUSY: return Result::BUSY; + case USBH_NOT_SUPPORTED: return Result::NOT_SUPPORTED; + case USBH_UNRECOVERED_ERROR: return Result::UNRECOVERED_ERROR; + case USBH_ERROR_SPEED_UNKNOWN: return Result::ERROR_SPEED_UNKNOWN; + case USBH_FAIL: + default: return Result::FAIL; + } + } }; // Global dfu handle @@ -39,19 +61,23 @@ USBHostHandle::Result USBHostHandle::Impl::Init(USBHostHandle::Config config) { config_ = config; /* Init host Library, add supported class and start the library. */ - if(USBH_Init(&hUsbHostHS, USBH_UserProcess, HOST_HS) != USBH_OK) + USBH_StatusTypeDef sta; + sta = USBH_Init(&hUsbHostHS, USBH_UserProcess, HOST_HS); + if(sta != USBH_OK) { - return Result::ERR; + return ConvertStatus(sta); } - if(USBH_RegisterClass(&hUsbHostHS, USBH_MSC_CLASS) != USBH_OK) + sta = USBH_RegisterClass(&hUsbHostHS, USBH_MSC_CLASS); + if(sta != USBH_OK) { - return Result::ERR; + return ConvertStatus(sta); } - if(USBH_Start(&hUsbHostHS) != USBH_OK) + sta = USBH_Start(&hUsbHostHS); + if(sta != USBH_OK) { - return Result::ERR; + return ConvertStatus(sta); } - return Result::OK; + return ConvertStatus(sta); } USBHostHandle::Result USBHostHandle::Impl::Deinit() @@ -61,9 +87,14 @@ USBHostHandle::Result USBHostHandle::Impl::Deinit() return Result::OK; } -void USBHostHandle::Impl::Process() +USBHostHandle::Result USBHostHandle::Impl::Process() { - USBH_Process(&hUsbHostHS); + return ConvertStatus(USBH_Process(&hUsbHostHS)); +} + +USBHostHandle::Result USBHostHandle::Impl::ReEnumerate() +{ + return ConvertStatus(USBH_ReEnumerate(&hUsbHostHS)); } bool USBHostHandle::Impl::GetReady() @@ -89,9 +120,14 @@ bool USBHostHandle::GetReady() return pimpl_->GetReady(); } -void USBHostHandle::Process() +USBHostHandle::Result USBHostHandle::Process() +{ + return pimpl_->Process(); +} + +USBHostHandle::Result USBHostHandle::ReEnumerate() { - pimpl_->Process(); + return pimpl_->ReEnumerate(); } bool USBHostHandle::GetPresent() @@ -106,18 +142,42 @@ bool USBHostHandle::GetPresent() // This isn't super useful for our typical code structure static void USBH_UserProcess(USBH_HandleTypeDef *phost, uint8_t id) { + auto &conf = msd_impl.GetConfig(); switch(id) { case HOST_USER_SELECT_CONFIGURATION: break; - + case HOST_USER_CLASS_ACTIVE: + Appli_state = APPLICATION_READY; + if(conf.class_active_callback) + { + auto cb = (conf.class_active_callback); + cb(conf.userdata); + } + break; + case HOST_USER_CLASS_SELECTED: break; + case HOST_USER_CONNECTION: + Appli_state = APPLICATION_START; + if(conf.connect_callback) + { + auto cb = (conf.connect_callback); + cb(conf.userdata); + } + break; case HOST_USER_DISCONNECTION: Appli_state = APPLICATION_DISCONNECT; + if(conf.disconnect_callback) + { + auto cb = (conf.disconnect_callback); + cb(conf.userdata); + } + break; + case HOST_USER_UNRECOVERED_ERROR: + if(conf.error_callback) + { + auto cb = (conf.error_callback); + cb(conf.userdata); + } break; - - case HOST_USER_CLASS_ACTIVE: Appli_state = APPLICATION_READY; break; - - case HOST_USER_CONNECTION: Appli_state = APPLICATION_START; break; - default: break; } } \ No newline at end of file diff --git a/src/hid/usb_host.h b/src/hid/usb_host.h index 95aecec..102b5ae 100644 --- a/src/hid/usb_host.h +++ b/src/hid/usb_host.h @@ -25,15 +25,63 @@ typedef enum class USBHostHandle { public: - enum Result + /** @brief return codes from the USB Processing + * can be used to check the state of USB while running + * outside of what may be happening with the limited user callbacks. + * + * At this time, these correlate directly to the ST Middleware + * USBH_StatusTypeDef codes + */ + enum class Result { - OK = 0, - ERR + OK, + BUSY, + FAIL, + NOT_SUPPORTED, + UNRECOVERED_ERROR, + ERROR_SPEED_UNKNOWN, }; - /** Configuration structure for interfacing with MSD Driver */ + /** @brief User defineable callback for USB Connection */ + typedef void (*ConnectCallback)(void* data); + + /** @brief User defineable callback for USB Disconnection */ + typedef void (*DisconnectCallback)(void* data); + + /** @brief User defineable callback upon completion of class initialization + * For example, when a USB drive is connected and the mass storage class + * initialization has finished, this callback will fire. + * + * @param userdata a pointer to some arbitrary data for use by the user. + * this is supplied in the Config struct. Can be used to avoid globals. + * + * @todo At some point this may be replaced for individual callbacks + * for each supported USB Host class. + */ + typedef void (*ClassActiveCallback)(void* userdata); + + /** @brief User defineable callback for USB Unrecoverable Error + * @todo add some sort of feedback about the type of error, etc. + * if possible + */ + typedef void (*ErrorCallback)(void* data); + + /** @brief Configuration structure for interfacing with MSD Driver */ struct Config { + Config() + : connect_callback(nullptr), + disconnect_callback(nullptr), + class_active_callback(nullptr), + error_callback(nullptr), + userdata(nullptr) + { + } + ConnectCallback connect_callback; + DisconnectCallback disconnect_callback; + ClassActiveCallback class_active_callback; + ErrorCallback error_callback; + void* userdata; }; /** Initializes the USB drivers and starts timeout. @@ -50,7 +98,10 @@ class USBHostHandle /** Manages usb host functionality * */ - void Process(); + Result Process(); + + /** Forces USB host to re-enumerate device */ + Result ReEnumerate(); /** Returns true if a Mass Storage Device is connected * and ready for communicaton diff --git a/src/per/spi.cpp b/src/per/spi.cpp index e7df53a..4baab0b 100644 --- a/src/per/spi.cpp +++ b/src/per/spi.cpp @@ -1083,6 +1083,26 @@ extern "C" void SPI1_IRQHandler(void) HAL_SPI_IRQHandler(&spi_handles[0].hspi_); } +extern "C" void SPI2_IRQHandler(void) +{ + HAL_SPI_IRQHandler(&spi_handles[1].hspi_); +} + +extern "C" void SPI3_IRQHandler(void) +{ + HAL_SPI_IRQHandler(&spi_handles[2].hspi_); +} + +extern "C" void SPI4_IRQHandler(void) +{ + HAL_SPI_IRQHandler(&spi_handles[3].hspi_); +} + +extern "C" void SPI5_IRQHandler(void) +{ + HAL_SPI_IRQHandler(&spi_handles[4].hspi_); +} + void HalSpiDmaRxStreamCallback(void) { ScopedIrqBlocker block; diff --git a/src/per/tim.cpp b/src/per/tim.cpp index 65f67d0..fbd1a1a 100644 --- a/src/per/tim.cpp +++ b/src/per/tim.cpp @@ -2,6 +2,7 @@ #include "util/hal_map.h" #include "sys/system.h" + // To save from digging in reference manual here are some notes: // // The following TIMs are on APB1 Clock (RM0433 Rev7 - pg 458): @@ -36,8 +37,23 @@ class TimerHandle::Impl void DelayMs(uint32_t del); void DelayUs(uint32_t del); + void SetCallback(TimerHandle::PeriodElapsedCallback cb, void* data) + { + if(cb) + { + callback_ = cb; + cb_data_ = data; + } + } + + void InternalCallback(); + + TimerHandle::Config config_; TIM_HandleTypeDef tim_hal_handle_; + + TimerHandle::PeriodElapsedCallback callback_; + void* cb_data_; }; // Error Handler @@ -53,6 +69,24 @@ static void Error_Handler() static TimerHandle::Impl tim_handles[4]; +/** @brief returns a poitner to the private implementation object associated + * with the peripheral instance (register base address) + * @return Pointer to global tim_handle object or NULL + */ +static TimerHandle::Impl* get_tim_impl_from_instance(TIM_TypeDef* per_instance) +{ + /** Check each impl */ + for(int i = 0; i < 4; i++) + { + TimerHandle::Impl* p = &tim_handles[i]; + if(p->tim_hal_handle_.Instance == per_instance) + { + return p; + } + } + return NULL; +} + // TIM functions TimerHandle::Result TimerHandle::Impl::Init(const TimerHandle::Config& config) @@ -76,9 +110,9 @@ TimerHandle::Result TimerHandle::Impl::Init(const TimerHandle::Config& config) // Default to longest period (16-bit timers handled separaately for clarity, // though 16-bit timers extra bits are probably don't care. if(tim_hal_handle_.Instance == TIM2 || tim_hal_handle_.Instance == TIM5) - tim_hal_handle_.Init.Period = 0xffffffff; + tim_hal_handle_.Init.Period = config_.period; else - tim_hal_handle_.Init.Period = 0xffff; + tim_hal_handle_.Init.Period = (uint16_t)config_.period; // Default Clock Division as none. tim_hal_handle_.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; @@ -126,9 +160,18 @@ TimerHandle::Result TimerHandle::Impl::DeInit() TimerHandle::Result TimerHandle::Impl::Start() { - return HAL_TIM_Base_Start(&tim_hal_handle_) == HAL_OK - ? TimerHandle::Result::OK - : TimerHandle::Result::ERR; + if(config_.enable_irq) + { + return HAL_TIM_Base_Start_IT(&tim_hal_handle_) == HAL_OK + ? TimerHandle::Result::OK + : TimerHandle::Result::ERR; + } + else + { + return HAL_TIM_Base_Start(&tim_hal_handle_) == HAL_OK + ? TimerHandle::Result::OK + : TimerHandle::Result::ERR; + } } TimerHandle::Result TimerHandle::Impl::Stop() @@ -140,7 +183,9 @@ TimerHandle::Result TimerHandle::Impl::Stop() TimerHandle::Result TimerHandle::Impl::SetPeriod(uint32_t ticks) { + config_.period = ticks; tim_hal_handle_.Instance->ARR = ticks; + tim_hal_handle_.Init.Period = ticks; return Result::OK; } @@ -191,30 +236,72 @@ void TimerHandle::Impl::DelayUs(uint32_t del) DelayTick(del * (GetFreq() / 1000000)); } +void TimerHandle::Impl::InternalCallback() +{ + if(callback_) + { + callback_(cb_data_); + } +} + // HAL Functions extern "C" { void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* tim_baseHandle) { + TimerHandle::Impl* impl + = get_tim_impl_from_instance(tim_baseHandle->Instance); + /** In the case of TIM6 (DAC usage) there will be no impl. + * The preceding enable_irq checks should be false + * since the default constructor sets it that way + */ + TimerHandle::Config cfg; + if(impl) + cfg = impl->GetConfig(); if(tim_baseHandle->Instance == TIM2) { __HAL_RCC_TIM2_CLK_ENABLE(); + if(cfg.enable_irq) + { + HAL_NVIC_SetPriority(TIM2_IRQn, 0x0f, 0); + HAL_NVIC_EnableIRQ(TIM2_IRQn); + } } else if(tim_baseHandle->Instance == TIM3) { __HAL_RCC_TIM3_CLK_ENABLE(); + if(cfg.enable_irq) + { + HAL_NVIC_SetPriority(TIM3_IRQn, 0x0f, 0); + HAL_NVIC_EnableIRQ(TIM3_IRQn); + } } else if(tim_baseHandle->Instance == TIM4) { __HAL_RCC_TIM4_CLK_ENABLE(); + if(cfg.enable_irq) + { + HAL_NVIC_SetPriority(TIM4_IRQn, 0x0f, 0); + HAL_NVIC_EnableIRQ(TIM4_IRQn); + } } else if(tim_baseHandle->Instance == TIM5) { __HAL_RCC_TIM5_CLK_ENABLE(); + /** @todo make this conditional based on user config */ + if(cfg.enable_irq) + { + HAL_NVIC_SetPriority(TIM5_IRQn, 0x0f, 0); + HAL_NVIC_EnableIRQ(TIM5_IRQn); + } } else if(tim_baseHandle->Instance == TIM6) { __HAL_RCC_TIM6_CLK_ENABLE(); + /** DAC Peripheral shares IRQ with TIM6 + * and is implemented as part of DAC + * callback structure. + */ } } @@ -223,28 +310,64 @@ extern "C" if(tim_baseHandle->Instance == TIM2) { __HAL_RCC_TIM2_CLK_DISABLE(); + HAL_NVIC_DisableIRQ(TIM2_IRQn); } else if(tim_baseHandle->Instance == TIM3) { __HAL_RCC_TIM3_CLK_DISABLE(); + HAL_NVIC_DisableIRQ(TIM3_IRQn); } else if(tim_baseHandle->Instance == TIM4) { __HAL_RCC_TIM4_CLK_DISABLE(); + HAL_NVIC_DisableIRQ(TIM4_IRQn); } else if(tim_baseHandle->Instance == TIM5) { __HAL_RCC_TIM5_CLK_DISABLE(); + HAL_NVIC_DisableIRQ(TIM5_IRQn); } else if(tim_baseHandle->Instance == TIM6) { __HAL_RCC_TIM6_CLK_DISABLE(); + /** DAC Peripheral shares IRQ with TIM6 + * and is implemented as part of DAC + * callback structure. + */ } } } // ISRs and event handlers +extern "C" void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef* htim) +{ + TimerHandle::Impl* impl = get_tim_impl_from_instance(htim->Instance); + if(impl) + impl->InternalCallback(); +} + +extern "C" void TIM2_IRQHandler(void) +{ + HAL_TIM_IRQHandler(&tim_handles[(int)TimerHandle::Config::Peripheral::TIM_2] + .tim_hal_handle_); +} +extern "C" void TIM3_IRQHandler(void) +{ + HAL_TIM_IRQHandler(&tim_handles[(int)TimerHandle::Config::Peripheral::TIM_3] + .tim_hal_handle_); +} +extern "C" void TIM4_IRQHandler(void) +{ + HAL_TIM_IRQHandler(&tim_handles[(int)TimerHandle::Config::Peripheral::TIM_4] + .tim_hal_handle_); +} +extern "C" void TIM5_IRQHandler(void) +{ + HAL_TIM_IRQHandler(&tim_handles[(int)TimerHandle::Config::Peripheral::TIM_5] + .tim_hal_handle_); +} + // Interface TimerHandle::Result TimerHandle::Init(const Config& config) @@ -318,6 +441,11 @@ void TimerHandle::DelayUs(uint32_t del) pimpl_->DelayUs(del); } +void TimerHandle::SetCallback(PeriodElapsedCallback cb, void* data) +{ + pimpl_->SetCallback(cb, data); +} + } // namespace daisy diff --git a/src/per/tim.h b/src/per/tim.h index 77a9718..c37b25e 100644 --- a/src/per/tim.h +++ b/src/per/tim.h @@ -6,7 +6,7 @@ namespace daisy { -/** Hardare timer peripheral support. +/** @brief Hardare timer peripheral support. ** ** Supports general-function TIM peripherals: ** - TIM2, TIM3, TIM4, TIM5 @@ -20,24 +20,30 @@ namespace daisy ** recommended. The data can be converted to the final time-base after getting the difference in ticks. ** (Using GetFreq() can be used for these time-base calculations). ** - ** TODO: - ** - Fix issues with realtime getters, and wrapping of the timer(s). + ** User callbacks can be set, and changed at any point during operation. However, + ** the Config::enable_irq must be set to true when initializing for the interrupts + ** to turn on and function. + ** + ** @todo Fix issues with realtime getters, and wrapping of the timer(s). ** - This very noticeable with default settings for the 16-bit counters. - ** - Dispatch periodic callback(s) - ** - Other General purpose timers - ** - Non-internal clock sources - ** - Use of the four-tim channels per tim + ** @todo Other General purpose timers + ** @todo Non-internal clock sources + ** @todo Use of the four-tim channels per tim ** - PWM, etc. ** - InputCapture/OutputCompare, etc. - ** - HRTIM - ** - Advanced timers (TIM1/TIM8) + ** @todo HRTIM + ** @todo Advanced timers (TIM1/TIM8) ** */ class TimerHandle { public: + /** @brief Configuration struct for the Peripheral + * @note These settings are used during initialization + * and changing them afterwards may not have the desired effect. + */ struct Config { - /** Hardwaare Timer to configure, and use. */ + /** @brief Hardware Timer to configure, and use. */ enum class Peripheral { TIM_2 = 0, /**< 32-bit counter */ @@ -46,8 +52,8 @@ class TimerHandle TIM_5, /**< 32-bit counter*/ }; - /** Direction of the auto-reload counter. - ** TODO: Add support for the various + /** @brief Direction of the auto-reload counter. + ** @todo Add support for the various ** versions of Up/Down counters. ** */ enum class CounterDir @@ -56,32 +62,54 @@ class TimerHandle DOWN, }; - Peripheral periph; - CounterDir dir; + Peripheral periph; /**< Hardware Peripheral */ + CounterDir dir; /**< Counter direction */ + + /** @brief period in ticks at TIM frequency that counter will reset based on dir + * @note TIM3 and TIM4 are both 16-bit timers. So the period maximum is 0xffff. + */ + uint32_t period; + bool enable_irq; /**< Enable interrupt for user based callback */ + + /* @brief Constructor for default states */ + Config() + : periph(Peripheral::TIM_2), + dir(CounterDir::UP), + period(0xffffffff), + enable_irq(false) + { + } }; - /** Return values for TIM funcitons. */ + /** @brief Return values for TIM funcitons. */ enum class Result { OK, ERR, }; + /** @brief User Callback type that will fire at the end of each timer + * period. This requires that Config::enable_irq is true before Init + * @param data pointer to arbitrary user-provided data + */ + typedef void (*PeriodElapsedCallback)(void* data); + TimerHandle() : pimpl_(nullptr) {} TimerHandle(const TimerHandle& other) = default; TimerHandle& operator=(const TimerHandle& other) = default; ~TimerHandle() {} - /** Initializes the timer according to the configuration */ + /** @brief Initializes the timer according to the configuration */ Result Init(const Config& config); - /** Deinitializes the timer */ + /** @brief Deinitializes the timer */ Result DeInit(); - /** Returns a const reference to the Config struct */ + /** @brief Returns a const reference to the Config struct */ const Config& GetConfig() const; - /** Sets the period of the Timer. + /** @brief Sets the period of the Timer. + * ** This is the number of ticks it takes before it wraps back around. ** For self-managed timing, this can be left at the default. (0xffff for 16-bit ** and 0xffffffff for 32-bit timers). @@ -89,7 +117,8 @@ class TimerHandle ** */ Result SetPeriod(uint32_t ticks); - /** Sets the Prescalar applied to the TIM peripheral. + /** @brief Sets the Prescalar applied to the TIM peripheral. + * ** This can be any number up to 0xffff ** This will adjust the rate of ticks: ** Calculated as APBN_Freq / prescalar per tick @@ -99,44 +128,52 @@ class TimerHandle ** */ Result SetPrescaler(uint32_t val); - /** Starts the TIM peripheral specified by Config */ + /** @brief Starts the TIM peripheral specified by Config */ Result Start(); - /** Stops the TIM peripheral specified by Config */ + /** @brief Stops the TIM peripheral specified by Config */ Result Stop(); - /** Returns the frequency of each tick of the timer in Hz */ + /** @brief Returns the frequency of each tick of the timer in Hz */ uint32_t GetFreq(); - /** Returns the number of counter position. + /** @brief Returns the number of counter position. + * ** This increments according to Config::CounterDir, ** and wraps around at the specified period (maxing out ** at 2^16 or 2^32 depending on the chosen TIM peripheral. */ uint32_t GetTick(); - /** Returns the ticks scaled as milliseconds + /** @brief Returns the ticks scaled as milliseconds ** ** Use care when using for measurements and ensure that ** the TIM period can handle the maximum desired measurement. ***/ uint32_t GetMs(); - /** Returns the ticks scaled as microseconds + /** @brief Returns the ticks scaled as microseconds ** ** Use care when using for measurements and ensure that ** the TIM period can handle the maximum desired measurement. ***/ uint32_t GetUs(); - /** Stay within this function for del ticks */ + /** @brief Stay within this function for del ticks */ void DelayTick(uint32_t del); - /** Stay within this function for del milliseconds */ + /** @brief Stay within this function for del milliseconds */ void DelayMs(uint32_t del); - /** Stay within this function for del microseconds */ + /** @brief Stay within this function for del microseconds */ void DelayUs(uint32_t del); + /** @brief Sets the PeriodElapsedCallback that will fire + * whenever the timer reaches the end of it's period. + * @param cb user callback + * @param data optional pointer to arbitrary data (defaults to nullptr) + */ + void SetCallback(PeriodElapsedCallback cb, void* data = nullptr); + class Impl; private: diff --git a/src/per/uart.cpp b/src/per/uart.cpp index 55fc667..07fef26 100644 --- a/src/per/uart.cpp +++ b/src/per/uart.cpp @@ -1,6 +1,8 @@ #include #include "per/uart.h" #include "util/ringbuffer.h" +#include "util/scopedirqblocker.h" + extern "C" { #include "util/hal_map.h" @@ -8,16 +10,9 @@ extern "C" using namespace daisy; -// Uncomment to use a second FIFO that is copied during the UART callback -// This will help with issues where data is overwritten while its being processed -// Cost: -// * 264 bytes (sizeof(UartRingBuffer)), to D1 RAM -// * 160 bytes on FLASH -// * Time to copy DMA FIFO to queue FIFO -#define UART_RX_DOUBLE_BUFFER 1 - #define UART_RX_BUFF_SIZE 256 +// the fifo buffer to be DMA read into typedef RingBuffer UartRingBuffer; static UartRingBuffer DMA_BUFFER_MEM_SECTION uart_dma_fifo; @@ -30,43 +25,107 @@ static void Error_Handler() class UartHandler::Impl { public: - UartHandler::Result Init(const UartHandler::Config& config); + struct UartDmaJob + { + uint8_t* data_rx = nullptr; + uint8_t* data_tx = nullptr; + uint16_t size = 0; + StartCallbackFunctionPtr start_callback = nullptr; + EndCallbackFunctionPtr end_callback = nullptr; + void* callback_context = nullptr; + DmaDirection direction = DmaDirection::TX; + + bool IsValidJob() const + { + if(direction == DmaDirection::TX) + { + return data_tx != nullptr; + } + else if(direction == DmaDirection::RX) + { + return data_rx != nullptr; + } + return false; + } + void Invalidate() { data_rx = data_tx = nullptr; } + }; - const UartHandler::Config& GetConfig() const { return config_; } + Result Init(const Config& config); - int PollReceive(uint8_t* buff, size_t size, uint32_t timeout); + const Config& GetConfig() const { return config_; } - UartHandler::Result StartRx(); + Result BlockingTransmit(uint8_t* buff, size_t size, uint32_t timeout); + Result BlockingReceive(uint8_t* buff, size_t size, uint32_t timeout); - bool RxActive(); + Result DmaTransmit(uint8_t* buff, + size_t size, + StartCallbackFunctionPtr start_callback, + EndCallbackFunctionPtr end_callback, + void* callback_context); - UartHandler::Result FlushRx(); + Result DmaReceive(uint8_t* buff, + size_t size, + StartCallbackFunctionPtr start_callback, + EndCallbackFunctionPtr end_callback, + void* callback_context); - UartHandler::Result PollTx(uint8_t* buff, size_t size); - uint8_t PopRx(); + Result StartDmaTx(uint8_t* buff, + size_t size, + StartCallbackFunctionPtr start_callback, + EndCallbackFunctionPtr end_callback, + void* callback_context); - size_t Readable(); + Result StartDmaRx(uint8_t* buff, + size_t size, + StartCallbackFunctionPtr start_callback, + EndCallbackFunctionPtr end_callback, + void* callback_context); - int CheckError(); + static void GlobalInit(); + static bool IsDmaBusy(); + static void DmaTransferFinished(UART_HandleTypeDef* huart, Result result); + + static void QueueDmaTransfer(size_t uart_idx, const UartDmaJob& job); + static bool IsDmaTransferQueuedFor(size_t uart_idx); + + static void DmaReceiveFifoEndCallback(void* context, Result res); + + Result SetDmaPeripheral(); - UartHandler::Result InitPins(); + Result InitDma(bool rx, bool tx); - UartHandler::Result DeInitPins(); + Result InitPins(); + + Result DeInitPins(); + + void HandleFifo(); + + Result DmaReceiveFifo(); + + Result FlushFifo(); + + uint8_t PopFifo(); + + size_t ReadableFifo(); + + int CheckError(); - void UARTRxComplete(); + static constexpr uint8_t kNumUartWithDma = 9; + static volatile int8_t dma_active_peripheral_; + static UartDmaJob queued_dma_transfers_[kNumUartWithDma]; + static EndCallbackFunctionPtr next_end_callback_; + static void* next_callback_context_; + Config config_; UART_HandleTypeDef huart_; DMA_HandleTypeDef hdma_rx_; - bool receiving_; - size_t rx_size_, rx_last_pos_; - UartRingBuffer* dma_fifo_rx_; - bool rx_active_, tx_active_; -#ifdef UART_RX_DOUBLE_BUFFER - UartRingBuffer queue_rx_; -#endif - - UartHandler::Config config_; + DMA_HandleTypeDef hdma_tx_; + + bool using_fifo_; + UartRingBuffer* dma_fifo_rx_; // pointer to FIFO DMA reads into + UartRingBuffer queue_rx_; // double buffer ( user reads from ) + size_t rx_last_pos_; }; @@ -99,6 +158,14 @@ UartHandler::Impl* MapInstanceToHandle(USART_TypeDef* instance) return NULL; } +void UartHandler::Impl::GlobalInit() +{ + // init the scheduler queue + dma_active_peripheral_ = -1; + for(int per = 0; per < kNumUartWithDma; per++) + queued_dma_transfers_[per] = UartHandler::Impl::UartDmaJob(); +} + UartHandler::Result UartHandler::Impl::Init(const UartHandler::Config& config) { config_ = config; @@ -186,81 +253,424 @@ UartHandler::Result UartHandler::Impl::Init(const UartHandler::Config& config) return Result::ERR; } - // Internal bits + // Fifo stuff + using_fifo_ = false; dma_fifo_rx_ = &uart_dma_fifo; dma_fifo_rx_->Init(); - rx_size_ = UART_RX_BUFF_SIZE; - // Buffer that gets copied - rx_active_ = false; - tx_active_ = false; -#ifdef UART_RX_DOUBLE_BUFFER queue_rx_.Init(); -#endif return Result::OK; } -int UartHandler::Impl::PollReceive(uint8_t* buff, size_t size, uint32_t timeout) + +UartHandler::Result UartHandler::Impl::SetDmaPeripheral() { - return HAL_UART_Receive(&huart_, (uint8_t*)buff, size, timeout); + switch(config_.periph) + { + case UartHandler::Config::Peripheral::USART_1: + hdma_rx_.Init.Request = DMA_REQUEST_USART1_RX; + hdma_tx_.Init.Request = DMA_REQUEST_USART1_TX; + break; + case UartHandler::Config::Peripheral::USART_2: + hdma_rx_.Init.Request = DMA_REQUEST_USART2_RX; + hdma_tx_.Init.Request = DMA_REQUEST_USART2_TX; + break; + case UartHandler::Config::Peripheral::USART_3: + hdma_rx_.Init.Request = DMA_REQUEST_USART3_RX; + hdma_tx_.Init.Request = DMA_REQUEST_USART3_TX; + break; + case UartHandler::Config::Peripheral::UART_4: + hdma_rx_.Init.Request = DMA_REQUEST_UART4_RX; + hdma_tx_.Init.Request = DMA_REQUEST_UART4_TX; + break; + case UartHandler::Config::Peripheral::UART_5: + hdma_rx_.Init.Request = DMA_REQUEST_UART5_RX; + hdma_tx_.Init.Request = DMA_REQUEST_UART5_TX; + break; + case UartHandler::Config::Peripheral::USART_6: + hdma_rx_.Init.Request = DMA_REQUEST_USART6_RX; + hdma_tx_.Init.Request = DMA_REQUEST_USART6_TX; + break; + case UartHandler::Config::Peripheral::UART_7: + hdma_rx_.Init.Request = DMA_REQUEST_UART7_RX; + hdma_tx_.Init.Request = DMA_REQUEST_UART7_TX; + break; + case UartHandler::Config::Peripheral::UART_8: + hdma_rx_.Init.Request = DMA_REQUEST_UART8_RX; + hdma_tx_.Init.Request = DMA_REQUEST_UART8_TX; + break; + + // LPUART1 is on BDMA_REQUEST_LPUART1_RX/TX + default: return UartHandler::Result::ERR; + } + return UartHandler::Result::OK; } -UartHandler::Result UartHandler::Impl::StartRx() +UartHandler::Result UartHandler::Impl::InitDma(bool rx, bool tx) { - int status = 0; - // Now start Rx - status = HAL_UART_Receive_DMA( - &huart_, (uint8_t*)dma_fifo_rx_->GetMutableBuffer(), rx_size_); - if(status == 0) - rx_active_ = true; - return rx_active_ ? Result::OK : Result::ERR; + hdma_rx_.Instance = DMA1_Stream5; + hdma_rx_.Init.PeriphInc = DMA_PINC_DISABLE; + hdma_rx_.Init.MemInc = DMA_MINC_ENABLE; + hdma_rx_.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; + hdma_rx_.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; + hdma_rx_.Init.Mode = using_fifo_ ? DMA_CIRCULAR : DMA_NORMAL; + hdma_rx_.Init.Priority = DMA_PRIORITY_VERY_HIGH; + hdma_rx_.Init.FIFOMode = DMA_FIFOMODE_DISABLE; + hdma_rx_.Init.Direction = DMA_PERIPH_TO_MEMORY; + + hdma_tx_.Instance = DMA2_Stream4; + hdma_tx_.Init.PeriphInc = DMA_PINC_DISABLE; + hdma_tx_.Init.MemInc = DMA_MINC_ENABLE; + hdma_tx_.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; + hdma_tx_.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; + hdma_tx_.Init.Mode = DMA_NORMAL; + hdma_tx_.Init.Priority = DMA_PRIORITY_VERY_HIGH; + hdma_tx_.Init.FIFOMode = DMA_FIFOMODE_DISABLE; + hdma_tx_.Init.Direction = DMA_MEMORY_TO_PERIPH; + SetDmaPeripheral(); + + if(rx) + { + if(HAL_DMA_Init(&hdma_rx_) != HAL_OK) + { + Error_Handler(); + return UartHandler::Result::ERR; + } + __HAL_LINKDMA(&huart_, hdmarx, hdma_rx_); + } + + if(tx) + { + if(HAL_DMA_Init(&hdma_tx_) != HAL_OK) + { + Error_Handler(); + return UartHandler::Result::ERR; + } + __HAL_LINKDMA(&huart_, hdmatx, hdma_tx_); + } + + if(using_fifo_) + { + // Disable HalfTransfer Interrupt + ((DMA_Stream_TypeDef*)hdma_rx_.Instance)->CR &= ~(DMA_SxCR_HTIE); + + //enable idle interrupts + __HAL_UART_ENABLE_IT(&huart_, UART_IT_IDLE); + } + + return UartHandler::Result::OK; } -bool UartHandler::Impl::RxActive() +// formerly known as "UARTRxComplete()" +void UartHandler::Impl::HandleFifo() { - return rx_active_; + size_t len, cur_pos; + + //get current write pointer + cur_pos = (UART_RX_BUFF_SIZE + - ((DMA_Stream_TypeDef*)huart_.hdmarx->Instance)->NDTR) + & (UART_RX_BUFF_SIZE - 1); + + //calculate how far the DMA write pointer has moved + len = (cur_pos - rx_last_pos_ + UART_RX_BUFF_SIZE) % UART_RX_BUFF_SIZE; + + dma_fifo_rx_->Advance(len); + rx_last_pos_ = cur_pos; + + // Copy to queue fifo we don't want to use primary fifo to avoid + // changes to the buffer while its being processed + uint8_t processbuf[256]; + dma_fifo_rx_->ImmediateRead(processbuf, len); + queue_rx_.Overwrite(processbuf, len); } -//this originally had a useless status var hanging about -//I don't think we can actually error check this... -UartHandler::Result UartHandler::Impl::FlushRx() +void UartHandler::Impl::DmaTransferFinished(UART_HandleTypeDef* huart, + UartHandler::Result result) { -#ifdef UART_RX_DOUBLE_BUFFER - queue_rx_.Flush(); -#else - dma_fifo_rx_->Flush(); -#endif + ScopedIrqBlocker block; + + // on an error, reinit the peripheral to clear any flags + if(result != UartHandler::Result::OK) + HAL_UART_Init(huart); + + dma_active_peripheral_ = -1; + + if(next_end_callback_ != nullptr) + { + // the callback may setup another transmission, hence we shouldn't reset this to + // nullptr after the callback - it might overwrite the new transmission. + auto callback = next_end_callback_; + next_end_callback_ = nullptr; + // make the callback + callback(next_callback_context_, result); + } + + // the callback could have started a new transmission right away... + if(IsDmaBusy()) + return; + + // dma is still idle. Check if another UART peripheral waits for a job. + for(int per = 0; per < kNumUartWithDma; per++) + if(IsDmaTransferQueuedFor(per)) + { + UartHandler::Result result; + if(queued_dma_transfers_[per].direction + == UartHandler::DmaDirection::TX) + { + result = uart_handles[per].StartDmaTx( + queued_dma_transfers_[per].data_tx, + queued_dma_transfers_[per].size, + queued_dma_transfers_[per].start_callback, + queued_dma_transfers_[per].end_callback, + queued_dma_transfers_[per].callback_context); + } + else + { + result = uart_handles[per].StartDmaRx( + queued_dma_transfers_[per].data_rx, + queued_dma_transfers_[per].size, + queued_dma_transfers_[per].start_callback, + queued_dma_transfers_[per].end_callback, + queued_dma_transfers_[per].callback_context); + } + if(result == UartHandler::Result::OK) + { + // remove the job from the queue + queued_dma_transfers_[per].Invalidate(); + return; + } + } +} + +bool UartHandler::Impl::IsDmaBusy() +{ + return dma_active_peripheral_ >= 0; +} + +bool UartHandler::Impl::IsDmaTransferQueuedFor(size_t uart_idx) +{ + return queued_dma_transfers_[uart_idx].IsValidJob(); +} + +void UartHandler::Impl::QueueDmaTransfer(size_t uart_idx, const UartDmaJob& job) +{ + // wait for any previous job on this peripheral to finish + // and the queue position to become free + while(IsDmaTransferQueuedFor(uart_idx)) + { + continue; + }; + + // queue the job + ScopedIrqBlocker block; + queued_dma_transfers_[uart_idx] = job; +} + + +UartHandler::Result UartHandler::Impl::DmaTransmit( + uint8_t* buff, + size_t size, + UartHandler::StartCallbackFunctionPtr start_callback, + UartHandler::EndCallbackFunctionPtr end_callback, + void* callback_context) +{ + // if dma is currently running - queue a job + if(IsDmaBusy()) + { + UartDmaJob job; + job.data_tx = buff; + job.size = size; + job.direction = UartHandler::DmaDirection::TX; + job.start_callback = start_callback; + job.end_callback = end_callback; + job.callback_context = callback_context; + + const int uart_idx = int(config_.periph); + + // queue a job (blocks until the queue position is free) + QueueDmaTransfer(uart_idx, job); + // TODO: the user can't tell if he got returned "OK" + // because the transfer was executed or because it was queued... + // should we change that? + return UartHandler::Result::OK; + } + + return StartDmaTx( + buff, size, start_callback, end_callback, callback_context); +} + +UartHandler::Result UartHandler::Impl::StartDmaTx( + uint8_t* buff, + size_t size, + UartHandler::StartCallbackFunctionPtr start_callback, + UartHandler::EndCallbackFunctionPtr end_callback, + void* callback_context) +{ + while(HAL_UART_GetState(&huart_) != HAL_UART_STATE_READY) {}; + + if(InitDma(false, true) != UartHandler::Result::OK) + { + if(end_callback) + end_callback(callback_context, UartHandler::Result::ERR); + return UartHandler::Result::ERR; + } + + ScopedIrqBlocker block; + + dma_active_peripheral_ = int(config_.periph); + next_end_callback_ = end_callback; + next_callback_context_ = callback_context; + + if(start_callback) + start_callback(callback_context); + + if(HAL_UART_Transmit_DMA(&huart_, buff, size) != HAL_OK) + { + dma_active_peripheral_ = -1; + next_end_callback_ = NULL; + next_callback_context_ = NULL; + if(end_callback) + end_callback(callback_context, UartHandler::Result::ERR); + return UartHandler::Result::ERR; + } + return UartHandler::Result::OK; +} + +UartHandler::Result UartHandler::Impl::DmaReceive( + uint8_t* buff, + size_t size, + UartHandler::StartCallbackFunctionPtr start_callback, + UartHandler::EndCallbackFunctionPtr end_callback, + void* callback_context) +{ + // if dma is currently running - queue a job + if(IsDmaBusy()) + { + UartDmaJob job; + job.data_rx = buff; + job.size = size; + job.direction = UartHandler::DmaDirection::RX; + job.start_callback = start_callback; + job.end_callback = end_callback; + job.callback_context = callback_context; + + const int uart_idx = int(config_.periph); + + // queue a job (blocks until the queue position is free) + QueueDmaTransfer(uart_idx, job); + // TODO: the user can't tell if he got returned "OK" + // because the transfer was executed or because it was queued... + // should we change that? + return UartHandler::Result::OK; + } + + return StartDmaRx( + buff, size, start_callback, end_callback, callback_context); +} + +UartHandler::Result UartHandler::Impl::StartDmaRx( + uint8_t* buff, + size_t size, + UartHandler::StartCallbackFunctionPtr start_callback, + UartHandler::EndCallbackFunctionPtr end_callback, + void* callback_context) +{ + while(HAL_UART_GetState(&huart_) != HAL_UART_STATE_READY) {}; + + if(InitDma(true, false) != UartHandler::Result::OK) + { + if(end_callback) + end_callback(callback_context, UartHandler::Result::ERR); + return UartHandler::Result::ERR; + } + + ScopedIrqBlocker block; + + dma_active_peripheral_ = int(config_.periph); + next_end_callback_ = end_callback; + next_callback_context_ = callback_context; + + if(start_callback) + start_callback(callback_context); + + if(HAL_UART_Receive_DMA(&huart_, buff, size) != HAL_OK) + { + dma_active_peripheral_ = -1; + next_end_callback_ = NULL; + next_callback_context_ = NULL; + if(end_callback) + end_callback(callback_context, UartHandler::Result::ERR); + return UartHandler::Result::ERR; + } + return UartHandler::Result::OK; +} + +UartHandler::Result +UartHandler::Impl::BlockingReceive(uint8_t* buff, size_t size, uint32_t timeout) +{ + if(HAL_UART_Receive(&huart_, (uint8_t*)buff, size, timeout) != HAL_OK) + { + return Result::ERR; + } return Result::OK; } -UartHandler::Result UartHandler::Impl::PollTx(uint8_t* buff, size_t size) +UartHandler::Result UartHandler::Impl::BlockingTransmit(uint8_t* buff, + size_t size, + uint32_t timeout) { - HAL_StatusTypeDef status - = HAL_UART_Transmit(&huart_, (uint8_t*)buff, size, 10); - return (status == HAL_OK ? Result::OK : Result::ERR); + if(HAL_UART_Transmit(&huart_, (uint8_t*)buff, size, timeout) != HAL_OK) + { + return Result::ERR; + } + return Result::OK; } -int UartHandler::Impl::CheckError() +//gets called if the buffer is overrun, transfer buffers and restart DMA +void UartHandler::Impl::DmaReceiveFifoEndCallback(void* context, + UartHandler::Result res) { - return HAL_UART_GetError(&huart_); + // when the DMA hits the end in circular mode, it's considered an ERROR + // for some reason it sometimes comes up as OK as well though + // for now we'll catch the errors and just reset + if(res == UartHandler::Result::OK || res == UartHandler::Result::ERR) + { + UartHandler::Impl* handle = (UartHandler::Impl*)context; + handle->HandleFifo(); + HAL_UART_Init(&handle->huart_); + handle->DmaReceiveFifo(); + } } -uint8_t UartHandler::Impl::PopRx() +UartHandler::Result UartHandler::Impl::DmaReceiveFifo() +{ + using_fifo_ = true; + return DmaReceive((uint8_t*)dma_fifo_rx_->GetMutableBuffer(), + UART_RX_BUFF_SIZE, + NULL, + UartHandler::Impl::DmaReceiveFifoEndCallback, + (void*)this); +} + +UartHandler::Result UartHandler::Impl::FlushFifo() +{ + queue_rx_.Flush(); + return Result::OK; +} + +uint8_t UartHandler::Impl::PopFifo() { -#ifdef UART_RX_DOUBLE_BUFFER return queue_rx_.Read(); -#else - return dma_fifo_rx_->Read(); -#endif } -size_t UartHandler::Impl::Readable() +size_t UartHandler::Impl::ReadableFifo() { -#ifdef UART_RX_DOUBLE_BUFFER return queue_rx_.readable(); -#else - return dma_fifo_rx_->readable(); -#endif +} + +int UartHandler::Impl::CheckError() +{ + return HAL_UART_GetError(&huart_); } typedef struct @@ -415,75 +825,12 @@ UartHandler::Result UartHandler::Impl::DeInitPins() return Result::OK; } -// Callbacks -void UartHandler::Impl::UARTRxComplete() -{ - size_t len, cur_pos; - //get current write pointer - cur_pos = (rx_size_ - ((DMA_Stream_TypeDef*)huart_.hdmarx->Instance)->NDTR) - & (rx_size_ - 1); - //calculate how far the DMA write pointer has moved - len = (cur_pos - rx_last_pos_ + rx_size_) % rx_size_; - //check message size - if(len <= rx_size_) - { - dma_fifo_rx_->Advance(len); - rx_last_pos_ = cur_pos; -#ifdef UART_RX_DOUBLE_BUFFER - // Copy to queue fifo we don't want to use primary fifo to avoid - // changes to the buffer while its being processed - uint8_t processbuf[256]; - dma_fifo_rx_->ImmediateRead(processbuf, len); - queue_rx_.Overwrite(processbuf, len); -#endif - } - else - { - while(1) - ; //implement message to large exception - } -} -void HAL_UART_RxCpltCallback(UART_HandleTypeDef* huart) -{ - UartHandler::Impl* handle = MapInstanceToHandle(huart->Instance); - if(__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE)) - { - handle->UARTRxComplete(); - } -} - -void HAL_UART_ErrorCallback(UART_HandleTypeDef* huart) -{ - switch(huart->ErrorCode) - { - case HAL_UART_ERROR_NONE: break; - case HAL_UART_ERROR_PE: break; // Parity Error - case HAL_UART_ERROR_NE: break; // Noise Error - case HAL_UART_ERROR_FE: break; // Frame Error - case HAL_UART_ERROR_ORE: break; // Overrun Error - case HAL_UART_ERROR_DMA: break; // DMA Transfer Erro - default: break; - } - // Mark rx as deactivated - MapInstanceToHandle(huart->Instance)->rx_active_ = false; -} -void HAL_UART_AbortCpltCallback(UART_HandleTypeDef* huart) -{ - // asm("bkpt 255"); -} -void HAL_UART_AbortTransmitCpltCallback(UART_HandleTypeDef* huart) -{ - // asm("bkpt 255"); -} -void HAL_UART_AbortReceiveCpltCallback(UART_HandleTypeDef* huart) -{ - // asm("bkpt 255"); -} +volatile int8_t UartHandler::Impl::dma_active_peripheral_; +UartHandler::Impl::UartDmaJob + UartHandler::Impl::queued_dma_transfers_[kNumUartWithDma]; -// Unimplemented HAL Callbacks -//void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart); -//void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart); -//void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart); +UartHandler::EndCallbackFunctionPtr UartHandler::Impl::next_end_callback_; +void* UartHandler::Impl::next_callback_context_; // HAL Interface functions void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle) @@ -492,7 +839,6 @@ void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle) dsy_hal_map_gpio_clk_enable(handle->config_.pin_config.rx.port); dsy_hal_map_gpio_clk_enable(handle->config_.pin_config.tx.port); - //enable the clock for our periph switch(handle->config_.periph) { case UartHandler::Config::Peripheral::USART_1: @@ -529,48 +875,19 @@ void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle) Error_Handler(); } - /* USART1 DMA Init */ - /* USART1_RX Init */ - //usart1 uses dma by default for now - if(handle->huart_.Instance == USART1) - { - handle->hdma_rx_.Instance = DMA1_Stream5; - handle->hdma_rx_.Init.Request = DMA_REQUEST_USART1_RX; - handle->hdma_rx_.Init.Direction = DMA_PERIPH_TO_MEMORY; - handle->hdma_rx_.Init.PeriphInc = DMA_PINC_DISABLE; - handle->hdma_rx_.Init.MemInc = DMA_MINC_ENABLE; - handle->hdma_rx_.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; - handle->hdma_rx_.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; - handle->hdma_rx_.Init.Mode = DMA_CIRCULAR; - handle->hdma_rx_.Init.Priority = DMA_PRIORITY_LOW; - handle->hdma_rx_.Init.FIFOMode = DMA_FIFOMODE_DISABLE; - if(HAL_DMA_Init(&handle->hdma_rx_) != HAL_OK) - { - Error_Handler(); - } - - __HAL_LINKDMA(uartHandle, hdmarx, handle->hdma_rx_); - - // Disable HalfTransfer Interrupt - ((DMA_Stream_TypeDef*)handle->hdma_rx_.Instance)->CR - &= ~(DMA_SxCR_HTIE); - - IRQn_Type types[] = {USART1_IRQn, - USART2_IRQn, - USART3_IRQn, - UART4_IRQn, - UART5_IRQn, - USART6_IRQn, - UART7_IRQn, - UART8_IRQn, - LPUART1_IRQn}; - - HAL_NVIC_SetPriority(types[(int)handle->config_.periph], 0, 0); - HAL_NVIC_EnableIRQ(types[(int)handle->config_.periph]); - } + // enable the interrupts + IRQn_Type types[] = {USART1_IRQn, + USART2_IRQn, + USART3_IRQn, + UART4_IRQn, + UART5_IRQn, + USART6_IRQn, + UART7_IRQn, + UART8_IRQn, + LPUART1_IRQn}; - /* USER CODE BEGIN USART1_MspInit 1 */ - __HAL_UART_ENABLE_IT(&handle->huart_, UART_IT_IDLE); + HAL_NVIC_SetPriority(types[(int)handle->config_.periph], 0, 0); + HAL_NVIC_EnableIRQ(types[(int)handle->config_.periph]); } void HAL_UART_MspDeInit(UART_HandleTypeDef* uartHandle) @@ -612,6 +929,7 @@ void HAL_UART_MspDeInit(UART_HandleTypeDef* uartHandle) handle->DeInitPins(); HAL_DMA_DeInit(uartHandle->hdmarx); + HAL_DMA_DeInit(uartHandle->hdmatx); IRQn_Type types[] = {USART1_IRQn, USART2_IRQn, @@ -627,20 +945,24 @@ void HAL_UART_MspDeInit(UART_HandleTypeDef* uartHandle) HAL_NVIC_DisableIRQ(types[(int)handle->config_.periph]); } +extern "C" void dsy_uart_global_init() +{ + UartHandler::Impl::GlobalInit(); +} + +// HAL Interrupts. void UART_IRQHandler(UartHandler::Impl* handle) { HAL_UART_IRQHandler(&handle->huart_); - // if(__HAL_UART_GET_FLAG(&huart, UART_FLAG_IDLE)) - // { - if((handle->huart_.Instance->ISR & UART_FLAG_IDLE) == UART_FLAG_IDLE) + + if(__HAL_UART_GET_FLAG(&handle->huart_, UART_FLAG_IDLE) + && handle->using_fifo_) { - HAL_UART_RxCpltCallback(&handle->huart_); - //__HAL_UART_CLEAR_IDLEFLAG(&huart); + handle->HandleFifo(); handle->huart_.Instance->ICR = UART_FLAG_IDLE; } } -// HAL Interrupts. extern "C" { void USART1_IRQHandler() { UART_IRQHandler(&uart_handles[0]); } @@ -652,14 +974,60 @@ extern "C" void UART7_IRQHandler() { UART_IRQHandler(&uart_handles[6]); } void UART8_IRQHandler() { UART_IRQHandler(&uart_handles[7]); } void LPUART1_IRQHandler() { UART_IRQHandler(&uart_handles[8]); } +} - void DMA1_Stream5_IRQHandler() - { - //TODO for now USART1 is the only one working with DMA - //in the future we want to keep track of who connects to which DMA - //stream, then refer to that info here - HAL_DMA_IRQHandler(&uart_handles[0].hdma_rx_); - } +void HalUartDmaRxStreamCallback(void) +{ + ScopedIrqBlocker block; + if(UartHandler::Impl::dma_active_peripheral_ >= 0) + HAL_DMA_IRQHandler( + &uart_handles[UartHandler::Impl::dma_active_peripheral_].hdma_rx_); +} +extern "C" void DMA1_Stream5_IRQHandler(void) +{ + HalUartDmaRxStreamCallback(); +} + +void HalUartDmaTxStreamCallback(void) +{ + ScopedIrqBlocker block; + if(UartHandler::Impl::dma_active_peripheral_ >= 0) + HAL_DMA_IRQHandler( + &uart_handles[UartHandler::Impl::dma_active_peripheral_].hdma_tx_); +} +extern "C" void DMA2_Stream4_IRQHandler(void) +{ + HalUartDmaTxStreamCallback(); +} + +extern "C" void HAL_UART_TxCpltCallback(UART_HandleTypeDef* huart) +{ + UartHandler::Impl::DmaTransferFinished(huart, UartHandler::Result::OK); +} + +extern "C" void HAL_UART_RxCpltCallback(UART_HandleTypeDef* huart) +{ + UartHandler::Impl::DmaTransferFinished(huart, UartHandler::Result::OK); +} + +extern "C" void HAL_UART_ErrorCallback(UART_HandleTypeDef* huart) +{ + UartHandler::Impl::DmaTransferFinished(huart, UartHandler::Result::ERR); +} + +extern "C" void HAL_UART_AbortCpltCallback(UART_HandleTypeDef* huart) +{ + // asm("bkpt 255"); +} + +extern "C" void HAL_UART_AbortTransmitCpltCallback(UART_HandleTypeDef* huart) +{ + // asm("bkpt 255"); +} + +extern "C" void HAL_UART_AbortReceiveCpltCallback(UART_HandleTypeDef* huart) +{ + // asm("bkpt 255"); } // ====================================================================== @@ -677,42 +1045,94 @@ const UartHandler::Config& UartHandler::GetConfig() const return pimpl_->GetConfig(); } -int UartHandler::PollReceive(uint8_t* buff, size_t size, uint32_t timeout) +UartHandler::Result +UartHandler::BlockingTransmit(uint8_t* buff, size_t size, uint32_t timeout) { - return pimpl_->PollReceive(buff, size, timeout); + return pimpl_->BlockingTransmit(buff, size, timeout); } -UartHandler::Result UartHandler::StartRx() +UartHandler::Result +UartHandler::BlockingReceive(uint8_t* buffer, uint16_t size, uint32_t timeout) +{ + return pimpl_->BlockingReceive(buffer, size, timeout); +} + + +UartHandler::Result +UartHandler::DmaTransmit(uint8_t* buff, + size_t size, + UartHandler::StartCallbackFunctionPtr start_callback, + UartHandler::EndCallbackFunctionPtr end_callback, + void* callback_context) { - return pimpl_->StartRx(); + return pimpl_->DmaTransmit( + buff, size, start_callback, end_callback, callback_context); } -bool UartHandler::RxActive() +UartHandler::Result +UartHandler::DmaReceive(uint8_t* buff, + size_t size, + UartHandler::StartCallbackFunctionPtr start_callback, + UartHandler::EndCallbackFunctionPtr end_callback, + void* callback_context) { - return pimpl_->RxActive(); + return pimpl_->DmaReceive( + buff, size, start_callback, end_callback, callback_context); } -UartHandler::Result UartHandler::FlushRx() +UartHandler::Result UartHandler::DmaReceiveFifo() { - return pimpl_->FlushRx(); + return pimpl_->DmaReceiveFifo(); } -UartHandler::Result UartHandler::PollTx(uint8_t* buff, size_t size) +UartHandler::Result UartHandler::FlushFifo() { - return pimpl_->PollTx(buff, size); + return pimpl_->FlushFifo(); } -uint8_t UartHandler::PopRx() +uint8_t UartHandler::PopFifo() { - return pimpl_->PopRx(); + return pimpl_->PopFifo(); } -size_t UartHandler::Readable() +size_t UartHandler::ReadableFifo() { - return pimpl_->Readable(); + return pimpl_->ReadableFifo(); } int UartHandler::CheckError() { return pimpl_->CheckError(); -} \ No newline at end of file +} + +// ========= wrappers to be deprecated ========= + +int UartHandler::PollReceive(uint8_t* buff, size_t size, uint32_t timeout) +{ + return pimpl_->BlockingReceive(buff, size, timeout) == Result::ERR; +} + +UartHandler::Result UartHandler::PollTx(uint8_t* buff, size_t size) +{ + return pimpl_->BlockingTransmit(buff, size, 10); +} + +uint8_t UartHandler::PopRx() +{ + return pimpl_->PopFifo(); +} + +UartHandler::Result UartHandler::StartRx() +{ + return pimpl_->DmaReceiveFifo(); +} + +UartHandler::Result UartHandler::FlushRx() +{ + return pimpl_->FlushFifo(); +} + +size_t UartHandler::Readable() +{ + return pimpl_->ReadableFifo(); +} diff --git a/src/per/uart.h b/src/per/uart.h index efa35c8..197b45d 100644 --- a/src/per/uart.h +++ b/src/per/uart.h @@ -77,11 +77,12 @@ class UartHandler Config() { - // user must init periph, pin_config, and periph + // user must init periph, pin_config, and mode stopbits = StopBits::BITS_1; parity = Parity::NONE; wordlength = WordLength::BITS_8; baudrate = 4800; + // baudrate = 31250; } Peripheral periph; @@ -104,64 +105,130 @@ class UartHandler ERR /**< & */ }; + enum class DmaDirection + { + RX, /**< & */ + TX /**< & */ + }; + /** Initializes the UART Peripheral */ Result Init(const Config& config); /** Returns the current config. */ const Config& GetConfig() const; - /** Reads the amount of bytes in blocking mode with a 10ms timeout. - \param *buff Buffer to read to - \param size Buff size - \param timeout How long to timeout for (10ms?) - \return Data received - */ - int PollReceive(uint8_t* buff, size_t size, uint32_t timeout); + /** A callback to be executed right before a dma transfer is started. */ + typedef void (*StartCallbackFunctionPtr)(void* context); + /** A callback to be executed after a dma transfer is completed. */ + typedef void (*EndCallbackFunctionPtr)(void* context, Result result); - /** Starts a DMA Receive callback to fill a buffer of specified size. - Data is populated into a FIFO queue, and can be queried with the - functions below. - Size of the buffer is internally fixed to 256. - Variable message lengths are transferred to the FIFO queue - anytime there is 1 byte-period without incoming data - \return OK or ERROR + /** Blocking transmit + \param buff input buffer + \param size buffer size + \param timeout how long in milliseconds the function will wait + before returning without successful communication */ - Result StartRx(); + Result BlockingTransmit(uint8_t* buff, size_t size, uint32_t timeout = 100); + + /** Polling Receive + \param buffer input buffer + \param size buffer size + \param timeout How long to timeout for in milliseconds + \return Whether the receive was successful or not + */ + Result + BlockingReceive(uint8_t* buffer, uint16_t size, uint32_t timeout = 100); + + /** DMA-based transmit + \param *buff input buffer + \param size buffer size + \param start_callback A callback to execute when the transfer starts, or NULL. + The callback is called from an interrupt, so keep it fast. + \param end_callback A callback to execute when the transfer finishes, or NULL. + The callback is called from an interrupt, so keep it fast. + \param callback_context A pointer that will be passed back to you in the callbacks. + \return Whether the transmit was successful or not + */ + Result DmaTransmit(uint8_t* buff, + size_t size, + UartHandler::StartCallbackFunctionPtr start_callback, + UartHandler::EndCallbackFunctionPtr end_callback, + void* callback_context); + + /** DMA-based receive + \param *buff input buffer + \param size buffer size + \param start_callback A callback to execute when the transfer starts, or NULL. + The callback is called from an interrupt, so keep it fast. + \param end_callback A callback to execute when the transfer finishes, or NULL. + The callback is called from an interrupt, so keep it fast. + \param callback_context A pointer that will be passed back to you in the callbacks. + \return Whether the receive was successful or not + */ + Result DmaReceive(uint8_t* buff, + size_t size, + UartHandler::StartCallbackFunctionPtr start_callback, + UartHandler::EndCallbackFunctionPtr end_callback, + void* callback_context); - /** \return whether Rx DMA is listening or not. */ - bool RxActive(); + /** \return the result of HAL_UART_GetError() to the user. */ + int CheckError(); - /** Flushes the Receive Queue - \return OK or ERROR + /** Start the DMA Receive with a double buffered FIFO + \return OK or ERR */ - Result FlushRx(); + Result DmaReceiveFifo(); + + /** Flush all of the data from the fifo + \return OK or ERR + */ + Result FlushFifo(); + + /** Get the top item off of the FIFO + \return Top item from the FIFO + */ + uint8_t PopFifo(); - /** Sends an amount of data in blocking mode. - \param *buff Buffer of data to send - \param size Buffer size - \return OK or ERROR - */ + /** How much data is in the FIFO + \return number of elements ready to pop from FIFO + */ + size_t ReadableFifo(); + + /** Will be deprecated soon! Wrapper for BlockingTransmit */ + int PollReceive(uint8_t* buff, size_t size, uint32_t timeout); + + /** Will be deprecated soon! Wrapper for BlockingTransmit */ Result PollTx(uint8_t* buff, size_t size); - /** Pops the oldest byte from the FIFO. - \return Popped byte - */ + /** Will be deprecated soon! Wrapper for DmaReceiveFifo */ + Result StartRx(); + + /** Will be deprecated soon! + \return true. New DMA will always restart itself. + */ + bool RxActive() { return true; } + + /** Will be deprecated soon! Wrapper for FlushFifo */ + Result FlushRx(); + + /** Will be deprecated soon! Wrapper PopFifo */ uint8_t PopRx(); - /** Checks if there are any unread bytes in the FIFO - \return 1 or 0 ?? - */ + /** Will be deprecated soon! Wrapper for ReadableFifo */ size_t Readable(); - /** \return the result of HAL_UART_GetError() to the user. */ - int CheckError(); - class Impl; /**< & */ private: Impl* pimpl_; }; +extern "C" +{ + /** internal. Used for global init. */ + void dsy_uart_global_init(); +}; + /** @} */ } // namespace daisy diff --git a/src/sys/dma.c b/src/sys/dma.c index e376257..73c0a1e 100644 --- a/src/sys/dma.c +++ b/src/sys/dma.c @@ -28,9 +28,11 @@ extern "C" // DMA1_Stream4_IRQn interrupt configuration HAL_NVIC_SetPriority(DMA1_Stream4_IRQn, 0, 0); HAL_NVIC_EnableIRQ(DMA1_Stream4_IRQn); - // DMA1_Stream5_IRQn interrupt configuration + // DMA1_Stream5_IRQn and DMA2_Stream4_IRQn interrupt configuration for uart rx and tx HAL_NVIC_SetPriority(DMA1_Stream5_IRQn, 0, 0); HAL_NVIC_EnableIRQ(DMA1_Stream5_IRQn); + HAL_NVIC_SetPriority(DMA2_Stream4_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(DMA2_Stream4_IRQn); // DMA1_Stream6_IRQn interrupt configuration for I2C HAL_NVIC_SetPriority(DMA1_Stream6_IRQn, 0, 0); HAL_NVIC_EnableIRQ(DMA1_Stream6_IRQn); @@ -65,8 +67,9 @@ extern "C" HAL_NVIC_DisableIRQ(DMA1_Stream3_IRQn); // DMA1_Stream4_IRQn interrupt configuration HAL_NVIC_DisableIRQ(DMA1_Stream4_IRQn); - // DMA1_Stream5_IRQn interrupt configuration + // DMA1_Stream5_IRQn and DMA2_Stream4_IRQn interrupt configuration for uart rx and tx HAL_NVIC_DisableIRQ(DMA1_Stream5_IRQn); + HAL_NVIC_DisableIRQ(DMA2_Stream4_IRQn); // DMA1_Stream6_IRQn interrupt configuration for I2C HAL_NVIC_DisableIRQ(DMA1_Stream6_IRQn); // DMA2_Stream0_IRQn, interrupt configuration for DAC Ch1 diff --git a/src/sys/stm32h7xx_hal_conf.h b/src/sys/stm32h7xx_hal_conf.h index 4f3b13d..3f05998 100644 --- a/src/sys/stm32h7xx_hal_conf.h +++ b/src/sys/stm32h7xx_hal_conf.h @@ -164,7 +164,7 @@ extern "C" * @brief This is the HAL system configuration section */ #define VDD_VALUE ((uint32_t)3300) /*!< Value of VDD in mv */ -#define TICK_INT_PRIORITY ((uint32_t)0x0F) /*!< tick interrupt priority */ +#define TICK_INT_PRIORITY ((uint32_t)0x0E) /*!< tick interrupt priority */ #define USE_RTOS 0 #define USE_SD_TRANSCEIVER 0U /*!< use uSD Transceiver */ diff --git a/src/sys/system.cpp b/src/sys/system.cpp index 6e001cc..acead70 100644 --- a/src/sys/system.cpp +++ b/src/sys/system.cpp @@ -12,6 +12,7 @@ extern "C" { extern void dsy_i2c_global_init(); extern void dsy_spi_global_init(); + extern void dsy_uart_global_init(); } // Jump related stuff @@ -220,6 +221,7 @@ void System::Init(const System::Config& config) dsy_dma_init(); dsy_i2c_global_init(); dsy_spi_global_init(); + dsy_uart_global_init(); // Initialize Caches if(config.use_dcache) diff --git a/src/util/PersistentStorage.h b/src/util/PersistentStorage.h index 7cbc574..9661da4 100644 --- a/src/util/PersistentStorage.h +++ b/src/util/PersistentStorage.h @@ -101,8 +101,8 @@ class PersistentStorage /** Restores the settings stored in the QSPI */ void RestoreDefaults() { - *settings_ = default_settings_; - state_ = State::FACTORY; + settings_ = default_settings_; + state_ = State::FACTORY; StoreSettingsIfChanged(); } diff --git a/src/util/color.h b/src/util/color.h index 8613d72..ef7036b 100644 --- a/src/util/color.h +++ b/src/util/color.h @@ -61,6 +61,14 @@ class Color /** Returns the 0-1 value for Blue */ inline float Blue() const { return blue_; } + /** Returns a scaled color by a float */ + Color operator*(float scale) + { + Color c; + c.Init(red_ * scale, green_ * scale, blue_ * scale); + return c; + } + private: static const float standard_colors[LAST][3]; float red_, green_, blue_; diff --git a/tests/Midi_gtest.cpp b/tests/Midi_gtest.cpp index 1c1998d..28b15a0 100644 --- a/tests/Midi_gtest.cpp +++ b/tests/Midi_gtest.cpp @@ -330,11 +330,11 @@ TEST_F(MidiTest, allNotesOff) { uint8_t msg[] = {(uint8_t)(0x80 + (3 << 4) + chn), 123, 0}; MidiEvent event = ParseAndPop(msg, 3); - AllNotesOffEvent anoEvent = event.AsAllNotesOff(); + AllNotesOffEvent allOnEvent = event.AsAllNotesOff(); EXPECT_EQ((uint8_t)event.type, ChannelMode); EXPECT_EQ((uint8_t)event.cm_type, AllNotesOff); - EXPECT_EQ((uint8_t)anoEvent.channel, chn); + EXPECT_EQ((uint8_t)allOnEvent.channel, chn); } EXPECT_FALSE(midi.HasEvents()); } @@ -573,22 +573,22 @@ TEST_F(MidiTest, runningStatus) //NoteOn with status bit uint8_t msgs[] = {0x90, 0x10, 0x0f}; MidiEvent event = ParseAndPop(msgs, 3); - NoteOnEvent noEvent = event.AsNoteOn(); + NoteOnEvent onEvent = event.AsNoteOn(); EXPECT_EQ(event.type, NoteOn); - EXPECT_EQ(noEvent.channel, 0); - EXPECT_EQ(noEvent.note, 0x10); - EXPECT_EQ(noEvent.velocity, 0x0f); + EXPECT_EQ(onEvent.channel, 0); + EXPECT_EQ(onEvent.note, 0x10); + EXPECT_EQ(onEvent.velocity, 0x0f); //running status for(uint8_t i = 1; i < 20; i++) { msgs[0] = msgs[1] = i; MidiEvent event = ParseAndPop(msgs, 2); - NoteOnEvent noEvent = event.AsNoteOn(); + NoteOnEvent onEvent = event.AsNoteOn(); EXPECT_EQ(event.type, NoteOn); - EXPECT_EQ(noEvent.channel, 0); - EXPECT_EQ(noEvent.note, i); - EXPECT_EQ(noEvent.velocity, i); + EXPECT_EQ(onEvent.channel, 0); + EXPECT_EQ(onEvent.note, i); + EXPECT_EQ(onEvent.velocity, i); } EXPECT_FALSE(midi.HasEvents()); @@ -638,5 +638,149 @@ TEST_F(MidiTest, badData) msgs[1] = i; Parse(msgs, 2); } + EXPECT_FALSE(midi.HasEvents()); +} + +// send some status bytes without data between valid messages with running status +TEST_F(MidiTest, runningStatusAndStatusBytes) +{ + uint8_t noteon = 0x90; + uint8_t running_status[6] = {0x40, 100, 0x40, 0x01, 0x40, 100}; + uint8_t status_bytes[3] = {0x90, 0x91, 0xB3}; + + Parse(¬eon, 1); + for(int i = 0; i < 3; i++) + { + MidiEvent event = ParseAndPop(&running_status[i * 2], 2); + NoteOnEvent onEvent = event.AsNoteOn(); + EXPECT_EQ(event.type, NoteOn); + EXPECT_EQ(onEvent.channel, 0); + EXPECT_EQ(onEvent.note, 0x40); + EXPECT_EQ(onEvent.velocity, running_status[i * 2 + 1]); + } + + Parse(status_bytes, 3); + EXPECT_FALSE(midi.HasEvents()); + + Parse(¬eon, 1); + for(int i = 0; i < 3; i++) + { + MidiEvent event = ParseAndPop(&running_status[i * 2], 2); + NoteOnEvent onEvent = event.AsNoteOn(); + EXPECT_EQ(event.type, NoteOn); + EXPECT_EQ(onEvent.channel, 0); + EXPECT_EQ(onEvent.note, 0x40); + EXPECT_EQ(onEvent.velocity, running_status[i * 2 + 1]); + } + + EXPECT_FALSE(midi.HasEvents()); +} + +// send running status noteons alternating between velocity 100 and 0 +TEST_F(MidiTest, mayoTest) +{ + uint8_t buff[60] + = {145, 48, 100, 48, 0, 48, 100, 48, 0, 48, 100, 48, 0, 48, 100, + 48, 0, 145, 48, 100, 48, 0, 48, 100, 48, 0, 48, 100, 48, 0, + 48, 100, 145, 48, 0, 48, 100, 48, 0, 48, 100, 48, 0, 48, 100, + 48, 0, 145, 48, 100, 48, 0, 48, 100, 48, 0, 48, 100, 48, 0}; + + Parse(buff, 60); + for(int i = 0; i < 14; i++) + { + MidiEvent on = midi.PopEvent(); + MidiEvent off = midi.PopEvent(); + + EXPECT_EQ(on.type, NoteOn); + EXPECT_EQ(off.type, NoteOff); + + NoteOnEvent noOn = on.AsNoteOn(); + NoteOffEvent noOff = off.AsNoteOff(); + + EXPECT_EQ(noOn.channel, 1); + EXPECT_EQ(noOff.channel, 1); + EXPECT_EQ(noOn.note, 48); + EXPECT_EQ(noOff.note, 48); + EXPECT_EQ(noOn.velocity, 100); + EXPECT_EQ(noOff.velocity, 0); + } + + EXPECT_FALSE(midi.HasEvents()); +} + +// send running status messages with one data byte +TEST_F(MidiTest, singleByteRunningStatusTest) +{ + // == Channel Pressure == + uint8_t status = 0xD3; // channel pressure, channel 3 + Parse(&status, 1); + + for(uint8_t i = 0; i < 128; i++) + { + MidiEvent ev = ParseAndPop(&i, 1); + + EXPECT_EQ(ev.type, ChannelPressure); + + ChannelPressureEvent chPressure = ev.AsChannelPressure(); + + EXPECT_EQ(chPressure.channel, 3); + EXPECT_EQ(chPressure.pressure, i); + } + + EXPECT_FALSE(midi.HasEvents()); + + // == Program Change == + status = 0xC3; // program change, channel 3 + Parse(&status, 1); + + for(uint8_t i = 0; i < 128; i++) + { + MidiEvent ev = ParseAndPop(&i, 1); + + EXPECT_EQ(ev.type, ProgramChange); + + ProgramChangeEvent progChange = ev.AsProgramChange(); + + EXPECT_EQ(progChange.channel, 3); + EXPECT_EQ(progChange.program, i); + } + + EXPECT_FALSE(midi.HasEvents()); + + // == MTC Quarter Frame == + status = 0xF1; // quarter frame + Parse(&status, 1); + + for(uint8_t i = 0; i < 128; i++) + { + MidiEvent ev = ParseAndPop(&i, 1); + + EXPECT_EQ(ev.type, SystemCommon); + EXPECT_EQ(ev.sc_type, MTCQuarterFrame); + + MTCQuarterFrameEvent qfEv = ev.AsMTCQuarterFrame(); + + EXPECT_EQ(qfEv.value, i & 0x0F); + EXPECT_EQ(qfEv.message_type, (i & 0x70) >> 4); + } + + EXPECT_FALSE(midi.HasEvents()); + + // == Song Select == + status = 0xF3; // song select + Parse(&status, 1); + + for(uint8_t i = 0; i < 128; i++) + { + MidiEvent ev = ParseAndPop(&i, 1); + + EXPECT_EQ(ev.type, SystemCommon); + EXPECT_EQ(ev.sc_type, SongSelect); + + SongSelectEvent songSel = ev.AsSongSelect(); + + EXPECT_EQ(songSel.song, i); + } + EXPECT_FALSE(midi.HasEvents()); } \ No newline at end of file