第6章:硬件适配层应用——串口
本章导读
前面 LED 是"灯"、按键是"键",这一章的串口是开发者的"眼睛和嘴巴"——程序跑得对不对、数据收到没有,全靠串口打印出来看。
Z-Stack 的 HAL 提供了完善的串口驱动 API(定义在 hal_uart.h 和 hal_uart.c 中)。本章先看 GEC2530 底板的串口硬件链路,再动手完成串口回环实验:电脑发什么,开发板原样发回来。
一、先看电路:GEC2530 底板的串口(对照原理图)
打开原理图 Sheet 3,串口部分以 MAX3232(U3) 为核心:
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_3 | UART_TX(发送) | → MAX3232 → DB9 / TTL 接口 |
| P0_2 | UART_RX(接收) | ← MAX3232 ← DB9 / TTL 接口 |
| P0_4 | UART_RTS(请求发送) | → MAX3232 |
| P0_5 | UART_CTS(允许发送) | ← MAX3232 |
看懂这个电路:
- 为什么要 MAX3232? CC2530 的串口输出是 TTL 电平(0~3.3V),而电脑串口(RS232)是 ±12V 电平,两者不能直接连。MAX3232 负责电平转换,两边才"听得懂";
- 两个对外接口:DB9(标准 RS232,接老式电脑串口)和 CON3(UART_TTL,接 USB-TTL 转换模块,比如 CH340)。现在的笔记本电脑没有 DB9 口,实验一般用 USB-TTL 模块接 CON3;
- 本实验不需要硬件流控(RTS/CTS),所以代码里
flowControl = FALSE。
⚠️ 接线提醒:USB-TTL 模块的 TX 接开发板的 RX(P0_2)、RX 接开发板的 TX(P0_3)——交叉连接,GND 共地。接反是串口实验最常见的问题。
二、串口配置:三步搞定
① 配置函数 zclSampleSw_InitUart()
在 zcl_samplesw.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() 中调用一次即可完成串口配置:
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 中定义的宏:
#define ZCLSAMPLESW_UART_BUF_LEN 128 // 接收缓冲区:128 字节三、接收串口数据:回调函数
当串口接收到数据时,HAL 会自动调用回调函数 zclSampleSw_UartCB。开发者在这个函数里接收并处理数据:
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 中添加:
HAL_UART=TRUE
INT_HEAP_LEN=2048
② 先搞清楚:堆和栈是程序的两块"内存仓库"
写程序时,内存不是"乱用的"——程序运行时主要用三块地盘:全局区、栈、堆。其中栈和堆是大家最容易搞混的两个,这里一次讲清楚。
栈(Stack):自动整理的叠盘子
栈是什么:程序运行中自动使用的一块内存,专门干三件事:
- 函数调用:调用函数时,返回地址、参数被"压栈";函数返回时"出栈";
- 局部变量:函数里声明的局部变量(比如
uint8 i;)就放在栈上; - 中断现场:中断发生时,CPU 先把现场(寄存器)压栈保存,处理完再弹出来恢复。
特点:
| 特点 | 说明 |
|---|---|
| 自动管理 | 不用你申请/释放,编译器自动"压栈/出栈" |
| 后进先出(LIFO) | 后压进去的先弹出来 |
| 容量小 | 8051 的栈空间有限(几百字节量级) |
| 速度最快 | 由 CPU 栈指针直接操作 |
生活类比:餐厅后厨叠盘子——后放的盘子先取走(后进先出)。函数 A 调用函数 B:B 的"盘子"压在最上面,B 执行完先"取走",A 才能继续。
栈溢出的后果:局部变量太多、函数嵌套太深(比如在中断里干重活),栈被"压爆"——程序莫名其妙跑飞、数据被破坏,而且很难排查。
堆(Heap):需要钥匙的储物柜
堆是什么:程序预留的一块内存池,供"动态申请"使用。Z-Stack 里就是 OSAL 动态内存池(第 3.4 节动态内存讲过):
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 内存很金贵),总量固定,此消彼长:
┌──────────────────────────────────────────────┐
│ 整个 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 配置里可以查看栈大小;程序跑飞、串口打印乱码时,优先检查是不是栈溢出。
五、调试仿真:跑起来看效果
按以下步骤进行调试仿真:
- 编译工程,使用仿真下载器把程序烧录到 GEC2530 开发板;
- 断开仿真下载器,用 USB-TTL 模块连接开发板的 CON3(UART_TTL):TX→P0_2、RX→P0_3、GND→GND;
- 打开串口助手(如友善串口调试助手),配置:波特率 115200、数据位 8、校验 None、停止位 1、流控 None;
- 通过串口助手发送字符串到开发板;
- 开发板收到字符串后,会原样发回到串口助手:

效果:在串口助手的发送框输入
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)的原因是?
内容来源:紫甲 IoT《ZigBee 3.0 开发指南》第三部分·第6章,经本站整理优化以便于理解,电路部分已对照 GEC2530 开发板底板原理图核对;原站链接