用戶在使用 nRF connect SDK 的時候經常會操作的外設有GPIO,I2C,SPI,UART。我們就以 nRF connect SDK 2.7.0 中的例程代碼 nrfsamplesbluetoothperipheral_lbs 為基礎,來演示上述外設的簡單使用。使用的硬件是開發板 nRF52840 DK.
準備工作
首先我們在原本的工程目錄的 boards 文件夾里,添加文件 nrf52840dk_nrf52840.overlay。通過這個文件我們可以修改 devicetree。 編譯完成后,我們可以查看 buildzephyrzephyr.dts,以確認devicetree 的更改是否生效。
我們還可以通過修改 prj.conf 來修改 Kconfig。編譯完成后,我們可以查看 buildzephyr.config 以確認 Kconfig 的更改是否生效。

GPIO 控制
首先我們演示如何刪除原有的按鍵和LED的 node 。按照下面的代碼,來修改 devicetree,就可以刪除 button3 和 led3。
/ {
aliases {
/delete-property/ sw3;
/delete-property/ led3;
};
};
/delete-node/ &button3;
/delete-node/ &led3;
接著我們來更改控制 led2 的管腳。這里我們用 P0.04 控制 led2。
&led2 {
gpios = &gpio0 4 GPIO_ACTIVE_LOW?>;
};
最后我們添加一個用戶 GPIO 。這里添加了一個名為 user_gpios 的 node。然后又定義了 user_io0,它是 user_gpios 的 subnode。
/ {
user_gpios {
compatible = "gpio-leds";
user_io0: user_io0 {
gpios = &gpio0 16 GPIO_ACTIVE_LOW?>;
label = "user gpio 0";
};
};
};
我們不僅在 devicetree 里添加這個 GPIO ,還要在 main.c 里添加代碼使用這個GPIO。下面這句代碼中,我們聲明了結構體變量 user_gpio0,并用宏 GPIO_DT_SPEC_GET 根據 devicetree 里的定義初始化它。
const struct gpio_dt_spec user_gpio_0 = GPIO_DT_SPEC_GET(DT_NODELABEL(user_io0),gpios);
下面這段代碼中 gpio_is_ready_dt 是用來檢查 GPIO 的狀態是否是就緒。用函數 gpio_pin_configure_dt 把 user_gpio_0 配置成輸出。gpio_pin_toggle_dt 用來翻轉 GPIO。
if (!gpio_is_ready_dt(&user_gpio_0)) {
printk("%s: device not ready.n", user_gpio_0.port->name);
return 0;
}
gpio_pin_configure_dt(&user_gpio_0, GPIO_OUTPUT_ACTIVE);
for (index = 0; index < 100; index++) {
gpio_pin_toggle_dt(&user_gpio_0);
k_sleep(K_MSEC(100));
}
從下面的代碼可以看出翻轉 GPIO 這個操作有兩種 API 可以調用。二者的主要區別是 gpio_pin_toggle_dt 不需要指明引腳 。
/** * @brief Toggle pin level from a @p gpio_dt_spec. * * This is equivalent to: * * gpio_pin_toggle(spec->port, spec->pin); * * @param spec GPIO specification from devicetree * @return a value from gpio_pin_toggle() */ static inline int gpio_pin_toggle_dt(const struct gpio_dt_spec *spec) { return gpio_pin_toggle(spec->port, spec->pin); }
I2C 設備控制
Nordic 的芯片中 I2C 接口是由外設 TWI 來實現的,I2C master 由 TWIM 實現, I2C master 由 TWIS 實現。這里將演示如何用一個 TWIM 來連接兩個 I2C slave 設備。
首先我們還是先修改 devicetree。我們使用 i2c1 這個 node。 一方面按照應用的要求修改這個 node 的 propertise,另一方面在這個 node 里創建兩個 sub-node。
i2c 的時鐘頻率通過 clock-frequency 來定義。
i2c 的引腳通過 pinctrl-0 和 pinctrl-1 定義。我們將在后面分析 i2c1_default 和 i2c1_sleep 的定義。
這兩個 sub-node 一個是 user_i2c_sensor,另一個是 user_i2c_eeprom。這兩個 sub-node 通過 propertise reg 來定義各自的 I2C 地址。
&i2c1 {
status = "ok";
clock-frequency = ;
pinctrl-0 = < &i2c1_default >;
pinctrl-1 = < &i2c1_sleep >;
pinctrl-names = "default", "sleep";
user_i2c_sensor: user_i2c_sensor@0 {
compatible = "i2c-user-define";
reg = 0xA?>;
};
user_i2c_eeprom: user_i2c_eeprom@0 {
compatible = "i2c-user-define";
reg = 0x5?>;
};
};
i2c1_default 和 i2c1_sleep的定義如下。TWIM_SDA 信號使用的是引腳 P0.04,TWIM_SCL 信號使用的是引腳 P0.03。
&pinctrl {
i2c1_default: i2c1_default {
group1 {
psels = ,
;
};
};
i2c1_sleep: i2c1_sleep {
group1 {
psels = ,
;
low-power-enable;
};
};
};
修改 prj.conf 添加 CONFIG_I2C=y
修改完 devicetree 我們在來添加操作 i2c 的代碼。分別定義 i2c1_sensor 和 i2c1_eeprom,它們對應剛才 i2c1 的兩個子節點。
const struct i2c_dt_spec i2c1_sensor = I2C_DT_SPEC_GET(DT_NODELABEL(user_i2c_sensor)); const struct i2c_dt_spec i2c1_eeprom= I2C_DT_SPEC_GET(DT_NODELABEL(user_i2c_eeprom));
i2c 設備在讀寫操作前無需調用 API 來配置 ,直接調用下面的寫函數。
err = i2c_write_dt(&i2c1_sensor, buf, 1); err = i2c_write_dt(&i2c1_eeprom, buf, 1);
通過邏輯分析儀我們可以看到如下的總線數據,操作的目標地址分別是我們在 devicetree 里設置的數值 0x05 和 0x0A 。

