nRF Connect SDK Fundamentals – [Lesson 5] – Exercise 1 – Controlling LEDs through UART – v2.9.0-v2.7.0

Controlling LEDs through UART

In the exercise, you will learn how to control the LEDs on your development kit through UART. We will utilize the UART controller that is connected by default to the interface MCU on your Nordic development kit to provide UART over USB.

We will set up the UART in asynchronous mode so that a callback function is called every time there is new data. We will set up the receive buffer size to 10 bytes and the receive timeout to 100 microseconds. This means when a byte is received and there is inactivity (no new data) after 100 microseconds, or 10 bytes is received, a UART_RX_RDY event is generated.

In this exercise, we will simply practice the steps covered in the UART Driver section to interact with the UART hardware in an asynchronous manner.

Note

This exercise is not supported on the Thingy (Thingy:53, Thingy:91, Thingy:91 X).

Exercise steps

1. In the GitHub repository for this course, open the base code for this exercise, found in l5/l5_e1whichever version of whichever version branch you are using.

This code is based on the blinky sample.

2. Enable the serial driver (UART driver) by adding the following two lines in prj.conf.

CONFIG_SERIAL=y
CONFIG_UART_ASYNC_API=y
Kconfig

As we mentioned before, the first line is usually enabled by default through the board’s devicetree. However, the second line is important to enable the asynchronous API of the serial driver.

3. Include the header file of the UART driver in main.c.

#include <zephyr/drivers/uart.h>
C

4. Get the device pointer of the UART hardware and verify that it is ready.

4.1 Get the device pointer of the UART hardware.

Remember, in nRF Connect SDK, any peripheral is accessed as a pointer of type struct device.

const struct device *uart= DEVICE_DT_GET(DT_NODELABEL(uart0));
C
const struct device *uart= DEVICE_DT_GET(DT_NODELABEL(uart20));
C

Note

On the nRF54 Series devices, the peripherals have two digits. In this exercise, we will be using the &uart20 instance.

4.2 Verify that the UART device is ready.

	if (!device_is_ready(uart)){
		printk("UART device not ready\r\n");
		return 1 ;
	}
C

5. Get the device pointers of the LEDs through gpio_dt_spec and verify that it is ready.

5.1 Get the device pointer of the LEDs

Since we are using UART to control the LEDs (1-3) on the development kit (this will be LEDs 0-2 on the nRF54 DKs), we need to get the device pointer for these as well. As we saw in Lesson 2, this is encapsulated in the API-specific structure gpio_dt_spec and can be retrieved using GPIO_DT_SPEC_GET().

The nRF7002 DK only has 2 LEDs, so this step requires a different code snippet if you’re using this board.

static const struct gpio_dt_spec led0 = GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios);
static const struct gpio_dt_spec led1 = GPIO_DT_SPEC_GET(DT_ALIAS(led1), gpios);
static const struct gpio_dt_spec led2 = GPIO_DT_SPEC_GET(DT_ALIAS(led2), gpios);
C
static const struct gpio_dt_spec led0 = GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios);
static const struct gpio_dt_spec led1 = GPIO_DT_SPEC_GET(DT_ALIAS(led1), gpios);
C

5.2 Verify that the LED devices are ready.

Since all the LEDs are associated with the same port, &gpio0, we only need to check one of the devices.

	if (!device_is_ready(led0.port)){
		printk("GPIO device is not ready\r\n");
		return 1;
	}
C

6. Configure the GPIOs of the LEDs

Since the nRF7002 DK only has 2 LEDs, this step requires a different code snippet excluding the third LED.

ret = gpio_pin_configure_dt(&led0, GPIO_OUTPUT_ACTIVE);
	if (ret < 0) {
		return 1 ; 
	}
	ret = gpio_pin_configure_dt(&led1, GPIO_OUTPUT_ACTIVE);
	if (ret < 0) {
		return 1 ;
	}
	ret = gpio_pin_configure_dt(&led2, GPIO_OUTPUT_ACTIVE);
	if (ret < 0) {
		return 1 ;
	}
C
ret = gpio_pin_configure_dt(&led0, GPIO_OUTPUT_ACTIVE);
	if (ret < 0) {
		return 1 ; 
	}
	ret = gpio_pin_configure_dt(&led1, GPIO_OUTPUT_ACTIVE);
	if (ret < 0) {
		return 1 ;
	}
C

7. Define the application callback function for UART. For our simple LED application, we are only interested in the UART_RX_RDY and the UART_RX_DISABLED events related to receiving data over UART. For your own application, this might differ as we discussed in the UART driver section.

