本项目是一个基于 ESP32-S3的物联网应用程序,主要功能是实现摄像头视频流传输、串口数据交互和 MQTT 协议通信,构建了一个完整的 "感知 - 传输 - 控制" 物联网设备系统。实现了:
- 数据上行:串口设备数据通过 MQTT 上报。
- 视频传输:摄像头数据通过 TCP 实时发送到服务器。
- 远程控制:通过 MQTT 接收命令,控制视频流开关和外部设备。
一 .工具介绍:
1. 硬件概述
ESP32-S3物联网网关,具有以下功能:
-
将串口接收的数据转发到MQTT服务器
-
将MQTT订阅的命令转发到串口
-
根据MQTT命令控制视频流传输
-
实现稳定的网络连接和错误处理
stm32F103
- 作为串口设备连接ESP32_S3
- 模拟物联网设备通过串口发送数据给esp32-S3
- 接收MQTT控制命令执行相应操作(本例程仅以控制led开关为例)
2.ESP32-S3
2.1 与esp32-cam的比较
我买的是乐鑫科技的ESP32-S3,也是阴差阳错买的,因为原来用的esp32-cam在使用Tonny下载程序老出问题,应该是因为在同时使用WiFi和摄像头时的供电不足,原因分析如下:
(1)单独拍照正常:执行拍照程序能正常工作,证明摄像头硬件和初始化代码没问题。
(2)WiFi+摄像头同时工作失败:当同时启用WiFi和摄像头时出现OSError: Camera Init Failed
,这是典型的供电不足表现
(3)在网上也搜到了其他博主类似问题,然后他换了5v电压源之后就解决了。然后我在换电源时候,不太会整给烧了
(4)也是最重要的,因为我换了这个板子,基于之前代码修改后成功了,说明之前代码没问题,但是它毕竟是esp32的分支,准备工作还是有些不一样,下面说明:
其实在Tonny用的python解释器还是一样,都是MicroPython (ESP32),只是下载固件不一样,硬件设计肯定不一样啊。
当时烧录的客服给的资料里的S3固件,但是打开之后交互页面错误一直刷配置信息啥的,后来用下面博客里给的固件烧录之后就正常用了,大概就是直接在ubuntu里改固件配置,没仔细看博主最后有直接提供固件文件,太感谢了:(超详细!)一文详解esp32s3编译micropython固件,驱动摄像头,拍摄第一张照片_esp32s3cam micropython-CSDN博客
2.2 ESP32-S3概述
ESP32-S3 是一款集成 2.4 GHz Wi-Fi 和 Bluetooth 5 (LE) 的 MCU 芯片,支持远距离模式 (Long Range)。ESP32-S3 搭载 Xtensa® 32 位 LX7 双核处理器,主频高达 240 MHz,内置 512 KB SRAM (TCM),具有 45 个可编程 GPIO 管脚和丰富的通信接口。ESP32-S3 支持更大容量的高速 Octal SPI flash 和片外 RAM,支持用户配置数据缓存与指令缓存。
3. stm32F103
我用的是实验室里的STM32F103C8T6型号,这个板子有点特殊,使用虚拟串口,如下图用户手册截图,也就是说,它连接不了esp32的串口接口,只能 USB线连接电脑然后安装专用 USB 转串口驱动程序在电脑上使用串口。如下图接口“USB虚拟串口”。所以项目实现有点不一样,详情往下看,文字说明还不如放到后面项目实现里讲更容易理解。
实物图:
4.Tonny--python IDE
4.1 概述
Thonny 是一款专为初学者设计的 Python 集成开发环境(IDE),以下是根据官网展示的特点对它的简要介绍:
- 易于上手:Thonny 内置了 Python 3.10,用户只需安装一个简单的安装程序,就可以开始学习编程。即使已经安装了独立的 Python 环境,也可以在 Thonny 中使用。并且,Thonny 初始用户界面去除了所有可能会分散初学者注意力的功能,让新手能够专注于编程学习。
- 变量查看便捷:在完成基础的编程练习(如输出 "Hello world!" )之后,用户可以通过选择 “View”(视图) -> “Variables”(变量),来查看程序和 Shell 命令对 Python 变量的影响,帮助初学者理解变量在程序执行过程中的变化。
- 简单的调试器:Thonny 提供了简单易用的调试功能。按下 Ctrl+F5 而不是 F5,就可以逐行运行程序,无需设置断点。按下 F6 可执行较大的步骤(跳过函数内部执行),按下 F7 可执行较小的步骤(进入函数内部执行)。调试步骤遵循程序结构,而不仅仅是代码行顺序,便于用户理解程序执行逻辑。
当然,想要了解更多IDE信息可以自行搜索,我觉着这里重点是想引入下面内容:Thonny还支持MicroPython编程,使得它在单片机和物联网开发中也颇受欢迎。
4.2 micropython
1.基于Python 3的微控制器编程语言
MicroPython是Python 3编程语言的精简实现,专为微控制器及资源受限的嵌入式系统设计,保留了Python核心语法特性并优化了内存占用 。其技术架构包含独立解析器、编译器和虚拟机,支持GPIO、I2C、SPI等硬件接口的直接操作,可通过REPL交互环境实现实时调试 。该语言兼容ESP32、Raspberry Pi Pico、Pyboard等主流开发板,广泛应用于物联网设备开发、智能硬件原型设计及嵌入式教育领域。
2. 应用场景
物联网终端
通过集成MQTT协议栈与Wi-Fi模块,MicroPython可实现传感器数据采集与云端同步。典型案例包括:
-
环境监测系统:集成DHT11温湿度传感器与OLED显示屏,每5分钟上传数据至云平台
-
智能灌溉装置:基于土壤湿度传感器触发继电器,实现自动化浇水与异常报警
来自百度百科,可以自行搜索更多相关内容。
二.项目实现
因为项目很多步骤很复杂,起初运行结果不对也不知道哪个部分有问题,然后一部分一部分测试好像顺利多了,可以定位问题。
1.串口通信测试
基于 ESP32-S3 的 UART(串口)通信测试程序,主要功能是通过指定的串口引脚发送数据并接收响应,同时通过 LED 提供视觉反馈。
1.1 下载esp32端代码
from machine import UART, Pin
import time
# 初始化UART0 - 使用正确的引脚
uart = UART(1, baudrate=115200, tx=43, rx=44)
# 添加LED用于视觉反馈(可选)
led = Pin(48, Pin.OUT) # 使用GPIO48作为状态指示灯
print("ESP32-S3 UART测试开始...")
try:
while True:
led.value(1) # LED亮表示发送中
uart.write("hello\r\n")
print("已发送: hell0")
# 等待响应
time.sleep(0.5)
led.value(0) # LED灭
# 检查并读取响应
if uart.any():
response = uart.read()
try:
# 尝试解码响应
decoded = response.decode('utf-8').strip()
print(f"收到响应: {decoded}")
except:
print(f"收到原始数据: {response}")
else:
print("未收到响应")
# 等待2秒后重试
time.sleep(2)
except KeyboardInterrupt:
print("程序已停止")
1.2 主要用途
- 验证 ESP32-S3 的 UART1(tx=43, rx=44)引脚是否正常工作。
- 测试与外接串口设备(如传感器、单片机、PC 等)的通信是否畅通。
- 通过 LED 和控制台输出,直观展示发送 / 接收状态,便于调试。
至于串口引脚,查看模块原理图,可以发现引出的是43,44,反正我找手册半天没发现有说明💔:
1.3 结果验证
可以看到esp32端代码实现,
- 发送数据时点亮 LED,提供视觉反馈。
- 串口发送 "hello\r\n"(
\r\n
是回车换行符,确保接收方正确识别一条完整消息)。 - 通过
uart.any()
判断是否有数据到达,避免空读。 - 支持两种数据处理方式:能解码为 UTF-8 字符串时正常显示,解码失败(如接收二进制数据)时显示原始字节,提高兼容性。
电脑串口端接收到hello并且向设备发送“dong”:
Tonny运行结果:
1.4 思路
综上,也就是说,我们可以把电脑充当串口设备,通过usb转ttl以及串口调试助手实现串口设备发送数据到esp32设备。当然通信可以双向的,esp32实现的网络功能,接收到指令可以发给串口物联网设备,我们把电脑当作物联网设备接收指令,然后显示串口接收指令。那么只是可视化接收到的指令,反向控制并没有实现,需要串口实际接到stm32芯片串口,stm32收到指令进行操作。因为我的stm32f103是虚拟串口只能连接电脑而不是标准串口接口rx,tx,所以电脑接收到指令再串口转发给stm32,然后它读取到串口数据,执行操作开关灯这种,其实就是中间加了一个桥梁。
移植也很简单,直接esp32设备串口tx,rx不连接usb转串口的tx,rx而是物联网设备芯片的tx,rx就行。
2. esp32 代码实现
因为网络通信部分以及视频流传输部分之前文章详细介绍过,这里不再详细说明,如果感兴趣可以查看之前文章。
import socket
import network
import camera
import time
import ubinascii
import sys
from machine import UART, Pin, reset
from umqttsimple import MQTTClient
# 配置参数
WIFI_SSID = 'wifi名字'
WIFI_PASSWORD = 'wifi密码'
MQTT_SERVER = 'MQTT服务器IP'
MQTT_PORT = 1883 #MQTT服务端口号
MQTT_CLIENT_ID = 'esp32cam_' + ubinascii.hexlify(network.WLAN().config('mac')).decode()
MQTT_DATA_TOPIC = b"device/data" # 设备数据主题
MQTT_COMMAND_TOPIC = b"device/command" # 设备控制命令主题
MQTT_VIDEO_TOPIC = b"camera/control" # 视频控制主题
TCP_SERVER = "公网服务器ip"
TCP_PORT = 9877 #视频流tcp传输端口
# 全局状态
streaming_enabled = False # 视频流传输状态
tcp_socket = None # TCP套接字
mqtt_client = None # MQTT客户端
# 初始化串口
uart = UART(1, baudrate=115200, tx=43, rx=44)
def connect_wifi():
"""连接WiFi"""
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
print('连接WiFi:', WIFI_SSID)
wlan.connect(WIFI_SSID, WIFI_PASSWORD)
# 等待连接,最多30秒
for i in range(30):
if wlan.isconnected():
break
print('连接中...', i)
time.sleep(1)
if wlan.isconnected():
print('网络配置:', wlan.ifconfig())
return True
else:
print('WiFi连接失败')
return False
def init_camera():
"""初始化摄像头"""
try:
# 尝试初始化摄像头
camera.init(0, format=camera.JPEG)
print("摄像头初始化成功")
# 摄像头设置
camera.flip(1) # 上下翻转
camera.mirror(1) # 左右镜像
camera.framesize(camera.FRAME_HVGA) # 分辨率
camera.speffect(camera.EFFECT_NONE) # 特效
camera.saturation(0) # 饱和度
camera.brightness(0) # 亮度
camera.contrast(0) # 对比度
camera.quality(10) # 质量
return True
except Exception as e:
print("摄像头初始化失败:", e)
try:
camera.deinit()
time.sleep(1)
camera.init(0, format=camera.JPEG)
return True
except:
print("摄像头重试失败")
return False
def connect_mqtt():
"""连接MQTT服务器"""
global mqtt_client
try:
# 创建MQTT客户端
mqtt_client = MQTTClient(MQTT_CLIENT_ID, MQTT_SERVER, MQTT_PORT)
mqtt_client.set_callback(mqtt_callback)
mqtt_client.connect()
print("MQTT连接成功")
# 订阅命令主题
mqtt_client.subscribe(MQTT_COMMAND_TOPIC)
mqtt_client.subscribe(MQTT_VIDEO_TOPIC)
print("已订阅主题:", MQTT_COMMAND_TOPIC, MQTT_VIDEO_TOPIC)
return True
except Exception as e:
print("MQTT连接失败:", e)
return False
def mqtt_callback(topic, msg):
"""MQTT回调函数"""
global streaming_enabled
print(f"收到MQTT消息: {topic} => {msg}")
try:
# 视频控制命令
if topic == MQTT_VIDEO_TOPIC:
cmd = msg.decode().lower()
if cmd == "on":
streaming_enabled = True
print("视频流已启用")
elif cmd == "off":
streaming_enabled = False
print("视频流已禁用")
return
# 设备控制命令 - 转发到串口
if topic == MQTT_COMMAND_TOPIC:
uart.write(f"{msg}\r\n")
print(f"命令已转发到串口: {msg}")
except Exception as e:
print("处理MQTT消息错误:", e)
def init_tcp_connection():
"""初始化TCP连接"""
global tcp_socket
try:
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tcp_socket.connect((TCP_SERVER, TCP_PORT))
print(f"已连接到TCP服务器 {TCP_SERVER}:{TCP_PORT}")
return True
except Exception as e:
print(f"TCP连接失败: {e}")
return False
def send_video_frame():
"""发送视频帧"""
global tcp_socket
try:
# 捕获图像
buf = camera.capture()
buf_len = len(buf)
# 发送长度前缀
tcp_socket.sendall(str.encode("%-16s" % buf_len))
# 发送图像数据
tcp_socket.sendall(buf)
print(f"发送视频帧: {buf_len}字节")
return True
except Exception as e:
print("发送视频帧失败:", e)
try:
tcp_socket.close()
except:
pass
tcp_socket = None
return False
def process_serial_data():
"""处理串口数据"""
global mqtt_client # 声明全局变量
if uart.any():
try:
# 读取串口数据
data = uart.read()
# 尝试解码
try:
decoded = data.decode('utf-8').strip()
print(f"串口收到: {decoded}")
except:
decoded = str(data)
print(f"串口收到原始数据: {data}")
# 转发到MQTT
if mqtt_client is not None:
try:
mqtt_client.publish(MQTT_DATA_TOPIC, data)
print(f"数据已转发到MQTT: {decoded}")
except Exception as e:
print("MQTT发布失败:", e)
return True
except Exception as e:
print("处理串口数据错误:", e)
return False
return True
def main():
"""主程序"""
global streaming_enabled, tcp_socket, mqtt_client # 声明全局变量
# 初始化硬件
if not connect_wifi():
print("网络连接失败,5秒后重启...")
time.sleep(5)
reset()
if not init_camera():
print("摄像头初始化失败,继续运行其他功能...")
if not connect_mqtt():
print("MQTT连接失败,继续运行其他功能...")
# 主循环
last_video_time = time.time()
last_mqtt_check = time.time()
print("系统启动完成,开始主循环...")
try:
while True:
current_time = time.time()
# 处理MQTT消息
if mqtt_client is not None:
try:
# 定期检查消息
if current_time - last_mqtt_check > 0.1:
mqtt_client.check_msg()
last_mqtt_check = current_time
except Exception as e:
print("MQTT检查错误:", e)
# 尝试重新连接
try:
mqtt_client.disconnect()
except:
pass
mqtt_client = None
print("尝试重新连接MQTT...")
connect_mqtt()
# 处理串口数据
process_serial_data()
# 视频流处理
if streaming_enabled:
# 初始化TCP连接
if tcp_socket is None:
if not init_tcp_connection():
print("TCP连接失败,等待重试...")
time.sleep(2)
continue
# 控制帧率(约10FPS)
if current_time - last_video_time > 0.1:
if send_video_frame():
last_video_time = current_time
# 短暂延时
time.sleep_ms(10)
except KeyboardInterrupt:
print("程序被中断")
except Exception as e:
print("主循环错误:", e)
print("5秒后重启...")
time.sleep(5)
reset()
finally:
# 清理资源
print("清理资源...")
try:
if tcp_socket is not None:
tcp_socket.close()
except:
pass
try:
if mqtt_client is not None:
mqtt_client.disconnect()
except:
pass
try:
camera.deinit()
except:
pass
# 程序入口
if __name__ == "__main__":
# 添加全局异常捕获
try:
main()
except Exception as e:
print("未捕获的异常:", e)
print("10秒后重启...")
time.sleep(10)
reset()
2.1 网络通信
(1) WiFi 连接 connect_wifi()
(2) MQTT 客户端 connect_mqtt()
与消息处理
- 功能:创建 MQTT 客户端并连接服务器,订阅控制命令主题。
- 唯一性设计:客户端 ID 使用设备 MAC 地址,避免多设备冲突。
2.2 摄像头与视频流模块
(1) 摄像头初始化 init_camera()
- 功能:初始化摄像头并配置参数,失败时尝试重试。
(2) 视频流传输 init_tcp_connection()
与 send_video_frame()
- 传输协议:先发送数据长度(16 字节),再发送图像数据,便于服务器正确解析。
2.3 串口数据处理
2.3.1 功能
读取串口数据(来自外部设备)并通过 MQTT 上报。
定义了一个串口对象,串口设备可连接默认串口发送数据,通过 uart.any()检查是否有串口数据,然后uart.read() 读取数据,转发到MQTT服务器,这里虚拟串口电脑充当串口设备连接到esp32串口进行测试。
2.3.2 代码测试
1.串口调试助手
串口发送nihaoya到esp32
2.esp32设备端运行情况
3.MQTTX充当客户端订阅
可以看到,客户端订阅了设备上报数据主题device/data,服务器收到设备上报的数据就会转发给这个客户端
2.3.3 总结
也就是说,我们通过电脑(虚拟)串口向esp32串口发送数据,它读取串口数据并且转发到MQTT服务器上,可以被其他客户端订阅接收。
同理,反向控制逻辑也一样,esp32作为客户端订阅某一控制主题,收到相应指令,串口转发到要控制的设备,设备读取串口数据执行相应操作,比如读到了‘led_on’,就把led打开亮灯。
这里有两个控制逻辑,一是用户可以控制esp32物联网系统视频传输功能的开启关闭,这个其实就没有esp32接收网络数据后控制其他设备,esp32直接处理关闭视频流。
二是用户可以控制esp32物联网系统中其他设备,那么就是esp32接收到指令后不是它自己要做什么操作,只是把指令通过串口传输,另一个设备读取串口指令然后执行相应操作。
嗯这里顺便把视频传输功能的开启关闭测试结果说明,至于另一个测试结果,在下面stm32部分说明。
注意,视频流想要在内网电脑上显示,不仅仅涉及MQTT,之前文章讲过整的内网穿透,所以不要忘记开启fprc服务,然后mqtt服务呢,检查一下服务器上是不是运行了,mqtt连接端口1883,web管理网页18083端口是不是防火墙放行了,我明明之前配置好了,但是总是自个关上了。检查指令:
1. EMQX 服务状态检查
sudo systemctl status emqx
-
如果状态为
inactive (dead)
:说明服务未启动成功 -
如果状态为
active (running)
:检查端口监听情况
2. 端口监听检查
运行以下命令确认18083端口是否被监听:
sudo netstat -tuln | grep 18083
-
正常情况应显示:
0.0.0.0:18083
或:::18083
3.防火墙未开放端口(Ubuntu 20.04 默认使用 UFW):
sudo ufw status sudo ufw allow 18083 # 如果未放行则执行此命令
客户端向控制主题发布’on‘指令,esp32订阅该控制主题,收到指令,视频传输打开。
如下图,esp32设备对应上述两个控制逻辑,订阅主题device/command,camera/control
同时,esp32发布设备数据到主题device/data,其他客户端订阅可接收设备数据。
2.4 主程序
主程序是整个系统的调度中心,实现:
- 初始化硬件(WiFi、摄像头、MQTT)。
- 主循环轮询处理多任务:(1)定期检查 MQTT 消息(每 0.1 秒一次)(2)实时处理串口数据。(3)根据
streaming_enabled
状态控制视频流传输(约 10FPS)。 - 异常处理与资源清理。
3.串口设备stm32
3.1 esp32相关代码
上面是MQTT回调函数里一部分代码,也就是收到mqtt消息之后会自动调用的函数,esp32会检查消息主题,是否是视频控制命令还是设备控制命令,如果是设备控制命令,将接收的消息用串口函数发送到连接的串口设备stm32,接着就是stm32读取串口数据相应操作了
我测试的是esp32收到控制指令后,发送到电脑串口,如果非说逻辑有啥不一样的,应该就是电脑这个串口设备接收到指令之后,只是显示,而不是像stm32接收到led-on,之后打开led。
所以分为两个部分,电脑串口确实收到esp32发过来的led-on指令并且显示之后,再通过串口发给stm32,只是麻烦一些,但可能调试很方便。
3.2 代码测试
MQTTX客户端发送控制指令:led_on,led_off
esp32订阅device/command主题,收到mqtt消息后,发送串口数据给电脑显示:
电脑串口连接stm32并且发送刚刚接收的指令led_on,led_off; stm32读取串口,获得指令并且控制led模式:
说明:led初始状态,两个led交替闪烁。收到led_on之后,两个led常亮,收到led_off之后,两个led都灭。
视频:
1
3.3 stm32端代码分析
因为我正在学习stm32F103,参考正点原子的资料,所以我的工程模板在他们模板基础上改的,因为硬件有些不一样又不是一个厂家,而且我的是vct6,他们的战舰是ZE啥忘记了。
然后这是我的工程文件夹,可以发现其实只有main.c还有system不一样,一是主函数设计肯定不一样,二是system文件夹,是原子封装的串口,时钟,延时函数,还是很方便很好用的,然后这里提供主函数源码以及system文件夹修改使用说明。
3.3.1 main.c
#include "stm32f10x.h"
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include <string.h> // 添加字符串处理头文件
void led_init()
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); // 使能GPIOC端口时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; // 同时配置PC0和PC1
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // IO口速度为50MHz
GPIO_Init(GPIOC, &GPIO_InitStructure); // 初始化GPIOC
GPIO_SetBits(GPIOC, GPIO_Pin_0 | GPIO_Pin_1); // 初始状态:两个LED都熄灭
}
// LED控制函数
void led_set(uint8_t state)
{
if (state == 0) { // 关闭所有LED
GPIO_SetBits(GPIOC, GPIO_Pin_0);
GPIO_SetBits(GPIOC, GPIO_Pin_1);
} else if (state == 1) { // 开启所有LED
GPIO_ResetBits(GPIOC, GPIO_Pin_0);
GPIO_ResetBits(GPIOC, GPIO_Pin_1);
}
}
int main(void)
{
sys_stm32_clock_init(6);
delay_init(72); // 延时初始化
usart_init(72, 115200); // 初始化串口:时钟72MHz,波特率115200
led_init(); // 初始化LED
uint8_t t;
uint8_t len;
uint16_t times = 0;
uint8_t led_command = 0; // LED控制标志: 0=闪烁, 1=常亮, 2=熄灭
printf("系统启动\r\n");
printf("发送 'on' 开启LED,'off' 关闭LED\r\n");
while(1)
{
if (g_usart_rx_sta & 0x8000) // 接收到了数据
{
len = g_usart_rx_sta & 0x3fff; // 得到此次接收到的数据长度
// 添加结束符,将接收缓冲区转换为字符串
g_usart_rx_buf[len] = '\0';
printf("\r\n收到命令: %s\r\n", g_usart_rx_buf);
// 检查是否为"led_on"命令(不区分大小写)
if (strstr((char*)g_usart_rx_buf, "led_on") != NULL) {
led_set(1); // 开启所有LED
printf("LED已开启\r\n");
led_command = 1; // 设置LED常亮标志
}
// 检查是否为"led_off"命令(不区分大小写)
else if (strstr((char*)g_usart_rx_buf, "led_off") != NULL) {
led_set(0); // 关闭所有LED
printf("LED已关闭\r\n");
led_command = 2; // 设置LED熄灭标志
}
// 不是on/off命令,回显消息
else {
printf("您发送的消息为:\r\n");
for (t = 0; t < len; t++) {
USART_UX->DR = g_usart_rx_buf[t];
while ((USART_UX->SR & 0X40) == 0); // 等待发送结束
}
}
printf("\r\n\r\n"); // 插入换行
g_usart_rx_sta = 0; // 状态寄存器清零
}
else
{
times++;
// 根据LED控制标志决定行为
if (led_command == 0) {
// 默认模式:LED闪烁
if (times % 30 == 0) {
// 简单的闪烁逻辑
static uint8_t blink_state = 0;
if (blink_state == 0) {
GPIO_ResetBits(GPIOC, GPIO_Pin_0);
GPIO_SetBits(GPIOC, GPIO_Pin_1);
} else {
GPIO_SetBits(GPIOC, GPIO_Pin_0);
GPIO_ResetBits(GPIOC, GPIO_Pin_1);
}
blink_state = !blink_state;
}
}
else if (led_command == 1) {
// LED保持常亮 - 不需要额外操作
}
else if (led_command == 2) {
// LED保持熄灭 - 不需要额外操作
}
if (times % 5000 == 0) {
printf("\r\n正点原子 STM32开发板 串口实验\r\n");
printf("发送 'on' 开启LED,'off' 关闭LED\r\n");
printf("正点原子@ALIENTEK\r\n\r\n");
}
if (times % 200 == 0) {
printf("请输入命令或数据,以回车键结束\r\n");
}
delay_ms(10);
}
}
}
下面以咱的主要控制对象led为媒介,介绍主函数逻辑:
1.LED 初始化(led_init()
函数)
初始化连接到PC0
和PC1
引脚的两个 LED。
2.LED 状态控制(led_set()
函数)
通过state
参数统一控制两个 LED 的状态,简化主程序中的调用。
3.主循环中的 LED 逻辑(main()
函数)
主循环中通过led_command
标志变量控制 LED 的三种状态:默认闪烁、常亮、关闭
-
led_command
是核心控制变量,通过串口命令修改其值,进而改变 LED 行为。 -
串口命令处理
当串口接收到数据时,通过字符串匹配判断命令,并更新
led_command
: -
命令匹配:使用
strstr
函数判断接收的字符串中是否包含"led_on"
或"led_off"
(不要求完全匹配,只要包含关键词即可)。 -
状态更新:执行
led_set
改变 LED 当前状态,并将led_command
分别设为1
(常亮)或2
(关闭)。 -
常亮模式(
led_command == 1
):-
一旦通过
led_on
命令设置,LED 保持常亮,主循环不再修改其状态。
-
-
关闭模式(
led_command == 2
):-
一旦通过
led_off
命令设置,LED 保持熄灭,主循环不再修改其状态。
-
4.整体逻辑总结
-
初始化:
led_init
配置引脚为输出,初始状态为熄灭。 -
控制接口:
led_set
函数封装了 LED 的开启 / 关闭操作,简化主程序调用。 -
命令交互:通过串口接收
led_on
/led_off
命令,更新led_command
标志。 -
状态执行:主循环根据
led_command
的值,分别执行 “交替闪烁”“常亮”“关闭” 三种行为。
说明,这里的led相关函数直接放到了main.c文件里,其实更规范应该建一个hardware库,把要操作的自定义的硬件封装到这个文件夹,比如你还要用到mpu6050,然后把这两源码都放到这个文件夹。这里因为只操作led,而且博主图方便是新手嘛就这么整了。
可以看到,在led初始化函数里操作的是pc0和pc1,因为我板子上Led是这两引脚。这里提是想强调,注意代码和硬件的对应,后面的串口,时钟配置都体现了这点(其实博主这个大学生对stm32这学期刚学点,下面只是简单理解尽量讲明白,刚学的🤫)。
3.3.2 system文件夹相关
sys_stm32_clock_init(6);
delay_init(72); // 延时初始化
usart_init(72, 115200); // 初始化串口:时钟72MHz,波特率115200
可以看到main.c初始化部分有上面三个,都是用到了原子的system文件夹里的delay.c,sys.c,usart.c。然后我的板子不是原子的,所以要改一下涉及到的硬件。
上面是正点原子写的初始化delay,sys源码部分,可以看到延时初始化部分SYSTICK用到内部时钟源,所以延时可以正常用,不用改,而他们的时钟设置函数选择PLLSRC来自外部时钟源,都是f103内部时钟频率一样,但是外部时钟他们的开发板接的是8M的晶振,咱们的可能不是,反正我的不是💔。
1.STM32f103时钟系统
STM32 时钟系统的知识在《STM32中文参考手册V10》中的有非常详细的讲解,感兴趣可以自学,下面简要说明。
在STM32中,有五个时钟源,为HSI、HSE、LSI、LSE、PLL。从时钟频率来分可以分为 高速时钟源和低速时钟源,其中HIS,HSE以及PLL是高速时钟,LSI和LSE是低速时 钟。从来源可分为外部时钟源和内部时钟源,外部时钟源就是从外部通过接晶振的方式获取时钟源,其中HSE和LSE是外部时钟源,其他的是内部时钟源。5个时钟源: ①、HSI是高速内部时钟,RC振荡器,频率为8MHz。 ②、HSE 是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为 4MHz~16MHz。我们的开发板接的是8M的晶振。 ③、LSI是低速内部时钟,RC振荡器,频率为40kHz。独立看门狗的时钟源只能是LSI,同 时LSI还可以作为RTC的时钟源。 ④、LSE是低速外部时钟,接频率为32.768kHz的石英晶体。这个主要是RTC的时钟源。 ⑤、PLL 为锁相环倍频输出,其时钟输入源可选择为HSI/2、HSE或者HSE/2。倍频可选择为2~16 倍,但是其输出频率最大不得超过72MHz。
2.代码修改1
下面说一下博主板子外部时钟频率,截图是手册上说的,不太明白然后去示例代码找时钟源码配置,在DP开发板示例代码时钟初始化部分:有写,/* *****************初始化时钟晶振,设置成72MHZ系统时钟 时钟源由VPC3分频而来,为12M。 ***************************/,然后就确定了是12M。然后按照原子提供的封装函数:
sys_stm32_clock_init(uint32_t plln)
—— 系统时钟初始化入口函数
功能概述
作为时钟初始化的 “准备阶段”,负责在配置具体时钟前对系统时钟相关寄存器进行复位和初始化,确保时钟配置的 “干净环境”,最后调用sys_clock_set
完成实际时钟配置,并设置中断向量表地址。
按照最高主频72=12*6,所以代码修改第一个地方: sys_stm32_clock_init(6);
他们外部时钟8M,72=8*9,所以他们参数是9,大家根据自己外部时钟源频率设置。
3.代码修改2
usart_init(72, 115200); // 初始化串口:时钟72MHz,波特率115200
我的工程不用修改,换一个板子换一个串口就需要了。串口初始化源码部分:
相关头文件宏定义:可以看到参数介绍及配置,他们是默认使用串口1引脚PA9,PA10,大家用F103可能都是默认串口引脚PA9,PA10,反正我目前见得是,估计都不用修改,除非你想用其他串口,用多个串口,然后改一下配置就好。
最后,不是博主不想给现成的工程哈,我没整过下次再学吧,然后原子的工程模板,去他们资料下载中心找自己硬件下载资料,资料下载后程序源码文件夹里都有。