news 2026/9/16 1:16:52

STM32F103串口通信实战:标准库UART配置与调试全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103串口通信实战:标准库UART配置与调试全解析

简介:面向嵌入式初学者与STM32开发者,这套实验工程以STM32F103C8T6为核心,基于标准外设库演示UART串口通信的完整实现流程。工程通过串口输入1、2、3任意数字,分别输出不同内容,可直观观察字符接收、分支判断与回显发送的处理过程,适合从固件库角度理解串口外设的初始化及中断数据收发方式。资源包共175个文件、约5.78MB,主体为C源码、头文件、汇编启动文件及Keil工程配置,同时带有Hex烧录文件、Map映射表、列表文件和调试日志等,既能直接编译烧录,也方便对照中间产物排查问题。整套资料结构清晰,作者MQ0522将项目文件与编译输出一并整理,读者可以边改边验证,逐步掌握串口初始化、串口中断、字符解析和发送响应的编码技巧,为后续实现更复杂的通信协议或调试功能打好基础。目前该实验已有2645人学习下载,适合入门阶段的STM32开发者按需取用。

1. 串口通信实验,先把UART搞明白

STM32F103的UART串口实验往往不是第一个程序,但却是第一个能让你“看见”数据的程序。C8T6最小系统板上电,PA9/PA10接USB转串口,发送1、2、3得到不同反馈,这个实验把GPIO复用、时钟、中断和波特率都串了起来。对新手来说,它比点灯更有实际意义;对老手来说,标准库的寄存器操作和中断优先级设计也能看出工程素养。下面的内容从硬件最小系统到代码实现,再到串口1与串口3的差异,给出可以直接编译的工程和调试时真正会遇到的坑。

2. 标准库工程下的USART外设与引脚配置

2.1 UART协议与C8T6的引脚映射

UART协议全称通用异步收发器,一帧数据由起始位、数据位、可选校验位和停止位组成。STM32F103C8T6内部集成3个USART,其中USART1挂在APB2总线,USART2和USART3挂在APB1总线。实际实验多用USART1,因为PA9(TX)和PA10(RX)引脚直接引出到最小系统板的排针上。许多stm32f103最小系统板同时把PA9/PA10连到ST-Link的虚拟串口,这意味着不需要额外USB转TTL模块,直接用ST-Link的串口通道就能看到打印信息。

注意:USART1的TX引脚必须配置为推挽复用输出,RX引脚配置为浮空输入或上拉输入。不要直接使用GPIO_Mode_Out_PP,否则发送时引脚波形不正确。

2.1.1 引脚复用模式与标准库结构体

标准库中GPIO_InitTypeDef结构体的GPIO_Mode决定了引脚工作模式。对于串口功能,我们使用以下配置表:

引脚复用功能GPIO_Mode说明
PA9USART1_TXGPIO_Mode_AF_PP推挽复用输出
PA10USART1_RXGPIO_Mode_IN_FLOATING浮空输入
PB10USART3_TXGPIO_Mode_AF_PP推挽复用输出
PB11USART3_RXGPIO_Mode_IN_FLOATING浮空输入

PA9作为TX,推挽输出能够提供足够的驱动能力。PA10作为RX,浮空输入意味着引脚电平完全由外部决定,如果外部没有拉高,空闲状态下可能检测到噪声。很多工程师习惯在硬件上加10k上拉电阻,或者用GPIO_Mode_IPU设置为内部上拉。但标准库默认示例通常使用浮空输入,方便配合不同电平的外设。

2.2 时钟树开启顺序与stm32标准库新建工程的时钟配置

新建标准库工程时,会复制一份stm32f10x_rcc.c 和 stm32f10x_usart.c 进工程。但很多人没有真正理解RCC是做什么的。STM32外设使用前必须打开对应总线上的时钟,否则寄存器写入无效。

对于USART1,它位于APB2总线,同时需要操作GPIOA,所以配置代码如下:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);

这句话同时打开GPIOA和USART1的时钟。逻辑说明:RCC_APB2PeriphClockCmd函数将指定的外设时钟使能位置1,参数是APB2总线上的外设位掩码。有时我们看到别人只写RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE),那么USART1寄存器不可用,调试时先卡死在USART_Init。

如果改用USART3,时钟配置变为:

RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

USART3在APB1上,GPIOB在APB2上,必须分两条指令开启。经常有人在APB1上找不到GPIOB,因为GPIOB永远挂在APB2。这是stm32f103串口1和串口3使用差异中最容易踩的一个点。

2.2.1 GPIO参数细节

配置PA9和PA10:

GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure);

代码说明:GPIO_Speed在STM32标准库中只对输出模式有意义,RX配置浮空输入时该字段不生效,但为了结构体完整,通常先填充了一次。将TX和RX的初始化放在同一个结构体变量中连续执行,可以减少临时变量占用。参数说明:GPIO_Speed_50MHz表示输出引脚的最大翻转速率,USART1的最高波特率是4.5Mbps,50MHz完全满足。如果你后期在同一引脚复用其他功能,比如定时器PWM,需要重新考虑速度。

2.3 USART_Init参数与波特率计算

USART初始化中的关键参数如下:

USART_InitTypeDef USART_InitStructure; USART_InitStructure.USART_BaudRate = 115200; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; USART_Init(USART1, &USART_InitStructure); USART_Cmd(USART1, ENABLE);

USART_BaudRate是期望波特率,库函数会根据外设时钟自动计算USART_BRR寄存器值。比如在72MHz系统时钟下,USART1挂在APB2,APB2时钟也是72MHz,计算得到BRR=0x271,程序可以打印或查看寄存器验证。

8位数据无校验和1位停止位是UART通信协议最常用的配置。如果使用9位数据位(如用于多机通信的地址/数据标志),需要把USART_WordLength改为9b,并配置USART_Parity为有校验。此时数据帧中会包含校验位,实际有效数据仍然是8位。实验里的1、2、3是ASCII字符,用8位无校验即可。

HardwareFlowControl设置为None表示不启用硬件流控。如果启用,需要额外占用CTS/RTS引脚,并且发送端必须等待对端拉高信号。C8T6最小系统板上一般不引出这些引脚,使用None最稳妥。

注意:USART_Cmd(USART1, ENABLE)必须在所有配置完成后才执行。很多人把使能放在USART_Init之前,导致USART_Init内部操作寄存器时外设还处于关闭状态,部分设置不会立即生效。标准库这样设计是因为某些外设的位域需要在外设禁用状态下修改。

3. 串口接收中断与命令分发的实战写法

3.1 接收寄存器与标志位行为

USART接收数据时,硬件将收到的字节放入USART_DR寄存器,同时置位RXNE标志。程序读取DR数据后,该标志自动清零。标准库中USART_ReceiveData(USART1)返回的是DR寄存器的低8位,该函数内会读取DR,因此可以清除RXNE。

实验要求根据接收到的字符输出不同内容。最直接的方式是在中断服务函数中判断:

void USART1_IRQHandler(void) { uint8_t ch; if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { ch = USART_ReceiveData(USART1); if (ch == '1') { UART_SendString(USART1, "You pressed 1\r\n"); } else if (ch == '2') { UART_SendString(USART1, "You pressed 2\r\n"); } else if (ch == '3') { UART_SendString(USART1, "You pressed 3\r\n"); } } }

逻辑说明:进入中断后必须先判断中断标志是否有RXNE,防止其它USART中断(比如TC)触发后误进入该分支。读取USART_ReceiveData会清除标志,所以不需要手动调用清除函数。UART_SendString是自定义的字符串发送函数,在下一节给出实现。

使用单字符判断比strcmp更高效。如果接收的是多个字符组成的字符串,比如从ModbusPoll软件写入的传感器寄存器地址,就需要结合stm32f103标准库中的strcmp函数,但要确保缓冲区以'\0'结尾,否则会越界。字符串比较在中断中使用时要格外小心,类似strcmp(buffer, "1")这种写法在嵌入式里并不推荐,因为strlen和循环指令会拉长中断执行时间。

3.2 发送函数与TXE、TC标志

标准库中发送一个字节通常这样写:

标志位触发条件操作
RXNE收到一个字节且数据寄存器未读读取USART_DR自动清零
TXE数据寄存器空,可以写入新字节写入USART_DR自动清零
TC移位寄存器完成最后一位发送读USART_SR再写USART_DR清除

