Skip to content

第6章:硬件适配层应用——串口 ​

本章导读

前面 LED 是"灯"、按键是"键",这一章的串口是开发者的"眼睛和嘴巴"——程序跑得对不对、数据收到没有,全靠串口打印出来看。

Z-Stack 的 HAL 提供了完善的串口驱动 API(定义在 hal_uart.h 和 hal_uart.c 中)。本章先看 GEC2530 底板的串口硬件链路,再动手完成串口回环实验:电脑发什么,开发板原样发回来。

一、先看电路:GEC2530 底板的串口(对照原理图) ​

打开原理图 Sheet 3,串口部分以 MAX3232(U3) 为核心:

text
CC2530 UART0                       MAX3232(U3)             对外接口
┌──────────┐   ┌────────────────┐   ┌──────────────┐
│ P0_3 = TX ├──→│ T1IN → T1OUT   ├──→│ DB9 (RS232)   │  ← 接电脑 RS232
│ P0_2 = RX ├←──│ R1OUT ← R1IN   ├←──│ CON3 (UART_TTL)│  ← 接 USB-TTL 模块
│ P0_4 = RTS├──→│ T2IN → T2OUT   ├──→│               │
│ P0_5 = CTS├←──│ R2OUT ← R2IN   ├←──│               │
└──────────┘   └────────────────┘   └──────────────┘

引脚对应关系(对照原理图 Sheet 3,串口区):

CC2530 引脚信号去向
P0_3UART_TX(发送)→ MAX3232 → DB9 / TTL 接口
P0_2UART_RX(接收)← MAX3232 ← DB9 / TTL 接口
P0_4UART_RTS(请求发送)→ MAX3232
P0_5UART_CTS(允许发送)← MAX3232

看懂这个电路:

  1. 为什么要 MAX3232? CC2530 的串口输出是 TTL 电平(0~3.3V),而电脑串口(RS232)是 ±12V 电平,两者不能直接连。MAX3232 负责电平转换,两边才"听得懂";
  2. 两个对外接口:DB9(标准 RS232,接老式电脑串口)和 CON3(UART_TTL,接 USB-TTL 转换模块,比如 CH340)。现在的笔记本电脑没有 DB9 口,实验一般用 USB-TTL 模块接 CON3;
  3. 本实验不需要硬件流控(RTS/CTS),所以代码里 flowControl = FALSE。

⚠️ 接线提醒:USB-TTL 模块的 TX 接开发板的 RX(P0_2)、RX 接开发板的 TX(P0_3)——交叉连接,GND 共地。接反是串口实验最常见的问题。

二、串口配置:三步搞定 ​

① 配置函数 zclSampleSw_InitUart() ​

在 zcl_samplesw.c 文件末尾定义串口配置函数:

c
static void zclSampleSw_InitUart(void) {
    halUARTCfg_t uartConfig;

    /* UART Configuration */
    uartConfig.configured = TRUE;            // 允许配置
    uartConfig.baudRate = HAL_UART_BR_115200;// 波特率:115200
    uartConfig.flowControl = FALSE;          // 关闭硬件流控
    uartConfig.flowControlThreshold = 0;     // 与流控相关(此处不用)
    uartConfig.rx.maxBufSize = ZCLSAMPLESW_UART_BUF_LEN; // 接收缓冲区大小
    uartConfig.tx.maxBufSize = 0;            // 发送缓冲区大小(不需要)
    uartConfig.idleTimeout = 6;              // 默认超时时间
    uartConfig.intEnable = TRUE;             // 使能中断
    uartConfig.callBackFunc = zclSampleSw_UartCB; // 设置回调函数

    /* Start UART */
    HalUARTOpen(HAL_UART_PORT_0, &uartConfig); // 根据配置打开串口0
}

② 在应用层初始化中调用 ​

在应用层初始化函数 zclSampleSw_Init() 中调用一次即可完成串口配置:

c
void zclSampleSw_Init(byte task_id) {
    // ……其他初始化……

    // Init Uart
    zclSampleSw_InitUart();   // ★ 配置并打开串口 0
}

③ 理解配置项 ​

配置项含义本实验取值
baudRate波特率:串口通信速度115200(与串口助手一致)
rx.maxBufSize接收缓冲区大小ZCLSAMPLESW_UART_BUF_LEN
tx.maxBufSize发送缓冲区大小0(不需要发送缓冲)
callBackFunc收到数据时调用的回调函数zclSampleSw_UartCB

