串口通信学习

不需要校验位就选8位,需要校验位就选9位!

USRT

USART框图

STM32的外设引脚

这是USART的基本结构。

数据帧,八位是

这个公式还是很重要的!

如果在编辑器里面使用printf打印汉字的话,会出现乱码的话,前提是你的编码格式使用的UTF8 ,就在keil5里面这里加上这个**--no-multibyte-chars**

HEX 数据包 这个概念在不同领域有不同的含义,但核心思想是一样的:

它指的是用十六进制(Hexadecimal)形式表示的一个数据包 ,数据包包含通信所需的头部、数据区和校验等信息

1."HEX"是什么意思?

  • HEX十六进制 的缩写。

  • 在计算机和嵌入式通信中,二进制数据通常用十六进制表示,因为它更简洁、人类更容易阅读。

  • 例如:

    • 二进制:1010 1111

    • 十六进制:0xAF

2."数据包"是什么意思?

  • 数据包(Data Packet)是通信双方传输的完整数据单元

  • 一个数据包通常包含:

    1. 帧头 / 起始标志 (Start Byte / Header)

      用来标识一个包的开始,例如 0xAA 0x55

    2. 长度字段 (Length)

      表示数据区的字节数

    3. 命令字 / 功能码 (Command)

      表示这个包的用途(如读取、写入、状态查询)

    4. 数据区 (Data)

      实际要传输的内容

    5. 校验码 (Checksum / CRC)

      用来检测数据是否损坏

    6. 帧尾 / 结束标志 (End Byte)

      表示包的结束(可选)

3.HEX 数据包的定义示例

假设我们设计一个用于串口通信的 HEX 数据包格式:

cs 复制代码
[0]   帧头1         1 byte    固定为 0xAA
[1]   帧头2         1 byte    固定为 0x55
[2]   长度           1 byte    数据区+命令字的总长度
[3]   命令字         1 byte    例如 0x01 表示读取数据
[4..n]数据区         N byte    实际数据
[n+1] 校验码         1 byte    所有字节异或和或 CRC
[n+2] 帧尾           1 byte    固定为 0x0D

举例:

bash 复制代码
AA 55 04 01 10 20 35 0D
  • AA 55 帧头

  • 04 长度(后面 4 个字节:01 10 20 35)

  • 01 命令字(读取数据)

  • 10 20 数据区(两个字节的数据)

  • 35 校验码

  • 0D 帧尾

  1. 为什么要用 HEX 表示数据包?
  • 可读性好

    十六进制每两个字符正好表示一个字节

  • 方便调试

    串口调试助手、逻辑分析仪等都用 HEX 格式

  • 跨平台兼容

    HEX 表示的是原始二进制,不受编码格式影响

文本数据包 (Text Data Packet)指的是以文本形式(可读字符)来组织和传输的一个完整数据单元,它和 HEX 数据包最大的区别是:

  • HEX 数据包里每个字节是二进制,调试时常用十六进制显示

  • 文本数据包 直接用可见字符(ASCII/UTF-8等编码)表示内容,例如 "TEMP=25.6;HUM=78%\n"

1.文本数据包的核心定义

一个文本数据包一般包含以下部分:

  • 起始标志(Start Flag)

    • 用于标识数据包的开始

    • 例如 "$$", "<START>", "#"

  • 数据内容(Payload / Body)

    • 全部是可见字符(字母、数字、符号)

    • 一般使用分隔符分割字段,例如 ,;| 或空格

  • 结束标志(End Flag)

    • 表示数据包结束

    • 常用 \n(换行符)、\r\n(回车换行)、"<END>"

  • 可选校验(Checksum)

    • 校验可以直接用十进制数字或十六进制字符串表示

    • 放在数据末尾,方便检测数据完整性

  1. 文本数据包示例

串口发送传感器数据

bash 复制代码
$TEMP=25.6,HUM=78%,BAT=3.7V*
  • $ 起始标志

  • TEMP=25.6,HUM=78%,BAT=3.7V 数据区(用逗号分隔字段)

  • * 结束标志

带校验的例子(NMEA GPS 协议风格)

bash 复制代码
$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47
  • $GPGGA 起始标志+数据类型

  • 逗号分隔的多个字段

  • *47 末尾 * 后是校验值(XOR 校验)

自定义协议例子

bash 复制代码
<START>ID=001;CMD=READ;TEMP=25.6;HUM=78;<END>
  • <START> 起始标志

  • ID=001;CMD=READ;TEMP=25.6;HUM=78; 数据区,字段以 ; 分隔

  • <END> 结束标志

3.文本数据包的优缺点

优点:

  • 人类可直接阅读、调试方便(用串口助手就能看懂)

  • 跨平台性好,不依赖字节序

  • 可直接使用字符串处理函数解析

缺点:

  • 占用带宽较大(字符比原始二进制长)

  • 解析速度慢于固定结构的 HEX 数据包

  • 对浮点数等类型需要额外转换(ASCII ↔ 数值)

4.文本数据包的典型应用

  • 串口调试协议(如 AT 命令、NMEA GPS 数据)

  • HTTP、MQTT 等网络应用层协议

  • 传感器调试输出

  • 物联网设备日志与命令传输

