第九周 串口学习笔记

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 时,说明收到了一个字节。

做法:

  1. 把收到的数据(SBUF)存进缓冲区数组 Uart_Buf
  2. 索引 Uart_Rx_Index 加 1
  3. 计时器 Uart_Recv_Tick 清零(表示"刚刚还有数据来")
  4. 清掉 RI 标志

防溢出:如果索引超过 10(缓冲区大小),就归零并清空缓冲区,防止数组越界。


4. 判断"一帧数据收完了":超时解析

关键问题:串口是一个字节一个字节来的,怎么知道对方"一句话说完了"?

PDF 用的是超时判断,很像"打字停顿":

  • 定时器每 1ms 把 Tick 加 1
  • 每收到一个字节,Tick 就清零重新计时
  • 如果 10ms 内没有新数据,就认为这一帧结束了,开始处理

处理流程(Uart_Proc 函数):

  1. 如果索引是 0(压根没收到数据)→ 直接返回
  2. 如果 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
{
    // 格式错误,报错
}

判断条件两个:

  1. 成功解析出 2 个数字(result == 2)
  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清空 + 索引归零,准备收下一帧

核心就三句话:

  1. 发送靠 printf 重定向,省事;
  2. 接收靠"超时 10ms"判断一帧收完,稳;
  3. 解析靠 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 → :white_check_mark:
  • 错误数据 "(99,9#)" → sscanf 读到 # 停了 → chars_read 比总长度少 → :cross_mark: 被识破

一句话:%n 就是"读了多少个字符的记录员",用来抓出那种"前半段对、后半段是垃圾"的错误数据。


快速选择口诀

变量是 unsigned char  →  %bu
变量是 unsigned int   →  %u
变量是 int(有符号)   →  %d
想要验证格式是否完整   →  末尾加 %n
要解析 "名字:数字"     →  %[^:]:%d

最容易犯的 3 个错

  1. unsigned char 用了 %u 或 %d → 字节数不对,数据错乱
  2. sscanf 里忘了加 %n 验证 → 错误格式 "(99,9#)" 也能混过去
  3. 用 %n 想读取数字 → %n 不读数据,只计数,别搞混

记住这几点,串口收发基本就不会翻车了。