转载自:博客园 |作者:zhengyi_hot
用户在使用 nRF connect SDK(NCS) 的时候经常会操作的外设有GPIO,I2C,SPI,UART。
我们就以 NCS 2.7.0 中的例程代码 nrf\samples\bluetooth\peripheral_lbs 为基础,来演示上述外设的简单使用。
使用的硬件是开发板 nRF52840 DK。
具体操作
准备工作
首先我们在原本的工程目录的 boards 文件夹里,添加文件 nrf52840dk_nrf52840.overlay。通过这个文件我们可以修改 devicetree。 编译完成后,我们可以查看 build\zephyr\zephyr.dts,以确认devicetree 的更改是否生效。
我们还可以通过修改 prj.conf 来修改 Kconfig。编译完成后,我们可以查看 build\zephyr\.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。
1. i2c 的时钟频率通过 clock-frequency 来定义。
2. i2c 的引脚通过 pinctrl-0 和 pinctrl-1 定义。我们将在后面分析 i2c1_default 和 i2c1_sleep 的定义。
3. 这两个 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>;
};
};
4. i2c1_default 和 i2c1_sleep的定义如下。TWIM_SDA 信号使用的是引脚 P0.04,TWIM_SCL 信号使用的是引脚 P0.03。
&pinctrl {
i2c1_default: i2c1_default {
group1 {
psels =
0 , 4)>,
0 , 3)>;};
};
i2c1_sleep: i2c1_sleep {
group1 {
psels =
0 , 4)>,
0 , 3)>;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。
1. 这里我们使用 spi2, 并且关闭 spi1。在 nordic 的nRF52 系列芯片中,相同数字编号的 TWIM, TWIS, SPIM, SPIS 是共用一组硬件模块的。在上面 i2c 中我们已经使用 i2c1, 所以这里我们就不能同时使用 spi1了。
2. cs-gpios 定义了 P0.26 和 P0.27 两 个CS 信号。 SPI 用不同的片选信号,区分不同的 slave 设备。
3. devicetree node spi2 下定义了两个 sub-node 分别是 user_spi_adc 和 user_spi_flash。 sub-node 里定义了三个 propertise。propertise compatible 的取值来自于我们在工程里新添加的文件 dts\bindings\spi-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 =
8 )>;};
user_spi_flash: user_spi_flash@0 {
compatible = "spi-user-define";
reg = <1>;
spi-max-frequency =
8 )>;};
};
4. 来看一下我们新添加的 dts\bindings\spi-user-define.yaml 里面的内容。如下图 spi-user-define.yaml 里面包含了 spi-device.yaml 文件,这个文件的位置在目录 zephyr\dts\bindings\spi 。
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:
5. SPI 引脚定义如下 CLK P0.28, MISO P0.29, MOSI P0.30。
spi2_default: spi2_default {
group1 {
psels =
0 , 28)>,
0 , 29)>,
0 , 30)>;};
};
spi2_sleep: spi2_sleep {
group1 {
psels =
0 , 28)>,
0 , 29)>,
0 , 30)>;low-power-enable;
};
};
修改 prj.conf 添加 CONFIG_SPI=y CONFIG_SPI_ASYNC=y。
在 main.c 里添加 SPI 的应用代码。下面这段代码定义了两个结构体变量,并通过宏 SPI_DT_SPEC_GET 用 devicetree 里的参数初始化了这两个结构体变量。
| 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 =
1 , 1)>;bias-pull-up;
};
group2 {
psels =
1 , 2)>;};
};
uart1_sleep: uart1_sleep {
group1 {
psels =
1 , 1)>,
1 , 2)>;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 %d\n", err);
err = uart_rx_enable(uart1, uart_rx_buf, MAX_UART_BUF_LEN, UART_RX_TIMEOUT_MS);
//printk("uart_rx_enable return %d\n", err);
err = uart_tx(uart1, uart_tx_buf, 6, SYS_FOREVER_MS);
//printk("uart_tx return %d\n", 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_DONE\n");
break;
case UART_RX_RDY:
printk("UART_RX_RDY\n");
printk("received %d bytes\n", evt->data.rx.len);
break;
case UART_RX_DISABLED:
printk("UART_RX_DISABLED\n");
break;
case UART_RX_BUF_REQUEST:
printk("UART_RX_BUF_REQUEST\n");
uart_rx_buf_rsp(uart1, uart_rx_buf2, MAX_UART_BUF_LEN);
break;
case UART_RX_BUF_RELEASED:
printk("UART_RX_BUF_RELEASED\n");
break;
case UART_TX_ABORTED:
printk("UART_TX_ABORTED\n");
break;
default:
break;
}
}
UART 应用代码的优化
上面的 uart 演示代码中,我们只实现了简单的收发。下面我们将进一步在此基础上优化 UART 的收发代码。这一部分的修改都在 main.c 里,主要涉及下面几个部分:
Thread 线程
Semaphore 信号量
线程间通讯 Message queue
线程 下面的代码中通过 K_THREAD_DEFINE 定义了 一个独立的线程来处理 uart 相关的代码。线程处理函数 uart_thread_task 中:也是先用 uart_callback_set 设置了回调函数;再用 uart_rx_enable 使能了接收;然后是一个 for 循环,在里面不断的接收消息,根据消息中的指令发送数据,或者处理接收到的数据。
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_task\n");
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 item\n");
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 函数。通过查询文件 build\zephyr\.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);
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_task\n");
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_task\n");
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 item\n");
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 的开发速度。
中文官网:www.nordicsemi.cn
英文官网:www.nordicsemi.com
微信公众号:nordicsemi
https://devzone.nordicsemi.com
北京分公司: +86 010 8438 2767
上海分公司: +86 21 6330 0620
深圳分公司: +86 755 8322 0147
sales.cn@nordicsemi.no
点击“阅读原文” 探索更多Nordic资讯