STM32单片机C语言模块化编程实战:按键控制LED灯并串口打印详解与示例

一、开发环境

硬件:正点原子探索者 V3 STM32F407 开发板

单片机:STM32F407ZGT6

Keil版本:5.32

STM32CubeMX版本:6.9.2

STM32Cube MCU Packges版本:STM32F4 V1.27.1

虽然这里演示的是STM32F407,但是STM32F103还是STM32H系列等,但是可直接将LED、按键、串口文件复制使用,仅供需改头文件的引脚。之前介绍了很多关于点灯的方法,比如轮询、定时器中断、PWM、按键点灯等方式,这些文章使用的编程方法都不是模块化的编写方式,往往会导致代码可读性差、重用性差、扩展性差以及测试和维护困难等问题。为了避免这些问题,我们实际工作中通常会采用模块化的编写方法,这样可以确保代码结构清晰、功能明确,提高代码的可读性和可维护性,同时降低功能之间的耦合度,增强代码的重用性和扩展性。模块化的编写方式还有助于实现代码的并行开发,提高开发效率,使得整个项目更加易于管理和维护。

基于之前的按键点灯的程序和printf重定向输出进行修改,我将为您详细阐述如何使用STM32F407的HAL库,并结合STM32CubeMX配置工具,通过模块化分层方法用按键分别控制两个LED灯并通过串口打印按键与灯的状态,即用引脚PE2和PE3按键分别控制PF9和PF10引脚LED,通过USART1打印信息。这一简洁而高效的流程将助您迅速掌握LED、按键、串口模块化编写方法。

1.LED灯
用drv_led.h和drv_led.c作为一个独立的模块,并提供三个LED驱动程序的接口

int LedDrvInit(BoardLed led);//初始化指定的LED

int LedDrvWrite(BoardLed led, LedStatus status);//设置指定LED的状态

int LedDrvRead(BoardLed led);//读取指定LED的当前状态

2.按键

用drv_key.h和drv_key.c作为一个独立的模块,并提供两个KEY驱动程序的接口

int KeyDrvInit(BoardKey key);//用于初始化指定的按键。  
int KeyDrvRead(BoardKey key);//用于读取指定按键的状态。  

3.串口USART1

int UartDrvInit(BoardUart uart);// 定义宏,将DbgUart映射到具体的USART(通用同步异步收发器)硬件接口,这里映射到USART1 
// 声明UartDrvWrite函数,该函数用于向指定的UART接口写入数据,参数pbuf指向要写入的数据,length表示数据长度
int UartDrvWrite(BoardUart uart, unsigned char *pbuf, unsigned short length);
// 声明UartDrvRead函数,该函数用于从指定的UART接口读取数据,参数pbuf用于存储读取到的数据,length表示读取的数据长度
int UartDrvRead(BoardUart uart, unsigned char *pbuf, unsigned short length);

 二、配置STM32CubeMX

  1. 启动STM32CubeMX,新建STM32CubeMX项目​​
  2. 选择MCU:在软件中选择你的STM32型号-STM32F407ZGT6。​​
  3. 选择时钟源:

  4. 配置时钟:
  5. 使能Debug功能:Serial Wire
  6. HAL库时基选择:SysTick
  7. 配置LED引脚:当前硬件的LED灯的引脚是PF9和PF10:在Pinout & Configuration标签页中,找到LED连接的GPIO端口,并设置为输出模式,通常选择Push-Pull,GPIO output level选低电平。
  8. 配置KEY引脚:当前硬件的KEY的引脚是PE2和PE3:在Pinout & Configuration标签页中,找到KEY连接的GPIO端口,并设置为输入模式,通常选择Pull-up。
  9. 配置USART1串口:
  10. 配置工程参数:在Project标签页中,配置项目名称和位置,选择工具链MDK-ARM。​​
  11. 生成代码:在Code Generator标签页中,配置工程外设文件与HAL库,勾选头文件.c和.h文件分开,然后点击Project > Generate Code生成代码。 

