基于STM32的智能天气时钟
1. GPIO


定义初始化结构体
GPIO_InitTypeDef GPIO_InitStruct;
作用:定义一个名为
GPIO_InitStruct的结构体变量,用于存储GPIO的配置参数。说明:
GPIO_InitTypeDef是库中定义的结构体类型,包含GPIO的所有可配置项。
2. 结构体默认初始化
GPIO_StructInit(&GPIO_InitStruct);
作用:用默认值填充结构体,避免未初始化的随机值。
各参数详细含义:
| 参数 | 值 | 说明 |
|---|---|---|
| GPIO_Mode | GPIO_Mode_AF | 复用功能模式,引脚用于UART、SPI、I2C等外设 |
| GPIO_PuPd | GPIO_PuPd_UP | 内部上拉电阻,引脚默认电平被拉高 |
| GPIO_OType | GPIO_OType_PP | 推挽输出,可输出高电平和低电平,驱动能力强 |
| GPIO_Speed | GPIO_Speed_50MHz | 高速模式,适合高频信号传输 |


无论是使用标准库还是HAL库,其本质上都是进行读写寄存器的操作:
标准库 (SPL):寄存器视角的抽象
设计哲学:“让我来帮你操作寄存器,但你仍然需要知道你在操作哪个外设的哪个功能。”
示例:
USART_SendData(USART1, data);分析:这个函数很清晰,它帮你完成了向USART1的数据寄存器(DR)写入数据的操作。但你仍然需要知道是USART1,并且在此之后,你可能还需要手动检查状态寄存器(SR)的“发送完成”标志(TC)来判断数据是否发完。它抽象了“写寄存器”这个动作,但没有抽象“发送一帧数据”这个完整业务流程。
HAL库:业务逻辑视角的抽象
设计哲学:“告诉我你想做什么(What),别管我怎么做(How)。我会管理整个流程和状态。”
示例:
HAL_UART_Transmit(&huart1, &data, 1, 1000);分析:这个函数接受一个句柄(Handle),它封装了UART的所有状态和配置。你告诉它:“用huart1这个串口,发送1个字节的数据data,我给你1000ms的超时时间。” 在函数内部,它可能做了以下一系列寄存器操作:
检查状态寄存器(SR)判断串口是否就绪。
向数据寄存器(DR)写入数据。
等待状态寄存器(SR)的TC标志置位,或者等待超时。
如果超时,会设置错误代码。
关键:HAL为你管理了状态机、超时机制和错误处理。这是它比标准库抽象层次更高的根本体现。
2. 串行通信原理与应用

通过逻辑分析仪分析串口发送数据时的特征:
channel1(TX)为电脑向单片机的串口发送数据,channl0(RX)为单片机接收电脑端发送的数据。这里向单片机发送字符“A”,其ASCII码为65,转换为二进制为1000 0001。

上面传输时,没有设置校验位,如果设置校验位,会在停止位(Stop bit)之前增加一个bit的奇偶校验位(计算传输的二进制数据中1的个数)。
常用模式:
1152000bps:表示每秒传输1152000比特位,那么在没有奇偶校验位的情况下,每秒最多可以传输 1152000%(1+8+1)=115200个字节数据,注意这里需要加上起始位和停止位。
8N1:表示8位传输数据,没有校验位,1位停止位。
3. 如何编程

3.1 串口编程时遇到的问题
使用for()循环由电脑向单片机串口发送字符“hello world”,
代码如下:

这里的str[]=“hello world”。
a. USART_ClearFlag(USART1, USART_FLAG_TC);
* 这是确保发送可靠的关键一步。
* USART_FLAG_TC 是 "Transmission Complete"(发送完成) 标志位。
* 这个标志位的含义是:发送移位寄存器中的数据已经全部移位发出了,并且数据寄存器(TDR)也是空的(即整个发送流程彻底完成)。
* 在发送新一个字节之前,先清除这个标志位,是为了避免之前可能遗留的已完成状态影响本次发送的等待判断。
b. USART_SendData(USART1, str[i]);
* 这是启动发送的命令。
* 函数将你要发送的字符(str[i])写入到USART1的发送数据寄存器(TDR)。
* 一旦数据被写入TDR,USART外设就会自动开始发送过程(加入起始位、停止位等,并通过TX引脚一位一位地发送出去)。此时,TC标志位会被硬件自动清零。
c. while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
* 这是一个阻塞等待循环,直到当前字节发送完成。
* USART_GetFlagStatus 函数用于检查 TC 标志位的状态。
* RESET 通常等于0,表示标志位未被设置。
* 只要 TC 标志位为 RESET(即发送未完成),CPU就会一直在这里空循环等待。
如果不加上while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);那么单片机的串口可能只会接收到最后一个字符“d”。
这是因为CPU执行速度(纳秒级)远远快于串口发送速度(毫秒级)。
例如,在115200波特率下,发送1个字节(8位数据+1起始位+1停止位=10位)需要的时间大约是:
10 bits / 115200 bits-per-second ≈ 87 μs
而CPU执行一条指令可能只需要几十纳秒(0.000087 ms)。没有等待的话,CPU可以在87μs内执行成百上千条指令。
同时,上面的代码看起来是没有什么问题,但是如果使用逻辑分析仪观察时,就会发现两个字符发送的时间是有时间间隔的:

原因:在for()循环中每次都需要判断:i < strlen(str),而strlen(str)是一个函数,每次调用这个函数都需要一定的时间,这就造成了发送的相邻的两份字符之间存在时间间隔。
解决方法:

3.2 串口编程的方法



4. 按键

4.1按键代码编写

5. 中断

EXTI为外部\事件中断控制器,可以连接GPIO引脚;
外部中断是指当某一模块发生作用时(比如说按键按下),其响应是发送至CPU进行处理的;事件中断是指当某一模块发生作用时,其响应是由其他模块进行处理的。
NVIC为嵌套向量中断控制器,用于设置中断的优先级,因为会存在很多中断,如外部\事件中断(EXTI)、串口中断(UART)和定时器中断。
对于不同的中断源需要进行不同的设置(初始化),例如对于外部\事件(EXTI),可能需要设置GPIO口的哪一个引脚连接到外部中断控制器的引脚上;对串口中断,可能需要设置是发送数据之前发生中断,还是接收到数据之后产生中断;对于定时器中断,可能需要设置每隔一定的时间的时间产生中断。

