1. 项目概述:从零上手NRF51822的串口通信

搞嵌入式开发,尤其是蓝牙低功耗(BLE)应用,Nordic的NRF51822这颗芯片绝对是老朋友了。它集成了Cortex-M0内核和2.4GHz射频,在智能穿戴、物联网传感器节点等领域应用极广。但很多时候,我们第一步要做的不是搞复杂的无线协议,而是先把最基础、最“古老”的串口(UART)给调通。为啥?因为串口是我们和芯片“对话”、打印调试信息、接收上位机指令的最直接窗口。没有稳定的串口通信,后续的BLE配置、功能调试都会像盲人摸象。

我最近在做一个基于NRF51822的传感器数据采集项目,第一步就是搭建可靠的UART通信链路,把采集到的温湿度、电池电压等数据实时发送到PC端的串口助手,同时也能接收PC下发的控制指令。这个过程看似基础,但NRF51822的UART外设有些特性非常贴心,配置和使用上也有不少细节需要注意,直接照搬标准库函数可能会踩坑。今天,我就结合自己的实操代码和踩过的“坑”,把NRF51822的UART通信从硬件连接到软件驱动,再到实战调试,给你掰开揉碎了讲清楚。无论你是刚接触这颗芯片的新手,还是想优化现有通信代码的老鸟,相信都能找到有用的干货。

2. NRF51822 UART外设核心特性深度解析

在动手写代码之前,我们必须先吃透硬件。NRF51822的UART模块虽然标准,但Nordic给它赋予了一些非常灵活且实用的特性,理解这些特性是高效、稳定使用它的前提。

2.1 全双工与自动流控:解放CPU的利器

NRF51822的UART支持标准的 全双工异步通信 ,这意味着它可以同时进行发送(TX)和接收(RX),互不干扰。这对于需要频繁双向数据交换的应用(如AT指令模组控制)至关重要。

更值得一提的是其 硬件自动流控制(Hardware Flow Control) 功能。它通过RTS(Request To Send)和CTS(Clear To Send)两根信号线来实现。很多初级开发者会忽略这个功能,觉得麻烦,但在高速或大数据量通信时,它是保证数据不丢失的“保险丝”。

它是如何工作的? 当NRF51822(作为数据接收方)的接收缓冲区快满时,它会自动拉低RTS信号,告诉发送方(例如PC或另一个MCU):“我快忙不过来了,请暂停发送”。发送方检测到CTS信号变低后,就会暂停发送,直到CTS恢复为高。这个过程完全由硬件自动完成,不需要CPU干预,极大地减轻了软件负担,避免了因处理不及时而导致的数据覆盖丢失。在项目提供的代码中,作者提到“暂时不用到RTS和CTS”,这在低波特率、小数据包、非连续发送的场景下是可行的。但如果你需要以115200甚至更高的波特率持续传输大量数据,我强烈建议你启用硬件流控。

2.2 极致的引脚复用灵活性

这是NRF51822一个非常强大的优势: 多达32个GPIO口中的任意一个,都可以被配置为UART的TX或RX引脚 。这通过 PSELTXD PSELRXD 寄存器来设置。

为什么这个特性如此重要?

  1. PCB布局自由度大增 :你不再需要为了迁就固定的UART引脚而把芯片放在一个别扭的位置,或者绕很长的线。你可以选择离连接器(如USB转串口芯片)最近、布线最顺的引脚。
  2. 动态重映射 :你甚至可以在运行时根据不同的工作模式切换UART引脚。例如,在正常模式使用一组引脚连接主控,在固件升级(DFU)模式切换到另一组引脚连接编程器。
  3. 冲突规避 :当某个默认UART引脚与其他重要功能(如某个关键的ADC输入或PWM输出)冲突时,你可以轻松地将UART换到别的引脚上,而无需重新设计电路。

在提供的 simple_uart_config 函数中,正是通过 NRF_UART0->PSELTXD = txd_pin_number; NRF_UART0->PSELRXD = rxd_pin_number; 这两行代码实现了引脚的动态指定。

2.3 奇偶校验与错误处理

