串口通信学习
不需要校验位就选8位,需要校验位就选9位!
USRT
USART框图
STM32的外设引脚
这是USART的基本结构。
数据帧,八位是
这个公式还是很重要的!
如果在编辑器里面使用printf打印汉字的话,会出现乱码的话,前提是你的编码格式使用的UTF8,就在keil5里面这里加上这个--no-multibyte-chars
HEX 数据包这个概念在不同领域有不同的含义,但核心思想是一样的:
它指的是用十六进制(Hexadecimal)形式表示的一个数据包,数据包包含通信所需的头部、数据区和校验等信息。
1.“HEX”是什么意思?
HEX 是 十六进制 的缩写。
在计算机和嵌入式通信中,二进制数据通常用十六进制表示,因为它更简洁、人类更容易阅读。
例如:
二进制:
1010 1111
十六进制:
0xAF
2.“数据包”是什么意思?
数据包(Data Packet)是通信双方传输的完整数据单元。
一个数据包通常包含:
帧头 / 起始标志(Start Byte / Header)
用来标识一个包的开始,例如0xAA 0x55
长度字段(Length)
表示数据区的字节数命令字 / 功能码(Command)
表示这个包的用途(如读取、写入、状态查询)数据区(Data)
实际要传输的内容校验码(Checksum / CRC)
用来检测数据是否损坏帧尾 / 结束标志(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 架构的
EAX
、EBX
,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
做了两件事:取出
args
当前指向位置的值(比如第一次是 10)将
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)里的定时器硬件模块,在设定的时间间隔自动触发中断服务函数,让你在固定时间做某件事。
它结合了两个东西:
定时器(硬件计时器)
中断机制(硬件事件触发 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 更新
超时检测