| 特性 | Modbus协议 | 串口通信 (UART) | I2C协议 | SPI协议 |
|---|---|---|---|---|
| 本质 | 应用层协议 | 物理层/数据链路层协议 | 硬件通信协议 | 硬件通信协议 |
| 通信架构 | 主从架构 (1主多从) | 对等架构 (点对点) | 主从架构 (1主多从) | 主从架构 (1主多从) |
| 信号线 | 依赖底层硬件 (如RS-485) | TX (发送), RX (接收), GND | SDA (数据), SCL (时钟) | SCLK (时钟), MOSI (主出从入), MISO (主入从出), SS/CS (片选) |
| 通信方式 | 半双工 (RS-485) | 全双工 | 半双工 | 全双工 |
| 同步方式 | 异步 | 异步 | 同步 (由主设备提供SCL时钟) | 同步 (由主设备提供SCLK时钟) |
| 拓扑结构 | 总线型 (可挂载多个设备) | 点对点 (通常1对1) | 总线型 (可挂载多个设备) | 点对点或星型 (每个从设备独立片选) |
| 寻址方式 | 软件地址 (从站地址) | 无地址 | 硬件地址 (7位/10位地址) | 硬件片选 (通过CS引脚选择从设备) |
| 速度 | 慢 (依赖底层硬件,常用9600-115200bps) | 慢 (常用9600-115200bps) | 中速 (标准模式100kbps,快速模式400kbps) | 高速 (通常10+Mbps,甚至更高) |
| 软件复杂度 | 高 (需实现协议栈) | 低 (配置好参数即可收发) | 中 (需实现字节级收发和协议) | 低 (主要是硬件移位寄存器操作) |
| 硬件复杂度 | 低 (依赖现有硬件接口) | 低 (MCU基本都自带) | 非常低 (2根线,无需片选) | 高 (线多,每个从设备需独立片选) |
| 主要应用场景 | 工业控制、物联网 (PLC、传感器、电表) | 调试日志、GPS模块、老旧设备 | 板内通信 (传感器、EEPROM、RTC) | 高速板内通信 (Flash、SD卡、显示屏、ADC) |
5.1如何使用此驱动
要将I/O引脚用作外部中断源,请按以下步骤操作:
(#) 使用GPIO_Init()将I/O配置为输入模式
(#) 使用SYSCFG_EXTILineConfig()选择EXTI线路的输入源引脚
(#) 使用EXTI_Init()选择模式(中断/事件)并配置触发方式(上升沿/下降沿/双边沿)
(#) 使用NVIC_Init()配置映射到EXTI线路的NVIC中断请求通道
[..]
(@) 必须启用SYSCFG APB时钟才能通过RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE)获得对SYSCFG_EXTICRx寄存器的写访问权限
下面分步写代码:
1.初始化外设时钟:

2. 中断优先级组配置:

3.配置GPIO--初始化设置:

4.使用SYSCFG_EXTILineConfig()选择EXTI线路的输入源引脚:将GPIOC口的引脚4和5设置为中断模式

5.使用EXTI_Init()选择模式为中断,并配置触发方式上升沿:

6.使用NVIC_Init()配置映射到EXTI线路的NVIC中断请求通道:

5.2 中断服务函数
中断服务函数,也叫中断处理器,是一段特殊的、预先写好的函数。当微控制器的中断发生时,CPU会自动暂停当前正在执行的主程序,跳转到这个函数来运行。运行完毕后,CPU再返回主程序继续执行。
中断服务函数的特点
特殊的函数名:编译器通过固定的函数名来识别ISR。
例如:
SysTick定时器中断服务函数:

按键中断服务函数:

6. 回调函数
函数本身是一个地址,我们可以把函数的地址赋值给另外一个变量,那么这个变量存放的就是函数的地址,所以这个变量就是指针变量,也就是函数指针。通过调用函数指针,实现对函数的间接访问。
定义了一个函数指针类型keyfunc,然后声明了三个该类型的静态全局函数指针变量(keyl_func_t、key2_func_t和key3_func_t)。




执行完上面的代码后:
key1_func = led1_toggle;


7. 定时器
定时器分为高级定时器、通用定时器和基本定时器。
定时器最基本的功能就是计数器,基本定时器结构如下:

系统时钟RCC通过预分频器PSC可以将系统时钟频率进行分频,计数器CNT进行累加。
通用定时器的结构如下:

通用定时器除了可以接受来自系统的时钟,还可以接收来自外部时钟TIMx_CH1,当接收来自外部时钟TIMx_CH1时,可以捕获引脚的波形。
捕获(Capture)功能是单片机定时器的一个硬件特性,它能够在外部引脚发生特定电平跳变时,自动记录下定时器的当前值。
核心目的:精确测量外部信号的时间参数(脉宽、周期、频率)。
核心优势:硬件自动完成,速度快,精度高,不占用CPU资源,只有在需要读数时才通过中断通知CPU。
核心流程:设置触发条件(边沿)→ 硬件自动抓拍时间戳并存入CCRx → 产生中断 → CPU读取并处理。
7.1 如何编程

8. I2C协议


上面是I2C协议的硬件实现,对应的是GPIO口的开漏输出,即P-MOS断开只有N-MOS可以使用,设备1和设备2均可以使SCL为高电平或低电平。

8.1程序编写

8.2 I2C时序

I2C有两根线,一根数据线SDA和一根时钟线SCL。
起始条件 (Start Condition) 和停止条件 (Stop Condition)
起始条件 (S):当 SCL为高电平 时,SDA发生一个从高到低 的电平跳变。这个状态是唯一的,表示一次传输的开始。
停止条件 (P):当 SCL为高电平 时,SDA发生一个从低到高 的电平跳变。这个状态表示一次传输的结束。
一个典型的I²C数据传输帧由以下部分组成:
起始条件 (S)
从机地址 (Slave Address):7位或10位地址,用于选择总线上的哪个从机。
读写位 (R/W#):1位,表示主机是想写数据到从机(0)还是从从机读数据(1)。
应答位 (ACK):从机回应地址匹配信号。
数据字节 (Data Byte):8位的数据,可以是命令、寄存器地址或实际数据。
应答位 (ACK):在每一个数据字节后都有,当有从机与从机地址匹配的上时,SDA会变成低电平,否则为高电平。
...(重复步骤5和6以传输更多数据)
停止条件 (P) 或 重复起始条件 (Sr)。重复起始条件在不释放总线的情况下开始一个新的通信序列。
传输数据时,数据线SDA和时钟线SCL的高低电平变化:
SCL为高电平期间,SDA线上的数据必须保持稳定(不变)。此时的数据位有效,接收方(主机或从机)会在这个时刻采样(读取)SDA的值。
SCL为低电平期间,SDA线上的电平才允许发生变化,为传输下一位数据做准备。
9 SPI协议


SPI vs. I2C 核心特性对比表
| 特性 | SPI (Serial Peripheral Interface) | I2C (Inter-Integrated Circuit) |
|---|---|---|
| 全称 | 串行外设接口 | 内部集成电路 |
| 线路数量 | 4线制(SCLK, MOSI, MISO, SS) (每增加一个从机,需增加一条SS线) | 2线制(SDA, SCL) (支持大量从机而无需增加线路) |
| 通信方式 | 全双工 (Full-Duplex) (数据可同时收发) | 半双工 (Half-Duplex) (数据分时收发,同一时刻只能向一个方向传输) |
| 拓扑结构 | 主从式,点对点或菊花链 | 主从式,多主多从(所有设备都挂载到总线上) |
| 从机选择方式 | 硬件片选 (SS/CS) 主机通过专用引脚拉低来选中从机 | 软件地址寻址 主机在数据传输开始时发送一个从机地址来选中它 |
| 数据传输速率 | 非常高(通常可达几十甚至上百MHz) | 中低速(标准模式100kHz,快速模式400kHz,高速模式3.4MHz) |
| 协议复杂度 | 非常简单 只有移位时钟,无格式要求。数据就是数据。 | 相对复杂 有起始位、停止位、应答位(ACK/NACK) 等固定格式。 |
| 有无时钟 | 同步(有独立的时钟线SCLK) | 同步(有独立的时钟线SCL) |
| 流控与应答 | 无硬件应答机制 主机无法知道从机是否成功接收数据。 | 有硬件应答机制 每个字节后,接收方必须发送一个ACK位,确保数据被确认接收。 |
| 方向控制 | 通过独立的信号线(MOSI和MISO)实现 | 通过协议和地址中的读写位控制方向 |
| 功耗 | 相对较高(线路多,速度通常更快) | 相对较低(线路少,上拉电阻阻值可以很大) |
| 专利与版权 | 无版权,事实标准 | 由NXP(原飞利浦)管理,但无需授权费 |
| 典型应用场景 | 需要高速数据传输的场合: • 存储器(Flash, EEPROM) • SD卡 • 传感器(高精度ADC) • 显示屏(OLED, TFT) | 需要连接大量设备且速度要求不高的场合: • 传感器(温度、湿度) • 实时时钟(RTC) • I/O端口扩展器 • EEPROM |
如何选择?
选择 SPI 当:
你需要非常高的速度。
你连接的从设备不多,且主机的IO口引脚足够。
你的从设备只支持SPI(如很多ADC和显示屏)。
你不需要硬件确认数据是否接收。
选择 I2C 当:
你的速度要求不高。
你需要连接很多设备(超过3-4个),但想节省主机的IO引脚。
你的电路板空间紧张,希望布线简单(只有2根线)。
数据可靠性很重要,你需要硬件应答机制。
简单总结:SPI追求速度和简单,I2C追求引脚效率和可靠性。