NRF51822的UART内置了奇偶校验生成与检查功能。奇偶校验是一种简单的检错机制,可以检测出传输过程中单个位的错误。你可以在配置时选择奇校验、偶校验或无校验。

关键点在于“自动” :一旦使能了奇偶校验,硬件会自动在发送的每个数据帧末尾添加校验位,并在接收时自动检查。如果接收端检测到奇偶校验错误,会置位相应的错误标志位(如 ERRORSRC 寄存器中的 PARITY 位)。在提供的示例代码中,并没有开启奇偶校验( PARITY 寄存器未配置,默认为禁用)。对于要求高可靠性的通信,例如工业环境,开启奇偶校验是一个低成本的有效措施。你需要添加如下配置:

NRF_UART0->CONFIG |= (UART_CONFIG_PARITY_Included << UART_CONFIG_PARITY_Pos);

注意 :启用奇偶校验会增加每个数据帧的传输时间(多了一个位),并且需要通信双方(发送和接收设备)配置完全一致的校验方式,否则所有数据都会因校验错误而被丢弃。

3. 硬件连接与电平转换电路设计

软件跑得再溜,硬件连接不对也是白搭。NRF51822的GPIO口是 3.3V CMOS电平 ,而标准的RS-232电平是±3V到±15V。因此,直接连接到PC的COM口(RS-232)会损坏芯片!我们必须进行电平转换。

3.1 经典方案:MAX232芯片

项目资料中提到了MAX232,这是一款非常经典且廉价的RS-232电平转换芯片。它内部有电荷泵,仅需+5V单电源即可产生RS-232所需的正负电压,非常方便。

接线详解(以项目中的示意图为例):

  • NRF51822 TX (Pin 9) -> MAX232的T1IN引脚 。这里要注意逻辑:NRF51822的TX(发送数据)端,应该连接到MAX232的“TTL/CMOS电平输入”端,即T1IN。
  • MAX232的T1OUT引脚 -> DB9母头的Pin 2 (RXD) 。T1OUT输出的是RS-232电平,应连接到PC串口线的接收端。
  • NRF51822 RX (Pin 10) -> MAX232的R1OUT引脚 。NRF51822的RX(接收数据)端,应连接到MAX232的“TTL/CMOS电平输出”端,即R1OUT。
  • MAX232的R1IN引脚 -> DB9母头的Pin 3 (TXD) 。R1IN接收来自PC的RS-232电平,应连接到PC串口线的发送端。

所以,资料中的“RX (NRF51822-Pin 9)——MAX232-TX”表述可能容易引起歧义。更准确的描述是: NRF51822的TX引脚连接至MAX232的TTL侧输入(T1IN);NRF51822的RX引脚连接至MAX232的TTL侧输出(R1OUT) 。MAX232的“TX”/“RX”通常指其RS-232侧。

3.2 现代简易方案:USB转TTL串口模块

对于现在的开发者和笔记本电脑(大多已没有原生串口),更常用的方案是使用 USB转TTL串口模块 (如基于CH340、CP2102、FT232等芯片的模块)。这种模块直接输出3.3V TTL电平,可以与NRF51822直连,无需MAX232。

接线方法(极其简单):

  • 模块的3.3V -> NRF51822的VDD (如果模块供电可靠)
  • 模块的GND -> NRF51822的GND (必须共地!)
  • 模块的TX -> NRF51822的RX (数据从模块发往芯片)
  • 模块的RX -> NRF51822的TX (数据从芯片发往模块)

实操心得 :强烈推荐使用USB转TTL模块,它省去了额外电源和DB9接头,连接简单,且通常兼容3.3V/5V。购买时请确认模块支持3.3V电平输出。连接前,务必用万用表测量一下模块TX引脚的空载电压,确保是3.3V而非5V,以防损坏NRF51822。

3.3 电源与去耦

无论采用哪种方案,良好的电源去耦都必不可少。在NRF51822的VDD和GND引脚附近,一定要放置一个 0.1μF的陶瓷电容 ,并尽量靠近芯片引脚。如果使用有源晶振,还需为晶振电源引脚添加去耦电容。稳定的电源是高速数字通信(即使38400波特率不算很高)的基础,能有效减少因电源噪声导致的数据错误。

