3、UART串口通信驱动开发
UART串口,搞嵌入式的人天天打交道。说它是物联网设备最基础的通信方式,一点不过分。我最早接触鸿蒙南向开发时,第一个调通的外设就是UART。为什么?因为串口控制台是调试的命脉,你连log都打不出来,后面还怎么玩?
这一章,咱们就把UART从协议到驱动,从配置到实战,彻底捋一遍。
3.1 UART协议基础
UART全称是Universal Asynchronous Receiver/Transmitter,通用异步收发器。说白了,就是一根线发数据,一根线收数据,再加一根地线,三根线就能通信。
为什么叫异步?因为没有独立的时钟线。收发双方靠约定的波特率来同步。你想想看,两个人约好每秒说多少个字,不用看表也知道对方什么时候开口。
| 参数 | 说明 | 常见值 |
|---|---|---|
| 波特率 | 每秒传输的符号数 | 9600、115200 |
| 数据位 | 一帧中有效数据的位数 | 8位(最常用) |
| 停止位 | 帧结束标志位 | 1位、2位 |
| 校验位 | 可选奇偶校验 | 无、奇校验、偶校验 |
一帧UART数据长这样:起始位(低电平)+ 数据位(LSB或MSB优先)+ 可选的校验位 + 停止位(高电平)。空闲时,TX线保持高电平。
3.2 鸿蒙UART驱动框架
鸿蒙的UART驱动框架,走的是HDF(Hardware Driver Foundation)路线。你不需要直接操作寄存器,框架帮你把硬件抽象好了。
整体架构分三层:
- 应用层:通过POSIX接口或HDF IO Service调用
- 驱动框架层:HDF核心、UART控制器管理、回调注册
- 硬件适配层:芯片厂商实现的Platform驱动,操作具体寄存器
我刚开始看HDF框架时,觉得层数太多。后来发现,这种分层在芯片换型时特别香——你只需要重写最底层的适配代码,上层完全不用动。
3.3 串口收发数据实现
在鸿蒙上操作UART,最直接的方式是用HDF提供的UART接口。我习惯先调通一个简单的回环测试——发什么收什么,确认硬件链路没问题。
核心步骤:
- 通过HDF获取UART设备句柄
- 配置波特率、数据格式
- 注册接收回调或轮询读取
- 调用发送接口写数据
下面是一个简化的收发示例:
#include "hdf_io_service.h"
#include "uart_if.h"
// 获取UART设备
struct UartHost *uartHost = UartOpen(0);
if (uartHost == NULL) {
printf("UART open failed\n");
return -1;
}
// 配置波特率115200,8N1
struct UartAttribute attr;
attr.baudRate = 115200;
attr.dataBits = UART_ATTR_DATABIT_8;
attr.parity = UART_ATTR_PARITY_NONE;
attr.stopBits = UART_ATTR_STOPBIT_1;
UartSetAttribute(uartHost, &attr);
// 发送数据
uint8_t txBuf[] = "Hello UART\n";
int32_t ret = UartWrite(uartHost, txBuf, sizeof(txBuf));
// 接收数据(轮询方式)
uint8_t rxBuf[64];
int32_t len = UartRead(uartHost, rxBuf, sizeof(rxBuf));
3.4 波特率与数据格式配置
波特率配置是UART驱动里最容易出问题的地方。为什么?因为实际波特率和理论值往往有偏差,取决于时钟源和分频系数。
鸿蒙的UART框架里,UartSetAttribute会帮你做分频计算。但如果你用的芯片时钟比较特殊,建议手动算一下实际波特率误差。
| 目标波特率 | 时钟频率(MHz) | 分频系数 | 实际波特率 | 误差 |
|---|---|---|---|---|
| 115200 | 16 | 139 | 115107 | 0.08% |
| 9600 | 16 | 1667 | 9598 | 0.02% |
| 115200 | 24 | 208 | 115384 | 0.16% |
误差在2%以内通常没问题。超过2%,连续收几个字节就可能错位。我遇到过一块开发板,串口芯片用的外部晶振不准,115200下误差到了3.5%,收长数据包必丢字节。后来降到57600才稳定。
数据格式配置要点:
- 数据位:8位最通用,7位用于某些老协议
- 校验位:无校验最省事,奇偶校验能检测单bit错误
- 停止位:1位足够,2位用于慢速设备或长距离
3.5 实战:串口控制台交互
理论说完了,咱们动手做一个串口控制台。功能很简单:通过串口输入命令,设备解析并返回结果。这是物联网设备调试的标配功能。
设计思路:
- UART中断接收,逐字节存入环形缓冲区
- 检测到换行符(\n或\r\n)时,提取一行命令
- 解析命令字符串,匹配预定义命令表
- 执行对应函数,通过UART发送返回结果
命令表结构:
typedef struct {
const char *cmd;
void (*handler)(char *args);
} CmdEntry;
static CmdEntry g_cmdTable[] = {
{"help", CmdHelp},
{"version", CmdVersion},
{"reset", CmdReset},
{"gpio", CmdGpio},
{"adc", CmdAdc},
{NULL, NULL}
};
主循环里这样处理:
while (1) {
// 从环形缓冲区取一行
char *line = GetLineFromRingBuffer();
if (line == NULL) {
continue;
}
// 去掉尾部换行符
TrimNewline(line);
// 查找命令表
for (int i = 0; g_cmdTable[i].cmd != NULL; i++) {
if (strncmp(line, g_cmdTable[i].cmd, strlen(g_cmdTable[i].cmd)) == 0) {
char *args = line + strlen(g_cmdTable[i].cmd);
while (*args == ' ') args++;
g_cmdTable[i].handler(args);
break;
}
}
// 没匹配到,打印错误
UartWrite(uartHost, "Unknown command\n", 16);
}
实际效果:串口输入 help,返回所有支持的命令列表;输入 gpio on,点亮板载LED;输入 adc,读取并返回当前ADC值。整个交互过程不到50行核心代码。
这个控制台框架我用了好几年,从STM32移植到鸿蒙,基本结构没变过。你想想看,有了这个基础,后面加I2C、SPI设备的调试命令,就是加一行表项的事。
嗯,UART串口这块就聊到这儿。记住一句话:串口是嵌入式工程师的耳朵和嘴巴,调通它,你才能听见设备在说什么。