寄存器

在计算机和单片机(包括 STM32、51 单片机等)中,寄存器 (Register)是位于 CPU 内部的一种容量极小、速度极快的存储单元,用来临时保存和控制数据、指令以及硬件状态。

你可以把它想象成 CPU 手边的"超高速便利贴"

  • 内存(RAM)像是在隔壁房间的仓库,取数据需要跑过去

  • 寄存器就在 CPU 旁边,一伸手就能拿到

1.寄存器的分类

寄存器按用途大致分为两大类:

① 通用寄存器

  • 作用:临时保存运算数据、中间结果

  • 例子:x86 架构的 EAXEBX,ARM 架构的 R0 ~ R12

  • 特点:编译器和汇编程序可以自由使用

② 特殊功能寄存器(SFR, Special Function Register)

  • 作用:控制硬件外设、反映状态

  • 这些寄存器直接映射到硬件电路中,通过它们就能控制 GPIO、定时器、串口等功能

  • 在 STM32 中,这些寄存器是内存映射寄存器,用地址访问,比如:

cpp 复制代码
GPIOA->ODR = 0x01; // 让 PA0 输出高电平

这里的 ODR(Output Data Register)就是 GPIO 的输出数据寄存器。

  1. 寄存器的特点
  • 速度极快(比 RAM 还快)

  • 容量很小(几十到几百个寄存器)

  • 与 CPU/外设直接连接

  • 通过位(bit)控制硬件功能

  1. 寄存器在单片机中的例子

以 STM32F103 为例,假设要点亮 PA5 引脚上的 LED:

cpp 复制代码
RCC->APB2ENR |= (1 << 2); // 开启 GPIOA 时钟
GPIOA->CRL &= ~(0xF << 20); // 清空 PA5 模式位
GPIOA->CRL |=  (0x1 << 20); // 设置 PA5 为推挽输出
GPIOA->ODR |=  (1 << 5);    // 置位 PA5 输出高电平
  • RCC->APB2ENR:外设时钟使能寄存器

  • GPIOA->CRL:端口配置寄存器低位(控制 PA0~PA7)

  • GPIOA->ODR:输出数据寄存器

这些寄存器本质上都是内存地址,比如 GPIOA->ODR 实际是:

cpp 复制代码
0x4001080C

往这个地址写 1,就等于给 PA5 脚送高电平。

4.用简单比喻理解

  • 寄存器:CPU 桌上的小便签,拿取速度最快(直接操作)

  • RAM:隔壁房间的文件柜(速度较慢)

  • 硬盘:地下仓库(速度最慢)

C语言可变参数

C 语言可变参数 (Variable Arguments)指的是一个函数在声明时参数的数量不固定,可以根据调用时的需要传入不同数量的实参。

最典型的例子就是标准库中的 printf() 函数:

cpp 复制代码
printf("Hello %s, age %d\n", "Tom", 18);

printf 的第一个参数是固定的格式化字符串,后面跟多少参数由格式字符串决定,这就是可变参数的用法。

一、、可变参数函数的声明方式

在函数形参列表的末尾使用**省略号 ...**表示:

cpp 复制代码
#include <stdarg.h> // 必须包含的头文件

void myFunc(int count, ...); // count 表示后面有多少参数
  • 固定参数:省略号前的部分,必须有至少一个固定参数(方便定位可变参数起点)。

  • 可变参数 :省略号 ... 表示数量和类型在编译期不固定。

二、可变参数的原理

在 C 语言中,可变参数通过 栈传递stdarg.h 提供了访问它们的宏:

  • va_list ------ 保存参数信息的变量类型

  • va_start ------ 初始化 va_list,定位到可变参数起点

  • va_arg ------ 取出一个参数

  • va_end ------ 清理工作

三、可变参数函数实现示例

例如写一个求任意数量整数和的函数:

cpp 复制代码
#include <stdio.h>
#include <stdarg.h>

// sum(count, ...): 传入 count 个整数,返回它们的和
int sum(int count, ...) {
    va_list args;           // 定义参数列表变量
    va_start(args, count);  // 初始化,从 count 后的参数开始取

    int total = 0;
    for (int i = 0; i < count; i++) {
        total += va_arg(args, int); // 每次取出一个 int 参数
    }

    va_end(args);           // 清理
    return total;
}

int main() {
    printf("%d\n", sum(3, 10, 20, 30)); // 输出 60
    printf("%d\n", sum(5, 1, 2, 3, 4, 5)); // 输出 15
    return 0;
}

四、注意事项

  • 类型安全性差

    编译器无法检查可变参数类型是否正确,比如 va_arg(args, int) 和实际类型不匹配会导致错误行为。

  • 必须依赖固定参数来控制读取数量

    否则无法知道何时停止读取。

  • 跨平台注意数据对齐

    参数在栈上的对齐方式可能和平台架构有关。

  • 宏和可变参数

    宏中也能用 ... 表示可变参数(C99 及之后)。

cpp 复制代码
#include <stdio.h>
#include <stdarg.h>

