当前位置: 首页 > news >正文

串口通信学习

不需要校验位就选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 数据包格式:

[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

举例:

AA 55 04 01 10 20 35 0D
  • AA 55 帧头

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

  • 01 命令字(读取数据)

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

  • 35 校验码

  • 0D 帧尾

4. 为什么要用 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)

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

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

2. 文本数据包示例

串口发送传感器数据

$TEMP=25.6,HUM=78%,BAT=3.7V*
  • $ 起始标志

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

  • * 结束标志

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

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

  • 逗号分隔的多个字段

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

自定义协议例子

<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 中,这些寄存器是内存映射寄存器,用地址访问,比如:

  • GPIOA->ODR = 0x01; // 让 PA0 输出高电平
    

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

2. 寄存器的特点

  • 速度极快(比 RAM 还快)

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

  • 与 CPU/外设直接连接

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

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

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

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 实际是:

0x4001080C

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

4.用简单比喻理解

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

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

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

C语言可变参数

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

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

printf("Hello %s, age %d\n", "Tom", 18);

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

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

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

#include <stdarg.h> // 必须包含的头文件void myFunc(int count, ...); // count 表示后面有多少参数
  • 固定参数:省略号前的部分,必须有至少一个固定参数(方便定位可变参数起点)。

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

二、可变参数的原理

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

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

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

  • va_arg —— 取出一个参数

  • va_end —— 清理工作

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

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

#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)); // 输出 60printf("%d\n", sum(5, 1, 2, 3, 4, 5)); // 输出 15return 0;
}

四、注意事项

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

  • 必须依赖固定参数来控制读取数量
    否则无法知道何时停止读取。

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

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

#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); // 依次取出一个 intprintf("%d\n", val);}va_end(args);
}int main() {show(3, 10, 20, 30);return 0;
}

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

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

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

va_start(args, count)

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

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

  • args ──► 10
    

    va_arg(args, int)

  • va_arg 做了两件事:

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

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

  • 取值过程:

  • 第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 翻转、计时器变量++ 等)

  • 清除中断标志

    • 防止中断反复触发

3. STM32 定时器中断示例

#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; // ARRtim.TIM_Prescaler = 7199; // PSCtim.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 更新

  • 超时检测

http://www.xdnf.cn/news/1289827.html

相关文章:

  • 数据分析专栏记录之 -基础数学与统计知识
  • Spring-Cache 缓存数据
  • windows git安装步骤
  • XGBoost 的适用场景以及与 CNN、LSTM 的区别
  • 网络协议——HTTP协议
  • Linux服务:Apache 虚拟主机配置指南:多站点部署三种方式详解
  • 【超详细!题解|两种做法】洛谷P3196 [HNOI2008] 神奇的国度[MCS算法]
  • 深入剖析 React 合成事件:透过 onClick 看本质
  • 过程设计工具深度解析-软件工程之详细设计(补充篇)
  • Nginx 高级配置
  • 【后端】Spring @Resource和@Autowired的用法和区别
  • 通用同步/异步收发器USART串口
  • excel-随笔记
  • [ 数据结构 ] 时间和空间复杂度
  • Python初学者笔记第二十二期 -- (JSON数据解析)
  • VGG改进(2):基于Local Attention的模型优化
  • 【图像算法 - 13】基于 YOLO12 与 OpenCV 的实时目标点击跟踪系统(系统介绍 + 源码详细)
  • 获取数组,字符串,集合的长度
  • C++——高性能组件
  • 算法打卡力扣第88题:合并两个有序数组(easy)
  • 解释 Spring MVC 的工作原理
  • _init__.py的作用
  • 智能装配线cad【8张】三维图+设计说明书
  • linux 执行ls命令文件夹显示全白色
  • Langchain入门:文本摘要
  • 多轮问答与指代消解
  • 一维数组的创建、初始化与使用指南
  • “生成式UI革命”:Tambo AI如何让你的应用“开口说话、动手搭界面” | 全面深剖、案例实践与未来展望
  • Python函数篇:从零到精通
  • ubuntu24下keychorn键盘连接不了的改建页面的问题修复