三、代码实现与部署

  1.  新建文件:LED灯的驱动drv_led.h和drv_led.c :​ drv_led.h

    #ifndef __DRV_LED_H
    #define __DRV_LED_Htypedef enum{LED1 = 1,LED2
    }BoardLed;typedef enum{led_on = 0,led_off = 1
    }LedStatus;#define LED1_PIN      GPIO_PIN_9
    #define LED1_PORT     GPIOF
    #define LED2_PIN      GPIO_PIN_10
    #define LED2_PORT     GPIOFint LedDrvInit(BoardLed led);
    int LedDrvWrite(BoardLed led, LedStatus status);
    int LedDrvRead(BoardLed led);#endif /* __DRV_LED_H */
    

    drv_led.c

    #include "drv_led.h"
    #include "stm32f4xx_hal.h"int LedDrvInit(BoardLed led)
    {switch(led){case LED1:{break;}case LED2:{break;}default:break;}return 0;
    }int LedDrvWrite(BoardLed led, LedStatus status)
    {switch(led){case LED1:{HAL_GPIO_WritePin(LED1_PORT, LED1_PIN, (GPIO_PinState)status);break;}case LED2:{HAL_GPIO_WritePin(LED2_PORT, LED2_PIN, (GPIO_PinState)status);break;}default:break;}return 0;
    }int LedDrvRead(BoardLed led)
    {LedStatus status = led_on;switch(led){case LED1:{status = (LedStatus)HAL_GPIO_ReadPin(LED1_PORT, LED1_PIN);break;}case LED2:{status = (LedStatus)HAL_GPIO_ReadPin(LED2_PORT, LED2_PIN);break;}default:break;}return status;
    }
    
  2. 添加路径:将drv_led.c添加到所属组, drv_led.h添加到头文件的路径中。
  3. 添加按键代码:drv_key.h和drv_key.c,方法与LED的一样。drv_key.h
    // #ifndef __DRV_KEY_H 是预处理指令,用于防止头文件的内容在一个编译单元中被多次包含。  
    // 如果__DRV_KEY_H还没有被定义,则继续处理此头文件的内容;如果已经定义了,则忽略。  
    #ifndef __DRV_KEY_H
    #define __DRV_KEY_H// 定义一个名为BoardKey的枚举类型,用于表示不同的按键。
    typedef enum{K1 = 1,// K1键,其值为1  K2,    // K2键,其值为2(因为K1为1,所以K2自动为2)  K3,K4
    }BoardKey;// 定义一个名为KeyStatus的枚举类型,用于表示按键的状态。  
    typedef enum{  isPressed = 0,  // 按键被按下,其值为0  isReleased = 1  // 按键被释放,其值为1  
    }KeyStatus; // 定义了一系列的宏,用于表示按键对应的GPIO引脚和端口。  
    // 例如,K1_PIN代表K1键连接的GPIO引脚,而K1_PORT代表该引脚所在的GPIO端口。  
    #define K1_PIN          GPIO_PIN_0
    #define K1_PORT         GPIOA
    #define K2_PIN          GPIO_PIN_2
    #define K2_PORT         GPIOE
    #define K3_PIN          GPIO_PIN_3
    #define K3_PORT         GPIOE
    #define K4_PIN          GPIO_PIN_4
    #define K4_PORT         GPIOEint KeyDrvInit(BoardKey key);//用于初始化指定的按键。  
    int KeyDrvRead(BoardKey key);//用于读取指定按键的状态。  #endif /* __DRV_KEY_H */
    
    drv_key.c
    #include "drv_key.h"
    #include "stm32f4xx_hal.h"int KeyDrvInit(BoardKey key)
    {switch(key){case K1:{break;}case K2:{break;}case K3:{break;}case K4:{break;}default:break;}return 0;
    }int KeyDrvRead(BoardKey key)
    {KeyStatus status = isReleased;switch(key){case K1:{status = (KeyStatus)HAL_GPIO_ReadPin(K1_PORT, K1_PIN);break;}case K2:{status = (KeyStatus)HAL_GPIO_ReadPin(K2_PORT, K2_PIN);break;}case K3:{status = (KeyStatus)HAL_GPIO_ReadPin(K3_PORT, K3_PIN);break;}case K4:{status = (KeyStatus)HAL_GPIO_ReadPin(K4_PORT, K4_PIN);break;}default:break;}return status;
    }
    
  4. 添加串口代码:drv_uart.h和drv_uart.c,方法与LED的一样。                                   drv_uart.h
    // 防止头文件被重复包含,这是一种常见的预处理指令用法,用来确保头文件在一个编译单元中只被包含一次  
    #ifndef __DRV_UART_H
    #define __DRV_UART_H// 定义一个枚举类型BoardUart,用来区分不同功能的UART(通用异步收发器)  
    typedef enum{DbgUart = 1,// 定义一个枚举类型BoardUart,用来区分不同功能的UART(通用异步收发器)  WiFiBTUart // WiFi蓝牙UART,后面会介绍WiFi蓝牙  
    }BoardUart;// 定义宏,将DbgUart映射到具体的USART(通用同步异步收发器)硬件接口,这里映射到USART1  
    #define DBGUART     USART1
    #define WiFiUART    USART3int UartDrvInit(BoardUart uart);// 定义宏,将DbgUart映射到具体的USART(通用同步异步收发器)硬件接口,这里映射到USART1 
    // 声明UartDrvWrite函数,该函数用于向指定的UART接口写入数据,参数pbuf指向要写入的数据,length表示数据长度
    int UartDrvWrite(BoardUart uart, unsigned char *pbuf, unsigned short length);
    // 声明UartDrvRead函数,该函数用于从指定的UART接口读取数据,参数pbuf用于存储读取到的数据,length表示读取的数据长度
    int UartDrvRead(BoardUart uart, unsigned char *pbuf, unsigned short length);// 结束头文件防止重复包含的检查  
    #endif /* __DRV_UART_H */
    
    drv_uart.c
    #include "drv_uart.h"
    #include "usart.h"
    #include "stm32f4xx_hal.h"int UartDrvInit(BoardUart uart)
    {switch(uart){case DbgUart:{break;}case WiFiBTUart:{break;}default:break;}return 0;
    }int UartDrvWrite(BoardUart uart, unsigned char *pbuf, unsigned short length)
    {int ret = -1;switch(uart){case DbgUart:{HAL_StatusTypeDef status = HAL_UART_Transmit(&huart1, pbuf, length, length*5);if(HAL_OK == status)    ret = 0;break;}case WiFiBTUart:{break;}default:break;}return ret;
    }int UartDrvRead(BoardUart uart, unsigned char *pbuf, unsigned short length)
    {int ret = -1;switch(uart){case DbgUart:{HAL_StatusTypeDef status = HAL_UART_Receive(&huart1, pbuf, length, length*5);if(HAL_OK == status)    ret = 0;break;}case WiFiBTUart:{break;}default:break;}return ret;
    }
  5. 添加打印重定向代码:printf.h和printf.c,方法与LED的一样。                                               printf.h 
    #ifndef __PRINTF_H
    #define __PRINTF_H#ifndef USE_PRINTF
    #define USE_PRINTF  (1)
    #endif /* USE_PRINTF */#if USE_PRINTF#include <stdio.h>#define xprintf(...)    printf(__VA_ARGS__)
    #else #define xprintf(...)
    #endif /* USE_PRINTF */#endif /* __PRINTF_H */
    
     printf.c
    #include "drv_uart.h"
    #include <stdio.h>struct __FILE{int handle;
    };FILE __stdout;int fputc(int ch, FILE *f)
    {(void)f;int ret = UartDrvWrite(DbgUart, (unsigned char*)&ch, 1);if(0 == ret)return ch;return 0;
    }
    
  6. 在main.c添加代码:添加头文件
    #include "drv_led.h"
    #include "drv_key.h"
    #include "drv_uart.h"
    #include "printf.h"
    #include <string.h>
    
       /* USER CODE BEGIN 2 */LedStatus d1_s = led_off; //灯状态LedStatus d2_s = led_off;LedDrvInit(LED1);LedDrvInit(LED2);KeyDrvInit(K2);KeyDrvInit(K3);UartDrvInit(DbgUart);/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */if(KeyDrvRead(K2) == isPressed)/* 检测按键的状态 */  {HAL_Delay(100);/* 消抖处理 */ if(KeyDrvRead(K2) == isPressed){d1_s =!d1_s; /* 切换LED1状态 */  LedDrvWrite(LED1, d1_s); /* 更新LED1的显示状态 */  UartDrvWrite(DbgUart,(unsigned char *)"KEY1 is Pressed,LED1 is On\r\n", strlen("KEY1 is Pressed,LED1 is On\r\n"));	}}if(KeyDrvRead(K3) == isPressed)		/* 检测按键的状态 */  {HAL_Delay(100);/* 消抖处理 */ if(KeyDrvRead(K3) == isPressed){d2_s =!d2_s;/* 切换LED1状态 */ LedDrvWrite(LED2, d2_s);/* 检测按键的状态 */  UartDrvWrite(DbgUart,(unsigned char *)"KEY2 is Pressed,LED2 is On\r\n", strlen("KEY2 is Pressed,LED2 is On\r\n"));}}	}/* USER CODE END 3 */
  7. 编译代码:Keil编译生成的代码。
  8. 烧录程序:将编译好的程序用ST-LINK烧录到STM32微控制器中。

