You are on page 1of 41

UN IV

G E

P EK I N 北京大学内部教材

R
S I TY
未经本校允许,不得翻印
1 8 9 8

微处理器与接口技术实验

北京大学信息科学技术学院
2020 年 8 月
目 录

实验一 GPIO 的基本操作 1


1.1 实验目的 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2 实验原理 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.3 实验步骤 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.4 思考题 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6

实验二 中断系统与低功耗模式 7
2.1 实验目的 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.2 实验原理 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.3 实验步骤 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.4 思考题 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10

实验三 串口通信 11
3.1 实验目的 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3.2 实验原理 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3.3 实验步骤 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
3.4 思考题 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15

实验四 LCD 显示与 DMA 传输 16


4.1 实验目的 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
4.2 实验原理 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
4.3 实验步骤 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
4.4 思考题 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20

实验五 模数和数模转换 22
5.1 实验目的 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
5.2 实验原理 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
5.3 实验步骤 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
5.4 思考题 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26

实验六 I2 C 总线 27
6.1 实验目的 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
6.2 实验原理 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
6.3 实验步骤 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
6.4 思考题 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29

i
ii 目 录

实验七 USB 总线设备 30


7.1 实验目的 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
7.2 实验原理 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
7.3 实验步骤 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
7.4 思考题 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
前 言

本课程为“微处理器与接口技术”的实验部分,通过操作真实的硬件,加深对理论课相关知识的了解。
本实验课程选用 STM32F469I-Disco 开发板作为实验平台,增加了部分外设和传感器,通过 C 语言编程,
控制相应的接口完成实验内容。
STM32F469 是 ST(意法半导体)公司的处理器产品,
工作频率最高可达 180MHz,
内部包含 2M 字节
Flash 和 384K 字节 SRAM,支持多种数字模拟接口,在开发板上包含的主要外设有:

• 4” WVGA TFT 显示器与触摸屏


• 128-Mbit SDRAM
• 128-Mbit Quad-SPI Flash
• 2x USB、 耳机插孔、 TF 卡槽
• 3 MEMs 麦克风
• Arduino 兼容接口

开发软件使用的是 ST 公司的 STM32CubeIDE,通过开发板上的 ST-Link/v2-1 在线调试器,可以把


编译好的程序直接下载到处理器中的 Flash 中。STM32CubeIDE 是基于 Eclipse 的集成开发环境,建立
新工程的主要步骤如下:

1. 使用自己的工作区, 可以使用 File 菜单下的“Switch Workspace”


2. 在 File 菜单下选择新建“STM32 project”
3. 在“Target Select”对话框中选择“Board Selector”然后通过搜索 469 找到 STM32F469I-DISCO 开
发板,选择这个开发板后点击“Next”
4. 输入工程的名称,选择 C 语言、Executable 二进制类型和 STM32Cube 类型的项目类型,然后点击
“Finish”
5. 在提示是否初始化所有外设为缺省模式的对话框中选择“No”
6. 在提示是否打开 STM32CubeMx 视图的时候选择“Yes”
7. 具体配置外设的方式参考后续的实验内容

本讲义对实验原理的介绍仅包括和实验内容最直接相关的部分,在做实验的过程中为了对所使用的
平台和知识点有更加深入的了解,还需要阅读相关的数据手册和资料,主要包括下面四个文档:

• STM32 Cortex®-M4 MCUs and MPUs Programming Manual


• STM32F469xx and STM32F479xx advanced Arm®-based 32-bit MCUs Reference Manual
• STM32F469xx Datasheet
• Description of STM32F4 HAL and LL drivers, User Manual

这四个文档的篇幅都比较大,并不需要通读,主要作为手册使用,当对部分模块有疑问时,有针对性的
查找即可。
前三个文档可以分别被称为 “编程手册”,
“参考手册”, 它们可以在 STM32CubeIDE
“数据手册”。
中阅读,入口在建立工程时的“Target Select”对话框,
“MCU/MPU Selector”页面下,当“Part Number”
为“STM32F469NI”时的“Docs & Resources”中。

iii
iv 目 录
实验一 GPIO 的基本操作

1.1 实验目的
1. 熟悉 STM32CubeIDE 的基本工作流程
2. 掌握 ARM 处理器外设通过内存映射方式访问的基本方式
3. 掌握 STM32 GPIO 的基本控制方法

1.2 实验原理
1.2.1 STM32 与开发环境简介
本实验所使用的处理器是 STM32F469NI,属于 ST 公司 STM32 系列处理器中一款高性能的产品。
虽然都是基于 ARM 架构,
但与大多数人更熟悉的手机处理器不同,
它属于 Cortex-M 系列,
主要面向嵌入
式应用,例如工业控制、家庭安防、医疗设备和家用电气等等。
ST 公司的 STM32 产品线也很丰富,包括面向低功耗的 STM32L 系列,面向无线通信的 STM32WL
系列,
面向主流应用和高性能应用的 STM32F 系列等。在实际项目中应该根据实际性能和功能的需求,

取合适的处理器型号。
STM32 的软件开发环境也支持 IAR、Keil 等多种商业软件,本教材使用的是免费的基于 Eclipse
的 STM32CubeIDE,它包括一个 C/C++ 的集成开发环境,并集成了 ST 公司的自动代码生成工具
STM32CubeMx。其中 STM32CubeMx 的使用方法要求对所使用的芯片有全面的了解,读者可以在实验
的过程中逐步熟悉它的使用,而建立工程具体的使用步骤可以参考前言部分。工程建立之后就包括了完整
的程序框架,
如图 1.1 新建的工程中,代码主要在 Core/Src 目录下,其中主要的文件功能如下:

• main.c,包含 main 函数的主程序


• stm32f4xx_hal_msp.c,部分外设的初始化操作函数定义
• stm32f4xx_it.c,处理器的中断和异常处理函数定义
• syscals.c, sysmem.c,C 语言的底层支持函数
• system_stm32f4xx.c,系统底层初始化代码

扩展名为 ioc 的文件是处理器的外设配置文件,通过 STM32CubeMx 插件进行读写,可以实现外


设初始化代码的自动生成,如果不使用 STM32CubeMx 就不包含此文件。在 Inc 目录中值得注意的是
stm32f4xx_hal_conf.h,它定义了代码中可以使用的 API 模块。Startup 目录中的是系统复位后执行的
最初代码,包含了复位中断的处理代码和中断向量表等,为汇编语言编写,一般不需要修改。Drivers 目录
中包含的是处理器厂商提供的函数库。
由于现在的处理器都越来越复杂,为了提高开发的效率,处理器厂商都提供了高层的 API 接口,
便于给不同的处理器型号提供统一的编程接口,简化处理器外设的设置和操作过程。对于本课程使用的
STM32 处理来说会接触到如下几种类型的 API:

• HAL 驱动,对各种外设模块的功能进行封装,
提供的函数包含 HAL 字符串

1
2 实验一 GPIO 的基本操作

图 1.1: STM32CubeIDE 的工程框架

• LL 驱动,对外设功能的简单封装,适合对性能有要求的高级用户使用,提供的函数都包括 LL 字符串
• BSP 驱动,对开发板外设的封装,仅适合对应开发板,
提供的函数包含 BSP 字符串

这些 API 都提供完整的代码,具有详细的注释,其中每个文件开头部分的介绍值得在编程前仔细阅读。
另外有一般的库文件在 CMSIS 目录中,这里是 ARM 公司提供的面向处理器底层的头文件,主要包
含通用的处理器寄存器定义。

1.2.2 STM32F4 的内存布局


STM32 系列处理器的程序总线、数据总线都是 32 位,它的程序、数据、寄存器、IO 端口全部被安排在
了统一的 4G 空间的线性地址内。因此程序可以使用同样的方式访问所有这些软硬件资源。表 1.1 是部分
内存的分配情况,完整的表格请参考 Reference Manual。

表 1.1: STM32F469 处理器的内存布局(部分)


地址范围 内容
0x0000 0000 – 0x001F FFFF 启动代码区,可配置为 Flash 或 SRAM 等
0x0800 0000 – 0x081F FFFF 内部 Flash
0x1000 0000 – 0x1000 FFFF CCM 核心耦合内存
0x2000 0000 – 0x2004 FFFF SRAM
0x4000 0000 – 0xFFFF FFFF 外设地址

STM32F469 的各种外部设备寄存器就映射在 0x4000 0000 开始的内存中,根据所属的不同总线分


布在内存的不同位置,
例如 GPIOD 的寄存器所在的位置就是从地址 0x4002 0C00 到 0x4002 0FFF 这个
范围。

1.2.3 GPIO
GPIO(Gerneral-Purpose I/O)代表通用输入输出端口,在 STM32 处理器中可以作为普通的 I/O 端
口,也可以作为特定外设的管脚,具体实现的功能和特性都可以通过寄存器来设定。STM32F469NI 处理
器最多可以有 161 个 GPIO 端口,分布在从 GPIOA 到 GPIOK 的 11 个组中。
1.2 实验原理 3

每组 GPIO 都有 10 个 32 位的寄存器与其对应,下面对每个寄存器的功能进行简要说明:

1. GPIO 管脚模式寄存器,每两位控制一个管脚

• 00 代表输入模式
• 01 代码输出模式
• 10 其他功能模式(作为其他外设的管脚)
• 11 模拟模式(Analog mode)