4. 软件驱动层代码逐行剖析与优化

理解了硬件,我们再来深度剖析项目提供的驱动代码,并讨论如何将其优化得更健壮、更实用。

4.1 基础配置函数 simple_uart_config

这是UART的初始化核心。我们逐行分析:

void simple_uart_config(uint8_t txd_pin_number, uint8_t rxd_pin_number) {
    // 1. 配置GPIO引脚模式
    nrf_gpio_cfg_output(txd_pin_number); // TX引脚配置为输出
    nrf_gpio_cfg_input(rxd_pin_number, NRF_GPIO_PIN_NOPULL); // RX引脚配置为输入,无上拉/下拉

    // 2. 绑定引脚到UART外设
    NRF_UART0->PSELTXD = txd_pin_number;
    NRF_UART0->PSELRXD = rxd_pin_number;

    // 3. 配置波特率
    NRF_UART0->BAUDRATE = (UART_BAUDRATE_BAUDRATE_Baud38400 << UART_BAUDRATE_BAUDRATE_Pos);

    // 4. 使能UART
    NRF_UART0->ENABLE = (UART_ENABLE_ENABLE_Enabled << UART_ENABLE_ENABLE_Pos);

    // 5. 启动发送和接收任务
    NRF_UART0->TASKS_STARTTX = 1;
    NRF_UART0->TASKS_STARTRX = 1;

    // 6. 清除可能存在的旧事件标志
    NRF_UART0->EVENTS_RXDRDY = 0;
}

优化与注意事项:

  • 波特率选择 :示例使用了38400。常见的还有9600, 19200, 115200等。 UART_BAUDRATE_BAUDRATE_Baud115200 是Nordic SDK中定义好的宏。确保与PC端串口工具设置的波特率完全一致,否则收到的是乱码。
  • 引脚配置顺序 :先配置GPIO模式,再赋值给 PSEL 寄存器,这是一个好习惯。
  • 缺少关键配置 :这个函数没有配置数据位、停止位、奇偶校验。它依赖于硬件的默认状态(通常是8位数据位,1位停止位,无校验)。为了代码清晰和可移植,建议显式配置 CONFIG 寄存器:
    NRF_UART0->CONFIG = (UART_CONFIG_HWFC_Disabled << UART_CONFIG_HWFC_Pos) |
                        (UART_CONFIG_PARITY_Excluded << UART_CONFIG_PARITY_Pos) |
                        (UART_CONFIG_STOP_One << UART_CONFIG_STOP_Pos);
    

4.2 阻塞式发送与接收函数

提供的 simple_uart_put simple_uart_get 是典型的 阻塞式(Polling) 函数。

  • simple_uart_put :将数据写入 TXD 寄存器,然后循环查询 EVENTS_TXDRDY 事件,直到硬件发送完成。在此期间,CPU被完全占用,无法执行其他任务。
  • simple_uart_get :循环查询 EVENTS_RXDRDY 事件,直到收到一个字节。这是“死等”,如果一直没有数据到来,程序就会卡死在这里。

阻塞式函数的优缺点:

  • 优点 :代码简单直观,易于理解。
  • 缺点 :效率极低,严重浪费CPU资源,在实时性要求高的系统中不可接受。

4.3 带超时的接收函数 simple_uart_get_with_timeout

这个函数是对纯阻塞式接收的一个改进。它引入了一个超时机制,如果在一定时间( timeout_ms 毫秒)内没有收到数据,函数会返回 false ,而不是永远等待。

代码逻辑分析:

  1. 函数进入一个 while 循环,查询 EVENTS_RXDRDY
  2. 每次循环,检查 timeout_ms 是否大于等于0。如果是,则延时1毫秒( nrf_delay_us(1000) ),然后 timeout_ms 自减。
  3. 如果在超时发生前 EVENTS_RXDRDY 变为1,则跳出循环,清除事件,读取数据,返回 true
  4. 如果超时( timeout_ms 减为负),则跳出循环,返回 false

