基于 ESP32-S3的物联网应用程序

本项目是一个基于 ESP32-S3的物联网应用程序,主要功能是实现摄像头视频流传输、串口数据交互和 MQTT 协议通信,构建了一个完整的 "感知 - 传输 - 控制" 物联网设备系统。实现了:

  • 数据上行:串口设备数据通过 MQTT 上报。
  • 视频传输:摄像头数据通过 TCP 实时发送到服务器。
  • 远程控制:通过 MQTT 接收命令,控制视频流开关和外部设备。

一 .工具介绍:

1. 硬件概述

ESP32-S3物联网网关,具有以下功能:

  1. 将串口接收的数据转发到MQTT服务器

  2. 将MQTT订阅的命令转发到串口

  3. 根据MQTT命令控制视频流传输

  4. 实现稳定的网络连接和错误处理

stm32F103

  1. 作为串口设备连接ESP32_S3
  2. 模拟物联网设备通过串口发送数据给esp32-S3
  3. 接收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),以下是根据官网展示的特点对它的简要介绍:

  1. 易于上手:Thonny 内置了 Python 3.10,用户只需安装一个简单的安装程序,就可以开始学习编程。即使已经安装了独立的 Python 环境,也可以在 Thonny 中使用。并且,Thonny 初始用户界面去除了所有可能会分散初学者注意力的功能,让新手能够专注于编程学习。
  2. 变量查看便捷:在完成基础的编程练习(如输出 "Hello world!" )之后,用户可以通过选择 “View”(视图) -> “Variables”(变量),来查看程序和 Shell 命令对 Python 变量的影响,帮助初学者理解变量在程序执行过程中的变化。
  3. 简单的调试器: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 主要用途

  1. 验证 ESP32-S3 的 UART1(tx=43, rx=44)引脚是否正常工作。
  2. 测试与外接串口设备(如传感器、单片机、PC 等)的通信是否畅通。
  3. 通过 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 主程序 

主程序是整个系统的调度中心,实现:

  1. 初始化硬件(WiFi、摄像头、MQTT)。
  2. 主循环轮询处理多任务:(1)定期检查 MQTT 消息(每 0.1 秒一次)(2)实时处理串口数据。(3)根据streaming_enabled状态控制视频流传输(约 10FPS)。
  3. 异常处理与资源清理。

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() 函数)

初始化连接到PC0PC1引脚的两个 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.整体逻辑总结

  1. 初始化:led_init配置引脚为输出,初始状态为熄灭。

  2. 控制接口:led_set函数封装了 LED 的开启 / 关闭操作,简化主程序调用。

  3. 命令交互:通过串口接收led_on/led_off命令,更新led_command标志。

  4. 状态执行:主循环根据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,反正我目前见得是,估计都不用修改,除非你想用其他串口,用多个串口,然后改一下配置就好。

最后,不是博主不想给现成的工程哈,我没整过下次再学吧,然后原子的工程模板,去他们资料下载中心找自己硬件下载资料,资料下载后程序源码文件夹里都有。

正点原子资料下载中心 — 正点原子资料下载中心 1.0.0 文档

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值