nRF7002DK Fitness Tracker using MMA7361 Accelerometer and GC9A01 driven round LCD display

Hello,

I'm currently working on a fitness tracker application using the Nordic nRF7002 DK board, paired with an analog MMA7361 accelerometer, and a round GC9A01 SPI-based LCD display.

I'm facing some challenges with properly integrating these components and ensuring compatibility within the nRF Connect SDK (Zephyr) environment. Specifically, I'd appreciate if you could provide guidance on:

  1. Hardware Integration:

    • Recommended connection pins and guidelines for interfacing the MMA7361 analog outputs with the built-in ADC on the nRF7002 DK.

    • Recommended wiring and SPI pin assignments for integrating the GC9A01 display with nRF7002 DK.

  2. Software Integration:

    • Recommended Zephyr drivers/modules for efficiently reading analog accelerometer data (via SAADC).

    • Recommended libraries or driver implementations compatible with Zephyr for controlling a GC9A01 LCD (or examples of similar displays).

    • Tips on effectively managing simultaneous SPI and ADC operations within Zephyr RTOS.

  3. Sample Code/References:

    • Could you point me to existing Zephyr or nRF Connect SDK examples or projects that demonstrate similar integration (analog sensor + SPI display)?

Any additional insights, recommended best practices, or reference materials would be greatly appreciated.

Thank you in advance!