四、运行结果

观察结果:一旦程序烧录完成并运行,你应该能看到按不同的按键会点亮不同的LED灯,串口打印按键和灯的状态。如果一切正常,恭喜你,你现在已经是一个掌握模块化的编写“点灯大师”了!​​

五、总结

模块化的编写方式对之前的代码封装了一层,提供了与LED、按键、串口硬件交互的接口,使得软件开发者可以在不直接操作硬件的情况下控制LED灯、按键、串口,可以直接用到STM32F103、STM32H系列等中,如果引脚不一样,只需修改引脚即可。通过上面的代码,希望你更多的采用模块化的编写方式,确保代码结构清晰、功能明确,提高可读性和可维护性,降低功能耦合,增强重用和扩展性,也促进并行开发(比如A员工做LED灯、B员工做按键、C员工做串口),提升效率,便于项目管理和维护。

六、注意事项

1.确保你的开发环境和工具链已经正确安装和配置。

2.在STM32CubeMX中配置GPIO时,注意选择正确的引脚和模式。

3.在编写代码时,确保使用正确的GPIO端口和引脚宏定义。

4.LED没有按预期点亮,按一下复位键,检查代码、连接和电源是否正确。

6.串口没有打印,检查代码、连接、电源、波特率是否正确,串口是否打开。