static void uart_cb(const struct device *dev, struct uart_event *evt, void *user_data)
{
	switch (evt->type) {

	case UART_RX_RDY:
		if ((evt->data.rx.len) == 1) {

			if (evt->data.rx.buf[evt->data.rx.offset] == '1') {
				gpio_pin_toggle_dt(&led0);
			} else if (evt->data.rx.buf[evt->data.rx.offset] == '2') {
				gpio_pin_toggle_dt(&led1);
			} else if (evt->data.rx.buf[evt->data.rx.offset] == '3') {
				gpio_pin_toggle_dt(&led2);
			}
		}
		break;
	case UART_RX_DISABLED:
		uart_rx_enable(dev, rx_buf, sizeof rx_buf, RECEIVE_TIMEOUT);
		break;

	default:
		break;
	}
}
C
static void uart_cb(const struct device *dev, struct uart_event *evt, void *user_data)
{
	switch (evt->type) {

	case UART_RX_RDY:
		if ((evt->data.rx.len) == 1) {

			if (evt->data.rx.buf[evt->data.rx.offset] == '1') {
				gpio_pin_toggle_dt(&led0);
			} else if (evt->data.rx.buf[evt->data.rx.offset] == '2') {
				gpio_pin_toggle_dt(&led1);
			}
		}
		break;
	case UART_RX_DISABLED:
		uart_rx_enable(dev, rx_buf, sizeof rx_buf, RECEIVE_TIMEOUT);
		break;

	default:
		break;
	}
}
C

Since the LEDs on the nRF54 Series development kits are 0-indexed, we need to check against the numbers ‘0’, ‘1’ and ‘2’.

static void uart_cb(const struct device *dev, struct uart_event *evt, void *user_data)
{
	switch (evt->type) {

	case UART_RX_RDY:
		if ((evt->data.rx.len) == 1) {
			if (evt->data.rx.buf[evt->data.rx.offset] == '0') {
				gpio_pin_toggle_dt(&led0);
			} else if (evt->data.rx.buf[evt->data.rx.offset] == '1') {
				gpio_pin_toggle_dt(&led1);
			} else if (evt->data.rx.buf[evt->data.rx.offset] == '2') {
				gpio_pin_toggle_dt(&led2);
			}
		}
		break;
	case UART_RX_DISABLED:
		uart_rx_enable(dev, rx_buf, sizeof rx_buf, RECEIVE_TIMEOUT);
		break;

	default:
		break;
	}
}
C

Note

With if((evt->data.rx.len) == 1), we are assuming a terminal in char mode, which is the default terminal in nRF Connect for VS Code (nRF Terminal) and PuTTY. These two terminal by default, does not require you to press enter to send the data, hence do not send extra bytes.
On the other hand,  if you are using a serial terminal in “line mode”, you need to press enter to send the data, which typically sends two extra bytes “/r/n” (Depending on the terminal configuration). Therefore you need to change the condition to match the length of the data you are sending.

8. Register the UART callback function by calling the function uart_callback_set().

	ret = uart_callback_set(uart, uart_cb, NULL);
		if (ret) {
			return 1;
		}
C

9. Now let’s send some data over UART.

9.1 Define the transmission buffer, which is a buffer to hold the data to be sent over UART.

static uint8_t tx_buf[] =   {"nRF Connect SDK Fundamentals Course\r\n"
                             "Press 1-3 on your keyboard to toggle LEDS 1-3 on your development kit\r\n"};
C
static uint8_t tx_buf[] =   {"nRF Connect SDK Fundamentals Course\r\n"
                             "Press 1-2 on your keyboard to toggle LEDS 1-2 on your development kit\r\n"};
C
static uint8_t tx_buf[] =   {"nRF Connect SDK Fundamentals Course\r\n"
                             "Press 0-2 on your keyboard to toggle LEDS 0-2 on your development kit\r\n"};
C

9.2 Send the data over UART by calling uart_tx(). Since we are not using flow control, pass SYS_FOREVER_US to disable transmission timeout. This is not related to receiving timeout.

	ret = uart_tx(uart, tx_buf, sizeof(tx_buf), SYS_FOREVER_US);
	if (ret) {
		return 1;
	}
C

10. Let’s start receiving data!

10.1 Define the receive buffer. For this simple exercise of controlling LEDs through UART, we will define a small buffer of size 10 of type uint8_t.

10.1.1 Define the size of the receive buffer.

#define RECEIVE_BUFF_SIZE 10
C

