1. 串口初始化:不用背,直接"生成"
串口初始化的代码不要自己默写,用 STC-ISP 下载软件自动生成就行。
生成时选好这几个条件:
- 12T 模式、12MHz 晶振
- 定时器 2 当波特率发生器
- 串口 1
- 波特率看题目要求,一般就是 9600
生成完的代码里,手动补上两行就完事:
ES = 1; // 开串口中断
EA = 1; // 开总中断
一句话:初始化代码 = 软件生成 + 补两行中断开关。
2. 串口发送:让 printf 直接能用
正常发送一个字节很麻烦,要写 SBUF、等 TI 之类的。PDF 用了一个"偷懒"办法——串口重定向。
原理:C51 里 printf 底层会调用 putchar 这个函数,所以我们自己写一个 putchar,把字符塞进 SBUF 发出去:
extern char putchar(char ch)
{
SBUF = ch; // 把字符丢进发送寄存器
while (TI == 0); // 等它发完
TI = 0; // 清标志
return ch;
}
这样以后发送数据直接:
printf("%bu", my_data);
就像在电脑上打印一样简单。
注意坑:格式符要用对!比如无符号字符要用 %bu,写成 %d 就会发错数据。
3. 串口接收:收到就存进"篮子"
接收靠中断。当 RI = 1 时,说明收到了一个字节。
做法:
- 把收到的数据(SBUF)存进缓冲区数组
Uart_Buf - 索引
Uart_Rx_Index加 1 - 计时器
Uart_Recv_Tick清零(表示"刚刚还有数据来") - 清掉 RI 标志
防溢出:如果索引超过 10(缓冲区大小),就归零并清空缓冲区,防止数组越界。
4. 判断"一帧数据收完了":超时解析
关键问题:串口是一个字节一个字节来的,怎么知道对方"一句话说完了"?
PDF 用的是超时判断,很像"打字停顿":
- 定时器每 1ms 把
Tick加 1 - 每收到一个字节,
Tick就清零重新计时 - 如果 10ms 内没有新数据,就认为这一帧结束了,开始处理
处理流程(Uart_Proc 函数):
- 如果索引是 0(压根没收到数据)→ 直接返回
- 如果
Tick >= 10(超时了)→ 解析数据 → 清空缓冲区 → 索引归零
类比:就像聊天时,对方一直打字,你一直等;对方停 10 秒没动静,你就知道他这句话说完了。
5.uart.c/h
#include "uart.h"
void Uart1_Init(void) // 9600bps@12.000MHz
{
SCON = 0x50; // 8位数据,可变波特率
AUXR |= 0x01; // 串口1选择定时器2为波特率发生器
AUXR &= 0xFB; // 定时器时钟12T模式
T2L = 0xE6; // 设置定时初始值
T2H = 0xFF; // 设置定时初始值
AUXR |= 0x10; // 定时器2开始计时
ES = 1; // 使能串口1中断
EA = 1;
}
extern char putchar(char ch)
{
SBUF = ch;
while (TI == 0);
TI = 0;
return ch;
}
6.主函数
pdata unsigned char Uart_Buf[10] = {0}; // 串口接收缓冲区
idata unsigned char Uart_Rx_Index; // 串口接收索引
idata unsigned char Uart_Recv_Tick; // 串口接收时间标志
idata unsigned char Uart_Rx_Flag;
void Uart_Proc()
{
if (Uart_Rx_Index == 0)
return; // 无数据,直接返回
if (Uart_Recv_Tick >= 10)
{ // 若接收超时,对数据读取,清空缓存区
Uart_Recv_Tick = 0;
Uart_Rx_Flag = 0 ;
//处理函数
memset(Uart_Buf, 0, Uart_Rx_Index); // 清空接收数据
Uart_Rx_Index = 0;
}
}
void Uart1_Isr(void) interrupt 4
{
// 若接收到数据
if (RI)
{
Uart_Rx_Flag = 1; // 接收标志
Uart_Recv_Tick = 0; // 清零接收时间标志
Uart_Buf[Uart_Rx_Index++] = SBUF; // 将数据保存到缓冲区
RI = 0; // 清除接收中断标志
if (Uart_Rx_Index > 10)
{
Uart_Rx_Index = 0; // 防止溢出
memset(Uart_Buf, 0, 10);
}
}
}
// 定时器
void Time1_Isr(void) interrupt 3
{
if (Uart_Rx_Flag)
Uart_Recv_Tick++;
}
// 串口接收计时增加
6. 常用"工具函数"(都要 #include "string.h")
| 函数 | 作用 | 大白话 |
|---|---|---|
memset |
把内存填成指定值 | 清空缓冲区,全填 0 |
strcmp |
比较两个字符串 | 判断收到的是不是某个指令,要完全相等 |
sscanf |
按格式从字符串里取数字 | 从 "(99,99)" 里抠出 99 和 99 |
atoi |
字符串转整数 | 把 "123" 变成数字 123 |
特别注意:匹配指令用 strcmp(完整匹配),别用 strncmp,因为它只比前几个字符,"??" 也能匹配成 "?",会误判。
7. 重点坑:sscanf 会"半途而废"
这是 PDF 最精彩的部分。
比如题目要求收到坐标 "(99,99)"。正常用 sscanf 解析没问题:
sscanf(buf, "(%u,%u)", &x, &y);
但是,如果收到的是错误数据 "(99,9#)",sscanf 遇到 # 会停止解析,结果还是解析出 x=99, y=9,程序以为数据是对的!
类比:你让人读"99,9#",他读到 # 不认识就停了,但把前面读到的 99 和 9 当成正确答案交给你了。
解决办法:加一个 %n 做"长度验证"
%n 不会读取内容,但它会记录 sscanf 到底读了多少个字符。
int chars_read;
int result = sscanf(buf, "(%u,%u)%n", &x, &y, &chars_read);
if(result == 2 && chars_read == strlen(buf))
{
// 格式完全正确,正常处理
}
else
{
// 格式错误,报错
}
判断条件两个:
- 成功解析出 2 个数字(
result == 2) - 读取的字符数 == 整个字符串长度(
chars_read == strlen(buf))
如果是 "(99,9#)",sscanf 读到 # 就停了,chars_read 会比总长度短,于是被识别为错误数据。
总结成一张"傻瓜流程图"
初始化:ISP生成 + ES=1 + EA=1
↓
发送:自己写 putchar → 用 printf 随便发
↓
接收:中断里 RI=1 → 存进缓冲区 → 计时清零
↓
判断收完:定时器1ms加1,超过10ms没新数据 = 一帧结束
↓
解析:sscanf + %n 验证长度 → 格式对了才处理
↓
处理完:memset清空 + 索引归零,准备收下一帧
核心就三句话:
- 发送靠
printf重定向,省事; - 接收靠"超时 10ms"判断一帧收完,稳;
- 解析靠
sscanf + %n验证格式,防止错数据蒙混过关。
8.符号
一图看懂常用格式符
| 格式符 | 用于什么类型 | 什么时候用 | 例子 |
|---|---|---|---|
%bu |
unsigned char(1字节无符号) |
发送/打印一个字节的数据,比如温度、编号等小数字(0~255) | printf("%bu", my_data); |
%bd |
signed char(1字节有符号) |
发送有符号的字节(-128~127) | printf("%bd", temp); |
%bx / %bX |
unsigned char(十六进制) |
想看一个字节的十六进制值 | printf("%bx", data); |
%u |
unsigned int(2字节无符号) |
发送/解析 16 位无符号整数(0~65535),如坐标值 | sscanf(buf, "(%u,%u)", &x, &y); |
%d |
int(2字节有符号) |
发送/解析 16 位有符号整数(-32768~32767) | sscanf(buf, ":%d", &value); |
%s |
字符串 | 发送/读取字符串 | printf("%s", str); |
%n |
不读数据,只"计数" | 记录已经读了多少个字符,用来验证格式是否完整 | sscanf(buf, "(%u,%u)%n", &x, &y, &n); |
%[^:] |
字符串(直到冒号停止) | 解析指令格式,如 "CMD:123" 中的 CMD |
sscanf(buf, "%[^:]:%d", cmd, &value); |
关键:%bu 和 %u 怎么区分?
就看变量类型!
cunsigned char my_data = 5; // 1 字节 → 用 %bu
unsigned int x, y; // 2 字节 → 用 %u
- 变量是
unsigned char(0~255)→%bu - 变量是
unsigned int(0~65535)→%u
PDF 特别强调的坑:
unsigned char my_data = 5;
printf("%bu", my_data); // ✅ 正确
printf("%d", my_data); // ❌ 错误,会发送错数据
因为 %d 是按 2 字节有符号数去读,而 my_data 只有 1 字节,类型对不上,数据就乱了。
%n 是干什么的?它很特殊
%n 不是用来读取数据的,它是一个"计数器"。
它会把"sscanf 到目前为止读了多少个字符"存进一个 int 变量里。
用法:放在格式串的最后面
int chars_read;
int result = sscanf(buf, "(%u,%u)%n", &x, &y, &chars_read);
然后配合 strlen 做完整验证:
if(result == 2 && chars_read == strlen(buf))
{
// 格式完全正确
}
else
{
// 格式错误
}
原理:
- 正常数据
"(99,99)"→ sscanf 全部读完 →chars_read等于总长度 7 →
- 错误数据
"(99,9#)"→ sscanf 读到#停了 →chars_read比总长度少 →
被识破
一句话:
%n就是"读了多少个字符的记录员",用来抓出那种"前半段对、后半段是垃圾"的错误数据。
快速选择口诀
变量是 unsigned char → %bu
变量是 unsigned int → %u
变量是 int(有符号) → %d
想要验证格式是否完整 → 末尾加 %n
要解析 "名字:数字" → %[^:]:%d
最容易犯的 3 个错
unsigned char用了%u或%d→ 字节数不对,数据错乱sscanf里忘了加%n验证 → 错误格式"(99,9#)"也能混过去- 用
%n想读取数字 →%n不读数据,只计数,别搞混
记住这几点,串口收发基本就不会翻车了。
