diff --git a/CMakeLists.txt b/CMakeLists.txt index 0f1c568..d960b4d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,13 +7,18 @@ cmake_minimum_required(VERSION 3.20.0) find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) + project(app_core) # NORDIC SDK APP START target_sources(app PRIVATE src/main.cpp) # NORDIC SDK APP END +# COMPILER AND LINKER OPTIONS + +# Add project directories zephyr_library_include_directories(.) +add_subdirectory(src) # Add CMSIS library from nrfSDK include_directories(..\\..\\..\\..\\modules\\hal\\cmsis\\CMSIS\\DSP\\Include) diff --git a/nrf5340dk_nrf5340_cpuapp.overlay b/nrf5340dk_nrf5340_cpuapp.overlay new file mode 100644 index 0000000..40bcef6 --- /dev/null +++ b/nrf5340dk_nrf5340_cpuapp.overlay @@ -0,0 +1,30 @@ +// To get started, press Ctrl+Space to bring up the completion menu and view the available nodes. + +// You can also use the buttons in the sidebar to perform actions on nodes. +// Actions currently available include: + +// * Enabling / disabling the node +// * Adding the bus to a bus +// * Removing the node +// * Connecting ADC channels + +// For more help, browse the DeviceTree documentation at https://docs.zephyrproject.org/latest/guides/dts/index.html +// You can also visit the nRF DeviceTree extension documentation at https://nrfconnect.github.io/vscode-nrf-connect/devicetree/nrfdevicetree.html + +&spi0 { + max-frequency = ; + easydma-maxcnt-bits = <16>; +}; + +&pwm0 { + status = "disabled"; +}; + +&sw_pwm { + status = "okay"; + channel-gpios = <&gpio0 4 PWM_POLARITY_INVERTED>; +}; + +&pwm_led0 { + pwms = <&sw_pwm 0 PWM_MSEC(20) PWM_POLARITY_INVERTED>; +}; diff --git a/prj.conf b/prj.conf index 4cd7834..2f84eb1 100644 --- a/prj.conf +++ b/prj.conf @@ -6,17 +6,27 @@ CONFIG_MULTITHREADING=y CONFIG_KERNEL_MEM_POOL=y -CONFIG_NUM_PREEMPT_PRIORITIES=0 +CONFIG_NUM_PREEMPT_PRIORITIES=5 CONFIG_SYS_CLOCK_EXISTS=y +CONFIG_ZERO_LATENCY_IRQS=y CONFIG_ARM_MPU=y +CONFIG_PWM=y CONFIG_I2C=n CONFIG_WATCHDOG=y -CONFIG_GPIO=y CONFIG_SPI=y +CONFIG_SPI_ASYNC=y CONFIG_SERIAL=y CONFIG_FLASH=n -CONFIG_PM=n -CONFIG_BOARD_ENABLE_CPUNET=y + +# Enable log settings +CONFIG_LOG=y +CONFIG_STDOUT_CONSOLE=n +CONFIG_UART_CONSOLE=n +# CONFIG_RTT=y +CONFIG_USE_SEGGER_RTT=y +CONFIG_LOG_BACKEND_RTT=y +CONFIG_CBPRINTF_FP_A_SUPPORT=y +# CONFIG_BOARD_ENABLE_CPUNET=y # Enable CMSIS to be used through nrfSDK CONFIG_FPU=y @@ -25,7 +35,6 @@ CONFIG_CMSIS_DSP=y CONFIG_CMSIS_DSP_TRANSFORM=y CONFIG_CMSIS_DSP_BASICMATH=y CONFIG_CMSIS_DSP_COMPLEXMATH=y -CONFIG_CMSIS_NN=y CONFIG_NEWLIB_LIBC_MIN_REQUIRED_HEAP_SIZE=2048 # Enable hardware peripherals @@ -33,6 +42,7 @@ CONFIG_DMA=y CONFIG_CPP=y # Enable firmware settings +CONFIG_USERSPACE=y CONFIG_DEBUG_THREAD_INFO=y CONFIG_DEBUG_OPTIMIZATIONS=y CONFIG_DYNAMIC_INTERRUPTS=n @@ -42,8 +52,14 @@ CONFIG_THREAD_STACK_INFO=y CONFIG_THREAD_CUSTOM_DATA=n CONFIG_BOOT_BANNER=n CONFIG_BOOT_DELAY=0 -CONFIG_CONSOLE=y -CONFIG_UART_CONSOLE=y CONFIG_STDOUT_CONSOLE=n -CONFIG_PRINTK=y -CONFIG_EARLY_CONSOLE=n \ No newline at end of file +CONFIG_EARLY_CONSOLE=n + +# Enable CPP specific flags +# NOTE: some are deprecated but we need them to avoid linker errors +CONFIG_CPLUSPLUS=y +CONFIG_LIB_CPLUSPLUS=y +CONFIG_NEWLIB_LIBC=y +CONFIG_STD_CPP14=y + +CONFIG_NRFX_SPIM0=y diff --git a/src/AnalogFrontEndWrapper.h b/src/AnalogFrontEndWrapper.h new file mode 100644 index 0000000..40d8d80 --- /dev/null +++ b/src/AnalogFrontEndWrapper.h @@ -0,0 +1,17 @@ +#pragma once + +#include "Data.h" + +class AnalogFrontEndWrapper { + +public: + virtual ~AnalogFrontEndWrapper() = default; + virtual void Initialize() = 0; + // virtual void Configure() = 0; + virtual void Start()=0; + virtual void Wakeup()=0; + virtual void Standby()=0; + virtual void Reset()=0; + virtual void Stop()=0; + virtual void ReadData()=0; +}; \ No newline at end of file diff --git a/src/ArmMatrixWrapper.h b/src/ArmMatrixWrapper.h new file mode 100644 index 0000000..ec9c420 --- /dev/null +++ b/src/ArmMatrixWrapper.h @@ -0,0 +1,355 @@ +#include +#include +#include "arm_math.h" + + + +template +class ArmMatrixWrapper { +private: + static constexpr uint32_t MaxBufferSize = MaxRows * MaxCols; + float32_t data[MaxBufferSize]; + arm_matrix_instance_f32 matrix; + +public: + ArmMatrixWrapper() { + arm_mat_init_f32(&matrix, MaxRows, MaxCols, data); + }; + + // Copy cstor + ArmMatrixWrapper(const ArmMatrixWrapper& other) { + memcpy(data, other.data, MaxBufferSize * sizeof(float32_t)); + arm_mat_init_f32(&matrix, MaxRows, MaxCols, data); + }; + + // Assignment optor + ArmMatrixWrapper& operator=(const ArmMatrixWrapper& other) { + if (this != &other) { + memcpy(data, other.data, MaxBufferSize * sizeof(float32_t)); + arm_mat_init_f32(&matrix, MaxRows, MaxCols, data); + } + return *this; + }; + + // Dstor + ~ArmMatrixWrapper() {}; + + // Getters and Setters + bool set_rows(uint32_t rows) { + if (rows <= MaxRows) { + arm_mat_init_f32(&matrix, rows, matrix.numCols, data); + return true; + } else { + // NOTE: silent failure + return false; + } + }; + + bool set_cols(uint32_t cols) { + if (cols <= MaxCols) { + arm_mat_init_f32(&matrix, matrix.numRows, cols, data); + return true; + } else { + // Handle error: Columns exceeds maximum allowed value + return false; + } + }; + + uint32_t get_rows() const { + return matrix.numRows; + }; + + uint32_t get_cols() const { + return matrix.numCols; + }; + + // Access Functions + float32_t at(uint32_t index) const { + if (index < MaxBufferSize) { + return data[index]; + } else { + // Note: silent error + return 0.0f; + } + }; + + bool set_at(float32_t value, uint32_t index) { + if (index < MaxBufferSize) { + data[index] = value; + return true; + } else { + return false; + } + }; + + float32_t at(uint32_t i, uint32_t j) const { + if (i < matrix.numRows && j < matrix.numCols) { + return data[i * matrix.numCols + j]; + } else { + return 0.0f; + } + }; + + bool set_at(float32_t value, uint32_t i, uint32_t j) { + if (i < matrix.numRows && j < matrix.numCols) { + data[i * matrix.numCols + j] = value; + return true; + } else { + return false; + } + }; + + // Overloaded Operators + ArmMatrixWrapper operator+(const ArmMatrixWrapper& other) const { + ArmMatrixWrapper result; // deepy copy + arm_mat_add_f32(&matrix, &other.matrix, &result.matrix); + return result; + }; + + ArmMatrixWrapper operator-(const ArmMatrixWrapper& other) const { + ArmMatrixWrapper result; + arm_mat_sub_f32(&matrix, &other.matrix, &result.matrix); + return result; + }; + + ArmMatrixWrapper operator*(const ArmMatrixWrapper& other) const { + ArmMatrixWrapper result; + arm_mat_mult_f32(&matrix, &other.matrix, &result.matrix); + return result; + }; + + ArmMatrixWrapper& operator+=(const ArmMatrixWrapper& other) { + arm_mat_add_f32(&matrix, &other.matrix, &matrix); + return *this; + }; + + ArmMatrixWrapper& operator-=(const ArmMatrixWrapper& other) { + arm_mat_sub_f32(&matrix, &other.matrix, &matrix); + return *this; + }; + + ArmMatrixWrapper& operator*=(const ArmMatrixWrapper& other) { + arm_mat_mult_f32(&matrix, &other.matrix, &matrix); + return *this; + }; + + // Matrix Operations + ArmMatrixWrapper transpose() const { + ArmMatrixWrapper result; + arm_mat_trans_f32(&matrix, &result.matrix); + return result; + }; + + bool inverse() { + arm_status status = arm_mat_inverse_f32(&matrix, &matrix); + return (status == ARM_MATH_SUCCESS); + }; + + bool scale(float32_t scaleFactor) { + arm_mat_scale_f32(&matrix, scaleFactor, &matrix); + return true; // Assuming the CMSIS-DSP function always succeeds + }; + + // Utility + void set_to_identity() { + // arm_mat_identity_f32(&matrix); + }; + + void set_to_zero() { + memset(data, 0, MaxBufferSize * sizeof(float32_t)); + matrix.numCols = 0; + matrix.numRows = 0; + }; + + // statistics + float32_t maximum() const { + float32_t maxValue; + uint32_t maxIndex; + arm_max_f32(data, MaxBufferSize, &maxValue, &maxIndex); + return maxValue; + }; + + float32_t minimum() const { + float32_t minValue; + uint32_t minIndex; + arm_min_f32(data, MaxBufferSize, &minValue, &minIndex); + return minValue; + }; + + float32_t mean() const { + float32_t meanValue; + arm_mean_f32(data, MaxBufferSize, &meanValue); + return meanValue; + }; + + float32_t power() const { + float32_t powerValue; + arm_power_f32(data, MaxBufferSize, &powerValue); + return powerValue; + }; + + float32_t rms() const { + float32_t rmsValue; + arm_rms_f32(data, MaxBufferSize, &rmsValue); + return rmsValue; + }; + + float32_t standardDeviation() const { + float32_t stdDevValue; + arm_std_f32(data, MaxBufferSize, &stdDevValue); + return stdDevValue; + }; + + float32_t variance() const { + float32_t varianceValue; + arm_var_f32(data, MaxBufferSize, &varianceValue); + return varianceValue; + }; + + float32_t meanSquareError(const ArmMatrixWrapper& other) const { + float32_t mseValue; + arm_mean_squared_error_f32(data, other.data, MaxBufferSize, &mseValue); + return mseValue; + }; + + // Computes the raw FFT for specific channel given a number of samples + ArmMatrixWrapper rawFFT(uint32_t channel) const { + + // Struct to store the output of the FFT for one channel + ArmMatrixWrapper rawResult; + + // Temporary arrays to store initial data and intermediate FFT values + float32_t inputFFT[matrix.numRows]; + float32_t outputFFT[matrix.numRows]; + + // O indicates FFT and 1 indicates inverse FFT (This flag will not change) + const uint32_t ifftFlag = 0; + + // Initalize the instance with the specified number of rows + arm_rfft_fast_instance_f32 fft_instance; + arm_rfft_fast_init_f32(&fft_instance, matrix.numRows); + + for (uint32_t i = 0; i < matrix.numRows; i++) + { + inputFFT[i] = at(i, channel); + } + + // Compute the FFT and store the outputs of the FFT in the array + arm_rfft_fast_f32(&fft_instance, inputFFT , outputFFT, ifftFlag); + + // The first entry is the DC offset so simply set to 0 + outputFFT[0] = 0; + + // Copy output FFT to result + memcpy(rawResult.data, outputFFT, MaxBufferSize * sizeof(float32_t)); + return rawResult; + }; + + + // Computes the single-sided FFT for a specific channel given a number of samples + ArmMatrixWrapper singleSideFFT(uint32_t channel) const { + + // Struct to store the output of the FFT for one channel + ArmMatrixWrapper FFTResult; + + // Temporary arrays to store initial data and intermediate FFT values + float32_t outputFFTMag[matrix.numRows/2]; + + // Extract the magnitude values because FFT values are complex and have both a magnitude and phase. + // matrix.numRows/2 because we are looking at one-sided spectrum (# of magnitudes we want to look at) + arm_cmplx_mag_f32(rawFFT(channel).data, outputFFTMag, matrix.numRows/2); + + // Copy the output to the result matrix + memcpy(FFTResult.data, outputFFTMag, MaxBufferSize * sizeof(float32_t)); + return FFTResult; + + } + + // Computes the single-sided Power given the single-sided FFT of a channel + ArmMatrixWrapper singleSidePower(uint32_t channel) const { + + // Struct to store the output of the FFT for one channel + ArmMatrixWrapper PowerResult; + + // Temporary arrays to store power FFT values + float32_t powerFFT[matrix.numRows/2]; + + // Compute the power of the FFT + arm_cmplx_mag_squared_f32(rawFFT(channel).data, powerFFT, matrix.numRows/2); + + // Check if the scaling is required for the above function + // Copy the output to the result matrix + memcpy(PowerResult.data, powerFFT, MaxBufferSize * sizeof(float32_t)); + return PowerResult; + + } + + // Computes the single-sided Band Power given the single-sided FFT of a channel, + // Pwelch of specified channel and specific band power range type + float32_t singleSideBandPower(uint32_t channel, uint32_t sampleFreq, uint32_t bandSelect, const ArmMatrixWrapper& PWelch,) const { + + // The frequency resolution or frequency bin width + float32_t freqRes = sampleFreq / MaxRows; + + // The following band limits: delta band, theta band, alpha band, beta band + float32_t bandRanges[] = {1,3,4,7,8,12,13,30}; + + // Low and high limit for specified band range of frequencies + uint32_t lowLimit = 0; + uint32_t highLimit = 0; + + switch(bandSelect){ + + // delta band (Note: make an enum for the different band types) + case 0:{ + lowLimit = (uint32_t)(floor(bandRanges[0] / freqRes)); + highLimit = (uint32_t)(ceil(bandRanges[1] / freqRes)); + break; + } + // theta band + case 1:{ + lowLimit = (uint32_t)(floor(bandRanges[2] / freqRes)); + highLimit = (uint32_t)(ceil(bandRanges[3] / freqRes)); + break; + } + // alpha band + case 2:{ + lowLimit = (uint32_t)(floor(bandRanges[4] / freqRes)); + highLimit = (uint32_t)(ceil(bandRanges[5] / freqRes)); + break; + } + // beta band + case 3:{ + lowLimit = (uint32_t)(floor(bandRanges[6] / freqRes)); + highLimit = (uint32_t)(ceil(bandRanges[7] / freqRes)); + break; + } + default:{ + break; + } + + } + + // Calculating the band power for specified band range of frequnencies + float32_t bandPower = 0; + for(uint32_t i = lowLimit; i < highLimit; lowLimit++){ + bandPower += (PWelch.data[i] * freqRes); + } + return bandPower; + } + + // Computes the single-sided Relative Band Power given the specified channel and + // specified band power + float32_t singleSideRelativeBandPower(uint32_t channel, float32_t bandPower){ + + // Calculate summation of total power + float32_t totalPower = 0; + for(uint32_t i = 0; i < MaxRows/2; i++){ + totalPower += singleSidePower(channel).data[i]; + } + // Calculate relative band power relative to the total power + float32_t relativeBandpower = bandPower / totalPower; + return relativeBandpower; + } + +}; \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..1f7adea --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,7 @@ +# Include source files in the src directory +file(GLOB_RECURSE APP_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp") +target_sources(app PRIVATE ${APP_SOURCES}) + +# Add the core directory +add_subdirectory(core) +add_subdirectory(drivers) diff --git a/src/CustomFrontEndWrapper.cpp b/src/CustomFrontEndWrapper.cpp new file mode 100644 index 0000000..bb0e977 --- /dev/null +++ b/src/CustomFrontEndWrapper.cpp @@ -0,0 +1,33 @@ +#include "CustomFrontEndWrapper.h" + +void CustomFrontEndWrapper::Initialize() { + // TODO +} + +void CustomFrontEndWrapper::Configure() { + // TODO +} + +void CustomFrontEndWrapper::Start() { + // TODO +} + +void CustomFrontEndWrapper::Wakeup() { + // TODO +} + +void CustomFrontEndWrapper::Standby() { + // TODO +} + +void CustomFrontEndWrapper::Reset() { + // TODO +} + +void CustomFrontEndWrapper::Stop() { + // TODO +} + +void CustomFrontEndWrapper::ReadData() { + // TODO +} \ No newline at end of file diff --git a/src/CustomFrontEndWrapper.h b/src/CustomFrontEndWrapper.h new file mode 100644 index 0000000..ea1475e --- /dev/null +++ b/src/CustomFrontEndWrapper.h @@ -0,0 +1,15 @@ +#pragma once + +#include "AnalogFrontEndWrapper.h" + +class CustomFrontEndWrapper : public AnalogFrontEndWrapper { +public: + void Initialize() override; + void Configure(); + void Start() override; + void Wakeup() override; + void Standby() override; + void Reset() override; + void Stop() override; + void ReadData() override; +}; \ No newline at end of file diff --git a/src/Data.h b/src/Data.h new file mode 100644 index 0000000..40bb926 --- /dev/null +++ b/src/Data.h @@ -0,0 +1,12 @@ +#pragma once +// #include +#include + +constexpr uint8_t max_electrodes = 6; +constexpr uint8_t max_sample_rate_hz = 250; +constexpr uint32_t max_samples = max_electrodes * max_sample_rate_hz * 30; + +struct eeg_sample { + uint8_t buf[3] = { 0 }; // for max 24 bit adc readings TODO: add units + uint64_t timestamp_ms = 0; +}; diff --git a/src/DataAcquisitionThread.cpp b/src/DataAcquisitionThread.cpp new file mode 100644 index 0000000..227f1ef --- /dev/null +++ b/src/DataAcquisitionThread.cpp @@ -0,0 +1,51 @@ +#include "DataAcquisitionThread.h" + +LOG_MODULE_REGISTER(eegals_app_core_daq, LOG_LEVEL_DBG); +K_THREAD_STACK_DEFINE(data_acq_stack_area, DATA_ACQ_THREAD_STACK_SIZE_B); +struct k_thread data_acq_thread_data; + +DataAcquisitionThread::DataAcquisitionThread() { + AFEWrapper = TIFrontEndWrapper(); + // set up data manager to listen to AFE + // DataBufferManager::spi_dma_setup(spi_dev); +} + +void DataAcquisitionThread::Initialize() { + LOG_DBG("DataAcq::%s -- initializing AFE Wrapper", __FUNCTION__); + AFEWrapper.Initialize(); + + if (id == nullptr) { + LOG_DBG("DataAcq::%s -- making thread", __FUNCTION__); + id = k_thread_create( + &data_acq_thread_data, data_acq_stack_area, + K_THREAD_STACK_SIZEOF(data_acq_stack_area), + DataAcquisitionThread::RunThreadSequence, + this, NULL, NULL, + DATA_ACQ_THREAD_PRIORITY, 0, K_NO_WAIT + ); + + k_thread_name_set(id, "DataAcquisitionThread"); + LOG_DBG("DataAcq::%s -- thread create successful", __FUNCTION__); + } +} + +void DataAcquisitionThread::Run() { + // set AFE in continuous read mode + uint8_t message = 0; + while (true) { + if (message_queue.get_with_blocking_wait(message)) { + LOG_DBG("DataAcq::%s -- received message at %u ms", __FUNCTION__, k_uptime_get_32()); + switch (static_cast(message)) { + case STOP_READING_AFE: + // AFEWrapper.Stop(); + break; + case START_READING_AFE: + // AFEWrapper.Start(); + break; + case INVALID: + default: + break; + } + } + } +} diff --git a/src/DataAcquisitionThread.h b/src/DataAcquisitionThread.h new file mode 100644 index 0000000..f94ab91 --- /dev/null +++ b/src/DataAcquisitionThread.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include "core/Thread.h" +#include "TIFrontEndWrapper.h" + +#define DATA_ACQ_THREAD_STACK_SIZE_B 4096 +#define DATA_ACQ_THREAD_PRIORITY 4 // max based on prj config +#define DATA_ACQ_THREAD_MSG_Q_DEPTH 10 + +class DataAcquisitionThread : public Thread { +private: + TIFrontEndWrapper AFEWrapper; + DataAcquisitionThread(); + ~DataAcquisitionThread() = default; + + // need to delete copy cstor and assign optor + DataAcquisitionThread(const DataAcquisitionThread &) = delete; + DataAcquisitionThread& operator=(const DataAcquisitionThread&) = delete; + +public: + enum DataAcquisitionThreadMessage : uint8_t { + STOP_READING_AFE = 0, + START_READING_AFE, + INVALID, + }; + + void Initialize() override; + void Run() override; + + static DataAcquisitionThread& GetInstance() { + // NOTE: this method of using static local variable is thread-safe + // NOTE: this has to be implemented in the child class + static DataAcquisitionThread instance; + return instance; + } +}; \ No newline at end of file diff --git a/src/DataBufferManager.cpp b/src/DataBufferManager.cpp new file mode 100644 index 0000000..db0934b --- /dev/null +++ b/src/DataBufferManager.cpp @@ -0,0 +1,60 @@ +#include "DataBufferManager.h" +// log level declaration +LOG_MODULE_REGISTER(data_buffer_manager, LOG_LEVEL_DBG); + +struct dma_config DataBufferManager::dma_cfg = { + .channel_direction = PERIPHERAL_TO_MEMORY, + .complete_callback_en = 1, + .source_burst_length = 1, // accept 1 sample at a time + .dma_callback = DMATransfer, +}; + +// struct Semaphore DataBufferManager::eeg_buffer_semaphore; +// eeg_sample DataBufferManager::dma_buffer[max_samples]; +// size_t DataBufferManager::buffer_index; + +void DataBufferManager::Write() { + +} + +void DataBufferManager::Read() { + +} + +void DataBufferManager::DMASetup(device* spi_dev) { + // set up basic source buffer for dma + dma_block_cfg.dest_address = (uintptr_t)source_buffer; + dma_block_cfg.source_address = (uintptr_t)&spi_dev->data; + + if (dma_config(dma_dev, AFE_DMA_CHANNEL, &dma_cfg)) { + LOG_ERR("Failed to configure DMA"); + // TODO: error handling :D + return; + } + + if (dma_start(dma_dev, AFE_DMA_CHANNEL)) { + LOG_ERR("Failed to start DMA transfer"); + // TODO: error handling + } +} + +void DataBufferManager::DMATransfer(const struct device *dev, void *user_data, uint32_t channel, int status) { + if (status == 0) { + LOG_DBG("DMA transfer complete success"); + for (size_t i = 0; i < AFE_DMA_BLOCK_SIZE; i++) { + LOG_INF("Sample[%u]: %d", i, source_buffer[i]); + } + ResetBuffer(); + // Perform any post-transfer processing or signal completion + + } else { + LOG_ERR("DMA transfer failed with status %d", status); + // TODO: error handling + } + + // in either case, start the DMA again + if (dma_start(dma_dev, AFE_DMA_CHANNEL)) { + LOG_ERR("Failed to start DMA transfer"); + // TODO: error handling + } +} diff --git a/src/DataBufferManager.h b/src/DataBufferManager.h new file mode 100644 index 0000000..2b2fef5 --- /dev/null +++ b/src/DataBufferManager.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "Data.h" +#include "core/Semaphore.h" + +#define AFE_DMA_CHANNEL 0 +#define AFE_DMA_BURST_NUM_SAMPLES 5 +#define AFE_DMA_BLOCK_SIZE 256 + +class DataBufferManager { +// NOTE: needs to be fully static, only one instance should exist +private: + static struct device* dma_dev; + static struct dma_config dma_cfg; + static struct dma_block_config dma_block_cfg; + // note: each afe sample is 15 bytes, burst size is 5, so max usage is 15 * 5 = 75 + static uint8_t source_buffer[AFE_DMA_BLOCK_SIZE]; + + //// TODO: use this instead + // static Semaphore eeg_buffer_semaphore; + // static eeg_sample dma_buffer[max_samples]; + // static size_t buffer_index; + +public: + static void Read(); + static void Write(); + static void DMASetup(device* spi_dev); + static void DMATransfer(const struct device *dev, void *user_data, uint32_t channel, int status); + + // inline function to be used in callback + static void ResetBuffer() { + memset(source_buffer, 0, sizeof(source_buffer)); + }; +}; \ No newline at end of file diff --git a/src/TIFrontEndWrapper.cpp b/src/TIFrontEndWrapper.cpp new file mode 100644 index 0000000..28699e1 --- /dev/null +++ b/src/TIFrontEndWrapper.cpp @@ -0,0 +1,48 @@ +#include "TIFrontEndWrapper.h" +// log level declaration +LOG_MODULE_REGISTER(TI_front_end_wrapper, LOG_LEVEL_DBG); + +TIFrontEndWrapper::TIFrontEndWrapper() : + afe_spi_device(const_cast(DEVICE_DT_GET(AFE_SPI))), + afe_spi_config{ADS1299_SPI_FREQUENCY_HZ, (SPI_OP_MODE_MASTER | SPI_TRANSFER_MSB | SPI_WORD_SET(8)), 0}, + afe_driver(afe_spi_device, &afe_spi_config) +{ + if (!device_is_ready(afe_spi_device)) { + LOG_ERR("TIFE::%s -- SPI device not ready!", __FUNCTION__); + // todo: error handling + } +} + +void TIFrontEndWrapper::Initialize() { + // set up AFE + afe_driver.ads1299_powerup_reset(); + afe_driver.ads1299_check_id(); + afe_driver.ads1299_init_regs(); +} + +void TIFrontEndWrapper::Start() { + afe_driver.ads1299_start_rdatac(); +} + +void TIFrontEndWrapper::Wakeup() { + afe_driver.ads1299_wake(); +} + +void TIFrontEndWrapper::Standby() { + afe_driver.ads1299_standby(); +} + +void TIFrontEndWrapper::Reset() { + afe_driver.ads1299_powerup_reset(); +} + +void TIFrontEndWrapper::Stop() { + afe_driver.ads1299_stop_rdatac(); +} + +void TIFrontEndWrapper::ReadData() { + int32_t channel_1, channel_2, channel_3, channel_4; + + afe_driver.get_eeg_voltage_samples(&channel_1, &channel_2, &channel_3, &channel_4); + LOG_DBG("TIFE::%s -- Read samples: %d, %d, %d, %d", __FUNCTION__, channel_1, channel_2, channel_3, channel_4); +} \ No newline at end of file diff --git a/src/TIFrontEndWrapper.h b/src/TIFrontEndWrapper.h new file mode 100644 index 0000000..9db7eea --- /dev/null +++ b/src/TIFrontEndWrapper.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include + +#include "drivers/ads1299-x.h" +#include "AnalogFrontEndWrapper.h" +#include "DataBufferManager.h" + +#define AFE_SPI DT_NODELABEL(spi_afe) + +class TIFrontEndWrapper : public AnalogFrontEndWrapper { +private: + device* afe_spi_device; + spi_config afe_spi_config; + ADS1299Driver afe_driver; + +public: + TIFrontEndWrapper(); + ~TIFrontEndWrapper() = default; + void Initialize() override; + void Start() override; + void Wakeup() override; + void Standby() override; + void Reset() override; + void Stop() override; + void ReadData() override; +}; diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt new file mode 100644 index 0000000..f2cc183 --- /dev/null +++ b/src/core/CMakeLists.txt @@ -0,0 +1,5 @@ +# Include all source files in the src/core directory +file(GLOB_RECURSE CORE_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp") + +# Add the sources to the app target +target_sources(app PRIVATE ${CORE_SOURCES}) diff --git a/src/core/MessageQueue.h b/src/core/MessageQueue.h new file mode 100644 index 0000000..8c180f4 --- /dev/null +++ b/src/core/MessageQueue.h @@ -0,0 +1,73 @@ +#pragma once + +#include + +template class MessageQueue { +private: + size_t msg_size_B; + size_t queue_depth_B; + char message_buffer[depth * sizeof(element_type)]; + struct k_msgq queue; +public: + MessageQueue() { + msg_size_B = sizeof(element_type); + queue_depth_B = msg_size_B * depth; + k_msgq_init(&queue, message_buffer, depth, msg_size_B); + }; + ~MessageQueue() = default; + + bool push(element_type& msg) { + if (k_msgq_put(&queue, static_cast(&msg), K_NO_WAIT) != 0) { + // TODO: uart debug message send failed + return false; + } + return true; + }; + + bool push_with_timeout(element_type& msg, int64_t ticks) { + if (k_msgq_put(&queue, static_cast(&msg), k_timeout_t(ticks)) != 0) { + return false; + } + return true; + }; + + bool get(element_type& msg) { + // NOTE: this MUST be used if accessing queue from ISR + if (k_msgq_get(&queue, static_cast(&msg), K_NO_WAIT) != 0) { + return false; + } + return true; + }; + + bool get_with_timeout(element_type& msg, int64_t ticks) { + if (k_msgq_get(&queue, static_cast(&msg), k_timeout_t(ticks)) != 0) { + return false; + } + return true; + }; + + bool get_with_blocking_wait(element_type& msg) { + if (k_msgq_get(&queue, static_cast(&msg), K_FOREVER) != 0) { + return false; + } + return true; + } + + bool peak(element_type& msg) { + if (k_msgq_peek(&queue, static_cast(&msg)) != 0) { + return false; + } + return true; + }; + + bool peak_at(element_type& msg, uint32_t idx) { + if (k_msgq_peek_at(&queue, static_cast(&msg), idx) != 0) { + return false; + } + return true; + }; + + void resetMessageQueue() { + k_msgq_purge(&queue); + }; +}; \ No newline at end of file diff --git a/src/core/Semaphore.cpp b/src/core/Semaphore.cpp new file mode 100644 index 0000000..1bdd0de --- /dev/null +++ b/src/core/Semaphore.cpp @@ -0,0 +1,50 @@ +#include "Semaphore.h" + +/* + * @brief Semaphore basic constructor + */ +Semaphore::Semaphore() +{ + printk("Create Semaphore %p\n", this); + k_sem_init(&_sema_internal, 0, SEMAPHORE_MAX_TAKE); +} + +/* + * @brief wait for a Semaphore + * + * Test a Semaphore to see if it has been signaled. If the signal + * count is greater than zero, it is decremented. + * + * @return 1 when Semaphore is available + */ +int Semaphore::wait(void) +{ + k_sem_take(&_sema_internal, K_FOREVER); + return 1; +} + +/* + * @brief wait for a Semaphore within a specified timeout + * + * Test a Semaphore to see if it has been signaled. If the signal + * count is greater than zero, it is decremented. The function + * waits for timeout specified + * + * @param timeout the specified timeout in ticks + * + * @return 1 if Semaphore is available, 0 if timed out + */ +int Semaphore::wait(int timeout) +{ + return k_sem_take(&_sema_internal, K_MSEC(timeout)); +} + +/** + * @brief Signal a Semaphore + * + * This routine signals the specified Semaphore. + */ +void Semaphore::give(void) +{ + k_sem_give(&_sema_internal); +} \ No newline at end of file diff --git a/src/core/Semaphore.h b/src/core/Semaphore.h new file mode 100644 index 0000000..8562090 --- /dev/null +++ b/src/core/Semaphore.h @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2015-2016 Wind River Systems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include +#include +#include + +/* specify delay between greetings (in ms); compute equivalent in ticks */ +#define SLEEPTIME 500 +#define SEMAPHORE_MAX_TAKE 1 +/* + * @class cpp_semaphore + * @brief Semaphore + * + * Class derives from the pure virtual semaphore class and + * implements it's methods for the semaphore + */ +class Semaphore { +protected: + struct k_sem _sema_internal; +public: + Semaphore(); + ~Semaphore() {} + int wait(void); + int wait(int timeout); + void give(void); +}; \ No newline at end of file diff --git a/src/core/Thread.h b/src/core/Thread.h new file mode 100644 index 0000000..f927bf9 --- /dev/null +++ b/src/core/Thread.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include "MessageQueue.h" + +template class Thread { +protected: + k_tid_t id; + MessageQueue message_queue; + +public: + Thread() : id(nullptr) {}; + virtual ~Thread() {}; + + virtual void Initialize() = 0; + virtual void Run () = 0; + + bool SendMessage(uint8_t msg) { + if (message_queue.push(msg) == false) { + // TODO: uart debug message send failed + return false; + } + return true; + }; + + // This is a static function required by k_thread_create + // NOTE: can't have a virtual static function, need this to be defined for all + static void RunThreadSequence(void* instance, void*, void*) { + // HAVE to receive an instance ptr to run the correct thread sequence + // because static functions are not associated with thread objects + static_cast(instance)->Run(); + }; + + //// NOTE: MAY need to add killing/re-starting threads + // void Start(); + // void Kill(); +}; \ No newline at end of file diff --git a/src/drivers/CMakeLists.txt b/src/drivers/CMakeLists.txt new file mode 100644 index 0000000..39d3f19 --- /dev/null +++ b/src/drivers/CMakeLists.txt @@ -0,0 +1,7 @@ +# Include all source files in the src/drivers directory +file(GLOB_RECURSE DRIVER_CPP_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp") +file(GLOB_RECURSE DRIVER_C_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.c") + +# Add the sources to the app target +target_sources(app PRIVATE ${DRIVER_CPP_SOURCES}) +target_sources(app PRIVATE ${DRIVER_C_SOURCES}) diff --git a/src/drivers/ads1299-x.cpp b/src/drivers/ads1299-x.cpp new file mode 100644 index 0000000..54dc6ba --- /dev/null +++ b/src/drivers/ads1299-x.cpp @@ -0,0 +1,298 @@ +/* Copyright (c) 2016 Musa Mahmood + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "ads1299-x.h" +#include +#include +// #include "compiler_abstraction.h" +// #include "nrf.h" + +LOG_MODULE_REGISTER(ads1299_driver, LOG_LEVEL_DBG); + +/**@TX,RX Stuff: */ +#define TX_RX_MSG_LENGTH 7 + +/**@DEBUG STUFF */ +#define BYTE_TO_BINARY_PATTERN "%c%c%c%c%c%c%c%c" +#define BYTE_TO_BINARY(byte) \ + (byte & 0x80 ? '1' : '0'), \ + (byte & 0x40 ? '1' : '0'), \ + (byte & 0x20 ? '1' : '0'), \ + (byte & 0x10 ? '1' : '0'), \ + (byte & 0x08 ? '1' : '0'), \ + (byte & 0x04 ? '1' : '0'), \ + (byte & 0x02 ? '1' : '0'), \ + (byte & 0x01 ? '1' : '0') +#define BYTE_TO_BINARY_PATTERN_16BIT "%c%c%c%c %c%c%c%c %c%c%c%c %c%c%c%c\r\n" +#define BYTE_TO_BINARY_16BIT(byte) \ + (byte & 0x8000 ? '1' : '0'), \ + (byte & 0x4000 ? '1' : '0'), \ + (byte & 0x2000 ? '1' : '0'), \ + (byte & 0x1000 ? '1' : '0'), \ + (byte & 0x800 ? '1' : '0'), \ + (byte & 0x400 ? '1' : '0'), \ + (byte & 0x200 ? '1' : '0'), \ + (byte & 0x100 ? '1' : '0'), \ + (byte & 0x80 ? '1' : '0'), \ + (byte & 0x40 ? '1' : '0'), \ + (byte & 0x20 ? '1' : '0'), \ + (byte & 0x10 ? '1' : '0'), \ + (byte & 0x08 ? '1' : '0'), \ + (byte & 0x04 ? '1' : '0'), \ + (byte & 0x02 ? '1' : '0'), \ + (byte & 0x01 ? '1' : '0') + +/** +8 \Note: ADS1299 Default Registers +*/ + +uint8_t ads1299_default_registers[] = { + ADS1299_REGDEFAULT_CONFIG1, + ADS1299_REGDEFAULT_CONFIG2, + ADS1299_REGDEFAULT_CONFIG3, + ADS1299_REGDEFAULT_LOFF, + ADS1299_REGDEFAULT_CH1SET, + ADS1299_REGDEFAULT_CH2SET, + ADS1299_REGDEFAULT_CH3SET, + ADS1299_REGDEFAULT_CH4SET, + ADS1299_REGDEFAULT_CH5SET, + ADS1299_REGDEFAULT_CH6SET, + ADS1299_REGDEFAULT_CH7SET, + ADS1299_REGDEFAULT_CH8SET, + ADS1299_REGDEFAULT_BIAS_SENSP, + ADS1299_REGDEFAULT_BIAS_SENSN, + ADS1299_REGDEFAULT_LOFF_SENSP, + ADS1299_REGDEFAULT_LOFF_SENSN, + ADS1299_REGDEFAULT_LOFF_FLIP, + ADS1299_REGDEFAULT_LOFF_STATP, + ADS1299_REGDEFAULT_LOFF_STATN, + ADS1299_REGDEFAULT_GPIO, + ADS1299_REGDEFAULT_MISC1, + ADS1299_REGDEFAULT_MISC2, + ADS1299_REGDEFAULT_CONFIG4 +}; + +/* + * SPI Transceive Wrapper: + */ +void ADS1299Driver::ads1299_spi_transfer(uint8_t* tx_data, size_t tx_len, uint8_t* rx_data, size_t rx_len) { + // current behaviour: function will continue to transmit despite errors in tx/rx buf + struct spi_buf tx_buf = { .buf = NULL, .len = 0 }; + struct spi_buf rx_buf = { .buf = NULL, .len = 0 }; + + struct spi_buf_set tx_buf_set = { .buffers = NULL, .count = 0,}; + struct spi_buf_set rx_buf_set = { .buffers = NULL, .count = 0,}; + + if (tx_data != NULL && tx_len > 0) { + tx_buf.buf = tx_data; + tx_buf.len = tx_len; + tx_buf_set.buffers = &tx_buf; + } + if (rx_data != NULL && rx_len > 0) { + rx_buf.buf = rx_data; + rx_buf.len = rx_len; + rx_buf_set.buffers = &rx_buf; + } + + int ret = spi_transceive(spi_dev, spi_cfg, &tx_buf_set, &rx_buf_set); + if (ret) { + LOG_ERR("SPI transfer failed with error %d", ret); + return; + } +}; + +/* + * ADS1299 CONTROL FUNCTIONS: + */ +void ADS1299Driver::ads1299_init_regs(void) { + uint8_t num_registers = 23; + uint8_t txrx_size = num_registers + 2; + uint8_t wreg_init_opcode = 0x41; + uint8_t tx_data_spi[txrx_size] = { 0 }; + uint8_t rx_data_spi[txrx_size] = { 0 }; + + tx_data_spi[0] = wreg_init_opcode; + tx_data_spi[1] = num_registers - 1; + + for (int j = 0; j < num_registers; ++j) { + tx_data_spi[j + 2] = ads1299_default_registers[j]; + } + + ads1299_spi_transfer(tx_data_spi, txrx_size, rx_data_spi, txrx_size); + k_msleep(150); + LOG_INF("Power-on reset and initialization procedure.."); +}; + +void ADS1299Driver::ads1299_powerup_reset(void) +{ + #if defined(BOARD_PCA10028) | defined(BOARD_NRF_BREAKOUT) + nrf_gpio_pin_clear(ADS1299_PWDN_RST_PIN); + #endif + #if defined(BOARD_FULL_EEG_V1) + nrf_gpio_pin_clear(ADS1299_RESET_PIN); + nrf_gpio_pin_clear(ADS1299_PWDN_PIN); + #endif + k_msleep(50); + LOG_DBG(" ADS1299-x POWERED UP AND RESET..\r\n"); +}; + +void ADS1299Driver::ads1299_powerdn(void) +{ + #if defined(BOARD_PCA10028) | defined(BOARD_NRF_BREAKOUT) + nrf_gpio_pin_clear(ADS1299_PWDN_RST_PIN); + #endif + #if defined(BOARD_FULL_EEG_V1) + nrf_gpio_pin_clear(ADS1299_RESET_PIN); + nrf_gpio_pin_clear(ADS1299_PWDN_PIN); + #endif + k_msleep(20); + LOG_DBG(" ADS1299-x POWERED DOWN..\r\n"); +}; + +void ADS1299Driver::ads1299_powerup(void) +{ + #if defined(BOARD_PCA10028) | defined(BOARD_NRF_BREAKOUT) + nrf_gpio_pin_set(ADS1299_PWDN_RST_PIN); + #endif + #if defined(BOARD_FULL_EEG_V1) + nrf_gpio_pin_set(ADS1299_RESET_PIN); + nrf_gpio_pin_set(ADS1299_PWDN_PIN); + #endif + k_msleep(1000); // Allow time for power-on reset + LOG_DBG(" ADS1299-x POWERED UP...\r\n"); +}; + +void ADS1299Driver::ads1299_standby(void) { + uint8_t tx_data_spi = ADS1299_OPC_STANDBY; + uint8_t rx_data_spi; + + ads1299_spi_transfer(&tx_data_spi, 1, &rx_data_spi, 1); + LOG_DBG("ADS1299-x placed in standby mode..."); +}; + +void ADS1299Driver::ads1299_wake(void) { + uint8_t tx_data_spi = ADS1299_OPC_WAKEUP; + uint8_t rx_data_spi; + + ads1299_spi_transfer(&tx_data_spi, 1, &rx_data_spi, 1); + k_msleep(10); // Allow time to wake up - 10ms + LOG_DBG(" ADS1299-x Wakeup.."); +}; + +void ADS1299Driver::ads1299_soft_start_conversion(void) { + uint8_t tx_data_spi = ADS1299_OPC_START; + uint8_t rx_data_spi; + + ads1299_spi_transfer(&tx_data_spi, 1, &rx_data_spi, 1); + LOG_DBG(" Start ADC conversion.."); +}; + +void ADS1299Driver::ads1299_stop_rdatac(void) { + uint8_t tx_data_spi = ADS1299_OPC_SDATAC; + uint8_t rx_data_spi; + + ads1299_spi_transfer(&tx_data_spi, 1, &rx_data_spi, 1); + LOG_DBG(" Continuous Data Output Disabled.."); +}; + +void ADS1299Driver::ads1299_start_rdatac(void) { + uint8_t tx_data_spi = ADS1299_OPC_RDATAC; + uint8_t rx_data_spi; + + ads1299_spi_transfer(&tx_data_spi, 1, &rx_data_spi, 1); + LOG_DBG(" Continuous Data Output Enabled.."); +}; + +void ADS1299Driver::ads1299_check_id(void) { + uint8_t device_id_reg_value; + uint8_t tx_data_spi[3]; + uint8_t rx_data_spi[7]; + tx_data_spi[0] = 0x20; //Request Device ID + tx_data_spi[1] = 0x01; //Intend to read 1 byte + tx_data_spi[2] = 0x00; //This will be replaced by Reg Data + + ads1299_spi_transfer(tx_data_spi, 2+tx_data_spi[1], rx_data_spi, 2+tx_data_spi[1]); + + k_msleep(20); //Wait for response: + device_id_reg_value = rx_data_spi[2]; + bool is_ads_1299_4 = (device_id_reg_value & 0x1F) == (ADS1299_4_DEVICE_ID); + bool is_ads_1299_6 = (device_id_reg_value & 0x1F) == (ADS1299_6_DEVICE_ID); + bool is_ads_1299 = (device_id_reg_value & 0x1F) == (ADS1299_DEVICE_ID); + + uint8_t revisionVersion = (device_id_reg_value & 0xE0)>>5; + if (is_ads_1299||is_ads_1299_6||is_ads_1299_4) { + LOG_DBG("Device Address Matches!"); + } else { + LOG_DBG("********SPI I/O Error, Device Not Detected! ***********"); + LOG_DBG("SPI Transfer Dump:"); + LOG_DBG("ID[b0->2]: [0x%x | 0x%x | 0x%x]", rx_data_spi[0],rx_data_spi[1],rx_data_spi[2]); + LOG_DBG("ID[b3->6]: [0x%x | 0x%x | 0x%x | 0x%x]", rx_data_spi[3],rx_data_spi[4],rx_data_spi[5],rx_data_spi[6]); + } + if (is_ads_1299) { + LOG_DBG("Device Name: ADS1299"); + } else if (is_ads_1299_6) { + LOG_DBG("Device Name: ADS1299-6"); + } else if (is_ads_1299_4) { + LOG_DBG("Device Name: ADS1299-4"); + } + if (is_ads_1299||is_ads_1299_6||is_ads_1299_4) { + LOG_DBG("Device Revision #%d",revisionVersion); + LOG_DBG("Device ID: 0x%x",device_id_reg_value); + } +}; + +/* DATA RETRIEVAL FUNCTIONS **********************************************************************************************************************/ + +/**@brief Function for acquiring a EEG Voltage Measurement samples. + * + * @details Uses SPI + * + */ +void ADS1299Driver::get_eeg_voltage_samples(int32_t *eeg1, int32_t *eeg2, int32_t *eeg3, int32_t *eeg4) { + uint8_t tx_rx_data[15] = { + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00 + }; + + ads1299_spi_transfer(tx_rx_data, 15, tx_rx_data, 15); + + uint8_t cnt = 0; + do { + if(tx_rx_data[0]==0xC0){ + *eeg1 = ( (tx_rx_data[3] << 16) | (tx_rx_data[4] << 8) | (tx_rx_data[5]) ); + *eeg2 = ( (tx_rx_data[6] << 16) | (tx_rx_data[7] << 8) | (tx_rx_data[8]) ); + *eeg3 = ( (tx_rx_data[9] << 16) | (tx_rx_data[10] << 8) | (tx_rx_data[11]) ); + *eeg4 = ( (tx_rx_data[12] << 16) | (tx_rx_data[13] << 8) | (tx_rx_data[14]) ); + break; + } + cnt++; + k_msleep(1); + } while(cnt<255); + //LOG_DBG("B0-2 = [0x%x 0x%x 0x%x | cnt=%d]\r\n",tx_rx_data[0],tx_rx_data[1],tx_rx_data[2],cnt); + //LOG_DBG("DATA:[0x%x 0x%x 0x%x 0x%x]\r\n",*eeg1,*eeg2,*eeg3,*eeg4); +}; + +// // // // // // +// End of File // +// // // // // // diff --git a/src/drivers/ads1299-x.h b/src/drivers/ads1299-x.h new file mode 100644 index 0000000..135c08a --- /dev/null +++ b/src/drivers/ads1299-x.h @@ -0,0 +1,211 @@ +/* Copyright (c) 2017 Musa Mahmood + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** @file + * + * @brief Functions for initializing and controlling Texas Instruments ADS1299 analog front-end. + */ + +#ifndef ADS1299_H__ +#define ADS1299_H__ + +#include +#include +#include +#include + +/**@SPI STUFF*/ +#define BOARD_NRF53_DEV +#ifdef BOARD_NRF53_DEV + #define ADS1299_SPI_SCLK_PIN DT_GPIO_PIN(DT_ALIAS(spi0_sck), gpios) + #define ADS1299_SPI_CS_PIN DT_GPIO_PIN(DT_ALIAS(spi0_cs), gpios) + #define ADS1299_SPI_MOSI_PIN DT_GPIO_PIN(DT_ALIAS(spi0_mosi), gpios) + #define ADS1299_SPI_MISO_PIN DT_GPIO_PIN(DT_ALIAS(spi0_miso), gpios) + #define ADS1299_SPI_FREQUENCY_HZ 1000000 +#elif defined(BOARD_PCA10028) + #define ADS1299_SPI_SCLK_PIN 10 + #define ADS1299_SPI_CS_PIN 11 + #define ADS1299_SPI_MOSI_PIN 14 //MASTER (nRF) OUT; SLAVE (ADS) DIN + #define ADS1299_SPI_MISO_PIN 9 //MASTER (nRF) IN ; SLAVE (ADS) DOUT + #define ADS1299_PWDN_RST_PIN 12 + #define ADS1299_DRDY_PIN 8 +#elif defined(BOARD_NRF_BREAKOUT) + //BOARD_CUSTOM BOARD_NRF_BREAKOUT + #define ADS1299_SPI_SCLK_PIN 9 + #define ADS1299_SPI_CS_PIN 10 + #define ADS1299_SPI_MOSI_PIN 12 //MASTER (nRF) OUT; SLAVE (ADS) DIN + #define ADS1299_SPI_MISO_PIN 8 //MASTER (nRF) IN ; SLAVE (ADS) DOUT + #define ADS1299_PWDN_RST_PIN 11 + #define ADS1299_DRDY_PIN 15 +#elif defined(BOARD_FULL_EEG_V1) + #define ADS1299_DRDY_PIN 8 + #define ADS1299_SPI_MISO_PIN 9 //MASTER (nRF) IN ; SLAVE (ADS) DOUT + #define ADS1299_SPI_SCLK_PIN 10 + #define ADS1299_SPI_CS_PIN 11 + #define ADS1299_RESET_PIN 12 + #define ADS1299_PWDN_PIN 13 + #define ADS1299_SPI_MOSI_PIN 14 //MASTER (nRF) OUT; SLAVE (ADS) DIN +#endif + + +#define ADS1299_NUM_REGS 24 + +/** + * \brief ADS1291_2 register addresses. + * + * Consult the ADS1291/2 datasheet and user's guide for more information. + */ +#define ADS1299_REGADDR_ID 0x00 ///< Chip ID register. Read-only. +#define ADS1299_REGADDR_CONFIG1 0x01 ///< Configuration register 1. Controls conversion mode and data rate. +#define ADS1299_REGADDR_CONFIG2 0x02 ///< Configuration register 2. Controls LOFF comparator, reference, CLK pin, and test signal. +#define ADS1299_REGADDR_CONFIG3 0x03 +#define ADS1299_REGADDR_LOFF 0x04 ///< Lead-off control register. Controls lead-off frequency, magnitude, and threshold. +#define ADS1299_REGADDR_CH1SET 0x05 ///< Channel 1 settings register. Controls channel 1 input mux, gain, and power-down. +#define ADS1299_REGADDR_CH2SET 0x06 ///< Channel 2 settings register (ADS1292x only). Controls channel 2 input mux, gain, and power-down. +#define ADS1299_REGADDR_CH3SET 0x07 +#define ADS1299_REGADDR_CH4SET 0x08 +#define ADS1299_REGADDR_CH5SET 0x09 +#define ADS1299_REGADDR_CH6SET 0x0A +#define ADS1299_REGADDR_CH7SET 0x0B +#define ADS1299_REGADDR_CH8SET 0x0C +#define ADS1299_REGADDR_BIAS_SENSP 0x0D ///< RLD sense selection. Controls PGA chop frequency, RLD buffer, and channels for RLD derivation. +#define ADS1299_REGADDR_BIAS_SENSN 0x0E +#define ADS1299_REGADDR_LOFF_SENSP 0x0F ///< Lead-off sense selection. Controls current direction and selects channels that will use lead-off detection. +#define ADS1299_REGADDR_LOFF_SENSN 0x10 +#define ADS1299_REGADDR_LOFF_FLIP 0x11 +#define ADS1299_REGADDR_LOFF_STATP 0x12 ///< Lead-off status register. Bit 6 controls clock divider. For bits 4:0, 0: lead on, 1: lead off. +#define ADS1299_REGADDR_LOFF_STATN 0x13 +#define ADS1299_REGADDR_GPIO 0x14 ///< GPIO register. Controls state and direction of the ADS1291_2 GPIO pins. +#define ADS1299_REGADDR_MISC1 0x15 ///< Respiration 1 (ADS1292R only). See datasheet. +#define ADS1299_REGADDR_MISC2 0x16 ///< Respiration 2. Controls offset calibration, respiration modulator freq, and RLDREF signal source. +#define ADS1299_REGADDR_CONFIG4 0x17 + +/** + * \brief ADS1299 SPI communication opcodes. + * + * Consult the ADS1299 datasheet and user's guide for more information. + * For RREG and WREG opcodes, the first byte (opcode) must be ORed with the address of the register to be read/written. + * The command is completed with a second byte 000n nnnn, where n nnnn is (# registers to read) - 1. + */ + +#define ADS1299_OPC_WAKEUP 0x02 ///< Wake up from standby. +#define ADS1299_OPC_STANDBY 0x04 ///< Enter standby. +#define ADS1299_OPC_RESET 0x06 ///< Reset all registers. +#define ADS1299_OPC_START 0x08 ///< Start data conversions. +#define ADS1299_OPC_STOP 0x0A ///< Stop data conversions. +#define ADS1299_OPC_OFFSETCAL 0x1A ///< Calibrate channel offset. RESP2.CALIB_ON must be 1. Execute after every PGA gain change. +#define ADS1299_OPC_RDATAC 0x10 ///< Read data continuously (registers cannot be read or written in this mode). +#define ADS1299_OPC_SDATAC 0x11 ///< Stop continuous data read. +#define ADS1299_OPC_RDATA 0x12 ///< Read single data value. + +#define ADS1299_OPC_RREG 0x20 ///< Read register value. System must not be in RDATAC mode. +#define ADS1299_OPC_WREG 0x40 ///< Write register value. + +/* ID REGISTER ********************************************************************/ + +/** + * \brief Factory-programmed device ID for ADS1299 & ADS1299-x. + */ + +#define ADS1299_4_DEVICE_ID 0x1C //Device ID [0bvvv11100] Where vvv is the version bits +#define ADS1299_6_DEVICE_ID 0x1D //Device ID [0bvvv11101] +#define ADS1299_DEVICE_ID 0x1E //Device ID [0bvvv11101] + +/* DEFAULT REGISTER VALUES ********************************************************/ + +//Use multi-line copy from excel file. +#define ADS1299_REGDEFAULT_CONFIG1 0xB6 ///< Configuration register 1. Controls conversion mode and data rate. +#define ADS1299_REGDEFAULT_CONFIG2 0xD2 ///< Configuration register 2. Controls LOFF comparator, reference, CLK pin, and test signal. +#define ADS1299_REGDEFAULT_CONFIG3 0xEC +#define ADS1299_REGDEFAULT_LOFF 0x02 ///< Lead-off control register. Controls lead-off frequency, magnitude, and threshold. +#define ADS1299_REGDEFAULT_CH1SET 0x60 ///< Channel 1 settings register. Controls channel 1 input mux, gain, and power-down. +#define ADS1299_REGDEFAULT_CH2SET 0x60 ///< +#define ADS1299_REGDEFAULT_CH3SET 0x60 +#define ADS1299_REGDEFAULT_CH4SET 0x60 +#define ADS1299_REGDEFAULT_CH5SET 0xF1 +#define ADS1299_REGDEFAULT_CH6SET 0xF1 +#define ADS1299_REGDEFAULT_CH7SET 0xF1 +#define ADS1299_REGDEFAULT_CH8SET 0xF1 +#define ADS1299_REGDEFAULT_BIAS_SENSP 0x0F ///< +#define ADS1299_REGDEFAULT_BIAS_SENSN 0x0F +#define ADS1299_REGDEFAULT_LOFF_SENSP 0x00 ///< +#define ADS1299_REGDEFAULT_LOFF_SENSN 0x00 +#define ADS1299_REGDEFAULT_LOFF_FLIP 0x00 +#define ADS1299_REGDEFAULT_LOFF_STATP 0x00 ///< +#define ADS1299_REGDEFAULT_LOFF_STATN 0x00 +#define ADS1299_REGDEFAULT_GPIO 0x0F ///< +#define ADS1299_REGDEFAULT_MISC1 0x20 ///< +#define ADS1299_REGDEFAULT_MISC2 0x00 ///< +#define ADS1299_REGDEFAULT_CONFIG4 0x00 + +/* SUPPORTED DATA RATES ********************************************************/ +#define ADS1299_ODR_125_SPS 0x00 +#define ADS1299_ODR_250_SPS 0x01 +#define ADS1299_ODR_500_SPS 0x02 +#define ADS1299_ODR_1000_SPS 0x03 +#define ADS1299_ODR_2000_SPS 0x04 +#define ADS1299_ODR_4000_SPS 0x05 +#define ADS1299_ODR_8000_SPS 0x06 + +class ADS1299Driver { +private: + /* spi structs */ + // Both are created and managed outside, should not be deleted here + // consider shared ptr? + const device* spi_dev; + const spi_config* spi_cfg; + +public: + explicit ADS1299Driver(device* spi, spi_config* cfg) : spi_dev(spi), spi_cfg(cfg) {}; + ~ADS1299Driver() {}; // do nothing + + /** + * \brief Initialize the ADS1299-x. + * + * This function performs the power-on reset and initialization procedure documented on page 61 of the + * ADS1299 datasheet, up to "Send SDATAC Command." + */ + void ads1299_spi_transfer(uint8_t* tx_data, size_t tx_len, uint8_t* rx_data, size_t rx_len); + + void ads1299_powerup_reset(void); + + void ads1299_init_regs(void); + + void ads1299_powerdn(void); + + void ads1299_powerup(void); + + void ads1299_standby(void); + + void ads1299_wake(void); + + void ads1299_soft_start_conversion(void); + + void ads1299_stop_rdatac(void); + + void ads1299_start_rdatac(void); + + void ads1299_check_id(void); + + void get_eeg_voltage_samples(int32_t *eeg1, int32_t *eeg2, int32_t *eeg3, int32_t *eeg4); +}; + +#endif // ADS1299_H__ diff --git a/src/main.cpp b/src/main.cpp index 48c3543..2244444 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,141 +6,48 @@ #include #include +#include +#include #include #include #include #include -#include "arm_math.h" +#include "ArmMatrixWrapper.h" +#include "DataAcquisitionThread.h" +#include "drivers/ads1299-x.h" -#define ASSERT_MSG_BUFFER_ALLOC_FAILED "buffer allocation failed" -#define ASSERT_MSG_SNR_LIMIT_EXCEED "signal-to-noise ratio " \ - "error limit exceeded" +// temporary +#include -/** @brief Power OFF entire RAM and suspend CPU forever. - * - * Function operates only on `register` variables, so GCC will not use - * RAM after function prologue. This is also true even with optimizations - * disabled. Interrupts are disabled to make sure that they will never - * be executed when RAM is powered off. - * - * @param reg_begin Address to `POWER` register of the first NRF_VMC->RAM item - * @param reg_last Address to `POWER` register of the last NRF_VMC->RAM item - */ - -#define SNR_ERROR_THRESH ((float32_t)120) -constexpr int32_t sigproc_buf_num_int = 8 * 1000 * 5; -constexpr int32_t afe_buf_num_int = 8 * 1000 * 1; - -static float32_t test_buffer_sigproc [sigproc_buf_num_int] = { 0 }; -static float32_t test_buffer_afe [afe_buf_num_int] = { 0 }; - -void disable_ram_and_wfi(register volatile uint32_t *reg_begin, - register volatile uint32_t *reg_last) -{ - __disable_irq(); - - do { - *reg_begin = 0; - reg_begin += sizeof(NRF_VMC->RAM[0]) / sizeof(reg_begin[0]); - } while (reg_begin <= reg_last); - - __DSB(); - do { - __WFI(); - } while (1); -} - -/** - * NOTE: SHOULD NOT USE MALLOC OR USE SAFER ZEPHYR METHOD - * NOTE: USE ZEPHYR SAFE ASSERT -*/ -static void test_arm_rfft_f32_real_backend( - bool inverse, const uint32_t *input, const uint32_t *ref, - size_t length) -{ - arm_rfft_fast_instance_f32 inst; - float32_t *output, *scratch; - - /* Initialise instance */ - arm_rfft_fast_init_f32(&inst, length); - - /* Allocate output buffer */ - output = (float32_t*)malloc(length * sizeof(float32_t)); - // zassert_not_null(output, ASSERT_MSG_BUFFER_ALLOC_FAILED); - - scratch = (float32_t*)calloc(length + 2, sizeof(float32_t)); /* see #24701 */ - // zassert_not_null(scratch, ASSERT_MSG_BUFFER_ALLOC_FAILED); - - /* Load data in place */ - memcpy(scratch, input, length * sizeof(float32_t)); - - /* Run test function */ - arm_rfft_fast_f32(&inst, scratch, output, inverse); +LOG_MODULE_REGISTER(eegals_app_core, LOG_LEVEL_DBG); - /* Validate output */ - // zassert_true( - // test_snr_error_f32(length, output, (float32_t *)ref, - // SNR_ERROR_THRESH), - // ASSERT_MSG_SNR_LIMIT_EXCEED); +static const struct pwm_dt_spec pwm_led0 = PWM_DT_SPEC_GET(DT_ALIAS(pwm_led0)); - /* Free output buffer */ - free(output); - free(scratch); -} +#define LOG_DELAY_MS 500 +#define LED_PERIOD 1000 +#define LED_OFF 0 +#define LED_CHANNEL 0 +#define LED_BLUE_PULSE_WIDTH 1000 -static void test_arm_rfft_f32_real(const uint32_t *input, const uint32_t *ref, size_t length) { - test_arm_rfft_f32_real_backend(false, input, ref, length); -} int main(void) { - /* Power off RAM and suspend CPU */ - // disable_ram_and_wfi(&NRF_VMC->RAM[0].POWER, - // &NRF_VMC->RAM[ARRAY_SIZE(NRF_VMC->RAM) - 1].POWER); - - test_buffer_sigproc[0] = 1; - test_buffer_afe[0] = 1; - - size_t fft_length = 100; - uint32_t fft_input[fft_length] = { 0 }; - uint32_t fft_output[fft_length] = { 0 }; - - test_arm_rfft_f32_real(fft_input, fft_output, fft_length); - - return 0; -} - -/** @brief Allow access to specific GPIOs for the network core. - * - * Function is executed very early during system initialization to make sure - * that the network core is not started yet. More pins can be added if the - * network core needs them. - */ -static int network_gpio_allow(void) -{ - - /* When the use of the low frequency crystal oscillator (LFXO) is - * enabled, do not modify the configuration of the pins P0.00 (XL1) - * and P0.01 (XL2), as they need to stay configured with the value - * Peripheral. - */ - uint32_t start_pin = (IS_ENABLED(CONFIG_SOC_ENABLE_LFXO) ? 2 : 0); - - /* Allow the network core to use all GPIOs. */ - for (uint32_t i = start_pin; i < P0_PIN_NUM; i++) { - NRF_P0_S->PIN_CNF[i] = (GPIO_PIN_CNF_MCUSEL_NetworkMCU << - GPIO_PIN_CNF_MCUSEL_Pos); + LOG_INF("Hello world from %s", CONFIG_BOARD); + + // DataAcquisitionThread::GetInstance().Initialize(); + + while(1) { + LOG_DBG("main thread up time: %u ms", k_uptime_get_32()); + pwm_set_dt(&pwm_led0, LED_PERIOD, LED_BLUE_PULSE_WIDTH); + k_msleep(LOG_DELAY_MS); + pwm_set_dt(&pwm_led0, LED_PERIOD, LED_OFF); + k_msleep(LOG_DELAY_MS); + // DataAcquisitionThread::GetInstance().SendMessage( + // DataAcquisitionThread::START_READING_AFE + // ); } - for (uint32_t i = 0; i < P1_PIN_NUM; i++) { - NRF_P1_S->PIN_CNF[i] = (GPIO_PIN_CNF_MCUSEL_NetworkMCU << - GPIO_PIN_CNF_MCUSEL_Pos); - } - - return 0; -} - -SYS_INIT(network_gpio_allow, PRE_KERNEL_1, CONFIG_KERNEL_INIT_PRIORITY_OBJECTS); +};