意法半导体 STM32F103 标准库典型实例

博主之前文章介绍的STC51系列单片机是一款结构简单、易于学习的嵌入式微控制器,但是由于标准 8051 架构诞生于 70 年代,其硬件架构、资源数量以及编程方式都已显老旧,成本和性能方面也皆落后于其它架构产品,市场占有率逐步遭到侵蚀,目前仅常见于一些教学与发烧友使用的范畴。伴随近几年物联网行业的快速兴起,STM32等基于 ARM Cortex 内核的微控制器,凭借丰富的片上资源与简单易用的标准外设库,逐步成为消费与工业领域中的主流产品。

意法半导体成立于 1987 年,由意大利 SGS 和法国 Thomson 两家半导体企业合并而成,本文所介绍的STM32F103C8T6属于该公司应用极为广泛的型号,其提供的STM32F10x Standard Peripheral Library标准外设库对 STM32 片上资源进行了完善的封装,相对于 ST 公司目前力推的HAL/LL库,标准外设库更加接近于传统的寄存器操作,因而也较为容易向兆易创新GD32等国产微控制器移植。

片上资源概览

STM32F103C8T6基于 ARM 32 位 Cortex™-M3 内核,使用2.0V ~ 3.6V电压供电,工作频率最高可以达到72MHz,内部采用64K128K字节 Flash 程序存储器,以及高达20K字节的 SRAM 数据存储器;内置 CRC 循环冗余校验以及 96 位编码(即 24 位的十六进制数)的芯片唯一序列号(例如:52FF69067871515237582567)。

其主系统主要由 4 个控制单元(DCode 总线D-bus、系统总线S-bus、通用DMA1、通用DMA2)以及 4 个受控单元(内部SRAM、内部FlashFSMC、AHB 到 APB 的桥AHB2APBx)组成,它们通过一个多级的 AHB 总线相互进行连接,如下图所示:

architecture
  • ICode 总线:连接 M3 内核指令总线与 Flash 的指令接口,用于进行指令预取。
  • DCode 总线:连接 M3 内核 DCode 总线与 Flash 的数据接口,用于完成常量加载和调试访问。
  • System 总线:连接 M3 内核系统总线与总线矩阵 Bus Matrix,用于协调内核与 DMA 之间的通信。
  • DMA 总线:连接 DMA 的 AHB 主接口与总线矩阵 Bus Matrix,用于协调 DCode 和 DMA 对 SRAM、Flash 以及其它外设的访问。
  • Bus Matrix 总线矩阵:利用轮换算法管理内核系统总线与 DMA 主总线之间访问的仲裁,由 4 个控制单元(DCode、系统总线、DMA1 和 DMA2 总线)以及 4 个受控单元(FLITF、SRAM、FSMC、AHB2APB 桥)组成。
  • AHB/APB bridges:两个 AHB/APB 桥提供了 AHB 与 2 条 APB 总线之间的同步连接,APB1操作速度低于36MHzAPB2操作速度最高可达72MHz。每次复位以后,除 SRAM 和 FLITF 以外的外设都会被关闭。使用外设之前,必须通过设置寄存器RCC_AHBENR打开该外设的时钟。

AHB(高级高性能总线,Advanced High performance Bus)是一种系统总线,主要用于连接 CPU、DMA、DSP 等高性能模块,由主模块、从模块、基础结构三部分组成,数据传输总是由主模块发起从模块回应。APB(高级外围总线,Advanced Peripheral Bus)是一种外围总线,主要用于 UART 等低带宽外设之间的连接,其唯一的主模块是 APB 桥;两者都遵循 ARM 公司推出的AMBA芯片总线规范。

时钟系统

由于 STM32 外设资源众多,工作的时钟频率各不相同,所以采用了多达 5 个时钟源:片上经过出厂调校的8MHz RC 振荡器系统时钟HSI,以及带校准的40kHz RC 振荡器作为实时时钟LSI,也可以采用外置4 ~ 16MHz晶体振荡器作为系统时钟HSE,以及带校准功能的32kHz RTC 振荡器作为实时时钟LSE;最后还内置了用于对 CPU 时钟进行倍频的的PLL锁相环。

缩写 名称 频率 外部连接 功能 用途 特性
HSE,High-Speed External Clock Signal 外部高速晶体振荡器 4 ~ 16MHz 4 ~ 16MHz晶振 - 系统时钟/RTC 成本高,温漂小
LSE,Low-Speed External Clock Signal 外部低速晶体振荡器 32.768kHz 32.768kHz晶振 带校准 RTC 成本高,温漂小
HSI,High-Speed Internal Clock Signal 内部高速 RC 振荡器 8MHz 出厂调校 系统时钟 成本低,温漂大
LSI,Low-Speed Internal Clock Signal 内部低速 RC 振荡器 40kHz 带校准 RTC 成本低,温漂大
PLL,Phase Locked Loop 锁相环倍频输出 2~16倍,小于72MHz HSI÷2HSEHSE÷2 - RTC 成本低,温漂大

注意HSIHSEPLL属于高速时钟源,LSILSE属于低速时钟源,任何时钟源都可以根据需要,独立进行启动或者关闭,从而优化芯片功耗。

下面是外部高速时钟源HSE的交流时序图,注意图中Tʜsᴇ标识的部分为一个系统时钟周期。

下图当中,当HSI作为PLL时钟的输入时,最高系统时钟频率只能达到64MHz。当使用 USB 功能时,必须同时使用HSEPLL,并且 CPU 的频率必须为48MHz72MHz。当需要的ADC采样时间为1μs的时候,**APB2**必须设置为14MHz28MHz56MHz

clock-tree

GPIO

STM32F103C8T6采用 LQFP48 方式封装,一共拥有 37 个 I/O 引脚,被分为PA(15 个)、PB(15 个)、PC(3 个)、PD(2 个)、PE(0 个)五个组,所有 I/O 接口可以映像到 16 个外部中断,并且大部份端口都可以可以兼容5V信号。每个 I/O 端口可以接受或输出8mA电流,灌电流则可达到20mA,下面是详细的引脚定义图:

LQFP48

每个 GPIO 端口都拥有两个 32 位配置寄存器GPIOx_CRLGPIOx_CRH,两个 32 位数据寄存器GPIOx_IDRGPIOx_ODR,一个 32 位置位/复位寄存器GPIOx_BSRR和一个 16 位复位寄存器GPIOx_BRR和一个 32 位锁定寄存器GPIOx_LCKR

GPIO 端口的每个位都可以通过软件将其配置为输出(推挽输出GPIO_Mode_Out_PP、开漏输出GPIO_Mode_Out_OD)、输入(浮空输入GPIO_Mode_IN_FLOATING、上拉输入GPIO_Mode_IPU、下拉输入GPIO_Mode_IPD、模拟输入GPIO_Mode_AIN)、复用(复用推挽输出GPIO_Mode_AF_PP、复用开漏输出GPIO_Mode_AF_OD)功能。

GPIO-structure

除具有模拟输入功能的引脚之外,所有 GPIO 都拥有大电流通过能力。必要时可以对 GPIO 进行锁定,以避免意外擦写 GPIO 相关的寄存器。位于APB2上的 GPIO 引脚,其脉冲转换速度可达18MHz

定时器

STM32F103C8T6拥有 7 个定时器,其中 1 个用于电机控制的 16 位 PWM 高级控制定时器、3 个 16 位通用定时器、2 个看门狗定时器(包含独立型的和窗口型)、1 个 24 位自减型系统嘀嗒定时器。

interrupt
  1. 高级控制定时器 TIM1TIM1可以被视为分配到 6 个通道的三相 PWM 发生器,具有带死区插入的互补 PWM 输出,还可以用作完整的通用定时器;其四个独立通道可分别用于:输入捕获输出比较、产生边缘或中心对齐模式的PWM单脉冲输出。当配置为 16 位普通定时器时,与TIM2TIM3TIM4具有相同功能;配置为 16 位 PWM 发生器时,具有0 ~ 100%的全调制能力。
  2. 通用定时器 TIM2、TIM3、TIM4:STM32F103C8T6 内置有 3 个可同步运行的标准定时器,每个定时器都拥有一个 16 位自动加载递加/递减计数器、一个 16 位预分频器、4 个独立通道,每个通道都可用于输入捕获输出比较PWM单脉冲输出,它们还可以通过定时器链接功能与高级控制定时器 TIM1 协同工作,从而提供同步或事件链接功能。
  3. 独立看门狗定时器 IWDG:用于发生问题时复位整个系统,或作为一个自由定时器为应用程序提供超时管理;内部基于 12 位递减计数器和 8 位预分频器,并由内置40kHz的 RC 振荡器提供时钟,由于该 RC 振荡器独立于主时钟,因此可以运行在停机和待机模式。可通过程序配置为软件或者硬件启动的看门狗。
  4. 窗口看门狗定时器 WWDG:用于在发生问题时复位整个系统,它由主时钟驱动,具有早期预警中断功能;其内置有 7 位的递减计数器,并且可以设置为自由运行。
  5. 系统嘀嗒定时器 SysTick:仅用于实时操作系统,也可作为一个标准的递减计数器,具有 24 位的递减计数器、自动重加载功能、当计数器为 0 时能产生一个可屏蔽系统中断、可编程时钟源等特性。

通信接口

STM32F103C8T6拥有 2 个 I²C 接口、3 个 USART 接口、2 个 SPI 接口、1 个 CAN 接口、1 个 USB 2.0 全速接口。

interrupt
  • I²C:内置 I²C 总线接口能够工作于多主模式或从模式,支持标准和快速模式;I²C 接口支持 7 位或 10 位寻址,7 位从模式时支持双从地址寻址,并且内置了硬件 CRC 发生器/校验器,支持使用 DMA 操作并支持 SMBus 总线 2.0 版/PMBus 总线。I²C 总线的连接线一般不超过 2 米,并且理论上数据线需要增加2KΩ上拉电阻,所有与STM32F103C8T6连接的设备都需要共同接地。
  • USART:片上的 USART 接口具有硬件 CTS 和 RTS 信号管理、支持 IrDA SIR ENDEC 传输编解码、兼容 ISO7816 的智能卡并提供 LIN 主/从功能;其中,USART1 接口通信速率可达4.5 Mbit/S,而其它 USART 接口可达 2.25 Mbit/S;所有 USART 接口都可以使用 DMA 操作。
  • SPISTM32F103C8T6拥有 2 个 SPI 接口,主、从模式下全双工和半双工通信速率可达18 Mbit/S。3 位预分频器可以产生 8 种主模式频率,并且每帧可配置为 8 位或 16 位,所有 SPI 接口依然可以使用 DMA 操作。
  • CAN:同时兼容 CAN 2.0A 和 2.0B 规范,位速率高达1Mbit/S,可以接收和发送 11 位标识符的标准帧,也可以收发 29 位标识符的扩展帧。
  • USB 2.0:内嵌 1 个全速 USB 控制器(12Mbit/S),具有待机/唤醒功能,其专用48MHz时钟由内部主锁相环PLL直接产生(时钟源必须为HSE晶体振荡器)。

51 架构单片机内置的 UART 是通用异步收发器,没有同步时钟线;而 STM32 中的 USART 是通用同步/异步收发器,带有同步时钟线USART_CK;由于异步模式更加常用,而同步模式使用频率较少,所以二者区别不大。

DMA

DMA(直接内存存取,Direct Memory Access)用来提供在外设与存储器或者存储器与存储器之间的高速数据传输,传输过程无需经过 CPU 进行干预,数据直接通过 DMA 快速进行操作,从而节省大量 CPU 资源。

DMA

STM32F103 拥有 2 个 DMA 控制器共 12 个通道,其中DMA1拥有 7 个通道,DMA2拥有 5 个通道,每个通道都用来管理外部设备对片内存储器的访问请求,此外还有一个仲裁器来协调各个 DMA 请求的优先级。除了管理外部设备(Timer、ADC、SPI、I²C、USART)到储存器之间数据的双向传输,DMA 还能够管理存储器之间的数据传输。

STM32F103 最小系统

STM32F103C8T6的最小系统电路由电源电路复位电路时钟电路程序下载电路4 部份组成,具体请参考下面的电路图:

mcu

电源电路STM32F103C8T6拥有三路逻辑电源VDD_1VDD_2VDD_3和一路模拟电源VDDA(由于当前实验电路对于模拟电压的读取精度无特殊要求,所以模拟电源与逻辑电源可以共用)。此外,VBAT引脚上还连接了一枚3V纽扣电池BT1,用于为内部的 RTC 时钟供电。而后续串接的去耦电容C1C3C3C4则主要用于稳定电源以及滤除杂波。STM32F103C8T6使用的3V电压是通过AMS1117-3V稳压芯片获得,该芯片将计算机 Micro USB 接口的5V供电电压转换为3.3V电压,然后将这个3.3V电压连接至继电器J1的开关控制位。

mcu

注意:上面电路原理图当中的VCC是指C = Circuit,表示接入电路的电压;VDD是指D = Device,表示元件内部的工作电压;VSS是指S = Series,表示公共连接,通常指公共接地端。

复位电路STM32F103C8T6内部已经拥有一个上电复位电路,但是生产环境下为了防止复位引脚悬空,通常还是会连接一组由电容C9电阻R7共同组成的 RC 外部复位电路,而手动外部复位则是通过后续连接的微动开关K1(即实验电路里的复位按键)来完成。

mcu

时钟电路:频率为8MHz的晶振TX1是外部系统时钟,由C5C6两枚电容协助起震;频率为32.768KHz的晶振TX2用于外部RTC实时时钟电路,由C7C8两枚电容协助起震。

程序下载电路:芯片STC15W201S用于切换单片机启动模式,实现 ASP 程序自动下载;STM32F103C8T6使用 USART 串口为单片机下载程序,实验电路中 USB 转 TTL 电平模块的TXDRXD引脚分别连接至单片机的PA10/USART1_RXPA9/USART1_TXGND引脚与单片机的VSS_1VSS_2VSS_3VSSA共同接地。

STM32F103C8T6通过BOOT0BOOT1两个引脚的电平状态组合,选择何时接收串口传送过来的程序以及何时运行这些程序,即设置微控制器的启动方式。当按键K1处于弹起状态时,将会拉低STC15W201SASPK引脚的电平状态,而STC15W201SASPL引脚连接了一枚 LED 状态指示灯并接入 GND,通过STC15W201S单片机控制STM32F103C8T6BOOT0BOOT1引脚电平状态,进而实现程序的自动下载。

启动模式 BOOT1 BOOT0 说明
Flash ISP 任意电平 0 从闪存启动,即从 Flash 开始执行用户程序。
Bootloader 0 1 擦写 Bootloader 接收串口传送的程序
RAM ISP 1 1 从 SRAM 启动,下载速度较快,主要用于调试阶段。

ISP在线系统编程(In-System Programming)是一种无需将程序存储芯片从嵌入式设备上取出就能对其进行编程与程序下载的方法。

Keil MDK-ARM 设置

虽然当前 ST 公司正在力推STM32Cube(HAL & LL)固件库,但是出于使用习惯,这里依然选择了STM32 Standard Peripheral Libraries标准固件库来编写实验代码,新建一个Merkava文件夹,并添加CMSISLibStartupUser四个子文件夹,具体目录结构如下所示:

1
2
3
4
5
6
7
Merkava:
├─CMSIS
├─Lib
│ ├─inc
│ └─src
├─Startup
└─User

STM32F10x_V3.5.0\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x目录下的stm32f10x.hsystem_stm32f10x.csystem_stm32f10x.h三个文件,以及STM32F10x_V3.5.0\Libraries\CMSIS\CM3\CoreSupport目录下的所有文件复制到刚才新建的CMSIS目录下,完成后目录结构如下:

1
2
3
D:\Workspace\merkava\CMSIS (master -> origin)
λ ls
core_cm3.c core_cm3.h stm32f10x.h system_stm32f10x.c system_stm32f10x.h

然后进入STM32F10x_V3.5.0\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\startup\arm目录将所有文件复制到刚才新建的Startup目录:

1
2
3
4
D:\Workspace\merkava\Startup (master -> origin)
λ ls
startup_stm32f10x_cl.s startup_stm32f10x_hd_vl.s startup_stm32f10x_ld_vl.s startup_stm32f10x_md_vl.s
startup_stm32f10x_hd.s startup_stm32f10x_ld.s startup_stm32f10x_md.s startup_stm32f10x_xl.s

接下来再将STM32F10x_V3.5.0\Libraries\STM32F10x_StdPeriph_Driver目录下的srcinc两个子文件夹拷贝到刚才新建的Lib目录:

1
2
3
D:\Workspace\merkava\Lib (master -> origin)
λ ls
inc/ src/

最后将STM32F10x_V3.5.0\Project\STM32F10x_StdPeriph_Template目录下的main.cstm32f10x_conf.hstm32f10x_it.cstm32f10x_it.h四个文件拷贝至刚才新建的User目录:

1
2
3
D:\Workspace\merkava\User (master -> origin)
λ ls
main.c stm32f10x_conf.h stm32f10x_it.c stm32f10x_it.h

完成 STM32 官方标准库文件的拷贝与复制之后,还需要对 Keil uVision 进行相应设置。首先新建一个【New uVision Project】,由于 Keil uVision 5.28.0.0 并未内置 STM32F103C8T6 支持包,因此需要点击菜单栏上的【Pack Installer】功能手动进行安装。当建立好 uVision 项目之后,再点击【Manage Project Items】图标将刚才建立的代码目录添加至 Keil uVision 项目。

manage-project-items.png

CMSIS目录下的 2 个.c文件全部添加至 Keil uVision 项目的CMSIS组,然后将Lib\src目录下的所有文件添加至Lib组,将Startup目录下的startup_stm32f10x_md.s汇编文件添加至Startup组,最后将User下的 2 个.c文件添加至User组,最后保存退出。

接下来进一步对 Keil uVision 进行一些初始化的设置,这里首先设置【Target】选项卡下面的外部晶振频率为8.0 MHz,然后勾选【Output】选项卡下的Create HEX File

options-for-target-1

然后在【C/C++】选项卡下的【Preprocessor Symbols】内的【Define】输入框内填入USE_STDPERIPH_DRIVER,STM32F10X_MD

options-for-target-2

最后点击【Include Paths】左侧的添加按钮,把刚才新建的那些文件夹逐一选入。

options-for-target-3

官方库文件添加完毕之后,还需要新建BasicHardware两个目录来放置开发人员编写的代码,同样按照以上步骤将其添加至 Keil uVision 开发环境当中,至此项目中所有目录的功能说明如下:

  • CMSIS:内核驱动程序;
  • Lib:内部功能基本函数库;
  • Startup:汇编编写的启动程序;
  • User:用户主函数以及其它用户程序;
  • Basic:内部功能驱动函数;
  • Hardware:外部硬件驱动函数。

LED 发光二极管

LED 闪烁

  • delay_us():微秒级延时函数;
  • delay_ms():毫秒级延时函数;
  • delay_s():秒级延时函数;
led

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/** User/main.c */
#include "delay.h"
#include "led.h"
#include "stm32f10x.h"
#include "sys.h"

int main(void) {
RCC_Configuration(); // 时钟设置
LED_Init();

while (1) {
/** 方式 1
typedef enum {
Bit_RESET = 0,
Bit_SET
} BitAction; // 位于 stm32f10x_gpio.h
*/
GPIO_WriteBit(LEDPORT, LED1, (BitAction)(1)); // LED1 输出高电平 1

delay_us(50000); // 延时 1 秒
GPIO_WriteBit(LEDPORT, LED1, (BitAction)(0)); // LED1 输出低电平 0
delay_us(50000); // 延时 1 秒

/** 方式 2 */
GPIO_WriteBit(LEDPORT, LED1, (BitAction)(1 - GPIO_ReadOutputDataBit(LEDPORT, LED1))); // 取反 LED1
delay_ms(500); // 延时 1 秒

/** 方式 3 */
GPIO_SetBits(LEDPORT, LED1); // LED 置为高电平 1
delay_s(1); // 延时 1 秒
GPIO_ResetBits(LEDPORT, LED1); // LED 重置为低电平 0
delay_s(1); // 延时 1 秒

/** 方式 4 */
GPIO_Write(LEDPORT, 0x0001); // 将 32 位状态值写入整组 GPIO
delay_s(2); // 延时 1 秒
GPIO_Write(LEDPORT, 0x0000); // 将 32 位状态值写入整组 GPIO
delay_s(2); // 延时 1 秒
}
}

led.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/** Hardware/LED/led.h */

#ifndef __LED_H
#define __LED_H
#include "sys.h"

/*
// 通过 PXout() 函数直接操作 GPIO 底层寄存器
#define LED1 PBout(0) // 直接操作 PB0,使用方式:LED1 = 0
#define LED2 PBout(1) // 直接操作 PB1,使用方式:LED2 = 1
*/

#define LEDPORT GPIOB // 定义 GPIOB
#define LED1 GPIO_Pin_0 // 定义 GPIO_Pin_0
#define LED2 GPIO_Pin_1 // 定义 GPIO_Pin_1

void LED_Init(void); // 初始化

#endif

led.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/** Hardware/LED/led.c */
#include "led.h"

/* LED 初始化函数 */
void LED_Init(void) {
/*
typedef struct {
uint16_t GPIO_Pin; // GPIO 编号
GPIOSpeed_TypeDef GPIO_Speed; // GPIO 速度
GPIOMode_TypeDef GPIO_Mode; // GPIO 模式
} GPIO_InitTypeDef;
*/
GPIO_InitTypeDef GPIO_InitStructure; // GPIO 初始化参数结构体的声明

/* AHB 外设时钟使能 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC, ENABLE);

/* GPIO 初始化参数结构体 */
GPIO_InitStructure.GPIO_Pin = LED1 | LED2; // 选择 GPIO 编号(0~15 / all)

/*
GPIO_Mode_AIN 模拟输入 GPIO_Mode_IN_FLOATING 浮空输入 GPIO_Mode_IPD 下拉输入
GPIO_Mode_IPU 上拉输入 GPIO_Mode_Out_PP 推挽输出 GPIO_Mode_Out_OD 开漏输出
GPIO_Mode_AF_PP 复用推挽输出 GPIO_Mode_AF_OD 复用开漏输出
*/
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 选择 GPIO 工作模式为推挽输出

GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置 GPIO 接口速度(2MHz / 10MHz / 50MHz)

GPIO_Init(LEDPORT, &GPIO_InitStructure); // 运行 GPIO 初始化库函数,将设置写入寄存器
}

delay.h

1
2
3
4
5
6
7
8
9
10
/** Basic/delay/delay.h */
#ifndef __DELAY_H
#define __DELAY_H
#include "sys.h"

void delay_s(u16 s);
void delay_ms(u16 ms);
void delay_us(u32 us);

#endif

delay.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/** Basic/delay/delay.c
* 基于系统时基定时器SysTick的延时函数,出于性能考量,采用了寄存器操作实现,并未使用库函数。
* */
#include "delay.h"

#define AHB_INPUT 72 // RCC 里设置的 AHB 时钟频率,单位为 MHz

/* 微秒 us 级延时,参数最大值为 233015 */
void delay_us(u32 uS) {
SysTick->LOAD = AHB_INPUT * uS; // 计数器重装初值,当前频率为 72MHz,即 72 次为 1us
SysTick->VAL = 0x00; // 定时器计数器清零
SysTick->CTRL = 0x00000005; // 时钟源 HCLK,打开定时器
while (!(SysTick->CTRL & 0x00010000)); // 等待计数值归 0
SysTick->CTRL = 0x00000004; // 关闭定时器
}

/* 毫秒 ms 级延时,参数最大值为 65535 */
void delay_ms(u16 ms) {
while (ms-- != 0) {
delay_us(1000); // 调用 1000us 延时函数
}
}

/* 秒 s 级延时,参数最大值为 65535 */
void delay_s(u16 s) {
while (s-- != 0) {
delay_ms(1000); // 调用 1000ms 延时函数
}
}

LED 呼吸灯

uX类型变量存放在 SRAM 当中,程序中可以任意进行修改。

  • u8 :8 位无符号变量;
  • u16 :16 位无符号变量;
  • u32 :32 位无符号变量;

vuX类型变量主要用于中断处理函数。

  • vu8 :易变的 8 位无符号变量;
  • vu16 :易变的 16 位无符号变量;
  • vu32 :易变的 32 位无符号变量;

ucX类型变量存放在 Flash 当中,程序里只能读不能写。

  • uc8 :只读的 8 位无符号变量;
  • uc16 :只读的 16 位无符号变量;
  • uc32 :只读的 32 位无符号变量;

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/** User/main.c */

#include "delay.h"
#include "led.h"
#include "stm32f10x.h"
#include "sys.h"

int main(void) {
/* 变量定义 */
u8 MENU;
u16 t, i;

/* 初始化程序 */
RCC_Configuration(); // 时钟设置
LED_Init();
MENU = 0; // 设置初始值
t = 1;

while (1) {
/* 变亮循环 */
if (MENU == 0) {
for (i = 0; i < 10; i++) {
GPIO_WriteBit(LEDPORT, LED1, (BitAction)(1)); // LED1 输出高电平 1
delay_us(t); // 延时
GPIO_WriteBit(LEDPORT, LED1, (BitAction)(0)); // LED1 输出低电平 0
delay_us(501 - t); // 延时
}
t++;
if (t == 500) {
MENU = 1; // 切换至变暗循环
}
}

/* 变暗循环 */
if (MENU == 1) {
for (i = 0; i < 10; i++) {
GPIO_WriteBit(LEDPORT, LED1, (BitAction)(1)); // LED1 输出高电平 1
delay_us(t); // 延时
GPIO_WriteBit(LEDPORT, LED1, (BitAction)(0)); // LED1 输出低电平 0
delay_us(501 - t); // 延时
}
t--;
if (t == 1) {
MENU = 0; // 切换至变亮循环
}
}
}
}

LED 按键

GPIO 通常为高电平状态,当按键K2K3按下时,PA0PA1将分别被下拉为低电平状态。

key

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/** User/main.c */
#include "delay.h"
#include "key.h"
#include "led.h"
#include "stm32f10x.h"
#include "sys.h"

int main(void) {
u8 a;
RCC_Configuration(); // 时钟设置
LED_Init(); // 初始化 LED
KEY_Init(); // 初始化按键

while (1) {
/* 示例 1:无锁存 */
if (GPIO_ReadInputDataBit(KEYPORT, KEY1)) { // 读按键接口的电平
GPIO_ResetBits(LEDPORT, LED1); // LED灯都为低电平(0)
} else {
GPIO_SetBits(LEDPORT, LED1); // LED灯都为高电平(1)
}

/* 示例 2:无锁存 */
GPIO_WriteBit(LEDPORT, LED1, (BitAction)(!GPIO_ReadInputDataBit(KEYPORT, KEY1)));

/* 示例 3:有锁存(较为常用) */
if (!GPIO_ReadInputDataBit(KEYPORT, KEY1)) { // 读取按键对应 GPIO 引脚的电平状态
delay_ms(20); // 调用延时函数消除按键抖动
if (!GPIO_ReadInputDataBit(KEYPORT, KEY1)) { // 读取按键对应 GPIO 引脚的电平状态
GPIO_WriteBit(LEDPORT, LED1, (BitAction)(1 - GPIO_ReadOutputDataBit(LEDPORT, LED1))); // 采用 1 减去状态值的方式使 LED 状态取反
while (!GPIO_ReadInputDataBit(KEYPORT, KEY1))
; // 等待按键松开
}
}

/* 示例 4:有锁存 */
if (!GPIO_ReadInputDataBit(KEYPORT, KEY1)) { // 读取按键对应 GPIO 引脚的电平状态
delay_ms(20); // 调用延时函数消除按键抖动
if (!GPIO_ReadInputDataBit(KEYPORT, KEY1)) { // 读取按键对应 GPIO 引脚的电平状态
a++; // 变量自增 1

if (a > 3) {
a = 0; // 当变量大于 3 时清零
}

GPIO_Write(LEDPORT, a); // 将变量值写入 LED 对应的 GPIO 上的 PB0 和 PB1
while (!GPIO_ReadInputDataBit(KEYPORT, KEY1)); // 等待按键松开
}
}
}
}

key.h

1
2
3
4
5
6
7
8
9
10
11
12
/** Hardware/KEY/key.h */
#ifndef __KEY_H
#define __KEY_H
#include "sys.h"

#define KEYPORT GPIOA // 定义 GPIO 组
#define KEY1 GPIO_Pin_0 // 定义具体的 GPIO 引脚
#define KEY2 GPIO_Pin_1 // 定义具体的 GPIO 引脚

void KEY_Init(void); // 初始化按键

#endif

key.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** Hardware/KEY/key.c */
#include "key.h"

// 微动开关的接口初始化
void KEY_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure; // 定义 GPIO 初始化枚举结构
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 启动相应的总线时钟
GPIO_InitStructure.GPIO_Pin = KEY1 | KEY2; // 定时按键对应的 GPIO 编号,0~15 或者 all
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 选择 GPIO 工作方式为上拉输入,让按键在未按下时上拉至高电平状态

// GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 此处注释该语句,是由于 GPIO 设置为输入状态时,不需要对 GPIO 速度进行设置

GPIO_Init(KEYPORT, &GPIO_InitStructure);
}

Flash 闪存

使用 Flash 存储器保存上面 LED 按键实验当中示例 4 的按键状态,需要向Lib目录下添加stm32f10x_flash.c库文件,操作 Flash 存储器时有以下注意事项:

  • 必须严格遵循先擦后写的操作顺序;
  • Flash 每页拥有 1024 个地址,但整个 Flash 的起始地址为0x08000000
  • Flash 的擦除操作必须以为单位,写入时必须以 16 位宽度为单位,允许跨页进行写入;
  • 进行 Flash 擦写操作时,必须打开内/外部的高速晶振;
  • 由于 Flash 存储器可以擦写 10 万次左右,所以不能进行死循环擦写数据,造成该页的损坏;
  • 擦写 Flash 时需要避开已经使用了的用户程序存储区,否则错误的擦除用户程序导致错误;
  • Flash 每擦除一页(1k大小)需要耗费10ms,操作起来速度较慢,并且不能进行单个字节的擦写。

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/** User/main.c */
#include "delay.h"
#include "flash.h"
#include "key.h"
#include "led.h"
#include "stm32f10x.h"
#include "sys.h"

#define FLASH_START_ADDR 0x0801f000 // 预定义待操作的 Flash 地址

int main(void) {
u16 a; // 控制 LED 状态的 16 位变量 a
RCC_Configuration(); // 时钟设置
LED_Init(); // 初始化 LED
KEY_Init(); // 初始化按键

a = FLASH_R(FLASH_START_ADDR); // 从指定的 Flash 页地址读取数据

GPIO_Write(LEDPORT, a); // 将变量值写入 LED 对应 GPIO 的 PB0 和 PB1

while (1) {
/* 有锁存按键 */
if (!GPIO_ReadInputDataBit(KEYPORT, KEY1)) { // 读取按键对应 GPIO 引脚的电平状态
delay_ms(20); // 调用延时函数消除按键抖动
if (!GPIO_ReadInputDataBit(KEYPORT, KEY1)) { // 读取按键对应 GPIO 引脚的电平状态
a++; // 变量自增 1

if (a > 3) {
a = 0; // 当变量大于 3 时清零
}

GPIO_Write(LEDPORT, a); // 将变量值写入 LED 对应的 GPIO 上的 PB0 和 PB1
FLASH_W(FLASH_START_ADDR, a); // 将控制 LED 状态的变量 a 写入到 Flash 的指定页地址
while (!GPIO_ReadInputDataBit(KEYPORT, KEY1)); // 等待按键松开
}
}
}
}

flash.h

1
2
3
4
5
6
7
8
9
/** Basic/flash/flash.h */
#ifndef __FLASH_H
#define __FLASH_H
#include "sys.h"

void FLASH_W(u32 add, u16 dat);
u16 FLASH_R(u32 add);

#endif

flash.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/** Basic/flash/flash.c  */
#include "flash.h"

/* FLASH 写数据函数,参数 add 是 32 位的 Flash 地址,参数 dat 是待写入的 16 位数据 */
void FLASH_W(u32 add, u16 dat) {
// RCC_HSICmd(ENABLE); // 由于 Flash 操作必须在高速时钟下进行,由于目前已经使用了外部高速时钟,所以就无需再开启内部的高速时钟

FLASH_Unlock(); // Flash 存放了用户下载程序,为了防止误操作,STM32 设置了操作锁定功能,每次操作之前必须先解锁才能进行操作
FLASH_ClearFlag(FLASH_FLAG_BSY | FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); // 解锁后清除标志位
FLASH_ErasePage(add); // 擦除 add 地址所在的 Flash 页,擦除以后才可以进行写入
FLASH_ProgramHalfWord(add, dat); // 向指定页的 addr 地址写入数据 dat
FLASH_ClearFlag(FLASH_FLAG_BSY | FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); // 清除标志位
FLASH_Lock(); // 重新将 Flash 上锁
}

/* FLASH 读数据函数,参数 add 是 32 位的 Flash 地址,返回待读取的 16 位数据 */
u16 FLASH_R(u32 add) {
u16 a;
a = *(u16 *)(add); // 从指定 Flash 页的 addr 地址开始进行读取
return a;
}

注意:如果需要使用 Flash 存储临时数据,需要考虑到当前单片机程序所占用的空间大小,然后在用户程序没有占用的空白区域里,相对靠后的位置放置临时的自定义数据。

Buzzer 无源蜂鸣器

蜂鸣器BP1一端通过限流电阻R1连接到 3V 电源,另外一端通过 PNP 三极管VT1进行控制,该三极管的集电极连接至 GND,基极通过限流电阻R3连接至 STM32 的PB5引脚;当PB5输出高电平时,三极管的集电极C端与发射极E端断开,蜂鸣器处于断开状态;当PB5输出低电平时,三极管的集电极C端与发射极E端导通,蜂鸣器开始上电工作。如果要发出1KHz频率的声音,需要 1 秒需要经过 1000 个周期,即1000us

buzzer

按键控制蜂鸣器

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/** User/main.c */
#include "buzzer.h"
#include "delay.h"
#include "flash.h"
#include "key.h"
#include "led.h"
#include "stm32f10x.h"
#include "sys.h"

#define FLASH_START_ADDR 0x0801f000 // 写入的起始地址

int main(void) {
u16 a;
RCC_Configuration(); // 配置时钟
LED_Init(); // 初始化 LED
KEY_Init(); // 初始化按键

BUZZER_Init(); // 初始化蜂鸣器
BUZZER_BEEP1(); // 蜂鸣器响 1 声,用于上电提示

a = FLASH_R(FLASH_START_ADDR); // 从指定的 Flash 页地址读取数据

GPIO_Write(LEDPORT, a | 0xfffc & GPIO_ReadOutputData(LEDPORT)); // 将变量值写入 LED 对应 GPIOB 的 PB0 和 PB1 上面,并且保持其它 GPIOB 引脚的状态不变

while (1) {
/* 有锁存按键 */
if (!GPIO_ReadInputDataBit(KEYPORT, KEY1)) { // 读取按键对应 GPIO 引脚的电平状态
delay_ms(20); // 调用延时函数消除按键抖动
if (!GPIO_ReadInputDataBit(KEYPORT, KEY1)) { // 读取按键对应 GPIO 引脚的电平状态
a++; // 变量自增 1
if (a > 3) {
a = 0; // 当变量大于 3 时清零
}

GPIO_Write(LEDPORT, a | 0xfffc & GPIO_ReadOutputData(LEDPORT)); // 将变量值写入 LED 对应 GPIOB 的 PB0 和 PB1 上面,并且保持其它 GPIOB 引脚的状态不变
BUZZER_BEEP1(); // 蜂鸣器响一声

FLASH_W(FLASH_START_ADDR, a); // 将控制 LED 状态的变量 a 写入到 Flash 的指定页地址

while (!GPIO_ReadInputDataBit(KEYPORT, KEY1)); // 等待按键松开
}
}
}
}

buzzer.h

1
2
3
4
5
6
7
8
9
10
11
12
/** Hardware/BUZZER/buzzer.h */
#ifndef __BUZZER_H
#define __BUZZER_H
#include "sys.h"

#define BUZZERPORT GPIOB // 预定义 GPIO 组
#define BUZZER GPIO_Pin_5 // 预定义控制蜂鸣器的 GPIO 端口

void BUZZER_Init(void); // 蜂鸣器初始化函数
void BUZZER_BEEP1(void); // 蜂鸣器响一声函数

#endif

buzzer.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/** Hardware/BUZZER/buzzer.c  */
#include "buzzer.h"
#include "delay.h"

/* 蜂鸣器接口初始化函数 */
void BUZZER_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = BUZZER; // 设置 GPIO 引脚号
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 选择 GPIO 工作方式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置 GPIO 工作速度(2MHz / 10MHz / 50MHz)
GPIO_Init(BUZZERPORT, &GPIO_InitStructure);

GPIO_WriteBit(BUZZERPORT, BUZZER, (BitAction)(1)); // 蜂鸣器端口初始化默认输出高电平 1,确保三极管处于断开状态,防止蜂鸣器损坏
}

/* 蜂鸣器响 1 声工作函数 */
void BUZZER_BEEP1(void) {
u16 i;
/* 产生 200 个频率周期,让蜂鸣器鸣响一段时间 */
for (i = 0; i < 200; i++) {
/* 高低电平各持续 500us,一个周期就是 1000us */
GPIO_WriteBit(BUZZERPORT, BUZZER, (BitAction)(0)); // 向蜂鸣器接口输出低电平 0
delay_us(500); // 保持低电平持续 500us
GPIO_WriteBit(BUZZERPORT, BUZZER, (BitAction)(1)); // 向蜂鸣器接口输出高电平 1,高电平放置在最后是为了防止蜂鸣器损坏
delay_us(500); // 保持高电平持续 500us
}
}

播放 MIDI

MIDI(乐器数字接口,Musical Instrument Digital Interface)采用音符的数字控制信号来记录音乐,即 MIDI 音乐传输的并非声音信号本身,而是 MIDI 控制指令, MIDI 信号传输时通常采用异步串行方式, 波特率为31.25 × (1 ± 0.01) KBaud

音符 频率 音符 频率 音符 频率
低音 1 262 Hz 中音 1 523 Hz 高音 1 1046 Hz
低音 1# 277 Hz 中音 1# 554 Hz 高音 1# 1109 Hz
低音 2 294 Hz 中音 2 587 Hz 高音 2 1175 Hz
低音 2# 311 Hz 中音 2# 622 Hz 高音 2# 1245 Hz
低音 3 330 Hz 中音 3 659 Hz 高音 3 1318 Hz
低音 4 349 Hz 中音 4 698 Hz 高音 4 1397 Hz
低音 4# 370 Hz 中音 4# 740 Hz 高音 4# 1480 Hz
低音 5 392 Hz 中音 5 784 Hz 高音 5 1568 Hz
低音 5# 415 Hz 中音 5# 831 Hz 高音 5# 1661 Hz
低音 6 440 Hz 中音 6 880 Hz 高音 6 1760 Hz
低音 6# 466 Hz 中音 6# 932 Hz 高音 6# 1865 Hz
低音 7 494 Hz 中音 7 988 Hz 高音 7 1976 Hz

本实验代码基于前一个蜂鸣器实验的代码,仅仅是添加了 MIDI 播放相关的头文件与函数,并在主函数中对该函数进行了调用。

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/** User/main.c */
#include "buzzer.h"
#include "delay.h"
#include "flash.h"
#include "key.h"
#include "led.h"
#include "stm32f10x.h"
#include "sys.h"

#define FLASH_START_ADDR 0x0801f000 // 写入的起始地址

int main(void) {
u16 a;
RCC_Configuration(); // 配置时钟
LED_Init(); // 初始化 LED
KEY_Init(); // 初始化按键

BUZZER_Init(); // 初始化蜂鸣器
MIDI_PLAY(); // 开始播放 MIDI 音乐

a = FLASH_R(FLASH_START_ADDR); // 从指定的 Flash 页地址读取数据

GPIO_Write(LEDPORT, a | 0xfffc & GPIO_ReadOutputData(LEDPORT)); // 将变量值写入 LED 对应 GPIOB 的 PB0 和 PB1 上面,并且保持其它 GPIOB 引脚的状态不变

while (1) {
/* 有锁存按键 */
if (!GPIO_ReadInputDataBit(KEYPORT, KEY1)) { // 读取按键对应 GPIO 引脚的电平状态
delay_ms(20); // 调用延时函数消除按键抖动
if (!GPIO_ReadInputDataBit(KEYPORT, KEY1)) { // 读取按键对应 GPIO 引脚的电平状态
a++; // 变量自增 1
if (a > 3) {
a = 0; // 当变量大于 3 时清零
}

GPIO_Write(LEDPORT, a | 0xfffc & GPIO_ReadOutputData(LEDPORT)); // 将变量值写入 LED 对应 GPIOB 的 PB0 和 PB1 上面,并且保持其它 GPIOB 引脚的状态不变
BUZZER_BEEP1(); // 蜂鸣器响一声

FLASH_W(FLASH_START_ADDR, a); // 将控制 LED 状态的变量 a 写入到 Flash 的指定页地址

while (!GPIO_ReadInputDataBit(KEYPORT, KEY1)); // 等待按键松开
}
}
}
}

buzzer.h

1
2
3
4
5
6
7
8
9
10
11
12
13
/** Hardware/xx/xx.h */
#ifndef __BUZZER_H
#define __BUZZER_H
#include "sys.h"

#define BUZZERPORT GPIOB // 预定义 GPIO 组
#define BUZZER GPIO_Pin_5 // 预定义控制蜂鸣器的 GPIO 端口

void BUZZER_Init(void); // 蜂鸣器初始化函数
void BUZZER_BEEP1(void); // 蜂鸣器响一声函数
void MIDI_PLAY(void); // 播放 MIDI 音乐

#endif

buzzer.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/** Hardware/xx/xx.c  */
#include "buzzer.h"
#include "delay.h"

/* 蜂鸣器接口初始化函数 */
void BUZZER_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = BUZZER; // 设置 GPIO 引脚号
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 选择 GPIO 工作方式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置 GPIO 工作速度(2MHz / 10MHz / 50MHz)
GPIO_Init(BUZZERPORT, &GPIO_InitStructure);

GPIO_WriteBit(BUZZERPORT, BUZZER, (BitAction)(1)); // 蜂鸣器端口初始化默认输出高电平 1,确保三极管处于断开状态,防止蜂鸣器损坏
}

/* 蜂鸣器响 1 声工作函数 */
void BUZZER_BEEP1(void) {
u16 i;
/* 产生 200 个频率周期,让蜂鸣器鸣响一段时间 */
for (i = 0; i < 200; i++) {

/* 高低电平各持续 500us,一个周期就是 1000us */
GPIO_WriteBit(BUZZERPORT, BUZZER, (BitAction)(0)); // 向蜂鸣器接口输出低电平 0
delay_us(500); // 保持低电平持续 500us
GPIO_WriteBit(BUZZERPORT, BUZZER, (BitAction)(1)); // 向蜂鸣器接口输出高电平 1,高电平放置在最后是为了防止蜂鸣器损坏
delay_us(500); // 保持高电平持续 500us
}
}

/* MIDI 控制信号表,数组奇数位为音调频率,偶数位为时间长度 */
uc16 music1[78] = { 330, 750, 440, 375, 494, 375, 523, 750, 587, 375, 659, 375, 587, 750, 494, 375, 392, 375, 440, 1500, 330, 750, 440, 375, 494, 375, 523, 750, 587, 375, 659, 375, 587, 750, 494, 375, 392, 375, 784, 1500, 659, 750, 698, 375, 784, 375, 880, 750, 784, 375, 698, 375, 659, 750, 587, 750, 659, 750, 523, 375, 494, 375, 440, 750, 440, 375, 494, 375, 523, 750, 523, 750, 494, 750, 392, 750, 440, 3000 };

/* MIDI 音乐播放函数 */
void MIDI_PLAY(void) {
u16 i, e;
for (i = 0; i < 39; i++) {

/* for 循环中的算式决定了 MIDI 控制信号的持续时间长度,算式 1 * 2 和 i * 2 + 1 分别用于获取数组偶、奇数位的数据,频率乘以时间最后除以 1000 将单位转换为毫秒 */
for (e = 0; e < music1[i * 2] * music1[i * 2 + 1] / 1000; e++) {
GPIO_WriteBit(BUZZERPORT, BUZZER, (BitAction)(0)); // 向蜂鸣器接口输出低电平 0
delay_us(500000 / music1[i * 2]); // 确定当前播放的音符,其中 500000 是 50 万微秒即 0.5 秒,除以振动次数可以得到 0.5 秒内要振动的次数,以及对应的延时时间长度
GPIO_WriteBit(BUZZERPORT, BUZZER, (BitAction)(1)); // 向蜂鸣器接口输出高电平 1,高电平放置在最后是为了防止蜂鸣器损坏
delay_us(500000 / music1[i * 2]); // 确定当前播放的音符,原理同上
}
}
}

USART 串行通信

USART 发送

本程序示例代码基于上一步播放 MIDI 音乐的项目构建,只是往Lib文件夹内增加了stm32f10x_usart.c库文件,以及向Basic文件夹添加了 USART 相关的支持代码。

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/** User/main.c */
#include "delay.h"
#include "stm32f10x.h"
#include "sys.h"
#include "usart.h"

int main(void) {
u8 a = 7, b = 8;
RCC_Configuration(); // 时钟配置
USART1_Init(115200); // 使用波特率为参数初始化 USART1

while (1) {

/* 发送方式 1 */
USART_SendData(USART1, 0x55); // 发送单个数值
while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET)
; //检查发送中断标志位

/* 发送方式 2 */
printf("STM32F103 "); // 发送字符串
printf("STM32 %d %d ", a, b); // 发送带有格式化字符的字符串

/* 发送方式 3 */
USART1_printf("STM32 %d %d ", a, b);

delay_ms(1000); // 延时
}
}

usart.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/** Basic/usart/usart.h */
#ifndef __USART_H
#define __USART_H
#include "stdio.h"
#include "sys.h"
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>

#define USART_n USART1 // 定义使用 printf() 函数使用的串口,其它串口可以同时使用 USART_printf() 函数进行操作

#define USART1_REC_LEN 200 // 定义 USART1 最大接收字节数
#define USART2_REC_LEN 200 // 定义 USART2 最大接收字节数
#define USART3_REC_LEN 200 // 定义 USART3 最大接收字节数

/* 禁用掉不需要的串口,可以有效减少编译量 */
#define EN_USART1 1 // 使能(1)或者禁止(0)串口 1
#define EN_USART2 0 // 使能(1)或者禁止(0)串口 2
#define EN_USART3 0 // 使能(1)或者禁止(0)串口 3

extern u8 USART1_RX_BUF[USART1_REC_LEN]; // 设置接收缓冲区最大数据量为 USART_REC_LEN 个字节,最后的字节为换行符
extern u8 USART2_RX_BUF[USART2_REC_LEN]; // 设置接收缓冲区最大数据量为 USART_REC_LEN 个字节,最后的字节为换行符
extern u8 USART3_RX_BUF[USART3_REC_LEN]; // 设置接收缓冲区最大数据量为 USART_REC_LEN 个字节,最后的字节为换行符

extern u16 USART1_RX_STA; // 接收状态标记
extern u16 USART2_RX_STA; // 接收状态标记
extern u16 USART3_RX_STA; // 接收状态标记

/* 函数声明 */
void USART1_Init(u32 bound); // 串口 1 初始化并启动
void USART2_Init(u32 bound); // 串口 2 初始化并启动
void USART3_Init(u32 bound); // 串口 3 初始化并启动
void USART1_printf(char *fmt, ...); // 串口 1 的 printf() 函数
void USART2_printf(char *fmt, ...); // 串口 2 的 printf() 函数
void USART3_printf(char *fmt, ...); // 串口 3 的 printf() 函数

#endif

usart.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
/** Basic/usart/usart.c  */
#include "sys.h"
#include "usart.h"

/** 重写 printf() 函数用于发送 UART 串口数据,可以在 usart.h 文件内修改串口号 */
#if 1
#pragma import(__use_no_semihosting)

/* 标准库所需的支持函数 */
struct __FILE {
int handle;
};
FILE __stdout;

/* 定义 _sys_exit() 函数,避免使用半主机模式 */
_sys_exit(int x) {
x = x;
}

/* 重新定义 fputc() 函数 */
int fputc(int ch, FILE *f) {
while ((USART_n->SR & 0X40) == 0); // 循环发送数据,直至发送完毕
USART_n->DR = (u8)ch;
return ch;
}
#endif


/** ----- USART1 相关的工具函数 ----- */
#if EN_USART1 // USART1 是否使能
u8 USART1_RX_BUF[USART1_REC_LEN]; // 数据接收缓冲区,最大值为 USART_REC_LEN 字节
u16 USART1_RX_STA = 0; // 接收状态标记,bit15 是接收完成标志,bit14 表示接收到 0x0d,bit13~0 表示接收到的有效字节数

/* USART1 的 printf() 函数 */
void USART1_printf(char *fmt, ...) {
char buffer[USART1_REC_LEN + 1]; // 数据的长度
u8 i = 0;
va_list arg_ptr;
va_start(arg_ptr, fmt);
vsnprintf(buffer, USART1_REC_LEN + 1, fmt, arg_ptr);
while ((i < USART1_REC_LEN) && (i < strlen(buffer))) {
USART_SendData(USART1, (u8)buffer[i++]);
while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET)
;
}
va_end(arg_ptr);
}

/* 初始化并启动 USART1 */
void USART1_Init(u32 bound) {
/* GPIO端口设置 */
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); // 使能 USART1(PA9/USART1_TX) 和 GPIOA 时钟

/* USART1 发送引脚 PA9/USART1_TX 配置 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);

/*USART1 接收引脚 PA10/USART1_RX 配置*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);

/* 配置 Usart1 的 NVIC 向量中断控制器 */
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3; // 设置抢占优先级 3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; // 设置子优先级 3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能 IRQ 通道
NVIC_Init(&NVIC_InitStructure); // 始化 NVIC 寄存器

/* 初始化 USART1 设置 */
USART_InitStructure.USART_BaudRate = bound; // 一般设置为 9600;
USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 字长为 8 位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1; // 一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No; // 不使用奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 不使用硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // 设置收发模式
USART_Init(USART1, &USART_InitStructure); // 初始化串口
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 开启 ENABLE 或者关闭 DISABLE 中断
USART_Cmd(USART1, ENABLE); // 串口使能
}

/* USART1 串口中断服务程序,主函数可以通过判断 if(USART1_RX_STA & 0xC000) 是否为 true,然后读取 USART1_RX_BUF[] 数组,如果读取到 0x0d 0x0a 就表示数据接收完成,即超级终端按下回车键;注意主函数完成串口数据处理之后,需要将 USART1_RX_STA 置为零 */
void USART1_IRQHandler(void) {
u8 Res;

/* 接收到的数据必须以 0x0d 0x0a 结尾 */
if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
Res = USART_ReceiveData(USART1); // 读取接收到的数据
printf("%c", Res); // 将接收到的数据发送回电脑
/* 如果接收没有完成 */
if ((USART1_RX_STA & 0x8000) == 0) {
/* 如果接收到 0x0d */
if (USART1_RX_STA & 0x4000) {
if (Res != 0x0a)
USART1_RX_STA = 0; // 接收错误,重新开始
else
USART1_RX_STA |= 0x8000; // 接收完成
}
/* 如果没有接收到 0X0D */
else {
if (Res == 0x0d)
USART1_RX_STA |= 0x4000;
else {
USART1_RX_BUF[USART1_RX_STA & 0X3FFF] = Res; // 将接收到的数据存放至 USART1_RX_BUF[] 数组,其中 USART1_RX_STA & 0x3FFF 是不包括回车的数据长度
USART1_RX_STA++; // 数据长度计数自增 1
if (USART1_RX_STA > (USART1_REC_LEN - 1))
USART1_RX_STA = 0; // 如果接收到的数据错误,那么重新开始接收
}
}
}
}
}
#endif


/** ----- USART2 相关的工具函数 ----- */
#if EN_USART2 // USART2 是否使能
u8 USART2_RX_BUF[USART2_REC_LEN]; // 数据接收缓冲区,最大值为 USART_REC_LEN 字节
u16 USART2_RX_STA = 0; // 接收状态标记,bit15 是接收完成标志,bit14 表示接收到 0x0d,bit13~0 表示接收到的有效字节数

/* USART2 的 printf() 函数 */
void USART2_printf(char *fmt, ...) {
char buffer[USART2_REC_LEN + 1]; // 数据的长度
u8 i = 0;
va_list arg_ptr;
va_start(arg_ptr, fmt);
vsnprintf(buffer, USART2_REC_LEN + 1, fmt, arg_ptr);
while ((i < USART2_REC_LEN) && (i < strlen(buffer))) {
USART_SendData(USART2, (u8)buffer[i++]);
while (USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET)
;
}
va_end(arg_ptr);
}

/* 初始化并启动 USART2 */
void USART2_Init(u32 bound) {
/* GPIO端口设置 */
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 使能 USART2 所在的 GPIOA 时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); // 使能串口 RCC 时钟

/* USART2 发送引脚 PA2/USART2_TX 配置 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);

/*USART2 接收引脚 PA3/USART2_RX 配置*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);

/* 配置 USART2 的 NVIC 向量中断控制器 */
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3; // 设置抢占优先级 3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; // 设置子优先级 3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能 IRQ 通道
NVIC_Init(&NVIC_InitStructure); // 始化 NVIC 寄存器

/* 初始化 USART2 设置 */
USART_InitStructure.USART_BaudRate = bound; // 一般设置为 9600;
USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 字长为 8 位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1; // 一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No; // 不使用奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 不使用硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // 设置收发模式
USART_Init(USART2, &USART_InitStructure); // 初始化串口
USART_ITConfig(USART2, USART_IT_RXNE, ENABLE); // 开启 ENABLE 或者关闭 DISABLE 中断
USART_Cmd(USART2, ENABLE); // 串口使能
}

/* USART2 串口中断服务程序,主函数可以通过判断 if(USART2_RX_STA & 0xC000) 是否为 true,然后读取 USART2_RX_BUF[] 数组,如果读取到 0x0d 0x0a 就表示数据接收完成,即超级终端按下回车键;注意主函数完成串口数据处理之后,需要将 USART2_RX_STA 置为零 */
void USART2_IRQHandler(void) {
u8 Res;

/* 接收到的数据必须以 0x0d 0x0a 结尾 */
if (USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) {
Res = USART_ReceiveData(USART2); // 读取接收到的数据
printf("%c", Res); // 将接收到的数据发送回电脑
/* 如果接收没有完成 */
if ((USART2_RX_STA & 0x8000) == 0) {
/* 如果接收到 0x0d */
if (USART2_RX_STA & 0x4000) {
if (Res != 0x0a)
USART2_RX_STA = 0; // 接收错误,重新开始
else
USART2_RX_STA |= 0x8000; // 接收完成
}
/* 如果没有接收到 0X0D */
else {
if (Res == 0x0d)
USART2_RX_STA |= 0x4000;
else {
USART2_RX_BUF[USART2_RX_STA & 0X3FFF] = Res; // 将接收到的数据存放至 USART2_RX_BUF[] 数组,其中 USART2_RX_STA & 0x3FFF 是不包括回车的数据长度
USART2_RX_STA++; // 数据长度计数自增 1
if (USART2_RX_STA > (USART2_REC_LEN - 1))
USART2_RX_STA = 0; // 如果接收到的数据错误,那么重新开始接收
}
}
}
}
}
#endif


/** ----- USART3 相关的工具函数 ----- */
#if EN_USART3 // USART3 是否使能
u8 USART3_RX_BUF[USART3_REC_LEN]; // 数据接收缓冲区,最大值为 USART_REC_LEN 字节
u16 USART3_RX_STA = 0; // 接收状态标记,bit15 是接收完成标志,bit14 表示接收到 0x0d,bit13~0 表示接收到的有效字节数

/* USART3 的 printf() 函数 */
void USART3_printf(char *fmt, ...) {
char buffer[USART3_REC_LEN + 1]; // 数据的长度
u8 i = 0;
va_list arg_ptr;
va_start(arg_ptr, fmt);
vsnprintf(buffer, USART3_REC_LEN + 1, fmt, arg_ptr);
while ((i < USART3_REC_LEN) && (i < strlen(buffer))) {
USART_SendData(USART3, (u8)buffer[i++]);
while (USART_GetFlagStatus(USART3, USART_FLAG_TC) == RESET);
}
va_end(arg_ptr);
}

/* 初始化并启动 USART3 */
void USART3_Init(u32 bound) {
/* GPIO端口设置 */
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 使能 USART3 所在的 GPIOA 时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE); // 使能串口 RCC 时钟

/* USART3 发送引脚 PA9/USART3_TX 配置 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);

/*USART3 接收引脚 PA10/USART3_RX 配置*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);

/* 配置 USART3 的 NVIC 向量中断控制器 */
NVIC_InitStructure.NVIC_IRQChannel = USART3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3; // 设置抢占优先级 3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; // 设置子优先级 3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能 IRQ 通道
NVIC_Init(&NVIC_InitStructure); // 始化 NVIC 寄存器

/* 初始化 USART3 设置 */
USART_InitStructure.USART_BaudRate = bound; // 一般设置为 9600;
USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 字长为 8 位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1; // 一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No; // 不使用奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 不使用硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // 设置收发模式
USART_Init(USART3, &USART_InitStructure); // 初始化串口
USART_ITConfig(USART3, USART_IT_RXNE, ENABLE); // 开启 ENABLE 或者关闭 DISABLE 中断
USART_Cmd(USART3, ENABLE); // 串口使能
}

/* USART3 串口中断服务程序 */
void USART3_IRQHandler(void) {
u8 Res;

if (USART_GetITStatus(USART3, USART_IT_RXNE) != RESET) {
Res = USART_ReceiveData(USART3); // 读取接收到的数据
if (Res == 'S') { // 判断数据是否为S,即 STOP
USART3_RX_STA = 1; // 如果为 STOP 则标志位置为 1
} else if (Res == 'K') { // 判断数据是否为K,即 OK
USART3_RX_STA = 2; // 如果为 OK 则标志位置为 2
}
}
}
#endif

USART 接收

单片机接收数据主要有中断查询两种方式,本实验工程基于上一步 USART 数据发送的代码。

查询方式

采用查询方式实现 USART 串口数据的接收,这里的查询是指在主循环中不断的检测数据接收标志位,如果标志位为1就表示接收到了串口数据,然后再来对接收到的数据进行处理。

main.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** User/main.c */
#include "delay.h"
#include "stm32f10x.h"
#include "sys.h"
#include "usart.h"

int main(void) {
u8 a;
RCC_Configuration(); // 时钟配置
USART1_Init(115200); // 使用波特率为参数初始化 USART1

while (1) {

/* 以查询方式接收 USART 数据,参数 USART1 是待查询的串口,参数 USART_FLAG_RXNE 表示待查询的串口数据接收寄存器非空标志位 */
if (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) != RESET) {
a = USART_ReceiveData(USART1); // 读取接收到的数据
printf("%c", a); // 返回接收到的数据
}

// delay_ms(1000);
}
}
usart.c
1
2
3
4
5
6
7
8
9
10
/** Basic/usart/usart.c */

#if EN_USART1
/* 初始化并启动 USART1 */
void USART1_Init(u32 bound) {
/* ...... 省略 ...... */
USART_ITConfig(USART1, USART_IT_RXNE, DISABLE); // 关闭串口中断,避免 USART 接收到数据时自动跳转至中断函数
/* ...... 省略 ...... */
}
#endif

中断方式

查询方式实现比较简单,但是多任务处理时需要等待主函数查询标志位,实时性较差。为了解决这个问题,需要使用中断方式进行串口数据的接收。

usart.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/** Basic/usart/usart.c */

#if EN_USART1
/* 初始化并启动 USART1 */
void USART1_Init(u32 bound) {
/* ...... 省略 ...... */
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 开启串口中断,当串口接收到数据时自动跳转到串口中断服务函数 USART1_IRQHandler
/* ...... 省略 ...... */
}

/* USART1 串口中断服务函数 */
void USART1_IRQHandler(void){
u8 a;

/* USART_GetITStatus 是库函数提供的中断标志位判断函数,参数 USART_IT_RXNE 表示接收中断 */
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET){
a =USART_ReceiveData(USART1); // 读取接收到的数据
printf("%c",a); // 将接收到的数据返回
}
}
#endif

USART 控制 LED

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/** User/main.c */
#include "buzzer.h"
#include "delay.h"
#include "key.h"
#include "led.h"
#include "stm32f10x.h"
#include "sys.h"
#include "usart.h"

int main(void) {
u8 a;
RCC_Configuration(); // 配置时钟
LED_Init(); // 初始化 LED
KEY_Init(); // 初始化 按键
BUZZER_Init(); // 初始化 蜂鸣器
USART1_Init(115200); // 初始化 串口

while (1) {
/* 采用查询方式接收,需要关闭 usart.c 当中的串口中断 */
if (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) != RESET) {
a = USART_ReceiveData(USART1); // 读取接收到的数据
switch (a) {
case '0':
GPIO_WriteBit(LEDPORT, LED1, (BitAction)(0)); // 控制 LED
printf("%c:LED1 OFF ", a);
break;
case '1':
GPIO_WriteBit(LEDPORT, LED1, (BitAction)(1)); // 控制 LED
printf("%c:LED1 ON ", a);
break;
case '2':
BUZZER_BEEP1(); // 蜂鸣器响一声
printf("%c:BUZZER ", a); // 将接收到的数据返回给发送方
break;
default:
break;
}
}

/* 按键控制 */
if (!GPIO_ReadInputDataBit(KEYPORT, KEY1)) { // 读按键接口的电平
delay_ms(20); // 延时 20ms 去除抖动
if (!GPIO_ReadInputDataBit(KEYPORT, KEY1)) { // 读取按键状态
while (!GPIO_ReadInputDataBit(KEYPORT, KEY1)); // 等待按键松开
printf("KEY1 "); //
}
}
if (!GPIO_ReadInputDataBit(KEYPORT, KEY2)) { // 读取按键状态
delay_ms(20); // 去除抖动
if (!GPIO_ReadInputDataBit(KEYPORT, KEY2)) { // 去除抖动
while (!GPIO_ReadInputDataBit(KEYPORT, KEY2)); // 等待按键松开
printf("KEY2 ");
}
}

// delay_ms(1000); //延时
}
}

usart.c

1
2
3
4
5
6
7
8
9
10
/** Basic/usart/usart.c */

#if EN_USART1
/* 初始化并启动 USART1 */
void USART1_Init(u32 bound) {
/* ...... 省略 ...... */
USART_ITConfig(USART1, USART_IT_RXNE, DISABLE); // 关闭串口中断,避免 USART 接收到数据时自动跳转至中断函数
/* ...... 省略 ...... */
}
#endif

终端 USART 控制 LED

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/** User/main.c */
#include "buzzer.h"
#include "delay.h"
#include "key.h"
#include "led.h"
#include "stm32f10x.h"
#include "sys.h"
#include "usart.h"

int main(void) {
RCC_Configuration();
LED_Init(); // 初始化 LED
KEY_Init(); // 初始化 按键
BUZZER_Init(); // 初始化 蜂鸣器
USART1_Init(115200); // 初始化 串口
USART1_RX_STA = 0xC000; // 初始值设置为回车键按下的状态,使启动时可以自动显示菜单

while (1) {
/* 如果接收状态标志位为 0xC000,则表示已经接收到数据 */
if (USART1_RX_STA & 0xC000) {
/* 直接按下回车键,将会重新展示功能菜单 */
if ((USART1_RX_STA & 0x3FFF) == 0) {
printf("\033[1;47;33m\r\n"); // 设置颜色
printf(" 1y--开LED1灯 1n--关LED1灯 \r\n");
printf(" 2y--开LED2灯 2n--关LED2灯 \r\n");
printf(" 请输入控制指令,按回车键执行! \033[0m\r\n"); // 将颜色恢复到默认值,并且切换到下一行
}
/* 判断终端输入的是否为 2 个字符,输入的第 1 个字符是否为"1",第 2 个字符是否为"y" */
else if ((USART1_RX_STA & 0x3FFF) == 2 && USART1_RX_BUF[0] == '1' && USART1_RX_BUF[1] == 'y') {
GPIO_SetBits(LEDPORT, LED1); // 点亮 LED1
printf("1y -- LED1灯已经点亮!\r\n");
}
/* 判断终端输入的是否为 2 个字符,输入的第 1 个字符是否为"1",第 2 个字符是否为"n" */
else if ((USART1_RX_STA & 0x3FFF) == 2 && USART1_RX_BUF[0] == '1' && USART1_RX_BUF[1] == 'n') {
GPIO_ResetBits(LEDPORT, LED1); // 熄灭 LED1
printf("1n -- LED1灯已经熄灭!\r\n");
}
/* 判断终端输入的是否为 2 个字符,输入的第 1 个字符是否为"2",第 2 个字符是否为"y" */
else if ((USART1_RX_STA & 0x3FFF) == 2 && USART1_RX_BUF[0] == '2' && USART1_RX_BUF[1] == 'y') {
GPIO_SetBits(LEDPORT, LED2); // 点亮 LED2
printf("2y -- LED2灯已经点亮!\r\n");
}
/* 判断终端输入的是否为 2 个字符,输入的第 1 个字符是否为"2",第 2 个字符是否为"n" */
else if ((USART1_RX_STA & 0x3FFF) == 2 && USART1_RX_BUF[0] == '2' && USART1_RX_BUF[1] == 'n') {
GPIO_ResetBits(LEDPORT, LED2); // 熄灭 LED2
printf("2n -- LED2灯已经熄灭!\r\n");
}
/* 如果以上条件都不成立,就打印一个指令错误提示信息 */
else {
printf("指令错误!\r\n");
}
USART1_RX_STA = 0; // 将接收状态标志位清零
}
}
}

usart.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/** Basic/usart/usart.c */

#if EN_USART1
/* 初始化并启动 USART1 */
void USART1_Init(u32 bound) {
/* ...... 省略 ...... */
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 打开串口中断,当串口接收到数据时自动跳转至串口中断服务函数
/* ...... 省略 ...... */
}

/* USART1 串口中断服务函数 */
void USART1_IRQHandler(void) {
u8 Res;

/* USART_GetITStatus 是库函数提供的中断标志位判断函数,参数 USART_IT_RXNE 表示当前为接收中断 */
if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
Res = USART_ReceiveData(USART1); // 将接收到的数据存放至 Res
printf("%c", Res); // 将接收到的数据 Res 返回

/* 如果接收未完成 */
if ((USART1_RX_STA & 0x8000) == 0) {
/* 如果接收到 0x0d */
if (USART1_RX_STA & 0x4000) {
/* 回车键是由 0x0a/换行与 0x0d/回车两个 ASCII 码组合而成 */
if (Res != 0x0a)
USART1_RX_STA = 0; // 接收数据错误,状态位清零,重新开始接收
else
USART1_RX_STA |= 0x8000; // 状态变量的最高位置 1 ,接收完成
}
/* 如果未接收到 0x0d */
else {
/* 如果按下回车,0x0d 表示 ASCII 码的回车键,*/
if (Res == 0x0d)
USART1_RX_STA |= 0x4000; // 将状态标志位的最高位的第 2 位置 1,该标志位用于标记串口是否接收到回车键
/* 如果按下的并不是回车,那么执行下面程序 */
else {
USART1_RX_BUF[USART1_RX_STA & 0X3FFF] = Res; // 将接收到的数据存放到全局变量数组当中,USART1_RX_STA & 0X3FFF 表示要存放的数组索引位置
USART1_RX_STA++; // 数据长度自增 1
if (USART1_RX_STA > (USART1_REC_LEN - 1))
USART1_RX_STA = 0; // 如果读取的数据超出限定长度,那么状态标志位置 0,重新开始接收
}
}
}
}
}
#endif

RTC 实时时钟

STM32F103 的备份寄存器可以保存20 Byte的用户数据,而且与 RTC 实时时钟一样独立工作,并不会被系统、电源等复位方式复位。RTC 拥有多个时钟源输入,可以使用外部的32.768KHz晶振配合 20 位预分频器产生一个 1 秒的时间基准信号。

STM32 的 RTC 实时时钟使用一个 32 位计数器(可计时 136 年)进行计时,计时的起始基准时间为 Unix 的1970年1月1日 0时0分0秒。如果要读取当前的时间值,可以先读取 32 位的 RTC 计数值,然后以前面的 Unix 时间作为起点,加上计数器中的秒数,再换算为年月日时分秒格式,即可得到当前的实时时间。

本实验继续延用前面的工程项目,但是向Basic文件夹添加了rtc.crtc.h两个文件,并引入了stm32f10x_rtc.c库文件。实验电路中的LED1以秒为单位,当秒数值为奇数时 LED 点亮,为偶数时 LED 熄灭,LED2以分钟为单位,当分钟值为奇数时 LED 点亮,为偶数时 LED 熄灭。

LED 间隔 1 秒/分 闪烁

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/** User/main.c */
#include "buzzer.h"
#include "delay.h"
#include "key.h"
#include "led.h"
#include "rtc.h"
#include "stm32f10x.h"
#include "sys.h"
#include "usart.h"

int main(void) {
RCC_Configuration(); // 初始化系统时钟
RTC_Config(); // 初始化实时时钟

LED_Init(); // 初始化 LED
KEY_Init(); // 初始化 按键
BUZZER_Init(); // 初始化 蜂鸣器
USART1_Init(115200); // 初始化串口,参数为波特率
USART1_RX_STA = 0xC000; // 初始值设置为回车,从而展示选项菜单
while (1) {

/* 读取时间值,并判断返回值是否为 0,非 0 时表示读取错误 */
if (RTC_Get() == 0) {
GPIO_WriteBit(LEDPORT, LED1, (BitAction)(rsec % 2)); // 控制 LED1,将RTC读取的秒钟值除以 2,秒数为奇数时 LED1 输出高电平点亮,为偶数时输出低电平熄灭,从而达到间隔 1 秒亮灭闪烁的效果
GPIO_WriteBit(LEDPORT, LED2, (BitAction)(rmin % 2)); // 控制 LED2,同样的原理,LED2 以分钟为单位进行点亮和熄灭
}
}
}

rtc.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** Basic/rtc/rtc.h */
#ifndef __RTC_H
#define __RTC_H
#include "sys.h"

/* 采用 extern 关键字声明全局变量,用于存放时钟的年、月、日、时、分、秒结果 */
extern u16 ryear;
extern u8 rmon, rday, rhour, rmin, rsec, rweek;


/* 由于 RTC 同时使用了主电源和备用电池两种方式供电,所以这里定义了两个不同的配置函数 */
void RTC_First_Config(void); // 备用电池断开,RTC 内部数据丢失,进行首次上电配置

/* 由用户直接调用的函数,先初始化,然后进行读写 */
void RTC_Config(void); // 备用电池未断开,进行基本初始化,维持基本走时即可
u8 RTC_Get(void); // 读取当前时间,并保存到上面声明的全局变量当中
u8 RTC_Set(u16 syear, u8 smon, u8 sday, u8 hour, u8 min, u8 sec); // 设置时间,将参数所传递的时间转换为32位 RTC 计数器值

u8 Is_Leap_Year(u16 year); // 判断是否为闰年
u8 RTC_Get_Week(u16 year, u8 month, u8 day); // 基于年、月、日来计算星期数

#endif

rtc.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
/** Basic/rtc/rtc.c  */
#include "rtc.h"
#include "sys.h"

/* 存放时钟结果的全局变量 */
u16 ryear; // 4 位的年
u8 rmon, rday, rhour, rmin, rsec, rweek; // 2 位的月、日、时、分、秒、周

/* 首次启用 RTC 时的设置 */
void RTC_First_Config(void) {
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); // 启用 PWR 和 BKP 的时钟
PWR_BackupAccessCmd(ENABLE); // 解锁 RTC 与备用寄存器访问
BKP_DeInit(); // 复位备用寄存器为默认值
RCC_LSEConfig(RCC_LSE_ON); // 启用外部 32.768KHZ 晶振

while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET); // 等待晶振稳定工作
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); // RTC 时钟源配置为 LSE,即 32.768KHz 外部低速晶振
RCC_RTCCLKCmd(ENABLE); // 启动 RTC

RTC_WaitForSynchro(); // 等待 APB1 时钟与 RTC 时钟同步
RTC_WaitForLastTask(); // 读写寄存器之前确认之前操作已经结束

/* 分频值计算公式:RTC period = RTCCLK/RTC_PR = (32.768 KHz)/(32767 + 1) */
RTC_SetPrescaler(32767); // 设置 RTC 分频器,从而自动将 RTC 时钟转换为 1Hz,即每秒计数 1 次,
RTC_WaitForLastTask(); // 等待寄存器写入完成

/* 如果不需要使用 RTC 秒中断,可以屏蔽下面 2 条语句 */
// RTC_ITConfig(RTC_IT_SEC, ENABLE); // 秒中断使能,即每间隔 1 秒触发一次 RTC 中断
// RTC_WaitForLastTask(); // 等待寄存器写入完成
}

/* 初始化 RTC 实时时钟 */
void RTC_Config(void) {
/* 预先向后备寄存器 1 存放了一个特殊字符 0xA5A5,通过读取并判断该字符是否存在,可以得知 RTC 当前是否属于首次上电,如果是则需要重新对其进行配置 */
if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5) {
RTC_First_Config(); // 重新配置RTC
BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); // 配置完成以后,重新向后备寄存器写入特殊字符 0xA5A5
}
/* 如果后备寄存器未掉电,则无需重新配置 RTC */
else {
/* 调用 RCC_GetFlagStatus() 函数判断当前的复位类型 */
if (RCC_GetFlagStatus(RCC_FLAG_PORRST) != RESET) {
// 上电复位后执行的内容
} else if (RCC_GetFlagStatus(RCC_FLAG_PINRST) != RESET) {
// 外部 RST 引脚复位后执行的内容
}

RCC_ClearFlag(); // 清除 RCC 当中的复位标志
RCC_RTCCLKCmd(ENABLE); // 虽然无需进行 RTC 配置,且其掉电后依靠后备电池正常运行,但是每次上电以后依然要重新使能 RTCCLK
RTC_WaitForSynchro(); // 等待 APB1 时钟与 RTC 时钟同步

/* 如果不需要使用 RTC 秒中断,可以屏蔽下面 2 条语句 */
// RTC_ITConfig(RTC_IT_SEC, ENABLE); // 秒中断使能,即每间隔 1 秒触发一次 RTC 中断
// RTC_WaitForLastTask(); // 等待寄存器写入完成
}

/* 是否启动 RTC 的输出功能,通常不会使用 */
#ifdef RTCClockOutput_Enable
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE);
BKP_TamperPinCmd(DISABLE);
BKP_RTCOutputConfig(BKP_RTCOutputSource_CalibClock);
#endif
}

/* RTC 实时时钟 1 秒触发中断函数,需要启用 RTC 秒中断 */
void RTC_IRQHandler(void) {
if (RTC_GetITStatus(RTC_IT_SEC) != RESET) {
// 将进入秒中断以后需要执行的程序放置在此处
}
RTC_ClearITPendingBit(RTC_IT_SEC);
RTC_WaitForLastTask();
}

/* 闹钟中断处理函数,需要启用闹钟中断,并且调高优先级使用 */
void RTCAlarm_IRQHandler(void) {
if (RTC_GetITStatus(RTC_IT_ALR) != RESET) {
// 将进入闹钟中断以后需要执行的程序放置在此处
}
RTC_ClearITPendingBit(RTC_IT_ALR);
RTC_WaitForLastTask();
}

/* 闰年判断函数,参数 year 是输入的年份,返回 1 说明是闰年,返回 0 说明不是 */
u8 Is_Leap_Year(u16 year) {
/* 闰年判断的计算公式 */
if (year % 4 == 0) {
if (year % 100 == 0) {
if (year % 400 == 0)
return 1;
else
return 0;
} else
return 1;
} else
return 0;
}

/* 月份相关的数据表 */
u8 const table_week[12] = {0, 3, 3, 6, 1, 4, 6, 2, 5, 0, 3, 5}; // 月修正数据表
const u8 mon_table[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; // 平年月份日期表

/* 写入 RTC 实时时间,参数以 1970 年 1 月 1 日为基准,在 1970 ~ 2099 年范围内有效,返回 0 表示成功,返回其它表示错误 */
u8 RTC_Set(u16 syear, u8 smon, u8 sday, u8 hour, u8 min, u8 sec) {
u16 t;
u32 seccount = 0;

/* 将传入参数转换为 RTC 计数器值的转换算法 */
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 += (u32)mon_table[t] * 86400; // 将前面月份的秒钟数累加
if (Is_Leap_Year(syear) && t == 1)
seccount += 86400; // 闰年 2 月份增加一天时的秒钟数
}

seccount += (u32)(sday - 1) * 86400; // 将前面日期的秒钟数相加
seccount += (u32)hour * 3600; // 小时的秒钟数
seccount += (u32)min * 60; // 分钟的秒钟数
seccount += sec; // 最后的秒钟数

RTC_First_Config(); // 重置 RTC 时钟
BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); // 重新将识别符 0xA5A5 写入后备寄存器
RTC_SetCounter(seccount); // 将转换算后的计数器值写入 RTC 计数器
RTC_WaitForLastTask(); // 等待寄存器写入完成

return 0;
}

/* 读取 RTC 实时时间,返回 0 表示成功 */
u8 RTC_Get(void) {
static u16 daycnt = 0;
u32 timecount = 0;
u32 temp = 0;
u16 temp1 = 0;
timecount = RTC_GetCounter();
temp = timecount / 86400; // 得到秒钟数对应的天数

/* 如果超过 1 天 */
if (daycnt != temp) {
daycnt = temp;
temp1 = 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) { // 当年是否为闰年
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;
}

/* 根据传入的年、月、日参数,计算出相应的星期值,允许时间范围为 1901-2099 年,该函数由 RTC_Get() 调用 */
u8 RTC_Get_Week(u16 year, u8 month, u8 day) {
u16 temp2;
u8 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); // 返回由 0 ~ 6 表示的星期值
}

终端日历程序

本示例基于上一步实验的工程文件,仅在main()函数开始位置定义了一个 8 位变量bya,然后在主函数的while()循环当中,加入了串口识别以及 RTC 操作相关的代码,最后在usart.c初始化代码的尾部使能了串口中断。

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/** User/main.c */
#include "buzzer.h"
#include "delay.h"
#include "key.h"
#include "led.h"
#include "rtc.h"
#include "stm32f10x.h"
#include "sys.h"
#include "usart.h"

int main(void) {
u8 bya;
RCC_Configuration(); // 初始化 系统时钟
RTC_Config(); // 初始化 实时时钟
LED_Init(); // 初始化 LED
KEY_Init(); // 初始化 按键
BUZZER_Init(); // 初始化 蜂鸣器
USART1_Init(115200); // 初始化 串口
USART1_RX_STA = 0xC000; // 初始值设置为回车键按下的状态,使启动时可以自动显示菜单

while (1) {
/* 标志位为 0xC000 表示数据接收完成,可以开始进行处理 */
if (USART1_RX_STA & 0xC000) {

/* 按下回车键显示功能菜单 */
if ((USART1_RX_STA & 0x3FFF) == 0) {

/* 获取时间值,同时判断返回值是否为 0,不为 0 读取的值是错误的 */
if (RTC_Get() == 0) {
printf(" RTC 实时时钟测试程序 \r\n");
/* 打印日期时间 */
printf(" 现在实时时间:%d-%d-%d %d:%d:%d ", ryear, rmon, rday, rhour, rmin, rsec);
/* 打印星期时间 */
if (rweek == 0)
printf("星期日 \r\n");
if (rweek == 1)
printf("星期一 \r\n");
if (rweek == 2)
printf("星期二 \r\n");
if (rweek == 3)
printf("星期三 \r\n");
if (rweek == 4)
printf("星期四 \r\n");
if (rweek == 5)
printf("星期五 \r\n");
if (rweek == 6)
printf("星期六 \r\n");

printf(" 按下回车键更新时间,输入字母 C 初始化时钟 \r\n");
printf(" 设置时间,格式为 20170806120000,并按回车键确定 \r\n");
} else {
printf("读取失败\r\n"); // 时钟读取错误时打印提示信息
}
}
/* 判断接收数据的数量是否为 2 个 */
else if ((USART1_RX_STA & 0x3FFF) == 1) {
/* 通过判断数组的起始位是否为 'c' 或 'C' 来判断当前是否按下 C 键 */
if (USART1_RX_BUF[0] == 'c' || USART1_RX_BUF[0] == 'C') {
RTC_First_Config(); // 重新初始化实时时钟
BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); // 配置完成以后,将标识字符 0xA5A5 写入后备寄存器
printf("初始化成功! \r\n");
} else {
printf("指令错误! \r\n");
}
}
/* 判断接收数据的数量是否为 14 个,表示接收到的是重新设置 RTC 时间的操作 */
else if ((USART1_RX_STA & 0x3FFF) == 14) {
/* 将终端发送来的数据转换并写入 RTC,可以通过减去 0x30 得到十进制的 0 ~ 9 数据 */
ryear = (USART1_RX_BUF[0] - 0x30) * 1000 + (USART1_RX_BUF[1] - 0x30) * 100 + (USART1_RX_BUF[2] - 0x30) * 10 + USART1_RX_BUF[3] - 0x30;
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;
bya = RTC_Set(ryear, rmon, rday, rhour, rmin, rsec); // 将结果写入 RTC 计数器,并将返回值赋予 bya 变量

if (bya == 0)
printf("写入成功! \r\n"); // 显示写入成功
else
printf("写入失败! \r\n"); // 显示写入失败

} else {
printf("指令错误! \r\n");
}
USART1_RX_STA = 0; // 串口接收状态标志位清零
}
}
}

usart.c

1
2
3
4
5
6
7
8
9
10
/** Basic/usart/usart.c */

#if EN_USART1
/* 初始化并启动 USART1 */
void USART1_Init(u32 bound) {
/* ...... 省略 ...... */
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 传入 ENABLE 使能了串口中断
/* ...... 省略 ...... */
}
#endif

RCC 复位与时钟控制器

RCC 是复位和时钟控制器(Reset Clock Control)的英文缩写,用于设置单片机的复位以及系统时钟的分配(选择输入的时钟源),上面的实验程序开头部分都会调用到 RCC 设置函数。

rtc.c

1
2
3
4
5
6
7
8
9
10
11
/** Basic/rtc/rtc.c  */

void RTC_First_Config(void) { // 首次启用RTC的设置
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); // 启用 PWR 和 BKP 的时钟
/* ...... 省略 ...... */
RCC_LSEConfig(RCC_LSE_ON); // 启用外部 32.768KHZ 晶振
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET); // 等待晶振稳定工作
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); // RTC 时钟源配置为 LSE,即 32.768KHz 外部低速晶振
RCC_RTCCLKCmd(ENABLE); // 启动 RTC
/* ...... 省略 ...... */
}

main.c

1
2
3
4
5
/** User/main.c */
int main(void) {
RCC_Configuration(); // 初始化 RCC 系统时钟,配置时钟输入源
/* ...... 省略 ...... */
}

sys.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/** Basic/sys/sys.c */

/* RCC时钟的设置 */
void RCC_Configuration(void) {
ErrorStatus HSEStartUpStatus;
RCC_DeInit(); // 将 RCC 寄存器设置为默认值
RCC_HSEConfig(RCC_HSE_ON); // 使能外部高速晶振
HSEStartUpStatus = RCC_WaitForHSEStartUp(); // 等待外部高速晶振使能完成,并获取其状态

if (HSEStartUpStatus == SUCCESS) {
/* 设置 PLL 时钟源以及倍频系数
参数 RCC_PLLSource_HSE_Div1 表示使用的 PLL 类型为外部晶振不除以 2 方式;
参数 RCC_PLLMul_x 表示要设置的倍频系数为 9,即 PLLCLK = 72MHZ */
RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9);

/* 设置 AHB 总线时钟 HCLK,主要负责外部存储器时钟,参数 RCC_SYSCLK_Div1 是 AHB 总线的分频系数,设置为 1 表示不分频,即等于 72MHz */
RCC_HCLKConfig(RCC_SYSCLK_Div1);

/* 设置 APB1(负责DA、USB、SPI、I2C、CAN、USART2/3/4/5、普通定时器) 与 APB2(负责AD、GPIO、高级定时器、USART1) 总线的时钟 */
RCC_PCLK1Config(RCC_HCLK_Div2); // 设置 APB1 总线时钟为 2 分频,即 PCLK1 = HCLK ÷ 2 = 36MHz
RCC_PCLK2Config(RCC_HCLK_Div1); // 设置 APB2 总线时钟为 1 分频,即 PCLK2 = HCLK = 72MHz

/* 设置 Flash 存储器延时时钟的周期数值 */
FLASH_SetLatency(FLASH_Latency_2); // 单片机频率为 0~24MHz 时设置为 Latency_0;为 24~48MHz 时设置为 Latency_1;为 48~72MHz 时设置为 Latency_2
FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable); // 开启 Flash 功能的预取缓存模式,预取缓存是指将需要使用的数据,提前从 Flash 中读取并存放至 SRAM 当中

RCC_PLLCmd(ENABLE); // 锁相环倍频器 PLL 使能
while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET); // 等待 PLL 进入稳定工作状态
RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); // 将系统时钟源 SYSCLK 设置为 PLL
while (RCC_GetSYSCLKSource() != 0x08)
; // 等待 SYSCLK 时钟源切换 PLL 并进入稳定状态
}

/* 设备外设时钟,通常将这些代码放置到该功能的初始化函数当中 */
// RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOE, ENABLE);
// RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
// RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);
// RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
// RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
}

TTP223 触摸按键

TTP223是一款单通道电容触摸按键检测芯片,工作电压在2.0V ~ 5.5V范围之间,灵敏度可由0 ~ 50pF的外部电容进行调节。

touch-key

上面电路图当中,触摸按键TTP223_1TTP223_2TTP223_3TTP223_4经过 P10 跳线座,分别连接到 STM32 的 PA0PA1PA2PA3四个引脚,并且中间并联了一枚 LED 指示灯以及限流电阻。按键电路上15pFC3电容用来进行灵敏度调节,而C4则属于滤波电容。

TTP223
编号 名称 类型 定义
1 Q O 触摸按键的输出管脚
2 VSS P 电源负极接地端
3 I IO 传感器输入,连接至金属触摸按键上面
4 AHLB I-PL 选择输出电平,默认为1表示低电平有效,如果为0表示高电平有效
5 VDD P 电源正极输入引脚
6 TOG I-PL 输出类型选择,默认为高电平1触发模式(即带有按键锁存),如果为0表示直接模式

注意:TTP223 电容触摸芯片在上电瞬间,会读取感测电极的电容状态,并以此作为按键没有按下时的初始状态;因此,上电的一瞬间手指不能放置到按键上面。

单击控制核心板 LED

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/** User/main.c */
#include "delay.h"
#include "led.h"
#include "stm32f10x.h"
#include "sys.h"
#include "touch_key.h"

int main(void) {
RCC_Configuration(); // 初始化 系统时钟
LED_Init(); // 初始化 LED
TOUCH_KEY_Init(); // 初始化 按键
while (1) {
/* 读触摸按键 A 的电平状态 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A)) {
GPIO_WriteBit(LEDPORT, LED1, (BitAction)(1)); // 控制 LED,无需按键去抖处理
}
/* 读触摸按键 B 的电平状态 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_B)) {
GPIO_WriteBit(LEDPORT, LED2, (BitAction)(1)); // LED 控制,无需按键去抖处理
}
/* 读触摸按键 B 的电平状态 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_C)) {
GPIO_WriteBit(LEDPORT, LED1 | LED2, (BitAction)(0)); // LED 控制,无需按键去抖处理
}
/* 读触摸按键 B 的电平状态 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_D)) {
GPIO_WriteBit(LEDPORT, LED1 | LED2, (BitAction)(1)); // LED 控制,无需按键去抖处理
}
}
}

touch_key.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/** Hardware/TOUCH_KEY/touch_key.h */
#ifndef __TOUCH_KEY_H
#define __TOUCH_KEY_H
#include "sys.h"

#define TOUCH_KEYPORT GPIOA // 定义 GPIO 分组

#define TOUCH_KEY_A GPIO_Pin_0 // 定义 GPIO 接口
#define TOUCH_KEY_B GPIO_Pin_1 // 定义 GPIO 接口
#define TOUCH_KEY_C GPIO_Pin_2 // 定义 GPIO 接口
#define TOUCH_KEY_D GPIO_Pin_3 // 定义 GPIO 接口

void TOUCH_KEY_Init(void); // 触摸按键初始化函数

#endif

touch_key.c

1
2
3
4
5
6
7
8
9
10
/** Hardware/TOUCH_KEY/touch_key.c  */
#include "touch_key.h"

/* 触摸按键初始化函数 */
void TOUCH_KEY_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure; // 定义 GPIO 初始化枚举结构
GPIO_InitStructure.GPIO_Pin = TOUCH_KEY_A | TOUCH_KEY_B | TOUCH_KEY_C | TOUCH_KEY_D; // 选择 GPIO 端口
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 选择 GPIO 接口工作方式为上拉输入
GPIO_Init(TOUCH_KEYPORT, &GPIO_InitStructure);
}

双击/长按控制核心板 LED

本实验项目沿用上面的源代码,未进行目录结构上的任何修改,仅在main.c文件当中添加了#define KEYA_SPEED1 100#define KEYA_SPEED2 10两条用于区分长按与双击时间的宏定义语句,下面是触摸按键单击、双击、长按时产生的信号时序示意图:

TTP223

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/** User/main.c */
#include "delay.h"
#include "led.h"
#include "stm32f10x.h"
#include "sys.h"
#include "touch_key.h"

#define KEYA_SPEED1 100 // 定义长按的时间长度(单位为 10ms),即 100 * 10ms = 1秒,当按键按下的时间持续 1 秒钟以后就认为是长按
#define KEYA_SPEED2 10 // 定义双击的时间长度(单位为 20ms),即 10 * 20ms = 200毫秒,即第一次按键放开之后,如果 200ms 以内还有按键按下,就认为是双击

int main(void) {
u8 a = 0, b, c = 0;
RCC_Configuration(); // 初始化 系统时钟
LED_Init(); // 初始化 LED
TOUCH_KEY_Init(); // 初始化 按键

while (1) {
/* 判断触摸按键 A 是否按下 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A)) {
delay_ms(20); // 为了让程序兼容核心板上的按键 K1,这里调用了去抖函数

/* 判断长短按键 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A)) {
/* 循环进行长按状态的判断,如果 c < 100,这里 KEYA_SPEED1 为上面宏定义的值 100 */
while ((!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A)) && c < KEYA_SPEED1) {
c++;
delay_ms(10);
}

/* 长按处理代码,如果 c >= 100 */
if (c >= KEYA_SPEED1) {
GPIO_WriteBit(LEDPORT, LED1, (BitAction)(1)); // 点亮 LED1
while (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A));
}
/* 单击处理代码 */
else {
/* 首先判断双击,如果 b < 10,这里 KEYA_SPEED2 为上面宏定义的值 10 */
for (b = 0; b < KEYA_SPEED2; b++) {
delay_ms(20);
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A)) {
a = 1;
GPIO_WriteBit(LEDPORT, LED2, (BitAction)(1)); // 点亮 LED2
while (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A));
}
}
/* 然后判断单击 */
if (a == 0) {
GPIO_WriteBit(LEDPORT, LED1 | LED2, (BitAction)(0)); // 将 LED1 和 LED2 全部熄灭
}
}

a = 0; c = 0; // 参数清零,确保下次判断时 2 个参数都恢复为初始值
}
}
}
}

滑动控制 LED

需要进行滑动操作的 A、B、C、D 四个触摸按键间距必须足够的小,才会呈现出A - AB - B - BC - C - CD - D的滑动轨迹,而非仅仅被识别为 4 次点击操作。

touch-key

本实验项目同样基于上一步代码进行修改,引入了#include "usart.h"串口操作相关的头文件,便于在main.c当中将滑动操作的轨迹打印到 USART 串口上面。

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
/** User/main.c */
#include "delay.h"
#include "led.h"
#include "stm32f10x.h"
#include "sys.h"
#include "touch_key.h"
#include "usart.h"

#define KEYA_SPEED1 100 // 定义长按的时间长度(单位为 10ms),即 100 * 10ms = 1秒,当按键按下的时间持续 1 秒钟以后就认为是长按
#define KEYA_SPEED2 10 // 定义双击的时间长度(单位为 20ms),即 10 * 20ms = 200毫秒,即第一次按键放开之后,如果 200ms 以内还有按键按下,就认为是双击

int main(void) {
u16 k = 1000; // 用于滑动计数的变量,即串口终端上看到的计数值
u8 a = 0, b, c = 0;
u8 s = 0; // 定义用于判断是否刚结束完滑动的标志位

RCC_Configuration(); // 初始化 系统时钟
USART1_Init(115200); // 初始化 串口
LED_Init(); // 初始化 LED
TOUCH_KEY_Init(); // 初始化 按键

while (1) {
/* 触摸按键 A 处理程序,先检测按键是否按下 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A)) {
delay_ms(20); // 按键消抖

/* 判断长短键 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A)) {
while ((!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A)) && c < KEYA_SPEED1) {
c++;
delay_ms(10); // 长按计时
}

/* 长按处理 */
if (c >= KEYA_SPEED1) {
// 长按后执行的程序放到此处
GPIO_WriteBit(LEDPORT, LED1, (BitAction)(1)); // 点亮 LED1
printf("A键长按 \r\n");
while (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A)); // 等待按键 A 放开
}
/* 单击处理 */
else {
/* 判断当 A 键按下之后,按键 B 是否被按下,如果被按下就说明当前为滑动操作 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_B)) {
k++; // 用于显示的计数值
printf("A键右滑 %d \r\n", k);
a = 1; s = 1; // 变量 a 是单双击判断标志位,变量 s 是刚刚完成滑动的标志位,赋值以后程序流程就不再进入双击与单击的判断
}

/* 判断是否为双击 */
if (a == 0) {
for (b = 0; b < KEYA_SPEED2; b++) {
delay_ms(20);
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A)) {
a = 1;
GPIO_WriteBit(LEDPORT, LED2, (BitAction)(1)); // 点亮 LED2
printf("A键双击 \r\n");
while (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A));
}
}

/* 判断是否为单击 */
if (a == 0) {
/* 判断是否刚执行完滑动操作,如果是则属于滑动的放开操作,本次不需要进行单击处理 */
if (s == 1) {
s = 0;
}
/* 如果不是则正常执行单击处理 */
else {
GPIO_WriteBit(LEDPORT, LED1 | LED2, (BitAction)(0)); // 将 LED1 和 LED2 全部熄灭
printf("A键单击 \r\n");
}
}
}
}
a = 0; c = 0; // 参数清零,确保下次判断时 2 个参数都恢复为初始值
}
}

/* 触摸按键 B 处理程序,先检测按键是否按下 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_B)) {
delay_ms(20); // 按键消抖

/* 判断长短键 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_B)) {
while ((!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_B)) && c < KEYA_SPEED1) {
c++;
delay_ms(10); // 长按计时
}

/* 长按处理 */
if (c >= KEYA_SPEED1) {
// 长按后执行的程序放到此处
GPIO_WriteBit(LEDPORT, LED1, (BitAction)(1)); // 点亮 LED1
printf("B键长按 \r\n");
while (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_B)); // 等待按键 B 放开
}
/* 单击处理 */
else {
/* 判断当 B 键按下之后,按键 C 是否被按下,如果被按下就说明当前为右滑操作 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_C)) {
k++; // 用于显示的计数值
printf("B键右滑 %d \r\n", k);
a = 1; s = 1; // 变量 a 是单双击判断标志位,变量 s 是刚执行完滑动的标志位,赋值以后程序流程就不再进入双击与单击的判断
}
/* 判断当 B 键按下之后,按键 A 是否被按下,如果被按下就说明当前为左滑操作 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A)) {
k++; // 用于显示的计数值
printf("B键左滑 %d \r\n", k);
a = 1; s = 1; // 变量 a 是单双击判断标志位,变量 s 是刚执行完滑动的标志位,赋值以后程序流程就不再进入双击与单击的判断
}

/* 判断是否为双击 */
if (a == 0) {
for (b = 0; b < KEYA_SPEED2; b++) {
delay_ms(20);
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_B)) {
a = 1;
GPIO_WriteBit(LEDPORT, LED2, (BitAction)(1)); // 点亮 LED2
printf("B键双击 \r\n");
while (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_B));
}
}

/* 判断是否为单击 */
if (a == 0) {
/* 判断是否刚执行完滑动操作,如果是则属于滑动的放开操作,本次不需要进行单击处理 */
if (s == 1) {
s = 0;
}
/* 如果不是则正常执行单击处理 */
else {
GPIO_WriteBit(LEDPORT, LED1 | LED2, (BitAction)(0)); // 将 LED1 和 LED2 全部熄灭
printf("B键单击 \r\n");
}
}
}
}
a = 0; c = 0; // 参数清零,确保下次判断时 2 个参数都恢复为初始值
}
}

/* 触摸按键 C 处理程序,先检测按键是否按下 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_C)) {
delay_ms(20); // 按键消抖

/* 判断长短键 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_C)) {
while ((!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_C)) && c < KEYA_SPEED1) {
c++;
delay_ms(10); // 长按计时
}

/* 长按处理 */
if (c >= KEYA_SPEED1) {
// 长按后执行的程序放到此处
GPIO_WriteBit(LEDPORT, LED1, (BitAction)(1)); // 点亮 LED1
printf("C键长按 \r\n");
while (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_C)); // 等待按键 C 放开
}
/* 单击处理 */
else {
/* 判断当 C 键按下之后,按键 D 是否被按下,如果被按下就说明当前为右滑操作 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_D)) {
k++; // 用于显示的计数值
printf("C键右滑 %d \r\n", k);
a = 1; s = 1; // 变量 a 是单双击判断标志位,变量 s 是刚执行完滑动的标志位,赋值以后程序流程就不再进入双击与单击的判断
}
/* 判断当 C 键按下之后,按键 B 是否被按下,如果被按下就说明当前为左滑操作 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_B)) {
k++; // 用于显示的计数值
printf("C键左滑 %d \r\n", k);
a = 1; s = 1; // 变量 a 是单双击判断标志位,变量 s 是刚执行完滑动的标志位,赋值以后程序流程就不再进入双击与单击的判断
}

/* 判断是否为双击 */
if (a == 0) {
for (b = 0; b < KEYA_SPEED2; b++) {
delay_ms(20);
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_C)) {
a = 1;
GPIO_WriteBit(LEDPORT, LED2, (BitAction)(1)); // 点亮 LED2
printf("C键双击 \r\n");
while (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_C));
}
}

/* 判断是否为单击 */
if (a == 0) {
/* 判断是否刚执行完滑动操作,如果是则属于滑动的放开操作,本次不需要进行单击处理 */
if (s == 1) {
s = 0;
}
/* 如果不是则正常执行单击处理 */
else {
GPIO_WriteBit(LEDPORT, LED1 | LED2, (BitAction)(0)); // 将 LED1 和 LED2 全部熄灭
printf("C键单击 \r\n");
}
}
}
}
a = 0; c = 0; // 参数清零,确保下次判断时 2 个参数都恢复为初始值
}
}

/* 触摸按键 D 处理程序,先检测按键是否按下 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_D)) {
delay_ms(20); // 按键消抖

/* 判断长短键 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_D)) {
while ((!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_D)) && c < KEYA_SPEED1) {
c++;
delay_ms(10); // 长按计时
}

/* 长按处理 */
if (c >= KEYA_SPEED1) {
// 长按后执行的程序放到此处
GPIO_WriteBit(LEDPORT, LED1, (BitAction)(1)); // 点亮 LED1
printf("D键长按 \r\n");
while (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_D)); // 等待按键 D 放开
}
/* 单击处理 */
else {

/* 判断当 D 键按下之后,按键 C 是否被按下,如果被按下就说明当前为左滑操作 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_C)) {
k++; // 用于显示的计数值
printf("D键左滑 %d \r\n", k);
a = 1; s = 1; // 变量 a 是单双击判断标志位,变量 s 是刚执行完滑动的标志位,赋值以后程序流程就不再进入双击与单击的判断
}

/* 判断是否为双击 */
if (a == 0) {
for (b = 0; b < KEYA_SPEED2; b++) {
delay_ms(20);
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_D)) {
a = 1;
GPIO_WriteBit(LEDPORT, LED2, (BitAction)(1)); // 点亮 LED2
printf("D键双击 \r\n");
while (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_D));
}
}

/* 判断是否为单击 */
if (a == 0) {
/* 判断是否刚执行完滑动操作,如果是则属于滑动的放开操作,本次不需要进行单击处理 */
if (s == 1) {
s = 0;
}
/* 如果不是则正常执行单击处理 */
else {
GPIO_WriteBit(LEDPORT, LED1 | LED2, (BitAction)(0)); // 将 LED1 和 LED2 全部熄灭
printf("D键单击 \r\n");
}
}
}
}
a = 0; c = 0; // 参数清零,确保下次判断时 2 个参数都恢复为初始值
}
}
}
}

TM1640 驱动共阴极数码管

TM1640 由深圳天威电子推出的一款数码管专用驱动控制芯片,采用 SOP28 形式封装,并使用 5V 工作电压。

tm1640
符号 引脚名称 引脚编号 功能说明
DIN 数据输入 7 串行数据输入,输入数据在 SCLK 的低电平变化,在 SCLK 的高电平被传输
SCLK 时钟输入 8 在上升沿输入数据
SEG1~SEG8 输出【段】 9~16 段输出,P 管开漏输出
GRID1~GRID11 和 GRID12~GRID16 输出【位】 1~5 和 18~28 位输出,N 管开漏输出
VDD 逻辑电源 17 接电源正
VSS 逻辑地 6 接系统地

STM32F103C8T6的通过两线式通信接口与TM1640进行连接,当时钟信号CLK为高电平时,数据信号DIN保持不变;仅当CLK为低电平时DIN的信号才会发生改变,数据传输时总是低位在前,高位在后。数据输入的开始条件为CLK为高时 DIN由高变低负跳变;结束条件是CLK为高时DIN由低向高正跳变,指令传输的时序图如下:

tm1640

TM1640 的数据线DIN和时钟同步线SCLK引脚通过跳线帽P9连接至 STM32 的PA11PA12引脚,其第 17 脚和 6 脚分别连接至 5V 电源与GND,并在回路上串接了C1C2两枚滤波电容;GR1GR8引脚则各自连接到共阴极数码管的位选端上,SEG1SEG8引脚则连接至段选端。另外,电路中 8 枚独立 LED 的负极分别连接至 TM1640 的第 26 脚,正极则分别连接至SEG1SEG8引脚。

diagram

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/** User/main.c */
#include "TM1640.h"
#include "delay.h"
#include "rtc.h"
#include "stm32f10x.h"
#include "sys.h"

int main(void) {
u8 c = 0x01;
RCC_Configuration(); // 初始化 系统时钟
RTC_Config(); // 初始化 RTC
TM1640_Init(); // 初始化 TM1640
while (1) {
if (RTC_Get() == 0) {
/* 调用 TM1640 显示函数,用于显示天数,后面加 10 是为了点亮小数点 */
TM1640_display(0, rday / 10);
TM1640_display(1, rday % 10 + 10);
/* 调用 TM1640 显示函数,用于显示小时,后面加 10 是为了点亮小数点 */
TM1640_display(2, rhour / 10);
TM1640_display(3, rhour % 10 + 10);
/* 调用 TM1640 显示函数,用于显示分钟,后面加 10 是为了点亮小数点 */
TM1640_display(4, rmin / 10);
TM1640_display(5, rmin % 10 + 10);
/* 调用 TM1640 显示函数,用于显示秒数,后面加 10 是为了点亮小数点 */
TM1640_display(6, rsec / 10);
TM1640_display(7, rsec % 10);

TM1640_led(c); // 高电平点亮与 TM1640 连接的 8 位独立 LED

c <<= 1; // 通过对局部变量 c 进行左移操作,实现出流水灯效果
if (c == 0x00)
c = 0x01; // 局部变量 c 左移操作结束之后,让 c 恢复默认值,从而达到反复显示流水灯的效果
delay_ms(125); // 通过延时函数来决定流水灯的速度
}
}
}

TM1640.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/** Hardware/TM1640/TM1640.h */
#ifndef __TM1640_H
#define __TM1640_H
#include "sys.h"

/* 定义 TM1640 所使用的 GPIO 端口 */
#define TM1640_GPIOPORT GPIOA
#define TM1640_SCLK GPIO_Pin_11
#define TM1640_DIN GPIO_Pin_12

#define TM1640_LEDPORT 0xC8 // 定义连接到 TM1640 的 8 个独立 LED 操作地址

void TM1640_Init(void); // 初始化函数
void TM1640_led(u8 date); // 8 个独立 LED 驱动函数
void TM1640_display(u8 address, u8 date); // 数码管显示函数
void TM1640_display_add(u8 address, u8 date); // 数码管显示函数,自有自动累加 1 功能

#endif

TM1640.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/** Hardware/TM1640/TM1640.c  */
#include "TM1640.h"
#include "delay.h"

#define DEL 1 // 如果数码管显示不稳定,可以增大这个数值,从而减慢通信速度

/* 地址模式的设置 */
// #define TM1640MEDO_ADD 0x40 // 地址自动加 1 模式
#define TM1640MEDO_ADD 0x44 // 固定地址模式,推荐使用该模式

/* 设置显示亮度,0x80 可以关闭显示,0x88、0x89、0x8a、0x8b、0x8c、0x8d、0x8e、0x8f 分别对应的脉冲宽度为 1/16、2/16、4/16、10/16、11/16、12/16、13/16、14/16 */
// #define TM1640MEDO_DISPLAY 0x88 // 最小亮度
// #define TM1640MEDO_DISPLAY 0x89
// #define TM1640MEDO_DISPLAY 0x8a
// #define TM1640MEDO_DISPLAY 0x8b
#define TM1640MEDO_DISPLAY 0x8c // 推荐亮度
// #define TM1640MEDO_DISPLAY 0x8d
// #define TM1640MEDO_DISPLAY 0x8f // 最大亮度
#define TM1640MEDO_DISPLAY_OFF 0x80 // 关闭显示

/* 通信时序,启动 */
void TM1640_start() {
GPIO_WriteBit(TM1640_GPIOPORT, TM1640_DIN, (BitAction)(1)); // 引脚输出高电平 1
GPIO_WriteBit(TM1640_GPIOPORT, TM1640_SCLK, (BitAction)(1)); // 引脚输出高电平 1
delay_us(DEL);
GPIO_WriteBit(TM1640_GPIOPORT, TM1640_DIN, (BitAction)(0)); // 引脚输出低电平 0
delay_us(DEL);
GPIO_WriteBit(TM1640_GPIOPORT, TM1640_SCLK, (BitAction)(0)); // 引脚输出低电平 0
delay_us(DEL);
}

/* 通信时序,结束 */
void TM1640_stop() {
GPIO_WriteBit(TM1640_GPIOPORT, TM1640_DIN, (BitAction)(0)); // 引脚输出低电平 0
GPIO_WriteBit(TM1640_GPIOPORT, TM1640_SCLK, (BitAction)(1)); // 引脚输出高电平 1
delay_us(DEL);
GPIO_WriteBit(TM1640_GPIOPORT, TM1640_DIN, (BitAction)(1)); // 引脚输出高电平 1
delay_us(DEL);
}

/* 通信时序,写数据 */
void TM1640_write(u8 date) { //
u8 i;
u8 aa;
aa = date;
GPIO_WriteBit(TM1640_GPIOPORT, TM1640_DIN, (BitAction)(0)); // 引脚输出低电平 0
GPIO_WriteBit(TM1640_GPIOPORT, TM1640_SCLK, (BitAction)(0)); // 引脚输出低电平 0
for (i = 0; i < 8; i++) {
GPIO_WriteBit(TM1640_GPIOPORT, TM1640_SCLK, (BitAction)(0)); // 引脚输出低电平 0
delay_us(DEL);

if (aa & 0x01) {
GPIO_WriteBit(TM1640_GPIOPORT, TM1640_DIN, (BitAction)(1)); // 引脚输出高电平 1
delay_us(DEL);
} else {
GPIO_WriteBit(TM1640_GPIOPORT, TM1640_DIN, (BitAction)(0)); // 引脚输出低电平 0
delay_us(DEL);
}
GPIO_WriteBit(TM1640_GPIOPORT, TM1640_SCLK, (BitAction)(1)); // 引脚输出高电平 1
delay_us(DEL);
aa = aa >> 1;
}
GPIO_WriteBit(TM1640_GPIOPORT, TM1640_DIN, (BitAction)(0)); // 引脚输出 0
GPIO_WriteBit(TM1640_GPIOPORT, TM1640_SCLK, (BitAction)(0)); // 引脚输出 0
}

/* TM1640 初始化函数 */
void TM1640_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC, ENABLE);
GPIO_InitStructure.GPIO_Pin = TM1640_DIN | TM1640_SCLK; // 选择 GPIO 端口号
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 设置为推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置 GPIO 接口速度为 50MHz
GPIO_Init(TM1640_GPIOPORT, &GPIO_InitStructure);

/* 将两个 GPIO 默认初始为高电平 1 */
GPIO_WriteBit(TM1640_GPIOPORT, TM1640_DIN, (BitAction)(1));
GPIO_WriteBit(TM1640_GPIOPORT, TM1640_SCLK, (BitAction)(1));

TM1640_start();
TM1640_write(TM1640MEDO_ADD); // 设置固定地址还是自动加 1 模式
TM1640_stop();
TM1640_start();
TM1640_write(TM1640MEDO_DISPLAY); // 设置显示亮度
TM1640_stop();
}

/* 8 个独立 LED 控制函数 */
void TM1640_led(u8 date) {
TM1640_start();
TM1640_write(TM1640_LEDPORT);
TM1640_write(date);
TM1640_stop();
}

/* 固定地址模式显示函数 */
void TM1640_display(u8 address, u8 date) {
/* 数字 0 ~ 9 的显示段码表,每个数组元素分别对应 0 1 2 3 4 5 6 7 8 9 0. 1. 2. 3. 4. 5. 6. 7. 8. 9. 空 */
const u8 buff[21] = {0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f, 0xbf, 0x86, 0xdb, 0xcf, 0xe6, 0xed, 0xfd, 0x87, 0xff, 0xef, 0x00};

TM1640_start();
TM1640_write(0xC0 + address); // 写入显示位置
TM1640_write(buff[date]); // 写入显示数据
TM1640_stop();
}

/* 地址自动加 1 模式显示函数 */
void TM1640_display_add(u8 address, u8 date) {
u8 i;
/* 数字 0 ~ 9 的显示段码表,每个数组元素分别对应 0 1 2 3 4 5 6 7 8 9 0. 1. 2. 3. 4. 5. 6. 7. 8. 9. 空 */
const u8 buff[21] = {0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f, 0xbf, 0x86, 0xdb, 0xcf, 0xe6, 0xed, 0xfd, 0x87, 0xff, 0xef, 0x00};

TM1640_start();
TM1640_write(0xC0 + address); // 设置起始地址

for (i = 0; i < 16; i++) {
TM1640_write(buff[date]);
}
TM1640_stop();
}

EC11 旋转编码器

实验电路中所使用的 EC11 旋转编码器,旋转一圈产生的脉冲次数为20次(即旋转 360° 需要经过 20 个位格),且向下按压时可作为开关使用,最大工作电压为5V,最大工作电流为10mA,其内部等效原理图如下所示:

sketch

当按下旋钮时,开关K1闭合,导通第12引脚;当旋转旋钮时,开关K2K3以一定的先后顺序导通,通过两者的导通顺序就可以判断旋转方向和旋转的隔断数,如下是旋钮分别向左和向右旋转时,开关K2K3产生的时序差异比较图:

sequence

注意:由于旋转编码器内部采用的是机械式微动开关,旋钮旋转过一个隔断会产生一个低电平触发,此时会在该低电平触发的下降沿和上升沿产生约2ms左右的按键抖动。

diagram

电路图当中,将 EC11 旋转编码器的第14引脚连接至GND,而第235脚通过跳线帽P8连接至 STM32F103 的PA6PA7PB2引脚,此时只需要将GPIO设置为上拉电阻输入方式,就可以在K1K2K3任何一个开关闭合导通时,向 GPIO 端口输入低电平,从而通过这 3 个 GPIO 读取到了旋转编码器内部 3 个微动开关的状态。

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/** User/main.c */
#include "TM1640.h"
#include "delay.h"
#include "encoder.h"
#include "rtc.h"
#include "stm32f10x.h"
#include "sys.h"

int main(void) { //主程序
u8 a = 0, b = 0, c = 0x01;
RCC_Configuration(); // 初始化系统时钟
RTC_Config(); // 初始化 RTC 实时时钟
ENCODER_Init(); // 初始化 旋转编码器
TM1640_Init(); // 初始化 TM1640

/* 数码管 8 位的显示内容,其中前 2 位显示变量 a 的十位和个位 */
TM1640_display(0, a / 10);
TM1640_display(1, a % 10);

/* 数码管其它位保持熄灭状态 */
TM1640_display(2, 20);
TM1640_display(3, 20);
TM1640_display(4, 20);
TM1640_display(5, 20);
TM1640_display(6, 20);
TM1640_display(7, 20);

while (1) {
b = ENCODER_READ(); // 读取旋转编码器的当前状态

/* 如果 b 等于 1,说明当前为向右旋转 */
if (b == 1) {
a++;
if (a > 99)
a = 0;
}

/* 如果 b 等于 2,说明当前为向左旋转 */
if (b == 2) {
if (a == 0)
a = 100;
a--;
}

/* 如果 b 等于 3,就表示旋钮按下 */
if (b == 3)
a = 0;

/* 如果存在旋转操作,就调用数码管显示变量 a 的十位和个位 */
if (b != 0) {
TM1640_display(0, a / 10);
TM1640_display(1, a % 10);
}
}
}

encoder.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/** Hardware/ENCODER/encoder.h */
#ifndef __ENCODER_H
#define __ENCODER_H
#include "delay.h"
#include "sys.h"

/* 定义旋钮旋转控制对应的 3 个 GPIO */
#define ENCODER_PORT_A GPIOA
#define ENCODER_L GPIO_Pin_6
#define ENCODER_D GPIO_Pin_7

/* 定义旋钮按下导通对应的 1 个 GPIO */
#define ENCODER_PORT_B GPIOB
#define ENCODER_R GPIO_Pin_2

void ENCODER_Init(void);
u8 ENCODER_READ(void);

#endif

encoder.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/** Hardware/ENCODER/encoder.c  */
#include "encoder.h"

u8 KUP; // 编码器旋钮卡死标志位,如果旋钮处于卡死状态则置 1,如果未发生卡死则清零
u16 cou; // 卡死状态的循环计数器

/* 旋转编码器初始化函数 */
void ENCODER_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure; // 定义GPIO的初始化枚举结构
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC, ENABLE);

/* 设置 PA 引脚上的 2 个 GPIO,并配置为上拉电阻输入方式 */
GPIO_InitStructure.GPIO_Pin = ENCODER_L | ENCODER_D;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(ENCODER_PORT_A, &GPIO_InitStructure);

/* 设置 PB 引脚上的 1 个 GPIO,并配置为上拉电阻输入方式 */
GPIO_InitStructure.GPIO_Pin = ENCODER_R;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(ENCODER_PORT_B, &GPIO_InitStructure);
}

/* 旋转编码器状态读取函数,返回值为当前编码器的操作状态,如果为 1 表示右转,为 2 表示左转,为 3 表示按下 */
u8 ENCODER_READ(void) {
u8 a; // 存放按键的值
u8 kt;
a = 0;

/* 判断旋钮是否解除卡死 */
if (GPIO_ReadInputDataBit(ENCODER_PORT_A, ENCODER_L))
KUP = 0;

/* 判断是否旋转旋钮,同时判断旋钮卡死标志位 */
if (!GPIO_ReadInputDataBit(ENCODER_PORT_A, ENCODER_L) && KUP == 0) {
delay_us(100); // 等待进入稳定状态
kt = GPIO_ReadInputDataBit(ENCODER_PORT_B, ENCODER_R); // 读取 K3 开关的状态,并将其暂存至变量 kt
delay_ms(3); // 消除微动开关机械抖动

/* 重新读取 K2 开关的状态,确定 K2 是否还依然处于低电平,如果是则表明状态有效 */
if (!GPIO_ReadInputDataBit(ENCODER_PORT_A, ENCODER_L)) {

/* 通过 K3 开关的状态判断当前是左转还是右转 */
if (kt == 0) {
a = 1; // 旋钮右转
} else {
a = 2; // 旋钮左转
}

cou = 0; // 初始卡死判断计数器
/* 循环判断 K2 对应的 GPIO 是否为低电平,如果旋钮发生卡死,则不会退出循环。与此同时,判断 cou 变量是否小于 60000,即旋钮隔断的时间是否小于 1.2 秒,一旦超出 1.2 秒之后则认为发生卡死并退出循环,从而避免由于旋钮卡死造成程序陷入死循环 */
while (!GPIO_ReadInputDataBit(ENCODER_PORT_A, ENCODER_L) && cou < 60000) {
cou++; // 等待放开旋钮,同时累加判断卡死
KUP = 1; // 标识当前旋钮处于卡死状态
delay_us(20); // 延时 20 × 60000 = 1.2秒
}
}
}

/* 判断旋钮是否按下,同时判断旋钮卡死标志位 */
if (!GPIO_ReadInputDataBit(ENCODER_PORT_A, ENCODER_D) && KUP == 0) {
delay_ms(20); // 机械式微动开头去抖

/* 再次判断旋钮是否按下 */
if (!GPIO_ReadInputDataBit(ENCODER_PORT_A, ENCODER_D)) {
a = 3; // 旋钮按下时,变量 a 的值为 3
while (ENCODER_D == 0); // 如果需要等待旋钮松开,可以取消该条程序的注释
}
}
return a; // 将 a 的值返回
}

注意:旋钮锁死是指旋钮停留在两个机械开关隔断之间的状态,卡死会造成 K2 和 K3 都处于低电平状态无法退出。

I²C 总线

LM75A 温度传感器

I²C总线属于两线制的通信连接方式,传输线路上需要使用1kΩ ~ 10kΩ范围阻值的上拉电阻,并且复用为 I²C 接口的 GPIO 引脚需要设置为复用开漏模式。由于 I²C 总线上,所有设备都连接到相同的数据线 SDA时钟线 SCL,因此通过每个设备的唯一地址来加以区分,该地址由 7 位十六进制数组成,一条总线上最多可挂载 127 个设备。新版 I²C 规范增加了 10 位地址模式,可以容纳的设备数量达到了 1023 个。

STM32F103C8T6拥有 2 组I²C总线接口,分别是由PB6PB7组成的I²C1,由PB10PB11组成的I²C2。其中,I²C1通过跳线帽P11串联R18R19两枚5.1kΩ上拉电阻以后,连接至实验电路中的 OLED 液晶显示屏以及 LM75A 温度传感器。

diagram-lm75a

LM75A是由恩智浦公司生产的一款温度传感器,一共拥有 8 个引脚,其中第 1 和 2 引脚分别连接至 I²C 总线的两条通信线路,第 3 脚是中断输出,第 4 和 8 脚分别是电源的VCCGND,而第 5、6、7 引脚可以用来定义设备地址。

diagram-lm75a
引脚编号 引脚名称 功能说明
1 SDA 串行数据输入输出。
2 SCL 串行时钟输入。
3 OS 过热时中断输出。
4 GND 电源负极接地。
5 A2 设备地址设置 2。
6 A1 设备地址设置 1。
7 A0 设备地址设置 0。
8 +Vs 电源正极供电。

注意:实验程序中,会将 A0、A1、A2 这 3 位的值都置为1,由于前 4 位地址固定为1001,最低位读写位默认为 0,那么 LM75A 的设备地址应为:1001 111 0 = 0x9E

本实验以上一个项目作为模板修改完成,在Basic文件夹下加入i2c.hi2c.c两个程序文件,然后在Hardware目录下添加了lm75a.hlm75a.c两个源文件,最后将官方的stm32f10x_i2c.c库文件加入到Lib目录。

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/** User/main.c */
#include "TM1640.h"
#include "delay.h"
#include "lm75a.h"
#include "stm32f10x.h"
#include "sys.h"

int main(void) {
u8 buffer[3];
u8 c = 0x01;
RCC_Configuration(); // 初始化 系统时钟
I2C_Configuration(); // 初始化 I²C 总线
TM1640_Init(); // 初始化 TM1640

/* 初始化显示内容 */
TM1640_display(0, 20);
TM1640_display(1, 20);
TM1640_display(2, 20);
TM1640_display(3, 20);
TM1640_display(4, 20);
TM1640_display(5, 20);
TM1640_display(6, 20);
TM1640_display(7, 20);

while (1) {
LM75A_GetTemp(buffer); // 将 LM75A 获取的温度数据读取到变量 buffer

TM1640_display(0, buffer[1] / 10); // 温度值整数部分十位
TM1640_display(1, buffer[1] % 10 + 10); // 温度值整数部分个位
TM1640_display(2, buffer[2] / 10); // 温度值小数部分十位
TM1640_display(3, buffer[2] % 10); // 温度值小数部分个位

/* 点亮与 TM1640 数码管相连的 8 枚独立 LED,并呈现流水灯效果 */
TM1640_led(c);
c <<= 1;
if (c == 0x00)
c = 0x01;

delay_ms(150);
}
}

i2c.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/** Basic/i2c/i2c.h */
#ifndef __I2C_H
#define __I2C_H
#include "sys.h"

/* 定义 I²C 总线需要使用的 GPIO 引脚 */
#define I2CPORT GPIOB
#define I2C_SCL GPIO_Pin_6
#define I2C_SDA GPIO_Pin_7

#define HostAddress 0xc0 // STM32F103C8T6 作为 I²C 总线主设备的地址
#define BusSpeed 200000 // 总线速度过高,可能会出现通信数据出错卡死的情况,建议不高于 400000

void I2C_Configuration(void);
void I2C_SEND_BUFFER(u8 SlaveAddr, u8 WriteAddr, u8 *pBuffer, u16 NumByteToWrite);
void I2C_SEND_BYTE(u8 SlaveAddr, u8 writeAddr, u8 pBuffer);
void I2C_READ_BUFFER(u8 SlaveAddr, u8 readAddr, u8 *pBuffer, u16 NumByteToRead);
u8 I2C_READ_BYTE(u8 SlaveAddr, u8 readAddr);

#endif

i2c.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
/** Basic/i2c/i2c.c  */
#include "i2c.h"

/* I²C 初始化 */
void I2C_GPIO_Init(void) { //
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC, ENABLE); // 配置 GPIO 相关的时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); // 启动 I2C 功能
GPIO_InitStructure.GPIO_Pin = I2C_SCL | I2C_SDA; // 选择 GPIO 端口
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 选择 GPIO 工作方式为复用开漏输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置 GPIO 传输速度
GPIO_Init(I2CPORT, &GPIO_InitStructure); // 将设置写入 GPIO 初始化程序
}

/* I²C 配置 */
void I2C_Configuration(void) {
I2C_InitTypeDef I2C_InitStructure;
I2C_GPIO_Init(); // 初始化 GPIO
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; // 设置为 I²C 模式
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStructure.I2C_OwnAddress1 = HostAddress; // 主设备地址
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; // 允许应答
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; // 设备为 7 位地址模式
I2C_InitStructure.I2C_ClockSpeed = BusSpeed; // 设置 I²C 总线速度
I2C_Init(I2C1, &I2C_InitStructure);
I2C_Cmd(I2C1, ENABLE); // 开启 I²C
}

/* 通过 I²C 总线发送多个字节数据,参数 SlaveAddr 是目标设备地址,参数 WriteAddr 表示向该设备哪个地址写入数据,参数 pBuffer 为待发送的数据,参数 NumByteToWrite 为写入数据的数量 */
void I2C_SEND_BUFFER(u8 SlaveAddr, u8 WriteAddr, u8 *pBuffer, u16 NumByteToWrite) {
I2C_GenerateSTART(I2C1, ENABLE); // 产生起始位
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); // 等待完成
I2C_Send7bitAddress(I2C1, SlaveAddr, I2C_Direction_Transmitter); // 发送设备地址
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); // 等待完成
I2C_SendData(I2C1, WriteAddr); // 发送需要写入数据的目标设备地址
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); // 等待完成

/* 循环发送多个数据 */
while (NumByteToWrite--) {
I2C_SendData(I2C1, *pBuffer); // 发送数据
pBuffer++; // 待写入数据的指针地址自增 1
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); // 等待完成
}

I2C_GenerateSTOP(I2C1, ENABLE); //产生停止信号
}

/* 通过 I²C 总线发送 1 个字节数据,参数 SlaveAddr 是目标设备地址,参数 WriteAddr 表示向该设备哪个地址写入数据,参数 pBuffer 为待发送的数据 */
void I2C_SEND_BYTE(u8 SlaveAddr, u8 writeAddr, u8 pBuffer) {
I2C_GenerateSTART(I2C1, ENABLE); // 发送开始信号
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); // 等待完成
I2C_Send7bitAddress(I2C1, SlaveAddr, I2C_Direction_Transmitter); // 发送目标设备地址,并设置为发送者模式
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); // 等待完成
I2C_SendData(I2C1, writeAddr); // 该设备需要写入数据的地址
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); // 等待完成
I2C_SendData(I2C1, pBuffer); // 发送待写入的内容
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); // 等待完成
I2C_GenerateSTOP(I2C1, ENABLE); // 发送结束信号
}

/* 通过 I²C 总线读取多个字节的数据,参数 SlaveAddr 为从设备地址,参数 readAddr 为从设备上待读取的地址,参数 pBuffer 为读取数据所存放的指针,参数 NumByteToRead 为待读取的字节数 */
void I2C_READ_BUFFER(u8 SlaveAddr, u8 readAddr, u8 *pBuffer, u16 NumByteToRead) {
while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
I2C_GenerateSTART(I2C1, ENABLE); // 发送开始信号
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); // 等待完成
I2C_Send7bitAddress(I2C1, SlaveAddr, I2C_Direction_Transmitter); // 写入器件地址
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); // 等待完成
I2C_Cmd(I2C1, ENABLE);
I2C_SendData(I2C1, readAddr); // 发送从设备上待读取的地址
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); // 等待完成
I2C_GenerateSTART(I2C1, ENABLE); //开启信号
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); // 等待完成
I2C_Send7bitAddress(I2C1, SlaveAddr, I2C_Direction_Receiver); // 发送从设备地址
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); // 等待完成

/* 循环读取多个数据 */
while (NumByteToRead) {
/* 如果读取到的是最后 1 个数据 */
if (NumByteToRead == 1) {
I2C_AcknowledgeConfig(I2C1, DISABLE); // 读取最后 1 个数据时关闭应答位
I2C_GenerateSTOP(I2C1, ENABLE); // 发送停止位
}
/* 正常读取数据 */
if (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)) {
*pBuffer = I2C_ReceiveData(I2C1); // 将数据读取出到 pBuffer 指针
pBuffer++; // 读取数据指针 pBuffer 自增 1
NumByteToRead--; // 待读取字节数自减 1
}
}

I2C_AcknowledgeConfig(I2C1, ENABLE);
}

/* 通过 I²C 总线读取 1 个字节数据,参数 SlaveAddr 为从设备地址,参数 readAddr 为从设备上待读取的地址 */
u8 I2C_READ_BYTE(u8 SlaveAddr, u8 readAddr) {
u8 a;

while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); // 判断总线是否繁忙,空闲后开始执行后续代码
I2C_GenerateSTART(I2C1, ENABLE); // 发起始信号
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); // 等待完成
I2C_Send7bitAddress(I2C1, SlaveAddr, I2C_Direction_Transmitter); // 发送从设备地址
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); // 等待完成
I2C_Cmd(I2C1, ENABLE); // 开启 I²C1
I2C_SendData(I2C1, readAddr); // 发送从设备上待读取的地址
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); // 等待完成
I2C_GenerateSTART(I2C1, ENABLE); // 允许 I²C 从设备产生开始信号,让从设备可以向单片机主设备发送数据
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); // 等待完成
I2C_Send7bitAddress(I2C1, SlaveAddr, I2C_Direction_Receiver); // 发送从设备地址
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); // 等待完成
I2C_AcknowledgeConfig(I2C1, DISABLE); // 读取最后 1 个数据时关闭应答位
I2C_GenerateSTOP(I2C1, ENABLE); // 发送停止位
a = I2C_ReceiveData(I2C1); // 将接收到的数据保存至变量 a

return a;
}

lm75a.h

1
2
3
4
5
6
7
8
9
10
11
12
/** Hardware/LM75A/lm75a.h */
#ifndef __LM75A_H
#define __LM75A_H
#include "i2c.h"
#include "sys.h"

#define LM75A_ADD 0x9E // LM75A温度传感器的 I²C 设备地址

void LM75A_GetTemp(u8 *Tempbuffer); // 温度读取函数声明
void LM75A_POWERDOWN(void); // 温度传感器进入掉电模式,常用于低功耗场合

#endif

lm75a.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/** Hardware/LM75A/lm75a.c */
#include "lm75a.h"

/* 读取 LM75A 温度值,取值范围在 -55℃ ~ 125℃ 之间,参数值 Tempbuffer 是温度值指针存放的位置,包含有 3 个字节的数据,第1个字节是温度正负号,第2个字节是温度整数值,第3个则是温度小数值 */
void LM75A_GetTemp(u8 *Tempbuffer) {
u8 buf[2]; // 用于保存从 LM75A 中读取的温度值
u8 t = 0, a = 0;
I2C_READ_BUFFER(LM75A_ADD, 0x00, buf, 2); // 调用 I²C 总线驱动函数,读取 LM75A 设备地址上温度寄存器的 2 个字节数据到 buf 数组当中,, 读取温度
t = buf[0]; // 读取温度整数部分
*Tempbuffer = 0; // 将温度值设置为正数

/* 如果温度值的符号位为 1,表示温度为负值 */
if (t & 0x80) {
*Tempbuffer = 1; // 将温度值设置为负数
t = ~t;
t++; // 将温度数据的原码,取反后加 1 得到补码
}
if (t & 0x01) { a = a + 1; }
if (t & 0x02) { a = a + 2; }
if (t & 0x04) { a = a + 4; }
if (t & 0x08) { a = a + 8; }
if (t & 0x10) { a = a + 16; }
if (t & 0x20) { a = a + 32; }
if (t & 0x40) { a = a + 64; }

Tempbuffer++; // 指针地址自增 1
*Tempbuffer = a; // 然后写入温度数据

a = 0;
t = buf[1]; // 处理温度值的小数部分
if (t & 0x20) { a = a + 12; }
if (t & 0x40) { a = a + 25; }
if (t & 0x80) { a = a + 50; }

Tempbuffer++; // 指针地址自增 1
*Tempbuffer = a; // 写入温度数据的小数部分
}

/* 需要降低功耗时,可以让 LM75A 进入掉电模式,再次调用 LM75A_GetTemp() 就可恢复到正常模式 */
void LM75A_POWERDOWN(void) {
I2C_SEND_BYTE(LM75A_ADD, 0x01, 1); // 调用 I²C 总线驱动函数,将 LM75A 的配置寄存器值置为 1,开启掉电模式
}

OLED 无字库液晶屏

12864 液晶是指128 × 64像素分辨率的显示屏幕,当前实验电路采用的是型号为 OLED0561 的 OLED 液晶显示屏,采用 SH1106 主控芯片,可以进行单色无灰度的显示,屏幕采用 I²C 总线与STM32F103C8T6微控制器进行通信。

diagram-oled

Hardware目录下的OLED0561文件夹内新建ASCII_8x16(八乘十六的 ASCII 码字库)、CHS_16x16.h(十六乘十六的汉字编码)、PIC1.h(图片编码)以及 OLED 显示驱动程序oled0561.holed0561.c源文件。

注意:128 × 64像素的屏幕划分为16 × 8个分辨率为8 × 8的区块,显示英文和数字时需要 2 个8 × 8区块组成的8 × 16分辨率区域,显示汉字则需要 4 个8 × 8区块组成的16 × 16分辨率区域。因此,整个屏幕每行可以显示 16 个英文和数字或者 8 个汉字,一共可以显示 4 行。

oled0561.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/** Hardware/OLED0561/oled0561.h */
#ifndef __OLED_H
#define __OLED_H
#include "i2c.h"
#include "sys.h"

#define OLED0561_ADD 0x78 // 定义 OLED 的固定 I²C 总线地址
#define COM 0x00 // 定义固定的 OLED 指令
#define DAT 0x40 // 定义固定的 OLED 数据

void OLED0561_Init(void); // 初始化 OLED
void OLED_DISPLAY_ON(void); // 打开 OLED 屏幕显示
void OLED_DISPLAY_OFF(void); // 关闭 OLED 屏幕显示
void OLED_DISPLAY_LIT(u8 x); // 设置 OLED 屏幕亮度(0 ~ 255)
void OLED_DISPLAY_CLEAR(void); // 清除屏幕当前显示内容
void OLED_DISPLAY_8x16(u8 x, u8 y, u16 w); // 显示单个 8 * 16 的英文或数字
void OLED_DISPLAY_8x16_BUFFER(u8 row, u8 *str); // 显示 8 * 16 的英文或数字组成的字符串
void OLED_DISPLAY_16x16(u8 x,u8 y,u16 w); // 显示 16 * 16 汉字
void OLED_DISPLAY_PIC1(void); // 显示全屏图片

#endif

oled0561.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
/** Hardware/OLED0561/oled0561.c  */
#include "ASCII_8x16.h"
#include "CHS_16x16.h"
#include "PIC1.h"
#include "oled0561.h"

/** OLED 屏幕初始化函数 */
void OLED0561_Init(void) {
OLED_DISPLAY_OFF(); // 关闭 OLED 屏幕显示,避免上电瞬间屏幕显示乱码
OLED_DISPLAY_CLEAR(); // 清除屏幕当前显示的内容
OLED_DISPLAY_ON(); // 开启 OLED 屏幕显示,设定 OLED 初始状态
}

/** 屏幕显示开启函数 */
void OLED_DISPLAY_ON(void) {
/* 屏幕主控芯片 SH1106 的控制指令*/
u8 buf[28] = {
0xae, // 0xae 关闭显示,
0x00, 0x10, // 开始地址双字节
0xd5, 0x80, // 时钟频率
0xa8, 0x3f, // 复用率
0xd3, 0x00, // 显示偏移量
0XB0, // 写入页位置
0x40, // 显示开始线
0x8d, 0x14, // VCC 电源
0xa1, // 设置段重映射
0xc8, // COM 输出方式
0xda, 0x12, // COM 输出方式
0x81, 0xff, // 对比度
0xd9, 0xf1, // 充电周期
0xdb, 0x30, // VCC电压输出
0x20, 0x00, // 水平寻址设置
0xa4, // 0xa4 正常显示,0xa5 整体点亮
0xa6, // 0xa6 正常显示,0xa7 反色显示
0xaf // 0xaf 打开显示
};

/* 调用 I²C 总线发送函数,第 1 个参数是 OLED 地址,第 2 个是操作指令,第 3 个是待发送的数据,第 4 个是发送数据长度 */
I2C_SAND_BUFFER(OLED0561_ADD, COM, buf, 28);
}

/** 屏幕显示关闭函数 */
void OLED_DISPLAY_OFF(void) {
u8 buf[3] = {
0xae, // 0xae 关闭显示
0x8d, 0x10, // 关闭 OLED 屏幕的 VCC 电源
};

I2C_SAND_BUFFER(OLED0561_ADD, COM, buf, 3); // 将 buf 通过 I²C 总线发送给 OLED 屏幕
}

/** 屏幕亮度设置函数,0 为最低,255 为最高 */
void OLED_DISPLAY_LIT(u8 x) {
I2C_SAND_BYTE(OLED0561_ADD, COM, 0x81); // 选择亮度指令
I2C_SAND_BYTE(OLED0561_ADD, COM, x); // 设置亮度值
}

/** 屏幕显示清除函数 */
void OLED_DISPLAY_CLEAR(void) {
u8 j, t;

// 循环的起始页地址为 0xB0,本质是向屏幕所有显示区域写入 0 使其熄灭
for (t = 0xB0; t < 0xB8; t++) {
I2C_SAND_BYTE(OLED0561_ADD, COM, t); // 选择向哪个区块行写入数据
I2C_SAND_BYTE(OLED0561_ADD, COM, 0x10); // 写入数据起始列地址的高 4 位
I2C_SAND_BYTE(OLED0561_ADD, COM, 0x00); // 写入数据起始列地址的低 4 位

/* 整页内容填充,可以将 132 列修改为 128 列 */
for (j = 0; j < 132; j++) {
I2C_SAND_BYTE(OLED0561_ADD, DAT, 0x00); // 向 132 列写入 0
}
}
}

/** 向屏幕指定位置写入字符,参数 x 定义在哪页写入字符,参数 y 是具体的列坐标, 参数 w 是 ASCII 字符编码*/
void OLED_DISPLAY_8x16(u8 x, u8 y, u16 w) {
u8 j, t, c = 0;
y = y + 2; // 加上偏移量,用于匹配主控芯片起始地址为 0x02 的屏幕

/* 选择向哪个 8 * 8 区块写入数据 */
for (t = 0; t < 2; t++) {
I2C_SAND_BYTE(OLED0561_ADD, COM, 0xb0 + x); // 选择向哪个区块行写入数据,取值范围从 0xB0 到 0xB7
I2C_SAND_BYTE(OLED0561_ADD, COM, y / 16 + 0x10); // 写入数据起始列地址的高 4 位
I2C_SAND_BYTE(OLED0561_ADD, COM, y % 16); // 写入数据起始列地址的低 4 位

/* 向 OLED 发送 ASCII 字符编码 */
for (j = 0; j < 8; j++) {
I2C_SAND_BYTE(OLED0561_ADD, DAT, ASCII_8x16[(w * 16) + c - 512]);
c++;
}

x++; // 页地址加 1 跳入下一行
}
}

/** 字符串显示函数,参数 row 向哪个区块行写入,参数 str 是待写入的内容 */
void OLED_DISPLAY_8x16_BUFFER(u8 row, u8 *str) {
u8 r = 0;

/* 判断字符数据是否发送结束 */
while (*str != '\0') {
OLED_DISPLAY_8x16(row, r * 8, *str++);
r++;
}
}

/** 16*16 汉字显示函数,参数 x 是待写入的行地址,参数 y 是待写入的列地址,参数 w 是待显示汉字的编码 */
void OLED_DISPLAY_16x16(u8 x, u8 y, u16 w) {
u8 j, t, c = 0;

for (t = 0; t < 2; t++) {
I2C_SAND_BYTE(OLED0561_ADD, COM, 0xb0 + x); // 选择向哪个区块行写入数据,取值范围从 0xB0 到 0xB7
I2C_SAND_BYTE(OLED0561_ADD, COM, y / 16 + 0x10); // 写入数据起始列地址的高 4 位
I2C_SAND_BYTE(OLED0561_ADD, COM, y % 16); // 写入数据起始列地址的低 4 位

/* 向 OLED 发送汉字编码 */
for (j = 0; j < 16; j++) {
I2C_SAND_BYTE(OLED0561_ADD, DAT, GB_16[(w * 32) + c]);
c++;
}

x++; // 页地址加 1 跳入下一行
}

I2C_SAND_BYTE(OLED0561_ADD, COM, 0xAF); //开显示
}

/** 全屏图片显示函数 */
void OLED_DISPLAY_PIC1(void) {
u8 m, i;

/* 将数据发送至 8 个区块行 */
for (m = 0; m < 8; m++) {
I2C_SAND_BYTE(OLED0561_ADD, COM, 0xb0 + m); // 选择向哪个区块行写入数据,取值范围从 0xB0 到 0xB7
I2C_SAND_BYTE(OLED0561_ADD, COM, 0x10); // 写入数据起始列地址的高 4 位
I2C_SAND_BYTE(OLED0561_ADD, COM, 0x02); // 写入数据起始列地址的低 4 位

/* 向 OLED 的每个区块行发送 128 列的图片编码数据 */
for (i = 0; i < 128; i++) {
I2C_SAND_BYTE(OLED0561_ADD, DAT, PIC1[i + m * 128]);
}
}
}

ASCII_8x16.h

1
2
3
4
5
6
7
8
9
10
11
12
/** Hardware/OLED0561/ASCII_8x16.h */
#ifndef __ASCII_8x16_H
#define __ASCII_8x16_H

/** ASCII 字模数据表 */
const u8 ASCII_8x16[] = {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
/* ... ... ... ... ... */
0x80,0xC0,0x60,0x30,0x60,0xC0,0x80,0x00,0x07,0x07,0x04,0x04,0x04,0x07,0x07,0x00
};

#endif

CHS_16x16.h

1
2
3
4
5
6
7
8
9
10
11
12
/** Hardware/OLED0561/CHS_16x16.h */
#ifndef __CHS_16x16_H
#define __CHS_16x16_H

/** 汉字字模数据表 */
uc8 GB_16[] = {
0x10,0x22,0x64,0x0C,0x80,0x08,0x49,0x4A,
/* ... ... ... ... ... */
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
};

#endif

PIC1.h

1
2
3
4
5
6
7
8
9
10
11
12
/** Hardware/OLED0561/PIC1.h */
#ifndef __PIC1_H
#define __PIC1_H

/** 汉字字模数据表 */
uc8 PIC1[] = {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
/* ... ... ... ... ... */
0x7F,0x7F,0x77,0x74,0x67,0x67,0x63,0x00
};

#endif

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/** User/main.c */
#include "delay.h"
#include "lm75a.h"
#include "oled0561.h"
#include "stm32f10x.h"
#include "sys.h"

int main(void) {
u8 buffer[3];
delay_ms(100); // 此处必须调用延时函数,等待相关元器件上电并且工作正常
RCC_Configuration(); // 系统时钟初始化

I2C_Configuration(); // 初始化 I²C 总线
LM75A_GetTemp(buffer); // 读取温度传感器 LM75A 的数据
OLED0561_Init(); // 初始化 OLED 显示屏幕
OLED_DISPLAY_LIT(100); // 设置 OLED 屏幕亮度

/* OLED 全屏显示开机图片 */
OLED_DISPLAY_PIC1(); // 显示 OLED 全屏图片
delay_ms(1000); // 调用延时函数使图片显示停留 1 秒钟
OLED_DISPLAY_CLEAR(); // 清空 OLED 屏幕显示

/* OLED 显示数字和字母组成的温度值 */
OLED_DISPLAY_8x16_BUFFER(0, " Hank"); // 显示字符串 Hank
OLED_DISPLAY_8x16_BUFFER(6, " Temp:"); // 显示字符串 Temp:

/* OLED 显示汉字 */
OLED_DISPLAY_16x16(2, 2 * 16, 0); // 在第 2 个区块行的 2 * 16 列显示第 0 号汉字
OLED_DISPLAY_16x16(2, 3 * 16, 1); // 在第 2 个区块行的 2 * 16 列显示第 1 号汉字
OLED_DISPLAY_16x16(2, 4 * 16, 2); // 在第 2 个区块行的 2 * 16 列显示第 2 号汉字
OLED_DISPLAY_16x16(2, 5 * 16, 3); // 在第 2 个区块行的 2 * 16 列显示第 3 号汉字

while (1) {
LM75A_GetTemp(buffer); //读取LM75A的温度数据

/* 如果 buffer 缓冲区第 1 个元素等于 1 */
if (buffer[0]) {
OLED_DISPLAY_8x16(6, 7 * 8, '-'); // 那么就显示负号
}

/* 显示温度值 */
OLED_DISPLAY_8x16(6, 8 * 8, buffer[1] / 10 + 0x30); // 整数部分十位
OLED_DISPLAY_8x16(6, 9 * 8, buffer[1] % 10 + 0x30); // 整数部分个位
OLED_DISPLAY_8x16(6, 10 * 8, '.'); // 显示小数点
OLED_DISPLAY_8x16(6, 11 * 8, buffer[2] / 10 + 0x30); // 小数部分的个位
OLED_DISPLAY_8x16(6, 12 * 8, buffer[2] % 10 + 0x30); // 小数部分的十位
OLED_DISPLAY_8x16(6, 13 * 8, 'C'); // 摄氏度符号

delay_ms(200); // 调用延时函数,让显示内容停留一段时间
}
}

Relay 继电器

首先,将实验电路上继电器对应的P26跳线帽短接,然后将触摸按键对应的P10跳线帽短接,本实验使用触摸按键来进行继电器开关控制,并使用ULN2003A达林顿芯片来驱动继电器。

diagram-oled

上面的电路图当中,继电器 1 和 2 分别连接至STM32F103C8T6PA13PA14引脚(上电时 JTAG 电路将这两个引脚默认为 JTAG 模式,本实验中需要将其手动设置为 GPIO 模式),然后 PA13 和 PA14 分别通过P26跳线帽连接至ULN2003达林顿管芯片的IN5IN6引脚(由于其内部使用了非门电路,其左边输入与右边输出的状态相反),最后其OUT5/J1OUT6/J2引脚分别用于控制原理图中的继电器U13U12

这样,当PA13PA14输出高电平时,经过ULN2003反向之后,对应输出低电平,由于继电器另一端连接在5V高电平上,从而使继电器线圈吸合工作,导致OUT1AOUT1C触点导通或者OUT2AOUT2C触点导通;反之,如果PA13PA14输出低电平,则OUT1AOUT1B导通或者OUT2AOUT2B导通。此外,实验电路还在继电器U13U12上连接了LED9LED10两枚 LED 指示灯,以及相应的R28R29两只限流电阻。

realy.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** Hardware/REALY/realy.h */
#ifndef __RELAY_H
#define __RELAY_H
#include "sys.h"

#define RELAYPORT GPIOA // 定义 GPIO 组
#define RELAY1 GPIO_Pin_14 // 定义 GPIO 引脚
#define RELAY2 GPIO_Pin_13 // 定义 GPIO 引脚

void RELAY_Init(void); // 继电器初始化函数
void RELAY_1(u8 c); // 继电器 1 控制函数
void RELAY_2(u8 c); // 继电器 2 控制函数

#endif

realy.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/** Hardware/REALY/realy.c  */
#include "relay.h"

/** 继电器初始化函数 */
void RELAY_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOE, ENABLE); // 使能 APB2 外设时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // 启动 AFIO 重映射功能时钟
GPIO_InitStructure.GPIO_Pin = RELAY1 | RELAY2; // 选择 GPIO 端口
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 设置 GPIO 为推挽输出方式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置 GPIO 频率为 50MHz
GPIO_Init(RELAYPORT, &GPIO_InitStructure);

GPIO_PinRemapConfig(GPIO_Remap_SWJ_Disable, ENABLE); // 禁用 JTAG 模式
GPIO_ResetBits(RELAYPORT, RELAY1 | RELAY2); // 初始化继电器状态为低电平 0,使其处于关闭状态
}

/** 继电器 1 控制函数,参数 c 为 0 时继电器断开,为 1 时吸合 */
void RELAY_1(u8 c) {
GPIO_WriteBit(RELAYPORT, RELAY1, (BitAction)(c)); // 写入 GPIO 状态
}

/** 继电器 2 控制函数,参数 c 为 0 时继电器断开,为 1 时吸合 */
void RELAY_2(u8 c) {
GPIO_WriteBit(RELAYPORT, RELAY2, (BitAction)(c)); // 写入 GPIO 状态
}

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/** User/main.c */
#include "delay.h"
#include "relay.h"
#include "stm32f10x.h"
#include "sys.h"
#include "touch_key.h"

int main(void) {
RCC_Configuration(); // 配置时钟
TOUCH_KEY_Init(); // 初始化触摸按键
RELAY_Init(); // 初始化继电器

while (1) {
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A))
RELAY_1(1); // 触摸按键 A 按下时,继电器 1 吸合
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_B))
RELAY_1(0); // 触摸按键 B 按下时,继电器 1 放开
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_C))
RELAY_2(1); // 触摸按键 C 按下时,继电器 2 吸合
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_D))
RELAY_2(0); // 触摸按键 D 按下时,继电器 2 放开
}
}

ULN2003 驱动步进电机

短接实验电路上步进电机对应的P27跳线帽,以及触摸按键对应的P10跳线帽,本实验将会采用触摸按键对步进电机进行控制。步进电机是一种可以通过脉冲信号数量旋转特定角度的元器件,步进电机拥有蓝A+、粉A-、黄B+、橙B-、红COM五条连接线,向 COM 端输入高电平,其它端口输出相应的低电平,就可以让步进电机开始旋转(四拍模式下电机旋转 90 度,八拍模式下电机旋转 45 度),宣传完成之后需要及时断电,否则会造成某个线圈长时间通电烧毁。

当前实验电路采用的是五线四相步进电机(5 条接线,4 组线圈),通过STM32F103C8T6PB3PB4PB8PB9四个引脚进行控制,它们分别连接至ULN2003达林顿芯片输入端的MO_1/IN1MO_2/IN2MO_3/IN3MO_4/IN4引脚,而输出端则连接至P20插座上步进电机的5 条导线,具体接线方式请参考前一小节继电器相关的电路图。

按键控制步进电机

step_motor.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/** Hardware/STEP_MOTOR/step_motor.h */
#ifndef __STEP_MOTOR_H
#define __STEP_MOTOR_H
#include "delay.h"
#include "sys.h"

#define STEP_MOTOR_PORT GPIOB // 定义 GPIO 分组
#define STEP_MOTOR_A GPIO_Pin_3 // 步进电机控制相关的 GPIO 引脚
#define STEP_MOTOR_B GPIO_Pin_4 // 步进电机控制相关的 GPIO 引脚
#define STEP_MOTOR_C GPIO_Pin_8 // 步进电机控制相关的 GPIO 引脚
#define STEP_MOTOR_D GPIO_Pin_9 // 步进电机控制相关的 GPIO 引脚

void STEP_MOTOR_Init(void); // 步进电机初始化函数
void STEP_MOTOR_OFF(void); // 步进电机关闭函数

/** 步进电机驱动函数 */
void STEP_MOTOR_4S(u8 speed);
void STEP_MOTOR_4R(u8 speed);
void STEP_MOTOR_4L(u8 speed);
void STEP_MOTOR_8R(u8 speed);
void STEP_MOTOR_8L(u8 speed);

#endif

step_motor.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
/** Hardware/STEP_MOTOR/step_motor.c  */
#include "step_motor.h"

/** 步进电机初始化函数 */
void STEP_MOTOR_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOE, ENABLE); // 使能 APB2 外设时钟

RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // 启动 AFIO 重映射功能时钟
GPIO_InitStructure.GPIO_Pin = STEP_MOTOR_A | STEP_MOTOR_B | STEP_MOTOR_C | STEP_MOTOR_D; // 选择 GPIO 引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 设置 GPIO 为推挽输出方式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置 GPIO 频率为 50MHz
GPIO_Init(STEP_MOTOR_PORT, &GPIO_InitStructure);

GPIO_PinRemapConfig(GPIO_Remap_SWJ_Disable, ENABLE); // 禁用 JTAG 模式
STEP_MOTOR_OFF(); // 初始化继电器为关闭状态
}

/** 步进电机关闭函数 */
void STEP_MOTOR_OFF(void) {
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_B | STEP_MOTOR_C | STEP_MOTOR_D); // 将步进电机相关的 GPIO 全部置为低电平 0
}

/** 步进电机制动函数 */
void STEP_MOTOR_4S(u8 speed) {
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_C); // AC 设置为低电平
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_B | STEP_MOTOR_D); // BD 设置为高电平
delay_ms(speed); // 延时
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_B | STEP_MOTOR_C | STEP_MOTOR_D); // ABCD 设置为低电平
delay_ms(speed); // 延时
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_B | STEP_MOTOR_D); // BD 设置为低电平
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_C); // AC 设置为高电平
delay_ms(speed); // 延时
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_B | STEP_MOTOR_C | STEP_MOTOR_D); // ABCD 设置为低电平
delay_ms(speed); // 延时
STEP_MOTOR_OFF(); // 步进电机断电,防止过热
}

/** 4 拍方式顺时针转动函数,转动速度快,力度小*/
void STEP_MOTOR_4R(u8 speed) {
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_C | STEP_MOTOR_D); // C 设置为低电平 0
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_B); // A 设置为高电平 1
delay_ms(speed); // 延时
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_D); // A D 设置为低电平 0
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_B | STEP_MOTOR_C); // B C 设置为高电平 1
delay_ms(speed); // 延时
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_B); // A B 设置为低电平 0
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_C | STEP_MOTOR_D); // C D 设置为高电平 1
delay_ms(speed); // 延时
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_B | STEP_MOTOR_C); // B C 设置为低电平 0
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_D); // A D 设置为高电平 1
delay_ms(speed); // 延时
STEP_MOTOR_OFF(); // 步进电机断电,防止过热
}

/** 4 拍方式逆时针转动函数,转动速度快,力度小*/
void STEP_MOTOR_4L(u8 speed) { //电机逆时针,4拍,速度快,力小
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_B); // 设置为低电平 0
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_C | STEP_MOTOR_D); // 设置为高电平 1
delay_ms(speed); // 延时
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_D); // 设置为低电平 0
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_B | STEP_MOTOR_C); // 设置为高电平 1
delay_ms(speed); // 延时
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_C | STEP_MOTOR_D); // 设置为低电平 0
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_B); // 设置为高电平 1
delay_ms(speed); // 延时
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_B | STEP_MOTOR_C); // 设置为低电平 0
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_D); // 设置为高电平 1
delay_ms(speed); // 延时
STEP_MOTOR_OFF(); // 步进电机断电,防止过热
}

/** 8 拍方式顺时针转动函数,转动速度慢,力度大,角度小 */
void STEP_MOTOR_8R(u8 speed) {
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_B | STEP_MOTOR_C | STEP_MOTOR_D); // B C D设置为低电平 0
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_A); // A 设置为高电平 1
delay_ms(speed); // 延时
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_C | STEP_MOTOR_D); // C D 设置为低电平 0
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_B); // A B 设置为高电平 1
delay_ms(speed); // 延时
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_C | STEP_MOTOR_D); // A C D 设置为低电平 0
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_B); // B 设置为高电平 1
delay_ms(speed); // 延时
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_D); // A D 设置为低电平 0
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_B | STEP_MOTOR_C); // B C 设置为高电平 1
delay_ms(speed); // 延时
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_B | STEP_MOTOR_D); // A B D 设置为低电平 0
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_C); // C 设置为高电平 1
delay_ms(speed); // 延时
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_B); // A B 设置为低电平 0
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_C | STEP_MOTOR_D); // C D 设置为高电平 1
delay_ms(speed); // 延时
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_B | STEP_MOTOR_C); // A B C 设置为低电平 0
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_D); // D 设置为高电平 1
delay_ms(speed); // 延时
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_B | STEP_MOTOR_C); // B C 设置为低电平 0
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_D); // A D 设置为高电平 1
delay_ms(speed); // 延时
STEP_MOTOR_OFF(); // 步进电机断电,防止过热
}

/** 8 拍方式逆时针转动函数,转动速度慢,力度大,角度小 */
void STEP_MOTOR_8L(u8 speed) {
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_B | STEP_MOTOR_C); // A B C 设置为低电平 0
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_D); // D 设置为高电平 1
delay_ms(speed); // 延时
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_B); // A B 设置为低电平 0
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_C | STEP_MOTOR_D); // C D 设置为高电平 1
delay_ms(speed); // 延时
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_B | STEP_MOTOR_D); // A B D 设置为低电平 0
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_C); // C 设置为高电平 1
delay_ms(speed); // 延时
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_D); // A D 设置为低电平 0
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_B | STEP_MOTOR_C); // B C 设置为高电平 1
delay_ms(speed); // 延时
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_C | STEP_MOTOR_D); // A C D 设置为低电平 0
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_B); // B 设置为高电平 1
delay_ms(speed); // 延时
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_C | STEP_MOTOR_D); // C D 设置为低电平 0
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_B); // A B 设置为高电平 1
delay_ms(speed); // 延时
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_B | STEP_MOTOR_C | STEP_MOTOR_D); // B C D 设置为低电平 0
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_A); // A 设置为高电平 1
delay_ms(speed); // 延时
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_B | STEP_MOTOR_C); // B C 设置为低电平 0
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_D); // A D 设置为高电平 1
delay_ms(speed); // 延时
STEP_MOTOR_OFF(); // 步进电机断电,防止过热
}

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/** User/main.c */
#include "delay.h"
#include "relay.h"
#include "step_motor.h"
#include "stm32f10x.h"
#include "sys.h"
#include "touch_key.h"

int main(void) {
RCC_Configuration(); // 时钟设置
TOUCH_KEY_Init(); // 初始化触摸按键
RELAY_Init(); // 初始化继电器
STEP_MOTOR_Init(); // 初始化步进电机

while (1) {
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A))
STEP_MOTOR_4R(3); // 触摸按键 A 按下,步进电机 4 步右转
else if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_B))
STEP_MOTOR_4L(3); // 触摸按键 B 按下,步进电机 4 步左转
else if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_C))
STEP_MOTOR_8R(3); // 触摸按键 C 按下,步进电机 8 步右转
else if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_D))
STEP_MOTOR_8L(3); // 触摸按键 D 按下,步进电机 8 步左转
else
STEP_MOTOR_OFF(); // 无按键按下时,步进电机断电,防止损坏线圈
}
}

步进电机步数控制

step_motor.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/** Hardware/STEP_MOTOR/step_motor.h */
#ifndef __STEP_MOTOR_H
#define __STEP_MOTOR_H
#include "delay.h"
#include "sys.h"

extern u8 STEP; // 单步计数全局变量,用于标识步进电机当前运行至哪一步
#define STEP_MOTOR_PORT GPIOB // 定义 GPIO 分组
#define STEP_MOTOR_A GPIO_Pin_3 // 步进电机控制相关的 GPIO 引脚
#define STEP_MOTOR_B GPIO_Pin_4 // 步进电机控制相关的 GPIO 引脚
#define STEP_MOTOR_C GPIO_Pin_8 // 步进电机控制相关的 GPIO 引脚
#define STEP_MOTOR_D GPIO_Pin_9 // 步进电机控制相关的 GPIO 引脚

void STEP_MOTOR_Init(void); // 步进电机初始化函数
void STEP_MOTOR_OFF(void); // 步进电机关闭函数

void STEP_MOTOR_8A(u8 a, u16 speed); // 单步运行函数
void STEP_MOTOR_NUM(u8 RL, u16 num, u8 speed); // 按步数运行函数
void STEP_MOTOR_LOOP(u8 RL, u8 LOOP, u8 speed); // 按圈数运行函数

#endif

step_motor.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/** Hardware/STEP_MOTOR/step_motor.c  */
#include "step_motor.h"

u8 STEP;

/** 步进电机初始化函数 */
void STEP_MOTOR_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOE, ENABLE); // 使能 APB2 外设时钟

RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // 启动 AFIO 重映射功能时钟
GPIO_InitStructure.GPIO_Pin = STEP_MOTOR_A | STEP_MOTOR_B | STEP_MOTOR_C | STEP_MOTOR_D; // 选择 GPIO 引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 设置 GPIO 为推挽输出方式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置 GPIO 频率为 50MHz
GPIO_Init(STEP_MOTOR_PORT, &GPIO_InitStructure);

GPIO_PinRemapConfig(GPIO_Remap_SWJ_Disable, ENABLE); // 禁用 JTAG 模式
STEP_MOTOR_OFF(); // 初始化继电器为关闭状态
}

/** 步进电机关闭函数 */
void STEP_MOTOR_OFF(void) {
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_B | STEP_MOTOR_C | STEP_MOTOR_D); // 将步进电机相关的 GPIO 全部置为低电平 0
}

/** 单步 8 拍控制函数,参数 a 是电机步数(即转动至指定拍数位置),参数 speed 是旋转速度 */
void STEP_MOTOR_8A(u8 a, u16 speed) {
switch (a) {
case 0:
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_B | STEP_MOTOR_C | STEP_MOTOR_D); // B C D 设置为低电平
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_A); // A 设置为高电平
break;
case 1:
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_C | STEP_MOTOR_D); // C D 设置为低电平
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_B); // A B 设置为高电平
break;
case 2:
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_C | STEP_MOTOR_D); // A C D 设置为低电平
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_B); // B 设置为高电平
break;
case 3:
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_D); // A D 设置为低电平
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_B | STEP_MOTOR_C); // B C 设置为高电平
break;
case 4:
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_B | STEP_MOTOR_D); // A B D 设置为低电平
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_C); // C 设置为高电平
break;
case 5:
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_B); // A B 设置为低电平
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_C | STEP_MOTOR_D); // C D 设置为高电平
break;
case 6:
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_B | STEP_MOTOR_C); // A B C 设置为低电平
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_D); // D 设置为高电平
break;
case 7:
GPIO_ResetBits(STEP_MOTOR_PORT, STEP_MOTOR_B | STEP_MOTOR_C); // B C 设置为低电平
GPIO_SetBits(STEP_MOTOR_PORT, STEP_MOTOR_A | STEP_MOTOR_D); // A D 设置为高电平
break;
default:
break;
}

delay_ms(speed); // 延时
STEP_MOTOR_OFF(); // 步进电机断电,防止过热
}

/** 按步数运行函数,参数 RL 是旋转方向(为 1 时顺时针,为 0 时逆时针),参数 num 是转动的步数,参数 speed 是旋转速度 */
void STEP_MOTOR_NUM(u8 RL, u16 num, u8 speed) {
u16 i;
for (i = 0; i < num; i++) {
/* 判断方向*/
if (RL == 1) {
STEP++;
if (STEP > 7)
STEP = 0;
} else {
if (STEP == 0)
STEP = 8;
STEP--;
}
STEP_MOTOR_8A(STEP, speed);
}
}

/** 按圈数运行函数,参数 RL 是转动方向,参数 LOOP 是旋转圈数,参数 speed 是旋转速度 */
void STEP_MOTOR_LOOP(u8 RL, u8 LOOP, u8 speed) {
STEP_MOTOR_NUM(RL, LOOP * 4076, speed); // 旋转 4076 拍可以让步进电机输出轴刚好旋转 1 圈
}

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/** User/main.c */
#include "delay.h"
#include "relay.h"
#include "step_motor.h"
#include "stm32f10x.h"
#include "sys.h"
#include "touch_key.h"

int main(void) {
RCC_Configuration(); // 时钟设置
TOUCH_KEY_Init(); // 初始化触摸按键
RELAY_Init(); // 初始化继电器
STEP_MOTOR_Init(); // 初始化步进电机

while (1) {
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A))
STEP_MOTOR_LOOP(0, 1, 3); // 右转 1 圈
else if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_B))
STEP_MOTOR_LOOP(1, 1, 3); // 左转 1 圈
else if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_C))
STEP_MOTOR_NUM(0, 100, 3); // 右转 100 步
else if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_D))
STEP_MOTOR_NUM(1, 100, 3); // 左转 100 步
else
STEP_MOTOR_OFF(); // 无按键按下时,步进电机断电,防止损坏线圈
}
}

RS232 串行通信

短接实验电路上RS232对应的P13跳线帽,并且断开RS485对应的P22跳线帽,以及右上角网络标号 MY1680 对应的PB10PB11跳线帽。

注意:将 RS232 的 DB9 接头最上面一排的第 2、3 针(即 RX 与 TX)用杜邦线短接在一起,可以方便的进行串口收发测试。

上面的电路图当中,STM32F103C8T6USART3引脚RX/PB10TX/PB11,分别通过跳线座P13连接至SP3232电平转换芯片的T1INR1OUT引脚,然后经过T1OUTR1IN连接至DB9插头的第 2 输入和第 3 输出针脚。此外,还将DB9插头的第 5 脚连接至GND

usart.h

1
2
/** Hardware/USART/usart.h */
#define EN_USART3 1 // 使能 USART3

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/** User/main.c */
#include "delay.h"
#include "oled0561.h"
#include "relay.h"
#include "stm32f10x.h"
#include "sys.h"
#include "touch_key.h"
#include "usart.h"

int main(void) {
u8 a;
delay_ms(100); // 等待相关元器件上电并且工作正常
RCC_Configuration(); // 初始化系统时钟
TOUCH_KEY_Init(); // 初始化触摸按键
RELAY_Init(); // 初始化继电器

/* 显示文字到 OLED 屏幕 */
I2C_Configuration(); // 初始化 I²C 总线
OLED0561_Init(); // 初始化 OLED
OLED_DISPLAY_8x16_BUFFER(0, " Hank ");
OLED_DISPLAY_8x16_BUFFER(2, " RS232 Test ");
OLED_DISPLAY_8x16_BUFFER(6, "TX: RX: ");

USART3_Init(115200); // 初始化 USART3

while (1) {
/* 向 RS232 发送字符 A,并且显示到 OLED 屏幕*/
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A)) {
USART3_printf("%c", 'A');
OLED_DISPLAY_8x16(6, 4 * 8, 'A');
}
/* 向 RS232 发送字符 B,并且显示到 OLED 屏幕 */
else if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_B)) {
USART3_printf("%c", 'B');
OLED_DISPLAY_8x16(6, 4 * 8, 'B');
}
/* 向 RS232 发送字符 C,并且显示到 OLED 屏幕 */
else if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_C)) {
USART3_printf("%c", 'C');
OLED_DISPLAY_8x16(6, 4 * 8, 'C');
}
/* 向 RS232 发送字符 D,并且显示到 OLED 屏幕 */
else if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_D)) {
USART3_printf("%c", 'D');
OLED_DISPLAY_8x16(6, 4 * 8, 'D');
}

/* 采用查询方式(查询串口待处理标志位)接收数据 */
if (USART_GetFlagStatus(USART3, USART_FLAG_RXNE) != RESET) {
a = USART_ReceiveData(USART3); // 读取接收到的数据
OLED_DISPLAY_8x16(6, 11 * 8, a); // 显示到 OLED 上面
}
}
}

注意:RS232 串口与之前小节介绍的 USART 串行通信没有本质区别,唯一的差别在于硬件上将 TTL 电平转换为了 RS232 电平,而这样的转换并不影响之前编写的 USART 串行驱动程序的工作。

RS485 串行通信

RS485 通信依然是基于STM32F103C8T6的 USART 串行接口,其相比 RS232 可靠性更高传输距离更远(USART 最大传输距离 2 米,RS232 最大传输距离 20 米,RS485 最大传输距离达到 1000 米)。开始进一步学习之前,需要对实验电路当中的跳线进行设置,将实验电路上的RS232对应的P13跳线帽断开,然后将RS485对应的P22跳线帽短接,最后还需要断开右上角网络标号MY1680对应的PB10PB11跳线帽。

注意: 由于 RS485 采用差分电平(±2V ~ 6V)通信机制,无法像 RS232 示例程序那样将 RX 和 TX 短接起来进行测试,因此必须借助于一个 USB 转 RS485 设备才能进行正确的完成通信实验。

RS485 的TXRX接口分别连接至STM32F103C8T6上面 USART3 所复用的PB11PB10引脚,另外一个收发选择接口RE则连接至一个普通的PA8引脚(当RE输出高电平时,后续SP3485电平转换芯片处于发送状态,反之输出低电平就处于接收状态)。RS485 的TXRXRE经过P22跳线插座以后,分别连接到SP3485电平转换芯片的RODI以及REDE引脚,然后经由AB引脚输出至标号为P23的接线端子。

注意: SP3485 电平转换芯片输出的 A 和 B 两条信号线之间连接了一枚360Ω的电阻R30,然后 A 和 B 各自连接了一枚360Ω的上拉电阻R31和下拉电阻R32,这些电阻的作用是为了保证通信的稳定性。

rs485.h

1
2
3
4
5
6
7
8
9
10
11
12
/** Hardware/RS485/rs485.h */
#ifndef __RS485_H
#define __RS485_H
#include "sys.h"

#define RS485PORT GPIOA // 定义 GPIO 组
#define RS485_RE GPIO_Pin_8 // 定义 GPIO 接口,用于切换 SP3485 的收发状态

void RS485_Init(void); // 初始化
void RS485_printf(char *fmt, ...); // RS485 通信发送函数

#endif

rs485.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/** Hardware/RS485/rs485.c  */
#include "rs485.h"
#include "sys.h"
#include "usart.h"

/** RS485 接口初始化函数 */
void RS485_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = RS485_RE; // 选择 GPIO 引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 选择 GPIO 工作方式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置 GPIO 工作频率为 50MHz
GPIO_Init(RS485PORT, &GPIO_InitStructure);

GPIO_ResetBits(RS485PORT, RS485_RE); // RE 引脚用于控制收/发状态,为 1 时发送,为 0 时接收
}

/** RS485 通信发送函数,参数 fmt 是待发送的字符 */
void RS485_printf(char *fmt, ...) {
char buffer[USART3_REC_LEN + 1]; // 数据的长度
u8 i = 0;
va_list arg_ptr;

GPIO_SetBits(RS485PORT, RS485_RE); // 设置为高电平,让 SP3485 切换至发送状态

va_start(arg_ptr, fmt);
vsnprintf(buffer, USART3_REC_LEN + 1, fmt, arg_ptr);
while ((i < USART3_REC_LEN) && (i < strlen(buffer))) {
USART_SendData(USART3, (u8)buffer[i++]);
while (USART_GetFlagStatus(USART3, USART_FLAG_TC) == RESET)
;
}
va_end(arg_ptr);

GPIO_ResetBits(RS485PORT, RS485_RE); // 设置为低电平,让 SP3485 切换至接收状态
}

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/** User/main.c */
#include "delay.h"
#include "oled0561.h"
#include "relay.h"
#include "rs485.h"
#include "stm32f10x.h"
#include "sys.h"
#include "touch_key.h"
#include "usart.h"

int main(void) {
u8 a;
delay_ms(100); // 等待相关元器件上电并且工作正常
RCC_Configuration(); // 初始化系统时钟
TOUCH_KEY_Init(); // 初始化触摸按键
RELAY_Init(); // 初始化继电器

/* 显示文字到 OLED 屏幕 */
I2C_Configuration(); // 初始化 I²C 总线
OLED0561_Init(); // 初始化 OLED
OLED_DISPLAY_8x16_BUFFER(0, " Hank ");
OLED_DISPLAY_8x16_BUFFER(2, " RS485 Test ");
OLED_DISPLAY_8x16_BUFFER(6, "TX: RX: ");

/* 初始化 RS485 接口,必须遵循如下先后顺序 */
USART3_Init(115200); // 必须先调用 USART3 初始化函数
RS485_Init(); // 然后再调用 RS485 初始化函数

while (1) {
/* 向 RS485 发送字符 A,并且显示到 OLED 屏幕*/
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A)) {
RS485_printf("%c", 'A');
OLED_DISPLAY_8x16(6, 4 * 8, 'A');
}
/* 向 RS485 发送字符 B,并且显示到 OLED 屏幕 */
else if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_B)) {
RS485_printf("%c", 'B');
OLED_DISPLAY_8x16(6, 4 * 8, 'B');
}
/* 向 RS485 发送字符 C,并且显示到 OLED 屏幕 */
else if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_C)) {
RS485_printf("%c", 'C');
OLED_DISPLAY_8x16(6, 4 * 8, 'C');
}
/* 向 RS485 发送字符 D,并且显示到 OLED 屏幕 */
else if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_D)) {
RS485_printf("%c", 'D');
OLED_DISPLAY_8x16(6, 4 * 8, 'D');
}

/* 依然采用查询方式接收数据 */
if (USART_GetFlagStatus(USART3, USART_FLAG_RXNE) != RESET) {
a = USART_ReceiveData(USART3); // 读取接收到的数据
OLED_DISPLAY_8x16(6, 11 * 8, a); // 显示到 OLED 上面
}
}
}

CAN 总线通信

由于实验电路当中,数码管占用了 CAN 总线实验需要的 GPIO 引脚,所以需要短接实验电路上 CAN 总线对应的P24跳线帽,并且断开数码管对应的P9跳线帽。

上面电路图当中的TJA1050是 NXP 推出的 CAN 协议控制器与物理总线之间的接口转换芯片,采用了5V电压驱动。

CAN 总线的CAN_TXCAN_RX分别连接至STM32F103C8T6PA12PA11引脚,然后经过 P24 跳线插座连接至 CAN 总线收发器 TJA1050 芯片的TXDRXD引脚,然后经由CANHCANL引脚输出,两条输出线之前还并联了一枚120ΩR33电阻用于稳定总线工作,最后连接至P25接线端子。

协议分析

CAN(控制器区域网络,Controller Area Network)是一种标准化的串行通信协议,该协议经过 ISO 标准化以后分为ISO11898高速通信标准(125Kbps ~ 1Mbps,有效距离 40 米)和ISO11519-2低速通信标准(10Kbps ~ 125Kbps,有效距离 1 千米)。如果 CAN 总线上只挂载了两个设备,那么可以简单的将其视为 USART 串口来使用。而如果挂载了多个设备,则需要使用到 CAN 总线协议的邮箱、识别符、过滤器等高级特性。

CAN 的优点在于,总线空闲时所有单元都可以发送数据,当 2 个以上的总线设备同时发送数据时,根据标识符来判断相应优先级,此时会对各条消息 ID 的每个位逐个进行仲裁,优先级最高的单元可以继续数据发送,较低的单元则立刻停止发送并进入接收工作状态。

注意:CAN 总线上的设备都必须基于相同的波特率进行通信。

CAN总线发送部分包含有如下的 5 个概念:

  • 报文:CAN 设备发送出去的完整数据信息,一条数据帧或者遥控帧的报文格式如下所示:起始位 | 标识符 | 控制位 | 数据内容 | CRC校验位 | ACK位 | 结束位
  • 发送邮箱:用于报文发送的调度器,STM32F103C8T6拥有 3 个发送邮箱,待发送数据会放入优先级最高的空邮箱,已放入数据的邮箱将会处于正在或等待发送的状态;
  • 帧种类:不同用途的报文种类,分为数据帧(用于发送单元向接收单元传递数据)、遥控帧(用于接收单元向具有相同标识的发送单元请求数据)、错误帧(用于检测到错误时向其它单元通知错误)、过载帧(用于接收单元通知其它尚未做好接收准备的单元)、帧间帧(用于分离数据/遥控帧与前面的其它帧);
  • 标识符:属于 CAN 报文的一部分,用于 CAN 总线设备判断数据是否发给自身,标识符不符的报文会被过滤器移除;
  • 帧格式:报文当中包含的内容,数据帧遥控帧都具有标准(11 个位标识符)和扩展(29 个位标识符)两种格式;

进行数据接收时,与过滤器用来过滤掉与标识符不匹配的报文)匹配的报文会被放入FIFO 邮箱先入先出),STM32F103C8T6拥有 2 个 FIFO 接收邮箱,每个 FIFO 拥有 3 层深度。当过滤器处于列表模式时,必须标识符每个位都完全相同才视为匹配;处于屏蔽位模式时,屏蔽组为1的对应标识符位必须匹配,为0的对应标识符位无效。过滤器的优先级基于以下规则来判定:

  1. 位宽为32位的过滤器,优先级高于位宽为16位的过滤器;
  2. 位宽相同的过滤器,标识符列表模式的优先级高于屏蔽位模式
  3. 位宽与模式都相同的过滤器,优先级由过滤器号决定,号码越小优先级越高。

注意:进行接下来的 CAN 总线收发实验之前,需要先向 Lib 文件夹添加stm32f10x_can.c标准外设库文件。

can.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/** Hardware/CAN/can.h */
#ifndef __CAN_H
#define __CAN_H
#include "sys.h"

#define CAN_INT_ENABLE 0 // 是否开启 CAN 总线接收中断模式(1 开启,0 关闭)

/* 设置波特率 = PCLK1 ÷ ((1+8+7)*9)) = 36MHz ÷ 16 ÷ 9 = 250Kbits */
#define tsjw CAN_SJW_1tq // 取值范围 1 ~ 4
#define tbs1 CAN_BS1_8tq // 取值范围 1 ~ 16
#define tbs2 CAN_BS2_7tq // 取值范围 1 ~ 8

#define brp 9 // 设定了 1 个时间单位的长度为 9

u 8 CAN1_Configuration(void); // CAN 总线初始化函数
u8 CAN_Send_Msg(u8 *msg, u8 len); // CAN 总线数据发送函数
u8 CAN_Receive_Msg(u8 *buf); // CAN 总线数据接收函数

#endif

can.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
/** Hardware/CAN/can.c */
#include "can.h"

/** CAN 总线初始化函数,返回 0 表示成功,其它表示失败 */
u8 CAN1_Configuration(void) {
GPIO_InitTypeDef GPIO_InitStructure;
CAN_InitTypeDef CAN_InitStructure;
CAN_FilterInitTypeDef CAN_FilterInitStructure;

#if CAN_INT_ENABLE
NVIC_InitTypeDef NVIC_InitStructure;
#endif
/* 设置时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 使能 GPIOA 分组时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); // 使能 CAN1 总线时钟

/* 设置 GPIO */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出模式
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入模式
GPIO_Init(GPIOA, &GPIO_InitStructure);

/* 设置 CAN 总线功能 */
CAN_InitStructure.CAN_TTCM = DISABLE; // 关闭时间触发通信模式
CAN_InitStructure.CAN_ABOM = DISABLE; // 关闭软件自动离线管理
CAN_InitStructure.CAN_AWUM = DISABLE; // 关闭睡眠模式通过软件唤醒
CAN_InitStructure.CAN_NART = ENABLE; // 开启禁止报文自动传送
CAN_InitStructure.CAN_RFLM = DISABLE; // 关闭报文锁定
CAN_InitStructure.CAN_TXFP = DISABLE; // 优先级由报文标识符确定
CAN_InitStructure.CAN_Mode = CAN_Mode_Normal; // CAN_Mode_Normal 普通模式,CAN_Mode_LoopBack 回环模式

/* 设置 CAN 总线波特率 */
CAN_InitStructure.CAN_SJW = tsjw;
CAN_InitStructure.CAN_BS1 = tbs1;
CAN_InitStructure.CAN_BS2 = tbs2;
CAN_InitStructure.CAN_Prescaler = brp;
CAN_Init(CAN1, &CAN_InitStructure); // 初始化 CAN1

/* 设置 CAN 总线过滤器 */
CAN_FilterInitStructure.CAN_FilterNumber = 0; // 设置过滤器 0
CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask; // 设置为屏蔽位模式
CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit; // 设置为 32bit 宽度
CAN_FilterInitStructure.CAN_FilterIdHigh = 0x0000; // 设置 32 位的 ID 值
CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000;
CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x0000; // 设置 32 位屏蔽值
CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000;
CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0; // 将过滤器 0 关联至 FIFO0 邮箱
CAN_FilterInitStructure.CAN_FilterActivation = ENABLE; // 激活过滤器 0
CAN_FilterInit(&CAN_FilterInitStructure); // 使用上述设置初始化过滤器

#if CAN_INT_ENABLE
/* CAN 总线中断接收设置 */
CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE); // 允许 FIFO0 邮箱产生中断
NVIC_InitStructure.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 设置中断主优先级为 1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 设置中断次优先级为 0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
#endif
return 0;
}

/** CAN 数据发送函数,参数 msg 是待发送数据(最大 8 字节),参数 len 是待发送的数据长度(最大值 8),返回值为 0 表示发送成功,其它值表示失败*/
u8 CAN_Send_Msg(u8 *msg, u8 len) {
u8 mbox;
u16 i = 0;
CanTxMsg TxMessage;
TxMessage.StdId = 0x12; // 设置标准格式下,标识符的值
TxMessage.ExtId = 0x00; // 设置扩展格式下,标识符的值
TxMessage.IDE = CAN_Id_Standard; // 设置为标准帧格式
TxMessage.RTR = CAN_RTR_Data; // 设置为数据帧
TxMessage.DLC = len; // 待发送的数据长度

for (i = 0; i < len; i++) {
TxMessage.Data[i] = msg[i]; // 将参数 msg 数组内的元素,逐个发送至 CAN 总线控制器
}

mbox = CAN_Transmit(CAN1, &TxMessage); // 将 TxMessage 数据写入至 CAN 总线 1,返回值 mbox 表示数据被放入了哪个邮箱

i = 0;
/* 等待数据发送至邮箱,成功返回 0,失败返回 1 */
while ((CAN_TransmitStatus(CAN1, mbox) == CAN_TxStatus_Failed) && (i < 0XFFF)) {
i++; // 等待发送结束
if (i >= 0XFFF) {
return 1; // 超出预定时间,数据发送失败,返回 1
}
return 0;
}
}

/** 采用查询模方式接收 CAN 总线数据,参数 buf 是接收数据缓冲区,返回 0 表示当前没有数据接收,其它值则表示接收数据的长度 */
u8 CAN_Receive_Msg(u8 *buf) {
u32 i;
CanRxMsg RxMessage;

/* 判断 FIFO0 是否有数据 */
if (CAN_MessagePending(CAN1, CAN_FIFO0) == 0) {
return 0; // 未接收到数据返回 0
CAN_Receive(CAN1, CAN_FIFO0, &RxMessage); // 读取 FIFO0 数据
for (i = 0; i < 8; i++) {
buf[i] = RxMessage.Data[i];
}
return RxMessage.DLC; // 返回数据长度
}
}

/** CAN 中断接收程序,需要将 can.h 的 CAN_INT_ENABLE 置为 1 才会使能中断 */
void USB_LP_CAN1_RX0_IRQHandler(void) {
CanRxMsg RxMessage;
vu8 CAN_ReceiveBuff[8]; // CAN 数据接收数组
vu8 i = 0;
vu8 u8_RxLen = 0;

/* 清空相关的寄存器 */
CAN_ReceiveBuff[0] = 0;
RxMessage.StdId = 0x00;
RxMessage.ExtId = 0x00;
RxMessage.IDE = 0;
RxMessage.RTR = 0;
RxMessage.DLC = 0;
RxMessage.FMI = 0;

for (i = 0; i < 8; i++) {
RxMessage.Data[i] = 0x00;
}

CAN_Receive(CAN1, CAN_FIFO0, &RxMessage); // 读取 FIFO0 邮箱数据
u8_RxLen = RxMessage.DLC; // 获取数据的数量

/* 判断标识符是否一致 */
if (RxMessage.StdId == 0x12) {
CAN_ReceiveBuff[0] = RxMessage.DLC; // 将接收到的数据存放至 CAN 数据接收数组的第 0 位
for (i = 0; i < u8_RxLen; i++) {
CAN_ReceiveBuff[i] = RxMessage.Data[i]; // 将 8 位数据存放至 CAN 数据接收数组的其它位
}
}
}

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/** User/main.c */
#include "can.h"
#include "delay.h"
#include "oled0561.h"
#include "relay.h"
#include "stm32f10x.h"
#include "sys.h"
#include "touch_key.h"

int main(void) {
u8 x;
u8 buff[8];

delay_ms(100); // 等待相关元器件上电并且工作正常
RCC_Configuration(); // 初始化系统时钟
TOUCH_KEY_Init(); // 初始化触摸按键
RELAY_Init(); // 初始化继电器
CAN1_Configuration(); // 初始化 CAN 总线
I2C_Configuration(); // 初始化 I²C 总线
OLED0561_Init(); // 初始化 OLED

/* 显示文字到 OLED 屏幕 */
OLED_DISPLAY_8x16_BUFFER(0, " Hank ");
OLED_DISPLAY_8x16_BUFFER(2, " CAN Test ");
OLED_DISPLAY_8x16_BUFFER(6, "TX: RX: ");

while (1) {
/* 向 CAN 发送字符 A,并且显示到 OLED 屏幕*/
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A)) {
buff[0] = 'A';
CAN_Send_Msg(buff, 1);
OLED_DISPLAY_8x16(6, 4 * 8, 'A');
}
/* 向 CAN 发送字符 B,并且显示到 OLED 屏幕 */
else if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_B)) {
buff[0] = 'B';
CAN_Send_Msg(buff, 1);
OLED_DISPLAY_8x16(6, 4 * 8, 'B');
}
/* 向 CAN 发送字符 C,并且显示到 OLED 屏幕 */
else if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_C)) {
buff[0] = 'C';
CAN_Send_Msg(buff, 1);
OLED_DISPLAY_8x16(6, 4 * 8, 'C');
}
/* 向 CAN 发送字符 D,并且显示到 OLED 屏幕 */
else if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_D)) {
buff[0] = 'D';
CAN_Send_Msg(buff, 1);
OLED_DISPLAY_8x16(6, 4 * 8, 'D');
}

/* 查询方式接收数据,判断 CAN 总线接收数据的长度,非 0 时表示接受到了数据 */
x = CAN_Receive_Msg(buff);
if (x) {
OLED_DISPLAY_8x16(6, 11 * 8, buff[0]); // 在 OLED 上显示接收的数据
}
}
}

ADC 模数转换

ADC 是STM32F103C8T6用于采集模拟值的片上外设,STM32F103C8T6拥有 2 个 12 位分辨率(读出数据的位长度)的 ADC 模数转换器,它们共用 16 个外部通道(即 ADC 输入引脚ADC12_IN0 ~ ADC12_IN9),并且都可使用 DMA 进行操作。此外,还有VDDAVSSA引脚用于提供 ADC 功能的电源输入,为它们提供稳定的电源输入可以确保采集的精度与稳定性。

光敏电阻

首先,短接 ADC 对应的P8跳线座,让光敏电阻与STM32F103C8T6正常连接,然后将stm32f10x_dmastm32f10x_adc两个标准外设库文件添加到项目当中。

电路图最左侧的RG1就是光敏电阻(光线越强阻值越小,光线越弱阻值越大),将它的一个引脚接地,另一个引脚通过10K上拉电阻连接至3.3V电源上,然后通过1K限流电阻经由P8跳线插座连接至STM32F103C8T6PA4PA5引脚。这样,光敏电阻阻值的变化,就会使得 ADC 采集对应的PA4PA5引脚的输入电压发生变化。

adc.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/** Basic/adc/adc.h */
#ifndef __ADC_H
#define __ADC_H
#include "sys.h"

#define ADC1_DR_Address ((uint32_t)0x4001244C) // ADC1 外设地址,由 ADC1 外设起始地址加上偏移量获得

#define ADCPORT GPIOA // 设置 GPIO 引脚分组
#define ADC_CH4 GPIO_Pin_4 // 定义 ADC 为电压电位器
#define ADC_CH5 GPIO_Pin_5 // 定义 ADC 为光敏电阻
#define ADC_CH6 GPIO_Pin_6 // 定义 ADC 为摇杆 X 轴
#define ADC_CH7 GPIO_Pin_7 // 定义 ADC 为摇杆 Y 轴

void ADC_Configuration(void);
void ADC_GPIO_Init(void);
void ADC_DMA_Init(void);

#endif

adc.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/** Basic/adc/adc.c */
#include "adc.h"

vu16 ADC_DMA_IN5; // 用于存放 ADC 最终转换的数据

/** ADC 使用 DMA 传输的初始化函数 */
void ADC_DMA_Init(void) {
DMA_InitTypeDef DMA_InitStructure; // 定义 DMA 初始化结构体
DMA_DeInit(DMA1_Channel1); // 复位 DMA 通道 1
DMA_InitStructure.DMA_PeripheralBaseAddr = ADC1_DR_Address; // 设置 DMA 通道外设基地址,即 DMA 从哪里读取数据
DMA_InitStructure.DMA_MemoryBaseAddr = (u32)&ADC_DMA_IN5; // 设置 DMA 通道 ADC 数据存储器地址,即 DMA 将读取的数据放置到什么位置
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 指定外设源地址
DMA_InitStructure.DMA_BufferSize = 1; // 设置 DMA 缓冲区大小,根据 ADC 采集通道数决定,当前仅有光敏电阻 1 个数据量,所以这里选择 1
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 设置当前外设寄存器地址为不变化
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Disable; // 当前 DMA 存储器地址是否自增 1,由于当前只采集 1 个数据,所以选择 Disable
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 设置 DMA 需要读取的数据宽度为 16 位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // 设置 DMA 存放地址的数据宽度为 16 位
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 设置 DMA 通道操作模式为环形缓冲模式
DMA_InitStructure.DMA_Priority = DMA_Priority_High; // 设置 DMA 通道优先级为高
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 禁止 DMA 通道为存储器到存储器的传输
DMA_Init(DMA1_Channel1, &DMA_InitStructure); // 初始化 DMA 通道 1
DMA_Cmd(DMA1_Channel1, ENABLE); // 使能 DMA 通道 1
}

/** ADC 相关的 GPIO 初始化函数 */
void ADC_GPIO_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure; // 定义 GPIO 初始化结构体
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC, ENABLE); // 使能 GPIO 相关时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 使能 DMA 相关时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); // 使能 ADC1 相关时钟
GPIO_InitStructure.GPIO_Pin = ADC_CH5; // 选择 PA5 引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 设置为模拟输入模式
GPIO_Init(ADCPORT, &GPIO_InitStructure);
}

/** ADC 初始化函数 */
void ADC_Configuration(void) {
ADC_InitTypeDef ADC_InitStructure; // 定义 ADC 初始化结构体
ADC_GPIO_Init(); // 调用 ADC 相关的 GPIO 初始化函数
ADC_DMA_Init(); // 调用 ADC 相关的 DMA 初始化函数

ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; // 设置 ADC1 和 ADC2 独立工作模式
ADC_InitStructure.ADC_ScanConvMode = ENABLE; // 使能 ADC 扫描
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 设置 ADC 转换运行在连续模式,即循环进行采集
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 有软件控制转换
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 设置数据排列为右对齐
ADC_InitStructure.ADC_NbrOfChannel = 1; // 设置当前使用的 ADC 通道数目
ADC_Init(ADC1, &ADC_InitStructure);

ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 1, ADC_SampleTime_28Cycles5); // 设置 ADC1 的通道 ADC_Channel_5,转换顺序为 1,采样时间为 ADC_SampleTime_28Cycles5
ADC_DMACmd(ADC1, ENABLE); // 开启 ADC 的 DMA 功能
ADC_Cmd(ADC1, ENABLE); // 使能 ADC1 开始工作

ADC_ResetCalibration(ADC1); // 重置 ADC1 校准
while (ADC_GetResetCalibrationStatus(ADC1)); // 等待 ADC1 校准重置完成
ADC_StartCalibration(ADC1); // 开始ADC1校准
while (ADC_GetCalibrationStatus(ADC1)); // 等待 ADC1 校准完成

ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 使能 ADC1 软件转换,开始执行 ADC 模拟量采集
}

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/** User/main.c */
#include "adc.h"
#include "delay.h"
#include "oled0561.h"
#include "relay.h"
#include "stm32f10x.h"
#include "sys.h"
#include "touch_key.h"

extern vu16 ADC_DMA_IN5; // 声明外部变量,用于存放 ADC 采集的数据

int main(void) {
delay_ms(500); // 等待相关元器件上电并且工作正常
RCC_Configuration(); // 初始化系统时钟
TOUCH_KEY_Init(); // 初始化触摸按键
RELAY_Init(); // 初始化继电器
ADC_Configuration(); // 初始化 ADC 设置
I2C_Configuration(); // 初始化 I²C 总线
OLED0561_Init(); // 初始化 OLED

/* 显示文字到 OLED 屏幕 */
OLED_DISPLAY_8x16_BUFFER(0, " Hank ");
OLED_DISPLAY_8x16_BUFFER(2, " ADC Test ");
OLED_DISPLAY_8x16_BUFFER(6, " ADC_IN5: ");

while (1) {
/* 将光敏电阻的 ADC 数据显示到 OLED */
OLED_DISPLAY_8x16(6, 10 * 8, ADC_DMA_IN5 / 1000 + 0x30); // 显示 ADC 采集的数据的千位
OLED_DISPLAY_8x16(6, 11 * 8, ADC_DMA_IN5 % 1000 / 100 + 0x30); // 显示 ADC 采集的数据的百位
OLED_DISPLAY_8x16(6, 12 * 8, ADC_DMA_IN5 % 100 / 10 + 0x30); // 显示 ADC 采集的数据的十位
OLED_DISPLAY_8x16(6, 13 * 8, ADC_DMA_IN5 % 10 + 0x30); // 显示 ADC 采集的数据的个位
delay_ms(500); // 每间隔 500ms 刷新一次 OLED 屏幕数据
}
}

电位器&光敏电阻 2 通道 ADC 采集

上面的光敏电阻实验只演示了 1 个通道的示例,这里再来完成一个电位器、光敏电阻的 2 通道 ADC 采集。代码执行时,OLED 屏幕会同时显示ADC_IN4(电位器电压)和ADCIN5(光敏电阻电压)两组数据。

adc.h

adc.h没有进行任何改动,继续沿用上一个实验当中的头文件代码。

adc.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/** Basic/adc/adc.c  */
#include "adc.h"

vu16 ADC_DMA_IN[2]; // 用于存放 ADC 转换数据的数组

/** ADC 使用 DMA 传输的初始化函数 */
void ADC_DMA_Init(void) {
DMA_InitTypeDef DMA_InitStructure; // 定义 DMA 初始化结构体
DMA_DeInit(DMA1_Channel1); // 复位 DMA 通道 1
DMA_InitStructure.DMA_PeripheralBaseAddr = ADC1_DR_Address; // 设置 DMA 通道外设基地址,即 DMA 从哪里读取数据
DMA_InitStructure.DMA_MemoryBaseAddr = (u32)&ADC_DMA_IN; // 设置 DMA 通道 ADC 数据存储器地址,即 DMA 将读取的数据放置到什么位置
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 指定外设源地址
DMA_InitStructure.DMA_BufferSize = 2; // 设置 DMA 缓冲区大小,当前采用了光敏电阻和电位器 2 个数据量,所以这里修改为 2
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 设置当前外设寄存器地址为不变化
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 当前 DMA 存储器地址是否自增 1,由于当前只采集 1 个数据,所以选择 Disable
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 设置 DMA 需要读取的数据宽度为 16 位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // 设置 DMA 存放地址的数据宽度为 16 位
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 设置 DMA 通道操作模式为环形缓冲模式
DMA_InitStructure.DMA_Priority = DMA_Priority_High; // 设置 DMA 通道优先级为高
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 禁止 DMA 通道为存储器到存储器的传输
DMA_Init(DMA1_Channel1, &DMA_InitStructure); // 初始化 DMA 通道 1
DMA_Cmd(DMA1_Channel1, ENABLE); // 使能 DMA 通道 1
}

/** ADC 相关的 GPIO 初始化函数 */
void ADC_GPIO_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure; // 定义 GPIO 初始化结构体
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC, ENABLE); // 使能 GPIO 相关时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 使能 DMA 相关时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); // 使能 ADC1 相关时钟
GPIO_InitStructure.GPIO_Pin = ADC_CH4 | ADC_CH5; // 同时选择 PA4 和 PA5 引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 设置为模拟输入模式
GPIO_Init(ADCPORT, &GPIO_InitStructure);
}

/** ADC 初始化函数 */
void ADC_Configuration(void) {
ADC_InitTypeDef ADC_InitStructure; // 定义 ADC 初始化结构体
ADC_GPIO_Init(); // 调用 ADC 相关的 GPIO 初始化函数
ADC_DMA_Init(); // 调用 ADC 相关的 DMA 初始化函数

ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; // 设置 ADC1 和 ADC2 独立工作模式
ADC_InitStructure.ADC_ScanConvMode = ENABLE; // 使能 ADC 扫描
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 设置 ADC 转换运行在连续模式,即循环进行采集
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 有软件控制转换
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 设置数据排列为右对齐
ADC_InitStructure.ADC_NbrOfChannel = 2; // 设置当前使用的 ADC 通道数目
ADC_Init(ADC1, &ADC_InitStructure);

ADC_RegularChannelConfig(ADC1, ADC_Channel_4, 1, ADC_SampleTime_28Cycles5); // 设置 ADC1 的通道 ADC_Channel_4,转换顺序为 1 先转换,采样时间为 ADC_SampleTime_28Cycles5
ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 2, ADC_SampleTime_28Cycles5); // 设置 ADC1 的通道 ADC_Channel_5,转换顺序为 2 后转换,采样时间为 ADC_SampleTime_28Cycles5
ADC_DMACmd(ADC1, ENABLE); // 开启 ADC 的 DMA 功能
ADC_Cmd(ADC1, ENABLE); // 使能 ADC1 开始工作

ADC_ResetCalibration(ADC1); // 重置 ADC1 校准
while (ADC_GetResetCalibrationStatus(ADC1)); // 等待 ADC1 校准重置完成
ADC_StartCalibration(ADC1); // 开始ADC1校准
while (ADC_GetCalibrationStatus(ADC1)); // 等待 ADC1 校准完成

ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 使能 ADC1 软件转换,开始执行 ADC 模拟量采集
}

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/** User/main.c */
#include "adc.h"
#include "delay.h"
#include "oled0561.h"
#include "relay.h"
#include "stm32f10x.h"
#include "sys.h"
#include "touch_key.h"

extern vu16 ADC_DMA_IN[2]; // 声明外部变量数组,用于存放 ADC 将要采集的 2 个数据

int main(void) {
delay_ms(500); // 等待相关元器件上电并且工作正常
RCC_Configuration(); // 初始化系统时钟
TOUCH_KEY_Init(); // 初始化触摸按键
RELAY_Init(); // 初始化继电器
ADC_Configuration(); // 初始化 ADC 设置
I2C_Configuration(); // 初始化 I²C 总线
OLED0561_Init(); // 初始化 OLED

/* 显示文字到 OLED 屏幕 */
OLED_DISPLAY_8x16_BUFFER(0, " Hank ");
OLED_DISPLAY_8x16_BUFFER(2, " ADC Test ");
OLED_DISPLAY_8x16_BUFFER(4, " ADC_IN4: ");
OLED_DISPLAY_8x16_BUFFER(6, " ADC_IN5: ");

while (1) {
/* 将电位器的 ADC 数据显示到 OLED */
OLED_DISPLAY_8x16(4, 10 * 8, ADC_DMA_IN[0] / 1000 + 0x30);
OLED_DISPLAY_8x16(4, 11 * 8, ADC_DMA_IN[0] % 1000 / 100 + 0x30);
OLED_DISPLAY_8x16(4, 12 * 8, ADC_DMA_IN[0] % 100 / 10 + 0x30);
OLED_DISPLAY_8x16(4, 13 * 8, ADC_DMA_IN[0] % 10 + 0x30);

/* 将光敏电阻的 ADC 数据显示到 OLED */
OLED_DISPLAY_8x16(6, 10 * 8, ADC_DMA_IN[1] / 1000 + 0x30);
OLED_DISPLAY_8x16(6, 11 * 8, ADC_DMA_IN[1] % 1000 / 100 + 0x30);
OLED_DISPLAY_8x16(6, 12 * 8, ADC_DMA_IN[1] % 100 / 10 + 0x30);
OLED_DISPLAY_8x16(6, 13 * 8, ADC_DMA_IN[1] % 10 + 0x30);

delay_ms(500); // 每间隔 500ms 刷新一次 OLED 屏幕数据
}
}

模拟量摇杆

模拟量摇杆的机械结构由 2 个电位器和 1 个微动开关组成,进入本实验之前,需要将实验电路当中模拟量遥感对应的P17跳线座短接,同时断开旋转编码器对应的P18跳线,避免两者共用一组 GPIO 引脚。

模拟量摇杆的 X 轴电位器JS_X、Y 轴电位器JS_Y、微动开头JS_D分别经由P17跳线插座连接至STM32F103C8T6PA6PA7PB2引脚。将下面程序下载到实验电路运行以后,OLED 屏幕显示的ADC_IN7是摇杆上下调整的模拟量值,ADC_IN6则是摇杆左右调整的模拟量值。如果分别向左上、右上、左下、右下摆动摇杆,则会使ADC_IN6ADC_IN7两组值同时发生变化。如果直接按下摇杆,将会在 OLED 屏幕的左上角显示H字母,放开则会直接消失。

JoyStick.h

1
2
3
4
5
6
7
8
9
10
11
/** Hardware/JoyStick/JoyStick.h */
#ifndef __KEY_H
#define __KEY_H
#include "sys.h"

#define JoyStickPORT GPIOB // 定义 GPIO 分组
#define JoyStick_KEY GPIO_Pin_2 // 定义 GPIO 引脚

void JoyStick_Init(void); // 初始化函数

#endif

JoyStick.c

1
2
3
4
5
6
7
8
9
10
11
/** Hardware/JoyStick/JoyStick.c */
#include "JoyStick.h"

/** 模拟量摇杆的微动开关初始化函数 */
void JoyStick_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure; // 定义 GPIO 初始化枚举结构
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC, ENABLE);
GPIO_InitStructure.GPIO_Pin = JoyStick_KEY; // 选择 GPIO 引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 设置 GPIO 为上拉电阻输入模式
GPIO_Init(JoyStickPORT, &GPIO_InitStructure);
}

adc.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* ... 省略雷同代码 ... */

/** ADC 相关的 GPIO 初始化函数 */
void ADC_GPIO_Init(void) {
/* ... 省略雷同代码 ... */
GPIO_InitStructure.GPIO_Pin = ADC_CH6 | ADC_CH7; // 同时选择 PA6 和 PA7 引脚
/* ... 省略雷同代码 ... */
}

/** ADC 初始化函数 */
void ADC_Configuration(void) {
/* ... 省略雷同代码 ... */
ADC_RegularChannelConfig(ADC1, ADC_Channel_6, 1, ADC_SampleTime_28Cycles5); // 设置 ADC1 的通道 ADC_Channel_6,转换顺序为 1 先转换,采样时间为 ADC_SampleTime_28Cycles5
ADC_RegularChannelConfig(ADC1, ADC_Channel_7, 2, ADC_SampleTime_28Cycles5); // 设置 ADC1 的通道 ADC_Channel_7,转换顺序为 2 后转换,采样时间为 ADC_SampleTime_28Cycles5
/* ... 省略雷同代码 ... */
}

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/** User/main.c */
#include "JoyStick.h"
#include "adc.h"
#include "delay.h"
#include "oled0561.h"
#include "relay.h"
#include "stm32f10x.h"
#include "sys.h"
#include "touch_key.h"

extern vu16 ADC_DMA_IN[2]; // 声明外部变量数组,用于存放 ADC 将要采集的 2 个数据

int main(void) {
delay_ms(500); // 等待相关元器件上电并且工作正常
RCC_Configuration(); // 初始化系统时钟
TOUCH_KEY_Init(); // 初始化触摸按键
RELAY_Init(); // 初始化继电器
ADC_Configuration(); // 初始化 ADC 设置
JoyStick_Init(); // 初始化模拟量摇杆的微动开关
I2C_Configuration(); // 初始化 I²C 总线
OLED0561_Init(); // 初始化 OLED 屏幕

/* 显示信息到 OLED 屏幕 */
OLED_DISPLAY_8x16_BUFFER(0, " Hank ");
OLED_DISPLAY_8x16_BUFFER(2, " ADC Test ");
OLED_DISPLAY_8x16_BUFFER(4, " ADC_IN6: ");
OLED_DISPLAY_8x16_BUFFER(6, " ADC_IN7: ");

while (1) {
/* 将电位器的 ADC 数据显示到 OLED */
OLED_DISPLAY_8x16(4, 10 * 8, ADC_DMA_IN[0] / 1000 + 0x30);
OLED_DISPLAY_8x16(4, 11 * 8, ADC_DMA_IN[0] % 1000 / 100 + 0x30);
OLED_DISPLAY_8x16(4, 12 * 8, ADC_DMA_IN[0] % 100 / 10 + 0x30);
OLED_DISPLAY_8x16(4, 13 * 8, ADC_DMA_IN[0] % 10 + 0x30);

/* 将光敏电阻的 ADC 数据显示到 OLED */
OLED_DISPLAY_8x16(6, 10 * 8, ADC_DMA_IN[1] / 1000 + 0x30);
OLED_DISPLAY_8x16(6, 11 * 8, ADC_DMA_IN[1] % 1000 / 100 + 0x30);
OLED_DISPLAY_8x16(6, 12 * 8, ADC_DMA_IN[1] % 100 / 10 + 0x30);
OLED_DISPLAY_8x16(6, 13 * 8, ADC_DMA_IN[1] % 10 + 0x30);

/* 模拟量摇杆微动开关按下处理 */
if (GPIO_ReadInputDataBit(JoyStickPORT, JoyStick_KEY) == 0) {
OLED_DISPLAY_8x16(0, 0, 'H'); // 如果按键按下,显示字母 H
} else {
OLED_DISPLAY_8x16(0, 0, ' '); // 如果按键放开,显示空格
}

delay_ms(500); // 每间隔 500ms 刷新一次 OLED 屏幕数据
}
}

MY1650 音频解码芯片

MY1690-16S是深圳市迈优科技有限公司自主研发的一款串口控制的插卡音频播放芯片,可以进行 MP3 和 WAV 双格式解码,最大支持 32G 的 TF 存储卡,也可以外接 U 盘或者 USB 数据线连接至电脑,以更换 SD 卡音频文件。进行本实验之前,需要先短接实验电路右上角的 TF CARDMY1680USB 相关的跳线座,然后再分别断接旋转编码器、模拟量摇杆对应的P18P17跳线座。

STM32F103C8T6的 USART3 外设对应的PB10PB11经由上面原理图中的TFMUSIC_RXTFMUSIC_TX分别连接至P14跳线插座的RXTX,最后连接到MY1690-16S音频播放芯片的 RXTX 引脚。此外,网络标号为J8的 SD 卡座的DOCLKDI引脚分别经由P16跳线座连接至MY1690-16SMY_DATMY_CMDMY_CLK引脚。最后,MY1690ADKEY引脚经过P16跳线座以后,最终连接至LM4871音频功率放大芯片的电源控制引脚SHUTDOWN

管脚号 管脚名称 功能描述 备注
1 SD_DAT 接 SD 卡 -
2 SD_CMD 接 SD 卡 -
3 SD_CLK 接 SD 卡 -
4 ADKEY 电阻分压功能选择脚 通过不同阻值分压实现多种功能
5 DM USB 信号线 U 盘或者 USB 连接电脑更换 TF 卡文件
6 DP USB 信号线 U 盘或者 USB 连接电脑更换 TF 卡文件
7 VPN 内部电源偏置电压 1uF 电容接地
8 VSS 模拟信号地 模拟地,连接大功率功放时与数字地分开
9 DACL 右声道音频信号输出 连接耳机或者功放
10 DACR 左声道音频信号输出 连接耳机或者功放
11 3V3 内部 LDO 3.3V 输出 可以为 TF 卡,驱动电流100mA以内
12 DC5V 芯片电源正极输入 电源范围3.4V ~ 5.5V
13 GND 系统接地 -
14 TX UART 异步串口数据输出 3.3V的 TTL 信号
15 RX UART 异步串口数据输入 3.3V的 TTL 信号
16 BUSY 播放时输出高电平,暂停或停止时为低电平 -

如前所述,MY1690-16SMY_DATMY_CMDMY_CLK是连接到 TF 卡上的,可连接控制按键盘的ADKEY引脚通过22KΩ电阻直接连接到3.3V电源,另外DMDP引脚用于连接开发板上的 Micro USB 接口(如果连接了 USB 接口但是没有插入 TF 卡,MY1690 会将 TF 卡的 U 盘功能切换为声卡功能)。MY1690-16SVPN引脚则连接至1uF电容,VSS引脚直接接地,DACLDACR是左右声道音频输出,当前实验电路只有一个单声道扬声器,所以各经过1uF电容后将双声道合并为单声道经由功放芯片LM4871音频放大后接入扬声器;BUSY引脚连接到一枚外部 LED,芯片工作时自动点亮。

MY1690-16S采用 3.3V 的 UART 异步串行接口连接,通讯数据格式为1 位起始位8 位数据位无奇偶校验位1 位停止位;此外,其传输的数据必须遵循如下协议格式:

1
起始码(0x7E) | 长度(参考手册) | 操作码(参考手册) | 参数(参考手册) | 校验码(参考手册) | 结束码(0xEF)

音乐播放

usart.h

1
#define EN_USART3 1 // 使能 USART3

usart.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/** USART3 初始化函数 */
void USART3_Init(u32 BaudRate) {
/* ... 省略雷同代码 ... */
USART_ITConfig(USART3, USART_IT_RXNE, ENABLE); // 使能 USART 接收中断
/* ... 省略雷同代码 ... */
}

/** USART3 中断服务程序 */
void USART3_IRQHandler(void) {
u8 Res;

/* 判断接收中断 */
if (USART_GetITStatus(USART3, USART_IT_RXNE) != RESET) {
Res = USART_ReceiveData(USART3); // 读取 USART3 接收到的数据

/* 判断数据是否为 STOP,省略读取 S */
if (Res == 'S') {
USART3_RX_STA = 1; // 标志位置为 1
}
/* 判断数据是否为 OK,省略读取 K */
else if (Res == 'K') {
USART3_RX_STA = 2; // 标志位置为 2
}
}
}

MY1690.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/** Hardware/MY1690/MY1690.h */
#ifndef __MY1690_H
#define __MY1690_H
#include "sys.h"
#include "usart.h"

void MY1690_Init(void); // MY1690 初始化函数
void MY1690_PLAY(void); // 播放
void MY1690_PREV(void); // 上一曲
void MY1690_NEXT(void); // 下一曲
void MY1690_PAUSE(void); // 暂停
void MY1690_VUP(void); // 音量加 1
void MY1690_VDOWN(void); // 音量减 1
void MY1690_STOP(void); // 停止

/* MY1690 指令输入函数 */
void MY1690_CMD1(u8 a);
void MY1690_CMD2(u8 a, u8 b);
void MY1690_CMD3(u8 a, u16 b);

#endif

MY1690.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
/** Hardware/MY1690/MY1690.c  */
#include "MY1690.h"

/** MY1690 初始化函数 */
void MY1690_Init(void) {
USART3_Init(9600); // 初始化 USART 为 9600 波特率
MY1690_STOP(); // 发送一条停止播放指令,从而激活 MY1690
}

/** 播放 */
void MY1690_PLAY(void) {
USART3_printf("\x7e\x03\x11\x12\xef"); // 符号 \x 后面是十六进制数
}
/** 暂停 */
void MY1690_PAUSE(void) {
USART3_printf("\x7e\x03\x12\x11\xef");
}
/** 上一曲 */
void MY1690_PREV(void) {
USART3_printf("\x7e\x03\x14\x17\xef");
}
/** 下一曲 */
void MY1690_NEXT(void) {
USART3_printf("\x7e\x03\x13\x10\xef");
}
/** 音量加 1 */
void MY1690_VUP(void) {
USART3_printf("\x7e\x03\x15\x16\xef");
}
/** 音量减 1 */
void MY1690_VDOWN(void) {
USART3_printf("\x7e\x03\x16\x15\xef");
}
/** 停止函数 */
void MY1690_STOP(void) {
USART3_printf("\x7e\x03\x1E\x1D\xef");
}

/** 无参数指令的发送函数,参数 a 是操作码 */
void MY1690_CMD1(u8 a) {
u8 i;
i = 3 ^ a; // 生成校验码

USART_SendData(USART3, 0x7e); // 发送起始码
while (USART_GetFlagStatus(USART3, USART_FLAG_TC) == RESET); // 检查发送中断标志位
USART_SendData(USART3, 0x03); // 发送数据长度
while (USART_GetFlagStatus(USART3, USART_FLAG_TC) == RESET); // 检查发送中断标志位
USART_SendData(USART3, a); // 发送操作码
while (USART_GetFlagStatus(USART3, USART_FLAG_TC) == RESET); // 检查发送中断标志位
USART_SendData(USART3, i); // 发送校验码
while (USART_GetFlagStatus(USART3, USART_FLAG_TC) == RESET); // 检查发送中断标志位
USART_SendData(USART3, 0xef); // 发送结束码
while (USART_GetFlagStatus(USART3, USART_FLAG_TC) == RESET); // 检查发送中断标志位
}

/** 有参数指令的发送函数,参数 a 是操作码 b 是 8 位参数 */
void MY1690_CMD2(u8 a, u8 b) {
u8 i;
i = 4 ^ a; // 生成校验码
i = i ^ b; // 生成校验码

USART_SendData(USART3, 0x7e); // 发送起始码
while (USART_GetFlagStatus(USART3, USART_FLAG_TC) == RESET); // 检查发送中断标志位
USART_SendData(USART3, 0x04); // 发送数据长度
while (USART_GetFlagStatus(USART3, USART_FLAG_TC) == RESET); // 检查发送中断标志位
USART_SendData(USART3, a); // 发送操作码
while (USART_GetFlagStatus(USART3, USART_FLAG_TC) == RESET); // 检查发送中断标志位
USART_SendData(USART3, b); // 发送 8 位参数
while (USART_GetFlagStatus(USART3, USART_FLAG_TC) == RESET); // 检查发送中断标志位
USART_SendData(USART3, i); // 发送校验码
while (USART_GetFlagStatus(USART3, USART_FLAG_TC) == RESET); // 检查发送中断标志位
USART_SendData(USART3, 0xef); // 发送结束码
while (USART_GetFlagStatus(USART3, USART_FLAG_TC) == RESET); // 检查发送中断标志位
}

/** 有参数指令的发送函数,参数 a 是操作码 b 是 16 位参数 */
void MY1690_CMD3(u8 a, u16 b) {
u8 i, c, d;

/* 将 16 位参数分解为 2 个 8 位参数 */
c = b / 0x100;
d = b % 0x100;

i = 5 ^ a; // 获取校验码
i = i ^ c; // 获取校验码
i = i ^ d; // 获取校验码

USART_SendData(USART3, 0x7e); // 发送起始码
while (USART_GetFlagStatus(USART3, USART_FLAG_TC) == RESET); // 检查发送中断标志位
USART_SendData(USART3, 0x05); // 发送数据长度
while (USART_GetFlagStatus(USART3, USART_FLAG_TC) == RESET); // 检查发送中断标志位
USART_SendData(USART3, a); // 发送操作码
while (USART_GetFlagStatus(USART3, USART_FLAG_TC) == RESET); // 检查发送中断标志位
USART_SendData(USART3, c); // 发送 16 位参数高 8 位
while (USART_GetFlagStatus(USART3, USART_FLAG_TC) == RESET); // 检查发送中断标志位
USART_SendData(USART3, d); // 发送 16 位参数低 8 位
while (USART_GetFlagStatus(USART3, USART_FLAG_TC) == RESET); // 检查发送中断标志位
USART_SendData(USART3, i); // 发送校验码
while (USART_GetFlagStatus(USART3, USART_FLAG_TC) == RESET); // 检查发送中断标志位
USART_SendData(USART3, 0xef); // 发送结束码
while (USART_GetFlagStatus(USART3, USART_FLAG_TC) == RESET); // 检查发送中断标志位
}

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
/** User/main.c */
#include "delay.h"
#include "oled0561.h"
#include "relay.h"
#include "stm32f10x.h"
#include "sys.h"
#include "touch_key.h"
#include "encoder.h"
#include "my1690.h"
#include "usart.h"

int main(void) {
u8 b;
u8 MP3 = 0; // 初始化 MY1690 为暂停状态

delay_ms(500); // 等待相关元器件上电并且工作正常
RCC_Configuration(); // 初始化系统时钟
TOUCH_KEY_Init(); // 初始化触摸按键
RELAY_Init(); // 初始化继电器
ENCODER_Init(); // 初始化旋转编码器
MY1690_Init(); // 初始化 MY1690 以及 USART
I2C_Configuration(); // 初始化 I²C 总线
OLED0561_Init(); // 初始化 OLED

/* 显示文字到 OLED 屏幕 */
OLED_DISPLAY_8x16_BUFFER(0, " Hank ");
OLED_DISPLAY_8x16_BUFFER(3, " MP3 Play Test ");

while (1) {
/* 判断 4 个触摸按键是否按下 */
if (GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A) == 0 || GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_B) == 0 || GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_C) == 0 || GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_D) == 0) {
delay_ms(20); // 去抖延时,便于程序移植到机械按键

/* A 键按下,播放上一曲 */
if (GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A) == 0) {
MY1690_PREV();
OLED_DISPLAY_8x16_BUFFER(6, " -- PREV -- ");
delay_ms(500);
OLED_DISPLAY_8x16_BUFFER(6, " -- PLAY -- ");
}
/* B 键按下,播放下一曲 */
if (GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_B) == 0) {
MY1690_NEXT();
OLED_DISPLAY_8x16_BUFFER(6, " -- NEXT -- ");
delay_ms(500);
OLED_DISPLAY_8x16_BUFFER(6, " -- PLAY -- ");
}
/* C 键按下,设置音量为最大 */
if (GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_C) == 0) {
MY1690_CMD2(0x31, 30);
delay_ms(500);
}
/* C 键按下,播放第 0004 曲目 */
if (GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_D) == 0) {
MY1690_CMD3(0x41, 0x04);
delay_ms(500);
}

/* 等待触摸按键放开 */
while (GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A) == 0 || GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_B) == 0 || GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_C) == 0 || GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_D) == 0);
}

/* 读取旋转编码器状态值,并进行判断分析 */
b = ENCODER_READ();
/* 右转增大音量 */
if (b == 1) {
MY1690_VUP();
}
/* 左转降低音量 */
if (b == 2) {
MY1690_VDOWN();
}
/* 按下旋转编码器,执行播放或暂停操作 */
if (b == 3) {
/* 如果当前处于播放状态 */
if (MP3 == 0) {
MP3 = 1;
MY1690_PLAY();
OLED_DISPLAY_8x16_BUFFER(6, " -- PLAY -- ");
}
/* 如果当前处于暂停状态 */
else if (MP3 == 1) {
MP3 = 0;
MY1690_PAUSE();
OLED_DISPLAY_8x16_BUFFER(6, " -- PAUSE -- ");
}
delay_ms(500);
}

/* MY1690 串行接口接收到指令的处理,标志位 1 表示收到 STOP 指令,标志位 2 表示收到 OK 指令 */
if (USART3_RX_STA == 1) {
MP3 = 0;
OLED_DISPLAY_8x16_BUFFER(6, " -- STOP -- ");
USART3_RX_STA = 0; // 标志位清 0
} else if (USART3_RX_STA == 2) {
USART3_RX_STA = 0; // 标志位清 0
}
}
}

时钟语音播报

TF 卡语音播报文件当中,0001 ~ 0011是阿拉伯数字0 ~ 10的发音,0012现在是北京时间的发音,0013的发音,0014的发音,0015的发音。

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#include "delay.h"
#include "oled0561.h"
#include "relay.h"
#include "rtc.h"
#include "stm32f10x.h"
#include "sys.h"
#include "touch_key.h"

#include "encoder.h"
#include "my1690.h"
#include "usart.h"

int main(void) {
delay_ms(500); // 等待相关元器件上电并且工作正常

RCC_Configuration(); // 初始化系统时钟
RTC_Config(); // 初始化实时时钟
TOUCH_KEY_Init(); // 初始化触摸按键
RELAY_Init(); // 初始化继电器
ENCODER_Init(); // 初始化旋转编码器
MY1690_Init(); // 初始化 MY1690 以及 USART
I2C_Configuration(); // 初始化 I²C 总线
OLED0561_Init(); // 初始化 OLED

/* 显示文字到 OLED 屏幕 */
OLED_DISPLAY_8x16_BUFFER(0, " Hank ");
OLED_DISPLAY_8x16_BUFFER(2, " MP3 Time Read ");

while (1) {
/* 读取实时时钟 RTC 的数据*/
if (RTC_Get() == 0) {
/* OLED 显示小时 */
OLED_DISPLAY_8x16(4, 8 * 3, rhour / 10 + 0x30);
OLED_DISPLAY_8x16(4, 8 * 4, rhour % 10 + 0x30);
OLED_DISPLAY_8x16(4, 8 * 5, ':');
/* OLED 显示分钟 */
OLED_DISPLAY_8x16(4, 8 * 6, rmin / 10 + 0x30);
OLED_DISPLAY_8x16(4, 8 * 7, rmin % 10 + 0x30);
OLED_DISPLAY_8x16(4, 8 * 8, ':');
/* OLED 显示秒钟 */
OLED_DISPLAY_8x16(4, 8 * 9, rsec / 10 + 0x30);
OLED_DISPLAY_8x16(4, 8 * 10, rsec % 10 + 0x30);
delay_ms(200);
}

/* 判断 4 个触摸按键是否有被按下 */
if (GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A) == 0 || GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_B) == 0 || GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_C) == 0 || GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_D) == 0) {
delay_ms(20);
/* 如果 A 键被按下,就采用 MY1690 的组合播放功能,一次性播放多条指令 */
if (GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A) == 0) {
OLED_DISPLAY_8x16_BUFFER(6, " -- PLAY -- ");

/* 播放小时值十位和个位对应的语音编号 */
MY1690_CMD3(0x41, 12); // 播放曲目 12,"现在是北京时间"

/* 如果小时值十位等于 0 时 */
if (rhour / 10 == 0) {
// 不发音
}
/* 当小时值的十位等于 1 时 */
else if (rhour / 10 == 1) {
MY1690_CMD3(0x41, 11); // 播放曲目 11,"十"
}
/* 当小时值十位除了为 0 或者 1 之外,进行正常播报 */
else {
MY1690_CMD3(0x41, rhour / 10 + 1); // 播放小时值十位对应的语音编号
MY1690_CMD3(0x41, 11); // 播放曲目 11,"十"
}
/* 当整个小时值等于 0 或者 仅个位不等于 0 时 */
if (rhour % 10 != 0 || rhour == 0) {
MY1690_CMD3(0x41, rhour % 10 + 1); // 播放小时值个位对应的语音编号
}
MY1690_CMD3(0x41, 13); // 播放曲目 11,"点"

/* 播放分钟值十位和个位对应的语音编号,判断方式与小时相同 */
if (rmin / 10 == 0) {
MY1690_CMD3(0x41, rmin / 10 + 1);
} else if (rmin / 10 == 1) {
MY1690_CMD3(0x41, 11);
} else {
MY1690_CMD3(0x41, rmin / 10 + 1);
MY1690_CMD3(0x41, 11);
}
if (rmin % 10 != 0 || rmin == 0) {
MY1690_CMD3(0x41, rmin % 10 + 1);
}
MY1690_CMD3(0x41, 14); // 播放曲目 11,"点"

/* 播放秒数值十位和个位对应的语音编号,判断方式与小时相同 */
if (rsec / 10 == 0) {
MY1690_CMD3(0x41, rsec / 10 + 1);
} else if (rsec / 10 == 1) {
MY1690_CMD3(0x41, 11);
} else {
MY1690_CMD3(0x41, rsec / 10 + 1);
MY1690_CMD3(0x41, 11);
}
if (rsec % 10 != 0 || rsec == 0) {
MY1690_CMD3(0x41, rsec % 10 + 1);
}
MY1690_CMD3(0x41, 15); // 播放曲目 11,"点"
}

/* 等待按键松开 */
while (GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A) == 0 || GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_B) == 0 || GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_C) == 0 || GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_D) == 0);
}

/* USART3 接收处理 */
if (USART3_RX_STA == 1) { // 标志位为 1 就表示收到 STOP
OLED_DISPLAY_8x16_BUFFER(6, " -- STOP -- ");
USART3_RX_STA = 0; // 将串口数据标志位清 0
}
}
}

CH376 文件管理控制芯片

CH376T 是江苏沁恒股份有限公司生产的一款文件管理控制芯片,主要用于单片机系统读写 U 盘或者 SD 卡,并且可以通过 SPI 和 UART 两种总线方式与STM32F103C8T6进行通信,当前实验电路选择的是 SPI 总线连接方式。设置好右上角的跳线坐,短接实验电路右上角的 TF CARDMY1680USB 跳线座,并且将stm32f10x_spi标准外设库添加至当前工程,后续将着手完成一个用于检测 U 盘插入状态以及进行文件系统读取的实验程序。

STM32F103C8T6拥有 2 个 SPI 接口(SPI1PA4/SPI1_NSSPA5/SPI1_SCKPA6/SPI1_MISOPA7/SPI1_MOSI引脚,SPI2PB12/SPI2_NSSPB13/SPI2_SCKPB14/SPI2_MISOPB15/SPI2_MOSI引脚),每个接口拥有 MISO(主机接收,从机发送)、MOSI(主机发送,从机接收)、SCK(同步时钟)、NSS(设备选择) 四条信号线,并且所有的主从设备都需要进行共地连接。由于 SPI 总线上的设备没有地址的概念,所以需要通过NSS信号线的电平状态来选中需要通信的设备(低电平使能,高电平失能)。STM32F103C8T6只拥有 1 条NSS信号线,如果需要连接更多的设备,就需要使用到普通的 GPIO 引脚来模拟相关的功能。

实验电路中,电路图里的SPI_CSSPI_CLKSPI_DISPI_DO分别代表SPI2PB12/SPI2_NSSPB13/SPI2_SCKPB14/SPI2_MISOPB15/SPI2_MOSI引脚,经过P16PA15跳线座以后,连接到 CH376T 芯片的SCSSCKSDISDO引脚。 CH376T 芯片的INT引脚连接 到 TF 卡实验电路的的 TF 插入状态标志引脚CdSPI引脚接入到低电平选择使用 SPI 通信,SD_DISD_CKSD_DOSD_CS连接至 TF 卡电路,V33.3V电源引脚,UD+UD-则是连接到 USB 插槽的两条数据线,最后XOXI用于连接一枚外部的12MHz晶振以及起振电容C20

U 盘插拔状态

spi.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/** Basic/spi/spi.h */
#ifndef __SPI_H
#define __SPI_H
#include "sys.h"

#define SPI2PORT GPIOB // 定义 GPIO 组

/* 定义 SPI 通信相关的 GPIO 引脚 */
#define SPI2_MOSI GPIO_Pin_15
#define SPI2_MISO GPIO_Pin_14
#define SPI2_SCK GPIO_Pin_13
#define SPI2_NSS GPIO_Pin_12

void SPI2_Init(void); // SPI2 总线初始化函数
u8 SPI2_SendByte(u8 Byte); // SPI2 总线数据发送函数

#endif

spi.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/** Basic/spi/spi.c */
#include "spi.h"

/** SPI2 初始化函数 */
void SPI2_Init(void) {
SPI_InitTypeDef SPI_InitStructure; // 声明 SPI 结构体
GPIO_InitTypeDef GPIO_InitStructure; // 声明 GPIO 结构体
RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE); // 使能 SPI2 总线时钟

/** 设置 MISO 引脚为浮空输入模式 */
GPIO_InitStructure.GPIO_Pin = SPI2_MISO;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(SPI2PORT, &GPIO_InitStructure);

/** 设置 SCK 和 MOSI 引脚为 50MHz 的推挽输出模式 */
GPIO_InitStructure.GPIO_Pin = SPI2_MOSI | SPI2_SCK;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(SPI2PORT, &GPIO_InitStructure);

/** 设置 NSS 引脚为 50MHz 的推挽输出模式 */
GPIO_InitStructure.GPIO_Pin = SPI2_NSS;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(SPI2PORT, &GPIO_InitStructure);

SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 设置 SPI 总线为双线全双工模式
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 设置为 SPI 主机模式,即由 SCK 主动产生时钟
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 设置 SPI 数据大小为 8 位帧结构
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; // 设置空闲状态时,SCK 的状态为保持高电平
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; // 设置时钟相位,为 1 表示在 SCK 奇数沿采样,为 2 表示在偶数沿采样
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 设置 NSS 为软件控制,即 SPI 通信时自动将 NSS 拉低,通信完成后自动拉高
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256; // 设置时钟预分频值为 256
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // 设置 SPI 总线时序为 MSB 高位在前
SPI_InitStructure.SPI_CRCPolynomial = 7; // 设置为不使用 CRC 较验
SPI_Init(SPI2, &SPI_InitStructure); // 初始化 SPI2 配置
SPI_Cmd(SPI2, ENABLE); // 使能 SPI2 总线
}

/** SPI2 总线数据收发函数,参数 Byte 表示待发送的参数,返回值是总线接收到的数据 */
u8 SPI2_SendByte(u8 Byte) {
while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET); // 循环判断 SPI2 发送寄存器是否为空
SPI_I2S_SendData(SPI2, Byte); // 通过 SPI2 总线发送数据
while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET); // 循环判断 SPI2 接收寄存器是否为空
return SPI_I2S_ReceiveData(SPI2); // 将 SPI2 总线接收到的数据返回
}

ch376inc.h

1
2
3
4
5
/** Hardware/CH376/ch376inc.h 由官方提供的 CH376 芯片控制命令,具体功能可以参考 CH376 官方数据手册 */

/* ... 省略冗余代码 ... */
#define CMD01_GET_IC_VER 0x01 // 获取 CH376 芯片及固件版本
/* ... 省略冗余代码 ... */

ch376.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/** Hardware/CH376/ch376.h */
#ifndef __CH376_H
#define __CH376_H
#include "ch376inc.h"
#include "delay.h"
#include "spi.h"
#include "sys.h"

#define CH376_INTPORT GPIOA // 定义 GPIO 分组
#define CH376_INT GPIO_Pin_15 // 定义 GPIO 引脚

/* 声明 ch376.c 当中定义的函数 */
void CH376_PORT_INIT(void);
void xEndCH376Cmd(void);
void xWriteCH376Cmd(u8 mCmd);
void xWriteCH376Data(u8 mData);
u8 xReadCH376Data(void);
u8 Query376Interrupt(void);
u8 mInitCH376Host(void);

#endif

ch376.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
/** Hardware/CH376/ch376.c */
#include "CH376.h"

/** CH376 相关的 GPIO 引脚初始化函数 */
void CH376_PORT_INIT(void) {
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 设置为上拉输入模式
GPIO_Init(GPIOA, &GPIO_InitStructure);

/* 设置 GPIO 引脚初始状态 */
GPIO_SetBits(CH376_INTPORT, CH376_INT); // 设置中断输入引脚为高电平
GPIO_SetBits(SPI2PORT, SPI2_NSS); // 设置 NSS 片选为高电平
}

/** 结束 CH376 命令发送 */
void xEndCH376Cmd(void) {
GPIO_SetBits(SPI2PORT, SPI2_NSS); // 直接设置 NSS 为高电平,使 SPI 片选无效,从而结束 CH376 命令
}

/** 通过 SPI 总线发送数据,参数 d 是待发送数据*/
void Spi376OutByte(u8 d) {
SPI2_SendByte(d);
}

/** 通过 SPI 总线接收 1 个字节的数据,返回值就是接收到的数据 */
u8 Spi376InByte(void) {
while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET); // 循环判断接收寄存器是否收到数据
return SPI_I2S_ReceiveData(SPI2);
}

/** 向 CH376 写入命令,参数 mCmd 是待发送的命令 */
void xWriteCH376Cmd(u8 mCmd) {
GPIO_SetBits(SPI2PORT, SPI2_NSS); // 禁止 SPI 片选,防止之前未调用 xEndCH376Cmd() 结束命令发送
delay_us(20); // 延时确保正确写入
GPIO_ResetBits(SPI2PORT, SPI2_NSS); // 重新开启 SPI 片选
Spi376OutByte(mCmd); // 发送命令码
delay_us(1700); // 延时确保正确写入
}

/** 向 CH376 芯片写入数据,参数 mData 是待发送的数据 */
void xWriteCH376Data(u8 mData) {
Spi376OutByte(mData); // 调用 CH376 的 SPI 总线数据发送函数
delay_us(800);
}

/** 从 CH376 芯片读取数据 */
u8 xReadCH376Data(void) {
u8 i;
delay_us(10);
i = SPI2_SendByte(0xFF); // 调用 SPI2 的数据发送函数
return (i);
}

/** 查询 CH376 中断状态,返回 0 表示没有中断,1 表示有中断 */
u8 Query376Interrupt(void) {
u8 i;
i = GPIO_ReadInputDataBit(CH376_INTPORT, CH376_INT); // 从 GPIO 端口读取中断状态并返回
return (i);
}

/** CH376 芯片初始化函数 */
u8 mInitCH376Host(void) {
u8 res;

delay_ms(600); // 等待相关元器件上电并且工作正常
CH376_PORT_INIT(); // 调用 CH376 相关的 GPIO 引脚初始化函数

/* 测试 STM32F103C8T6 与 CH376 之间通讯接口的工作状态 */
xWriteCH376Cmd(CMD11_CHECK_EXIST); // 发送测试命令
xWriteCH376Data(0x55); // 发送数据
res = xReadCH376Data(); // 接收响应
xEndCH376Cmd(); // 结束与 CH376 的通信
/* 如果接收到的数据不是 0x55 的按位取反值 0xAA */
if (res != 0xAA) {
return (ERR_USB_UNKNOWN); // 返回通信异常命令,后续代码将不会被执行
}

/* 测试成功则开始设置 USB 的工作模式 */
xWriteCH376Cmd(CMD11_SET_USB_MODE); // 设置 USB 工作模式
xWriteCH376Data(0x06); // 切换为主机模式
delay_us(20); // 保证设置正确结束
res = xReadCH376Data(); // 读取 CH376 返回数据
xEndCH376Cmd(); // 结束总线通信
/* 判断 CH376 命令执行状态 */
if (res == CMD_RET_SUCCESS) {
return (USB_INT_SUCCESS); // 操作成功返回 SUCCESS
} else {
return (ERR_USB_UNKNOWN); // 操作失败返回 UNKNOWN
}
}

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/** User/main.c */
#include "ch376.h"
#include "delay.h"
#include "oled0561.h"
#include "relay.h"
#include "spi.h"
#include "stm32f10x.h"
#include "sys.h"
#include "touch_key.h"

int main(void) {
u8 s;
delay_ms(500); // 等待相关元器件上电并且工作正常
RCC_Configuration(); // 初始化系统时钟
TOUCH_KEY_Init(); // 初始化触摸按键
RELAY_Init(); // 初始化继电器
I2C_Configuration(); // 初始化 I²C 总线
OLED0561_Init(); // 初始化 OLED

/* 显示文字到 OLED 屏幕 */
OLED_DISPLAY_8x16_BUFFER(0, " Hank ");
OLED_DISPLAY_8x16_BUFFER(2, " U Disk Test ");

/* 初始化 CH376 文件管理控制芯片 */
SPI2_Init(); // 首先初始化 SPI 总线
if (mInitCH376Host() == USB_INT_SUCCESS) {
OLED_DISPLAY_8x16_BUFFER(4, " CH376 OK! "); // 如果初始化成功就向 OLED 屏幕显示字符串
}

while (1) {
s = CH376DiskConnect(); // 读取 U 盘状态
if (s == USB_INT_SUCCESS) {
OLED_DISPLAY_8x16_BUFFER(6, " U Disk Ready! "); // U 盘插入,显示字符串
} else {
OLED_DISPLAY_8x16_BUFFER(6, " "); // U 盘拔出,显示空白
}
delay_ms(500);
}
}

文件系统读写

filesys.h

1
2
/** Hardware/CH376/filesys.h,由沁恒公司官方提供 http://www.wch.cn/search?t=all&q=ch376 */

filesys.c

1
/** Hardware/CH376/filesys.c,由沁恒公司官方提供 http://www.wch.cn/search?t=all&q=ch376 */

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/** User/main.c */

#include "ch376.h"
#include "delay.h"
#include "filesys.h"
#include "oled0561.h"
#include "relay.h"
#include "spi.h"
#include "stm32f10x.h"
#include "sys.h"
#include "touch_key.h"
#include <stdio.h>
#include <string.h>

u8 buf[128]; // 数据缓冲区,用于存放临时数据

int main(void) {
u8 s i;
delay_ms(500); // 等待相关元器件上电并且工作正常
RCC_Configuration(); // 初始化系统时钟
TOUCH_KEY_Init(); // 初始化触摸按键
RELAY_Init(); // 初始化继电器
I2C_Configuration(); // 初始化 I²C 总线
OLED0561_Init(); // 初始化 OLED

/* 显示文字到 OLED 屏幕 */
OLED_DISPLAY_8x16_BUFFER(0, " Hank ");
OLED_DISPLAY_8x16_BUFFER(2, " U Disk Test ");

/* 初始化 CH376 文件管理控制芯片 */
SPI2_Init(); // 首先初始化 SPI 总线
if (mInitCH376Host() == USB_INT_SUCCESS) {
OLED_DISPLAY_8x16_BUFFER(4, " CH376 OK! "); // 如果初始化成功就向 OLED 屏幕显示字符串
}

while (1) {
/* 循环等待 U 盘插入 */
while (CH376DiskConnect() != USB_INT_SUCCESS) {
delay_ms(100);
}
OLED_DISPLAY_8x16_BUFFER(6, " U DISK Ready! "); // U 盘插入后,向 OLED 显示字符串
delay_ms(200); // 延时确保操作正确

/* 循环 100 次,初始化 U 盘 */
for (i = 0; i < 100; i++) {
delay_ms(50);
s = CH376DiskMount(); // 初始化磁盘并且测试磁盘是否就绪,即建立与 U 盘的连接,并且等待 U 盘进入工作状态

/* 判断 U 盘状态 */
if (s == USB_INT_SUCCESS) {
break; // 如果 U 盘准备就绪,跳出 for 循环
} else if (s == ERR_DISK_DISCON) {
break; // 如果 U 盘未准备就绪,只是跳出 for 循环,并未进一步处理
}
/* 针对某些型号 U 盘总是返回未准备好的处理 */
if (CH376GetDiskStatus() >= DEF_DISK_MOUNTED && i >= 5) {
break;
}
}
OLED_DISPLAY_8x16_BUFFER(6, " U Disk Init! "); // 显示 U 盘初始化完成提示信息
delay_ms(200); // 延时确保指令操作正确

/* 文件操作相关的代码 */
s = CH376FileCreatePath("/Hank.txt"); // 创建文件
delay_ms(200); // 延时确保指令操作正确
s = sprintf((char *)buf, "Bit by bit:https://uinika.github.io/"); // 将字符放置到缓冲区数组 buf,并返回数组长度
s = CH376ByteWrite(buf, s, NULL); // 将数据写入 Hank.txt 文件
delay_ms(200); // 延时确保指令操作正确
s = CH376FileClose(TRUE); // 关闭文件保存数据
OLED_DISPLAY_8x16_BUFFER(6, " U DISK SUCCESS "); // 显示 U 盘操作完成提示信息

/* 循环判断 U 盘是否拔出 */
while (CH376DiskConnect() == USB_INT_SUCCESS) {
delay_ms(500);
}
OLED_DISPLAY_8x16_BUFFER(6, " "); // 不显示任何提示信息
delay_ms(200); // 延时确保指令操作正确
}
}

4 × 4 矩阵键盘

将 4 × 4 矩阵键盘连接到STM32F103C8T6PA0 ~ PA7共 4 个 GPIO 引脚,同时断开 ADC 输入相关的P8跳线座、模拟量摇杆对应的P17跳线座、触摸按键对应的P10跳线座、旋转编码器对应的P18跳线座。

上面的电路图里的红色与蓝色接线,作用类似于地图的经线与纬线,只需要获取按键触发的经纬线,就可以知道当前哪个按键被按下。例如,如果按下【A】键,此时将PA0 ~ PA3设置为上拉电阻输入模式并置为高电平1,再将PA4 ~ PA7设置为推挽输出模式并置为低电平0,此时按下【A】键让 PA2 与 PA6 导通,此时 PA2 被 PA6 下拉为低电平,即 PA2 对应的【8】【9】【A】【B】当中有按键被按下。

接下来,交换两组 GPIO 的工作模式,即让PA0 ~ PA3设置为推挽输出模式并置为低电平0,然后PA4 ~ PA7设置为上拉电阻输入模式并置为高电平1,此时按下【A】键会让 PA2 把 PA6 下拉为低电平,由此可知按下【A】键会导致 PA2 和 PA6 分别被下拉为低电平0,根据矩阵键盘的经纬线原则,就可以定位当前被按下的是【A】键。这种方式的缺点在于,同一时间只允许 1 枚按键被按下,如果发生多个按键同时按下的情况,就不能正确的判断按键位置。

注意:矩阵键盘直接连接到微控制器会占用大量 GPIO 引脚,因此也可以采用专用的矩阵键盘驱动芯片CH456(可同时驱动 16 位数码管以及 8×8 矩阵按键)、MAX7300自动扫描矩阵键盘状态。

KEYPAD4×4.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/** Hardware/KEYPAD4×4/KEYPAD4×4.h */
#ifndef __KEYPAD4x4_H
#define __KEYPAD4x4_H
#include "delay.h"
#include "sys.h"

#define KEYPAD4x4PORT GPIOA // 定义 GPIO 分组

/* 矩阵键盘连接的 GPIO 引脚 */
#define KEY1 GPIO_Pin_0
#define KEY2 GPIO_Pin_1
#define KEY3 GPIO_Pin_2
#define KEY4 GPIO_Pin_3
#define KEYa GPIO_Pin_4
#define KEYb GPIO_Pin_5
#define KEYc GPIO_Pin_6
#define KEYd GPIO_Pin_7

void KEYPAD4x4_Init(void); // 初始化矩阵键盘,正向
void KEYPAD4x4_Init2(void); // 初始化矩阵键盘,反向
u8 KEYPAD4x4_Read(void); // 读取被按下按键的键值

#endif

KEYPAD4×4.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/** Hardware/KEYPAD4×4/KEYPAD4×4.c  */
#include "KEYPAD4x4.h"

/** 初始化矩阵键盘,正向 */
void KEYPAD4x4_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = KEYa | KEYb | KEYc | KEYd; // 选择 GPIO 引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 设置为上拉电阻输入模式
GPIO_Init(KEYPAD4x4PORT, &GPIO_InitStructure);

GPIO_InitStructure.GPIO_Pin = KEY1 | KEY2 | KEY3 | KEY4; // 选择 GPIO 引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 设置为推挽输出方式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(KEYPAD4x4PORT, &GPIO_InitStructure);
}

/** 初始化矩阵键盘,反向 */
void KEYPAD4x4_Init2(void) {
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = KEY1 | KEY2 | KEY3 | KEY4; // 选择 GPIO 引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 设置为上拉电阻输入模式
GPIO_Init(KEYPAD4x4PORT, &GPIO_InitStructure);

GPIO_InitStructure.GPIO_Pin = KEYa | KEYb | KEYc | KEYd; // 选择 GPIO 引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 设置为推挽输出方式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(KEYPAD4x4PORT, &GPIO_InitStructure);
}

/** 矩阵键盘读取函数,返回值为当前被按下按键的键值 */
u8 KEYPAD4x4_Read(void) {
u8 a = 0, b = 0; // 定义变量
KEYPAD4x4_Init(); // 调用按键初始化函数

GPIO_ResetBits(KEYPAD4x4PORT, KEY1 | KEY2 | KEY3 | KEY4); // 设置 1234 为低电平
GPIO_SetBits(KEYPAD4x4PORT, KEYa | KEYb | KEYc | KEYd); // 设置 ABCD 为高电平

/* 检测按键是否被按下 */
if (!GPIO_ReadInputDataBit(KEYPAD4x4PORT, KEYa) || !GPIO_ReadInputDataBit(KEYPAD4x4PORT, KEYb) || !GPIO_ReadInputDataBit(KEYPAD4x4PORT, KEYc) || !GPIO_ReadInputDataBit(KEYPAD4x4PORT, KEYd)) {
delay_ms(20); // 按键去抖,延时 20 毫秒

/* 继续检测按键是否被按下 */
if (!GPIO_ReadInputDataBit(KEYPAD4x4PORT, KEYa) || !GPIO_ReadInputDataBit(KEYPAD4x4PORT, KEYb) || !GPIO_ReadInputDataBit(KEYPAD4x4PORT, KEYc) || !GPIO_ReadInputDataBit(KEYPAD4x4PORT, KEYd)) {
a = GPIO_ReadInputData(KEYPAD4x4PORT) & 0xff; // 获取 PA0 ~ PA7 这 8 个 GPIO 引脚的状态,并放入变量 a
}

KEYPAD4x4_Init2(); // 调用按键初始化函数,反转
GPIO_SetBits(KEYPAD4x4PORT, KEY1 | KEY2 | KEY3 | KEY4); // 设置 1234 为高电平
GPIO_ResetBits(KEYPAD4x4PORT, KEYa | KEYb | KEYc | KEYd); // 设置 ABCD 为低电平
b = GPIO_ReadInputData(KEYPAD4x4PORT) & 0xff; // 再次获取 8 个 GPIO 引脚状态,并放入变量 b
a = a | b; // 对 2 次获取的按键状态进行或运算

/* 对键值进行判断 */
switch (a) {
case 0xee: b = 16; break;
case 0xed: b = 15; break;
case 0xeb: b = 14; break;
case 0xe7: b = 13; break;
case 0xde: b = 12; break;
case 0xdd: b = 11; break;
case 0xdb: b = 10; break;
case 0xd7: b = 9; break;
case 0xbe: b = 8; break;
case 0xbd: b = 7; break;
case 0xbb: b = 6; break;
case 0xb7: b = 5; break;
case 0x7e: b = 4; break;
case 0x7d: b = 3; break;
case 0x7b: b = 2; break;
case 0x77: b = 1; break;
default: b = 0; break; // 键值错误,多发生于多个按键同时按下时
}

/* 等待按键放开 */
while (!GPIO_ReadInputDataBit(KEYPAD4x4PORT, KEY1) || !GPIO_ReadInputDataBit(KEYPAD4x4PORT, KEY2) || !GPIO_ReadInputDataBit(KEYPAD4x4PORT, KEY3) || !GPIO_ReadInputDataBit(KEYPAD4x4PORT, KEY4));
delay_ms(20); // 延时去抖
}
return (b); // 返回 b 作为按键编号
}

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/** User/main.c */
#include "KEYPAD4x4.h"
#include "delay.h"
#include "oled0561.h"
#include "relay.h"
#include "stm32f10x.h"
#include "sys.h"

int main(void) {
u8 s;
delay_ms(500); // 等待相关元器件上电并且工作正常
RCC_Configuration(); // 初始化系统时钟
RELAY_Init(); // 初始化继电器
I2C_Configuration(); // 初始化 I²C 总线
OLED0561_Init(); // 初始化 OLED

/* 显示文字信息到 OLED 屏幕 */
OLED_DISPLAY_8x16_BUFFER(0, " Hank ");
OLED_DISPLAY_8x16_BUFFER(3, " Keyboard Test: ");

KEYPAD4x4_Init(); // 初始化阵列键盘

while (1) {
s = KEYPAD4x4_Read(); // 调用按键值读取函数,获取按键值

/* 按键值不为 0 说明有按键按下 */
if (s != 0) {
OLED_DISPLAY_8x16_BUFFER(6, " Key NO. ");
OLED_DISPLAY_8x16(6, 8 * 8, s / 10 + 0x30); // 显示按键编号的十位
OLED_DISPLAY_8x16(6, 9 * 8, s % 10 + 0x30); // 显示按键编号的个位
}
}
}

EXTI 与 NVIC

中断是指一个突然发生的事件打断了当前 MCU 的执行流程,微控制器转而去处理该中断事件,完成之后再重新返回到之前的执行流程。STM32F103C8T6允许多种类型的中断源,包括外部 GPIOADCUSARTI²CRTCUSBPVD等。本小节将要进行分析的 EXTI 外部中断支持将所有 GPIO 设置为中断源,可由上升沿下降沿高低电平3 种方式进行触发,并且可以选择中断方式(触发后进入专门的中断处理函数,需要 MCU 介入)或者事件方式(触发后自动运行相应功能,毋须 MCU 介入)触发。

下面的表格,整理了STM32F103C8T6与 EXTI 外部中断相关的所有 GPIO 引脚、中断标志位、中断处理函数:

GPIO 引脚 中断标志位 中断处理函数
PA0 ~ PG0 EXTI0 EXTI0_IRQHandler()
PA1 ~ PG1 EXTI1 EXTI1_IRQHandler()
PA2 ~ PG2 EXTI2 EXTI2_IRQHandler()
PA3 ~ PG3 EXTI3 EXTI3_IRQHandler()
PA4 ~ PG4 EXTI4 EXTI4_IRQHandler()
GPIO 引脚 中断标志位 中断处理函数
PA5 ~ PG5 EXTI5 EXTI9_5_IRQHandler()
PA6 ~ PG6 EXTI6 EXTI9_5_IRQHandler()
PA7 ~ PG7 EXTI7 EXTI9_5_IRQHandler()
PA8 ~ PG8 EXTI8 EXTI9_5_IRQHandler()
PA9 ~ PG9 EXTI9 EXTI9_5_IRQHandler()
GPIO 引脚 中断标志位 中断处理函数
PA10 ~ PG10 EXTI10 EXTI15_10_IRQHandler()
PA11 ~ PG11 EXTI11 EXTI15_10_IRQHandler()
PA12 ~ PG12 EXTI12 EXTI15_10_IRQHandler()
PA13 ~ PG13 EXTI13 EXTI15_10_IRQHandler()
PA14 ~ PG14 EXTI14 EXTI15_10_IRQHandler()
PA15 ~ PG15 EXTI15 EXTI15_10_IRQHandler()

NVIC 嵌套中断向量控制器,这里的嵌套是指中断的嵌套,中断向量是指中断处理函数的入口地址,即 NVIC 本质上就是 Cortex M3 当中,用于对中断处理函数进行相应设置的硬件单元。NVIC 当中涉及两个比较重要的概念:

  • 抢占式优先级(主优先级):中断嵌套时,主优先级高的中断可以嵌套在主优先级低的中断当中,相同主优先级的中断不能进行嵌套。
  • 响应式优先级(子优先级):如果同时发生相同主优先级的中断,STM32F103C8T6会首先处理子优先级较高的中断。
1
2
3
/* 数字越小,优先级越高 */
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2; // 设置 NVIC 抢占优先级为 2
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2; // 设置 NVIC 子优先级为 2

抢占和响应优先级数值的设定范围,由sys.h当中定义的 NVIC 分组号NVIC_PriorityGroup_x来进行配置,不同的分组决定了抢占和响应优先级的数量,如果超过分组最大的优先级设定范围,代码可能会在执行过程中发生不可预知的错误,本实验相关的 NVIC 分组号由下面代码进行设置:

1
2
3
4
/* 嵌套中断向量控制器 NVIC 配置 */
void NVIC_Configuration(void){
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 设置 NVIC 中断分组号为 2,即 4 个抢占优先级与 4 个响应优先级
}

最后,将前一小节主函数扫描矩阵键盘实验的代码,修改为通过下降沿进行触发的中断来判断,具体代码如下所示:

NVIC.h

1
2
3
4
5
6
7
8
9
/** Basic/nvic/NVIC.h */
#ifndef __NVIC_H
#define __NVIC_H
#include "sys.h"

extern u8 INT_MARK; // 中断标志位
void KEYPAD4x4_INT_INIT(void); // 阵列键盘中断初始化函数

#endif

NVIC.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/** Basic/nvic/NVIC.c */
#include "NVIC.h"

u8 INT_MARK; // 中断标志位

/** 阵列键盘中断初始化函数 */
void KEYPAD4x4_INT_INIT(void) {
NVIC_InitTypeDef NVIC_InitStruct; // 向量中断控制器 NVIC 结构体变量
EXTI_InitTypeDef EXTI_InitStruct; // 外部中断 EXTI 结构体变量

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 开启 GPIOA 时钟,该代码原本位于 KEYPAD4×4.c,由于不能对同一时钟反复配置,所以挪动到这里
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // 使能 AFIO 重映射功能时钟

/* 第 1 个中断 */
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource4); // 映射 GPIO 引脚至 EXTI,参数 GPIO_PinSource4 是需要连接到的中断通道
EXTI_InitStruct.EXTI_Line = EXTI_Line4; // 设置外部中断线
EXTI_InitStruct.EXTI_LineCmd = ENABLE; // 使能外部中断
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt; // 设置为中断模式
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling; // 设置为下降沿触发中断
EXTI_Init(&EXTI_InitStruct); // 初始化外部中断 EXTI 设置
NVIC_InitStruct.NVIC_IRQChannel = EXTI4_IRQn; // 设置 NVIC 中断请求通道
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; // 使能 NVIC 中断请求通道
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2; // 设置 NVIC 的抢占优先级为 2
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2; // 设置 NVIC 的子优先级为 2
NVIC_Init(&NVIC_InitStruct); // 初始化向量中断控制器 NVIC 设置

/* 第 2 个中断 */
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource5); // 映射 GPIO 引脚至 EXTI,参数 GPIO_PinSource5 是需要连接到的中断通道
EXTI_InitStruct.EXTI_Line = EXTI_Line5; // 设置外部中断线
EXTI_InitStruct.EXTI_LineCmd = ENABLE; // 使能外部中断
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt; // 设置为中断模式
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling; // 设置为下降沿触发中断
EXTI_Init(&EXTI_InitStruct); // 初始化外部中断 EXTI 设置
NVIC_InitStruct.NVIC_IRQChannel = EXTI9_5_IRQn; // 设置 NVIC 中断请求通道
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; // 使能 NVIC 中断请求通道
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2; // 设置 NVIC 的抢占优先级为 2
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2; // 设置 NVIC 的子优先级为 2
NVIC_Init(&NVIC_InitStruct); // 初始化向量中断控制器 NVIC 设置

/* 第 3 个中断 */
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource6); // 映射 GPIO 引脚至 EXTI,参数 GPIO_PinSource6 是需要连接到的中断通道
EXTI_InitStruct.EXTI_Line = EXTI_Line6; // 设置外部中断线
EXTI_InitStruct.EXTI_LineCmd = ENABLE; // 使能外部中断
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt; // 设置为中断模式
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling; // 设置为下降沿触发中断
EXTI_Init(&EXTI_InitStruct); // 初始化外部中断 EXTI 设置
NVIC_InitStruct.NVIC_IRQChannel = EXTI9_5_IRQn; // 设置 NVIC 中断请求通道
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; // 使能 NVIC 中断请求通道
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2; // 设置 NVIC 的抢占优先级为 2
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2; // 设置 NVIC 的子优先级为 2
NVIC_Init(&NVIC_InitStruct); // 初始化向量中断控制器 NVIC 设置

/* 第 4 个中断 */
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource7); // 映射 GPIO 引脚至 EXTI,参数 GPIO_PinSource7 是需要连接到的中断通道
EXTI_InitStruct.EXTI_Line = EXTI_Line7; // 设置外部中断线
EXTI_InitStruct.EXTI_LineCmd = ENABLE; // 使能外部中断
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt; // 设置为中断模式
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling; // 设置为下降沿触发中断
EXTI_Init(&EXTI_InitStruct); // 初始化外部中断 EXTI 设置
NVIC_InitStruct.NVIC_IRQChannel = EXTI9_5_IRQn; // 设置 NVIC 中断请求通道
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; // 使能 NVIC 中断请求通道
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2; // 设置 NVIC 的抢占优先级为 2
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2; // 设置 NVIC 的子优先级为 2
NVIC_Init(&NVIC_InitStruct); // 初始化向量中断控制器 NVIC 设置
}

/* 外部中断 4 中断处理函数 */
void EXTI4_IRQHandler(void) {
/* 判断 EXTI_Line4 中断线是否发生中断 */
if (EXTI_GetITStatus(EXTI_Line4) != RESET) {
INT_MARK = 1; // 标志位置 1,表示产生了按键中断
EXTI_ClearITPendingBit(EXTI_Line4); // 清除中断线内的标志位
}
}

/* 外部中断 5 6 7 中断处理函数,由于多路中断复用该处理函数,因此进入函数后需要判断当前中断属于哪条中断线 */
void EXTI9_5_IRQHandler(void) {
/* 判断 EXTI_Line5 中断线是否发生中断 */
if (EXTI_GetITStatus(EXTI_Line5) != RESET) {
INT_MARK = 2; // 标志位置 1,表示产生了按键中断
EXTI_ClearITPendingBit(EXTI_Line5); // 清除中断线内的标志位
}
/* 判断 EXTI_Line6 中断线是否发生中断 */
if (EXTI_GetITStatus(EXTI_Line6) != RESET) {
INT_MARK = 3; // 标志位置 1,表示产生了按键中断
EXTI_ClearITPendingBit(EXTI_Line6); // 清除中断线内的标志位
}
/* 判断 EXTI_Line7 中断线是否发生中断 */
if (EXTI_GetITStatus(EXTI_Line7) != RESET) {
INT_MARK = 4; // 标志位置 1,表示产生了按键中断
EXTI_ClearITPendingBit(EXTI_Line7); // 清除中断线内的标志位
}
}

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/** User/main.c */
#include "KEYPAD4x4.h"
#include "NVIC.h"
#include "delay.h"
#include "oled0561.h"
#include "relay.h"
#include "stm32f10x.h"
#include "sys.h"

int main(void) {
u8 s;
delay_ms(500); // 等待相关元器件上电并且工作正常
RCC_Configuration(); // 初始化系统时钟
RELAY_Init(); // 初始化继电器
I2C_Configuration(); // 初始化 I²C 总线
OLED0561_Init(); // 初始化 OLED

/* 显示文字信息到 OLED 屏幕 */
OLED_DISPLAY_8x16_BUFFER(0, " Hank ");
OLED_DISPLAY_8x16_BUFFER(3, " Keyboard Test: ");

INT_MARK = 0; // 标志位,用于标识当前是否发生中断,默认为 0
NVIC_Configuration(); // 设置中断优先级
KEYPAD4x4_Init(); // 初始化阵列键盘
KEYPAD4x4_INT_INIT(); // 初始化阵列键盘中断

while (1) {
/* 判断上面声明的中断标志位,如果非 0 则表示有按键按下 */
if (INT_MARK) {
INT_MARK = 0; // 中断标志位变量清 0
s = KEYPAD4x4_Read(); // 调用按键值读取函数,获取按键值

/* 按键值不为 0 说明有按键按下 */
if (s != 0) {
OLED_DISPLAY_8x16_BUFFER(6, " Key NO. ");
OLED_DISPLAY_8x16(6, 8 * 8, s / 10 + 0x30); // 显示按键编号的十位
OLED_DISPLAY_8x16(6, 9 * 8, s % 10 + 0x30); // 显示按键编号的个位
}
}
}
}

SG90 舵机

SG90 舵机工作电压为5V,最大工作电流为2A,可旋转角度为180°。由于需要使用到触摸按键来进行舵机控制,因此实验开始之前,需要先短接触摸按键相关的P10跳线座。

上面接线图当中,标号为PWM的橙色线用于传输脉冲宽度调制信号,标号为VCC的红色线和标号为GND的棕色线分别用于表示电源正负极。STM32F103C8T6对于舵机的控制主要依靠 PWM 信号线来传输连续的波形。

上面的信号波形示意图当中,如果高电平持续时间长度介于0.5ms ~ 2.5ms之间,则会让舵盘的转角介于0° ~ 180°范围以内。

SG90.h

1
2
3
4
5
6
7
8
9
10
11
12
13
/** Hardware/SG90/SG90.h */
#ifndef __SG90_H
#define __SG90_H
#include "delay.h"
#include "sys.h"

#define SE_PORT GPIOA // 定义 GPIO 分组
#define SE_OUT GPIO_Pin_15 // 定义 GPIO 引脚

void SG90_Init(void); // SG90 舵机初始化函数
void SG90_angle(u8 a); // 舵机旋转角度控制函数

#endif

SG90.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/** Hardware/SG90/SG90.c  */
#include "SG90.h"

/** SG90 舵机初始化函数 */
void SG90_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = SE_OUT; // 设置为 PA15 引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 设置为推挽输出工作模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // GPIO 引脚输出频率为 50MHz
GPIO_Init(SE_PORT, &GPIO_InitStructure);

GPIO_WriteBit(SE_PORT, SE_OUT, (BitAction)(0)); // 初始化 GPIO 引脚为低电平 0
}

/** 舵机旋转角度控制函数,参数 a 是角度 0° ~ 180° */
void SG90_angle(u8 a) {
u8 b = 100; // 角度偏移量校正
GPIO_WriteBit(SE_PORT, SE_OUT, (BitAction)(1)); // PA15 引脚输出高电平 1
delay_us(500 + a * 10 + b); // 高电平状态延时时间
GPIO_WriteBit(SE_PORT, SE_OUT, (BitAction)(0)); // PA15 引脚输出低电平 0
delay_us(19500 - a * 10 - b); // 低电平状态延时时间
}

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/** User/main.c */
#include "SG90.h"
#include "delay.h"
#include "oled0561.h"
#include "relay.h"
#include "stm32f10x.h"
#include "sys.h"
#include "touch_key.h"

int main(void) {
delay_ms(500); // 等待相关元器件上电并且工作正常
RCC_Configuration(); // 初始化系统时钟
RELAY_Init(); // 初始化继电器
I2C_Configuration(); // 初始化 I²C 总线
OLED0561_Init(); // 初始化 OLED

/* 显示文字信息到 OLED 屏幕 */
OLED_DISPLAY_8x16_BUFFER(0, " Hank ");
OLED_DISPLAY_8x16_BUFFER(3, " SG90 Test ");

TOUCH_KEY_Init(); // 初始化触摸按键
SG90_Init(); // 初始化 SG90 舵机
SG90_angle(0); // 设置舵机初始角度为最小值 0

while (1) {
/* 如果 A 键按下 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A)) {
OLED_DISPLAY_8x16_BUFFER(6, " Angle 0 ");
SG90_angle(0); // 设置舵机角度为 0
}
/* 如果 B 键按下 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_B)) {
OLED_DISPLAY_8x16_BUFFER(6, " Angle 45 ");
SG90_angle(45); // 设置舵机角度为 45
}
/* 如果 C 键按下 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_C)) {
OLED_DISPLAY_8x16_BUFFER(6, " Angle 90 ");
SG90_angle(90); // 设置舵机角度为 90
}
/* 如果 D 键按下 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_D)) {
OLED_DISPLAY_8x16_BUFFER(6, " Angle 180 ");
SG90_angle(180); // 设置舵机角度为 180
}
}
}

PWM 脉冲宽度调制

脉冲宽度调制(PWM,Pulse Width Modulation)有时也被称为占空比,可以通过STM32F103C8T6的定时器功能(包括 1 个高级定时器TIM1和 3 个普通定时器TIM2TIM3TIM4)来产生 PWM。具体而言,就是借助定时器的TIMx_ARR寄存器溢出值产生 PWM 周期,然后由TIMx_CCRx寄存器产生高低电平的占空比。

本实验沿用前一小节通过延时函数驱动舵机的程序与跳线设置,此外为了连接定时器相关的 GPIO 输出,需要使用杜邦线将PB0PA15短接。本实验将会通过触摸按键产生 PWM 信号来控制舵机,由于 PWM 是使用定时器生成的,所以无论用户是否按下触摸按键 PWM 都会一直处于工作状态,此时由于输入电平长度有一定误差,因而按下触摸按键时舵机会产生抖动。另外,在舵机工作的同时,实验电路中核心板附近连接到 PB0 引脚的 LED 指示灯会持续点亮,由于本实验会将PB0PA15短接,因此 LED 也会体现出当前 PWM 生成的信号状态。

pwm.h

1
2
3
4
5
6
7
8
/** Basic/pwm/pwm.h */
#ifndef __PWM_H
#define __PWM_H
#include "sys.h"

void TIM3_PWM_Init(u16 arr, u16 psc);

#endif

pwm.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/** Basic/pwm/pwm.c  */
#include "pwm.h"

/** 定时器 3 的 PWM 初始化函数,参数 arr 是计数重载值,参数 psc 是预分频系数 */
void TIM3_PWM_Init(u16 arr, u16 psc) {
/* PWM 相关的 3 种结构体 */
GPIO_InitTypeDef GPIO_InitStrue;
TIM_OCInitTypeDef TIM_OCInitStrue;
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStrue;

/* PWM 相关的时钟设置 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 使能 TIM3 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 使能 GPIOB 时钟,因为 LED 连接在 PB0 引脚
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // 使能 AFIO 时钟,因为定时器 3 的通道 3 复用 BP5 引脚

/* GPIO 设置 */
GPIO_InitStrue.GPIO_Pin = GPIO_Pin_0; // 选择 GPIO0 引脚
GPIO_InitStrue.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出模式
GPIO_InitStrue.GPIO_Speed = GPIO_Speed_50MHz; // 输出频率 50MHz
GPIO_Init(GPIOB, &GPIO_InitStrue);

// 没有进行 GPIO 引脚重映射时,TIM3 的 4 个通道 CH1、CH2、CH3、CH4 分别对应 PA6、PA7、PB0、PB1 引脚
// GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE); // 开启部分重映射时,TIM3 的 4 个通道分别被映射至 PB4、PB5、PB0、PB1

TIM_TimeBaseInitStrue.TIM_Period = arr; // 自动重装载值为 arr
TIM_TimeBaseInitStrue.TIM_Prescaler = psc; // 预分频系数为 psc
TIM_TimeBaseInitStrue.TIM_CounterMode = TIM_CounterMode_Up; // 工作模式为向上累加计数
TIM_TimeBaseInitStrue.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟分频因子,用于内部计数器滤波,仅影响电路的稳定性
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStrue); // 初始化 定时器3 TIM3 设置

TIM_OCInitStrue.TIM_OCMode = TIM_OCMode_PWM1; // 设置为 TIM 脉冲宽度调试模式 1,即当前计数值小于 CCR 时输出高电平 1,否则输出 0
TIM_OCInitStrue.TIM_OCPolarity = TIM_OCPolarity_High; // 设置极性,即有效电平为高电平 1,无效电平为低电平 0
TIM_OCInitStrue.TIM_OutputState = TIM_OutputState_Enable; // 定时器状态输出使能,即将定时器的状态通过 GPIO 引脚输出
TIM_OC3Init(TIM3, &TIM_OCInitStrue); // 初始化定时器 3 的通道 3
TIM_OC3PreloadConfig(TIM3, TIM_OCPreload_Enable); // 使能预装载寄存器
TIM_Cmd(TIM3, ENABLE); // 使能定时器 3
}

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/** User/main.c */
#include "delay.h"
#include "oled0561.h"
#include "pwm.h"
#include "relay.h"
#include "stm32f10x.h"
#include "sys.h"
#include "touch_key.h"

int main(void) {
delay_ms(500); // 等待相关元器件上电并且工作正常
RCC_Configuration(); // 初始化系统时钟
RELAY_Init(); // 初始化继电器
I2C_Configuration(); // 初始化 I²C 总线
OLED0561_Init(); // 初始化 OLED

/* 显示文字信息到 OLED 屏幕 */
OLED_DISPLAY_8x16_BUFFER(0, " Hank ");
OLED_DISPLAY_8x16_BUFFER(3, " SG90 Test2 ");
TOUCH_KEY_Init(); // 初始化触摸按键

/*
* 计算公式为溢出时间 Tout 秒 = (arr+1)(psc+1)/Tclk,即 (59999+1)*(23+1)/72000000 = 0.02s = 20ms
* 公式中 Tclk 为通用定时器时钟,如果 APB1 没有分频,则为 72MHz 系统时钟
*/
TIM3_PWM_Init(59999, 23); // 通过定时器 3 实现 PWM 输出,参数 59999 是定时器溢出值 ARR,参数 23 是时钟分频系数

while (1) {
/* 如果 D 键按下 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_A)) {
OLED_DISPLAY_8x16_BUFFER(6, " Angle 0 ");
/* 如果整个 PWM 周期为 20ms,而已知定时 20ms 需要 60000 次计数,那么定时 0.5 毫秒需要的计数次数 = 0.5 * 60000/20 = 1500 */
TIM_SetCompare3(TIM3, 1500); // 设置定时器 3 的 CCR2值调节占空比,1500 表示 0°
}
/* 如果 D 键按下 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_B)) {
OLED_DISPLAY_8x16_BUFFER(6, " Angle 45 ");
TIM_SetCompare3(TIM3, 3000); // 设置定时器 3 的 CCR2值调节占空比,3000 表示 45°
}
/* 如果 D 键按下 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_C)) {
OLED_DISPLAY_8x16_BUFFER(6, " Angle 90 ");
TIM_SetCompare3(TIM3, 4500); // 设置定时器 3 的 CCR2值调节占空比,4500 表示 90°
}
/* 如果 D 键按下 */
if (!GPIO_ReadInputDataBit(TOUCH_KEYPORT, TOUCH_KEY_D)) {
OLED_DISPLAY_8x16_BUFFER(6, " Angle 180 ");
TIM_SetCompare3(TIM3, 7500); // 设置定时器 3 的 CCR2值调节占空比,7500 表示 180°
}
}
}

DHT11 温湿度传感器

DHT11 是一款由广州奥松电子出品的温湿度传感器,其供电电压介于3V ~ 5.5V之间,温度采集范围为0°C ~ 50°C,湿度采集范围为20% ~ 90%,一共拥有 4 个引脚(第 1 脚连接至5V电源,第 2 脚连接到微控制器的PA15引脚,第 3 脚悬空,第 4 脚连接至电源负极 GND),采用单总线通信方式与STM32F103C8T6进行连接。

DHT11 采用简化的串行单总线通信方式,即所有的数据交换与控制都只通过一条数据线完成。单总线结构通常需要外接一枚4.7kΩ上拉电阻,在总线闲置时将其钳制为高电平状态。STM32F103C8T6DHT11通信时,每次会传送 40 位数据(8 个字节),并且遵循高位先出的顺序,具体数据格式如下所示:

1
8 位湿度整数部分 | 8 位湿度小数部分 | 8 位温度整数部分 | 8 位温度小数部分 | 8 位校验位

注意: 8 位校验位的值等于前面 4 部分累加结果的最后 8 位。进行数据采集时,必须间隔 1 秒以上进行,以确保获得正确的结果。

dht11.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** Hardware/DHT11/dht11.h */
#ifndef __DHT11_H
#define __DHT11_H
#include "delay.h"
#include "sys.h"

#define DHT11PORT GPIOA // 定义 GPIO 分组
#define DHT11_IO GPIO_Pin_15 // 定义 GPIO 引脚

/* 用于封装 dht11.c 内部处理流程 */
void DHT11_IO_OUT(void);
void DHT11_IO_IN(void);
void DHT11_RST(void);
u8 Dht11_Check(void);
u8 Dht11_ReadBit(void);
u8 Dht11_ReadByte(void);

/* 提供给 main 函数进行调用 */
u8 DHT11_Init(void);
u8 DHT11_ReadData(u8 *h);

#endif

dht11.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
/** Hardware/DHT11/dht11.c  */
#include "dht11.h"

/** 将单总线连接的 GPIO 引脚设置为推挽输出模式,用于单总线发送数据 */
void DHT11_IO_OUT(void) {
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = DHT11_IO;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 设置推挽输出工作模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DHT11PORT, &GPIO_InitStructure);
}

/** 将单总线连接的 GPIO 引脚设置为上拉电阻输入模式,用于单总线接收数据 */
void DHT11_IO_IN(void) {
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = DHT11_IO;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 设置上拉电阻输入模式
GPIO_Init(DHT11PORT, &GPIO_InitStructure);
}

/** 发送起始信号给 DHT11 传感器,该信号由 18ms 低电平和 20us~40us 高电平组成 */
void DHT11_RST(void) {
DHT11_IO_OUT();
GPIO_ResetBits(DHT11PORT, DHT11_IO); // 置为低电平 0
delay_ms(20); // 延时 18ms
GPIO_SetBits(DHT11PORT, DHT11_IO); // 置为高电平 1
delay_us(30); // 延时 20us~40us
}

/** 等待 DHT11 响应状态,返回 0 表示接收成功,返回 1 表示未检测到设备 */
u8 Dht11_Check(void) {
u8 retry = 0;
DHT11_IO_IN(); // 将单总线连接的 GPIO 引脚设置为上拉电阻输入模式

/* 如果检测到 GPIO 引脚接收到低电平就跳出循环,变量 retry 用于避免陷入死循环 */
while (GPIO_ReadInputDataBit(DHT11PORT, DHT11_IO) && retry < 100) {
retry++;
delay_us(1);
}
/* 如果等待超过 100ms 依然未检测到低电平,就返回未检测到设备的状态码 1 */
if (retry >= 100) {
return 1;
} else {
retry = 0; // 清零以便用于下次循环
}

/* 如果检测到 GPIO 引脚接收到高电平则跳出循环 */
while (!GPIO_ReadInputDataBit(DHT11PORT, DHT11_IO) && retry < 100) {
retry++;
delay_us(1);
}
if (retry >= 100) {
return 1;
}

return 0; // 返回接收成功状态码 0
}

/** 从 DHT11 读取 1 个位的数据,返回值是读取到的二进制数据 0 或 1 */
u8 Dht11_ReadBit(void) {
u8 retry = 0;

/* 检测到 GPIO 引脚接收到低电平就跳出循环 */
while (GPIO_ReadInputDataBit(DHT11PORT, DHT11_IO) && retry < 100) {
retry++;
delay_us(1);
}
retry = 0;

/* 检测到 GPIO 引脚接收到高电平则跳出循环 */
while (!GPIO_ReadInputDataBit(DHT11PORT, DHT11_IO) && retry < 100) {
retry++;
delay_us(1);
}

/* 判断通信时序对应的电平状态 */
delay_us(40); // 延时 40us 以后,开始判断 GPIO 电平状态
if (GPIO_ReadInputDataBit(DHT11PORT, DHT11_IO)) {
return 1;
} else {
return 0;
}
}

/** 从 DHT11 读取 1 个字节数据,返回值是读取到的字节数据 */
u8 Dht11_ReadByte(void) {
u8 i, dat;
dat = 0;

/* 将位读取函数循环调用 8 次 */
for (i = 0; i < 8; i++) {
dat <<= 1;
dat |= Dht11_ReadBit();
}
return dat;
}

/** DHT11 初始化函数,返回 0 表示成功,返回 1 表示设备错误 */
u8 DHT11_Init(void) {
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOE, ENABLE); // 使能 APB2 外设时钟
DHT11_RST(); // 让 DHT11 连接的 GPIO 引脚复位,从而发出起始信号
return Dht11_Check(); // 返回 DHT11 响应状态函数的返回值
}

/** 读取 DHT11 温湿度数据,参数 h 用于存放读取到的温湿度数据的整数部分 */
u8 DHT11_ReadData(u8 *h) {
u8 buf[5]; // 缓存由 DHT11 读取的 40 位数据,即 8 个字节
u8 i;
DHT11_RST(); // 复位 DHT11 对应的 GPIO 引脚,发送起始信号

/* 判断 DHT11 响应状态 */
if (Dht11_Check() == 0) {
/* 循环 5 次读取 5 个字节数据 */
for (i = 0; i < 5; i++) {
buf[i] = Dht11_ReadByte(); // 将读取到的字节数据存放至缓存变量 buf
}
/* 对数据校验位进行校验 */
if ((buf[0] + buf[1] + buf[2] + buf[3]) == buf[4]) {
*h = buf[0]; // 将读取到的第 1 个数据,即湿度值的整数部分存放至指针 h
h++; // 指针地址增加 1
*h = buf[2]; // 将读取到的第 3 个数据,即温度值的整数部分存放至指针 h
}
} else {
return 1; // 读取失败
}
return 0; // 读取成功
}

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/** User/main.c */
#include "delay.h"
#include "dht11.h"
#include "oled0561.h"
#include "relay.h"
#include "stm32f10x.h"
#include "sys.h"

int main(void) {
u8 b[2]; // 保存由 DHT11 读取的温湿度值
delay_ms(1000); // 等待 DHT11 上电并且工作正常
RCC_Configuration(); // 初始化系统时钟
RELAY_Init(); // 初始化继电器
I2C_Configuration(); // 初始化 I²C 总线
OLED0561_Init(); // 初始化 OLED

/* 显示文字信息到 OLED 屏幕 */
OLED_DISPLAY_8x16_BUFFER(0, " Hank ");
OLED_DISPLAY_8x16_BUFFER(2, " DHT11 Test ");

/* 调用 DHT11 初始化函数,返回 0 表示成功,返回 1 说明失败 */
if (DHT11_Init() == 0) {
OLED_DISPLAY_8x16_BUFFER(4, "Humidity: % ");
OLED_DISPLAY_8x16_BUFFER(6, "Temperature: C");
} else {
OLED_DISPLAY_8x16_BUFFER(4, " Init Error! "); // 初始化失败时显示错误信息
}
delay_ms(1000); // 初始化 DHT11 之后延时不低于 1 秒

while (1) {
/* 将读取到的十进制温湿度数据保存至数组变量 a,同时返回 0 表示成功,返回 1 表示失败 */
if (DHT11_ReadData(b) == 0) {
/* 显示湿度值的十位与个位 */
OLED_DISPLAY_8x16(4, 9 * 8, b[0] / 10 + 0x30);
OLED_DISPLAY_8x16(4, 10 * 8, b[0] % 10 + 0x30);
/* 显示温度值的十位与个位 */
OLED_DISPLAY_8x16(6, 12 * 8, b[1] / 10 + 0x30);
OLED_DISPLAY_8x16(6, 13 * 8, b[1] % 10 + 0x30);
} else {
OLED_DISPLAY_8x16_BUFFER(6, " Read Error! "); // 读取错误时显示提示信息
}

delay_ms(1000); // 确保数据刷新周期不低于 1 秒钟
}
}

MPU6050 加速度陀螺仪

MPU-6050 整合了三轴加速度和三轴陀螺仪传感器,有效降低了封装尺寸以及两者配合工作时的误差。本小节实验程序将会在 OLED 屏幕左侧的X Y Z字符后面分别显示三轴加速度(位移)传感值,右侧的X Y Z字符后面则用于显示三轴陀螺仪(方向)传感值。

MPU6050模块的VCCGND引脚分别连接至实验电路的5VGND进行供电,然后再将 I²C 通信相关的SCLSDA分别连接至STM32F103C8T6PB6PB7引脚,这样就完成了实验电路的全部连接工作,下面的表格对模块的各引脚进行了详细的功能说明:

MPU6050 模块引脚 STM32F103C8T6 引脚 功能定义
VCC 5V 电源正极
GND GND 电源负极
SCL PB6 从模式 I²C 时钟线
SDA PB7 从模式 I²C 数据线
XCL 悬空 主模式 I²C 时钟线
XDA 悬空 主模式 I²C 数据线
AD0 悬空 设备地址,悬空时为低电平
INT 悬空 中断输出,即向微控制器发送中断信号

MPU6050 的设备地址为0xD0,而官方数据手册上给出的地址是0x68,由于 I²C 的地址为 7 位,使用 8 位字节的方式进行表达时,会出现两种状况:在低位补0结果为1101 0000,在高位补0结果为0110 1000,本实验电路使用STM32F103C8T6的 I²C 外设时,采用的是0xD0地址。

MPU6050.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/** Hardware/MPU6050/MPU6050.h */
#ifndef __MPU6050_H
#define __MPU6050_H
#include "delay.h"
#include "i2c.h"
#include "sys.h"

#define MPU6050_ADD 0xD0 // MPU6050 设备地址,如果模块的 AD0 引脚处于悬空或低电平状态时,该地址为 0xD0,否则为 0xD2

/* MPU6050 寄存器地址映射 */
#define MPU6050_RA_SMPLRT_DIV 0x19
#define MPU6050_RA_CONFIG 0x1A
#define MPU6050_RA_GYRO_CONFIG 0x1B
#define MPU6050_RA_ACCEL_CONFIG 0x1C
#define MPU6050_RA_PWR_MGMT_1 0x6B

/* MPU6050 的 X、Y、Z 原始数据所在的寄存器地址 */
#define MPU6050_RA_ACCEL_XOUT_H 0x3B
#define MPU6050_RA_ACCEL_XOUT_L 0x3C
#define MPU6050_RA_ACCEL_YOUT_H 0x3D
#define MPU6050_RA_ACCEL_YOUT_L 0x3E
#define MPU6050_RA_ACCEL_ZOUT_H 0x3F
#define MPU6050_RA_ACCEL_ZOUT_L 0x40
#define MPU6050_RA_TEMP_OUT_H 0x41
#define MPU6050_RA_TEMP_OUT_L 0x42
#define MPU6050_RA_GYRO_XOUT_H 0x43
#define MPU6050_RA_GYRO_XOUT_L 0x44
#define MPU6050_RA_GYRO_YOUT_H 0x45
#define MPU6050_RA_GYRO_YOUT_L 0x46
#define MPU6050_RA_GYRO_ZOUT_H 0x47
#define MPU6050_RA_GYRO_ZOUT_L 0x48

void MPU6050_Init(void); // MPU6050 初始化函数
void MPU6050_READ(u16 *n); // MPU6050 原始数据读取函数

#endif

MPU6050.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/** Hardware/MPU6050/MPU6050.c  */
#include "MPU6050.h"

/** MPU6050 初始化函数 */
void MPU6050_Init(void) {
I2C_SAND_BYTE(MPU6050_ADD, MPU6050_RA_PWR_MGMT_1, 0x80); // 调用 I²C 总线发送函数,让 MPU6050 进入复位并解除休眠
delay_ms(1000); // 等待设备命令执行就绪
I2C_SAND_BYTE(MPU6050_ADD, MPU6050_RA_PWR_MGMT_1, 0x00); // 调用 I²C 总线发送函数,让 MPU6050 进入正常工作状态
I2C_SAND_BYTE(MPU6050_ADD, MPU6050_RA_SMPLRT_DIV, 0x07); // 调用 I²C 总线发送函数,设置陀螺仪采样率
I2C_SAND_BYTE(MPU6050_ADD, MPU6050_RA_CONFIG, 0x06); // 调用 I²C 总线发送函数,修改设置寄存器
I2C_SAND_BYTE(MPU6050_ADD, MPU6050_RA_ACCEL_CONFIG, 0x00); // 调用 I²C 总线发送函数,配置加速度传感器的取值范围
I2C_SAND_BYTE(MPU6050_ADD, MPU6050_RA_GYRO_CONFIG, 0x18); // 调用 I²C 总线发送函数,设置 MPU6050 的测量范围
}

/** 读取 MPU6050 的 X、Y、Z 三轴原始数据,参数 n 用于存放读取到的原始数据,其中 n[0] = AX、n[1] = AY、n[2] = AZ、n[3] = GX、n[4] = GY、n[5] = GZ */
void MPU6050_READ(u16 *n) {
u8 i; // for 循环索引
u8 t[14]; // 存放原始数据的临时变量

/* 通过 I²C 连续读取函数,从 MPU6050_RA_ACCEL_XOUT_H 地址读取 14 个字节的数据,并将其存放至临时变量 t 当中 */
I2C_READ_BUFFER(MPU6050_ADD, MPU6050_RA_ACCEL_XOUT_H, t, 14);
/* 存放加速度数据 */
for (i = 0; i < 3; i++) {
n[i] = ((t[2 * i] << 8) + t[2 * i + 1]);
}
/* 存放陀螺仪数据 */
for (i = 4; i < 7; i++) {
n[i - 1] = ((t[2 * i] << 8) + t[2 * i + 1]);
}
}

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/** User/main.c */
#include "MPU6050.h"
#include "delay.h"
#include "oled0561.h"
#include "relay.h"
#include "stm32f10x.h"
#include "sys.h"

int main(void) {
u16 t[6] = {0};

delay_ms(500); // 等待相关元器件上电并且工作正常
RCC_Configuration(); // 初始化系统时钟
RELAY_Init(); // 初始化继电器
I2C_Configuration(); // 初始化 I²C 总线
OLED0561_Init(); // 初始化 OLED

/* 显示文字信息到 OLED 屏幕 */
OLED_DISPLAY_8x16_BUFFER(0, " MPU6050 Test ");
OLED_DISPLAY_8x16_BUFFER(2, "X: X: ");
OLED_DISPLAY_8x16_BUFFER(4, "Y: Y: ");
OLED_DISPLAY_8x16_BUFFER(6, "Z: Z: ");

MPU6050_Init(); // 初始化 MPU6050 模块

while (1) {
MPU6050_READ(t); // 读取 MPU6050 的数据到上面声明的数组变量 t,其中第 0 ~ 2 位存放加速度值,第 3 ~ 5 位存放陀螺仪值

/* 显示 t 数组第 0 个元素的数据到 OLED 屏幕 */
OLED_DISPLAY_8x16(2, 2 * 8, t[0] / 10000 + 0x30); // 万位
OLED_DISPLAY_8x16(2, 3 * 8, t[0] % 10000 / 1000 + 0x30); // 千位
OLED_DISPLAY_8x16(2, 4 * 8, t[0] % 1000 / 100 + 0x30); // 百位
OLED_DISPLAY_8x16(2, 5 * 8, t[0] % 100 / 10 + 0x30); // 十位
OLED_DISPLAY_8x16(2, 6 * 8, t[0] % 10 + 0x30); // 个位

/* 显示 t 数组第 3 个元素的数据到 OLED 屏幕 */
OLED_DISPLAY_8x16(2, 11 * 8, t[3] / 10000 + 0x30); // 万位
OLED_DISPLAY_8x16(2, 12 * 8, t[3] % 10000 / 1000 + 0x30); // 千位
OLED_DISPLAY_8x16(2, 13 * 8, t[3] % 1000 / 100 + 0x30); // 百位
OLED_DISPLAY_8x16(2, 14 * 8, t[3] % 100 / 10 + 0x30); // 十位
OLED_DISPLAY_8x16(2, 15 * 8, t[3] % 10 + 0x30); // 个位

/* 显示 t 数组第 1 个元素的数据到 OLED 屏幕 */
OLED_DISPLAY_8x16(4, 2 * 8, t[1] / 10000 + 0x30); // 万位
OLED_DISPLAY_8x16(4, 3 * 8, t[1] % 10000 / 1000 + 0x30); // 千位
OLED_DISPLAY_8x16(4, 4 * 8, t[1] % 1000 / 100 + 0x30); // 百位
OLED_DISPLAY_8x16(4, 5 * 8, t[1] % 100 / 10 + 0x30); // 十位
OLED_DISPLAY_8x16(4, 6 * 8, t[1] % 10 + 0x30); // 个位

/* 显示 t 数组第 4 个元素的数据到 OLED 屏幕 */
OLED_DISPLAY_8x16(4, 11 * 8, t[4] / 10000 + 0x30); // 万位
OLED_DISPLAY_8x16(4, 12 * 8, t[4] % 10000 / 1000 + 0x30); // 千位
OLED_DISPLAY_8x16(4, 13 * 8, t[4] % 1000 / 100 + 0x30); // 百位
OLED_DISPLAY_8x16(4, 14 * 8, t[4] % 100 / 10 + 0x30); // 十位
OLED_DISPLAY_8x16(4, 15 * 8, t[4] % 10 + 0x30); // 个位

/* 显示 t 数组第 2 个元素的数据到 OLED 屏幕 */
OLED_DISPLAY_8x16(6, 2 * 8, t[2] / 10000 + 0x30); // 万位
OLED_DISPLAY_8x16(6, 3 * 8, t[2] % 10000 / 1000 + 0x30); // 千位
OLED_DISPLAY_8x16(6, 4 * 8, t[2] % 1000 / 100 + 0x30); // 百位
OLED_DISPLAY_8x16(6, 5 * 8, t[2] % 100 / 10 + 0x30); // 十位
OLED_DISPLAY_8x16(6, 6 * 8, t[2] % 10 + 0x30); // 个位

/* 显示 t 数组第 5 个元素的数据到 OLED 屏幕 */
OLED_DISPLAY_8x16(6, 11 * 8, t[5] / 10000 + 0x30); // 万位
OLED_DISPLAY_8x16(6, 12 * 8, t[5] % 10000 / 1000 + 0x30); // 千位
OLED_DISPLAY_8x16(6, 13 * 8, t[5] % 1000 / 100 + 0x30); // 百位
OLED_DISPLAY_8x16(6, 14 * 8, t[5] % 100 / 10 + 0x30); // 十位
OLED_DISPLAY_8x16(6, 15 * 8, t[5] % 10 + 0x30); // 个位

delay_ms(200); // 修改该延时值,可以调整 MPU6050 的数据刷新频率
}
}

Low-power Modes 低功耗模式

微控制器的功率是各种片上外设与模块的功率总和,低功耗模式则是通过关闭部分芯片内部的功能,以达到降低功耗节约电能的目的。STM32F103C8T6拥有睡眠(Sleep)停机(Stop)待机(Standby)三种低功耗模式模式,并且支持通过Vʙᴀᴛ引脚为实时时钟以及备用寄存器供电。

低功耗模式 关闭功能 唤醒方式
睡眠模式 ARM 内核 所有内、外部功能的中断事件
停机模式 ARM 内核以及内部所有功能、PLL 分频器、HSE 外部中断输入引脚 EXTI、电源电压监控中断 PVD、RTC 时钟、USB 唤醒信号
待机模式 ARM 内核以及内部所有功能、PLL 分频器、HSE、SDRAM NRST 引脚外部复位、独立看门狗 IWDG 复位、专用唤醒引脚 WKUP、RTC 时钟

参考:STM32F103C8T6 正常模式下工作电流约10mA,进入睡眠模式后为2mA,进入停机模式之后仅20uA,而进入待机模式则只有2uA,这些数据根据程序和集成电路工艺的不同而略有差异,但是能对低功耗下各模式的能耗状况有一个直观的认识。

当 ARM Cortex M3 内核空闲的时候,可以进入睡眠模式。由于仅仅只关闭了内核,节能效果有限,因此极少在祼机系统当中使用,但是常用于嵌入式操作系统当中。而进入停机模式以后,SRAM 里的内容不会消失,程序也不会自动复位,在唤醒以后可以恢复之前的工作状态,其节能效果与待机模式相近,在电池供电的场景下具备一定优势。但是,进入待机模式以后,SRAM 的数据将会掉电丢失,唤醒后程序会自动复位,由于待机停机两种模式下消耗的电流差别仅以uA为单位,因此更多场景会采用停机模式,而仅在与之前工作状态无关,且运行并不频繁的场景下考虑采用待机模式

Sleep 睡眠模式

将下面的程序编译下载运行,实验电路上电STM32F103C8T6默认进入睡眠模式,但是按下核心板电路上的KEY1按键,可以通过产生中断唤醒微控制器。当按键弹起后经过0.5秒左右时间,微控制器又会重新进入睡眠模式。

NVIC.h

1
2
3
4
5
6
7
8
9
/** Basic/nvic/NVIC.h */
#ifndef __NVIC_H
#define __NVIC_H
#include "sys.h"

extern u8 INT_MARK; // 中断标志位
void KEY_INT_INIT(void); // 按键中断初始化函数

#endif

NVIC.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/** Basic/nvic/NVIC.c */
#include "NVIC.h"

u8 INT_MARK; // 中断标志位

/** 按键中断初始化函数 */
void KEY_INT_INIT(void) {
NVIC_InitTypeDef NVIC_InitStruct;
EXTI_InitTypeDef EXTI_InitStruct;

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 使能 GPIO 相关时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // 使能 AFIO 重映射功能时钟
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); // 映射 GPIO 引脚至 EXTI

EXTI_InitStruct.EXTI_Line = EXTI_Line0; // 设置外部中断线
EXTI_InitStruct.EXTI_LineCmd = ENABLE; // 使能外部中断
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt; // 设置为中断模式
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling; // 设置为下降沿触发中断
EXTI_Init(&EXTI_InitStruct); // 初始化外部中断 EXTI 设置
NVIC_InitStruct.NVIC_IRQChannel = EXTI0_IRQn; // 设置 NVIC 中断请求通道
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; // 使能 NVIC 中断请求通道
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2; // 设置 NVIC 的抢占优先级为 2
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2; // 设置 NVIC 的子优先级为 2
NVIC_Init(&NVIC_InitStruct); // 初始化向量中断控制器 NVIC 设置
}

/* 外部中断 0 的中断处理函数 */
void EXTI0_IRQHandler(void) {
/* 判断 EXTI_Line0 中断线是否发生中断 */
if (EXTI_GetITStatus(EXTI_Line0) != RESET) {
INT_MARK = 1; // 标志位置 1,表示产生了按键中断
EXTI_ClearITPendingBit(EXTI_Line0); // 清除中断线内的标志位
}
}

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/** User/main.c */
#include "NVIC.h"
#include "delay.h"
#include "key.h"
#include "led.h"
#include "oled0561.h"
#include "relay.h"
#include "stm32f10x.h"
#include "sys.h"

int main(void) {
delay_ms(500); // 等待相关元器件上电并且工作正常
RCC_Configuration(); // 初始化系统时钟
RELAY_Init(); // 初始化继电器
LED_Init(); // 初始化核心板 LED
KEY_Init(); // 初始化核心板按键
I2C_Configuration(); // 初始化 I²C 总线
OLED0561_Init(); // 初始化 OLED

OLED_DISPLAY_8x16_BUFFER(0, " Sleep Test "); // 显示文字信息到 OLED 屏幕

INT_MARK = 0; // 中断标志位初始化为 0
NVIC_Configuration(); // 设置中断优先级
KEY_INT_INIT(); // 调用按键中断初始化函数,将 KEY1 按键对应的 PA0 设置为按键中断输入

/* 睡眠模式设置 */
NVIC_SystemLPConfig(NVIC_LP_SEVONPEND, DISABLE); // DISABLE 表示只有使能的中断或事件才能唤醒 ARM 内核,ENABLE 表示任何中断与事件都可以唤醒 ARM 内核
NVIC_SystemLPConfig(NVIC_LP_SLEEPDEEP, DISABLE); // DISABLE 表示设置低功耗模式为睡眠模式,ENABLE 表示进入低功耗时为深度睡眠模式。
NVIC_SystemLPConfig(NVIC_LP_SLEEPONEXIT, DISABLE); // DISABLE 表示唤醒进入线程模式以后不再进入睡眠模式,ENABLE 表示唤醒执行完相应中断处理以后直接进入睡眠模式

while (1) {
GPIO_WriteBit(LEDPORT, LED1, (BitAction)(1)); // 点亮 LED
OLED_DISPLAY_8x16_BUFFER(4, " MCU Sleep! ");
delay_ms(500);

/* __WFI() 与 __WFE() 函数均声明并定义在内核相关的 core_cm3.h 和 core_cm3.c 文件 */
__WFI(); // 执行到此处以后进入睡眠模式,并且等待中断唤醒,后续代码将不会再执行
// __WFE(); // 进入睡眠模式,等待事件唤醒

GPIO_WriteBit(LEDPORT, LED1, (BitAction)(0)); // 熄灭 LED
OLED_DISPLAY_8x16_BUFFER(4, " MCU Wake Up! ");
delay_ms(500);
}
}

Stop 停机模式

将下面的程序编译下载运行,实验电路上电STM32F103C8T6默认进入停机模式,按下核心板电路上的KEY1按键,可以通过产生中断唤醒微控制器。当按键弹起0.5秒以后,微控制器又会重新进入停机模式。

NVIC.h

1
/** Basic/nvic/NVIC.h,代码与睡眠模式实验相同 */

NVIC.c

1
/** Basic/nvic/NVIC.c,代码与睡眠模式实验相同 */

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/** User/main.c */
#include "NVIC.h"
#include "delay.h"
#include "key.h"
#include "led.h"
#include "oled0561.h"
#include "relay.h"
#include "stm32f10x.h"
#include "sys.h"

int main(void) {
delay_ms(500); // 等待相关元器件上电并且工作正常
RCC_Configuration(); // 初始化系统时钟
RELAY_Init(); // 初始化继电器
LED_Init(); // 初始化核心板 LED
KEY_Init(); // 初始化核心板按键
I2C_Configuration(); // 初始化 I²C 总线
OLED0561_Init(); // 初始化 OLED

OLED_DISPLAY_8x16_BUFFER(0, " Stop Test "); // 显示文字信息到 OLED 屏幕

INT_MARK = 0; // 中断标志位初始化为 0
NVIC_Configuration(); // 设置中断优先级
KEY_INT_INIT(); // 调用按键中断初始化函数,将 KEY1 按键对应的 PA0 设置为按键中断输入

RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); // 使能电源 PWR 相关时钟,因为停机模式是电源 PWR 功能的组成部分

while (1) {
GPIO_WriteBit(LEDPORT, LED1, (BitAction)(1)); // 点亮 LED
OLED_DISPLAY_8x16_BUFFER(4, " MCU Stop! ");
delay_ms(500);

/*
* 参数 PWR_Regulator_ON 指电源不进入低功耗模式,参数 PWR_Regulator_LowPower 让电源进入低功耗模式
* 参数 PWR_STOPEntry_WFI 表示采用中断方式唤醒,参数 PWR_STOPEntry_WFE 表示采用事件方式唤醒
*/
PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI); // 调用标准库函数进入停机模式,并采用中断方式唤醒,后续代码将不会再执行

RCC_Configuration(); // 初始化系统时钟,由于停机模式唤醒后会默认采用 HSI 内部高速时钟,因此需要重新进行初始化

GPIO_WriteBit(LEDPORT, LED1, (BitAction)(0)); // 熄灭 LED
OLED_DISPLAY_8x16_BUFFER(4, " MCU Wake Up! ");
delay_ms(500);
}
}

Standby 待机模式

本实验需要断开触摸按键对应PA10跳线座的PA0跳线,下面程序下载上电以后,STM32F103C8T6默认进入待机模式,只能通过实验电路上的【复位 K1】和【唤醒 K2】两个按键进行唤醒。例如:K2按键一端连接到3.3V电源,另外一端连接至WKUP/PA0引脚,按键按下时该引脚会输入一个高电平上升沿信号,从而唤醒微控制器。

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/** User/main.c */
#include "delay.h"
#include "led.h"
#include "oled0561.h"
#include "relay.h"
#include "stm32f10x.h"
#include "sys.h"

int main(void) {
delay_ms(500); // 等待相关元器件上电并且工作正常
RCC_Configuration(); // 初始化系统时钟
RELAY_Init(); // 初始化继电器
LED_Init(); // 初始化核心板 LED
I2C_Configuration(); // 初始化 I²C 总线
OLED0561_Init(); // 初始化 OLED

OLED_DISPLAY_8x16_BUFFER(0, " Standby Test "); // 显示文字信息到 OLED 屏幕

RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); // 使能电源 PWR 相关时钟,由于待机和停机模式都需要使用到 PWR 时钟
PWR_WakeUpPinCmd(ENABLE); // 调用标准库函数,开启 WKUP 功能唤醒引脚,待机时 WKUP/PA0 引脚为模拟输入,该引脚如果接收到上升沿信号,就会使微控制器重新唤醒

GPIO_WriteBit(LEDPORT, LED1, (BitAction)(0)); // 熄灭 LED
OLED_DISPLAY_8x16_BUFFER(4, " MCU Reset! ");
delay_ms(500);

GPIO_WriteBit(LEDPORT, LED1, (BitAction)(1)); // 点亮 LED
OLED_DISPLAY_8x16_BUFFER(4, " Standby! ");
delay_ms(500);

PWR_EnterSTANDBYMode(); // 进入待机模式,程序不会再向下执行,唤醒后由于 SRAM 保存的内容消失,代码会复位并重新执行
}

WDG 看门狗定时器

看门狗(WDG,Watch Dog)是STM32F103C8T6的一个片上外设单元,原理上是一个定时计数器,当其开始递减计数以后,每间隔一段时间微控制器就会发出一条复位指令,从而使其重新开始递减计数;如果运行期间看门狗未进行正确的复位,则递减计数至0以后,看门狗就会强制复位系统或进行其它指定处理。

IWDG 独立看门狗

独立看门狗(IWDG,Independent Watchdog)基于一个 12 位的递减计数器和一个 8 位的预分频器,并由内部独立于主时钟的40kHz的 RC 振荡器提供时钟源,可以运行于停机与待机模式之下。独立看门狗可以在计数到0之前随时进行喂狗操作,主要用于监控程序是否正常运行

注意:编写实验工程时,必须向项目当中加入stm32f10x_iwdg.hstm32f10x_iwdg.c标准函数库文件。

iwdg.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/** Basic/iwdg/iwdg.h */
#ifndef __IWDG_H
#define __IWDG_H
#include "sys.h"

/*
* 看门狗定时时间(单位:ms)= (预分频值 * 重装载值) / 40
* 本实验将预分频值 pre 设置为 64,重装载值设置为 625,由上面公式可知独立看门狗的定时时间约为 1 秒钟
*/
#define pre IWDG_Prescaler_64 // 分频器取值范围,可选 4、8、16、32、64、128、256
#define rlr 625 // 重装载值取值范围,0 ~ 0xFFF(将 0xFFF 换算为十进制数等于 4095)

void IWDG_Init(void); // 独立看门狗初始化函数
void IWDG_Feed(void); // 独立看门喂狗函数

#endif

iwdg.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/** Basic/iwdg/iwdg.c  */
#include "iwdg.h"

/** 独立看门狗初始化函数 */
void IWDG_Init(void) {
IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable); // 使能寄存器 IWDG_PR 和 IWDG_RLR 的写操作,允许独立看门狗写数据
IWDG_SetPrescaler(pre); // 设置独立看门狗计数器的预分频值 64
IWDG_SetReload(rlr); // 设置独立看门狗计数器的重装载值 625
IWDG_ReloadCounter(); // 将重装载值写入计数器
IWDG_Enable(); // 使能独立看门狗,让计数器开始进行递减计数
}

/** 独立看门喂狗函数 */
void IWDG_Feed(void) {
IWDG_ReloadCounter(); // 将重装载值 625 重新写入计数器
}

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/** User/main.c */
#include "delay.h"
#include "iwdg.h"
#include "key.h"
#include "led.h"
#include "oled0561.h"
#include "relay.h"
#include "stm32f10x.h"
#include "sys.h"

int main(void) {
delay_ms(500); // 等待相关元器件上电并且工作正常
RCC_Configuration(); // 初始化系统时钟
RELAY_Init(); // 初始化继电器
LED_Init(); // 初始化核心板 LED
KEY_Init(); // 初始化核心板按键
I2C_Configuration(); // 初始化 I²C 总线
OLED0561_Init(); // 初始化 OLED

/* 显示文字信息到 OLED 屏幕 */
OLED_DISPLAY_8x16_BUFFER(0, " IWDG Test ");
OLED_DISPLAY_8x16_BUFFER(4, " Reset! ");
delay_ms(800);
OLED_DISPLAY_8x16_BUFFER(4, " ");

IWDG_Init(); // 初始化独立看门狗,设置在 1 秒内进行喂狗操作,同时计数器开始递减计数

while (1) {
IWDG_Feed(); // 调用标准外设库函数,执行喂狗操作

/* 如果 KEY1 按键被按下 */
if (!GPIO_ReadInputDataBit(KEYPORT, KEY1)) {
delay_s(2); // 延时 2 秒使程序流程不能执行喂狗操作,进而导致程序复位
}
}
}

WWDG 窗口看门狗

窗口看门狗(WWDG,Window Watchdog)内置一个 7 位递减计数器,并且由主时钟进行驱动,具备早期预警中断功能。窗口看门狗必须在指定的时间范围内进行喂狗操作,主要作用是监控单片机运行时效是否精确

注意:编写实验工程时,必须向项目当中加入stm32f10x_wwdg.hstm32f10x_wwdg.c标准函数库文件。

wwdg.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/** Basic/wwdg/wwdg.h */
#ifndef __WWDG_H
#define __WWDG_H
#include "sys.h"

/*
* 上窗口超时时间(单位:us)= 4096 * 预分频值 * (计数器重装载 - 上窗口边界值) / APB1时钟频率(单位:MHz)
* 下窗口超时时间(单位:us)= 4096 * 预分频值 * (计数器重装载 - 0x40) / APB1时钟频率(单位:MHz)
* 本实验计数器重装载值为 0x7f,上窗口边界值为 0x50,预分频值为 8,得到上窗口时间 = 48ms,下窗口时间 = 64ms
*/
#define WWDG_CNT 0x7F // 计数器重装载值,取值范围 0x40 ~ 0x7F
#define wr 0x50 // 用户设置的上窗口边界值,取值范围 0x40 ~ 0x7F
#define fprer WWDG_Prescaler_8 // 预分频值,取值范围 1、2、4、8

void WWDG_Init(void); // 窗口看门狗初始化函数
void WWDG_NVIC_Init(void); // 窗口看门狗中断服务初始化函数
void WWDG_Feed(void); // 窗口看门喂狗函数

#endif

wwdg.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/** Basic/wwdg/wwdg.c  */
#include "wwdg.h"

/** 窗口看门狗初始化函数 */
void WWDG_Init(void) {
RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG, ENABLE); // 使能窗口看门狗对应的时钟
WWDG_SetPrescaler(fprer); // 设置窗口看门狗计数器的预分频值 8
WWDG_SetWindowValue(wr); // 设置上窗口边界值 0x50
WWDG_Enable(WWDG_CNT); // 使能窗口看门狗,并且设置重装载值 0x7F
WWDG_ClearFlag(); // 清除提前唤醒中断标志位
WWDG_NVIC_Init(); // 初始化窗口看门狗对应的 NVIC
WWDG_EnableIT(); // 使能窗口看门狗中断
}

/** 窗口看门狗中断服务初始化函数,被 WWDG_Init() 所调用 */
void WWDG_NVIC_Init(void) {
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = WWDG_IRQn; // 设置窗口看门狗中断请求通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; // 设置抢占优先级为 2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; // 设置子优先级为 3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能中断请求通道
NVIC_Init(&NVIC_InitStructure); // 初始化 NVIC 中断向量控制器
}

/** 窗口看门喂狗函数 */
void WWDG_Feed(void) {
WWDG_SetCounter(WWDG_CNT); // 调用标准外设库提供的喂狗操作函数,参数为计数器重装载值 0x7F
}

/** 窗口看门狗中断服务函数,窗口期没有执行喂狗操作时,计数值递减至 0x40 时就会触发该中断 */
void WWDG_IRQHandler(void) {
WWDG_ClearFlag(); // 清除提前唤醒中断标志位

/* 按需加入系统复位之前需要执行的任务 */
}

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/** User/main.c */
#include "delay.h"
#include "key.h"
#include "led.h"
#include "oled0561.h"
#include "relay.h"
#include "stm32f10x.h"
#include "sys.h"
#include "wwdg.h"

int main(void) {
delay_ms(500); // 等待相关元器件上电并且工作正常
RCC_Configuration(); // 初始化系统时钟
RELAY_Init(); // 初始化继电器
LED_Init(); // 初始化核心板 LED
KEY_Init(); // 初始化核心板按键
I2C_Configuration(); // 初始化 I²C 总线
OLED0561_Init(); // 初始化 OLED

/* 显示文字信息到 OLED 屏幕 */
OLED_DISPLAY_8x16_BUFFER(0, " WWDG Test ");
OLED_DISPLAY_8x16_BUFFER(4, " Reset! ");
delay_ms(800);
OLED_DISPLAY_8x16_BUFFER(4, " ");

WWDG_Init(); // 初始化窗口看门狗,设置喂狗操作的间隔时间

while (1) {
delay_ms(54); // 避开窗口时间之前的位置,即计数初始值 ~ 上窗口边界
WWDG_Feed(); // 调用标准外设库函数,进行喂狗操作

/* 如果 KEY1 按键按下 */
if (!GPIO_ReadInputDataBit(KEYPORT, KEY1)) {
delay_s(2); // 延时 2 秒使程序流程不能执行喂狗操作,进而导致程序复位
}
}
}

TIM2/3/4 标准定时器

STM32F103C8T6内置了TIM2TIM3TIM4三个可同步运行的标准定时器,每个定时器都有一个 16 位自动加载递加/递减计数器、一个 16 位预分频器、4 个独立的通道,其中每个通道都可用于输入捕获、输出比较、PWM 和单脉冲模式输出。

本小节将会基于TIM3标准定时器 3 实现了一个让LED1间隔1秒进行闪烁的实验,进行实验之前需要先往项目中加入定时器相关的stm32f10x_tim.hstm32f10x_tim.c标准函数库源文件。

tim.h

1
2
3
4
5
6
7
8
9
/** Basic/tim/tim.h */
#ifndef __PWM_H
#define __PWM_H
#include "sys.h"

void TIM3_Init(u16 arr, u16 psc); // 定时器 3 初始化函数
void TIM3_NVIC_Init(void); // 定时器 3 嵌套中断向量 NVIC 初始化函数

#endif

tim.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/** Basic/tim/tim.c  */
#include "led.h"
#include "tim.h"

/*
* 定时器 3 初始化函数,参数 arr 是计数重装载值,参数 psc 是预分频系数
* 定时时间 = ((重装载值 + 1) * (预分频系数 + 1)) / 时钟频率
* 例如:重装载值 = 9999,预分频系数 = 7199,则可以定时 1 秒钟
*/
void TIM3_Init(u16 arr, u16 psc) {
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStrue;

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 使能 TIM3 相关时钟
TIM3_NVIC_Init(); // 调用 TIM3 嵌套中断向量 NVIC 初始化函数

TIM_TimeBaseInitStrue.TIM_Period = arr; // 设置计数重装载值 arr
TIM_TimeBaseInitStrue.TIM_Prescaler = psc; // 设置预分频系数 psc
TIM_TimeBaseInitStrue.TIM_CounterMode = TIM_CounterMode_Up; // 计数器向上计数溢出
TIM_TimeBaseInitStrue.TIM_ClockDivision = TIM_CKD_DIV1; // 定时器分频因子,通常设为 TIM_CKD_DIV1
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStrue); // 初始化 TIM3 设置
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); // 使能 TIM3 中断
TIM_Cmd(TIM3, ENABLE); // 使能 TIM3,此时定时器开始工作,每间隔 1 秒产生一次定时器中断,然后跳转至相应的中断服务函数
}

/** 定时器 3 嵌套中断向量 NVIC 初始化函数 */
void TIM3_NVIC_Init(void) {
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn; // 定义 TIM3 中断请求通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x3; // 设置主优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x3; // 设置子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能 NVIC 中断请求通道
NVIC_Init(&NVIC_InitStructure);
}

/** 定时器 3 中断服务函数 */
void TIM3_IRQHandler(void) {
/* 判断当前是否为 TIM3 定时器中断 */
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) {
TIM_ClearITPendingBit(TIM3, TIM_IT_Update); // 如果是则清空中断标志位
/* 用户自定义处理代码 */
GPIO_WriteBit(LEDPORT, LED1, (BitAction)(1 - GPIO_ReadOutputDataBit(LEDPORT, LED1))); // 对 LED1 电平状态取反,实现间隔 1 秒闪烁效果
}
}

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/** User/main.c */
#include "delay.h"
#include "key.h"
#include "led.h"
#include "oled0561.h"
#include "relay.h"
#include "stm32f10x.h"
#include "sys.h"
#include "tim.h"

int main(void) {
delay_ms(500); // 等待相关元器件上电并且工作正常
RCC_Configuration(); // 初始化系统时钟
RELAY_Init(); // 初始化继电器
LED_Init(); // 初始化核心板 LED
KEY_Init(); // 初始化核心板按键
I2C_Configuration(); // 初始化 I²C 总线
OLED0561_Init(); // 初始化 OLED

OLED_DISPLAY_8x16_BUFFER(0, " TIM3 Test "); // 显示文字信息到 OLED 屏幕

TIM3_Init(9999, 7199); // 定时器初始化函数,通过设置参数 9999 与 7199 可以产生 1 秒的定时时间

while (1) {
/* 放置用户程序,例如将 LED1 的闪烁代码放置在 TIM3 中断服务函数里 */
}
}

CRC 循环冗余校验

STM32F103C8T6内置的循环冗余校验(CRC,Cyclic Redundancy Check)计算单元基于固定的多项式,可以得到任意 32 位的 CRC 计算结果,该结果可以应用于通信与数据存储的校验。在进行实验之前需要向工程当中加入 CRC 校验相关的stm32f10x_crc.hstm32f10x_crc.c标准函数库文件。

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/** User/main.c */
#include "delay.h"
#include "key.h"
#include "led.h"
#include "oled0561.h"
#include "relay.h"
#include "stm32f10x.h"
#include "sys.h"

int main(void) {
/* CRC 校验相关数据 */
u32 a, b;
u8 c;
u32 y[3] = {0x87654321, 0x98765432, 0x09876543};

delay_ms(500); // 等待相关元器件上电并且工作正常
RCC_Configuration(); // 初始化系统时钟
RELAY_Init(); // 初始化继电器
LED_Init(); // 初始化核心板 LED
KEY_Init(); // 初始化核心板按键
I2C_Configuration(); // 初始化 I²C 总线
OLED0561_Init(); // 初始化 OLED

OLED_DISPLAY_8x16_BUFFER(0, " CRC Test "); // 显示文字信息到 OLED 屏幕
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_CRC, ENABLE); // 使能 CRC 相关时钟

while (1) {
/* 采用单独数据的方式进行 CRC 计算,最后变量 a 存放的是 3 个独立数据的 CRC 计算结果,32 位 */
CRC_ResetDR(); // 使用 CRC 计算之前需要先进行复位
CRC_CalcCRC(0x12345678); // 使用标准外设库函数向 CRC 寄存器写入数据,参数就是待写入 CRC 的 32 位数据,最后返回 32 位 CRC 计算结果
CRC_CalcCRC(0x23456789);
a = CRC_CalcCRC(0x34567890); // 将返回的 32 位 CRC 计算结果赋给变量 a

/* 采用数组的方式进行 CRC 计算,最后变量 b 存放的是数组 y 的 3 个元素 CRC 计算结果,32 位 */
CRC_ResetDR(); // 使用 CRC 计算之前再次进行复位
b = CRC_CalcBlockCRC(y, 3); // 使用标准外设库向 CRC 寄存器写入数组数据,参数 y 是待写入 CRC 的数组,参数 3 是数组长度,最后将返回的 32 位 CRC 计算结果赋给变量 b

/* 采用数组的方式进行 CRC 计算 读取和写入临时数据寄存器,最终变量 c 存放的是之前写入 CRC_IDR 寄存器的数据 0x5a,8位 */
CRC_SetIDRegister(0x5a); // 使用标准外设库向 CRC 独立寄存器 CRC_IDR 写入 8 位数据
c = CRC_GetIDRegister(); // 使用标准外设库从 CRC 独立寄存器 CRC_IDR 读取数据
}
}

芯片 ID 编码

STM32F103C8T6拥有 96 位芯片 ID 编码,分别固定存放于如下 12 个地址,其中每个地址存放 8 位编码。因此,读取读取该 ID 编码的时候,可以采用8 位方式读取 12 个数据16 位方式读取 6 个数据32 位方式读取 3 个数据三种方式。

1
2
/* 低位 ⇐===-- 芯片 ID 编码存放地址 --===⇒ 高位 */
0x1FFFF7E8 | 0x1FFFF7E9 | 0x1FFFF7EA | 0x1FFFF7EB | 0x1FFFF7EC | 0x1FFFF7ED | 0x1FFFF7EE | 0x1FFFF7EF | 0x1FFFF7F0 | 0x1FFFF7F1 | 0x1FFFF7F2 | 0x1FFFF7F3

小端数据存放是指数据整合时,将低位地址数据放入内存的低位地址部分;大端数据存放方式正好反其道而行之,即将将低位地址数据放入内存的高位地址部分。ARM 架构微控制器为小端存放方式,而 8051 架构单片机为大端存放方式。

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/** User/main.c */
#include "delay.h"
#include "key.h"
#include "led.h"
#include "oled0561.h"
#include "relay.h"
#include "stm32f10x.h"
#include "sys.h"
#include "usart.h"

int main(void) {
u32 ID[3]; // 声明拥有 3 个 32 位元素数据的数组

delay_ms(500); // 等待相关元器件上电并且工作正常
RCC_Configuration(); // 初始化系统时钟
RELAY_Init(); // 初始化继电器
LED_Init(); // 初始化核心板 LED
KEY_Init(); // 初始化核心板按键
USART1_Init(115200); // 初始化 USART 串行接口,参数是波特率
I2C_Configuration(); // 初始化 I²C 总线
OLED0561_Init(); // 初始化 OLED

OLED_DISPLAY_8x16_BUFFER(0, " STM32 ChipID "); // 显示文字信息到 OLED 屏幕

/* 采用 32 位方式读取芯片 ID 编码 */
ID[0] = *(__IO u32 *)(0X1FFFF7E8); // 读取前面 4 个字节
ID[1] = *(__IO u32 *)(0X1FFFF7EC); // 读取中间 4 个字节
ID[2] = *(__IO u32 *)(0X1FFFF7F0); // 读取后面 4 个字节

printf("STM32F103C8T6 ChipID: %08X %08X %8X \r\n", ID[0], ID[1], ID[2]); // 将 ID 编码打印至 USART 输出,格式字符串中的 08 表示数据不足 8 位时,则以 8 位格式补 0 进行显示

/* 检查从微控制器内读取到的 ID 是否与给定的编码匹配 */
if (ID[0] == 0x0669FF52 && ID[1] == 0x52517178 && ID[2] == 0x67255837) {
printf("ChipID match! \r\n"); // 匹配
} else {
printf("chipID error! \r\n"); // 不匹配
}

while (1) {}
}