SPI 設備控制
Nordic 的芯片中 SPI 接口的 master 端通過 SPIM 實現, slave 端通過 SPIS 實現。這里將演示如何用一個 SPIM 來連接兩個 SPI slave 設備。
首先修改 devicetree。
這里我們使用 spi2, 并且關閉 spi1。在 nordic 的nRF52 系列芯片中,相同數字編號的 TWIM, TWIS, SPIM, SPIS 是共用一組硬件模塊的。在上面 i2c 中我們已經使用 i2c1, 所以這里我們就不能同時使用 spi1了。
cs-gpios 定義了 P0.26 和 P0.27 兩 個CS 信號。 SPI 用不同的片選信號,區分不同的 slave 設備。
devicetree node spi2 下定義了兩個 sub-node 分別是 user_spi_adc 和 user_spi_flash。 sub-node 里定義了三個 propertise。propertise compatible 的取值來自于我們在工程里新添加的文件 dtsbindingsspi-user-define.yaml。 propertise reg 的取值和前面的 propertise cs-gpios 呼應,reg = <0> 的 sub-node 使用 cs-gpios 里面定義的第一個 CS 引腳。reg = <1> 的 sub-node 使用 cs-gpios 里面定義的第二個 CS 引腳。propertise spi-max-frequency 定義 SPI 的時鐘頻率。兩個不同的 SPI 設備可以使用不同的時鐘頻率驅動。
&spi1 {
status = "disabled";
};
&spi2 {
status = "okay";
cs-gpios = &gpio0 26 GPIO_ACTIVE_LOW?>,
&gpio0 27 GPIO_ACTIVE_LOW?>;
pinctrl-0 = < &spi2_default >;
pinctrl-1 = < &spi2_sleep >;
pinctrl-names = "default", "sleep";
user_spi_adc: user_spi_adc@0 {
compatible = "spi-user-define";
reg = 0?>;
spi-max-frequency = ;
};
user_spi_flash: user_spi_flash@0 {
compatible = "spi-user-define";
reg = 1?>;
spi-max-frequency = ;
};
};
來看一下我們新添加的 dtsbindingsspi-user-define.yaml 里面的內容。如下圖 spi-user-define.yaml 里面包含了 spi-device.yaml 文件,這個文件的位置在目錄 zephyrdtsbindingsspi 。
compatible: "spi-user-define" include: [spi-device.yaml]
spi-device.yaml 文件里面定義了 spi 節點需要的一些 propertise。 比如我們在 sub-node 里定義的 propertise spi-max-frequency。
# Copyright (c) 2018, I-SENSE group of ICCS
# SPDX-License-Identifier: Apache-2.0
# Common fields for SPI devices
include: [base.yaml, power.yaml]
on-bus: spi
properties:
reg:
required: true
spi-max-frequency:
type: int
required: true
description: Maximum clock frequency of device's SPI interface in Hz
duplex:
type: int
default: 0
description: |
Duplex mode, full or half. By default it's always full duplex thus 0
as this is, by far, the most common mode.
Use the macros not the actual enum value, here is the concordance
list (see dt-bindings/spi/spi.h)
0 SPI_FULL_DUPLEX
2048 SPI_HALF_DUPLEX
enum:
- 0
- 2048
frame-format:
type: int
default: 0
description: |
Motorola or TI frame format. By default it's always Motorola's,
thus 0 as this is, by far, the most common format.
Use the macros not the actual enum value, here is the concordance
list (see dt-bindings/spi/spi.h)
0 SPI_FRAME_FORMAT_MOTOROLA
32768 SPI_FRAME_FORMAT_TI
enum:
- 0
- 32768
spi-cpol:
SPI 引腳定義如下 CLK P0.28, MISO P0.29, MOSI P0.30。
spi2_default: spi2_default {
group1 {
psels = ,
,
;
};
};
spi2_sleep: spi2_sleep {
group1 {
psels = ,
,
;
low-power-enable;
};
};
修改 prj.conf 添加 CONFIG_SPI=y CONFIG_SPI_ASYNC=y。
在 main.c 里添加 SPI 的應用代碼。下面這段代碼定義了兩個結構體變量,并通過宏 SPI_DT_SPEC_GET 用 devicetree 里的參數初始化了這兩個結構體變量。
#define SPI_OP SPI_OP_MODE_MASTER | SPI_MODE_CPOL | SPI_MODE_CPHA
| SPI_WORD_SET(8) | SPI_LINES_SINGLE
static struct spi_dt_spec spim2_adc = SPI_DT_SPEC_GET(DT_NODELABEL(user_spi_adc), SPI_OP, 0);
static struct spi_dt_spec spim2_flash = SPI_DT_SPEC_GET(DT_NODELABEL(user_spi_flash), SPI_OP, 0);
spi 驅動支持多 buffer 所以要定義 buffer 個數,和每個 buffer 的長度。同樣 spi 在讀寫之前無需調用配置函數,直接調用讀寫函數就行。
struct spi_buf_set tx_bufs; struct spi_buf spi_tx_buf; tx_bufs.buffers = &spi_tx_buf; tx_bufs.count = 1; spi_tx_buf.buf = buf; spi_tx_buf.len = 2; err = spi_write_dt(&spim2_adc, &tx_bufs); err = spi_write_dt(&spim2_flash, &tx_bufs);
下面是SPI的波形。可以看到和不同的 spi slave 設備通訊的時候, spi master 會拉低不同的 CS 引腳。