2. GPIO 输出类型寄存器,每一位控制一个管脚,0 代表推挽输出,1 代表漏极开路输出


3. GPIO 输出速度寄存器,每两位控制一个管脚,从 00 到 11 分别代表从慢到快的速度
4. GPIO 上拉下拉控制,每两位控制一个管脚

• 00 代表没有电阻连接
• 01 代表上拉
• 10 代表下拉
• 11 保留

5. GPIO 输入数据寄存器,每一位控制一个管脚,读入管脚的输入状态
6. GPIO 输出数据寄存器,每一位控制一个管脚,控制管脚的输出状态
7. GPIO 设置清除寄存器,每一位控制一个管脚,写入 “1” 有效,低 16 位用来设置管脚高电平,高 16
位用来设置管脚低电平
8. GPIO 锁定配置寄存器,每一位控制一个管脚,通过特殊的操作时序锁定 GPIO 管脚的配置状态,以
避免意外的配置变化
9. GPIO 其他功能配置寄存器,
每四位控制一个管脚,
共有 2 个 32 位寄存器,
用来从可能的 16 种其他
功能中进行选择

这些寄存器在 C 语言中可以用如下代码表示(stm32f469xx.h 中定义):

typedef struct
{
__IO uint32_t MODER; /* port mode register, Address offset: 0x00 */
__IO uint32_t OTYPER; /* port output type register, Address offset: 0x04 */
__IO uint32_t OSPEEDR; /* port output speed register, Address offset: 0x08 */
__IO uint32_t PUPDR; /* port pull-up/pull-down register, Address offset: 0x0C */
__IO uint32_t IDR; /* port input data register, Address offset: 0x10 */
__IO uint32_t ODR; /* port output data register, Address offset: 0x14 */
__IO uint32_t BSRR; /* port bit set/reset register, Address offset: 0x18 */
__IO uint32_t LCKR; /* port configuration lock register, Address offset: 0x1C */
__IO uint32_t AFR[2]; /* alternate function registers, Address offset: 0x20-0x24 */
} GPIO_TypeDef;
#define GPIOD_BASE (AHB1PERIPH_BASE + 0x0C00UL)
#define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)

这样在代码中就可以使用 GPIOD->MODER 来使用相应的寄存器了。例如设置 GPIOD 的管脚 4 为输


出,
就可以使用下面的代码(同时参考图 1.2):

GPIOD->MODER = 1 << 8;
4 实验一 GPIO 的基本操作

图 1.2: GPIO MODER 寄存器结构

1.2.4 时钟配置

数字系统的时钟是非常重要的信号,它保证各个模块可以在统一的“步调”下工作,而处理器系统的
时钟就更为复杂,不同的模块对时钟频率的需求不同,但时钟都来自同一个源(RTC 模块等特殊情况例
外),通过倍频和分频模块生成系统中的不同频率信号,这个结构一般被称为时钟树(Clock Tree),图 1.3
是 STM32CubeMx 在配置时钟的界面。当缺省没有修改的时候,处理器的时钟源是内部的高频 RC 振
荡器(HSI),频率为 16MHz,但这个时钟并不是很精确,在需要精确定时的时候会使用外部的晶振,然
后通过锁相环(PLL)和分频器为各个子模块提供合适的时钟信号。在配置使用外部晶振之前,需要在
“Pinout & Configuration”配置页的 RCC(Reset and Clock Control)模块将 HSE(High Speed Clock)
设置为晶体,
如图 1.4 所示。

图 1.3: 时钟配置界面

STM32F469I-DISCO 开发板所包含的晶振频率是 8MHz,可配置的最大系统时钟是 180MHz。时


钟的配置并不是速度越快越好,不同的模块有不同的时钟要求,如果配置的某个时钟频率不符合要求,
STM32CubeMx 会用红色提示。同时由于频率越高系统的功耗越大,因此配置的时钟频率要符合系统的
应用环境。
1.3 实验步骤 5

图 1.4: RCC 模块配置界面

1.3 实验步骤
1.3.1 LED 闪烁实验
STM32F469I-DISCO 开发板的正面有四个 LED 指示灯,颜色分别为绿、橙、红和蓝。对应的 GPIO
管脚分别是 PG6(GPIOG 的 Pin6)、 PD5 和 PK3。编写程序让橙色 LED 以 1Hz 频率闪烁。
PD4、
首先完全按照前言中的步骤建立一个新的工程,外设的配置不需要修改,在生成的代码中找到 main
函数,使用 HAL_GPIO_WritePin 函数和改变 GPIO 的输出状态,使用 HAL_Delay 函数进行延时。编好的
代码如下所示:

/* USER CODE BEGIN WHILE */


while (1)
{
HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_SET);
HAL_Delay(500);
HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET);
HAL_Delay(500);
/* USER CODE END WHILE */

注意在代码中包含很多 USER CODE BEGIN 和 USER CODE END 的注释对,添加的代码要放在这样的注释


对之间,否则如果修改芯片配置需要重新生成代码的话,
新编的代码就可能会被覆盖掉。
使用 Project 菜单中的 Build All(或者工具栏上的“锤子”按钮)编译工程,注意查看集成环境下方
Console 部分显示的信息,
如果有错误请根据信息提示进行修改代码。当编译正常完成之后可以在 Run 菜
单下面执行 Run 命令(或者工具栏上的“箭头”按钮)将二进制代码下载到开发板运行。如果需要调试可
以使用 Debug 命令(或者工具栏上的“虫子”按钮)进入调试界面,从调试界面返回要使用 Terminate 命
令(或者工具栏上的“停止”按钮)

1.3.2 熟悉代码结构
请阅读自动生成的代码(包括 Src 目录下的源码和 Inc 目录下的头文件)
,熟悉基本的函数功能。在集
成环境浏览代码时按下 Ctrl 键的同时可以用鼠标点击函数或结构体等语法元素,编辑器可以跳转到相应
的定义位置,
便于深入了解。
6 实验一 GPIO 的基本操作

阅读 stm32f4xx_hal_gpio.c 代码,找出一个函数可以简化上面的示例代码。
直接使用 GPIO 的 BSRR 寄存器,设置红色 LED(PD5)与橙色 LED 同样闪烁。
通过调试功能查看 LED 是通过高电平点亮还是低电平点亮。

1.3.3 输入按键的读取
开发板背面有一个蓝色的按键,连接到了处理器的 PA0 管脚,通过设置相应的 GPIO 管脚为输入并
使用 HAL_GPIO_ReadPin 函数不断读取其状态,每次检测到按键都改变一下蓝色 LED 灯的显示状态。

1.3.4 使用外部晶振
通过 STM32CubeMx 工具配置开发板使用外部晶振,重新生成代码,检查程序的运行效果。

1.4 思考题
1. 不使用 API 直接操作寄存器的好处是什么?
2. 生成代码中为什么将全部的 GPIO 都进行了初始化?
3. 使用 BSRR 寄存器比 ODR 寄存器有什么优势和劣势?
实验二 中断系统与低功耗模式

2.1 实验目的
1. 了解中断的概念和相关软件的编写
2. 掌握 STM32 低功耗模式的基本原理和使用方法

2.2 实验原理
2.2.1 STM32 的中断系统
在上一个实验中,由于代码中 HAL_Delay 函数的存在,有很大的几率按键没有被检测到,对于一些特
定的应用,丢失一个信号是不能容忍的,这时候就往往需要采用中断的方法。
STM32 的中断系统由 NVIC(Nested Vectored Interrupt Controller)控制,它支持除了 Cortex-M4
核心的 10 个中断源外还支持另外 93 个中断源,
具体的列表可以参考手册,
下面的代码是启动代码中的中
断向量部分(汇编代码,只列举了前 10 个中断向量):

g_pfnVectors:
.word _estack
.word Reset_Handler

.word NMI_Handler
.word HardFault_Handler
.word MemManage_Handler
.word BusFault_Handler
.word UsageFault_Handler
.word 0
.word 0
.word 0
.word 0
.word SVC_Handler
.word DebugMon_Handler
.word 0
.word PendSV_Handler
.word SysTick_Handler

这也是用户程序二进制目标程序的最初部分,最终被定位到处理器内存空间的起始位置。从这个列表也可
以看出目标程序最初并不是可以执行的代码,而是一些数据和指针。最开始是堆栈的顶部地址,接下来是
各个不同的中断服务程序所在的地址,当一个中断事件发生之后,PC 指针就会跳转到相应的地址去执行
处理程序。

7
8 实验二 中断系统与低功耗模式

NVIC 的配置寄存器主要有如下几个类型:

1. NVIC_ISER,用来设置中断允许位
2. NVIC_ICER,用来清除中断允许位
3. NVIC_ISPR,用来设置中断未处理标志(Pending)
4. NVIC_ICPR,用来清除中断未处理标志
5. NVIC_IABR,相应的位标志中断活动状态
6. NVIC_IPR,用来设置中断的优先级
7. STIR,软件触发中断寄存器, 写入中断 ID 就可以触发相应中断

由于 NVIC 最多可以支持 240 个中断源,


所以每种寄存器都有多组寄存器与之对应。对于 NVIC_ISER
来说,
每个二进制位就可以控制一个中断源,
因此有 NVIC_ISER0~NVIC_ISER7 共八个寄存器。而每个中断
优先级可以是 0 到 255 之间的任意值,因此就有 NVIC_IPR0~NVIC_IPR59 共 60 个寄存器。
CMSIS 库提供了多个函数用于简化中断的控制,从函数的名字就可以容易的猜出它的含义:

void NVIC_EnableIRQ(IRQn_Type IRQn)


void NVIC_DisableIRQ(IRQn_Type IRQn)
void NVIC_SetPendingIRQ(IRQn_Type IRQn)
void NVIC_ClearPendingIRQ(IRQn_Type IRQn)
uint32_t NVIC_GetPendingIRQ(IRQn_Type IRQn)
void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority)
uint32_t NVIC_GetPriority(IRQn_Type IRQn)

关于 NVIC 的工作方式如下几点需要注意:

• 只有中断允许位有效,相应的中断才能触发
• 中断触发后相应的中断未处理标志就变为有效
• 处理器进入中断处理程序(ISR)后,相应的未处理标志自动清除
• 优先级值越小,相应的优先级越高, 高优先级中断将优先处理

2.2.2 系统计时器模块
系统计时器模块(System Timer,别称 SysTick)是一个 24 位的计数器,配置完成后会从一个预设值
开始随着系统计时器进行减计数,当计数到 0 的时候会触发 SysTick 中断,然后重新加载预设值,继续进
行计数。因此相当于系统的“心跳”,HAL_Delay 函数就依赖这个模块才能正常工作,因此几乎每个程序都
需要它。main.c 中的 HAL_Init 调用中对系统计时器模块进行初始化,每毫秒发生一次中断。
由于在启动代码中已经将所有的中断处理函数都列举出来,其中系统计时器模块的中断处理函数为
SysTick_Handler,因此只要在 stm32f4xx_it.c 将其实现就可以了。这部分代码比较简单,本质上就是
将一个全局变量 uwTick 在每次进入中断的时候进行“+1”的运算。

2.2.3 外部中断信号
STM32F469 使用 EXTI 模块控制外部的 23 个中断源,
开发板上的用户按键所连接的 PA0 管脚就连
接到了 EXTI0 上,从手册中的中断列表中可以看到它的中断编号是 6。EXTI 也有一系列的寄存器对其
进行控制:

• EXTI_IMR,中断屏蔽寄存器
• EXTI_EMR,事件屏蔽寄存器
• EXTI_RTSR,上升沿触发选择寄存器
• EXTI_FTSR,下降沿触发选择寄存器
2.3 实验步骤 9

• EXTI_SWIER,软件触发中断/事件寄存器
• EXTI_PR,中断未处理标志寄存器

需要注意的是 EXTI 模块的中断未处理标志不是自动清除的,


必须由用户代码来清除。另外这里对中
断和事件(Interrupt/Event)进行了区分,只有中断才会触发中断处理程序,而事件可以用来唤醒休眠的系
统,不能触发程序执行。ARM 处理器有 WFI 和 WFE 两个使处理器进入低功耗模式的指令,简单来说前
者通过中断来唤醒,后者通过事件来唤醒,具体的细节还要参考所使用的处理器手册。

2.2.4 低功耗模式
为了避免无故浪费电能,大部分处理器都支持低功耗的运行模式。降低系统运行功耗的方法有很多
种,比较简单有下面两个方法:

1. 关闭空闲外设的时钟
2. 降低系统时钟

时钟的配置由 RCC 模块完成,


HAL 提供了一些列函数来允许和禁止外设的时钟信号,
例如 __HAL_RCC
_GPIOA_CLK_DISABLE 函数可以用来关闭 GPIOA 的时钟。
改变系统的时钟频率可以参考 SystemClock_Config
函数,同样也是对 RCC 模块进行配置,这里不再详细说明。
ARM 的 Cortex-M 支持两种低功耗模式,都可以通过 WFE 或者 WFI 汇编指令进入,具体进
入哪种模式根据系统控制寄存器的相应位来决定。这两种模式是 SLEEP 模式和 DEEPSLEEP 模式,
STM32F469 处理在 DEEPSLEEP 的基础上实现了 STOP 模式和 STANDBY 模式。
SLEEP 模式下处理器核心的时钟被停止,但各个外设仍然保持原样,从此模式恢复的速度比较快。
STOP 模式下大部分的时钟都被停止,外部和内部的高速晶振都被禁止,内部 SRAM 和寄存器的内
容会被保持,从此模式恢复后使用的是内部 RC 晶振,因此要重新配置时钟。STOP 模式下所有的 I/O 依
然保持进入低功耗之前的状态,系统耗电量在毫安量级。
STANDBY 模式下只有 RTC 部分依然供电,
SRAM 和其他寄存器的内容都会丢失,
从此状态恢复时
几乎全部寄存器都会复位,
类似于系统重新启动。STANDBY 模式下除了个别管脚,
I/O 都处于高阻状态,
因此也最省电,系统耗电量在微安量级。
STM32F469 的 PWR_CR 和 PWR_CSR 寄存器对处理器的低功耗状态还有更加细致的设定,
比如电压控
制、
电源状态等,具体细节请参考数据手册。
进入三种不同低功耗模式可以用 HAL 提供的函数:

HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI);
HAL_PWR_EnterSTOPMode(PWR_MAINREGULATOR_ON)
HAL_PWR_EnterSTANDBYMode()

PWR_MAINREGULATOR_ON 表示进入低功耗模式时使用主稳压器,
另外一个可选的值是 PWR_LOWPOWERREGULATOR_ON
代表使用低功耗稳压器。PWR_SLEEPENTRY_WFI 表示使用 WFI 指令进入低功耗模式,另一个可选值是
PWR_SLEEPENTRY_WFE。

2.3 实验步骤
2.3.1 利用系统计时器计时
阅读代码中系统计时器相关代码,掌握 uwTick 变量的含义和 HAL_Dealy 函数的实现原理,编写代码
使开发板上三个 LED 采用不同的频率闪烁(分别为 3Hz,
5Hz 和 7Hz 为例)

提示:这个过程有点类似操作系统对四个进程进行调度,而这四个进程的唤醒频率各个不同。编写合
适的算法保证每个进程都能正确调度。
10 实验二 中断系统与低功耗模式

2.3.2 通过用户按键实现中断功能
在前一个程序的基础上使用蓝色按键控制第四个 LED 的开关状态,使用中断服务程序的方法实现。
允许中断的代码如下:

NVIC_EnableIRQ(EXTI0_IRQn);

中断服务程序编写在 stm32f4xx_it.c 中,内容如下:

void EXTI0_IRQHandler(void) # 中断服务程序名称


{
EXTI->PR = GPIO_PIN_0; # 清除中断标志
HAL_GPIO_TogglePin(LED4_GPIO_Port, LED4_Pin);
}

2.3.3 测量 STOP 模式的工作电流


编写程序使得处理器在工作 5 秒中后进入低功耗模式,
通过蓝色按钮可以将处理器唤醒,
再等待 5 秒
钟继续进入低功耗模式。测量两种状态下的工作电流,用一个 LED 灯指示当前的运行模式:灯亮表示正
常模式,灯灭表示低功耗模式。
记录两种状态下的电流,比较 SLEEP 模式和 STOP 模式的功耗差别。
在 main.c 中找到下面一行代码,将其注释掉,否则程序工作会不正常。

HAL_GPIO_Init(LCD_INT_GPIO_Port, &GPIO_InitStruct);

2.4 思考题
1. 系统计时器的中断频率对系统有什么影响?
2. 按键抖动是什么现象?应该如何避免?
3. 请解释注释掉那行代码的原因。
实验三 串口通信

3.1 实验目的
1. 掌握通过查询和中断方式处理串口数据的方法。
2. 掌握通过串口交互的基本手段。

3.2 实验原理
3.2.1 串行通信的异步和同步传送方式
CPU 与外设的基本通信方式可分为并行通信和串行通信两类。并行通信是指要传输的数据字各二进
制位同时并行传送的通信方式,而串行通信是指数据逐位顺序串行传送的通信方式。
在并行通信中,由于有与字宽对应的多根传输线并行传送数据,因此收发电路简单,与传统的总线类
似。但当数据远程距离增加时,传输线路的开销和数据位的同步就成为突出问题。而串行通信只需一对传
输线,并且可以利用电话线等现有通信信道作为传输介质,因而可以大大降低传输线路的成本,在实际应
用中更为常见。目前由于串行通信的时钟不断提高,串行通信的传送速度明显高于并行通信。
串行通信分为异步传送和同步传送两类。异步通信在发送字符时,
发送端可以在任意时刻开始发送字
符,因此必须在每一个字符的开始和结束的地方加上标志,即加上开始位和停止位,以便使接收端能够正
确地将每一个字符接收下来。单片机串行通信就是一直异步通信方式。
异步传送的特点是:

1. 数据以字符方式随机且断续地在线路上传送(但在同一字符的内部的传送是连续的)
。各字符的传送
依发送方的需要可连续,也可间断。
2. 通信双方用各自的时钟源来控制发送和接收。
3. 通信双方按异步通信协议传输字符。

