钛比科技 NB-IoT 开发板

Jun 28, 2026 · 6196 字

前阵子从朋友那薅来一块钛比科技的 NB-IoT 开发板,STM32F103ZET + 移远 BC95 模组架构。

什么是 NB-IoT

NB-IoT(Narrowband IoT)是 3GPP 在 Release 13 定义的一种低功耗广域网技术,直接跑在运营商授权频段上。跟 LoRa、Sigfox 这些非授权频段的方案相比,最大的优势是不用自己搭基站,插张物联网卡就能用,覆盖也比 2G/4G 更深——号称能穿两堵墙还有信号。

PSM 和 eDRX 两种省电模式是它跟传统蜂窝通信拉开差距的地方。PSM 模式下模组深度休眠,下行不可达但功耗能压到微安级;eDRX 则是在两次寻呼之间拉长间隔,兼顾省电和实时性。对电池供电的物联网终端来说,两个模式基本是标配。

华为和中国电信在这块的推动力度很大,国内 NB-IoT 网络覆盖已经比较成熟了。简单理解的话,它就是为水表、路灯、烟感这类低频小数据量场景量身定制的蜂窝通信方案。

硬件概览

板子核心是两颗芯片:STM32F103ZET6 做应用处理器,移远 BC95-B5 做 NB-IoT 通信模组。F103ZET6 是 Cortex-M3 内核,72MHz 主频,128KB Flash、20KB RAM,112 个 GPIO——在 F1 系列里算顶配了,串口、I2C、SPI、12 位 ADC 一应俱全。

BC95 模组通过 UART 跟主控通信,AT 指令集驱动。模组独立供电 3.8V,峰值电流能到 2A——NB-IoT 发射瞬间的电流尖峰确实不小,板子专门给模组做了单独一路 LDO 保证瞬态响应,这个细节处理得不错。

板载资源列一下:

  • 8 路拨码开关,拿来配硬件参数或切换启动模式
  • 3 个 LED、2 个复位按键
  • JTAG/SWD 调试口,接 ST-Link 就能在线调试
  • RS232 和 RS485 接口,工业场景能用上
  • SMA 天线接口,50Ω 阻抗匹配,接收灵敏度 -135dBm
  • 12V DC 供电,板上 LDO 分别输出 3.3V 和 3.8V

扩展方面,I2C 可以挂 SHT30 温湿度传感器,SPI 能接 LCD 或 LoRa 模块,可玩性还行。

开发环境

以前搞 STM32 基本绕不开 Keil,界面古老不说,编辑器体验属实折磨人。我之前写过一篇用 STM32CubeIDE for VSCode 开发的记录,这次继续这套流程。

简单说就是:STM32CubeMX 生成 CMake 工程(Toolchain 选 GCC),VSCode 装 STM32CubeIDE 插件包,打开项目目录自动识别,点一下转换就能编译调试。不用装 CubeIDE 本体,插件包自带了编译器和调试器,省掉好几个 GB 的 IDE 安装。

大致步骤:

  1. VSCode 扩展市场搜 STM32CubeIDE for VSCode 安装,左侧栏会多一个 STM32 图标
  2. CubeMX 里选 STM32F103ZET6,配好引脚和时钟,Project Manager 里 Toolchain/IDE 选 CMake
  3. Generate Code 后在 VSCode 打开项目文件夹,插件自动检测到 CMake 项目
  4. 左下角齿轮图标编译,运行和调试里选 ST-Link GDB Server 就能烧录调试

引脚配置不复杂:USART1(PA9/PA10)接 BC95 模组做 AT 指令通道,USART2(PA2/PA3)留作 printf 调试输出,SWD(PA13/PA14)接 ST-Link,再配一个 LED(PE5)做状态指示。

CubeMX 生成代码后,用 HAL_UARTEx_ReceiveToIdle_DMA 来收 BC95 的应答帧——这个 API 是 HAL 库比较新的接口,利用串口空闲中断自动检测帧尾,比老式的定长接收灵活太多。

AT 指令驱动 BC95 上云

玩 NB-IoT 绕不开的一件事就是用 AT 指令把模组怼上网,然后发数据到平台。我这次做的 Demo 不复杂,上电后 LED 慢闪表示正在注册网络,注册成功后 LED 常亮,每分钟通过 UDP 往服务器发一条模拟的温度上报。

AT 指令封装

BC95 的 AT 交互需要一套带超时的发送-应答机制。先发指令,然后在串口中断里攒应答数据,主循环轮询匹配关键字:

typedef enum {
  BC95_OK,
  BC95_TIMEOUT,
  BC95_ERROR
} BC95_Status;

#define BC95_RX_BUF_SIZE 512
static char bc95_rx_buf[BC95_RX_BUF_SIZE];
static volatile uint8_t bc95_rx_ready = 0;

BC95_Status BC95_SendCmd(const char *cmd, const char *expect,
                         uint32_t timeout_ms) {
  bc95_rx_ready = 0;
  memset(bc95_rx_buf, 0, BC95_RX_BUF_SIZE);

  HAL_UART_Transmit(&huart1, (uint8_t *)cmd, strlen(cmd), 100);
  HAL_UART_Transmit(&huart1, (uint8_t *)"\r\n", 2, 100);

  uint32_t tick = HAL_GetTick();
  while (HAL_GetTick() - tick < timeout_ms) {
    if (bc95_rx_ready) {
      bc95_rx_ready = 0;
      if (strstr(bc95_rx_buf, expect))
        return BC95_OK;
      if (strstr(bc95_rx_buf, "ERROR"))
        return BC95_ERROR;
    }
    HAL_Delay(10);
  }
  return BC95_TIMEOUT;
}