void show(int count, ...) {
    va_list args;
    va_start(args, count); // 定位到第一个可变参数

    for (int i = 0; i < count; i++) {
        int val = va_arg(args, int); // 依次取出一个 int
        printf("%d\n", val);
    }

    va_end(args);
}

int main() {
    show(3, 10, 20, 30);
    return 0;
}

内存栈图示(调用 show(3, 10, 20, 30) 时)

假设我们是 x86 栈向下增长 的情况(地址从高到低),函数调用时的栈大致如下:

cpp 复制代码
高地址
┌───────────────────────┐
│    返回地址            │ ← main 调用 show 后返回的地址
├───────────────────────┤
│ count = 3             │ ← 固定参数
├───────────────────────┤
│ 10                    │ ← 第1个可变参数
├───────────────────────┤
│ 20                    │ ← 第2个可变参数
├───────────────────────┤
│ 30                    │ ← 第3个可变参数
└───────────────────────┘
低地址

va_start(args, count)

  • va_start 的作用是:
    args 指针指向 count 后面的第一个可变参数(10)

  • 底层会用 count 在栈上的地址 + 它的大小(sizeof(count)) 来得到可变参数的起点。

bash 复制代码
args ──► 10

va_arg(args, int)

  • va_arg 做了两件事:

    1. 取出 args 当前指向位置的值(比如第一次是 10)

    2. args 移动到下一个参数的位置 (加上 sizeof(int)

  • 取值过程:

bash 复制代码
第1次:args=10 → 返回10 → args指向20
第2次:args=20 → 返回20 → args指向30
第3次:args=30 → 返回30 → args指向结束位置

va_end(args)

  • va_end 主要是做清理,防止野指针问题(实际可能什么都不做,但必须写)

总结:

  • va_start:定位到第一个可变参数

  • va_arg:取值并移动指针

  • va_end:结束可变参数处理

  • 栈上参数是连续存放的,所以可以用指针依次取出

定时器中断

定时器中断其实就是利用单片机(或 CPU)里的定时器硬件模块,在设定的时间间隔自动触发中断服务函数,让你在固定时间做某件事。

它结合了两个东西:

  1. 定时器(硬件计时器)

  2. 中断机制(硬件事件触发 CPU 自动跳到某段代码执行)

1.基本原理

可以把它想成一个厨房的闹钟

  • 你在闹钟上设定"10分钟"

  • 闹钟(定时器硬件)开始计时

  • 时间一到,闹钟"叮"一下(产生中断信号)

  • 你(CPU)放下手里的事,去处理闹钟(执行中断函数)

  • 处理完再继续原来的工作

在 STM32 或 51 单片机中:

  • 定时器寄存器 控制定时周期

  • 中断控制器(NVIC)接收到定时器溢出事件后调用中断服务函数(ISR)

2.定时器中断的触发流程

  • 配置定时器参数

    • 预分频器(Prescaler):降低时钟频率

    • 自动重装值(ARR):定时器计数到这个值时溢出

  • 使能定时器中断

    • 设置定时器的 UIE(更新中断使能)位

    • NVIC 使能对应的中断通道

  • 启动定时器

  • 计数溢出 → 触发中断请求(IRQ)

  • 执行中断服务函数(ISR)

    • 在 ISR 中处理任务(如 LED 翻转、计时器变量++ 等)
  • 清除中断标志

    • 防止中断反复触发
  1. STM32 定时器中断示例
cpp 复制代码
#include "stm32f10x.h"

void TIM2_IRQHandler(void) {
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) {
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 清除中断标志

        GPIOA->ODR ^= (1 << 5); // 翻转 PA5
    }
}

void Timer2_Init(void) {
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    GPIO_InitTypeDef gpio;
    gpio.GPIO_Pin = GPIO_Pin_5;
    gpio.GPIO_Mode = GPIO_Mode_Out_PP;
    gpio.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &gpio);

    TIM_TimeBaseInitTypeDef tim;
    tim.TIM_Period = 9999; // ARR
    tim.TIM_Prescaler = 7199; // PSC
    tim.TIM_ClockDivision = TIM_CKD_DIV1;
    tim.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM2, &tim);

    TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
    NVIC_EnableIRQ(TIM2_IRQn);
    TIM_Cmd(TIM2, ENABLE);
}

int main(void) {
    Timer2_Init();
    while (1) {
        // 主循环可做其他事
    }
}

上面例子里:

  • 定时器频率 = 72MHz / (PSC+1) / (ARR+1) = 72MHz / 7200 / 10000 = 1Hz

  • 每秒进一次中断,ISR 里翻转一次 LED

4.定时器中断的应用

  • 周期性任务调度(实时操作系统里的节拍)

  • LED 闪烁

  • 传感器采样定时

  • 电机 PWM 更新

  • 超时检测

相关推荐
西岸行者5 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意5 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码6 天前
嵌入式学习路线
学习
毛小茛6 天前
计算机系统概论——校验码
学习
babe小鑫6 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms6 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下6 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。6 天前
2026.2.25监控学习
学习
im_AMBER6 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J6 天前
从“Hello World“ 开始 C++
c语言·c++·学习