TXE和TC是串口发送中最重要的两个标志。TXE表示数据从软件写入到硬件缓存,但硬件还没有完全发出。TC表示移位寄存器已经发送完最后一位。在串口关闭或MCU进入低功耗模式前,必须等待TC,否则最后一个字节会被截断。

发送一个字节的函数:

void USART_SendChar(USART_TypeDef* USARTx, uint8_t ch) { while (USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET); USART_SendData(USARTx, ch); }

这个函数等待TXE置位后写入数据。如果连续发送大量数据,最后一次发送后需要等待TC,所以字符串发送函数最好在末尾补上等待TC:

void UART_SendString(USART_TypeDef* USARTx, char* str) { while (*str) { USART_SendChar(USARTx, (uint8_t)*str++); } while (USART_GetFlagStatus(USARTx, USART_FLAG_TC) == RESET); }

参数说明:USARTx可以是USART1或USART3,函数内部通过指针访问不同外设寄存器,标准库将外设基地址封装成结构体指针,所以通用性很好。str是C字符串,必须用'\0'结尾,否则循环不会停止。

3.3 NVIC中断优先级配置与常见误区

要使能USART1接收中断,需要配置NVIC和USART的中断源:

NVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);

NVIC_PriorityGroupConfig在整个工程中只需调用一次,通常放在main函数开头。如果多个外设使用中断,需要规划抢占优先级和子优先级。USART1的中断优先级如果比定时器高,那么定时器回调可能被串口打断,导致采样时间抖动。

有初学者在USART1_IRQHandler中调用delay函数,其中等待SysTick中断,如果SysTick优先级比USART低,会造成死锁。建议中断服务函数中不要做延时,而是把接收到的字符存入循环缓冲区,主循环再解析。

3.3.1 简易环形缓冲区设计

为了避免实验只接收一个字符,可以加入环形缓冲区:

#define RX_BUF_SIZE 16 volatile uint8_t rx_buf[RX_BUF_SIZE]; volatile uint8_t rx_head = 0, rx_tail = 0;

中断中写入:

uint8_t ch = USART_ReceiveData(USART1); uint8_t next = (rx_head + 1) % RX_BUF_SIZE; if (next != rx_tail) { rx_buf[rx_head] = ch; rx_head = next; }

主循环中读取并判断:

while (rx_tail != rx_head) { uint8_t cmd = rx_buf[rx_tail]; rx_tail = (rx_tail + 1) % RX_BUF_SIZE; // 根据cmd分发 }

这个模式在很多协议栈中都能看到,比如ModbusPoll上位机软件发送的Modbus帧,也是逐字节进入缓冲区后解析。实验中的1、2、3是单字节命令,用该缓冲区可以避免中断中做耗时打印。

4. 串口1与串口3的使用差异及启动文件选择

4.1 外设时钟总线和引脚复用差异

串口1和串口3在标准库开发中的差异不只在引脚号上。USART1挂在APB2,最高支持72MHz时钟;USART3挂在APB1,最高36MHz。SystemInit后APB1的时钟等于系统时钟除以2,也就是36MHz。虽然两者都能生成115200的波特率,但USART_BRR的量化精度不同。在48MHz系统时钟这种非整数倍情况下,串口3的波特率误差可能比串口1更大。

从代码上看,串口3的RCC使能要放在APB1组,GPIOB的RCC使能还是放在APB2组。我们有下面这个对照表:

配置项USART1USART3
总线APB2APB1
TX/RX引脚PA9/PA10PB10/PB11
RCC时钟组RCC_APB2Periph_USART1RCC_APB1Periph_USART3
引脚时钟RCC_APB2Periph_GPIOARCC_APB2Periph_GPIOB
TX GPIO模式AF_PPAF_PP
RX GPIO模式IN_FLOATINGIN_FLOATING
DMA通道(发送)DMA1_Channel4DMA1_Channel2
中断IRQnUSART1_IRQnUSART3_IRQn

这里最常被忽视的是DMA通道。USART1_TX使用DMA1_Channel4,USART3_TX使用DMA1_Channel2。如果按串口1的通道配置去驱动串口3,DMA不会触发请求,发送中断永远不进来。

注意:USART1和USART3的RX引脚输入模式都是浮空。如果外部RS485芯片控制方向,TX和RX可能复用同一对引脚,需要额外用GPIO控制DE/RE引脚。stm32f103基于cubemx hal库的485收发程序很多,但标准库下同样需要把DE/RE引脚当作普通GPIO操作,在发送前拉高,发送后拉低。

4.1.1 串口3初始化样例

下面是标准库下配置串口3的最小代码段:

RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, &GPIO_InitStructure);