这个实现存在一个严重问题: nrf_delay_us(1000) 是一个 忙等待 延时函数,它依然会阻塞CPU。这意味着在等待超时的过程中,CPU什么也做不了,只是空转。这并没有从根本上解决阻塞式IO的效率问题。

更优的超时处理思路(非阻塞结合定时器): 真正的超时应该基于硬件定时器。我们可以采用“中断+软件超时判断”或“定时器硬件超时”的方式。一个更实用的非阻塞架构是:

  1. 在UART接收中断服务程序(ISR)中,将收到的字节存入一个环形缓冲区(FIFO)。
  2. 应用层的 get 函数只是从这个环形缓冲区中取数据。
  3. 如果需要超时,可以记录调用 get 函数时的时间戳,然后在一个非阻塞的主循环中,检查当前时间与时间戳的差值是否超过设定值,同时检查缓冲区是否有数据。

4.4 中断驱动与环形缓冲区:工业级解决方案

对于任何严肃的嵌入式项目,我都推荐使用 中断驱动+环形缓冲区 的UART驱动模型。这才是解放CPU、实现可靠高效通信的正道。

核心架构:

  1. 初始化 :配置UART引脚、波特率等, 使能接收中断 NRF_UART0->INTENSET = UART_INTENSET_RXDRDY_Msk; )。
  2. 中断服务程序(ISR)
    void UART0_IRQHandler(void) {
        if (NRF_UART0->EVENTS_RXDRDY) {
            NRF_UART0->EVENTS_RXDRDY = 0;
            uint8_t data = (uint8_t)NRF_UART0->RXD;
            // 将数据data写入环形缓冲区(注意处理缓冲区满的情况)
            ring_buffer_write(&rx_buffer, data);
        }
        // 还可以处理发送完成中断、错误中断等
    }
    
  3. 发送函数 :可以采用查询或中断方式。中断方式更高效:将待发送数据放入发送环形缓冲区,在发送完成中断( TXDRDY )中从缓冲区取出下一个字节发送。
  4. 应用层API
    • uart_get_char(uint8_t *ch) : 尝试从接收环形缓冲区读取一个字节,成功返回true,缓冲区空则返回false。 非阻塞
    • uart_send_string(const char *str) : 将字符串放入发送缓冲区,并触发发送(如果发送器空闲)。

环形缓冲区的实现: 这是一个经典的“生产者-消费者”模型。UART接收中断是生产者,向缓冲区写数据;应用层是消费者,从缓冲区读数据。需要两个索引(写索引 write_idx 和读索引 read_idx )和一个固定大小的数组。关键操作是判断缓冲区空和满的条件,通常使用“留一空位”法来区分。

避坑技巧 :在中断服务程序(ISR)中操作环形缓冲区时,如果主程序也会访问这个缓冲区(比如在 uart_get_char 中读),那么就需要考虑 临界区保护 。对于Cortex-M0,在ISR中操作是安全的,因为ISR会打断主程序。但更严谨的做法是,在非ISR的读写操作前暂时关闭全局中断( __disable_irq() ),操作完成后立即开启( __enable_irq() ),以防止在非原子操作期间被中断打断导致数据错乱。

5. 实战:构建一个健壮的UART通信框架

让我们基于中断和环形缓冲区,重新构建一个更健壮、可用的UART驱动框架。这里我会给出核心代码片段和设计思路。

5.1 数据结构定义与初始化

首先,定义环形缓冲区和相关的控制结构。

// uart_driver.h
#ifndef UART_DRIVER_H
#define UART_DRIVER_H

#include <stdbool.h>
#include <stdint.h>

#define UART_RX_BUFFER_SIZE 256
#define UART_TX_BUFFER_SIZE 256

void uart_init(uint32_t baudrate, uint8_t tx_pin, uint8_t rx_pin);
bool uart_get_char(uint8_t *ch);
void uart_put_char(uint8_t ch);
void uart_send_string(const char *str);
bool uart_is_tx_busy(void);

#endif // UART_DRIVER_H
// uart_driver.c
#include "uart_driver.h"
#include "nrf.h"
#include "nrf_gpio.h"

