不需要校驗位就選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 更新
超時檢測