这段代码与串口1的差异一目了然。GPIOB的引脚编号和RCC寄存器不同,但USART_Init结构体完全相同。关键是USART_Init函数会根据传入的USART3选择内部对应的总线时钟频率,而该频率来自RCC_GetClocksFreq,因此必须先使能USART3的时钟,否则读取到0会导致波特率计算异常。

4.2 外部晶振频率对串口波特率的影响

stm32f103标准库默认HSE_VALUE是8MHz。SystemInit会根据HSE_VALUE和RCC_CFGR的PLL倍频系数把系统时钟设为72MHz。如果你的最小系统板上焊的是12MHz晶振,而没有修改stm32f10x.h中的HSE_VALUE和PLL设置,那么APB2时钟变成108MHz甚至超出规格,串口输出乱码。

在调试Uart实验中,如果PC端收到的全是乱码,先检查这一点:

#define HSE_VALUE ((uint32_t)8000000)

将宏改为实际晶振频率,再确认RCC_CFGR中的PLLMULL为9倍,得到72MHz。使用8MHz晶振时,HSE_VALUE不变,但PLL倍频系数应为9;使用12MHz晶振时,倍频系数要调整为6,凑成72MHz。

注意:某些串口下载器(如FT232R、CP2104)驱动异常也会导致乱码,但这类问题通常表现为完全无数据或一打开串口就报错,不会出现规律性乱码。如果字符大部分正确,偶尔错一两个,说明波特率偏移超出容忍范围。

4.3 Boot0和Boot1对串口下载与调试的影响

实验过程中经常遇到下载失败。用UART1下载程序时需要把BOOT0置1,BOOT1置0,让芯片进入系统存储器启动。此时内置bootloader接管USART1。下载完成后把BOOT0跳回0,按复位键运行用户程序。

如果使用ST-LINK/J-Link下载,BOOT0不需要置1,但前提是程序没有关闭SWD引脚。有些例程为了省电,把PA13/PA14复用为普通GPIO,会导致下次无法用DAP连接。解决办法是:按住复位键不放,连接调试器,配置keil在连接前复位,然后擦除整个Flash。这个操作里BOOT0和BOOT1的跳线也要确认处在用户Flash模式。

提示:stm32f103 dap下载失败 boot1这个热搜词的场景,多半是跳线帽插错或者BOOT1悬空导致电平不确定。最简单的处理方式是BOOT1直接接GND,BOOT0用跳线帽,这样大多数下载问题都能排除。

5. 进阶:printf重定向与DMA串口发送

5.1 通过fputc重定向printf到UART

标准库开发中打印调试信息最方便的方法就是重定向fputc。在MDK工程中添加:

#include <stdio.h> int fputc(int ch, FILE *f) { USART_SendChar(USART1, (uint8_t)ch); return ch; }

同时在工程设置中勾选Use MicroLIB。如果不勾选,printf内部的文件系统接口会走半主机模式,在嵌入式环境里会卡死。这里有一个技巧:如果发送字符串长度在几十字节内,只要等待TXE标志,不需要等TC。这样fputc返回很快,但最后几个字节仍在移位寄存器中。如果你接下来要操作串口1的DMA或直接修改USART外设,需要先调用:

while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);

实际上,在标准库中USART_SendChar内部等待TXE,这已经能保证数据寄存器空,但不能保证发送移位完成。所以临界操作前等待TC是防止丢最后一个字符的标准做法。

5.2 DMA单次发送模式

高频打印或者通过串口发送波形数据时,轮询发送会占用大量CPU时间。stm32f103标准库可以使用DMA1通道4为USART1发送提供搬运。

DMA_InitTypeDef DMA_InitStructure; static uint8_t tx_buf[128]; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel4); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)tx_buf; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize = 128; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel4, &DMA_InitStructure); USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); DMA_Cmd(DMA1_Channel4, ENABLE);