// 环形缓冲区结构体
typedef struct {
    uint8_t buffer[UART_RX_BUFFER_SIZE];
    volatile uint16_t head; // 写索引(由中断修改)
    volatile uint16_t tail; // 读索引(由应用修改)
} ring_buffer_t;

static ring_buffer_t rx_buffer = { .head = 0, .tail = 0 };
static ring_buffer_t tx_buffer = { .head = 0, .tail = 0 };
static volatile bool tx_in_progress = false;

// 环形缓冲区辅助函数(静态,内部使用)
static bool rb_is_empty(ring_buffer_t *rb) {
    return (rb->head == rb->tail);
}

static bool rb_is_full(ring_buffer_t *rb) {
    return (((rb->head + 1) % UART_RX_BUFFER_SIZE) == rb->tail);
}

static void rb_write(ring_buffer_t *rb, uint8_t data) {
    if (!rb_is_full(rb)) {
        rb->buffer[rb->head] = data;
        rb->head = (rb->head + 1) % UART_RX_BUFFER_SIZE;
    } else {
        // 缓冲区满,数据丢失!这里可以增加错误计数或触发回调
    }
}

static bool rb_read(ring_buffer_t *rb, uint8_t *data) {
    if (!rb_is_empty(rb)) {
        *data = rb->buffer[rb->tail];
        rb->tail = (rb->tail + 1) % UART_RX_BUFFER_SIZE;
        return true;
    }
    return false;
}

5.2 中断服务程序与核心驱动函数

接下来是实现中断服务和核心API。

// UART初始化
void uart_init(uint32_t baudrate, uint8_t tx_pin, uint8_t rx_pin) {
    // 1. 配置GPIO
    nrf_gpio_cfg_output(tx_pin);
    nrf_gpio_cfg_input(rx_pin, NRF_GPIO_PIN_NOPULL);

    // 2. 断开引脚(防止干扰),再连接
    NRF_UART0->PSELTXD = 0xFFFFFFFF; // 断开
    NRF_UART0->PSELRXD = 0xFFFFFFFF;
    NRF_UART0->PSELTXD = tx_pin;
    NRF_UART0->PSELRXD = rx_pin;

    // 3. 配置波特率、数据格式
    NRF_UART0->BAUDRATE = baudrate;
    NRF_UART0->CONFIG = (UART_CONFIG_HWFC_Disabled << UART_CONFIG_HWFC_Pos) |
                        (UART_CONFIG_PARITY_Excluded << UART_CONFIG_PARITY_Pos);

    // 4. 使能中断
    NRF_UART0->INTENSET = UART_INTENSET_RXDRDY_Msk; // 使能接收中断
    NVIC_EnableIRQ(UART0_IRQn); // 使能NVIC中的UART0中断

    // 5. 使能UART并启动收发
    NRF_UART0->ENABLE = UART_ENABLE_ENABLE_Enabled;
    NRF_UART0->TASKS_STARTRX = 1;
    NRF_UART0->TASKS_STARTTX = 1; // 启动发送器,等待数据
}

// UART0中断处理函数
void UART0_IRQHandler(void) {
    // 处理接收中断
    if (NRF_UART0->EVENTS_RXDRDY) {
        NRF_UART0->EVENTS_RXDRDY = 0;
        uint8_t data = (uint8_t)NRF_UART0->RXD;
        rb_write(&rx_buffer, data); // 写入接收缓冲区
    }

    // 处理发送完成中断
    if (NRF_UART0->EVENTS_TXDRDY) {
        NRF_UART0->EVENTS_TXDRDY = 0;
        uint8_t next_byte;
        if (rb_read(&tx_buffer, &next_byte)) {
            // 发送缓冲区还有数据,发送下一个字节
            NRF_UART0->TXD = next_byte;
        } else {
            // 发送缓冲区空,停止发送任务,标志位置为空闲
            tx_in_progress = false;
        }
    }
}