Parents
  • Hello,

    For the display, perhaps you can test the sample:

    NCS\zephyr\samples\subsys\display\cfb

    But you need to specify the display you are using using devicetree. You should be able to build this sample for the "reel_board", which has a display. You can look at the reel_board devicetree files located in:

    NCS\zephyr\boards\phytec\reel_board\reel_board.dts.

    You can add something like this to a file called nrf5340dk_nrf5340_cpuapp.overlay in your application's main folder, and any parts that you copy from the zephyr\boards\nordic\nrf5340dk\nrf5340dk_nrf5340_cpuapp.dts and paste in your overlay file, and then change there will overwrite what it says in the original .dts file.

    As for SPI and ADC, I would recommend that you check out the Developer Academy. Both the nRF Connect SDK Fundamentals and Intermediate courses are relevant. The intermediate course even has a lesson dedicated to the PWM, SPI and ADC peripherals.

    Best regards,

    Edvin

  • Sorry to bother you again. I've managed to get the ADC‐reading part somewhat working:

    #include <zephyr/kernel.h>
    #include <zephyr/device.h>
    #include <zephyr/devicetree.h>
    #include <zephyr/logging/log.h>
    /* STEP 3.1 - Include the header file of the Zephyr ADC API */
    #include <zephyr/drivers/adc.h>
    #include <math.h>
    
    /* STEP 3.2 - Define a variable of type adc_dt_spec for each channel */
    static const struct adc_dt_spec adc_channel0 = ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), 0);
    static const struct adc_dt_spec adc_channel1 = ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), 1);
    static const struct adc_dt_spec adc_channel2 = ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), 2);
    
    LOG_MODULE_REGISTER(Fitness, LOG_LEVEL_DBG);
    
    int main(void)
    {
    	int err;
    	uint32_t count = 0;
    
    	/* STEP 4.1 - Define a variable of type adc_sequence and a buffer of type uint16_t */
    	int16_t buf0;
    	struct adc_sequence sequence0 = {
    		.buffer = &buf0,
    		/* buffer size in bytes, not number of samples */
    		.buffer_size = sizeof(buf0),
    		// Optional
    		//.calibrate = true,
    	};
    	int16_t buf1;
    	struct adc_sequence sequence1 = {
    		.buffer = &buf1,
    		/* buffer size in bytes, not number of samples */
    		.buffer_size = sizeof(buf1),
    		// Optional
    		//.calibrate = true,
    	};
    	int16_t buf2;
    	struct adc_sequence sequence2 = {
    		.buffer = &buf2,
    		/* buffer size in bytes, not number of samples */
    		.buffer_size = sizeof(buf2),
    		// Optional
    		//.calibrate = true,
    	};
    
    	/* STEP 3.3 - validate that the ADC peripheral (SAADC) is ready */
    	if (!adc_is_ready_dt(&adc_channel0))
    	{
    		LOG_ERR("ADC controller devivce %s not ready", adc_channel0.dev->name);
    		return 0;
    	}
    	if (!adc_is_ready_dt(&adc_channel1))
    	{
    		LOG_ERR("ADC controller devivce %s not ready", adc_channel1.dev->name);
    		return 0;
    	}
    	if (!adc_is_ready_dt(&adc_channel2))
    	{
    		LOG_ERR("ADC controller devivce %s not ready", adc_channel2.dev->name);
    		return 0;
    	}
    	/* STEP 3.4 - Setup the ADC channel */
    	err = adc_channel_setup_dt(&adc_channel0);
    	if (err < 0)
    	{
    		LOG_ERR("Could not setup channel #%d (%d)", 0, err);
    		return 0;
    	}
    	err = adc_channel_setup_dt(&adc_channel1);
    	if (err < 0)
    	{
    		LOG_ERR("Could not setup channel #%d (%d)", 0, err);
    		return 0;
    	}
    	err = adc_channel_setup_dt(&adc_channel2);
    	if (err < 0)
    	{
    		LOG_ERR("Could not setup channel #%d (%d)", 0, err);
    		return 0;
    	}
    	/* STEP 4.2 - Initialize the ADC sequence */
    	err = adc_sequence_init_dt(&adc_channel0, &sequence0);
    	if (err < 0)
    	{
    		LOG_ERR("Could not initalize sequnce");
    		return 0;
    	}
    	err = adc_sequence_init_dt(&adc_channel1, &sequence1);
    	if (err < 0)
    	{
    		LOG_ERR("Could not initalize sequnce");
    		return 0;
    	}
    	err = adc_sequence_init_dt(&adc_channel2, &sequence2);
    	if (err < 0)
    	{
    		LOG_ERR("Could not initalize sequnce");
    		return 0;
    	}
    
    	while (1)
    	{
    		int val_mv0;
    		int val_mv1;
    		int val_mv2;
    
    		/* STEP 5 - Read a sample from the ADC */
    		err = adc_read(adc_channel0.dev, &sequence0);
    		if (err < 0)
    		{
    			LOG_ERR("Could not read (%d)", err);
    			continue;
    		}
    
    		err = adc_read(adc_channel1.dev, &sequence1);
    		if (err < 0)
    		{
    			LOG_ERR("Could not read (%d)", err);
    			continue;
    		}
    
    		err = adc_read(adc_channel2.dev, &sequence2);
    		if (err < 0)
    		{
    			LOG_ERR("Could not read (%d)", err);
    			continue;
    		}
    
    		val_mv0 = (int)buf0;
    		LOG_INF("ADC reading[%u]: %s, channel %d: Raw: %d", count++, adc_channel0.dev->name,
    				adc_channel0.channel_id, val_mv0);
    
    		val_mv1 = (int)buf1;
    		LOG_INF("ADC reading[%u]: %s, channel %d: Raw: %d", count++, adc_channel1.dev->name,
    				adc_channel1.channel_id, val_mv1);
    
    		val_mv2 = (int)buf2;
    		LOG_INF("ADC reading[%u]: %s, channel %d: Raw: %d", count++, adc_channel2.dev->name,
    				adc_channel2.channel_id, val_mv2);
    
    		/* STEP 6 - Convert raw value to mV*/
    		err = adc_raw_to_millivolts_dt(&adc_channel0, &val_mv0);
    		/* conversion to mV may not be supported, skip if not */
    		if (err < 0)
    		{
    			LOG_WRN(" (value in mV not available)\n");
    		}
    		else
    		{
    			LOG_INF(" = %d mV", val_mv0);
    		}
    		err = adc_raw_to_millivolts_dt(&adc_channel1, &val_mv1);
    		/* conversion to mV may not be supported, skip if not */
    		if (err < 0)
    		{
    			LOG_WRN(" (value in mV not available)\n");
    		}
    		else
    		{
    			LOG_INF(" = %d mV", val_mv1);
    		}
    		err = adc_raw_to_millivolts_dt(&adc_channel2, &val_mv2);
    		/* conversion to mV may not be supported, skip if not */
    		if (err < 0)
    		{
    			LOG_WRN(" (value in mV not available)\n");
    		}
    		else
    		{
    			LOG_INF(" = %d mV", val_mv2);
    		}
    
    		k_sleep(K_MSEC(1000));
    	}
    	return 0;
    }

    I’m not sure if I can simplify the code, since I’ve repeated it for all three channels. Now I’m stuck on the display part. From what I’ve read, the display’s SPI interface requires 3.3 V logic, but my nRF7002DK only provides 1.8 V logic. Is that correct, and would I need a level shifter, or is there a way to connect it without extra components?

    Thank you in advance!