UART 控制
Nordic 的芯片中 UART 接口叫做 UARTE。這里的 E 是指 EasyDMA , UART 可以使用 DMA 來連續收發。
修改 Devicetree。這里使用 uart1。propertise current-speed 設置 uart 的波特率。
&uart1 {
status = "okay";
current-speed = 115200?>;
pinctrl-0 = < &uart1_default >;
pinctrl-1 = < &uart1_sleep >;
pinctrl-names = "default", "sleep";
};
TXD pin 為 P1.02, RXD pin 為 P1.01。
uart1_default: uart1_default {
group1 {
psels = ;
bias-pull-up;
};
group2 {
psels = ;
};
};
uart1_sleep: uart1_sleep {
group1 {
psels = ,
;
low-power-enable;
};
};
修改 prj.conf 在里面添加 CONFIG_UART_ASYNC_API=y CONFIG_UART_ASYNC_RX_HELPER=y。
修改 main.c 添加 uart 收發代碼。 uart_callback_set 設置 callback 函數 uart_cb。因為這里采用的是異步收發的模式,所以設置callback 函數是必備的。uart_rx_enable 使能接收。uart_tx 發送數據。
err = uart_callback_set(uart1, uart_cb, NULL);
//printk("uart_callback_set return %dn", err);
err = uart_rx_enable(uart1, uart_rx_buf, MAX_UART_BUF_LEN, UART_RX_TIMEOUT_MS);
//printk("uart_rx_enable return %dn", err);
err = uart_tx(uart1, uart_tx_buf, 6, SYS_FOREVER_MS);
//printk("uart_tx return %dn", err);
callback 函數 uart_cb 可能由多種事件觸發。比如當接收到數據后會觸發回調,并在參數 EVT 傳遞 UART_RX_RDY 和接收到的數據和長度。
static void uart_cb(const struct device *dev, struct uart_event *evt, void *user_data)
{
ARG_UNUSED(dev);
//LOG_INF("uart_cb evt->type:%d", evt->type);
switch (evt->type) {
case UART_TX_DONE:
printk("UART_TX_DONEn");
break;
case UART_RX_RDY:
printk("UART_RX_RDYn");
printk("received %d bytesn", evt->data.rx.len);
break;
case UART_RX_DISABLED:
printk("UART_RX_DISABLEDn");
break;
case UART_RX_BUF_REQUEST:
printk("UART_RX_BUF_REQUESTn");
uart_rx_buf_rsp(uart1, uart_rx_buf2, MAX_UART_BUF_LEN);
break;
case UART_RX_BUF_RELEASED:
printk("UART_RX_BUF_RELEASEDn");
break;
case UART_TX_ABORTED:
printk("UART_TX_ABORTEDn");
break;
default:
break;
}
}
我們在 DK 上把 TXD 引腳和 RXD 引腳短接來測試 UART 的收發,可以看到如下的 log 信息。UART 收到了自己發送的6字節的數據。