// 应用层API:尝试读取一个字节(非阻塞)
bool uart_get_char(uint8_t *ch) {
    bool ret;
    // 短暂关闭中断,确保读索引操作的原子性
    __disable_irq();
    ret = rb_read(&rx_buffer, ch);
    __enable_irq();
    return ret;
}

// 应用层API:发送一个字节
void uart_put_char(uint8_t ch) {
    // 将数据放入发送缓冲区
    bool start_tx = false;
    __disable_irq();
    if (!rb_is_full(&tx_buffer)) {
        rb_write(&tx_buffer, ch);
        if (!tx_in_progress) {
            start_tx = true;
            tx_in_progress = true;
        }
    } else {
        // 发送缓冲区满,处理策略:可以等待或丢弃。这里简单丢弃新数据。
    }
    __enable_irq();

    // 如果发送器空闲,则启动第一次发送
    if (start_tx) {
        uint8_t first_byte;
        __disable_irq();
        rb_read(&tx_buffer, &first_byte); // 刚写入的字节
        __enable_irq();
        NRF_UART0->TXD = first_byte;
    }
}

// 发送字符串
void uart_send_string(const char *str) {
    while (*str) {
        uart_put_char(*str++);
    }
}

5.3 在主循环中的应用示例

使用这个新的驱动框架,你的主程序将变得非常简洁高效:

#include "uart_driver.h"
#include "nrf_delay.h"

int main(void) {
    // 初始化UART,波特率115200,使用P0.09作TX,P0.10作RX
    uart_init(UART_BAUDRATE_BAUDRATE_Baud115200, 9, 10);
    uart_send_string("System Booted.\r\n");

    while (1) {
        uint8_t received_char;
        // 非阻塞检查并处理接收到的字符
        if (uart_get_char(&received_char)) {
            // 回显接收到的字符
            uart_put_char(received_char);
            // 如果是回车或换行,额外发送一个换行
            if (received_char == '\r' || received_char == '\n') {
                uart_put_char('\n');
            }
        }

        // 这里可以放心地执行其他任务,如传感器采样、LED闪烁、算法处理等
        // UART通信在后台由中断自动处理,完全不会阻塞这里
        nrf_delay_ms(100); // 模拟其他任务
        // ... 其他代码
    }
}

这个框架的优势在于,主循环 while(1) 不再被UART的 while 等待循环阻塞,可以高效地执行其他任务。所有接收到的数据都被安全地缓存在环形缓冲区中,等待主循环处理。

6. 调试技巧与常见问题排查实录

即使代码写得再漂亮,实际调试中总会遇到各种问题。下面是我在调试NRF51822 UART时积累的一些常见问题及解决方法。

6.1 问题速查表