Reply
  • Sorry to bother you again. I've managed to get the ADC‐reading part somewhat working:

    #include <zephyr/kernel.h>
    #include <zephyr/device.h>
    #include <zephyr/devicetree.h>
    #include <zephyr/logging/log.h>
    /* STEP 3.1 - Include the header file of the Zephyr ADC API */
    #include <zephyr/drivers/adc.h>
    #include <math.h>
    
    /* STEP 3.2 - Define a variable of type adc_dt_spec for each channel */
    static const struct adc_dt_spec adc_channel0 = ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), 0);
    static const struct adc_dt_spec adc_channel1 = ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), 1);
    static const struct adc_dt_spec adc_channel2 = ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), 2);
    
    LOG_MODULE_REGISTER(Fitness, LOG_LEVEL_DBG);
    
    int main(void)
    {
    	int err;
    	uint32_t count = 0;
    
    	/* STEP 4.1 - Define a variable of type adc_sequence and a buffer of type uint16_t */
    	int16_t buf0;
    	struct adc_sequence sequence0 = {
    		.buffer = &buf0,
    		/* buffer size in bytes, not number of samples */
    		.buffer_size = sizeof(buf0),
    		// Optional
    		//.calibrate = true,
    	};
    	int16_t buf1;
    	struct adc_sequence sequence1 = {
    		.buffer = &buf1,
    		/* buffer size in bytes, not number of samples */
    		.buffer_size = sizeof(buf1),
    		// Optional
    		//.calibrate = true,
    	};
    	int16_t buf2;
    	struct adc_sequence sequence2 = {
    		.buffer = &buf2,
    		/* buffer size in bytes, not number of samples */
    		.buffer_size = sizeof(buf2),
    		// Optional
    		//.calibrate = true,
    	};
    
    	/* STEP 3.3 - validate that the ADC peripheral (SAADC) is ready */
    	if (!adc_is_ready_dt(&adc_channel0))
    	{
    		LOG_ERR("ADC controller devivce %s not ready", adc_channel0.dev->name);
    		return 0;
    	}
    	if (!adc_is_ready_dt(&adc_channel1))
    	{
    		LOG_ERR("ADC controller devivce %s not ready", adc_channel1.dev->name);
    		return 0;
    	}
    	if (!adc_is_ready_dt(&adc_channel2))
    	{
    		LOG_ERR("ADC controller devivce %s not ready", adc_channel2.dev->name);
    		return 0;
    	}
    	/* STEP 3.4 - Setup the ADC channel */
    	err = adc_channel_setup_dt(&adc_channel0);
    	if (err < 0)
    	{
    		LOG_ERR("Could not setup channel #%d (%d)", 0, err);
    		return 0;
    	}
    	err = adc_channel_setup_dt(&adc_channel1);
    	if (err < 0)
    	{
    		LOG_ERR("Could not setup channel #%d (%d)", 0, err);
    		return 0;
    	}
    	err = adc_channel_setup_dt(&adc_channel2);
    	if (err < 0)
    	{
    		LOG_ERR("Could not setup channel #%d (%d)", 0, err);
    		return 0;
    	}
    	/* STEP 4.2 - Initialize the ADC sequence */
    	err = adc_sequence_init_dt(&adc_channel0, &sequence0);
    	if (err < 0)
    	{
    		LOG_ERR("Could not initalize sequnce");
    		return 0;
    	}
    	err = adc_sequence_init_dt(&adc_channel1, &sequence1);
    	if (err < 0)
    	{
    		LOG_ERR("Could not initalize sequnce");
    		return 0;
    	}
    	err = adc_sequence_init_dt(&adc_channel2, &sequence2);
    	if (err < 0)
    	{
    		LOG_ERR("Could not initalize sequnce");
    		return 0;
    	}
    
    	while (1)
    	{
    		int val_mv0;
    		int val_mv1;
    		int val_mv2;
    
    		/* STEP 5 - Read a sample from the ADC */
    		err = adc_read(adc_channel0.dev, &sequence0);
    		if (err < 0)
    		{
    			LOG_ERR("Could not read (%d)", err);
    			continue;
    		}
    
    		err = adc_read(adc_channel1.dev, &sequence1);
    		if (err < 0)
    		{
    			LOG_ERR("Could not read (%d)", err);
    			continue;
    		}
    
    		err = adc_read(adc_channel2.dev, &sequence2);
    		if (err < 0)
    		{
    			LOG_ERR("Could not read (%d)", err);
    			continue;
    		}
    
    		val_mv0 = (int)buf0;
    		LOG_INF("ADC reading[%u]: %s, channel %d: Raw: %d", count++, adc_channel0.dev->name,
    				adc_channel0.channel_id, val_mv0);
    
    		val_mv1 = (int)buf1;
    		LOG_INF("ADC reading[%u]: %s, channel %d: Raw: %d", count++, adc_channel1.dev->name,
    				adc_channel1.channel_id, val_mv1);
    
    		val_mv2 = (int)buf2;
    		LOG_INF("ADC reading[%u]: %s, channel %d: Raw: %d", count++, adc_channel2.dev->name,
    				adc_channel2.channel_id, val_mv2);
    
    		/* STEP 6 - Convert raw value to mV*/
    		err = adc_raw_to_millivolts_dt(&adc_channel0, &val_mv0);
    		/* conversion to mV may not be supported, skip if not */
    		if (err < 0)
    		{
    			LOG_WRN(" (value in mV not available)\n");
    		}
    		else
    		{
    			LOG_INF(" = %d mV", val_mv0);
    		}
    		err = adc_raw_to_millivolts_dt(&adc_channel1, &val_mv1);
    		/* conversion to mV may not be supported, skip if not */
    		if (err < 0)
    		{
    			LOG_WRN(" (value in mV not available)\n");
    		}
    		else
    		{
    			LOG_INF(" = %d mV", val_mv1);
    		}
    		err = adc_raw_to_millivolts_dt(&adc_channel2, &val_mv2);
    		/* conversion to mV may not be supported, skip if not */
    		if (err < 0)
    		{
    			LOG_WRN(" (value in mV not available)\n");
    		}
    		else
    		{
    			LOG_INF(" = %d mV", val_mv2);
    		}
    
    		k_sleep(K_MSEC(1000));
    	}
    	return 0;
    }

    I’m not sure if I can simplify the code, since I’ve repeated it for all three channels. Now I’m stuck on the display part. From what I’ve read, the display’s SPI interface requires 3.3 V logic, but my nRF7002DK only provides 1.8 V logic. Is that correct, and would I need a level shifter, or is there a way to connect it without extra components?

    Thank you in advance!

Children
No Data
Related