




不需要校验位就选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 数据包格式:
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 帧尾
- 为什么要用 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)
-
校验可以直接用十进制数字或十六进制字符串表示
-
放在数据末尾,方便检测数据完整性
-
- 文本数据包示例
串口发送传感器数据
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 架构的
EAX
、EBX
,ARM 架构的R0
~R12
-
特点:编译器和汇编程序可以自由使用
② 特殊功能寄存器(SFR, Special Function Register)
-
作用:控制硬件外设、反映状态
-
这些寄存器直接映射到硬件电路中,通过它们就能控制 GPIO、定时器、串口等功能
-
在 STM32 中,这些寄存器是内存映射寄存器,用地址访问,比如:
cpp
GPIOA->ODR = 0x01; // 让 PA0 输出高电平
这里的 ODR
(Output Data Register)就是 GPIO 的输出数据寄存器。
- 寄存器的特点
-
速度极快(比 RAM 还快)
-
容量很小(几十到几百个寄存器)
-
与 CPU/外设直接连接
-
通过位(bit)控制硬件功能
- 寄存器在单片机中的例子
以 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
做了两件事:-
取出
args
当前指向位置的值(比如第一次是 10) -
将
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)里的定时器硬件模块,在设定的时间间隔自动触发中断服务函数,让你在固定时间做某件事。
它结合了两个东西:
-
定时器(硬件计时器)
-
中断机制(硬件事件触发 CPU 自动跳到某段代码执行)
1.基本原理
可以把它想成一个厨房的闹钟:
-
你在闹钟上设定"10分钟"
-
闹钟(定时器硬件)开始计时
-
时间一到,闹钟"叮"一下(产生中断信号)
-
你(CPU)放下手里的事,去处理闹钟(执行中断函数)
-
处理完再继续原来的工作
在 STM32 或 51 单片机中:
-
定时器寄存器 控制定时周期
-
中断控制器(NVIC)接收到定时器溢出事件后调用中断服务函数(ISR)
2.定时器中断的触发流程
-
配置定时器参数
-
预分频器(Prescaler):降低时钟频率
-
自动重装值(ARR):定时器计数到这个值时溢出
-
-
使能定时器中断
-
设置定时器的
UIE
(更新中断使能)位 -
NVIC 使能对应的中断通道
-
-
启动定时器
-
计数溢出 → 触发中断请求(IRQ)
-
执行中断服务函数(ISR)
- 在 ISR 中处理任务(如 LED 翻转、计时器变量++ 等)
-
清除中断标志
- 防止中断反复触发
- 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 更新
-
超时检测