STM32物联网入门(1-30)
CubeIDE 安装与汉化
这是接下来用到的开发板
- 首先到 ST 官网下载 CubelDE ,下载地址:STM32CubeIDE,版本的话可以看自己需求,我的话选择最新版(目前是1.9.0)的前一个版本 1.8.0,点击下载后会弹出来需要填写个人信息如实填写即可(注意:邮箱一定要如实填写,推荐QQ邮箱),填写完成提交后会弹出“注册成功”的字样,并且官方把下载链接发送到你邮箱,这时候登陆QQ邮箱点击即可跳转回官方,然后等待片刻安装包就会弹出来,安装即可(注意:安装在没有中文的路径,不然会安装不了!!)
- 安装完解压即可然后双击软件开始安装(安装过程一般不需要改一直点下一步即可)
- 安装完成在桌面双击打开即可
- 这样就安装好了下一步开始安装汉化补丁
- 汉化包下载地址:汉化包,点击网址进去按下面步骤即可
- 设置编码和字体
CubeMX 安装(蓝桥杯需要)
- 首先到官网下载:[STM32CubeMX](https://www.st.com/zh/development-tools/stm32cubemx.html# get-software)
新建 CubeIDE工程
- 然后工程就创建完成了,下面是功能分布
设置时钟和IO端口
- 开启RCC功能
- 开启RTC功能
开启RTC功能中的时钟输入源
- 配置时钟树
设置端口
- 现在以开发板上的 LED1,LED2为例设置引脚,首先打开原理图,通过原理图可以知道 LED1 连接在 PB0 ,LED2 连接在 PB1 ,并且它们另一端都是接地,当 PB0 或者 PB1 为输出模式并且高电平时 LED1 或者 LED2 点亮
- 根据核心板原理图配置剩余的IO端口
注:晶振引脚已经在 RTC功能设置里设置了(见上面)
LED 端口设置
KEY 端口设置
USART1 端口设置
- 核心板设置完,则开始设置单片机上的其他端口,下面是单片机的原理图,根据网络标号进行设置对应的端口即可
由于唤醒键与核心板上的 KEY1 按键复用的 PA0端口,当多个功能复用一个端口时,要按单片机启动后最先用到的功能来设置,如果中途需要改为其他功能,可以在程序里以新功能再初始化设置一次;PA0 端口最先使用的是 KEY1 按键功能,KEY1 已经在核心板部分设置过了,所以唤醒键不需要设置
JTAG 端口并不普通,它是 ARM 核心的标配功能,所有 ARM 内核的单片机都有 JTAG 接口,由于它的特殊性,它的设置需要在功能分组中展开系统内核,JTAG 没有参数设置只需开启即可
ADC1,ADC2 端口设置
继电器 端口设置
- DHT11(温湿度传感器)
因为温湿度传感器是单总线,需要用GPIO模拟通信协议时同一个端口有两种状态输入又输出,所以先设置为输出模式,等程序中需要输入时,再用程序代码切换到输入模式
RS485 端口设置
RS485 的通信直接使用单片机内部的 USART2 串口功能,但 RS485总线还需要一个收发选择端口 RE,还要占用一个 GPIO 作为 RE 使用,当 RE 为高电平时 RS485 处于发送状态,为低电平时为接收状态
CAN总线 端口设置
USB 端口设置
USB 接口是单片机内部的 USB 从设备功能,可以让单片机通过 USB 连接到电脑,实现 USB键盘,USB串口,U盘等功能
闪存芯片 端口设置
闪存芯片是指开发板上的 W25Q128 存储芯片,它通过 SPI总线与单片机通信,另外,连接到 SPI总线上的每个设备都必须独立再连接一个使能控制端口CS,CS连接在 PB12 端口,PB12输出低电平,闪存芯片将被激活
蓝牙模块 端口设置
这个到时候用到再设置
WiFi模块 端口设置
单片机与 WiFI模块使用 USART3 串口通信
- 至此,已经完成开发板上的 IO端口设置
设置完成后,空闲引脚有PA7、PB6、PB7PB6和PB7可做 I2C 接口,PA7可做ADC接口虽然PC13空闲但其性能有限,需谨慎使用
仿真接口设置为JTAG模式时占用5个引脚设置为SWD模式时占用2个接口
CAN总线与USB从机接口不能同时使用;使用USB功能时要关闭CAN功能和接口定义
注意:STM32F103 单片机有个特殊设计:CAN功能与USB功能共用一组RAM空间两个功能不能同时使用
工程的编译与下载
- 生成代码
这样就生成代码完毕了
- 编译设置
- 工程编译
如果出现有错误则检查一下是不是文件路径有中文,记住之前安装时用到的路径都不能有中文!!!(或者再编译一次我第一次编译也有错误再编译一次就没了);如果没有错误则会在左边资源管理器中的 Debug 文件会生成 hex 文件
- Flymcu烧录
这个我在 stm32入门100步文章有写这里就不介绍如何下载了
把之前生成的 hex文件烧录到单片机上可以看到 LED1 和 LED2 都亮了!
- CubelDE仿真器烧录
如果找不到这个界面可以重启软件试试
如果“升级”按钮一直是灰色的则仿真器重新上电(注意不是拔接在笔记本的USB线是拔连接在仿真器靠绿色灯那端的线再插回去)
- ST-LINK Utility烧录
但是使用 CubeIDE 仿真有一个局限,必须拥有一套完整的工程文件,在打开的工程里编译后才能下载,如果我们只拥有 hex文件,就不能了,所以这要需要一个软件就是:ST-LINK Utility 软件,它可以将编译输出的hex文件直接下载到单片机里,不需要CubeIDE 工程文件,也不需要开发板上必须有USB转串口的电路,只要将仿真器简单连接在单片机对应的引脚上就能完成下载,这种方式适合在产品生产阶段给单片机批量下载程序
首先下载安装 ST-LINK Utility 软件(可以去ST官网下载:STSW-LINK004 - STM32 ST-LINK utility),安装过程一路 “NEXT” 即可,如果下载后打不开弹窗有“java”字样的英文可能就是没有安装 java 环境导致安装不了,可以去看我另一篇安装 java 环境的文章
- 安装完后打开 ST-LINK Utility
- 最后可以在单片机上看到效果了
HAL库的结构与使用
HAL库的整体结构
- 文件夹结构
- 工程树结构
- 禁用HAL库的方法
- 改用LL库的方法
- 快捷键
按【Shift】+【Ctrl】+【+】/【-】可以放大或者缩小页面
【Ctrl】+【/】可以注释一行
【Alt】+【/】可以代码提示
RCC时钟与延时函数
- 出现红色代表有错误要及时改正否则运行会出错
内核与外设时钟
- (以下描述以下面图片标号代替)最终频率4是系统时钟,其它时钟都是基于系统时钟的,它表示着单片机最大频率,频率越大速度越快,系统时钟作为源头,分配给各内部总线和功能,通过选择器2可以选择 3 个输入源,分别是第一个是 HSI 内部高速时钟输入(没有经过分频器和倍频器),直接给系统时钟 8MHz 频率,由于 HSI 采用的是 RC 振荡器,导致频率精度不高,不能用于对时间精度要求很好的项目中;第二个是 HSE 直接输入(需要单片机外接4~16MHz晶振,虽然外接晶振增加成本,但是时间精度大大提高),适用于时间精度要求好的场合,选择此项后的系统时钟频率就是 HSE 晶振的频率,所以 HSE 是8HMz系统时钟就是8HMz;第三个是 PLLCLK 输入(是经过分配器,倍频器和选择器分配后的频率,加入了 PLL 锁相环电路,可以在一定范围内调整频率值),前两个选项虽然简单直接,但得到的频率是固定值没法超频或者降频 通过选择器3可以切换 HSI 和 HSE 输入源,选择器后端进入 PLL 锁相环电路,通过倍频器1升高频率,最终可分配给 SYSCLK 系统时钟和 USB 时钟,STM32F103单片机系统时钟最大值是72HMz
- SYSCLK时钟经过分频器2到达最终频率5,这是 HCLK 时钟,HCLK 时钟通过内部高速数据总线 AHB,把频率提供给 ARM 内核,存储控制器,中断控制器,DMA 等内核功能,可以设置分频器2来降低 HCLK 频率,但通常都会让 HCLK 和 SYSCLK 保持一致,当项目里对这些内核功能性能有要求时可以通过设置 HCLK 来实现,标注中的最终频率6所包含的就是 HCLK 时钟所控制的部分,其中给系统内核定时器的频率还能通过分频器3进一步设置
- FCLK 时钟是自由运行时钟,作用是专为 ARM 内核提供运行的时钟频率,刚刚讲的 HCLK 时钟也为内核提供时钟频率,它们区别是:HCLK 是从 AHB 总线的电路上提供时钟,而 FCLK 不受总线的限制,即使 AHB 总线停止工作,FCLK 时钟也能直接向 ARM 内核提供时钟,由于内核时钟与 AHB 总线时钟必须频率相同才能正常工作,所以在时钟树里这两个值始终相同
- PCLK 外设时钟,它作用是给单片机的外设功能提供时钟(这里说的外设并不是单片机外面的设备,而是相对于 ARM 内核以外的单片机内部功能),PCLK 时钟分配给 APB1总线和 APB2总线两个部分,每个总线上都挂着不同的单片机内部功能,最终频率7就是 APB1相关的时钟(包括 APB1外设时钟和APB1定时器时钟),可以通过分频器4和倍频器2来设置频率(注意:APB1外设时钟最大频率是36HMz,APB1定时器时钟最大频率是72HMz),最终频率9就是 APB2相关的时钟(包括 APB2外设时钟和APB2定时器时钟,它们最大频率都是72HMz),可以通过分频器5和倍频器3来设置频率
APB1 和APB2 挂载的:
- ADC功能时钟,ADC功能涉及到模数转换器,标注为最终频率10,它的时钟挂接在 APB2总线上,同时提供给 <font color='ADC1 和 ADC2 两个功能</>ADC1 和 ADC2 两个功能,通过分频器6可以设置频率,最大值为14MHz
独立时钟
- RTC时钟在时钟树视图左上角,可通过选择器1切换3个输入源,第一个输入源是 HSE 外部高速时钟,经过了分频器1的128分频,当 HSE 晶振是8HMz时,经过128分频得到约62.5KHz的频率进入RTC时钟;第二个输入源是 LSE 外部32.768KHz的低速时钟,未经过分频和倍频,直接输入到RTC时钟,得到32.768KHz频率;第三个输入源是 LSI 内部40KHz的低速时钟,也未经过分频和倍频,得到40KHz频率;三种输入源让 RTC 时钟得到不同的频率值 RTC功能配上备用电池可以为系统提供掉电依然走时的功能,这功能可用于实时时钟的走时,还可以把 RTC 时钟当成一个长时间定时器使用,走时一般要求精度很高,这时必须在 LSE上外接精度很高,温度漂移很小的32.768KHz晶振,选择器1就必须选择 LSE 输入源,但如果把 RTC当长时间定时器使用,则对精度要求低,误差几分钟都无所谓,则不用外接晶振,使用精度不高的 LSI 或者 HSI 也能达到要求,如果对系统时钟的频率精度要求也不高,连HSE外部8MHz也可以省去
- 独立看门狗时钟,它的作用是监测单片机是否出错,所以看门狗的运行必须完全独立于系统时钟之外,独立看门狗是直接引入40KHz的 LSI 内部低速时钟,保证了输入源的稳定可靠,最终看门狗的固定频率是40KHz,不允许修改
- FLASH 编程时钟,作用是使用串口或JTAG接口给单片机下载程序时,为FLASH编程操作提供时钟,在单片机下载过程中单片机处于 BOOTLOADER 模式,系统时钟没有启动,所以 HSI 内部高速时钟直接给 FLASH编程提供独立的时钟输入,FLASH编程时钟频率固定是8MHz,不允许修改
- USB接口时钟,USB功能是指单片机内置的USB从设备接口,此功能在单片机内部是独立工作的,USB时钟频率允许我们修改,通过选择器3可切换 HSI 和 HSE 两个输入源,可通过分频器7,分频器8,分频器9,倍频器1来调配频率值(注意:只有分频器7是USB时钟专属的,其他都与系统时钟设置共享,设置时需要考虑到系统时钟的联动变化),USB时钟的最大频率是48MHz
时钟输出
- 时钟树视图中的最终频率9是单片机预留的时钟频率输出功能(时钟输出缩写是MCO),在f103c8t6单片机上,MCO复用在PA8端口上,如果想开启MCO时钟输出功能需要将端口视图中的PA8端口改成MCO模式 ,通过选择器4可以切换4个时钟输入源,分别是 PLLCLK锁相环时钟,HSI内部高速时钟,HSE外部高速时钟,SYSCLK系统时钟,其中 PLLCLK是标注为最终频率11的锁相环频率输入,MCO时钟输出功能多用于给其他芯片提供时钟基准或者做单片机之间的同步运行
RCC的程序代码
- 启动文件中的代码
单片机在最开始运行时并不是从main函数开始而是先运行汇编启动文件,启动文件里对单片机的 RAM,FLASH,时钟,电源,中断等基础功能进行初始化设置,这些是保证单片机正常工作的必要条件,由于汇编文件非常重要,不允许修改,下面图中设置向量表作用是在单片机运行中发生异常时会自动跳转到对应的异常处理函数中执行
- CubeMX生成的代码
这是RCC函数,选中函数名按【F3】可以跳转到实现函数
- 程序中如何修改时钟设置
注意:在main.c里修改了配置,视图里是不会更新的!!!所以推荐在视图里改然后重新生成代码或者新建函数把生成的代码复制进去修改再调用新建的函数(这种方法比较麻烦)
HAL库中的延时函数
- 延时函数的位置
LED与按键驱动程序
- 创建LED驱动文件
创建 .c 和 .h 文件(只示范.c,.h操作类似)
- 用户代码要写在【开始处】和【结束处】之间!!!(写在其他位置当视图重新生成代码时会不见了的)
- 关于相对路径问题
1、一个点:表示当前目录 即类似使用: "./pretrained_emb/glove_300_nyt.emb",
2、两个点:表示当前目录的上级目录 类似: "../pretrained_emb/glove_300_nyt.emb",
3、如果是当前目录的上级的上级目录 "../../pretrained_emb/glove_300_nyt.emb",
一次类推,再往上级,直接加../
LED程序
本节用到的HAL函数
- HAL_GPIO_WritePin //使得对应的引脚输出高电平或者低电平
- HAL_Delay //毫秒延时
类型:
uint8_t //无符号1个字节的整型
uint16_t //无符号2个字节的整型
uint32_t //无符号4个字节的整型
uint64_t //无符号8个字节的整型
led.h
# ifndef LED_LED_H_
# define LED_LED_H_
# include "stm32f1xx_hal.h" //HAL库文件声明
# include "main.h" //IO定义与初始化函数在main.c文件中,必须引用
void LED_1(uint8_t a);//LED1独立控制函数(0为熄灭,其他值为点亮)
void LED_2(uint8_t a);//LED2独立控制函数(0为熄灭,其他值为点亮)
void LED_ALL(uint8_t a);//LED1~4整组操作函数(低4位的1/0状态对应4个LED亮灭,最低位对应LED1)
void LED_1_Contrary(void);//LED1状态取反
void LED_2_Contrary(void);//LED2状态取反
# endif /* LED_LED_H_ */
led.c
- GPIOB:端口组
- LED1_Pin:是PB0端口,正确写法是GPIO_Pin_0,但是由于在CubeMX中给PB0设置了标注LED1,所以这里是用【标注名称】+【_Pin】来代替端口0写法
- GPIO_PIN_SET:高电平(1)
- GPIO_PIN_RESET:低电平(0)
# include "led.h"
void LED_1(uint8_t a)//LED1独立控制函数(0为熄灭,其他值为点亮)
{
if(a)HAL_GPIO_WritePin(GPIOB,LED1_Pin,GPIO_PIN_SET);
else HAL_GPIO_WritePin(GPIOB,LED1_Pin,GPIO_PIN_RESET);
}
void LED_2(uint8_t a)//LED2独立控制函数(0为熄灭,其他值为点亮)
{
if(a)HAL_GPIO_WritePin(GPIOB,LED2_Pin,GPIO_PIN_SET);
else HAL_GPIO_WritePin(GPIOB,LED2_Pin,GPIO_PIN_RESET);
}
void LED_ALL(uint8_t a)//LED1~2整组操作函数(低2位的1/0状态对应2个LED亮灭,最低位对应LED1)
{
if(a&0x01)HAL_GPIO_WritePin(GPIOB,LED1_Pin,GPIO_PIN_SET);
else HAL_GPIO_WritePin(GPIOB,LED1_Pin,GPIO_PIN_RESET);
if(a&0x02)HAL_GPIO_WritePin(GPIOB,LED2_Pin,GPIO_PIN_SET);
else HAL_GPIO_WritePin(GPIOB,LED2_Pin,GPIO_PIN_RESET);
}
void LED_1_Contrary(void){
HAL_GPIO_WritePin(GPIOB,LED1_Pin,1-HAL_GPIO_ReadPin(GPIOB,LED1_Pin));
}
void LED_2_Contrary(void){
HAL_GPIO_WritePin(GPIOB,LED2_Pin,1-HAL_GPIO_ReadPin(GPIOB,LED2_Pin));
}
main.c
# include "main.h"
# include "adc.h"
# include "can.h"
# include "rtc.h"
# include "spi.h"
# include "usart.h"
# include "gpio.h"
# include "usb.h"
/* USER CODE BEGIN Includes */
# include "../../icode/led/led.h"
/* USER CODE END Includes */
int main()
{
while (1)
{
LED_1(1);//LED1灯控制(1点亮,0熄灭)
LED_2(0);//LED2灯控制(1点亮,0熄灭)
HAL_Delay(500);//延时500ms
LED_1(0);//LED1灯控制(1点亮,0熄灭)
LED_2(1);//LED2灯控制(1点亮,0熄灭)
HAL_Delay(500);//延时500ms
}
}
实验现象
按键程序
本节用到的HAL函数
- HAL_GPIO_ReadPin //读取引脚电平
用到了 7.1 的 led.h,led.c 文件;只需添加 key.h,key.c
key.h
# ifndef KEY_KEY_H_
# define KEY_KEY_H_
# include "stm32f1xx_hal.h" //HAL库文件声明
# include "main.h" //IO定义与初始化函数在main.c文件中,必须引用
uint8_t KEY_1(void);//按键1
uint8_t KEY_2(void);//按键2
# endif /* KEY_KEY_H_ */
key.c
# include "key.h"
uint8_t KEY_1(void)
{
uint8_t a;
a=0;//如果未进入按键处理,则返回0
if(HAL_GPIO_ReadPin(GPIOA,KEY1_Pin)==GPIO_PIN_RESET){//读按键接口的电平
HAL_Delay(20);//延时去抖动
if(HAL_GPIO_ReadPin(GPIOA,KEY1_Pin)==GPIO_PIN_RESET){ //读按键接口的电平
a=1;//进入按键处理,返回1
}
}
while(HAL_GPIO_ReadPin(GPIOA,KEY1_Pin)==GPIO_PIN_RESET); //等待按键松开
return a;
}
uint8_t KEY_2(void)
{
uint8_t a;
a=0;//如果未进入按键处理,则返回0
if(HAL_GPIO_ReadPin(GPIOA,KEY2_Pin)==GPIO_PIN_RESET){//读按键接口的电平
HAL_Delay(20);//延时去抖动
if(HAL_GPIO_ReadPin(GPIOA,KEY2_Pin)==GPIO_PIN_RESET){ //读按键接口的电平
a=1;//进入按键处理,返回1
}
}
while(HAL_GPIO_ReadPin(GPIOA,KEY2_Pin)==GPIO_PIN_RESET); //等待按键松开
return a;
}
main.c
# include "main.h"
# include "adc.h"
# include "can.h"
# include "rtc.h"
# include "spi.h"
# include "usart.h"
# include "gpio.h"
/* USER CODE BEGIN Includes */
# include "../../icode/led/led.h"
# include "../../icode/key/key.h"
/* USER CODE END Includes */
int main()
{
while (1)
{
if(KEY_1())
{
LED_1(1);
LED_2(1);
}
if(KEY_2())
{
LED_1(0);
LED_2(0);
}
}
}
蜂鸣器与继电器驱动程序
蜂鸣器程序(自创us延时)
本节用到的HAL函数
- HAL_RCC_GetHCLKFreq //读取HCLK时钟频率值
用到了 7.2 的 led.h,led.c,key.h,key.c 文件;添加 delay.h,delay.c ,buzzer.h,buzzer.c 文件
delay.h
# ifndef DELAY_DELAY_H_
# define DELAY_DELAY_H_
# include "stm32f1xx_hal.h" //HAL库文件声明
void delay_us(uint32_t us); //C文件中的函数声明
# endif /* DELAY_DELAY_H_ */
delay.c
# include "delay.h"
void delay_us(uint32_t us) //利用CPU循环实现的非精准应用的微秒延时函数
{
uint32_t delay = (HAL_RCC_GetHCLKFreq() / 8000000 * us); //使用HAL_RCC_GetHCLKFreq()函数获取主频值,经算法得到1微秒的循环次数
while (delay--); //循环delay次,达到1微秒延时
}
buzzer.h
# ifndef BUZZER_BUZZER_H_
# define BUZZER_BUZZER_H_
# include "stm32f1xx_hal.h" //HAL库文件声明
# include "main.h"
# include "../delay/delay.h"
void BUZZER_SOLO1(void);
void BUZZER_SOLO2(void);
# endif /* BUZZER_BUZZER_H_ */
buzzer.c
- BEEP1_GPIO_Port:是CubeMX生成的main.h 里面宏定义的,跟 GPIOB 一个意思
# include "buzzer.h"
# define time1 50 //单音的时长
# define hz1 1 //单音的音调(单位毫秒)
void BUZZER_SOLO1(void){//蜂鸣器输出单音的报警音(样式1:HAL库的精准延时函数)
uint16_t i;
for(i=0;i<time1;i++){//循环次数决定单音的时长
HAL_GPIO_WritePin(BEEP1_GPIO_Port,BEEP1_Pin,GPIO_PIN_RESET); //蜂鸣器接口输出低电平0
HAL_Delay(hz1); //延时(毫秒级延时最小1微秒,实现的单调较低,因不需要额外编写微秒级延时函数所以最简单实用)
HAL_GPIO_WritePin(BEEP1_GPIO_Port,BEEP1_Pin,GPIO_PIN_SET); //蜂鸣器接口输出高电平1
HAL_Delay(hz1); //延时
}
}
# define time2 200 //单音的时长
# define hz2 500 //单音的音调(单位微秒)
void BUZZER_SOLO2(void){//蜂鸣器输出单音的报警音(样式2:CPU微秒级延时)
uint16_t i;
for(i=0;i<time2;i++){//循环次数决定单音的时长
HAL_GPIO_WritePin(BEEP1_GPIO_Port,BEEP1_Pin,GPIO_PIN_RESET); //蜂鸣器接口输出低电平0
delay_us(hz2); //延时
HAL_GPIO_WritePin(BEEP1_GPIO_Port,BEEP1_Pin,GPIO_PIN_SET); //蜂鸣器接口输出高电平1
delay_us(hz2); //延时
}
}
main.c
# include "main.h"
# include "adc.h"
# include "can.h"
# include "rtc.h"
# include "spi.h"
# include "usart.h"
# include "gpio.h"
/* USER CODE BEGIN Includes */
# include "../../icode/led/led.h"
# include "../../icode/key/key.h"
# include "../../icode/delay/delay.h"
# include "../../icode/buzzer/buzzer.h"
/* USER CODE END Includes */
int main()
{
while (1)
{
if(KEY_1())
{
LED_1(1);
LED_2(1);
BUZZER_SOLO1();//蜂鸣器输出单音的报警声样式1
}
if(KEY_2())
{
LED_1(0);
LED_2(0);
BUZZER_SOLO2();//蜂鸣器输出单音的报警声样式2
}
}
}
继电器程序
文件用 8.1 ; 添加 relay.h,relay.c 文件
- 注意:用三极管也能驱动继电器(可以不用ULN2003),但是只能用在数量少的继电器,多个继电器还是用ULN2003方便
relay.h
# ifndef INC_RELAY_H_
# define INC_RELAY_H_
//继电器接口定义与初始化函数在MX中设置并生成在main.c文件中
# include "stm32f1xx_hal.h" //HAL库文件声明
# include "main.h" //IO定义与初始化函数在main.c文件中,必须引用
void RELAY_1(uint8_t c);//继电器控制1
# endif /* INC_RELAY_H_ */
relay.c
# include "relay.h"
void RELAY_1(uint8_t c){ //继电器的控制程序(c=0继电器放开,c=1继电器吸合)
if(c)HAL_GPIO_WritePin(GPIOA,RELAY1_Pin,GPIO_PIN_RESET); //继电器吸
else HAL_GPIO_WritePin(GPIOA,RELAY1_Pin,GPIO_PIN_SET); //继电器松
}
main.c
# include "main.h"
# include "adc.h"
# include "can.h"
# include "rtc.h"
# include "spi.h"
# include "usart.h"
# include "gpio.h"
/* USER CODE BEGIN Includes */
# include "../../icode/led/led.h"
# include "../../icode/key/key.h"
# include "../../icode/delay/delay.h"
# include "../../icode/buzzer/buzzer.h"
# include "../../icode/relay/relay.h"
/* USER CODE END Includes */
int main()
{
while(1)
{
if(KEY_1())
{
LED_1(1);
LED_2(1);
BUZZER_SOLO1();//蜂鸣器输出单音的报警声样式1
RELAY_1(1);//继电器吸合
}
if(KEY_2())
{
LED_1(0);
LED_2(0);
BUZZER_SOLO2();//蜂鸣器输出单音的报警声样式2
RELAY_1(0);//继电器放开
}
}
}
串口通信(USART)
本节用到的HAL函数
- HAL_UART_Transmit //串口发送指定长度的数据 如果超时没发送完成,则不再发送,返回超时标志(HAL_TIMEOUT)
- HAL_UART_RxCpltCallback //串口中断回调函数
- HAL_UART_Receive_IT //串口中断模式接收
文件添加 usart.h,usart.c,retarget.h,retarget.c (printf重定义文件) - 因为和我们即将添加的文件有冲突,所以首先需要按下面图片完成设置
- 将 retarget.h ,retarget.c 粘贴在下面路径
串口中断回调函数
- 因为串口中断回调函数包含了所有串口中断事件(USART1,USART2,USART3中的任何一个串口产生中断都会进入同一个中断回调函数来处理中断事件)所以 usart.c 需要处理3个串口的中断
- HAL库中断回调方式:当触发中断时,同样会进入中断处理函数,但是中断处理函数里只标明中断来源,然后快速退出中断,回到主函数,回到主函数并不是继续执行之前中止的程序,而是自动调用中断回调函数,对中断事件的处理都放在回调函数里,由于程序已经退出了中断状态回归主函数,所以其他中断不会受阻,这就是回调函数的作用和意义
- 在 stm32f1xx_hal_uart.c 文件有串口中断回调函数的弱函数定义
- 函数名称前面加上__weak 修饰符,我们一般称这个函数为“弱函数”
- 加上了__weak 声明的函数,用户可以在用户文件中重新定义一个同名函数,最终编译器编译时,会选择用户定义的函数,如果没重新定义这个函数,那么编译器就会执行弱函数,并且编译器不会报错 所以我们可以在别的地方定义一个相同名字的函数,而不必也尽量不要修改之前的函数
- 弱函数相当于后补球员,当场上有正式球员时不能上场,如果正式球员缺席则后补上场,保证比赛进行
retarget.h
# ifndef INC_RETARGET_H_
# define INC_RETARGET_H_
# include "stm32f1xx_hal.h"
# include "stdio.h"//用于printf函数串口重映射
# include <sys/stat.h>
void RetargetInit(UART_HandleTypeDef *huart);
int _isatty(int fd);
int _write(int fd, char* ptr, int len);
int _close(int fd);
int _lseek(int fd, int ptr, int dir);
int _read(int fd, char* ptr, int len);
int _fstat(int fd, struct stat* st);
# endif /* INC_RETARGET_H_ */
retarget.c
# include <_ansi.h>
# include <_syslist.h>
# include <errno.h>
# include <sys/time.h>
# include <sys/times.h>
# include <limits.h>
# include <signal.h>
# include <../Inc/retarget.h>
# include <stdint.h>
# include <stdio.h>
# if !defined(OS_USE_SEMIHOSTING)
# define STDIN_FILENO 0
# define STDOUT_FILENO 1
# define STDERR_FILENO 2
UART_HandleTypeDef *gHuart;
void RetargetInit(UART_HandleTypeDef *huart) {
gHuart = huart;
/* Disable I/O buffering for STDOUT stream, so that
* chars are sent out as soon as they are printed. */
setvbuf(stdout, NULL, _IONBF, 0);
}
int _isatty(int fd) {
if (fd >= STDIN_FILENO && fd <= STDERR_FILENO)
return 1;
errno = EBADF;
return 0;
}
int _write(int fd, char* ptr, int len) {
HAL_StatusTypeDef hstatus;
if (fd == STDOUT_FILENO || fd == STDERR_FILENO) {
hstatus = HAL_UART_Transmit(gHuart, (uint8_t *) ptr, len, HAL_MAX_DELAY);
if (hstatus == HAL_OK)
return len;
else
return EIO;
}
errno = EBADF;
return -1;
}
int _close(int fd) {
if (fd >= STDIN_FILENO && fd <= STDERR_FILENO)
return 0;
errno = EBADF;
return -1;
}
int _lseek(int fd, int ptr, int dir) {
(void) fd;
(void) ptr;
(void) dir;
errno = EBADF;
return -1;
}
int _read(int fd, char* ptr, int len) {
HAL_StatusTypeDef hstatus;
if (fd == STDIN_FILENO) {
hstatus = HAL_UART_Receive(gHuart, (uint8_t *) ptr, 1, HAL_MAX_DELAY);
if (hstatus == HAL_OK)
return 1;
else
return EIO;
}
errno = EBADF;
return -1;
}
int _fstat(int fd, struct stat* st) {
if (fd >= STDIN_FILENO && fd <= STDERR_FILENO) {
st->st_mode = S_IFCHR;
return 0;
}
errno = EBADF;
return 0;
}
# endif //# if !defined(OS_USE_SEMIHOSTING)
usart.h
# ifndef INC_USART_H_
# define INC_USART_H_
# include "stm32f1xx_hal.h" //HAL库文件声明
# include <string.h>//用于字符串处理的库
# include "../inc/retarget.h"//用于printf函数串口重映射
extern UART_HandleTypeDef huart1;//声明USART1的HAL库结构体
extern UART_HandleTypeDef huart2;//声明USART2的HAL库结构体
extern UART_HandleTypeDef huart3;//声明USART2的HAL库结构体
# define USART1_REC_LEN 200//定义USART1最大接收字节数
# define USART2_REC_LEN 200//定义USART1最大接收字节数
# define USART3_REC_LEN 200//定义USART1最大接收字节数
extern uint8_t USART1_RX_BUF[USART1_REC_LEN];//接收缓冲,最大USART_REC_LEN个字节.末字节为换行符
extern uint16_t USART1_RX_STA;//接收状态标记
extern uint8_t USART1_NewData;//当前串口中断接收的1个字节数据的缓存
extern uint8_t USART2_RX_BUF[USART2_REC_LEN];//接收缓冲,最大USART_REC_LEN个字节.末字节为换行符
extern uint16_t USART2_RX_STA;//接收状态标记
extern uint8_t USART2_NewData;//当前串口中断接收的1个字节数据的缓存
extern uint8_t RS485orBT;//当RS485orBT标志位为1时是RS485模式,为0时是蓝牙模式
extern uint8_t USART3_RX_BUF[USART3_REC_LEN];//接收缓冲,最大USART_REC_LEN个字节.末字节为换行符
extern uint16_t USART3_RX_STA;//接收状态标记
extern uint8_t USART3_NewData;//当前串口中断接收的1个字节数据的缓存
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);//串口中断回调函数声明
# endif /* INC_USART_H_ */
usart.c
- 当前只用到USART1,所以只需看USART1部分即可
- HAL_UART_Receive_IT 和 HAL_UART_Receive 的区别就是︰中断接收是有数据到了才去读;直接接收是直接读取.如果超时就返回
- HAL_UART_Receive_T 配置后,有数据来,计数会在调用中断函数之后自动减1 只有到计数为O时,才会关闭中断并调用回调函数 至此有数据来不再调用中断函数,因为中断已经失效
- HAL_UART_Receive_IT 在计数未至0之前,应该可以读取之前接收到的数据,但这样做应该比较危险
- 在下面程序中,使用HAL_UART_Receive_IT(&huart1, (uint8_t *)&USART1_NewData, 1); 即Size设置为1,只接收1Byte数据,在每次中断结束后重新配置来使能中断 也就是像一个圆圈
# include "usart.h"
uint8_t USART1_RX_BUF[USART1_REC_LEN];//接收缓冲,最大USART_REC_LEN个字节.
uint16_t USART1_RX_STA=0;//接收状态标记//bit15:接收完成标志,bit14:接收到0x0d,bit13~0:接收到的有效字节数目
uint8_t USART1_NewData;//当前串口中断最新接收的1个字节数据的缓存
uint8_t USART2_RX_BUF[USART2_REC_LEN];//接收缓冲,最大USART_REC_LEN个字节.
uint16_t USART2_RX_STA=0;//接收状态标记//bit15:接收完成标志,bit14:接收到0x0d,bit13~0:接收到的有效字节数目
uint8_t USART2_NewData;//当前串口中断接收的1个字节数据的缓存
uint8_t RS485orBT;//当RS485orBT标志位为1时是RS485模式,为0时是蓝牙模式
uint8_t USART3_RX_BUF[USART3_REC_LEN];//接收缓冲,最大USART_REC_LEN个字节.
uint16_t USART3_RX_STA=0;//接收状态标记//bit15:接收完成标志,bit14:接收到0x0d,bit13~0:接收到的有效字节数目
uint8_t USART3_NewData;//当前串口中断接收的1个字节数据的缓存
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)//串口中断回调函数
{
if(huart == &huart1) //判断中断来源(串口1:USB转串口)
{
printf("%c", USART1_NewData); //把收到的数据以 a符号变量 发送回电脑
if((USART1_RX_STA & 0x8000) == 0) //接收未完成
{
if(USART1_RX_STA & 0x4000) //接收到了0x0d
{
if(USART1_NewData != 0x0a)USART1_RX_STA = 0; //接收错误,重新开始
else USART1_RX_STA |= 0x8000; //接收完成了
}
else //还没收到0X0D
{
if(USART1_NewData == 0x0d)USART1_RX_STA |= 0x4000;
else
{
USART1_RX_BUF[USART1_RX_STA & 0X3FFF] = USART1_NewData; //将收到的数据放入数组
USART1_RX_STA++; //数据长度计数加1
if(USART1_RX_STA > (USART1_REC_LEN - 1))USART1_RX_STA = 0; //接收数据错误,重新开始接收
}
}
}
HAL_UART_Receive_IT(&huart1, (uint8_t *)&USART1_NewData, 1); //再开启接收中断
}
if(huart == &huart2) //判断中断来源(RS485/蓝牙模块)
{
if(RS485orBT) //当RS485orBT标志位为1时是RS485模式,为0时是蓝牙模式
{
USART2_RX_BUF[0] = USART2_NewData; //将接收到的数据放入缓存数组(因只用到1个数据,所以只存放在数据[0]位置)
USART2_RX_STA++;//数据接收标志位加1
}
else
{
printf("%c", USART2_NewData); //把收到的数据以 a符号变量 发送回电脑
}
HAL_UART_Receive_IT(&huart2, (uint8_t *)&USART2_NewData, 1); //再开启接收中断
}
if(huart == &huart3) //判断中断来源(串口3:WIFI模块)
{
printf("%c", USART3_NewData); //把收到的数据以 a符号变量 发送回电脑
HAL_UART_Receive_IT(&huart3, (uint8_t *)&USART3_NewData, 1); //再开启接收中断
}
}
main.c(基于usart.c的)
- 可以把 if 语句改成判断标志位是否为1,这样不用回车LED也能亮灭
# include "main.h"
# include "adc.h"
# include "can.h"
# include "rtc.h"
# include "spi.h"
# include "usart.h"
# include "gpio.h"
/* USER CODE BEGIN Includes */
# include "../../icode/led/led.h"
# include "../../icode/key/key.h"
# include "../../icode/delay/delay.h"
# include "../../icode/buzzer/buzzer.h"
# include "../../icode/relay/relay.h"
# include "../../icode/usart/usart.h"
/* USER CODE END Includes */
int main()
{
HAL_UART_Receive_IT(&huart1, (uint8_t *)&USART1_NewData, 1); //开启串口1接收中断
while(1)
{
if(USART1_RX_STA & 0xc000) //串口1判断中断接收标志位(判断是不是回车)
{
if(USART1_RX_BUF[0] == '1')
{
LED_1(1);
LED_2(1);
}
if(USART1_RX_BUF[0] == '0')
{
LED_1(0);
LED_2(0);
}
USART1_RX_STA = 0; //串口接收标志清0,即开启下一轮
}
}
}
main.c(使用 HAL库串口发送)
- HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
UART_HandleTypeDef *huart UATR的别名 (如 : UART_HandleTypeDef huart1; 别名就是huart1 )
*pData 需要发送的数据(这里可以用 unsigned char 代替 uint8_t)
Size 发送的字节数
Timeout 最大发送时间(发送数据超过该时间退出发送,时间计算如下图)
- 在main主函数里首先调用HAL_UART_Receive_IT ,因为单片机上电时串口接收中断默认是关闭状态
# include "main.h"
# include "adc.h"
# include "can.h"
# include "rtc.h"
# include "spi.h"
# include "usart.h"
# include "gpio.h"
/* USER CODE BEGIN Includes */
# include "../../icode/led/led.h"
# include "../../icode/key/key.h"
# include "../../icode/delay/delay.h"
# include "../../icode/buzzer/buzzer.h"
# include "../../icode/relay/relay.h"
# include "../inc/retarget.h"//用于printf函数串口重映射
/* USER CODE END Includes */
int main()
{
RetargetInit(&huart1);//将printf()函数映射到USART1串口上
while(1)
{
if(KEY_1())
{
HAL_UART_Transmit(&huart1, (uint8_t *)&"KEY1\r\n", 6, 0xffff); //串口发送,串口号1,内容“KEY1”,数量6,溢出时间0xffff,&可以省略不写
}
if(KEY_2())
{
printf("KEY2\r\n");//向USART1串口发送字符串
}
}
}
ADC与DMA驱动程序
本节用到的HAL函数
- HAL_ADC_Start //开启ADC
- HAL_ADC_PollForConversion //等待转换结束,只适用于轮询
- HAL_IS_BIT_SET //系统宏定义
- HAL_ADC_GetState //返回ADC状态
- HAL_ADC_GetValue //读取ADC的值
- HAL_ADCEx_Calibration_Start //ADC校准;有些芯片不支持校准,F4不支持
文件添加 adc.h,adc.c
- ADC采样时间的最小周期为1.5个周期,加上12.5个周期,那就是最小为14个周期,即总转换时间会大于或等于14个周期;STM32 的 ADC 最大的转换速率为 1Mhz,也就是转换时间为 1us, 当选择14MHz的时钟频率作为ADC的时钟频率时;因为ADC最小转换周期为14个时钟周期,14MHz的时钟,转换时间刚好为1us,而STM32 的 ADC 最大的转换速率为 1Mhz,转换最小时间为 1us,如果选择大于14MHz的时钟频率作为ADC的时钟频率时,转换时间就会少于1us,所以会降低结果的准确性
- 注意:频率会影响到读取功能,如果读取不了可以通过改这些频率
- 设置 ADC1和 ADC2 参数,两个设置一样
adc.h
# ifndef ADC_ADC_H_
# define ADC_ADC_H_
# include "stm32f1xx_hal.h" //HAL库文件声明
extern ADC_HandleTypeDef hadc1;
extern ADC_HandleTypeDef hadc2;
uint16_t ADC_IN_1(void);
uint16_t ADC_IN_2(void);
# endif /* ADC_ADC_H_ */
adc.c
# include "adc.h"
uint16_t ADC_IN_1(void) //ADC采集程序
{
HAL_ADC_Start(&hadc1);//开始ADC采集
HAL_ADC_PollForConversion(&hadc1,500);//等待采集结束
if(HAL_IS_BIT_SET(HAL_ADC_GetState(&hadc1), HAL_ADC_STATE_REG_EOC))//读取ADC完成标志位
{
return HAL_ADC_GetValue(&hadc1);//读出ADC数值
}
return 0;
}
uint16_t ADC_IN_2(void) //ADC采集程序
{
HAL_ADC_Start(&hadc2);//开始ADC采集
HAL_ADC_PollForConversion(&hadc2,500);//等待采集结束
if(HAL_IS_BIT_SET(HAL_ADC_GetState(&hadc2), HAL_ADC_STATE_REG_EOC))//读取ADC完成标志位
{
return HAL_ADC_GetValue(&hadc2);//读出ADC数值
}
return 0;
}
main.c
- HAL_ADCEx_Calibration_Start(&hadc1); :作用是在ADC开始转换之前先校准转换电路,使后续换得的值更准确,只需调用一次即可
# include "main.h"
# include "adc.h"
# include "can.h"
# include "rtc.h"
# include "spi.h"
# include "usart.h"
# include "gpio.h"
/* USER CODE BEGIN Includes */
# include "../../icode/led/led.h"
# include "../../icode/key/key.h"
# include "../../icode/delay/delay.h"
# include "../../icode/buzzer/buzzer.h"
# include "../../icode/relay/relay.h"
# include "../../icode/adc/adc.h"
/* USER CODE END Includes */
int main()
{
RetargetInit(&huart1);//将printf()函数映射到USART1串口上
HAL_ADCEx_Calibration_Start(&hadc1);//ADC采样校准
while(1)
{
a1 = ADC_IN_1(); //读出ADC1数值(电位器)
a2 = ADC_IN_2(); //读出ADC2数值(光敏电阻)
printf("ADC1=%04d ADC2=%04d \r\n", a1, a2); //向串口1发送字符串
HAL_Delay(500);//延时500ms
}
}
DMA读取单路程序
本节用到的HAL函数
- HAL_ADC_Start_DMA //开启DMA功能
- 首先到 ADC1 功能那把单次转换模式转换成连续转换模式
- 如果不勾选【寄存器】选项,那么地址就不会自动加1,每次传递的数值都只放在第一个寄存器里,数值不断覆盖;【半字】代表传递到16位的寄存器中
- 字(Word):在ARM体系结构(32位机)中,字的长度为32位,而在8位/16位处理器体系结构中,字的长度一般为16位
- 半字(Half-Word):在ARM体系结构(32位机)中,半字的长度为16位,与8位/16位处理器体系结构中字的长度一致
- 字节(Byte):在ARM体系结构(32位机)和8位/16位处理器体系结构中,字节的长度均为8位
- 生成的DMA初始化函数在ADC初始化函数下面需要调整一下
main.c
- HAL_ADC_Start_DMA(&hadc1,(uint32_t*)&a1,1); :参数1:要传入的功能 参数2:传递的数据放在哪个寄存器 参数3:传递的数据长度(也就是数据数量),每次读取1个16位数据,下次传递时将上次的覆盖
- 注意:ADC采用校准跟启动DMA函数不能调转,否则读取不了,可以把校准函数删除,这样也可以
- 注意:使用下面频率时,只能使用 /8,使用其他会出现读取不了的情况
# include "main.h"
# include "adc.h"
# include "can.h"
# include "rtc.h"
# include "spi.h"
# include "usart.h"
# include "gpio.h"
/* USER CODE BEGIN Includes */
# include "../../icode/led/led.h"
# include "../../icode/key/key.h"
# include "../../icode/delay/delay.h"
# include "../../icode/buzzer/buzzer.h"
# include "../../icode/relay/relay.h"
# include "../../icode/usart/usart.h"
# include "../../icode/adc/adc.h"
/* USER CODE END Includes */
int main()
{
uint16_t a1, a2; //用于ADC读取的数据
RetargetInit(&huart1);//将printf()函数映射到USART1串口上
HAL_UART_Receive_IT(&huart1, (uint8_t *)&USART1_NewData, 1); //开启串口1接收中断
HAL_ADCEx_Calibration_Start(&hadc1);//ADC采样校准
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)&a1, 1); //启动DMA,采集数据存入的变量地址,长度1
while(1)
{
//a1=ADC_IN_1();//读出ADC1数值(电位器)
a2 = ADC_IN_2(); //读出ADC2数值(光敏电阻)
printf("ADC1=%04d ADC2=%04d \r\n", a1, a2); //向串口1发送字符串
HAL_Delay(500);//延时500ms
}
}
实验效果跟上一个一样
DMA读取多路程序
- 因为 ADC2 不支持 DMA,只能把 ADC2 的通道5改成 ADC1 的通道5,然后在 ADC1 里循环交替,读取通道4和通道5的数值
main.c
# include "main.h"
# include "adc.h"
# include "can.h"
# include "rtc.h"
# include "spi.h"
# include "usart.h"
# include "gpio.h"
/* USER CODE BEGIN Includes */
# include "../../icode/led/led.h"
# include "../../icode/key/key.h"
# include "../../icode/delay/delay.h"
# include "../../icode/buzzer/buzzer.h"
# include "../../icode/relay/relay.h"
# include "../../icode/usart/usart.h"
# include "../../icode/adc/adc.h"
/* USER CODE END Includes */
int main()
{
uint16_t dmaadc[2];//用于多路ADC读取的数据
RetargetInit(&huart1);//将printf()函数映射到USART1串口上
HAL_UART_Receive_IT(&huart1, (uint8_t *)&USART1_NewData, 1); //开启串口1接收中断
HAL_ADCEx_Calibration_Start(&hadc1);//ADC采样校准
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)&dmaadc, 2); //启动DMA,采集数据存入的变量地址,长度2
while(1)
{
//a1=ADC_IN_1();//读出ADC1数值(电位器)
a2 = ADC_IN_2(); //读出ADC2数值(光敏电阻)
printf("ADC1=%04d ADC2=%04d \r\n", dmaadc[0], dmaadc[1]); //向串口1发送字符串
HAL_Delay(500);//延时500ms
}
}
RTC与BKP驱动程序
本节用到的HAL函数
- __HAL_RCC_PWR_CLK_ENABLE //系统宏定义
- HAL_PWR_EnableBkUpAccess //取消后备区域写保护
- HAL_RTCEx_BKUPRead //读取后备储存器
- HAL_RTCEx_BKUPWrite //写入后备储存器
- MX_RTC_Init //CubeMX生成的RTC初始化函数
- HAL_RTC_GetTime //读出时间值
- HAL_RTC_GetDate //读出日期值(先读时间再度日期!)
- HAL_RTC_SetTime //写入时间值
- HAL_RTC_SetDate //写入日期值
后面需要添加自制的 rtc.c,rtc.h 文件
BKP功能是单片机内部的一组16位寄存器,可以自由存放数据,BKP的寄存器与其他寄存器的最大区别是它可由单片机第1脚连接的3v电池供电,使单片机电源断电后,BKP寄存器中的数据不会丢失
- 打开CubeMX生成的 rtc.c 文件进行下面的编辑(STM32F1系列官方库没有保存年月日的功能,断电上电后会恢复初始值,修改代码后RTC的时,分,秒数值将可以断电走时,但年,月,日,星期值无法保存)
__HAL_RCC_PWR_CLK_ENABLE();//使能电源时机PWR
HAL_PWR_EnableBkUpAccess();//取消备份区域写保护
- 当此程序单片机首次上电时,BKP第一组的值默认是0xFFFF,当默认值不是0x5050时,则执行 if 语句,将0x5050写入BKP第一组寄存器中(0x5050可以是任意值,当标记而已),此时单片机寄存器第一组数值始终是0x5050,直到单片机外接备用电池断开或者电量不足,才能回到默认的0xFFFF
if(HAL_RTCEx_BKUPRead(&hrtc,RTC_BKP_DR1)!=0x5050)//判断是否首次上电
{
HAL_RTCEx_BKUPWrite(&hrtc,RTC_BKP_DR1,0x5050);//标记数值(写入上电检查数值)
}
main.c
# include "main.h"
# include "adc.h"
# include "can.h"
# include "rtc.h"
# include "spi.h"
# include "usart.h"
# include "gpio.h"
/* USER CODE BEGIN Includes */
# include "../../icode/led/led.h"
# include "../../icode/key/key.h"
# include "../../icode/delay/delay.h"
# include "../../icode/buzzer/buzzer.h"
# include "../../icode/relay/relay.h"
# include "../../icode/usart/usart.h"
# include "../../icode/adc/adc.h"
/* USER CODE END Includes */
int main()
{
RTC_DateTypeDef RtcDate;//定义存放RTC结构体
RTC_TimeTypeDef RtcTime;
RetargetInit(&huart1);//将printf()函数映射到USART1串口上
HAL_UART_Receive_IT(&huart1, (uint8_t *)&USART1_NewData, 1); //开启串口1接收中断
while(1)
{
if(USART1_RX_STA & 0xC000) //如果标志位是0xC000表示收到数据串完成,可以处理
{
if((USART1_RX_STA & 0x3FFF) == 0) //单独的回车键再显示一次欢迎词
{
HAL_RTC_GetTime(&hrtc, &RtcTime, RTC_FORMAT_BIN); //读出时间值
HAL_RTC_GetDate(&hrtc, &RtcDate, RTC_FORMAT_BIN); //一定要先读出时间值再读出日期值
printf("广东理工职业学校 \r\n");
printf("实时时间:%04d-%02d-%02d %02d:%02d:%02d \r\n", 2000 + RtcDate.Year, RtcDate.Month, RtcDate.Date, RtcTime.Hours, RtcTime.Minutes, RtcTime.Seconds); //显示年月日时分秒
printf("单击回车键更新时间,输入字母C初始化时钟 \r\n");
printf("请输入设置时间,格式20220513210000,按回车键确定");
}
else if((USART1_RX_STA & 0x3FFF) == 1) //判断是不是1个
{
if(USART1_RX_BUF[0] == 'c' || USART1_RX_BUF[0] == 'C') //判断是否输入'c'或者'C'
{
MX_RTC_Init();//初始化时钟
printf("初始化时钟成功!\r\n");//显示初始化成功
}
else
{
printf("指令错误!\r\n");//显示指令错误
}
}
else if((USART1_RX_STA & 0x3FFF) == 14) //判断数据是不是14个
{
//将超级终端发过来的数据换算写入RTC
RtcDate.Year = (USART1_RX_BUF[2] - 0x30) * 10 + USART1_RX_BUF[3] - 0x30; //减0x30才能得到十进制的0~9
RtcDate.Month = (USART1_RX_BUF[4] - 0x30) * 10 + USART1_RX_BUF[5] - 0x30;
RtcDate.Date = (USART1_RX_BUF[6] - 0x30) * 10 + USART1_RX_BUF[7] - 0x30;
RtcTime.Hours = (USART1_RX_BUF[8] - 0x30) * 10 + USART1_RX_BUF[9] - 0x30;
RtcTime.Minutes = (USART1_RX_BUF[10] - 0x30) * 10 + USART1_RX_BUF[11] - 0x30;
RtcTime.Seconds = (USART1_RX_BUF[12] - 0x30) * 10 + USART1_RX_BUF[13] - 0x30;
if(HAL_RTC_SetTime(&hrtc, &RtcTime, RTC_FORMAT_BIN) != HAL_OK) //将数据写入RTC程序
{
printf("写入时间失败! \r\n");
}
else if(HAL_RTC_SetDate(&hrtc, &RtcDate, RTC_FORMAT_BIN) != HAL_OK)
{
printf("写入日期失败! \r\n");
}
else
{
printf("写入成功!\r\n");
}
}
else
{
printf("指令错误!\r\n");
}
USART1_RX_STA = 0; //将串口数据位清0
}
}
}
实验现象
- 为了让年月日也能断电不会丢失,完成下面设置,设置完后 MX_RTC_Init 从 static 静态函数变成普通函数,函数中定义的结构体也不再是全局变量,还需要把之前在系统生成的 rtc.c 文件里添加的代码删掉
rtc.h
# ifndef INC_RTC_H_
# define INC_RTC_H_
# include "stm32f1xx_hal.h" //HAL库文件声明
# include "main.h" //IO定义与初始化函数在main.c文件中,必须引用
/*
//时间读写与设置说明//
1,在mani.c文件中主循环之前放入RTC_Init();可使能RTC时钟
RTC_Init函数自带判断首次上电功能
2,使用RTC_Get();读出时间 读出的数据存放在:
年 ryear (16位)
月 rmon (以下都是8位)
日 rday
时 rhour
分 rmin
秒 rsec
周 rweek
3,使用RTC_Set(4位年,2位月,2位日,2位时,2位分,2位秒); 写入时间
例如:RTC_Set(2022,8,6,21,34,0);
其他函数都是帮助如上3个函数的,不需要调用
注意要使用RTC_Get和RTC_Set的返回值,为0时表示读写正确
*/
extern RTC_HandleTypeDef hrtc;
//声明rtc.c文件中定义的全局变量(注意:这里不能给变量赋值)
extern uint16_t ryear;
extern uint8_t rmon,rday,rhour,rmin,rsec,rweek;
void RTC_Init(void); //用户自建的带有上电BPK判断的RTC初始化【在主循环前调用】
uint8_t Is_Leap_Year(uint16_t year);//判断是否是闰年函数
uint8_t RTC_Get(void);//读出当前时间值【主函数中需要读RTC时调用】
uint8_t RTC_Set(uint16_t syear,uint8_t smon,uint8_t sday,uint8_t hour,uint8_t min,uint8_t sec);//写入当前时间【主函数中需要写入RTC时调用】
uint8_t RTC_Get_Week(uint16_t year,uint8_t month,uint8_t day);//按年月日计算星期
# endif
rtc.c
# include "rtc.h"
//以下2行全局变量,用于RTC时间的读取与读入
uint16_t ryear; //4位年
uint8_t rmon,rday,rhour,rmin,rsec,rweek;//2位月日时分秒周
void RTC_Init(void) //用户自建的带有上电BPK判断的RTC初始化
{
hrtc.Instance = RTC;
hrtc.Init.AsynchPrediv = RTC_AUTO_1_SECOND;
hrtc.Init.OutPut = RTC_OUTPUTSOURCE_NONE;
if (HAL_RTC_Init(&hrtc) != HAL_OK)
{
Error_Handler();
}
if(HAL_RTCEx_BKUPRead(&hrtc,RTC_BKP_DR1)!=0X5050){ //判断是否首次上电
HAL_RTCEx_BKUPWrite(&hrtc,RTC_BKP_DR1,0X5050); //标记数值 下次不执行“首次上电”的部分
RTC_Set(2022,1,1,0,0,0);//写入RTC时间的操作RTC_Set(4位年,2位月,2位日,2位时,2位分,2位秒)
}
}
//判断是否是闰年函数
//月份 1 2 3 4 5 6 7 8 9 10 11 12
//闰年 31 29 31 30 31 30 31 31 30 31 30 31
//非闰年 31 28 31 30 31 30 31 31 30 31 30 31
//输入:年份
//输出:该年份是不是闰年.1,是.0,不是
uint8_t Is_Leap_Year(uint16_t year)
{
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) //能被4整除并且不能被100整除 或者 能被400整除就是闰年
return 1;
else
return 0;
}
//设置时钟
//把输入的时钟转换为秒钟
//以1970年1月1日为基准
//1970~2099年为合法年份
//月份数据表
uint8_t const table_week[12]={0,3,3,6,1,4,6,2,5,0,3,5}; //月修正数据表
const uint8_t mon_table[12]={31,28,31,30,31,30,31,31,30,31,30,31};//平年的月份日期表
//写入时间
uint8_t RTC_Set(uint16_t syear,uint8_t smon,uint8_t sday,uint8_t hour,uint8_t min,uint8_t sec){ //写入当前时间(1970~2099年有效),
uint16_t t;
uint32_t seccount=0;
if(syear<2000||syear>2099)return 1;//syear范围1970-2099,此处设置范围为2000-2099
for(t=1970;t<syear;t++){ //把所有年份的秒钟相加
if(Is_Leap_Year(t))seccount+=31622400;//闰年的秒钟数
else seccount+=31536000; //平年的秒钟数
}
smon-=1;
for(t=0;t<smon;t++){ //把前面月份的秒钟数相加
seccount+=(uint32_t)mon_table[t]*86400;//月份秒钟数相加
if(Is_Leap_Year(syear)&&t==1)seccount+=86400;//闰年2月份增加一天的秒钟数
}
seccount+=(uint32_t)(sday-1)*86400;//把前面日期的秒钟数相加
seccount+=(uint32_t)hour*3600;//小时秒钟数
seccount+=(uint32_t)min*60; //分钟秒钟数
seccount+=sec;//最后的秒钟加上去
//【寄存器操作】因为HAL库的不完善,无法直接调用RTC_ReadTimeCounter函数 此处改用寄存器直接操作
RTC->CRL|=1<<4; //允许配置
RTC->CNTL=seccount&0xffff;
RTC->CNTH=seccount>>16;
RTC->CRL&=~(1<<4);//配置更新
while(!(RTC->CRL&(1<<5)));//等待RTC寄存器操作完成
//【寄存器操作】结束
return 0; //返回值:0,成功;其他:错误代码.
}
//读出时间
uint8_t RTC_Get(void){//读出当前时间值 //返回值:0,成功;其他:错误代码.
static uint16_t daycnt=0;
uint32_t timecount=0;
uint32_t temp=0;
uint16_t temp1=0;
//【寄存器操作】因为HAL库的不完善,无法直接调用RTC_WriteTimeCounter函数 此处改用寄存器直接操作
timecount=RTC->CNTH;//得到计数器中的值(秒钟数)
timecount<<=16;
timecount+=RTC->CNTL;
//【寄存器操作】结束
temp=timecount/86400; //得到天数(秒钟数对应的)
if(daycnt!=temp){//超过一天了
daycnt=temp;
temp1=1970; //从1970年开始
while(temp>=365){
if(Is_Leap_Year(temp1)){//是闰年
if(temp>=366)temp-=366;//闰年的秒钟数
else {temp1++;break;}
}
else temp-=365; //平年
temp1++;
}
ryear=temp1;//得到年份
temp1=0;
while(temp>=28){//超过了一个月
if(Is_Leap_Year(ryear)&&temp1==1){//当年是不是闰年/2月份
if(temp>=29)temp-=29;//闰年的秒钟数
else break;
}else{
if(temp>=mon_table[temp1])temp-=mon_table[temp1];//平年
else break;
}
temp1++;
}
rmon=temp1+1;//得到月份
rday=temp+1; //得到日期
}
temp=timecount%86400; //得到秒钟数
rhour=temp/3600; //小时
rmin=(temp%3600)/60; //分钟
rsec=(temp%3600)%60; //秒钟
rweek=RTC_Get_Week(ryear,rmon,rday);//获取星期
return 0;
}
uint8_t RTC_Get_Week(uint16_t year,uint8_t month,uint8_t day){ //按年月日计算星期(只允许1901-2099年)//已由RTC_Get调用
uint16_t temp2;
uint8_t yearH,yearL;
yearH=year/100;
yearL=year%100;
// 如果为21世纪,年份数加100
if (yearH>19)yearL+=100;
// 所过闰年数只算1900年之后的
temp2=yearL+yearL/4;
temp2=temp2%7;
temp2=temp2+day+table_week[month-1];
if (yearL%4==0&&month<3)temp2--;
return(temp2%7); //返回星期值
}
main.c
# include "main.h"
# include "adc.h"
# include "can.h"
# include "rtc.h"
# include "spi.h"
# include "usart.h"
# include "gpio.h"
/* USER CODE BEGIN Includes */
# include "../../icode/led/led.h"
# include "../../icode/key/key.h"
# include "../../icode/delay/delay.h"
# include "../../icode/buzzer/buzzer.h"
# include "../../icode/relay/relay.h"
# include "../../icode/usart/usart.h"
# include "../../icode/adc/adc.h"
# include "../../icode/rtc/rtc.h"
/* USER CODE END Includes */
int main()
{
uint16_t dmaadc[2];//用于多路ADC读取的数据
RetargetInit(&huart1);//将printf()函数映射到USART1串口上
HAL_UART_Receive_IT(&huart1, (uint8_t *)&USART1_NewData, 1); //开启串口1接收中断
RTC_Init();//自编的初始化函数
while(1)
{
if(USART1_RX_STA&0xC000)//如果标志位是0xC000表示收到数据串完成,可以处理
{
if((USART1_RX_STA&0x3FFF)==0)//单独的回车键再显示一次欢迎词
{
RTC_Get();//读取当前RTC日期与时间
printf("广东理工职业学校 \r\n");
printf("实时时间:%04d-%02d-%02d %02d:%02d:%02d \r\n",ryear,rmon,rday,rhour,rmin,rsec);//显示年月日时分秒
printf("单击回车键更新时间,输入字母C初始化时钟 \r\n");
printf("请输入设置时间,格式20220513210000,按回车键确定 \r\n");
}
else if((USART1_RX_STA&0x3FFF)==1)//判断是不是1个
{
if(USART1_RX_BUF[0]=='c'||USART1_RX_BUF[0]=='C')//判断是否输入'c'或者'C'
{
MX_RTC_Init();//初始化时钟
printf("初始化时钟成功!\r\n");//显示初始化成功
}
else
{
printf("指令错误!\r\n");//显示指令错误
}
}
else if((USART1_RX_STA&0x3FFF)==14)//判断数据是不是14个
{
//将超级终端发过来的数据换算写入RTC
ryear=(USART1_RX_BUF[0]-0x30)*1000+(USART1_RX_BUF[1]-0x30)*100+(USART1_RX_BUF[2]-0x30)*10+(USART1_RX_BUF[3]-0x30);//减0x30才能得到十进制的0~9
rmon=(USART1_RX_BUF[4]-0x30)*10+USART1_RX_BUF[5]-0x30;
rday=(USART1_RX_BUF[6]-0x30)*10+USART1_RX_BUF[7]-0x30;
rhour=(USART1_RX_BUF[8]-0x30)*10+USART1_RX_BUF[9]-0x30;
rmin=(USART1_RX_BUF[10]-0x30)*10+USART1_RX_BUF[11]-0x30;
rsec=(USART1_RX_BUF[12]-0x30)*10+USART1_RX_BUF[13]-0x30;
if(RTC_Set(ryear,rmon,rday,rhour,rmin,rsec)!=HAL_OK)//将数据写入RTC程序
{
printf("写入时间失败! \r\n");
}
else
{
printf("写入成功!\r\n");
}
}
else
{
printf("指令错误!\r\n");
}
USART1_RX_STA=0;//将串口数据位清0
}
}
}
温湿度传感器驱动程序
本节用到的HAL函数
- HAL_GPIO_Init //GPIO初始化函数,用法跟固件库差不多
- HAL_GPIO_WritePin //给某个GPIO写0或1
- HAL_GPIO_ReadPin //读取指定的输入端口引脚的电平值
添加 dht11.h,dht11.c
dht11.h
# ifndef DHT11_DHT11_H_
# define DHT11_DHT11_H_
# include "stm32f1xx_hal.h"
# include "../delay/delay.h"
void DHT11_IO_OUT (void);
void DHT11_IO_IN (void);
void DHT11_RST (void);
uint8_t Dht11_Check(void);
uint8_t Dht11_ReadBit(void);
uint8_t Dht11_ReadByte(void);
uint8_t DHT11_Init (void);
uint8_t DHT11_ReadData(uint8_t *h);
# endif /* DHT11_DHT11_H_ */
dht11.c
# include "dht11.h"
# include "main.h"
void DHT11_IO_OUT (void){ //端口变为输出
GPIO_InitTypeDef GPIO_InitStruct={0};
GPIO_InitStruct.Pin = DHT11_DA_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
void DHT11_IO_IN (void){ //端口变为输入
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = DHT11_DA_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
void DHT11_RST (void){ //DHT11端口复位,发出起始信号(IO发送)
DHT11_IO_OUT();
HAL_GPIO_WritePin(GPIOB,DHT11_DA_Pin, GPIO_PIN_RESET);
HAL_Delay(20); //拉低至少18ms
HAL_GPIO_WritePin(GPIOB,DHT11_DA_Pin, GPIO_PIN_SET);
delay_us(30); //主机拉高20~40us
}
uint8_t Dht11_Check(void){ //等待DHT11回应,返回1:未检测到DHT11,返回0:成功(IO接收)
uint8_t retry=0;
DHT11_IO_IN();//IO到输入状态
while (HAL_GPIO_ReadPin(GPIOB,DHT11_DA_Pin)&&retry<100){//DHT11会拉低40~80us
retry++;
delay_us(1);
}
if(retry>=100)return 1; else retry=0;
while (!HAL_GPIO_ReadPin(GPIOB,DHT11_DA_Pin)&&retry<100){//DHT11拉低后会再次拉高40~80us
retry++;
delay_us(1);
}
if(retry>=100)return 1;
return 0;
}
uint8_t Dht11_ReadBit(void){ //从DHT11读取一个位 返回值:1/0
uint8_t retry=0;
while(HAL_GPIO_ReadPin(GPIOB,DHT11_DA_Pin)&&retry<100){//等待变为低电平
retry++;
delay_us(1);
}
retry=0;
while(!HAL_GPIO_ReadPin(GPIOB,DHT11_DA_Pin)&&retry<100){//等待变高电平
retry++;
delay_us(1);
}
delay_us(40);//等待40us //用于判断高低电平,即数据1或0
if(HAL_GPIO_ReadPin(GPIOB,DHT11_DA_Pin))return 1; else return 0;
}
uint8_t Dht11_ReadByte(void){ //从DHT11读取一个字节 返回值:读到的数据
uint8_t i,dat;
dat=0;
for (i=0;i<8;i++){
dat<<=1;
dat|=Dht11_ReadBit();
}
return dat;
}
uint8_t DHT11_Init (void){ //DHT11初始化
DHT11_RST();//DHT11端口复位,发出起始信号
return Dht11_Check(); //等待DHT11回应
}
uint8_t DHT11_ReadData(uint8_t *h){ //读取一次数据//湿度值(十进制,范围:20%~90%) ,温度值(十进制,范围:0~50°),返回值:0,正常;1,失败
uint8_t buf[5];
uint8_t i;
DHT11_RST();//DHT11端口复位,发出起始信号
if(Dht11_Check()==0){ //等待DHT11回应
for(i=0;i<5;i++){//读取5位数据
buf[i]=Dht11_ReadByte(); //读出数据
}
if((buf[0]+buf[1]+buf[2]+buf[3])==buf[4]){ //数据校验
*h=buf[0]; //将湿度值放入指针1
h++;
*h=buf[2]; //将温度值放入指针2
}
}else return 1;
return 0;
}
main.c
# include "main.h"
# include "adc.h"
# include "can.h"
# include "rtc.h"
# include "spi.h"
# include "usart.h"
# include "gpio.h"
/* USER CODE BEGIN Includes */
# include "../../icode/led/led.h"
# include "../../icode/key/key.h"
# include "../../icode/delay/delay.h"
# include "../../icode/buzzer/buzzer.h"
# include "../../icode/relay/relay.h"
# include "../../icode/usart/usart.h"
# include "../../icode/adc/adc.h"
# include "../../icode/dht11/dht11.h"
/* USER CODE END Includes */
int main()
{
uint8_t DHT11_BUF[2] = {0}; //存放DHT11数据
RetargetInit(&huart1);//将printf()函数映射到USART1串口上
HAL_UART_Receive_IT(&huart1, (uint8_t *)&USART1_NewData, 1); //开启串口1接收中断
HAL_Delay(500);
if(DHT11_Init() == 0)
{
printf("准备就绪!\r\n");
}
else
{
printf("初始化 失败!!! \r\n");
}
HAL_Delay(1500);
DHT11_ReadData(DHT11_BUF);//读出DHT11数据
while(1)
{
DHT11_ReadData(DHT11_BUF);//读出DHT11数据
printf("湿度:%02d%% 温度:%02d℃ \r\n", DHT11_BUF[0], DHT11_BUF[1]); //显示温湿度,两个%才能显示一个%
HAL_Delay(1500);//延时
}
}
实验现象
SPI闪存芯片驱动程序
本节用到的HAL函数
- HAL_SPI_TransmitReceive //同时接收和发送函数
添加 w25qxx.h,w25qxx.h 文件;手册中文可以参考:W25Q128BV-datasheet中文资料-ispsubb-ChinaUnix博客
W25Q128 芯片介绍
W25Q128FV串行Flash内存由可编程的65536页组成,每一页256字节 可以在一时间编程高达256字节的内存 擦除内存可以是按16个页擦除(即一个Sector),128个页擦除(八个Sector),256个页擦除(16个Sector),或者整片擦除 标准SPI通信支持时钟频率高达104MHz,Dual SPI通信支持时钟频率高达208MHz,QSPI通信支持时钟频率高达416MHz 例如W25Q128,内存为16MByte(128Mbits/8bit),它有256个块,每个块有16个扇区,每个扇区有16页
写内存的时候都需要注意Flash的一个通病,那就是Flash编程只能将1写为0,而不能将0写成1 所以我们需要在写内存的时候将内存擦除,使用内存擦除指令擦除内存,内存变为0xFF,然后再写内存 有时候我们可能要格式化内存,那么就还需要整片擦除指令
了解引脚封装与功能
分析电路连接
- 首先,芯片4脚GND连接电源的GND,8脚VCC连接到3.3V电源上,芯片3脚WP(# :低电平有效)连接VCC使WP接到高电平,即不使用硬件写保护功能,芯片始终可以写入数据,芯片7脚HOLD也连接到高电平,关闭保持状态使芯片始终可操作,芯片1脚CS片选通过P6跳线连接到单片机的PB12端口,芯片的2,5,6脚是标准的SPI总线接口,它们通过P6跳线连接到PB13~PB15端口
开始设置与编写程序
w25qxx.h
# ifndef W25Q128_W25QXX_H_
# define W25Q128_W25QXX_H_
# include "stm32f1xx_hal.h" //HAL库文件声明
# include "../delay/delay.h"
//25系列FLASH芯片厂商与容量代号(厂商代号EF)
# define W25Q80 0XEF13
# define W25Q16 0XEF14
# define W25Q32 0XEF15
# define W25Q64 0XEF16
# define W25Q128 0XEF17
# define W25Q256 0XEF18
# define EX_FLASH_ADD 0x000000 //W25Q128的地址是24位宽
extern uint16_t W25QXX_TYPE;//定义W25QXX芯片型号
extern SPI_HandleTypeDef hspi2;
//////////////////////////////////////////////////////////////////////////////////
//指令表
# define W25X_WriteEnable 0x06 //写操作允许,置1
# define W25X_WriteDisable 0x04 //写操作禁止,置0
# define W25X_ReadStatusReg1 0x05 //读状态寄存器1
# define W25X_ReadStatusReg2 0x35 //读状态寄存器2
# define W25X_ReadStatusReg3 0x15 //读状态寄存器3
# define W25X_WriteStatusReg1 0x01 //写状态寄存器1
# define W25X_WriteStatusReg2 0x31 //写状态寄存器2
# define W25X_WriteStatusReg3 0x11 //写状态寄存器3
# define W25X_ReadData 0x03 //从内存中顺序读取一个或多个数据字节
# define W25X_FastReadData 0x0B //快速读取指令类似于读取数据指令
# define W25X_FastReadDual 0x3B //快速读取双输出指令(快速下载代码的理想选择)
# define W25X_PageProgram 0x02 //擦除一页(256字节)
# define W25X_BlockErase 0xD8 //擦除指定块(64k)
# define W25X_SectorErase 0x20 //擦除扇区(4k)
# define W25X_ChipErase 0xC7 //擦除整个芯片
# define W25X_PowerDown 0xB9 //掉电模式
# define W25X_ReleasePowerDown 0xAB //解除掉电模式
# define W25X_DeviceID 0xAB //获取设备ID号
# define W25X_ManufactDeviceID 0x90 //读取制造商/设备ID
# define W25X_JedecDeviceID 0x9F //也是读取ID
# define W25X_Enable4ByteAddr 0xB7
# define W25X_Exit4ByteAddr 0xE9
uint8_t SPI2_ReadWriteByte(uint8_t TxData);//SPI2总线底层读写
void W25QXX_CS(uint8_t a);//W25QXX片选引脚控制
uint8_t W25QXX_Init(void);//初始化W25QXX函数
uint16_t W25QXX_ReadID(void);//读取FLASH ID
uint8_t W25QXX_ReadSR(uint8_t regno);//读取状态寄存器
void W25QXX_4ByteAddr_Enable(void);//使能4字节地址模式
void W25QXX_Write_SR(uint8_t regno,uint8_t sr);//写状态寄存器
void W25QXX_Write_Enable(void);//写使能
void W25QXX_Write_Disable(void);//写保护
void W25QXX_Write_NoCheck(uint8_t* pBuffer,uint32_t WriteAddr,uint16_t NumByteToWrite);//无检验写SPI FLASH
void W25QXX_Read(uint8_t* pBuffer,uint32_t ReadAddr,uint16_t NumByteToRead);//读取flash
void W25QXX_Write(uint8_t* pBuffer,uint32_t WriteAddr,uint16_t NumByteToWrite);//写入flash
void W25QXX_Erase_Chip(void);//整片擦除
void W25QXX_Erase_Sector(uint32_t Dst_Addr);//扇区擦除
void W25QXX_Wait_Busy(void);//等待空闲
void W25QXX_PowerDown(void);//进入掉电模式
void W25QXX_WAKEUP(void);//唤醒
# endif /* W25Q128_W25QXX_H_ */
- HAL_SPI_TransmitReceive(&hspi2,&TxData,&Rxdata,1,1000); :参数1:功能句柄 参数2:发送数据起始地址 参数3:接收数据起始地址 参数4:收发数据的数量(当前是1个字节) 参数5:超时时间
- 当然如果你只需发送不想接收也可以填任意参数然后不用管返回值,但是一般都是发挥全双工通信高效率
- W25QXX_ReadID 读取芯片ID函数指令的来源:
w25qxx.c
# include "w25qxx.h"
# include "main.h"
uint16_t W25QXX_TYPE=W25Q128;//默认是W25Q128
//4Kbytes为一个Sector
//16个扇区为1个Block
//W25Q128
//容量为16M字节,共有128个Block,4096个Sector
//SPI2总线读写一个字节
//参数是写入的字节,返回值是读出的字节
uint8_t SPI2_ReadWriteByte(uint8_t TxData)
{
uint8_t Rxdata;//定义一个变量Rxdata
HAL_SPI_TransmitReceive(&hspi2,&TxData,&Rxdata,1,1000);//调用固件库函数收发数据
return Rxdata;//返回收到的数据
}
void W25QXX_CS(uint8_t a)//软件控制函数(0为低电平,其他值为高电平)
{
if(a==0)HAL_GPIO_WritePin(W25Q128_CS_GPIO_Port, W25Q128_CS_Pin, GPIO_PIN_RESET);//使能闪存芯片读写操作
else HAL_GPIO_WritePin(W25Q128_CS_GPIO_Port, W25Q128_CS_Pin, GPIO_PIN_SET);//禁止闪存芯片读写操作
}
//初始化SPI FLASH的IO口
uint8_t W25QXX_Init(void)
{
uint8_t temp;//定义一个变量temp
W25QXX_CS(1);//0片选开启,1片选关闭
W25QXX_TYPE = W25QXX_ReadID();//读取FLASH ID.
if(W25QXX_TYPE == W25Q256)//SPI FLASH为W25Q256时才用设置为4字节地址模式
{
temp = W25QXX_ReadSR(3);//读取状态寄存器3,判断地址模式
if((temp&0x01)==0)//如果不是4字节地址模式,则进入4字节地址模式
{
W25QXX_CS(0);//0片选开启,1片选关闭
SPI2_ReadWriteByte(W25X_Enable4ByteAddr);//发送进入4字节地址模式指令
W25QXX_CS(1);//0片选开启,1片选关闭
}
}
if(W25QXX_TYPE==W25Q256||W25QXX_TYPE==W25Q128||W25QXX_TYPE==W25Q64
||W25QXX_TYPE==W25Q32||W25QXX_TYPE==W25Q16||W25QXX_TYPE==W25Q80)
return 0; else return 1;//如果读出ID是现有型号列表中的一个,则识别芯片成功!
}
//读取W25QXX的状态寄存器,W25QXX一共有3个状态寄存器
//状态寄存器1:
//BIT7 6 5 4 3 2 1 0
//SPR RV TB BP2 BP1 BP0 WEL BUSY
//SPR:默认0,状态寄存器保护位,配合WP使用
//TB,BP2,BP1,BP0:FLASH区域写保护设置
//WEL:写使能锁定
//BUSY:忙标记位(1,忙;0,空闲)
//默认:0x00
//状态寄存器2:
//BIT7 6 5 4 3 2 1 0
//SUS CMP LB3 LB2 LB1 (R) QE SRP1
//状态寄存器3:
//BIT7 6 5 4 3 2 1 0
//HOLD/RST DRV1 DRV0 (R) (R) WPS (R) (R)
//regno:状态寄存器号,范:1~3
//返回值:状态寄存器值
uint8_t W25QXX_ReadSR(uint8_t regno)
{
uint8_t byte=0,command=0;
switch(regno)
{
case 1:
command=W25X_ReadStatusReg1;//读状态寄存器1指令
break;
case 2:
command=W25X_ReadStatusReg2;//读状态寄存器2指令
break;
case 3:
command=W25X_ReadStatusReg3;//读状态寄存器3指令
break;
default:
command=W25X_ReadStatusReg1;//读状态寄存器1指令
break;
}
W25QXX_CS(0);//0片选开启,1片选关闭
SPI2_ReadWriteByte(command);//发送读取状态寄存器命令
byte=SPI2_ReadWriteByte(0Xff);//读取一个字节
W25QXX_CS(1);//0片选开启,1片选关闭
return byte;//返回变量byte
}
//写W25QXX状态寄存器
void W25QXX_Write_SR(uint8_t regno,uint8_t sr)
{
uint8_t command=0;
switch(regno)
{
case 1:
command=W25X_WriteStatusReg1;//写状态寄存器1指令
break;
case 2:
command=W25X_WriteStatusReg2;//写状态寄存器2指令
break;
case 3:
command=W25X_WriteStatusReg3;//写状态寄存器3指令
break;
default:
command=W25X_WriteStatusReg1;
break;
}
W25QXX_CS(0);//0片选开启,1片选关闭
SPI2_ReadWriteByte(command);//发送写取状态寄存器命令
SPI2_ReadWriteByte(sr);//写入一个字节
W25QXX_CS(1);//0片选开启,1片选关闭
}
//W25QXX写使能
//将WEL置位
void W25QXX_Write_Enable(void)
{
W25QXX_CS(0);//0片选开启,1片选关闭
SPI2_ReadWriteByte(W25X_WriteEnable);//发送写使能
W25QXX_CS(1);//0片选开启,1片选关闭
}
//W25QXX写禁止
//将WEL清零
void W25QXX_Write_Disable(void)
{
W25QXX_CS(0);//0片选开启,1片选关闭
SPI2_ReadWriteByte(W25X_WriteDisable);//发送写禁止指令
W25QXX_CS(1);//0片选开启,1片选关闭
}
//读取芯片ID
//高8位是厂商代号(本程序不判断厂商代号)
//低8位是容量大小
//0XEF13型号为W25Q80
//0XEF14型号为W25Q16
//0XEF15型号为W25Q32
//0XEF16型号为W25Q64
//0XEF17型号为W25Q128(目前洋桃2号开发板使用128容量芯片)
//0XEF18型号为W25Q256
uint16_t W25QXX_ReadID(void)
{
uint16_t Temp = 0;
W25QXX_CS(0);//0片选开启,1片选关闭
SPI2_ReadWriteByte(0x90);//发送读取ID命令
SPI2_ReadWriteByte(0x00);
SPI2_ReadWriteByte(0x00);
SPI2_ReadWriteByte(0x00);
Temp|=SPI2_ReadWriteByte(0xFF)<<8;//
Temp|=SPI2_ReadWriteByte(0xFF);//合并成16位
W25QXX_CS(1);//0片选开启,1片选关闭
return Temp;
}
//读取SPI FLASH
//在指定地址开始读取指定长度的数据
//pBuffer:数据存储区
//ReadAddr:开始读取的地址(24bit),数据所在的存储器地址
//NumByteToRead:要读取的字节数(最大65535)
void W25QXX_Read(uint8_t* pBuffer,uint32_t ReadAddr,uint16_t NumByteToRead)
{
uint16_t i;
W25QXX_CS(0);//0片选开启,1片选关闭
SPI2_ReadWriteByte(W25X_ReadData);//发送读取命令
if(W25QXX_TYPE==W25Q256)//如果是W25Q256的话地址为4字节的,要发送最高8位
{
SPI2_ReadWriteByte((uint8_t)((ReadAddr)>>24));
}
SPI2_ReadWriteByte((uint8_t)((ReadAddr)>>16));//发送24bit地址
SPI2_ReadWriteByte((uint8_t)((ReadAddr)>>8));
SPI2_ReadWriteByte((uint8_t)ReadAddr);
for(i=0;i<NumByteToRead;i++)
{
pBuffer[i]=SPI2_ReadWriteByte(0XFF);//循环读数
}
W25QXX_CS(1);//0片选开启,1片选关闭
}
//SPI在一页(0~65535)内写入少于256个字节的数据
//在指定地址开始写入最大256字节的数据
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大256),该数不应该超过该页的剩余字节数!!!
void W25QXX_Write_Page(uint8_t* pBuffer,uint32_t WriteAddr,uint16_t NumByteToWrite)
{
uint16_t i;
W25QXX_Write_Enable();//SET WEL
W25QXX_CS(0);//0片选开启,1片选关闭
SPI2_ReadWriteByte(W25X_PageProgram);//发送写页命令
if(W25QXX_TYPE==W25Q256)//如果是W25Q256的话地址为4字节的,要发送最高8位
{
SPI2_ReadWriteByte((uint8_t)((WriteAddr)>>24));
}
SPI2_ReadWriteByte((uint8_t)((WriteAddr)>>16));//发送24bit地址
SPI2_ReadWriteByte((uint8_t)((WriteAddr)>>8));
SPI2_ReadWriteByte((uint8_t)WriteAddr);
for(i=0;i<NumByteToWrite;i++)SPI2_ReadWriteByte(pBuffer[i]);//循环写数
W25QXX_CS(1);//0片选开启,1片选关闭
W25QXX_Wait_Busy();//等待写入结束
}
//无检验写SPI FLASH
//必须确保所写的地址范围内的数据全部为0XFF,否则在非0XFF处写入的数据将失败!
//具有自动换页功能
//在指定地址开始写入指定长度的数据,但是要确保地址不越界!
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大65535)
//CHECK OK
void W25QXX_Write_NoCheck(uint8_t* pBuffer,uint32_t WriteAddr,uint16_t NumByteToWrite)
{
uint16_t pageremain;
pageremain=256-WriteAddr%256; //单页剩余的字节数
if(NumByteToWrite<=pageremain)pageremain=NumByteToWrite;//不大于256个字节
while(1)
{
W25QXX_Write_Page(pBuffer,WriteAddr,pageremain);
if(NumByteToWrite==pageremain)break;//写入结束了
else //NumByteToWrite>pageremain
{
pBuffer+=pageremain;
WriteAddr+=pageremain;
NumByteToWrite-=pageremain; //减去已经写入了的字节数
if(NumByteToWrite>256)pageremain=256; //一次可以写入256个字节
else pageremain=NumByteToWrite; //不够256个字节了
}
};
}
//写SPI FLASH
//在指定地址开始写入指定长度的数据
//该函数带擦除操作!
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大65535)
uint8_t W25QXX_BUFFER[4096];
void W25QXX_Write(uint8_t* pBuffer,uint32_t WriteAddr,uint16_t NumByteToWrite)
{
uint32_t secpos;
uint16_t secoff;
uint16_t secremain;
uint16_t i;
uint8_t* W25QXX_BUF;
W25QXX_BUF=W25QXX_BUFFER;
secpos=WriteAddr/4096;//扇区地址
secoff=WriteAddr%4096;//在扇区内的偏移
secremain=4096-secoff;//扇区剩余空间大小
//printf("ad:%X,nb:%X\r\n",WriteAddr,NumByteToWrite);//测试用
if(NumByteToWrite<=secremain)secremain=NumByteToWrite;//不大于4096个字节
while(1)
{
W25QXX_Read(W25QXX_BUF,secpos*4096,4096);//读出整个扇区的内容
for(i=0;i<secremain;i++)//校验数据
{
if(W25QXX_BUF[secoff+i]!=0XFF)break;//需要擦除
}
if(i<secremain)//需要擦除
{
W25QXX_Erase_Sector(secpos);//擦除这个扇区
for(i=0;i<secremain;i++)//复制
{
W25QXX_BUF[i+secoff]=pBuffer[i];
}
W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096);//写入整个扇区
}else W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);//写已经擦除了的,直接写入扇区剩余区间.
if(NumByteToWrite==secremain)break;//写入结束了
else//写入未结束
{
secpos++;//扇区地址增1
secoff=0;//偏移位置为0
pBuffer+=secremain; //指针偏移
WriteAddr+=secremain;//写地址偏移
NumByteToWrite-=secremain;//字节数递减
if(NumByteToWrite>4096)secremain=4096;//下一个扇区还是写不完
else secremain=NumByteToWrite;//下一个扇区可以写完了
}
};
}
//擦除整个芯片
//等待时间超长...
void W25QXX_Erase_Chip(void)
{
W25QXX_Write_Enable();//SET WEL
W25QXX_Wait_Busy();//等待忙状态
W25QXX_CS(0);//0片选开启,1片选关闭
SPI2_ReadWriteByte(W25X_ChipErase);//发送片擦除命令
W25QXX_CS(1);//0片选开启,1片选关闭
W25QXX_Wait_Busy();//等待芯片擦除结束
}
//擦除一个扇区
//Dst_Addr:扇区地址 根据实际容量设置
//擦除一个扇区的最少时间:150ms
void W25QXX_Erase_Sector(uint32_t Dst_Addr)
{
Dst_Addr*=4096;
W25QXX_Write_Enable();//SET WEL
W25QXX_Wait_Busy();
W25QXX_CS(0);//0片选开启,1片选关闭
SPI2_ReadWriteByte(W25X_SectorErase);//发送扇区擦除指令
if(W25QXX_TYPE==W25Q256)//如果是W25Q256的话地址为4字节的,要发送最高8位
{
SPI2_ReadWriteByte((uint8_t)((Dst_Addr)>>24));
}
SPI2_ReadWriteByte((uint8_t)((Dst_Addr)>>16));//发送24bit地址
SPI2_ReadWriteByte((uint8_t)((Dst_Addr)>>8));
SPI2_ReadWriteByte((uint8_t)Dst_Addr);
W25QXX_CS(1);//0片选开启,1片选关闭
W25QXX_Wait_Busy();//等待擦除完成
}
//等待空闲
void W25QXX_Wait_Busy(void)
{
while((W25QXX_ReadSR(1)&0x01)==0x01);//等待BUSY位清空
}
//进入掉电模式
void W25QXX_PowerDown(void)
{
W25QXX_CS(0);//0片选开启,1片选关闭
SPI2_ReadWriteByte(W25X_PowerDown);//发送掉电命令 0xB9
W25QXX_CS(1);//0片选开启,1片选关闭
delay_us(3);//等待TPD
}
//唤醒
void W25QXX_WAKEUP(void)
{
W25QXX_CS(0);//0片选开启,1片选关闭
SPI2_ReadWriteByte(W25X_ReleasePowerDown);//发送电源唤醒指令 0xAB
W25QXX_CS(1);//0片选开启,1片选关闭
delay_us(3);//等待TRES1
}
main.c
# include "main.h"
# include "adc.h"
# include "can.h"
# include "rtc.h"
# include "spi.h"
# include "usart.h"
# include "gpio.h"
/* USER CODE BEGIN Includes */
# include "../../icode/led/led.h"
# include "../../icode/key/key.h"
# include "../../icode/delay/delay.h"
# include "../../icode/buzzer/buzzer.h"
# include "../../icode/relay/relay.h"
# include "../../icode/usart/usart.h"
# include "../../icode/adc/adc.h"
# include "../../icode/w25q128/w25qxx.h"
/* USER CODE END Includes */
int main()
{
uint8_t EX_FLASH_BUF[1];//w25q128芯片数据缓存数组
RetargetInit(&huart1);//将printf()函数映射到USART1串口上
HAL_UART_Receive_IT(&huart1, (uint8_t *)&USART1_NewData, 1); //开启串口1接收中断
HAL_Delay(500);
W25QXX_Init();//芯片初始化
printf("W25Q128测试程序:按KEY1键显示芯片ID,按KEY2键将0x00地址中的数值加1 \r\n");
while(1)
{
if(KEY_1())
{
EX_FLASH_BUF[0] = W25QXX_ReadID(); //读出芯片ID,芯片十进制ID是61207(十六进制表示0xEF17)
printf("芯片ID:%x \r\n", EX_FLASH_BUF[0]); //显示芯片ID
BUZZER_SOLO1();//提示音1
}
if(KEY_2())
{
BUZZER_SOLO2();//提示音2
W25QXX_Read(EX_FLASH_BUF, EX_FLASH_ADD, 1); //读出芯片数据(参数:读出数据要存放的数值 读取的开始地址 数量)
EX_FLASH_BUF[0]++;//数据加一,注意不是地址加一
if(EX_FLASH_BUF[0] > 200) //如果数值大于指定最大值
{
EX_FLASH_BUF[0] = 0; //清0
}
W25QXX_Write(EX_FLASH_BUF, EX_FLASH_ADD, 1); //写入芯片数据(参数:读出数据要存放的数值 读取的开始地址 数量)
printf("读出0x00地址数据:%d \r\n", EX_FLASH_BUF[0]); //读出数据
BUZZER_SOLO1();//提示音1
}
}
}
实验现象
- 即使断电,0x00地址数据也不会丢失
USB从设备串口驱动程序
本节用到的HAL函数
- HAL_CAN_MspDeInit //禁用CAN功能
- CDC_Transmit_FS //usb虚拟串口发送数据函数
- memset //内存空间初始化
- STM32F103xx增强型系列产品,内嵌一个兼容全速USB的设备控制器,遵循全速USB设备(12兆位/秒)标准,端点可由软件配置,具有待机/唤醒功能 USB专用的48MHz时钟由内部主PLL直接产生(时钟源必须是一个HSE品体振荡器) 这个频率是固定的
- CubeMX设置可以看 3.1
- stm32f103单片机内部的USB接口只能做从设备,电脑是主设备
- 完成下面设置
usbd_cdc_if.c
uint8_t USB_RX_BUF[USB_REC_LEN];//接收缓冲,最大USB_REC_LEN个字节
uint8_t USB_RX_STA=0;//接收状态标记(接收到的有效字节数量)
if(*Len<USB_REC_LEN)//判断收到的数据量是否小于寄存器上限
{
uint16_t i;
USB_RX_STA=*Len;//将数据量值放入标志位
for(i=0;i<*Len;i++)//循环(循环次数=数据数量)
{
USB_RX_BUF[i]=Buf[i];//将数据内容放到数据寄存器
}
}
uint32_t TimeStart=HAL_GetTick();
while(hcdc->TxState)
{
if(HAL_GetTick() - TimeStart > 10)
{
return USBD_BUSY;
}
else
{
break;
}
}
TimeStart = HAL_GetTick();
while(hcdc->TxState)
{
if(HAL_GetTick() - TimeStart > 10)
{
return USBD_BUSY;
}
}
# include <stdarg.h>
void USB_printf(const char *format,...)//USB模拟串口的打印函数
{
va_list args;
uint32_t length;
va_start(args,format);
length=vsnprintf((char*)UserTxBufferFS,APP_TX_DATA_SIZE,(char*)format,args);
va_end(args);
CDC_Transmit_FS(UserTxBufferFS,length);
}
usbd_cdc_if.h
# define USB_REC_LEN 200//定义USB串口最大接收字节数
extern uint8_t USB_RX_BUF[USB_REC_LEN];//接收缓冲,最大USB_REC_LEN个字节,末字节为换行符
extern uint16_t USB_RX_STA;//接收状态标记(接收到的有效字节数量)
void USB_printf(const char *format,...);//USB模拟串口的打印函数
main.c
- 使用USB功能需要禁用CAN功能
# include "../../icode/led/led.h"
# include "../../icode/key/key.h"
# include "../../icode/delay/delay.h"
# include "../../icode/buzzer/buzzer.h"
# include "../../icode/relay/relay.h"
# include "../inc/retarget.h"//用于printf函数串口重映射
# include "../../icode/usart/usart.h"
# include "../../icode/adc/adc.h"
# include "../../icode/rtc/rtc.h"
# include "../../icode/dht11/dht11.h"
# include "../../icode/w25q128/w25qxx.h"
# include "../../USB_DEVICE/APP/usbd_cdc_if.h"
int main(void)
{
RetargetInit(&huart1);//将printf()函数映射到USART1串口上
HAL_UART_Receive_IT(&huart1, (uint8_t *)&USART1_NewData, 1); //开启串口1接收中断
HAL_CAN_MspDeInit(&hcan);//关闭CAN功能,使USB功能可被电脑识别
while (1)
{
//USB模拟串口的查询接收处理(其编程原理与USART1串口收发相同)
if(USB_RX_STA != 0) //判断是否有数据
{
USB_printf("USB_RX:");//向USB模拟串口发送字符串
CDC_Transmit_FS(USB_RX_BUF, USB_RX_STA); //USB串口发送,将接收的数据发回给电脑端(参数1是数据内容,参数2是数据量)
USB_printf("\r\n");//向USB模拟串口发送字符串(回车)
USB_RX_STA = 0; //数据标志位清0
memset(USB_RX_BUF, 0, sizeof(USB_RX_BUF)); //USB串口数据寄存器清0
}
}
}
- 先把程序下载到单片机再拔出来插到底板的USB从机接口
实验现象
省电模式,CRC与芯片ID
本节用到的HAL函数
- HAL_PWR_EnterSLEEPMode //进入睡眠模式
- HAL_PWR_EnterSTOPMode //进入停机模式
- SystemClock_Config //初始化系统时钟
- __HAL_PWR_GET_FLAG //系统宏定义
- HAL_PWR_DisableWakeUpPin //禁用WKUP引脚唤醒功能
- __HAL_PWR_CLEAR_FLAG //系统宏定义(清除 PWR 的挂起标志)
- HAL_PWR_EnableWakeUpPin //使能WKUP引脚唤醒功能
- HAL_PWR_EnterSTANDBYMode //进入待机模式
- HAL_CRC_Calculate //载入数据并返回计算结果
- HAL_CRC_Accumulate //载入数据并返回计算结果
睡眠模式
main.c
- 无需在 CubeMX设置
# include "../../icode/led/led.h"
# include "../../icode/key/key.h"
# include "../../icode/delay/delay.h"
# include "../../icode/buzzer/buzzer.h"
# include "../../icode/relay/relay.h"
# include "../inc/retarget.h"//用于printf函数串口重映射
# include "../../icode/usart/usart.h"
# include "../../icode/adc/adc.h"
# include "../../icode/rtc/rtc.h"
# include "../../icode/dht11/dht11.h"
# include "../../icode/w25q128/w25qxx.h"
# include "../../USB_DEVICE/APP/usbd_cdc_if.h"
int main(void)
{
RetargetInit(&huart1);//将printf()函数映射到USART1串口上
HAL_UART_Receive_IT(&huart1, (uint8_t *)&USART1_NewData, 1); //开启串口1接收中断
HAL_CAN_MspDeInit(&hcan);//关闭CAN功能,使USB功能可被电脑识别
while (1)
{
LED_1(1);//LED亮
LED_2(0);//LED灭
HAL_Delay(100);
LED_1(0);//LED灭
LED_2(1);//LED亮
HAL_Delay(100);
HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI); //进入睡眠模式(任意中断可唤醒)
}
}
观察实验现象可以发现LED正常闪烁,这并不是睡眠模式开启失败,而是内部功能有很多中断连续出现,导致ARM内核无法长时间睡眠,这是正常现象
停机模式
- 将PA0设置为外部中断,按下KEY1被输入一个下降沿信号就会触发中断,使停机的单片机唤醒
main.c
# include "../../icode/led/led.h"
# include "../../icode/key/key.h"
# include "../../icode/delay/delay.h"
# include "../../icode/buzzer/buzzer.h"
# include "../../icode/relay/relay.h"
# include "../inc/retarget.h"//用于printf函数串口重映射
# include "../../icode/usart/usart.h"
# include "../../icode/adc/adc.h"
# include "../../icode/rtc/rtc.h"
# include "../../icode/dht11/dht11.h"
# include "../../icode/w25q128/w25qxx.h"
# include "../../USB_DEVICE/APP/usbd_cdc_if.h"
int main(void)
{
RetargetInit(&huart1);//将printf()函数映射到USART1串口上
HAL_UART_Receive_IT(&huart1, (uint8_t *)&USART1_NewData, 1); //开启串口1接收中断
HAL_CAN_MspDeInit(&hcan);//关闭CAN功能,使USB功能可被电脑识别
while (1)
{
LED_1(1);//LED亮
LED_2(0);//LED灭
HAL_Delay(500);
LED_1(0);//LED灭
LED_2(1);//LED亮
HAL_Delay(500);
if(KEY_2())
{
printf("进入【停机】状态!(按KEY1键外部中断唤醒)\r\n");//串口中断
BUZZER_SOLO1();
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); //进入停机模式
//此处进入停机模式
//接下来程序是在唤醒后执行
SystemClock_Config();//唤醒后需要重新初始化时钟
printf("退出【停机】状态 \r\n");//串口发送
}
}
}
实验现象
待机模式
- 待机模式是在停机模式的基础上关闭了SRAM的电源,使正在运行的程序全部丢失,只能复位重启,在待机模式下,连外部中断也不能唤醒,要想用端口唤醒单片机,最常用的就是 WKUP 功能引脚,它是专用的待机唤醒引脚,当引脚出现一个上升沿信号,单片机就会唤醒,它复用在 PA0端口上 (此处不需要设置,下面会用代码实现)
- 设置下面内容
main.c
# include "../../icode/led/led.h"
# include "../../icode/key/key.h"
# include "../../icode/delay/delay.h"
# include "../../icode/buzzer/buzzer.h"
# include "../../icode/relay/relay.h"
# include "../inc/retarget.h"//用于printf函数串口重映射
# include "../../icode/usart/usart.h"
# include "../../icode/adc/adc.h"
# include "../../icode/rtc/rtc.h"
# include "../../icode/dht11/dht11.h"
# include "../../icode/w25q128/w25qxx.h"
# include "../../USB_DEVICE/APP/usbd_cdc_if.h"
int main(void)
{
RetargetInit(&huart1);//将printf()函数映射到USART1串口上
HAL_UART_Receive_IT(&huart1, (uint8_t *)&USART1_NewData, 1); //开启串口1接收中断
printf("\r\n单片机启动!(按KEY2进入【待机】模式)\r\n");
if(__HAL_PWR_GET_FLAG(PWR_FLAG_SB) != RESET) //判断本次复位是不是从待机模式中唤醒
{
//可在插入待机唤醒的处理程序
printf("从【待机】模式唤醒 \r\n");
HAL_PWR_DisableWakeUpPin(PWR_WAKEUP_PIN1);//禁止WKUP引脚的唤醒功能
__HAL_PWR_CLEAR_FLAG(PWR_FLAG_SB);//清除待机标志位
}
while (1)
{
LED_1(1);//LED亮
LED_2(0);//LED灭
HAL_Delay(500);
LED_1(0);//LED灭
LED_2(1);//LED亮
HAL_Delay(500);
if(KEY_2())
{
printf("进入【待机】状态!(按开发板的”休眠唤醒“键唤醒)\r\n");
BUZZER_SOLO1();
HAL_GPIO_WritePin(GPIOA, KEY1_Pin, GPIO_PIN_RESET);//PA0端口变成低电平,准备好唤醒键初始电平
__HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);//清除唤醒标志位
HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1);//使能WKUP引脚的唤醒功能(使能PA0)
HAL_PWR_EnterSTANDBYMode();//进入待机模式
}
}
}
实验现象
CRC和芯片ID功能
- 设置
main.c
- HAL_CRC_Calculate 和 HAL_CRC_Accumulate 区别是:前者每次调用都会清除CRC寄存器的值(类似于计算器清0),适用于一次性的数据校验;后者则计算前不会清0,适用于不连续的累加式校验
# include "../../icode/led/led.h"
# include "../../icode/key/key.h"
# include "../../icode/delay/delay.h"
# include "../../icode/buzzer/buzzer.h"
# include "../../icode/relay/relay.h"
# include "../inc/retarget.h"//用于printf函数串口重映射
# include "../../icode/usart/usart.h"
# include "../../icode/adc/adc.h"
# include "../../icode/rtc/rtc.h"
# include "../../icode/dht11/dht11.h"
# include "../../icode/w25q128/w25qxx.h"
# include "../../USB_DEVICE/APP/usbd_cdc_if.h"
static const uint32_t CRCBUF[4] = {0x61, 0x62, 0x63, 0x64}; //写入任意要发送的数据
int main(void)
{
uint32_t a, b, c;
RetargetInit(&huart1);//将printf()函数映射到USART1串口上
HAL_UART_Receive_IT(&huart1, (uint8_t *)&USART1_NewData, 1); //开启串口1接收中断
while (1)
{
c = HAL_CRC_Calculate(&hcrc, (uint32_t *)CRCBUF, 4); //载入CRC数据并返回计算结果
//c=HAL_CRC_Accumulate(&hcrc,(uint32_t*)CRCBUF,4)//载入CRC数据并返回计算结果
printf("CRC计算结果:%08X \r\n", c); //将计算结果显示在超级终端
a = *(__IO uint32_t *)(0x1FFFF7E8); //读出3个32位芯片ID(高字节)
b = *(__IO uint32_t *)(0x1FFFF7EC); //
c = *(__IO uint32_t *)(0x1FFFF1F0); //(低字节)
printf("芯片ID:%08X %08X %08X \r\n", a, b, c); //输出16进制ID
while(1);//执行结束在这循环
}
}
实验现象
外部中断与定时器
本节用到的HAL函数
- HAL_GPIO_TogglePin //翻转GPIO引脚电平状态(0变1,1变0)
- __weak void HAL_GPIO_EXTI_Callback //外部中断回调函数(弱函数)
- EXTI0_IRQHandler //EXTI0中断的服务函数
- HAL_GPIO_EXTI_IRQHandler //GPIO外部中断处理函数
- HAL_TIM_Base_Start_IT //开启定时器中断
- HAL_TIM_PWM_Start //开启定时器PWM
- __HAL_TIM_SetCompare //系统宏定义(设置占空比)
STM32F1支持将所有GPIO设置为外部中断输入 外部IO可由上沿、下沿、高低电平的三种方式触发 可选择中断或事件触发
- 设置
- 下面了解即可
- 注意:外部中断初始化函数是在GPIO初始化函数里完成的
- 外部中断处理函数是在 stm32f1xx_it.c 里
- 第一个函数内容是清除中断标志位并调用中断回调函数
- 第二个是中断回调函数(弱函数),只要我们自己在自己程序中创建一个同名的中断回调函数即可
main.c
- 注意:需要修改 key.c 文件 外部中断函数调用时不能使用系统自带的延时函数
# include "../../icode/led/led.h"
# include "../../icode/key/key.h"
# include "../../icode/delay/delay.h"
# include "../../icode/buzzer/buzzer.h"
# include "../../icode/relay/relay.h"
# include "../inc/retarget.h"//用于printf函数串口重映射
# include "../../icode/usart/usart.h"
# include "../../icode/adc/adc.h"
# include "../../icode/rtc/rtc.h"
# include "../../icode/dht11/dht11.h"
# include "../../icode/w25q128/w25qxx.h"
# include "../../USB_DEVICE/APP/usbd_cdc_if.h"
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)//外部中断回调函数
{
if(GPIO_Pin == KEY1_Pin) //判断产生中断的端口
{
if(KEY_1())//再通过按键处理函数判断是按下还是放开
{
//LED_1_Contrary();//每按一次按键,LED电平状态反转一次
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);//这个是跟上面功能一样这个是HAL库
}
}
}
int main(void)
{
RetargetInit(&huart1);//将printf()函数映射到USART1串口上
HAL_UART_Receive_IT(&huart1, (uint8_t *)&USART1_NewData, 1); //开启串口1接收中断
while (1)
{
//没代码
}
}
定时器
- 设置1秒的定时器
(7199+1)X(9999+1)/ 72 = 1,000,000us=1s - 自动重装载初值功能:无论允许还是禁止,中断处理程序都会为定时器自动重装载初值 允许和禁止区别是:Enable,自动重载寄存器的值不会马上生效,而是在下一个更新事件出现时由影子寄存器传入;Disable,写入自动重载寄存器立刻生效
main.c
# include "../../icode/led/led.h"
# include "../../icode/key/key.h"
# include "../../icode/delay/delay.h"
# include "../../icode/buzzer/buzzer.h"
# include "../../icode/relay/relay.h"
# include "../inc/retarget.h"//用于printf函数串口重映射
# include "../../icode/usart/usart.h"
# include "../../icode/adc/adc.h"
# include "../../icode/rtc/rtc.h"
# include "../../icode/dht11/dht11.h"
# include "../../icode/w25q128/w25qxx.h"
# include "../../USB_DEVICE/APP/usbd_cdc_if.h"
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)//外部中断回调函数
{
if(htim == (&htim2)) //判断产生中断的定时器
{
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);//这个是跟上面功能一样这个是HAL库
}
}
int main(void)
{
RetargetInit(&huart1);//将printf()函数映射到USART1串口上
HAL_UART_Receive_IT(&huart1, (uint8_t *)&USART1_NewData, 1); //开启串口1接收中断
HAL_TIM_Base_Start_IT(&htim2);//开启定时器中断(必须开启才能进入中断处理回调函数)
while (1)
{
//没代码
}
}
PWM
- 通道极性是指占空比中 “占” 的部分是高电平还是低电平
main.c
# include "../../icode/led/led.h"
# include "../../icode/key/key.h"
# include "../../icode/delay/delay.h"
# include "../../icode/buzzer/buzzer.h"
# include "../../icode/relay/relay.h"
# include "../inc/retarget.h"//用于printf函数串口重映射
# include "../../icode/usart/usart.h"
# include "../../icode/adc/adc.h"
# include "../../icode/rtc/rtc.h"
# include "../../icode/dht11/dht11.h"
# include "../../icode/w25q128/w25qxx.h"
# include "../../USB_DEVICE/APP/usbd_cdc_if.h"
int main(void)
{
uint16_t a=0;
RetargetInit(&huart1);//将printf()函数映射到USART1串口上
HAL_UART_Receive_IT(&huart1, (uint8_t *)&USART1_NewData, 1); //开启串口1接收中断
HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_3);//开启定时器PWM输出
while (1)
{
__HAL_TIM_SetCompare(&htim3,TIM_CHANNEL_3,a);//设置占空比
a++;//占空比+1
if(a>=499)//占空比达到最大清0
{
a=0;
}
HAL_Delay(10);
}
}
RS485总线驱动程序
添加 rs485.h,rs485.c 文件,用到了 usart.h,usart.c
- 其中PA2和PA3端口是 USART2 功能的专用引脚
- 芯片左侧是RS485接口部分,接口有两个端子A和B,可以用两根电线连接到另一个RS485设备,由于RS485采用差分电平,所以两个设备不需要共地,即使电压差不同,电源电压不同也能正常通信
- 设置(确保已经设置好端口,PA2是USART2_TX PA3是USART2_RX)
rs485.h
# ifndef RS485_RS485_H_
# define RS485_RS485_H_
# include "stm32f1xx_hal.h" //HAL库文件声明
# include <string.h>//用于字符串处理的库
# include <stdarg.h>
# include <stdlib.h>
# include "stdio.h"
extern UART_HandleTypeDef huart2;//声明USART2的HAL库结构体
void RS485_printf (char *fmt, ...); //RS485发送
# endif /* RS485_RS485_H_ */
rs485.c
# include "rs485.h"
# include "../usart/usart.h"
# include "main.h"
/*
RS485总线通信,使用UART8,这是RS485专用的printf函数
调用方法:RS485_printf("123"); //向UART8发送字符123
*/
void RS485_printf (char *fmt, ...)
{
char buff[USART2_REC_LEN+1]; //用于存放转换后的数据 [长度]
uint16_t i=0;
va_list arg_ptr;
HAL_GPIO_WritePin(RS485_RE_GPIO_Port,RS485_RE_Pin, GPIO_PIN_SET);//RS485收发选择线RE为高电平(发送)
va_start(arg_ptr,fmt);
vsnprintf(buff, USART2_REC_LEN+1,fmt,arg_ptr);//数据转换
i=strlen(buff);//得出数据长度
if(strlen(buff)>USART2_REC_LEN)i=USART2_REC_LEN;//如果长度大于最大值,则长度等于最大值(多出部分忽略)
HAL_UART_Transmit(&huart2,(uint8_t *)buff,i,0xffff);//串口发送函数(串口号,内容,数量,溢出时间)
va_end(arg_ptr);
HAL_GPIO_WritePin(RS485_RE_GPIO_Port,RS485_RE_Pin, GPIO_PIN_RESET);//RS485收发选择线RE为低电平(接收)
}
//所有USART串口的中断回调函数HAL_UART_RxCpltCallback,统一存放在【USART1.C】文件中
- usart.c 文件里这里需要写 1,代表使用RS485模式,其他不需要添加
main.c
# include "../../icode/led/led.h"
# include "../../icode/key/key.h"
# include "../../icode/delay/delay.h"
# include "../../icode/buzzer/buzzer.h"
# include "../../icode/relay/relay.h"
# include "../inc/retarget.h"//用于printf函数串口重映射
# include "../../icode/usart/usart.h"
# include "../../icode/adc/adc.h"
# include "../../icode/rtc/rtc.h"
# include "../../icode/dht11/dht11.h"
# include "../../icode/w25q128/w25qxx.h"
# include "../../USB_DEVICE/APP/usbd_cdc_if.h"
# include "../../icode/rs485/rs485.h"
int main(void)
{
RetargetInit(&huart1);//将printf()函数映射到USART1串口上
HAL_UART_Receive_IT(&huart1, (uint8_t *)&USART1_NewData, 1); //开启串口1接收中断
HAL_UART_Receive_IT(&huart2, (uint8_t *)&USART2_NewData, 1); //开启串口2接收中断
while (1)
{
if(USART2_RX_STA != 0) //串口2判断中断接收标志位(处理从RS485外部设备接收的字符)
{
BUZZER_SOLO1();//提示音1
RS485_printf("%c", USART_RX2_BUF[0]); //串口发送
USART2_RX_STA = 0; //清除标志位
}
if(KEY_1())
{
BUZZER_SOLO2();//提示音2
RS485_printf("A");//向RS485发送字符A
}
if(KEY_2())
{
BUZZER_SOLO2();//提示音2
RS485_printf("B");//向RS485发送字符B
}
}
}
实验现象
首先将100步的RS485程序烧到 洋桃1号开发板,然后再把本节程序烧到 lot开发板,再拿两条杜邦线连接两个单片机,注意连接:A–>A B–>B 然后两个板子连上电源,在lot板上按KEY1时,1号开发板OLED屏幕上RX会显示“A”,并且蜂鸣器会响,KEY2同理;当在1号开发板上按触摸按键A时,OLED屏幕上TX会显示“A”,RX也会显示“A”,这样代表两个板子已经通了
CAN总线驱动程序
本节用到的HAL函数
- HAL_CAN_ConfigFilter //设置接收过滤器
- HAL_CAN_Start //开启CAN通讯
- HAL_CAN_ActivateNotification //使能中断
- HAL_CAN_GetRxMessage //从Rx FIFO 收取一个 CAN 帧
- HAL_CAN_GetTxMailboxesFreeLevel //Return Tx mailboxes free level
- HAL_CAN_AddTxMessage //发送数据
添加 can1.h,can1.c
can1.h
# ifndef CAN_CAN1_H_
# define CAN_CAN1_H_
# include "stm32f1xx_hal.h" //HAL库文件声明
# include <string.h>//用于字符串处理的库
# include <stdarg.h>
# include <stdlib.h>
# include "stdio.h"
extern CAN_HandleTypeDef hcan;//声明的HAL库结构体
CAN_TxHeaderTypeDef TxMeg;//CAN发送设置相关结构体
CAN_RxHeaderTypeDef RxMeg;//CAN接收设置相关结构体
# define CAN1_ID_H 0x0000 //32位基础ID设置(高16位)
# define CAN1_ID_L 0x0000 //32位基础ID设置(低16位)
# define CAN1_MASK_H 0x0000 //32位屏蔽MASK设置(高16位)
# define CAN1_MASK_L 0x0000 //32位屏蔽MASK设置(低16位)
# define CAN1_REC_LEN 200//定义CAN1最大接收字节数
extern uint8_t CAN1_RX_BUF[CAN1_REC_LEN];//接收缓冲,最大CAN1_REC_LEN个字节.末字节为换行符
extern uint16_t CAN1_RX_STA;//接收状态标记
void CAN_User_Init(CAN_HandleTypeDef* hcan );//CAN用户初始化函数
void HAL_CAN_RxFifo1MsgPendingCallback(CAN_HandleTypeDef *hcan);//CAN接收回调函数
uint8_t CAN1_SendNormalData(CAN_HandleTypeDef* hcan,uint16_t ID,uint8_t *pData,uint16_t Len);//CAN发送函数
void CAN1_printf (char *fmt, ...);//CAN总线通信,使用CAN1,这是CAN专用的printf函数
# endif /* CAN_CAN1_H_ */
- CAN_User_Init 函数跟系统生成的初始化函数不一样,自动生成的初始化函数里是对设置的波特率等参数进行初始化,而CAN用户初始化函数是对CAN总线的帧模式,ID,过滤器等高级应用进行设置,这是CubeMX参数里没有的内容(注意:这里参数跟要通信的板子设置是一样,否则会通信失败)
- HAL_CAN_RxFifo1MsgPendingCallback 是接收回调函数,当CAN总线收到数据后,将进入中断回调函数
- CAN1_printf 和 RS485里的RS485_printf 函数原理一样
can1.c
# include "can1.h" //库文件声明
# include "main.h"
CAN_HandleTypeDef hcan;//声明的HAL库结构体
uint8_t CAN1_RX_BUF[CAN1_REC_LEN];//接收缓冲,最大CAN1_REC_LEN个字节.末字节为换行符
uint16_t CAN1_RX_STA;//接收状态标记
void CAN_User_Init(CAN_HandleTypeDef* hcan )//CAN总线用户初始化函数
{
CAN_FilterTypeDef sFilterConfig;
HAL_StatusTypeDef HAL_Status;
TxMeg.IDE = CAN_ID_STD;//扩展帧标识(STD标准帧/EXT扩展帧)
TxMeg.RTR = CAN_RTR_DATA;//远程帧标识(DATA数据帧/REMOTE远程帧)
sFilterConfig.FilterBank = 0;//过滤器0
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;//设为IDLIST列表模式/IDMASK屏蔽模式
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;//过滤器位宽度
sFilterConfig.FilterIdHigh = CAN1_ID_H;//32位基础ID设置(高16位)
sFilterConfig.FilterIdLow = CAN1_ID_L;//32位基础ID设置(低16位)
sFilterConfig.FilterMaskIdHigh = CAN1_MASK_H;//32位屏蔽MASK设置(高16位)
sFilterConfig.FilterMaskIdLow = CAN1_MASK_L;//32位屏蔽MASK设置(低16位)
sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO1;//接收到的报文放入FIFO1位置
sFilterConfig.FilterActivation = ENABLE;//ENABLE激活过滤器,DISABLE禁止过滤器
sFilterConfig.SlaveStartFilterBank = 0;//过滤器组设置(单个CAN总线时无用)
HAL_Status=HAL_CAN_ConfigFilter(hcan,&sFilterConfig);//将以上结构体参数设置到CAN寄存器中
if(HAL_Status!=HAL_OK){//判断开启是否成功
//开启CAN总线失败的处理程序,写在此处
printf("\n\rCAN设置失败!\n\r"); //串口发送
}
HAL_Status=HAL_CAN_Start(hcan); //开启CAN总线功能
if(HAL_Status!=HAL_OK){//判断开启是否成功
//开启CAN总线失败的处理程序,写在此处
printf("\n\rCAN初始化失败!\n\r"); //串口发送
}
//若不使用CAN中断,可删除以下4行
HAL_Status=HAL_CAN_ActivateNotification(hcan,CAN_IT_RX_FIFO1_MSG_PENDING);//开启CAN总线中断
if(HAL_Status!=HAL_OK){
//开启CAN总线挂起中断失败的处理程序,写在此处
printf("\n\rCAN中断初始化失败!\n\r"); //串口发送
}
}
void HAL_CAN_RxFifo1MsgPendingCallback(CAN_HandleTypeDef *hcan) //接收回调函数(函数名不可改)
{
uint8_t Data[8];//接收缓存数组
HAL_StatusTypeDef HAL_RetVal;//判断状态的枚举
HAL_RetVal=HAL_CAN_GetRxMessage(hcan,CAN_RX_FIFO1,&RxMeg,Data);//接收邮箱中的数据
if (HAL_OK==HAL_RetVal){//判断接收是否成功
//接收成功后的数据处理程序,写在此处 (数据在Data数组中)
//以下2行是采用简单的寄存器查寻方式处理接收数据,每次只接收1位 在实际项目中的复杂接收程序可自行编写
CAN1_RX_BUF[0]=Data[0];//将接收到的数据放入缓存数组(因只用到1个数据,所以只存放在数据[0]位置)
CAN1_RX_STA++;//数据接收标志位加1
}
}
//CAN发送数据函数(参数:总线名,ID,数据数组,数量 返回值:0成功HAL_OK,1参数错误HAL_ERROR,2发送失败HAL_BUSY)
//示例:CAN1_SendNormalData(&hcan1,0,CAN_buffer,8);//CAN发送数据函数
uint8_t CAN1_SendNormalData(CAN_HandleTypeDef* hcan,uint16_t ID,uint8_t *pData,uint16_t Len)
{
HAL_StatusTypeDef HAL_RetVal;//判断状态的枚举
uint16_t SendTimes,SendCNT=0;
uint8_t FreeTxNum=0;
uint32_t CAN_TX_BOX0;
TxMeg.StdId=ID;
if(!hcan||!pData||!Len){
printf("\n\rCAN发送失败!\n\r"); //串口发送
return HAL_ERROR;//如果总线名、数据、数量任何一个为0则返回值为1
}
SendTimes=Len/8+(Len%8?1:0);
FreeTxNum=HAL_CAN_GetTxMailboxesFreeLevel(hcan);//得出空闲邮箱的数量
TxMeg.DLC=8;
while(SendTimes--){//循环判断分批发送是否结束
if(0==SendTimes){//如果分批发送结束
if(Len%8)TxMeg.DLC=Len%8;//则加入最后不足8个的数据内容
}
while(0 == FreeTxNum){
FreeTxNum = HAL_CAN_GetTxMailboxesFreeLevel(hcan);
}
// HAL_Delay(1);//延时防止速度过快导致的发送失败
//开始发送数据(参数:总线名,设置参数,数据,邮箱号)
HAL_RetVal=HAL_CAN_AddTxMessage(hcan,&TxMeg,pData+SendCNT,&CAN_TX_BOX0);
if(HAL_RetVal!=HAL_OK){
printf("\n\rCAN总线忙碌!\n\r"); //串口发送
return HAL_BUSY;//如果发送失败,则返回值为2
}
SendCNT+=8;
}
return HAL_OK;//如果发送成功结束,返回值为0
}
//CAN总线通信,使用CAN1,这是CAN专用的printf函数
//调用方法:CAN1_printf("123"); //向UART8发送字符123
void CAN1_printf (char *fmt, ...)
{
char buff[CAN1_REC_LEN+1]; //用于存放转换后的数据 [长度]
uint16_t i=0;
va_list arg_ptr;
va_start(arg_ptr, fmt);
vsnprintf(buff, CAN1_REC_LEN+1, fmt, arg_ptr);//数据转换
i=strlen(buff);//得出数据长度
if(strlen(buff)>CAN1_REC_LEN)i=CAN1_REC_LEN;//如果长度大于最大值,则长度等于最大值(多出部分忽略)
CAN1_SendNormalData(&hcan,0x12,(uint8_t *)buff,i);//CAN发送数据函数(ID为0x12)
va_end(arg_ptr);
}
main.c
# include "../../icode/led/led.h"
# include "../../icode/key/key.h"
# include "../../icode/delay/delay.h"
# include "../../icode/buzzer/buzzer.h"
# include "../../icode/relay/relay.h"
# include "../inc/retarget.h"//用于printf函数串口重映射
# include "../../icode/usart/usart.h"
# include "../../icode/adc/adc.h"
# include "../../icode/rtc/rtc.h"
# include "../../icode/dht11/dht11.h"
# include "../../icode/w25q128/w25qxx.h"
# include "../../USB_DEVICE/APP/usbd_cdc_if.h"
# include "../../icode/rs485/rs485.h"
# include "../../icode/can/can1.h"
int main(void)
{
RetargetInit(&huart1);//将printf()函数映射到USART1串口上
HAL_UART_Receive_IT(&huart1, (uint8_t *)&USART1_NewData, 1); //开启串口1接收中断
HAL_UART_Receive_IT(&huart2, (uint8_t *)&USART2_NewData, 1); //开启串口2接收中断
HAL_CAN_MspInit(&hcan);//开启CAN功能(因USB与CAN共用RAM,不能同时使用,USB用完想用CAN可在CAN收发前打开)
CAN_User_Init(&hcan);//CAN1初始化同时开启CAN1功能
while (1)
{
if(CAN1_RX_STA != 0) //CAN判断中断接收标志位(处理从CAN外部设备接收的字符)
{
BUZZER_SOLO1();//提示音1
CAN1_printf("%c", CAN1_RX_BUF[0]); //CAN发送
CAN1_RX_STA = 0; //清除标志位
}
if(KEY_1())
{
BUZZER_SOLO2();//提示音2
CAN1_printf("A");//向CAN1发送字符A
}
if(KEY_2())
{
BUZZER_SOLO2();//提示音2
CAN1_printf("B");//向CAN1发送字符B
}
}
}
实验现象
接线什么的跟RS485流程一样
蓝牙模块驱动程序
添加 bt.h,bt.c,用到了 usart.h,usart.c
JDY-08 蓝牙
JDY-08 透传模块是基于蓝牙 4.0 协议标准
-
蓝牙模块 1脚 连接3.3V,24脚 连接GND,6脚 没有连接指示灯,19脚和20脚 通过P10跳线连接到单片机的USART2串口,22脚 是睡眠唤醒通过P10跳线连接到单片机PA8端口(注意:由于RS485模块跟蓝牙模块共同占用PA2,PA3,PA8,所以两者不能同时连接)
-
设置
-
如果程序需要用到模块睡眠模式,则初始电平设置为高电平,这样在唤醒时才能被程序控制产生低电平脉冲,如果不使用睡眠唤醒功能则初始电平要设置为低电平,即禁用唤醒功能
-
usart.c 需要修改
-
添加代码
if(huart == &huart2) //判断中断来源(RS485/蓝牙模块)
{
if(RS485orBT) //当RS485orBT标志位为1时是RS485模式,为0时是蓝牙模式
{
USART2_RX_BUF[0] = USART2_NewData; //将接收到的数据放入缓存数组(因只用到1个数据,所以只存放在数据[0]位置)
USART2_RX_STA++;//数据接收标志位加1
HAL_UART_Receive_IT(&huart2, (uint8_t *)&USART2_NewData, 1); //再开启接收中断
}
else
{
if((USART2_RX_STA&0x8000)==0)//接收未完成
{
if(USART2_RX_STA&0x4000)//接收到0x0d
{
if(USART2_NewData!=0x0a)
{
USART2_RX_STA=0;//接收错误,重新开始
}
else
{
USART2_RX_STA|=0x8000;//接收完成
}
}
else//还没接收到0x0d
{
if(USART2_NewData==0x0d)
{
USART2_RX_STA|=0x4000;
}
else
{
USART2_RX_BUF[USART2_RX_STA&0x3FFF]=USART2_NewData;//将接收的数据存入数组
USART2_RX_STA++;//数据长度+1
if(USART2_RX_STA>(USART2_REC_LEN-1))
{
USART2_RX_STA=0;//接收数据错误,重新开始接收
}
}
}
HAL_UART_Receive_IT(&huart2,(uint8_t*)&USART2_NewData,1);//再开启接收中断
}
}
}
bt.h
# ifndef BT_BT_H_
# define BT_BT_H_
# include "stm32f1xx_hal.h" //HAL库文件声明
# include <string.h>//用于字符串处理的库
# include <stdarg.h>
# include <stdlib.h>
# include "stdio.h"
extern UART_HandleTypeDef huart2;//声明UART2的HAL库结构体
void BT_printf (char *fmt, ...); //BT蓝牙模块发送
# endif /* BT_BT_H_ */
bt.c
# include "bt.h"
# include "main.h"
# include "../usart/usart.h"
//蓝牙模块通信,使用UART2(与RS485复用),这是BT蓝牙的printf函数
//调用方法:BT_printf("123"); //向UART3发送字符123
void BT_printf (char *fmt, ...)
{
char buff[USART2_REC_LEN+1]; //用于存放转换后的数据 [长度]
uint16_t i=0;
va_list arg_ptr;
va_start(arg_ptr, fmt);
vsnprintf(buff, USART2_REC_LEN+1, fmt, arg_ptr);//数据转换
i=strlen(buff);//得出数据长度
if(strlen(buff)>USART2_REC_LEN)i=USART2_REC_LEN;//如果长度大于最大值,则长度等于最大值(多出部分忽略)
HAL_UART_Transmit(&huart2,(uint8_t *)buff,i,0xffff);//串口发送函数(串口号,内容,数量,溢出时间)
va_end(arg_ptr);
}
//所有USART串口的中断回调函数HAL_UART_RxCpltCallback,统一存放在【USART1.C】文件中
main.c
# include "../../icode/led/led.h"
# include "../../icode/key/key.h"
# include "../../icode/delay/delay.h"
# include "../../icode/buzzer/buzzer.h"
# include "../../icode/relay/relay.h"
# include "../inc/retarget.h"//用于printf函数串口重映射
# include "../../icode/usart/usart.h"
# include "../../icode/adc/adc.h"
# include "../../icode/rtc/rtc.h"
# include "../../icode/dht11/dht11.h"
# include "../../icode/w25q128/w25qxx.h"
# include "../../USB_DEVICE/APP/usbd_cdc_if.h"
# include "../../icode/rs485/rs485.h"
# include "../../icode/can/can1.h"
# include "../../icode/bt/bt.h"
int main(void)
{
RetargetInit(&huart1);//将printf()函数映射到USART1串口上
HAL_UART_Receive_IT(&huart1, (uint8_t *)&USART1_NewData, 1); //开启串口1接收中断
HAL_UART_Receive_IT(&huart2, (uint8_t *)&USART2_NewData, 1); //开启串口2接收中断
while (1)
{
if(USART2_RX_STA != 0) //判断中断接收标志位(蓝牙模块BT,使用USART2)
{
BUZZER_SOLO1();//提示音1
BT_printf("%c", USART2_RX_BUF[0]); //蓝牙发送(仅发送第一个字符)
USART2_RX_STA = 0; //清除标志位
}
if(KEY_1())
{
BUZZER_SOLO2();//提示音2
BT_printf("A");//向蓝牙发送字符A
}
if(KEY_2())
{
BUZZER_SOLO2();//提示音2
BT_printf("B");//向蓝牙发送字符B
}
}
}
- APP透传测试
需要安装手机APP进行蓝牙测试(书签中)
透传,即透明传输,指在通讯中不管传输业务如何,只负责将传输内容由源地址传输到目的地址,而且不对业务数据内容做任何改变
实验现象
蓝牙AT指令
- 具体见数据手册
- 注意:模块没与手机连接时是AT命令模式与手机连接后不能进行AT命令操作
- 修改usart.c 中 HAL_UART_RxCpltCallback函数因为在蓝牙模块回复的数据中,只有结束符0x0A(\n),于是处理程序不能同时判断 \r\n,而只需要判断 \n
- 0x7FFF:0111 1111 1111 1111
- 注意需要把LED设置好
if(huart == &huart2) //判断中断来源(RS485/蓝牙模块)
{
if(RS485orBT) //当RS485orBT标志位为1时是RS485模式,为0时是蓝牙模式
{
USART2_RX_BUF[0] = USART2_NewData; //将接收到的数据放入缓存数组(因只用到1个数据,所以只存放在数据[0]位置)
USART2_RX_STA++;//数据接收标志位加1
HAL_UART_Receive_IT(&huart2, (uint8_t *)&USART2_NewData, 1); //再开启接收中断
}
else
{
if((USART2_RX_STA&0x8000)==0)//接收未完成(将USART2_RX_STA最高位1位规定是接收完成标志位)
{
if(USART2_NewData==0x0A)//如收到0x0A表示接收到结束符(蓝牙模块回复数据以0x0A为结束符)
{
USART2_RX_STA|=0x8000;
}
else//还没接收到0x0A则继续接收数据内容并把数量+1
{
USART2_RX_BUF[USART2_RX_STA&0x7FFF]=USART2_NewData;//将收到的数据放入数组
USART2_RX_STA++;//数据长度计数+1
if(USART2_RX_STA>(USART2_REC_LEN-1))
{
USART2_RX_STA=0;//接收数据错误,重新开始接收
}
}
}
}
HAL_UART_Receive_IT(&huart2,(uint8_t*)&USART2_NewData,1);//再开启接收中断
}
main.c
# include "../../icode/led/led.h"
# include "../../icode/key/key.h"
# include "../../icode/delay/delay.h"
# include "../../icode/buzzer/buzzer.h"
# include "../../icode/relay/relay.h"
# include "../inc/retarget.h"//用于printf函数串口重映射
# include "../../icode/usart/usart.h"
# include "../../icode/adc/adc.h"
# include "../../icode/rtc/rtc.h"
# include "../../icode/dht11/dht11.h"
# include "../../icode/w25q128/w25qxx.h"
# include "../../USB_DEVICE/APP/usbd_cdc_if.h"
# include "../../icode/rs485/rs485.h"
# include "../../icode/can/can1.h"
# include "../../icode/bt/bt.h"
int main(void)
{
RetargetInit(&huart1);//将printf()函数映射到USART1串口上
HAL_UART_Receive_IT(&huart1, (uint8_t *)&USART1_NewData, 1); //开启串口1接收中断
HAL_UART_Receive_IT(&huart2, (uint8_t *)&USART2_NewData, 1); //开启串口2接收中断
LED_1(0);//熄灭
while (1)
{
if(USART2_RX_STA & 0x8000) //判断中断接受标志位(USART2)
{
if((USART2_RX_STA & 0x7FFF) == 3
&& USART2_RX_BUF[0] == '+'
&& USART2_RX_BUF[1] == 'O'
&& USART2_RX_BUF[2] == 'K') //判断接收到“+OK”
{
LED_1(1);//点亮LED1
}
else if((USART2_RX_STA & 0x7FFF) == 4
&& USART2_RX_BUF[0] == '+'
&& USART2_RX_BUF[1] == 'E'
&& USART2_RX_BUF[2] == 'R'
&& USART2_RX_BUF[3] == 'R') //判断接收到“+ERR”
{
LED_2(1);//点亮LED2
}
USART2_RX_STA = 0; //标志位清0
}
if(KEY_1())//按下KEY1
{
BUZZER_SOLO2();//提示音
LED_1(0);
LED_2(0);
BT_printf("AT+NAMEFUCK1314");//向蓝牙模块发送AT指令,修改蓝牙名称
}
if(KEY_2())//按下KEY2
{
BUZZER_SOLO2();//提示音
LED_1(0);
LED_2(0);
BT_printf("AT+ISCEN1");//向蓝牙模块发送AT指令开启密码
}
}
}
实验现象
在没连接时按下KEY1时发送指令,接收到“+OK”则LED1点亮,命令错误则LED2点亮;连接上后LED1 和 LED2 熄灭,手机没连上蓝牙时是AT命令模式,连上后不能执行命令!
APP控制单片机
main.c(在上面例子的基础上修改while里面即可)
while(1)
{
if(USART2_RX_STA&0x8000)//判断中断接受标志位(USART2)
{
if((USART2_RX_STA&0x7FFF)==3
&& USART2_RX_BUF[0]=='+'
&& USART2_RX_BUF[1]=='O'
&& USART2_RX_BUF[2]=='K')//判断接收到“+OK”
{
LED_1(1);//点亮LED1
HAL_Delay(400);
LED_1(0);
}
USART2_RX_STA=0;//标志位清0
}
if((USART2_RX_STA&0x7FFF)==1)//判断接收到1个数据
{
switch(USART2_RX_BUF[0])//判断接收的数据
{
case 0x41:
RELAY_1(1);//继电器吸合
BT_printf("Relay NO");//返回数据内容
break;
case 0x44:
RELAY_1(0);//继电器放开
BT_printf("Relay OFF");
break;
case 0x42:
LED_1(1);//LED1亮
BT_printf("LED1 NO");
break;
case 0x45:
LED_1(0);//LED1灭
BT_printf("LED1 OFF");
break;
case 0x43:
BUZZER_SOLO1();//提示音1
BT_printf("BEEP");
break;
case 0x46:
BT_printf("CPU Reset");
HAL_Delay(1000);
__set_FAULTMASK(1);//关闭所有中断
NVIC_SystemReset();//系统复位
break;
default:
BT_printf("ERROR");
break;
}
}
}
蓝牙模块的扩展应用
- 新建一个控件
- 这里有10个可以添加的控件样式,其中的每一个控件如果链接到发送数据包,则可用于向开发板发送数据指令,如果链接到接收数据包,则可用于显示开发板发来的状态或数值
- 接下来修改蓝牙模块接收处理程序,在上面程序的 usart.c 修改即可
- 逻辑值的差异导致校验码也不同
main.c
- 其中校验码的值需要每次在确定数据内容后重新计算,所以这里先给一个初始值为0xFF
# include "../../icode/led/led.h"
# include "../../icode/key/key.h"
# include "../../icode/delay/delay.h"
# include "../../icode/buzzer/buzzer.h"
# include "../../icode/relay/relay.h"
# include "../inc/retarget.h"//用于printf函数串口重映射
# include "../../icode/usart/usart.h"
# include "../../icode/adc/adc.h"
# include "../../icode/rtc/rtc.h"
# include "../../icode/dht11/dht11.h"
# include "../../icode/w25q128/w25qxx.h"
# include "../../USB_DEVICE/APP/usbd_cdc_if.h"
# include "../../icode/rs485/rs485.h"
# include "../../icode/can/can1.h"
# include "../../icode/bt/bt.h"
int main(void)
{
uint8_t buf[7] = {0xA5, 0x00, 0x01, 0x3B, 0x00, 0xFF, 0x5A}; //创建要发送的数组
RetargetInit(&huart1);//将printf()函数映射到USART1串口上
HAL_UART_Receive_IT(&huart1, (uint8_t *)&USART1_NewData, 1); //开启串口1接收中断
HAL_UART_Receive_IT(&huart2, (uint8_t *)&USART2_NewData, 1); //开启串口2接收中断
LED_1(0);//熄灭
while (1)
{
if(USART2_RX_STA & 0x8000)
{
if((USART2_RX_STA & 0x7FFF) == 6
&& USART2_RX_BUF[0] == 0xA5 //判断第一个数据是不是包头
&& USART2_RX_BUF[5] == //判断第6个校验码是不是前4个数据之和的最低8位
(USART2_RX_BUF[1] + USART2_RX_BUF[2] + USART2_RX_BUF[3] + USART2_RX_BUF[4]) % 0x100)
{
if(USART2_RX_BUF[1] & 0x01) //判断逻辑值中最低位是1则点亮LED
{
LED_1(1);
}
else
{
LED_1(0);
}
}
USART2_RX_STA = 0; //标志位清0
}
if(KEY_1())//发送数据包
{
BUZZER_SOLO2();
buf[1] = 0x01; //可在计算校验码之前按需求修改数据值
buf[5] = (buf[1] + buf[2] + buf[3] + buf[4]) % 0x100; //数据相加得出校验码,取低8位
HAL_UART_Transmit(&huart2, (uint8_t *)buf, 7, 0xffff); //串口发送函数
}
if(KEY_2())//发送数据包
{
BUZZER_SOLO2();
buf[1] = 0x00; //可在计算校验码之前按需求修改数据值
buf[5] = (buf[1] + buf[2] + buf[3] + buf[4]) % 0x100; //数据相加得出校验码,取低8位
HAL_UART_Transmit(&huart2, (uint8_t *)buf, 7, 0xffff); //串口发送函数
}
}
}
APP控制蓝牙模块IO端口
- 在这方法中没有单片机的参与,主要是需要手机APP 通过UUID 列表切换通信命令,实现APP直接控制蓝牙模块的IO端口,蓝牙模块的GPIO电平与单片机的GPIO电平相同
WIFI模块原理与AT指令
介绍
ESP-12F
是安信可科技 开发的Wi-Fi 模块,核心处理器是ESP8266
,支持标准的IEEE802.11 b/g/n 协议
,完整的TCP/IP协议栈
- ESP8266拥有完整的且自成体系的Wi-Fi网络功能,既能够
独立应用
,也可以作为从机
搭载于其他主机MCU运行。ESP8266只需通过SPI/SDIO 接口
或UART接口
即可作为Wi-Fi适配器,应用到基于任何微控制器设计中。 - 主频支持
80 MHz
和160 MHz
,支持RTOS
- 支持
UART/GPIO/ADC/PWM/SPI/I2C接口
,采用SMD-22
封装 - 支持串口
本地升级
和远程固件升级(FOTA)
外观
洋桃板子原理图
WiFi模块与USART1透传
- 首先需要设置端口:
- 删除USART1和USART3相关代码,
GPIO端口电平互传,相当于导线连接
- 在while里面加入这两行代码:
//【GPIO对应关系】USART1:电脑USB转串口(RX:PA10,TX:PA9)。USART3:WIF工模块(RX:PB10,TX:PB11)
//将PA10(电脑串口1的RX)的电平状态发送给PB11(WIFI模块串口3的TX)
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_10,HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_10));
//将PB11(WIFI模块串口3的RX)的电平状态发送给PA9(电脑串口1的TX)
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_9,HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_11));
串口助手调试AT指令
打开文件夹找到 AiThinker Serial Tool V1.2.3
双击打开
界面按默认即可点击 打开串口
接着重启板子,这时候助手会显示一堆乱码然后有一个 reday
表示启动成功 ,接着发送 AT
会显示 OK
固件烧录:https://docs.ai-thinker.com/固件汇总
由于固件版本不同,可能会导致有些AT指令的格式,WIFI回复数据的样式有所差异
WIFI模块的TCP通信
WiFi模块连接路由器
注:由于没有现成的路由器设备,这个实验我拿手机热点充当无线路由器做实验
步骤
- 下载上面的
WIFI模块与USART1串口透传
的程序,使开发板处于运行状态 - 由于WIFI模块是
字符收发
,所以不要勾选择十六进制,要勾选发送新行
,这样WIFI模块才会正常接收指令 - 接下来需要路由器正常工作并且需要支持
2.4G无线频段
,尽量靠近路由器,路由器热点名称要设置为字母和数字
,并设好连接密码
WIFI的两种模式
AP模式:允许其他无线设备接入WIFI模块, WIFI模块作为路由器使用
STATION模式:WIFI模块需要连接到AP设备上, WIFI模块作为终端设备使用
AP+STA模式:两种模式的共存模式,即可以通过互联网控制可实现无缝切换,方便操作
- 打开串口按顺序输入命令:
# 设置模式为Station模式(保存到flash)
AT+CWMODE_DEF=1
# 查询热点
AT+CWLAP
# 通过名称密码连接
AT+CWJAP_DEF="yang5201314","00000000"
# 这时候弹出下面代表连接成功:
# WIFI CONNECTED
# WIFI GOT IP
# OK
目前WIFI模块已经连接到路由器
在1的基础上连接一台电脑
上一步中WIFI模块已经连接到路由器,现在把电脑也连接到这个路由器,WIFI模块与电脑就处于同一局域网内,就可以进行TCP通信了!
步骤
- 打开
TCPUDPDbg
测试工具软件,这次实验电脑端定义为服务器端
,WIFI模块定义为客户端
- 打开软件后点击
创建服务器
(本机端口默认3456即可) -> 点击启动服务器
(在服务器模式下记住服务器的IP地址和端口号) - 接着输入命令:
# 设备单连接
AT+CIPMUX=0
# 建立TCP连接
AT+CIPSTART="TCP","172.20.10.7",8951
# 返回OK表示连接成功
踩坑记录
安照教程那样结果一直是错误,我后面打开本机IP信息那把IP改成当前的就OK了那个软件不行改也改不了不推荐用
推荐下载:串口转网络调试助手
- 接着在串口转网络调试助手上发送数据wifi模块就会收到数据,
8
表示数据量,后面的表示数据内容
,就此实现了电脑服务器端向WIFI模块发送数据
WIFI模块向电脑服务器端发送数据
:
# 发送指令和数据量
AT+CIPSEND=5
# 返回:收到'>'表示可写入数据
# 写入数据后点击发送返回OK表示发送成功
- 如果想断开连接可以输入下面命令
# 断开与电脑的TCP连接,返回 `CLOSED`表示断开成功
AT+CIPCLOSE
# 断开与路由器(热点)连接,返回 `WIFI DISCONNECT`表示断开成功
AT+CWQAP
以WiFi模块为服务器的TCP连接
# 设置模式为AP+STA模式(保存到flash)
AT+CWMODE_DEF=3
# 复位WIFI模块,使模式修改生效
AT+RST
# 连接到热点会弹出:
# WIFI DISCONNECT
# WIFI CONNECTED
# WIFI GOT IP
# 设置AP的配置参数(名称,密码,通道号,允许连接数)
AT+CWSAP_DEF="ESP8266","12345678",11,2
# 设置多连接
AT+CIPMUX=1
# 开启服务器,端口号1001
AT+CIPSERVER=1,1001
# 查询本地IP地址
AT+CIFSR
# 得到AP的IP地址是:192.168.4.1
连接成功后调试助手显示 0,CONNECT
(0表示设备号)
这样就实现了电脑端向WIFI模块发送数据
WIFI模块向电脑端发送数据(方法与之前的一样操作)
WiFi模块与手机APP通信
- 安卓手机安装并且打开
TCP_UDP安卓手机测试APP.apk
- 输入WIFI模块AP的IP地址与端口号,点击连接即可,然后可以发送数据电脑端测试工具会显示数据
WIFI模块的单片机控制应用
wifi.h
# ifndef WIFI_WIFI_H_
# define WIFI_WIFI_H_
# include "stm32f1xx_hal.h" //HAL库文件声明
# include <string.h>//用于字符串处理的库
# include <stdarg.h>
# include <stdlib.h>
# include "stdio.h"
extern UART_HandleTypeDef huart3;//声明UART2的HAL库结构体
void WIFI_printf (char *fmt, ...); //WIFI模块发送
void WIFI_TCP_SEND (char *fmt, ...);//在TCP模式下的发送数据(不处理返回状态的盲发)
# endif /* WIFI_WIFI_H_ */
wifi.c
# include "wifi.h"
# include "../usart/usart.h"
# include "main.h"
//WIFI模块通信,使用UART3,这是专用的printf函数
//调用方法:WIFI_printf("123"); //向USART3发送字符123
void WIFI_printf (char *fmt, ...)
{
char buff[USART3_REC_LEN+1]; //用于存放转换后的数据 [长度]
uint16_t i=0;
va_list arg_ptr;
va_start(arg_ptr, fmt);
vsnprintf(buff, USART3_REC_LEN+1, fmt, arg_ptr);//数据转换
i=strlen(buff);//得出数据长度
if(strlen(buff)>USART3_REC_LEN)i=USART3_REC_LEN;//如果长度大于最大值,则长度等于最大值(多出部分忽略)
HAL_UART_Transmit(&huart3,(uint8_t *)buff,i,0xffff);//串口发送函数(串口号,内容,数量,溢出时间)
va_end(arg_ptr);
}
//WIFI模块在TCP模式下的数据发送:TCP发送的规定是先发AT+CIPSEND=数量,等待返回“>“后再发送数据内容。
//调用方法:WIFI_TCP_SEND("123\r\n"); //TCP方式发送字符123和回车换行
void WIFI_TCP_SEND (char *fmt, ...)
{
char buff[USART3_REC_LEN+1]; //用于存放转换后的数据 [长度]
uint16_t i=0;
va_list arg_ptr;
va_start(arg_ptr, fmt);
vsnprintf(buff, USART3_REC_LEN+1, fmt, arg_ptr);//数据转换
i=strlen(buff);//得出数据长度
if(strlen(buff)>USART3_REC_LEN)i=USART3_REC_LEN;//如果长度大于最大值,则长度等于最大值(多出部分忽略)
WIFI_printf("AT+CIPSEND=%d\r\n",i);//先发送AT指令和数据数量
HAL_Delay(100);//毫秒延时等待WIFI模块返回">",此处没做返回是不是">"的判断。稳定性要求高的项目要另加判断。
HAL_UART_Transmit(&huart3,(uint8_t *)buff,i,0xffff);//发送数据内容(串口号,内容,数量,溢出时间)
va_end(arg_ptr);
}
//所有USART串口的中断回调函数HAL_UART_RxCpltCallback,统一存放在【USART1.C】文件中。
usart.c(添加下面代码)
- 格式是固定的只有数据量和数据内容是不固定的(这里程序固定数据量为
1
) - 思路是:把
0x0A
当做起始码,当接收到0x0A
时再判断数组中的字符是不是+IPD,1:
,如果是则设置对应的标志位完成接收
uint8_t USART3_RX_BUF[USART3_REC_LEN];//接收缓冲,最大USART_REC_LEN个字节.
uint16_t USART3_RX_STA=0;//接收状态标记//bit15:接收完成标志,bit14:接收到0x0d,bit13~0:接收到的有效字节数目
uint8_t USART3_NewData;//当前串口中断接收的1个字节数据的缓存
if(huart == &huart3) //判断中断来源(串口3:WIFI模块)
{
//【原始数据内容】字符:+IPD,1:A 十六进制:0D 0A 2B 49 50 44 2C 31 3A 41(其中1是数量,A是数据内容)
//【数据接收原理】当接收到0x0A(即“回车”中的“\r“)时触发接下来的数据采集程序,
//首先清空USART3_RX_BUF[]寄存器,然后将USART3_RX_STA的16位中最高位第2位置1(01000000 00000000)
//此时开始采集接下来收到的数据,当收到前6个数据是“+IPD,1:”,且第7个数据不等于0时,表示成功收完数据
//然后将接收的第7位的一个字节数据内容放入USART3_RX_STA寄存器低8位,并将16位中最高位置1(10000000 xxxxxxxx)。
//【调用方法】在主函数中用if语句判断(USART_RX_STA&0x8000),为真时表示成功收到数据。
//然后读USART_RX_STA寄存器低14位的内容(USART_RX_STA&0x3FFF),即是数据的内容(1个字节)。
//主函数处理完数据后要将USART_RX_STA清0,才能开启下一次数据接收。
if(USART3_RX_STA&0x4000){//判断开始标志位为1时(16位中高位第2位)进入数据采集处理
USART3_RX_BUF[USART3_RX_STA&0x3FFF]=USART3_NewData;//将
USART3_RX_STA++;
if(USART3_RX_BUF[0]=='+'&& //判断返回字符前几位是不是“+IPD,1:”
USART3_RX_BUF[1]=='I'&&
USART3_RX_BUF[2]=='P'&&
USART3_RX_BUF[3]=='D'&&
USART3_RX_BUF[4]==','&&
USART3_RX_BUF[5]=='1'&&//限定只接收1个数量的数据(可根据实际要求的数量修改)
USART3_RX_BUF[6]==':'&&
USART3_RX_BUF[7]!=0){ //同时判断第1个数据内容是否为0,为0表示还没有收到数据
USART3_RX_STA = USART3_RX_BUF[7]+0x8000;//将数据内容写入寄存器,16位最高位置1表示接收完成
}
}
if(USART3_NewData==0x0A && !(USART3_RX_STA&0x8000))//判断是否收到“回车”中的“\r“(0x0A)
{
USART3_RX_STA=0x4000;//将开始采集标志位置1(16位中最高位第2位)
for(a=0;a<200;a++)//循环200次
{
USART3_RX_BUF[a]=0;//将数据寄存器清0
}
}
HAL_UART_Receive_IT(&huart3,(uint8_t *)&USART3_NewData,1); //再开启串口3接收中断
}
main.c
HAL_UART_Receive_IT(&huart3,(uint8_t*)&USART3_NewData,1);//开启串口3接收中断
USART3_RX_STA=0;//标志位清0,准备下次接收
LED_1(0);//LED1控制 //LED状态复位
LED_2(0);//LED2控制
while (1)
{
//TCP接收数据的处理
//只有串口3接收到开头为“+IPD,“时才被识别为接收到数据,数据内容在USART3_RX_STA&0x3FFF。
if(USART3_RX_STA&0x8000)//判断中断接收标志位(WIFI模块使用USART3)
{
switch (USART3_RX_STA&0x3FFF)//判断接收数据的内容
{
case 'A':
RELAY_1(1);//继电器的控制程序(0继电器放开,1继电器吸合)
WIFI_TCP_SEND("Relay ON:OK!\r\n");//发送AT指令 TCP发送数据内容
break;
case 'B':
RELAY_1(0);//继电器的控制程序(0继电器放开,1继电器吸合)
WIFI_TCP_SEND("Relay OFF:OK!\r\n");//发送AT指令 TCP发送数据内容
break;
case 'C':
BUZZER_SOLO1();//蜂鸣器输出单音的报警音
WIFI_TCP_SEND("Beep:OK!\r\n");//发送AT指令 TCP发送数据内容
break;
case 'D':
LED_1(1);//LED1独立控制函数(0为熄灭,其他值为点亮)
WIFI_TCP_SEND("LED1 ON:OK!\r\n");//发送AT指令 TCP发送数据内容
break;
case 'E':
LED_1(0);//LED1独立控制函数(0为熄灭,其他值为点亮)
WIFI_TCP_SEND("LED1 OFF:OK!\r\n");//发送AT指令 TCP发送数据内容
break;
default:
//冗余语句
break;
}
USART3_RX_STA=0;//标志位清0,准备下次接收
}
if(KEY_1())//按下KEY1判断
{
BUZZER_SOLO2();//提示音
WIFI_printf("AT+CIPSTART=\"TCP\",\"192.168.1.4\",3456\r\n");//发送AT指令 连接TCP服务器(IP和端口号需按实际修改)
HAL_Delay(100);//等待
}
if(KEY_2())//按下KEY2判断
{
BUZZER_SOLO2();//提示音
WIFI_TCP_SEND("www.doyoung.net\r\n");//发送AT指令 TCP模式下的发送数据
HAL_Delay(100);//等待
}
}
先将手机连上跟WIFI模块一样的热点
把代码的IP改成这个重新编译下载到开发板,然后点击应用 TCP Server
,弹出的输入框输入端口号,然后按K1按键建立TCP连接
创建阿里云物联网平台
介绍
云端服务器
:
- 自建/租用服务器(自行在服务器上开发物联网平台)
- 利用现有的免费/付费物联网平台(如阿里云)
应用层通信协议的选择:
HTTP超文本传输协议
:常见于浏览器网页,适用于性能高,内存大的网络设备(如手机,电脑)
MQTT消息队列遥测传输协议
:数据量小,网络稳定性要求低,适用于物联网设备(如智能插座)
FTP文件传输协议,SMTP邮件传送协议,DHCP动态主机配置协议等
,不在物联网通信考虑之列
步骤
- 浏览器打开阿里云,注册登陆后在产品一栏搜索
物联网平台
,进入后点击开通即可
- 进入后开始下面步骤:
点击在线设备数
->设备管理--产品--创建产品
设备名称尽量简单明快,只使用英文和数字
安装MQTT.fx软件
点击支持与服务--文档中心--物联网--阿里云物联网平台--快速入门--使用MQTT.fx接入物联网平台
-> 点击下载即可(安装过程默认即可)
打开需要许可才能进去的可以去官网申请一下免费3个月然后会发邮件到你邮箱,打开那个邮件附带文本,复制里面 所有内容
(注意是所有!!!),然后打开 MQTT.fx软件点击 Paste From Clipboard
它会自动粘贴然后点 ok
就进去了
点击 Extras(额外)--Edit Connection Profiles Shift+Meta+C(编辑连接配置文件)
回到阿里云平台点击 设备--查看--(MQTT连接参数那栏)查看
,然后把参数按顺序复制到配置文件,可以参考我的:
Profile Name
可以任意填
接着回到主页面选择你刚刚创建的配置名称,点击右边的 connect
,这时候右边的点变成绿色了,回到阿里云平台刷新会看到 状态/启用状态
变成 在线,表示连接成功(连接失败可能参数复制错误了)
测试数据收发
下行测试
:服务器发送,电脑端接收
点击 产品--查看--Topic类列表--自定义Topic--复制带有【订阅】权限的地址
,然后去到MQTT主页面进行下面操作:
接着回到阿里云操作:
点击 设备--查看--Topic列表--发布消息
阿里云平台也可以查看发送的消息:点击 监控运维--日志服务(选择你产品--找到消息内容那列点击【查看】即可)
上行测试
:电脑端发送,服务器接收
阿里云步骤:点击 产品--查看--Topic类列表--自定义Topic--复制带有【发布】权限的地址(带error那个一般)
,然后去到MQTT主页面进行下面操作:
接着回到阿里云像之前那样在日志服务那查看最近一条信息:
STM32连接阿里云平台
本节用到的HAL函数
- __HAL_TIM_CLEAR_FLAG
首先把 aliyun文件夹
添加到 icode
里,文件在资源里
esp8266.h
是设置AP热点,云平台IP与端口号
iot.h
是设置云平台三元组信息与订阅参数
按实际修改成你平台的参数即可(下面信息截图备用)
三元组信息是:设备证书
踩坑
如果出现下面错误需要屏蔽 MX_DMA_Init();
,或者看这篇文章解决:STM32 region `FLASH‘ overflowed by xxx bytes 问题解决
MQTT数据举例分析
参考手册 MQTT手册3.1.1
修改mtqq 文件夹的 teansport.c
(MQTT数据收发底层接口函数)
esp8266.h
# ifndef ESP8266_ESP8266_H
# define ESP8266_ESP8266_H
# include "stm32f1xx_hal.h"
# include "string.h"
# include "stdlib.h"
# include "../../usart/usart.h"
# include "../../wifi/wifi.h"
//【网络连接信息】在下方修改设置您的路由器热点和物联网平台IP地址+端口号信息(手动复制正确信息到双引号内)
# define SSID "yang5201314" //无线路由器热点名称【必须按您的实际情况修改】
# define PASS "00000000" //无线路由器热点密码【必须按您的实际情况修改】
# define IOT_DOMAIN_NAME "iot-06z00igtpvr8mvu.mqtt.iothub.aliyuncs.com" //云服务器IP地址【必须按您的实际情况修改】
# define IOT_PORTNUM "1883" //云服务器端口号
uint8_t esp8266_send_cmd(char *cmd, char *ack, uint16_t waittime);
uint8_t* esp8266_check_cmd(uint8_t *str);
uint8_t esp8266_Connect_IOTServer(void); //连接物联网云服务器IP
uint8_t esp8266_Connect_AP(void); //连接AP路由器
uint8_t esp8266_Connect_Server(void); //连接服务器
uint8_t esp8266_quit_trans(void); //判断指令退出
# endif
esp8266.c
# include "esp8266.h"
uint8_t esp8266_send_cmd(char *cmd, char *ack, uint16_t waittime) //ESP8266发送指令(底层函数)
{
uint8_t res = 0;
USART3_RX_STA = 0;
memset(USART3_RX_BUF,0,USART3_REC_LEN); //将串口3的缓存空间清0
WIFI_printf("%s\r\n", cmd); //调用WIFI模块专用的发送函数
if(waittime) //需要等待应答
{
while(--waittime) //等待倒计时
{
HAL_Delay(10);//HAL库延时函数
if(USART3_RX_STA&0x8000) //接收到期待的应答结果
{
if(esp8266_check_cmd((uint8_t *)ack))
{
printf("回复信息:%s\r\n",(uint8_t *)ack);//反馈应答信息
break; //得到有效数据
}
USART3_RX_STA=0; //串口3标志位清0
}
}
if(waittime==0)res=1;
}
return res;
}
uint8_t* esp8266_check_cmd(uint8_t *str) //ESP8266检查指令(底层函数)
{
char *strx=0;
if(USART3_RX_STA&0X8000) //接收到一次数据了
{
USART3_RX_BUF[USART3_RX_STA&0X7FFF] = 0; //添加结束符
printf("%s\r\n",(char*)USART3_RX_BUF);
strx=strstr((const char*)USART3_RX_BUF,(const char*)str);
}
return (uint8_t*)strx;
}
uint8_t esp8266_quit_trans(void) //ESP8266退出判断(底层函数)
{
while((USART3->SR&0X40)==0); //等待发送空
USART3->DR='+';
HAL_Delay(15); //大于串口组帧时间(10ms)
while((USART3->SR&0X40)==0); //等待发送空
USART3->DR='+';
HAL_Delay(15); //大于串口组帧时间(10ms)
while((USART3->SR&0X40)==0); //等待发送空
USART3->DR='+';
HAL_Delay(500); //等待500ms
return esp8266_send_cmd("AT","OK",20); //退出透传判断
}
uint8_t esp8266_Connect_IOTServer(void) //ESP8266连接到物联网平台服务器
{
//状态检测
printf("准备配置模块\r\n");
HAL_Delay(100);
esp8266_send_cmd("AT","OK",50);
printf("准备退出透传模式\n");
if(esp8266_quit_trans())
{
printf("退出透传模式失败,准备重启\r\n");
return 6;
}else printf("退出透传模式成功\r\n");
printf("准备关闭回显\r\n");
if(esp8266_send_cmd("ATE0","OK",50))
{
printf("关闭回显失败准备重启\r\n");
return 1;
}else printf("关闭回显成功\r\n");
printf("查询模块是否在线\r\n");
if(esp8266_send_cmd("AT","OK",50))
{
printf("模块不在线准备重启\r\n");
return 1;
}else printf("设置查询在线成功\r\n");
//设置
printf("准备设置STA模式\r\n");
if(esp8266_send_cmd("AT+CWMODE=1","OK",50))
{
printf("设置STA模式失败准备重启\r\n");
return 1;
}else printf("设置STA模式成功\r\n");
printf("准备重启\r\n");
if(esp8266_send_cmd("AT+RST","OK",50))
{
printf("重启失败,准备重启\r\n");
return 2;
}else printf("重启成功,等待三秒\r\n");
HAL_Delay(3000);//延时等待WIFI模块就绪
printf("准备取消自动连接\r\n");
if(esp8266_send_cmd("AT+CWAUTOCONN=0","OK",50))
{
printf("取消自动连接失败,准备重启\r\n");
return 3;
}else printf("取消自动连接成功\r\n");
printf("准备链接路由器\r\n");
if(esp8266_Connect_AP())
{
printf("连接路由器失败,等待重启\r\n");
return 4;
}else printf("连接路由器成功\r\n");
HAL_Delay(4000);//延时等待WIFI模块就绪
printf("准备开启DHCP\r\n");
if(esp8266_send_cmd("AT+CWDHCP=1,1","OK",100))
{
printf("开启DHCP失败,准备重启\r\n");
return 7;
}else printf("设置DHCP成功\r\n");
//TCP连接
printf("设置为关闭多路连接\r\n");
if(esp8266_send_cmd("AT+CIPMUX=0","OK",100))
{
printf("关闭多路连接失败,准备重启\r\n");
return 7;
}else printf("设置关闭多路连接成功\r\n");
printf("准备链接服务器\r\n");
if(esp8266_Connect_Server())
{
printf("连接服务器失败,等待重启\r\n");
return 8;
}else printf("连接服务器成功\r\n");
printf("准备退出透传模式\n");
if(esp8266_quit_trans())
{
printf("退出透传模式失败,准备重启\r\n");
return 6;
}else printf("退出透传模式成功\r\n");
printf("设置为透传模式\r\n");
if(esp8266_send_cmd("AT+CIPMODE=1","OK",50))
{
printf("设置透传失败,准备重启\r\n");
return 6;
}else printf("设置透传成功\r\n");
//发送数据
printf("设置开启透传模式\r\n");
if(esp8266_send_cmd("AT+CIPSEND","OK",1000))
{
printf("开启透传失败,准备重启\r\n");
return 9;
}else printf("开启透传成功\r\n");
USART3_RX_STA = 0;
memset(USART3_RX_BUF,0,USART3_REC_LEN);
return 0; //一切顺利返回0
}
uint8_t esp8266_Connect_AP() //ESP8266连接AP设备(无线路由器)
{
uint8_t i=10;
char *p = (char*)malloc(50);//分配存储空间的指针
sprintf((char*)p,"AT+CWJAP=\"%s\",\"%s\"",SSID,PASS);//发送连接AT指令
while(esp8266_send_cmd(p,"WIFI GOT IP",1000) && i)//循环判断等待连接AP的结果
{
printf("链接AP失败,尝试重新连接\r\n"); //连接失败的反馈信息
i--;
}
free(p);//释放分配的空间和指针
if(i) return 0;//执行成功返回0
else return 1;//执行失败返回1
}
uint8_t esp8266_Connect_Server() //ESP8266连接到服务器
{
uint8_t i=10;
char *p = (char*)malloc(50);//分配存储空间的指针
sprintf((char*)p,"AT+CIPSTART=\"TCP\",\"%s\",\%s",IOT_DOMAIN_NAME,IOT_PORTNUM);
while(esp8266_send_cmd(p,"CONNECT",1000) && i)
{
printf("链接服务器失败,尝试重新连接\r\n");
i--;
}
free(p);//释放分配的空间和指针
if(i)return 0;//执行成功返回0
else return 1;//执行失败返回1
}
iot.h
# ifndef IOT_IOT_H
# define IOT_IOT_H
# include "stm32f1xx_hal.h"
# include "string.h"
# include "stdlib.h"
# include "../../usart/usart.h"
# include "../mqtt/MQTTPacket.h"
# include "../mqtt/transport.h"
//【三元组信息】在下方修改设置您的物联网云平台提供的三元组信息(手动复制正确信息到双引号内)
# define PRODUCTKEY "hol6NTFOx5m" //产品ID(ProductKey)【必须按您的实际情况修改】
# define PRODUCTKEY_LEN strlen(PRODUCTKEY) //产品ID长度
# define DEVICENAME "ESP8266" //设备名(DeviceName)【必须按您的实际情况修改】
# define DEVICENAME_LEN strlen(DEVICENAME) //设备名长度
# define DEVICESECRE "d9042dc73348641a111b8752d54f17cc" //设备秘钥(DeviceSecret)【必须按您的实际情况修改】
# define DEVICESECRE_LEN strlen(DEVICESECRE) //设备秘钥长度
# define TOPIC_SUBSCRIBE "/hol6NTFOx5m/ESP8266/user/get" //订阅权限的地址【必须按您的实际情况修改】
# define TOPIC_QOS 0 //QoS服务质量数值(0/1)
# define MSGID 1 //信息识别ID
# define TOPIC_PUBLISH "/hol6NTFOx5m/ESP8266/user/update/error" //发布权限的地址【必须按您的实际情况修改】
# define MQTTVERSION 4 //MQTT协议版本号(3表示V3.1,4表示V3.1.1)
# define KEEPALIVEINTERVAL 120 //保活计时器,服务器收到客户端消息(含心跳包)的最大间隔(单位是秒)
extern uint16_t buflen; //临时缓存数量
extern unsigned char buf[200]; //临时缓存数组
uint8_t IOT_connect(void); //IOT物联网平台连接
void IOT_ping(void); //IOT物联网平台PING(心跳包)
uint8_t IOT_subscribe(void);//subscribe主题订阅(订阅成功后才能接收订阅消息)
uint8_t IOT_publish(char* payload);//publish主题发布(参数是发布信息内容,用双引号包含)
# endif
iot.c
# include "iot.h"
# include "../hmac/utils_hmac.h"
uint16_t buflen=200;
unsigned char buf[200];
char ClientID[128];
uint8_t ClientID_len;
char Username[128];
uint8_t Username_len;
char Password[128];
uint8_t Password_len;
uint8_t IOT_connect()
{
uint16_t a;
uint32_t len;
char temp[128];
printf("开始连接云端服务器\r\n");
MQTTPacket_connectData data = MQTTPacket_connectData_initializer;//配置部分可变头部的值
buflen = sizeof(buf);
memset(buf,0,buflen);
memset(ClientID,0,128);//客户端ID的缓冲区全部清零
sprintf(ClientID,"%s|securemode=3,signmethod=hmacsha1|",DEVICENAME);//构建客户端ID,并存入缓冲区
memset(Username,0,128);//用户名的缓冲区全部清零
sprintf(Username,"%s&%s",DEVICENAME,PRODUCTKEY);//构建用户名,并存入缓冲区
Username_len = strlen(Username);
memset(temp,0,128);//临时缓冲区全部清零
sprintf(temp,"clientId%sdeviceName%sproductKey%s",DEVICENAME,DEVICENAME,PRODUCTKEY);//构建加密时的明文
utils_hmac_sha1(temp,strlen(temp),Password,DEVICESECRE,DEVICESECRE_LEN);//以DeviceSecret为秘钥对temp中的明文,进行hmacsha1加密,结果就是密码,并保存到缓冲区中
Password_len = strlen(Password);//计算用户名的长度
printf("ClientId:%s\r\n",ClientID);
printf("Username:%s\r\n",Username);
printf("Password:%s\r\n",Password);
//【重要参数设置】可修改版本号、保活时间
data.MQTTVersion = MQTTVERSION; //MQTT协议版本号
data.clientID.cstring = ClientID; //客户端标识,用于区分每个客户端xxx为自定义,后面为固定格式
data.keepAliveInterval = KEEPALIVEINTERVAL; //保活计时器,定义了服务器收到客户端消息的最大时间间隔,单位是秒
data.cleansession = 1; //该标志置1服务器必须丢弃之前保持的客户端的信息,将该连接视为“不存在”
data.username.cstring = Username; //用户名 DeviceName&ProductKey
data.password.cstring = Password; //密码,工具生成
len = MQTTSerialize_connect(buf, buflen, &data);//构造连接的报文
transport_sendPacketBuffer(0,buf, len);//发送连接请求
unsigned char sessionPresent, connack_rc;
a=0;
while(MQTTPacket_read(buf, buflen, transport_getdata) != CONNACK || a>1000)//等待胳回复
{
HAL_Delay(10);//必要的延时等待
a++;//超时计数加1
}
if(a>1000)NVIC_SystemReset();//当计数超时,则复位单片机
while(MQTTDeserialize_connack(&sessionPresent, &connack_rc, buf, buflen) != 1 || connack_rc != 0);
if(connack_rc != 0)
{
printf("连接回复:%uc\r\n",connack_rc);
}
printf("连接成功!\r\n");
return 0;//执行成功返回0
}
void IOT_ping(void)//发送心跳包PING(保持与云服务器的连接)
{
uint32_t len;
len = MQTTSerialize_pingreq(buf, buflen); //计算数据长度
transport_sendPacketBuffer(0, buf, len); //发送数据
HAL_Delay(200);//必要的延时等待
printf("发送心跳包Ping... ");
}
uint8_t IOT_subscribe(void)//subscribe主题订阅(订阅成功后才能接收订阅消息)
{
uint32_t len;
int req_qos = TOPIC_QOS;
MQTTString topicString = MQTTString_initializer;//定义Topic结构体并初始化
topicString.cstring = TOPIC_SUBSCRIBE;
len = MQTTSerialize_subscribe(buf, buflen, 0, MSGID, 1, &topicString, &req_qos);//订阅发送数据编码
transport_sendPacketBuffer(0, buf, len);
HAL_Delay(100);//必要的延时等待
if(MQTTPacket_read(buf, buflen, transport_getdata) == SUBACK) //等待订阅回复
{
unsigned short submsgid;
int subcount;
int granted_qos;
MQTTDeserialize_suback(&submsgid, 1, &subcount, &granted_qos, buf, buflen);//回复的订阅确认数据解码
if(granted_qos != 0) //qos不为0表示订阅成功
{
printf("订阅成功 GrantedQoS=%d\r\n", granted_qos);
return 0; //订阅成功
}
}
printf("订阅失败\r\n");
return 1; //订阅失败
}
uint8_t IOT_publish(char* payload)//publish主题发布(参数是发布信息内容,用双引号包含)
{
uint32_t len;
MQTTString topicString = MQTTString_initializer;//定义Topic结构体并初始化
topicString.cstring = TOPIC_PUBLISH;
int payloadlen = strlen(payload);//用函数计算发布信息内容的长度
printf("发布信息:%.*s\r\n", payloadlen, payload);
//将要发送的信息payload通过MQTTSerialize_publish编码后用transport_sendPacketBuffer发送给云服务器
len = MQTTSerialize_publish(buf, buflen, 0, 0, 0, 0, topicString,
(unsigned char*)payload, payloadlen);//发布数据编码
transport_sendPacketBuffer(0, buf, len); //发送编码好的最终数据
HAL_Delay(100);//必要的延时等待
return 1;
}
usart.c
- 这里关键是加入定时器的清0和开启计时,它的作用是
判断每组数据之间的间隔
if(huart ==&huart3)//判断中断来源(串口3:WIFI模块)//接收完的一批数据,还没有被处理,则不再接收其他数据
{
if(USART3_RX_STA<USART3_REC_LEN)//还可以接收数据
{
__HAL_TIM_SET_COUNTER(&htim2,0); //计数器清空
if(USART3_RX_STA==0) //使能定时器2的中断
{
__HAL_TIM_ENABLE(&htim2); //使能定时器2
}
USART3_RX_BUF[USART3_RX_STA++] = USART3_NewData;//最新接收数据放入数组
}else
{
USART3_RX_STA|=0x8000;//强制标记接收完成
}
HAL_UART_Receive_IT(&huart3,(uint8_t *)&USART3_NewData,1); //再开启串口3接收中断
}
tim.h
# ifndef TIM_TIM_H_
# define TIM_TIM_H_
# include "main.h"
# include "../usart/usart.h"
TIM_HandleTypeDef htim2;
# endif /* TIM_TIM_H_ */
tim.c
- 这里赋0表示结束符,注意前面是++,所以到这了直接&就行了
# include "tim.h"
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) //定时器中断回调函数
{
if(htim ==&htim2)//判断是否是定时器2中断(定时器到时表示一组字符串接收结束)
{
USART3_RX_BUF[USART3_RX_STA&0X7FFF]=0;//添加结束符
USART3_RX_STA|=0x8000;//接收标志位最高位置1表示接收完成
__HAL_TIM_CLEAR_FLAG(&htim2,TIM_EVENTSOURCE_UPDATE );//清除TIM2更新中断标志
__HAL_TIM_DISABLE(&htim2);//关闭定时器2
}
}
main.c
//头文件加进来
# include "../../icode/aliyun/esp8266/esp8266.h"
# include "../../icode/aliyun/mqtt/MQTTPacket.h"
# include "../../icode/aliyun/mqtt/transport.h"
# include "../../icode/aliyun/iot/iot.h"
# include "../../icode/tim/tim.h"
int main(void)
{
uint16_t a=0,b=0;
int t,qos,payloadinlen; //为下面即将解析的消息定义所需变量
unsigned char dup,retained;
unsigned short msgid;
unsigned char* payloadin;
MQTTString receiveTopic;
//初始化那把MX_DMA_Init();屏蔽掉否则会报错FLASH溢出啥的
RetargetInit(&huart1);//将printf()函数映射到USART1串口上
HAL_UART_Receive_IT(&huart1,(uint8_t*)&USART1_NewData,1);//开启串口1接收中断
HAL_UART_Receive_IT(&huart3,(uint8_t*)&USART3_NewData,1);//开启串口3接收中断
HAL_TIM_Base_Start_IT(&htim2);//开启定时器中断(必须开启才能进入中断处理回调函数)
// while(esp8266_Connect_AP());//连接AP无线路由器热点(热点参数在esp8266.h。WIFI模块已保存热点时可屏蔽)
while(esp8266_Connect_IOTServer());//AT指令连接TCP连接云服务器(IP和端口参数在esp8266.h文件内修改设置)
while(IOT_connect());//用MQTT协议+三元组信息连接阿里云物联网平台(三元组参数在iot.h文件内修改设置)
printf("订阅云服务器\r\n");
HAL_Delay(100);//等待
IOT_subscribe();//主题订阅(订阅成功后才能接收订阅消息)
a=0xFFF0; //强制发送心跳包的计数溢出,立即重发心跳包
LED_1(0);//LED状态初始化 关
LED_2(0);
while (1)
{
//循环发送心跳包,以保持设备在线
HAL_Delay(10);//主循环的间隔延时(防止刷新过快)
a++;//计算加1
if(a>1000){ //每1000*10毫秒延时发送一次Ping心跳包(保保持与云服务器的连接)
a=0;//计算标志清0
IOT_ping();//发送Ping心跳包
if(MQTTPacket_read(buf, buflen, transport_getdata)==PINGRESP){//判断心跳包是不回复确认
printf("心跳成功\r\n"); //回复0xD0,0x00时表示心跳成功的回复
}else {
printf("心跳失败\r\n");//无回复表示失败
BUZZER_SOLO1();//蜂鸣器输出单音(提示心跳失败)
a=0xFFF0; //强制发送心跳包的计数溢出,立即重发心跳包
b++;//重启计数加1
if(b>20) //如果快速发送心跳包20次后无回复,则复位WIFI模块重新连接
{
while(esp8266_Connect_IOTServer());//AT指令连接TCP连接云服务器(IP和端口参数在esp8266.h文件内修改设置)
while(IOT_connect());//用MQTT协议+三元组信息连接阿里云物联网平台(三元组参数在iot.h文件内修改设置)
a=0;b=0;//计算标志清0
}
}
USART3_RX_STA = 0;//串口3接收标志位清0
}
//接收云端的订阅消息
if(USART3_RX_STA&0x8000) //判断云服务器发布的消息
{
switch (USART3_RX_BUF[0]/16){//判断接收到的报文类型
case PUBLISH:
BUZZER_SOLO1();//蜂鸣器输出单音
t = MQTTDeserialize_publish( //对接收的MQTT原始数据进行解码(返回1表示成功,其他值表示错误)
&dup, //【得出】重发标志位(0首发,1早前报文的重发)
&qos, //【得出】服务质量等级(0最多分发一次,1至少分发一次,2只分发一次)
&retained, //【得出】保留位参数
&msgid, //【得出】消息ID
&receiveTopic, //【得出】订阅主题名
&payloadin, //【得出】消息内容
&payloadinlen, //【得出】消息长度
USART3_RX_BUF, USART3_RX_STA&0x7FFF); //【输入】原始数据缓存(数组+数量)
if(t){//如果数据正确
printf("接收到主题:%.*s ", receiveTopic.lenstring.len, receiveTopic.lenstring.data);//显示接收主题
printf("消息内容:%.*s ", payloadinlen, payloadin);//显示消息内容的字符串
printf("QoS:%d\r\n", qos);//显示接收QoS
USART3_RX_STA = 0;//标志位清0
//数据控制开发板的程序
if(strstr((const char*)payloadin,(const char*)"LED1 ON"))//比对信息内容是不是LED1 ON
{
LED_1(1);
IOT_publish("LED1 ON OK!");//publish主题发布(发送到云平台)
}else if(strstr((const char*)payloadin,(const char*)"LED1 OFF"))//同上
{
LED_1(0);
IOT_publish("LED1 OFF OK!");//publish主题发布(发送到云平台)
}else if(strstr((const char*)payloadin,(const char*)"LED2 ON"))//同上
{
LED_2(1);
IOT_publish("LED2 ON OK!");//publish主题发布(发送到云平台)
}else if(strstr((const char*)payloadin,(const char*)"LED2 OFF"))//同上
{
LED_2(0);
IOT_publish("LED2 OFF OK!");//publish主题发布(发送到云平台)
}else if(strstr((const char*)payloadin,(const char*)"RELAY1 ON"))//同上
{
RELAY_1(1);
IOT_publish("RELAY1 ON OK!");//publish主题发布(发送到云平台)
}else if(strstr((const char*)payloadin,(const char*)"RELAY1 OFF"))//同上
{
RELAY_1(0);
IOT_publish("RELAY1 OFF OK!");//publish主题发布(发送到云平台)
}
}else{
printf("接收订阅消息时出错\r\n");//接收错误时的显示
}
break;
case CONNACK: //连接报文确认
//插入您的处理程序(也可空置)
break;
case SUBACK: //订阅请求报文确认
//插入您的处理程序(也可空置)
break;
case UNSUBACK: //取消订阅报文确认
//插入您的处理程序(也可空置)
break;
default:
//冗余语句
break;
}
USART3_RX_STA = 0;//串口3接收标志位清0
}
//按键操作
if(KEY_1())//按下KEY1判断【连接云服务器并订阅主题】
{
BUZZER_SOLO2();//提示音
// while(esp8266_Connect_AP());//连接AP无线路由器热点(热点参数在esp8266.h。WIFI模块已保存热点时可屏蔽)
while(esp8266_Connect_IOTServer());//连接TCP连接云服务器(IP和端口参数在esp8266.h文件内修改设置)
while(IOT_connect());//用MQTT协议+三元组信息连接阿里云物联网平台(三元组参数在iot.h文件内修改设置)
printf("订阅云服务器\r\n");
IOT_subscribe();//主题订阅(订阅成功后才能接收订阅消息)
HAL_Delay(100);//等待
}
if(KEY_2())//按下KEY2判断【向服务器发布信息】
{
BUZZER_SOLO2();//提示音
IOT_publish("TEST www.doyoung.net");//publish主题发布(参数是发布信息内容,用双引号包含)
HAL_Delay(100);//等待
}
}
}
程序实验
把程序烧进单片机,然后打开超级终端软件,波特率115200、8、无、1、无
,重启开发板观察WIFI模块发来的调试信息(心跳成功表示与云平台连接成功)(请确保WIFI模块曾经连接过热点,这样重启才会自动连接)