UART 應用代碼的優化
上面的 uart 演示代碼中,我們只實現了簡單的收發。下面我們將進一步在此基礎上優化 UART 的收發代碼。這一部分的修改都在 main.c 里,主要涉及下面幾個部分:
Thread 線程
Semaphore 信號量
線程間通訊 Message queue
線程 下面的代碼中通過 K_THREAD_DEFINE 定義了 一個獨立的線程來處理 uart 相關的代碼。線程處理函數 uart_thread_task 中:也是先用 uart_callback_set 設置了回調函數;再用 uart_rx_enable 使能了接收;然后是一個 for 循環,在里面不斷的接收消息,根據消息中的指令發送數據,或者處理接收到的數據。
#define UART_THREAD_STACK_SIZE 512
#define UART_THREAD_PRIORITY -1
void uart_thread_task(void)
{
int err;
struct uart_data_item_type uart_msgq;
k_sem_take(&uart_thread_start, K_FOREVER);
printk("uart_thread_taskn");
err = uart_callback_set(uart1, uart_cb, NULL);
err = uart_rx_enable(uart1, uart_rx_buf, MAX_UART_BUF_LEN, UART_RX_TIMEOUT_MS);
for (;;) {
k_msgq_get(&uart_data_msgq, &uart_msgq, K_FOREVER);
printk("received uart data itemn");
switch(uart_msgq.cmd) {
case UART_CMD_TX:
memcpy(uart_tx_buf,&uart_msgq.data, sizeof(uint32_t));
err = uart_tx(uart1, uart_tx_buf, sizeof(uint32_t), SYS_FOREVER_MS);
break;
case UART_CMD_DATA_PROCESS:
break;
default:
break;
}
}
}
K_THREAD_DEFINE(uart_thread_id, UART_THREAD_STACK_SIZE, uart_thread_task, NULL, NULL,
NULL, UART_THREAD_PRIORITY, 0, 0);
上面的代碼中用 K_THREAD_DEFINE 定義線程的時候,需要指定此線程的優先級 UART_THREAD_PRIORITY。UART_THREAD_PRIORITY 的數據類型是 integer,可以是正數也可以是負數。優先級的數字越小,優先級越高,負數的優先級比正數高。thread 的優先級取值為負數時,此 thread 為協同線程 cooperative thread 。當這種線程正在執行的時候,其它更高優先級的線程不能打斷它,必須等它執行完再執行下一個線程。當 thread 的優先級取值為正數,此 thread 為搶占線程 preemptible thread。當這種線程正在執行的時候,其它更高優先級的線程可以打斷它,跳轉到高優先級的任務。等高優先級的線程執行完才返回原 thread 繼續執行。回到例程代碼,從應用的角度出發,我們希望 uart_thread_task 的執行優先級大于 main 函數。通過查詢文件 buildzephyr.config 我們得知 CONFIG_MAIN_THREAD_PRIORITY 的取值為 0,也就是說 main thread 當前的優先級為 0, 所以我們定義了 UART_THREAD_PRIORITY 為 -1。這樣 uart thread 的優先級就高于 main thread, 而且 uart thread 的執行不會被其它更高優先級的 thread 打斷。需要注意的是這里的不能被打斷只是對 thread 而言,中斷是可以打斷 cooperative thread 的。
信號量 函數 uart_thread_task 的優先級比 main 函數高,所以會先于main 函數執行。如果之前的函數 uart_thread_task 里沒有 k_sem_take(&uart_thread_start, K_FOREVER),就會出現如下圖的現象。我們看到 uart thread 的 log 是先于 main thread 被打印出來的。