异步通信格式如图 3.1所示,每个字符由起始位、数据位、奇偶校验位和停止位四个部分顺序组成。这
四个部分组成异步传输中的一个传输单元,即字符帧。
空 起 数 校 停 起 数 校 停 空
闲 始 据 验 止 始 据 验 止 闲
位 位 位 位 位 位 位 位 位 位
{

1 1 0 D0 D1 D2 D3 D4 D5 D6 D7 C 1 0 D0 D1 D2 D3 D4 D5 D6 D7 C 1 1 1
第N个字符帧 第N+1个字符帧

图 3.1: 异步通信字符帧格式

起始位为“0”信号,占 1 位。起始位的作用有两个:

1. 表示一个新字符帧的开始。即线路上不传送字符时,应保持为“1”。接收端检测线路状态连续为“1”
后或在停止位后有一个“0”,就知道将发来一个新的字符帧。

11
12 实验三 串口通信

2. 用以同步接收端的时钟,以保证后续的接收能正确进行。

数据位紧接于起始位后面,它可以占 5、6、7 或 8 位不等,数据的位数依最佳传送速率来确定。如所传


数据为 ASCII 码字符,则常取 7 位。数据位传输的顺序,总是最低位(LSB)D0 在先。
奇偶校验位在数据位之后占 1 位,
用来检验信息传送否有错。它的状态常由发送端的奇偶校验电路确
定。奇偶位的值取决于校验类型,若为偶校验,则数据位和校验位中逻辑“1”的个数必须是偶数;若为奇校
验,则数据位和校验位中逻辑“1”的个数必须是奇数。也可以规定不用奇偶校验位,或用其它的校验方法
来检验信息传送过程是否有错。
停止位用“1”来表征一个字符帧的结束。停止位可以占 1 位、1.5 位或 2 位不等。接收端收到停止位
时,表明这一字符已接收完毕,也表明下一个字符帧可能到来。若停止位以后不是紧接着传送下一个字符
帧,则让线路上保持为“1”,即空闲等待状态。图 3.1既表示一个字符紧接一个字符传送的情况,又表示两
个字符间有空闲位的情况。
串行通信的一个重要指标是波特率。它定义为每秒钟传送二进制数码的位数,以“位/秒”
(bps)为单
位。在异步通信中,
波待率 =(每个字符帧的位数)×(每秒传送的字符数)
常用的波特率有 600、1200、
2400、4800、 19200(bps)等。
9600、
由于异步通信双方各用自己的时钟源,若时钟频率等于波特率,则频率稍有偏差就会产生接收错误。
时钟频率应比波特率高,
时钟频率与波特率的比一般选 16:1 或者 64:1。采用较高频率的时钟,
在一位数据
内就有 16 或 64 个时钟,就可以保证捕捉正确的信号。
因此,在异步通信中,收发双方必须事先约定两件事:一是规定字符帧格式,即规定字符各部分所占的
位数,是否采用校验,以及校验的方式等;二是规定所采用的波特率以及时钟频率和波特率间的比例关系。
异步传送由于不传送同步时钟脉冲,所以设备比较简单,实现起来方便,它还可根据需要连续地或有间隙
地传送数据,对各字符间的间隙长度没有限制。缺点是在数据字符串中要加上起同步作用的起始位和停止
位,降低了有效数据位的传送速率,仅适合于低速通信的场合。

3.2.2 使用 STM32CubeMx 设置串口


STM32F469 最多支持 8 个串口,其中支持同步传输的叫做 USART(Universal Synchronous Asyn-

chronous Receiver Transmitter)仅支持异步传输的称为 UART(Universal Asynchronous Receiver Trans-
。开发板上的 USART3 通过 ST-Link 的 USB 接口作为虚拟串口被引出,这个串口的基本配置为:
mitter)
115200,8N1,即波特率为 115200,数据位 8 位,没有奇偶校验,1 个停止位,没有流控。
在新建工程的时候,使用 STM32CubeMx 选择 USART3,配置选项如图 3.2 所示。
图 3.2 中的 Over Sampling 由 USART_CR1 中的 OVER8 位来控制,当设置为“1”时表示采用 16 倍过
采样,否则表示 8 倍过采样。为了提高系统的抗干扰能力,一般采用 16 倍过采样,也就是用波特率的 16
倍时钟对接收数据进行采样,使用中间的三个采样点来作为电平高低的判断,如图 3.3 所示。

3.2.3 串口相关寄存器
串口数据寄存器为 DR,它由两个寄存器组成:在读取的时候代表的是接收数据寄存器(RDR),在写
入的时候代表的是发送数据寄存器(TDR)
。这个寄存器只有低 8 位有效,写入这个寄存器的低 8 位数据
将通过发送信号线(TxD)发出,而从接收信号线(RxD)上收到的有效数据将保存在这个寄存器中。
串口状态寄存器为 SR,
它的结构如图 3.4 所示:
其中 TXE 代表发送数据寄存器空,当写入到 TDR 中的数据被传递到移位寄存器(准备发出)时,
TXE 位被硬件置“1”,当有数据被写入 DR 时 TXE 位置“0”。
RXNE 位代表接收数据准备好,当完整的数据被写入 RDR 时,RXNE 位被硬件置“1”,当数据通过
DR 读出后,RXNE 位置“0”。
3.2 实验原理 13

图 3.2: USART3 的配置

图 3.3: 对数据位采用 16 倍过采样

图 3.4: 串口状态寄存器结构

ORE 代表接收缓冲区溢出,当新的数据已经准备好,但 RXNE 位为“1”的时候由硬件置“1”,对 SR


和 DR 的连续读出可以把 ORE 置“0”。当发生接收缓冲区溢出时,RDR 中的数据不会被覆盖,但新接到
的数据会被后续数据覆盖。
14 实验三 串口通信

串口的控制寄存器有 3 个,分别是 CR1、CR2 和 CR3,它们对串口的各种工作模式进行控制,具体的


细节请参考手册。CR1 的寄存器结构如图 3.5 所示。其中部分控制位含义如下:

• OVER8,控制过采样倍数, “0”代表 16 倍,
“1”代表 8 倍
• UE,USART 允许位
• W,“0”代表 8 位字长,
“1”代表 9 位字长
• PCE,奇偶校验允许位
• PS,奇偶校验选择, “0”代表偶校验, “1”代表奇校验
• TXEIE,发送中断允许位
• RXNEIE,接收中断允许位
• TE,发送允许位
• RE,接收允许位

图 3.5: 串口控制寄存器 1 结构

串口的波特率寄存器为 BRR,其结构如图 3.6 所示:

图 3.6: 串口波特率寄存器结构

波特率的计算公式为:
fCK
T x/Rx baud =
8 × (2 − OVER8) × USARTDIV

其中 fCK 为串口模块的时钟频率,
USARDIV 通过 BRR 寄存器中的尾数(Mantissa)和分数(Fraction)
来决定。下面的公式以 OVER8 = 0 为例:

USARDIV = M antissa + F raction/16

更一般的情况是计算出 USARDIV,然后把小数部分乘以 16,得到的最接近整数作为 Fraction(如果这


个数是 16 就需要进位到 Mantissa 部分),而整数部分保存在 Mantissa 中。

3.3 实验步骤
3.3.1 查询方式串口通信
建立新的工程,使用 STM32CubeMx 设置好时钟和 USART3,不要设置中断。在主程序中采用查询
方式不断将接收到的数据再发送出去。参考代码如下:
3.4 思考题 15

while ((USART3->SR & USART_SR_RXNE) == 0); // 等待接收完成


buf = USART3->DR; // 读入接收数据
USART3->DR = buf; // 发送数据
while ((USART3->SR & USART_SR_TXE) == 0); // 等待发送完成

在 PC 机上通过串口调试工具发送字符,应该可以看到相应的字符“回显”在接收区,试着用 HEX 模
式发送 0x0D 和 0x0A 等特殊字符,观察显示结果(文本显示模式)

3.3.2 采用中断方式接收
按照如下要求完成程序:

• 主程序为 LED2 按照固定的频率闪烁,采用 HAL_Dealy 延时


• 采用中断的方式从串口接收数据, 接收的内容为一个 1000 以内的十进制数
• 用这个十进制数作为 LED2 改变状态的等待时间
• 不要使用 STM32CubeMx 设置串口的中断模式,自己编写相关代码

允许串口中断和接收中断的代码如下:

NVIC_EnableIRQ(USART3_IRQn);
SET_BIT(USART3->CR1, USART_CR1_RXNEIE);

其他需要提示的部分有:

• 串口中断的函数原型为: void USART3_IRQHandler(void)


• 可以用 int atoi(char *buf) 函数将字符串转换为整数,字符串以“
0”结尾

3.3.3 实现串口数据交互
简单的串口操作可以使用 HAL_UART_Transmit 和 HAL_UART_Receive 来完成,
阅读 stm32f4xx_hal_uart.c
代码,了解这两个函数的用法,实现下面的功能。
串口终端设备很多都支持一种称为 AT 指令的串口命令,通过给设备传递 AT 指令达到对设备的控
制。例如给设备发送“AT\r\n”,设备回复“OK\r\n”表示设备正常;发送“ATE1\r\n”表示设置串口回显,
”ATE0\r\n“表示设置串口不回显。设备在命令正常执行的时候通过串口回复“OK\r\n”或者其他信息,错
误的时候回复”ERR\r\n”之类信息。
完善上一个程序,使得操作界面更加友好,用 AT 指令控制 LED 的闪烁频率。支持前面提到的“AT”
命令、
“ATE”命令和下面的“ATD”命令:
“ATD100”代表延时 100 毫秒。

3.4 思考题
1. 请画出串口输出 0x81 时 TxD 信号的完整波形。
2. 请计算当采用 16 倍过采样时,串口通信可以允许多大的波特率误差。
实验四 LCD 显示与 DMA 传输

4.1 实验目的
1. 掌握 STM32 BSP 代码的使用方法
2. 了解 SDRAM 的基本知识
3. 了解显示屏的基本参数和配置
4. 掌握 DMA 的基本概念和使用

4.2 实验原理
4.2.1 BSP 驱动代码的使用
在 ST 公司提供的驱动包中 Driver\BSP 目录下包含了一系列开发板的板级支持包。
其中 STM32469I-
Discovery 目录下包含本书使用开发板的驱动,主要包括如下几个文件:

• stm32469i_discovery_audio.c 音频输出驱动
• stm32469i_discovery_eeprom.c EEPROM 存储器驱动
• stm32469i_discovery_lcd.c 显示屏驱动
• stm32469i_discovery_qspi.c QSPI 接口驱动
• stm32469i_discovery_sd.c SD 卡接口驱动
• stm32469i_discovery_sdram.c 板载 SDRAM 驱动
• stm32469i_discovery_ts.c 触摸屏驱动
• stm32469i_discovery.c 板载其它设备驱动,如 LED、按钮等

如果要使用这些驱动只要把这些源文件加入自己的工程即可,例如需要驱动 LCD 时,可以直接用文


件浏览器把显示屏驱动源代码拖入工程目录中。由于这些驱动往往还依赖于其它的驱动代码,
所以还需要
把相应的代码也加入到工程中来,
对于 LCD 的例子,
最终的工程结构如图 4.1 所示。其中 ota8009a.c 文
件来自 BSP 目录下的 Components 目录,是 TFT 驱动芯片的驱动文件。
由于对工程的结构进行了修改,还需要对工程的属性进行修改,否则编译器会找不到新加的头文件位
置。如图 4.2 所示加入 Driver\BSP 目录。

4.2.2 LCD 显示基本原理


微处理器系统的显示模块主要包含显示控制器和显示面板两个部分。显示面板的主要参数有两个,

是显示分辨率,例如 320 × 240 的分辨率表示显示面板可以显示 240 行、每行 320 个点;另外一个是显示
深度,表示每个点所能表示的颜色个数,比如 24 位深度表示的是可以显示 22 4 种颜色,每个颜色可以用 8
位红色分量、8 位绿色分量和 8 位蓝色分量来表示。这样一个颜色需要用 3 个字节来表示,320 × 240 的
分辨率就需要用 320 × 240 × 3 = 230400 个字节的存储空间来表示。这样的显示空间一般被称为帧缓存
(Framebuffer),
显示控制器的基本功能就是把帧缓存中的内容显示到显示面板上。

16
4.2 实验原理 17

图 4.1: LCD 工程中包含的文件

图 4.2: 工程属性中头文件位置配置

显示面板的常见接口有三种,最简单的是显示总线接口(DBI,Display Bus Interface),这种接口最大


的特点就是显示面板自带帧缓存,存储处理器端发过来的数据,并由内部的控制芯片将数据显示到面板上,
处理器只要发送一次数据即可,这帧数据会一直显示在面板上。显示总线接口对处理器的要求很低,可以
通过简单的串行接口或者 GPIO 相连。
另外一种接口是显示像素接口(DPI, ,
Display Pixel Interface)也称 RGB 接口,
显示面板没有帧缓存,
它的每一帧数据都需要处理器驱动显示。处理器采用并行方式在一个时钟(像素时钟)内传输一个像素的
数据,并且为保证数据的同步,引入 VSYNC HSYNC 等信号:

• Vsync:帧同步信号
• vbp: 帧同步信号的后肩,单位为行
18 实验四 LCD 显示与 DMA 传输

• vfp: 帧同步信号的前肩,单位为行
• Hsync:行同步信号
• hbp:行同步信号的后肩, 单位为像素
• hfp: 行同步信号的前肩,单位为像素
• Dotclk:点时钟或称像素时钟
• DE: 数据有效信号

这些数据的设置要同显示面板的要求相对应,一般可以从面板的数据手册中获得。
由于显示面板对带宽的要求越来越高,新的显示串行接口(DSI,Display Serial Interface)采用差分信
号传输,单通道最高可以达到 1Gbps 的速率。通过上层协议的设计使得硬件接口得到统一,便于连接不同
的显示面板。图 4.3 是 STM32 的 DSI 框图。

图 4.3: DSI 设备架构

其中 LTDC 是 STM32 的 DPI 接口模块,


它同时为 DSI 模块提供了输入信号,
因此在使用 DSI 的同
时也要对 LTDC 进行初始化。

4.2.3 DMA 的基本原理


DMA(Direct Memory Access)技术在微处理器系统中非常常用,主要用来对连续的内存块进行读写。
当设置了读写地址和读写模式之后,
内存操作完全由 DMA 控制器来完成,
减轻了 CPU 的负担,
提高了系
统整体的运行效率。DMA 操作除了可以在内存之间拷贝数据,还可以在内存和寄存器间复制数据,例如
串口通信的过程也可以通过 DMA 来实现。DMA 技术的基本运行模式为:

1. 设置 DMA 控制器的源地址、目的地址和工作模式
2. 启动 DMA 传输
3. 在 DMA 完成的中断处理程序中完成数据的后续处理(也可以通过查询方式等待 DMA 传输结束)

在 STM32 系统中有一个专门用来进行图形处理的 DMA 控制器,


被称为 DMA2D(又被称作 Chrom-
。它除了可以进行内存之间的复制之外,还可以针对图形应用实现特殊的操作:
ART Accelerator)

• 对目标区域进行颜色填充
• 将(部分)源图像复制到目标图像区域
• 在复制图像的同时可以对颜色的描述格式进行转换
• 可以将两幅图像混合后再进行复制
4.3 实验步骤 19

4.3 实验步骤
4.3.1 直接写入帧缓存
按照图 4.1 中所示文件建立工程,在 main 函数中增加如下代码以驱动 LCD:

BSP_SDRAM_Init();
BSP_LCD_InitEx(LCD_ORIENTATION_PORTRAIT);
BSP_LCD_LayerDefaultInit(0, LAYER0_ADDRESS);
BSP_LCD_SelectLayer(0);
BSP_LCD_DisplayOn();

STM32469I-Discovery 使用的液晶屏是型号为 OTM8009A 的 TFT 面板,分辨率为 480 × 800,颜色模式


为 24 位深度加上 8 位的 Alpha 通道,ARGB 排列,所需要的缓冲区空间大于内部 RAM 空间,必须使用
板载的 SDRAM,
因此程序要先对 SDRAM 进行初始化。在初始化 LCD 之后,
还要设置缓冲区的地址,

里设置为 SDRAM 的起始地址:
0xC000 0000。STM32 的 LTDC 支持两个图层(Layer)
,在显示的时候可
以将两个图层进行混合,上面代码仅使用了一层。
编写代码在帧缓冲中画一条红色的直线,起始坐标 (100,100),终点坐标 (400,400)。注意在绘制直线
前需要对整个屏幕区域做一次清屏操作,即用单色进行填充,同时还要设置 Alpha 通道为 0xff,使得颜色
并不透明。

4.3.2 利用 DMA 功能进行颜色填充


阅读 stm32469i_discovery_lcd 中的 BSP_LCD_FillRect 函数,了解用 DMA2D 实现屏幕填充的
方法。

hdma2d_eval.Init.Mode = DMA2D_R2M; // 寄存器到内存模式


hdma2d_eval.Init.ColorMode = DMA2D_ARGB8888; // 输出颜色格式
hdma2d_eval.Init.OutputOffset = OffLine; // 每传输一行需要增加的偏移

hdma2d_eval.Instance = DMA2D; // 使用 DMA2D

if(HAL_DMA2D_Init(&hdma2d_eval) == HAL_OK) // 初始化 DMA 参数


{
// 配置 DMA2D 的前景或者背景
if(HAL_DMA2D_ConfigLayer(&hdma2d_eval, LayerIndex) == HAL_OK)
{
// 启动 DMA 传输,ColorIndex 为填充颜色
if (HAL_DMA2D_Start(&hdma2d_eval, ColorIndex, (uint32_t)pDst, xSize, ySize) == HAL_OK)
{
// 等待 DMA 传输结束
HAL_DMA2D_PollForTransfer(&hdma2d_eval, 10);
}
}
}

编写程序利用 HAL_GetTick 函数计算使用循环和 DMA 方式进行全屏填充所需要的时间,并把结果


显示在屏幕上,理解 DMA 方式的优点。显示字符串的时候可以使用 BSP 提供的函数,例如:
20 实验四 LCD 显示与 DMA 传输

BSP_LCD_SetTextColor(LCD_COLOR_WHITE);
sprintf(str, "loop:%d", loopdelay);
BSP_LCD_DisplayStringAt(10, 10, (uint8_t*)str, LEFT_MODE);

4.3.3 选择合适的实际更新缓冲区
编写一个程序,每隔 1 秒钟改变一下屏幕的颜色(用单色填充),观察屏幕的显示效果,你会发现屏
幕有一些“闪烁”。实际上屏幕会有一帧的显示颜色是上下两部分不同的。这是由于软件更新帧缓冲和
LTDC 更新屏幕不完全同步造成的。这种现象叫做割裂现象(Tearing Effect),在图形编程的时候要注意
避免。避免割裂的方式是在合适的时机更新缓冲区,
与 LTDC 更新获得同步。
LTDC 可以设置在扫描屏幕的某一行时产生中断信号,通过中断来同步缓冲区的更新。HAL 驱动提
供了 HAL_LTDC_ProgramLineEvent 函数,用来设置产生中断的扫描行。同时在 stm32f4xx_it.c 中增加
HAL 提供的缺省中断处理函数:

void LTDC_IRQHandler(void)
{
HAL_LTDC_IRQHandler(&hltdc_eval);
}

然后在 main.c 中实现如下回调函数,就可以通过一个全局变量获得同步。

void HAL_LTDC_LineEventCallback(LTDC_HandleTypeDef *hltdc)


{
line_event = 1;
}

主程序中的示例代码如下:

while (1)
{
line_event = 0; // 初始化事件标志
HAL_LTDC_ProgramLineEvent(&hltdc_eval, 533); // 设置扫描行为 533
while (line_event == 0); // 等待扫描中断
// 更新缓冲区内容
......

在屏幕上绘制一个小方块(50 × 50),不断更新其显示位置来获得动画的效果,
移动方式为按照直线运
动,
如果碰到屏幕边缘就会反弹。通过同步技术避免屏幕闪烁。
由于绘制复杂一些的图形需要的时间会相对较长,为了不让绘制一半的图像被意外显示出来,常见的
解决方案是采用双缓冲技术:一个缓冲区用来显示,另外一个缓冲区用来绘制,当绘制完成后只要通过设
置缓冲区起始地址,切换一下两个缓冲区就可以了。

4.4 思考题
1. STM32 的字节序是什么模式?以位于 (0,0) 的像素为例,在 ARGB 颜色模式下,0 地址、1 地址、2
地址、
3 地址分别保存了颜色的哪个部分。
4.4 思考题 21

2. 简单解释 DMA 传输的速度为什么比用循环快好多。


3. 为什么把 LTDC 的扫描行中断设置为 533?设置成 0 可以吗?
实验五 模数和数模转换

5.1 实验目的
1. 了解 STM32 数模和模数转换电路的原理和使用方法
2. 掌握 STM32 中定时器和计数器的简单使用方法
3. 熟悉示波器的使用

5.2 实验原理
5.2.1 数模转换电路
数字测控系统中,最终输出的控制信号往往是模拟信号,而实现把数字量转换为模拟量的设备称为数
模转换器(DAC, 。在 STM32F469 芯片中包含两个 12 位精度的 DAC,
Digital-to-Analog Converter) 可以
同时独立工作,支持多种触发模式。当数据被写入保持寄存器(DAC_DHR)时,DAC 在软件触发或者外部事
件的触发下开始进行数模转换,转换形成的电压将驱动在相应的管脚上。
对于 32 位系统来说,
DAC 有多种方式保存 12 位的数字 5.1,
对于不同的方式也有不同的寄存器与其
对应。

图 5.1: 单通道模式下的数据对齐方式

• 8 位右对齐使用 DAC_DHR8R 寄存器


• 12 位左对齐使用 DAC_DHR12L 寄存器
• 12 位右对齐使用 DAC_DHR12R 寄存器

写入到 DHR 寄存器中的数据会传递到数据输出寄存器(DOR),最终管脚的输出电压为:


DOR
DACoutput = VREF ×
4096
从开始数模转换到模拟电压获得稳定输出的时间是 DAC 的重要指标,
实际情况还会受到电源电压和负载
的影响,对于 STM32F469 来说这个转换时间的典型值为 3µS。
DAC 的控制寄存器为 DAC_CR,它的结构如图 5.2 所示:
其中 EN 位用来控制 DAC 的工作,设置为“0”的时候表示 DAC 被禁止,设置为“1”的时候 DAC 正
常工作。TSEL 用来控制 DAC 的触发源,它可以是软件触发、外部信号触发或者是内部计数器(6 种可以

22
5.2 实验原理 23

图 5.2: DAC 控制寄存器

选择),它只能在 EN 位设置之前改变。BOFF 位用来控制 DAC 的输出驱动器,当设置为“0”的时候驱动


器工作,可以免去外部另接的运放驱动电路。
WAVE 位可以控制 DAC 输出三角波或者噪音信号:
“01”表示输出噪音,噪音的幅度由 MAMP 位控
制;
“1x”表示输出三角波,幅度也由 MAMP 位控制。输出三角波的时候可以通过数据输出寄存器叠加一
个固定值,形成偏移的效果。
DAC_SWTRIGR 寄存器是 DAC 的软件触发源,最低两位控制位分别控制两个 DAC 的转换开始。

5.2.2 模数转换器
与数模转换的过程相反,在数字测控系统中,经常要把检测到的连续变化的模拟信号,如温度、压力、
速度等转换为离散的数字量,才能输入系统进行处理。实现模拟量到数字量转换的设备就是模数转换器
(ADC)
STM32F469 支持三个独立的 ADC,
最高支持 12 位的分辨率,
可以根据不同的应用要求,
配置成不同
的工作模式,
这里只介绍最基本的使用方法,更多的细节还要参考手册。
ADC 的控制寄存器 ADC_CR2 中的 ADON 位控制着它的电源,只有将其设置为“1”才可以使用
ADC。每个 ADC 控制器最多可以连接 19 个模拟端口,
这些端口通过模拟开关接入到 ADC 的输入端,

中 ADC1 的第 18 号端口(最后一个)端口连接了内部温度传感器的输出,可以测量处理器的温度。
19 个模拟端口的输入信号可以分为可重复的两组,分别是普通组(regular group)和注入组(injected
group),每个组都可以按任意顺序排列一些输入依次进行转换,分别由 ADC_SQR 和 ADC_JSQR 两组寄存器
来控制。图 5.3 是普通组的寄存器格式。

图 5.3: ADC_SQR1 寄存器格式

其中 L[3:0] 控制了普通组中输入信号序列的个数,可以设置为 1 到 16。SQ16[4:0] 设置了第 16


个转换信号的输入序号(从 0 到 18 选择)。同样还有另外 15 个输入信号的设置,SQ12 到 SQ1 分布在
ADC_SQR2 和 ADC_SQR3 中,而 ADC_JSQR 只有一个寄存器,最多设置四组转换序列。
每次 ADC 的采样时间由 ADC_SMPR 设置,可以分别为每个通道设置最小 3 个时钟周期,最多 480 个
时钟周期的采样时间。而对于 12 位精度转换来说,还需要 12 个时钟周期的转换时间,因此每次采样的最
快转换时间为 15 个时钟周期(降低采样精度还可以提高)

ADC 的转换可以通过软件写入 ACD_CR2 的 SWSTART 位触发,也可以由外部信号或者计数器信号来
触发。转换结束后数据被保存在 ADC_DR 寄存器中,同时 EOC 标志被设置有效(需要软件清除)

24 实验五 模数和数模转换

5.2.3 计数器的基本使用
在前面的章节中介绍过系统计时器模块,
除了可以用它计时外,
STM32F469 还支持 15 个功能不同的
计数器/计时器模块(timer)
。首先来看一下比较简单的 TIM6 和 TIM7,它们除了可以作为简单的 16 位
计数器之外,
还可以专门用来触发 DAC 的转换动作。
计数器的当前值保存在 TIMx_CNT 寄存器中,
当它在工作的时候会从 0 计数到 TIMx_ARR 寄存器中保
存的数值,
然后计数值重新变为 0 并触发一次溢出信号。计数器的计时时钟由模块时钟通过 TIMx_PSC 设
置的分频系数获得。
计数器其他工作模式有控制寄存器来控制。例如 TIMx_CR1 中的 CEN 用来启动计数器,
OPM 用来设置
计数器工作在单次模式(设置为“1”)还是连续模式。其它与中断和 DMA 相关的设置请参考手册。

图 5.4: 计数器 TIMx_CR1 寄存器

TIM2 到 TIM5 的四个计数器被称为通用计数器,功能比基本计数器(TIM6 和 TIM7)要丰富,除了


支持基本计数器的功能外,还支持一些特殊应用场合的模式,比如:

1. 输入捕获模式:通过配置输入信号的捕获模式(上升沿或者下降沿),当采样到相应信号后,当前的计
数值会保存在 TIMx_CCRx 寄存器中。
2. 比较输出模式:
当计数值与 TIMx_CCRx 相同时,
通过配置的管脚输出触发信号,
同时可以配置产生中
断或者 DMA 请求。
3. PWM 输出模式:使用 TIMx_ARR 寄存器配置输出频率,TIMx_CCRx 配置输出占空比。
4. 主从模式:
计数器还可以在内部进行级联,
主计数器的输出可以作为从计数器的时钟或者复位、
启动、
暂停信号。

计数器的计数方式还可以通过控制寄存器进行修改:

• DIR 位控制计数器为加计数(设置为“0”)还是减计数。
• CMS 位控制计数器为中间对齐模式,即先加计数再进行减计数。

5.3 实验步骤
5.3.1 用 DAC 实现波形发生器
配置 DAC 通道 1,
使用 TIM6 作为触发,
输出幅度最大为 1024 的三角波。设置 APB1 Timer 的时钟
为 48MHz,
DAC 与 TIM6 的设置如图 5.5 和图 5.6 所示。
在主程序中增加如下代码,启动 DAC 工作。

HAL_TIM_Base_Start(&htim6); // 启动计时器
HAL_DAC_Start(&hdac, DAC_CHANNEL_1); // 启动 DAC 通道 1
// 设置 DAC 数字格式,增加偏移量
HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, 0x100);

DAC 的通道 1 通过 PA4 管脚输出,在开发板上为 CN8 的 A5 管脚。使用示波器观察 DAC 输出的


信号,将其频率与计算的值进行比较。
5.3 实验步骤 25

图 5.5: DAC 在软件中的设置

图 5.6: TIM6 在软件中的设置

5.3.2 摇杆传感器的数据读取
摇杆传感器(图 5.7)通过游戏机上常见的摇杆,识别两个方向的输入,它有五个管脚:

GND 地线
+5V 电源线,实验中连接 3.3V 电压
VRX X 方向模拟量输出,对应 X 方向摇杆偏移
VRY Y 方向模拟量输出,对应 Y 方向摇杆偏移
SW 按键,按下摇杆输出高电平, 抬起输出低电平
26 实验五 模数和数模转换

图 5.7: 摇杆传感器

实际上,摇杆分别控制两个可变电阻器,VRX 的输出就是 X 方向可变电阻器的输出。实验中,摇杆传


感器的 VRX 连接 A0 端口,VRY 连接 A1 端口,分别对应 ADC1 的 IN9 和 IN12。新建工程时选择初始
化 ADC1 和 USART3,在代码中采用如下方式读取转换结果:

ADC_ChannelConfTypeDef sConfig = {0};


sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES;
sConfig.Channel = ADC_CHANNEL_9; # IN9
HAL_ADC_ConfigChannel(&hadc1, &sConfig); # 配置通道
HAL_ADC_Start(&hadc1); # 启动转换
HAL_ADC_PollForConversion(&hadc1, 100); # 等待转换结束
int value = HAL_ADC_GetValue(&hadc1); # 读取转换结果

编写代码,实现在串口中每隔 1 秒钟打印一次 IN9 和 IN12 的转换结果。

5.4 思考题
1. 摇杆传感器的 +5V 端子为什么不能接 +5V?
实验六 I2C 总线

6.1 实验目的
1. 了解 I2 C 总线的基本原理
2. 掌握使用 STM32F469 作为主设备进行 I2 C 通信的方法
3. 了解触摸屏的基本原理

6.2 实验原理
6.2.1 I2 C 总线的基本原理
I2 C 总线标准是一种具有多主设备、多从设备支持的串行总线。I2 C 标准最初由 Philips 公司提出,
主要面向一些低速的接口设备,如模数/数模转换芯片和各类传感器芯片等。另外一个串行协议 SMBus
(System Management Bus),电器标准与 I2 C 兼容,多数情况下可以认为是兼容协议。STM32469I 的 I2 C
设备即可以作为总线上的主设备也可以作为从设备,本实验内容仅介绍主设备的使用方式。
I2 C 的总线规定了两条信号,其中 SDA 为双向的数据信号,SCL 为双向的时钟信号。这两个信号都
被设计成漏极开路,因此可以同其他 I2 C 设备进行线与。典型的 I2 C 总线拓扑结构如图 6.1 所示。

VDD
SDA Rp
I2C SCL
主设备
从设备 从设备 从设备

图 6.1: I2 C 总线的连接

I2 C 设备在进行数据传输的时候,数据信号只在时钟信号为低时变化,在时钟上升沿进行采样。而如
果在时钟信号为高的时候数据信号发生变化,则代表两种特殊的控制信号:起始位和停止位。其中数据信
号由高变低代表起始位,表示一次数据传输的开始;数据信号由低变高代表停止位,表示一次数据传输的
结束。图 6.2 为 I2 C 总线的时序图。

SDA

SCL
Start Stop

图 6.2: I2 C 总线的时序

I2 C 数据传输由主设备产生一个起始位开始,然后传递 7 个地址位(对于 7 位地址模式),指定通信的


从设备。接下来传递的一位是读写位,0 表示写入,1 表示读出。再接下来的一位由从设备驱动,如果从设

27
28 实验六 I2 C 总线

备的地址与主设备发出的地址相同,从设备将把 SDA 信号拉低,表示确认这次数据传输(ACK)


。当从设
备地址被确认,真正的数据传输就开始了。如果是写入数据,
则数据线仍由主设备驱动,
从设备在每个字节
传输之后也要驱动一位的 ACK 信号。如果是读出数据,则数据线由从设备驱动,主设备仅在应答时输出
ACK 位。最后由主设备产生一个停止位表示数据传输结束。所有的 I2 C 数据与地址,都是先传递最高位,
最后传递最低位。

6.2.2 FT6206 电容触摸屏控制器


STM32F469I-Discovery 开发板使用了 FT6206 芯片来控制集成的电容触摸屏。FT6206 的控制接口
为 I C 总线,其地址为 0x54。FT6206 可以提供自动校准,对环境和电容变化不敏感,可以通过 I2 C 接口
2

直接提供 X 和 Y 的绝对坐标。图 6.3 列出了部分寄存器的结构。

图 6.3: FT6206 部分寄存器结构

其中 P1_XH 的高 2 位([7:6])是事件标志,00 代表按下,01 代表抬起。低 4 位是 X 坐标的第 [11:8]


位。P1_XL 是 X 坐标的第 [7:0] 位。
P1_YH 的高 4 位是触摸事件的 ID,如果为 0F 则表示无效,其低 4 位是 Y 坐标的第 [11:8] 位,加上
P1_YL 组成 12 位的 Y 坐标。

6.2.3 I2 C 接口的基本用法
对于工作于主模式下的 STM32 来说,主要的操作有发送和接收两个函数来实现。
HAL_I2C_Master_Transmit 函数用来发送数据,它有五个参数:I2 C 设备实例的句柄;设备地址;数据
指针;数据长度;超时时间。例如下面的代码向 0x54 地址发送一个字节的数据。其中的 0x54 是 FT6206
的地址。

HAL_I2C_Master_Transmit(&hi2c1, 0x54, buf, 1, 100);

HAL_I2C_Master_Receive 函数用来接收数据,
它也同样有五个参数,
参数顺序一样,
只是这里用指针
来接收数据。例如:

HAL_I2C_Master_Receive(&hi2c1, 0x54, buf, 1, 1000);

对于 FT6206 的寄存器操作,一般都是先发送一个字节的寄存器地址,然后再读入这个寄存器保存的
内容。例如 FT6206 的 0xA8 寄存器保存了设备的 ID,内容应该是 0x11,可以通过下面的代码进行读取:

uint8_t buf[10]; # 数据缓冲区数组


buf[0] = 0xA8; # 寄存器地址
6.3 实验步骤 29

if ((HAL_I2C_Master_Transmit(&hi2c1, 0x54, buf, 1, 100) == HAL_OK)


&& (HAL_I2C_Master_Receive(&hi2c1, 0x54, buf, 1, 1000) == HAL_OK)
&& (buf[0] == 0x11))
HAL_UART_Transmit(&huart3, (uint8_t *)"OK\r\n", 4, 300);

由于这种寄存器操作十分普遍,因此还有一种更简便的读取方法:

HAL_I2C_Mem_Read(&hi2c1, 0x54, 0xA8, 1, buf, 1, 1000);

它不但代码写起来简单,在硬件时序上也利用了 I2 C 的 “重启” 技术,节省了执行的时间。

6.3 实验步骤
6.3.1 I2 C 功能测试
在建立工程的时候选择初始化 I2C1 设备和 USART3 设备,
主程序中读取 FT6206 的设备 ID(0xA8
地址),并将读取结果输出在串口上。

6.3.2 触摸屏功能测试
当发生触摸事件时(按下或者抬起)
,触摸屏设备可以通过中断方式通知用户程序。这个中断信号通过
PJ5 管脚引入处理器,初始化代码已经设置这个管脚为上升沿触发中断,因此只要在增加相关的中断处理
程序即可。
使用下面代码允许中断:

NVIC_EnableIRQ(EXTI9_5_IRQn);

在中断程序设置触摸屏事件标志:

void EXTI9_5_IRQHandler(void)
{
EXTI->PR = GPIO_PIN_5; # 清除中断标志
touch_flag = 1; # 设置程序标志
}

在主程序中编写代码,将当前触摸屏接触的坐标通过串口打印出来。

6.3.3 触摸屏与 LCD 的功能联调


结合 LCD 的相关代码,编写简单画图程序,
允许用户使用手指在触摸屏上作画。

6.4 思考题
1. 如果两个设备的 I2 C 地址相同,如何将其应用到一个系统中。
实验七 USB 总线设备

7.1 实验目的
1. 掌握 USB 总线的相关知识
2. 了解 STM32F469I 芯片对 USB 相关协议的支持
3. 使用开发板实现 USB 简单设备

7.2 实验原理
7.2.1 USB 协议的基本概念
虽然当前最新的 USB 协议已经到了 4.0 版本,但大多数的设备并不需要太高的速度,STM32F469I
支持的协议版本也只有 2.0,因此下面的介绍也主要以 2.0 版本为主。
USB 是一种 “主–从” 式总线,一个主设备可以连接多个从设备,所有 USB 传输都由主机启动,外设
响应传输。而 STM32F469I 所支持的是一种称为 OTG(On The Go)的协议,它即可以配置成主设备,也
可以配置成从设备,增加了灵活性,可以适用于更多的场景。
USB 2.0 标准规定了三种传输速率:低速(LS,Low Speed)模式传输速率为 1.5Mbps ,多用于键盘和
鼠标;全速(FS,Full Speed)模式传输速率为 12Mbps;高速(HS,
High Speed)模式传输速率为 480Mbps。
USB 协议规定了四种传输类型:控制传输(用来控制 USB 的协议流程,如配置地址,传递状态等)、批
量传输(用于数据量比较大的传输,例如存储设备的数据传递)、同步传输(用于实时性要求比较高的传输,
例如音视频数据)、
中断传输(用于数据量小,实时性要求高的场合,例如键盘和鼠标的数据传输)

USB 的设备类别分成不同的类别(Class),常用的类有 HID(Human Interface Device,用户输入设
备)、音频设备、CDC(Commnucation Device Class,通信设备,如串口等)、MSC(Mass Storage Class,大
容量存储设备)、
DFU(Device Firmware Upgrade,固件代码升级)等。

7.2.2 USB 设备识别过程


USB 设备在接入主机之后,会进行多次通信以确保设备被正确配置,这个过程可以简单的概括如下:

1. 主机识别 USB 设备接入,并通过电气特性判断速度类型


2. 复位 USB 设备,此时该设备地址为 0
3. 获取 USB 设备描述表,了解设备基本信息
4. 设定设备地址, 获取设备的配置信息和接口信息
5. 加载设备驱动或者直接对设备进行初始化等操作

这个过程大部分由硬件来完成,但设备的描述信息是可以定制的。
USB 设备描述符(Device Descriptor)是最重要的信息,主要包含如下内容:

• bDeviceClass, 设备的类型
• idVender, idProduct,设备 ID,用来唯一识别设备

30
7.2 实验原理 31

图 7.1: 设备信息结构

设备支持的配置数目
• bNumConfigurations,

USB 配置描述符(Configuration Descriptor)描述了设备接口数目,


需要电流等信息,
每个设备一次只
能有一个配置被激活。
USB 接口(Interface)可以看作是设备的功能模块,它包含若干端点(Endpoint)完成数据的传输,实
现特定的功能。接口描述符包含对接口基本信息的描述。
USB 端点可以看作数据传输的起点和终点,每个端点都有独立的编址,其中端点 0 所有设备都必须
支持,用来传输控制数据。例如设备在配置初期的传输就通过这个端点来完成。

7.2.3 STM32Cube USB 设备库


USB 协议本身十分复杂,
ST 公司提供的中间件(Middleware)实现了其中大部分的通用功能。使用管
脚配置工具,设置 USB OTG FS 设备工作在设备模式下(图 7.2)
。同时设置中间件的 USB DEVICE 模
块使用 HID 类别的设备(图 7.3)
。然后还需要设置时钟模块使用 HSE,并保证 USB 设备的 48MHz 时钟
(PLL48CLK)

图 7.2: USB 硬件设置

此时保存工程可以生成最基本的 HID USB 设备代码框架。工程中增加了两个目录,


其中 Middlewares
目录中包含了 USB 协议的底层实现,一般不需要修改:
32 实验七 USB 总线设备

图 7.3: USB 固件设置

• usbd_core.c 文件包含了 USB 协议的核心功能


• usbd_ctlreq.c 和 usbd_ioreq.c 文件实现了控制和端口请求操作
• usbd_hid.c 实现了 HID 设备的基本框架

USB_DEVICE 目录中包含了 USB 模块需要实现的具体功能,一般也不需要修改:

• usb_device.c USB 设备的初始化功能接口


• usb_desc.c USB 设备的描述信息
• usb_conf.c USB 设备底层接口

7.2.4 HID 设备概述


HID 设备是相对比较简单的设备,如键盘、鼠标等。它们通过 HID 报告描述符描述设备的数据格式,
接下来以鼠标为例,简要介绍该描述符的数据格式。
常用的鼠标需要发送的数据有按键和光标移动等信息,其中按键可以用一个 bit 来表示。整个数据格
式可以如表 7.1 来表示。

表 7.1: 鼠标数据格式
Bit7 Bit6 Bit5 Bit4 Bit3 Bit2 Bit1 Bit 0
Byte0 右键 中键 左键
Byte1 X 方向移动数据
Byte2 Y 方向移动数据
Byte3 滚轮移动数据
Byte4 唤醒信息

具体到报告描述符的构造,鼠标的三个按键的数据可以表示为

USAGE_PAGE (Button) // 描述按键


USAGE_MINIMUM (Button 1) // 最小为 1
USAGE_MAXIMUM (Button 3) // 最大为 3
LOGICAL_MINIMUM (0) // 数值最小为 0
LOGICAL_MAXIMUM (1) // 最大为 1,
即布尔类型
7.3 实验步骤 33

REPORT_COUNT (3) // 一共三个按键


REPORT_SIZE (1) // 每个大小为 1 比特
INPUT (Data,Var,Abs) // 发送数据到主机
REPORT_COUNT (1) // 还有 1 部分数据是填充位
REPORT_SIZE (5) // 大小是 5 比特
INPUT (Cnst,Var,Abs) // 发送数据到主机

对于 X 方向移动数据,也可以类似构造

USAGE_PAGE (Generic Desktop) // 描述鼠标数据


USAGE (X) // X 方向
LOGICAL_MINIMUM (-127) // 最小值-127
LOGICAL_MAXIMUM (127) // 最大值 127
REPORT_SIZE (8) // 8 比特
REPORT_COUNT (1) // 1 个字节
INPUT (Data,Var,Rel) // 发送数据

这些数据最后都会作为二进制值保存在处理器的 ROM 中,一般不需要改变。

7.3 实验步骤
7.3.1 实现鼠标按键功能
按照实验原理部分建立支持 USB 设备的工程,
找到 usbd_hid.c 文件,
查看其中的 USBD_HID_CfgHSDesc
结构体,通过注释了解设备配置信息,确认其中的 nInterfaceProtocol 设置为了鼠标功能。
对于 HID 设备,
还需要个关注它的报告描述符。同样在这个文件中 HID_MOUSE_ReportDesc 就是
这个描述符的具体内容。通过 http://eleccelerator.com/usbdescreqparser/ 上的工具可以对其进行解析,
后面的注释就是每一项的含义。这个描述符也有一些工具可以生成,不必手工填写。

0x05, 0x01, // Usage Page (Generic Desktop Ctrls)


0x09, 0x02, // Usage (Mouse)
0xA1, 0x01, // Collection (Application)
0x09, 0x01, // Usage (Pointer)
0xA1, 0x00, // Collection (Physical)
0x05, 0x09, // Usage Page (Button)
0x19, 0x01, // Usage Minimum (0x01)
0x29, 0x03, // Usage Maximum (0x03)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x95, 0x03, // Report Count (3)
0x75, 0x01, // Report Size (1)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x95, 0x01, // Report Count (1)
0x75, 0x05, // Report Size (5)
0x81, 0x01, // Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x30, // Usage (X)
0x09, 0x31, // Usage (Y)
34 实验七 USB 总线设备

0x09, 0x38, // Usage (Wheel)


0x15, 0x81, // Logical Minimum (-127)
0x25, 0x7F, // Logical Maximum (127)
0x75, 0x08, // Report Size (8)
0x95, 0x03, // Report Count (3)
0x81, 0x06, // Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position)
0xC0, // End Collection
0x09, 0x3C, // Usage (Motion Wakeup)
0x05, 0xFF, // Usage Page (Reserved 0xFF)
0x09, 0x01, // Usage (0x01)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x02, // Report Count (2)
0xB1, 0x22, // Feature (Data,Var,Abs,No Wrap,Linear,No Preferred State,No Null Position,Non-volatile)
0x75, 0x06, // Report Size (6)
0x95, 0x01, // Report Count (1)
0xB1, 0x01, // Feature (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0xC0, // End Collection

这里比较重要的信息是鼠标数据的结构,它基本与表 7.1 相同,包含 5 个字节。因此程序中需要发送


的数据长度也是 5 个字节:

#define CLICK_REPORT_SIZE 5
uint8_t click_report[CLICK_REPORT_SIZE] = {0};

由于我们还需要访问 USB 相关功能,要使用 USB 设备句柄,因此还要在 main.c 中引用:

extern USBD_HandleTypeDef hUsbDeviceFS;

下面的代码是发送按键的示例,对其进行完善,实现这个 USB 设备的按键功能。通过 USB 线连接开


发板和电脑,
测试鼠标按键功能。

if(HAL_GPIO_ReadPin(USER_BUTTON_GPIO_Port, USER_BUTTON_Pin) == GPIO_PIN_SET){


HAL_GPIO_WritePin(USER_LED_1_GPIO_Port, USER_LED_1_Pin, GPIO_PIN_SET);

click_report[0] = 1; // send button press


USBD_HID_SendReport(&hUsbDeviceFS, click_report, CLICK_REPORT_SIZE);
HAL_Delay(50); // Delay some time

click_report[0] = 0; // send button release


USBD_HID_SendReport(&hUsbDeviceFS, click_report, CLICK_REPORT_SIZE);
HAL_Delay(200);
HAL_GPIO_WritePin(USER_LED_1_GPIO_Port, USER_LED_1_Pin, GPIO_PIN_RESET);
}
7.4 思考题 35

7.3.2 完善鼠标功能
前面的代码仅仅实现了按键功能,接下来可以利用摇杆传感器,实现 X/Y 方向的数据生成,完善鼠标
功能。

7.4 思考题
1. 发送按键按下和按键抬起的最小间隔是多少?

You might also like