其中 ZCLSAMPLESW_UART_BUF_LEN 是在 zcl_samplesw.c 中定义的宏:

c
#define ZCLSAMPLESW_UART_BUF_LEN 128   // 接收缓冲区:128 字节

三、接收串口数据:回调函数 ​

当串口接收到数据时,HAL 会自动调用回调函数 zclSampleSw_UartCB。开发者在这个函数里接收并处理数据:

c
static void zclSampleSw_UartCB(uint8 port, uint8 event) {
    // 获取当前串口接收缓冲区有多少字节的数据
    uint8 rxLen = Hal_UART_RxBufLen(HAL_UART_PORT_0);

    if (rxLen != 0) {  // 有数据
        // 从串口缓冲区中读取数据
        HalUARTRead(HAL_UART_PORT_0, zclSampleSw_UartBuf, rxLen);
        // 通过串口发送数据(把数据发回串口助手 → 形成回环)
        HalUARTWrite(HAL_UART_PORT_0, zclSampleSw_UartBuf, rxLen);
    }
}

三个串口操作 API:

API作用
Hal_UART_RxBufLen(port)查看当前串口接收缓冲区有多少字节数据
HalUARTRead(port, buf, len)从串口缓冲区中读取数据
HalUARTWrite(port, buf, len)通过串口发送数据(发到串口助手)

回环原理:电脑发字符串 → 串口收到 → 回调函数读出 → 原样写回 → 电脑串口助手显示。这就是"开发板的回声"。

四、启用串口:宏定义与堆大小 ​

① 启用 HAL_UART ​

使用串口前,需要启用宏 HAL_UART。在 IAR 工程 Options → C/C++ Compiler → Preprocessor → Defined symbols 中添加:

text
HAL_UART=TRUE
INT_HEAP_LEN=2048

串口宏定义配置

② 先搞清楚:堆和栈是程序的两块"内存仓库" ​

写程序时,内存不是"乱用的"——程序运行时主要用三块地盘:全局区、栈、堆。其中栈和堆是大家最容易搞混的两个,这里一次讲清楚。

栈(Stack):自动整理的叠盘子 ​

栈是什么:程序运行中自动使用的一块内存,专门干三件事:

  1. 函数调用:调用函数时,返回地址、参数被"压栈";函数返回时"出栈";
  2. 局部变量:函数里声明的局部变量(比如 uint8 i;)就放在栈上;
  3. 中断现场:中断发生时,CPU 先把现场(寄存器)压栈保存,处理完再弹出来恢复。

特点:

特点说明
自动管理不用你申请/释放,编译器自动"压栈/出栈"
后进先出(LIFO)后压进去的先弹出来
容量小8051 的栈空间有限(几百字节量级)
速度最快由 CPU 栈指针直接操作

生活类比:餐厅后厨叠盘子——后放的盘子先取走(后进先出)。函数 A 调用函数 B:B 的"盘子"压在最上面,B 执行完先"取走",A 才能继续。

栈溢出的后果:局部变量太多、函数嵌套太深(比如在中断里干重活),栈被"压爆"——程序莫名其妙跑飞、数据被破坏,而且很难排查。

堆(Heap):需要钥匙的储物柜 ​

堆是什么:程序预留的一块内存池,供"动态申请"使用。Z-Stack 里就是 OSAL 动态内存池(第 3.4 节动态内存讲过):

c
char *mem = osal_mem_alloc(32);   // 从堆里"租"32 字节
// ……用……
osal_mem_free(mem);               // 用完"还"回去(必须还!)

特点:

特点说明
手动管理自己 osal_mem_alloc 申请、osal_mem_free 释放
生命周期由你控制想存多久存多久,直到你释放
容量可配置大小由 INT_HEAP_LEN 宏决定
不释放会泄漏借了不还 = 内存泄漏,跑久就崩(3.4 铁律)

生活类比:仓库里的储物柜——需要时租(alloc)、用完必须还(free)。租期由你定,但不还柜子,仓库慢慢就满了。

堆 vs 栈:一张表看懂 ​

维度栈(Stack)堆(Heap)
谁管理编译器/CPU 自动你手动(alloc/free)
用途函数调用、局部变量、中断现场动态数据(消息、缓冲区)
存取方式后进先出(LIFO)任意顺序
容量由链接器配置(较小)由 INT_HEAP_LEN 配置
典型错误栈溢出(程序跑飞)内存泄漏(借了不还)
类比叠盘子储物柜