现象 可能原因 排查步骤与解决方法
完全无数据收发 1. 电源未接通或电压不对。
2. 接线错误(TX/RX接反)。
3. 引脚配置错误(未正确映射PSEL)。
4. UART外设未使能( ENABLE 寄存器)。
1. 用万用表测量NRF51822的VDD是否为3.3V,GND是否连通。
2. 重点检查 :确保MCU的TX接转换模块的RX,MCU的RX接转换模块的TX。这是最容易出错的地方。
3. 检查 simple_uart_config uart_init 中传入的引脚编号是否正确,并用示波器或逻辑分析仪测量该引脚是否有波形。
4. 确认代码中执行了 NRF_UART0->ENABLE = UART_ENABLE_ENABLE_Enabled;
收到乱码 1. 波特率不匹配 (最常见)。
2. 数据格式不匹配(数据位、停止位、校验位)。
3. 电源噪声大,信号质量差。
1. 双盲检查 :确保代码中设置的波特率(如 115200 )与PC端串口工具(如Putty、SecureCRT)设置的波特率 完全一致 ,一个数字都不能错。
2. 检查 CONFIG 寄存器设置,默认通常是8N1(8数据位,无校验,1停止位),与串口工具设置一致。
3. 检查电源去耦电容是否焊接良好,信号线是否过长或靠近干扰源。尝试降低波特率(如降到9600)测试。
只能发送不能接收,或反之 1. 单向接线错误或虚焊。
2. 中断配置错误(仅影响接收)。
3. 对方设备未发送/接收。
1. 分别检查TX和RX通路。对于接收问题,可以尝试让MCU自发自收(将MCU的TX引脚用杜邦线短接到RX引脚),发送一段数据看是否能收到自己发出的,以此隔离对方设备的问题。
2. 如果使用中断接收,确认 INTENSET 寄存器已使能 RXDRDY ,且NVIC中断已开启( NVIC_EnableIRQ(UART0_IRQn) )。
3. 确认PC端串口工具已正确打开串口,且“流控制”选项设置为“无”(除非你使用了RTS/CTS)。
通信一段时间后死机或不稳定 1. 接收缓冲区溢出(未及时读取)。
2. 中断服务程序处理时间过长或发生重入。
3. 堆栈溢出。
1. 如果使用查询式,确保主循环频率足够高能及时读取数据。如果使用中断+缓冲区,检查缓冲区大小是否足够,并确保应用层能及时消费数据。
2. 确保中断服务程序尽可能短小,只做最必要的操作(如存数据、清标志)。避免在ISR中调用可能阻塞或耗时的函数(如 printf )。
3. 在调试器中检查堆栈使用情况,适当增加堆栈大小。
使用printf重定向后不正常 1. 底层 fputc _write 函数实现有误。
2. 半主机(Semihosting)未正确关闭。
1. 确保你重定向的 fputc 函数最终调用了正确的UART发送函数(如 uart_put_char )。
2. 对于ARM Cortex-M,在使用标准库 printf 时,可能需要禁用半主机。在工程设置中添加编译宏 --specs=nosys.specs 或实现 _sys_... 系列系统调用。

6.2 高级调试工具:逻辑分析仪

当软件排查无法定位问题时,硬件工具就派上用场了。一个哪怕是最基础的逻辑分析仪(比如Saleae Logic 8或国产的诸多型号),都能极大地提升调试效率。

如何使用逻辑分析仪调试UART:

  1. 连接 :将逻辑分析仪的通道1和通道2分别连接到NRF51822的TX和RX引脚,并共地。
  2. 设置 :在逻辑分析仪软件中,添加“异步串行”(UART)解码器。设置正确的波特率、数据位、停止位、校验位。
  3. 抓取 :让系统运行,触发抓取波形。
  4. 分析
    • 看TX线 :是否有波形?波形是否符合UART标准(起始位低电平,停止位高电平)?解码出的数据是否是你代码期望发送的?
    • 看RX线 :当你在PC端串口工具发送数据时,NRF51822的RX引脚上是否有波形?波形是否规整?
    • 对比 :将TX和RX波形放在一起看,可以清晰看到通信的全双工过程。

通过逻辑分析仪,你可以直观地确认:硬件连接是否导通、信号电平是否正确、波特率是否准确、数据内容是否符合预期。这能直接区分是软件问题还是硬件问题。

6.3 关于功耗的考量

NRF51822的核心优势是低功耗。UART外设在工作时功耗相对射频部分较小,但仍需注意:

  • 及时关闭 :如果项目中有长时间不需要UART通信的睡眠阶段,务必在进入低功耗模式前禁用UART外设( NRF_UART0->ENABLE = UART_ENABLE_ENABLE_Disabled; ),并可能将相关GPIO配置为输入模式并上拉/下拉,以减少漏电流。
  • 唤醒源 :NRF51822的UART本身不能作为唤醒源(从System OFF模式唤醒)。如果你需要在睡眠时通过串口唤醒,通常需要将RX引脚配置为GPIO中断,检测起始位的下降沿来唤醒MCU,然后MCU再初始化UART进行通信。这部分设计需要仔细规划。

从最初简单的阻塞式查询,到后来基于中断和环形缓冲区的非阻塞驱动,这个过程中对芯片外设的理解、对实时系统设计的思考都在不断加深。对于NRF51822这类资源有限的MCU,一个好的驱动设计就是在资源、效率和复杂度之间找到最佳平衡点。希望我分享的这些代码框架和调试经验,能让你在下次遇到串口问题时,少走些弯路,更快地让数据流畅地跑起来。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