七、预告

下一节将LED、按键、串口封装成一个GPIO类,直接3归1,敬请关注!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/2289.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

书生·浦语大模型第二期实战营第三节-茴香豆:搭建你的 RAG 智能助理 笔记和作业

来源&#xff1a; 视频教程&#xff1a;茴香豆&#xff1a;搭建你的 RAG 智能助理 文字教程&#xff1a;茴香豆&#xff1a;搭建你的 RAG 智能助理 作业来源&#xff1a;第三课作业 茴香豆web链接&#xff1a;茴香豆web-零编程接入飞书微信&#xff08;更新了高精度 LLM&am…

使用R语言生成频数分布表

概要 使用R语言生成频数分布表 在R语言中&#xff0c;可以使用freq()函数来生成频数分布表。首先&#xff0c;将需要分组的数据存储在一个向量中。然后&#xff0c;使用freq()函数将这个向量作为参数输入&#xff0c;即可生成频数分布表。以下是一个示例&#xff1a; 示例 …

内插和抽取

抽取&#xff1a; 频域表达式的关系&#xff1a; 1、角频率扩大M倍 2、移动2pi、22pi…&#xff08;n-1&#xff09; 2pi 3、相加 4、幅度变为1/M 内插&#xff1a; 加入低通滤波&#xff0c;减小混叠&#xff0c;但是由于截短&#xff0c;也会造成误差&#xff0c;但是…