串口接收回调用 Idle 中断触发,DMA 循环接收:

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
  if (huart->Instance == USART1) {
    bc95_rx_ready = 1;
    bc95_rx_buf[Size] = '\0';
    HAL_UARTEx_ReceiveToIdle_DMA(&huart1, (uint8_t *)bc95_rx_buf,
                                  BC95_RX_BUF_SIZE);
  }
}

网络注册

BC95 上电后走一套标准注册流程。先检查 SIM 卡状态,再查信号和注册状态:

int BC95_Init(void) {
  // 测试通信
  if (BC95_SendCmd("AT", "OK", 2000) != BC95_OK) return -1;

  // 关闭回显,减少数据解析的噪音
  BC95_SendCmd("ATE0", "OK", 1000);

  // 查询 SIM 卡状态,READY 表示正常
  if (BC95_SendCmd("AT+CPIN?", "+CPIN: READY", 2000) != BC95_OK) {
    printf("SIM error\r\n");
    return -1;
  }

  // 查询信号质量
  BC95_SendCmd("AT+CSQ", "OK", 2000);

  // 查询网络注册状态,1=本地注册 5=漫游注册
  if (BC95_SendCmd("AT+CEREG?", "+CEREG: 0,1", 5000) != BC95_OK &&
      BC95_SendCmd("AT+CEREG?", "+CEREG: 0,5", 5000) != BC95_OK) {
    printf("Network registration failed\r\n");
    return -1;
  }

  // 获取模组 IP,确认附着成功
  BC95_SendCmd("AT+CGPADDR", "OK", 2000);

  printf("NB-IoT network ready\r\n");
  return 0;
}

有个坑值得提一下:AT+CSQ 返回的 +CSQ: 99,99 不一定表示没信号,BC95 在 PSM 唤醒后首次查信号经常返回 99,实际上网络是通的。判断联网与否还是以 AT+CEREG? 的注册状态和 AT+CGPADDR 获取到的 IP 为准。

UDP 数据发送

BC95 走 UDP 用 socket 方式操作,创建 socket、发包、关闭一套带走:

int BC95_SendUDP(const char *ip, uint16_t port,
                 const uint8_t *data, uint16_t len) {
  char cmd[256];
  char hex[2];

  // 创建 UDP socket,protocol=17 表示 UDP
  if (BC95_SendCmd("AT+NSOCR=DGRAM,17,0,1", "OK", 3000) != BC95_OK)
    return -1;

  // 拼装 AT+NSOST 指令,数据部分要转十六进制字符串
  int pos = snprintf(cmd, sizeof(cmd), "AT+NSOST=0,%s,%u,%u,",
                     ip, port, len);
  for (int i = 0; i < len; i++) {
    snprintf(hex, sizeof(hex), "%02X", data[i]);
    cmd[pos++] = hex[0];
    cmd[pos++] = hex[1];
  }
  cmd[pos] = '\0';

  if (BC95_SendCmd(cmd, "OK", 5000) != BC95_OK) {
    BC95_SendCmd("AT+NSOCL=0", "OK", 2000);
    return -1;
  }

  // 关闭 socket 释放资源
  BC95_SendCmd("AT+NSOCL=0", "OK", 2000);
  return 0;
}

AT+NSOCR 的几个参数需要留意:DGRAM 表示数据报类型,17 是 UDP 协议号,0 是本地端口(0 表示自动分配),1 是接收控制(不接收)。

主循环:定时上报

主循环里每秒自增计数器,满 60 秒就发一条 JSON 格式的温度数据:

int main(void) {
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART1_UART_Init();
  MX_USART2_UART_Init();

  // 启动 DMA 接收
  HAL_UARTEx_ReceiveToIdle_DMA(&huart1, (uint8_t *)bc95_rx_buf,
                                BC95_RX_BUF_SIZE);

  // LED 闪烁等待网络注册
  while (BC95_Init() != 0) {
    HAL_GPIO_TogglePin(GPIOE, GPIO_PIN_5);
    HAL_Delay(500);
    printf("Retrying network registration...\r\n");
  }

  // 常亮表示就绪
  HAL_GPIO_WritePin(GPIOE, GPIO_PIN_5, GPIO_PIN_SET);

  uint8_t counter = 0;
  while (1) {
    HAL_Delay(1000);
    counter++;

    if (counter >= 60) {
      counter = 0;
      float temp = 25.0f + (float)(HAL_GetTick() % 1000) / 100.0f;

      uint8_t payload[32];
      int len = snprintf((char *)payload, sizeof(payload),
                         "{\"temp\":%.1f}", temp);

      printf("Sending: %s\r\n", payload);
      if (BC95_SendUDP("xxx.xxx.xxx.xxx", 8888, payload, len) == BC95_OK) {
        printf("Send OK\r\n");
      } else {
        printf("Send failed\r\n");
      }
    }
  }
}

服务端用 netcat 开个 UDP 监听就能收到数据:

nc -u -l 8888

如果想接入正式平台,BC95 也支持 CoAP(AT+NCOAP 系列指令),可以直接怼华为 OceanConnect 或者电信 IoT 平台,注册设备后拿到 endpoint 和 token 就行。MQTT 的话 BC95 同样有 AT+QMTOPEN/AT+QMTCONN 这套指令,走阿里云 IoT、OneNET 都没问题,流程大同小异。

粤ICP备2025414119号 粤公网安备44030002006951号

© 2026 Saurlax · Powered by Astro