用户在使用 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"; }; }; };
const struct gpio_dt_spec user_gpio_0 = GPIO_DT_SPEC_GET(DT_NODELABEL(user_io0),gpios);
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)); }
/** * @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 = <I2C_BITRATE_STANDARD>; 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 = <NRF_PSEL(TWIM_SDA, 0, 4)>, <NRF_PSEL(TWIM_SCL, 0, 3)>; }; }; i2c1_sleep: i2c1_sleep { group1 { psels = <NRF_PSEL(TWIM_SDA, 0, 4)>, <NRF_PSEL(TWIM_SCL, 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。
- 这里我们使用 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 的取值来自于我们在工程里新添加的文件 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 = <DT_FREQ_M(8)>; }; user_spi_flash: user_spi_flash@0 { compatible = "spi-user-define"; reg = <1>; spi-max-frequency = <DT_FREQ_M(8)>; }; };
- 来看一下我们新添加的 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]
# 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 = <NRF_PSEL(SPIM_SCK, 0, 28)>, <NRF_PSEL(SPIM_MISO, 0, 29)>, <NRF_PSEL(SPIM_MOSI, 0, 30)>; }; }; spi2_sleep: spi2_sleep { group1 { psels = <NRF_PSEL(SPIM_SCK, 0, 28)>, <NRF_PSEL(SPIM_MISO, 0, 29)>, <NRF_PSEL(SPIM_MOSI, 0, 30)>; 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);
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"; };
uart1_default: uart1_default { group1 { psels = <NRF_PSEL(UART_RX, 1, 1)>; bias-pull-up; }; group2 { psels = <NRF_PSEL(UART_TX, 1, 2)>; }; }; uart1_sleep: uart1_sleep { group1 { psels = <NRF_PSEL(UART_RX, 1, 1)>, <NRF_PSEL(UART_TX, 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);
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; } }
- 我们在 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_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 被打印出来的。
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_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 (;;) {
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);
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)); }
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; } } }
总结
本文从实际操作出发,介绍了用户最常用的一些外设如 GPIO, I2C, SPI, UART 的配置和使用方法。并介绍了一些简单 RTOS 组件的应用如 thread, semaphore, message queue。希望能帮助 Nordic 用户加快 nRF Connect SDK 的开发速度。
标签:msgq,thread,spi,uart,nRF,UART,user,Basic,SDK From: https://www.cnblogs.com/victor-zheng/p/18470237