【YOLOv8改进[检测头Head]】YOLOv8的“新头”之动态头(DynamicHead)

目录 一 DynamicHead 二 YOLOv8的“新头”之动态头 1 总体修改 2 配置文件 3 训练 其他 一 DynamicHead 官方论文地址&#xff1a;https://arxiv.org/pdf/2106.08322.pdf 官方代码地址&#xff1a;GitCode - 开发者的代码家园 在计算机视觉应用中&#xff0c;目标检测…

第四百七十五回

文章目录 1. 概念介绍2. 功能与用法2.1 主要功能2.2 使用方法 3. 示例代码4. 内容总结 我们在上一章回中介绍了"四个bublue包对比与总结"相关的内容&#xff0c;本章回中将介绍Get包.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我们在本章回中将介…

力扣HOT100 - 2. 两数相加

解题思路&#xff1a; 缺位的节点进行补零处理&#xff0c;如97323补充为973023 注意相加的进位问题 class Solution {public ListNode addTwoNumbers(ListNode l1, ListNode l2) {ListNode head null, tail null;int carry 0;while (l1 ! null || l2 ! null) {int n1 l…

2013-2021年各省经济韧性相关测度指标面板数据

2013-2021年各省经济韧性相关测度指标面板数据 1、时间&#xff1a;2013-2021年 2、指标&#xff1a;城镇化率 %、财政科学技术支出&#xff08;亿元&#xff09;、万人高等教育在校人数&#xff08;万人&#xff09;、财政教育支出&#xff08;亿元&#xff09;、第三产业占…

semaphore信号量使用+原理分析

1.概述 Semaphore 信号量&#xff0c;相当于一个计数器&#xff0c;通常用来限制线程的数量。 每个线程操作前会先获取一个许可证&#xff0c;逻辑处理完成之后就归还这个许可证。 通俗的解释&#xff1a;相当于一个停车场&#xff0c;有10个停车位&#xff0c;进来一个车&am…

本地部署Docker容器可视化图形管理工具DockerUI并实现无公网IP远程访问——“cpolar内网穿透”

文章目录 前言1. 安装部署DockerUI2. 安装cpolar内网穿透3. 配置DockerUI公网访问地址4. 公网远程访问DockerUI5. 固定DockerUI公网地址 前言 DockerUI是一个docker容器镜像的可视化图形化管理工具。DockerUI可以用来轻松构建、管理和维护docker环境。它是完全开源且免费的。基…

stack、queue(priority_queue)的模拟实现和deque的简单介绍

stack和queue(priority_queue) 1. 容器适配器 适配器(Adapter)&#xff1a;一种用来修饰容器(Containers)或仿函数(Functors)或迭代器(Iterator)接口的东西。 适配器是一种设计模式&#xff0c;该模式将一个类的接口转换成客户希望的另外一个接口。 现实中拿插座来说&#xf…

C语言进阶课程学习记录- 函数与宏分析

C语言进阶课程学习记录- 函数与宏分析 实验-宏和函数实验-宏的副作用实验-宏的妙用小结 本文学习自狄泰软件学院 唐佐林老师的 C语言进阶课程&#xff0c;图片全部来源于课程PPT&#xff0c;仅用于个人学习记录 实验-宏和函数 #include <stdio.h>#define RESET(p, len) …