逻辑说明:DMA_PeripheralBaseAddr指向USART1->DR,DMA_DIR_PeripheralDST表示数据由内存搬到外设。MemoryInc使能后,DMA每传输一个字节自动将内存地址加1。DMA_Mode_Normal表示传输完指定数量字节后停止,不再自动重装。

使用时每次要发送新数据,先关闭DMA,更新DMA_MemoryBaseAddr和DMA_BufferSize,重新使能。比如:

void UART_DMA_Send(uint8_t* data, uint16_t len) { DMA_Cmd(DMA1_Channel4, DISABLE); DMA1_Channel4->CMAR = (uint32_t)data; DMA1_Channel4->CNDTR = len; DMA_Cmd(DMA1_Channel4, ENABLE); }

注意不要在DMA还在传输时修改内存数组,否则数据会被部分覆盖。

5.3 检查发送完成的两种方式

DMA传输可以在中断里等待DMA1_Channel4_IRQn触发,也可以在主循环中轮询DMA_GetFlagStatus(DMA1_FLAG_TC4)。为了实验简单,我们可以在UART_DMA_Send后轮询:

while (DMA_GetFlagStatus(DMA1_FLAG_TC4) == RESET); DMA_ClearFlag(DMA1_FLAG_TC4);

TC4标志表示DMA通道4传输完成,但并不代表USART移位寄存器已经发送完成。如果接下来要关闭USART或进入低功耗,还需要等待USART_FLAG_TC。这两个标志对应不同阶段,很多人混淆。

验证完整效果时,用逻辑分析仪观察PA9引脚,在115200波特率下每个bit约8.68us,发送字符'1'即0x31,起始位低电平,然后LSB起始数据01000110,停止位高电平。如果你看到波形只对了一半,检查DMA_MemoryInc是否设置为Enable,如果忘记设置,每个字节都从同一个内存地址读取,波形会单调重复。

提示:stm32f103多路捕获实验和ADC采样实验中常需要同时采集多路模拟量,此时串口DMA发送就可以把CPU从逐字节搬移中解放出来。标准库下USART1_DMA通道固定为DMA1_Channel4,不可随意更改,想换通道只能换串口。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 1:15:58

MNE脑电数据预处理完整流程:从读取到保存的实战指南

写这份学习笔记之前&#xff0c;我翻了翻之前跑过的分析流程&#xff0c;发现真正卡住新手的往往不是某个算法不好懂&#xff0c;而是“读数据之后不知道下一步该干嘛”。MNE这个库本身功能很全&#xff0c;但全也意味着容易迷路&#xff1a;是先滤波还是先剪裁&#xff1f;用c…

作者头像 李华
网站建设 2026/9/16 1:15:41

OpenClaw 跑 Sub-agent 编排:Key 走 TaoToken

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 1:14:09

数据理解先行:疫情下的骑手行为预估实战复盘

“新冠期间饿了么骑士行为预估”这个赛题&#xff0c;我从拿到数据包到真正开始建模&#xff0c;中间隔了整整一周。这一周我几乎没碰模型&#xff0c;全在啃数据和业务逻辑。事后回头看&#xff0c;这一周恰恰是整个比赛周期里价值最大的一段时间——因为所有特征工程的灵感、…

作者头像 李华
网站建设 2026/9/16 1:12:38

基于greenlet协程的SDN实时流量控制框架

简介&#xff1a;本资源是一套基于SDN架构的网络流量监控与控制系统完整Python实现&#xff0c;面向计算机专业本科生、研究生及网络开发初学者&#xff0c;适用于毕业设计、课程大作业与SDN实践项目。项目采用OpenFlow协议对接控制器&#xff08;如Ryu或POX&#xff09;&#…

作者头像 李华
网站建设 2026/9/16 1:11:05

RTL-SDR与gr-gsm实战:从天线到Wireshark的GSM空口信号解码

写这篇东西的起因挺朴素&#xff1a;有天我收拾房间翻出一根吃灰多年的 RTL-SDR 电视棒&#xff0c;随手接上电脑扫了一圈频谱&#xff0c;发现原本以为早就“退网”的 GSM 频段里竟然还有活跃的信号。GSM 在我印象里是诺基亚 3310 时代的东西&#xff0c;实际上一查才知道&…

作者头像 李华