堆和栈共用同一块 RAM:此消彼长 ​

关键:堆和栈都在同一块 RAM 里(8051 内存很金贵),总量固定,此消彼长:

text
┌──────────────────────────────────────────────┐
│                整个 RAM(大小固定)             │
├──────────────┬───────────────┬───────────────┤
│   全局变量    │     堆        │     栈        │
│  (全局区)   │ INT_HEAP_LEN  │ (链接器配置) │
│    固定      │   可调        │   可调        │
└──────────────┴───────────────┴───────────────┘
       堆调大 → 栈变小;堆调小 → 栈变大

③ INT_HEAP_LEN 是什么?—— 串口要"吃"栈 ​

HEAP(堆) 的大小由宏 INT_HEAP_LEN 决定,程序动态申请内存(osal_mem_alloc)时就从这块堆里取。

  • 工程默认堆大小是 3071 字节;
  • 定义 INT_HEAP_LEN=2048 后,堆减少到 2048 字节;
  • 为什么减小? 串口通信功能(尤其是 printf 格式化输出、串口驱动的局部缓冲、中断处理)会消耗较多的栈空间。RAM 总量固定,为了让栈够用,就把堆"让"出一部分给栈。

类比:堆和栈是程序的两块"内存仓库",总量固定——串口功能需要大仓库(栈),就把小仓库(堆)缩一点,给大仓库腾地方。缩堆不是浪费,而是"匀内存"给更需要它的栈。

⚠️ 怎么确认栈够不够?IAR 的 Linker → Stack 配置里可以查看栈大小;程序跑飞、串口打印乱码时,优先检查是不是栈溢出。

五、调试仿真:跑起来看效果 ​

按以下步骤进行调试仿真:

  1. 编译工程,使用仿真下载器把程序烧录到 GEC2530 开发板;
  2. 断开仿真下载器,用 USB-TTL 模块连接开发板的 CON3(UART_TTL):TX→P0_2、RX→P0_3、GND→GND;
  3. 打开串口助手(如友善串口调试助手),配置:波特率 115200、数据位 8、校验 None、停止位 1、流控 None;
  4. 通过串口助手发送字符串到开发板;
  5. 开发板收到字符串后,会原样发回到串口助手:

串口助手回环效果

效果:在串口助手的发送框输入 1234567890dgfdh,点发送;接收区会立刻显示同样的字符串——回环成功!

六、小结 ​

问题答案
串口硬件链路?CC2530 UART0(P0_3 TX / P0_2 RX)→ MAX3232 电平转换 → DB9 / TTL
为什么要 MAX3232?CC2530 是 TTL 电平,电脑 RS232 是 ±12V,需要转换
怎么配置串口?填 halUARTCfg_t 结构体 → HalUARTOpen() 打开
怎么接收数据?注册回调函数,在回调里 Hal_UART_RxBufLen + HalUARTRead + HalUARTWrite
要定义哪些宏?HAL_UART=TRUE + INT_HEAP_LEN=2048
栈和堆有什么区别?栈=自动管理(函数调用/局部变量/中断,叠盘子);堆=手动管理(alloc/free,储物柜)
为什么减小堆?堆栈共用 RAM 此消彼长,串口/printf 吃栈,把堆缩小给栈腾地方

一句话总结:串口是协议栈的"调试窗口"——硬件上靠 MAX3232 做电平转换,软件上靠"配置 + 回调"两步就能收发。以后你看任何串口数据(组网日志、ZigBee 报文),都走这条链路。

课堂练习:串口 ​

依据第6章:串口链路、配置回调、堆栈概念作答。点击选项即可作答、即时反馈对错。

串口链路(MAX3232)、配置回调、堆栈概念

1 CC2530 UART0 的发送(TX)和接收(RX)引脚分别是?

2 为什么 CC2530 与电脑串口之间需要 MAX3232?

3 串口收到数据后,程序靠什么机制拿到数据?

4 栈(Stack)和堆(Heap)最本质的区别是?

5 串口工程里定义 INT_HEAP_LEN=2048(把堆从 3071 减小到 2048)的原因是?

已完成 0 / 5 题

内容来源:紫甲 IoT《ZigBee 3.0 开发指南》第三部分·第6章,经本站整理优化以便于理解,电路部分已对照 GEC2530 开发板底板原理图核对;原站链接

aioteer · 物联网专业课程群

💬AI 伴学配置