【Nginx】centos和Ubuntu操作系统下载Nginx配置文件并启动Nginx服务详解

目录 &#x1f337; 安装Nginx环境 &#x1f340; centos操作系统 &#x1f340; ubuntu操作系统 &#x1f337; 安装Nginx环境 以下是在linux系统中安装Nginx的步骤&#xff1a; 查看服务器属于哪个操作系统 cat /etc/os-release安装 yum&#xff1a; 如果你确定你的系统…

Spring是如何解决循环依赖的

简介 所谓循环依赖指的是&#xff1a;BeanA对象的创建依赖于BeanB&#xff0c;BeanB对象的创建也依赖于BeanA&#xff0c;这就造成了死循环&#xff0c;如果不做处理的话势必会造成栈溢出。Spring通过提前曝光机制&#xff0c;利用三级缓存解决循环依赖问题。 变量描述single…

【C语言__指针01__复习篇11】

目录 前言 一、什么是指针 二、计算机中常见的单位 三、CPU是怎样找到一块内存空间的 四、如何得到变量的地址 五、指针变量 六、解引用指针变量的作用 七、指针变量的大小 八、指针变量类型的意义 8.1 指针的解引用 8.2 指针-整数 九、void*指针 十、const修饰变…

PCB走线宽度、PCB走线宽度计算、PCB走线宽度和电流

目录 一、什么是PCB走线宽度&#xff1f; 二、什么是走线&#xff1f; 三、哪些因素对走线宽度至关重要&#xff1f; 1、信号走线 2、电源走线 3、直线宽度和信号反射 四、怎么计算PCB走线宽度&#xff1f; 1、使用PCB走线宽度计算器 2、使用方程式 五、怎么计算PCB 走…

25计算机考研院校数据分析 | 上海交通大学

上海交通大学电子信息与电气工程学院成立于2001年12月&#xff0c;其前身可湖源至百年前的电机专科&#xff0c;具有中国电气工程师“摇篮”之美称。50年代根据学科发展需要分为电工与计算机科学系(三系)和电子工程系(四系)。1985年&#xff0c;三系和四系合并&#xff0c;成立…

Abaqus2024 安装教程(附免费安装包资源)

鼠标右击软件压缩包&#xff0c;选择“解压到Abaqus2024”。 鼠标右击“此电脑”&#xff0c;选择“属性”。 点击“高级系统设置”。 点击“环境变量”。 点击“新建”。 变量名输入&#xff1a;NOLICENSECHECK 变量值输入&#xff1a;true 然后点击“确定”。 点击“确定”。…

【大模型系列】预训练

数据 数据预处理 预处理流程&#xff1a; 原始语料库—>质量过滤&#xff08;语种过滤、统计过滤、关键词过滤、分类器过滤&#xff09;—>敏感内容过滤&#xff08;有毒内容、隐私内容PII&#xff09;—>数据去重&#xff08;句子级别、文档级别、数据集级别&#…

【vue】el-tree的新增/编辑/删除节点

1、概述 关于树形结构的新增同级节点&#xff0c;新增子级节点&#xff0c;修改节点名称&#xff0c;删除节点等四种操作&#xff0c;各种参数配置完全继承el-tree&#xff0c;本篇使用vue2 element-ui 2、效果图展示 3、调用方式 <template><Tree:data"tree…

椋鸟数据结构笔记#11:排序·下

文章目录 外排序&#xff08;外部排序&#xff09;文件拆分并排序归并文件两个文件归并多文件归并优化 萌新的学习笔记&#xff0c;写错了恳请斧正。 外排序&#xff08;外部排序&#xff09; 当数据量非常庞大以至于无法全部写入内存时&#xff0c;我们应该怎么排序这些数据呢…