10.1.2 Define the receive buffer and initialize its members to zeros.

static uint8_t rx_buf[RECEIVE_BUFF_SIZE] = {0};
C

10.2 Define the receiving timeout period to be 100 microseconds. This means when a byte is received and there is inactivity (no new data) for 100 microseconds, a UART_RX_RDY event is generated. If you are going to use receive timeout in your application, pick a value that matches your application requirements.

#define RECEIVE_TIMEOUT 100
C

10.3 Start receiving by calling uart_rx_enable() and pass it the address of the receive buffer.

	ret = uart_rx_enable(uart ,rx_buf,sizeof rx_buf,RECEIVE_TIMEOUT);
	if (ret) {
		return 1;
	}
C

11. Build the application and flash it on your development kit.

12. View configuration conflicts.

Now that we’ve built the application, you can go back to the prj.conf file and notice that the first line we added, CONFIG_SERIAL, has a blue squiggly line under it. This indicates a configuration conflict, usually because the Kconfig symbol is set more than once in the active build context.

Hover your mouse over the Kconfig symbol to see the configuration conflict. Here we can see that CONFIG_SERIAL is set more than once in the build.

Go back to the nRF Connect window.

  1. Select the build context for the application (l5_e1), then under the Details view, expand Kconfig. Here we can see all the files that make up the resulting build.
  2. Open the file that matches the build target you are using (in this case the nRF52840 DK)
  3. This is the Kconfig file in the board target definition, zephyr/boards/nordic/
  4. Here we see that CONFIG_SERIAL was enabled in the board’s devicetree.

13. View the Devicetree Visual Editor.

Let’s also check that the UART peripheral was enabled in the devicetree by using the Devicetree Visual Editor.

13.1 Open the Deviceetree Visual Editor.

  1. Select the application image (l5_e1)
  2. Under the ACTIONS view, click on Devicetree.
  3. Alternatively, click on the icon in the left margin to open the Devicetree Visual Editor window.

This will open the Devicetree Visual Editor, which looks like the image below

  • Context Files: Lists the devicetree file for the active build context (shown in the Build Contexts View).
  • Context Overview: Lists the devicetree information for the active build context.
  • Nodes: Lists all available nodes for the selected devicetree file. All changes to nodes and pins here are automatically mirrored in the code of the active devicetree file. The nodes are also represented in the graphical Editor View, which lets you inspect and modify node properties and pin assignments. The graphical Editor View is covered in Lesson 3 in nRF Connect SDK Intermediate.

13.2 Confirm that the UART peripheral has been enabled.

In the Nodes view, find the soc menu and expand it by clicking on the arrow to the right. Then scroll down until you found uart0 and notice that its box has a check mark to indicate that it is enabled in the build context.

Testing

14. You should observe that the three LEDs on your development kit are all switched on. Using a serial terminal such as the integrated nRF Terminal or PuTTY, you should see the below output:

*** Booting nRF Connect SDK v2.8.0-preview1-11645184a54d ***
*** Using Zephyr OS v3.7.99-adcffa835a8e ***
Press 1-3 on your keyboard to toggle LED 1-3 on your development kit
Terminal
*** Booting nRF Connect SDK v2.8.0-preview1-11645184a54d ***
*** Using Zephyr OS v3.7.99-adcffa835a8e ***
Press 1-2 on your keyboard to toggle LED 1-2 on your development kit
Terminal
*** Booting nRF Connect SDK v2.8.0-preview1-11645184a54d ***
*** Using Zephyr OS v3.7.99-adcffa835a8e ***
Press 0-2 on your keyboard to toggle LED 0-2 on your development kit
Terminal

We are using the default baud rate (115200) and UART settings set in the devicetree. If you want to change that, you can do it using the UART API as explained earlier.

Writing a number from 1 to 3 (or 0 to 2 for the nRF54 DKs) in the serial emulator will toggle the corresponding LED on your development kit.

Note

If you are using a Thingy:91, LED1 is the red LED, LED2 is the green LED and LED3 is the blue LED.

The solution for this exercise can be found in the GitHub repository, l5/l5_e1_sol of whichever version branch you are using.

Switch language?

Progress is tracked separately for each language. Switching will continue from your progress in that language or start fresh if you haven't begun.

Your current progress is saved, and you can switch back anytime.

Register an account
Already have an account? Log in
(All fields are required unless specified optional)

  • 8 or more characters
  • Upper and lower case letters
  • At least one number or special character

Forgot your password?
Enter the email associated with your account, and we will send you a link to reset your password.