從應用的角度,我們希望 uart_thread_task 在 main 函數啟動完廣播之后再執行。這就引入了一個不同線程之間的同步問題。Zephyr RTOS 中可以通過 semaphore 解決不同 thread 間的同步問題。下面的代碼中通過 K_SEM_DEFINE 定義了一個為 uart_thread_start 的 semaphore 。 函數 uart_thread_task 執行到函數 k_sem_take 時,如果 uart_thread_start 沒有被釋放,當前 thread 會被掛起等待,直到 semaphore 被釋放。
static K_SEM_DEFINE(uart_thread_start, 0, 1);
#define UART_THREAD_STACK_SIZE 512
#define UART_THREAD_PRIORITY -1
void uart_thread_task(void)
{
int err;
struct uart_data_item_type uart_msgq;
k_sem_take(&uart_thread_start, K_FOREVER);
printk("uart_thread_taskn");
err = uart_callback_set(uart1, uart_cb, NULL);
err = uart_rx_enable(uart1, uart_rx_buf, MAX_UART_BUF_LEN, UART_RX_TIMEOUT_MS);
for (;;) {
在 main 里通過 k_sem_give 釋放 uart_thread_start。uart 線程會打斷當前的 main thread 從 k_sem_take 繼續執行。
err = spi_write_dt(&spim2_adc, &tx_bufs);
err = spi_write_dt(&spim2_flash, &tx_bufs);
k_sem_give(&uart_thread_start);
struct uart_data_item_type main_msgq;
main_msgq.cmd = UART_CMD_TX;
main_msgq.data = 0;
for (;;) {
while (k_msgq_put(&uart_data_msgq, &main_msgq, K_NO_WAIT) != 0) {
/* message queue is full: purge old data & try again */
k_msgq_purge(&uart_data_msgq);
}
main_msgq.data++;
dk_set_led(RUN_STATUS_LED, (++blink_status) % 2);
k_sleep(K_MSEC(RUN_LED_BLINK_INTERVAL));
}
線程間通訊 演示代碼中 main thread 會把要發送的數據通過線程通訊發送到 uart thread, uart thread 調用驅動函數發送。zephyr 中提供了多種線程間通訊方式,具體如下圖,這里使用的是 message queue。

下面的代碼中 K_MSGQ_DEFINE 定義了一個名為 uart_data_msgq 的 message queue。uart_data_msgq 的緩沖區里最多可以容納 8 個消息。
struct uart_data_item_type {
uint8_t cmd;
uint32_t data;
};
K_MSGQ_DEFINE(uart_data_msgq, sizeof(struct uart_data_item_type), 8, 4);
下面這段代碼來自于 main thread 的 main 函數。代碼會定時循環把待發送的數據打包成一個 message,然后推送到 message queue 里面。
struct uart_data_item_type main_msgq;
main_msgq.cmd = UART_CMD_TX;
main_msgq.data = 0;
for (;;) {
while (k_msgq_put(&uart_data_msgq, &main_msgq, K_NO_WAIT) != 0) {
/* message queue is full: purge old data & try again */
k_msgq_purge(&uart_data_msgq);
}
main_msgq.data++;
dk_set_led(RUN_STATUS_LED, (++blink_status) % 2);
k_sleep(K_MSEC(RUN_LED_BLINK_INTERVAL));
}
下面的代碼來自 uart thread 的 uart_thread_task 函數。 函數等待 message queue 里推送來的 message。得到 message 后,根據里面的 cmd 字段來處理發送或者接收數據。
void uart_thread_task(void)
{
int err;
struct uart_data_item_type uart_msgq;
k_sem_take(&uart_thread_start, K_FOREVER);
printk("uart_thread_taskn");
err = uart_callback_set(uart1, uart_cb, NULL);
err = uart_rx_enable(uart1, uart_rx_buf, MAX_UART_BUF_LEN, UART_RX_TIMEOUT_MS);
for (;;) {
k_msgq_get(&uart_data_msgq, &uart_msgq, K_FOREVER);
printk("received uart data itemn");
switch(uart_msgq.cmd) {
case UART_CMD_TX:
memcpy(uart_tx_buf,&uart_msgq.data, sizeof(uint32_t));
err = uart_tx(uart1, uart_tx_buf, sizeof(uint32_t), SYS_FOREVER_MS);
break;
case UART_CMD_DATA_PROCESS:
break;
default:
break;
}
}
}
下面是加入線程間通訊的代碼后得到的 log,當我們把 TX 和 RX 引腳短接后可以看出 uart thread 不斷的發送從 main thread 傳輸的數據。

總結
本文從實際操作出發,介紹了用戶最常用的一些外設如 GPIO, I2C, SPI, UART 的配置和使用方法。并介紹了一些簡單 RTOS 組件的應用如 thread, semaphore, message queue。希望能幫助 Nordic 用戶加快 nRF Connect SDK 的開發速度。
審核編輯 黃宇
-
NRF
+關注
關注
0文章
50瀏覽量
38619 -
SDK
+關注
關注
3文章
1095瀏覽量
51282
發布評論請先 登錄
Nordic 推出nRF Connect for Cloud 的無線物聯網設計方案
nRF Connect SDK(NCS)/Zephyr固件升級詳解 – 重點講述MCUboot和藍牙空中升級
Nordic nRF Connect SDK 官方開發文檔、學習資料下載鏈接
如何調試nRF5 SDK
Nordic無線開發---nRF Connect SDK 3.0更新版的安裝入門介紹
深度技術解析低功耗藍牙廠商nordic的nRF Connect SDK裸機選項方案
使用nRF52840芯片的USB Host 功能參考例程
講述Nordic nRF5 SDK的主要調試手段,以幫助大家快速定位問題
DFU協議簡介 NCS DFU升級步驟說明
基于XIAO nRF52840的鑰匙尋找器
nRF5 SDK軟件架構及softdevice工作原理
如何調試nRF5 SDK
nRF Connect SDK 使用 nPM2100 評估套件 (PCA10170) 為 nPM2100 電源管理 IC (PMIC) 的開發

nRF Connect SDK Basic
評論